From 9dd27a328b7b14acd2439cf2541ac7bd5dcdc7ff Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 31 Mar 2026 17:53:15 +0000 Subject: [PATCH] Add failing tests: highlight returns empty due to set! in named let Root cause: set! inside named let loop does not persist mutations to the outer scope in the WASM kernel. tokenize-sx uses this pattern (set! acc (append acc ...)) inside (let loop () ...) and gets empty tokens. This makes highlight return "(<> )" for all inputs, causing empty source code blocks on every reactive island example page. 6 failing tests document the bug at each level: - set! in named let loop (root cause) - tokenize-sx (empty tokens) - highlight (empty output) - component-source + highlight (end-to-end) Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/node/test-highlight.js | 100 +++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/node/test-highlight.js diff --git a/tests/node/test-highlight.js b/tests/node/test-highlight.js new file mode 100644 index 00000000..10a75752 --- /dev/null +++ b/tests/node/test-highlight.js @@ -0,0 +1,100 @@ +#!/usr/bin/env node +/** + * test-highlight.js — Tests for the SX syntax highlighter. + * + * FAILING: highlight returns empty because set! inside named let loop + * does not persist mutations to the outer scope in the WASM kernel. + */ +const { createSxEnv } = require('./sx-harness'); +const fs = require('fs'); +const path = require('path'); + +const HIGHLIGHT_SRC = path.resolve(__dirname, '../../lib/highlight.sx'); + +let passed = 0, failed = 0; +const origLog = console.log; +const origErr = console.error; + +function assert(name, cond, detail) { + if (cond) { passed++; origLog(` \u2713 ${name}`); } + else { failed++; origErr(` \u2717 ${name}${detail ? ' — ' + detail : ''}`); } +} + +async function main() { + origLog('=== Highlight Tests ===\n'); + const t0 = Date.now(); + + // ---- Root cause: set! in named let loop ---- + origLog('1. set! in named let loop'); + { + const env = await createSxEnv({}); + // This is the exact pattern tokenize-sx uses + const result = env.eval( + '(let ((acc (list)) (i 0)) (let loop () (when (< i 3) (set! acc (append acc (list i))) (set! i (+ i 1)) (loop))) acc)' + ); + assert('named let set! persists', result?.items?.length === 3, + `expected 3 items, got ${result?.items?.length}`); + env.close(); + } + + // ---- Tokenizer ---- + origLog('2. tokenize-sx'); + { + const env = await createSxEnv({}); + env.load(fs.readFileSync(HIGHLIGHT_SRC, 'utf8')); + + const t1 = env.eval('(tokenize-sx "(+ 1 2)")'); + assert('tokenize-sx non-empty', t1?.items?.length > 0, + `expected tokens, got ${t1?.items?.length}`); + + const t2 = env.eval('(tokenize-sx "(define x 42)")'); + assert('tokenize define', t2?.items?.length > 0, + `expected tokens, got ${t2?.items?.length}`); + + env.close(); + } + + // ---- Highlight output ---- + origLog('3. highlight'); + { + const env = await createSxEnv({}); + env.load(fs.readFileSync(HIGHLIGHT_SRC, 'utf8')); + + const h1 = env.eval('(highlight "(+ 1 2)" "lisp")'); + assert('highlight non-empty', h1 !== '(<> )' && h1?.length > 5, + `got "${h1}"`); + + const h2 = env.eval('(highlight "(defisland ~counter () (div))" "lisp")'); + assert('highlight defisland', h2?.includes?.('defisland'), + `got "${h2?.substring(0, 100)}"`); + + env.close(); + } + + // ---- End-to-end: component-source + highlight ---- + origLog('4. component-source + highlight'); + { + const env = await createSxEnv({}); + env.load(fs.readFileSync(HIGHLIGHT_SRC, 'utf8')); + env.load(fs.readFileSync(path.resolve(__dirname, '../../sx/sx/reactive-islands/index.sx'), 'utf8')); + + // component-source needs pretty-print (server-only), test with manual source + const source = env.eval('(let ((c ~reactive-islands/index/demo-counter)) (str "(defisland ~" (component-name c) " " (component-params c) " " (component-body c) ")"))'); + assert('component source string', typeof source === 'string' && source.length > 20, + `got ${typeof source}: "${source?.substring?.(0, 50)}"`); + + if (typeof source === 'string' && source.length > 20) { + const highlighted = env.eval(`(highlight "${source.replace(/"/g, '\\"').replace(/\n/g, '\\n')}" "lisp")`); + assert('highlighted source non-empty', highlighted !== '(<> )' && highlighted?.length > 10, + `got "${highlighted?.substring?.(0, 100)}"`); + } + + 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); });