Skip to content

Commit

Permalink
Implement new AST: assert.
Browse files Browse the repository at this point in the history
This is a new AST that will allow executing a
simple expression and conditionally pass/fail
and cleanup when it completes.
  • Loading branch information
Connor Rigby authored and ConnorRigby committed Aug 21, 2019
1 parent 7faebcb commit 52f0cce
Show file tree
Hide file tree
Showing 12 changed files with 351 additions and 3 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -56,3 +56,4 @@ nerves-hub
*.pem
*.db
*.db-journal
*.lua
74 changes: 74 additions & 0 deletions docs/celery_script/if_expressions.md
@@ -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
```
44 changes: 42 additions & 2 deletions farmbot_celery_script/lib/farmbot_celery_script/compiler.ex
Expand Up @@ -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.
Expand All @@ -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 ->
Expand All @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -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()
Expand Down
14 changes: 14 additions & 0 deletions farmbot_celery_script/lib/farmbot_celery_script/sys_calls.ex
Expand Up @@ -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])
Expand Down
Expand Up @@ -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}
Expand Down
56 changes: 56 additions & 0 deletions farmbot_os/lib/farmbot_os/lua.ex
@@ -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 comments on commit 52f0cce

Please sign in to comment.