Elixir is Your Compiler
Orb lets you use the entire Elixir programming language at WebAssembly compile time. This allows you to generate code dynamically, use existing Elixir libraries, or make network requests.
Vanilla Elixir has two steps: Elixir-compile-time and Elixir-runtime. Elixir-compile-time is when its macros are run for example. Orb adds one more step in the middle that straddles the Elixir and WebAssembly worlds: WebAssembly-compile-time.
Here’s each step in Orb in the order they run:
- Elixir-compile-time: Top-level Elixir code is run, macros are run.
- WebAssembly-compile-time: Elixir functions are run.
- WebAssembly-runtime: WebAssembly instructions are run. Elixir doesn’t exist at this step!
1. Elixir macros executed at Elixir compile-time
Macros are one of the most powerful features of Elixir. They allow you to transform the abstract syntax tree (AST) of the Elixir language. For example, Ecto’s declarative queries are powered by macros.
Processing do
blocks with macros
A common building block in Elixir are do
blocks. You can consume these “blocks” using Elixir macros, which can unwrap the underlying list of Elixir AST elements. You can then process these elements.
Orb globals are defined using macros consuming do
blocks. The core piece of Orb defw
is made using macros and do
blocks. They end up iterating through each Elixir AST and doing something with it — adding a new global to the list of globals, or adding a new function to the list of WebAssembly functions. They are what most make it feel like you are writing Elixir when using Orb.
2. Elixir functions executed at WebAssembly compile-time
Orb also has its own abstract syntax tree, represented using Elixir structs. You create these structs using helper functions in the Orb DSL. As these are really functions like any other Elixir function, you can compose them like any other Elixir function. Find yourself doing the same 3 things in a row all the time? Wrap it in a function.
Reduce boilerplate with inlining
Orb.snippet/1
lets you use the Orb’s DSL returning WebAssembly instructions as a value.
Here’s an example of a parser helper that iterates through a list of characters at compile time, mapping each to a WebAssembly instruction to read each character and finally boolean AND
them all together.
defmodule CharParser do
use Orb
def parse_chars(chars, offset) when is_list(chars) do
Orb.snippet do
chars
|> Enum.with_index(fn char, index ->
Memory.load!(I32.U8, offset + index) === char
end)
|> Enum.reduce(&I32.band/2)
end
end
end
The Memory.load!/2
and I32.band/2
provide the WebAssembly instructions via Orb’s DSL, while the rest is standard Elixir code. The result is just a sequence of WebAssembly instructions, effectively inlining a loop at compile time.
3. WebAssembly instructions executed at WebAssembly runtime
Finally we get to the WebAssembly instructions themselves being executed on a WebAssembly runtime like JavaScript’s WebAssembly.Instance
class or Wasmtime.
The above compilation steps have been done and the WebAssembly instructions have been converted to their runnable format via Orb.ToWasm
or Orb.ToWat
. If you need you can implement these protocols yourself.
For example, here’s how Orb.Memory
implements Orb.ToWat
:
defmodule Orb.Memory do
# …
defimpl Orb.ToWat do
def to_wat(%Orb.Memory{min: min}, indent) do
[
indent,
~S{(memory (export "memory")},
case min do
nil -> []
int -> [" ", to_string(int)]
end,
~S{)},
"\n"
]
end
end
end
This allows you to extend Orb’s syntax: if there’s a new WebAssembly feature you can implement the building blocks for its instructions yourself.