Revision a51a47026b5e820ac89c59d0fb68865f69550fb3 authored by Alain Mebsout on 24 May 2023, 19:41:48 UTC, committed by Marge Bot on 21 June 2023, 18:12:10 UTC
1 parent 8f44539
wasm_vm.ml
(*****************************************************************************)
(* *)
(* Open Source License *)
(* Copyright (c) 2022 TriliTech <contact@trili.tech> *)
(* *)
(* Permission is hereby granted, free of charge, to any person obtaining a *)
(* copy of this software and associated documentation files (the "Software"),*)
(* to deal in the Software without restriction, including without limitation *)
(* the rights to use, copy, modify, merge, publish, distribute, sublicense, *)
(* and/or sell copies of the Software, and to permit persons to whom the *)
(* Software is furnished to do so, subject to the following conditions: *)
(* *)
(* The above copyright notice and this permission notice shall be included *)
(* in all copies or substantial portions of the Software. *)
(* *)
(* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR*)
(* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, *)
(* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL *)
(* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER*)
(* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING *)
(* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER *)
(* DEALINGS IN THE SOFTWARE. *)
(* *)
(*****************************************************************************)
module Wasm = Tezos_webassembly_interpreter
open Wasm_pvm_state.Internal_state
let version_for_protocol : Pvm_input_kind.protocol -> Wasm_pvm_state.version =
function
| Nairobi -> V1
| Proto_alpha -> V2
let link_finished (ast : Wasm.Ast.module_) offset =
offset >= Wasm.Ast.Vector.num_elements ast.it.imports
let eval_has_finished = function
| Eval {config = {step_kont = Wasm.Eval.(SK_Result _); _}; _} -> true
| Padding -> true
(* explicit pattern matching to avoid new states introducing silent bugs *)
| Snapshot | Decode _ | Link _ | Init _ | Eval _ | Collect | Stuck _ -> false
let ticks_to_snapshot {current_tick; last_top_level_call; max_nb_ticks; _} =
let open Z in
max_nb_ticks - one - (current_tick - last_top_level_call)
(* The max number of tick is offsetted by 1, which corresponds to the input
tick. *)
let is_time_for_snapshot pvm_state =
Z.Compare.(ticks_to_snapshot pvm_state <= Z.zero)
let has_reboot_flag durable =
let open Lwt_syntax in
let+ allows_reboot =
Lwt.catch
(fun () -> Durable.(find_value durable Constants.reboot_flag_key))
(function exn -> raise exn)
in
allows_reboot <> None
let has_stuck_flag durable =
let open Lwt_syntax in
let+ stuck = Durable.(find_value durable Constants.stuck_flag_key) in
Option.is_some stuck
let has_upgrade_error_flag durable =
let open Lwt_syntax in
let+ error = Durable.(find_value durable Constants.upgrade_error_flag_key) in
Option.is_some error
let get_wasm_version {durable; _} =
let open Lwt_syntax in
let* cbv = Durable.find_value_exn durable Constants.version_key in
let+ bytes = Tezos_lazy_containers.Chunked_byte_vector.to_bytes cbv in
Data_encoding.Binary.of_bytes_exn Wasm_pvm_state.version_encoding bytes
let stack_size_limit = function Wasm_pvm_state.V0 -> 300 | V1 | V2 -> 60_000
(* The limit 60_000 has been chosen such that the simplest WASM program
consisting in trying to recursively call 60,000 times the same function
results in Wasmer raising a runtime error.
(module
(memory 1)
(export "mem" (memory 0))
(func $aux (param $n i32)
(i32.ne (local.get $n) (i32.const 0))
(if (then (call $aux (i32.sub (local.get $n) (i32.const 1))))))
(func (export "kernel_run")
(call $aux (i32.const 66_000))))
As a result, the key idea is that any program executing with Wasmer without
an error could be executing by the WASM PVM. *)
let patch_flags_on_eval_successful durable =
let open Lwt_syntax in
(* We have an empty set of admin instructions, but need to wait until we can restart *)
let* has_stuck_flag = has_stuck_flag durable in
let* durable =
if has_stuck_flag then
Durable.(
delete
~edit_readonly:true
~kind:Directory
durable
Constants.stuck_flag_key)
else Lwt.return durable
in
let* has_upgrade_error_flag = has_upgrade_error_flag durable in
let+ durable =
if has_upgrade_error_flag then
Durable.(
delete
~edit_readonly:true
~kind:Directory
durable
Constants.upgrade_error_flag_key)
else Lwt.return durable
in
durable
let mark_for_reboot {reboot_counter; durable; _} =
let open Lwt_syntax in
let+ has_reboot_flag = has_reboot_flag durable in
if has_reboot_flag then
if Z.Compare.(reboot_counter <= Z.zero) then `Forcing_yield else `Reboot
else `Yielding
(* Returns true is a fallback kernel is available, and it's different to
the currently running kernel. *)
let has_fallback_kernel durable =
let open Lwt_syntax in
let* kernel_hash = Durable.hash ~kind:Value durable Constants.kernel_key in
let+ fallback_hash =
Durable.hash ~kind:Durable.Value durable Constants.kernel_fallback_key
in
Option.is_some fallback_hash && kernel_hash <> fallback_hash
let initial_boot_state () =
Decode
(Tezos_webassembly_interpreter.Decode.initial_decode_kont
~name:Constants.wasm_main_module_name)
let save_fallback_kernel durable =
let open Lwt.Syntax in
let* kernel_hash = Durable.hash ~kind:Value durable Constants.kernel_key in
let* kernel_fallback_hash =
Durable.hash ~kind:Value durable Constants.kernel_fallback_key
in
if kernel_hash <> kernel_fallback_hash then
Durable.copy_tree_exn
durable
~edit_readonly:true
Constants.kernel_key
Constants.kernel_fallback_key
else Lwt.return durable
let unsafe_next_tick_state ~version ~stack_size_limit host_funcs
({buffers; durable; tick_state; _} as pvm_state) =
let open Lwt_syntax in
let return ?(status = Running) ?(durable = durable) state =
Lwt.return (durable, state, status)
in
match tick_state with
| Stuck ((Decode_error _ | Init_error _ | Link_error _) as e) ->
let cause =
match e with
| Decode_error e -> Wasm_pvm_errors.Decode_cause e
| Link_error e -> Wasm_pvm_errors.Link_cause e
| Init_error e -> Wasm_pvm_errors.Init_cause e
| _ -> assert false
in
let* has_fallback = has_fallback_kernel durable in
if has_fallback then
let* durable =
Durable.copy_tree_exn
durable
Constants.kernel_fallback_key
Constants.kernel_key
in
let* durable =
Durable.write_value_exn
~edit_readonly:true
durable
Constants.upgrade_error_flag_key
0L
""
in
return ~durable Padding
else return ~status:Failing (Stuck (No_fallback_kernel cause))
| Stuck e -> return ~status:Failing (Stuck e)
| Snapshot -> return (initial_boot_state ())
| Collect ->
return
~status:Failing
(Stuck (Wasm_pvm_errors.invalid_state "Collect is a input tick"))
| Padding when is_time_for_snapshot pvm_state -> (
let* reboot_status = mark_for_reboot pvm_state in
match reboot_status with
| `Reboot -> return ~status:Reboot Snapshot
| `Forcing_yield -> return ~status:Forcing_yield Collect
| `Yielding -> return ~status:Yielding Collect)
| _ when is_time_for_snapshot pvm_state ->
(* Execution took too many ticks *)
return ~status:Failing (Stuck Too_many_ticks)
| Decode {module_kont = MKStop ast_module; _} ->
return
(Link
{
ast_module;
externs = Wasm.Instance.Vector.empty ();
imports_offset = 0l;
})
| Decode m ->
let* kernel = Durable.find_value_exn durable Constants.kernel_key in
let* m =
Tezos_webassembly_interpreter.Decode.module_step
~allow_floats:false
kernel
m
in
return (Decode m)
| Link {ast_module; externs; imports_offset}
when link_finished ast_module imports_offset ->
let self = Wasm.Instance.Module_key Constants.wasm_main_module_name in
let module_reg = Wasm.Instance.ModuleMap.create () in
(* The module instance will be registered as [self] in
[module_reg] during the initialization. *)
return (Init {self; ast_module; init_kont = IK_Start externs; module_reg})
| Link {ast_module; externs; imports_offset} ->
let* {it = {module_name; item_name; _}; _} =
Wasm.Ast.Vector.get imports_offset ast_module.it.imports
in
if module_name = Constants.wasm_host_funcs_virual_module then
match Host_funcs.lookup_opt ~version item_name with
| Some extern ->
let externs, _ = Wasm.Ast.Vector.append extern externs in
return
(Link
{
ast_module;
externs;
imports_offset = Int32.succ imports_offset;
})
| None ->
return
~status:Failing
(Stuck (Wasm_pvm_errors.link_error `Item ~module_name ~item_name))
else
return
~status:Failing
(Stuck (Wasm_pvm_errors.link_error `Module ~module_name ~item_name))
| Init {self; ast_module = _; init_kont = IK_Stop; module_reg} -> (
let* module_inst =
Wasm.Instance.ModuleMap.get Constants.wasm_main_module_name module_reg
in
let* extern =
Lwt.catch
(fun () ->
let+ extern =
Wasm.Instance.NameMap.get
Constants.wasm_entrypoint
module_inst.Wasm.Instance.exports
in
Some extern)
(function
| Tezos_lazy_containers.Lazy_map.UnexpectedAccess -> return_none
| exn -> raise exn)
in
match extern with
| Some (Wasm.Instance.ExternFunc main_func) ->
let admin_instr' = Wasm.Eval.Invoke main_func in
let admin_instr = Wasm.Source.{it = admin_instr'; at = no_region} in
(* Clear the values and the locals in the frame. *)
let config =
Wasm.Eval.config
~stack_size_limit
self
(Tezos_lazy_containers.Lazy_vector.Int32Vector.empty ())
(Tezos_lazy_containers.Lazy_vector.Int32Vector.singleton
admin_instr)
in
(* Set kernel - now known to be valid - as fallback kernel,
if it is not already *)
let* durable = save_fallback_kernel durable in
return ~durable (Eval {config; module_reg})
| _ ->
(* We require a function with the name [main] to be exported
rather than any other structure. *)
return
~status:Failing
(Stuck
(Wasm_pvm_errors.invalid_state
(Format.sprintf
"Invalid_module: no `%s` function exported"
Constants.wasm_entrypoint))))
| Init {self; ast_module; init_kont; module_reg} ->
let* init_kont =
Wasm.Eval.init_step
~stack_size_limit
~filter_exports:true
~check_module_exports:Exports_memory_0
~module_reg
~self
buffers
host_funcs
ast_module
init_kont
in
return (Init {self; ast_module; init_kont; module_reg})
| Padding -> return Padding
| Eval {config = {step_kont = Wasm.Eval.(SK_Trapped _msg); _}; _} ->
let* durable =
Durable.write_value_exn
~edit_readonly:true
durable
Constants.stuck_flag_key
0L
""
in
return ~durable Padding
| _ when eval_has_finished tick_state ->
(* We have an empty set of admin instructions, but need to wait until we can restart *)
let* durable = patch_flags_on_eval_successful durable in
return ~durable Padding
| Eval {config; module_reg} ->
(* Continue execution. *)
let store = Durable.to_storage durable in
let* store', config =
Wasm.Eval.step ~host_funcs ~durable:store module_reg config buffers
in
let durable' = Durable.of_storage ~default:durable store' in
return ~durable:durable' (Eval {config; module_reg})
let exn_to_stuck pvm_state exn =
let error = Wasm_pvm_errors.extract_interpreter_error exn in
let wasm_error =
match error with
| `Interpreter error -> (
match pvm_state.tick_state with
| Decode _ -> Wasm_pvm_errors.Decode_error error
| Link _ -> Link_error error.Wasm_pvm_errors.raw_exception
| Init _ -> Init_error error
| Eval _ -> Eval_error error
| Snapshot | Stuck _ | Collect | Padding ->
Unknown_error error.raw_exception)
| `Unknown raw_exception -> Unknown_error raw_exception
in
Lwt.return (Stuck wasm_error)
let next_tick_state ~version ~stack_size_limit host_function_registry pvm_state
=
let open Lwt_syntax in
Lwt.catch
(fun () ->
unsafe_next_tick_state
~version
~stack_size_limit
host_function_registry
pvm_state)
(fun exn ->
let+ tick_state = exn_to_stuck pvm_state exn in
(pvm_state.durable, tick_state, Failing))
let next_last_top_level_call {current_tick; last_top_level_call; _} = function
| Forcing_yield | Yielding | Reboot -> Z.succ current_tick
| Failing | Running -> last_top_level_call
let next_reboot_counter {reboot_counter; maximum_reboots_per_input; _} status =
match status with
| Reboot -> Z.pred reboot_counter
| Forcing_yield | Yielding | Failing ->
Z.succ maximum_reboots_per_input (* one is used to read the inbox *)
| Running -> reboot_counter
(** When successfully restarting the VM, we can remove the
[Too_many_reboot] flag if it exists. On the contrary, we create it
in case we are failing. *)
let patch_too_many_reboot_flag durable =
let open Lwt_syntax in
function
| Yielding ->
Durable.delete
~edit_readonly:true
~kind:Directory
durable
Constants.too_many_reboot_flag_key
| Forcing_yield ->
Durable.(
write_value_exn
~edit_readonly:true
durable
Constants.too_many_reboot_flag_key
0L
"")
| _ -> return durable
(** When rebooting, we can remove the [Reboot] flag (because it has
achieved its purpose). On the contrary, when [Yielding] or
[Forcing_yield], we set the flag, because we will want to reboot
once the inbox is loaded. *)
let patch_reboot_flag durable =
let open Lwt_syntax in
function
| Reboot ->
Durable.delete
~edit_readonly:true
~kind:Directory
durable
Constants.reboot_flag_key
| Forcing_yield | Yielding ->
Durable.(write_value_exn durable Constants.reboot_flag_key 0L "")
| _ -> return durable
(** When rebooting, we update the new [reboot_counter] exposed to the
kernel. *)
let patch_reboot_counter durable reboot_counter =
let open Lwt_syntax in
function
| Running | Failing -> return durable
| Reboot | Forcing_yield | Yielding ->
(* Deleting first allows to not have to fetch the previous
contents, which is a full chunks. This reduces the size of
the resulting proof. *)
let* durable =
Durable.delete
~edit_readonly:true
~kind:Directory
durable
Constants.reboot_counter_key
in
Durable.write_value_exn
~edit_readonly:true
durable
Constants.reboot_counter_key
0L
(* We use a standard encoding of a 32-bit integers, instead of
the ad-hoc Z encoding, to make the life of the kernel
developers easier. *)
Data_encoding.(
Binary.to_string_exn
Data_encoding_utils.Little_endian.int32
(Z.to_int32 reboot_counter))
(** Every time the kernel yields, we reset the input buffer. *)
let clean_up_input_buffer buffers =
let open Tezos_webassembly_interpreter in
function
| Forcing_yield | Yielding -> Input_buffer.reset buffers.Eval.input | _ -> ()
(** [compute_step pvm_state] does one computation step on [pvm_state].
Returns the new state.
*)
let compute_step_with_host_functions ~version ~stack_size_limit registry
pvm_state =
let open Lwt_syntax in
(* Calculate the next tick state. *)
let* durable, tick_state, status =
next_tick_state ~version ~stack_size_limit registry pvm_state
in
let current_tick = Z.succ pvm_state.current_tick in
let last_top_level_call = next_last_top_level_call pvm_state status in
let reboot_counter = next_reboot_counter pvm_state status in
let* durable = patch_too_many_reboot_flag durable status in
let* durable = patch_reboot_flag durable status in
let* durable = patch_reboot_counter durable reboot_counter status in
let () = clean_up_input_buffer pvm_state.buffers status in
let pvm_state =
{
pvm_state with
tick_state;
durable;
current_tick;
last_top_level_call;
reboot_counter;
}
in
return pvm_state
let compute_step pvm_state =
let open Lwt_syntax in
let* version = get_wasm_version pvm_state in
let stack_size_limit = stack_size_limit version in
compute_step_with_host_functions
~version
~stack_size_limit
(Host_funcs.registry ~version ~write_debug:Noop)
pvm_state
let compute_step_with_debug ~write_debug pvm_state =
let open Lwt_syntax in
let* version = get_wasm_version pvm_state in
compute_step_with_host_functions
~version
~stack_size_limit:(stack_size_limit version)
(Host_funcs.registry ~version ~write_debug)
pvm_state
let input_request pvm_state =
match pvm_state.tick_state with
| Stuck (Decode_error _ | Init_error _ | Link_error _) ->
(* These stuck states are recovered on the next tick by
the fallback mechanism. *)
Wasm_pvm_state.No_input_required
| Stuck _ -> Wasm_pvm_state.Input_required
| Snapshot -> Wasm_pvm_state.No_input_required
| Collect -> Wasm_pvm_state.Input_required
| Eval {config; _} -> (
match Tezos_webassembly_interpreter.Eval.is_reveal_tick config with
| Some reveal -> Wasm_pvm_state.Reveal_required reveal
| None -> Wasm_pvm_state.No_input_required)
| _ -> Wasm_pvm_state.No_input_required
let is_top_level_padding pvm_state =
match pvm_state.tick_state with
| Padding -> not @@ is_time_for_snapshot pvm_state
| _ -> false
let measure_executed_ticks (transition : pvm_state -> pvm_state Lwt.t)
(initial_state : pvm_state) : (pvm_state * int64) Lwt.t =
let open Lwt.Syntax in
let open Z in
let+ final_state = transition initial_state in
let ticks_executed = final_state.current_tick - initial_state.current_tick in
(final_state, to_int64 ticks_executed)
let reveal_step payload pvm_state =
let open Lwt_syntax in
let return tick_state =
Lwt.return
{pvm_state with current_tick = Z.succ pvm_state.current_tick; tick_state}
in
match pvm_state.tick_state with
| Eval {config; module_reg} ->
let* config =
Tezos_webassembly_interpreter.Eval.reveal_step
Host_funcs.Aux.reveal
module_reg
payload
config
in
return (Eval {config; module_reg})
| Snapshot ->
return
(Stuck (Wasm_pvm_errors.invalid_state "No reveal expected during start"))
| Decode _ ->
return
(Stuck
(Wasm_pvm_errors.invalid_state "No reveal expected during decoding"))
| Link _ ->
return
(Stuck (Wasm_pvm_errors.invalid_state "No reveal expected during link"))
| Init _ ->
return
(Stuck
(Wasm_pvm_errors.invalid_state
"No reveal expected during initialization"))
| Collect ->
return
(Stuck
(Wasm_pvm_errors.invalid_state
"No reveal expected during collecting"))
| Stuck _ | Padding -> return pvm_state.tick_state
let compute_step_many_until ?(max_steps = 1L) ?reveal_builtins
?(write_debug = Builtins.Noop) should_continue pvm_state =
let open Lwt.Syntax in
assert (max_steps > 0L) ;
let* version = get_wasm_version pvm_state in
let stack_size_limit = stack_size_limit version in
let host_function_registry = Host_funcs.registry ~version ~write_debug in
let compute_step_with_reveal =
match reveal_builtins with
| Some reveal_builtins -> (
fun pvm_state ->
let info = input_request pvm_state in
match info with
| Reveal_required (Reveal_raw_data req) ->
let* res = reveal_builtins.Builtins.reveal_preimage req in
reveal_step (Bytes.of_string res) pvm_state
| Reveal_required Reveal_metadata ->
let* res = reveal_builtins.reveal_metadata () in
reveal_step (Bytes.of_string res) pvm_state
| _ ->
compute_step_with_host_functions
~version
~stack_size_limit
host_function_registry
pvm_state)
| None ->
compute_step_with_host_functions
~version
~stack_size_limit
host_function_registry
in
let rec go steps_left pvm_state =
let* continue = should_continue pvm_state in
if steps_left > 0L && continue then
if is_top_level_padding pvm_state then
(* We're in the top-level padding after the evaluation has
finished. That means we can skip up to the tick before the
snapshot in one go. *)
let bulk_ticks =
Z.(min (ticks_to_snapshot pvm_state) (of_int64 steps_left))
in
let pvm_state =
{
pvm_state with
tick_state = Padding;
current_tick = Z.add pvm_state.current_tick bulk_ticks;
}
in
go (Int64.sub steps_left (Z.to_int64 bulk_ticks)) pvm_state
else
let* pvm_state = compute_step_with_reveal pvm_state in
go (Int64.pred steps_left) pvm_state
else Lwt.return pvm_state
in
let one_or_more_steps pvm_state =
(* Make sure we perform at least 1 step. The assertion above ensures that
we were asked to perform at least 1. *)
let* pvm_state =
compute_step_with_host_functions
~version
~stack_size_limit
host_function_registry
pvm_state
in
go (Int64.pred max_steps) pvm_state
in
measure_executed_ticks one_or_more_steps pvm_state
let should_compute ?reveal_builtins pvm_state =
let input_request_val = input_request pvm_state in
match input_request_val with
| Input_required -> false
| No_input_required -> true
| Reveal_required _ -> Option.is_some reveal_builtins
let compute_step_many ?reveal_builtins ?write_debug ?(stop_at_snapshot = false)
~max_steps pvm_state =
compute_step_many_until
~max_steps
?reveal_builtins
?write_debug
(fun pvm_state ->
Lwt.return
(* should_compute && (stop_at_snapshot -> tick_state <> snapshot) *)
(should_compute ?reveal_builtins pvm_state
&& ((not stop_at_snapshot) || pvm_state.tick_state <> Snapshot)))
pvm_state
let update_output_buffer pvm_state level =
let output_buffer = pvm_state.buffers.Wasm.Eval.output in
if Wasm.Output_buffer.is_initialized output_buffer then
Wasm.Output_buffer.move_outbox_forward output_buffer
else Wasm.Output_buffer.initialize_outbox output_buffer level
let set_input_step input_info message pvm_state =
let open Lwt_syntax in
let open Wasm_pvm_state in
let {inbox_level; message_counter} = input_info in
let raw_level = Bounded.Non_negative_int32.to_value inbox_level in
let return ?(durable = pvm_state.durable) x = Lwt.return (durable, x) in
let return_stuck state_name =
return
(Stuck
(Wasm_pvm_errors.invalid_state
@@ Format.sprintf "No input required during %s" state_name))
in
let next_tick_state () =
match pvm_state.tick_state with
| Collect -> (
let* () =
Wasm.Input_buffer.(
enqueue
pvm_state.buffers.input
{raw_level; message_counter; payload = String.to_bytes message})
in
match Pvm_input_kind.from_raw_input message with
| Internal End_of_level -> return Padding
| Internal (Protocol_migration proto) ->
let* durable =
Durable.set_value_exn
~edit_readonly:true
pvm_state.durable
Constants.version_key
(Data_encoding.Binary.to_string_exn
Wasm_pvm_state.version_encoding
(version_for_protocol proto))
in
return ~durable Collect
| Internal Start_of_level ->
update_output_buffer pvm_state raw_level ;
return Collect
| _ -> return Collect)
| Stuck _ -> return pvm_state.tick_state
| Snapshot -> return_stuck "start"
| Decode _ -> return_stuck "decoding"
| Link _ -> return_stuck "link"
| Init _ -> return_stuck "initialization"
| Eval _ -> return_stuck "evaluation"
| Padding -> return_stuck "padding"
in
let+ durable, tick_state =
Lwt.catch next_tick_state (fun exn ->
let+ tick_state = exn_to_stuck pvm_state exn in
(pvm_state.durable, tick_state))
in
(* Increase the current tick counter and update last input *)
{
pvm_state with
durable;
tick_state;
current_tick = Z.succ pvm_state.current_tick;
last_input_info = Some input_info;
}
let get_output output_info output =
let open Lwt_syntax in
let open Wasm_pvm_state in
let {outbox_level; message_index} = output_info in
let outbox_level = Bounded.Non_negative_int32.to_value outbox_level in
let+ payload =
Wasm.Output_buffer.get_message
output
Wasm.Output_buffer.{outbox_level; message_index}
in
Bytes.to_string payload
let get_info ({current_tick; last_input_info; _} as pvm_state) =
let open Lwt_syntax in
let input_request = input_request pvm_state in
return
@@ Wasm_pvm_state.
{current_tick; last_input_read = last_input_info; input_request}
module Internal_for_tests = struct
let compute_step_many_with_hooks ?reveal_builtins ?write_debug
?after_fast_exec:_ =
compute_step_many ?reveal_builtins ?write_debug
end
![swh spinner](/static/img/swh-spinner.gif)
Computing file changes ...