HS: fix test-456 timeout + add sx_kernel_eval/hs_compile_inspect/hs_parse_inspect tools
- hs-run-filtered.js: add collectionExpressions to _NO_STEP_LIMIT_SUITES (fixes state corruption for downstream for-loop tests), add repeat-forever tests to _NO_STEP_LIMIT, extend slow deadline for collectionExpressions to 60s - tests/hs-kernel-eval.js: new standalone Node.js eval script — full WASM kernel + mock DOM, accepts HS_EVAL_EXPR/MODE/SETUP/FILES env vars, supports eval/compile/parse modes - tools/mcp_hs_test.py: add sx_kernel_eval, hs_compile_inspect, hs_parse_inspect tools - hosts/ocaml/bin/mcp_tree.ml: add host_stubs param to sx_harness_eval (OCaml build pending) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1892,8 +1892,34 @@ let handle_sx_harness_eval args =
|
|||||||
let file = args |> member "file" |> to_string_option in
|
let file = args |> member "file" |> to_string_option in
|
||||||
let setup_str = args |> member "setup" |> to_string_option in
|
let setup_str = args |> member "setup" |> to_string_option in
|
||||||
let files_json = try args |> member "files" with _ -> `Null in
|
let files_json = try args |> member "files" with _ -> `Null in
|
||||||
|
let host_stubs = match args |> member "host_stubs" with `Bool b -> b | _ -> false in
|
||||||
let e = !env in
|
let e = !env in
|
||||||
let warnings = ref [] in
|
let warnings = ref [] in
|
||||||
|
(* Inject stub host primitives so files using host-get/host-new/etc. can load *)
|
||||||
|
if host_stubs then begin
|
||||||
|
let stubs = {|
|
||||||
|
(define host-global (fn (&rest _) nil))
|
||||||
|
(define host-get (fn (&rest _) nil))
|
||||||
|
(define host-set! (fn (obj k v) v))
|
||||||
|
(define host-call (fn (&rest _) nil))
|
||||||
|
(define host-new (fn (&rest _) (dict)))
|
||||||
|
(define host-callback (fn (f) f))
|
||||||
|
(define host-typeof (fn (&rest _) "string"))
|
||||||
|
(define hs-ref-eq (fn (a b) (identical? a b)))
|
||||||
|
(define host-call-fn (fn (&rest _) nil))
|
||||||
|
(define host-iter? (fn (&rest _) false))
|
||||||
|
(define host-to-list (fn (&rest _) (list)))
|
||||||
|
(define host-await (fn (&rest _) nil))
|
||||||
|
(define host-new-function (fn (&rest _) nil))
|
||||||
|
(define load-library! (fn (&rest _) false))
|
||||||
|
|} in
|
||||||
|
let stub_exprs = Sx_parser.parse_all stubs in
|
||||||
|
List.iter (fun expr ->
|
||||||
|
try ignore (Sx_ref.eval_expr expr (Env e))
|
||||||
|
with exn ->
|
||||||
|
warnings := Printf.sprintf "Stub warning: %s" (Printexc.to_string exn) :: !warnings
|
||||||
|
) stub_exprs
|
||||||
|
end;
|
||||||
(* Collect all files to load *)
|
(* Collect all files to load *)
|
||||||
let all_files = match files_json with
|
let all_files = match files_json with
|
||||||
| `List items ->
|
| `List items ->
|
||||||
@@ -3018,7 +3044,8 @@ let tool_definitions = `List [
|
|||||||
("mock", `Assoc [("type", `String "string"); ("description", `String "Optional mock platform overrides as SX dict, e.g. {:fetch (fn (url) {:status 200})}")]);
|
("mock", `Assoc [("type", `String "string"); ("description", `String "Optional mock platform overrides as SX dict, e.g. {:fetch (fn (url) {:status 200})}")]);
|
||||||
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")]);
|
("file", `Assoc [("type", `String "string"); ("description", `String "Optional .sx file to load for definitions")]);
|
||||||
("files", `Assoc [("type", `String "array"); ("items", `Assoc [("type", `String "string")]); ("description", `String "Multiple .sx files to load in order")]);
|
("files", `Assoc [("type", `String "array"); ("items", `Assoc [("type", `String "string")]); ("description", `String "Multiple .sx files to load in order")]);
|
||||||
("setup", `Assoc [("type", `String "string"); ("description", `String "SX setup expression to run before main evaluation")])]
|
("setup", `Assoc [("type", `String "string"); ("description", `String "SX setup expression to run before main evaluation")]);
|
||||||
|
("host_stubs", `Assoc [("type", `String "boolean"); ("description", `String "If true, inject nil-returning stubs for host-get/host-set!/host-call/host-new/etc. so files that use host primitives can load in the harness")])]
|
||||||
["expr"];
|
["expr"];
|
||||||
tool "sx_nav" "Manage sx-docs navigation and articles. Modes: list (all nav items with status), check (validate consistency), add (create article + nav entry), delete (remove nav entry + page fn), move (move entry between sections, rewriting hrefs)."
|
tool "sx_nav" "Manage sx-docs navigation and articles. Modes: list (all nav items with status), check (validate consistency), add (create article + nav entry), delete (remove nav entry + page fn), move (move entry between sections, rewriting hrefs)."
|
||||||
[("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: list, check, add, delete, or move")]);
|
[("mode", `Assoc [("type", `String "string"); ("description", `String "Mode: list, check, add, delete, or move")]);
|
||||||
|
|||||||
263
tests/hs-kernel-eval.js
Normal file
263
tests/hs-kernel-eval.js
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Evaluate SX (or inspect HS compiler/parser output) in the full WASM kernel.
|
||||||
|
*
|
||||||
|
* Environment variables (preferred — avoids shell escaping):
|
||||||
|
* HS_EVAL_EXPR SX expression to evaluate (required unless --expr arg given)
|
||||||
|
* HS_EVAL_SETUP SX setup expression run before main eval
|
||||||
|
* HS_EVAL_FILES Comma-separated list of .sx files to load first
|
||||||
|
* HS_EVAL_MODE 'eval' (default) | 'compile' | 'parse'
|
||||||
|
* compile: wraps expr as hs-compile arg, returns SX AST string
|
||||||
|
* parse: wraps expr as hs-parse arg, returns parse tree string
|
||||||
|
*
|
||||||
|
* CLI fallback: first positional arg used as expression if HS_EVAL_EXPR not set.
|
||||||
|
*
|
||||||
|
* Output: JSON to stdout { ok: true, result: "..." }
|
||||||
|
* or { ok: false, error: "..." }
|
||||||
|
* Progress / load errors go to stderr.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
const PROJECT = path.resolve(__dirname, '..');
|
||||||
|
const WASM_DIR = path.join(PROJECT, 'shared/static/wasm');
|
||||||
|
const SX_DIR = path.join(WASM_DIR, 'sx');
|
||||||
|
|
||||||
|
// ── Load WASM kernel ────────────────────────────────────────────
|
||||||
|
eval(fs.readFileSync(path.join(WASM_DIR, 'sx_browser.bc.js'), 'utf8'));
|
||||||
|
const K = globalThis.SxKernel;
|
||||||
|
|
||||||
|
// ── Minimal DOM mock ────────────────────────────────────────────
|
||||||
|
class CL {
|
||||||
|
constructor() { this._s = new Set(); }
|
||||||
|
add(c) { if (c) this._s.add(c); }
|
||||||
|
remove(c) { this._s.delete(c); }
|
||||||
|
contains(c) { return this._s.has(c); }
|
||||||
|
toggle(c) { this._s.has(c) ? this.remove(c) : this.add(c); return this._s.has(c); }
|
||||||
|
_sync(v) { this._s = new Set((v||'').split(' ').filter(Boolean)); }
|
||||||
|
}
|
||||||
|
class El {
|
||||||
|
constructor(t) {
|
||||||
|
this.tagName = t.toUpperCase(); this.nodeName = this.tagName; this.nodeType = 1;
|
||||||
|
this.id = ''; this.className = ''; this.textContent = ''; this.innerHTML = '';
|
||||||
|
this.value = ''; this.checked = false; this.disabled = false; this.type = '';
|
||||||
|
this.style = { setProperty(p,v){this[p]=v;}, getPropertyValue(p){return this[p]||'';} };
|
||||||
|
this.attributes = {}; this.children = []; this.childNodes = [];
|
||||||
|
this.childNodes.item = i => this.childNodes[i] || null;
|
||||||
|
this.parentNode = null; this.parentElement = null; this._listeners = {};
|
||||||
|
this.classList = new CL();
|
||||||
|
this.dataset = {};
|
||||||
|
this.open = false; this.multiple = false; this.selected = false;
|
||||||
|
}
|
||||||
|
setAttribute(n,v) {
|
||||||
|
this.attributes[n] = String(v);
|
||||||
|
if (n==='id') this.id = v;
|
||||||
|
if (n==='class') { this.className = v; this.classList._sync(v); }
|
||||||
|
if (n==='value') this.value = v;
|
||||||
|
}
|
||||||
|
getAttribute(n) { return this.attributes[n] !== undefined ? this.attributes[n] : null; }
|
||||||
|
removeAttribute(n){ delete this.attributes[n]; }
|
||||||
|
hasAttribute(n) { return n in this.attributes; }
|
||||||
|
appendChild(c) { if(c){ c.parentNode=this; c.parentElement=this; this.children.push(c); this.childNodes.push(c); } return c; }
|
||||||
|
removeChild(c) { this.children=this.children.filter(x=>x!==c); this.childNodes=this.childNodes.filter(x=>x!==c); if(c){c.parentNode=null;c.parentElement=null;} return c; }
|
||||||
|
remove() { if(this.parentNode) this.parentNode.removeChild(this); }
|
||||||
|
prepend(c) { if(c){ c.parentNode=this; this.children.unshift(c); this.childNodes.unshift(c); } }
|
||||||
|
insertBefore(c,r) { if(!r) return this.appendChild(c); const i=this.childNodes.indexOf(r); if(i<0) return this.appendChild(c); this.childNodes.splice(i,0,c); this.children.splice(i,0,c); c.parentNode=this; return c; }
|
||||||
|
replaceChild(n,o) { const i=this.childNodes.indexOf(o); if(i>=0){ this.childNodes[i]=n; this.children[i]=n; n.parentNode=this; o.parentNode=null; } return o; }
|
||||||
|
cloneNode(deep) { const c=new El(this.tagName); if(deep) for(const ch of this.childNodes) c.appendChild(ch.cloneNode&&ch.cloneNode(true)||{...ch}); return c; }
|
||||||
|
addEventListener(t,h) { if(!this._listeners[t]) this._listeners[t]=[]; this._listeners[t].push(h); }
|
||||||
|
removeEventListener(t,h) { if(this._listeners[t]) this._listeners[t]=this._listeners[t].filter(x=>x!==h); }
|
||||||
|
dispatchEvent(ev) { (this._listeners[ev&&ev.type]||[]).forEach(h=>{ try{h(ev);}catch(e){} }); return true; }
|
||||||
|
querySelector(sel) {
|
||||||
|
if (!sel) return null;
|
||||||
|
if (sel.startsWith('#')) { const id=sel.slice(1); if(this.id===id) return this; for(const c of this.childNodes){const r=c.querySelector&&c.querySelector(sel); if(r) return r;} return null; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
querySelectorAll() { return []; }
|
||||||
|
closest(sel) { return sel && this.matches(sel) ? this : (this.parentNode && this.parentNode.closest ? this.parentNode.closest(sel) : null); }
|
||||||
|
matches(sel) {
|
||||||
|
if (!sel) return false;
|
||||||
|
if (sel.startsWith('#')) return this.id === sel.slice(1);
|
||||||
|
if (sel.startsWith('.')) return this.classList.contains(sel.slice(1));
|
||||||
|
return this.tagName.toLowerCase() === sel.toLowerCase();
|
||||||
|
}
|
||||||
|
focus() {}
|
||||||
|
blur() {}
|
||||||
|
click() { this.dispatchEvent(new Ev('click',{bubbles:true})); }
|
||||||
|
getBoundingClientRect() { return {width:0,height:0,top:0,left:0,right:0,bottom:0}; }
|
||||||
|
}
|
||||||
|
class Ev {
|
||||||
|
constructor(t,o) { this.type=t; const opts=o||{}; this.bubbles=opts.bubbles!==false; this.detail=opts.detail||null; this.target=null; this.currentTarget=null; }
|
||||||
|
preventDefault() {}
|
||||||
|
stopPropagation() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const _body = new El('body');
|
||||||
|
const _head = new El('head');
|
||||||
|
const _docListeners = {};
|
||||||
|
const _domRegistry = new Map(); // id -> El
|
||||||
|
|
||||||
|
function _findById(id) {
|
||||||
|
function find(el) {
|
||||||
|
if (!(el instanceof El)) return null;
|
||||||
|
if (el.id === id) return el;
|
||||||
|
for (const c of (el.childNodes||[])) { const r = find(c); if (r) return r; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return find(_body);
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.document = {
|
||||||
|
body: _body, head: _head, title: '',
|
||||||
|
createElement: t => new El(t),
|
||||||
|
createElementNS: (ns,t) => new El(t),
|
||||||
|
createTextNode: s => ({ nodeType:3, textContent:String(s||''), nodeName:'#text', parentNode:null }),
|
||||||
|
createDocumentFragment: () => { const f=new El('fragment'); f.nodeType=11; return f; },
|
||||||
|
createComment: s => ({ nodeType:8, textContent:s, nodeName:'#comment' }),
|
||||||
|
getElementById: id => _findById(id),
|
||||||
|
querySelector: sel => sel && sel.startsWith('#') ? _findById(sel.slice(1)) : null,
|
||||||
|
querySelectorAll: () => [],
|
||||||
|
addEventListener: (t,h) => { if(!_docListeners[t]) _docListeners[t]=[]; _docListeners[t].push(h); },
|
||||||
|
removeEventListener: (t,h) => { if(_docListeners[t]) _docListeners[t]=_docListeners[t].filter(x=>x!==h); },
|
||||||
|
dispatchEvent: ev => { (_docListeners[ev&&ev.type]||[]).forEach(h=>{ try{h(ev);}catch(e){} }); },
|
||||||
|
activeElement: null,
|
||||||
|
};
|
||||||
|
globalThis.CustomEvent = Ev;
|
||||||
|
globalThis.Event = Ev;
|
||||||
|
globalThis.window = globalThis;
|
||||||
|
globalThis.navigator = { userAgent: 'node' };
|
||||||
|
globalThis.location = { href:'http://localhost/', pathname:'/', search:'', hash:'' };
|
||||||
|
globalThis.history = { pushState(){}, replaceState(){} };
|
||||||
|
globalThis.getSelection = () => ({ toString: () => '' });
|
||||||
|
globalThis.console = { log:()=>{}, error:()=>{}, warn:()=>{}, info:()=>{}, debug:()=>{} };
|
||||||
|
globalThis.ResizeObserver = class { observe(){} unobserve(){} disconnect(){} };
|
||||||
|
globalThis.IntersectionObserver = class { constructor(cb){} observe(){} unobserve(){} disconnect(){} takeRecords(){return[];} };
|
||||||
|
|
||||||
|
// ── FFI registrations ───────────────────────────────────────────
|
||||||
|
K.registerNative('hs-ref-eq', a => a[0]===a[1]);
|
||||||
|
K.registerNative('host-global', a => { const n=a[0]; return (n in globalThis)?globalThis[n]:null; });
|
||||||
|
K.registerNative('host-get', a => {
|
||||||
|
if (a[0]==null) return null;
|
||||||
|
if (a[0] && a[0]._type==='list' && (a[1]==='length'||a[1]==='size')) return a[0].items.length;
|
||||||
|
if (a[0] instanceof El && a[1]==='innerText') return String(a[0].textContent||'');
|
||||||
|
const v = a[0][a[1]]; return v===undefined ? null : v;
|
||||||
|
});
|
||||||
|
K.registerNative('host-set!', a => { if(a[0]!=null){ a[0][a[1]]=a[2]; if(a[0] instanceof El && a[1]==='id' && a[2]) a[0].id=a[2]; } return a[2]; });
|
||||||
|
K.registerNative('host-call', a => {
|
||||||
|
const [o,m,...r]=a;
|
||||||
|
if(o==null){ const f=globalThis[m]; return typeof f==='function'?f.apply(null,r):null; }
|
||||||
|
if(o && typeof o[m]==='function'){ try{ const v=o[m].apply(o,r); return v===undefined?null:v; }catch(e){ return null; } }
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
K.registerNative('host-call-fn', a => {
|
||||||
|
const [fn,argList]=a;
|
||||||
|
if(!fn) return null;
|
||||||
|
const args=(argList&&argList._type==='list'&&argList.items)?Array.from(argList.items):(Array.isArray(argList)?argList:[]);
|
||||||
|
if(fn&&fn.__sx_handle!==undefined) return K.callFn(fn,args);
|
||||||
|
try{ return fn.apply(null,args); }catch(e){ return null; }
|
||||||
|
});
|
||||||
|
K.registerNative('host-new', a => { const C=typeof a[0]==='string'?globalThis[a[0]]:a[0]; return typeof C==='function'?new C(...a.slice(1)):null; });
|
||||||
|
K.registerNative('host-callback',a => {
|
||||||
|
const fn=a[0];
|
||||||
|
if(fn&&fn.__sx_handle!==undefined) return function(){ const r=K.callFn(fn,Array.from(arguments)); if(globalThis._driveAsync) globalThis._driveAsync(r); return r; };
|
||||||
|
return typeof fn==='function'?fn:function(){};
|
||||||
|
});
|
||||||
|
K.registerNative('host-typeof', a => { const o=a[0]; if(o==null) return 'nil'; if(o instanceof El) return 'element'; if(o instanceof Ev) return 'event'; return typeof o; });
|
||||||
|
K.registerNative('host-iter?', ([obj]) => obj!=null && typeof obj[Symbol.iterator]==='function');
|
||||||
|
K.registerNative('host-to-list', ([obj]) => { try{ return [...obj]; }catch(e){ return []; } });
|
||||||
|
K.registerNative('host-await', () => {});
|
||||||
|
K.registerNative('host-new-function', a => { const p=(a[0]&&a[0]._type==='list')?Array.from(a[0].items):[]; try{ return new Function(...p,a[1]); }catch(e){ return null; } });
|
||||||
|
K.registerNative('host-promise-state', a => { const p=a[0]; if(!p||typeof p.then!=='function') return null; const s=globalThis._promiseStates&&globalThis._promiseStates.get(p); return s?{ok:s.ok,value:s.value}:null; });
|
||||||
|
K.registerNative('load-library!', () => false);
|
||||||
|
|
||||||
|
// Async IO driver
|
||||||
|
let _evalDeadline = 0;
|
||||||
|
globalThis._driveAsync = function driveAsync(r, depth) {
|
||||||
|
depth = depth||0;
|
||||||
|
if (_evalDeadline && Date.now() > _evalDeadline) throw new Error('TIMEOUT: wall clock exceeded');
|
||||||
|
if (!r || !r.suspended || depth > 200) return;
|
||||||
|
const req = r.request;
|
||||||
|
const items = req && (req.items || req);
|
||||||
|
const op = items && items[0];
|
||||||
|
const opName = typeof op==='string' ? op : (op&&op.name)||String(op);
|
||||||
|
function doResume(v) { try{ const x=r.resume(v); driveAsync(x,depth+1); }catch(e){} }
|
||||||
|
if (opName==='io-sleep'||opName==='wait') doResume(null);
|
||||||
|
else if (opName==='io-wait-event') {
|
||||||
|
const target=items&&items[1];
|
||||||
|
const evName=typeof items[2]==='string'?items[2]:'';
|
||||||
|
const timeout=items&&items.length>3?items[3]:undefined;
|
||||||
|
if (typeof timeout==='number') { doResume(null); }
|
||||||
|
else if (target && target instanceof El && evName) {
|
||||||
|
const handler=function(ev){ target.removeEventListener(evName,handler); doResume(ev); };
|
||||||
|
target.addEventListener(evName,handler);
|
||||||
|
} else { doResume(null); }
|
||||||
|
}
|
||||||
|
else if (opName==='io-transition') doResume(null);
|
||||||
|
else doResume(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ── SX aliases ──────────────────────────────────────────────────
|
||||||
|
K.eval('(define SX_VERSION "hs-eval-1.0")');
|
||||||
|
K.eval('(define SX_ENGINE "ocaml-vm-sandbox")');
|
||||||
|
K.eval('(define parse sx-parse)');
|
||||||
|
K.eval('(define serialize sx-serialize)');
|
||||||
|
|
||||||
|
// ── Load HS modules ─────────────────────────────────────────────
|
||||||
|
const WEB = ['render','core-signals','signals','deps','router','page-helpers','freeze','dom','browser',
|
||||||
|
'adapter-html','adapter-sx','adapter-dom','boot-helpers','hypersx','engine','orchestration','boot'];
|
||||||
|
const HS = ['hs-tokenizer','hs-parser','hs-compiler','hs-runtime','hs-integration'];
|
||||||
|
K.beginModuleLoad();
|
||||||
|
for (const mod of [...WEB, ...HS]) {
|
||||||
|
const sp = path.join(SX_DIR, mod+'.sx');
|
||||||
|
const lp = path.join(PROJECT, 'lib/hyperscript', mod.replace(/^hs-/,'')+'.sx');
|
||||||
|
let s;
|
||||||
|
try {
|
||||||
|
const lpExists = mod.startsWith('hs-') && fs.existsSync(lp);
|
||||||
|
s = lpExists ? fs.readFileSync(lp,'utf8')
|
||||||
|
: fs.existsSync(sp) ? fs.readFileSync(sp,'utf8')
|
||||||
|
: fs.readFileSync(lp,'utf8');
|
||||||
|
} catch(e) { continue; }
|
||||||
|
try { K.load(s); } catch(e) { process.stderr.write(`LOAD ERROR: ${mod}: ${e.message}\n`); }
|
||||||
|
}
|
||||||
|
K.endModuleLoad();
|
||||||
|
|
||||||
|
// ── Extra files ─────────────────────────────────────────────────
|
||||||
|
const extraFiles = (process.env.HS_EVAL_FILES || '').split(',').filter(Boolean);
|
||||||
|
for (const f of extraFiles) {
|
||||||
|
try { K.load(fs.readFileSync(f.trim(),'utf8')); }
|
||||||
|
catch(e) { process.stderr.write(`FILE ERROR: ${f}: ${e.message}\n`); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Setup expression ────────────────────────────────────────────
|
||||||
|
const setup = process.env.HS_EVAL_SETUP || '';
|
||||||
|
if (setup) {
|
||||||
|
try { K.eval(setup); }
|
||||||
|
catch(e) {
|
||||||
|
process.stdout.write(JSON.stringify({ok:false,error:`Setup error: ${e.message||String(e)}`})+'\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main evaluation ─────────────────────────────────────────────
|
||||||
|
const mode = process.env.HS_EVAL_MODE || 'eval';
|
||||||
|
const rawExpr = process.env.HS_EVAL_EXPR || process.argv[2] || '';
|
||||||
|
if (!rawExpr) {
|
||||||
|
process.stdout.write(JSON.stringify({ok:false,error:'No expression provided. Set HS_EVAL_EXPR or pass as first argument.'})+'\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const expr = mode==='compile' ? `(str (hs-compile ${JSON.stringify(rawExpr)}))`
|
||||||
|
: mode==='parse' ? `(str (hs-parse ${JSON.stringify(rawExpr)}))`
|
||||||
|
: rawExpr;
|
||||||
|
|
||||||
|
_evalDeadline = Date.now() + parseInt(process.env.HS_EVAL_TIMEOUT_MS||'30000');
|
||||||
|
try {
|
||||||
|
const result = K.eval(expr);
|
||||||
|
let resultStr;
|
||||||
|
try { resultStr = JSON.stringify(result); } catch(e) { resultStr = String(result); }
|
||||||
|
process.stdout.write(JSON.stringify({ok:true,result:resultStr})+'\n');
|
||||||
|
} catch(e) {
|
||||||
|
process.stdout.write(JSON.stringify({ok:false,error:e.message||String(e)})+'\n');
|
||||||
|
}
|
||||||
@@ -791,10 +791,13 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
|||||||
"async hypertrace is reasonable",
|
"async hypertrace is reasonable",
|
||||||
"hypertrace from javascript is reasonable",
|
"hypertrace from javascript is reasonable",
|
||||||
"hypertrace is reasonable",
|
"hypertrace is reasonable",
|
||||||
|
"repeat forever works",
|
||||||
|
"repeat forever works w/o keyword",
|
||||||
]);
|
]);
|
||||||
// Suites where JIT cascade legitimately exceeds the per-test step limit.
|
// Suites where JIT cascade legitimately exceeds the per-test step limit.
|
||||||
const _NO_STEP_LIMIT_SUITES = new Set([
|
const _NO_STEP_LIMIT_SUITES = new Set([
|
||||||
"hs-upstream-core/runtimeErrors",
|
"hs-upstream-core/runtimeErrors",
|
||||||
|
"hs-upstream-expressions/collectionExpressions",
|
||||||
]);
|
]);
|
||||||
// Enable step limit for timeout protection — reset counter first so accumulation
|
// Enable step limit for timeout protection — reset counter first so accumulation
|
||||||
// across tests doesn't cause signed-32-bit wraparound (~2B extra steps before limit fires).
|
// across tests doesn't cause signed-32-bit wraparound (~2B extra steps before limit fires).
|
||||||
@@ -808,6 +811,7 @@ for(let i=startTest;i<Math.min(endTest,testCount);i++){
|
|||||||
};
|
};
|
||||||
const _SLOW_DEADLINE_SUITES = {
|
const _SLOW_DEADLINE_SUITES = {
|
||||||
"hs-upstream-core/runtimeErrors": 30000,
|
"hs-upstream-core/runtimeErrors": 30000,
|
||||||
|
"hs-upstream-expressions/collectionExpressions": 60000,
|
||||||
};
|
};
|
||||||
_testDeadline = Date.now() + (_SLOW_DEADLINE[name] || _SLOW_DEADLINE_SUITES[suite] || 10000);
|
_testDeadline = Date.now() + (_SLOW_DEADLINE[name] || _SLOW_DEADLINE_SUITES[suite] || 10000);
|
||||||
globalThis.__hs_deadline = _testDeadline; // expose to WASM cek_step_loop
|
globalThis.__hs_deadline = _testDeadline; // expose to WASM cek_step_loop
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import time
|
|||||||
|
|
||||||
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
RUNNER_PATH = os.path.join(PROJECT_DIR, "tests/hs-run-filtered.js")
|
RUNNER_PATH = os.path.join(PROJECT_DIR, "tests/hs-run-filtered.js")
|
||||||
GEN_PATH = os.path.join(PROJECT_DIR, "tests/playwright/generate-sx-tests.py")
|
GEN_PATH = os.path.join(PROJECT_DIR, "tests/playwright/generate-sx-tests.py")
|
||||||
|
EVAL_PATH = os.path.join(PROJECT_DIR, "tests/hs-kernel-eval.js")
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -218,6 +219,135 @@ def hs_test_status(args):
|
|||||||
return text_result("\n".join(info))
|
return text_result("\n".join(info))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared helper: run hs-kernel-eval.js
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _kernel_eval(mode, expr, setup=None, files=None, timeout_secs=60):
|
||||||
|
"""Run hs-kernel-eval.js and return a text_result."""
|
||||||
|
if not os.path.isfile(EVAL_PATH):
|
||||||
|
return error_result(f"Eval script not found at {EVAL_PATH}")
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["HS_EVAL_MODE"] = mode
|
||||||
|
env["HS_EVAL_EXPR"] = expr
|
||||||
|
env["HS_EVAL_TIMEOUT_MS"] = str(max(5000, int(timeout_secs) * 1000))
|
||||||
|
if setup:
|
||||||
|
env["HS_EVAL_SETUP"] = setup
|
||||||
|
if files:
|
||||||
|
env["HS_EVAL_FILES"] = ",".join(files)
|
||||||
|
timeout = max(10, min(int(timeout_secs), 300))
|
||||||
|
try:
|
||||||
|
r = subprocess.run(
|
||||||
|
["node", EVAL_PATH],
|
||||||
|
cwd=PROJECT_DIR, env=env,
|
||||||
|
capture_output=True, text=True, timeout=timeout,
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return error_result(f"Kernel eval timed out after {timeout}s")
|
||||||
|
stderr = (r.stderr or "").strip()
|
||||||
|
stdout = (r.stdout or "").strip()
|
||||||
|
# Parse JSON result from stdout
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
data = json.loads(stdout)
|
||||||
|
if data.get("ok"):
|
||||||
|
result = data.get("result", "nil")
|
||||||
|
# Unescape JSON-stringified result
|
||||||
|
try:
|
||||||
|
result = json.loads(result)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
out = f"Result: {result}"
|
||||||
|
else:
|
||||||
|
out = f"Error: {data.get('error', 'unknown error')}"
|
||||||
|
except Exception:
|
||||||
|
out = stdout or "(no output)"
|
||||||
|
if stderr:
|
||||||
|
# Filter noisy load-progress lines, keep errors
|
||||||
|
err_lines = [l for l in stderr.splitlines()
|
||||||
|
if not l.startswith("Loading") and not l.startswith("Modules") and "ms" not in l]
|
||||||
|
if err_lines:
|
||||||
|
out += "\n\nstderr:\n" + "\n".join(err_lines)
|
||||||
|
return text_result(out)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool: sx_kernel_eval
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def sx_kernel_eval(args):
|
||||||
|
"""Evaluate a SX expression in the full WASM kernel with HS modules loaded.
|
||||||
|
|
||||||
|
The kernel includes mock DOM, so HS runtime functions (hs-repeat-forever,
|
||||||
|
hs-compile, dom-dispatch, etc.) are available. Use this when sx_harness_eval
|
||||||
|
fails due to missing host primitives (host-new, host-get, etc.).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
expr: SX expression to evaluate (required).
|
||||||
|
setup: SX setup expression run before main eval (optional).
|
||||||
|
files: List of .sx files to load before eval (optional).
|
||||||
|
timeout_secs: Wall-clock cap in seconds (default 60, max 300).
|
||||||
|
"""
|
||||||
|
expr = args.get("expr", "").strip()
|
||||||
|
if not expr:
|
||||||
|
return error_result("'expr' is required")
|
||||||
|
return _kernel_eval(
|
||||||
|
mode="eval",
|
||||||
|
expr=expr,
|
||||||
|
setup=args.get("setup"),
|
||||||
|
files=args.get("files"),
|
||||||
|
timeout_secs=int(args.get("timeout_secs", 60)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool: hs_compile_inspect
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def hs_compile_inspect(args):
|
||||||
|
"""Compile an HS source string and return the generated SX AST.
|
||||||
|
|
||||||
|
Runs hs-compile on the source and returns its string representation.
|
||||||
|
Useful for debugging what AST the HS compiler produces for a given snippet.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hs_source: HS source code to compile (required).
|
||||||
|
timeout_secs: Wall-clock cap in seconds (default 30).
|
||||||
|
"""
|
||||||
|
src = args.get("hs_source", "").strip()
|
||||||
|
if not src:
|
||||||
|
return error_result("'hs_source' is required")
|
||||||
|
return _kernel_eval(
|
||||||
|
mode="compile",
|
||||||
|
expr=src,
|
||||||
|
timeout_secs=int(args.get("timeout_secs", 30)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Tool: hs_parse_inspect
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def hs_parse_inspect(args):
|
||||||
|
"""Parse an HS source string and return the raw parser AST (before compilation).
|
||||||
|
|
||||||
|
Runs hs-parse on the source and returns its string representation.
|
||||||
|
Useful for debugging tokenizer/parser output before the compiler sees it.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hs_source: HS source code to parse (required).
|
||||||
|
timeout_secs: Wall-clock cap in seconds (default 30).
|
||||||
|
"""
|
||||||
|
src = args.get("hs_source", "").strip()
|
||||||
|
if not src:
|
||||||
|
return error_result("'hs_source' is required")
|
||||||
|
return _kernel_eval(
|
||||||
|
mode="parse",
|
||||||
|
expr=src,
|
||||||
|
timeout_secs=int(args.get("timeout_secs", 30)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# JSON-RPC dispatch
|
# JSON-RPC dispatch
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -265,6 +395,40 @@ TOOLS = [
|
|||||||
{},
|
{},
|
||||||
[],
|
[],
|
||||||
),
|
),
|
||||||
|
tool(
|
||||||
|
"sx_kernel_eval",
|
||||||
|
"Evaluate a SX expression in the full WASM kernel with HS modules and mock DOM loaded. "
|
||||||
|
"Use when sx_harness_eval fails due to missing host primitives (host-new, host-get, etc.). "
|
||||||
|
"Has access to hs-compile, hs-parse, hs-repeat-forever, dom-dispatch, etc.",
|
||||||
|
{
|
||||||
|
"expr": {"type": "string", "description": "SX expression to evaluate"},
|
||||||
|
"setup": {"type": "string", "description": "SX setup expression run before eval (optional)"},
|
||||||
|
"files": {"type": "array", "items": {"type": "string"},
|
||||||
|
"description": "Extra .sx files to load before eval (optional)"},
|
||||||
|
"timeout_secs": {"type": "integer", "description": "Wall-clock cap in seconds (default 60, max 300)"},
|
||||||
|
},
|
||||||
|
["expr"],
|
||||||
|
),
|
||||||
|
tool(
|
||||||
|
"hs_compile_inspect",
|
||||||
|
"Compile an HS source snippet and return the generated SX AST string. "
|
||||||
|
"Runs hs-compile and returns (str result). Use to debug what AST the compiler produces.",
|
||||||
|
{
|
||||||
|
"hs_source": {"type": "string", "description": "HS source code to compile"},
|
||||||
|
"timeout_secs": {"type": "integer", "description": "Wall-clock cap in seconds (default 30)"},
|
||||||
|
},
|
||||||
|
["hs_source"],
|
||||||
|
),
|
||||||
|
tool(
|
||||||
|
"hs_parse_inspect",
|
||||||
|
"Parse an HS source snippet and return the raw parser AST (before compilation). "
|
||||||
|
"Runs hs-parse and returns (str result). Use to debug tokenizer/parser output.",
|
||||||
|
{
|
||||||
|
"hs_source": {"type": "string", "description": "HS source code to parse"},
|
||||||
|
"timeout_secs": {"type": "integer", "description": "Wall-clock cap in seconds (default 30)"},
|
||||||
|
},
|
||||||
|
["hs_source"],
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -278,6 +442,12 @@ def handle_tool(name, args):
|
|||||||
return hs_test_regen(args)
|
return hs_test_regen(args)
|
||||||
case "hs_test_status":
|
case "hs_test_status":
|
||||||
return hs_test_status(args)
|
return hs_test_status(args)
|
||||||
|
case "sx_kernel_eval":
|
||||||
|
return sx_kernel_eval(args)
|
||||||
|
case "hs_compile_inspect":
|
||||||
|
return hs_compile_inspect(args)
|
||||||
|
case "hs_parse_inspect":
|
||||||
|
return hs_parse_inspect(args)
|
||||||
case _:
|
case _:
|
||||||
return error_result(f"Unknown tool: {name}")
|
return error_result(f"Unknown tool: {name}")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user