diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0b87334b..ce14413b 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -10261,6 +10261,56 @@ fn installdecl(c: *checker, file: *node, d: *node) void = { if (k == nkind.N_LET) { scopedefineinmodule(c.top, nm, mod, skind.SK_VAR, nil, d); return; }; }; +// stamptuplebinds — distribute a tuple's per-element types onto a +// destructure binding chain, walked in lockstep with the resolved +// N_TTUPLE element chain (each `elems` link carries its element type on +// .lhs). Mirror of harec's create_unpack_bindings +// (ref/harec/src/check.c:1354-1419), which harec shares between +// let-unpack (check_expr_binding) and the for-each loop header +// (ref/harec/src/check.c:2308-2317) — the one shape behind ww's +// `let (a,b) = f()`, `for (let (a,b) .. s)`, and the ww-extension +// multi-assign `a, _ = f()`. +// +// `define` (the binding contexts: let-unpack + for-range) installs each +// named binder as a fresh SK_VAR and back-fills its declared type onto +// .lhs so use sites resolve through the N_IDENT exprtype path. Multi- +// assign targets are pre-declared lvalues, so it passes false: .lhs is +// left untouched (an N_INDEX/N_DOT target carries a live operand there) +// and only the type_ stamp fires on the still-untyped slots. +// +// Each binder/target node's own type_ is stamped from its element type so +// the asserttyped gate sees a typed node. This covers the discard `_` (an +// empty-str N_IDENT with no decl to read a type back from): harec drops +// `_` yet still advances the tuple slot, so that slot's element type is +// the honest type to stamp — `_` is UNBOUND, not UNTYPED. +fn stamptuplebinds(c: *checker, binds: *node, elems: *node, + define: bool, what: str) void = { + let b: *node = binds; + let pt: *node = elems; + for (b != nil) { + let et: *node = nil; + if (pt != nil) { et = pt.lhs; }; + if (define) { + if (b.lhs == nil) { b.lhs = et; }; + let bnm: str = b.str; + if (bnm.len > 0) { + checkmoduleshadow(c, bnm, what); + scopedefine(c.cur, bnm, skind.SK_VAR, nil, b); + }; + }; + if (b.type_ == nil) { + let src: *node = b.lhs; + if (src == nil) { src = et; }; + if (src != nil) { + let ti: *tinfo = tinfofornode(c, src); + if (ti != nil) { b.type_ = ti: *void; }; + }; + }; + b = b.next; + if (pt != nil) { pt = pt.next; }; + }; +}; + // resolvewalk — recursive AST walk that, for every nkind.N_IDENT and // nkind.N_TNAME seen, looks up the name and bumps the resolved/unresolved // counters. Local lets are installed in the current scope as soon as @@ -10354,15 +10404,22 @@ fn resolvewalk(c: *checker, n: *node) void = { if (k == nkind.N_FORRANGE) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; if (n.list != nil) { - let m: *node = n.list; - for (m != nil) { - let bnm: str = m.str; - if (bnm.len > 0) { - checkmoduleshadow(c, bnm, "binding"); - scopedefine(c.cur, bnm, skind.SK_VAR, nil, m); - }; - m = m.next; + // Tuple destructure `for (let (a,b) .. xs)`: peel the + // iterable's element type and distribute its tuple + // element types onto the binders, the same lockstep walk + // harec runs for the for-each header + // (ref/harec/src/check.c:2308-2317 → create_unpack_bindings). + let elems: *node = nil; + let it: *node = exprtype(c, n.lhs, nil); + if (it != nil) { + let et: *node = nil; + if (it.kind == nkind.N_TSLICE) { et = it.lhs; }; + if (it.kind == nkind.N_TARRAY) { et = it.lhs; }; + if (et != nil) { if (et.kind == nkind.N_TTUPLE) { + elems = et.list; + }; }; }; + stamptuplebinds(c, n.list, elems, true, "binding"); } else { let bnm: str = n.str; if (bnm.len > 0) { @@ -10452,19 +10509,29 @@ fn resolvewalk(c: *checker, n: *node) void = { pt = rty.list; }; }; }; }; + stamptuplebinds(c, n.list, pt, true, "let"); + return; + }; + + // `a, _ = call();` — tuple multi-assign (a retained ww extension over + // Hare; harec has no statement-position unpack-assign). Targets are + // pre-declared lvalues, resolved by the per-target resolvewalk below + // before the distribution; stamptuplebinds(define=false) only stamps + // still-nil slots, which is exactly the discard `_` (no decl, so the + // N_IDENT exprtype path leaves it untyped). Distribution mirrors the + // N_MLET/N_FORRANGE binders; see stamptuplebinds. + if (k == nkind.N_MASSIGN) { + if (n.rhs != nil) { resolvewalk(c, n.rhs); }; + let pt: *node = nil; + if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { + 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; }; - }; + for (l != nil) { resolvewalk(c, l); l = l.next; }; + stamptuplebinds(c, n.list, pt, false, ""); return; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 8ce31b87..f0fe1241 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -231,6 +231,56 @@ fn installdecl(c: *checker, file: *node, d: *node) void = { if (k == nkind.N_LET) { scopedefineinmodule(c.top, nm, mod, skind.SK_VAR, nil, d); return; }; }; +// stamptuplebinds — distribute a tuple's per-element types onto a +// destructure binding chain, walked in lockstep with the resolved +// N_TTUPLE element chain (each `elems` link carries its element type on +// .lhs). Mirror of harec's create_unpack_bindings +// (ref/harec/src/check.c:1354-1419), which harec shares between +// let-unpack (check_expr_binding) and the for-each loop header +// (ref/harec/src/check.c:2308-2317) — the one shape behind ww's +// `let (a,b) = f()`, `for (let (a,b) .. s)`, and the ww-extension +// multi-assign `a, _ = f()`. +// +// `define` (the binding contexts: let-unpack + for-range) installs each +// named binder as a fresh SK_VAR and back-fills its declared type onto +// .lhs so use sites resolve through the N_IDENT exprtype path. Multi- +// assign targets are pre-declared lvalues, so it passes false: .lhs is +// left untouched (an N_INDEX/N_DOT target carries a live operand there) +// and only the type_ stamp fires on the still-untyped slots. +// +// Each binder/target node's own type_ is stamped from its element type so +// the asserttyped gate sees a typed node. This covers the discard `_` (an +// empty-str N_IDENT with no decl to read a type back from): harec drops +// `_` yet still advances the tuple slot, so that slot's element type is +// the honest type to stamp — `_` is UNBOUND, not UNTYPED. +fn stamptuplebinds(c: *checker, binds: *node, elems: *node, + define: bool, what: str) void = { + let b: *node = binds; + let pt: *node = elems; + for (b != nil) { + let et: *node = nil; + if (pt != nil) { et = pt.lhs; }; + if (define) { + if (b.lhs == nil) { b.lhs = et; }; + let bnm: str = b.str; + if (bnm.len > 0) { + checkmoduleshadow(c, bnm, what); + scopedefine(c.cur, bnm, skind.SK_VAR, nil, b); + }; + }; + if (b.type_ == nil) { + let src: *node = b.lhs; + if (src == nil) { src = et; }; + if (src != nil) { + let ti: *tinfo = tinfofornode(c, src); + if (ti != nil) { b.type_ = ti: *void; }; + }; + }; + b = b.next; + if (pt != nil) { pt = pt.next; }; + }; +}; + // resolvewalk — recursive AST walk that, for every nkind.N_IDENT and // nkind.N_TNAME seen, looks up the name and bumps the resolved/unresolved // counters. Local lets are installed in the current scope as soon as @@ -324,15 +374,22 @@ fn resolvewalk(c: *checker, n: *node) void = { if (k == nkind.N_FORRANGE) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; if (n.list != nil) { - let m: *node = n.list; - for (m != nil) { - let bnm: str = m.str; - if (bnm.len > 0) { - checkmoduleshadow(c, bnm, "binding"); - scopedefine(c.cur, bnm, skind.SK_VAR, nil, m); - }; - m = m.next; + // Tuple destructure `for (let (a,b) .. xs)`: peel the + // iterable's element type and distribute its tuple + // element types onto the binders, the same lockstep walk + // harec runs for the for-each header + // (ref/harec/src/check.c:2308-2317 → create_unpack_bindings). + let elems: *node = nil; + let it: *node = exprtype(c, n.lhs, nil); + if (it != nil) { + let et: *node = nil; + if (it.kind == nkind.N_TSLICE) { et = it.lhs; }; + if (it.kind == nkind.N_TARRAY) { et = it.lhs; }; + if (et != nil) { if (et.kind == nkind.N_TTUPLE) { + elems = et.list; + }; }; }; + stamptuplebinds(c, n.list, elems, true, "binding"); } else { let bnm: str = n.str; if (bnm.len > 0) { @@ -422,19 +479,29 @@ fn resolvewalk(c: *checker, n: *node) void = { pt = rty.list; }; }; }; }; + stamptuplebinds(c, n.list, pt, true, "let"); + return; + }; + + // `a, _ = call();` — tuple multi-assign (a retained ww extension over + // Hare; harec has no statement-position unpack-assign). Targets are + // pre-declared lvalues, resolved by the per-target resolvewalk below + // before the distribution; stamptuplebinds(define=false) only stamps + // still-nil slots, which is exactly the discard `_` (no decl, so the + // N_IDENT exprtype path leaves it untyped). Distribution mirrors the + // N_MLET/N_FORRANGE binders; see stamptuplebinds. + if (k == nkind.N_MASSIGN) { + if (n.rhs != nil) { resolvewalk(c, n.rhs); }; + let pt: *node = nil; + if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { + 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; }; - }; + for (l != nil) { resolvewalk(c, l); l = l.next; }; + stamptuplebinds(c, n.list, pt, false, ""); return; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 79b8ddc7..17e8a3ed 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -10261,6 +10261,56 @@ fn installdecl(c: *checker, file: *node, d: *node) void = { if (k == nkind.N_LET) { scopedefineinmodule(c.top, nm, mod, skind.SK_VAR, nil, d); return; }; }; +// stamptuplebinds — distribute a tuple's per-element types onto a +// destructure binding chain, walked in lockstep with the resolved +// N_TTUPLE element chain (each `elems` link carries its element type on +// .lhs). Mirror of harec's create_unpack_bindings +// (ref/harec/src/check.c:1354-1419), which harec shares between +// let-unpack (check_expr_binding) and the for-each loop header +// (ref/harec/src/check.c:2308-2317) — the one shape behind ww's +// `let (a,b) = f()`, `for (let (a,b) .. s)`, and the ww-extension +// multi-assign `a, _ = f()`. +// +// `define` (the binding contexts: let-unpack + for-range) installs each +// named binder as a fresh SK_VAR and back-fills its declared type onto +// .lhs so use sites resolve through the N_IDENT exprtype path. Multi- +// assign targets are pre-declared lvalues, so it passes false: .lhs is +// left untouched (an N_INDEX/N_DOT target carries a live operand there) +// and only the type_ stamp fires on the still-untyped slots. +// +// Each binder/target node's own type_ is stamped from its element type so +// the asserttyped gate sees a typed node. This covers the discard `_` (an +// empty-str N_IDENT with no decl to read a type back from): harec drops +// `_` yet still advances the tuple slot, so that slot's element type is +// the honest type to stamp — `_` is UNBOUND, not UNTYPED. +fn stamptuplebinds(c: *checker, binds: *node, elems: *node, + define: bool, what: str) void = { + let b: *node = binds; + let pt: *node = elems; + for (b != nil) { + let et: *node = nil; + if (pt != nil) { et = pt.lhs; }; + if (define) { + if (b.lhs == nil) { b.lhs = et; }; + let bnm: str = b.str; + if (bnm.len > 0) { + checkmoduleshadow(c, bnm, what); + scopedefine(c.cur, bnm, skind.SK_VAR, nil, b); + }; + }; + if (b.type_ == nil) { + let src: *node = b.lhs; + if (src == nil) { src = et; }; + if (src != nil) { + let ti: *tinfo = tinfofornode(c, src); + if (ti != nil) { b.type_ = ti: *void; }; + }; + }; + b = b.next; + if (pt != nil) { pt = pt.next; }; + }; +}; + // resolvewalk — recursive AST walk that, for every nkind.N_IDENT and // nkind.N_TNAME seen, looks up the name and bumps the resolved/unresolved // counters. Local lets are installed in the current scope as soon as @@ -10354,15 +10404,22 @@ fn resolvewalk(c: *checker, n: *node) void = { if (k == nkind.N_FORRANGE) { if (n.lhs != nil) { resolvewalk(c, n.lhs); }; if (n.list != nil) { - let m: *node = n.list; - for (m != nil) { - let bnm: str = m.str; - if (bnm.len > 0) { - checkmoduleshadow(c, bnm, "binding"); - scopedefine(c.cur, bnm, skind.SK_VAR, nil, m); - }; - m = m.next; + // Tuple destructure `for (let (a,b) .. xs)`: peel the + // iterable's element type and distribute its tuple + // element types onto the binders, the same lockstep walk + // harec runs for the for-each header + // (ref/harec/src/check.c:2308-2317 → create_unpack_bindings). + let elems: *node = nil; + let it: *node = exprtype(c, n.lhs, nil); + if (it != nil) { + let et: *node = nil; + if (it.kind == nkind.N_TSLICE) { et = it.lhs; }; + if (it.kind == nkind.N_TARRAY) { et = it.lhs; }; + if (et != nil) { if (et.kind == nkind.N_TTUPLE) { + elems = et.list; + }; }; }; + stamptuplebinds(c, n.list, elems, true, "binding"); } else { let bnm: str = n.str; if (bnm.len > 0) { @@ -10452,19 +10509,29 @@ fn resolvewalk(c: *checker, n: *node) void = { pt = rty.list; }; }; }; }; + stamptuplebinds(c, n.list, pt, true, "let"); + return; + }; + + // `a, _ = call();` — tuple multi-assign (a retained ww extension over + // Hare; harec has no statement-position unpack-assign). Targets are + // pre-declared lvalues, resolved by the per-target resolvewalk below + // before the distribution; stamptuplebinds(define=false) only stamps + // still-nil slots, which is exactly the discard `_` (no decl, so the + // N_IDENT exprtype path leaves it untyped). Distribution mirrors the + // N_MLET/N_FORRANGE binders; see stamptuplebinds. + if (k == nkind.N_MASSIGN) { + if (n.rhs != nil) { resolvewalk(c, n.rhs); }; + let pt: *node = nil; + if (n.rhs != nil) { if (n.rhs.kind == nkind.N_CALL) { + 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; }; - }; + for (l != nil) { resolvewalk(c, l); l = l.next; }; + stamptuplebinds(c, n.list, pt, false, ""); return; }; diff --git a/test/wcc/901_asserttyped_gap.c b/test/wcc/901_asserttyped_gap.c index 377e0d0c..9f65867c 100644 --- a/test/wcc/901_asserttyped_gap.c +++ b/test/wcc/901_asserttyped_gap.c @@ -26,6 +26,8 @@ * D module-leaf == type/fn name fnmatch 0 (closed) * D module-leaf == type/fn name random 0 (closed) * E computed enum-member value-expr enum_corpus 0 (closed) + * F for-range tuple-destructure bind forrange_tuple 0 (closed) + * G tuple multi-assign discard `_` massign_blank 0 (closed) * * Exit code of wwdump_ww is intentionally not gated: the diagnostics * land on stderr regardless of the run's success, and arming the bail @@ -119,6 +121,10 @@ main(void) "D module-leaf == type/fn name", 0 }, { "test/wcc/901_enum_corpus.ww", "E computed enum-member value-expr", 0 }, + { "test/wcc/901_forrange_tuple.ww", + "F for-range tuple-destructure bind", 0 }, + { "test/wcc/901_massign_blank.ww", + "G tuple multi-assign discard `_`", 0 }, { NULL, NULL, 0 }, }; @@ -154,6 +160,6 @@ main(void) return 1; } printf("asserttyped_gap: ww-stage checker warn set matches manifest " - "on %d gap-corpus fixtures (C pinned, A+B+D+E closed)\n", n); + "on %d gap-corpus fixtures (C pinned, A+B+D+E+F+G closed)\n", n); return 0; } diff --git a/test/wcc/901_forrange_tuple.ww b/test/wcc/901_forrange_tuple.ww new file mode 100644 index 00000000..db0ef243 --- /dev/null +++ b/test/wcc/901_forrange_tuple.ww @@ -0,0 +1,24 @@ +// 901_forrange_tuple — for-range tuple-destructure binder gap fixture +// for the 901 asserttyped net (class F). `for (let (k, v) .. s)` over a +// `[](i64, i64)` left both binders — and every body use site of them, +// plus the N_BIN wrapping them — type_=nil: the N_FORRANGE handler +// installed the binders as SK_VAR but never back-filled their element +// type, so the N_IDENT exprtype path read a nil decl type. stamptuplebinds +// (the shared binder distributor, harec create_unpack_bindings) now peels +// the iterable element tuple and stamps each binder, so this reads 0. +package main; + +export fn main() i32 = { + let buf: [2]i64; + buf[0] = 1i64; + buf[1] = 2i64; + let s: [](i64, i64); + s.ptr = buf.ptr: *(i64, i64); + s.len = 1; + s.cap = 1; + let total: i64 = 0i64; + for (let (k, v) .. s) { + total += k + v; + }; + return total: i32; +}; diff --git a/test/wcc/901_massign_blank.ww b/test/wcc/901_massign_blank.ww new file mode 100644 index 00000000..deabd7d4 --- /dev/null +++ b/test/wcc/901_massign_blank.ww @@ -0,0 +1,25 @@ +// 901_massign_blank — tuple multi-assign discard `_` gap fixture for the +// 901 asserttyped net (class G). In `_, a = f()` the blank `_` parses to +// an empty-str N_IDENT with no decl, so the generic N_IDENT exprtype path +// left it type_=nil (945's own comment documents the gap). The `_` is a +// GAP, not an exemption — the slot has a real element type ([]u8 here), so +// stamptuplebinds(define=false) stamps it with that element type (harec +// drops `_` but still advances the tuple slot). a is pre-declared and +// resolves the normal way; this reads 0. +package main; + +fn mk() ([]u8, i64) = { + let b: str = "hi"; + let p: []u8; + p.ptr = b.ptr; + p.len = 2; + p.cap = 5; + return (p, 7i64); +}; + +export fn main() i32 = { + let a: i64 = 0i64; + _, a = mk(); + if (a != 7) { return 1; }; + return 0; +};