From 20ac0fe948bcae08c432de5774ad6727b72d1496 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 6 Mar 2026 16:58:53 +0000 Subject: [PATCH] Spec eval-cond and process-bindings in render.sx (remove platform implementations) eval-cond and process-bindings were hand-written platform JS in bootstrap_js.py rather than specced in .sx files. This violated the SX host architecture principle. Now specced in render.sx as shared render adapter helpers, bootstrapped to both JS and Python. eval-cond handles both scheme-style ((test body) ...) and clojure-style (test body test body ...) cond clauses. Returns unevaluated body expression for the adapter to render in its own mode. process-bindings evaluates let-binding pairs and returns extended env. Co-Authored-By: Claude Opus 4.6 --- shared/static/scripts/sx-browser.js | 64 +++++++++++++------------- shared/sx/ref/bootstrap_js.py | 36 ++------------- shared/sx/ref/bootstrap_py.py | 2 + shared/sx/ref/render.sx | 69 +++++++++++++++++++++++++++++ shared/sx/ref/sx_ref.py | 12 +++++ 5 files changed, 117 insertions(+), 66 deletions(-) diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index f4ce87a..46eb395 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -493,39 +493,7 @@ "map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1 }; } - // processBindings and evalCond — exposed for DOM adapter render forms - function processBindings(bindings, env) { - var local = merge(env); - for (var i = 0; i < bindings.length; i++) { - var pair = bindings[i]; - if (Array.isArray(pair) && pair.length >= 2) { - var name = isSym(pair[0]) ? pair[0].name : String(pair[0]); - local[name] = trampoline(evalExpr(pair[1], local)); - } - } - return local; - } - function evalCond(clauses, env) { - // Detect scheme-style ((test body) ...) vs clojure-style (test body test body ...) - var scheme = clauses.length > 0 && Array.isArray(clauses[0]) && clauses[0].length === 2; - if (scheme) { - for (var i = 0; i < clauses.length; i++) { - var clause = clauses[i]; - var test = clause[0], body = clause[1]; - if (isSym(test) && (test.name === "else" || test.name === ":else")) return body; - if (isKw(test) && test.name === "else") return body; - if (isSxTruthy(trampoline(evalExpr(test, env)))) return body; - } - } else { - for (var i = 0; i < clauses.length; i += 2) { - var test = clauses[i]; - if (isSym(test) && test.name === ":else") return clauses[i + 1]; - if (isKw(test) && test.name === "else") return clauses[i + 1]; - if (isSxTruthy(trampoline(evalExpr(test, env)))) return clauses[i + 1]; - } - } - return null; - } + // processBindings and evalCond — now specced in render.sx, bootstrapped above function isDefinitionForm(name) { return name === "define" || name === "defcomp" || name === "defmacro" || @@ -1082,6 +1050,36 @@ return append_b(inits, nth(binding, 1)); }, bindings) : reduce(function(acc, pai return (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && val)) ? (String(" ") + String(key)) : (isSxTruthy((isSxTruthy(contains(BOOLEAN_ATTRS, key)) && !val)) ? "" : (isSxTruthy(isNil(val)) ? "" : (isSxTruthy((isSxTruthy((key == "style")) && isStyleValue(val))) ? (String(" class=\"") + String(styleValueClass(val)) + String("\"")) : (String(" ") + String(key) + String("=\"") + String(escapeAttr((String(val)))) + String("\"")))))); })(); }, keys(attrs))); }; + // eval-cond + var evalCond = function(clauses, env) { return (isSxTruthy((isSxTruthy(!isEmpty(clauses)) && isSxTruthy((typeOf(first(clauses)) == "list")) && (len(first(clauses)) == 2))) ? evalCondScheme(clauses, env) : evalCondClojure(clauses, env)); }; + + // eval-cond-scheme + var evalCondScheme = function(clauses, env) { return (isSxTruthy(isEmpty(clauses)) ? NIL : (function() { + var clause = first(clauses); + var test = first(clause); + var body = nth(clause, 1); + return (isSxTruthy(sxOr((isSxTruthy((typeOf(test) == "symbol")) && sxOr((symbolName(test) == "else"), (symbolName(test) == ":else"))), (isSxTruthy((typeOf(test) == "keyword")) && (keywordName(test) == "else")))) ? body : (isSxTruthy(trampoline(evalExpr(test, env))) ? body : evalCondScheme(rest(clauses), env))); +})()); }; + + // eval-cond-clojure + var evalCondClojure = function(clauses, env) { return (isSxTruthy((len(clauses) < 2)) ? NIL : (function() { + var test = first(clauses); + var body = nth(clauses, 1); + return (isSxTruthy(sxOr((isSxTruthy((typeOf(test) == "keyword")) && (keywordName(test) == "else")), (isSxTruthy((typeOf(test) == "symbol")) && sxOr((symbolName(test) == "else"), (symbolName(test) == ":else"))))) ? body : (isSxTruthy(trampoline(evalExpr(test, env))) ? body : evalCondClojure(slice(clauses, 2), env))); +})()); }; + + // process-bindings + var processBindings = function(bindings, env) { return (function() { + var local = merge(env); + { var _c = bindings; for (var _i = 0; _i < _c.length; _i++) { var pair = _c[_i]; if (isSxTruthy((isSxTruthy((typeOf(pair) == "list")) && (len(pair) >= 2)))) { + (function() { + var name = (isSxTruthy((typeOf(first(pair)) == "symbol")) ? symbolName(first(pair)) : (String(first(pair)))); + return envSet(local, name, trampoline(evalExpr(nth(pair, 1), local))); +})(); +} } } + return local; +})(); }; + // === Transpiled from parser === diff --git a/shared/sx/ref/bootstrap_js.py b/shared/sx/ref/bootstrap_js.py index 2667497..04c708b 100644 --- a/shared/sx/ref/bootstrap_js.py +++ b/shared/sx/ref/bootstrap_js.py @@ -273,6 +273,8 @@ class JSEmitter: "dict-delete!": "dictDelete", "process-bindings": "processBindings", "eval-cond": "evalCond", + "eval-cond-scheme": "evalCondScheme", + "eval-cond-clojure": "evalCondClojure", "for-each-indexed": "forEachIndexed", "index-of": "indexOf_", "component-has-children?": "componentHasChildren", @@ -1823,39 +1825,7 @@ PLATFORM_JS_POST = ''' "map":1,"map-indexed":1,"filter":1,"reduce":1,"some":1,"every?":1,"for-each":1 }; } - // processBindings and evalCond — exposed for DOM adapter render forms - function processBindings(bindings, env) { - var local = merge(env); - for (var i = 0; i < bindings.length; i++) { - var pair = bindings[i]; - if (Array.isArray(pair) && pair.length >= 2) { - var name = isSym(pair[0]) ? pair[0].name : String(pair[0]); - local[name] = trampoline(evalExpr(pair[1], local)); - } - } - return local; - } - function evalCond(clauses, env) { - // Detect scheme-style ((test body) ...) vs clojure-style (test body test body ...) - var scheme = clauses.length > 0 && Array.isArray(clauses[0]) && clauses[0].length === 2; - if (scheme) { - for (var i = 0; i < clauses.length; i++) { - var clause = clauses[i]; - var test = clause[0], body = clause[1]; - if (isSym(test) && (test.name === "else" || test.name === ":else")) return body; - if (isKw(test) && test.name === "else") return body; - if (isSxTruthy(trampoline(evalExpr(test, env)))) return body; - } - } else { - for (var i = 0; i < clauses.length; i += 2) { - var test = clauses[i]; - if (isSym(test) && test.name === ":else") return clauses[i + 1]; - if (isKw(test) && test.name === "else") return clauses[i + 1]; - if (isSxTruthy(trampoline(evalExpr(test, env)))) return clauses[i + 1]; - } - } - return null; - } + // processBindings and evalCond — now specced in render.sx, bootstrapped above function isDefinitionForm(name) { return name === "define" || name === "defcomp" || name === "defmacro" || diff --git a/shared/sx/ref/bootstrap_py.py b/shared/sx/ref/bootstrap_py.py index d9c4f5a..2fee342 100644 --- a/shared/sx/ref/bootstrap_py.py +++ b/shared/sx/ref/bootstrap_py.py @@ -236,6 +236,8 @@ class PyEmitter: "map-indexed": "map_indexed", "map-dict": "map_dict", "eval-cond": "eval_cond", + "eval-cond-scheme": "eval_cond_scheme", + "eval-cond-clojure": "eval_cond_clojure", "process-bindings": "process_bindings", # deps.sx "scan-refs": "scan_refs", diff --git a/shared/sx/ref/render.sx b/shared/sx/ref/render.sx index 7bead17..9fabd11 100644 --- a/shared/sx/ref/render.sx +++ b/shared/sx/ref/render.sx @@ -124,6 +124,75 @@ (keys attrs))))) +;; -------------------------------------------------------------------------- +;; Render adapter helpers +;; -------------------------------------------------------------------------- +;; Shared by HTML and DOM adapters for evaluating control forms during +;; rendering. Unlike sf-cond (eval.sx) which returns a thunk for TCO, +;; eval-cond returns the unevaluated body expression so the adapter +;; can render it in its own mode (HTML string vs DOM nodes). + +;; eval-cond: find matching cond branch, return unevaluated body expr. +;; Handles both scheme-style ((test body) ...) and clojure-style +;; (test body test body ...). +(define eval-cond + (fn (clauses env) + (if (and (not (empty? clauses)) + (= (type-of (first clauses)) "list") + (= (len (first clauses)) 2)) + ;; Scheme-style + (eval-cond-scheme clauses env) + ;; Clojure-style + (eval-cond-clojure clauses env)))) + +(define eval-cond-scheme + (fn (clauses env) + (if (empty? clauses) + nil + (let ((clause (first clauses)) + (test (first clause)) + (body (nth clause 1))) + (if (or (and (= (type-of test) "symbol") + (or (= (symbol-name test) "else") + (= (symbol-name test) ":else"))) + (and (= (type-of test) "keyword") + (= (keyword-name test) "else"))) + body + (if (trampoline (eval-expr test env)) + body + (eval-cond-scheme (rest clauses) env))))))) + +(define eval-cond-clojure + (fn (clauses env) + (if (< (len clauses) 2) + nil + (let ((test (first clauses)) + (body (nth clauses 1))) + (if (or (and (= (type-of test) "keyword") (= (keyword-name test) "else")) + (and (= (type-of test) "symbol") + (or (= (symbol-name test) "else") + (= (symbol-name test) ":else")))) + body + (if (trampoline (eval-expr test env)) + body + (eval-cond-clojure (slice clauses 2) env))))))) + +;; process-bindings: evaluate let-binding pairs, return extended env. +;; bindings = ((name1 expr1) (name2 expr2) ...) +(define process-bindings + (fn (bindings env) + (let ((local (merge env))) + (for-each + (fn (pair) + (when (and (= (type-of pair) "list") (>= (len pair) 2)) + (let ((name (if (= (type-of (first pair)) "symbol") + (symbol-name (first pair)) + (str (first pair))))) + (env-set! local name (trampoline (eval-expr (nth pair 1) local)))))) + bindings) + local))) + + ;; -------------------------------------------------------------------------- ;; Platform interface (shared across adapters) ;; -------------------------------------------------------------------------- diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 54b7ca6..40f9dba 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -1140,6 +1140,18 @@ parse_element_args = lambda args, env: (lambda attrs: (lambda children: _sx_begi # render-attrs render_attrs = lambda attrs: join('', map(lambda key: (lambda val: (sx_str(' ', key) if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else val)) else ('' if sx_truthy((contains_p(BOOLEAN_ATTRS, key) if not sx_truthy(contains_p(BOOLEAN_ATTRS, key)) else (not sx_truthy(val)))) else ('' if sx_truthy(is_nil(val)) else (sx_str(' class="', style_value_class(val), '"') if sx_truthy(((key == 'style') if not sx_truthy((key == 'style')) else is_style_value(val))) else sx_str(' ', key, '="', escape_attr(sx_str(val)), '"'))))))(dict_get(attrs, key)), keys(attrs))) +# eval-cond +eval_cond = lambda clauses, env: (eval_cond_scheme(clauses, env) if sx_truthy(((not sx_truthy(empty_p(clauses))) if not sx_truthy((not sx_truthy(empty_p(clauses)))) else ((type_of(first(clauses)) == 'list') if not sx_truthy((type_of(first(clauses)) == 'list')) else (len(first(clauses)) == 2)))) else eval_cond_clojure(clauses, env)) + +# eval-cond-scheme +eval_cond_scheme = lambda clauses, env: (NIL if sx_truthy(empty_p(clauses)) else (lambda clause: (lambda test: (lambda body: (body if sx_truthy((((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))) if sx_truthy(((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else')))) else ((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')))) else (body if sx_truthy(trampoline(eval_expr(test, env))) else eval_cond_scheme(rest(clauses), env))))(nth(clause, 1)))(first(clause)))(first(clauses))) + +# eval-cond-clojure +eval_cond_clojure = lambda clauses, env: (NIL if sx_truthy((len(clauses) < 2)) else (lambda test: (lambda body: (body if sx_truthy((((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else')) if sx_truthy(((type_of(test) == 'keyword') if not sx_truthy((type_of(test) == 'keyword')) else (keyword_name(test) == 'else'))) else ((type_of(test) == 'symbol') if not sx_truthy((type_of(test) == 'symbol')) else ((symbol_name(test) == 'else') if sx_truthy((symbol_name(test) == 'else')) else (symbol_name(test) == ':else'))))) else (body if sx_truthy(trampoline(eval_expr(test, env))) else eval_cond_clojure(slice(clauses, 2), env))))(nth(clauses, 1)))(first(clauses))) + +# process-bindings +process_bindings = lambda bindings, env: (lambda local: _sx_begin(for_each(lambda pair: ((lambda name: _sx_dict_set(local, name, trampoline(eval_expr(nth(pair, 1), local))))((symbol_name(first(pair)) if sx_truthy((type_of(first(pair)) == 'symbol')) else sx_str(first(pair)))) if sx_truthy(((type_of(pair) == 'list') if not sx_truthy((type_of(pair) == 'list')) else (len(pair) >= 2))) else NIL), bindings), local))(merge(env)) + # === Transpiled from adapter-html ===