examples: lisp — own STR bytes; dotted-pair literals

vstr now copies the input bytes into the trans arena and promote_v
does the same into perm at the top-level boundary. STR cells used
to borrow the lexer's input slice; the REPL's buf-shift between
forms overwrote those bytes, so a top-level (define x "...") would
print garbage after the next read. Mirror of Hare's strings::dup,
arena-routed so the bytes share the cell's lifetime.

Parser learns dotted-pair literals: '(a b . c) splices the tail
into the cdr of the last cons. A bare '.' inside a list lexes as
tkind.DOT; outside a list it's still a parser error. Pre-fix the
'.' lexed as a one-byte SYM, producing a 3-element proper list.

Drop the unused args_to_slice — eval inlines on purpose (the
wwstage cgen drops slice.len through a tagged-union return).

Tests: 18 new probes (str-survives-3-defines, str-from-lambda,
dotted-pair walk + error edges) + a check_str helper. 101/101.
This commit is contained in:
2026-05-13 01:41:47 +09:00
parent 3f0d1939f5
commit ebfd8c3652
2 changed files with 170 additions and 34 deletions

View File

@@ -167,6 +167,61 @@ fn check_err(name: str, input: str, ep: **env) void = {
};
};
// check_str — assert kind==STR and byte-wise equal to `want`. Covers
// the str-ownership story: vstr/promote_v copy bytes into the arena
// so the value survives the REPL's buf reuse.
fn check_str(name: str, input: str, want: str, ep: **env) void = {
ntotal += 1;
let r = eval_str(input, ep);
match (r) {
case let v: *value => {
let p: *value = v;
if (p.kind != valkind.STR) {
fail(name, "kind != STR");
return;
};
let got: str = p.text;
if (got.len != want.len) {
faili(name, "wrong len", got.len: i64);
return;
};
let i: i32 = 0;
for (i < got.len) {
if (got[i] != want[i]) {
faili(name, "byte mismatch at", i: i64);
return;
};
i += 1;
};
ok(name);
};
case let _e: rterror => fail(name, "rterror");
};
};
// strconv.f64tos probes. We run them through lisp_test rather than a
// standalone strconv test program because there's no stdlib-test
// scaffolding yet (cf. test/wcc/900_stdlib.c which only checks
// modules compile, not behaviour). Move out when that lands.
fn check_f64tos(name: str, v: f64, want: str) void = {
ntotal += 1;
let buf: [32]u8;
let n: i32 = strconv.f64tos(buf[0:32], v);
if (n != want.len) {
faili(name, "wrong len", n: i64);
return;
};
let i: i32 = 0;
for (i < n) {
if (buf[i] != want[i]) {
faili(name, "byte mismatch at", i: i64);
return;
};
i += 1;
};
ok(name);
};
// run an expression for its effect (e.g. `(define ...)`); ignore the
// returned nil. Test passes iff there's no runtime error.
fn run(name: str, input: str, ep: **env) void = {
@@ -282,12 +337,63 @@ export fn main() i32 = {
check_float("float-promote", "(+ 1 2.5)", 3.5, ep);
check_bool ("float-cmp", "(< 1.0 2.0)", true, ep);
// ---- strconv.f64tos ----
// Direct probes against the new strconv entry. The lisp printer
// delegates to this, so any regression here surfaces in `(println
// 1.5)` style output too. See lib/strconv/strconv.ww for the
// documented subset (no NaN/Inf/sci, ≥9.22e18 → "huge").
check_f64tos("f64tos-int", 1.0, "1");
check_f64tos("f64tos-half", 1.5, "1.5");
check_f64tos("f64tos-pi", 3.14, "3.14");
check_f64tos("f64tos-tenth", 0.1, "0.1");
check_f64tos("f64tos-neg", -2.5, "-2.5");
check_f64tos("f64tos-zero", 0.0, "0");
check_f64tos("f64tos-hundred", 100.0, "100");
check_f64tos("f64tos-leadzero", 0.05, "0.05");
check_f64tos("f64tos-roundup", 0.9999996, "1");
check_f64tos("f64tos-trim", 123.450000, "123.45");
check_f64tos("f64tos-huge", 9.5e18, "huge");
// ---- runtime errors ----
check_err ("err-unbound", "this-symbol-isnt-bound", ep);
check_err ("err-car-not-pair", "(car 1)", ep);
check_err ("err-div-zero", "(/ 5 0)", ep);
check_err ("err-bad-arg", "(+ 'a 'b)", ep);
// ---- strings (vstr/promote_v deep-copy) ----
// Pre-fix, top-level (define s "..") then later use printed
// garbage because vstr borrowed the lexer's input buffer and
// the REPL shifted it between forms. vstr now owns its bytes,
// promote_v copies them into perm. Each probe runs eval_str on
// its own input, so any borrow back into a dead source slice
// would surface here as a byte mismatch.
check_str ("str-literal", "\"hello\"", "hello", ep);
run ("def-s1", "(define s1 \"first\")", ep);
run ("def-s2", "(define s2 \"second\")", ep);
run ("def-s3", "(define s3 \"third\")", ep);
check_str ("str-s1-survives", "s1", "first", ep);
check_str ("str-s2-survives", "s2", "second", ep);
check_str ("str-s3-survives", "s3", "third", ep);
// String embedded in a lambda body — the lambda's body cell
// holds a STR sub-cell that has to be promoted too.
run ("def-getstr", "(define getstr (lambda () \"inside\"))", ep);
check_str ("str-from-lambda", "(getstr)", "inside", ep);
// ---- dotted-pair literals ----
// '(1 . 2) used to lex `.` as a 1-byte SYM, producing a
// 3-element proper list. Now: lexer emits tkind.DOT inside a
// list and the parser splices it as the cdr.
check_kind ("dot-pair-kind", "'(1 . 2)", valkind.CONS, ep);
check_int ("dot-pair-car", "(car '(1 . 2))", 1i64, ep);
check_int ("dot-pair-cdr", "(cdr '(1 . 2))", 2i64, ep);
check_bool ("dot-pair-not-pair-cdr", "(pair? (cdr '(1 . 2)))", false, ep);
// Walk `(1 2 . 3)` → car=1, cadr=2, cddr=3 (the dotted tail).
check_int ("dot-tail-car", "(car '(1 2 . 3))", 1i64, ep);
check_int ("dot-tail-cadr", "(car (cdr '(1 2 . 3)))", 2i64, ep);
check_int ("dot-tail-cddr", "(cdr (cdr '(1 2 . 3)))", 3i64, ep);
check_err ("dot-leading", "'(. 2)", ep);
check_err ("dot-trailing", "'(1 . 2 3)", ep);
// ---- summary ----
let buf: [32]u8;
let n: i32 = 0;