OCaml CEK machine compiled to WebAssembly for browser execution
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 23m17s

- wasm_of_ocaml compiles OCaml SX engine to WASM (722/722 spec tests)
- js_of_ocaml fallback also working (722/722 spec tests)
- Thin JS platform layer (sx-platform.js) with ~80 DOM/browser natives
- Lambda callback bridge: SX lambdas callable from JS via handle table
- Side-channel pattern bypasses js_of_ocaml return-value property stripping
- Web adapters (signals, deps, router, adapter-html) load as SX source
- Render mode dispatch: HTML tags + fragments route to OCaml renderer
- Island/component accessors handle both Component and Island types
- Dict-based signal support (signals.sx creates dicts, not native Signal)
- Scope stack implementation (collect!/collected/emit!/emitted/context)
- Bundle script embeds web adapters + WASM loader + platform layer
- SX_USE_WASM env var toggles WASM engine in dev/production
- Bootstrap extended: --web flag transpiles web adapters, :effects stripping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-16 07:13:49 +00:00
parent 5ab3ecb7e0
commit 0caa965de0
44 changed files with 5167 additions and 171 deletions

View File

@@ -1,2 +1,3 @@
(library
(name sx))
(name sx)
(wrapped false))

File diff suppressed because one or more lines are too long

View File

@@ -131,35 +131,44 @@ let render_html_element tag args env =
(List.map (fun c -> render_to_html c env) children) in
"<" ^ tag ^ attr_str ^ ">" ^ content ^ "</" ^ tag ^ ">"
let render_component_generic ~params ~has_children ~body ~closure args env =
let kwargs = Hashtbl.create 8 in
let children_exprs = ref [] in
let skip = ref false in
let len = List.length args in
List.iteri (fun idx arg ->
if !skip then skip := false
else match arg with
| Keyword k when idx + 1 < len ->
let v = Sx_ref.eval_expr (List.nth args (idx + 1)) (Env env) in
Hashtbl.replace kwargs k v;
skip := true
| _ ->
children_exprs := arg :: !children_exprs
) args;
let children = List.rev !children_exprs in
let local = env_merge closure env in
List.iter (fun p ->
let v = match Hashtbl.find_opt kwargs p with Some v -> v | None -> Nil in
ignore (env_bind local p v)
) params;
if has_children then begin
let rendered_children = String.concat ""
(List.map (fun c -> render_to_html c env) children) in
ignore (env_bind local "children" (RawHTML rendered_children))
end;
render_to_html body local
let render_component comp args env =
match comp with
| Component c ->
let kwargs = Hashtbl.create 8 in
let children_exprs = ref [] in
let skip = ref false in
let len = List.length args in
List.iteri (fun idx arg ->
if !skip then skip := false
else match arg with
| Keyword k when idx + 1 < len ->
let v = Sx_ref.eval_expr (List.nth args (idx + 1)) (Env env) in
Hashtbl.replace kwargs k v;
skip := true
| _ ->
children_exprs := arg :: !children_exprs
) args;
let children = List.rev !children_exprs in
let local = env_merge c.c_closure env in
List.iter (fun p ->
let v = match Hashtbl.find_opt kwargs p with Some v -> v | None -> Nil in
ignore (env_bind local p v)
) c.c_params;
if c.c_has_children then begin
let rendered_children = String.concat ""
(List.map (fun c -> render_to_html c env) children) in
ignore (env_bind local "children" (RawHTML rendered_children))
end;
render_to_html c.c_body local
render_component_generic
~params:c.c_params ~has_children:c.c_has_children
~body:c.c_body ~closure:c.c_closure args env
| Island i ->
render_component_generic
~params:i.i_params ~has_children:i.i_has_children
~body:i.i_body ~closure:i.i_closure args env
| _ -> ""
let expand_macro (m : macro) args _env =
@@ -249,7 +258,7 @@ and render_list_to_html head args env =
(try
let v = env_get env name in
(match v with
| Component _ -> render_component v args env
| Component _ | Island _ -> render_component v args env
| Macro m ->
let expanded = expand_macro m args env in
do_render_to_html expanded env

View File

@@ -195,13 +195,59 @@ let _is_spread_prim a = _prim "spread?" [a]
let spread_attrs a = _prim "spread-attrs" [a]
let make_spread a = _prim "make-spread" [a]
(* Scope primitives — delegate to sx_ref.py's shared scope stacks *)
let sx_collect a b = prim_call "collect!" [a; b]
let sx_collected a = prim_call "collected" [a]
let sx_clear_collected a = prim_call "clear-collected!" [a]
let sx_emit a b = prim_call "emit!" [a; b]
let sx_emitted a = prim_call "emitted" [a]
let sx_context a b = prim_call "context" [a; b]
(* Scope stacks — thread-local stacks keyed by name string.
collect!/collected implement accumulator scopes.
emit!/emitted implement event emission scopes.
context reads the top of a named scope stack. *)
let _scope_stacks : (string, value list) Hashtbl.t = Hashtbl.create 8
let sx_collect name value =
let key = value_to_str name in
let stack = match Hashtbl.find_opt _scope_stacks key with
| Some s -> s | None -> [] in
(* Push value onto the top list of the stack *)
(match stack with
| List items :: rest ->
Hashtbl.replace _scope_stacks key (List (items @ [value]) :: rest)
| _ ->
Hashtbl.replace _scope_stacks key (List [value] :: stack));
Nil
let sx_collected name =
let key = value_to_str name in
match Hashtbl.find_opt _scope_stacks key with
| Some (List items :: _) -> List items
| _ -> List []
let sx_clear_collected name =
let key = value_to_str name in
(match Hashtbl.find_opt _scope_stacks key with
| Some (_ :: rest) -> Hashtbl.replace _scope_stacks key (List [] :: rest)
| _ -> ());
Nil
let sx_emit name value =
let key = value_to_str name in
let stack = match Hashtbl.find_opt _scope_stacks key with
| Some s -> s | None -> [] in
(match stack with
| List items :: rest ->
Hashtbl.replace _scope_stacks key (List (items @ [value]) :: rest)
| _ ->
Hashtbl.replace _scope_stacks key (List [value] :: stack));
Nil
let sx_emitted name =
let key = value_to_str name in
match Hashtbl.find_opt _scope_stacks key with
| Some (List items :: _) -> List items
| _ -> List []
let sx_context name default =
let key = value_to_str name in
match Hashtbl.find_opt _scope_stacks key with
| Some (v :: _) -> v
| _ -> default
(* Trampoline — forward-declared in sx_ref.ml, delegates to CEK eval_expr *)
(* This is a stub; the real trampoline is wired up in sx_ref.ml after eval_expr is defined *)
@@ -292,31 +338,99 @@ let dynamic_wind_call before body after _env =
result
(* Scope stack stubs — delegated to primitives when available *)
let scope_push name value = prim_call "collect!" [name; value]
let scope_pop _name = Nil
let provide_push name value = ignore name; ignore value; Nil
let provide_pop _name = Nil
let scope_push name value =
let key = value_to_str name in
let stack = match Hashtbl.find_opt _scope_stacks key with
| Some s -> s | None -> [] in
Hashtbl.replace _scope_stacks key (value :: stack);
Nil
(* Render mode stubs *)
let render_active_p () = Bool false
let render_expr _expr _env = Nil
let is_render_expr _expr = Bool false
let scope_pop name =
let key = value_to_str name in
(match Hashtbl.find_opt _scope_stacks key with
| Some (_ :: rest) -> Hashtbl.replace _scope_stacks key rest
| _ -> ());
Nil
let provide_push name value = scope_push name value
let provide_pop name = scope_pop name
(* Render mode — mutable refs so browser entry point can wire up the renderer *)
let _render_active_p_fn : (unit -> value) ref = ref (fun () -> Bool false)
let _render_expr_fn : (value -> value -> value) ref = ref (fun _expr _env -> Nil)
let _is_render_expr_fn : (value -> value) ref = ref (fun _expr -> Bool false)
let render_active_p () = !_render_active_p_fn ()
let render_expr expr env = !_render_expr_fn expr env
let is_render_expr expr = !_is_render_expr_fn expr
(* Signal accessors — handle both native Signal type and dict-based signals
from web/signals.sx which use {__signal: true, value: ..., subscribers: ..., deps: ...} *)
let is_dict_signal d = Hashtbl.mem d "__signal"
let signal_value s = match s with
| Signal sig' -> sig'.s_value
| Dict d when is_dict_signal d -> Sx_types.dict_get d "value"
| _ -> raise (Eval_error ("not a signal: " ^ Sx_types.type_of s))
let signal_set_value s v = match s with
| Signal sig' -> sig'.s_value <- v; v
| Dict d when is_dict_signal d -> Hashtbl.replace d "value" v; v
| _ -> raise (Eval_error "not a signal")
let signal_subscribers s = match s with
| Signal sig' -> List (List.map (fun _ -> Nil) sig'.s_subscribers)
| Dict d when is_dict_signal d -> Sx_types.dict_get d "subscribers"
| _ -> List []
(* These use Obj.magic to accept both SX values and OCaml closures.
The transpiler generates bare (fun () -> ...) for reactive subscribers
but signal_add_sub_b expects value. This is a known transpiler limitation. *)
let signal_add_sub_b s (f : _ ) = match s with
| Dict d when is_dict_signal d ->
let f_val : value = Obj.magic f in
let subs = match Sx_types.dict_get d "subscribers" with
| List l -> l | ListRef r -> !r | _ -> [] in
Hashtbl.replace d "subscribers" (List (subs @ [f_val])); Nil
| _ -> Nil
let signal_remove_sub_b s (f : _) = match s with
| Dict d when is_dict_signal d ->
let f_val : value = Obj.magic f in
let subs = match Sx_types.dict_get d "subscribers" with
| List l -> l | ListRef r -> !r | _ -> [] in
Hashtbl.replace d "subscribers" (List (List.filter (fun x -> x != f_val) subs)); Nil
| _ -> Nil
let signal_deps s = match s with
| Dict d when is_dict_signal d -> Sx_types.dict_get d "deps"
| _ -> List []
let signal_set_deps s deps = match s with
| Dict d when is_dict_signal d -> Hashtbl.replace d "deps" deps; Nil
| _ -> Nil
let notify_subscribers s = match s with
| Dict d when is_dict_signal d ->
let subs = match Sx_types.dict_get d "subscribers" with
| List l -> l | ListRef r -> !r | _ -> [] in
List.iter (fun sub ->
match sub with
| NativeFn (_, f) -> ignore (f [])
| Lambda _ -> ignore (Sx_types.env_bind (Sx_types.make_env ()) "_" Nil) (* TODO: call through CEK *)
| _ -> ()
) subs; Nil
| _ -> Nil
(* Signal accessors *)
let signal_value s = match s with Signal sig' -> sig'.s_value | _ -> raise (Eval_error "not a signal")
let signal_set_value s v = match s with Signal sig' -> sig'.s_value <- v; v | _ -> raise (Eval_error "not a signal")
let signal_subscribers s = match s with Signal sig' -> List (List.map (fun _ -> Nil) sig'.s_subscribers) | _ -> List []
let signal_add_sub_b _s _f = Nil
let signal_remove_sub_b _s _f = Nil
let signal_deps _s = List []
let signal_set_deps _s _d = Nil
let notify_subscribers _s = Nil
let flush_subscribers _s = Nil
let dispose_computed _s = Nil
(* Island scope stubs — accept OCaml functions from transpiled code *)
let with_island_scope _register_fn body_fn = body_fn ()
let register_in_scope _dispose_fn = Nil
(* Island scope stubs — accept OCaml functions from transpiled code.
Use Obj.magic for the same reason as signal_add_sub_b. *)
let with_island_scope (_register_fn : _) (body_fn : _) =
let body : unit -> value = Obj.magic body_fn in
body ()
let register_in_scope (_dispose_fn : _) = Nil
(* Component type annotation stub *)
let component_set_param_types_b _comp _types = Nil

View File

@@ -240,7 +240,10 @@ let is_component = function Component _ -> true | _ -> false
let is_island = function Island _ -> true | _ -> false
let is_macro = function Macro _ -> true | _ -> false
let is_thunk = function Thunk _ -> true | _ -> false
let is_signal = function Signal _ -> true | _ -> false
let is_signal = function
| Signal _ -> true
| Dict d -> Hashtbl.mem d "__signal"
| _ -> false
let is_callable = function
| Lambda _ | NativeFn _ | Continuation (_, _) -> true
@@ -287,26 +290,32 @@ let set_lambda_name l n = match l with
let component_name = function
| Component c -> String c.c_name
| Island i -> String i.i_name
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
let component_params = function
| Component c -> List (List.map (fun s -> String s) c.c_params)
| Island i -> List (List.map (fun s -> String s) i.i_params)
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
let component_body = function
| Component c -> c.c_body
| Island i -> i.i_body
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
let component_closure = function
| Component c -> Env c.c_closure
| Island i -> Env i.i_closure
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
let component_has_children = function
| Component c -> Bool c.c_has_children
| Island i -> Bool i.i_has_children
| v -> raise (Eval_error ("Expected component, got " ^ type_of v))
let component_affinity = function
| Component c -> String c.c_affinity
| Island _ -> String "client"
| _ -> String "auto"
let macro_params = function