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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user