From 41a82021a35dd1c3053f9814b76575e37fff847f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 01:40:02 +0900 Subject: [PATCH] wcc: ? error-subset propagation typecheck + tag remap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `expr?` previously did a brain-dead RET through whatever AX/DX/CX held — only safe when operand and enclosing fn had identical variant ordering. Tests relied on that alignment by construction. Now: - Typecheck: each non-first variant of operand must appear as a variant of the enclosing fn's return tagged union. Enclosing must itself be tagged (a non-tagged return has no slot for errors to land in). - Cgen: on tag != 0, walk operand's error variants and emit a conditional tag remap (cmp/jne/mov/jmp) for any whose index in enclosing differs from operand's. Identity cases emit nothing, so same-shape operands cost zero extra instructions. Selfhost cgen doesn't implement N_TRYPROP at all (no selfhost source uses `?`); byte-identity tests still pass. One existing e2e row used `?` with main returning i32 — relied on the old loose semantics. Switched to `!` (abort-on-error); it was exercising success-unwrap, not propagation. --- cmd/w6c/cgen.c | 28 ++++++++++++++++++++++++++- cmd/wcc/check.c | 33 +++++++++++++++++++++++++++++++- test/wcc/300_check.c | 11 +++++++++++ test/wcc/700_e2e.c | 45 ++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 113 insertions(+), 4 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index e7883664..bfce4097 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -1897,16 +1897,42 @@ cgexpr(Cg *c, Node *n, Local *locals) /* 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). */ + * 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. */ 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); char *cont = mklabel(c, "tryprop_ok"); ins2(c, A_CMPQ, aimm(0), 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 j = cg_tag_for_variant(r, p->type); + if (j < 0) j = 0; + if (j == i) continue; + char *skip = mklabel(c, "tryprop_skip"); + ins2(c, A_CMPQ, aimm(i), areg(D_AX)); + ins1(c, A_JNE, abranch(skip)); + ins2(c, A_MOVQ, aimm(j), areg(D_AX)); + ins1(c, A_JMP, abranch(propret)); + label(c, skip); + } + label(c, propret); + } ins2(c, A_MOVQ, areg(D_BP), areg(D_SP)); ins1(c, A_POPQ, areg(D_BP)); ins0(c, A_RET); diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index d9e6d41c..fa4cad4c 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -884,8 +884,39 @@ cexpr(Checker *c, Node *n) n->kind == N_TRYPROP ? "?" : "!", type_name(c->a, t)); } - /* Convention: first variant is the success type. */ + /* Convention: first variant is the success type; the remaining + * variants are the error subset. + * + * 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 *r = c->ret; + Type *ru = (r && r->kind == TY_NAMED) ? r->under : r; + if (ru == NULL || ru->kind != TY_TAGGED) { + err(c, n->pos, + "?: enclosing function must return a tagged " + "union to propagate errors (got %s)", + type_name(c->a, r)); + } else { + for (Tparam *e = first->next; e; e = e->next) { + int ok = 0; + for (Tparam *p = ru->params; p; + p = p->next) + if (variant_match(p->type, + e->type)) { + ok = 1; break; + } + if (!ok) + err(c, n->pos, + "?: error variant %s not in enclosing return %s", + type_name(c->a, e->type), + type_name(c->a, r)); + } + } + } return n->type = first ? first->type : ty_err; } case N_TUPLE: { diff --git a/test/wcc/300_check.c b/test/wcc/300_check.c index 87d87c76..b98ed89d 100644 --- a/test/wcc/300_check.c +++ b/test/wcc/300_check.c @@ -124,6 +124,17 @@ static const struct row rows[] = { "ok" }, /* default arm absorbs missing variants */ { "fn die() never = { for (true) { let _: i32 = 1; }; }; " "fn f() i32 = { die(); };", "ok" }, /* never assignable to anything */ + + /* ? error-subset propagation typecheck */ + { "fn inner() (i32 | str) = { return 1; }; " + "fn outer() i32 = { let v: i32 = inner()?; return v; };", + "enclosing function must return a tagged union" }, + { "fn inner() (i32 | str | bool) = { return 1; }; " + "fn outer() (i64 | str) = { let v: i32 = inner()?; return v: i64; };", + "error variant bool not in enclosing return" }, + { "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 */ }; int diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 2070c502..80e0a66d 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -520,13 +520,54 @@ static const struct row rows[] = { " };\n" " return 0;\n" "};", 4 }, - /* ? success unwrap when the first variant is itself str */ + /* ? with different success types but shared error: operand + * (i32 | str), enclosing (i64 | str). Success widens. */ + { "fn inner(b: bool) (i32 | str) = {\n" + " if (b) { return 7; };\n" + " return \"err\";\n" + "};\n" + "fn outer(b: bool) (i64 | str) = {\n" + " let v: i32 = inner(b)?;\n" + " return v: i64 + 100;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | str) = outer(false);\n" + " match (r) {\n" + " case let n: i64 => return n: i32;\n" + " case let s: str => return s.len: i32;\n" + " };\n" + " return 0;\n" + "};", 3 }, + /* ? 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" + " if (n == 0) { return \"z\"; };\n" + " if (n < 0) { return false; };\n" + " return n;\n" + "};\n" + "fn outer(n: i32) (i64 | bool | str) = {\n" + " let v: i32 = inner(n)?;\n" + " return v: i64 + 1000;\n" + "};\n" + "fn main() i32 = {\n" + " let r: (i64 | bool | str) = outer(-1);\n" + " match (r) {\n" + " case let n: i64 => return n: i32;\n" + " case let b: bool => { if (!b) { return 7; }; return 8; };\n" + " case let s: str => return 9;\n" + " };\n" + " return 0;\n" + "};", 7 }, + /* ! success unwrap when the first variant is itself str. + * Uses ! (not ?) because main's return is i32, not tagged — + * ? would require error variants to be propagatable to the + * enclosing return. ! aborts on the error variant instead. */ { "fn make() (str | i64) = {\n" " return \"ok\";\n" "};\n" "fn main() i32 = {\n" " let r: (str | i64) = make();\n" - " let v: str = r?;\n" + " let v: str = r!;\n" " return v.len: i32;\n" "};", 2 }, /* type error = str; named-alias variant works through the