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:
2026-03-11 04:53:34 +00:00
parent 46cd179703
commit 3906ab3558
12 changed files with 678 additions and 4526 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -656,8 +656,8 @@
(let ((spliced (trampoline (eval-expr (nth item 1) env))))
(if (= (type-of spliced) "list")
(concat result spliced)
(if (nil? spliced) result (append result spliced))))
(append result (qq-expand item env))))
(if (nil? spliced) result (concat result (list spliced)))))
(concat result (list (qq-expand item env)))))
(list)
template)))))))

View File

@@ -124,6 +124,8 @@
"eval-call" "evalCall"
"is-render-expr?" "isRenderExpr"
"render-expr" "renderExpr"
"render-active?" "renderActiveP"
"set-render-active!" "setRenderActiveB"
"call-lambda" "callLambda"
"call-component" "callComponent"
"parse-keyword-args" "parseKeywordArgs"

View File

@@ -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["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["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["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]
''',

View File

@@ -1,16 +1,14 @@
#!/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)
is loaded into the Python evaluator, which then uses it to translate the
spec .sx files into JavaScript.
The output (transpiled defines only) should be identical to what
bootstrap_js.py's JSEmitter produces.
This is the canonical JS bootstrapper. js.sx is loaded into the Python evaluator,
which uses it to translate the .sx spec files into JavaScript. Platform code
(types, primitives, DOM interface) comes from platform_js.py.
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
@@ -19,14 +17,32 @@ import sys
_HERE = os.path.dirname(os.path.abspath(__file__))
_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.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:
"""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")
with open(js_sx_path) as f:
source = f.read()
@@ -39,63 +55,187 @@ def load_js_sx() -> dict:
for expr in exprs:
evaluate(expr, env)
_js_sx_env = env
return env
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(
adapters: list[str] | None = None,
modules: list[str] | None = None,
extensions: list[str] | None = None,
spec_modules: list[str] | None = None,
) -> str:
"""Compile SX spec files to JavaScript using js.sx.
def main():
Args:
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
# Load js.sx into evaluator
ref_dir = _HERE
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 = [
("eval.sx", "eval"),
("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
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):
continue
with open(filepath) as f:
src = f.read()
defines = extract_defines(src)
# Convert defines to SX-compatible format
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
result = evaluate(
[Symbol("js-translate-file"), Symbol("_defines")],
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__":
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)

View File

@@ -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["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["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["zip-pairs"] = lambda c: [[c[i], c[i+1]] for i in _b_range(_b_len(c)-1)]

View File

@@ -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"
(deftest "basic defpage returns page-def"
(let ((p (defpage test-basic :path "/test" :auth :public :content (div "hello"))))
@@ -716,3 +719,5 @@
:content (~chunk :val val))))
(assert-equal true (get p "stream"))
(assert-true (not (nil? (get p "shell")))))))
) ;; end (when has-server-forms?)