farmbot_os/farmbot_celery_script/lib/farmbot_celery_script/compiler.ex

1131 lines
33 KiB
Elixir

defmodule FarmbotCeleryScript.Compiler do
@moduledoc """
Responsible for compiling canonical CeleryScript AST into
Elixir AST.
"""
require Logger
alias FarmbotCeleryScript.{
AST,
Compiler,
Compiler.IdentifierSanitizer
}
use Compiler.Tools
@valid_entry_points [:sequence, :rpc_request]
@kinds "install_farmware"
@kinds "update_farmware"
@kinds "remove_farmware"
@kinds "channel"
@kinds "explanation"
@kinds "rpc_ok"
@kinds "rpc_error"
@kinds "pair"
@kinds "scope_declaration"
@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 the ast
{_, _, _} = compiled = compile_ast(ast)
# 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([], __ENV__) do
{fun, _} when is_function(fun, 1) -> apply(fun, [env])
{{:error, error}, _} -> {:error, error}
end
end
# The compile macro right here is generated by the Compiler.Tools module.
# The goal of the macro is to do two things:
# 1) take out all the common code between each node impl.
# Example:
# compile :fire_laser, %{after: 100}, targets, do: quote, do: fire_at(targets)
# will compile down to:
# def compile_ast(%AST{kind: :fire_laser, args: %{after: 100}, body: targets})
#
# 2) Accumulate implemented nodes behind the scenes.
# This allows for the Corpus to throw warnings when a new node
# is added.
# Compiles a `sequence` into an Elixir `fn`.
compile :sequence, %{locals: %{body: params}} = args, block, meta do
# Sort the args.body into two arrays.
# The `params` side gets turned into
# a keyword list. These `params` are passed in from a previous sequence.
# The `body` side declares variables in _this_ scope.
{params_fetch, body} =
Enum.reduce(params, {[], []}, fn ast, {params, body} = _acc ->
case ast do
# declares usage of a parameter as defined by variable_declaration
%{kind: :parameter_declaration} -> {params ++ [compile_param_declaration(ast)], body}
# declares usage of a variable as defined inside the body of itself
%{kind: :parameter_application} -> {params ++ [compile_param_application(ast)], body}
# defines a variable exists
%{kind: :variable_declaration} -> {params, body ++ [ast]}
end
end)
{:__block__, [], assignments} = compile_block(body)
sequence_name = meta[:sequence_name] || args[:sequence_name]
steps = compile_block(block) |> decompose_block_to_steps()
steps = add_sequence_init_and_complete_logs(steps, sequence_name)
quote location: :keep do
fn params ->
# This quiets a compiler warning if there are no variables in this block
_ = inspect(params)
# Fetches variables from the previous execute()
# example:
# parent = Keyword.fetch!(params, :parent)
unquote_splicing(params_fetch)
unquote_splicing(assignments)
# Unquote the remaining sequence steps.
unquote(steps)
end
end
end
compile :rpc_request, %{label: _label}, block do
steps = compile_block(block) |> decompose_block_to_steps()
quote location: :keep do
fn params ->
# This quiets a compiler warning if there are no variables in this block
_ = inspect(params)
unquote(steps)
end
end
end
# Compiles a variable asignment.
compile :variable_declaration, %{label: var_name, data_value: data_value_ast} do
# Compiles the `data_value`
# and assigns the result to a variable named `label`
# Example:
# {
# "kind": "variable_declaration",
# "args": {
# "label": "parent",
# "data_value": {
# "kind": "point",
# "args": {
# "pointer_type": "Plant",
# "pointer_id": 456
# }
# }
# }
# }
# Will be turned into:
# parent = point("Plant", 456)
# NOTE: This needs to be Elixir AST syntax, not quoted
# because var! doesn't do what what we need.
var_name = IdentifierSanitizer.to_variable(var_name)
quote location: :keep do
unquote({var_name, [], nil}) = unquote(compile_ast(data_value_ast))
end
end
# `Assert` is a internal node useful for self testing.
def compile_ast(%AST{
kind: :assertion,
args: %{lua: expression, assertion_type: assertion_type, _then: then_ast},
comment: comment
}) do
comment_header =
if comment do
"[#{comment}] "
else
"[Assertion] "
end
quote location: :keep do
comment_header = unquote(comment_header)
assertion_type = unquote(assertion_type)
case FarmbotCeleryScript.SysCalls.eval_assertion(
unquote(comment),
unquote(compile_ast(expression))
) do
{:error, reason} ->
FarmbotCeleryScript.SysCalls.log_assertion(
false,
assertion_type,
"#{comment_header}failed to evaluate, aborting"
)
{:error, reason}
true ->
FarmbotCeleryScript.SysCalls.log_assertion(
true,
assertion_type,
"#{comment_header}passed, continuing execution"
)
:ok
false when assertion_type == "continue" ->
FarmbotCeleryScript.SysCalls.log_assertion(
false,
assertion_type,
"#{comment_header}failed, continuing execution"
)
:ok
false when assertion_type == "abort" ->
FarmbotCeleryScript.SysCalls.log_assertion(
false,
assertion_type,
"#{comment_header}failed, aborting"
)
{:error, "Assertion failed (aborting)"}
false when assertion_type == "recover" ->
FarmbotCeleryScript.SysCalls.log_assertion(
false,
assertion_type,
"#{comment_header}failed, recovering and continuing"
)
unquote(compile_block(then_ast))
false when assertion_type == "abort_recover" ->
FarmbotCeleryScript.SysCalls.log_assertion(
false,
assertion_type,
"#{comment_header}failed, recovering and aborting"
)
then_block = unquote(compile_block(then_ast))
then_block ++
[
FarmbotCeleryScript.Compiler.compile(%AST{kind: :abort, args: %{}}, [])
]
end
end
end
# Compiles an if statement.
compile :_if, %{_then: then_ast, _else: else_ast, lhs: lhs_ast, op: op, rhs: rhs} do
rhs = compile_ast(rhs)
# Turns the left hand side arg into
# a number. x, y, z, and pin{number} are special that need to be
# evaluated before evaluating the if statement.
# any AST is also aloud to be on the lefthand side as
# well, so if that is the case, compile it first.
lhs =
case lhs_ast do
"x" ->
quote [location: :keep],
do: FarmbotCeleryScript.SysCalls.get_cached_x()
"y" ->
quote [location: :keep],
do: FarmbotCeleryScript.SysCalls.get_cached_y()
"z" ->
quote [location: :keep],
do: FarmbotCeleryScript.SysCalls.get_cached_z()
"pin" <> pin ->
quote [location: :keep],
do: FarmbotCeleryScript.SysCalls.read_cached_pin(unquote(String.to_integer(pin)))
# Named pin has two intents here
# in this case we want to read the named pin.
%AST{kind: :named_pin} = ast ->
quote [location: :keep],
do: FarmbotCeleryScript.SysCalls.read_cached_pin(unquote(compile_ast(ast)))
%AST{} = ast ->
compile_ast(ast)
end
# Turn the `op` arg into Elixir code
if_eval =
case op do
"is" ->
# equality check.
# Examples:
# get_current_x() == 0
# get_current_y() == 10
# get_current_z() == 200
# read_pin(22, nil) == 5
# The ast will look like: {:==, [], lhs, compile_ast(rhs)}
quote location: :keep do
unquote(lhs) == unquote(rhs)
end
"not" ->
# ast will look like: {:!=, [], [lhs, compile_ast(rhs)]}
quote location: :keep do
unquote(lhs) != unquote(rhs)
end
"is_undefined" ->
# ast will look like: {:is_nil, [], [lhs]}
quote location: :keep do
is_nil(unquote(lhs))
end
"<" ->
# ast will look like: {:<, [], [lhs, compile_ast(rhs)]}
quote location: :keep do
unquote(lhs) < unquote(rhs)
end
">" ->
# ast will look like: {:>, [], [lhs, compile_ast(rhs)]}
quote location: :keep do
unquote(lhs) > unquote(rhs)
end
_ ->
quote location: :keep do
unquote(lhs)
end
end
truthy_suffix =
case then_ast do
%{kind: :execute} -> "branching"
%{kind: :nothing} -> "continuing execution"
end
falsey_suffix =
case else_ast do
%{kind: :execute} -> "branching"
%{kind: :nothing} -> "continuing execution"
end
# Finally, compile the entire if statement.
# outputted code will look something like:
# if get_current_x() == 123 do
# execute(123)
# else
# nothing()
# end
quote location: :keep do
prefix_string = FarmbotCeleryScript.SysCalls.format_lhs(unquote(lhs_ast))
# examples:
# "current x position is 100"
# "pin 13 > 1"
# "peripheral 10 is unknon"
result_str =
case unquote(op) do
"is" -> "#{prefix_string} is #{unquote(rhs)}"
"not" -> "#{prefix_string} is not #{unquote(rhs)}"
"is_undefined" -> "#{prefix_string} is unknown"
"<" -> "#{prefix_string} is less than #{unquote(rhs)}"
">" -> "#{prefix_string} is greater than #{unquote(rhs)}"
end
if unquote(if_eval) do
FarmbotCeleryScript.SysCalls.log(
"Evaluated IF statement: #{result_str}; #{unquote(truthy_suffix)}"
)
unquote(compile_block(then_ast))
else
FarmbotCeleryScript.SysCalls.log(
"Evaluated IF statement: #{result_str}; #{unquote(falsey_suffix)}"
)
unquote(compile_block(else_ast))
end
end
end
@iterables [:point_group, :every_point]
# Compiles an `execute` block.
# This one is actually pretty complex and is split into two parts.
# TODO(Connor) refactor this into it's own module, or at least two
# different functions.
compile :execute, %{sequence_id: id}, parameter_applications do
# if there is an iterable AST here,
# we need to compile _many_ sequences, not just one.
loop_parameter_appl_ast =
Enum.find_value(parameter_applications, fn
# check if this parameter_application is a iterable type
%{kind: :parameter_application, args: %{data_value: %{kind: kind}}} = iterable
when kind in @iterables ->
iterable
_other ->
false
end)
# if there was an iterable, inject it's value for every instance of that item
if loop_parameter_appl_ast do
# remove the iterable from the parameter applications,
# since it will be injected after this.
parameter_applications =
Enum.reduce(parameter_applications, [], fn
# Remove point_group from parameter appls
%{kind: :parameter_application, args: %{data_value: %{kind: :point_group}}}, acc -> acc
# Remove every_point from parameter appls
%{kind: :parameter_application, args: %{data_value: %{kind: :every_point}}}, acc -> acc
# Everything else gets added back
ast, acc -> acc ++ [ast]
end)
# will be a point_group or every_point node
group_ast = loop_parameter_appl_ast.args.data_value
# check if it's a point_group first, then fall back to every_point
point_group_arg = group_ast.args[:resource_id] || group_ast.args[:every_point_type]
# lookup all point_groups related to this value
case FarmbotCeleryScript.SysCalls.get_point_group(point_group_arg) do
{:error, reason} ->
quote location: :keep, do: Macro.escape({:error, unquote(reason)})
%{} = point_group ->
# Map over all the points returned by `get_point_group/1`
Enum.map(point_group.point_ids, fn point_id ->
# check if it's an every_point node first, if not fall back go generic pointer
pointer_type = group_ast.args[:every_point_type] || "GenericPointer"
# compile a `execute` ast, injecting the appropriate `point` ast with
# the matching `label`
compile_ast(%FarmbotCeleryScript.AST{
kind: :execute,
args: %{sequence_id: id},
body: [
# this is the injection. This parameter_application was removed
%FarmbotCeleryScript.AST{
kind: :parameter_application,
args: %{
# inject the replacement with the same label
label: loop_parameter_appl_ast.args.label,
data_value: %FarmbotCeleryScript.AST{
kind: :point,
args: %{pointer_type: pointer_type, pointer_id: point_id}
}
}
}
# add all other parmeter_applications back in the case of variables, etc
| parameter_applications
]
})
end)
end
else
quote location: :keep do
# We have to lookup the sequence by it's id.
case FarmbotCeleryScript.SysCalls.get_sequence(unquote(id)) do
%FarmbotCeleryScript.AST{} = ast ->
# compile the ast
env = unquote(compile_params_to_function_args(parameter_applications))
FarmbotCeleryScript.Compiler.compile(ast, env)
error ->
error
end
end
end
end
# Compiles `execute_script`
# TODO(Connor) - make this actually usable
compile :execute_script, %{label: package}, params do
env =
Enum.map(params, fn %{args: %{label: key, value: value}} ->
{to_string(key), value}
end)
quote location: :keep do
package = unquote(compile_ast(package))
env = unquote(Macro.escape(Map.new(env)))
FarmbotCeleryScript.SysCalls.log("Executing Farmware: #{package}", true)
FarmbotCeleryScript.SysCalls.execute_script(package, env)
end
end
compile :update_farmware, %{package: package} do
quote location: :keep do
package = unquote(compile_ast(package))
FarmbotCeleryScript.SysCalls.log("Updating Farmware: #{package}", true)
FarmbotCeleryScript.SysCalls.update_farmware(package)
end
end
# TODO(Connor) - see above TODO
compile :take_photo do
# {:execute_script, [], ["take_photo", {:%{}, [], []}]}
quote location: :keep do
FarmbotCeleryScript.SysCalls.execute_script("take-photo", %{})
end
end
compile :set_user_env, _args, pairs do
kvs =
Enum.map(pairs, fn %{kind: :pair, args: %{label: key, value: value}} ->
quote location: :keep do
FarmbotCeleryScript.SysCalls.set_user_env(unquote(key), unquote(value))
end
end)
quote location: :keep do
(unquote_splicing(kvs))
end
end
compile :install_first_party_farmware, _ do
quote location: :keep do
FarmbotCeleryScript.SysCalls.log("Installing first party Farmware")
FarmbotCeleryScript.SysCalls.install_first_party_farmware()
end
end
# Compiles a nothing block.
compile :nothing do
# AST looks like: {:nothing, [], []}
quote location: :keep do
FarmbotCeleryScript.SysCalls.nothing()
end
end
compile :abort do
quote location: :keep do
Macro.escape({:error, "aborted"})
end
end
# Compiles move_absolute
compile :move_absolute, %{location: location, offset: offset, speed: speed} do
quote location: :keep do
# Extract the location arg
with %{x: locx, y: locy, z: locz} = unquote(compile_ast(location)),
# Extract the offset arg
%{x: offx, y: offy, z: offz} = unquote(compile_ast(offset)) do
# Subtract the location from offset.
# Note: list syntax here for readability.
[x, y, z] = [
locx + offx,
locy + offy,
locz + offz
]
x_str = FarmbotCeleryScript.FormatUtil.format_float(x)
y_str = FarmbotCeleryScript.FormatUtil.format_float(y)
z_str = FarmbotCeleryScript.FormatUtil.format_float(z)
FarmbotCeleryScript.SysCalls.log("Moving to (#{x_str}, #{y_str}, #{z_str})", true)
FarmbotCeleryScript.SysCalls.move_absolute(x, y, z, unquote(compile_ast(speed)))
end
end
end
# compiles move_relative into move absolute
compile :move_relative, %{x: x, y: y, z: z, speed: speed} do
quote location: :keep do
with locx when is_number(locx) <- unquote(compile_ast(x)),
locy when is_number(locy) <- unquote(compile_ast(y)),
locz when is_number(locz) <- unquote(compile_ast(z)),
curx when is_number(curx) <- FarmbotCeleryScript.SysCalls.get_current_x(),
cury when is_number(cury) <- FarmbotCeleryScript.SysCalls.get_current_y(),
curz when is_number(curz) <- FarmbotCeleryScript.SysCalls.get_current_z() do
# Combine them
x = locx + curx
y = locy + cury
z = locz + curz
x_str = FarmbotCeleryScript.FormatUtil.format_float(x)
y_str = FarmbotCeleryScript.FormatUtil.format_float(y)
z_str = FarmbotCeleryScript.FormatUtil.format_float(z)
FarmbotCeleryScript.SysCalls.log(
"Moving relative to (#{x_str}, #{y_str}, #{z_str})",
true
)
FarmbotCeleryScript.SysCalls.move_absolute(x, y, z, unquote(compile_ast(speed)))
end
end
end
# compiles write_pin
compile :write_pin, %{pin_number: num, pin_mode: mode, pin_value: value} do
quote location: :keep do
pin = unquote(compile_ast(num))
mode = unquote(compile_ast(mode))
value = unquote(compile_ast(value))
with :ok <- FarmbotCeleryScript.SysCalls.write_pin(pin, mode, value) do
FarmbotCeleryScript.SysCalls.read_pin(pin, mode)
end
end
end
# compiles read_pin
compile :read_pin, %{pin_number: num, pin_mode: mode} do
quote location: :keep do
pin = unquote(compile_ast(num))
mode = unquote(compile_ast(mode))
FarmbotCeleryScript.SysCalls.read_pin(pin, mode)
end
end
# compiles set_servo_angle
compile :set_servo_angle, %{pin_number: pin_number, pin_value: pin_value} do
quote location: :keep do
pin = unquote(compile_ast(pin_number))
angle = unquote(compile_ast(pin_value))
FarmbotCeleryScript.SysCalls.log("Writing servo: #{pin}: #{angle}")
FarmbotCeleryScript.SysCalls.set_servo_angle(pin, angle)
end
end
# compiles set_pin_io_mode
compile :set_pin_io_mode, %{pin_number: pin_number, pin_io_mode: mode} do
quote location: :keep do
pin = unquote(compile_ast(pin_number))
mode = unquote(compile_ast(mode))
FarmbotCeleryScript.SysCalls.log("Setting pin mode: #{pin}: #{mode}")
FarmbotCeleryScript.SysCalls.set_pin_io_mode(pin, mode)
end
end
# Expands find_home(all) into three find_home/1 calls
compile :find_home, %{axis: "all"} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.log("Finding home on all axes", true)
with :ok <- FarmbotCeleryScript.SysCalls.find_home("z"),
:ok <- FarmbotCeleryScript.SysCalls.find_home("y") do
FarmbotCeleryScript.SysCalls.find_home("x")
end
end
end
# compiles find_home
compile :find_home, %{axis: axis} do
quote location: :keep do
with axis when axis in ["x", "y", "z"] <- unquote(compile_ast(axis)) do
FarmbotCeleryScript.SysCalls.log("Finding home on the #{String.upcase(axis)} axis", true)
FarmbotCeleryScript.SysCalls.find_home(axis)
else
{:error, reason} ->
{:error, reason}
end
end
end
# Expands home(all) into three home/1 calls
compile :home, %{axis: "all", speed: speed} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.log("Going to home on all axes", true)
with speed when is_number(speed) <- unquote(compile_ast(speed)),
:ok <- FarmbotCeleryScript.SysCalls.home("z", speed),
:ok <- FarmbotCeleryScript.SysCalls.home("y", speed) do
FarmbotCeleryScript.SysCalls.home("x", speed)
end
end
end
# compiles home
compile :home, %{axis: axis, speed: speed} do
quote location: :keep do
with axis when axis in ["x", "y", "z"] <- unquote(compile_ast(axis)),
speed when is_number(speed) <- unquote(compile_ast(speed)) do
FarmbotCeleryScript.SysCalls.log("Going to home on the #{String.upcase(axis)} axis", true)
FarmbotCeleryScript.SysCalls.home(axis, speed)
else
{:error, reason} ->
{:error, reason}
end
end
end
# Expands zero(all) into three zero/1 calls
compile :zero, %{axis: "all"} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.log("Zeroing all axes", true)
with :ok <- FarmbotCeleryScript.SysCalls.zero("z"),
:ok <- FarmbotCeleryScript.SysCalls.zero("y") do
FarmbotCeleryScript.SysCalls.zero("x")
end
end
end
# compiles zero
compile :zero, %{axis: axis} do
quote location: :keep do
with axis when axis in ["x", "y", "z"] <- unquote(compile_ast(axis)) do
FarmbotCeleryScript.SysCalls.log("Zeroing the #{String.upcase(axis)} axis", true)
FarmbotCeleryScript.SysCalls.zero(axis)
else
{:error, reason} ->
{:error, reason}
end
end
end
# Expands calibrate(all) into three calibrate/1 calls
compile :calibrate, %{axis: "all"} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.log("Calibrating all axes", true)
with :ok <- FarmbotCeleryScript.SysCalls.calibrate("z"),
:ok <- FarmbotCeleryScript.SysCalls.calibrate("y") do
FarmbotCeleryScript.SysCalls.calibrate("x")
else
{:error, reason} ->
{:error, reason}
end
end
end
# compiles calibrate
compile :calibrate, %{axis: axis} do
quote location: :keep do
with axis when axis in ["x", "y", "z"] <- unquote(compile_ast(axis)) do
FarmbotCeleryScript.SysCalls.log("Calibrating the #{String.upcase(axis)} axis", true)
FarmbotCeleryScript.SysCalls.calibrate(axis)
else
{:error, reason} ->
{:error, reason}
end
end
end
compile :wait, %{milliseconds: millis} do
quote location: :keep do
with millis when is_integer(millis) <- unquote(compile_ast(millis)) do
FarmbotCeleryScript.SysCalls.log("Waiting for #{millis} milliseconds")
FarmbotCeleryScript.SysCalls.wait(millis)
else
{:error, reason} ->
{:error, reason}
end
end
end
compile :send_message, %{message: msg, message_type: type}, channels 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)),
unquote(compile_ast(msg)),
unquote(channels)
)
end
end
# compiles coordinate
# Coordinate should return a vec3
compile :coordinate, %{x: x, y: y, z: z} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.coordinate(
unquote(compile_ast(x)),
unquote(compile_ast(y)),
unquote(compile_ast(z))
)
end
end
# compiles point
compile :point, %{pointer_type: type, pointer_id: id} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.point(unquote(compile_ast(type)), unquote(compile_ast(id)))
end
end
# compile a named pin
compile :named_pin, %{pin_id: id, pin_type: type} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.named_pin(unquote(compile_ast(type)), unquote(compile_ast(id)))
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.
compile :identifier, %{label: var_name} do
var_name = IdentifierSanitizer.to_variable(var_name)
quote location: :keep do
unquote({var_name, [], nil})
end
end
compile :tool, %{tool_id: tool_id} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.get_toolslot_for_tool(unquote(compile_ast(tool_id)))
end
end
compile :emergency_lock do
quote location: :keep do
FarmbotCeleryScript.SysCalls.emergency_lock()
end
end
compile :emergency_unlock do
quote location: :keep do
FarmbotCeleryScript.SysCalls.emergency_unlock()
end
end
compile :read_status do
quote location: :keep do
FarmbotCeleryScript.SysCalls.read_status()
end
end
compile :sync do
quote location: :keep do
FarmbotCeleryScript.SysCalls.sync()
end
end
compile :check_updates, %{package: "farmbot_os"} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.check_update()
end
end
compile :flash_firmware, %{package: package_name} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.flash_firmware(unquote(compile_ast(package_name)))
end
end
compile :power_off do
quote location: :keep do
FarmbotCeleryScript.SysCalls.power_off()
end
end
compile :reboot, %{package: "farmbot_os"} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.reboot()
end
end
compile :reboot, %{package: "arduino_firmware"} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.firmware_reboot()
end
end
compile :factory_reset, %{package: package} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.factory_reset(unquote(compile_ast(package)))
end
end
compile :change_ownership, %{}, body 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(email),
unquote(secret),
unquote(server)
)
end
end
compile :dump_info do
quote location: :keep do
FarmbotCeleryScript.SysCalls.dump_info()
end
end
compile :toggle_pin, %{pin_number: pin_number} do
quote location: :keep do
FarmbotCeleryScript.SysCalls.toggle_pin(unquote(pin_number))
end
end
compile :resource_update,
%{resource_type: kind, resource_id: id, label: label, value: value},
body do
initial = %{label => value}
# Technically now body isn't supported by this node.
extra =
Map.new(body, fn %{args: %{label: label, data_value: value}} ->
{label, value}
end)
# Make sure the initial stuff higher most priority
params = Map.merge(extra, initial)
quote do
FarmbotCeleryScript.SysCalls.resource_update(
unquote(compile_ast(kind)),
unquote(compile_ast(id)),
unquote(Macro.escape(params))
)
end
end
@doc """
Recursively compiles a list or single Celery AST into an Elixir `__block__`
"""
def compile_block(asts, acc \\ [])
def compile_block(%AST{} = ast, _) do
case compile_ast(ast) do
{_, _, _} = compiled ->
{:__block__, [], [compiled]}
compiled when is_list(compiled) ->
{:__block__, [], compiled}
end
end
def compile_block([ast | rest], acc) do
case compile_ast(ast) do
{_, _, _} = compiled ->
compile_block(rest, acc ++ [compiled])
compiled when is_list(compiled) ->
compile_block(rest, acc ++ compiled)
end
end
def compile_block([], acc), do: {:__block__, [], acc}
@doc """
Compiles a `execute` block to a parameter block
# Example
The body of this `execute` node
{
"kind": "execute",
"args": {
"sequence_id": 123
},
"body": [
{
"kind": "variable_declaration",
"args": {
"label": "variable_in_this_scope",
"data_value": {
"kind": "identifier",
"args": {
"label": "variable_in_next_scope"
}
}
}
}
]
}
Would be compiled to:
[variable_in_next_scope: variable_in_this_scope]
"""
def compile_params_to_function_args(list, acc \\ [])
def compile_params_to_function_args(
[%{kind: :parameter_application, args: args} | rest],
acc
) do
%{
label: next_scope_var_name,
data_value: data_value
} = args
next_scope_var_name = IdentifierSanitizer.to_variable(next_scope_var_name)
# next_value = compile_ast(data_value)
var =
quote location: :keep do
{unquote(next_scope_var_name), unquote(compile_ast(data_value))}
end
compile_params_to_function_args(rest, [var | acc])
end
def compile_params_to_function_args([], acc), do: acc
@doc """
Compiles a function block's params.
# Example
A `sequence`s `locals` that look like
{
"kind": "scope_declaration",
"args": {},
"body": [
{
"kind": "parameter_declaration",
"args": {
"label": "parent",
"default_value": {
"kind": "coordinate",
"args": {
"x": 100.0,
"y": 200.0,
"z": 300.0
}
}
}
]
}
Would be compiled to
parent = Keyword.get(params, :parent, %{x: 100, y: 200, z: 300})
"""
# Add parameter_declaration to the list of implemented kinds
@kinds "parameter_declaration"
def compile_param_declaration(%{args: %{label: var_name, default_value: default}}) do
var_name = IdentifierSanitizer.to_variable(var_name)
quote location: :keep do
unquote({var_name, [], __MODULE__}) =
Keyword.get(params, unquote(var_name), unquote(compile_ast(default)))
end
end
@doc """
Compiles a function block's assigned value.
# Example
A `sequence`s `locals` that look like
{
"kind": "scope_declaration",
"args": {},
"body": [
{
"kind": "parameter_application",
"args": {
"label": "parent",
"data_value": {
"kind": "coordinate",
"args": {
"x": 100.0,
"y": 200.0,
"z": 300.0
}
}
}
}
]
}
"""
# Add parameter_application to the list of implemented kinds
@kinds "parameter_application"
def compile_param_application(%{args: %{label: var_name, data_value: value}}) do
var_name = IdentifierSanitizer.to_variable(var_name)
quote location: :keep do
unquote({var_name, [], __MODULE__}) = unquote(compile_ast(value))
end
end
defp decompose_block_to_steps({:__block__, _, steps} = _orig) do
Enum.map(steps, fn step ->
quote location: :keep do
fn -> unquote(step) end
end
end)
end
defp add_sequence_init_and_complete_logs(steps, sequence_name) when is_binary(sequence_name) do
# This looks really weird because of the logs before and
# after the compiled steps
List.flatten([
quote do
fn ->
FarmbotCeleryScript.SysCalls.sequence_init_log("Starting #{unquote(sequence_name)}")
end
end,
steps,
quote do
fn ->
FarmbotCeleryScript.SysCalls.sequence_complete_log("#{unquote(sequence_name)} complete")
end
end
])
end
defp add_sequence_init_and_complete_logs(steps, _) do
steps
end
# defp print_compiled_code(compiled) do
# compiled
# |> Macro.to_string()
# |> Code.format_string!()
# |> IO.puts()
# end
end