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) <noreply@anthropic.com>
This commit is contained in:
100
tests/node/test-highlight.js
Normal file
100
tests/node/test-highlight.js
Normal file
@@ -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); });
|
||||
Reference in New Issue
Block a user