from structs import * from exceptions import * from lexer import lex from parser import parse from structs import T from pathlib import Path from glob import glob from collections import namedtuple from dataclasses import dataclass import subprocess import shlex import random import sys @dataclass class Arg: name: str type_: T optional: bool lazy: bool class Function: def __init__(self, name, params, body, args=None, many=None): self.name = name self.params = params self.body = body self.args = args self.many = many def arity_check(self, symbol, params): min_arity = len([a for a in self.args if not a.optional]) max_arity = -1 if self.many is not None else len(self.args) if len(params) < min_arity or (max_arity >= 0 and len(params) > max_arity): if max_arity < 0: fmt = f"{min_arity}+" elif min_arity != max_arity: fmt = f"{min_arity}-{max_arity}" else: fmt = f"{min_arity}" raise InterpretPanic(symbol, f"expected [{fmt}] arguments, received {len(params)}") return True def evaluate_args(self, symbol, params, env): self.arity_check(symbol, params) ret = [] for idx, param in enumerate(params): if idx < len(self.args): arg = self.args[idx] else: arg = self.many if arg.lazy: ret.append(param) continue ev = evaluate(param, env) if not isinstance(ev, arg.type_): exp = f":{arg.type_.__name__.lower()}" rec = f":{ev.type_.__name__.lower()}" raise InterpretPanic(symbol, f"received {rec}, expected {exp}", ev) ret.append(ev) return ret def call(self, expr, env): pass class Builtin(Function): def __init__(self, callable_, args=None, many=None): super().__init__("", None, callable_, args, many) def call(self, expr, env): self.arity_check(expr.args[0], expr.args[1:]) evaluated_args = self.evaluate_args(expr.args[0], expr.args[1:], env) return self.body(expr.args[0], evaluated_args, env) class UserFunction(Function): def __init__(self, name, params, body): # TODO this doesn't do type checking, or optional, or lazy args = [Arg("arg", T.Any, False, False)] * len(params) super().__init__(name, params, body, args) def call(self, expr, env): self.arity_check(expr.args[0], expr.args[1:]) evaluated_args = self.evaluate_args(expr.args[0], expr.args[1:], env) this_env = Environment(env) for idx, param in enumerate(self.params): this_env.register(param.name, evaluated_args[idx]) return interpret(self.body, this_env) class Environment: def __init__(self, parent=None): self.parent = parent self.environment = {} def register(self, key, value): self.environment[key] = value def reregister(self, key, value): if not self.contains(key): raise NebPanic(f"undefined symbol: '{key}") if key in self.environment: self.register(key, value) else: self.parent.reregister(key, value) def contains(self, key): if key in self.environment: return True elif self.parent is not None: return self.parent.contains(key) else: return False def get(self, key): if not self.contains(key): raise NebPanic(f"undefined symbol: '{key}") if key in self.environment: return self.environment[key] else: return self.parent.get(key) def __str__(self): out = "" for k, v in self.environment.items(): out += f"{k}: {v}, " return out GLOBALS = Environment() def interpret(exprs, env=GLOBALS): ret = None for expr in exprs: ret = evaluate(expr, env) return ret def evaluate(expr, env): if isinstance(expr, Literal) or isinstance(expr, Function): #return expr.value return expr elif isinstance(expr, Symbol): if not env.contains(expr.name): raise NebPanic(f"no such symbol: {expr}") return evaluate(env.get(expr.name), env) # if it's a literal list, return it if expr.data: return expr # if it's an empty list, return it elif len(expr.args) == 0: return expr if not isinstance(expr.args[0], Symbol): raise NebPanic("can't evaluate without a symbol") name = expr.args[0].name if env.contains(name): return env.get(name).call(expr, env) else: raise InterpretPanic(expr.args[0], "unable to evaluate") def interpretOr(symbol, args, env): # or returns true for the first expression that returns true for arg in args: ev = evaluate(arg, env) if not isinstance(ev, Bool): raise InterpretPanic(symbol, "requires :bool arguments") if ev.value == True: return ev return Bool(False) #GLOBALS.register("or", Builtin(interpretOr, 2)) or_arg = Arg("arg", T.Bool, False, True) GLOBALS.register("or", Builtin(interpretOr, [or_arg, or_arg], or_arg)) def interpretAnd(symbol, args, env): # and returns false for the first expression that returns false for arg in args: ev = evaluate(arg, env) if not isinstance(ev, Bool): raise InterpretPanic(symbol, "requires :bool arguments") if ev.value == False: return ev return Bool(True) GLOBALS.register("and", Builtin(interpretAnd, [or_arg, or_arg], or_arg)) def interpretEq(symbol, args, env): # NOTE this currently only works for literals # compare types because 0 != #false in neb if type(args[0]) == type(args[1]) and args[0].value == args[1].value: return Bool(True) else: return Bool(False) eq_arg = Arg("value", T.Literal, False, False) GLOBALS.register("eq?", Builtin(interpretEq, [eq_arg, eq_arg])) def interpretGreaterThan(symbol, args, env): return Bool(args[0].value > args[1].value) compare_arg = Arg("num", T.Number, False, False) GLOBALS.register(">", Builtin(interpretGreaterThan, [compare_arg, compare_arg])) def interpretGreaterThanEqual(symbol, args, env): return Bool(args[0].value >= args[1].value) GLOBALS.register(">=", Builtin(interpretGreaterThanEqual, [compare_arg, compare_arg])) def interpretLessThan(symbol, args, env): return Bool(args[0].value < args[1].value) GLOBALS.register("<", Builtin(interpretLessThan, [compare_arg, compare_arg])) def interpretLessThanEqual(symbol, args, env): return Bool(args[0].value <= args[1].value) GLOBALS.register("<=", Builtin(interpretLessThanEqual, [compare_arg, compare_arg])) def interpretAddition(symbol, args, env): res = 0 for arg in args: res += arg.value if isinstance(res, float): return Float(res) else: return Int(res) term_arg = Arg("term", T.Number, False, False) GLOBALS.register("+", Builtin(interpretAddition, [term_arg], term_arg)) def interpretSubtraction(symbol, args, env): if len(args) == 1: res = -args[0].value else: res = args[0].value for arg in args[1:]: res -= arg.value if isinstance(res, float): return Float(res) else: return Int(res) GLOBALS.register("-", Builtin(interpretSubtraction, [term_arg], term_arg)) def interpretMultiplication(symbol, args, env): res = args[0].value for arg in args[1:]: res = res * arg.value if isinstance(res, float): return Float(res) else: return Int(res) factor_arg = Arg("factor", T.Number, False, False) GLOBALS.register("*", Builtin(interpretMultiplication, [factor_arg, factor_arg], factor_arg)) def interpretDivision(symbol, args, env): ret = args[0].value / args[1].value if int(ret) == ret: return Int(int(ret)) else: return Float(ret) GLOBALS.register("/", Builtin(interpretDivision, [factor_arg, factor_arg])) def interpretNot(symbol, args, env): return Bool(not args[0].value) not_arg = Arg("not", T.Bool, False, False) GLOBALS.register("not", Builtin(interpretNot, [not_arg])) def interpretIf(symbol, args, env): # if cond t-branch [f-branch] if args[0].value: return evaluate(args[1], env) elif len(args) == 3: return evaluate(args[2], env) return List([]) cond = Arg("cond", T.Bool, False, False) t_branch = Arg("t-branch", T.Any, False, True) f_branch = Arg("f-branch", T.Any, True, True) GLOBALS.register("if", Builtin(interpretIf, [cond, t_branch, f_branch])) def interpretPrint(symbol, args, env): print(args[0].value) return List([]) # print returns nothing GLOBALS.register("print", Builtin(interpretPrint, [Arg("arg", T.String, False, False)])) def interpretDef(symbol, args, env): if not isinstance(args[0], Symbol): raise InterpretPanic(symbol, "requires a :string name", args[0]) name = args[0].name # NOTE: we are not evaluating the name!! if not isinstance(name, str): raise InterpretPanic(symbol, "requires a :string name") env.register(name, args[1]) # TODO since this isn't lazily evaluated, side effects are allowed (bad!) return List([]) def_name_arg = Arg("name", T.Any, False, True) def_val_arg = Arg("value", T.Any, False, False) GLOBALS.register("def", Builtin(interpretDef, [def_name_arg, def_val_arg])) def interpretRedef(symbol, args, env): if not isinstance(args[0], Symbol): raise InterpretPanic(symbol, "requires a :string name", args[0]) name = args[0].name # NOTE: we are not evaluating the name!! if not env.contains(name): raise InterpretPanic(symbol, "not previously defined", args[0]) env.reregister(name, args[1]) return List([]) GLOBALS.register("redef", Builtin(interpretRedef, [def_name_arg, def_val_arg])) def interpretLambda(symbol, args, env): if len(args[0].args) != 0: func = UserFunction("", args[0].args, args[1:]) else: func = UserFunction("", [], args[1:]) return func lambda_args_arg = Arg("args", T.Any, False, True) lambda_body_arg = Arg("body", T.Any, False, True) GLOBALS.register("lambda", Builtin(interpretLambda, [lambda_args_arg, lambda_body_arg], lambda_body_arg)) def interpretToString(symbol, args, env): item = args[0] if isinstance(item, String): return item elif isinstance(item, Literal): return String(str(item)) else: return String(f"{item}") GLOBALS.register("->string", Builtin(interpretToString, [Arg("arg", T.Any, False, False)])) def interpretConcat(symbol, args, env): # concat str1 str2...strN out = "" for arg in args: out += arg.value return String(out) string_arg = Arg("arg", T.String, False, False) GLOBALS.register("concat", Builtin(interpretConcat, [string_arg, string_arg], string_arg)) def interpretForCount(symbol, args, env): # for-count int exprs new_env = Environment(env) ret = None for idx in range(0, args[0].value): new_env.register("idx", Int(idx + 1)) for arg in args[1:]: ret = evaluate(arg, new_env) if ret is None: return List([]) return ret for_count_arg = Arg("count", T.Int, False, False) for_body_arg = Arg("body", T.Any, False, True) GLOBALS.register("for-count", Builtin(interpretForCount, [for_count_arg, for_body_arg], for_body_arg)) def interpretForEach(symbol, args, env): # for-each list exprs new_env = Environment(env) ret = None for item in args[0].args: new_env.register("_item_", evaluate(item, env)) for arg in args[1:]: ret = evaluate(arg, new_env) if ret is None: return List([]) return ret for_each_arg = Arg("list", T.List, False, False) GLOBALS.register("for-each", Builtin(interpretForEach, [for_each_arg, for_body_arg], for_body_arg)) def interpretPipe(symbol, args, env): new_env = Environment(env) pipe = None for arg in args: if pipe is not None: new_env.register("items", pipe) pipe = evaluate(arg, new_env) if pipe is None: return List([]) return pipe # TODO GLOBALS.register("|", Builtin(interpretPipe, 2)) def interpretBranch(symbol, args, env): for arg in args: if len(arg.args) != 2: raise InterpretPanic(symbol, "each branch requires two expressions") cond = evaluate(arg.args[0], env) # this is the condition if not isinstance(cond, Bool): raise InterpretPanic(symbol, "branch condition must be :bool", cond) if cond.value: return evaluate(arg.args[1], env) return List([]) GLOBALS.register("branch", Builtin(interpretBranch, [for_body_arg], for_body_arg)) def interpretFunc(symbol, args, env): # func (args) (exprs) # maybe: # arg [:type] -> type is optional # ?arg default -> 'arg' is optional, defaulted # *arg [:type] -> 'arg' is a list containing the remaining args if not isinstance(args[0], Symbol): raise InterpretPanic(symbol, "requires a :string name") name = args[0].name # NOTE: we are not evaluating the name!! # compose a lambda func = interpretLambda(None, args[1:], env) env.register(name, func) return List([]) GLOBALS.register("func", Builtin(interpretFunc, [def_name_arg, lambda_args_arg, lambda_body_arg], lambda_body_arg)) # THINGS NEEDED FOR AOC # - read the contents of a file def interpretReadLines(symbol, args, env): target_file_name = args[0].value target_file = Path(target_file_name).resolve() if not target_file.exists(): raise InterpretPanic(symbol, "no such file", target_file) with open(target_file, "r") as fil: data = fil.readlines() out = List([String(d) for d in data], True) # all lines are strings return out GLOBALS.register("read-lines", Builtin(interpretReadLines, [Arg("filename", T.String, False, False)])) # - strip whitespace from string def interpretStrip(symbol, args, env): return String(args[0].value.strip()) GLOBALS.register("strip", Builtin(interpretStrip, [Arg("filename", T.String, False, False)])) # - string->int and string->float def interpretStringToInt(symbol, args, env): try: val = int(args[0].value) return Int(val) except: raise InterpretPanic(symbol, "can't convert to an :int", args[0]) GLOBALS.register("string->int", Builtin(interpretStringToInt, [Arg("arg", T.String, False, False)])) # - split a string by a given field def interpretSplit(symbol, args, env): target = args[0] if len(args) == 1: return List([String(char) for char in target.value], True) splitter = args[1] ret = target.value.split(splitter.value) return List([String(r) for r in ret], True) GLOBALS.register("split", Builtin(interpretSplit, [Arg("target", T.String, False, False)], Arg("splitter", T.String, True, False))) # - get the length of a list def interpretListLength(symbol, args, env): return Int(len(args[0].args)) GLOBALS.register("list-length", Builtin(interpretListLength, [Arg("arg", T.List, False, False)])) # - first/rest of list def interpretFirst(symbol, args, env): if len(args[0].args) == 0: raise InterpretPanic(symbol, "list is empty") return evaluate(args[0].args[0], env) GLOBALS.register("first", Builtin(interpretFirst, [Arg("arg", T.List, False, False)])) def interpretRest(symbol, args, env): # TODO do we know it's not evaluated? return List(args[0].args[1:], True) # we don't evaluate the remainder of the list GLOBALS.register("rest", Builtin(interpretRest, [Arg("arg", T.List, False, False)])) # - iterate over list # - map def interpretMap(symbol, args, env): # TODO: to support lambdas, we can't assume the func is defined func = args[0] if not isinstance(func, Symbol): raise InterpretPanic(symbol, "requires a symbol as its first argument", func) lst = evaluate(args[1], env) if not isinstance(lst, List): raise InterpretPanic(symbol, "requires a :list as its second argument", lst) out = [] for arg in lst.args: ev = evaluate(List([func, arg]), env) out.append(ev) return List(out, True) GLOBALS.register("map", Builtin(interpretMap, [Arg("func", T.Any, False, True), Arg("list", T.List, False, False)])) #GLOBALS.register("map", Builtin(interpretMap, [Arg("func", T.Any, False, False), Arg("list", T.List, False, False)])) def interpretZip(symbol, args, env): z1 = args[0] z2 = args[1] if len(z1.args) != len(z2.args): raise InterpretPanic(symbol, "requires two :lists of the same size") out = [] for idx in range(len(z1.args)): f = z1.args[idx] s = z2.args[idx] out.append(List([f, s], True)) return List(out, True) zip_arg = Arg("list", T.List, False, False) GLOBALS.register("zip", Builtin(interpretZip, [zip_arg, zip_arg])) def interpretList(symbol, args, env): return List(args, True) GLOBALS.register("list", Builtin(interpretList, [], Arg("item", T.Any, False, False))) def interpretListReverse(symbol, args, env): new_args = args[0].args[:] # make a copy of the args new_args.reverse() return List(new_args, True) GLOBALS.register("list-reverse", Builtin(interpretListReverse, [Arg("list", T.List, False, False)])) def interpretApply(symbol, args, env): # TODO: to support lambdas, we can't assume the func is defined func = args[0] if not isinstance(func, Symbol): raise InterpretPanic(symbol, "requires a symbol as its first argument", func) new_lst = List([func] + args[1].args) return evaluate(new_lst, env) GLOBALS.register("apply", Builtin(interpretApply, [Arg("func", T.Any, False, True), Arg("list", T.List, False, False)])) def interpretGlob(symbol, args, env): items = glob(args[0].value) return List([String(item) for item in items], True) GLOBALS.register("glob", Builtin(interpretGlob, [Arg("regex", T.String, False, False)])) def interpretShell(symbol, args, env): # TODO either fail or throw exception (?) on error ret = subprocess.run(shlex.split(args[0].value), capture_output=True) return List([String(r) for r in ret.stdout.decode("utf-8").split("\n")], True) GLOBALS.register("$", Builtin(interpretShell, [Arg("command", T.String, False, False)])) def interpretEmpty(symbol, args, env): return Bool(len(args[0].args) == 0) GLOBALS.register("empty?", Builtin(interpretEmpty, [Arg("list", T.List, False, False)])) def interpretShuf(symbol, args, env): items = args[0].args[:] random.shuffle(items) return List(items, True) GLOBALS.register("shuf", Builtin(interpretShuf, [Arg("list", T.List, False, False)])) def interpretIsList(symbol, args, env): return Bool(isinstance(args[0], List)) GLOBALS.register("list?", Builtin(interpretIsList, [Arg("arg", T.Any, False, False)])) def interpretBlock(symbol, args, env): ret = List([]) for arg in args: ret = evaluate(arg, env) return ret block_arg = Arg("expr", T.Any, False, True) GLOBALS.register("block", Builtin(interpretBlock, [block_arg], block_arg)) def interpretExit(symbol, args, env): status = 0 if len(args) == 0 else args[0].value sys.exit(status) return List([]) exit_arg = Arg("status", T.Int, True, False) GLOBALS.register("exit", Builtin(interpretExit, [exit_arg])) def interpretUnlink(symbol, args, env): target_path = Path(args[0].value).resolve() if not target_path.exists(): raise InterpretPanic(symbol, "target file does not exist", target_path) target_path.unlink() return List([]) GLOBALS.register("unlink", Builtin(interpretUnlink, [Arg("filename", T.String, False, False)])) def interpretArgv(symbol, args, env): out = [] for arg in sys.argv[1:]: out.append(String(arg)) return List(out, True) GLOBALS.register("argv", Builtin(interpretArgv, [])) def interpretIn(symbol, args, env): target = args[0] lst = args[1] for arg in lst.args: if type(arg) == type(target) and arg.value == target.value: return Bool(True) return Bool(False) in_target_arg = Arg("target", T.Literal, False, False) in_list_arg = Arg("list", T.List, False, False) GLOBALS.register("in?", Builtin(interpretIn, [in_target_arg, in_list_arg])) def interpretLast(symbol, args, env): if len(args[0].args) == 0: raise InterpretPanic("List is empty") return evaluate(args[0].args[-1], env) GLOBALS.register("last", Builtin(interpretLast, [Arg("list", T.List, False, False)])) def interpretJoin(symbol, args, env): lst = args[0] target = args[1] return String(target.value.join([a.value for a in lst.args])) join_list_arg = Arg("list", T.List, False, False) join_string_arg = Arg("joiner", T.String, False, False) GLOBALS.register("join", Builtin(interpretJoin, [join_list_arg, join_string_arg])) def interpretWithWrite(symbol, args, env): target_file = args[0] new_env = Environment(env) target_path = Path(target_file.value).resolve() ret = Literal([]) with open(str(target_path), "w") as fil: new_env.register("_file_", List([fil], True)) # TODO wrong! for arg in args[1:]: ret = evaluate(arg, new_env) return ret GLOBALS.register("with-write", Builtin(interpretWithWrite, [Arg("filename", T.String, False, False)], Arg("exprs", T.Any, False, True))) def interpretWrite(symbol, args, env): # write :string :filehandle line = args[0] handle = args[1] handle.args[0].write(line.value) # TODO wrong! how do we evaluate a handle? return Literal([]) GLOBALS.register("write", Builtin(interpretWrite, [Arg("string", T.String, False, False), Arg("filename", T.List, False, False)])) def interpretNewline(symbol, args, env): return String("\n") GLOBALS.register("newline", Builtin(interpretNewline, [])) def interpretExists(symbol, args, env): return Bool(Path(args[0].value).resolve().exists()) GLOBALS.register("exists?", Builtin(interpretExists, [Arg("filename", T.String, False, False)])) def interpretFirstChar(symbol, args, env): if len(args[0].value) == 0: raise InterpretPanic(symbol, ":string is empty", ev) return String(args[0].value[0]) GLOBALS.register("first-char", Builtin(interpretFirstChar, [Arg("string", T.String, False, False)])) def interpretRestChar(symbol, args, env): return String(args[0].value[1:]) GLOBALS.register("rest-char", Builtin(interpretRestChar, [Arg("string", T.String, False, False)])) def interpretSlice(symbol, args, env): lst = args[0] idx = args[1] if len(args) == 2: return List(lst.args[idx.value - 1:]) length = args[2] diff = idx.value - 1 + length.value return List(lst.args[idx.value - 1:diff]) slice_list_arg = Arg("list", T.List, False, False) slice_idx_arg = Arg("idx", T.Int, False, False) slice_length_arg = Arg("length", T.Int, True, False) GLOBALS.register("slice", Builtin(interpretSlice, [slice_list_arg, slice_idx_arg, slice_length_arg])) def interpretClear(symbol, args, env): subprocess.run(["clear"]) return List([]) GLOBALS.register("clear", Builtin(interpretClear, [])) def interpretReadLine(symbol, args, env): ret = input(args[0].value) return String(ret) GLOBALS.register("read-line", Builtin(interpretReadLine, [Arg("prompt", T.String, False, False)])) def interpretReadChar(symbol, args, env): import termios, tty fd = sys.stdin.fileno() old = termios.tcgetattr(fd) try: tty.setraw(fd) ch = sys.stdin.buffer.read1(4) # some keys are >1 bytes except Exception: raise finally: termios.tcsetattr(fd, termios.TCSADRAIN, old) return String(ch.decode("utf-8")) GLOBALS.register("read-char", Builtin(interpretReadChar, [])) def interpretAppend(symbol, args, env): lst = args[0] val = args[1] items = lst.args[:] return List(items + [val], True) GLOBALS.register("append", Builtin(interpretAppend, [Arg("list", T.List, False, False), Arg("item", T.Any, False, False)])) # TODO: this is actually for records/structs/whatever they're called def interpretRemove(symbol, args, env): lst = args[0] key = args[1] out = [] for arg in lst.args: if arg.args[0].value != key.value: out.append(arg) return List(out, True) GLOBALS.register("remove", Builtin(interpretRemove, [Arg("list", T.List, False, False), Arg("key", T.Any, False, False)])) def interpretWhile(symbol, args, env): cond = args[0] ret = List([]) while True: ev = evaluate(cond, env) if not isinstance(ev, Bool): raise InterpretPanic(symbol, "expects a :bool condition", ev) if not ev.value: break for arg in args[1:]: ret = evaluate(arg, env) return ret GLOBALS.register("while", Builtin(interpretWhile, [Arg("cond", T.Bool, False, True)], Arg("expr", T.Any, False, True))) def interpretUse(symbol, args, env): target_file_name = args[0].value target_file = Path(target_file_name).resolve() if not target_file.exists(): raise InterpretPanic(symbol, "no such file", target_file) with open(target_file, "r") as fil: data = fil.read() interpret(parse(lex(data))) return List([]) GLOBALS.register("use", Builtin(interpretUse, [Arg("filename", T.String, False, False)])) def interpretAssert(symbol, args, env, ns): if args[0].value != True: raise InterpretPanic(symbol, "assertion failed") return List([]) GLOBALS.register("assert", Builtin(interpretAssert, [Arg("cond", T.Bool, False, False)]))