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:
@@ -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 "?";
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user