from structs import Literal, Symbol, List from pathlib import Path 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 Exception(f"expected {fmt} arguments, received {len(expr.args)}") 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 Exception(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 Exception(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 Exception(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 not isinstance(expr.args[0], Symbol): raise Exception("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}") 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") for arg in args: ev = evaluate(arg, env) #if ev not in (True, False): if not isinstance(ev, Literal) and ev.value not in (True, False): raise Exception("'or' needs boolean arguments") if ev.value == True: return ev return Literal(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 Exception("'and' has at least two operands") for arg in args: ev = evaluate(arg, env) #if ev not in (True, False): if not isinstance(ev, Literal) and ev.value not in (True, False): raise Exception("'and' needs boolean arguments") if ev.value == False: return ev return Literal(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 Exception("'eq?' can only compare literals") if first.value == second.value: return Literal(True) else: return Literal(False) GLOBALS.register("eq?", Builtin(interpretEq, 2)) def interpretComparison(symbol, args, env): left = evaluate(args[0], env) if not isinstance(left, Literal) or type(left.value) not in (int, float): raise Exception("'left' must be a number") right = evaluate(args[1], env) if not isinstance(right, Literal) or type(right.value) not in (int, float): raise Exception("'right' must be a number") if symbol.name == ">": return Literal(left.value > right.value) elif symbol.name == ">=": return Literal(left.value >= right.value) elif symbol.name == "<": return Literal(left.value < right.value) elif symbol.name == "<=": return Literal(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 Exception("term has at least one operand") res = None for arg in args: ev = evaluate(arg, env) if not isinstance(ev, Literal) or type(ev.value) not in (int, float): raise Exception("term must be a number") if res is None: res = ev.value elif symbol.name == "+": res += ev.value elif symbol.name == "-": res -= ev.value return Literal(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, Literal) or type(num.value) not in (int, float): raise Exception("numerator must be a number") denom = evaluate(args[1], env) if not isinstance(denom, Literal) or type(denom.value) not in (int, float): raise Exception("denominator must be a number") return Literal(num.value / denom.value) # TODO floats and ints else: if len(args) < 2: raise Exception("'*' requires at least two operands") first = evaluate(args[0], env) if not isinstance(first, Literal) or type(first.value) not in (int, float): raise Exception("'*' operand must be a number") res = first.value for arg in args[1:]: tmp = evaluate(arg, env) if not isinstance(tmp, Literal) or type(tmp.value) not in (int, float): raise Exception("'*' operand must be a number") res = res * tmp.value return Literal(res) GLOBALS.register("*", Builtin(interpretFactor)) GLOBALS.register("/", Builtin(interpretFactor, 2)) def interpretNot(symbol, args, env): res = evaluate(args[0], env) if not isinstance(res, Literal) or res.value not in (True, False): raise Exception("'not' only works on booleans") return Literal(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, Literal) or cond.value not in (True, False): raise Exception("'if' condition must be boolean") if cond.value: return evaluate(args[1], env) elif len(args) == 3: return evaluate(args[2], env) return None # 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, Literal) or not isinstance(ev.value, str): raise Exception("can only 'print' strings") print(ev.value) return None # print returns nothing 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") 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") ev = evaluate(args[1], env) env.register(name, ev) ''' if isinstance(ev, UserFunction): env.register(name, ev) else: env.register(name, args[1]) ''' return None 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") 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") ev = evaluate(args[1], env) env.reregister(name, ev) return None GLOBALS.register("redef", Builtin(interpretRedef, 2)) def interpretLambda(symbol, args, env): if args[0].args[0] != None: func = UserFunction("", args[0].args, args[1:]) else: func = UserFunction("", [], args[1:]) return func GLOBALS.register("lambda", Builtin(interpretLambda)) def interpretToString(symbol, args, env): return Literal(str(evaluate(args[0], env).value)) 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") out = "" for arg in args: tmp = evaluate(arg, env) if not isinstance(tmp, Literal) and not isinstance(tmp.value, str): raise Exception("'concat' arguments must be strings") out += tmp.value return Literal(out) GLOBALS.register("concat", Builtin(interpretConcat)) def interpretForCount(symbol, args, env): # for-count int exprs num = evaluate(args[0], env) if not isinstance(num, Literal) or type(num.value) is not int: raise Exception("'for-count' count must be an integer") new_env = Environment(env) ret = None for idx in range(0, num.value): new_env.register("idx", Literal(idx + 1)) for arg in args[1:]: ret = evaluate(arg, new_env) 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 Exception("'for-each' expects a list") new_env = Environment(env) ret = None for item in lst.args: new_env.register("_item_", item) for arg in args[1:]: ret = evaluate(arg, new_env) return ret GLOBALS.register("for-each", Builtin(interpretForEach)) def interpretPipe(symbol, args, env): if len(args) < 2: raise Exception("'|' takes 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) return pipe GLOBALS.register("|", Builtin(interpretPipe)) def interpretBranch(symbol, args, env): if len(args) == 0: raise Exception("'branch' takes at least one expression") for arg in args: if len(arg.args) != 2: raise Exception("'branch' branches have two expressions") cond = evaluate(arg.args[0], env) # this is the condition if cond.value: return evaluate(arg.args[1], env) return None GLOBALS.register("branch", Builtin(interpretBranch)) def interpretFunc(symbol, args, env): # func (args) (exprs) if len(args) < 3: raise Exception("'func' takes a name, arguments, and at least one expression") if not isinstance(args[0], Symbol): raise Exception("'func' requires a string literal as a 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 None 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 Exception(f"no such file: {target_file}") with open(target_file, "r") as fil: data = fil.readlines() out = List([Literal(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) return Literal(out.value.strip()) GLOBALS.register("strip", Builtin(interpretStrip, 1)) # - string->int and string->float def interpretStringToInt(symbol, args, env): try: val = int(args[0].value) return Literal(val) except: raise Exception(f"can't convert {args[0].value} to an int") 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, Literal) or not isinstance(target.value, str): raise Exception("'split' expects a string") splitter = evaluate(args[1], env) if not isinstance(splitter, Literal) or not isinstance(splitter.value, str): raise Exception("'split' expects a string as it's splitter") return List(target.value.split(splitter.value), True) GLOBALS.register("split", Builtin(interpretSplit, 2)) # - get the length of a list def interpretListLength(symbol, args, env): ev = evaluate(args[0], env) if not isinstance(ev, List): raise Exception("'first' expects a List") return Literal(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 Exception("'first' expects a List") if len(ev.args) == 0: raise Exception("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 Exception("'rest' expects a List") # 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 Exception("'map' takes a function as its first argument") lst = evaluate(args[1], env) if not isinstance(lst, List): raise Exception("'map' takes a List as its second argument") out = [] for arg in lst.args: #arg_ev = evaluate(arg, env) ev = evaluate(List([func, arg]), env) #ev = evaluate(List([func, arg_ev]), env) #out.append(Literal(ev)) # TODO this is probably wrong 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 Exception("'zip' only works on lists") z2 = evaluate(args[1], env) if not isinstance(z2, List): raise Exception("'zip' only works on lists") if len(z1.args) != len(z2.args): raise Exception("'zip' expects two lists of the same size") out = [] for idx in range(len(z1.args)): #f = z1.args[idx] #if not isinstance(f, Literal): # f = evaluate(f, env) #s = z2.args[idx] #if not isinstance(s, Literal): # s = evaluate(s, env) 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 Exception("'list-reverse' expects a list") 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 Exception("'apply' takes a function as its first argument") lst = evaluate(args[1], env) #lst = args[1] if not isinstance(lst, List): raise Exception("'apply' takes a List as its second argument") new_lst = List([func] + lst.args) return evaluate(new_lst, env) GLOBALS.register("apply", Builtin(interpretApply, 2))