#!/usr/bin/env node // run_wasm_corpus.js — W14/F2: run spec-test files through the SHIPPED // browser kernel (sx_browser.bc.wasm.js), headless in Node. // // The review (conformance.md F-2) found no runner feeds spec/tests through // the shipped browser artifact — F-1/F-3 host divergences existed // undetected precisely because of that. This runs ONE test file per // invocation (process isolation: a hanging file is killed by the driver's // timeout without taking down the sweep) and prints a parseable summary: // CORPUS-RESULT pass= fail= status=ok|load-error // // Usage: node hosts/ocaml/browser/run_wasm_corpus.js spec/tests/test-eval.sx // Driver: scripts/test-wasm-corpus.sh (sweeps the corpus, applies skips). // // Boot stubs and module preload mirror test_wasm_native.js (the blessed // boot path for the shipped kernel). const fs = require('fs'); const path = require('path'); const PROJECT_ROOT = path.resolve(__dirname, '../../..'); const WASM_DIR = path.join(PROJECT_ROOT, 'shared/static/wasm'); const target = process.argv[2]; if (!target) { console.error('usage: run_wasm_corpus.js '); process.exit(2); } // --- DOM stubs (as test_wasm_native.js) --- function makeElement(tag) { const el = { tagName: tag, _attrs: {}, _children: [], style: {}, childNodes: [], children: [], textContent: '', nodeType: 1, setAttribute(k, v) { el._attrs[k] = String(v); }, getAttribute(k) { return el._attrs[k] || null; }, removeAttribute(k) { delete el._attrs[k]; }, appendChild(c) { el._children.push(c); el.childNodes.push(c); el.children.push(c); return c; }, insertBefore(c) { el._children.push(c); el.childNodes.push(c); el.children.push(c); return c; }, removeChild(c) { return c; }, replaceChild(n) { return n; }, cloneNode() { return makeElement(tag); }, addEventListener() {}, removeEventListener() {}, dispatchEvent() {}, get innerHTML() { return el._children.map(c => { if (c._isText) return c.textContent || ''; if (c._isComment) return ''; return c.outerHTML || ''; }).join(''); }, set innerHTML(v) { el._children = []; el.childNodes = []; el.children = []; }, get outerHTML() { let s = '<' + tag; for (const k of Object.keys(el._attrs).sort()) s += ` ${k}="${el._attrs[k]}"`; s += '>'; if (['br', 'hr', 'img', 'input', 'meta', 'link'].includes(tag)) return s; return s + el.innerHTML + ''; }, dataset: new Proxy({}, { get(_, k) { return el._attrs['data-' + k.replace(/[A-Z]/g, c => '-' + c.toLowerCase())]; }, set(_, k, v) { el._attrs['data-' + k.replace(/[A-Z]/g, c => '-' + c.toLowerCase())] = v; return true; } }), querySelectorAll() { return []; }, querySelector() { return null; }, }; return el; } global.window = global; global.document = { createElement: makeElement, createDocumentFragment() { return makeElement('fragment'); }, head: makeElement('head'), body: makeElement('body'), querySelector() { return null; }, querySelectorAll() { return []; }, createTextNode(s) { return { _isText: true, textContent: String(s), nodeType: 3 }; }, addEventListener() {}, createComment(s) { return { _isComment: true, textContent: s || '', nodeType: 8 }; }, getElementsByTagName() { return []; }, }; global.localStorage = { getItem() { return null; }, setItem() {}, removeItem() {} }; global.CustomEvent = class { constructor(n, o) { this.type = n; this.detail = (o || {}).detail || {}; } }; global.MutationObserver = class { observe() {} disconnect() {} }; global.requestIdleCallback = fn => setTimeout(fn, 0); global.matchMedia = () => ({ matches: false }); global.navigator = { serviceWorker: { register() { return Promise.resolve(); } } }; global.location = { href: '', pathname: '/', hostname: 'localhost' }; global.history = { pushState() {}, replaceState() {} }; global.fetch = () => Promise.resolve({ ok: true, text() { return Promise.resolve(''); } }); global.XMLHttpRequest = class { open() {} send() {} }; async function main() { require(path.join(WASM_DIR, 'sx_browser.bc.wasm.js')); const K = await new Promise((resolve, reject) => { let tries = 0; const poll = setInterval(() => { if (globalThis.SxKernel) { clearInterval(poll); resolve(globalThis.SxKernel); } else if (++tries > 200) { clearInterval(poll); reject(new Error('SxKernel not found after 10s')); } }, 50); }); // --- 8 FFI host primitives (as test_wasm_native.js) --- K.registerNative('host-global', args => (args[0] in globalThis) ? globalThis[args[0]] : null); K.registerNative('host-get', args => { const [obj, prop] = args; if (obj == null) return null; const v = obj[prop]; return v === undefined ? null : v; }); K.registerNative('host-set!', args => { if (args[0] != null) args[0][args[1]] = args[2]; return args[2]; }); K.registerNative('host-call', args => { const [obj, method, ...rest] = args; if (obj == null || typeof obj[method] !== 'function') return null; const r = obj[method].apply(obj, rest); return r === undefined ? null : r; }); K.registerNative('host-new', args => new (Function.prototype.bind.apply(args[0], [null, ...args.slice(1)]))); K.registerNative('host-callback', args => function () { return K.callFn(args[0], Array.from(arguments)); }); K.registerNative('host-typeof', args => typeof args[0]); K.registerNative('host-await', args => args[0]); K.eval('(define SX_VERSION "wasm-corpus-1.0")'); K.eval('(define SX_ENGINE "ocaml-vm-wasm-corpus")'); K.eval('(define parse sx-parse)'); K.eval('(define serialize sx-serialize)'); // --- Web stack modules (source form; bytecode covered elsewhere) --- const sxDir = path.join(WASM_DIR, 'sx'); const modules = [ 'render', 'core-signals', 'signals', 'deps', 'router', 'page-helpers', 'freeze', 'bytecode', 'compiler', 'vm', 'dom', 'browser', 'adapter-html', 'adapter-sx', 'adapter-dom', 'boot-helpers', 'hypersx', 'harness', 'harness-reactive', 'harness-web', 'engine', 'orchestration', 'boot', ]; if (K.beginModuleLoad) K.beginModuleLoad(); for (const mod of modules) { K.load(fs.readFileSync(path.join(sxDir, mod + '.sx'), 'utf8')); } if (K.endModuleLoad) K.endModuleLoad(); // --- Test framework hooks --- let pass = 0, fail = 0; const suiteStack = []; K.registerNative('report-pass', () => { pass++; return null; }); K.registerNative('report-fail', args => { fail++; const suitePath = suiteStack.join(' > '); console.error(`FAIL: ${suitePath ? suitePath + ' > ' : ''}${args[0]}\n ${args[1]}`); return null; }); K.registerNative('push-suite', args => { suiteStack.push(args[0]); return null; }); K.registerNative('pop-suite', () => { suiteStack.pop(); return null; }); K.eval('(define test-allowed? (fn (name) true))'); K.eval('(define try-call (fn (thunk) (let ((result (cek-try thunk (fn (err) err)))) (if (and (= (type-of result) "string") (starts-with? result "Error")) {"ok" false "error" result} {"ok" true "error" nil}))))'); K.load(fs.readFileSync(path.join(PROJECT_ROOT, 'spec/tests/test-framework.sx'), 'utf8')); // --- Run the target file --- const rel = path.relative(PROJECT_ROOT, path.resolve(target)); let status = 'ok'; try { K.load(fs.readFileSync(path.resolve(target), 'utf8')); } catch (e) { status = 'load-error'; console.error(`LOAD-ERROR: ${rel}: ${e.message}`); } console.log(`CORPUS-RESULT ${rel} pass=${pass} fail=${fail} status=${status}`); process.exit(status !== 'ok' || fail > 0 ? 1 : 0); } main().catch(e => { console.error('FATAL:', e.message); process.exit(1); });