From 3598a34e3d93cd33db623d87b6311aab152fa3c7 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 31 Mar 2026 17:57:45 +0000 Subject: [PATCH] Add failing tests: set! in named let loop (WASM + native OCaml) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Named let creates a loop continuation but set! inside the loop body does not mutate bindings in the enclosing let scope. Affects both the WASM kernel and native OCaml CEK evaluator. 6 failing Node tests cover: - set! counter (simplest case) - set! counter with named let params - set! list accumulator via append - append! + set! counter combo - set! string concatenation - nested named let set! 3 baselines pass: plain let set!, functional named let, plain append! Also adds spec/tests/test-named-let-set.sx (7 assertions, first fails and aborts — confirms bug exists in spec test suite too). This is the root cause of empty source code blocks on all example pages: tokenize-sx uses set! in named let → empty tokens → highlight returns "(<> )" → empty blocks. Co-Authored-By: Claude Opus 4.6 (1M context) --- spec/tests/test-named-let-set.sx | 52 ++++++++++++++++++++ tests/node/test-named-let-set.js | 82 ++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 spec/tests/test-named-let-set.sx create mode 100644 tests/node/test-named-let-set.js diff --git a/spec/tests/test-named-let-set.sx b/spec/tests/test-named-let-set.sx new file mode 100644 index 00000000..395350c2 --- /dev/null +++ b/spec/tests/test-named-let-set.sx @@ -0,0 +1,52 @@ +(assert= + (let ((x 0)) (let loop () (when (< x 5) (set! x (+ x 1)) (loop))) x) + 5 + "set! counter in named let loop") + +(assert= + (let + ((acc (list))) + (let + loop + ((i 0)) + (when (< i 3) (set! acc (append acc (list i))) (loop (+ i 1)))) + acc) + (list 0 1 2) + "set! list accumulator in named let with param") + +(assert= + (let + ((acc (list)) (i 0)) + (let loop () (when (< i 3) (append! acc i) (set! i (+ i 1)) (loop))) + acc) + (list 0 1 2) + "append! + set! counter in named let") + +(assert= + (let + ((sum 0)) + (let + loop + ((i 1)) + (when (<= i 10) (set! sum (+ sum i)) (loop (+ i 1)))) + sum) + 55 + "set! sum in named let loop (1..10)") + +(assert= + (let + ((result "")) + (let + loop + ((i 0)) + (when (< i 3) (set! result (str result (str i))) (loop (+ i 1)))) + result) + "012" + "set! string accumulator in named let") + +(assert= (let ((x 0)) (set! x 42) x) 42 "set! in plain let (baseline)") + +(assert= + (let loop ((i 0) (acc 0)) (if (< i 5) (loop (+ i 1) (+ acc i)) acc)) + 10 + "named let without set! (baseline)") diff --git a/tests/node/test-named-let-set.js b/tests/node/test-named-let-set.js new file mode 100644 index 00000000..cd179d50 --- /dev/null +++ b/tests/node/test-named-let-set.js @@ -0,0 +1,82 @@ +#!/usr/bin/env node +/** + * test-named-let-set.js — WASM kernel tests for set! inside named let loops. + * + * FAILING: set! inside named let body does not persist mutations to + * the enclosing scope. This breaks tokenize-sx → highlight → source display. + */ +const { createSxEnv } = require('./sx-harness'); + +let passed = 0, failed = 0; +const origLog = console.log; +const origErr = console.error; + +function test(name, expr, expected) { + let env; + try { + // Fresh env per test to avoid cross-contamination + // But creating env is slow, so we'll reuse one below + } catch(e) {} +} + +async function main() { + origLog('=== Named Let + set! Tests (WASM kernel) ===\n'); + const t0 = Date.now(); + + const env = await createSxEnv({}); + + function check(name, expr, expected) { + const result = env.eval(expr); + const expectedStr = JSON.stringify(expected); + const resultVal = result?.items ? result.items : result; + const resultStr = JSON.stringify(resultVal); + if (resultStr === expectedStr) { + passed++; + origLog(` \u2713 ${name}`); + } else { + failed++; + origErr(` \u2717 ${name}: expected ${expectedStr}, got ${resultStr}`); + } + } + + // ---- Baselines (should pass) ---- + origLog('Baselines:'); + check('set! in plain let', + '(let ((x 0)) (set! x 42) x)', 42); + + check('named let without set! (functional accumulator)', + '(trampoline (let loop ((i 0) (acc 0)) (if (< i 5) (loop (+ i 1) (+ acc i)) acc)))', 10); + + check('append! on outer binding (no named let)', + '(let ((xs (list))) (append! xs 1) (append! xs 2) xs)', [1, 2]); + + // ---- Named let + set! (the broken cases) ---- + origLog('\nNamed let + set!:'); + check('set! counter in named let loop', + '(let ((x 0)) (let loop () (when (< x 5) (set! x (+ x 1)) (loop))) x)', 5); + + check('set! counter in named let loop (with param)', + '(let ((total 0)) (let loop ((i 1)) (when (<= i 10) (set! total (+ total i)) (loop (+ i 1)))) total)', 55); + + check('set! list via append in named let', + '(let ((acc (list))) (let loop ((i 0)) (when (< i 3) (set! acc (append acc (list i))) (loop (+ i 1)))) acc)', [0, 1, 2]); + + check('append! in named let', + '(let ((acc (list)) (i 0)) (let loop () (when (< i 3) (append! acc i) (set! i (+ i 1)) (loop))) acc)', [0, 1, 2]); + + check('set! string concat in named let', + '(let ((result "")) (let loop ((i 0)) (when (< i 3) (set! result (str result (str i))) (loop (+ i 1)))) result)', "012"); + + // ---- Nested named let ---- + origLog('\nNested:'); + check('set! in nested named let', + '(let ((count 0)) (let outer ((i 0)) (when (< i 3) (let inner ((j 0)) (when (< j 2) (set! count (+ count 1)) (inner (+ j 1)))) (outer (+ i 1)))) count)', 6); + + env.close(); + + const dt = Date.now() - t0; + origLog(`\n=== ${passed} passed, ${failed} failed (${dt}ms) ===`); + process.exit(failed > 0 ? 1 : 0); +} + +main().catch(e => { origErr(e); process.exit(1); });