diff --git a/Makefile b/Makefile index 0a1f5fdb..6ed158af 100644 --- a/Makefile +++ b/Makefile @@ -341,6 +341,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_xmod_variant_match \ $(BIN)/test_spread_variant_match \ $(BIN)/test_xmod_ident_prefer \ + $(BIN)/test_xmod_valglobal_run \ $(BIN)/test_widen_pad_zero_run \ $(BIN)/test_named_ptr_alias_variant_widen \ $(BIN)/test_single_field_struct_zeroinit \ @@ -811,6 +812,15 @@ $(BIN)/test_xmod_ident_prefer: test/wcc/794_xmod_ident_prefer.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #1 (cgen residual of #55): cross-module bare value-global load must +# mangle reference-site by resolved module / definition-site by the +# decl's own module — runtime + cs==ww byte-id. Self-contained single- +# file multi-package probe (953 model). +$(BIN)/test_xmod_valglobal_run: test/wcc/795_xmod_valglobal_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #15: widening a bare *vtable into a NAMED-alias variant (`stream` = # *vtable) of `(file | stream)` must compute the right tag, not default # to tag 0. Both-stage byte-id + runtime, plus a degenerate-ambiguity diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index b7a30f7c..84439340 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1107,6 +1107,36 @@ mod_lookup_for_fn(const char *name, const char *hint) return first; } +/* Value-global variant: mangle ONLY on an exact (name, hint) match; + * otherwise return NULL so the name stays bare. Unlike the fn variant + * there is NO first-leaf-match fallback — exported value globals are + * export-skipped from mod_map (mod_collect keeps their bare-name data + * ABI, see the skip at `!isfn && d->export`), so a first-match fallback + * would mis-mangle an exported `v` onto another module's private `v` + * (#1 cgen value-global module-qualifier, the cgen residual of #55). + * Bare-on-miss is correct: a missing entry means the leaf is either an + * exported global (its own bare symbol) or not module-private at all. + * + * HONEST BOUNDARY (rule 7) — do NOT "fix" the following into a + * workaround: if two modules BOTH export the same value leaf, both stay + * bare and the linker sees a duplicate symbol. That is a CORRECT, loud, + * link-time ABI clash (identical to C's two-extern-same-name rule), NOT + * a silent miscompile. A bare reference can never legitimately resolve + * to another module's PRIVATE global, so first-match is never wanted on + * the value path; the only ambiguity left is genuine duplicate exports, + * which belong to the linker, not to a cgen disambiguation heuristic. */ +static const char * +mod_lookup_value(const char *name, const char *hint) +{ + if (hint == NULL) return NULL; + for (Mod *m = mod_map; m; m = m->next) { + if (strcmp(m->name, name) != 0) continue; + if (m->module != NULL && strcmp(m->module, hint) == 0) + return m->module; + } + return NULL; +} + /* Collect every top-level `let` whose declared type we can store * in a single .data slot. Names not in this map fall through to * the old "drop assignment" path; with a clear link-time @@ -1306,6 +1336,20 @@ mod_mangle_fn(Cg *c, const char *ident, const char *hint) return mod_join(c, mod, ident); } +/* Value-global flavoured mangle: same shape as mod_mangle_fn but over + * mod_lookup_value (exact-(ident,hint)-or-bare, no first-match + * fallback). See mod_lookup_value for why value globals can't share the + * fn fallback. */ +static const char * +mod_mangle_value(Cg *c, const char *ident, const char *hint) +{ + const char *resolved = ffi_resolve(ident); + if (resolved != ident) return resolved; + const char *mod = mod_lookup_value(ident, hint); + if (mod == NULL) return ident; + return mod_join(c, mod, ident); +} + /* Forward decl — masym below depends on asym defined further down. */ static Adr asym(const char *s); @@ -1324,6 +1368,23 @@ mafn(Cg *c, const char *ident, const char *hint) return asym(mod_mangle_fn(c, ident, hint)); } +/* Value-global address builder. masym's non-hinted mod_lookup picks + * the first leaf-name match, so two modules with a same-leaf value + * global (`let v` in both) collapse onto one DATA label and a bare + * cross-module read resolves to the wrong module (#1 cgen value-global + * module-qualifier, the cgen residual of #55). Thread a per-site hint + * the way mafn does — curmod at a bare reference, the decl's own module + * at the definition label — over the same module-generic decl map. + * Kept distinct from mafn (vs renamed) to leave the fn-mangle path + * byte-for-byte untouched. Routes through mod_mangle_value (exact-or- + * bare) so an exported global stays bare instead of mis-mangling onto + * another module's same-leaf private global. */ +static Adr +mahint(Cg *c, const char *ident, const char *hint) +{ + return asym(mod_mangle_value(c, ident, hint)); +} + void cg_init(Cg *c, Arena *a) { @@ -2529,7 +2590,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * third 8B (cap); the address holder CX gets * overwritten by the cap as the last step, after * we no longer need it (#1/Phase 3). */ - ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX)); + ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod), + areg(D_CX)); ins2(c, A_MOVQ, amem(D_CX, 0), areg(D_AX)); ins2(c, A_MOVQ, amem(D_CX, 8), areg(D_BX)); ins2(c, A_MOVQ, amem(D_CX, 16), areg(D_CX)); @@ -2546,7 +2608,8 @@ cgexpr(Cg *c, Node *n, Local *locals) * (#129 Phase A.1 LOAD-side twin of the * emit_floatlit_data DATA-side SSoT). */ int op = type_isf32(n->type) ? A_MOVSS : A_MOVSD; - ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX)); + ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod), + areg(D_CX)); ins2(c, op, amem(D_CX, 0), areg(D_X0)); goto ident_done; } @@ -2560,13 +2623,15 @@ cgexpr(Cg *c, Node *n, Local *locals) int gop = let_islet(n->str) ? localloadop(n->type) : A_MOVQ; if (gop == A_MOVQ) { - ins2(c, A_MOVQ, masym(c, n->str), areg(D_AX)); + ins2(c, A_MOVQ, mahint(c, n->str, c->cur_mod), + areg(D_AX)); } else { /* w6a has no MOVSXD/MOVSWQ/MOVSBQ D_EXTERN * source form, so route through a LEAQ scratch * the same way top-level str/slice/float lets * do. */ - ins2(c, A_LEAQ, masym(c, n->str), areg(D_CX)); + ins2(c, A_LEAQ, mahint(c, n->str, c->cur_mod), + areg(D_CX)); ins2(c, gop, amem(D_CX, 0), areg(D_AX)); } } @@ -9354,7 +9419,7 @@ emit_data_row_zero(FILE *out, const char *dir, const char *name, int sz) * shape doesn't reduce to a foldable float literal. */ static int emit_floatlit_data(FILE *out, Cg *c, const char *directive, - const char *name, Type *t, Node *rhs) + const char *name, const char *module, Type *t, Node *rhs) { int isf32 = type_isf32(t); int sz = isf32 ? 4 : 8; @@ -9380,7 +9445,8 @@ emit_floatlit_data(FILE *out, Cg *c, const char *directive, v = x.u; } } - fprintf(out, "%s %s(SB),\"", directive, mod_mangle(c, name)); + fprintf(out, "%s %s(SB),\"", directive, + mod_mangle_value(c, name, module)); /* IEEE-754 sign-bit XOR for negation happens INSIDE the emit * loop on the top byte only — semantically identical to a whole- * u64 XOR with 2^63 (or 2^31 for f32) but never materialises @@ -9541,11 +9607,12 @@ emit_struct_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, u64 base) * the type isn't a struct. */ static int emit_struct_data(FILE *out, Cg *c, const char *directive, - const char *name, Type *t, Node *rhs) + const char *name, const char *module, Type *t, Node *rhs) { Type *u = (t && t->kind == TY_NAMED) ? t->under : t; if (u == NULL || u->kind != TY_STRUCT) return 0; - fprintf(out, "%s %s(SB),\"", directive, mod_mangle(c, name)); + fprintf(out, "%s %s(SB),\"", directive, + mod_mangle_value(c, name, module)); emit_struct_lit_bytes(out, c, t, rhs, 0); fputs("\"\n", out); return 1; @@ -9784,12 +9851,13 @@ emit_array_lit_bytes(FILE *out, Cg *c, Type *t, Node *rhs, int emit_phase) * bytes (would corrupt the asm if rhs reduces partway through). */ static int emit_array_data(FILE *out, Cg *c, const char *directive, - const char *name, Type *t, Node *rhs) + const char *name, const char *module, Type *t, Node *rhs) { Type *u = type_unwrap(t); if (u == NULL || u->kind != TY_ARRAY) return 0; if (!emit_array_lit_bytes(out, c, t, rhs, 0)) return 0; - fprintf(out, "%s %s(SB),\"", directive, mod_mangle(c, name)); + fprintf(out, "%s %s(SB),\"", directive, + mod_mangle_value(c, name, module)); emit_array_lit_bytes(out, c, t, rhs, 1); fputs("\"\n", out); return 1; @@ -9805,7 +9873,7 @@ emit_lets(Cg *c, FILE *out, Node *file) if (sz == 0) continue; if (let_isfloat(d->type)) { (void)emit_floatlit_data(out, c, "DATAW", - d->str, d->type, d->rhs); + d->str, d->module, d->type, d->rhs); continue; } /* #129 A.2: gate `!let_isstruct` so an 8B struct lit @@ -9830,7 +9898,8 @@ emit_lets(Cg *c, FILE *out, Node *file) * round-trip via the sign-extended u64. */ if (!fold_int_literal(r, &v)) continue; } - emit_data_row(out, "DATAW", mod_mangle(c, d->str), v); + emit_data_row(out, "DATAW", + mod_mangle_value(c, d->str, d->module), v); continue; } /* Strip leading casts on the rhs so a `nil: str` etc. @@ -9848,7 +9917,7 @@ emit_lets(Cg *c, FILE *out, Node *file) if (sz == (int)ty_str->size && r != NULL && r->kind == N_STRLIT && r->strlen > 0) { const char *lab = intern_strlit(c, r->str, r->strlen); - const char *sym = mod_mangle(c, d->str); + const char *sym = mod_mangle_value(c, d->str, d->module); u64 v = r->strlen; /* 16-byte payload: 8 zero placeholder + LE len. */ fprintf(out, "DATAW %s(SB),\"", sym); @@ -9868,7 +9937,7 @@ emit_lets(Cg *c, FILE *out, Node *file) * through to zero-init (existing path below). */ if (r != NULL && r->kind == N_ARRLIT && let_isarray(d->type)) { if (emit_array_data(out, c, "DATAW", d->str, - d->type, r)) + d->module, d->type, r)) continue; /* fall through to zero-init */ } @@ -9885,14 +9954,15 @@ emit_lets(Cg *c, FILE *out, Node *file) * so the link surfaced an undefined ref. */ if (is_struct && r->kind == N_STRUCTLIT) { if (emit_struct_data(out, c, "DATAW", d->str, - d->type, r)) + d->module, d->type, r)) continue; } if (is_struct) continue; if (is_array) continue; if (r->kind != N_NIL && !empty_str) continue; } - emit_data_row_zero(out, "DATAW", mod_mangle(c, d->str), sz); + emit_data_row_zero(out, "DATAW", + mod_mangle_value(c, d->str, d->module), sz); } } @@ -9913,7 +9983,8 @@ emit_defs(Cg *c, FILE *out, Node *file) if (d->kind != N_DEF || d->rhs == NULL) continue; u64 v; if (fold_int_literal(d->rhs, &v)) { - fprintf(out, "DATA %s(SB),\"", mod_mangle(c, d->str)); + fprintf(out, "DATA %s(SB),\"", + mod_mangle_value(c, d->str, d->module)); for (int i = 0; i < 8; i++) { unsigned b = (unsigned)((v >> (i * 8)) & 0xff); if (b == '"' || b == '\\') @@ -9932,7 +10003,7 @@ emit_defs(Cg *c, FILE *out, Node *file) * at link. */ if (let_isfloat(d->type)) { (void)emit_floatlit_data(out, c, "DATA", - d->str, d->type, d->rhs); + d->str, d->module, d->type, d->rhs); continue; } /* #129 A.2: struct-typed def with N_STRUCTLIT rhs. Parallel @@ -9942,7 +10013,7 @@ emit_defs(Cg *c, FILE *out, Node *file) * for the LOAD path to find something. */ if (let_isstruct(d->type) && d->rhs->kind == N_STRUCTLIT) { (void)emit_struct_data(out, c, "DATA", - d->str, d->type, d->rhs); + d->str, d->module, d->type, d->rhs); continue; } /* #129 A.3: array-typed def with N_ARRLIT rhs. Parallel to @@ -9951,7 +10022,7 @@ emit_defs(Cg *c, FILE *out, Node *file) * LEAQ name(SB). */ if (let_isarray(d->type) && d->rhs->kind == N_ARRLIT) { (void)emit_array_data(out, c, "DATA", - d->str, d->type, d->rhs); + d->str, d->module, d->type, d->rhs); continue; } } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 377246c3..344de431 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -19353,7 +19353,7 @@ fn cgident(c: *cgen, n: *node) void = { let mov: str = "MOVSD"; if (isf32type(c, n)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); @@ -19361,7 +19361,7 @@ fn cgident(c: *cgen, n: *node) void = { return; }; emitline("\tMOVQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), AX\n"); return; }; @@ -19393,7 +19393,7 @@ fn cgident(c: *cgen, n: *node) void = { // is overwritten by the cap as the last step, after // ptr/len are already loaded (#1/Phase 3). emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\tMOVQ\t(CX), AX\n"); emitline("\tMOVQ\t8(CX), BX\n"); @@ -19412,7 +19412,7 @@ fn cgident(c: *cgen, n: *node) void = { let mov: str = "MOVSD"; if (isf32type(c, lv.tnode)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); @@ -19428,11 +19428,11 @@ fn cgident(c: *cgen, n: *node) void = { let glop: str = localloadop(c, lvtnode); if (streq(glop, "MOVQ")) { emitline("\tMOVQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(glop); @@ -29849,7 +29849,7 @@ export fn letpreintern(c: *cgen, file: *node) void = { // an IEEE-754 sign-bit XOR (bit 63 f64, bit 31 f32) to avoid pulling // f64/f32 bitcast helpers into cgen. Returns true on emit, false if // rhs doesn't reduce to a foldable float literal. -fn emitfloatlitdata(c: *cgen, directive: str, name: str, +fn emitfloatlitdata(c: *cgen, directive: str, name: str, module: str, sz: i32, rhs: *node) bool = { let isf32: bool = (sz == 4); let bits: u64 = 0u64; @@ -29898,7 +29898,7 @@ fn emitfloatlitdata(c: *cgen, directive: str, name: str, }; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); // IEEE-754 sign-bit XOR for negation happens INSIDE the emit // loop on the top byte only — equivalent to a whole-u64 XOR with @@ -30084,7 +30084,7 @@ fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node, // emitstructdata — top-level wrapper. Opens the DATA/DATAW directive // then delegates to emitstructlitbytes. Shared between emitletdataw // struct arm and emitdefconstants struct arm (#129 A.2). -fn emitstructdata(c: *cgen, directive: str, name: str, +fn emitstructdata(c: *cgen, directive: str, name: str, module: str, structt: *tinfo, rhs: *node) bool = { let su: *tinfo = structt; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; @@ -30092,7 +30092,7 @@ fn emitstructdata(c: *cgen, directive: str, name: str, if (su.kind != tykind.TY_STRUCT) { return false; }; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); emitstructlitbytes(c, structt, rhs, 0u64); emitline("\"\n"); @@ -30371,7 +30371,7 @@ fn emitarraylitbytes(c: *cgen, arrt: *tinfo, rhs: *node, // behavior (the old loop emitted zeros when `elems` was nil); the // refactor would have skipped emit entirely without this branch, // causing `undefined reference to strconv.f64tos_buf` at link. -fn emitarraydata(c: *cgen, directive: str, name: str, +fn emitarraydata(c: *cgen, directive: str, name: str, module: str, arrt: *tinfo, rhs: *node) bool = { let au: *tinfo = arrt; for (au != nil && au.kind == tykind.TY_NAMED) { au = au.under; }; @@ -30381,7 +30381,7 @@ fn emitarraydata(c: *cgen, directive: str, name: str, let total: u64 = arrt.size; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); let i: u64 = 0u64; for (i < total) { emitdatawbyte(0u8); i = i + 1u64; }; @@ -30391,7 +30391,7 @@ fn emitarraydata(c: *cgen, directive: str, name: str, if (!emitarraylitbytes(c, arrt, rhs, 0)) { return false; }; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); emitarraylitbytes(c, arrt, rhs, 1); emitline("\"\n"); @@ -30414,8 +30414,8 @@ fn emitletdataw(c: *cgen, file: *node) void = { // (#129 Phase A.1, rule-12). Bare-call // discards the bool return (mirrors // cgen.ww:723 fmt.fprintln pattern). - emitfloatlitdata(c, "DATAW", nm, fsz, - d.rhs); + emitfloatlitdata(c, "DATAW", nm, d.nmod, + fsz, d.rhs); }; // #129 A.2: struct-typed let with N_STRUCTLIT rhs // routes through the emitstructdata SSoT helper. @@ -30428,7 +30428,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (r.kind == nkind.N_STRUCTLIT) { let st: *tinfo = d.lhs.type_: *tinfo; emitstructdata(c, "DATAW", nm, - st, r); + d.nmod, st, r); }; }; }; @@ -30459,7 +30459,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; if (ok) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; let n: u64 = v; @@ -30493,7 +30493,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { let lab: str = internstrlit(c, r.str); let v: u64 = r.str.len: u64; emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; @@ -30506,7 +30506,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; emitline("\"\n"); emitline("DATAR "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("+0(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); @@ -30525,7 +30525,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; if (ok) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; let szstr: i32 = primtypesize("str"): i32; @@ -30556,7 +30556,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; if (ok) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; let szsl: i32 = tyslicesize(): i32; @@ -30574,7 +30574,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (issg) { if (d.rhs == nil) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; for (i < sz) { @@ -30607,7 +30607,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (route) { let at: *tinfo = d.lhs.type_: *tinfo; emitarraydata(c, "DATAW", nm, - at, rh); + d.nmod, at, rh); }; }; }; @@ -30651,7 +30651,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { }; if (dfsz > 0) { emitfloatlitdata(c, "DATA", d.str, - dfsz, d.rhs); + d.nmod, dfsz, d.rhs); } else { // #129 A.2: struct-typed def with N_STRUCTLIT // rhs. The checker stamps d.lhs.type_ with the @@ -30667,7 +30667,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { if (su != nil) { if (su.kind == tykind.TY_STRUCT) { emitstructdata(c, "DATA", - d.str, st, r); + d.str, d.nmod, st, r); }; }; };}; @@ -30682,7 +30682,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { if (au != nil) { if (au.kind == tykind.TY_ARRAY) { emitarraydata(c, "DATA", - d.str, at, r); + d.str, d.nmod, at, r); }; }; };}; @@ -30699,7 +30699,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { // that motivated the divergence is gone), so the asm // surface is unchanged on the corpus. emitline("DATA "); - emitsymname(c, d.str); + emitsymnamehint(c, d.str, d.nmod); emitline("(SB),\""); let i: i32 = 0; let n: u64 = v; @@ -31218,6 +31218,39 @@ fn modlookupforfn(c: *cgen, name: str, hint: str) str = { return first; }; +// modlookupvalue — value-global variant: mangle ONLY on an exact +// (name, hint) match; otherwise empty so the name stays bare. Unlike +// modlookupforfn there is NO first-leaf-match fallback — exported value +// globals are export-skipped from c.mods (modcollect keeps their bare- +// name data ABI), so a first-match fallback would mis-mangle an exported +// `v` onto another module's private `v` (#1 cgen value-global module- +// qualifier, the cgen residual of #55). Mirrors cstage mod_lookup_value. +// +// HONEST BOUNDARY (rule 7) — do NOT "fix" the following into a +// workaround: if two modules BOTH export the same value leaf, both stay +// bare and the linker sees a duplicate symbol. That is a CORRECT, loud, +// link-time ABI clash (like C's two-extern-same-name rule), NOT a silent +// miscompile. A bare reference can never legitimately resolve to another +// module's PRIVATE global, so first-match is never wanted on the value +// path; the only ambiguity left is genuine duplicate exports, which +// belong to the linker, not to a cgen disambiguation heuristic. +fn modlookupvalue(c: *cgen, name: str, hint: str) str = { + let empty: str; + empty.ptr = nil; + empty.len = 0; + if (hint.len == 0) { return empty; }; + let m: *modent = c.mods; + for (m != nil) { + if (streq(m.mname, name)) { + if (m.nmod.len > 0 && streq(m.nmod, hint)) { + return m.nmod; + }; + }; + m = m.mnext; + }; + return empty; +}; + // emitsymname — write the asm symbol name for `ident`. Honours, in // order: FFI mapping (@symbol), module mangling (private decls), bare // name. Use everywhere a top-level non-fn name is emitted before `(SB)` @@ -31258,6 +31291,32 @@ fn emitfnname(c: *cgen, ident: str, hint: str) void = { emitbytes( ident.ptr, ident.len: u64); }; +// emitsymnamehint — write the asm symbol name for a value-global +// `ident`, threading `hint` the way emitfnname does for fns. +// emitsymname's non-hinted modlookup grabs the first +// leaf-name match, so two modules with a same-leaf value global (`let v` +// in both) collapse onto one DATA label and a bare cross-module read +// resolves to the wrong module (#1 cgen value-global module-qualifier, +// the cgen residual of #55). Pass c.curmod at a bare reference, the +// decl's own module (d.nmod) at a definition label. Routes through +// modlookupvalue (exact-or-bare) so an exported global stays bare +// instead of mis-mangling onto another module's same-leaf private +// global; kept distinct from emitfnname to leave the fn-mangle path +// byte-for-byte untouched. +fn emitsymnamehint(c: *cgen, ident: str, hint: str) void = { + let resolved: str = ffiresolve(c, ident); + if (resolved.ptr != ident.ptr) { + emitbytes( resolved.ptr, resolved.len: u64); + return; + }; + let mod: str = modlookupvalue(c, ident, hint); + if (mod.len > 0) { + emitbytes( mod.ptr, mod.len: u64); + emitbytes( ".".ptr, 1u64); + }; + emitbytes( ident.ptr, ident.len: u64); +}; + // ---- FFI map --------------------------------------------------------- fn fficollect(c: *cgen, file: *node) void = { diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index d3eeca41..ac69a018 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1259,7 +1259,7 @@ export fn letpreintern(c: *cgen, file: *node) void = { // an IEEE-754 sign-bit XOR (bit 63 f64, bit 31 f32) to avoid pulling // f64/f32 bitcast helpers into cgen. Returns true on emit, false if // rhs doesn't reduce to a foldable float literal. -fn emitfloatlitdata(c: *cgen, directive: str, name: str, +fn emitfloatlitdata(c: *cgen, directive: str, name: str, module: str, sz: i32, rhs: *node) bool = { let isf32: bool = (sz == 4); let bits: u64 = 0u64; @@ -1308,7 +1308,7 @@ fn emitfloatlitdata(c: *cgen, directive: str, name: str, }; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); // IEEE-754 sign-bit XOR for negation happens INSIDE the emit // loop on the top byte only — equivalent to a whole-u64 XOR with @@ -1494,7 +1494,7 @@ fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node, // emitstructdata — top-level wrapper. Opens the DATA/DATAW directive // then delegates to emitstructlitbytes. Shared between emitletdataw // struct arm and emitdefconstants struct arm (#129 A.2). -fn emitstructdata(c: *cgen, directive: str, name: str, +fn emitstructdata(c: *cgen, directive: str, name: str, module: str, structt: *tinfo, rhs: *node) bool = { let su: *tinfo = structt; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; @@ -1502,7 +1502,7 @@ fn emitstructdata(c: *cgen, directive: str, name: str, if (su.kind != tykind.TY_STRUCT) { return false; }; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); emitstructlitbytes(c, structt, rhs, 0u64); emitline("\"\n"); @@ -1781,7 +1781,7 @@ fn emitarraylitbytes(c: *cgen, arrt: *tinfo, rhs: *node, // behavior (the old loop emitted zeros when `elems` was nil); the // refactor would have skipped emit entirely without this branch, // causing `undefined reference to strconv.f64tos_buf` at link. -fn emitarraydata(c: *cgen, directive: str, name: str, +fn emitarraydata(c: *cgen, directive: str, name: str, module: str, arrt: *tinfo, rhs: *node) bool = { let au: *tinfo = arrt; for (au != nil && au.kind == tykind.TY_NAMED) { au = au.under; }; @@ -1791,7 +1791,7 @@ fn emitarraydata(c: *cgen, directive: str, name: str, let total: u64 = arrt.size; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); let i: u64 = 0u64; for (i < total) { emitdatawbyte(0u8); i = i + 1u64; }; @@ -1801,7 +1801,7 @@ fn emitarraydata(c: *cgen, directive: str, name: str, if (!emitarraylitbytes(c, arrt, rhs, 0)) { return false; }; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); emitarraylitbytes(c, arrt, rhs, 1); emitline("\"\n"); @@ -1824,8 +1824,8 @@ fn emitletdataw(c: *cgen, file: *node) void = { // (#129 Phase A.1, rule-12). Bare-call // discards the bool return (mirrors // cgen.ww:723 fmt.fprintln pattern). - emitfloatlitdata(c, "DATAW", nm, fsz, - d.rhs); + emitfloatlitdata(c, "DATAW", nm, d.nmod, + fsz, d.rhs); }; // #129 A.2: struct-typed let with N_STRUCTLIT rhs // routes through the emitstructdata SSoT helper. @@ -1838,7 +1838,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (r.kind == nkind.N_STRUCTLIT) { let st: *tinfo = d.lhs.type_: *tinfo; emitstructdata(c, "DATAW", nm, - st, r); + d.nmod, st, r); }; }; }; @@ -1869,7 +1869,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; if (ok) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; let n: u64 = v; @@ -1903,7 +1903,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { let lab: str = internstrlit(c, r.str); let v: u64 = r.str.len: u64; emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; @@ -1916,7 +1916,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; emitline("\"\n"); emitline("DATAR "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("+0(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); @@ -1935,7 +1935,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; if (ok) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; let szstr: i32 = primtypesize("str"): i32; @@ -1966,7 +1966,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; if (ok) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; let szsl: i32 = tyslicesize(): i32; @@ -1984,7 +1984,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (issg) { if (d.rhs == nil) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; for (i < sz) { @@ -2017,7 +2017,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (route) { let at: *tinfo = d.lhs.type_: *tinfo; emitarraydata(c, "DATAW", nm, - at, rh); + d.nmod, at, rh); }; }; }; @@ -2061,7 +2061,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { }; if (dfsz > 0) { emitfloatlitdata(c, "DATA", d.str, - dfsz, d.rhs); + d.nmod, dfsz, d.rhs); } else { // #129 A.2: struct-typed def with N_STRUCTLIT // rhs. The checker stamps d.lhs.type_ with the @@ -2077,7 +2077,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { if (su != nil) { if (su.kind == tykind.TY_STRUCT) { emitstructdata(c, "DATA", - d.str, st, r); + d.str, d.nmod, st, r); }; }; };}; @@ -2092,7 +2092,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { if (au != nil) { if (au.kind == tykind.TY_ARRAY) { emitarraydata(c, "DATA", - d.str, at, r); + d.str, d.nmod, at, r); }; }; };}; @@ -2109,7 +2109,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { // that motivated the divergence is gone), so the asm // surface is unchanged on the corpus. emitline("DATA "); - emitsymname(c, d.str); + emitsymnamehint(c, d.str, d.nmod); emitline("(SB),\""); let i: i32 = 0; let n: u64 = v; @@ -2628,6 +2628,39 @@ fn modlookupforfn(c: *cgen, name: str, hint: str) str = { return first; }; +// modlookupvalue — value-global variant: mangle ONLY on an exact +// (name, hint) match; otherwise empty so the name stays bare. Unlike +// modlookupforfn there is NO first-leaf-match fallback — exported value +// globals are export-skipped from c.mods (modcollect keeps their bare- +// name data ABI), so a first-match fallback would mis-mangle an exported +// `v` onto another module's private `v` (#1 cgen value-global module- +// qualifier, the cgen residual of #55). Mirrors cstage mod_lookup_value. +// +// HONEST BOUNDARY (rule 7) — do NOT "fix" the following into a +// workaround: if two modules BOTH export the same value leaf, both stay +// bare and the linker sees a duplicate symbol. That is a CORRECT, loud, +// link-time ABI clash (like C's two-extern-same-name rule), NOT a silent +// miscompile. A bare reference can never legitimately resolve to another +// module's PRIVATE global, so first-match is never wanted on the value +// path; the only ambiguity left is genuine duplicate exports, which +// belong to the linker, not to a cgen disambiguation heuristic. +fn modlookupvalue(c: *cgen, name: str, hint: str) str = { + let empty: str; + empty.ptr = nil; + empty.len = 0; + if (hint.len == 0) { return empty; }; + let m: *modent = c.mods; + for (m != nil) { + if (streq(m.mname, name)) { + if (m.nmod.len > 0 && streq(m.nmod, hint)) { + return m.nmod; + }; + }; + m = m.mnext; + }; + return empty; +}; + // emitsymname — write the asm symbol name for `ident`. Honours, in // order: FFI mapping (@symbol), module mangling (private decls), bare // name. Use everywhere a top-level non-fn name is emitted before `(SB)` @@ -2668,6 +2701,32 @@ fn emitfnname(c: *cgen, ident: str, hint: str) void = { emitbytes( ident.ptr, ident.len: u64); }; +// emitsymnamehint — write the asm symbol name for a value-global +// `ident`, threading `hint` the way emitfnname does for fns. +// emitsymname's non-hinted modlookup grabs the first +// leaf-name match, so two modules with a same-leaf value global (`let v` +// in both) collapse onto one DATA label and a bare cross-module read +// resolves to the wrong module (#1 cgen value-global module-qualifier, +// the cgen residual of #55). Pass c.curmod at a bare reference, the +// decl's own module (d.nmod) at a definition label. Routes through +// modlookupvalue (exact-or-bare) so an exported global stays bare +// instead of mis-mangling onto another module's same-leaf private +// global; kept distinct from emitfnname to leave the fn-mangle path +// byte-for-byte untouched. +fn emitsymnamehint(c: *cgen, ident: str, hint: str) void = { + let resolved: str = ffiresolve(c, ident); + if (resolved.ptr != ident.ptr) { + emitbytes( resolved.ptr, resolved.len: u64); + return; + }; + let mod: str = modlookupvalue(c, ident, hint); + if (mod.len > 0) { + emitbytes( mod.ptr, mod.len: u64); + emitbytes( ".".ptr, 1u64); + }; + emitbytes( ident.ptr, ident.len: u64); +}; + // ---- FFI map --------------------------------------------------------- fn fficollect(c: *cgen, file: *node) void = { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index a03f176a..857ebf7b 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -740,7 +740,7 @@ fn cgident(c: *cgen, n: *node) void = { let mov: str = "MOVSD"; if (isf32type(c, n)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); @@ -748,7 +748,7 @@ fn cgident(c: *cgen, n: *node) void = { return; }; emitline("\tMOVQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), AX\n"); return; }; @@ -780,7 +780,7 @@ fn cgident(c: *cgen, n: *node) void = { // is overwritten by the cap as the last step, after // ptr/len are already loaded (#1/Phase 3). emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\tMOVQ\t(CX), AX\n"); emitline("\tMOVQ\t8(CX), BX\n"); @@ -799,7 +799,7 @@ fn cgident(c: *cgen, n: *node) void = { let mov: str = "MOVSD"; if (isf32type(c, lv.tnode)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); @@ -815,11 +815,11 @@ fn cgident(c: *cgen, n: *node) void = { let glop: str = localloadop(c, lvtnode); if (streq(glop, "MOVQ")) { emitline("\tMOVQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(glop); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3b14f7c8..cff38fc4 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -19353,7 +19353,7 @@ fn cgident(c: *cgen, n: *node) void = { let mov: str = "MOVSD"; if (isf32type(c, n)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); @@ -19361,7 +19361,7 @@ fn cgident(c: *cgen, n: *node) void = { return; }; emitline("\tMOVQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), AX\n"); return; }; @@ -19393,7 +19393,7 @@ fn cgident(c: *cgen, n: *node) void = { // is overwritten by the cap as the last step, after // ptr/len are already loaded (#1/Phase 3). emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\tMOVQ\t(CX), AX\n"); emitline("\tMOVQ\t8(CX), BX\n"); @@ -19412,7 +19412,7 @@ fn cgident(c: *cgen, n: *node) void = { let mov: str = "MOVSD"; if (isf32type(c, lv.tnode)) { mov = "MOVSS"; }; emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(mov); @@ -19428,11 +19428,11 @@ fn cgident(c: *cgen, n: *node) void = { let glop: str = localloadop(c, lvtnode); if (streq(glop, "MOVQ")) { emitline("\tMOVQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), AX\n"); } else { emitline("\tLEAQ\t"); - emitsymname(c, nm); + emitsymnamehint(c, nm, c.curmod); emitline("(SB), CX\n"); emitline("\t"); emitline(glop); @@ -29849,7 +29849,7 @@ export fn letpreintern(c: *cgen, file: *node) void = { // an IEEE-754 sign-bit XOR (bit 63 f64, bit 31 f32) to avoid pulling // f64/f32 bitcast helpers into cgen. Returns true on emit, false if // rhs doesn't reduce to a foldable float literal. -fn emitfloatlitdata(c: *cgen, directive: str, name: str, +fn emitfloatlitdata(c: *cgen, directive: str, name: str, module: str, sz: i32, rhs: *node) bool = { let isf32: bool = (sz == 4); let bits: u64 = 0u64; @@ -29898,7 +29898,7 @@ fn emitfloatlitdata(c: *cgen, directive: str, name: str, }; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); // IEEE-754 sign-bit XOR for negation happens INSIDE the emit // loop on the top byte only — equivalent to a whole-u64 XOR with @@ -30084,7 +30084,7 @@ fn emitstructlitbytes(c: *cgen, structt: *tinfo, rhs: *node, // emitstructdata — top-level wrapper. Opens the DATA/DATAW directive // then delegates to emitstructlitbytes. Shared between emitletdataw // struct arm and emitdefconstants struct arm (#129 A.2). -fn emitstructdata(c: *cgen, directive: str, name: str, +fn emitstructdata(c: *cgen, directive: str, name: str, module: str, structt: *tinfo, rhs: *node) bool = { let su: *tinfo = structt; for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; }; @@ -30092,7 +30092,7 @@ fn emitstructdata(c: *cgen, directive: str, name: str, if (su.kind != tykind.TY_STRUCT) { return false; }; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); emitstructlitbytes(c, structt, rhs, 0u64); emitline("\"\n"); @@ -30371,7 +30371,7 @@ fn emitarraylitbytes(c: *cgen, arrt: *tinfo, rhs: *node, // behavior (the old loop emitted zeros when `elems` was nil); the // refactor would have skipped emit entirely without this branch, // causing `undefined reference to strconv.f64tos_buf` at link. -fn emitarraydata(c: *cgen, directive: str, name: str, +fn emitarraydata(c: *cgen, directive: str, name: str, module: str, arrt: *tinfo, rhs: *node) bool = { let au: *tinfo = arrt; for (au != nil && au.kind == tykind.TY_NAMED) { au = au.under; }; @@ -30381,7 +30381,7 @@ fn emitarraydata(c: *cgen, directive: str, name: str, let total: u64 = arrt.size; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); let i: u64 = 0u64; for (i < total) { emitdatawbyte(0u8); i = i + 1u64; }; @@ -30391,7 +30391,7 @@ fn emitarraydata(c: *cgen, directive: str, name: str, if (!emitarraylitbytes(c, arrt, rhs, 0)) { return false; }; emitline(directive); emitline(" "); - emitsymname(c, name); + emitsymnamehint(c, name, module); emitline("(SB),\""); emitarraylitbytes(c, arrt, rhs, 1); emitline("\"\n"); @@ -30414,8 +30414,8 @@ fn emitletdataw(c: *cgen, file: *node) void = { // (#129 Phase A.1, rule-12). Bare-call // discards the bool return (mirrors // cgen.ww:723 fmt.fprintln pattern). - emitfloatlitdata(c, "DATAW", nm, fsz, - d.rhs); + emitfloatlitdata(c, "DATAW", nm, d.nmod, + fsz, d.rhs); }; // #129 A.2: struct-typed let with N_STRUCTLIT rhs // routes through the emitstructdata SSoT helper. @@ -30428,7 +30428,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (r.kind == nkind.N_STRUCTLIT) { let st: *tinfo = d.lhs.type_: *tinfo; emitstructdata(c, "DATAW", nm, - st, r); + d.nmod, st, r); }; }; }; @@ -30459,7 +30459,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; if (ok) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; let n: u64 = v; @@ -30493,7 +30493,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { let lab: str = internstrlit(c, r.str); let v: u64 = r.str.len: u64; emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; for (i < 8) { emitdatawbyte(0u8); i += 1; }; @@ -30506,7 +30506,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; emitline("\"\n"); emitline("DATAR "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("+0(SB),"); emitbytes( lab.ptr, lab.len: u64); emitline("(SB)\n"); @@ -30525,7 +30525,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; if (ok) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; let szstr: i32 = primtypesize("str"): i32; @@ -30556,7 +30556,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { }; if (ok) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; let szsl: i32 = tyslicesize(): i32; @@ -30574,7 +30574,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (issg) { if (d.rhs == nil) { emitline("DATAW "); - emitsymname(c, nm); + emitsymnamehint(c, nm, d.nmod); emitline("(SB),\""); let i: i32 = 0; for (i < sz) { @@ -30607,7 +30607,7 @@ fn emitletdataw(c: *cgen, file: *node) void = { if (route) { let at: *tinfo = d.lhs.type_: *tinfo; emitarraydata(c, "DATAW", nm, - at, rh); + d.nmod, at, rh); }; }; }; @@ -30651,7 +30651,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { }; if (dfsz > 0) { emitfloatlitdata(c, "DATA", d.str, - dfsz, d.rhs); + d.nmod, dfsz, d.rhs); } else { // #129 A.2: struct-typed def with N_STRUCTLIT // rhs. The checker stamps d.lhs.type_ with the @@ -30667,7 +30667,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { if (su != nil) { if (su.kind == tykind.TY_STRUCT) { emitstructdata(c, "DATA", - d.str, st, r); + d.str, d.nmod, st, r); }; }; };}; @@ -30682,7 +30682,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { if (au != nil) { if (au.kind == tykind.TY_ARRAY) { emitarraydata(c, "DATA", - d.str, at, r); + d.str, d.nmod, at, r); }; }; };}; @@ -30699,7 +30699,7 @@ fn emitdefconstants(c: *cgen, file: *node) void = { // that motivated the divergence is gone), so the asm // surface is unchanged on the corpus. emitline("DATA "); - emitsymname(c, d.str); + emitsymnamehint(c, d.str, d.nmod); emitline("(SB),\""); let i: i32 = 0; let n: u64 = v; @@ -31218,6 +31218,39 @@ fn modlookupforfn(c: *cgen, name: str, hint: str) str = { return first; }; +// modlookupvalue — value-global variant: mangle ONLY on an exact +// (name, hint) match; otherwise empty so the name stays bare. Unlike +// modlookupforfn there is NO first-leaf-match fallback — exported value +// globals are export-skipped from c.mods (modcollect keeps their bare- +// name data ABI), so a first-match fallback would mis-mangle an exported +// `v` onto another module's private `v` (#1 cgen value-global module- +// qualifier, the cgen residual of #55). Mirrors cstage mod_lookup_value. +// +// HONEST BOUNDARY (rule 7) — do NOT "fix" the following into a +// workaround: if two modules BOTH export the same value leaf, both stay +// bare and the linker sees a duplicate symbol. That is a CORRECT, loud, +// link-time ABI clash (like C's two-extern-same-name rule), NOT a silent +// miscompile. A bare reference can never legitimately resolve to another +// module's PRIVATE global, so first-match is never wanted on the value +// path; the only ambiguity left is genuine duplicate exports, which +// belong to the linker, not to a cgen disambiguation heuristic. +fn modlookupvalue(c: *cgen, name: str, hint: str) str = { + let empty: str; + empty.ptr = nil; + empty.len = 0; + if (hint.len == 0) { return empty; }; + let m: *modent = c.mods; + for (m != nil) { + if (streq(m.mname, name)) { + if (m.nmod.len > 0 && streq(m.nmod, hint)) { + return m.nmod; + }; + }; + m = m.mnext; + }; + return empty; +}; + // emitsymname — write the asm symbol name for `ident`. Honours, in // order: FFI mapping (@symbol), module mangling (private decls), bare // name. Use everywhere a top-level non-fn name is emitted before `(SB)` @@ -31258,6 +31291,32 @@ fn emitfnname(c: *cgen, ident: str, hint: str) void = { emitbytes( ident.ptr, ident.len: u64); }; +// emitsymnamehint — write the asm symbol name for a value-global +// `ident`, threading `hint` the way emitfnname does for fns. +// emitsymname's non-hinted modlookup grabs the first +// leaf-name match, so two modules with a same-leaf value global (`let v` +// in both) collapse onto one DATA label and a bare cross-module read +// resolves to the wrong module (#1 cgen value-global module-qualifier, +// the cgen residual of #55). Pass c.curmod at a bare reference, the +// decl's own module (d.nmod) at a definition label. Routes through +// modlookupvalue (exact-or-bare) so an exported global stays bare +// instead of mis-mangling onto another module's same-leaf private +// global; kept distinct from emitfnname to leave the fn-mangle path +// byte-for-byte untouched. +fn emitsymnamehint(c: *cgen, ident: str, hint: str) void = { + let resolved: str = ffiresolve(c, ident); + if (resolved.ptr != ident.ptr) { + emitbytes( resolved.ptr, resolved.len: u64); + return; + }; + let mod: str = modlookupvalue(c, ident, hint); + if (mod.len > 0) { + emitbytes( mod.ptr, mod.len: u64); + emitbytes( ".".ptr, 1u64); + }; + emitbytes( ident.ptr, ident.len: u64); +}; + // ---- FFI map --------------------------------------------------------- fn fficollect(c: *cgen, file: *node) void = { diff --git a/test/wcc/795_xmod_valglobal_run.c b/test/wcc/795_xmod_valglobal_run.c new file mode 100644 index 00000000..2035c5a4 --- /dev/null +++ b/test/wcc/795_xmod_valglobal_run.c @@ -0,0 +1,216 @@ +/* + * 795_xmod_valglobal_run — project #1 close, the cgen residual of #55. + * Runtime + cs==ww byte-id net for the cross-module bare value-GLOBAL + * load miscompile. Sibling of the CHECKER test 794_xmod_ident_prefer, + * which deliberately omitted a byte-id assertion because THIS cgen bug + * would diverge the asm independently (see 794's NOTE). With #1 fixed, + * the byte-id now holds and is asserted here. + * + * THE BUG (cgen-only, both stages emit IDENTICAL wrong asm → byte-id + * was BLIND to it): a bare cross-module value-global load mangled its + * symbol via a NON-preferring leaf lookup (cstage masym->mod_mangle-> + * mod_lookup first-match; wwstage emitsymname->modlookup). With + * package aa; export let v: i32 = 7; fn getv() = { return v; } + * package main; import aa; let v: i32 = 99; main = { return aa.getv(); } + * the bare `v` inside aa.getv resolved to main.v, and aa.v's DATA slot + * was ALSO labeled main.v (collision) — so aa.getv() returned 99, not 7. + * Functions were already correct (they thread a cur_mod hint via mafn/ + * emitfnname); value globals did not. + * + * THE FIX (#1): reference-site mangle uses the resolved module (curmod- + * prefer for bare idents); definition-site mangle uses the decl's OWN + * module — threaded per-site the way fns already do (cstage mahint with + * c->cur_mod / d->module; wwstage emitsymnamehint with c.curmod / + * d.nmod). The value lookup mangles ONLY on an exact (name, module) + * match and otherwise leaves the name BARE — no first-leaf fallback — + * because exported value globals are export-skipped from the module map + * (they keep their bare-name data ABI), and a first-match fallback would + * mis-mangle an exported `v` onto another module's private `v`. So the + * exported aa.v stays `v`, the private main.v stays `main.v`, no + * collision. + * + * EACH ROW CARRIES BOTH DIMENSIONS (953_f64crossmod_run model): + * (a) cstage `ww build` (in a /tmp scratch dir) + run, asserting the + * exit code — pins that the converged asm is runtime-correct. + * (b) w6c vs w6c_ww `.s` cmp — FAILS if the stages diverge. Necessary + * because the bug was byte-id-blind (both stages were wrong the + * same way); a green byte-id alone never caught it, so the runtime + * row (a) is the real net and (b) guards rule-10 going forward. + * + * Single-file multi-package form (like 953): `package aa; ... package + * main; import aa; ...` in one source, so w6c/w6c_ww see the cross- + * module reference without -I path plumbing, and the build is self- + * contained (no selfhost traversal → no .combined.ww fixture race, so + * this lives in the parallel phase, not the 990-997 skip-set). + * + * GATE POLARITY: must stay GREEN. A wrong exit means the value-global + * mangle regressed (collision returned); a byte-id FAIL means the stages + * diverged on the mangle (rule-10 violation). + * + * SCOPE NOTE: str/slice/array value-globals are intentionally NOT rowed + * here — `len(str-global)` and indexed-global reads trip a SEPARATE, + * still-open cs!=ww divergence in the load body (the N_DOT-base / index + * sibling tracked as #229), orthogonal to this symbol-mangle fix. + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { const char *label; const char *src; int want_exit; }; + +static const struct row rows[] = { + /* canonical #1: exported aa.v (7) vs private main.v (99); aa.getv() + * must read aa's v, not collide onto main.v. Pre-fix: 99. */ + { "i32_let_export_vs_private", + "package aa;\n" + "export let v: i32 = 7;\n" + "export fn getv() i32 = { return v; };\n" + "package main;\n" + "import aa;\n" + "let v: i32 = 99;\n" + "export fn main() i32 = { return aa.getv(); };\n", 7 }, + /* def-constant flavour: exported def vs private def, same leaf. */ + { "def_export_vs_private", + "package aa;\n" + "export def K: i32 = 5;\n" + "export fn getk() i32 = { return K; };\n" + "package main;\n" + "import aa;\n" + "def K: i32 = 88;\n" + "export fn main() i32 = { return aa.getk(); };\n", 5 }, + /* float flavour: exercises the LEAQ+MOVSD load arm and the + * emit_floatlit_data DATA label. aa.getf() == 2.5 -> i32 2. */ + { "f64_let_export_vs_private", + "package aa;\n" + "export let f: f64 = 2.5;\n" + "export fn getf() f64 = { return f; };\n" + "package main;\n" + "import aa;\n" + "let f: f64 = 9.9;\n" + "export fn main() i32 = { return aa.getf(): i32; };\n", 2 }, + { NULL, NULL, 0 } +}; + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[1100], w6c_ww[1100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + if (access(w6c_ww, X_OK) != 0) { + fprintf(stderr, "xmod_valglobal: w6c_ww missing — cannot run the " + "cs==ww byte-id gate\n"); + return 1; + } + + int n = 0, fail = 0; + for (int i = 0; rows[i].src; i++, n++) { + char src[64]; + snprintf(src, sizeof src, "/tmp/wwxmv_%d_%d.ww", getpid(), i); + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + /* (a) cstage build + run in a scratch dir so intermediates and + * the output binary land there. */ + char tmpdir[64]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/wwxmv_%d_d_%d", getpid(), i); + mkdir(tmpdir, 0755); + + char cmd[2048]; + snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s", + tmpdir, bin, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: cstage build failed\n", + rows[i].label); + fail++; + unlink(src); rmdir(tmpdir); + continue; + } + + char outbin[128]; + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + + int got = runwait(outbin); + if (got != rows[i].want_exit) { + fprintf(stderr, "row[%s]: cstage exit %d, want %d\n", + rows[i].label, got, rows[i].want_exit); + fail++; + } + unlink(outbin); rmdir(tmpdir); + + /* (b) cs==ww byte-id gate. */ + char cs_s[64], ws_s[64]; + snprintf(cs_s, sizeof cs_s, "/tmp/wwxmv_%d_%d_cs.s", getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwxmv_%d_%d_ww.s", getpid(), i); + + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label); + fail++; unlink(src); continue; + } + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", + w6c_ww, ws_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww failed\n", rows[i].label); + fail++; unlink(src); unlink(cs_s); continue; + } + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER " + "(rule-10 byte-id violation)\n", rows[i].label); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + } + + if (fail) { + fprintf(stderr, "%d/%d xmod value-global tests failed\n", fail, n); + return 1; + } + printf("xmod_valglobal: %d/%d ok (cstage run + cs==ww byte-id)\n", n, n); + return 0; +}