Current section

Files

Jump to
orb lib orb instruction.ex
Raw

lib/orb/instruction.ex

defmodule Orb.Instruction do
@moduledoc false
defstruct [:pop_type, :push_type, :operation, :operands]
require Orb.Ops, as: Ops
alias Orb.Constants
# def new(type, operation, operands \\ [])
def new(type, operation), do: new(type, operation, [])
def new(type, operation, operands)
def new(Elixir.Integer, _, _) do
raise "Can’t create an instruction for Elixir.Integer, need concrete type like Orb.I64."
end
def new(Elixir.Float, _, _) do
raise "Can’t create an instruction for Elixir.Float, need concrete type like Orb.F64."
end
def new(type, {:call, param_types, name} = operation, params),
do: %__MODULE__{
push_type: type,
operation: operation,
operands: type_check_call!(param_types, name, params)
}
def new(type, operation, operands) do
%__MODULE__{
push_type: type,
operation: operation,
operands: type_check_operands!(type, operation, operands)
}
end
@spec wrap_constant!(any(), number() | %{:push_type => any(), optional(any()) => any()}) :: %{
:push_type => any(),
optional(any()) => any()
}
def wrap_constant!(type, value)
def wrap_constant!(Elixir.Integer, value) when is_number(value),
do: raise("Need concrete type not Elixir.Integer for constants.")
def wrap_constant!(Elixir.Float, value) when is_number(value),
do: raise("Need concrete type not Elixir.Float for constants.")
def wrap_constant!(type, value) when is_number(value),
do: %__MODULE__{
push_type: type,
operation: :const,
operands: [value]
}
def wrap_constant!(type, %{push_type: type} = value), do: value
# TODO: remove
def wrap_constant!(type_a, %{push_type: type_b} = value) do
case Ops.types_compatible?(type_a, type_b) do
true ->
value
false ->
raise Orb.TypeCheckError,
expected_type: type_a,
received_type: type_b,
instruction_identifier: "#{type_a}.const"
end
end
def i32(operation), do: new(:i32, operation)
def i32(operation, a) when is_list(a), do: new(:i32, operation, a)
def i32(operation, a), do: new(:i32, operation, [a])
def i32(operation, a, b), do: new(:i32, operation, [a, b])
def i64(operation), do: new(:i64, operation)
def i64(operation, a) when is_list(a), do: new(:i64, operation, a)
def i64(operation, a), do: new(:i64, operation, [a])
def i64(operation, a, b), do: new(:i64, operation, [a, b])
def f32(operation), do: new(:f32, operation)
def f32(operation, a) when is_list(a), do: new(:f32, operation, a)
def f32(operation, a), do: new(:f32, operation, [a])
def f32(operation, a, b), do: new(:f32, operation, [a, b])
def f64(operation), do: new(:f64, operation)
def f64(operation, a) when is_list(a), do: new(:f64, operation, a)
def f64(operation, a), do: new(:f64, operation, [a])
def f64(operation, a, b), do: new(:f64, operation, [a, b])
# TODO: remove call()
def call(f), do: new(:unknown_effect, {:call, f})
def call(f, args) when is_list(args), do: new(:unknown_effect, {:call, f}, args)
def typed_call(result_type, param_types, f, args) when is_list(args),
do: new(result_type, {:call, param_types, f}, args)
# def typed_call(output_type, f, args) when is_list(args), do: new(output_type, {:call, f}, args)
def local_get(type, local_name), do: new(type, {:local_get, local_name})
def local_tee(type, local_name, value), do: new(type, {:local_tee, local_name}, [value])
def local_set(type, local_name) do
%__MODULE__{
pop_type: type,
operation: {:local_set, local_name, type},
operands: []
}
end
# When passing Orb.Stack.Pop
def local_set(type, local_name, %{pop_type: type}) when not is_nil(type) do
%__MODULE__{
pop_type: type,
operation: {:local_set, local_name, type},
operands: []
}
end
def local_set(type, local_name, value) do
new(nil, {:local_set, local_name, type}, [value])
end
def global_get(type, global_name), do: new(type, {:global_get, global_name})
def global_set(type, global_name, value) do
new(nil, {:global_set, global_name, type}, [value])
end
@types_to_stores %{
i32: %{store: true, store8: true, store16: true},
i64: %{store: true, store8: true, store16: true, store32: true},
f32: %{store: true},
f64: %{store: true}
}
def memory_store(type, store, opts)
when is_map_key(@types_to_stores, type) and
is_map_key(:erlang.map_get(type, @types_to_stores), store) do
offset = Keyword.fetch!(opts, :offset)
value = Keyword.fetch!(opts, :value)
align = Keyword.get(opts, :align)
new(nil, {type, store, align}, [offset, value])
end
def memory_size(), do: new(:i32, {:memory, :size}, [])
# This is really a memory_effect but we don’t have a way to have
# both a function returning i32 and performing a memory effect.
# Which we should because any function can do both!
# TODO: add effects map. See InstructionSequence for a sketch.
def memory_grow(value), do: new(:i32, {:memory, :grow}, [value])
defp type_check_call!(param_types, name, params) do
params
|> Enum.with_index(fn param, index ->
param = Constants.expand_if_needed(param)
expected_type = Enum.at(param_types, index)
received_type =
cond do
is_integer(param) -> Elixir.Integer
# TODO: Change to Elixir.Float
is_float(param) -> :f32
%{push_type: type} = param -> type
end
types_must_match!(expected_type, received_type, "call #{name} param #{index}")
wrap_constant!(expected_type, param)
end)
end
defp type_check_operands!(type, operation, operands) do
operands
|> Enum.with_index(fn operand, index ->
operand = Constants.expand_if_needed(operand)
case type_check_operand!(type, operation, operand, index) do
# TODO: might be able to remove this?
nil ->
operand
value ->
value
end
end)
end
defp type_check_operand!(type, op, number, param_index)
when type in [:i64, :i32, :f64, :f32] and is_atom(op) and is_number(number) do
expected_type = Ops.param_type!(type, op, param_index)
received_type =
cond do
# is_integer(number) -> :i32
is_integer(number) -> Elixir.Integer
# TODO: F64
is_float(number) -> Elixir.Float
end
types_must_match!(expected_type, received_type, "#{type}.#{op}")
case expected_type do
Elixir.Integer ->
number
Elixir.Float ->
number
expected_type ->
wrap_constant!(expected_type, number)
end
end
defp type_check_operand!(:i32, op, %{push_type: received_type} = operand, param_index)
when is_atom(op) do
expected_type = Ops.i32_param_type!(op, param_index)
types_must_match!(expected_type, received_type, "i32.#{op}")
wrap_constant!(expected_type, operand)
end
# TODO: can this be removed? I believe it is handled by clause above.
defp type_check_operand!(:i32, op, operand, param_index) when is_atom(op) do
expected_type = Ops.i32_param_type!(op, param_index)
wrap_constant!(expected_type, operand)
end
defp type_check_operand!(:i64, op, %{push_type: received_type} = operand, param_index)
when is_atom(op) do
expected_type = Ops.i64_param_type!(op, param_index)
types_must_match!(expected_type, received_type, "i64.#{op}")
wrap_constant!(expected_type, operand)
end
# TODO: can this be removed?
defp type_check_operand!(:i64, op, operand, param_index) when is_atom(op) do
expected_type = Ops.i64_param_type!(op, param_index)
wrap_constant!(expected_type, operand)
end
defp type_check_operand!(
nil,
{:local_set, local_name, expected_type},
operand,
0
) do
received_type = get_operand_type(operand)
types_must_match!(expected_type, received_type, "local.set $#{local_name}")
operand
end
defp type_check_operand!(
nil,
{:global_set, global_name, expected_type},
operand,
0
) do
received_type = get_operand_type(operand)
types_must_match!(expected_type, received_type, "global.set $#{global_name}")
operand
end
defp type_check_operand!(
nil,
{:i32, :store, _},
operand,
1
) do
received_type = get_operand_type(operand)
types_must_match!(:i32, received_type, "i32.store")
operand
end
defp type_check_operand!(_type, _operation, operand, _index) do
operand
end
defp get_operand_type(%{push_type: received_type}), do: received_type
defp get_operand_type(n) when is_integer(n), do: Elixir.Integer
defp get_operand_type(n) when is_float(n), do: Elixir.Float
defp types_must_match!(same, same, _instruction_identifier), do: nil
defp types_must_match!(expected_type, received_type, instruction_identifier) do
case received_type do
:unknown ->
# Ignore
nil
type ->
Ops.types_compatible?(type, expected_type) or
raise Orb.TypeCheckError,
expected_type: expected_type,
received_type: type,
instruction_identifier: instruction_identifier
# nil
end
end
defimpl Orb.ToWat do
alias Orb.ToWat.Instructions
def to_wat(
%Orb.Instruction{
operation: {:call, _param_types, f},
operands: operands
},
indent
) do
[
indent,
"(call $",
to_string(f),
for(operand <- operands, do: [" ", Instructions.do_wat(operand)]),
")"
]
end
def to_wat(
%Orb.Instruction{
operation: {:call, f},
operands: operands
},
indent
) do
[
indent,
"(call $",
to_string(f),
for(operand <- operands, do: [" ", Instructions.do_wat(operand)]),
")"
]
end
def to_wat(
%Orb.Instruction{
operation: {:local_get, local_name},
operands: []
},
indent
) do
[indent, "(local.get $", to_string(local_name), ")"]
end
def to_wat(
%Orb.Instruction{
operation: {:local_tee, local_name},
operands: operands
},
indent
) do
[
for(operand <- operands, do: [indent, Instructions.do_wat(operand), "\n"]),
indent,
"(local.tee $",
to_string(local_name),
")"
]
end
def to_wat(
%Orb.Instruction{
operation: {:local_set, local_name, _},
operands: operands
},
indent
) do
[
for(operand <- operands, do: [indent, Instructions.do_wat(operand), "\n"]),
indent,
"(local.set $",
to_string(local_name),
")"
]
end
def to_wat(
%Orb.Instruction{
operation: {:global_get, global_name},
operands: []
},
indent
) do
[indent, "(global.get $", to_string(global_name), ")"]
end
def to_wat(
%Orb.Instruction{
operation: {:global_set, global_name, _},
operands: operands
},
indent
) do
[
for(operand <- operands, do: [indent, Instructions.do_wat(operand), "\n"]),
indent,
"(global.set $",
to_string(global_name),
")"
]
end
def to_wat(
%Orb.Instruction{
push_type: nil,
operation: {type, store, align},
operands: operands
},
indent
) do
[
indent,
"(",
to_string(type),
".",
to_string(store),
case align do
nil -> []
align -> [" align=", to_string(align)]
end,
for(operand <- operands, do: [" ", Instructions.do_wat(operand)]),
")"
]
end
def to_wat(
%Orb.Instruction{
push_type: :i32,
operation: {:memory, memory_op},
operands: operands
},
indent
) do
[
indent,
"(memory.",
to_string(memory_op),
for(operand <- operands, do: [" ", Instructions.do_wat(operand)]),
")"
]
end
def to_wat(
%Orb.Instruction{
push_type: type,
operation: :const,
operands: [number]
},
indent
) do
[
indent,
"(",
to_string(Ops.to_primitive_type(type)),
".const ",
to_string(number),
")"
]
end
def to_wat(
%Orb.Instruction{
push_type: type,
operation: operation,
operands: operands
},
indent
) do
[
indent,
"(",
to_string(type),
".",
to_string(operation),
for(operand <- operands, do: [" ", Instructions.do_wat(operand)]),
")"
]
end
end
defimpl Orb.ToWasm do
import Orb.Leb
def type_const(:i32), do: 0x41
def type_const(:i64), do: 0x42
def type_const(:f32), do: 0x43
def type_const(:f64), do: 0x44
def to_wasm(
%Orb.Instruction{
push_type: type,
operation: :const,
operands: [number]
},
_
) do
[type_const(type), uleb128(number)]
end
def to_wasm(
%Orb.Instruction{
push_type: :i32,
operation: operation,
operands: operands
},
context
) do
[
for(operand <- operands, do: Orb.ToWasm.to_wasm(operand, context)),
i32_operation(operation)
]
end
def to_wasm(
%Orb.Instruction{
push_type: :i64,
operation: operation,
operands: operands
},
context
) do
[
for(operand <- operands, do: Orb.ToWasm.to_wasm(operand, context)),
i64_operation(operation)
]
end
def to_wasm(
%Orb.Instruction{
push_type: nil,
operation: {:local_set, identifier, _},
operands: operands
},
context
) do
[
for(operand <- operands, do: Orb.ToWasm.to_wasm(operand, context)),
0x21,
uleb128(Orb.ToWasm.Context.fetch_local_index!(context, identifier))
]
end
defp i32_operation(:add), do: 0x6A
defp i32_operation(:sub), do: 0x6B
defp i32_operation(:mul), do: 0x6C
defp i32_operation(:div_s), do: 0x6D
defp i32_operation(:div_u), do: 0x6E
defp i32_operation(:rem_s), do: 0x6F
defp i32_operation(:rem_u), do: 0x70
defp i32_operation(:and), do: 0x71
defp i32_operation(:or), do: 0x72
defp i32_operation(:xor), do: 0x73
defp i32_operation(:shl), do: 0x74
defp i32_operation(:shr_s), do: 0x75
defp i32_operation(:shr_u), do: 0x76
defp i32_operation(:rotl), do: 0x77
defp i32_operation(:rotr), do: 0x78
def i64_operation(:add), do: 0x7C
def i64_operation(:sub), do: 0x7D
def i64_operation(:mul), do: 0x7E
def i64_operation(:div_s), do: 0x7F
def i64_operation(:div_u), do: 0x80
def i64_operation(:rem_s), do: 0x81
def i64_operation(:rem_u), do: 0x82
def i64_operation(:and), do: 0x83
def i64_operation(:or), do: 0x84
def i64_operation(:xor), do: 0x85
def i64_operation(:shl), do: 0x86
def i64_operation(:shr_s), do: 0x87
def i64_operation(:shr_u), do: 0x88
def i64_operation(:rotl), do: 0x89
def i64_operation(:rotr), do: 0x8A
# def to_wasm(
# %Orb.Instruction{
# operation: {:local_get, local_name},
# operands: []
# },
# _
# ) do
# [0x20, Orb.ToWasm.Context.local_index(local_name)]
# end
# def to_wasm(
# %Orb.Instruction{
# operation: {:local_set, local_name, _},
# operands: []
# },
# _
# ) do
# [0x21, Orb.ToWasm.Context.local_index(local_name)]
# end
# def to_wasm(
# %Orb.Instruction{
# operation: {:local_tee, local_name},
# operands: []
# },
# _
# ) do
# [0x22, Orb.ToWasm.Context.local_index(local_name)]
# end
end
end