├── .formatter.exs ├── .gitignore ├── README.md ├── benchmarks ├── add.dw ├── if.dw └── while.dw ├── config └── config.exs ├── dwarf ├── lib ├── cli.ex ├── env.ex ├── evaluator.ex ├── lexer.ex └── parser.ex ├── mix.exs ├── mix.lock └── test ├── dwarf_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /.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 | # Ignore package tarball (built via "mix hex.build"). 23 | dwarf-*.tar 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dwarf 2 | 3 | Dwarf is a very small scripting language that will ease concurrency. 4 | The syntax of Dwarf is heavily influenced by programming languages like Go and OCaml. 5 | 6 | # Dwarf-Interpreter 7 | 8 | The interpreter for dwarf is written in Elixir. 9 | 10 | To interpret a source program run 11 | 12 | ``` 13 | mix escript.build # in the main folder 14 | # this will generate an executable dwarf file 15 | ./dwarf path-to-file.dw # this will start the interpretition of the script 16 | ``` 17 | 18 | The "main" function is declared in lib/cli.ex 19 | 20 | ## Language description 21 | ----------------------- 22 | To see some examples of programs written in Dwarf, look inside the folder /benchmarks/*.dw 23 | 24 | The language will have very simple operations and expressions. 25 | Here is the CFG specification in BNF of the parser of Dwarf, 26 | 27 | ``` 28 | stmt ::= 29 | | `if` expr `then` stmt `else` stmt --- if expr then statement else statement 30 | | 'fun' ident ':=' '(' params ')' '->' stmt --- declaration of a function 31 | | type ident '=' expr --- assignment such as `a = 3 + 2` where a has already been declared 32 | | type ident ':=' expr --- declaration and assignment of a new variable `int a := 3 + 2` 33 | | '{' stmts '}' --- a list of statements 34 | | 'print' ident --- print a value to the command line 35 | | 'while' expr 'do' stmt --- a typical while statement 36 | 37 | expr ::= 38 | | op <> expr --- concatenation of two string 39 | | op 40 | op ::= 41 | | term + op --- addition 42 | | term - op --- substraction 43 | | term > op --- greater than 44 | | term >= op --- greater or equal 45 | | term < op --- smaller than 46 | | term <= op --- smaller or equal 47 | | term == op --- equality 48 | | term 49 | 50 | term ::= 51 | | factor * term --- multiplication 52 | | factor / term --- division 53 | | factor % term --- modulo 54 | | factor 55 | 56 | factor ::= 57 | | `true`, `false` --- booleans 58 | | number --- integer such as `7`,`8`,`9` no floats yet 59 | | var --- a variable such as a, foo, etc 60 | | function call = foo(args) --- `get()`, `square(3)"` 61 | | (expr) --- bracketed expression `(2 + 3) * 2` 62 | | unary operation --- "!" 63 | 64 | args ::= 65 | | list of expr (expr,expr) --- arguments passed to a function call 66 | | expr 67 | 68 | params ::= 69 | | type identifier --- a parameter of a function declaration 70 | | type identifier','params --- parameters of a function declaration 71 | 72 | stmts ::= 73 | | stmt stmts --- statements are a list of statement 74 | | epsilon --- stop the looping 75 | 76 | 77 | function ::= 78 | | fun type identifier params { stmts } --- declaration of a function, returns the last computed value 79 | 80 | type ::= 81 | | int --- variable can be of three types at the moment int, boolean, fun 82 | | boolean 83 | | fun 84 | ``` 85 | -------------------------------------------------------------------------------- /benchmarks/add.dw: -------------------------------------------------------------------------------- 1 | int a := 3 + 10 2 | int b := 8 - 2 * 3 3 | b = b + a 4 | print b -------------------------------------------------------------------------------- /benchmarks/if.dw: -------------------------------------------------------------------------------- 1 | int a := 3 * 5 + 8 2 | int b := 4 3 | if a > b then { 4 | b = 5 5 | print "a is bigger than " <> "b" 6 | print b 7 | } 8 | else { 9 | print "b is bigger than a" 10 | } 11 | 12 | fun add := (int c,int d) -> c + d + b 13 | 14 | print add(3,2) -------------------------------------------------------------------------------- /benchmarks/while.dw: -------------------------------------------------------------------------------- 1 | int b := 7 2 | b = b + 3 3 | 4 | 5 | while b > 1 do { 6 | print b 7 | b = b - 1 8 | } -------------------------------------------------------------------------------- /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 :dwarf, key: :value 14 | # 15 | # and access this configuration in your application as: 16 | # 17 | # Application.get_env(:dwarf, :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 | -------------------------------------------------------------------------------- /dwarf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicolasdilley/dwarf-interpreter/e3d2ded035dd45a0647bbeae776dce74352ab95e/dwarf -------------------------------------------------------------------------------- /lib/cli.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarf.CLI do 2 | alias Dwarf.{Lexer, Parser, Evaluator} 3 | 4 | @moduledoc """ 5 | Dwarf is a small language that only contain very few operations. 6 | 7 | The operations are : +,-,*,/,% 8 | The languages can support assignment on variable and multiple statements. 9 | 10 | To compile and run a file : 11 | 12 | call "./dwarf nameOfFile.dw" 13 | then "./nameOfFile.s" 14 | 15 | """ 16 | 17 | @doc """ 18 | Compiles the file passed in the args. 19 | 20 | """ 21 | def main(args \\ []) do 22 | {:ok, source} = File.read(List.first(args)) 23 | 24 | source 25 | # turns the source programs into a list of tokens 26 | |> Lexer.lex(1) 27 | # turns the list of tokens into an AST 28 | |> Parser.parse() 29 | # Inspect the output 30 | |> Evaluator.eval([]) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/env.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarf.Env do 2 | @moduledoc """ 3 | The environment is a list of all the declared variables and functions and their value. 4 | 5 | vars contained in the env have the form 6 | {name,{:fun,_,[args,stmt]}} 7 | {name,{:num,line,[value]}} 8 | {name,{:bool,line,[value]}} 9 | {name,{:string,line,[value]}} 10 | """ 11 | 12 | def add(env, {:fun, line, [name, args, stmt]}) do 13 | {updated, newEnv} = updateVal(env, {name, {:fun, line, [name, args, stmt]}}) 14 | 15 | if !updated do 16 | [{name, {:fun, line, [name, args, stmt]}} | newEnv] 17 | else 18 | newEnv 19 | end 20 | end 21 | 22 | def add(env, {:dec, line, [type, name, val]}) do 23 | {updated, newEnv} = updateVal(env, {name, {type, line, [name, val]}}) 24 | 25 | if !updated do 26 | [{name, {type, line, [name, val]}} | newEnv] 27 | else 28 | raise "Dynamic error: var #{name} was already declared can not declare twice the same variable. on line #{ 29 | line 30 | }" 31 | end 32 | end 33 | 34 | # update the value of varName in the environment 35 | # if varName does not exists raise an error 36 | def update(env, {:assign, line, [varName, val]}) do 37 | {updated, new_env} = 38 | List.foldl(env, {false, []}, fn var, {updated, newEnv} -> 39 | case var do 40 | {a, {type, _, [_, _]}} when varName == a -> 41 | {true, [{varName, {type, line, [varName, val]}} | newEnv]} 42 | 43 | x -> 44 | {updated, [x | newEnv]} 45 | end 46 | end) 47 | 48 | if !updated do 49 | raise "Dynamic error: Trying to assign to var #{varName} but it has not been declared yet." 50 | end 51 | 52 | new_env 53 | end 54 | 55 | def addArg(env, {:param, line, [type, name]}, val) do 56 | {updated, newEnv} = updateVal(env, {name, {type, line, [name, val]}}) 57 | 58 | if !updated do 59 | [{name, {type, line, [name, val]}} | env] 60 | else 61 | newEnv 62 | end 63 | end 64 | 65 | def get(env, varName) do 66 | var = 67 | Enum.find(env, fn x -> 68 | case x do 69 | {name, _} -> varName == name 70 | end 71 | end) 72 | 73 | case var do 74 | nil -> 75 | {false, {}} 76 | 77 | # returning the value of a var 78 | {_, {_, _, [_name, value]}} -> 79 | {true, value} 80 | 81 | # returning the function 82 | {_, a} -> 83 | {true, a} 84 | end 85 | end 86 | 87 | # Update the value if it is contained in the env and return a tuple with the first value 88 | # Telling if the env has been updated or not and the changed or unchanged env 89 | defp updateVal([], _), do: {false, []} 90 | 91 | defp updateVal(env, {name, {type, line, [name, val]}}) do 92 | List.foldl(env, {false, []}, fn var, {updated, newEnv} -> 93 | case var do 94 | {a, _} when name == a -> 95 | {true, [{name, {type, line, [name, val]}} | newEnv]} 96 | 97 | x -> 98 | {updated, [x | newEnv]} 99 | end 100 | end) 101 | end 102 | 103 | defp updateVal(env, {name, {type, line, [name, args, stmt]}}) do 104 | List.foldl(env, {false, []}, fn var, {updated, newEnv} -> 105 | case var do 106 | {_, a, _, _} when name == a -> 107 | {true, [{name, {type, line, [name, args, stmt]}} | newEnv]} 108 | 109 | x -> 110 | {updated, [x | newEnv]} 111 | end 112 | end) 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/evaluator.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarf.Evaluator do 2 | alias Dwarf.Env 3 | 4 | @moduledoc """ 5 | The evaluator look at the IR respresented as a list of ASTs and evaluate each of them in order. 6 | The evaluator expect that the IR has been typed checked staticly before hand. 7 | """ 8 | 9 | # Loop through each tree and evaluates them by giving them previous environment 10 | def eval(trees, env) do 11 | # time_before = Time.utc_now() 12 | 13 | List.foldl(trees, env, fn tree, prev_env -> 14 | {_, new_env} = eval_stmt(tree, prev_env) 15 | new_env 16 | end) 17 | 18 | # time_after = Time.utc_now() 19 | # IO.puts("end of eval : " <> to_string(Time.diff(time_before, time_after)) <> "s.") 20 | end 21 | 22 | defp eval_stmt(tree, env) do 23 | case tree do 24 | {:fun, line, args} -> 25 | {{:string, line, [List.first(args)]}, Env.add(env, tree)} 26 | 27 | {:dec, line, args} -> 28 | [type, name, expr] = args 29 | new_expr = eval_exp(expr, env) 30 | {new_expr, Env.add(env, {:dec, line, [type, name, new_expr]})} 31 | 32 | {:if, _, _} -> 33 | eval_if(tree, env) 34 | 35 | {:assign, line, args} -> 36 | [name, exp] = args 37 | new_expr = eval_exp(exp, env) 38 | new_env = Env.update(env, {:assign, line, [name, new_expr]}) 39 | {new_expr, new_env} 40 | 41 | {:while,_,args} -> 42 | {new_expr,new_env} = eval_while(args,env) 43 | {new_expr,remove_local_vars(env,new_env)} 44 | 45 | {:print, _, [expr]} -> 46 | new_expr = eval_exp(expr, env) 47 | IO.puts(new_expr) 48 | {new_expr, env} 49 | 50 | x when is_list(x) -> 51 | List.foldl(x, {nil,env}, fn tree, {_,prev_env} -> 52 | {exp, new_tree} = eval_stmt(tree, prev_env) 53 | {exp, new_tree} 54 | end) 55 | 56 | x -> 57 | {eval_exp(x, env), env} 58 | end 59 | end 60 | 61 | defp eval_while([expr,stmt],env) do 62 | expr2 = eval_exp(expr,env) 63 | case expr2 do 64 | false -> 65 | {expr2,new_env} = eval_stmt(stmt,env) 66 | {expr2,remove_local_vars(env,new_env)} 67 | _ -> 68 | {_,new_env} = eval_stmt(stmt,env) 69 | eval_while([expr,stmt],remove_local_vars(env,new_env)) 70 | end 71 | end 72 | 73 | defp eval_exp(tree, env) do 74 | case tree do 75 | {:uop, _, [exp]} -> 76 | !eval_exp(exp, env) 77 | 78 | {:op, _, _} -> 79 | eval_op(tree, env) 80 | 81 | {:num, _, args} -> 82 | List.first(args) 83 | 84 | {:string, _, args} -> 85 | List.first(args) 86 | 87 | {:bool, _, args} -> 88 | List.first(args) 89 | 90 | {:var, line, args} -> 91 | {result, value} = Env.get(env, List.first(args)) 92 | 93 | if !result do 94 | raise "Dynamic error : var #{List.first(args)} has not been declared on line #{line}" 95 | end 96 | 97 | value 98 | 99 | {:call, _, _} -> 100 | eval_fun(tree, env) 101 | end 102 | end 103 | 104 | defp eval_op(tree, env) do 105 | case tree do 106 | {:op, _, [:add, exp1, exp2]} -> eval_exp(exp1, env) + eval_exp(exp2, env) 107 | {:op, _, [:sub, exp1, exp2]} -> eval_exp(exp1, env) - eval_exp(exp2, env) 108 | {:op, _, [:div, exp1, exp2]} -> eval_exp(exp1, env) / eval_exp(exp2, env) 109 | {:op, _, [:mul, exp1, exp2]} -> eval_exp(exp1, env) * eval_exp(exp2, env) 110 | {:op, _, [:mod, exp1, exp2]} -> rem(eval_exp(exp1, env), eval_exp(exp2, env)) 111 | {:op, _, [:ge, exp1, exp2]} -> eval_exp(exp1, env) >= eval_exp(exp2, env) 112 | {:op, _, [:gt, exp1, exp2]} -> eval_exp(exp1, env) > eval_exp(exp2, env) 113 | {:op, _, [:se, exp1, exp2]} -> eval_exp(exp1, env) <= eval_exp(exp2, env) 114 | {:op, _, [:st, exp1, exp2]} -> eval_exp(exp1, env) < eval_exp(exp2, env) 115 | {:op, _, [:equality, exp1, exp2]} -> eval_exp(exp1, env) == eval_exp(exp2, env) 116 | {:op, _, [:concat, exp1, exp2]} -> eval_exp(exp1, env) <> eval_exp(exp2, env) 117 | end 118 | end 119 | 120 | defp eval_fun({:call, line, [name, args]}, env) do 121 | {valid, fun_spec} = Env.get(env, name) 122 | 123 | if !valid do 124 | raise "Dynamic error : Could not find a function with name #{name} on line : #{line}" 125 | end 126 | 127 | {:fun, _, [name, params, stmt]} = fun_spec 128 | 129 | if length(args) != length(params) do 130 | raise "Dynamic error : Trying to invoque function #{name} with #{length(args)} arguments while #{ 131 | name 132 | } expects #{length(params)} on line : #{line}" 133 | end 134 | 135 | {_, new_env, _} = 136 | List.foldl(params, {0, env, args}, fn param, {pos, old_env, old_args} -> 137 | # get and remove the arg from the list 138 | {{_type, _, [val]}, rest} = List.pop_at(old_args, 0) 139 | {pos + 1, Env.addArg(old_env, param, val), rest} 140 | end) 141 | 142 | {return_value, _} = eval_stmt(stmt, new_env) 143 | return_value 144 | end 145 | 146 | defp eval_fun({a, line, _}, _), 147 | do: raise("Dynamic error: Expected a call received #{to_string(a)} on line #{line}") 148 | 149 | defp eval_if({:if, _, [expr, stmt1, stmt2]}, env) do 150 | exp = eval_exp(expr, env) 151 | if(exp) do 152 | {new_expr, new_env} = eval_stmt(stmt1, env) 153 | 154 | # return the last expression done in the stmt and the env with the local var stripped 155 | {new_expr, remove_local_vars(env,new_env)} 156 | else 157 | {new_expr, new_env} = eval_stmt(stmt2, env) 158 | 159 | # return the last expression done in the stmt and the env with the local var stripped 160 | {new_expr, remove_local_vars(env,new_env)} 161 | end 162 | end 163 | 164 | defp eval_if(_, _), do: raise("Dynamic error : Expected an if statement") 165 | 166 | # Remove the addional variables that appears in new_env and not in old_env 167 | defp remove_local_vars(old_env,new_env) do 168 | Enum.reduce(old_env,[], 169 | fn old_var, acc -> 170 | 171 | {old_name,_} = old_var 172 | 173 | new_var = Enum.find(new_env, 174 | fn new_var -> 175 | {new_name,_} = new_var 176 | old_name == new_name 177 | end) 178 | 179 | if new_var != nil do 180 | [new_var|acc] 181 | else 182 | [old_var|acc] 183 | end 184 | end) 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /lib/lexer.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarf.Lexer do 2 | @moduledoc """ 3 | The lexer generates tokens from an input string. 4 | The tokens are incoded as 3-tuples where the first represent the name of the token, 5 | the second part is the line on which it appears and the last part are a list of arguments. 6 | IE. {:num,2,[10]} = the number '10' that appears on '2' 7 | 8 | tokens are : 9 | {:concat,_line,[]} -> the concat symbols "<>" 10 | {:op,_line,[:add]} -> Binary operation such as + - / * >= < == 11 | {:uop,,_line,[:not]} -> Unary operation Not 12 | {:ident,_line,["name of ident"]} -> an identification to a variable a := 3 Ident(a,{int,3}) 13 | {:num,_line,[10000]} -> a number 14 | {:string,_line,["string"]} -> a string "string" 15 | {:true,_line,[true]} -> the keyword "true" 16 | {:false,_line,[false]} -> the keyword "false" 17 | {:int,_line,[]} -> the integer type 18 | {:bool,_line,[]} -> the boolean type 19 | {:fun,_line,[]} -> the fun type 20 | {:dec,_line,[]} -> the symbol ":=" which is used to declare a new var or fun (IE.int a := 4) 21 | {:coma,_line,[]} -> a coma "," 22 | {:print,_line,[]} -> Output a value to the command line print 4 ("4") 23 | {:if,_line,[]} -> the keyword "if" 24 | {:then,_line,[]} -> the keyword "then" 25 | {:else,_line,[]} -> The keyword "else" 26 | {:arrow,_line,[]} -> the arrow symbole '->' 27 | {:lbracket,_line,[]},{:rbracket} -> "()" 28 | {:lcurly,_line,[]},{:rcurly} -> "{}" 29 | {:eq,_line,[]} -> "=" 30 | {:while,_line,[]} -> "while" 31 | {:do,_line,[]} -> "do" 32 | 33 | """ 34 | 35 | @doc """ 36 | ## Examples 37 | 38 | iex> Tokens.lex("thisShouldWork") 39 | true 40 | iex> Tokens.lex("9ThisShouldntWork") 41 | false 42 | 43 | """ 44 | @type op :: {:op, atom} 45 | @type uop :: {:uop, atom} 46 | @type str_token :: {String.t(), tuple} 47 | 48 | @spec lex(String.t(), integer) :: list 49 | def lex(toLex, num_lines) do 50 | # Create the list of keywords 51 | keywords = keywords() 52 | ident_re = ~r(^[a-zA-Z]\w*) 53 | number_re = ~r(^[0-9]+) 54 | space_re = ~r(^[ \h]+) 55 | newline_re = ~r(^[\n\\|\r\n]) 56 | string_re = ~r/^"(?:[^"\\]|\\.)*"/ 57 | 58 | cond do 59 | toLex == "" -> 60 | [] 61 | 62 | Regex.match?(space_re, toLex) -> 63 | lex(Regex.replace(space_re, toLex, "", global: false), num_lines) 64 | 65 | Regex.match?(newline_re, toLex) -> 66 | lex(Regex.replace(newline_re, toLex, "", global: false), num_lines + 1) 67 | 68 | Regex.match?(number_re, toLex) -> 69 | num = String.to_integer(List.first(Regex.run(number_re, toLex, [{:capture, :first}]))) 70 | 71 | [ 72 | {:num, num_lines, [num]} 73 | | lex(Regex.replace(number_re, toLex, "", global: false), num_lines) 74 | ] 75 | 76 | Regex.match?(string_re, toLex) -> 77 | string = List.first(Regex.run(string_re, toLex, [{:capture, :first}])) 78 | sliced_string = String.slice(string, 1, String.length(string) - 2) 79 | 80 | [ 81 | {:string, num_lines, [sliced_string]} 82 | | lex(Regex.replace(string_re, toLex, "", global: false), num_lines) 83 | ] 84 | 85 | true -> 86 | {result, str_token} = containsKeyword(toLex, keywords) 87 | 88 | cond do 89 | result -> 90 | case str_token do 91 | {str, {a, b}} -> 92 | [{a, num_lines, [b]} | lex(String.replace_leading(toLex, str, ""), num_lines)] 93 | 94 | {str, {a}} -> 95 | [{a, num_lines, []} | lex(String.replace_leading(toLex, str, ""), num_lines)] 96 | end 97 | 98 | Regex.match?(ident_re, toLex) -> 99 | id = List.first(Regex.run(ident_re, toLex, [{:capture, :first}])) 100 | token = {:ident, num_lines, [id]} 101 | [token | lex(Regex.replace(ident_re, toLex, "", global: false), num_lines)] 102 | 103 | true -> 104 | raise "could not parse : #{toLex} on line #{num_lines}" 105 | end 106 | end 107 | end 108 | 109 | # return a string representation of the token 110 | def show_token(token) do 111 | case token do 112 | # the arrow symbol -> 113 | {:arrow} -> 114 | "->" 115 | 116 | # operation such as + - 117 | {:op, _} -> 118 | show_op(token) 119 | 120 | # unary operation not 121 | {:uop, _} -> 122 | show_uop(token) 123 | 124 | # just a number like 1,2,3 125 | {:num, a} -> 126 | to_string(a) 127 | 128 | # just a string like "foo", "bar" 129 | {:string, a} -> 130 | a 131 | 132 | # the boolean true 133 | {true} -> 134 | "true" 135 | 136 | # the boolean false 137 | {false} -> 138 | "false" 139 | 140 | # ident == identifier such as name of var or function 141 | {:ident, nameOfIdent} -> 142 | nameOfIdent 143 | 144 | # the type "int" 145 | {:type, :int} -> 146 | "int" 147 | 148 | # lambda 149 | {:type, :fun} -> 150 | "fun" 151 | 152 | # the type "boolean" 153 | {:type, :bool} -> 154 | "boolean" 155 | 156 | # The type "String" -> 157 | {:type, :string} -> 158 | "string" 159 | 160 | # a print statement 161 | {:print} -> 162 | "print" 163 | 164 | # assign 165 | # := a var declaration 166 | {:dec} -> 167 | ":=" 168 | 169 | # = used for assignment 170 | {:eq} -> 171 | "=" 172 | 173 | # flow statement 174 | {:if} -> 175 | "if" 176 | 177 | {:else} -> 178 | "else" 179 | 180 | {:then} -> 181 | "then" 182 | 183 | # syntax brackets 184 | {:lcurly} -> 185 | "{" 186 | 187 | {:rcurly} -> 188 | "}" 189 | 190 | {:lbracket} -> 191 | "(" 192 | 193 | {:rbracket} -> 194 | ")" 195 | 196 | {:coma} -> 197 | "," 198 | {:while} -> 199 | "while" 200 | {:do} -> 201 | "do" 202 | 203 | {a} -> 204 | raise "Lexer Error : have received #{to_string(a)}, the lexer does not recognize this input" 205 | end 206 | end 207 | 208 | # shows the operator as a string from the token 209 | @spec show_op(op) :: String.t() 210 | defp show_op({:op, operator}) do 211 | case operator do 212 | :concat -> "<>" 213 | :sub -> "-" 214 | :add -> "+" 215 | :div -> "/" 216 | :mul -> "*" 217 | :mod -> "%" 218 | :gt -> ">" 219 | :ge -> ">=" 220 | :se -> "<=" 221 | :st -> "<" 222 | :equality -> "==" 223 | _ -> raise "Lexer Error : Unknown Binary operator : #{operator}" 224 | end 225 | end 226 | 227 | # if a non-operation token is given returns an error 228 | defp show_op({a}) do 229 | atom_str = to_string({a}) 230 | raise "Lexer Error : not a binary operation : #{atom_str}" 231 | end 232 | 233 | # return a string representation 234 | @spec show_uop(uop) :: String.t() 235 | defp show_uop({:uop, operator}) do 236 | case operator do 237 | :not -> "!" 238 | _ -> raise "Lexer Error : Unknown Unary operator : #{operator}" 239 | end 240 | end 241 | 242 | defp show_uop({a}) do 243 | atom_str = to_string(a) 244 | raise "not a unary operation : #{atom_str}" 245 | end 246 | 247 | # returns a list of all the keywords of the language 248 | @spec keywords() :: [str_token] 249 | defp keywords() do 250 | tokens = [ 251 | {:arrow}, 252 | {:uop, :not}, 253 | {:op, :concat}, 254 | {:op, :add}, 255 | {:op, :mul}, 256 | {:op, :sub}, 257 | {:op, :div}, 258 | {:op, :equality}, 259 | {:op, :ge}, 260 | {:op, :gt}, 261 | {:op, :se}, 262 | {:op, :st}, 263 | {true}, 264 | {false}, 265 | {:dec}, 266 | {:eq}, 267 | {:print}, 268 | {:type, :bool}, 269 | {:type, :int}, 270 | {:type, :fun}, 271 | {:type, :string}, 272 | {:if}, 273 | {:else}, 274 | {:then}, 275 | {:lcurly}, 276 | {:rcurly}, 277 | {:lbracket}, 278 | {:rbracket}, 279 | {:coma}, 280 | {:while}, 281 | {:do} 282 | ] 283 | 284 | token_to_map = fn a -> {show_token(a), a} end 285 | Enum.map(tokens, token_to_map) 286 | end 287 | 288 | @spec containsKeyword(String.t(), list) :: {boolean, tuple} 289 | defp containsKeyword(input, keywords) do 290 | Enum.reduce_while(keywords, {}, fn {key, val}, _ -> 291 | if !String.starts_with?(input, key) do 292 | {:cont, {false, {}}} 293 | else 294 | {:halt, {true, {key, val}}} 295 | end 296 | end) 297 | end 298 | end 299 | -------------------------------------------------------------------------------- /lib/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Dwarf.Parser do 2 | @moduledoc """ 3 | The parser takes a list of tokens and genarates an AST trees from it. 4 | The parse is a decent recursive parser. 5 | This AST tree will be further optimized by the optimizer. 6 | 7 | type of stmt = 8 | {:assign,line,[nameOfVar,expr]} -> type nameOfVar := expr 9 | {:print,line,[expr]} -> print expr 10 | {:call,line,[nameOfFunction,args]} -> add(args) 11 | {:dec,line,[type,ident,expr]} -> type ident := expr 12 | {:if,line,[expr,true_stmt,false_stmt]} -> if expr then true_stmt else false_stmt 13 | {:fun,line,[name,args,stmt]} -> fun name := (args) -> stmt 14 | {:while,line,[expr,stmt]} -> 'while' expr 'do' stmt 15 | 16 | type of expr = 17 | {:op,line,[expr,operator,expr2} = expr op expr2 18 | {:uop,line,[:not,expr]} = !expr 19 | {:var,line,[identifier]} -> var like a, foo, bar 20 | 21 | type of param = 22 | {:param,line,[type,name]} 23 | """ 24 | 25 | @doc """ 26 | Parses a list of tokens into an AST tree 27 | """ 28 | 29 | def parse([]), do: [] 30 | 31 | def parse(tokens) do 32 | {stmt, rest} = parse_stmt(tokens) 33 | [stmt | parse(rest)] 34 | end 35 | 36 | @spec parse_stmt(list) :: {list, list} 37 | defp parse_stmt([token | rest]) do 38 | case token do 39 | # parse a function declaration 40 | {:type, _, [:fun]} -> 41 | parse_fun_dec([token | rest]) 42 | 43 | # parse a var declaration (int i := 3) 44 | {:type, _, _} -> 45 | parse_var_dec([token | rest]) 46 | 47 | # parse an if statement 48 | {:if, _, _} -> 49 | parse_if_stmt([token | rest]) 50 | 51 | # print statement 52 | {:print, _, _} -> 53 | parse_print([token | rest]) 54 | 55 | # while statement 56 | {:while,_,_} -> 57 | parse_while([token |rest ]) 58 | 59 | # assignmment such as a = a + 4 60 | {:ident, _, _} -> 61 | case List.first(rest) do 62 | {:eq, _, _} -> parse_assign([token | rest]) 63 | _ -> parse_exp([token | rest]) 64 | end 65 | 66 | # parse a list of statements 67 | {:lcurly, _, _} -> 68 | parse_stmts([token | rest]) 69 | 70 | {a, line, _} -> 71 | raise "Parsing error : unexpected #{to_string(a)} on line : #{line}" 72 | end 73 | end 74 | 75 | defp parse_stmt([]), do: [] 76 | 77 | defp parse_factor([token | rest]) do 78 | case token do 79 | {:num, _, _} -> 80 | {token, rest} 81 | 82 | {:string, _, _} -> 83 | {token, rest} 84 | 85 | {true, line, [val]} -> 86 | {{:bool, line, [val]}, rest} 87 | 88 | {false, line, val} -> 89 | {{:bool, line, [val]}, rest} 90 | 91 | {:uop, line, [op]} -> 92 | {expr, rest1} = parse_factor(rest) 93 | {{:uop, line, [op, expr]}, rest1} 94 | 95 | {:ident, line, [a]} -> 96 | case rest do 97 | [{:lbracket, _, _} | _] -> parse_func([token | rest]) 98 | _ -> {{:var, line, [a]}, rest} 99 | end 100 | 101 | {:lbracket, _, _} -> 102 | {expr, rest1} = parse_exp(rest) 103 | 104 | case rest1 do 105 | [{:rbracket, _, _} | rest2] -> 106 | {expr, rest2} 107 | 108 | [{_, line, _} | _] -> 109 | raise "Parsing error: expected a ) on line #{line}" 110 | 111 | [] -> 112 | raise "Parsing error: unexpected end of file in a bracketised expression" 113 | end 114 | 115 | {a, line, _} -> 116 | raise "Parsing Error : expected a factor but got #{to_string(a)} on line #{line}" 117 | end 118 | end 119 | 120 | # look for the rest of operators with medium precedence 121 | defp parse_term([]), do: raise("Parsing error : End of file while trying to parse a term") 122 | 123 | defp parse_term(tokens) do 124 | {l_expr, rest} = parse_factor(tokens) 125 | 126 | case rest do 127 | [{:op, line, [:mul]} | rest1] -> 128 | {r_expr, rest2} = parse_term(rest1) 129 | {{:op, line, [:mul, l_expr, r_expr]}, rest2} 130 | 131 | [{:op, line, [:div]} | rest1] -> 132 | {r_expr, rest2} = parse_term(rest1) 133 | {{:op, line, [:div, l_expr, r_expr]}, rest2} 134 | 135 | [{:op, line, [:mod]} | rest1] -> 136 | {r_expr, rest2} = parse_term(rest1) 137 | {{:op, line, [:mod, l_expr, r_expr]}, rest2} 138 | 139 | _ -> 140 | {l_expr, rest} 141 | end 142 | end 143 | 144 | # look for operator with low precedence like addition or substraction 145 | # and then look for term 146 | defp parse_op(tokens) do 147 | {l_expr, rest} = parse_term(tokens) 148 | 149 | case rest do 150 | [{:op, line, [:add]} | rest1] -> 151 | {r_expr, rest2} = parse_op(rest1) 152 | {{:op, line, [:add, l_expr, r_expr]}, rest2} 153 | 154 | [{:op, line, [:sub]} | rest1] -> 155 | {r_expr, rest2} = parse_op(rest1) 156 | {{:op, line, [:sub, l_expr, r_expr]}, rest2} 157 | 158 | [{:op, line, [:ge]} | rest1] -> 159 | {r_expr, rest2} = parse_op(rest1) 160 | {{:op, line, [:ge, l_expr, r_expr]}, rest2} 161 | 162 | [{:op, line, [:gt]} | rest1] -> 163 | {r_expr, rest2} = parse_op(rest1) 164 | {{:op, line, [:gt, l_expr, r_expr]}, rest2} 165 | 166 | [{:op, line, [:se]} | rest1] -> 167 | {r_expr, rest2} = parse_op(rest1) 168 | {{:op, line, [:se, l_expr, r_expr]}, rest2} 169 | 170 | [{:op, line, [:st]} | rest1] -> 171 | {r_expr, rest2} = parse_op(rest1) 172 | {{:op, line, [:st, l_expr, r_expr]}, rest2} 173 | 174 | [{:op, line, [:equality]} | rest1] -> 175 | {r_expr, rest2} = parse_op(rest1) 176 | {{:op, line, [:equality, l_expr, r_expr]}, rest2} 177 | 178 | _ -> 179 | {l_expr, rest} 180 | end 181 | end 182 | 183 | defp parse_exp([]), do: raise("Parsing error: End of file while trying to parse an expression") 184 | @spec parse_exp(list) :: {tuple, list} 185 | defp parse_exp(tokens) do 186 | {l_expr, rest} = parse_op(tokens) 187 | 188 | case rest do 189 | [{:op, line, [:concat]} | rest1] -> 190 | {r_expr, rest2} = parse_exp(rest1) 191 | {{:op, line, [:concat, l_expr, r_expr]}, rest2} 192 | 193 | _ -> 194 | {l_expr, rest} 195 | end 196 | end 197 | 198 | # Parse a list of statement and return a list of statements and the rest of the tokens. 199 | @spec parse_stmts(list) :: {list, list} 200 | defp parse_stmts([token | rest]) do 201 | case token do 202 | {:rcurly, _, _} -> 203 | {[], rest} 204 | {:lcurly,_,_} -> 205 | {stmt, rest1} = parse_stmt(rest) 206 | {stmts, rest2} = parse_stmts(rest1) 207 | 208 | {[stmt | stmts], rest2} 209 | _ -> 210 | {stmt, rest1} = parse_stmt([token|rest]) 211 | {stmts, rest2} = parse_stmts(rest1) 212 | 213 | {[stmt | stmts], rest2} 214 | 215 | end 216 | end 217 | 218 | defp parse_func([token | rest]) do 219 | case token do 220 | {:ident, line, [a]} -> 221 | case rest do 222 | [{:lbracket, _, _} | [{:rbracket, _, _} | rest1]] -> 223 | {{:call, line, [a, []]}, rest1} 224 | 225 | [{:lbracket, _, _} | rest1] -> 226 | {args, rest2} = parse_args(rest1) 227 | 228 | case rest2 do 229 | [{:rbracket, line, _} | rest3] -> {{:call, line, [a, args]}, rest3} 230 | [{_, line, _}, _] -> raise "Parser error : Expected a ) on line #{line}" 231 | end 232 | 233 | [{_, line, _} | _] -> 234 | raise "Parser error : parsing a function without a ( on line #{line}" 235 | end 236 | 237 | {_, line, _} -> 238 | raise "Parser error : Expected an identifier for a function on line #{line}" 239 | end 240 | end 241 | 242 | # parses the args of a function such as add(2+3,8) 243 | defp parse_args([token | rest]) do 244 | {node, rest1} = parse_exp([token | rest]) 245 | 246 | case rest1 do 247 | [{:coma, _, _} | rest2] -> 248 | {args, rest3} = parse_args(rest2) 249 | {[node | args], rest3} 250 | 251 | _ -> 252 | {[node], rest1} 253 | end 254 | end 255 | 256 | defp parse_args([]), do: raise("Parsing Error: end of file while parsing args") 257 | 258 | @spec parse_var_dec(list) :: {tuple, list} 259 | defp parse_var_dec([token | rest]) do 260 | case rest do 261 | [] -> 262 | {a, line, _} = token 263 | 264 | raise "Parsing error : unexpected end of file in var dec after #{to_string(a)} on line : #{ 265 | line 266 | }" 267 | 268 | [{:ident, _, [a]} | rest1] -> 269 | case rest1 do 270 | [{:dec, _, _} | rest2] -> 271 | {exp, rest3} = parse_exp(rest2) 272 | {_type, line, [type]} = token 273 | {{:dec, line, [type, a, exp]}, rest3} 274 | 275 | [{a, line, _} | _] -> 276 | raise "Parsing error : expected := but got #{to_string(a)} on line #{line}" 277 | end 278 | 279 | [{a, line, _} | _] -> 280 | raise "Parsing error : expected a var name but got #{to_string(a)} on line : #{line}" 281 | end 282 | end 283 | 284 | defp parse_var_dec([]), 285 | do: raise("Parsing Error: end of file while parsing a variable declaration") 286 | 287 | # Parse a function declaration 288 | # To create a function "fun nameOfFunction := (int param1, fun param2) -> expr" 289 | defp parse_fun_dec([token | rest]) do 290 | case token do 291 | {:type, line, [:fun]} -> 292 | case rest do 293 | [{:ident, _, [function_name]} | rest1] -> 294 | case rest1 do 295 | [{:dec, _, _} | rest2] -> 296 | case rest2 do 297 | [{:lbracket, _, _} | [{:rbracket, _, _} | rest3]] -> 298 | {expr, rest4} = parse_exp(rest3) 299 | {{:fun, line, [function_name, [], expr]}, rest4} 300 | 301 | [{:lbracket, _, _} | rest1] -> 302 | {args, rest2} = parse_params(rest1) 303 | 304 | case rest2 do 305 | [{:rbracket, _, _} | [{:arrow, _, _} | rest3]] -> 306 | {expr, rest4} = parse_stmt(rest3) 307 | {{:fun, line, [function_name, args, expr]}, rest4} 308 | 309 | [{_, line} | _] -> 310 | raise "Parser error : Expected a ) on line #{line}" 311 | end 312 | 313 | [{_, line, _} | _] -> 314 | raise "Parser error : parsing a function without a ( on line #{line}" 315 | end 316 | 317 | [{a, line, _} | _] -> 318 | raise "Parsing error : expected a := name received : #{to_string(a)} on line : #{ 319 | line 320 | }" 321 | end 322 | 323 | [{a, line, _} | _] -> 324 | raise "Parsing error : Expected a function name received : #{to_string(a)} on line : #{ 325 | line 326 | }" 327 | end 328 | 329 | {_, line, _} -> 330 | raise "Parsing error : Trying to parse a function declaration without starting with 'fun' on line #{ 331 | line 332 | }" 333 | end 334 | end 335 | 336 | defp parse_params([]), do: raise("Parsing error : End of file expected a type") 337 | 338 | defp parse_params([token | rest]) do 339 | case token do 340 | {:type, line, [type]} -> 341 | case rest do 342 | [{:ident, _, [name]} | rest1] -> 343 | case rest1 do 344 | [{:coma, _, _} | rest2] -> 345 | {args, rest3} = parse_params(rest2) 346 | {[{:param, line, [type, name]} | args], rest3} 347 | 348 | _ -> 349 | {[{:param, line, [type, name]}], rest1} 350 | end 351 | end 352 | 353 | {a, line, _} -> 354 | raise "Parsing error : Expected a type received #{to_string(a)} on line : #{line}" 355 | end 356 | end 357 | 358 | defp parse_if_stmt([token | rest]) do 359 | case token do 360 | {:if, line, _} -> 361 | {expr, rest1} = parse_exp(rest) 362 | 363 | case rest1 do 364 | [{:then, _, _} | rest2] -> 365 | {true_stmt, rest3} = parse_stmt(rest2) 366 | 367 | case rest3 do 368 | [{:else, _, _} | rest4] -> 369 | {false_stmt, rest5} = parse_stmt(rest4) 370 | {{:if, line, [expr, true_stmt, false_stmt]}, rest5} 371 | 372 | [{a, line, _} | _] -> 373 | raise "Parsing error : Expected 'else' received : #{to_string(a)} on line : #{ 374 | line 375 | }" 376 | end 377 | 378 | [{_, line, _} | _] -> 379 | raise "Parsing error : Expected 'then' on line #{line}" 380 | 381 | [] -> 382 | raise "Parsing error : End of file expected then" 383 | end 384 | 385 | {a, line, _} -> 386 | raise "Parsing error : Expected 'if' received : #{to_string(a)} on line : #{line}" 387 | end 388 | end 389 | 390 | 391 | defp parse_while([{:while,line,_}| rest]) do 392 | {expr,rest1} = parse_exp(rest) 393 | case rest1 do 394 | [{:do,_,_}|rest2] -> 395 | {stmt,rest3} = parse_stmt(rest2) 396 | {{:while,line,[expr,stmt]},rest3} 397 | [{a,line,_}|_] -> 398 | raise "Parsing error : Expected 'do' received : #{to_string(a)} on line : #{line}" 399 | end 400 | end 401 | defp parse_while([{a,line,_}|_]), do: raise "Parsing error : Expected 'for' received : #{to_string(a)} on line : #{line}" 402 | defp parse_print([token | rest]) do 403 | case token do 404 | {:print, line, _} -> 405 | {exp, rest1} = parse_exp(rest) 406 | {{:print, line, [exp]}, rest1} 407 | 408 | {a, line, _} -> 409 | raise "Parsing error : expected a print got a #{to_string(a)} on line : #{line}" 410 | end 411 | end 412 | 413 | 414 | defp parse_assign([token | rest]) do 415 | case token do 416 | {:ident, line, [ident]} -> 417 | case rest do 418 | [{:eq, _, _} | rest1] -> 419 | {expr, rest2} = parse_exp(rest1) 420 | {{:assign, line, [ident, expr]}, rest2} 421 | 422 | [{a, line, _} | _] -> 423 | raise "Parsing error : expected an = but got : #{to_string(a)} on line : #{line}" 424 | end 425 | 426 | {a, line, _} -> 427 | raise "Parsing error : expected a variable name received : #{to_string(a)} on line : #{ 428 | line 429 | }" 430 | end 431 | end 432 | end 433 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Dwarf.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :dwarf, 7 | version: "0.1.0", 8 | elixir: "~> 1.4.5", 9 | start_permanent: Mix.env() == :prod, 10 | escript: escript(), 11 | deps: deps() 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | extra_applications: [:logger] 19 | ] 20 | end 21 | 22 | def escript do 23 | [main_module: Dwarf.CLI] 24 | end 25 | 26 | # Run "mix help deps" to learn about dependencies. 27 | defp deps do 28 | [ 29 | {:dialyxir, "~> 0.5", only: [:dev], runtime: false} 30 | # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}, 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, 3 | } 4 | -------------------------------------------------------------------------------- /test/dwarf_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DwarfTest do 2 | use ExUnit.Case 3 | doctest Dwarf 4 | 5 | test "greets the world" do 6 | assert Dwarf.hello() == :world 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------