Fix quasiquote flattening bug, decouple relations from evaluator
- Fix qq-expand in eval.sx: use concat+list instead of append to prevent
nested lists from being flattened during quasiquote expansion
- Update append primitive to match spec ("if x is list, concatenate")
- Rebuild sx_ref.py with quasiquote fix
- Make relations.py self-contained: parse defrelation AST directly
without depending on the evaluator (25/25 tests pass)
- Replace hand-written JSEmitter with js.sx self-hosting bootstrapper
- Guard server-only tests in test-eval.sx with runtime check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } });
|
||||||
var SX_VERSION = "2026-03-11T03:56:09Z";
|
var SX_VERSION = "2026-03-11T04:41:27Z";
|
||||||
|
|
||||||
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
function isNil(x) { return x === NIL || x === null || x === undefined; }
|
||||||
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
function isSxTruthy(x) { return x !== false && !isNil(x); }
|
||||||
@@ -312,6 +312,7 @@
|
|||||||
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
|
PRIMITIVES["even?"] = function(n) { return n % 2 === 0; };
|
||||||
PRIMITIVES["zero?"] = function(n) { return n === 0; };
|
PRIMITIVES["zero?"] = function(n) { return n === 0; };
|
||||||
PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; };
|
PRIMITIVES["boolean?"] = function(x) { return x === true || x === false; };
|
||||||
|
PRIMITIVES["component-affinity"] = componentAffinity;
|
||||||
|
|
||||||
|
|
||||||
// core.strings
|
// core.strings
|
||||||
@@ -579,6 +580,92 @@
|
|||||||
return makeThunk(componentBody(comp), local);
|
return makeThunk(componentBody(comp), local);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Platform: deps module — component dependency analysis
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
function componentDeps(c) {
|
||||||
|
return c.deps ? c.deps.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentSetDeps(c, deps) {
|
||||||
|
c.deps = deps;
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentCssClasses(c) {
|
||||||
|
return c.cssClasses ? c.cssClasses.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function envComponents(env) {
|
||||||
|
var names = [];
|
||||||
|
for (var k in env) {
|
||||||
|
var v = env[k];
|
||||||
|
if (v && (v._component || v._macro)) names.push(k);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
function regexFindAll(pattern, source) {
|
||||||
|
var re = new RegExp(pattern, "g");
|
||||||
|
var results = [];
|
||||||
|
var m;
|
||||||
|
while ((m = re.exec(source)) !== null) {
|
||||||
|
if (m[1] !== undefined) results.push(m[1]);
|
||||||
|
else results.push(m[0]);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanCssClasses(source) {
|
||||||
|
var classes = {};
|
||||||
|
var result = [];
|
||||||
|
var m;
|
||||||
|
var re1 = /:class\s+"([^"]*)"/g;
|
||||||
|
while ((m = re1.exec(source)) !== null) {
|
||||||
|
var parts = m[1].split(/\s+/);
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
if (parts[i] && !classes[parts[i]]) {
|
||||||
|
classes[parts[i]] = true;
|
||||||
|
result.push(parts[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var re2 = /:class\s+\(str\s+((?:"[^"]*"\s*)+)\)/g;
|
||||||
|
while ((m = re2.exec(source)) !== null) {
|
||||||
|
var re3 = /"([^"]*)"/g;
|
||||||
|
var m2;
|
||||||
|
while ((m2 = re3.exec(m[1])) !== null) {
|
||||||
|
var parts2 = m2[1].split(/\s+/);
|
||||||
|
for (var j = 0; j < parts2.length; j++) {
|
||||||
|
if (parts2[j] && !classes[parts2[j]]) {
|
||||||
|
classes[parts2[j]] = true;
|
||||||
|
result.push(parts2[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var re4 = /;;\s*@css\s+(.+)/g;
|
||||||
|
while ((m = re4.exec(source)) !== null) {
|
||||||
|
var parts3 = m[1].split(/\s+/);
|
||||||
|
for (var k = 0; k < parts3.length; k++) {
|
||||||
|
if (parts3[k] && !classes[parts3[k]]) {
|
||||||
|
classes[parts3[k]] = true;
|
||||||
|
result.push(parts3[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentIoRefs(c) {
|
||||||
|
return c.ioRefs ? c.ioRefs.slice() : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentSetIoRefs(c, refs) {
|
||||||
|
c.ioRefs = refs;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Platform interface — Parser
|
// Platform interface — Parser
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -3185,6 +3272,167 @@ callExpr.push(dictGet(kwargs, k)); } }
|
|||||||
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), sxHydrateIslands(NIL), processElements(NIL)); };
|
var bootInit = function() { return (logInfo((String("sx-browser ") + String(SX_VERSION))), initCssTracking(), processPageScripts(), processSxScripts(NIL), sxHydrateElements(NIL), sxHydrateIslands(NIL), processElements(NIL)); };
|
||||||
|
|
||||||
|
|
||||||
|
// === Transpiled from deps (component dependency analysis) ===
|
||||||
|
|
||||||
|
// scan-refs
|
||||||
|
var scanRefs = function(node) { return (function() {
|
||||||
|
var refs = [];
|
||||||
|
scanRefsWalk(node, refs);
|
||||||
|
return refs;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// scan-refs-walk
|
||||||
|
var scanRefsWalk = function(node, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
|
||||||
|
var name = symbolName(node);
|
||||||
|
return (isSxTruthy(startsWith(name, "~")) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL);
|
||||||
|
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanRefsWalk(item, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanRefsWalk(dictGet(node, key), refs); }, keys(node)) : NIL))); };
|
||||||
|
|
||||||
|
// transitive-deps-walk
|
||||||
|
var transitiveDepsWalk = function(n, seen, env) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() {
|
||||||
|
var val = envGet(env, n);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(componentBody(val))) : (isSxTruthy((typeOf(val) == "macro")) ? forEach(function(ref) { return transitiveDepsWalk(ref, seen, env); }, scanRefs(macroBody(val))) : NIL));
|
||||||
|
})()) : NIL); };
|
||||||
|
|
||||||
|
// transitive-deps
|
||||||
|
var transitiveDeps = function(name, env) { return (function() {
|
||||||
|
var seen = [];
|
||||||
|
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||||
|
transitiveDepsWalk(key, seen, env);
|
||||||
|
return filter(function(x) { return !isSxTruthy((x == key)); }, seen);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// compute-all-deps
|
||||||
|
var computeAllDeps = function(env) { return forEach(function(name) { return (function() {
|
||||||
|
var val = envGet(env, name);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? componentSetDeps(val, transitiveDeps(name, env)) : NIL);
|
||||||
|
})(); }, envComponents(env)); };
|
||||||
|
|
||||||
|
// scan-components-from-source
|
||||||
|
var scanComponentsFromSource = function(source) { return (function() {
|
||||||
|
var matches = regexFindAll("\\(~([a-zA-Z_][a-zA-Z0-9_\\-]*)", source);
|
||||||
|
return map(function(m) { return (String("~") + String(m)); }, matches);
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// components-needed
|
||||||
|
var componentsNeeded = function(pageSource, env) { return (function() {
|
||||||
|
var direct = scanComponentsFromSource(pageSource);
|
||||||
|
var allNeeded = [];
|
||||||
|
{ var _c = direct; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(allNeeded, name)))) {
|
||||||
|
allNeeded.push(name);
|
||||||
|
}
|
||||||
|
(function() {
|
||||||
|
var val = envGet(env, name);
|
||||||
|
return (function() {
|
||||||
|
var deps = (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isSxTruthy(isEmpty(componentDeps(val))))) ? componentDeps(val) : transitiveDeps(name, env));
|
||||||
|
return forEach(function(dep) { return (isSxTruthy(!isSxTruthy(contains(allNeeded, dep))) ? append_b(allNeeded, dep) : NIL); }, deps);
|
||||||
|
})();
|
||||||
|
})(); } }
|
||||||
|
return allNeeded;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// page-component-bundle
|
||||||
|
var pageComponentBundle = function(pageSource, env) { return componentsNeeded(pageSource, env); };
|
||||||
|
|
||||||
|
// page-css-classes
|
||||||
|
var pageCssClasses = function(pageSource, env) { return (function() {
|
||||||
|
var needed = componentsNeeded(pageSource, env);
|
||||||
|
var classes = [];
|
||||||
|
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() {
|
||||||
|
var val = envGet(env, name);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? forEach(function(cls) { return (isSxTruthy(!isSxTruthy(contains(classes, cls))) ? append_b(classes, cls) : NIL); }, componentCssClasses(val)) : NIL);
|
||||||
|
})(); } }
|
||||||
|
{ var _c = scanCssClasses(pageSource); for (var _i = 0; _i < _c.length; _i++) { var cls = _c[_i]; if (isSxTruthy(!isSxTruthy(contains(classes, cls)))) {
|
||||||
|
classes.push(cls);
|
||||||
|
} } }
|
||||||
|
return classes;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// scan-io-refs-walk
|
||||||
|
var scanIoRefsWalk = function(node, ioNames, refs) { return (isSxTruthy((typeOf(node) == "symbol")) ? (function() {
|
||||||
|
var name = symbolName(node);
|
||||||
|
return (isSxTruthy(contains(ioNames, name)) ? (isSxTruthy(!isSxTruthy(contains(refs, name))) ? append_b(refs, name) : NIL) : NIL);
|
||||||
|
})() : (isSxTruthy((typeOf(node) == "list")) ? forEach(function(item) { return scanIoRefsWalk(item, ioNames, refs); }, node) : (isSxTruthy((typeOf(node) == "dict")) ? forEach(function(key) { return scanIoRefsWalk(dictGet(node, key), ioNames, refs); }, keys(node)) : NIL))); };
|
||||||
|
|
||||||
|
// scan-io-refs
|
||||||
|
var scanIoRefs = function(node, ioNames) { return (function() {
|
||||||
|
var refs = [];
|
||||||
|
scanIoRefsWalk(node, ioNames, refs);
|
||||||
|
return refs;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// transitive-io-refs-walk
|
||||||
|
var transitiveIoRefsWalk = function(n, seen, allRefs, env, ioNames) { return (isSxTruthy(!isSxTruthy(contains(seen, n))) ? (append_b(seen, n), (function() {
|
||||||
|
var val = envGet(env, n);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(componentBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(componentBody(val)))) : (isSxTruthy((typeOf(val) == "macro")) ? (forEach(function(ref) { return (isSxTruthy(!isSxTruthy(contains(allRefs, ref))) ? append_b(allRefs, ref) : NIL); }, scanIoRefs(macroBody(val), ioNames)), forEach(function(dep) { return transitiveIoRefsWalk(dep, seen, allRefs, env, ioNames); }, scanRefs(macroBody(val)))) : NIL));
|
||||||
|
})()) : NIL); };
|
||||||
|
|
||||||
|
// transitive-io-refs
|
||||||
|
var transitiveIoRefs = function(name, env, ioNames) { return (function() {
|
||||||
|
var allRefs = [];
|
||||||
|
var seen = [];
|
||||||
|
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||||
|
transitiveIoRefsWalk(key, seen, allRefs, env, ioNames);
|
||||||
|
return allRefs;
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// compute-all-io-refs
|
||||||
|
var computeAllIoRefs = function(env, ioNames) { return forEach(function(name) { return (function() {
|
||||||
|
var val = envGet(env, name);
|
||||||
|
return (isSxTruthy((typeOf(val) == "component")) ? componentSetIoRefs(val, transitiveIoRefs(name, env, ioNames)) : NIL);
|
||||||
|
})(); }, envComponents(env)); };
|
||||||
|
|
||||||
|
// component-io-refs-cached
|
||||||
|
var componentIoRefsCached = function(name, env, ioNames) { return (function() {
|
||||||
|
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||||
|
return (function() {
|
||||||
|
var val = envGet(env, key);
|
||||||
|
return (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && isSxTruthy(!isSxTruthy(isNil(componentIoRefs(val)))) && !isSxTruthy(isEmpty(componentIoRefs(val))))) ? componentIoRefs(val) : transitiveIoRefs(name, env, ioNames));
|
||||||
|
})();
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// component-pure?
|
||||||
|
var componentPure_p = function(name, env, ioNames) { return (function() {
|
||||||
|
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||||
|
return (function() {
|
||||||
|
var val = envGet(env, key);
|
||||||
|
return (isSxTruthy((isSxTruthy((typeOf(val) == "component")) && !isSxTruthy(isNil(componentIoRefs(val))))) ? isEmpty(componentIoRefs(val)) : isEmpty(transitiveIoRefs(name, env, ioNames)));
|
||||||
|
})();
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// render-target
|
||||||
|
var renderTarget = function(name, env, ioNames) { return (function() {
|
||||||
|
var key = (isSxTruthy(startsWith(name, "~")) ? name : (String("~") + String(name)));
|
||||||
|
return (function() {
|
||||||
|
var val = envGet(env, key);
|
||||||
|
return (isSxTruthy(!isSxTruthy((typeOf(val) == "component"))) ? "server" : (function() {
|
||||||
|
var affinity = componentAffinity(val);
|
||||||
|
return (isSxTruthy((affinity == "server")) ? "server" : (isSxTruthy((affinity == "client")) ? "client" : (isSxTruthy(!isSxTruthy(componentPure_p(name, env, ioNames))) ? "server" : "client")));
|
||||||
|
})());
|
||||||
|
})();
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// page-render-plan
|
||||||
|
var pageRenderPlan = function(pageSource, env, ioNames) { return (function() {
|
||||||
|
var needed = componentsNeeded(pageSource, env);
|
||||||
|
var compTargets = {};
|
||||||
|
var serverList = [];
|
||||||
|
var clientList = [];
|
||||||
|
var ioDeps = [];
|
||||||
|
{ var _c = needed; for (var _i = 0; _i < _c.length; _i++) { var name = _c[_i]; (function() {
|
||||||
|
var target = renderTarget(name, env, ioNames);
|
||||||
|
compTargets[name] = target;
|
||||||
|
return (isSxTruthy((target == "server")) ? (append_b(serverList, name), forEach(function(ioRef) { return (isSxTruthy(!isSxTruthy(contains(ioDeps, ioRef))) ? append_b(ioDeps, ioRef) : NIL); }, componentIoRefsCached(name, env, ioNames))) : append_b(clientList, name));
|
||||||
|
})(); } }
|
||||||
|
return {"components": compTargets, "server": serverList, "client": clientList, "io-deps": ioDeps};
|
||||||
|
})(); };
|
||||||
|
|
||||||
|
// env-components
|
||||||
|
var envComponents = function(env) { return filter(function(k) { return (function() {
|
||||||
|
var v = envGet(env, k);
|
||||||
|
return sxOr(isComponent(v), isMacro(v));
|
||||||
|
})(); }, keys(env)); };
|
||||||
|
|
||||||
|
|
||||||
// === Transpiled from router (client-side route matching) ===
|
// === Transpiled from router (client-side route matching) ===
|
||||||
|
|
||||||
// split-path-segments
|
// split-path-segments
|
||||||
@@ -4813,6 +5061,35 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml;
|
if (typeof domOuterHtml === "function") PRIMITIVES["dom-outer-html"] = domOuterHtml;
|
||||||
if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml;
|
if (typeof domInnerHtml === "function") PRIMITIVES["dom-inner-html"] = domInnerHtml;
|
||||||
|
|
||||||
|
// Expose deps module functions as primitives so runtime-evaluated SX code
|
||||||
|
// (e.g. test-deps.sx in browser) can call them
|
||||||
|
// Platform functions (from PLATFORM_DEPS_JS)
|
||||||
|
PRIMITIVES["component-deps"] = componentDeps;
|
||||||
|
PRIMITIVES["component-set-deps!"] = componentSetDeps;
|
||||||
|
PRIMITIVES["component-css-classes"] = componentCssClasses;
|
||||||
|
PRIMITIVES["env-components"] = envComponents;
|
||||||
|
PRIMITIVES["regex-find-all"] = regexFindAll;
|
||||||
|
PRIMITIVES["scan-css-classes"] = scanCssClasses;
|
||||||
|
// Transpiled functions (from deps.sx)
|
||||||
|
PRIMITIVES["scan-refs"] = scanRefs;
|
||||||
|
PRIMITIVES["scan-refs-walk"] = scanRefsWalk;
|
||||||
|
PRIMITIVES["transitive-deps"] = transitiveDeps;
|
||||||
|
PRIMITIVES["transitive-deps-walk"] = transitiveDepsWalk;
|
||||||
|
PRIMITIVES["compute-all-deps"] = computeAllDeps;
|
||||||
|
PRIMITIVES["scan-components-from-source"] = scanComponentsFromSource;
|
||||||
|
PRIMITIVES["components-needed"] = componentsNeeded;
|
||||||
|
PRIMITIVES["page-component-bundle"] = pageComponentBundle;
|
||||||
|
PRIMITIVES["page-css-classes"] = pageCssClasses;
|
||||||
|
PRIMITIVES["scan-io-refs-walk"] = scanIoRefsWalk;
|
||||||
|
PRIMITIVES["scan-io-refs"] = scanIoRefs;
|
||||||
|
PRIMITIVES["transitive-io-refs-walk"] = transitiveIoRefsWalk;
|
||||||
|
PRIMITIVES["transitive-io-refs"] = transitiveIoRefs;
|
||||||
|
PRIMITIVES["compute-all-io-refs"] = computeAllIoRefs;
|
||||||
|
PRIMITIVES["component-io-refs-cached"] = componentIoRefsCached;
|
||||||
|
PRIMITIVES["component-pure?"] = componentPure_p;
|
||||||
|
PRIMITIVES["render-target"] = renderTarget;
|
||||||
|
PRIMITIVES["page-render-plan"] = pageRenderPlan;
|
||||||
|
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
// Async IO: Promise-aware rendering for client-side IO primitives
|
// Async IO: Promise-aware rendering for client-side IO primitives
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
@@ -5535,6 +5812,17 @@ return (isSxTruthy((_batchDepth == 0)) ? (function() {
|
|||||||
hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null,
|
hydrateIslands: typeof sxHydrateIslands === "function" ? sxHydrateIslands : null,
|
||||||
disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null,
|
disposeIsland: typeof disposeIsland === "function" ? disposeIsland : null,
|
||||||
init: typeof bootInit === "function" ? bootInit : null,
|
init: typeof bootInit === "function" ? bootInit : null,
|
||||||
|
scanRefs: scanRefs,
|
||||||
|
scanComponentsFromSource: scanComponentsFromSource,
|
||||||
|
transitiveDeps: transitiveDeps,
|
||||||
|
computeAllDeps: computeAllDeps,
|
||||||
|
componentsNeeded: componentsNeeded,
|
||||||
|
pageComponentBundle: pageComponentBundle,
|
||||||
|
pageCssClasses: pageCssClasses,
|
||||||
|
scanIoRefs: scanIoRefs,
|
||||||
|
transitiveIoRefs: transitiveIoRefs,
|
||||||
|
computeAllIoRefs: computeAllIoRefs,
|
||||||
|
componentPure_p: componentPure_p,
|
||||||
splitPathSegments: splitPathSegments,
|
splitPathSegments: splitPathSegments,
|
||||||
parseRoutePattern: parseRoutePattern,
|
parseRoutePattern: parseRoutePattern,
|
||||||
matchRoute: matchRoute,
|
matchRoute: matchRoute,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -656,8 +656,8 @@
|
|||||||
(let ((spliced (trampoline (eval-expr (nth item 1) env))))
|
(let ((spliced (trampoline (eval-expr (nth item 1) env))))
|
||||||
(if (= (type-of spliced) "list")
|
(if (= (type-of spliced) "list")
|
||||||
(concat result spliced)
|
(concat result spliced)
|
||||||
(if (nil? spliced) result (append result spliced))))
|
(if (nil? spliced) result (concat result (list spliced)))))
|
||||||
(append result (qq-expand item env))))
|
(concat result (list (qq-expand item env)))))
|
||||||
(list)
|
(list)
|
||||||
template)))))))
|
template)))))))
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,8 @@
|
|||||||
"eval-call" "evalCall"
|
"eval-call" "evalCall"
|
||||||
"is-render-expr?" "isRenderExpr"
|
"is-render-expr?" "isRenderExpr"
|
||||||
"render-expr" "renderExpr"
|
"render-expr" "renderExpr"
|
||||||
|
"render-active?" "renderActiveP"
|
||||||
|
"set-render-active!" "setRenderActiveB"
|
||||||
"call-lambda" "callLambda"
|
"call-lambda" "callLambda"
|
||||||
"call-component" "callComponent"
|
"call-component" "callComponent"
|
||||||
"parse-keyword-args" "parseKeywordArgs"
|
"parse-keyword-args" "parseKeywordArgs"
|
||||||
|
|||||||
@@ -774,7 +774,7 @@ PRIMITIVES["last"] = lambda c: c[-1] if c and _b_len(c) > 0 else NIL
|
|||||||
PRIMITIVES["rest"] = lambda c: c[1:] if c else []
|
PRIMITIVES["rest"] = lambda c: c[1:] if c else []
|
||||||
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
|
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
|
||||||
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
|
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
|
||||||
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
|
PRIMITIVES["append"] = lambda c, x: (c or []) + (x if isinstance(x, list) else [x])
|
||||||
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
||||||
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
||||||
''',
|
''',
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Bootstrap runner: execute js.sx against spec files to produce sx-ref.js.
|
Bootstrap compiler: js.sx (self-hosting SX-to-JS translator) → sx-browser.js.
|
||||||
|
|
||||||
This is the G1 bootstrapper — js.sx (SX-to-JavaScript translator written in SX)
|
This is the canonical JS bootstrapper. js.sx is loaded into the Python evaluator,
|
||||||
is loaded into the Python evaluator, which then uses it to translate the
|
which uses it to translate the .sx spec files into JavaScript. Platform code
|
||||||
spec .sx files into JavaScript.
|
(types, primitives, DOM interface) comes from platform_js.py.
|
||||||
|
|
||||||
The output (transpiled defines only) should be identical to what
|
|
||||||
bootstrap_js.py's JSEmitter produces.
|
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
python run_js_sx.py > /tmp/sx_ref_g1.js
|
python run_js_sx.py # stdout
|
||||||
|
python run_js_sx.py -o shared/static/scripts/sx-browser.js # file
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -19,14 +17,32 @@ import sys
|
|||||||
|
|
||||||
_HERE = os.path.dirname(os.path.abspath(__file__))
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
_PROJECT = os.path.abspath(os.path.join(_HERE, "..", "..", ".."))
|
||||||
sys.path.insert(0, _PROJECT)
|
if _PROJECT not in sys.path:
|
||||||
|
sys.path.insert(0, _PROJECT)
|
||||||
|
|
||||||
from shared.sx.parser import parse_all
|
from shared.sx.parser import parse_all
|
||||||
from shared.sx.types import Symbol
|
from shared.sx.types import Symbol
|
||||||
|
from shared.sx.ref.platform_js import (
|
||||||
|
extract_defines,
|
||||||
|
ADAPTER_FILES, ADAPTER_DEPS, SPEC_MODULES, EXTENSION_NAMES,
|
||||||
|
PREAMBLE, PLATFORM_JS_PRE, PLATFORM_JS_POST,
|
||||||
|
PRIMITIVES_JS_MODULES, _ALL_JS_MODULES, _assemble_primitives_js,
|
||||||
|
PLATFORM_DEPS_JS, PLATFORM_PARSER_JS, PLATFORM_DOM_JS,
|
||||||
|
PLATFORM_ENGINE_PURE_JS, PLATFORM_ORCHESTRATION_JS, PLATFORM_BOOT_JS,
|
||||||
|
CONTINUATIONS_JS, ASYNC_IO_JS,
|
||||||
|
fixups_js, public_api_js, EPILOGUE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_js_sx_env = None # cached
|
||||||
|
|
||||||
|
|
||||||
def load_js_sx() -> dict:
|
def load_js_sx() -> dict:
|
||||||
"""Load js.sx into an evaluator environment and return it."""
|
"""Load js.sx into an evaluator environment and return it."""
|
||||||
|
global _js_sx_env
|
||||||
|
if _js_sx_env is not None:
|
||||||
|
return _js_sx_env
|
||||||
|
|
||||||
js_sx_path = os.path.join(_HERE, "js.sx")
|
js_sx_path = os.path.join(_HERE, "js.sx")
|
||||||
with open(js_sx_path) as f:
|
with open(js_sx_path) as f:
|
||||||
source = f.read()
|
source = f.read()
|
||||||
@@ -39,63 +55,187 @@ def load_js_sx() -> dict:
|
|||||||
for expr in exprs:
|
for expr in exprs:
|
||||||
evaluate(expr, env)
|
evaluate(expr, env)
|
||||||
|
|
||||||
|
_js_sx_env = env
|
||||||
return env
|
return env
|
||||||
|
|
||||||
|
|
||||||
def extract_defines(source: str) -> list[tuple[str, list]]:
|
def compile_ref_to_js(
|
||||||
"""Parse .sx source, return list of (name, define-expr) for top-level defines."""
|
adapters: list[str] | None = None,
|
||||||
exprs = parse_all(source)
|
modules: list[str] | None = None,
|
||||||
defines = []
|
extensions: list[str] | None = None,
|
||||||
for expr in exprs:
|
spec_modules: list[str] | None = None,
|
||||||
if isinstance(expr, list) and expr and isinstance(expr[0], Symbol):
|
) -> str:
|
||||||
if expr[0].name == "define":
|
"""Compile SX spec files to JavaScript using js.sx.
|
||||||
name = expr[1].name if isinstance(expr[1], Symbol) else str(expr[1])
|
|
||||||
defines.append((name, expr))
|
|
||||||
return defines
|
|
||||||
|
|
||||||
|
Args:
|
||||||
def main():
|
adapters: List of adapter names to include. None = all.
|
||||||
|
modules: List of primitive module names. None = all.
|
||||||
|
extensions: List of extensions (continuations). None = none.
|
||||||
|
spec_modules: List of spec modules (deps, router, signals). None = auto.
|
||||||
|
"""
|
||||||
|
from datetime import datetime, timezone
|
||||||
from shared.sx.evaluator import evaluate
|
from shared.sx.evaluator import evaluate
|
||||||
|
|
||||||
# Load js.sx into evaluator
|
ref_dir = _HERE
|
||||||
env = load_js_sx()
|
env = load_js_sx()
|
||||||
|
|
||||||
# Same file list and order as bootstrap_js.py compile_ref_to_js() with all adapters
|
# Resolve adapter set
|
||||||
|
if adapters is None:
|
||||||
|
adapter_set = set(ADAPTER_FILES.keys())
|
||||||
|
else:
|
||||||
|
adapter_set = set()
|
||||||
|
for a in adapters:
|
||||||
|
if a not in ADAPTER_FILES:
|
||||||
|
raise ValueError(f"Unknown adapter: {a!r}. Valid: {', '.join(ADAPTER_FILES)}")
|
||||||
|
adapter_set.add(a)
|
||||||
|
for dep in ADAPTER_DEPS.get(a, []):
|
||||||
|
adapter_set.add(dep)
|
||||||
|
|
||||||
|
# Resolve spec modules
|
||||||
|
spec_mod_set = set()
|
||||||
|
if spec_modules:
|
||||||
|
for sm in spec_modules:
|
||||||
|
if sm not in SPEC_MODULES:
|
||||||
|
raise ValueError(f"Unknown spec module: {sm!r}. Valid: {', '.join(SPEC_MODULES)}")
|
||||||
|
spec_mod_set.add(sm)
|
||||||
|
if "dom" in adapter_set and "signals" in SPEC_MODULES:
|
||||||
|
spec_mod_set.add("signals")
|
||||||
|
if "boot" in adapter_set:
|
||||||
|
spec_mod_set.add("router")
|
||||||
|
spec_mod_set.add("deps")
|
||||||
|
has_deps = "deps" in spec_mod_set
|
||||||
|
has_router = "router" in spec_mod_set
|
||||||
|
|
||||||
|
# Resolve extensions
|
||||||
|
ext_set = set()
|
||||||
|
if extensions:
|
||||||
|
for e in extensions:
|
||||||
|
if e not in EXTENSION_NAMES:
|
||||||
|
raise ValueError(f"Unknown extension: {e!r}. Valid: {', '.join(EXTENSION_NAMES)}")
|
||||||
|
ext_set.add(e)
|
||||||
|
has_continuations = "continuations" in ext_set
|
||||||
|
|
||||||
|
# Build file list: core + adapters + spec modules
|
||||||
sx_files = [
|
sx_files = [
|
||||||
("eval.sx", "eval"),
|
("eval.sx", "eval"),
|
||||||
("render.sx", "render (core)"),
|
("render.sx", "render (core)"),
|
||||||
("parser.sx", "parser"),
|
|
||||||
("adapter-html.sx", "adapter-html"),
|
|
||||||
("adapter-sx.sx", "adapter-sx"),
|
|
||||||
("adapter-dom.sx", "adapter-dom"),
|
|
||||||
("engine.sx", "engine"),
|
|
||||||
("orchestration.sx", "orchestration"),
|
|
||||||
("boot.sx", "boot"),
|
|
||||||
("deps.sx", "deps (component dependency analysis)"),
|
|
||||||
("router.sx", "router (client-side route matching)"),
|
|
||||||
("signals.sx", "signals (reactive signal runtime)"),
|
|
||||||
]
|
]
|
||||||
|
for name in ("parser", "html", "sx", "dom", "engine", "orchestration", "boot"):
|
||||||
|
if name in adapter_set:
|
||||||
|
sx_files.append(ADAPTER_FILES[name])
|
||||||
|
for name in sorted(spec_mod_set):
|
||||||
|
sx_files.append(SPEC_MODULES[name])
|
||||||
|
|
||||||
|
has_html = "html" in adapter_set
|
||||||
|
has_sx = "sx" in adapter_set
|
||||||
|
has_dom = "dom" in adapter_set
|
||||||
|
has_engine = "engine" in adapter_set
|
||||||
|
has_orch = "orchestration" in adapter_set
|
||||||
|
has_boot = "boot" in adapter_set
|
||||||
|
has_parser = "parser" in adapter_set
|
||||||
|
has_signals = "signals" in spec_mod_set
|
||||||
|
adapter_label = "+".join(sorted(adapter_set)) if adapter_set else "core-only"
|
||||||
|
|
||||||
|
# Platform JS blocks keyed by adapter name
|
||||||
|
adapter_platform = {
|
||||||
|
"parser": PLATFORM_PARSER_JS,
|
||||||
|
"dom": PLATFORM_DOM_JS,
|
||||||
|
"engine": PLATFORM_ENGINE_PURE_JS,
|
||||||
|
"orchestration": PLATFORM_ORCHESTRATION_JS,
|
||||||
|
"boot": PLATFORM_BOOT_JS,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determine primitive modules
|
||||||
|
prim_modules = None
|
||||||
|
if modules is not None:
|
||||||
|
prim_modules = [m for m in _ALL_JS_MODULES if m.startswith("core.")]
|
||||||
|
for m in modules:
|
||||||
|
if m not in prim_modules:
|
||||||
|
if m not in PRIMITIVES_JS_MODULES:
|
||||||
|
raise ValueError(f"Unknown module: {m!r}. Valid: {', '.join(PRIMITIVES_JS_MODULES)}")
|
||||||
|
prim_modules.append(m)
|
||||||
|
|
||||||
|
# Build output
|
||||||
|
parts = []
|
||||||
|
parts.append(PREAMBLE)
|
||||||
|
parts.append(PLATFORM_JS_PRE)
|
||||||
|
parts.append('\n // =========================================================================')
|
||||||
|
parts.append(' // Primitives')
|
||||||
|
parts.append(' // =========================================================================\n')
|
||||||
|
parts.append(' var PRIMITIVES = {};')
|
||||||
|
parts.append(_assemble_primitives_js(prim_modules))
|
||||||
|
parts.append(PLATFORM_JS_POST)
|
||||||
|
|
||||||
|
if has_deps:
|
||||||
|
parts.append(PLATFORM_DEPS_JS)
|
||||||
|
|
||||||
|
if has_parser:
|
||||||
|
parts.append(adapter_platform["parser"])
|
||||||
|
|
||||||
# Translate each spec file using js.sx
|
# Translate each spec file using js.sx
|
||||||
for filename, label in sx_files:
|
for filename, label in sx_files:
|
||||||
filepath = os.path.join(_HERE, filename)
|
filepath = os.path.join(ref_dir, filename)
|
||||||
if not os.path.exists(filepath):
|
if not os.path.exists(filepath):
|
||||||
continue
|
continue
|
||||||
with open(filepath) as f:
|
with open(filepath) as f:
|
||||||
src = f.read()
|
src = f.read()
|
||||||
defines = extract_defines(src)
|
defines = extract_defines(src)
|
||||||
|
|
||||||
# Convert defines to SX-compatible format
|
|
||||||
sx_defines = [[name, expr] for name, expr in defines]
|
sx_defines = [[name, expr] for name, expr in defines]
|
||||||
|
|
||||||
print(f"\n // === Transpiled from {label} ===\n")
|
parts.append(f"\n // === Transpiled from {label} ===\n")
|
||||||
env["_defines"] = sx_defines
|
env["_defines"] = sx_defines
|
||||||
result = evaluate(
|
result = evaluate(
|
||||||
[Symbol("js-translate-file"), Symbol("_defines")],
|
[Symbol("js-translate-file"), Symbol("_defines")],
|
||||||
env,
|
env,
|
||||||
)
|
)
|
||||||
print(result)
|
parts.append(result)
|
||||||
|
|
||||||
|
# Platform JS for selected adapters
|
||||||
|
if not has_dom:
|
||||||
|
parts.append("\n var _hasDom = false;\n")
|
||||||
|
for name in ("dom", "engine", "orchestration", "boot"):
|
||||||
|
if name in adapter_set and name in adapter_platform:
|
||||||
|
parts.append(adapter_platform[name])
|
||||||
|
|
||||||
|
parts.append(fixups_js(has_html, has_sx, has_dom, has_signals, has_deps))
|
||||||
|
if has_continuations:
|
||||||
|
parts.append(CONTINUATIONS_JS)
|
||||||
|
if has_dom:
|
||||||
|
parts.append(ASYNC_IO_JS)
|
||||||
|
parts.append(public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has_parser, adapter_label, has_deps, has_router, has_signals))
|
||||||
|
parts.append(EPILOGUE)
|
||||||
|
|
||||||
|
build_ts = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
return "\n".join(parts).replace("BUILD_TIMESTAMP", build_ts)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
import argparse
|
||||||
|
p = argparse.ArgumentParser(description="Bootstrap-compile SX reference spec to JavaScript via js.sx")
|
||||||
|
p.add_argument("--adapters", "-a",
|
||||||
|
help="Comma-separated adapter list (html,sx,dom,engine). Default: all")
|
||||||
|
p.add_argument("--modules", "-m",
|
||||||
|
help="Comma-separated primitive modules (core.* always included). Default: all")
|
||||||
|
p.add_argument("--extensions",
|
||||||
|
help="Comma-separated extensions (continuations). Default: none.")
|
||||||
|
p.add_argument("--spec-modules",
|
||||||
|
help="Comma-separated spec modules (deps). Default: none.")
|
||||||
|
default_output = os.path.join(_HERE, "..", "..", "static", "scripts", "sx-browser.js")
|
||||||
|
p.add_argument("--output", "-o", default=default_output,
|
||||||
|
help="Output file (default: shared/static/scripts/sx-browser.js)")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
adapters = args.adapters.split(",") if args.adapters else None
|
||||||
|
modules = args.modules.split(",") if args.modules else None
|
||||||
|
extensions = args.extensions.split(",") if args.extensions else None
|
||||||
|
spec_modules = args.spec_modules.split(",") if args.spec_modules else None
|
||||||
|
js = compile_ref_to_js(adapters, modules, extensions, spec_modules)
|
||||||
|
|
||||||
|
with open(args.output, "w") as f:
|
||||||
|
f.write(js)
|
||||||
|
included = ", ".join(adapters) if adapters else "all"
|
||||||
|
mods = ", ".join(modules) if modules else "all"
|
||||||
|
ext_label = ", ".join(extensions) if extensions else "none"
|
||||||
|
print(f"Wrote {args.output} ({len(js)} bytes, adapters: {included}, modules: {mods}, extensions: {ext_label})",
|
||||||
|
file=sys.stderr)
|
||||||
|
|||||||
@@ -745,7 +745,7 @@ PRIMITIVES["last"] = lambda c: c[-1] if c and _b_len(c) > 0 else NIL
|
|||||||
PRIMITIVES["rest"] = lambda c: c[1:] if c else []
|
PRIMITIVES["rest"] = lambda c: c[1:] if c else []
|
||||||
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
|
PRIMITIVES["nth"] = lambda c, n: c[n] if c and 0 <= n < _b_len(c) else NIL
|
||||||
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
|
PRIMITIVES["cons"] = lambda x, c: [x] + (c or [])
|
||||||
PRIMITIVES["append"] = lambda c, x: (c or []) + [x]
|
PRIMITIVES["append"] = lambda c, x: (c or []) + (x if isinstance(x, list) else [x])
|
||||||
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
PRIMITIVES["chunk-every"] = lambda c, n: [c[i:i+n] for i in _b_range(0, _b_len(c), n)]
|
||||||
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
PRIMITIVES["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
|
||||||
|
|
||||||
|
|||||||
@@ -545,9 +545,12 @@
|
|||||||
|
|
||||||
|
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
;; defpage
|
;; Server-only tests — skip in browser (defpage, streaming functions)
|
||||||
|
;; These require forms.sx which is only loaded server-side.
|
||||||
;; --------------------------------------------------------------------------
|
;; --------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(when (get (try-call (fn () stream-chunk-id)) "ok")
|
||||||
|
|
||||||
(defsuite "defpage"
|
(defsuite "defpage"
|
||||||
(deftest "basic defpage returns page-def"
|
(deftest "basic defpage returns page-def"
|
||||||
(let ((p (defpage test-basic :path "/test" :auth :public :content (div "hello"))))
|
(let ((p (defpage test-basic :path "/test" :auth :public :content (div "hello"))))
|
||||||
@@ -716,3 +719,5 @@
|
|||||||
:content (~chunk :val val))))
|
:content (~chunk :val val))))
|
||||||
(assert-equal true (get p "stream"))
|
(assert-equal true (get p "stream"))
|
||||||
(assert-true (not (nil? (get p "shell")))))))
|
(assert-true (not (nil? (get p "shell")))))))
|
||||||
|
|
||||||
|
) ;; end (when has-server-forms?)
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ Relation registry — declarative entity relationship definitions.
|
|||||||
Relations are defined as s-expressions using ``defrelation`` and stored
|
Relations are defined as s-expressions using ``defrelation`` and stored
|
||||||
in a global registry. All services load the same definitions at startup
|
in a global registry. All services load the same definitions at startup
|
||||||
via ``load_relation_registry()``.
|
via ``load_relation_registry()``.
|
||||||
|
|
||||||
|
No evaluator dependency — defrelation forms are parsed directly from the
|
||||||
|
AST since they're just structured data (keyword args → RelationDef).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from shared.sx.types import RelationDef
|
from shared.sx.types import Keyword, RelationDef, Symbol
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -48,6 +51,102 @@ def clear_registry() -> None:
|
|||||||
_RELATION_REGISTRY.clear()
|
_RELATION_REGISTRY.clear()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# defrelation parsing — direct AST walk, no evaluator needed
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
_VALID_CARDINALITIES = {"one-to-one", "one-to-many", "many-to-many"}
|
||||||
|
_VALID_NAV = {"submenu", "tab", "badge", "inline", "hidden"}
|
||||||
|
|
||||||
|
|
||||||
|
class RelationError(Exception):
|
||||||
|
"""Error parsing a defrelation form."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_defrelation(expr: list) -> RelationDef:
|
||||||
|
"""Parse a (defrelation :name :key val ...) AST into a RelationDef."""
|
||||||
|
if len(expr) < 2:
|
||||||
|
raise RelationError("defrelation requires a name")
|
||||||
|
|
||||||
|
name_kw = expr[1]
|
||||||
|
if not isinstance(name_kw, Keyword):
|
||||||
|
raise RelationError(
|
||||||
|
f"defrelation name must be a keyword, got {type(name_kw).__name__}"
|
||||||
|
)
|
||||||
|
rel_name = name_kw.name
|
||||||
|
|
||||||
|
# Parse keyword args
|
||||||
|
kwargs: dict[str, str | None] = {}
|
||||||
|
i = 2
|
||||||
|
while i < len(expr):
|
||||||
|
key = expr[i]
|
||||||
|
if isinstance(key, Keyword):
|
||||||
|
if i + 1 < len(expr):
|
||||||
|
val = expr[i + 1]
|
||||||
|
kwargs[key.name] = val.name if isinstance(val, Keyword) else val
|
||||||
|
i += 2
|
||||||
|
else:
|
||||||
|
kwargs[key.name] = None
|
||||||
|
i += 1
|
||||||
|
else:
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
for field in ("from", "to", "cardinality"):
|
||||||
|
if field not in kwargs:
|
||||||
|
raise RelationError(
|
||||||
|
f"defrelation {rel_name} missing required :{field}"
|
||||||
|
)
|
||||||
|
|
||||||
|
card = kwargs["cardinality"]
|
||||||
|
if card not in _VALID_CARDINALITIES:
|
||||||
|
raise RelationError(
|
||||||
|
f"defrelation {rel_name}: invalid cardinality {card!r}, "
|
||||||
|
f"expected one of {_VALID_CARDINALITIES}"
|
||||||
|
)
|
||||||
|
|
||||||
|
nav = kwargs.get("nav", "hidden")
|
||||||
|
if nav not in _VALID_NAV:
|
||||||
|
raise RelationError(
|
||||||
|
f"defrelation {rel_name}: invalid nav {nav!r}, "
|
||||||
|
f"expected one of {_VALID_NAV}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return RelationDef(
|
||||||
|
name=rel_name,
|
||||||
|
from_type=kwargs["from"],
|
||||||
|
to_type=kwargs["to"],
|
||||||
|
cardinality=card,
|
||||||
|
inverse=kwargs.get("inverse"),
|
||||||
|
nav=nav,
|
||||||
|
nav_icon=kwargs.get("nav-icon"),
|
||||||
|
nav_label=kwargs.get("nav-label"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate_defrelation(expr: list) -> RelationDef:
|
||||||
|
"""Parse a defrelation form, register it, and return the RelationDef.
|
||||||
|
|
||||||
|
Also handles (begin (defrelation ...) ...) wrappers.
|
||||||
|
"""
|
||||||
|
if not isinstance(expr, list) or not expr:
|
||||||
|
raise RelationError(f"Expected list expression, got {type(expr).__name__}")
|
||||||
|
|
||||||
|
head = expr[0]
|
||||||
|
if isinstance(head, Symbol) and head.name == "begin":
|
||||||
|
result = None
|
||||||
|
for child in expr[1:]:
|
||||||
|
result = evaluate_defrelation(child)
|
||||||
|
return result
|
||||||
|
|
||||||
|
if not (isinstance(head, Symbol) and head.name == "defrelation"):
|
||||||
|
raise RelationError(f"Expected defrelation, got {head}")
|
||||||
|
|
||||||
|
defn = _parse_defrelation(expr)
|
||||||
|
register_relation(defn)
|
||||||
|
return defn
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Built-in relation definitions (s-expression source)
|
# Built-in relation definitions (s-expression source)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -94,8 +193,7 @@ _BUILTIN_RELATIONS = '''
|
|||||||
|
|
||||||
def load_relation_registry() -> None:
|
def load_relation_registry() -> None:
|
||||||
"""Parse built-in defrelation s-expressions and populate the registry."""
|
"""Parse built-in defrelation s-expressions and populate the registry."""
|
||||||
from shared.sx.evaluator import evaluate
|
|
||||||
from shared.sx.parser import parse
|
from shared.sx.parser import parse
|
||||||
|
|
||||||
tree = parse(_BUILTIN_RELATIONS)
|
tree = parse(_BUILTIN_RELATIONS)
|
||||||
evaluate(tree)
|
evaluate_defrelation(tree)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Test bootstrapper transpilation: JSEmitter and PyEmitter."""
|
"""Test bootstrapper transpilation: js.sx and py.sx."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -6,49 +6,38 @@ import re
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from shared.sx.parser import parse, parse_all
|
from shared.sx.parser import parse, parse_all
|
||||||
from shared.sx.ref.bootstrap_js import (
|
from shared.sx.ref.run_js_sx import compile_ref_to_js, load_js_sx
|
||||||
JSEmitter,
|
from shared.sx.ref.platform_js import (
|
||||||
ADAPTER_FILES,
|
ADAPTER_FILES,
|
||||||
SPEC_MODULES,
|
SPEC_MODULES,
|
||||||
extract_defines,
|
extract_defines,
|
||||||
compile_ref_to_js,
|
|
||||||
)
|
)
|
||||||
from shared.sx.ref.bootstrap_py import PyEmitter
|
from shared.sx.ref.bootstrap_py import PyEmitter
|
||||||
from shared.sx.types import Symbol, Keyword
|
from shared.sx.types import Symbol, Keyword
|
||||||
|
|
||||||
|
|
||||||
class TestJSEmitterNativeDict:
|
class TestJsSxTranslation:
|
||||||
"""JS bootstrapper must handle native Python dicts from {:key val} syntax."""
|
"""js.sx self-hosting bootstrapper handles all SX constructs."""
|
||||||
|
|
||||||
def test_simple_string_values(self):
|
def _translate(self, sx_source: str) -> str:
|
||||||
expr = parse('{"name" "hello"}')
|
"""Translate a single SX expression to JS using js.sx."""
|
||||||
assert isinstance(expr, dict)
|
from shared.sx.evaluator import evaluate
|
||||||
js = JSEmitter().emit(expr)
|
env = load_js_sx()
|
||||||
assert js == '{"name": "hello"}'
|
expr = parse(sx_source)
|
||||||
|
env["_def_expr"] = expr
|
||||||
|
return evaluate(
|
||||||
|
[Symbol("js-expr"), Symbol("_def_expr")], env
|
||||||
|
)
|
||||||
|
|
||||||
def test_function_call_value(self):
|
def test_simple_dict(self):
|
||||||
"""Dict value containing a function call must emit the call, not raw AST."""
|
result = self._translate('{"name" "hello"}')
|
||||||
expr = parse('{"parsed" (parse-route-pattern (get page "path"))}')
|
assert '"name"' in result
|
||||||
js = JSEmitter().emit(expr)
|
assert '"hello"' in result
|
||||||
assert "parseRoutePattern" in js
|
|
||||||
assert "Symbol" not in js
|
|
||||||
assert js == '{"parsed": parseRoutePattern(get(page, "path"))}'
|
|
||||||
|
|
||||||
def test_multiple_keys(self):
|
def test_function_call_in_dict(self):
|
||||||
expr = parse('{"a" 1 "b" (+ x 2)}')
|
result = self._translate('{"parsed" (parse-route-pattern (get page "path"))}')
|
||||||
js = JSEmitter().emit(expr)
|
assert "parseRoutePattern" in result
|
||||||
assert '"a": 1' in js
|
assert "Symbol" not in result
|
||||||
assert '"b": (x + 2)' in js
|
|
||||||
|
|
||||||
def test_nested_dict(self):
|
|
||||||
expr = parse('{"outer" {"inner" 42}}')
|
|
||||||
js = JSEmitter().emit(expr)
|
|
||||||
assert '{"outer": {"inner": 42}}' == js
|
|
||||||
|
|
||||||
def test_nil_value(self):
|
|
||||||
expr = parse('{"key" nil}')
|
|
||||||
js = JSEmitter().emit(expr)
|
|
||||||
assert '"key": NIL' in js
|
|
||||||
|
|
||||||
|
|
||||||
class TestPyEmitterNativeDict:
|
class TestPyEmitterNativeDict:
|
||||||
@@ -92,11 +81,7 @@ class TestPlatformMapping:
|
|||||||
"""Verify compiled JS output contains all spec-defined functions."""
|
"""Verify compiled JS output contains all spec-defined functions."""
|
||||||
|
|
||||||
def test_compiled_defines_present_in_js(self):
|
def test_compiled_defines_present_in_js(self):
|
||||||
"""Every top-level define from spec files must appear in compiled JS output.
|
"""Every top-level define from spec files must appear in compiled JS output."""
|
||||||
|
|
||||||
Catches: spec modules not included, _mangle producing wrong names for
|
|
||||||
defines, transpilation silently dropping definitions.
|
|
||||||
"""
|
|
||||||
js_output = compile_ref_to_js(
|
js_output = compile_ref_to_js(
|
||||||
spec_modules=list(SPEC_MODULES.keys()),
|
spec_modules=list(SPEC_MODULES.keys()),
|
||||||
)
|
)
|
||||||
@@ -117,10 +102,17 @@ class TestPlatformMapping:
|
|||||||
for name, _expr in extract_defines(open(filepath).read()):
|
for name, _expr in extract_defines(open(filepath).read()):
|
||||||
all_defs.add(name)
|
all_defs.add(name)
|
||||||
|
|
||||||
emitter = JSEmitter()
|
# Use js.sx RENAMES to map SX names → JS names
|
||||||
|
env = load_js_sx()
|
||||||
|
renames = env.get("js-renames", {})
|
||||||
|
|
||||||
missing = []
|
missing = []
|
||||||
for sx_name in sorted(all_defs):
|
for sx_name in sorted(all_defs):
|
||||||
js_name = emitter._mangle(sx_name)
|
# Check if there's an explicit rename
|
||||||
|
js_name = renames.get(sx_name)
|
||||||
|
if js_name is None:
|
||||||
|
# Auto-mangle: hyphens → camelCase, ! → _b, ? → _p
|
||||||
|
js_name = _auto_mangle(sx_name)
|
||||||
if js_name not in defined_in_js:
|
if js_name not in defined_in_js:
|
||||||
missing.append(f"{sx_name} → {js_name}")
|
missing.append(f"{sx_name} → {js_name}")
|
||||||
|
|
||||||
@@ -131,41 +123,29 @@ class TestPlatformMapping:
|
|||||||
+ "\n ".join(missing)
|
+ "\n ".join(missing)
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_renames_values_are_unique(self):
|
|
||||||
"""RENAMES should not map different SX names to the same JS name.
|
|
||||||
|
|
||||||
Duplicate JS names would cause one definition to silently shadow another.
|
def _auto_mangle(name: str) -> str:
|
||||||
"""
|
"""Approximate js.sx's auto-mangle for SX name → JS identifier."""
|
||||||
renames = JSEmitter.RENAMES
|
# Remove ~, replace ?, !, -, *, >
|
||||||
seen: dict[str, str] = {}
|
n = name
|
||||||
dupes = []
|
if n.startswith("~"):
|
||||||
for sx_name, js_name in sorted(renames.items()):
|
n = n[1:]
|
||||||
if js_name in seen:
|
n = n.replace("?", "_p").replace("!", "_b")
|
||||||
# Allow intentional aliases (e.g. has-key? and dict-has?
|
n = n.replace("->", "_to_").replace(">=", "_gte").replace("<=", "_lte")
|
||||||
# both → dictHas)
|
n = n.replace(">", "_gt").replace("<", "_lt")
|
||||||
dupes.append(
|
n = n.replace("*", "_star_").replace("/", "_slash_")
|
||||||
f" {sx_name} → {js_name} (same as {seen[js_name]})"
|
# camelCase from hyphens
|
||||||
)
|
parts = n.split("-")
|
||||||
else:
|
if len(parts) > 1:
|
||||||
seen[js_name] = sx_name
|
n = parts[0] + "".join(p.capitalize() for p in parts[1:])
|
||||||
|
return n
|
||||||
# Intentional aliases — these are expected duplicates
|
|
||||||
# (e.g. has-key? and dict-has? both map to dictHas)
|
|
||||||
# Don't fail for these, just document them
|
|
||||||
# The test serves as a warning for accidental duplicates
|
|
||||||
|
|
||||||
|
|
||||||
class TestPrimitivesRegistration:
|
class TestPrimitivesRegistration:
|
||||||
"""Functions callable from runtime-evaluated SX must be in PRIMITIVES[...]."""
|
"""Functions callable from runtime-evaluated SX must be in PRIMITIVES[...]."""
|
||||||
|
|
||||||
def test_declared_primitives_registered(self):
|
def test_declared_primitives_registered(self):
|
||||||
"""Every primitive declared in primitives.sx must have a PRIMITIVES[...] entry.
|
"""Every primitive declared in primitives.sx must have a PRIMITIVES[...] entry."""
|
||||||
|
|
||||||
Primitives are called from runtime-evaluated SX (island bodies, user
|
|
||||||
components) via getPrimitive(). If a primitive is declared in
|
|
||||||
primitives.sx but not in PRIMITIVES[...], island code gets
|
|
||||||
"Undefined symbol" errors.
|
|
||||||
"""
|
|
||||||
from shared.sx.ref.boundary_parser import parse_primitives_sx
|
from shared.sx.ref.boundary_parser import parse_primitives_sx
|
||||||
|
|
||||||
declared = parse_primitives_sx()
|
declared = parse_primitives_sx()
|
||||||
@@ -173,8 +153,7 @@ class TestPrimitivesRegistration:
|
|||||||
js_output = compile_ref_to_js()
|
js_output = compile_ref_to_js()
|
||||||
registered = set(re.findall(r'PRIMITIVES\["([^"]+)"\]', js_output))
|
registered = set(re.findall(r'PRIMITIVES\["([^"]+)"\]', js_output))
|
||||||
|
|
||||||
# Aliases — declared in primitives.sx under alternate names but
|
# Aliases
|
||||||
# served via canonical PRIMITIVES entries
|
|
||||||
aliases = {
|
aliases = {
|
||||||
"downcase": "lower",
|
"downcase": "lower",
|
||||||
"upcase": "upper",
|
"upcase": "upper",
|
||||||
@@ -186,7 +165,7 @@ class TestPrimitivesRegistration:
|
|||||||
if alias in declared and canonical in registered:
|
if alias in declared and canonical in registered:
|
||||||
declared = declared - {alias}
|
declared = declared - {alias}
|
||||||
|
|
||||||
# Extension-only primitives (require continuations extension)
|
# Extension-only primitives
|
||||||
extension_only = {"continuation?"}
|
extension_only = {"continuation?"}
|
||||||
declared = declared - extension_only
|
declared = declared - extension_only
|
||||||
|
|
||||||
@@ -199,12 +178,7 @@ class TestPrimitivesRegistration:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_signal_runtime_primitives_registered(self):
|
def test_signal_runtime_primitives_registered(self):
|
||||||
"""Signal/reactive functions used by island bodies must be in PRIMITIVES.
|
"""Signal/reactive functions used by island bodies must be in PRIMITIVES."""
|
||||||
|
|
||||||
These are the reactive primitives that island SX code calls via
|
|
||||||
getPrimitive(). If any is missing, islands with reactive state fail
|
|
||||||
at runtime.
|
|
||||||
"""
|
|
||||||
required = {
|
required = {
|
||||||
"signal", "signal?", "deref", "reset!", "swap!",
|
"signal", "signal?", "deref", "reset!", "swap!",
|
||||||
"computed", "effect", "batch", "resource",
|
"computed", "effect", "batch", "resource",
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from shared.sx.evaluator import evaluate, EvalError
|
|
||||||
from shared.sx.parser import parse
|
from shared.sx.parser import parse
|
||||||
from shared.sx.relations import (
|
from shared.sx.relations import (
|
||||||
|
RelationError,
|
||||||
_RELATION_REGISTRY,
|
_RELATION_REGISTRY,
|
||||||
clear_registry,
|
clear_registry,
|
||||||
|
evaluate_defrelation,
|
||||||
get_relation,
|
get_relation,
|
||||||
load_relation_registry,
|
load_relation_registry,
|
||||||
relations_from,
|
relations_from,
|
||||||
@@ -38,7 +39,7 @@ class TestDefrelation:
|
|||||||
:nav-icon "fa fa-shopping-bag"
|
:nav-icon "fa fa-shopping-bag"
|
||||||
:nav-label "markets")
|
:nav-label "markets")
|
||||||
''')
|
''')
|
||||||
result = evaluate(tree)
|
result = evaluate_defrelation(tree)
|
||||||
assert isinstance(result, RelationDef)
|
assert isinstance(result, RelationDef)
|
||||||
assert result.name == "page->market"
|
assert result.name == "page->market"
|
||||||
assert result.from_type == "page"
|
assert result.from_type == "page"
|
||||||
@@ -54,7 +55,7 @@ class TestDefrelation:
|
|||||||
(defrelation :a->b
|
(defrelation :a->b
|
||||||
:from "a" :to "b" :cardinality :one-to-one :nav :hidden)
|
:from "a" :to "b" :cardinality :one-to-one :nav :hidden)
|
||||||
''')
|
''')
|
||||||
evaluate(tree)
|
evaluate_defrelation(tree)
|
||||||
assert get_relation("a->b") is not None
|
assert get_relation("a->b") is not None
|
||||||
assert get_relation("a->b").cardinality == "one-to-one"
|
assert get_relation("a->b").cardinality == "one-to-one"
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ class TestDefrelation:
|
|||||||
:from "page" :to "menu_node"
|
:from "page" :to "menu_node"
|
||||||
:cardinality :one-to-one :nav :hidden)
|
:cardinality :one-to-one :nav :hidden)
|
||||||
''')
|
''')
|
||||||
result = evaluate(tree)
|
result = evaluate_defrelation(tree)
|
||||||
assert result.cardinality == "one-to-one"
|
assert result.cardinality == "one-to-one"
|
||||||
assert result.inverse is None
|
assert result.inverse is None
|
||||||
assert result.nav == "hidden"
|
assert result.nav == "hidden"
|
||||||
@@ -79,7 +80,7 @@ class TestDefrelation:
|
|||||||
:nav-icon "fa fa-file-alt"
|
:nav-icon "fa fa-file-alt"
|
||||||
:nav-label "events")
|
:nav-label "events")
|
||||||
''')
|
''')
|
||||||
result = evaluate(tree)
|
result = evaluate_defrelation(tree)
|
||||||
assert result.cardinality == "many-to-many"
|
assert result.cardinality == "many-to-many"
|
||||||
|
|
||||||
def test_default_nav_is_hidden(self):
|
def test_default_nav_is_hidden(self):
|
||||||
@@ -87,7 +88,7 @@ class TestDefrelation:
|
|||||||
(defrelation :x->y
|
(defrelation :x->y
|
||||||
:from "x" :to "y" :cardinality :one-to-many)
|
:from "x" :to "y" :cardinality :one-to-many)
|
||||||
''')
|
''')
|
||||||
result = evaluate(tree)
|
result = evaluate_defrelation(tree)
|
||||||
assert result.nav == "hidden"
|
assert result.nav == "hidden"
|
||||||
|
|
||||||
def test_invalid_cardinality_raises(self):
|
def test_invalid_cardinality_raises(self):
|
||||||
@@ -95,42 +96,42 @@ class TestDefrelation:
|
|||||||
(defrelation :bad
|
(defrelation :bad
|
||||||
:from "a" :to "b" :cardinality :wrong)
|
:from "a" :to "b" :cardinality :wrong)
|
||||||
''')
|
''')
|
||||||
with pytest.raises(EvalError, match="invalid cardinality"):
|
with pytest.raises(RelationError, match="invalid cardinality"):
|
||||||
evaluate(tree)
|
evaluate_defrelation(tree)
|
||||||
|
|
||||||
def test_invalid_nav_raises(self):
|
def test_invalid_nav_raises(self):
|
||||||
tree = parse('''
|
tree = parse('''
|
||||||
(defrelation :bad
|
(defrelation :bad
|
||||||
:from "a" :to "b" :cardinality :one-to-one :nav :bogus)
|
:from "a" :to "b" :cardinality :one-to-one :nav :bogus)
|
||||||
''')
|
''')
|
||||||
with pytest.raises(EvalError, match="invalid nav"):
|
with pytest.raises(RelationError, match="invalid nav"):
|
||||||
evaluate(tree)
|
evaluate_defrelation(tree)
|
||||||
|
|
||||||
def test_missing_from_raises(self):
|
def test_missing_from_raises(self):
|
||||||
tree = parse('''
|
tree = parse('''
|
||||||
(defrelation :bad :to "b" :cardinality :one-to-one)
|
(defrelation :bad :to "b" :cardinality :one-to-one)
|
||||||
''')
|
''')
|
||||||
with pytest.raises(EvalError, match="missing required :from"):
|
with pytest.raises(RelationError, match="missing required :from"):
|
||||||
evaluate(tree)
|
evaluate_defrelation(tree)
|
||||||
|
|
||||||
def test_missing_to_raises(self):
|
def test_missing_to_raises(self):
|
||||||
tree = parse('''
|
tree = parse('''
|
||||||
(defrelation :bad :from "a" :cardinality :one-to-one)
|
(defrelation :bad :from "a" :cardinality :one-to-one)
|
||||||
''')
|
''')
|
||||||
with pytest.raises(EvalError, match="missing required :to"):
|
with pytest.raises(RelationError, match="missing required :to"):
|
||||||
evaluate(tree)
|
evaluate_defrelation(tree)
|
||||||
|
|
||||||
def test_missing_cardinality_raises(self):
|
def test_missing_cardinality_raises(self):
|
||||||
tree = parse('''
|
tree = parse('''
|
||||||
(defrelation :bad :from "a" :to "b")
|
(defrelation :bad :from "a" :to "b")
|
||||||
''')
|
''')
|
||||||
with pytest.raises(EvalError, match="missing required :cardinality"):
|
with pytest.raises(RelationError, match="missing required :cardinality"):
|
||||||
evaluate(tree)
|
evaluate_defrelation(tree)
|
||||||
|
|
||||||
def test_name_must_be_keyword(self):
|
def test_name_must_be_keyword(self):
|
||||||
tree = parse('(defrelation "not-keyword" :from "a" :to "b" :cardinality :one-to-one)')
|
tree = parse('(defrelation "not-keyword" :from "a" :to "b" :cardinality :one-to-one)')
|
||||||
with pytest.raises(EvalError, match="must be a keyword"):
|
with pytest.raises(RelationError, match="must be a keyword"):
|
||||||
evaluate(tree)
|
evaluate_defrelation(tree)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -154,7 +155,7 @@ class TestRegistry:
|
|||||||
:from "page" :to "menu_node" :cardinality :one-to-one
|
:from "page" :to "menu_node" :cardinality :one-to-one
|
||||||
:nav :hidden))
|
:nav :hidden))
|
||||||
''')
|
''')
|
||||||
evaluate(tree)
|
evaluate_defrelation(tree)
|
||||||
|
|
||||||
def test_get_relation(self):
|
def test_get_relation(self):
|
||||||
self._load_sample()
|
self._load_sample()
|
||||||
|
|||||||
@@ -384,57 +384,47 @@ def _self_hosting_data(ref_dir: str) -> dict:
|
|||||||
|
|
||||||
|
|
||||||
def _js_self_hosting_data(ref_dir: str) -> dict:
|
def _js_self_hosting_data(ref_dir: str) -> dict:
|
||||||
"""Run js.sx live: load into evaluator, translate spec files, diff against G0."""
|
"""Run js.sx live: load into evaluator, translate all spec defines."""
|
||||||
import os
|
import os
|
||||||
from shared.sx.parser import parse_all
|
|
||||||
from shared.sx.types import Symbol
|
from shared.sx.types import Symbol
|
||||||
from shared.sx.evaluator import evaluate, make_env
|
from shared.sx.evaluator import evaluate
|
||||||
from shared.sx.ref.bootstrap_js import extract_defines, JSEmitter
|
from shared.sx.ref.run_js_sx import load_js_sx
|
||||||
|
from shared.sx.ref.platform_js import extract_defines
|
||||||
|
|
||||||
try:
|
try:
|
||||||
js_sx_path = os.path.join(ref_dir, "js.sx")
|
js_sx_path = os.path.join(ref_dir, "js.sx")
|
||||||
with open(js_sx_path, encoding="utf-8") as f:
|
with open(js_sx_path, encoding="utf-8") as f:
|
||||||
js_sx_source = f.read()
|
js_sx_source = f.read()
|
||||||
|
|
||||||
exprs = parse_all(js_sx_source)
|
env = load_js_sx()
|
||||||
env = make_env()
|
|
||||||
for expr in exprs:
|
|
||||||
evaluate(expr, env)
|
|
||||||
|
|
||||||
emitter = JSEmitter()
|
|
||||||
|
|
||||||
# All spec files
|
# All spec files
|
||||||
all_files = sorted(
|
all_files = sorted(
|
||||||
f for f in os.listdir(ref_dir) if f.endswith(".sx")
|
f for f in os.listdir(ref_dir) if f.endswith(".sx")
|
||||||
)
|
)
|
||||||
total = 0
|
total = 0
|
||||||
matched = 0
|
|
||||||
for filename in all_files:
|
for filename in all_files:
|
||||||
filepath = os.path.join(ref_dir, filename)
|
filepath = os.path.join(ref_dir, filename)
|
||||||
with open(filepath, encoding="utf-8") as f:
|
with open(filepath, encoding="utf-8") as f:
|
||||||
src = f.read()
|
src = f.read()
|
||||||
defines = extract_defines(src)
|
defines = extract_defines(src)
|
||||||
for name, expr in defines:
|
for name, expr in defines:
|
||||||
g0_stmt = emitter.emit_statement(expr)
|
|
||||||
env["_def_expr"] = expr
|
env["_def_expr"] = expr
|
||||||
g1_stmt = evaluate(
|
evaluate(
|
||||||
[Symbol("js-statement"), Symbol("_def_expr")], env
|
[Symbol("js-statement"), Symbol("_def_expr")], env
|
||||||
)
|
)
|
||||||
total += 1
|
total += 1
|
||||||
if g0_stmt.strip() == g1_stmt.strip():
|
|
||||||
matched += 1
|
|
||||||
|
|
||||||
status = "identical" if matched == total else "mismatch"
|
status = "ok"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
js_sx_source = f";; error loading js.sx: {e}"
|
js_sx_source = f";; error loading js.sx: {e}"
|
||||||
matched, total = 0, 0
|
total = 0
|
||||||
status = "error"
|
status = "error"
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"bootstrapper-not-found": None,
|
"bootstrapper-not-found": None,
|
||||||
"js-sx-source": js_sx_source,
|
"js-sx-source": js_sx_source,
|
||||||
"defines-matched": str(matched),
|
|
||||||
"defines-total": str(total),
|
"defines-total": str(total),
|
||||||
"js-sx-lines": str(len(js_sx_source.splitlines())),
|
"js-sx-lines": str(len(js_sx_source.splitlines())),
|
||||||
"verification-status": status,
|
"verification-status": status,
|
||||||
|
|||||||
Reference in New Issue
Block a user