From 4085742853bfb396681692b95b01d220bae7447f Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 12 May 2026 02:02:08 +0900 Subject: [PATCH] strconv: graduate to (T | invalid | overflow); add void expression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `type invalid = i32` (payload: byte index of first bad rune; mirrors Hare's strconv::invalid = !size) and `type overflow = void` (Hare's overflow = !void). stoi64/stou64 now return these instead of the str-error placeholder. atoi64 dropped — lib/CLAUDE.md says graduate in one go, don't keep both shapes around. To produce the void variant payload, `void` is now a real expression literal (TK_VOID kw, N_VOIDLIT). It evaluates to ty_void; codegen emits MOVQ $0, AX. Both kinds are appended at the tail of their enums to keep prior numeric values byte-stable for the wwdump-diff fixtures. check_file reorder: USE declarations are now installed in pass 1 alongside the type-decl placeholders so dotted type references (`strconv.invalid` from a typedecl body) resolve. DEF/FN/LET silently overwrite a USE-occupied slot — matches the old behavior where USE silently no-op'd when a same-name fn/def existed (the conflict manifested in selfhost main.combined.ww at `use parse;` colliding with `export fn parse(a)`). selfhost mirror: lib/ww/lex/tok.ww kwtab+name; lib/ww/ast.ww N_VOIDLIT def+print; lib/ww/parse/{expr,parse}.ww TK_VOID handling; selfhost/cmd/wcc/cgenexpr.ww N_VOIDLIT codegen. --- cmd/w6c/cgen.c | 3 +- cmd/wcc/ast.c | 1 + cmd/wcc/check.c | 32 ++++++++-- cmd/wcc/parse.c | 11 ++++ cmd/wcc/tok.c | 4 +- cmd/wcc/ww.h | 2 + lib/strconv/strconv.ww | 66 ++++++++----------- lib/ww/ast.ww | 4 +- lib/ww/lex/tok.ww | 7 +- lib/ww/parse/expr.ww | 4 ++ lib/ww/parse/parse.ww | 9 +++ selfhost/cmd/w6c/main.combined.ww | 96 +++++++++++++++------------- selfhost/cmd/wcc/cgenexpr.ww | 6 ++ selfhost/cmd/wwdump/main.combined.ww | 96 +++++++++++++++------------- selfhost/test/smoke.combined.ww | 66 ++++++++----------- test/wcc/700_e2e.c | 41 +++++++----- 16 files changed, 250 insertions(+), 198 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index eeff3265..42cf429e 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -572,7 +572,8 @@ cgexpr(Cg *c, Node *n, Local *locals) } case N_TRUE: cgexpr_int(c, 1); break; case N_FALSE: - case N_NIL: cgexpr_int(c, 0); break; + case N_NIL: + case N_VOIDLIT: cgexpr_int(c, 0); break; case N_IDENT: { int off = localfind(locals, n->str); if (off != 0) { diff --git a/cmd/wcc/ast.c b/cmd/wcc/ast.c index 0e06e6fc..a72ad5e4 100644 --- a/cmd/wcc/ast.c +++ b/cmd/wcc/ast.c @@ -83,6 +83,7 @@ nkname(Nkind k) case N_MASSIGN: return "massign"; case N_TYPETEST: return "typetest"; case N_TYPEASSERT: return "typeassert"; + case N_VOIDLIT: return "voidlit"; case N_LAST: return "last"; } return "?"; diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index bb04e697..894941c1 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -408,6 +408,7 @@ cexpr(Checker *c, Node *n) case N_TRUE: case N_FALSE: n->type = ty_untyped_bool; return n->type; case N_NIL: n->type = ty_untyped_nil; return n->type; + case N_VOIDLIT: n->type = ty_void; return n->type; case N_IDENT: { if (n->str && n->str[0] == '\0') return n->type = err(c, n->pos, @@ -1201,8 +1202,14 @@ check_file(Checker *c, Node *file) * For self-referential types we install the named-type placeholder * BEFORE resolving its body; the body may legitimately mention * the type itself (`type stream = struct { read: fn(*stream)... }`). - */ + * USE declarations are installed in this same step so dotted type + * references (`strconv.invalid`) resolve when typedecl bodies are + * walked in the next pass. */ for (Node *d = file->list; d; d = d->next) { + if (d->kind == N_USE) { + scope_define(c->cur, d->str, SK_USE, NULL, d); + continue; + } if (d->kind != N_TYPEDECL) continue; Type *named = type_named(c->a, d->str, NULL); if (!scope_define(c->cur, d->str, SK_TYPE, named, d)) @@ -1221,27 +1228,40 @@ check_file(Checker *c, Node *file) for (Node *d = file->list; d; d = d->next) { switch (d->kind) { case N_USE: - scope_define(c->cur, d->str, SK_USE, NULL, d); + /* already installed in pass 1; no-op here so the + * old fall-through doesn't re-define. */ break; case N_DEF: { Type *t = resolve_type(c, d->lhs); d->type = t; - if (!scope_define(c->cur, d->str, SK_DEF, t, d)) + Sym *prev = scope_lookup_local(c->cur, d->str); + if (prev && prev->kind == SK_USE) { + prev->kind = SK_DEF; prev->type = t; prev->decl = d; + } else if (!scope_define(c->cur, d->str, SK_DEF, t, d)) err(c, d->pos, "duplicate def %s", d->str); break; } case N_FNDECL: { Type *t = build_fn_type(c, d); d->type = t; - if (!scope_define(c->cur, d->str, SK_FN, t, d)) + Sym *prev = scope_lookup_local(c->cur, d->str); + if (prev && prev->kind == SK_USE) { + prev->kind = SK_FN; prev->type = t; prev->decl = d; + } else if (!scope_define(c->cur, d->str, SK_FN, t, d)) err(c, d->pos, "duplicate fn %s", d->str); break; } case N_LET: { Type *t = d->lhs ? resolve_type(c, d->lhs) : NULL; d->type = t; - if (d->str && d->str[0]) - scope_define(c->cur, d->str, SK_VAR, t, d); + if (d->str && d->str[0]) { + Sym *prev = scope_lookup_local(c->cur, d->str); + if (prev && prev->kind == SK_USE) { + prev->kind = SK_VAR; prev->type = t; + prev->decl = d; + } else + scope_define(c->cur, d->str, SK_VAR, t, d); + } break; } default: break; diff --git a/cmd/wcc/parse.c b/cmd/wcc/parse.c index de1ba2ba..90aa0628 100644 --- a/cmd/wcc/parse.c +++ b/cmd/wcc/parse.c @@ -222,6 +222,16 @@ parsetype(Parser *p) n->lhs = parsetype(p); return n; } + case TK_VOID: { + /* `void` keyword in type-expr context. Synthesise an + * N_TNAME so type-resolution treats it like any other + * primitive name. */ + Node *n = newnode(p->a, N_TNAME, pp); + n->str = "void"; + n->strlen = 4; + advance(p); + return n; + } case TK_IDENT: { Node *n = newnode(p->a, N_TNAME, pp); n->str = p->cur.text; @@ -455,6 +465,7 @@ parseprimary(Parser *p) case TK_TRUE: advance(p); return newnode(p->a, N_TRUE, pp); case TK_FALSE: advance(p); return newnode(p->a, N_FALSE, pp); case TK_NIL: advance(p); return newnode(p->a, N_NIL, pp); + case TK_VOID: advance(p); return newnode(p->a, N_VOIDLIT, pp); case TK_UNDER: { /* Bare `_` — valid only as a discard lvalue. We yield an N_IDENT * with empty str; the checker rejects it outside assignment diff --git a/cmd/wcc/tok.c b/cmd/wcc/tok.c index 418d83d3..1fb97773 100644 --- a/cmd/wcc/tok.c +++ b/cmd/wcc/tok.c @@ -40,7 +40,8 @@ static const struct kwent kwtab[] = { { "switch", TK_SWITCH }, { "true", TK_TRUE }, { "type", TK_TYPE }, - { "use", TK_USE } + { "use", TK_USE }, + { "void", TK_VOID } }; Tkind @@ -91,6 +92,7 @@ tokname(Tkind k) case TK_FALSE: return "false"; case TK_AS: return "as"; case TK_IS: return "is"; + case TK_VOID: return "void"; case TK_STATIC: return "static"; case TK_MATCH: return "match"; case TK_CONST: return "const"; diff --git a/cmd/wcc/ww.h b/cmd/wcc/ww.h index 394660c6..863c8196 100644 --- a/cmd/wcc/ww.h +++ b/cmd/wcc/ww.h @@ -176,6 +176,7 @@ typedef enum { * to keep the numeric value of every existing kind unchanged — * 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_LAST /* sentinel for tables */ } Tkind; @@ -294,6 +295,7 @@ typedef enum { * byte-for-byte. lhs=value, rhs=variant type expr. */ N_TYPETEST, /* lhs is T → bool */ N_TYPEASSERT, /* lhs as T → T (abort if tag mismatch) */ + N_VOIDLIT, /* `void` as expression — zero-size void value */ N_LAST } Nkind; diff --git a/lib/strconv/strconv.ww b/lib/strconv/strconv.ww index a5e5eee4..3d041262 100644 --- a/lib/strconv/strconv.ww +++ b/lib/strconv/strconv.ww @@ -1,10 +1,16 @@ // strconv — number↔string conversions. Decimal i64 to/from a fixed -// buffer. Two error idioms ship side by side: -// - Plan 9 style (atoi64): tuple `(value, ok)`. Pre-dates the -// tagged-union work; kept for callers that already use it. -// - Hare style (stoi64/stou64): `(value | str)`. The error -// variant carries a short, allocation-free message describing -// why the parse failed. Prefer this for new code. +// buffer. Error shapes mirror Hare's strconv types: (T | invalid | +// overflow) where each error is a named alias over a payload type +// (Hare uses !size / !void; ww uses i32 / void without the `!` mark). + +// invalid — input wasn't a valid number in the requested format. +// Payload is the byte index of the first offending position (Hare +// strconv::invalid is `!size` carrying the same). +export type invalid = i32; + +// overflow — input was valid but doesn't fit the target type. No +// payload (a single yes/no signal). Mirrors Hare's `!void` shape. +export type overflow = void; // u64tos — write `v` in decimal into `buf` and return the byte count. // Hare name; the buffer-in shape is the sanctioned Plan 9 subset of @@ -64,41 +70,20 @@ export fn i64tos(buf: []u8, v: i64) i32 = { return out; }; -export fn atoi64(s: str) (i64, bool) = { - let v: i64 = 0; - let i: i32 = 0; - let neg: bool = false; - if (s.len > 0) { - if (s[0] == 45u8) { neg = true; i = 1; }; - }; - if (i >= s.len) { return 0, false; }; - for (i < s.len) { - let c: u8 = s[i]; - if (c < 48u8) { return 0, false; }; - if (c > 57u8) { return 0, false; }; - v = v * 10 + ((c: i64) - 48); - i += 1; - }; - if (neg) { v = -v; }; - return v, true; -}; - -// stoi64 — Hare-style fallible signed decimal parser. The value -// variant is i64; the error variant is a short str (subset of Hare's -// (invalid | overflow) tagged-union). No locale, no whitespace, no -// underscores: a leading '-' is the only non-digit accepted, and only -// at position 0. -export fn stoi64(s: str) (i64 | str) = { - if (s.len == 0) { return "parse: empty"; }; +// stoi64 — Hare-style fallible signed decimal parser. No locale, no +// whitespace, no underscores: a leading '-' is the only non-digit +// accepted, and only at position 0. +export fn stoi64(s: str) (i64 | invalid | overflow) = { + if (s.len == 0) { return 0: invalid; }; let i: i32 = 0; let neg: bool = false; if (s[0] == 45u8) { neg = true; i = 1; }; - if (i >= s.len) { return "parse: lone sign"; }; + if (i >= s.len) { return i: invalid; }; let v: i64 = 0; for (i < s.len) { let c: u8 = s[i]; - if (c < 48u8) { return "parse: invalid digit"; }; - if (c > 57u8) { return "parse: invalid digit"; }; + if (c < 48u8) { return i: invalid; }; + if (c > 57u8) { return i: invalid; }; v = v * 10 + ((c: i64) - 48); i += 1; }; @@ -106,16 +91,15 @@ export fn stoi64(s: str) (i64 | str) = { return v; }; -// stou64 — fallible unsigned decimal parser. No leading sign. Mirrors -// Hare's strconv::stou64. -export fn stou64(s: str) (u64 | str) = { - if (s.len == 0) { return "parse: empty"; }; +// stou64 — fallible unsigned decimal parser. No leading sign. +export fn stou64(s: str) (u64 | invalid | overflow) = { + if (s.len == 0) { return 0: invalid; }; let v: u64 = 0u64; let i: i32 = 0; for (i < s.len) { let c: u8 = s[i]; - if (c < 48u8) { return "parse: invalid digit"; }; - if (c > 57u8) { return "parse: invalid digit"; }; + if (c < 48u8) { return i: invalid; }; + if (c > 57u8) { return i: invalid; }; v = v * 10u64 + ((c: u64) - 48u64); i += 1; }; diff --git a/lib/ww/ast.ww b/lib/ww/ast.ww index a733e966..d92de7c0 100644 --- a/lib/ww/ast.ww +++ b/lib/ww/ast.ww @@ -88,8 +88,9 @@ def N_MASSIGN: i32 = 59; // 990_selfhost test diffs astprint against the C side byte-for-byte. def N_TYPETEST: i32 = 60; def N_TYPEASSERT: i32 = 61; +def N_VOIDLIT: i32 = 62; -def N_LAST: i32 = 62; +def N_LAST: i32 = 63; // ---- Node ------------------------------------------------------------- @@ -190,6 +191,7 @@ fn nkname(k: i32) str = { if (k == N_MASSIGN) { return "massign"; }; if (k == N_TYPETEST) { return "typetest"; }; if (k == N_TYPEASSERT) { return "typeassert"; }; + if (k == N_VOIDLIT) { return "voidlit"; }; if (k == N_LAST) { return "last"; }; return "?"; }; diff --git a/lib/ww/lex/tok.ww b/lib/ww/lex/tok.ww index da7ac879..2b240ed7 100644 --- a/lib/ww/lex/tok.ww +++ b/lib/ww/lex/tok.ww @@ -108,9 +108,10 @@ 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_IS: i32 = 82; +def TK_VOID: i32 = 83; -def TK_LAST: i32 = 83; +def TK_LAST: i32 = 84; // ---- Pos / Tok -------------------------------------------------------- // @@ -181,6 +182,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = { if (streqn(p, "true", n)) { return TK_TRUE; }; if (streqn(p, "type", n)) { return TK_TYPE; }; if (streqn(p, "use", n)) { return TK_USE; }; + if (streqn(p, "void", n)) { return TK_VOID; }; return TK_NONE; }; @@ -222,6 +224,7 @@ export fn tokname(k: i32) str = { if (k == TK_FALSE) { return "false"; }; if (k == TK_AS) { return "as"; }; if (k == TK_IS) { return "is"; }; + if (k == TK_VOID) { return "void"; }; if (k == TK_STATIC) { return "static"; }; if (k == TK_MATCH) { return "match"; }; if (k == TK_CONST) { return "const"; }; diff --git a/lib/ww/parse/expr.ww b/lib/ww/parse/expr.ww index 84652734..efe00fca 100644 --- a/lib/ww/parse/expr.ww +++ b/lib/ww/parse/expr.ww @@ -52,6 +52,10 @@ fn parseprimary(p: *parser) *node = { advance(p); return newnode(p.a, N_NIL, pf, pl, pc); }; + if (p.curkind == TK_VOID) { + advance(p); + return newnode(p.a, N_VOIDLIT, pf, pl, pc); + }; if (p.curkind == TK_UNDER) { // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT // with empty str; the checker rejects it outside lvalue diff --git a/lib/ww/parse/parse.ww b/lib/ww/parse/parse.ww index cf23bdd4..a399bc0d 100644 --- a/lib/ww/parse/parse.ww +++ b/lib/ww/parse/parse.ww @@ -164,6 +164,15 @@ fn parsetype(p: *parser) *node = { return n; }; + if (p.curkind == TK_VOID) { + // `void` keyword in type-expr context — emit as N_TNAME so + // resolution treats it like any other primitive name. + let n: *node = newnode(p.a, N_TNAME, pf, pl, pc); + n.str = "void"; + advance(p); + return n; + }; + if (p.curkind == TK_IDENT) { let n: *node = newnode(p.a, N_TNAME, pf, pl, pc); n.str = p.curtext; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 43aac02a..632ef9ee 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -321,12 +321,18 @@ export fn freearena(a: *arena) void = { // MODULE: strconv // strconv — number↔string conversions. Decimal i64 to/from a fixed -// buffer. Two error idioms ship side by side: -// - Plan 9 style (atoi64): tuple `(value, ok)`. Pre-dates the -// tagged-union work; kept for callers that already use it. -// - Hare style (stoi64/stou64): `(value | str)`. The error -// variant carries a short, allocation-free message describing -// why the parse failed. Prefer this for new code. +// buffer. Error shapes mirror Hare's strconv types: (T | invalid | +// overflow) where each error is a named alias over a payload type +// (Hare uses !size / !void; ww uses i32 / void without the `!` mark). + +// invalid — input wasn't a valid number in the requested format. +// Payload is the byte index of the first offending position (Hare +// strconv::invalid is `!size` carrying the same). +export type invalid = i32; + +// overflow — input was valid but doesn't fit the target type. No +// payload (a single yes/no signal). Mirrors Hare's `!void` shape. +export type overflow = void; // u64tos — write `v` in decimal into `buf` and return the byte count. // Hare name; the buffer-in shape is the sanctioned Plan 9 subset of @@ -386,41 +392,20 @@ export fn i64tos(buf: []u8, v: i64) i32 = { return out; }; -export fn atoi64(s: str) (i64, bool) = { - let v: i64 = 0; - let i: i32 = 0; - let neg: bool = false; - if (s.len > 0) { - if (s[0] == 45u8) { neg = true; i = 1; }; - }; - if (i >= s.len) { return 0, false; }; - for (i < s.len) { - let c: u8 = s[i]; - if (c < 48u8) { return 0, false; }; - if (c > 57u8) { return 0, false; }; - v = v * 10 + ((c: i64) - 48); - i += 1; - }; - if (neg) { v = -v; }; - return v, true; -}; - -// stoi64 — Hare-style fallible signed decimal parser. The value -// variant is i64; the error variant is a short str (subset of Hare's -// (invalid | overflow) tagged-union). No locale, no whitespace, no -// underscores: a leading '-' is the only non-digit accepted, and only -// at position 0. -export fn stoi64(s: str) (i64 | str) = { - if (s.len == 0) { return "parse: empty"; }; +// stoi64 — Hare-style fallible signed decimal parser. No locale, no +// whitespace, no underscores: a leading '-' is the only non-digit +// accepted, and only at position 0. +export fn stoi64(s: str) (i64 | invalid | overflow) = { + if (s.len == 0) { return 0: invalid; }; let i: i32 = 0; let neg: bool = false; if (s[0] == 45u8) { neg = true; i = 1; }; - if (i >= s.len) { return "parse: lone sign"; }; + if (i >= s.len) { return i: invalid; }; let v: i64 = 0; for (i < s.len) { let c: u8 = s[i]; - if (c < 48u8) { return "parse: invalid digit"; }; - if (c > 57u8) { return "parse: invalid digit"; }; + if (c < 48u8) { return i: invalid; }; + if (c > 57u8) { return i: invalid; }; v = v * 10 + ((c: i64) - 48); i += 1; }; @@ -428,16 +413,15 @@ export fn stoi64(s: str) (i64 | str) = { return v; }; -// stou64 — fallible unsigned decimal parser. No leading sign. Mirrors -// Hare's strconv::stou64. -export fn stou64(s: str) (u64 | str) = { - if (s.len == 0) { return "parse: empty"; }; +// stou64 — fallible unsigned decimal parser. No leading sign. +export fn stou64(s: str) (u64 | invalid | overflow) = { + if (s.len == 0) { return 0: invalid; }; let v: u64 = 0u64; let i: i32 = 0; for (i < s.len) { let c: u8 = s[i]; - if (c < 48u8) { return "parse: invalid digit"; }; - if (c > 57u8) { return "parse: invalid digit"; }; + if (c < 48u8) { return i: invalid; }; + if (c > 57u8) { return i: invalid; }; v = v * 10u64 + ((c: u64) - 48u64); i += 1; }; @@ -555,9 +539,10 @@ 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_IS: i32 = 82; +def TK_VOID: i32 = 83; -def TK_LAST: i32 = 83; +def TK_LAST: i32 = 84; // ---- Pos / Tok -------------------------------------------------------- // @@ -628,6 +613,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = { if (streqn(p, "true", n)) { return TK_TRUE; }; if (streqn(p, "type", n)) { return TK_TYPE; }; if (streqn(p, "use", n)) { return TK_USE; }; + if (streqn(p, "void", n)) { return TK_VOID; }; return TK_NONE; }; @@ -669,6 +655,7 @@ export fn tokname(k: i32) str = { if (k == TK_FALSE) { return "false"; }; if (k == TK_AS) { return "as"; }; if (k == TK_IS) { return "is"; }; + if (k == TK_VOID) { return "void"; }; if (k == TK_STATIC) { return "static"; }; if (k == TK_MATCH) { return "match"; }; if (k == TK_CONST) { return "const"; }; @@ -1734,8 +1721,9 @@ def N_MASSIGN: i32 = 59; // 990_selfhost test diffs astprint against the C side byte-for-byte. def N_TYPETEST: i32 = 60; def N_TYPEASSERT: i32 = 61; +def N_VOIDLIT: i32 = 62; -def N_LAST: i32 = 62; +def N_LAST: i32 = 63; // ---- Node ------------------------------------------------------------- @@ -1836,6 +1824,7 @@ fn nkname(k: i32) str = { if (k == N_MASSIGN) { return "massign"; }; if (k == N_TYPETEST) { return "typetest"; }; if (k == N_TYPEASSERT) { return "typeassert"; }; + if (k == N_VOIDLIT) { return "voidlit"; }; if (k == N_LAST) { return "last"; }; return "?"; }; @@ -2043,6 +2032,10 @@ fn parseprimary(p: *parser) *node = { advance(p); return newnode(p.a, N_NIL, pf, pl, pc); }; + if (p.curkind == TK_VOID) { + advance(p); + return newnode(p.a, N_VOIDLIT, pf, pl, pc); + }; if (p.curkind == TK_UNDER) { // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT // with empty str; the checker rejects it outside lvalue @@ -3011,6 +3004,15 @@ fn parsetype(p: *parser) *node = { return n; }; + if (p.curkind == TK_VOID) { + // `void` keyword in type-expr context — emit as N_TNAME so + // resolution treats it like any other primitive name. + let n: *node = newnode(p.a, N_TNAME, pf, pl, pc); + n.str = "void"; + advance(p); + return n; + }; + if (p.curkind == TK_IDENT) { let n: *node = newnode(p.a, N_TNAME, pf, pl, pc); n.str = p.curtext; @@ -4900,6 +4902,12 @@ fn cgexpr(c: *cgen, n: *node) void = { emitline("\tMOVQ\t$0, AX\n"); return; }; + if (k == N_VOIDLIT) { + // void value: zero-size, but the consumer's ABI expects a + // deterministic AX. Emit 0 like nil/false do. + emitline("\tMOVQ\t$0, AX\n"); + return; + }; if (k == N_IDENT) { cgident(c, n); return; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index a4c34f56..b3b3825c 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -53,6 +53,12 @@ fn cgexpr(c: *cgen, n: *node) void = { emitline("\tMOVQ\t$0, AX\n"); return; }; + if (k == N_VOIDLIT) { + // void value: zero-size, but the consumer's ABI expects a + // deterministic AX. Emit 0 like nil/false do. + emitline("\tMOVQ\t$0, AX\n"); + return; + }; if (k == N_IDENT) { cgident(c, n); return; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 9af650bf..b0c9e088 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -321,12 +321,18 @@ export fn freearena(a: *arena) void = { // MODULE: strconv // strconv — number↔string conversions. Decimal i64 to/from a fixed -// buffer. Two error idioms ship side by side: -// - Plan 9 style (atoi64): tuple `(value, ok)`. Pre-dates the -// tagged-union work; kept for callers that already use it. -// - Hare style (stoi64/stou64): `(value | str)`. The error -// variant carries a short, allocation-free message describing -// why the parse failed. Prefer this for new code. +// buffer. Error shapes mirror Hare's strconv types: (T | invalid | +// overflow) where each error is a named alias over a payload type +// (Hare uses !size / !void; ww uses i32 / void without the `!` mark). + +// invalid — input wasn't a valid number in the requested format. +// Payload is the byte index of the first offending position (Hare +// strconv::invalid is `!size` carrying the same). +export type invalid = i32; + +// overflow — input was valid but doesn't fit the target type. No +// payload (a single yes/no signal). Mirrors Hare's `!void` shape. +export type overflow = void; // u64tos — write `v` in decimal into `buf` and return the byte count. // Hare name; the buffer-in shape is the sanctioned Plan 9 subset of @@ -386,41 +392,20 @@ export fn i64tos(buf: []u8, v: i64) i32 = { return out; }; -export fn atoi64(s: str) (i64, bool) = { - let v: i64 = 0; - let i: i32 = 0; - let neg: bool = false; - if (s.len > 0) { - if (s[0] == 45u8) { neg = true; i = 1; }; - }; - if (i >= s.len) { return 0, false; }; - for (i < s.len) { - let c: u8 = s[i]; - if (c < 48u8) { return 0, false; }; - if (c > 57u8) { return 0, false; }; - v = v * 10 + ((c: i64) - 48); - i += 1; - }; - if (neg) { v = -v; }; - return v, true; -}; - -// stoi64 — Hare-style fallible signed decimal parser. The value -// variant is i64; the error variant is a short str (subset of Hare's -// (invalid | overflow) tagged-union). No locale, no whitespace, no -// underscores: a leading '-' is the only non-digit accepted, and only -// at position 0. -export fn stoi64(s: str) (i64 | str) = { - if (s.len == 0) { return "parse: empty"; }; +// stoi64 — Hare-style fallible signed decimal parser. No locale, no +// whitespace, no underscores: a leading '-' is the only non-digit +// accepted, and only at position 0. +export fn stoi64(s: str) (i64 | invalid | overflow) = { + if (s.len == 0) { return 0: invalid; }; let i: i32 = 0; let neg: bool = false; if (s[0] == 45u8) { neg = true; i = 1; }; - if (i >= s.len) { return "parse: lone sign"; }; + if (i >= s.len) { return i: invalid; }; let v: i64 = 0; for (i < s.len) { let c: u8 = s[i]; - if (c < 48u8) { return "parse: invalid digit"; }; - if (c > 57u8) { return "parse: invalid digit"; }; + if (c < 48u8) { return i: invalid; }; + if (c > 57u8) { return i: invalid; }; v = v * 10 + ((c: i64) - 48); i += 1; }; @@ -428,16 +413,15 @@ export fn stoi64(s: str) (i64 | str) = { return v; }; -// stou64 — fallible unsigned decimal parser. No leading sign. Mirrors -// Hare's strconv::stou64. -export fn stou64(s: str) (u64 | str) = { - if (s.len == 0) { return "parse: empty"; }; +// stou64 — fallible unsigned decimal parser. No leading sign. +export fn stou64(s: str) (u64 | invalid | overflow) = { + if (s.len == 0) { return 0: invalid; }; let v: u64 = 0u64; let i: i32 = 0; for (i < s.len) { let c: u8 = s[i]; - if (c < 48u8) { return "parse: invalid digit"; }; - if (c > 57u8) { return "parse: invalid digit"; }; + if (c < 48u8) { return i: invalid; }; + if (c > 57u8) { return i: invalid; }; v = v * 10u64 + ((c: u64) - 48u64); i += 1; }; @@ -555,9 +539,10 @@ 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_IS: i32 = 82; +def TK_VOID: i32 = 83; -def TK_LAST: i32 = 83; +def TK_LAST: i32 = 84; // ---- Pos / Tok -------------------------------------------------------- // @@ -628,6 +613,7 @@ export fn kwlookup(p: *u8, n: i32) i32 = { if (streqn(p, "true", n)) { return TK_TRUE; }; if (streqn(p, "type", n)) { return TK_TYPE; }; if (streqn(p, "use", n)) { return TK_USE; }; + if (streqn(p, "void", n)) { return TK_VOID; }; return TK_NONE; }; @@ -669,6 +655,7 @@ export fn tokname(k: i32) str = { if (k == TK_FALSE) { return "false"; }; if (k == TK_AS) { return "as"; }; if (k == TK_IS) { return "is"; }; + if (k == TK_VOID) { return "void"; }; if (k == TK_STATIC) { return "static"; }; if (k == TK_MATCH) { return "match"; }; if (k == TK_CONST) { return "const"; }; @@ -1734,8 +1721,9 @@ def N_MASSIGN: i32 = 59; // 990_selfhost test diffs astprint against the C side byte-for-byte. def N_TYPETEST: i32 = 60; def N_TYPEASSERT: i32 = 61; +def N_VOIDLIT: i32 = 62; -def N_LAST: i32 = 62; +def N_LAST: i32 = 63; // ---- Node ------------------------------------------------------------- @@ -1836,6 +1824,7 @@ fn nkname(k: i32) str = { if (k == N_MASSIGN) { return "massign"; }; if (k == N_TYPETEST) { return "typetest"; }; if (k == N_TYPEASSERT) { return "typeassert"; }; + if (k == N_VOIDLIT) { return "voidlit"; }; if (k == N_LAST) { return "last"; }; return "?"; }; @@ -2043,6 +2032,10 @@ fn parseprimary(p: *parser) *node = { advance(p); return newnode(p.a, N_NIL, pf, pl, pc); }; + if (p.curkind == TK_VOID) { + advance(p); + return newnode(p.a, N_VOIDLIT, pf, pl, pc); + }; if (p.curkind == TK_UNDER) { // Bare `_` — valid only as a discard lvalue. Emit an N_IDENT // with empty str; the checker rejects it outside lvalue @@ -3011,6 +3004,15 @@ fn parsetype(p: *parser) *node = { return n; }; + if (p.curkind == TK_VOID) { + // `void` keyword in type-expr context — emit as N_TNAME so + // resolution treats it like any other primitive name. + let n: *node = newnode(p.a, N_TNAME, pf, pl, pc); + n.str = "void"; + advance(p); + return n; + }; + if (p.curkind == TK_IDENT) { let n: *node = newnode(p.a, N_TNAME, pf, pl, pc); n.str = p.curtext; @@ -4900,6 +4902,12 @@ fn cgexpr(c: *cgen, n: *node) void = { emitline("\tMOVQ\t$0, AX\n"); return; }; + if (k == N_VOIDLIT) { + // void value: zero-size, but the consumer's ABI expects a + // deterministic AX. Emit 0 like nil/false do. + emitline("\tMOVQ\t$0, AX\n"); + return; + }; if (k == N_IDENT) { cgident(c, n); return; }; diff --git a/selfhost/test/smoke.combined.ww b/selfhost/test/smoke.combined.ww index b906a1f3..ba91afd2 100644 --- a/selfhost/test/smoke.combined.ww +++ b/selfhost/test/smoke.combined.ww @@ -213,12 +213,18 @@ export fn getdents64(fd: i32, buf: *u8, n: u64) i64 = { // MODULE: strconv // strconv — number↔string conversions. Decimal i64 to/from a fixed -// buffer. Two error idioms ship side by side: -// - Plan 9 style (atoi64): tuple `(value, ok)`. Pre-dates the -// tagged-union work; kept for callers that already use it. -// - Hare style (stoi64/stou64): `(value | str)`. The error -// variant carries a short, allocation-free message describing -// why the parse failed. Prefer this for new code. +// buffer. Error shapes mirror Hare's strconv types: (T | invalid | +// overflow) where each error is a named alias over a payload type +// (Hare uses !size / !void; ww uses i32 / void without the `!` mark). + +// invalid — input wasn't a valid number in the requested format. +// Payload is the byte index of the first offending position (Hare +// strconv::invalid is `!size` carrying the same). +export type invalid = i32; + +// overflow — input was valid but doesn't fit the target type. No +// payload (a single yes/no signal). Mirrors Hare's `!void` shape. +export type overflow = void; // u64tos — write `v` in decimal into `buf` and return the byte count. // Hare name; the buffer-in shape is the sanctioned Plan 9 subset of @@ -278,41 +284,20 @@ export fn i64tos(buf: []u8, v: i64) i32 = { return out; }; -export fn atoi64(s: str) (i64, bool) = { - let v: i64 = 0; - let i: i32 = 0; - let neg: bool = false; - if (s.len > 0) { - if (s[0] == 45u8) { neg = true; i = 1; }; - }; - if (i >= s.len) { return 0, false; }; - for (i < s.len) { - let c: u8 = s[i]; - if (c < 48u8) { return 0, false; }; - if (c > 57u8) { return 0, false; }; - v = v * 10 + ((c: i64) - 48); - i += 1; - }; - if (neg) { v = -v; }; - return v, true; -}; - -// stoi64 — Hare-style fallible signed decimal parser. The value -// variant is i64; the error variant is a short str (subset of Hare's -// (invalid | overflow) tagged-union). No locale, no whitespace, no -// underscores: a leading '-' is the only non-digit accepted, and only -// at position 0. -export fn stoi64(s: str) (i64 | str) = { - if (s.len == 0) { return "parse: empty"; }; +// stoi64 — Hare-style fallible signed decimal parser. No locale, no +// whitespace, no underscores: a leading '-' is the only non-digit +// accepted, and only at position 0. +export fn stoi64(s: str) (i64 | invalid | overflow) = { + if (s.len == 0) { return 0: invalid; }; let i: i32 = 0; let neg: bool = false; if (s[0] == 45u8) { neg = true; i = 1; }; - if (i >= s.len) { return "parse: lone sign"; }; + if (i >= s.len) { return i: invalid; }; let v: i64 = 0; for (i < s.len) { let c: u8 = s[i]; - if (c < 48u8) { return "parse: invalid digit"; }; - if (c > 57u8) { return "parse: invalid digit"; }; + if (c < 48u8) { return i: invalid; }; + if (c > 57u8) { return i: invalid; }; v = v * 10 + ((c: i64) - 48); i += 1; }; @@ -320,16 +305,15 @@ export fn stoi64(s: str) (i64 | str) = { return v; }; -// stou64 — fallible unsigned decimal parser. No leading sign. Mirrors -// Hare's strconv::stou64. -export fn stou64(s: str) (u64 | str) = { - if (s.len == 0) { return "parse: empty"; }; +// stou64 — fallible unsigned decimal parser. No leading sign. +export fn stou64(s: str) (u64 | invalid | overflow) = { + if (s.len == 0) { return 0: invalid; }; let v: u64 = 0u64; let i: i32 = 0; for (i < s.len) { let c: u8 = s[i]; - if (c < 48u8) { return "parse: invalid digit"; }; - if (c > 57u8) { return "parse: invalid digit"; }; + if (c < 48u8) { return i: invalid; }; + if (c > 57u8) { return i: invalid; }; v = v * 10u64 + ((c: u64) - 48u64); i += 1; }; diff --git a/test/wcc/700_e2e.c b/test/wcc/700_e2e.c index 6c92a50b..3422b496 100644 --- a/test/wcc/700_e2e.c +++ b/test/wcc/700_e2e.c @@ -744,46 +744,53 @@ static const struct row rows[] = { " };\n" " return acc;\n" "};", 13 }, /* 1 byte written to fd 1, plus len(\"write failed\")=12 */ - /* strconv.stoi64: fallible signed decimal. Two successful - * parses contribute their values; one bad parse contributes - * the error message length (20 = len(\"parse: invalid digit\")). */ + /* strconv.stoi64: fallible signed decimal, graduated to + * (i64 | invalid | overflow). invalid carries the offending + * index; overflow is the void variant. */ { "use strconv;\n" + "type r_t = (i64 | strconv.invalid | strconv.overflow);\n" "fn main() i32 = {\n" - " let r1: (i64 | str) = strconv.stoi64(\"42\");\n" - " let r2: (i64 | str) = strconv.stoi64(\"-7\");\n" - " let r3: (i64 | str) = strconv.stoi64(\"abc\");\n" + " let r1: r_t = strconv.stoi64(\"42\");\n" + " let r2: r_t = strconv.stoi64(\"-7\");\n" + " let r3: r_t = strconv.stoi64(\"abc\");\n" " let acc: i32 = 0;\n" " match (r1) {\n" " case let v: i64 => acc += v: i32;\n" - " case let e: str => acc += -100;\n" + " case let e: strconv.invalid => acc += -100;\n" + " case let e: strconv.overflow => acc += -200;\n" " };\n" " match (r2) {\n" " case let v: i64 => acc += v: i32;\n" - " case let e: str => acc += -100;\n" + " case let e: strconv.invalid => acc += -100;\n" + " case let e: strconv.overflow => acc += -200;\n" " };\n" " match (r3) {\n" " case let v: i64 => acc += -100;\n" - " case let e: str => acc += e.len: i32;\n" + " case let e: strconv.invalid => acc += e: i32;\n" + " case let e: strconv.overflow => acc += -200;\n" " };\n" " return acc;\n" - "};", 55 }, /* 42 + (-7) + 20 */ - /* strconv.stou64: success path 123, error path captures - * len(\"parse: invalid digit\") = 20 for the leading-sign reject. */ + "};", 35 }, /* 42 + (-7) + 0 (invalid at index 0 in \"abc\") */ + /* strconv.stou64: success path; leading-sign rejected with + * invalid carrying the offending index. */ { "use strconv;\n" + "type r_t = (u64 | strconv.invalid | strconv.overflow);\n" "fn main() i32 = {\n" - " let r1: (u64 | str) = strconv.stou64(\"123\");\n" - " let r2: (u64 | str) = strconv.stou64(\"-1\");\n" + " let r1: r_t = strconv.stou64(\"123\");\n" + " let r2: r_t = strconv.stou64(\"-1\");\n" " let acc: i32 = 0;\n" " match (r1) {\n" " case let v: u64 => acc += v: i32;\n" - " case let e: str => acc += -100;\n" + " case let e: strconv.invalid => acc += -100;\n" + " case let e: strconv.overflow => acc += -200;\n" " };\n" " match (r2) {\n" " case let v: u64 => acc += -100;\n" - " case let e: str => acc += e.len: i32;\n" + " case let e: strconv.invalid => acc += e: i32;\n" + " case let e: strconv.overflow => acc += -200;\n" " };\n" " return acc;\n" - "};", 143 }, /* 123 + 20 */ + "};", 123 }, /* 123 + 0 (invalid at index 0 in \"-1\") */ /* strings.byteindex and strings.index: now (i32 | void). */ { "use strings;\n" "fn pick(r: (i32 | void), miss: i32) i32 = {\n"