wcc: match-as-expression with yield

`match (e) { ... }` can now sit in expression position, with each
arm using `yield expr;` to produce the match's value:

    let v = match (r) {
    case let n: i32 => yield n + 1;
    case let s: str => yield s.len: i32 + 100;
    };

TK_YIELD keyword + N_YIELD AST node, both appended at the tail of
their enums to keep prior numeric values byte-stable for the
wwdump-diff gates.

Checker: cexpr for N_MATCH walks each arm's body looking for the
first N_YIELD; the match's type is the unified yield type (or
ty_void if no yield, preserving the statement-form semantics).
Mismatched arm yields are flagged.

Cgen: a yield-target stack (separate from the loop break stack)
holds each enclosing match's end label. N_YIELD evaluates its
expression into AX (and BX for str) and JMPs to the topmost entry.
cgmatch pushes its end label on entry and pops on exit.

Selfhost mirror: lib/ww/lex/tok.ww kwtab+name, lib/ww/ast.ww
N_YIELD def+print, lib/ww/parse/stmt.ww yield-stmt; selfhost cgen
adds a yieldbuf to the cgen struct and a cgyield helper. Verified
end-to-end: a yield-using program compiled via the wwstage cgen
matches the C-cgen build's exit code.
This commit is contained in:
2026-05-12 03:08:00 +09:00
parent 67e27589fd
commit f267f99a2b
15 changed files with 231 additions and 14 deletions

View File

@@ -47,6 +47,13 @@ static const char *loop_cont[LOOP_MAX];
static const char *loop_brk[LOOP_MAX];
static int nloops;
/* Yield-target stack. Each entry is the end label of an enclosing
* match-as-expression; `yield expr;` evaluates expr (AX) and JMPs
* to the topmost entry. */
#define YIELD_MAX 16
static const char *yield_target[YIELD_MAX];
static int nyields;
static int
cg_isfloat(Type *t)
{
@@ -1928,6 +1935,10 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
}
char *end = mklabel(c, "match_end");
/* Push the end label as the yield target for arm bodies. */
if (nyields < YIELD_MAX) {
yield_target[nyields++] = end;
}
for (Node *cs = n->list; cs; cs = cs->next) {
char *next = mklabel(c, "match_next");
if (cs->type != NULL) {
@@ -2009,6 +2020,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
label(c, next);
}
label(c, end);
if (nyields > 0) nyields--;
break;
}
case N_TRYPROP: {
@@ -3254,6 +3266,14 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
defers[ndefers++] = n->lhs;
}
break;
case N_YIELD:
/* Evaluate the value into AX, then jump to the enclosing
* match's end label. str-typed yields land in (AX, BX);
* the consumer's let-init or call-arg site reads both. */
if (n->lhs) cgexpr(c, n->lhs, *locals);
if (nyields > 0)
ins1(c, A_JMP, abranch(yield_target[nyields - 1]));
break;
case N_BREAK:
if (nloops > 0)
ins1(c, A_JMP, abranch(loop_brk[nloops - 1]));

View File

@@ -85,6 +85,7 @@ nkname(Nkind k)
case N_TYPEASSERT: return "typeassert";
case N_VOIDLIT: return "voidlit";
case N_TBANG: return "tbang";
case N_YIELD: return "yield";
case N_LAST: return "last";
}
return "?";

View File

@@ -99,6 +99,33 @@ variant_present(Tparam *head, Type *vt)
return 0;
}
/* match_yield_type — walk a match arm's body looking for the type
* of its first `yield expr;` statement. Returns NULL if no yield
* was found. Doesn't descend into nested match bodies — each match
* is its own yield scope. */
static Type *
match_yield_type(Node *body)
{
if (body == NULL) return NULL;
if (body->kind == N_YIELD) return body->lhs ? body->lhs->type : NULL;
if (body->kind == N_MATCH) return NULL; /* inner match: own scope */
if (body->kind == N_BLOCK) {
for (Node *s = body->list; s; s = s->next) {
Type *t = match_yield_type(s);
if (t) return t;
}
return NULL;
}
if (body->kind == N_IF) {
Type *t = match_yield_type(body->body);
if (t) return t;
return match_yield_type(body->els);
}
if (body->kind == N_FOR || body->kind == N_FORRANGE)
return match_yield_type(body->body);
return NULL;
}
/* 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. */
@@ -913,7 +940,20 @@ cexpr(Checker *c, Node *n)
type_name(c->a, p->type));
}
}
n->type = ty_void;
/* match-as-expression: the type is the common yield type
* across arms. If no arm yields, the match is a statement
* and its type is void. */
Type *yt = NULL;
for (Node *cs = n->list; cs; cs = cs->next) {
Type *t = match_yield_type(cs->body);
if (t == NULL) continue;
if (yt == NULL) yt = t;
else if (!type_eq(yt, t) && !type_assignable(yt, t))
err(c, cs->pos,
"match arm yields %s, expected %s",
type_name(c->a, t), type_name(c->a, yt));
}
n->type = yt ? yt : ty_void;
return n->type;
}
case N_TYPETEST: case N_TYPEASSERT: {
@@ -1221,6 +1261,7 @@ cstmt(Checker *c, Node *n)
break;
}
case N_DEFER: (void)cexpr(c, n->lhs); break;
case N_YIELD: if (n->lhs) (void)cexpr(c, n->lhs); break;
case N_BREAK:
case N_CONTINUE:
if (c->loops == 0)

View File

@@ -1121,6 +1121,13 @@ parsestmt(Parser *p)
expect(p, TK_SEMI);
return n;
}
case TK_YIELD: {
advance(p);
Node *n = newnode(p->a, N_YIELD, pp);
n->lhs = parseexpr(p);
expect(p, TK_SEMI);
return n;
}
case TK_BREAK: {
advance(p);
Node *n = newnode(p->a, N_BREAK, pp);

View File

@@ -41,7 +41,8 @@ static const struct kwent kwtab[] = {
{ "true", TK_TRUE },
{ "type", TK_TYPE },
{ "use", TK_USE },
{ "void", TK_VOID }
{ "void", TK_VOID },
{ "yield", TK_YIELD }
};
Tkind
@@ -93,6 +94,7 @@ tokname(Tkind k)
case TK_AS: return "as";
case TK_IS: return "is";
case TK_VOID: return "void";
case TK_YIELD: return "yield";
case TK_STATIC: return "static";
case TK_MATCH: return "match";
case TK_CONST: return "const";

View File

@@ -177,6 +177,7 @@ typedef enum {
* the selfhost wwdump-diff test (990) is byte-sensitive. */
TK_IS, /* Hare-style type test: e is T */
TK_VOID, /* `void` — both a type name and a zero-size value */
TK_YIELD, /* `yield expr;` — value-return from a match arm */
TK_LAST /* sentinel for tables */
} Tkind;
@@ -297,6 +298,8 @@ typedef enum {
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_YIELD, /* `yield expr;` — set the enclosing match's value-
* return and jump to its end label. lhs = value. */
N_LAST
} Nkind;

View File

@@ -90,8 +90,9 @@ def N_TYPETEST: i32 = 60;
def N_TYPEASSERT: i32 = 61;
def N_VOIDLIT: i32 = 62;
def N_TBANG: i32 = 63;
def N_YIELD: i32 = 64;
def N_LAST: i32 = 64;
def N_LAST: i32 = 65;
// ---- Node -------------------------------------------------------------
@@ -194,6 +195,7 @@ fn nkname(k: i32) str = {
if (k == N_TYPEASSERT) { return "typeassert"; };
if (k == N_VOIDLIT) { return "voidlit"; };
if (k == N_TBANG) { return "tbang"; };
if (k == N_YIELD) { return "yield"; };
if (k == N_LAST) { return "last"; };
return "?";
};

View File

@@ -108,10 +108,11 @@ def TK_FATARROW: i32 = 81;
// 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_VOID: i32 = 83;
def TK_IS: i32 = 82;
def TK_VOID: i32 = 83;
def TK_YIELD: i32 = 84;
def TK_LAST: i32 = 84;
def TK_LAST: i32 = 85;
// ---- Pos / Tok --------------------------------------------------------
//
@@ -183,6 +184,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = {
if (streqn(p, "type", n)) { return TK_TYPE; };
if (streqn(p, "use", n)) { return TK_USE; };
if (streqn(p, "void", n)) { return TK_VOID; };
if (streqn(p, "yield", n)) { return TK_YIELD; };
return TK_NONE;
};
@@ -225,6 +227,7 @@ export fn tokname(k: i32) str = {
if (k == TK_AS) { return "as"; };
if (k == TK_IS) { return "is"; };
if (k == TK_VOID) { return "void"; };
if (k == TK_YIELD) { return "yield"; };
if (k == TK_STATIC) { return "static"; };
if (k == TK_MATCH) { return "match"; };
if (k == TK_CONST) { return "const"; };

View File

@@ -233,6 +233,13 @@ fn parsestmt(p: *parser) *node = {
expecttok(p, TK_SEMI, "expected ';' after defer");
return n;
};
if (p.curkind == TK_YIELD) {
advance(p);
let n: *node = newnode(p.a, N_YIELD, pf, pl, pc);
n.lhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after yield");
return n;
};
if (p.curkind == TK_BREAK) {
advance(p);
expecttok(p, TK_SEMI, "expected ';' after break");

View File

@@ -543,10 +543,11 @@ def TK_FATARROW: i32 = 81;
// 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_VOID: i32 = 83;
def TK_IS: i32 = 82;
def TK_VOID: i32 = 83;
def TK_YIELD: i32 = 84;
def TK_LAST: i32 = 84;
def TK_LAST: i32 = 85;
// ---- Pos / Tok --------------------------------------------------------
//
@@ -618,6 +619,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = {
if (streqn(p, "type", n)) { return TK_TYPE; };
if (streqn(p, "use", n)) { return TK_USE; };
if (streqn(p, "void", n)) { return TK_VOID; };
if (streqn(p, "yield", n)) { return TK_YIELD; };
return TK_NONE;
};
@@ -660,6 +662,7 @@ export fn tokname(k: i32) str = {
if (k == TK_AS) { return "as"; };
if (k == TK_IS) { return "is"; };
if (k == TK_VOID) { return "void"; };
if (k == TK_YIELD) { return "yield"; };
if (k == TK_STATIC) { return "static"; };
if (k == TK_MATCH) { return "match"; };
if (k == TK_CONST) { return "const"; };
@@ -1732,8 +1735,9 @@ def N_TYPETEST: i32 = 60;
def N_TYPEASSERT: i32 = 61;
def N_VOIDLIT: i32 = 62;
def N_TBANG: i32 = 63;
def N_YIELD: i32 = 64;
def N_LAST: i32 = 64;
def N_LAST: i32 = 65;
// ---- Node -------------------------------------------------------------
@@ -1836,6 +1840,7 @@ fn nkname(k: i32) str = {
if (k == N_TYPEASSERT) { return "typeassert"; };
if (k == N_VOIDLIT) { return "voidlit"; };
if (k == N_TBANG) { return "tbang"; };
if (k == N_YIELD) { return "yield"; };
if (k == N_LAST) { return "last"; };
return "?";
};
@@ -2664,6 +2669,13 @@ fn parsestmt(p: *parser) *node = {
expecttok(p, TK_SEMI, "expected ';' after defer");
return n;
};
if (p.curkind == TK_YIELD) {
advance(p);
let n: *node = newnode(p.a, N_YIELD, pf, pl, pc);
n.lhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after yield");
return n;
};
if (p.curkind == TK_BREAK) {
advance(p);
expecttok(p, TK_SEMI, "expected ';' after break");
@@ -5419,6 +5431,11 @@ fn cgmatch(c: *cgen, n: *node) void = {
};
};
let endl: str = mklabel(c, "match_end");
// Push end label as the yield target for this match's arm bodies.
if (c.yieldtop < LOOP_MAX) {
c.yieldbuf[c.yieldtop] = endl;
c.yieldtop += 1;
};
let cs: *node = n.list;
for (cs != nil) {
let nxt: str = mklabel(c, "match_next");
@@ -5534,6 +5551,7 @@ fn cgmatch(c: *cgen, n: *node) void = {
cs = cs.next;
};
emitlabel(endl);
if (c.yieldtop > 0) { c.yieldtop -= 1; };
return;
};
@@ -6522,9 +6540,26 @@ fn cgstmt(c: *cgen, n: *node) void = {
if (k == N_BREAK) { cgbreak(c, n); return; };
if (k == N_CONTINUE) { cgcontinue(c, n); return; };
if (k == N_YIELD) { cgyield(c, n); return; };
c.lastwasreturn = 0;
};
fn cgyield(c: *cgen, n: *node) void = {
// Evaluate the value into AX (and BX for str), then JMP to the
// enclosing match's end label. Falls through silently if there
// is no active match — should be a checker error eventually.
if (n.lhs != nil) { cgexpr(c, n.lhs); };
if (c.yieldtop > 0) {
let tgt: str = c.yieldbuf[c.yieldtop - 1];
emitline("\tJMP\t");
emitline(tgt);
emitline("\n");
};
c.lastwasreturn = 0;
return;
};
fn cgblock(c: *cgen, n: *node) void = {
let s: *node = n.list;
for (s != nil) {
@@ -7631,6 +7666,8 @@ type cgen = struct {
looptop: i32,
loopendbuf: *str, // stack of end labels for break
loopcontbuf: *str, // stack of cont labels for continue
yieldtop: i32,
yieldbuf: *str, // stack of match end labels for yield
};
fn cgeninit(c: *cgen, a: *arena) void = {
@@ -7645,6 +7682,8 @@ fn cgeninit(c: *cgen, a: *arena) void = {
c.looptop = 0;
c.loopendbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.loopcontbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.yieldtop = 0;
c.yieldbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
};
// localalloc — append a slot for `name` without dedup. Used for

View File

@@ -165,6 +165,8 @@ type cgen = struct {
looptop: i32,
loopendbuf: *str, // stack of end labels for break
loopcontbuf: *str, // stack of cont labels for continue
yieldtop: i32,
yieldbuf: *str, // stack of match end labels for yield
};
fn cgeninit(c: *cgen, a: *arena) void = {
@@ -179,6 +181,8 @@ fn cgeninit(c: *cgen, a: *arena) void = {
c.looptop = 0;
c.loopendbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.loopcontbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.yieldtop = 0;
c.yieldbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
};
// localalloc — append a slot for `name` without dedup. Used for

View File

@@ -438,6 +438,11 @@ fn cgmatch(c: *cgen, n: *node) void = {
};
};
let endl: str = mklabel(c, "match_end");
// Push end label as the yield target for this match's arm bodies.
if (c.yieldtop < LOOP_MAX) {
c.yieldbuf[c.yieldtop] = endl;
c.yieldtop += 1;
};
let cs: *node = n.list;
for (cs != nil) {
let nxt: str = mklabel(c, "match_next");
@@ -553,6 +558,7 @@ fn cgmatch(c: *cgen, n: *node) void = {
cs = cs.next;
};
emitlabel(endl);
if (c.yieldtop > 0) { c.yieldtop -= 1; };
return;
};

View File

@@ -41,9 +41,26 @@ fn cgstmt(c: *cgen, n: *node) void = {
if (k == N_BREAK) { cgbreak(c, n); return; };
if (k == N_CONTINUE) { cgcontinue(c, n); return; };
if (k == N_YIELD) { cgyield(c, n); return; };
c.lastwasreturn = 0;
};
fn cgyield(c: *cgen, n: *node) void = {
// Evaluate the value into AX (and BX for str), then JMP to the
// enclosing match's end label. Falls through silently if there
// is no active match — should be a checker error eventually.
if (n.lhs != nil) { cgexpr(c, n.lhs); };
if (c.yieldtop > 0) {
let tgt: str = c.yieldbuf[c.yieldtop - 1];
emitline("\tJMP\t");
emitline(tgt);
emitline("\n");
};
c.lastwasreturn = 0;
return;
};
fn cgblock(c: *cgen, n: *node) void = {
let s: *node = n.list;
for (s != nil) {

View File

@@ -543,10 +543,11 @@ def TK_FATARROW: i32 = 81;
// 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_VOID: i32 = 83;
def TK_IS: i32 = 82;
def TK_VOID: i32 = 83;
def TK_YIELD: i32 = 84;
def TK_LAST: i32 = 84;
def TK_LAST: i32 = 85;
// ---- Pos / Tok --------------------------------------------------------
//
@@ -618,6 +619,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = {
if (streqn(p, "type", n)) { return TK_TYPE; };
if (streqn(p, "use", n)) { return TK_USE; };
if (streqn(p, "void", n)) { return TK_VOID; };
if (streqn(p, "yield", n)) { return TK_YIELD; };
return TK_NONE;
};
@@ -660,6 +662,7 @@ export fn tokname(k: i32) str = {
if (k == TK_AS) { return "as"; };
if (k == TK_IS) { return "is"; };
if (k == TK_VOID) { return "void"; };
if (k == TK_YIELD) { return "yield"; };
if (k == TK_STATIC) { return "static"; };
if (k == TK_MATCH) { return "match"; };
if (k == TK_CONST) { return "const"; };
@@ -1732,8 +1735,9 @@ def N_TYPETEST: i32 = 60;
def N_TYPEASSERT: i32 = 61;
def N_VOIDLIT: i32 = 62;
def N_TBANG: i32 = 63;
def N_YIELD: i32 = 64;
def N_LAST: i32 = 64;
def N_LAST: i32 = 65;
// ---- Node -------------------------------------------------------------
@@ -1836,6 +1840,7 @@ fn nkname(k: i32) str = {
if (k == N_TYPEASSERT) { return "typeassert"; };
if (k == N_VOIDLIT) { return "voidlit"; };
if (k == N_TBANG) { return "tbang"; };
if (k == N_YIELD) { return "yield"; };
if (k == N_LAST) { return "last"; };
return "?";
};
@@ -2664,6 +2669,13 @@ fn parsestmt(p: *parser) *node = {
expecttok(p, TK_SEMI, "expected ';' after defer");
return n;
};
if (p.curkind == TK_YIELD) {
advance(p);
let n: *node = newnode(p.a, N_YIELD, pf, pl, pc);
n.lhs = parseexpr(p);
expecttok(p, TK_SEMI, "expected ';' after yield");
return n;
};
if (p.curkind == TK_BREAK) {
advance(p);
expecttok(p, TK_SEMI, "expected ';' after break");
@@ -5419,6 +5431,11 @@ fn cgmatch(c: *cgen, n: *node) void = {
};
};
let endl: str = mklabel(c, "match_end");
// Push end label as the yield target for this match's arm bodies.
if (c.yieldtop < LOOP_MAX) {
c.yieldbuf[c.yieldtop] = endl;
c.yieldtop += 1;
};
let cs: *node = n.list;
for (cs != nil) {
let nxt: str = mklabel(c, "match_next");
@@ -5534,6 +5551,7 @@ fn cgmatch(c: *cgen, n: *node) void = {
cs = cs.next;
};
emitlabel(endl);
if (c.yieldtop > 0) { c.yieldtop -= 1; };
return;
};
@@ -6522,9 +6540,26 @@ fn cgstmt(c: *cgen, n: *node) void = {
if (k == N_BREAK) { cgbreak(c, n); return; };
if (k == N_CONTINUE) { cgcontinue(c, n); return; };
if (k == N_YIELD) { cgyield(c, n); return; };
c.lastwasreturn = 0;
};
fn cgyield(c: *cgen, n: *node) void = {
// Evaluate the value into AX (and BX for str), then JMP to the
// enclosing match's end label. Falls through silently if there
// is no active match — should be a checker error eventually.
if (n.lhs != nil) { cgexpr(c, n.lhs); };
if (c.yieldtop > 0) {
let tgt: str = c.yieldbuf[c.yieldtop - 1];
emitline("\tJMP\t");
emitline(tgt);
emitline("\n");
};
c.lastwasreturn = 0;
return;
};
fn cgblock(c: *cgen, n: *node) void = {
let s: *node = n.list;
for (s != nil) {
@@ -7631,6 +7666,8 @@ type cgen = struct {
looptop: i32,
loopendbuf: *str, // stack of end labels for break
loopcontbuf: *str, // stack of cont labels for continue
yieldtop: i32,
yieldbuf: *str, // stack of match end labels for yield
};
fn cgeninit(c: *cgen, a: *arena) void = {
@@ -7645,6 +7682,8 @@ fn cgeninit(c: *cgen, a: *arena) void = {
c.looptop = 0;
c.loopendbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.loopcontbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
c.yieldtop = 0;
c.yieldbuf = amalloc(a, (LOOP_MAX: u64) * 16u64): *str;
};
// localalloc — append a slot for `name` without dedup. Used for

View File

@@ -538,6 +538,32 @@ static const struct row rows[] = {
" };\n"
" return 0;\n"
"};", 3 },
/* yield from match-as-expression: each arm yields a value;
* the match itself is bound to a let. */
{ "fn pick(b: bool) (i32 | str) = {\n"
" if (b) { return 7; };\n"
" return \"abc\";\n"
"};\n"
"fn main() i32 = {\n"
" let r: (i32 | str) = pick(true);\n"
" let v: i32 = match (r) {\n"
" case let n: i32 => yield n + 1;\n"
" case let s: str => yield s.len: i32 + 100;\n"
" };\n"
" return v;\n"
"};", 8 },
{ "fn pick(b: bool) (i32 | str) = {\n"
" if (b) { return 7; };\n"
" return \"abc\";\n"
"};\n"
"fn main() i32 = {\n"
" let r: (i32 | str) = pick(false);\n"
" let v: i32 = match (r) {\n"
" case let n: i32 => yield n + 1;\n"
" case let s: str => yield s.len: i32 + 100;\n"
" };\n"
" return v;\n"
"};", 103 },
/* Nullable pointer folding: `(*T | void)` is one 8-byte word
* where null = void variant. match/is/as/?/! all key off the
* pointer-vs-null discriminator instead of a separate tag. */