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:
12
Makefile
12
Makefile
@@ -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 $@ $<
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
543
test/wcc/989_callarg_typecheck.c
Normal file
543
test/wcc/989_callarg_typecheck.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user