parity probe: cross-tier corpus + fix python remainder + asm modulo
New: wasm/tests/parity-cross-tier.mjs runs the parity-corpus.mjs (216
test cases tagged by whitepaper section / R7RS concept) against three
tiers — native python (reference), c-wasm, asm-wasm — and fails on any
unknown divergence. Known gaps live in KNOWN_DIVERGE so the table stays
green while the bignum / call/cc / etc. work proceeds.
Wired into `make wasm-test` so a regression against any spec claim gets
caught before merge.
Bugs caught and fixed:
- python remainder: was `signed_a % signed_b * sign(a)`, which double-
applied the sign of a (python's % floors) — gave -3 for (-17, 5)
instead of the R7RS-correct -2. Now uses abs() on both sides.
- asm-wasm modulo: was i32.rem_s (truncated, remainder semantics)
where R7RS modulo wants sign of divisor. Added the "if rem and
divisor disagree on sign, add divisor" branch.
Cross-tier numbers after fix:
216 passing
3 known diverge: expt-2-100, expt-3-50, big-arith — all asm-wasm
(no bignums on the asm tier yet; whitepaper §2.1 claim still open)
0 fail
REPL layout: body is now the scroll container, prompt-bar is
position:fixed at the viewport bottom so it doesn't get pushed off
screen by a long transcript. Empty space above the prompt on a fresh
session reads like a terminal.
All other tests still pass: 20 unit, 8 integration, 11 functional.
This commit is contained in:
parent
c50a9da7e8
commit
7ea65dba21
17 changed files with 357 additions and 38 deletions
|
|
@ -3438,7 +3438,11 @@ def make_global_env():
|
|||
return n
|
||||
d(S('/'), _div)
|
||||
d(S('quotient'), lambda a, _: (lambda x, y: -(abs(int(x)) // abs(int(y))) if (x < 0) != (y < 0) else abs(int(x)) // abs(int(y)))(_num(a[0]), _num(a[1])))
|
||||
d(S('remainder'), lambda a, _: int(_num(a[0])) % int(_num(a[1])) * (1 if _num(a[0]) >= 0 else -1))
|
||||
# R7RS: remainder has the sign of the dividend; uses truncated division.
|
||||
# The previous impl did `signed_a % signed_b * sign(a)` which double-counted
|
||||
# the sign of a (Python's % floors, so `-17 % 5 == 3`) and gave -3 for
|
||||
# (-17, 5) instead of the correct -2. Use abs() on both sides, then re-sign.
|
||||
d(S('remainder'), lambda a, _: (abs(int(_num(a[0]))) % abs(int(_num(a[1])))) * (1 if _num(a[0]) >= 0 else -1))
|
||||
d(S('modulo'), lambda a, _: int(_num(a[0])) % int(_num(a[1])))
|
||||
d(S('expt'), lambda a, _: _num(a[0]) ** _num(a[1]))
|
||||
d(S('abs'), lambda a, _: abs(_num(a[0])))
|
||||
|
|
|
|||
|
|
@ -168,7 +168,7 @@ repl: build
|
|||
|
||||
# ─── Tests ─────────────────────────────────────────────────────────
|
||||
|
||||
test: test-unit test-integration test-functional-cross
|
||||
test: test-unit test-integration test-parity test-functional-cross
|
||||
|
||||
test-unit: build
|
||||
@echo "── wasm unit tests ──"
|
||||
|
|
@ -178,6 +178,15 @@ test-integration: build
|
|||
@echo "── wasm integration tests (cross-tier diff) ──"
|
||||
node tests/integration.mjs
|
||||
|
||||
# Cross-tier parity probe — every expression in parity-corpus.mjs runs on
|
||||
# native python (reference), c-wasm, and asm-wasm. Fails on any unknown
|
||||
# divergence so a regression against whitepaper § normative claims gets
|
||||
# caught before merge. Known gaps (bignums on asm-wasm today) live in
|
||||
# KNOWN_DIVERGE; trim that list as the gaps close.
|
||||
test-parity: build
|
||||
@echo "── wasm cross-tier parity probe ──"
|
||||
node tests/parity-cross-tier.mjs
|
||||
|
||||
# Cross-tier functional.lsp runner. C-wasm reaches full parity (205/205);
|
||||
# asm-wasm passes a documented growing subset and is exit-soft.
|
||||
test-functional-cross: build
|
||||
|
|
|
|||
|
|
@ -2498,11 +2498,19 @@
|
|||
(then (local.set $sum (i32.sub (i32.const 0) (local.get $sum)))))
|
||||
(return (call $make_fixnum (local.get $sum)))))
|
||||
|
||||
;; modulo
|
||||
;; modulo — R7RS: result has the sign of the divisor.
|
||||
;; i32.rem_s by itself gives remainder semantics (sign of dividend);
|
||||
;; we add the divisor if the rem and divisor disagree on sign.
|
||||
(if (i32.eq (local.get $id) (i32.const 23))
|
||||
(then
|
||||
(return (call $make_fixnum (i32.rem_s (call $fixnum_val (local.get $a))
|
||||
(call $fixnum_val (local.get $b)))))))
|
||||
(local.set $sum (i32.rem_s (call $fixnum_val (local.get $a))
|
||||
(call $fixnum_val (local.get $b))))
|
||||
(if (i32.and
|
||||
(i32.ne (local.get $sum) (i32.const 0))
|
||||
(i32.lt_s (i32.mul (local.get $sum) (call $fixnum_val (local.get $b)))
|
||||
(i32.const 0)))
|
||||
(then (local.set $sum (i32.add (local.get $sum) (call $fixnum_val (local.get $b))))))
|
||||
(return (call $make_fixnum (local.get $sum)))))
|
||||
|
||||
;; zero?
|
||||
(if (i32.eq (local.get $id) (i32.const 24))
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -3438,7 +3438,11 @@ def make_global_env():
|
|||
return n
|
||||
d(S('/'), _div)
|
||||
d(S('quotient'), lambda a, _: (lambda x, y: -(abs(int(x)) // abs(int(y))) if (x < 0) != (y < 0) else abs(int(x)) // abs(int(y)))(_num(a[0]), _num(a[1])))
|
||||
d(S('remainder'), lambda a, _: int(_num(a[0])) % int(_num(a[1])) * (1 if _num(a[0]) >= 0 else -1))
|
||||
# R7RS: remainder has the sign of the dividend; uses truncated division.
|
||||
# The previous impl did `signed_a % signed_b * sign(a)` which double-counted
|
||||
# the sign of a (Python's % floors, so `-17 % 5 == 3`) and gave -3 for
|
||||
# (-17, 5) instead of the correct -2. Use abs() on both sides, then re-sign.
|
||||
d(S('remainder'), lambda a, _: (abs(int(_num(a[0]))) % abs(int(_num(a[1])))) * (1 if _num(a[0]) >= 0 else -1))
|
||||
d(S('modulo'), lambda a, _: int(_num(a[0])) % int(_num(a[1])))
|
||||
d(S('expt'), lambda a, _: _num(a[0]) ** _num(a[1]))
|
||||
d(S('abs'), lambda a, _: abs(_num(a[0])))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
/* lumbda repl — grid-only layout. Inherits palette + base from style.css. */
|
||||
|
||||
body.repl {
|
||||
display: grid;
|
||||
/* header (fixed) scrolling-stream (everything else) */
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
/* Body scrolls naturally. The prompt-bar is position: fixed so it
|
||||
* stays glued to the viewport bottom; the transcript reserves bottom
|
||||
* padding equal to the prompt-bar height so its last line isn't hidden
|
||||
* underneath. */
|
||||
min-height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ─── Lock screen overlay ──────────────────────────────────────── */
|
||||
|
|
@ -176,12 +177,13 @@ body.repl {
|
|||
* fresh sessions show the prompt up near the top and it drifts down
|
||||
* with each new entry. */
|
||||
.repl-stream {
|
||||
overflow: auto;
|
||||
padding: 0.5rem 1rem 0.8rem;
|
||||
padding: 0.5rem 1rem 0;
|
||||
background: var(--code-bg);
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
align-content: start;
|
||||
/* Leave room for the fixed prompt bar at the viewport bottom. */
|
||||
padding-bottom: calc(3.6rem + env(safe-area-inset-bottom, 0));
|
||||
}
|
||||
.transcript {
|
||||
font-family: var(--mono);
|
||||
|
|
@ -232,12 +234,16 @@ body.repl {
|
|||
/* ─── Prompt bar ───────────────────────────────────────────────── */
|
||||
|
||||
.prompt-bar {
|
||||
position: fixed;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
z-index: 50;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
padding: 0.3rem 0;
|
||||
background: transparent;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--code-bg);
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
.prompt-bar .prompt-sigil {
|
||||
color: var(--green);
|
||||
|
|
|
|||
|
|
@ -287,7 +287,8 @@ function renderAll() {
|
|||
}
|
||||
transcriptEl.appendChild(block);
|
||||
}
|
||||
replStream.scrollTop = replStream.scrollHeight;
|
||||
// Body owns the scroll now; jump it to the latest entry.
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "instant" });
|
||||
}
|
||||
|
||||
// ─── Input handling ─────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
/* lumbda repl — grid-only layout. Inherits palette + base from style.css. */
|
||||
|
||||
body.repl {
|
||||
display: grid;
|
||||
/* header (fixed) scrolling-stream (everything else) */
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
/* Body scrolls naturally. The prompt-bar is position: fixed so it
|
||||
* stays glued to the viewport bottom; the transcript reserves bottom
|
||||
* padding equal to the prompt-bar height so its last line isn't hidden
|
||||
* underneath. */
|
||||
min-height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ─── Lock screen overlay ──────────────────────────────────────── */
|
||||
|
|
@ -176,12 +177,13 @@ body.repl {
|
|||
* fresh sessions show the prompt up near the top and it drifts down
|
||||
* with each new entry. */
|
||||
.repl-stream {
|
||||
overflow: auto;
|
||||
padding: 0.5rem 1rem 0.8rem;
|
||||
padding: 0.5rem 1rem 0;
|
||||
background: var(--code-bg);
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
align-content: start;
|
||||
/* Leave room for the fixed prompt bar at the viewport bottom. */
|
||||
padding-bottom: calc(3.6rem + env(safe-area-inset-bottom, 0));
|
||||
}
|
||||
.transcript {
|
||||
font-family: var(--mono);
|
||||
|
|
@ -232,12 +234,16 @@ body.repl {
|
|||
/* ─── Prompt bar ───────────────────────────────────────────────── */
|
||||
|
||||
.prompt-bar {
|
||||
position: fixed;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
z-index: 50;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
padding: 0.3rem 0;
|
||||
background: transparent;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--code-bg);
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
.prompt-bar .prompt-sigil {
|
||||
color: var(--green);
|
||||
|
|
|
|||
|
|
@ -287,7 +287,8 @@ function renderAll() {
|
|||
}
|
||||
transcriptEl.appendChild(block);
|
||||
}
|
||||
replStream.scrollTop = replStream.scrollHeight;
|
||||
// Body owns the scroll now; jump it to the latest entry.
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "instant" });
|
||||
}
|
||||
|
||||
// ─── Input handling ─────────────────────────────────────────────────
|
||||
|
|
|
|||
131
wasm/tests/parity-corpus.mjs
Normal file
131
wasm/tests/parity-corpus.mjs
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
// 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" },
|
||||
|
||||
// ─── 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".
|
||||
export const KNOWN_DIVERGE = {
|
||||
// Asm-WASM has 31-bit fixnum num/den rationals; no bignums yet.
|
||||
"expt-2-100": ["asm"],
|
||||
"expt-3-50": ["asm"],
|
||||
"big-arith": ["asm"],
|
||||
};
|
||||
134
wasm/tests/parity-cross-tier.mjs
Normal file
134
wasm/tests/parity-cross-tier.mjs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
// wasm/tests/parity-cross-tier.mjs
|
||||
// For every CORPUS entry, run the source on every tier (native python,
|
||||
// c-wasm, asm-wasm) and report mismatches against the expected output.
|
||||
// Known divergences (KNOWN_DIVERGE) are tolerated but printed in yellow
|
||||
// so the gap is visible without breaking the build.
|
||||
//
|
||||
// Python tier runs via the host's python3 lumbda.py because the Pyodide
|
||||
// loader needs a browser context. That keeps this test fast (no 10MB
|
||||
// CDN download) while still pinning behavior to the reference tier.
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { CORPUS, KNOWN_DIVERGE } from "./parity-corpus.mjs";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const dist = path.resolve(here, "..", "dist");
|
||||
const repoRoot = path.resolve(here, "..", "..");
|
||||
|
||||
const state = { ok: 0, fail: 0, knownDiverge: 0, failures: [] };
|
||||
|
||||
function check(tag, tier, expected, got, isKnown) {
|
||||
const passed = got === expected;
|
||||
if (passed) state.ok++;
|
||||
else if (isKnown) state.knownDiverge++;
|
||||
else { state.fail++; state.failures.push({ tag, tier, expected, got }); }
|
||||
const status = passed
|
||||
? "✓"
|
||||
: isKnown
|
||||
? "~" // known divergence
|
||||
: "✗";
|
||||
const label = `${status} ${tier.padEnd(6)} ${tag}`;
|
||||
if (!passed) {
|
||||
const detail = `expected ${JSON.stringify(expected)} · got ${JSON.stringify(got)}`;
|
||||
console.log(` ${label} — ${detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap each test source so the result is printed via display (uniform
|
||||
// across tiers — no write quoting, no "is this the last expression"
|
||||
// behavior to depend on). The source may contain multiple top-level
|
||||
// forms (e.g. defines), so we put the final expression in a (begin ...)
|
||||
// and display its result.
|
||||
function wrap(src) {
|
||||
// The source's last sub-expression is what we want to display.
|
||||
// We rely on the tiers each evaluating the lot and `(display ...)`
|
||||
// emitting display semantics on the value.
|
||||
return `(display (begin ${src})) (newline)`;
|
||||
}
|
||||
|
||||
// ─── tier adapters ─────────────────────────────────────────────────
|
||||
function nativePython(src) {
|
||||
const tmp = `/tmp/parity-${process.pid}.lsp`;
|
||||
fs.writeFileSync(tmp, wrap(src) + "\n");
|
||||
try {
|
||||
return execFileSync("python3", [path.join(repoRoot, "lumbda.py"), tmp], {
|
||||
encoding: "utf8", timeout: 30000,
|
||||
}).trim();
|
||||
} finally { try { fs.unlinkSync(tmp); } catch {} }
|
||||
}
|
||||
|
||||
async function withAsmTier() {
|
||||
const wasmBytes = fs.readFileSync(path.join(dist, "asm", "lumbda-asm.wasm"));
|
||||
const importObj = { env: { bend_call() { return 0; } } };
|
||||
const { instance } = await WebAssembly.instantiate(wasmBytes, importObj);
|
||||
const exp = instance.exports;
|
||||
exp.lumbda_init();
|
||||
return (src) => {
|
||||
const bytes = new TextEncoder().encode(wrap(src));
|
||||
new Uint8Array(exp.memory.buffer).set(bytes, exp.lumbda_source_ptr());
|
||||
try { exp.lumbda_eval(bytes.length); }
|
||||
catch (e) { return `THREW: ${e.message}`; }
|
||||
return new TextDecoder().decode(
|
||||
new Uint8Array(exp.memory.buffer, exp.lumbda_output_ptr(), exp.lumbda_output_len())
|
||||
).trim();
|
||||
};
|
||||
}
|
||||
|
||||
async function withCTier() {
|
||||
const factoryURL = "file://" + path.join(dist, "c", "lumbda-c.js");
|
||||
const createLumbdaC = (await import(factoryURL)).default;
|
||||
let out = [];
|
||||
const m = await createLumbdaC({
|
||||
locateFile: (p) => path.join(dist, "c", p),
|
||||
print: (line) => out.push(line),
|
||||
printErr: () => {},
|
||||
});
|
||||
m.cwrap("lumbda_wasm_init", null, [])();
|
||||
const _eval = m.cwrap("lumbda_wasm_eval", "number", ["string"]);
|
||||
const _free = m.cwrap("lumbda_wasm_free_result", null, ["number"]);
|
||||
return (src) => {
|
||||
out = [];
|
||||
const r = _eval(wrap(src));
|
||||
if (r) { const msg = m.UTF8ToString(r); _free(r); return msg; }
|
||||
return out.join("\n").trim();
|
||||
};
|
||||
}
|
||||
|
||||
(async () => {
|
||||
console.log("─".repeat(70));
|
||||
console.log("Cross-tier parity probe — python (ref) vs c-wasm vs asm-wasm");
|
||||
console.log("─".repeat(70));
|
||||
|
||||
const runAsm = await withAsmTier();
|
||||
const runC = await withCTier();
|
||||
|
||||
for (const entry of CORPUS) {
|
||||
const knownOn = KNOWN_DIVERGE[entry.tag] || [];
|
||||
const isAsmKnown = knownOn.includes("asm");
|
||||
const isCKnown = knownOn.includes("c");
|
||||
const isPyKnown = knownOn.includes("python");
|
||||
|
||||
const py = nativePython(entry.src);
|
||||
const cw = runC(entry.src);
|
||||
const aw = runAsm(entry.src);
|
||||
|
||||
check(entry.tag, "py-ref", entry.expected, py, isPyKnown);
|
||||
check(entry.tag, "c-wasm", entry.expected, cw, isCKnown);
|
||||
check(entry.tag, "asm", entry.expected, aw, isAsmKnown);
|
||||
}
|
||||
|
||||
console.log("─".repeat(70));
|
||||
console.log(` passing : ${state.ok}`);
|
||||
console.log(` known diverge: ${state.knownDiverge} (tracked in KNOWN_DIVERGE)`);
|
||||
console.log(` fail : ${state.fail}`);
|
||||
if (state.fail > 0) {
|
||||
console.log("\nUnknown divergences (fix or add to KNOWN_DIVERGE):");
|
||||
for (const f of state.failures) {
|
||||
console.log(` ${f.tier} ${f.tag}: expected ${JSON.stringify(f.expected)} got ${JSON.stringify(f.got)}`);
|
||||
}
|
||||
}
|
||||
process.exit(state.fail ? 1 : 0);
|
||||
})();
|
||||
Binary file not shown.
|
|
@ -3438,7 +3438,11 @@ def make_global_env():
|
|||
return n
|
||||
d(S('/'), _div)
|
||||
d(S('quotient'), lambda a, _: (lambda x, y: -(abs(int(x)) // abs(int(y))) if (x < 0) != (y < 0) else abs(int(x)) // abs(int(y)))(_num(a[0]), _num(a[1])))
|
||||
d(S('remainder'), lambda a, _: int(_num(a[0])) % int(_num(a[1])) * (1 if _num(a[0]) >= 0 else -1))
|
||||
# R7RS: remainder has the sign of the dividend; uses truncated division.
|
||||
# The previous impl did `signed_a % signed_b * sign(a)` which double-counted
|
||||
# the sign of a (Python's % floors, so `-17 % 5 == 3`) and gave -3 for
|
||||
# (-17, 5) instead of the correct -2. Use abs() on both sides, then re-sign.
|
||||
d(S('remainder'), lambda a, _: (abs(int(_num(a[0]))) % abs(int(_num(a[1])))) * (1 if _num(a[0]) >= 0 else -1))
|
||||
d(S('modulo'), lambda a, _: int(_num(a[0])) % int(_num(a[1])))
|
||||
d(S('expt'), lambda a, _: _num(a[0]) ** _num(a[1]))
|
||||
d(S('abs'), lambda a, _: abs(_num(a[0])))
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -3438,7 +3438,11 @@ def make_global_env():
|
|||
return n
|
||||
d(S('/'), _div)
|
||||
d(S('quotient'), lambda a, _: (lambda x, y: -(abs(int(x)) // abs(int(y))) if (x < 0) != (y < 0) else abs(int(x)) // abs(int(y)))(_num(a[0]), _num(a[1])))
|
||||
d(S('remainder'), lambda a, _: int(_num(a[0])) % int(_num(a[1])) * (1 if _num(a[0]) >= 0 else -1))
|
||||
# R7RS: remainder has the sign of the dividend; uses truncated division.
|
||||
# The previous impl did `signed_a % signed_b * sign(a)` which double-counted
|
||||
# the sign of a (Python's % floors, so `-17 % 5 == 3`) and gave -3 for
|
||||
# (-17, 5) instead of the correct -2. Use abs() on both sides, then re-sign.
|
||||
d(S('remainder'), lambda a, _: (abs(int(_num(a[0]))) % abs(int(_num(a[1])))) * (1 if _num(a[0]) >= 0 else -1))
|
||||
d(S('modulo'), lambda a, _: int(_num(a[0])) % int(_num(a[1])))
|
||||
d(S('expt'), lambda a, _: _num(a[0]) ** _num(a[1]))
|
||||
d(S('abs'), lambda a, _: abs(_num(a[0])))
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
/* lumbda repl — grid-only layout. Inherits palette + base from style.css. */
|
||||
|
||||
body.repl {
|
||||
display: grid;
|
||||
/* header (fixed) scrolling-stream (everything else) */
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
/* Body scrolls naturally. The prompt-bar is position: fixed so it
|
||||
* stays glued to the viewport bottom; the transcript reserves bottom
|
||||
* padding equal to the prompt-bar height so its last line isn't hidden
|
||||
* underneath. */
|
||||
min-height: 100vh;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* ─── Lock screen overlay ──────────────────────────────────────── */
|
||||
|
|
@ -176,12 +177,13 @@ body.repl {
|
|||
* fresh sessions show the prompt up near the top and it drifts down
|
||||
* with each new entry. */
|
||||
.repl-stream {
|
||||
overflow: auto;
|
||||
padding: 0.5rem 1rem 0.8rem;
|
||||
padding: 0.5rem 1rem 0;
|
||||
background: var(--code-bg);
|
||||
display: grid;
|
||||
grid-template-rows: auto auto auto;
|
||||
align-content: start;
|
||||
/* Leave room for the fixed prompt bar at the viewport bottom. */
|
||||
padding-bottom: calc(3.6rem + env(safe-area-inset-bottom, 0));
|
||||
}
|
||||
.transcript {
|
||||
font-family: var(--mono);
|
||||
|
|
@ -232,12 +234,16 @@ body.repl {
|
|||
/* ─── Prompt bar ───────────────────────────────────────────────── */
|
||||
|
||||
.prompt-bar {
|
||||
position: fixed;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
z-index: 50;
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr auto auto;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
padding: 0.3rem 0;
|
||||
background: transparent;
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--code-bg);
|
||||
border-top: 1px solid var(--rule);
|
||||
}
|
||||
.prompt-bar .prompt-sigil {
|
||||
color: var(--green);
|
||||
|
|
|
|||
|
|
@ -287,7 +287,8 @@ function renderAll() {
|
|||
}
|
||||
transcriptEl.appendChild(block);
|
||||
}
|
||||
replStream.scrollTop = replStream.scrollHeight;
|
||||
// Body owns the scroll now; jump it to the latest entry.
|
||||
window.scrollTo({ top: document.documentElement.scrollHeight, behavior: "instant" });
|
||||
}
|
||||
|
||||
// ─── Input handling ─────────────────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue