ww: add Hare-style is/as postfix ops on tagged unions

`e is T` returns bool (variant tag == T's index); `e as T` unwraps
to T or exit(1) on mismatch. Postfix, same precedence as `:` cast.
TK_IS / N_TYPETEST / N_TYPEASSERT appended at the tail of their
enums so every prior numeric value stays unchanged — the
990_selfhost wwdump-diff stays byte-clean.

Cgen mirrors the match-case slot-based load (tag at +0, value at
+8/+16), so an N_IDENT tagged-union local works just like a
match scrutinee. Selfhost cgen inlines the slot resolution
because the wwstage cgen drops sign bits on `*i32` output
parameters in this position.

Renames `errors.is` -> `errors.equal` (the only naming collision;
the existing comment already noted it shared shape with
strings.equal/bytes.equal).
This commit is contained in:
2026-05-11 23:21:08 +09:00
parent 8899ce5621
commit dd188ca460
16 changed files with 614 additions and 15 deletions

View File

@@ -81,6 +81,8 @@ nkname(Nkind k)
case N_TRYUNW: return "tryunw";
case N_MLET: return "mlet";
case N_MASSIGN: return "massign";
case N_TYPETEST: return "typetest";
case N_TYPEASSERT: return "typeassert";
case N_LAST: return "last";
}
return "?";

View File

@@ -710,6 +710,48 @@ cexpr(Checker *c, Node *n)
n->type = ty_void;
return n->type;
}
case N_TYPETEST: case N_TYPEASSERT: {
/* `e is T` → bool; `e as T` → T.
* Requires lhs to be a tagged union and T to be one of its
* variants. The variant-index lookup lives in cgen (it knows
* NAMED-vs-structural matching for the success-variant rules);
* here we just check the LHS shape and resolve T. */
Type *t = cexpr(c, n->lhs);
Type *vt = resolve_type(c, n->rhs);
/* Stash the variant on rhs->type — cgen reads it uniformly
* whether the expression returns bool (is) or the variant
* itself (as). */
if (n->rhs) n->rhs->type = vt;
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;
if (u == NULL || u->kind != TY_TAGGED) {
const char *op = (n->kind == N_TYPETEST) ? "is" : "as";
return n->type = err(c, n->pos,
"%s on non-tagged-union %s", op,
type_name(c->a, t));
}
/* Diagnostic-only: verify T appears as a variant. Mirrors
* cg_variant_match (NAMED ≡ pointer-identical, otherwise
* structural). Skipped silently if vt is ty_err. */
if (vt && vt != ty_err) {
int found = 0;
for (Tparam *p = u->params; p; p = p->next) {
if (p->type == NULL) continue;
if (p->type->kind == TY_NAMED &&
vt->kind == TY_NAMED) {
if (p->type == vt) { found = 1; break; }
} else if (p->type->kind == TY_NAMED ||
vt->kind == TY_NAMED) {
continue;
} else if (type_eq(p->type, vt)) {
found = 1; break;
}
}
if (!found)
err(c, n->pos, "%s is not a variant of %s",
type_name(c->a, vt), type_name(c->a, t));
}
return n->type = (n->kind == N_TYPETEST) ? ty_bool : vt;
}
case N_TRYPROP: case N_TRYUNW: {
Type *t = cexpr(c, n->lhs);
Type *u = (t && t->kind == TY_NAMED) ? t->under : t;

View File

@@ -670,6 +670,22 @@ parsepostfix(Parser *p, Node *lhs)
lhs = n;
break;
}
case TK_AS:
case TK_IS: {
/* Hare-style:
* e as T — assert lhs is variant T of its tagged
* union; abort if not. Yields T.
* e is T — bool: does lhs currently hold variant T?
* Postfix, same level as `:` cast. */
Tkind k = p->cur.kind;
advance(p);
Node *n = newnode(p->a,
k == TK_AS ? N_TYPEASSERT : N_TYPETEST, pp);
n->lhs = lhs;
n->rhs = parsetype(p);
lhs = n;
break;
}
default:
return lhs;
}

View File

@@ -29,6 +29,7 @@ static const struct kwent kwtab[] = {
{ "fn", TK_FN },
{ "for", TK_FOR },
{ "if", TK_IF },
{ "is", TK_IS },
{ "let", TK_LET },
{ "match", TK_MATCH },
{ "nil", TK_NIL },
@@ -89,6 +90,7 @@ tokname(Tkind k)
case TK_TRUE: return "true";
case TK_FALSE: return "false";
case TK_AS: return "as";
case TK_IS: return "is";
case TK_STATIC: return "static";
case TK_MATCH: return "match";
case TK_CONST: return "const";

View File

@@ -111,7 +111,7 @@ typedef enum {
TK_NIL,
TK_TRUE,
TK_FALSE,
TK_AS, /* reserved for future cast spelling, not active */
TK_AS, /* Hare-style type assertion: e as T */
TK_STATIC, /* Hare-style storage-class qualifier */
TK_MATCH, /* match expression head */
TK_CONST, /* const binding */
@@ -172,6 +172,11 @@ typedef enum {
TK_ARROW, /* -> (reserved) */
TK_FATARROW, /* => (match arms) */
/* Appended after TK_FATARROW (not grouped with the keyword block)
* to keep the numeric value of every existing kind unchanged —
* the selfhost wwdump-diff test (990) is byte-sensitive. */
TK_IS, /* Hare-style type test: e is T */
TK_LAST /* sentinel for tables */
} Tkind;
@@ -284,6 +289,12 @@ typedef enum {
N_MLET, /* let a, b = expr; list = N_LET stubs (str, lhs=type), rhs = expr */
N_MASSIGN, /* a, b = expr; list = lvalue exprs, rhs = expr */
/* Appended after N_MASSIGN — keeps the numeric value of every
* existing kind unchanged, so the selfhost AST dump still diffs
* byte-for-byte. lhs=value, rhs=variant type expr. */
N_TYPETEST, /* lhs is T → bool */
N_TYPEASSERT, /* lhs as T → T (abort if tag mismatch) */
N_LAST
} Nkind;