wcc/ww: checktryprop flattens spread variants in the success count

The F8 multi-success gate counted a `...inner` spread as one variant,
bypassing the multi-success reject. New trycountvariants recursively
flattens spreads (the #209 recursion; cstage check.c:2160). Also
carries the rule-7 deferral cites for the adjacent task-#50/#51 holes
(exprtypeoftry lookups, desugarcallargs fn-ptr bail, checkisas) — the
attempted scopelookupprefer hardening is byte-id-blocked by the #50
curmod layer (evidence in the task).
This commit is contained in:
2026-06-12 11:28:31 +09:00
parent 6f2ed0cc57
commit 780e680c1b
3 changed files with 183 additions and 15 deletions

View File

@@ -15672,6 +15672,13 @@ fn desugarcallargs(c: *checker, n: *node) void = {
};
}; };
let decl: *node = calleefndecl(c, n.lhs);
// task #51: calleefndecl nils every fn-VALUE callee — a deref `(*fp)(...)`
// (N_UN), an SK_VAR fn-ptr ident, a struct-field fn call — so this bail
// skips the #258 array→slice desugar AND the general arg typecheck for
// them, and cgen then pushes raw array words where the callee reads a 24B
// slice header. cstage drives the same arg loop off the callee TYPE
// (cmd/wcc/check.c:1828 type_chase_named(cexpr(callee))). The type-keyed
// callee path is MECHANISM (not a checker gate) — filed as task #51.
if (decl == nil) { return; };
let param: *node = decl.list;
let prev: *node = nil;
@@ -16200,6 +16207,13 @@ fn checkisas(c: *checker, n: *node) void = {
fn exprtypeoftry(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
if (e.kind == nkind.N_IDENT) {
// #11a/task #50: this bare-leaf lookup wants curmod preference
// (scopelookupprefer, the #53/#55 family) but the swap BROKE the
// 995 self-rebuild byte-id — in the bundled self-compile ww's
// scopelookupprefer diverges from cstage's resolution (the same
// curmod-in-bundle layer that blocked #5), flipping a try-operand
// stamp in the selfhost source. Held at plain scopelookup until #50
// lands the bundle curmod fix; aligning here in isolation is unsound.
let s: *sym = scopelookup(c.cur, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
@@ -16213,17 +16227,61 @@ fn exprtypeoftry(c: *checker, e: *node) *node = {
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
// #11b/task #51: an N_DOT callee `mod.f()?` is looked up by BARE
// LEAF here, NOT via the module qualifier (callee.lhs) the way
// exprtype's own N_CALL arm does (:3095 scopelookupinmodule) — a
// cross-module same-leaf `f` misbinds. The mechanism fix (module-
// keyed callee resolution + the deref-callee `(*fp)()?` shape that
// returns nil below) rides task #51 with the fn-ptr-callee desugar.
if (callee.kind == nkind.N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; };
// #11a/task #50: curmod preference wanted here too, held at plain
// scopelookup — the prefer swap broke 995 byte-id (bundle curmod
// divergence, same as the N_IDENT arm above + #5/#50).
let s: *sym = scopelookup(c.cur, nm);
if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
// task #51: a deref callee `(*fp)()?` / SK_VAR fn-ptr ident / struct-
// field fn call returns nil here, so checktryprop silently bails and
// the F8 multi-success reject is skipped for those shapes. The fix
// (type-keyed operand resolution: peel TPTR → TFN.ret) is mechanism,
// filed with the fn-ptr-callee desugar as task #51 — NOT a checker gate.
return nil;
};
// trycountvariants — #38/F3 (review item 8): walk a tagged union's variant
// list FLATTENING `...inner` spreads, accumulating the success count and the
// has-error flag. cstage counts/checks the resolve_type-FLATTENED u->params
// (cmd/wcc/check.c:2160-2165 over the TK_ELLIPSIS-spliced chain); ww's
// checktryprop walked the raw AST, so a `(...inner | e)` counted the spread as
// ONE success and the F8 multi-success gate never fired — the exact silent
// accept the gate exists to catch (a valid bool success then treated as error
// by the single-tag-compare cgen). iserror is judged against the OUTER union
// (`outer`) throughout — taggedhaserr(outer) holds because the spread's sibling
// or a spliced member carries the error, so each flattened member routes
// through varianterr, matching cstage's per-param iserror. Mirrors the #209
// spread recursion tinfofornode already runs over vu.params (check.ww:2275).
fn trycountvariants(c: *checker, outer: *node, v: *node, nsuccp: *int,
haserrp: *bool, depth: i32) void = {
for (v != nil) {
if (v.op == tkind.TK_ELLIPSIS && depth < 8i32) {
let inner: *node = resolvealias(c, unwrapbang(v));
if (inner != nil && inner.kind == nkind.N_TTAGGED) {
trycountvariants(c, outer, inner.list, nsuccp,
haserrp, depth + 1i32);
v = v.next;
continue;
};
};
if (iserrvariant(c, outer, v)) { *haserrp = true; }
else { *nsuccp += 1; };
v = v.next;
};
};
fn checktryprop(c: *checker, n: *node) void = {
if (n == nil) { return; };
let t: *node = exprtypeoftry(c, n.lhs);
@@ -16240,11 +16298,9 @@ fn checktryprop(c: *checker, n: *node) void = {
// N_TRYPROP/N_TRYUNW.
let haserr: bool = false;
let nsucc: int = 0;
let v: *node = u.list;
for (v != nil) {
if (iserrvariant(c, u, v)) { haserr = true; } else { nsucc += 1; };
v = v.next;
};
// #38/F3 (review item 8): flatten `...inner` spreads so the F8
// multi-success gate counts the spliced members, not the spread alias.
trycountvariants(c, u, u.list, &nsucc, &haserr, 0i32);
if (nsucc > 1) {
if (n.kind == nkind.N_TRYPROP) { cerr("?"); } else { cerr("!"); };
cerr(": multi-success union unwired (task #14): bind and match instead\n");

View File

@@ -5252,6 +5252,13 @@ fn desugarcallargs(c: *checker, n: *node) void = {
};
}; };
let decl: *node = calleefndecl(c, n.lhs);
// task #51: calleefndecl nils every fn-VALUE callee — a deref `(*fp)(...)`
// (N_UN), an SK_VAR fn-ptr ident, a struct-field fn call — so this bail
// skips the #258 array→slice desugar AND the general arg typecheck for
// them, and cgen then pushes raw array words where the callee reads a 24B
// slice header. cstage drives the same arg loop off the callee TYPE
// (cmd/wcc/check.c:1828 type_chase_named(cexpr(callee))). The type-keyed
// callee path is MECHANISM (not a checker gate) — filed as task #51.
if (decl == nil) { return; };
let param: *node = decl.list;
let prev: *node = nil;
@@ -5780,6 +5787,13 @@ fn checkisas(c: *checker, n: *node) void = {
fn exprtypeoftry(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
if (e.kind == nkind.N_IDENT) {
// #11a/task #50: this bare-leaf lookup wants curmod preference
// (scopelookupprefer, the #53/#55 family) but the swap BROKE the
// 995 self-rebuild byte-id — in the bundled self-compile ww's
// scopelookupprefer diverges from cstage's resolution (the same
// curmod-in-bundle layer that blocked #5), flipping a try-operand
// stamp in the selfhost source. Held at plain scopelookup until #50
// lands the bundle curmod fix; aligning here in isolation is unsound.
let s: *sym = scopelookup(c.cur, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
@@ -5793,17 +5807,61 @@ fn exprtypeoftry(c: *checker, e: *node) *node = {
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
// #11b/task #51: an N_DOT callee `mod.f()?` is looked up by BARE
// LEAF here, NOT via the module qualifier (callee.lhs) the way
// exprtype's own N_CALL arm does (:3095 scopelookupinmodule) — a
// cross-module same-leaf `f` misbinds. The mechanism fix (module-
// keyed callee resolution + the deref-callee `(*fp)()?` shape that
// returns nil below) rides task #51 with the fn-ptr-callee desugar.
if (callee.kind == nkind.N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; };
// #11a/task #50: curmod preference wanted here too, held at plain
// scopelookup — the prefer swap broke 995 byte-id (bundle curmod
// divergence, same as the N_IDENT arm above + #5/#50).
let s: *sym = scopelookup(c.cur, nm);
if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
// task #51: a deref callee `(*fp)()?` / SK_VAR fn-ptr ident / struct-
// field fn call returns nil here, so checktryprop silently bails and
// the F8 multi-success reject is skipped for those shapes. The fix
// (type-keyed operand resolution: peel TPTR → TFN.ret) is mechanism,
// filed with the fn-ptr-callee desugar as task #51 — NOT a checker gate.
return nil;
};
// trycountvariants — #38/F3 (review item 8): walk a tagged union's variant
// list FLATTENING `...inner` spreads, accumulating the success count and the
// has-error flag. cstage counts/checks the resolve_type-FLATTENED u->params
// (cmd/wcc/check.c:2160-2165 over the TK_ELLIPSIS-spliced chain); ww's
// checktryprop walked the raw AST, so a `(...inner | e)` counted the spread as
// ONE success and the F8 multi-success gate never fired — the exact silent
// accept the gate exists to catch (a valid bool success then treated as error
// by the single-tag-compare cgen). iserror is judged against the OUTER union
// (`outer`) throughout — taggedhaserr(outer) holds because the spread's sibling
// or a spliced member carries the error, so each flattened member routes
// through varianterr, matching cstage's per-param iserror. Mirrors the #209
// spread recursion tinfofornode already runs over vu.params (check.ww:2275).
fn trycountvariants(c: *checker, outer: *node, v: *node, nsuccp: *int,
haserrp: *bool, depth: i32) void = {
for (v != nil) {
if (v.op == tkind.TK_ELLIPSIS && depth < 8i32) {
let inner: *node = resolvealias(c, unwrapbang(v));
if (inner != nil && inner.kind == nkind.N_TTAGGED) {
trycountvariants(c, outer, inner.list, nsuccp,
haserrp, depth + 1i32);
v = v.next;
continue;
};
};
if (iserrvariant(c, outer, v)) { *haserrp = true; }
else { *nsuccp += 1; };
v = v.next;
};
};
fn checktryprop(c: *checker, n: *node) void = {
if (n == nil) { return; };
let t: *node = exprtypeoftry(c, n.lhs);
@@ -5820,11 +5878,9 @@ fn checktryprop(c: *checker, n: *node) void = {
// N_TRYPROP/N_TRYUNW.
let haserr: bool = false;
let nsucc: int = 0;
let v: *node = u.list;
for (v != nil) {
if (iserrvariant(c, u, v)) { haserr = true; } else { nsucc += 1; };
v = v.next;
};
// #38/F3 (review item 8): flatten `...inner` spreads so the F8
// multi-success gate counts the spliced members, not the spread alias.
trycountvariants(c, u, u.list, &nsucc, &haserr, 0i32);
if (nsucc > 1) {
if (n.kind == nkind.N_TRYPROP) { cerr("?"); } else { cerr("!"); };
cerr(": multi-success union unwired (task #14): bind and match instead\n");

View File

@@ -15672,6 +15672,13 @@ fn desugarcallargs(c: *checker, n: *node) void = {
};
}; };
let decl: *node = calleefndecl(c, n.lhs);
// task #51: calleefndecl nils every fn-VALUE callee — a deref `(*fp)(...)`
// (N_UN), an SK_VAR fn-ptr ident, a struct-field fn call — so this bail
// skips the #258 array→slice desugar AND the general arg typecheck for
// them, and cgen then pushes raw array words where the callee reads a 24B
// slice header. cstage drives the same arg loop off the callee TYPE
// (cmd/wcc/check.c:1828 type_chase_named(cexpr(callee))). The type-keyed
// callee path is MECHANISM (not a checker gate) — filed as task #51.
if (decl == nil) { return; };
let param: *node = decl.list;
let prev: *node = nil;
@@ -16200,6 +16207,13 @@ fn checkisas(c: *checker, n: *node) void = {
fn exprtypeoftry(c: *checker, e: *node) *node = {
if (e == nil) { return nil; };
if (e.kind == nkind.N_IDENT) {
// #11a/task #50: this bare-leaf lookup wants curmod preference
// (scopelookupprefer, the #53/#55 family) but the swap BROKE the
// 995 self-rebuild byte-id — in the bundled self-compile ww's
// scopelookupprefer diverges from cstage's resolution (the same
// curmod-in-bundle layer that blocked #5), flipping a try-operand
// stamp in the selfhost source. Held at plain scopelookup until #50
// lands the bundle curmod fix; aligning here in isolation is unsound.
let s: *sym = scopelookup(c.cur, e.str);
if (s == nil) { return nil; };
if (s.decl == nil) { return nil; };
@@ -16213,17 +16227,61 @@ fn exprtypeoftry(c: *checker, e: *node) *node = {
let nm: str;
nm.ptr = nil; nm.len = 0;
if (callee.kind == nkind.N_IDENT) { nm = callee.str; };
// #11b/task #51: an N_DOT callee `mod.f()?` is looked up by BARE
// LEAF here, NOT via the module qualifier (callee.lhs) the way
// exprtype's own N_CALL arm does (:3095 scopelookupinmodule) — a
// cross-module same-leaf `f` misbinds. The mechanism fix (module-
// keyed callee resolution + the deref-callee `(*fp)()?` shape that
// returns nil below) rides task #51 with the fn-ptr-callee desugar.
if (callee.kind == nkind.N_DOT) { nm = callee.str; };
if (nm.len == 0) { return nil; };
// #11a/task #50: curmod preference wanted here too, held at plain
// scopelookup — the prefer swap broke 995 byte-id (bundle curmod
// divergence, same as the N_IDENT arm above + #5/#50).
let s: *sym = scopelookup(c.cur, nm);
if (s == nil) { return nil; };
if (s.skind != skind.SK_FN) { return nil; };
if (s.decl == nil) { return nil; };
return s.decl.lhs;
};
// task #51: a deref callee `(*fp)()?` / SK_VAR fn-ptr ident / struct-
// field fn call returns nil here, so checktryprop silently bails and
// the F8 multi-success reject is skipped for those shapes. The fix
// (type-keyed operand resolution: peel TPTR → TFN.ret) is mechanism,
// filed with the fn-ptr-callee desugar as task #51 — NOT a checker gate.
return nil;
};
// trycountvariants — #38/F3 (review item 8): walk a tagged union's variant
// list FLATTENING `...inner` spreads, accumulating the success count and the
// has-error flag. cstage counts/checks the resolve_type-FLATTENED u->params
// (cmd/wcc/check.c:2160-2165 over the TK_ELLIPSIS-spliced chain); ww's
// checktryprop walked the raw AST, so a `(...inner | e)` counted the spread as
// ONE success and the F8 multi-success gate never fired — the exact silent
// accept the gate exists to catch (a valid bool success then treated as error
// by the single-tag-compare cgen). iserror is judged against the OUTER union
// (`outer`) throughout — taggedhaserr(outer) holds because the spread's sibling
// or a spliced member carries the error, so each flattened member routes
// through varianterr, matching cstage's per-param iserror. Mirrors the #209
// spread recursion tinfofornode already runs over vu.params (check.ww:2275).
fn trycountvariants(c: *checker, outer: *node, v: *node, nsuccp: *int,
haserrp: *bool, depth: i32) void = {
for (v != nil) {
if (v.op == tkind.TK_ELLIPSIS && depth < 8i32) {
let inner: *node = resolvealias(c, unwrapbang(v));
if (inner != nil && inner.kind == nkind.N_TTAGGED) {
trycountvariants(c, outer, inner.list, nsuccp,
haserrp, depth + 1i32);
v = v.next;
continue;
};
};
if (iserrvariant(c, outer, v)) { *haserrp = true; }
else { *nsuccp += 1; };
v = v.next;
};
};
fn checktryprop(c: *checker, n: *node) void = {
if (n == nil) { return; };
let t: *node = exprtypeoftry(c, n.lhs);
@@ -16240,11 +16298,9 @@ fn checktryprop(c: *checker, n: *node) void = {
// N_TRYPROP/N_TRYUNW.
let haserr: bool = false;
let nsucc: int = 0;
let v: *node = u.list;
for (v != nil) {
if (iserrvariant(c, u, v)) { haserr = true; } else { nsucc += 1; };
v = v.next;
};
// #38/F3 (review item 8): flatten `...inner` spreads so the F8
// multi-success gate counts the spliced members, not the spread alias.
trycountvariants(c, u, u.list, &nsucc, &haserr, 0i32);
if (nsucc > 1) {
if (n.kind == nkind.N_TRYPROP) { cerr("?"); } else { cerr("!"); };
cerr(": multi-success union unwired (task #14): bind and match instead\n");