Documentation and report.
This commit is contained in:
@@ -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.
|
||||||
@@ -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__())
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user