diff --git a/README.md b/README.md index a9073e0..ec3306e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,55 @@ # lisp-interpreter +This project is a simple Lisp interpreter written in Python, to the spec of New Mexico Tech's 2026 CSE3024 Project 2 assignment. + +## Running the Interpreter + +You may run the interpreter with the commands `make`, `make run`, or `python repl.py`. To supply an alternative location for the output file, specify it as a command-line argument. By default, the output file is `output.txt`. + +## Completeness + +The interpreter's support and completeness is outlined below: + +| Lisp Construct | Status | +|------------------------|--------| +| Variable reference | Done | +| Constant literal | Done | +| Quotation | Done | +| Conditional | Done | +| Variable definition | Done | +| Function call | Done | +| Assignment | Done | +| Function definition | Done | +| Arithmetic operators | Done | +| Integer type | Done | +| `car` and `cdr` | Done | +| `cons` support | Done | +| sqrt, pow | Done | +| Comparison expressions | Done | +| Logical operations | Done | +| `mapcar` function | Done | +| Lambda support | Not | + +## Implementation Notes + +### Data Types + +Data types were straightforward to implement, as I opted for a Python-native approach to data representation, with the actual back-end values being simple heterogeneous lists of lists, `int`s, `str`s, `float`s, and `bool`s. + +### Mathematical Operators + +All the built-in functions follow a similar principle, sifting through a match case of built-ins before checking for user defined functions. The mathematical operations, as well as the logical expressions, all simply check for correct parameters, then return the result of the native Python operation on the arguments. + +### Environment Management + +Variables and function definitions make use of the `Lisp` class's `env` property, a simple hashmap relating a symbol name to a value, for variables, or for a simple `Function` struct, for functions. When the function is executed, the `Lisp` instance recursively creates a new interpreter instance so as to offer the passed parameters to the expression within the function. + +### List Manipulation + +List manipulation was simple in this implementation, largely due to Python's loose typing and highly dynamic lists. `car`, `cdr`, and `cons` only needed simple list appending and concatenation. + +## References + +> [Make-a-Lisp](https://github.com/kanaka/mal/blob/master/process/guide.md) by `kanaka` on GitHub + +This resource proved to be less applicable than I had originally hoped, and in practice I only benefitted from the birds-eye understanding given by the REPL data-flow diagrams, and a starting point for the tokenization regex. The rest of the project was much more thorough than was useful to me, and the rest was through trial-and-error. \ No newline at end of file diff --git a/atom.py b/atom.py deleted file mode 100644 index ca7127e..0000000 --- a/atom.py +++ /dev/null @@ -1,21 +0,0 @@ -class Atom: - car: Atom | int | str | bool | None - cdr: Atom | None - - def __init__(self, car: Atom|int|str|bool|None, cdr: Atom|None): - self.car = car - self.cdr = cdr - - def setval(self, atom: Atom | int | str | bool | None): - self.car = atom - - def append(self, atom: Atom): - self.cdr = atom - - def __str__(self): - if type(self.car) is Atom: - return "(" + self.car.__str__() + ("" if self.cdr is None else (" " + self.cdr.__str__())) + ")" - elif self.car is None: - return "()" - else: - return self.car + ("" if self.cdr is None else self.cdr.__str__()) \ No newline at end of file diff --git a/lisp.py b/lisp.py index 3ea17ae..ef1678d 100644 --- a/lisp.py +++ b/lisp.py @@ -1,20 +1,11 @@ import math -import string from functools import reduce -def prettyprint(expression) -> str: - if type(expression) is list: - retvalue = "(" - for x in expression: - retvalue += prettyprint(x) + " " - return retvalue.rstrip() + ")" - elif type(expression) is bool: - return "T" if expression else "NIL" - elif str(): - return expression.upper() - else: - return str(expression) - +### LISP.PY +### Autumn Wolf +### 04/26/26 +### CSE3024 +# This file (and contained class) handles lisp interpreter details, environment management, and expression evaluation. class Function: def __init__(self, parameters, expression): @@ -28,11 +19,15 @@ class Lisp: else: self.env = env + # Recursively evaluate an expression def evaluate(self, expression): + # Handle literals if type(expression) is not list: if expression in self.env.keys(): return self.env[expression] return expression + + # Handling of some special operations operation = expression[0] if type(operation) is list: operation = self.evaluate(operation) @@ -41,7 +36,7 @@ class Lisp: if operation == "QUOTE": return expression[1:][0] if operation != "DEFUN": - arguments = [self.evaluate(x) for x in expression[1:]] + arguments = [self.evaluate(x) for x in expression[1:]] # Don't evaluate for defun or quote! else: arguments = expression[1:] match operation: @@ -53,6 +48,7 @@ class Lisp: return reduce(lambda x,y: x*y, arguments) case "/": return reduce(lambda x,y: x/y, arguments) + case "CAR": if len(arguments) != 1: raise ValueError("Expected 1 argument") @@ -68,6 +64,7 @@ class Lisp: return [arguments[0]]+arguments[1] else: return [arguments[0]]+arguments[1] + case "SQRT": if len(arguments) != 1: raise ValueError("Expected 1 argument") @@ -76,6 +73,7 @@ class Lisp: if len(arguments) != 2: raise ValueError("Expected 2 arguments") return pow(arguments[0], arguments[1]) + case "IF": if len(arguments) != 3: raise ValueError("Expected 3 arguments") @@ -111,9 +109,11 @@ class Lisp: if len(arguments) != 1: raise ValueError("Expected 1 argument") return not arguments[0] + case "QUIT": print(">> bye") raise SystemExit + case "DEFINE": if len(arguments) != 2: raise ValueError("Expected 2 arguments") @@ -132,6 +132,7 @@ class Lisp: else: raise ValueError("Undefined variable: " + str(arguments[0])) return arguments[0] + case "MAPCAR": if len(arguments) != 3: raise ValueError("Expected 3 arguments") @@ -140,14 +141,14 @@ class Lisp: result.append(self.evaluate([arguments[0], arguments[1][i], arguments[2][i]])) return result case _: - if operation in self.env.keys(): + if operation in self.env.keys(): # Check for user-defined functions if type(self.env[operation]) == Function: func = self.env[operation] functionrunner = Lisp(self.env) if len(arguments) != len(func.parameters): raise ValueError("Expected " + str(len(func.parameters)) + " arguments") for i in range(0, len(arguments)): - functionrunner.env[func.parameters[i]] = arguments[i] + functionrunner.env[func.parameters[i]] = arguments[i] # Populate sub-interpreter environment return functionrunner.evaluate(self.env[operation].expression) else: raise TypeError("This value is not a valid function: " + operation) diff --git a/reader.py b/reader.py index 9c7da0f..130dedc 100644 --- a/reader.py +++ b/reader.py @@ -1,7 +1,26 @@ import re import lisp -from lisp import prettyprint +### READER.PY +### Autumn Wolf +### 04/26/26 +### CSE3024 +# This file contains the main Reader class, which is responsible for tokenizing, parsing, and handling user input, +# passing it to the lisp interpreter for evaluation, then returning the results to the user. + +# Recursively print data for human readability +def prettyprint(expression) -> str: + if type(expression) is list: + retvalue = "(" + for x in expression: + retvalue += prettyprint(x) + " " + return retvalue.rstrip() + ")" + elif type(expression) is bool: + return "T" if expression else "NIL" + elif str(): + return expression.upper() + else: + return str(expression) class Reader: tokens: list[str] @@ -11,6 +30,7 @@ class Reader: self.interpreter = lisp.Lisp() self.outputfile = outputfile + # Run through tokens, passing them to the interpreter def run(self): if len(self.tokens) == 0: return @@ -26,6 +46,7 @@ class Reader: return self.flush() + # Capture or discard tokens based on Lisp syntax def tokenize(self, expression: str): self.tokens += re.findall(r"""(?:;.*|[\s,]*)([()']|"(?:\\.|[^\\"])*"?|[^\s()'",;]*)""", expression) @@ -42,6 +63,7 @@ class Reader: self.tokens = self.tokens[1:] return token + # Parse an expression into a nested list of values for interpreter consumption def read_expression(self): if len(self.tokens) == 0: return None @@ -67,6 +89,7 @@ class Reader: else: return self.read_atom() + # Smart value conversions for some Lisp-specific syntax, such as T and NIL. def read_atom(self): token = self.consume() if token.upper() == "T": diff --git a/repl.py b/repl.py index 18afe2a..0d8ff47 100644 --- a/repl.py +++ b/repl.py @@ -1,6 +1,13 @@ import reader import sys +### REPL.PY +### Autumn Wolf +### 04/26/26 +### CSE3024 +# This file is the main driver code for the interpreter, acting as an entrypoint for the program and handling +# terminal-side interaction and argument handling. + def main(): print("Welcome to the lisp interpreter! REPL expression results will be stored in an output file, output.txt by default. This can be overridden by passing an argument to the program.\n" "Enter (quit) to exit the program. Supported functionality includes mathematical operations, conditional statements, variables, user-defined functions, logical operations, and list manipulation.") diff --git a/wolf_project2.pdf b/wolf_project2.pdf new file mode 100644 index 0000000..6d4d1dc Binary files /dev/null and b/wolf_project2.pdf differ