Fix bootstrapper dict literal transpilation: emit values through emit()
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m51s

The SX parser produces native Python dicts for {:key val} syntax, but
both JSEmitter and PyEmitter had no dict case in emit() — falling through
to str(expr) which output raw AST. This broke client-side routing because
process-page-scripts used {"parsed" (parse-route-pattern ...)} and the
function call was emitted as a JS array of Symbols instead of an actual
function call.

Add _emit_native_dict() to both bootstrappers + 8 unit tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 16:24:44 +00:00
parent dd1c1c9a3c
commit 3749fe9625
4 changed files with 83 additions and 2 deletions

View File

@@ -2419,7 +2419,7 @@ callExpr.push(dictGet(kwargs, k)); } }
var text = domTextContent(s);
return (isSxTruthy((isSxTruthy(text) && !isEmpty(trim(text)))) ? (function() {
var pages = parse(text);
return forEach(function(page) { return append_b(_pageRoutes, merge(page, {'parsed': [Symbol('parse-route-pattern'), [Symbol('get'), Symbol('page'), 'path']]})); }, pages);
return forEach(function(page) { return append_b(_pageRoutes, merge(page, {"parsed": parseRoutePattern(get(page, "path"))})); }, pages);
})() : NIL);
})()) : NIL); }, scripts);
})(); };
@@ -3780,4 +3780,4 @@ callExpr.push(dictGet(kwargs, k)); } }
if (typeof module !== "undefined" && module.exports) module.exports = Sx;
else global.Sx = Sx;
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);
})(typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window : this);

View File

@@ -50,6 +50,8 @@ class JSEmitter:
return self._emit_symbol(expr.name)
if isinstance(expr, Keyword):
return self._js_string(expr.name)
if isinstance(expr, dict):
return self._emit_native_dict(expr)
if isinstance(expr, list):
return self._emit_list(expr)
return str(expr)
@@ -739,6 +741,13 @@ class JSEmitter:
parts = [self.emit(e) for e in exprs]
return "(" + ", ".join(parts) + ")"
def _emit_native_dict(self, expr: dict) -> str:
"""Emit a native Python dict (from parser's {:key val} syntax)."""
parts = []
for key, val in expr.items():
parts.append(f"{self._js_string(key)}: {self.emit(val)}")
return "{" + ", ".join(parts) + "}"
def _emit_dict_literal(self, expr) -> str:
pairs = expr[1:]
parts = []

View File

@@ -52,6 +52,8 @@ class PyEmitter:
return self._emit_symbol(expr.name)
if isinstance(expr, Keyword):
return self._py_string(expr.name)
if isinstance(expr, dict):
return self._emit_native_dict(expr)
if isinstance(expr, list):
return self._emit_list(expr)
return str(expr)
@@ -526,6 +528,13 @@ class PyEmitter:
parts = [self.emit(e) for e in exprs]
return "_sx_begin(" + ", ".join(parts) + ")"
def _emit_native_dict(self, expr: dict) -> str:
"""Emit a native Python dict (from parser's {:key val} syntax)."""
parts = []
for key, val in expr.items():
parts.append(f"{self._py_string(key)}: {self.emit(val)}")
return "{" + ", ".join(parts) + "}"
def _emit_dict_literal(self, expr) -> str:
pairs = expr[1:]
parts = []

View File

@@ -0,0 +1,63 @@
"""Test bootstrapper transpilation: JSEmitter and PyEmitter."""
from __future__ import annotations
import pytest
from shared.sx.parser import parse
from shared.sx.ref.bootstrap_js import JSEmitter
from shared.sx.ref.bootstrap_py import PyEmitter
class TestJSEmitterNativeDict:
"""JS bootstrapper must handle native Python dicts from {:key val} syntax."""
def test_simple_string_values(self):
expr = parse('{"name" "hello"}')
assert isinstance(expr, dict)
js = JSEmitter().emit(expr)
assert js == '{"name": "hello"}'
def test_function_call_value(self):
"""Dict value containing a function call must emit the call, not raw AST."""
expr = parse('{"parsed" (parse-route-pattern (get page "path"))}')
js = JSEmitter().emit(expr)
assert "parseRoutePattern" in js
assert "Symbol" not in js
assert js == '{"parsed": parseRoutePattern(get(page, "path"))}'
def test_multiple_keys(self):
expr = parse('{"a" 1 "b" (+ x 2)}')
js = JSEmitter().emit(expr)
assert '"a": 1' in js
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:
"""Python bootstrapper must handle native Python dicts from {:key val} syntax."""
def test_simple_string_values(self):
expr = parse('{"name" "hello"}')
py = PyEmitter().emit(expr)
assert py == "{'name': 'hello'}"
def test_function_call_value(self):
"""Dict value containing a function call must emit the call, not raw AST."""
expr = parse('{"parsed" (parse-route-pattern (get page "path"))}')
py = PyEmitter().emit(expr)
assert "parse_route_pattern" in py
assert "Symbol" not in py
def test_multiple_keys(self):
expr = parse('{"a" 1 "b" (+ x 2)}')
py = PyEmitter().emit(expr)
assert "'a': 1" in py
assert "'b': (x + 2)" in py