from structs import * from exceptions import * from lexer import lex from parser import parse 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_: TypeEnum optional: bool lazy: bool def __str__(self): opt = "?" if self.optional else "" lazy = "~" if self.lazy else "" return f"{lazy}{opt}{self.name} {self.type_}" 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 describe(self, name=None): if name is None: name = self.name out = [f"({name}"] if self.args is not None: for arg in self.args: out.append(f"{arg}") if self.many is not None: out.append(f"*{self.many}") return " ".join(out) + ")" 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 is_subtype_of(ev.type_, arg.type_): exp = f"{arg.type_}" rec = f"{ev.type_}" 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", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.BOOL, False, False) t_branch = Arg("t-branch", TypeEnum.ANY, False, True) f_branch = Arg("f-branch", TypeEnum.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", TypeEnum.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", TypeEnum.ANY, False, True) def_val_arg = Arg("value", TypeEnum.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", TypeEnum.ANY, False, True) lambda_body_arg = Arg("body", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.INT, False, False) for_body_arg = Arg("body", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.STRING, False, False)], Arg("splitter", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.ANY, False, True), Arg("list", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.ANY, False, True), Arg("list", TypeEnum.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", TypeEnum.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", TypeEnum.STRING, False, False)])) def interpretEmpty(symbol, args, env): return Bool(len(args[0].args) == 0) GLOBALS.register("empty?", Builtin(interpretEmpty, [Arg("list", TypeEnum.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", TypeEnum.LIST, False, False)])) def interpretIsList(symbol, args, env): return Bool(isinstance(args[0], List)) GLOBALS.register("list?", Builtin(interpretIsList, [Arg("arg", TypeEnum.ANY, False, False)])) def interpretBlock(symbol, args, env): ret = List([]) for arg in args: ret = evaluate(arg, env) return ret block_arg = Arg("expr", TypeEnum.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", TypeEnum.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", TypeEnum.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", TypeEnum.LITERAL, False, False) in_list_arg = Arg("list", TypeEnum.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", TypeEnum.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", TypeEnum.LIST, False, False) join_string_arg = Arg("joiner", TypeEnum.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", TypeEnum.STRING, False, False)], Arg("exprs", TypeEnum.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", TypeEnum.STRING, False, False), Arg("filename", TypeEnum.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", TypeEnum.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", TypeEnum.STRING, False, False)])) def interpretRestChar(symbol, args, env): return String(args[0].value[1:]) GLOBALS.register("rest-char", Builtin(interpretRestChar, [Arg("string", TypeEnum.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", TypeEnum.LIST, False, False) slice_idx_arg = Arg("idx", TypeEnum.INT, False, False) slice_length_arg = Arg("length", TypeEnum.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", TypeEnum.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", TypeEnum.LIST, False, False), Arg("item", TypeEnum.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", TypeEnum.LIST, False, False), Arg("key", TypeEnum.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", TypeEnum.BOOL, False, True)], Arg("expr", TypeEnum.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", TypeEnum.STRING, False, False)])) def interpretAssert(symbol, args, env): if args[0].value != True: raise InterpretPanic(symbol, "assertion failed") return List([]) GLOBALS.register("assert", Builtin(interpretAssert, [Arg("cond", TypeEnum.BOOL, False, False)]))