js: fix lambda binding (index-of on lists), add vectors + R7RS platform stubs

- Fix PRIMITIVES["index-of"] for arrays: return NIL when not found (matching
  OCaml semantics) so bind-lambda-params correctly detects absent &rest params.
  Previously String(array).indexOf() returned -1, which passed number? check
  and mis-fired the &rest branch, leaving non-&rest params unbound.
- Declare var _lastErrorKont_ and var hostError in IIFE scope (strict mode fix)
- Add PRIMITIVES["host-error"], ["try-catch"], ["without-io-hook"]
- Add env["test-allowed?"] stub in run_tests.js
- Add spec/tests/test-vectors.sx: 42 tests for all vector primitives
- Rebuild sx-browser.js: 1847 standard / 2362 full tests pass (up from 5)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 10:02:23 +00:00
parent 5a332fa430
commit 1d85e3a79c
4 changed files with 777 additions and 117 deletions

View File

@@ -842,6 +842,13 @@ PREAMBLE = '''\
if (a === b) return true;
if (a && b && a._sym && b._sym) return a.name === b.name;
if (a && b && a._kw && b._kw) return a.name === b.name;
if (a && b && a._vector && b._vector) {
if (a.arr.length !== b.arr.length) return false;
for (var _i = 0; _i < a.arr.length; _i++) {
if (!sxEq(a.arr[_i], b.arr[_i])) return false;
}
return true;
}
return false;
}
@@ -908,6 +915,44 @@ PREAMBLE = '''\
function SxSpread(attrs) { this.attrs = attrs || {}; }
SxSpread.prototype._spread = true;
function SxVector(arr) { this.arr = arr || []; }
SxVector.prototype._vector = true;
var _paramUidCounter = 0;
function SxParameter(defaultVal, converter) {
this._uid = ++_paramUidCounter;
this._default = defaultVal;
this._converter = converter || null;
}
SxParameter.prototype._parameter = true;
function parameter_p(x) { return x != null && x._parameter === true; }
function parameterUid(p) { return p._uid; }
function parameterDefault(p) { return p._default; }
function SxCallccContinuation(capturedKont) { this._captured = capturedKont; }
SxCallccContinuation.prototype._callcc = true;
function makeCallccContinuation(kont) { return new SxCallccContinuation(kont); }
function callccContinuation_p(x) { return x != null && x._callcc === true; }
function callccContinuationData(x) { return x._captured; }
function evalError_p(v) {
return v != null && typeof v === "object" && v["__eval_error__"] === true;
}
function sxApplyCek(f, args) {
try {
return typeof f === "function" ? f.apply(null, args) : f;
} catch (e) {
if (e && e._perform_request) throw e;
if (e && e._cek_suspend) throw e;
return {"__eval_error__": true, "message": e && e.message ? e.message : String(e)};
}
}
var _JIT_SKIP_SENTINEL = {"__jit_skip": true};
function jitTryCall(f, args) { return _JIT_SKIP_SENTINEL; }
function jitSkip_p(v) { return v === _JIT_SKIP_SENTINEL || (v != null && v["__jit_skip"] === true); }
var _scopeStacks = {};
function isSym(x) { return x != null && x._sym === true; }
@@ -1004,7 +1049,20 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
PRIMITIVES["split"] = function(s, sep) { return String(s).split(sep || " "); };
PRIMITIVES["join"] = function(sep, coll) { return coll.join(sep); };
PRIMITIVES["replace"] = function(s, old, nw) { return s.split(old).join(nw); };
PRIMITIVES["index-of"] = function(s, needle, from) { return String(s).indexOf(needle, from || 0); };
PRIMITIVES["index-of"] = function(s, needle, from) {
if (Array.isArray(s)) {
var _start = from || 0;
for (var _i = _start; _i < s.length; _i++) {
var _a = s[_i];
if (_a === needle) return _i;
if (_a != null && needle != null && typeof _a === "object" && typeof needle === "object") {
if ((_a._sym && needle._sym || _a._kw && needle._kw) && _a.name === needle.name) return _i;
}
}
return NIL;
}
return String(s).indexOf(needle, from || 0);
};
PRIMITIVES["starts-with?"] = function(s, p) { return String(s).indexOf(p) === 0; };
PRIMITIVES["ends-with?"] = function(s, p) { var str = String(s); return str.indexOf(p, str.length - p.length) !== -1; };
PRIMITIVES["slice"] = function(c, a, b) { if (!c || typeof c.slice !== "function") { console.error("[sx-debug] slice called on non-sliceable:", typeof c, c, "a=", a, "b=", b, new Error().stack); return []; } return b !== undefined ? c.slice(a, b) : c.slice(a); };
@@ -1086,6 +1144,39 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
};
''',
"core.vectors": '''
// core.vectors — R7RS mutable fixed-size arrays
PRIMITIVES["make-vector"] = function(n, fill) {
var arr = new Array(n);
var f = (fill !== undefined) ? fill : NIL;
for (var i = 0; i < n; i++) arr[i] = f;
return new SxVector(arr);
};
PRIMITIVES["vector"] = function() {
return new SxVector(Array.prototype.slice.call(arguments));
};
PRIMITIVES["vector?"] = function(x) { return x != null && x._vector === true; };
PRIMITIVES["vector-length"] = function(v) { return v.arr.length; };
PRIMITIVES["vector-ref"] = function(v, i) {
if (i < 0 || i >= v.arr.length) throw new Error("vector-ref: index " + i + " out of bounds (length " + v.arr.length + ")");
return v.arr[i];
};
PRIMITIVES["vector-set!"] = function(v, i, val) {
if (i < 0 || i >= v.arr.length) throw new Error("vector-set!: index " + i + " out of bounds (length " + v.arr.length + ")");
v.arr[i] = val; return NIL;
};
PRIMITIVES["vector->list"] = function(v) { return v.arr.slice(); };
PRIMITIVES["list->vector"] = function(l) { return new SxVector(l.slice()); };
PRIMITIVES["vector-fill!"] = function(v, val) {
for (var i = 0; i < v.arr.length; i++) v.arr[i] = val; return NIL;
};
PRIMITIVES["vector-copy"] = function(v, start, end) {
var s = (start !== undefined) ? start : 0;
var e = (end !== undefined) ? Math.min(end, v.arr.length) : v.arr.length;
return new SxVector(v.arr.slice(s, e));
};
''',
"stdlib.format": '''
// stdlib.format
PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); };
@@ -1234,6 +1325,7 @@ PLATFORM_JS_PRE = '''
if (x._macro) return "macro";
if (x._raw) return "raw-html";
if (x._sx_expr) return "sx-expr";
if (x._vector) return "vector";
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
if (Array.isArray(x)) return "list";
if (typeof x === "object") return "dict";
@@ -1400,6 +1492,12 @@ PLATFORM_JS_PRE = '''
// Placeholder — overridden by transpiled version from render.sx
function isRenderExpr(expr) { return false; }
// Last error continuation — saved when a raise goes unhandled, for post-mortem inspection.
var _lastErrorKont_ = null;
// hostError — throw a host-level error that propagates out of cekRun.
function hostError(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); }
// Render dispatch — call the active adapter's render function.
// Set by each adapter when loaded; defaults to identity (no rendering).
var _renderExprFn = null;
@@ -1743,6 +1841,13 @@ CEK_FIXUPS_JS = '''
PRIMITIVES["lambda-name"] = lambdaName;
PRIMITIVES["component?"] = isComponent;
PRIMITIVES["island?"] = isIsland;
PRIMITIVES["parameter?"] = parameter_p;
PRIMITIVES["parameter-uid"] = parameterUid;
PRIMITIVES["parameter-default"] = parameterDefault;
PRIMITIVES["make-parameter"] = function(defaultVal, converter) {
var p = new SxParameter(defaultVal, converter || null);
return p;
};
PRIMITIVES["make-symbol"] = function(n) { return new Symbol(n); };
PRIMITIVES["is-html-tag?"] = function(n) { return HTML_TAGS.indexOf(n) >= 0; };
function makeEnv() { return merge(componentEnv, PRIMITIVES); }
@@ -2031,7 +2136,7 @@ PLATFORM_DOM_JS = """
}
function domDispatch(el, name, detail) {
if (!_hasDom || !el) return false;
if (!_hasDom || !el || typeof el.dispatchEvent !== "function") return false;
var evt = new CustomEvent(name, { bubbles: true, cancelable: true, detail: detail || {} });
return el.dispatchEvent(evt);
}
@@ -2157,6 +2262,14 @@ PLATFORM_ORCHESTRATION_JS = """
// Platform interface — Orchestration (browser-only)
// =========================================================================
// --- Stubs for define-library functions not transpiled by extract_defines ---
// These are defined in orchestration.sx's define-library and called from
// boot.sx top-level defines. The JS bootstrapper only transpiles top-level
// defines, so we provide stubs here for functions that need a JS identity.
function flushCollectedStyles() { return NIL; }
function processElements(root) { return NIL; }
// --- Browser/Network ---
function browserNavigate(url) {
@@ -2642,6 +2755,10 @@ PLATFORM_ORCHESTRATION_JS = """
return el && el.closest ? el.closest(sel) : null;
}
function domDocument() {
return _hasDom ? document : null;
}
function domBody() {
return _hasDom ? document.body : null;
}
@@ -3085,6 +3202,8 @@ PLATFORM_BOOT_JS = """
// Platform interface — Boot (mount, hydrate, scripts, cookies)
// =========================================================================
function preloadIslandDefs() { return NIL; }
function resolveMountTarget(target) {
if (typeof target === "string") return _hasDom ? document.querySelector(target) : null;
return target;
@@ -3237,6 +3356,18 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
// Core primitives that require native JS (cannot be expressed via FFI)
// -----------------------------------------------------------------------
PRIMITIVES["error"] = function(msg) { throw new Error(msg); };
PRIMITIVES["host-error"] = function(msg) { throw new Error(typeof msg === "string" ? msg : inspect(msg)); };
PRIMITIVES["try-catch"] = function(tryFn, catchFn) {
try {
return cekRun(continueWithCall(tryFn, [], makeEnv(), [], []));
} catch(e) {
var msg = e && e.message ? e.message : String(e);
return cekRun(continueWithCall(catchFn, [msg], makeEnv(), [msg], []));
}
};
PRIMITIVES["without-io-hook"] = function(thunk) {
return cekRun(continueWithCall(thunk, [], makeEnv(), [], []));
};
PRIMITIVES["sort"] = function(lst) {
if (!Array.isArray(lst)) return lst;
return lst.slice().sort(function(a, b) {
@@ -3304,7 +3435,7 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_
PRIMITIVES["dom-tag-name"] = domTagName;
PRIMITIVES["dom-get-prop"] = domGetProp;
PRIMITIVES["dom-set-prop"] = domSetProp;
PRIMITIVES["reactive-text"] = reactiveText;
if (typeof reactiveText === "function") PRIMITIVES["reactive-text"] = reactiveText;
PRIMITIVES["set-interval"] = setInterval_;
PRIMITIVES["clear-interval"] = clearInterval_;
PRIMITIVES["promise-then"] = promiseThen;
@@ -3493,35 +3624,35 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
elif has_orch:
api_lines.append(' init: typeof engineInit === "function" ? engineInit : null,')
if has_deps:
api_lines.append(' scanRefs: scanRefs,')
api_lines.append(' scanComponentsFromSource: scanComponentsFromSource,')
api_lines.append(' transitiveDeps: transitiveDeps,')
api_lines.append(' computeAllDeps: computeAllDeps,')
api_lines.append(' componentsNeeded: componentsNeeded,')
api_lines.append(' pageComponentBundle: pageComponentBundle,')
api_lines.append(' pageCssClasses: pageCssClasses,')
api_lines.append(' scanIoRefs: scanIoRefs,')
api_lines.append(' transitiveIoRefs: transitiveIoRefs,')
api_lines.append(' computeAllIoRefs: computeAllIoRefs,')
api_lines.append(' componentPure_p: componentPure_p,')
api_lines.append(' scanRefs: typeof scanRefs === "function" ? scanRefs : null,')
api_lines.append(' scanComponentsFromSource: typeof scanComponentsFromSource === "function" ? scanComponentsFromSource : null,')
api_lines.append(' transitiveDeps: typeof transitiveDeps === "function" ? transitiveDeps : null,')
api_lines.append(' computeAllDeps: typeof computeAllDeps === "function" ? computeAllDeps : null,')
api_lines.append(' componentsNeeded: typeof componentsNeeded === "function" ? componentsNeeded : null,')
api_lines.append(' pageComponentBundle: typeof pageComponentBundle === "function" ? pageComponentBundle : null,')
api_lines.append(' pageCssClasses: typeof pageCssClasses === "function" ? pageCssClasses : null,')
api_lines.append(' scanIoRefs: typeof scanIoRefs === "function" ? scanIoRefs : null,')
api_lines.append(' transitiveIoRefs: typeof transitiveIoRefs === "function" ? transitiveIoRefs : null,')
api_lines.append(' computeAllIoRefs: typeof computeAllIoRefs === "function" ? computeAllIoRefs : null,')
api_lines.append(' componentPure_p: typeof componentPure_p === "function" ? componentPure_p : null,')
if has_page_helpers:
api_lines.append(' categorizeSpecialForms: categorizeSpecialForms,')
api_lines.append(' buildReferenceData: buildReferenceData,')
api_lines.append(' buildAttrDetail: buildAttrDetail,')
api_lines.append(' buildHeaderDetail: buildHeaderDetail,')
api_lines.append(' buildEventDetail: buildEventDetail,')
api_lines.append(' buildComponentSource: buildComponentSource,')
api_lines.append(' buildBundleAnalysis: buildBundleAnalysis,')
api_lines.append(' buildRoutingAnalysis: buildRoutingAnalysis,')
api_lines.append(' buildAffinityAnalysis: buildAffinityAnalysis,')
api_lines.append(' categorizeSpecialForms: typeof categorizeSpecialForms === "function" ? categorizeSpecialForms : null,')
api_lines.append(' buildReferenceData: typeof buildReferenceData === "function" ? buildReferenceData : null,')
api_lines.append(' buildAttrDetail: typeof buildAttrDetail === "function" ? buildAttrDetail : null,')
api_lines.append(' buildHeaderDetail: typeof buildHeaderDetail === "function" ? buildHeaderDetail : null,')
api_lines.append(' buildEventDetail: typeof buildEventDetail === "function" ? buildEventDetail : null,')
api_lines.append(' buildComponentSource: typeof buildComponentSource === "function" ? buildComponentSource : null,')
api_lines.append(' buildBundleAnalysis: typeof buildBundleAnalysis === "function" ? buildBundleAnalysis : null,')
api_lines.append(' buildRoutingAnalysis: typeof buildRoutingAnalysis === "function" ? buildRoutingAnalysis : null,')
api_lines.append(' buildAffinityAnalysis: typeof buildAffinityAnalysis === "function" ? buildAffinityAnalysis : null,')
if has_router:
api_lines.append(' splitPathSegments: splitPathSegments,')
api_lines.append(' parseRoutePattern: parseRoutePattern,')
api_lines.append(' matchRoute: matchRoute,')
api_lines.append(' findMatchingRoute: findMatchingRoute,')
api_lines.append(' urlToExpr: urlToExpr,')
api_lines.append(' autoQuoteUnknowns: autoQuoteUnknowns,')
api_lines.append(' prepareUrlExpr: prepareUrlExpr,')
api_lines.append(' splitPathSegments: typeof splitPathSegments === "function" ? splitPathSegments : null,')
api_lines.append(' parseRoutePattern: typeof parseRoutePattern === "function" ? parseRoutePattern : null,')
api_lines.append(' matchRoute: typeof matchRoute === "function" ? matchRoute : null,')
api_lines.append(' findMatchingRoute: typeof findMatchingRoute === "function" ? findMatchingRoute : null,')
api_lines.append(' urlToExpr: typeof urlToExpr === "function" ? urlToExpr : null,')
api_lines.append(' autoQuoteUnknowns: typeof autoQuoteUnknowns === "function" ? autoQuoteUnknowns : null,')
api_lines.append(' prepareUrlExpr: typeof prepareUrlExpr === "function" ? prepareUrlExpr : null,')
if has_dom:
api_lines.append(' registerIo: typeof registerIoPrimitive === "function" ? registerIoPrimitive : null,')
@@ -3529,21 +3660,21 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
api_lines.append(' asyncRender: typeof asyncSxRenderWithEnv === "function" ? asyncSxRenderWithEnv : null,')
api_lines.append(' asyncRenderToDom: typeof asyncRenderToDom === "function" ? asyncRenderToDom : null,')
if has_signals:
api_lines.append(' signal: signal,')
api_lines.append(' deref: deref,')
api_lines.append(' reset: reset_b,')
api_lines.append(' swap: swap_b,')
api_lines.append(' computed: computed,')
api_lines.append(' effect: effect,')
api_lines.append(' batch: batch,')
api_lines.append(' isSignal: isSignal,')
api_lines.append(' makeSignal: makeSignal,')
api_lines.append(' defStore: defStore,')
api_lines.append(' useStore: useStore,')
api_lines.append(' clearStores: clearStores,')
api_lines.append(' emitEvent: emitEvent,')
api_lines.append(' onEvent: onEvent,')
api_lines.append(' bridgeEvent: bridgeEvent,')
api_lines.append(' signal: typeof signal === "function" ? signal : null,')
api_lines.append(' deref: typeof deref === "function" ? deref : null,')
api_lines.append(' reset: typeof reset_b === "function" ? reset_b : null,')
api_lines.append(' swap: typeof swap_b === "function" ? swap_b : null,')
api_lines.append(' computed: typeof computed === "function" ? computed : null,')
api_lines.append(' effect: typeof effect === "function" ? effect : null,')
api_lines.append(' batch: typeof batch === "function" ? batch : null,')
api_lines.append(' isSignal: typeof isSignal === "function" ? isSignal : null,')
api_lines.append(' makeSignal: typeof makeSignal === "function" ? makeSignal : null,')
api_lines.append(' defStore: typeof defStore === "function" ? defStore : null,')
api_lines.append(' useStore: typeof useStore === "function" ? useStore : null,')
api_lines.append(' clearStores: typeof clearStores === "function" ? clearStores : null,')
api_lines.append(' emitEvent: typeof emitEvent === "function" ? emitEvent : null,')
api_lines.append(' onEvent: typeof onEvent === "function" ? onEvent : null,')
api_lines.append(' bridgeEvent: typeof bridgeEvent === "function" ? bridgeEvent : null,')
api_lines.append(' makeSpread: makeSpread,')
api_lines.append(' isSpread: isSpread,')
api_lines.append(' spreadAttrs: spreadAttrs,')

View File

@@ -293,6 +293,8 @@ env["pop-suite"] = function() {
return null;
};
env["test-allowed?"] = function(name) { return true; };
// Load test framework
const projectDir = path.join(__dirname, "..", "..");
const specTests = path.join(projectDir, "spec", "tests");