farmbot_os/farmbot_celery_script/lib/farmbot_celery_script/compilers/sequence_compiler.ex

186 lines
6.3 KiB
Elixir

defmodule FarmbotCeleryScript.Compiler.Sequence do
import FarmbotCeleryScript.Compiler.Utils
@iterables [:point_group, :every_point]
def sequence(%{args: %{locals: %{body: params_or_iterables}}} = ast, env) do
# if there is an iterable AST here,
# we need to compile _many_ sequences, not just one.
loop_parameter_appl_ast =
Enum.find_value(params_or_iterables, 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 loop_parameter_appl_ast,
do: compile_sequence_iterable(loop_parameter_appl_ast, ast, env),
else: compile_sequence(ast, env)
end
def compile_sequence_iterable(
loop_parameter_appl_ast,
%{
args: %{locals: %{body: params} = locals} = sequence_args,
meta: sequence_meta
} = sequence_ast,
env
) do
sequence_name =
sequence_meta[:sequence_name] || sequence_args[:sequence_name]
# remove the iterable from the parameter applications,
# since it will be injected after this.
_params =
Enum.reduce(params, [], 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[:point_group_id] || 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)})
%{name: group_name} = point_group ->
total = Enum.count(point_group.point_ids)
# Map over all the points returned by `get_point_group/1`
{body, _} =
Enum.reduce(point_group.point_ids, {[], 1}, fn point_id,
{acc, index} ->
# 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"
parameter_application = %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}
}
}
}
sequence_name =
case FarmbotCeleryScript.SysCalls.point(pointer_type, point_id) do
%{name: name, x: x, y: y, z: z} when is_binary(sequence_name) ->
pos = FarmbotCeleryScript.FormatUtil.format_coord(x, y, z)
sequence_name <> " [#{index} / #{total}] - #{name} #{pos}"
%{name: name, x: x, y: y, z: z} ->
pos = FarmbotCeleryScript.FormatUtil.format_coord(x, y, z)
"unnamed iterable sequence [#{index} / #{total}] - #{name} #{
pos
}"
_ ->
"unknown iterable [#{index} / #{total}]"
end
# compile a `sequence` ast, injecting the appropriate `point` ast with
# the matching `label`
# TODO(Connor) - the body of this ast should have the
# params as sorted earlier. Figure out why this doesn't work
body =
compile_sequence(
%{
sequence_ast
| meta: %{sequence_name: sequence_name},
args: %{locals: %{locals | body: [parameter_application]}}
},
env
)
{acc ++ body, index + 1}
end)
add_sequence_init_and_complete_logs_ittr(
body,
sequence_name <> " - #{group_name} (#{total} items)"
)
end
end
def compile_sequence(
%{args: %{locals: %{body: params}} = args, body: block, meta: meta},
env
) 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, env)], body}
# declares usage of a variable as defined inside the body of itself
%{kind: :parameter_application} ->
{params ++ [compile_param_application(ast, env)], body}
# defines a variable exists
%{kind: :variable_declaration} ->
{params, body ++ [ast]}
end
end)
{:__block__, env, assignments} = compile_block(body, env)
sequence_name = meta[:sequence_name] || args[:sequence_name]
steps = compile_block(block, env) |> 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
end