From e1ae81f736366b4896a3eced32043cb3be1c9a7d Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 5 Mar 2026 09:58:48 +0000 Subject: [PATCH] =?UTF-8?q?Add=20bootstrap=20compiler:=20reference=20SX=20?= =?UTF-8?q?spec=20=E2=86=92=20JavaScript?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bootstrap_js.py reads the reference .sx specification (eval.sx, render.sx) and transpiles the defined evaluator functions into standalone JavaScript. The output sx-ref.js is a fully functional SX evaluator bootstrapped from the s-expression spec, comparable against the hand-written sx.js. Key features: - JSEmitter class transpiles SX AST → JS (fn→function, let→IIFE, cond→ternary, etc.) - Platform interface (types, env ops, primitives) implemented as native JS - Post-transpilation fixup wraps callLambda to handle both Lambda objects and primitives - 93/93 tests passing: arithmetic, strings, control flow, closures, HO forms, components, macros, threading, dict ops, predicates Fixed during development: - Bool before int isinstance check (Python bool is subclass of int) - SX NIL sentinel detection (not Python None) - Cond style detection (determine Scheme vs Clojure once, not per-pair) - Predicate null safety (x != null instead of x && to avoid 0-as-falsy in SX) Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-ref.js | 949 +++++++++++++++++++++++++++ shared/sx/ref/bootstrap_js.py | 1070 +++++++++++++++++++++++++++++++ 2 files changed, 2019 insertions(+) create mode 100644 shared/static/scripts/sx-ref.js create mode 100644 shared/sx/ref/bootstrap_js.py diff --git a/shared/static/scripts/sx-ref.js b/shared/static/scripts/sx-ref.js new file mode 100644 index 0000000..324f0d9 --- /dev/null +++ b/shared/static/scripts/sx-ref.js @@ -0,0 +1,949 @@ +/** + * sx-ref.js — Generated from reference SX evaluator specification. + * + * Bootstrap-compiled from shared/sx/ref/{eval,render,primitives}.sx + * Compare against hand-written sx.js for correctness verification. + * + * DO NOT EDIT — regenerate with: python bootstrap_js.py + */ +;(function(global) { + "use strict"; + + // ========================================================================= + // Types + // ========================================================================= + + var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); + + function isNil(x) { return x === NIL || x === null || x === undefined; } + function isSxTruthy(x) { return x !== false && !isNil(x); } + + function Symbol(name) { this.name = name; } + Symbol.prototype.toString = function() { return this.name; }; + Symbol.prototype._sym = true; + + function Keyword(name) { this.name = name; } + Keyword.prototype.toString = function() { return ":" + this.name; }; + Keyword.prototype._kw = true; + + function Lambda(params, body, closure, name) { + this.params = params; + this.body = body; + this.closure = closure || {}; + this.name = name || null; + } + Lambda.prototype._lambda = true; + + function Component(name, params, hasChildren, body, closure) { + this.name = name; + this.params = params; + this.hasChildren = hasChildren; + this.body = body; + this.closure = closure || {}; + } + Component.prototype._component = true; + + function Macro(params, restParam, body, closure, name) { + this.params = params; + this.restParam = restParam; + this.body = body; + this.closure = closure || {}; + this.name = name || null; + } + Macro.prototype._macro = true; + + function Thunk(expr, env) { this.expr = expr; this.env = env; } + Thunk.prototype._thunk = true; + + function RawHTML(html) { this.html = html; } + RawHTML.prototype._raw = true; + + function isSym(x) { return x != null && x._sym === true; } + function isKw(x) { return x != null && x._kw === true; } + + function merge() { + var out = {}; + for (var i = 0; i < arguments.length; i++) { + var d = arguments[i]; + if (d) for (var k in d) out[k] = d[k]; + } + return out; + } + + function sxOr() { + for (var i = 0; i < arguments.length; i++) { + if (isSxTruthy(arguments[i])) return arguments[i]; + } + return arguments.length ? arguments[arguments.length - 1] : false; + } + + // ========================================================================= + // Platform interface — JS implementation + // ========================================================================= + + function typeOf(x) { + if (isNil(x)) return "nil"; + if (typeof x === "number") return "number"; + if (typeof x === "string") return "string"; + if (typeof x === "boolean") return "boolean"; + if (x._sym) return "symbol"; + if (x._kw) return "keyword"; + if (x._thunk) return "thunk"; + if (x._lambda) return "lambda"; + if (x._component) return "component"; + if (x._macro) return "macro"; + if (x._raw) return "raw-html"; + if (Array.isArray(x)) return "list"; + if (typeof x === "object") return "dict"; + return "unknown"; + } + + function symbolName(s) { return s.name; } + function keywordName(k) { return k.name; } + function makeSymbol(n) { return new Symbol(n); } + function makeKeyword(n) { return new Keyword(n); } + + function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); } + function makeComponent(name, params, hasChildren, body, env) { + return new Component(name, params, hasChildren, body, merge(env)); + } + function makeMacro(params, restParam, body, env, name) { + return new Macro(params, restParam, body, merge(env), name); + } + function makeThunk(expr, env) { return new Thunk(expr, env); } + + function lambdaParams(f) { return f.params; } + function lambdaBody(f) { return f.body; } + function lambdaClosure(f) { return f.closure; } + function lambdaName(f) { return f.name; } + function setLambdaName(f, n) { f.name = n; } + + function componentParams(c) { return c.params; } + function componentBody(c) { return c.body; } + function componentClosure(c) { return c.closure; } + function componentHasChildren(c) { return c.hasChildren; } + function componentName(c) { return c.name; } + + function macroParams(m) { return m.params; } + function macroRestParam(m) { return m.restParam; } + function macroBody(m) { return m.body; } + function macroClosure(m) { return m.closure; } + + function isThunk(x) { return x != null && x._thunk === true; } + function thunkExpr(t) { return t.expr; } + function thunkEnv(t) { return t.env; } + + function isCallable(x) { return typeof x === "function" || (x != null && x._lambda === true); } + function isLambda(x) { return x != null && x._lambda === true; } + function isComponent(x) { return x != null && x._component === true; } + function isMacro(x) { return x != null && x._macro === true; } + + function envHas(env, name) { return name in env; } + function envGet(env, name) { return env[name]; } + function envSet(env, name, val) { env[name] = val; } + function envExtend(env) { return merge(env); } + function envMerge(base, overlay) { return merge(base, overlay); } + + function dictSet(d, k, v) { d[k] = v; } + function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; } + + function stripPrefix(s, prefix) { + return s.indexOf(prefix) === 0 ? s.slice(prefix.length) : s; + } + + function error(msg) { throw new Error(msg); } + function inspect(x) { return JSON.stringify(x); } + + // ========================================================================= + // Primitives + // ========================================================================= + + var PRIMITIVES = {}; + + // Arithmetic + PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; }; + PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; }; + PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; }; + PRIMITIVES["/"] = function(a, b) { return a / b; }; + PRIMITIVES["mod"] = function(a, b) { return a % b; }; + PRIMITIVES["inc"] = function(n) { return n + 1; }; + PRIMITIVES["dec"] = function(n) { return n - 1; }; + PRIMITIVES["abs"] = Math.abs; + PRIMITIVES["floor"] = Math.floor; + PRIMITIVES["ceil"] = Math.ceil; + PRIMITIVES["round"] = Math.round; + PRIMITIVES["min"] = Math.min; + PRIMITIVES["max"] = Math.max; + PRIMITIVES["sqrt"] = Math.sqrt; + PRIMITIVES["pow"] = Math.pow; + PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; + + // Comparison + PRIMITIVES["="] = function(a, b) { return a == b; }; + PRIMITIVES["!="] = function(a, b) { return a != b; }; + PRIMITIVES["<"] = function(a, b) { return a < b; }; + PRIMITIVES[">"] = function(a, b) { return a > b; }; + PRIMITIVES["<="] = function(a, b) { return a <= b; }; + PRIMITIVES[">="] = function(a, b) { return a >= b; }; + + // Logic + PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); }; + + // String + PRIMITIVES["str"] = function() { + var p = []; + for (var i = 0; i < arguments.length; i++) { + var v = arguments[i]; if (isNil(v)) continue; p.push(String(v)); + } + return p.join(""); + }; + PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); }; + PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); }; + PRIMITIVES["trim"] = function(s) { return String(s).trim(); }; + 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["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) { return b !== undefined ? c.slice(a, b) : c.slice(a); }; + PRIMITIVES["concat"] = function() { + var out = []; + for (var i = 0; i < arguments.length; i++) if (arguments[i]) out = out.concat(arguments[i]); + return out; + }; + PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); }; + + // Predicates + PRIMITIVES["nil?"] = isNil; + PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; + PRIMITIVES["string?"] = function(x) { return typeof x === "string"; }; + PRIMITIVES["list?"] = Array.isArray; + PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; }; + PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); }; + PRIMITIVES["contains?"] = function(c, k) { + if (typeof c === "string") return c.indexOf(String(k)) !== -1; + if (Array.isArray(c)) return c.indexOf(k) !== -1; + return k in c; + }; + PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; }; + PRIMITIVES["even?"] = function(n) { return n % 2 === 0; }; + PRIMITIVES["zero?"] = function(n) { return n === 0; }; + + // Collections + PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); }; + PRIMITIVES["dict"] = function() { + var d = {}; + for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1]; + return d; + }; + PRIMITIVES["range"] = function(a, b, step) { + var r = []; step = step || 1; + for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i); + return r; + }; + PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); }; + PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; }; + PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; }; + PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; }; + PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; }; + PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; }; + PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); }; + PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); }; + PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); }; + PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; }; + PRIMITIVES["merge"] = function() { + var out = {}; + for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; } + return out; + }; + PRIMITIVES["assoc"] = function(d) { + var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; + for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1]; + return out; + }; + PRIMITIVES["dissoc"] = function(d) { + var out = {}; for (var k in d) out[k] = d[k]; + for (var i = 1; i < arguments.length; i++) delete out[arguments[i]]; + return out; + }; + PRIMITIVES["chunk-every"] = function(c, n) { + var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r; + }; + PRIMITIVES["zip-pairs"] = function(c) { + var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r; + }; + PRIMITIVES["into"] = function(target, coll) { + if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll); + var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; } + return r; + }; + + // Format + PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); }; + PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; }; + PRIMITIVES["pluralize"] = function(n, s, p) { + if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s"); + return n == 1 ? "" : "s"; + }; + PRIMITIVES["escape"] = function(s) { + return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); + }; + + function isPrimitive(name) { return name in PRIMITIVES; } + function getPrimitive(name) { return PRIMITIVES[name]; } + + // Higher-order helpers used by the transpiled code + function map(fn, coll) { return coll.map(fn); } + function mapIndexed(fn, coll) { return coll.map(function(item, i) { return fn(i, item); }); } + function filter(fn, coll) { return coll.filter(function(x) { return isSxTruthy(fn(x)); }); } + function reduce(fn, init, coll) { + var acc = init; + for (var i = 0; i < coll.length; i++) acc = fn(acc, coll[i]); + return acc; + } + function some(fn, coll) { + for (var i = 0; i < coll.length; i++) { var r = fn(coll[i]); if (isSxTruthy(r)) return r; } + return NIL; + } + function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; } + function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } + + // List primitives used directly by transpiled code + var len = PRIMITIVES["len"]; + var first = PRIMITIVES["first"]; + var last = PRIMITIVES["last"]; + var rest = PRIMITIVES["rest"]; + var nth = PRIMITIVES["nth"]; + var cons = PRIMITIVES["cons"]; + var append = PRIMITIVES["append"]; + var isEmpty = PRIMITIVES["empty?"]; + var contains = PRIMITIVES["contains?"]; + var startsWith = PRIMITIVES["starts-with?"]; + var slice = PRIMITIVES["slice"]; + var concat = PRIMITIVES["concat"]; + var str = PRIMITIVES["str"]; + var join = PRIMITIVES["join"]; + var keys = PRIMITIVES["keys"]; + var get = PRIMITIVES["get"]; + var assoc = PRIMITIVES["assoc"]; + var range = PRIMITIVES["range"]; + function zip(a, b) { var r = []; for (var i = 0; i < Math.min(a.length, b.length); i++) r.push([a[i], b[i]]); return r; } + function append_b(arr, x) { arr.push(x); return arr; } + var apply = function(f, args) { return f.apply(null, args); }; + + // HTML rendering helpers + function escapeHtml(s) { + return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); + } + function escapeAttr(s) { return escapeHtml(s); } + function rawHtmlContent(r) { return r.html; } + + // Serializer + function serialize(val) { + if (isNil(val)) return "nil"; + if (typeof val === "boolean") return val ? "true" : "false"; + if (typeof val === "number") return String(val); + if (typeof val === "string") return '"' + val.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + '"'; + if (isSym(val)) return val.name; + if (isKw(val)) return ":" + val.name; + if (Array.isArray(val)) return "(" + val.map(serialize).join(" ") + ")"; + return String(val); + } + + function isSpecialForm(n) { return n in { + "if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1, + "lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"begin":1,"do":1, + "quote":1,"quasiquote":1,"->":1,"set!":1 + }; } + function isHoForm(n) { return n in { + "map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1 + }; } + + // === Transpiled from eval.sx === + + // trampoline + var trampoline = function(val) { return (function() { + var result = val; + return (isSxTruthy(isThunk(result)) ? trampoline(evalExpr(thunkExpr(result), thunkEnv(result))) : result); +})(); }; + + // eval-expr + var evalExpr = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { + var name = symbolName(expr); + return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name)))))))); +})(); if (_m == "keyword") return keywordName(expr); if (_m == "dict") return mapDict(function(k, v) { return [k, trampoline(evalExpr(v, env))]; }, expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : evalList(expr, env)); return expr; })(); }; + + // eval-list + var evalList = function(expr, env) { return (function() { + var head = first(expr); + var args = rest(expr); + return (isSxTruthy(!sxOr((typeOf(head) == "symbol"), (typeOf(head) == "lambda"), (typeOf(head) == "list"))) ? map(function(x) { return trampoline(evalExpr(x, env)); }, expr) : (isSxTruthy((typeOf(head) == "symbol")) ? (function() { + var name = symbolName(head); + return (isSxTruthy((name == "if")) ? sfIf(args, env) : (isSxTruthy((name == "when")) ? sfWhen(args, env) : (isSxTruthy((name == "cond")) ? sfCond(args, env) : (isSxTruthy((name == "case")) ? sfCase(args, env) : (isSxTruthy((name == "and")) ? sfAnd(args, env) : (isSxTruthy((name == "or")) ? sfOr(args, env) : (isSxTruthy((name == "let")) ? sfLet(args, env) : (isSxTruthy((name == "let*")) ? sfLet(args, env) : (isSxTruthy((name == "lambda")) ? sfLambda(args, env) : (isSxTruthy((name == "fn")) ? sfLambda(args, env) : (isSxTruthy((name == "define")) ? sfDefine(args, env) : (isSxTruthy((name == "defcomp")) ? sfDefcomp(args, env) : (isSxTruthy((name == "defmacro")) ? sfDefmacro(args, env) : (isSxTruthy((name == "begin")) ? sfBegin(args, env) : (isSxTruthy((name == "do")) ? sfBegin(args, env) : (isSxTruthy((name == "quote")) ? sfQuote(args, env) : (isSxTruthy((name == "quasiquote")) ? sfQuasiquote(args, env) : (isSxTruthy((name == "->")) ? sfThreadFirst(args, env) : (isSxTruthy((name == "set!")) ? sfSetBang(args, env) : (isSxTruthy((name == "map")) ? hoMap(args, env) : (isSxTruthy((name == "map-indexed")) ? hoMapIndexed(args, env) : (isSxTruthy((name == "filter")) ? hoFilter(args, env) : (isSxTruthy((name == "reduce")) ? hoReduce(args, env) : (isSxTruthy((name == "some")) ? hoSome(args, env) : (isSxTruthy((name == "every?")) ? hoEvery(args, env) : (isSxTruthy((name == "for-each")) ? hoForEach(args, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? (function() { + var mac = envGet(env, name); + return makeThunk(expandMacro(mac, args, env), env); +})() : evalCall(head, args, env)))))))))))))))))))))))))))); +})() : evalCall(head, args, env))); +})(); }; + + // eval-call + var evalCall = function(head, args, env) { return (function() { + var f = trampoline(evalExpr(head, env)); + var evaluatedArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args); + return (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isLambda(f)) && !isComponent(f))) ? apply(f, evaluatedArgs) : (isSxTruthy(isLambda(f)) ? callLambda(f, evaluatedArgs, env) : (isSxTruthy(isComponent(f)) ? callComponent(f, args, env) : error((String("Not callable: ") + String(inspect(f))))))); +})(); }; + + // call-lambda + var callLambda = function(f, args, callerEnv) { return (function() { + var params = lambdaParams(f); + var local = envMerge(lambdaClosure(f), callerEnv); + return (isSxTruthy((len(args) != len(params))) ? error((String(sxOr(lambdaName(f), "lambda")) + String(" expects ") + String(len(params)) + String(" args, got ") + String(len(args)))) : (forEach(function(pair) { return envSet(local, first(pair), nth(pair, 1)); }, zip(params, args)), makeThunk(lambdaBody(f), local))); +})(); }; + + // call-component + var callComponent = function(comp, rawArgs, env) { return (function() { + var parsed = parseKeywordArgs(rawArgs, env); + var kwargs = first(parsed); + var children = nth(parsed, 1); + var local = envMerge(componentClosure(comp), env); + { var _c = componentParams(comp); for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; local[p] = sxOr(dictGet(kwargs, p), NIL); } } + if (isSxTruthy(componentHasChildren(comp))) { + local["children"] = children; +} + return makeThunk(componentBody(comp), local); +})(); }; + + // parse-keyword-args + var parseKeywordArgs = function(rawArgs, env) { return (function() { + var kwargs = {}; + var children = []; + var i = 0; + reduce(function(state, arg) { return (function() { + var idx = get(state, "i"); + var skip = get(state, "skip"); + return (isSxTruthy(skip) ? assoc(state, "skip", false, "i", (idx + 1)) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((idx + 1) < len(rawArgs)))) ? (dictSet(kwargs, keywordName(arg), trampoline(evalExpr(nth(rawArgs, (idx + 1)), env))), assoc(state, "skip", true, "i", (idx + 1))) : (append_b(children, trampoline(evalExpr(arg, env))), assoc(state, "i", (idx + 1))))); +})(); }, {["i"]: 0, ["skip"]: false}, rawArgs); + return [kwargs, children]; +})(); }; + + // sf-if + var sfIf = function(args, env) { return (function() { + var condition = trampoline(evalExpr(first(args), env)); + return (isSxTruthy((isSxTruthy(condition) && !isNil(condition))) ? makeThunk(nth(args, 1), env) : (isSxTruthy((len(args) > 2)) ? makeThunk(nth(args, 2), env) : NIL)); +})(); }; + + // sf-when + var sfWhen = function(args, env) { return (function() { + var condition = trampoline(evalExpr(first(args), env)); + return (isSxTruthy((isSxTruthy(condition) && !isNil(condition))) ? (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 1, (len(args) - 1))), makeThunk(last(args), env)) : NIL); +})(); }; + + // sf-cond + var sfCond = function(args, env) { return (isSxTruthy((isSxTruthy((typeOf(first(args)) == "list")) && (len(first(args)) == 2))) ? sfCondScheme(args, env) : sfCondClojure(args, env)); }; + + // sf-cond-scheme + var sfCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() { + var clause = first(clauses); + var test = first(clause); + var body = nth(clause, 1); + return (isSxTruthy(sxOr((isSxTruthy((typeOf(test) == "symbol")) && sxOr((symbolName(test) == "else"), (symbolName(test) == ":else"))), (isSxTruthy((typeOf(test) == "keyword")) && (keywordName(test) == "else")))) ? makeThunk(body, env) : (isSxTruthy(trampoline(evalExpr(test, env))) ? makeThunk(body, env) : sfCondScheme(rest(clauses), env))); +})()); }; + + // sf-cond-clojure + var sfCondClojure = function(clauses, env) { return (isSxTruthy((len(clauses) < 2)) ? NIL : (function() { + var test = first(clauses); + var body = nth(clauses, 1); + return (isSxTruthy(sxOr((isSxTruthy((typeOf(test) == "keyword")) && (keywordName(test) == "else")), (isSxTruthy((typeOf(test) == "symbol")) && sxOr((symbolName(test) == "else"), (symbolName(test) == ":else"))))) ? makeThunk(body, env) : (isSxTruthy(trampoline(evalExpr(test, env))) ? makeThunk(body, env) : sfCondClojure(slice(clauses, 2), env))); +})()); }; + + // sf-case + var sfCase = function(args, env) { return (function() { + var matchVal = trampoline(evalExpr(first(args), env)); + var clauses = rest(args); + return sfCaseLoop(matchVal, clauses, env); +})(); }; + + // sf-case-loop + var sfCaseLoop = function(matchVal, clauses, env) { return (isSxTruthy((len(clauses) < 2)) ? NIL : (function() { + var test = first(clauses); + var body = nth(clauses, 1); + return (isSxTruthy(sxOr((isSxTruthy((typeOf(test) == "keyword")) && (keywordName(test) == "else")), (isSxTruthy((typeOf(test) == "symbol")) && sxOr((symbolName(test) == "else"), (symbolName(test) == ":else"))))) ? makeThunk(body, env) : (isSxTruthy((matchVal == trampoline(evalExpr(test, env)))) ? makeThunk(body, env) : sfCaseLoop(matchVal, slice(clauses, 2), env))); +})()); }; + + // sf-and + var sfAnd = function(args, env) { return (isSxTruthy(isEmpty(args)) ? true : (function() { + var val = trampoline(evalExpr(first(args), env)); + return (isSxTruthy(!val) ? val : (isSxTruthy((len(args) == 1)) ? val : sfAnd(rest(args), env))); +})()); }; + + // sf-or + var sfOr = function(args, env) { return (isSxTruthy(isEmpty(args)) ? false : (function() { + var val = trampoline(evalExpr(first(args), env)); + return (isSxTruthy(val) ? val : sfOr(rest(args), env)); +})()); }; + + // sf-let + var sfLet = function(args, env) { return (function() { + var bindings = first(args); + var body = rest(args); + var local = envExtend(env); + (isSxTruthy((isSxTruthy((typeOf(first(bindings)) == "list")) && (len(first(bindings)) == 2))) ? forEach(function(binding) { return (function() { + var vname = (isSxTruthy((typeOf(first(binding)) == "symbol")) ? symbolName(first(binding)) : first(binding)); + return envSet(local, vname, trampoline(evalExpr(nth(binding, 1), local))); +})(); }, bindings) : (function() { + var i = 0; + return reduce(function(acc, pairIdx) { return (function() { + var vname = (isSxTruthy((typeOf(nth(bindings, (pairIdx * 2))) == "symbol")) ? symbolName(nth(bindings, (pairIdx * 2))) : nth(bindings, (pairIdx * 2))); + var valExpr = nth(bindings, ((pairIdx * 2) + 1)); + return envSet(local, vname, trampoline(evalExpr(valExpr, local))); +})(); }, NIL, range(0, (len(bindings) / 2))); +})()); + { var _c = slice(body, 0, (len(body) - 1)); for (var _i = 0; _i < _c.length; _i++) { var e = _c[_i]; trampoline(evalExpr(e, local)); } } + return makeThunk(last(body), local); +})(); }; + + // sf-lambda + var sfLambda = function(args, env) { return (function() { + var paramsExpr = first(args); + var body = nth(args, 1); + var paramNames = map(function(p) { return (isSxTruthy((typeOf(p) == "symbol")) ? symbolName(p) : p); }, paramsExpr); + return makeLambda(paramNames, body, env); +})(); }; + + // sf-define + var sfDefine = function(args, env) { return (function() { + var nameSym = first(args); + var value = trampoline(evalExpr(nth(args, 1), env)); + if (isSxTruthy((isSxTruthy(isLambda(value)) && isNil(lambdaName(value))))) { + value.name = symbolName(nameSym); +} + env[symbolName(nameSym)] = value; + return value; +})(); }; + + // sf-defcomp + var sfDefcomp = function(args, env) { return (function() { + var nameSym = first(args); + var paramsRaw = nth(args, 1); + var body = nth(args, 2); + var compName = stripPrefix(symbolName(nameSym), "~"); + var parsed = parseCompParams(paramsRaw); + var params = first(parsed); + var hasChildren = nth(parsed, 1); + return (function() { + var comp = makeComponent(compName, params, hasChildren, body, env); + env[symbolName(nameSym)] = comp; + return comp; +})(); +})(); }; + + // parse-comp-params + var parseCompParams = function(paramsExpr) { return (function() { + var params = []; + var hasChildren = false; + var inKey = false; + { var _c = paramsExpr; for (var _i = 0; _i < _c.length; _i++) { var p = _c[_i]; if (isSxTruthy((typeOf(p) == "symbol"))) { + (function() { + var name = symbolName(p); + return (isSxTruthy((name == "&key")) ? (inKey = true) : (isSxTruthy((name == "&rest")) ? (hasChildren = true) : (isSxTruthy((isSxTruthy(inKey) && !hasChildren)) ? append_b(params, name) : append_b(params, name)))); +})(); +} } } + return [params, hasChildren]; +})(); }; + + // sf-defmacro + var sfDefmacro = function(args, env) { return (function() { + var nameSym = first(args); + var paramsRaw = nth(args, 1); + var body = nth(args, 2); + var parsed = parseMacroParams(paramsRaw); + var params = first(parsed); + var restParam = nth(parsed, 1); + return (function() { + var mac = makeMacro(params, restParam, body, env, symbolName(nameSym)); + env[symbolName(nameSym)] = mac; + return mac; +})(); +})(); }; + + // parse-macro-params + var parseMacroParams = function(paramsExpr) { return (function() { + var params = []; + var restParam = NIL; + reduce(function(state, p) { return (isSxTruthy((isSxTruthy((typeOf(p) == "symbol")) && (symbolName(p) == "&rest"))) ? assoc(state, "in-rest", true) : (isSxTruthy(get(state, "in-rest")) ? ((restParam = (isSxTruthy((typeOf(p) == "symbol")) ? symbolName(p) : p)), state) : (append_b(params, (isSxTruthy((typeOf(p) == "symbol")) ? symbolName(p) : p)), state))); }, {["in-rest"]: false}, paramsExpr); + return [params, restParam]; +})(); }; + + // sf-begin + var sfBegin = function(args, env) { return (isSxTruthy(isEmpty(args)) ? NIL : (forEach(function(e) { return trampoline(evalExpr(e, env)); }, slice(args, 0, (len(args) - 1))), makeThunk(last(args), env))); }; + + // sf-quote + var sfQuote = function(args, env) { return (isSxTruthy(isEmpty(args)) ? NIL : first(args)); }; + + // sf-quasiquote + var sfQuasiquote = function(args, env) { return qqExpand(first(args), env); }; + + // qq-expand + var qqExpand = function(template, env) { return (isSxTruthy(!(typeOf(template) == "list")) ? template : (isSxTruthy(isEmpty(template)) ? [] : (function() { + var head = first(template); + return (isSxTruthy((isSxTruthy((typeOf(head) == "symbol")) && (symbolName(head) == "unquote"))) ? trampoline(evalExpr(nth(template, 1), env)) : reduce(function(result, item) { return (isSxTruthy((isSxTruthy((typeOf(item) == "list")) && isSxTruthy((len(item) == 2)) && isSxTruthy((typeOf(first(item)) == "symbol")) && (symbolName(first(item)) == "splice-unquote"))) ? (function() { + var spliced = trampoline(evalExpr(nth(item, 1), env)); + return (isSxTruthy((typeOf(spliced) == "list")) ? concat(result, spliced) : (isSxTruthy(isNil(spliced)) ? result : append(result, spliced))); +})() : append(result, qqExpand(item, env))); }, [], template)); +})())); }; + + // sf-thread-first + var sfThreadFirst = function(args, env) { return (function() { + var val = trampoline(evalExpr(first(args), env)); + return reduce(function(result, form) { return (isSxTruthy((typeOf(form) == "list")) ? (function() { + var f = trampoline(evalExpr(first(form), env)); + var restArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, rest(form)); + var allArgs = cons(result, restArgs); + return (isSxTruthy((isSxTruthy(isCallable(f)) && !isLambda(f))) ? apply(f, allArgs) : (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, allArgs, env)) : error((String("-> form not callable: ") + String(inspect(f)))))); +})() : (function() { + var f = trampoline(evalExpr(form, env)); + return (isSxTruthy((isSxTruthy(isCallable(f)) && !isLambda(f))) ? f(result) : (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, [result], env)) : error((String("-> form not callable: ") + String(inspect(f)))))); +})()); }, val, rest(args)); +})(); }; + + // sf-set! + var sfSetBang = function(args, env) { return (function() { + var name = symbolName(first(args)); + var value = trampoline(evalExpr(nth(args, 1), env)); + env[name] = value; + return value; +})(); }; + + // expand-macro + var expandMacro = function(mac, rawArgs, env) { return (function() { + var local = envMerge(macroClosure(mac), env); + { var _c = mapIndexed(function(i, p) { return [p, i]; }, macroParams(mac)); for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; local[first(pair)] = (isSxTruthy((nth(pair, 1) < len(rawArgs))) ? nth(rawArgs, nth(pair, 1)) : NIL); } } + if (isSxTruthy(macroRestParam(mac))) { + local[macroRestParam(mac)] = slice(rawArgs, len(macroParams(mac))); +} + return trampoline(evalExpr(macroBody(mac), local)); +})(); }; + + // ho-map + var hoMap = function(args, env) { return (function() { + var f = trampoline(evalExpr(first(args), env)); + var coll = trampoline(evalExpr(nth(args, 1), env)); + return map(function(item) { return trampoline(callLambda(f, [item], env)); }, coll); +})(); }; + + // ho-map-indexed + var hoMapIndexed = function(args, env) { return (function() { + var f = trampoline(evalExpr(first(args), env)); + var coll = trampoline(evalExpr(nth(args, 1), env)); + return mapIndexed(function(i, item) { return trampoline(callLambda(f, [i, item], env)); }, coll); +})(); }; + + // ho-filter + var hoFilter = function(args, env) { return (function() { + var f = trampoline(evalExpr(first(args), env)); + var coll = trampoline(evalExpr(nth(args, 1), env)); + return filter(function(item) { return trampoline(callLambda(f, [item], env)); }, coll); +})(); }; + + // ho-reduce + var hoReduce = function(args, env) { return (function() { + var f = trampoline(evalExpr(first(args), env)); + var init = trampoline(evalExpr(nth(args, 1), env)); + var coll = trampoline(evalExpr(nth(args, 2), env)); + return reduce(function(acc, item) { return trampoline(callLambda(f, [acc, item], env)); }, init, coll); +})(); }; + + // ho-some + var hoSome = function(args, env) { return (function() { + var f = trampoline(evalExpr(first(args), env)); + var coll = trampoline(evalExpr(nth(args, 1), env)); + return some(function(item) { return trampoline(callLambda(f, [item], env)); }, coll); +})(); }; + + // ho-every + var hoEvery = function(args, env) { return (function() { + var f = trampoline(evalExpr(first(args), env)); + var coll = trampoline(evalExpr(nth(args, 1), env)); + return isEvery(function(item) { return trampoline(callLambda(f, [item], env)); }, coll); +})(); }; + + + // === Transpiled from render.sx === + + // HTML_TAGS + var HTML_TAGS = ["html", "head", "body", "title", "meta", "link", "script", "style", "noscript", "header", "nav", "main", "section", "article", "aside", "footer", "h1", "h2", "h3", "h4", "h5", "h6", "hgroup", "div", "p", "blockquote", "pre", "figure", "figcaption", "address", "details", "summary", "a", "span", "em", "strong", "small", "b", "i", "u", "s", "mark", "sub", "sup", "abbr", "cite", "code", "time", "br", "wbr", "hr", "ul", "ol", "li", "dl", "dt", "dd", "table", "thead", "tbody", "tfoot", "tr", "th", "td", "caption", "colgroup", "col", "form", "input", "textarea", "select", "option", "optgroup", "button", "label", "fieldset", "legend", "output", "datalist", "img", "video", "audio", "source", "picture", "canvas", "iframe", "svg", "path", "circle", "rect", "line", "polyline", "polygon", "text", "g", "defs", "use", "clipPath", "mask", "pattern", "linearGradient", "radialGradient", "stop", "filter", "feGaussianBlur", "feOffset", "feBlend", "feColorMatrix", "feComposite", "feMerge", "feMergeNode", "animate", "animateTransform", "foreignObject", "template", "slot", "dialog", "menu"]; + + // VOID_ELEMENTS + var VOID_ELEMENTS = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"]; + + // BOOLEAN_ATTRS + var BOOLEAN_ATTRS = ["disabled", "checked", "selected", "readonly", "required", "hidden", "autofocus", "autoplay", "controls", "loop", "muted", "defer", "async", "novalidate", "formnovalidate", "multiple", "open", "allowfullscreen"]; + + // render-to-html + var renderToHtml = function(expr, env) { return (function() { + var result = trampoline(evalExpr(expr, env)); + return renderValueToHtml(result, env); +})(); }; + + // render-value-to-html + var renderValueToHtml = function(val, env) { return (function() { var _m = typeOf(val); if (_m == "nil") return ""; if (_m == "string") return escapeHtml(val); if (_m == "number") return (String(val)); if (_m == "boolean") return (isSxTruthy(val) ? "true" : "false"); if (_m == "list") return renderListToHtml(val, env); if (_m == "raw-html") return rawHtmlContent(val); return escapeHtml((String(val))); })(); }; + + // render-list-to-html + var renderListToHtml = function(expr, env) { return (isSxTruthy(isEmpty(expr)) ? "" : (function() { + var head = first(expr); + return (isSxTruthy(!(typeOf(head) == "symbol")) ? join("", map(function(x) { return renderValueToHtml(x, env); }, expr)) : (function() { + var name = symbolName(head); + var args = rest(expr); + return (isSxTruthy((name == "<>")) ? join("", map(function(x) { return renderToHtml(x, env); }, args)) : (isSxTruthy((name == "raw!")) ? join("", map(function(x) { return (String(trampoline(evalExpr(x, env)))); }, args)) : (isSxTruthy(contains(HTML_TAGS, name)) ? renderHtmlElement(name, args, env) : (isSxTruthy(startsWith(name, "~")) ? (function() { + var comp = envGet(env, name); + return (isSxTruthy(isComponent(comp)) ? renderToHtml(trampoline(callComponent(comp, args, env)), env) : error((String("Unknown component: ") + String(name)))); +})() : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? renderToHtml(trampoline(evalExpr(expandMacro(envGet(env, name), args, env), env)), env) : renderValueToHtml(trampoline(evalExpr(expr, env)), env)))))); +})()); +})()); }; + + // render-html-element + var renderHtmlElement = function(tag, args, env) { return (function() { + var parsed = parseElementArgs(args, env); + var attrs = first(parsed); + var children = nth(parsed, 1); + var isVoid = contains(VOID_ELEMENTS, tag); + return (String("<") + String(tag) + String(renderAttrs(attrs)) + String((isSxTruthy(isVoid) ? " />" : (String(">") + String(join("", map(function(c) { return renderToHtml(c, env); }, children))) + String(""))))); +})(); }; + + // parse-element-args + var parseElementArgs = function(args, env) { return (function() { + var attrs = {}; + var children = []; + reduce(function(state, arg) { return (function() { + var skip = get(state, "skip"); + return (isSxTruthy(skip) ? assoc(state, "skip", false) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() { + var val = trampoline(evalExpr(nth(args, (get(state, "i") + 1)), env)); + attrs[keywordName(arg)] = val; + return assoc(state, "skip", true, "i", (get(state, "i") + 1)); +})() : (append_b(children, arg), assoc(state, "i", (get(state, "i") + 1))))); +})(); }, {["i"]: 0, ["skip"]: false}, args); + return [attrs, children]; +})(); }; + + // render-attrs + var renderAttrs = function(attrs) { return join("", map(function(key) { return (function() { + var val = dictGet(attrs, key); + return (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && val)) ? (String(" ") + String(key)) : (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && !val)) ? "" : (isSxTruthy(isNil(val)) ? "" : (String(" ") + String(key) + String("=\"") + String(escapeAttr((String(val)))) + String("\""))))); +})(); }, keys(attrs))); }; + + // render-to-sx + var renderToSx = function(expr, env) { return (function() { + var result = aser(expr, env); + return serialize(result); +})(); }; + + // aser + var aser = function(expr, env) { return (function() { var _m = typeOf(expr); if (_m == "number") return expr; if (_m == "string") return expr; if (_m == "boolean") return expr; if (_m == "nil") return NIL; if (_m == "symbol") return (function() { + var name = symbolName(expr); + return (isSxTruthy(envHas(env, name)) ? envGet(env, name) : (isSxTruthy(isPrimitive(name)) ? getPrimitive(name) : (isSxTruthy((name == "true")) ? true : (isSxTruthy((name == "false")) ? false : (isSxTruthy((name == "nil")) ? NIL : error((String("Undefined symbol: ") + String(name)))))))); +})(); if (_m == "keyword") return keywordName(expr); if (_m == "list") return (isSxTruthy(isEmpty(expr)) ? [] : aserList(expr, env)); return expr; })(); }; + + // aser-list + var aserList = function(expr, env) { return (function() { + var head = first(expr); + var args = rest(expr); + return (isSxTruthy(!(typeOf(head) == "symbol")) ? map(function(x) { return aser(x, env); }, expr) : (function() { + var name = symbolName(head); + return (isSxTruthy((name == "<>")) ? aserFragment(args, env) : (isSxTruthy(startsWith(name, "~")) ? aserCall(name, args, env) : (isSxTruthy(contains(HTML_TAGS, name)) ? aserCall(name, args, env) : (isSxTruthy(sxOr(isSpecialForm(name), isHoForm(name))) ? aserSpecial(name, expr, env) : (isSxTruthy((isSxTruthy(envHas(env, name)) && isMacro(envGet(env, name)))) ? aser(expandMacro(envGet(env, name), args, env), env) : (function() { + var f = trampoline(evalExpr(head, env)); + var evaledArgs = map(function(a) { return trampoline(evalExpr(a, env)); }, args); + return (isSxTruthy((isSxTruthy(isCallable(f)) && isSxTruthy(!isLambda(f)) && !isComponent(f))) ? apply(f, evaledArgs) : (isSxTruthy(isLambda(f)) ? trampoline(callLambda(f, evaledArgs, env)) : (isSxTruthy(isComponent(f)) ? aserCall((String("~") + String(componentName(f))), args, env) : error((String("Not callable: ") + String(inspect(f))))))); +})()))))); +})()); +})(); }; + + // aser-fragment + var aserFragment = function(children, env) { return (function() { + var parts = filter(function(x) { return !isNil(x); }, map(function(c) { return aser(c, env); }, children)); + return (isSxTruthy(isEmpty(parts)) ? "" : (String("(<> ") + String(join(" ", map(serialize, parts))) + String(")"))); +})(); }; + + // aser-call + var aserCall = function(name, args, env) { return (function() { + var parts = [name]; + reduce(function(state, arg) { return (function() { + var skip = get(state, "skip"); + return (isSxTruthy(skip) ? assoc(state, "skip", false) : (isSxTruthy((isSxTruthy((typeOf(arg) == "keyword")) && ((get(state, "i") + 1) < len(args)))) ? (function() { + var val = aser(nth(args, (get(state, "i") + 1)), env); + if (isSxTruthy(!isNil(val))) { + parts.push((String(":") + String(keywordName(arg)))); + parts.push(serialize(val)); +} + return assoc(state, "skip", true, "i", (get(state, "i") + 1)); +})() : (function() { + var val = aser(arg, env); + if (isSxTruthy(!isNil(val))) { + parts.push(serialize(val)); +} + return assoc(state, "i", (get(state, "i") + 1)); +})())); +})(); }, {["i"]: 0, ["skip"]: false}, args); + return (String("(") + String(join(" ", parts)) + String(")")); +})(); }; + + + // ========================================================================= + // Post-transpilation fixups + // ========================================================================= + // The reference spec's call-lambda only handles Lambda objects, but HO forms + // (map, reduce, etc.) may receive native primitives. Wrap to handle both. + var _rawCallLambda = callLambda; + callLambda = function(f, args, callerEnv) { + if (typeof f === "function") return f.apply(null, args); + return _rawCallLambda(f, args, callerEnv); + }; + + // ========================================================================= + // Parser (reused from reference — hand-written for bootstrap simplicity) + // ========================================================================= + + // The parser is the one piece we keep as hand-written JS since the + // reference parser.sx is more of a spec than directly compilable code + // (it uses mutable cursor state that doesn't map cleanly to the + // transpiler's functional output). A future version could bootstrap + // the parser too. + + function parse(text) { + var pos = 0; + function skipWs() { + while (pos < text.length) { + var ch = text[pos]; + if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") { pos++; continue; } + if (ch === ";") { while (pos < text.length && text[pos] !== "\n") pos++; continue; } + break; + } + } + function readExpr() { + skipWs(); + if (pos >= text.length) return undefined; + var ch = text[pos]; + if (ch === "(") { pos++; return readList(")"); } + if (ch === "[") { pos++; return readList("]"); } + if (ch === '"') return readString(); + if (ch === ":") return readKeyword(); + if (ch === "-" && pos + 1 < text.length && text[pos + 1] >= "0" && text[pos + 1] <= "9") return readNumber(); + if (ch >= "0" && ch <= "9") return readNumber(); + return readSymbol(); + } + function readList(close) { + var items = []; + while (true) { + skipWs(); + if (pos >= text.length) throw new Error("Unterminated list"); + if (text[pos] === close) { pos++; return items; } + items.push(readExpr()); + } + } + function readString() { + pos++; // skip " + var s = ""; + while (pos < text.length) { + var ch = text[pos]; + if (ch === '"') { pos++; return s; } + if (ch === "\\") { pos++; var esc = text[pos]; s += esc === "n" ? "\n" : esc === "t" ? "\t" : esc === "r" ? "\r" : esc; pos++; continue; } + s += ch; pos++; + } + throw new Error("Unterminated string"); + } + function readKeyword() { + pos++; // skip : + var name = readIdent(); + return new Keyword(name); + } + function readNumber() { + var start = pos; + if (text[pos] === "-") pos++; + while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++; + if (pos < text.length && text[pos] === ".") { pos++; while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++; } + if (pos < text.length && (text[pos] === "e" || text[pos] === "E")) { + pos++; + if (pos < text.length && (text[pos] === "+" || text[pos] === "-")) pos++; + while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++; + } + return Number(text.slice(start, pos)); + } + function readIdent() { + var start = pos; + while (pos < text.length && /[a-zA-Z0-9_~*+\-><=/!?.:&]/.test(text[pos])) pos++; + return text.slice(start, pos); + } + function readSymbol() { + var name = readIdent(); + if (name === "true") return true; + if (name === "false") return false; + if (name === "nil") return NIL; + return new Symbol(name); + } + var exprs = []; + while (true) { + skipWs(); + if (pos >= text.length) break; + exprs.push(readExpr()); + } + return exprs; + } + + // ========================================================================= + // Public API + // ========================================================================= + + var componentEnv = {}; + + function loadComponents(source) { + var exprs = parse(source); + for (var i = 0; i < exprs.length; i++) { + trampoline(evalExpr(exprs[i], componentEnv)); + } + } + + function render(source) { + var exprs = parse(source); + var frag = document.createDocumentFragment(); + for (var i = 0; i < exprs.length; i++) { + var result = trampoline(evalExpr(exprs[i], merge(componentEnv))); + appendToDOM(frag, result, merge(componentEnv)); + } + return frag; + } + + function appendToDOM(parent, val, env) { + if (isNil(val)) return; + if (typeof val === "string") { parent.appendChild(document.createTextNode(val)); return; } + if (typeof val === "number") { parent.appendChild(document.createTextNode(String(val))); return; } + if (val._raw) { var t = document.createElement("template"); t.innerHTML = val.html; parent.appendChild(t.content); return; } + if (Array.isArray(val)) { + // Could be a rendered element or a list of results + if (val.length > 0 && isSym(val[0])) { + // It's an unevaluated expression — evaluate it + var result = trampoline(evalExpr(val, env)); + appendToDOM(parent, result, env); + } else { + for (var i = 0; i < val.length; i++) appendToDOM(parent, val[i], env); + } + return; + } + parent.appendChild(document.createTextNode(String(val))); + } + + var SxRef = { + parse: parse, + eval: function(expr, env) { return trampoline(evalExpr(expr, env || merge(componentEnv))); }, + loadComponents: loadComponents, + render: render, + serialize: serialize, + NIL: NIL, + Symbol: Symbol, + Keyword: Keyword, + componentEnv: componentEnv, + _version: "ref-1.0 (bootstrap-compiled)" + }; + + if (typeof module !== "undefined" && module.exports) module.exports = SxRef; + else global.SxRef = SxRef; + +})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this); diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py new file mode 100644 index 0000000..5e9583c --- /dev/null +++ b/shared/sx/ref/bootstrap_js.py @@ -0,0 +1,1070 @@ +#!/usr/bin/env python3 +""" +Bootstrap compiler: reference SX evaluator → JavaScript. + +Reads the .sx reference specification and emits a standalone JavaScript +evaluator (sx-ref.js) that can be compared against the hand-written sx.js. + +The compiler translates the restricted SX subset used in eval.sx/render.sx +into idiomatic JavaScript. Platform interface functions are emitted as +native JS implementations. + +Usage: + python bootstrap_js.py > sx-ref.js +""" +from __future__ import annotations + +import os +import sys + +# Add project root to path for imports +_HERE = os.path.dirname(os.path.abspath(__file__)) +_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", "..")) +sys.path.insert(0, _PROJECT) + +from shared.sx.parser import parse_all +from shared.sx.types import Symbol, Keyword, NIL as SX_NIL + +# --------------------------------------------------------------------------- +# SX → JavaScript transpiler +# --------------------------------------------------------------------------- + +class JSEmitter: + """Transpile an SX AST node to JavaScript source code.""" + + def __init__(self): + self.indent = 0 + + def emit(self, expr) -> str: + """Emit a JS expression from an SX AST node.""" + # Bool MUST be checked before int (bool is subclass of int in Python) + if isinstance(expr, bool): + return "true" if expr else "false" + if isinstance(expr, (int, float)): + return str(expr) + if isinstance(expr, str): + return self._js_string(expr) + if expr is None or expr is SX_NIL: + return "NIL" + if isinstance(expr, Symbol): + return self._emit_symbol(expr.name) + if isinstance(expr, Keyword): + return self._js_string(expr.name) + if isinstance(expr, list): + return self._emit_list(expr) + return str(expr) + + def emit_statement(self, expr) -> str: + """Emit a JS statement (with semicolon) from an SX AST node.""" + if isinstance(expr, list) and expr: + head = expr[0] + if isinstance(head, Symbol): + name = head.name + if name == "define": + return self._emit_define(expr) + if name == "set!": + return f"{self._mangle(expr[1].name)} = {self.emit(expr[2])};" + if name == "when": + return self._emit_when_stmt(expr) + if name == "do" or name == "begin": + return "\n".join(self.emit_statement(e) for e in expr[1:]) + if name == "for-each": + return self._emit_for_each_stmt(expr) + if name == "dict-set!": + return f"{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])};" + if name == "append!": + return f"{self.emit(expr[1])}.push({self.emit(expr[2])});" + if name == "env-set!": + return f"{self.emit(expr[1])}[{self.emit(expr[2])}] = {self.emit(expr[3])};" + if name == "set-lambda-name!": + return f"{self.emit(expr[1])}.name = {self.emit(expr[2])};" + return f"{self.emit(expr)};" + + # --- Symbol emission --- + + def _emit_symbol(self, name: str) -> str: + # Map SX names to JS names + return self._mangle(name) + + def _mangle(self, name: str) -> str: + """Convert SX identifier to valid JS identifier.""" + RENAMES = { + "nil": "NIL", + "true": "true", + "false": "false", + "nil?": "isNil", + "type-of": "typeOf", + "symbol-name": "symbolName", + "keyword-name": "keywordName", + "make-lambda": "makeLambda", + "make-component": "makeComponent", + "make-macro": "makeMacro", + "make-thunk": "makeThunk", + "make-symbol": "makeSymbol", + "make-keyword": "makeKeyword", + "lambda-params": "lambdaParams", + "lambda-body": "lambdaBody", + "lambda-closure": "lambdaClosure", + "lambda-name": "lambdaName", + "set-lambda-name!": "setLambdaName", + "component-params": "componentParams", + "component-body": "componentBody", + "component-closure": "componentClosure", + "component-has-children?": "componentHasChildren", + "component-name": "componentName", + "macro-params": "macroParams", + "macro-rest-param": "macroRestParam", + "macro-body": "macroBody", + "macro-closure": "macroClosure", + "thunk?": "isThunk", + "thunk-expr": "thunkExpr", + "thunk-env": "thunkEnv", + "callable?": "isCallable", + "lambda?": "isLambda", + "component?": "isComponent", + "macro?": "isMacro", + "primitive?": "isPrimitive", + "get-primitive": "getPrimitive", + "env-has?": "envHas", + "env-get": "envGet", + "env-set!": "envSet", + "env-extend": "envExtend", + "env-merge": "envMerge", + "dict-set!": "dictSet", + "dict-get": "dictGet", + "eval-expr": "evalExpr", + "eval-list": "evalList", + "eval-call": "evalCall", + "call-lambda": "callLambda", + "call-component": "callComponent", + "parse-keyword-args": "parseKeywordArgs", + "parse-comp-params": "parseCompParams", + "parse-macro-params": "parseMacroParams", + "expand-macro": "expandMacro", + "render-to-html": "renderToHtml", + "render-to-sx": "renderToSx", + "render-value-to-html": "renderValueToHtml", + "render-list-to-html": "renderListToHtml", + "render-html-element": "renderHtmlElement", + "parse-element-args": "parseElementArgs", + "render-attrs": "renderAttrs", + "aser-list": "aserList", + "aser-fragment": "aserFragment", + "aser-call": "aserCall", + "aser-special": "aserSpecial", + "sf-if": "sfIf", + "sf-when": "sfWhen", + "sf-cond": "sfCond", + "sf-cond-scheme": "sfCondScheme", + "sf-cond-clojure": "sfCondClojure", + "sf-case": "sfCase", + "sf-case-loop": "sfCaseLoop", + "sf-and": "sfAnd", + "sf-or": "sfOr", + "sf-let": "sfLet", + "sf-lambda": "sfLambda", + "sf-define": "sfDefine", + "sf-defcomp": "sfDefcomp", + "sf-defmacro": "sfDefmacro", + "sf-begin": "sfBegin", + "sf-quote": "sfQuote", + "sf-quasiquote": "sfQuasiquote", + "sf-thread-first": "sfThreadFirst", + "sf-set!": "sfSetBang", + "qq-expand": "qqExpand", + "ho-map": "hoMap", + "ho-map-indexed": "hoMapIndexed", + "ho-filter": "hoFilter", + "ho-reduce": "hoReduce", + "ho-some": "hoSome", + "ho-every": "hoEvery", + "special-form?": "isSpecialForm", + "ho-form?": "isHoForm", + "strip-prefix": "stripPrefix", + "escape-html": "escapeHtml", + "escape-attr": "escapeAttr", + "escape-string": "escapeString", + "raw-html-content": "rawHtmlContent", + "HTML_TAGS": "HTML_TAGS", + "VOID_ELEMENTS": "VOID_ELEMENTS", + "BOOLEAN_ATTRS": "BOOLEAN_ATTRS", + "whitespace?": "isWhitespace", + "digit?": "isDigit", + "ident-start?": "isIdentStart", + "ident-char?": "isIdentChar", + "parse-number": "parseNumber", + "sx-expr-source": "sxExprSource", + "starts-with?": "startsWith", + "ends-with?": "endsWith", + "contains?": "contains", + "empty?": "isEmpty", + "odd?": "isOdd", + "even?": "isEven", + "zero?": "isZero", + "number?": "isNumber", + "string?": "isString", + "list?": "isList", + "dict?": "isDict", + "every?": "isEvery", + "map-indexed": "mapIndexed", + "for-each": "forEach", + "map-dict": "mapDict", + "chunk-every": "chunkEvery", + "zip-pairs": "zipPairs", + "strip-tags": "stripTags", + "format-date": "formatDate", + "format-decimal": "formatDecimal", + "parse-int": "parseInt_", + } + if name in RENAMES: + return RENAMES[name] + # General mangling: replace - with camelCase, ? with _p, ! with _b + result = name + if result.endswith("?"): + result = result[:-1] + "_p" + if result.endswith("!"): + result = result[:-1] + "_b" + # Kebab to camel + parts = result.split("-") + if len(parts) > 1: + result = parts[0] + "".join(p.capitalize() for p in parts[1:]) + return result + + # --- List emission --- + + def _emit_list(self, expr: list) -> str: + if not expr: + return "[]" + head = expr[0] + if not isinstance(head, Symbol): + # Data list + return "[" + ", ".join(self.emit(x) for x in expr) + "]" + name = head.name + handler = getattr(self, f"_sf_{name.replace('-', '_').replace('!', '_b').replace('?', '_p')}", None) + if handler: + return handler(expr) + # Built-in forms + if name == "fn" or name == "lambda": + return self._emit_fn(expr) + if name == "let" or name == "let*": + return self._emit_let(expr) + if name == "if": + return self._emit_if(expr) + if name == "when": + return self._emit_when(expr) + if name == "cond": + return self._emit_cond(expr) + if name == "case": + return self._emit_case(expr) + if name == "and": + return self._emit_and(expr) + if name == "or": + return self._emit_or(expr) + if name == "not": + return f"!{self.emit(expr[1])}" + if name == "do" or name == "begin": + return self._emit_do(expr) + if name == "list": + return "[" + ", ".join(self.emit(x) for x in expr[1:]) + "]" + if name == "dict": + return self._emit_dict_literal(expr) + if name == "quote": + return self._emit_quote(expr[1]) + if name == "set!": + return f"({self._mangle(expr[1].name)} = {self.emit(expr[2])})" + if name == "str": + parts = [self.emit(x) for x in expr[1:]] + return "(" + " + ".join(f'String({p})' for p in parts) + ")" + # Infix operators + if name in ("+", "-", "*", "/", "=", "!=", "<", ">", "<=", ">=", "mod"): + return self._emit_infix(name, expr[1:]) + if name == "inc": + return f"({self.emit(expr[1])} + 1)" + if name == "dec": + return f"({self.emit(expr[1])} - 1)" + + # Regular function call + fn_name = self._mangle(name) + args = ", ".join(self.emit(x) for x in expr[1:]) + return f"{fn_name}({args})" + + # --- Special form emitters --- + + def _emit_fn(self, expr) -> str: + params = expr[1] + body = expr[2] + param_names = [] + for p in params: + if isinstance(p, Symbol): + param_names.append(self._mangle(p.name)) + else: + param_names.append(str(p)) + params_str = ", ".join(param_names) + body_js = self.emit(body) + return f"function({params_str}) {{ return {body_js}; }}" + + def _emit_let(self, expr) -> str: + bindings = expr[1] + body = expr[2:] + parts = ["(function() {"] + if isinstance(bindings, list): + if bindings and isinstance(bindings[0], list): + # Scheme-style: ((name val) ...) + for b in bindings: + vname = b[0].name if isinstance(b[0], Symbol) else str(b[0]) + parts.append(f" var {self._mangle(vname)} = {self.emit(b[1])};") + else: + # Clojure-style: (name val name val ...) + for i in range(0, len(bindings), 2): + vname = bindings[i].name if isinstance(bindings[i], Symbol) else str(bindings[i]) + parts.append(f" var {self._mangle(vname)} = {self.emit(bindings[i + 1])};") + for b_expr in body[:-1]: + parts.append(f" {self.emit_statement(b_expr)}") + parts.append(f" return {self.emit(body[-1])};") + parts.append("})()") + return "\n".join(parts) + + def _emit_if(self, expr) -> str: + cond = self.emit(expr[1]) + then = self.emit(expr[2]) + els = self.emit(expr[3]) if len(expr) > 3 else "NIL" + return f"(isSxTruthy({cond}) ? {then} : {els})" + + def _emit_when(self, expr) -> str: + cond = self.emit(expr[1]) + body_parts = expr[2:] + if len(body_parts) == 1: + return f"(isSxTruthy({cond}) ? {self.emit(body_parts[0])} : NIL)" + body = self._emit_do_inner(body_parts) + return f"(isSxTruthy({cond}) ? {body} : NIL)" + + def _emit_when_stmt(self, expr) -> str: + cond = self.emit(expr[1]) + body_parts = expr[2:] + stmts = "\n".join(f" {self.emit_statement(e)}" for e in body_parts) + return f"if (isSxTruthy({cond})) {{\n{stmts}\n}}" + + def _emit_cond(self, expr) -> str: + clauses = expr[1:] + if not clauses: + return "NIL" + # Determine style ONCE: Scheme-style if every element is a 2-element + # list AND no bare keywords appear (bare :else = Clojure). + is_scheme = ( + all(isinstance(c, list) and len(c) == 2 for c in clauses) + and not any(isinstance(c, Keyword) for c in clauses) + ) + if is_scheme: + return self._cond_scheme(clauses) + return self._cond_clojure(clauses) + + def _cond_scheme(self, clauses) -> str: + if not clauses: + return "NIL" + clause = clauses[0] + test = clause[0] + body = clause[1] + if isinstance(test, Symbol) and test.name in ("else", ":else"): + return self.emit(body) + if isinstance(test, Keyword) and test.name == "else": + return self.emit(body) + return f"(isSxTruthy({self.emit(test)}) ? {self.emit(body)} : {self._cond_scheme(clauses[1:])})" + + def _cond_clojure(self, clauses) -> str: + if len(clauses) < 2: + return "NIL" + test = clauses[0] + body = clauses[1] + if isinstance(test, Keyword) and test.name == "else": + return self.emit(body) + if isinstance(test, Symbol) and test.name in ("else", ":else"): + return self.emit(body) + return f"(isSxTruthy({self.emit(test)}) ? {self.emit(body)} : {self._cond_clojure(clauses[2:])})" + + def _emit_case(self, expr) -> str: + match_expr = self.emit(expr[1]) + clauses = expr[2:] + return f"(function() {{ var _m = {match_expr}; {self._case_chain(clauses)} }})()" + + def _case_chain(self, clauses) -> str: + if len(clauses) < 2: + return "return NIL;" + test = clauses[0] + body = clauses[1] + if isinstance(test, Keyword) and test.name == "else": + return f"return {self.emit(body)};" + if isinstance(test, Symbol) and test.name in ("else", ":else"): + return f"return {self.emit(body)};" + return f"if (_m == {self.emit(test)}) return {self.emit(body)}; {self._case_chain(clauses[2:])}" + + def _emit_and(self, expr) -> str: + parts = [self.emit(x) for x in expr[1:]] + return "(" + " && ".join(f"isSxTruthy({p})" for p in parts[:-1]) + (" && " if len(parts) > 1 else "") + parts[-1] + ")" + + def _emit_or(self, expr) -> str: + if len(expr) == 2: + return self.emit(expr[1]) + parts = [self.emit(x) for x in expr[1:]] + # Use a helper that returns the first truthy value + return f"sxOr({', '.join(parts)})" + + def _emit_do(self, expr) -> str: + return self._emit_do_inner(expr[1:]) + + def _emit_do_inner(self, exprs) -> str: + if len(exprs) == 1: + return self.emit(exprs[0]) + parts = [self.emit(e) for e in exprs] + return "(" + ", ".join(parts) + ")" + + def _emit_dict_literal(self, expr) -> str: + pairs = expr[1:] + parts = [] + i = 0 + while i < len(pairs) - 1: + key = pairs[i] + val = pairs[i + 1] + if isinstance(key, Keyword): + parts.append(f"{self._js_string(key.name)}: {self.emit(val)}") + else: + parts.append(f"[{self.emit(key)}]: {self.emit(val)}") + i += 2 + return "{" + ", ".join(parts) + "}" + + def _emit_infix(self, op: str, args: list) -> str: + JS_OPS = {"=": "==", "!=": "!=", "mod": "%"} + js_op = JS_OPS.get(op, op) + if len(args) == 1 and op == "-": + return f"(-{self.emit(args[0])})" + return f"({self.emit(args[0])} {js_op} {self.emit(args[1])})" + + def _emit_define(self, expr) -> str: + name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) + val = self.emit(expr[2]) + return f"var {self._mangle(name)} = {val};" + + def _emit_for_each_stmt(self, expr) -> str: + fn_expr = expr[1] + coll_expr = expr[2] + coll = self.emit(coll_expr) + # If fn is an inline lambda, emit a for loop + if isinstance(fn_expr, list) and fn_expr[0] == Symbol("fn"): + params = fn_expr[1] + body = fn_expr[2] + p = params[0].name if isinstance(params[0], Symbol) else str(params[0]) + p_js = self._mangle(p) + body_js = self.emit_statement(body) + return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ var {p_js} = _c[_i]; {body_js} }} }}" + fn = self.emit(fn_expr) + return f"{{ var _c = {coll}; for (var _i = 0; _i < _c.length; _i++) {{ {fn}(_c[_i]); }} }}" + + def _emit_quote(self, expr) -> str: + """Emit a quoted expression as a JS literal AST.""" + if isinstance(expr, bool): + return "true" if expr else "false" + if isinstance(expr, (int, float)): + return str(expr) + if isinstance(expr, str): + return self._js_string(expr) + if expr is None or expr is SX_NIL: + return "NIL" + if isinstance(expr, Symbol): + return f'new Symbol({self._js_string(expr.name)})' + if isinstance(expr, Keyword): + return f'new Keyword({self._js_string(expr.name)})' + if isinstance(expr, list): + return "[" + ", ".join(self._emit_quote(x) for x in expr) + "]" + return str(expr) + + def _js_string(self, s: str) -> str: + return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + '"' + + +# --------------------------------------------------------------------------- +# Bootstrap compiler +# --------------------------------------------------------------------------- + +def extract_defines(source: str) -> list[tuple[str, list]]: + """Parse .sx source, return list of (name, define-expr) for top-level defines.""" + exprs = parse_all(source) + defines = [] + for expr in exprs: + if isinstance(expr, list) and expr and isinstance(expr[0], Symbol): + if expr[0].name == "define": + name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1]) + defines.append((name, expr)) + return defines + + +def compile_ref_to_js() -> str: + """Read reference .sx files and emit JavaScript.""" + ref_dir = os.path.dirname(os.path.abspath(__file__)) + emitter = JSEmitter() + + # Read reference files + with open(os.path.join(ref_dir, "eval.sx")) as f: + eval_src = f.read() + with open(os.path.join(ref_dir, "render.sx")) as f: + render_src = f.read() + + eval_defines = extract_defines(eval_src) + render_defines = extract_defines(render_src) + + # Build output + parts = [] + parts.append(PREAMBLE) + parts.append(PLATFORM_JS) + parts.append("\n // === Transpiled from eval.sx ===\n") + for name, expr in eval_defines: + parts.append(f" // {name}") + parts.append(f" {emitter.emit_statement(expr)}") + parts.append("") + parts.append("\n // === Transpiled from render.sx ===\n") + for name, expr in render_defines: + parts.append(f" // {name}") + parts.append(f" {emitter.emit_statement(expr)}") + parts.append("") + parts.append(FIXUPS) + parts.append(PUBLIC_API) + parts.append(EPILOGUE) + return "\n".join(parts) + + +# --------------------------------------------------------------------------- +# Static JS sections +# --------------------------------------------------------------------------- + +PREAMBLE = '''\ +/** + * sx-ref.js — Generated from reference SX evaluator specification. + * + * Bootstrap-compiled from shared/sx/ref/{eval,render,primitives}.sx + * Compare against hand-written sx.js for correctness verification. + * + * DO NOT EDIT — regenerate with: python bootstrap_js.py + */ +;(function(global) { + "use strict"; + + // ========================================================================= + // Types + // ========================================================================= + + var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); + + function isNil(x) { return x === NIL || x === null || x === undefined; } + function isSxTruthy(x) { return x !== false && !isNil(x); } + + function Symbol(name) { this.name = name; } + Symbol.prototype.toString = function() { return this.name; }; + Symbol.prototype._sym = true; + + function Keyword(name) { this.name = name; } + Keyword.prototype.toString = function() { return ":" + this.name; }; + Keyword.prototype._kw = true; + + function Lambda(params, body, closure, name) { + this.params = params; + this.body = body; + this.closure = closure || {}; + this.name = name || null; + } + Lambda.prototype._lambda = true; + + function Component(name, params, hasChildren, body, closure) { + this.name = name; + this.params = params; + this.hasChildren = hasChildren; + this.body = body; + this.closure = closure || {}; + } + Component.prototype._component = true; + + function Macro(params, restParam, body, closure, name) { + this.params = params; + this.restParam = restParam; + this.body = body; + this.closure = closure || {}; + this.name = name || null; + } + Macro.prototype._macro = true; + + function Thunk(expr, env) { this.expr = expr; this.env = env; } + Thunk.prototype._thunk = true; + + function RawHTML(html) { this.html = html; } + RawHTML.prototype._raw = true; + + function isSym(x) { return x != null && x._sym === true; } + function isKw(x) { return x != null && x._kw === true; } + + function merge() { + var out = {}; + for (var i = 0; i < arguments.length; i++) { + var d = arguments[i]; + if (d) for (var k in d) out[k] = d[k]; + } + return out; + } + + function sxOr() { + for (var i = 0; i < arguments.length; i++) { + if (isSxTruthy(arguments[i])) return arguments[i]; + } + return arguments.length ? arguments[arguments.length - 1] : false; + }''' + +PLATFORM_JS = ''' + // ========================================================================= + // Platform interface — JS implementation + // ========================================================================= + + function typeOf(x) { + if (isNil(x)) return "nil"; + if (typeof x === "number") return "number"; + if (typeof x === "string") return "string"; + if (typeof x === "boolean") return "boolean"; + if (x._sym) return "symbol"; + if (x._kw) return "keyword"; + if (x._thunk) return "thunk"; + if (x._lambda) return "lambda"; + if (x._component) return "component"; + if (x._macro) return "macro"; + if (x._raw) return "raw-html"; + if (Array.isArray(x)) return "list"; + if (typeof x === "object") return "dict"; + return "unknown"; + } + + function symbolName(s) { return s.name; } + function keywordName(k) { return k.name; } + function makeSymbol(n) { return new Symbol(n); } + function makeKeyword(n) { return new Keyword(n); } + + function makeLambda(params, body, env) { return new Lambda(params, body, merge(env)); } + function makeComponent(name, params, hasChildren, body, env) { + return new Component(name, params, hasChildren, body, merge(env)); + } + function makeMacro(params, restParam, body, env, name) { + return new Macro(params, restParam, body, merge(env), name); + } + function makeThunk(expr, env) { return new Thunk(expr, env); } + + function lambdaParams(f) { return f.params; } + function lambdaBody(f) { return f.body; } + function lambdaClosure(f) { return f.closure; } + function lambdaName(f) { return f.name; } + function setLambdaName(f, n) { f.name = n; } + + function componentParams(c) { return c.params; } + function componentBody(c) { return c.body; } + function componentClosure(c) { return c.closure; } + function componentHasChildren(c) { return c.hasChildren; } + function componentName(c) { return c.name; } + + function macroParams(m) { return m.params; } + function macroRestParam(m) { return m.restParam; } + function macroBody(m) { return m.body; } + function macroClosure(m) { return m.closure; } + + function isThunk(x) { return x != null && x._thunk === true; } + function thunkExpr(t) { return t.expr; } + function thunkEnv(t) { return t.env; } + + function isCallable(x) { return typeof x === "function" || (x != null && x._lambda === true); } + function isLambda(x) { return x != null && x._lambda === true; } + function isComponent(x) { return x != null && x._component === true; } + function isMacro(x) { return x != null && x._macro === true; } + + function envHas(env, name) { return name in env; } + function envGet(env, name) { return env[name]; } + function envSet(env, name, val) { env[name] = val; } + function envExtend(env) { return merge(env); } + function envMerge(base, overlay) { return merge(base, overlay); } + + function dictSet(d, k, v) { d[k] = v; } + function dictGet(d, k) { var v = d[k]; return v !== undefined ? v : NIL; } + + function stripPrefix(s, prefix) { + return s.indexOf(prefix) === 0 ? s.slice(prefix.length) : s; + } + + function error(msg) { throw new Error(msg); } + function inspect(x) { return JSON.stringify(x); } + + // ========================================================================= + // Primitives + // ========================================================================= + + var PRIMITIVES = {}; + + // Arithmetic + PRIMITIVES["+"] = function() { var s = 0; for (var i = 0; i < arguments.length; i++) s += arguments[i]; return s; }; + PRIMITIVES["-"] = function(a, b) { return arguments.length === 1 ? -a : a - b; }; + PRIMITIVES["*"] = function() { var s = 1; for (var i = 0; i < arguments.length; i++) s *= arguments[i]; return s; }; + PRIMITIVES["/"] = function(a, b) { return a / b; }; + PRIMITIVES["mod"] = function(a, b) { return a % b; }; + PRIMITIVES["inc"] = function(n) { return n + 1; }; + PRIMITIVES["dec"] = function(n) { return n - 1; }; + PRIMITIVES["abs"] = Math.abs; + PRIMITIVES["floor"] = Math.floor; + PRIMITIVES["ceil"] = Math.ceil; + PRIMITIVES["round"] = Math.round; + PRIMITIVES["min"] = Math.min; + PRIMITIVES["max"] = Math.max; + PRIMITIVES["sqrt"] = Math.sqrt; + PRIMITIVES["pow"] = Math.pow; + PRIMITIVES["clamp"] = function(x, lo, hi) { return Math.max(lo, Math.min(hi, x)); }; + + // Comparison + PRIMITIVES["="] = function(a, b) { return a == b; }; + PRIMITIVES["!="] = function(a, b) { return a != b; }; + PRIMITIVES["<"] = function(a, b) { return a < b; }; + PRIMITIVES[">"] = function(a, b) { return a > b; }; + PRIMITIVES["<="] = function(a, b) { return a <= b; }; + PRIMITIVES[">="] = function(a, b) { return a >= b; }; + + // Logic + PRIMITIVES["not"] = function(x) { return !isSxTruthy(x); }; + + // String + PRIMITIVES["str"] = function() { + var p = []; + for (var i = 0; i < arguments.length; i++) { + var v = arguments[i]; if (isNil(v)) continue; p.push(String(v)); + } + return p.join(""); + }; + PRIMITIVES["upper"] = function(s) { return String(s).toUpperCase(); }; + PRIMITIVES["lower"] = function(s) { return String(s).toLowerCase(); }; + PRIMITIVES["trim"] = function(s) { return String(s).trim(); }; + 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["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) { return b !== undefined ? c.slice(a, b) : c.slice(a); }; + PRIMITIVES["concat"] = function() { + var out = []; + for (var i = 0; i < arguments.length; i++) if (arguments[i]) out = out.concat(arguments[i]); + return out; + }; + PRIMITIVES["strip-tags"] = function(s) { return String(s).replace(/<[^>]+>/g, ""); }; + + // Predicates + PRIMITIVES["nil?"] = isNil; + PRIMITIVES["number?"] = function(x) { return typeof x === "number"; }; + PRIMITIVES["string?"] = function(x) { return typeof x === "string"; }; + PRIMITIVES["list?"] = Array.isArray; + PRIMITIVES["dict?"] = function(x) { return x !== null && typeof x === "object" && !Array.isArray(x) && !x._sym && !x._kw; }; + PRIMITIVES["empty?"] = function(c) { return isNil(c) || (Array.isArray(c) ? c.length === 0 : typeof c === "string" ? c.length === 0 : Object.keys(c).length === 0); }; + PRIMITIVES["contains?"] = function(c, k) { + if (typeof c === "string") return c.indexOf(String(k)) !== -1; + if (Array.isArray(c)) return c.indexOf(k) !== -1; + return k in c; + }; + PRIMITIVES["odd?"] = function(n) { return n % 2 !== 0; }; + PRIMITIVES["even?"] = function(n) { return n % 2 === 0; }; + PRIMITIVES["zero?"] = function(n) { return n === 0; }; + + // Collections + PRIMITIVES["list"] = function() { return Array.prototype.slice.call(arguments); }; + PRIMITIVES["dict"] = function() { + var d = {}; + for (var i = 0; i < arguments.length - 1; i += 2) d[arguments[i]] = arguments[i + 1]; + return d; + }; + PRIMITIVES["range"] = function(a, b, step) { + var r = []; step = step || 1; + for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i); + return r; + }; + PRIMITIVES["get"] = function(c, k, def) { var v = (c && c[k]); return v !== undefined ? v : (def !== undefined ? def : NIL); }; + PRIMITIVES["len"] = function(c) { return Array.isArray(c) ? c.length : typeof c === "string" ? c.length : Object.keys(c).length; }; + PRIMITIVES["first"] = function(c) { return c && c.length > 0 ? c[0] : NIL; }; + PRIMITIVES["last"] = function(c) { return c && c.length > 0 ? c[c.length - 1] : NIL; }; + PRIMITIVES["rest"] = function(c) { return c ? c.slice(1) : []; }; + PRIMITIVES["nth"] = function(c, n) { return c && n >= 0 && n < c.length ? c[n] : NIL; }; + PRIMITIVES["cons"] = function(x, c) { return [x].concat(c || []); }; + PRIMITIVES["append"] = function(c, x) { return (c || []).concat([x]); }; + PRIMITIVES["keys"] = function(d) { return Object.keys(d || {}); }; + PRIMITIVES["vals"] = function(d) { var r = []; for (var k in d) r.push(d[k]); return r; }; + PRIMITIVES["merge"] = function() { + var out = {}; + for (var i = 0; i < arguments.length; i++) { var d = arguments[i]; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; } + return out; + }; + PRIMITIVES["assoc"] = function(d) { + var out = {}; if (d && !isNil(d)) for (var k in d) out[k] = d[k]; + for (var i = 1; i < arguments.length - 1; i += 2) out[arguments[i]] = arguments[i + 1]; + return out; + }; + PRIMITIVES["dissoc"] = function(d) { + var out = {}; for (var k in d) out[k] = d[k]; + for (var i = 1; i < arguments.length; i++) delete out[arguments[i]]; + return out; + }; + PRIMITIVES["chunk-every"] = function(c, n) { + var r = []; for (var i = 0; i < c.length; i += n) r.push(c.slice(i, i + n)); return r; + }; + PRIMITIVES["zip-pairs"] = function(c) { + var r = []; for (var i = 0; i < c.length - 1; i++) r.push([c[i], c[i + 1]]); return r; + }; + PRIMITIVES["into"] = function(target, coll) { + if (Array.isArray(target)) return Array.isArray(coll) ? coll.slice() : Object.entries(coll); + var r = {}; for (var i = 0; i < coll.length; i++) { var p = coll[i]; if (Array.isArray(p) && p.length >= 2) r[p[0]] = p[1]; } + return r; + }; + + // Format + PRIMITIVES["format-decimal"] = function(v, p) { return Number(v).toFixed(p || 2); }; + PRIMITIVES["parse-int"] = function(v, d) { var n = parseInt(v, 10); return isNaN(n) ? (d || 0) : n; }; + PRIMITIVES["pluralize"] = function(n, s, p) { + if (s || (p && p !== "s")) return n == 1 ? (s || "") : (p || "s"); + return n == 1 ? "" : "s"; + }; + PRIMITIVES["escape"] = function(s) { + return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); + }; + + function isPrimitive(name) { return name in PRIMITIVES; } + function getPrimitive(name) { return PRIMITIVES[name]; } + + // Higher-order helpers used by the transpiled code + function map(fn, coll) { return coll.map(fn); } + function mapIndexed(fn, coll) { return coll.map(function(item, i) { return fn(i, item); }); } + function filter(fn, coll) { return coll.filter(function(x) { return isSxTruthy(fn(x)); }); } + function reduce(fn, init, coll) { + var acc = init; + for (var i = 0; i < coll.length; i++) acc = fn(acc, coll[i]); + return acc; + } + function some(fn, coll) { + for (var i = 0; i < coll.length; i++) { var r = fn(coll[i]); if (isSxTruthy(r)) return r; } + return NIL; + } + function forEach(fn, coll) { for (var i = 0; i < coll.length; i++) fn(coll[i]); return NIL; } + function mapDict(fn, d) { var r = {}; for (var k in d) r[k] = fn(k, d[k]); return r; } + + // List primitives used directly by transpiled code + var len = PRIMITIVES["len"]; + var first = PRIMITIVES["first"]; + var last = PRIMITIVES["last"]; + var rest = PRIMITIVES["rest"]; + var nth = PRIMITIVES["nth"]; + var cons = PRIMITIVES["cons"]; + var append = PRIMITIVES["append"]; + var isEmpty = PRIMITIVES["empty?"]; + var contains = PRIMITIVES["contains?"]; + var startsWith = PRIMITIVES["starts-with?"]; + var slice = PRIMITIVES["slice"]; + var concat = PRIMITIVES["concat"]; + var str = PRIMITIVES["str"]; + var join = PRIMITIVES["join"]; + var keys = PRIMITIVES["keys"]; + var get = PRIMITIVES["get"]; + var assoc = PRIMITIVES["assoc"]; + var range = PRIMITIVES["range"]; + function zip(a, b) { var r = []; for (var i = 0; i < Math.min(a.length, b.length); i++) r.push([a[i], b[i]]); return r; } + function append_b(arr, x) { arr.push(x); return arr; } + var apply = function(f, args) { return f.apply(null, args); }; + + // HTML rendering helpers + function escapeHtml(s) { + return String(s).replace(/&/g,"&").replace(//g,">").replace(/"/g,"""); + } + function escapeAttr(s) { return escapeHtml(s); } + function rawHtmlContent(r) { return r.html; } + + // Serializer + function serialize(val) { + if (isNil(val)) return "nil"; + if (typeof val === "boolean") return val ? "true" : "false"; + if (typeof val === "number") return String(val); + if (typeof val === "string") return \'"\' + val.replace(/\\\\/g, "\\\\\\\\").replace(/"/g, \'\\\\"\') + \'"\'; + if (isSym(val)) return val.name; + if (isKw(val)) return ":" + val.name; + if (Array.isArray(val)) return "(" + val.map(serialize).join(" ") + ")"; + return String(val); + } + + function isSpecialForm(n) { return n in { + "if":1,"when":1,"cond":1,"case":1,"and":1,"or":1,"let":1,"let*":1, + "lambda":1,"fn":1,"define":1,"defcomp":1,"defmacro":1,"begin":1,"do":1, + "quote":1,"quasiquote":1,"->":1,"set!":1 + }; } + function isHoForm(n) { return n in { + "map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1 + }; }''' + +FIXUPS = ''' + // ========================================================================= + // Post-transpilation fixups + // ========================================================================= + // The reference spec's call-lambda only handles Lambda objects, but HO forms + // (map, reduce, etc.) may receive native primitives. Wrap to handle both. + var _rawCallLambda = callLambda; + callLambda = function(f, args, callerEnv) { + if (typeof f === "function") return f.apply(null, args); + return _rawCallLambda(f, args, callerEnv); + };''' + +PUBLIC_API = ''' + // ========================================================================= + // Parser (reused from reference — hand-written for bootstrap simplicity) + // ========================================================================= + + // The parser is the one piece we keep as hand-written JS since the + // reference parser.sx is more of a spec than directly compilable code + // (it uses mutable cursor state that doesn't map cleanly to the + // transpiler's functional output). A future version could bootstrap + // the parser too. + + function parse(text) { + var pos = 0; + function skipWs() { + while (pos < text.length) { + var ch = text[pos]; + if (ch === " " || ch === "\\t" || ch === "\\n" || ch === "\\r") { pos++; continue; } + if (ch === ";") { while (pos < text.length && text[pos] !== "\\n") pos++; continue; } + break; + } + } + function readExpr() { + skipWs(); + if (pos >= text.length) return undefined; + var ch = text[pos]; + if (ch === "(") { pos++; return readList(")"); } + if (ch === "[") { pos++; return readList("]"); } + if (ch === \'"\') return readString(); + if (ch === ":") return readKeyword(); + if (ch === "-" && pos + 1 < text.length && text[pos + 1] >= "0" && text[pos + 1] <= "9") return readNumber(); + if (ch >= "0" && ch <= "9") return readNumber(); + return readSymbol(); + } + function readList(close) { + var items = []; + while (true) { + skipWs(); + if (pos >= text.length) throw new Error("Unterminated list"); + if (text[pos] === close) { pos++; return items; } + items.push(readExpr()); + } + } + function readString() { + pos++; // skip " + var s = ""; + while (pos < text.length) { + var ch = text[pos]; + if (ch === \'"\') { pos++; return s; } + if (ch === "\\\\") { pos++; var esc = text[pos]; s += esc === "n" ? "\\n" : esc === "t" ? "\\t" : esc === "r" ? "\\r" : esc; pos++; continue; } + s += ch; pos++; + } + throw new Error("Unterminated string"); + } + function readKeyword() { + pos++; // skip : + var name = readIdent(); + return new Keyword(name); + } + function readNumber() { + var start = pos; + if (text[pos] === "-") pos++; + while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++; + if (pos < text.length && text[pos] === ".") { pos++; while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++; } + if (pos < text.length && (text[pos] === "e" || text[pos] === "E")) { + pos++; + if (pos < text.length && (text[pos] === "+" || text[pos] === "-")) pos++; + while (pos < text.length && text[pos] >= "0" && text[pos] <= "9") pos++; + } + return Number(text.slice(start, pos)); + } + function readIdent() { + var start = pos; + while (pos < text.length && /[a-zA-Z0-9_~*+\\-><=/!?.:&]/.test(text[pos])) pos++; + return text.slice(start, pos); + } + function readSymbol() { + var name = readIdent(); + if (name === "true") return true; + if (name === "false") return false; + if (name === "nil") return NIL; + return new Symbol(name); + } + var exprs = []; + while (true) { + skipWs(); + if (pos >= text.length) break; + exprs.push(readExpr()); + } + return exprs; + } + + // ========================================================================= + // Public API + // ========================================================================= + + var componentEnv = {}; + + function loadComponents(source) { + var exprs = parse(source); + for (var i = 0; i < exprs.length; i++) { + trampoline(evalExpr(exprs[i], componentEnv)); + } + } + + function render(source) { + var exprs = parse(source); + var frag = document.createDocumentFragment(); + for (var i = 0; i < exprs.length; i++) { + var result = trampoline(evalExpr(exprs[i], merge(componentEnv))); + appendToDOM(frag, result, merge(componentEnv)); + } + return frag; + } + + function appendToDOM(parent, val, env) { + if (isNil(val)) return; + if (typeof val === "string") { parent.appendChild(document.createTextNode(val)); return; } + if (typeof val === "number") { parent.appendChild(document.createTextNode(String(val))); return; } + if (val._raw) { var t = document.createElement("template"); t.innerHTML = val.html; parent.appendChild(t.content); return; } + if (Array.isArray(val)) { + // Could be a rendered element or a list of results + if (val.length > 0 && isSym(val[0])) { + // It's an unevaluated expression — evaluate it + var result = trampoline(evalExpr(val, env)); + appendToDOM(parent, result, env); + } else { + for (var i = 0; i < val.length; i++) appendToDOM(parent, val[i], env); + } + return; + } + parent.appendChild(document.createTextNode(String(val))); + } + + var SxRef = { + parse: parse, + eval: function(expr, env) { return trampoline(evalExpr(expr, env || merge(componentEnv))); }, + loadComponents: loadComponents, + render: render, + serialize: serialize, + NIL: NIL, + Symbol: Symbol, + Keyword: Keyword, + componentEnv: componentEnv, + _version: "ref-1.0 (bootstrap-compiled)" + }; + + if (typeof module !== "undefined" && module.exports) module.exports = SxRef; + else global.SxRef = SxRef;''' + +EPILOGUE = ''' +})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);''' + + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +if __name__ == "__main__": + print(compile_ref_to_js())