Documentation and report.

This commit is contained in:
etc404
2026-04-27 01:44:38 -06:00
parent b04f9945ea
commit c65b29485b
6 changed files with 102 additions and 39 deletions
+53
View File
@@ -1,2 +1,55 @@
# lisp-interpreter # 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.
-21
View File
@@ -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__())
+18 -17
View File
@@ -1,20 +1,11 @@
import math import math
import string
from functools import reduce from functools import reduce
def prettyprint(expression) -> str: ### LISP.PY
if type(expression) is list: ### Autumn Wolf
retvalue = "(" ### 04/26/26
for x in expression: ### CSE3024
retvalue += prettyprint(x) + " " # This file (and contained class) handles lisp interpreter details, environment management, and expression evaluation.
return retvalue.rstrip() + ")"
elif type(expression) is bool:
return "T" if expression else "NIL"
elif str():
return expression.upper()
else:
return str(expression)
class Function: class Function:
def __init__(self, parameters, expression): def __init__(self, parameters, expression):
@@ -28,11 +19,15 @@ class Lisp:
else: else:
self.env = env self.env = env
# Recursively evaluate an expression
def evaluate(self, expression): def evaluate(self, expression):
# Handle literals
if type(expression) is not list: if type(expression) is not list:
if expression in self.env.keys(): if expression in self.env.keys():
return self.env[expression] return self.env[expression]
return expression return expression
# Handling of some special operations
operation = expression[0] operation = expression[0]
if type(operation) is list: if type(operation) is list:
operation = self.evaluate(operation) operation = self.evaluate(operation)
@@ -41,7 +36,7 @@ class Lisp:
if operation == "QUOTE": if operation == "QUOTE":
return expression[1:][0] return expression[1:][0]
if operation != "DEFUN": 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: else:
arguments = expression[1:] arguments = expression[1:]
match operation: match operation:
@@ -53,6 +48,7 @@ class Lisp:
return reduce(lambda x,y: x*y, arguments) return reduce(lambda x,y: x*y, arguments)
case "/": case "/":
return reduce(lambda x,y: x/y, arguments) return reduce(lambda x,y: x/y, arguments)
case "CAR": case "CAR":
if len(arguments) != 1: if len(arguments) != 1:
raise ValueError("Expected 1 argument") raise ValueError("Expected 1 argument")
@@ -68,6 +64,7 @@ class Lisp:
return [arguments[0]]+arguments[1] return [arguments[0]]+arguments[1]
else: else:
return [arguments[0]]+arguments[1] return [arguments[0]]+arguments[1]
case "SQRT": case "SQRT":
if len(arguments) != 1: if len(arguments) != 1:
raise ValueError("Expected 1 argument") raise ValueError("Expected 1 argument")
@@ -76,6 +73,7 @@ class Lisp:
if len(arguments) != 2: if len(arguments) != 2:
raise ValueError("Expected 2 arguments") raise ValueError("Expected 2 arguments")
return pow(arguments[0], arguments[1]) return pow(arguments[0], arguments[1])
case "IF": case "IF":
if len(arguments) != 3: if len(arguments) != 3:
raise ValueError("Expected 3 arguments") raise ValueError("Expected 3 arguments")
@@ -111,9 +109,11 @@ class Lisp:
if len(arguments) != 1: if len(arguments) != 1:
raise ValueError("Expected 1 argument") raise ValueError("Expected 1 argument")
return not arguments[0] return not arguments[0]
case "QUIT": case "QUIT":
print(">> bye") print(">> bye")
raise SystemExit raise SystemExit
case "DEFINE": case "DEFINE":
if len(arguments) != 2: if len(arguments) != 2:
raise ValueError("Expected 2 arguments") raise ValueError("Expected 2 arguments")
@@ -132,6 +132,7 @@ class Lisp:
else: else:
raise ValueError("Undefined variable: " + str(arguments[0])) raise ValueError("Undefined variable: " + str(arguments[0]))
return arguments[0] return arguments[0]
case "MAPCAR": case "MAPCAR":
if len(arguments) != 3: if len(arguments) != 3:
raise ValueError("Expected 3 arguments") raise ValueError("Expected 3 arguments")
@@ -140,14 +141,14 @@ class Lisp:
result.append(self.evaluate([arguments[0], arguments[1][i], arguments[2][i]])) result.append(self.evaluate([arguments[0], arguments[1][i], arguments[2][i]]))
return result return result
case _: case _:
if operation in self.env.keys(): if operation in self.env.keys(): # Check for user-defined functions
if type(self.env[operation]) == Function: if type(self.env[operation]) == Function:
func = self.env[operation] func = self.env[operation]
functionrunner = Lisp(self.env) functionrunner = Lisp(self.env)
if len(arguments) != len(func.parameters): if len(arguments) != len(func.parameters):
raise ValueError("Expected " + str(len(func.parameters)) + " arguments") raise ValueError("Expected " + str(len(func.parameters)) + " arguments")
for i in range(0, len(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) return functionrunner.evaluate(self.env[operation].expression)
else: else:
raise TypeError("This value is not a valid function: " + operation) raise TypeError("This value is not a valid function: " + operation)
+24 -1
View File
@@ -1,7 +1,26 @@
import re import re
import lisp 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: class Reader:
tokens: list[str] tokens: list[str]
@@ -11,6 +30,7 @@ class Reader:
self.interpreter = lisp.Lisp() self.interpreter = lisp.Lisp()
self.outputfile = outputfile self.outputfile = outputfile
# Run through tokens, passing them to the interpreter
def run(self): def run(self):
if len(self.tokens) == 0: if len(self.tokens) == 0:
return return
@@ -26,6 +46,7 @@ class Reader:
return return
self.flush() self.flush()
# Capture or discard tokens based on Lisp syntax
def tokenize(self, expression: str): def tokenize(self, expression: str):
self.tokens += re.findall(r"""(?:;.*|[\s,]*)([()']|"(?:\\.|[^\\"])*"?|[^\s()'",;]*)""", expression) self.tokens += re.findall(r"""(?:;.*|[\s,]*)([()']|"(?:\\.|[^\\"])*"?|[^\s()'",;]*)""", expression)
@@ -42,6 +63,7 @@ class Reader:
self.tokens = self.tokens[1:] self.tokens = self.tokens[1:]
return token return token
# Parse an expression into a nested list of values for interpreter consumption
def read_expression(self): def read_expression(self):
if len(self.tokens) == 0: if len(self.tokens) == 0:
return None return None
@@ -67,6 +89,7 @@ class Reader:
else: else:
return self.read_atom() return self.read_atom()
# Smart value conversions for some Lisp-specific syntax, such as T and NIL.
def read_atom(self): def read_atom(self):
token = self.consume() token = self.consume()
if token.upper() == "T": if token.upper() == "T":
+7
View File
@@ -1,6 +1,13 @@
import reader import reader
import sys 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(): 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" 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.") "Enter (quit) to exit the program. Supported functionality includes mathematical operations, conditional statements, variables, user-defined functions, logical operations, and list manipulation.")
BIN
View File
Binary file not shown.