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:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -11,6 +11,7 @@
|
|||||||
/sym_link
|
/sym_link
|
||||||
/memiotest
|
/memiotest
|
||||||
/trypromote
|
/trypromote
|
||||||
|
/tagged_ptr_ret
|
||||||
|
|
||||||
# Per-module build artifacts. The .combined.ww files under selfhost/
|
# Per-module build artifacts. The .combined.ww files under selfhost/
|
||||||
# are intentionally tracked — they're frozen bootstrap inputs.
|
# are intentionally tracked — they're frozen bootstrap inputs.
|
||||||
|
|||||||
@@ -402,18 +402,21 @@ resolve_type(Checker *c, Node *n)
|
|||||||
/* Nullable pointer folding: `(*T | void)` collapses to a
|
/* Nullable pointer folding: `(*T | void)` collapses to a
|
||||||
* single 8-byte pointer slot; null bit pattern is the void
|
* single 8-byte pointer slot; null bit pattern is the void
|
||||||
* variant. Mirrors Hare's `(*T | null)`. Detected on exact
|
* 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) {
|
if (nv == 2) {
|
||||||
Tparam *a = head;
|
Tparam *a = head;
|
||||||
Tparam *b = head->next;
|
Tparam *b = head->next;
|
||||||
Type *au = (a->type && a->type->kind == TY_NAMED)
|
int aptr = a->type && a->type->kind == TY_PTR;
|
||||||
? a->type->under : a->type;
|
int bptr = b->type && b->type->kind == TY_PTR;
|
||||||
Type *bu = (b->type && b->type->kind == TY_NAMED)
|
int avoid = a->type && a->type->kind == TY_VOID
|
||||||
? b->type->under : b->type;
|
&& !a->type->iserror;
|
||||||
int aptr = au && au->kind == TY_PTR;
|
int bvoid = b->type && b->type->kind == TY_VOID
|
||||||
int bptr = bu && bu->kind == TY_PTR;
|
&& !b->type->iserror;
|
||||||
int avoid = au && au->kind == TY_VOID;
|
|
||||||
int bvoid = bu && bu->kind == TY_VOID;
|
|
||||||
if ((aptr && bvoid) || (avoid && bptr)) {
|
if ((aptr && bvoid) || (avoid && bptr)) {
|
||||||
t->nullable = 1;
|
t->nullable = 1;
|
||||||
t->size = 8;
|
t->size = 8;
|
||||||
|
|||||||
57
selfhost/test/tagged_ptr_ret.ww
Normal file
57
selfhost/test/tagged_ptr_ret.ww
Normal 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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user