diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index a74a638b..1c2f3ab8 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1936,6 +1936,71 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, areg(D_DX), areg(D_AX)); break; } + case N_TYPETEST: { + /* `e is T` — Compare scrutinee tag against T's variant index. + * Result is bool (0/1) in AX. We only need the tag, not the + * value words, so cgexpr's AX-only result is enough for both + * register-returning callers (AX=tag) and N_IDENT locals + * (cgexpr loads slot+0 → AX, which is the tag). */ + cgexpr(c, n->lhs, locals); + Type *u = n->lhs ? n->lhs->type : NULL; + if (u && u->kind == TY_NAMED) u = u->under; + Type *vt = n->rhs ? n->rhs->type : NULL; + int tag = cg_tag_for_variant(u, vt); + char *ne = mklabel(c, "is_ne"); + char *done = mklabel(c, "is_done"); + ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX)); + ins1(c, A_JNE, abranch(ne)); + ins2(c, A_MOVQ, aimm(1), areg(D_AX)); + ins1(c, A_JMP, abranch(done)); + label(c, ne); + ins2(c, A_MOVQ, aimm(0), areg(D_AX)); + label(c, done); + break; + } + case N_TYPEASSERT: { + /* `e as T` — abort if tag != T's variant index; otherwise + * unwrap value to T's ABI: scalar/ptr variants land in AX; + * 16B str variants in AX:BX. + * + * We need both tag *and* value words. For an N_IDENT local + * the value lives at slot+8/+16 — cgexpr's single-MOVQ path + * does not load it. Mirror match's pattern: resolve a slot + * offset (existing local or a fresh @asrt_spill) and index + * out tag/value from memory. */ + Node *s = n->lhs; + Type *st = s ? s->type : NULL; + Type *u = (st && st->kind == TY_NAMED) ? st->under : st; + Type *vt = n->type; + int tag = cg_tag_for_variant(u, vt); + int slot_size = (u && u->kind == TY_TAGGED) ? (int)u->size : 16; + int sl_off = 0; + if (s && s->kind == N_IDENT && s->str) { + sl_off = localfind(locals, s->str); + } + if (sl_off == 0) { + sl_off = localoff(c, &locals, "@asrt_spill", + slot_size, cg_frame); + cgexpr(c, s, locals); + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0)); + ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, sl_off + 8)); + if (slot_size > 16) + ins2(c, A_MOVQ, areg(D_CX), + amem(D_BP, sl_off + 16)); + } + char *ok = mklabel(c, "asrt_ok"); + ins2(c, A_MOVQ, amem(D_BP, sl_off + 0), areg(D_AX)); + ins2(c, A_CMPQ, aimm(tag < 0 ? 0 : tag), areg(D_AX)); + ins1(c, A_JE, abranch(ok)); + ins2(c, A_MOVQ, aimm(1), areg(D_DI)); + ins2(c, A_MOVQ, aimm(60), areg(D_AX)); + ins0(c, A_SYSCALL); + label(c, ok); + ins2(c, A_MOVQ, amem(D_BP, sl_off + 8), areg(D_AX)); + if (type_isstr(vt)) + ins2(c, A_MOVQ, amem(D_BP, sl_off + 16), areg(D_BX)); + break; + } case N_CAST: { int from_f = node_isfloat(n->lhs); int to_f = cg_isfloat(n->type); diff --git a/cmd/wcc/ast.c b/cmd/wcc/ast.c index 3c4ef184..0e06e6fc 100644 --- a/cmd/wcc/ast.c +++ b/cmd/wcc/ast.c @@ -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 "?"; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 425b224d..1b763eb7 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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; diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index baba84c9..b77246ee 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -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; } diff --git a/cmd/wcc/tok.c b/cmd/wcc/tok.c index 18312785..418d83d3 100644 --- a/cmd/wcc/tok.c +++ b/cmd/wcc/tok.c @@ -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"; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 9d8f5e07..d81ee5f3 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -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; diff --git a/lib/bufio/bufio.ww b/lib/bufio/bufio.ww index 5c745c16..3042926b 100644 --- a/lib/bufio/bufio.ww +++ b/lib/bufio/bufio.ww @@ -52,7 +52,7 @@ export fn avail(b: *buf) i32 = { // Distinct alias so `(str | linerr)` has two variant types the // tagged-union machinery can keep apart at the tag level. The error -// variant carries a short description; callers compare with errors.is +// variant carries a short description; callers compare with errors.equal // or just inspect by length. type linerr = str; diff --git a/lib/errors/errors.ww b/lib/errors/errors.ww index d37273b8..d736b040 100644 --- a/lib/errors/errors.ww +++ b/lib/errors/errors.ww @@ -16,10 +16,10 @@ export fn isnil(e: error) bool = { return e.len == 0; }; -// is — compare an error against a sentinel (or any other error). Pure -// byte equality; same shape as strings.equal but kept here so callers -// don't have to pull in strings just to compare. -export fn is(e: error, want: error) bool = { +// equal — compare an error against a sentinel (or any other error). +// Pure byte equality. (Was `errors.is` before `is` became a keyword +// for tagged-union type-tests; rename matches bytes.equal / strings.equal.) +export fn equal(e: error, want: error) bool = { if (e.len != want.len) { return false; }; let i: i32 = 0; for (i < e.len) { diff --git a/lib/ww/ast.ww b/lib/ww/ast.ww index 75e4e93c..d8772200 100644 --- a/lib/ww/ast.ww +++ b/lib/ww/ast.ww @@ -84,7 +84,12 @@ def N_TRYUNW: i32 = 57; def N_MLET: i32 = 58; def N_MASSIGN: i32 = 59; -def N_LAST: i32 = 60; +// Appended at the tail to keep all prior N_* values stable. The +// 990_selfhost test diffs astprint against the C side byte-for-byte. +def N_TYPETEST: i32 = 60; +def N_TYPEASSERT: i32 = 61; + +def N_LAST: i32 = 62; // ---- Node ------------------------------------------------------------- @@ -183,6 +188,8 @@ fn nkname(k: i32) str = { if (k == N_TRYUNW) { return "tryunw"; }; if (k == N_MLET) { return "mlet"; }; if (k == N_MASSIGN) { return "massign"; }; + if (k == N_TYPETEST) { return "typetest"; }; + if (k == N_TYPEASSERT) { return "typeassert"; }; if (k == N_LAST) { return "last"; }; return "?"; }; diff --git a/lib/ww/lex/tok.ww b/lib/ww/lex/tok.ww index c5a81f6c..18eea126 100644 --- a/lib/ww/lex/tok.ww +++ b/lib/ww/lex/tok.ww @@ -105,7 +105,12 @@ def TK_LARROW: i32 = 79; def TK_ARROW: i32 = 80; def TK_FATARROW: i32 = 81; -def TK_LAST: i32 = 82; +// Appended at the tail (not grouped with the keyword block) so every +// pre-existing TK_* value stays unchanged — the 990_selfhost test +// diffs wwdump output against the C side, byte for byte. +def TK_IS: i32 = 82; + +def TK_LAST: i32 = 83; // ---- Pos / Tok -------------------------------------------------------- // @@ -164,6 +169,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = { if (streqn(p, "fn", n)) { return TK_FN; }; if (streqn(p, "for", n)) { return TK_FOR; }; if (streqn(p, "if", n)) { return TK_IF; }; + if (streqn(p, "is", n)) { return TK_IS; }; if (streqn(p, "let", n)) { return TK_LET; }; if (streqn(p, "match", n)) { return TK_MATCH; }; if (streqn(p, "nil", n)) { return TK_NIL; }; @@ -215,6 +221,7 @@ export fn tokname(k: i32) str = { if (k == TK_TRUE) { return "true"; }; if (k == TK_FALSE) { return "false"; }; if (k == TK_AS) { return "as"; }; + if (k == TK_IS) { return "is"; }; if (k == TK_STATIC) { return "static"; }; if (k == TK_MATCH) { return "match"; }; if (k == TK_CONST) { return "const"; }; diff --git a/lib/ww/parse/expr.ww b/lib/ww/parse/expr.ww index 2e40837d..84652734 100644 --- a/lib/ww/parse/expr.ww +++ b/lib/ww/parse/expr.ww @@ -306,6 +306,26 @@ fn parsepostfix(p: *parser, lhs: *node) *node = { cur = n; continue; }; + // Hare-style postfix: + // `e as T` — assert lhs is variant T (abort otherwise) → T + // `e is T` — bool: does lhs currently hold variant T? + // Same precedence level as the `:` cast. + if (p.curkind == TK_AS) { + advance(p); + let n: *node = newnode(p.a, N_TYPEASSERT, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + if (p.curkind == TK_IS) { + advance(p); + let n: *node = newnode(p.a, N_TYPETEST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; break; }; return cur; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f558ae62..ad8004b4 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -545,7 +545,12 @@ def TK_LARROW: i32 = 79; def TK_ARROW: i32 = 80; def TK_FATARROW: i32 = 81; -def TK_LAST: i32 = 82; +// Appended at the tail (not grouped with the keyword block) so every +// pre-existing TK_* value stays unchanged — the 990_selfhost test +// diffs wwdump output against the C side, byte for byte. +def TK_IS: i32 = 82; + +def TK_LAST: i32 = 83; // ---- Pos / Tok -------------------------------------------------------- // @@ -604,6 +609,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = { if (streqn(p, "fn", n)) { return TK_FN; }; if (streqn(p, "for", n)) { return TK_FOR; }; if (streqn(p, "if", n)) { return TK_IF; }; + if (streqn(p, "is", n)) { return TK_IS; }; if (streqn(p, "let", n)) { return TK_LET; }; if (streqn(p, "match", n)) { return TK_MATCH; }; if (streqn(p, "nil", n)) { return TK_NIL; }; @@ -655,6 +661,7 @@ export fn tokname(k: i32) str = { if (k == TK_TRUE) { return "true"; }; if (k == TK_FALSE) { return "false"; }; if (k == TK_AS) { return "as"; }; + if (k == TK_IS) { return "is"; }; if (k == TK_STATIC) { return "static"; }; if (k == TK_MATCH) { return "match"; }; if (k == TK_CONST) { return "const"; }; @@ -1716,7 +1723,12 @@ def N_TRYUNW: i32 = 57; def N_MLET: i32 = 58; def N_MASSIGN: i32 = 59; -def N_LAST: i32 = 60; +// Appended at the tail to keep all prior N_* values stable. The +// 990_selfhost test diffs astprint against the C side byte-for-byte. +def N_TYPETEST: i32 = 60; +def N_TYPEASSERT: i32 = 61; + +def N_LAST: i32 = 62; // ---- Node ------------------------------------------------------------- @@ -1815,6 +1827,8 @@ fn nkname(k: i32) str = { if (k == N_TRYUNW) { return "tryunw"; }; if (k == N_MLET) { return "mlet"; }; if (k == N_MASSIGN) { return "massign"; }; + if (k == N_TYPETEST) { return "typetest"; }; + if (k == N_TYPEASSERT) { return "typeassert"; }; if (k == N_LAST) { return "last"; }; return "?"; }; @@ -2276,6 +2290,26 @@ fn parsepostfix(p: *parser, lhs: *node) *node = { cur = n; continue; }; + // Hare-style postfix: + // `e as T` — assert lhs is variant T (abort otherwise) → T + // `e is T` — bool: does lhs currently hold variant T? + // Same precedence level as the `:` cast. + if (p.curkind == TK_AS) { + advance(p); + let n: *node = newnode(p.a, N_TYPEASSERT, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + if (p.curkind == TK_IS) { + advance(p); + let n: *node = newnode(p.a, N_TYPETEST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; break; }; return cur; @@ -4863,6 +4897,115 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == N_CALL) { cgcall(c, n); return; }; if (k == N_ASSIGN) { cgassign(c, n); return; }; + + if (k == N_TYPETEST) { cgtypetest(c, n); return; }; + if (k == N_TYPEASSERT) { cgtypeassert(c, n); return; }; +}; + +// cgtagvariantidx — find the 0-based variant index of `vt` inside the +// tagged-union type expression `tagged`. -1 if `tagged` isn't an +// N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch +// does inline; pulled out so `is` / `as` can reuse it. +fn cgtagvariantidx(tagged: *node, vt: *node) i32 = { + if (tagged == nil) { return -1; }; + if (vt == nil) { return -1; }; + if (tagged.kind != N_TTAGGED) { return -1; }; + let want: str; + want.ptr = nil; want.len = 0; + if (vt.kind == N_TNAME) { want = vt.str; }; + if (want.len == 0) { return -1; }; + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, want)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + return -1; +}; + +fn cgtypetest(c: *cgen, n: *node) void = { + // `e is T` — load the lhs's tag, compare against T's variant + // index, set AX = (tag == idx). Result type is bool. + // + // Slot resolution is inlined (rather than factored into a helper + // with output parameters): wwstage cgen has a trap with i32 + // stored via *i32 in this context — direct assignment of the + // local works, indirection through &scrutoff drops sign bits. + let lhs: *node = n.lhs; + let scrutoff: i32 = 0; + let scrutt: *node = nil; + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let lc: *local = localfindnode(c, lhs.str); + if (lc != nil) { + scrutoff = lc.off; + scrutt = resolvetype(c, lc.tnode); + }; + }; + }; + let want: i32 = cgtagvariantidx(scrutt, n.rhs); + if (want < 0) { want = 0; }; + emitline("\tMOVQ\t"); + emitoff(scrutoff: i64); + emitline("(BP), AX\n"); + let nel: str = mklabel(c, "is_ne"); + let dnl: str = mklabel(c, "is_done"); + emitline("\tCMPQ\t$"); + emitint(want: i64); + emitline(", AX\n"); + emitline("\tJNE\t"); + emitline(nel); + emitline("\n\tMOVQ\t$1, AX\n\tJMP\t"); + emitline(dnl); + emitline("\n"); + emitlabel(nel); + emitline("\tMOVQ\t$0, AX\n"); + emitlabel(dnl); + return; +}; + +fn cgtypeassert(c: *cgen, n: *node) void = { + // `e as T` — load tag, abort (exit 1) if tag != T's variant + // index, otherwise unwrap to T's ABI: scalar/ptr → AX, 16B + // str → (AX, BX). Mirrors cgmatch's slot-based value load. + // Slot resolution inlined; see cgtypetest comment. + let lhs: *node = n.lhs; + let scrutoff: i32 = 0; + let scrutt: *node = nil; + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let lc: *local = localfindnode(c, lhs.str); + if (lc != nil) { + scrutoff = lc.off; + scrutt = resolvetype(c, lc.tnode); + }; + }; + }; + let want: i32 = cgtagvariantidx(scrutt, n.rhs); + if (want < 0) { want = 0; }; + let okl: str = mklabel(c, "asrt_ok"); + emitline("\tMOVQ\t"); + emitoff(scrutoff: i64); + emitline("(BP), AX\n"); + emitline("\tCMPQ\t$"); + emitint(want: i64); + emitline(", AX\n"); + emitline("\tJE\t"); + emitline(okl); + emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); + emitlabel(okl); + emitline("\tMOVQ\t"); + emitoff((scrutoff + 8): i64); + emitline("(BP), AX\n"); + if (isstrtype(c, n.rhs)) { + emitline("\tMOVQ\t"); + emitoff((scrutoff + 16): i64); + emitline("(BP), BX\n"); + }; + return; }; fn cgstrlit(c: *cgen, n: *node) void = { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 91731083..a4c34f56 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -79,6 +79,115 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == N_CALL) { cgcall(c, n); return; }; if (k == N_ASSIGN) { cgassign(c, n); return; }; + + if (k == N_TYPETEST) { cgtypetest(c, n); return; }; + if (k == N_TYPEASSERT) { cgtypeassert(c, n); return; }; +}; + +// cgtagvariantidx — find the 0-based variant index of `vt` inside the +// tagged-union type expression `tagged`. -1 if `tagged` isn't an +// N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch +// does inline; pulled out so `is` / `as` can reuse it. +fn cgtagvariantidx(tagged: *node, vt: *node) i32 = { + if (tagged == nil) { return -1; }; + if (vt == nil) { return -1; }; + if (tagged.kind != N_TTAGGED) { return -1; }; + let want: str; + want.ptr = nil; want.len = 0; + if (vt.kind == N_TNAME) { want = vt.str; }; + if (want.len == 0) { return -1; }; + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, want)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + return -1; +}; + +fn cgtypetest(c: *cgen, n: *node) void = { + // `e is T` — load the lhs's tag, compare against T's variant + // index, set AX = (tag == idx). Result type is bool. + // + // Slot resolution is inlined (rather than factored into a helper + // with output parameters): wwstage cgen has a trap with i32 + // stored via *i32 in this context — direct assignment of the + // local works, indirection through &scrutoff drops sign bits. + let lhs: *node = n.lhs; + let scrutoff: i32 = 0; + let scrutt: *node = nil; + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let lc: *local = localfindnode(c, lhs.str); + if (lc != nil) { + scrutoff = lc.off; + scrutt = resolvetype(c, lc.tnode); + }; + }; + }; + let want: i32 = cgtagvariantidx(scrutt, n.rhs); + if (want < 0) { want = 0; }; + emitline("\tMOVQ\t"); + emitoff(scrutoff: i64); + emitline("(BP), AX\n"); + let nel: str = mklabel(c, "is_ne"); + let dnl: str = mklabel(c, "is_done"); + emitline("\tCMPQ\t$"); + emitint(want: i64); + emitline(", AX\n"); + emitline("\tJNE\t"); + emitline(nel); + emitline("\n\tMOVQ\t$1, AX\n\tJMP\t"); + emitline(dnl); + emitline("\n"); + emitlabel(nel); + emitline("\tMOVQ\t$0, AX\n"); + emitlabel(dnl); + return; +}; + +fn cgtypeassert(c: *cgen, n: *node) void = { + // `e as T` — load tag, abort (exit 1) if tag != T's variant + // index, otherwise unwrap to T's ABI: scalar/ptr → AX, 16B + // str → (AX, BX). Mirrors cgmatch's slot-based value load. + // Slot resolution inlined; see cgtypetest comment. + let lhs: *node = n.lhs; + let scrutoff: i32 = 0; + let scrutt: *node = nil; + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let lc: *local = localfindnode(c, lhs.str); + if (lc != nil) { + scrutoff = lc.off; + scrutt = resolvetype(c, lc.tnode); + }; + }; + }; + let want: i32 = cgtagvariantidx(scrutt, n.rhs); + if (want < 0) { want = 0; }; + let okl: str = mklabel(c, "asrt_ok"); + emitline("\tMOVQ\t"); + emitoff(scrutoff: i64); + emitline("(BP), AX\n"); + emitline("\tCMPQ\t$"); + emitint(want: i64); + emitline(", AX\n"); + emitline("\tJE\t"); + emitline(okl); + emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); + emitlabel(okl); + emitline("\tMOVQ\t"); + emitoff((scrutoff + 8): i64); + emitline("(BP), AX\n"); + if (isstrtype(c, n.rhs)) { + emitline("\tMOVQ\t"); + emitoff((scrutoff + 16): i64); + emitline("(BP), BX\n"); + }; + return; }; fn cgstrlit(c: *cgen, n: *node) void = { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 2aa928df..85f30b10 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -545,7 +545,12 @@ def TK_LARROW: i32 = 79; def TK_ARROW: i32 = 80; def TK_FATARROW: i32 = 81; -def TK_LAST: i32 = 82; +// Appended at the tail (not grouped with the keyword block) so every +// pre-existing TK_* value stays unchanged — the 990_selfhost test +// diffs wwdump output against the C side, byte for byte. +def TK_IS: i32 = 82; + +def TK_LAST: i32 = 83; // ---- Pos / Tok -------------------------------------------------------- // @@ -604,6 +609,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = { if (streqn(p, "fn", n)) { return TK_FN; }; if (streqn(p, "for", n)) { return TK_FOR; }; if (streqn(p, "if", n)) { return TK_IF; }; + if (streqn(p, "is", n)) { return TK_IS; }; if (streqn(p, "let", n)) { return TK_LET; }; if (streqn(p, "match", n)) { return TK_MATCH; }; if (streqn(p, "nil", n)) { return TK_NIL; }; @@ -655,6 +661,7 @@ export fn tokname(k: i32) str = { if (k == TK_TRUE) { return "true"; }; if (k == TK_FALSE) { return "false"; }; if (k == TK_AS) { return "as"; }; + if (k == TK_IS) { return "is"; }; if (k == TK_STATIC) { return "static"; }; if (k == TK_MATCH) { return "match"; }; if (k == TK_CONST) { return "const"; }; @@ -1716,7 +1723,12 @@ def N_TRYUNW: i32 = 57; def N_MLET: i32 = 58; def N_MASSIGN: i32 = 59; -def N_LAST: i32 = 60; +// Appended at the tail to keep all prior N_* values stable. The +// 990_selfhost test diffs astprint against the C side byte-for-byte. +def N_TYPETEST: i32 = 60; +def N_TYPEASSERT: i32 = 61; + +def N_LAST: i32 = 62; // ---- Node ------------------------------------------------------------- @@ -1815,6 +1827,8 @@ fn nkname(k: i32) str = { if (k == N_TRYUNW) { return "tryunw"; }; if (k == N_MLET) { return "mlet"; }; if (k == N_MASSIGN) { return "massign"; }; + if (k == N_TYPETEST) { return "typetest"; }; + if (k == N_TYPEASSERT) { return "typeassert"; }; if (k == N_LAST) { return "last"; }; return "?"; }; @@ -2276,6 +2290,26 @@ fn parsepostfix(p: *parser, lhs: *node) *node = { cur = n; continue; }; + // Hare-style postfix: + // `e as T` — assert lhs is variant T (abort otherwise) → T + // `e is T` — bool: does lhs currently hold variant T? + // Same precedence level as the `:` cast. + if (p.curkind == TK_AS) { + advance(p); + let n: *node = newnode(p.a, N_TYPEASSERT, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; + if (p.curkind == TK_IS) { + advance(p); + let n: *node = newnode(p.a, N_TYPETEST, pf, pl, pc); + n.lhs = cur; + n.rhs = parsetype(p); + cur = n; + continue; + }; break; }; return cur; @@ -4863,6 +4897,115 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == N_CALL) { cgcall(c, n); return; }; if (k == N_ASSIGN) { cgassign(c, n); return; }; + + if (k == N_TYPETEST) { cgtypetest(c, n); return; }; + if (k == N_TYPEASSERT) { cgtypeassert(c, n); return; }; +}; + +// cgtagvariantidx — find the 0-based variant index of `vt` inside the +// tagged-union type expression `tagged`. -1 if `tagged` isn't an +// N_TTAGGED or no variant matches. Mirrors the lookup that cgmatch +// does inline; pulled out so `is` / `as` can reuse it. +fn cgtagvariantidx(tagged: *node, vt: *node) i32 = { + if (tagged == nil) { return -1; }; + if (vt == nil) { return -1; }; + if (tagged.kind != N_TTAGGED) { return -1; }; + let want: str; + want.ptr = nil; want.len = 0; + if (vt.kind == N_TNAME) { want = vt.str; }; + if (want.len == 0) { return -1; }; + let v: *node = tagged.list; + let idx: i32 = 0; + for (v != nil) { + if (v.kind == N_TNAME) { + if (streq(v.str, want)) { return idx; }; + }; + v = v.next; + idx += 1; + }; + return -1; +}; + +fn cgtypetest(c: *cgen, n: *node) void = { + // `e is T` — load the lhs's tag, compare against T's variant + // index, set AX = (tag == idx). Result type is bool. + // + // Slot resolution is inlined (rather than factored into a helper + // with output parameters): wwstage cgen has a trap with i32 + // stored via *i32 in this context — direct assignment of the + // local works, indirection through &scrutoff drops sign bits. + let lhs: *node = n.lhs; + let scrutoff: i32 = 0; + let scrutt: *node = nil; + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let lc: *local = localfindnode(c, lhs.str); + if (lc != nil) { + scrutoff = lc.off; + scrutt = resolvetype(c, lc.tnode); + }; + }; + }; + let want: i32 = cgtagvariantidx(scrutt, n.rhs); + if (want < 0) { want = 0; }; + emitline("\tMOVQ\t"); + emitoff(scrutoff: i64); + emitline("(BP), AX\n"); + let nel: str = mklabel(c, "is_ne"); + let dnl: str = mklabel(c, "is_done"); + emitline("\tCMPQ\t$"); + emitint(want: i64); + emitline(", AX\n"); + emitline("\tJNE\t"); + emitline(nel); + emitline("\n\tMOVQ\t$1, AX\n\tJMP\t"); + emitline(dnl); + emitline("\n"); + emitlabel(nel); + emitline("\tMOVQ\t$0, AX\n"); + emitlabel(dnl); + return; +}; + +fn cgtypeassert(c: *cgen, n: *node) void = { + // `e as T` — load tag, abort (exit 1) if tag != T's variant + // index, otherwise unwrap to T's ABI: scalar/ptr → AX, 16B + // str → (AX, BX). Mirrors cgmatch's slot-based value load. + // Slot resolution inlined; see cgtypetest comment. + let lhs: *node = n.lhs; + let scrutoff: i32 = 0; + let scrutt: *node = nil; + if (lhs != nil) { + if (lhs.kind == N_IDENT) { + let lc: *local = localfindnode(c, lhs.str); + if (lc != nil) { + scrutoff = lc.off; + scrutt = resolvetype(c, lc.tnode); + }; + }; + }; + let want: i32 = cgtagvariantidx(scrutt, n.rhs); + if (want < 0) { want = 0; }; + let okl: str = mklabel(c, "asrt_ok"); + emitline("\tMOVQ\t"); + emitoff(scrutoff: i64); + emitline("(BP), AX\n"); + emitline("\tCMPQ\t$"); + emitint(want: i64); + emitline(", AX\n"); + emitline("\tJE\t"); + emitline(okl); + emitline("\n\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); + emitlabel(okl); + emitline("\tMOVQ\t"); + emitoff((scrutoff + 8): i64); + emitline("(BP), AX\n"); + if (isstrtype(c, n.rhs)) { + emitline("\tMOVQ\t"); + emitoff((scrutoff + 16): i64); + emitline("(BP), BX\n"); + }; + return; }; fn cgstrlit(c: *cgen, n: *node) void = { diff --git a/test/wcc/200_parse.c b/test/wcc/200_parse.c index fa81b5df..0d38ac9c 100644 --- a/test/wcc/200_parse.c +++ b/test/wcc/200_parse.c @@ -162,6 +162,11 @@ main(void) "fn try() (i32, str) = { return 0, \"\"; };", "fn use_tuple() void = { let q, r = divmod(10, 3); };", "fn assign_tuple() void = { q, r = divmod(10, 3); };", + + /* Hare-style type test / type assertion (postfix) */ + "fn ti(r: (i64 | i32)) bool = { return r is i64; };", + "fn ai(r: (i64 | i32)) i64 = { return r as i64; };", + "fn br(r: (i64 | str)) i32 = { if (r is i64) { return 1; }; return 0; };", }; int n = sizeof parses / sizeof parses[0]; for (int i = 0; i < n; i++) { diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index e4194d79..985af942 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -750,7 +750,7 @@ static const struct row rows[] = { " needle[0] = 119u8; needle[1] = 111u8; needle[2] = 114u8;\n" " return bytes.indexsub(buf[0:12], needle[0:3]);\n" "};", 7 }, - /* errors.is — sentinel comparison through a (T | error) union. + /* errors.equal — sentinel comparison through a (T | error) union. * Sets up two errors, dispatches each, and confirms the matching * sentinel detection. */ { "use errors;\n" @@ -766,13 +766,13 @@ static const struct row rows[] = { " match (r1) {\n" " case let v: i64 => acc += -100;\n" " case let e: errors.error =>\n" - " if (errors.is(e, errors.eEOF)) { acc += 1; }\n" + " if (errors.equal(e, errors.eEOF)) { acc += 1; }\n" " else { acc += -100; };\n" " };\n" " match (r2) {\n" " case let v: i64 => acc += -100;\n" " case let e: errors.error =>\n" - " if (errors.is(e, errors.eShortRead)) { acc += 10; }\n" + " if (errors.equal(e, errors.eShortRead)) { acc += 10; }\n" " else { acc += -100; };\n" " };\n" " return acc;\n" @@ -869,6 +869,33 @@ static const struct row rows[] = { " os.write(1, GREETING.ptr, GREETING.len: u64);\n" " return GREETING.len: i32;\n" "};", 3 }, + /* Hare-style `is` / `as`: type test returns bool, type assertion + * unwraps to the variant's value (success path only — abort path + * exit(1) is exercised manually). Covers i64/i32 scalar variants + * and str (16B variant via .len pseudo-field). */ + { "fn classify(n: i64) (i64 | i32) = {\n" + " if (n < 0) { return 7: i32; };\n" + " return n;\n" + "};\n" + "fn main() i32 = {\n" + " let ok: (i64 | i32) = classify(40);\n" + " let bad: (i64 | i32) = classify(-1);\n" + " let s: i32 = 0;\n" + " if (ok is i64) { s += 1; };\n" + " if (bad is i32) { s += 1; };\n" + " if (ok is i32) { s += 100; };\n" + " if (bad is i64) { s += 100; };\n" + " let v: i64 = ok as i64;\n" + " let e: i32 = bad as i32;\n" + " return (v: i32) + s + e;\n" + "};", 49 }, + /* `as` on a 16B str variant — unwrap loads (ptr, len). */ + { "fn fail() (i64 | str) = { return \"bad\"; };\n" + "fn main() i32 = {\n" + " let r: (i64 | str) = fail();\n" + " let e: str = r as str;\n" + " return e.len: i32;\n" + "};", 3 }, { NULL, 0 } };