diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index dd8dbba0..b2a6ee1d 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -39115,32 +39115,50 @@ 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. +// defaultinferredlets — #66(b-i)/#134-neg: an inferred module-global whose rhs +// is an int literal (`let s = 42;`) or a single unary +/-/~ over one +// (`let s = -42;`/`~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 (and folds the unary). Mirror that here at the single global-decl pass: +// peel one unary +/-/~ over an N_INTLIT (operand `.lhs`, operator `.op`, as +// foldintliteral) and rewrite the annotation to the concrete machine word `int`. +// The inferred decl is then structurally the typed control (`let s: int = -42`), +// so all three consumers fire on the existing typed-path code — byte-identical +// to cstage. int (not i32) per [[project_int_machine_word_derived_limits]] — +// i32 is the #108 truncation trap, opposite polarity. +// Scope — INT literal operand ONLY: one unary level (covers -42/+42/~42); a +// nested unary (`- -42`) is a #134-residual (cstage folds it via +// foldintliteral's recursion, ww peels one level and leaves it silent) — not +// widened here. A const-EXPR rhs (`let s = 7*6`, N_BIN) is #133 — a +// SEPARATE loud both-stage gap (no DATA → link-fail) — and a unary over a +// NON-literal (`let s = -x`) is not constant; both stay on their current route. +// An inferred FLOAT global (`let s = 3.0;`) is the #134-float leg carved to +// #135: cstage integer-types the inferred float at the USE site (MOVQ, not +// MOVSD), so defaulting it ww-only here would emit MOVSD vs cstage's MOVQ = a +// cs≠ww divergence (rule-10) — it ships only WITH the cstage float-use-site fix. +// 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) { + if (d.lhs != nil && d.rhs != nil + && d.lhs.kind == nkind.N_TNAME + && streq(d.lhs.str, "untyped_int")) { + let opnd: *node = d.rhs; + if (opnd.kind == nkind.N_UN + && (opnd.op == tkind.TK_PLUS + || opnd.op == tkind.TK_MINUS + || opnd.op == tkind.TK_TILDE)) { + opnd = opnd.lhs; + }; + if (opnd != nil + && opnd.kind == nkind.N_INTLIT) { d.lhs.str = "int"; }; }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 344e6dba..48c98201 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -1040,32 +1040,50 @@ 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. +// defaultinferredlets — #66(b-i)/#134-neg: an inferred module-global whose rhs +// is an int literal (`let s = 42;`) or a single unary +/-/~ over one +// (`let s = -42;`/`~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 (and folds the unary). Mirror that here at the single global-decl pass: +// peel one unary +/-/~ over an N_INTLIT (operand `.lhs`, operator `.op`, as +// foldintliteral) and rewrite the annotation to the concrete machine word `int`. +// The inferred decl is then structurally the typed control (`let s: int = -42`), +// so all three consumers fire on the existing typed-path code — byte-identical +// to cstage. int (not i32) per [[project_int_machine_word_derived_limits]] — +// i32 is the #108 truncation trap, opposite polarity. +// Scope — INT literal operand ONLY: one unary level (covers -42/+42/~42); a +// nested unary (`- -42`) is a #134-residual (cstage folds it via +// foldintliteral's recursion, ww peels one level and leaves it silent) — not +// widened here. A const-EXPR rhs (`let s = 7*6`, N_BIN) is #133 — a +// SEPARATE loud both-stage gap (no DATA → link-fail) — and a unary over a +// NON-literal (`let s = -x`) is not constant; both stay on their current route. +// An inferred FLOAT global (`let s = 3.0;`) is the #134-float leg carved to +// #135: cstage integer-types the inferred float at the USE site (MOVQ, not +// MOVSD), so defaulting it ww-only here would emit MOVSD vs cstage's MOVQ = a +// cs≠ww divergence (rule-10) — it ships only WITH the cstage float-use-site fix. +// 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) { + if (d.lhs != nil && d.rhs != nil + && d.lhs.kind == nkind.N_TNAME + && streq(d.lhs.str, "untyped_int")) { + let opnd: *node = d.rhs; + if (opnd.kind == nkind.N_UN + && (opnd.op == tkind.TK_PLUS + || opnd.op == tkind.TK_MINUS + || opnd.op == tkind.TK_TILDE)) { + opnd = opnd.lhs; + }; + if (opnd != nil + && opnd.kind == nkind.N_INTLIT) { d.lhs.str = "int"; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index e9c21e71..d6874473 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -39115,32 +39115,50 @@ 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. +// defaultinferredlets — #66(b-i)/#134-neg: an inferred module-global whose rhs +// is an int literal (`let s = 42;`) or a single unary +/-/~ over one +// (`let s = -42;`/`~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 (and folds the unary). Mirror that here at the single global-decl pass: +// peel one unary +/-/~ over an N_INTLIT (operand `.lhs`, operator `.op`, as +// foldintliteral) and rewrite the annotation to the concrete machine word `int`. +// The inferred decl is then structurally the typed control (`let s: int = -42`), +// so all three consumers fire on the existing typed-path code — byte-identical +// to cstage. int (not i32) per [[project_int_machine_word_derived_limits]] — +// i32 is the #108 truncation trap, opposite polarity. +// Scope — INT literal operand ONLY: one unary level (covers -42/+42/~42); a +// nested unary (`- -42`) is a #134-residual (cstage folds it via +// foldintliteral's recursion, ww peels one level and leaves it silent) — not +// widened here. A const-EXPR rhs (`let s = 7*6`, N_BIN) is #133 — a +// SEPARATE loud both-stage gap (no DATA → link-fail) — and a unary over a +// NON-literal (`let s = -x`) is not constant; both stay on their current route. +// An inferred FLOAT global (`let s = 3.0;`) is the #134-float leg carved to +// #135: cstage integer-types the inferred float at the USE site (MOVQ, not +// MOVSD), so defaulting it ww-only here would emit MOVSD vs cstage's MOVQ = a +// cs≠ww divergence (rule-10) — it ships only WITH the cstage float-use-site fix. +// 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) { + if (d.lhs != nil && d.rhs != nil + && d.lhs.kind == nkind.N_TNAME + && streq(d.lhs.str, "untyped_int")) { + let opnd: *node = d.rhs; + if (opnd.kind == nkind.N_UN + && (opnd.op == tkind.TK_PLUS + || opnd.op == tkind.TK_MINUS + || opnd.op == tkind.TK_TILDE)) { + opnd = opnd.lhs; + }; + if (opnd != nil + && opnd.kind == nkind.N_INTLIT) { d.lhs.str = "int"; }; }; diff --git a/test/wcc/947_inferred_scalar_global_run.c b/test/wcc/947_inferred_scalar_global_run.c index e8a721cd..3615e79c 100644 --- a/test/wcc/947_inferred_scalar_global_run.c +++ b/test/wcc/947_inferred_scalar_global_run.c @@ -59,6 +59,28 @@ static const struct row rows[] = { "export fn main() i32 = {\n" " return s: i32;\n" "};\n", 42 }, + /* R3 — #134 neg leg: inferred unary-over-int-literal module-global + * `let s = -42;` (rhs N_UN(TK_MINUS) over N_INTLIT). Pre-fix wwstage + * emitted neither DATAW nor MOVQ (un-defaulted untyped_int annotation) + * → exit 168 (garbage). cstage folds the unary, emits DATAW + load + * (exit 214 = -42 low-8). Post-fix defaultinferredlets peels the unary + * and defaults to `int`, so cs==ww and both run -42 (exit 214). */ + { "neg_inferred", + "package main;\n" + "let s = -42;\n" + "export fn main() i32 = {\n" + " return s: i32;\n" + "};\n", 214 }, + /* R4 — #134 neg typed control: `let s: int = -42;` (TYPED). Already + * cs==ww today (concrete annotation → letscalarprim fires). Locks the + * construction proof: R3's emitted .s is byte-identical to this typed + * control (the inferred decl IS the typed decl after defaulting). */ + { "neg_typed_ctrl", + "package main;\n" + "let s: int = -42;\n" + "export fn main() i32 = {\n" + " return s: i32;\n" + "};\n", 214 }, { NULL, NULL, 0 } };