Phase 7a: affinity annotations + fix parser escape sequences
Add :affinity :client/:server/:auto annotations to defcomp, with render-target function combining affinity + IO analysis. Includes spec (eval.sx, deps.sx), tests, Python evaluator, and demo page. Fix critical bug: Python SX parser _ESCAPE_MAP was missing \r and \0, causing bootstrapped JS parser to treat 'r' as whitespace — breaking all client-side SX parsing. Also add \0 to JS string emitter and fix serializer round-tripping for \r and \0. Reserved word escaping: bootstrappers now auto-append _ to identifiers colliding with JS/Python reserved words (e.g. default → default_, final → final_), so the spec never needs to avoid host language keywords. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,20 @@ from shared.sx.types import Symbol, Keyword, NIL as SX_NIL
|
||||
# SX → JavaScript transpiler
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# JS reserved words — SX parameter/variable names that collide get _ suffix
|
||||
_JS_RESERVED = frozenset({
|
||||
"abstract", "arguments", "await", "boolean", "break", "byte", "case",
|
||||
"catch", "char", "class", "const", "continue", "debugger", "default",
|
||||
"delete", "do", "double", "else", "enum", "eval", "export", "extends",
|
||||
"final", "finally", "float", "for", "function", "goto", "if",
|
||||
"implements", "import", "in", "instanceof", "int", "interface", "let",
|
||||
"long", "native", "new", "package", "private", "protected", "public",
|
||||
"return", "short", "static", "super", "switch", "synchronized", "this",
|
||||
"throw", "throws", "transient", "try", "typeof", "var", "void",
|
||||
"volatile", "while", "with", "yield",
|
||||
})
|
||||
|
||||
|
||||
class JSEmitter:
|
||||
"""Transpile an SX AST node to JavaScript source code."""
|
||||
|
||||
@@ -114,6 +128,7 @@ class JSEmitter:
|
||||
"component-closure": "componentClosure",
|
||||
"component-has-children?": "componentHasChildren",
|
||||
"component-name": "componentName",
|
||||
"component-affinity": "componentAffinity",
|
||||
"macro-params": "macroParams",
|
||||
"macro-rest-param": "macroRestParam",
|
||||
"macro-body": "macroBody",
|
||||
@@ -176,6 +191,7 @@ class JSEmitter:
|
||||
"sf-lambda": "sfLambda",
|
||||
"sf-define": "sfDefine",
|
||||
"sf-defcomp": "sfDefcomp",
|
||||
"defcomp-kwarg": "defcompKwarg",
|
||||
"sf-defmacro": "sfDefmacro",
|
||||
"sf-begin": "sfBegin",
|
||||
"sf-quote": "sfQuote",
|
||||
@@ -278,6 +294,7 @@ class JSEmitter:
|
||||
"for-each-indexed": "forEachIndexed",
|
||||
"index-of": "indexOf_",
|
||||
"component-has-children?": "componentHasChildren",
|
||||
"component-affinity": "componentAffinity",
|
||||
# engine.sx
|
||||
"ENGINE_VERBS": "ENGINE_VERBS",
|
||||
"DEFAULT_SWAP": "DEFAULT_SWAP",
|
||||
@@ -531,6 +548,7 @@ class JSEmitter:
|
||||
"transitive-io-refs": "transitiveIoRefs",
|
||||
"compute-all-io-refs": "computeAllIoRefs",
|
||||
"component-pure?": "componentPure_p",
|
||||
"render-target": "renderTarget",
|
||||
# router.sx
|
||||
"split-path-segments": "splitPathSegments",
|
||||
"make-route-segment": "makeRouteSegment",
|
||||
@@ -552,6 +570,9 @@ class JSEmitter:
|
||||
parts = result.split("-")
|
||||
if len(parts) > 1:
|
||||
result = parts[0] + "".join(p.capitalize() for p in parts[1:])
|
||||
# Escape JS reserved words
|
||||
if result in _JS_RESERVED:
|
||||
result = result + "_"
|
||||
return result
|
||||
|
||||
# --- List emission ---
|
||||
@@ -1018,7 +1039,7 @@ class JSEmitter:
|
||||
return str(expr)
|
||||
|
||||
def _js_string(self, s: str) -> str:
|
||||
return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t") + '"'
|
||||
return '"' + s.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t").replace("\0", "\\0") + '"'
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1995,12 +2016,13 @@ PREAMBLE = '''\
|
||||
}
|
||||
Lambda.prototype._lambda = true;
|
||||
|
||||
function Component(name, params, hasChildren, body, closure) {
|
||||
function Component(name, params, hasChildren, body, closure, affinity) {
|
||||
this.name = name;
|
||||
this.params = params;
|
||||
this.hasChildren = hasChildren;
|
||||
this.body = body;
|
||||
this.closure = closure || {};
|
||||
this.affinity = affinity || "auto";
|
||||
}
|
||||
Component.prototype._component = true;
|
||||
|
||||
@@ -2308,8 +2330,8 @@ PLATFORM_JS_PRE = '''
|
||||
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 makeComponent(name, params, hasChildren, body, env, affinity) {
|
||||
return new Component(name, params, hasChildren, body, merge(env), affinity);
|
||||
}
|
||||
function makeMacro(params, restParam, body, env, name) {
|
||||
return new Macro(params, restParam, body, merge(env), name);
|
||||
@@ -2327,6 +2349,7 @@ PLATFORM_JS_PRE = '''
|
||||
function componentClosure(c) { return c.closure; }
|
||||
function componentHasChildren(c) { return c.hasChildren; }
|
||||
function componentName(c) { return c.name; }
|
||||
function componentAffinity(c) { return c.affinity || "auto"; }
|
||||
|
||||
function macroParams(m) { return m.params; }
|
||||
function macroRestParam(m) { return m.restParam; }
|
||||
|
||||
Reference in New Issue
Block a user