review quick-wins: JIT gate, crash guards, crit-2 signal-return, regen repair

Server (sx_server.ml):
- HTTP mode: JIT hook now opt-in via SX_SERVING_JIT, matching epoch mode
  (was unconditional — live serving-JIT miscompiles J1/J2/J3 de-risked)
- command channel: malformed/non-ASCII line returns an error response
  instead of killing the shared process (C1/C1b)
- response cache: soft error pages no longer cached (S4);
  http_render_page returns (html, is_error)

Kernel spec + regen:
- crit-2: signal-return frame stored the saved kont under :f but the reader
  looked up "saved-kont" — handler value became the whole program's result
  and the covering test passed vacuously. Fixed; raise-continuable now also
  resumes at the raise site (rest-k, not unwound-k), mirroring signal-condition
- quasiquote: R7RS longhand unquote-splicing aliased to splice-unquote
  (used to serialize literally — silent zero-splice)
- guard: re-raise sentinel gensym'd per execution (was forgeable by any
  (list '__guard-reraise__ x) value)
- do: IIFE-head form no longer misparses as a Scheme do-loop
- render: area/base/embed/param/track added to HTML_TAGS (were void-only
  and rendered as Undefined symbol)
- REGEN REPAIR: checked-in sx_ref.ml carried hand-written additions that
  every regeneration silently lost (let-values/define-values/delay/
  delay-force registrations, AdtValue define-type) plus 5 regen blockers
  (arrow-name mangling, 3-arg get, &rest defines, HO-position helper refs,
  transpiler prim-table gaps). Moved into bootstrap.py FIXUPS/skips and the
  transpiler prim table — regen is now reproducible, compiles, and tests
  at baseline (CI Dockerfile.test steps 3-4 could not previously have
  produced a compiling kernel)

Primitives:
- contains?: dict key-check arm per its spec doc
- expt: promotes to float on int63 overflow ((expt 2 100) returned 0)
- mcp_tree parity with sx_primitives: get (Integer indices + 3-arg default),
  split (literal substring, was char-class — the historical gotcha lived
  here), empty? on ""/{}, contains?, equal?, keyword-name, char-code
  (Integer), parse-number (Integer-aware)

Python/docs:
- shared/sx/boundary.py: dead validation now logs a one-time WARNING instead
  of silently no-oping (full revival gated: tier-1 declarations deleted and
  SX_BOUNDARY_STRICT=1 is live in production compose)
- CLAUDE.md: canonical reference now points at spec/*.sx; island authoring
  rules corrected (let IS sequential, bodies ARE implicit begin)

Verification: full suite 5762 passed / 274 failed — fail set byte-identical
to the pre-change baseline (273 in-progress hs-* + pre-existing r7rs radix
shadow). All repros verified fixed on both the native binary and the rebuilt
WASM browser kernel. Review findings: /tmp/sx-review/*.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 13:49:43 +00:00
parent 071c2f9a8a
commit dc7aa709bd
14 changed files with 3445 additions and 3213 deletions

View File

@@ -173,15 +173,16 @@ artdag/
## SX Language — Canonical Reference ## SX Language — Canonical Reference
The SX language is defined by a self-hosting specification in `shared/sx/ref/`. **Read these files for authoritative SX semantics** — they supersede any implementation detail in `sx.js` or Python evaluators. The SX language is defined by a self-hosting specification in `spec/`. **Read these files for authoritative SX semantics.** (The former `shared/sx/ref/*.sx` copies were deleted — only `BOUNDARY.md` remains there.) The spec is transpiled into the OCaml kernel (`hosts/ocaml/lib/sx_ref.ml`, generated by `hosts/ocaml/bootstrap.py` — never edit the generated file; edit the spec and regenerate).
### Specification files ### Specification files
- **`shared/sx/ref/eval.sx`** — Core evaluator: types, trampoline (TCO), `eval-expr` dispatch, special forms (`if`, `when`, `cond`, `case`, `let`, `and`, `or`, `lambda`, `define`, `defcomp`, `defmacro`, `quasiquote`), higher-order forms (`map`, `filter`, `reduce`, `some`, `every?`, `for-each`), macro expansion, function/lambda/component calling. - **`spec/evaluator.sx`** — The CEK-machine evaluator: frames, TCO trampoline, `step`/dispatch, special forms (`if`, `when`, `cond`, `case`, `let`, `and`, `or`, `lambda`, `define`, `defcomp`, `defmacro`, `quasiquote`, `guard`, `handler-bind`), higher-order forms (`map`, `filter`, `reduce`, `some`, `every?`, `for-each`), continuations (`call/cc`, `reset`/`shift`), macro expansion, strict-typing machinery, function/lambda/component calling.
- **`shared/sx/ref/parser.sx`** — Tokenizer and parser: grammar, string escapes, dict literals `{:key val}`, quote sugar (`` ` ``, `,`, `,@`), serializer. - **`spec/parser.sx`** — Tokenizer and parser: grammar, string escapes, dict literals `{:key val}`, quote sugar (`` ` ``, `,`, `,@`), serializer.
- **`shared/sx/ref/primitives.sx`** — All ~80 built-in pure functions: arithmetic, comparison, predicates, string ops, collection ops, dict ops, format helpers, CSSX style primitives. - **`spec/primitives.sx`** — Declarative specs for the built-in pure functions: arithmetic, comparison, predicates, string ops, collection ops, dict ops, format helpers, CSSX style primitives.
- **`shared/sx/ref/render.sx`** — Three rendering modes: `render-to-html` (server HTML), `render-to-sx`/`aser` (SX wire format for client), `render-to-dom` (browser). HTML tag registry, void elements, boolean attrs. - **`spec/render.sx`** — Rendering registries + escaping (HTML tag registry, void elements, boolean attrs); the mode dispatch (`render-to-html`, `aser`, `render-to-dom`) lives in `web/adapter-*.sx`.
- **`shared/sx/ref/bootstrap_js.py`** — Transpiler: reads the `.sx` spec files and emits `sx-ref.js`. - **`spec/special-forms.sx`, `spec/eval-rules.sx`** — Form specs and machine-readable evaluation rules.
- **`spec/signals.sx`, `spec/coroutines.sx`, `spec/canonical.sx`, `spec/stdlib.sx`** — Reactive signals, coroutines, canonical serialization, stdlib.
- **`spec/harness.sx`** — Test harness: mock IO platform for testing components. Sessions, IO interception, log queries, assertions (`assert-io-called`, `assert-io-count`, `assert-io-args`, `assert-no-io`, `assert-state`). Extensible — new platforms add entries to the platform dict. Loaded automatically by test runners. - **`spec/harness.sx`** — Test harness: mock IO platform for testing components. Sessions, IO interception, log queries, assertions (`assert-io-called`, `assert-io-count`, `assert-io-args`, `assert-no-io`, `assert-state`). Extensible — new platforms add entries to the platform dict. Loaded automatically by test runners.
- **`spec/tests/test-harness.sx`** — Tests for the harness itself (15 tests). - **`spec/tests/test-harness.sx`** — Tests for the harness itself (15 tests).
@@ -310,7 +311,7 @@ cd artdag/l1 && mypy app/types.py app/routers/recipes.py tests/
### SX Rendering Pipeline ### SX Rendering Pipeline
The SX system renders component trees defined in s-expressions. Canonical semantics are in `shared/sx/ref/` (see "SX Language" section above). The same AST can be evaluated in different modes depending on where the server/client rendering boundary is drawn: The SX system renders component trees defined in s-expressions. Canonical semantics are in `spec/` (see "SX Language" section above). The same AST can be evaluated in different modes depending on where the server/client rendering boundary is drawn:
- `render_to_html(name, **kw)` — server-side, produces HTML. Maps to `render-to-html` in the spec. - `render_to_html(name, **kw)` — server-side, produces HTML. Maps to `render-to-html` in the spec.
- `render_to_sx(name, **kw)` — server-side, produces SX wire format. Maps to `aser` in the spec. Component calls stay **unexpanded**. - `render_to_sx(name, **kw)` — server-side, produces SX wire format. Maps to `aser` in the spec. Component calls stay **unexpanded**.
@@ -450,7 +451,7 @@ printf '(epoch 1)\n(prim-check "my-compiled-fn")\n' | sx_server.exe
Key patterns discovered from the reactive runtime demos (see `sx/sx/reactive-runtime.sx`): Key patterns discovered from the reactive runtime demos (see `sx/sx/reactive-runtime.sx`):
1. **Multi-expression bodies need `(do ...)`**`fn`, `let`, and `when` bodies evaluate only the last expression. Wrap multiples in `(do expr1 expr2 expr3)`. 1. **Multi-expression bodies sequence (implicit begin)**`fn`, `let`, and `when` bodies evaluate every expression and return the last (verified on both the CEK and the VM; the old "only the last expression runs" rule described a deleted evaluator). `(do ...)` still works and is fine for clarity.
2. **`let` is parallel, not sequential** — bindings in the same `let` can't reference each other. Use nested `let` blocks when functions need to reference signals defined earlier. 2. **`let` is SEQUENTIAL (`let*` semantics)** — later bindings see earlier ones (`(let ((a 1) (b a)) b)` → 1; tested intent, both engines). `cond`, however, has a mode-detection footgun with multi-expression clause bodies — use flat pairs with explicit `(do ...)` results.
3. **Reactive text needs `(deref (computed ...))`** — bare `(len (deref items))` is NOT reactive. Wrap in `(deref (computed (fn () (len (deref items)))))`. 3. **Reactive text needs `(deref (computed ...))`** — bare `(len (deref items))` is NOT reactive. Wrap in `(deref (computed (fn () (len (deref items)))))`.
4. **Effects go in inner `let`** — signals in outer `let`, functions and effects in inner `let`. The OCaml SSR evaluator can't resolve outer `let` bindings from same-`let` lambdas. 4. **Effects go in inner `let`** — signals in outer `let`, functions and effects in inner `let`. The OCaml SSR evaluator can't resolve outer `let` bindings from same-`let` lambdas.

View File

@@ -375,18 +375,34 @@ let setup_env () =
| [String s] when String.length s = 1 -> | [String s] when String.length s = 1 ->
let c = s.[0] in Bool ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) let c = s.[0] in Bool ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
| _ -> Bool false); | _ -> Bool false);
(* Parity with sx_primitives: Integer, not float — make-char requires an
Integer codepoint, so the float version broke #\a char literals here. *)
bind "char-code" (fun args -> match args with bind "char-code" (fun args -> match args with
| [String s] when String.length s > 0 -> Number (float_of_int (Char.code s.[0])) | [String s] when String.length s > 0 -> Integer (Char.code s.[0])
| _ -> Number 0.0); | _ -> Integer 0);
bind "code-char" (fun args -> match args with bind "code-char" (fun args -> match args with
| [Number n] -> String (String.make 1 (Char.chr (int_of_float n))) | [Number n] -> String (String.make 1 (Char.chr (int_of_float n)))
| _ -> String ""); | _ -> String "");
(* Parity with sx_primitives: Integer-aware — the float-only version
shadowed the kernel one and broke rationals through the guest parser. *)
bind "parse-number" (fun args -> match args with bind "parse-number" (fun args -> match args with
| [String s] -> (try Number (float_of_string s) with _ -> Nil) | [String s] ->
let has_dec = String.contains s '.' in
let has_exp = String.contains s 'e' || String.contains s 'E' in
if has_dec || has_exp then
(try Number (float_of_string s) with Failure _ -> Nil)
else
(match int_of_string_opt s with
| Some n -> Integer n
| None -> (try Number (float_of_string s) with Failure _ -> Nil))
| _ -> Nil); | _ -> Nil);
bind "identical?" (fun args -> match args with bind "identical?" (fun args -> match args with
| [a; b] -> Bool (a == b) | [a; b] -> Bool (a == b)
| _ -> Bool false); | _ -> Bool false);
(* Parity with sx_server: equal? exists in the real runtime env *)
bind "equal?" (fun args -> match args with
| [a; b] -> Bool (a = b)
| _ -> raise (Eval_error "equal?: expected 2 args"));
(* Character classification for SX parser.sx *) (* Character classification for SX parser.sx *)
bind "ident-start?" (fun args -> match args with bind "ident-start?" (fun args -> match args with
| [String s] when String.length s = 1 -> | [String s] when String.length s = 1 ->
@@ -427,8 +443,10 @@ let setup_env () =
(* Runtime functions needed by tree-tools *) (* Runtime functions needed by tree-tools *)
bind "symbol-name" (fun args -> match args with bind "symbol-name" (fun args -> match args with
| [Symbol s] -> String s | _ -> String ""); | [Symbol s] -> String s | _ -> String "");
(* Parity with sx_server: error on non-keyword (was a silent "") *)
bind "keyword-name" (fun args -> match args with bind "keyword-name" (fun args -> match args with
| [Keyword k] -> String k | _ -> String ""); | [Keyword k] -> String k
| _ -> raise (Eval_error "keyword-name: expected keyword"));
bind "make-symbol" (fun args -> match args with bind "make-symbol" (fun args -> match args with
| [String s] -> Symbol s | _ -> Nil); | [String s] -> Symbol s | _ -> Nil);
(* Environment operations needed by harness *) (* Environment operations needed by harness *)
@@ -443,12 +461,30 @@ let setup_env () =
| _ -> Bool false); | _ -> Bool false);
bind "make-env" (fun _args -> Env (make_env ())); bind "make-env" (fun _args -> Env (make_env ()));
bind "keys" (fun args -> match args with bind "keys" (fun args -> match args with
| [Dict d] -> List (Hashtbl.fold (fun k _ acc -> String k :: acc) d []) | [Dict d] -> List (dict_keys d)
| _ -> List []); | _ -> List []);
bind "get" (fun args -> match args with (* Parity with sx_primitives: Integer indices (literals parse as Integer,
| [Dict d; String k] -> (match Hashtbl.find_opt d k with Some v -> v | None -> Nil) so the Number-only arm returned nil for every (get lst 1)), Symbol keys,
| [Dict d; Keyword k] -> (match Hashtbl.find_opt d k with Some v -> v | None -> Nil) and the 3-arg default form — default only when the key is ABSENT. *)
| [List items; Number n] -> (let i = int_of_float n in if i >= 0 && i < List.length items then List.nth items i else Nil) bind "get" (fun args ->
let dict_key = function
| String k | Keyword k | Symbol k -> Some k
| _ -> None in
let list_idx = function
| Integer i -> Some i
| Number n -> Some (int_of_float n)
| _ -> None in
match args with
| [Dict d; key] | [Dict d; key; _] when dict_key key <> None && Hashtbl.mem d (Option.get (dict_key key)) ->
Hashtbl.find d (Option.get (dict_key key))
| [Dict _; _] -> Nil
| [Dict _; _; default] -> default
| ([List items; idx] | [ListRef { contents = items }; idx]) when list_idx idx <> None ->
(let i = Option.get (list_idx idx) in
if i >= 0 && i < List.length items then List.nth items i else Nil)
| ([List items; idx; default] | [ListRef { contents = items }; idx; default]) when list_idx idx <> None ->
(let i = Option.get (list_idx idx) in
if i >= 0 && i < List.length items then List.nth items i else default)
| _ -> Nil); | _ -> Nil);
bind "dict-set!" (fun args -> match args with bind "dict-set!" (fun args -> match args with
| [Dict d; String k; v] -> Hashtbl.replace d k v; v | [Dict d; String k; v] -> Hashtbl.replace d k v; v
@@ -478,8 +514,12 @@ let setup_env () =
| [Number _] -> Bool true | _ -> Bool false); | [Number _] -> Bool true | _ -> Bool false);
bind "callable?" (fun args -> match args with bind "callable?" (fun args -> match args with
| [NativeFn _ | Lambda _ | Component _ | Island _ | VmClosure _] -> Bool true | _ -> Bool false); | [NativeFn _ | Lambda _ | Component _ | Island _ | VmClosure _] -> Bool true | _ -> Bool false);
(* Parity with sx_primitives: empty string and empty dict are empty
(test-primitives.sx:89 asserts this; the old arms said false). *)
bind "empty?" (fun args -> match args with bind "empty?" (fun args -> match args with
| [List []] | [ListRef { contents = [] }] -> Bool true | [List []] | [ListRef { contents = [] }] -> Bool true
| [String s] -> Bool (String.length s = 0)
| [Dict d] -> Bool (Hashtbl.length d = 0)
| [Nil] -> Bool true | _ -> Bool false); | [Nil] -> Bool true | _ -> Bool false);
bind "contains?" (fun args -> match args with bind "contains?" (fun args -> match args with
| [String s; String sub] -> | [String s; String sub] ->
@@ -490,6 +530,11 @@ let setup_env () =
in Bool (String.length sub = 0 || find 0) in Bool (String.length sub = 0 || find 0)
| [List l; v] | [ListRef { contents = l }; v] -> | [List l; v] | [ListRef { contents = l }; v] ->
Bool (List.exists (fun x -> x = v) l) Bool (List.exists (fun x -> x = v) l)
| [Dict d; key] ->
(* Dicts: key check — matches the sx_primitives fix *)
(match key with
| String k | Keyword k | Symbol k -> Bool (Hashtbl.mem d k)
| _ -> Bool false)
| _ -> Bool false); | _ -> Bool false);
bind "starts-with?" (fun args -> match args with bind "starts-with?" (fun args -> match args with
| [String s; String prefix] -> | [String s; String prefix] ->
@@ -560,9 +605,28 @@ let setup_env () =
| _ -> Nil); | _ -> Nil);
bind "trim" (fun args -> match args with bind "trim" (fun args -> match args with
| [String s] -> String (String.trim s) | _ -> String ""); | [String s] -> String (String.trim s) | _ -> String "");
(* Parity with sx_primitives: literal SUBSTRING separator (the old
split_on_char d.[0] was char-class semantics — the historical
"split is char-class" gotcha lived HERE, not in the kernel), keeps
empties, empty separator → chars, and no crash on "". *)
bind "split" (fun args -> match args with bind "split" (fun args -> match args with
| [String s; String d] -> | [String s; String sep] ->
List (List.map (fun p -> String p) (String.split_on_char d.[0] s)) let sl = String.length s and pl = String.length sep in
if pl = 0 then
List (List.init sl (fun i -> String (String.make 1 s.[i])))
else if pl = 1 then
List (List.map (fun p -> String p) (String.split_on_char sep.[0] s))
else begin
let parts = ref [] and start = ref 0 and i = ref 0 in
while !i <= sl - pl do
if String.sub s !i pl = sep then begin
parts := String.sub s !start (!i - !start) :: !parts;
start := !i + pl; i := !i + pl
end else incr i
done;
parts := String.sub s !start (sl - !start) :: !parts;
List (List.map (fun p -> String p) (List.rev !parts))
end
| _ -> List []); | _ -> List []);
(* sx-parse — use the native OCaml parser for bootstrapping *) (* sx-parse — use the native OCaml parser for bootstrapping *)
bind "sx-parse" (fun args -> match args with bind "sx-parse" (fun args -> match args with

View File

@@ -1151,7 +1151,7 @@ let make_test_env () =
bind "promise?" (fun args -> bind "promise?" (fun args ->
match args with match args with
| [v] -> Bool (Sx_ref.is_promise v) | [v] -> Sx_ref.promise_p v
| _ -> Bool false); | _ -> Bool false);
bind "make-promise" (fun args -> bind "make-promise" (fun args ->
@@ -1166,7 +1166,7 @@ let make_test_env () =
bind "force" (fun args -> bind "force" (fun args ->
match args with match args with
| [p] -> Sx_ref.force_promise p | [p] -> Sx_ref.force p
| _ -> Nil); | _ -> Nil);
env env

View File

@@ -2631,8 +2631,12 @@ let http_render_page env path headers =
Printf.eprintf "[http] route error for %s: %s\n%!" path (Printexc.to_string e); Printf.eprintf "[http] route error for %s: %s\n%!" path (Printexc.to_string e);
Nil Nil
in in
(* Build an error page AST that keeps the layout intact *) (* Build an error page AST that keeps the layout intact.
Sets is_error_page so callers can avoid caching soft error pages —
a transient routing failure must not be served from cache until restart. *)
let is_error_page = ref false in
let error_page_ast msg = let error_page_ast msg =
is_error_page := true;
List [Symbol "div"; Keyword "class"; String "p-8 max-w-2xl mx-auto"; List [Symbol "div"; Keyword "class"; String "p-8 max-w-2xl mx-auto";
List [Symbol "h2"; Keyword "class"; String "text-xl font-semibold text-rose-600 mb-4"; List [Symbol "h2"; Keyword "class"; String "text-xl font-semibold text-rose-600 mb-4";
String "Page Error"]; String "Page Error"];
@@ -2672,7 +2676,7 @@ let http_render_page env path headers =
| String s | SxExpr s -> s | _ -> serialize_value body_result in | String s | SxExpr s -> s | _ -> serialize_value body_result in
let t1 = Unix.gettimeofday () in let t1 = Unix.gettimeofday () in
Printf.eprintf "[sx-http] %s (SX) aser=%.3fs body=%d\n%!" path (t1 -. t0) (String.length body_str); Printf.eprintf "[sx-http] %s (SX) aser=%.3fs body=%d\n%!" path (t1 -. t0) (String.length body_str);
Some body_str Some (body_str, !is_error_page)
end else begin end else begin
(* Full page: aser → SSR → shell *) (* Full page: aser → SSR → shell *)
let outer_layout = get_app_str "outer-layout" "~shared:layout/app-body" in let outer_layout = get_app_str "outer-layout" "~shared:layout/app-body" in
@@ -2727,7 +2731,7 @@ let http_render_page env path headers =
let t4 = Unix.gettimeofday () in let t4 = Unix.gettimeofday () in
Printf.eprintf "[sx-http] %s route=%.3fs aser=%.3fs ssr=%.3fs shell=%.3fs total=%.3fs html=%d\n%!" Printf.eprintf "[sx-http] %s route=%.3fs aser=%.3fs ssr=%.3fs shell=%.3fs total=%.3fs html=%d\n%!"
path (t1 -. t0) (t2 -. t1) (t3 -. t2) (t4 -. t3) (t4 -. t0) (String.length html); path (t1 -. t0) (t2 -. t1) (t3 -. t2) (t4 -. t3) (t4 -. t0) (String.length html);
Some html Some (html, !is_error_page)
end end
end end
@@ -4159,8 +4163,18 @@ let http_mode port =
http_inject_shell_statics env static_dir sx_sxc; http_inject_shell_statics env static_dir sx_sxc;
(* Init shared VM globals AFTER all files loaded + shell statics injected. (* Init shared VM globals AFTER all files loaded + shell statics injected.
The env_bind hook keeps it in sync with any future bindings. *) The env_bind hook keeps it in sync with any future bindings. *)
(* Enable lazy JIT — compile lambdas to bytecode on first call *) (* Lazy JIT is OPT-IN via SX_SERVING_JIT=1, matching the epoch serving mode.
register_jit_hook env; The serving JIT has confirmed miscompiles (`->` in argument position
evaluates steps once per remaining step and leaves stack residue; any
VM exception re-runs the whole call on the CEK, double-applying side
effects; user-macro call args are evaluated eagerly before fallback).
Until those are fixed, HTTP rendering runs on the CEK by default —
the response cache carries the hot paths. *)
(match Sys.getenv_opt "SX_SERVING_JIT" with
| Some ("1" | "true" | "yes" | "on") ->
register_jit_hook env
| _ ->
Printf.eprintf "[sx-http] serving JIT disabled (opt in with SX_SERVING_JIT=1)\n%!");
(* Install global IO resolver so perform works inside aser/eval_expr. (* Install global IO resolver so perform works inside aser/eval_expr.
This lets components call measure-text during server-side rendering. *) This lets components call measure-text during server-side rendering. *)
Sx_types._cek_io_resolver := Some (fun request _state -> Sx_types._cek_io_resolver := Some (fun request _state ->
@@ -4207,7 +4221,9 @@ let http_mode port =
let cache_response path = let cache_response path =
match http_render_page env path [] with match http_render_page env path [] with
| Some html -> | Some (_, true) ->
Printf.eprintf "[cache] %s → error page, not cached\n%!" path
| Some (html, false) ->
let resp = http_response html in let resp = http_response html in
Hashtbl.replace response_cache path resp; Hashtbl.replace response_cache path resp;
Printf.eprintf "[cache] %s → %d bytes\n%!" path (String.length html) Printf.eprintf "[cache] %s → %d bytes\n%!" path (String.length html)
@@ -4297,7 +4313,7 @@ let http_mode port =
let response = let response =
try try
match http_render_page env path headers with match http_render_page env path headers with
| Some body -> | Some (body, is_err) ->
(* htmx requests get HTML; SX requests get SX wire format *) (* htmx requests get HTML; SX requests get SX wire format *)
let final_body = if is_htmx then let final_body = if is_htmx then
(try (try
@@ -4309,7 +4325,7 @@ let http_mode port =
let ct = if is_ajax && not is_htmx then "text/sx; charset=utf-8" let ct = if is_ajax && not is_htmx then "text/sx; charset=utf-8"
else "text/html; charset=utf-8" in else "text/html; charset=utf-8" in
let resp = http_response ~content_type:ct final_body in let resp = http_response ~content_type:ct final_body in
Hashtbl.replace response_cache cache_key resp; if not is_err then Hashtbl.replace response_cache cache_key resp;
resp resp
| None -> http_response ~status:404 "<h1>Not Found</h1>" | None -> http_response ~status:404 "<h1>Not Found</h1>"
with e -> with e ->
@@ -4663,7 +4679,7 @@ let http_mode port =
String.lowercase_ascii k = "hx-request") headers in String.lowercase_ascii k = "hx-request") headers in
let response = let response =
try match http_render_page env path headers with try match http_render_page env path headers with
| Some body -> | Some (body, is_err) ->
let final_body = if is_htmx then let final_body = if is_htmx then
(try (try
let exprs = Sx_parser.parse_all body in let exprs = Sx_parser.parse_all body in
@@ -4674,7 +4690,7 @@ let http_mode port =
let ct = if is_htmx then "text/html; charset=utf-8" let ct = if is_htmx then "text/html; charset=utf-8"
else "text/sx; charset=utf-8" in else "text/sx; charset=utf-8" in
let resp = http_response ~content_type:ct final_body in let resp = http_response ~content_type:ct final_body in
if not is_htmx then Hashtbl.replace response_cache cache_key resp; if not is_htmx && not is_err then Hashtbl.replace response_cache cache_key resp;
resp resp
| None -> http_response ~status:404 | None -> http_response ~status:404
"(div :class \"p-8\" (h2 :class \"text-rose-600 font-semibold\" \"Page not found\") (p :class \"text-stone-500\" \"No route matched this path\"))" "(div :class \"p-8\" (h2 :class \"text-rose-600 font-semibold\" \"Page not found\") (p :class \"text-stone-500\" \"No route matched this path\"))"
@@ -4690,7 +4706,7 @@ let http_mode port =
Don't cache: response varies by cookie value. *) Don't cache: response varies by cookie value. *)
let response = let response =
try match http_render_page env path [] with try match http_render_page env path [] with
| Some body -> http_response body | Some (body, _) -> http_response body
| None -> http_response ~status:404 "<h1>Not Found</h1>" | None -> http_response ~status:404 "<h1>Not Found</h1>"
with e -> with e ->
Printf.eprintf "[render] Cookie render error for %s: %s\n%!" path (Printexc.to_string e); Printf.eprintf "[render] Cookie render error for %s: %s\n%!" path (Printexc.to_string e);
@@ -4947,7 +4963,11 @@ let site_mode () =
let line = String.trim line in let line = String.trim line in
if line = "" then () if line = "" then ()
else begin else begin
let exprs = Sx_parser.parse_all line in (* A malformed line must never kill the shared command channel:
report it as an error response and keep serving. *)
match (try Ok (Sx_parser.parse_all line) with e -> Error e) with
| Error e -> send_error ("Malformed command line: " ^ Printexc.to_string e)
| Ok exprs ->
match exprs with match exprs with
| [List [Symbol "epoch"; Number n]] -> | [List [Symbol "epoch"; Number n]] ->
current_epoch := int_of_float n current_epoch := int_of_float n
@@ -4956,7 +4976,7 @@ let site_mode () =
(* render-page: full SSR pipeline — URL → complete HTML *) (* render-page: full SSR pipeline — URL → complete HTML *)
| [List [Symbol "render-page"; String path]] -> | [List [Symbol "render-page"; String path]] ->
(try match http_render_page env path [] with (try match http_render_page env path [] with
| Some html -> send_ok_blob html | Some (html, _) -> send_ok_blob html
| None -> send_error ("render-page: no route for " ^ path) | None -> send_error ("render-page: no route for " ^ path)
with e -> send_error ("render-page: " ^ Printexc.to_string e)) with e -> send_error ("render-page: " ^ Printexc.to_string e))
(* nav-urls: flat list of (href label) from nav tree *) (* nav-urls: flat list of (href label) from nav tree *)
@@ -5045,7 +5065,11 @@ let () =
Printf.eprintf "[sx-server] discarding stale io-response (%d chars)\n%!" Printf.eprintf "[sx-server] discarding stale io-response (%d chars)\n%!"
(String.length line) (String.length line)
else begin else begin
let exprs = Sx_parser.parse_all line in (* A malformed line must never kill the shared command channel:
report it as an error response and keep serving. *)
match (try Ok (Sx_parser.parse_all line) with e -> Error e) with
| Error e -> send_error ("Malformed command line: " ^ Printexc.to_string e)
| Ok exprs ->
match exprs with match exprs with
(* Epoch marker: (epoch N) — set current epoch, read next command *) (* Epoch marker: (epoch N) — set current epoch, read next command *)
| [List [Symbol "epoch"; Number n]] -> | [List [Symbol "epoch"; Number n]] ->

View File

@@ -156,8 +156,7 @@ let sf_define_type args env_val =
(match pargs with (match pargs with
| [v] -> | [v] ->
(match v with (match v with
| Dict d -> Bool (Hashtbl.mem d "_adt" && | AdtValue a -> Bool (a.av_type = type_name)
(match Hashtbl.find_opt d "_type" with Some (String t) -> t = type_name | _ -> false))
| _ -> Bool false) | _ -> Bool false)
| _ -> Bool false))); | _ -> Bool false)));
List.iter (fun spec -> List.iter (fun spec ->
@@ -171,21 +170,18 @@ let sf_define_type args env_val =
if List.length ctor_args <> arity then if List.length ctor_args <> arity then
raise (Eval_error (Printf.sprintf "%s: expected %d args, got %d" raise (Eval_error (Printf.sprintf "%s: expected %d args, got %d"
cn arity (List.length ctor_args))) cn arity (List.length ctor_args)))
else begin else
let d = Hashtbl.create 4 in AdtValue {
Hashtbl.replace d "_adt" (Bool true); av_type = type_name;
Hashtbl.replace d "_type" (String type_name); av_ctor = cn;
Hashtbl.replace d "_ctor" (String cn); av_fields = Array.of_list ctor_args;
Hashtbl.replace d "_fields" (List ctor_args); }));
Dict d
end));
env_bind_v (cn ^ "?") env_bind_v (cn ^ "?")
(NativeFn (cn ^ "?", fun pargs -> (NativeFn (cn ^ "?", fun pargs ->
(match pargs with (match pargs with
| [v] -> | [v] ->
(match v with (match v with
| Dict d -> Bool (Hashtbl.mem d "_adt" && | AdtValue a -> Bool (a.av_ctor = cn)
(match Hashtbl.find_opt d "_ctor" with Some (String c) -> c = cn | _ -> false))
| _ -> Bool false) | _ -> Bool false)
| _ -> Bool false))); | _ -> Bool false)));
List.iteri (fun idx fname -> List.iteri (fun idx fname ->
@@ -194,28 +190,53 @@ let sf_define_type args env_val =
(match pargs with (match pargs with
| [v] -> | [v] ->
(match v with (match v with
| Dict d -> | AdtValue a ->
(match Hashtbl.find_opt d "_fields" with if idx < Array.length a.av_fields then a.av_fields.(idx)
| Some (List fs) ->
if idx < List.length fs then List.nth fs idx
else raise (Eval_error (cn ^ "-" ^ fname ^ ": index out of bounds")) else raise (Eval_error (cn ^ "-" ^ fname ^ ": index out of bounds"))
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not an ADT"))) | _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not an ADT")))
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": not a dict")))
| _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": expected 1 arg"))))) | _ -> raise (Eval_error (cn ^ "-" ^ fname ^ ": expected 1 arg")))))
) field_names ) field_names
| _ -> ()) | _ -> ())
) ctor_specs; ) ctor_specs;
Nil Nil
(* Register define-type via custom_special_forms so the CEK dispatch finds it. (* Register special forms via custom_special_forms so the CEK dispatch finds
The top-level (register-special-form! ...) in spec/evaluator.sx is not a them. The top-level (register-special-form! ...) calls in spec/evaluator.sx
define and therefore is not transpiled; we wire it up here instead. *) are not defines and therefore are not transpiled; we wire them up here.
let-values/define-values/delay/delay-force point at the TRANSPILED spec
functions (sf_let_values etc.) — these registrations were previously
hand-appended to the generated sx_ref.ml and were silently lost on every
regeneration (2026-07 review). *)
let () = ignore (register_special_form (String "define-type") let () = ignore (register_special_form (String "define-type")
(NativeFn ("define-type", fun call_args -> (NativeFn ("define-type", fun call_args ->
match call_args with match call_args with
| [args; env] -> sf_define_type args env | [args; env] -> sf_define_type args env
| _ -> Nil))) | _ -> Nil)))
let () = ignore (register_special_form (String "let-values")
(NativeFn ("let-values", fun call_args ->
match call_args with
| [args; env] -> sf_let_values args env
| _ -> Nil)))
let () = ignore (register_special_form (String "define-values")
(NativeFn ("define-values", fun call_args ->
match call_args with
| [args; env] -> sf_define_values args env
| _ -> Nil)))
let () = ignore (register_special_form (String "delay")
(NativeFn ("delay", fun call_args ->
match call_args with
| [args; env] -> sf_delay args env
| _ -> Nil)))
let () = ignore (register_special_form (String "delay-force")
(NativeFn ("delay-force", fun call_args ->
match call_args with
| [args; env] -> sf_delay_force args env
| _ -> Nil)))
""" """
@@ -263,7 +284,21 @@ def compile_spec_to_ml(spec_dir: str | None = None) -> str:
"pad-left", "pad-right", "char-at", "substring", "pad-left", "pad-right", "char-at", "substring",
# sf-define-type uses &rest + empty-dict literals that the transpiler # sf-define-type uses &rest + empty-dict literals that the transpiler
# can't emit as valid OCaml; hand-written implementation in FIXUPS. # can't emit as valid OCaml; hand-written implementation in FIXUPS.
"sf-define-type"} "sf-define-type",
# Arrow-named portability shims for non-OCaml hosts. All three
# are registered natively in sx_primitives.ml, and the
# transpiler has no mangling rule for '>' in identifiers
# (emits invalid OCaml like `string_>symbol`).
"string->symbol", "symbol->string", "integer->char",
# values/in-range use &rest, which the transpiler can't emit
# (see sf-define-type above). in-range is registered natively;
# values has never been in the compiled kernel — bound only in
# run_tests.ml (2026-07 review F-7); making it a real kernel
# primitive is a tracked follow-up.
"values", "in-range", "build-range",
# zero-arg guest shim; the native gensym (with optional
# prefix) is registered in sx_primitives.ml
"gensym"}
defines = [(n, e) for n, e in defines if n not in skip] defines = [(n, e) for n, e in defines if n not in skip]
# Deduplicate — keep last definition for each name (CEK overrides tree-walk) # Deduplicate — keep last definition for each name (CEK overrides tree-walk)

View File

@@ -430,8 +430,20 @@ let () =
register "expt" (fun args -> register "expt" (fun args ->
match args with match args with
| [Integer a; Integer b] when b >= 0 -> | [Integer a; Integer b] when b >= 0 ->
let rec ipow base e acc = if e = 0 then acc else ipow base (e - 1) (acc * base) in (* Overflow-checked integer power: promote to float instead of
Integer (ipow a b 1) silently wrapping int63 ((expt 2 100) used to return 0). *)
let mul_ovf x y =
if x = 0 || y = 0 then Some 0
else let p = x * y in
if p / y <> x then None else Some p in
let rec ipow base e acc =
if e = 0 then Some acc
else match mul_ovf acc base with
| None -> None
| Some acc' -> ipow base (e - 1) acc' in
(match ipow a b 1 with
| Some n -> Integer n
| None -> Number (Float.pow (float_of_int a) (float_of_int b)))
| [a; b] -> Number (Float.pow (as_number a) (as_number b)) | [a; b] -> Number (Float.pow (as_number a) (as_number b))
| _ -> raise (Eval_error "expt: 2 args")); | _ -> raise (Eval_error "expt: 2 args"));
register "quotient" (fun args -> register "quotient" (fun args ->
@@ -1227,6 +1239,12 @@ let () =
else if String.sub s i (String.length sub) = sub then true else if String.sub s i (String.length sub) = sub then true
else find (i + 1) else find (i + 1)
in Bool (find 0) in Bool (find 0)
| [Dict d; key] ->
(* Dicts: key check (per the spec doc). Keywords evaluate to their
string name, so (contains? {:a 1} :a) arrives as String "a". *)
(match key with
| String k | Keyword k | Symbol k -> Bool (Hashtbl.mem d k)
| _ -> Bool false)
| _ -> raise (Eval_error "contains?: 2 args")); | _ -> raise (Eval_error "contains?: 2 args"));
register "range" (fun args -> register "range" (fun args ->
match args with match args with

File diff suppressed because one or more lines are too long

View File

@@ -904,6 +904,12 @@ let dict_set (d : dict) key v = Hashtbl.replace d key v
let dict_delete (d : dict) key = Hashtbl.remove d key let dict_delete (d : dict) key = Hashtbl.remove d key
(* keys/vals order: OCaml Hashtbl fold order is unspecified (in practice
reverse insertion for fresh tables). NOTE (2026-07 review): this is a
determinism footgun for serialization/content-addressing, but render
attr emission depends on the current order (tests pin source-order
attributes), so changing it is gated on the insertion-order dict ruling
(RULINGS R29) — do not "fix" locally. *)
let dict_keys (d : dict) = let dict_keys (d : dict) =
Hashtbl.fold (fun k _ acc -> String k :: acc) d [] Hashtbl.fold (fun k _ acc -> String k :: acc) d []

View File

@@ -956,7 +956,13 @@
(= op "starts-with?") (= op "starts-with?")
(= op "ends-with?") (= op "ends-with?")
(= op "string-contains?") (= op "string-contains?")
(= op "string-length")) (= op "string-length")
(= op "vector?")
(= op "vector->list")
(= op "list->vector")
(= op "vector-length")
(= op "vector-ref")
(= op "gensym"))
(str (str
"(prim_call " "(prim_call "
(ml-quote-string op) (ml-quote-string op)

File diff suppressed because one or more lines are too long

View File

@@ -1792,7 +1792,7 @@
blake2_js_for_wasm_create: blake2_js_for_wasm_create}; blake2_js_for_wasm_create: blake2_js_for_wasm_create};
} }
(globalThis)) (globalThis))
({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["unix-100664f4",[2]],["sx-809891af",[2,3,4]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,6]],["dune__exe__Sx_browser-4878f9e1",[2,5,7]],["std_exit-10fb8830",[2]],["start-8c705527",0]],"generated":(b=>{var ({"link":[["runtime-0db9b496",0],["prelude-d7e4b000",0],["stdlib-23ce0836",[]],["re-9a0de245",[2]],["unix-100664f4",[2]],["sx-c1aa02f5",[2,3,4]],["jsoo_runtime-f96b44a8",[2]],["js_of_ocaml-651f6707",[2,6]],["dune__exe__Sx_browser-b2ad0885",[2,5,7]],["std_exit-10fb8830",[2]],["start-8c705527",0]],"generated":(b=>{var
c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new c=b,a=b?.module?.export||b;return{"env":{"caml_ba_kind_of_typed_array":()=>{throw new
Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new Error("caml_ba_kind_of_typed_array not implemented")},"caml_exn_with_js_backtrace":()=>{throw new
Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new Error("caml_exn_with_js_backtrace not implemented")},"caml_int64_create_lo_mi_hi":()=>{throw new

View File

@@ -26,8 +26,21 @@ _DECLARED_IO: frozenset[str] | None = None
_DECLARED_HELPERS: dict[str, frozenset[str]] | None = None _DECLARED_HELPERS: dict[str, frozenset[str]] | None = None
_LOAD_FAILURE_WARNED = False
def _load_declarations() -> None: def _load_declarations() -> None:
global _DECLARED_PURE, _DECLARED_IO, _DECLARED_HELPERS # KNOWN-BROKEN (2026-07 review, finding C24): `.ref.boundary_parser` was
# moved to hosts/python/boundary_parser.py AND its tier-1 declaration
# source (shared/sx/ref/boundary.sx) was deleted, so this ImportError
# fires on every call and boundary validation has been a silent no-op —
# including under SX_BOUNDARY_STRICT=1, which production compose sets.
# Do NOT "fix" the import in isolation: reviving validation while strict
# mode is live in production requires first recreating the core
# declarations and proving zero violations across all services
# (remediation plan, Phase 2 "Python boundary"). Until then we make the
# dead state visible instead of silent.
global _DECLARED_PURE, _DECLARED_IO, _DECLARED_HELPERS, _LOAD_FAILURE_WARNED
if _DECLARED_PURE is not None: if _DECLARED_PURE is not None:
return return
try: try:
@@ -42,6 +55,14 @@ def _load_declarations() -> None:
# Don't cache failure — parser may not be ready yet (circular import # Don't cache failure — parser may not be ready yet (circular import
# during startup). Will retry on next call. Validation functions # during startup). Will retry on next call. Validation functions
# skip checks when declarations aren't loaded. # skip checks when declarations aren't loaded.
if not _LOAD_FAILURE_WARNED:
_LOAD_FAILURE_WARNED = True
logger.warning(
"SX boundary validation is INACTIVE (declarations failed to "
"load: %s). All validate_* calls are no-ops, even under "
"SX_BOUNDARY_STRICT=1.", e,
)
else:
logger.debug("Boundary declarations not ready yet: %s", e) logger.debug("Boundary declarations not ready yet: %s", e)

View File

@@ -1312,7 +1312,12 @@
(= (type-of item) "list") (= (type-of item) "list")
(= (len item) 2) (= (len item) 2)
(= (type-of (first item)) "symbol") (= (type-of (first item)) "symbol")
(= (symbol-name (first item)) "splice-unquote")) ;; ,@ parses to splice-unquote; accept the R7RS longhand
;; unquote-splicing too (it used to serialize literally —
;; a silent zero-splice)
(or
(= (symbol-name (first item)) "splice-unquote")
(= (symbol-name (first item)) "unquote-splicing")))
(let (let
((spliced (trampoline (eval-expr (nth item 1) env)))) ((spliced (trampoline (eval-expr (nth item 1) env))))
(if (if
@@ -1395,7 +1400,9 @@
(let (let
((result (apply producer (list)))) ((result (apply producer (list))))
(if (if
(and (dict? result) (get result :_values false)) ;; 2-arg get: absent key → nil (falsy) — the explicit false default
;; was redundant AND the OCaml transpiler only emits 2-arg get
(and (dict? result) (get result :_values))
(apply consumer (get result :_list)) (apply consumer (get result :_list))
(apply consumer (list result)))))) (apply consumer (list result))))))
@@ -1415,7 +1422,7 @@
(let (let
((result (trampoline (eval-expr val-expr local)))) ((result (trampoline (eval-expr val-expr local))))
(let (let
((vs (if (and (dict? result) (get result :_values false)) (get result :_list) (list result)))) ((vs (if (and (dict? result) (get result :_values)) (get result :_list) (list result))))
(for-each-indexed (for-each-indexed
(fn (fn
(idx name) (idx name)
@@ -1442,7 +1449,7 @@
(let (let
((result (trampoline (eval-expr val-expr env)))) ((result (trampoline (eval-expr val-expr env))))
(let (let
((vs (if (and (dict? result) (get result :_values false)) (get result :_list) (list result)))) ((vs (if (and (dict? result) (get result :_values)) (get result :_list) (list result))))
(for-each-indexed (for-each-indexed
(fn (idx name) (env-bind! env (symbol-name name) (nth vs idx))) (fn (idx name) (env-bind! env (symbol-name name) (nth vs idx)))
names) names)
@@ -1699,7 +1706,9 @@
(body (rest args)) (body (rest args))
(var (first var-clauses)) (var (first var-clauses))
(clauses (rest var-clauses)) (clauses (rest var-clauses))
(sentinel (make-symbol "__guard-reraise__"))) ;; gensym: fresh tag per guard execution — a user value shaped
;; (list '__guard-reraise__ x) must not be mistaken for a re-raise
(sentinel (gensym "__guard-reraise__")))
(step-eval-list (step-eval-list
(list (list
(quote let) (quote let)
@@ -1841,12 +1850,18 @@
("io" (step-sf-io args env kont)) ("io" (step-sf-io args env kont))
("begin" (step-sf-begin args env kont)) ("begin" (step-sf-begin args env kont))
("do" ("do"
;; do-loop only when it really looks like Scheme do:
;; every element of the first form is a (var init [step])
;; list AND a test-clause list follows. A begin-style
;; (do ((fn (x) x) 5) ...) — IIFE head — used to misparse
;; as a binding list.
(if (if
(and (and
(not (empty? args)) (>= (len args) 2)
(list? (first args)) (list? (first args))
(not (empty? (first args))) (not (empty? (first args)))
(list? (first (first args)))) (every? (fn (b) (list? b)) (first args))
(list? (nth args 1)))
(let (let
((bindings (first args)) ((bindings (first args))
(test-clause (nth args 1)) (test-clause (nth args 1))
@@ -2056,7 +2071,9 @@
(args env) (args env)
(let ((thunk (make-lambda (list) (first args) env))) {:_iterative true :forced false :value nil :thunk thunk :_promise true}))) (let ((thunk (make-lambda (list) (first args) env))) {:_iterative true :forced false :value nil :thunk thunk :_promise true})))
(define promise? (fn (v) (and (dict? v) (get v :_promise false)))) ;; 2-arg get throughout: absent key → nil (falsy) — explicit false/nil
;; defaults were redundant AND the OCaml transpiler only emits 2-arg get
(define promise? (fn (v) (and (dict? v) (get v :_promise))))
;; ═══════════════════════════════════════════════════════════════ ;; ═══════════════════════════════════════════════════════════════
;; Part 11: Entry Points ;; Part 11: Entry Points
@@ -2075,12 +2092,12 @@
(not (promise? p)) (not (promise? p))
p p
(if (if
(get p :forced false) (get p :forced)
(get p :value nil) (get p :value)
(let (let
((result (apply (get p :thunk nil) (list)))) ((result (apply (get p :thunk) (list))))
(let (let
((final (if (and (get p :_iterative false) (promise? result)) (force result) result))) ((final (if (and (get p :_iterative) (promise? result)) (force result) result)))
(dict-set! p :forced true) (dict-set! p :forced true)
(dict-set! p :value final) (dict-set! p :value final)
final)))))) final))))))
@@ -2971,12 +2988,15 @@
((registered ((registered
(get (env-get env "*adt-registry*") type-name))) (get (env-get env "*adt-registry*") type-name)))
(when (when
(and registered (not (some match-clause-is-else? clauses))) ;; eta-expanded: a bare helper in HO position transpiles to
;; cek_call on an OCaml function (type error); a lambda body
;; calling it directly emits a plain application
(and registered (not (some (fn (c) (match-clause-is-else? c)) clauses)))
(let (let
((clause-ctors ((clause-ctors
(filter (filter
(fn (n) (not (nil? n))) (fn (n) (not (nil? n)))
(map match-clause-ctor-name clauses)))) (map (fn (c) (match-clause-ctor-name c)) clauses))))
(match-warn-non-exhaustive (match-warn-non-exhaustive
env type-name registered clause-ctors))))))))) env type-name registered clause-ctors)))))))))
@@ -4507,8 +4527,12 @@
rest-k))))) rest-k)))))
("restart" (make-cek-value value env rest-k)) ("restart" (make-cek-value value env rest-k))
("signal-return" ("signal-return"
;; the frame constructor stores the caller's kont under :f
;; (natively cf_f) — reading "saved-kont" returned nil and
;; silently discarded the whole continuation outside the
;; signal site (handler value became the program's result)
(let (let
((saved-kont (get frame "saved-kont"))) ((saved-kont (get frame "f")))
(make-cek-value value (get frame "env") saved-kont))) (make-cek-value value (get frame "env") saved-kont)))
("comp-trace" (make-cek-value value env rest-k)) ("comp-trace" (make-cek-value value env rest-k))
("cond-arrow" ("cond-arrow"
@@ -4565,8 +4589,12 @@
(list condition) (list condition)
(if (if
continuable? continuable?
;; R7RS: the handler's value returns to the RAISE SITE,
;; so save rest-k (the kont at raise time) — unwound-k
;; has already dropped the frames between the raise
;; site and the handler (mirrors step-sf-signal)
(kont-push (kont-push
(make-signal-return-frame fenv unwound-k) (make-signal-return-frame fenv rest-k)
unwound-k) unwound-k)
(kont-push (kont-push
(make-raise-guard-frame fenv unwound-k) (make-raise-guard-frame fenv unwound-k)

View File

@@ -160,7 +160,15 @@
"template" "template"
"slot" "slot"
"dialog" "dialog"
"menu")) "menu"
;; void elements must ALSO be here — render routes a head symbol to
;; the element renderer only when it's in HTML_TAGS; these five were
;; only in VOID_ELEMENTS and fell through to "Undefined symbol"
"area"
"base"
"embed"
"param"
"track"))
;; Self-closing tags (br, img, hr, etc.) ;; Self-closing tags (br, img, hr, etc.)
(define (define