from structs import * from exceptions import * from lexer import lex from parser import parse from pathlib import Path from glob import glob import subprocess import shlex import random import sys class Function: def __init__(self, name, params, body, *arities): self.name = name self.params = params self.body = body if len(arities) == 0: self.arities = None else: self.arities = arities def call(self, expr, env): pass class Builtin(Function): def __init__(self, callable_, *arities): super().__init__("", None, callable_, *arities) def call(self, expr, env): if self.arities is not None and len(expr.args[1:]) not in self.arities: fmt = f"[{self.arities[0]}" for arity in self.arities[1:]: fmt += f", {arity}" fmt += "]" raise InterpretPanic(expr.args[0], f"expected {fmt} arguments, received {len(expr.args) - 1}") return self.body(expr.args[0], expr.args[1:], env) class UserFunction(Function): def __init__(self, name, params, body): super().__init__(name, params, body, len(params)) def call(self, expr, env): this_env = Environment(env) for idx, param in enumerate(self.params): # TODO this is wrong!!! this won't always be a literal #this_env.register(param.name, Literal(evaluate(expr.args[idx+1],env))) this_env.register(param.name, evaluate(expr.args[idx+1],env)) 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): #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 name == "def": return interpretDef(expr.args[0], expr.args[1:], env) elif 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 if len(args) < 2: raise InterpretPanic(symbol, "requires at least two arguments") 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)) def interpretAnd(symbol, args, env): # and returns false for the first expression that returns false if len(args) < 2: raise InterpretPanic(symbol, "requires at least two arguments") 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)) def interpretEq(symbol, args, env): # equal # NOTE this currently only works for literals first = evaluate(args[0], env) second = evaluate(args[1], env) if not (isinstance(first, Literal) and isinstance(second, Literal)): raise InterpretPanic(symbol, "can only compare :literals") # compare types because 0 != #false in neb # TODO number equality? if type(first) == type(second) and first.value == second.value: return Bool(True) else: return Bool(False) GLOBALS.register("eq?", Builtin(interpretEq, 2)) def interpretComparison(symbol, args, env): left = evaluate(args[0], env) if not (isinstance(left, Int) or isinstance(left, Float)): raise InterpretPanic(symbol, "first argument must be a :number", left) right = evaluate(args[1], env) if not (isinstance(right, Int) or isinstance(right, Float)): raise InterpretPanic(symbol, "second argument must be a :number", right) if symbol.name == ">": return Bool(left.value > right.value) elif symbol.name == ">=": return Bool(left.value >= right.value) elif symbol.name == "<": return Bool(left.value < right.value) elif symbol.name == "<=": return Bool(left.value <= right.value) GLOBALS.register(">", Builtin(interpretComparison, 2)) GLOBALS.register(">=", Builtin(interpretComparison, 2)) GLOBALS.register("<", Builtin(interpretComparison, 2)) GLOBALS.register("<=", Builtin(interpretComparison, 2)) def interpretTerm(symbol, args, env): if len(args) < 1: raise InterpretPanic(symbol, "requires at least one argument") res = None for arg in args: ev = evaluate(arg, env) if not (isinstance(ev, Int) or isinstance(ev, Float)): raise InterpretPanic(symbol, "argument must be a :number", ev) if res is None: res = ev.value elif symbol.name == "+": res += ev.value elif symbol.name == "-": res -= ev.value if isinstance(res, float): return Float(res) else: return Int(res) GLOBALS.register("+", Builtin(interpretTerm)) GLOBALS.register("-", Builtin(interpretTerm)) def interpretFactor(symbol, args, env): if symbol.name == "/": num = evaluate(args[0], env) if not (isinstance(num, Int) or isinstance(num, Float)): raise InterpretPanic(symbol, "numerator must be a :number", num) denom = evaluate(args[1], env) if not (isinstance(denom, Int) or isinstance(denom, Float)): raise InterpretPanic(symbol, "denominator must be a :number", denom) ret = num.value / denom.value if int(ret) == ret: return Int(int(ret)) else: return Float(ret) else: if len(args) < 2: raise InterpretPanic(symbol, "requires at least two arguments") first = evaluate(args[0], env) if not (isinstance(first, Int) or isinstance(first, Float)): raise InterpretPanic(symbol, "argument must be a :number", first) res = first.value for arg in args[1:]: tmp = evaluate(arg, env) if not (isinstance(tmp, Int) or isinstance(tmp, Float)): raise InterpretPanic(symbol, "argument must be a :number", tmp) res = res * tmp.value if isinstance(res, float): return Float(res) else: return Int(res) GLOBALS.register("*", Builtin(interpretFactor)) GLOBALS.register("/", Builtin(interpretFactor, 2)) def interpretNot(symbol, args, env): res = evaluate(args[0], env) if not isinstance(res, Bool): raise InterpretPanic(symbol, "requires a :bool", res) return Bool(not res.value) GLOBALS.register("not", Builtin(interpretNot, 1)) def interpretIf(symbol, args, env): # if cond t-branch [f-branch] cond = evaluate(args[0], env) if not isinstance(cond, Bool): raise InterpretPanic(symbol, "condition must be :bool", cond) if cond.value: return evaluate(args[1], env) elif len(args) == 3: return evaluate(args[2], env) return List([]) # this shouldn't be reached GLOBALS.register("if", Builtin(interpretIf, 2, 3)) def interpretPrint(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): raise InterpretPanic(symbol, "requires a :string") print(ev.value) return List([]) # print returns nothing GLOBALS.register("print", Builtin(interpretPrint, 1)) 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") ev = evaluate(args[1], env) env.register(name, ev) return List([]) GLOBALS.register("def", Builtin(interpretDef, 2)) 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]) ev = evaluate(args[1], env) env.reregister(name, ev) return List([]) GLOBALS.register("redef", Builtin(interpretRedef, 2)) 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 GLOBALS.register("lambda", Builtin(interpretLambda)) def interpretToString(symbol, args, env): ev = evaluate(args[0], env) if isinstance(ev, String): return ev elif isinstance(ev, Literal): return String(str(ev)) else: return String(f"{ev}") GLOBALS.register("->string", Builtin(interpretToString, 1)) def interpretConcat(symbol, args, env): # concat str1 str2...strN if len(args) < 2: raise InterpretPanic(symbol, "requires at least two arguments") out = "" for arg in args: tmp = evaluate(arg, env) if not isinstance(tmp, String): raise InterpretPanic(symbol, "requires :string", tmp) out += tmp.value return String(out) GLOBALS.register("concat", Builtin(interpretConcat)) def interpretForCount(symbol, args, env): # for-count int exprs num = evaluate(args[0], env) if not isinstance(num, Int): raise InterpretPanic(symbol, "count must be an integer", num) new_env = Environment(env) ret = None for idx in range(0, num.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 GLOBALS.register("for-count", Builtin(interpretForCount)) def interpretForEach(symbol, args, env): # for-each list exprs lst = evaluate(args[0], env) if not isinstance(lst, List): raise InterpretPanic(symbok, "requires a :list", lst) new_env = Environment(env) ret = None for item in lst.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 GLOBALS.register("for-each", Builtin(interpretForEach)) def interpretPipe(symbol, args, env): if len(args) < 2: raise InterpretPanic(symbol, "requires at least two expressions") 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 GLOBALS.register("|", Builtin(interpretPipe)) def interpretBranch(symbol, args, env): if len(args) == 0: raise InterpretPanic(symbol, "requires at least one pair of expressions") 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)) def interpretFunc(symbol, args, env): # func (args) (exprs) if len(args) < 3: raise InterpretPanic(symbol, "requires a name, argument list, and at least one expression") 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)) # THINGS NEEDED FOR AOC # - read the contents of a file def interpretReadLines(symbol, args, env): target_file_name = evaluate(args[0], env).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, 1)) # - strip whitespace from string def interpretStrip(symbol, args, env): out = evaluate(args[0], env) if not isinstance(out, String): raise InterpretPanic(symbol, "requires a :string", out) return String(out.value.strip()) GLOBALS.register("strip", Builtin(interpretStrip, 1)) # - string->int and string->float def interpretStringToInt(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): raise InterpretPanic(symbol, "requires a :string", ev) try: val = int(ev.value) return Int(val) except: raise InterpretPanic(symbol, "can't convert to an :int", ev) GLOBALS.register("string->int", Builtin(interpretStringToInt, 1)) # - split a string by a given field def interpretSplit(symbol, args, env): target = evaluate(args[0], env) if not isinstance(target, String): raise InterpretPanic(symbol, "requires a :string as its first argument", target) if len(args) == 1: return List([String(char) for char in target.value], True) splitter = evaluate(args[1], env) if not isinstance(splitter, String): raise InterpretPanic(symbol, "requires a :string as its second argument", splitter) ret = target.value.split(splitter.value) return List([String(r) for r in ret], True) GLOBALS.register("split", Builtin(interpretSplit, 1, 2)) # - get the length of a list def interpretListLength(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): raise InterpretPanic(symbol, "requires a :list", ev) return Int(len(ev.args)) GLOBALS.register("list-length", Builtin(interpretListLength, 1)) # - first/rest of list def interpretFirst(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): raise InterpretPanic(symbol, "requires a :list", ev) if len(ev.args) == 0: raise InterpretPanic(symbol, "list is empty") return evaluate(ev.args[0], env) GLOBALS.register("first", Builtin(interpretFirst, 1)) def interpretRest(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): raise InterpretPanic(symbol, "requires a :list", ev) # TODO do we know it's not evaluated? return List(ev.args[1:], True) # we don't evaluate the remainder of the list GLOBALS.register("rest", Builtin(interpretRest, 1)) # - 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, 2)) def interpretZip(symbol, args, env): z1 = evaluate(args[0], env) if not isinstance(z1, List): raise InterpretPanic(symbol, "requires two :lists", z1) z2 = evaluate(args[1], env) if not isinstance(z2, List): raise InterpretPanic(symbol, "requires two :lists", z2) 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 = evaluate(z1.args[idx], env) s = evaluate(z2.args[idx], env) out.append(List([f, s], True)) return List(out, True) GLOBALS.register("zip", Builtin(interpretZip, 2)) def interpretList(symbol, args, env): out = [] for arg in args: out.append(evaluate(arg, env)) return List(out, True) GLOBALS.register("list", Builtin(interpretList)) def interpretListReverse(symbol, args, env): lst = evaluate(args[0], env) if not isinstance(lst, List): raise InterpretPanic(symbol, "requires a :list", lst) new_args = lst.args[:] # make a copy of the args new_args.reverse() return List(new_args, True) GLOBALS.register("list-reverse", Builtin(interpretListReverse, 1)) def interpretApply(symbol, args, env): 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) new_lst = List([func] + lst.args) return evaluate(new_lst, env) GLOBALS.register("apply", Builtin(interpretApply, 2)) def interpretGlob(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): raise InterpretPanic(symbol, "requires a :string", ev) items = glob(ev.value) return List([String(item) for item in items], True) GLOBALS.register("glob", Builtin(interpretGlob, 1)) def interpretShell(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): raise InterpretPanic(symbol, "requires a :string", ev) # TODO either fail or throw exception (?) on error ret = subprocess.run(shlex.split(ev.value), capture_output=True) return List([String(r) for r in ret.stdout.decode("utf-8").split("\n")], True) GLOBALS.register("$", Builtin(interpretShell, 1)) def interpretEmpty(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): raise InterpretPanic(symbol, "requires a :list", ev) return Bool(len(ev.args) == 0) GLOBALS.register("empty?", Builtin(interpretEmpty, 1)) def interpretShuf(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): raise InterpretPanic(symbol, "expects a :list", ev) items = ev.args[:] random.shuffle(items) return List(items, True) GLOBALS.register("shuf", Builtin(interpretShuf, 1)) def interpretIsList(symbol, args, env): ev = evaluate(args[0], env) return Bool(isinstance(ev, List)) GLOBALS.register("list?", Builtin(interpretIsList, 1)) def interpretBlock(symbol, args, env): ret = List([]) for arg in args: ret = evaluate(arg, env) return ret GLOBALS.register("block", Builtin(interpretBlock)) def interpretExit(symbol, args, env): if len(args) > 1: raise InterpretPanic(symbol, "expects one (optional) argument") status = 0 if len(args) == 0 else evaluate(args[0], env).value if not isinstance(status, int): raise InterpretPanic(symbol, "expects an :int", status) sys.exit(status) return List([]) GLOBALS.register("exit", Builtin(interpretExit, 0, 1)) def interpretUnlink(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): raise InterpretPanic(symbol, "expects a :string", ev) target_path = Path(ev.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, 1)) 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, 0)) def interpretIn(symbol, args, env): target = evaluate(args[0], env) if not isinstance(target, Literal): raise InterpretPanic(symbol, "expects a :literal as its first argument", target) lst = evaluate(args[1], env) if not isinstance(lst, List): raise InterpretPanic(symbol, "expects a :list as its second argument", lst) for arg in lst.args: ev = evaluate(arg, env) if type(ev) == type(target) and ev.value == target.value: return Bool(True) return Bool(False) GLOBALS.register("in?", Builtin(interpretIn, 2)) def interpretLast(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): raise InterpretPanic("'last' expects a List") if len(ev.args) == 0: raise InterpretPanic("List is empty") return evaluate(ev.args[-1], env) GLOBALS.register("last", Builtin(interpretLast, 1)) def interpretJoin(symbol, args, env): lst = evaluate(args[0], env) if not isinstance(lst, List): raise InterpretPanic(symbol, "expects a :list as its first argument", lst) target = evaluate(args[1], env) if not isinstance(target, String): raise InterpretPanic(symbol, "expects a :string as its second argument", target) return String(target.value.join(lst.args)) GLOBALS.register("join", Builtin(interpretJoin, 2)) def interpretWithWrite(symbol, args, env): if len(args) == 0: raise InterpretPanic(symbol, "expects at least one argument") target_file = evaluate(args[0], env) if not isinstance(target_file, String): raise InterpretPanic(symbol, "expects a :string as its first argument", target_file) 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)) def interpretWrite(symbol, args, env): # write :string :filehandle line = evaluate(args[0], env) if not isinstance(line, String): raise InterpretPanic(symbol, "expects a :string as its first argument", line) handle = evaluate(args[1], env) handle.args[0].write(line.value) # TODO wrong! how do we evaluate a handle? return Literal([]) GLOBALS.register("write", Builtin(interpretWrite, 2)) def interpretNewline(symbol, args, env): return String("\n") GLOBALS.register("newline", Builtin(interpretNewline, 0)) def interpretExists(symbol, args, env): file_or_dir = evaluate(args[0], env) if not isinstance(file_or_dir, String): raise InterpretPanic(symbol, "expects a :string", file_or_dir) return Bool(Path(file_or_dir.value).resolve().exists()) GLOBALS.register("exists?", Builtin(interpretExists, 1)) def interpretFirstChar(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): raise InterpretPanic(symbol, "expects a :string", ev) if len(ev.value) == 0: raise InterpretPanic(symbol, ":string is empty", ev) return String(ev.value[0]) GLOBALS.register("first-char", Builtin(interpretFirstChar, 1)) def interpretRestChar(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): raise InterpretPanic(symbol, "expects a string", ev) return String(ev.value[1:]) GLOBALS.register("rest-char", Builtin(interpretRestChar, 1)) def interpretSlice(symbol, args, env): lst = evaluate(args[0], env) if not isinstance(lst, List): raise InterpretPanic(symbol, "expects a :list as its first argument", lst) idx = evaluate(args[1], env) if not isinstance(idx, Int): raise InterpretPanic(symbol, "expects an :int as its second argument", idx) if len(args) == 2: return List(lst.args[idx.value - 1:]) length = evaluate(args[2], env) if not isinstance(length, Int): raise InterpretPanic(symbol, "expects an :int as its third argument", length) diff = idx.value - 1 + length.value return List(lst.args[idx.value - 1:diff]) GLOBALS.register("slice", Builtin(interpretSlice, 2, 3)) def interpretClear(symbol, args, env): subprocess.run(["clear"]) return List([]) GLOBALS.register("clear", Builtin(interpretClear, 0)) def interpretInput(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): raise InterpretPanic(symbol, "expects a :string", ev) ret = input(ev.value) return String(ret) GLOBALS.register("input", Builtin(interpretInput, 1)) def interpretAppend(symbol, args, env): lst = evaluate(args[0], env) if not isinstance(lst, List): raise InterpretPanic(symbol, "expects a :list as its first argument", lst) val = evaluate(args[1], env) items = lst.args[:] return List(items + [val], True) GLOBALS.register("append", Builtin(interpretAppend, 2)) def interpretRemove(symbol, args, env): lst = evaluate(args[0], env) if not isinstance(lst, List): raise InterpretPanic(symbol, "expects a :list as its first argument", lst) key = evaluate(args[1], env) 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, 2)) 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)) def interpretAnsiEscape(symbol, args, env): return String(f"\033") GLOBALS.register("ansi-escape", Builtin(interpretAnsiEscape, 0)) def interpretUse(symbol, args, env): target_file_name = evaluate(args[0], env).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, 1))