Packages
orb
0.0.18
0.2.2
0.2.1
0.2.0
0.1.1
0.1.0
0.0.51
0.0.49
0.0.48
0.0.47
0.0.46
0.0.45
0.0.44
0.0.43
0.0.42
0.0.41
0.0.40
0.0.39
0.0.38
0.0.37
0.0.36
0.0.35
0.0.34
0.0.33
0.0.32
0.0.31
0.0.30
0.0.28
0.0.27
0.0.26
0.0.25
0.0.24
0.0.23
0.0.22
0.0.21
0.0.20
0.0.19
0.0.18
0.0.17
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
retired
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
DSL for WebAssembly
Current section
Files
Jump to
Current section
Files
lib/orb/dsl.ex
defmodule Orb.DSL do
@moduledoc """
The main DSL which is imported automatically when you call `Orb.wasm/2`.
"""
alias Orb.Instruction
alias Orb.Ops
require Ops
defmacro func(call, do: block) do
define_func(call, :public, [], block, __CALLER__)
end
defmacro func(call, locals, do: block) when is_list(locals) do
define_func(call, :public, [locals: locals], block, __CALLER__)
end
defmacro func(call, result_type, do: block) do
define_func(call, :public, [result: result_type], block, __CALLER__)
end
defmacro func(call, result_type, locals, do: block) when is_list(locals) do
define_func(call, :public, [result: result_type, locals: locals], block, __CALLER__)
end
def funcp(options) do
name = Keyword.fetch!(options, :name)
Orb.Func.Type.imported_func(name, options[:params], options[:result])
end
defmacro funcp(call, do: block) do
define_func(call, :private, [], block, __CALLER__)
end
defmacro funcp(call, locals, do: block) when is_list(locals) do
define_func(call, :private, [locals: locals], block, __CALLER__)
end
defmacro funcp(call, result_type, do: block) do
define_func(call, :private, [result: result_type], block, __CALLER__)
end
defmacro funcp(call, result_type, locals, do: block) when is_list(locals) do
define_func(call, :private, [result: result_type, locals: locals], block, __CALLER__)
end
def __define_func(call, visibility, options, block, env) do
define_func(call, visibility, options, block, env)
end
defp define_func(call, visibility, options, block, env) do
call = Macro.expand_once(call, __ENV__)
line = case call do
{_, meta, _} -> meta[:line]
_ -> env.line
end
{name, args} =
case Macro.decompose_call(call) do
:error -> {Orb.DSL.__expand_identifier(call, __ENV__), []}
other -> other
end
name = name
exported_names =
case visibility do
:public -> quote do: [to_string(unquote(name))]
:private -> []
end
params =
case args do
[args] when is_list(args) ->
for {name, type} <- args do
Macro.escape(%Orb.Func.Param{name: name, type: Macro.expand_literals(type, env)})
end
[] ->
[]
[_ | _] ->
raise CompileError,
line: line,
file: env.file,
description:
"Cannot define function with multiple arguments, use keyword list instead."
end
arg_types =
case args do
[args] when is_list(args) ->
for {name, type} <- args do
{name, Macro.expand_literals(type, env)}
end
args ->
for {name, _meta, [type]} <- args do
{name, Macro.expand_literals(type, env)}
end
end
result_type = Keyword.get(options, :result, nil) |> Macro.expand_literals(env)
local_types =
for {key, type} <- Keyword.get(options, :locals, []) do
{key, Macro.expand_literals(type, env)}
end
locals = Map.new(arg_types ++ local_types)
# block = Macro.expand_once(block, __ENV__)
block_items =
case block do
{:__block__, _meta, block_items} -> block_items
single -> [single]
end
block_items = Macro.expand(block_items, env)
block_items = do_snippet(locals, block_items)
quote do
%Orb.Func{
name: unquote(name),
params: unquote(params),
result: unquote(result_type),
local_types: unquote(local_types),
body: unquote(block_items),
# body: fn ->
# if Process.get(Orb.DSL, false) do
# unquote(block_items)
# else
# []
# end
# end,
exported_names: unquote(exported_names)
}
end
end
def do_snippet(locals, block_items) do
Macro.prewalk(block_items, fn
# local[at!: offset] = value
{:=, _meta,
[
{{:., _, [Access, :get]}, _,
[
{local, _, nil},
[
at!: offset
]
]},
value
]}
when is_atom(local) and is_map_key(locals, local) ->
# FIXME: add error message
bytes_factor = locals[local].byte_count()
store_instruction =
case bytes_factor do
1 -> :store8
4 -> :store
end
local_get_instruction =
quote do
Instruction.local_get(unquote(locals[local]), unquote(local))
end
computed_offset =
case {offset, bytes_factor} do
{0, _} ->
quote do: unquote(local_get_instruction)
{offset, 1} ->
quote do: Orb.I32.add(unquote(local_get_instruction), unquote(offset))
# We can compute at compile-time
{offset, factor} when is_integer(offset) ->
quote do:
Orb.I32.add(
unquote(local_get_instruction),
unquote(offset * factor)
)
# We can only compute at runtime
{offset, factor} ->
quote do:
Orb.I32.add(
unquote(local_get_instruction),
I32.mul(unquote(offset), unquote(factor))
)
end
quote do:
Orb.Instruction.i32(
unquote(store_instruction),
unquote(computed_offset),
unquote(value)
)
{:=, _, [{local, _, nil}, input]}
when is_atom(local) and is_map_key(locals, local) and
is_struct(:erlang.map_get(local, locals), Orb.VariableReference) ->
quote do: Orb.Instruction.local_set(unquote(locals[local]), unquote(local), unquote(input))
{:=, _, [{local, _, nil}, input]}
when is_atom(local) and is_map_key(locals, local) ->
quote do: Orb.Instruction.local_set(unquote(locals[local]), unquote(local), unquote(input))
{local, _meta, nil} when is_atom(local) and is_map_key(locals, local) ->
quote do: Orb.VariableReference.local(unquote(local), unquote(locals[local]))
# @some_global = input
{:=, _, [{:@, _, [{global, _, nil}]}, input]} when is_atom(global) ->
# quote do: Orb.Instruction.global_set(unquote(Macro.var(:wasm_global_type, nil)).(unquote(global)), unquote(global), unquote(input))
quote do: Orb.Instruction.global_set(Orb.__lookup_global_type!(unquote(global)), unquote(global), unquote(input))
# @some_global
# node = {:@, meta, [{global, _, nil}]} when is_atom(global) ->
# if global == :weekdays_i32 do
# dbg(meta)
# dbg(node)
# end
#
# {:global_get, meta, [global]}
{:=, _, [{:_, _, nil}, value]} ->
quote do: [unquote(value), :drop]
other ->
other
end)
end
def export(f = %Orb.Func{}, name) when is_binary(name) do
update_in(f.exported_names, fn names -> [name | names] end)
end
# TODO: unused
def i32_const(value), do: {:i32_const, value}
def i32_boolean(0), do: {:i32_const, 0}
def i32_boolean(1), do: {:i32_const, 1}
def i32(n) when is_integer(n), do: {:i32_const, n}
def i32(false), do: {:i32_const, 0}
def i32(true), do: {:i32_const, 1}
def i32(op) when op in Ops.i32(:all), do: {:i32, op}
@doc """
Pushes a value onto the current stack.
"""
def push(value)
def push(tuple)
when is_tuple(tuple) and elem(tuple, 0) in [:i32, :i32_const, :local_get, :global_get],
do: tuple
# FIXME: assumes only 32-bit integer, doesn’t work with 64-bit or float
def push(n) when is_integer(n), do: {:i32_const, n}
# FIXME: assumes only 32-bit float, doesn’t work with 64-bit float
def push(n) when is_float(n), do: {:f32_const, n}
def push(%Orb.VariableReference{} = ref), do: ref
def push(do: %Orb.Instruction{operation: {:local_set, identifier, type}, operands: [value]}), do: Orb.Instruction.local_tee(type, identifier, value)
@doc """
Push value then run the block. Useful for when you mutate a variable but want its previous value.
"""
def push(value, do: block) do
[
value,
__get_block_items(block)
# :pop
]
end
# def global_get(identifier), do: Orb.Instruction.global_get(Orb.__lookup_global_type!(identifier), identifier)
def global_get(identifier), do: {:global_get, identifier}
def global_set(identifier), do: {:global_set, identifier}
def local_set(identifier), do: {:local_set, identifier}
def typed_call(output_type, f, args) when is_list(args),
do: Instruction.typed_call(output_type, f, args)
@doc """
Call local function `f`.
"""
@deprecated "Use typed_call/3 instead."
def call(f), do: Instruction.call(f, [])
@doc """
Call local function `f`, passing arguments `args` when list, or single argument otherwise.
"""
@deprecated "Use typed_call/3 instead."
def call(f, args)
def call(f, args) when is_list(args), do: Instruction.call(f, args)
def call(f, a), do: Instruction.call(f, [a])
@doc """
Call local function `f`, passing arguments `a` & `b`.
"""
@deprecated "Use typed_call/3 instead."
def call(f, a, b), do: Instruction.call(f, [a, b])
@doc """
Call local function `f`, passing argument `a`, `b`, `c`.
"""
@deprecated "Use typed_call/3 instead."
def call(f, a, b, c), do: Instruction.call(f, [a, b, c])
@doc """
Call local function `f`, passing argument `a`, `b`, `c`, `d`.
"""
@deprecated "Use typed_call/3 instead."
def call(f, a, b, c, d), do: Instruction.call(f, [a, b, c, d])
@doc """
Call local function `f`, passing argument `a`, `b`, `c`, `d`, `e`.
"""
@deprecated "Use typed_call/3 instead."
def call(f, a, b, c, d, e), do: Instruction.call(f, [a, b, c, d, e])
@doc """
Call local function `f`, passing argument `a`, `b`, `c`, `d`, `e`, `f`.
"""
@deprecated "Use typed_call/3 instead."
def call(f, a, b, c, d, e, f), do: Instruction.call(f, [a, b, c, d, e, f])
def __expand_identifier(identifier, env) do
identifier = Macro.expand_once(identifier, env) |> Kernel.to_string()
case identifier do
"Elixir." <> _rest = string ->
string |> Module.split() |> Enum.join(".")
other ->
other
end
end
@doc """
Declare a loop that iterates through a source.
"""
defmacro loop({:<-, _, [item, source]}, do: block) do
result_type = nil
{set_item, identifier} =
case item do
{:_, _, _} ->
{[], "_"}
_ ->
{quote(
do: [
unquote(source)[:value],
Orb.VariableReference.as_set(unquote(item))
]
), quote(do: unquote(item).identifier)}
end
block_items =
quote(
do:
Orb.IfElse.new(
# unquote(source),
unquote(source)[:valid?],
[
unquote(set_item),
unquote(__get_block_items(block)),
unquote(source)[:next],
Orb.VariableReference.as_set(unquote(source)),
%Orb.Loop.Branch{identifier: unquote(identifier)}
]
)
)
quote do
%Orb.Loop{
identifier: unquote(identifier),
result: unquote(result_type),
body: unquote(block_items)
}
end
end
@doc """
Declare a loop.
"""
defmacro loop(identifier, options \\ [], do: block) do
identifier = __expand_identifier(identifier, __CALLER__)
result_type =
Keyword.get(options, :result, nil) |> Macro.expand_literals(__CALLER__)
while = Keyword.get(options, :while, nil)
block_items = __get_block_items(block)
block_items =
Macro.prewalk(block_items, fn
{{:., _, [{:__aliases__, _, [identifier]}, :continue]}, _, []} ->
quote do: %Orb.Loop.Branch{identifier: unquote(identifier)}
{{:., _, [{:__aliases__, _, [identifier]}, :continue]}, _, [[if: condition]]} ->
quote do: %Orb.Loop.Branch{
identifier: unquote(identifier),
if: unquote(condition)
}
other ->
other
end)
block_items =
case while do
nil ->
block_items
condition ->
quote do:
Orb.IfElse.new(
unquote(condition),
[
unquote(block_items),
%Orb.Loop.Branch{identifier: unquote(identifier)}
]
)
end
# quote bind_quoted: [identifier: identifier] do
quote do
%Orb.Loop{
identifier: unquote(identifier),
result: unquote(result_type),
body: unquote(block_items)
}
end
end
@doc """
Run code at compile-time.
"""
defmacro inline(do: block) do
quote do
with do
# TODO: DRY this up
# use Orb.RestoreKernel
import Orb.Global.DSL, only: []
import Orb.IfElse.DSL, only: []
import Orb.I32.DSL, only: []
import Orb.U32.DSL, only: []
import Orb.S32.DSL, only: []
import Orb.F32.DSL, only: []
import Kernel
unquote(block)
end
end
end
@doc """
Run Elixir for-comprehension at compile-time.
"""
defmacro inline({:for, _meta, [for_arg]}, do: block) do
quote do
inline(do: for(unquote(for_arg), do: unquote(block)))
end
end
@doc """
Used to bring back wasm-mode when inside `inline/1`.
"""
defmacro wasm(mode \\ Orb.S32, do: block) do
mode = Macro.expand_literals(mode, __CALLER__)
pre = Orb.__mode_pre(mode)
quote do
with do
unquote(pre)
import Orb, only: []
import Orb.DSL
require Orb.Control, as: Control
unquote(__get_block_items(block))
end
end
end
@doc """
Declare a constant string, which will be extracted to the top of the module, and its address substituted in place.
"""
def const(value) do
{:const_string, value}
end
# TODO: decide if this idea is dead.
@doc false
def const_set_insert(set_name, string) when is_atom(set_name) and is_binary(string) do
:todo
end
# TODO: add a comptime keyword like Zig: https://kristoff.it/blog/what-is-zig-comptime/
@doc """
Return from a function. You may wish to `push/1` values before returning.
"""
def return(), do: :return
@doc """
Return from a function if a condition is true.
"""
def return(value_or_condition)
def return(if: condition), do: Orb.IfElse.new(condition, :return)
def return(value), do: {:return, value}
@doc """
Return from a function with the provided value only if a condition is true.
"""
def return(value, if: condition), do: Orb.IfElse.new(condition, {:return, value})
@doc """
A no-op instruction.
See https://en.wikipedia.org/wiki/NOP_(code)
"""
def nop(), do: :nop
@doc """
Drop the last pushed value from the stack.
"""
def drop(), do: :drop
@doc """
Execute the passed expression, but ignore its result by immediately dropping its value.
"""
def drop(expression), do: [expression, :drop]
@doc """
Denote a point in code that should not be reachable. Traps.
Useful for exhaustive conditionals or code you know will not execute.
"""
def unreachable!(), do: :unreachable
@doc """
Asserts a condition that _must_ be true, otherwise traps.
"""
def assert!(condition) do
Orb.IfElse.new(
condition,
nop(),
unreachable!()
)
end
def mut!(term), do: Orb.MutRef.from(term)
@doc """
For when there’s a language feature of WebAssembly that Orb doesn’t provide. Please file an issue if there’s something you wish existed. https://github.com/RoyalIcing/Orb/issues
"""
def raw_wat(source), do: {:raw_wat, String.trim(source)}
def __get_block_items(block) do
case block do
nil -> nil
{:__block__, _meta, block_items} -> block_items
single -> [single]
end
end
end