HS: asyncError — rejected promise triggers catch block (+1 test)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 48s

Three-part fix for hs-upstream-core/asyncError test 2/2:

1. runtime.sx hs-win-call: when an async call returns a rejected promise,
   store the error value in window.__hs_async_error (side-channel) and
   raise the sentinel "__hs_async_error__" so the value survives the
   raise boundary intact.

2. compiler.sx catch clause: inject `(let ((var (host-hs-normalize-exc var))) ...)`
   around the catch body so the sentinel gets swapped for the real error
   object before user code runs. Uses let (not set!) so shadowing works
   correctly for guard catch variables.

3. tests/hs-run-filtered.js:
   - host-promise-state wraps JS Error objects as plain {message:...} dicts
     before they cross the WASM boundary (Error.toString() was producing
     "Error: boom" strings instead of accessible objects)
   - host-hs-normalize-exc native retrieves the side-channel value when
     the sentinel arrives in a catch variable
   - host-get coercion restricted to El instances — plain JS objects with
     a "value" key were being stringified to "[object Object]"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 02:07:52 +00:00
parent 846650da07
commit abbb1fe5c6
8 changed files with 175 additions and 172 deletions

View File

@@ -399,6 +399,8 @@ globalThis.cancelAnimationFrame=()=>{};
// cluster-36b: globalFunction mock for "can call functions" test.
// The test calls globalFunction("foo") via hyperscript and checks window.calledWith.
globalThis.globalFunction = function(x) { globalThis.calledWith = x; };
// cluster-asyncError: function that returns a rejected promise.
globalThis.failAsync = function() { return Promise.reject(new Error("boom")); };
// HsMutationObserver — cluster-32 mutation mock. Maintains a global
// registry; setAttribute/appendChild/removeChild/_setInnerHTML hooks below
// fire matching observers synchronously. A re-entry guard
@@ -574,7 +576,9 @@ K.registerNative('host-get',a=>{
if(a[0] instanceof El && a[1]==='innerText') return String(a[0].textContent||'');
let v=a[0][a[1]];
if(v===undefined)return null;
if((a[1]==='innerHTML'||a[1]==='textContent'||a[1]==='value'||a[1]==='className')&&typeof v!=='string')v=String(v!=null?v:'');
// 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:'');
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];});
@@ -623,7 +627,25 @@ K.registerNative('host-promise-state', a => {
if (!p || typeof p.then !== 'function') return null;
const s = _promiseStates.get(p);
if (!s) return null;
return {ok: s.ok, value: s.value};
// Wrap Error objects as plain dicts — the WASM bridge serializes arbitrary
// JS objects to strings, so we extract message before crossing the boundary.
const val = s.value instanceof Error
? {message: s.value.message}
: (s.value != null ? s.value : null);
return {ok: s.ok, value: val};
});
// Normalize exception in catch blocks: if this is the async-error sentinel string,
// retrieve the original error object from the side-channel global instead.
K.registerNative('host-hs-normalize-exc', a => {
const val = a[0];
const pending = globalThis.__hs_async_error;
if (pending !== undefined && pending !== null && val === '__hs_async_error__') {
globalThis.__hs_async_error = null;
return pending;
}
globalThis.__hs_async_error = null;
return val;
});
let _testDeadline = 0;