From f6ac7fb2f8684dae6287c7b79d67d249db49298e Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 29 May 2026 18:05:53 +0900 Subject: [PATCH] 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. --- Makefile | 9 + cmd/wcc/check.c | 73 ++++- selfhost/cmd/w6c/main.combined.ww | 153 ++++++++++- selfhost/cmd/wcc/check.ww | 153 ++++++++++- selfhost/cmd/wwdump/main.combined.ww | 153 ++++++++++- test/wcc/783_amp_fn_assign_run.c | 389 +++++++++++++++++++++++++++ 6 files changed, 908 insertions(+), 22 deletions(-) create mode 100644 test/wcc/783_amp_fn_assign_run.c diff --git a/Makefile b/Makefile index 074d4278..097e8ca0 100644 --- a/Makefile +++ b/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 * / (*|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 \ diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 7b42dc72..f31cf336 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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. */ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 1735f67b..60eec532 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 `*`. 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"); }; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 25edd362..a10f64b3 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -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 `*`. 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"); }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index d360a848..bb1207d2 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 `*`. 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"); }; }; diff --git a/test/wcc/783_amp_fn_assign_run.c b/test/wcc/783_amp_fn_assign_run.c new file mode 100644 index 00000000..5294ff64 --- /dev/null +++ b/test/wcc/783_amp_fn_assign_run.c @@ -0,0 +1,389 @@ +/* + * 783_amp_fn_assign_run — project #206 close. Pins that a bare + * `&fn_name` is assignable into a `*` slot and a + * `(* | 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 +#include +#include +#include +#include + +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 * 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; +}