From f54ebf26f82cb8d787c64edf05f6a48bfae861af Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 11 Mar 2026 03:42:04 +0000 Subject: [PATCH] Separate eval from render: render-active? gate in eval-list eval-list only dispatches to the render adapter when render-active? is true. render-to-html and aser set render-active! on entry. Pure evaluate() calls no longer stringify component results through the render adapter. Fixes component children parity: (defcomp ~wrap (&key &rest children) children) now returns [1,2,3] in eval mode, renders to "123" only in render mode. Parity: 112/116 pass (remaining 4 are hand-written evaluator.py bugs). Co-Authored-By: Claude Opus 4.6 --- shared/sx/ref/adapter-html.sx | 1 + shared/sx/ref/adapter-sx.sx | 1 + shared/sx/ref/eval.sx | 4 ++-- shared/sx/ref/platform_py.py | 20 +++++++++++++++- shared/sx/ref/sx_ref.py | 43 +++++++++++++++++++++++++++-------- 5 files changed, 56 insertions(+), 13 deletions(-) diff --git a/shared/sx/ref/adapter-html.sx b/shared/sx/ref/adapter-html.sx index 9627faa..039090e 100644 --- a/shared/sx/ref/adapter-html.sx +++ b/shared/sx/ref/adapter-html.sx @@ -15,6 +15,7 @@ (define render-to-html (fn (expr env) + (set-render-active! true) (case (type-of expr) ;; Literals — render directly "nil" "" diff --git a/shared/sx/ref/adapter-sx.sx b/shared/sx/ref/adapter-sx.sx index 4d0d3fd..b045faa 100644 --- a/shared/sx/ref/adapter-sx.sx +++ b/shared/sx/ref/adapter-sx.sx @@ -24,6 +24,7 @@ (fn (expr env) ;; Evaluate for SX wire format — serialize rendering forms, ;; evaluate control flow and function calls. + (set-render-active! true) (case (type-of expr) "number" expr "string" expr diff --git a/shared/sx/ref/eval.sx b/shared/sx/ref/eval.sx index 3a1c92c..b525811 100644 --- a/shared/sx/ref/eval.sx +++ b/shared/sx/ref/eval.sx @@ -174,8 +174,8 @@ (let ((mac (env-get env name))) (make-thunk (expand-macro mac args env) env)) - ;; Render expression — delegate to active adapter. - (is-render-expr? expr) + ;; Render expression — delegate to active adapter (only when rendering). + (and (render-active?) (is-render-expr? expr)) (render-expr expr env) ;; Fall through to function call diff --git a/shared/sx/ref/platform_py.py b/shared/sx/ref/platform_py.py index 3701587..d7d3619 100644 --- a/shared/sx/ref/platform_py.py +++ b/shared/sx/ref/platform_py.py @@ -524,6 +524,19 @@ def is_render_expr(expr): # Render dispatch -- set by adapter _render_expr_fn = None +# Render mode flag -- set by render-to-html/aser, checked by eval-list +# When false, render expressions (HTML tags, components) fall through to eval-call +_render_mode = False + + +def render_active_p(): + return _render_mode + + +def set_render_active_b(val): + global _render_mode + _render_mode = bool(val) + def render_expr(expr, env): if _render_expr_fn: @@ -1342,9 +1355,14 @@ def public_api_py(has_html: bool, has_sx: bool, has_deps: bool = False) -> str: '', 'def render(expr, env=None):', ' """Render expr to HTML string."""', + ' global _render_mode', ' if env is None:', ' env = {}', - ' return render_to_html(expr, env)', + ' try:', + ' _render_mode = True', + ' return render_to_html(expr, env)', + ' finally:', + ' _render_mode = False', '', '', 'def make_env(**kwargs):', diff --git a/shared/sx/ref/sx_ref.py b/shared/sx/ref/sx_ref.py index 5c90438..2e0d80e 100644 --- a/shared/sx/ref/sx_ref.py +++ b/shared/sx/ref/sx_ref.py @@ -1,12 +1,10 @@ -# WARNING: special-forms.sx declares forms not in eval.sx: reset, shift -# WARNING: eval.sx dispatches forms not in special-forms.sx: form? """ sx_ref.py -- Generated from reference SX evaluator specification. Bootstrap-compiled from shared/sx/ref/{eval,render,adapter-html,adapter-sx}.sx Compare against hand-written evaluator.py / html.py for correctness verification. -DO NOT EDIT -- regenerate with: python bootstrap_py.py +DO NOT EDIT -- regenerate with: python run_py_sx.py """ from __future__ import annotations @@ -485,11 +483,25 @@ def is_render_expr(expr): # Render dispatch -- set by adapter _render_expr_fn = None +# Render mode flag -- set by render-to-html/aser, checked by eval-list +# When false, render expressions (HTML tags, components) fall through to eval-call +_render_mode = False + + +def render_active_p(): + return _render_mode + + +def set_render_active_b(val): + global _render_mode + _render_mode = bool(val) + def render_expr(expr, env): if _render_expr_fn: return _render_expr_fn(expr, env) - return expr + # No adapter — fall through to eval_call so components still evaluate + return eval_call(first(expr), rest(expr), env) def strip_prefix(s, prefix): @@ -711,12 +723,12 @@ PRIMITIVES["upper"] = lambda s: str(s).upper() PRIMITIVES["lower"] = lambda s: str(s).lower() PRIMITIVES["trim"] = lambda s: str(s).strip() PRIMITIVES["split"] = lambda s, sep=" ": str(s).split(sep) -PRIMITIVES["join"] = lambda sep, coll: sep.join(coll) +PRIMITIVES["join"] = lambda sep, coll: sep.join(str(x) for x in coll) PRIMITIVES["replace"] = lambda s, old, new: s.replace(old, new) PRIMITIVES["index-of"] = lambda s, needle, start=0: str(s).find(needle, start) PRIMITIVES["starts-with?"] = lambda s, p: str(s).startswith(p) PRIMITIVES["ends-with?"] = lambda s, p: str(s).endswith(p) -PRIMITIVES["slice"] = lambda c, a, b=None: c[a:b] if b is not None else c[a:] +PRIMITIVES["slice"] = lambda c, a, b=None: c[int(a):] if (b is None or b is NIL) else c[int(a):int(b)] PRIMITIVES["concat"] = lambda *args: _b_sum((a for a in args if a), []) @@ -939,7 +951,7 @@ trampoline = lambda val: (lambda result: (trampoline(eval_expr(thunk_expr(result eval_expr = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('dict', lambda: map_dict(lambda k, v: trampoline(eval_expr(v, env)), expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else eval_list(expr, env))), (None, lambda: expr)]) # eval-list -eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defisland(args, env) if sx_truthy((name == 'defisland')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy(is_render_expr(expr)) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr)) +eval_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: trampoline(eval_expr(x, env)), expr) if sx_truthy((not sx_truthy(((type_of(head) == 'symbol') if sx_truthy((type_of(head) == 'symbol')) else ((type_of(head) == 'lambda') if sx_truthy((type_of(head) == 'lambda')) else (type_of(head) == 'list')))))) else ((lambda name: (sf_if(args, env) if sx_truthy((name == 'if')) else (sf_when(args, env) if sx_truthy((name == 'when')) else (sf_cond(args, env) if sx_truthy((name == 'cond')) else (sf_case(args, env) if sx_truthy((name == 'case')) else (sf_and(args, env) if sx_truthy((name == 'and')) else (sf_or(args, env) if sx_truthy((name == 'or')) else (sf_let(args, env) if sx_truthy((name == 'let')) else (sf_let(args, env) if sx_truthy((name == 'let*')) else (sf_letrec(args, env) if sx_truthy((name == 'letrec')) else (sf_lambda(args, env) if sx_truthy((name == 'lambda')) else (sf_lambda(args, env) if sx_truthy((name == 'fn')) else (sf_define(args, env) if sx_truthy((name == 'define')) else (sf_defcomp(args, env) if sx_truthy((name == 'defcomp')) else (sf_defisland(args, env) if sx_truthy((name == 'defisland')) else (sf_defmacro(args, env) if sx_truthy((name == 'defmacro')) else (sf_defstyle(args, env) if sx_truthy((name == 'defstyle')) else (sf_defhandler(args, env) if sx_truthy((name == 'defhandler')) else (sf_defpage(args, env) if sx_truthy((name == 'defpage')) else (sf_defquery(args, env) if sx_truthy((name == 'defquery')) else (sf_defaction(args, env) if sx_truthy((name == 'defaction')) else (sf_begin(args, env) if sx_truthy((name == 'begin')) else (sf_begin(args, env) if sx_truthy((name == 'do')) else (sf_quote(args, env) if sx_truthy((name == 'quote')) else (sf_quasiquote(args, env) if sx_truthy((name == 'quasiquote')) else (sf_thread_first(args, env) if sx_truthy((name == '->')) else (sf_set_bang(args, env) if sx_truthy((name == 'set!')) else (sf_reset(args, env) if sx_truthy((name == 'reset')) else (sf_shift(args, env) if sx_truthy((name == 'shift')) else (sf_dynamic_wind(args, env) if sx_truthy((name == 'dynamic-wind')) else (ho_map(args, env) if sx_truthy((name == 'map')) else (ho_map_indexed(args, env) if sx_truthy((name == 'map-indexed')) else (ho_filter(args, env) if sx_truthy((name == 'filter')) else (ho_reduce(args, env) if sx_truthy((name == 'reduce')) else (ho_some(args, env) if sx_truthy((name == 'some')) else (ho_every(args, env) if sx_truthy((name == 'every?')) else (ho_for_each(args, env) if sx_truthy((name == 'for-each')) else ((lambda mac: make_thunk(expand_macro(mac, args, env), env))(env_get(env, name)) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (render_expr(expr, env) if sx_truthy((render_active_p() if not sx_truthy(render_active_p()) else is_render_expr(expr))) else eval_call(head, args, env))))))))))))))))))))))))))))))))))))))))(symbol_name(head)) if sx_truthy((type_of(head) == 'symbol')) else eval_call(head, args, env))))(rest(expr)))(first(expr)) # eval-call eval_call = lambda head, args, env: (lambda f: (lambda evaluated_args: (apply(f, evaluated_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else ((not sx_truthy(is_component(f))) if not sx_truthy((not sx_truthy(is_component(f)))) else (not sx_truthy(is_island(f))))))) else (call_lambda(f, evaluated_args, env) if sx_truthy(is_lambda(f)) else (call_component(f, args, env) if sx_truthy(is_component(f)) else (call_component(f, args, env) if sx_truthy(is_island(f)) else error(sx_str('Not callable: ', inspect(f))))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env))) @@ -1186,7 +1198,10 @@ is_render_expr = lambda expr: (False if sx_truthy(((not sx_truthy((type_of(expr) # === Transpiled from adapter-html === # render-to-html -render_to_html = lambda expr, env: _sx_case(type_of(expr), [('nil', lambda: ''), ('string', lambda: escape_html(expr)), ('number', lambda: sx_str(expr)), ('boolean', lambda: ('true' if sx_truthy(expr) else 'false')), ('list', lambda: ('' if sx_truthy(empty_p(expr)) else render_list_to_html(expr, env))), ('symbol', lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env)), ('keyword', lambda: escape_html(keyword_name(expr))), ('raw-html', lambda: raw_html_content(expr)), (None, lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env))]) +render_to_html = _sx_fn(lambda expr, env: ( + set_render_active_b(True), + _sx_case(type_of(expr), [('nil', lambda: ''), ('string', lambda: escape_html(expr)), ('number', lambda: sx_str(expr)), ('boolean', lambda: ('true' if sx_truthy(expr) else 'false')), ('list', lambda: ('' if sx_truthy(empty_p(expr)) else render_list_to_html(expr, env))), ('symbol', lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env)), ('keyword', lambda: escape_html(keyword_name(expr))), ('raw-html', lambda: raw_html_content(expr)), (None, lambda: render_value_to_html(trampoline(eval_expr(expr, env)), env))]) +)[-1]) # render-value-to-html render_value_to_html = lambda val, env: _sx_case(type_of(val), [('nil', lambda: ''), ('string', lambda: escape_html(val)), ('number', lambda: sx_str(val)), ('boolean', lambda: ('true' if sx_truthy(val) else 'false')), ('list', lambda: render_list_to_html(val, env)), ('raw-html', lambda: raw_html_content(val)), (None, lambda: escape_html(sx_str(val)))]) @@ -1243,7 +1258,10 @@ serialize_island_state = lambda kwargs: (NIL if sx_truthy(is_empty_dict(kwargs)) render_to_sx = lambda expr, env: (lambda result: (result if sx_truthy((type_of(result) == 'string')) else serialize(result)))(aser(expr, env)) # aser -aser = lambda expr, env: _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else aser_list(expr, env))), (None, lambda: expr)]) +aser = _sx_fn(lambda expr, env: ( + set_render_active_b(True), + _sx_case(type_of(expr), [('number', lambda: expr), ('string', lambda: expr), ('boolean', lambda: expr), ('nil', lambda: NIL), ('symbol', lambda: (lambda name: (env_get(env, name) if sx_truthy(env_has(env, name)) else (get_primitive(name) if sx_truthy(is_primitive(name)) else (True if sx_truthy((name == 'true')) else (False if sx_truthy((name == 'false')) else (NIL if sx_truthy((name == 'nil')) else error(sx_str('Undefined symbol: ', name))))))))(symbol_name(expr))), ('keyword', lambda: keyword_name(expr)), ('list', lambda: ([] if sx_truthy(empty_p(expr)) else aser_list(expr, env))), (None, lambda: expr)]) +)[-1]) # aser-list aser_list = lambda expr, env: (lambda head: (lambda args: (map(lambda x: aser(x, env), expr) if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))) else (lambda name: (aser_fragment(args, env) if sx_truthy((name == '<>')) else (aser_call(name, args, env) if sx_truthy(starts_with_p(name, '~')) else (aser_call(name, args, env) if sx_truthy((name == 'lake')) else (aser_call(name, args, env) if sx_truthy((name == 'marsh')) else (aser_call(name, args, env) if sx_truthy(contains_p(HTML_TAGS, name)) else (aser_special(name, expr, env) if sx_truthy((is_special_form(name) if sx_truthy(is_special_form(name)) else is_ho_form(name))) else (aser(expand_macro(env_get(env, name), args, env), env) if sx_truthy((env_has(env, name) if not sx_truthy(env_has(env, name)) else is_macro(env_get(env, name)))) else (lambda f: (lambda evaled_args: (apply(f, evaled_args) if sx_truthy((is_callable(f) if not sx_truthy(is_callable(f)) else ((not sx_truthy(is_lambda(f))) if not sx_truthy((not sx_truthy(is_lambda(f)))) else ((not sx_truthy(is_component(f))) if not sx_truthy((not sx_truthy(is_component(f)))) else (not sx_truthy(is_island(f))))))) else (trampoline(call_lambda(f, evaled_args, env)) if sx_truthy(is_lambda(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_component(f)) else (aser_call(sx_str('~', component_name(f)), args, env) if sx_truthy(is_island(f)) else error(sx_str('Not callable: ', inspect(f))))))))(map(lambda a: trampoline(eval_expr(a, env)), args)))(trampoline(eval_expr(head, env)))))))))))(symbol_name(head))))(rest(expr)))(first(expr)) @@ -1509,9 +1527,14 @@ def evaluate(expr, env=None): def render(expr, env=None): """Render expr to HTML string.""" + global _render_mode if env is None: env = {} - return render_to_html(expr, env) + try: + _render_mode = True + return render_to_html(expr, env) + finally: + _render_mode = False def make_env(**kwargs):