Sandbox bytecode loading: K.load + load-sxbc, bytecode param, web stack sxbc via loadModule
Bytecode modules now load correctly in sandbox mode. HS .sxbc modules
use K.load('(load-sxbc ...)') which syncs defines to eval env. Web stack
.sxbc modules use K.loadModule with import suspension drive loop.
K.eval used directly for expression eval (not thunk wrapper) so bytecode-
defined symbols are visible. Falls back to callFn thunk on IO suspension.
Sandbox now reproduces the bytecode repeat bug: source gives 6/6
suspensions, bytecode gives 4/6. Bug is in bytecode compilation of
when/do across perform boundaries, not the runtime wrapper.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1331,7 +1331,7 @@ const SANDBOX_STACKS = {
|
||||
],
|
||||
};
|
||||
|
||||
async function modeSandbox(page, expr, files, setup, stack) {
|
||||
async function modeSandbox(page, expr, files, setup, stack, bytecode) {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const PROJECT_ROOT = path.resolve(__dirname, '../..');
|
||||
@@ -1489,25 +1489,47 @@ async function modeSandbox(page, expr, files, setup, stack) {
|
||||
if (window.SxKernel.beginModuleLoad) window.SxKernel.beginModuleLoad();
|
||||
});
|
||||
for (const mod of modules) {
|
||||
// Try .sx file in wasm/sx/ dir
|
||||
const sxPath = path.join(SX_DIR, mod + '.sx');
|
||||
// Also try lib/hyperscript/ for hs- prefixed modules
|
||||
const libPath = path.join(PROJECT_ROOT, 'lib/hyperscript', mod.replace(/^hs-/, '') + '.sx');
|
||||
let src;
|
||||
try {
|
||||
src = fs.existsSync(sxPath)
|
||||
? fs.readFileSync(sxPath, 'utf8')
|
||||
: fs.readFileSync(libPath, 'utf8');
|
||||
} catch(e) {
|
||||
loadErrors.push(mod + ': file not found');
|
||||
continue;
|
||||
let loaded = false;
|
||||
// Try bytecode first if requested
|
||||
if (bytecode) {
|
||||
const bcPath = path.join(SX_DIR, mod + '.sxbc');
|
||||
if (fs.existsSync(bcPath)) {
|
||||
const bcSrc = fs.readFileSync(bcPath, 'utf8');
|
||||
const err = await page.evaluate(src => {
|
||||
try {
|
||||
const K = window.SxKernel;
|
||||
// Use K.load with (load-sxbc ...) wrapper — handles IO suspension in CEK
|
||||
window.__sxbcText = src;
|
||||
const r = K.load('(load-sxbc (first (parse (host-global "__sxbcText"))))');
|
||||
delete window.__sxbcText;
|
||||
if (typeof r === 'string' && r.startsWith('Error')) return r;
|
||||
return null;
|
||||
} catch(e) { delete window.__sxbcText; return 'CATCH: ' + e.message; }
|
||||
}, bcSrc);
|
||||
if (err) loadErrors.push(mod + '.sxbc: ' + err);
|
||||
else { loaded = true; loadedModules.push(mod + '.sxbc'); }
|
||||
}
|
||||
}
|
||||
// Fall back to .sx source
|
||||
if (!loaded) {
|
||||
const sxPath = path.join(SX_DIR, mod + '.sx');
|
||||
const libPath = path.join(PROJECT_ROOT, 'lib/hyperscript', mod.replace(/^hs-/, '') + '.sx');
|
||||
let src;
|
||||
try {
|
||||
src = fs.existsSync(sxPath)
|
||||
? fs.readFileSync(sxPath, 'utf8')
|
||||
: fs.readFileSync(libPath, 'utf8');
|
||||
} catch(e) {
|
||||
loadErrors.push(mod + ': file not found');
|
||||
continue;
|
||||
}
|
||||
const err = await page.evaluate(src => {
|
||||
try { window.SxKernel.load(src); return null; }
|
||||
catch(e) { return e.message; }
|
||||
}, src);
|
||||
if (err) loadErrors.push(mod + ': ' + err);
|
||||
else loadedModules.push(mod);
|
||||
}
|
||||
const err = await page.evaluate(src => {
|
||||
try { window.SxKernel.load(src); return null; }
|
||||
catch(e) { return e.message; }
|
||||
}, src);
|
||||
if (err) loadErrors.push(mod + ': ' + err);
|
||||
else loadedModules.push(mod);
|
||||
}
|
||||
await page.evaluate(() => {
|
||||
if (window.SxKernel.endModuleLoad) window.SxKernel.endModuleLoad();
|
||||
@@ -1549,11 +1571,22 @@ async function modeSandbox(page, expr, files, setup, stack) {
|
||||
window._asyncPending = 0;
|
||||
let syncResult;
|
||||
try {
|
||||
K.eval('(define _sandbox_thunk (fn () ' + expr + '))');
|
||||
syncResult = K.callFn(K.eval('_sandbox_thunk'), []);
|
||||
// First try K.eval directly (handles most expressions)
|
||||
syncResult = K.eval(expr);
|
||||
} catch(e) {
|
||||
resolve({ result: 'Error: ' + e.message, ioTrace: window._ioTrace });
|
||||
return;
|
||||
// If eval fails with IO suspension, retry via callFn thunk
|
||||
if (e.message && e.message.includes('suspension')) {
|
||||
try {
|
||||
K.eval('(define _sandbox_thunk (fn () ' + expr + '))');
|
||||
syncResult = K.callFn(K.eval('_sandbox_thunk'), []);
|
||||
} catch(e2) {
|
||||
resolve({ result: 'Error: ' + e2.message, ioTrace: window._ioTrace });
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
resolve({ result: 'Error: ' + e.message, ioTrace: window._ioTrace });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// If the thunk itself suspended (direct perform), drive it inline
|
||||
@@ -1688,7 +1721,7 @@ async function main() {
|
||||
result = await modeEvalAt(browser, url, args.phase || 'before-hydrate', args.expr || '(type-of ~cssx/tw)');
|
||||
break;
|
||||
case 'sandbox':
|
||||
result = await modeSandbox(page, args.expr || '"hello"', args.files || [], args.setup || '', args.stack || '');
|
||||
result = await modeSandbox(page, args.expr || '"hello"', args.files || [], args.setup || '', args.stack || '', !!args.bytecode);
|
||||
break;
|
||||
default:
|
||||
result = { error: `Unknown mode: ${mode}` };
|
||||
|
||||
Reference in New Issue
Block a user