#!/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'); }