feat: Implement (a common subset) G-code parsing.

It turns out that G-code is a turing complete programming language. That was a surprise! I've implemented enough of the parser to be able to parse the output of Fusion 360 and Cura, both of which I use on a daily basis.  It is not a complete implementation.
This commit is contained in:
James Harton 2021-01-06 20:59:13 +13:00
parent fa7eb9e961
commit ad8f6c44ef
95 changed files with 9832 additions and 273 deletions

View file

@ -2,7 +2,18 @@ defmodule Gcode do
alias Gcode.{Model.Program, Model.Serialise, Result}
@moduledoc """
Documentation for `Gcode`.
Gcode - a library for parsing and serialising G-code.
If you haven't heard of G-code before, then you probably don't need this
library, but if you're working with CNC machines or 3D printers then G-code is
the defacto standard for working with these machines. As such it behoves us
to have first class support for working with G-code in Elixir.
You're welcome.
For functions related to parsing G-code files and commands, see the `Parser`
module. For generating your own programs see the contents of `Model`, and for
converting programs back into G-code see the `Model.Serialise` protocol.
"""
@doc """

View file

@ -2,16 +2,21 @@ defmodule Gcode.Model.Block do
use Gcode.Option
use Gcode.Result
defstruct words: [], comment: none()
alias Gcode.Model.{Block, Comment, Skip, Word}
import Gcode.Model.Expr.Helpers
alias Gcode.Model.{Block, Comment, Expr, Skip, Word}
defguardp is_pushable(value)
when is_struct(value, Block) or is_struct(value, Comment) or is_struct(value, Skip) or
is_struct(value, Word) or is_expression(value)
@moduledoc """
A sequence of G-code words.
A sequence of G-code words on a single line.
"""
@type t :: %Block{words: [block_contents], comment: Option.t(Comment)}
@typedoc "Any error results in this module will return this type"
@type block_error :: {:block_error, String.t()}
@type block_contents :: Word.t() | Skip.t()
@type block_contents :: Word.t() | Skip.t() | Expr.t()
@doc """
Initialise a new empty G-code program.
@ -37,20 +42,10 @@ defmodule Gcode.Model.Block do
...> {:ok, block} = Block.comment(block, comment)
...> Result.ok?(block.comment)
true
iex> {:ok, comment} = Comment.init("Jen, in the swing seat, with her night terrors")
...> {:ok, block} = Block.init()
...> {:ok, block} = Block.comment(block, comment)
...> Block.comment(block, comment)
{:error, {:block_error, "Block already contains a comment"}}
"""
@spec comment(t, Comment.t()) :: Result.t(t, block_error)
def comment(%Block{comment: none()} = block, %Comment{} = comment),
do: {:ok, %Block{block | comment: some(comment)}}
def comment(%Block{comment: some(_)}, _comment),
do: {:error, {:block_error, "Block already contains a comment"}}
def comment(%Block{} = block, comment),
do: ok(%Block{block | comment: some(comment)})
@doc """
Pushes a `Word` onto the word list.
@ -66,19 +61,26 @@ defmodule Gcode.Model.Block do
...> {:ok, block} = Block.push(block, word)
...> {:ok, word} = Word.init("N", 100)
...> Block.push(block, word)
{:ok, %Block{words: [%Word{word: "N", address: 100}, %Word{word: "G", address: 0}]}}
{:ok, %Block{words: [%Word{word: "N", address: %Integer{i: 100}}, %Word{word: "G", address: %Integer{i: 0}}]}}
"""
@spec push(t, block_contents) :: Result.t(t, block_error)
def push(%Block{words: words} = block, word)
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words),
do: {:ok, %Block{block | words: [word | words]}}
def push(%Block{words: words} = block, pushable)
when is_pushable(pushable) and is_list(words),
do: ok(%Block{block | words: [pushable | words]})
def push(%Block{words: words}, word)
when (is_struct(word, Word) or is_struct(word, Skip)) and is_list(words),
do:
{:error,
{:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}}
def push(%Block{words: words}, pushable) when is_pushable(pushable),
do:
error(
{:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}
)
def push(%Block{words: words}, pushable) when is_list(words),
do:
error(
{:block_error,
"Expected element to be pushable, but it is not. Received #{inspect(pushable)}"}
)
@doc """
An accessor which returns the block's words in the correct order.
@ -89,14 +91,15 @@ defmodule Gcode.Model.Block do
...> {:ok, word} = Word.init("N", 100)
...> {:ok, block} = Block.push(block, word)
...> Block.words(block)
{:ok, [%Word{word: "G", address: 0}, %Word{word: "N", address: 100}]}
{:ok, [%Word{word: "G", address: %Integer{i: 0}}, %Word{word: "N", address: %Integer{i: 100}}]}
"""
@spec words(t) :: Result.t([Word.t()], block_error)
def words(%Block{words: words}) when is_list(words), do: {:ok, Enum.reverse(words)}
def words(%Block{words: words}) when is_list(words), do: ok(Enum.reverse(words))
def words(%Block{words: words}),
do:
{:error,
{:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}}
error(
{:block_error,
"Expected block to contain a list of words, but it contains #{inspect(words)}"}
)
end

View file

@ -3,6 +3,12 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Block do
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Describe` protocol for `Block`, meaning that we can convert
blocks into human-readable strings.
"""
@doc false
@spec describe(Block.t(), options :: []) :: Option.t(String.t())
def describe(%Block{words: words, comment: some(comment)}, options) do
words = describe_words(words, options)

View file

@ -3,6 +3,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Block do
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Serialise` protocol for `Block`, meaning that blocks can be
turned into G-code output.
"""
@spec serialise(Block.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Block{words: words, comment: some(comment)}) do
words

View file

@ -2,6 +2,11 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Comment do
alias Gcode.Model.Comment
use Gcode.Option
@moduledoc """
Implements the `Describe` protocol for `Comment`.
"""
@doc "Stubbornly refuse to describe comments"
@spec describe(Comment.t(), options :: []) :: Option.t(String.t())
def describe(_comment, _opts \\ []), do: none()
end

View file

@ -3,6 +3,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Comment do
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Serialise` protocol for `Comment`, allowing it to be turned
into G-code output.
"""
@spec serialise(Comment.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Comment{comment: comment}) when is_binary(comment) do
comment

15
lib/gcode/model/expr.ex Normal file
View file

@ -0,0 +1,15 @@
defprotocol Gcode.Model.Expr do
use Gcode.Result
alias Gcode.Model.Expr
@moduledoc """
A protocol for evaluating expressions.
"""
@type scalar :: number | boolean | String.t()
@type expr :: scalar | [scalar]
@type result :: Result.t(expr)
@spec evaluate(Expr.t()) :: result
def evaluate(_expr)
end

View file

@ -0,0 +1,36 @@
defmodule Gcode.Model.Expr.Binary do
use Gcode.Option
defstruct op: none(), lhs: none(), rhs: none()
alias Gcode.Model.{Expr, Expr.Binary}
import Gcode.Model.Expr.Helpers
use Gcode.Result
@moduledoc """
Represents a binary (or infix) expression in G-code, consisting of two
operands (`lhs` and `rhs`) and an operator to apply.
"""
@operators ~w[* / + - == != < <= > >= && || ^]a
@typedoc "Valid infix operators"
@type operator :: :* | :/ | :+ | :- | :== | :!= | :< | :<= | :> | :>= | :&& | :|| | :^
@type t :: %Binary{op: Option.t(operator), lhs: Option.t(Expr.t()), rhs: Option.t(Expr.t())}
@doc """
Wrap an operator and two expressions in a binary expression.
"""
@spec init(operator, Expr.t(), Expr.t()) :: Result.t(t)
def init(operator, lhs, rhs)
when operator in @operators and is_expression(lhs) and is_expression(rhs),
do: ok(%Binary{op: some(operator), lhs: some(lhs), rhs: some(rhs)})
def init(operator, lhs, rhs),
do:
error(
{:expression_error,
"Expected an operator and two expressions, but received #{
inspect(operator: operator, lhs: lhs, rhs: rhs)
}"}
)
end

View file

@ -0,0 +1,177 @@
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Binary do
alias Gcode.Model.{Expr, Expr.Binary}
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Expr` protocol for `Binary`, which will try and perform the
infix calculation to the best of it's ability.
"""
@spec evaluate(Binary.t()) :: Expr.result()
def evaluate(%Binary{op: some(op), lhs: some(lhs), rhs: some(rhs)}) do
with ok(lhs) <- Expr.evaluate(lhs),
ok(rhs) <- Expr.evaluate(rhs),
do: do_evaluate(op, lhs, rhs)
end
def evaluate(_binary), do: error({:program_error, "Unable to evaluate binary expression"})
defp do_evaluate(:*, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs * rhs)
defp do_evaluate(:*, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs * rhs)
defp do_evaluate(:*, lhs, rhs),
do:
error(
{:program_error,
"Both sides of a multiplication expression must be the same type, and either integers or floats. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:/, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs / rhs)
defp do_evaluate(:/, lhs, rhs),
do:
error(
{:program_error,
"Both sides of a division expression must be the floats. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:+, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs + rhs)
defp do_evaluate(:+, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs + rhs)
defp do_evaluate(:+, lhs, rhs),
do:
error(
{:program_error,
"Both sides of an addition expression must be the same type, and either integers or floats. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:-, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs - rhs)
defp do_evaluate(:-, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs - rhs)
defp do_evaluate(:-, lhs, rhs),
do:
error(
{:program_error,
"Both sides of a subtraction expression must be the same type, and either integers or floats. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:==, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs == rhs)
defp do_evaluate(:==, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs == rhs)
defp do_evaluate(:==, lhs, rhs) when is_binary(lhs) and is_binary(rhs), do: ok(lhs == rhs)
defp do_evaluate(:==, lhs, rhs),
do:
error(
{:program_error,
"Both sides of an equality expression must be the same type, and either integers, floats or strings. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:!=, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs != rhs)
defp do_evaluate(:!=, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs != rhs)
defp do_evaluate(:!=, lhs, rhs) when is_binary(lhs) and is_binary(rhs), do: ok(lhs != rhs)
defp do_evaluate(:!=, lhs, rhs),
do:
error(
{:program_error,
"Both sides of an inequality expression must be the same type, and either integers, floats or strings. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:<, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs < rhs)
defp do_evaluate(:<, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs < rhs)
defp do_evaluate(:<, lhs, rhs),
do:
error(
{:program_error,
"Both sides of an LT expression must be the same type, and either integers or floats. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:<=, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs <= rhs)
defp do_evaluate(:<=, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs <= rhs)
defp do_evaluate(:<=, lhs, rhs),
do:
error(
{:program_error,
"Both sides of an LTE expression must be the same type, and either integers or floats. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:>, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs > rhs)
defp do_evaluate(:>, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs > rhs)
defp do_evaluate(:>, lhs, rhs),
do:
error(
{:program_error,
"Both sides of an GT expression must be the same type, and either integers or floats. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:>=, lhs, rhs) when is_integer(lhs) and is_integer(rhs), do: ok(lhs >= rhs)
defp do_evaluate(:>=, lhs, rhs) when is_float(lhs) and is_float(rhs), do: ok(lhs >= rhs)
defp do_evaluate(:>=, lhs, rhs),
do:
error(
{:program_error,
"Both sides of an GTE expression must be the same type, and either integers or floats. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:&&, lhs, rhs) when is_boolean(lhs) and is_boolean(rhs), do: ok(lhs && rhs)
defp do_evaluate(:&&, lhs, rhs),
do:
error(
{:program_error,
"Both sides of a logical and expression must be booleans. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:||, lhs, rhs) when is_boolean(lhs) and is_boolean(rhs), do: ok(lhs || rhs)
defp do_evaluate(:||, lhs, rhs),
do:
error(
{:program_error,
"Both sides of a logical or expression must be booleans. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(:^, lhs, rhs) when is_binary(lhs) and is_binary(rhs), do: ok(lhs <> rhs)
defp do_evaluate(:^, lhs, rhs),
do:
error(
{:program_error,
"Both sides of a string concatenation must be strings. Received #{
inspect(lhs: lhs, rhs: rhs)
}"}
)
defp do_evaluate(op, lhs, rhs),
do:
error({:program_error, "Invalid infix expression. #{inspect(op: op, lhs: lhs, rhs: rhs)}"})
end

View file

@ -0,0 +1,18 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Binary do
alias Gcode.Model.{Expr.Binary, Serialise}
use Gcode.Option
use Gcode.Result
@moduledoc """
Implement the `Serialise` protocol for `Binary`, allowing them to be convered
into G-code output.
"""
@spec serialise(Binary.t()) :: Serialise.result()
def serialise(%Binary{op: some(op), lhs: some(lhs), rhs: some(rhs)}) do
with ok(lhs) <- Serialise.serialise(lhs),
ok(rhs) <- Serialise.serialise(rhs) do
ok([lhs, to_string(op), rhs])
end
end
end

View file

@ -0,0 +1,21 @@
defmodule Gcode.Model.Expr.Boolean do
defstruct b: false
alias Gcode.Model.Expr.Boolean
use Gcode.Result
@moduledoc """
Represents a boolean expression in G-code. Can be either `true` or `false`.
"""
@type t :: %Boolean{b: boolean}
@doc """
Initialise a `Boolean` from a boolean value.
"""
@spec init(boolean) :: Result.t(t)
def init(value) when is_boolean(value),
do: ok(%Boolean{b: value})
def init(value),
do: error({:expression_error, "Expected a boolean value, instead received #{inspect(value)}"})
end

View file

@ -0,0 +1,13 @@
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Boolean do
alias Gcode.Model.Expr
alias Gcode.Model.Expr.Boolean
use Gcode.Result
@moduledoc """
Implements the `Expr` protocol for `Boolean`, which will return either true or
false.
"""
@spec evaluate(Boolean.t()) :: Expr.result()
def evaluate(%Boolean{b: b}), do: ok(b)
end

View file

@ -0,0 +1,14 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Boolean do
alias Gcode.Model.Expr.Boolean
use Gcode.Result
@moduledoc """
Implement the `Serialise` protocol for `Boolean`, allowing them to be convered
into G-code output.
"""
@spec serialise(Boolean.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Boolean{b: true}), do: ok(["true"])
def serialise(%Boolean{b: false}), do: ok(["false"])
def serialise(_), do: error({:serialise_error, "Invalid boolean"})
end

View file

@ -0,0 +1,31 @@
defmodule Gcode.Model.Expr.Constant do
use Gcode.Option
use Gcode.Result
defstruct name: none()
alias Gcode.Model.Expr.Constant
@moduledoc """
Represents a number of special constant values defined by some G-code
controllers:
* `iterations` - the number of completed iterations of the innermost loop.
* `line` - the current line number in the file being executed.
* `null` - the null object.
* `pi` - the constant π.
* `result` - 0 if the last G-, M- or T-command on this input channel was
successful, 1 if it returned a warning, 2 if it returned an error.
"""
@type constant :: :iterations | :line | :null | :pi | :result
@type t :: %Constant{name: Option.t(constant)}
@doc """
Initialise a `Constant`.
"""
def init(name) when name in ~w[iterations line null pi result]a,
do: ok(%Constant{name: name})
def init(name),
do:
error({:expression_error, "Expected a valid constant name, but received #{inspect(name)}"})
end

View file

@ -0,0 +1,25 @@
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Constant do
alias Gcode.Model.Expr
alias Gcode.Model.Expr.Constant
use Gcode.Result
@moduledoc """
Implements the `Expr` protocol for `Constant`, which will return either true
or false.
Currently only knows how to evaluate the following constants:
* `pi` evaluates to the result of `:math.pi()`
* `null` evaulates to `nil`
Other constants cannot be evaluated at this time, because they need to
understand the machine state.
"""
@spec evaluate(Constant.t()) :: Expr.result()
def evaluate(%Constant{name: :pi}), do: ok(:math.pi())
def evaluate(%Constant{name: :null}), do: ok(nil)
def evaluate(%Constant{name: name}),
do: error({:program_error, "Unable to evaluate constant `#{name}`"})
end

View file

@ -0,0 +1,12 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Constant do
alias Gcode.Model.Expr.Constant
use Gcode.Result
@moduledoc """
Implement the `Serialise` protocol for `Constant`, allowing them to be
convered into G-code output.
"""
@spec serialise(Constant.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Constant{name: name}), do: ok([to_string(name)])
end

View file

@ -0,0 +1,21 @@
defmodule Gcode.Model.Expr.Float do
defstruct f: 0.0
alias Gcode.Model.Expr.Float
use Gcode.Result
@moduledoc """
Represents a floating-point number expression in G-code.
"""
@type t :: %Float{f: float}
@doc """
Initialise a `Float` from a floating-point value.
"""
@spec init(float) :: Result.t(t)
def init(value) when is_float(value),
do: ok(%Float{f: value})
def init(value),
do: error({:expression_error, "Expected a float value, instead received #{inspect(value)}"})
end

View file

@ -0,0 +1,13 @@
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Float do
alias Gcode.Model.Expr
alias Gcode.Model.Expr.Float
use Gcode.Result
@moduledoc """
Implements the `Expr` protocol for `Float`, which will return evaluate to a
float.
"""
@spec evaluate(Float.t()) :: Expr.result()
def evaluate(%Float{f: f}), do: ok(f)
end

View file

@ -0,0 +1,13 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Float do
alias Gcode.Model.Expr.Float
use Gcode.Result
@moduledoc """
Implement the `Serialise` protocol for `Float`, allowing them to be
convered into G-code output.
"""
@spec serialise(Float.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Float{f: value}) when is_float(value), do: ok([Elixir.Float.to_string(value)])
def serialise(_), do: error({:serialise_error, "Invalid float"})
end

View file

@ -0,0 +1,17 @@
defmodule Gcode.Model.Expr.Helpers do
alias Gcode.Model.Expr
@moduledoc """
Helpers for working with expressions.
"""
@doc """
A guard which ensures that `value` is an expression struct.
"""
@spec is_expression(any) :: Macro.t()
defguard is_expression(value)
when is_struct(value, Expr.Binary) or is_struct(value, Expr.Boolean) or
is_struct(value, Expr.Constant) or is_struct(value, Expr.Float) or
is_struct(value, Expr.Integer) or is_struct(value, Expr.List) or
is_struct(value, Expr.String) or is_struct(value, Expr.Unary)
end

View file

@ -0,0 +1,18 @@
defmodule Gcode.Model.Expr.Integer do
defstruct i: 0
alias Gcode.Model.Expr.Integer
use Gcode.Result
@moduledoc """
Represents an integer number expression in G-code.
"""
@type t :: %Integer{i: integer}
@spec init(integer) :: Result.t(t)
def init(value) when is_integer(value),
do: ok(%Integer{i: value})
def init(value),
do: error({:expression_error, "Expected an integer value, but received #{inspect(value)}"})
end

View file

@ -0,0 +1,13 @@
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Integer do
alias Gcode.Model.Expr
alias Gcode.Model.Expr.Integer
use Gcode.Result
@moduledoc """
Implements the `Expr` protocol for `Integer`, which will return evaluate to a
integer.
"""
@spec evaluate(Integer.t()) :: Expr.result()
def evaluate(%Integer{i: i}), do: ok(i)
end

View file

@ -0,0 +1,15 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Integer do
alias Gcode.Model.Expr.Integer
use Gcode.Result
@moduledoc """
Implement the `Serialise` protocol for `Integer`, allowing them to be
convered into G-code output.
"""
@spec serialise(Integer.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Integer{i: value}) when is_integer(value),
do: ok([Elixir.Integer.to_string(value)])
def serialise(_), do: error({:serialise_error, "Invalid integer"})
end

View file

@ -0,0 +1,28 @@
defmodule Gcode.Model.Expr.List do
defstruct elements: []
alias Gcode.Model.{Expr, Expr.List}
use Gcode.Result
import Gcode.Model.Expr.Helpers
@moduledoc """
Represents an array expression in G-code.
"""
@type t :: %List{elements: [Expr.t()]}
@doc """
Initialise a `List` from a boolean value.
"""
@spec init :: Result.t(t)
def init, do: ok(%List{})
@doc """
Push an expressions onto the list.
"""
@spec push(t, Expr.t()) :: Result.t(t)
def push(%List{elements: elements}, expr) when is_expression(expr),
do: ok(%List{elements: [expr | elements]})
def push(%List{}, expr),
do: error({:expression_error, "Expected expression, but received #{inspect(expr)}"})
end

View file

@ -0,0 +1,18 @@
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.List do
alias Gcode.Model.{Expr, Expr.List}
use Gcode.Result
@moduledoc """
Implements the `Expr` protocol for `List`, which will return evaluate to a
list.
"""
@spec evaluate(List.t()) :: Expr.result()
def evaluate(%List{elements: elements}) do
elements =
elements
|> Enum.map(&Expr.evaluate/1)
ok(elements)
end
end

View file

@ -0,0 +1,12 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.List do
alias Gcode.Model.{Expr.List, Serialise}
use Gcode.Result
@moduledoc """
Implement the `Serialise` protocol for `List`. Unfortunately it's impossible
to serialise a list into G-code.
"""
@spec serialise(List.t()) :: Serialise.result()
def serialise(%List{}), do: error({:serialise_error, "Cannot serialise a list"})
end

View file

@ -0,0 +1,40 @@
defmodule Gcode.Model.Expr.String do
alias Gcode.Model.Expr.String
use Gcode.Option
use Gcode.Result
defstruct value: Option.none()
@moduledoc """
Represents a string expression in G-code.
"""
@type t :: %String{
value: Option.t(String.t())
}
@doc """
Initialise a comment.
## Example
iex> "Doc, in the carpark, with plutonium"
...> |> String.init()
{:ok, %String{value: "Doc, in the carpark, with plutonium"}}
"""
@spec init(Elixir.String.t()) :: Result.t(t)
def init(comment) when is_binary(comment) do
if Elixir.String.printable?(comment) do
ok(%String{value: comment})
else
error(
{:string_error, "String should be a valid UTF-8 string, received #{inspect(comment)}"}
)
end
end
def init(comment),
do:
error(
{:string_error, "String should be a valid UTF-8 string, received #{inspect(comment)}"}
)
end

View file

@ -0,0 +1,13 @@
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.String do
alias Gcode.Model.Expr
alias Gcode.Model.Expr.String
use Gcode.Result
@moduledoc """
Implements the `Expr` protocol for `String`, which will return evaluate to an
Erlang binary.
"""
@spec evaluate(String.t()) :: Expr.result()
def evaluate(%String{value: value}), do: ok(value)
end

View file

@ -0,0 +1,12 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.String do
alias Gcode.Model.Expr.String
use Gcode.Result
@moduledoc """
Implement the `Serialise` protocol for `String`, allowing them to be converted
into G-code output.
"""
@spec serialise(String.t()) :: Result.t([Elixir.String.t()], {:serialise_error, any})
def serialise(%String{value: value}), do: ok([inspect(value)])
end

View file

@ -0,0 +1,41 @@
defmodule Gcode.Model.Expr.Unary do
use Gcode.Option
defstruct op: none(), expr: none()
alias Gcode.Model.{Expr, Expr.Unary}
import Gcode.Model.Expr.Helpers
use Gcode.Result
@moduledoc """
Represents a unary (or prefix) expression in G-code. A unary consists of a
single operand and an operator.
"""
@operators ~w[! + - #]a
@typedoc "Valid unary operators"
@type operator :: :! | :+ | :- | :"#"
@type t :: %Unary{op: Option.t(atom), expr: Option.t(Expr.t())}
@doc """
Wrap an inner expression and operator in a unary expression.
"""
@spec init(operator, Expr.t()) :: Result.t(t)
def init(operator, expr) when operator in @operators and is_expression(expr),
do: ok(%Unary{op: some(operator), expr: some(expr)})
def init(operator, expr) when operator in @operators,
do: error({:expression_error, "Expected expression, but received #{inspect(expr)}"})
def init(operator, expr) when is_expression(expr),
do: error({:expression_error, "Expected unary operator, but received #{inspect(operator)}"})
def init(operator, expr),
do:
error(
{:expression_error,
"Expected unary operator and expression, but received #{
inspect(operator: operator, expression: expr)
}"}
)
end

View file

@ -0,0 +1,78 @@
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.Unary do
alias Gcode.Model.{Expr, Expr.Unary}
use Gcode.Option
use Gcode.Result
@moduledoc """
Implements the `Expr` protocol for `Unary`, which will attempt to apply the
operator to the operand.
"""
@spec evaluate(Unary.t()) :: Expr.result()
def evaluate(%Unary{op: some(:!), expr: some(inner)}) do
case Expr.evaluate(inner) do
ok(result) when is_boolean(result) ->
ok(!result)
ok(other) ->
error(
{:program_error,
"Expected expression to evaulate to boolean, but received #{inspect(other)}"}
)
error(result) ->
error(result)
end
end
def evaluate(%Unary{op: some(:-), expr: some(inner)}) do
case Expr.evaluate(inner) do
ok(result) when is_number(result) ->
ok(0 - result)
ok(other) ->
error(
{:program_error,
"Expected expression to evaluate to a number, but received #{inspect(other)}"}
)
error(reason) ->
error(reason)
end
end
def evaluate(%Unary{op: some(:+), expr: some(inner)}) do
case Expr.evaluate(inner) do
ok(result) when is_number(result) ->
ok(0 + result)
ok(other) ->
error(
{:program_error,
"Expected expression to evaluate to a number, but received #{inspect(other)}"}
)
error(reason) ->
error(reason)
end
end
def evaluate(%Unary{op: some(:"#"), expr: some(inner)}) do
case Expr.evaluate(inner) do
ok(result) when is_list(result) ->
ok(length(result))
ok(result) when is_binary(result) ->
ok(String.length(result))
ok(other) ->
error(
{:program_error,
"Expected expression to evaluate to an array or string, but received #{inspect(other)}"}
)
error(reason) ->
error(reason)
end
end
end

View file

@ -0,0 +1,21 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.Unary do
alias Gcode.Model.{Expr.Unary, Serialise}
use Gcode.Option
use Gcode.Result
@moduledoc """
Implement the `Serialise` protocol for `Unary`, allowing them to be converted
into G-code output.
"""
@spec serialise(Unary.t()) :: Serialise.result()
def serialise(%Unary{op: some(op), expr: some(inner)}) do
case Serialise.serialise(inner) do
ok(inner) -> ok([to_string(op) | inner])
error(reason) -> error(reason)
end
end
def serialise(unary),
do: error({:serialise_error, "Invalid unary: #{inspect(unary)}"})
end

View file

@ -1,6 +1,6 @@
defmodule Gcode.Model.Program do
defstruct elements: []
alias Gcode.Model.{Block, Comment, Program, Skip, Tape}
alias Gcode.Model.{Block, Comment, Program, Tape}
use Gcode.Result
@moduledoc """
@ -20,7 +20,7 @@ defmodule Gcode.Model.Program do
elements: [element]
}
@type element :: Block.t() | Comment.t() | Skip.t() | Tape.t()
@type element :: Block.t() | Comment.t() | Tape.t()
@type error :: {:program_error, String.t()}
@doc """
@ -43,8 +43,7 @@ defmodule Gcode.Model.Program do
@spec push(t, element) :: Result.t(t, error)
def push(%Program{elements: elements} = program, element)
when is_list(elements) and
(is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Skip) or
is_struct(element, Tape)),
(is_struct(element, Block) or is_struct(element, Comment) or is_struct(element, Tape)),
do: ok(%Program{program | elements: [element | elements]})
def push(%Program{elements: elements}, _element) when not is_list(elements),

View file

@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Program do
alias Gcode.Model.{Describe, Program}
use Gcode.Option
@moduledoc """
Implements the `Describe` protocol for `Program`.
"""
@spec describe(Program.t(), options :: []) :: Option.t(String.t())
def describe(%Program{elements: elements}, options) do
lines =

View file

@ -1,6 +1,9 @@
defimpl Enumerable, for: Gcode.Model.Program do
alias Gcode.Model.Program
@moduledoc false
@moduledoc """
Implements the `Enumerable` protocol for `Program`.
"""
@spec count(Program.t()) :: {:ok, non_neg_integer()} | {:error, module}
def count(%Program{elements: elements}), do: {:ok, Enum.count(elements)}

View file

@ -2,6 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Program do
alias Gcode.{Model.Program, Model.Serialise}
use Gcode.Result
@moduledoc """
Implements the `Serialise` protocol for `Program`, allowing it to be turned
into G-code output.
"""
@spec serialise(Program.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Program{elements: elements}) do
with elements <- Enum.reverse(elements),

View file

@ -3,9 +3,11 @@ defprotocol Gcode.Model.Serialise do
alias Gcode.Result
@moduledoc """
A protocol which is used to serialise the model into a string.
A protocol which is used to serialise the model into G-code output.
"""
@type result :: Result.t([String.t()], {:serialise_error, String.t()})
@spec serialise(Serialise.t()) :: Result.t([String.t()])
def serialise(serialisable)
end

View file

@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Skip do
alias Gcode.Model.Skip
use Gcode.Option
@moduledoc """
Implements the `Describe` protocol for `Skip`.
"""
@spec describe(Skip.t(), options :: []) :: Option.t(String.t())
def describe(_skip, _opts \\ []), do: none()
end

View file

@ -2,7 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Skip do
alias Gcode.{Model.Skip, Result}
use Gcode.Option
use Gcode.Result
@moduledoc false
@moduledoc """
Implements the `Serialise` protocol for `Skip`, allowing it to be turned into
G-code output.
"""
@spec serialise(Skip.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Skip{number: none()}), do: ok(["/"])

View file

@ -2,6 +2,10 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Tape do
alias Gcode.Model.Tape
use Gcode.Option
@moduledoc """
Implements the `Describe` protocol for `Tape`.
"""
@spec describe(Tape.t(), options :: []) :: Option.t(String.t())
def describe(_tape, _opts \\ []), do: none()
end

View file

@ -2,7 +2,11 @@ defimpl Gcode.Model.Serialise, for: Gcode.Model.Tape do
alias Gcode.{Model.Tape, Result}
use Gcode.Option
use Gcode.Result
@moduledoc false
@moduledoc """
Implements the `Serialise` protocol for `Tape`, allowing it to be turned into
G-code output.
"""
@spec serialise(Tape.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Tape{leader: none()}), do: ok(["%"])

View file

@ -1,5 +1,6 @@
defmodule Gcode.Model.Word do
alias Gcode.Model.Word
alias Gcode.Model.{Expr, Word}
import Gcode.Model.Expr.Helpers
use Gcode.Option
use Gcode.Result
@ -11,7 +12,7 @@ defmodule Gcode.Model.Word do
@type t :: %Word{
word: String.t(),
address: number
address: Expr.t()
}
@doc """
@ -20,10 +21,10 @@ defmodule Gcode.Model.Word do
## Example
iex> Word.init("G", 0)
{:ok, %Word{word: "G", address: 0}}
{:ok, %Word{word: "G", address: %Integer{i: 0}}}
"""
@spec init(String.t(), number) :: Result.t(t)
def init(word, address) when is_binary(word) and is_number(address) do
@spec init(String.t(), number | Expr.t()) :: Result.t(t)
def init(word, address) when is_binary(word) and is_expression(address) do
if Regex.match?(~r/^[A-Z]$/, word) do
ok(%Word{word: word, address: address})
else
@ -31,9 +32,31 @@ defmodule Gcode.Model.Word do
end
end
def init(word, address) when is_number(address),
def init(word, address) when is_binary(word) and is_integer(address) do
if Regex.match?(~r/^[A-Z]$/, word) do
ok(address) = Expr.Integer.init(address)
ok(%Word{word: word, address: address})
else
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
end
end
def init(word, address) when is_binary(word) and is_float(address) do
if Regex.match?(~r/^[A-Z]$/, word) do
ok(address) = Expr.Float.init(address)
ok(%Word{word: word, address: address})
else
error({:word_error, "Expected word to be a single character, received #{inspect(word)}"})
end
end
def init(word, address) when is_expression(address),
do: error({:word_error, "Expected word to be a string, received #{inspect(word)}"})
def init(_word, address),
do: error({:word_error, "Expected address to be a number, received #{inspect(address)}"})
do:
error(
{:word_error,
"Expected address to be an expression or a number, received #{inspect(address)}"}
)
end

View file

@ -1,7 +1,10 @@
defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
import Gcode.Model.Expr.Helpers
use Gcode.Option
use Gcode.Result
alias Gcode.Model.Word
alias Gcode.Model.{Expr, Word}
# credo:disable-for-this-file Credo.Check.Refactor.CyclomaticComplexity
@moduledoc """
Describes common/conventional words.
@ -49,283 +52,560 @@ defimpl Gcode.Model.Describe, for: Gcode.Model.Word do
do: do_describe(word, Enum.into(options, %{}))
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
when axis in ~w[A B C] and angle >= 0,
do: some("Rotate #{axis} axis counterclockwise to #{angle}º")
when axis in ~w[A B C] do
case Expr.evaluate(angle) do
ok(angle) when is_number(angle) and angle >= 0 ->
some("Rotate #{axis} axis counterclockwise to #{angle}º")
ok(angle) when is_number(angle) and angle < 0 ->
some("Rotate #{axis} axis clockwise to #{abs(angle)}º")
error(_) ->
none()
end
end
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
when axis in ~w[A B C] and angle >= 0,
do: some("Rotate #{axis} axis counterclockwise by #{angle}º")
when axis in ~w[A B C] do
case Expr.evaluate(angle) do
ok(angle) when is_number(angle) and angle >= 0 ->
some("Rotate #{axis} axis counterclockwise by #{angle}º")
ok(angle) when is_number(angle) and angle < 0 ->
some("Rotate #{axis} axis clockwise by #{abs(angle)}º")
error(_) ->
none()
end
end
defp do_describe(%Word{word: axis, address: angle}, _)
when axis in ~w[A B C] and angle >= 0,
do: some("Rotate #{axis} axis counterclockwise by/to #{angle}º")
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :absolute})
when axis in ~w[A B C] and angle < 0,
do: some("Rotate #{axis} axis clockwise to #{abs(angle)}º")
when axis in ~w[A B C] do
case Expr.evaluate(angle) do
ok(angle) when is_number(angle) and angle >= 0 ->
some("Rotate #{axis} axis counterclockwise by/to #{angle}º")
ok(angle) when is_number(angle) and angle < 0 ->
some("Rotate #{axis} axis clockwise by/to #{abs(angle)}º")
error(_) ->
none()
end
end
defp do_describe(%Word{word: "D", address: depth}, %{operation: :turning} = options) do
case distance_with_unit(depth, options) do
some(depth) ->
some("Depth of cut #{depth}")
_ ->
none()
end
end
defp do_describe(%Word{word: "D", address: aperture}, %{operation: :plotting}) do
case Expr.evaluate(aperture) do
some(aperture) when is_number(aperture) -> some("Aperture #{aperture}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :left} = options) do
case distance_with_unit(offset, options) do
some(offset) -> some("Left radial offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :right} = options) do
case distance_with_unit(offset, options) do
some(offset) -> some("Right radial offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: offset}, options) do
case distance_with_unit(offset, options) do
some(offset) -> some("Radial offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: axis, address: angle}, %{positioning: :relative})
when axis in ~w[A B C] and angle < 0,
do: some("Rotate #{axis} axis clockwise by #{abs(angle)}º")
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options) do
case feedrate(feedrate, options) do
some(feedrate) -> some("Extruder feedrate #{feedrate}")
_ -> none()
end
end
defp do_describe(%Word{word: axis, address: angle}, _)
when axis in ~w[A B C] and angle < 0,
do: some("Rotate #{axis} axis clockwise by/to #{abs(angle)}º")
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options) do
case feedrate(feedrate, options) do
some(feedrate) -> some("Precision feedrate #{feedrate}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: depth}, %{operation: :turning} = options),
do: some("Depth of cut #{distance_with_unit(depth, options)}")
defp do_describe(%Word{word: "F", address: feedrate}, options) do
case feedrate(feedrate, options) do
some(feedrate) -> some("Feedrate #{feedrate}")
_ -> none()
end
end
defp do_describe(%Word{word: "D", address: aperture}, %{operation: :plotting}),
do: some("Aperture #{aperture}")
defp do_describe(%Word{word: "G", address: address}, options) do
case {Expr.evaluate(address), options} do
{ok(0), _} ->
some("Rapid move")
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :left} = options),
do: some("Left radial offset #{distance_with_unit(offset, options)}")
{ok(1), _} ->
some("Linear move")
defp do_describe(%Word{word: "D", address: offset}, %{compensation: :right} = options),
do: some("Right radial offset #{distance_with_unit(offset, options)}")
{ok(2), _} ->
some("Clockwise circular move")
defp do_describe(%Word{word: "D", address: offset}, options),
do: some("Radial offset #{distance_with_unit(offset, options)}")
{ok(3), _} ->
some("Counterclockwise circular move")
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :printing} = options),
do: some("Extruder feedrate #{feedrate(feedrate, options)}")
{ok(4), _} ->
some("Dwell")
defp do_describe(%Word{word: "E", address: feedrate}, %{operation: :turning} = options),
do: some("Precision feedrate #{feedrate(feedrate, options)}")
{ok(5), _} ->
some("High-precision contour control")
defp do_describe(%Word{word: "F", address: feedrate}, options),
do: some("Feedrate #{feedrate(feedrate, options)}")
{ok(5.1), _} ->
some("AI advanced preview control")
defp do_describe(%Word{word: "G", address: 0}, _), do: some("Rapid move")
defp do_describe(%Word{word: "G", address: 1}, _), do: some("Linear move")
defp do_describe(%Word{word: "G", address: 2}, _), do: some("Clockwise circular move")
defp do_describe(%Word{word: "G", address: 3}, _), do: some("Counterclockwise circular move")
defp do_describe(%Word{word: "G", address: 4}, _), do: some("Dwell")
defp do_describe(%Word{word: "G", address: 5}, _), do: some("High-precision contour control")
defp do_describe(%Word{word: "G", address: 5.1}, _), do: some("AI advanced preview control")
defp do_describe(%Word{word: "G", address: 6.1}, _), do: some("NURBS machining")
defp do_describe(%Word{word: "G", address: 7}, _), do: some("Imaginary axis designation")
defp do_describe(%Word{word: "G", address: 9}, _), do: some("Exact stop check - non-modal")
defp do_describe(%Word{word: "G", address: 10}, _), do: some("Programmable data input")
defp do_describe(%Word{word: "G", address: 11}, _), do: some("Data write cancel")
defp do_describe(%Word{word: "G", address: 17}, _), do: some("XY plane selection")
defp do_describe(%Word{word: "G", address: 18}, _), do: some("ZX plane selection")
defp do_describe(%Word{word: "G", address: 19}, _), do: some("YZ plane selection")
defp do_describe(%Word{word: "G", address: 20}, _), do: some("Unit is inches")
defp do_describe(%Word{word: "G", address: 21}, _), do: some("Unit is mm")
defp do_describe(%Word{word: "G", address: 28}, _), do: some("Return to home position")
{ok(6.1), _} ->
some("NURBS machining")
defp do_describe(%Word{word: "G", address: 30}, _),
do: some("Return to secondary home position")
{ok(7), _} ->
some("Imaginary axis designation")
defp do_describe(%Word{word: "G", address: 31}, _), do: some("Feed until skip function")
defp do_describe(%Word{word: "G", address: 32}, _), do: some("Single-point threading")
defp do_describe(%Word{word: "G", address: 33}, _), do: some("Variable pitch threading")
defp do_describe(%Word{word: "G", address: 40}, _), do: some("Tool radius compensation off")
defp do_describe(%Word{word: "G", address: 41}, _), do: some("Tool radius compensation left")
defp do_describe(%Word{word: "G", address: 42}, _), do: some("Tool radius compensation right")
{ok(9), _} ->
some("Exact stop check - non-modal")
defp do_describe(%Word{word: "G", address: 43}, _),
do: some("Tool height offset compensation negative")
{ok(10), _} ->
some("Programmable data input")
defp do_describe(%Word{word: "G", address: 44}, _),
do: some("Tool height offset compensation positive")
{ok(11), _} ->
some("Data write cancel")
defp do_describe(%Word{word: "G", address: 45}, _), do: some("Axis offset single increase")
defp do_describe(%Word{word: "G", address: 46}, _), do: some("Axis offset single decrease")
defp do_describe(%Word{word: "G", address: 47}, _), do: some("Axis offset double increase")
defp do_describe(%Word{word: "G", address: 48}, _), do: some("Axis offset double decrease")
{ok(17), _} ->
some("XY plane selection")
defp do_describe(%Word{word: "G", address: 49}, _),
do: some("Tool length offset compensation cancel")
{ok(18), _} ->
some("ZX plane selection")
defp do_describe(%Word{word: "G", address: 50}, %{operation: :turning}),
do: some("Position register")
{ok(19), _} ->
some("YZ plane selection")
defp do_describe(%Word{word: "G", address: 50}, _), do: some("Scaling function cancel")
defp do_describe(%Word{word: "G", address: 52}, _), do: some("Local coordinate system")
defp do_describe(%Word{word: "G", address: 53}, _), do: some("Machine coordinate system")
{ok(20), _} ->
some("Unit is inches")
defp do_describe(%Word{word: "G", address: address}, _)
when address in [54, 55, 56, 57, 58, 59, 54.1],
do: some("Work coordinate system")
{ok(21), _} ->
some("Unit is mm")
defp do_describe(%Word{word: "G", address: 61}, _), do: some("Exact stop check - modal")
defp do_describe(%Word{word: "G", address: 62}, _), do: some("Automatic corner override")
defp do_describe(%Word{word: "G", address: 64}, _), do: some("Default cutting mode")
defp do_describe(%Word{word: "G", address: 68}, _), do: some("Rotate coordinate system")
{ok(28), _} ->
some("Return to home position")
defp do_describe(%Word{word: "G", address: 69}, _),
do: some("Turn off coordinate system rotation")
{ok(30), _} ->
some("Return to secondary home position")
defp do_describe(%Word{word: "G", address: 70}, %{operation: :turning}),
do: some("Fixed cycle, multiple repetitive cycle - for finishing")
{ok(31), _} ->
some("Feed until skip function")
defp do_describe(%Word{word: "G", address: 71}, %{operation: :turning}),
do: some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis")
{ok(32), _} ->
some("Single-point threading")
defp do_describe(%Word{word: "G", address: 72}, %{operation: :turning}),
do: some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis")
{ok(33), _} ->
some("Variable pitch threading")
defp do_describe(%Word{word: "G", address: 73}, %{operation: :turning}),
do: some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition")
{ok(40), _} ->
some("Tool radius compensation off")
defp do_describe(%Word{word: "G", address: 73}, _), do: some("Peck drilling cycle")
{ok(41), _} ->
some("Tool radius compensation left")
defp do_describe(%Word{word: "G", address: 74}, %{operation: :turning}),
do: some("Peck drilling cycle")
{ok(42), _} ->
some("Tool radius compensation right")
defp do_describe(%Word{word: "G", address: 74}, _), do: some("Tapping cycle")
{ok(43), _} ->
some("Tool height offset compensation negative")
defp do_describe(%Word{word: "G", address: 75}, %{operation: :turning}),
do: some("Peck grooving cycle")
{ok(44), _} ->
some("Tool height offset compensation positive")
defp do_describe(%Word{word: "G", address: 76}, %{operation: :turning}),
do: some("Threading cycle")
{ok(45), _} ->
some("Axis offset single increase")
defp do_describe(%Word{word: "G", address: 76}, _), do: some("Fine boring cycle")
defp do_describe(%Word{word: "G", address: 80}, _), do: some("Cancel cycle")
defp do_describe(%Word{word: "G", address: 81}, _), do: some("Simple drilling cycle")
defp do_describe(%Word{word: "G", address: 82}, _), do: some("Drilling cycle with dwell")
defp do_describe(%Word{word: "G", address: 83}, _), do: some("Peck drilling cycle")
{ok(46), _} ->
some("Axis offset single decrease")
defp do_describe(%Word{word: "G", address: 84}, _),
do: some("Tapping cycle, righthand thread, M03 spindle direction")
{ok(47), _} ->
some("Axis offset double increase")
defp do_describe(%Word{word: "G", address: 84.2}, _),
do: some("Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder")
{ok(48), _} ->
some("Axis offset double decrease")
defp do_describe(%Word{word: "G", address: 84.3}, _),
do: some("Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder")
{ok(49), _} ->
some("Tool length offset compensation cancel")
defp do_describe(%Word{word: "G", address: 85}, _), do: some("Boring cycle, feed in/feed out")
{ok(50), %{operation: :turning}} ->
some("Position register")
defp do_describe(%Word{word: "G", address: 86}, _),
do: some("Boring cycle, feed in/spindle stop/rapid out")
{ok(50), _} ->
some("Scaling function cancel")
defp do_describe(%Word{word: "G", address: 87}, _), do: some("Boring cycle, backboring")
{ok(52), _} ->
some("Local coordinate system")
defp do_describe(%Word{word: "G", address: 88}, _),
do: some("Boring cycle, feed in/spindle stop/manual operation")
{ok(53), _} ->
some("Machine coordinate system")
defp do_describe(%Word{word: "G", address: 89}, _),
do: some("Boring cycle, feed in/dwell/feed out")
{ok(address), _} when address in [54, 55, 56, 57, 58, 59, 54.1] ->
some("Work coordinate system")
defp do_describe(%Word{word: "G", address: 90}, _), do: some("Absolute positioning")
defp do_describe(%Word{word: "G", address: 91}, _), do: some("Relative positioning")
defp do_describe(%Word{word: "G", address: 92}, _), do: some("Position register")
defp do_describe(%Word{word: "G", address: 94}, _), do: some("Feedrate per minute")
defp do_describe(%Word{word: "G", address: 95}, _), do: some("Feedrate per revolution")
defp do_describe(%Word{word: "G", address: 96}, _), do: some("Constant surface speed")
defp do_describe(%Word{word: "G", address: 97}, _), do: some("Constant spindle speed")
{ok(61), _} ->
some("Exact stop check - modal")
defp do_describe(%Word{word: "G", address: 98}, %{operation: :turning}),
do: some("Feedrate per minute")
{ok(62), _} ->
some("Automatic corner override")
defp do_describe(%Word{word: "G", address: 98}, _),
do: some("Return to initial Z level in canned cycle")
{ok(64), _} ->
some("Default cutting mode")
defp do_describe(%Word{word: "G", address: 99}, %{operation: :turning}),
do: some("Feedrate per revolution")
{ok(68), _} ->
some("Rotate coordinate system")
defp do_describe(%Word{word: "G", address: 99}, _),
do: some("Return to R level in canned cycle")
{ok(69), _} ->
some("Turn off coordinate system rotation")
defp do_describe(%Word{word: "G", address: 100}, _), do: some("Tool length measurement")
{ok(70), %{operation: :turning}} ->
some("Fixed cycle, multiple repetitive cycle - for finishing")
defp do_describe(%Word{word: "H", address: length}, options),
do: some("Tool length offset #{distance_with_unit(length, options)}")
{ok(71), %{operation: :turning}} ->
some("Fixed cycle, multiple repetitive cycle - for roughing with Z axis emphasis")
defp do_describe(%Word{word: "I", address: offset}, options),
do: some("X arc center offset #{distance_with_unit(offset, options)}")
{ok(72), %{operation: :turning}} ->
some("Fixed cycle, multiple repetitive cycle - for roughing with X axis emphasis")
defp do_describe(%Word{word: "J", address: offset}, options),
do: some("Y arc center offset #{distance_with_unit(offset, options)}")
{ok(73), %{operation: :turning}} ->
some("Fixed cycle, multiple repetitive cycle - for roughing with pattern repetition")
defp do_describe(%Word{word: "K", address: offset}, options),
do: some("Z arc center offset #{distance_with_unit(offset, options)}")
{ok(73), _} ->
some("Peck drilling cycle")
defp do_describe(%Word{word: "L", address: count}, _), do: some("Loop count #{count}")
{ok(74), %{operation: :turning}} ->
some("Peck drilling cycle")
defp do_describe(%Word{word: "M", address: 0}, _), do: some("Compulsory stop")
defp do_describe(%Word{word: "M", address: 1}, _), do: some("Optional stop")
defp do_describe(%Word{word: "M", address: 2}, _), do: some("End of program")
defp do_describe(%Word{word: "M", address: 3}, _), do: some("Spindle on clockwise")
defp do_describe(%Word{word: "M", address: 4}, _), do: some("Spindle on counterclockwise")
defp do_describe(%Word{word: "M", address: 5}, _), do: some("Spindle stop")
defp do_describe(%Word{word: "M", address: 6}, _), do: some("Automatic tool change")
defp do_describe(%Word{word: "M", address: 7}, _), do: some("Coolant mist")
defp do_describe(%Word{word: "M", address: 8}, _), do: some("Coolant flood")
defp do_describe(%Word{word: "M", address: 9}, _), do: some("Coolant off")
defp do_describe(%Word{word: "M", address: 10}, _), do: some("Pallet clamp on")
defp do_describe(%Word{word: "M", address: 11}, _), do: some("Pallet clamp off")
{ok(74), _} ->
some("Tapping cycle")
defp do_describe(%Word{word: "M", address: 13}, _),
do: some("Spindle on clockwise and coolant flood")
{ok(75), %{operation: :turning}} ->
some("Peck grooving cycle")
defp do_describe(%Word{word: "M", address: 19}, _), do: some("Spindle orientation")
{ok(76), %{operation: :turning}} ->
some("Threading cycle")
defp do_describe(%Word{word: "M", address: 21}, %{operation: :turning}),
do: some("Tailstock forward")
{ok(76), _} ->
some("Fine boring cycle")
defp do_describe(%Word{word: "M", address: 21}, _), do: some("Mirror X axis")
{ok(80), _} ->
some("Cancel cycle")
defp do_describe(%Word{word: "M", address: 22}, %{operation: :turning}),
do: some("Tailstock backward")
{ok(81), _} ->
some("Simple drilling cycle")
defp do_describe(%Word{word: "M", address: 22}, _), do: some("Mirror Y axis")
{ok(82), _} ->
some("Drilling cycle with dwell")
defp do_describe(%Word{word: "M", address: 23}, %{operation: :turning}),
do: some("Thread gradual pullout on")
{ok(83), _} ->
some("Peck drilling cycle")
defp do_describe(%Word{word: "M", address: 23}, _), do: some("Mirror off")
{ok(84), _} ->
some("Tapping cycle, righthand thread, M03 spindle direction")
defp do_describe(%Word{word: "M", address: 24}, %{operation: :turning}),
do: some("Thread gradual pullout off")
{ok(84.2), _} ->
some("Tapping cycle, righthand thread, M03 spindle direction, rigid toolholder")
defp do_describe(%Word{word: "M", address: 30}, _), do: some("End of program")
{ok(84.3), _} ->
some("Tapping cycle, lefthand thread, M04 spindle direction, rigid toolholder")
defp do_describe(%Word{word: "M", address: gear}, %{operation: :turning})
when is_integer(gear) and gear > 40 and gear < 45,
do: some("Gear select #{gear - 40}")
{ok(85), _} ->
some("Boring cycle, feed in/feed out")
defp do_describe(%Word{word: "M", address: 48}, _), do: some("Feedrate override allowed")
defp do_describe(%Word{word: "M", address: 49}, _), do: some("Feedrate override not allowed")
defp do_describe(%Word{word: "M", address: 52}, _), do: some("Unload tool")
defp do_describe(%Word{word: "M", address: 60}, _), do: some("Automatic pallet change")
defp do_describe(%Word{word: "M", address: 98}, _), do: some("Subprogram call")
defp do_describe(%Word{word: "M", address: 99}, _), do: some("Subprogram end")
defp do_describe(%Word{word: "M", address: 100}, _), do: some("Clean nozzle")
defp do_describe(%Word{word: "N", address: line}, _), do: some("Line #{line}")
defp do_describe(%Word{word: "O", address: name}, _), do: some("Program #{name}")
defp do_describe(%Word{word: "P", address: param}, _), do: some("Parameter #{param}")
{ok(86), _} ->
some("Boring cycle, feed in/spindle stop/rapid out")
defp do_describe(%Word{word: "Q", address: distance}, options),
do: some("Peck increment #{distance_with_unit(distance, options)}")
{ok(87), _} ->
some("Boring cycle, backboring")
defp do_describe(%Word{word: "R", address: distance}, options),
do: some("Radius #{distance_with_unit(distance, options)}")
{ok(88), _} ->
some("Boring cycle, feed in/spindle stop/manual operation")
defp do_describe(%Word{word: "S", address: speed}, _), do: some("Speed #{speed}")
defp do_describe(%Word{word: "T", address: tool}, _), do: some("Tool #{tool}")
{ok(89), _} ->
some("Boring cycle, feed in/dwell/feed out")
defp do_describe(%Word{word: "X", address: position}, options),
do: some("X #{distance_with_unit(position, options)}")
{ok(90), _} ->
some("Absolute positioning")
defp do_describe(%Word{word: "Y", address: position}, options),
do: some("Y #{distance_with_unit(position, options)}")
{ok(91), _} ->
some("Relative positioning")
defp do_describe(%Word{word: "Z", address: position}, options),
do: some("Z #{distance_with_unit(position, options)}")
{ok(92), _} ->
some("Position register")
{ok(94), _} ->
some("Feedrate per minute")
{ok(95), _} ->
some("Feedrate per revolution")
{ok(96), _} ->
some("Constant surface speed")
{ok(97), _} ->
some("Constant spindle speed")
{ok(98), %{operation: :turning}} ->
some("Feedrate per minute")
{ok(98), _} ->
some("Return to initial Z level in canned cycle")
{ok(99), %{operation: :turning}} ->
some("Feedrate per revolution")
{ok(99), _} ->
some("Return to R level in canned cycle")
{ok(100), _} ->
some("Tool length measurement")
_ ->
none()
end
end
defp do_describe(%Word{word: "H", address: length}, options) do
case distance_with_unit(length, options) do
ok(length) -> some("Tool length offset #{length}")
_ -> none()
end
end
defp do_describe(%Word{word: "I", address: offset}, options) do
case distance_with_unit(offset, options) do
ok(offset) -> some("X arc center offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "J", address: offset}, options) do
case distance_with_unit(offset, options) do
ok(offset) -> some("Y arc center offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "K", address: offset}, options) do
case distance_with_unit(offset, options) do
ok(offset) -> some("Z arc center offset #{offset}")
_ -> none()
end
end
defp do_describe(%Word{word: "L", address: count}, _) do
case Expr.evaluate(count) do
ok(count) -> some("Loop count #{count}")
_ -> none()
end
end
defp do_describe(%Word{word: "M", address: address}, options) do
case {Expr.evaluate(address), options} do
{ok(0), _} ->
some("Compulsory stop")
{ok(1), _} ->
some("Optional stop")
{ok(2), _} ->
some("End of program")
{ok(3), _} ->
some("Spindle on clockwise")
{ok(4), _} ->
some("Spindle on counterclockwise")
{ok(5), _} ->
some("Spindle stop")
{ok(6), _} ->
some("Automatic tool change")
{ok(7), _} ->
some("Coolant mist")
{ok(8), _} ->
some("Coolant flood")
{ok(9), _} ->
some("Coolant off")
{ok(10), _} ->
some("Pallet clamp on")
{ok(11), _} ->
some("Pallet clamp off")
{ok(13), _} ->
some("Spindle on clockwise and coolant flood")
{ok(19), _} ->
some("Spindle orientation")
{ok(21), %{operation: :turning}} ->
some("Tailstock forward")
{ok(21), _} ->
some("Mirror X axis")
{ok(22), %{operation: :turning}} ->
some("Tailstock backward")
{ok(22), _} ->
some("Mirror Y axis")
{ok(23), %{operation: :turning}} ->
some("Thread gradual pullout on")
{ok(23), _} ->
some("Mirror off")
{ok(24), %{operation: :turning}} ->
some("Thread gradual pullout off")
{ok(30), _} ->
some("End of program")
{ok(gear), %{operation: :turning}} when gear > 40 and gear < 45 ->
some("Gear select #{gear - 40}")
{ok(48), _} ->
some("Feedrate override allowed")
{ok(49), _} ->
some("Feedrate override not allowed")
{ok(52), _} ->
some("Unload tool")
{ok(60), _} ->
some("Automatic pallet change")
{ok(98), _} ->
some("Subprogram call")
{ok(99), _} ->
some("Subprogram end")
{ok(100), _} ->
some("Clean nozzle")
_ ->
none()
end
end
defp do_describe(%Word{word: "N", address: line}, _) do
case Expr.evaluate(line) do
ok(line) -> some("Line #{line}")
_ -> none()
end
end
defp do_describe(%Word{word: "O", address: name}, _) do
case Expr.evaluate(name) do
ok(name) -> some("Program #{name}")
_ -> none()
end
end
defp do_describe(%Word{word: "P", address: param}, _) do
case Expr.evaluate(param) do
ok(param) -> some("Parameter #{param}")
_ -> none()
end
end
defp do_describe(%Word{word: "Q", address: distance}, options) do
case distance_with_unit(distance, options) do
ok(distance) -> some("Peck increment #{distance}")
_ -> none()
end
end
defp do_describe(%Word{word: "R", address: distance}, options) do
case distance_with_unit(distance, options) do
ok(distance) -> some("Radius #{distance}")
_ -> none()
end
end
defp do_describe(%Word{word: "S", address: speed}, _) do
case Expr.evaluate(speed) do
ok(speed) -> some("Speed #{speed}")
_ -> none()
end
end
defp do_describe(%Word{word: "T", address: tool}, _) do
case Expr.evaluate(tool) do
ok(tool) -> some("Tool #{tool}")
_ -> none()
end
end
defp do_describe(%Word{word: axis, address: distance}, options) when axis in ~w[X Y Z] do
case distance_with_unit(distance, options) do
ok(distance) -> some("#{axis} #{distance}")
_ -> none()
end
end
defp do_describe(%Word{}, _options), do: none()
defp distance_with_unit(distance, %{units: :mm}), do: "#{distance}mm"
defp distance_with_unit(distance, %{units: :inches}), do: "#{distance}\""
defp distance_with_unit(distance, _), do: distance
defp distance_with_unit(distance, %{units: :mm}) when is_number(distance),
do: some("#{distance}mm")
defp feedrate(distance, %{operation: :turning} = options),
do: "#{distance_with_unit(distance, options)}/rev"
defp distance_with_unit(distance, %{units: :inches}) when is_number(distance),
do: some("#{distance}\"")
defp feedrate(distance, options), do: "#{distance_with_unit(distance, options)}/min"
defp distance_with_unit(distance, _) when is_number(distance),
do: some(to_string(distance))
defp distance_with_unit(distance, options) when is_expression(distance) do
case Expr.evaluate(distance) do
ok(distance) when is_number(distance) -> distance_with_unit(distance, options)
_ -> none()
end
end
defp distance_with_unit(_, _), do: none()
defp feedrate(distance, %{operation: :turning} = options) do
case distance_with_unit(distance, options) do
some(distance) -> some("#{distance}/rev")
_ -> none()
end
end
defp feedrate(distance, options) do
case distance_with_unit(distance, options) do
some(distance) -> some("#{distance}/min")
_ -> none()
end
end
end

View file

@ -1,10 +1,17 @@
defimpl Inspect, for: Gcode.Model.Word do
alias Gcode.Model.{Describe, Word}
alias Gcode.Model.{Describe, Expr, Word}
use Gcode.Option
use Gcode.Result
import Inspect.Algebra
@moduledoc false
def inspect(%Word{word: letter, address: address} = word, opts) do
address =
case Expr.evaluate(address) do
ok(address) -> address
error(reason) -> "Address error: #{inspect(reason)}"
end
case Describe.describe(word) do
some(description) ->
concat([

View file

@ -1,15 +1,18 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Word do
alias Gcode.Model.Word
alias Gcode.Model.{Word, Serialise}
use Gcode.Option
use Gcode.Result
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Word{word: word, address: address}) when is_integer(address) do
ok(["#{word}#{address}"])
end
@moduledoc """
Implements the `Serialise` protocol for `Word`, allowing it to be turned into
G-code output.
"""
def serialise(%Word{word: word, address: address}) when is_float(address) do
ok(["#{word}#{address}"])
@spec serialise(Word.t()) :: Result.t([String.t()], {:serialise_error, any})
def serialise(%Word{word: word, address: address})
when is_binary(word) and byte_size(word) == 1 do
with ok(address) <- Serialise.serialise(address),
do: ok(["#{word}#{address}"])
end
def serialise(_word), do: error({:serialise_error, "invalid word"})

View file

@ -3,6 +3,7 @@ defmodule Gcode.Option do
A helper which represents an optional type.
"""
@spec __using__(any) :: Macro.t()
defmacro __using__(_) do
quote do
alias Gcode.Option
@ -16,14 +17,17 @@ defmodule Gcode.Option do
@type some(t) :: {:ok, t}
@type opt_none :: :error
@doc "Is the value a none?"
@spec none?(t(any)) :: boolean
def none?(:error), do: true
def none?({:ok, _}), do: false
@doc "Is the value a some?"
@spec some?(t(any)) :: boolean
def some?(:error), do: false
def some?({:ok, _}), do: true
@doc "Create or match a none"
@spec none :: Macro.t()
defmacro none do
quote do
@ -31,6 +35,7 @@ defmodule Gcode.Option do
end
end
@doc "Create or match a some"
@spec some(any) :: Macro.t()
defmacro some(pattern) do
quote do
@ -38,6 +43,7 @@ defmodule Gcode.Option do
end
end
@doc "Attempt to unwrap an option. Raises an error if the option is a none"
@spec unwrap!(t) :: any | no_return
def unwrap!({:ok, result}), do: result
def unwrap!(:error), do: raise("Attempt to unwrap a none")

211
lib/gcode/parser.ex Normal file
View file

@ -0,0 +1,211 @@
defmodule Gcode.Parser do
use Gcode.Result
alias Gcode.Model.{Block, Comment, Expr, Program, Word}
alias Gcode.Parser.{Engine, Error}
@moduledoc """
A parser for G-code programs.
This parser converts G-code input (in UTF-8 encoding) into representations
with the contents of `Gcode.Model`.
"""
@doc """
Attempt to parse a G-code program from a string.
"""
@spec parse_string(String.t()) :: Result.t(Program.t(), {:parse_error, String.t()})
def parse_string(input) do
with ok(tokens) <- Engine.parse(input),
ok(program) <- hydrate(tokens) do
ok(program)
else
{:error, reason} ->
{:parse_error, reason}
{:error, {message, unexpected, _, {line, _}, col}} ->
{:parse_error,
"Unexpected #{inspect(unexpected)} at line: #{line}:#{col + 1}. #{message}."}
end
end
@doc """
Attempt to parse the G-code program at the given path.
"""
@spec parse_file(Path.t()) :: Result.t(Program.t(), {:parse_error, String.t()})
def parse_file(path) do
with ok(input) <- File.read(path),
ok(tokens) <- Engine.parse(input),
ok(program) <- hydrate(tokens) do
ok(program)
else
{:error, reason} ->
{:parse_error, reason}
{:error, {message, unexpected, _, {line, _}, col}} ->
{:parse_error,
"Unexpected #{inspect(unexpected)} at line: #{line}:#{col + 1}. #{message}."}
end
end
@doc """
Parse and stream the G-code program from a string.
Note that this function doesn't yield `Program` objects, but blocks, comments,
etc.
"""
@spec stream_string!(String.t()) :: Enumerable.t() | no_return
def stream_string!(input) do
input
|> String.split(~r/\r?\n/)
|> Stream.with_index()
|> ParallelStream.map(&trim_line/1)
|> ParallelStream.reject(&(elem(&1, 0) == ""))
|> ParallelStream.map(&parse_line!/1)
end
@doc """
Parse and stream the G-code program at the given location.
Note that this function doesn't yield `Program` objects, but blocks, comments,
etc.
"""
@spec stream_file!(Path.t()) :: Enumerable.t() | no_return
def stream_file!(path) do
path
|> File.stream!()
|> Stream.with_index()
|> ParallelStream.map(&trim_line/1)
|> ParallelStream.reject(&(elem(&1, 0) == ""))
|> ParallelStream.map(&parse_line!/1)
end
defp trim_line({input, line_no}), do: {String.trim(input), line_no}
defp parse_line!({input, line_no}) do
with ok(tokens) <- Engine.parse(input),
ok(%Program{elements: [element]}) <- hydrate(tokens) do
element
else
ok(%Program{elements: elements}) ->
raise Error, "Expected line to result in 1 element, but contained #{length(elements)}"
ok(%Block{}) ->
raise Error, "Expected parser to return a program, but returned a block instead."
error({:block_error, reason}) ->
raise Error, "Block error #{reason}"
error({:comment_error, reason}) ->
raise Error, "Comment error #{reason}"
error({:expression_error, reason}) ->
raise Error, "Expression error #{reason}"
error({:parse_error, reason}) ->
raise Error, "Parse error: #{reason}"
error({:program_error, reason}) ->
raise Error, "Program error: #{reason}"
error({:string_error, reason}) ->
raise Error, "String error: #{reason}"
error({:word_error, reason}) ->
raise Error, "Word error: #{reason}"
error({message, unexpected, _, _, col}) ->
raise Error,
"Unexpected #{inspect(unexpected)} at line: #{line_no + 1}:#{col + 1}. #{message}."
end
end
defp hydrate(tokens) do
with ok(program) <- Program.init(),
do: hydrate(tokens, program)
end
defp hydrate([], result), do: ok(result)
defp hydrate([{:comment, comment} | remaining], %Program{} = program) do
with ok(comment) <- Comment.init(List.to_string(comment)),
ok(program) <- Program.push(program, comment),
do: hydrate(remaining, program)
end
defp hydrate([{:comment, comment} | remaining], %Block{} = block) do
with ok(comment) <- Comment.init(List.to_string(comment)),
ok(block) <- Block.comment(block, comment),
do: hydrate(remaining, block)
end
defp hydrate([{:block, words} | remaining], %Program{} = program) do
with ok(block) <- Block.init(),
ok(block) <- hydrate(words, block),
ok(program) <- Program.push(program, block),
do: hydrate(remaining, program)
end
defp hydrate([{:word, contents} | remaining], %Block{} = block) do
with ok(command) <- Keyword.fetch(contents, :command),
ok(address) <- Keyword.fetch(contents, :address),
ok(address) <- expression(address),
ok(word) <- Word.init(List.to_string(command), address),
ok(block) <- Block.push(block, word),
do: hydrate(remaining, block)
end
defp hydrate([{:string, _} = str | remaining], %Block{} = block) do
with ok(str) <- expression([str]),
ok(block) <- Block.push(block, str),
do: hydrate(remaining, block)
end
defp hydrate([{:newline, _} | remaining], %Program{} = program), do: hydrate(remaining, program)
defp hydrate([{:newline, _}], %Block{} = block), do: ok(block)
defp expression([{:-, inner}]) do
with ok(inner) <- expression(inner),
do: Expr.Unary.init(:-, inner)
end
defp expression([{:+, inner}]) do
with ok(inner) <- expression(inner),
do: Expr.Unary.init(:+, inner)
end
defp expression([{:!, inner}]) do
with ok(inner) <- expression(inner),
do: Expr.Unary.init(:!, inner)
end
defp expression([{:"#", inner}]) do
with ok(inner) <- expression(inner),
do: Expr.Unary.init(:"#", inner)
end
defp expression(integer: value) do
value =
value
|> List.to_string()
|> String.to_integer()
Expr.Integer.init(value)
end
defp expression(float: value) do
value =
value
|> List.to_string()
|> String.to_float()
Expr.Float.init(value)
end
defp expression(string: value) do
value =
value
|> List.to_string()
Expr.String.init(value)
end
end

238
lib/gcode/parser/engine.ex Normal file
View file

@ -0,0 +1,238 @@
defmodule Gcode.Parser.Engine do
import NimbleParsec
use Gcode.Result
@moduledoc """
A parser for G-code programs using Parsec.
"""
defcombinatorp(
:whitespace,
[
string(" "),
string("\t")
]
|> choice()
|> repeat()
|> ignore()
)
defcombinatorp(
:whitespace?,
optional(parsec(:whitespace))
)
defcombinatorp(
:newline,
[string("\r"), string("\n")] |> choice() |> times(min: 1) |> tag(:newline)
)
defcombinatorp(
:eol,
empty()
|> parsec(:whitespace?)
|> choice([parsec(:newline), eos()])
)
defcombinatorp(
:tape,
empty()
|> ignore(string("%"))
|> optional(
parsec(:whitespace?)
|> repeat(utf8_char([]))
)
|> tag(:tape)
)
defcombinatorp(
:braces_comment,
empty()
|> ignore(
string("(")
|> parsec(:whitespace?)
)
|> repeat(
lookahead_not(
choice([
string(")"),
parsec(:whitespace)
|> string(")")
])
)
|> utf8_char([])
)
|> tag(:comment)
|> ignore(
parsec(:whitespace?)
|> string(")")
)
)
defcombinatorp(
:semi_comment,
empty()
|> ignore(
string(";")
|> parsec(:whitespace?)
)
|> repeat(
lookahead_not(parsec(:eol))
|> utf8_char([])
)
|> tag(:comment)
)
defcombinatorp(
:comment,
choice([
parsec(:braces_comment),
parsec(:semi_comment)
])
)
defcombinatorp(
:integer,
times(
utf8_char([?0..?9]),
min: 1
)
|> tag(:integer)
)
defcombinatorp(
:float,
times(utf8_char([?0..?9]), min: 1)
|> utf8_char([?.])
|> times(utf8_char([?0..?9]), min: 1)
|> tag(:float)
)
defcombinatorp(
:number,
choice([
parsec(:float),
parsec(:integer)
])
)
defcombinator(
:constant,
choice([
choice([
string("true"),
string("false")
])
|> tag(:boolean),
string("iterations")
|> tag(:iterations),
string("line")
|> tag(:line),
string("pi")
|> tag(:pi),
string("result")
|> tag(:result)
])
|> tag(:constant)
)
defcombinator(
:prefix,
choice([
ignore(string("!") |> parsec(:whitespace?)) |> tag(parsec(:expression), :!),
ignore(string("+") |> parsec(:whitespace?)) |> tag(parsec(:expression), :+),
ignore(string("-") |> parsec(:whitespace?)) |> tag(parsec(:expression), :-),
ignore(string("#") |> parsec(:whitespace?)) |> tag(parsec(:expression), :"#")
])
)
defcombinator(
:expression,
choice([
parsec(:prefix),
parsec(:number),
parsec(:constant)
])
)
defcombinatorp(
:bare_string,
times(
lookahead_not(
parsec(:whitespace?)
|> choice([
parsec(:newline),
parsec(:comment)
])
)
|> utf8_char([]),
min: 1
)
|> tag(:string)
)
defcombinatorp(
:quoted_string,
ignore(string("\""))
|> repeat(
lookahead_not(
choice([
parsec(:newline),
string("\"")
])
)
|> utf8_char([])
)
|> tag(:string)
|> ignore(optional(string("\"")))
)
defcombinatorp(:string, choice([parsec(:quoted_string), parsec(:bare_string)]))
defcombinatorp(
:word,
empty()
|> utf8_char([?A..?Z])
|> tag(:command)
|> parsec(:whitespace?)
|> tag(
parsec(:expression),
:address
)
|> tag(:word)
)
defcombinatorp(
:block,
empty()
|> times(
choice([
parsec(:word),
parsec(:string)
])
|> parsec(:whitespace?),
min: 1
)
|> optional(parsec(:comment))
|> tag(:block)
)
defcombinatorp(
:line,
choice([
parsec(:tape),
parsec(:comment),
parsec(:block)
])
)
defparsecp(:program, times(parsec(:line) |> parsec(:eol), min: 1))
@spec parse(String.t()) :: Result.t(keyword)
def parse(program) do
case program(program) do
{:ok, tokens, _, _, _, _} -> ok(tokens)
{:error, a, b, c, d, e} -> error({a, b, c, d, e})
end
end
end

View file

@ -0,0 +1,8 @@
defmodule Gcode.Parser.Error do
defexception message: nil
@moduledoc """
Parser's streaming outputs have no way to return a result type, so we are
forced to rely on exceptions. These are those exceptions.
"""
end

View file

@ -1,6 +1,8 @@
defmodule Gcode.Result do
@moduledoc """
A helper which represents a result type.
This is really just a wrapper around Erlang's ok/error tuples.
"""
@type t :: t(any, any)
@ -9,6 +11,7 @@ defmodule Gcode.Result do
@type ok(result) :: {:ok, result}
@type error(error) :: {:error, error}
@spec __using__(any) :: Macro.t()
defmacro __using__(_) do
quote do
alias Gcode.Result
@ -17,6 +20,7 @@ defmodule Gcode.Result do
end
end
@doc "Initialise or match an ok value"
@spec ok(any) :: Macro.t()
defmacro ok(result) do
quote do
@ -24,6 +28,7 @@ defmodule Gcode.Result do
end
end
@doc "Initialise or match an error value"
@spec error(any) :: Macro.t()
defmacro error(error) do
quote do
@ -31,18 +36,22 @@ defmodule Gcode.Result do
end
end
@doc "Is the result ok?"
@spec ok?(t) :: boolean
def ok?({:ok, _}), do: true
def ok?({:error, _}), do: false
@doc "Is the result an error?"
@spec error?(t) :: boolean
def error?({:ok, _}), do: false
def error?({:error, _}), do: true
@doc "Attempt to unwrap a result and return the inner value. Raises an exception if the result contains an error."
@spec unwrap!(t) :: any | no_return
def unwrap!({:ok, result}), do: result
def unwrap!({:error, error}), do: raise(error)
@doc "Convert a successful result another result."
@spec map(t, (any -> t)) :: t
def map({:ok, value}, mapper) when is_function(mapper, 1) do
case mapper.(value) do

View file

@ -1,7 +1,10 @@
defmodule Gcode.Result.Enum do
@moduledoc false
use Gcode.Result
@moduledoc """
Common enumerableish functions on results.
"""
@type result :: Result.t()
@type result(result) :: Result.t(result)
@type result(result, error) :: Result.t(result, error)
@ -13,32 +16,44 @@ defmodule Gcode.Result.Enum do
reducer :: (any, any -> result(any))
) :: result(any)
def reduce_while_ok(elements, acc, reducer) when is_function(reducer, 2) do
Enum.reduce_while(elements, {:ok, acc}, fn element, {:ok, acc} ->
Enum.reduce_while(elements, ok(acc), fn element, ok(acc) ->
case reducer.(element, acc) do
{:ok, acc} -> {:cont, {:ok, acc}}
{:error, reason} -> {:halt, {:error, reason}}
ok(acc) -> {:cont, ok(acc)}
error(reason) -> {:halt, error(reason)}
end
end)
end
@doc """
Maps a collection of results using a mapping function.
Both the input to the map must be an ok result and the result of each mapping
function.
"""
@spec map(result([any]), mapper :: (any -> result(any))) :: result([any])
def map({:ok, enumerable}, mapper) when is_function(mapper, 1) do
def map(ok(enumerable), mapper) when is_function(mapper, 1) do
reduce_while_ok(enumerable, [], fn element, acc ->
case mapper.(element) do
{:ok, mapped} -> {:ok, [mapped | acc]}
{:error, reason} -> {:error, reason}
ok(mapped) -> ok([mapped | acc])
error(reason) -> error(reason)
end
end)
|> reverse()
end
def map({:error, reason}, _mapper), do: {:error, reason}
def map(error(reason), _mapper), do: error(reason)
@doc """
Reverse the enumerable contents of an ok result.
"""
@spec reverse(result([any])) :: result([any])
def reverse({:ok, enumerable}), do: {:ok, Enum.reverse(enumerable)}
def reverse({:error, reason}), do: {:error, reason}
def reverse(ok(enumerable)), do: ok(Enum.reverse(enumerable))
def reverse(error(reason)), do: error(reason)
@doc """
Join the string contents of an ok result.
"""
@spec join(result([String.t()]), String.t()) :: result(String.t())
def join({:ok, strings}, joiner) when is_binary(joiner), do: {:ok, Enum.join(strings, joiner)}
def join({:error, reason}, _joiner), do: {:error, reason}
def join(ok(strings), joiner) when is_binary(joiner), do: ok(Enum.join(strings, joiner))
def join(error(reason), _joiner), do: error(reason)
end

View file

@ -44,7 +44,9 @@ defmodule Gcode.MixProject do
{:ex_doc, ">= 0.0.0", only: ~w[dev test]a, runtime: false},
{:earmark, ">= 0.0.0", only: ~w[dev test]a, runtime: false},
{:credo, "~> 1.1", only: ~w[dev test]a, runtime: false},
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false}
{:git_ops, "~> 2.3", only: ~w[dev test]a, runtime: false},
{:nimble_parsec, "~> 1.1"},
{:parallel_stream, "~> 1.0"}
]
end

View file

@ -11,4 +11,5 @@
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm", "639b2e8749e11b87b9eb42f2ad325d161c170b39b288ac8d04c4f31f8f0823eb"},
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,501 @@
(1001)
(Machine)
( vendor: Gennmitsu)
( model: 3018 Pro)
( description: Gennmitsu 3018 Pro)
(T1 D=3.175 CR=0 - ZMIN=-6 - flat end mill)
G90 G94
G17
G21
(When using Fusion 360 for Personal Use, the feedrate of )
(rapid moves is reduced to match the feedrate of cutting )
(moves, which can increase machining time. Unrestricted )
(rapid moves are available with a Fusion 360 Subscription. )
G28 G91 Z0
G90
(2D Contour1)
T1
S1000 M3
G54
G0 X160.042 Y80.893
Z5
G1 Z2 F120
Z1.317 F10
G19 G3 Y81.21 Z1 J0.317 K0 F120
G1 Y81.527
G17 G3 X159.724 Y81.845 I-0.318 J0
G1 X21.16 Z0.52
G3 X21.16 Y78.155 Z0.5 I0 J-1.845
G1 X159.724 Z0.02
G3 X159.724 Y81.845 Z0 I0 J1.845
G1 X21.16 Z-0.48
G3 X21.16 Y78.155 Z-0.5 I0 J-1.845
G1 X159.724 Z-0.98
G3 X159.724 Y81.845 Z-1 I0 J1.845
G1 X21.16 Z-1.48
G3 X21.16 Y78.155 Z-1.5 I0 J-1.845
G1 X159.724 Z-1.98
G3 X159.724 Y81.845 Z-2 I0 J1.845
G1 X21.16 Z-2.48
G3 X21.16 Y78.155 Z-2.5 I0 J-1.845
G1 X159.724 Z-2.98
G3 X159.724 Y81.845 Z-3 I0 J1.845
G1 X21.16 Z-3.48
G3 X21.16 Y78.155 Z-3.5 I0 J-1.845
G1 X159.724 Z-3.98
G3 X159.724 Y81.845 Z-4 I0 J1.845
G1 X21.16 Z-4.48
G3 X21.16 Y78.155 Z-4.5 I0 J-1.845
G1 X159.724 Z-4.98
G3 X159.724 Y81.845 Z-5 I0 J1.845
G1 X21.16 Z-5.48
G3 X21.16 Y78.155 Z-5.5 I0 J-1.845
G1 X159.724 Z-5.98
G3 X159.724 Y81.845 Z-6 I0 J1.845
G1 X21.16
G3 X21.16 Y78.155 I0 J-1.845
G1 X159.724
G3 X159.724 Y81.845 I0 J1.845
G2 X159.486 Y82.004 I0 J0.258
G3 X159.248 Y82.162 I-0.238 J-0.099
G1 X21.16 F90
G3 X21.16 Y77.838 I0 J-2.162
G1 X159.724
G3 X159.724 Y82.162 I0 J2.162
G1 X159.248
G3 X158.93 Y81.845 I0 J-0.317 F120
G1 Y81.527
G19 G2 Y81.21 Z-5.683 J0 K0.318
G1 Z3
X151.956 Y94.683
Z2
Z1.317 F10
G18 G2 X152.274 Z1 I0.318 K0 F120
G1 X152.592
G17 G3 X152.909 Y95 I0 J0.317
X151.064 Y96.845 Z0.989 I-1.845 J0
G1 X29.82 Z0.511
G3 X29.82 Y93.155 Z0.489 I0 J-1.845
G1 X151.064 Z0.011
G3 X151.064 Y96.845 Z-0.011 I0 J1.845
G1 X29.82 Z-0.489
G3 X29.82 Y93.155 Z-0.511 I0 J-1.845
G1 X151.064 Z-0.989
G3 X151.064 Y96.845 Z-1.011 I0 J1.845
G1 X29.82 Z-1.489
G3 X29.82 Y93.155 Z-1.511 I0 J-1.845
G1 X151.064 Z-1.989
G3 X151.064 Y96.845 Z-2.011 I0 J1.845
G1 X29.82 Z-2.489
G3 X29.82 Y93.155 Z-2.511 I0 J-1.845
G1 X151.064 Z-2.989
G3 X151.064 Y96.845 Z-3.011 I0 J1.845
G1 X29.82 Z-3.489
G3 X29.82 Y93.155 Z-3.511 I0 J-1.845
G1 X151.064 Z-3.989
G3 X151.064 Y96.845 Z-4.011 I0 J1.845
G1 X29.82 Z-4.489
G3 X29.82 Y93.155 Z-4.511 I0 J-1.845
G1 X151.064 Z-4.989
G3 X151.064 Y96.845 Z-5.011 I0 J1.845
G1 X29.82 Z-5.489
G3 X29.82 Y93.155 Z-5.511 I0 J-1.845
G1 X151.064 Z-5.989
G3 X152.909 Y95 Z-6 I0 J1.845
X151.064 Y96.845 I-1.845 J0
G1 X29.82
G3 X29.82 Y93.155 I0 J-1.845
G1 X151.064
G3 X152.909 Y95 I0 J1.845
G2 X153.055 Y95.229 I0.253 J0
G3 X153.174 Y95.472 I-0.092 J0.196
X151.064 Y97.162 I-2.11 J-0.472 F90
G1 X29.82
G3 X29.82 Y92.838 I0 J-2.162
G1 X151.064
G3 X153.174 Y95.472 I0 J2.162
X152.795 Y95.713 I-0.31 J-0.069 F120
G1 X152.485 Y95.644
X152.416 Y95.628 Z-5.992
X152.351 Y95.613 Z-5.969
X152.292 Y95.6 Z-5.931
X152.243 Y95.589 Z-5.88
X152.206 Y95.581 Z-5.82
X152.183 Y95.576 Z-5.753
X152.175 Y95.574 Z-5.683
Z3
X142.721 Y110.893
Z2
Z1.317 F10
G19 G3 Y111.21 Z1 J0.317 K0 F120
G1 Y111.527
G17 G3 X142.404 Y111.845 I-0.317 J0
G1 X38.481 Z0.526
G3 X38.481 Y108.155 Z0.5 I0 J-1.845
G1 X142.404 Z0.026
G3 X142.404 Y111.845 Z0 I0 J1.845
G1 X38.481 Z-0.474
G3 X38.481 Y108.155 Z-0.5 I0 J-1.845
G1 X142.404 Z-0.974
G3 X142.404 Y111.845 Z-1 I0 J1.845
G1 X38.481 Z-1.474
G3 X38.481 Y108.155 Z-1.5 I0 J-1.845
G1 X142.404 Z-1.974
G3 X142.404 Y111.845 Z-2 I0 J1.845
G1 X38.481 Z-2.474
G3 X38.481 Y108.155 Z-2.5 I0 J-1.845
G1 X142.404 Z-2.974
G3 X142.404 Y111.845 Z-3 I0 J1.845
G1 X38.481 Z-3.474
G3 X38.481 Y108.155 Z-3.5 I0 J-1.845
G1 X142.404 Z-3.974
G3 X142.404 Y111.845 Z-4 I0 J1.845
G1 X38.481 Z-4.474
G3 X38.481 Y108.155 Z-4.5 I0 J-1.845
G1 X142.404 Z-4.974
G3 X142.404 Y111.845 Z-5 I0 J1.845
G1 X38.481 Z-5.474
G3 X38.481 Y108.155 Z-5.5 I0 J-1.845
G1 X142.404 Z-5.974
G3 X142.404 Y111.845 Z-6 I0 J1.845
G1 X38.481
G3 X38.481 Y108.155 I0 J-1.845
G1 X142.404
G3 X142.404 Y111.845 I0 J1.845
G2 X142.166 Y112.004 I0 J0.258
G3 X141.928 Y112.162 I-0.238 J-0.099
G1 X38.481 F90
G3 X38.481 Y107.838 I0 J-2.162
G1 X142.404
G3 X142.404 Y112.162 I0 J2.162
G1 X141.928
G3 X141.61 Y111.845 I0 J-0.317 F120
G1 Y111.527
G19 G2 Y111.21 Z-5.683 J0 K0.318
G1 Z3
X46.824 Y124.107
Z2
Z1.317 F10
G2 Y123.79 Z1 J-0.317 K0 F120
G1 Y123.473
G17 G3 X47.141 Y123.155 I0.317 J0
G1 X133.744 Z0.531
G3 X133.744 Y126.845 Z0.5 I0 J1.845
G1 X47.141 Z0.031
G3 X47.141 Y123.155 Z0 I0 J-1.845
G1 X133.744 Z-0.469
G3 X133.744 Y126.845 Z-0.5 I0 J1.845
G1 X47.141 Z-0.969
G3 X47.141 Y123.155 Z-1 I0 J-1.845
G1 X133.744 Z-1.469
G3 X133.744 Y126.845 Z-1.5 I0 J1.845
G1 X47.141 Z-1.969
G3 X47.141 Y123.155 Z-2 I0 J-1.845
G1 X133.744 Z-2.469
G3 X133.744 Y126.845 Z-2.5 I0 J1.845
G1 X47.141 Z-2.969
G3 X47.141 Y123.155 Z-3 I0 J-1.845
G1 X133.744 Z-3.469
G3 X133.744 Y126.845 Z-3.5 I0 J1.845
G1 X47.141 Z-3.969
G3 X47.141 Y123.155 Z-4 I0 J-1.845
G1 X133.744 Z-4.469
G3 X133.744 Y126.845 Z-4.5 I0 J1.845
G1 X47.141 Z-4.969
G3 X47.141 Y123.155 Z-5 I0 J-1.845
G1 X133.744 Z-5.469
G3 X133.744 Y126.845 Z-5.5 I0 J1.845
G1 X47.141 Z-5.969
G3 X47.141 Y123.155 Z-6 I0 J-1.845
G1 X133.744
G3 X133.744 Y126.845 I0 J1.845
G1 X47.141
G3 X47.141 Y123.155 I0 J-1.845
G2 X47.379 Y122.996 I0 J-0.258
G3 X47.617 Y122.838 I0.238 J0.099
G1 X133.744 F90
G3 X133.744 Y127.162 I0 J2.162
G1 X47.141
G3 X47.141 Y122.838 I0 J-2.162
G1 X47.617
G3 X47.935 Y123.155 I0 J0.317 F120
G1 Y123.473
G19 G3 Y123.79 Z-5.683 J0 K0.318
G1 Z3
X124.766 Y139.107
Z2
Z1.317 F10
G2 Y138.79 Z1 J-0.318 K0 F120
G1 Y138.473
G17 G3 X125.083 Y138.155 I0.317 J0
X125.083 Y141.845 Z0.961 I0 J1.845
G1 X55.801 Z0.5
G3 X55.801 Y138.155 Z0.461 I0 J-1.845
G1 X125.083 Z0
G3 X125.083 Y141.845 Z-0.039 I0 J1.845
G1 X55.801 Z-0.5
G3 X55.801 Y138.155 Z-0.539 I0 J-1.845
G1 X125.083 Z-1
G3 X125.083 Y141.845 Z-1.039 I0 J1.845
G1 X55.801 Z-1.5
G3 X55.801 Y138.155 Z-1.539 I0 J-1.845
G1 X125.083 Z-2
G3 X125.083 Y141.845 Z-2.039 I0 J1.845
G1 X55.801 Z-2.5
G3 X55.801 Y138.155 Z-2.539 I0 J-1.845
G1 X125.083 Z-3
G3 X125.083 Y141.845 Z-3.039 I0 J1.845
G1 X55.801 Z-3.5
G3 X55.801 Y138.155 Z-3.539 I0 J-1.845
G1 X125.083 Z-4
G3 X125.083 Y141.845 Z-4.039 I0 J1.845
G1 X55.801 Z-4.5
G3 X55.801 Y138.155 Z-4.539 I0 J-1.845
G1 X125.083 Z-5
G3 X125.083 Y141.845 Z-5.039 I0 J1.845
G1 X55.801 Z-5.5
G3 X55.801 Y138.155 Z-5.539 I0 J-1.845
G1 X125.083 Z-6
G3 X125.083 Y141.845 I0 J1.845
G1 X55.801
G3 X55.801 Y138.155 I0 J-1.845
G1 X125.083
G2 X125.312 Y138.009 I0 J-0.253
G3 X125.555 Y137.89 I0.196 J0.092
X125.083 Y142.163 I-0.472 J2.11 F90
G1 X55.801
G3 X55.801 Y137.837 I0 J-2.163
G1 X125.083
G3 X125.555 Y137.89 I0 J2.163
X125.796 Y138.269 I-0.069 J0.31 F120
G1 X125.727 Y138.579
X125.711 Y138.648 Z-5.992
X125.696 Y138.713 Z-5.969
X125.683 Y138.772 Z-5.931
X125.672 Y138.821 Z-5.88
X125.664 Y138.858 Z-5.82
X125.659 Y138.881 Z-5.753
X125.657 Y138.889 Z-5.683
Z3
X29.503 Y64.107
Z2
Z1.317 F10
G19 G2 Y63.79 Z1 J-0.317 K0 F120
G1 Y63.472
G17 G3 X29.82 Y63.155 I0.317 J0
G1 X151.064 Z0.523
G3 X151.064 Y66.845 Z0.5 I0 J1.845
G1 X29.82 Z0.023
G3 X29.82 Y63.155 Z0 I0 J-1.845
G1 X151.064 Z-0.477
G3 X151.064 Y66.845 Z-0.5 I0 J1.845
G1 X29.82 Z-0.977
G3 X29.82 Y63.155 Z-1 I0 J-1.845
G1 X151.064 Z-1.477
G3 X151.064 Y66.845 Z-1.5 I0 J1.845
G1 X29.82 Z-1.977
G3 X29.82 Y63.155 Z-2 I0 J-1.845
G1 X151.064 Z-2.477
G3 X151.064 Y66.845 Z-2.5 I0 J1.845
G1 X29.82 Z-2.977
G3 X29.82 Y63.155 Z-3 I0 J-1.845
G1 X151.064 Z-3.477
G3 X151.064 Y66.845 Z-3.5 I0 J1.845
G1 X29.82 Z-3.977
G3 X29.82 Y63.155 Z-4 I0 J-1.845
G1 X151.064 Z-4.477
G3 X151.064 Y66.845 Z-4.5 I0 J1.845
G1 X29.82 Z-4.977
G3 X29.82 Y63.155 Z-5 I0 J-1.845
G1 X151.064 Z-5.477
G3 X151.064 Y66.845 Z-5.5 I0 J1.845
G1 X29.82 Z-5.977
G3 X29.82 Y63.155 Z-6 I0 J-1.845
G1 X151.064
G3 X151.064 Y66.845 I0 J1.845
G1 X29.82
G3 X29.82 Y63.155 I0 J-1.845
G2 X30.059 Y62.996 I0 J-0.258
G3 X30.297 Y62.838 I0.238 J0.099
G1 X151.064 F90
G3 X151.064 Y67.162 I0 J2.162
G1 X29.82
G3 X29.82 Y62.838 I0 J-2.162
G1 X30.297
G3 X30.614 Y63.155 I0 J0.317 F120
G1 Y63.472
G19 G3 Y63.79 Z-5.683 J0 K0.318
G1 Z3
X143.296 Y49.682
Z2
Z1.317 F10
G18 G2 X143.614 Z1 I0.318 K0 F120
G1 X143.932
G17 G3 X144.249 Y50 I0 J0.318
X142.404 Y51.845 Z0.987 I-1.845 J0
G1 X38.481 Z0.513
G3 X38.481 Y48.155 Z0.487 I0 J-1.845
G1 X142.404 Z0.013
G3 X142.404 Y51.845 Z-0.013 I0 J1.845
G1 X38.481 Z-0.487
G3 X38.481 Y48.155 Z-0.513 I0 J-1.845
G1 X142.404 Z-0.987
G3 X142.404 Y51.845 Z-1.013 I0 J1.845
G1 X38.481 Z-1.487
G3 X38.481 Y48.155 Z-1.513 I0 J-1.845
G1 X142.404 Z-1.987
G3 X142.404 Y51.845 Z-2.013 I0 J1.845
G1 X38.481 Z-2.487
G3 X38.481 Y48.155 Z-2.513 I0 J-1.845
G1 X142.404 Z-2.987
G3 X142.404 Y51.845 Z-3.013 I0 J1.845
G1 X38.481 Z-3.487
G3 X38.481 Y48.155 Z-3.513 I0 J-1.845
G1 X142.404 Z-3.987
G3 X142.404 Y51.845 Z-4.013 I0 J1.845
G1 X38.481 Z-4.487
G3 X38.481 Y48.155 Z-4.513 I0 J-1.845
G1 X142.404 Z-4.987
G3 X142.404 Y51.845 Z-5.013 I0 J1.845
G1 X38.481 Z-5.487
G3 X38.481 Y48.155 Z-5.513 I0 J-1.845
G1 X142.404 Z-5.987
G3 X144.249 Y50 Z-6 I0 J1.845
X142.404 Y51.845 I-1.845 J0
G1 X38.481
G3 X38.481 Y48.155 I0 J-1.845
G1 X142.404
G3 X144.249 Y50 I0 J1.845
G2 X144.395 Y50.229 I0.253 J0
G3 X144.514 Y50.472 I-0.092 J0.196
X142.404 Y52.162 I-2.11 J-0.472 F90
G1 X38.481
G3 X38.481 Y47.838 I0 J-2.162
G1 X142.404
G3 X144.514 Y50.472 I0 J2.162
X144.135 Y50.713 I-0.31 J-0.069 F120
G1 X143.825 Y50.644
X143.756 Y50.628 Z-5.992
X143.691 Y50.613 Z-5.969
X143.632 Y50.6 Z-5.931
X143.583 Y50.589 Z-5.88
X143.546 Y50.581 Z-5.82
X143.523 Y50.576 Z-5.753
X143.515 Y50.574 Z-5.683
Z3
X133.426 Y34.107
Z2
Z1.317 F10
G19 G2 Y33.79 Z1 J-0.317 K0 F120
G1 Y33.472
G17 G3 X133.744 Y33.155 I0.318 J0
X133.744 Y36.845 Z0.969 I0 J1.845
G1 X47.141 Z0.5
G3 X47.141 Y33.155 Z0.469 I0 J-1.845
G1 X133.744 Z0
G3 X133.744 Y36.845 Z-0.031 I0 J1.845
G1 X47.141 Z-0.5
G3 X47.141 Y33.155 Z-0.531 I0 J-1.845
G1 X133.744 Z-1
G3 X133.744 Y36.845 Z-1.031 I0 J1.845
G1 X47.141 Z-1.5
G3 X47.141 Y33.155 Z-1.531 I0 J-1.845
G1 X133.744 Z-2
G3 X133.744 Y36.845 Z-2.031 I0 J1.845
G1 X47.141 Z-2.5
G3 X47.141 Y33.155 Z-2.531 I0 J-1.845
G1 X133.744 Z-3
G3 X133.744 Y36.845 Z-3.031 I0 J1.845
G1 X47.141 Z-3.5
G3 X47.141 Y33.155 Z-3.531 I0 J-1.845
G1 X133.744 Z-4
G3 X133.744 Y36.845 Z-4.031 I0 J1.845
G1 X47.141 Z-4.5
G3 X47.141 Y33.155 Z-4.531 I0 J-1.845
G1 X133.744 Z-5
G3 X133.744 Y36.845 Z-5.031 I0 J1.845
G1 X47.141 Z-5.5
G3 X47.141 Y33.155 Z-5.531 I0 J-1.845
G1 X133.744 Z-6
G3 X133.744 Y36.845 I0 J1.845
G1 X47.141
G3 X47.141 Y33.155 I0 J-1.845
G1 X133.744
G2 X133.973 Y33.009 I0 J-0.253
G3 X134.216 Y32.89 I0.196 J0.092
X133.744 Y37.162 I-0.472 J2.11 F90
G1 X47.141
G3 X47.141 Y32.838 I0 J-2.162
G1 X133.744
G3 X134.216 Y32.89 I0 J2.162
X134.457 Y33.269 I-0.069 J0.31 F120
G1 X134.388 Y33.579
X134.372 Y33.648 Z-5.992
X134.357 Y33.713 Z-5.969
X134.344 Y33.772 Z-5.931
X134.333 Y33.821 Z-5.88
X134.325 Y33.858 Z-5.82
X134.32 Y33.881 Z-5.753
X134.318 Y33.889 Z-5.683
Z3
X124.766 Y19.108
Z2
Z1.317 F10
G19 G2 Y18.79 Z1 J-0.317 K0 F120
G1 Y18.472
G17 G3 X125.083 Y18.155 I0.317 J0
X125.083 Y21.845 Z0.961 I0 J1.845
G1 X55.801 Z0.5
G3 X55.801 Y18.155 Z0.461 I0 J-1.845
G1 X125.083 Z0
G3 X125.083 Y21.845 Z-0.039 I0 J1.845
G1 X55.801 Z-0.5
G3 X55.801 Y18.155 Z-0.539 I0 J-1.845
G1 X125.083 Z-1
G3 X125.083 Y21.845 Z-1.039 I0 J1.845
G1 X55.801 Z-1.5
G3 X55.801 Y18.155 Z-1.539 I0 J-1.845
G1 X125.083 Z-2
G3 X125.083 Y21.845 Z-2.039 I0 J1.845
G1 X55.801 Z-2.5
G3 X55.801 Y18.155 Z-2.539 I0 J-1.845
G1 X125.083 Z-3
G3 X125.083 Y21.845 Z-3.039 I0 J1.845
G1 X55.801 Z-3.5
G3 X55.801 Y18.155 Z-3.539 I0 J-1.845
G1 X125.083 Z-4
G3 X125.083 Y21.845 Z-4.039 I0 J1.845
G1 X55.801 Z-4.5
G3 X55.801 Y18.155 Z-4.539 I0 J-1.845
G1 X125.083 Z-5
G3 X125.083 Y21.845 Z-5.039 I0 J1.845
G1 X55.801 Z-5.5
G3 X55.801 Y18.155 Z-5.539 I0 J-1.845
G1 X125.083 Z-6
G3 X125.083 Y21.845 I0 J1.845
G1 X55.801
G3 X55.801 Y18.155 I0 J-1.845
G1 X125.083
G2 X125.312 Y18.009 I0 J-0.253
G3 X125.555 Y17.89 I0.196 J0.092
X125.083 Y22.163 I-0.472 J2.11 F90
G1 X55.801
G3 X55.801 Y17.837 I0 J-2.163
G1 X125.083
G3 X125.555 Y17.89 I0 J2.163
X125.796 Y18.269 I-0.069 J0.31 F120
G1 X125.727 Y18.579
X125.711 Y18.648 Z-5.992
X125.696 Y18.713 Z-5.969
X125.683 Y18.772 Z-5.931
X125.672 Y18.821 Z-5.88
X125.664 Y18.858 Z-5.82
X125.659 Y18.881 Z-5.753
X125.657 Y18.889 Z-5.683
Z5
G28 G91 Z0
G90
G53 G0 X0 Y0
M5
M30

View file

@ -1,6 +1,6 @@
defmodule Gcode.Model.Block.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Block, Serialise, Word}
alias Gcode.Model.{Block, Expr, Serialise, Word}
use Gcode.Result
@moduledoc false
@ -8,9 +8,11 @@ defmodule Gcode.Model.Block.SerialiseTest do
assert ok(block) =
with(
ok(block) <- Block.init(),
ok(word) <- Word.init("G", 0),
ok(address) <- Expr.Integer.init(0),
ok(word) <- Word.init("G", address),
ok(block) <- Block.push(block, word),
ok(word) <- Word.init("N", 100),
ok(address) <- Expr.Integer.init(100),
ok(word) <- Word.init("N", address),
ok(block) <- Block.push(block, word),
do: ok(block)
)

View file

@ -1,6 +1,6 @@
defmodule Gcode.Model.BlockTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Block, Comment, Word}
alias Gcode.Model.{Block, Comment, Expr.Integer, Word}
use Gcode.Option
use Gcode.Result
doctest Gcode.Model.Block

View file

@ -0,0 +1,53 @@
defmodule Gcode.Model.Expr.Binary.ExprTest do
use ExUnit.Case, async: true
use Gcode.Result
import InfixHelper
@moduledoc false
describe "Expr.evaluate/1" do
it_evaluates_to(:*, 2, 3, ok(6))
it_evaluates_to(:*, 2.0, 3.0, ok(6.0))
it_evaluates_to(:*, 2, 3.0, error({:program_error, _}))
it_evaluates_to(:/, 3.0, 2.0, ok(1.5))
it_evaluates_to(:/, 2, 3, error({:program_error, _}))
it_evaluates_to(:+, 2, 3, ok(5))
it_evaluates_to(:+, 2.0, 3.0, ok(5.0))
it_evaluates_to(:-, 2, 3, ok(-1))
it_evaluates_to(:-, 2.0, 3.0, ok(-1.0))
it_evaluates_to(:==, 1, 1, ok(true))
it_evaluates_to(:==, 1, 2, ok(false))
it_evaluates_to(:==, 1.0, 1.0, ok(true))
it_evaluates_to(:==, 1.0, 2.0, ok(false))
it_evaluates_to(:==, "a", "a", ok(true))
it_evaluates_to(:==, "a", "b", ok(false))
it_evaluates_to(:!=, 1, 1, ok(false))
it_evaluates_to(:!=, 1, 2, ok(true))
it_evaluates_to(:!=, 1.0, 1.0, ok(false))
it_evaluates_to(:!=, 1.0, 2.0, ok(true))
it_evaluates_to(:!=, "a", "a", ok(false))
it_evaluates_to(:!=, "a", "b", ok(true))
it_evaluates_to(:<, 1, 2, ok(true))
it_evaluates_to(:<, 1, 1, ok(false))
it_evaluates_to(:<, 1.0, 2.0, ok(true))
it_evaluates_to(:<, 1.0, 1.0, ok(false))
it_evaluates_to(:<=, 1, 2, ok(true))
it_evaluates_to(:<=, 1, 1, ok(true))
it_evaluates_to(:<=, 1.0, 2.0, ok(true))
it_evaluates_to(:<=, 1.0, 1.0, ok(true))
it_evaluates_to(:>, 1, 2, ok(false))
it_evaluates_to(:>, 1, 1, ok(false))
it_evaluates_to(:>, 1.0, 2.0, ok(false))
it_evaluates_to(:>, 1.0, 1.0, ok(false))
it_evaluates_to(:>=, 1, 2, ok(false))
it_evaluates_to(:>=, 1, 1, ok(true))
it_evaluates_to(:>=, 1.0, 2.0, ok(false))
it_evaluates_to(:>=, 1.0, 1.0, ok(true))
it_evaluates_to(:&&, true, true, ok(true))
it_evaluates_to(:&&, true, false, ok(false))
it_evaluates_to(:&&, false, false, ok(false))
it_evaluates_to(:||, true, true, ok(true))
it_evaluates_to(:||, true, false, ok(true))
it_evaluates_to(:||, false, false, ok(false))
it_evaluates_to(:^, "a", "b", ok("ab"))
end
end

View file

@ -0,0 +1,20 @@
defmodule Gcode.Model.Expr.Binary.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr.Binary, Expr.Integer, Serialise}
use Gcode.Result
@moduledoc false
describe "Serialise.serialise/1" do
for op <- ~w[* / + - == != < <= > >= && || ^]a do
quote do
test "when the operator is `#{unquote(op)}` it serialises correctly" do
ok(lhs) = Integer.init(1)
ok(rhs) = Integer.init(2)
ok(unary) = Binary.init(unquote(op), lhs, rhs)
as_s = to_string(unquote(op))
assert ok(["1", as_s, "2"]) = Serialise.serialise(unary)
end
end
end
end
end

View file

@ -0,0 +1,17 @@
defmodule Gcode.Model.Expr.BinaryTest do
use ExUnit.Case, async: true
alias Gcode.Model.Expr.{Binary, Integer}
use Gcode.Option
use Gcode.Result
@moduledoc false
describe "init/3" do
test "when the operator and expressions are valid, it is ok" do
ok(lhs) = Integer.init(1)
ok(rhs) = Integer.init(2)
assert ok(%Binary{op: some(:-), lhs: some(^lhs), rhs: some(^rhs)}) =
Binary.init(:-, lhs, rhs)
end
end
end

View file

@ -0,0 +1,18 @@
defmodule Gcode.Model.Expr.Boolean.ExprTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr, Expr.Boolean}
use Gcode.Result
@moduledoc false
describe "Expr.evaluate/1" do
test "when the value is `true` it evaluates to `true`" do
ok(bool) = Boolean.init(true)
assert ok(true) = Expr.evaluate(bool)
end
test "when the value is `false` it evaluates to `false`" do
ok(bool) = Boolean.init(false)
assert ok(false) = Expr.evaluate(bool)
end
end
end

View file

@ -0,0 +1,18 @@
defmodule Gcode.Model.Expr.Boolean.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr.Boolean, Serialise}
use Gcode.Result
@moduledoc false
describe "Serialise.serialise/1" do
test "when the value is `true` it is serialised to `\"true\"`" do
ok(bool) = Boolean.init(true)
assert ok(["true"]) = Serialise.serialise(bool)
end
test "when the value is `false` it is serialised to `\"false\"`" do
ok(bool) = Boolean.init(false)
assert ok(["false"]) = Serialise.serialise(bool)
end
end
end

View file

@ -0,0 +1,20 @@
defmodule Gcode.Model.Expr.BooleanTest do
use ExUnit.Case, async: true
alias Gcode.Model.Expr.Boolean
use Gcode.Result
@moduledoc false
describe "init/1" do
test "when the argument is `true` it is ok" do
assert ok(%Boolean{}) = Boolean.init(true)
end
test "when the argument is `false` it is ok" do
assert ok(%Boolean{}) = Boolean.init(false)
end
test "when passed any other argument, it fails" do
assert error({:expression_error, _}) = Boolean.init(nil)
end
end
end

View file

@ -0,0 +1,29 @@
defmodule Gcode.Model.Expr.Constant.ExprTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr, Expr.Constant}
use Gcode.Result
@moduledoc false
describe "Expr.evaluate/1" do
test "when the constant is `:pi` it returns Pi" do
ok(const) = Constant.init(:pi)
assert ok(pi) = Expr.evaluate(const)
assert_in_delta :math.pi(), pi, 0.1
end
test "when the constant is `line` it returns an error" do
ok(const) = Constant.init(:line)
assert error(_) = Expr.evaluate(const)
end
test "when the constant is `null` it returns `nil`" do
ok(const) = Constant.init(:null)
assert ok(nil) = Expr.evaluate(const)
end
test "when the constant is `result` it returns an error" do
ok(const) = Constant.init(:result)
assert error(_) = Expr.evaluate(const)
end
end
end

View file

@ -0,0 +1,13 @@
defmodule Gcode.Model.Expr.Constant.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr.Constant, Serialise}
use Gcode.Result
@moduledoc false
describe "Serialise.serialise/1" do
test "it serialises correctly" do
ok(const) = Constant.init(:pi)
assert ok(["pi"]) = Serialise.serialise(const)
end
end
end

View file

@ -0,0 +1,20 @@
defmodule Gcode.Model.Expr.ConstantTest do
use ExUnit.Case, async: true
alias Gcode.Model.Expr.Constant
use Gcode.Result
@moduledoc false
describe "init/1" do
for name <- ~w[iterations line null pi result]a do
quote do
test "when the argument is `#{unquote(name)}`, it is ok" do
assert ok(%Constant{name: unquote(name)}) = Constant.init(unquote(name))
end
end
end
test "otherwise, it is an error" do
assert error(_) = Constant.init(:wat)
end
end
end

View file

@ -0,0 +1,13 @@
defmodule Gcode.Model.Expr.Float.ExprTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr, Expr.Float}
use Gcode.Result
@moduledoc false
describe "Expr.evaluate/1" do
test "when the value is is a float it evaluates to it" do
ok(float) = Float.init(1.23)
assert ok(1.23) = Expr.evaluate(float)
end
end
end

View file

@ -0,0 +1,13 @@
defmodule Gcode.Model.Expr.Float.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr.Float, Serialise}
use Gcode.Result
@moduledoc false
describe "Serialise.serialise/1" do
test "it serialises correctly" do
ok(float) = Float.init(1.23)
assert ok(["1.23"]) = Serialise.serialise(float)
end
end
end

View file

@ -0,0 +1,16 @@
defmodule Gcode.Model.Expr.FloatTest do
use ExUnit.Case, async: true
alias Gcode.Model.Expr.Float
use Gcode.Result
@moduledoc false
describe "init/1" do
test "when the value is a float, it is ok" do
assert ok(%Float{}) = Float.init(1.23)
end
test "when the value is not a float, it is an error" do
assert error({:expression_error, _}) = Float.init(123)
end
end
end

View file

@ -0,0 +1,13 @@
defmodule Gcode.Model.Expr.Integer.ExprTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr, Expr.Integer}
use Gcode.Result
@moduledoc false
describe "Expr.evaluate/1" do
test "when the value is is an integer it evaluates to it" do
ok(integer) = Integer.init(123)
assert ok(123) = Expr.evaluate(integer)
end
end
end

View file

@ -0,0 +1,13 @@
defmodule Gcode.Model.Expr.Integer.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr.Integer, Serialise}
use Gcode.Result
@moduledoc false
describe "Serialise.serialise/1" do
test "it serialises correctly" do
ok(float) = Integer.init(123)
assert ok(["123"]) = Serialise.serialise(float)
end
end
end

View file

@ -0,0 +1,16 @@
defmodule Gcode.Model.Expr.IntegerTest do
use ExUnit.Case, async: true
alias Gcode.Model.Expr.Integer
use Gcode.Result
@moduledoc false
describe "init/1" do
test "when the value is an integer, it is ok" do
assert ok(%Integer{}) = Integer.init(123)
end
test "when the value is not an integer, it is an error" do
assert error({:expression_error, _}) = Integer.init(1.23)
end
end
end

View file

@ -0,0 +1,14 @@
defimpl Gcode.Model.Expr, for: Gcode.Model.Expr.List do
alias Gcode.Model.{Expr, Expr.List}
use Gcode.Result
@moduledoc false
@spec evaluate(List.t()) :: Expr.result()
def evaluate(%List{elements: elements}) do
elements =
elements
|> Enum.map(&Expr.evaluate/1)
ok(elements)
end
end

View file

@ -0,0 +1,13 @@
defimpl Gcode.Model.Serialise, for: Gcode.Model.Expr.List do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr.List, Serialise}
use Gcode.Result
@moduledoc false
describe "Serialise.serialise/1" do
test "it cannot be serialised" do
ok(list) = List.init()
assert error(_) = Serialise.serialise(list)
end
end
end

View file

@ -0,0 +1,25 @@
defmodule Gcode.Model.Expr.ListTest do
use ExUnit.Case, async: true
alias Gcode.Model.Expr.{Integer, List}
use Gcode.Result
@moduledoc false
describe "init/1" do
test "it is ok" do
assert ok(%List{}) = List.init()
end
end
describe "push/2" do
test "when the element is an expression, it is ok" do
ok(list) = List.init()
ok(expr) = Integer.init(123)
assert ok(%List{elements: [^expr]}) = List.push(list, expr)
end
test "otherwise it's an error" do
ok(list) = List.init()
assert error({:expression_error, _}) = List.push(list, :marty)
end
end
end

View file

@ -0,0 +1,63 @@
defmodule Gcode.Model.Expr.Unary.ExprTest do
use ExUnit.Case, async: true
alias Gcode.Model.{
Expr,
Expr.Boolean,
Expr.Float,
Expr.Integer,
Expr.List,
Expr.String,
Expr.Unary
}
use Gcode.Result
@moduledoc false
describe "Expr.evaluate/1" do
test "when the operator is `!` and the inner value evaluates to a boolean, it returns it's inverse" do
ok(inner) = Boolean.init(true)
ok(unary) = Unary.init(:!, inner)
assert ok(false) = Expr.evaluate(unary)
end
test "when the operator is `!` and the inner value evaluates to a non-boolean, it returns an error" do
ok(inner) = Integer.init(123)
ok(unary) = Unary.init(:!, inner)
assert error({:program_error, _}) = Expr.evaluate(unary)
end
test "when the operator is `+` the inner value is an integer, it evaluates it" do
ok(inner) = Integer.init(123)
ok(inner) = Unary.init(:-, inner)
ok(unary) = Unary.init(:+, inner)
assert ok(-123) = Expr.evaluate(unary)
end
test "when the operator is `+` the inner value is an float, it evaluates it" do
ok(inner) = Float.init(1.23)
ok(unary) = Unary.init(:+, inner)
assert ok(1.23) = Expr.evaluate(unary)
end
test "when the operator is `#` the inner value evaluates to a list, it returns it's length" do
ok(list) = List.init()
ok(int) = Integer.init(123)
ok(inner) = List.push(list, int)
ok(unary) = Unary.init(:"#", inner)
assert ok(1) = Expr.evaluate(unary)
end
test "when the operator is `#` the inner value evaluates to a string, it returns it's length" do
ok(inner) = String.init("Marty McFly")
ok(unary) = Unary.init(:"#", inner)
assert ok(11) = Expr.evaluate(unary)
end
test "when the operator is `#`, otherwise it returns an error" do
ok(inner) = Integer.init(123)
ok(unary) = Unary.init(:"#", inner)
assert error({:program_error, _}) = Expr.evaluate(unary)
end
end
end

View file

@ -0,0 +1,19 @@
defmodule Gcode.Model.Expr.Unary.SerialiseTest do
use ExUnit.Case, async: true
alias Gcode.Model.{Expr.Integer, Expr.Unary, Serialise}
use Gcode.Result
@moduledoc false
describe "Serialise.serialise/1" do
for op <- ~w[! + - #]a do
quote do
test "when the operator is `#{unquote(op)}` it serialises correctly" do
ok(inner) = Integer.init(123)
ok(unary) = Unary.init(unquote(op), inner)
as_s = to_string(unquote(op))
assert ok([as_s, "123"]) = Serialise.serialise(unary)
end
end
end
end
end

View file

@ -0,0 +1,27 @@
defmodule Gcode.Model.Expr.UnaryTest do
use ExUnit.Case, async: true
alias Gcode.Model.Expr.{Integer, Unary}
use Gcode.Option
use Gcode.Result
@moduledoc false
describe "init/2" do
test "when the operator is valid and the inner value is an expression, it is ok" do
ok(inner) = Integer.init(123)
assert ok(%Unary{op: some(:-), expr: some(^inner)}) = Unary.init(:-, inner)
end
test "when the operator is valid and the inner value is not an expresion, it is an error" do
assert error({:expression_error, _}) = Unary.init(:-, 1.21)
end
test "when the inner value is an expression but the operator is invalid, it an error" do
ok(inner) = Integer.init(123)
assert error({:expression_error, _}) = Unary.init(:%, inner)
end
test "when both the operator and inner value are invalid, it is an error" do
assert error({:expression_error, _}) = Unary.init(:%, 1.21)
end
end
end

View file

@ -1,6 +1,6 @@
defmodule Gcode.Model.WordTest do
use ExUnit.Case, async: true
alias Gcode.Model.Word
alias Gcode.Model.{Expr.Integer, Word}
doctest Gcode.Model.Word
@moduledoc false
end

View file

@ -0,0 +1,57 @@
defmodule Gcode.Parser.EngineTest do
use ExUnit.Case, async: true
use Gcode.Result
use ParserEngineHelper
import FixtureHelper
@moduledoc false
it_parses_into("%", tape: '')
it_parses_into("% hello", tape: 'hello')
it_parses_into("()", comment: '')
it_parses_into("(hello)", comment: 'hello')
it_parses_into("( hello )", comment: 'hello')
it_parses_into("; hello", comment: 'hello')
it_parses_into("G0", block: [word: [command: 'G', address: [integer: '0']]])
it_parses_into("G 0", block: [word: [command: 'G', address: [integer: '0']]])
it_parses_into("G54.1", block: [word: [command: 'G', address: [float: '54.1']]])
it_parses_into("G 54.1", block: [word: [command: 'G', address: [float: '54.1']]])
it_parses_into("G-1", block: [word: [command: 'G', address: [{:-, [integer: '1']}]]])
it_parses_into("G+1", block: [word: [command: 'G', address: [{:+, [integer: '1']}]]])
it_parses_into("G!1", block: [word: [command: 'G', address: [{:!, [integer: '1']}]]])
it_parses_into("G1 X112.518 Y131.525 E59.51636 (hello)",
block: [
word: [command: 'G', address: [integer: '1']],
word: [command: 'X', address: [float: '112.518']],
word: [command: 'Y', address: [float: '131.525']],
word: [command: 'E', address: [float: '59.51636']],
comment: 'hello'
]
)
it_parses_into("M82 ;absolute extrusion mode",
block: [word: [command: 'M', address: [integer: '82']], comment: 'absolute extrusion mode']
)
it_parses_into("M117 Hello world",
block: [word: [command: 'M', address: [integer: '117']], string: 'Hello world']
)
it_parses_into(read_fixture("fusion_360_milling_grbl.nc"), fn tokens ->
lines =
tokens
|> Enum.reject(&(elem(&1, 0) == :newline))
|> Enum.count()
assert lines == 500
end)
it_parses_into(read_fixture("cura_marlin.gcode"), fn tokens ->
lines =
tokens
|> Enum.reject(&(elem(&1, 0) == :newline))
|> Enum.count()
assert lines == 6723
end)
end

133
test/gcode/parser_test.exs Normal file
View file

@ -0,0 +1,133 @@
defmodule Gcode.ParserTest do
use ExUnit.Case, async: true
alias Gcode.Parser
alias Gcode.Model.{Block, Comment, Expr, Program, Word}
use Gcode.Option
use Gcode.Result
import FixtureHelper
@moduledoc false
describe "parse_string/1" do
test "( hello )" do
assert ok(program) = Parser.parse_string("( hello )")
assert %Program{elements: [%Comment{comment: "hello"}]} = program
end
test "G29" do
assert ok(program) = Parser.parse_string("G29")
assert %Program{
elements: [%Block{words: [%Word{word: "G", address: %Expr.Integer{i: 29}}]}]
} = program
end
test "G54.1" do
assert ok(program) = Parser.parse_string("G54.1")
assert %Program{
elements: [%Block{words: [%Word{word: "G", address: %Expr.Float{f: 54.1}}]}]
} = program
end
test "X-12" do
assert ok(program) = Parser.parse_string("X-12")
assert %Program{
elements: [
%Block{
words: [
%Word{
word: "X",
address: %Expr.Unary{
op: some(:-),
expr: some(%Expr.Integer{i: 12})
}
}
]
}
]
} = program
end
test "M117 Marty McFly" do
assert ok(program) = Parser.parse_string("M117 Marty McFly")
assert %Program{
elements: [
%Block{
words: [
%Expr.String{value: "Marty McFly"},
%Word{word: "M", address: %Expr.Integer{i: 117}}
]
}
]
} = program
end
test "it can parse `fusion_360_milling_grbl.nc`" do
input = read_fixture("fusion_360_milling_grbl.nc")
assert ok(%Program{}) = Parser.parse_string(input)
end
test "it can parse `cura_marlin.gcode`" do
input = read_fixture("cura_marlin.gcode")
assert ok(%Program{}) = Parser.parse_string(input)
end
end
describe "parse_file/1" do
test "it can parse `fusion_360_milling_grbl.nc`" do
assert ok(%Program{elements: elements}) =
Parser.parse_file(fixture_path("fusion_360_milling_grbl.nc"))
assert 500 = length(elements)
end
test "it can parse `cura_marlin.gcode`" do
assert ok(%Program{elements: elements}) =
Parser.parse_file(fixture_path("cura_marlin.gcode"))
assert 6723 = length(elements)
end
end
describe "stream_string!/1" do
test "it can stream `fusion_360_milling_grbl.nc`" do
elements =
read_fixture("fusion_360_milling_grbl.nc")
|> Parser.stream_string!()
|> Enum.to_list()
assert 500 = length(elements)
end
test "it can stream `cura_marlin.gcode`" do
elements =
read_fixture("cura_marlin.gcode")
|> Parser.stream_string!()
|> Enum.to_list()
assert 6723 = length(elements)
end
end
describe "stream_file!/1" do
test "it can stream `fusion_360_milling_grbl.nc`" do
elements =
fixture_path("fusion_360_milling_grbl.nc")
|> Parser.stream_file!()
|> Enum.to_list()
assert 500 = length(elements)
end
test "it can stream `cura_marlin.gcode`" do
elements =
fixture_path("cura_marlin.gcode")
|> Parser.stream_file!()
|> Enum.to_list()
assert 6723 = length(elements)
end
end
end

View file

@ -0,0 +1,17 @@
defmodule FixtureHelper do
@moduledoc false
def read_fixture(name) do
name
|> fixture_path()
|> File.read!()
end
def fixture_path(name),
do:
:gcode
|> :code.priv_dir()
|> List.to_string()
|> Path.join("fixtures")
|> Path.join(name)
end

View file

@ -0,0 +1,28 @@
defmodule InfixHelper do
alias Gcode.Model.{Expr, Expr.Binary}
use Gcode.Result
@moduledoc false
@type value :: boolean | integer | float | String.t()
@spec cast_expression(value) :: Result.t(Expr.t())
def cast_expression(value) when is_integer(value), do: Expr.Integer.init(value)
def cast_expression(value) when is_float(value), do: Expr.Float.init(value)
def cast_expression(value) when is_boolean(value), do: Expr.Boolean.init(value)
def cast_expression(value) when is_binary(value), do: Expr.String.init(value)
@spec it_evaluates_to(atom, value, value, any) :: Macro.t()
defmacro it_evaluates_to(op, lhs, rhs, result) do
quote do
test "when the operator is `#{unquote(op)}` and the lhs is `#{inspect(unquote(lhs))}` and the rhs is `#{
inspect(unquote(rhs))
}` it is correct" do
ok(lhs) = InfixHelper.cast_expression(unquote(lhs))
ok(rhs) = InfixHelper.cast_expression(unquote(rhs))
ok(bin) = Binary.init(unquote(op), lhs, rhs)
assert unquote(result) = Expr.evaluate(bin)
end
end
end
end

View file

@ -0,0 +1,43 @@
defmodule ParserEngineHelper do
alias Gcode.Parser.Engine
@moduledoc false
defmacro __using__(_) do
quote do
require ParserEngineHelper
import ParserEngineHelper
end
end
defmacro it_parses_into(input, {:fn, _, _} = callback) do
quote do
input_length = byte_size(unquote(input))
description =
if input_length < 60,
do: unquote(input),
else: "#{input_length} byte input"
test description do
assert ok(tokens) = Engine.parse(unquote(input))
unquote(callback).(tokens)
end
end
end
defmacro it_parses_into(input, pattern) do
quote do
input_length = byte_size(unquote(input))
description =
if input_length < 60,
do: unquote(input),
else: "#{input_length} byte input"
test description do
assert ok(tokens) = Engine.parse(unquote(input))
assert unquote(pattern) = tokens
end
end
end
end