wcc: ? error-subset propagation typecheck + tag remap
`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.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user