From f12c19eaa38fc829193d6ed2f79dfb6842c59d01 Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 11 May 2026 21:04:30 +0000 Subject: [PATCH] =?UTF-8?q?HS:=20test=20runner=20=E2=80=94=20unwrap=20valu?= =?UTF-8?q?e=20handles=20before=20native=20interop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new kernel ABI wraps atoms (number, string, boolean, nil) in opaque handles {_type, __sx_handle}. When such handles flow through host-call into native JS functions, value equality breaks: each integer literal becomes a unique handle object, so JS Set.add(handle_for_1) does NOT dedup against a prior set.add(handle_for_1). Same problem for any JS API that uses identity or value equality on incoming arguments. Fix: add _unwrapHandle that converts handles back to JS primitives via K.stringify, and apply it to argument lists in host-call and host-new (the two natives that pass user values into native JS constructors / methods). Forward-compatible: no-op when called with already-unwrapped plain values from the legacy kernel. Root-cause analysis traced through: 1. Test 27 'can append a value to a set' failed (Expected 3, got 4) on the new WASM only. Set was admitting duplicates. 2. dbg-set.js minimal repro confirmed each `1` literal arriving at set.add as a different {_type, __sx_handle} object. 3. JS Set.add uses SameValueZero — handle objects with the same underlying value are still distinct identity. 4. Unwrapping in host-call/host-new resolves the equality issue. This is preparation for the JIT Phase 1 WASM rollout (which still needs more native-interop unwrap audits before it can replace the pre-merge WASM that the test tree currently pins). Co-Authored-By: Claude Sonnet 4.6 --- tests/hs-run-filtered.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/hs-run-filtered.js b/tests/hs-run-filtered.js index d5baf1e1..c874ecbd 100755 --- a/tests/hs-run-filtered.js +++ b/tests/hs-run-filtered.js @@ -36,6 +36,26 @@ if (K && typeof K.eval === 'function' && K.stringify) { }; } +// 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'); @@ -688,9 +708,9 @@ K.registerNative('host-get',a=>{ 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',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.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 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-new',a=>{const C=typeof a[0]==='string'?globalThis[a[0]]:a[0];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;});