├── samples ├── sample.kimi ├── closure.kimi ├── factorial.kimi ├── max.kimi └── map.kimi ├── errors.py ├── .gitignore ├── LICENSE ├── evaluator.py ├── parser.py ├── special_forms.py ├── kimi ├── tokenizer.py ├── environments.py ├── tests.py └── README.md /samples/sample.kimi: -------------------------------------------------------------------------------- 1 | (do 2 | (define x 3) 3 | (define y 4) 4 | (+ x y) 5 | ) 6 | -------------------------------------------------------------------------------- /samples/closure.kimi: -------------------------------------------------------------------------------- 1 | (do 2 | (define make_adder (lambda n (lambda x (+ n x)))) 3 | (define add3 (make_adder 3)) 4 | (add3 4) 5 | ) 6 | -------------------------------------------------------------------------------- /samples/factorial.kimi: -------------------------------------------------------------------------------- 1 | (do 2 | (define fact 3 | (lambda n 4 | (if (= n 0) 1 5 | (* n (fact (- n 1)))))) 6 | (fact 3) 7 | ) 8 | -------------------------------------------------------------------------------- /samples/max.kimi: -------------------------------------------------------------------------------- 1 | (do 2 | (define max 3 | (lambda a b 4 | (if (= a b) 5 | nil 6 | (if (> a b) a b) 7 | ) 8 | ) 9 | ) 10 | (max 4 5) 11 | ) 12 | -------------------------------------------------------------------------------- /samples/map.kimi: -------------------------------------------------------------------------------- 1 | (do 2 | (define map 3 | (lambda fn list 4 | (if (= list nil) 5 | nil 6 | (prepend (fn (first list)) (map fn (rest list))) 7 | ) 8 | ) 9 | ) 10 | (map ! (list true false true)) 11 | ) 12 | -------------------------------------------------------------------------------- /errors.py: -------------------------------------------------------------------------------- 1 | # Kimi language interpreter in Python 3 2 | # Anjana Vakil 3 | # http://www.github.com/vakila/kimi 4 | 5 | def complain_and_die(message): 6 | print(message) 7 | quit() 8 | 9 | def assert_or_complain(assertion, message): 10 | try: 11 | assert assertion 12 | except AssertionError: 13 | complain_and_die(message) 14 | 15 | 16 | def throw_error(err_type, message): 17 | error = err_type.upper() + " ERROR!" 18 | print(error, message) 19 | quit() 20 | 21 | def assert_or_throw(assertion, err_type, message): 22 | try: 23 | assert assertion 24 | except AssertionError: 25 | throw_error(err_type, message) 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### CUSTOM 2 | scratch 3 | 4 | ### PYTHON 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anjana Sofia Vakil 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /evaluator.py: -------------------------------------------------------------------------------- 1 | # Kimi language interpreter in Python 3 2 | # Anjana Vakil 3 | # http://www.github.com/vakila/kimi 4 | 5 | import special_forms as sf 6 | from environments import Environment 7 | from errors import * 8 | 9 | SPECIALS = sf.special_forms() 10 | 11 | def evaluate(expression, environment): 12 | '''Take an expression and environment as dictionaries. 13 | Evaluate the expression in the context of the environment, and return the result. 14 | 15 | >>> evaluate(parse(tokenize("(+ 1 2)")), standard_env()) 16 | 3 17 | ''' 18 | # print("EVALUATING:", expression) 19 | expr_type = expression['type'] 20 | # print("EXPR_TYPE:", expr_type) 21 | if expr_type == 'literal': 22 | return expression['value'] 23 | elif expr_type == 'symbol': 24 | symbol = expression['value'] 25 | return environment.get(symbol) 26 | elif expr_type == 'apply': 27 | operator = expression['operator'] 28 | if operator['type'] == 'symbol' and operator['value'] in SPECIALS: 29 | return SPECIALS[operator['value']](expression['arguments'], environment) 30 | fn = evaluate(operator, environment) 31 | assert_or_throw(callable(fn), "type", 'Trying to call a non-function. Did you use parentheses correctly?') 32 | return fn(*[evaluate(arg, environment) for arg in expression['arguments']]) 33 | else: 34 | complain_and_die("PARSING ERROR! Unexpected expression type: " + str(expression) + ".") 35 | -------------------------------------------------------------------------------- /parser.py: -------------------------------------------------------------------------------- 1 | # Kimi language interpreter in Python 3 2 | # Anjana Vakil 3 | # http://www.github.com/vakila/kimi 4 | 5 | from errors import * 6 | 7 | def parse(tokens): 8 | '''Take a list of tokens representing a program, return a tree representing the program's syntax. 9 | The tree is a nested dictionary, where each dictionary in the tree is an expression. 10 | 11 | Expressions as dictionaries: 12 | - All contain the key 'type' 13 | - Based on the value of 'type', the dictionary will also have other keys: 14 | - type 'apply' (function application) has keys 'operator' (value: expression) and 'arguments' (value: tuple of expressions) 15 | - type 'symbol' (variable or operator) has key 'name' (value: string representing the variable/operator) 16 | - type 'literal' (number, string, boolean, ...) has key 'value' (value: the literal) 17 | 18 | >>> parse(tokenize("(+ 1 2)")) 19 | {'type': 'apply', 20 | 'operator': {'type': 'symbol', 'value': '+'}, 21 | 'arguments': ({'type': 'literal', 'value': 1}, 22 | {'type': 'literal', 'value': 2})} 23 | 24 | ''' 25 | # print("tokens:", tokens) 26 | if len(tokens) == 0: 27 | throw_error("syntax", "Nothing left to parse.") 28 | (token_type, token_value) = tokens.pop(0) 29 | if token_type == 'closing': 30 | throw_error("syntax", "Unexpected ')'.") 31 | elif token_type == 'opening': 32 | # print("OPENING") 33 | operator = parse(tokens) 34 | arguments = [] 35 | while True: 36 | if not tokens: 37 | throw_error("syntax", "Unexpected end of program.") 38 | next_token = tokens[0] 39 | if next_token[0] == 'closing': 40 | # print("CLOSING") 41 | tokens.pop(0) 42 | # print("tokens:", tokens) 43 | break 44 | arguments.append(parse(tokens)) 45 | arguments = tuple(arguments) 46 | return {'type': 'apply', 'operator': operator, 'arguments': arguments} 47 | else: 48 | return {'type': token_type, 'value': token_value} 49 | -------------------------------------------------------------------------------- /special_forms.py: -------------------------------------------------------------------------------- 1 | # Kimi language interpreter in Python 3 2 | # Anjana Vakil 3 | # http://www.github.com/vakila/kimi 4 | 5 | import evaluator as ev 6 | from environments import Environment 7 | from errors import * 8 | 9 | def do(args, env): 10 | do_env = Environment(name="do", outer=env) 11 | if len(args) == 0: 12 | throw_error("syntax", "Incorrect use of (do ...): must take at least one argument.") 13 | result = None 14 | for a in args: 15 | result = ev.evaluate(a, do_env) 16 | return result 17 | 18 | def lamb(args, env): 19 | # print("\n") 20 | # print("args (" + str(len(args)) + "):", args) 21 | # print("env: ", env.name) 22 | if len(args) < 2: 23 | throw_error("syntax", "Incorrect use of (lambda ...): must take at least two arguments (at least one variable and a body).") 24 | largs = args[:-1] 25 | lbody = args[-1] 26 | # print("largs (" + str(len(largs)) + "):", largs) 27 | for l in largs: 28 | assert_or_throw(l['type'] == 'symbol', "syntax", "Incorrect use of (lambda ...): the anonymous function's variables must be symbols.") 29 | largs = tuple(la['value'] for la in largs) 30 | # print("lbody:", lbody) 31 | def anonymous(*arguments): 32 | # print("inside anonymous function") 33 | # print("arguments(" + str(len(arguments)) + "):", arguments) 34 | if len(arguments) != len(largs): 35 | throw_error("syntax", "This function takes " + str(len(largs)) + " arguments (" + str(len(arguments)) + " provided).") 36 | lenv = Environment(name="anon_fn", outer=env, variables=largs, values=arguments) 37 | return ev.evaluate(lbody, lenv) 38 | return anonymous 39 | 40 | def define(args, env): 41 | if len(args) != 2: 42 | throw_error("syntax", "Incorrect use of (define ...): must take exactly two arguments.") 43 | assert_or_throw(args[0]['type'] == 'symbol', "type", "Incorrect use of (define ...): the variable must be a symbol.") 44 | variable = args[0]['value'] 45 | value = ev.evaluate(args[1], env) 46 | env.set(variable, value) 47 | return value 48 | 49 | def cond(args, env): 50 | if len(args) != 3: 51 | throw_error("syntax", "Incorrect use of (if ...): must take exactly three arguments (a test, a pass case, and a fail case).") 52 | test = ev.evaluate(args[0], env) 53 | if type(test) != bool: 54 | throw_error("type", "Incorrect use of (if ...): the test must evaluate to a boolean.") 55 | if test: 56 | return ev.evaluate(args[1], env) 57 | else: 58 | return ev.evaluate(args[2], env) 59 | 60 | def special_forms(): 61 | specials = dict() 62 | 63 | specials['do'] = do 64 | specials['lambda'] = lamb 65 | specials['define'] = define 66 | specials['if'] = cond 67 | 68 | return specials 69 | -------------------------------------------------------------------------------- /kimi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # Kimi language interpreter in Python 3 4 | # Anjana Vakil 5 | # http://www.github.com/vakila/kimi 6 | 7 | import sys 8 | from tokenizer import tokenize 9 | from parser import parse 10 | from evaluator import evaluate 11 | from environments import standard_env 12 | from errors import * 13 | 14 | def execute(program): 15 | '''Take a Kimi program as a string. Tokenize the program, parse the tokens into a tree, 16 | then evaluate the tree. Return the result, or an error message.''' 17 | return evaluate(parse(tokenize(program)), standard_env()) 18 | 19 | def repl(): 20 | '''An interactive Read-Evaluate-Print Loop that takes in Kimi code from a prompt and evaluates it.''' 21 | quit_commands = ["exit", "quit", "q"] 22 | print("Welcome to Kimi!") 23 | print("See the README (https://github.com/vakila/kimi) for information about Kimi.") 24 | print('To exit the interpreter, type "' + '" or "'.join(quit_commands) + '".') 25 | prompt = 'kimi> ' 26 | global_env = standard_env() 27 | while True: 28 | command = input(prompt) 29 | if command == "": 30 | continue 31 | if command in quit_commands: 32 | return "Goodbye!" 33 | val = evaluate(parse(tokenize(command)), global_env) 34 | print(kimify(val)) 35 | 36 | def kimify(exp): 37 | '''Convert a Python object back into a Kimi-readable string.''' 38 | if exp == None: 39 | return "nil" 40 | elif type(exp) == bool: 41 | return {True: "true", False: "false"}[exp] 42 | elif type(exp) == int: 43 | return str(exp) 44 | elif type(exp) == str: 45 | return '"' + exp + '"' 46 | elif type(exp) == tuple: 47 | return "(list " + kimify_list(exp) + ")" 48 | elif callable(exp): 49 | return "<" + exp.__name__ + " function>" 50 | 51 | def kimify_list(tups): 52 | if tups[1] == None: 53 | return kimify(tups[0]) 54 | else: 55 | return " ".join([kimify(tups[0]), kimify_list(tups[1])]) 56 | 57 | 58 | if __name__ == "__main__": 59 | if len(sys.argv) == 1: 60 | repl() 61 | #activate repl 62 | elif len(sys.argv) == 2: 63 | program = sys.argv[1] 64 | if program.endswith('.kimi'): 65 | with open(program, 'r') as f: 66 | program = f.read() 67 | # print("Evaluating program:") 68 | # print(program) 69 | # print("\nResult:") 70 | print(kimify(execute(program))) 71 | else: 72 | print("Usage:") 73 | print("Activate the interactive interpreter (REPL): $ python3 kimi.py") 74 | print("Evaluate a Kimi program in an external file: $ python3 kimi.py my_program.kimi") 75 | print('Evaluate a simple Kimi program as a string: $ python3 kimi.py "(+ 1 2)"') 76 | -------------------------------------------------------------------------------- /tokenizer.py: -------------------------------------------------------------------------------- 1 | # Kimi language interpreter in Python 3 2 | # Anjana Vakil 3 | # http://www.github.com/vakila/kimi 4 | 5 | from errors import * 6 | 7 | def tokenize(string): 8 | '''Take a program as a string, return the tokenized program as a list of strings. 9 | 10 | >>> tokenize("-1") 11 | [('literal', -1)] 12 | 13 | >>> tokenize("(+ 1 2)") 14 | [('opening', None), ('symbol', '+'), ('literal', 1), ('literal', 2), ('closing', None)] 15 | ''' 16 | assert_or_throw(string.count('(') == string.count(')'), "syntax", "Mismatching parentheses!") 17 | assert_or_throw('(((' not in string, "syntax", 'Incorrect parenthesis use: "(((". Opening parenthesis must be immediately followed by a function.') 18 | special = ['(',')','"'] 19 | whitespaces = [' ','\n','\t'] 20 | tokens = [] 21 | remaining = string 22 | while remaining: 23 | this_char = remaining[0] 24 | if this_char in whitespaces: 25 | remaining = remaining[1:] 26 | continue 27 | if this_char in ["(", ")"]: 28 | # the token is this character 29 | if this_char == "(": 30 | token_type = 'opening' 31 | try: 32 | next_char = remaining[1] 33 | except IndexError: 34 | throw_error("syntax", 'Incorrect parenthesis use: "(" at end of program.') 35 | else: 36 | if next_char in [")", '"'] or next_char in whitespaces : 37 | throw_error("syntax", "Incorrect parenthesis use: " + '"' + this_char + next_char + '". Opening parenthesis must be immediately followed by a function.') 38 | if this_char == ")": 39 | token_type = 'closing' 40 | token_value = None 41 | remaining = remaining[1:] 42 | elif this_char == '"': 43 | # the token is everything until the next " 44 | endquote_index = remaining[1:].find('"') 45 | if endquote_index == -1: 46 | throw_error("syntax", "Improper string syntax.") 47 | endquote_index += 1 48 | token_value = remaining[1:endquote_index] 49 | token_type = 'literal' 50 | remaining = remaining[endquote_index+1:] 51 | else: 52 | # the token is everything until the next whitespace or special character 53 | token_value = "" 54 | while this_char not in special and this_char not in whitespaces: 55 | token_value += this_char 56 | remaining = remaining[1:] 57 | if not remaining: 58 | break 59 | this_char = remaining[0] 60 | try: 61 | # anything that can be converted to int is a literal number 62 | token_value = int(token_value) 63 | token_type = "literal" 64 | except ValueError: 65 | # everything else is a symbol 66 | token_type = "symbol" 67 | tokens.append((token_type, token_value)) 68 | return tokens 69 | -------------------------------------------------------------------------------- /environments.py: -------------------------------------------------------------------------------- 1 | # Kimi language interpreter in Python 3 2 | # Anjana Vakil 3 | # http://www.github.com/vakila/kimi 4 | 5 | import operator as op 6 | from errors import * 7 | 8 | class Environment(dict): 9 | 10 | def __init__(self, name = "global", outer = None, variables=(), values=()): 11 | self.name = name 12 | self.outer = outer 13 | self.update(zip(variables, values)) 14 | 15 | def get(self, key): 16 | if key in self: 17 | return self[key] 18 | elif self.outer == None: 19 | throw_error("name", "Undefined variable: " + key) 20 | else: 21 | return self.outer.get(key) 22 | 23 | def set(self, key, value): 24 | if key in self: 25 | throw_error("name", "Variable " + key + " already exists in " + self.name + " environment!") 26 | # else warn if exists in an outer env 27 | else: 28 | self[key] = value 29 | 30 | def standard_env(): 31 | '''Returns the standard environment as a dictionary of (variable: value) pairs 32 | ''' 33 | env = Environment() 34 | 35 | add_booleans(env) 36 | add_nil(env) 37 | add_arithmetic(env) 38 | add_logic(env) 39 | add_equality(env) 40 | add_comparison(env) 41 | add_strings(env) 42 | add_lists(env) 43 | 44 | return env 45 | 46 | 47 | def add_booleans(env): 48 | env['true'] = True 49 | env['false'] = False 50 | return env 51 | 52 | def add_nil(env): 53 | env['nil'] = None 54 | 55 | def verify_arg_type(fn, t): 56 | '''Function wrapper that makes function fn only accept arguments of type t. 57 | Throws an error if non-t arguments are passed to fn, otherwise calls fn on the arguments. 58 | ''' 59 | def verifier(*args): 60 | for arg in args: 61 | assert_or_complain(type(arg) == t, 62 | "TYPE ERROR! Invalid argument type: " + str(arg) + " is type " + type(arg).__name__ + ", expected type " + t.__name__ + ".") 63 | return fn(*args) 64 | return verifier 65 | 66 | def add_arithmetic(env): 67 | add_builtins([ 68 | ('+', op.add), 69 | ('-', op.sub), 70 | ('*', op.mul), 71 | ('/', op.floordiv), 72 | ('%', op.mod)], env, int) 73 | 74 | def add_logic(env): 75 | add_builtins([ 76 | ('&', lambda a,b: a and b), 77 | ('|', lambda a,b: a or b), 78 | ('!', lambda a: not a)], env, bool) 79 | 80 | def add_equality(env): 81 | def equals(a, b): 82 | if type(a) != type(b): 83 | return False 84 | else: 85 | return a == b 86 | env["="] = equals 87 | 88 | def add_comparison(env): 89 | add_builtins([ 90 | ('>', op.gt), 91 | ('<', op.lt), 92 | ('>=', op.ge), 93 | ('<=', op.le)], env, int) 94 | 95 | def add_strings(env): 96 | pass 97 | 98 | def add_lists(env): 99 | def prepend(first, rest): 100 | return (first, rest) 101 | 102 | def first(listy): 103 | if listy == None: 104 | return None 105 | return listy[0] 106 | 107 | def rest(listy): 108 | if listy == None: 109 | return None 110 | return listy[1] 111 | 112 | def make_list(*args): 113 | result = None 114 | for x in reversed(args): 115 | result = (x, result) 116 | return result 117 | 118 | add_builtins([ 119 | ('list', make_list), 120 | ('prepend', prepend), 121 | ('first', first), 122 | ('rest', rest)], env) 123 | 124 | def add_builtins(pairs, env, arg_type=None): 125 | for (symbol, fn) in pairs: 126 | if arg_type: 127 | env[symbol] = verify_arg_type(fn, arg_type) 128 | else: 129 | env[symbol] = fn 130 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from kimi import * 3 | 4 | class TestTokenize(unittest.TestCase): 5 | 6 | def test_numbers(self): 7 | # Valid number literals 8 | self.assertEqual(tokenize("3"), [('literal', 3)]) 9 | self.assertEqual(tokenize("+3"), [('literal', 3)]) 10 | self.assertEqual(tokenize("-4"), [('literal', -4)]) 11 | # Not valid (considered symbols) 12 | self.assertEqual(tokenize("2.5"), [('symbol', '2.5')]) 13 | self.assertEqual(tokenize("-2-4"), [('symbol', '-2-4')]) 14 | 15 | def test_strings(self): 16 | # Valid string literals 17 | self.assertEqual(tokenize('"some string"'), [('literal', 'some string')]) 18 | self.assertEqual(tokenize('"some (string)"'), [('literal', 'some (string)')]) 19 | self.assertEqual(tokenize('''"some 'string'"'''), [('literal', "some 'string'")]) 20 | # Not valid 21 | self.assertEqual(tokenize('"some \"string\""'), [('literal', 'some '), ('symbol', 'string'), ('literal', '')]) 22 | 23 | def test_symbols(self): 24 | # Valid symbols 25 | self.assertEqual(tokenize("x"), [('symbol', 'x')]) 26 | self.assertEqual(tokenize("123abc123"), [('symbol', '123abc123')]) 27 | self.assertEqual(tokenize("--thing--"), [('symbol', '--thing--')]) 28 | # Not valid 29 | self.assertEqual(tokenize("x y"), [('symbol', 'x'), ('symbol', 'y')]) 30 | self.assertEqual(tokenize("z(x)"), [('symbol', 'z'), ('opening', None), ('symbol', 'x'), ('closing', None)]) 31 | 32 | 33 | def test_apply(self): 34 | self.assertEqual(tokenize("(- 1 2)"), [('opening', None), ('symbol', '-'), ('literal', 1), ('literal', 2), ('closing', None)]) 35 | self.assertEqual(tokenize("(define square (lambda x (* x x)))"), 36 | [('opening', None), ('symbol', 'define'), ('symbol', 'square'), 37 | ('opening', None), ('symbol', 'lambda'), ('symbol', 'x'), 38 | ('opening', None), ('symbol', '*'), ('symbol', 'x'), ('symbol', 'x'), 39 | ('closing', None), ('closing', None), ('closing', None)]) 40 | 41 | def test_syntax_errors(self): 42 | self.assertRaises(SystemExit, tokenize, ("( + 1 2 )")) 43 | self.assertRaises(SystemExit, tokenize, ("(((+ 1 2)))")) 44 | self.assertRaises(SystemExit, tokenize, (")+ 1 2(")) 45 | self.assertRaises(SystemExit, tokenize, ("+ 1 2()")) 46 | # self.assertEqual(tokenize("(+ 1 2) (+ 3 4)"), 47 | # [('opening', None), ('symbol', '+'), ('literal', 1), ('literal', 2), ('closing', None), 48 | # ('opening', None), ('symbol', '+'), ('literal', 3), ('literal', 4), ('closing', None)] ) 49 | 50 | class TestParse(unittest.TestCase): 51 | 52 | def test_parse(self): 53 | self.assertEqual(parse(tokenize("(+ 1 2)")), 54 | {'type': 'apply', 55 | 'operator': {'type': 'symbol', 'value': '+'}, 56 | 'arguments': ({'type': 'literal', 'value': 1}, 57 | {'type': 'literal', 'value': 2})}) 58 | self.assertEqual(parse(tokenize("(define square (lambda x (* x x)))")), 59 | {'type': 'apply', 60 | 'operator': {'type': 'symbol', 'value': 'define'}, 61 | 'arguments': ({'type': 'symbol', 'value': 'square'}, 62 | {'type': 'apply', 63 | 'operator': {'type': 'symbol', 'value': 'lambda'}, 64 | 'arguments': ({'type': 'symbol', 'value': 'x'}, 65 | {'type': 'apply', 66 | 'operator': {'type': 'symbol', 'value': '*'}, 67 | 'arguments': ({'type': 'symbol', 'value': 'x'}, 68 | {'type': 'symbol', 'value': 'x'})})})}) 69 | 70 | 71 | class TestExecute(unittest.TestCase): 72 | 73 | def test_atoms(self): 74 | self.assertEqual(execute("-10"), -10) 75 | self.assertEqual(execute("true"), True) 76 | self.assertEqual(execute('"string"'), "string") 77 | 78 | def test_nesting(self): 79 | self.assertEqual(execute("(| (& true false) (! true))"), False) 80 | self.assertEqual(execute("(+ (* 2 3) (- 4 2))"), 8) 81 | 82 | @unittest.expectedFailure 83 | def test_bad_program(self): 84 | self.assertRaises(SystemExit, execute, ("(+ (1) (2))")) 85 | self.assertEqual(execute("(+ 1 2) (+ 3 4)"), 7) #or throw error 86 | 87 | class TestBuiltins(unittest.TestCase): 88 | 89 | def test_arithmetic(self): 90 | # Addition 91 | self.assertEqual(execute("(+ 1 2)"), 3) 92 | self.assertEqual(execute("(+ -1 2)"), 1) 93 | # Subtraction 94 | self.assertEqual(execute("(- 2 1)"), 1) 95 | self.assertEqual(execute("(- 1 -2)"), 3) 96 | # Multiplication 97 | self.assertEqual(execute("(* 2 4)"), 8) 98 | self.assertEqual(execute("(* 3 -2)"), -6) 99 | # Floor division 100 | self.assertEqual(execute("(/ 6 2)"), 3) 101 | self.assertEqual(execute("(/ 7 2)"), 3) 102 | self.assertEqual(execute("(/ 1 2)"), 0) 103 | self.assertEqual(execute("(/ 6 -2)"), -3) 104 | self.assertEqual(execute("(/ -3 -2)"), 1) 105 | # Modulo 106 | self.assertEqual(execute("(% 7 2)"), 1) 107 | self.assertEqual(execute("(% 6 -4)"), -2) 108 | self.assertEqual(execute("(% 2 3)"), 2) 109 | 110 | def test_logic(self): 111 | # And 112 | self.assertEqual(execute("(& true true)"), True) 113 | self.assertEqual(execute("(& true false)"), False) 114 | self.assertEqual(execute("(& false true)"), False) 115 | self.assertEqual(execute("(& false false)"), False) 116 | # Or 117 | self.assertEqual(execute("(| true true)"), True) 118 | self.assertEqual(execute("(| true false)"), True) 119 | self.assertEqual(execute("(| false true)"), True) 120 | self.assertEqual(execute("(| false false)"), False) 121 | # Not 122 | self.assertEqual(execute("(! true)"), False) 123 | self.assertEqual(execute("(! false)"), True) 124 | 125 | def test_equality(self): 126 | self.assertEqual(execute("(= 1 1)"), True) 127 | self.assertEqual(execute("(= 1 2)"), False) 128 | self.assertEqual(execute('(= "yes" "yes")'), True) 129 | self.assertEqual(execute('(= "yes" "no")'), False) 130 | self.assertEqual(execute("(= false false)"), True) 131 | self.assertEqual(execute("(= true false)"), False) 132 | 133 | def test_comparison(self): 134 | # Greater than 135 | self.assertEqual(execute("(> 2 1)"), True) 136 | self.assertEqual(execute("(> 2 2)"), False) 137 | self.assertEqual(execute("(> 1 2)"), False) 138 | # Less than 139 | self.assertEqual(execute("(< 2 1)"), False) 140 | self.assertEqual(execute("(< 2 2)"), False) 141 | self.assertEqual(execute("(< 1 2)"), True) 142 | # Greater or equal 143 | self.assertEqual(execute("(>= 2 1)"), True) 144 | self.assertEqual(execute("(>= 2 2)"), True) 145 | self.assertEqual(execute("(>= 1 2)"), False) 146 | # Less or equal 147 | self.assertEqual(execute("(<= 2 1)"), False) 148 | self.assertEqual(execute("(<= 2 2)"), True) 149 | self.assertEqual(execute("(<= 1 2)"), True) 150 | 151 | def test_lists(self): 152 | self.assertEqual(execute("(prepend 1 (prepend 2 nil))"), (1, (2, None))) 153 | self.assertEqual(execute("(list 1 2)"), (1, (2, None))) 154 | self.assertEqual(execute("(first (list 1 2))"), 1) 155 | self.assertEqual(execute("(rest (list 1 2))"), (2, None)) 156 | 157 | 158 | class TestSpecialForms(unittest.TestCase): 159 | 160 | def test_do(self): 161 | self.assertEqual(execute("(do (> 4 3))"), True) 162 | self.assertEqual(execute("(do (+ 1 2) (+ 3 4))"), 7) 163 | 164 | def test_lambda(self): 165 | self.assertTrue(callable(execute("(lambda x (* x x))"))) 166 | self.assertEqual(execute("((lambda x (* x x)) 2)"), 4) 167 | self.assertEqual(execute("((lambda a b (! (& a b))) true false)"), True) 168 | self.assertEqual(execute("((lambda a ((lambda y (- a y)) 3)) 7)"), 4) 169 | # 170 | def test_define(self): 171 | self.assertEqual(execute("(do (define x 1) (+ x x))"), 2) 172 | 173 | def test_if(self): 174 | self.assertEqual(execute("(if true 1 2)"), 1) 175 | self.assertEqual(execute("(if false 1 2)"), 2) 176 | self.assertRaises(SystemExit, execute, "(if 1 2 3)") 177 | 178 | 179 | if __name__ == '__main__': 180 | unittest.main() 181 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kimi 2 | A lispy toy programming language that keeps it minimal, interpreted in Python 3. 3 | 4 | Made by [Anjana Vakil](https://github.com/vakila) at the [Recurse Center](https://www.recurse.com). 5 | 6 | ### Why did I build Kimi? 7 | A few weeks into my batch at the [Recurse Center](https://www.recurse.com), resident [Prabhakar Ragde](https://cs.uwaterloo.ca/~plragde/) gave a talk called "Small, Elegant, Practical: The benefits of a minimal approach", in which he laid out a minimal set of features (see below) for a small, elegant programming language. It was interesting to know that a programming language doesn't in principle need to have a huge set of features, like most of the major languages do. 8 | 9 | I didn't know much about how programming languages are created before coming to RC, and although I had a vague sense of what interpreters/compilers do, I didn't really know what was going on under the hood. So, with some inspiration from Prabhakar's talk, and some encouragement from RC facilitator [John Workman](http://workmajj.com/), I decided to write a little interpreter for such a minimal language myself. 10 | 11 | ### How did I build Kimi? 12 | In crafting the language, I tried to stick as closely as possible to the set of features that a minimal, elegant language should have, as laid out by Prabhakar: 13 | * `lambda` expressions 14 | * Some useful built-in functions (e.g. arithmetic) 15 | * Function application 16 | * Variable definition 17 | * Conditional evaluation 18 | * Lists 19 | 20 | To write the interpreter, I loosely followed two tutorials: 21 | * the [Programming language project](http://eloquentjavascript.net/11_language.html) outlined by another RC resident, [Marijn Haverbeke](http://marijnhaverbeke.nl/), in his excellent and fun book [Eloquent Javascript](http://eloquentjavascript.net). Marijn himself also provided invaluable assistance by pair-programming with me on some components of the interpreter. 22 | * [Peter Norvig]()'s tutorial [(How to Write a (Lisp) Interpreter (in Python))](http://norvig.com/lispy.html). 23 | 24 | 25 | --- 26 | # The Kimi language 27 | 28 | ## An example program 29 | In `samples/sample.kimi`: 30 | ~~~ 31 | (do 32 | (define x 3) 33 | (define y 4) 34 | (+ x y) 35 | ) 36 | ~~~ 37 | Running the program : 38 | ~~~ 39 | $ python3 kimi.py samples/sample.kimi 40 | 7 41 | ~~~ 42 | 43 | 44 | *See the `samples` directory for more examples!* 45 | 46 | 47 | ## Basics 48 | * **Parentheses** are used to signal function calls, just like other lispy languages. Parentheses are not used for grouping, or any other purpose. An opening parenthesis must be immediately followed by a function (i.e. a builtin, the name of a `define`d function, or a `lambda` expression). For example, `(+ 1 2)` is a valid Kimi program; `( + 1 2 )`, `(((+ 1 2)))`, and `(+ (1) (2))` are not. 49 | * **Numbers** are limited to integers (e.g. `1`, `-439`). Kimi assumes that anything that *can* be interpreted as an integer *is* an integer; for example, `2` and `+2` become `2`, and `-2` becomes `-2`. A number containing a decimal point (e.g. `2.5`) will *not* be considered an integer, but a **symbol** (see below). 50 | * **Strings** must be wrapped in double quotes (e.g. `"my string"`). Kimi assumes anything surrounded by double quotes is a string. Escaped double quotes are not supported, but single quotes can be used (e.g. `"my \"quote\" string"` is not a valid string, but `"my 'quote' string"` is). 51 | * **Booleans** are `true` and `false` (based on Python's `True` and `False`). 52 | * Anything in your program that is not one of the above is considered a **symbol**. 53 | 54 | ## Defining names 55 | * Names can be assigned like so: `(define x 5)`. 56 | * Any symbol (see above) is a valid name, as long as it does not already exist in the given environment. For example, `x`, `123abc123`, and `--thing--` are valid names, but `define`, `-`, `nil`, and `first` are not, since they already exist as built-in functions (see below). 57 | * Just because something *can* be used as a name doesn't mean it *should*; for example `2.5` and `-2-4` are valid names (see above), but not very good ones! 58 | 59 | ## Conditionals 60 | * Conditional statements are written in the form `(if )`. 61 | * The first argument (the test) must be an expression that evaluates to a boolean. 62 | * If the test evaluates to `true`, the second argument will be evaluated. If not, the third argument will be evaluated. 63 | * For example: `(if true 1 2) => 1`, `(if false 1 2) => 2`. 64 | 65 | ## Lambda expressions 66 | * Lambda expressions can be used to create anonymous functions. For example, `(lambda x (* x x))` evaluates to a function that takes one (integer) argument and returns its square. 67 | * Lambdas are written in the form `(lambda args... body)`, where `args...` stands for one or more arguments and `body` stands for an expression that will evaluate to a function application. 68 | 69 | ## Lists 70 | * All non-empty lists are built up from `nil`, Kimi's equivalent to Python's `None`. In other words, all lists contain `nil` as the last element. An empty list is represented as simply `nil`. 71 | * Non-empty lists are written as `(list 1 2 3)`. Internally, they are represented as nested tuples of pairs of values, where the innermost tuple contains `nil` as its second value. For example, Kimi interprets `(list 1 2 3)` as `(1, (2, (3, nil)))`. 72 | * `prepend` adds an argument to the front of a list, and `list` is essentially a shorthand for multiple `prepend` calls: `(list 1) = (prepend 1 nil) => (1, nil)`, `(list 1 2) = (prepend 1 (prepend 2 nil)) => (1, (2, nil))` 73 | * `first` returns the first item in the list: `(first (list 1 2)) => 1` 74 | * `rest` allow you to access the remainder of the list, i.e. the second item of the tuple: `(rest (list 1 2)) => (2, nil)` 75 | 76 | ## Using `do` 77 | * To imperatively execute several commands one after the other, wrap them in `(do ...)`. Kimi will evaluate each expression in turn, and return the result of the last expression evaluated. For example, the following programs both give `7`: 78 | 79 | ~~~ 80 | (do (define x 3) (define y 4) (+ x y)) 81 | ~~~ 82 | 83 | ~~~ 84 | (do (+ 1 2) (+ 3 4)) 85 | ~~~ 86 | * Each `do` block has its own scope; names defined in one `do` block are not accessible from parent or sibling blocks. For example, the following programs will throw errors when trying to access `x`: 87 | 88 | ~~~ 89 | (do 90 | (do (define x 3)) 91 | (+ 1 x) 92 | ) 93 | ~~~ 94 | 95 | ~~~ 96 | (do 97 | (do (define x 3)) 98 | (do (define y 4) (+ x y)) 99 | ) 100 | ~~~ 101 | * Kimi does not know how to imperatively evaluate multiple expressions if they are not wrapped in a `do` block. In this case, Kimi will evaluate the first command it finds and ignore the rest. For example, we saw above that this program evaluates to `7`: 102 | 103 | ~~~ 104 | (do (+ 1 2) (+ 3 4)) 105 | ~~~ 106 | 107 | But this program evaluates to `3`: 108 | 109 | ~~~ 110 | (+ 1 2) (+ 3 4) 111 | ~~~ 112 | 113 | ## Built-in functions 114 | * Arithmetic: 115 | * `+` (addition): `(+ 1 2) => 3` 116 | * `-` (subtraction): `(- 2 1) => 1` 117 | * `*` (multiplication): `(* 2 4) => 8` 118 | * `/` (floor division, as we have only integers): `(/ 6 2) => 3`, `(/ 7 2) => 3` 119 | * `%` (modulo): `(% 7 2) => 1` 120 | * *These functions take only integer arguments* 121 | * Logic: 122 | * `!` (not): `(! true) => False`, `(! false) => True` 123 | * `&` (and): `(& true true) => True`, `(& true false) => False` 124 | * `|` (inclusive or): `(| true false) => True`, `(| false false) => False` 125 | * *These functions take only boolean arguments* 126 | * Equality: 127 | * `=`: `(= 1 1) => True`, `(= "yes" "yes") => True`, `(= true false) = False` 128 | * *This function takes integer, string, or boolean arguments; arguments must be of the same type* 129 | * Test for inequality using a combination of `!` and `=`, e.g. `(! (= 1 2)) => True` 130 | * Comparison: 131 | * `>` (greater than): `(> 2 1) => True` 132 | * `<` (less than): `(< 1 2) = True` 133 | * `>=` (greater than or equal to): `(>= 2 2) => True` 134 | * `<=` (less than or equal to): `(<= 3 2) = False` 135 | * *These functions take only integer arguments* 136 | 137 | --- 138 | # Using Kimi 139 | 140 | ## Running Kimi code 141 | You have three options for playing with Kimi code: 142 | 143 | 1. Interact with the Kimi interpreter (REPL): 144 | 145 | $ kimi 146 | Welcome to Kimi! 147 | See the README (https://github.com/vakila/kimi) for information about Kimi. 148 | To exit the interpreter, type "exit" or "quit" or "q". 149 | kimi> ... 150 | 151 | 2. Run a program from a `.kimi` file: 152 | 153 | $ kimi my_program.kimi 154 | ... 155 | 156 | 3. Type a program as a string on the command line (only recommended for simple programs): 157 | 158 | $ kimi "(+ 1 2)" 159 | 3 160 | 161 | Note: to run the command `kimi`, you'll need to add the path to the `kimi/` directory to your `PATH`, e.g. add these lines to `~/.profile`: 162 | 163 | PATH="/path/to/kimi:${PATH}" 164 | export PATH 165 | 166 | But if you don't add `kimi` to your `PATH`, you can still run the commands above one of two ways: 167 | 168 | $ ./kimi 169 | $ python3 kimi 170 | 171 | 172 | ## Running tests 173 | Using unittest (recommended): 174 | 175 | $ python3 tests.py 176 | 177 | Using doctest (deprecated): 178 | 179 | $ python3 -m doctest -v kimi.py 180 | 181 | --- 182 | --------------------------------------------------------------------------------