Merge hs-f into architecture: JIT Phase 2/3 + native unwrap sweep + dict-eq fix
JIT Phase 2 (LRU eviction) + Phase 3 (manual reset), lib/jit.sx convenience layer, 21 host-* natives ABI-compatible with WASM kernel handles, dict-eq fix (structural eq for plain dicts + Integer/Number in equal?), io-wait-event interceptor fix, HS test runner unwrap shim for post-JIT-P1 value handles. Conflicts resolved: - tests/hs-run-filtered.js: combined arch's fake-timer block (for socket RPC tests) with hs-f's auto-unwrap shim - shared/static/wasm/sx_browser.bc.js: took hs-f's regenerated bundle
This commit is contained in:
@@ -17,7 +17,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const FILTERED = path.join(__dirname, 'hs-run-filtered.js');
|
||||
const TOTAL = parseInt(process.env.HS_TOTAL || '1496');
|
||||
const TOTAL = parseInt(process.env.HS_TOTAL || '1514');
|
||||
const FROM = parseInt(process.env.HS_FROM || '0');
|
||||
const BATCH_SIZE = parseInt(process.env.HS_BATCH_SIZE || '150');
|
||||
const PARALLEL = parseInt(process.env.HS_PARALLEL || '1');
|
||||
|
||||
@@ -33,6 +33,48 @@ globalThis.__hsFlushTimers = { call: function() {
|
||||
for (const { cb } of batch) { try { cb(); } catch (_) {} }
|
||||
}};
|
||||
|
||||
// Auto-unwrap shim: the post-JIT-Phase-1 kernel returns numbers, strings,
|
||||
// booleans, and nil as opaque value handles ({_type, __sx_handle}). Tests
|
||||
// expect plain JS values from K.eval like the pre-rewrite kernel did. Wrap
|
||||
// once at boot rather than touching all 23 K.eval call sites.
|
||||
if (K && typeof K.eval === 'function' && K.stringify) {
|
||||
const _kEval = K.eval.bind(K);
|
||||
K.eval = function(expr) {
|
||||
const r = _kEval(expr);
|
||||
if (r && typeof r === 'object' && typeof r._type === 'string') {
|
||||
switch (r._type) {
|
||||
case 'number': { const s = K.stringify(r); const n = Number(s);
|
||||
return Number.isInteger(n) || /^-?\d+$/.test(s) ? n : (Number.isNaN(n) ? r : n); }
|
||||
case 'string': return K.stringify(r);
|
||||
case 'boolean': return K.stringify(r) === 'true';
|
||||
case 'nil': return null;
|
||||
default: return r; // list/dict/symbol — leave as handle
|
||||
}
|
||||
}
|
||||
return r;
|
||||
};
|
||||
}
|
||||
|
||||
// Value-handle unwrap helper for native interop. The new kernel wraps atoms
|
||||
// (number, string, boolean, nil) in {_type, __sx_handle} handles. JS natives
|
||||
// receiving these in argument lists would do reference-equality on the handle
|
||||
// instead of value-equality on the underlying primitive — breaking things
|
||||
// like JS Set dedup (each literal `1` becomes a new handle). Unwrap before
|
||||
// handing off to native JS.
|
||||
function _unwrapHandle(v) {
|
||||
if (v && typeof v === 'object' && typeof v._type === 'string' && K.stringify) {
|
||||
switch (v._type) {
|
||||
case 'number': { const s = K.stringify(v); const n = Number(s);
|
||||
return Number.isInteger(n) || /^-?\d+$/.test(s) ? n : n; }
|
||||
case 'string': return K.stringify(v);
|
||||
case 'boolean': return K.stringify(v) === 'true';
|
||||
case 'nil': return null;
|
||||
default: return v;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
// Step limit API — exposed from OCaml kernel
|
||||
const STEP_LIMIT = parseInt(process.env.HS_STEP_LIMIT || '1000000');
|
||||
|
||||
@@ -664,35 +706,36 @@ const _log = _origLog; // keep reference for our own output
|
||||
// JS-level reference equality for host objects (works around OCaml boxing).
|
||||
// The SX `=` primitive doesn't do JS === for host objects in the WASM kernel.
|
||||
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-global',a=>{const n=_unwrapHandle(a[0]);return(n in globalThis)?globalThis[n]:null;});
|
||||
K.registerNative('host-get',a=>{
|
||||
if(a[0]==null)return null;
|
||||
const k=_unwrapHandle(a[1]);
|
||||
// SX lists (arrive as {_type:'list', items:[...]}) don't expose length/size
|
||||
// through JS property access. Hand-roll common collection queries so
|
||||
// compiled HS `x.length` / `x.size` works on scoped lists.
|
||||
if(a[0] && a[0]._type==='list' && (a[1]==='length' || a[1]==='size')) return a[0].items.length;
|
||||
if(a[0] && a[0]._type==='list' && typeof a[1]==='number') return a[0].items[a[1]]!==undefined?a[0].items[a[1]]:null;
|
||||
if(a[0] && a[0]._type==='dict' && a[1]==='size') return Object.keys(a[0]).filter(k=>k!=='_type').length;
|
||||
if(a[0] && a[0]._type==='list' && (k==='length' || k==='size')) return a[0].items.length;
|
||||
if(a[0] && a[0]._type==='list' && typeof k==='number') return a[0].items[k]!==undefined?a[0].items[k]:null;
|
||||
if(a[0] && a[0]._type==='dict' && k==='size') return Object.keys(a[0]).filter(x=>x!=='_type').length;
|
||||
// innerText is DOM-level alias for textContent (close enough for mock purposes)
|
||||
if(a[0] instanceof El && a[1]==='innerText') return String(a[0].textContent||'');
|
||||
if(a[0] instanceof El && k==='innerText') return String(a[0].textContent||'');
|
||||
// RPC dispatch object: _hsRpcDispatch bypasses Proxy-in-WASM-kernel nil issue
|
||||
if(a[0] && typeof a[0]._hsRpcDispatch==='function'){const rv=a[0]._hsRpcDispatch(String(a[1]));return rv===undefined?null:rv;}
|
||||
let v=a[0][a[1]];
|
||||
if(a[0] && typeof a[0]._hsRpcDispatch==='function'){const rv=a[0]._hsRpcDispatch(String(k));return rv===undefined?null:rv;}
|
||||
let v=a[0][k];
|
||||
if(v===undefined)return null;
|
||||
// Only coerce DOM property strings for actual DOM elements — plain JS objects
|
||||
// (e.g. promise-state dicts with a "value" key) must not be stringified.
|
||||
if(a[0] instanceof El&&(a[1]==='innerHTML'||a[1]==='textContent'||a[1]==='value'||a[1]==='className')&&typeof v!=='string')v=String(v!=null?v:'');
|
||||
if(a[0] instanceof El&&(k==='innerHTML'||k==='textContent'||k==='value'||k==='className')&&typeof v!=='string')v=String(v!=null?v:'');
|
||||
return v;
|
||||
});
|
||||
K.registerNative('host-set!',a=>{if(a[0]!=null){const v=a[2]; if(a[1]==='innerHTML'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0]._setInnerHTML(s);a[0][a[1]]=a[0].innerHTML;} else if(a[1]==='textContent'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0].textContent=s;a[0].innerHTML=s;for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];} else{a[0][a[1]]=v;}} return a[2];});
|
||||
K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');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(typeof fn!=='function'&&!(fn&&fn.__sx_handle!==undefined))return null;const callArgs=(argList&&argList._type==='list'&&argList.items)?Array.from(argList.items):(Array.isArray(argList)?argList:[]);if(fn&&fn.__sx_handle!==undefined){try{return K.callFn(fn,callArgs);}catch(e){const msg=e&&e.message||'';if(String(msg).includes('TIMEOUT'))throw e;return null;}}function sxToJs(v){if(v&&v._type==='list'&&v.items)return Array.from(v.items).map(sxToJs);return v;}try{const v=fn.apply(null,callArgs.map(sxToJs));return v===undefined?null:v;}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-set!',a=>{if(a[0]!=null){const k=_unwrapHandle(a[1]);const v=_unwrapHandle(a[2]); if(k==='innerHTML'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0]._setInnerHTML(s);a[0][k]=a[0].innerHTML;} else if(k==='textContent'&&a[0] instanceof El){const s=v===null?'null':v===undefined?'':String(v);a[0].textContent=s;a[0].innerHTML=s;for(const c of a[0].children){c.parentElement=null;c.parentNode=null;}a[0].children=[];a[0].childNodes=[];} else{a[0][k]=v;}} return a[2];});
|
||||
K.registerNative('host-call',a=>{if(_testDeadline&&Date.now()>_testDeadline)throw new Error('TIMEOUT: wall clock exceeded');const[o,mRaw,...r]=a;const m=_unwrapHandle(mRaw);if(o==null){const f=globalThis[m];return typeof f==='function'?f.apply(null,r.map(_unwrapHandle)):null;}if(o&&typeof o[m]==='function'){try{const v=o[m].apply(o,r.map(_unwrapHandle));return v===undefined?null:v;}catch(e){return null;}}return null;});
|
||||
K.registerNative('host-call-fn',a=>{const[fn,argList]=a;if(typeof fn!=='function'&&!(fn&&fn.__sx_handle!==undefined))return null;const callArgs=(argList&&argList._type==='list'&&argList.items)?Array.from(argList.items):(Array.isArray(argList)?argList:[]);if(fn&&fn.__sx_handle!==undefined){try{return K.callFn(fn,callArgs);}catch(e){const msg=e&&e.message||'';if(String(msg).includes('TIMEOUT'))throw e;return null;}}function sxToJs(v){if(v&&v._type==='list'&&v.items)return Array.from(v.items).map(sxToJs);return _unwrapHandle(v);}try{const v=fn.apply(null,callArgs.map(sxToJs));return v===undefined?null:v;}catch(e){return null;}});
|
||||
K.registerNative('host-new',a=>{const nameOrCtor=_unwrapHandle(a[0]);const C=typeof nameOrCtor==='string'?globalThis[nameOrCtor]:nameOrCtor;return typeof C==='function'?new C(...a.slice(1).map(_unwrapHandle)):null;});
|
||||
K.registerNative('host-callback',a=>{const fn=a[0];if(typeof fn==='function'&&fn.__sx_handle===undefined)return fn;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 function(){};});
|
||||
K.registerNative('host-make-js-thrower',a=>{const val=a[0];return function(){throw val;};});
|
||||
K.registerNative('host-typeof',a=>{const o=a[0];if(o==null)return'nil';if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';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-make-js-thrower',a=>{const val=_unwrapHandle(a[0]);return function(){throw val;};});
|
||||
K.registerNative('host-typeof',a=>{let o=a[0];if(o==null)return'nil';if(o&&typeof o==='object'&&typeof o._type==='string'&&'__sx_handle' in o)return o._type;if(o instanceof El)return'element';if(o&&o.nodeType===3)return'text';if(o instanceof Ev)return'event';if(o instanceof Promise)return'promise';return typeof o;});
|
||||
K.registerNative('host-iter?',([obj])=>{const o=_unwrapHandle(obj);return o!=null&&typeof o[Symbol.iterator]==='function';});
|
||||
K.registerNative('host-to-list',([obj])=>{const o=_unwrapHandle(obj);try{return[...o];}catch(e){return[];}});
|
||||
K.registerNative('host-await',a=>{});
|
||||
K.registerNative('load-library!',()=>false);
|
||||
K.registerNative('hs-is-set?',a=>a[0] instanceof Set);
|
||||
@@ -725,10 +768,10 @@ Promise.resolve = function(v) {
|
||||
|
||||
K.registerNative('host-new-function', a => {
|
||||
const paramList = a[0];
|
||||
const src = a[1];
|
||||
const src = _unwrapHandle(a[1]);
|
||||
const params = paramList && paramList._type === 'list' && paramList.items
|
||||
? Array.from(paramList.items)
|
||||
: Array.isArray(paramList) ? paramList : [];
|
||||
? Array.from(paramList.items).map(_unwrapHandle)
|
||||
: Array.isArray(paramList) ? paramList.map(_unwrapHandle) : [];
|
||||
try { return new Function(...params, src); } catch(e) { return null; }
|
||||
});
|
||||
|
||||
@@ -861,9 +904,11 @@ globalThis._driveAsync=function driveAsync(r,d){d=d||0;if(_testDeadline && Date.
|
||||
else if(opName==='io-parse-html'){const resp=items&&items[1];const htmlStr=resp&&(resp._html||resp._body)?String(resp._html||resp._body):'';const frag=new El('fragment');frag.nodeType=11;if(htmlStr)frag._setInnerHTML(htmlStr);doResume(frag);}
|
||||
else if(opName==='io-settle')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;
|
||||
const target=_unwrapHandle(items&&items[1]);
|
||||
const evNameRaw=_unwrapHandle(items&&items[2]);
|
||||
const evName=typeof evNameRaw==='string'?evNameRaw:'';
|
||||
const timeoutRaw=items&&items.length>3?_unwrapHandle(items[3]):undefined;
|
||||
const timeout=typeof timeoutRaw==='number'?timeoutRaw:undefined;
|
||||
if(typeof timeout==='number'){
|
||||
// `wait for EV or Nms` — timeout wins immediately in the mock (tests use 0ms)
|
||||
doResume(null);
|
||||
|
||||
Reference in New Issue
Block a user