From ea76ee4aa3e7a2daa780ecc66188e697bc7884ba Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 19 May 2026 19:10:21 +0900 Subject: [PATCH] cmd/wcc/check+test: don't fold (*T | !void) into nullable-ptr ABI resolve_type for N_TTAGGED was peeling NAMED aliases to TY_VOID before deciding the union is a nullable pointer, which caught (*T | nomem) (nomem = !void) and routed it through cstage's ptr-in-AX shortcut. wwstage's isnullabletype is purely AST-keyed on bare `void`, so any alias or error-tagged void naturally fell through to the general AX=tag, DX=word0 ABI. Rule 10 says align richer DOWN: gate the cstage classifier on iserror==0 so only the literal (*T | void) shape still folds to nullable-ptr. The literal void case stays intact for 700_e2e:642/661/1129. Smoke test selfhost/test/tagged_ptr_ret.ww exercises (*u8 | nomem) across both arms; cstage and wwstage now emit byte-identical asm modulo the pre-existing #20 fmt.formatfield divergence. --- .gitignore | 1 + cmd/wcc/check.c | 21 ++++++------ selfhost/test/tagged_ptr_ret.ww | 57 +++++++++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 selfhost/test/tagged_ptr_ret.ww diff --git a/.gitignore b/.gitignore index 1031ee68..3afb57ea 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ /sym_link /memiotest /trypromote +/tagged_ptr_ret # Per-module build artifacts. The .combined.ww files under selfhost/ # are intentionally tracked — they're frozen bootstrap inputs. diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index a0dd281d..fccf3f9f 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -402,18 +402,21 @@ resolve_type(Checker *c, Node *n) /* Nullable pointer folding: `(*T | void)` collapses to a * single 8-byte pointer slot; null bit pattern is the void * variant. Mirrors Hare's `(*T | null)`. Detected on exact - * two-variant shape with one TY_PTR and one TY_VOID. */ + * two-variant shape with one TY_PTR and one literal TY_VOID + * (not NAMED, not `!`-flagged): aligns DOWN to wwstage's + * isnullabletype which is AST-keyed and only matches a bare + * `void` name. Task #25 — `(*T | nomem)` where `nomem = !void` + * must take the general tagged-return ABI (AX=tag, DX=word0) + * so cstage and wwstage emit byte-identical asm. */ if (nv == 2) { Tparam *a = head; Tparam *b = head->next; - Type *au = (a->type && a->type->kind == TY_NAMED) - ? a->type->under : a->type; - Type *bu = (b->type && b->type->kind == TY_NAMED) - ? b->type->under : b->type; - int aptr = au && au->kind == TY_PTR; - int bptr = bu && bu->kind == TY_PTR; - int avoid = au && au->kind == TY_VOID; - int bvoid = bu && bu->kind == TY_VOID; + int aptr = a->type && a->type->kind == TY_PTR; + int bptr = b->type && b->type->kind == TY_PTR; + int avoid = a->type && a->type->kind == TY_VOID + && !a->type->iserror; + int bvoid = b->type && b->type->kind == TY_VOID + && !b->type->iserror; if ((aptr && bvoid) || (avoid && bptr)) { t->nullable = 1; t->size = 8; diff --git a/selfhost/test/tagged_ptr_ret.ww b/selfhost/test/tagged_ptr_ret.ww new file mode 100644 index 00000000..5131b1ef --- /dev/null +++ b/selfhost/test/tagged_ptr_ret.ww @@ -0,0 +1,57 @@ +// selfhost/test/tagged_ptr_ret.ww — smoke for the (*T | nomem) return ABI. +// +// Task #25 (ww-strings-redesign): cstage used to fold `(*T | !void)`-shaped +// returns into the nullable-pointer-in-AX encoding (richer optimization), +// while wwstage emitted the documented general tagged-return ABI +// (AX=tag, DX=word0). Per CLAUDE.md rule 10 the richer side aligns DOWN — +// cstage now restricts the nullable fold to literal `void` variants, so +// `(*T | nomem)` (`type nomem = !void;`) takes the general path on both +// stages and the 993/995 byte-identity tests stay green once #17 lands a +// (*T | nomem) signature in lib/. +// +// nomem is declared locally because it is not yet predeclared in the +// universe scope (that move is #17). Two match arms cover both runtime +// outcomes — success unwrap (tag=0, ptr payload in DX) and error +// propagation (tag=1) — exercising the same AX/DX ABI both stages must +// agree on. + +package test; + +import fmt; + +type nomem = !void; + +fn alloc1(fail: i64) (*u8 | nomem) = { + if (fail != 0i64) { let e: nomem; return e; }; + let buf: [1]u8; + return buf.ptr; +}; + +fn caller(fail: i64) (*u8 | nomem) = { + let p: *u8 = alloc1(fail)?; + return p; +}; + +export fn main() i32 = { + let rc: i32 = 0; + match (caller(0i64)) { + case let p: *u8 => { + fmt.println("ok"); + if (p == nil) { rc = 1; }; + }; + case nomem => { + fmt.println("unexpected nomem on ok path"); + rc = 2; + }; + }; + match (caller(1i64)) { + case let p: *u8 => { + fmt.println("unexpected ptr on err path"); + rc = 3; + }; + case nomem => { + fmt.println("nomem as expected"); + }; + }; + return rc; +};