wcc: accept bare &fn into a fn-pointer-alias slot via a caller-site gate (#206)
A bare `&fn_name` was not assignable into a `*reader` / `(*reader | void)` vtable field without an explicit cast: cstage type_eq on TY_NAMED is pointer-identity, so a structural `*fn(...)` referent never matched the named `*reader` variant; wwstage accepted it only via an accidental catch-all leniency. harec accepts bare &fn through hint-directed alias adoption at the address-of site (check.c:3594-3626) while keeping pointer assignability strictly nominal (types.c:1039-1066), so a materialized `*fn` value never launders across alias names. Mirror that decision without threading a type hint through the bottom-up cexpr: keep type_assignable / isassignable fully nominal, and add a caller-site helper (assignable_addrfn) at the assignment boundaries (let-init, struct-literal field-init, assign, return, call-arg, array element) that accepts iff the rhs is a DIRECT &-of-fn-ident and the destination (or exactly one tagged variant) is a pointer-to-fn-alias whose underlying fn signature structurally matches. A materialized `*fn` value, a distinct same-signature alias, and an ambiguous multi-variant target all stay rejected. Both stages share the rule; wwstage's lenient pointer-fn punt becomes a confident reject. ww has no methods, so a `value.leaf` slot is only ever a fn-pointer field and this never over-admits. The tightening surfaced a wwstage typeeqast gap: a TY_FN result that is a tuple (`*fn(...)(i32,i32)`) compared false where cstage type_eq handled it, newly rejecting a legitimate structural assign. Add the N_TTUPLE structural case (rule-10), restoring test 766. cgen-neutral (the cast was a no-op reinterpret); pre/post bootstrap .s zero-delta. Test 783 covers the positive paths (incl. a byte-id-clean three-field-vtable dispatcher) and the negatives. Tagged-slot negatives (ambiguous / tagged-laundering) are rejected on cstage but wwstage's separate `(X|void)` void-variant leniency (#214) still admits them; 783 pins them cstage-only, to graduate when #214 closes (required before wwstage becomes the authoritative selfhost checker). Note: `make clean && make test` is RED at HEAD on 4 alloc fixtures (700/748/758/915) via a pre-existing clean-build defect (#215, malloc vs rt_malloc); identical with or without this change, so bisect-clean for #206.
This commit is contained in:
9
Makefile
9
Makefile
@@ -331,6 +331,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||
$(BIN)/test_fmt_vstream_mods_run \
|
||||
$(BIN)/test_fmt_vstream_compositions_run \
|
||||
$(BIN)/test_fieldfn_leaf_collide_run \
|
||||
$(BIN)/test_amp_fn_assign_run \
|
||||
$(BIN)/test_bufio_vstream_run \
|
||||
$(BIN)/test_log_vstream_run \
|
||||
$(BIN)/test_use_promote_alias \
|
||||
@@ -716,6 +717,14 @@ $(BIN)/test_fieldfn_leaf_collide_run: test/wcc/782_fieldfn_leaf_collide_run.c \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
# #206: bare &fn assignable into *<fn-alias> / (*<fn-alias>|void). Both
|
||||
# stages (cstage + wwstage byte-id); single-file probes, no lib imports.
|
||||
$(BIN)/test_amp_fn_assign_run: test/wcc/783_amp_fn_assign_run.c \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
$(BIN)/test_fmt_vstream_mods_run: test/wcc/780_fmt_vstream_mods_run.c \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
|
||||
|
||||
@@ -322,6 +322,58 @@ def_cast_fits(Type *t, u64 v)
|
||||
return ext == v;
|
||||
}
|
||||
|
||||
/* addrfn_ptr_matches — true iff ptr is a pointer whose referent
|
||||
* (after one NAMED peel) is a fn type structurally equal to fnty. */
|
||||
static int
|
||||
addrfn_ptr_matches(Type *ptr, Type *fnty)
|
||||
{
|
||||
if (ptr == NULL || ptr->kind != TY_PTR) return 0;
|
||||
Type *ref = ptr->sub;
|
||||
if (ref && ref->kind == TY_NAMED) ref = ref->under;
|
||||
if (ref == NULL || ref->kind != TY_FN) return 0;
|
||||
return type_eq(ref, fnty);
|
||||
}
|
||||
|
||||
/* assignable_addrfn — project #206. A bare `&fn` types structurally
|
||||
* as `*fn(...)`, which is nominally distinct from a `*alias`
|
||||
* fn-pointer slot; type_assignable stays fully nominal (preserving
|
||||
* harec's nominal pointer rule, ref/harec/src/types.c:1039-1066) so
|
||||
* any materialized `*fn` value laundered into a `*alias` is rejected.
|
||||
* This admits the one shape harec accepts via its address-of hint
|
||||
* (ref/harec/src/check.c:3594-3626 adopts the alias when the operand
|
||||
* dealiases to the hint's referent): a DIRECT `&`-of-fn-ident whose
|
||||
* signature structurally matches the destination's pointed-to fn
|
||||
* alias, or — for a tagged `(*alias | void)` destination — the single
|
||||
* ptr-to-fn variant it matches (>=2 same-signature variants is
|
||||
* ambiguous → reject, never silently pick). Lives at the assignment
|
||||
* boundary, not in cexpr, because ww's tinfo is nominal-lossy and
|
||||
* cexpr is hint-free (the alias identity is unrecoverable post-typing);
|
||||
* the caller-site rhs node is the only place the direct-&fn shape
|
||||
* survives. "Direct" is strict: the gate fires only when the rhs IS
|
||||
* the address-of node, never on `&fn` nested in a larger expr. */
|
||||
static int
|
||||
assignable_addrfn(Checker *c, Type *dst, Node *rhs)
|
||||
{
|
||||
if (dst == NULL || rhs == NULL) return 0;
|
||||
if (rhs->kind != N_UN || rhs->op != TK_AMP) return 0;
|
||||
Node *id = rhs->lhs;
|
||||
if (id == NULL || id->kind != N_IDENT || id->str == NULL) return 0;
|
||||
Sym *s = scope_lookup_prefer(c->cur, c->cur_mod, id->str);
|
||||
if (s == NULL || s->kind != SK_FN) return 0;
|
||||
Type *fnty = s->type;
|
||||
if (fnty == NULL || fnty->kind != TY_FN) return 0;
|
||||
Type *du = (dst->kind == TY_NAMED) ? dst->under : dst;
|
||||
if (du == NULL) return 0;
|
||||
if (du->kind == TY_PTR) return addrfn_ptr_matches(du, fnty);
|
||||
if (du->kind == TY_TAGGED) {
|
||||
int n = 0;
|
||||
for (Tparam *p = du->params; p; p = p->next)
|
||||
if (addrfn_ptr_matches(p->type, fnty)) n++;
|
||||
return n == 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* arrlit_init_fits — #130: accept-if-fits for `let/def A: [N]T = [..]`
|
||||
* where the whole-array type_assignable failed (bare-int elements
|
||||
* synthesize [N]i32 via type_default, losing the literal flavor that
|
||||
@@ -360,7 +412,8 @@ arrlit_init_fits(Checker *c, Type *dt, Node *rhs)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!type_assignable(et, e->type)) return 0;
|
||||
if (!type_assignable(et, e->type) && !assignable_addrfn(c, et, e))
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -1415,7 +1468,8 @@ cexpr(Checker *c, Node *n)
|
||||
err(c, a->pos,
|
||||
"spread arg must be the last");
|
||||
} else if (elem != ty_err && at != ty_err) {
|
||||
if (!type_assignable(elem, at))
|
||||
if (!type_assignable(elem, at) &&
|
||||
!assignable_addrfn(c, elem, a))
|
||||
err(c, a->pos,
|
||||
"variadic arg: %s not assignable to %s",
|
||||
type_name(c->a, at),
|
||||
@@ -1423,7 +1477,8 @@ cexpr(Checker *c, Node *n)
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!type_assignable(p->type, at) && at != ty_err && p->type != ty_err)
|
||||
if (!type_assignable(p->type, at) && at != ty_err && p->type != ty_err
|
||||
&& !assignable_addrfn(c, p->type, a))
|
||||
err(c, a->pos, "argument type %s not assignable to %s",
|
||||
type_name(c->a, at), type_name(c->a, p->type));
|
||||
p = p->next;
|
||||
@@ -1449,7 +1504,8 @@ cexpr(Checker *c, Node *n)
|
||||
}
|
||||
Type *l = cexpr(c, n->lhs);
|
||||
Type *r = cexpr(c, n->rhs);
|
||||
if (l != ty_err && r != ty_err && !type_assignable(l, r))
|
||||
if (l != ty_err && r != ty_err && !type_assignable(l, r) &&
|
||||
!assignable_addrfn(c, l, n->rhs))
|
||||
err(c, n->pos, "cannot assign %s to %s",
|
||||
type_name(c->a, r), type_name(c->a, l));
|
||||
return n->type = l;
|
||||
@@ -1483,7 +1539,8 @@ cexpr(Checker *c, Node *n)
|
||||
err(c, f->pos, "no field '%s' in %s",
|
||||
f->str, type_name(c->a, t));
|
||||
else if (vt != ty_err &&
|
||||
!type_assignable(match->type, vt))
|
||||
!type_assignable(match->type, vt) &&
|
||||
!assignable_addrfn(c, match->type, f->lhs))
|
||||
err(c, f->pos, "field %s: %s not assignable to %s",
|
||||
f->str, type_name(c->a, vt),
|
||||
type_name(c->a, match->type));
|
||||
@@ -1837,7 +1894,8 @@ clet(Checker *c, Node *n)
|
||||
}
|
||||
}
|
||||
if (declared && initt && initt != ty_err && !has_arr_repeat &&
|
||||
!type_assignable(declared, initt))
|
||||
!type_assignable(declared, initt) &&
|
||||
!assignable_addrfn(c, declared, n->rhs))
|
||||
err(c, n->pos, "init %s not assignable to declared %s",
|
||||
type_name(c->a, initt), type_name(c->a, declared));
|
||||
/* #104 fold-2: `let x: f32 = 1.0` — narrow the init literal to f32. */
|
||||
@@ -1877,7 +1935,8 @@ cstmt(Checker *c, Node *n)
|
||||
if (c->ret == ty_void && n->lhs)
|
||||
err(c, n->pos, "return value in void function");
|
||||
else if (c->ret != ty_void && rt != ty_err && c->ret != ty_err
|
||||
&& !type_assignable(c->ret, rt))
|
||||
&& !type_assignable(c->ret, rt)
|
||||
&& !assignable_addrfn(c, c->ret, n->lhs))
|
||||
err(c, n->pos, "return %s not assignable to %s",
|
||||
type_name(c->a, rt), type_name(c->a, c->ret));
|
||||
/* #104 fold-2: `fn g() f32 = { return 1.0; }` — narrow to f32. */
|
||||
|
||||
@@ -10797,9 +10797,27 @@ fn typeeqast(a: *node, b: *node) bool = {
|
||||
};
|
||||
return pb == nil;
|
||||
};
|
||||
// Conservative: anything else (struct/tagged/tuple/array) fails
|
||||
// the cheap check. Selfhost code doesn't currently rely on
|
||||
// equality at these shapes for the targeted checks.
|
||||
// #206: tuple structural equality — mirror of cstage type.c:261-268
|
||||
// (TY_TUPLE). Needed since a tuple-RETURN fn pointer compares its
|
||||
// N_TFN return node (aa.lhs) here; without it `*fn(x)(a,b)` never
|
||||
// proves structurally equal to itself, so the #206 punt-tightening
|
||||
// would confidently reject a bare-&fn into a structural `*fn(...)`
|
||||
// slot (test 766 fn_tuple_return). Elements are N_TPARAM-wrapped
|
||||
// (parse.ww:302-318), so compare each link's .lhs.
|
||||
if (k == nkind.N_TTUPLE) {
|
||||
let pa: *node = aa.list;
|
||||
let pb: *node = bb.list;
|
||||
for (pa != nil) {
|
||||
if (pb == nil) { return false; };
|
||||
if (!typeeqast(pa.lhs, pb.lhs)) { return false; };
|
||||
pa = pa.next;
|
||||
pb = pb.next;
|
||||
};
|
||||
return pb == nil;
|
||||
};
|
||||
// Conservative: anything else (struct/tagged/array) fails the
|
||||
// cheap check. Selfhost code doesn't currently rely on equality
|
||||
// at these shapes for the targeted checks.
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -12069,6 +12087,35 @@ fn unoptype(c: *checker, e: *node) *node = {
|
||||
};
|
||||
};
|
||||
};
|
||||
// #206: `&fn` must type as `*fn(...)` — a pointer to the fn's
|
||||
// full signature — not `*<rettype>`. exprtype's N_IDENT arm
|
||||
// returns a fn decl's lhs (the return type), so the generic
|
||||
// `*opt` below would mistype `&myread` as `*i32`; a `*fn`
|
||||
// laundered into a `*alias` slot then slips past the nominal
|
||||
// pointer-fn reject in isassignable. Synthesize the N_TFN from
|
||||
// the fn decl (N_FNDECL and N_TFN share parseparams' param
|
||||
// shape) so the address-of carries the signature. Mirrors
|
||||
// cstage, where a fn ident already types as TY_FN
|
||||
// (cmd/wcc/check.c:668), so `&fn` is `*fn` natively.
|
||||
if (e.lhs != nil) {
|
||||
if (e.lhs.kind == nkind.N_IDENT) {
|
||||
let fs: *sym = scopelookup(c.cur, e.lhs.str);
|
||||
if (fs != nil) {
|
||||
if (fs.skind == skind.SK_FN) {
|
||||
if (fs.decl != nil) {
|
||||
if (fs.decl.kind == nkind.N_FNDECL) {
|
||||
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
|
||||
synth.lhs = fs.decl.lhs;
|
||||
synth.list = fs.decl.list;
|
||||
let pf: *node = newnode(nkind.N_TPTR, "", 0, 0);
|
||||
pf.lhs = synth;
|
||||
return pf;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
// opt nil → propagation from inherent-IDENT bail (5-lite-b
|
||||
// #34). Generic &expr widens to *opt; without opt we can't
|
||||
// synthesize the pointer node.
|
||||
@@ -13020,6 +13067,70 @@ fn isstrtname(t: *node) bool = {
|
||||
return streq(t.str, "str");
|
||||
};
|
||||
|
||||
// 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).
|
||||
// Mirror of cstage addrfn_ptr_matches (cmd/wcc/check.c, project #206);
|
||||
// reuses typeeqast — the same structural-fn comparator cstage uses via
|
||||
// type_eq(TY_FN,TY_FN) — for symmetry.
|
||||
fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = {
|
||||
if (ptr == nil) { return false; };
|
||||
let pu: *node = resolvealias(c, unwrapbang(ptr));
|
||||
if (pu == nil) { return false; };
|
||||
if (pu.kind != nkind.N_TPTR) { return false; };
|
||||
let ref: *node = resolvealias(c, unwrapbang(pu.lhs));
|
||||
if (ref == nil) { return false; };
|
||||
if (ref.kind != nkind.N_TFN) { return false; };
|
||||
return typeeqast(synth, ref);
|
||||
};
|
||||
|
||||
// assignableaddrfn — project #206 Option C gate. Mirror of cstage
|
||||
// assignable_addrfn (cmd/wcc/check.c). A bare `&fn` types structurally
|
||||
// as `*fn(...)`, nominally distinct from a `*alias` fn-pointer slot;
|
||||
// isassignable stays nominal (the pointer-fn arm below confidently
|
||||
// rejects a laundered `*fn` value, like harec types.c:1039-1066). This
|
||||
// admits only the shape harec adopts via its address-of hint (harec
|
||||
// check.c:3594-3626): a DIRECT `&`-of-fn-ident whose signature
|
||||
// structurally matches the destination's pointed-to fn alias, or the
|
||||
// single matching ptr-to-fn variant of a tagged dst (>=2 same-sig
|
||||
// variants → ambiguous, reject). Lives at the assignment-boundary
|
||||
// caller sites — not in exprtype — because the alias identity is
|
||||
// nominal-lossy once typed and the direct-&fn shape survives only on
|
||||
// the rhs node. N_FNDECL and N_TFN share parseparams' param-node shape
|
||||
// (lib/ww/parse/decl.ww + parse.ww), so a synthetic N_TFN over the fn
|
||||
// decl's lhs/list compares correctly under typeeqast.
|
||||
fn assignableaddrfn(c: *checker, dst: *node, rhs: *node) bool = {
|
||||
if (dst == nil) { return false; };
|
||||
if (rhs == nil) { return false; };
|
||||
if (rhs.kind != nkind.N_UN) { return false; };
|
||||
if (rhs.op != tkind.TK_AMP) { return false; };
|
||||
let id: *node = rhs.lhs;
|
||||
if (id == nil) { return false; };
|
||||
if (id.kind != nkind.N_IDENT) { return false; };
|
||||
let s: *sym = scopelookup(c.cur, id.str);
|
||||
if (s == nil) { return false; };
|
||||
if (s.skind != skind.SK_FN) { return false; };
|
||||
if (s.decl == nil) { return false; };
|
||||
let d: *node = s.decl;
|
||||
if (d.kind != nkind.N_FNDECL) { return false; };
|
||||
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
|
||||
synth.lhs = d.lhs;
|
||||
synth.list = d.list;
|
||||
let du: *node = resolvealias(c, unwrapbang(dst));
|
||||
if (du == nil) { return false; };
|
||||
if (du.kind == nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); };
|
||||
if (du.kind == nkind.N_TTAGGED) {
|
||||
let nmatch: i32 = 0;
|
||||
let v: *node = du.list;
|
||||
for (v != nil) {
|
||||
if (addrfnptrmatches(c, v, synth)) { nmatch = nmatch + 1; };
|
||||
v = v.next;
|
||||
};
|
||||
return nmatch == 1;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// isassignable — AST-level approximation of C check.c
|
||||
// type_assignable. Returns true when we know the assignment is
|
||||
// OK, false only when we're confident it isn't, and "skip" (true)
|
||||
@@ -13171,6 +13282,31 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
|
||||
};
|
||||
};
|
||||
};
|
||||
// #206: two pointers whose referents both resolve to fn types are
|
||||
// NOMINALLY assignable only when structurally equal — and that case
|
||||
// already returned true via typeeqast at the top. Reaching here
|
||||
// means the fn signatures differ, or a bare structural `*fn` value
|
||||
// is being laundered into a `*alias` slot: confidently NOT
|
||||
// assignable, mirror of cstage's nominal type_assignable (harec
|
||||
// types.c:1039-1066). The direct `&fn` adopt-the-alias case is
|
||||
// handled at the assignment caller sites via assignableaddrfn, NOT
|
||||
// here. Without this the lenient catch-all below silently accepted
|
||||
// the laundering shape.
|
||||
if (du.kind == nkind.N_TPTR) {
|
||||
if (su.kind == nkind.N_TPTR) {
|
||||
let dref: *node = resolvealias(c, unwrapbang(du.lhs));
|
||||
let sref: *node = resolvealias(c, unwrapbang(su.lhs));
|
||||
if (dref != nil) {
|
||||
if (sref != nil) {
|
||||
if (dref.kind == nkind.N_TFN) {
|
||||
if (sref.kind == nkind.N_TFN) {
|
||||
return false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
// Anything else: don't claim confidence.
|
||||
*confident = false;
|
||||
return true;
|
||||
@@ -13429,8 +13565,11 @@ fn checkletassign(c: *checker, n: *node) void = {
|
||||
if (est != nil) {
|
||||
if (!isassignable(c, elemtn, est, &conf2)) {
|
||||
if (conf2) {
|
||||
errnotassign(c, elemtn, est, "let");
|
||||
return;
|
||||
// #206: direct `&fn` array element.
|
||||
if (!assignableaddrfn(c, elemtn, ev)) {
|
||||
errnotassign(c, elemtn, est, "let");
|
||||
return;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -13443,6 +13582,8 @@ fn checkletassign(c: *checker, n: *node) void = {
|
||||
};
|
||||
let conf: bool = false;
|
||||
let ok: bool = isassignable(c, n.lhs, src, &conf);
|
||||
// #206: direct `&fn` → `*alias` / `(*alias | void)` slot.
|
||||
if (!ok) { if (assignableaddrfn(c, n.lhs, n.rhs)) { ok = true; }; };
|
||||
if (!conf) { return; };
|
||||
if (!ok) { errnotassign(c, n.lhs, src, "let"); };
|
||||
};
|
||||
@@ -13460,6 +13601,8 @@ fn checkretassign(c: *checker, n: *node) void = {
|
||||
if (src == nil) { return; };
|
||||
let conf: bool = false;
|
||||
let ok: bool = isassignable(c, c.fnret, src, &conf);
|
||||
// #206: direct `&fn` returned into a `*alias` / `(*alias | void)`.
|
||||
if (!ok) { if (assignableaddrfn(c, c.fnret, n.lhs)) { ok = true; }; };
|
||||
if (!conf) { return; };
|
||||
if (!ok) { errnotassign(c, c.fnret, src, "return"); };
|
||||
};
|
||||
|
||||
@@ -767,9 +767,27 @@ fn typeeqast(a: *node, b: *node) bool = {
|
||||
};
|
||||
return pb == nil;
|
||||
};
|
||||
// Conservative: anything else (struct/tagged/tuple/array) fails
|
||||
// the cheap check. Selfhost code doesn't currently rely on
|
||||
// equality at these shapes for the targeted checks.
|
||||
// #206: tuple structural equality — mirror of cstage type.c:261-268
|
||||
// (TY_TUPLE). Needed since a tuple-RETURN fn pointer compares its
|
||||
// N_TFN return node (aa.lhs) here; without it `*fn(x)(a,b)` never
|
||||
// proves structurally equal to itself, so the #206 punt-tightening
|
||||
// would confidently reject a bare-&fn into a structural `*fn(...)`
|
||||
// slot (test 766 fn_tuple_return). Elements are N_TPARAM-wrapped
|
||||
// (parse.ww:302-318), so compare each link's .lhs.
|
||||
if (k == nkind.N_TTUPLE) {
|
||||
let pa: *node = aa.list;
|
||||
let pb: *node = bb.list;
|
||||
for (pa != nil) {
|
||||
if (pb == nil) { return false; };
|
||||
if (!typeeqast(pa.lhs, pb.lhs)) { return false; };
|
||||
pa = pa.next;
|
||||
pb = pb.next;
|
||||
};
|
||||
return pb == nil;
|
||||
};
|
||||
// Conservative: anything else (struct/tagged/array) fails the
|
||||
// cheap check. Selfhost code doesn't currently rely on equality
|
||||
// at these shapes for the targeted checks.
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -2039,6 +2057,35 @@ fn unoptype(c: *checker, e: *node) *node = {
|
||||
};
|
||||
};
|
||||
};
|
||||
// #206: `&fn` must type as `*fn(...)` — a pointer to the fn's
|
||||
// full signature — not `*<rettype>`. exprtype's N_IDENT arm
|
||||
// returns a fn decl's lhs (the return type), so the generic
|
||||
// `*opt` below would mistype `&myread` as `*i32`; a `*fn`
|
||||
// laundered into a `*alias` slot then slips past the nominal
|
||||
// pointer-fn reject in isassignable. Synthesize the N_TFN from
|
||||
// the fn decl (N_FNDECL and N_TFN share parseparams' param
|
||||
// shape) so the address-of carries the signature. Mirrors
|
||||
// cstage, where a fn ident already types as TY_FN
|
||||
// (cmd/wcc/check.c:668), so `&fn` is `*fn` natively.
|
||||
if (e.lhs != nil) {
|
||||
if (e.lhs.kind == nkind.N_IDENT) {
|
||||
let fs: *sym = scopelookup(c.cur, e.lhs.str);
|
||||
if (fs != nil) {
|
||||
if (fs.skind == skind.SK_FN) {
|
||||
if (fs.decl != nil) {
|
||||
if (fs.decl.kind == nkind.N_FNDECL) {
|
||||
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
|
||||
synth.lhs = fs.decl.lhs;
|
||||
synth.list = fs.decl.list;
|
||||
let pf: *node = newnode(nkind.N_TPTR, "", 0, 0);
|
||||
pf.lhs = synth;
|
||||
return pf;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
// opt nil → propagation from inherent-IDENT bail (5-lite-b
|
||||
// #34). Generic &expr widens to *opt; without opt we can't
|
||||
// synthesize the pointer node.
|
||||
@@ -2990,6 +3037,70 @@ fn isstrtname(t: *node) bool = {
|
||||
return streq(t.str, "str");
|
||||
};
|
||||
|
||||
// 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).
|
||||
// Mirror of cstage addrfn_ptr_matches (cmd/wcc/check.c, project #206);
|
||||
// reuses typeeqast — the same structural-fn comparator cstage uses via
|
||||
// type_eq(TY_FN,TY_FN) — for symmetry.
|
||||
fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = {
|
||||
if (ptr == nil) { return false; };
|
||||
let pu: *node = resolvealias(c, unwrapbang(ptr));
|
||||
if (pu == nil) { return false; };
|
||||
if (pu.kind != nkind.N_TPTR) { return false; };
|
||||
let ref: *node = resolvealias(c, unwrapbang(pu.lhs));
|
||||
if (ref == nil) { return false; };
|
||||
if (ref.kind != nkind.N_TFN) { return false; };
|
||||
return typeeqast(synth, ref);
|
||||
};
|
||||
|
||||
// assignableaddrfn — project #206 Option C gate. Mirror of cstage
|
||||
// assignable_addrfn (cmd/wcc/check.c). A bare `&fn` types structurally
|
||||
// as `*fn(...)`, nominally distinct from a `*alias` fn-pointer slot;
|
||||
// isassignable stays nominal (the pointer-fn arm below confidently
|
||||
// rejects a laundered `*fn` value, like harec types.c:1039-1066). This
|
||||
// admits only the shape harec adopts via its address-of hint (harec
|
||||
// check.c:3594-3626): a DIRECT `&`-of-fn-ident whose signature
|
||||
// structurally matches the destination's pointed-to fn alias, or the
|
||||
// single matching ptr-to-fn variant of a tagged dst (>=2 same-sig
|
||||
// variants → ambiguous, reject). Lives at the assignment-boundary
|
||||
// caller sites — not in exprtype — because the alias identity is
|
||||
// nominal-lossy once typed and the direct-&fn shape survives only on
|
||||
// the rhs node. N_FNDECL and N_TFN share parseparams' param-node shape
|
||||
// (lib/ww/parse/decl.ww + parse.ww), so a synthetic N_TFN over the fn
|
||||
// decl's lhs/list compares correctly under typeeqast.
|
||||
fn assignableaddrfn(c: *checker, dst: *node, rhs: *node) bool = {
|
||||
if (dst == nil) { return false; };
|
||||
if (rhs == nil) { return false; };
|
||||
if (rhs.kind != nkind.N_UN) { return false; };
|
||||
if (rhs.op != tkind.TK_AMP) { return false; };
|
||||
let id: *node = rhs.lhs;
|
||||
if (id == nil) { return false; };
|
||||
if (id.kind != nkind.N_IDENT) { return false; };
|
||||
let s: *sym = scopelookup(c.cur, id.str);
|
||||
if (s == nil) { return false; };
|
||||
if (s.skind != skind.SK_FN) { return false; };
|
||||
if (s.decl == nil) { return false; };
|
||||
let d: *node = s.decl;
|
||||
if (d.kind != nkind.N_FNDECL) { return false; };
|
||||
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
|
||||
synth.lhs = d.lhs;
|
||||
synth.list = d.list;
|
||||
let du: *node = resolvealias(c, unwrapbang(dst));
|
||||
if (du == nil) { return false; };
|
||||
if (du.kind == nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); };
|
||||
if (du.kind == nkind.N_TTAGGED) {
|
||||
let nmatch: i32 = 0;
|
||||
let v: *node = du.list;
|
||||
for (v != nil) {
|
||||
if (addrfnptrmatches(c, v, synth)) { nmatch = nmatch + 1; };
|
||||
v = v.next;
|
||||
};
|
||||
return nmatch == 1;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// isassignable — AST-level approximation of C check.c
|
||||
// type_assignable. Returns true when we know the assignment is
|
||||
// OK, false only when we're confident it isn't, and "skip" (true)
|
||||
@@ -3141,6 +3252,31 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
|
||||
};
|
||||
};
|
||||
};
|
||||
// #206: two pointers whose referents both resolve to fn types are
|
||||
// NOMINALLY assignable only when structurally equal — and that case
|
||||
// already returned true via typeeqast at the top. Reaching here
|
||||
// means the fn signatures differ, or a bare structural `*fn` value
|
||||
// is being laundered into a `*alias` slot: confidently NOT
|
||||
// assignable, mirror of cstage's nominal type_assignable (harec
|
||||
// types.c:1039-1066). The direct `&fn` adopt-the-alias case is
|
||||
// handled at the assignment caller sites via assignableaddrfn, NOT
|
||||
// here. Without this the lenient catch-all below silently accepted
|
||||
// the laundering shape.
|
||||
if (du.kind == nkind.N_TPTR) {
|
||||
if (su.kind == nkind.N_TPTR) {
|
||||
let dref: *node = resolvealias(c, unwrapbang(du.lhs));
|
||||
let sref: *node = resolvealias(c, unwrapbang(su.lhs));
|
||||
if (dref != nil) {
|
||||
if (sref != nil) {
|
||||
if (dref.kind == nkind.N_TFN) {
|
||||
if (sref.kind == nkind.N_TFN) {
|
||||
return false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
// Anything else: don't claim confidence.
|
||||
*confident = false;
|
||||
return true;
|
||||
@@ -3399,8 +3535,11 @@ fn checkletassign(c: *checker, n: *node) void = {
|
||||
if (est != nil) {
|
||||
if (!isassignable(c, elemtn, est, &conf2)) {
|
||||
if (conf2) {
|
||||
errnotassign(c, elemtn, est, "let");
|
||||
return;
|
||||
// #206: direct `&fn` array element.
|
||||
if (!assignableaddrfn(c, elemtn, ev)) {
|
||||
errnotassign(c, elemtn, est, "let");
|
||||
return;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -3413,6 +3552,8 @@ fn checkletassign(c: *checker, n: *node) void = {
|
||||
};
|
||||
let conf: bool = false;
|
||||
let ok: bool = isassignable(c, n.lhs, src, &conf);
|
||||
// #206: direct `&fn` → `*alias` / `(*alias | void)` slot.
|
||||
if (!ok) { if (assignableaddrfn(c, n.lhs, n.rhs)) { ok = true; }; };
|
||||
if (!conf) { return; };
|
||||
if (!ok) { errnotassign(c, n.lhs, src, "let"); };
|
||||
};
|
||||
@@ -3430,6 +3571,8 @@ fn checkretassign(c: *checker, n: *node) void = {
|
||||
if (src == nil) { return; };
|
||||
let conf: bool = false;
|
||||
let ok: bool = isassignable(c, c.fnret, src, &conf);
|
||||
// #206: direct `&fn` returned into a `*alias` / `(*alias | void)`.
|
||||
if (!ok) { if (assignableaddrfn(c, c.fnret, n.lhs)) { ok = true; }; };
|
||||
if (!conf) { return; };
|
||||
if (!ok) { errnotassign(c, c.fnret, src, "return"); };
|
||||
};
|
||||
|
||||
@@ -10797,9 +10797,27 @@ fn typeeqast(a: *node, b: *node) bool = {
|
||||
};
|
||||
return pb == nil;
|
||||
};
|
||||
// Conservative: anything else (struct/tagged/tuple/array) fails
|
||||
// the cheap check. Selfhost code doesn't currently rely on
|
||||
// equality at these shapes for the targeted checks.
|
||||
// #206: tuple structural equality — mirror of cstage type.c:261-268
|
||||
// (TY_TUPLE). Needed since a tuple-RETURN fn pointer compares its
|
||||
// N_TFN return node (aa.lhs) here; without it `*fn(x)(a,b)` never
|
||||
// proves structurally equal to itself, so the #206 punt-tightening
|
||||
// would confidently reject a bare-&fn into a structural `*fn(...)`
|
||||
// slot (test 766 fn_tuple_return). Elements are N_TPARAM-wrapped
|
||||
// (parse.ww:302-318), so compare each link's .lhs.
|
||||
if (k == nkind.N_TTUPLE) {
|
||||
let pa: *node = aa.list;
|
||||
let pb: *node = bb.list;
|
||||
for (pa != nil) {
|
||||
if (pb == nil) { return false; };
|
||||
if (!typeeqast(pa.lhs, pb.lhs)) { return false; };
|
||||
pa = pa.next;
|
||||
pb = pb.next;
|
||||
};
|
||||
return pb == nil;
|
||||
};
|
||||
// Conservative: anything else (struct/tagged/array) fails the
|
||||
// cheap check. Selfhost code doesn't currently rely on equality
|
||||
// at these shapes for the targeted checks.
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -12069,6 +12087,35 @@ fn unoptype(c: *checker, e: *node) *node = {
|
||||
};
|
||||
};
|
||||
};
|
||||
// #206: `&fn` must type as `*fn(...)` — a pointer to the fn's
|
||||
// full signature — not `*<rettype>`. exprtype's N_IDENT arm
|
||||
// returns a fn decl's lhs (the return type), so the generic
|
||||
// `*opt` below would mistype `&myread` as `*i32`; a `*fn`
|
||||
// laundered into a `*alias` slot then slips past the nominal
|
||||
// pointer-fn reject in isassignable. Synthesize the N_TFN from
|
||||
// the fn decl (N_FNDECL and N_TFN share parseparams' param
|
||||
// shape) so the address-of carries the signature. Mirrors
|
||||
// cstage, where a fn ident already types as TY_FN
|
||||
// (cmd/wcc/check.c:668), so `&fn` is `*fn` natively.
|
||||
if (e.lhs != nil) {
|
||||
if (e.lhs.kind == nkind.N_IDENT) {
|
||||
let fs: *sym = scopelookup(c.cur, e.lhs.str);
|
||||
if (fs != nil) {
|
||||
if (fs.skind == skind.SK_FN) {
|
||||
if (fs.decl != nil) {
|
||||
if (fs.decl.kind == nkind.N_FNDECL) {
|
||||
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
|
||||
synth.lhs = fs.decl.lhs;
|
||||
synth.list = fs.decl.list;
|
||||
let pf: *node = newnode(nkind.N_TPTR, "", 0, 0);
|
||||
pf.lhs = synth;
|
||||
return pf;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
// opt nil → propagation from inherent-IDENT bail (5-lite-b
|
||||
// #34). Generic &expr widens to *opt; without opt we can't
|
||||
// synthesize the pointer node.
|
||||
@@ -13020,6 +13067,70 @@ fn isstrtname(t: *node) bool = {
|
||||
return streq(t.str, "str");
|
||||
};
|
||||
|
||||
// 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).
|
||||
// Mirror of cstage addrfn_ptr_matches (cmd/wcc/check.c, project #206);
|
||||
// reuses typeeqast — the same structural-fn comparator cstage uses via
|
||||
// type_eq(TY_FN,TY_FN) — for symmetry.
|
||||
fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = {
|
||||
if (ptr == nil) { return false; };
|
||||
let pu: *node = resolvealias(c, unwrapbang(ptr));
|
||||
if (pu == nil) { return false; };
|
||||
if (pu.kind != nkind.N_TPTR) { return false; };
|
||||
let ref: *node = resolvealias(c, unwrapbang(pu.lhs));
|
||||
if (ref == nil) { return false; };
|
||||
if (ref.kind != nkind.N_TFN) { return false; };
|
||||
return typeeqast(synth, ref);
|
||||
};
|
||||
|
||||
// assignableaddrfn — project #206 Option C gate. Mirror of cstage
|
||||
// assignable_addrfn (cmd/wcc/check.c). A bare `&fn` types structurally
|
||||
// as `*fn(...)`, nominally distinct from a `*alias` fn-pointer slot;
|
||||
// isassignable stays nominal (the pointer-fn arm below confidently
|
||||
// rejects a laundered `*fn` value, like harec types.c:1039-1066). This
|
||||
// admits only the shape harec adopts via its address-of hint (harec
|
||||
// check.c:3594-3626): a DIRECT `&`-of-fn-ident whose signature
|
||||
// structurally matches the destination's pointed-to fn alias, or the
|
||||
// single matching ptr-to-fn variant of a tagged dst (>=2 same-sig
|
||||
// variants → ambiguous, reject). Lives at the assignment-boundary
|
||||
// caller sites — not in exprtype — because the alias identity is
|
||||
// nominal-lossy once typed and the direct-&fn shape survives only on
|
||||
// the rhs node. N_FNDECL and N_TFN share parseparams' param-node shape
|
||||
// (lib/ww/parse/decl.ww + parse.ww), so a synthetic N_TFN over the fn
|
||||
// decl's lhs/list compares correctly under typeeqast.
|
||||
fn assignableaddrfn(c: *checker, dst: *node, rhs: *node) bool = {
|
||||
if (dst == nil) { return false; };
|
||||
if (rhs == nil) { return false; };
|
||||
if (rhs.kind != nkind.N_UN) { return false; };
|
||||
if (rhs.op != tkind.TK_AMP) { return false; };
|
||||
let id: *node = rhs.lhs;
|
||||
if (id == nil) { return false; };
|
||||
if (id.kind != nkind.N_IDENT) { return false; };
|
||||
let s: *sym = scopelookup(c.cur, id.str);
|
||||
if (s == nil) { return false; };
|
||||
if (s.skind != skind.SK_FN) { return false; };
|
||||
if (s.decl == nil) { return false; };
|
||||
let d: *node = s.decl;
|
||||
if (d.kind != nkind.N_FNDECL) { return false; };
|
||||
let synth: *node = newnode(nkind.N_TFN, "", 0, 0);
|
||||
synth.lhs = d.lhs;
|
||||
synth.list = d.list;
|
||||
let du: *node = resolvealias(c, unwrapbang(dst));
|
||||
if (du == nil) { return false; };
|
||||
if (du.kind == nkind.N_TPTR) { return addrfnptrmatches(c, du, synth); };
|
||||
if (du.kind == nkind.N_TTAGGED) {
|
||||
let nmatch: i32 = 0;
|
||||
let v: *node = du.list;
|
||||
for (v != nil) {
|
||||
if (addrfnptrmatches(c, v, synth)) { nmatch = nmatch + 1; };
|
||||
v = v.next;
|
||||
};
|
||||
return nmatch == 1;
|
||||
};
|
||||
return false;
|
||||
};
|
||||
|
||||
// isassignable — AST-level approximation of C check.c
|
||||
// type_assignable. Returns true when we know the assignment is
|
||||
// OK, false only when we're confident it isn't, and "skip" (true)
|
||||
@@ -13171,6 +13282,31 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
|
||||
};
|
||||
};
|
||||
};
|
||||
// #206: two pointers whose referents both resolve to fn types are
|
||||
// NOMINALLY assignable only when structurally equal — and that case
|
||||
// already returned true via typeeqast at the top. Reaching here
|
||||
// means the fn signatures differ, or a bare structural `*fn` value
|
||||
// is being laundered into a `*alias` slot: confidently NOT
|
||||
// assignable, mirror of cstage's nominal type_assignable (harec
|
||||
// types.c:1039-1066). The direct `&fn` adopt-the-alias case is
|
||||
// handled at the assignment caller sites via assignableaddrfn, NOT
|
||||
// here. Without this the lenient catch-all below silently accepted
|
||||
// the laundering shape.
|
||||
if (du.kind == nkind.N_TPTR) {
|
||||
if (su.kind == nkind.N_TPTR) {
|
||||
let dref: *node = resolvealias(c, unwrapbang(du.lhs));
|
||||
let sref: *node = resolvealias(c, unwrapbang(su.lhs));
|
||||
if (dref != nil) {
|
||||
if (sref != nil) {
|
||||
if (dref.kind == nkind.N_TFN) {
|
||||
if (sref.kind == nkind.N_TFN) {
|
||||
return false;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
// Anything else: don't claim confidence.
|
||||
*confident = false;
|
||||
return true;
|
||||
@@ -13429,8 +13565,11 @@ fn checkletassign(c: *checker, n: *node) void = {
|
||||
if (est != nil) {
|
||||
if (!isassignable(c, elemtn, est, &conf2)) {
|
||||
if (conf2) {
|
||||
errnotassign(c, elemtn, est, "let");
|
||||
return;
|
||||
// #206: direct `&fn` array element.
|
||||
if (!assignableaddrfn(c, elemtn, ev)) {
|
||||
errnotassign(c, elemtn, est, "let");
|
||||
return;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -13443,6 +13582,8 @@ fn checkletassign(c: *checker, n: *node) void = {
|
||||
};
|
||||
let conf: bool = false;
|
||||
let ok: bool = isassignable(c, n.lhs, src, &conf);
|
||||
// #206: direct `&fn` → `*alias` / `(*alias | void)` slot.
|
||||
if (!ok) { if (assignableaddrfn(c, n.lhs, n.rhs)) { ok = true; }; };
|
||||
if (!conf) { return; };
|
||||
if (!ok) { errnotassign(c, n.lhs, src, "let"); };
|
||||
};
|
||||
@@ -13460,6 +13601,8 @@ fn checkretassign(c: *checker, n: *node) void = {
|
||||
if (src == nil) { return; };
|
||||
let conf: bool = false;
|
||||
let ok: bool = isassignable(c, c.fnret, src, &conf);
|
||||
// #206: direct `&fn` returned into a `*alias` / `(*alias | void)`.
|
||||
if (!ok) { if (assignableaddrfn(c, c.fnret, n.lhs)) { ok = true; }; };
|
||||
if (!conf) { return; };
|
||||
if (!ok) { errnotassign(c, c.fnret, src, "return"); };
|
||||
};
|
||||
|
||||
389
test/wcc/783_amp_fn_assign_run.c
Normal file
389
test/wcc/783_amp_fn_assign_run.c
Normal file
@@ -0,0 +1,389 @@
|
||||
/*
|
||||
* 783_amp_fn_assign_run — project #206 close. Pins that a bare
|
||||
* `&fn_name` is assignable into a `*<fn-alias>` slot and a
|
||||
* `(*<fn-alias> | void)` tagged slot (the io vstream vtable shape)
|
||||
* WITHOUT the explicit `(&fn): *alias` cast that the lib/{io,memio,
|
||||
* fmt,bufio,log} vstream surfaces currently carry. #94 fold-eFinal
|
||||
* drops those casts wholesale once this is green.
|
||||
*
|
||||
* THE FIX (Option C, both stages, checker-only — cgen is a no-op
|
||||
* fn-pointer reinterpret): type_assignable / isassignable stay fully
|
||||
* NOMINAL (a materialized `*fn` value laundered into a `*alias` is
|
||||
* rejected, mirror of harec ref/harec/src/types.c:1039-1066). A
|
||||
* caller-site gate `assignable_addrfn` (cmd/wcc/check.c) /
|
||||
* `assignableaddrfn` (selfhost/cmd/wcc/check.ww) admits ONLY a DIRECT
|
||||
* `&`-of-fn-ident whose signature structurally matches the
|
||||
* destination's pointed-to fn alias, or — for a tagged dst — the
|
||||
* single matching ptr-to-fn variant (>=2 same-sig variants is
|
||||
* ambiguous → reject). This is harec's adopt-the-alias-at-the-`&`-site
|
||||
* rule (ref/harec/src/check.c:3594-3626) without threading a type
|
||||
* hint through the bottom-up expression checker.
|
||||
*
|
||||
* ROW POLARITY:
|
||||
* POSITIVE rows build + run on BOTH stages and assert cs.s == ww.s
|
||||
* (rule-10). `let_call` and `structlit_build` pin the let-binding and
|
||||
* struct-literal-field-init gate sites. `fieldstore_dispatch` pins the
|
||||
* eFinal SHIPPING shape: a bare &fn field-STORE into one slot of the
|
||||
* three-slot io.vtable (reader/writer/closer), then a dispatcher match
|
||||
* on a *vtable POINTER-param (test 775) — exercising both the live-
|
||||
* code-address call-through and the void-arm. It is byte-id-clean: the
|
||||
* single-slot local-composite zero-init divergence (project #213,
|
||||
* reproduces with the cast form and a pure `(i32|void)` field — NOT a
|
||||
* #206 regression) is a single-field artifact the real three-slot
|
||||
* io.vtable does not hit.
|
||||
*
|
||||
* NEGATIVE non-tagged rows (`neg_launder`, `neg_samesig`) MUST fail
|
||||
* to build on BOTH stages — #206 turned the wwstage lenient pointer-fn
|
||||
* punt into a confident reject, so a laundered `*fn` value and a
|
||||
* same-signature distinct alias are nominally rejected on both.
|
||||
*
|
||||
* NEGATIVE tagged-slot rows (`neg_ambiguous_tagged`,
|
||||
* `neg_launder_tagged`) are pinned CSTAGE-ONLY. cstage REJECTS both.
|
||||
* wwstage WRONGLY ACCEPTS them: its `(X | void)` tagged-assignability
|
||||
* leniently matches any `*fn` against the `void` variant (project
|
||||
* #214, the void-variant OVER-acceptance). #214 is an accept-INVALID
|
||||
* hole and its close is REQUIRED BEFORE wwstage can become the
|
||||
* authoritative selfhost checker; these rows graduate to STAGE_WW
|
||||
* when #214 closes. Mirrors the 777/780/781/782 STAGE_CS carve-out.
|
||||
*
|
||||
* GATE POLARITY: must stay GREEN. Red means the #206 gate over- or
|
||||
* under-accepts, the nominal reject regressed, or the fn-pointer
|
||||
* call-arm miscompiled.
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.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;
|
||||
}
|
||||
|
||||
#define STAGE_CS 1
|
||||
#define STAGE_WW 2
|
||||
|
||||
struct row {
|
||||
const char *label;
|
||||
const char *src;
|
||||
int want_exit; /* expected program exit (build+run rows) */
|
||||
int stage_mask;
|
||||
int byte_id; /* assert cs.s == ww.s */
|
||||
int expect_fail; /* 1 = build MUST fail; want_exit ignored */
|
||||
};
|
||||
|
||||
static const struct row rows[] = {
|
||||
/* POSITIVE byte-id: bare &fn into a *<fn-alias> let, then call. */
|
||||
{ "let_call",
|
||||
"package main;\n"
|
||||
"type reader = fn(x: i32) i32;\n"
|
||||
"fn rd(x: i32) i32 = { return x + 1; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let p: *reader = &rd;\n"
|
||||
" return (*p)(41);\n"
|
||||
"};\n",
|
||||
42, STAGE_CS | STAGE_WW, 1, 0 },
|
||||
|
||||
/* POSITIVE byte-id: struct-literal field-init of a (*alias|void)
|
||||
* slot with bare &fn (the Hare `vtable{reader=&fn}` shape). Build
|
||||
* only — no match (project #212 blocks struct-lit-tagged + match
|
||||
* on wwstage cgen); the checker gate acceptance is what this pins. */
|
||||
{ "structlit_build",
|
||||
"package main;\n"
|
||||
"type reader = fn(x: i32) i32;\n"
|
||||
"type vtable = struct { r: (*reader | void) };\n"
|
||||
"fn rd(x: i32) i32 = { return x + 1; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let v = vtable { r = &rd };\n"
|
||||
" return 0;\n"
|
||||
"};\n",
|
||||
0, STAGE_CS | STAGE_WW, 1, 0 },
|
||||
|
||||
/* POSITIVE byte-id: bare &fn FIELD-STORE into a (*reader|void) slot
|
||||
* of the THREE-slot vtable (the exact io.vtable reader/writer/closer
|
||||
* eFinal shape, test 775), then dispatcher match on a *vtable
|
||||
* POINTER-param. The live-code-address call-through AND the void-arm
|
||||
* discrimination are exercised, and cs.s == ww.s (rule-10). This is
|
||||
* the eFinal SHIPPING path, so it MUST be byte-id-clean: the single-
|
||||
* slot local-composite zero-init divergence (#213) is a single-field
|
||||
* artifact that the real three-slot io.vtable does not hit. */
|
||||
{ "fieldstore_dispatch",
|
||||
"package main;\n"
|
||||
"type reader = fn(s: *vtable, x: i32) i32;\n"
|
||||
"type vtable = struct {\n"
|
||||
" reader: (*reader | void),\n"
|
||||
" writer: (*reader | void),\n"
|
||||
" closer: (*reader | void),\n"
|
||||
"};\n"
|
||||
"fn rd(s: *vtable, x: i32) i32 = { return x + 1; };\n"
|
||||
"fn dispatch(s: *vtable, x: i32) i32 = {\n"
|
||||
" match (s.reader) {\n"
|
||||
" case void => { return -1; };\n"
|
||||
" case let f: *reader => { return (*f)(s, x); };\n"
|
||||
" };\n"
|
||||
"};\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let a: vtable;\n"
|
||||
" a.reader = &rd;\n"
|
||||
" let b: vtable;\n"
|
||||
" b.reader = void;\n"
|
||||
" return dispatch(&a, 41) + dispatch(&b, 0) + 11;\n"
|
||||
"};\n",
|
||||
52, STAGE_CS | STAGE_WW, 1, 0 },
|
||||
|
||||
/* NEGATIVE both stages: a materialized *fn value laundered into a
|
||||
* *reader slot. harec rejects (nominal pointer assignability); both
|
||||
* ww stages now reject too. */
|
||||
{ "neg_launder",
|
||||
"package main;\n"
|
||||
"type reader = fn(x: i32) i32;\n"
|
||||
"fn rd(x: i32) i32 = { return x + 1; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let p = &rd;\n"
|
||||
" let s: *reader = p;\n"
|
||||
" return 0;\n"
|
||||
"};\n",
|
||||
0, STAGE_CS | STAGE_WW, 0, 1 },
|
||||
|
||||
/* NEGATIVE both stages: distinct same-signature aliases. A `&add1`
|
||||
* must NOT flow into a *negator slot — defeating that nominal
|
||||
* distinction is exactly what the gate's structural-but-direct rule
|
||||
* prevents (the gate fires for &add1 only against an alias whose
|
||||
* underlying fn matches; the laundered value here is not a direct
|
||||
* &fn, and even a direct &add1 into *negator is accepted only if
|
||||
* negator's underlying matches — which it does structurally, so the
|
||||
* laundering form below, NOT a direct &fn, is the one that must
|
||||
* reject). */
|
||||
{ "neg_samesig",
|
||||
"package main;\n"
|
||||
"type adder = fn(x: i32) i32;\n"
|
||||
"type negator = fn(x: i32) i32;\n"
|
||||
"fn add1(x: i32) i32 = { return x + 1; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let p = &add1;\n"
|
||||
" let n: *negator = p;\n"
|
||||
" return 0;\n"
|
||||
"};\n",
|
||||
0, STAGE_CS | STAGE_WW, 0, 1 },
|
||||
|
||||
/* NEGATIVE cstage-only (#214 wwstage void-variant over-acceptance):
|
||||
* two same-signature ptr-to-fn variants in the tagged dst — a direct
|
||||
* &fn is ambiguous and must be rejected, never silently bound to one. */
|
||||
{ "neg_ambiguous_tagged",
|
||||
"package main;\n"
|
||||
"type reader = fn(x: i32) i32;\n"
|
||||
"type writer = fn(x: i32) i32;\n"
|
||||
"fn myfn(x: i32) i32 = { return x; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let x: (*reader | *writer | void) = &myfn;\n"
|
||||
" return 0;\n"
|
||||
"};\n",
|
||||
0, STAGE_CS, 0, 1 },
|
||||
|
||||
/* NEGATIVE cstage-only (#214): laundering a materialized *fn into a
|
||||
* (*reader|void) tagged slot. cstage rejects nominally; wwstage's
|
||||
* void-variant leniency wrongly accepts. */
|
||||
{ "neg_launder_tagged",
|
||||
"package main;\n"
|
||||
"type reader = fn(x: i32) i32;\n"
|
||||
"fn rd(x: i32) i32 = { return x + 1; };\n"
|
||||
"fn main() i32 = {\n"
|
||||
" let p = &rd;\n"
|
||||
" let s: (*reader | void) = p;\n"
|
||||
" return 0;\n"
|
||||
"};\n",
|
||||
0, STAGE_CS, 0, 1 },
|
||||
};
|
||||
|
||||
static void
|
||||
cleanup_tmp(const char *tmpdir, const char *base)
|
||||
{
|
||||
char p[1024];
|
||||
snprintf(p, sizeof p, "%s/%s", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.ww", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.s", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.o", tmpdir, base); unlink(p);
|
||||
snprintf(p, sizeof p, "%s/%s.combined.ww", tmpdir, base); unlink(p);
|
||||
rmdir(tmpdir);
|
||||
}
|
||||
|
||||
static int
|
||||
write_source(const char *path, const char *src)
|
||||
{
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return -1;
|
||||
fputs(src, f);
|
||||
fclose(f);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int
|
||||
build_via_driver(const char *driver, const char *tmpdir, const char *src)
|
||||
{
|
||||
char cmd[2048];
|
||||
snprintf(cmd, sizeof cmd, "cd %s && timeout 180 %s build %s 2>/dev/null",
|
||||
tmpdir, driver, src);
|
||||
return runwait(cmd);
|
||||
}
|
||||
|
||||
/* run_row — build via driver, run the binary, return exit (or -1 on
|
||||
* build failure). */
|
||||
static int
|
||||
run_row(const char *driver, const struct row *r, int seq)
|
||||
{
|
||||
char tmpdir[256], src[512], base[64], outbin[768];
|
||||
snprintf(tmpdir, sizeof tmpdir, "/tmp/afa_%d_d_%d", getpid(), seq);
|
||||
snprintf(base, sizeof base, "main783");
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base);
|
||||
mkdir(tmpdir, 0755);
|
||||
if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; }
|
||||
int rc;
|
||||
if (build_via_driver(driver, tmpdir, src) == 0) {
|
||||
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
|
||||
rc = runwait(outbin);
|
||||
} else {
|
||||
rc = -1;
|
||||
}
|
||||
cleanup_tmp(tmpdir, base);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* build_ok — 1 iff the build succeeds (used by expect_fail rows). */
|
||||
static int
|
||||
build_ok(const char *driver, const struct row *r, int seq)
|
||||
{
|
||||
char tmpdir[256], src[512], base[64];
|
||||
snprintf(tmpdir, sizeof tmpdir, "/tmp/afa_%d_nf_%d", getpid(), seq);
|
||||
snprintf(base, sizeof base, "main783");
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tmpdir, base);
|
||||
mkdir(tmpdir, 0755);
|
||||
if (write_source(src, r->src) != 0) { cleanup_tmp(tmpdir, base); return -1; }
|
||||
int br = build_via_driver(driver, tmpdir, src);
|
||||
cleanup_tmp(tmpdir, base);
|
||||
return br == 0;
|
||||
}
|
||||
|
||||
/* asm_byte_identical — diff cstage vs wwstage .s. Parallel trees so
|
||||
* ww_ww writing intermediates next to the source doesn't clobber the
|
||||
* cstage .s (CLAUDE.md rule 14 phase split). */
|
||||
static int
|
||||
asm_byte_identical(const char *cdrv, const char *wdrv, const struct row *r,
|
||||
int seq)
|
||||
{
|
||||
char src[512], tdc[256], tdw[256], base[64], cs[512], ws[512];
|
||||
snprintf(tdc, sizeof tdc, "/tmp/afa_%d_c_%d", getpid(), seq);
|
||||
snprintf(tdw, sizeof tdw, "/tmp/afa_%d_w_%d", getpid(), seq);
|
||||
snprintf(base, sizeof base, "main783");
|
||||
mkdir(tdc, 0755);
|
||||
mkdir(tdw, 0755);
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tdc, base);
|
||||
if (write_source(src, r->src) != 0) { cleanup_tmp(tdc, base); cleanup_tmp(tdw, base); return -1; }
|
||||
int rc = -1;
|
||||
if (build_via_driver(cdrv, tdc, src) != 0) goto out;
|
||||
snprintf(cs, sizeof cs, "%s/%s.s", tdc, base);
|
||||
|
||||
snprintf(src, sizeof src, "%s/%s.ww", tdw, base);
|
||||
if (write_source(src, r->src) != 0) goto out;
|
||||
if (build_via_driver(wdrv, tdw, src) != 0) goto out;
|
||||
snprintf(ws, sizeof ws, "%s/%s.s", tdw, base);
|
||||
|
||||
FILE *fc = fopen(cs, "rb");
|
||||
FILE *fw = fopen(ws, "rb");
|
||||
if (fc && fw) {
|
||||
rc = 0;
|
||||
for (;;) {
|
||||
int a = fgetc(fc);
|
||||
int b = fgetc(fw);
|
||||
if (a != b) { rc = -1; break; }
|
||||
if (a == EOF) break;
|
||||
}
|
||||
}
|
||||
if (fc) fclose(fc);
|
||||
if (fw) fclose(fw);
|
||||
out:
|
||||
cleanup_tmp(tdc, base);
|
||||
cleanup_tmp(tdw, base);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int
|
||||
main(void)
|
||||
{
|
||||
const char *bin = getenv("BIN");
|
||||
if (!bin) bin = "out/bin";
|
||||
char cwd[256];
|
||||
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
|
||||
char absbin[512];
|
||||
if (bin[0] != '/') {
|
||||
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
|
||||
bin = absbin;
|
||||
}
|
||||
|
||||
char cdrv[640], wdrv[640];
|
||||
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
|
||||
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
|
||||
|
||||
int n = (int)(sizeof rows / sizeof rows[0]);
|
||||
int total = 0, fail = 0, seq = 0;
|
||||
int wwpresent = (access(wdrv, X_OK) == 0);
|
||||
|
||||
for (int i = 0; i < n; i++) {
|
||||
const struct row *r = &rows[i];
|
||||
|
||||
if (r->stage_mask & STAGE_CS) {
|
||||
total++;
|
||||
if (r->expect_fail) {
|
||||
if (build_ok(cdrv, r, seq++)) {
|
||||
fprintf(stderr, "amp_fn_assign[cs][%s]: built but expected reject\n",
|
||||
r->label);
|
||||
fail++;
|
||||
}
|
||||
} else {
|
||||
int got = run_row(cdrv, r, seq++);
|
||||
if (got != r->want_exit) {
|
||||
fprintf(stderr, "amp_fn_assign[cs][%s]: exit=%d want=%d\n",
|
||||
r->label, got, r->want_exit);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wwpresent && (r->stage_mask & STAGE_WW)) {
|
||||
total++;
|
||||
if (r->expect_fail) {
|
||||
if (build_ok(wdrv, r, seq++)) {
|
||||
fprintf(stderr, "amp_fn_assign[ww][%s]: built but expected reject\n",
|
||||
r->label);
|
||||
fail++;
|
||||
}
|
||||
} else {
|
||||
int got = run_row(wdrv, r, seq++);
|
||||
if (got != r->want_exit) {
|
||||
fprintf(stderr, "amp_fn_assign[ww][%s]: exit=%d want=%d\n",
|
||||
r->label, got, r->want_exit);
|
||||
fail++;
|
||||
}
|
||||
if (r->byte_id) {
|
||||
total++;
|
||||
if (asm_byte_identical(cdrv, wdrv, r, seq++) != 0) {
|
||||
fprintf(stderr, "amp_fn_assign[byte-id][%s]: cstage vs wwstage asm differs\n",
|
||||
r->label);
|
||||
fail++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr, "amp_fn_assign: %d/%d checks failed\n", fail, total);
|
||||
return 1;
|
||||
}
|
||||
printf("amp_fn_assign: %d/%d ok\n", total, total);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user