From 0e02b15f1e37eb8ddb5844c671bf7856509d1218 Mon Sep 17 00:00:00 2001 From: Matvey Karpov Date: Wed, 17 Jan 2024 17:08:41 +0700 Subject: [PATCH] Module names refactoring: Parsing -> Parser, Interpreting -> Interpreter (#10) --- .formatter.exs | 2 +- .recode.exs | 2 +- README.md | 4 +- coveralls.json | 2 +- lib/ex_pression.ex | 8 +- .../{interpreting.ex => interpreter.ex} | 2 +- lib/ex_pression/{parsing.ex => parser.ex} | 4 +- .../{parsing => parser}/grammar.ex | 2 +- test/ex_pression/interpreter_test.exs | 166 ++++++++++++++++++ test/ex_pression/interpreting_test.exs | 166 ------------------ .../{parsing_test.exs => parser_test.exs} | 36 ++-- 11 files changed, 197 insertions(+), 197 deletions(-) rename lib/ex_pression/{interpreting.ex => interpreter.ex} (99%) rename lib/ex_pression/{parsing.ex => parser.ex} (88%) rename lib/ex_pression/{parsing => parser}/grammar.ex (99%) create mode 100644 test/ex_pression/interpreter_test.exs delete mode 100644 test/ex_pression/interpreting_test.exs rename test/ex_pression/{parsing_test.exs => parser_test.exs} (50%) diff --git a/.formatter.exs b/.formatter.exs index 1582841..73d5fe5 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -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"] ] diff --git a/.recode.exs b/.recode.exs index 1f8637c..8559e41 100644 --- a/.recode.exs +++ b/.recode.exs @@ -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 diff --git a/README.md b/README.md index b41f8ce..41ffd6e 100644 --- a/README.md +++ b/README.md @@ -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! diff --git a/coveralls.json b/coveralls.json index 45968b1..7bbf84e 100644 --- a/coveralls.json +++ b/coveralls.json @@ -3,6 +3,6 @@ "minimum_coverage": 90 }, "skip_files": [ - "lib/ex_pression/parsing/grammar.ex" + "lib/ex_pression/parser/grammar.ex" ] } \ No newline at end of file diff --git a/lib/ex_pression.ex b/lib/ex_pression.ex index 28a9115..2c260bb 100644 --- a/lib/ex_pression.ex +++ b/lib/ex_pression.ex @@ -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() @@ -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} @@ -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} diff --git a/lib/ex_pression/interpreting.ex b/lib/ex_pression/interpreter.ex similarity index 99% rename from lib/ex_pression/interpreting.ex rename to lib/ex_pression/interpreter.ex index ace23f6..820b869 100644 --- a/lib/ex_pression/interpreting.ex +++ b/lib/ex_pression/interpreter.ex @@ -1,4 +1,4 @@ -defmodule ExPression.Interpreting do +defmodule ExPression.Interpreter do @moduledoc """ Evaluating AST """ diff --git a/lib/ex_pression/parsing.ex b/lib/ex_pression/parser.ex similarity index 88% rename from lib/ex_pression/parsing.ex rename to lib/ex_pression/parser.ex index 94d9d2a..17fc549 100644 --- a/lib/ex_pression/parsing.ex +++ b/lib/ex_pression/parser.ex @@ -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()}} diff --git a/lib/ex_pression/parsing/grammar.ex b/lib/ex_pression/parser/grammar.ex similarity index 99% rename from lib/ex_pression/parsing/grammar.ex rename to lib/ex_pression/parser/grammar.ex index e305c50..82bcd48 100644 --- a/lib/ex_pression/parsing/grammar.ex +++ b/lib/ex_pression/parser/grammar.ex @@ -1,4 +1,4 @@ -defmodule ExPression.Parsing.Grammar do +defmodule ExPression.Parser.Grammar do @moduledoc """ Expressions formal language grammar definition """ diff --git a/test/ex_pression/interpreter_test.exs b/test/ex_pression/interpreter_test.exs new file mode 100644 index 0000000..506d680 --- /dev/null +++ b/test/ex_pression/interpreter_test.exs @@ -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 diff --git a/test/ex_pression/interpreting_test.exs b/test/ex_pression/interpreting_test.exs deleted file mode 100644 index feb2c86..0000000 --- a/test/ex_pression/interpreting_test.exs +++ /dev/null @@ -1,166 +0,0 @@ -defmodule ExPression.InterpretingTest do - use ExUnit.Case - alias ExPression.Interpreting - alias ExPression.Parsing - - 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} = Parsing.parse("div(5, 2)") - assert {:ok, 2} == Interpreting.eval(ast, %{}, Kernel) - end - - test "function call: 3 arguments" do - {:ok, ast} = Parsing.parse("concat(1, 2, 3)") - assert {:ok, "123"} == Interpreting.eval(ast, %{}, TestModule) - end - - test "function call: complex args" do - {:ok, ast} = Parsing.parse("concat(concat(1, 2, 3), concat(4, 5, 6), concat(7, 8, 9))") - assert {:ok, "123456789"} == Interpreting.eval(ast, %{}, TestModule) - end - - test "function call with variable" do - {:ok, ast} = Parsing.parse("div(5, x)") - assert {:ok, 2} == Interpreting.eval(ast, %{"x" => 2}, Kernel) - end - - test "function call with no vars + field access" do - {:ok, ast} = Parsing.parse("create_obj().a.b") - assert {:ok, "c"} == Interpreting.eval(ast, %{}, TestModule) - end - - test "function from standard library" do - {:ok, ast} = Parsing.parse("str(1)") - assert {:ok, "1"} == Interpreting.eval(ast) - end - - test "sum of two numbers" do - {:ok, ast} = Parsing.parse("1 + 0.5") - assert {:ok, 1.5} == Interpreting.eval(ast) - end - - test "minus number" do - {:ok, ast} = Parsing.parse("1 - 0.5") - assert {:ok, 0.5} == Interpreting.eval(ast) - end - - test "ops order" do - {:ok, ast} = Parsing.parse("2+3*4+5") - assert {:ok, 19} == Interpreting.eval(ast) - end - - test "parenthesis" do - {:ok, ast} = Parsing.parse("(2+3)*(4+5)") - assert {:ok, 45} == Interpreting.eval(ast) - end - - test "bool 1" do - {:ok, ast} = Parsing.parse("true or false") - assert {:ok, true} == Interpreting.eval(ast) - end - - test "bool 2" do - {:ok, ast} = Parsing.parse("true and false") - assert {:ok, false} == Interpreting.eval(ast) - end - - test "bool 3" do - {:ok, ast} = Parsing.parse("not false") - assert {:ok, true} == Interpreting.eval(ast) - end - - test "bool 4" do - {:ok, ast} = Parsing.parse("1 == 1") - assert {:ok, true} == Interpreting.eval(ast) - end - - test "bool 5" do - {:ok, ast} = Parsing.parse("1 != 1") - assert {:ok, false} == Interpreting.eval(ast) - end - - test "array 1" do - {:ok, ast} = Parsing.parse("[1, 2, 3]") - assert {:ok, [1, 2, 3]} == Interpreting.eval(ast) - end - - test "array 2" do - {:ok, ast} = Parsing.parse("[1 - 2, 2, 3]") - assert {:ok, [-1, 2, 3]} == Interpreting.eval(ast) - end - - test "array 3" do - {:ok, ast} = Parsing.parse("[1 - 2, str(2), [4, 5]]") - assert {:ok, [-1, "2", [4, 5]]} == Interpreting.eval(ast) - end - - test "obj 1" do - {:ok, ast} = Parsing.parse("{}") - assert {:ok, %{}} == Interpreting.eval(ast) - end - - test "obj 2" do - {:ok, ast} = Parsing.parse(~s({"a": "b"})) - assert {:ok, %{"a" => "b"}} == Interpreting.eval(ast) - end - - test "obj 3" do - {:ok, ast} = Parsing.parse(~s({"a": 1 + 2, "b": [{}], "c": {"d": "e"}})) - assert {:ok, %{"a" => 3, "b" => [%{}], "c" => %{"d" => "e"}}} == Interpreting.eval(ast) - end - - test "access 1" do - {:ok, ast} = Parsing.parse("[1, 2][0]") - assert {:ok, 1} == Interpreting.eval(ast) - end - - test "access 2" do - {:ok, ast} = Parsing.parse("[[1, 2], 3][0][1]") - assert {:ok, 2} == Interpreting.eval(ast) - end - - test "access 3" do - {:ok, ast} = Parsing.parse(~s({"a": "b", "c": "d"}["a"])) - assert {:ok, "b"} == Interpreting.eval(ast) - end - - test "access 4" do - {:ok, ast} = Parsing.parse(~s({"a": "b", "c": "d"}[x])) - assert {:ok, "d"} == Interpreting.eval(ast, %{"x" => "c"}) - end - end - - describe "#sad_path" do - test "unbound variable 1" do - {:ok, ast} = Parsing.parse(~s({"a": x})) - assert {:error, {:var_not_bound, "x"}} == Interpreting.eval(ast) - end - - test "unbound variable 2" do - {:ok, ast} = Parsing.parse(~s([1, x])) - assert {:error, {:var_not_bound, "x"}} == Interpreting.eval(ast) - end - - test "function not defined" do - {:ok, ast} = Parsing.parse("not_exist()") - assert {:error, {:fun_not_defined, "not_exist", 0}} == Interpreting.eval(ast) - end - - test "function call error" do - {:ok, ast} = Parsing.parse("div(5, 0)") - - assert {:error, {:function_call_exception, :div, [5, 0], %ArithmeticError{}, _msg}} = - Interpreting.eval(ast, %{}, Kernel) - end - end -end diff --git a/test/ex_pression/parsing_test.exs b/test/ex_pression/parser_test.exs similarity index 50% rename from test/ex_pression/parsing_test.exs rename to test/ex_pression/parser_test.exs index cbb64e4..a301cdb 100644 --- a/test/ex_pression/parsing_test.exs +++ b/test/ex_pression/parser_test.exs @@ -1,81 +1,81 @@ -defmodule ExPression.ParsingTest do +defmodule ExPression.ParserTest do use ExUnit.Case - alias ExPression.Parsing + alias ExPression.Parser describe "#happy_path" do test "string" do - assert {:ok, "string"} == Parsing.parse(~s("string")) + assert {:ok, "string"} == Parser.parse(~s("string")) end test "function call" do - assert {:ok, {:fun_call, ["f", {:var, ["x"]}]}} == Parsing.parse("f(x)") + assert {:ok, {:fun_call, ["f", {:var, ["x"]}]}} == Parser.parse("f(x)") end test "field access" do assert {:ok, {:field_access, [{:var, ["x"]}, "field_name"]}} == - Parsing.parse("x.field_name") + Parser.parse("x.field_name") end test "nested field access" do assert {:ok, {:field_access, [{:field_access, [{:var, ["x"]}, "field_name"]}, "another_field"]}} == - Parsing.parse("x.field_name.another_field") + Parser.parse("x.field_name.another_field") end test "2 args function call: int and string" do assert {:ok, {:fun_call, ["my_function", 1, "some"]}} == - Parsing.parse("my_function(1, \"some\")") + Parser.parse("my_function(1, \"some\")") end test "float number" do - assert {:ok, {:fun_call, ["str", 1.5]}} == Parsing.parse("str(1.5)") + assert {:ok, {:fun_call, ["str", 1.5]}} == Parser.parse("str(1.5)") end test "sum of to numbers" do - assert {:ok, {:+, [1, 0.5]}} == Parsing.parse("1 + 0.5") + assert {:ok, {:+, [1, 0.5]}} == Parser.parse("1 + 0.5") end test "ops order" do - assert {:ok, {:+, [2, {:+, [{:*, [3, 4]}, 5]}]}} == Parsing.parse("2+3*4+5") + assert {:ok, {:+, [2, {:+, [{:*, [3, 4]}, 5]}]}} == Parser.parse("2+3*4+5") end test "bool 2" do - assert {:ok, {:and, [true, false]}} == Parsing.parse("true and false") + assert {:ok, {:and, [true, false]}} == Parser.parse("true and false") end test "access 1" do - assert {:ok, {:access, [{:array, [1, 2]}, 0]}} == Parsing.parse("[1, 2][0]") + assert {:ok, {:access, [{:array, [1, 2]}, 0]}} == Parser.parse("[1, 2][0]") end test "access 2" do assert {:ok, {:access, [{:access, [{:array, [{:array, [1, 2]}, 3]}, 0]}, 0]}} == - Parsing.parse("[[1, 2], 3][0][0]") + Parser.parse("[[1, 2], 3][0][0]") end test "empty obj" do - assert {:ok, {:obj, []}} == Parsing.parse("{}") + assert {:ok, {:obj, []}} == Parser.parse("{}") end test "obj with 1 entry" do - assert {:ok, {:obj, [{"a", "b"}]}} == Parsing.parse(~s({"a": "b"})) + assert {:ok, {:obj, [{"a", "b"}]}} == Parser.parse(~s({"a": "b"})) end test "complex obj" do assert {:ok, {:obj, [{"c", {:obj, [{"d", "e"}]}}, {"b", {:array, [obj: []]}}, {"a", {:+, [1, 2]}}]}} == - Parsing.parse(~s({"a": 1 + 2, "b": [{}], "c": {"d": "e"}})) + Parser.parse(~s({"a": 1 + 2, "b": [{}], "c": {"d": "e"}})) end end describe "#sad_path" do test "invalid expression 2" do - assert {:error, {:parsing_error, "}"}} == Parsing.parse(~s({}})) + assert {:error, {:parsing_error, "}"}} == Parser.parse(~s({}})) end test "invalid expression 3" do - assert assert {:error, {:parsing_error, " +"}} == Parsing.parse(~s(1 +)) + assert assert {:error, {:parsing_error, " +"}} == Parser.parse(~s(1 +)) end end end