wcc: general call-arg typecheck via assignability union, both stages

wwstage's desugarcallargs ran no general per-arg typecheck (only the
narrow #258 array-to-slice arm): any mistyped scalar call-arg silently
miscompiled (int read as a 24B slice header; the -T face was a user
const __wwtests building a garbage test binary). Route every call-arg
through the predicate union isassignable()||assignableaddrfn(),
mirroring cstage type_assignable||assignable_addrfn and the check.c:1869
diagnostic. Confident scalar/aggregate and aggregate/aggregate
kind-mismatch rejects live in shared isassignable; the concrete-to-
tagged arm is shape-matched-lenient via tagshape() (AST mirror of cgen
taggedvariantindext) so genuine variant members keep flowing while
shape-mismatched aggregates reject. Reserve __wwtests under -T in both
stages (mirror the main reservation, check.c:2996). New table-driven
989_callarg_typecheck, 31 fixtures, reject rows proven red on pre-fix
binaries.

Deferred, filed, site-commented: the assign seam rides #178->#36
(typeeqast cannot compare variadic/module-qualified fn sigs); the
same-coarse-shape same-leaf nominal collision over-accept rides #37
(#10/#66 — the distinguishing module is absent from the AST surface
isassignable operates on).
This commit is contained in:
2026-06-11 20:45:02 +09:00
parent 8d2d157a58
commit 556a65ee86
6 changed files with 1291 additions and 45 deletions

View File

@@ -247,6 +247,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_arr_infer_len \
$(BIN)/test_arr_cap_reject \
$(BIN)/test_tagged_subset_reject \
$(BIN)/test_callarg_typecheck \
$(BIN)/test_arr_ptr_global \
$(BIN)/test_def_arr_infer_len \
$(BIN)/test_def_arr_len \
@@ -620,6 +621,17 @@ $(BIN)/test_test_filter: test/wcc/989_test_filter.c $(BIN)/ww $(BIN)/ww_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
# 989_callarg_typecheck drives `ww build` + `ww test -c` then `<comp> -T`
# on BOTH driver/compiler twins (#24 general call-arg typecheck + #34
# fn-rvalue stamp + #29 rune-lit coerce), so it needs the full cstage +
# wwstage tool sets plus libwwrt for the link.
$(BIN)/test_callarg_typecheck: test/wcc/989_callarg_typecheck.c \
$(BIN)/ww $(BIN)/ww_ww \
$(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_let_global: test/wcc/630_let_global.c $(BIN)/ww $(BIN)/w6c \
$(BIN)/w6a $(BIN)/w6l $(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<

View File

@@ -2999,6 +2999,16 @@ check_file(Checker *c, Node *file)
&& strcmp(d->str, "main") == 0 && d->body != NULL)
err(c, d->pos, "test mode: main is synthesized "
"by -T; remove the explicit main");
/* #24(b): the synth table OWNS `__wwtests` — loud-reject a user
* decl of that name (twin of the `main` reservation above). A user
* __wwtests whose type happens to match run()'s [](str,*fn()void)
* param slips the general call-arg check but still silently shadows
* the synth table; reserve the NAME so the collision is loud
* regardless of type. Any decl kind. */
for (Node *d = file->list; d; d = d->next)
if (d->str && strcmp(d->str, "__wwtests") == 0)
err(c, d->pos, "test mode: __wwtests is reserved "
"by -T; rename the declaration");
/* (c) collect @test fns in file->list order; build one table row
* `("<name>", &<name>)` per validated @test fn. */
Node *rhead = NULL, *rtail = NULL;

View File

@@ -14284,6 +14284,21 @@ fn isstrtname(t: *node) bool = {
return streq(t.str, "str");
};
// tagshape — #24/#37: the coarse variant-shape bucket of a (resolved) type
// node, the AST-side mirror of cgen taggedvariantindext's str/slice shape
// fallback (cgenutil.ww:3062-3070, `wantstr`/`wantslice` over typeisstr/
// typeisslice). Three buckets: 2=slice, 1=str, 0=scalar/other (ptr / struct
// / tuple / chan / fn / int / enum / ...). Used by the concrete→tagged
// aggregate-shape-lenient leg to keep a tagged accept lenient ONLY against a
// shape-compatible variant — same classifier cgen boxes with, so the checker
// accept and the cgen box agree (rule-12: reuse the in-tree classifier).
fn tagshape(t: *node) i32 = {
if (t == nil) { return 0i32; };
if (t.kind == nkind.N_TSLICE) { return 2i32; };
if (isstrtname(t)) { return 1i32; };
return 0i32;
};
// addrfnptrmatches — true iff `ptr` (after alias-resolve) is a
// pointer whose referent resolves to a fn type structurally equal to
// `synth` (a synthetic N_TFN built from a fn decl's ret + params).
@@ -14397,6 +14412,21 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
// #24: a SPREAD variant (`...formattable`) keeps the lenient
// escape — cstage flattens spreads at resolve_type so its
// type_assignable sees the spread's inlined numeric leaves and
// accepts `take(42)` into `(...formattable | bool)`; wwstage
// stays AST-keyed (#115) and cannot flatten, so a confident
// reject here would OVER-reject what cstage accepts (the new c3
// general call-arg check made this path reachable). Mirror the
// tagged→tagged spread escape (:4117). Spread decl-form flatten
// is #199b, deferred.
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
// #23: no DIRECT variant accepts an untyped int -> confident
// reject (mirror cstage type.c:343 `return 0`). ww does NOT
// flatten a nested union variant (#199-alpha non-drill); an int
@@ -14416,6 +14446,18 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
if (streq(du.str, "void")) { return false; };
if (streq(du.str, "str")) { return false; };
};
// #24: untyped int into a known AGGREGATE (slice/array/ptr/fn/chan/
// tuple/struct) — confident reject. `let xs: []int = 5` read the int
// as a 24B slice header (the #24 silent-garbage; the catch-all below
// left it unconfident → silent accept). cstage type_assignable
// rejects untyped_int into a non-numeric aggregate (cmd/wcc/type.c).
// The TTAGGED case is handled above; this is the bare aggregate.
if (du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY
|| du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN
|| du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE
|| du.kind == nkind.N_TSTRUCT) {
return false;
};
// Unknown shapes: stay quiet.
*confident = false;
return true;
@@ -14449,6 +14491,22 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
// #24: spread variant keeps the lenient escape (cstage flattens;
// wwstage can't) — same rationale as the untyped-int arm above.
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
return false;
};
// #24: untyped float into a known AGGREGATE — confident reject (twin
// of the untyped-int aggregate arm above; `let xs: []f64 = 1.5`).
if (du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY
|| du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN
|| du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE
|| du.kind == nkind.N_TSTRUCT) {
return false;
};
*confident = false;
@@ -14471,6 +14529,14 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
if (v.kind == nkind.N_TFN) { return true; };
v = v.next;
};
// #24: spread variant keeps the lenient escape (cstage flattens;
// wwstage can't) — same rationale as the untyped-int arm above.
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
// A5: no nullable (ptr/slice/chan/fn) variant -> confident
// reject (cstage type.c:316 loop returns 0; nil accepts only
// into ptr/slice/chan/fn per type.c:382-385). *confident is
@@ -14512,6 +14578,70 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
// #24: no variant matched. A SCALAR src (known primitive, su is
// N_TNAME) is a CONFIDENT reject — `int` into `(str | bool)` (the
// #23/#199-α path). An AGGREGATE src (ptr/slice/struct/tuple/chan/
// fn — su.kind != N_TNAME) stays LENIENT: wwstage's nominal-lossy
// model can't confirm a cross-module ptr/struct variant (the `stream`
// variant is `*vtable` but io.handle's consumer hands a `*io.vtable`
// from `&cgoutstream.vt`, or a qualified `io.stream` alias — bare-vs-
// qualified NAMED identity, #10/#66; typeeqast can't span it), while
// cstage flattens+resolves and ACCEPTS (io.stream → io.handle =
// (file | stream)). Pre-c3 the loop accepted ANY src via the first
// scalar variant's lenient-true short-circuit; the c3 scalar↔aggregate
// reject removed that crutch, exposing the latent nominal gap, so
// distinguish by src shape here. The handoff's "preserve concrete→
// tagged accept" path.
//
// An AGGREGATE src (su.kind != N_TNAME) stays lenient ONLY against a
// SHAPE-COMPATIBLE variant — tagshape mirrors cgen taggedvariantindext's
// str/slice/scalar-other classifier (cgenutil.ww:3062), so the checker
// accept and the cgen box agree (rule-12). A shape-MISMATCHED aggregate
// (e.g. a `[]int` slice src into a tagged with no slice variant) is a
// CONFIDENT reject, matching cstage's nominal type_assignable; this is
// the reachable win (#24-B, rob/lead-ruled shape-narrowing over the
// blanket aggregate-lenient).
//
// RULE-7 TRACKED RESIDUAL (task #37, behind the #10/#66 nominal arc;
// NEVER silent): the SAME-COARSE-SHAPE leg still OVER-ACCEPTS a cross-
// module SAME-LEAF collision that cstage REJECTS — e.g. `mod1.stream`
// (a `*mod1.wbox`, scalar/other shape) passed where `io2.handle =
// (io2.file | io2.stream)` is wanted (io2.stream is also scalar/other,
// so the shapes match and this stays lenient). cstage rejects it on
// NOMINAL identity; wwstage accepts. PROVEN STRUCTURALLY UNREACHABLE
// here: the tagged-union variant node is a BARE name (`stream`,
// N_TNAME, no module) — BYTE-IDENTICAL for the genuine io2.stream and
// the collision mod1.stream — so isassignable, which is AST-NODE-keyed,
// has no bit to tell them apart; the distinguishing identity lives only
// in the tinfo layer (#66 per-decl TY_NAMED ptr, which cgen's
// flatvariantidxt already uses). The reject becomes reachable ONLY when
// isassignable is converted to nominal-tinfo keying = the #37 /
// #10/#66 work itself, NOT a c3-scope change. (B) shape-narrowing
// shrinks the residual from "all aggregate→tagged" to "same-coarse-
// shape same-leaf" but cannot close the same-shape ptr↔ptr collision.
// This is the LEAF-NAME nominal-collision family also documented at the
// tagged→tagged qualleaf bridge (this fn, below) — the eventual #10/#66
// sweep must convert BOTH sites uniformly (enumerate for the sweep:
// (i) this concrete→tagged shape-lenient leg, (ii) the tagged→tagged
// qualleaf bridge). Pre-c3 the collision was ALSO accepted (call-arg
// ran no check; let/return short-circuited on the scalar variant) — c3
// is NEUTRAL on it.
if (su.kind != nkind.N_TNAME) {
let ss: i32 = tagshape(su);
let sp: *node = du.list;
for (sp != nil) {
let svu: *node = resolvealias(c, unwrapbang(sp));
if (svu != nil) {
if (tagshape(svu) == ss) {
*confident = false;
return true;
};
};
sp = sp.next;
};
// no shape-compatible variant → confident reject (the #24-B win)
return false;
};
return false;
};
// tagged → tagged: structural variant list compare. Skip
@@ -14652,6 +14782,54 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
return false;
};
};
// #24: a known scalar primitive vs a known aggregate (slice / array /
// ptr / fn / chan / tuple / struct), and two aggregates of DIFFERENT
// kinds, are CONFIDENT rejects — mirror cstage type_assignable, which
// separates scalar from aggregate and rejects a kind mismatch (the int
// read as a 24B slice header was the #24 silent-garbage). du/su are
// already alias-RESOLVED (:3943/3944), so `type A = []int` arrives as
// N_TSLICE. The array→slice BORROW (su N_TARRAY into du N_TSLICE),
// untyped / nil / tagged, and the known-primitive-pair / fn-ptr / fn-fn
// shapes all returned above before reaching here. The `&fn` adopt-the-
// alias case (a *fn N_TPTR src into a bare-fn N_TFN dst — different
// aggregate kinds) is rescued at the let/return/call-arg sites by the
// assignableaddrfn UNION, so a reject here is correct (the caller's
// union accepts the genuine &fn). SAME-kind aggregate structural
// mismatches ([]int vs []str, *u8 vs *i32) stay lenient below —
// wwstage's nominal-lossy model can't span them (the #10 gap); cstage
// rejects via structural type_assignable, a filed residual under-reject,
// NOT a new over-reject. A NAMED struct/alias dst that does NOT resolve
// to a known kind stays N_TNAME-non-prim → neither set → lenient.
let dprim: bool = du.kind == nkind.N_TNAME
&& (isnumerictname(du) || isstrtname(du)
|| streq(du.str, "bool") || streq(du.str, "void"));
let sprim: bool = su.kind == nkind.N_TNAME
&& (isnumerictname(su) || isstrtname(su)
|| streq(su.str, "bool") || streq(su.str, "void"));
let daggr: bool = du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY
|| du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN
|| du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE
|| du.kind == nkind.N_TSTRUCT;
let saggr: bool = su.kind == nkind.N_TSLICE || su.kind == nkind.N_TARRAY
|| su.kind == nkind.N_TPTR || su.kind == nkind.N_TFN
|| su.kind == nkind.N_TCHAN || su.kind == nkind.N_TTUPLE
|| su.kind == nkind.N_TSTRUCT;
if (sprim && daggr) { return false; };
if (dprim && saggr) { return false; };
// #24: two aggregates of DIFFERENT kinds → confident reject (array/slice
// into ptr/fn/chan/tuple is the 24B/16B-header misread). EXEMPT a STRUCT
// on either side: wwstage's name-keyed resolvealias mis-resolves a bare
// cross-module same-leaf type name to the WRONG module's struct (#224 —
// `type s = *vtable` in sa vs `type s = struct{}` in sb; sa.read's bare
// `s` param resolves to sb's struct), so a struct-vs-ptr "mismatch" here
// is an artifact of the lossy resolution, not a real type error — cstage
// resolves `s` correctly and ACCEPTS (test 784). Same nominal-lossy
// principle as the concrete→tagged aggregate-lenient arm above; the
// struct-into-ptr genuine mismatch stays a filed #224/#10 under-reject.
if (daggr && saggr && du.kind != su.kind
&& du.kind != nkind.N_TSTRUCT && su.kind != nkind.N_TSTRUCT) {
return false;
};
// Anything else: don't claim confidence.
*confident = false;
return true;
@@ -15246,6 +15424,21 @@ fn calleefndecl(c: *checker, callee: *node) *node = {
// arg flows into the gather as an element, not the slice itself).
fn desugarcallargs(c: *checker, n: *node) void = {
if (n == nil) { return; };
// #24: a 1-arg `free(x)` is the Hare no-op pseudo-builtin (#27), NOT the
// rt 2-arg `free(p: *void, n: u64)` that calleefndecl resolves to in the
// bundle (lib seeds both: the nil-decl builtin at check.ww:137 AND
// rt's @symbol("rt_free") decl). cstage intercepts the builtin by name +
// arity BEFORE call resolution (cmd/wcc/check.c:1650, n->list->next ==
// NULL) and runs NO arg typecheck; exprtype's free arm (:3014) is the
// wwstage twin but runs after this seam. Skip so `free(charset)` /
// `free(slice)` (regex finish #27) isn't checked against rt_free's *void
// param. `free` is the only builtin name with a colliding real decl
// (len/alloc/append/delete/insert keep nil decls → calleefndecl bails).
if (n.lhs != nil) { if (n.lhs.kind == nkind.N_IDENT) {
if (streq(n.lhs.str, "free")) {
if (n.list != nil) { if (n.list.next == nil) { return; }; };
};
}; };
let decl: *node = calleefndecl(c, n.lhs);
if (decl == nil) { return; };
let param: *node = decl.list;
@@ -15265,21 +15458,25 @@ fn desugarcallargs(c: *checker, n: *node) void = {
// param/destination width).
let runet: *node = coercerunelit(c, a, param.lhs);
if (runet != nil) { atype = runet; };
// #258: an array arg into a []T param with a
// MISMATCHED element is not a borrow — loud reject,
// mirror cstage's call-arg type_assignable failure.
// Gated to the array→slice-param shape so wwstage's
// broader call-arg leniency (it runs no general
// param typecheck) is untouched.
let pu: *node = resolvealias(c, unwrapbang(param.lhs));
let au: *node = resolvealias(c, unwrapbang(atype));
if (pu != nil) { if (au != nil) {
if (pu.kind == nkind.N_TSLICE) { if (au.kind == nkind.N_TARRAY) {
if (!typeeqast(c, pu.lhs, au.lhs)) {
errnotassign(c, param.lhs, atype, "argument");
};
}; };
}; };
// #24: GENERAL per-arg assignability — align UP to
// cstage check.c:1867-1870, which type_assignables
// every non-variadic call arg (`argument type %s not
// assignable to %s`). wwstage previously ran NO general
// param typecheck (only a narrow #258 array→slice arm),
// so any mistyped scalar call-arg silently miscompiled
// (an int read as a 24B slice header). The shared
// isassignable SUBSUMES that #258 arm: its N_TSLICE/
// N_TARRAY arm is a confident reject on an element
// mismatch and an accept on a match (the desugar below
// then borrows). Conf-gated + UNIONed with
// assignableaddrfn exactly like the let/return sibling
// sites (:5151/5154, :5222/5225); the spread-tagged
// lenient escape (isassignable :4078) keeps conf=false so
// `take(42)` into `(...formattable | bool)` stays accepted.
let conf: bool = false;
let ok: bool = isassignable(c, param.lhs, atype, &conf);
if (!ok) { if (assignableaddrfn(c, param.lhs, a)) { ok = true; }; };
if (conf) { if (!ok) { errnotassign(c, param.lhs, atype, "argument"); }; };
// #12: overlong array-lit CALL-ARG — `g([1,2,3])`.
// Reject at CHECK time (clean over-fill msg) instead
// of falling to cgen #271's late aggregate-arg loud.
@@ -15324,6 +15521,23 @@ fn checkassign(c: *checker, n: *node) void = {
// width (byte-id-neutral). Removes the getopt `'X': u8` index casts.
let runet: *node = coercerunelit(c, n.rhs, ltn);
if (runet != nil) { rtn = runet; };
// #24/#36 (rule-7 deferred-divergence, NEVER silent): the ASSIGN seam
// does NOT yet route the general conf-gated UNION (isassignable ||
// assignableaddrfn) that the let / return / call-arg seams run — so a
// mistyped bare-assignment `x = some_slice` (a 24B slice header into an
// 8B int slot) is still silently accepted here, the one remaining
// member of the #24 cat-A. cstage DOES check it (cmd/wcc/check.c:1899,
// `cannot assign %s to %s`, every op incl. compound). The union was
// implemented + reverted: it correctly closed `x = slice` and matched
// cstage on `p += 1`, but surfaced a false over-reject of an EXACT-
// signature bare fn assigned to a fn-pointer struct field (lib/log
// `r.logger.println = stdprintln`) because typeeqast compares fn types
// at the AST level and cannot match a variadic + module-qualified-param
// fn signature (the #178 divergence, self-flagged at the typeeqast
// N_TFN arm). So the assign seam is BLOCKED on #178 and filed as task
// #36 (the bounded #178 typeeqast fn-compare fix, task #35, lands
// first). Until then this seam runs only coercerunelit + the #258
// array→slice desugar below.
// #31/#33: bare array-literal rhs has no backing — loud-reject
// (supported only at a `let`).
if (!rejectarrlitborrow(c, ltn, n.rhs)) {
@@ -16099,6 +16313,19 @@ export fn checkfile(c: *checker, file: *node) void = {
cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n");
c.errs += 1;
};
// #24(b): the synth table OWNS `__wwtests` — loud-reject a user
// decl of that name (mirror the `main` reservation; cstage
// cmd/wcc/check.c). A user `__wwtests` whose type HAPPENS to
// match run()'s `[](str, *fn()void)` param slips the general
// call-arg check (a) but still silently shadows the synth table,
// so the synth `run(__wwtests)` iterates the user's table, not
// the collected @tests — reserve the NAME so the collision is
// loud regardless of type. Any decl kind (const/let/fn).
if (streq(u.str, "__wwtests")) {
cerr(u.file);
cerr(": error: test mode: __wwtests is reserved by -T; rename the declaration\n");
c.errs += 1;
};
u = u.next;
};
// (c) collect @test fns in file.list order; build one table row

View File

@@ -3864,6 +3864,21 @@ fn isstrtname(t: *node) bool = {
return streq(t.str, "str");
};
// tagshape — #24/#37: the coarse variant-shape bucket of a (resolved) type
// node, the AST-side mirror of cgen taggedvariantindext's str/slice shape
// fallback (cgenutil.ww:3062-3070, `wantstr`/`wantslice` over typeisstr/
// typeisslice). Three buckets: 2=slice, 1=str, 0=scalar/other (ptr / struct
// / tuple / chan / fn / int / enum / ...). Used by the concrete→tagged
// aggregate-shape-lenient leg to keep a tagged accept lenient ONLY against a
// shape-compatible variant — same classifier cgen boxes with, so the checker
// accept and the cgen box agree (rule-12: reuse the in-tree classifier).
fn tagshape(t: *node) i32 = {
if (t == nil) { return 0i32; };
if (t.kind == nkind.N_TSLICE) { return 2i32; };
if (isstrtname(t)) { return 1i32; };
return 0i32;
};
// addrfnptrmatches — true iff `ptr` (after alias-resolve) is a
// pointer whose referent resolves to a fn type structurally equal to
// `synth` (a synthetic N_TFN built from a fn decl's ret + params).
@@ -3977,6 +3992,21 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
// #24: a SPREAD variant (`...formattable`) keeps the lenient
// escape — cstage flattens spreads at resolve_type so its
// type_assignable sees the spread's inlined numeric leaves and
// accepts `take(42)` into `(...formattable | bool)`; wwstage
// stays AST-keyed (#115) and cannot flatten, so a confident
// reject here would OVER-reject what cstage accepts (the new c3
// general call-arg check made this path reachable). Mirror the
// tagged→tagged spread escape (:4117). Spread decl-form flatten
// is #199b, deferred.
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
// #23: no DIRECT variant accepts an untyped int -> confident
// reject (mirror cstage type.c:343 `return 0`). ww does NOT
// flatten a nested union variant (#199-alpha non-drill); an int
@@ -3996,6 +4026,18 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
if (streq(du.str, "void")) { return false; };
if (streq(du.str, "str")) { return false; };
};
// #24: untyped int into a known AGGREGATE (slice/array/ptr/fn/chan/
// tuple/struct) — confident reject. `let xs: []int = 5` read the int
// as a 24B slice header (the #24 silent-garbage; the catch-all below
// left it unconfident → silent accept). cstage type_assignable
// rejects untyped_int into a non-numeric aggregate (cmd/wcc/type.c).
// The TTAGGED case is handled above; this is the bare aggregate.
if (du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY
|| du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN
|| du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE
|| du.kind == nkind.N_TSTRUCT) {
return false;
};
// Unknown shapes: stay quiet.
*confident = false;
return true;
@@ -4029,6 +4071,22 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
// #24: spread variant keeps the lenient escape (cstage flattens;
// wwstage can't) — same rationale as the untyped-int arm above.
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
return false;
};
// #24: untyped float into a known AGGREGATE — confident reject (twin
// of the untyped-int aggregate arm above; `let xs: []f64 = 1.5`).
if (du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY
|| du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN
|| du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE
|| du.kind == nkind.N_TSTRUCT) {
return false;
};
*confident = false;
@@ -4051,6 +4109,14 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
if (v.kind == nkind.N_TFN) { return true; };
v = v.next;
};
// #24: spread variant keeps the lenient escape (cstage flattens;
// wwstage can't) — same rationale as the untyped-int arm above.
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
// A5: no nullable (ptr/slice/chan/fn) variant -> confident
// reject (cstage type.c:316 loop returns 0; nil accepts only
// into ptr/slice/chan/fn per type.c:382-385). *confident is
@@ -4092,6 +4158,70 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
// #24: no variant matched. A SCALAR src (known primitive, su is
// N_TNAME) is a CONFIDENT reject — `int` into `(str | bool)` (the
// #23/#199-α path). An AGGREGATE src (ptr/slice/struct/tuple/chan/
// fn — su.kind != N_TNAME) stays LENIENT: wwstage's nominal-lossy
// model can't confirm a cross-module ptr/struct variant (the `stream`
// variant is `*vtable` but io.handle's consumer hands a `*io.vtable`
// from `&cgoutstream.vt`, or a qualified `io.stream` alias — bare-vs-
// qualified NAMED identity, #10/#66; typeeqast can't span it), while
// cstage flattens+resolves and ACCEPTS (io.stream → io.handle =
// (file | stream)). Pre-c3 the loop accepted ANY src via the first
// scalar variant's lenient-true short-circuit; the c3 scalar↔aggregate
// reject removed that crutch, exposing the latent nominal gap, so
// distinguish by src shape here. The handoff's "preserve concrete→
// tagged accept" path.
//
// An AGGREGATE src (su.kind != N_TNAME) stays lenient ONLY against a
// SHAPE-COMPATIBLE variant — tagshape mirrors cgen taggedvariantindext's
// str/slice/scalar-other classifier (cgenutil.ww:3062), so the checker
// accept and the cgen box agree (rule-12). A shape-MISMATCHED aggregate
// (e.g. a `[]int` slice src into a tagged with no slice variant) is a
// CONFIDENT reject, matching cstage's nominal type_assignable; this is
// the reachable win (#24-B, rob/lead-ruled shape-narrowing over the
// blanket aggregate-lenient).
//
// RULE-7 TRACKED RESIDUAL (task #37, behind the #10/#66 nominal arc;
// NEVER silent): the SAME-COARSE-SHAPE leg still OVER-ACCEPTS a cross-
// module SAME-LEAF collision that cstage REJECTS — e.g. `mod1.stream`
// (a `*mod1.wbox`, scalar/other shape) passed where `io2.handle =
// (io2.file | io2.stream)` is wanted (io2.stream is also scalar/other,
// so the shapes match and this stays lenient). cstage rejects it on
// NOMINAL identity; wwstage accepts. PROVEN STRUCTURALLY UNREACHABLE
// here: the tagged-union variant node is a BARE name (`stream`,
// N_TNAME, no module) — BYTE-IDENTICAL for the genuine io2.stream and
// the collision mod1.stream — so isassignable, which is AST-NODE-keyed,
// has no bit to tell them apart; the distinguishing identity lives only
// in the tinfo layer (#66 per-decl TY_NAMED ptr, which cgen's
// flatvariantidxt already uses). The reject becomes reachable ONLY when
// isassignable is converted to nominal-tinfo keying = the #37 /
// #10/#66 work itself, NOT a c3-scope change. (B) shape-narrowing
// shrinks the residual from "all aggregate→tagged" to "same-coarse-
// shape same-leaf" but cannot close the same-shape ptr↔ptr collision.
// This is the LEAF-NAME nominal-collision family also documented at the
// tagged→tagged qualleaf bridge (this fn, below) — the eventual #10/#66
// sweep must convert BOTH sites uniformly (enumerate for the sweep:
// (i) this concrete→tagged shape-lenient leg, (ii) the tagged→tagged
// qualleaf bridge). Pre-c3 the collision was ALSO accepted (call-arg
// ran no check; let/return short-circuited on the scalar variant) — c3
// is NEUTRAL on it.
if (su.kind != nkind.N_TNAME) {
let ss: i32 = tagshape(su);
let sp: *node = du.list;
for (sp != nil) {
let svu: *node = resolvealias(c, unwrapbang(sp));
if (svu != nil) {
if (tagshape(svu) == ss) {
*confident = false;
return true;
};
};
sp = sp.next;
};
// no shape-compatible variant → confident reject (the #24-B win)
return false;
};
return false;
};
// tagged → tagged: structural variant list compare. Skip
@@ -4232,6 +4362,54 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
return false;
};
};
// #24: a known scalar primitive vs a known aggregate (slice / array /
// ptr / fn / chan / tuple / struct), and two aggregates of DIFFERENT
// kinds, are CONFIDENT rejects — mirror cstage type_assignable, which
// separates scalar from aggregate and rejects a kind mismatch (the int
// read as a 24B slice header was the #24 silent-garbage). du/su are
// already alias-RESOLVED (:3943/3944), so `type A = []int` arrives as
// N_TSLICE. The array→slice BORROW (su N_TARRAY into du N_TSLICE),
// untyped / nil / tagged, and the known-primitive-pair / fn-ptr / fn-fn
// shapes all returned above before reaching here. The `&fn` adopt-the-
// alias case (a *fn N_TPTR src into a bare-fn N_TFN dst — different
// aggregate kinds) is rescued at the let/return/call-arg sites by the
// assignableaddrfn UNION, so a reject here is correct (the caller's
// union accepts the genuine &fn). SAME-kind aggregate structural
// mismatches ([]int vs []str, *u8 vs *i32) stay lenient below —
// wwstage's nominal-lossy model can't span them (the #10 gap); cstage
// rejects via structural type_assignable, a filed residual under-reject,
// NOT a new over-reject. A NAMED struct/alias dst that does NOT resolve
// to a known kind stays N_TNAME-non-prim → neither set → lenient.
let dprim: bool = du.kind == nkind.N_TNAME
&& (isnumerictname(du) || isstrtname(du)
|| streq(du.str, "bool") || streq(du.str, "void"));
let sprim: bool = su.kind == nkind.N_TNAME
&& (isnumerictname(su) || isstrtname(su)
|| streq(su.str, "bool") || streq(su.str, "void"));
let daggr: bool = du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY
|| du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN
|| du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE
|| du.kind == nkind.N_TSTRUCT;
let saggr: bool = su.kind == nkind.N_TSLICE || su.kind == nkind.N_TARRAY
|| su.kind == nkind.N_TPTR || su.kind == nkind.N_TFN
|| su.kind == nkind.N_TCHAN || su.kind == nkind.N_TTUPLE
|| su.kind == nkind.N_TSTRUCT;
if (sprim && daggr) { return false; };
if (dprim && saggr) { return false; };
// #24: two aggregates of DIFFERENT kinds → confident reject (array/slice
// into ptr/fn/chan/tuple is the 24B/16B-header misread). EXEMPT a STRUCT
// on either side: wwstage's name-keyed resolvealias mis-resolves a bare
// cross-module same-leaf type name to the WRONG module's struct (#224 —
// `type s = *vtable` in sa vs `type s = struct{}` in sb; sa.read's bare
// `s` param resolves to sb's struct), so a struct-vs-ptr "mismatch" here
// is an artifact of the lossy resolution, not a real type error — cstage
// resolves `s` correctly and ACCEPTS (test 784). Same nominal-lossy
// principle as the concrete→tagged aggregate-lenient arm above; the
// struct-into-ptr genuine mismatch stays a filed #224/#10 under-reject.
if (daggr && saggr && du.kind != su.kind
&& du.kind != nkind.N_TSTRUCT && su.kind != nkind.N_TSTRUCT) {
return false;
};
// Anything else: don't claim confidence.
*confident = false;
return true;
@@ -4826,6 +5004,21 @@ fn calleefndecl(c: *checker, callee: *node) *node = {
// arg flows into the gather as an element, not the slice itself).
fn desugarcallargs(c: *checker, n: *node) void = {
if (n == nil) { return; };
// #24: a 1-arg `free(x)` is the Hare no-op pseudo-builtin (#27), NOT the
// rt 2-arg `free(p: *void, n: u64)` that calleefndecl resolves to in the
// bundle (lib seeds both: the nil-decl builtin at check.ww:137 AND
// rt's @symbol("rt_free") decl). cstage intercepts the builtin by name +
// arity BEFORE call resolution (cmd/wcc/check.c:1650, n->list->next ==
// NULL) and runs NO arg typecheck; exprtype's free arm (:3014) is the
// wwstage twin but runs after this seam. Skip so `free(charset)` /
// `free(slice)` (regex finish #27) isn't checked against rt_free's *void
// param. `free` is the only builtin name with a colliding real decl
// (len/alloc/append/delete/insert keep nil decls → calleefndecl bails).
if (n.lhs != nil) { if (n.lhs.kind == nkind.N_IDENT) {
if (streq(n.lhs.str, "free")) {
if (n.list != nil) { if (n.list.next == nil) { return; }; };
};
}; };
let decl: *node = calleefndecl(c, n.lhs);
if (decl == nil) { return; };
let param: *node = decl.list;
@@ -4845,21 +5038,25 @@ fn desugarcallargs(c: *checker, n: *node) void = {
// param/destination width).
let runet: *node = coercerunelit(c, a, param.lhs);
if (runet != nil) { atype = runet; };
// #258: an array arg into a []T param with a
// MISMATCHED element is not a borrow — loud reject,
// mirror cstage's call-arg type_assignable failure.
// Gated to the array→slice-param shape so wwstage's
// broader call-arg leniency (it runs no general
// param typecheck) is untouched.
let pu: *node = resolvealias(c, unwrapbang(param.lhs));
let au: *node = resolvealias(c, unwrapbang(atype));
if (pu != nil) { if (au != nil) {
if (pu.kind == nkind.N_TSLICE) { if (au.kind == nkind.N_TARRAY) {
if (!typeeqast(c, pu.lhs, au.lhs)) {
errnotassign(c, param.lhs, atype, "argument");
};
}; };
}; };
// #24: GENERAL per-arg assignability — align UP to
// cstage check.c:1867-1870, which type_assignables
// every non-variadic call arg (`argument type %s not
// assignable to %s`). wwstage previously ran NO general
// param typecheck (only a narrow #258 array→slice arm),
// so any mistyped scalar call-arg silently miscompiled
// (an int read as a 24B slice header). The shared
// isassignable SUBSUMES that #258 arm: its N_TSLICE/
// N_TARRAY arm is a confident reject on an element
// mismatch and an accept on a match (the desugar below
// then borrows). Conf-gated + UNIONed with
// assignableaddrfn exactly like the let/return sibling
// sites (:5151/5154, :5222/5225); the spread-tagged
// lenient escape (isassignable :4078) keeps conf=false so
// `take(42)` into `(...formattable | bool)` stays accepted.
let conf: bool = false;
let ok: bool = isassignable(c, param.lhs, atype, &conf);
if (!ok) { if (assignableaddrfn(c, param.lhs, a)) { ok = true; }; };
if (conf) { if (!ok) { errnotassign(c, param.lhs, atype, "argument"); }; };
// #12: overlong array-lit CALL-ARG — `g([1,2,3])`.
// Reject at CHECK time (clean over-fill msg) instead
// of falling to cgen #271's late aggregate-arg loud.
@@ -4904,6 +5101,23 @@ fn checkassign(c: *checker, n: *node) void = {
// width (byte-id-neutral). Removes the getopt `'X': u8` index casts.
let runet: *node = coercerunelit(c, n.rhs, ltn);
if (runet != nil) { rtn = runet; };
// #24/#36 (rule-7 deferred-divergence, NEVER silent): the ASSIGN seam
// does NOT yet route the general conf-gated UNION (isassignable ||
// assignableaddrfn) that the let / return / call-arg seams run — so a
// mistyped bare-assignment `x = some_slice` (a 24B slice header into an
// 8B int slot) is still silently accepted here, the one remaining
// member of the #24 cat-A. cstage DOES check it (cmd/wcc/check.c:1899,
// `cannot assign %s to %s`, every op incl. compound). The union was
// implemented + reverted: it correctly closed `x = slice` and matched
// cstage on `p += 1`, but surfaced a false over-reject of an EXACT-
// signature bare fn assigned to a fn-pointer struct field (lib/log
// `r.logger.println = stdprintln`) because typeeqast compares fn types
// at the AST level and cannot match a variadic + module-qualified-param
// fn signature (the #178 divergence, self-flagged at the typeeqast
// N_TFN arm). So the assign seam is BLOCKED on #178 and filed as task
// #36 (the bounded #178 typeeqast fn-compare fix, task #35, lands
// first). Until then this seam runs only coercerunelit + the #258
// array→slice desugar below.
// #31/#33: bare array-literal rhs has no backing — loud-reject
// (supported only at a `let`).
if (!rejectarrlitborrow(c, ltn, n.rhs)) {
@@ -5679,6 +5893,19 @@ export fn checkfile(c: *checker, file: *node) void = {
cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n");
c.errs += 1;
};
// #24(b): the synth table OWNS `__wwtests` — loud-reject a user
// decl of that name (mirror the `main` reservation; cstage
// cmd/wcc/check.c). A user `__wwtests` whose type HAPPENS to
// match run()'s `[](str, *fn()void)` param slips the general
// call-arg check (a) but still silently shadows the synth table,
// so the synth `run(__wwtests)` iterates the user's table, not
// the collected @tests — reserve the NAME so the collision is
// loud regardless of type. Any decl kind (const/let/fn).
if (streq(u.str, "__wwtests")) {
cerr(u.file);
cerr(": error: test mode: __wwtests is reserved by -T; rename the declaration\n");
c.errs += 1;
};
u = u.next;
};
// (c) collect @test fns in file.list order; build one table row

View File

@@ -14284,6 +14284,21 @@ fn isstrtname(t: *node) bool = {
return streq(t.str, "str");
};
// tagshape — #24/#37: the coarse variant-shape bucket of a (resolved) type
// node, the AST-side mirror of cgen taggedvariantindext's str/slice shape
// fallback (cgenutil.ww:3062-3070, `wantstr`/`wantslice` over typeisstr/
// typeisslice). Three buckets: 2=slice, 1=str, 0=scalar/other (ptr / struct
// / tuple / chan / fn / int / enum / ...). Used by the concrete→tagged
// aggregate-shape-lenient leg to keep a tagged accept lenient ONLY against a
// shape-compatible variant — same classifier cgen boxes with, so the checker
// accept and the cgen box agree (rule-12: reuse the in-tree classifier).
fn tagshape(t: *node) i32 = {
if (t == nil) { return 0i32; };
if (t.kind == nkind.N_TSLICE) { return 2i32; };
if (isstrtname(t)) { return 1i32; };
return 0i32;
};
// addrfnptrmatches — true iff `ptr` (after alias-resolve) is a
// pointer whose referent resolves to a fn type structurally equal to
// `synth` (a synthetic N_TFN built from a fn decl's ret + params).
@@ -14397,6 +14412,21 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
// #24: a SPREAD variant (`...formattable`) keeps the lenient
// escape — cstage flattens spreads at resolve_type so its
// type_assignable sees the spread's inlined numeric leaves and
// accepts `take(42)` into `(...formattable | bool)`; wwstage
// stays AST-keyed (#115) and cannot flatten, so a confident
// reject here would OVER-reject what cstage accepts (the new c3
// general call-arg check made this path reachable). Mirror the
// tagged→tagged spread escape (:4117). Spread decl-form flatten
// is #199b, deferred.
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
// #23: no DIRECT variant accepts an untyped int -> confident
// reject (mirror cstage type.c:343 `return 0`). ww does NOT
// flatten a nested union variant (#199-alpha non-drill); an int
@@ -14416,6 +14446,18 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
if (streq(du.str, "void")) { return false; };
if (streq(du.str, "str")) { return false; };
};
// #24: untyped int into a known AGGREGATE (slice/array/ptr/fn/chan/
// tuple/struct) — confident reject. `let xs: []int = 5` read the int
// as a 24B slice header (the #24 silent-garbage; the catch-all below
// left it unconfident → silent accept). cstage type_assignable
// rejects untyped_int into a non-numeric aggregate (cmd/wcc/type.c).
// The TTAGGED case is handled above; this is the bare aggregate.
if (du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY
|| du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN
|| du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE
|| du.kind == nkind.N_TSTRUCT) {
return false;
};
// Unknown shapes: stay quiet.
*confident = false;
return true;
@@ -14449,6 +14491,22 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
// #24: spread variant keeps the lenient escape (cstage flattens;
// wwstage can't) — same rationale as the untyped-int arm above.
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
return false;
};
// #24: untyped float into a known AGGREGATE — confident reject (twin
// of the untyped-int aggregate arm above; `let xs: []f64 = 1.5`).
if (du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY
|| du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN
|| du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE
|| du.kind == nkind.N_TSTRUCT) {
return false;
};
*confident = false;
@@ -14471,6 +14529,14 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
if (v.kind == nkind.N_TFN) { return true; };
v = v.next;
};
// #24: spread variant keeps the lenient escape (cstage flattens;
// wwstage can't) — same rationale as the untyped-int arm above.
for (let p: *node = du.list; p != nil; p = p.next) {
if (p.op == tkind.TK_ELLIPSIS) {
*confident = false;
return true;
};
};
// A5: no nullable (ptr/slice/chan/fn) variant -> confident
// reject (cstage type.c:316 loop returns 0; nil accepts only
// into ptr/slice/chan/fn per type.c:382-385). *confident is
@@ -14512,6 +14578,70 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
};
v = v.next;
};
// #24: no variant matched. A SCALAR src (known primitive, su is
// N_TNAME) is a CONFIDENT reject — `int` into `(str | bool)` (the
// #23/#199-α path). An AGGREGATE src (ptr/slice/struct/tuple/chan/
// fn — su.kind != N_TNAME) stays LENIENT: wwstage's nominal-lossy
// model can't confirm a cross-module ptr/struct variant (the `stream`
// variant is `*vtable` but io.handle's consumer hands a `*io.vtable`
// from `&cgoutstream.vt`, or a qualified `io.stream` alias — bare-vs-
// qualified NAMED identity, #10/#66; typeeqast can't span it), while
// cstage flattens+resolves and ACCEPTS (io.stream → io.handle =
// (file | stream)). Pre-c3 the loop accepted ANY src via the first
// scalar variant's lenient-true short-circuit; the c3 scalar↔aggregate
// reject removed that crutch, exposing the latent nominal gap, so
// distinguish by src shape here. The handoff's "preserve concrete→
// tagged accept" path.
//
// An AGGREGATE src (su.kind != N_TNAME) stays lenient ONLY against a
// SHAPE-COMPATIBLE variant — tagshape mirrors cgen taggedvariantindext's
// str/slice/scalar-other classifier (cgenutil.ww:3062), so the checker
// accept and the cgen box agree (rule-12). A shape-MISMATCHED aggregate
// (e.g. a `[]int` slice src into a tagged with no slice variant) is a
// CONFIDENT reject, matching cstage's nominal type_assignable; this is
// the reachable win (#24-B, rob/lead-ruled shape-narrowing over the
// blanket aggregate-lenient).
//
// RULE-7 TRACKED RESIDUAL (task #37, behind the #10/#66 nominal arc;
// NEVER silent): the SAME-COARSE-SHAPE leg still OVER-ACCEPTS a cross-
// module SAME-LEAF collision that cstage REJECTS — e.g. `mod1.stream`
// (a `*mod1.wbox`, scalar/other shape) passed where `io2.handle =
// (io2.file | io2.stream)` is wanted (io2.stream is also scalar/other,
// so the shapes match and this stays lenient). cstage rejects it on
// NOMINAL identity; wwstage accepts. PROVEN STRUCTURALLY UNREACHABLE
// here: the tagged-union variant node is a BARE name (`stream`,
// N_TNAME, no module) — BYTE-IDENTICAL for the genuine io2.stream and
// the collision mod1.stream — so isassignable, which is AST-NODE-keyed,
// has no bit to tell them apart; the distinguishing identity lives only
// in the tinfo layer (#66 per-decl TY_NAMED ptr, which cgen's
// flatvariantidxt already uses). The reject becomes reachable ONLY when
// isassignable is converted to nominal-tinfo keying = the #37 /
// #10/#66 work itself, NOT a c3-scope change. (B) shape-narrowing
// shrinks the residual from "all aggregate→tagged" to "same-coarse-
// shape same-leaf" but cannot close the same-shape ptr↔ptr collision.
// This is the LEAF-NAME nominal-collision family also documented at the
// tagged→tagged qualleaf bridge (this fn, below) — the eventual #10/#66
// sweep must convert BOTH sites uniformly (enumerate for the sweep:
// (i) this concrete→tagged shape-lenient leg, (ii) the tagged→tagged
// qualleaf bridge). Pre-c3 the collision was ALSO accepted (call-arg
// ran no check; let/return short-circuited on the scalar variant) — c3
// is NEUTRAL on it.
if (su.kind != nkind.N_TNAME) {
let ss: i32 = tagshape(su);
let sp: *node = du.list;
for (sp != nil) {
let svu: *node = resolvealias(c, unwrapbang(sp));
if (svu != nil) {
if (tagshape(svu) == ss) {
*confident = false;
return true;
};
};
sp = sp.next;
};
// no shape-compatible variant → confident reject (the #24-B win)
return false;
};
return false;
};
// tagged → tagged: structural variant list compare. Skip
@@ -14652,6 +14782,54 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
return false;
};
};
// #24: a known scalar primitive vs a known aggregate (slice / array /
// ptr / fn / chan / tuple / struct), and two aggregates of DIFFERENT
// kinds, are CONFIDENT rejects — mirror cstage type_assignable, which
// separates scalar from aggregate and rejects a kind mismatch (the int
// read as a 24B slice header was the #24 silent-garbage). du/su are
// already alias-RESOLVED (:3943/3944), so `type A = []int` arrives as
// N_TSLICE. The array→slice BORROW (su N_TARRAY into du N_TSLICE),
// untyped / nil / tagged, and the known-primitive-pair / fn-ptr / fn-fn
// shapes all returned above before reaching here. The `&fn` adopt-the-
// alias case (a *fn N_TPTR src into a bare-fn N_TFN dst — different
// aggregate kinds) is rescued at the let/return/call-arg sites by the
// assignableaddrfn UNION, so a reject here is correct (the caller's
// union accepts the genuine &fn). SAME-kind aggregate structural
// mismatches ([]int vs []str, *u8 vs *i32) stay lenient below —
// wwstage's nominal-lossy model can't span them (the #10 gap); cstage
// rejects via structural type_assignable, a filed residual under-reject,
// NOT a new over-reject. A NAMED struct/alias dst that does NOT resolve
// to a known kind stays N_TNAME-non-prim → neither set → lenient.
let dprim: bool = du.kind == nkind.N_TNAME
&& (isnumerictname(du) || isstrtname(du)
|| streq(du.str, "bool") || streq(du.str, "void"));
let sprim: bool = su.kind == nkind.N_TNAME
&& (isnumerictname(su) || isstrtname(su)
|| streq(su.str, "bool") || streq(su.str, "void"));
let daggr: bool = du.kind == nkind.N_TSLICE || du.kind == nkind.N_TARRAY
|| du.kind == nkind.N_TPTR || du.kind == nkind.N_TFN
|| du.kind == nkind.N_TCHAN || du.kind == nkind.N_TTUPLE
|| du.kind == nkind.N_TSTRUCT;
let saggr: bool = su.kind == nkind.N_TSLICE || su.kind == nkind.N_TARRAY
|| su.kind == nkind.N_TPTR || su.kind == nkind.N_TFN
|| su.kind == nkind.N_TCHAN || su.kind == nkind.N_TTUPLE
|| su.kind == nkind.N_TSTRUCT;
if (sprim && daggr) { return false; };
if (dprim && saggr) { return false; };
// #24: two aggregates of DIFFERENT kinds → confident reject (array/slice
// into ptr/fn/chan/tuple is the 24B/16B-header misread). EXEMPT a STRUCT
// on either side: wwstage's name-keyed resolvealias mis-resolves a bare
// cross-module same-leaf type name to the WRONG module's struct (#224 —
// `type s = *vtable` in sa vs `type s = struct{}` in sb; sa.read's bare
// `s` param resolves to sb's struct), so a struct-vs-ptr "mismatch" here
// is an artifact of the lossy resolution, not a real type error — cstage
// resolves `s` correctly and ACCEPTS (test 784). Same nominal-lossy
// principle as the concrete→tagged aggregate-lenient arm above; the
// struct-into-ptr genuine mismatch stays a filed #224/#10 under-reject.
if (daggr && saggr && du.kind != su.kind
&& du.kind != nkind.N_TSTRUCT && su.kind != nkind.N_TSTRUCT) {
return false;
};
// Anything else: don't claim confidence.
*confident = false;
return true;
@@ -15246,6 +15424,21 @@ fn calleefndecl(c: *checker, callee: *node) *node = {
// arg flows into the gather as an element, not the slice itself).
fn desugarcallargs(c: *checker, n: *node) void = {
if (n == nil) { return; };
// #24: a 1-arg `free(x)` is the Hare no-op pseudo-builtin (#27), NOT the
// rt 2-arg `free(p: *void, n: u64)` that calleefndecl resolves to in the
// bundle (lib seeds both: the nil-decl builtin at check.ww:137 AND
// rt's @symbol("rt_free") decl). cstage intercepts the builtin by name +
// arity BEFORE call resolution (cmd/wcc/check.c:1650, n->list->next ==
// NULL) and runs NO arg typecheck; exprtype's free arm (:3014) is the
// wwstage twin but runs after this seam. Skip so `free(charset)` /
// `free(slice)` (regex finish #27) isn't checked against rt_free's *void
// param. `free` is the only builtin name with a colliding real decl
// (len/alloc/append/delete/insert keep nil decls → calleefndecl bails).
if (n.lhs != nil) { if (n.lhs.kind == nkind.N_IDENT) {
if (streq(n.lhs.str, "free")) {
if (n.list != nil) { if (n.list.next == nil) { return; }; };
};
}; };
let decl: *node = calleefndecl(c, n.lhs);
if (decl == nil) { return; };
let param: *node = decl.list;
@@ -15265,21 +15458,25 @@ fn desugarcallargs(c: *checker, n: *node) void = {
// param/destination width).
let runet: *node = coercerunelit(c, a, param.lhs);
if (runet != nil) { atype = runet; };
// #258: an array arg into a []T param with a
// MISMATCHED element is not a borrow — loud reject,
// mirror cstage's call-arg type_assignable failure.
// Gated to the array→slice-param shape so wwstage's
// broader call-arg leniency (it runs no general
// param typecheck) is untouched.
let pu: *node = resolvealias(c, unwrapbang(param.lhs));
let au: *node = resolvealias(c, unwrapbang(atype));
if (pu != nil) { if (au != nil) {
if (pu.kind == nkind.N_TSLICE) { if (au.kind == nkind.N_TARRAY) {
if (!typeeqast(c, pu.lhs, au.lhs)) {
errnotassign(c, param.lhs, atype, "argument");
};
}; };
}; };
// #24: GENERAL per-arg assignability — align UP to
// cstage check.c:1867-1870, which type_assignables
// every non-variadic call arg (`argument type %s not
// assignable to %s`). wwstage previously ran NO general
// param typecheck (only a narrow #258 array→slice arm),
// so any mistyped scalar call-arg silently miscompiled
// (an int read as a 24B slice header). The shared
// isassignable SUBSUMES that #258 arm: its N_TSLICE/
// N_TARRAY arm is a confident reject on an element
// mismatch and an accept on a match (the desugar below
// then borrows). Conf-gated + UNIONed with
// assignableaddrfn exactly like the let/return sibling
// sites (:5151/5154, :5222/5225); the spread-tagged
// lenient escape (isassignable :4078) keeps conf=false so
// `take(42)` into `(...formattable | bool)` stays accepted.
let conf: bool = false;
let ok: bool = isassignable(c, param.lhs, atype, &conf);
if (!ok) { if (assignableaddrfn(c, param.lhs, a)) { ok = true; }; };
if (conf) { if (!ok) { errnotassign(c, param.lhs, atype, "argument"); }; };
// #12: overlong array-lit CALL-ARG — `g([1,2,3])`.
// Reject at CHECK time (clean over-fill msg) instead
// of falling to cgen #271's late aggregate-arg loud.
@@ -15324,6 +15521,23 @@ fn checkassign(c: *checker, n: *node) void = {
// width (byte-id-neutral). Removes the getopt `'X': u8` index casts.
let runet: *node = coercerunelit(c, n.rhs, ltn);
if (runet != nil) { rtn = runet; };
// #24/#36 (rule-7 deferred-divergence, NEVER silent): the ASSIGN seam
// does NOT yet route the general conf-gated UNION (isassignable ||
// assignableaddrfn) that the let / return / call-arg seams run — so a
// mistyped bare-assignment `x = some_slice` (a 24B slice header into an
// 8B int slot) is still silently accepted here, the one remaining
// member of the #24 cat-A. cstage DOES check it (cmd/wcc/check.c:1899,
// `cannot assign %s to %s`, every op incl. compound). The union was
// implemented + reverted: it correctly closed `x = slice` and matched
// cstage on `p += 1`, but surfaced a false over-reject of an EXACT-
// signature bare fn assigned to a fn-pointer struct field (lib/log
// `r.logger.println = stdprintln`) because typeeqast compares fn types
// at the AST level and cannot match a variadic + module-qualified-param
// fn signature (the #178 divergence, self-flagged at the typeeqast
// N_TFN arm). So the assign seam is BLOCKED on #178 and filed as task
// #36 (the bounded #178 typeeqast fn-compare fix, task #35, lands
// first). Until then this seam runs only coercerunelit + the #258
// array→slice desugar below.
// #31/#33: bare array-literal rhs has no backing — loud-reject
// (supported only at a `let`).
if (!rejectarrlitborrow(c, ltn, n.rhs)) {
@@ -16099,6 +16313,19 @@ export fn checkfile(c: *checker, file: *node) void = {
cerr(": error: test mode: main is synthesized by -T; remove the explicit main\n");
c.errs += 1;
};
// #24(b): the synth table OWNS `__wwtests` — loud-reject a user
// decl of that name (mirror the `main` reservation; cstage
// cmd/wcc/check.c). A user `__wwtests` whose type HAPPENS to
// match run()'s `[](str, *fn()void)` param slips the general
// call-arg check (a) but still silently shadows the synth table,
// so the synth `run(__wwtests)` iterates the user's table, not
// the collected @tests — reserve the NAME so the collision is
// loud regardless of type. Any decl kind (const/let/fn).
if (streq(u.str, "__wwtests")) {
cerr(u.file);
cerr(": error: test mode: __wwtests is reserved by -T; rename the declaration\n");
c.errs += 1;
};
u = u.next;
};
// (c) collect @test fns in file.list order; build one table row

View File

@@ -0,0 +1,543 @@
/*
* 989_callarg_typecheck — #24: wwstage align UP to cstage on GENERAL
* call-argument assignability (CHECKER-ONLY, cat-A silent-miscompile).
*
* THE BUG: wwstage ran NO general call-arg typecheck. desugarcallargs
* (selfhost/cmd/wcc/check.ww) only had the narrow #258 array→slice arm,
* so ANY mistyped non-array scalar call-arg was SILENTLY accepted: an
* `int` passed where a `[]T` param expects a 24B slice header builds
* rc=0 and runs garbage (the int's 8 bytes read as the slice .len/.ptr).
* cstage rejects every non-variadic arg at cmd/wcc/check.c:1867-1870
* (`argument type %s not assignable to %s`).
*
* The -T face is the headline silent miscompile: a user
* `const __wwtests: int` + ≥1 @test fn shadows the synth test table, so
* the synth `run(__wwtests)` passes the int 99 where run() wants the
* `[](str, *fn() void)` table — cstage rejected it loud, wwstage
* silently built a broken binary that iterated 99 as a slice .len and
* printed 63 garbage FAIL rows / crashed.
*
* THE FIX is two stacked concerns, both align-UP-to-cstage:
* (a) general per-arg isassignable at the desugarcallargs choke-point,
* mirroring check.c:1867 (conf-gated like the let/return sibling
* sites; subsumes the old narrow #258 reject arm).
* (b) reserve the synth `__wwtests` name under -T in BOTH stages,
* mirroring the `main` reservation (check.c:2996 / check.ww). A
* user `__wwtests` whose type HAPPENS to match run()'s param
* ([](str,*fn()void)) slips (a) but still silently shadows the
* synth table → reservation rejects it loud regardless of type.
*
* Rows (every row builds+runs on cstage `ww` and, when present, wwstage
* `ww_ww`; rule-10 — both stages must agree):
* row | shape | verdict
* ---------------------+------------------------------------+----------
* scalar_for_slice | takesslice(int) | REJECT [bug]
* str_for_int | takesint(str) | REJECT
* slice_borrow_ok | takesslice([3]int arr) | ok 0 (#258)
* scalar_arg_ok | takesint(5) | ok 7 (control)
*
* -T faces (bundle via `ww test -c`, then `<comp> -T <combined>` must
* loud-reject; the synth's run() callee is resolved from the bundled
* lib/test):
* wwtests_int | const __wwtests: int | REJECT [(a)]
* wwtests_shadow | const __wwtests: [](str,*fn()void) | REJECT [(b)]
*
* wwtests_shadow is the (b) teeth: its type matches run()'s param so (a)
* stays silent (both unpatched stages built it rc=0, silently running
* the user's 1-entry table instead of the collected @test set) — only
* the name reservation rejects it.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
struct row {
const char *label;
const char *src;
int expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */
int want_exit; /* meaningful only when expect_build */
};
static const struct row rows[] = {
/* (1) THE BUG — int where []int is wanted → REJECT both stages.
* Pre-fix wwstage built rc=0 and read the int as a slice header. */
{ "scalar_for_slice",
"package main;\n"
"fn takesslice(xs: []int) void = { return; };\n"
"export fn main() int = {\n"
" let n: int = 5;\n"
" takesslice(n);\n"
" return 0;\n"
"};\n",
0, 0 },
/* (2) str where int is wanted → REJECT both stages. A second scalar
* shape: the general check is not array-specific. */
{ "str_for_int",
"package main;\n"
"fn takesint(x: int) void = { return; };\n"
"export fn main() int = {\n"
" let s: str = \"hi\";\n"
" takesint(s);\n"
" return 0;\n"
"};\n",
0, 0 },
/* (3) CONTROL — [3]int into a []int param is the legit #258 borrow
* (matching element) → ACCEPT both, run to 0. The general check must
* NOT regress the array→slice borrow it subsumes. */
{ "slice_borrow_ok",
"package main;\n"
"fn takesslice(xs: []int) int = { return len(xs): int; };\n"
"export fn main() int = {\n"
" let a: [3]int = [1, 2, 3];\n"
" if (takesslice(a) != 3) { return 1; };\n"
" return 0;\n"
"};\n",
1, 0 },
/* (4) CONTROL — a correctly-typed scalar arg → ACCEPT both, run to 7.
* Pins that the new check does not over-reject a valid scalar call. */
{ "scalar_arg_ok",
"package main;\n"
"fn takesint(x: int) int = { return x; };\n"
"export fn main() int = { return takesint(7); };\n",
1, 7 },
/* (5) scalar→aggregate at the LET context — `let xs: []int = 5` read
* the int as a 24B slice header (same silent-garbage class as the
* call-arg #24, via the SHARED isassignable). REJECT both. */
{ "let_slice_eq_scalar",
"package main;\n"
"export fn main() int = { let xs: []int = 5; return 0; };\n",
0, 0 },
/* (6) bare fn name into a fn-alias param → ACCEPT both (#34/c1': a
* bare fn rvalue types as its fn TYPE; the general check then sees a
* matched fn type). run to 7. */
{ "fn_bare_accept",
"package main;\n"
"type myfn = fn() int;\n"
"fn g() int = { return 7; };\n"
"fn use_it(f: myfn) int = { return f(); };\n"
"export fn main() int = { return use_it(g); };\n",
1, 7 },
/* (7) annotated `g: myfn` into the same param → ACCEPT both, run 7. */
{ "fn_cast_accept",
"package main;\n"
"type myfn = fn() int;\n"
"fn g() int = { return 7; };\n"
"fn use_it(f: myfn) int = { return f(); };\n"
"export fn main() int = { return use_it(g: myfn); };\n",
1, 7 },
/* (8) `&g` (*fn) into a bare-fn alias param → REJECT both. ww-cstage
* takes NO &-required rule for a bare-fn arg, and the *fn-vs-fn KIND
* mismatch is a confident reject (the assignableaddrfn union admits a
* genuine &fn only into a *fn / *alias slot, not a bare-fn alias). */
{ "fn_addr_reject",
"package main;\n"
"type myfn = fn() int;\n"
"fn g() int = { return 7; };\n"
"fn use_it(f: myfn) int = { return f(); };\n"
"export fn main() int = { return use_it(&g); };\n",
0, 0 },
/* (9) matched-signature fn rvalue into a fn slot → ACCEPT both, run 5.
* Pins #34's correct-stamp path (fn type vs fn type, equal sigs). */
{ "fn_match_sig_accept",
"package main;\n"
"fn g() int = { return 5; };\n"
"export fn main() int = { let p: fn() int = g; return p(); };\n",
1, 5 },
/* (10) MISMATCHED-signature fn rvalue into a fn slot → REJECT both
* (#34: `let p: fn() int = h` where h: fn() str — was a silent
* mis-accept via the lenient catch-all; the fn-type stamp now compares
* structurally and rejects). */
{ "fn_mismatch_sig_reject",
"package main;\n"
"fn h() str = { return \"\"; };\n"
"export fn main() int = { let p: fn() int = h; return 0; };\n",
0, 0 },
/* (11) aggregate<->aggregate KIND mismatch — a [3]int array arg into a
* `*int` param. Both aggregate, different kind -> confident reject (the
* array->slice borrow is the ONLY implicit array coercion; array->ptr
* is not). REJECT both. */
{ "aggr_array_into_ptr",
"package main;\n"
"fn takesptr(p: *int) void = { return; };\n"
"export fn main() int = {\n"
" let a: [3]int = [1, 2, 3];\n"
" takesptr(a);\n"
" return 0;\n"
"};\n",
0, 0 },
/* (12) CONTROL — a concrete value into a SPREAD-tagged param must NOT
* over-reject: cstage flattens `...inner` at resolve_type and accepts
* the int leaf; wwstage can't flatten, so it stays LENIENT on a spread
* variant (the faithful escape, mirroring tagged->tagged #115). ACCEPT
* both, run 0. Pins the new check does not regress spread unions. */
{ "spread_tagged_accept",
"package main;\n"
"type inner = (int | str);\n"
"fn take(x: (...inner | bool)) void = { return; };\n"
"export fn main() int = { take(42); return 0; };\n",
1, 0 },
};
/* run_build — build+run `src` via `driver`; returns the binary's exit
* code, or -1 on a build failure. */
static int
run_build(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/cat_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/cat_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -2;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, src);
int brc = runwait(cmd);
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = -1;
if (brc == 0) got = runwait(outbin);
unlink(src); unlink(outbin); rmdir(tmpdir);
return brc == 0 ? got : -1;
}
/* build_should_fail — the build must error on `driver`; returns 0 when
* it correctly FAILS, non-zero when it wrongly succeeded. */
static int
build_should_fail(const char *driver, const char *src, int i)
{
char s[64], tmpdir[64], cmd[1024];
snprintf(s, sizeof s, "/tmp/catn_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/catn_%d_d_%d", getpid(), i);
FILE *f = fopen(s, "wb");
if (!f) return -1;
fputs(src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s 2>/dev/null",
tmpdir, driver, s);
int rc = runwait(cmd);
unlink(s);
const char *base = strrchr(s, '/');
base = base ? base + 1 : s;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
unlink(outbin);
rmdir(tmpdir);
return rc == 0 ? -1 : 0; /* build must NOT succeed */
}
/* -T reject faces — a user fixture is bundled with lib/test via
* `ww test -c` (compiler-neutral; the synth `run()` callee resolves from
* the bundle), then `<comp> -T <combined>` must loud-reject. */
struct trow {
const char *label;
const char *src;
};
static const struct trow trows[] = {
/* (a) the headline cat-A: user __wwtests:int shadows the synth table
* → run(int) where run wants [](str,*fn()void). Pre-fix wwstage built
* a broken binary (int read as a 24B slice header). */
{ "wwtests_int",
"package main;\n"
"const __wwtests: int = 99;\n"
"@test fn checkfoo() void = { return; };\n" },
/* (b) reservation teeth: a user __wwtests whose type MATCHES run()'s
* param typechecks fine — (a)'s isassignable stays silent — but it
* silently shadows the synth table (both unpatched stages built it
* rc=0, running the user's 1-entry table, not the collected @tests).
* Only the `__wwtests` name reservation rejects it. */
{ "wwtests_shadow",
"package main;\n"
"fn dummy() void = { return; };\n"
"const __wwtests: [](str, *fn() void) = [(\"x\", &dummy)];\n"
"@test fn checkfoo() void = { return; };\n" },
};
/* tbundle_reject — bundle `src` via `drv test -c`, then `comp -T` the
* combined unit; the compile must exit nonzero. Returns 0 on the
* expected reject. */
static int
tbundle_reject(const char *bin, const char *comp, const char *drv,
const struct trow *t, int i)
{
int pid = getpid();
char src[128], stem[128], comb[160], cmd[4096];
snprintf(src, sizeof src, "/tmp/catt_%s_%d_%d.ww", comp, pid, i);
snprintf(stem, sizeof stem, "/tmp/catt_%s_%d_%d", comp, pid, i);
snprintf(comb, sizeof comb, "%s.combined.ww", stem);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(t->src, f);
fclose(f);
snprintf(cmd, sizeof cmd,
"%s/%s test -c -o %s %s > /dev/null 2>&1", bin, drv, stem, src);
runwait(cmd);
if (access(comb, 0) != 0) {
fprintf(stderr, "callarg_typecheck[%s][%s]: %s produced no %s\n",
comp, t->label, drv, comb);
unlink(src);
return -1;
}
snprintf(cmd, sizeof cmd, "%s/%s -T %s -o /dev/null 2>/dev/null",
bin, comp, comb);
int rc = runwait(cmd);
unlink(src); unlink(comb);
char tmp[200];
snprintf(tmp, sizeof tmp, "%s.s", stem); unlink(tmp);
snprintf(tmp, sizeof tmp, "%s.o", stem); unlink(tmp);
unlink(stem);
if (rc == 0) {
fprintf(stderr, "callarg_typecheck[%s][%s]: %s -T accepted "
"(expected a loud reject)\n", comp, t->label, comp);
return -1;
}
return 0;
}
/* multimod_build_fail — write io2.ww (always; defines handle=(file|stream)),
* mod1.ww (when withmod1), and a main.ww with `mainbody`, then `drv build -I`
* the tree. Returns 0 when the build correctly FAILS (the arg is rejected),
* non-zero when it wrongly built. Drives the #24 cross-module collision rows
* that single-file rows[] can't express. */
static int
multimod_build_fail(const char *drv, const char *mainbody, int withmod1,
int tag)
{
int pid = getpid();
char dir[96], io2d[160], mod1d[160], p[224], cmd[2048], rm[256];
snprintf(dir, sizeof dir, "/tmp/catcoll_%d_%d", pid, tag);
snprintf(io2d, sizeof io2d, "%s/io2", dir);
snprintf(mod1d, sizeof mod1d, "%s/mod1", dir);
mkdir(dir, 0755); mkdir(io2d, 0755);
if (withmod1) mkdir(mod1d, 0755);
snprintf(p, sizeof p, "%s/io2.ww", io2d);
FILE *f = fopen(p, "wb");
if (!f) return -1;
fputs("package io2;\n"
"export type vtable = struct { x: i32 };\n"
"export type stream = *vtable;\n"
"export type file = i32;\n"
"export type handle = (file | stream);\n"
"export fn take(h: handle) int = { return 7; };\n", f);
fclose(f);
if (withmod1) {
snprintf(p, sizeof p, "%s/mod1.ww", mod1d);
f = fopen(p, "wb");
if (!f) return -1;
fputs("package mod1;\n"
"export type wbox = struct { y: i64 };\n"
"export type stream = *wbox;\n"
"export fn mk() stream = { return nil; };\n", f);
fclose(f);
}
snprintf(p, sizeof p, "%s/main.ww", dir);
f = fopen(p, "wb");
if (!f) return -1;
fputs(mainbody, f);
fclose(f);
if (withmod1)
snprintf(cmd, sizeof cmd,
"cd %s && %s build -I %s -I %s main.ww >/dev/null 2>&1",
dir, drv, io2d, mod1d);
else
snprintf(cmd, sizeof cmd,
"cd %s && %s build -I %s main.ww >/dev/null 2>&1",
dir, drv, io2d);
int rc = runwait(cmd);
snprintf(rm, sizeof rm, "rm -rf %s", dir);
runwait(rm);
return rc == 0 ? -1 : 0; /* build must NOT succeed */
}
/* a []int SLICE passed where io2.handle = (file | stream) is wanted — NO
* slice variant. SHAPE-MISMATCH: cstage rejects nominally, and c3's (B)
* shape-matched-lenient leg ALSO rejects (slice src, no slice-shape variant)
* — the #24-B reachable win, asserted DUAL-STAGE (both drivers must fail). */
static const char COLLIDE_SHAPE_MISMATCH[] =
"package main;\n"
"import io2;\n"
"export fn main() int = {\n"
" let xs: []int = [1, 2, 3];\n"
" return io2.take(xs);\n"
"};\n";
/* mod1.stream (a *mod1.wbox) passed where io2.handle is wanted — a cross-
* module SAME-LEAF, SAME-COARSE-SHAPE (both ptr → scalar/other) collision.
* cstage REJECTS on NOMINAL identity; wwstage's AST-keyed isassignable
* OVER-ACCEPTS it — the tracked #10/#66/#37 residual that (B) shape-narrowing
* cannot reach (the tagged variant node is a bare `stream` with no module,
* byte-identical to the genuine io2.stream; the distinguishing identity lives
* only in tinfo/#66, reachable only by the #37 nominal-tinfo conversion). The
* cs-side pin below asserts ONLY cstage's reject (catching a cs regression +
* recording the eventual ww target); the ww over-accept is documented, NOT
* asserted (a dual-stage row would be dark until #37 lands). */
static const char COLLIDE_SAME_SHAPE[] =
"package main;\n"
"import io2;\n"
"import mod1;\n"
"export fn main() int = {\n"
" let s: mod1.stream = mod1.mk();\n"
" return io2.take(s);\n"
"};\n";
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[1024], wdrv[1024], wcomp[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
snprintf(wcomp, sizeof wcomp, "%s/w6c_ww", bin);
struct { const char *name; const char *drv; int gated; }
drivers[] = {
{ "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1 },
{ NULL, NULL, 0 },
};
int n = (int)(sizeof rows / sizeof rows[0]);
int tn = (int)(sizeof trows / sizeof trows[0]);
int total = 0, fail = 0;
for (int d = 0; drivers[d].name; d++) {
if (drivers[d].gated && access(drivers[d].drv, X_OK) != 0) {
fprintf(stderr, "callarg_typecheck: skip %s (no %s)\n",
drivers[d].name, drivers[d].drv);
continue;
}
for (int i = 0; i < n; i++) {
total++;
if (rows[i].expect_build) {
int got = run_build(drivers[d].drv, &rows[i], i);
if (got != rows[i].want_exit) {
fprintf(stderr, "callarg_typecheck[%s][%s]: "
"exit=%d want=%d\n", drivers[d].name,
rows[i].label, got, rows[i].want_exit);
fail++;
}
} else {
if (build_should_fail(drivers[d].drv, rows[i].src,
100 + i) != 0) {
fprintf(stderr, "callarg_typecheck[%s][%s]: "
"built ok, expected a loud reject\n",
drivers[d].name, rows[i].label);
fail++;
}
}
}
}
/* -T faces: comp ∈ {w6c (cstage), w6c_ww (wwstage)}; bundle is built
* by the cstage driver (compiler-neutral). wwstage gated. */
for (int i = 0; i < tn; i++) {
total++;
if (tbundle_reject(bin, "w6c", "ww", &trows[i], i) != 0)
fail++;
}
if (access(wcomp, X_OK) == 0) {
for (int i = 0; i < tn; i++) {
total++;
if (tbundle_reject(bin, "w6c_ww", "ww", &trows[i],
10 + i) != 0)
fail++;
}
}
/* #24-B reachable win: the SHAPE-MISMATCH collision ([]int slice into a
* (file|stream) tagged with no slice variant) REJECTS on BOTH stages —
* dual-stage row (cstage nominal + c3's shape-matched-lenient leg). */
total++;
if (multimod_build_fail(cdrv, COLLIDE_SHAPE_MISMATCH, 0, 1) != 0) {
fprintf(stderr, "callarg_typecheck[cstage][collide_shape_mismatch]:"
" built ok, expected reject\n");
fail++;
}
if (access(wcomp, X_OK) == 0) { /* wwstage gated (ww_ww present) */
total++;
if (multimod_build_fail(wdrv, COLLIDE_SHAPE_MISMATCH, 0, 2) != 0) {
fprintf(stderr, "callarg_typecheck[wwstage]"
"[collide_shape_mismatch]: built ok, expected "
"reject (#24-B shape-matched-lenient)\n");
fail++;
}
}
/* #37 cs-side pin: the SAME-SHAPE cross-module collision rejects on
* cstage; the wwstage over-accept is the documented #10/#66/#37 nominal
* residual (NOT asserted — would be dark until #37 lands). */
total++;
if (multimod_build_fail(cdrv, COLLIDE_SAME_SHAPE, 1, 3) != 0) {
fprintf(stderr, "callarg_typecheck[cstage][collide_same_shape]: "
"built ok, expected cstage to reject the same-leaf "
"collision (#37 cs-side pin)\n");
fail++;
}
if (fail) {
fprintf(stderr, "callarg_typecheck: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("callarg_typecheck: %d/%d ok\n", total, total);
return 0;
}