├── .gitignore ├── .travis.yml ├── README.md ├── compiler ├── compiler.py ├── runtime.c └── tests.py ├── examples └── hello-world.scm ├── interpreter ├── built_ins │ ├── __init__.py │ ├── base.py │ ├── chars.py │ ├── control.py │ ├── equivalence.py │ ├── io.py │ ├── lists.py │ ├── numbers.py │ ├── strings.py │ └── vectors.py ├── data_types.py ├── errors.py ├── evaluator.py ├── lexer.py ├── main.py ├── primitives.py ├── scheme_parser.py ├── tests.py └── utils.py ├── notes.py ├── repl ├── requirements.pip └── standard_library └── library.scm /.gitignore: -------------------------------------------------------------------------------- 1 | # Python files 2 | *.pyc 3 | parser.out 4 | parsetab.py 5 | 6 | # Compiler output 7 | compiler/main 8 | compiler/scheme.s 9 | 10 | # Editor files 11 | *~ 12 | TAGS 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 3.2 4 | - 3.3 5 | install: pip install -r requirements.pip --use-mirrors 6 | script: nosetests interpreter/tests.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Minimal scheme: a toy scheme interpreter written in Python 2 | 3 | [![Build Status](https://secure.travis-ci.org/Wilfred/Minimal-scheme.png?branch=master)](http://travis-ci.org/Wilfred/Minimal-scheme) 4 | 5 | # Interpreter 6 | 7 | This is a from-scratch Scheme implementation written for pleasure and 8 | education purposes only. It is targetting R5RS 9 | ([HTML copy of spec](http://people.csail.mit.edu/jaffer/r5rs_toc.html)), 10 | or at least an interesting subset of it. I'd also like it to run the 11 | [code in SICP](http://mitpress.mit.edu/sicp/code/index.html). 12 | 13 | All functionality is implemented with corresponding tests. Functions 14 | are generally thorough with their error messages, and we strive to 15 | give informative error messages. 16 | 17 | ### Installation 18 | 19 | $ virtualenv ~/.py_envs/scheme -p python3 20 | $ . ~/.py_envs/scheme/bin/activate 21 | (scheme)$ pip install -r requirements.pip 22 | 23 | ### Interactive usage 24 | 25 | (scheme)$ ./repl 26 | Welcome to Minimal Scheme 0.1 alpha. 27 | scheme> (+ 1 1) 28 | 2 29 | 30 | ### Script usage 31 | 32 | (scheme)$ python interpreter/main.py examples/hello-world.scm 33 | hello world 34 | 35 | ### Running the tests 36 | 37 | (scheme)$ nosetests interpreter/tests.py 38 | 39 | ## Terminology 40 | 41 | The terms `primitive`, `built-in` and `standard function` are used to 42 | refer to different things in minimal scheme. 43 | 44 | A `primitive` is written in Python and controls whether or not it 45 | evaluates its arguments. 46 | 47 | A `built-in` is written in Python but has all its arguments evaluated 48 | automatically. 49 | 50 | A `standard function` is written in Scheme. 51 | 52 | ## Functionality implemented 53 | 54 | ### Primitives 55 | 56 | `define`, `lambda`, `if`, `begin`, `quote`, `eqv?`, `eq?`, 57 | `quasiquote`, `unquote`, `unquote-splicing` 58 | 59 | ### Binding 60 | 61 | `let` 62 | 63 | ### Conditionals 64 | 65 | `cond`, `not`, `and` (binary only), `or` (binary only) 66 | 67 | ### Integers and floats 68 | 69 | `number?`, `complex?`, `rational?`, `real?`, `exact?`, `inexact?`, 70 | `+`, `-`, `*`, `/`, `<`, `<=`, `>`, `>=`, `=`, `zero?`, `positive?`, 71 | `negative?`, `odd?`, `even?`, `abs`, `quotient`, `modulo`, 72 | `remainder`, `exp`, `log` 73 | 74 | No support for exact fractions or complex numbers. 75 | 76 | ### Characters 77 | 78 | `char?`, `char=?`, `char?`, `char<=?`, `char>=?` 79 | 80 | ### Lists 81 | 82 | `car`, `cdr`, `caar`, `cadr`, `cdar`, `cddr`, `cons`, `null?`, 83 | `pair?`, `list`, `length`, `set-car!`, `set-cdr!` 84 | 85 | ### Control 86 | 87 | `map` (unary only), `for-each` (unary only), `procedure?`, `apply` 88 | 89 | ### Vectors 90 | 91 | `make-vector`, `vector?`, `vector-ref`, `vector-set!`, 92 | `vector-length`, `vector`, `vector->list`, `list->vector`, 93 | `vector-fill!` 94 | 95 | No distinction between constant vectors and normal vectors. 96 | 97 | ### Strings 98 | 99 | `string?`, `make-string`, `string-length`, `string-ref`, `string-set!` 100 | 101 | ### Macros 102 | 103 | `defmacro` 104 | 105 | ### I/O 106 | 107 | `display`, `newline` (both stdout only) 108 | 109 | ### Other 110 | 111 | Comments work too! 112 | 113 | ## Bugs and tasks 114 | 115 | ### Unimplemented features 116 | 117 | * Hygenic R5RS Macros 118 | * Tail call optimisation 119 | * Variadic lambdas 120 | * Nested define statements 121 | * File I/O 122 | * Closures 123 | 124 | ### Known bugs 125 | 126 | * `(eq? 1 1.0)` 127 | * Error checking in `exact` and `inexact` 128 | * `/` doesn't check type of arguments 129 | * `car` crashes on non-lists 130 | * No external representations for defmacro 131 | * String literals are mutable (so string-set! violates specification) 132 | * List literals are mutable (so set-car! would violate specification) 133 | * Using set-cdr! to make a circular list crashes 134 | * Remainder is not defined for floating point numbers 135 | * Interpreter is case sensitive 136 | * Complex returns true on real numbers 137 | * `cond` doesn't allow multiple statements after a conditional 138 | 139 | ### Cleanup tasks 140 | 141 | * Add slice support for our linked list, then clean up variadic 142 | function stuff 143 | * Remove `eval_program` -- it's just 144 | `map(eval_s_expression, s_expressions)` 145 | * Rename `internal_result` to `actual_result` in tests.py 146 | * Fix width of evaluator.py 147 | * Distinguish between incorrect type errors and incorrect arity 148 | errors, printing accordingly 149 | * Need a check_type() function 150 | * Move the more complex maths operations (`exp`, `log` etc) to library.scm 151 | * Add a base class for Nil and Cons 152 | * Add assertions to atoms to make sure they only hold the correct type 153 | * Add tests for type checking on built-ins and primitives 154 | * Test type coercion for arithmetic (e.g. `(+ 1 2.0)`) 155 | * Test external representations 156 | 157 | ### Future ideas 158 | 159 | * Compare with other Scheme interpreters written in Python for 160 | elegance of approach, error friendliness, performance, test coverage 161 | * Stack traces on error with line numbers 162 | * Add documentation via [docco](https://github.com/jashkenas/docco) 163 | * Remove PLY dependency 164 | * Explore R5RS compliance with http://sisc-scheme.org/r5rs_pitfall.php 165 | 166 | # Compiler 167 | 168 | There's also a skeleton compiler here, this is based on the paper 169 | [An Incremental Approach to Compiler Construction](http://scheme2006.cs.uchicago.edu/11-ghuloum.pdf). 170 | -------------------------------------------------------------------------------- /compiler/compiler.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def compile_literal(value): 5 | return "movl $%s, %%eax" % value 6 | 7 | 8 | def compile_scm(code): 9 | asm = "" 10 | if isinstance(code, int): 11 | asm += compile_literal(code) 12 | 13 | template = """ .text 14 | .globl entry_point 15 | entry_point: 16 | %s 17 | ret 18 | """ % (asm,) 19 | 20 | return template 21 | 22 | with open('scheme.s', 'w') as f: 23 | f.write(template) 24 | 25 | 26 | def create_binary(program): 27 | """Given text of a scheme program, write assembly and link it into an 28 | executable. 29 | 30 | """ 31 | # todo: lex and parse 32 | 33 | with open('scheme.s', 'w') as f: 34 | f.write(compile_scm(program)) 35 | 36 | os.system('gcc runtime.c scheme.s -o main') 37 | 38 | 39 | if __name__ == '__main__': 40 | program = 34 41 | create_binary(program) 42 | -------------------------------------------------------------------------------- /compiler/runtime.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main() { 4 | printf("%d\n", entry_point()); 5 | return 0; 6 | } 7 | -------------------------------------------------------------------------------- /compiler/tests.py: -------------------------------------------------------------------------------- 1 | from subprocess import check_output 2 | from unittest import main, TestCase 3 | 4 | from compiler import create_binary 5 | 6 | class LiteralIntegerTest(TestCase): 7 | def assertEvaluatesRepr(self, program, result_repr): 8 | """Assert that the given program, when compiled and executed, writes 9 | result_repr to stdout. 10 | 11 | """ 12 | create_binary(program) 13 | self.assertEqual(check_output(['./main']).strip(), result_repr) 14 | 15 | def test_42(self): 16 | self.assertEvaluatesRepr(42, "42") 17 | 18 | 19 | if __name__ == '__main__': 20 | main() 21 | -------------------------------------------------------------------------------- /examples/hello-world.scm: -------------------------------------------------------------------------------- 1 | (display "hello world") 2 | (newline) -------------------------------------------------------------------------------- /interpreter/built_ins/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import built_ins 2 | 3 | # Although we don't do anything after importing, we need to execute 4 | # these scripts to ensure their built-ins are loaded into the 5 | # built_ins dict. 6 | from . import lists 7 | from . import numbers 8 | from . import equivalence 9 | from . import chars 10 | from . import strings 11 | from . import vectors 12 | from . import io 13 | from . import control 14 | -------------------------------------------------------------------------------- /interpreter/built_ins/base.py: -------------------------------------------------------------------------------- 1 | built_ins = {} 2 | 3 | # a decorator for giving a name to built-in 4 | def define_built_in(function_name): 5 | def define_built_in_decorator(function): 6 | built_ins[function_name] = function 7 | 8 | # we return the function too, so we can use multiple decorators 9 | return function 10 | 11 | return define_built_in_decorator 12 | 13 | 14 | -------------------------------------------------------------------------------- /interpreter/built_ins/chars.py: -------------------------------------------------------------------------------- 1 | from .base import define_built_in 2 | from utils import check_argument_number 3 | 4 | from data_types import (Boolean, Character) 5 | from errors import SchemeTypeError 6 | 7 | 8 | @define_built_in('char?') 9 | def is_char(arguments): 10 | check_argument_number('char?', arguments, 1, 1) 11 | 12 | if isinstance(arguments[0], Character): 13 | return Boolean(True) 14 | 15 | return Boolean(False) 16 | 17 | 18 | @define_built_in('char=?') 19 | def char_equal(arguments): 20 | check_argument_number('char=?', arguments, 2, 2) 21 | 22 | if not isinstance(arguments[0], Character) or not isinstance(arguments[1], Character): 23 | raise SchemeTypeError("char=? takes only character arguments, got a " 24 | "%s and a %s." % (arguments[0].__class__, 25 | arguments[1].__class__)) 26 | 27 | if arguments[0].value == arguments[1].value: 28 | return Boolean(True) 29 | 30 | return Boolean(False) 31 | 32 | 33 | @define_built_in('char') 166 | def greater_than(arguments): 167 | check_argument_number('>', arguments, 2) 168 | 169 | for i in range(len(arguments) - 1): 170 | if not arguments[i].value > arguments[i+1].value: 171 | return Boolean(False) 172 | 173 | return Boolean(True) 174 | 175 | 176 | @define_built_in('>=') 177 | def greater_or_equal(arguments): 178 | check_argument_number('>=', arguments, 2) 179 | 180 | for i in range(len(arguments) - 1): 181 | if not arguments[i].value >= arguments[i+1].value: 182 | return Boolean(False) 183 | 184 | return Boolean(True) 185 | 186 | 187 | @define_built_in('quotient') 188 | def quotient(arguments): 189 | # integer division 190 | check_argument_number('quotient', arguments, 2, 2) 191 | 192 | if not isinstance(arguments[0], Integer) or not isinstance(arguments[1], Integer): 193 | raise SchemeTypeError("quotient is only defined for integers, " 194 | "got %s and %s." % (arguments[0].__class__, 195 | arguments[1].__class__)) 196 | 197 | # Python's integer division floors, whereas Scheme rounds towards zero 198 | x1 = arguments[0].value 199 | x2 = arguments[1].value 200 | result = math.trunc(x1 / x2) 201 | 202 | return Integer(result) 203 | 204 | 205 | @define_built_in('modulo') 206 | def modulo(arguments): 207 | check_argument_number('modulo', arguments, 2, 2) 208 | 209 | if not isinstance(arguments[0], Integer) or not isinstance(arguments[1], Integer): 210 | raise SchemeTypeError("modulo is only defined for integers, " 211 | "got %s and %s." % (arguments[0].__class__, 212 | arguments[1].__class__)) 213 | 214 | return Integer(arguments[0].value % arguments[1].value) 215 | 216 | 217 | @define_built_in('remainder') 218 | def remainder(arguments): 219 | check_argument_number('remainder', arguments, 2, 2) 220 | 221 | if not isinstance(arguments[0], Integer) or not isinstance(arguments[1], Integer): 222 | raise SchemeTypeError("remainder is only defined for integers, " 223 | "got %s and %s." % (arguments[0].__class__, 224 | arguments[1].__class__)) 225 | 226 | # as with quotient, we can't use Python's integer division here because it floors rather than truncates 227 | x1 = arguments[0].value 228 | x2 = arguments[1].value 229 | value = x1 - (math.trunc(x1 / x2) * x2) 230 | 231 | return Integer(value) 232 | 233 | 234 | @define_built_in('exp') 235 | def exp(arguments): 236 | check_argument_number('exp', arguments, 1, 1) 237 | 238 | if arguments[0].__class__ not in [Integer, FloatingPoint]: 239 | raise SchemeTypeError("exp only takes integers or floats, " 240 | "got %s" % arguments[0].__class__) 241 | 242 | x1 = arguments[0].value 243 | return FloatingPoint(math.exp(x1)) 244 | 245 | 246 | @define_built_in('log') 247 | def log(arguments): 248 | check_argument_number('log', arguments, 1, 1) 249 | 250 | if not isinstance(arguments[0], Number): 251 | raise SchemeTypeError("Log is only defined for numbers, " 252 | "you gave me %s." % arguments[0].__class__) 253 | 254 | x1 = arguments[0].value 255 | return FloatingPoint(math.log(x1)) 256 | -------------------------------------------------------------------------------- /interpreter/built_ins/strings.py: -------------------------------------------------------------------------------- 1 | from .base import define_built_in 2 | from utils import check_argument_number 3 | 4 | from data_types import (Boolean, Character, String, Integer) 5 | from errors import SchemeTypeError, InvalidArgument 6 | 7 | 8 | @define_built_in('string?') 9 | def is_string(arguments): 10 | check_argument_number('string?', arguments, 1, 1) 11 | 12 | if isinstance(arguments[0], String): 13 | return Boolean(True) 14 | 15 | return Boolean(False) 16 | 17 | 18 | @define_built_in('make-string') 19 | def make_string(arguments): 20 | check_argument_number('make-string', arguments, 1, 2) 21 | 22 | string_length_atom = arguments[0] 23 | 24 | if not isinstance(string_length_atom, Integer): 25 | raise SchemeTypeError("String length must be an integer, " 26 | "got %d." % string_length_atom.__class__) 27 | 28 | string_length = string_length_atom.value 29 | 30 | if string_length < 0: 31 | raise InvalidArgument("String length must be non-negative, " 32 | "got %d." % string_length) 33 | 34 | if len(arguments) == 1: 35 | return String(' ' * string_length) 36 | 37 | else: 38 | repeated_character_atom = arguments[1] 39 | 40 | if not isinstance(repeated_character_atom, Character): 41 | raise SchemeTypeError("The second argument to make-string must be" 42 | " a character, got a %s." % repeated_character_atom.__class__) 43 | 44 | repeated_character = repeated_character_atom.value 45 | return String(repeated_character * string_length) 46 | 47 | 48 | @define_built_in('string-length') 49 | def string_length(arguments): 50 | check_argument_number('string-length', arguments, 1, 1) 51 | 52 | string_atom = arguments[0] 53 | if not isinstance(string_atom, String): 54 | raise SchemeTypeError("string-length takes a string as its argument, " 55 | "not a %s." % string_atom.__class__) 56 | 57 | string_length = len(string_atom.value) 58 | return Integer(string_length) 59 | 60 | @define_built_in('string-ref') 61 | def string_ref(arguments): 62 | check_argument_number('string-ref', arguments, 2, 2) 63 | 64 | string_atom = arguments[0] 65 | if not isinstance(string_atom, String): 66 | raise SchemeTypeError("string-ref takes a string as its first argument, " 67 | "not a %s." % string_atom.__class__) 68 | 69 | char_index_atom = arguments[1] 70 | if not isinstance(char_index_atom, Integer): 71 | raise SchemeTypeError("string-ref takes an integer as its second argument, " 72 | "not a %s." % char_index_atom.__class__) 73 | 74 | string = string_atom.value 75 | char_index = char_index_atom.value 76 | 77 | if char_index >= len(string): 78 | # FIXME: this will say 0--1 if string is "" 79 | raise InvalidArgument("String index out of bounds: index must be in" 80 | " the range 0-%d, got %d." % (len(string) - 1, char_index)) 81 | 82 | return Character(string[char_index]) 83 | 84 | 85 | @define_built_in('string-set!') 86 | def string_set(arguments): 87 | check_argument_number('string-set!', arguments, 3, 3) 88 | 89 | string_atom = arguments[0] 90 | if not isinstance(string_atom, String): 91 | raise SchemeTypeError("string-set! takes a string as its first argument, " 92 | "not a %s." % string_atom.__class__) 93 | 94 | char_index_atom = arguments[1] 95 | if not isinstance(char_index_atom, Integer): 96 | raise SchemeTypeError("string-set! takes an integer as its second argument, " 97 | "not a %s." % char_index_atom.__class__) 98 | 99 | replacement_char_atom = arguments[2] 100 | if not isinstance(replacement_char_atom, Character): 101 | raise SchemeTypeError("string-set! takes a character as its third argument, " 102 | "not a %s." % replacement_char_atom.__class__) 103 | 104 | string = string_atom.value 105 | char_index = char_index_atom.value 106 | 107 | if char_index >= len(string): 108 | # FIXME: this will say 0--1 if string is "" 109 | raise InvalidArgument("String index out of bounds: index must be in" 110 | " the range 0-%d, got %d." % (len(string) - 1, char_index)) 111 | 112 | characters = list(string) 113 | characters[char_index] = replacement_char_atom.value 114 | new_string = "".join(characters) 115 | 116 | string_atom.value = new_string 117 | 118 | return None 119 | -------------------------------------------------------------------------------- /interpreter/built_ins/vectors.py: -------------------------------------------------------------------------------- 1 | from .base import define_built_in 2 | from utils import check_argument_number 3 | from data_types import Vector, Boolean, Nil, Integer 4 | 5 | 6 | @define_built_in('vector?') 7 | def is_vector(arguments): 8 | check_argument_number('make-vector', arguments, 1, 1) 9 | 10 | if isinstance(arguments[0], Vector): 11 | return Boolean(True) 12 | 13 | return Boolean(False) 14 | 15 | 16 | @define_built_in('make-vector') 17 | def make_vector(arguments): 18 | check_argument_number('make-vector', arguments, 1, 2) 19 | 20 | # todo: type check this is an integer 21 | vector_length = arguments[0].value 22 | 23 | vector = Vector(vector_length) 24 | 25 | # If we're given an initialisation value, use it. 26 | if len(arguments) == 2: 27 | init_value = arguments[1] 28 | for i in range(vector_length): 29 | vector[i] = init_value 30 | 31 | return vector 32 | 33 | 34 | @define_built_in('vector-ref') 35 | def vector_ref(arguments): 36 | check_argument_number('vector-ref', arguments, 2, 2) 37 | 38 | vector = arguments[0] 39 | index = arguments[1].value 40 | 41 | return vector[index] 42 | 43 | 44 | @define_built_in('vector-set!') 45 | def vector_set(arguments): 46 | check_argument_number('vector-ref', arguments, 3, 3) 47 | 48 | vector = arguments[0] 49 | index = arguments[1].value 50 | new_value = arguments[2] 51 | 52 | vector[index] = new_value 53 | 54 | return Nil() 55 | 56 | 57 | @define_built_in('vector-length') 58 | def vector_length(arguments): 59 | check_argument_number('vector-length', arguments, 1, 1) 60 | 61 | # todo: check type 62 | vector = arguments[0] 63 | 64 | return Integer(len(vector)) 65 | -------------------------------------------------------------------------------- /interpreter/data_types.py: -------------------------------------------------------------------------------- 1 | from collections import Sequence 2 | from errors import CircularList 3 | 4 | 5 | class Atom(object): 6 | """An abstract class for every base type in Scheme.""" 7 | def __init__(self, value): 8 | self.value = value 9 | 10 | def __repr__(self): 11 | return "<%s: %s>" % (self.__class__.__name__, str(self.value)) 12 | 13 | def __eq__(self, other): 14 | if isinstance(other, self.__class__) and other.value == self.value: 15 | return True 16 | 17 | return False 18 | 19 | 20 | class Symbol(Atom): 21 | def get_external_representation(self): 22 | return self.value 23 | 24 | 25 | class Number(Atom): 26 | def __eq__(self, other): 27 | # we allow different types to be equal only for numbers 28 | if isinstance(other, Number) and self.value == other.value: 29 | return True 30 | 31 | return False 32 | 33 | def get_external_representation(self): 34 | return str(self.value) 35 | 36 | 37 | class Integer(Number): 38 | pass 39 | 40 | class FloatingPoint(Number): 41 | pass 42 | 43 | class Boolean(Atom): 44 | def get_external_representation(self): 45 | if self.value: 46 | return "#t" 47 | else: 48 | return "#f" 49 | 50 | 51 | class Character(Atom): 52 | def get_external_representation(self): 53 | return "#\%s" % self.value 54 | 55 | 56 | class String(Atom): 57 | def __repr__(self): 58 | return "" % self.value 59 | 60 | def get_external_representation(self): 61 | return "%r" % self.value 62 | 63 | 64 | class Cons(Sequence): 65 | @staticmethod 66 | def from_list(python_list): 67 | if not python_list: 68 | return Nil() 69 | else: 70 | head = python_list[0] 71 | tail = Cons.from_list(python_list[1:]) 72 | return Cons(head, tail) 73 | 74 | def __init__(self, head, tail=None): 75 | self.head = head 76 | 77 | if tail: 78 | self.tail = tail 79 | else: 80 | self.tail = Nil() 81 | 82 | def __len__(self): 83 | """Find the length of this linked list. We return an error if the list 84 | is circular, and handle dotted lists gracefully. 85 | 86 | Since Python doesn't have TCO, we are forced to use an 87 | iterative approach. 88 | 89 | """ 90 | if self.is_circular(): 91 | raise CircularList("Can't take the length of a circular list.") 92 | 93 | length = 1 94 | tail = self.tail 95 | 96 | while True: 97 | if isinstance(tail, Nil): 98 | # Reached the end of the list. 99 | return length 100 | elif isinstance(tail, Cons): 101 | # Not yet at the end of the list. 102 | length += 1 103 | tail = tail.tail 104 | else: 105 | # At the end of an improper list. 106 | return length + 1 107 | 108 | def is_circular(self): 109 | tail = self.tail 110 | seen_elements = set() 111 | 112 | while True: 113 | if id(tail) in seen_elements: 114 | return True 115 | else: 116 | # We can't hash our list items, but we're only 117 | # interested in checking if it's an item we've seen 118 | # before. Using id() is sufficient. 119 | seen_elements.add(id(tail)) 120 | 121 | if isinstance(tail, Nil): 122 | # Reached the end of the list. 123 | return False 124 | elif isinstance(tail, Cons): 125 | # Not yet at the end of the list. 126 | tail = tail.tail 127 | else: 128 | # At the end of an improper list. 129 | return False 130 | 131 | def is_proper(self): 132 | """Does this list end with a Nil?""" 133 | if self.is_circular(): 134 | return False 135 | 136 | tail = self.tail 137 | 138 | while True: 139 | if isinstance(tail, Nil): 140 | # Reached the end of the list. 141 | return True 142 | elif isinstance(tail, Cons): 143 | # Not yet at the end of the list. 144 | tail = tail.tail 145 | else: 146 | # At the end of an improper list. 147 | return False 148 | 149 | def __getitem__(self, index): 150 | if index == 0: 151 | return self.head 152 | else: 153 | return self.tail.__getitem__(index - 1) 154 | 155 | def __setitem__(self, index, value): 156 | if index == 0: 157 | self.head = value 158 | else: 159 | return self.tail.__setitem__(index - 1, value) 160 | 161 | def __repr__(self): 162 | return "" % str(self.get_external_representation()) 163 | 164 | def __bool__(self): 165 | # a cons represents is a non-empty list, which we treat as true 166 | return True 167 | 168 | def __eq__(self, other): 169 | if not isinstance(other, Cons): 170 | return False 171 | 172 | element = self 173 | other_element = other 174 | 175 | while True: 176 | if element.head != other_element.head: 177 | return False 178 | else: 179 | # continue on the list 180 | element = element.tail 181 | other_element = other_element.tail 182 | 183 | if not isinstance(element, Cons) or not isinstance(other_element, Cons): 184 | return element == other_element 185 | 186 | def get_external_representation(self): 187 | if self.is_circular(): 188 | # todo: find a better way of printing these. 189 | return "#" 190 | 191 | contents = "" 192 | element = self 193 | 194 | while True: 195 | if isinstance(element, Nil): 196 | # Reached the end of the list. 197 | break 198 | elif isinstance(element, Cons): 199 | # Not yet at the end of the list. 200 | contents += " " + element.head.get_external_representation() 201 | element = element.tail 202 | else: 203 | # At the end of an improper list. 204 | contents += " . " + element.get_external_representation() 205 | break 206 | 207 | return "(%s)" % contents.strip() 208 | 209 | 210 | class Nil(Sequence): 211 | def is_circular(self): 212 | return False 213 | 214 | def is_proper(self): 215 | return True 216 | 217 | # the empty list for our linked list structure 218 | def __len__(self): 219 | return 0 220 | 221 | def __getitem__(self, index): 222 | raise IndexError 223 | 224 | def __setitem__(self, index, value): 225 | raise IndexError 226 | 227 | def __repr__(self): 228 | return "" 229 | 230 | def __bool__(self): 231 | return False 232 | 233 | def __eq__(self, other): 234 | # a Nil is always equal to a Nil 235 | if isinstance(other, Nil): 236 | return True 237 | return False 238 | 239 | def get_external_representation(self): 240 | return "()" 241 | 242 | class Vector(Sequence): 243 | def __init__(self, length): 244 | self.value = [] 245 | for index in range(length): 246 | self.value.append(Nil()) 247 | 248 | def __getitem__(self, index): 249 | return self.value[index] 250 | 251 | def __setitem__(self, index, new_value): 252 | self.value[index] = new_value 253 | 254 | def __len__(self): 255 | return len(self.value) 256 | 257 | def __eq__(self, other): 258 | if isinstance(other, Vector) and self.value == other.value: 259 | return True 260 | 261 | return False 262 | 263 | def get_external_representation(self): 264 | item_reprs = [item.get_external_representation() 265 | for item in self.value] 266 | return "#(%s)" % (" ".join(item_reprs)) 267 | 268 | @classmethod 269 | def from_list(cls, values): 270 | vector = Vector(len(values)) 271 | vector.value = values 272 | 273 | return vector 274 | 275 | 276 | """Function classes. These are currently only used in order to add an 277 | external representation. 278 | 279 | """ 280 | 281 | class Function(object): 282 | def __init__(self, func, name): 283 | self.function = func 284 | self.name = name 285 | 286 | def __call__(self, *args, **kwargs): 287 | return self.function(*args, **kwargs) 288 | 289 | 290 | class BuiltInFunction(Function): 291 | def get_external_representation(self): 292 | return "#" % self.name 293 | 294 | 295 | class PrimitiveFunction(Function): 296 | def get_external_representation(self): 297 | return "#" % self.name 298 | 299 | 300 | class UserFunction(Function): 301 | def get_external_representation(self): 302 | return "#" % self.name 303 | 304 | 305 | class LambdaFunction(Function): 306 | def __init__(self, func): 307 | self.function = func 308 | 309 | def get_external_representation(self): 310 | return "#" 311 | -------------------------------------------------------------------------------- /interpreter/errors.py: -------------------------------------------------------------------------------- 1 | class InterpreterException(Exception): 2 | def __init__(self, message): 3 | self.message = message 4 | 5 | def __str__(self): 6 | return "Interpreter error: %s" % self.message 7 | 8 | class CircularList(InterpreterException): 9 | pass 10 | 11 | class UndefinedVariable(InterpreterException): 12 | pass 13 | 14 | class RedefinedVariable(InterpreterException): 15 | pass 16 | 17 | class SchemeTypeError(InterpreterException): 18 | # 'TypeError' is a built-in Python exception 19 | pass 20 | 21 | class SchemeSyntaxError(InterpreterException): 22 | # SyntaxError is also a built-in Python exception 23 | pass 24 | 25 | class SchemeArityError(InterpreterException): 26 | pass 27 | 28 | class InvalidArgument(InterpreterException): 29 | pass 30 | 31 | class SchemeStackOverflow(InterpreterException): 32 | def __init__(self): 33 | super().__init__("Stack overflown") 34 | 35 | -------------------------------------------------------------------------------- /interpreter/evaluator.py: -------------------------------------------------------------------------------- 1 | from scheme_parser import parser 2 | from data_types import Atom, Symbol, BuiltInFunction 3 | from errors import (UndefinedVariable, SchemeTypeError, SchemeStackOverflow, 4 | SchemeSyntaxError) 5 | from built_ins import built_ins 6 | from copy import deepcopy 7 | 8 | def load_built_ins(environment): 9 | 10 | # a built-in differs from primitives: it always has all its arguments evaluated 11 | # it also doesn't need the global scope, so we don't pass it for code brevity 12 | def arguments_evaluated(function): 13 | def decorated_function(arguments, _environment): 14 | arguments = deepcopy(arguments) 15 | # evaluate the arguments, then pass them to the function 16 | for i in range(len(arguments)): 17 | (arguments[i], _environment) = eval_s_expression(arguments[i], _environment) 18 | 19 | return (function(arguments), _environment) 20 | 21 | return decorated_function 22 | 23 | for (function_name, function) in built_ins.items(): 24 | built_in_function = BuiltInFunction(arguments_evaluated(function), 25 | function_name) 26 | 27 | environment[function_name] = built_in_function 28 | 29 | return environment 30 | 31 | 32 | def load_standard_library(environment): 33 | with open('standard_library/library.scm') as library_file: 34 | library_code = library_file.read() 35 | _, environment = eval_program(library_code, environment) 36 | 37 | return environment 38 | 39 | 40 | def eval_program(program, initial_environment): 41 | if initial_environment: 42 | environment = initial_environment 43 | else: 44 | environment = {} 45 | 46 | # a program is a linked list of s-expressions 47 | s_expressions = parser.parse(program) 48 | 49 | if not s_expressions: 50 | return (None, environment) 51 | 52 | result = None 53 | 54 | for s_expression in s_expressions: 55 | result, environment = eval_s_expression(s_expression, environment) 56 | 57 | return (result, environment) 58 | 59 | 60 | def eval_s_expression(s_expression, environment): 61 | if isinstance(s_expression, Atom): 62 | return eval_atom(s_expression, environment) 63 | else: 64 | try: 65 | return eval_list(s_expression, environment) 66 | except RuntimeError as e: 67 | if e.args[0].startswith("maximum recursion depth exceeded"): 68 | raise SchemeStackOverflow() 69 | else: 70 | raise e 71 | 72 | 73 | def eval_list(linked_list, environment): 74 | if not linked_list: 75 | raise SchemeSyntaxError("() is not syntactically valid.") 76 | 77 | # find the function/primitive we are calling 78 | function, environment = eval_s_expression(linked_list[0], environment) 79 | 80 | if isinstance(function, Atom): 81 | raise SchemeTypeError("You can only call functions, but " 82 | "you gave me a %s." % function.__class__) 83 | 84 | # call it (internally we require the function to decide whether or 85 | # not to evaluate the arguments) 86 | return function(linked_list.tail, environment) 87 | 88 | def eval_atom(atom, environment): 89 | # with the exception of symbols, atoms evaluate to themselves 90 | if isinstance(atom, Symbol): 91 | return eval_symbol(atom.value, environment) 92 | else: 93 | return (atom, environment) 94 | 95 | def eval_symbol(symbol_string, environment): 96 | 97 | if symbol_string in primitives: 98 | # We don't allow primitives to be overridden. 99 | return (primitives[symbol_string], environment) 100 | 101 | elif symbol_string in environment: 102 | return (environment[symbol_string], environment) 103 | 104 | elif symbol_string in built_ins: 105 | return (built_ins[symbol_string], environment) 106 | 107 | else: 108 | raise UndefinedVariable('%s has not been defined (environment: %s).' % (symbol_string, sorted(environment.keys()))) 109 | 110 | # this import has to be after eval_s_expression to avoid circular import issues 111 | from primitives import primitives 112 | -------------------------------------------------------------------------------- /interpreter/lexer.py: -------------------------------------------------------------------------------- 1 | import ply.lex 2 | from errors import SchemeSyntaxError 3 | 4 | tokens = ('LPAREN', 'RPAREN', 'SYMBOL', 'INTEGER', 'BOOLEAN', 'FLOATING_POINT', 5 | 'CHARACTER', 'STRING', 'QUOTESUGAR', 'QUASIQUOTESUGAR', 6 | 'UNQUOTESUGAR', 'UNQUOTESPLICINGSUGAR') 7 | 8 | t_LPAREN = r'\(' 9 | t_RPAREN = r'\)' 10 | t_SYMBOL = r'[a-zA-Z!$%&*+./:<=>?"@^_~-][0-9a-zA-Z!$%&*+./:<=>?"@^_~-]*' 11 | t_QUOTESUGAR = r"'" 12 | t_QUASIQUOTESUGAR = r"`" 13 | t_UNQUOTESUGAR = r"," 14 | t_UNQUOTESPLICINGSUGAR = r",@" 15 | 16 | def t_STRING(t): 17 | r'"(\\"|\\n|[a-zA-Z*+/!?=<>. -])*"' 18 | # strip leading and trailing doublequote from input 19 | t.value = t.value[1:-1] 20 | 21 | # replace escaped characters with their Python representation 22 | t.value = t.value.replace('\\"', '"') 23 | t.value = t.value.replace('\\n', '\n') 24 | return t 25 | 26 | def t_FLOATING_POINT(t): 27 | r"-?([0-9]*\.[0-9]+)|([0-9]+\.[0-9]*)" 28 | t.value = float(t.value) 29 | return t 30 | 31 | def t_INTEGER(t): 32 | r'-?[0-9]+' 33 | t.value = int(t.value) 34 | return t 35 | 36 | def t_BOOLEAN(t): 37 | r'\#t|\#f' 38 | if t.value == "#t": 39 | t.value = True 40 | else: 41 | t.value = False 42 | 43 | return t 44 | 45 | def t_CHARACTER(t): 46 | r'\#\\(space|newline|.)' 47 | # throw away leading #\ 48 | t.value = t.value[2:] 49 | 50 | if t.value == 'space': 51 | t.value = ' ' 52 | elif t.value == 'newline': 53 | t.value = '\n' 54 | 55 | return t 56 | 57 | t_ignore_COMMENT = r";[^\n]*" 58 | 59 | # whitespace 60 | t_ignore = ' \t\n' 61 | 62 | def t_error(t): 63 | raise SchemeSyntaxError('Could not lex the remainder of input: "%s"' % t.value) 64 | 65 | lexer = ply.lex.lex() 66 | 67 | -------------------------------------------------------------------------------- /interpreter/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | import os 5 | import cmd 6 | 7 | from evaluator import eval_program, load_standard_library, load_built_ins 8 | from errors import InterpreterException, SchemeSyntaxError, SchemeTypeError 9 | 10 | class Repl(cmd.Cmd): 11 | intro = "Welcome to Minimal Scheme 0.2 alpha." 12 | prompt = "scheme> " 13 | 14 | def __init__(self, initial_environment): 15 | self.environment = initial_environment 16 | super().__init__() 17 | 18 | def onecmd(self, program): 19 | if program == 'EOF': 20 | print() # for tidyness' sake 21 | sys.exit(0) 22 | 23 | try: 24 | result, self.environment = eval_program(program, self.environment) 25 | 26 | if not result is None: 27 | if hasattr(result, "get_external_representation"): 28 | # is an atom or list 29 | print(result.get_external_representation()) 30 | else: 31 | # function object 32 | print(result) 33 | 34 | except SchemeSyntaxError as e: 35 | print("Syntax error: %s" % e.message) 36 | except SchemeTypeError as e: 37 | print("Type error: %s" % e.message) 38 | except InterpreterException as e: 39 | print("Error: %s" % e.message) 40 | 41 | 42 | if __name__ == '__main__': 43 | environment = {} 44 | environment = load_built_ins(environment) 45 | environment = load_standard_library(environment) 46 | 47 | if len(sys.argv) > 1: 48 | # program file passed in 49 | path = os.path.abspath(sys.argv[1]) 50 | program = open(path, 'r').read() 51 | 52 | try: 53 | eval_program(program, environment) 54 | except SchemeSyntaxError as e: 55 | print("Syntax error: %s" % e.message) 56 | except SchemeTypeError as e: 57 | print("Type error: %s" % e.message) 58 | except InterpreterException as e: 59 | print("Error: %s" % e.message) 60 | 61 | else: 62 | # interactive mode 63 | Repl(environment).cmdloop() 64 | -------------------------------------------------------------------------------- /interpreter/primitives.py: -------------------------------------------------------------------------------- 1 | from evaluator import eval_s_expression 2 | from errors import (SchemeTypeError, RedefinedVariable, SchemeSyntaxError, UndefinedVariable, 3 | SchemeArityError) 4 | from data_types import Nil, Cons, Atom, Symbol, Boolean, UserFunction, LambdaFunction 5 | from copy import deepcopy 6 | from utils import check_argument_number 7 | 8 | primitives = {} 9 | 10 | # a decorator for creating a primitive function object and giving it a name 11 | def define_primitive(function_name): 12 | def define_primitive_decorator(function): 13 | primitives[function_name] = function 14 | 15 | # we return the function too, so we can use multiple decorators 16 | return function 17 | 18 | return define_primitive_decorator 19 | 20 | 21 | @define_primitive('define') 22 | def define(arguments, environment): 23 | check_argument_number('define', arguments, 2) 24 | 25 | if isinstance(arguments[0], Atom): 26 | return define_variable(arguments, environment) 27 | else: 28 | return define_function(arguments, environment) 29 | 30 | 31 | def define_variable(arguments, environment): 32 | if not isinstance(arguments[0], Symbol): 33 | raise SchemeTypeError("Tried to assign to a %s, which isn't a symbol." % arguments[0].__class__) 34 | 35 | if arguments[0].value in environment: 36 | raise RedefinedVariable("Cannot define %s, as it has already been defined." % arguments[0].value) 37 | 38 | variable_name = arguments[0].value 39 | variable_value_expression = arguments[1] 40 | 41 | result, environment = eval_s_expression(variable_value_expression, environment) 42 | environment[variable_name] = result 43 | 44 | return (None, environment) 45 | 46 | 47 | def define_function(arguments, environment): 48 | function_name_with_parameters = arguments[0] 49 | function_name = function_name_with_parameters[0] 50 | 51 | if not isinstance(function_name, Symbol): 52 | raise SchemeTypeError("Function names must be symbols, not a %s." % function_name.__class__) 53 | 54 | # check that all our arguments are symbols: 55 | function_parameters = function_name_with_parameters.tail 56 | 57 | for parameter in function_parameters: 58 | if not isinstance(parameter, Symbol): 59 | raise SchemeTypeError("Function arguments must be symbols, not a %s." % parameter.__class__) 60 | 61 | # check if this function can take a variable number of arguments 62 | is_variadic = False 63 | 64 | for parameter in function_parameters: 65 | if parameter.value == '.': 66 | if is_variadic: 67 | raise SchemeSyntaxError("May not have . more than once in a parameter list.") 68 | else: 69 | is_variadic = True 70 | 71 | if is_variadic: 72 | return define_variadic_function(arguments, environment) 73 | else: 74 | return define_normal_function(arguments, environment) 75 | 76 | 77 | def define_normal_function(arguments, environment): 78 | function_name_with_parameters = arguments[0] 79 | function_name = function_name_with_parameters[0] 80 | function_parameters = function_name_with_parameters.tail 81 | 82 | function_body = arguments.tail 83 | 84 | # a function with a fixed number of arguments 85 | def named_function(_arguments, _environment): 86 | check_argument_number(function_name.value, _arguments, 87 | len(function_parameters), len(function_parameters)) 88 | 89 | local_environment = {} 90 | 91 | # evaluate arguments 92 | _arguments = deepcopy(_arguments) 93 | for i in range(len(_arguments)): 94 | (_arguments[i], _environment) = eval_s_expression(_arguments[i], _environment) 95 | 96 | # assign to parameters 97 | for (parameter_name, parameter_value) in zip(function_parameters, 98 | _arguments): 99 | local_environment[parameter_name.value] = parameter_value 100 | 101 | # create new environment, where local variables mask globals 102 | new_environment = dict(_environment, **local_environment) 103 | 104 | # evaluate the function block 105 | for s_exp in function_body: 106 | result, new_environment = eval_s_expression(s_exp, new_environment) 107 | 108 | # update any global variables that weren't masked 109 | for variable_name in _environment: 110 | if variable_name not in local_environment: 111 | _environment[variable_name] = new_environment[variable_name] 112 | 113 | return (result, _environment) 114 | 115 | # assign this function to this name 116 | environment[function_name.value] = UserFunction(named_function, 117 | function_name.value) 118 | 119 | return (None, environment) 120 | 121 | 122 | def define_variadic_function(arguments, environment): 123 | function_name_with_parameters = arguments[0] 124 | function_name = arguments[0][0] 125 | function_parameters = function_name_with_parameters.tail 126 | 127 | function_body = arguments.tail 128 | 129 | dot_position = function_parameters.index(Symbol('.')) 130 | 131 | if dot_position < len(function_parameters) - 2: 132 | raise SchemeSyntaxError("You can only have one improper list " 133 | "(you have %d parameters after the '.')." % (len(function_parameters) - 1 - dot_position)) 134 | if dot_position == len(function_parameters) - 1: 135 | raise SchemeSyntaxError("Must name an improper list parameter after '.'.") 136 | 137 | def named_variadic_function(_arguments, _environment): 138 | # a function that takes a variable number of arguments 139 | if dot_position == 0: 140 | explicit_parameters = Nil() 141 | else: 142 | explicit_parameters = deepcopy(function_parameters) 143 | 144 | # create a linked list holding all the parameters before the dot 145 | current_head = explicit_parameters 146 | 147 | # find the position in the list just before the dot 148 | for i in range(dot_position - 2): 149 | current_head = current_head.tail 150 | 151 | # then remove the rest of the list 152 | current_head.tail = Nil() 153 | 154 | improper_list_parameter = function_parameters[dot_position + 1] 155 | 156 | # check we have been given sufficient arguments for our explicit parameters 157 | check_argument_number(function_name.value, _arguments, 158 | len(explicit_parameters)) 159 | 160 | local_environment = {} 161 | 162 | # evaluate arguments 163 | _arguments = deepcopy(_arguments) 164 | for i in range(len(_arguments)): 165 | (_arguments[i], _environment) = eval_s_expression(_arguments[i], _environment) 166 | 167 | # assign parameters 168 | for (parameter, parameter_value) in zip(explicit_parameters, 169 | _arguments): 170 | local_environment[parameter] = parameter_value 171 | 172 | # put the remaining arguments in our improper parameter 173 | remaining_arguments = _arguments 174 | for i in range(len(explicit_parameters)): 175 | remaining_arguments = remaining_arguments.tail 176 | 177 | local_environment[improper_list_parameter.value] = remaining_arguments 178 | 179 | new_environment = dict(_environment, **local_environment) 180 | 181 | # evaluate our function_body in this environment 182 | for s_exp in function_body: 183 | result, new_environment = eval_s_expression(s_exp, new_environment) 184 | 185 | # update global variables that weren't masked by locals 186 | for variable_name in _environment: 187 | if variable_name not in local_environment: 188 | _environment[variable_name] = new_environment[variable_name] 189 | 190 | return (result, _environment) 191 | 192 | # assign this function to this name 193 | environment[function_name.value] = UserFunction(named_variadic_function, 194 | function_name.value) 195 | 196 | return (None, environment) 197 | 198 | 199 | @define_primitive('set!') 200 | def set_variable(arguments, environment): 201 | check_argument_number('set!', arguments, 2, 2) 202 | 203 | variable_name = arguments[0] 204 | 205 | if not isinstance(variable_name, Symbol): 206 | raise SchemeTypeError("Tried to assign to a %s, which isn't a symbol." % variable_name.__class__) 207 | 208 | if variable_name.value not in environment: 209 | raise UndefinedVariable("Can't assign to undefined variable %s." % variable_name.value) 210 | 211 | variable_value_expression = arguments[1] 212 | result, environment = eval_s_expression(variable_value_expression, environment) 213 | environment[variable_name.value] = result 214 | 215 | return (None, environment) 216 | 217 | @define_primitive('if') 218 | def if_function(arguments, environment): 219 | check_argument_number('if', arguments, 2, 3) 220 | 221 | condition, environment = eval_s_expression(arguments[0], environment) 222 | 223 | # everything except an explicit false boolean is true 224 | if not condition == Boolean(False): 225 | then_expression = arguments[1] 226 | return eval_s_expression(then_expression, environment) 227 | else: 228 | if len(arguments) == 3: 229 | else_expression = arguments[2] 230 | return eval_s_expression(else_expression, environment) 231 | 232 | 233 | @define_primitive('lambda') 234 | def make_lambda_function(arguments, environment): 235 | check_argument_number('lambda', arguments, 2) 236 | 237 | parameter_list = arguments[0] 238 | function_body = arguments.tail 239 | 240 | if isinstance(parameter_list, Atom): 241 | raise SchemeTypeError("The first argument to `lambda` must be a list of variables.") 242 | 243 | for parameter in parameter_list: 244 | if not isinstance(parameter, Symbol): 245 | raise SchemeTypeError("Parameters of lambda functions must be symbols, not %s." % parameter.__class__) 246 | 247 | def lambda_function(_arguments, _environment): 248 | check_argument_number('(anonymous function)', _arguments, 249 | len(parameter_list), len(parameter_list)) 250 | 251 | local_environment = {} 252 | 253 | for (parameter_name, parameter_expression) in zip(parameter_list, 254 | _arguments): 255 | local_environment[parameter_name.value], _environment = eval_s_expression(parameter_expression, _environment) 256 | 257 | new_environment = dict(_environment, **local_environment) 258 | 259 | # now we have set up the correct scope, evaluate our function block 260 | for s_exp in function_body: 261 | result, new_environment = eval_s_expression(s_exp, new_environment) 262 | 263 | # update any global variables that weren't masked 264 | for variable_name in _environment: 265 | if variable_name not in local_environment: 266 | _environment[variable_name] = new_environment[variable_name] 267 | 268 | return (result, _environment) 269 | 270 | return (LambdaFunction(lambda_function), environment) 271 | 272 | 273 | @define_primitive('quote') 274 | def return_argument_unevaluated(arguments, environment): 275 | check_argument_number('quote', arguments, 1, 1) 276 | 277 | return (arguments[0], environment) 278 | 279 | 280 | @define_primitive('begin') 281 | def evaluate_sequence(arguments, environment): 282 | result = None 283 | 284 | for argument in arguments: 285 | result, environment = eval_s_expression(argument, environment) 286 | 287 | return (result, environment) 288 | 289 | 290 | @define_primitive('quasiquote') 291 | def quasiquote(arguments, environment): 292 | """Returns the arguments unevaluated, except for any occurrences 293 | of unquote. 294 | 295 | """ 296 | def recursive_eval_unquote(s_expression, _environment): 297 | """Return a copy of s_expression, with all occurrences of 298 | unquoted s-expressions replaced by their evaluated values. 299 | 300 | Note that we can only have unquote-splicing in a sublist, 301 | since we can only return one value, e.g `,@(1 2 3). 302 | 303 | """ 304 | if isinstance(s_expression, Atom): 305 | return (s_expression, _environment) 306 | 307 | elif isinstance(s_expression, Nil): 308 | return (s_expression, _environment) 309 | 310 | elif s_expression[0] == Symbol("unquote"): 311 | check_argument_number('unquote', arguments, 1, 1) 312 | return eval_s_expression(s_expression[1], _environment) 313 | 314 | else: 315 | # return a list of s_expressions that have been 316 | # recursively checked for unquote 317 | list_elements = [] 318 | 319 | for element in s_expression: 320 | if isinstance(element, Cons) and \ 321 | element[0] == Symbol('unquote-splicing'): 322 | check_argument_number('unquote-splicing', element.tail, 1, 1) 323 | 324 | (result, _environment) = eval_s_expression(element[1], _environment) 325 | 326 | if not isinstance(result, Cons) and not isinstance(result, Nil): 327 | raise SchemeArityError("unquote-splicing requires a list.") 328 | 329 | for item in result: 330 | list_elements.append(item) 331 | 332 | else: 333 | (result, _environment) = recursive_eval_unquote(element, _environment) 334 | list_elements.append(result) 335 | 336 | return (Cons.from_list(list_elements), _environment) 337 | 338 | check_argument_number('quasiquote', arguments, 1, 1) 339 | 340 | return recursive_eval_unquote(arguments[0], environment) 341 | 342 | 343 | @define_primitive('defmacro') 344 | def defmacro(arguments, environment): 345 | """defmacro is a restricted version of Common Lisp's defmacro: 346 | http://www.ai.mit.edu/projects/iiip/doc/CommonLISP/HyperSpec/Body/mac_defmacro.html 347 | 348 | Syntax: 349 | defmacro 350 | 351 | """ 352 | check_argument_number('defmacro', arguments, 3, 3) 353 | 354 | macro_name = arguments[0].value 355 | raw_macro_arguments = [arg.value for arg in arguments[1]] 356 | 357 | if len(raw_macro_arguments) > 1 and raw_macro_arguments[-2] == ".": 358 | is_variadic = True 359 | macro_arguments = raw_macro_arguments[:-2] 360 | 361 | variadic_argument_name = raw_macro_arguments[-1] 362 | else: 363 | macro_arguments = raw_macro_arguments 364 | is_variadic = False 365 | 366 | replacement_body = arguments[2] 367 | 368 | def expand_then_eval(arguments, _environment): 369 | """Expand this macro once, then continue evaluation.""" 370 | if is_variadic: 371 | if len(arguments) < len(macro_arguments): 372 | raise SchemeArityError("Macro %s takes at least %d arguments, but got %d." 373 | % (macro_name, len(macro_arguments), 374 | len(arguments))) 375 | 376 | else: 377 | if len(arguments) != len(macro_arguments): 378 | raise SchemeArityError("Macro %s takes %d arguments, but got %d." 379 | % (macro_name, len(macro_arguments), 380 | len(arguments))) 381 | 382 | new_environment = dict(_environment) 383 | for (variable_name, variable_value) in zip(macro_arguments, arguments): 384 | new_environment[variable_name] = variable_value 385 | 386 | if is_variadic: 387 | remaining_arguments = [] 388 | for index, arg in enumerate(arguments): 389 | if index >= len(macro_arguments): 390 | remaining_arguments.append(arg) 391 | 392 | new_environment[variadic_argument_name] = Cons.from_list(remaining_arguments) 393 | 394 | (s_expression_after_expansion, new_environment) = eval_s_expression(replacement_body, new_environment) 395 | 396 | # restore old environment, ignoring variables hidden by scope 397 | for variable_name in _environment: 398 | if variable_name not in macro_arguments: 399 | _environment[variable_name] = new_environment[variable_name] 400 | 401 | # continue evaluation where we left off 402 | return eval_s_expression(s_expression_after_expansion, _environment) 403 | 404 | environment[macro_name] = expand_then_eval 405 | 406 | return (None, environment) 407 | -------------------------------------------------------------------------------- /interpreter/scheme_parser.py: -------------------------------------------------------------------------------- 1 | import ply.yacc 2 | 3 | from lexer import tokens 4 | from data_types import (Cons, Nil, Symbol, Integer, FloatingPoint, Boolean, 5 | Character, String) 6 | from errors import SchemeSyntaxError 7 | 8 | """Grammar for our minimal scheme: 9 | 10 | # a program is a series of s-expressions 11 | program : sexpression program 12 | | 13 | 14 | # an s-expression is a list or an atom 15 | sexpression : atom 16 | | list 17 | 18 | # unlike a normal list, using ' only permits one argument 19 | list : ( listarguments ) 20 | | QUOTESUGAR sexpression 21 | | QUASIQUOTESUGAR sexpression 22 | | UNQUOTESUGAR sexpression 23 | | UNQUOTESPLICINGSUGAR sexpression 24 | 25 | listarguments : sexpression listarguments 26 | | 27 | 28 | atom : SYMBOL | NUMBER | BOOLEAN | CHARACTER | STRING 29 | 30 | """ 31 | 32 | # now, parse an expression and build a parse tree: 33 | 34 | def p_program(p): 35 | "program : sexpression program" 36 | p[0] = Cons(p[1], p[2]) 37 | 38 | def p_program_empty(p): 39 | "program :" 40 | p[0] = Nil() 41 | 42 | def p_sexpression_atom(p): 43 | "sexpression : atom" 44 | p[0] = p[1] 45 | 46 | def p_sexpression_list(p): 47 | "sexpression : list" 48 | p[0] = p[1] 49 | 50 | def p_list(p): 51 | "list : LPAREN listarguments RPAREN" 52 | p[0] = p[2] 53 | 54 | def p_list_quotesugar(p): 55 | "list : QUOTESUGAR sexpression" 56 | # convert 'foo to (quote foo) 57 | p[0] = Cons(Symbol("quote"), Cons(p[2])) 58 | 59 | def p_list_quasiquotesugar(p): 60 | "list : QUASIQUOTESUGAR sexpression" 61 | # convert `foo to (quasiquote foo) 62 | p[0] = Cons(Symbol("quasiquote"), Cons(p[2])) 63 | 64 | def p_list_unquotesugar(p): 65 | "list : UNQUOTESUGAR sexpression" 66 | # convert ,foo to (unquote foo) 67 | p[0] = Cons(Symbol("unquote"), Cons(p[2])) 68 | 69 | def p_list_unquotesplicingsugar(p): 70 | "list : UNQUOTESPLICINGSUGAR sexpression" 71 | # convert ,foo to (unquote foo) 72 | p[0] = Cons(Symbol("unquote-splicing"), Cons(p[2])) 73 | 74 | def p_listarguments_one(p): 75 | "listarguments : sexpression listarguments" 76 | # a list is therefore a nested tuple: 77 | p[0] = Cons(p[1], p[2]) 78 | 79 | def p_listargument_empty(p): 80 | "listarguments :" 81 | p[0] = Nil() 82 | 83 | def p_atom_symbol(p): 84 | "atom : SYMBOL" 85 | p[0] = Symbol(p[1]) 86 | 87 | def p_atom_number(p): 88 | "atom : INTEGER" 89 | p[0] = Integer(p[1]) 90 | 91 | def p_atom_floating_point(p): 92 | "atom : FLOATING_POINT" 93 | p[0] = FloatingPoint(p[1]) 94 | 95 | def p_atom_boolean(p): 96 | "atom : BOOLEAN" 97 | p[0] = Boolean(p[1]) 98 | 99 | def p_atom_character(p): 100 | "atom : CHARACTER" 101 | p[0] = Character(p[1]) 102 | 103 | def p_atom_string(p): 104 | "atom : STRING" 105 | p[0] = String(p[1]) 106 | 107 | def p_error(p): 108 | raise SchemeSyntaxError("Parse error.") 109 | 110 | 111 | parser = ply.yacc.yacc() 112 | -------------------------------------------------------------------------------- /interpreter/tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import unittest 4 | import sys 5 | from io import StringIO 6 | 7 | from evaluator import eval_program, load_standard_library, load_built_ins 8 | from errors import (SchemeTypeError, SchemeStackOverflow, SchemeSyntaxError, 9 | SchemeArityError) 10 | from data_types import (Vector, Cons, Nil, Integer, Boolean, String, 11 | Character, FloatingPoint) 12 | 13 | 14 | class InterpreterTest(unittest.TestCase): 15 | def setUp(self): 16 | self.environment = {} 17 | self.environment = load_built_ins(self.environment) 18 | self.environment = load_standard_library(self.environment) 19 | 20 | def evaluate(self, program): 21 | internal_result, final_environment = eval_program(program, self.environment) 22 | return internal_result 23 | 24 | def assertEvaluatesTo(self, program, expected_result): 25 | result = self.evaluate(program) 26 | self.assertEqual(result, expected_result) 27 | 28 | def assertEvaluatesAs(self, program, expected_result): 29 | internal_result = self.evaluate(program) 30 | 31 | if internal_result is None: 32 | result = Nil() 33 | else: 34 | result = internal_result 35 | 36 | self.assertEqual(result, expected_result) 37 | 38 | 39 | class LexerTest(InterpreterTest): 40 | def test_integer(self): 41 | program = "3" 42 | self.assertEvaluatesTo(program, Integer(3)) 43 | 44 | def test_floating_point(self): 45 | program = "2.0" 46 | self.assertEvaluatesTo(program, FloatingPoint(2.0)) 47 | 48 | def test_boolean(self): 49 | program = "#t" 50 | self.assertEvaluatesTo(program, Boolean(True)) 51 | 52 | def test_character(self): 53 | program = "#\\a" 54 | self.assertEvaluatesTo(program, Character('a')) 55 | 56 | program = "#\\newline" 57 | self.assertEvaluatesTo(program, Character('\n')) 58 | 59 | program = "#\\space" 60 | self.assertEvaluatesTo(program, Character(' ')) 61 | 62 | def test_string(self): 63 | program = '""' 64 | self.assertEvaluatesTo(program, String("")) 65 | 66 | program = '" "' 67 | self.assertEvaluatesTo(program, String(" ")) 68 | 69 | program = '" \\" "' 70 | self.assertEvaluatesTo(program, String(' " ')) 71 | 72 | program = '" \\n "' 73 | self.assertEvaluatesTo(program, String(' \n ')) 74 | 75 | def test_invalid(self): 76 | program = '\\y' 77 | self.assertRaises(SchemeSyntaxError, eval_program, program, None) 78 | 79 | 80 | class ParserTest(InterpreterTest): 81 | def test_mismatched_parens(self): 82 | program = "(" 83 | self.assertRaises(SchemeSyntaxError, eval_program, program, None) 84 | 85 | 86 | class EvaluatorTest(InterpreterTest): 87 | def test_empty_program(self): 88 | program = "" 89 | self.assertEvaluatesTo(program, None) 90 | 91 | def test_variable_evaluation(self): 92 | program = "(define x 28) x" 93 | self.assertEvaluatesTo(program, Integer(28)) 94 | 95 | def test_procedure_call(self): 96 | program = "((if #f + *) 3 4)" 97 | self.assertEvaluatesTo(program, Integer(12)) 98 | 99 | def test_if_two_arguments(self): 100 | program = "(if #t 1)" 101 | self.assertEvaluatesTo(program, Integer(1)) 102 | 103 | def test_if_three_arguments(self): 104 | program = "(if #t 2 3)" 105 | self.assertEvaluatesTo(program, Integer(2)) 106 | 107 | def test_variable_assignment(self): 108 | program = "(define x 1) (set! x 2) x" 109 | self.assertEvaluatesTo(program, Integer(2)) 110 | 111 | def test_function_definition(self): 112 | program = "(define (x) 1) (x)" 113 | self.assertEvaluatesTo(program, Integer(1)) 114 | 115 | def test_function_long_body(self): 116 | program = "(define (x) 1 2) (x)" 117 | self.assertEvaluatesTo(program, Integer(2)) 118 | 119 | def test_variadic_function_long_body(self): 120 | program = "(define (foo . args) 1 2) (foo)" 121 | self.assertEvaluatesTo(program, Integer(2)) 122 | 123 | def test_variadic_function_definition(self): 124 | # test that we can put nothing in the impure list parameter 125 | program = "(define (foo . args) 1) (foo)" 126 | self.assertEvaluatesTo(program, Integer(1)) 127 | 128 | # test that we can put multiple things in the impure list parameter 129 | program = "(define (f . everything) everything) (f 1 2 3)" 130 | self.assertEvaluatesTo(program, Cons.from_list([Integer(1), Integer(2), Integer(3)])) 131 | 132 | # test that the improper list parameter is evaluated 133 | program = "(define (g . everything) everything) (g (+ 2 3))" 134 | self.assertEvaluatesTo(program, Cons(Integer(5))) 135 | 136 | def test_lambda(self): 137 | program = "((lambda (x) (+ x x)) 4)" 138 | self.assertEvaluatesTo(program, Integer(8)) 139 | 140 | program = "((lambda () 1))" 141 | self.assertEvaluatesTo(program, Integer(1)) 142 | 143 | def test_lambda_long_body(self): 144 | program = "((lambda () 1 2))" 145 | self.assertEvaluatesTo(program, Integer(2)) 146 | 147 | def test_begin(self): 148 | program = "(begin)" 149 | self.assertEvaluatesTo(program, None) 150 | 151 | program = "(begin (define x 1) (+ x 3))" 152 | self.assertEvaluatesTo(program, Integer(4)) 153 | 154 | def test_comment(self): 155 | program = "; 1" 156 | 157 | self.assertEvaluatesTo(program, None) 158 | 159 | def test_quote(self): 160 | program = "(quote (1 2 3))" 161 | self.assertEvaluatesTo(program, Cons.from_list([Integer(1), Integer(2), Integer(3)])) 162 | 163 | program = "(quote ())" 164 | self.assertEvaluatesTo(program, Nil()) 165 | 166 | def test_quote_sugar(self): 167 | program = "'(1 2 3)" 168 | self.assertEvaluatesTo(program, Cons.from_list([Integer(1), Integer(2), Integer(3)])) 169 | 170 | def test_type_error(self): 171 | program = "(2 2)" 172 | self.assertRaises(SchemeTypeError, eval_program, program, None) 173 | 174 | def test_stack_overflow(self): 175 | # FIXME: with TCO, this is actually just an infinite loop 176 | program = "(define (f) (f)) (f)" 177 | self.assertRaises(SchemeStackOverflow, eval_program, program, None) 178 | 179 | def test_call_empty_list(self): 180 | program = "()" 181 | self.assertRaises(SchemeSyntaxError, eval_program, program, None) 182 | 183 | def test_quasiquote(self): 184 | program = "(quasiquote (1 1))" 185 | self.assertEvaluatesTo(program, Cons.from_list([Integer(1), Integer(1)])) 186 | 187 | program = "(quasiquote (unquote 1))" 188 | self.assertEvaluatesTo(program, Integer(1)) 189 | 190 | program = "(quasiquote (1 (unquote (+ 2 2))))" 191 | self.assertEvaluatesTo(program, Cons(Integer(1), Cons(Integer(4)))) 192 | 193 | program = "(quasiquote (1 (unquote-splicing '(2 2))))" 194 | self.assertEvaluatesTo(program, Cons(Integer(1), Cons(Integer(2), Cons(Integer(2))))) 195 | 196 | def test_quasiquote_sugar(self): 197 | program = "`,1" 198 | self.assertEvaluatesTo(program, Integer(1)) 199 | 200 | program = "`(1 ,@'(2 2))" 201 | self.assertEvaluatesTo(program, Cons.from_list([Integer(1), Integer(2), Integer(2)])) 202 | 203 | 204 | class EquivalenceTest(InterpreterTest): 205 | def test_eqv(self): 206 | program = "(eqv? 1 1)" 207 | self.assertEvaluatesTo(program, Boolean(True)) 208 | 209 | program = "(eqv? (quote foo) (quote foo))" 210 | self.assertEvaluatesTo(program, Boolean(True)) 211 | 212 | program = "(eqv? car car)" 213 | self.assertEvaluatesTo(program, Boolean(True)) 214 | 215 | program = "(eqv? (quote ()) (quote ()))" 216 | self.assertEvaluatesTo(program, Boolean(True)) 217 | 218 | program = "(eqv? (cons 1 2) (cons 1 2))" 219 | self.assertEvaluatesTo(program, Boolean(False)) 220 | 221 | def test_eq(self): 222 | program = "(eq? (quote foo) (quote foo))" 223 | self.assertEvaluatesTo(program, Boolean(True)) 224 | 225 | 226 | class ListTest(InterpreterTest): 227 | def test_car(self): 228 | program = "(car (quote (1 2 3)))" 229 | self.assertEvaluatesTo(program, Integer(1)) 230 | 231 | def test_cdr(self): 232 | program = "(cdr (quote (1 2 3)))" 233 | self.assertEvaluatesTo(program, Cons.from_list([Integer(2), Integer(3)])) 234 | 235 | program = "(cdr (quote (1 2 3)) 1)" 236 | with self.assertRaises(SchemeArityError): 237 | self.evaluate(program) 238 | 239 | def test_car_cdr_compositions(self): 240 | program = "(caar '((1 3) 2))" 241 | self.assertEvaluatesTo(program, Integer(1)) 242 | 243 | program = "(cadr '((1 3) 2))" 244 | self.assertEvaluatesTo(program, Integer(2)) 245 | 246 | program = "(cdar '((1 3) 2))" 247 | self.assertEvaluatesTo(program, Cons(Integer(3))) 248 | 249 | program = "(cddr '((1 3) 2))" 250 | self.assertEvaluatesTo(program, Nil()) 251 | 252 | def test_set_car(self): 253 | program = "(define x (list 4 5 6)) (set-car! x 1) x" 254 | self.assertEvaluatesTo(program, Cons.from_list([Integer(1), Integer(5), Integer(6)])) 255 | 256 | def test_set_cdr(self): 257 | program = "(define x (list 4 5 6)) (set-cdr! x 1) x" 258 | result = self.evaluate(program) 259 | self.assertEqual(result, Cons(Integer(4), Integer(1))) 260 | 261 | def test_cons(self): 262 | program = "(cons 1 (quote (2 3)))" 263 | self.assertEvaluatesTo(program, Cons.from_list([Integer(1), Integer(2), Integer(3)])) 264 | 265 | program = "(cons 1 2)" 266 | self.assertEvaluatesTo(program, Cons(Integer(1), Integer(2))) 267 | 268 | def test_null(self): 269 | program = "(null? 1)" 270 | self.assertEvaluatesTo(program, Boolean(False)) 271 | 272 | program = "(null? '())" 273 | self.assertEvaluatesTo(program, Boolean(True)) 274 | 275 | program = "(null? '(1 2))" 276 | self.assertEvaluatesTo(program, Boolean(False)) 277 | 278 | def test_is_list(self): 279 | program = "(list? '())" 280 | self.assertEvaluatesTo(program, Boolean(True)) 281 | 282 | program = "(list? 1)" 283 | self.assertEvaluatesTo(program, Boolean(False)) 284 | 285 | def test_list(self): 286 | program = "(list)" 287 | self.assertEvaluatesTo(program, Nil()) 288 | 289 | program = "(list 1 (+ 2 3))" 290 | self.assertEvaluatesTo(program, Cons.from_list([Integer(1), Integer(5)])) 291 | 292 | def test_length(self): 293 | program = "(length '())" 294 | self.assertEvaluatesTo(program, Integer(0)) 295 | 296 | program = "(length (cons 2 (cons 3 '())))" 297 | self.assertEvaluatesTo(program, Integer(2)) 298 | 299 | def test_pair(self): 300 | program = "(pair? (quote (a b)))" 301 | self.assertEvaluatesTo(program, Boolean(True)) 302 | 303 | program = "(pair? (quote ()))" 304 | self.assertEvaluatesTo(program, Boolean(False)) 305 | 306 | program = "(pair? 1)" 307 | self.assertEvaluatesTo(program, Boolean(False)) 308 | 309 | 310 | class ControlTest(InterpreterTest): 311 | def test_is_procedure(self): 312 | program = "(procedure? car)" 313 | self.assertEvaluatesTo(program, Boolean(True)) 314 | 315 | program = "(procedure? 1)" 316 | self.assertEvaluatesTo(program, Boolean(False)) 317 | 318 | program = "(procedure? (lambda (x) (+ x 1)))" 319 | self.assertEvaluatesTo(program, Boolean(True)) 320 | 321 | def test_map(self): 322 | program = "(map (lambda (x) (+ x 1)) '(2 3))" 323 | self.assertEvaluatesTo(program, Cons(Integer(3), Cons(Integer(4)))) 324 | 325 | def test_for_each(self): 326 | program = """(let ((total 0)) 327 | (for-each 328 | (lambda (x) (set! total (+ total x))) 329 | '(1 2 3)) 330 | total)""" 331 | self.assertEvaluatesTo(program, Integer(6)) 332 | 333 | 334 | class MathsTest(InterpreterTest): 335 | def test_addition(self): 336 | program = "(+ 1 2 3)" 337 | self.assertEvaluatesTo(program, Integer(6)) 338 | 339 | program = "(+)" 340 | self.assertEvaluatesTo(program, Integer(0)) 341 | 342 | program = "(+ 1 2.5)" 343 | self.assertEvaluatesTo(program, FloatingPoint(3.5)) 344 | 345 | def test_subtraction(self): 346 | program = "(- 1 2 3)" 347 | self.assertEvaluatesTo(program, Integer(-4)) 348 | 349 | program = "(- 2)" 350 | self.assertEvaluatesTo(program, Integer(-2)) 351 | 352 | program = "(- 0.1)" 353 | self.assertEvaluatesTo(program, FloatingPoint(-0.1)) 354 | 355 | program = "(- 10.0 0.5)" 356 | self.assertEvaluatesTo(program, FloatingPoint(9.5)) 357 | 358 | def test_multiplication(self): 359 | program = "(* 2 2 3)" 360 | self.assertEvaluatesTo(program, Integer(12)) 361 | 362 | program = "(*)" 363 | self.assertEvaluatesTo(program, Integer(1)) 364 | 365 | program = "(* 3 0.5)" 366 | self.assertEvaluatesTo(program, FloatingPoint(1.5)) 367 | 368 | def test_division(self): 369 | program = "(/ 8)" 370 | self.assertEvaluatesTo(program, FloatingPoint(0.125)) 371 | 372 | program = "(/ 12 3 2)" 373 | self.assertEvaluatesTo(program, Integer(2)) 374 | 375 | def test_less_than(self): 376 | program = "(< 1 1)" 377 | self.assertEvaluatesTo(program, Boolean(False)) 378 | 379 | program = "(< 1 2 4)" 380 | self.assertEvaluatesTo(program, Boolean(True)) 381 | 382 | def test_less_or_equal(self): 383 | program = "(<= 1 1)" 384 | self.assertEvaluatesTo(program, Boolean(True)) 385 | 386 | program = "(<= 1 0)" 387 | self.assertEvaluatesTo(program, Boolean(False)) 388 | 389 | def test_greater_than(self): 390 | program = "(> 1 1)" 391 | self.assertEvaluatesTo(program, Boolean(False)) 392 | 393 | program = "(> 11 10 0)" 394 | self.assertEvaluatesTo(program, Boolean(True)) 395 | 396 | def test_greater_or_equal(self): 397 | program = "(>= 1 1)" 398 | self.assertEvaluatesTo(program, Boolean(True)) 399 | 400 | program = "(>= 1 3 2)" 401 | self.assertEvaluatesTo(program, Boolean(False)) 402 | 403 | def test_equality(self): 404 | program = "(=)" 405 | self.assertEvaluatesTo(program, Boolean(True)) 406 | 407 | program = "(= 0)" 408 | self.assertEvaluatesTo(program, Boolean(True)) 409 | 410 | program = "(= 0 0)" 411 | self.assertEvaluatesTo(program, Boolean(True)) 412 | 413 | program = "(= 0 1)" 414 | self.assertEvaluatesTo(program, Boolean(False)) 415 | 416 | program = "(= 1.0 1)" 417 | self.assertEvaluatesTo(program, Boolean(True)) 418 | 419 | def test_number(self): 420 | program = "(number? 1)" 421 | self.assertEvaluatesTo(program, Boolean(True)) 422 | 423 | program = "(number? 1.0)" 424 | self.assertEvaluatesTo(program, Boolean(True)) 425 | 426 | program = "(number? #\\a)" 427 | self.assertEvaluatesTo(program, Boolean(False)) 428 | 429 | def test_complex(self): 430 | program = "(complex? 0)" 431 | self.assertEvaluatesTo(program, Boolean(True)) 432 | 433 | def test_rational(self): 434 | program = "(rational? 1.1)" 435 | self.assertEvaluatesTo(program, Boolean(True)) 436 | 437 | def test_real(self): 438 | program = "(real? 1.2)" 439 | self.assertEvaluatesTo(program, Boolean(True)) 440 | 441 | def test_exact(self): 442 | program = "(exact? 1)" 443 | self.assertEvaluatesTo(program, Boolean(True)) 444 | 445 | program = "(exact? 1.0)" 446 | self.assertEvaluatesTo(program, Boolean(False)) 447 | 448 | def test_inexact(self): 449 | program = "(inexact? 0)" 450 | self.assertEvaluatesTo(program, Boolean(False)) 451 | 452 | program = "(inexact? 0.0)" 453 | self.assertEvaluatesTo(program, Boolean(True)) 454 | 455 | def test_quotient(self): 456 | program = "(quotient 3 2)" 457 | self.assertEvaluatesTo(program, Integer(1)) 458 | 459 | program = "(quotient 4 2)" 460 | self.assertEvaluatesTo(program, Integer(2)) 461 | 462 | program = "(quotient -13 4)" 463 | self.assertEvaluatesTo(program, Integer(-3)) 464 | 465 | def test_modulo(self): 466 | program = "(modulo 4 2)" 467 | self.assertEvaluatesTo(program, Integer(0)) 468 | 469 | program = "(modulo 5 2)" 470 | self.assertEvaluatesTo(program, Integer(1)) 471 | 472 | program = "(modulo -13 4)" 473 | self.assertEvaluatesTo(program, Integer(3)) 474 | 475 | def test_remainder(self): 476 | program = "(remainder 4 2)" 477 | self.assertEvaluatesTo(program, Integer(0)) 478 | 479 | program = "(remainder 5 2)" 480 | self.assertEvaluatesTo(program, Integer(1)) 481 | 482 | program = "(remainder -13 4)" 483 | self.assertEvaluatesTo(program, Integer(-1)) 484 | 485 | program = "(remainder 13 -4)" 486 | self.assertEvaluatesTo(program, Integer(1)) 487 | 488 | def test_exp(self): 489 | program = "(exp 0)" 490 | self.assertEvaluatesTo(program, FloatingPoint(1.0)) 491 | 492 | program = "(exp 2)" 493 | self.assertEvaluatesTo(program, FloatingPoint(7.38905609893065)) 494 | 495 | def test_log(self): 496 | program = "(log 1)" 497 | self.assertEvaluatesTo(program, FloatingPoint(0.0)) 498 | 499 | program = "(log 7.38905609893065)" 500 | self.assertEvaluatesTo(program, FloatingPoint(2.0)) 501 | 502 | class LibraryMathsTest(InterpreterTest): 503 | def test_zero_predicate(self): 504 | program = "(zero? 0)" 505 | self.assertEvaluatesTo(program, Boolean(True)) 506 | 507 | def test_positive_predicate(self): 508 | program = "(positive? 1)" 509 | self.assertEvaluatesTo(program, Boolean(True)) 510 | 511 | program = "(positive? -1)" 512 | self.assertEvaluatesTo(program, Boolean(False)) 513 | 514 | program = "(positive? 0)" 515 | self.assertEvaluatesTo(program, Boolean(True)) 516 | 517 | def test_negative_predicate(self): 518 | program = "(negative? -1)" 519 | self.assertEvaluatesTo(program, Boolean(True)) 520 | 521 | program = "(negative? 3)" 522 | self.assertEvaluatesTo(program, Boolean(False)) 523 | 524 | def test_odd_predicate(self): 525 | program = "(odd? 1)" 526 | self.assertEvaluatesTo(program, Boolean(True)) 527 | 528 | program = "(odd? 0)" 529 | self.assertEvaluatesTo(program, Boolean(False)) 530 | 531 | def test_even_predicate(self): 532 | program = "(even? 6)" 533 | self.assertEvaluatesTo(program, Boolean(True)) 534 | 535 | program = "(even? 7)" 536 | self.assertEvaluatesTo(program, Boolean(False)) 537 | 538 | def test_abs(self): 539 | program = "(abs -5.1)" 540 | self.assertEvaluatesTo(program, FloatingPoint(5.1)) 541 | 542 | program = "(abs 0.2)" 543 | self.assertEvaluatesTo(program, FloatingPoint(0.2)) 544 | 545 | 546 | class CharacterTest(InterpreterTest): 547 | def test_char_predicate(self): 548 | program = "(char? #\\a)" 549 | self.assertEvaluatesTo(program, Boolean(True)) 550 | 551 | program = "(char? 0)" 552 | self.assertEvaluatesTo(program, Boolean(False)) 553 | 554 | program = "(char? (quote (x)))" 555 | self.assertEvaluatesTo(program, Boolean(False)) 556 | def test_equality(self): 557 | program = "(char=? #\\ #\\space)" 558 | self.assertEvaluatesTo(program, Boolean(True)) 559 | 560 | def test_char_less_than(self): 561 | program = "(char? #\\1 #\\0)" 566 | self.assertEvaluatesTo(program, Boolean(True)) 567 | 568 | program = "(char>? #\\0 #\\0)" 569 | self.assertEvaluatesTo(program, Boolean(False)) 570 | 571 | program = "(char>? #\\0 #\\1)" 572 | self.assertEvaluatesTo(program, Boolean(False)) 573 | 574 | def test_char_less_or_equal(self): 575 | program = "(char<=? #\\y #\\z)" 576 | self.assertEvaluatesTo(program, Boolean(True)) 577 | 578 | program = "(char<=? #\\z #\\z)" 579 | self.assertEvaluatesTo(program, Boolean(True)) 580 | 581 | program = "(char<=? #\\z #\\y)" 582 | self.assertEvaluatesTo(program, Boolean(False)) 583 | 584 | def test_char_greater_or_equal(self): 585 | program = "(char>=? #\\y #\\z)" 586 | self.assertEvaluatesTo(program, Boolean(False)) 587 | 588 | program = "(char>=? #\\( #\\()" 589 | self.assertEvaluatesTo(program, Boolean(True)) 590 | 591 | program = "(char>=? #\\z #\\y)" 592 | self.assertEvaluatesTo(program, Boolean(True)) 593 | 594 | 595 | class StringTest(InterpreterTest): 596 | def test_string_predicate(self): 597 | program = '(string? "foo")' 598 | self.assertEvaluatesTo(program, Boolean(True)) 599 | 600 | program = '(string? (quote ("foo")))' 601 | self.assertEvaluatesTo(program, Boolean(False)) 602 | 603 | def test_make_string(self): 604 | program = '(make-string 2)' 605 | self.assertEvaluatesTo(program, String(" ")) 606 | 607 | program = '(make-string 2 #\\a)' 608 | self.assertEvaluatesTo(program, String("aa")) 609 | 610 | program = '(make-string 0 #\\a)' 611 | self.assertEvaluatesTo(program, String("")) 612 | 613 | def test_string_length(self): 614 | program = '(string-length "")' 615 | self.assertEvaluatesTo(program, Integer(0)) 616 | 617 | program = '(string-length "abcdef")' 618 | self.assertEvaluatesTo(program, Integer(6)) 619 | 620 | def test_string_ref(self): 621 | program = '(string-ref "abc" 2)' 622 | self.assertEvaluatesTo(program, Character('c')) 623 | 624 | def test_string_set(self): 625 | program = '(define s "abc") (string-set! s 0 #\z) s' 626 | self.assertEvaluatesTo(program, String('zbc')) 627 | 628 | 629 | class BooleanTest(InterpreterTest): 630 | def test_not(self): 631 | program = '(not #f)' 632 | self.assertEvaluatesTo(program, Boolean(True)) 633 | 634 | program = '(not \'())' 635 | self.assertEvaluatesTo(program, Boolean(False)) 636 | 637 | def test_is_boolean(self): 638 | program = "(boolean? #t)" 639 | self.assertEvaluatesTo(program, Boolean(True)) 640 | 641 | program = "(boolean? 1)" 642 | self.assertEvaluatesTo(program, Boolean(False)) 643 | 644 | def test_and(self): 645 | program = "(and (= 2 2) (> 2 1))" 646 | self.assertEvaluatesTo(program, Boolean(True)) 647 | 648 | program = "(and \"foo\" \"bar\")" 649 | self.assertEvaluatesTo(program, String("bar")) 650 | 651 | def test_or(self): 652 | program = "(or (= 2 3) (> 2 1))" 653 | self.assertEvaluatesTo(program, Boolean(True)) 654 | 655 | program = "(or (= 2 3) (> 1 2))" 656 | self.assertEvaluatesTo(program, Boolean(False)) 657 | 658 | program = "(or \"foo\" \"bar\")" 659 | self.assertEvaluatesTo(program, String("foo")) 660 | 661 | 662 | class VectorTest(InterpreterTest): 663 | def test_make_vector(self): 664 | program = '(make-vector 0)' 665 | self.assertEvaluatesAs(program, Vector(0)) 666 | 667 | program = '(make-vector 2)' 668 | self.assertEvaluatesAs(program, Vector(2)) 669 | 670 | def test_make_vector_with_init(self): 671 | program = '(make-vector 1 3)' 672 | self.assertEvaluatesAs(program, Vector.from_list([Integer(3)])) 673 | 674 | def test_is_vector(self): 675 | program = "(vector? '())" 676 | self.assertEvaluatesTo(program, Boolean(False)) 677 | 678 | program = "(vector? (make-vector 1))" 679 | self.assertEvaluatesTo(program, Boolean(True)) 680 | 681 | def test_vector_set(self): 682 | program = "(let ((v (make-vector 1))) (vector-set! v 0 5) v)" 683 | self.assertEvaluatesAs(program, Vector.from_list([Integer(5)])) 684 | 685 | def test_vector_ref(self): 686 | program = "(let ((v (make-vector 1))) (vector-set! v 0 5) (vector-ref v 0))" 687 | self.assertEvaluatesTo(program, Integer(5)) 688 | 689 | def test_vector_length(self): 690 | program = "(vector-length (make-vector 5))" 691 | self.assertEvaluatesTo(program, Integer(5)) 692 | 693 | def test_vector(self): 694 | program = "(vector 1 2)" 695 | self.assertEvaluatesAs( 696 | program, 697 | Vector.from_list([Integer(1), Integer(2)])) 698 | 699 | def test_vector_to_list(self): 700 | program = "(vector->list (vector 1 2))" 701 | self.assertEvaluatesTo(program, Cons.from_list([Integer(1), Integer(2)])) 702 | 703 | def test_list_to_vector(self): 704 | program = "(list->vector (list 1 2))" 705 | self.assertEvaluatesAs( 706 | program, 707 | Vector.from_list([Integer(1), Integer(2)])) 708 | 709 | def test_vector_fill(self): 710 | program = "(let ((v (make-vector 1))) (vector-fill! v 5) v)" 711 | self.assertEvaluatesAs( 712 | program, 713 | Vector.from_list([Integer(5)])) 714 | 715 | 716 | class IOTest(InterpreterTest): 717 | def setUp(self): 718 | super().setUp() 719 | self.saved_stdout = sys.stdout 720 | 721 | self.fake_stdout = StringIO() 722 | sys.stdout = self.fake_stdout 723 | 724 | def tearDown(self): 725 | sys.stdout = self.saved_stdout 726 | 727 | def test_display(self): 728 | program = '(display "hello")' 729 | eval_program(program, self.environment) 730 | 731 | self.assertEqual(sys.stdout.getvalue(), "hello") 732 | 733 | def test_newline(self): 734 | program = '(newline)' 735 | eval_program(program, self.environment) 736 | 737 | self.assertEqual(sys.stdout.getvalue(), "\n") 738 | 739 | 740 | class MacroTest(InterpreterTest): 741 | """Test macro definition, but also test syntax defined in the 742 | standard library using macros. 743 | 744 | """ 745 | def test_defmacro(self): 746 | program = '(defmacro inc (argument) `(+ 1 ,argument)) (inc 5)' 747 | self.assertEvaluatesTo(program, Integer(6)) 748 | 749 | def test_let(self): 750 | program = "(let ((x 1)) x)" 751 | self.assertEvaluatesTo(program, Integer(1)) 752 | 753 | def test_let_last_argument(self): 754 | program = "(let ((x 1)) 2 x)" 755 | self.assertEvaluatesTo(program, Integer(1)) 756 | 757 | def test_cond(self): 758 | program = "(cond ((else 1)))" 759 | self.assertEvaluatesTo(program, Integer(1)) 760 | 761 | program = "(define x 1) (cond (((> x 0) 3) (else 1)))" 762 | self.assertEvaluatesTo(program, Integer(3)) 763 | 764 | program = "(define y 1) (cond (((< y 0) 3) (else 1)))" 765 | self.assertEvaluatesTo(program, Integer(1)) 766 | 767 | 768 | if __name__ == '__main__': 769 | unittest.main() 770 | -------------------------------------------------------------------------------- /interpreter/utils.py: -------------------------------------------------------------------------------- 1 | from errors import SchemeArityError 2 | 3 | def check_argument_number(function_name, given_arguments, 4 | min_arguments, max_arguments=None): 5 | assert max_arguments is None or min_arguments <= max_arguments 6 | 7 | right_argument_number = True 8 | 9 | if len(given_arguments) < min_arguments: 10 | right_argument_number = False 11 | 12 | if max_arguments and len(given_arguments) > max_arguments: 13 | right_argument_number = False 14 | 15 | if not right_argument_number: 16 | if min_arguments == max_arguments: 17 | raise SchemeArityError("%s requires exactly %d argument(s), but " 18 | "received %d." % (function_name, 19 | min_arguments, 20 | len(given_arguments))) 21 | else: 22 | if max_arguments: 23 | raise SchemeArityError("%s requires between %d and %d argument(s), but " 24 | "received %d." % (function_name, 25 | min_arguments, 26 | max_arguments, 27 | len(given_arguments))) 28 | else: 29 | raise SchemeArityError("%s requires at least %d argument(s), but " 30 | "received %d." % (function_name, 31 | min_arguments, 32 | len(given_arguments))) 33 | -------------------------------------------------------------------------------- /notes.py: -------------------------------------------------------------------------------- 1 | """A worked example with Fibonacci numbers, to demonstrate tail recursion in Python.""" 2 | 3 | # fibonacci, stack consuming 4 | def fib(n): 5 | if n == 0: 6 | return 1 7 | 8 | return n * fib(n-1) 9 | 10 | 11 | # tail recursive style 12 | def fib2inner(n, accum): 13 | if n == 0: 14 | return accum 15 | 16 | return fib2inner(n - 1, n * accum) # would not consume stack if we had tail recursion 17 | 18 | def fib2(n): 19 | return fib2inner(n, 1) 20 | 21 | 22 | # continuation passing style 23 | def fib3inner(n, continuation): 24 | if n == 0: 25 | return continuation(1) 26 | 27 | return fib3inner(n - 1, lambda x: continuation(x * n)) # still consumes stack 28 | 29 | def fib3(n): 30 | return fib3inner(n, lambda x: x) 31 | 32 | 33 | # tail recursive continuation passing 34 | def fib4inner(n, accum, continuation): 35 | if n == 0: 36 | return continuation(accum) 37 | 38 | return fib4inner(n - 1, n * accum, continuation) 39 | 40 | def fib4(n): 41 | return fib4inner(n, 1, lambda x: x) 42 | 43 | 44 | # a trampoline gives us proper tail recursion 45 | def trampoline(func): 46 | while callable(func): 47 | func = func() 48 | 49 | return func 50 | 51 | # trampolined, tail recursive, passing a continuation 52 | def fib5inner(n, accum, continuation): 53 | if n == 0: 54 | return lambda: continuation(accum) 55 | 56 | return lambda: fib5inner(n - 1, n * accum, continuation) 57 | 58 | def fib5(n): 59 | return trampoline(fib5inner(n, 1, lambda x: x)) 60 | -------------------------------------------------------------------------------- /repl: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python interpreter/main.py -------------------------------------------------------------------------------- /requirements.pip: -------------------------------------------------------------------------------- 1 | ply==3.4 2 | nose==1.2.1 3 | -------------------------------------------------------------------------------- /standard_library/library.scm: -------------------------------------------------------------------------------- 1 | ; maths predicates: 2 | (define (zero? x) 3 | (if (= x 0) 4 | #t 5 | #f)) 6 | 7 | (define (positive? x) 8 | (if (>= x 0) 9 | #t 10 | #f)) 11 | 12 | (define (negative? x) 13 | (if (< x 0) 14 | #t 15 | #f)) 16 | 17 | (define (odd? x) 18 | (if (= (modulo x 2) 1) 19 | #t 20 | #f)) 21 | 22 | (define (even? x) 23 | (if (= (modulo x 2) 0) 24 | #t 25 | #f)) 26 | 27 | (define (abs x) 28 | (if (positive? x) 29 | x 30 | (- x))) 31 | 32 | ; list functions 33 | (define (null? x) 34 | (eqv? x '())) 35 | 36 | ; FIXME: doesn't handle infinite lists 37 | (define (list? x) 38 | (if (null? x) 39 | #t 40 | (if (pair? x) 41 | (list? (cdr x)) 42 | #f))) 43 | 44 | (define (list . args) 45 | args) 46 | 47 | (define (length x) 48 | (if (null? x) 49 | 0 50 | (+ 1 (length (cdr x))))) 51 | 52 | (define (caar x) 53 | (car (car x))) 54 | 55 | (define (cadr x) 56 | (car (cdr x))) 57 | 58 | (define (cdar x) 59 | (cdr (car x))) 60 | 61 | (define (cddr x) 62 | (cdr (cdr x))) 63 | 64 | ; control features 65 | (define (map function list) 66 | (if (null? list) 67 | '() 68 | (cons (function (car list)) 69 | (map function (cdr list))))) 70 | 71 | (define (for-each function list) 72 | (if (null? list) 73 | '() 74 | (begin 75 | (function (car list)) 76 | (for-each function (cdr list))))) 77 | 78 | ; scoping macros 79 | (defmacro let (assignments . body) 80 | `((lambda ,(map car assignments) ,@body) 81 | ,@(map cadr assignments))) 82 | 83 | (defmacro cond (clauses) 84 | (let ((first-clause (car clauses))) 85 | ; if we reach an else statment we evaluate it unconditionally 86 | (if (eqv? (car first-clause) 'else) 87 | (cadr first-clause) 88 | ; if this condition is true, evaluate the body of that condition 89 | `(if ,(car first-clause) 90 | ,(cadr first-clause) 91 | ; otherwise recurse on the rest of the clauses 92 | (cond ,(cdr clauses)))))) 93 | 94 | ; vector functions 95 | (define (vector . args) 96 | (let ((v (make-vector (length args))) 97 | (index 0)) 98 | (for-each 99 | (lambda (arg) 100 | (vector-set! v index arg) 101 | (set! index (+ index 1))) 102 | args) 103 | v)) 104 | 105 | (define (vector->list vector) 106 | ;; vector-list-iter is a recursive helper function that moves 107 | ;; through the vector and builds a list. 108 | (let ((vector->list-iter 109 | (lambda (index) 110 | (if (>= index (vector-length vector)) 111 | '() 112 | (cons 113 | (vector-ref vector index) 114 | (vector->list-iter (+ index 1))))))) 115 | (vector->list-iter 0))) 116 | 117 | (define (list->vector list) 118 | (let ((v (make-vector (length list))) 119 | (index 0)) 120 | (for-each 121 | (lambda (item) 122 | (vector-set! v index item) 123 | (set! index (+ index 1))) 124 | list) 125 | v)) 126 | 127 | (define (vector-fill! vector fill) 128 | (let ((vector-fill-iter 129 | (lambda (index) 130 | (if (>= index (vector-length vector)) 131 | '() 132 | (begin 133 | (vector-set! vector index fill) 134 | (vector-fill-iter (+ index 1))))))) 135 | (vector-fill-iter 0))) 136 | 137 | ; I/O 138 | (define (newline) 139 | (display "\n")) 140 | 141 | ; booleans 142 | ; note that R5RS requires 'and and 'or to take a variable number of arguments 143 | (defmacro and (x y) 144 | `(if ,x (if ,y ,y #f) #f)) 145 | 146 | (defmacro or (x y) 147 | `(if ,x ,x ,y)) 148 | 149 | (define (not x) 150 | (eqv? x #f)) 151 | 152 | (define (boolean? x) 153 | (or (eqv? x #f) 154 | (eqv? x #t))) 155 | 156 | 157 | ; characters 158 | (define (char>? x y) 159 | (and (not (char=? x y)) 160 | (not (char=? x y) 167 | (not (char