wcc: Hare-style !T error marker on tagged-union variants

A type prefixed with `!` is flagged as an error variant. When any
variant in a tagged union carries the flag, `?` propagation uses
those (and only those) as the error subset; the unflagged variant
is the success type. The legacy "first variant = success" rule still
applies when no `!`-flag is present, so existing code keeps working.

- TK_NOT in parsetype → N_TBANG wrapper (lhs = inner type expr).
  Appended to Nkind tail for wwdump-diff byte stability.
- resolve_type N_TBANG: wraps primitives in a fresh Type copy so the
  iserror bit doesn't taint shared globals like ty_str/ty_i32; flips
  the bit in place on NAMED (already unique per alias decl).
- Type.iserror; type_named and typedecl inherit it from under.
- New check.c helpers: tagged_has_errflag, tagged_is_error_variant,
  tagged_success_type. N_TRYPROP uses them to find the error subset
  and verify each error variant is propagatable to the enclosing
  return.
- cgen mirrors with cg_tagged_success_tag + cg_variant_is_error.
  `?` compares AX against the success tag (no longer always 0) and
  remaps each error variant's tag for the enclosing fn. `!` aborts
  on any non-success tag.

strconv.invalid and strconv.overflow now use `!`-flagged shape
(`!i32` and `!void`) — visible signal in the API surface that they
are error types, matching Hare. The (i64 | invalid | overflow)
return shape and behavior are unchanged for callers; their match
arms still bind the same way.

Selfhost: lib/ww/parse/parse.ww recognises `!T` and emits N_TBANG.
The selfhost typechecker and cgen ignore the flag — none of the
selfhost sources use `!`, so byte-identity gates are unaffected.
The selfhost mirror catches up when there's a source using it.
This commit is contained in:
2026-05-12 02:39:54 +09:00
parent fd45aedf6c
commit 594a2bad62
14 changed files with 237 additions and 50 deletions

View File

@@ -329,13 +329,13 @@ export fn freearena(a: *arena) void = {
// (Hare uses !size / !void; ww uses i32 / void without the `!` mark).
// invalid — input wasn't a valid number in the requested format.
// Payload is the byte index of the first offending position (Hare
// strconv::invalid is `!size` carrying the same).
export type invalid = i32;
// Payload is the byte index of the first offending position. Mirrors
// Hare's strconv::invalid = !size (we use i32 instead of size).
export type invalid = !i32;
// overflow — input was valid but doesn't fit the target type. No
// payload (a single yes/no signal). Mirrors Hare's `!void` shape.
export type overflow = void;
// payload (a single yes/no signal). Mirrors Hare's !void shape.
export type overflow = !void;
// u64tos — write `v` in decimal into `buf` and return the byte count.
// Hare name; the buffer-in shape is the sanctioned Plan 9 subset of
@@ -1735,8 +1735,9 @@ def N_MASSIGN: i32 = 59;
def N_TYPETEST: i32 = 60;
def N_TYPEASSERT: i32 = 61;
def N_VOIDLIT: i32 = 62;
def N_TBANG: i32 = 63;
def N_LAST: i32 = 63;
def N_LAST: i32 = 64;
// ---- Node -------------------------------------------------------------
@@ -1838,6 +1839,7 @@ fn nkname(k: i32) str = {
if (k == N_TYPETEST) { return "typetest"; };
if (k == N_TYPEASSERT) { return "typeassert"; };
if (k == N_VOIDLIT) { return "voidlit"; };
if (k == N_TBANG) { return "tbang"; };
if (k == N_LAST) { return "last"; };
return "?";
};
@@ -2982,6 +2984,14 @@ fn parsetype(p: *parser) *node = {
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
if (p.curkind == TK_NOT) {
// `!T` — Hare error-flagged type wrapper.
advance(p);
let n: *node = newnode(p.a, N_TBANG, pf, pl, pc);
n.lhs = parsetype(p);
return n;
};
if (p.curkind == TK_STAR) {
advance(p);
let n: *node = newnode(p.a, N_TPTR, pf, pl, pc);

View File

@@ -329,13 +329,13 @@ export fn freearena(a: *arena) void = {
// (Hare uses !size / !void; ww uses i32 / void without the `!` mark).
// invalid — input wasn't a valid number in the requested format.
// Payload is the byte index of the first offending position (Hare
// strconv::invalid is `!size` carrying the same).
export type invalid = i32;
// Payload is the byte index of the first offending position. Mirrors
// Hare's strconv::invalid = !size (we use i32 instead of size).
export type invalid = !i32;
// overflow — input was valid but doesn't fit the target type. No
// payload (a single yes/no signal). Mirrors Hare's `!void` shape.
export type overflow = void;
// payload (a single yes/no signal). Mirrors Hare's !void shape.
export type overflow = !void;
// u64tos — write `v` in decimal into `buf` and return the byte count.
// Hare name; the buffer-in shape is the sanctioned Plan 9 subset of
@@ -1735,8 +1735,9 @@ def N_MASSIGN: i32 = 59;
def N_TYPETEST: i32 = 60;
def N_TYPEASSERT: i32 = 61;
def N_VOIDLIT: i32 = 62;
def N_TBANG: i32 = 63;
def N_LAST: i32 = 63;
def N_LAST: i32 = 64;
// ---- Node -------------------------------------------------------------
@@ -1838,6 +1839,7 @@ fn nkname(k: i32) str = {
if (k == N_TYPETEST) { return "typetest"; };
if (k == N_TYPEASSERT) { return "typeassert"; };
if (k == N_VOIDLIT) { return "voidlit"; };
if (k == N_TBANG) { return "tbang"; };
if (k == N_LAST) { return "last"; };
return "?";
};
@@ -2982,6 +2984,14 @@ fn parsetype(p: *parser) *node = {
let pl: i32 = p.curline;
let pc: i32 = p.curcol;
if (p.curkind == TK_NOT) {
// `!T` — Hare error-flagged type wrapper.
advance(p);
let n: *node = newnode(p.a, N_TBANG, pf, pl, pc);
n.lhs = parsetype(p);
return n;
};
if (p.curkind == TK_STAR) {
advance(p);
let n: *node = newnode(p.a, N_TPTR, pf, pl, pc);