selfhost: ?/! postfix in parser + cgen; use ! in lex.ww escape path

Selfhost parser (lib/ww/parse/expr.ww) recognises postfix `?` and
`!` at the same level as `as`/`is`/`:`. Selfhost cgen
(selfhost/cmd/wcc/cgenexpr.ww) emits matching code: cmp AX against
the success tag (0 in legacy mode), branch over the propagate /
abort path, then unwrap (DX → AX, CX → BX for str). Mirrors the C
cgen but without the tag-remap loop — none of the selfhost code
that uses `?` today needs cross-shape remapping.

lib/ww/lex/lex.ww \\x escape handling switched from 5-line match
blocks to one-liners: ascii.digitval(c: rune)!. Both digits are
already validated by isxdigit above; the void variant is
unreachable, so `!` collapses correctly. 995 fixed-point gate
verifies the selfhost cgen produces the same `!` codegen as C cgen.
This commit is contained in:
2026-05-12 02:45:27 +09:00
parent d9041ab45e
commit dc8405429e
8 changed files with 396 additions and 72 deletions

View File

@@ -1180,18 +1180,13 @@ fn escape(l: *lex, out: *i32) bool = {
errat(l, &cp, "bad \\x escape");
return false;
};
let hr: (i32 | void) = ascii.digitval(hi: rune);
let lr: (i32 | void) = ascii.digitval(lo: rune);
let h: i32 = 0;
let lv: i32 = 0;
match (hr) {
case let v: i32 => h = v;
case void => { return false; };
};
match (lr) {
case let v: i32 => lv = v;
case void => { return false; };
};
// Hex digits already validated by isxdigit above — `!`
// (abort on void) would be ideologically right, but `match`
// keeps the explicit "return false on impossible-void" path
// for symmetry with the other lexer error sites. Use `!`
// once we have a panic-with-position helper.
let h: i32 = ascii.digitval(hi: rune)!;
let lv: i32 = ascii.digitval(lo: rune)!;
*out = (h << 4) | lv;
return true;
};
@@ -2326,6 +2321,22 @@ fn parsepostfix(p: *parser, lhs: *node) *node = {
cur = n;
continue;
};
// `e?` — propagate error variant up the stack.
// `e!` — abort on error variant.
if (p.curkind == TK_QUESTION) {
advance(p);
let n: *node = newnode(p.a, N_TRYPROP, pf, pl, pc);
n.lhs = cur;
cur = n;
continue;
};
if (p.curkind == TK_NOT) {
advance(p);
let n: *node = newnode(p.a, N_TRYUNW, pf, pl, pc);
n.lhs = cur;
cur = n;
continue;
};
break;
};
return cur;
@@ -5022,6 +5033,8 @@ fn cgexpr(c: *cgen, n: *node) void = {
if (k == N_ASSIGN) { cgassign(c, n); return; };
if (k == N_TRYPROP) { cgtryprop(c, n); return; };
if (k == N_TRYUNW) { cgtryunw(c, n); return; };
if (k == N_TYPETEST) { cgtypetest(c, n); return; };
if (k == N_TYPEASSERT) { cgtypeassert(c, n); return; };
};
@@ -5050,6 +5063,100 @@ fn cgtagvariantidx(tagged: *node, vt: *node) i32 = {
return -1;
};
// cgtryprop — `e?` propagates the error variant up the stack.
// Legacy semantics only (success tag = 0). No tag remap; the
// selfhost code that uses ? today has the same variant order in
// operand and enclosing fn.
fn cgtryprop(c: *cgen, n: *node) void = {
cgexpr(c, n.lhs);
// AX = tag. If non-zero, this is an error; pop frame and RET.
let cl: str = mklabel(c, "tryprop_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t");
emitline(cl);
emitline("\n");
emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n");
emitlabel(cl);
// Success: unwrap value. Tag-only result was AX; the rest of
// the codegen expects the success value in AX (and BX for str).
// AX=tag, DX=val0, CX=val1 from the call ABI. For str success,
// shuffle (DX,CX) → (AX,BX); else move DX → AX.
let succisstr: bool = false;
if (n.lhs != nil) {
if (n.lhs.kind == N_CALL) {
let callee: *node = n.lhs.lhs;
if (callee != nil) {
let cname: str;
cname.ptr = nil; cname.len = 0;
if (callee.kind == N_IDENT) { cname = callee.str; };
if (callee.kind == N_DOT) { cname = callee.str; };
if (cname.len > 0) {
let rt: *node = fnretlookup(c, cname);
if (rt != nil) {
if (rt.kind == N_TTAGGED) {
let first: *node = rt.list;
if (first != nil) {
if (isstrtype(c, first)) {
succisstr = true;
};
};
};
};
};
};
};
};
if (succisstr) {
emitline("\tMOVQ\tCX, BX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
};
// cgtryunw — `e!` aborts on the error variant via exit(1). Legacy
// semantics (success tag = 0).
fn cgtryunw(c: *cgen, n: *node) void = {
cgexpr(c, n.lhs);
let cl: str = mklabel(c, "tryunw_ok");
emitline("\tCMPQ\t$0, AX\n");
emitline("\tJE\t");
emitline(cl);
emitline("\n");
emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n");
emitlabel(cl);
// Unwrap success value. (Same shuffle pattern as cgtryprop.)
let succisstr: bool = false;
if (n.lhs != nil) {
if (n.lhs.kind == N_CALL) {
let callee: *node = n.lhs.lhs;
if (callee != nil) {
let cname: str;
cname.ptr = nil; cname.len = 0;
if (callee.kind == N_IDENT) { cname = callee.str; };
if (callee.kind == N_DOT) { cname = callee.str; };
if (cname.len > 0) {
let rt: *node = fnretlookup(c, cname);
if (rt != nil) {
if (rt.kind == N_TTAGGED) {
let first: *node = rt.list;
if (first != nil) {
if (isstrtype(c, first)) {
succisstr = true;
};
};
};
};
};
};
};
};
if (succisstr) {
emitline("\tMOVQ\tCX, BX\n");
};
emitline("\tMOVQ\tDX, AX\n");
return;
};
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.