wasm_exec (wasm v0.3.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.

Where things are

2,400 lines. Three functions carry almost all of it, and AGENTS.md singles them out because three separate changes to them have each cost about 70% on QuickJS while the synthetic loop measured nothing:

functionwhat it is
run/3the dispatch loop. One clause per IR instruction, in %%% dispatch
branch/3control flow: taking a label, unwinding to it, in %%% branches
do_call/4the call trampoline, in %%% calls

Anything you add on those three goes through bench/paths/realbench.erl on QuickJS, five interleaved pairs, both orderings, before you believe it.

The rest by banner: %%% call entry is where an invocation starts and where generated code is entered; %%% bounded operand cache is the operand stack; %%% operations, for generated code holds the helpers wasm_core emits calls to, so the interpreted and compiled paths cannot disagree; %%% exceptions, %%% gc, %%% simd memory and %%% bulk memory are the proposals.

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.

Call a function this generated module does not hold but a sibling does.

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(),
                 module_handle :: undefined | wasm_module_cache:handle(),
                 leases :: undefined | atomics:atomics_ref(),
                 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(),
                     module_handle :: undefined | wasm_module_cache:handle(),
                     leases :: undefined | atomics:atomics_ref(),
                     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(),
                          module_handle :: undefined | wasm_module_cache:handle(),
                          leases :: undefined | atomics:atomics_ref(),
                          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(),
                        module_handle :: undefined | wasm_module_cache:handle(),
                        leases :: undefined | atomics:atomics_ref(),
                        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(),
                         module_handle :: undefined | wasm_module_cache:handle(),
                         leases :: undefined | atomics:atomics_ref(),
                         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(),
                         module_handle :: undefined | wasm_module_cache:handle(),
                         leases :: undefined | atomics:atomics_ref(),
                         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(),
                       module_handle :: undefined | wasm_module_cache:handle(),
                       leases :: undefined | atomics:atomics_ref(),
                       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(),
                                  module_handle :: undefined | wasm_module_cache:handle(),
                                  leases :: undefined | atomics:atomics_ref(),
                                  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(),
                           module_handle :: undefined | wasm_module_cache:handle(),
                           leases :: undefined | atomics:atomics_ref(),
                           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(),
                           module_handle :: undefined | wasm_module_cache:handle(),
                           leases :: undefined | atomics:atomics_ref(),
                           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(),
                          module_handle :: undefined | wasm_module_cache:handle(),
                          leases :: undefined | atomics:atomics_ref(),
                          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.

shard_call(Inst, Mut, Idx, Args, Depth, Target, Head, Gen)

-spec shard_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(),
                       module_handle :: undefined | wasm_module_cache:handle(),
                       leases :: undefined | atomics:atomics_ref(),
                       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(),
                 module(),
                 non_neg_integer() | binary()) ->
                    {[term()],
                     #mut{globals :: tuple(),
                          tables :: tuple(),
                          mems :: tuple(),
                          dropped_elems :: map(),
                          dropped_datas :: map()}}.

Call a function this generated module does not hold but a sibling does.

One wasm module may be compiled into several generated ones, and a call between them is a call, not a crossing: which unit holds which function was decided before either was generated, so the name is a literal exactly as the caller's own is. Going out through the interpreter and back in through the head of the chain instead measured 268.5 ms against 174.4 on QuickJS.

{error, _} means the sibling's slot was refilled between generation and now, which its stamp check catches. The crossing is the right answer to that, and it is the same one a caller with no sibling at all gets.

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