├── .gitignore ├── LICENSE ├── README.md ├── Vagrantfile ├── diylang ├── __init__.py ├── ast.py ├── evaluator.py ├── interpreter.py ├── parser.py ├── repl.py └── types.py ├── example.diy ├── parts ├── 1.md ├── 2.md ├── 3.md ├── 4.md ├── 5.md ├── 6.md ├── 7.md ├── 8.md ├── language.md └── python.md ├── repl ├── repl.bat ├── run-tests.bat ├── run-tests.sh ├── stdlib.diy └── tests ├── test_1_parsing.py ├── test_2_evaluating_simple_expressions.py ├── test_3_evaluating_complex_expressions.py ├── test_4_working_with_variables_and_environments.py ├── test_5_adding_functions_to_the_mix.py ├── test_6_working_with_lists.py ├── test_7_using_the_language.py ├── test_8_final_touches.py ├── test_provided_code.py └── test_sanity_checks.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vagrant/ 3 | 4 | # Eclipse 5 | .project 6 | .pydevproject 7 | .settings 8 | 9 | # PyCharm 10 | .idea 11 | 12 | *.bak 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Kjetil Valle 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## DIY Lang 2 | 3 | > batteries included, some assembly required 4 | 5 | In this tutorial/workshop we'll be implementing our own little language, more or less from scratch. 6 | 7 | By the end of the tutorial you will be the proud author of a programming language, and will hopefully better understand how programming languages work on a fundamental level. 8 | 9 | ### What we will be making 10 | 11 | We will make a relatively simple, but neat language. We aim for the following features: 12 | 13 | - A handful of data types (integers, booleans and symbols) 14 | - Variables 15 | - First class functions with lexical scoping 16 | - That nice homemade quality feeling 17 | 18 | We will *not* have: 19 | 20 | - A proper type system 21 | - Error handling 22 | - Good performance 23 | - And much, much more 24 | 25 | The language should be able to interpret the following code by the time we are done: 26 | 27 | ```lisp 28 | (define fact 29 | ;; Factorial function 30 | (lambda (n) 31 | (if (eq n 0) 32 | 1 ; Factorial of 0 is 1 33 | (* n (fact (- n 1)))))) 34 | 35 | ;; When parsing the file, the last statement is returned 36 | (fact 5) 37 | ``` 38 | 39 | The syntax is very similar to languages in the Lisp family. If you find the example unfamiliar, you might want to have a look at [a more detailed description of the language](parts/language.md). 40 | 41 | ### Prerequisites 42 | 43 | First, clone this repo. 44 | 45 | ```bash 46 | git clone https://github.com/kvalle/diy-lang.git 47 | cd diy-lang 48 | ``` 49 | 50 | Then, depending on your platform: 51 | 52 | - **Mac**: Install [Python](http://www.python.org/), either from the webpage or using `brew`. Then run `easy_install nose` to install `nose`, the test runner we'll be using. 53 | 54 | *Optional: If you are familiar with [virtualenv](http://www.virtualenv.org/en/latest/) you might want to install `nose` in a separate pyenv to keep everything tidy.* 55 | 56 | - **Windows/Linux**: Install [Python](http://www.python.org/), either from the webpage or your package manager of choice. Then install [Pip](https://pypi.python.org/pypi/pip). Finally, install `nose` like this: `pip install nose`. 57 | 58 | *Optional: If you are familiar with [virtualenv](http://www.virtualenv.org/en/latest/) you might want to install `nose` in a separate pyenv to keep everything tidy.* 59 | 60 | - **Vagrant**: If you have [Vagrant](https://www.vagrantup.com/) installed, an easy way to get going is to use the provided `Vagrantfile`. Use `vagrant up` to boot the box, and `vagrant ssh` to log in. The project folder is synced to `/home/vagrant/diy-lang`. 61 | 62 | 63 | #### Test your setup 64 | 65 | Once installed, run `nosetests --stop` see that everything is working properly. This will run the test suite, stopping at the first failure. Expect something like the following: 66 | 67 | ```bash 68 | $ nosetests --stop 69 | E 70 | ====================================================================== 71 | ERROR: TEST 1.1: Parsing a single symbol. 72 | ---------------------------------------------------------------------- 73 | Traceback (most recent call last): 74 | File "/usr/local/lib/python2.7/dist-packages/nose/case.py", line 197, in runTest 75 | self.test(*self.arg) 76 | File "/home/vagrant/diy-lang/tests/test_1_parsing.py", line 15, in test_parse_single_symbol 77 | assert_equals('foo', parse('foo')) 78 | File "/home/vagrant/diy-lang/diylang/parser.py", line 17, in parse 79 | raise NotImplementedError("DIY") 80 | NotImplementedError: DIY 81 | 82 | ---------------------------------------------------------------------- 83 | Ran 1 test in 0.034s 84 | 85 | FAILED (errors=1) 86 | ``` 87 | 88 | 89 | ### A few tips 90 | 91 | Take the time to consider the following points before we get going: 92 | 93 | - **Keep things simple** 94 | 95 | Don't make things more complicated than they need to be. The tests should hopefully guide you every step of the way. 96 | 97 | - **Read the test descriptions** 98 | 99 | Each test has a small text describing what you are going to implement and why. Reading these should make things easier, and you might end up learning more. 100 | 101 | - **Use the provided functions** 102 | 103 | Some of the more boring details are already taken care of. Take the time to look at the functions provided in `parser.py`, and the various imports in files where you need to do some work. 104 | 105 | - **The Python cheat sheet in `python.md`** 106 | 107 | Unless you're fluent in Python, there should be some helpful pointers in the [Python cheat sheet](https://github.com/kvalle/diy-lang/blob/master/parts/python.md). Also, if Python is very new to you, the [Python tutorial](https://docs.python.org/2/tutorial/index.html) might prove helpful. 108 | 109 | - **Description of your language** 110 | 111 | Read a description of the language you are going to make in [language.md](https://github.com/kvalle/diy-lang/blob/master/parts/language.md). 112 | 113 | ### Get started! 114 | 115 | The workshop is split up into eight parts. Each consist of an introduction, and a bunch of unit tests which it is your task to make run. When all the tests run, you'll have implemented that part of the language. 116 | 117 | Have fun! 118 | 119 | - [Part 1: parsing](parts/1.md) 120 | - [Part 2: evaluating simple expressions](parts/2.md) 121 | - [Part 3: evaluating complex expressions](parts/3.md) 122 | - [Part 4: working with variables](parts/4.md) 123 | - [Part 5: functions](parts/5.md) 124 | - [Part 6: working with lists](parts/6.md) 125 | - [Part 7: using your language](parts/7.md) 126 | - [Part 8: final touches](parts/8.md) 127 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure("2") do |config| 5 | config.vm.box = "ubuntu/bionic64" 6 | 7 | config.vm.provision "shell", inline: <<-SCRIPT 8 | sudo apt-get update 9 | sudo apt-get install python3 -y 10 | sudo apt-get install python3-pip -y 11 | sudo pip3 install nose 12 | sudo apt-get install inotify-tools -y 13 | SCRIPT 14 | 15 | config.vm.synced_folder "", "/home/vagrant/diy-lang" 16 | end 17 | -------------------------------------------------------------------------------- /diylang/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | -------------------------------------------------------------------------------- /diylang/ast.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .types import Closure, String 4 | 5 | """ 6 | This module contains a few simple helper functions for checking the type of 7 | ASTs. 8 | """ 9 | 10 | 11 | def is_symbol(x): 12 | return isinstance(x, str) 13 | 14 | 15 | def is_string(x): 16 | return isinstance(x, String) 17 | 18 | 19 | def is_list(x): 20 | return isinstance(x, list) 21 | 22 | 23 | def is_boolean(x): 24 | return isinstance(x, bool) 25 | 26 | 27 | def is_integer(x): 28 | return isinstance(x, int) 29 | 30 | 31 | def is_closure(x): 32 | return isinstance(x, Closure) 33 | 34 | 35 | def is_atom(x): 36 | return (is_symbol(x) or 37 | is_integer(x) or 38 | is_string(x) or 39 | is_boolean(x) or 40 | is_closure(x)) 41 | -------------------------------------------------------------------------------- /diylang/evaluator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .types import Environment, DiyLangError, Closure, String 4 | from .ast import is_boolean, is_atom, is_symbol, is_list, is_closure, \ 5 | is_integer, is_string 6 | from .parser import unparse 7 | 8 | """ 9 | This is the Evaluator module. The `evaluate` function below is the heart 10 | of your language, and the focus for most of parts 2 through 6. 11 | 12 | A score of useful functions is provided for you, as per the above imports, 13 | making your work a bit easier. (We're supposed to get through this thing 14 | in a day, after all.) 15 | """ 16 | 17 | 18 | def evaluate(ast, env): 19 | """Evaluate an Abstract Syntax Tree in the specified environment.""" 20 | raise NotImplementedError("DIY") 21 | -------------------------------------------------------------------------------- /diylang/interpreter.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from .evaluator import evaluate 4 | from .parser import parse, unparse, parse_multiple 5 | from .types import Environment 6 | 7 | 8 | def interpret(source, env=None): 9 | """ 10 | Interpret a DIY Lang program statement 11 | 12 | Accepts a program statement as a string, interprets it, and then 13 | returns the resulting DIY Lang expression as string. 14 | """ 15 | if env is None: 16 | env = Environment() 17 | 18 | return unparse(evaluate(parse(source), env)) 19 | 20 | 21 | def interpret_file(filename, env=None): 22 | """ 23 | Interpret a DIY Lang file 24 | 25 | Accepts the name of a DIY Lang file containing a series of statements. 26 | Returns the value of the last expression of the file. 27 | """ 28 | if env is None: 29 | env = Environment() 30 | 31 | with open(filename, 'r') as sourcefile: 32 | source = "".join(sourcefile.readlines()) 33 | 34 | asts = parse_multiple(source) 35 | results = [evaluate(ast, env) for ast in asts] 36 | return unparse(results[-1]) 37 | -------------------------------------------------------------------------------- /diylang/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import re 4 | from .ast import is_boolean, is_list 5 | from .types import DiyLangError, String 6 | 7 | """ 8 | This is the parser module, with the `parse` function which you'll implement as 9 | part 1 of the workshop. Its job is to convert strings into data structures that 10 | the evaluator can understand. 11 | """ 12 | 13 | 14 | def parse(source): 15 | """Parse string representation of one *single* expression 16 | into the corresponding Abstract Syntax Tree.""" 17 | 18 | raise NotImplementedError("DIY") 19 | 20 | # 21 | # Below are a few useful utility functions. These should come in handy when 22 | # implementing `parse`. We don't want to spend the day implementing parenthesis 23 | # counting, after all. 24 | # 25 | 26 | 27 | def remove_comments(source): 28 | """Remove from a string anything in between a ; and a line break""" 29 | return re.sub(r";.*\n", "\n", source) 30 | 31 | 32 | def find_matching_paren(source, start=0): 33 | """Given a string and the index of an opening parenthesis, determines 34 | the index of the matching closing paren.""" 35 | 36 | assert source[start] == '(' 37 | pos = start 38 | open_brackets = 1 39 | while open_brackets > 0: 40 | pos += 1 41 | if len(source) == pos: 42 | raise DiyLangError("Incomplete expression: %s" % source[start:]) 43 | if source[pos] == '(': 44 | open_brackets += 1 45 | if source[pos] == ')': 46 | open_brackets -= 1 47 | return pos 48 | 49 | 50 | def split_exps(source): 51 | """Splits a source string into sub expressions 52 | that can be parsed individually. 53 | 54 | Example: 55 | 56 | > split_exps("foo bar (baz 123)") 57 | ["foo", "bar", "(baz 123)"] 58 | """ 59 | 60 | rest = source.strip() 61 | exps = [] 62 | while rest: 63 | exp, rest = first_expression(rest) 64 | exps.append(exp) 65 | return exps 66 | 67 | 68 | def first_expression(source): 69 | """Split string into (exp, rest) where exp is the 70 | first expression in the string and rest is the 71 | rest of the string after this expression.""" 72 | 73 | source = source.strip() 74 | 75 | if source[0] == "'": 76 | exp, rest = first_expression(source[1:]) 77 | return source[0] + exp, rest 78 | elif source[0] == "(": 79 | last = find_matching_paren(source) 80 | return source[:last + 1], source[last + 1:] 81 | else: 82 | match = re.match(r"^[^\s)(']+", source) 83 | end = match.end() 84 | atom = source[:end] 85 | return atom, source[end:] 86 | 87 | # 88 | # The functions below, `parse_multiple` and `unparse` are implemented in order 89 | # for the REPL to work. Don't worry about them when implementing the language. 90 | # 91 | 92 | 93 | def parse_multiple(source): 94 | """Creates a list of ASTs from program source constituting 95 | multiple expressions. 96 | 97 | Example: 98 | 99 | > parse_multiple("(foo bar) (baz 1 2 3)") 100 | [['foo', 'bar'], ['baz', 1, 2, 3]] 101 | 102 | """ 103 | 104 | source = remove_comments(source) 105 | return [parse(exp) for exp in split_exps(source)] 106 | 107 | 108 | def unparse(ast): 109 | """Turns an AST back into DIY Lang program source""" 110 | 111 | if is_boolean(ast): 112 | return "#t" if ast else "#f" 113 | elif is_list(ast): 114 | if len(ast) > 0 and ast[0] == "quote": 115 | return "'%s" % unparse(ast[1]) 116 | else: 117 | return "(%s)" % " ".join([unparse(x) for x in ast]) 118 | else: 119 | # integers or symbols (or lambdas) 120 | return str(ast) 121 | -------------------------------------------------------------------------------- /diylang/repl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | from .types import DiyLangError, Environment 7 | from .parser import remove_comments 8 | from .interpreter import interpret 9 | 10 | # importing this gives readline goodness when running on systems 11 | # where it is supported (i.e. UNIX-y systems) 12 | try: 13 | import readline 14 | except ImportError: 15 | pass 16 | 17 | # Python 2 / Python 3 compatibility 18 | try: 19 | input = raw_input 20 | except NameError: 21 | pass 22 | 23 | 24 | def repl(env=None): 25 | """Start the interactive Read-Eval-Print-Loop""" 26 | 27 | eof = "^Z" if sys.platform[0:3] == 'win' else "^D" 28 | 29 | print("") 30 | print(" " + faded(" \`. T ")) 31 | print(" Welcome to " + faded(" .--------------.___________) \ | T ")) 32 | print(" the DIY Lang " + faded(" |//////////////|___________[ ] ! T | ")) 33 | print(" REPL " + faded(" `--------------' ) ( | ! ")) 34 | print(" " + faded(" '-' ! ")) 35 | print(faded(" use " + eof + " to exit")) 36 | print("") 37 | 38 | if env is None: 39 | env = Environment() 40 | 41 | while True: 42 | try: 43 | source = read_expression() 44 | print(interpret(source, env)) 45 | except DiyLangError as e: 46 | print(colored("!", "red")) 47 | print(faded(str(e.__class__.__name__) + ":")) 48 | print(str(e)) 49 | except KeyboardInterrupt: 50 | msg = "Interrupted. " + faded("(Use " + eof + " to exit)") 51 | print("\n" + colored("! ", "red") + msg) 52 | except EOFError: 53 | print(faded("\nBye! o/")) 54 | sys.exit(0) 55 | except Exception as e: 56 | print(colored("! ", "red") + 57 | faded("The Python is showing through…")) 58 | print(faded(" " + str(e.__class__.__name__) + ":")) 59 | print(str(e)) 60 | 61 | 62 | def read_expression(): 63 | """Read from stdin until we have at least one s-expression""" 64 | 65 | exp = "" 66 | open_parens = 0 67 | while True: 68 | line, parens = read_line("> " if not exp.strip() else "… ") 69 | open_parens += parens 70 | exp += line 71 | if exp.strip() and open_parens <= 0: 72 | break 73 | 74 | return exp.strip() 75 | 76 | 77 | def read_line(prompt): 78 | """Return tuple of user input line and number of unclosed parens""" 79 | 80 | line = input(colored(prompt, "reset", "dark")) 81 | line = remove_comments(line + "\n") 82 | return line, line.count("(") - line.count(")") 83 | 84 | 85 | def colored(text, color, attr=None): 86 | attributes = { 87 | 'bold': 1, 88 | 'dark': 2 89 | } 90 | colors = { 91 | 'grey': 30, 92 | 'red': 31, 93 | 'green': 32, 94 | 'yellow': 33, 95 | 'blue': 34, 96 | 'magenta': 35, 97 | 'cyan': 36, 98 | 'white': 37, 99 | 'reset': 0 100 | } 101 | ansi_format = '\033[%dm' 102 | 103 | if os.getenv('ANSI_COLORS_DISABLED'): 104 | return text 105 | 106 | color = ansi_format % colors[color] 107 | attr = ansi_format % attributes[attr] if attr is not None else "" 108 | reset = ansi_format % colors['reset'] 109 | 110 | return color + attr + text + reset 111 | 112 | 113 | def faded(text): 114 | return colored(text, "reset", attr='dark') 115 | -------------------------------------------------------------------------------- /diylang/types.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """ 4 | This module holds some types we'll have use for along the way. 5 | 6 | It's your job to implement the Closure and Environment types. 7 | The DiyLangError class you can have for free :) 8 | """ 9 | 10 | 11 | class DiyLangError(Exception): 12 | """General DIY Lang error class.""" 13 | pass 14 | 15 | 16 | class Closure(object): 17 | 18 | def __init__(self, env, params, body): 19 | raise NotImplementedError("DIY") 20 | 21 | def __repr__(self): 22 | return "" % len(self.params) 23 | 24 | 25 | class Environment(object): 26 | 27 | def __init__(self, variables=None): 28 | self.bindings = variables if variables else {} 29 | 30 | def lookup(self, symbol): 31 | raise NotImplementedError("DIY") 32 | 33 | def extend(self, variables): 34 | raise NotImplementedError("DIY") 35 | 36 | def set(self, symbol, value): 37 | raise NotImplementedError("DIY") 38 | 39 | 40 | class String(object): 41 | 42 | """ 43 | Simple data object for representing DIY Lang strings. 44 | 45 | Ignore this until you start working on part 8. 46 | """ 47 | 48 | def __init__(self, val=""): 49 | self.val = val 50 | 51 | def __str__(self): 52 | return '"{}"'.format(self.val) 53 | 54 | def __eq__(self, other): 55 | return isinstance(other, String) and other.val == self.val 56 | -------------------------------------------------------------------------------- /example.diy: -------------------------------------------------------------------------------- 1 | ;; Some example DIY Lang code. By the end of the tutorial, our DIY Lang will be 2 | ;; able to run this. 3 | 4 | ;; To run the code: 5 | ;; 6 | ;; ./diy example.diy 7 | ;; 8 | 9 | (define fact 10 | ;; Factorial function 11 | (lambda (n) 12 | (if (eq n 0) 13 | 1 ; Factorial of 0 is 1 14 | (* n (fact (- n 1)))))) 15 | 16 | ;; When parsing the file, the last statement is returned 17 | (fact 5) 18 | -------------------------------------------------------------------------------- /parts/1.md: -------------------------------------------------------------------------------- 1 | ## Part 1: parsing 2 | 3 | The language we are making is an interpreted one. This means that we basically need to implement two things: a **parser** and an **evaluator**. In this first part, we implement the parser. 4 | 5 | The job of the parser is to convert the program into something the evaluator understands. The evaluator evaluates whatever the parser produces, and returns the result. Here is a nice diagram to explain everything: 6 | 7 | ``` 8 | 9 | +-----------+ +-------------+ 10 | text | | AST | | result 11 | +-------->| parser |+------>| evaluator |+--------> 12 | | | | | 13 | +-----------+ +-------------+ 14 | ``` 15 | 16 | The format produced by the parser is called the *abstract syntax tree* (AST) of the program. 17 | 18 | ### Our AST 19 | 20 | So what does our AST look like? Lets have a sneak peek. 21 | 22 | ```python 23 | >>> from diylang.parser import parse 24 | >>> program = """ 25 | ... (define fact 26 | ... ;; Factorial function 27 | ... (lambda (n) 28 | ... (if (eq n 0) 29 | ... 1 ; Factorial of 0 is 1, and we deny 30 | ... ; the existence of negative numbers 31 | ... (* n (fact (- n 1)))))) 32 | ... """ 33 | >>> parse(program) 34 | ['define', 'fact', ['lambda', ['n'], ['if', ['eq', 'n', 0], 1, ['*', 'n', ['fact', ['-', 'n', 1]]]]]] 35 | ``` 36 | 37 | The AST, then, is created as follows: 38 | 39 | - Comments are removed. 40 | - Symbols are represented as strings. 41 | + `"foo"` parses to `"foo"` 42 | - The symbols `#t` and `#f` are represented by Python's `True` and `False`, respectively. 43 | + `"#t"` parses to `True` 44 | - Integers are represented as Python integers. 45 | + `"42"` parses to `42` 46 | - The DIY Lang list expressions are represented as Python lists. 47 | `"(foo #f 100)"` parses to `["foo", False, 100]` 48 | - Nested expressions are parsed accordingly. 49 | + `"((+ (- 1 2) (* (- 4 1) 42)))"` parses to `[['+', ['-', 1, 2], ['*', ['-', 4, 1], 42]]]` 50 | 51 | ### Your turn 52 | 53 | The parsing is done in `parser.py`. It is your job to implement the `parse` function here. A lot of the gritty work of counting parentheses and such has been done for you, but you must stitch everything together. 54 | 55 | - Have a look at the provided functions in `diylang/parser.py` before you start. These should prove useful. 56 | - The following command runs the tests, stopping at the first one failed. 57 | 58 | ```bash 59 | nosetests tests/test_1_parsing.py --stop 60 | ``` 61 | - Run the tests and hack away until the tests are passing. Each test has a description, and you should probably read it if you get stuck. 62 | 63 | ### What's next? 64 | 65 | Go to [part 2](2.md) where we evaluate some simple expressions. 66 | -------------------------------------------------------------------------------- /parts/2.md: -------------------------------------------------------------------------------- 1 | ## Part 2: evaluating simple expressions 2 | 3 | Now that we have the parser up and running, it's time to start working on the evaluator. We'll start with some simple expressions, such as evaluating numbers or booleans, and a few of the most basic *special forms* in the language. 4 | 5 | - `quote` takes one argument which is returned directly (without being evaluated). 6 | - `atom` also takes a single argument, and returns true or false depending on whether the argument is an atom. 7 | - `eq` returns true if both its arguments are the same atom, and false otherwise. 8 | - The arithmetic operators (`+`, `-`, `*`, `/`, `mod` and `>`) all take two arguments, and do exactly what you would expect. 9 | 10 | This time, your work is in the file `evaluator.py`. 11 | 12 | ### Make it happen! 13 | 14 | The following command runs the tests, stopping at the first one failed. You know the drill. 15 | 16 | ```bash 17 | nosetests tests/test_2_evaluating_simple_expressions.py --stop 18 | ``` 19 | 20 | ### Play while you work 21 | 22 | Now that we are beginning to get an interpreter going, we can start testing the results in the read-eval-print-loop (REPL). 23 | 24 | Start the REPL from the command line, and try the language as we move along. 25 | 26 | ```bash 27 | ./repl 28 | ``` 29 | 30 | Remember, you'll need to restart the REPL for it to pick up any changes you make to the language. 31 | 32 | ### What's next? 33 | 34 | Head on to [part 3](3.md) where the expressions we take become slightly more complex. 35 | -------------------------------------------------------------------------------- /parts/3.md: -------------------------------------------------------------------------------- 1 | ## Part 3: evaluating complex expressions 2 | 3 | You have now already made a simple language, able to evaluate nested arithmetic expressions. It is time to add the ability to use control structures. 4 | 5 | For our language, an `if` statement will suffice. The `if` takes three arguments. The first one is the predicate `p`, which is always evaluated. The second **or** third argument is then evaluated and returned depending on the value of `p`. 6 | 7 | ### Make it happen! 8 | 9 | Go on, you know what to do. 10 | 11 | ```bash 12 | nosetests tests/test_3_evaluating_complex_expressions.py --stop 13 | ``` 14 | 15 | ### Play while you work 16 | 17 | Remember that the REPL is a great way to play around with your language while you work on it. 18 | 19 | ```bash 20 | ./repl 21 | > (if (> 42 100) 22 | … 'foo 23 | … 'bar) 24 | bar 25 | ``` 26 | 27 | ### What's next? 28 | 29 | Go to [part 4](4.md) where we add environments that'll enable us to work with variables. 30 | -------------------------------------------------------------------------------- /parts/4.md: -------------------------------------------------------------------------------- 1 | ## Part 4: working with variables 2 | 3 | So far, our interpreted language is only able do deal with expressions one by one. In a real programming language, we need to be able to store intermediate results in variables, and then later to look them up from the environment. 4 | 5 | We will start by implementing the `Environment` class (which can be found in `diylang/types.py`). Then we'll extend `evaluate` to handle expressions using defined variables, and the creation of new variables with the `define` form. 6 | 7 | Run the tests, and get going! 8 | 9 | ```bash 10 | nosetests tests/test_4_working_with_variables_and_environments.py --stop 11 | ``` 12 | 13 | ### What's next? 14 | 15 | In [part 5](5.md) the environments we just implemented enable us to make lexically scoped functions. 16 | -------------------------------------------------------------------------------- /parts/5.md: -------------------------------------------------------------------------------- 1 | ## Part 5: functions 2 | 3 | This part is the one you might have been waiting for. It's time to add functions to our little language. 4 | 5 | Functions are created with the `lambda` form, which returns an instance of `Closure` (find the class definition in `diylang/types.py`). The first few tests guide you to implement the `lambda` form correctly. 6 | 7 | The next tests concern calling functions. A function call happens when we evaluate a list in which the first element is a function closure. 8 | 9 | Finally, we handle some situations where function calls are done incorrectly, and make sure we give appropriate errors. 10 | 11 | ### Make it happen! 12 | 13 | This is probably the most difficult part of making the language, so don't worry if it takes a bit longer than the previous parts. 14 | 15 | ```bash 16 | nosetests tests/test_5_adding_functions_to_the_mix.py --stop 17 | ``` 18 | 19 | ### What's next? 20 | 21 | Ready for the last part of the language? In [part 6](6.md) we add a ways to work with lists. 22 | -------------------------------------------------------------------------------- /parts/6.md: -------------------------------------------------------------------------------- 1 | ## Part 6: working with lists 2 | 3 | This is the last section before we're done implementing the language, and compared to the last part it should be relatively easy. 4 | 5 | Any proper language needs good data structures, and *lists* are one of them. To be able to work properly with lists, we’ll introduce four new forms to our language: 6 | 7 | - `cons` is used to construct lists from a "head" element, and the rest of the list (the "tail"). 8 | - `head` extracts the first element of a list 9 | - `tail` returns the rest of the elements, once the first is dropped. 10 | - `empty` takes a list as input, and returns `#t` if it is empty and `#f` otherwise. 11 | 12 | Go on then, finish your language. 13 | 14 | ```bash 15 | nosetests tests/test_6_working_with_lists.py --stop 16 | ``` 17 | 18 | ### What's next? 19 | 20 | With the language implementation done, it's time to use our language in [part 7](7.md). 21 | -------------------------------------------------------------------------------- /parts/7.md: -------------------------------------------------------------------------------- 1 | ## Part 7: using your language 2 | 3 | Congratulations! You have now made your own programming language. It's time to take it out for a spin. 4 | 5 | Any programming language with respect for itself needs to have a standard library. This is a collection of useful functions provided along with the language core. 6 | 7 | In this section we will create various useful functions, all implemented purely in DIY Lang. No Python this time! 8 | 9 | For this part, you should consider the provided tests more like suggestions than something you *have* to follow. It is your language, after all, and you decide what should be in its library. 10 | 11 | ```bash 12 | nosetests tests/test_7_using_the_language.py --stop 13 | ``` 14 | 15 | ### What's next? 16 | 17 | You have created a programming language, *and* used it to write serious code. 18 | If you'd like to wrap up here, that's perfectly fine, and you should be proud of what you have done. 19 | 20 | If, however, you want more, then move on to [part 8](8.md) for some optional final challenges. 21 | -------------------------------------------------------------------------------- /parts/8.md: -------------------------------------------------------------------------------- 1 | ## Part 8: a few final touches 2 | 3 | Yay, you've already implemented your language, and even used it to create some code. Well done! 4 | 5 | *But wait, there's more!* At least if you want it to be. 6 | 7 | In this last part, we present a few suggestions for improvements and new features you could be adding to the language. 8 | 9 | For one thing, the language cannot represent strings yet. 10 | And what language with respect for itself does not support strings? 11 | There's also a few common control structures, such as `let` and `cond` we could add, as well as some syntactic sugar for defining functions. 12 | 13 | Treat these suggestions as just that -- suggestions. 14 | At this point the language is yours, and if you want something completely different, then that's perfectly fine. 15 | 16 | 17 | ```bash 18 | nosetests tests/test_8_final_touches.py --stop 19 | ``` 20 | -------------------------------------------------------------------------------- /parts/language.md: -------------------------------------------------------------------------------- 1 | ## The Language 2 | 3 | The syntax of our little language is Lisp-inspired. This is mainly to make it easy to write the parser. 4 | 5 | We will handle two types of expressions: **atoms** and **lists**. 6 | 7 | - Atoms can be numbers (`42`), booleans(`#t` and `#f`) or symbols (`foobar`). 8 | - Lists consists of a series of zero or more expressions (other atoms or lists) separated by spaces and enclosed by parentheses. 9 | 10 | ### Evaluation rules 11 | 12 | - Numbers and booleans evaluate to themselves. 13 | - Symbols are treated as variable references. When evaluated, their values are looked up in the environment. 14 | - Lists are treated as function calls (or calls to the special forms built into the language). 15 | - Anything in between semicolons (`;`) and the end of a line is considered a comment and ignored. 16 | 17 | ### Special Forms 18 | 19 | The language will have a set of "special forms". These are the construct built into the language. If a list expression is evaluated, and the first element is one of a number of defined symbols, the special form is executed: 20 | 21 | Here is a brief explanation of each form: 22 | 23 | - `quote` takes one argument which is returned without it being evaluated. 24 | - `atom` is a predicate indicating whether or not it's one argument is an atom. 25 | - `eq` returns true (`#t`) if both its arguments are the same atom. 26 | - `+`, `-`, `*`, `/`, `mod` and `>` all take two arguments, and does exactly what you would expect. (Note that since we have no floating point numbers, the `/` represent integer division.) 27 | - `if` is the conditional, taking three arguments. It's return value is the result of evaluating the second or third argument, depending on the value of the first one. 28 | - `define` is used to define new variables in the environment. 29 | - `lambda` creates function closures. 30 | - `cons` is used to construct lists from a head (element) and the tail (list). 31 | - `head` returns the first element of a list. 32 | - `tail` returns all but the first element of a list. 33 | 34 | ### Function calls 35 | 36 | If a list is evaluated, and the first element is something other than one of the special forms, it is expected to be a function closure. Function closures are created using the `lambda` form. 37 | 38 | Here is a (rather silly) example showing how to define and use a function. 39 | 40 | ```lisp 41 | (define my-function 42 | ;; This function returns 42, unless the argument 43 | ;; actually is 42. In that case, we return 1000. 44 | (lambda (n) 45 | (if (eq n 42) 46 | 1000 47 | 42))) 48 | 49 | (my-function 42) ;; => 1000 50 | ``` 51 | 52 | This might for some be the most "magic" part, and one that you hopefully will understand a lot better after implementing the language. 53 | -------------------------------------------------------------------------------- /parts/python.md: -------------------------------------------------------------------------------- 1 | ## Python Cheat Sheet 2 | 3 | This is not an introduction to Python. 4 | For that, see the [Python tutorial](https://docs.python.org/2/tutorial/) or the [Python module index](https://docs.python.org/3/py-modindex.html). 5 | Instead, this lists some tips and pointers that will prove useful when working on your language. 6 | 7 | ### Lists 8 | 9 | Lists will comprise our ASTs, so you'll need lists pretty early on. The [tutorial page on lists](https://docs.python.org/2/tutorial/datastructures.html#more-on-lists) should prove useful. 10 | 11 | ### Dictionaries 12 | 13 | We'll be using [dictionaries](https://docs.python.org/2/library/stdtypes.html#typesmapping) when representing the program environments. 14 | 15 | - Remember that dicts are mutable in Python. Use the [`copy`](https://docs.python.org/2/library/stdtypes.html#dict.copy) function when a copy is needed. 16 | - To update a dictionary with values from another, use [`update`](https://docs.python.org/2/library/stdtypes.html#set.update). 17 | - You will find yourself needing to make a dictionary from a list of keys and a list of values. To do so, combine the [`dict`](https://docs.python.org/2/library/functions.html#func-dict) and [`zip`](https://docs.python.org/2/library/functions.html#zip) functions like this: 18 | 19 | ```python 20 | >>> dict(zip(["foo", "bar"], [1, 2])) 21 | {'foo': 1, 'bar': 2} 22 | ``` 23 | 24 | Read more about dicts in the [documentation](https://docs.python.org/2/tutorial/datastructures.html#dictionaries). 25 | 26 | ### Strings 27 | 28 | - Strings works in many ways like lists. Thus, you can substring using indices: 29 | 30 | ```python 31 | >>> "hello world"[6:] 32 | 'world' 33 | ``` 34 | 35 | - Remove unwanted whitespace using [`str.strip()`](https://docs.python.org/2/library/stdtypes.html#str.strip). 36 | 37 | - It is also useful to know about how to do [string interpolation](https://docs.python.org/2/library/stdtypes.html#string-formatting-operations) in Python. 38 | 39 | ```python 40 | >>> "Hey, %s language!" % "cool" 41 | 'Hey, cool language!' 42 | >>> "%d bottles of %s on the wall" % (99, "beer") 43 | '99 bottles of beer on the wall' 44 | >>> "%(num)s bottles of %(what)s on the %(where)s, %(num)d bottles of %(what)s" \ 45 | ... % {"num": 99, "what": "beer", "where": "wall"} 46 | '99 bottles of beer on the wall, 99 bottles of beer' 47 | ``` 48 | 49 | ### Classes 50 | 51 | When defining a class, all methods take a special argument `self` as the first argument. The `__init__` method works as constructor for the class. 52 | 53 | ```python 54 | class Knight: 55 | 56 | def __init__(self, sound): 57 | self.sound = sound 58 | 59 | def speak(self): 60 | print self.sound 61 | ``` 62 | 63 | You don't provide the `self` when creating instances or calling methods: 64 | 65 | ```python 66 | >>> knight = Knight("ni") 67 | >>> knight.speak() 68 | ni 69 | ``` 70 | 71 | ### Default argument values 72 | 73 | One thing it is easy to be bitten by is the way Python handles default function argument values. These are members of the function itself, and not "reset" every time the function is called. 74 | 75 | ```python 76 | >>> def function(data=[]): 77 | ... data.append(1) 78 | ... return data 79 | ... 80 | >>> function() 81 | [1] 82 | >>> function() 83 | [1, 1] 84 | >>> function() 85 | [1, 1, 1] 86 | ``` 87 | 88 | Beware this when you implement the `Environment` class. 89 | -------------------------------------------------------------------------------- /repl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | from os.path import dirname, relpath, join 6 | 7 | from diylang.interpreter import interpret_file 8 | from diylang.repl import repl 9 | from diylang.types import Environment, DiyLangError 10 | 11 | env = Environment() 12 | 13 | try: 14 | interpret_file(join(dirname(relpath(__file__)), 'stdlib.diy'), env) 15 | except DiyLangError as e: 16 | # Just ignore exceptions from stdlib. 17 | # These will generally fail until part 6 is done anyways. 18 | pass 19 | 20 | if len(sys.argv) > 1: 21 | print(interpret_file(sys.argv[1], env)) 22 | else: 23 | repl(env) 24 | -------------------------------------------------------------------------------- /repl.bat: -------------------------------------------------------------------------------- 1 | @python repl %* 2 | -------------------------------------------------------------------------------- /run-tests.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | rem Batch script for running the test suite. 3 | rem Requires the python package `nose` to be installed. 4 | set nosetests=call python -W ignore::DeprecationWarning -m nose 5 | 6 | %nosetests% ^ 7 | tests/test_provided_code.py ^ 8 | tests/test_1_parsing.py ^ 9 | tests/test_2_evaluating_simple_expressions.py ^ 10 | tests/test_3_evaluating_complex_expressions.py ^ 11 | tests/test_4_working_with_variables_and_environments.py ^ 12 | tests/test_5_adding_functions_to_the_mix.py ^ 13 | tests/test_6_working_with_lists.py ^ 14 | tests/test_7_using_the_language.py ^ 15 | tests/test_8_final_touches.py ^ 16 | tests/test_sanity_checks.py ^ 17 | --stop 18 | -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Bash script for running the test suite every time a file is changed. 4 | # Requires the python package `nose` to be installed, as well as the 5 | # `inotifytools` commands. 6 | 7 | function run_tests { 8 | nosetests \ 9 | tests/test_provided_code.py \ 10 | tests/test_1_parsing.py \ 11 | tests/test_2_evaluating_simple_expressions.py \ 12 | tests/test_3_evaluating_complex_expressions.py \ 13 | tests/test_4_working_with_variables_and_environments.py \ 14 | tests/test_5_adding_functions_to_the_mix.py \ 15 | tests/test_6_working_with_lists.py \ 16 | tests/test_7_using_the_language.py \ 17 | tests/test_8_final_touches.py \ 18 | tests/test_sanity_checks.py \ 19 | --stop 20 | } 21 | 22 | run_tests 23 | 24 | function log_test_run { 25 | msg="Prepared to run tests on new changes..." 26 | echo -e "\033[1;37m> $msg\033[0m" 27 | } 28 | 29 | if command -v inotifywait >/dev/null; then 30 | log_test_run 31 | while inotifywait -q -r -e modify . ; do 32 | run_tests 33 | done 34 | fi 35 | 36 | if command -v fswatch >/dev/null; then 37 | log_test_run 38 | fswatch . | (while read; do run_tests; done) 39 | fi 40 | -------------------------------------------------------------------------------- /stdlib.diy: -------------------------------------------------------------------------------- 1 | ;; Some logical operators. 2 | 3 | (define not 4 | (lambda (b) 5 | (if b #f #t))) 6 | 7 | ;; DIY -- Implement the rest of your standard library 8 | ;; here as part 7 of the workshop. 9 | -------------------------------------------------------------------------------- /tests/test_1_parsing.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nose.tools import assert_equals, assert_true, assert_raises_regexp 4 | 5 | from diylang.ast import is_integer 6 | from diylang.parser import parse, unparse 7 | from diylang.types import DiyLangError 8 | 9 | 10 | def test_parse_single_symbol(): 11 | """TEST 1.1: Parsing a single symbol. 12 | 13 | Symbols are represented by text strings. Parsing a single atom should 14 | result in an AST consisting of only that symbol.""" 15 | 16 | assert_equals('foo', parse('foo')) 17 | 18 | 19 | def test_parse_boolean(): 20 | """TEST 1.2: Parsing single booleans. 21 | 22 | Booleans are the special symbols #t and #f. In the ASTs they are 23 | represented by Python's True and False, respectively.""" 24 | 25 | assert_equals(True, parse('#t')) 26 | assert_equals(False, parse('#f')) 27 | 28 | 29 | def test_parse_integer(): 30 | """TEST 1.3: Parsing single integer. 31 | 32 | Integers are represented in the ASTs as Python ints. 33 | 34 | Tip: String objects have a handy .isdigit() method. 35 | """ 36 | 37 | assert_equals(42, parse('42')) 38 | assert_equals(1337, parse('1337')) 39 | assert_true(is_integer(parse('42')), 40 | "Numbers should be represented as integers in the AST") 41 | 42 | 43 | def test_parse_list_of_symbols(): 44 | """TEST 1.4: Parsing list of only symbols. 45 | 46 | A list is represented by a number of elements surrounded by parens. Python 47 | lists are used to represent lists as ASTs. 48 | 49 | Tip: The useful helper function `find_matching_paren` is already provided 50 | in `parse.py`. 51 | """ 52 | 53 | assert_equals(['foo', 'bar', 'baz'], parse('(foo bar baz)')) 54 | assert_equals([], parse('()')) 55 | 56 | 57 | def test_parse_list_of_mixed_types(): 58 | """TEST 1.5: Parsing a list containing different types. 59 | 60 | When parsing lists, make sure each of the sub-expressions are also parsed 61 | properly.""" 62 | 63 | assert_equals(['foo', True, 123], parse('(foo #t 123)')) 64 | 65 | 66 | def test_parse_on_nested_list(): 67 | """TEST 1.6: Parsing should also handle nested lists properly.""" 68 | 69 | program = '(foo (bar ((#t)) x) (baz y))' 70 | ast = ['foo', 71 | ['bar', [[True]], 'x'], 72 | ['baz', 'y']] 73 | assert_equals(ast, parse(program)) 74 | 75 | 76 | def test_parse_exception_missing_paren(): 77 | """TEST 1.7: The proper exception should be raised if the expression 78 | is incomplete.""" 79 | 80 | with assert_raises_regexp(DiyLangError, 'Incomplete expression'): 81 | parse('(foo (bar x y)') 82 | 83 | 84 | def test_parse_exception_extra_paren(): 85 | """TEST 1.8: Another exception is raised if the expression is too large. 86 | 87 | The parse function expects to receive only one single expression. Anything 88 | more than this, should result in the proper exception.""" 89 | 90 | with assert_raises_regexp(DiyLangError, 'Expected EOF'): 91 | parse('(foo (bar x y)))') 92 | 93 | 94 | def test_parse_with_extra_whitespace(): 95 | """TEST 1.9: Excess whitespace should be removed. 96 | 97 | Tip: String objects have a handy .strip() method. 98 | """ 99 | 100 | program = """ 101 | 102 | (program with much whitespace) 103 | """ 104 | expected_ast = ['program', 'with', 'much', 'whitespace'] 105 | assert_equals(expected_ast, parse(program)) 106 | 107 | 108 | def test_parse_comments(): 109 | """TEST 1.10: All comments should be stripped away as part of 110 | the parsing.""" 111 | 112 | program = """ 113 | ;; this first line is a comment 114 | (define variable 115 | ; here is another comment 116 | (if #t 117 | 42 ; inline comment! 118 | (something else))) 119 | """ 120 | expected_ast = ['define', 'variable', 121 | ['if', True, 122 | 42, 123 | ['something', 'else']]] 124 | assert_equals(expected_ast, parse(program)) 125 | 126 | 127 | def test_parse_larger_example(): 128 | """TEST 1.11: Test a larger example to check that everything works 129 | as expected""" 130 | 131 | program = """ 132 | (define fact 133 | ;; Factorial function 134 | (lambda (n) 135 | (if (<= n 1) 136 | 1 ; Factorial of 0 is 1, and we deny 137 | ; the existence of negative numbers 138 | (* n (fact (- n 1)))))) 139 | """ 140 | ast = ['define', 'fact', 141 | ['lambda', ['n'], 142 | ['if', ['<=', 'n', 1], 143 | 1, 144 | ['*', 'n', ['fact', ['-', 'n', 1]]]]]] 145 | assert_equals(ast, parse(program)) 146 | 147 | # The following tests checks that quote expansion works properly 148 | 149 | 150 | def test_expand_single_quoted_symbol(): 151 | """TEST 1.12: Quoting is a shorthand syntax for calling the `quote` form. 152 | 153 | Examples: 154 | 155 | 'foo -> (quote foo) 156 | '(foo bar) -> (quote (foo bar)) 157 | 158 | """ 159 | assert_equals(["foo", ["quote", "nil"]], parse("(foo 'nil)")) 160 | 161 | 162 | def test_nested_quotes(): 163 | """TEST 1.13: Nested quotes should work as expected""" 164 | assert_equals(["quote", ["quote", ["quote", ["quote", "foo"]]]], 165 | parse("''''foo")) 166 | 167 | 168 | def test_expand_crazy_quote_combo(): 169 | """TEST 1.14: One final test to see that quote expansion works.""" 170 | 171 | source = "'(this ''''(makes ''no) 'sense)" 172 | assert_equals(source, unparse(parse(source))) 173 | -------------------------------------------------------------------------------- /tests/test_2_evaluating_simple_expressions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nose.tools import assert_equals, assert_raises 4 | 5 | from diylang.types import DiyLangError 6 | from diylang.types import Environment 7 | from diylang.evaluator import evaluate 8 | from diylang.parser import parse 9 | 10 | """ 11 | We will start by implementing evaluation of simple expressions. 12 | """ 13 | 14 | 15 | def test_evaluating_boolean(): 16 | """TEST 2.1: Booleans should evaluate to themselves.""" 17 | 18 | assert_equals(True, evaluate(True, Environment())) 19 | assert_equals(False, evaluate(False, Environment())) 20 | 21 | 22 | def test_evaluating_integer(): 23 | """TEST 2.2: ...and so should integers.""" 24 | assert_equals(42, evaluate(42, Environment())) 25 | 26 | 27 | def test_evaluating_quote(): 28 | """TEST 2.3: When a call is done to the `quote` form, the argument should 29 | be returned without being evaluated. 30 | 31 | (quote foo) -> foo 32 | """ 33 | 34 | assert_equals("foo", evaluate(["quote", "foo"], Environment())) 35 | assert_equals([1, 2, False], 36 | evaluate(["quote", [1, 2, False]], Environment())) 37 | assert_equals([], evaluate(["quote", []], Environment())) 38 | 39 | 40 | def test_evaluating_atom_function(): 41 | """TEST 2.4: The `atom` form is used to determine whether an expression is 42 | an atom. 43 | 44 | Atoms are expressions that are not list, i.e. integers, booleans or 45 | symbols. Remember that the argument to `atom` must be evaluated before the 46 | check is done. 47 | """ 48 | 49 | assert_equals(True, evaluate(["atom", True], Environment())) 50 | assert_equals(True, evaluate(["atom", False], Environment())) 51 | assert_equals(True, evaluate(["atom", 42], Environment())) 52 | assert_equals(True, evaluate(["atom", ["quote", "foo"]], Environment())) 53 | assert_equals(False, evaluate(["atom", ["quote", [1, 2]]], Environment())) 54 | 55 | 56 | def test_evaluating_eq_function(): 57 | """TEST 2.5: The `eq` form is used to check whether two expressions are 58 | the same atom.""" 59 | 60 | assert_equals(True, evaluate(["eq", 1, 1], Environment())) 61 | assert_equals(False, evaluate(["eq", 1, 2], Environment())) 62 | 63 | # From this point, the ASTs might sometimes be too long or cumbersome to 64 | # write down explicitly, and we'll use `parse` to make them for us. 65 | # Remember, if you need to have a look at exactly what is passed to 66 | # `evaluate`, just add a print statement in the test (or in `evaluate`). 67 | 68 | assert_equals(True, evaluate(parse("(eq 'foo 'foo)"), Environment())) 69 | assert_equals(False, evaluate(parse("(eq 'foo 'bar)"), Environment())) 70 | 71 | # Lists are never equal, because lists are not atoms 72 | assert_equals( 73 | False, evaluate(parse("(eq '(1 2 3) '(1 2 3))"), Environment())) 74 | 75 | 76 | def test_basic_math_operators(): 77 | """TEST 2.6: To be able to do anything useful, we need some basic math 78 | operators. 79 | 80 | Since we only operate with integers, `/` must represent integer division. 81 | `mod` is the modulo operator. 82 | """ 83 | 84 | assert_equals(4, evaluate(["+", 2, 2], Environment())) 85 | assert_equals(1, evaluate(["-", 2, 1], Environment())) 86 | assert_equals(3, evaluate(["/", 6, 2], Environment())) 87 | assert_equals(3, evaluate(["/", 7, 2], Environment())) 88 | assert_equals(6, evaluate(["*", 2, 3], Environment())) 89 | assert_equals(1, evaluate(["mod", 7, 2], Environment())) 90 | assert_equals(True, evaluate([">", 7, 2], Environment())) 91 | assert_equals(False, evaluate([">", 2, 7], Environment())) 92 | assert_equals(False, evaluate([">", 7, 7], Environment())) 93 | 94 | 95 | def test_math_operators_only_work_on_numbers(): 96 | """TEST 2.7: The math functions should only allow numbers as arguments.""" 97 | 98 | with assert_raises(DiyLangError): 99 | evaluate(parse("(+ 1 'foo)"), Environment()) 100 | with assert_raises(DiyLangError): 101 | evaluate(parse("(- 1 'foo)"), Environment()) 102 | with assert_raises(DiyLangError): 103 | evaluate(parse("(/ 1 'foo)"), Environment()) 104 | with assert_raises(DiyLangError): 105 | evaluate(parse("(mod 1 'foo)"), Environment()) 106 | -------------------------------------------------------------------------------- /tests/test_3_evaluating_complex_expressions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nose.tools import assert_equals 4 | 5 | from diylang.types import Environment 6 | from diylang.evaluator import evaluate 7 | from diylang.parser import parse 8 | 9 | 10 | def test_nested_expression(): 11 | """TEST 3.1: Remember, functions should evaluate their arguments. 12 | 13 | (Except `quote` and `if`, that is, which aren't really functions...) Thus, 14 | nested expressions should work just fine without any further work at this 15 | point. 16 | 17 | If this test is failing, make sure that `+`, `>` and so on is evaluating 18 | their arguments before operating on them. 19 | """ 20 | 21 | ast = parse("(eq #f (> (- (+ 1 3) (* 2 (mod 7 4))) 4))") 22 | assert_equals(True, evaluate(ast, Environment())) 23 | 24 | 25 | def test_basic_if_statement(): 26 | """TEST 3.2: If statements are the basic control structures. 27 | 28 | The `if` should first evaluate its first argument. If this evaluates to 29 | true, then the second argument is evaluated and returned. Otherwise the 30 | third and last argument is evaluated and returned instead. 31 | """ 32 | 33 | assert_equals(42, evaluate(parse("(if #t 42 1000)"), Environment())) 34 | assert_equals(1000, evaluate(parse("(if #f 42 1000)"), Environment())) 35 | assert_equals(True, evaluate(parse("(if #t #t #f)"), Environment())) 36 | 37 | 38 | def test_that_only_correct_branch_is_evaluated(): 39 | """TEST 3.3: The branch of the if statement that is discarded should never 40 | be evaluated.""" 41 | 42 | ast = parse("(if #f (this should not be evaluated) 42)") 43 | assert_equals(42, evaluate(ast, Environment())) 44 | 45 | 46 | def test_if_with_sub_expressions(): 47 | """TEST 3.4: A final test with a more complex if expression. 48 | This test should already be passing if the above ones are.""" 49 | 50 | ast = parse(""" 51 | (if (> 1 2) 52 | (- 1000 1) 53 | (+ 40 (- 3 1))) 54 | """) 55 | assert_equals(42, evaluate(ast, Environment())) 56 | 57 | 58 | def test_that_quote_does_not_evaluate_its_argument(): 59 | """TEST 3.5: Calling `quote`, should still return its argument without 60 | evaluating it. This test should already be passing, but lets just make sure 61 | that `quote` still works as intended now that we have a few more powerful 62 | features. 63 | """ 64 | 65 | ast = parse(""" 66 | '(if (> 1 50) 67 | (- 1000 1) 68 | #f) 69 | """) 70 | assert_equals(['if', ['>', 1, 50], ['-', 1000, 1], False], 71 | evaluate(ast, Environment())) 72 | -------------------------------------------------------------------------------- /tests/test_4_working_with_variables_and_environments.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nose.tools import assert_equals, assert_raises_regexp 4 | 5 | from diylang.types import DiyLangError, Environment 6 | from diylang.evaluator import evaluate 7 | from diylang.parser import parse 8 | 9 | """ 10 | Before we go on to evaluating programs using variables, we need to implement 11 | an environment to store them in. 12 | 13 | It is time to fill in the blanks in the `Environment` class located in 14 | `types.py`. 15 | """ 16 | 17 | 18 | def test_simple_lookup(): 19 | """TEST 4.1: An environment should store variables and provide lookup. 20 | 21 | Tip: Implement this in the lookup() method in types.py. 22 | """ 23 | 24 | env = Environment({"var": 42}) 25 | assert_equals(42, env.lookup("var")) 26 | 27 | 28 | def test_lookup_on_missing_raises_exception(): 29 | """TEST 4.2: When looking up an undefined symbol, an error should be raised. 30 | 31 | The error message should contain the relevant symbol, and inform that it 32 | has not been defined. 33 | """ 34 | 35 | with assert_raises_regexp(DiyLangError, "my-missing-var"): 36 | empty_env = Environment() 37 | empty_env.lookup("my-missing-var") 38 | 39 | 40 | def test_lookup_from_inner_env(): 41 | """TEST 4.3: The `extend` function returns a new environment extended with 42 | more bindings. 43 | 44 | Tip: The Dictionary class has a convenient .update method. 45 | """ 46 | 47 | env = Environment({"foo": 42}) 48 | env = env.extend({"bar": True}) 49 | assert_equals(42, env.lookup("foo")) 50 | assert_equals(True, env.lookup("bar")) 51 | 52 | 53 | def test_lookup_deeply_nested_var(): 54 | """TEST 4.4: Extending overwrites old bindings to the same variable 55 | name.""" 56 | 57 | env = Environment({"a": 1}).extend({"b": 2}).extend({"c": 3}). \ 58 | extend({"foo": 100}) 59 | assert_equals(100, env.lookup("foo")) 60 | 61 | 62 | def test_extend_returns_new_environment(): 63 | """TEST 4.5: The extend method should create a new environment, leaving the 64 | old one unchanged.""" 65 | 66 | env = Environment({"foo": 1}) 67 | extended = env.extend({"foo": 2}) 68 | 69 | assert_equals(1, env.lookup("foo")) 70 | assert_equals(2, extended.lookup("foo")) 71 | 72 | 73 | def test_set_changes_environment_in_place(): 74 | """TEST 4.6: When calling `set` the environment should be updated""" 75 | 76 | env = Environment() 77 | env.set("foo", 2) 78 | assert_equals(2, env.lookup("foo")) 79 | 80 | 81 | def test_redefine_variables_illegal(): 82 | """TEST 4.7: Variables can only be defined once. 83 | 84 | Setting a variable in an environment where it is already defined should 85 | result in an appropriate error. 86 | """ 87 | 88 | env = Environment({"foo": 1}) 89 | with assert_raises_regexp(DiyLangError, "already defined"): 90 | env.set("foo", 2) 91 | 92 | 93 | """ 94 | With the `Environment` working, it's time to implement evaluation of 95 | expressions with variables. 96 | """ 97 | 98 | 99 | def test_evaluating_symbol(): 100 | """TEST 4.8: Symbols (other than #t and #f) are treated as variable references. 101 | 102 | When evaluating a symbol, the corresponding value should be looked up in 103 | the environment. 104 | """ 105 | 106 | env = Environment({"foo": 42}) 107 | assert_equals(42, evaluate("foo", env)) 108 | 109 | 110 | def test_lookup_missing_variable(): 111 | """TEST 4.9: Referencing undefined variables should raise an appropriate 112 | exception. 113 | 114 | This test should already be working if you implemented the environment 115 | correctly. 116 | """ 117 | 118 | with assert_raises_regexp(DiyLangError, "my-var"): 119 | evaluate("my-var", Environment()) 120 | 121 | 122 | def test_define(): 123 | """TEST 4.10: Test of simple define statement. 124 | 125 | The `define` form is used to define new bindings in the environment. 126 | A `define` call should result in a change in the environment. What you 127 | return from evaluating the definition is not important (although it 128 | affects what is printed in the REPL). 129 | """ 130 | 131 | env = Environment() 132 | evaluate(parse("(define x 1000)"), env) 133 | assert_equals(1000, env.lookup("x")) 134 | 135 | 136 | def test_define_with_wrong_number_of_arguments(): 137 | """TEST 4.11: Defines should have exactly two arguments, or raise an error. 138 | 139 | This type of check could benefit the other forms we implement as well, 140 | and you might want to add them elsewhere. It quickly get tiresome to 141 | test for this however, so the tests won't require you to. 142 | """ 143 | 144 | with assert_raises_regexp(DiyLangError, "Wrong number of arguments"): 145 | evaluate(parse("(define x)"), Environment()) 146 | 147 | with assert_raises_regexp(DiyLangError, "Wrong number of arguments"): 148 | evaluate(parse("(define x 1 2)"), Environment()) 149 | 150 | 151 | def test_define_with_non_symbol_as_variable(): 152 | """TEST 4.12: Defines require the first argument to be a symbol.""" 153 | 154 | with assert_raises_regexp(DiyLangError, "not a symbol"): 155 | evaluate(parse("(define #t 42)"), Environment()) 156 | 157 | 158 | def test_define_should_evaluate_the_argument(): 159 | """TEST 4.13: Defines should evaluate the argument before storing it in 160 | the environment. 161 | """ 162 | 163 | env = Environment() 164 | evaluate(parse("(define x (+ 1 41))"), env) 165 | assert_equals(42, env.lookup("x")) 166 | 167 | 168 | def test_variable_lookup_after_define(): 169 | """TEST 4.14: Test define and lookup variable in same environment. 170 | 171 | This test should already be working when the above ones are passing. 172 | """ 173 | 174 | env = Environment() 175 | evaluate(parse("(define foo (+ 2 2))"), env) 176 | assert_equals(4, evaluate("foo", env)) 177 | -------------------------------------------------------------------------------- /tests/test_5_adding_functions_to_the_mix.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nose.tools import assert_equals, assert_raises_regexp, \ 4 | assert_raises, assert_true, assert_is_instance 5 | 6 | from diylang.ast import is_list 7 | from diylang.evaluator import evaluate 8 | from diylang.parser import parse 9 | from diylang.types import Closure, DiyLangError, Environment 10 | 11 | """ 12 | This part is all about defining and using functions. 13 | 14 | We'll start by implementing the `lambda` form which is used to create function 15 | closures. 16 | """ 17 | 18 | 19 | def test_lambda_evaluates_to_closure(): 20 | """TEST 5.1: The lambda form should evaluate to a Closure 21 | 22 | Tip: You'll find the Closure class ready in types.py, just finish the 23 | constructor. 24 | """ 25 | 26 | ast = ["lambda", [], 42] 27 | closure = evaluate(ast, Environment()) 28 | assert_is_instance(closure, Closure) 29 | 30 | 31 | def test_lambda_closure_keeps_defining_env(): 32 | """TEST 5.2: The closure should keep a copy of the environment where it was 33 | defined. 34 | 35 | Once we start calling functions later, we'll need access to the environment 36 | from when the function was created in order to resolve all free variables. 37 | """ 38 | 39 | env = Environment({"foo": 1, "bar": 2}) 40 | ast = ["lambda", [], 42] 41 | closure = evaluate(ast, env) 42 | assert_equals(closure.env, env) 43 | 44 | 45 | def test_lambda_closure_holds_function(): 46 | """TEST 5.3: The closure contains the parameter list and function body 47 | too.""" 48 | 49 | closure = evaluate(parse("(lambda (x y) (+ x y))"), Environment()) 50 | 51 | assert_equals(["x", "y"], closure.params) 52 | assert_equals(["+", "x", "y"], closure.body) 53 | 54 | 55 | def test_lambda_arguments_are_lists(): 56 | """TEST 5.4: The parameters of a `lambda` should be a list.""" 57 | 58 | closure = evaluate(parse("(lambda (x y) (+ x y))"), Environment()) 59 | assert_true(is_list(closure.params)) 60 | 61 | with assert_raises(DiyLangError): 62 | evaluate(parse("(lambda not-a-list (body of fn))"), Environment()) 63 | 64 | 65 | def test_lambda_number_of_arguments(): 66 | """TEST 5.5: The `lambda` form should expect exactly two arguments.""" 67 | 68 | with assert_raises_regexp(DiyLangError, "number of arguments"): 69 | evaluate(parse("(lambda (foo) (bar) (baz))"), Environment()) 70 | 71 | 72 | def test_defining_lambda_with_error_in_body(): 73 | """TEST 5.6: The function body should not be evaluated when the lambda is 74 | defined. 75 | 76 | The call to `lambda` should return a function closure holding, among other 77 | things the function body. The body should not be evaluated before the 78 | function is called. 79 | """ 80 | 81 | ast = parse(""" 82 | (lambda (x y) 83 | (function body ((that) would never) work)) 84 | """) 85 | assert_is_instance(evaluate(ast, Environment()), Closure) 86 | 87 | 88 | """ 89 | Now that we have the `lambda` form implemented, let's see if we can call some 90 | functions. 91 | 92 | When evaluating ASTs which are lists, if the first element isn't one of the 93 | special forms we have been working with so far, it is a function call. The 94 | first element of the list is the function, and the rest of the elements are 95 | arguments. 96 | """ 97 | 98 | 99 | def test_evaluating_call_to_closure(): 100 | """TEST 5.7: The first case we'll handle is when the AST is a list with an 101 | actual closure as the first element. 102 | 103 | In this first test, we'll start with a closure with no arguments and no 104 | free variables. All we need to do is to evaluate and return the function 105 | body. 106 | """ 107 | 108 | closure = evaluate(parse("(lambda () (+ 1 2))"), Environment()) 109 | ast = [closure] 110 | result = evaluate(ast, Environment()) 111 | assert_equals(3, result) 112 | 113 | 114 | def test_evaluating_call_to_closure_with_arguments(): 115 | """TEST 5.8: The function body must be evaluated in an environment where 116 | the parameters are bound. 117 | 118 | Create an environment where the function parameters (which are stored in 119 | the closure) are bound to the actual argument values in the function call. 120 | Use this environment when evaluating the function body. 121 | 122 | Tip: The `zip` and `dict` functions should prove useful when constructing 123 | the new environment. 124 | """ 125 | 126 | env = Environment() 127 | closure = evaluate(parse("(lambda (a b) (+ a b))"), env) 128 | ast = [closure, 4, 5] 129 | 130 | assert_equals(9, evaluate(ast, env)) 131 | 132 | 133 | def test_creating_closure_with_environment(): 134 | """TEST 5.9: The function parameters must properly shadow the outer scope's 135 | bindings. 136 | 137 | When the same bindings exist in the environment and function parameters, 138 | the function parameters must properly overwrite the environment bindings. 139 | """ 140 | 141 | env = Environment({"a": 42, "b": "foo"}) 142 | closure = evaluate(parse("(lambda (a b) (+ a b))"), env) 143 | ast = [closure, 4, 5] 144 | 145 | assert_equals(9, evaluate(ast, env)) 146 | 147 | 148 | def test_call_to_function_should_evaluate_arguments(): 149 | """TEST 5.10: Call to function should evaluate all arguments. 150 | 151 | When a function is applied, the arguments should be evaluated before being 152 | bound to the parameter names. 153 | """ 154 | 155 | env = Environment() 156 | closure = evaluate(parse("(lambda (a) (+ a 5))"), env) 157 | ast = [closure, parse("(if #f 0 (+ 10 10))")] 158 | 159 | assert_equals(25, evaluate(ast, env)) 160 | 161 | 162 | def test_evaluating_call_to_closure_with_free_variables(): 163 | """TEST 5.11: The body should be evaluated in the environment from the closure. 164 | 165 | The function's free variables, i.e. those not specified as part of the 166 | parameter list, should be looked up in the environment from where the 167 | function was defined. This is the environment included in the closure. Make 168 | sure this environment is used when evaluating the body. 169 | """ 170 | 171 | closure = evaluate(parse("(lambda (x) (+ x y))"), Environment({"y": 1})) 172 | ast = [closure, 0] 173 | result = evaluate(ast, Environment({"y": 2})) 174 | assert_equals(1, result) 175 | 176 | 177 | """ 178 | Okay, now we're able to evaluate ASTs with closures as the first element. But 179 | normally the closures don't just happen to be there all by themselves. 180 | Generally we'll find some expression, evaluate it to a closure, and then 181 | evaluate a new AST with the closure just like we did above. 182 | 183 | (some-exp arg1 arg2 ...) -> (closure arg1 arg2 ...) -> result-of-function-call 184 | 185 | """ 186 | 187 | 188 | def test_calling_very_simple_function_in_environment(): 189 | """TEST 5.12: A call to a symbol corresponds to a call to its value in the 190 | environment. 191 | 192 | When a symbol is the first element of the AST list, it is resolved to its 193 | value in the environment (which should be a function closure). An AST with 194 | the variables replaced with its value should then be evaluated instead. 195 | """ 196 | 197 | env = Environment() 198 | evaluate(parse("(define add (lambda (x y) (+ x y)))"), env) 199 | assert_is_instance(env.lookup("add"), Closure) 200 | 201 | result = evaluate(parse("(add 1 2)"), env) 202 | assert_equals(3, result) 203 | 204 | 205 | def test_calling_lambda_directly(): 206 | """TEST 5.13: It should be possible to define and call functions directly. 207 | 208 | A lambda definition in the call position of an AST should be evaluated, and 209 | then evaluated as before. 210 | """ 211 | 212 | ast = parse("((lambda (x) x) 42)") 213 | result = evaluate(ast, Environment()) 214 | assert_equals(42, result) 215 | 216 | 217 | def test_calling_complex_expression_which_evaluates_to_function(): 218 | """TEST 5.14: Actually, all ASTs that are lists beginning with anything except 219 | atoms, or with a symbol, should be evaluated and then called. 220 | 221 | In this test, a call is done to the if-expression. The `if` should be 222 | evaluated, which will result in a `lambda` expression. The lambda is 223 | evaluated, giving a closure. The result is an AST with a `closure` as the 224 | first element, which we already know how to evaluate. 225 | """ 226 | 227 | ast = parse(""" 228 | ((if #f 229 | wont-evaluate-this-branch 230 | (lambda (x) (+ x y))) 231 | 2) 232 | """) 233 | env = Environment({'y': 3}) 234 | assert_equals(5, evaluate(ast, env)) 235 | 236 | 237 | """ 238 | Now that we have the happy cases working, let's see what should happen when 239 | function calls are done incorrectly. 240 | """ 241 | 242 | 243 | def test_calling_atom_raises_exception(): 244 | """TEST 5.15: A function call to a non-function should result in an 245 | error.""" 246 | 247 | with assert_raises_regexp(DiyLangError, "not a function"): 248 | evaluate(parse("(#t 'foo 'bar)"), Environment()) 249 | with assert_raises_regexp(DiyLangError, "not a function"): 250 | evaluate(parse("(42)"), Environment()) 251 | 252 | 253 | def test_make_sure_arguments_to_functions_are_evaluated(): 254 | """TEST 5.16: The arguments passed to functions should be evaluated 255 | 256 | We should accept parameters that are produced through function 257 | calls. If you are seeing stack overflows, e.g. 258 | 259 | RuntimeError: maximum recursion depth exceeded while calling a Python 260 | object 261 | 262 | then you should double-check that you are properly evaluating the passed 263 | function arguments. 264 | """ 265 | 266 | env = Environment() 267 | res = evaluate(parse("((lambda (x) x) (+ 1 2))"), env) 268 | assert_equals(res, 3) 269 | 270 | 271 | def test_calling_with_wrong_number_of_arguments(): 272 | """TEST 5.17: Functions should raise exceptions when called with wrong 273 | number of arguments.""" 274 | 275 | env = Environment() 276 | evaluate(parse("(define fn (lambda (p1 p2) 'whatever))"), env) 277 | error_msg = "wrong number of arguments, expected 2 got 3" 278 | with assert_raises_regexp(DiyLangError, error_msg): 279 | evaluate(parse("(fn 1 2 3)"), env) 280 | 281 | 282 | def test_calling_nothing(): 283 | """TEST 5.18: Calling nothing should fail (remember to quote empty data 284 | lists)""" 285 | 286 | with assert_raises(DiyLangError): 287 | evaluate(parse("()"), Environment()) 288 | 289 | 290 | def test_make_sure_arguments_are_evaluated_in_correct_environment(): 291 | """Test 5.19: Function arguments should be evaluated in correct environment 292 | 293 | Function arguments should be evaluated in the environment where the 294 | function is called, and not in the environment captured by the function. 295 | """ 296 | 297 | env = Environment({'x': 3}) 298 | evaluate(parse("(define foo (lambda (x) x))"), env) 299 | env = env.extend({'x': 4}) 300 | assert_equals(evaluate(parse("(foo (+ x 1))"), env), 5) 301 | 302 | 303 | """ 304 | One final test to see that recursive functions are working as expected. 305 | The good news: this should already be working by now :) 306 | """ 307 | 308 | 309 | def test_calling_function_recursively(): 310 | """TEST 5.20: Tests that a named function is included in the environment where 311 | it is evaluated. 312 | """ 313 | 314 | env = Environment() 315 | evaluate(parse(""" 316 | (define my-fn 317 | ;; A meaningless, but recursive, function 318 | (lambda (x) 319 | (if (eq x 0) 320 | 42 321 | (my-fn (- x 1))))) 322 | """), env) 323 | 324 | assert_equals(42, evaluate(parse("(my-fn 0)"), env)) 325 | assert_equals(42, evaluate(parse("(my-fn 10)"), env)) 326 | -------------------------------------------------------------------------------- /tests/test_6_working_with_lists.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nose.tools import assert_equals, assert_raises 4 | 5 | from diylang.evaluator import evaluate 6 | from diylang.parser import parse 7 | from diylang.types import DiyLangError, Environment 8 | 9 | 10 | def test_creating_lists_by_quoting(): 11 | """TEST 6.1: One way to create lists is by quoting. 12 | 13 | We have already implemented `quote` so this test should already be 14 | passing. 15 | 16 | The reason we need to use `quote` here is that otherwise the expression 17 | would be seen as a call to the first element -- `1` in this case, which 18 | obviously isn't even a function. 19 | """ 20 | 21 | assert_equals(parse("(1 2 3 #t)"), 22 | evaluate(parse("'(1 2 3 #t)"), Environment())) 23 | 24 | 25 | def test_creating_list_with_cons(): 26 | """TEST 6.2: The `cons` functions prepends an element to the front of a 27 | list.""" 28 | 29 | result = evaluate(parse("(cons 0 '(1 2 3))"), Environment()) 30 | assert_equals(parse("(0 1 2 3)"), result) 31 | 32 | 33 | def test_creating_longer_lists_with_only_cons(): 34 | """TEST 6.3: `cons` needs to evaluate it's arguments. 35 | 36 | Like all the other special forms and functions in our language, `cons` is 37 | call-by-value. This means that the arguments must be evaluated before we 38 | create the list with their values. 39 | """ 40 | 41 | result = evaluate( 42 | parse("(cons 3 (cons (- 4 2) (cons 1 '())))"), Environment()) 43 | assert_equals(parse("(3 2 1)"), result) 44 | 45 | 46 | def test_getting_first_element_from_list(): 47 | """TEST 6.4: `head` extracts the first element of a list.""" 48 | 49 | assert_equals(1, evaluate(parse("(head '(1))"), Environment())) 50 | assert_equals(1, evaluate(parse("(head '(1 2 3 4 5))"), Environment())) 51 | 52 | 53 | def test_getting_first_element_from_empty_list(): 54 | """TEST 6.5: If the list is empty there is no first element, and `head 55 | should raise an error.""" 56 | 57 | with assert_raises(DiyLangError): 58 | evaluate(parse("(head (quote ()))"), Environment()) 59 | 60 | 61 | def test_getting_head_from_value(): 62 | """TEST 6.6: Must be list to get `head`.""" 63 | 64 | with assert_raises(DiyLangError): 65 | evaluate(parse("(head #t)"), Environment()) 66 | 67 | 68 | def test_getting_tail_of_list(): 69 | """TEST 6.7: `tail` returns the tail of the list. 70 | 71 | The tail is the list retained after removing the first element. 72 | """ 73 | 74 | assert_equals([2, 3], evaluate(parse("(tail '(1 2 3))"), Environment())) 75 | assert_equals([], evaluate(parse("(tail '(1))"), Environment())) 76 | 77 | 78 | def test_getting_tail_from_empty_list(): 79 | """TEST 6.8: If the list is empty there is no tail, and `tail` should raise 80 | an error.""" 81 | 82 | with assert_raises(DiyLangError): 83 | evaluate(parse("(tail (quote ()))"), Environment()) 84 | 85 | 86 | def test_getting_tail_from_value(): 87 | """TEST 6.9: Must be list to get `tail`.""" 88 | 89 | with assert_raises(DiyLangError): 90 | evaluate(parse("(tail 1)"), Environment()) 91 | 92 | 93 | def test_checking_whether_list_is_empty(): 94 | """TEST 6.10: The `empty` form checks whether or not a list is empty.""" 95 | 96 | assert_equals(False, evaluate(parse("(empty '(1 2 3))"), Environment())) 97 | assert_equals(False, evaluate(parse("(empty '(1))"), Environment())) 98 | 99 | assert_equals(True, evaluate(parse("(empty '())"), Environment())) 100 | assert_equals(True, evaluate(parse("(empty (tail '(1)))"), Environment())) 101 | 102 | assert_equals(False, evaluate( 103 | parse("(empty somelist)"), Environment({"somelist": [1, 2, 3]}))) 104 | assert_equals( 105 | True, 106 | evaluate(parse("(empty somelist)"), Environment({"somelist": []}))) 107 | 108 | 109 | def test_getting_empty_from_value(): 110 | """TEST 6.11: Must be list to see if empty.""" 111 | 112 | with assert_raises(DiyLangError): 113 | evaluate(parse("(empty 321)"), Environment()) 114 | -------------------------------------------------------------------------------- /tests/test_7_using_the_language.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nose.tools import assert_equals 4 | from os.path import dirname, relpath, join 5 | 6 | from diylang.interpreter import interpret, interpret_file 7 | from diylang.types import Environment 8 | 9 | env = Environment() 10 | path = join(dirname(relpath(__file__)), '..', 'stdlib.diy') 11 | interpret_file(path, env) 12 | 13 | """ 14 | Consider these tests as suggestions for what a standard library for 15 | your language could contain. Each test function tests the implementation 16 | of one stdlib function. 17 | 18 | Put the implementation in the file `stdlib.diy` at the root directory 19 | of the repository. The first function, `not` is already defined for you. 20 | It's your job to create the rest, or perhaps something completely different? 21 | 22 | Anything you put in `stdlib.diy` is also available from the REPL, so feel 23 | free to test things out there. 24 | 25 | $ ./repl 26 | > (not #t) 27 | #f 28 | 29 | PS: Note that in these tests, `interpret` is used. In addition to parsing 30 | and evaluating, it "unparses" the result, hence strings such as "#t" as the 31 | expected result instead of `True`. 32 | """ 33 | 34 | 35 | def test_not(): 36 | """TEST 7.1: Implementing (not ...)""" 37 | assert_equals("#t", interpret('(not #f)', env)) 38 | assert_equals("#f", interpret('(not #t)', env)) 39 | 40 | 41 | def test_or(): 42 | """TEST 7.2: Implementing (or ...)""" 43 | assert_equals("#f", interpret('(or #f #f)', env)) 44 | assert_equals("#t", interpret('(or #t #f)', env)) 45 | assert_equals("#t", interpret('(or #f #t)', env)) 46 | assert_equals("#t", interpret('(or #t #t)', env)) 47 | 48 | 49 | def test_and(): 50 | """TEST 7.3: Implementing (and ...)""" 51 | assert_equals("#f", interpret('(and #f #f)', env)) 52 | assert_equals("#f", interpret('(and #t #f)', env)) 53 | assert_equals("#f", interpret('(and #f #t)', env)) 54 | assert_equals("#t", interpret('(and #t #t)', env)) 55 | 56 | 57 | def test_xor(): 58 | """TEST 7.4: Implementing (xor ...)""" 59 | assert_equals("#f", interpret('(xor #f #f)', env)) 60 | assert_equals("#t", interpret('(xor #t #f)', env)) 61 | assert_equals("#t", interpret('(xor #f #t)', env)) 62 | assert_equals("#f", interpret('(xor #t #t)', env)) 63 | 64 | 65 | # The language core just contains the > operator. 66 | # It's time to implement the rest. 67 | 68 | 69 | def test_greater_or_equal(): 70 | """TEST 7.5: Implementing (>= ...)""" 71 | assert_equals("#f", interpret('(>= 1 2)', env)) 72 | assert_equals("#t", interpret('(>= 2 2)', env)) 73 | assert_equals("#t", interpret('(>= 2 1)', env)) 74 | 75 | 76 | def test_less_or_equal(): 77 | """TEST 7.6: Implementing (<= ...)""" 78 | assert_equals("#t", interpret('(<= 1 2)', env)) 79 | assert_equals("#t", interpret('(<= 2 2)', env)) 80 | assert_equals("#f", interpret('(<= 2 1)', env)) 81 | 82 | 83 | def test_less_than(): 84 | """TEST 7.7: Implementing (< ...)""" 85 | assert_equals("#t", interpret('(< 1 2)', env)) 86 | assert_equals("#f", interpret('(< 2 2)', env)) 87 | assert_equals("#f", interpret('(< 2 1)', env)) 88 | 89 | 90 | # Lets also implement some basic list functions. 91 | # These should be pretty easy with some basic recursion. 92 | 93 | def test_length(): 94 | """TEST 7.8: Count the number of element in the list. 95 | 96 | Tip: How many elements are there in the empty list? 97 | """ 98 | 99 | assert_equals("5", interpret("(length '(1 2 3 4 5))", env)) 100 | assert_equals("3", interpret("(length '(#t '(1 2 3) 'foo-bar))", env)) 101 | assert_equals("0", interpret("(length '())", env)) 102 | 103 | 104 | def test_sum(): 105 | """TEST 7.9: Calculate the sum of all elements in the list.""" 106 | 107 | assert_equals("5", interpret("(sum '(1 1 1 1 1))", env)) 108 | assert_equals("10", interpret("(sum '(1 2 3 4))", env)) 109 | assert_equals("0", interpret("(sum '())", env)) 110 | 111 | 112 | def test_range(): 113 | """TEST 7.10: Output a list with a range of numbers. 114 | 115 | The two arguments define the bounds of the (inclusive) bounds of the range. 116 | """ 117 | 118 | assert_equals("(1 2 3 4 5)", interpret("(range 1 5)", env)) 119 | assert_equals("(1)", interpret("(range 1 1)", env)) 120 | assert_equals("()", interpret("(range 2 1)", env)) 121 | 122 | 123 | def test_append(): 124 | """TEST 7.11: Append should merge two lists together.""" 125 | 126 | assert_equals("()", interpret("(append '() '())", env)) 127 | assert_equals("(1)", interpret("(append '() '(1))", env)) 128 | assert_equals("(2)", interpret("(append '(2) '())", env)) 129 | assert_equals("(1 2 3 4 5)", interpret("(append '(1 2) '(3 4 5))", env)) 130 | assert_equals("(#t #f 'maybe)", 131 | interpret("(append '(#t) '(#f 'maybe))", env)) 132 | 133 | 134 | def test_reverse(): 135 | """TEST 7.12: Reverse simply outputs the same list with elements in reverse 136 | order. 137 | 138 | Tip: See if you might be able to utilize the function you just made. 139 | """ 140 | 141 | assert_equals("()", interpret("(reverse '())", env)) 142 | assert_equals("(1)", interpret("(reverse '(1))", env)) 143 | assert_equals("(4 3 2 1)", interpret("(reverse '(1 2 3 4))", env)) 144 | 145 | 146 | # Next, our standard library should contain the three most fundamental 147 | # functions: `filter`, `map` and `reduce`. 148 | 149 | def test_filter(): 150 | """TEST 7.13: Filter removes any element not satisfying a predicate from a 151 | list.""" 152 | 153 | interpret(""" 154 | (define even 155 | (lambda (x) 156 | (eq (mod x 2) 0))) 157 | """, env) 158 | assert_equals("(2 4 6)", interpret("(filter even '(1 2 3 4 5 6))", env)) 159 | 160 | 161 | def test_map(): 162 | """TEST 7.14: Map applies a given function to all elements of a list.""" 163 | 164 | interpret(""" 165 | (define inc 166 | (lambda (x) (+ 1 x))) 167 | """, env) 168 | assert_equals("(2 3 4)", interpret("(map inc '(1 2 3))", env)) 169 | 170 | 171 | def test_reduce(): 172 | """TEST 7.15: Reduce, also known as fold, reduce a list into a single value. 173 | 174 | It does this by combining elements two by two, until there is only 175 | one left. 176 | 177 | If this is unfamiliar to you, have a look at: 178 | http://en.wikipedia.org/wiki/Fold_(higher-order_function) 179 | """ 180 | 181 | interpret(""" 182 | (define max 183 | (lambda (a b) 184 | (if (> a b) a b))) 185 | """, env) 186 | 187 | # Evaluates as (max 1 (max 6 (max 3 (max 2 0)))) -> 6 188 | assert_equals("6", interpret("(reduce max 0 '(1 6 3 2))", env)) 189 | 190 | interpret(""" 191 | (define add 192 | (lambda (a b) (+ a b))) 193 | """, env) 194 | 195 | # Lets see if we can improve a bit on `sum` while we're at it 196 | assert_equals("10", interpret("(reduce add 0 (range 1 4))", env)) 197 | 198 | 199 | # Finally, no stdlib is complete without a sorting algorithm. 200 | # Quicksort or mergesort might be good options, but you choose which 201 | # ever one you prefer. 202 | 203 | # You might want to implement a few helper functions for this one. 204 | 205 | def test_sort(): 206 | """TEST 7.16: Implementing (sort ...)""" 207 | 208 | assert_equals("()", interpret("(sort '())", env)) 209 | assert_equals("(1)", interpret("(sort '(1))", env)) 210 | assert_equals("(1 2 3 4 5 6 7)", 211 | interpret("(sort '(6 3 7 2 4 1 5))", env)) 212 | assert_equals("(1 2 3 4 5 6 7)", 213 | interpret("(sort '(1 2 3 4 5 6 7))", env)) 214 | assert_equals("(1 2 3 4 5 6 7)", 215 | interpret("(sort '(7 6 5 4 3 2 1))", env)) 216 | assert_equals("(1 1 1)", 217 | interpret("(sort '(1 1 1))", env)) 218 | -------------------------------------------------------------------------------- /tests/test_8_final_touches.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from os.path import dirname, relpath, join 4 | 5 | from nose.tools import assert_equals, assert_is_instance, \ 6 | assert_raises_regexp, with_setup 7 | 8 | from diylang.interpreter import interpret, interpret_file 9 | from diylang.types import Environment, String, DiyLangError, Closure 10 | from diylang.parser import parse 11 | 12 | env = None 13 | 14 | 15 | def prepare_env(): 16 | global env 17 | env = Environment() 18 | path = join(dirname(relpath(__file__)), '..', 'stdlib.diy') 19 | interpret_file(path, env) 20 | 21 | """ 22 | In this last part, we provide tests for some suggestions on how to improve 23 | the language a bit. Treat these tasks as optional, and suggestions only. 24 | Feel free to do something completely different, if you fancy. 25 | """ 26 | 27 | """ 28 | Suggestion 1: `cond` 29 | 30 | First off, we will implement a new control structure, the `cond` form (not to 31 | be confused with `cons`). The name `cond` is short for "conditional", and is 32 | sort of a buffed up version of `if`. 33 | 34 | Implement this as a new case in the `evaluate` function in `evaluator.py`. 35 | """ 36 | 37 | 38 | @with_setup(prepare_env) 39 | def test_cond_returns_right_branch(): 40 | """ 41 | `cond` takes as arguments a list of tuples (two-element lists, or 42 | "conses"). 43 | 44 | The first element of each tuple is evaluated in order, until one evaluates 45 | to `#t`. The second element of that tuple is returned. 46 | """ 47 | 48 | program = """ 49 | (cond ((#f 'foo) 50 | (#t 'bar) 51 | (#f 'baz))) 52 | """ 53 | assert_equals("bar", interpret(program, env)) 54 | 55 | 56 | @with_setup(prepare_env) 57 | def test_cond_doesnt_evaluate_all_branches(): 58 | """ 59 | Of all the second tuple elements, only the one we return is ever evaluated. 60 | """ 61 | 62 | interpret("(define foo 42)", env) 63 | 64 | program = """ 65 | (cond ((#f fire-the-missiles) 66 | (#t foo) 67 | (#f something-else-we-wont-do))) 68 | """ 69 | assert_equals("42", interpret(program, env)) 70 | 71 | 72 | @with_setup(prepare_env) 73 | def test_cond_not_evaluating_more_predicates_than_necessary(): 74 | """ 75 | Once we find a predicate that evaluates to `#t`, no more predicates should 76 | be evaluated. 77 | """ 78 | 79 | program = """ 80 | (cond ((#f 1) 81 | (#t 2) 82 | (dont-evaluate-me! 3))) 83 | """ 84 | assert_equals("2", interpret(program, env)) 85 | 86 | 87 | @with_setup(prepare_env) 88 | def test_cond_evaluates_predicates(): 89 | """ 90 | Remember to evaluate the predicates before checking whether they are true. 91 | """ 92 | 93 | program = """ 94 | (cond (((not #t) 'totally-not-true) 95 | ((> 4 3) 'tru-dat))) 96 | """ 97 | 98 | assert_equals("tru-dat", interpret(program, env)) 99 | 100 | 101 | @with_setup(prepare_env) 102 | def test_cond_returns_false_as_default(): 103 | """ 104 | If we evaluate all the predicates, only to find that none of them turned 105 | out to be true, then `cond` should return `#f`. 106 | """ 107 | 108 | program = """ 109 | (cond ((#f 'no) 110 | (#f 'nope) 111 | (#f 'i-dont-even))) 112 | """ 113 | 114 | assert_equals("#f", interpret(program, env)) 115 | 116 | 117 | """ 118 | Suggestion 2: Strings 119 | 120 | So far, our new language has been missing a central data type, one that no 121 | real language could do without -- strings. So, lets add them to the language. 122 | """ 123 | 124 | 125 | @with_setup(prepare_env) 126 | def test_parsing_simple_strings(): 127 | """ 128 | First things first, we need to be able to parse the strings. 129 | 130 | Since we already use python strings for our symbols, we need something 131 | else. Lets use a simple data type, `String`, which you will (rather 132 | conveniently) find ready made in the file `types.py`. 133 | 134 | > Side note: 135 | > 136 | > This is where it starts to show that we could have used smarter 137 | > representation of our types. We wanted to keep things simple early on, 138 | > and now we pay the price. We could have represented our types as tuples 139 | > of type and value, or perhaps made classes for all of them. 140 | > 141 | > Feel free to go back and fix this. Refactor as much as you wish -- just 142 | > remember to update the tests accordingly. 143 | """ 144 | 145 | ast = parse('"foo bar"') 146 | assert_is_instance(ast, String) 147 | assert_equals("foo bar", ast.val) 148 | 149 | 150 | @with_setup(prepare_env) 151 | def test_parsing_empty_string(): 152 | """ 153 | Empty strings are strings too! 154 | """ 155 | 156 | assert_equals('', parse('""').val) 157 | 158 | 159 | @with_setup(prepare_env) 160 | def test_parsing_strings_with_escaped_double_quotes(): 161 | """ 162 | We should be able to create strings with "-characters by escaping them. 163 | """ 164 | 165 | ast = parse('"Say \\"what\\" one more time!"') 166 | 167 | assert_is_instance(ast, String) 168 | assert_equals('Say \\"what\\" one more time!', ast.val) 169 | 170 | 171 | @with_setup(prepare_env) 172 | def test_parsing_unclosed_strings(): 173 | """ 174 | Strings that are not closed result in an parse error. 175 | """ 176 | 177 | with assert_raises_regexp(DiyLangError, 'Unclosed string'): 178 | parse('"hey, close me!') 179 | 180 | 181 | @with_setup(prepare_env) 182 | def test_parsing_strings_are_closed_by_first_closing_quotes(): 183 | """ 184 | Strings are delimited by the first and last (unescaped) double quotes. 185 | 186 | Thus, unescaped quotes followed by anything at all should be considered 187 | invalid and throw an exception. 188 | """ 189 | 190 | with assert_raises_regexp(DiyLangError, 'Expected EOF'): 191 | parse('"foo" bar"') 192 | 193 | 194 | @with_setup(prepare_env) 195 | def test_parsing_strings_with_parens_in_them(): 196 | """ 197 | Strings should be allowed to contain parens. 198 | 199 | The parser, so far, rather naively counts parens to determine the end of 200 | a list. We need to make a small adjustment to make it knows not to consider 201 | parens within strings. 202 | 203 | Tip: You'll probably need to change the function `find_matching_paren` in 204 | `parser.py` to solve this. 205 | """ 206 | 207 | actual = parse("(define foo \"string with a ) inside it\")") 208 | expected = ["define", "foo", String("string with a ) inside it")] 209 | 210 | assert_equals(expected, actual) 211 | 212 | 213 | @with_setup(prepare_env) 214 | def test_parsing_of_strings(): 215 | """ 216 | A final sanity check, to make sure parsing strings works. 217 | 218 | This test should already pass if you've done the above correctly. 219 | """ 220 | 221 | program = "(head '(#t \"()((()()) wtf \\\" ())) is )))) ()() going on \"))" 222 | 223 | assert_equals("#t", interpret(program)) 224 | 225 | 226 | @with_setup(prepare_env) 227 | def test_evaluating_strings(): 228 | """ 229 | Strings are one of the basic data types, and thus an atom. Strings should 230 | therefore evaluate to themselves. 231 | """ 232 | 233 | random_quote = '"The limits of my language means the limits of my world."' 234 | 235 | assert_equals(random_quote, interpret(random_quote, env)) 236 | 237 | 238 | @with_setup(prepare_env) 239 | def test_empty_strings_behave_as_empty_lists(): 240 | """ 241 | It is common in many languages for strings to behave as lists. This can be 242 | rather convenient, so let's make it that way here as well. 243 | 244 | We have four basic list functions: `cons`, `head`, `tail` and `empty`. 245 | 246 | To take the easy one first: `empty` should only return `#t` for the empty 247 | string (and empty lists, as before). 248 | """ 249 | 250 | assert_equals("#t", interpret('(empty "")')) 251 | assert_equals("#f", interpret('(empty "not empty")')) 252 | 253 | 254 | @with_setup(prepare_env) 255 | def test_strings_have_heads_and_tails(): 256 | """ 257 | Next, `head` and `tail` needs to extract the first character and the rest 258 | of the characters, respectively, from the string. 259 | """ 260 | 261 | assert_equals('"f"', interpret('(head "foobar")')) 262 | assert_equals('"oobar"', interpret('(tail "foobar")')) 263 | 264 | 265 | @with_setup(prepare_env) 266 | def test_consing_strings_back_together(): 267 | """ 268 | Finally, we need to be able to reconstruct a string from its head and tail 269 | """ 270 | 271 | assert_equals('"foobar"', interpret('(cons "f" "oobar")')) 272 | 273 | 274 | """ 275 | Suggestion 3: `let` 276 | 277 | The `let` form enables us to make local bindings. 278 | 279 | It takes two arguments. First a list of bindings, secondly an expression to be 280 | evaluated within an environment where those bindings exist. 281 | """ 282 | 283 | 284 | @with_setup(prepare_env) 285 | def test_let_returns_result_of_the_given_expression(): 286 | """ 287 | The result when evaluating a `let` binding is the evaluation of the 288 | expression given as argument. 289 | 290 | Let's first try one without any bindings. 291 | """ 292 | 293 | program = "(let () (if #t 'yep 'nope))" 294 | 295 | assert_equals("yep", interpret(program, env)) 296 | 297 | 298 | @with_setup(prepare_env) 299 | def test_let_extends_environment(): 300 | """ 301 | The evaluation of the inner expression should have available the bindings 302 | provided within the first argument. 303 | """ 304 | 305 | program = """ 306 | (let ((foo (+ 1000 42))) 307 | foo) 308 | """ 309 | 310 | assert_equals("1042", interpret(program, env)) 311 | 312 | 313 | @with_setup(prepare_env) 314 | def test_let_bindings_have_access_to_previous_bindings(): 315 | """ 316 | Each new binding should have access to the previous bindings in the list 317 | """ 318 | 319 | program = """ 320 | (let ((foo 10) 321 | (bar (+ foo 5))) 322 | bar) 323 | """ 324 | 325 | assert_equals("15", interpret(program, env)) 326 | 327 | 328 | @with_setup(prepare_env) 329 | def test_let_bindings_overshadow_outer_environment(): 330 | """ 331 | Let bindings should shadow definitions in from outer environments 332 | """ 333 | 334 | interpret("(define foo 1)", env) 335 | 336 | program = """ 337 | (let ((foo 2)) 338 | foo) 339 | """ 340 | 341 | assert_equals("2", interpret(program, env)) 342 | 343 | 344 | @with_setup(prepare_env) 345 | def test_let_bindings_do_not_affect_outer_environment(): 346 | """ 347 | After the let is evaluated, all of it's bindings are forgotten 348 | """ 349 | 350 | interpret("(define foo 1)", env) 351 | 352 | assert_equals("2", interpret("(let ((foo 2)) foo)", env)) 353 | assert_equals("1", interpret("foo", env)) 354 | 355 | 356 | """ 357 | Suggestion 4: `defn` 358 | 359 | So far, to define functions we have had to write 360 | 361 | (define my-function 362 | (lambda (foo bar) 363 | 'function-body-here)) 364 | 365 | It is a bit ugly to have to make a lambda every time you want a named function. 366 | 367 | Let's add some syntactic sugar, shall we: 368 | 369 | (defn my-function (foo bar) 370 | 'function-body-here) 371 | 372 | """ 373 | 374 | 375 | @with_setup(prepare_env) 376 | def test_defn_binds_the_variable_just_like_define(): 377 | """ 378 | Like `define`, the `defn` form should bind a variable to the environment. 379 | 380 | This variable should be a closure, just like if we had defined a new 381 | variable using the old `define` + `lambda` syntax. 382 | """ 383 | 384 | interpret("(defn foo (x) (> x 10))", env) 385 | 386 | assert_is_instance(env.lookup("foo"), Closure) 387 | 388 | 389 | @with_setup(prepare_env) 390 | def test_defn_result_in_the_correct_closure(): 391 | """ 392 | The closure created should be no different than from the old syntax. 393 | """ 394 | 395 | interpret("(defn foo-1 (x) (> x 10))", env) 396 | interpret("(define foo-2 (lambda (x) (> x 10)))", env) 397 | 398 | foo1 = env.lookup("foo-1") 399 | foo2 = env.lookup("foo-2") 400 | 401 | assert_equals(foo1.body, foo2.body) 402 | assert_equals(foo1.params, foo2.params) 403 | assert_equals(foo1.env, foo2.env) 404 | -------------------------------------------------------------------------------- /tests/test_provided_code.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nose.tools import assert_equals, assert_raises_regexp, assert_raises 4 | 5 | from diylang.parser import unparse, find_matching_paren 6 | from diylang.types import DiyLangError 7 | 8 | """ 9 | This module contains a few tests for the code provided for part 1. 10 | All tests here should already pass, and should be of no concern to 11 | you as a workshop attendee. 12 | """ 13 | 14 | # Tests for find_matching_paren function in parser.py 15 | 16 | 17 | def test_find_matching_paren(): 18 | source = "(foo (bar) '(this ((is)) quoted))" 19 | assert_equals(32, find_matching_paren(source, 0)) 20 | assert_equals(9, find_matching_paren(source, 5)) 21 | 22 | 23 | def test_find_matching_empty_parens(): 24 | assert_equals(1, find_matching_paren("()", 0)) 25 | 26 | 27 | def test_find_matching_paren_throws_exception_on_bad_initial_position(): 28 | """If asked to find closing paren from an index where there is no opening 29 | paren, the function should raise an error""" 30 | 31 | with assert_raises(AssertionError): 32 | find_matching_paren("string without parens", 4) 33 | 34 | 35 | def test_find_matching_paren_throws_exception_on_no_closing_paren(): 36 | """The function should raise error when there is no matching paren to be 37 | found""" 38 | 39 | with assert_raises_regexp(DiyLangError, "Incomplete expression"): 40 | find_matching_paren("string (without closing paren", 7) 41 | 42 | # Tests for unparse in parser.py 43 | 44 | 45 | def test_unparse_atoms(): 46 | assert_equals("123", unparse(123)) 47 | assert_equals("#t", unparse(True)) 48 | assert_equals("#f", unparse(False)) 49 | assert_equals("foo", unparse("foo")) 50 | 51 | 52 | def test_unparse_list(): 53 | assert_equals("((foo bar) baz)", unparse([["foo", "bar"], "baz"])) 54 | 55 | 56 | def test_unparse_quotes(): 57 | assert_equals("''(foo 'bar '(1 2))", unparse( 58 | ["quote", ["quote", ["foo", ["quote", "bar"], ["quote", [1, 2]]]]])) 59 | 60 | 61 | def test_unparse_bool(): 62 | assert_equals("#t", unparse(True)) 63 | assert_equals("#f", unparse(False)) 64 | 65 | 66 | def test_unparse_int(): 67 | assert_equals("1", unparse(1)) 68 | assert_equals("1337", unparse(1337)) 69 | assert_equals("-42", unparse(-42)) 70 | 71 | 72 | def test_unparse_symbol(): 73 | assert_equals("+", unparse("+")) 74 | assert_equals("foo", unparse("foo")) 75 | assert_equals("lambda", unparse("lambda")) 76 | 77 | 78 | def test_unparse_another_list(): 79 | assert_equals("(1 2 3)", unparse([1, 2, 3])) 80 | assert_equals("(if #t 42 #f)", 81 | unparse(["if", True, 42, False])) 82 | 83 | 84 | def test_unparse_other_quotes(): 85 | assert_equals("'foo", unparse(["quote", "foo"])) 86 | assert_equals("'(1 2 3)", 87 | unparse(["quote", [1, 2, 3]])) 88 | 89 | 90 | def test_unparse_empty_list(): 91 | assert_equals("()", unparse([])) 92 | -------------------------------------------------------------------------------- /tests/test_sanity_checks.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from nose.tools import assert_equals 4 | 5 | from diylang.interpreter import interpret 6 | from diylang.types import Environment 7 | 8 | 9 | def test_gcd(): 10 | """Tests Greatest Common Divisor (GCD). 11 | 12 | This test is intended to run after you have completed the core of the 13 | language, just to make sure that everything is holding together. 14 | """ 15 | 16 | program = """ 17 | (define gcd 18 | (lambda (a b) 19 | (if (eq b 0) 20 | a 21 | (gcd b (mod a b))))) 22 | """ 23 | 24 | env = Environment() 25 | interpret(program, env) 26 | 27 | assert_equals("6", interpret("(gcd 108 30)", env)) 28 | assert_equals("1", interpret("(gcd 17 5)", env)) 29 | --------------------------------------------------------------------------------