defmodule AlchemyVM.Executor do alias AlchemyVM.Frame alias AlchemyVM.Memory alias AlchemyVM.Gas alias AlchemyVM.HostFunction.API use Bitwise use AlchemyVM.DSL require IEx alias Decimal, as: D @moduledoc false # Reference for tests being used: https://github.com/WebAssembly/wabt/tree/master/test defp typecast_param({:i32, param}), do: <> defp typecast_param({:i64, param}), do: <> defp typecast_param({:f32, param}), do: <> defp typecast_param({:f64, param}), do: <> def create_frame_and_execute(vm, addr, gas_limit, opts, gas \\ 0, stack \\ [], parameters \\ []) do case elem(vm.store.funcs, addr) do {{inputs, outputs}, module_ref, instr, locals} -> {args, stack} = if length(parameters) > 0 do args = inputs |> Tuple.to_list() |> Enum.zip(parameters) |> Enum.map(&typecast_param/1) {args, stack} else Enum.split(stack, tuple_size(inputs)) end %{^module_ref => module} = vm.modules frame = %Frame{ module: module, instructions: instr, locals: List.to_tuple(args ++ locals), gas_limit: gas_limit } total_instr = map_size(instr) {outputs, execute(frame, vm, gas, stack, total_instr, gas_limit, opts)} {:hostfunc, {inputs, outputs}, mname, fname, module_ref} -> {args, stack} = if length(parameters) > 0 do args = inputs |> Tuple.to_list() |> Enum.zip(parameters) |> Enum.map(&typecast_param/1) {args, stack} else Enum.split(stack, tuple_size(inputs)) end %{^module_ref => module} = vm.modules func = module.resolved_imports |> Map.get(mname) |> Map.get(fname) # Start an API agent that isolates VM state until the host function # finishes running. {:ok, ctx} = API.start_link(vm) return_val = apply(func, [ctx, args]) # Get updated state from the API agent vm = API.state(ctx) # Kill the API agent now that it's served it's purpose API.stop(ctx) # TODO: How should we handle gas for host functions? Does gas price # get passed in? Do we default to a gas value? Gas needs to be updated # instead of just getting passed through if !is_binary(return_val) do {outputs, {vm, gas, stack}} else {outputs, {vm, gas, [return_val | stack]}} end end end # What happens is we pass in the main limit for the gas & the gas_limit, # then every iteration before we procedd we check the gas limit and the # returned op_gas (gas accumulted from executing that opcode) # Example List Options [trace: false] def execute(frame, vm, gas, stack, total_instr, gas_limit, opts, next_instr \\ 0) def execute(_frame, vm, gas, stack, _total, gas_limit, opts, _next) when gas_limit != :infinity and gas > gas_limit, do: IEx.pry #{:error, :reached_gas_limit} def execute(_frame, vm, gas, stack, total_instr, _gas_limit, _opts, next_instr) when next_instr >= total_instr or next_instr < 0, do: {vm, gas, stack} def execute(frame, vm, gas, stack, total_instr, gas_limit, opts, next_instr) do %{^next_instr => instr} = frame.instructions {{frame, vm, next_instr}, gas, stack} = instruction({frame, vm, next_instr}, gas, stack, opts, instr) if opts[:trace] do write_to_file(instr, gas) end execute(frame, vm, gas, stack, total_instr, gas_limit, opts, next_instr + 1) end # Begin i32 Instructions ===================================================== defop i32_const(immediates: [i32]) do {ctx, gas + Gas.cost(:i32_const), [<> | stack]} end defop i32_add(<>, <>) do {ctx, gas + Gas.cost(:i32_add), [<<(a + b)::integer-32-little>> | stack]} end defop i32_sub(<>, <>) do {ctx, gas + Gas.cost(:i32_sub), [<<(a - b)::integer-32-little>> | stack]} end defop i32_mul(<>, <>) do {ctx, gas + Gas.cost(:i32_mul), [<<(a * b)::integer-32-little>> | stack]} end defop i32_div_s(<>, <>) do if b == 0, do: trap("Divide by zero in i32.div_s") if a / b >= 2147483648, do: trap("Out of bounds in i32.div_s") res = <> {ctx, gas + Gas.cost(:i32_div_s), [res | stack]} end defop i32_div_u(<>, <>) do if b == 0, do: trap("Divide by zero in i32.div_s") res = <> {ctx, gas + Gas.cost(:i32_div_u), [res | stack]} end defop i32_rem_s(<>, <>) do if b == 0, do: trap("Divide by zero in i32.rem_s") {ctx, gas + Gas.cost(:i32_rem_s), [<> | stack]} end defop i32_rem_u(<>, <>) do if b == 0, do: trap("Divide by zero in i32.rem_u") {ctx, gas + Gas.cost(:i32_rem_u), [<> | stack]} end defop i32_rotl(<>, <>) do {ctx, gas + Gas.cost(:i32_rotl), [<> | stack]} end defop i32_rotr(<>, <>) do {ctx, gas + Gas.cost(:i32_rotr), [<> | stack]} end defop i32_and(<>, <>) do {ctx, gas + Gas.cost(:i32_and), [<<(a &&& b)::integer-32-little>> | stack]} end defop i32_or(<>, <>) do {ctx, gas + Gas.cost(:i32_or), [<<(a ||| b)::integer-32-little>> | stack]} end defop i32_xor(<>, <>) do {ctx, gas + Gas.cost(:i32_xor), [<> | stack]} end defop i32_shl(<>, <>) do {ctx, gas + Gas.cost(:i32_shl), [<<(a <<< b)::integer-32-little>> | stack]} end defop i32_shr_u(<>, <>) do {ctx, gas + Gas.cost(:i32_shr_u), [<<(a >>> b)::integer-32-little>> | stack]} end defop i32_shr_s(<>, <>) do {ctx, gas + Gas.cost(:i32_shr_s), [<<(a >>> b)::integer-32-little-signed>> | stack]} end defop i32_eq(a, b) do result = if a === b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_eq), [result | stack]} end defop i32_ne(a, b) do result = if a !== b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_ne), [result | stack]} end defop i32_eqz(a) do result = if a === <<0, 0, 0, 0>>, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_eqz), [result | stack]} end defop i32_lt_u(<>, <>) do result = if a < b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_lt_u), [result | stack]} end defop i32_gt_u(<>, <>) do result = if a > b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_gt_u), [result | stack]} end defop i32_le_u(<>, <>) do result = if a <= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_le_u), [result | stack]} end defop i32_ge_u(<>, <>) do result = if a >= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_ge_u), [result | stack]} end defop i32_le_s(<>, <>) do result = if a <= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_le_s), [result | stack]} end defop i32_ge_s(<>, <>) do result = if a >= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_ge_s), [result | stack]} end defop i32_lt_s(<>, <>) do result = if a < b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_lt_s), [result | stack]} end defop i32_gt_s(<>, <>) do result = if a > b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i32_gt_s), [result | stack]} end defop i32_popcnt(i32) do count = (for <>, do: bit) |> Enum.reject(& &1 !== 1) |> length() {ctx, gas + Gas.cost(:i32_popcnt, count), [<> | stack]} end defop i32_ctz(i32) do num_zeros = (for <>, do: bit) |> trailing_zeros() {ctx, gas + Gas.cost(:i32_ctz, num_zeros), [<> | stack]} end defop i32_clz(i32) do num_zeros = (for <>, do: bit) |> leading_zeros() {ctx, gas + Gas.cost(:i32_clz, num_zeros), [<> | stack]} end defop i32_load(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx # TODO: Should this be the first memory in the module? Can this reference an imported memory? mem_addr = hd(frame.module.memaddrs) i32 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 4) {ctx, gas + Gas.cost(:i32_load), [i32 | stack]} end defop i32_load8_s(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx # TODO: Should this be the first memory in the module? Can this reference an imported memory? mem_addr = hd(frame.module.memaddrs) i8bin = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 1) <> = i8bin sign = if i8 >= 0, do: 0, else: 255 {ctx, gas + Gas.cost(:i32_load8_s), [i8bin <> <> | stack]} end defop i32_load16_s(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx # TODO: Should this be the first memory in the module? Can this reference an imported memory? mem_addr = hd(frame.module.memaddrs) i16bin = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 2) <> = i16bin sign = if i16 >= 0, do: 0, else: 255 {ctx, gas + Gas.cost(:i32_load16_s), [i16bin <> <> | stack]} end defop i32_load8_u(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx # TODO: Should this be the first memory in the module? Can this reference an imported memory? mem_addr = hd(frame.module.memaddrs) i8 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 1) {ctx, gas + Gas.cost(:i32_load8_u), [i8 <> <<0, 0, 0>> | stack]} end defop i32_load16_u(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx # TODO: Should this be the first memory in the module? Can this reference an imported memory? mem_addr = hd(frame.module.memaddrs) i16 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 2) {ctx, gas + Gas.cost(:i32_load16_u), [i16 <> <<0, 0>> | stack]} end defop i32_store(value, <>, immediates: [_align, offset]) do {frame, vm, n} = ctx # TODO: Should this be the first memory in the module? Can this reference an imported memory? mem_addr = hd(frame.module.memaddrs) mem = vm.store.mems |> Enum.at(mem_addr) |> Memory.put_at(i32addr + offset, value) store_mems = List.replace_at(vm.store.mems, mem_addr, mem) store = Map.put(vm.store, :mems, store_mems) {{frame, Map.put(vm, :store, store), n}, gas + Gas.cost(:i32_store), stack} end defop i32_store8(value, <>, immediates: [_align, offset]) do {frame, vm, n} = ctx # TODO: Should this be the first memory in the module? Can this reference an imported memory? mem_addr = hd(frame.module.memaddrs) # Value is little endian, so grabbing the first byte is effectively wrapping <> = value mem = vm.store.mems |> Enum.at(mem_addr) |> Memory.put_at(i32addr + offset, i8) store_mems = List.replace_at(vm.store.mems, mem_addr, mem) store = Map.put(vm.store, :mems, store_mems) {{frame, Map.put(vm, :store, store), n}, gas + Gas.cost(:i32_store8), stack} end defop i32_store16(value, <>, immediates: [_align, offset]) do {frame, vm, n} = ctx # TODO: Should this be the first memory in the module? Can this reference an imported memory? mem_addr = hd(frame.module.memaddrs) # Value is little endian, so grabbing the first 2 bytes is effectively wrapping <> = value mem = vm.store.mems |> Enum.at(mem_addr) |> Memory.put_at(i32addr + offset, i16) store_mems = List.replace_at(vm.store.mems, mem_addr, mem) store = Map.put(vm.store, :mems, store_mems) {{frame, Map.put(vm, :store, store), n}, gas + Gas.cost(:i32_store16), stack} end defop i32_trunc_u_f32(<>) do {ctx, gas + Gas.cost(:i32_trunc_u_f32), [<> | stack]} end defop i32_trunc_s_f32(<>) do {ctx, gas + Gas.cost(:i32_trunc_s_f32), [<> | stack]} end defop i32_trunc_u_f64(<>) do {ctx, gas + Gas.cost(:i32_trunc_u_f32), [<> | stack]} end defop i32_trunc_s_f64(<>) do {ctx, gas + Gas.cost(:i32_trunc_s_f64), [<> | stack]} end # We don't actually need to do anything here, the value is already in binary, # we'll just read it in as a float in the next instruction that uses this value. defop i32_reinterpret_f32 do {ctx, gas + Gas.cost(:i32_reinterpret_f32), stack} end defop i32_wrap_i64(<>) do {ctx, gas + Gas.cost(:i32_wrap_i64), [i32 | stack]} end # End i32 Instructions ======================================================= # Begin i64 Instructions ===================================================== defop i64_const(immediates: [i64]) do {ctx, gas + Gas.cost(:i64_const), [<> | stack]} end defop i64_add(<>, <>) do {ctx, gas + Gas.cost(:i64_add), [<<(a + b)::integer-64-little>> | stack]} end defop i64_sub(<>, <>) do {ctx, gas + Gas.cost(:i64_sub), [<<(a - b)::integer-64-little>> | stack]} end defop i64_mul(<>, <>) do {ctx, gas + Gas.cost(:i64_mul), [<<(a * b)::integer-64-little>> | stack]} end defop i64_div_s(<>, <>) do if b == 0, do: trap("Divide by zero in i64.div_s") if a / b == 9.223372036854776e18, do: trap("Out of bounds in i64.div_s") {ctx, gas + Gas.cost(:i64_div_s), [<> | stack]} end defop i64_div_u(<>, <>) do if b == 0, do: trap("Divide by zero in i64.div_u") {ctx, gas + Gas.cost(:i64_div_u), [<> | stack]} end defop i64_rem_s(<>, <>) do if b == 0, do: trap("Divide by zero in i64.rem_s") {ctx, gas + Gas.cost(:i64_rem_s), [<> | stack]} end defop i64_rem_u(<>, <>) do if b == 0, do: trap("Divide by zero in i64.rem_u") {ctx, gas + Gas.cost(:i64_rem_u), [<> | stack]} end defop i64_rotl(<>, <>) do {ctx, gas + Gas.cost(:i64_rotl), [<> | stack]} end defop i64_rotr(<>, <>) do {ctx, gas + Gas.cost(:i64_rotr), [<> | stack]} end defop i64_and(<>, <>) do {ctx, gas + Gas.cost(:i64_and), [<<(a &&& b)::integer-64-little>> | stack]} end defop i64_or(<>, <>) do {ctx, gas + Gas.cost(:i64_or), [<<(a ||| b)::integer-64-little>> | stack]} end defop i64_xor(<>, <>) do {ctx, gas + Gas.cost(:i64_xor), [<> | stack]} end defop i64_shl(<>, <>) do {ctx, gas + Gas.cost(:i64_shl), [<<(a <<< b)::integer-64-little>> | stack]} end defop i64_shr_u(<>, <>) do {ctx, gas + Gas.cost(:i64_shr_u), [<<(a >>> b)::integer-64-little>> | stack]} end defop i64_shr_s(<>, <>) do {ctx, gas + Gas.cost(:i64_shr_s), [<<(a >>> b)::integer-64-little-signed>> | stack]} end defop i64_eq(b, a) do result = if a === b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_eq), [result | stack]} end defop i64_ne(b, a) do result = if a !== b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_eq), [result | stack]} end defop i64_eqz(<>) do result = if a === 0, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_eqz), [result| stack]} end defop i64_lt_u(<>, <>) do result = if a < b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_lt_u), [result | stack]} end defop i64_gt_u(<>, <>) do result = if a > b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_gt_u), [result | stack]} end defop i64_le_u(<>, <>) do result = if a <= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_le_u), [result | stack]} end defop i64_ge_u(<>, <>) do result = if a >= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_ge_u), [result | stack]} end defop i64_le_s(<>, <>) do result = if a <= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_le_s), [result | stack]} end defop i64_ge_s(<>, <>) do result = if a >= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_ge_s), [result | stack]} end defop i64_lt_s(<>, <>) do result = if a < b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_lt_s), [result | stack]} end defop i64_gt_s(<>, <>) do result = if a > b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:i64_gt_s), [result | stack]} end defop i64_popcnt(i64) do count = (for <>, do: bit) |> Enum.reject(& &1 !== 1) |> length() {ctx, gas + Gas.cost(:i64_popcnt, count), [<> | stack]} end defop i64_clz(i64) do num_zeros = (for <>, do: bit) |> leading_zeros() {ctx, gas + Gas.cost(:i64_clz, num_zeros), [<> | stack]} end defop i64_ctz(i64) do num_zeros = (for <>, do: bit) |> trailing_zeros() {ctx, gas + Gas.cost(:i64_ctz, num_zeros), [<> | stack]} end defop i64_load(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx mem_addr = hd(frame.module.memaddrs) i64 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 8) {ctx, gas + Gas.cost(:i64_load), [i64 | stack]} end defop i64_load8_s(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx mem_addr = hd(frame.module.memaddrs) i8 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 1) sign = if i8 >= 0, do: 0, else: 255 {ctx, gas + Gas.cost(:i64_load8_s), [i8 <> <> | stack]} end defop i64_load16_s(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx mem_addr = hd(frame.module.memaddrs) i16 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 2) sign = if i16 >= 0, do: 0, else: 255 {ctx, gas + Gas.cost(:i64_load16_s), [i16 <> <> | stack]} end defop i64_load32_s(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx mem_addr = hd(frame.module.memaddrs) i32 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 4) sign = if i32 >= 0, do: 0, else: 255 {ctx, gas + Gas.cost(:i64_load32_s), [i32 <> <> | stack]} end defop i64_load8_u(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx mem_addr = hd(frame.module.memaddrs) i8 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 1) {ctx, gas + Gas.cost(:i64_load8_u), [i8 <> <<0, 0, 0, 0, 0, 0, 0>> | stack]} end defop i64_load16_u(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx mem_addr = hd(frame.module.memaddrs) i16 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 2) {ctx, gas + Gas.cost(:i64_load16_u), [i16 <> <<0, 0, 0, 0, 0, 0>> | stack]} end defop i64_load32_u(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx mem_addr = hd(frame.module.memaddrs) i32 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 4) {ctx, gas + Gas.cost(:i64_load32_u), [i32 <> <<0, 0, 0, 0>> | stack]} end defop i64_store(value, <>, immediates: [_align, offset]) do {frame, vm, n} = ctx mem_addr = hd(frame.module.memaddrs) mem = vm.store.mems |> Enum.at(mem_addr) |> Memory.put_at(i32addr + offset, value) store_mems = List.replace_at(vm.store.mems, mem_addr, mem) store = Map.put(vm.store, :mems, store_mems) {{frame, Map.put(vm, :store, store), n}, gas + Gas.cost(:i64_store), stack} end defop i64_store8(value, <>, immediates: [_align, offset]) do {frame, vm, n} = ctx mem_addr = hd(frame.module.memaddrs) <> = value mem = vm.store.mems |> Enum.at(mem_addr) |> Memory.put_at(i32addr + offset, i8) store_mems = List.replace_at(vm.store.mems, mem_addr, mem) store = Map.put(vm.store, :mems, store_mems) {{frame, Map.put(vm, :store, store), n}, gas + Gas.cost(:i64_store8), stack} end defop i64_store16(value, <>, immediates: [_align, offset]) do {frame, vm, n} = ctx mem_addr = hd(frame.module.memaddrs) <> = value mem = vm.store.mems |> Enum.at(mem_addr) |> Memory.put_at(i32addr + offset, i16) store_mems = List.replace_at(vm.store.mems, mem_addr, mem) store = Map.put(vm.store, :mems, store_mems) {{frame, Map.put(vm, :store, store), n}, gas + Gas.cost(:i64_store16), stack} end defop i64_store32(value, <>, immediates: [_align, offset]) do {frame, vm, n} = ctx mem_addr = hd(frame.module.memaddrs) <> = value mem = vm.store.mems |> Enum.at(mem_addr) |> Memory.put_at(i32addr + offset, i32) store_mems = List.replace_at(vm.store.mems, mem_addr, mem) store = Map.put(vm.store, :mems, store_mems) {{frame, Map.put(vm, :store, store), n}, gas + Gas.cost(:i64_store32), stack} end defop i64_trunc_u_f32(<>) do {ctx, gas + Gas.cost(:i64_trunc_u_f32), [<> | stack]} end defop i64_trunc_s_f32(<>) do {ctx, gas + Gas.cost(:i64_trunc_s_f32), [<> | stack]} end defop i64_trunc_u_f64(<>) do {ctx, gas + Gas.cost(:i64_trunc_u_f64), [<> | stack]} end defop i64_trunc_s_f64(<>) do {ctx, gas + Gas.cost(:i64_trunc_s_f64), [<> | stack]} end defop i64_extend_u_i32(i32) do {ctx, gas + Gas.cost(:i64_extend_u_i32), [i32 <> <<0, 0, 0, 0>> | stack]} end defop i64_extend_s_i32(i32a) do <> = i32a sign = if i32 >= 0, do: 0, else: 255 {ctx, gas + Gas.cost(:i64_extend_s_i32), [i32a <> <> | stack]} end defop i64_reinterpret_f64 do {ctx, gas + Gas.cost(:i64_reinterpret_f32), stack} end # End i64 Instructions ======================================================= # Begin f32 Instructions ===================================================== defop f32_const(immediates: [f32]) do {ctx, gas + Gas.cost(:f32_const), [<> | stack]} end defop f32_lt(<>, <>) do result = if a < b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f32_lt), [result | stack]} end defop f32_le(<>, <>) do result = if a <= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f32_le), [result | stack]} end defop f32_ge(<>, <>) do result = if a >= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f32_ge), [result | stack]} end defop f32_gt(<>, <>) do result = if a > b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f32_gt), [result | stack]} end defop f32_add(<>, <>) do {ctx, gas + Gas.cost(:f32_add), [<<(a + b)::float-32-little>> | stack]} end defop f32_sub(<>, <>) do {ctx, gas + Gas.cost(:f32_sub), [<<(a - b)::float-32-little>> | stack]} end defop f32_mul(<>, <>) do {ctx, gas + Gas.cost(:f32_mul), [<<(a * b)::float-32-little>> | stack]} end defop f32_div(<>, <>) do if b == 0 do trap("Divide by zero in f32.div") end {ctx, gas + Gas.cost(:f32_div), [<<(a / b)::float-32-little>> | stack]} end defop f32_sqrt(<>) do {ctx, gas + Gas.cost(:f32_sqrt), [<<:math.sqrt(a)::float-32-little>> | stack]} end defop f32_nearest(<>) do {ctx, gas + Gas.cost(:f32_nearest), [<> | stack]} end defop f32_trunc(<>) do {ctx, gas + Gas.cost(:f32_trunc), [<> | stack]} end defop f32_floor(<>) do {ctx, gas + Gas.cost(:f32_floor), [<> | stack]} end defop f32_ceil(<>) do {ctx, gas + Gas.cost(:f32_ceil), [<> | stack]} end defop f32_neg(<>) do result = if a == 0.0, do: 0.0, else: a * -1 {ctx, gas + Gas.cost(:f32_neg), [<> | stack]} end defop f32_abs(<>) do {ctx, gas + Gas.cost(:f32_abs), [<> | stack]} end defop f32_min(<>, <>) do {ctx, gas + Gas.cost(:f32_min), [<> | stack]} end defop f32_max(<>, <>) do {ctx, gas + Gas.cost(:f32_max), [<> | stack]} end defop f32_load(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx mem_addr = hd(frame.module.memaddrs) f32 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 4) {ctx, gas + Gas.cost(:f32_load), [f32 | stack]} end defop f32_store(value, <>, immediates: [_align, offset]) do {frame, vm, n} = ctx mem_addr = hd(frame.module.memaddrs) mem = vm.store.mems |> Enum.at(mem_addr) |> Memory.put_at(i32addr + offset, value) store_mems = List.replace_at(vm.store.mems, mem_addr, mem) store = Map.put(vm.store, :mems, store_mems) {{frame, Map.put(vm, :store, store), n}, gas + Gas.cost(:f32_store), stack} end defop f32_eq(a, b) do result = if a === b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f32_eq), [result | stack]} end defop f32_ne(a, b) do result = if a !== b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f32_eq), [result | stack]} end defop f32_copysign(<>, <>) do magnitude = abs(a) sign = if b >= 0, do: 1, else: -1 result = magnitude * sign # This needs to be here because of a weird bug (?) where 0.0 * -1 would be # <<0, 0, 0, 128>> instead of <<0, 0, 0, 0>>, even though both were 0.0 result = if result == 0.0, do: 0.0, else: result {ctx, gas + Gas.cost(:f32_copysign), [<> | stack]} end defop f32_convert_s_i32(<>) do {ctx, gas + Gas.cost(:f32_convert_s_i32), [<<(a * 1.0)::float-32-little>> | stack]} end defop f32_convert_u_i32(<>) do {ctx, gas + Gas.cost(:f32_convert_u_i32), [<<(a * 1.0)::float-32-little>> | stack]} end defop f32_convert_s_i64(<>) do {ctx, gas + Gas.cost(:f32_convert_s_i64), [<<(a * 1.0)::float-32-little>> | stack]} end defop f32_convert_u_i64(<>) do {ctx, gas + Gas.cost(:f32_convert_u_i64), [<<(a * 1.0)::float-32-little>> | stack]} end # TODO: Revisit this -- it's a naive solution that has a few issues (can # break with very large numbers) defop f32_demote_f64(<>) do {ctx, gas + Gas.cost(:f32_demote_f64), [<> | stack]} end defop f32_reinterpret_i32(a) do {ctx, gas + Gas.cost(:f32_reinterpret_i32), [a | stack]} end # End f32 Instructions ======================================================= # Begin f64 Instructions ===================================================== defop f64_const(immediates: [f64]) do {ctx, gas + Gas.cost(:f64_const), [<> | stack]} end defop f64_add(<>, <>) do {ctx, gas + Gas.cost(:f64_add), [<<(a + b)::float-64-little>> | stack]} end defop f64_sub(<>, <>) do {ctx, gas + Gas.cost(:f64_sub), [<<(a - b)::float-64-little>> | stack]} end defop f64_mul(<>, <>) do {ctx, gas + Gas.cost(:f64_mul), [<<(a * b)::float-64-little>> | stack]} end defop f64_min(<>, <>) do {ctx, gas + Gas.cost(:f64_min), [<> | stack]} end defop f64_max(<>, <>) do {ctx, gas + Gas.cost(:f64_max), [<> | stack]} end defop f64_lt(<>, <>) do result = if a < b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f64_lt), [result | stack]} end defop f64_le(<>, <>) do result = if a <= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f64_le), [result | stack]} end defop f64_ge(<>, <>) do result = if a >= b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f64_ge), [result | stack]} end defop f64_gt(<>, <>) do result = if a > b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f64_gt), [result | stack]} end defop f64_store(value, <>, immediates: [_align, offset]) do {frame, vm, n} = ctx mem_addr = hd(frame.module.memaddrs) mem = vm.store.mems |> Enum.at(mem_addr) |> Memory.put_at(i32addr + offset, value) store_mems = List.replace_at(vm.store.mems, mem_addr, mem) store = Map.put(vm.store, :mems, store_mems) {{frame, Map.put(vm, :store, store), n}, gas + Gas.cost(:f64_store), stack} end defop f64_load(<>, immediates: [_align, offset]) do {frame, vm, _n} = ctx mem_addr = hd(frame.module.memaddrs) f64 = vm.store.mems |> Enum.at(mem_addr) |> Memory.get_at(i32addr + offset, 8) {ctx, gas + Gas.cost(:f64_load), [f64 | stack]} end defop f64_eq(a, b) do result = if a === b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f64_eq), [result | stack]} end defop f64_ne(a, b) do result = if a !== b, do: <<1, 0, 0, 0>>, else: <<0, 0, 0, 0>> {ctx, gas + Gas.cost(:f64_ne), [result | stack]} end defop f64_copysign(<>, <>) do magnitude = abs(a) sign = if b >= 0, do: 1, else: -1 result = if magnitude == 0.0, do: 0.0, else: magnitude * sign {ctx, gas + Gas.cost(:f64_copysign), [<> | stack]} end defop f64_nearest(<>) do {ctx, gas + Gas.cost(:f64_nearest), [<> | stack]} end defop f64_trunc(<>) do {ctx, gas + Gas.cost(:f64_trunc), [<> | stack]} end defop f64_floor(<>) do {ctx, gas + Gas.cost(:f64_floor), [<> | stack]} end defop f64_neg(<>) do result = if a == 0.0, do: 0.0, else: a * -1 {ctx, gas + Gas.cost(:f64_neg), [<> | stack]} end defop f64_abs(<>) do {ctx, gas + Gas.cost(:f64_abs), [<> | stack]} end defop f64_sqrt(<>) do {ctx, gas + Gas.cost(:f64_sqrt), [<<:math.sqrt(a)::float-64-little>> | stack]} end defop f64_ceil(<>) do {ctx, gas + Gas.cost(:f64_ceil), [<> | stack]} end defop f64_convert_s_i64(<>) do {ctx, gas + Gas.cost(:f32_convert_s_i64), [<> | stack]} end defop f64_convert_u_i64(<>) do {ctx, gas + Gas.cost(:f32_convert_u_i64), [<> | stack]} end defop f64_convert_s_i32(<>) do {ctx, gas + Gas.cost(:f32_convert_s_i32), [<> | stack]} end defop f64_convert_u_i32(<>) do {ctx, gas + Gas.cost(:f64_convert_u_i32), [<> | stack]} end defop f64_promote_f32(<>) do {ctx, gas + Gas.cost(:f64_promote_f32), [<> | stack]} end defop f64_reinterpret_i64(a) do {ctx, gas + Gas.cost(:f64_reinterpret_i64), [a | stack]} end # End f64 Instructions ======================================================= # Begin Type Agnostic Instructions =========================================== defop call(immediates: [funcidx]) do {frame, vm, n} = ctx %{^funcidx => func_addr} = frame.module.funcaddrs # TODO: Maybe this shouldn't pass the existing stack in? {_outputs, {vm, gas, stack}} = create_frame_and_execute(vm, func_addr, frame.gas_limit, opts, gas, stack) {{frame, vm, n}, gas + Gas.cost(:call), stack} end defop set_global(value, immediates: [idx]) do {frame, vm, n} = ctx globals = List.replace_at(vm.globals, idx, value) {{frame, Map.put(vm, :globals, globals), n}, gas + Gas.cost(:set_global), stack} end defop set_local(value, immediates: [idx]) do {frame, vm, n} = ctx locals = put_elem(frame.locals, idx, value) {{Map.put(frame, :locals, locals), vm, n}, gas + Gas.cost(:set_local), stack} end defop get_local(immediates: [idx]) do {frame, _vm, _n} = ctx {ctx, gas + Gas.cost(:get_local), [elem(frame.locals, idx) | stack]} end defop get_global(immediates: [idx]) do {_frame, vm, _n} = ctx {ctx, gas + Gas.cost(:get_global), [Enum.at(vm.globals, idx) | stack]} end defop tee_local(immediates: [idx]) do {frame, vm, n} = ctx [value | _] = stack locals = put_elem(frame.locals, idx, value) {{Map.put(frame, :locals, locals), vm, n}, gas + Gas.cost(:tee_local), stack} end defop grow_memory(<>) do {frame, vm, n} = ctx memory = Memory.grow(vm.memory, pages) vm = Map.put(vm, :memory, memory) {{frame, vm, n}, gas + Gas.cost(:grow_memory), [length(vm.memory) | stack]} end defop current_memory do {_frame, vm, _n} = ctx {ctx, gas + Gas.cost(:current_memory), [length(vm.memory.pages) | stack]} end defop loop(immediates: [_result_type]) do {frame, vm, n} = ctx labels = [{n, n} | frame.labels] snapshots = [stack | frame.snapshots] frame = Map.merge(frame, %{labels: labels, snapshots: snapshots}) {{frame, vm, n}, gas + Gas.cost(:loop), stack} end defop block(immediates: [_result_type, end_idx]) do {frame, vm, n} = ctx labels = [{n, end_idx - 1} | frame.labels] snapshots = [stack | frame.snapshots] frame = Map.merge(frame, %{labels: labels, snapshots: snapshots}) {{frame, vm, n}, gas + Gas.cost(:block), stack} end defop select(condition, b, a) do stack = if condition == <<1, 0, 0, 0>>, do: [a | stack], else: [b | stack] {ctx, gas + Gas.cost(:select), stack} end defop br_if(condition, immediates: [label_idx]) do if condition == <<1, 0, 0, 0>> do break_to(ctx, gas + Gas.cost(:br_if), stack, label_idx) else {ctx, gas + Gas.cost(:br_if), stack} end end defop drop(_) do {ctx, gas + Gas.cost(:drop), stack} end defop br(immediates: [label_idx]) do break_to(ctx, gas + Gas.cost(:br), stack, label_idx) end defop return do {frame, vm, _n} = ctx {{frame, vm, -10}, gas + Gas.cost(:return), stack} end defop unreachable do {ctx, gas + Gas.cost(:unreachable), stack} end defop nop do {ctx, gas + Gas.cost(:nop), stack} end defp instruction({frame, vm, n}, gas, [<<1, 0, 0, 0>> | stack], _opts, {:if, _type, _else_idx, end_idx}) do labels = [{n, end_idx} | frame.labels] snapshots = [stack | frame.snapshots] frame = Map.merge(frame, %{labels: labels, snapshots: snapshots}) {{frame, vm, n}, gas + Gas.cost(:if), stack} end defp instruction({frame, vm, _n}, gas, [_ | stack], _opts, {:if, _type, else_idx, end_idx}) do next_instr = if else_idx != :none, do: else_idx, else: end_idx {{frame, vm, next_instr}, gas + Gas.cost(:if), stack} end # This just skips to end because the only time an "else" instruction # is evaluated is immediately following the execution of an "if" body, which # means we don't actually want to execute the "else" body. The "if" opcode # will jump to the body of the else branch if needed. defp instruction({frame, vm, _n}, gas, stack, _opts, {:else, end_idx}) do {{frame, vm, end_idx}, gas + Gas.cost(:else), stack} end defp instruction({%{labels: []} = frame, vm, n}, gas, stack, _opts, :end), do: {{frame, vm, n}, gas + Gas.cost(:end, true), stack} defp instruction({frame, vm, n}, gas, stack, _opts, :end) do [_ | labels] = frame.labels [_ | snapshots] = frame.snapshots {{Map.merge(frame, %{labels: labels, snapshots: snapshots}), vm, n}, gas + Gas.cost(:end, false), stack} end defp instruction(ctx, gas, stack, opts, op) do IO.inspect op IEx.pry end defp break_to({frame, vm, _n}, gas, stack, label_idx) do {label_instr_idx, next_instr} = Enum.at(frame.labels, label_idx) snapshot = Enum.at(frame.snapshots, label_idx) %{^label_instr_idx => instr} = frame.instructions drop_changes = fn type -> if type != :no_res do [res | _] = stack [res | snapshot] else snapshot end end stack = case instr do {:loop, _} -> snapshot {:if, res_type, _, _} -> drop_changes.(res_type) {:block, res_type, _} -> drop_changes.(res_type) end {{frame, vm, next_instr}, gas + 2, stack} end defp rotl(number, shift), do: (number <<< shift) ||| (number >>> (0x1F &&& (32 + ~~~(shift + 1)))) &&& ~~~(0xFFFFFFFF <<< shift) defp rotr(number, shift), do: (number >>> shift) ||| (number <<< (0x1F &&& (32 + ~~~(-shift + 1)))) &&& ~~~(0xFFFFFFFF <<< -shift) def float_demote(number) do D.set_context(%D.Context{D.get_context | precision: 6}) number * 10 |> :erlang.float_to_binary([decimals: 6]) |> D.new() end defp trap(reason), do: raise "Runtime Error -- #{reason}" defp trailing_zeros(bin_list) do bin_list |> Enum.reverse() |> leading_zeros() end defp leading_zeros(bin_list), do: Enum.find_index(bin_list, & &1 == 1) defp create_entry(instruction) when not is_tuple(instruction), do: to_string(instruction) defp create_entry({instruction, _variable}), do: create_entry(instruction) defp create_entry({:if, _rtype, _else_idx, _end_idx}), do: create_entry(:if) defp create_entry(other), do: create_entry("Trace not implemented for: #{inspect(other)}") defp write_to_file(instruction, gas) do './trace.log' |> Path.expand() |> Path.absname() |> File.write("#{create_entry(instruction)} #{gas}\n", [:append]) end end