wasm_exec (wasm v0.1.0)

View Source

The interpreter.

Nothing here is called directly; you arrive through wasm:call/3. Read it when you want to know why execution behaves as it does on the BEAM, or before you change the dispatch loop.

Continuation lists, not a program counter. WebAssembly control flow is structured: br N can only exit N enclosing blocks or jump back to a loop header. Nothing can jump to an arbitrary offset. So the instruction stream never needs to be flattened and there is no program counter at all. A block's body is a nested cons list, and entering one pushes a control frame holding the list to resume on exit.

This is the opposite of what a C runtime does, and the benchmarks are why. Walking a cons list measured 3.7 ns per instruction; the flattened bytecode-plus-index shape that every C interpreter uses measured 5.6 ns, because element/2 with a runtime index is a bounds-checked operation while matching a list head is a dereference the compiler turns into a jump table. Porting Wasmtime's IR shape would have cost about 35% for nothing.

Saved stack tails instead of heights. A control frame stores the operand stack list as it was on entry, not its depth. Leaving a block normally then costs nothing at all (validation guarantees the stack is already correct), and branching out is take(Arity) ++ SavedTail. Tracking an integer height would mean maintaining a counter on every push and pop.

Explicit call frames. Calls push onto frames rather than recursing through Erlang. Erlang has no first-class continuations, so recursion would make suspension impossible; explicit frames keep the whole execution state a plain term that can be captured, inspected from another process, and bounded.

Fuel. Charged at back-edges and calls only. Every unbounded execution has to pass through one or the other, so bounding those bounds everything while straight-line code pays nothing. Fuel is there for resource limits and metering, not for scheduler safety: every dispatch step here is an Erlang function call and therefore consumes a reduction, so the BEAM preempts this loop whether or not you enabled fuel. A pure-Erlang interpreter cannot monopolise a scheduler, which is precisely what a NIF-based one can.

Summary

Functions

Invoke function FuncIdx with Args.

A call out of compiled code into an interpreted function.

Invoke, converting an escaping exception into an error.

Charge one level of call depth, or raise what enter/5 raises.

Take away a budget, restoring whatever an enclosing invocation had.

Drop a passive data segment, answering the new state.

Read a global that is an inline value rather than a shared cell.

A memory load, for generated code. Bounds-checks and traps as the interpreter does.

Copy a range between two memories, or within one.

Fill a range of a memory with one byte.

Grow a memory, answering the old size and the new state.

Copy from a passive data segment into a memory.

The size of a memory in pages.

A unary operation on one operand value.

A binary operation on two operand values. Traps exactly as the interpreter does.

Install a budget for an outermost invocation, answering what to restore.

Write a global, and record the mutation so a trap does not lose it.

A vector load, for generated code.

A vector load into one lane of an existing vector.

A vector store. Writes bytes in place, so #mut{} does not change.

A memory store. Writes into atomics in place, so #mut{} does not change.

Functions

call(Inst, Mut, FuncIdx, Args, Opts)

-spec call(#inst{id :: reference(),
                 ckpt :: reference(),
                 entry_key :: undefined | reference(),
                 types :: tuple(),
                 funcs :: tuple(),
                 exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                 elems :: tuple(),
                 datas :: tuple(),
                 globaltypes :: tuple(),
                 tags :: tuple(),
                 canon :: tuple(),
                 fields :: tuple(),
                 kinds :: tuple(),
                 supers :: tuple(),
                 heap :: undefined | wasm_heap:heap(),
                 identity :: undefined | {sha256, binary()} | reference(),
                 store :: term(),
                 version :: term(),
                 limits :: map(),
                 ctx :: term()},
           #mut{globals :: tuple(),
                tables :: tuple(),
                mems :: tuple(),
                dropped_elems :: map(),
                dropped_datas :: map()},
           non_neg_integer(),
           [term()],
           map()) ->
              {ok,
               [term()],
               #mut{globals :: tuple(),
                    tables :: tuple(),
                    mems :: tuple(),
                    dropped_elems :: map(),
                    dropped_datas :: map()}}.

Invoke function FuncIdx with Args.

call_out(Inst, Mut, Idx, Args, Depth, Mod, Gen)

-spec call_out(#inst{id :: reference(),
                     ckpt :: reference(),
                     entry_key :: undefined | reference(),
                     types :: tuple(),
                     funcs :: tuple(),
                     exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                     elems :: tuple(),
                     datas :: tuple(),
                     globaltypes :: tuple(),
                     tags :: tuple(),
                     canon :: tuple(),
                     fields :: tuple(),
                     kinds :: tuple(),
                     supers :: tuple(),
                     heap :: undefined | wasm_heap:heap(),
                     identity :: undefined | {sha256, binary()} | reference(),
                     store :: term(),
                     version :: term(),
                     limits :: map(),
                     ctx :: term()},
               #mut{globals :: tuple(),
                    tables :: tuple(),
                    mems :: tuple(),
                    dropped_elems :: map(),
                    dropped_datas :: map()},
               non_neg_integer(),
               [term()],
               non_neg_integer(),
               module(),
               non_neg_integer()) ->
                  {[term()],
                   #mut{globals :: tuple(),
                        tables :: tuple(),
                        mems :: tuple(),
                        dropped_elems :: map(),
                        dropped_datas :: map()}}.

A call out of compiled code into an interpreted function.

call/5 rebuilds fuel and depth from the ?BUDGET process dictionary (init_state/4) and so does not inherit a compiled caller's depth. This takes it explicitly, which is what keeps max_depth meaning the same thing on both sides of the crossing.

Priced before it was written, in bench/paths/callcost.erl: 37.4 to 43.4 ns against the interpreter's own 43.5 to 44.7 for the same call.

call_toplevel(Inst, Mut, FuncIdx, Args, Opts)

-spec call_toplevel(#inst{id :: reference(),
                          ckpt :: reference(),
                          entry_key :: undefined | reference(),
                          types :: tuple(),
                          funcs :: tuple(),
                          exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                          elems :: tuple(),
                          datas :: tuple(),
                          globaltypes :: tuple(),
                          tags :: tuple(),
                          canon :: tuple(),
                          fields :: tuple(),
                          kinds :: tuple(),
                          supers :: tuple(),
                          heap :: undefined | wasm_heap:heap(),
                          identity :: undefined | {sha256, binary()} | reference(),
                          store :: term(),
                          version :: term(),
                          limits :: map(),
                          ctx :: term()},
                    #mut{globals :: tuple(),
                         tables :: tuple(),
                         mems :: tuple(),
                         dropped_elems :: map(),
                         dropped_datas :: map()},
                    non_neg_integer(),
                    [term()],
                    map()) ->
                       {ok,
                        [term()],
                        #mut{globals :: tuple(),
                             tables :: tuple(),
                             mems :: tuple(),
                             dropped_elems :: map(),
                             dropped_datas :: map()}}.

Invoke, converting an escaping exception into an error.

Only your own call is the outermost frame; a nested invocation lets the exception keep unwinding, which is what foreign_call/5 relies on.

check_depth(Inst, Depth)

-spec check_depth(#inst{id :: reference(),
                        ckpt :: reference(),
                        entry_key :: undefined | reference(),
                        types :: tuple(),
                        funcs :: tuple(),
                        exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                        elems :: tuple(),
                        datas :: tuple(),
                        globaltypes :: tuple(),
                        tags :: tuple(),
                        canon :: tuple(),
                        fields :: tuple(),
                        kinds :: tuple(),
                        supers :: tuple(),
                        heap :: undefined | wasm_heap:heap(),
                        identity :: undefined | {sha256, binary()} | reference(),
                        store :: term(),
                        version :: term(),
                        limits :: map(),
                        ctx :: term()},
                  non_neg_integer()) ->
                     ok.

Charge one level of call depth, or raise what enter/5 raises.

Compiled recursion runs on the Erlang stack, which grows on the process heap rather than overflowing, so without this a runaway one exhausts memory instead of trapping.

close_budget/1

-spec close_budget(term()) -> ok.

Take away a budget, restoring whatever an enclosing invocation had.

data_drop_at/3

-spec data_drop_at(#inst{id :: reference(),
                         ckpt :: reference(),
                         entry_key :: undefined | reference(),
                         types :: tuple(),
                         funcs :: tuple(),
                         exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                         elems :: tuple(),
                         datas :: tuple(),
                         globaltypes :: tuple(),
                         tags :: tuple(),
                         canon :: tuple(),
                         fields :: tuple(),
                         kinds :: tuple(),
                         supers :: tuple(),
                         heap :: undefined | wasm_heap:heap(),
                         identity :: undefined | {sha256, binary()} | reference(),
                         store :: term(),
                         version :: term(),
                         limits :: map(),
                         ctx :: term()},
                   #mut{globals :: tuple(),
                        tables :: tuple(),
                        mems :: tuple(),
                        dropped_elems :: map(),
                        dropped_datas :: map()},
                   non_neg_integer()) ->
                      #mut{globals :: tuple(),
                           tables :: tuple(),
                           mems :: tuple(),
                           dropped_elems :: map(),
                           dropped_datas :: map()}.

Drop a passive data segment, answering the new state.

global_at(Mu, I)

-spec global_at(#mut{globals :: tuple(),
                     tables :: tuple(),
                     mems :: tuple(),
                     dropped_elems :: map(),
                     dropped_datas :: map()},
                non_neg_integer()) ->
                   term().

Read a global that is an inline value rather than a shared cell.

indirect_out(Inst, Mut, TypeIdx, TableIdx, I, Args, Depth, Mod, Gen)

-spec indirect_out(#inst{id :: reference(),
                         ckpt :: reference(),
                         entry_key :: undefined | reference(),
                         types :: tuple(),
                         funcs :: tuple(),
                         exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                         elems :: tuple(),
                         datas :: tuple(),
                         globaltypes :: tuple(),
                         tags :: tuple(),
                         canon :: tuple(),
                         fields :: tuple(),
                         kinds :: tuple(),
                         supers :: tuple(),
                         heap :: undefined | wasm_heap:heap(),
                         identity :: undefined | {sha256, binary()} | reference(),
                         store :: term(),
                         version :: term(),
                         limits :: map(),
                         ctx :: term()},
                   #mut{globals :: tuple(),
                        tables :: tuple(),
                        mems :: tuple(),
                        dropped_elems :: map(),
                        dropped_datas :: map()},
                   non_neg_integer(),
                   non_neg_integer(),
                   term(),
                   [term()],
                   non_neg_integer(),
                   module(),
                   non_neg_integer()) ->
                      {[term()],
                       #mut{globals :: tuple(),
                            tables :: tuple(),
                            mems :: tuple(),
                            dropped_elems :: map(),
                            dropped_datas :: map()}}.

An indirect call out of compiled code.

indirect_target/4 already does the whole resolution -- bounds, null, and the type check against the canonical type -- and already tells a target in this instance from one in another, so it is reused rather than restated and the three traps it raises stay identical. It wants an #st{}, and the two fields it reads are the two given here.

Every target crosses back into the interpreter, including one that was compiled alongside the caller. Turning that into a local call needs a per-module dispatcher and is worth its own measurement.

init_state(Inst, Mut, Args, Opts)

-spec init_state(#inst{id :: reference(),
                       ckpt :: reference(),
                       entry_key :: undefined | reference(),
                       types :: tuple(),
                       funcs :: tuple(),
                       exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                       elems :: tuple(),
                       datas :: tuple(),
                       globaltypes :: tuple(),
                       tags :: tuple(),
                       canon :: tuple(),
                       fields :: tuple(),
                       kinds :: tuple(),
                       supers :: tuple(),
                       heap :: undefined | wasm_heap:heap(),
                       identity :: undefined | {sha256, binary()} | reference(),
                       store :: term(),
                       version :: term(),
                       limits :: map(),
                       ctx :: term()},
                 #mut{globals :: tuple(),
                      tables :: tuple(),
                      mems :: tuple(),
                      dropped_elems :: map(),
                      dropped_datas :: map()},
                 [term()],
                 map()) ->
                    #st{stack :: [term()],
                        locals :: tuple(),
                        frames :: [tuple() | toplevel],
                        inst ::
                            #inst{id :: reference(),
                                  ckpt :: reference(),
                                  entry_key :: undefined | reference(),
                                  types :: tuple(),
                                  funcs :: tuple(),
                                  exports ::
                                      #{binary() => {func | table | mem | global, non_neg_integer()}},
                                  elems :: tuple(),
                                  datas :: tuple(),
                                  globaltypes :: tuple(),
                                  tags :: tuple(),
                                  canon :: tuple(),
                                  fields :: tuple(),
                                  kinds :: tuple(),
                                  supers :: tuple(),
                                  heap :: undefined | wasm_heap:heap(),
                                  identity :: undefined | {sha256, binary()} | reference(),
                                  store :: term(),
                                  version :: term(),
                                  limits :: map(),
                                  ctx :: term()},
                        mut ::
                            #mut{globals :: tuple(),
                                 tables :: tuple(),
                                 mems :: tuple(),
                                 dropped_elems :: map(),
                                 dropped_datas :: map()},
                        fuel :: non_neg_integer() | infinity,
                        depth :: non_neg_integer(),
                        max_depth :: pos_integer(),
                        code :: undefined | {module(), non_neg_integer()}}.

load_at(Mu, M, N, Kind, Addr)

-spec load_at(#mut{globals :: tuple(),
                   tables :: tuple(),
                   mems :: tuple(),
                   dropped_elems :: map(),
                   dropped_datas :: map()},
              non_neg_integer(),
              pos_integer(),
              atom(),
              non_neg_integer()) ->
                 term().

A memory load, for generated code. Bounds-checks and traps as the interpreter does.

load_spec/1

memory_copy_at/8

-spec memory_copy_at(#mut{globals :: tuple(),
                          tables :: tuple(),
                          mems :: tuple(),
                          dropped_elems :: map(),
                          dropped_datas :: map()},
                     non_neg_integer(),
                     non_neg_integer(),
                     term(),
                     term(),
                     term(),
                     32 | 64,
                     32 | 64) ->
                        ok.

Copy a range between two memories, or within one.

memory_fill_at(Mu, M, D, B, N, W)

-spec memory_fill_at(#mut{globals :: tuple(),
                          tables :: tuple(),
                          mems :: tuple(),
                          dropped_elems :: map(),
                          dropped_datas :: map()},
                     non_neg_integer(),
                     term(),
                     term(),
                     term(),
                     32 | 64) ->
                        ok.

Fill a range of a memory with one byte.

memory_grow_at/5

-spec memory_grow_at(#inst{id :: reference(),
                           ckpt :: reference(),
                           entry_key :: undefined | reference(),
                           types :: tuple(),
                           funcs :: tuple(),
                           exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                           elems :: tuple(),
                           datas :: tuple(),
                           globaltypes :: tuple(),
                           tags :: tuple(),
                           canon :: tuple(),
                           fields :: tuple(),
                           kinds :: tuple(),
                           supers :: tuple(),
                           heap :: undefined | wasm_heap:heap(),
                           identity :: undefined | {sha256, binary()} | reference(),
                           store :: term(),
                           version :: term(),
                           limits :: map(),
                           ctx :: term()},
                     #mut{globals :: tuple(),
                          tables :: tuple(),
                          mems :: tuple(),
                          dropped_elems :: map(),
                          dropped_datas :: map()},
                     non_neg_integer(),
                     term(),
                     32 | 64) ->
                        {integer(),
                         #mut{globals :: tuple(),
                              tables :: tuple(),
                              mems :: tuple(),
                              dropped_elems :: map(),
                              dropped_datas :: map()}}.

Grow a memory, answering the old size and the new state.

memory_init_at(Inst, Mu, D, M, Da, So, N, W)

-spec memory_init_at(#inst{id :: reference(),
                           ckpt :: reference(),
                           entry_key :: undefined | reference(),
                           types :: tuple(),
                           funcs :: tuple(),
                           exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                           elems :: tuple(),
                           datas :: tuple(),
                           globaltypes :: tuple(),
                           tags :: tuple(),
                           canon :: tuple(),
                           fields :: tuple(),
                           kinds :: tuple(),
                           supers :: tuple(),
                           heap :: undefined | wasm_heap:heap(),
                           identity :: undefined | {sha256, binary()} | reference(),
                           store :: term(),
                           version :: term(),
                           limits :: map(),
                           ctx :: term()},
                     #mut{globals :: tuple(),
                          tables :: tuple(),
                          mems :: tuple(),
                          dropped_elems :: map(),
                          dropped_datas :: map()},
                     non_neg_integer(),
                     non_neg_integer(),
                     term(),
                     term(),
                     term(),
                     32 | 64) ->
                        ok.

Copy from a passive data segment into a memory.

memory_size_at(Mu, M)

-spec memory_size_at(#mut{globals :: tuple(),
                          tables :: tuple(),
                          mems :: tuple(),
                          dropped_elems :: map(),
                          dropped_datas :: map()},
                     non_neg_integer()) ->
                        non_neg_integer().

The size of a memory in pages.

op1/2

-spec op1(atom(), term()) -> term().

A unary operation on one operand value.

op2(Op, A, B)

-spec op2(atom(), term(), term()) -> term().

A binary operation on two operand values. Traps exactly as the interpreter does.

open_budget(Opts)

-spec open_budget(map()) -> term().

Install a budget for an outermost invocation, answering what to restore.

wasm:call/4 is the only caller. A nested one inherits what is already there.

set_global_at/4

-spec set_global_at(#inst{id :: reference(),
                          ckpt :: reference(),
                          entry_key :: undefined | reference(),
                          types :: tuple(),
                          funcs :: tuple(),
                          exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                          elems :: tuple(),
                          datas :: tuple(),
                          globaltypes :: tuple(),
                          tags :: tuple(),
                          canon :: tuple(),
                          fields :: tuple(),
                          kinds :: tuple(),
                          supers :: tuple(),
                          heap :: undefined | wasm_heap:heap(),
                          identity :: undefined | {sha256, binary()} | reference(),
                          store :: term(),
                          version :: term(),
                          limits :: map(),
                          ctx :: term()},
                    #mut{globals :: tuple(),
                         tables :: tuple(),
                         mems :: tuple(),
                         dropped_elems :: map(),
                         dropped_datas :: map()},
                    non_neg_integer(),
                    term()) ->
                       #mut{globals :: tuple(),
                            tables :: tuple(),
                            mems :: tuple(),
                            dropped_elems :: map(),
                            dropped_datas :: map()}.

Write a global, and record the mutation so a trap does not lose it.

The checkpoint is the reason this is a named helper rather than a setelement/3 open-coded in Core: every store mutation has to be visible to the invocation's catch, or what a trapping call wrote is silently rolled back in compiled code and kept in interpreted code.

simd_load_at(Mu, M, Op, Offset, W, N, Base)

-spec simd_load_at(#mut{globals :: tuple(),
                        tables :: tuple(),
                        mems :: tuple(),
                        dropped_elems :: map(),
                        dropped_datas :: map()},
                   non_neg_integer(),
                   atom(),
                   non_neg_integer(),
                   32 | 64,
                   pos_integer(),
                   term()) ->
                      term().

A vector load, for generated code.

simd_load_lane_at(Mu, M, Op, Offset, W, N, Lane, Base, V)

-spec simd_load_lane_at(#mut{globals :: tuple(),
                             tables :: tuple(),
                             mems :: tuple(),
                             dropped_elems :: map(),
                             dropped_datas :: map()},
                        non_neg_integer(),
                        atom(),
                        non_neg_integer(),
                        32 | 64,
                        pos_integer(),
                        non_neg_integer(),
                        term(),
                        term()) ->
                           term().

A vector load into one lane of an existing vector.

simd_store_at(Mu, M, Offset, W, Base, V)

-spec simd_store_at(#mut{globals :: tuple(),
                         tables :: tuple(),
                         mems :: tuple(),
                         dropped_elems :: map(),
                         dropped_datas :: map()},
                    non_neg_integer(),
                    non_neg_integer(),
                    32 | 64,
                    term(),
                    term()) ->
                       ok.

A vector store. Writes bytes in place, so #mut{} does not change.

simd_store_lane_at(Mu, M, Op, Offset, W, Lane, Base, V)

-spec simd_store_lane_at(#mut{globals :: tuple(),
                              tables :: tuple(),
                              mems :: tuple(),
                              dropped_elems :: map(),
                              dropped_datas :: map()},
                         non_neg_integer(),
                         atom(),
                         non_neg_integer(),
                         32 | 64,
                         non_neg_integer(),
                         term(),
                         term()) ->
                            ok.

A store of one lane of a vector.

store_at(Mu, M, N, Kind, Addr, Value)

-spec store_at(#mut{globals :: tuple(),
                    tables :: tuple(),
                    mems :: tuple(),
                    dropped_elems :: map(),
                    dropped_datas :: map()},
               non_neg_integer(),
               pos_integer(),
               atom(),
               non_neg_integer(),
               term()) ->
                  ok.

A memory store. Writes into atomics in place, so #mut{} does not change.

store_spec/1