Fix bootstrapper dict literal transpilation: emit values through emit()
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 4m51s
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:
@@ -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);
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
63
shared/sx/tests/test_bootstrapper.py
Normal file
63
shared/sx/tests/test_bootstrapper.py
Normal 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
|
||||
Reference in New Issue
Block a user