├── test
├── e2e
│ ├── perl
│ │ ├── codes
│ │ │ └── .keep
│ │ ├── mock
│ │ │ ├── Plugins.pm
│ │ │ ├── Log.pm
│ │ │ ├── Globals.pm
│ │ │ └── Commands.pm
│ │ └── runner.pl
│ ├── blank_macro_test.exs
│ ├── rand_test.exs
│ ├── special_variables_test.exs
│ ├── log_test.exs
│ ├── cli_command_test.exs
│ ├── random_test.exs
│ ├── call_test.exs
│ ├── interpolation_test.exs
│ ├── if_block_test.exs
│ └── postfix_if_test.exs
├── test_helper.exs
├── functional
│ ├── semantic_analysis
│ │ ├── special_variables_test.exs
│ │ ├── call_test.exs
│ │ └── variables_test.exs
│ └── optimization
│ │ ├── dead_code_strip_test.exs
│ │ └── constant_folding_test.exs
└── helpers
│ ├── optimization_helper.ex
│ ├── semantic_analysis.ex
│ └── e2e_helper.ex
├── .travis.yml
├── docs
├── logo-big.png
├── logo-small.png
└── examples.md
├── lib
├── semantic_analysis
│ ├── fatal_error.ex
│ ├── validates
│ │ ├── special_variables.ex
│ │ ├── macros.ex
│ │ └── variables.ex
│ ├── latest_variable_writes.ex
│ ├── symbols_table.ex
│ └── semantic_analysis.ex
├── code_generation
│ ├── footer.ex
│ ├── header.ex
│ └── body.ex
├── parsers
│ ├── blank_spaces.ex
│ ├── comment.ex
│ ├── lazy.ex
│ ├── top_level_block.ex
│ ├── syntax_error.ex
│ ├── macro_block
│ │ ├── flow_control
│ │ │ ├── single_check.ex
│ │ │ ├── postfix_if.ex
│ │ │ ├── condition.ex
│ │ │ └── if_block.ex
│ │ ├── variables
│ │ │ ├── hash_variable.ex
│ │ │ ├── array_variable.ex
│ │ │ ├── commands
│ │ │ │ ├── decrement_command.ex
│ │ │ │ ├── increment_command.ex
│ │ │ │ └── undef_command.ex
│ │ │ ├── hash_keywords
│ │ │ │ ├── delete_command.ex
│ │ │ │ ├── keys_command.ex
│ │ │ │ └── values_command.ex
│ │ │ ├── array_keywords
│ │ │ │ ├── pop_command.ex
│ │ │ │ ├── shift_command.ex
│ │ │ │ ├── push_command.ex
│ │ │ │ └── unshift_command.ex
│ │ │ ├── assignments
│ │ │ │ ├── scalar_assignment_command.ex
│ │ │ │ ├── array_assignment_command.ex
│ │ │ │ └── hash_assignment_command.ex
│ │ │ └── scalar_variable.ex
│ │ ├── commands
│ │ │ ├── do_command.ex
│ │ │ ├── log_command.ex
│ │ │ ├── pause_command.ex
│ │ │ └── call_command.ex
│ │ ├── scalar_value
│ │ │ ├── scalar_value.ex
│ │ │ ├── rand_command.ex
│ │ │ └── random_command.ex
│ │ ├── macro.ex
│ │ └── macro_block.ex
│ ├── parser.ex
│ ├── metadata.ex
│ ├── identifier.ex
│ └── text_value.ex
├── error
│ ├── utils.ex
│ └── error.ex
├── optimization
│ ├── optimization.ex
│ ├── dead_code_strip
│ │ └── dead_code_strip.ex
│ └── constant_folding
│ │ └── constant_folding.ex
└── macrocompiler.ex
├── mix.lock
├── macro.txt
├── .gitignore
├── mix.exs
├── config
└── config.exs
└── README.md
/test/e2e/perl/codes/.keep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: elixir
2 | elixir: '1.6.6'
3 | otp_release: '19.0'
4 |
5 |
--------------------------------------------------------------------------------
/docs/logo-big.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macabeus/macro-compiler/HEAD/docs/logo-big.png
--------------------------------------------------------------------------------
/test/e2e/perl/mock/Plugins.pm:
--------------------------------------------------------------------------------
1 | package Plugins;
2 |
3 | sub register { }
4 |
5 | 1;
6 |
7 |
--------------------------------------------------------------------------------
/docs/logo-small.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/macabeus/macro-compiler/HEAD/docs/logo-small.png
--------------------------------------------------------------------------------
/lib/semantic_analysis/fatal_error.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.SemanticAnalysis.FatalError do
2 | defexception [:message]
3 | end
4 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [], [], "hexpm"}}
2 |
--------------------------------------------------------------------------------
/lib/code_generation/footer.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.CodeGeneration.Footer do
2 | def generate() do
3 | ["1;"]
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/test/e2e/perl/mock/Log.pm:
--------------------------------------------------------------------------------
1 | package Log;
2 |
3 | use Exporter 'import';
4 |
5 | our @EXPORT_OK = qw(message);
6 |
7 | sub message {
8 | print @_;
9 | }
10 |
11 | 1;
12 |
13 |
--------------------------------------------------------------------------------
/macro.txt:
--------------------------------------------------------------------------------
1 | macro goToRandomCity {
2 | @cities = (prontera, payon, geffen, morroc)
3 | $randomCity = $cities[&rand(0, 3)]
4 |
5 | log I'll go to $randomCity !
6 | do move $randomCity
7 | }
8 |
--------------------------------------------------------------------------------
/test/e2e/perl/runner.pl:
--------------------------------------------------------------------------------
1 | use lib './mock';
2 | use Commands;
3 | use Plugins;
4 | require "./codes/$ARGV[0].pl";
5 |
6 | if ($ARGV[1] != "0") {
7 | macroCompiled::macro_Test();
8 | }
9 |
10 | if ($ARGV[2]) {
11 | &$Commands::commandHandle('macroCompiled', $ARGV[2]);
12 | }
13 |
--------------------------------------------------------------------------------
/lib/parsers/blank_spaces.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.BlankSpaces do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | def parser() do
6 | take_while(
7 | fn ?\s -> true;
8 | ?\n -> true;
9 | ?\t -> true;
10 |
11 | _ -> false
12 | end
13 | )
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/test/e2e/blank_macro_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.BlankMacro do
2 | use MacroCompiler.Test.Helper.E2e
3 |
4 | def code, do: """
5 | macro Test {
6 | # nothing
7 | }
8 | """
9 |
10 | test_output :string, "should return nothing", fn value ->
11 | value == ""
12 | end
13 | end
14 |
15 |
--------------------------------------------------------------------------------
/test/e2e/perl/mock/Globals.pm:
--------------------------------------------------------------------------------
1 | ###
2 | package Char;
3 |
4 | sub new {
5 | my $class = shift;
6 | my $self = {
7 | zeny => shift
8 | };
9 | }
10 |
11 | ###
12 | package Globals;
13 |
14 | use Exporter 'import';
15 |
16 | our @EXPORT_OK = qw($char) ;
17 |
18 | our $char = new Char(1000);
19 |
20 | 1;
21 |
22 |
--------------------------------------------------------------------------------
/lib/parsers/comment.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.Comment do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | def parser() do
6 | sequence([
7 | ignore(char("#")),
8 |
9 | take_while(
10 | fn ?\n -> false;
11 |
12 | _ -> true
13 | end
14 | )
15 | ])
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/test/e2e/perl/mock/Commands.pm:
--------------------------------------------------------------------------------
1 | package Commands;
2 |
3 | use Exporter 'import';
4 | use strict;
5 |
6 | our $commandHandle;
7 |
8 | our @EXPORT_OK = qw(register);
9 |
10 | sub register {
11 | foreach my $command (@_) {
12 | my ($name, $desc, $func) = @$command;
13 | $commandHandle = $func;
14 | }
15 | }
16 |
17 | 1;
18 |
19 |
--------------------------------------------------------------------------------
/test/e2e/rand_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.Rand do
2 | use MacroCompiler.Test.Helper.E2e
3 |
4 | def code, do: """
5 | macro Test {
6 | $value = &rand(0, 5)
7 | log $value
8 | }
9 | """
10 |
11 | test_output :integer, "should be between 0 and 5", fn value ->
12 | Enum.member?(0..5, value)
13 | end
14 | end
15 |
16 |
--------------------------------------------------------------------------------
/test/e2e/special_variables_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.SpecialVariables do
2 | use MacroCompiler.Test.Helper.E2e
3 |
4 | def code, do: """
5 | macro Test {
6 | log I have $.zeny zeny!
7 | }
8 | """
9 |
10 | test_output :string, "should print the amount of zeny", fn value ->
11 | value == "I have 1000 zeny!"
12 | end
13 | end
14 |
15 |
--------------------------------------------------------------------------------
/lib/parsers/lazy.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.Lazy do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | alias Combine.ParserState
6 |
7 | defparser lazy_parser(%ParserState{status: :ok} = state, generator) do
8 | (generator.()).(state)
9 | end
10 |
11 | defmacro lazy(body) do
12 | quote do
13 | lazy_parser(fn -> unquote(body) end)
14 | end
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/e2e/log_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.Log do
2 | use MacroCompiler.Test.Helper.E2e
3 |
4 | def code, do: """
5 | macro Test {
6 | log foo
7 | log 123
8 | }
9 | """
10 |
11 | test_output :string, "should be foo", fn value ->
12 | value == "foo"
13 | end
14 |
15 | test_output :integer, "should be 123", fn value ->
16 | value == 123
17 | end
18 | end
19 |
20 |
--------------------------------------------------------------------------------
/lib/parsers/top_level_block.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.TopLevelBlock do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | alias MacroCompiler.Parser.Macro
6 | alias MacroCompiler.Parser.Comment
7 |
8 | def parser() do
9 | many(
10 | choice([
11 | ignore(spaces()),
12 | ignore(newline()),
13 | ignore(Comment.parser()),
14 |
15 | Macro.parser()
16 | ])
17 | )
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/test/e2e/cli_command_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.CLICommand do
2 | use MacroCompiler.Test.Helper.E2e, %{
3 | run_test_macro: false,
4 | run_cli_command: "ManualCallingMacro"
5 | }
6 |
7 | def code, do: """
8 | macro ManualCallingMacro {
9 | log called!
10 | }
11 | """
12 |
13 | test_output :string, "should called ManualCallingMacro", fn value ->
14 | value == "called!"
15 | end
16 | end
17 |
18 |
--------------------------------------------------------------------------------
/test/e2e/random_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.Random do
2 | use MacroCompiler.Test.Helper.E2e
3 |
4 | def code, do: """
5 | macro Test {
6 | $city = &random(prontera, payon, geffen, marroc)
7 | log $city
8 | }
9 | """
10 |
11 | test_output :string, "should be a random city", fn value ->
12 | cities = ["prontera", "payon", "geffen", "marroc"]
13 | Enum.member?(cities, value)
14 | end
15 | end
16 |
17 |
--------------------------------------------------------------------------------
/lib/parsers/syntax_error.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.SyntaxError do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | alias MacroCompiler.Parser.SyntaxError
6 | alias Combine.ParserState
7 |
8 | defexception [:message, :line, :offset]
9 |
10 | defparser raiseAtPosition(%ParserState{status: :ok, line: line, column: col} = state) do
11 | raise SyntaxError,
12 | message: "Unknow syntax error",
13 | line: line,
14 | offset: col
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/flow_control/single_check.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.SingleCheck do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.SingleCheck
8 | alias MacroCompiler.Parser.ScalarValue
9 |
10 | @enforce_keys [:scalar_variable]
11 | defstruct [:scalar_variable]
12 |
13 | parser_command do
14 | ScalarValue.parser()
15 | end
16 |
17 | def map_command(scalar_variable) do
18 | %SingleCheck{scalar_variable: scalar_variable}
19 | end
20 | end
21 |
22 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/hash_variable.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.HashVariable do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.HashVariable
8 | alias MacroCompiler.Parser.Identifier
9 |
10 | @enforce_keys [:name]
11 | defstruct [:name]
12 |
13 | parser_command do
14 | sequence([
15 | ignore(string("%")),
16 | Identifier.parser()
17 | ])
18 | end
19 |
20 | def map_command([name]) do
21 | %HashVariable{name: name}
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/array_variable.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.ArrayVariable do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.ArrayVariable
8 | alias MacroCompiler.Parser.Identifier
9 |
10 | @enforce_keys [:name]
11 | defstruct [:name]
12 |
13 | parser_command do
14 | sequence([
15 | ignore(string("@")),
16 | Identifier.parser()
17 | ])
18 | end
19 |
20 | def map_command([name]) do
21 | %ArrayVariable{name: name}
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/commands/do_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.DoCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.DoCommand
8 | alias MacroCompiler.Parser.TextValue
9 |
10 | @enforce_keys [:text]
11 | defstruct [:text]
12 |
13 | parser_command do
14 | sequence([
15 | ignore(string("do")),
16 | skip(space()),
17 |
18 | TextValue.parser(false)
19 | ])
20 | end
21 |
22 | def map_command([text]) do
23 | %DoCommand{text: text}
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/commands/log_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.LogCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.LogCommand
8 | alias MacroCompiler.Parser.TextValue
9 |
10 | @enforce_keys [:text]
11 | defstruct [:text]
12 |
13 | parser_command do
14 | sequence([
15 | ignore(string("log")),
16 | ignore(spaces()),
17 |
18 | TextValue.parser(false)
19 | ])
20 | end
21 |
22 | def map_command([text]) do
23 | %LogCommand{text: text}
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/parsers/parser.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | alias MacroCompiler.Parser.Metadata
6 |
7 | defmacro parser_command(do: body) do
8 | quote do
9 | def parser() do
10 | map(
11 | sequence([
12 | Metadata.getMetadata(),
13 | unquote(body)
14 | ]),
15 |
16 | fn [metadata, node] ->
17 | {
18 | map_command(node),
19 | metadata
20 | }
21 | end
22 | )
23 | end
24 | end
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/scalar_value/scalar_value.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.ScalarValue do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser.Lazy
6 |
7 | alias MacroCompiler.Parser.ScalarVariable
8 | alias MacroCompiler.Parser.RandCommand
9 | alias MacroCompiler.Parser.RandomCommand
10 | alias MacroCompiler.Parser.TextValue
11 |
12 | def parser() do
13 | choice([
14 | lazy(ScalarVariable.parser()),
15 | lazy(RandCommand.parser()),
16 | lazy(RandomCommand.parser()),
17 | lazy(TextValue.parser())
18 | ])
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/commands/pause_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.PauseCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.PauseCommand
8 |
9 | @enforce_keys [:seconds]
10 | defstruct [:seconds]
11 |
12 | parser_command do
13 | sequence([
14 | ignore(string("pause")),
15 | skip(spaces()),
16 |
17 | option(either(
18 | float(),
19 | integer()
20 | ))
21 | ])
22 | end
23 |
24 | def map_command([seconds]) do
25 | %PauseCommand{seconds: seconds}
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/test/e2e/call_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.Call do
2 | use MacroCompiler.Test.Helper.E2e
3 |
4 | def code, do: """
5 | macro Test {
6 | log start
7 | call Called
8 | log end
9 | }
10 |
11 | macro Called {
12 | log called
13 | }
14 | """
15 |
16 | test_output :string, "should startly print 'start'", fn value ->
17 | value == "start"
18 | end
19 |
20 | test_output :string, "should print 'called'", fn value ->
21 | value == "called"
22 | end
23 |
24 | test_output :string, "should print at end 'end'", fn value ->
25 | value == "end"
26 | end
27 | end
28 |
29 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/commands/decrement_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.DecrementCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.DecrementCommand
8 | alias MacroCompiler.Parser.ScalarVariable
9 |
10 | @enforce_keys [:scalar_variable]
11 | defstruct [:scalar_variable]
12 |
13 | parser_command do
14 | sequence([
15 | ScalarVariable.parser(),
16 |
17 | ignore(string("--"))
18 | ])
19 | end
20 |
21 | def map_command([scalar_variable]) do
22 | %DecrementCommand{scalar_variable: scalar_variable}
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/commands/increment_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.IncrementCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.IncrementCommand
8 | alias MacroCompiler.Parser.ScalarVariable
9 |
10 | @enforce_keys [:scalar_variable]
11 | defstruct [:scalar_variable]
12 |
13 | parser_command do
14 | sequence([
15 | ScalarVariable.parser(),
16 |
17 | ignore(string("++"))
18 | ])
19 | end
20 |
21 | def map_command([scalar_variable]) do
22 | %IncrementCommand{scalar_variable: scalar_variable}
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/parsers/metadata.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.Metadata do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | alias Combine.ParserState
6 | alias MacroCompiler.Parser.Metadata
7 |
8 | @enforce_keys [:line, :offset]
9 | defstruct [:line, :offset, :ignore]
10 |
11 | defparser getMetadata(%ParserState{status: :ok, line: line, column: col, results: results} = state) do
12 | case Process.get(:no_metadata) do
13 | true ->
14 | %{state | :results => [%Metadata{line: 0, offset: 0} | results]}
15 | _ ->
16 | %{state | :results => [%Metadata{line: line, offset: col} | results]}
17 | end
18 | end
19 | end
20 |
--------------------------------------------------------------------------------
/lib/parsers/identifier.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.Identifier do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | def parser() do
6 | map(
7 | take_while(
8 | fn 0x20 -> false;
9 | ?\n -> false;
10 | ?, -> false;
11 | ?( -> false;
12 | ?) -> false;
13 | ?[ -> false;
14 | ?] -> false;
15 | ?{ -> false;
16 | ?} -> false;
17 | ?+ -> false;
18 | ?- -> false;
19 | ?# -> false;
20 |
21 | _ -> true
22 | end
23 | ),
24 | fn name -> List.to_string(name) end
25 | )
26 | end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/hash_keywords/delete_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.DeleteCommand do
2 | use Combine
3 |
4 | import MacroCompiler.Parser
5 |
6 | alias MacroCompiler.Parser.ScalarVariable
7 | alias MacroCompiler.Parser.DeleteCommand
8 |
9 | @enforce_keys [:scalar_variable]
10 | defstruct [:scalar_variable]
11 |
12 | parser_command do
13 | sequence([
14 | ignore(string("&delete(")),
15 |
16 | ScalarVariable.parser(),
17 |
18 | ignore(string(")"))
19 | ])
20 | end
21 |
22 | def map_command([scalar_variable]) do
23 | %DeleteCommand{scalar_variable: scalar_variable}
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/array_keywords/pop_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.PopCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.PopCommand
8 | alias MacroCompiler.Parser.ArrayVariable
9 |
10 | @enforce_keys [:array_variable]
11 | defstruct [:array_variable]
12 |
13 | parser_command do
14 | sequence([
15 | ignore(string("&pop(")),
16 |
17 | ArrayVariable.parser(),
18 |
19 | ignore(string(")"))
20 | ])
21 | end
22 |
23 | def map_command([array_variable]) do
24 | %PopCommand{array_variable: array_variable}
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/array_keywords/shift_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.ShiftCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.ShiftCommand
8 | alias MacroCompiler.Parser.ArrayVariable
9 |
10 | @enforce_keys [:array_variable]
11 | defstruct [:array_variable]
12 |
13 | parser_command do
14 | sequence([
15 | ignore(string("&shift(")),
16 |
17 | ArrayVariable.parser(),
18 |
19 | ignore(string(")"))
20 | ])
21 | end
22 |
23 | def map_command([array_variable]) do
24 | %ShiftCommand{array_variable: array_variable}
25 | end
26 | end
27 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # The directory Mix will write compiled artifacts to.
2 | /_build/
3 |
4 | # If you run "mix test --cover", coverage assets end up here.
5 | /cover/
6 |
7 | # The directory Mix downloads your dependencies sources to.
8 | /deps/
9 |
10 | # Where 3rd-party dependencies like ExDoc output generated docs.
11 | /doc/
12 |
13 | # Ignore .fetch files in case you like to edit your project deps locally.
14 | /.fetch
15 |
16 | # If the VM crashes, it generates a dump, let's ignore it too.
17 | erl_crash.dump
18 |
19 | # Also ignore archive artifacts (built via "mix archive.build").
20 | *.ez
21 |
22 | # Macro compiled file
23 | perl.pl
24 |
25 | # e2e temp files
26 | test/e2e/perl/codes/
27 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/scalar_value/rand_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.RandCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.RandCommand
8 | alias MacroCompiler.Parser.ScalarValue
9 |
10 | @enforce_keys [:min, :max]
11 | defstruct [:min, :max]
12 |
13 | parser_command do
14 | sequence([
15 | ignore(string("&rand(")),
16 |
17 | ScalarValue.parser(),
18 |
19 | ignore(char(",")),
20 | skip(spaces()),
21 |
22 | ScalarValue.parser(),
23 |
24 | ignore(char(")"))
25 | ])
26 | end
27 |
28 | def map_command([min, max]) do
29 | %RandCommand{min: min, max: max}
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/e2e/interpolation_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.Interpolation do
2 | use MacroCompiler.Test.Helper.E2e
3 |
4 | def code, do: """
5 | macro Test {
6 | $scalar = foo
7 | log scalar: $scalar
8 |
9 | @array = (1, 2)
10 | log array: @array
11 |
12 | %hash = (1 => foo, 2 => bar)
13 | log hash: %hash
14 | }
15 | """
16 |
17 | test_output :string, "should can interpolate a scalar", fn value ->
18 | value == "scalar: foo"
19 | end
20 |
21 | test_output :string, "should can interpolate an array", fn value ->
22 | value == "array: 2"
23 | end
24 |
25 | test_output :string, "should can interpolate a hash", fn value ->
26 | value == "hash: 2"
27 | end
28 | end
29 |
30 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/scalar_value/random_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.RandomCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.RandomCommand
8 | alias MacroCompiler.Parser.ScalarValue
9 |
10 | @enforce_keys [:values]
11 | defstruct [:values]
12 |
13 | parser_command do
14 | sequence([
15 | ignore(string("&random(")),
16 |
17 | sep_by(
18 | ScalarValue.parser(),
19 |
20 | sequence([
21 | char(?,),
22 | skip(spaces())
23 | ])
24 | ),
25 |
26 | ignore(char(?)))
27 | ])
28 | end
29 |
30 | def map_command([values]) do
31 | %RandomCommand{values: values}
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :macrocompiler,
7 | version: "0.1.0",
8 | elixir: "~> 1.5",
9 | elixirc_paths: elixirc_paths(Mix.env),
10 | start_permanent: Mix.env == :prod,
11 | deps: deps()
12 | ]
13 | end
14 |
15 | defp elixirc_paths(:test), do: ["lib", "test/helpers"]
16 | defp elixirc_paths(_), do: ["lib"]
17 |
18 | # Run "mix help compile.app" to learn about applications.
19 | def application do
20 | [
21 | extra_applications: [:logger]
22 | ]
23 | end
24 |
25 | # Run "mix help deps" to learn about dependencies.
26 | defp deps do
27 | [
28 | {:combine, "~> 0.10.0"}
29 | ]
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/flow_control/postfix_if.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.PostfixIf do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.PostfixIf
8 | alias MacroCompiler.Parser.SingleCheck
9 | alias MacroCompiler.Parser.Condition
10 |
11 | @enforce_keys [:condition]
12 | defstruct [:condition, :block]
13 |
14 | parser_command do
15 | sequence([
16 | skip(spaces()),
17 |
18 | ignore(string("if (")),
19 |
20 | choice([
21 | Condition.parser(),
22 | SingleCheck.parser()
23 | ]),
24 |
25 | ignore(string(")"))
26 | ])
27 | end
28 |
29 | def map_command([condition]) do
30 | %PostfixIf{condition: condition}
31 | end
32 | end
33 |
34 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/commands/undef_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.UndefCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.UndefCommand
8 | alias MacroCompiler.Parser.ScalarVariable
9 |
10 | @enforce_keys [:scalar_variable]
11 | defstruct [:scalar_variable]
12 |
13 | parser_command do
14 | sequence([
15 | ScalarVariable.parser(),
16 |
17 | skip(spaces()),
18 | ignore(string("=")),
19 | skip(spaces()),
20 |
21 | ignore(choice([
22 | string("undef"),
23 | string("unset")
24 | ]))
25 | ])
26 | end
27 |
28 | def map_command([scalar_variable]) do
29 | %UndefCommand{scalar_variable: scalar_variable}
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/test/functional/semantic_analysis/special_variables_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.Functional.SemanticAnalysis.SpecialVariables do
2 | use MacroCompiler.Test.Helper.SemanticAnalysis
3 |
4 | test_should_works(
5 | "should can read a special variable",
6 | """
7 | macro Test {
8 | log $.zeny
9 | }
10 | """
11 | )
12 |
13 | test_semantic_error(
14 | "should fail when try to write in a special variable",
15 | """
16 | macro Test {
17 | $.zeny = 1
18 | }
19 | """,
20 | [
21 | [
22 | message: [:red, "$.zeny", :default_color, " is a special variable, reassigning is not allowed"],
23 | metadatas: [%MacroCompiler.Parser.Metadata{ignore: nil, line: 2, offset: 4}]
24 | ]
25 | ]
26 | )
27 | end
28 |
29 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/macro.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.Macro do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.Macro
8 | alias MacroCompiler.Parser.MacroBlock
9 | alias MacroCompiler.Parser.Identifier
10 |
11 | @enforce_keys [:name, :block]
12 | defstruct [:name, :block]
13 |
14 | parser_command do
15 | sequence([
16 | ignore(string("macro")),
17 | ignore(spaces()),
18 |
19 | Identifier.parser(),
20 |
21 | ignore(spaces()),
22 | ignore(char("{")),
23 | skip(newline()),
24 |
25 | MacroBlock.parser(),
26 |
27 | skip(char("}"))
28 | ])
29 | end
30 |
31 | def map_command([macro_name, block]) do
32 | %Macro{name: macro_name, block: block}
33 | end
34 | end
35 |
--------------------------------------------------------------------------------
/lib/error/utils.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Error.Utils do
2 | alias MacroCompiler.Parser.Metadata
3 |
4 | def calc_line_and_column(file, %Metadata{line: line, offset: offset}) do
5 | calc_line_and_column(file, line, offset)
6 | end
7 |
8 | def calc_line_and_column(file, line, offset) do
9 | file_lines = file
10 | |> String.split("\n")
11 |
12 | calc(file_lines, line - 1, offset)
13 | end
14 |
15 | defp calc(file_lines, line, offset) do
16 | line_length = file_lines
17 | |> Enum.at(line)
18 | |> String.length
19 |
20 | line_length = line_length + 1 # adding + 1 because we need to count the "\n"
21 |
22 | if offset >= line_length do
23 | calc(file_lines, line + 1, offset - line_length)
24 | else
25 | {line + 1, offset}
26 | end
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/test/functional/semantic_analysis/call_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.Functional.SemanticAnalysis.Call do
2 | use MacroCompiler.Test.Helper.SemanticAnalysis
3 |
4 | test_should_works(
5 | "should can call a macro",
6 | """
7 | macro Test {
8 | call ShouldCall
9 | }
10 |
11 | macro ShouldCall {
12 | }
13 | """
14 | )
15 |
16 | test_semantic_error(
17 | "should fail when try to call an unknown macro",
18 | """
19 | macro Test {
20 | call UnknownMacro
21 | }
22 | """,
23 | [
24 | [
25 | message: ["macro ", :red, "UnknownMacro", :default_color, " is called but it has never been written."],
26 | metadatas: [%MacroCompiler.Parser.Metadata{ignore: nil, line: 2, offset: 4}]
27 | ]
28 | ]
29 | )
30 | end
31 |
32 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/array_keywords/push_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.PushCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.PushCommand
8 | alias MacroCompiler.Parser.ArrayVariable
9 | alias MacroCompiler.Parser.TextValue
10 |
11 | @enforce_keys [:array_variable, :text]
12 | defstruct [:array_variable, :text]
13 |
14 | parser_command do
15 | sequence([
16 | ignore(string("&push(")),
17 |
18 | ArrayVariable.parser(),
19 |
20 | skip(spaces()),
21 | ignore(string(",")),
22 | skip(spaces()),
23 |
24 | TextValue.parser(),
25 |
26 | ignore(string(")"))
27 | ])
28 | end
29 |
30 | def map_command([array_variable, text]) do
31 | %PushCommand{array_variable: array_variable, text: text}
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/array_keywords/unshift_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.UnshiftCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.UnshiftCommand
8 | alias MacroCompiler.Parser.ArrayVariable
9 | alias MacroCompiler.Parser.TextValue
10 |
11 | @enforce_keys [:array_variable, :text]
12 | defstruct [:array_variable, :text]
13 |
14 | parser_command do
15 | sequence([
16 | ignore(string("&unshift(")),
17 |
18 | ArrayVariable.parser(),
19 |
20 | skip(spaces()),
21 | ignore(string(",")),
22 | skip(spaces()),
23 |
24 | TextValue.parser(),
25 |
26 | ignore(string(")"))
27 | ])
28 | end
29 |
30 | def map_command([array_variable, text]) do
31 | %UnshiftCommand{array_variable: array_variable, text: text}
32 | end
33 | end
34 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/assignments/scalar_assignment_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.ScalarAssignmentCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.ScalarAssignmentCommand
8 | alias MacroCompiler.Parser.ScalarVariable
9 | alias MacroCompiler.Parser.ScalarValue
10 |
11 | @enforce_keys [:scalar_variable, :scalar_value]
12 | defstruct [:scalar_variable, :scalar_value]
13 |
14 | parser_command do
15 | sequence([
16 | ScalarVariable.parser(),
17 |
18 | skip(spaces()),
19 | ignore(string("=")),
20 | skip(spaces()),
21 |
22 | ScalarValue.parser()
23 | ])
24 | end
25 |
26 | def map_command([scalar_variable, scalar_value]) do
27 | %ScalarAssignmentCommand{scalar_variable: scalar_variable, scalar_value: scalar_value}
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/hash_keywords/keys_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.KeysCommand do
2 | use Combine
3 |
4 | import MacroCompiler.Parser
5 |
6 | alias MacroCompiler.Parser.ArrayVariable
7 | alias MacroCompiler.Parser.HashVariable
8 | alias MacroCompiler.Parser.KeysCommand
9 |
10 | @enforce_keys [:array_variable, :param_hash_variable]
11 | defstruct [:array_variable, :param_hash_variable]
12 |
13 | parser_command do
14 | sequence([
15 | ArrayVariable.parser(),
16 |
17 | skip(spaces()),
18 | ignore(string("=")),
19 | skip(spaces()),
20 |
21 | ignore(string("&keys(")),
22 |
23 | HashVariable.parser(),
24 |
25 | ignore(string(")"))
26 | ])
27 | end
28 |
29 | def map_command([array_variable, param_hash_variable]) do
30 | %KeysCommand{array_variable: array_variable, param_hash_variable: param_hash_variable}
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/test/e2e/if_block_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.IfBlock do
2 | use MacroCompiler.Test.Helper.E2e
3 |
4 | def code, do: """
5 | macro Test {
6 | if (0) {
7 | log should not print it
8 | }
9 |
10 | if (1) {
11 | log should print it!
12 | }
13 |
14 | $value = 1
15 | if ($value) {
16 | log should can use a variable
17 | }
18 |
19 | if ($value == 1) {
20 | log should can compare a variable
21 | }
22 | }
23 | """
24 |
25 | test_output :string, "should print log at 'if' block", fn value ->
26 | value == "should print it!"
27 | end
28 |
29 | test_output :string, "should can use a variable", fn value ->
30 | value == "should can use a variable"
31 | end
32 |
33 | test_output :string, "should can compare a variable", fn value ->
34 | value == "should can compare a variable"
35 | end
36 | end
37 |
38 |
--------------------------------------------------------------------------------
/test/e2e/postfix_if_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.E2e.PostfixIf do
2 | use MacroCompiler.Test.Helper.E2e
3 |
4 | def code, do: """
5 | macro Test {
6 | log well... if not print it, then something wrong happened if (1)
7 |
8 | $value = 1000 if (1)
9 | call CallIt if ($.zeny > 0)
10 |
11 | call ShouldNotCallIt if (1000 != 1000)
12 | }
13 |
14 | macro CallIt {
15 | log value is $value
16 | }
17 |
18 | macro ShouldNotCallIt {
19 | log not call it
20 | }
21 | """
22 |
23 | test_output :string, "should print log", fn value ->
24 | value == "well... if not print it, then something wrong happened"
25 | end
26 |
27 | test_output :string, "should call macro CallIt", fn value ->
28 | value == "value is 1000"
29 | end
30 |
31 | test_output :string, "should not call macro ShouldNotCallIt", fn value ->
32 | value == ""
33 | end
34 | end
35 |
36 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/hash_keywords/values_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.ValuesCommand do
2 | use Combine
3 |
4 | import MacroCompiler.Parser
5 |
6 | alias MacroCompiler.Parser.ArrayVariable
7 | alias MacroCompiler.Parser.HashVariable
8 | alias MacroCompiler.Parser.ValuesCommand
9 |
10 | @enforce_keys [:array_variable, :param_hash_variable]
11 | defstruct [:array_variable, :param_hash_variable]
12 |
13 | parser_command do
14 | sequence([
15 | ArrayVariable.parser(),
16 |
17 | skip(spaces()),
18 | ignore(string("=")),
19 | skip(spaces()),
20 |
21 | ignore(string("&values(")),
22 |
23 | HashVariable.parser(),
24 |
25 | ignore(string(")"))
26 | ])
27 | end
28 |
29 | def map_command([array_variable, param_hash_variable]) do
30 | %ValuesCommand{array_variable: array_variable, param_hash_variable: param_hash_variable}
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/flow_control/condition.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.Condition do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.Condition
8 | alias MacroCompiler.Parser.ScalarValue
9 |
10 | @enforce_keys [:scalar_variable, :operator, :value]
11 | defstruct [:scalar_variable, :operator, :value]
12 |
13 | parser_command do
14 | sequence([
15 | ScalarValue.parser(),
16 |
17 | skip(spaces()),
18 | choice([
19 | string(">="),
20 | string(">"),
21 | string("=="),
22 | string("="),
23 | string("<="),
24 | string("<"),
25 | string("!=")
26 | ]),
27 | skip(spaces()),
28 |
29 | ScalarValue.parser()
30 | ])
31 | end
32 |
33 | def map_command([scalar_variable, operator, value]) do
34 | %Condition{scalar_variable: scalar_variable, operator: operator, value: value}
35 | end
36 | end
37 |
38 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/flow_control/if_block.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.IfBlock do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 | import MacroCompiler.Parser.Lazy
7 |
8 | alias MacroCompiler.Parser.IfBlock
9 | alias MacroCompiler.Parser.SingleCheck
10 | alias MacroCompiler.Parser.Condition
11 | alias MacroCompiler.Parser.MacroBlock
12 |
13 | @enforce_keys [:condition, :block]
14 | defstruct [:condition, :block]
15 |
16 | parser_command do
17 | sequence([
18 | ignore(string("if (")),
19 |
20 | choice([
21 | Condition.parser(),
22 | SingleCheck.parser()
23 | ]),
24 |
25 | ignore(string(")")),
26 | skip(spaces()),
27 |
28 | ignore(string("{")),
29 | skip(newline()),
30 |
31 | lazy(MacroBlock.parser()),
32 |
33 | skip(char("}"))
34 | ])
35 | end
36 |
37 | def map_command([condition, block]) do
38 | %IfBlock{condition: condition, block: block}
39 | end
40 | end
41 |
42 |
--------------------------------------------------------------------------------
/lib/semantic_analysis/validates/special_variables.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.SemanticAnalysis.Validates.SpecialVariables do
2 | alias MacroCompiler.SemanticAnalysis.SymbolsTable
3 |
4 | def validate_special_variables(%{macros: symbols_table_macros}) do
5 | special_variables_written =
6 | symbols_table_macros
7 | |> SymbolsTable.list_written_variables
8 | |> SymbolsTable.filter_special_variable
9 |
10 | special_variables_written
11 | |> Enum.reduce(%{}, fn ({name, metadata}, acc) ->
12 | case Map.fetch(acc, name) do
13 | {:ok, metadatas} ->
14 | %{acc | name => [metadata | metadatas]}
15 |
16 | :error ->
17 | Map.put(acc, name, [metadata])
18 | end
19 | end)
20 | |> Enum.map(fn ({variable_name, metadatas}) ->
21 | %{
22 | type: :error,
23 | metadatas: metadatas,
24 | message: [:red, variable_name, :default_color, " is a special variable, reassigning is not allowed"]
25 | }
26 | end)
27 | end
28 | end
29 |
30 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/assignments/array_assignment_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.ArrayAssignmentCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.ArrayAssignmentCommand
8 | alias MacroCompiler.Parser.ArrayVariable
9 | alias MacroCompiler.Parser.TextValue
10 |
11 | @enforce_keys [:array_variable, :texts]
12 | defstruct [:array_variable, :texts]
13 |
14 | parser_command do
15 | sequence([
16 | ArrayVariable.parser(),
17 |
18 | skip(spaces()),
19 | ignore(string("=")),
20 | skip(spaces()),
21 |
22 | ignore(char("(")),
23 |
24 | sep_by(
25 | TextValue.parser(),
26 | sequence([
27 | char(","),
28 | skip(spaces())
29 | ])
30 | ),
31 |
32 | ignore(char(")"))
33 | ])
34 | end
35 |
36 | def map_command([scalar_variable, texts]) do
37 | %ArrayAssignmentCommand{array_variable: scalar_variable, texts: texts}
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/scalar_variable.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.ScalarVariable do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.ScalarVariable
8 | alias MacroCompiler.Parser.Identifier
9 | alias MacroCompiler.Parser.ScalarValue
10 |
11 | @enforce_keys [:name, :array_position, :hash_position]
12 | defstruct [:name, :array_position, :hash_position]
13 |
14 | parser_command do
15 | sequence([
16 | ignore(string("$")),
17 | Identifier.parser(),
18 |
19 | option(
20 | between(
21 | char("["),
22 | ScalarValue.parser(),
23 | char("]")
24 | )
25 | ),
26 | option(
27 | between(
28 | char("{"),
29 | Identifier.parser(),
30 | char("}")
31 | )
32 | )
33 | ])
34 | end
35 |
36 | def map_command([name, array_position, hash_position]) do
37 | %ScalarVariable{name: name, array_position: array_position, hash_position: hash_position}
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/semantic_analysis/validates/macros.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.SemanticAnalysis.Validates.Macros do
2 | alias MacroCompiler.SemanticAnalysis.SymbolsTable
3 |
4 | def validate_macros(%{macros: symbols_table_macros}) do
5 | macros_read =
6 | symbols_table_macros
7 | |> SymbolsTable.list_read_macros
8 |
9 | macros_write =
10 | symbols_table_macros
11 | |> SymbolsTable.list_written_macros
12 |
13 | macros_read
14 | |> Enum.reject(fn {macro, _metadata} -> Enum.member?(macros_write, macro.name) end)
15 | |> Enum.reduce(%{}, fn({macro, metadata}, acc) ->
16 | case Map.fetch(acc, macro.name) do
17 | {:ok, metadatas} ->
18 | %{acc | macro.name => [metadata | metadatas]}
19 |
20 | :error ->
21 | Map.put(acc, macro.name, [metadata])
22 | end
23 | end)
24 | |> Enum.map(fn({macro_name, metadatas}) -> %{
25 | type: :error,
26 | metadatas: metadatas,
27 | message: ["macro ", :red, macro_name, :default_color, " is called but it has never been written."]
28 | } end)
29 | end
30 | end
31 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/commands/call_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.CallCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.CallCommand
8 | alias MacroCompiler.Parser.Identifier
9 |
10 | @enforce_keys [:macro, :params]
11 | defstruct [:macro, :params]
12 |
13 | parser_command do
14 | sequence([
15 | ignore(string("call")),
16 | ignore(spaces()),
17 |
18 | Identifier.parser(),
19 |
20 | either(
21 | ignore(char(?\n)),
22 |
23 | many(
24 | between(
25 | sequence([
26 | spaces(),
27 | char(?")
28 | ]),
29 |
30 | take_while(fn ?" -> false; _ -> true end),
31 |
32 | char(?")
33 | )
34 | )
35 | )
36 | ])
37 | end
38 |
39 | def map_command([macro]) do
40 | %CallCommand{macro: macro, params: []}
41 | end
42 |
43 | def map_command([macro, params]) do
44 | %CallCommand{macro: macro, params: params |> Enum.map(&List.to_string/1)}
45 | end
46 | end
47 |
--------------------------------------------------------------------------------
/lib/semantic_analysis/latest_variable_writes.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.SemanticAnalysis.LatestVariableWrites do
2 | alias MacroCompiler.Parser.ScalarAssignmentCommand
3 | alias MacroCompiler.Parser.ScalarVariable
4 | alias MacroCompiler.Parser.TextValue
5 |
6 |
7 | def build(block) do
8 | Enum.map(block, &list_variables_assignments/1)
9 | |> Enum.reject(&is_nil/1)
10 | |> Enum.reduce(%{}, fn (variavel, acc) ->
11 | Map.put(acc, variavel.name, variavel.determinist)
12 | end)
13 | end
14 |
15 | defp list_variables_assignments({
16 | %ScalarAssignmentCommand{
17 | scalar_variable: {%ScalarVariable{name: name, array_position: nil, hash_position: nil}, _},
18 | scalar_value: scalar_value
19 | },
20 | _metadata
21 | }) do
22 | %{
23 | name: name,
24 | determinist: determinist_value(scalar_value)
25 | }
26 | end
27 |
28 | defp list_variables_assignments(_node) do
29 |
30 | end
31 |
32 | defp determinist_value(%TextValue{} = node) do
33 | node
34 | end
35 |
36 | defp determinist_value(_node) do
37 | :is_not_determinist
38 | end
39 | end
40 |
--------------------------------------------------------------------------------
/lib/optimization/optimization.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Optimization do
2 | alias MacroCompiler.Optimization.DeadCodeStrip
3 | alias MacroCompiler.Optimization.ConstantFolding
4 | alias MacroCompiler.SemanticAnalysis
5 |
6 | # Run all optimizations
7 | def build_ast_optimized(ast) do
8 | build_ast_optimized(ast, [DeadCodeStrip, ConstantFolding])
9 | end
10 |
11 | # Run a couple of optimizations
12 | def build_ast_optimized(ast, opts) when is_list(opts) do
13 | symbols_table = SemanticAnalysis.build_symbols_table(ast)
14 |
15 | optimized_ast =
16 | Enum.reduce(opts, ast, fn (opt, current_ast) ->
17 | opt.optimize(current_ast, symbols_table)
18 | end)
19 |
20 | if optimized_ast == ast do
21 | optimized_ast
22 | else
23 | build_ast_optimized(optimized_ast, opts)
24 | end
25 | end
26 |
27 | # Run a single optimization
28 | def build_ast_optimized(ast, opt) when is_atom(opt) do
29 | symbols_table = SemanticAnalysis.build_symbols_table(ast)
30 |
31 | optimized_ast =
32 | opt.optimize(ast, symbols_table)
33 |
34 | if optimized_ast == ast do
35 | optimized_ast
36 | else
37 | build_ast_optimized(optimized_ast, opt)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | use Mix.Config
4 |
5 | # This configuration is loaded before any dependency and is restricted
6 | # to this project. If another project depends on this project, this
7 | # file won't be loaded nor affect the parent project. For this reason,
8 | # if you want to provide default values for your application for
9 | # 3rd-party users, it should be done in your "mix.exs" file.
10 |
11 | # You can configure your application as:
12 | #
13 | # config :macrocompiler, key: :value
14 | #
15 | # and access this configuration in your application as:
16 | #
17 | # Application.get_env(:macrocompiler, :key)
18 | #
19 | # You can also configure a 3rd-party app:
20 | #
21 | # config :logger, level: :info
22 | #
23 |
24 | # It is also possible to import configuration files, relative to this
25 | # directory. For example, you can emulate configuration per environment
26 | # by uncommenting the line below and defining dev.exs, test.exs and such.
27 | # Configuration from the imported file will override the ones defined
28 | # here (which is why it is important to import them last).
29 | #
30 | # import_config "#{Mix.env}.exs"
31 |
--------------------------------------------------------------------------------
/test/functional/optimization/dead_code_strip_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.Functional.DeadCodeStrip do
2 | use MacroCompiler.Test.Helper.Optimization
3 |
4 | @optimization MacroCompiler.Optimization.DeadCodeStrip
5 |
6 | test_equivalents_ast(
7 | "Should strip unnecessary variable declaration",
8 | """
9 | macro Test {
10 | $foo = 1
11 | @bar = (prontera, geffen, morroc)
12 | %baz = (1 => 2, 3 => 4)
13 | }
14 | """,
15 | """
16 | macro Test {
17 | }
18 | """
19 | )
20 |
21 | test_equivalents_ast(
22 | "Should not strip useful variable declaration",
23 | """
24 | macro Test {
25 | $useful = 1
26 | log $useful
27 | }
28 | """,
29 | """
30 | macro Test {
31 | $useful = 1
32 | log $useful
33 | }
34 | """
35 | )
36 |
37 | test_different_ast(
38 | "Should keep variable assignment if it is read in an 'if' block",
39 | """
40 | macro Test {
41 | $foo = 1
42 |
43 | if (1) {
44 | log $foo
45 | }
46 | }
47 | """,
48 | """
49 | macro Test {
50 | if (1) {
51 | log $foo
52 | }
53 | }
54 | """
55 | )
56 | end
57 |
58 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/variables/assignments/hash_assignment_command.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.HashAssignmentCommand do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | import MacroCompiler.Parser
6 |
7 | alias MacroCompiler.Parser.HashAssignmentCommand
8 | alias MacroCompiler.Parser.HashVariable
9 | alias MacroCompiler.Parser.Identifier
10 | alias MacroCompiler.Parser.TextValue
11 |
12 | @enforce_keys [:hash_variable, :keystexts]
13 | defstruct [:hash_variable, :keystexts]
14 |
15 | parser_command do
16 | sequence([
17 | HashVariable.parser(),
18 |
19 | skip(spaces()),
20 | ignore(string("=")),
21 | skip(spaces()),
22 |
23 | ignore(char("(")),
24 |
25 | sep_by(
26 | sequence([
27 | Identifier.parser(),
28 | ignore(spaces()),
29 | ignore(string("=>")),
30 | ignore(spaces()),
31 | TextValue.parser()
32 | ]),
33 |
34 | sequence([
35 | char(","),
36 | skip(spaces())
37 | ])
38 | ),
39 |
40 | ignore(char(")"))
41 | ])
42 | end
43 |
44 | def map_command([hash_variable, keystexts]) do
45 | %HashAssignmentCommand{hash_variable: hash_variable, keystexts: keystexts}
46 | end
47 | end
48 |
--------------------------------------------------------------------------------
/test/helpers/optimization_helper.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.Helper.Optimization do
2 | alias MacroCompiler.Parser.TopLevelBlock
3 | alias MacroCompiler.Optimization
4 |
5 | defmacro __using__(_opts) do
6 | quote do
7 | use ExUnit.Case, async: true
8 | import MacroCompiler.Test.Helper.Optimization
9 | end
10 | end
11 |
12 | def build_optimized_ast(code, optimization) do
13 | [ast] = Combine.parse(code, TopLevelBlock.parser())
14 |
15 | Optimization.build_ast_optimized(ast, optimization)
16 | end
17 |
18 | defmacro test_equivalents_ast(description, code_a, code_b) do
19 | quote do
20 | test unquote(description) do
21 | Process.put(:no_metadata, true)
22 | Process.put(:no_keep_ignored_node, true)
23 |
24 | ast_a = build_optimized_ast(unquote(code_a), @optimization)
25 | ast_b = build_optimized_ast(unquote(code_b), @optimization)
26 |
27 | Process.put(:no_metadata, nil)
28 | Process.put(:no_keep_ignored_node, nil)
29 |
30 | assert ast_a == ast_b
31 | end
32 | end
33 | end
34 |
35 | defmacro test_different_ast(description, code_a, code_b) do
36 | quote do
37 | test unquote(description) do
38 | Process.put(:no_metadata, true)
39 | Process.put(:no_keep_ignored_node, true)
40 |
41 | ast_a = build_optimized_ast(unquote(code_a), @optimization)
42 | ast_b = build_optimized_ast(unquote(code_b), @optimization)
43 |
44 | Process.put(:no_metadata, nil)
45 | Process.put(:no_keep_ignored_node, nil)
46 |
47 | assert ast_a != ast_b
48 | end
49 | end
50 | end
51 | end
52 |
53 |
--------------------------------------------------------------------------------
/lib/macrocompiler.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler do
2 | use Combine
3 |
4 | alias MacroCompiler.Parser.TopLevelBlock
5 | alias MacroCompiler.Parser.SyntaxError
6 |
7 | alias MacroCompiler.SemanticAnalysis
8 | alias MacroCompiler.SemanticAnalysis.FatalError, as: FatalSemanticError
9 |
10 | alias MacroCompiler.Error
11 |
12 | alias MacroCompiler.Optimization
13 |
14 | alias MacroCompiler.CodeGeneration.Header, as: CodeGenerationHeader
15 | alias MacroCompiler.CodeGeneration.Body, as: CodeGenerationBody
16 | alias MacroCompiler.CodeGeneration.Footer, as: CodeGenerationFooter
17 |
18 | def compiler(macro_file) do
19 | file = File.read!(macro_file)
20 |
21 | try do
22 | [ast] = Combine.parse(file, TopLevelBlock.parser())
23 |
24 | symbols_table = SemanticAnalysis.build_symbols_table(ast)
25 | validates_result = SemanticAnalysis.run_validates(symbols_table)
26 | Error.show(file, validates_result)
27 | Error.raise_fatal_error(validates_result)
28 |
29 | optimized_ast = Optimization.build_ast_optimized(ast)
30 |
31 | []
32 | |> Enum.concat(CodeGenerationHeader.generate(optimized_ast, symbols_table))
33 | |> Enum.concat(CodeGenerationBody.start_generate(optimized_ast))
34 | |> Enum.concat(CodeGenerationFooter.generate())
35 |
36 | rescue
37 | e in SyntaxError ->
38 | Error.show(file, e)
39 |
40 | e in FatalSemanticError ->
41 | IO.puts e.message
42 | end
43 | end
44 |
45 | def print_result(generated_code) do
46 | generated_code
47 | |> Enum.each(&IO.puts/1)
48 | end
49 | end
50 |
51 |
52 | case System.argv do
53 | [] -> MacroCompiler.compiler("macro.txt") |> MacroCompiler.print_result
54 | ["test"] -> nil
55 | ["test", _] -> nil
56 | [macro_file] -> MacroCompiler.compiler(macro_file) |> MacroCompiler.print_result
57 | end
58 |
--------------------------------------------------------------------------------
/lib/parsers/text_value.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.TextValue do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | alias MacroCompiler.Parser.TextValue
6 | alias MacroCompiler.Parser.ScalarVariable
7 | alias MacroCompiler.Parser.ArrayVariable
8 | alias MacroCompiler.Parser.HashVariable
9 |
10 | @enforce_keys [:values]
11 | defstruct [:values]
12 |
13 | defp get_char(limited) do
14 | if limited do
15 | satisfy(
16 | char(),
17 | fn
18 | "\n" -> false;
19 | "#" -> false;
20 | "," -> false;
21 | "(" -> false;
22 | ")" -> false;
23 | "]" -> false;
24 | "{" -> false;
25 | "}" -> false;
26 | "+" -> false;
27 | "-" -> false;
28 | " " -> false;
29 |
30 | _ -> true
31 | end
32 | )
33 | else
34 | satisfy(
35 | char(),
36 | fn
37 | "\n" -> false;
38 | "#" -> false;
39 |
40 | _ -> true
41 | end
42 | )
43 | end
44 | end
45 |
46 | def if_is_not_postfix_if(block) do
47 | if_not(
48 | sequence([
49 | spaces(),
50 | string("if"),
51 | spaces(),
52 | char(?()
53 | ]),
54 | block
55 | )
56 | end
57 |
58 | def parser(limited \\ true) do
59 | map(
60 | many(
61 | choice([
62 | map(string("\\,"), fn _ -> "," end),
63 | string("\\$"),
64 | string("\\@"),
65 | string("\\%"),
66 | string("\\#"),
67 | ScalarVariable.parser(),
68 | ArrayVariable.parser(),
69 | HashVariable.parser(),
70 | if_is_not_postfix_if(
71 | get_char(limited)
72 | )
73 | ])
74 | ),
75 | fn values -> %TextValue{values: values} end
76 | )
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/semantic_analysis/validates/variables.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.SemanticAnalysis.Validates.Variables do
2 | alias MacroCompiler.SemanticAnalysis.SymbolsTable
3 |
4 | def validate_variables(%{macros: symbols_table_macros}) do
5 | variables_read =
6 | symbols_table_macros
7 | |> SymbolsTable.list_read_variables
8 | |> SymbolsTable.reject_special_variable
9 |
10 | variables_read_names =
11 | variables_read
12 | |> Enum.map(fn {name, _} -> name end)
13 |
14 |
15 | variables_write =
16 | symbols_table_macros
17 | |> SymbolsTable.list_written_variables
18 | |> SymbolsTable.reject_special_variable
19 |
20 | variables_write_names =
21 | variables_write
22 | |> Enum.map(fn {name, _} -> name end)
23 |
24 |
25 | messages_variables_read =
26 | variables_read
27 | |> Enum.reject(fn {name, _} -> Enum.member?(variables_write_names, name) end)
28 | |> Enum.reduce(%{}, fn({name, metadata}, acc) ->
29 | case Map.fetch(acc, name) do
30 | {:ok, metadatas} ->
31 | %{acc | name => [metadata | metadatas]}
32 |
33 | :error ->
34 | Map.put(acc, name, [metadata])
35 | end
36 | end)
37 | |> Enum.map(fn({variable_name, metadatas}) -> %{
38 | type: :error,
39 | metadatas: metadatas,
40 | message: ["variable ", :red, variable_name, :default_color, " is read but it has never been written."]
41 | } end)
42 |
43 | messages_variables_write =
44 | variables_write
45 | |> Enum.reject(fn {name, _} -> Enum.member?(variables_read_names, name) end)
46 | |> Enum.reduce(%{}, fn({name, metadata}, acc) ->
47 | case Map.fetch(acc, name) do
48 | {:ok, metadatas} ->
49 | %{acc | name => [metadata | metadatas]}
50 |
51 | :error ->
52 | Map.put(acc, name, [metadata])
53 | end
54 | end)
55 | |> Enum.map(fn({variable_name, metadatas}) -> %{
56 | type: :warning,
57 | metadatas: metadatas,
58 | message: ["variable ", :red, variable_name, :default_color, " is write but it has never read."]
59 | } end)
60 |
61 | [messages_variables_read, messages_variables_write]
62 | end
63 | end
64 |
--------------------------------------------------------------------------------
/test/helpers/semantic_analysis.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.Helper.SemanticAnalysis do
2 | alias MacroCompiler.Parser.TopLevelBlock
3 | alias MacroCompiler.SemanticAnalysis
4 |
5 | alias MacroCompiler.Error
6 | alias MacroCompiler.SemanticAnalysis.FatalError, as: FatalSemanticError
7 |
8 | defmacro __using__(_opts) do
9 | quote do
10 | use ExUnit.Case, async: true
11 | import MacroCompiler.Test.Helper.SemanticAnalysis
12 | end
13 | end
14 |
15 | def get_validates_result(code) do
16 | [ast] = Combine.parse(code, TopLevelBlock.parser())
17 |
18 | symbols_table = SemanticAnalysis.build_symbols_table(ast)
19 | SemanticAnalysis.run_validates(symbols_table)
20 | end
21 |
22 | defmacro test_should_works(description, code) do
23 | quote do
24 | test unquote(description) do
25 | validates_result = get_validates_result(unquote(code))
26 |
27 | assert length(validates_result) == 0
28 | Error.raise_fatal_error(validates_result)
29 | end
30 | end
31 | end
32 |
33 | defmacro test_semantic_warning(description, code, compare_list) do
34 | quote do
35 | test unquote(description) do
36 | validates_result = get_validates_result(unquote(code))
37 |
38 | List.zip([validates_result, unquote(compare_list)])
39 | |> Enum.each(fn {validate_result, [message: message, metadatas: metadatas]} ->
40 | assert validate_result.message == message
41 | assert validate_result.metadatas == metadatas
42 | assert validate_result.type == :warning
43 | end)
44 | end
45 | end
46 | end
47 |
48 | defmacro test_semantic_error(description, code, compare_list) do
49 | quote do
50 | test unquote(description) do
51 | validates_result = get_validates_result(unquote(code))
52 |
53 | List.zip([validates_result, unquote(compare_list)])
54 | |> Enum.each(fn {validate_result, [message: message, metadatas: metadatas]} ->
55 | assert validate_result.message == message
56 | assert validate_result.metadatas == metadatas
57 | assert validate_result.type == :error
58 | end)
59 |
60 | assert_raise FatalSemanticError, fn ->
61 | Error.raise_fatal_error(validates_result)
62 | end
63 | end
64 | end
65 | end
66 | end
67 |
68 |
--------------------------------------------------------------------------------
/test/helpers/e2e_helper.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.Helper.E2e do
2 | def compiler_and_run_macro(macro_name, event_macro_code, run_test_macro, run_cli_command) do
3 | File.write!("test/e2e/perl/codes/#{macro_name}.txt", event_macro_code)
4 |
5 | perl_code =
6 | MacroCompiler.compiler("test/e2e/perl/codes/#{macro_name}.txt")
7 | |> Enum.join("\n")
8 | File.write!("test/e2e/perl/codes/#{macro_name}.pl", perl_code)
9 |
10 | run_test_macro_perl_bool = case run_test_macro do
11 | true -> "1"
12 | false -> "0"
13 | end
14 |
15 | {output, 0} = System.cmd("perl", ["runner.pl", macro_name, run_test_macro_perl_bool, run_cli_command], cd: "test/e2e/perl/")
16 |
17 | output
18 | |> String.split("\n")
19 | end
20 |
21 | defmacro __using__(opts) do
22 | options = case opts do
23 | {_, _, keyword_list} ->
24 | Enum.into(keyword_list, %{})
25 | _ ->
26 | %{}
27 | end
28 |
29 | run_test_macro = case options do
30 | %{run_test_macro: value} -> value
31 | _ -> true
32 | end
33 |
34 | run_cli_command = case options do
35 | %{run_cli_command: value} -> value
36 | _ -> ""
37 | end
38 |
39 | quote do
40 | use ExUnit.Case, async: true
41 | import MacroCompiler.Test.Helper.E2e
42 |
43 | @output_index 0
44 |
45 | setup_all do
46 | file_name = __MODULE__ |> to_string() |> String.split(".") |> List.last
47 | perl_outputs =
48 | MacroCompiler.Test.Helper.E2e.compiler_and_run_macro(file_name, code(), unquote(run_test_macro), unquote(run_cli_command))
49 | {:ok, %{perl_outputs: perl_outputs}}
50 | end
51 | end
52 | end
53 |
54 | defmacro test_output(type, desc, assertion) do
55 | quote do
56 | test unquote(desc), %{perl_outputs: perl_outputs} do
57 | type = unquote(type)
58 | assertion = unquote(assertion)
59 |
60 | output = Enum.at(perl_outputs, @output_index)
61 |
62 | output_casted =
63 | case type do
64 | :string ->
65 | output
66 | :integer ->
67 | {value, _} = Integer.parse(output)
68 | value
69 | end
70 |
71 | assert assertion.(output_casted)
72 | end
73 |
74 | @output_index @output_index + 1
75 | end
76 | end
77 | end
78 |
79 |
--------------------------------------------------------------------------------
/test/functional/semantic_analysis/variables_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.Functional.SemanticAnalysis.Variables do
2 | use MacroCompiler.Test.Helper.SemanticAnalysis
3 |
4 | test_should_works(
5 | "should can read variables if it was written",
6 | """
7 | macro Test {
8 | $scalar = value
9 | log $scalar
10 |
11 | @array = ($scalar)
12 | log @array
13 |
14 | %hash = (key => $scalar)
15 | log %hash
16 | }
17 | """
18 | )
19 |
20 | test_should_works(
21 | "should can read variables still that it was written at another macro",
22 | """
23 | macro Test {
24 | log $scalar
25 | }
26 |
27 | macro SetValue {
28 | $scalar = value
29 | }
30 | """
31 | )
32 |
33 | test_semantic_warning(
34 | "should warning when write a variable that was never read",
35 | """
36 | macro Test {
37 | $scalar = value
38 | @array = ()
39 | %hash = ()
40 | }
41 | """,
42 | [
43 | [
44 | message: ["variable ", :red, "$scalar", :default_color, " is write but it has never read."],
45 | metadatas: [%MacroCompiler.Parser.Metadata{ignore: nil, line: 2, offset: 4}]
46 | ],
47 | [
48 | message: ["variable ", :red, "%hash", :default_color, " is write but it has never read."],
49 | metadatas: [%MacroCompiler.Parser.Metadata{ignore: nil, line: 2, offset: 40}]
50 | ],
51 | [
52 | message: ["variable ", :red, "@array", :default_color, " is write but it has never read."],
53 | metadatas: [%MacroCompiler.Parser.Metadata{ignore: nil, line: 2, offset: 24}]
54 | ]
55 | ]
56 | )
57 |
58 | test_semantic_error(
59 | "should fail when try to read a variable that has never been written",
60 | """
61 | macro Test {
62 | log $scalar
63 | log @array if (1)
64 | }
65 | """,
66 | [
67 | [
68 | message: ["variable ", :red, "$scalar", :default_color, " is read but it has never been written."],
69 | metadatas: [%MacroCompiler.Parser.Metadata{ignore: nil, line: 2, offset: 8}]
70 | ],
71 | [
72 | message: ["variable ", :red, "@array", :default_color, " is read but it has never been written."],
73 | metadatas: [%MacroCompiler.Parser.Metadata{ignore: nil, line: 2, offset: 24}]
74 | ]
75 | ]
76 | )
77 | end
78 |
79 |
--------------------------------------------------------------------------------
/test/functional/optimization/constant_folding_test.exs:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Test.Functional.ConstantFolding do
2 | use MacroCompiler.Test.Helper.Optimization
3 |
4 | @optimization MacroCompiler.Optimization.ConstantFolding
5 |
6 | test_equivalents_ast(
7 | "Should propagate constant scalar value",
8 | """
9 | macro Test {
10 | $foo = 1
11 | log $foo
12 | }
13 | """,
14 | """
15 | macro Test {
16 | $foo = 1
17 | log 1
18 | }
19 | """
20 | )
21 |
22 | test_equivalents_ast(
23 | "Should propagate constant scalar value set in macro called",
24 | """
25 | macro Test {
26 | call SetVars
27 | log $foo $bar $baz
28 | }
29 |
30 | macro SetVars {
31 | $foo = 1
32 | $bar = 2
33 | $baz = 3
34 | }
35 | """,
36 | """
37 | macro Test {
38 | call SetVars
39 | log 1 2 3
40 | }
41 |
42 | macro SetVars {
43 | $foo = 1
44 | $bar = 2
45 | $baz = 3
46 | }
47 | """
48 | )
49 |
50 | test_equivalents_ast(
51 | "Should propagate the constant value still it was written outside of 'if' block",
52 | """
53 | macro Test {
54 | $foo = 1
55 |
56 | if ($.zeny > 500) {
57 | log $foo
58 | }
59 | }
60 | """,
61 | """
62 | macro Test {
63 | $foo = 1
64 |
65 | if ($.zeny > 500) {
66 | log 1
67 | }
68 | }
69 | """
70 | )
71 |
72 | test_different_ast(
73 | "Should keep variable reference if it was written in an 'if' block",
74 | """
75 | macro Test {
76 | $foo = 1
77 | $foo = 2 if ($.zeny > 500)
78 | log $foo
79 | }
80 | """,
81 | """
82 | macro Test {
83 | $foo = 1
84 | $foo = 2 if ($.zeny > 500)
85 | log 1
86 | }
87 | """
88 | )
89 |
90 | test_different_ast(
91 | "Should not propagate the constant value if the variable was written inside of 'if' block",
92 | """
93 | macro Test {
94 | $foo = 1
95 |
96 | if ($.zeny > 500) {
97 | $bar = 22
98 | }
99 |
100 | log $baz
101 | }
102 | """,
103 | """
104 | macro Test {
105 | $foo = 1
106 |
107 | if ($.zeny > 500) {
108 | $bar = 22
109 | }
110 |
111 | log 22
112 | }
113 | """
114 | )
115 | end
116 |
117 |
--------------------------------------------------------------------------------
/lib/parsers/macro_block/macro_block.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Parser.MacroBlock do
2 | use Combine
3 | use Combine.Helpers
4 |
5 | alias MacroCompiler.Parser.SyntaxError
6 |
7 | alias MacroCompiler.Parser.DoCommand
8 | alias MacroCompiler.Parser.LogCommand
9 | alias MacroCompiler.Parser.CallCommand
10 | alias MacroCompiler.Parser.UndefCommand
11 | alias MacroCompiler.Parser.ScalarAssignmentCommand
12 | alias MacroCompiler.Parser.ArrayAssignmentCommand
13 | alias MacroCompiler.Parser.HashAssignmentCommand
14 | alias MacroCompiler.Parser.IncrementCommand
15 | alias MacroCompiler.Parser.DecrementCommand
16 | alias MacroCompiler.Parser.PauseCommand
17 | alias MacroCompiler.Parser.PushCommand
18 | alias MacroCompiler.Parser.PopCommand
19 | alias MacroCompiler.Parser.ShiftCommand
20 | alias MacroCompiler.Parser.UnshiftCommand
21 | alias MacroCompiler.Parser.Comment
22 | alias MacroCompiler.Parser.DeleteCommand
23 | alias MacroCompiler.Parser.KeysCommand
24 | alias MacroCompiler.Parser.ValuesCommand
25 | alias MacroCompiler.Parser.BlankSpaces
26 | alias MacroCompiler.Parser.PostfixIf
27 | alias MacroCompiler.Parser.IfBlock
28 |
29 | def parser() do
30 | many(
31 | map(
32 | sequence([
33 | skip(BlankSpaces.parser()),
34 | choice([
35 | ignore(Comment.parser()),
36 |
37 | DoCommand.parser(),
38 | LogCommand.parser(),
39 | CallCommand.parser(),
40 | UndefCommand.parser(),
41 | ScalarAssignmentCommand.parser(),
42 | ArrayAssignmentCommand.parser(),
43 | HashAssignmentCommand.parser(),
44 | IncrementCommand.parser(),
45 | DecrementCommand.parser(),
46 | PauseCommand.parser(),
47 | PushCommand.parser(),
48 | PopCommand.parser(),
49 | ShiftCommand.parser(),
50 | UnshiftCommand.parser(),
51 | DeleteCommand.parser(),
52 | KeysCommand.parser(),
53 | ValuesCommand.parser(),
54 | IfBlock.parser(),
55 |
56 | # If we could not understand the command in this line, and it's not a close-braces,
57 | # then it's a syntax error
58 | if_not(char(?}), SyntaxError.raiseAtPosition()),
59 | ]),
60 | option(PostfixIf.parser()),
61 | skip(BlankSpaces.parser())
62 | ]),
63 |
64 | fn
65 | [node, nil] -> node
66 | [node_command, {node_postfix, postfix_metadata}] -> {%{node_postfix | block: [node_command]}, postfix_metadata}
67 | [] -> nil
68 | [nil] -> nil
69 | end)
70 | )
71 | end
72 | end
73 |
--------------------------------------------------------------------------------
/lib/semantic_analysis/symbols_table.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.SemanticAnalysis.SymbolsTable do
2 | def list_written_macros(symbols_table) do
3 | symbols_table
4 | |> get_in([Access.all(), Access.key(:macro_write), Access.key(:name)])
5 | end
6 |
7 | def list_read_macros(symbols_table) do
8 | macros_block = get_macros_block(symbols_table)
9 |
10 | listMacro(:macro_read, macros_block)
11 | |> filter_nil
12 | end
13 |
14 | def list_written_variables(symbols_table) do
15 | macros_block = get_macros_block(symbols_table)
16 |
17 | list(:variable_write, macros_block)
18 | |> filter_nil
19 | end
20 |
21 | def list_read_variables(symbols_table) do
22 | macros_block = get_macros_block(symbols_table)
23 |
24 | list(:variable_read, macros_block)
25 | |> filter_nil
26 | end
27 |
28 | def list_special_variables(symbols_table) do
29 | read_variables = list_read_variables(symbols_table)
30 |
31 | read_variables
32 | |> Enum.map(fn {name, _metadata} -> name end)
33 | |> Enum.filter(&is_special_variable/1)
34 | |> MapSet.new
35 | end
36 |
37 | # Private functions
38 |
39 | defp get_macros_block(symbols_table) do
40 | symbols_table
41 | |> get_in([Access.all(), Access.key(:macro_write), Access.key(:block)])
42 | |> filter_nil
43 | end
44 |
45 | # TODO: This function needs to be refactored
46 | defp listMacro(operation, symbols_table, acc \\ []) do
47 | m =
48 | symbols_table
49 | |> get_in([Access.all(), Access.key(operation)])
50 | |> filter_nil
51 |
52 | a =
53 | symbols_table
54 | |> get_in([Access.all(), Access.key(:variable_read)])
55 | |> filter_nil
56 |
57 | if (length(a) > 0) do
58 | acc = [acc | m]
59 | [acc | listMacro(operation, a, acc)]
60 | else
61 | [acc | m]
62 | end
63 | end
64 |
65 | # TODO: This function needs to be refactored
66 | defp list(operation, symbols_table, acc \\ []) do
67 | occurrences =
68 | symbols_table
69 | |> get_in([Access.all(), Access.key(operation)])
70 | |> filter_nil
71 |
72 | a =
73 | occurrences
74 | |> get_in([Access.all(), Access.key(:variable_name)])
75 |
76 | if (length(a) > 0) do
77 | acc = [acc | a]
78 | list(operation, occurrences, acc)
79 | else
80 | acc
81 | end
82 | end
83 |
84 | defp is_special_variable(variable_name) do
85 | String.slice(variable_name, 1..1) == "."
86 | end
87 |
88 | def filter_special_variable(variable_list) do
89 | variable_list
90 | |> Enum.filter(fn {name, _metadata} -> is_special_variable(name) end)
91 | end
92 |
93 | def reject_special_variable(variable_list) do
94 | variable_list
95 | |> Enum.reject(fn {name, _metadata} -> is_special_variable(name) end)
96 | end
97 |
98 | defp filter_nil(list) do
99 | list
100 | |> List.flatten
101 | |> Enum.reject(&is_nil/1)
102 | end
103 | end
104 |
105 |
--------------------------------------------------------------------------------
/lib/error/error.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Error do
2 | import MacroCompiler.Error.Utils
3 |
4 | alias MacroCompiler.Parser.SyntaxError
5 | alias MacroCompiler.SemanticAnalysis.FatalError, as: FatalSemanticError
6 |
7 | defp puts_stderr(message), do: IO.puts(:stderr, message)
8 |
9 | ###
10 | # Compiler-time error message
11 | def show(file, %SyntaxError{message: message, line: line, offset: offset}) do
12 | {line, col} = calc_line_and_column(file, line, offset)
13 |
14 | puts_stderr IO.ANSI.format([:red, :bright, "#{message}\n"])
15 |
16 | file
17 | |> String.split("\n")
18 | |> Enum.with_index(1)
19 | |> Enum.filter(fn {_, index} ->
20 | index >= line - 2 and index <= line + 2
21 | end)
22 | |> Enum.map(fn {lineText, index} ->
23 | if index == line do
24 | lineTextSliced0 = String.slice(lineText, 0..(col-1))
25 | lineTextSliced1 = String.slice(lineText, col..String.length(lineText))
26 |
27 | IO.ANSI.format([:bright, "#{index} - ", lineTextSliced0, :red, lineTextSliced1], true)
28 | else
29 | "#{index} - #{lineText}"
30 | end
31 | end)
32 | |> Enum.each(&puts_stderr/1)
33 |
34 | puts_stderr "\n\nMacro couldn't be compiled. Sorry"
35 | end
36 |
37 | ###
38 | # Semantic analysis error message
39 | def show(file, validates_result) do
40 | validates_result
41 | |> sort_validates_result
42 | |> Enum.map(fn %{type: type, message: message, metadatas: metadatas} ->
43 | format_message(file, type, message, metadatas)
44 | |> IO.ANSI.format
45 | end)
46 | |> Enum.each(&puts_stderr/1)
47 | end
48 |
49 | defp sort_validates_result(validates_result) do
50 | priorities = %{error: 0, warning: 1}
51 |
52 | Enum.sort(validates_result, fn (%{type: type_a}, %{type: type_b}) ->
53 | Map.get(priorities, type_a) < Map.get(priorities, type_b)
54 | end)
55 | end
56 |
57 | defp format_message(file, type, message, metadatas) do
58 | prefix = case type do
59 | :warning ->
60 | IO.ANSI.format([:yellow, :bright, "Warning: "])
61 |
62 | :error ->
63 | IO.ANSI.format([:yellow, :bright, "FATAL ERROR: "])
64 | end
65 |
66 | [prefix | message] ++ " #{metadates_to_line_column_message(file, metadatas)}"
67 | end
68 |
69 | def raise_fatal_error(validates_result) do
70 | if has_fatal_error?(validates_result) do
71 | raise FatalSemanticError, message: "Could not be compiled because some fatal error happened"
72 | end
73 | end
74 |
75 | defp has_fatal_error?(validates_result) do
76 | Enum.any?(validates_result, fn %{type: type} ->
77 | type == :error
78 | end)
79 | end
80 |
81 | defp metadates_to_line_column_message(file, metadatas) do
82 | occurrences =
83 | metadatas
84 | |> Enum.map(&calc_line_and_column(file, &1))
85 | |> Enum.reverse
86 |
87 | occurrences_text =
88 | occurrences
89 | |> Enum.map(fn {line, column} -> "#{line}:#{column}" end)
90 | |> Enum.join(" and ")
91 |
92 | "It's happened at #{occurrences_text}"
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/lib/optimization/dead_code_strip/dead_code_strip.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Optimization.DeadCodeStrip do
2 | alias MacroCompiler.Parser.ScalarVariable
3 | alias MacroCompiler.Parser.ArrayVariable
4 | alias MacroCompiler.Parser.HashVariable
5 |
6 | alias MacroCompiler.SemanticAnalysis.SymbolsTable
7 |
8 | @ignore_node true
9 | @keep_node false
10 |
11 | def optimize(ast, %{macros: symbols_table_macros}) do
12 | variables_read =
13 | symbols_table_macros
14 | |> SymbolsTable.list_read_variables
15 | |> Enum.map(fn {name, _} -> name end)
16 | |> MapSet.new
17 |
18 | variables_written =
19 | symbols_table_macros
20 | |> SymbolsTable.list_written_variables
21 | |> Enum.map(fn {name, _} -> name end)
22 | |> MapSet.new
23 |
24 | variables_never_read =
25 | MapSet.difference(variables_written, variables_read)
26 |
27 | tips = %{
28 | variables_never_read: variables_never_read
29 | }
30 |
31 | run(ast, tips)
32 | end
33 |
34 |
35 | defp run({_node, %{ignore: true}} = node, _tips) do
36 | node
37 | end
38 |
39 | defp run({node, metadata}, tips) do
40 | run_result = run(node, tips)
41 |
42 | case run_result do
43 | {node, @ignore_node} ->
44 | case Process.get(:no_keep_ignored_node) do
45 | true ->
46 | nil
47 | _ ->
48 | {node, %{metadata | ignore: true}}
49 | end
50 |
51 | {node, @keep_node} ->
52 | {node, metadata}
53 | end
54 | end
55 |
56 | defp run(block, tips) when is_list(block) do
57 | Enum.map(block, fn block -> run(block, tips) end)
58 | |> Enum.reject(&is_nil/1)
59 | end
60 |
61 | defp run(%{block: block} = node, tips) do
62 | {
63 | %{node | block: run(block, tips)},
64 | @keep_node
65 | }
66 | end
67 |
68 | defp run(
69 | %{scalar_variable: {%ScalarVariable{name: scalar_name}, _metadata}} = node,
70 | %{variables_never_read: variables_never_read})
71 | do
72 | case Enum.member?(variables_never_read, "$#{scalar_name}") do
73 | true ->
74 | {node, @ignore_node}
75 |
76 | false ->
77 | {node, @keep_node}
78 | end
79 | end
80 |
81 | defp run(
82 | %{array_variable: {%ArrayVariable{name: array_name}, _metadata}} = node,
83 | %{variables_never_read: variables_never_read})
84 | do
85 | case Enum.member?(variables_never_read, "@#{array_name}") do
86 | true ->
87 | {node, @ignore_node}
88 |
89 | false ->
90 | {node, @keep_node}
91 | end
92 | end
93 |
94 | defp run(
95 | %{hash_variable: {%HashVariable{name: hash_name}, _metadata}} = node,
96 | %{variables_never_read: variables_never_read})
97 | do
98 | case Enum.member?(variables_never_read, "%#{hash_name}") do
99 | true ->
100 | {node, @ignore_node}
101 |
102 | false ->
103 | {node, @keep_node}
104 | end
105 | end
106 |
107 | defp run(undefinedNode, _tips) do
108 | {undefinedNode, @keep_node}
109 | end
110 | end
111 |
--------------------------------------------------------------------------------
/docs/examples.md:
--------------------------------------------------------------------------------
1 | # Examples
2 |
3 | This is a page with examples of codes compiled from EventMacro to OpenKore plugin (Perl)
4 |
5 | ## Scalar variables
6 |
7 | ```
8 | macro scalarVariables {
9 | # Scalar variables declaration
10 | $foo = 1
11 | $bar = bar
12 | $baz = The $foo and $bar
13 |
14 | # Logs
15 | log \$foo value: $foo
16 | log \$bar value: $bar
17 | log \$baz value: $baz
18 | }
19 | ```
20 |
21 | ```
22 | package macroCompiled;
23 | use Log qw(message);
24 | my $bar;
25 | my $baz;
26 | my $foo;
27 | Plugins::register('macroCompiled', 'Compiled version of eventMacro.txt', &on_unload);
28 | sub on_unload { }
29 |
30 | sub macro_scalarVariables {
31 | $foo = "1";
32 | $bar = "bar";
33 | $baz = "The $foo and $bar";
34 |
35 | message "\$foo value: $foo"."\n";
36 | message "\$bar value: $bar"."\n";
37 | message "\$baz value: $baz"."\n";
38 | }
39 | ```
40 |
41 | ## Array variables
42 |
43 | ```
44 | macro arrayVariables {
45 | # Array variable declaration
46 | @array = (prontera, 42, don't panic)
47 |
48 | # Array variable manipulation
49 | $firstElement = $array[0]
50 | log The first element is $firstElement
51 |
52 | log And the second element is $array[1]
53 |
54 | &push(@array, openkore)
55 | log \@array now has @array elements
56 |
57 | &shift(@array)
58 | log \@array now has @array elements
59 | }
60 | ```
61 |
62 | ```
63 | package macroCompiled;
64 | use Log qw(message);
65 | my $firstElement;
66 | my @array;
67 | Plugins::register('macroCompiled', 'Compiled version of eventMacro.txt', &on_unload);
68 | sub on_unload { }
69 |
70 | sub macro_arrayVariables {
71 | @array = ("prontera","42","don't panic");
72 |
73 | $firstElement = $array["0"];
74 | message "The first element is $firstElement"."\n";
75 |
76 | message "And the second element is ".$array["1"].""."\n";
77 |
78 | push @array,"openkore";
79 | message "\@array now has ".scalar(@array)." elements"."\n";
80 |
81 | shift @array;
82 | message "\@array now has ".scalar(@array)." elements"."\n";
83 | }
84 | ```
85 |
86 | ## Hash variable
87 |
88 | ```
89 | macro hashVariables {
90 | # Hash variable declaration
91 | %hash = (city => prontera, goodNumber => 42, message => don't panic)
92 |
93 | # Hash variable manipulation
94 | $city = $hash{city}
95 | log The city element is $city
96 |
97 | log And the good number is $hash{goodNumber}
98 |
99 | &delete($hash{message})
100 | log \%hash now has %hash elements
101 |
102 | @keys = &keys(%hash)
103 | log The keys is $keys[0] and $keys[1]
104 |
105 | @values = &values(%hash)
106 | log And the values is $values[0] and $values[1]
107 | }
108 | ```
109 |
110 | ```
111 | package macroCompiled;
112 | use Log qw(message);
113 | my $city;
114 | my %hash;
115 | my @keys;
116 | my @values;
117 | Plugins::register('macroCompiled', 'Compiled version of eventMacro.txt', &on_unload);
118 | sub on_unload { }
119 |
120 | sub macro_hashVariables {
121 | %hash = ("city" => "prontera","goodNumber" => "42","message" => "don't panic");
122 |
123 | $city = $hash{city};
124 | message "The city element is $city"."\n";
125 |
126 | message "And the good number is $hash{goodNumber}"."\n";
127 |
128 | delete $hash{message};
129 | message "\%hash now has ".scalar(keys %hash)." elements"."\n";
130 |
131 | @keys = keys %hash;
132 | message "The keys is ".$keys["0"]." and ".$keys["1"].""."\n";
133 |
134 | @values = values %hash;
135 | message "And the values is ".$values["0"]." and ".$values["1"].""."\n";
136 | }
137 | ```
138 |
139 | ## Rand and Random
140 |
141 | ```
142 | macro randoms {
143 | # Random number
144 | $min = 2
145 | $randomNumber = &rand($min, 10)
146 | log The random number is $randomNumber
147 |
148 | # Random element from array
149 | @cities = (prontera, payon, geffen, morroc)
150 | $randomCity = $cities[&rand(0, 3)]
151 | log I'll go to $randomCity !
152 |
153 | # Or...
154 | $randomCity = &random(prontera, payon, geffen, marroc)
155 | log I'll go to $randomCity !
156 | }
157 | ```
158 |
159 | ```
160 | package macroCompiled;
161 | use Log qw(message);
162 | my $min;
163 | my $randomCity;
164 | my $randomNumber;
165 | my @cities;
166 | Plugins::register('macroCompiled', 'Compiled version of eventMacro.txt', &on_unload);
167 | sub on_unload { }
168 |
169 | sub macro_randoms {
170 | $min = "2";
171 | $randomNumber = ($min + int(rand(1 + "10" - $min)));
172 | message "The random number is $randomNumber"."\n";
173 |
174 | @cities = ("prontera","payon","geffen","morroc");
175 | $randomCity = $cities[("0" + int(rand(1 + "3" - "0")))];
176 | message "I'll go to $randomCity !"."\n";
177 |
178 | $randomCity = (("prontera","payon","geffen","marroc")[int (rand 4)]);
179 | message "I'll go to $randomCity !"."\n";
180 | }
181 | ```
182 |
183 | ## Calling function
184 |
185 | ```
186 | macro a {
187 | log I'll call another macro
188 | call b
189 | }
190 |
191 | macro b {
192 | log Macro b called!
193 | }
194 | ```
195 |
196 | ```
197 | package macroCompiled;
198 | use Log qw(message);
199 | Plugins::register('macroCompiled', 'Compiled version of eventMacro.txt', &on_unload);
200 | sub on_unload { }
201 |
202 | sub macro_a {
203 | message "I'll call another macro"."\n";
204 | ¯o_b();
205 | }
206 | sub macro_b {
207 | message "Macro b called!"."\n";
208 | }
209 | ```
210 |
211 | # Optimization
212 |
213 | MacroCompiler has Optimization phase, in order to create an equivalent code, but that results in a faster and smaller code.
214 |
215 | ## Dead code strip
216 |
217 | It'll removes useless variables.
218 |
219 | This both codes are equivalents:
220 |
221 | ```
222 | macro blah {
223 | $foo++
224 | $foo++
225 | $bar = 3
226 | @array = (1, $foo, $bar)
227 |
228 | $a = 1
229 | $b = 2
230 | %c = (a => $a, b => $b)
231 |
232 | log only $bar is read
233 | }
234 | ```
235 |
236 | ```
237 | macro blah {
238 | $bar = 3
239 |
240 | log only $bar is read
241 | }
242 | ```
243 |
--------------------------------------------------------------------------------
/lib/code_generation/header.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.CodeGeneration.Header do
2 | alias MacroCompiler.Parser.DoCommand
3 | alias MacroCompiler.Parser.LogCommand
4 | alias MacroCompiler.Parser.UndefCommand
5 | alias MacroCompiler.Parser.ScalarAssignmentCommand
6 | alias MacroCompiler.Parser.ArrayAssignmentCommand
7 | alias MacroCompiler.Parser.HashAssignmentCommand
8 | alias MacroCompiler.Parser.ScalarVariable
9 | alias MacroCompiler.Parser.ArrayVariable
10 | alias MacroCompiler.Parser.HashVariable
11 | alias MacroCompiler.Parser.IncrementCommand
12 | alias MacroCompiler.Parser.DecrementCommand
13 | alias MacroCompiler.Parser.PushCommand
14 | alias MacroCompiler.Parser.PopCommand
15 | alias MacroCompiler.Parser.ShiftCommand
16 | alias MacroCompiler.Parser.UnshiftCommand
17 | alias MacroCompiler.Parser.DeleteCommand
18 | alias MacroCompiler.Parser.KeysCommand
19 | alias MacroCompiler.Parser.ValuesCommand
20 |
21 | alias MacroCompiler.SemanticAnalysis.SymbolsTable
22 |
23 | def generate(node, %{macros: symbols_table, special_variables: special_variables}) do
24 | []
25 | |> Enum.concat(["package macroCompiled;"])
26 | |> Enum.concat(start_find_requirements(node))
27 | |> Enum.concat(import_special_variables(special_variables))
28 | |> Enum.concat([
29 | """
30 | Plugins::register('macroCompiled', 'Compiled version of eventMacro.txt', \&on_unload);
31 | sub on_unload { }
32 | """
33 | ])
34 | |> Enum.concat(commands_register(symbols_table))
35 | end
36 |
37 | defp import_special_variables(special_variables) do
38 | special_variables
39 | |> Enum.map(fn
40 | "$.zeny" -> "use Globals qw($char);"
41 | end)
42 | end
43 |
44 | defp commands_register(symbols_table) do
45 | macros_hash_value =
46 | symbols_table
47 | |> SymbolsTable.list_written_macros
48 | |> Enum.map(&"#{&1} => \\¯o_#{&1},")
49 |
50 | [
51 | "Commands::register(",
52 | "['macroCompiled', 'MacroCompiled plugin', \\&commandHandler]",
53 | ");",
54 | "my %macros = (",
55 | macros_hash_value,
56 | ");",
57 | "sub commandHandler {",
58 | " my $macroFunc = $macros{$_[1]};",
59 | " &$macroFunc;",
60 | "}"
61 | ]
62 | end
63 |
64 | defp start_find_requirements(node) do
65 | find_requirements(node)
66 | |> List.flatten
67 | |> MapSet.new
68 | |> MapSet.delete(nil)
69 | |> Enum.map(&(
70 | case &1 do
71 | %{module: module_name} -> "use #{module_name};"
72 | %{variable: variable_name} -> "my #{variable_name};"
73 | end
74 | ))
75 | end
76 |
77 | defp find_requirements({_node, %{ignore: true}}) do
78 |
79 | end
80 |
81 | defp find_requirements({node, _metadata}) do
82 | find_requirements(node)
83 | end
84 |
85 | defp find_requirements(block) when is_list(block) do
86 | Enum.map(block, &(find_requirements(&1)))
87 | end
88 |
89 | defp find_requirements(%{block: block}) do
90 | find_requirements(block)
91 | end
92 |
93 | defp find_requirements(%DoCommand{text: _text}) do
94 | %{module: "Commands"}
95 | end
96 |
97 | defp find_requirements(%LogCommand{text: _text}) do
98 | %{module: "Log qw(message)"}
99 | end
100 |
101 | defp find_requirements(%ScalarAssignmentCommand{scalar_variable: scalar_variable, scalar_value: _scalar_value}) do
102 | find_requirements(scalar_variable)
103 | end
104 |
105 | defp find_requirements(%ArrayAssignmentCommand{array_variable: array_variable, texts: _texts}) do
106 | find_requirements(array_variable)
107 | end
108 |
109 | defp find_requirements(%HashAssignmentCommand{hash_variable: hash_variable, keystexts: _keystexts}) do
110 | find_requirements(hash_variable)
111 | end
112 |
113 | defp find_requirements(%DeleteCommand{scalar_variable: scalar_variable}) do
114 | find_requirements(scalar_variable)
115 | end
116 |
117 | defp find_requirements(%KeysCommand{array_variable: array_variable, param_hash_variable: param_hash_variable}) do
118 | [
119 | find_requirements(array_variable),
120 | find_requirements(param_hash_variable)
121 | ]
122 | end
123 |
124 | defp find_requirements(%ValuesCommand{array_variable: array_variable, param_hash_variable: param_hash_variable}) do
125 | [
126 | find_requirements(array_variable),
127 | find_requirements(param_hash_variable)
128 | ]
129 | end
130 |
131 | defp find_requirements(%UndefCommand{scalar_variable: scalar_variable}) do
132 | find_requirements(scalar_variable)
133 | end
134 |
135 | defp find_requirements(%IncrementCommand{scalar_variable: scalar_variable}) do
136 | find_requirements(scalar_variable)
137 | end
138 |
139 | defp find_requirements(%DecrementCommand{scalar_variable: scalar_variable}) do
140 | find_requirements(scalar_variable)
141 | end
142 |
143 | defp find_requirements(%PushCommand{array_variable: array_variable, text: _text}) do
144 | find_requirements(array_variable)
145 | end
146 |
147 | defp find_requirements(%PopCommand{array_variable: array_variable}) do
148 | find_requirements(array_variable)
149 | end
150 |
151 | defp find_requirements(%ShiftCommand{array_variable: array_variable}) do
152 | find_requirements(array_variable)
153 | end
154 |
155 | defp find_requirements(%UnshiftCommand{array_variable: array_variable, text: _text}) do
156 | find_requirements(array_variable)
157 | end
158 |
159 | defp find_requirements(%ScalarVariable{name: name, array_position: nil, hash_position: hash_position}) do
160 | case {name, hash_position} do
161 | {name, nil} ->
162 | %{variable: "$#{name}"}
163 |
164 | {name, _hash_position} ->
165 | %{variable: "%#{name}"}
166 | end
167 | end
168 |
169 | defp find_requirements(%ArrayVariable{name: name}) do
170 | %{variable: "@#{name}"}
171 | end
172 |
173 | defp find_requirements(%HashVariable{name: name}) do
174 | %{variable: "%#{name}"}
175 | end
176 |
177 | defp find_requirements(_undefinedNode) do
178 |
179 | end
180 | end
181 |
--------------------------------------------------------------------------------
/lib/optimization/constant_folding/constant_folding.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.Optimization.ConstantFolding do
2 | alias MacroCompiler.Parser.Macro
3 | alias MacroCompiler.Parser.ScalarVariable
4 | alias MacroCompiler.Parser.LogCommand
5 | alias MacroCompiler.Parser.TextValue
6 | alias MacroCompiler.Parser.CallCommand
7 | alias MacroCompiler.Parser.RandCommand
8 | alias MacroCompiler.Parser.ScalarAssignmentCommand
9 | alias MacroCompiler.Parser.ArrayAssignmentCommand
10 | alias MacroCompiler.Parser.HashAssignmentCommand
11 | alias MacroCompiler.Parser.PushCommand
12 |
13 |
14 | def optimize(ast, %{macros: symbols_table_macro}) do
15 | tuple_macro_last_write_variables =
16 | symbols_table_macro
17 | |> Enum.map(&{&1.macro_write.name, &1.macro_write.last_write_variables})
18 |
19 | Process.put(:macro_last_write_variables, tuple_macro_last_write_variables)
20 |
21 | optimized_ast =
22 | ast
23 | |> Enum.map(
24 | fn {%Macro{block: block} = node, metadata} ->
25 | {%{node | block: optimize_block(block).optimized_block}, metadata}
26 | end
27 | )
28 |
29 | Process.put(:macro_last_write_variables, nil)
30 |
31 | optimized_ast
32 | end
33 |
34 | defp optimize_block(block, initial_variables_context\\%{}) do
35 | {optimized_block, final_variables_context} =
36 | Enum.reduce(
37 | block,
38 | {[], initial_variables_context},
39 | fn
40 | ({%{block: block} = block_node, metadata}, {nodes, variables_context}) ->
41 | %{
42 | optimized_block: optimized_block,
43 | variables_writen: variables_writen
44 | } = optimize_block(block, variables_context)
45 |
46 | {_, variables_context} =
47 | variables_context
48 | |> Map.split(variables_writen)
49 |
50 | optimized_block_node = %{block_node | block: optimized_block}
51 |
52 | {[{optimized_block_node, metadata} | nodes], variables_context}
53 |
54 | (current_node, {nodes, variables_context}) ->
55 | {node, updated_variables_context} =
56 | run(current_node, variables_context)
57 |
58 | {[node | nodes], updated_variables_context}
59 | end
60 | )
61 |
62 | variable_context_differences =
63 | initial_variables_context
64 | |> Enum.reject(fn {var_name, var_value} ->
65 | Map.get(final_variables_context, var_name) == var_value
66 | end)
67 | |> Enum.map(fn {var_name, _var_value} ->
68 | var_name
69 | end)
70 |
71 | %{
72 | optimized_block: Enum.reverse(optimized_block),
73 | variables_writen: variable_context_differences
74 | }
75 | end
76 |
77 | defp run({_node, %{ignore: true}} = node, variables_context) do
78 | {node, variables_context}
79 | end
80 |
81 | defp run(
82 | {
83 | %ScalarAssignmentCommand{
84 | scalar_variable: {%ScalarVariable{name: scalar_name, array_position: nil, hash_position: nil}, _},
85 | scalar_value: scalar_value
86 | } = node,
87 | metadata
88 | },
89 | variables_context
90 | ) do
91 | optimized_node =
92 | {%{node | scalar_value: optimize_scalar_value(scalar_value, variables_context)}, metadata}
93 |
94 | updated_variables_context =
95 | case scalar_value do
96 | # determinist value
97 | %TextValue{} ->
98 | Map.put(variables_context, scalar_name, scalar_value)
99 |
100 | # non-determinist value
101 | _ ->
102 | Map.delete(variables_context, scalar_name)
103 | end
104 |
105 | {optimized_node, updated_variables_context}
106 | end
107 |
108 | defp run({%LogCommand{text: text} = node, metadata}, variables_context) do
109 | {
110 | {%{node | text: optimize_scalar_value(text, variables_context)}, metadata},
111 | variables_context
112 | }
113 | end
114 |
115 | defp run({%ArrayAssignmentCommand{texts: texts} = node, metadata}, variables_context) do
116 | optimized_texts =
117 | texts
118 | |> Enum.map(&optimize_scalar_value(&1, variables_context))
119 |
120 | {
121 | {%{node | texts: optimized_texts}, metadata},
122 | variables_context
123 | }
124 | end
125 |
126 | defp run({%HashAssignmentCommand{keystexts: keystexts} = node, metadata}, variables_context) do
127 | optimized_keystexts =
128 | keystexts
129 | |> Enum.map(
130 | &[
131 | Enum.at(&1, 0),
132 | optimize_scalar_value(Enum.at(&1, 1), variables_context)
133 | ]
134 | )
135 |
136 | {
137 | {%{node | keystexts: optimized_keystexts}, metadata},
138 | variables_context
139 | }
140 | end
141 |
142 | defp run({%PushCommand{text: text} = node, metadata}, variables_context) do
143 | {
144 | {%{node | text: optimize_scalar_value(text, variables_context)}, metadata},
145 | variables_context
146 | }
147 | end
148 |
149 | defp run({%CallCommand{macro: macro}, _} = node, variables_context) do
150 | {_macro_name, last_write_variables} =
151 | List.keyfind(Process.get(:macro_last_write_variables), macro, 0)
152 |
153 | {node, Map.merge(variables_context, last_write_variables)}
154 | end
155 |
156 | defp run(node, variables_context) do
157 | {node, variables_context}
158 | end
159 |
160 | defp optimize_scalar_value({%RandCommand{min: min, max: max}, metadata}, variables_context) do
161 | {
162 | {
163 | %RandCommand{
164 | min: optimize_scalar_value(min, variables_context),
165 | max: optimize_scalar_value(max, variables_context)
166 | },
167 | metadata
168 | },
169 |
170 | variables_context
171 | }
172 | end
173 |
174 | defp optimize_scalar_value({%ScalarVariable{name: scalar_name, array_position: nil, hash_position: nil}, _metadata} = node, variables_context) do
175 | case Map.get(variables_context, scalar_name, nil) do
176 | %TextValue{} = text_value ->
177 | text_value
178 |
179 | :is_not_determinist ->
180 | node
181 |
182 | nil ->
183 | node
184 | end
185 | end
186 |
187 | defp optimize_scalar_value(%TextValue{values: values}, variables_context) do
188 | optimized_values =
189 | values
190 | |> Enum.map(&case &1 do
191 | {%ScalarVariable{name: scalar_name}, _} ->
192 | case Map.get(variables_context, scalar_name, nil) do
193 | # determinist value
194 | %TextValue{values: more_values} ->
195 | more_values
196 |
197 | # non-determinist value
198 | _ ->
199 | &1
200 | end
201 |
202 | char ->
203 | char
204 | end)
205 | |> List.flatten
206 |
207 | %TextValue{values: optimized_values}
208 | end
209 |
210 | defp optimize_scalar_value(node, _variables_context) do
211 | node
212 | end
213 | end
214 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # MacroCompiler
4 | > The best way to create macros.
5 |
6 | [](https://travis-ci.com/macabeus/macro-compiler)
7 |
8 | MacroCompiler compiles [EventMacro](http://openkore.com/index.php/EventMacro) to a [OpenKore](https://github.com/OpenKore/openkore/) plugin (that is, Perl). EventMacro is a language to automate the actions from OpenKore – the bot used in the Rangarok online game. It compiles to Perl since OpenKore itself is written in Perl.
9 |
10 | You may use EventMacro to configure the bot to complete quests or to buy itens, for example. Currently the only solution to run EventMacro on OpenKore is using a regex-based interpreter, which is a bad solution. This project aims to offer a faster, more reliable and flexible alternative to run macros on OpenKore.
11 |
12 | **Faster** because the OpenKore doesn't need to interpret a Macro code to run Perl code. Now it runs Perl code directly and the compiler can optimize the EventMacro.
13 | **More reliable** because the compiler shows errors and you are able to fix them before actually running the bot.
14 | **More flexible** because it is easier to add new features in a compiler than in a regex-based interpreter.
15 |
16 | >Hey! Warning: This project is under construction, thus it is incomplete and currently only has a tiny subset of EventMacro commands.
17 |
18 | # Example
19 |
20 | The macro
21 |
22 | ```
23 | macro goToRandomCity {
24 | @cities = (prontera, payon, geffen, morroc)
25 | $randomCity = $cities[&rand(0, 3)]
26 |
27 | log I'll go to $randomCity !
28 | do move $randomCity
29 | }
30 | ```
31 |
32 | will compile to
33 |
34 | ```
35 | package macroCompiled;
36 | use Log qw(message);
37 | my $randomCity;
38 | my @cities;
39 | Plugins::register('macroCompiled', 'Compiled version of eventMacro.txt', &on_unload);
40 | sub on_unload { }
41 |
42 | sub macro_goToRandomCity {
43 | @cities = ("prontera","payon","geffen","morroc");
44 | $randomCity = $cities[("0" + int(rand(1 + "3" - "0")))];
45 | message "I'll go to $randomCity !"."\n";
46 | Commands::run("move $randomCity");
47 | }
48 |
49 | ```
50 |
51 | Then you could use this plugin. [You may see more examples here.](docs/examples.md)
52 |
53 | # How to run
54 |
55 | You must have Elixir in your computer. [Read how to install Elixir here](https://elixir-lang.org/install.html).
56 |
57 | Run the command below so you can compile your macro:
58 |
59 | ```
60 | mix run lib/macrocompiler.ex path/of/eventMacro.txt > macro.pl
61 | ````
62 |
63 | # Test
64 |
65 | ```
66 | mix test
67 | ```
68 |
69 | # How does it work?
70 |
71 | Since this is a project for studying purposes, I will explain how I created it, including its logic and design. Also, you can see [this asciinema](https://asciinema.org/a/199032) about how to add a new command at the compiler, step by step - this video has just 4 minutes!
72 |
73 | ## Talks
74 |
75 | As a part of my study, I presented some talks about compilers and I used this project as a study case.
76 |
77 | - Talk in English at The Conf 2018 🇬🇧 [Slides](https://speakerdeck.com/macabeus/demystifying-compilers-by-writing-your-own)
78 |
79 |
80 |
81 | - Talk in Portuguese at Pagar.me 🇧🇷 [Slides Part 1](https://speakerdeck.com/macabeus/aprendendo-compiladores-fazendo-um-parte-1) and [Slides Part 2](https://speakerdeck.com/macabeus/aprendendo-compiladores-fazendo-um-parte-2)
82 |
83 |
84 |
85 | ## Language design
86 |
87 | I **have not** designed EventMacro: its specs have been already made by other people and my compiler only needs to keep interoperability. A few important things that have influenced EventMacro design are:
88 |
89 | - Perl inspiration; given the fact that OpenKore is written in Perl and many people who work on OpenKore project also write macros;
90 | - The syntax was designed in order to make it easy to write a regexp-based interpreter;
91 | - It was designed aiming to ease non-programmers' learning process.
92 |
93 | > Designing a programming language and a compiler are processes that have very different focuses, but many tasks in common. [You may read more about it here.](https://www.quora.com/Which-is-the-difference-between-design-a-programming-language-and-design-a-compiler/answer/Quildreen-Motta)
94 |
95 | ## Parser
96 |
97 | I decided to write the parser using a parser combinator-based strategy because Elixir already has an awesome library for doing so: [Combine](https://github.com/bitwalker/combine). In this phase, the parser maps the source code on a representation called AST - Abstract Syntax Tree. We only have this intermediary representation through the whole compiler.
98 |
99 | An advantage of parser combinator is that we get the AST directly, but a disadvantage is that we will have poor error messages.
100 |
101 | On my compiler, each node on the AST is a tuple with two elements, where the first element is the struct representing the mapped code and the second one is its metadata. The metadata is important to return a meaningful error message on the next phases (e.g. to tell the coder the line and column where the error happened). Another situation where the metadata is important is on the optimization phase - I will provide more details about it bellow.
102 |
103 | ## Semantic analysis
104 |
105 | The AST built on the previous phase is passed to the semantic analyzer. It builds a data structure called symbol table, which describes the names used on the code (function and variable names, for example). We could describe the arity of a function, for example.
106 |
107 | The aim of semantic analysis is to check whether the code is semantically valid or not, and it uses the symbol table to do so. For example, it checks if there are any variables that are being used but which have never been written.
108 |
109 | ## Optimization
110 |
111 | The optimizer uses both the AST and the symbol table in order to create an equivalent AST, but that results in a faster and smaller code. For example, an optimization implemented in MacroCompiler is [dead code elimination](https://en.wikipedia.org/wiki/Dead_code_elimination). A variable that is written but never called is useless so the optimizer finds these situations and tells the node's metadata to ignore or completely remove this node on code generation phase.
112 |
113 | ## Code generation
114 |
115 | Using the AST, we could map it to another language. In our context, an OpenKore plugin - that is, a Perl code. Since the EventMacro language and Perl are very similar, it's easy to do this mapping.
116 |
117 | We have two phases of code generation: header and body. On the header we find global requirements to declare on top of file - for example, variables declarations, because on EventMacro all variables are globals–but the same doesn't happen on Perl. On the body we generate the code itself.
118 |
--------------------------------------------------------------------------------
/lib/code_generation/body.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.CodeGeneration.Body do
2 | alias MacroCompiler.Parser.Macro
3 | alias MacroCompiler.Parser.CallCommand
4 | alias MacroCompiler.Parser.DoCommand
5 | alias MacroCompiler.Parser.LogCommand
6 | alias MacroCompiler.Parser.UndefCommand
7 | alias MacroCompiler.Parser.ScalarAssignmentCommand
8 | alias MacroCompiler.Parser.ArrayAssignmentCommand
9 | alias MacroCompiler.Parser.HashAssignmentCommand
10 | alias MacroCompiler.Parser.ScalarVariable
11 | alias MacroCompiler.Parser.ArrayVariable
12 | alias MacroCompiler.Parser.HashVariable
13 | alias MacroCompiler.Parser.IncrementCommand
14 | alias MacroCompiler.Parser.DecrementCommand
15 | alias MacroCompiler.Parser.PauseCommand
16 | alias MacroCompiler.Parser.PushCommand
17 | alias MacroCompiler.Parser.PopCommand
18 | alias MacroCompiler.Parser.TextValue
19 | alias MacroCompiler.Parser.ShiftCommand
20 | alias MacroCompiler.Parser.UnshiftCommand
21 | alias MacroCompiler.Parser.DeleteCommand
22 | alias MacroCompiler.Parser.KeysCommand
23 | alias MacroCompiler.Parser.ValuesCommand
24 | alias MacroCompiler.Parser.RandCommand
25 | alias MacroCompiler.Parser.RandomCommand
26 | alias MacroCompiler.Parser.PostfixIf
27 | alias MacroCompiler.Parser.Condition
28 | alias MacroCompiler.Parser.SingleCheck
29 | alias MacroCompiler.Parser.IfBlock
30 |
31 | def start_generate(block) do
32 | Enum.map(block, &(generate(&1)))
33 | |> List.flatten
34 | end
35 |
36 | defp generate({_node, %{ignore: true}}) do
37 |
38 | end
39 |
40 | defp generate({node, _metadata}) do
41 | generate(node)
42 | end
43 |
44 | defp generate(block) when is_list(block) do
45 | Enum.map(block, &(generate(&1)))
46 | end
47 |
48 |
49 | defp generate(%Macro{name: name, block: block}) do
50 | [
51 | "sub macro_#{name} {",
52 | generate(block),
53 | "}"
54 | ]
55 | end
56 |
57 | defp generate(%TextValue{values: values}) do
58 | values = values
59 | |> Enum.map(&(
60 | case &1 do
61 | {%ScalarVariable{array_position: nil}, _metadata} ->
62 | generate(&1)
63 |
64 | {%ScalarVariable{array_position: _array_position}, _metadata} ->
65 | [
66 | "\".",
67 | generate(&1),
68 | ".\""
69 | ]
70 |
71 | {%ArrayVariable{name: name}, _metadata} ->
72 | "\".scalar(@#{name}).\""
73 |
74 | {%HashVariable{name: name}, _metadata} ->
75 | "\".scalar(keys %#{name}).\""
76 |
77 | "\"" ->
78 | "\\\""
79 |
80 | char ->
81 | char
82 | end)
83 | )
84 | |> List.to_string
85 |
86 | "\"#{values}\""
87 | end
88 |
89 | defp generate(%CallCommand{macro: macro, params: params}) do
90 | params =
91 | params
92 | |> Enum.map(&("\"#{&1}\""))
93 | |> Enum.join(",")
94 |
95 | "¯o_#{macro}(#{params});"
96 | end
97 |
98 | defp generate(%DoCommand{text: text}) do
99 | [
100 | "Commands::run(",
101 | generate(text),
102 | ");"
103 | ]
104 | end
105 |
106 | defp generate(%LogCommand{text: text}) do
107 | [
108 | "message ",
109 | generate(text),
110 | ".\"\\n\";"
111 | ]
112 | end
113 |
114 | defp generate(%ScalarVariable{name: ".zeny"}) do
115 | "$char->{zeny}"
116 | end
117 |
118 | defp generate(%ScalarVariable{name: name, array_position: array_position, hash_position: hash_position}) do
119 | case {name, array_position, hash_position} do
120 | {name, nil, nil} ->
121 | "$#{name}"
122 |
123 | {name, array_position, nil} ->
124 | [
125 | "$#{name}[",
126 | generate(array_position),
127 | "]"
128 | ]
129 |
130 | {name, nil, hash_position} ->
131 | "$#{name}{#{hash_position}}"
132 | end
133 | end
134 |
135 | defp generate(%ScalarAssignmentCommand{scalar_variable: scalar_variable, scalar_value: scalar_value}) do
136 | [
137 | generate(scalar_variable),
138 | " = ",
139 | generate(scalar_value),
140 | ";"
141 | ]
142 | end
143 |
144 | defp generate(%ArrayVariable{name: name}) do
145 | "@#{name}"
146 | end
147 |
148 | defp generate(%ArrayAssignmentCommand{array_variable: array_variable, texts: texts}) do
149 | texts =
150 | texts
151 | |> Enum.map(&(generate(&1)))
152 | |> Enum.join(",")
153 |
154 | [
155 | generate(array_variable),
156 | " = (#{texts});"
157 | ]
158 | end
159 |
160 | defp generate(%HashVariable{name: name}) do
161 | "%#{name}"
162 | end
163 |
164 | defp generate(%HashAssignmentCommand{hash_variable: hash_variable, keystexts: keystexts}) do
165 | keystexts =
166 | keystexts
167 | |> Enum.map(&("\"#{Enum.at(&1, 0)}\" => #{generate(Enum.at(&1, 1))}"))
168 | |> Enum.join(",")
169 |
170 | [
171 | generate(hash_variable),
172 | " = (#{keystexts});"
173 | ]
174 | end
175 |
176 | defp generate(%DeleteCommand{scalar_variable: scalar_variable}) do
177 | [
178 | "delete ",
179 | generate(scalar_variable),
180 | ";"
181 | ]
182 | end
183 |
184 | defp generate(%KeysCommand{array_variable: array_variable, param_hash_variable: param_hash_variable}) do
185 | [
186 | generate(array_variable),
187 | "= keys ",
188 | generate(param_hash_variable),
189 | ";"
190 | ]
191 | end
192 |
193 | defp generate(%ValuesCommand{array_variable: array_variable, param_hash_variable: param_hash_variable}) do
194 | [
195 | generate(array_variable),
196 | "= values ",
197 | generate(param_hash_variable),
198 | ";"
199 | ]
200 | end
201 |
202 | defp generate(%UndefCommand{scalar_variable: scalar_variable}) do
203 | [
204 | "undef ",
205 |
206 | generate(scalar_variable),
207 |
208 | ";"
209 | ]
210 | end
211 |
212 | defp generate(%IncrementCommand{scalar_variable: scalar_variable}) do
213 | [
214 | generate(scalar_variable),
215 |
216 | "++;"
217 | ]
218 | end
219 |
220 | defp generate(%DecrementCommand{scalar_variable: scalar_variable}) do
221 | [
222 | generate(scalar_variable),
223 |
224 | "--;"
225 | ]
226 | end
227 |
228 | defp generate(%PauseCommand{seconds: _seconds}) do
229 | # TODO
230 | end
231 |
232 | defp generate(%PushCommand{array_variable: array_variable, text: text}) do
233 | [
234 | "push ",
235 | generate(array_variable),
236 | ",",
237 | generate(text),
238 | ";"
239 | ]
240 | end
241 |
242 | defp generate(%PopCommand{array_variable: array_variable}) do
243 | [
244 | "pop ",
245 | generate(array_variable),
246 | ";"
247 | ]
248 | end
249 |
250 | defp generate(%ShiftCommand{array_variable: array_variable}) do
251 | [
252 | "shift ",
253 | generate(array_variable),
254 | ";"
255 | ]
256 | end
257 |
258 | defp generate(%UnshiftCommand{array_variable: array_variable, text: text}) do
259 | [
260 | "unshift ",
261 | generate(array_variable),
262 | ",",
263 | generate(text),
264 | ";"
265 | ]
266 | end
267 |
268 | defp generate(%RandCommand{min: min, max: max}) do
269 | [
270 | "(",
271 | generate(min),
272 | " + int(rand(1 + ",
273 | generate(max),
274 | " - ",
275 | generate(min),
276 | ")))"
277 | ]
278 | end
279 |
280 | defp generate(%RandomCommand{values: values}) do
281 | valuesMapped =
282 | values
283 | |> Enum.map(&generate(&1))
284 | |> Enum.join(",")
285 |
286 | [
287 | "((",
288 | valuesMapped,
289 | ")[int (rand #{length(values)})])"
290 | ]
291 | end
292 |
293 | defp generate(%PostfixIf{condition: condition, block: block}) do
294 | [
295 | "if (",
296 | generate(condition),
297 | ") {",
298 | generate(block),
299 | "}"
300 | ]
301 | end
302 |
303 | defp generate(%Condition{scalar_variable: scalar_variable, operator: operator, value: value}) do
304 | [
305 | generate(scalar_variable),
306 | operator,
307 | generate(value)
308 | ]
309 | end
310 |
311 | defp generate(%SingleCheck{scalar_variable: scalar_variable}) do
312 | [
313 | generate(scalar_variable)
314 | ]
315 | end
316 |
317 | defp generate(%IfBlock{condition: condition, block: block}) do
318 | [
319 | "if (",
320 | generate(condition),
321 | ") {",
322 | generate(block),
323 | "}"
324 | ]
325 | end
326 |
327 | defp generate(_undefinedNode) do
328 |
329 | end
330 | end
331 |
--------------------------------------------------------------------------------
/lib/semantic_analysis/semantic_analysis.ex:
--------------------------------------------------------------------------------
1 | defmodule MacroCompiler.SemanticAnalysis do
2 | alias MacroCompiler.Parser.Macro
3 | alias MacroCompiler.Parser.CallCommand
4 | alias MacroCompiler.Parser.DoCommand
5 | alias MacroCompiler.Parser.LogCommand
6 | alias MacroCompiler.Parser.UndefCommand
7 | alias MacroCompiler.Parser.ScalarAssignmentCommand
8 | alias MacroCompiler.Parser.ArrayAssignmentCommand
9 | alias MacroCompiler.Parser.HashAssignmentCommand
10 | alias MacroCompiler.Parser.ScalarVariable
11 | alias MacroCompiler.Parser.ArrayVariable
12 | alias MacroCompiler.Parser.HashVariable
13 | alias MacroCompiler.Parser.IncrementCommand
14 | alias MacroCompiler.Parser.DecrementCommand
15 | alias MacroCompiler.Parser.PushCommand
16 | alias MacroCompiler.Parser.PopCommand
17 | alias MacroCompiler.Parser.TextValue
18 | alias MacroCompiler.Parser.ShiftCommand
19 | alias MacroCompiler.Parser.UnshiftCommand
20 | alias MacroCompiler.Parser.DeleteCommand
21 | alias MacroCompiler.Parser.KeysCommand
22 | alias MacroCompiler.Parser.ValuesCommand
23 | alias MacroCompiler.Parser.RandCommand
24 | alias MacroCompiler.Parser.RandomCommand
25 | alias MacroCompiler.Parser.PostfixIf
26 | alias MacroCompiler.Parser.Condition
27 | alias MacroCompiler.Parser.IfBlock
28 | alias MacroCompiler.Parser.SingleCheck
29 |
30 | alias MacroCompiler.SemanticAnalysis.LatestVariableWrites
31 | alias MacroCompiler.SemanticAnalysis.SymbolsTable
32 |
33 | import MacroCompiler.SemanticAnalysis.Validates.Variables
34 | import MacroCompiler.SemanticAnalysis.Validates.Macros
35 | import MacroCompiler.SemanticAnalysis.Validates.SpecialVariables
36 |
37 | def build_symbols_table(ast) do
38 | symbols_table =
39 | symbols_table(ast)
40 | |> List.flatten
41 |
42 | %{macros: symbols_table, special_variables: SymbolsTable.list_special_variables(symbols_table)}
43 | end
44 |
45 | def run_validates(symbols_table) do
46 | List.flatten([
47 | validate_variables(symbols_table),
48 | validate_macros(symbols_table),
49 | validate_special_variables(symbols_table)
50 | ])
51 | end
52 |
53 |
54 | defp symbols_table(block) when is_list(block) do
55 | Enum.map(block, &symbols_table/1)
56 | end
57 |
58 | defp symbols_table({_node, %{ignore: true}}) do
59 |
60 | end
61 |
62 |
63 | defp symbols_table({%Macro{name: name, block: block}, _metadata}) do
64 | %{
65 | macro_write: %{
66 | name: name,
67 | block: symbols_table(block),
68 | last_write_variables: LatestVariableWrites.build(block)
69 | }
70 | }
71 | end
72 |
73 | defp symbols_table({%CallCommand{macro: macro, params: params}, metadata}) do
74 | %{
75 | macro_read: {%{name: macro, params: params}, metadata}
76 | }
77 | end
78 |
79 | defp symbols_table({%DoCommand{text: text}, _metadata}) do
80 | %{
81 | variable_read: symbols_table(text)
82 | }
83 | end
84 |
85 | defp symbols_table({%LogCommand{text: text}, _metadata}) do
86 | %{
87 | variable_read: symbols_table(text)
88 | }
89 | end
90 |
91 | defp symbols_table({%ScalarAssignmentCommand{scalar_variable: scalar_variable, scalar_value: scalar_value}, _metadata}) do
92 | %{
93 | variable_write: symbols_table(scalar_variable),
94 | variable_read: symbols_table(scalar_value)
95 | }
96 | end
97 |
98 | defp symbols_table({%ArrayAssignmentCommand{array_variable: array_variable, texts: texts}, _metadata}) do
99 | %{
100 | variable_write: symbols_table(array_variable),
101 | variable_read: symbols_table(texts)
102 | }
103 | end
104 |
105 | defp symbols_table({%HashAssignmentCommand{hash_variable: hash_variable, keystexts: keystexts}, _metadata}) do
106 | %{
107 | variable_write: symbols_table(hash_variable),
108 | variable_read: symbols_table(keystexts)
109 | }
110 | end
111 |
112 | defp symbols_table({%UndefCommand{scalar_variable: scalar_variable}, _metadata}) do
113 | %{
114 | variable_write: symbols_table(scalar_variable)
115 | }
116 | end
117 |
118 | defp symbols_table({%IncrementCommand{scalar_variable: scalar_variable}, _metadata}) do
119 | %{
120 | variable_write: symbols_table(scalar_variable)
121 | }
122 | end
123 |
124 | defp symbols_table({%DecrementCommand{scalar_variable: scalar_variable}, _metadata}) do
125 | %{
126 | variable_write: symbols_table(scalar_variable)
127 | }
128 | end
129 |
130 | defp symbols_table({%PushCommand{array_variable: array_variable, text: text}, _metadata}) do
131 | %{
132 | variable_write: symbols_table(array_variable),
133 | variable_read: symbols_table(text)
134 | }
135 | end
136 |
137 | defp symbols_table({%PopCommand{array_variable: array_variable}, _metadata}) do
138 | %{
139 | variable_read: symbols_table(array_variable)
140 | }
141 | end
142 |
143 | defp symbols_table({%ShiftCommand{array_variable: array_variable}, _metadata}) do
144 | %{
145 | variable_read: symbols_table(array_variable)
146 | }
147 | end
148 |
149 | defp symbols_table({%UnshiftCommand{array_variable: array_variable, text: text}, _metadata}) do
150 | %{
151 | variable_write: symbols_table(array_variable),
152 | variable_read: symbols_table(text)
153 | }
154 | end
155 |
156 | defp symbols_table({%ScalarVariable{name: name, array_position: array_position, hash_position: hash_position}, metadata}) do
157 | case {array_position, hash_position} do
158 | {nil, nil} ->
159 | %{
160 | variable_name: {"$#{name}", metadata}
161 | }
162 |
163 | {array_position, nil} ->
164 | %{
165 | variable_name: {"@#{name}", metadata},
166 | variable_read: symbols_table(array_position)
167 | }
168 |
169 | {nil, hash_position} ->
170 | %{
171 | variable_name: {"%#{name}", metadata},
172 | variable_read: symbols_table(hash_position)
173 | }
174 | end
175 | end
176 |
177 | defp symbols_table({%ArrayVariable{name: name}, metadata}) do
178 | %{
179 | variable_name: {"@#{name}", metadata}
180 | }
181 | end
182 |
183 | defp symbols_table({%HashVariable{name: name}, metadata}) do
184 | %{
185 | variable_name: {"%#{name}", metadata}
186 | }
187 | end
188 |
189 | defp symbols_table({%DeleteCommand{scalar_variable: scalar_variable}, _metadata}) do
190 | %{
191 | variable_write: symbols_table(scalar_variable)
192 | }
193 | end
194 |
195 | defp symbols_table({%KeysCommand{array_variable: array_variable, param_hash_variable: param_hash_variable}, _metadata}) do
196 | %{
197 | variable_write: symbols_table(array_variable),
198 | variable_read: symbols_table(param_hash_variable)
199 | }
200 | end
201 |
202 | defp symbols_table({%ValuesCommand{array_variable: array_variable, param_hash_variable: param_hash_variable}, _metadata}) do
203 | %{
204 | variable_write: symbols_table(array_variable),
205 | variable_read: symbols_table(param_hash_variable)
206 | }
207 | end
208 |
209 | defp symbols_table(%TextValue{values: values}) do
210 | values
211 | |> Enum.map(&(
212 | case &1 do
213 | {%ScalarVariable{name: _name, array_position: _array_position, hash_position: _hash_position}, _metadata} ->
214 | symbols_table(&1)
215 |
216 | {%ArrayVariable{name: _name}, _metadata} ->
217 | symbols_table(&1)
218 |
219 | {%HashVariable{name: _name}, _metadata} ->
220 | symbols_table(&1)
221 |
222 | _ ->
223 | nil
224 | end)
225 | )
226 | end
227 |
228 | defp symbols_table({%RandCommand{min: min, max: max}, _metadata}) do
229 | [min, max]
230 | |> Enum.map(&(
231 | %{
232 | variable_read: symbols_table(&1)
233 | }
234 | ))
235 | end
236 |
237 | defp symbols_table({%RandomCommand{values: values}, _metadata}) do
238 | values
239 | |> Enum.map(&(
240 | %{
241 | variable_read: symbols_table(&1)
242 | }
243 | ))
244 | end
245 |
246 | defp symbols_table({%PostfixIf{condition: condition, block: block}, _metadata}) do
247 | [
248 | %{variable_read: symbols_table(condition)},
249 | %{variable_read: symbols_table(block)},
250 | %{variable_write: symbols_table(block)}
251 | ]
252 | end
253 |
254 | defp symbols_table({%IfBlock{condition: condition, block: block}, _metadata}) do
255 | [
256 | %{variable_read: symbols_table(condition)},
257 | %{variable_read: symbols_table(block)},
258 | %{variable_write: symbols_table(block)}
259 | ]
260 | end
261 |
262 | defp symbols_table({%Condition{scalar_variable: scalar_variable, value: value}, _metadata}) do
263 | [scalar_variable, value]
264 | |> Enum.map(&(
265 | %{
266 | variable_read: symbols_table(&1)
267 | }
268 | ))
269 | end
270 |
271 | defp symbols_table({%SingleCheck{scalar_variable: scalar_variable}, _metadata}) do
272 | [
273 | %{variable_read: symbols_table(scalar_variable)}
274 | ]
275 | end
276 |
277 | defp symbols_table(_undefinedNode) do
278 |
279 | end
280 | end
281 |
--------------------------------------------------------------------------------