Packages

Add a dice-rolling DSL with sigil support and reusable compiled functions to your application.

Current section

Files

Jump to
ex_dice_roller lib compiler.ex
Raw

lib/compiler.ex

defmodule ExDiceRoller.Compiler do
@moduledoc """
Provides functionality for compiling expressions into ready-to-execute
functions.
"""
alias ExDiceRoller.{Parser, Tokenizer}
@type compiled_val :: compiled_fun | number
@type compiled_fun :: (Keyword.t() -> number)
@type fun_info_tuple :: {function, atom, list(any)}
@doc """
Compiles a provided `t:Parser.expression/0` into an anonymous function.
```elixir
{:ok, roll_fun} = ExDiceRoller.compile("1dx+10")
{:ok, _}
ExDiceRoller.execute(roll_fun, x: 5)
11
ExDiceRoller.execute(roll_fun, x: "10d100")
523
```
"""
@spec compile(Parser.expression()) :: compiled_val
def compile({:digit, compiled_val}),
do: compiled_val |> to_string() |> String.to_integer()
def compile({:roll, left_expr, right_expr}) do
num = compile(left_expr)
sides = compile(right_expr)
compile_roll(num, is_function(num), sides, is_function(sides))
end
def compile({{:operator, op}, left_expr, right_expr}) do
left_expr = compile(left_expr)
right_expr = compile(right_expr)
compile_op(op, left_expr, is_function(left_expr), right_expr, is_function(right_expr))
end
def compile({:var, _} = var), do: compile_var(var)
@doc """
Shows the nested functions and relationships of a compiled function.
```elixir
> {:ok, fun} = ExDiceRoller.compile("1d8+(1-x)d(2*y)")
{:ok, #Function<0.84780260/1 in ExDiceRoller.Compiler.compile_add/4>}
> ExDiceRoller.Compiler.fun_info fun
{#Function<0.16543174/1 in ExDiceRoller.Compiler.compile_add/4>,
:"-compile_add/4-fun-0-",
[
{#Function<12.16543174/1 in ExDiceRoller.Compiler.compile_roll/4>,
:"-compile_roll/4-fun-3-", [1, 8]},
{#Function<9.16543174/1 in ExDiceRoller.Compiler.compile_roll/4>,
:"-compile_roll/4-fun-0-",
[
{#Function<15.16543174/1 in ExDiceRoller.Compiler.compile_sub/4>,
:"-compile_sub/4-fun-2-",
[
1,
{#Function<16.16543174/1 in ExDiceRoller.Compiler.compile_var/1>,
:"-compile_var/1-fun-0-", ['x']}
]},
{#Function<8.16543174/1 in ExDiceRoller.Compiler.compile_mul/4>,
:"-compile_mul/4-fun-2-",
[
2,
{#Function<16.16543174/1 in ExDiceRoller.Compiler.compile_var/1>,
:"-compile_var/1-fun-0-", ['y']}
]}
]}
]}
```
"""
@spec fun_info(compiled_fun) :: fun_info_tuple
def fun_info(fun) when is_function(fun) do
info = :erlang.fun_info(fun)
{fun, info[:name],
info[:env]
|> Enum.reverse()
|> Enum.map(fn child ->
fun_info(child)
end)}
end
def fun_info(num) when is_number(num), do: num
def fun_info(str) when is_list(str), do: str
@spec compile_roll(compiled_val, boolean, compiled_val, boolean) :: compiled_fun
defp compile_roll(num, true, sides, true) do
fn args -> roll_final(num.(args), sides.(args)) end
end
defp compile_roll(num, true, sides, false), do: fn args -> roll_final(num.(args), sides) end
defp compile_roll(num, false, sides, true), do: fn args -> roll_final(num, sides.(args)) end
defp compile_roll(num, false, sides, false), do: fn _args -> roll_final(num, sides) end
@spec roll_final(number, number) :: integer
defp roll_final(0, _), do: 0
defp roll_final(_, 0), do: 0
defp roll_final(num, sides) when num >= 0 and sides >= 0 do
num = round(num)
sides = round(sides)
Enum.reduce(1..num, 0, fn _, total -> Enum.random(1..sides) + total end)
end
defp roll_final(_, _),
do: raise(ArgumentError, "neither number of dice nor number of sides cannot be less than 0")
@spec compile_op(list, compiled_val, boolean, compiled_val, boolean) :: compiled_val
defp compile_op('+', l, l_fun?, r, r_fun?), do: compile_add(l, l_fun?, r, r_fun?)
defp compile_op('-', l, l_fun?, r, r_fun?), do: compile_sub(l, l_fun?, r, r_fun?)
defp compile_op('*', l, l_fun?, r, r_fun?), do: compile_mul(l, l_fun?, r, r_fun?)
defp compile_op('/', l, l_fun?, r, r_fun?), do: compile_div(l, l_fun?, r, r_fun?)
@spec compile_add(compiled_val, boolean, compiled_val, boolean) :: compiled_val
defp compile_add(l, true, r, true), do: fn args -> l.(args) + r.(args) end
defp compile_add(l, true, r, false), do: fn args -> l.(args) + r end
defp compile_add(l, false, r, true), do: fn args -> l + r.(args) end
defp compile_add(l, false, r, false), do: l + r
@spec compile_sub(compiled_val, boolean, compiled_val, boolean) :: compiled_val
defp compile_sub(l, true, r, true), do: fn args -> l.(args) - r.(args) end
defp compile_sub(l, true, r, false), do: fn args -> l.(args) - r end
defp compile_sub(l, false, r, true), do: fn args -> l - r.(args) end
defp compile_sub(l, false, r, false), do: l - r
@spec compile_mul(compiled_val, boolean, compiled_val, boolean) :: compiled_val
defp compile_mul(l, true, r, true), do: fn args -> l.(args) * r.(args) end
defp compile_mul(l, true, r, false), do: fn args -> l.(args) * r end
defp compile_mul(l, false, r, true), do: fn args -> l * r.(args) end
defp compile_mul(l, false, r, false), do: l * r
@spec compile_div(compiled_val, boolean, compiled_val, boolean) :: compiled_val
defp compile_div(l, true, r, true), do: fn args -> l.(args) / r.(args) end
defp compile_div(l, true, r, false), do: fn args -> l.(args) / r end
defp compile_div(l, false, r, true), do: fn args -> l / r.(args) end
defp compile_div(l, false, r, false), do: l / r
@spec compile_var({:var, charlist}) :: compiled_fun
defp compile_var({:var, var}), do: fn args -> var_final(var, args) end
@spec var_final(charlist, Keyword.t()) :: number
defp var_final(var, args) do
key = var |> to_string() |> String.to_atom()
case Keyword.get(args, key) do
nil ->
raise ArgumentError, "no variable #{inspect(var)} was found in the arguments"
val when is_integer(val) or is_float(val) ->
val
val when is_bitstring(val) ->
{:ok, tokens} = Tokenizer.tokenize(val)
{:ok, parsed} = Parser.parse(tokens)
maybe_fun = compile(parsed)
case is_function(maybe_fun) do
false -> maybe_fun
true -> maybe_fun.([])
end
end
end
end