├── 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 | [![Build Status](https://travis-ci.com/macabeus/macro-compiler.svg?branch=master)](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 | --------------------------------------------------------------------------------