diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0e980330..8f349d31 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7532,6 +7532,56 @@ fn resolvewalk(c: *checker, n: *node) void = { return; }; + // `let (a, b) = call();` / `let a, b = call();` — destructure + // bindings. #121 (Package B, A-narrow): distribute the callee's + // tuple return-type element types onto the un-annotated bindings so + // later references stamp n.type_, matching cgen's structural binding- + // type classifier (cgmlet's rettupleof→localadd path). This closes + // the unstamped-float-destructure gap that the exprfloatkind collapse + // (commit 2's bridge) needs: without it a `let (f,i)=mk()` f64 binding + // reads stamp nil → would disagree with the structural f64. + // + // GUARD bare N_IDENT callee only: exprtype's N_CALL N_DOT arm is the + // shelved #16/#17 cross-module-shadow gap (check.ww:2250-2253), so a + // module-qualified callee resolves to the wrong/nil return type — + // stamping off it would be worse than the nil it replaces. Bare same- + // module callees resolve correctly today. Faithful to harec + // create_unpack_bindings (ref/harec/src/check.c:1354-1416) and the + // cstage twin (cmd/wcc/check.c:1912 N_MLET, which uses cexpr ungated); + // narrowed to N_IDENT pending #16/#17. Annotated bindings keep their + // own type. Mirrors the N_FORRANGE binding-install shape above; the + // bindings would otherwise install (unstamped) via the generic N_LET + // walk, so this early return must register them itself. + if (k == nkind.N_MLET) { + if (n.rhs != nil) { resolvewalk(c, n.rhs); }; + let pt: *node = nil; + if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { + let callee: *node = n.rhs.lhs; + if (callee != nil) { if (callee.kind == nkind.N_IDENT) { + // `rt` would shadow the imported lib/rt module + // (checkmoduleshadow errors); `rty` avoids it. + let rty: *node = exprtype(c, n.rhs, nil); + if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) { + pt = rty.list; + }; }; + }; }; + }; }; + let l: *node = n.list; + for (l != nil) { + if (l.lhs == nil) { + if (pt != nil) { l.lhs = pt.lhs; }; + }; + let bnm: str = l.str; + if (bnm.len > 0) { + checkmoduleshadow(c, bnm, "let"); + scopedefine(c.cur, bnm, skind.SK_VAR, nil, l); + }; + l = l.next; + if (pt != nil) { pt = pt.next; }; + }; + return; + }; + if (k == nkind.N_DOT) { // Walk only the base; the .field name is a member, not a // free identifier. diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index f70561ac..3c670a2c 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -381,6 +381,56 @@ fn resolvewalk(c: *checker, n: *node) void = { return; }; + // `let (a, b) = call();` / `let a, b = call();` — destructure + // bindings. #121 (Package B, A-narrow): distribute the callee's + // tuple return-type element types onto the un-annotated bindings so + // later references stamp n.type_, matching cgen's structural binding- + // type classifier (cgmlet's rettupleof→localadd path). This closes + // the unstamped-float-destructure gap that the exprfloatkind collapse + // (commit 2's bridge) needs: without it a `let (f,i)=mk()` f64 binding + // reads stamp nil → would disagree with the structural f64. + // + // GUARD bare N_IDENT callee only: exprtype's N_CALL N_DOT arm is the + // shelved #16/#17 cross-module-shadow gap (check.ww:2250-2253), so a + // module-qualified callee resolves to the wrong/nil return type — + // stamping off it would be worse than the nil it replaces. Bare same- + // module callees resolve correctly today. Faithful to harec + // create_unpack_bindings (ref/harec/src/check.c:1354-1416) and the + // cstage twin (cmd/wcc/check.c:1912 N_MLET, which uses cexpr ungated); + // narrowed to N_IDENT pending #16/#17. Annotated bindings keep their + // own type. Mirrors the N_FORRANGE binding-install shape above; the + // bindings would otherwise install (unstamped) via the generic N_LET + // walk, so this early return must register them itself. + if (k == nkind.N_MLET) { + if (n.rhs != nil) { resolvewalk(c, n.rhs); }; + let pt: *node = nil; + if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { + let callee: *node = n.rhs.lhs; + if (callee != nil) { if (callee.kind == nkind.N_IDENT) { + // `rt` would shadow the imported lib/rt module + // (checkmoduleshadow errors); `rty` avoids it. + let rty: *node = exprtype(c, n.rhs, nil); + if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) { + pt = rty.list; + }; }; + }; }; + }; }; + let l: *node = n.list; + for (l != nil) { + if (l.lhs == nil) { + if (pt != nil) { l.lhs = pt.lhs; }; + }; + let bnm: str = l.str; + if (bnm.len > 0) { + checkmoduleshadow(c, bnm, "let"); + scopedefine(c.cur, bnm, skind.SK_VAR, nil, l); + }; + l = l.next; + if (pt != nil) { pt = pt.next; }; + }; + return; + }; + if (k == nkind.N_DOT) { // Walk only the base; the .field name is a member, not a // free identifier. diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 7dd27129..e7d82233 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7532,6 +7532,56 @@ fn resolvewalk(c: *checker, n: *node) void = { return; }; + // `let (a, b) = call();` / `let a, b = call();` — destructure + // bindings. #121 (Package B, A-narrow): distribute the callee's + // tuple return-type element types onto the un-annotated bindings so + // later references stamp n.type_, matching cgen's structural binding- + // type classifier (cgmlet's rettupleof→localadd path). This closes + // the unstamped-float-destructure gap that the exprfloatkind collapse + // (commit 2's bridge) needs: without it a `let (f,i)=mk()` f64 binding + // reads stamp nil → would disagree with the structural f64. + // + // GUARD bare N_IDENT callee only: exprtype's N_CALL N_DOT arm is the + // shelved #16/#17 cross-module-shadow gap (check.ww:2250-2253), so a + // module-qualified callee resolves to the wrong/nil return type — + // stamping off it would be worse than the nil it replaces. Bare same- + // module callees resolve correctly today. Faithful to harec + // create_unpack_bindings (ref/harec/src/check.c:1354-1416) and the + // cstage twin (cmd/wcc/check.c:1912 N_MLET, which uses cexpr ungated); + // narrowed to N_IDENT pending #16/#17. Annotated bindings keep their + // own type. Mirrors the N_FORRANGE binding-install shape above; the + // bindings would otherwise install (unstamped) via the generic N_LET + // walk, so this early return must register them itself. + if (k == nkind.N_MLET) { + if (n.rhs != nil) { resolvewalk(c, n.rhs); }; + let pt: *node = nil; + if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { + let callee: *node = n.rhs.lhs; + if (callee != nil) { if (callee.kind == nkind.N_IDENT) { + // `rt` would shadow the imported lib/rt module + // (checkmoduleshadow errors); `rty` avoids it. + let rty: *node = exprtype(c, n.rhs, nil); + if (rty != nil) { if (rty.kind == nkind.N_TTUPLE) { + pt = rty.list; + }; }; + }; }; + }; }; + let l: *node = n.list; + for (l != nil) { + if (l.lhs == nil) { + if (pt != nil) { l.lhs = pt.lhs; }; + }; + let bnm: str = l.str; + if (bnm.len > 0) { + checkmoduleshadow(c, bnm, "let"); + scopedefine(c.cur, bnm, skind.SK_VAR, nil, l); + }; + l = l.next; + if (pt != nil) { pt = pt.next; }; + }; + return; + }; + if (k == nkind.N_DOT) { // Walk only the base; the .field name is a member, not a // free identifier. diff --git a/test/wcc/954_tuprecv_run.c b/test/wcc/954_tuprecv_run.c index aa4eb89f..58000a59 100644 --- a/test/wcc/954_tuprecv_run.c +++ b/test/wcc/954_tuprecv_run.c @@ -55,7 +55,7 @@ runwait(const char *cmd) return -1; } -struct row { const char *label; const char *src; int want_exit; }; +struct row { const char *label; const char *src; int want_exit; int chk_stamped; }; static const struct row rows[] = { /* mixed (f64, i64): f=2.5 -> i32 2, i=7 -> i32 7, 2+7 = 9. The @@ -105,7 +105,7 @@ static const struct row rows[] = { "export fn main() i32 = {\n" "\tlet (f, i) = mk();\n" "\treturn (f: i32) + (i: i32);\n" - "};\n", 9 }, + "};\n", 9, 1 }, /* CONTROL — wide/str element single-var (i64, str) is a 32B tuple * handled by the separate 32B receive branch; the fix's sz==16 gate * excludes it, so it stays byte-id pre- and post-fix. t.0 = 7. */ @@ -241,6 +241,29 @@ main(void) "byte-id violation)\n", rows[i].label); fail++; } + + /* (c) #121 A-narrow stamp gate: the un-annotated float- + * destructure binding must now carry a checker type stamp, so + * w6c_ww emits no `asserttyped:` diagnostic. Non-vacuous — + * pre-stamp (HEAD) w6c_ww fires asserttyped on the f64 binding + * ident (nil n.type_). */ + if (rows[i].chk_stamped) { + char errf[80]; + snprintf(errf, sizeof errf, + "/tmp/wwtup_%d_%d_err.txt", getpid(), i); + snprintf(cmd, sizeof cmd, + "%s -o /dev/null %s 2>%s", w6c_ww, src, errf); + runwait(cmd); + snprintf(cmd, sizeof cmd, + "grep -q asserttyped %s", errf); + if (runwait(cmd) == 0) { + fprintf(stderr, "row[%s]: w6c_ww emitted " + "asserttyped (destructure binding " + "unstamped)\n", rows[i].label); + fail++; + } + unlink(errf); + } unlink(src); unlink(cs_s); unlink(ws_s); } diff --git a/test/wcc/956_tuprecv_f64_run.c b/test/wcc/956_tuprecv_f64_run.c index 32f3b2a6..ea8bf447 100644 --- a/test/wcc/956_tuprecv_f64_run.c +++ b/test/wcc/956_tuprecv_f64_run.c @@ -60,7 +60,7 @@ runwait(const char *cmd) return -1; } -struct row { const char *label; const char *src; int want_exit; }; +struct row { const char *label; const char *src; int want_exit; int chk_stamped; }; static const struct row rows[] = { /* BUG — minimal repro. norm is BRANCHED (inner issub() CALL clobbers @@ -133,7 +133,7 @@ static const struct row rows[] = { "\tif (m != 16.0) { return 1; };\n" "\tif (i != 0) { return 2; };\n" "\treturn 0;\n" - "};\n", 0 }, + "};\n", 0, 1 }, /* BUG — DESTRUCTURE order-swap `let (i,m)=norm()`, (i64,f64). f64 * binding m is element 1 (cursor DX); pre-fix MOVQ DX,slot garbage, * post-fix MOVSD X0,slot. i=0, m=16.0. */ @@ -149,7 +149,7 @@ static const struct row rows[] = { "\tif (m != 16.0) { return 1; };\n" "\tif (i != 0) { return 2; };\n" "\treturn 0;\n" - "};\n", 0 }, + "};\n", 0, 1 }, /* BUG — REASSIGN form `m,i = norm()` (N_MASSIGN / cgmassign+tupstore) * into pre-declared slots. Same f64-element-from-X0 defect. m=16.0. */ { "massign_f64_i64_br", @@ -347,6 +347,29 @@ main(void) "byte-id violation)\n", rows[i].label); fail++; } + + /* (c) #121 A-narrow stamp gate: the un-annotated float- + * destructure binding must now carry a checker type stamp, so + * w6c_ww emits no `asserttyped:` diagnostic. Non-vacuous — + * pre-stamp (HEAD) w6c_ww fires asserttyped on the f64 binding + * ident (nil n.type_). */ + if (rows[i].chk_stamped) { + char errf[80]; + snprintf(errf, sizeof errf, + "/tmp/wwtupf_%d_%d_err.txt", getpid(), i); + snprintf(cmd, sizeof cmd, + "%s -o /dev/null %s 2>%s", w6c_ww, src, errf); + runwait(cmd); + snprintf(cmd, sizeof cmd, + "grep -q asserttyped %s", errf); + if (runwait(cmd) == 0) { + fprintf(stderr, "row[%s]: w6c_ww emitted " + "asserttyped (destructure binding " + "unstamped)\n", rows[i].label); + fail++; + } + unlink(errf); + } unlink(src); unlink(cs_s); unlink(ws_s); }