Decouple core evaluator from web platform, extract libraries
The core evaluator (spec/evaluator.sx) is now the irreducible computational core with zero web, rendering, or type-system knowledge. 2531 → 2313 lines. - Add extensible special form registry (*custom-special-forms* + register-special-form!) - Add render dispatch hooks (*render-check* / *render-fn*) replacing hardcoded render-active?/is-render-expr?/render-expr - Extract freeze scopes → spec/freeze.sx (library, not core) - Extract content addressing → spec/content.sx (library, not core) - Move sf-deftype/sf-defeffect → spec/types.sx (self-registering) - Move sf-defstyle → web/forms.sx (self-registering with all web forms) - Move web tests (defpage, streaming) → web/tests/test-forms.sx - Add is-else-clause? helper (replaces 5 inline patterns) - Make escape-html/escape-attr library functions in render.sx (pure SX, not platform-provided) - Add foundations plan: Step 3.5 (data representations), Step 3.7 (verified components), OCaml for Step 4d - Update all three bootstrappers (JS 957/957, Python 744/744, OCaml 952/952) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -43,16 +43,30 @@ PREAMBLE = """\
|
||||
open Sx_types
|
||||
open Sx_runtime
|
||||
|
||||
(* Trampoline — evaluates thunks via the CEK machine.
|
||||
eval_expr is defined in the transpiled block below. *)
|
||||
let trampoline v = v (* CEK machine doesn't produce thunks *)
|
||||
(* Trampoline — forward ref, resolved after eval_expr is defined. *)
|
||||
let trampoline_fn : (value -> value) ref = ref (fun v -> v)
|
||||
let trampoline v = !trampoline_fn v
|
||||
|
||||
|
||||
|
||||
(* === Mutable state for strict mode === *)
|
||||
(* These are defined as top-level refs because the transpiler cannot handle
|
||||
global set! mutation (it creates local refs that shadow the global). *)
|
||||
let _strict_ref = ref (Bool false)
|
||||
let _prim_param_types_ref = ref Nil
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# OCaml fixups — override iterative CEK run
|
||||
# OCaml fixups — wire up trampoline + iterative CEK run
|
||||
FIXUPS = """\
|
||||
|
||||
(* Wire up trampoline to resolve thunks via the CEK machine *)
|
||||
let () = trampoline_fn := (fun v ->
|
||||
match v with
|
||||
| Thunk (expr, env) -> eval_expr expr (Env env)
|
||||
| _ -> v)
|
||||
|
||||
(* Override recursive cek_run with iterative loop *)
|
||||
let cek_run_iterative state =
|
||||
let s = ref state in
|
||||
@@ -122,7 +136,63 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str:
|
||||
parts.append(result)
|
||||
|
||||
parts.append(FIXUPS)
|
||||
return "\n".join(parts)
|
||||
output = "\n".join(parts)
|
||||
|
||||
# Post-process: fix mutable globals that the transpiler can't handle.
|
||||
# The transpiler emits local refs for set! targets within functions,
|
||||
# but top-level globals (*strict*, *prim-param-types*) need to use
|
||||
# the pre-declared refs from the preamble.
|
||||
import re
|
||||
|
||||
# Fix *strict*: use _strict_ref instead of immutable let rec binding
|
||||
output = re.sub(
|
||||
r'and _strict_ =\n \(Bool false\)',
|
||||
'and _strict_ = !_strict_ref',
|
||||
output,
|
||||
)
|
||||
# Fix set-strict!: use _strict_ref instead of local ref
|
||||
output = re.sub(
|
||||
r'and set_strict_b val\' =\n let _strict_ = ref Nil in \(_strict_ := val\'; Nil\)',
|
||||
"and set_strict_b val' =\n _strict_ref := val'; Nil",
|
||||
output,
|
||||
)
|
||||
# Fix *prim-param-types*: use _prim_param_types_ref
|
||||
output = re.sub(
|
||||
r'and _prim_param_types_ =\n Nil',
|
||||
'and _prim_param_types_ = !_prim_param_types_ref',
|
||||
output,
|
||||
)
|
||||
# Fix set-prim-param-types!: use _prim_param_types_ref
|
||||
output = re.sub(
|
||||
r'and set_prim_param_types_b types =\n let _prim_param_types_ = ref Nil in \(_prim_param_types_ := types; Nil\)',
|
||||
"and set_prim_param_types_b types =\n _prim_param_types_ref := types; Nil",
|
||||
output,
|
||||
)
|
||||
|
||||
# Fix all runtime reads of _strict_ and _prim_param_types_ to deref
|
||||
# the mutable refs instead of using the stale let-rec bindings.
|
||||
# This is needed because let-rec value bindings capture initial values.
|
||||
# Use regex with word boundary to avoid replacing _strict_ref with
|
||||
# !_strict_refref.
|
||||
def fix_mutable_reads(text):
|
||||
lines = text.split('\n')
|
||||
fixed = []
|
||||
for line in lines:
|
||||
# Skip the definition lines
|
||||
stripped = line.strip()
|
||||
if stripped.startswith('and _strict_ =') or stripped.startswith('and _prim_param_types_ ='):
|
||||
fixed.append(line)
|
||||
continue
|
||||
# Replace _strict_ as a standalone identifier only (not inside
|
||||
# other names like set_strict_b). Match when preceded by space,
|
||||
# paren, or start-of-line, and followed by space, paren, or ;.
|
||||
line = re.sub(r'(?<=[ (])_strict_(?=[ );])', '!_strict_ref', line)
|
||||
line = re.sub(r'(?<=[ (])_prim_param_types_(?=[ );])', '!_prim_param_types_ref', line)
|
||||
fixed.append(line)
|
||||
return '\n'.join(fixed)
|
||||
output = fix_mutable_reads(output)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -297,10 +297,26 @@ let scope_pop _name = Nil
|
||||
let provide_push name value = ignore name; ignore value; Nil
|
||||
let provide_pop _name = Nil
|
||||
|
||||
(* Render mode stubs *)
|
||||
let render_active_p () = Bool false
|
||||
let render_expr _expr _env = Nil
|
||||
let is_render_expr _expr = Bool false
|
||||
(* Custom special forms registry — mutable dict *)
|
||||
let custom_special_forms = Dict (Hashtbl.create 4)
|
||||
|
||||
(* register-special-form! — add a handler to the custom registry *)
|
||||
let register_special_form name handler =
|
||||
(match custom_special_forms with
|
||||
| Dict tbl -> Hashtbl.replace tbl (value_to_str name) handler; handler
|
||||
| _ -> raise (Eval_error "custom_special_forms not a dict"))
|
||||
|
||||
(* Render check/fn hooks — nil by default, set by platform if needed *)
|
||||
let render_check = Nil
|
||||
let render_fn = Nil
|
||||
|
||||
(* is-else-clause? — check if a cond/case test is an else marker *)
|
||||
let is_else_clause v =
|
||||
match v with
|
||||
| Keyword k -> Bool (k = "else" || k = "default")
|
||||
| Symbol s -> Bool (s = "else" || s = "default")
|
||||
| Bool true -> Bool true
|
||||
| _ -> Bool false
|
||||
|
||||
(* Signal accessors *)
|
||||
let signal_value s = match s with Signal sig' -> sig'.s_value | _ -> raise (Eval_error "not a signal")
|
||||
|
||||
@@ -123,9 +123,11 @@
|
||||
"provide-push!" "provide_push"
|
||||
"provide-pop!" "provide_pop"
|
||||
"sx-serialize" "sx_serialize"
|
||||
"render-active?" "render_active_p"
|
||||
"is-render-expr?" "is_render_expr"
|
||||
"render-expr" "render_expr"
|
||||
"*custom-special-forms*" "custom_special_forms"
|
||||
"register-special-form!" "register_special_form"
|
||||
"*render-check*" "render_check"
|
||||
"*render-fn*" "render_fn"
|
||||
"is-else-clause?" "is_else_clause"
|
||||
"HTML_TAGS" "html_tags"
|
||||
"VOID_ELEMENTS" "void_elements"
|
||||
"BOOLEAN_ATTRS" "boolean_attrs"
|
||||
@@ -192,15 +194,12 @@
|
||||
"cek-call" "cek-run" "sx-call" "sx-apply"
|
||||
"collect!" "collected" "clear-collected!" "context" "emit!" "emitted"
|
||||
"scope-push!" "scope-pop!" "provide-push!" "provide-pop!"
|
||||
"render-active?" "render-expr" "is-render-expr?"
|
||||
"with-island-scope" "register-in-scope"
|
||||
"signal-value" "signal-set-value" "signal-subscribers"
|
||||
"signal-add-sub!" "signal-remove-sub!" "signal-deps" "signal-set-deps"
|
||||
"notify-subscribers" "flush-subscribers" "dispose-computed"
|
||||
"continuation?" "continuation-data" "make-cek-continuation"
|
||||
"dynamic-wind-call" "strip-prefix"
|
||||
"sf-defhandler" "sf-defpage" "sf-defquery" "sf-defaction"
|
||||
"make-handler-def" "make-query-def" "make-action-def" "make-page-def"
|
||||
"component-set-param-types!" "parse-comp-params" "parse-macro-params"
|
||||
"parse-keyword-args"))
|
||||
|
||||
@@ -215,6 +214,15 @@
|
||||
;; Check _known_defines (set by bootstrap.py)
|
||||
(some (fn (d) (= d name)) _known_defines)))))
|
||||
|
||||
;; Dynamic globals — top-level defines that hold SX values (not functions).
|
||||
;; When these appear as callees, use cek_call for dynamic dispatch.
|
||||
(define ml-dynamic-globals
|
||||
(list "*render-check*" "*render-fn*"))
|
||||
|
||||
(define ml-is-dyn-global?
|
||||
(fn ((name :as string))
|
||||
(some (fn (g) (= g name)) ml-dynamic-globals)))
|
||||
|
||||
;; Check if a variable is "dynamic" — locally bound to a non-function expression.
|
||||
;; These variables hold SX values (from eval-expr, get, etc.) and need cek_call
|
||||
;; when used as callees. We encode this in the set-vars list as "dyn:name".
|
||||
@@ -421,8 +429,12 @@
|
||||
(let ((head (first expr))
|
||||
(args (rest expr)))
|
||||
(if (not (= (type-of head) "symbol"))
|
||||
;; Data list
|
||||
(str "[" (join "; " (map (fn (x) (ml-expr-inner x set-vars)) expr)) "]")
|
||||
;; Non-symbol head: if head is a list (call expr), dispatch via cek_call;
|
||||
;; otherwise treat as data list
|
||||
(if (list? head)
|
||||
(str "(cek_call (" (ml-expr-inner head set-vars)
|
||||
") (List [" (join "; " (map (fn (x) (ml-expr-inner x set-vars)) args)) "]))")
|
||||
(str "[" (join "; " (map (fn (x) (ml-expr-inner x set-vars)) expr)) "]"))
|
||||
(let ((op (symbol-name head)))
|
||||
(cond
|
||||
;; fn/lambda
|
||||
@@ -607,8 +619,8 @@
|
||||
;; Regular function call
|
||||
:else
|
||||
(let ((callee (ml-mangle op)))
|
||||
(if (ml-is-dyn-var? op set-vars)
|
||||
;; Dynamic callee (local var bound to non-fn expr) — dispatch via cek_call
|
||||
(if (or (ml-is-dyn-var? op set-vars) (ml-is-dyn-global? op))
|
||||
;; Dynamic callee (local var or dynamic global) — dispatch via cek_call
|
||||
(str "(cek_call (" callee ") (List [" (join "; " (map (fn (x) (ml-expr-inner x set-vars)) args)) "]))")
|
||||
;; Static callee — direct OCaml call
|
||||
(if (empty? args)
|
||||
|
||||
Reference in New Issue
Block a user