diff options
| author | mryouse | 2022-06-05 03:06:12 +0000 |
|---|---|---|
| committer | mryouse | 2022-06-05 03:06:12 +0000 |
| commit | 86104621edc9758155909e0b6e7b9822190815fc (patch) | |
| tree | 572e8720273daa75e93da50c75808d046ceae3d8 /interpreter.py | |
| parent | ed78cd23f92b8b96dd7ffa3313be2262b0b29ff0 (diff) | |
refactor: better error messages
Diffstat (limited to 'interpreter.py')
| -rw-r--r-- | interpreter.py | 159 |
1 files changed, 82 insertions, 77 deletions
diff --git a/interpreter.py b/interpreter.py index ef0bc4a..47b2363 100644 --- a/interpreter.py +++ b/interpreter.py @@ -1,4 +1,5 @@ from structs import * +from exceptions import * from pathlib import Path from glob import glob import subprocess @@ -31,7 +32,7 @@ class Builtin(Function): for arity in self.arities[1:]: fmt += f", {arity}" fmt += "]" - raise Exception(f"expected {fmt} arguments, received {len(expr.args)}") + 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): @@ -58,7 +59,7 @@ class Environment: def reregister(self, key, value): if not self.contains(key): - raise Exception(f"undefined symbol: '{key}") + raise NebPanic(f"undefined symbol: '{key}") if key in self.environment: self.register(key, value) else: @@ -74,7 +75,7 @@ class Environment: def get(self, key): if not self.contains(key): - raise Exception(f"undefined symbol: '{key}") + raise NebPanic(f"undefined symbol: '{key}") if key in self.environment: return self.environment[key] else: @@ -100,7 +101,7 @@ def evaluate(expr, env): return expr elif isinstance(expr, Symbol): if not env.contains(expr.name): - raise Exception(f"no such symbol: {expr}") + raise NebPanic(f"no such symbol: {expr}") return evaluate(env.get(expr.name), env) # if it's a literal list, return it @@ -111,23 +112,23 @@ def evaluate(expr, env): return expr if not isinstance(expr.args[0], Symbol): - raise Exception("can't evaluate without a 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 Exception(f"unable to evaluate: {expr}") + 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 Exception("'or' has at least two operands") + raise InterpretPanic(symbol, "requires at least two arguments") for arg in args: ev = evaluate(arg, env) if not isinstance(ev, Bool): - raise Exception("'or' needs boolean arguments") + raise InterpretPanic(symbol, "requires :bool arguments") if ev.value == True: return ev return Bool(False) @@ -137,11 +138,11 @@ 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 Exception("'and' has at least two operands") + raise InterpretPanic(symbol, "requires at least two arguments") for arg in args: ev = evaluate(arg, env) if not isinstance(ev, Bool): - raise Exception("'and' needs boolean arguments") + raise InterpretPanic(symbol, "requires :bool arguments") if ev.value == False: return ev return Bool(True) @@ -154,7 +155,7 @@ def interpretEq(symbol, args, env): first = evaluate(args[0], env) second = evaluate(args[1], env) if not (isinstance(first, Literal) and isinstance(second, Literal)): - raise Exception("'eq?' can only compare literals") + 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: @@ -167,10 +168,10 @@ 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 Exception("'left' must be a number") + 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 Exception("'right' must be a number") + raise InterpretPanic(symbol, "second argument must be a :number", right) if symbol.name == ">": return Bool(left.value > right.value) @@ -188,12 +189,12 @@ GLOBALS.register("<=", Builtin(interpretComparison, 2)) def interpretTerm(symbol, args, env): if len(args) < 1: - raise Exception("term has at least one operand") + 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 Exception("term must be a number") + raise InterpretPanic(symbol, "argument must be a :number", ev) if res is None: res = ev.value elif symbol.name == "+": @@ -212,10 +213,10 @@ def interpretFactor(symbol, args, env): if symbol.name == "/": num = evaluate(args[0], env) if not (isinstance(num, Int) or isinstance(num, Float)): - raise Exception("numerator must be a number") + raise InterpretPanic(symbol, "numerator must be a :number", num) denom = evaluate(args[1], env) if not (isinstance(denom, Int) or isinstance(denom, Float)): - raise Exception("denominator must be a number") + raise InterpretPanic(symbol, "denominator must be a :number", denom) ret = num.value / denom.value if int(ret) == ret: return Int(int(ret)) @@ -223,15 +224,15 @@ def interpretFactor(symbol, args, env): return Float(ret) else: if len(args) < 2: - raise Exception("'*' requires at least two operands") + raise InterpretPanic(symbol, "requires at least two arguments") first = evaluate(args[0], env) if not (isinstance(first, Int) or isinstance(first, Float)): - raise Exception("'*' operand must be a number") + 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 Exception("'*' operand must be a number") + raise InterpretPanic(symbol, "argument must be a :number", tmp) res = res * tmp.value if isinstance(res, float): return Float(res) @@ -244,7 +245,7 @@ GLOBALS.register("/", Builtin(interpretFactor, 2)) def interpretNot(symbol, args, env): res = evaluate(args[0], env) if not isinstance(res, Bool): - raise Exception("'not' only works on booleans") + raise InterpretPanic(symbol, "requires a :bool", res) return Bool(not res.value) GLOBALS.register("not", Builtin(interpretNot, 1)) @@ -253,7 +254,7 @@ def interpretIf(symbol, args, env): # if cond t-branch [f-branch] cond = evaluate(args[0], env) if not isinstance(cond, Bool): - raise Exception("'if' condition must be boolean") + raise InterpretPanic(symbol, "condition must be :bool", cond) if cond.value: return evaluate(args[1], env) elif len(args) == 3: @@ -265,7 +266,7 @@ GLOBALS.register("if", Builtin(interpretIf, 2, 3)) def interpretPrint(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): - raise Exception("can only 'print' strings") + raise InterpretPanic(symbol, "requires a :string") print(ev.value) return List([]) # print returns nothing @@ -274,10 +275,10 @@ GLOBALS.register("print", Builtin(interpretPrint, 1)) def interpretDef(symbol, args, env): if not isinstance(args[0], Symbol): - raise Exception("'def' requires a string literal as a name") + 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 Exception("'def' requires a string literal as a name") + raise InterpretPanic(symbol, "requires a :string name") ev = evaluate(args[1], env) env.register(name, ev) @@ -287,10 +288,10 @@ GLOBALS.register("def", Builtin(interpretDef, 2)) def interpretRedef(symbol, args, env): if not isinstance(args[0], Symbol): - raise Exception("'redef' requires a string literal as a name") + 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 Exception("'redef' only works on previously defined variables") + raise InterpretPanic(symbol, "not previously defined", args[0]) ev = evaluate(args[1], env) env.reregister(name, ev) @@ -321,12 +322,12 @@ GLOBALS.register("->string", Builtin(interpretToString, 1)) def interpretConcat(symbol, args, env): # concat str1 str2...strN if len(args) < 2: - raise Exception("'concat' takes at least two arguments") + raise InterpretPanic(symbol, "requires at least two arguments") out = "" for arg in args: tmp = evaluate(arg, env) if not isinstance(tmp, String): - raise Exception("'concat' arguments must be strings") + raise InterpretPanic(symbol, "requires :string", tmp) out += tmp.value return String(out) @@ -336,7 +337,7 @@ def interpretForCount(symbol, args, env): # for-count int exprs num = evaluate(args[0], env) if not isinstance(num, Int): - raise Exception("'for-count' count must be an integer") + raise InterpretPanic(symbol, "count must be an integer", num) new_env = Environment(env) ret = None for idx in range(0, num.value): @@ -353,7 +354,7 @@ def interpretForEach(symbol, args, env): # for-each list exprs lst = evaluate(args[0], env) if not isinstance(lst, List): - raise Exception("'for-each' expects a list") + raise InterpretPanic(symbok, "requires a :list", lst) new_env = Environment(env) ret = None for item in lst.args: @@ -368,7 +369,7 @@ GLOBALS.register("for-each", Builtin(interpretForEach)) def interpretPipe(symbol, args, env): if len(args) < 2: - raise Exception("'|' takes at least two expressions") + raise InterpretPanic(symbol, "requires at least two expressions") new_env = Environment(env) pipe = None for arg in args: @@ -383,11 +384,13 @@ GLOBALS.register("|", Builtin(interpretPipe)) def interpretBranch(symbol, args, env): if len(args) == 0: - raise Exception("'branch' takes at least one expression") + raise InterpretPanic(symbol, "requires at least one pair of expressions") for arg in args: if len(arg.args) != 2: - raise Exception("'branch' branches have two expressions") + raise InterpretPanic(symbol, "each branch requires two expressions") cond = evaluate(arg.args[0], env) # this is the condition + if not isinstance(cond, Boolean): + raise InterpretPanic(symbol, "branch condition must be :bool", cond) if cond.value: return evaluate(arg.args[1], env) return List([]) @@ -397,9 +400,9 @@ GLOBALS.register("branch", Builtin(interpretBranch)) def interpretFunc(symbol, args, env): # func <name> (args) (exprs) if len(args) < 3: - raise Exception("'func' takes a name, arguments, and at least one expression") + raise InterpretPanic(symbol, "requires a name, argument list, and at least one expression") if not isinstance(args[0], Symbol): - raise Exception("'func' requires a string literal as a name") + raise InterpretPanic(symbol, "requires a :string name") name = args[0].name # NOTE: we are not evaluating the name!! # compose a lambda @@ -416,7 +419,7 @@ 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 Exception(f"no such file: {target_file}") + 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 @@ -427,6 +430,8 @@ 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)) @@ -435,12 +440,12 @@ GLOBALS.register("strip", Builtin(interpretStrip, 1)) def interpretStringToInt(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): - raise Exception("'string->int' expects a string") + raise InterpretPanic(symbol, "requires a :string", ev) try: val = int(ev.value) return Int(val) except: - raise Exception(f"can't convert {ev} to an int") + raise InterpretPanic(symbol, "can't convert to an :int", ev) GLOBALS.register("string->int", Builtin(interpretStringToInt, 1)) @@ -448,10 +453,10 @@ GLOBALS.register("string->int", Builtin(interpretStringToInt, 1)) def interpretSplit(symbol, args, env): target = evaluate(args[0], env) if not isinstance(target, String): - raise Exception("'split' expects a string") + raise InterpretPanic(symbol, "requires a :string as its first argument", target) splitter = evaluate(args[1], env) if not isinstance(splitter, String): - raise Exception("'split' expects a string as it's splitter") + 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) @@ -461,7 +466,7 @@ GLOBALS.register("split", Builtin(interpretSplit, 2)) def interpretListLength(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): - raise Exception("'list-length' expects a List") + raise InterpretPanic(symbol, "requires a :list", ev) return Literal(len(ev.args)) GLOBALS.register("list-length", Builtin(interpretListLength, 1)) @@ -470,9 +475,9 @@ GLOBALS.register("list-length", Builtin(interpretListLength, 1)) def interpretFirst(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): - raise Exception("'first' expects a List") + raise InterpretPanic(symbol, "requires a :list", ev) if len(ev.args) == 0: - raise Exception("List is empty") + raise InterpretPanic(symbol, "list is empty") return evaluate(ev.args[0], env) GLOBALS.register("first", Builtin(interpretFirst, 1)) @@ -480,7 +485,7 @@ GLOBALS.register("first", Builtin(interpretFirst, 1)) def interpretRest(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): - raise Exception("'rest' expects a 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 @@ -492,10 +497,10 @@ 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 Exception("'map' takes a function as its first argument") + raise InterpretPanic(symbol, "requires a symbol as its first argument", func) lst = evaluate(args[1], env) if not isinstance(lst, List): - raise Exception("'map' takes a List as its second argument") + raise InterpretPanic(symbol, "requires a :list as its second argument", lst) out = [] for arg in lst.args: ev = evaluate(List([func, arg]), env) @@ -507,12 +512,12 @@ GLOBALS.register("map", Builtin(interpretMap, 2)) def interpretZip(symbol, args, env): z1 = evaluate(args[0], env) if not isinstance(z1, List): - raise Exception("'zip' only works on lists") + raise InterpretPanic(symbol, "requires two :lists", z1) z2 = evaluate(args[1], env) if not isinstance(z2, List): - raise Exception("'zip' only works on lists") + raise InterpretPanic(symbol, "requires two :lists", z2) if len(z1.args) != len(z2.args): - raise Exception("'zip' expects two lists of the same size") + raise InterpretPanic(symbol, "requires two :lists of the same size") out = [] for idx in range(len(z1.args)): f = evaluate(z1.args[idx], env) @@ -533,7 +538,7 @@ GLOBALS.register("list", Builtin(interpretList)) def interpretListReverse(symbol, args, env): lst = evaluate(args[0], env) if not isinstance(lst, List): - raise Exception("'list-reverse' expects a 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) @@ -543,10 +548,10 @@ GLOBALS.register("list-reverse", Builtin(interpretListReverse, 1)) def interpretApply(symbol, args, env): func = args[0] if not isinstance(func, Symbol): - raise Exception("'apply' takes a function as its first argument") + raise InterpretPanic(symbol, "requires a symbol as its first argument", func) lst = evaluate(args[1], env) if not isinstance(lst, List): - raise Exception("'apply' takes a List as its second argument") + raise InterpretPanic(symbol, "requires a :list as its second argument". lst) new_lst = List([func] + lst.args) return evaluate(new_lst, env) @@ -555,7 +560,7 @@ GLOBALS.register("apply", Builtin(interpretApply, 2)) def interpretGlob(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): - raise Exception("'glob' expects a string") + raise InterpretPanic(symbol, "requires a :string", ev) items = glob(ev.value) return List([String(item) for item in items], True) @@ -564,7 +569,7 @@ GLOBALS.register("glob", Builtin(interpretGlob, 1)) def interpretShell(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): - raise Exception("'$' expects a 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) @@ -574,7 +579,7 @@ GLOBALS.register("$", Builtin(interpretShell, 1)) def interpretEmpty(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): - raise Exception("'empty?' expects a List") + raise InterpretPanic(symbol, "requires a :list", ev) return Bool(len(ev.args) == 0) GLOBALS.register("empty?", Builtin(interpretEmpty, 1)) @@ -582,7 +587,7 @@ GLOBALS.register("empty?", Builtin(interpretEmpty, 1)) def interpretShuf(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): - raise Exception("'shuf' expects a List") + raise InterpretPanic(symbol, "expects a :list", ev) items = ev.args[:] random.shuffle(items) return List(items, True) @@ -605,10 +610,10 @@ GLOBALS.register("block", Builtin(interpretBlock)) def interpretExit(symbol, args, env): if len(args) > 1: - raise Exception("'exit' only takes one (optional) argument") + 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 Exception("'exit' requires an :int") + raise InterpretPanic(symbol, "expects an :int", status) sys.exit(status) return List([]) @@ -617,10 +622,10 @@ GLOBALS.register("exit", Builtin(interpretExit, 0, 1)) def interpretUnlink(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): - raise Exception("'unlink' expects a string") + raise InterpretPanic(symbol, "expects a :string", ev) target_path = Path(ev.value).resolve() if not target_path.exists(): - raise Exception("'unlink' expects the target file to exist") + raise InterpretPanic(symbol, "target file does not exist", target_path) target_path.unlink() return List([]) @@ -637,10 +642,10 @@ GLOBALS.register("argv", Builtin(interpretArgv, 0)) def interpretIn(symbol, args, env): target = evaluate(args[0], env) if not isinstance(target, Literal): - raise Exception("'in?' expects a literal as its first argument") + raise InterpretPanic(symbol, "expects a :literal as its first argument", target) lst = evaluate(args[1], env) if not isinstance(lst, List): - raise Exception("'in?' expects a list as its second argument") + 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: @@ -652,9 +657,9 @@ GLOBALS.register("in?", Builtin(interpretIn, 2)) def interpretLast(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): - raise Exception("'last' expects a List") + raise InterpretPanic("'last' expects a List") if len(ev.args) == 0: - raise Exception("List is empty") + raise InterpretPanic("List is empty") return evaluate(ev.args[-1], env) GLOBALS.register("last", Builtin(interpretLast, 1)) @@ -662,20 +667,20 @@ GLOBALS.register("last", Builtin(interpretLast, 1)) def interpretJoin(symbol, args, env): lst = evaluate(args[0], env) if not isinstance(lst, List): - raise Exception("'join' expects a List as its first argument") + raise InterpretPanic(symbol, "expects a :list as its first argument", lst) target = evaluate(args[1], env) if not isinstance(target, String): - raise Exception("'join' expects a :string as its second argument") + 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 Exception("'with-write' expects at least one argument") + raise InterpretPanic(symbol, "expects at least one argument") target_file = evaluate(args[0], env) if not isinstance(target_file, String): - raise Exception("'with-write' expects a filename as its first argument") + 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([]) @@ -691,7 +696,7 @@ def interpretWrite(symbol, args, env): # write :string :filehandle line = evaluate(args[0], env) if not isinstance(line, String): - raise Exception("'write' expects a string as its first argument") + 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([]) @@ -706,7 +711,7 @@ 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 Exception("'exists?' expects a string as its first argument") + raise InterpretPanic(symbol, "expects a :string", file_or_dir) return Bool(Path(file_or_dir.value).resolve().exists()) GLOBALS.register("exists?", Builtin(interpretExists, 1)) @@ -714,9 +719,9 @@ GLOBALS.register("exists?", Builtin(interpretExists, 1)) def interpretFirstChar(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): - raise Exception("'first-char' expects a string") + raise InterpretPanic(symbol, "expects a :string", ev) if len(ev.value) == 0: - raise Exception("string is empty") + raise InterpretPanic(symbol, ":string is empty", ev) return String(ev.value[0]) GLOBALS.register("first-char", Builtin(interpretFirstChar, 1)) @@ -724,7 +729,7 @@ GLOBALS.register("first-char", Builtin(interpretFirstChar, 1)) def interpretRestChar(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, String): - raise Exception("'rest-char' expects a string") + raise InterpretPanic(symbol, "expects a string", ev) return String(ev.value[1:]) GLOBALS.register("rest-char", Builtin(interpretRestChar, 1)) @@ -732,15 +737,15 @@ GLOBALS.register("rest-char", Builtin(interpretRestChar, 1)) def interpretSlice(symbol, args, env): lst = evaluate(args[0], env) if not isinstance(lst, List): - raise Exception("'slice' expects a list as its first argument") + raise InterpretPanic(symbol, "expects a :list as its first argument", lst) idx = evaluate(args[1], env) if not isinstance(idx, Int): - raise Exception("'slice' expects an integer as its second argument") + 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 Exception("'slice' expects an integer as its third argument") + 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]) |
