Skip to content

Commit

Permalink
Module names refactoring: Parsing -> Parser, Interpreting -> Interpre…
Browse files Browse the repository at this point in the history
…ter (#10)
  • Loading branch information
mtvch committed Jan 17, 2024
1 parent 75d3cf2 commit 0e02b15
Show file tree
Hide file tree
Showing 11 changed files with 197 additions and 197 deletions.
2 changes: 1 addition & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
Enum.flat_map(
["{mix,.formatter,.recode,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"],
&Path.wildcard(&1, match_dot: true)
) -- ["lib/ex_pression/parsing/grammar.ex"]
) -- ["lib/ex_pression/parser/grammar.ex"]
]
2 changes: 1 addition & 1 deletion .recode.exs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
Enum.flat_map(
["{mix,.formatter,.recode,.credo}.exs", "{config,lib,test}/**/*.{ex,exs}"],
&Path.wildcard(&1, match_dot: true)
) -- ["lib/ex_pression/parsing/grammar.ex"],
) -- ["lib/ex_pression/parser/grammar.ex"],
formatter: {Recode.Formatter, []},
tasks: [
# Tasks could be added by a tuple of the tasks module name and an options
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ iex> ExPression.eval(~s/diff($"2023-02-02", $"2022-02-02")/, functions_module: M
Full language description can be found in [FULL_DESCRIPTION.md](./FULL_DESCRIPTION.md)

## Implementation
String representation of expression is parsed into AST form. Parsing is done with PEG grammar parser [xpeg](https://github.com/zevv/xpeg). Grammar is defined in module `ExPression.Parsing.Grammar`.
AST interpretation logic is written in plain `Elixir` in module `ExPression.Interpreting`.
String representation of expression is parsed into AST form. Parsing is done with PEG grammar parser [xpeg](https://github.com/zevv/xpeg). Grammar is defined in module `ExPression.Parser.Grammar`.
AST interpretation logic is written in plain `Elixir` in module `ExPression.Interpreter`.

## Contribution
Feel free to make a pull request. All contributions are appreciated!
2 changes: 1 addition & 1 deletion coveralls.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
"minimum_coverage": 90
},
"skip_files": [
"lib/ex_pression/parsing/grammar.ex"
"lib/ex_pression/parser/grammar.ex"
]
}
8 changes: 4 additions & 4 deletions lib/ex_pression.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ defmodule ExPression do
Evaluate user input expression.
"""
alias ExPression.Error
alias ExPression.Interpreting
alias ExPression.Parsing
alias ExPression.Interpreter
alias ExPression.Parser

@type ast() :: any()

Expand All @@ -15,7 +15,7 @@ defmodule ExPression do
"""
@spec parse(binary()) :: {:ok, ast()} | {:error, ExPression.Error.t()}
def parse(expression_str) when is_binary(expression_str) do
case Parsing.parse(expression_str) do
case Parser.parse(expression_str) do
{:ok, ast} ->
{:ok, ast}

Expand Down Expand Up @@ -60,7 +60,7 @@ defmodule ExPression do
bindings = Keyword.get(opts, :bindings, %{})
functions_module = Keyword.get(opts, :functions_module)

case Interpreting.eval(ast, bindings, functions_module) do
case Interpreter.eval(ast, bindings, functions_module) do
{:ok, res} ->
{:ok, res}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule ExPression.Interpreting do
defmodule ExPression.Interpreter do
@moduledoc """
Evaluating AST
"""
Expand Down
4 changes: 2 additions & 2 deletions lib/ex_pression/parsing.ex → lib/ex_pression/parser.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
defmodule ExPression.Parsing do
defmodule ExPression.Parser do
@moduledoc """
Parsing expressions in strings format with convertion to AST format
"""
alias ExPression.Parsing.Grammar
alias ExPression.Parser.Grammar
@peg Grammar.peg()

@spec parse(binary()) :: {:ok, ast :: any()} | {:error, {:parsing_error, binary()}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule ExPression.Parsing.Grammar do
defmodule ExPression.Parser.Grammar do
@moduledoc """
Expressions formal language grammar definition
"""
Expand Down
166 changes: 166 additions & 0 deletions test/ex_pression/interpreter_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
defmodule ExPression.InterpreterTest do
use ExUnit.Case
alias ExPression.Interpreter
alias ExPression.Parser

defmodule TestModule do
def create_obj do
%{"a" => %{"b" => "c"}}
end

def concat(a, b, c) do
"#{a}#{b}#{c}"
end
end

describe "#happy_path" do
test "function call" do
{:ok, ast} = Parser.parse("div(5, 2)")
assert {:ok, 2} == Interpreter.eval(ast, %{}, Kernel)
end

test "function call: 3 arguments" do
{:ok, ast} = Parser.parse("concat(1, 2, 3)")
assert {:ok, "123"} == Interpreter.eval(ast, %{}, TestModule)
end

test "function call: complex args" do
{:ok, ast} = Parser.parse("concat(concat(1, 2, 3), concat(4, 5, 6), concat(7, 8, 9))")
assert {:ok, "123456789"} == Interpreter.eval(ast, %{}, TestModule)
end

test "function call with variable" do
{:ok, ast} = Parser.parse("div(5, x)")
assert {:ok, 2} == Interpreter.eval(ast, %{"x" => 2}, Kernel)
end

test "function call with no vars + field access" do
{:ok, ast} = Parser.parse("create_obj().a.b")
assert {:ok, "c"} == Interpreter.eval(ast, %{}, TestModule)
end

test "function from standard library" do
{:ok, ast} = Parser.parse("str(1)")
assert {:ok, "1"} == Interpreter.eval(ast)
end

test "sum of two numbers" do
{:ok, ast} = Parser.parse("1 + 0.5")
assert {:ok, 1.5} == Interpreter.eval(ast)
end

test "minus number" do
{:ok, ast} = Parser.parse("1 - 0.5")
assert {:ok, 0.5} == Interpreter.eval(ast)
end

test "ops order" do
{:ok, ast} = Parser.parse("2+3*4+5")
assert {:ok, 19} == Interpreter.eval(ast)
end

test "parenthesis" do
{:ok, ast} = Parser.parse("(2+3)*(4+5)")
assert {:ok, 45} == Interpreter.eval(ast)
end

test "bool 1" do
{:ok, ast} = Parser.parse("true or false")
assert {:ok, true} == Interpreter.eval(ast)
end

test "bool 2" do
{:ok, ast} = Parser.parse("true and false")
assert {:ok, false} == Interpreter.eval(ast)
end

test "bool 3" do
{:ok, ast} = Parser.parse("not false")
assert {:ok, true} == Interpreter.eval(ast)
end

test "bool 4" do
{:ok, ast} = Parser.parse("1 == 1")
assert {:ok, true} == Interpreter.eval(ast)
end

test "bool 5" do
{:ok, ast} = Parser.parse("1 != 1")
assert {:ok, false} == Interpreter.eval(ast)
end

test "array 1" do
{:ok, ast} = Parser.parse("[1, 2, 3]")
assert {:ok, [1, 2, 3]} == Interpreter.eval(ast)
end

test "array 2" do
{:ok, ast} = Parser.parse("[1 - 2, 2, 3]")
assert {:ok, [-1, 2, 3]} == Interpreter.eval(ast)
end

test "array 3" do
{:ok, ast} = Parser.parse("[1 - 2, str(2), [4, 5]]")
assert {:ok, [-1, "2", [4, 5]]} == Interpreter.eval(ast)
end

test "obj 1" do
{:ok, ast} = Parser.parse("{}")
assert {:ok, %{}} == Interpreter.eval(ast)
end

test "obj 2" do
{:ok, ast} = Parser.parse(~s({"a": "b"}))
assert {:ok, %{"a" => "b"}} == Interpreter.eval(ast)
end

test "obj 3" do
{:ok, ast} = Parser.parse(~s({"a": 1 + 2, "b": [{}], "c": {"d": "e"}}))
assert {:ok, %{"a" => 3, "b" => [%{}], "c" => %{"d" => "e"}}} == Interpreter.eval(ast)
end

test "access 1" do
{:ok, ast} = Parser.parse("[1, 2][0]")
assert {:ok, 1} == Interpreter.eval(ast)
end

test "access 2" do
{:ok, ast} = Parser.parse("[[1, 2], 3][0][1]")
assert {:ok, 2} == Interpreter.eval(ast)
end

test "access 3" do
{:ok, ast} = Parser.parse(~s({"a": "b", "c": "d"}["a"]))
assert {:ok, "b"} == Interpreter.eval(ast)
end

test "access 4" do
{:ok, ast} = Parser.parse(~s({"a": "b", "c": "d"}[x]))
assert {:ok, "d"} == Interpreter.eval(ast, %{"x" => "c"})
end
end

describe "#sad_path" do
test "unbound variable 1" do
{:ok, ast} = Parser.parse(~s({"a": x}))
assert {:error, {:var_not_bound, "x"}} == Interpreter.eval(ast)
end

test "unbound variable 2" do
{:ok, ast} = Parser.parse(~s([1, x]))
assert {:error, {:var_not_bound, "x"}} == Interpreter.eval(ast)
end

test "function not defined" do
{:ok, ast} = Parser.parse("not_exist()")
assert {:error, {:fun_not_defined, "not_exist", 0}} == Interpreter.eval(ast)
end

test "function call error" do
{:ok, ast} = Parser.parse("div(5, 0)")

assert {:error, {:function_call_exception, :div, [5, 0], %ArithmeticError{}, _msg}} =
Interpreter.eval(ast, %{}, Kernel)
end
end
end
Loading

0 comments on commit 0e02b15

Please sign in to comment.