Implement new AST: `assert`.
This is a new AST that will allow executing a simple expression and conditionally pass/fail and cleanup when it completes.pull/974/head
parent
a04ddf0159
commit
7e1ceaf7be
|
@ -56,3 +56,4 @@ nerves-hub
|
|||
*.pem
|
||||
*.db
|
||||
*.db-journal
|
||||
*.lua
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
# CeleryScript IF `expression` field.
|
||||
|
||||
The CeleryScript `if` block takes a possible left hand side value of
|
||||
`expression` which allows an arbitrary string to be evaluated. This
|
||||
expression is evaluated against a lua 5.2 interpreter.
|
||||
|
||||
## Lua API
|
||||
The following functions are available for usage along with [Lua's
|
||||
standard library](https://www.lua.org/manual/5.2/).
|
||||
|
||||
```lua
|
||||
-- Comments are ignored by the interpreter
|
||||
|
||||
-- help(function_name)
|
||||
-- Returns docs for a function
|
||||
|
||||
print(help("send_message"));
|
||||
print(help("get_position"));
|
||||
|
||||
-- get_position()
|
||||
-- Returns a table containing the current position data
|
||||
|
||||
position = get_position();
|
||||
if position.x <= 20.55 then
|
||||
return true;
|
||||
else
|
||||
print("current position: (", position.x, ",", position.y, "," position.z, ")");
|
||||
return false;
|
||||
end
|
||||
|
||||
-- get_pins()
|
||||
-- Returns a table containing current pin data
|
||||
|
||||
pins = get_pins();
|
||||
if pins[9] == 1.0 then
|
||||
return true;
|
||||
end
|
||||
|
||||
-- send_message(type, message, channels)
|
||||
-- Sends a message to farmbot's logger
|
||||
|
||||
send_message("info", "hello, world", ["toast"])
|
||||
```
|
||||
|
||||
## Expression contract
|
||||
Expressions are expected to be evaluated in a certain way. The evaluation will fail
|
||||
if this contract is not met. An expression should return one of the following values:
|
||||
* `true`
|
||||
* `false`
|
||||
* `("error", "string reason signaling an error happened")`
|
||||
|
||||
### Examples
|
||||
|
||||
Check if the x position is within a range of 5 and 10
|
||||
|
||||
```lua
|
||||
position = get_position();
|
||||
return position.x >= 5 and position.x <= 10;
|
||||
```
|
||||
|
||||
Check is a pin is a toggled, with error checking
|
||||
|
||||
```lua
|
||||
-- All farmbot functions will return a tuple containing an error
|
||||
-- if something bad happens
|
||||
|
||||
position, positionErr = get_position();
|
||||
pins, pinErr = get_pins();
|
||||
if positionErr or pinErr then
|
||||
return "error", positionErr or pinErr;
|
||||
else
|
||||
return pins[9] == 1.0
|
||||
end
|
||||
```
|
|
@ -166,8 +166,35 @@ defmodule FarmbotCeleryScript.Compiler do
|
|||
end
|
||||
end
|
||||
|
||||
# `Assert` is a internal node useful for self testing.
|
||||
compile :assertion, %{lua: expression, op: op, _then: then_ast} do
|
||||
quote location: :keep do
|
||||
case FarmbotCeleryScript.SysCalls.eval_assertion(unquote(compile_ast(expression))) do
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
true ->
|
||||
:ok
|
||||
|
||||
false when unquote(op) == "abort" ->
|
||||
FarmbotCeleryScript.SysCalls.log("Assertion failed (aborting)")
|
||||
{:error, "Assertion failed (aborting)"}
|
||||
|
||||
false when unquote(op) == "recover" ->
|
||||
FarmbotCeleryScript.SysCalls.log("Assertion failed (recovering)")
|
||||
unquote(compile_block(then_ast))
|
||||
|
||||
false when unquote(op) == "abort_recover" ->
|
||||
FarmbotCeleryScript.SysCalls.log("Assertion failed (recovering then aborting)")
|
||||
unquote(compile_block([then_ast, %AST{kind: :abort, args: %{}}]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Compiles an if statement.
|
||||
compile :_if, %{_then: then_ast, _else: else_ast, lhs: lhs, 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.
|
||||
|
@ -188,6 +215,10 @@ defmodule FarmbotCeleryScript.Compiler do
|
|||
quote [location: :keep],
|
||||
do: FarmbotCeleryScript.SysCalls.read_pin(unquote(String.to_integer(pin)), nil)
|
||||
|
||||
"expression" ->
|
||||
quote [location: :keep],
|
||||
do: FarmbotCeleryScript.SysCalls.eval_assertion(rhs)
|
||||
|
||||
# Named pin has two intents here
|
||||
# in this case we want to read the named pin.
|
||||
%AST{kind: :named_pin} = ast ->
|
||||
|
@ -198,8 +229,6 @@ defmodule FarmbotCeleryScript.Compiler do
|
|||
compile_ast(ast)
|
||||
end
|
||||
|
||||
rhs = compile_ast(rhs)
|
||||
|
||||
# Turn the `op` arg into Elixir code
|
||||
if_eval =
|
||||
case op do
|
||||
|
@ -238,6 +267,11 @@ defmodule FarmbotCeleryScript.Compiler do
|
|||
quote location: :keep do
|
||||
unquote(lhs) > unquote(rhs)
|
||||
end
|
||||
|
||||
_ ->
|
||||
quote location: :keep do
|
||||
unquote(lhs)
|
||||
end
|
||||
end
|
||||
|
||||
# Finally, compile the entire if statement.
|
||||
|
@ -336,6 +370,12 @@ defmodule FarmbotCeleryScript.Compiler do
|
|||
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
|
||||
|
|
|
@ -136,7 +136,7 @@ defmodule FarmbotCeleryScript.Scheduler do
|
|||
end
|
||||
|
||||
def handle_info(:checkup, %{next: nil} = state) do
|
||||
Logger.debug("Scheduling next checkup with no next")
|
||||
# Logger.debug("Scheduling next checkup with no next")
|
||||
|
||||
state
|
||||
|> schedule_next_checkup()
|
||||
|
|
|
@ -67,6 +67,20 @@ defmodule FarmbotCeleryScript.SysCalls do
|
|||
@callback log(message :: String.t()) :: any()
|
||||
@callback sequence_init_log(message :: String.t()) :: any()
|
||||
@callback sequence_complete_log(message :: String.t()) :: any()
|
||||
@callback eval_assertion(expression :: String.t()) :: true | false | error()
|
||||
|
||||
def eval_assertion(sys_calls \\ @sys_calls, expression) when is_binary(expression) do
|
||||
case sys_calls.eval_assertion(expression) do
|
||||
true ->
|
||||
true
|
||||
|
||||
false ->
|
||||
false
|
||||
|
||||
{:error, reason} when is_binary(reason) ->
|
||||
or_error(sys_calls, :eval_assertion, [expression], reason)
|
||||
end
|
||||
end
|
||||
|
||||
def log(sys_calls \\ @sys_calls, message) when is_binary(message) do
|
||||
apply(sys_calls, :log, [message])
|
||||
|
|
|
@ -129,6 +129,9 @@ defmodule FarmbotCeleryScript.SysCalls.Stubs do
|
|||
@impl true
|
||||
def zero(axis), do: error(:zero, [axis])
|
||||
|
||||
@impl true
|
||||
def eval_assertion(expression), do: error(:eval_assertion, [expression])
|
||||
|
||||
defp error(fun, _args) do
|
||||
msg = """
|
||||
CeleryScript syscall stubbed: #{fun}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
defmodule FarmbotOS.Lua do
|
||||
@type t() :: tuple()
|
||||
@type table() :: [{any, any}]
|
||||
alias FarmbotOS.Lua.CeleryScript
|
||||
|
||||
@doc """
|
||||
Evaluates some Lua code. The code should
|
||||
return a boolean value.
|
||||
"""
|
||||
def eval_assertion(str) when is_binary(str) do
|
||||
init()
|
||||
|> set_table([:get_position], &CeleryScript.get_position/2)
|
||||
|> set_table([:get_pins], &CeleryScript.get_pins/2)
|
||||
|> set_table([:send_message], &CeleryScript.send_message/2)
|
||||
|> set_table([:help], &CeleryScript.help/2)
|
||||
|> set_table([:version], &CeleryScript.version/2)
|
||||
|> eval(str)
|
||||
|> case do
|
||||
{:ok, [true | _]} ->
|
||||
true
|
||||
|
||||
{:ok, [false | _]} ->
|
||||
false
|
||||
|
||||
{:ok, [_, reason]} when is_binary(reason) ->
|
||||
{:error, reason}
|
||||
|
||||
{:ok, _data} ->
|
||||
{:error, "bad return value from expression evaluation"}
|
||||
|
||||
{:error, {:lua_error, _error, _lua}} ->
|
||||
{:error, "lua runtime error evaluating expression"}
|
||||
|
||||
{:error, {:badmatch, {:error, [{line, :luerl_parse, parse_error}], _}}} ->
|
||||
{:error, "failed to parse expression (line:#{line}): #{IO.iodata_to_binary(parse_error)}"}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@spec init() :: t()
|
||||
def init do
|
||||
:luerl.init()
|
||||
end
|
||||
|
||||
@spec set_table(t(), Path.t(), any()) :: t()
|
||||
def set_table(lua, path, value) do
|
||||
:luerl.set_table(path, value, lua)
|
||||
end
|
||||
|
||||
@spec eval(t(), String.t()) :: {:ok, any()} | {:error, any()}
|
||||
def eval(lua, hook) when is_binary(hook) do
|
||||
:luerl.eval(hook, lua)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,147 @@
|
|||
defmodule FarmbotOS.Lua.CeleryScript do
|
||||
alias FarmbotCeleryScript.SysCalls
|
||||
|
||||
@doc """
|
||||
Returns a table containing position data
|
||||
|
||||
## Example
|
||||
|
||||
print("x", farmbot.get_position().x);
|
||||
print("y", farmbot.get_position()["y"]);
|
||||
position = farmbot.get_position();
|
||||
print("z", position.z);
|
||||
"""
|
||||
def get_position(["x"], lua) do
|
||||
case SysCalls.get_current_x() do
|
||||
x when is_number(x) ->
|
||||
{[x, nil], lua}
|
||||
|
||||
{:error, reason} ->
|
||||
{[nil, reason], lua}
|
||||
end
|
||||
end
|
||||
|
||||
def get_position(["y"], lua) do
|
||||
case SysCalls.get_current_y() do
|
||||
y when is_number(y) ->
|
||||
{[y, nil], lua}
|
||||
|
||||
{:error, reason} ->
|
||||
{[nil, reason], lua}
|
||||
end
|
||||
end
|
||||
|
||||
def get_position(["z"], lua) do
|
||||
case SysCalls.get_current_z() do
|
||||
z when is_number(z) ->
|
||||
{[z, nil], lua}
|
||||
|
||||
{:error, reason} ->
|
||||
{[nil, reason], lua}
|
||||
end
|
||||
end
|
||||
|
||||
def get_position(_args, lua) do
|
||||
with x when is_number(x) <- SysCalls.get_current_x(),
|
||||
y when is_number(y) <- SysCalls.get_current_y(),
|
||||
z when is_number(z) <- SysCalls.get_current_z() do
|
||||
{[[{"x", x}, {"y", y}, {"z", z}], nil], lua}
|
||||
else
|
||||
{:error, reason} ->
|
||||
{[nil, reason], lua}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a table with pins data
|
||||
|
||||
## Example
|
||||
|
||||
print("pin9", farmbot.get_pin()["9"]);
|
||||
"""
|
||||
def get_pins(_args, lua) do
|
||||
case do_get_pins(Enum.to_list(0..69)) do
|
||||
{:ok, contents} ->
|
||||
{[contents, nil], lua}
|
||||
|
||||
{:error, reason} ->
|
||||
{[nil, reason], lua}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
# Example Usage
|
||||
|
||||
## With channels
|
||||
|
||||
farmbot.send_message("info", "hello, world", ["email", "toast"])
|
||||
|
||||
## No channels
|
||||
|
||||
farmbot.send_message("info", "hello, world")
|
||||
|
||||
"""
|
||||
def send_message([kind, message], lua) do
|
||||
do_send_message(kind, message, [], lua)
|
||||
end
|
||||
|
||||
def send_message([kind, message | channels], lua) do
|
||||
channels = Enum.map(channels, &String.to_atom/1)
|
||||
do_send_message(kind, message, channels, lua)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns help docs about a function.
|
||||
"""
|
||||
def help([function_name], lua) do
|
||||
function_name = String.to_atom(function_name)
|
||||
|
||||
case Code.fetch_docs(__MODULE__) do
|
||||
{:docs_v1, _, _, _, _, _, docs} ->
|
||||
docs =
|
||||
Enum.find_value(docs, fn
|
||||
{{:function, ^function_name, _arity}, _, _, %{"en" => docs}, _} ->
|
||||
IO.iodata_to_binary(docs)
|
||||
|
||||
_other ->
|
||||
false
|
||||
end)
|
||||
|
||||
if docs,
|
||||
do: {[docs, nil], lua},
|
||||
else: {[nil, "docs not found"], lua}
|
||||
|
||||
{:error, reason} ->
|
||||
{[nil, reason], lua}
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Returns the current version of farmbot."
|
||||
def version(_args, lua) do
|
||||
{[FarmbotCore.Project.version(), nil], lua}
|
||||
end
|
||||
|
||||
defp do_send_message(kind, message, channels, lua) do
|
||||
case SysCalls.send_message(kind, message, channels) do
|
||||
:ok ->
|
||||
{[true, nil], lua}
|
||||
|
||||
{:error, reason} ->
|
||||
{[nil, reason], lua}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_pins(nums, acc \\ [])
|
||||
|
||||
defp do_get_pins([p | rest], acc) do
|
||||
case FarmbotFirmware.request({:pin_read, [p: p]}) do
|
||||
{:ok, {_, {:report_pin_value, [p: ^p, v: v]}}} ->
|
||||
do_get_pins(rest, [{to_string(p), v} | acc])
|
||||
|
||||
er ->
|
||||
er
|
||||
end
|
||||
end
|
||||
|
||||
defp do_get_pins([], acc), do: {:ok, Enum.reverse(acc)}
|
||||
end
|
|
@ -23,6 +23,8 @@ defmodule FarmbotOS.SysCalls do
|
|||
SetPinIOMode
|
||||
}
|
||||
|
||||
alias FarmbotOS.Lua
|
||||
|
||||
alias FarmbotCore.{Asset, Asset.Repo, Asset.Private, Asset.Sync, BotState, Leds}
|
||||
alias FarmbotExt.{API, API.Reconciler, API.SyncGroup}
|
||||
|
||||
|
@ -58,6 +60,9 @@ defmodule FarmbotOS.SysCalls do
|
|||
@impl true
|
||||
defdelegate set_pin_io_mode(pin, mode), to: SetPinIOMode
|
||||
|
||||
@impl true
|
||||
defdelegate eval_assertion(expression), to: Lua
|
||||
|
||||
@impl true
|
||||
def log(message) do
|
||||
if FarmbotCore.Asset.fbos_config(:sequence_body_log) do
|
||||
|
|
|
@ -82,6 +82,7 @@ defmodule FarmbotOS.MixProject do
|
|||
{:nerves_hub_cli, "~> 0.7", runtime: false},
|
||||
{:shoehorn, "~> 0.6"},
|
||||
{:ring_logger, "~> 0.8"},
|
||||
{:luerl, github: "rvirding/luerl"},
|
||||
|
||||
# Host/test only dependencies.
|
||||
{:excoveralls, "~> 0.10", only: [:test], targets: [:host]},
|
||||
|
|
|
@ -33,6 +33,8 @@
|
|||
"jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"},
|
||||
"jsx": {:hex, :jsx, "2.9.0", "d2f6e5f069c00266cad52fb15d87c428579ea4d7d73a33669e12679e203329dd", [:mix, :rebar3], [], "hexpm"},
|
||||
"lager": {:hex, :lager, "3.6.5", "831910109f3fcb503debf658ca0538836b348c58bfbf349a6d48228096ce9040", [:rebar3], [{:goldrush, "0.1.9", [hex: :goldrush, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"luer": {:git, "https://github.com/rvirding/luerl.git", "ce4e1b5a66a2a37efe2f8cd16e365ad9845b5015", []},
|
||||
"luerl": {:git, "https://github.com/rvirding/luerl.git", "ce4e1b5a66a2a37efe2f8cd16e365ad9845b5015", []},
|
||||
"makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"mdns_lite": {:hex, :mdns_lite, "0.1.0", "efad834847576ab7641d1016754ec6f512d765788ae3718e1ac6a648c85eab12", [:mix], [{:dns, "~> 2.1", [hex: :dns, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
|
|
|
@ -241,6 +241,11 @@ defmodule Farmbot.TestSupport.CeleryScript.TestSysCalls do
|
|||
call({:zero, [axis]})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def eval_assertion(expression) do
|
||||
call({:eval_assertion, [expression]})
|
||||
end
|
||||
|
||||
defp call(data) do
|
||||
{handler, kind, args} = GenServer.call(__MODULE__, data, :infinity)
|
||||
handler.(kind, args)
|
||||
|
|
Loading…
Reference in New Issue