Browser kernel: - Add `parse` native fn (matches server: unwrap single, list for multiple) - Restore env==global_env guard on _env_bind_hook (let bindings must not leak to _vm_globals — caused JIT CSSX "Not callable: nil" errors) - Add _env_bind_hook call in env_set_id so set! mutations sync to VM globals - Fire _vm_global_set_hook from OP_DEFINE so VM defines sync back to CEK env CEK evaluator: - Replace recursive cek_run with iterative while loop using sx_truthy (previous attempt used strict Bool true matching, broke in wasm_of_ocaml) - Remove dead cek_run_iterative function Web modules: - Remove find-matching-route and parse-route-pattern stubs from boot-helpers.sx that shadowed real implementations from router.sx - Sync boot-helpers.sx to dist/static dirs for bytecode compilation Platform (sx-platform.js): - Set data-sx-ready attribute after boot completes (was only in boot-init which sx-platform.js doesn't call — it steps through boot manually) - Add document-level click delegation for a[sx-get] links as workaround for bytecoded bind-event not attaching per-element listeners (VM closure issue under investigation — bind-event runs but dom-add-listener calls don't result in addEventListener) Tests: - New test_kernel.js: 24 tests covering env sync, parse, route matching, host FFI/preventDefault, deep recursion - New navigation test: "sx-get link fetches SX not HTML and preserves layout" (currently catches layout breakage after SPA swap — known issue) Known remaining issues: - JIT CSSX failures: closure-captured variables resolve to nil in VM bytecode - SPA content swap via execute-request breaks page layout - Bytecoded bind-event doesn't attach per-element addEventListener (root cause unknown — when listen-target guard appears to block despite element being valid) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
245 lines
8.0 KiB
JavaScript
245 lines
8.0 KiB
JavaScript
#!/usr/bin/env node
|
|
// WASM kernel integration tests: env sync, globals, pages parsing, preventDefault
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
require(path.join(__dirname, '../_build/default/browser/sx_browser.bc.js'));
|
|
|
|
const K = globalThis.SxKernel;
|
|
|
|
// Load compiler for evalVM support
|
|
const compilerFiles = ['lib/bytecode.sx', 'lib/compiler.sx', 'lib/vm.sx'];
|
|
for (const f of compilerFiles) {
|
|
K.load(fs.readFileSync(path.join(__dirname, '../../..', f), 'utf8'));
|
|
}
|
|
|
|
let passed = 0, failed = 0;
|
|
function test(name, fn) {
|
|
try {
|
|
const result = fn();
|
|
if (result === true) {
|
|
passed++;
|
|
} else {
|
|
console.log(` FAIL: ${name} — got ${JSON.stringify(result)}`);
|
|
failed++;
|
|
}
|
|
} catch (e) {
|
|
console.log(` FAIL: ${name} — ${e.message || e}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
// ================================================================
|
|
// 1. Env binding / globals sync
|
|
// ================================================================
|
|
|
|
test('define at top level visible to VM', () => {
|
|
K.eval('(define _test-toplevel-1 42)');
|
|
return K.evalVM('_test-toplevel-1') === 42;
|
|
});
|
|
|
|
test('define in begin visible to VM', () => {
|
|
K.eval('(begin (define _test-begin-1 99))');
|
|
return K.evalVM('_test-begin-1') === 99;
|
|
});
|
|
|
|
test('set! on global syncs to VM', () => {
|
|
K.eval('(define _test-set-g 1)');
|
|
K.eval('(set! _test-set-g 55)');
|
|
return K.evalVM('_test-set-g') === 55;
|
|
});
|
|
|
|
test('VM define syncs back to CEK', () => {
|
|
K.evalVM('(define _test-vm-def 777)');
|
|
return K.eval('_test-vm-def') === 777;
|
|
});
|
|
|
|
test('CEK and VM see same value after multiple updates', () => {
|
|
K.eval('(define _test-ping 0)');
|
|
K.eval('(set! _test-ping 1)');
|
|
K.evalVM('(set! _test-ping 2)');
|
|
const cek = K.eval('_test-ping');
|
|
const vm = K.evalVM('_test-ping');
|
|
return cek === 2 && vm === 2;
|
|
});
|
|
|
|
test('lambda defined at top level callable from VM', () => {
|
|
K.eval('(define _test-top-fn (fn (x) (* x 10)))');
|
|
return K.evalVM('(_test-top-fn 3)') === 30;
|
|
});
|
|
|
|
// ================================================================
|
|
// 2. Parse function (pages-sx format)
|
|
// ================================================================
|
|
|
|
test('parse single dict', () => {
|
|
const r = K.eval('(get (parse "{:name \\"home\\" :path \\"/\\"}") "name")');
|
|
return r === 'home';
|
|
});
|
|
|
|
test('parse multiple dicts returns list', () => {
|
|
const r = K.eval('(len (parse "{:a 1}\\n{:b 2}\\n{:c 3}"))');
|
|
return r === 3;
|
|
});
|
|
|
|
test('parse single expr unwraps', () => {
|
|
return K.eval('(type-of (parse "42"))') === 'number';
|
|
});
|
|
|
|
test('parse multiple exprs returns list', () => {
|
|
return K.eval('(type-of (parse "1 2 3"))') === 'list';
|
|
});
|
|
|
|
test('parse dict with content string', () => {
|
|
const r = K.eval('(get (parse "{:name \\"test\\" :content \\"(div \\\\\\\"hello\\\\\\\")\\" :has-data false}") "content")');
|
|
return typeof r === 'string' && r.includes('div');
|
|
});
|
|
|
|
test('parse dict with path param pattern', () => {
|
|
const r = K.eval('(get (parse "{:path \\"/docs/<slug>\\"}") "path")');
|
|
return r === '/docs/<slug>';
|
|
});
|
|
|
|
// ================================================================
|
|
// 3. Route pattern parsing (requires router.sx loaded)
|
|
// ================================================================
|
|
|
|
// Load router module
|
|
const routerSrc = fs.readFileSync(path.join(__dirname, '../../../web/router.sx'), 'utf8');
|
|
K.load(routerSrc);
|
|
|
|
test('parse-route-pattern splits static path', () => {
|
|
const r = K.eval('(len (parse-route-pattern "/docs/intro"))');
|
|
return r === 2;
|
|
});
|
|
|
|
test('parse-route-pattern detects param segments', () => {
|
|
const r = K.eval('(get (nth (parse-route-pattern "/docs/<slug>") 1) "type")');
|
|
return r === 'param';
|
|
});
|
|
|
|
test('parse-route-pattern detects literal segments', () => {
|
|
const r = K.eval('(get (first (parse-route-pattern "/docs/<slug>")) "type")');
|
|
return r === 'literal';
|
|
});
|
|
|
|
test('find-matching-route matches static path', () => {
|
|
K.eval('(define _test-routes (list (merge {:name "home" :path "/"} {:parsed (parse-route-pattern "/")})))');
|
|
const r = K.eval('(get (find-matching-route "/" _test-routes) "name")');
|
|
return r === 'home';
|
|
});
|
|
|
|
test('find-matching-route matches param path', () => {
|
|
K.eval('(define _test-routes2 (list (merge {:name "doc" :path "/docs/<slug>"} {:parsed (parse-route-pattern "/docs/<slug>")})))');
|
|
const r = K.eval('(get (find-matching-route "/docs/intro" _test-routes2) "name")');
|
|
return r === 'doc';
|
|
});
|
|
|
|
test('find-matching-route returns nil for no match', () => {
|
|
return K.eval('(nil? (find-matching-route "/unknown" _test-routes))') === true;
|
|
});
|
|
|
|
// ================================================================
|
|
// 4. Click handler preventDefault pattern
|
|
// ================================================================
|
|
|
|
// Register host FFI primitives (normally done by sx-platform.js)
|
|
K.registerNative("host-global", function(args) {
|
|
var name = args[0];
|
|
return (typeof name === 'string') ? globalThis[name] : undefined;
|
|
});
|
|
K.registerNative("host-get", function(args) {
|
|
var obj = args[0], key = args[1];
|
|
if (obj == null) return null;
|
|
var v = obj[key];
|
|
return v === undefined ? null : v;
|
|
});
|
|
K.registerNative("host-call", function(args) {
|
|
var obj = args[0], method = args[1];
|
|
var callArgs = args.slice(2);
|
|
if (obj == null || typeof obj[method] !== 'function') return null;
|
|
try { return obj[method].apply(obj, callArgs); } catch(e) { return null; }
|
|
});
|
|
K.registerNative("host-set!", function(args) {
|
|
var obj = args[0], key = args[1], val = args[2];
|
|
if (obj != null) obj[key] = val;
|
|
return null;
|
|
});
|
|
|
|
test('host-call preventDefault on mock event', () => {
|
|
let prevented = false;
|
|
globalThis._testMockEvent = {
|
|
preventDefault: () => { prevented = true; },
|
|
type: 'click',
|
|
target: { tagName: 'A', getAttribute: () => '/test' }
|
|
};
|
|
K.eval('(host-call (host-global "_testMockEvent") "preventDefault")');
|
|
delete globalThis._testMockEvent;
|
|
return prevented === true;
|
|
});
|
|
|
|
test('host-get reads property from JS object', () => {
|
|
globalThis._testObj = { foo: 42 };
|
|
const r = K.eval('(host-get (host-global "_testObj") "foo")');
|
|
delete globalThis._testObj;
|
|
return r === 42;
|
|
});
|
|
|
|
test('host-set! writes property on JS object', () => {
|
|
globalThis._testObj2 = { val: 0 };
|
|
K.eval('(host-set! (host-global "_testObj2") "val" 99)');
|
|
const r = globalThis._testObj2.val;
|
|
delete globalThis._testObj2;
|
|
return r === 99;
|
|
});
|
|
|
|
test('click handler pattern: check target, prevent, navigate', () => {
|
|
let prevented = false;
|
|
let navigated = null;
|
|
globalThis._testClickEvent = {
|
|
preventDefault: () => { prevented = true; },
|
|
type: 'click',
|
|
target: { tagName: 'A', href: '/about' }
|
|
};
|
|
globalThis._testNavigate = (url) => { navigated = url; };
|
|
|
|
K.eval(`
|
|
(let ((e (host-global "_testClickEvent")))
|
|
(let ((tag (host-get (host-get e "target") "tagName")))
|
|
(when (= tag "A")
|
|
(host-call e "preventDefault")
|
|
(host-call (host-global "_testNavigate") "call" nil
|
|
(host-get (host-get e "target") "href")))))
|
|
`);
|
|
|
|
delete globalThis._testClickEvent;
|
|
delete globalThis._testNavigate;
|
|
return prevented === true && navigated === '/about';
|
|
});
|
|
|
|
// ================================================================
|
|
// 5. Iterative cek_run — deep evaluation without stack overflow
|
|
// ================================================================
|
|
|
|
test('deep recursion via foldl (100 iterations)', () => {
|
|
const r = K.eval('(reduce + 0 (map (fn (x) x) (list ' +
|
|
Array.from({length: 100}, (_, i) => i + 1).join(' ') + ')))');
|
|
return r === 5050;
|
|
});
|
|
|
|
test('deeply nested let bindings', () => {
|
|
// Build (let ((x0 0)) (let ((x1 (+ x0 1))) ... (let ((xN (+ xN-1 1))) xN)))
|
|
let expr = 'x49';
|
|
for (let i = 49; i >= 0; i--) {
|
|
const prev = i === 0 ? '0' : `(+ x${i-1} 1)`;
|
|
expr = `(let ((x${i} ${prev})) ${expr})`;
|
|
}
|
|
return K.eval(expr) === 49;
|
|
});
|
|
|
|
// ================================================================
|
|
// Results
|
|
// ================================================================
|
|
|
|
console.log(`\n${passed} passed, ${failed} failed`);
|
|
process.exit(failed > 0 ? 1 : 0);
|