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

@@ -170,6 +170,45 @@ cg_variant_match(Type *vt, Type *src)
return type_eq(vt, src); return type_eq(vt, src);
} }
/* cg_tagged_success_tag — index of the success variant in a tagged
* union. Mirrors check.c tagged_success_type: explicit-flag mode
* picks the first non-`!`-marked variant; legacy mode picks index 0. */
static int
cg_tagged_success_tag(Type *t)
{
if (t == NULL) return 0;
if (t->kind == TY_NAMED) t = t->under;
if (t == NULL || t->kind != TY_TAGGED) return 0;
int has_err = 0;
for (Tparam *p = t->params; p; p = p->next)
if (p->type && p->type->iserror) { has_err = 1; break; }
if (!has_err) return 0;
int idx = 0;
for (Tparam *p = t->params; p; p = p->next, idx++)
if (p->type && !p->type->iserror) return idx;
return 0;
}
static int
cg_variant_is_error(Type *t, int idx)
{
if (t == NULL) return 0;
if (t->kind == TY_NAMED) t = t->under;
if (t == NULL || t->kind != TY_TAGGED) return 0;
int has_err = 0;
for (Tparam *p = t->params; p; p = p->next)
if (p->type && p->type->iserror) { has_err = 1; break; }
int i = 0;
for (Tparam *p = t->params; p; p = p->next, i++) {
if (i == idx) {
if (has_err) return p->type && p->type->iserror;
/* legacy: index 0 is success, rest are errors */
return idx != 0;
}
}
return 0;
}
/* Find the variant-tag index of `vt` inside the tagged-union type `t`. /* Find the variant-tag index of `vt` inside the tagged-union type `t`.
* Returns -1 if `t` is not tagged or `vt` does not match a variant. * Returns -1 if `t` is not tagged or `vt` does not match a variant.
* Used by N_MATCH dispatch and by the let/assign/return tag synthesis. */ * Used by N_MATCH dispatch and by the let/assign/return tag synthesis. */
@@ -1905,32 +1944,32 @@ cgexpr(Cg *c, Node *n, Local *locals)
} }
case N_TRYPROP: { case N_TRYPROP: {
/* Evaluate tagged value: AX=tag, DX=value0[, CX=value1]. /* Evaluate tagged value: AX=tag, DX=value0[, CX=value1].
* If tag != 0, propagate as the current function's return. * If the tag matches an error variant, propagate as the
* On success, unwrap to the success-variant ABI: ≤8B values * current function's return (with a tag remap to the
* land in AX; str values land in (AX=ptr, BX=len). * enclosing fn's variant order). On success, unwrap to the
* * success-variant ABI: ≤8B values in AX; str values in
* Tag remap: when operand and enclosing fn have different * (AX=ptr, BX=len). */
* variant orderings, the operand's error tag must be
* translated to the enclosing fn's tag for the same variant
* type. For each non-first variant V_i in operand at index
* i, if i != enclosing's index for V (call it j), emit a
* conditional MOV $j → AX. Identity cases emit nothing. */
cgexpr(c, n->lhs, locals); cgexpr(c, n->lhs, locals);
Type *u = n->lhs ? n->lhs->type : NULL; Type *u = n->lhs ? n->lhs->type : NULL;
if (u && u->kind == TY_NAMED) u = u->under; if (u && u->kind == TY_NAMED) u = u->under;
Type *r = cg_ret_type; Type *r = cg_ret_type;
if (r && r->kind == TY_NAMED) r = r->under; if (r && r->kind == TY_NAMED) r = r->under;
Type *first = (u && u->kind == TY_TAGGED && u->params) int s_tag = cg_tagged_success_tag(u);
? u->params->type : NULL; Type *succ_t = NULL;
int success_is_str = type_isstr(first); if (u && u->kind == TY_TAGGED) {
int i = 0;
for (Tparam *p = u->params; p; p = p->next, i++)
if (i == s_tag) { succ_t = p->type; break; }
}
int success_is_str = type_isstr(succ_t);
char *cont = mklabel(c, "tryprop_ok"); char *cont = mklabel(c, "tryprop_ok");
ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX));
ins1(c, A_JE, abranch(cont)); ins1(c, A_JE, abranch(cont));
if (u && r && r->kind == TY_TAGGED && u->params) { if (u && r && r->kind == TY_TAGGED && u->params) {
char *propret = mklabel(c, "tryprop_ret"); char *propret = mklabel(c, "tryprop_ret");
int i = 1; int i = 0;
for (Tparam *p = u->params->next; p; for (Tparam *p = u->params; p; p = p->next, i++) {
p = p->next, i++) { if (!cg_variant_is_error(u, i)) continue;
int j = cg_tag_for_variant(r, p->type); int j = cg_tag_for_variant(r, p->type);
if (j < 0) j = 0; if (j < 0) j = 0;
if (j == i) continue; if (j == i) continue;
@@ -1957,11 +1996,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
cgexpr(c, n->lhs, locals); cgexpr(c, n->lhs, locals);
Type *u = n->lhs ? n->lhs->type : NULL; Type *u = n->lhs ? n->lhs->type : NULL;
if (u && u->kind == TY_NAMED) u = u->under; if (u && u->kind == TY_NAMED) u = u->under;
Type *first = (u && u->kind == TY_TAGGED && u->params) int s_tag = cg_tagged_success_tag(u);
? u->params->type : NULL; Type *succ_t = NULL;
int success_is_str = type_isstr(first); if (u && u->kind == TY_TAGGED) {
int i = 0;
for (Tparam *p = u->params; p; p = p->next, i++)
if (i == s_tag) { succ_t = p->type; break; }
}
int success_is_str = type_isstr(succ_t);
char *cont = mklabel(c, "tryunw_ok"); char *cont = mklabel(c, "tryunw_ok");
ins2(c, A_CMPQ, aimm(0), areg(D_AX)); ins2(c, A_CMPQ, aimm(s_tag), areg(D_AX));
ins1(c, A_JE, abranch(cont)); ins1(c, A_JE, abranch(cont));
ins2(c, A_MOVQ, aimm(1), areg(D_DI)); ins2(c, A_MOVQ, aimm(1), areg(D_DI));
ins2(c, A_MOVQ, aimm(60), areg(D_AX)); ins2(c, A_MOVQ, aimm(60), areg(D_AX));

View File

@@ -84,6 +84,7 @@ nkname(Nkind k)
case N_TYPETEST: return "typetest"; case N_TYPETEST: return "typetest";
case N_TYPEASSERT: return "typeassert"; case N_TYPEASSERT: return "typeassert";
case N_VOIDLIT: return "voidlit"; case N_VOIDLIT: return "voidlit";
case N_TBANG: return "tbang";
case N_LAST: return "last"; case N_LAST: return "last";
} }
return "?"; return "?";

View File

@@ -99,11 +99,66 @@ variant_present(Tparam *head, Type *vt)
return 0; return 0;
} }
/* tagged_has_errflag — true iff any variant is `!`-marked. Determines
* whether the union uses Hare's explicit error subset or the legacy
* "first variant = success" convention. */
static int
tagged_has_errflag(Type *u)
{
if (u == NULL || u->kind != TY_TAGGED) return 0;
for (Tparam *p = u->params; p; p = p->next)
if (p->type && p->type->iserror) return 1;
return 0;
}
/* tagged_is_error_variant — does `v` (a variant of `u`) belong to
* the error subset? Explicit-flag mode: only variants with iserror=1.
* Legacy mode (no flags): everything except the first variant. */
static int
tagged_is_error_variant(Type *u, Type *v)
{
if (u == NULL || u->kind != TY_TAGGED || v == NULL) return 0;
if (tagged_has_errflag(u)) return v->iserror != 0;
/* legacy: first variant is success, rest are errors */
return u->params && u->params->type != v;
}
/* tagged_success_type — the success variant's type. Explicit-flag
* mode: the first non-flagged variant. Legacy: the first variant. */
static Type *
tagged_success_type(Type *u)
{
if (u == NULL || u->kind != TY_TAGGED) return NULL;
if (tagged_has_errflag(u)) {
for (Tparam *p = u->params; p; p = p->next)
if (p->type && !p->type->iserror) return p->type;
return NULL;
}
return u->params ? u->params->type : NULL;
}
static Type * static Type *
resolve_type(Checker *c, Node *n) resolve_type(Checker *c, Node *n)
{ {
if (n == NULL) return ty_void; if (n == NULL) return ty_void;
switch (n->kind) { switch (n->kind) {
case N_TBANG: {
/* `!T` — mark the resolved type as an error type. Wrap
* primitives in a fresh NAMED-less copy so we don't taint
* the shared ty_void / ty_str / ty_i32 globals. NAMED
* types are already unique per alias decl, so we can flip
* the bit in place. */
Type *t = resolve_type(c, n->lhs);
if (t == NULL || t == ty_err) return t;
if (t->kind == TY_NAMED) {
t->iserror = 1;
return t;
}
Type *t2 = newtype(c->a, t->kind);
*t2 = *t;
t2->iserror = 1;
return t2;
}
case N_TNAME: case N_TNAME:
return resolve_typename(c, n); return resolve_typename(c, n);
case N_TPTR: case N_TPTR:
@@ -890,15 +945,20 @@ cexpr(Checker *c, Node *n)
n->kind == N_TRYPROP ? "?" : "!", n->kind == N_TRYPROP ? "?" : "!",
type_name(c->a, t)); type_name(c->a, t));
} }
/* Convention: first variant is the success type; the remaining /* Error subset = `!`-flagged variants (Hare semantics) or
* variants are the error subset. * everything-but-first when no flags are present (legacy).
* *
* For ? : each error variant must be propagatable — i.e. it * For ? : each error variant must be propagatable — i.e. it
* must be a variant of the enclosing function's return type * must be a variant of the enclosing function's return type
* (so the caller can match on it). cgen does the tag remap. * (so the caller can match on it). cgen does the tag remap.
* For ! : no propagation, so no subset check. */ * For ! : no propagation, so no subset check. */
Tparam *first = u->params; Type *succ = tagged_success_type(u);
if (n->kind == N_TRYPROP && first && first->next) { int has_errors = 0;
for (Tparam *p = u->params; p; p = p->next)
if (tagged_is_error_variant(u, p->type)) {
has_errors = 1; break;
}
if (n->kind == N_TRYPROP && has_errors) {
Type *r = c->ret; Type *r = c->ret;
Type *ru = (r && r->kind == TY_NAMED) ? r->under : r; Type *ru = (r && r->kind == TY_NAMED) ? r->under : r;
if (ru == NULL || ru->kind != TY_TAGGED) { if (ru == NULL || ru->kind != TY_TAGGED) {
@@ -907,7 +967,9 @@ cexpr(Checker *c, Node *n)
"union to propagate errors (got %s)", "union to propagate errors (got %s)",
type_name(c->a, r)); type_name(c->a, r));
} else { } else {
for (Tparam *e = first->next; e; e = e->next) { for (Tparam *e = u->params; e; e = e->next) {
if (!tagged_is_error_variant(u, e->type))
continue;
int ok = 0; int ok = 0;
for (Tparam *p = ru->params; p; for (Tparam *p = ru->params; p;
p = p->next) p = p->next)
@@ -923,7 +985,7 @@ cexpr(Checker *c, Node *n)
} }
} }
} }
return n->type = first ? first->type : ty_err; return n->type = succ ? succ : ty_err;
} }
case N_TUPLE: { case N_TUPLE: {
/* keep untyped element types; assignability is checked /* keep untyped element types; assignability is checked
@@ -1223,6 +1285,7 @@ check_file(Checker *c, Node *file)
if (under) { if (under) {
d->type->size = under->size; d->type->size = under->size;
d->type->align = under->align; d->type->align = under->align;
d->type->iserror = under->iserror;
} }
} }
for (Node *d = file->list; d; d = d->next) { for (Node *d = file->list; d; d = d->next) {

View File

@@ -151,6 +151,16 @@ parsetype(Parser *p)
{ {
Pos pp = p->cur.pos; Pos pp = p->cur.pos;
switch (p->cur.kind) { switch (p->cur.kind) {
case TK_NOT: {
/* `!T` — error-flagged type. The flag propagates through
* NAMED aliases and lives on the underlying Type, not on
* a wrapper. The AST keeps an N_TBANG wrapper so prints
* and selfhost can recognise the marker. */
advance(p);
Node *n = newnode(p->a, N_TBANG, pp);
n->lhs = parsetype(p);
return n;
}
case TK_STAR: { case TK_STAR: {
advance(p); advance(p);
Node *n = newnode(p->a, N_TPTR, pp); Node *n = newnode(p->a, N_TPTR, pp);

View File

@@ -122,6 +122,7 @@ type_named(Arena *a, const char *name, Type *under)
if (under) { if (under) {
t->size = under->size; t->size = under->size;
t->align = under->align; t->align = under->align;
t->iserror = under->iserror;
} }
return t; return t;
} }

View File

@@ -296,6 +296,7 @@ typedef enum {
N_TYPETEST, /* lhs is T → bool */ N_TYPETEST, /* lhs is T → bool */
N_TYPEASSERT, /* lhs as T → T (abort if tag mismatch) */ N_TYPEASSERT, /* lhs as T → T (abort if tag mismatch) */
N_VOIDLIT, /* `void` as expression — zero-size void value */ N_VOIDLIT, /* `void` as expression — zero-size void value */
N_TBANG, /* `!T` — error-flagged type. lhs = inner type. */
N_LAST N_LAST
} Nkind; } Nkind;
@@ -403,6 +404,10 @@ struct Type {
int variadic; int variadic;
const char *name; /* named alias / debug */ const char *name; /* named alias / debug */
Type *under; /* underlying resolved type for NAMED */ Type *under; /* underlying resolved type for NAMED */
int iserror;/* Hare-style `!T` error mark; propagates
* through NAMED aliases. Variants with
* iserror=1 are the propagation target of
* the `?` operator. */
}; };
extern Type *ty_void, *ty_bool, *ty_rune; extern Type *ty_void, *ty_bool, *ty_rune;

View File

@@ -4,13 +4,13 @@
// (Hare uses !size / !void; ww uses i32 / void without the `!` mark). // (Hare uses !size / !void; ww uses i32 / void without the `!` mark).
// invalid — input wasn't a valid number in the requested format. // invalid — input wasn't a valid number in the requested format.
// Payload is the byte index of the first offending position (Hare // Payload is the byte index of the first offending position. Mirrors
// strconv::invalid is `!size` carrying the same). // Hare's strconv::invalid = !size (we use i32 instead of size).
export type invalid = i32; export type invalid = !i32;
// overflow — input was valid but doesn't fit the target type. No // overflow — input was valid but doesn't fit the target type. No
// payload (a single yes/no signal). Mirrors Hare's `!void` shape. // payload (a single yes/no signal). Mirrors Hare's !void shape.
export type overflow = void; export type overflow = !void;
// u64tos — write `v` in decimal into `buf` and return the byte count. // 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 // Hare name; the buffer-in shape is the sanctioned Plan 9 subset of

View File

@@ -89,8 +89,9 @@ def N_MASSIGN: i32 = 59;
def N_TYPETEST: i32 = 60; def N_TYPETEST: i32 = 60;
def N_TYPEASSERT: i32 = 61; def N_TYPEASSERT: i32 = 61;
def N_VOIDLIT: i32 = 62; def N_VOIDLIT: i32 = 62;
def N_TBANG: i32 = 63;
def N_LAST: i32 = 63; def N_LAST: i32 = 64;
// ---- Node ------------------------------------------------------------- // ---- Node -------------------------------------------------------------
@@ -192,6 +193,7 @@ fn nkname(k: i32) str = {
if (k == N_TYPETEST) { return "typetest"; }; if (k == N_TYPETEST) { return "typetest"; };
if (k == N_TYPEASSERT) { return "typeassert"; }; if (k == N_TYPEASSERT) { return "typeassert"; };
if (k == N_VOIDLIT) { return "voidlit"; }; if (k == N_VOIDLIT) { return "voidlit"; };
if (k == N_TBANG) { return "tbang"; };
if (k == N_LAST) { return "last"; }; if (k == N_LAST) { return "last"; };
return "?"; return "?";
}; };

View File

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

View File

@@ -221,13 +221,13 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = {
// (Hare uses !size / !void; ww uses i32 / void without the `!` mark). // (Hare uses !size / !void; ww uses i32 / void without the `!` mark).
// invalid — input wasn't a valid number in the requested format. // invalid — input wasn't a valid number in the requested format.
// Payload is the byte index of the first offending position (Hare // Payload is the byte index of the first offending position. Mirrors
// strconv::invalid is `!size` carrying the same). // Hare's strconv::invalid = !size (we use i32 instead of size).
export type invalid = i32; export type invalid = !i32;
// overflow — input was valid but doesn't fit the target type. No // overflow — input was valid but doesn't fit the target type. No
// payload (a single yes/no signal). Mirrors Hare's `!void` shape. // payload (a single yes/no signal). Mirrors Hare's !void shape.
export type overflow = void; export type overflow = !void;
// u64tos — write `v` in decimal into `buf` and return the byte count. // 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 // Hare name; the buffer-in shape is the sanctioned Plan 9 subset of

View File

@@ -135,6 +135,16 @@ static const struct row rows[] = {
{ "fn inner() (i32 | str | bool) = { return 1; }; " { "fn inner() (i32 | str | bool) = { return 1; }; "
"fn outer() (i64 | bool | str) = { let v: i32 = inner()?; return v: i64; };", "fn outer() (i64 | bool | str) = { let v: i32 = inner()?; return v: i64; };",
"ok" }, /* reversed-order error subset is OK */ "ok" }, /* reversed-order error subset is OK */
/* `!`-flagged error variants (Hare's explicit error marker) */
{ "type invalid = !i32; "
"fn parse() (i64 | invalid) = { return 5: invalid; }; "
"fn caller() (i64 | invalid) = { let v: i64 = parse()?; return v + 1; };",
"ok" },
{ "type invalid = !i32; type overflow = !void; "
"fn parse() (invalid | i64 | overflow) = { return 0i64; }; "
"fn caller() i32 = { let v: i64 = parse()?; return v: i32; };",
"enclosing function must return a tagged union" },
}; };
int int

View File

@@ -538,6 +538,29 @@ static const struct row rows[] = {
" };\n" " };\n"
" return 0;\n" " return 0;\n"
"};", 3 }, "};", 3 },
/* `!`-flagged error variants: success picked by absence of `!`,
* errors picked by presence. Tag remap still works across
* different variant orders between operand and enclosing fn. */
{ "type invalid = !i32;\n"
"type overflow = !void;\n"
"fn inner(n: i32) (invalid | i64 | overflow) = {\n"
" if (n == 0) { return 7: invalid; };\n"
" if (n < 0) { return void: overflow; };\n"
" return n: i64 + 1000;\n"
"};\n"
"fn outer(n: i32) (overflow | i64 | invalid) = {\n"
" let v: i64 = inner(n)?;\n"
" return v + 1;\n"
"};\n"
"fn main() i32 = {\n"
" let r: (overflow | i64 | invalid) = outer(0);\n"
" match (r) {\n"
" case let v: i64 => return v: i32;\n"
" case let e: invalid => return e + 100;\n"
" case let e: overflow => return 999;\n"
" };\n"
" return 0;\n"
"};", 107 },
/* ? with reversed error variant order: operand (i32 | str | bool), /* ? with reversed error variant order: operand (i32 | str | bool),
* enclosing (i64 | bool | str). Tag remap maps str:1→2, bool:2→1. */ * enclosing (i64 | bool | str). Tag remap maps str:1→2, bool:2→1. */
{ "fn inner(n: i32) (i32 | str | bool) = {\n" { "fn inner(n: i32) (i32 | str | bool) = {\n"