From fbb9d294a8d62e6af1259720ab85b4edff4f42c1 Mon Sep 17 00:00:00 2001 From: connor rigby Date: Tue, 29 Jan 2019 15:01:08 -0800 Subject: [PATCH] Cleanup+document new ElixirAST compiler --- .../fixture/master_sequence.json | 375 +++++++++--------- .../lib/farmbot_celery_script/ast.ex | 11 +- .../lib/farmbot_celery_script/ast/compiler.ex | 236 +++++++---- .../mix/tasks.farmbot.celeryscript.compile.ex | 95 ++++- 4 files changed, 434 insertions(+), 283 deletions(-) diff --git a/farmbot_celery_script/fixture/master_sequence.json b/farmbot_celery_script/fixture/master_sequence.json index 0e473787..88cd849e 100644 --- a/farmbot_celery_script/fixture/master_sequence.json +++ b/farmbot_celery_script/fixture/master_sequence.json @@ -1,194 +1,199 @@ { - "kind": "sequence", - "name": "Test Sequence (TM)", - "color": "red", - "id": 2, - "args": { - "version": 20180209, - "locals": { - "kind": "scope_declaration", - "args": {} - } - }, - "body": [{ - "kind": "move_absolute", - "args": { - "speed": 100, - "offset": { - "kind": "coordinate", - "args": { - "y": 20, - "x": 10, - "z": -30 - } - }, - "location": { - "kind": "point", - "args": { - "pointer_type": "Plant", - "pointer_id": 1 - } + "kind": "sequence", + "name": "Test Sequence (TM)", + "color": "red", + "id": 2, + "comment": "This is the root", + "args": { + "version": 20180209, + "locals": { + "kind": "scope_declaration", + "args": {} } - } }, - { - "kind": "move_relative", - "args": { - "x": 10, - "y": 20, - "z": 30, - "speed": 50 - }, - "comment": "Slow move" - }, - { - "kind": "write_pin", - "args": { - "pin_number": 0, - "pin_value": 0, - "pin_mode": 0 - } - }, - { - "kind": "write_pin", - "args": { - "pin_mode": 0, - "pin_value": 1, - "pin_number": { - "kind": "named_pin", - "args": { - "pin_type": "Peripheral", - "pin_id": 5 - } - } - } - }, - { - "kind": "read_pin", - "args": { - "pin_mode": 0, - "label": "---", - "pin_number": 0 - } - }, - { - "kind": "read_pin", - "args": { - "pin_mode": 1, - "label": "---", - "pin_number": { - "kind": "named_pin", - "args": { - "pin_type": "Sensor", - "pin_id": 1 - } - } - } - }, - { - "kind": "wait", - "args": { - "milliseconds": 100 - } - }, - { - "kind": "send_message", - "args": { - "message": "FarmBot is at position {{ x }}, {{ y }}, {{ z }}.", - "message_type": "success" - }, - "body": [{ - "kind": "channel", - "args": { - "channel_name": "toast" - } + "body": [ + { + "kind": "move_absolute", + "args": { + "speed": 100, + "offset": { + "kind": "coordinate", + "args": { + "y": 20, + "x": 10, + "z": -30 + } + }, + "location": { + "kind": "point", + "args": { + "pointer_type": "Plant", + "pointer_id": 1 + } + } + } }, { - "kind": "channel", - "args": { - "channel_name": "email" - } + "kind": "move_relative", + "args": { + "x": 10, + "y": 20, + "z": 30, + "speed": 50 + }, + "comment": "Slow move" }, { - "kind": "channel", - "args": { - "channel_name": "espeak" - } + "kind": "write_pin", + "args": { + "pin_number": 0, + "pin_value": 0, + "pin_mode": 0 + } + }, + { + "kind": "write_pin", + "args": { + "pin_mode": 0, + "pin_value": 1, + "pin_number": { + "kind": "named_pin", + "args": { + "pin_type": "Peripheral", + "pin_id": 5 + } + } + } + }, + { + "kind": "read_pin", + "args": { + "pin_mode": 0, + "label": "---", + "pin_number": 0 + } + }, + { + "kind": "read_pin", + "args": { + "pin_mode": 1, + "label": "---", + "pin_number": { + "kind": "named_pin", + "args": { + "pin_type": "Sensor", + "pin_id": 1 + } + } + } + }, + { + "kind": "wait", + "args": { + "milliseconds": 100 + } + }, + { + "kind": "send_message", + "args": { + "message": "FarmBot is at position {{ x }}, {{ y }}, {{ z }}.", + "message_type": "success" + }, + "body": [ + { + "kind": "channel", + "args": { + "channel_name": "toast" + } + }, + { + "kind": "channel", + "args": { + "channel_name": "email" + } + }, + { + "kind": "channel", + "args": { + "channel_name": "espeak" + } + } + ] + }, + { + "kind": "find_home", + "args": { + "speed": 100, + "axis": "all" + } + }, + { + "kind": "_if", + "args": { + "rhs": 0, + "op": "is_undefined", + "lhs": "x", + "_then": { + "kind": "execute", + "args": { + "sequence_id": 1 + } + }, + "_else": { + "kind": "nothing", + "args": {} + } + } + }, + { + "kind": "_if", + "args": { + "rhs": 500, + "op": ">", + "_then": { + "kind": "nothing", + "args": {} + }, + "_else": { + "kind": "execute", + "args": { + "sequence_id": 1 + } + }, + "lhs": { + "kind": "named_pin", + "args": { + "pin_type": "Sensor", + "pin_id": 2 + } + } + } + }, + { + "kind": "execute", + "args": { + "sequence_id": 1 + } + }, + { + "kind": "execute_script", + "args": { + "label": "plant-detection" + }, + "body": [ + { + "kind": "pair", + "args": { + "value": 0, + "label": "plant_detection_input" + }, + "comment": "Input" + } + ] + }, + { + "kind": "take_photo", + "args": {} } - ] - }, - { - "kind": "find_home", - "args": { - "speed": 100, - "axis": "all" - } - }, - { - "kind": "_if", - "args": { - "rhs": 0, - "op": "is_undefined", - "lhs": "x", - "_then": { - "kind": "execute", - "args": { - "sequence_id": 1 - } - }, - "_else": { - "kind": "nothing", - "args": {} - } - } - }, - { - "kind": "_if", - "args": { - "rhs": 500, - "op": ">", - "_then": { - "kind": "nothing", - "args": {} - }, - "_else": { - "kind": "execute", - "args": { - "sequence_id": 1 - } - }, - "lhs": { - "kind": "named_pin", - "args": { - "pin_type": "Sensor", - "pin_id": 2 - } - } - } - }, - { - "kind": "execute", - "args": { - "sequence_id": 1 - } - }, - { - "kind": "execute_script", - "args": { - "label": "plant-detection" - }, - "body": [{ - "kind": "pair", - "args": { - "value": 0, - "label": "plant_detection_input" - }, - "comment": "Input" - }] - }, - { - "kind": "take_photo", - "args": {} - } - ] + ] } \ No newline at end of file diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast.ex index 113bdc8f..dbadddfa 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/ast.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/ast.ex @@ -4,7 +4,7 @@ defmodule Farmbot.CeleryScript.AST do Ast nodes. """ alias Farmbot.CeleryScript.AST - alias AST.{Heap, Slicer, Unslicer} + alias AST.{Compiler, Heap, Slicer, Unslicer} @typedoc "Arguments to a ast node." @type args :: map @@ -30,7 +30,7 @@ defmodule Farmbot.CeleryScript.AST do def decode(map_or_list_of_maps) def decode(%{__struct__: _} = thing) do - thing |> Map.from_struct() |> decode + thing |> Map.from_struct() |> decode() end def decode(%{} = thing) do @@ -71,11 +71,12 @@ defmodule Farmbot.CeleryScript.AST do end @spec new(atom, map, [map]) :: t() - def new(kind, args, body) when is_map(args) and is_list(body) do + def new(kind, args, body, comment \\ nil) when is_map(args) and is_list(body) do %__MODULE__{ kind: String.to_atom(to_string(kind)), args: args, - body: body + body: body, + comment: comment } |> decode() end @@ -86,4 +87,6 @@ defmodule Farmbot.CeleryScript.AST do @spec unslice(Heap.t(), Address.t()) :: AST.t() def unslice(%Heap{} = heap, %Address{} = addr), do: Unslicer.run(heap, addr) + + defdelegate compile(ast), to: Compiler end diff --git a/farmbot_celery_script/lib/farmbot_celery_script/ast/compiler.ex b/farmbot_celery_script/lib/farmbot_celery_script/ast/compiler.ex index f5f6f847..36c0d506 100644 --- a/farmbot_celery_script/lib/farmbot_celery_script/ast/compiler.ex +++ b/farmbot_celery_script/lib/farmbot_celery_script/ast/compiler.ex @@ -8,9 +8,19 @@ defmodule Farmbot.CeleryScript.AST.Compiler do @doc """ Recursive function that will emit Elixir AST from CeleryScript AST. + + ## CeleryEnv + A keyword list of the compile environment. + * resource_id + * resource_type """ + def compile(ast, celery_env \\ [resource_id: 0, resource_type: "unspecified"]) + # Compiles a `sequence` into an Elixir `fn`. - def compile(%AST{kind: :sequence, args: %{locals: %{body: params}}, body: block}) do + def compile( + %AST{kind: :sequence, args: %{locals: %{body: params}}, body: block} = ast, + celery_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. @@ -30,13 +40,18 @@ defmodule Farmbot.CeleryScript.AST.Compiler do # parent = Keyword.fetch!(params, :parent) unquote_splicing(params) # Unquote the remaining sequence steps. - unquote(compile_block(body ++ block)) + unquote(compile_block(body ++ block, celery_env)) end end + |> add_meta(ast, celery_env) end # Compiles a variable asignment. - def compile(%AST{kind: :variable_declaration, args: %{label: var_name, data_value: ast}}) do + def compile( + %AST{kind: :variable_declaration, args: %{label: var_name, data_value: data_value_ast}} = + ast, + celery_env + ) do # Compiles the `data_value` # and assigns the result to a variable named `label` # Example: @@ -54,15 +69,21 @@ defmodule Farmbot.CeleryScript.AST.Compiler do # } # } # Will be turned into: - # parent = point("Plant", 456) - {:=, [], [{String.to_atom(var_name), [], __MODULE__}, compile(ast)]} + # parent = point("Plant", 456) + # NOTE: This needs to be Elixir AST syntax, not quoted + # because var! doesn't do what what we need. + {:=, [], [{String.to_atom(var_name), [], __MODULE__}, compile(data_value_ast, celery_env)]} + |> add_meta(ast, celery_env) end # Compiles an if statement. - def compile(%AST{ - kind: :_if, - args: %{_then: then_ast, _else: else_ast, lhs: lhs, op: op, rhs: rhs} - }) do + def compile( + %AST{ + kind: :_if, + args: %{_then: then_ast, _else: else_ast, lhs: lhs, op: op, rhs: rhs} + } = ast, + celery_env + ) do # 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. @@ -74,7 +95,7 @@ defmodule Farmbot.CeleryScript.AST.Compiler do "y" -> {:get_current_y, [], []} "z" -> {:get_current_z, [], []} "pin" <> pin -> {:read_pin, [], [String.to_integer(pin), nil]} - %AST{} = ast -> compile(ast) + %AST{} = ast -> compile(ast, celery_env) end # Turn the `op` arg into Elixir code @@ -89,13 +110,13 @@ defmodule Farmbot.CeleryScript.AST.Compiler do # read_pin(22, nil) == 5 # The ast will look like: {:==, [], lhs, compile(rhs)} quote do - unquote(lhs) == unquote(compile(lhs)) + unquote(lhs) == unquote(compile(lhs, celery_env)) end "not" -> # ast will look like: {:!=, [], [lhs, compile(rhs)]} quote do - unquote(lhs) != unquote(compile(rhs)) + unquote(lhs) != unquote(compile(rhs, celery_env)) end "is_undefined" -> @@ -107,21 +128,16 @@ defmodule Farmbot.CeleryScript.AST.Compiler do "<" -> # ast will look like: {:<, [], [lhs, compile(rhs)]} quote do - unquote(lhs) < unquote(compile(rhs)) + unquote(lhs) < unquote(compile(rhs, celery_env)) end ">" -> # ast will look like: {:>, [], [lhs, compile(rhs)]} quote do - unquote(lhs) > unquote(compile(rhs)) + unquote(lhs) > unquote(compile(rhs, celery_env)) end end - # {:if, [], [ - # if_eval, - # [do: compile_block(then_ast), else: compile_block(else_ast)] - # ]} - # Finally, compile the entire if statement. # outputted code will look something like: # if get_current_x() == 123 do @@ -131,19 +147,23 @@ defmodule Farmbot.CeleryScript.AST.Compiler do # end quote do if unquote(if_eval), - do: unquote(compile_block(then_ast)), - else: unquote(compile_block(else_ast)) + do: unquote(compile_block(then_ast, celery_env)), + else: unquote(compile_block(else_ast, celery_env)) end + |> add_meta(ast, celery_env) end # Compiles an `execute` block. - def compile(%AST{kind: :execute, args: %{sequence_id: id}, body: variable_declarations}) do + def compile( + %AST{kind: :execute, args: %{sequence_id: id}, body: variable_declarations} = ast, + celery_env + ) do quote do # We have to lookup the sequence by it's id. case get_sequence(unquote(id)) do - {:ok, %AST{} = ast} -> + {:ok, %AST{} = ast, new_celery_env} -> # compile the ast - fun = unquote(__MODULE__).compile_block(ast) + fun = unquote(__MODULE__).compile(ast, new_celery_env) # And call it, serializing all the variables it expects. # see the `compile_param_application/1` docs for more info. fun.(unquote(compile_param_application(variable_declarations))) @@ -152,117 +172,149 @@ defmodule Farmbot.CeleryScript.AST.Compiler do handle_error(err) end end + |> add_meta(ast, celery_env) end # Compiles `execute_script` # TODO(Connor) - make this actually usable - def compile(%AST{kind: :execute_script, args: %{label: package}, body: params}) do + def compile( + %AST{kind: :execute_script, args: %{label: package}, body: params} = ast, + celery_env + ) do env = Enum.map(params, fn %{args: %{label: key, value: value}} -> {to_string(key), value} end) - # {:execute_script, [], [package, {:%{}, [], env}]} quote do execute_script(unquote(package), unquote(Map.new(env))) end + |> add_meta(ast, celery_env) end # TODO(Connor) - see above TODO - def compile(%AST{kind: :take_photo}) do + def compile(%AST{kind: :take_photo} = ast, celery_env) do # {:execute_script, [], ["take_photo", {:%{}, [], []}]} quote do execute_script("take_photo", %{}) end + |> add_meta(ast, celery_env) end # Compiles a nothing block. - def compile(%AST{kind: :nothing}) do + def compile(%AST{kind: :nothing} = ast, celery_env) do # AST looks like: {:nothing, [], []} quote do nothing() end + |> add_meta(ast, celery_env) end # Compiles move_absolute - def compile(%AST{ - kind: :move_absolute, - args: %{location: location, offset: offset, speed: speed} - }) do + def compile( + %AST{ + kind: :move_absolute, + args: %{location: location, offset: offset, speed: speed} + } = ast, + celery_env + ) do quote do # Extract the location arg - %{x: locx, y: locy, z: locz} = unquote(compile(location)) + %{x: locx, y: locy, z: locz} = unquote(compile(location, celery_env)) # Extract the offset arg - %{x: offx, y: offy, z: offz} = unquote(compile(offset)) + %{x: offx, y: offy, z: offz} = unquote(compile(offset, celery_env)) # Subtract the location from offset. # Note: list syntax here for readability. [x, y, z] = [offx - locx, offy - locy, offz - locz] - move_absolute(x, y, z, unquote(compile(speed))) + move_absolute(x, y, z, unquote(compile(speed, celery_env))) end + |> add_meta(ast, celery_env) end # compiles move_relative into move absolute - def compile(%AST{kind: :move_relative, args: %{x: x, y: y, z: z, speed: speed}}) do + def compile( + %AST{kind: :move_relative, args: %{x: x, y: y, z: z, speed: speed}} = ast, + celery_env + ) do quote do # build a vec3 of passed in args %{x: locx, y: locy, z: locz} = %{ - x: unquote(compile(x)), - y: unquote(compile(y)), - z: unquote(compile(z)) + x: unquote(compile(x, celery_env)), + y: unquote(compile(y, celery_env)), + z: unquote(compile(z, celery_env)) } # build a vec3 of the current position - %{x: offx, y: offy, z: offz} = %{x: get_current_x(), y: get_current_y, z: get_current_y()} + %{x: offx, y: offy, z: offz} = %{ + x: get_current_x(), + y: get_current_y, + z: get_current_y() + } # Subtract the location from offset. # Note: list syntax here for readability. [x, y, z] = [offx - locx, offy - locy, offz - locz] - move_absolute(x, y, z, unquote(compile(speed))) + move_absolute(x, y, z, unquote(compile(speed, celery_env))) end + |> add_meta(ast, celery_env) end # compiles write_pin - def compile(%AST{kind: :write_pin, args: %{pin_number: num, pin_mode: mode, pin_value: val}}) do - # {:write_pin, [], [compile(num), compile(mode), compile(val)]} + def compile( + %AST{kind: :write_pin, args: %{pin_number: num, pin_mode: mode, pin_value: val}} = ast, + celery_env + ) do quote do - write_pin(unquote(compile(num)), unquote(compile(mode)), unquote(compile(val))) + write_pin( + unquote(compile(num, celery_env)), + unquote(compile(mode, celery_env)), + unquote(compile(val, celery_env)) + ) end + |> add_meta(ast, celery_env) end # compiles read_pin - def compile(%AST{kind: :read_pin, args: %{pin_number: num, pin_mode: mode}}) do - # {:read_pin, [], [compile(num), compile(mode)]} + def compile(%AST{kind: :read_pin, args: %{pin_number: num, pin_mode: mode}} = ast, celery_env) do quote do - read_pin(unquote(compile(num)), unquote(compile(mode))) + read_pin(unquote(compile(num, celery_env)), unquote(compile(mode, celery_env))) end + |> add_meta(ast, celery_env) end # Expands find_home(all) into three find_home/1 calls - def compile(%AST{kind: :find_home, args: %{axis: "all", speed: speed}}) do + def compile(%AST{kind: :find_home, args: %{axis: "all", speed: speed}} = ast, celery_env) do quote do - find_home("x", unquote(compile(speed))) - find_home("y", unquote(compile(speed))) - find_home("z", unquote(compile(speed))) + find_home("x", unquote(compile(speed, celery_env))) + find_home("y", unquote(compile(speed, celery_env))) + find_home("z", unquote(compile(speed, celery_env))) end + |> add_meta(ast, celery_env) end # compiles find_home - def compile(%AST{kind: :find_home, args: %{axis: axis, speed: speed}}) do + def compile(%AST{kind: :find_home, args: %{axis: axis, speed: speed}} = ast, celery_env) do quote do - find_home(unquote(compile(axis)), unquote(compile(speed))) + find_home(unquote(compile(axis, celery_env)), unquote(compile(speed, celery_env))) end + |> add_meta(ast, celery_env) end # compiles wait - def compile(%AST{kind: :wait, args: %{milliseconds: millis}}) do + def compile(%AST{kind: :wait, args: %{milliseconds: millis}} = ast, celery_env) do quote do - find_home(unquote(compile(millis))) + find_home(unquote(compile(millis, celery_env))) end + |> add_meta(ast, celery_env) end # compiles send_message - def compile(%AST{kind: :send_message, args: %{message: msg, message_type: type}, body: channels}) do + def compile( + %AST{kind: :send_message, args: %{message: msg, message_type: type}, body: channels} = + ast, + celery_env + ) do # body gets turned into a list of atoms. # Example: # [{kind: "channel", args: {channel_name: "email"}}] @@ -275,69 +327,84 @@ defmodule Farmbot.CeleryScript.AST.Compiler do quote do # send_message("success", "Hello world!", [:email, :toast]) - send_message(unquote(compile(type)), unquote(compile(msg)), unquote(channels)) + send_message( + unquote(compile(type, celery_env)), + unquote(compile(msg, celery_env)), + unquote(channels) + ) end + |> add_meta(ast, celery_env) end # compiles coordinate # Coordinate should return a vec3 - def compile(%AST{kind: :coordinate, args: %{x: x, y: y, z: z}}) do + def compile(%AST{kind: :coordinate, args: %{x: x, y: y, z: z}} = ast, celery_env) do quote do - coordinate(unquote(compile(x)), unquote(compile(y)), unquote(compile(z))) + coordinate( + unquote(compile(x, celery_env)), + unquote(compile(y, celery_env)), + unquote(compile(z, celery_env)) + ) end + |> add_meta(ast, celery_env) end # compiles point - def compile(%AST{kind: :point, args: %{pointer_type: type, pointer_id: id}}) do + def compile(%AST{kind: :point, args: %{pointer_type: type, pointer_id: id}} = ast, celery_env) do quote do - point(unquote(compile(type)), unquote(compile(id))) + point(unquote(compile(type, celery_env)), unquote(compile(id, celery_env))) end + |> add_meta(ast, celery_env) end # compile a named pin - def compile(%AST{kind: :named_pin, args: %{pin_id: id, pin_type: type}}) do + def compile(%AST{kind: :named_pin, args: %{pin_id: id, pin_type: type}} = ast, celery_env) do quote do - pin(unquote(compile(type)), unquote(compile(id))) + pin(unquote(compile(type, celery_env)), unquote(compile(id, celery_env))) end + |> add_meta(ast, celery_env) 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 compile(%AST{kind: :identifier, args: %{label: var_name}}) do - {String.to_atom(var_name), [], __MODULE__} + def compile(%AST{kind: :identifier, args: %{label: var_name}} = ast, celery_env) do + meta = [celery_kind: :identifier, celery_comments: []] + + {String.to_atom(var_name), meta, __MODULE__} + |> add_meta(ast, celery_env) end # Numbers and strings are treated as literals. - def compile(lit) when is_number(lit), do: lit - def compile(lit) when is_binary(lit), do: lit + def compile(lit, _celery_env) when is_number(lit), do: lit + def compile(lit, _celery_env) when is_binary(lit), do: lit @doc """ Recursively compiles a list or single Celery AST into an Elixir `__block__` """ - def compile_block(asts, acc \\ []) + def compile_block(asts, celery_env, acc \\ []) - def compile_block(%AST{} = ast, _) do - case compile(ast) do + def compile_block(%AST{} = ast, celery_env, _) do + case compile(ast, celery_env) do {_, _, _} = compiled -> - compiled + {:__block__, [], [compiled]} compiled when is_list(compiled) -> {:__block__, [], compiled} end end - def compile_block([ast | rest], acc) do - case compile(ast) do + def compile_block([ast | rest], celery_env, acc) do + case compile(ast, celery_env) do {_, _, _} = compiled -> - compile_block(rest, acc ++ [compiled]) + compile_block(rest, celery_env, acc ++ [compiled]) compiled when is_list(compiled) -> - compile_block(rest, acc ++ compiled) + compile_block(rest, celery_env, acc ++ compiled) end end - def compile_block([], acc), do: {:__block__, [], acc} + def compile_block([], _, acc), do: {:__block__, [], acc} @doc """ Compiles a `execute` block to a paramater block @@ -405,12 +472,25 @@ defmodule Farmbot.CeleryScript.AST.Compiler do parent = Keyword.fetch!(params, :parent) """ - def compile_param_declaration(%{args: %{label: var_name, data_type: type}}) do + def compile_param_declaration(%{args: %{label: var_name, data_type: _type}}) do var_fetch = quote do Keyword.fetch!(params, unquote(String.to_atom(var_name))) end - {:=, [type: type], [{String.to_atom(var_name), [], __MODULE__}, var_fetch]} + {:=, [], [{String.to_atom(var_name), [], __MODULE__}, var_fetch]} + end + + @doc "Adds metadata about a celery ast to the resulting elixir ast." + def add_meta({a, meta, body}, %AST{kind: kind, comment: comment}, more \\ []) do + meta = + meta + |> Keyword.update(:celery_comments, [comment], fn comments -> + (comment || []) ++ comments + end) + |> Keyword.put(:celery_kind, kind) + |> Keyword.merge(more) + + {a, meta, body} end end diff --git a/farmbot_celery_script/lib/mix/tasks.farmbot.celeryscript.compile.ex b/farmbot_celery_script/lib/mix/tasks.farmbot.celeryscript.compile.ex index 3cb5af71..46a609a7 100644 --- a/farmbot_celery_script/lib/mix/tasks.farmbot.celeryscript.compile.ex +++ b/farmbot_celery_script/lib/mix/tasks.farmbot.celeryscript.compile.ex @@ -1,24 +1,87 @@ defmodule Mix.Tasks.Farmbot.CeleryScript.Compile do @moduledoc """ Compile a json representation of CeleryScript + + # Usage + mix farmbot.celery_script.compile [switches] input1.json input2.json + + # Switches + * `--out filename.exs` (default=stdout) - Output compiled Elixir code to `filename.exs` + * `--ast filename.exs` (default=false) - Output Elixir AST to `filename.exs` + * `--format false` (default=true) - Format the resulting code """ + @shortdoc true + + alias Farmbot.CeleryScript.AST + use Mix.Task - def run([in_filename, out_filename]) do - File.read!(in_filename) - |> Jason.decode!() - |> Farmbot.CeleryScript.AST.decode() - |> Farmbot.CeleryScript.AST.Compiler.compile() - # |> fn(data) -> - # stuff = inspect(data, limit: :infinity) - # stuff = Code.format_string!(stuff) - # File.write!("ast.exs", stuff) - # data - # end.() - |> Macro.to_string() - |> Code.format_string!() - |> (fn data -> - File.write!(out_filename, data) - end).() + @options [ + strict: [ + out: :string, + ast: :string, + format: :boolean, + help: :boolean + ], + aliases: [ + o: :out, + a: :ast, + f: :format + ] + ] + + def run(args) do + {switches, files, _} = OptionParser.parse(args, @options) + output_filename = Keyword.get(switches, :out, "stdout") + ast_filename = Keyword.get(switches, :ast, false) + format = Keyword.get(switches, :format, true) + + if Keyword.get(switches, :help, false) do + Mix.Task.run("help", ["farmbot.celery_script.compile"]) + System.halt(0) + end + + asts = + Enum.map(files, fn filename -> + filename + |> File.read!() + |> Jason.decode!() + |> case do + %{} = data -> [AST.decode(data)] + data when is_list(data) -> Enum.map(data, &AST.decode/1) + end + |> Enum.map(&AST.compile/1) + end) + + case ast_filename do + false -> + asts + + "stdout" -> + IO.inspect(asts, limit: :infinity) + + filename -> + File.write!(filename, inspect(asts, limit: :infinity)) + asts + end + + code = + asts + |> Macro.to_string() + |> case do + code when format == true -> + Code.format_string!(code) |> to_string() + + code -> + code + end + + case output_filename do + "stdout" -> + IO.puts(code) + + filename -> + File.write!(filename, code) + end end end