IO registry: defio declares platform suspension points

Core SX has zero IO — platforms extend __io-registry via (defio name
:category :data/:code/:effect ...). The server web platform declares 44
operations in web/io.sx. batchable_helpers now derived from registry
(:batchable true) instead of hardcoded list. Startup validation warns if
bound IO ops lack registry entries. Browser gets empty registry, ready
for step 5 (IO suspension).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-02 23:21:48 +00:00
parent 17b6c872f2
commit ede05c26f5
16 changed files with 486 additions and 58 deletions

View File

@@ -608,6 +608,25 @@ let make_test_env () =
island
| _ -> Nil)));
(* defio — IO registry for platform suspension points *)
let io_registry = Hashtbl.create 64 in
ignore (Sx_types.env_bind env "__io-registry" (Dict io_registry));
ignore (Sx_ref.register_special_form (String "defio") (NativeFn ("defio", fun sf_args ->
let raw_args = match sf_args with
| [List a; Env _] | [ListRef { contents = a }; Env _] -> a
| _ -> [] in
match raw_args with
| String name :: rest ->
let entry = Hashtbl.create 8 in
let rec parse = function
| Keyword k :: v :: rest -> Hashtbl.replace entry k v; parse rest
| _ -> () in
parse rest;
Hashtbl.replace entry "name" (String name);
Hashtbl.replace io_registry name (Dict entry);
Dict entry
| _ -> Nil)));
(* --- Primitives for canonical.sx / content tests --- *)
bind "contains-char?" (fun args ->
match args with

View File

@@ -1948,7 +1948,27 @@ let http_setup_declarative_stubs env =
noop "defquery";
noop "defaction";
noop "defrelation";
noop "defstyle"
noop "defstyle";
(* IO registry — starts empty, platforms extend via defio.
defio is a special form that populates __io-registry with metadata
about suspension points (IO ops that require platform resolution). *)
let io_registry = Hashtbl.create 64 in
ignore (env_bind env "__io-registry" (Dict io_registry));
ignore (Sx_ref.register_special_form (String "defio") (NativeFn ("defio", fun sf_args ->
let raw_args = match sf_args with
| [List a; Env _] | [ListRef { contents = a }; Env _] -> a
| _ -> [] in
match raw_args with
| String name :: rest ->
let entry = Hashtbl.create 8 in
let rec parse = function
| Keyword k :: v :: rest -> Hashtbl.replace entry k v; parse rest
| _ -> () in
parse rest;
Hashtbl.replace entry "name" (String name);
Hashtbl.replace io_registry name (Dict entry);
Dict entry
| _ -> Nil)))
let http_setup_platform_constructors env =
(* Platform constructor functions expected by evaluator.sx.
@@ -2365,6 +2385,7 @@ let http_mode port =
spec_base ^ "/signals.sx";
lib_base ^ "/compiler.sx";
web_base ^ "/adapter-html.sx"; web_base ^ "/adapter-sx.sx";
web_base ^ "/io.sx";
web_base ^ "/web-forms.sx"; web_base ^ "/engine.sx";
web_base ^ "/request-handler.sx";
web_base ^ "/page-helpers.sx";
@@ -2404,11 +2425,35 @@ let http_mode port =
load_dir sx_sx;
let t1 = Unix.gettimeofday () in
Printf.eprintf "[sx-http] All files loaded in %.3fs\n%!" (t1 -. t0);
(* Derive batchable_helpers from __io-registry *)
(try match env_get env "__io-registry" with
| Dict registry ->
let batchable = Hashtbl.fold (fun name entry acc ->
match entry with
| Dict d -> (match Hashtbl.find_opt d "batchable" with
| Some (Bool true) -> name :: acc | _ -> acc)
| _ -> acc) registry [] in
if batchable <> [] then batchable_helpers := batchable;
Printf.eprintf "[sx-http] IO registry: %d ops, %d batchable\n%!"
(Hashtbl.length registry) (List.length batchable);
(* Validate: warn if bound IO ops are not declared *)
let expected = ["query"; "action"; "request-arg"; "request-method";
"ctx"; "helper"; "json-encode"; "into"; "sleep";
"set-response-status"; "set-response-header"] in
List.iter (fun name ->
if not (Hashtbl.mem registry name) then
Printf.eprintf "[sx-http] WARNING: IO '%s' bound but not in registry\n%!" name
) expected
| _ -> ()
with _ -> ());
(* Extract app config from __app-config dict *)
(try match env_get env "__app-config" with
| Dict d ->
_app_config := Some d;
batchable_helpers := get_app_list "batchable-helpers" ["highlight"; "component-source"];
(* App config can add extra batchable helpers on top of registry *)
let extra = get_app_list "batchable-helpers" [] in
if extra <> [] then
batchable_helpers := List.sort_uniq String.compare (!batchable_helpers @ extra);
Printf.eprintf "[sx-http] App config loaded: title=%s prefix=%s\n%!"
(get_app_str "title" "?") (get_app_str "path-prefix" "?")
| _ -> Printf.eprintf "[sx-http] WARNING: __app-config is not a dict\n%!"

View File

@@ -671,6 +671,27 @@ let () =
bind "define-page-helper" (fun _ -> Nil);
(* IO registry — starts empty in browser. Platforms extend via defio.
Browser has zero suspension points initially; future browser IO
(lazy module loads, fetch-request) will add entries here. *)
let io_registry = Hashtbl.create 16 in
ignore (env_bind global_env "__io-registry" (Dict io_registry));
ignore (Sx_ref.register_special_form (String "defio") (NativeFn ("defio", fun sf_args ->
let raw_args = match sf_args with
| [List a; Env _] | [ListRef { contents = a }; Env _] -> a
| _ -> [] in
match raw_args with
| String name :: rest ->
let entry = Hashtbl.create 8 in
let rec parse = function
| Keyword k :: v :: rest -> Hashtbl.replace entry k v; parse rest
| _ -> () in
parse rest;
Hashtbl.replace entry "name" (String name);
Hashtbl.replace io_registry name (Dict entry);
Dict entry
| _ -> Nil)));
(* --- Render --- *)
Sx_render.setup_render_env global_env;
bind "set-render-active!" (fun _ -> Nil);