wcc: stamp for-range tuple-binds and massign blank via shared helper

For-range tuple-destructure binders (for (let (k,v) .. s)) and the tuple
massign discard _ were left nil-typed: the for-range binders are N_IDENT
use-sites and the _ slot, though unbound, has a real element type. Add a
shared stamptuplebinds helper — one lockstep walk distributing an N_TTUPLE's
per-element types onto a binder chain — refactoring the existing N_MLET
destructure loop into it (behavior identical) and adding N_FORRANGE and
N_MASSIGN call-sites. _ is STAMPED with its slot's element type (unbound is
not untyped), not exempted. Mirrors harec create_unpack_bindings
(ref/harec/src/check.c:1354-1419), the routine harec shares between let-unpack
and the for-each header (:2308-2317).

A prerequisite for arming the wwstage asserttyped bail. byte-id holds (cgen
derives binder/elem widths structurally, never off type_; 990-997 green).
Extends the 901 gap-corpus with 901_forrange_tuple.ww + 901_massign_blank.ww.
This commit is contained in:
2026-05-28 09:06:54 +09:00
parent 3252bd1709
commit f892a91223
6 changed files with 317 additions and 61 deletions

View File

@@ -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;
};

View File

@@ -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;
};

View File

@@ -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;
};