From 9fde8a5804228331125407382123ce58f04431a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Vouillon?= Date: Thu, 30 Apr 2026 16:52:56 +0200 Subject: [PATCH 1/9] Tests: sync lib-hashtbl/htbl with upstream OCaml 5.5 OCaml 5.5 added Hashtbl.find_and_remove and find_and_replace, and the SeededS module type now requires them. The vendored htbl.ml lagged, so the HofM functor failed to satisfy SeededS. Pull in the updated upstream test (adds the two methods and a new assertion block) and bump the dune build_if gate to 5.5. --- compiler/tests-ocaml/lib-hashtbl/dune | 2 +- compiler/tests-ocaml/lib-hashtbl/htbl.ml | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/compiler/tests-ocaml/lib-hashtbl/dune b/compiler/tests-ocaml/lib-hashtbl/dune index c6aa851e80..e1e3e482b9 100644 --- a/compiler/tests-ocaml/lib-hashtbl/dune +++ b/compiler/tests-ocaml/lib-hashtbl/dune @@ -20,6 +20,6 @@ (deps ../../../LICENSE) (modules htbl) (build_if - (>= %{ocaml_version} 5)) + (>= %{ocaml_version} 5.5)) (libraries) (modes js wasm)) diff --git a/compiler/tests-ocaml/lib-hashtbl/htbl.ml b/compiler/tests-ocaml/lib-hashtbl/htbl.ml index 4af75a9f02..b5249de2d6 100644 --- a/compiler/tests-ocaml/lib-hashtbl/htbl.ml +++ b/compiler/tests-ocaml/lib-hashtbl/htbl.ml @@ -130,10 +130,12 @@ module HofM (M: Map.S) : Hashtbl.SeededS with type key = M.key = let copy = Hashtbl.copy let add = Hashtbl.add let remove = Hashtbl.remove + let find_and_remove = Hashtbl.find_and_remove let find = Hashtbl.find let find_opt = Hashtbl.find_opt let find_all = Hashtbl.find_all let replace = Hashtbl.replace + let find_and_replace = Hashtbl.find_and_replace let mem = Hashtbl.mem let iter = Hashtbl.iter let fold = Hashtbl.fold @@ -272,3 +274,19 @@ let () = let l = List.sort compare l in List.iter (fun (k, v) -> Printf.printf "%i,%i\n" k v) l; Printf.printf "%i elements\n" (Hashtbl.length h) + +let () = + let h = Hashtbl.create 16 in + Hashtbl.add h 0 0; + assert (Hashtbl.find_and_replace h 0 1 = Some 0); + assert (Hashtbl.find_and_remove h 0 = Some 1); + assert (Hashtbl.find_and_remove h 0 = None); + assert (Hashtbl.find_and_replace h 0 1 = None); + assert (Hashtbl.find_and_remove h 0 = Some 1); + Hashtbl.clear h; + Hashtbl.add h 0 0; + Hashtbl.add h 0 1; + assert (Hashtbl.find_and_replace h 0 2 = Some 1); + assert (Hashtbl.find_and_remove h 0 = Some 2); + assert (Hashtbl.find_and_remove h 0 = Some 0); + assert (Hashtbl.find_and_remove h 0 = None); From a756c9a3bb89ce3a1efb6e3b0cdaafd9ff9f04ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Vouillon?= Date: Thu, 30 Apr 2026 15:49:03 +0200 Subject: [PATCH 2/9] Runtime: add caml_runtime_hashtbl_{is_,}randomize{,d} (OCaml 5.6) OCaml 5.6 stdlib's Hashtbl now reads the runtime's randomization flag through two new primitives. Implement them in the JS and Wasm runtimes, gated on Version >= 5.6. --- runtime/js/gc.js | 19 +++++++++++++++++++ runtime/wasm/gc.wat | 14 ++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/runtime/js/gc.js b/runtime/js/gc.js index 15ca706460..df7fcdb31a 100644 --- a/runtime/js/gc.js +++ b/runtime/js/gc.js @@ -196,3 +196,22 @@ function caml_gc_tweak_list_active(_unit) { function caml_gc_tweak_list_active(_unit) { return 0; } + +//Provides: caml_runtime_hashtbl_randomized +//Version: >= 5.6 +var caml_runtime_hashtbl_randomized = 0; + +//Provides: caml_runtime_hashtbl_randomize +//Requires: caml_runtime_hashtbl_randomized +//Version: >= 5.6 +function caml_runtime_hashtbl_randomize(_unit) { + caml_runtime_hashtbl_randomized = 1; + return 0; +} + +//Provides: caml_runtime_hashtbl_is_randomized +//Requires: caml_runtime_hashtbl_randomized +//Version: >= 5.6 +function caml_runtime_hashtbl_is_randomized(_unit) { + return caml_runtime_hashtbl_randomized; +} diff --git a/runtime/wasm/gc.wat b/runtime/wasm/gc.wat index bf36b21af5..40b23315f1 100644 --- a/runtime/wasm/gc.wat +++ b/runtime/wasm/gc.wat @@ -176,4 +176,18 @@ (func (export "caml_gc_tweak_list_active") (param (ref eq)) (result (ref eq)) (ref.i31 (i32.const 0))) + +(@if (>= ocaml_version (5 6 0)) +(@then + (global $caml_runtime_hashtbl_randomized (mut i32) (i32.const 0)) + + (func (export "caml_runtime_hashtbl_randomize") + (param (ref eq)) (result (ref eq)) + (global.set $caml_runtime_hashtbl_randomized (i32.const 1)) + (ref.i31 (i32.const 0))) + + (func (export "caml_runtime_hashtbl_is_randomized") + (param (ref eq)) (result (ref eq)) + (ref.i31 (global.get $caml_runtime_hashtbl_randomized))) +)) ) From 6a108075643fc62c1a3e0673a6f25553ff06aaa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Vouillon?= Date: Thu, 30 Apr 2026 16:42:06 +0200 Subject: [PATCH 3/9] Effects: support OCaml 5.6 1-word continuations OCaml 5.6 reshaped the effect-handler ABI (commit a0ab87686082): RESUME/RESUMETERM/REPERFORMTERM no longer carry a tail/last_fiber slot, the effc handler arity drops by one (last_fiber removed), and the cont_last_fiber primitive is gone. In parse_bytecode, stop reading the tail slot for >= 5.6 and emit a placeholder Pc 0 so the IR shape (%resume 4-arg, %reperform 3-arg) stays stable -- effects.ml and the rest of the pipeline are unchanged. In the JS and Wasm runtimes (CPS and JSPI modes), fork the perform / reperform paths by OCaml version: the >= 5.6 variants call the user handler without last_fiber. Reperform reuses the tail we already maintain at cont[2] on every (re)perform, so no chain-walk is needed. --- compiler/lib/parse_bytecode.ml | 35 ++++++---- runtime/js/effect.js | 56 +++++++++++++++ runtime/wasm/effect.wat | 123 +++++++++++++++++++++++++++++++-- 3 files changed, 197 insertions(+), 17 deletions(-) diff --git a/compiler/lib/parse_bytecode.ml b/compiler/lib/parse_bytecode.ml index c3897da95d..2dd1771c7e 100644 --- a/compiler/lib/parse_bytecode.ml +++ b/compiler/lib/parse_bytecode.ml @@ -2529,11 +2529,13 @@ and compile infos pc state (instrs : instr list) = Var.print arg; let state, tail = - match Ocaml_version.compare Ocaml_version.current [ 5; 2 ] < 0 with - | true -> State.pop 2 state, Pc (Int (Targetint.of_int_exn 0)) - | false -> - let tail = State.peek 2 state in - State.pop 3 state, Pv tail + if + Ocaml_version.compare Ocaml_version.current [ 5; 2 ] < 0 + || Ocaml_version.compare Ocaml_version.current [ 5; 6 ] >= 0 + then State.pop 2 state, Pc (Int (Targetint.of_int_exn 0)) + else + let tail = State.peek 2 state in + State.pop 3 state, Pv tail in compile infos @@ -2546,11 +2548,13 @@ and compile infos pc state (instrs : instr list) = let arg = State.peek 1 state in let x, state = State.fresh_var state in let tail = - match Ocaml_version.compare Ocaml_version.current [ 5; 2 ] < 0 with - | true -> Pc (Int (Targetint.of_int_exn 0)) - | false -> - let tail = State.peek 2 state in - Pv tail + if + Ocaml_version.compare Ocaml_version.current [ 5; 2 ] < 0 + || Ocaml_version.compare Ocaml_version.current [ 5; 6 ] >= 0 + then Pc (Int (Targetint.of_int_exn 0)) + else + let tail = State.peek 2 state in + Pv tail in if debug_parser () then @@ -2579,13 +2583,18 @@ and compile infos pc state (instrs : instr list) = | REPERFORMTERM -> let eff = State.accu state in let stack = State.peek 0 state in - let tail = State.peek 1 state in - let state = State.pop 2 state in + let tail, state = + if Ocaml_version.compare Ocaml_version.current [ 5; 6 ] >= 0 + then Pc (Int (Targetint.of_int_exn 0)), State.pop 1 state + else + let tail = State.peek 1 state in + Pv tail, State.pop 2 state + in let x, state = State.fresh_var state in if debug_parser () then Format.printf "return reperform(%a, %a)@." Var.print eff Var.print stack; - ( Let (x, Prim (Extern "%reperform", [ Pv eff; Pv stack; Pv tail ])) :: instrs + ( Let (x, Prim (Extern "%reperform", [ Pv eff; Pv stack; tail ])) :: instrs , Return x , state ) | EVENT | BREAK | FIRST_UNIMPLEMENTED_OP -> assert false) diff --git a/runtime/js/effect.js b/runtime/js/effect.js index b1ac8c5f3c..eed14b8450 100644 --- a/runtime/js/effect.js +++ b/runtime/js/effect.js @@ -139,6 +139,7 @@ function caml_make_unhandled_effect_exn(eff) { //Requires: caml_get_cps_fun //If: effects //Version: >= 5.0 +//Version: < 5.6 function caml_perform_effect(eff, k0) { if (caml_current_stack.e === 0) { var exn = caml_make_unhandled_effect_exn(eff); @@ -157,6 +158,30 @@ function caml_perform_effect(eff, k0) { : caml_trampoline_return(handler, [eff, cont, last_fiber, k1]); } +//Provides: caml_perform_effect +//Requires: caml_pop_fiber, caml_stack_check_depth, caml_trampoline_return +//Requires: caml_make_unhandled_effect_exn, caml_current_stack +//Requires: caml_get_cps_fun +//If: effects +//Version: >= 5.6 +function caml_perform_effect(eff, k0) { + if (caml_current_stack.e === 0) { + var exn = caml_make_unhandled_effect_exn(eff); + throw exn; + } + // Get current effect handler + var handler = caml_current_stack.h[3]; + var last_fiber = caml_current_stack; + last_fiber.k = k0; + var cont = [245 /*continuation*/, last_fiber, last_fiber]; + // Move to parent fiber and execute the effect handler there + // The handler is defined in Stdlib.Effect, so we know that the arity matches + var k1 = caml_pop_fiber(); + return caml_stack_check_depth() + ? caml_get_cps_fun(handler)(eff, cont, k1) + : caml_trampoline_return(handler, [eff, cont, k1]); +} + //Provides: caml_reperform_effect //Requires: caml_pop_fiber, caml_stack_check_depth, caml_trampoline_return //Requires: caml_make_unhandled_effect_exn, caml_current_stack @@ -164,6 +189,7 @@ function caml_perform_effect(eff, k0) { //Requires: caml_get_cps_fun //If: effects //Version: >= 5.0 +//Version: < 5.6 function caml_reperform_effect(eff, cont, last, k0) { if (caml_current_stack.e === 0) { var exn = caml_make_unhandled_effect_exn(eff); @@ -185,6 +211,36 @@ function caml_reperform_effect(eff, cont, last, k0) { : caml_trampoline_return(handler, [eff, cont, last_fiber, k1]); } +//Provides: caml_reperform_effect +//Requires: caml_pop_fiber, caml_stack_check_depth, caml_trampoline_return +//Requires: caml_make_unhandled_effect_exn, caml_current_stack +//Requires: caml_resume_stack, caml_continuation_use_noexc +//Requires: caml_get_cps_fun +//If: effects +//Version: >= 5.6 +function caml_reperform_effect(eff, cont, _last, k0) { + if (caml_current_stack.e === 0) { + var exn = caml_make_unhandled_effect_exn(eff); + var stack = caml_continuation_use_noexc(cont); + caml_resume_stack(stack, cont[2], k0); + throw exn; + } + // Get current effect handler + var handler = caml_current_stack.h[3]; + var last_fiber = caml_current_stack; + last_fiber.k = k0; + // [cont_last_fiber] is gone in OCaml 5.6, but we still maintain the tail + // at cont[2] ourselves on every (re)perform. + cont[2].e = last_fiber; + cont[2] = last_fiber; + // Move to parent fiber and execute the effect handler there + // The handler is defined in Stdlib.Effect, so we know that the arity matches + var k1 = caml_pop_fiber(); + return caml_stack_check_depth() + ? caml_get_cps_fun(handler)(eff, cont, k1) + : caml_trampoline_return(handler, [eff, cont, k1]); +} + //Provides: caml_get_cps_fun //If: effects //If: !doubletranslate diff --git a/runtime/wasm/effect.wat b/runtime/wasm/effect.wat index 00ef85d8e8..ec9e37dd27 100644 --- a/runtime/wasm/effect.wat +++ b/runtime/wasm/effect.wat @@ -297,6 +297,8 @@ (field $eff (ref eq)) (field $cont (ref eq))))) +(@if (< ocaml_version (5 6 0)) +(@then (func $call_effect_handler (param $tail (ref eq)) (param $venv (ref eq)) (result (ref eq)) (local $env (ref $call_handler_env)) @@ -309,7 +311,27 @@ (local.tee $handler (ref.cast (ref $closure_3) (struct.get $call_handler_env $handler (local.get $env)))) - (struct.get $closure_3 1 (local.get $handler)))) + (struct.get $closure_3 1 (local.get $handler))))) +(@else + (type $function_2 + (func (param (ref eq) (ref eq) (ref eq)) (result (ref eq)))) + (type $closure_2 + (sub $closure + (struct (field (ref $function_1)) (field (ref $function_2))))) + + (func $call_effect_handler + (param $_tail (ref eq)) (param $venv (ref eq)) (result (ref eq)) + (local $env (ref $call_handler_env)) + (local $handler (ref $closure_2)) + (local.set $env (ref.cast (ref $call_handler_env) (local.get $venv))) + (return_call_ref $function_2 + (struct.get $call_handler_env $eff (local.get $env)) + (struct.get $call_handler_env $cont (local.get $env)) + (local.tee $handler + (ref.cast (ref $closure_2) + (struct.get $call_handler_env $handler (local.get $env)))) + (struct.get $closure_2 1 (local.get $handler))))) +) (func $do_perform (param $k0 (ref $cont)) (param $eff (ref eq)) @@ -379,6 +401,8 @@ (local.get $k1) (struct.get $cont $cont_func (local.get $k1)))) +(@if (< ocaml_version (5 6 0)) +(@then (func $reperform (export "%reperform") (param $eff (ref eq)) (param $cont (ref eq)) (param $tail (ref eq)) (result (ref eq)) @@ -392,7 +416,29 @@ (return_call $capture_continuation (ref.func $do_reperform) (struct.new $reperform - (local.get $eff) (local.get $cont) (local.get $tail)))) + (local.get $eff) (local.get $cont) (local.get $tail))))) +(@else + (func $reperform (export "%reperform") + (param $eff (ref eq)) (param $cont (ref eq)) (param $_tail (ref eq)) + (result (ref eq)) + (local $tail (ref eq)) + ;; [cont_last_fiber] is gone in OCaml 5.6, but we still maintain the + ;; tail at cont[2] ourselves on every (re)perform. + (local.set $tail + (array.get $block (ref.cast (ref $block) (local.get $cont)) + (i32.const 2))) + (if (ref.is_null (struct.get $fiber $next (global.get $stack))) + (then + (return_call $resume + (call $caml_continuation_use_noexc (local.get $cont)) + (global.get $raise_unhandled) + (local.get $eff) + (local.get $tail)))) + (return_call $capture_continuation + (ref.func $do_reperform) + (struct.new $reperform + (local.get $eff) (local.get $cont) (local.get $tail))))) +) ;; Allocate a stack @@ -444,6 +490,9 @@ (result (ref eq)))) (type $cps_closure (sub (struct (field (ref $function_2))))) (type $cps_closure_0 (sub (struct (field (ref $function_1))))) + (type $cps_closure_2 + (sub $cps_closure + (struct (field (ref $function_2)) (field (ref $function_3))))) (type $cps_closure_3 (sub $cps_closure (struct (field (ref $function_2)) (field (ref $function_4))))) @@ -684,6 +733,8 @@ (call $caml_named_value (global.get $already_resumed)))) (ref.i31 (i32.const 0))) +(@if (< ocaml_version (5 6 0)) +(@then (func (export "caml_perform_effect") (param $eff (ref eq)) (param $k0 (ref eq)) (result (ref eq)) (local $handler (ref eq)) (local $k1 (ref eq)) @@ -706,8 +757,35 @@ (local.get $eff) (local.get $cont) (local.get $last_fiber) (local.get $k1) (local.get $handler) (struct.get $cps_closure_3 1 - (ref.cast (ref $cps_closure_3) (local.get $handler))))) + (ref.cast (ref $cps_closure_3) (local.get $handler)))))) +(@else + (func (export "caml_perform_effect") + (param $eff (ref eq)) (param $k0 (ref eq)) (result (ref eq)) + (local $handler (ref eq)) (local $k1 (ref eq)) + (local $cont (ref $block)) + (local $last_fiber (ref $cps_fiber)) + (if (ref.is_null + (struct.get $cps_fiber $next (global.get $cps_fiber_stack))) + (then + (return_call $raise_unhandled + (local.get $eff) (ref.i31 (i32.const 0))))) + (local.set $handler + (struct.get $cps_fiber $effect (global.get $cps_fiber_stack))) + (local.set $last_fiber (global.get $cps_fiber_stack)) + (struct.set $cps_fiber $cont (local.get $last_fiber) (local.get $k0)) + (local.set $cont + (array.new_fixed $block 3 (ref.i31 (global.get $cont_tag)) + (local.get $last_fiber) (local.get $last_fiber))) + (local.set $k1 (call $caml_pop_fiber)) + (return_call_ref $function_3 + (local.get $eff) (local.get $cont) + (local.get $k1) (local.get $handler) + (struct.get $cps_closure_2 1 + (ref.cast (ref $cps_closure_2) (local.get $handler)))))) +) +(@if (< ocaml_version (5 6 0)) +(@then (func (export "caml_reperform_effect") (param $eff (ref eq)) (param $vcont (ref eq)) (param $vtail (ref eq)) (param $k0 (ref eq)) (result (ref eq)) @@ -738,7 +816,44 @@ (local.get $k1) (local.get $handler) (struct.get $cps_closure_3 1 - (ref.cast (ref $cps_closure_3) (local.get $handler))))) + (ref.cast (ref $cps_closure_3) (local.get $handler)))))) +(@else + (func (export "caml_reperform_effect") + (param $eff (ref eq)) (param $vcont (ref eq)) (param $_vtail (ref eq)) + (param $k0 (ref eq)) (result (ref eq)) + (local $handler (ref eq)) (local $k1 (ref eq)) + (local $cont (ref $block)) + (local $tail (ref $cps_fiber)) (local $last_fiber (ref $cps_fiber)) + (local.set $cont (ref.cast (ref $block) (local.get $vcont))) + ;; [cont_last_fiber] is gone in OCaml 5.6, but we still maintain the + ;; tail at cont[2] ourselves on every (re)perform. + (local.set $tail + (ref.cast (ref $cps_fiber) + (array.get $block (local.get $cont) (i32.const 2)))) + (if (ref.is_null + (struct.get $cps_fiber $next (global.get $cps_fiber_stack))) + (then + (drop + (call $caml_resume_stack + (call $caml_continuation_use_noexc (local.get $vcont)) + (local.get $tail) + (local.get $k0))) + (return_call $raise_unhandled + (local.get $eff) (ref.i31 (i32.const 0))))) + (local.set $handler + (struct.get $cps_fiber $effect (global.get $cps_fiber_stack))) + (local.set $last_fiber (global.get $cps_fiber_stack)) + (struct.set $cps_fiber $cont (local.get $last_fiber) (local.get $k0)) + (struct.set $cps_fiber $next (local.get $tail) (local.get $last_fiber)) + (array.set $block (local.get $cont) (i32.const 2) (local.get $last_fiber)) + (local.set $k1 (call $caml_pop_fiber)) + (return_call_ref $function_3 + (local.get $eff) (local.get $cont) + (local.get $k1) + (local.get $handler) + (struct.get $cps_closure_2 1 + (ref.cast (ref $cps_closure_2) (local.get $handler)))))) +) (func $cps_call_handler (param $handler (ref eq)) (param $x (ref eq)) (result (ref eq)) From 22a321ad211408ee2d5cd174248505e2a1de8a18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Vouillon?= Date: Thu, 30 Apr 2026 16:46:17 +0200 Subject: [PATCH 4/9] gen_stubs: support OCaml 5.6 primitive_description OCaml 5.6 (commit 068ef65491, "Implement primitive aliases") moved external declarations from value_description.pval_prim to a separate primitive_description type, with a pprim_kind variant distinguishing plain primitives (Pprim_decl) from aliases (Pprim_alias). Use ppx_optcomp_light to fork between the two AST shapes: pre-5.6 hooks Ast_mapper.value_description to read pval_prim, 5.6+ hooks primitive_description and reads the string list from Pprim_decl (aliases are skipped, since they reference an OCaml value rather than a C primitive). --- lib/gen_stubs/dune | 4 +++- lib/gen_stubs/gen_stubs.ml | 30 ++++++++++++++++++++++++------ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/lib/gen_stubs/dune b/lib/gen_stubs/dune index 019242693e..3a46ceeb69 100644 --- a/lib/gen_stubs/dune +++ b/lib/gen_stubs/dune @@ -1,3 +1,5 @@ (executable (name gen_stubs) - (libraries compiler-libs.common)) + (libraries compiler-libs.common) + (preprocess + (pps ppx_optcomp_light))) diff --git a/lib/gen_stubs/gen_stubs.ml b/lib/gen_stubs/gen_stubs.ml index 190780b53c..c7ac7042a3 100644 --- a/lib/gen_stubs/gen_stubs.ml +++ b/lib/gen_stubs/gen_stubs.ml @@ -28,6 +28,29 @@ void %s () { s s +let collect_prims externals _mapper desc = + let l = List.filter (fun x -> x.[0] <> '%') desc.Parsetree.pval_prim in + externals := List.fold_right String_set.add l !externals; + desc +[@@if ocaml_version < (5, 6, 0)] + +let collect_prims externals _mapper desc = + (match desc.Parsetree.pprim_kind with + | Pprim_decl (_, l) -> + let l = List.filter (fun x -> x.[0] <> '%') l in + externals := List.fold_right String_set.add l !externals + | Pprim_alias _ -> ()); + desc +[@@if ocaml_version >= (5, 6, 0)] + +let make_mapper externals = + { Ast_mapper.default_mapper with value_description = collect_prims externals } +[@@if ocaml_version < (5, 6, 0)] + +let make_mapper externals = + { Ast_mapper.default_mapper with primitive_description = collect_prims externals } +[@@if ocaml_version >= (5, 6, 0)] + let () = let mls = ref [] in let except_mls = ref [] in @@ -39,12 +62,7 @@ let () = let get_externals l = let l = List.filter real_ml l in let externals = ref String_set.empty in - let value_description _mapper desc = - let l = List.filter (fun x -> x.[0] <> '%') desc.Parsetree.pval_prim in - externals := List.fold_right String_set.add l !externals; - desc - in - let mapper = { Ast_mapper.default_mapper with value_description } in + let mapper = make_mapper externals in List.iter (fun ml -> let in_ = open_in ml in From 16efbb0c883365e95fd09fb02f1be3181fabf228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Vouillon?= Date: Thu, 30 Apr 2026 16:56:06 +0200 Subject: [PATCH 5/9] toplevel_expect: support OCaml 5.6 The 5.4 toplevel_expect_test.ml works unchanged on 5.6. --- tools/toplevel_expect/gen.ml | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/toplevel_expect/gen.ml b/tools/toplevel_expect/gen.ml index eceee8853e..c49746845f 100644 --- a/tools/toplevel_expect/gen.ml +++ b/tools/toplevel_expect/gen.ml @@ -42,4 +42,5 @@ let () = | 5, 3 -> dump_file "toplevel_expect_test.ml-5.3" | 5, 4 -> dump_file "toplevel_expect_test.ml-5.4" | 5, 5 -> dump_file "toplevel_expect_test.ml-5.4" + | 5, 6 -> dump_file "toplevel_expect_test.ml-5.4" | _ -> failwith ("unsupported version " ^ Sys.ocaml_version)) From 5785c75ec9b78e198698e92eddd0835f3d91c27c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Vouillon?= Date: Thu, 30 Apr 2026 17:06:16 +0200 Subject: [PATCH 6/9] Wasm runtime: add caml_uniform_array_make It is used since OCaml 5.6 to implement Atomic.Array. Export it as an alias on caml_make_vect, mirroring the JS runtime. --- runtime/wasm/array.wat | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/runtime/wasm/array.wat b/runtime/wasm/array.wat index 46ac866ff1..3f936387ea 100644 --- a/runtime/wasm/array.wat +++ b/runtime/wasm/array.wat @@ -29,7 +29,9 @@ (global $empty_array (ref eq) (array.new_fixed $block 1 (ref.i31 (i32.const 0)))) - (func $caml_make_vect (export "caml_make_vect") (export "caml_array_make") + (func $caml_make_vect + (export "caml_make_vect") (export "caml_array_make") + (export "caml_uniform_array_make") (param $n (ref eq)) (param $v (ref eq)) (result (ref eq)) (local $sz i32) (local $b (ref $block)) (local $f f64) (local.set $sz (i31.get_s (ref.cast (ref i31) (local.get $n)))) From 6ae1de80cd941daaff5688eef8552352decb1b99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Vouillon?= Date: Thu, 30 Apr 2026 17:23:50 +0200 Subject: [PATCH 7/9] calc_lexer: add catch-all rule OCaml 5.6's ocamllex emits a missing-case warning when the rule has no '| _' fallback. The calculator only accepts digits, whitespace, and a fixed set of operators; an unexpected character was previously implementation-defined and is now a parse failure. --- compiler/tests-jsoo/calc_lexer.mll | 1 + 1 file changed, 1 insertion(+) diff --git a/compiler/tests-jsoo/calc_lexer.mll b/compiler/tests-jsoo/calc_lexer.mll index 4986ea12a5..5025bd062b 100644 --- a/compiler/tests-jsoo/calc_lexer.mll +++ b/compiler/tests-jsoo/calc_lexer.mll @@ -14,3 +14,4 @@ rule token = parse | '(' { LPAREN } | ')' { RPAREN } | eof { raise Eof } + | _ as c { failwith (Printf.sprintf "unexpected character %C" c) } From 7262f46e77a4f2b895350c31bb2642cea1f4c603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Vouillon?= Date: Thu, 30 Apr 2026 16:49:37 +0200 Subject: [PATCH 8/9] test_gc: suppress deprecated alert on Gc.stat OCaml 5.6 deprecated Gc.stat in favour of full_major () followed by quick_stat (). The "stat" test deliberately exercises Gc.stat itself to verify the record's layout, so suppress the alert at the call site rather than substitute the recommended replacement. --- compiler/tests-jsoo/lib-gc/test_gc.ml | 2 +- compiler/tests-jsoo/test_gc.ml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/tests-jsoo/lib-gc/test_gc.ml b/compiler/tests-jsoo/lib-gc/test_gc.ml index d3e2d16b6d..f33725fb29 100644 --- a/compiler/tests-jsoo/lib-gc/test_gc.ml +++ b/compiler/tests-jsoo/lib-gc/test_gc.ml @@ -36,5 +36,5 @@ type control = let () = assert ((Gc.get ()).custom_minor_max_size = 0); - assert ((Gc.stat ()).forced_major_collections = 0); + assert (((Gc.stat [@alert "-deprecated"]) ()).forced_major_collections = 0); assert ((Gc.quick_stat ()).forced_major_collections = 0) diff --git a/compiler/tests-jsoo/test_gc.ml b/compiler/tests-jsoo/test_gc.ml index 4e1685c06c..8226e28c7d 100644 --- a/compiler/tests-jsoo/test_gc.ml +++ b/compiler/tests-jsoo/test_gc.ml @@ -31,7 +31,7 @@ let ok () = print_endline "OK" let ko size = Printf.printf "size=%d, ocaml_version=%s" size Sys.ocaml_version let%expect_test "stat" = - let s = Gc.stat () in + let s = (Gc.stat [@alert "-deprecated"]) () in let size = Obj.size (Obj.repr s) in (match size with | 18 when ocaml_version >= (5, 5) -> ok () From ed5ee90848637d585c91497ecfa189ff828890e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Vouillon?= Date: Thu, 30 Apr 2026 14:56:12 +0200 Subject: [PATCH 9/9] Update OCaml bound --- compiler/lib/magic_number.ml | 2 +- dune-project | 2 +- js_of_ocaml-compiler.opam | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compiler/lib/magic_number.ml b/compiler/lib/magic_number.ml index 403280afc0..900b96943f 100644 --- a/compiler/lib/magic_number.ml +++ b/compiler/lib/magic_number.ml @@ -68,7 +68,7 @@ let () = let current = Ocaml_version.current in if Ocaml_version.compare current [ 4; 13 ] < 0 then failwith "OCaml version unsupported. Upgrade to OCaml 4.13 or newer." - else if Ocaml_version.compare current [ 5; 6 ] >= 0 + else if Ocaml_version.compare current [ 5; 7 ] >= 0 then failwith "OCaml version unsupported. Upgrade js_of_ocaml." let v = snd (of_string Ocaml_common.Config.exec_magic_number) diff --git a/dune-project b/dune-project index e35053e5c4..806ce38546 100644 --- a/dune-project +++ b/dune-project @@ -19,7 +19,7 @@ (description "Js_of_ocaml is a compiler from OCaml bytecode to JavaScript. It makes it possible to run pure OCaml programs in JavaScript environment like browsers and Node.js") (depends - (ocaml (and (>= 4.13) (< 5.6))) + (ocaml (and (>= 4.13) (< 5.7))) (num :with-test) (ppx_expect (and (>= v0.16.1) :with-test)) (ppxlib (>= 0.33)) diff --git a/js_of_ocaml-compiler.opam b/js_of_ocaml-compiler.opam index 1c7e4f20eb..10963ded9c 100644 --- a/js_of_ocaml-compiler.opam +++ b/js_of_ocaml-compiler.opam @@ -13,7 +13,7 @@ doc: "https://ocsigen.org/js_of_ocaml/latest/manual/overview" bug-reports: "https://github.com/ocsigen/js_of_ocaml/issues" depends: [ "dune" {>= "3.20"} - "ocaml" {>= "4.13" & < "5.6"} + "ocaml" {>= "4.13" & < "5.7"} "num" {with-test} "ppx_expect" {>= "v0.16.1" & with-test} "ppxlib" {>= "0.33"}