Initial commit - fix problems & refactor code

This commit is contained in:
2025-12-22 19:10:46 +03:00
commit ff36917cb5
39 changed files with 1499 additions and 0 deletions
View File
+54
View File
@@ -0,0 +1,54 @@
from __future__ import annotations
from typing import Any, Callable, Optional
from string import whitespace
from core.primitives.symbol import symbolKey, isSymbol
from core.printer import Representation
from core.primitives.list import NIL
class Environment:
"""Environment for variable bindings with lexical scoping."""
frame: dict[str, Any]
parent: Environment | None
def __init__(self, parent: Environment | None) -> None:
self.frame: dict[str, Any] = {}
self.parent: Environment | None = parent
def add(self, symbol_key: Any, value: Any) -> None:
"""Add a new binding to the current environment frame."""
self.frame[symbolKey(symbol_key)] = value
def set(self, symbol_key: Any, value: Any) -> None:
"""Set a binding in the environment chain."""
key: str = symbolKey(symbol_key)
currentEnvironment: Environment = self
while currentEnvironment is not None:
if key in currentEnvironment.frame:
currentEnvironment.frame[key] = value
return
assert currentEnvironment is Environment
currentEnvironment = currentEnvironment.parent
raise SyntaxError(f"no such symbol: {key}")
def get(self, symbol_key: Any) -> Any:
"""Get a binding from the environment chain."""
key: str = symbolKey(symbol_key)
currentEnvironment: Environment | None = self
while currentEnvironment is not None:
if key in currentEnvironment.frame:
return currentEnvironment.frame[key]
currentEnvironment = currentEnvironment.parent
return symbol_key
EVAL_NIL: Callable[[], Any] = lambda: NIL
ENV: Environment = Environment(None)
MEANINGFUL: str = "\"();'"
IGNORE: str = whitespace + ","
RepresentateAST: Callable[[Any], str] = lambda value: (
value if type(value) is str and not isSymbol(value) else Representation(value)
)
TCO: bool = True
@@ -0,0 +1,24 @@
from core.tokens import BinaryOperator
from core.exceptions import runtimeError
from core.environment import RepresentateAST
def evalBinaryOperator(op: BinaryOperator, a, b):
try:
match op:
case BinaryOperator.ADD:
return a + b
case BinaryOperator.SUB:
return a - b
case BinaryOperator.MUL:
return a * b
case BinaryOperator.DIV:
if type(a) is type(b) is int:
return a // b
return a / b
case BinaryOperator.MOD:
return a % b
case BinaryOperator.STRCONCAT:
return RepresentateAST(a) + RepresentateAST(b)
except Exception as e:
raise runtimeError(f'cannot apply {repr(op)} to {repr(a)} and {repr(b)}') from e
@@ -0,0 +1,14 @@
from core.tokens import BinaryPredicate
from core.exceptions import runtimeError
def evalBinaryPredicate(op: BinaryPredicate, a, b):
try:
match op:
case BinaryPredicate.LT:
return a < b
case BinaryPredicate.GT:
return a > b
case BinaryPredicate.EQ:
return a == b
except Exception as e:
raise runtimeError(f'cannot apply {repr(op)} to {repr(a)} and {repr(b)}') from e
+53
View File
@@ -0,0 +1,53 @@
from core.primitives.list import NIL, cons, car, cdr
from core.primitives.symbol import isSymbol, symbolName
from core.exceptions import syntaxError
from core.tokens import Lambda
from core.tail_recursion import tailRecursion
# from core.evaluator.naively_evaluate import naivelyEvaluate
from core.evaluator.utils.getFirstN import getFirstN
from core.environment import TCO
def evalCall(head, tail, environment, environmentParent, strict):
from core.evaluator.naively_evaluate import naivelyEvaluate
args = head.args
if args == NIL and tail != NIL:
raise syntaxError(f'more than zero args for {repr(head)}')
while args != NIL:
firstArg = car(args)
if not isSymbol(firstArg):
raise syntaxError(f'cannot take non-symbol as argument: {repr(firstArg)}')
if symbolName(firstArg) == '.':
firstArg = getFirstN(args, 2)[1]
arr = []
while tail != NIL:
arr.append(naivelyEvaluate(car(tail), environmentParent, True))
tail = cdr(tail)
arg = NIL
for elem in reversed(arr):
arg = cons(elem, arg)
args = NIL
else:
if tail == NIL:
break
arg = naivelyEvaluate(car(tail), environmentParent, True)
args = cdr(args)
tail = cdr(tail)
environment.add(firstArg, arg)
if tail != NIL:
raise syntaxError(f'extra args {repr(tail)}\nfor not variable lambda {repr(head)}')
if args != NIL:
return Lambda(args, head.body, environment)
if not TCO:
return naivelyEvaluate(head.body, environment, True)
ret = tailRecursion(naivelyEvaluate, head.body, environment, False)
return tailRecursion.eval(ret) if strict else ret
+49
View File
@@ -0,0 +1,49 @@
from core.primitives.list import cons, car, cdr, NIL
from core.primitives.symbol import symbolName, symbolKey
from core.tokens import Macro
from core.exceptions import syntaxError
from core.tail_recursion import tailRecursion
from core.macro_expand import macroExpand
# from core.evaluator.naively_evaluate import naivelyEvaluate
from core.evaluator.utils.getFirstN import getFirstN
from core.environment import TCO
def evalMacro(head, tail, environment, strict):
from core.evaluator.naively_evaluate import naivelyEvaluate
d = dict()
args = head.args
if args == NIL and tail != NIL:
raise syntaxError(f'more than zero args for {repr(head)}')
while args != NIL:
firstArg = car(args)
if symbolName(firstArg) == '.':
firstArg = getFirstN(args, 2)[1]
arg = tail
args = NIL
tail = NIL
else:
if tail == NIL:
break
arg = car(tail)
args = cdr(args)
tail = cdr(tail)
d[symbolKey(firstArg)] = arg
if tail != NIL:
raise syntaxError(f'extra args {repr(tail)}\nfor not variable macro {repr(head)}')
expanded = macroExpand(head.body, d)
if args != NIL:
return Macro(args, expanded)
if not TCO:
return naivelyEvaluate(expanded, environment, True)
ret = tailRecursion(naivelyEvaluate, expanded, environment, False)
return tailRecursion.eval(ret) if strict else ret
+137
View File
@@ -0,0 +1,137 @@
from core.tokens import (
SpecialForm,
Lambda,
Macro,
Pure
)
from core.primitives.list import (
cons,
car,
cdr,
isList
)
from core.primitives.symbol import symbol
from core.environment import NIL, RepresentateAST
from core.primitives.symbol import isSymbol
from core.exceptions import (
syntaxError,
runtimeError
)
# from core.evaluator.naively_evaluate import naivelyEvaluate
from core.evaluator.utils.getFirstN import getFirstN
from core.parser.parse_expression import parseExpression
def evalSpecialForm(head, tail, env, strict):
from core.evaluator.naively_evaluate import naivelyEvaluate
match head:
case SpecialForm.QUOTE:
return getFirstN(tail, 1)[0]
case SpecialForm.EVAL:
return naivelyEvaluate(naivelyEvaluate(getFirstN(tail, 1)[0], env, True), env, strict)
case SpecialForm.TYPEOF:
a = naivelyEvaluate(getFirstN(tail, 1)[0], env, True)
return 'List' if isList(a) else 'Symbol' if isSymbol(a) else str(a.__class__.__name__)
case SpecialForm.CONS:
a, b = getFirstN(tail, 2)
evaluated_b = naivelyEvaluate(b, env, True)
if not isList(evaluated_b):
raise syntaxError('cannot add element to non-list')
return cons(naivelyEvaluate(a, env, True), evaluated_b)
case SpecialForm.CAR:
a = naivelyEvaluate(getFirstN(tail, 1)[0], env, True)
if not isList(a):
return a # config moment
if a == NIL:
raise syntaxError("NIL's head???") # config moment
return car(a)
case SpecialForm.CDR:
a = naivelyEvaluate(getFirstN(tail, 1)[0], env, True)
if not isList(a):
return NIL # config moment
if a == NIL:
raise syntaxError("NIL's tail???") # config moment
return cdr(a)
case SpecialForm.IF:
a, b, c = getFirstN(tail, 3) # a ? b : c
evaled_a = naivelyEvaluate(a, env, True)
if type(evaled_a) is bool:
return naivelyEvaluate(b, env, strict) if evaled_a else naivelyEvaluate(c, env, strict)
raise syntaxError(f'not boolean {a} in (++ {repr(head)} {repr(tail)})') # TODO
case SpecialForm.DO:
if tail == NIL:
return NIL
ev = NIL
while cdr(tail) != NIL:
ev = naivelyEvaluate(car(tail), env, True)
tail = cdr(tail)
return naivelyEvaluate(car(tail), env, strict)
case SpecialForm.PRINT:
print(RepresentateAST(naivelyEvaluate(getFirstN(tail, 1)[0], env, True)), end='')
return NIL
case SpecialForm.FLUSH:
print(end='', flush=True)
return NIL
case SpecialForm.READ:
getFirstN(tail, 0)
s = input('\nreading: ')
return parseExpression(s)[0]
case SpecialForm.SYMBOL:
a = naivelyEvaluate(getFirstN(tail, 1)[0], env, True)
if not isinstance(a, str):
raise syntaxError('cannot make symbol from non-string')
return symbol(a)
case SpecialForm.DEF:
a, b = getFirstN(tail, 2)
if isSymbol(a):
env.add(a, naivelyEvaluate(b, env, True))
return NIL
raise syntaxError('cannot define non-symbol')
case SpecialForm.SET:
a, b = getFirstN(tail, 2)
if isSymbol(a):
env.set(a, naivelyEvaluate(b, env, True))
return NIL
raise syntaxError('cannot set non-Symbol')
case SpecialForm.LAMBDA:
a, b = getFirstN(tail, 2)
if isList(a):
return Lambda(a, b, env)
raise syntaxError(f'wrong args form for lambda: {repr(a)}')
# some errors may be here
case SpecialForm.PURE:
a, b = getFirstN(tail, 2)
if isList(a):
return Pure(a, b)
raise syntaxError(f'wrong args form for pure: {repr(a)}')
case SpecialForm.MACRO:
a, b = getFirstN(tail, 2)
if isList(a):
return Macro(a, b)
raise syntaxError(f'wrong args form for macro: {repr(a)}')
case SpecialForm.RAISE:
cause = naivelyEvaluate(getFirstN(tail, 1)[0], env, True)
raise runtimeError(RepresentateAST(cause))
case _:
raise syntaxError
+73
View File
@@ -0,0 +1,73 @@
from core.tokens import (
BinaryOperator,
BinaryPredicate,
SpecialForm,
Lambda,
Pure,
Macro,
)
# debug
import sys
from core.primitives.symbol import (
isSymbol
)
from core.primitives.list import (
cons, car, cdr,
isList,
NIL
)
from core.environment import EVAL_NIL, Environment, RepresentateAST
from core.evaluator.evaluate_binary_operator import evalBinaryOperator
from core.evaluator.evaluate_binary_predicate import evalBinaryPredicate
from core.evaluator.evaluate_special_form import evalSpecialForm
from core.evaluator.evaluate_call import evalCall
from core.evaluator.evaluate_macro import evalMacro
from core.evaluator.utils.getFirstN import getFirstN
from core.exceptions import error, syntaxError, typeError
def naivelyEvaluate(value, localEnvironment, strict):
if isSymbol(value):
return localEnvironment.get(value)
elif isList(value):
if value == NIL:
return EVAL_NIL()
head = naivelyEvaluate(car(value), localEnvironment, True)
tail = cdr(value)
try:
if isinstance(head, (BinaryOperator, BinaryPredicate)):
try:
a, b = getFirstN(tail, 2)
evaledA = naivelyEvaluate(a, localEnvironment, True)
evaledB = naivelyEvaluate(b, localEnvironment, True)
if isinstance(head, BinaryOperator):
return evalBinaryOperator(head, evaledA, evaledB)
return evalBinaryPredicate(head, evaledA, evaledB)
except syntaxError as e:
raise syntaxError(f'wrong args for: {repr(value)}') from e
elif isinstance(head, SpecialForm):
return evalSpecialForm(head, tail, localEnvironment, strict)
elif isinstance(head, (Lambda, Pure)):
if isinstance(head, Lambda):
environment = Environment(head.env)
else:
environment = Environment(parent=localEnvironment)
return evalCall(head, tail, environment, localEnvironment, strict)
elif isinstance(head, Macro):
return evalMacro(head, tail, localEnvironment, strict)
except error as e:
raise syntaxError(f'wrong args for {repr(head)}: {repr(cdr(value))}') from e
raise typeError(f'wrong head form: {RepresentateAST(head)}')
return value
+32
View File
@@ -0,0 +1,32 @@
from typing import Any
from core.primitives.list import cons, car, cdr, NIL
from core.exceptions import syntaxError
from core.printer import Representation
def getFirstN(_list: Any, n: int) -> list[Any]:
"""Get first N elements.
Args:
_list: A list.
n: The expected number of elements.
Returns:
A list of the extracted elements.
Raises:
syntaxError: If the list has wrong number of elements.
"""
result: list[Any] = []
current: Any = _list
while current != NIL and len(result) < n:
result.append(car(current))
current = cdr(current)
if current != NIL:
raise syntaxError(f"extra elems: {Representation(current)}")
if len(result) < n:
raise syntaxError("lack of elems")
return result
+31
View File
@@ -0,0 +1,31 @@
"""Exception classes for Lisp interpreter."""
class error(Exception):
"""Base exception for all Lisp-related errors."""
pass
class syntaxError(error):
"""Raised when there is a syntax error in the Lisp code."""
pass
class runtimeError(error):
"""Raised when there is a runtime error during evaluation."""
pass
class valueError(error):
"""Raised when there is an invalid value."""
pass
class typeError(error):
"""Raised when there is a type error during evaluation."""
pass
+28
View File
@@ -0,0 +1,28 @@
from typing import Any
from core.primitives.symbol import symbolKey, isSymbol
from core.primitives.list import NIL, isList, cons, car, cdr
def macroExpand(body: Any, bindings: dict[str, Any]) -> Any:
"""Expand a macro by substituting bindings into the body.
Args:
body: The macro body to expand.
bindings: Dictionary of symbol-to-value bindings.
Returns:
The expanded body.
"""
if isSymbol(body):
if symbolKey(body) in bindings:
return bindings[symbolKey(body)] # auto quote
return body
if isList(body):
if body == NIL:
return NIL
return cons(
macroExpand(car(body), bindings), macroExpand(cdr(body), bindings)
)
return body
+29
View File
@@ -0,0 +1,29 @@
from core.parser.parse_expression import parseExpression
from core.evaluator.naively_evaluate import naivelyEvaluate
from core.exceptions import error
from core.environment import ENV
from traceback import print_exception
def parseEvaluate(source: str, *, load: bool = False) -> str:
results: list[str] = []
s = source
while s:
try:
expr, s = parseExpression(s)
value = naivelyEvaluate(expr, ENV, True)
results.append(repr(value))
except error as e:
# Unwrap nested causes so that the original error message is shown
cause = e
while cause.__cause__ and isinstance(cause.__cause__, error):
cause = cause.__cause__
print(cause)
break
except KeyboardInterrupt:
return "interrupted\n"
except Exception as exc:
print_exception(exc)
break
return ' '.join(results)
View File
+10
View File
@@ -0,0 +1,10 @@
from core.environment import IGNORE
def cleanString(s: str, ignore: str = IGNORE) -> str:
s = s.lstrip(ignore)
while s and s[0] == ';':
line_end: int = s.find('\n')
if line_end < 0:
return ''
s = s[line_end:].lstrip(ignore)
return s
+9
View File
@@ -0,0 +1,9 @@
from core.environment import IGNORE, MEANINGFUL
import re
def findPrefix(s: str, separators: str = IGNORE + MEANINGFUL) -> str:
pattern = rf'^[^{separators}]*'
match = re.match(pattern, s)
if match:
return match.group(0)
return ''
+52
View File
@@ -0,0 +1,52 @@
from core.parser.clean_string import cleanString
from core.parser.parse_token import parseToken
from core.parser.find_prefix import findPrefix
from core.primitives.list import (
cons,
NIL
)
from core.exceptions import (
syntaxError,
runtimeError
)
from core.tokens import SpecialForm
def parseExpression(s: str) -> tuple:
s = cleanString(s)
if not s:
return NIL, s
match s[0]:
case '"': # string
matching = s.find('"', 1)
if matching < 0:
raise syntaxError(f'the closing " is missing: {s}')
return s[1:matching], s[matching + 1:]
case '(': # list
cache = s # saving initial view for Error illustration
s = s[1:]
arr: list = []
while s and s[0] != ')':
elem, s = parseExpression(s)
arr.append(elem)
s = cleanString(s)
if not s:
raise syntaxError(f'the closing ) is missing: {cache}')
result = NIL
for elem in reversed(arr):
result = cons(elem, result)
return result, s[1:]
case ')':
raise syntaxError(f'extra closing bracket: {s}') # TODO debug
case "'":
try:
v, rem = parseExpression(s[1:])
except Exception as e:
raise runtimeError(f'invalid quote: {s}') from e
if v is None:
raise syntaxError(f'quote of nothing: {s}')
return cons(SpecialForm.QUOTE, cons(v, NIL)), rem
case _: # maybe we can find token
first = findPrefix(s)
return parseToken(first) if first else NIL, s[len(first):]
+22
View File
@@ -0,0 +1,22 @@
from core.tokens import (
SpecialForm,
BinaryOperator,
BinaryPredicate
)
from core.primitives.symbol import symbol
def parseToken(token: str):
for en in [SpecialForm, BinaryOperator, BinaryPredicate]:
if token in en:
return en(token)
if token in ['true', 'false']:
return token == 'true'
try:
return int(token)
except ValueError:
if token == 'inf':
return symbol(token)
try:
return float(token)
except ValueError:
return symbol(token)
View File
+26
View File
@@ -0,0 +1,26 @@
from typing import Any
from core.exceptions import syntaxError
# from core.printer import Representation
class LinkedList:
value = None
nxt = None
def __init__(self, value_, nxt_):
self.value = value_
self.nxt = nxt_
def car(lst):
return lst.value
def cdr(lst):
return lst.nxt
def cons(val, lst):
return LinkedList(val, lst)
def isList(val):
return isinstance(val, LinkedList)
NIL = cons(None, None)
+25
View File
@@ -0,0 +1,25 @@
from functools import lru_cache
class Symbol:
_name: str
def __init__(self, name):
self._name = name
@property
def name(self):
return self._name
@lru_cache(maxsize=None)
def symbol(name: str) -> Symbol:
return Symbol(name)
def symbolName(sym: Symbol) -> str:
# return sym.name[1:]
return sym.name
def symbolKey(sym: Symbol) -> str:
return sym.name
def isSymbol(obj) -> bool:
return isinstance(obj, Symbol)
+55
View File
@@ -0,0 +1,55 @@
from typing import Any
from core.primitives.list import cons, car, cdr, isList
from core.primitives.symbol import isSymbol, symbolName
from core.tokens import BaseToken, Lambda, Dambda, Macro, SpecialForm
from core.primitives.list import NIL
def Representation(obj: Any) -> str:
"""Return a showable string representation of an object in Lisp notation.
Args:
obj: The object to represent.
Returns:
A string that could be parsed back by the Lisp parser.
"""
if obj is None:
obj = NIL
if isList(obj):
result_str: str = ""
is_first: bool = True
current_item: Any = obj
while current_item != NIL:
if is_first:
is_first = False
else:
result_str += " "
result_str += Representation(car(current_item))
current_item = cdr(current_item)
return f"({result_str})"
elif isSymbol(obj):
return symbolName(obj) # type: ignore
elif isinstance(obj, str):
return f'"{obj}"'
elif isinstance(obj, BaseToken):
return obj.value
elif isinstance(obj, bool):
return "true" if obj else "false"
elif isinstance(obj, Lambda):
return f"({Representation(SpecialForm.LAMBDA)} {Representation(obj.args)} {Representation(obj.body)})"
elif isinstance(obj, Dambda):
return f"({Representation(SpecialForm.DAMBDA)} {Representation(obj.args)} {Representation(obj.body)})"
elif isinstance(obj, Macro):
return f"({Representation(SpecialForm.MACRO)} {Representation(obj.args)} {Representation(obj.body)})"
return str(obj)
+38
View File
@@ -0,0 +1,38 @@
from core.environment import IGNORE, MEANINGFUL
import re
def processString(source: str) -> str:
"""Remove blank characters and comments from source code.
Args:
source: The source code string.
Returns:
The cleaned source code.
"""
source = source.lstrip(IGNORE)
while source and source[0] == ";":
line_end: int = source.find("\n")
if line_end < 0:
return ""
source = source[line_end:].lstrip(IGNORE)
return source
def findPrefix(source: str, separators: str = IGNORE + MEANINGFUL) -> str:
"""Find a meaningful prefix (token) in the source string.
Args:
source: The source code string.
separators: Characters that mark token boundaries.
Returns:
The prefix token found, or empty string if none.
"""
pattern: str = rf"^[^{separators}]*"
match = re.match(pattern, source)
return match.group(0) if match else ""
+23
View File
@@ -0,0 +1,23 @@
from typing import Callable, Any
class tailRecursion:
"""Tail-call optimization wrapper."""
f: Callable[..., Any]
args: tuple[Any, ...]
kwargs: dict[str, Any]
def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
self.f: Callable[..., Any] = func
self.args: tuple[Any, ...] = args
self.kwargs: dict[str, Any] = kwargs
def __call__(self) -> Any:
return self.f(*self.args, **self.kwargs)
@staticmethod
def eval(val: Any) -> Any:
"""Evaluate tail calls recursively until completion."""
while isinstance(val, tailRecursion):
val = val()
return val
+87
View File
@@ -0,0 +1,87 @@
from __future__ import annotations
from enum import Enum
from typing import Any, TYPE_CHECKING
# from core.environment import Environment
if TYPE_CHECKING:
from core.environment import Environment
class Lambda:
"""Lambda function with arguments, body, and closure environment"""
args: Any
body: Any
env: Environment
def __init__(self, args: Any, body: Any, env: Environment) -> None:
self.args: Any = args
self.body: Any = body
self.env: Environment = env
class Dambda:
"""Dambda function"""
args: Any
body: Any
def __init__(self, args: Any, body: Any) -> None:
self.args: Any = args
self.body: Any = body
class Macro:
"""Macro definition"""
args: Any
body: Any
def __init__(self, args: Any, body: Any) -> None:
self.args: Any = args
self.body: Any = body
class Pure():
"""Pure definition"""
args: Any
body: Any
def __init__(self, args: Any, body: Any) -> None:
self.args: Any = args
self.body: Any = body
class BaseToken(Enum):
"""Base class for enumerations"""
class SpecialForm(BaseToken):
EVAL = "eval"
IF = "if"
DO = "do"
PRINT = "print"
READ = "read"
SYMBOL = "symbol"
DEF = "def"
SET = "set!"
FLUSH = "flush"
PURE = "pure"
LAMBDA = "lambda"
DAMBDA = "dambda"
MACRO = "macro"
RAISE = "raise"
QUOTE = "quote"
CONS = "cons"
CAR = "car"
CDR = "cdr"
TYPEOF = "typeof"
class BinaryOperator(BaseToken):
ADD = "_+_"
SUB = "_-_"
MUL = "_*_"
DIV = "_/_"
MOD = "_%_"
STRCONCAT = "_++_"
class BinaryPredicate(BaseToken):
LT = "_<_"
GT = "_>_"
EQ = "_=_"