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

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

View File

@@ -99,11 +99,66 @@ variant_present(Tparam *head, Type *vt)
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 *
resolve_type(Checker *c, Node *n)
{
if (n == NULL) return ty_void;
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:
return resolve_typename(c, n);
case N_TPTR:
@@ -890,15 +945,20 @@ cexpr(Checker *c, Node *n)
n->kind == N_TRYPROP ? "?" : "!",
type_name(c->a, t));
}
/* Convention: first variant is the success type; the remaining
* variants are the error subset.
/* Error subset = `!`-flagged variants (Hare semantics) or
* everything-but-first when no flags are present (legacy).
*
* For ? : each error variant must be propagatable — i.e. it
* must be a variant of the enclosing function's return type
* (so the caller can match on it). cgen does the tag remap.
* For ! : no propagation, so no subset check. */
Tparam *first = u->params;
if (n->kind == N_TRYPROP && first && first->next) {
Type *succ = tagged_success_type(u);
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 *ru = (r && r->kind == TY_NAMED) ? r->under : r;
if (ru == NULL || ru->kind != TY_TAGGED) {
@@ -907,7 +967,9 @@ cexpr(Checker *c, Node *n)
"union to propagate errors (got %s)",
type_name(c->a, r));
} 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;
for (Tparam *p = ru->params; p;
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: {
/* keep untyped element types; assignability is checked
@@ -1223,6 +1285,7 @@ check_file(Checker *c, Node *file)
if (under) {
d->type->size = under->size;
d->type->align = under->align;
d->type->iserror = under->iserror;
}
}
for (Node *d = file->list; d; d = d->next) {

View File

@@ -151,6 +151,16 @@ parsetype(Parser *p)
{
Pos pp = p->cur.pos;
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: {
advance(p);
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) {
t->size = under->size;
t->align = under->align;
t->iserror = under->iserror;
}
return t;
}

View File

@@ -296,6 +296,7 @@ typedef enum {
N_TYPETEST, /* lhs is T → bool */
N_TYPEASSERT, /* lhs as T → T (abort if tag mismatch) */
N_VOIDLIT, /* `void` as expression — zero-size void value */
N_TBANG, /* `!T` — error-flagged type. lhs = inner type. */
N_LAST
} Nkind;
@@ -403,6 +404,10 @@ struct Type {
int variadic;
const char *name; /* named alias / debug */
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;