diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 42cf429e..e0c28b1c 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -170,6 +170,45 @@ cg_variant_match(Type *vt, Type *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`. * 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. */ @@ -1905,32 +1944,32 @@ cgexpr(Cg *c, Node *n, Local *locals) } case N_TRYPROP: { /* Evaluate tagged value: AX=tag, DX=value0[, CX=value1]. - * If tag != 0, propagate as the current function's return. - * On success, unwrap to the success-variant ABI: ≤8B values - * land in AX; str values land in (AX=ptr, BX=len). - * - * Tag remap: when operand and enclosing fn have different - * 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. */ + * If the tag matches an error variant, propagate as the + * current function's return (with a tag remap to the + * enclosing fn's variant order). On success, unwrap to the + * success-variant ABI: ≤8B values in AX; str values in + * (AX=ptr, BX=len). */ cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; Type *r = cg_ret_type; if (r && r->kind == TY_NAMED) r = r->under; - Type *first = (u && u->kind == TY_TAGGED && u->params) - ? u->params->type : NULL; - int success_is_str = type_isstr(first); + int s_tag = cg_tagged_success_tag(u); + Type *succ_t = NULL; + 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"); - 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)); if (u && r && r->kind == TY_TAGGED && u->params) { char *propret = mklabel(c, "tryprop_ret"); - int i = 1; - for (Tparam *p = u->params->next; p; - p = p->next, i++) { + int i = 0; + for (Tparam *p = u->params; p; p = p->next, i++) { + if (!cg_variant_is_error(u, i)) continue; int j = cg_tag_for_variant(r, p->type); if (j < 0) j = 0; if (j == i) continue; @@ -1957,11 +1996,16 @@ cgexpr(Cg *c, Node *n, Local *locals) cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; - Type *first = (u && u->kind == TY_TAGGED && u->params) - ? u->params->type : NULL; - int success_is_str = type_isstr(first); + int s_tag = cg_tagged_success_tag(u); + Type *succ_t = NULL; + 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"); - 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)); ins2(c, A_MOVQ, aimm(1), areg(D_DI)); ins2(c, A_MOVQ, aimm(60), areg(D_AX)); diff --git a/cmd/wcc/ast.c b/cmd/wcc/ast.c index a72ad5e4..8c39aaf5 100644 --- a/cmd/wcc/ast.c +++ b/cmd/wcc/ast.c @@ -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 "?"; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 894941c1..cf744ed7 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -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) { diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index 90aa0628..cebd789c 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -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); diff --git a/cmd/wcc/type.c b/cmd/wcc/type.c index 8c66ff5f..c8ecba94 100644 --- a/cmd/wcc/type.c +++ b/cmd/wcc/type.c @@ -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; } diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 863c8196..363319a0 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -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; diff --git a/lib/strconv/strconv.ww b/lib/strconv/strconv.ww index 3d041262..3e36ae41 100644 --- a/lib/strconv/strconv.ww +++ b/lib/strconv/strconv.ww @@ -4,13 +4,13 @@ // (Hare uses !size / !void; ww uses i32 / void without the `!` mark). // invalid — input wasn't a valid number in the requested format. -// Payload is the byte index of the first offending position (Hare -// strconv::invalid is `!size` carrying the same). -export type invalid = i32; +// Payload is the byte index of the first offending position. Mirrors +// Hare's strconv::invalid = !size (we use i32 instead of size). +export type invalid = !i32; // overflow — input was valid but doesn't fit the target type. No -// payload (a single yes/no signal). Mirrors Hare's `!void` shape. -export type overflow = void; +// payload (a single yes/no signal). Mirrors Hare's !void shape. +export type overflow = !void; // 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 diff --git a/lib/ww/ast.ww b/lib/ww/ast.ww index d92de7c0..bdef1ad4 100644 --- a/lib/ww/ast.ww +++ b/lib/ww/ast.ww @@ -89,8 +89,9 @@ def N_MASSIGN: i32 = 59; def N_TYPETEST: i32 = 60; def N_TYPEASSERT: i32 = 61; def N_VOIDLIT: i32 = 62; +def N_TBANG: i32 = 63; -def N_LAST: i32 = 63; +def N_LAST: i32 = 64; // ---- Node ------------------------------------------------------------- @@ -192,6 +193,7 @@ fn nkname(k: i32) str = { if (k == N_TYPETEST) { return "typetest"; }; if (k == N_TYPEASSERT) { return "typeassert"; }; if (k == N_VOIDLIT) { return "voidlit"; }; + if (k == N_TBANG) { return "tbang"; }; if (k == N_LAST) { return "last"; }; return "?"; }; diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww index 60c1fe7f..909f9321 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/parse/parse.ww @@ -129,6 +129,14 @@ fn parsetype(p: *parser) *node = { let pl: i32 = p.curline; 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) { advance(p); let n: *node = newnode(p.a, N_TPTR, pf, pl, pc); diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index d604b478..37ff58da 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -329,13 +329,13 @@ export fn freearena(a: *arena) void = { // (Hare uses !size / !void; ww uses i32 / void without the `!` mark). // invalid — input wasn't a valid number in the requested format. -// Payload is the byte index of the first offending position (Hare -// strconv::invalid is `!size` carrying the same). -export type invalid = i32; +// Payload is the byte index of the first offending position. Mirrors +// Hare's strconv::invalid = !size (we use i32 instead of size). +export type invalid = !i32; // overflow — input was valid but doesn't fit the target type. No -// payload (a single yes/no signal). Mirrors Hare's `!void` shape. -export type overflow = void; +// payload (a single yes/no signal). Mirrors Hare's !void shape. +export type overflow = !void; // 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 @@ -1735,8 +1735,9 @@ def N_MASSIGN: i32 = 59; def N_TYPETEST: i32 = 60; def N_TYPEASSERT: i32 = 61; def N_VOIDLIT: i32 = 62; +def N_TBANG: i32 = 63; -def N_LAST: i32 = 63; +def N_LAST: i32 = 64; // ---- Node ------------------------------------------------------------- @@ -1838,6 +1839,7 @@ fn nkname(k: i32) str = { if (k == N_TYPETEST) { return "typetest"; }; if (k == N_TYPEASSERT) { return "typeassert"; }; if (k == N_VOIDLIT) { return "voidlit"; }; + if (k == N_TBANG) { return "tbang"; }; if (k == N_LAST) { return "last"; }; return "?"; }; @@ -2982,6 +2984,14 @@ fn parsetype(p: *parser) *node = { let pl: i32 = p.curline; 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) { advance(p); let n: *node = newnode(p.a, N_TPTR, pf, pl, pc); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 25af5a5c..935f4220 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -329,13 +329,13 @@ export fn freearena(a: *arena) void = { // (Hare uses !size / !void; ww uses i32 / void without the `!` mark). // invalid — input wasn't a valid number in the requested format. -// Payload is the byte index of the first offending position (Hare -// strconv::invalid is `!size` carrying the same). -export type invalid = i32; +// Payload is the byte index of the first offending position. Mirrors +// Hare's strconv::invalid = !size (we use i32 instead of size). +export type invalid = !i32; // overflow — input was valid but doesn't fit the target type. No -// payload (a single yes/no signal). Mirrors Hare's `!void` shape. -export type overflow = void; +// payload (a single yes/no signal). Mirrors Hare's !void shape. +export type overflow = !void; // 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 @@ -1735,8 +1735,9 @@ def N_MASSIGN: i32 = 59; def N_TYPETEST: i32 = 60; def N_TYPEASSERT: i32 = 61; def N_VOIDLIT: i32 = 62; +def N_TBANG: i32 = 63; -def N_LAST: i32 = 63; +def N_LAST: i32 = 64; // ---- Node ------------------------------------------------------------- @@ -1838,6 +1839,7 @@ fn nkname(k: i32) str = { if (k == N_TYPETEST) { return "typetest"; }; if (k == N_TYPEASSERT) { return "typeassert"; }; if (k == N_VOIDLIT) { return "voidlit"; }; + if (k == N_TBANG) { return "tbang"; }; if (k == N_LAST) { return "last"; }; return "?"; }; @@ -2982,6 +2984,14 @@ fn parsetype(p: *parser) *node = { let pl: i32 = p.curline; 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) { advance(p); let n: *node = newnode(p.a, N_TPTR, pf, pl, pc); diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index 23d3cc59..6fdda02d 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -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). // invalid — input wasn't a valid number in the requested format. -// Payload is the byte index of the first offending position (Hare -// strconv::invalid is `!size` carrying the same). -export type invalid = i32; +// Payload is the byte index of the first offending position. Mirrors +// Hare's strconv::invalid = !size (we use i32 instead of size). +export type invalid = !i32; // overflow — input was valid but doesn't fit the target type. No -// payload (a single yes/no signal). Mirrors Hare's `!void` shape. -export type overflow = void; +// payload (a single yes/no signal). Mirrors Hare's !void shape. +export type overflow = !void; // 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 diff --git a/test/wcc/300_check.c b/test/wcc/300_check.c index b98ed89d..b6d6a34e 100644 --- a/test/wcc/300_check.c +++ b/test/wcc/300_check.c @@ -135,6 +135,16 @@ static const struct row rows[] = { { "fn inner() (i32 | str | bool) = { return 1; }; " "fn outer() (i64 | bool | str) = { let v: i32 = inner()?; return v: i64; };", "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 diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 3422b496..91148270 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -538,6 +538,29 @@ static const struct row rows[] = { " };\n" " return 0;\n" "};", 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), * enclosing (i64 | bool | str). Tag remap maps str:1→2, bool:2→1. */ { "fn inner(n: i32) (i32 | str | bool) = {\n"