Fix env-shadowing: rebind host extension points after .sx file load

evaluator.sx defines *custom-special-forms* and register-special-form!
which shadow the host's native bindings when loaded at runtime. The
native bindings route to Sx_ref.custom_special_forms (the dict the CEK
evaluator checks), but the SX-level defines create a separate dict.

Fix: rebind_host_extensions runs after every load command, re-asserting
the native register-special-form! and *custom-special-forms* bindings.

Add regression test: custom form registered before evaluator.sx load
survives and remains callable via CEK dispatch afterward.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 18:29:29 +00:00
parent 2d8741779e
commit 3ae49b69f5
2 changed files with 28 additions and 1 deletions

View File

@@ -711,6 +711,18 @@ let register_jit_hook env =
(* ====================================================================== *) (* ====================================================================== *)
(* Re-assert host-provided extension points after loading .sx files.
evaluator.sx defines *custom-special-forms* and register-special-form!
which shadow the native bindings from setup_evaluator_bridge. *)
let rebind_host_extensions env =
Hashtbl.replace env.bindings "register-special-form!"
(NativeFn ("register-special-form!", fun args ->
match args with
| [String name; handler] ->
ignore (Sx_ref.register_special_form (String name) handler); Nil
| _ -> raise (Eval_error "register-special-form!: expected (name handler)")));
ignore (env_bind env "*custom-special-forms*" Sx_ref.custom_special_forms)
(* Command dispatch *) (* Command dispatch *)
(* ====================================================================== *) (* ====================================================================== *)
@@ -727,6 +739,9 @@ let rec dispatch env cmd =
ignore (Sx_ref.eval_expr expr (Env env)); ignore (Sx_ref.eval_expr expr (Env env));
incr count incr count
) exprs; ) exprs;
(* Rebind host extension points after .sx load — evaluator.sx
defines *custom-special-forms* which shadows the native dict *)
rebind_host_extensions env;
send_ok_value (Number (float_of_int !count)) send_ok_value (Number (float_of_int !count))
with with
| Eval_error msg -> send_error msg | Eval_error msg -> send_error msg
@@ -1142,7 +1157,9 @@ let cli_load_files env files =
ignore (Sx_ref.eval_expr expr (Env env)) ignore (Sx_ref.eval_expr expr (Env env))
) exprs ) exprs
end end
) files ) files;
(* Rebind after load in case .sx files shadowed host extension points *)
rebind_host_extensions env
let cli_mode mode = let cli_mode mode =
let env = make_server_env () in let env = make_server_env () in

View File

@@ -101,6 +101,16 @@ check('defhandler via eval', '(has-key? (defhandler test-h (&key x) x) \"__type\
check('definition-form-extensions populated', '(> (len *definition-form-extensions*) 0)', 'true') check('definition-form-extensions populated', '(> (len *definition-form-extensions*) 0)', 'true')
check('RENDER_HTML_FORMS has defstyle', '(contains? RENDER_HTML_FORMS \"defstyle\")', 'true') check('RENDER_HTML_FORMS has defstyle', '(contains? RENDER_HTML_FORMS \"defstyle\")', 'true')
# Env-shadowing regression: custom forms survive evaluator.sx load
bridge2 = OcamlSync()
bridge2.eval('(register-special-form! \"shadow-test\" (fn (args env) 42))')
bridge2.load('spec/evaluator.sx')
check('custom form survives evaluator.sx load',
bridge2.eval('(has-key? *custom-special-forms* \"shadow-test\")'), 'true')
bridge2.eval('(register-special-form! \"post-load\" (fn (args env) 99))')
check('custom form callable after evaluator.sx load',
bridge2.eval('(post-load 1)'), '99')
print(f'\\nResults: {ok} passed, {fail} failed') print(f'\\nResults: {ok} passed, {fail} failed')
import sys; sys.exit(1 if fail > 0 else 0) import sys; sys.exit(1 if fail > 0 else 0)
" "