lumbda/wasm/tests/parity-corpus.mjs
russell@unturf.com ee5e997df7
wat: internal-define scoping (R7RS letrec*) + c-wasm gc gap documented
WAT — leading (define ...) forms in a lambda body now bind LOCALLY
(letrec*-equivalent) instead of polluting the global env. Implementation:
apply for closures pre-processes the body in three passes:
  1. hoist_internal_defines walks leading defines, env_define each name
     to VOID in the new env, returns the extended env.
  2. strip_leading_defines returns the body with the defines removed.
  3. fill_internal_defines evaluates each define's value-expression in
     the new env (so mutual references work) and env_set the real value.

(define x 1)
(define (f) (define x 99) x)
(f)   ; → 99 (was 99, still 99)
x     ; → 1  (was 99 wrongly — fixed)

(define (h) (define helper (lambda (x) (* x 2))) (helper 5))
helper  ; → unbound (was a leaked global procedure — fixed)

C-WASM — added a thorough doc-block in lumbda_wasm_entry.c covering
the gc.c fallback malloc situation and three plausible real fixes
(Boehm-em build, custom mark-sweep over NaN-boxed heap, generational
reset). Repl tabbar already surfaces the pressure to the user.

Parity corpus locks the new scoping behavior:
  internal-define-local    — global x stays 1
  internal-define-returns  — f returns 99
  internal-define-mutual   — mutually-recursive internal defines

Tests: 20 unit, 8 integration, 11 functional, 249 parity all green.
2026-06-14 17:54:33 -04:00

156 lines
11 KiB
JavaScript

// wasm/tests/parity-corpus.mjs
// The cross-tier parity corpus. Each entry is one expression; we expect
// every tier to produce the same output. Entries are tagged with the
// whitepaper section / R7RS concept they exercise so a regression can be
// traced to the normative claim it violates.
//
// `expected` is set to the python tier's output (the reference, per
// "Exact rational arithmetic uses Python's Fraction type"); we mark the
// known-divergent ones with `knownDiverge: ["asm"]` so the suite remains
// green while documenting the gap.
export const CORPUS = [
// ─── numeric tower: rationals ────────────────────────────────────
{ tag: "rat-div", src: "(/ 67 7)", expected: "67/7" },
{ tag: "rat-div-one", src: "(/ 1 3)", expected: "1/3" },
{ tag: "rat-div-exact", src: "(/ 6 2)", expected: "3" },
{ tag: "rat-add", src: "(+ 1/3 1/6)", expected: "1/2" },
{ tag: "rat-mul", src: "(* 2/3 3/4)", expected: "1/2" },
{ tag: "rat-sub", src: "(- 3/4 1/2)", expected: "1/4" },
{ tag: "rat-mixed-add", src: "(+ 1 1/2)", expected: "3/2" },
{ tag: "rat-eq-norm", src: "(= 1/2 2/4)", expected: "#t" },
{ tag: "rat-eq-int", src: "(= 3 6/2)", expected: "#t" },
{ tag: "rat-lt", src: "(< 1/3 1/2)", expected: "#t" },
{ tag: "number?-rat", src: "(number? 1/3)", expected: "#t" },
{ tag: "integer?-rat", src: "(integer? 1/3)", expected: "#f" },
{ tag: "integer?-int", src: "(integer? 6/2)", expected: "#t" },
// ─── numeric tower: bignums (whitepaper §2.1 claim) ──────────────
{ tag: "expt-2-100", src: "(expt 2 100)",
expected: "1267650600228229401496703205376" },
{ tag: "expt-3-50", src: "(expt 3 50)",
expected: "717897987691852588770249" },
{ tag: "big-arith", src: "(* 12345678901234567890 12345678901234567890)",
expected: "152415787532388367501905199875019052100" },
{ tag: "expt-2-1024", src: "(expt 2 1024)",
expected: "179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137216" },
{ tag: "big-zero", src: "(- (expt 2 100) (expt 2 100))", expected: "0" },
{ tag: "big-cmp", src: "(< 99999999999 1000000000000)", expected: "#t" },
{ tag: "big-eq", src: "(= (expt 2 100) (* (expt 2 50) (expt 2 50)))", expected: "#t" },
{ tag: "big-int?", src: "(integer? (expt 2 100))", expected: "#t" },
{ tag: "big-num?", src: "(number? (expt 2 100))", expected: "#t" },
// ─── GC / memory pressure (allocate-and-drop) ────────────────────
// These exercise the asm-wasm copying collector and verify the
// Python + C tier GCs handle the same workload identically.
{ tag: "gc-throwaway", expected: "ok",
src: "(let loop ((i 0)) (if (= i 500) (quote ok) (begin (cons i i) (loop (+ i 1)))))" },
{ tag: "gc-retained-length", expected: "500",
src: "(define lst (let loop ((i 0) (acc (quote ()))) (if (= i 500) acc (loop (+ i 1) (cons i acc))))) (length lst)" },
{ tag: "gc-survives-eval", expected: "(1 4 9 16)",
src: "(let loop ((i 0)) (if (= i 200) 0 (begin (cons i i) (loop (+ i 1))))) (map (lambda (x) (* x x)) (list 1 2 3 4))" },
// ─── Internal-define scoping (R7RS letrec*) ──────────────────────
{ tag: "internal-define-local", expected: "1",
src: "(define x 1) (define (f) (define x 99) x) (f) x" },
{ tag: "internal-define-returns", expected: "99",
src: "(define (f) (define x 99) x) (f)" },
{ tag: "internal-define-mutual", expected: "#t",
src: "(define (g) (define (e? n) (if (= n 0) #t (o? (- n 1)))) (define (o? n) (if (= n 0) #f (e? (- n 1)))) (e? 6)) (g)" },
// ─── division semantics ──────────────────────────────────────────
{ tag: "quotient-pos", src: "(quotient 17 5)", expected: "3" },
{ tag: "quotient-neg", src: "(quotient -17 5)", expected: "-3" },
{ tag: "remainder-pos", src: "(remainder 17 5)", expected: "2" },
{ tag: "remainder-neg", src: "(remainder -17 5)", expected: "-2" },
{ tag: "modulo-pos", src: "(modulo 17 5)", expected: "2" },
{ tag: "modulo-neg", src: "(modulo -17 5)", expected: "3" },
// ─── arithmetic edge cases ───────────────────────────────────────
{ tag: "neg-arith", src: "(- 5)", expected: "-5" },
{ tag: "empty-add", src: "(+)", expected: "0" },
{ tag: "empty-mul", src: "(*)", expected: "1" },
{ tag: "multi-arith", src: "(+ 1 2 3 4 5)", expected: "15" },
{ tag: "abs-pos", src: "(abs 5)", expected: "5" },
{ tag: "abs-neg", src: "(abs -5)", expected: "5" },
{ tag: "min", src: "(min 5 3 8 1 7)", expected: "1" },
{ tag: "max", src: "(max 5 3 8 1 7)", expected: "8" },
// ─── booleans / truthiness ───────────────────────────────────────
{ tag: "if-true", src: "(if #t 'yes 'no)", expected: "yes" },
{ tag: "if-false", src: "(if #f 'yes 'no)", expected: "no" },
{ tag: "if-zero", src: "(if 0 'truthy 'falsy)", expected: "truthy" },
{ tag: "if-nil", src: "(if '() 'truthy 'falsy)", expected: "truthy" },
{ tag: "if-empty-str", src: "(if \"\" 'truthy 'falsy)", expected: "truthy" },
{ tag: "not-#f", src: "(not #f)", expected: "#t" },
{ tag: "not-#t", src: "(not #t)", expected: "#f" },
{ tag: "not-zero", src: "(not 0)", expected: "#f" },
// ─── equality ────────────────────────────────────────────────────
{ tag: "eq?-int", src: "(eq? 1 1)", expected: "#t" },
{ tag: "eq?-symbol", src: "(eq? 'a 'a)", expected: "#t" },
{ tag: "equal?-list", src: "(equal? '(1 2 3) '(1 2 3))", expected: "#t" },
{ tag: "equal?-str", src: "(equal? \"abc\" \"abc\")", expected: "#t" },
// ─── pairs / lists ───────────────────────────────────────────────
{ tag: "cons", src: "(cons 1 2)", expected: "(1 . 2)" },
{ tag: "list", src: "(list 1 2 3)", expected: "(1 2 3)" },
{ tag: "length", src: "(length '(a b c d))", expected: "4" },
{ tag: "reverse", src: "(reverse '(1 2 3))", expected: "(3 2 1)" },
{ tag: "append", src: "(append '(1 2) '(3 4))", expected: "(1 2 3 4)" },
{ tag: "map", src: "(map (lambda (x) (* x x)) '(1 2 3 4))",
expected: "(1 4 9 16)" },
{ tag: "filter-odd", src: "(filter odd? '(1 2 3 4 5))",
expected: "(1 3 5)" },
{ tag: "fold-left", src: "(fold-left + 0 '(1 2 3 4 5))", expected: "15" },
// ─── strings ─────────────────────────────────────────────────────
{ tag: "str-len", src: "(string-length \"hello\")", expected: "5" },
{ tag: "str-append", src: "(string-append \"foo\" \"bar\")",
expected: "foobar" },
{ tag: "substring", src: "(substring \"hello world\" 6 11)",
expected: "world" },
{ tag: "str-upcase", src: "(string-upcase \"hello\")", expected: "HELLO" },
{ tag: "str-num", src: "(string->number \"42\")", expected: "42" },
{ tag: "num-str", src: "(number->string 42)", expected: "42" },
// ─── characters ──────────────────────────────────────────────────
{ tag: "char-int", src: "(char->integer #\\A)", expected: "65" },
{ tag: "int-char", src: "(integer->char 65)", expected: "A" },
{ tag: "char-alpha", src: "(char-alphabetic? #\\a)", expected: "#t" },
// ─── vectors ─────────────────────────────────────────────────────
{ tag: "vec-make", src: "(vector 1 2 3)", expected: "#(1 2 3)" },
{ tag: "vec-len", src: "(vector-length (vector 1 2 3))", expected: "3" },
{ tag: "vec-ref", src: "(vector-ref (vector 10 20 30) 1)", expected: "20" },
// ─── special forms ──────────────────────────────────────────────
{ tag: "let-basic", src: "(let ((x 3) (y 4)) (+ x x y))", expected: "10" },
{ tag: "let*-shadow", src: "(let* ((x 1) (x (+ x 10)) (x (* x 2))) x)",
expected: "22" },
{ tag: "letrec", src: "(letrec ((f (lambda (n) (if (= n 0) 1 (* n (f (- n 1))))))) (f 5))",
expected: "120" },
{ tag: "named-let", src: "(let loop ((i 0) (acc 0)) (if (= i 10) acc (loop (+ i 1) (+ acc i))))",
expected: "45" },
{ tag: "cond-else", src: "(cond ((= 1 2) 'no) (else 'yes))", expected: "yes" },
{ tag: "case-match", src: "(case 2 ((1) 'one) ((2 3) 'two-three) (else 'big))",
expected: "two-three" },
{ tag: "when", src: "(when (> 3 1) 'yes)", expected: "yes" },
{ tag: "and-pass", src: "(and 1 2 3)", expected: "3" },
{ tag: "or-first", src: "(or #f 7 8)", expected: "7" },
// ─── deep recursion / TCO ───────────────────────────────────────
{ tag: "tco-loop", src: "(let loop ((i 0)) (if (= i 50000) i (loop (+ i 1))))",
expected: "50000" },
{ tag: "mutual-tco", src: "(define (a n) (if (= n 0) 'done-a (b (- n 1)))) (define (b n) (if (= n 0) 'done-b (a (- n 1)))) (a 100000)",
expected: "done-a" },
];
// Tiers where each tag is known to diverge today. Keep this short and
// remove entries as the gaps close — that's how we track "% to parity".
// As of the WAT bignum landing (tag 10 + num_add/sub/mul/cmp promotion),
// the corpus has zero known divergences and the whitepaper §2.1 claim
// "(expt 2 1024) returns the exact value across all three tiers" is
// satisfied. New tests that surface a fresh gap get added here so the
// regression suite stays green.
export const KNOWN_DIVERGE = {};