Add host FFI primitives and web/lib DOM+browser libraries

Introduce 8 irreducible host FFI primitives that replace 40+ native DOM
and browser primitives:

  host-global    — access global object (window/document)
  host-get       — read property from host object
  host-set!      — write property on host object
  host-call      — call method on host object
  host-new       — construct host object
  host-callback  — wrap SX function as host callback
  host-typeof    — check host object type
  host-await     — await host promise

All DOM and browser operations are now expressible as SX library
functions built on these 8 primitives:

  web/lib/dom.sx     — createElement, querySelector, appendChild,
                        setAttribute, addEventListener, classList, etc.
  web/lib/browser.sx — localStorage, history, fetch, setTimeout,
                        promises, console, matchMedia, etc.

The existing native implementations remain as fallback — the library
versions shadow them in transpiled code. Incremental migration: callers
don't change, only the implementation moves from out-of-band to in-band.

JS 957+1080, Python 744, OCaml 952 — zero regressions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 09:22:57 +00:00
parent 4ce4762237
commit 4308591982
4 changed files with 562 additions and 0 deletions

View File

@@ -38,6 +38,8 @@ ADAPTER_FILES = {
"parser": ("parser.sx", "parser"),
"html": ("adapter-html.sx", "adapter-html"),
"sx": ("adapter-sx.sx", "adapter-sx"),
"dom-lib": ("lib/dom.sx", "lib/dom (DOM library)"),
"browser-lib": ("lib/browser.sx", "lib/browser (browser API library)"),
"dom": ("adapter-dom.sx", "adapter-dom"),
"engine": ("engine.sx", "engine"),
"orchestration": ("orchestration.sx","orchestration"),
@@ -46,6 +48,9 @@ ADAPTER_FILES = {
# Dependencies
ADAPTER_DEPS = {
"dom-lib": [],
"browser-lib": ["dom-lib"],
"dom": ["dom-lib", "browser-lib"],
"engine": ["dom"],
"orchestration": ["engine", "dom"],
"boot": ["dom", "engine", "orchestration", "parser"],
@@ -1435,6 +1440,87 @@ PLATFORM_JS_POST = '''
function isSpecialForm(n) { return false; }
function isHoForm(n) { return false; }
// -----------------------------------------------------------------------
// Host FFI — the irreducible web platform primitives
// All DOM/browser operations are built on these in web/lib/dom.sx
// -----------------------------------------------------------------------
PRIMITIVES["host-global"] = function(name) {
if (typeof globalThis !== "undefined" && name in globalThis) return globalThis[name];
if (typeof window !== "undefined" && name in window) return window[name];
return NIL;
};
PRIMITIVES["host-get"] = function(obj, prop) {
if (obj == null || obj === NIL) return NIL;
var v = obj[prop];
return v === undefined || v === null ? NIL : v;
};
PRIMITIVES["host-set!"] = function(obj, prop, val) {
if (obj != null && obj !== NIL) obj[prop] = val === NIL ? null : val;
};
PRIMITIVES["host-call"] = function() {
var obj = arguments[0], method = arguments[1];
var args = [];
for (var i = 2; i < arguments.length; i++) {
var a = arguments[i];
args.push(a === NIL ? null : a);
}
if (obj == null || obj === NIL) {
// Global function call
var fn = typeof globalThis !== "undefined" ? globalThis[method] : window[method];
if (typeof fn === "function") return fn.apply(null, args);
return NIL;
}
if (typeof obj[method] === "function") {
try { return obj[method].apply(obj, args); }
catch(e) { return NIL; }
}
return NIL;
};
PRIMITIVES["host-new"] = function() {
var name = arguments[0];
var args = Array.prototype.slice.call(arguments, 1).map(function(a) { return a === NIL ? null : a; });
var Ctor = typeof globalThis !== "undefined" ? globalThis[name] : window[name];
if (typeof Ctor !== "function") return NIL;
// Support 0-4 args (covers all practical cases)
switch (args.length) {
case 0: return new Ctor();
case 1: return new Ctor(args[0]);
case 2: return new Ctor(args[0], args[1]);
case 3: return new Ctor(args[0], args[1], args[2]);
default: return new Ctor(args[0], args[1], args[2], args[3]);
}
};
PRIMITIVES["host-callback"] = function(fn) {
// Wrap SX function/lambda as a native JS callback
if (typeof fn === "function") return fn;
if (fn && fn._type === "lambda") {
return function() {
var a = Array.prototype.slice.call(arguments);
return cekCall(fn, a);
};
}
return function() {};
};
PRIMITIVES["host-typeof"] = function(obj) {
if (obj == null || obj === NIL) return "nil";
if (obj instanceof Element) return "element";
if (obj instanceof Text) return "text";
if (obj instanceof DocumentFragment) return "fragment";
if (obj instanceof Document) return "document";
if (obj instanceof Event) return "event";
if (obj instanceof Promise) return "promise";
if (obj instanceof AbortController) return "abort-controller";
return typeof obj;
};
PRIMITIVES["host-await"] = function(promise, callback) {
if (promise && typeof promise.then === "function") {
var cb = typeof callback === "function" ? callback :
(callback && callback._type === "lambda") ?
function(v) { return cekCall(callback, [v]); } : function() {};
promise.then(cb);
}
};
// processBindings and evalCond — now specced in render.sx, bootstrapped above
function isDefinitionForm(name) {