wcc/cgen: #66(b-i) wwstage inferred-literal scalar global — default annotation to int, emit DATA+load (align up)

An inferred-literal scalar module-global (`let s = 42;`) was wwstage
silent-wrong: the checker stamps the annotation N_TNAME("untyped_int"),
which letscalarprim does not recognise, so letemitsize returns 0 — the
global is dropped from collectlets (no DATA emitted) AND cgident falls to
the silent module-leaf (no load), running garbage. cstage defaults
untyped_int to an 8B int before emit (DATAW + MOVQ), which is correct.

Fix (wwstage-only, align up to cstage): defaultinferredlets in cgen.ww,
called from cgfile (cgendecl.ww) before collectlets, rewrites the
annotation "untyped_int" -> "int" (8B machine word, NOT i32 — the #108
truncation trap is the opposite polarity) for a module-level N_LET whose
rhs is N_INTLIT. All three consumers (letemitsize, emitletdataw, cgident
global-read) then resolve a concrete int. cstage is untouched.

Scope: N_INTLIT only. A const-expr inferred global (`let s = 7*6;`, N_BIN)
stays on its existing path — that is a separate live cs!=ww silent
miscompile tracked as #133, out of scope here.

Pin: 947_inferred_scalar_global_run — inferred `let s=42` (42, base
wwstage garbage) + typed control, cs==ww byte-id.
This commit is contained in:
2026-06-07 12:26:57 +09:00
parent 2c09d13ca3
commit 74e60e89f0
6 changed files with 292 additions and 0 deletions

View File

@@ -38056,6 +38056,7 @@ export fn cgfile(c: *cgen, file: *node) void = {
collectfnrets(c, file);
fficollect(c, file);
collectmods(c, file);
defaultinferredlets(c, file);
collectlets(c, file);
let d: *node = file.list;
for (d != nil) {
@@ -39114,6 +39115,40 @@ fn letemitsize(c: *cgen, d: *node) i32 = {
return 0;
};
// defaultinferredlets — #66(b-i): an inferred module-global int-literal
// `let s = 42;` is stamped by the checker with an N_TNAME("untyped_int")
// annotation (d.lhs). letemitsize / emitletdataw / the cgident global-read arm
// key on that annotation's name, which letscalarprim doesn't recognise → the
// global is dropped from collectlets (no DATAW) and the read falls to the
// silent module-leaf (no MOVQ, MOVSXD on stale AX → wrong). cstage instead
// type_default's the untyped int to the 8B machine word BEFORE emit. Mirror
// that here at the single global-decl pass: rewrite the annotation to the
// concrete machine word `int` so all three consumers resolve it as an 8B int
// global (DATAW + MOVQ, byte-identical to cstage). int (not i32) per
// [[project_int_machine_word_derived_limits]] — i32 is the #108 truncation
// trap, opposite polarity.
// Guarded to N_INTLIT ONLY: a const-EXPR rhs (`let s = 7*6`, N_BIN) is also
// stamped untyped_int but is #66(b-ii) — a SEPARATE loud both-stage gap (no
// DATA → link-fail) — and must stay on its loud route, not be defaulted to a
// silent value. Runs before collectlets in cgfile so the mutated d.lhs is
// visible to letpreintern + emitletdataw too.
fn defaultinferredlets(c: *cgen, file: *node) void = {
if (file == nil) { return; };
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_LET) {
if (d.lhs != nil && d.rhs != nil) {
if (d.lhs.kind == nkind.N_TNAME
&& streq(d.lhs.str, "untyped_int")
&& d.rhs.kind == nkind.N_INTLIT) {
d.lhs.str = "int";
};
};
};
d = d.next;
};
};
fn collectlets(c: *cgen, file: *node) void = {
c.lets = nil;
if (file == nil) { return; };

View File

@@ -1040,6 +1040,40 @@ fn letemitsize(c: *cgen, d: *node) i32 = {
return 0;
};
// defaultinferredlets — #66(b-i): an inferred module-global int-literal
// `let s = 42;` is stamped by the checker with an N_TNAME("untyped_int")
// annotation (d.lhs). letemitsize / emitletdataw / the cgident global-read arm
// key on that annotation's name, which letscalarprim doesn't recognise → the
// global is dropped from collectlets (no DATAW) and the read falls to the
// silent module-leaf (no MOVQ, MOVSXD on stale AX → wrong). cstage instead
// type_default's the untyped int to the 8B machine word BEFORE emit. Mirror
// that here at the single global-decl pass: rewrite the annotation to the
// concrete machine word `int` so all three consumers resolve it as an 8B int
// global (DATAW + MOVQ, byte-identical to cstage). int (not i32) per
// [[project_int_machine_word_derived_limits]] — i32 is the #108 truncation
// trap, opposite polarity.
// Guarded to N_INTLIT ONLY: a const-EXPR rhs (`let s = 7*6`, N_BIN) is also
// stamped untyped_int but is #66(b-ii) — a SEPARATE loud both-stage gap (no
// DATA → link-fail) — and must stay on its loud route, not be defaulted to a
// silent value. Runs before collectlets in cgfile so the mutated d.lhs is
// visible to letpreintern + emitletdataw too.
fn defaultinferredlets(c: *cgen, file: *node) void = {
if (file == nil) { return; };
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_LET) {
if (d.lhs != nil && d.rhs != nil) {
if (d.lhs.kind == nkind.N_TNAME
&& streq(d.lhs.str, "untyped_int")
&& d.rhs.kind == nkind.N_INTLIT) {
d.lhs.str = "int";
};
};
};
d = d.next;
};
};
fn collectlets(c: *cgen, file: *node) void = {
c.lets = nil;
if (file == nil) { return; };

View File

@@ -619,6 +619,7 @@ export fn cgfile(c: *cgen, file: *node) void = {
collectfnrets(c, file);
fficollect(c, file);
collectmods(c, file);
defaultinferredlets(c, file);
collectlets(c, file);
let d: *node = file.list;
for (d != nil) {

View File

@@ -38056,6 +38056,7 @@ export fn cgfile(c: *cgen, file: *node) void = {
collectfnrets(c, file);
fficollect(c, file);
collectmods(c, file);
defaultinferredlets(c, file);
collectlets(c, file);
let d: *node = file.list;
for (d != nil) {
@@ -39114,6 +39115,40 @@ fn letemitsize(c: *cgen, d: *node) i32 = {
return 0;
};
// defaultinferredlets — #66(b-i): an inferred module-global int-literal
// `let s = 42;` is stamped by the checker with an N_TNAME("untyped_int")
// annotation (d.lhs). letemitsize / emitletdataw / the cgident global-read arm
// key on that annotation's name, which letscalarprim doesn't recognise → the
// global is dropped from collectlets (no DATAW) and the read falls to the
// silent module-leaf (no MOVQ, MOVSXD on stale AX → wrong). cstage instead
// type_default's the untyped int to the 8B machine word BEFORE emit. Mirror
// that here at the single global-decl pass: rewrite the annotation to the
// concrete machine word `int` so all three consumers resolve it as an 8B int
// global (DATAW + MOVQ, byte-identical to cstage). int (not i32) per
// [[project_int_machine_word_derived_limits]] — i32 is the #108 truncation
// trap, opposite polarity.
// Guarded to N_INTLIT ONLY: a const-EXPR rhs (`let s = 7*6`, N_BIN) is also
// stamped untyped_int but is #66(b-ii) — a SEPARATE loud both-stage gap (no
// DATA → link-fail) — and must stay on its loud route, not be defaulted to a
// silent value. Runs before collectlets in cgfile so the mutated d.lhs is
// visible to letpreintern + emitletdataw too.
fn defaultinferredlets(c: *cgen, file: *node) void = {
if (file == nil) { return; };
let d: *node = file.list;
for (d != nil) {
if (d.kind == nkind.N_LET) {
if (d.lhs != nil && d.rhs != nil) {
if (d.lhs.kind == nkind.N_TNAME
&& streq(d.lhs.str, "untyped_int")
&& d.rhs.kind == nkind.N_INTLIT) {
d.lhs.str = "int";
};
};
};
d = d.next;
};
};
fn collectlets(c: *cgen, file: *node) void = {
c.lets = nil;
if (file == nil) { return; };