From 2f385cec000d1ceffa443a3cac5e353f310e5ef3 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 02:10:10 +0900 Subject: [PATCH] ascii: graduate digitval to (i32 | void); selfhost tagged ABI follow-on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ascii.digitval returns (i32 | void) instead of an i32 -1 sentinel. Two callers updated to match-on the result (lib/ww/lex/lex.ww escape parse, selfhost/test/smoke.ww probe 6). `!` would have been more idiomatic at both call sites — both have verified isxdigit beforehand — but the selfhost parser doesn't yet recognize postfix `!`/`?`, so using them in bootstrap-bound code breaks the 993/995 byte-identity gates. Match is fine for now. Selfhost cgen follow-on for the 8-byte-rounded tagged-union ABI (landed in 1e2f55a for the C side): - cgenutil.slotsize: tagged size = 8 (tag) + max(payload), padded to 8-byte multiple. Was hardcoded 24. - cgendecl prologue: spill size/8 arg registers, not always 3. - cgenstmt cglet tagged-call path: spill the CX value-word only when the slot is >16 bytes. All three were emitting 3-register patterns appropriate to (T | str) sized unions and overflowing the new 16-byte (i32 | void) slots. --- lib/ascii/ascii.ww | 8 +-- lib/ww/lex/lex.ww | 14 ++++- selfhost/cmd/w6c/main.combined.ww | 84 ++++++++++++++++++---------- selfhost/cmd/wcc/cgendecl.ww | 36 +++++------- selfhost/cmd/wcc/cgenstmt.ww | 10 +++- selfhost/cmd/wcc/cgenutil.ww | 16 +++++- selfhost/cmd/wwdump/main.combined.ww | 84 ++++++++++++++++++---------- selfhost/test/smoke.combined.ww | 14 +++-- selfhost/test/smoke.ww | 6 +- 9 files changed, 173 insertions(+), 99 deletions(-) diff --git a/lib/ascii/ascii.ww b/lib/ascii/ascii.ww index 265abf71..5904f9a3 100644 --- a/lib/ascii/ascii.ww +++ b/lib/ascii/ascii.ww @@ -53,9 +53,9 @@ export fn isxdigit(c: rune) bool = { return false; }; -// digitval — value of `c` as a hex/decimal digit, or -1 if not one. -// Useful when scanning numeric literals. -export fn digitval(c: rune) i32 = { +// digitval — value of `c` as a hex/decimal digit. void variant means +// `c` isn't a hex digit. Useful when scanning numeric literals. +export fn digitval(c: rune) (i32 | void) = { if (isdigit(c)) { return (c - 48): i32; }; if (c >= 65) { if (c <= 70) { return ((c - 65) + 10): i32; }; @@ -63,7 +63,7 @@ export fn digitval(c: rune) i32 = { if (c >= 97) { if (c <= 102) { return ((c - 97) + 10): i32; }; }; - return -1; + return; }; // isidstart / isidpart — identifier classes used by the lexer. diff --git a/lib/ww/lex/lex.ww b/lib/ww/lex/lex.ww index 56ca2fc2..c7358c5c 100644 --- a/lib/ww/lex/lex.ww +++ b/lib/ww/lex/lex.ww @@ -240,8 +240,18 @@ fn escape(l: *lex, out: *i32) bool = { errat(l, &cp, "bad \\x escape"); return false; }; - let h: i32 = ascii.digitval(hi: rune); - let lv: i32 = ascii.digitval(lo: rune); + 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; }; + }; *out = (h << 4) | lv; return true; }; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 632ef9ee..a41a36a8 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -895,9 +895,9 @@ export fn isxdigit(c: rune) bool = { return false; }; -// digitval — value of `c` as a hex/decimal digit, or -1 if not one. -// Useful when scanning numeric literals. -export fn digitval(c: rune) i32 = { +// digitval — value of `c` as a hex/decimal digit. void variant means +// `c` isn't a hex digit. Useful when scanning numeric literals. +export fn digitval(c: rune) (i32 | void) = { if (isdigit(c)) { return (c - 48): i32; }; if (c >= 65) { if (c <= 70) { return ((c - 65) + 10): i32; }; @@ -905,7 +905,7 @@ export fn digitval(c: rune) i32 = { if (c >= 97) { if (c <= 102) { return ((c - 97) + 10): i32; }; }; - return -1; + return; }; // isidstart / isidpart — identifier classes used by the lexer. @@ -1176,8 +1176,18 @@ fn escape(l: *lex, out: *i32) bool = { errat(l, &cp, "bad \\x escape"); return false; }; - let h: i32 = ascii.digitval(hi: rune); - let lv: i32 = ascii.digitval(lo: rune); + 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; }; + }; *out = (h << 4) | lv; return true; }; @@ -4592,7 +4602,21 @@ fn slotsize(c: *cgen, typn: *node) i32 = { }; return total; }; - if (k == N_TTAGGED){ return 24; }; + if (k == N_TTAGGED){ + // Slot = 8 (tag) + max(variant payload sizes), rounded up + // to an 8-byte multiple so the reg-passing ABI (size/8 + // words) doesn't drop the last value register. Mirrors C + // cgen's resolve_type for N_TTAGGED. + let v: *node = typn.list; + let maxsz: i32 = 0; + for (v != nil) { + let sz: i32 = slotsize(c, v); + if (sz > maxsz) { maxsz = sz; }; + v = v.next; + }; + let pad: i32 = (maxsz + 7) & ~7; + return 8 + pad; + }; if (k == N_TNAME) { let nm: str = typn.str; if (streq(nm, "str")) { return 16; }; @@ -6400,15 +6424,19 @@ fn cglet(c: *cgen, n: *node) void = { }; cgexpr(c, rhs); if (rhsreturnstagged) { + // Spill size/8 registers (tag + value words). + // Slots smaller than 24 don't carry a CX word. emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\tDX, "); emitoff((off + 8): i64); emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); + if (sz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + }; c.lastwasreturn = 0; return; }; @@ -7004,27 +7032,21 @@ fn cgfnparams(c: *cgen, params: *node) void = { if (p.kind == N_PARAM) { let nm: str = p.str; if (istaggedtype(p.lhs)) { - // tagged-union param: passed in 3 regs (tag, v0, v1), - // 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; + // tagged-union param: spill size/8 registers + // (tag + value words). Slot sized to match. + let slot: i32 = slotsize(c, p.lhs); + let off: i32 = localadd(c, nm, slot, p.lhs); + let nw: i32 = slot / 8; + let w: i32 = 0; + for (w < nw) { + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + w*8): i64); + emitline("(BP)\n"); + idx += 1; + w += 1; + }; } else { if (isslicetype(c, p.lhs)) { // slice param: 3 regs (ptr, len, cap), 24-byte slot. let off: i32 = localadd(c, nm, 24, p.lhs); diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 39b7ca61..5631125e 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -128,27 +128,21 @@ fn cgfnparams(c: *cgen, params: *node) void = { if (p.kind == N_PARAM) { let nm: str = p.str; if (istaggedtype(p.lhs)) { - // tagged-union param: passed in 3 regs (tag, v0, v1), - // 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; + // tagged-union param: spill size/8 registers + // (tag + value words). Slot sized to match. + let slot: i32 = slotsize(c, p.lhs); + let off: i32 = localadd(c, nm, slot, p.lhs); + let nw: i32 = slot / 8; + let w: i32 = 0; + for (w < nw) { + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + w*8): i64); + emitline("(BP)\n"); + idx += 1; + w += 1; + }; } else { if (isslicetype(c, p.lhs)) { // slice param: 3 regs (ptr, len, cap), 24-byte slot. let off: i32 = localadd(c, nm, 24, p.lhs); diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 2df61457..4c8d4cb7 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -184,15 +184,19 @@ fn cglet(c: *cgen, n: *node) void = { }; cgexpr(c, rhs); if (rhsreturnstagged) { + // Spill size/8 registers (tag + value words). + // Slots smaller than 24 don't carry a CX word. emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\tDX, "); emitoff((off + 8): i64); emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); + if (sz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + }; c.lastwasreturn = 0; return; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index d32217a5..d72e82a8 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -702,7 +702,21 @@ fn slotsize(c: *cgen, typn: *node) i32 = { }; return total; }; - if (k == N_TTAGGED){ return 24; }; + if (k == N_TTAGGED){ + // Slot = 8 (tag) + max(variant payload sizes), rounded up + // to an 8-byte multiple so the reg-passing ABI (size/8 + // words) doesn't drop the last value register. Mirrors C + // cgen's resolve_type for N_TTAGGED. + let v: *node = typn.list; + let maxsz: i32 = 0; + for (v != nil) { + let sz: i32 = slotsize(c, v); + if (sz > maxsz) { maxsz = sz; }; + v = v.next; + }; + let pad: i32 = (maxsz + 7) & ~7; + return 8 + pad; + }; if (k == N_TNAME) { let nm: str = typn.str; if (streq(nm, "str")) { return 16; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b0c9e088..873701cd 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -895,9 +895,9 @@ export fn isxdigit(c: rune) bool = { return false; }; -// digitval — value of `c` as a hex/decimal digit, or -1 if not one. -// Useful when scanning numeric literals. -export fn digitval(c: rune) i32 = { +// digitval — value of `c` as a hex/decimal digit. void variant means +// `c` isn't a hex digit. Useful when scanning numeric literals. +export fn digitval(c: rune) (i32 | void) = { if (isdigit(c)) { return (c - 48): i32; }; if (c >= 65) { if (c <= 70) { return ((c - 65) + 10): i32; }; @@ -905,7 +905,7 @@ export fn digitval(c: rune) i32 = { if (c >= 97) { if (c <= 102) { return ((c - 97) + 10): i32; }; }; - return -1; + return; }; // isidstart / isidpart — identifier classes used by the lexer. @@ -1176,8 +1176,18 @@ fn escape(l: *lex, out: *i32) bool = { errat(l, &cp, "bad \\x escape"); return false; }; - let h: i32 = ascii.digitval(hi: rune); - let lv: i32 = ascii.digitval(lo: rune); + 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; }; + }; *out = (h << 4) | lv; return true; }; @@ -4592,7 +4602,21 @@ fn slotsize(c: *cgen, typn: *node) i32 = { }; return total; }; - if (k == N_TTAGGED){ return 24; }; + if (k == N_TTAGGED){ + // Slot = 8 (tag) + max(variant payload sizes), rounded up + // to an 8-byte multiple so the reg-passing ABI (size/8 + // words) doesn't drop the last value register. Mirrors C + // cgen's resolve_type for N_TTAGGED. + let v: *node = typn.list; + let maxsz: i32 = 0; + for (v != nil) { + let sz: i32 = slotsize(c, v); + if (sz > maxsz) { maxsz = sz; }; + v = v.next; + }; + let pad: i32 = (maxsz + 7) & ~7; + return 8 + pad; + }; if (k == N_TNAME) { let nm: str = typn.str; if (streq(nm, "str")) { return 16; }; @@ -6400,15 +6424,19 @@ fn cglet(c: *cgen, n: *node) void = { }; cgexpr(c, rhs); if (rhsreturnstagged) { + // Spill size/8 registers (tag + value words). + // Slots smaller than 24 don't carry a CX word. emitline("\tMOVQ\tAX, "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\tDX, "); emitoff((off + 8): i64); emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); + if (sz > 16) { + emitline("\tMOVQ\tCX, "); + emitoff((off + 16): i64); + emitline("(BP)\n"); + }; c.lastwasreturn = 0; return; }; @@ -7004,27 +7032,21 @@ fn cgfnparams(c: *cgen, params: *node) void = { if (p.kind == N_PARAM) { let nm: str = p.str; if (istaggedtype(p.lhs)) { - // tagged-union param: passed in 3 regs (tag, v0, v1), - // 24-byte slot. - let off: i32 = localadd(c, nm, 24, p.lhs); - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - idx += 1; - emitline("\tMOVQ\t"); - emitline(argregname(idx)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - idx += 1; + // tagged-union param: spill size/8 registers + // (tag + value words). Slot sized to match. + let slot: i32 = slotsize(c, p.lhs); + let off: i32 = localadd(c, nm, slot, p.lhs); + let nw: i32 = slot / 8; + let w: i32 = 0; + for (w < nw) { + emitline("\tMOVQ\t"); + emitline(argregname(idx)); + emitline(", "); + emitoff((off + w*8): i64); + emitline("(BP)\n"); + idx += 1; + w += 1; + }; } else { if (isslicetype(c, p.lhs)) { // slice param: 3 regs (ptr, len, cap), 24-byte slot. let off: i32 = localadd(c, nm, 24, p.lhs); diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index ba91afd2..431b4d27 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -376,9 +376,9 @@ export fn isxdigit(c: rune) bool = { return false; }; -// digitval — value of `c` as a hex/decimal digit, or -1 if not one. -// Useful when scanning numeric literals. -export fn digitval(c: rune) i32 = { +// digitval — value of `c` as a hex/decimal digit. void variant means +// `c` isn't a hex digit. Useful when scanning numeric literals. +export fn digitval(c: rune) (i32 | void) = { if (isdigit(c)) { return (c - 48): i32; }; if (c >= 65) { if (c <= 70) { return ((c - 65) + 10): i32; }; @@ -386,7 +386,7 @@ export fn digitval(c: rune) i32 = { if (c >= 97) { if (c <= 102) { return ((c - 97) + 10): i32; }; }; - return -1; + return; }; // isidstart / isidpart — identifier classes used by the lexer. @@ -559,7 +559,11 @@ export fn main() i32 = { if (!ascii.isalpha(122)) { return 16; }; // 'z' if (!ascii.isidstart(95)) { return 17; }; // '_' if (!ascii.isidpart(48)) { return 18; }; // '0' is part - if (ascii.digitval(70) != 15) { return 19; }; // 'F' = 15 + let dv: (i32 | void) = ascii.digitval(70); + match (dv) { + case let v: i32 => { if (v != 15) { return 19; }; }; // 'F' = 15 + case void => { return 19; }; + }; if (ascii.tolower(65) != 97) { return 20; }; // 'A' -> 'a' // Probe 7 — file open/read via the new os APIs. /proc/self/cmdline diff --git a/selfhost/test/smoke.ww b/selfhost/test/smoke.ww index 2f28a638..187f2ce2 100644 --- a/selfhost/test/smoke.ww +++ b/selfhost/test/smoke.ww @@ -142,7 +142,11 @@ export fn main() i32 = { if (!ascii.isalpha(122)) { return 16; }; // 'z' if (!ascii.isidstart(95)) { return 17; }; // '_' if (!ascii.isidpart(48)) { return 18; }; // '0' is part - if (ascii.digitval(70) != 15) { return 19; }; // 'F' = 15 + let dv: (i32 | void) = ascii.digitval(70); + match (dv) { + case let v: i32 => { if (v != 15) { return 19; }; }; // 'F' = 15 + case void => { return 19; }; + }; if (ascii.tolower(65) != 97) { return 20; }; // 'A' -> 'a' // Probe 7 — file open/read via the new os APIs. /proc/self/cmdline