299 lines
8.4 KiB
Elixir
299 lines
8.4 KiB
Elixir
defmodule FarmbotCeleryScript.Compiler do
|
|
@moduledoc """
|
|
Responsible for compiling canonical CeleryScript AST into
|
|
Elixir AST.
|
|
"""
|
|
require Logger
|
|
|
|
alias FarmbotCeleryScript.{
|
|
AST,
|
|
Compiler,
|
|
Compiler.IdentifierSanitizer
|
|
}
|
|
|
|
@doc "Sets debug mode for the compiler"
|
|
def debug_mode(bool \\ true) do
|
|
old = Application.get_env(:farmbot_celery_script, __MODULE__, [])
|
|
new = Keyword.put(old, :debug, bool)
|
|
Application.put_env(:farmbot_celery_script, __MODULE__, new)
|
|
bool
|
|
end
|
|
|
|
@doc "Returns current debug mode value"
|
|
def debug_mode?() do
|
|
Application.get_env(:farmbot_celery_script, __MODULE__)[:debug] || false
|
|
end
|
|
|
|
@valid_entry_points [:sequence, :rpc_request]
|
|
|
|
@typedoc """
|
|
Compiled CeleryScript node should compile to an anon function.
|
|
Entrypoint nodes such as
|
|
* `rpc_request`
|
|
* `sequence`
|
|
will compile to a function that takes a Keyword list of variables. This function
|
|
needs to be executed before scheduling/executing.
|
|
|
|
Non entrypoint nodes compile to a function that symbolizes one individual step.
|
|
|
|
## Examples
|
|
|
|
`rpc_request` will be compiled to something like:
|
|
```
|
|
fn params ->
|
|
[
|
|
# Body of the `rpc_request` compiled in here.
|
|
]
|
|
end
|
|
```
|
|
|
|
as compared to a "simple" node like `wait` will compile to something like:
|
|
```
|
|
fn() -> wait(200) end
|
|
```
|
|
"""
|
|
@type compiled :: (Keyword.t() -> [(() -> any())]) | (() -> any())
|
|
|
|
@doc """
|
|
Recursive function that will emit Elixir AST from CeleryScript AST.
|
|
"""
|
|
@spec compile(AST.t(), Keyword.t()) :: [compiled()]
|
|
def compile(ast, env \\ [])
|
|
|
|
def compile(%AST{kind: :abort}, _env) do
|
|
fn -> {:error, "aborted"} end
|
|
end
|
|
|
|
def compile(%AST{kind: kind} = ast, env) when kind in @valid_entry_points do
|
|
compile_entry_point(compile_ast(ast, env), env, [])
|
|
end
|
|
|
|
def compile_entry_point([{_, new_env, _} = compiled | rest], env, acc) do
|
|
env = Keyword.merge(env, new_env)
|
|
debug_mode?() && print_compiled_code(compiled)
|
|
# entry points must be evaluated once more with the calling `env`
|
|
# to return a list of compiled `steps`
|
|
|
|
# TODO: investigate why i have to turn this to a string
|
|
# before eval ing it?
|
|
# case Code.eval_quoted(compiled, [], __ENV__) do
|
|
case Macro.to_string(compiled) |> Code.eval_string(new_env, __ENV__) do
|
|
{fun, new_env} when is_function(fun, 1) ->
|
|
env = Keyword.merge(env, new_env)
|
|
compile_entry_point(rest, env, acc ++ apply(fun, [env]))
|
|
|
|
{{:error, error}, _} ->
|
|
{:error, error}
|
|
end
|
|
end
|
|
|
|
def compile_entry_point([], _, acc) do
|
|
acc
|
|
end
|
|
|
|
defdelegate assertion(ast, env), to: Compiler.Assertion
|
|
defdelegate calibrate(ast, env), to: Compiler.AxisControl
|
|
defdelegate coordinate(ast, env), to: Compiler.DataControl
|
|
defdelegate execute(ast, env), to: Compiler.Execute
|
|
defdelegate execute_script(ast, env), to: Compiler.Farmware
|
|
defdelegate find_home(ast, env), to: Compiler.AxisControl
|
|
defdelegate home(ast, env), to: Compiler.AxisControl
|
|
defdelegate unquote(:_if)(ast, env), to: Compiler.If
|
|
defdelegate install_first_party_farmware(ast, env), to: Compiler.Farmware
|
|
defdelegate move_absolute(ast, env), to: Compiler.AxisControl
|
|
defdelegate move_relative(ast, env), to: Compiler.AxisControl
|
|
defdelegate named_pin(ast, env), to: Compiler.DataControl
|
|
defdelegate point(ast, env), to: Compiler.DataControl
|
|
defdelegate read_pin(ast, env), to: Compiler.PinControl
|
|
defdelegate resource_update(ast, env), to: Compiler.DataControl
|
|
defdelegate rpc_request(ast, env), to: Compiler.RPCRequest
|
|
defdelegate sequence(ast, env), to: Compiler.Sequence
|
|
defdelegate set_pin_io_mode(ast, env), to: Compiler.PinControl
|
|
defdelegate set_servo_angle(ast, env), to: Compiler.PinControl
|
|
defdelegate set_user_env(ast, env), to: Compiler.Farmware
|
|
defdelegate take_photo(ast, env), to: Compiler.Farmware
|
|
defdelegate tool(ast, env), to: Compiler.DataControl
|
|
defdelegate toggle_pin(ast, env), to: Compiler.PinControl
|
|
defdelegate update_farmware(ast, env), to: Compiler.Farmware
|
|
defdelegate variable_declaration(ast, env), to: Compiler.VariableDeclaration
|
|
defdelegate write_pin(ast, env), to: Compiler.PinControl
|
|
defdelegate zero(ast, env), to: Compiler.AxisControl
|
|
|
|
def compile_ast(ast_or_literal, env)
|
|
|
|
def compile_ast(%AST{kind: kind} = ast, env) do
|
|
if function_exported?(__MODULE__, kind, 2),
|
|
do: apply(__MODULE__, kind, [ast, env]),
|
|
else: raise("no compiler for #{kind}")
|
|
end
|
|
|
|
def compile_ast(lit, _env) when is_number(lit), do: lit
|
|
|
|
def compile_ast(lit, _env) when is_binary(lit), do: lit
|
|
|
|
def nothing(_ast, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.nothing()
|
|
end
|
|
end
|
|
|
|
def abort(_ast, _env) do
|
|
quote location: :keep do
|
|
Macro.escape({:error, "aborted"})
|
|
end
|
|
end
|
|
|
|
def wait(%{args: %{milliseconds: millis}}, env) do
|
|
quote location: :keep do
|
|
with millis when is_integer(millis) <- unquote(compile_ast(millis, env)) do
|
|
FarmbotCeleryScript.SysCalls.log("Waiting for #{millis} milliseconds")
|
|
FarmbotCeleryScript.SysCalls.wait(millis)
|
|
else
|
|
{:error, reason} ->
|
|
{:error, reason}
|
|
end
|
|
end
|
|
end
|
|
|
|
def send_message(
|
|
%{args: %{message: msg, message_type: type}, body: channels},
|
|
env
|
|
) do
|
|
# body gets turned into a list of atoms.
|
|
# Example:
|
|
# [{kind: "channel", args: {channel_name: "email"}}]
|
|
# is turned into:
|
|
# [:email]
|
|
channels =
|
|
Enum.map(channels, fn %{
|
|
kind: :channel,
|
|
args: %{channel_name: channel_name}
|
|
} ->
|
|
String.to_atom(channel_name)
|
|
end)
|
|
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.send_message(
|
|
unquote(compile_ast(type, env)),
|
|
unquote(compile_ast(msg, env)),
|
|
unquote(channels)
|
|
)
|
|
end
|
|
end
|
|
|
|
# compiles identifier into a variable.
|
|
# We have to use Elixir ast syntax here because
|
|
# var! doesn't work quite the way we want.
|
|
def identifier(%{args: %{label: var_name}}, env) do
|
|
var_name = IdentifierSanitizer.to_variable(var_name)
|
|
|
|
quote location: :keep do
|
|
unquote({var_name, env, nil})
|
|
end
|
|
end
|
|
|
|
def emergency_lock(_, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.emergency_lock()
|
|
end
|
|
end
|
|
|
|
def emergency_unlock(_, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.emergency_unlock()
|
|
end
|
|
end
|
|
|
|
def read_status(_, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.read_status()
|
|
end
|
|
end
|
|
|
|
def sync(_, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.sync()
|
|
end
|
|
end
|
|
|
|
def check_updates(_, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.check_update()
|
|
end
|
|
end
|
|
|
|
def flash_firmware(%{args: %{package: package_name}}, env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.flash_firmware(
|
|
unquote(compile_ast(package_name, env))
|
|
)
|
|
end
|
|
end
|
|
|
|
def power_off(_, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.power_off()
|
|
end
|
|
end
|
|
|
|
def reboot(%{args: %{package: "farmbot_os"}}, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.reboot()
|
|
end
|
|
end
|
|
|
|
def reboot(%{args: %{package: "arduino_firmware"}}, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.firmware_reboot()
|
|
end
|
|
end
|
|
|
|
def factory_reset(%{args: %{package: package}}, env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.factory_reset(
|
|
unquote(compile_ast(package, env))
|
|
)
|
|
end
|
|
end
|
|
|
|
def change_ownership(%{body: body}, env) do
|
|
pairs =
|
|
Map.new(body, fn %{args: %{label: label, value: value}} ->
|
|
{label, value}
|
|
end)
|
|
|
|
email = Map.fetch!(pairs, "email")
|
|
|
|
secret =
|
|
Map.fetch!(pairs, "secret")
|
|
|> Base.decode64!(padding: false, ignore: :whitespace)
|
|
|
|
server = Map.get(pairs, "server")
|
|
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.change_ownership(
|
|
unquote(compile_ast(email, env)),
|
|
unquote(compile_ast(secret, env)),
|
|
unquote(compile_ast(server, env))
|
|
)
|
|
end
|
|
end
|
|
|
|
def dump_info(_, _env) do
|
|
quote location: :keep do
|
|
FarmbotCeleryScript.SysCalls.dump_info()
|
|
end
|
|
end
|
|
|
|
defp print_compiled_code(compiled) do
|
|
IO.puts("========")
|
|
|
|
compiled
|
|
|> Macro.to_string()
|
|
|> Code.format_string!()
|
|
|> IO.puts()
|
|
|
|
IO.puts("========\n\n")
|
|
end
|
|
end
|