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

@@ -86,6 +86,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; };
};
@@ -114,6 +116,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.