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.
This commit is contained in:
2026-05-19 19:10:21 +09:00
parent 3fe968c8a0
commit ea76ee4aa3
3 changed files with 70 additions and 9 deletions

View File

@@ -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;
};