├── .formatter.exs ├── .github └── workflows │ └── exunit.yml ├── .gitignore ├── README.md ├── config └── config.exs ├── lib ├── mix │ └── tasks │ │ └── repl.ex ├── monkey.ex └── monkey │ ├── ast │ ├── array_literal.ex │ ├── block_statement.ex │ ├── boolean.ex │ ├── call_expression.ex │ ├── expression_statement.ex │ ├── function_literal.ex │ ├── hash_literal.ex │ ├── identifier.ex │ ├── if_expression.ex │ ├── index_expression.ex │ ├── infix_expression.ex │ ├── integer_literal.ex │ ├── let_statement.ex │ ├── node.ex │ ├── prefix_expression.ex │ ├── program.ex │ ├── return_statement.ex │ └── string_literal.ex │ ├── evaluator.ex │ ├── evaluator │ └── builtins.ex │ ├── lexer.ex │ ├── object │ ├── array.ex │ ├── boolean.ex │ ├── builtin.ex │ ├── environment.ex │ ├── error.ex │ ├── function.ex │ ├── hash.ex │ ├── integer.ex │ ├── null.ex │ ├── object.ex │ ├── return_value.ex │ └── string.ex │ ├── parser.ex │ ├── repl.ex │ └── token.ex ├── mix.exs └── test ├── monkey ├── evaluator_test.exs ├── lexer_test.exs ├── object │ └── hash_key_test.exs └── parser_test.exs └── test_helper.exs /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.github/workflows/exunit.yml: -------------------------------------------------------------------------------- 1 | name: ExUnit Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup elixir 15 | uses: erlef/setup-beam@v1 16 | with: 17 | elixir-version: 1.14.3 18 | otp-version: 25.2.1 19 | - name: Run Tests 20 | run: mix test 21 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Writing An Interpreter In Elixir 2 | 3 | [![Build Status](https://travis-ci.org/fabrik42/writing_an_interpreter_in_elixir.svg?branch=master)](https://travis-ci.org/fabrik42/writing_an_interpreter_in_elixir) 4 | 5 | --- 6 | 7 | ## Introduction 8 | 9 | This project is an interpreter for the [Monkey](https://interpreterbook.com/index.html#the-monkey-programming-language) programming language, featuring its own lexer, AST producing parser and evaluator. The Monkey programming language as well as the structure of the interpreter are based on the book [Writing An Interpreter In Go](https://interpreterbook.com/). 10 | 11 | I really enjoyed reading this book and following the implementation of the interpreter in Go, because it was built using simple and straightforward patterns. You learn to build an interpreter completely by yourself, no generators, no external dependencies, just you and your code. :) 12 | 13 | And even though - at first sight - an interpreter seems to be a big, hairy, complex machine, the author manages to implement a fully working version using easy to understand code. 14 | 15 | Patterns like early returns, simple for-loops and small amounts of well encapsulated state are well known to most programmers and easy to reason about. 16 | 17 | However, none of these things exist in Elixir and I was looking to get my hands dirty with this language. So I decided to implement the interpreter for Monkey in Elixir instead of Go, to see which other patterns can be applied to solve the problems with a more functional approach. 18 | 19 | I know that there are way more sophisticated patterns in functional languages, like monads, but I wanted to solve this problem using only the standard lib of Elixir. First of all, because the reference implementation in Golang did the same and second, because I wanted to dive deeper into the standard lib. 20 | 21 | It is important to keep in mind, that the whole basic structure of the interpreter is derived from the original Golang book. So maybe there are even better ways to lay out an interpreter in Elixir. If this is the case, let me know! :) 22 | 23 | The code in this repository is the current state of this interpreter. It is fully functional, tested and I tried my best to implement it using Elixir best practices. There are still some rough edges and I think there is still a lot to learn for me, about Elixir and functional programming in general. 24 | 25 | You are very welcome to walk through this code and if you spot something that could be done better, [please let me know](https://github.com/fabrik42/writing_an_interpreter_in_elixir/issues/new). 26 | 27 | ## Monkey Language Features 28 | 29 | A list of language features can be found [here](https://interpreterbook.com/index.html#the-monkey-programming-language). 30 | 31 | You should be able to try them all out in the REPL (see below). 32 | 33 | ## Usage 34 | 35 | This project only uses the stdlib of Elixir, so no `mix deps.get` is necessary. 36 | 37 | ### Starting the REPL 38 | 39 | There is a handy mix task to start a Monkey REPL so you can play around with the language. 40 | 41 | ``` 42 | mix repl 43 | ``` 44 | 45 | ### Running the tests 46 | 47 | ``` 48 | mix test 49 | ``` 50 | 51 | ## Notable changes to the original Golang implementation 52 | 53 | ### Token 54 | 55 | * Does not use constants but atoms to specify a token type. 56 | 57 | ### Lexer 58 | 59 | * Does not maintain state and is implemented using recursive function calls. 60 | 61 | ### Parser 62 | 63 | * Prefix parse functions are defined using pattern matching in function calls instead of a lookup table with functions as values and extra nil handling. 64 | 65 | ### Evaluator 66 | 67 | * As structs are immutable, the evaluator will not only output the evaluated result, but also the environment that was used to evaluate the code (defined variables). 68 | * As a consequence, the evaluator will also be called with an environment to work with, so it can resume working in the same environment (see REPL). 69 | 70 | ## TODOs 71 | 72 | * Check code quality, Elixir ways of doing things 73 | * Add Typespecs and Dialyzer for static analysis 74 | * Try to remove TODOs aka nested conditions 75 | * Maybe more builtin functions 76 | -------------------------------------------------------------------------------- /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 | import 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 for your application as: 12 | # 13 | # config :monkey, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:monkey, :key) 18 | # 19 | # Or 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 | -------------------------------------------------------------------------------- /lib/mix/tasks/repl.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Repl do 2 | use Mix.Task 3 | alias Monkey.Repl 4 | 5 | def run(_) do 6 | user = "whoami" |> System.cmd([]) |> elem(0) |> String.trim_trailing() 7 | 8 | IO.puts("Hello #{user}! This is the Monkey programming language!") 9 | IO.puts("Feel free to type in commands") 10 | Repl.loop() 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/monkey.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey do 2 | @moduledoc """ 3 | Elixir implementation of the Monkey programming language. 4 | """ 5 | end 6 | -------------------------------------------------------------------------------- /lib/monkey/ast/array_literal.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.ArrayLiteral do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :elements] 5 | defstruct [ 6 | :token, 7 | # expression[] 8 | :elements 9 | ] 10 | 11 | defimpl Node, for: __MODULE__ do 12 | def token_literal(expression), do: expression.token.literal 13 | 14 | def node_type(_), do: :expression 15 | 16 | def to_string(expression) do 17 | elements = 18 | expression.elements 19 | |> Enum.map(&Node.to_string/1) 20 | |> Enum.join(", ") 21 | 22 | "[#{elements}]" 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/monkey/ast/block_statement.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.BlockStatement do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :statements] 5 | defstruct [:token, :statements] 6 | 7 | defimpl Node, for: __MODULE__ do 8 | def token_literal(expression), do: expression.token.literal 9 | 10 | def node_type(_), do: :expression 11 | 12 | def to_string(expression), 13 | do: expression.statements |> Enum.map(&Node.to_string/1) |> Enum.join() 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/monkey/ast/boolean.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.BooleanLiteral do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :value] 5 | defstruct [:token, :value] 6 | 7 | defimpl Node, for: __MODULE__ do 8 | def token_literal(expression), do: expression.token.literal 9 | 10 | def node_type(_), do: :expression 11 | 12 | def to_string(expression), do: expression.token.literal 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/monkey/ast/call_expression.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.CallExpression do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :function, :arguments] 5 | defstruct [ 6 | :token, 7 | # expression (identifier or function literal) 8 | :function, 9 | # expression[] 10 | :arguments 11 | ] 12 | 13 | defimpl Node, for: __MODULE__ do 14 | def token_literal(expression), do: expression.token.literal 15 | 16 | def node_type(_), do: :expression 17 | 18 | def to_string(expression) do 19 | function = Node.to_string(expression.function) 20 | 21 | arguments = 22 | expression.arguments 23 | |> Enum.map(&Node.to_string/1) 24 | |> Enum.join(", ") 25 | 26 | "#{function}(#{arguments})" 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/monkey/ast/expression_statement.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.ExpressionStatement do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :expression] 5 | defstruct [:token, :expression] 6 | 7 | defimpl Node, for: __MODULE__ do 8 | def token_literal(statement), do: statement.token.literal 9 | 10 | def node_type(_), do: :statement 11 | 12 | def to_string(%{expression: nil}), do: "" 13 | def to_string(statement), do: Node.to_string(statement.expression) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/monkey/ast/function_literal.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.FunctionLiteral do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :parameters, :body] 5 | defstruct [ 6 | :token, 7 | # identifier[] 8 | :parameters, 9 | # block statement 10 | :body 11 | ] 12 | 13 | defimpl Node, for: __MODULE__ do 14 | def token_literal(expression), do: expression.token.literal 15 | 16 | def node_type(_), do: :expression 17 | 18 | def to_string(expression) do 19 | literal = Node.token_literal(expression) 20 | body = Node.to_string(expression.body) 21 | 22 | params = 23 | expression.parameters 24 | |> Enum.map(&Node.to_string/1) 25 | |> Enum.join(", ") 26 | 27 | "#{literal}(#{params})#{body}" 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/monkey/ast/hash_literal.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.HashLiteral do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :pairs] 5 | defstruct [ 6 | :token, 7 | # %{expression => expression} 8 | :pairs 9 | ] 10 | 11 | defimpl Node, for: __MODULE__ do 12 | def token_literal(expression), do: expression.token.literal 13 | 14 | def node_type(_), do: :expression 15 | 16 | def to_string(expression) do 17 | pairs = 18 | expression.pairs 19 | |> Enum.map(fn {key, value} -> 20 | "#{Node.to_string(key)}:#{Node.to_string(value)}" 21 | end) 22 | |> Enum.join(", ") 23 | 24 | "{#{pairs}}" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/monkey/ast/identifier.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.Identifier do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :value] 5 | defstruct [:token, :value] 6 | 7 | defimpl Node, for: __MODULE__ do 8 | def token_literal(identifier), do: identifier.token.literal 9 | 10 | def node_type(_), do: :expression 11 | 12 | def to_string(identifier), do: identifier.value 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/monkey/ast/if_expression.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.IfExpression do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :condition, :consequence] 5 | defstruct [ 6 | :token, 7 | # expression 8 | :condition, 9 | # block statement 10 | :consequence, 11 | # block statement 12 | :alternative 13 | ] 14 | 15 | defimpl Node, for: __MODULE__ do 16 | def token_literal(expression), do: expression.token.literal 17 | 18 | def node_type(_), do: :expression 19 | 20 | def to_string(expression) do 21 | condition = Node.to_string(expression.condition) 22 | consequence = Node.to_string(expression.consequence) 23 | alternative = alternative_to_string(expression.alternative) 24 | "if#{condition} #{consequence}#{alternative}" 25 | end 26 | 27 | defp alternative_to_string(nil), do: "" 28 | 29 | defp alternative_to_string(alternative) do 30 | string = Node.to_string(alternative) 31 | "else #{string}" 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/monkey/ast/index_expression.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.IndexExpression do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :left, :index] 5 | defstruct [ 6 | :token, 7 | # expression 8 | :left, 9 | # expression 10 | :index 11 | ] 12 | 13 | defimpl Node, for: __MODULE__ do 14 | def token_literal(expression), do: expression.token.literal 15 | 16 | def node_type(_), do: :expression 17 | 18 | def to_string(expression) do 19 | left = Node.to_string(expression.left) 20 | index = Node.to_string(expression.index) 21 | "(#{left}[#{index}])" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/monkey/ast/infix_expression.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.InfixExpression do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :left, :operator, :right] 5 | defstruct [ 6 | # the operator token 7 | :token, 8 | # expression 9 | :left, 10 | :operator, 11 | # expression 12 | :right 13 | ] 14 | 15 | defimpl Node, for: __MODULE__ do 16 | def token_literal(expression), do: expression.token.literal 17 | 18 | def node_type(_), do: :expression 19 | 20 | def to_string(expression) do 21 | left = Node.to_string(expression.left) 22 | operator = expression.operator 23 | right = Node.to_string(expression.right) 24 | "(#{left} #{operator} #{right})" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/monkey/ast/integer_literal.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.IntegerLiteral do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :value] 5 | defstruct [:token, :value] 6 | 7 | defimpl Node, for: __MODULE__ do 8 | def token_literal(literal), do: literal.token.literal 9 | 10 | def node_type(_), do: :expression 11 | 12 | def to_string(literal), do: literal.token.literal 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/monkey/ast/let_statement.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.LetStatement do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :name, :value] 5 | defstruct [ 6 | :token, 7 | # identifier 8 | :name, 9 | # expression 10 | :value 11 | ] 12 | 13 | defimpl Node, for: __MODULE__ do 14 | def token_literal(statement), do: statement.token.literal 15 | 16 | def node_type(_), do: :statement 17 | 18 | def to_string(statement) do 19 | out = [ 20 | Node.token_literal(statement), 21 | " ", 22 | Node.to_string(statement.name), 23 | " = " 24 | ] 25 | 26 | out = if statement.value, do: out ++ [Node.to_string(statement.value)], else: out 27 | Enum.join(out) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/monkey/ast/node.ex: -------------------------------------------------------------------------------- 1 | defprotocol Monkey.Ast.Node do 2 | @doc "Returns the literal value of the token" 3 | def token_literal(node) 4 | 5 | @doc "The type of the node, either :statement or :expression" 6 | def node_type(node) 7 | 8 | @doc "Prints the node as a string" 9 | def to_string(node) 10 | end 11 | -------------------------------------------------------------------------------- /lib/monkey/ast/prefix_expression.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.PrefixExpression do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :operator, :right] 5 | defstruct [ 6 | # the prefix token 7 | :token, 8 | :operator, 9 | # expression 10 | :right 11 | ] 12 | 13 | defimpl Node, for: __MODULE__ do 14 | def token_literal(expression), do: expression.token.literal 15 | 16 | def node_type(_), do: :expression 17 | 18 | def to_string(expression) do 19 | operator = expression.operator 20 | right = Node.to_string(expression.right) 21 | "(#{operator}#{right})" 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/monkey/ast/program.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.Program do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:statements] 5 | defstruct [:statements] 6 | 7 | def token_literal(program) do 8 | if length(program.statements) > 0 do 9 | program.statements 10 | |> List.first() 11 | |> Node.token_literal() 12 | else 13 | "" 14 | end 15 | end 16 | 17 | def to_string(program), do: program.statements |> Enum.map(&Node.to_string/1) |> Enum.join() 18 | end 19 | -------------------------------------------------------------------------------- /lib/monkey/ast/return_statement.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.ReturnStatement do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :return_value] 5 | defstruct [:token, :return_value] 6 | 7 | defimpl Node, for: __MODULE__ do 8 | def token_literal(statement), do: statement.token.literal 9 | 10 | def node_type(_), do: :statement 11 | 12 | def to_string(%{return_value: nil} = statement) do 13 | Node.token_literal(statement) 14 | end 15 | 16 | def to_string(statement) do 17 | literal = Node.token_literal(statement) 18 | value = Node.to_string(statement.return_value) 19 | "#{literal} #{value}" 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/monkey/ast/string_literal.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Ast.StringLiteral do 2 | alias Monkey.Ast.Node 3 | 4 | @enforce_keys [:token, :value] 5 | defstruct [:token, :value] 6 | 7 | defimpl Node, for: __MODULE__ do 8 | def token_literal(literal), do: literal.token.literal 9 | 10 | def node_type(_), do: :expression 11 | 12 | def to_string(literal), do: literal.token.literal 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/monkey/evaluator.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Evaluator do 2 | alias Monkey.Ast.{ 3 | ArrayLiteral, 4 | BlockStatement, 5 | BooleanLiteral, 6 | CallExpression, 7 | ExpressionStatement, 8 | FunctionLiteral, 9 | HashLiteral, 10 | Identifier, 11 | IfExpression, 12 | IndexExpression, 13 | InfixExpression, 14 | IntegerLiteral, 15 | LetStatement, 16 | PrefixExpression, 17 | Program, 18 | ReturnStatement, 19 | StringLiteral 20 | } 21 | 22 | alias Monkey.Evaluator.Builtins 23 | 24 | alias Monkey.Object.{ 25 | Array, 26 | Boolean, 27 | Builtin, 28 | Environment, 29 | Error, 30 | Function, 31 | Hash, 32 | Integer, 33 | Null, 34 | Object, 35 | ReturnValue, 36 | String 37 | } 38 | 39 | @cached_true %Boolean{value: true} 40 | @cached_false %Boolean{value: false} 41 | @cached_null %Null{} 42 | 43 | def eval(%Program{} = ast_node, env), do: eval_program(ast_node, env) 44 | def eval(%ExpressionStatement{} = ast_node, env), do: eval(ast_node.expression, env) 45 | 46 | def eval(%ReturnStatement{} = ast_node, env) do 47 | {value, env} = eval(ast_node.return_value, env) 48 | 49 | cond do 50 | is_error(value) -> 51 | {value, env} 52 | 53 | true -> 54 | value = %ReturnValue{value: value} 55 | {value, env} 56 | end 57 | end 58 | 59 | def eval(%LetStatement{} = ast_node, env) do 60 | {value, env} = eval(ast_node.value, env) 61 | 62 | cond do 63 | is_error(value) -> 64 | {value, env} 65 | 66 | true -> 67 | env = Environment.set(env, ast_node.name.value, value) 68 | {value, env} 69 | end 70 | end 71 | 72 | def eval(%IntegerLiteral{} = ast_node, env) do 73 | value = %Integer{value: ast_node.value} 74 | {value, env} 75 | end 76 | 77 | def eval(%BooleanLiteral{} = ast_node, env) do 78 | value = from_native_bool(ast_node.value) 79 | {value, env} 80 | end 81 | 82 | def eval(%StringLiteral{} = ast_node, env) do 83 | value = %String{value: ast_node.value} 84 | {value, env} 85 | end 86 | 87 | def eval(%PrefixExpression{} = ast_node, env) do 88 | {right, env} = eval(ast_node.right, env) 89 | 90 | cond do 91 | is_error(right) -> 92 | {right, env} 93 | 94 | true -> 95 | value = eval_prefix_expression(ast_node.operator, right) 96 | {value, env} 97 | end 98 | end 99 | 100 | def eval(%InfixExpression{} = ast_node, env) do 101 | {left, env} = eval(ast_node.left, env) 102 | {right, env} = eval(ast_node.right, env) 103 | 104 | cond do 105 | is_error(left) -> 106 | {left, env} 107 | 108 | is_error(right) -> 109 | {right, env} 110 | 111 | true -> 112 | value = eval_infix_expression(ast_node.operator, left, right) 113 | {value, env} 114 | end 115 | end 116 | 117 | def eval(%BlockStatement{} = ast_node, env) do 118 | eval_block_statement(ast_node, env) 119 | end 120 | 121 | def eval(%IfExpression{} = ast_node, env) do 122 | eval_if_expression(ast_node, env) 123 | end 124 | 125 | def eval(%Identifier{} = ast_node, env) do 126 | value = Environment.get(env, ast_node.value) 127 | builtin = Builtins.get(ast_node.value) 128 | 129 | cond do 130 | value -> {value, env} 131 | builtin -> {builtin, env} 132 | true -> {error("identifier not found: #{ast_node.value}"), env} 133 | end 134 | end 135 | 136 | def eval(%FunctionLiteral{} = ast_node, env) do 137 | params = ast_node.parameters 138 | body = ast_node.body 139 | value = %Function{parameters: params, body: body, environment: env} 140 | {value, env} 141 | end 142 | 143 | def eval(%CallExpression{} = ast_node, env) do 144 | {function, env} = eval(ast_node.function, env) 145 | 146 | case function do 147 | %Error{} -> 148 | {function, env} 149 | 150 | _ -> 151 | {args, env} = eval_expressions(ast_node.arguments, env) 152 | 153 | if length(args) == 1 && is_error(Enum.at(args, 0)) do 154 | value = Enum.at(args, 0) 155 | {value, env} 156 | else 157 | value = apply_function(function, args) 158 | {value, env} 159 | end 160 | end 161 | end 162 | 163 | def eval(%ArrayLiteral{} = ast_node, env) do 164 | {elements, env} = eval_expressions(ast_node.elements, env) 165 | 166 | if length(elements) == 1 && is_error(Enum.at(elements, 0)) do 167 | value = Enum.at(elements, 0) 168 | {value, env} 169 | else 170 | value = %Array{elements: elements} 171 | {value, env} 172 | end 173 | end 174 | 175 | def eval(%IndexExpression{} = ast_node, env) do 176 | {left, env} = eval(ast_node.left, env) 177 | {index, env} = eval(ast_node.index, env) 178 | 179 | cond do 180 | is_error(left) -> 181 | {left, env} 182 | 183 | is_error(index) -> 184 | {index, env} 185 | 186 | true -> 187 | value = eval_index_expression(left, index) 188 | {value, env} 189 | end 190 | end 191 | 192 | def eval(%HashLiteral{} = ast_node, env) do 193 | eval_hash_literal(ast_node, env) 194 | end 195 | 196 | defp eval_program(program, env, last_evaluated \\ nil) do 197 | do_eval_program(program.statements, env, last_evaluated) 198 | end 199 | 200 | defp do_eval_program([], env, last_evaluated), do: {last_evaluated, env} 201 | 202 | defp do_eval_program([statement | rest], env, _evaluated) do 203 | {value, env} = eval(statement, env) 204 | 205 | case value do 206 | %ReturnValue{} -> {value.value, env} 207 | %Error{} -> {value, env} 208 | _ -> do_eval_program(rest, env, value) 209 | end 210 | end 211 | 212 | defp eval_block_statement(block, env, last_evaluated \\ nil) do 213 | do_eval_block_statement(block.statements, env, last_evaluated) 214 | end 215 | 216 | defp do_eval_block_statement([], env, last_evaluated), do: {last_evaluated, env} 217 | 218 | defp do_eval_block_statement([statement | rest], env, _last_evaluated) do 219 | {value, env} = eval(statement, env) 220 | 221 | case value do 222 | %ReturnValue{} -> {value, env} 223 | %Error{} -> {value, env} 224 | _ -> do_eval_block_statement(rest, env, value) 225 | end 226 | end 227 | 228 | defp eval_expressions(expressions, env) do 229 | {evaluated, env} = 230 | Enum.reduce_while(expressions, {[], env}, fn expression, {acc, env} -> 231 | {value, env} = eval(expression, env) 232 | 233 | case value do 234 | %Error{} -> {:halt, {value, env}} 235 | _ -> {:cont, {[value | acc], env}} 236 | end 237 | end) 238 | 239 | evaluated = Enum.reverse(evaluated) 240 | {evaluated, env} 241 | end 242 | 243 | defp eval_prefix_expression(operator, right) do 244 | case operator do 245 | "!" -> eval_bang_operator_expression(right) 246 | "-" -> eval_minus_operator_expression(right) 247 | _ -> error("unknown operator: #{operator}#{Object.type(right)}") 248 | end 249 | end 250 | 251 | defp eval_bang_operator_expression(right) do 252 | case right do 253 | @cached_true -> @cached_false 254 | @cached_false -> @cached_true 255 | @cached_null -> @cached_true 256 | _ -> @cached_false 257 | end 258 | end 259 | 260 | defp eval_minus_operator_expression(right) do 261 | case right do 262 | %Integer{} -> %Integer{value: -right.value} 263 | _ -> error("unknown operator: -#{Object.type(right)}") 264 | end 265 | end 266 | 267 | defp eval_infix_expression(operator, %Integer{} = left, %Integer{} = right), 268 | do: eval_integer_infix_expression(operator, left, right) 269 | 270 | defp eval_infix_expression(operator, %String{} = left, %String{} = right), 271 | do: eval_string_infix_expression(operator, left, right) 272 | 273 | defp eval_infix_expression("==", left, right), do: from_native_bool(left == right) 274 | defp eval_infix_expression("!=", left, right), do: from_native_bool(left != right) 275 | 276 | defp eval_infix_expression(operator, left, right) do 277 | left_type = Object.type(left) 278 | right_type = Object.type(right) 279 | 280 | message = 281 | if left_type != right_type do 282 | "type mismatch: #{left_type} #{operator} #{right_type}" 283 | else 284 | "unknown operator: #{left_type} #{operator} #{right_type}" 285 | end 286 | 287 | error(message) 288 | end 289 | 290 | defp eval_integer_infix_expression(operator, left, right) do 291 | case operator do 292 | "+" -> %Integer{value: left.value + right.value} 293 | "-" -> %Integer{value: left.value - right.value} 294 | "*" -> %Integer{value: left.value * right.value} 295 | "/" -> %Integer{value: round(left.value / right.value)} 296 | "<" -> from_native_bool(left.value < right.value) 297 | ">" -> from_native_bool(left.value > right.value) 298 | "==" -> from_native_bool(left.value == right.value) 299 | "!=" -> from_native_bool(left.value != right.value) 300 | _ -> error("unknown operator: #{Object.type(left)} #{operator} #{Object.type(right)}") 301 | end 302 | end 303 | 304 | defp eval_string_infix_expression(operator, left, right) do 305 | case operator do 306 | "+" -> %String{value: left.value <> right.value} 307 | _ -> error("unknown operator: #{Object.type(left)} #{operator} #{Object.type(right)}") 308 | end 309 | end 310 | 311 | defp eval_if_expression(expression, env) do 312 | {condition, env} = eval(expression.condition, env) 313 | 314 | cond do 315 | is_error(condition) -> {condition, env} 316 | is_truthy(condition) -> eval(expression.consequence, env) 317 | expression.alternative != nil -> eval(expression.alternative, env) 318 | true -> {@cached_null, env} 319 | end 320 | end 321 | 322 | defp eval_index_expression(%Array{} = left, %Integer{} = index), 323 | do: eval_array_index_expression(left, index) 324 | 325 | defp eval_index_expression(%Hash{} = left, index), do: eval_hash_index_expression(left, index) 326 | 327 | defp eval_index_expression(left, _), 328 | do: error("index operator not supported: #{Object.type(left)}") 329 | 330 | defp eval_array_index_expression(array, index) do 331 | idx = index.value 332 | 333 | cond do 334 | idx < 0 -> @cached_null 335 | true -> Enum.at(array.elements, idx, @cached_null) 336 | end 337 | end 338 | 339 | defp eval_hash_index_expression(hash, index) do 340 | key = Hash.Hashable.hash(index) 341 | 342 | pair = 343 | cond do 344 | is_error(key) -> error("unusable as hash key: #{Object.type(index)}") 345 | true -> Map.get(hash.pairs, key, @cached_null) 346 | end 347 | 348 | case pair do 349 | %Error{} -> pair 350 | @cached_null -> pair 351 | _ -> pair.value 352 | end 353 | end 354 | 355 | defp eval_hash_literal(ast_node, env) do 356 | pairs = Map.to_list(ast_node.pairs) 357 | eval_hash_pair(pairs, env, %{}) 358 | end 359 | 360 | defp eval_hash_pair([] = _pairs, env, evaluated_pairs) do 361 | hash = %Hash{pairs: evaluated_pairs} 362 | {hash, env} 363 | end 364 | 365 | defp eval_hash_pair([pair | rest] = _pairs, env, evaluated_pairs) do 366 | {key, value} = pair 367 | 368 | {key, env} = eval(key, env) 369 | {value, env} = eval(value, env) 370 | hash_key = Hash.Hashable.hash(key) 371 | 372 | cond do 373 | is_error(key) -> 374 | {key, env} 375 | 376 | is_error(value) -> 377 | {value, env} 378 | 379 | is_error(hash_key) -> 380 | {hash_key, env} 381 | 382 | true -> 383 | hash_pair = %Hash.Pair{key: key, value: value} 384 | evaluated_pairs = Map.put(evaluated_pairs, hash_key, hash_pair) 385 | eval_hash_pair(rest, env, evaluated_pairs) 386 | end 387 | end 388 | 389 | defp apply_function(%Function{} = function, args) do 390 | extended_env = extended_function_env(function, args) 391 | {value, _env} = eval(function.body, extended_env) 392 | unwrap_return_value(value) 393 | end 394 | 395 | defp apply_function(%Builtin{} = function, args), do: function.fn.(args) 396 | defp apply_function(function, _), do: error("not a function: #{Object.type(function)}") 397 | 398 | defp extended_function_env(function, args) do 399 | env = Environment.build_enclosed(function.environment) 400 | pairs = Enum.zip(function.parameters, args) 401 | 402 | List.foldl(pairs, env, fn {identifier, arg}, env -> 403 | Environment.set(env, identifier.value, arg) 404 | end) 405 | end 406 | 407 | defp unwrap_return_value(obj) do 408 | case obj do 409 | %ReturnValue{} -> obj.value 410 | _ -> obj 411 | end 412 | end 413 | 414 | defp is_truthy(obj) do 415 | case obj do 416 | @cached_true -> true 417 | @cached_false -> false 418 | @cached_null -> false 419 | _ -> true 420 | end 421 | end 422 | 423 | defp is_error(%Error{}), do: true 424 | defp is_error(_), do: false 425 | 426 | defp error(message), do: %Error{message: message} 427 | 428 | defp from_native_bool(true), do: @cached_true 429 | defp from_native_bool(false), do: @cached_false 430 | end 431 | -------------------------------------------------------------------------------- /lib/monkey/evaluator/builtins.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Evaluator.Builtins do 2 | alias Monkey.Object.{ 3 | Builtin, 4 | Error, 5 | Null, 6 | Object 7 | } 8 | 9 | def get("len"), do: %Builtin{fn: &len/1} 10 | def get("puts"), do: %Builtin{fn: &puts/1} 11 | def get(_), do: nil 12 | 13 | def len([arg] = args) when length(args) == 1 do 14 | case arg do 15 | %Monkey.Object.String{} -> 16 | result = String.length(arg.value) 17 | %Monkey.Object.Integer{value: result} 18 | 19 | %Monkey.Object.Array{} -> 20 | result = length(arg.elements) 21 | %Monkey.Object.Integer{value: result} 22 | 23 | _ -> 24 | error("argument to `len` not supported, got #{Object.type(arg)}") 25 | end 26 | end 27 | 28 | def len(args), do: error("wrong number of arguments. got=#{length(args)}, want=1") 29 | 30 | def puts(args) do 31 | Enum.each(args, fn arg -> 32 | arg 33 | |> Object.inspect() 34 | |> IO.puts() 35 | end) 36 | 37 | %Null{} 38 | end 39 | 40 | defp error(message), do: %Error{message: message} 41 | end 42 | -------------------------------------------------------------------------------- /lib/monkey/lexer.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Lexer do 2 | alias Monkey.Token 3 | 4 | def tokenize(input) do 5 | chars = String.split(input, "", trim: true) 6 | tokenize(chars, []) 7 | end 8 | 9 | defp tokenize(_chars = [], tokens) do 10 | Enum.reverse([Token.new(type: :eof, literal: "") | tokens]) 11 | end 12 | 13 | defp tokenize(chars = [ch | rest], tokens) do 14 | cond do 15 | is_whitespace(ch) -> tokenize(rest, tokens) 16 | is_letter(ch) -> read_identifier(chars, tokens) 17 | is_digit(ch) -> read_number(chars, tokens) 18 | is_two_char_operator(chars) -> read_two_char_operator(chars, tokens) 19 | is_quote(ch) -> read_string(chars, tokens) 20 | true -> read_next_char(chars, tokens) 21 | end 22 | end 23 | 24 | defp read_identifier(chars, tokens) do 25 | {identifier, rest} = Enum.split_while(chars, &is_letter/1) 26 | identifier = Enum.join(identifier) 27 | token = Token.new(type: Token.lookup_ident(identifier), literal: identifier) 28 | 29 | tokenize(rest, [token | tokens]) 30 | end 31 | 32 | defp read_number(chars, tokens) do 33 | {number, rest} = Enum.split_while(chars, &is_digit/1) 34 | number = Enum.join(number) 35 | token = Token.new(type: :int, literal: number) 36 | 37 | tokenize(rest, [token | tokens]) 38 | end 39 | 40 | defp read_two_char_operator(chars, tokens) do 41 | {literal, rest} = Enum.split(chars, 2) 42 | literal = Enum.join(literal) 43 | 44 | token = 45 | case literal do 46 | "==" -> Token.new(type: :eq, literal: literal) 47 | "!=" -> Token.new(type: :not_eq, literal: literal) 48 | end 49 | 50 | tokenize(rest, [token | tokens]) 51 | end 52 | 53 | def read_string([_quote | rest], tokens) do 54 | {string, [_quote | rest]} = Enum.split_while(rest, &(!is_quote(&1))) 55 | string = Enum.join(string) 56 | token = Token.new(type: :string, literal: string) 57 | 58 | tokenize(rest, [token | tokens]) 59 | end 60 | 61 | defp read_next_char(_chars = [ch | rest], tokens) do 62 | token = 63 | case ch do 64 | "=" -> Token.new(type: :assign, literal: ch) 65 | ";" -> Token.new(type: :semicolon, literal: ch) 66 | ":" -> Token.new(type: :colon, literal: ch) 67 | "(" -> Token.new(type: :lparen, literal: ch) 68 | ")" -> Token.new(type: :rparen, literal: ch) 69 | "+" -> Token.new(type: :plus, literal: ch) 70 | "-" -> Token.new(type: :minus, literal: ch) 71 | "!" -> Token.new(type: :bang, literal: ch) 72 | "*" -> Token.new(type: :asterisk, literal: ch) 73 | "/" -> Token.new(type: :slash, literal: ch) 74 | "<" -> Token.new(type: :lt, literal: ch) 75 | ">" -> Token.new(type: :gt, literal: ch) 76 | "," -> Token.new(type: :comma, literal: ch) 77 | "{" -> Token.new(type: :lbrace, literal: ch) 78 | "}" -> Token.new(type: :rbrace, literal: ch) 79 | "[" -> Token.new(type: :lbracket, literal: ch) 80 | "]" -> Token.new(type: :rbracket, literal: ch) 81 | _ -> Token.new(type: :illegal, literal: "") 82 | end 83 | 84 | tokenize(rest, [token | tokens]) 85 | end 86 | 87 | defp is_letter(ch) do 88 | ("a" <= ch && ch <= "z") || ("A" <= ch && ch <= "Z") || ch == "_" 89 | end 90 | 91 | defp is_digit(ch) do 92 | "0" <= ch && ch <= "9" 93 | end 94 | 95 | defp is_whitespace(ch) do 96 | ch == " " || ch == "\n" || ch == "\t" 97 | end 98 | 99 | defp is_quote(ch), do: ch == "\"" 100 | 101 | defp is_two_char_operator(chars) do 102 | (Enum.at(chars, 0) == "!" || Enum.at(chars, 0) == "=") && Enum.at(chars, 1) == "=" 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/monkey/object/array.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.Array do 2 | alias Monkey.Object.Object 3 | 4 | @enforce_keys [:elements] 5 | defstruct [:elements] 6 | 7 | defimpl Object, for: __MODULE__ do 8 | def type(_), do: "ARRAY" 9 | 10 | def inspect(obj) do 11 | elements = 12 | obj.elements 13 | |> Enum.map(&Object.inspect/1) 14 | |> Enum.join(", ") 15 | 16 | "[#{elements}]" 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/monkey/object/boolean.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.Boolean do 2 | alias Monkey.Object.{Object, Hash} 3 | 4 | @enforce_keys [:value] 5 | defstruct [:value] 6 | 7 | defimpl Object, for: __MODULE__ do 8 | def type(_), do: "BOOLEAN" 9 | 10 | def inspect(obj), do: Atom.to_string(obj.value) 11 | end 12 | 13 | defimpl Hash.Hashable, for: __MODULE__ do 14 | def hash(obj) do 15 | value = 16 | case obj.value do 17 | true -> 1 18 | false -> 0 19 | end 20 | 21 | %Hash.Key{type: Object.type(obj), value: value} 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/monkey/object/builtin.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.Builtin do 2 | alias Monkey.Object.Object 3 | 4 | @enforce_keys [:fn] 5 | defstruct [:fn] 6 | 7 | defimpl Object, for: __MODULE__ do 8 | def type(_), do: "BUILTIN" 9 | 10 | def inspect(_), do: "builtin function" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/monkey/object/environment.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.Environment do 2 | @enforce_keys [:store] 3 | defstruct [:store, :outer] 4 | 5 | def build do 6 | %Monkey.Object.Environment{store: %{}} 7 | end 8 | 9 | def build_enclosed(outer) do 10 | env = build() 11 | %{env | outer: outer} 12 | end 13 | 14 | def get(env, name) do 15 | value = Map.get(env.store, name) 16 | 17 | cond do 18 | is_nil(value) && env.outer -> __MODULE__.get(env.outer, name) 19 | true -> value 20 | end 21 | end 22 | 23 | def set(env, name, val), do: %{env | store: Map.put(env.store, name, val)} 24 | end 25 | -------------------------------------------------------------------------------- /lib/monkey/object/error.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.Error do 2 | alias Monkey.Object.Object 3 | 4 | @enforce_keys [:message] 5 | defstruct [:message] 6 | 7 | defimpl Object, for: __MODULE__ do 8 | def type(_), do: "ERROR_OBJ" 9 | 10 | def inspect(obj), do: "ERROR: #{obj.message}" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/monkey/object/function.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.Function do 2 | alias Monkey.Object.Object 3 | alias Monkey.Ast.Node 4 | 5 | @enforce_keys [:parameters, :body, :environment] 6 | defstruct [ 7 | # identifiers[] 8 | :parameters, 9 | # block statement 10 | :body, 11 | :environment 12 | ] 13 | 14 | defimpl Object, for: __MODULE__ do 15 | def type(_), do: "FUNCTION" 16 | 17 | def inspect(obj) do 18 | params = 19 | obj.parameters 20 | |> Enum.map(&Node.to_string/1) 21 | |> Enum.join(", ") 22 | 23 | body = Node.to_string(obj.body) 24 | "fn(#{params}) {#{body}}" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/monkey/object/hash.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.Hash do 2 | alias Monkey.Object.{Error, Object} 3 | 4 | @enforce_keys [:pairs] 5 | defstruct [ 6 | # %{Hash.Key, Hash.Pair} 7 | :pairs 8 | ] 9 | 10 | defmodule Key do 11 | @enforce_keys [:type, :value] 12 | defstruct [:type, :value] 13 | end 14 | 15 | defmodule Pair do 16 | @enforce_keys [:key, :value] 17 | defstruct [:key, :value] 18 | end 19 | 20 | defprotocol Hashable do 21 | @doc "Returns the hash key of the object" 22 | @fallback_to_any true 23 | def hash(obj) 24 | end 25 | 26 | defimpl Hashable, for: Any do 27 | def hash(obj) do 28 | %Error{message: "unusable as hash key: #{Object.type(obj)}"} 29 | end 30 | end 31 | 32 | defimpl Object, for: __MODULE__ do 33 | def type(_), do: "HASH" 34 | 35 | def inspect(obj) do 36 | pairs = 37 | obj.pairs 38 | |> Enum.map(fn {_key, pair} -> 39 | "#{Object.inspect(pair.key)}:#{Object.inspect(pair.value)}" 40 | end) 41 | |> Enum.join(", ") 42 | 43 | "{#{pairs}}" 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/monkey/object/integer.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.Integer do 2 | alias Monkey.Object.{Object, Hash} 3 | 4 | @enforce_keys [:value] 5 | defstruct [:value] 6 | 7 | defimpl Object, for: __MODULE__ do 8 | def type(_), do: "INTEGER" 9 | 10 | def inspect(obj), do: Integer.to_string(obj.value) 11 | end 12 | 13 | defimpl Hash.Hashable, for: __MODULE__ do 14 | def hash(obj) do 15 | %Hash.Key{type: Object.type(obj), value: obj.value} 16 | end 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/monkey/object/null.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.Null do 2 | alias Monkey.Object.Object 3 | 4 | defstruct [] 5 | 6 | defimpl Object, for: __MODULE__ do 7 | def type(_), do: "NULL" 8 | 9 | def inspect(_), do: "null" 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/monkey/object/object.ex: -------------------------------------------------------------------------------- 1 | defprotocol Monkey.Object.Object do 2 | @doc "Returns the type of the object as a string" 3 | def type(obj) 4 | 5 | @doc "Returns the value of the object as a string" 6 | def inspect(obj) 7 | end 8 | -------------------------------------------------------------------------------- /lib/monkey/object/return_value.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.ReturnValue do 2 | alias Monkey.Object.Object 3 | 4 | @enforce_keys [:value] 5 | defstruct [:value] 6 | 7 | defimpl Object, for: __MODULE__ do 8 | def type(_), do: "RETURN_VALUE" 9 | 10 | def inspect(obj), do: Object.inspect(obj.value) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/monkey/object/string.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.String do 2 | alias Monkey.Object.{Object, Hash} 3 | 4 | @enforce_keys [:value] 5 | defstruct [:value] 6 | 7 | defimpl Object, for: __MODULE__ do 8 | def type(_), do: "STRING" 9 | 10 | def inspect(obj), do: obj.value 11 | end 12 | 13 | defimpl Hash.Hashable, for: __MODULE__ do 14 | def hash(obj) do 15 | value = :crypto.hash(:md5, obj.value) |> Base.encode16() 16 | %Hash.Key{type: Object.type(obj), value: value} 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/monkey/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Parser do 2 | alias Monkey.Ast.{ 3 | ArrayLiteral, 4 | BlockStatement, 5 | BooleanLiteral, 6 | CallExpression, 7 | ExpressionStatement, 8 | FunctionLiteral, 9 | HashLiteral, 10 | Identifier, 11 | IfExpression, 12 | IndexExpression, 13 | InfixExpression, 14 | IntegerLiteral, 15 | LetStatement, 16 | PrefixExpression, 17 | Program, 18 | ReturnStatement, 19 | StringLiteral 20 | } 21 | 22 | alias Monkey.Parser 23 | alias Monkey.Token 24 | 25 | @enforce_keys [:curr, :peek, :tokens, :errors] 26 | defstruct [:curr, :peek, :tokens, :errors] 27 | 28 | @precedence_levels %{ 29 | lowest: 0, 30 | equals: 1, 31 | less_greater: 2, 32 | sum: 3, 33 | product: 4, 34 | prefix: 5, 35 | call: 6, 36 | index: 7 37 | } 38 | 39 | @precedences %{ 40 | eq: @precedence_levels.equals, 41 | not_eq: @precedence_levels.equals, 42 | lt: @precedence_levels.less_greater, 43 | gt: @precedence_levels.less_greater, 44 | plus: @precedence_levels.sum, 45 | minus: @precedence_levels.sum, 46 | slash: @precedence_levels.product, 47 | asterisk: @precedence_levels.product, 48 | lparen: @precedence_levels.call, 49 | lbracket: @precedence_levels.index 50 | } 51 | 52 | def from_tokens(tokens) do 53 | [curr | [peek | rest]] = tokens 54 | %Parser{curr: curr, peek: peek, tokens: rest, errors: []} 55 | end 56 | 57 | def parse_program(p, statements \\ []), do: do_parse_program(p, statements) 58 | 59 | defp do_parse_program(%Parser{curr: %Token{type: :eof}} = p, statements) do 60 | statements = Enum.reverse(statements) 61 | program = %Program{statements: statements} 62 | {p, program} 63 | end 64 | 65 | defp do_parse_program(%Parser{} = p, statements) do 66 | {p, statement} = parse_statement(p) 67 | 68 | statements = 69 | case statement do 70 | nil -> statements 71 | statement -> [statement | statements] 72 | end 73 | 74 | p = next_token(p) 75 | 76 | do_parse_program(p, statements) 77 | end 78 | 79 | defp next_token(%Parser{tokens: []} = p) do 80 | %{p | curr: p.peek, peek: nil} 81 | end 82 | 83 | defp next_token(%Parser{} = p) do 84 | [next_peek | rest] = p.tokens 85 | %{p | curr: p.peek, peek: next_peek, tokens: rest} 86 | end 87 | 88 | defp parse_statement(p) do 89 | case p.curr.type do 90 | :let -> parse_let_statement(p) 91 | :return -> parse_return_statement(p) 92 | _ -> parse_expression_statement(p) 93 | end 94 | end 95 | 96 | defp parse_let_statement(p) do 97 | let_token = p.curr 98 | 99 | with {:ok, p, ident_token} <- expect_peek(p, :ident), 100 | {:ok, p, _assign_token} <- expect_peek(p, :assign), 101 | p <- next_token(p), 102 | {:ok, p, value} <- parse_expression(p, @precedence_levels.lowest) do 103 | identifier = %Identifier{token: ident_token, value: ident_token.literal} 104 | statement = %LetStatement{token: let_token, name: identifier, value: value} 105 | p = skip_semicolon(p) 106 | {p, statement} 107 | else 108 | _ -> {p, nil} 109 | end 110 | end 111 | 112 | defp parse_return_statement(p) do 113 | return_token = p.curr 114 | p = next_token(p) 115 | {_, p, return_value} = parse_expression(p, @precedence_levels.lowest) 116 | p = skip_semicolon(p) 117 | statement = %ReturnStatement{token: return_token, return_value: return_value} 118 | {p, statement} 119 | end 120 | 121 | defp parse_expression_statement(p) do 122 | token = p.curr 123 | {_, p, expression} = parse_expression(p, @precedence_levels.lowest) 124 | statement = %ExpressionStatement{token: token, expression: expression} 125 | p = skip_semicolon(p) 126 | {p, statement} 127 | end 128 | 129 | defp parse_expression(p, precedence) do 130 | case prefix_parse_fns(p.curr.type, p) do 131 | {p, nil} -> 132 | {:error, p, nil} 133 | 134 | {p, expression} -> 135 | {p, expression} = check_infix(p, expression, precedence) 136 | {:ok, p, expression} 137 | end 138 | end 139 | 140 | defp check_infix(p, left, precedence) do 141 | allowed = p.peek.type != :semicolon && precedence < peek_precedence(p) 142 | 143 | with true <- allowed, 144 | infix_fn <- infix_parse_fns(p.peek.type), 145 | true <- infix_fn != nil do 146 | p = next_token(p) 147 | {p, infix} = infix_fn.(p, left) 148 | check_infix(p, infix, precedence) 149 | else 150 | _ -> {p, left} 151 | end 152 | end 153 | 154 | defp prefix_parse_fns(:ident, p), do: parse_identifier(p) 155 | defp prefix_parse_fns(:int, p), do: parse_integer_literal(p) 156 | defp prefix_parse_fns(:bang, p), do: parse_prefix_expression(p) 157 | defp prefix_parse_fns(:minus, p), do: parse_prefix_expression(p) 158 | defp prefix_parse_fns(true, p), do: parse_boolean(p) 159 | defp prefix_parse_fns(false, p), do: parse_boolean(p) 160 | defp prefix_parse_fns(:lparen, p), do: parse_grouped_expression(p) 161 | defp prefix_parse_fns(:if, p), do: parse_if_expression(p) 162 | defp prefix_parse_fns(:function, p), do: parse_function_literal(p) 163 | defp prefix_parse_fns(:string, p), do: parse_string_literal(p) 164 | defp prefix_parse_fns(:lbracket, p), do: parse_array_literal(p) 165 | defp prefix_parse_fns(:lbrace, p), do: parse_hash_literal(p) 166 | 167 | defp prefix_parse_fns(_, p) do 168 | error = "No prefix function found for #{p.curr.type}" 169 | p = add_error(p, error) 170 | {p, nil} 171 | end 172 | 173 | defp parse_identifier(p) do 174 | identifier = %Identifier{token: p.curr, value: p.curr.literal} 175 | {p, identifier} 176 | end 177 | 178 | defp parse_integer_literal(p) do 179 | int = Integer.parse(p.curr.literal) 180 | 181 | case int do 182 | :error -> 183 | error = "Could not parse #{p.curr.literal} as integer" 184 | p = add_error(p, error) 185 | {p, nil} 186 | 187 | {val, _} -> 188 | expression = %IntegerLiteral{token: p.curr, value: val} 189 | {p, expression} 190 | end 191 | end 192 | 193 | defp parse_prefix_expression(p) do 194 | token = p.curr 195 | operator = p.curr.literal 196 | 197 | p = next_token(p) 198 | {_, p, right} = parse_expression(p, @precedence_levels.prefix) 199 | prefix = %PrefixExpression{token: token, operator: operator, right: right} 200 | 201 | {p, prefix} 202 | end 203 | 204 | defp parse_boolean(p) do 205 | boolean = %BooleanLiteral{token: p.curr, value: p.curr.type == true} 206 | {p, boolean} 207 | end 208 | 209 | defp parse_grouped_expression(p) do 210 | p = next_token(p) 211 | {_, p, expression} = parse_expression(p, @precedence_levels.lowest) 212 | 213 | case expect_peek(p, :rparen) do 214 | {:error, p, nil} -> {p, nil} 215 | {:ok, p, _} -> {p, expression} 216 | end 217 | end 218 | 219 | defp parse_if_expression(p) do 220 | token = p.curr 221 | 222 | with {:ok, p, _peek} <- expect_peek(p, :lparen), 223 | p <- next_token(p), 224 | {:ok, p, condition} <- parse_expression(p, @precedence_levels.lowest), 225 | {:ok, p, _peek} <- expect_peek(p, :rparen), 226 | {:ok, p, _peek} <- expect_peek(p, :lbrace) do 227 | {p, consequence} = parse_block_statement(p) 228 | {p, alternative} = parse_if_alternative(p) 229 | 230 | expression = %IfExpression{ 231 | token: token, 232 | condition: condition, 233 | consequence: consequence, 234 | alternative: alternative 235 | } 236 | 237 | {p, expression} 238 | else 239 | {:error, p, _} -> {p, nil} 240 | end 241 | end 242 | 243 | defp parse_if_alternative(%Parser{peek: %Token{type: :else}} = p) do 244 | p = next_token(p) 245 | 246 | case expect_peek(p, :lbrace) do 247 | {:error, p, _} -> {p, nil} 248 | {:ok, p, _} -> parse_block_statement(p) 249 | end 250 | end 251 | 252 | defp parse_if_alternative(p), do: {p, nil} 253 | 254 | defp parse_function_literal(p) do 255 | token = p.curr 256 | 257 | with {:ok, p, _peek} <- expect_peek(p, :lparen), 258 | {p, parameters} <- parse_function_parameters(p), 259 | {:ok, p, _peek} <- expect_peek(p, :lbrace) do 260 | {p, body} = parse_block_statement(p) 261 | expression = %FunctionLiteral{token: token, parameters: parameters, body: body} 262 | {p, expression} 263 | else 264 | {:error, p, _} -> {p, nil} 265 | end 266 | end 267 | 268 | defp parse_function_parameters(p, identifiers \\ []) do 269 | do_parse_function_parameters(p, identifiers) 270 | end 271 | 272 | defp do_parse_function_parameters(%Parser{peek: %Token{type: :rparen}} = p, [] = identifiers) do 273 | p = next_token(p) 274 | {p, identifiers} 275 | end 276 | 277 | defp do_parse_function_parameters(p, identifiers) do 278 | p = next_token(p) 279 | identifiers = identifiers ++ [%Identifier{token: p.curr, value: p.curr.literal}] 280 | 281 | case p.peek.type do 282 | :comma -> 283 | p = next_token(p) 284 | do_parse_function_parameters(p, identifiers) 285 | 286 | _ -> 287 | # TODO: no nested case please! 288 | case expect_peek(p, :rparen) do 289 | {:ok, p, _peek} -> {p, identifiers} 290 | {:error, p, nil} -> {p, nil} 291 | end 292 | end 293 | end 294 | 295 | defp parse_block_statement(p, statements \\ []) do 296 | token = p.curr 297 | p = next_token(p) 298 | do_parse_block_statement(p, token, statements) 299 | end 300 | 301 | defp do_parse_block_statement(%Parser{curr: %Token{type: :rbrace}} = p, token, statements) do 302 | statements = Enum.reverse(statements) 303 | block = %BlockStatement{token: token, statements: statements} 304 | {p, block} 305 | end 306 | 307 | defp do_parse_block_statement(%Parser{} = p, token, statements) do 308 | {p, statement} = parse_statement(p) 309 | 310 | statements = 311 | case statement do 312 | nil -> statements 313 | statement -> [statement | statements] 314 | end 315 | 316 | p = next_token(p) 317 | do_parse_block_statement(p, token, statements) 318 | end 319 | 320 | defp parse_string_literal(p) do 321 | expression = %StringLiteral{token: p.curr, value: p.curr.literal} 322 | {p, expression} 323 | end 324 | 325 | defp parse_array_literal(p) do 326 | token = p.curr 327 | {p, elements} = parse_expression_list(p, :rbracket) 328 | array = %ArrayLiteral{token: token, elements: elements} 329 | {p, array} 330 | end 331 | 332 | defp parse_hash_literal(p) do 333 | token = p.curr 334 | do_parse_hash_literal(p, token, %{}) 335 | end 336 | 337 | defp do_parse_hash_literal(%Parser{peek: %Token{type: :rbrace}} = p, token, %{} = pairs) do 338 | p = next_token(p) 339 | hash = %HashLiteral{token: token, pairs: pairs} 340 | {p, hash} 341 | end 342 | 343 | defp do_parse_hash_literal(p, token, pairs) do 344 | p = next_token(p) 345 | 346 | with {:ok, p, key} <- parse_expression(p, @precedence_levels.lowest), 347 | {:ok, p, _peek} <- expect_peek(p, :colon), 348 | p <- next_token(p), 349 | {:ok, p, value} <- parse_expression(p, @precedence_levels.lowest) do 350 | pairs = Map.put(pairs, key, value) 351 | 352 | if p.peek.type == :rbrace do 353 | p = next_token(p) 354 | hash = %HashLiteral{token: token, pairs: pairs} 355 | {p, hash} 356 | else 357 | # TODO: no nested conditions 358 | case expect_peek(p, :comma) do 359 | {:ok, p, _peek} -> do_parse_hash_literal(p, token, pairs) 360 | {:error, p, _peek} -> {p, nil} 361 | end 362 | end 363 | else 364 | {:error, p, _} -> {p, nil} 365 | end 366 | end 367 | 368 | defp infix_parse_fns(:plus), do: &parse_infix_expression(&1, &2) 369 | defp infix_parse_fns(:minus), do: &parse_infix_expression(&1, &2) 370 | defp infix_parse_fns(:slash), do: &parse_infix_expression(&1, &2) 371 | defp infix_parse_fns(:asterisk), do: &parse_infix_expression(&1, &2) 372 | defp infix_parse_fns(:eq), do: &parse_infix_expression(&1, &2) 373 | defp infix_parse_fns(:not_eq), do: &parse_infix_expression(&1, &2) 374 | defp infix_parse_fns(:lt), do: &parse_infix_expression(&1, &2) 375 | defp infix_parse_fns(:gt), do: &parse_infix_expression(&1, &2) 376 | defp infix_parse_fns(:lparen), do: &parse_call_expression(&1, &2) 377 | defp infix_parse_fns(:lbracket), do: &parse_index_expression(&1, &2) 378 | defp infix_parse_fns(_), do: nil 379 | 380 | defp parse_infix_expression(p, left) do 381 | token = p.curr 382 | operator = p.curr.literal 383 | 384 | precedence = curr_precedence(p) 385 | p = next_token(p) 386 | {_, p, right} = parse_expression(p, precedence) 387 | infix = %InfixExpression{token: token, left: left, operator: operator, right: right} 388 | 389 | {p, infix} 390 | end 391 | 392 | defp parse_call_expression(p, function) do 393 | token = p.curr 394 | {p, arguments} = parse_expression_list(p, :rparen) 395 | expression = %CallExpression{token: token, function: function, arguments: arguments} 396 | {p, expression} 397 | end 398 | 399 | def parse_index_expression(p, left) do 400 | token = p.curr 401 | p = next_token(p) 402 | {_, p, index} = parse_expression(p, @precedence_levels.lowest) 403 | expression = %IndexExpression{token: token, left: left, index: index} 404 | 405 | case expect_peek(p, :rbracket) do 406 | {:ok, p, _peek} -> {p, expression} 407 | {:error, p, nil} -> {p, nil} 408 | end 409 | end 410 | 411 | defp parse_expression_list(p, end_token, arguments \\ []) do 412 | do_parse_expression_list(p, end_token, arguments) 413 | end 414 | 415 | defp do_parse_expression_list( 416 | %Parser{peek: %Token{type: end_token}} = p, 417 | end_token, 418 | [] = arguments 419 | ) do 420 | p = next_token(p) 421 | {p, arguments} 422 | end 423 | 424 | defp do_parse_expression_list(p, end_token, arguments) do 425 | p = next_token(p) 426 | {_, p, argument} = parse_expression(p, @precedence_levels.lowest) 427 | arguments = arguments ++ [argument] 428 | 429 | case p.peek.type do 430 | :comma -> 431 | p = next_token(p) 432 | do_parse_expression_list(p, end_token, arguments) 433 | 434 | _ -> 435 | # TODO: no nested case please! 436 | case expect_peek(p, end_token) do 437 | {:ok, p, _peek} -> {p, arguments} 438 | {:error, p, nil} -> {p, nil} 439 | end 440 | end 441 | end 442 | 443 | defp skip_semicolon(p) do 444 | if p.peek.type == :semicolon, do: next_token(p), else: p 445 | end 446 | 447 | defp curr_precedence(p), do: Map.get(@precedences, p.curr.type, @precedence_levels.lowest) 448 | 449 | defp peek_precedence(p), do: Map.get(@precedences, p.peek.type, @precedence_levels.lowest) 450 | 451 | defp expect_peek(%Parser{peek: peek} = p, expected_type) do 452 | if peek.type == expected_type do 453 | p = next_token(p) 454 | {:ok, p, peek} 455 | else 456 | error = "Expected next token to be :#{expected_type}, got :#{peek.type} instead" 457 | p = add_error(p, error) 458 | {:error, p, nil} 459 | end 460 | end 461 | 462 | defp add_error(p, msg), do: %{p | errors: p.errors ++ [msg]} 463 | end 464 | -------------------------------------------------------------------------------- /lib/monkey/repl.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Repl do 2 | alias Monkey.Evaluator 3 | alias Monkey.Lexer 4 | alias Monkey.Object.{Environment, Object} 5 | alias Monkey.Parser 6 | 7 | @prompt ">> " 8 | 9 | def loop(env \\ Environment.build()) do 10 | input = IO.gets(@prompt) 11 | tokens = Lexer.tokenize(input) 12 | parser = Parser.from_tokens(tokens) 13 | {parser, program} = Parser.parse_program(parser) 14 | 15 | case length(parser.errors) do 16 | 0 -> 17 | {result, env} = Evaluator.eval(program, env) 18 | IO.puts(Object.inspect(result)) 19 | loop(env) 20 | 21 | _ -> 22 | print_parser_errors(parser.errors) 23 | loop(env) 24 | end 25 | end 26 | 27 | defp print_parser_errors(errors) do 28 | IO.puts("Woops! We ran into some monkey business here!") 29 | IO.puts("Parser Errors:") 30 | Enum.each(errors, &IO.puts/1) 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/monkey/token.ex: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Token do 2 | @enforce_keys [:type, :literal] 3 | defstruct [:type, :literal] 4 | 5 | @keywords %{ 6 | "fn" => :function, 7 | "let" => :let, 8 | "true" => true, 9 | "false" => false, 10 | "if" => :if, 11 | "else" => :else, 12 | "return" => :return 13 | } 14 | 15 | @types %{ 16 | illegal: "ILLEGAL", 17 | eof: "EOF", 18 | # identifiers and literals 19 | # add, foobar, x, y, ... 20 | ident: "IDENT", 21 | # 123 22 | int: "INT", 23 | string: "STRING", 24 | # operators 25 | assign: "=", 26 | plus: "+", 27 | minus: "-", 28 | bang: "!", 29 | asterisk: "*", 30 | slash: "/", 31 | lt: "<", 32 | gt: ">", 33 | eq: "==", 34 | not_eq: "!=", 35 | # delimiters 36 | comma: ",", 37 | semicolon: ",", 38 | colon: ":", 39 | lparen: "(", 40 | rparen: ")", 41 | lbrace: "{", 42 | rbrace: "}", 43 | lbracket: "[", 44 | rbracket: "]", 45 | # keywords 46 | function: "FUNCTION", 47 | let: "LET", 48 | true: "TRUE", 49 | false: "FALSE", 50 | if: "IF", 51 | else: "ELSE", 52 | return: "RETURN" 53 | } 54 | 55 | def new(type: type, literal: literal) when is_atom(type) and is_binary(literal) do 56 | if Map.has_key?(@types, type) do 57 | %__MODULE__{type: type, literal: literal} 58 | else 59 | raise "Token with type #{inspect(type)} is not defined" 60 | end 61 | end 62 | 63 | def lookup_ident(ident) do 64 | Map.get(@keywords, ident, :ident) 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :monkey, 7 | version: "0.1.0", 8 | elixir: "~> 1.14", 9 | build_embedded: Mix.env() == :prod, 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps() 12 | ] 13 | end 14 | 15 | # Configuration for the OTP application 16 | # 17 | # Type "mix help compile.app" for more information 18 | def application do 19 | # Specify extra applications you'll use from Erlang/Elixir 20 | [extra_applications: [:crypto, :logger]] 21 | end 22 | 23 | # Dependencies can be Hex packages: 24 | # 25 | # {:my_dep, "~> 0.3.0"} 26 | # 27 | # Or git/path repositories: 28 | # 29 | # {:my_dep, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"} 30 | # 31 | # Type "mix help deps" for more examples and options 32 | defp deps do 33 | [] 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /test/monkey/evaluator_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Monkey.EvaluatorTest do 2 | use ExUnit.Case 3 | 4 | alias Monkey.Ast.Node 5 | alias Monkey.Evaluator 6 | alias Monkey.Lexer 7 | alias Monkey.Object.Array 8 | alias Monkey.Object.Boolean 9 | alias Monkey.Object.Environment 10 | alias Monkey.Object.Error 11 | alias Monkey.Object.Function 12 | alias Monkey.Object.Hash 13 | alias Monkey.Object.Integer 14 | alias Monkey.Object.Null 15 | alias Monkey.Object.String 16 | alias Monkey.Parser 17 | 18 | def test_eval(input) do 19 | tokens = Lexer.tokenize(input) 20 | parser = Parser.from_tokens(tokens) 21 | {parser, program} = Parser.parse_program(parser) 22 | env = Environment.build() 23 | 24 | assert length(parser.errors) == 0 25 | {result, _env} = Evaluator.eval(program, env) 26 | result 27 | end 28 | 29 | def test_integer_object(object, expected) do 30 | assert %Integer{} = object 31 | assert object.value == expected 32 | end 33 | 34 | def test_boolean_object(object, expected) do 35 | assert %Boolean{} = object 36 | assert object.value == expected 37 | end 38 | 39 | def test_null_object(object) do 40 | assert %Null{} = object 41 | end 42 | 43 | test "eval integer expression" do 44 | values = [ 45 | {"5", 5}, 46 | {"10", 10}, 47 | {"-5", -5}, 48 | {"-10", -10}, 49 | {"5 + 5 + 5 + 5 - 10", 10}, 50 | {"2 * 2 * 2 * 2 * 2", 32}, 51 | {"-50 + 100 + -50", 0}, 52 | {"5 * 2 + 10", 20}, 53 | {"5 + 2 * 10", 25}, 54 | {"20 + 2 * -10", 0}, 55 | {"50 / 2 * 2 + 10", 60}, 56 | {"2 * (5 + 10)", 30}, 57 | {"3 * 3 * 3 + 10", 37}, 58 | {"3 * (3 * 3) + 10", 37}, 59 | {"(5 + 10 * 2 + 15 / 3) * 2 + -10", 50} 60 | ] 61 | 62 | Enum.each(values, fn {input, expected} -> 63 | evaluated = test_eval(input) 64 | test_integer_object(evaluated, expected) 65 | end) 66 | end 67 | 68 | test "eval boolean expression" do 69 | values = [ 70 | {"true", true}, 71 | {"false", false}, 72 | {"true == true", true}, 73 | {"false == false", true}, 74 | {"true == false", false}, 75 | {"true != false", true}, 76 | {"false != true", true}, 77 | {"(1 < 2) == true", true}, 78 | {"(1 < 2) == false", false}, 79 | {"(1 > 2) == true", false}, 80 | {"(1 > 2) == false", true} 81 | ] 82 | 83 | Enum.each(values, fn {input, expected} -> 84 | evaluated = test_eval(input) 85 | test_boolean_object(evaluated, expected) 86 | end) 87 | end 88 | 89 | test "bang operator" do 90 | values = [ 91 | {"!true", false}, 92 | {"!false", true}, 93 | {"!5", false}, 94 | {"!!true", true}, 95 | {"!!false", false}, 96 | {"!!5", true}, 97 | {"1 < 2", true}, 98 | {"1 > 2", false}, 99 | {"1 < 1", false}, 100 | {"1 > 1", false}, 101 | {"1 == 1", true}, 102 | {"1 != 1", false}, 103 | {"1 == 2", false}, 104 | {"1 != 2", true} 105 | ] 106 | 107 | Enum.each(values, fn {input, expected} -> 108 | evaluated = test_eval(input) 109 | test_boolean_object(evaluated, expected) 110 | end) 111 | end 112 | 113 | test "if/else expressions" do 114 | values = [ 115 | {"if (true) { 10 }", 10}, 116 | {"if (false) { 10 }", nil}, 117 | {"if (1) { 10 }", 10}, 118 | {"if (1 < 2) { 10 }", 10}, 119 | {"if (1 > 2) { 10 }", nil}, 120 | {"if (1 > 2) { 10 } else { 20 }", 20}, 121 | {"if (1 < 2) { 10 } else { 20 }", 10} 122 | ] 123 | 124 | Enum.each(values, fn {input, expected} -> 125 | evaluated = test_eval(input) 126 | 127 | if expected do 128 | test_integer_object(evaluated, expected) 129 | else 130 | test_null_object(evaluated) 131 | end 132 | end) 133 | end 134 | 135 | test "return statements" do 136 | values = [ 137 | {"return 10;", 10}, 138 | {"return 10; 9;", 10}, 139 | {"return 2 * 5; 9;", 10}, 140 | {"9; return 2 * 5; 9;", 10}, 141 | {""" 142 | if (10 > 1) { 143 | if (10 > 1) { 144 | return 10; 145 | } 146 | return 1; 147 | } 148 | """, 10} 149 | ] 150 | 151 | Enum.each(values, fn {input, expected} -> 152 | evaluated = test_eval(input) 153 | test_integer_object(evaluated, expected) 154 | end) 155 | end 156 | 157 | test "error handling" do 158 | values = [ 159 | { 160 | "5 + true;", 161 | "type mismatch: INTEGER + BOOLEAN" 162 | }, 163 | { 164 | "5 + true; 5;", 165 | "type mismatch: INTEGER + BOOLEAN" 166 | }, 167 | { 168 | "-true", 169 | "unknown operator: -BOOLEAN" 170 | }, 171 | { 172 | "true + false;", 173 | "unknown operator: BOOLEAN + BOOLEAN" 174 | }, 175 | { 176 | "5; true + false; 5", 177 | "unknown operator: BOOLEAN + BOOLEAN" 178 | }, 179 | { 180 | "if (10 > 1) { true + false; }", 181 | "unknown operator: BOOLEAN + BOOLEAN" 182 | }, 183 | { 184 | """ 185 | if (10 > 1) { 186 | if (10 > 1) { 187 | return true + false; 188 | } 189 | 190 | return 1; 191 | } 192 | """, 193 | "unknown operator: BOOLEAN + BOOLEAN" 194 | }, 195 | { 196 | "foobar", 197 | "identifier not found: foobar" 198 | }, 199 | { 200 | ~s("Hello" - "World"), 201 | "unknown operator: STRING - STRING" 202 | }, 203 | { 204 | ~s/{"name": "Monkey"}[fn(x) { x }];/, 205 | "unusable as hash key: FUNCTION" 206 | } 207 | ] 208 | 209 | Enum.each(values, fn {input, expected} -> 210 | evaluated = test_eval(input) 211 | assert %Error{} = evaluated 212 | assert evaluated.message == expected 213 | end) 214 | end 215 | 216 | test "let statements" do 217 | values = [ 218 | {"let a = 5; a;", 5}, 219 | {"let a = 5 * 5; a;", 25}, 220 | {"let a = 5; let b = a; b;", 5}, 221 | {"let a = 5; let b = a; let c = a + b + 5; c;", 15} 222 | ] 223 | 224 | Enum.each(values, fn {input, expected} -> 225 | evaluated = test_eval(input) 226 | test_integer_object(evaluated, expected) 227 | end) 228 | end 229 | 230 | test "function object" do 231 | input = "fn(x) { x + 2; };" 232 | 233 | function = test_eval(input) 234 | assert %Function{} = function 235 | assert length(function.parameters) == 1 236 | assert Node.to_string(Enum.at(function.parameters, 0)) == "x" 237 | assert Node.to_string(function.body) == "(x + 2)" 238 | end 239 | 240 | test "function application" do 241 | values = [ 242 | {"let identity = fn(x) { x; }; identity(5);", 5}, 243 | {"let identity = fn(x) { return x; }; identity(5);", 5}, 244 | {"let double = fn(x) { x * 2; }; double(5);", 10}, 245 | {"let add = fn(x, y) { x + y; }; add(5, 5);", 10}, 246 | {"let add = fn(x, y) { x + y; }; add(5 + 5, add(5, 5));", 20}, 247 | {"fn(x) { x; }(5)", 5} 248 | ] 249 | 250 | Enum.each(values, fn {input, expected} -> 251 | evaluated = test_eval(input) 252 | test_integer_object(evaluated, expected) 253 | end) 254 | end 255 | 256 | test "closures" do 257 | input = """ 258 | let newAdder = fn(x) { 259 | fn(y) { x + y }; 260 | }; 261 | 262 | let addTwo = newAdder(2); 263 | addTwo(2); 264 | """ 265 | 266 | evaluated = test_eval(input) 267 | test_integer_object(evaluated, 4) 268 | end 269 | 270 | test "string literal" do 271 | input = ~s("Hello World!") 272 | string = test_eval(input) 273 | 274 | assert %String{} = string 275 | assert string.value == "Hello World!" 276 | end 277 | 278 | test "string concatenation" do 279 | input = ~s("Hello" + " " + "World!") 280 | string = test_eval(input) 281 | 282 | assert %String{} = string 283 | assert string.value == "Hello World!" 284 | end 285 | 286 | test "builtin functions" do 287 | values = [ 288 | {~s/len("")/, 0}, 289 | {~s/len("four")/, 4}, 290 | {~s/len("hello world")/, 11}, 291 | {~s/len(1)/, "argument to `len` not supported, got INTEGER"}, 292 | {~s/len("one", "two")/, "wrong number of arguments. got=2, want=1"} 293 | ] 294 | 295 | Enum.each(values, fn {input, expected} -> 296 | evaluated = test_eval(input) 297 | 298 | cond do 299 | is_integer(expected) -> 300 | test_integer_object(evaluated, expected) 301 | 302 | is_bitstring(expected) -> 303 | assert %Error{} = evaluated 304 | assert evaluated.message == expected 305 | end 306 | end) 307 | end 308 | 309 | test "array literals" do 310 | input = "[1, 2 * 2, 3 + 3]" 311 | array = test_eval(input) 312 | 313 | assert %Array{} = array 314 | assert length(array.elements) == 3 315 | 316 | test_integer_object(Enum.at(array.elements, 0), 1) 317 | test_integer_object(Enum.at(array.elements, 1), 4) 318 | test_integer_object(Enum.at(array.elements, 2), 6) 319 | end 320 | 321 | test "array index expressions" do 322 | values = [ 323 | {"[1, 2, 3][0]", 1}, 324 | {"[1, 2, 3][1]", 2}, 325 | {"[1, 2, 3][2]", 3}, 326 | {"let i = 0; [1][i];", 1}, 327 | {"[1, 2, 3][1 + 1];", 3}, 328 | {"let myArray = [1, 2, 3]; myArray[2];", 3}, 329 | {"let myArray = [1, 2, 3]; myArray[0] + myArray[1] + myArray[2];", 6}, 330 | {"let myArray = [1, 2, 3]; let i = myArray[0]; myArray[i]", 2}, 331 | {"[1, 2, 3][3]", nil}, 332 | {"[1, 2, 3][-1]", nil} 333 | ] 334 | 335 | Enum.each(values, fn {input, expected} -> 336 | evaluated = test_eval(input) 337 | 338 | cond do 339 | is_integer(expected) -> 340 | test_integer_object(evaluated, expected) 341 | 342 | true -> 343 | assert %Null{} = evaluated 344 | end 345 | end) 346 | end 347 | 348 | test "hash literals" do 349 | input = """ 350 | let two = "two"; 351 | { 352 | "one": 10 - 9, 353 | two: 1 + 1, 354 | "thr" + "ee": 6 / 2, 355 | 4: 4, 356 | true: 5, 357 | false: 6 358 | } 359 | """ 360 | 361 | hash = test_eval(input) 362 | assert %Hash{} = hash 363 | 364 | expected = %{ 365 | Hash.Hashable.hash(%String{value: "one"}) => 1, 366 | Hash.Hashable.hash(%String{value: "two"}) => 2, 367 | Hash.Hashable.hash(%String{value: "three"}) => 3, 368 | Hash.Hashable.hash(%Integer{value: 4}) => 4, 369 | Hash.Hashable.hash(%Boolean{value: true}) => 5, 370 | Hash.Hashable.hash(%Boolean{value: false}) => 6 371 | } 372 | 373 | assert length(Map.keys(expected)) == length(Map.keys(hash.pairs)) 374 | 375 | Enum.each(expected, fn {expected_key, expected_value} -> 376 | pair = hash.pairs[expected_key] 377 | assert pair 378 | test_integer_object(pair.value, expected_value) 379 | end) 380 | end 381 | 382 | test "hash index expressions" do 383 | values = [ 384 | {~s/{"foo": 5}["foo"]/, 5}, 385 | {~s/{"foo": 5}["bar"]/, nil}, 386 | {~s/let key = "foo"; {"foo": 5}[key]/, 5}, 387 | {~s/{}["foo"]/, nil}, 388 | {~s/{5: 5}[5]/, 5}, 389 | {~s/{true: 5}[true]/, 5}, 390 | {~s/{false: 5}[false]/, 5} 391 | ] 392 | 393 | Enum.each(values, fn {input, expected} -> 394 | evaluated = test_eval(input) 395 | 396 | cond do 397 | is_integer(expected) -> 398 | test_integer_object(evaluated, expected) 399 | 400 | true -> 401 | assert %Null{} = evaluated 402 | end 403 | end) 404 | end 405 | end 406 | -------------------------------------------------------------------------------- /test/monkey/lexer_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Monkey.LexerTest do 2 | use ExUnit.Case 3 | 4 | alias Monkey.Token 5 | 6 | test "converts a string into a list of tokens" do 7 | input = "=+(){},;" 8 | 9 | expected = [ 10 | %Token{type: :assign, literal: "="}, 11 | %Token{type: :plus, literal: "+"}, 12 | %Token{type: :lparen, literal: "("}, 13 | %Token{type: :rparen, literal: ")"}, 14 | %Token{type: :lbrace, literal: "{"}, 15 | %Token{type: :rbrace, literal: "}"}, 16 | %Token{type: :comma, literal: ","}, 17 | %Token{type: :semicolon, literal: ";"}, 18 | %Token{type: :eof, literal: ""} 19 | ] 20 | 21 | tokens = Monkey.Lexer.tokenize(input) 22 | 23 | assert length(tokens) == length(expected) 24 | 25 | Enum.zip(expected, tokens) 26 | |> Enum.each(&assert elem(&1, 0) == elem(&1, 1)) 27 | end 28 | 29 | test "converts real monkey code into a list of tokens" do 30 | input = """ 31 | let five = 5; 32 | let ten = 10; 33 | 34 | let add = fn(x, y) { 35 | x + y; 36 | }; 37 | 38 | let result = add(five, ten); 39 | !-/*5; 40 | 5 < 10 > 5; 41 | 42 | if (5 < 10) { 43 | return true; 44 | } else { 45 | return false; 46 | } 47 | 48 | 10 == 10; 49 | 10 != 9; 50 | "foobar" 51 | "foo bar" 52 | [1, 2]; 53 | {"foo": "bar"} 54 | """ 55 | 56 | expected = [ 57 | %Token{type: :let, literal: "let"}, 58 | %Token{type: :ident, literal: "five"}, 59 | %Token{type: :assign, literal: "="}, 60 | %Token{type: :int, literal: "5"}, 61 | %Token{type: :semicolon, literal: ";"}, 62 | %Token{type: :let, literal: "let"}, 63 | %Token{type: :ident, literal: "ten"}, 64 | %Token{type: :assign, literal: "="}, 65 | %Token{type: :int, literal: "10"}, 66 | %Token{type: :semicolon, literal: ";"}, 67 | %Token{type: :let, literal: "let"}, 68 | %Token{type: :ident, literal: "add"}, 69 | %Token{type: :assign, literal: "="}, 70 | %Token{type: :function, literal: "fn"}, 71 | %Token{type: :lparen, literal: "("}, 72 | %Token{type: :ident, literal: "x"}, 73 | %Token{type: :comma, literal: ","}, 74 | %Token{type: :ident, literal: "y"}, 75 | %Token{type: :rparen, literal: ")"}, 76 | %Token{type: :lbrace, literal: "{"}, 77 | %Token{type: :ident, literal: "x"}, 78 | %Token{type: :plus, literal: "+"}, 79 | %Token{type: :ident, literal: "y"}, 80 | %Token{type: :semicolon, literal: ";"}, 81 | %Token{type: :rbrace, literal: "}"}, 82 | %Token{type: :semicolon, literal: ";"}, 83 | %Token{type: :let, literal: "let"}, 84 | %Token{type: :ident, literal: "result"}, 85 | %Token{type: :assign, literal: "="}, 86 | %Token{type: :ident, literal: "add"}, 87 | %Token{type: :lparen, literal: "("}, 88 | %Token{type: :ident, literal: "five"}, 89 | %Token{type: :comma, literal: ","}, 90 | %Token{type: :ident, literal: "ten"}, 91 | %Token{type: :rparen, literal: ")"}, 92 | %Token{type: :semicolon, literal: ";"}, 93 | %Token{type: :bang, literal: "!"}, 94 | %Token{type: :minus, literal: "-"}, 95 | %Token{type: :slash, literal: "/"}, 96 | %Token{type: :asterisk, literal: "*"}, 97 | %Token{type: :int, literal: "5"}, 98 | %Token{type: :semicolon, literal: ";"}, 99 | %Token{type: :int, literal: "5"}, 100 | %Token{type: :lt, literal: "<"}, 101 | %Token{type: :int, literal: "10"}, 102 | %Token{type: :gt, literal: ">"}, 103 | %Token{type: :int, literal: "5"}, 104 | %Token{type: :semicolon, literal: ";"}, 105 | %Token{type: :if, literal: "if"}, 106 | %Token{type: :lparen, literal: "("}, 107 | %Token{type: :int, literal: "5"}, 108 | %Token{type: :lt, literal: "<"}, 109 | %Token{type: :int, literal: "10"}, 110 | %Token{type: :rparen, literal: ")"}, 111 | %Token{type: :lbrace, literal: "{"}, 112 | %Token{type: :return, literal: "return"}, 113 | %Token{type: true, literal: "true"}, 114 | %Token{type: :semicolon, literal: ";"}, 115 | %Token{type: :rbrace, literal: "}"}, 116 | %Token{type: :else, literal: "else"}, 117 | %Token{type: :lbrace, literal: "{"}, 118 | %Token{type: :return, literal: "return"}, 119 | %Token{type: false, literal: "false"}, 120 | %Token{type: :semicolon, literal: ";"}, 121 | %Token{type: :rbrace, literal: "}"}, 122 | %Token{type: :int, literal: "10"}, 123 | %Token{type: :eq, literal: "=="}, 124 | %Token{type: :int, literal: "10"}, 125 | %Token{type: :semicolon, literal: ";"}, 126 | %Token{type: :int, literal: "10"}, 127 | %Token{type: :not_eq, literal: "!="}, 128 | %Token{type: :int, literal: "9"}, 129 | %Token{type: :semicolon, literal: ";"}, 130 | %Token{type: :string, literal: "foobar"}, 131 | %Token{type: :string, literal: "foo bar"}, 132 | %Token{type: :lbracket, literal: "["}, 133 | %Token{type: :int, literal: "1"}, 134 | %Token{type: :comma, literal: ","}, 135 | %Token{type: :int, literal: "2"}, 136 | %Token{type: :rbracket, literal: "]"}, 137 | %Token{type: :semicolon, literal: ";"}, 138 | %Token{type: :lbrace, literal: "{"}, 139 | %Token{type: :string, literal: "foo"}, 140 | %Token{type: :colon, literal: ":"}, 141 | %Token{type: :string, literal: "bar"}, 142 | %Token{type: :rbrace, literal: "}"}, 143 | %Token{type: :eof, literal: ""} 144 | ] 145 | 146 | tokens = Monkey.Lexer.tokenize(input) 147 | 148 | assert length(tokens) == length(expected) 149 | 150 | Enum.zip(expected, tokens) 151 | |> Enum.each(&assert elem(&1, 0) == elem(&1, 1)) 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /test/monkey/object/hash_key_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Monkey.Object.HashKeyTest do 2 | use ExUnit.Case 3 | 4 | alias Monkey.Object.Boolean 5 | alias Monkey.Object.Hash 6 | alias Monkey.Object.Integer 7 | alias Monkey.Object.String 8 | 9 | test "string hash keys" do 10 | hello1 = %String{value: "Hello World"} 11 | hello2 = %String{value: "Hello World"} 12 | 13 | diff1 = %String{value: "My name is johnny"} 14 | diff2 = %String{value: "My name is johnny"} 15 | 16 | assert Hash.Hashable.hash(hello1) == Hash.Hashable.hash(hello2) 17 | assert Hash.Hashable.hash(diff1) == Hash.Hashable.hash(diff2) 18 | assert Hash.Hashable.hash(hello1) != Hash.Hashable.hash(diff1) 19 | end 20 | 21 | test "boolean hash keys" do 22 | true1 = %Boolean{value: true} 23 | true2 = %Boolean{value: true} 24 | false1 = %Boolean{value: false} 25 | false2 = %Boolean{value: false} 26 | 27 | assert Hash.Hashable.hash(true1) == Hash.Hashable.hash(true2) 28 | assert Hash.Hashable.hash(false1) == Hash.Hashable.hash(false2) 29 | assert Hash.Hashable.hash(true1) != Hash.Hashable.hash(false1) 30 | end 31 | 32 | test "integer hash keys" do 33 | one1 = %Integer{value: 1} 34 | one2 = %Integer{value: 1} 35 | two1 = %Integer{value: 2} 36 | two2 = %Integer{value: 2} 37 | 38 | assert Hash.Hashable.hash(one1) == Hash.Hashable.hash(one2) 39 | assert Hash.Hashable.hash(two1) == Hash.Hashable.hash(two2) 40 | assert Hash.Hashable.hash(one1) != Hash.Hashable.hash(two1) 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /test/monkey/parser_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Monkey.ParserTest do 2 | use ExUnit.Case 3 | alias Monkey.Ast.ArrayLiteral 4 | alias Monkey.Ast.BooleanLiteral 5 | alias Monkey.Ast.CallExpression 6 | alias Monkey.Ast.ExpressionStatement 7 | alias Monkey.Ast.FunctionLiteral 8 | alias Monkey.Ast.HashLiteral 9 | alias Monkey.Ast.Identifier 10 | alias Monkey.Ast.IfExpression 11 | alias Monkey.Ast.IndexExpression 12 | alias Monkey.Ast.InfixExpression 13 | alias Monkey.Ast.IntegerLiteral 14 | alias Monkey.Ast.LetStatement 15 | alias Monkey.Ast.Node 16 | alias Monkey.Ast.PrefixExpression 17 | alias Monkey.Ast.Program 18 | alias Monkey.Ast.ReturnStatement 19 | alias Monkey.Ast.StringLiteral 20 | alias Monkey.Lexer 21 | alias Monkey.Parser 22 | 23 | def test_literal_expression(expression, value) do 24 | case value do 25 | v when is_integer(v) -> test_integer_literal(expression, v) 26 | v when is_boolean(v) -> test_boolean_literal(expression, v) 27 | v when is_bitstring(v) -> test_identifier(expression, v) 28 | end 29 | end 30 | 31 | def test_let_statement(statement, name) do 32 | assert %LetStatement{} = statement 33 | assert Node.token_literal(statement) == "let" 34 | assert statement.name.value == name 35 | assert Node.token_literal(statement.name) == name 36 | end 37 | 38 | def test_integer_literal(expression, value) do 39 | assert %IntegerLiteral{} = expression 40 | assert expression.value == value 41 | assert Node.token_literal(expression) == Integer.to_string(value) 42 | end 43 | 44 | def test_identifier(expression, value) do 45 | assert %Identifier{} = expression 46 | assert expression.value == value 47 | assert Node.token_literal(expression) == value 48 | end 49 | 50 | def test_boolean_literal(expression, value) do 51 | assert %BooleanLiteral{} = expression 52 | assert expression.value == value 53 | assert Node.token_literal(expression) == Atom.to_string(value) 54 | end 55 | 56 | def test_infix_expression(expression, left, operator, right) do 57 | assert %InfixExpression{} = expression 58 | test_literal_expression(expression.left, left) 59 | assert expression.operator == operator 60 | test_literal_expression(expression.right, right) 61 | end 62 | 63 | def parse_input(input) do 64 | tokens = Lexer.tokenize(input) 65 | parser = Parser.from_tokens(tokens) 66 | {parser, program} = Parser.parse_program(parser) 67 | 68 | if length(parser.errors) > 0, do: IO.inspect(parser.errors) 69 | assert length(parser.errors) == 0 70 | 71 | {parser, program} 72 | end 73 | 74 | def parse_one_expression_statement(input) do 75 | {_, program} = parse_input(input) 76 | assert length(program.statements) == 1 77 | 78 | statement = Enum.at(program.statements, 0) 79 | assert %ExpressionStatement{} = statement 80 | statement 81 | end 82 | 83 | test "parse let statements" do 84 | values = [ 85 | {"let x = 5;", "x", 5}, 86 | {"let y = true;", "y", true}, 87 | {"let foobar = y", "foobar", "y"} 88 | ] 89 | 90 | Enum.each(values, fn {input, identifier, value} -> 91 | {_, program} = parse_input(input) 92 | assert length(program.statements) == 1 93 | 94 | statement = Enum.at(program.statements, 0) 95 | test_let_statement(statement, identifier) 96 | test_literal_expression(statement.value, value) 97 | end) 98 | end 99 | 100 | test "parse return statements" do 101 | values = [ 102 | {"return 5;", 5}, 103 | {"return true;", true}, 104 | {"return foobar;", "foobar"} 105 | ] 106 | 107 | Enum.each(values, fn {input, value} -> 108 | {_, program} = parse_input(input) 109 | assert length(program.statements) == 1 110 | 111 | statement = Enum.at(program.statements, 0) 112 | assert %ReturnStatement{} = statement 113 | assert Node.token_literal(statement) == "return" 114 | 115 | test_literal_expression(statement.return_value, value) 116 | end) 117 | end 118 | 119 | test "parse identifier expression" do 120 | input = "foobar;" 121 | statement = parse_one_expression_statement(input) 122 | 123 | identifier = statement.expression 124 | test_identifier(identifier, "foobar") 125 | end 126 | 127 | test "parse integer literal expression" do 128 | input = "5;" 129 | statement = parse_one_expression_statement(input) 130 | 131 | literal = statement.expression 132 | test_integer_literal(literal, 5) 133 | end 134 | 135 | test "parse prefix expressions" do 136 | values = [ 137 | {"!5;", "!", 5}, 138 | {"-15", "-", 15}, 139 | {"!foobar;", "!", "foobar"}, 140 | {"-foobar;", "-", "foobar"}, 141 | {"!true;", "!", true}, 142 | {"!false;", "!", false} 143 | ] 144 | 145 | Enum.each(values, fn {input, operator, value} -> 146 | statement = parse_one_expression_statement(input) 147 | 148 | expression = statement.expression 149 | assert %PrefixExpression{} = expression 150 | 151 | assert expression.operator == operator 152 | test_literal_expression(expression.right, value) 153 | end) 154 | end 155 | 156 | test "parse infix expressions" do 157 | values = [ 158 | {"5 + 5;", 5, "+", 5}, 159 | {"5 - 5;", 5, "-", 5}, 160 | {"5 * 5;", 5, "*", 5}, 161 | {"5 / 5;", 5, "/", 5}, 162 | {"5 > 5;", 5, ">", 5}, 163 | {"5 < 5;", 5, "<", 5}, 164 | {"5 == 5;", 5, "==", 5}, 165 | {"5 != 5;", 5, "!=", 5}, 166 | {"foobar + barfoo;", "foobar", "+", "barfoo"}, 167 | {"foobar - barfoo;", "foobar", "-", "barfoo"}, 168 | {"foobar * barfoo;", "foobar", "*", "barfoo"}, 169 | {"foobar / barfoo;", "foobar", "/", "barfoo"}, 170 | {"foobar > barfoo;", "foobar", ">", "barfoo"}, 171 | {"foobar < barfoo;", "foobar", "<", "barfoo"}, 172 | {"foobar == barfoo;", "foobar", "==", "barfoo"}, 173 | {"foobar != barfoo;", "foobar", "!=", "barfoo"}, 174 | {"true == true", true, "==", true}, 175 | {"true != false", true, "!=", false}, 176 | {"false == false", false, "==", false} 177 | ] 178 | 179 | Enum.each(values, fn {input, left, operator, right} -> 180 | statement = parse_one_expression_statement(input) 181 | 182 | expression = statement.expression 183 | test_infix_expression(expression, left, operator, right) 184 | end) 185 | end 186 | 187 | test "operator precedence parsing" do 188 | values = [ 189 | {"-a * b", "((-a) * b)"}, 190 | {"!-a", "(!(-a))"}, 191 | {"a + b + c", "((a + b) + c)"}, 192 | {"a + b - c", "((a + b) - c)"}, 193 | {"a * b * c", "((a * b) * c)"}, 194 | {"a * b / c", "((a * b) / c)"}, 195 | {"a + b / c", "(a + (b / c))"}, 196 | {"a + b * c + d / e - f", "(((a + (b * c)) + (d / e)) - f)"}, 197 | {"3 + 4; -5 * 5", "(3 + 4)((-5) * 5)"}, 198 | {"5 > 4 == 3 < 4", "((5 > 4) == (3 < 4))"}, 199 | {"5 < 4 != 3 > 4", "((5 < 4) != (3 > 4))"}, 200 | {"3 + 4 * 5 == 3 * 1 + 4 * 5", "((3 + (4 * 5)) == ((3 * 1) + (4 * 5)))"}, 201 | {"3 + 4 * 5 == 3 * 1 + 4 * 5", "((3 + (4 * 5)) == ((3 * 1) + (4 * 5)))"}, 202 | {"true", "true"}, 203 | {"false", "false"}, 204 | {"3 > 5 == false", "((3 > 5) == false)"}, 205 | {"3 < 5 == true", "((3 < 5) == true)"}, 206 | {"1 + (2 + 3) + 4", "((1 + (2 + 3)) + 4)"}, 207 | {"(5 + 5) * 2", "((5 + 5) * 2)"}, 208 | {"2 / (5 + 5)", "(2 / (5 + 5))"}, 209 | {"(5 + 5) * 2 * (5 + 5)", "(((5 + 5) * 2) * (5 + 5))"}, 210 | {"-(5 + 5)", "(-(5 + 5))"}, 211 | {"!(true == true)", "(!(true == true))"}, 212 | {"a + add(b * c) + d", "((a + add((b * c))) + d)"}, 213 | {"add(a, b, 1, 2 * 3, 4 + 5, add(6, 7 * 8))", 214 | "add(a, b, 1, (2 * 3), (4 + 5), add(6, (7 * 8)))"}, 215 | {"add(a + b + c * d / f + g)", "add((((a + b) + ((c * d) / f)) + g))"}, 216 | {"a * [1, 2, 3, 4][b * c] * d", "((a * ([1, 2, 3, 4][(b * c)])) * d)"}, 217 | {"add(a * b[2], b[1], 2 * [1, 2][1])", "add((a * (b[2])), (b[1]), (2 * ([1, 2][1])))"} 218 | ] 219 | 220 | Enum.each(values, fn {input, expected} -> 221 | {_, program} = parse_input(input) 222 | assert Program.to_string(program) == expected 223 | end) 224 | end 225 | 226 | test "parse boolean expressions" do 227 | values = [ 228 | {"true", true}, 229 | {"false", false} 230 | ] 231 | 232 | Enum.each(values, fn {input, value} -> 233 | statement = parse_one_expression_statement(input) 234 | 235 | boolean = statement.expression 236 | assert %BooleanLiteral{} = boolean 237 | assert boolean.value == value 238 | end) 239 | end 240 | 241 | test "parse if expression" do 242 | input = "if (x < y) { x }" 243 | statement = parse_one_expression_statement(input) 244 | 245 | expression = statement.expression 246 | assert %IfExpression{} = expression 247 | test_infix_expression(expression.condition, "x", "<", "y") 248 | 249 | assert length(expression.consequence.statements) == 1 250 | consequence = Enum.at(expression.consequence.statements, 0) 251 | test_identifier(consequence.expression, "x") 252 | 253 | assert expression.alternative == nil 254 | end 255 | 256 | test "parse if/else expression" do 257 | input = "if (x < y) { x } else { y }" 258 | statement = parse_one_expression_statement(input) 259 | 260 | expression = statement.expression 261 | assert %IfExpression{} = expression 262 | test_infix_expression(expression.condition, "x", "<", "y") 263 | 264 | assert length(expression.consequence.statements) == 1 265 | consequence = Enum.at(expression.consequence.statements, 0) 266 | assert %ExpressionStatement{} = consequence 267 | test_identifier(consequence.expression, "x") 268 | 269 | assert length(expression.alternative.statements) == 1 270 | alternative = Enum.at(expression.alternative.statements, 0) 271 | assert %ExpressionStatement{} = alternative 272 | test_identifier(alternative.expression, "y") 273 | end 274 | 275 | test "parse function literal" do 276 | input = "fn(x, y) { x + y; }" 277 | statement = parse_one_expression_statement(input) 278 | 279 | function = statement.expression 280 | assert %FunctionLiteral{} = function 281 | 282 | assert length(function.parameters) == 2 283 | test_literal_expression(Enum.at(function.parameters, 0), "x") 284 | test_literal_expression(Enum.at(function.parameters, 1), "y") 285 | 286 | assert length(function.body.statements) == 1 287 | body_statement = Enum.at(function.body.statements, 0) 288 | test_infix_expression(body_statement.expression, "x", "+", "y") 289 | end 290 | 291 | test "parse function parameters" do 292 | values = [ 293 | {"fn() {};", []}, 294 | {"fn(x) {};", ["x"]}, 295 | {"fn(x, y, z) {};", ["x", "y", "z"]} 296 | ] 297 | 298 | Enum.each(values, fn {input, parameters} -> 299 | statement = parse_one_expression_statement(input) 300 | 301 | function = statement.expression 302 | assert %FunctionLiteral{} = function 303 | 304 | assert length(function.parameters) == length(parameters) 305 | 306 | Enum.zip(function.parameters, parameters) 307 | |> Enum.each(fn {identifier, expected} -> 308 | test_literal_expression(identifier, expected) 309 | end) 310 | end) 311 | end 312 | 313 | test "parse call expression" do 314 | input = "add(1, 2 * 3, 4 + 5);" 315 | statement = parse_one_expression_statement(input) 316 | 317 | expression = statement.expression 318 | assert %CallExpression{} = expression 319 | 320 | test_identifier(expression.function, "add") 321 | 322 | assert length(expression.arguments) == 3 323 | test_literal_expression(Enum.at(expression.arguments, 0), 1) 324 | test_infix_expression(Enum.at(expression.arguments, 1), 2, "*", 3) 325 | test_infix_expression(Enum.at(expression.arguments, 2), 4, "+", 5) 326 | end 327 | 328 | test "parse string literal expression" do 329 | input = ~s{"hello world\";} 330 | statement = parse_one_expression_statement(input) 331 | 332 | expression = statement.expression 333 | assert %StringLiteral{} = expression 334 | assert expression.value == "hello world" 335 | end 336 | 337 | test "parse empty array literal" do 338 | input = "[]" 339 | statement = parse_one_expression_statement(input) 340 | 341 | array = statement.expression 342 | assert %ArrayLiteral{} = array 343 | assert length(array.elements) == 0 344 | end 345 | 346 | test "parse array literal" do 347 | input = "[1, 2 + 2, 3 * 3]" 348 | statement = parse_one_expression_statement(input) 349 | 350 | array = statement.expression 351 | assert %ArrayLiteral{} = array 352 | assert length(array.elements) == 3 353 | 354 | test_integer_literal(Enum.at(array.elements, 0), 1) 355 | test_infix_expression(Enum.at(array.elements, 1), 2, "+", 2) 356 | test_infix_expression(Enum.at(array.elements, 2), 3, "*", 3) 357 | end 358 | 359 | test "parse index expression" do 360 | input = "myArray[1 + 1]" 361 | statement = parse_one_expression_statement(input) 362 | 363 | index = statement.expression 364 | assert %IndexExpression{} = index 365 | test_identifier(index.left, "myArray") 366 | test_infix_expression(index.index, 1, "+", 1) 367 | end 368 | 369 | test "parse hash literal" do 370 | input = ~s/{"one": 1, "two": 2, "three": 3}/ 371 | statement = parse_one_expression_statement(input) 372 | 373 | hash = statement.expression 374 | assert %HashLiteral{} = hash 375 | assert length(Map.keys(hash.pairs)) == 3 376 | 377 | expected = %{ 378 | "one" => 1, 379 | "two" => 2, 380 | "three" => 3 381 | } 382 | 383 | Enum.each(hash.pairs, fn {key, value} -> 384 | assert %StringLiteral{} = key 385 | expected_value = expected[Node.to_string(key)] 386 | 387 | test_integer_literal(value, expected_value) 388 | end) 389 | end 390 | 391 | test "parse empty hash literal" do 392 | input = "{}" 393 | statement = parse_one_expression_statement(input) 394 | 395 | hash = statement.expression 396 | assert %HashLiteral{} = hash 397 | 398 | assert length(Map.keys(hash.pairs)) == 0 399 | end 400 | 401 | test "parse hash literal with boolean keys" do 402 | input = "{true: 1, false: 2}" 403 | statement = parse_one_expression_statement(input) 404 | 405 | hash = statement.expression 406 | assert %HashLiteral{} = hash 407 | assert length(Map.keys(hash.pairs)) == 2 408 | 409 | expected = %{ 410 | "true" => 1, 411 | "false" => 2 412 | } 413 | 414 | Enum.each(hash.pairs, fn {key, value} -> 415 | assert %BooleanLiteral{} = key 416 | expected_value = expected[Node.to_string(key)] 417 | 418 | test_integer_literal(value, expected_value) 419 | end) 420 | end 421 | 422 | test "parse hash literal with integer keys" do 423 | input = "{1: 1, 2: 2, 3: 3}" 424 | statement = parse_one_expression_statement(input) 425 | 426 | hash = statement.expression 427 | assert %HashLiteral{} = hash 428 | assert length(Map.keys(hash.pairs)) == 3 429 | 430 | expected = %{ 431 | "1" => 1, 432 | "2" => 2, 433 | "3" => 3 434 | } 435 | 436 | Enum.each(hash.pairs, fn {key, value} -> 437 | assert %IntegerLiteral{} = key 438 | expected_value = expected[Node.to_string(key)] 439 | 440 | test_integer_literal(value, expected_value) 441 | end) 442 | end 443 | 444 | test "parse hash literal with expressions" do 445 | input = ~s({"one": 0 + 1, "two": 10 - 8, "three": 15 / 5}) 446 | statement = parse_one_expression_statement(input) 447 | 448 | hash = statement.expression 449 | assert %HashLiteral{} = hash 450 | assert length(Map.keys(hash.pairs)) == 3 451 | 452 | expected = %{ 453 | "one" => &test_infix_expression(&1, 0, "+", 1), 454 | "two" => &test_infix_expression(&1, 10, "-", 8), 455 | "three" => &test_infix_expression(&1, 15, "/", 5) 456 | } 457 | 458 | Enum.each(hash.pairs, fn {key, value} -> 459 | assert %StringLiteral{} = key 460 | test_fn = expected[Node.to_string(key)] 461 | test_fn.(value) 462 | end) 463 | end 464 | end 465 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------