diff options
Diffstat (limited to 'neb/interpreter.py')
| -rw-r--r-- | neb/interpreter.py | 876 | 
1 files changed, 876 insertions, 0 deletions
| diff --git a/neb/interpreter.py b/neb/interpreter.py new file mode 100644 index 0000000..760b3a6 --- /dev/null +++ b/neb/interpreter.py @@ -0,0 +1,876 @@ +from .structs import * +from .exceptions import * +from .lexer import lex +from .parser import parse +from .typeclass import TypeEnum, is_subtype_of +from pathlib import Path +from glob import glob +import subprocess +import shlex +import random +import sys +import math + + +class Arg: + +    def __init__(self, name, type_, *, optional=False, lazy=False): +        self.name = name +        self.type_ = type_ +        self.optional = optional +        self.lazy = lazy + +    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 +        self.type_ = TypeEnum.ANY  # TODO no it's not + +    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, ns): +        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, ns) +            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__("<builtin>", None, callable_, args, many) + +    def call(self, expr, env, ns): +        self.arity_check(expr.args[0], expr.args[1:]) +        evaluated_args = self.evaluate_args(expr.args[0], expr.args[1:], env, ns) +        return self.body(expr.args[0], evaluated_args, env, ns) + + +class UserFunction(Function): +     +    def __init__(self, name, params, body): +        newparams, args, many = self.process_params(name, params) +        super().__init__(name, newparams, body, args, many) + +    def process_params(self, name, params): +        newparams = [] +        args = [] +        many = None +        prev_type = False +        first = True +        for param in params: +            if isinstance(param, Symbol): +                if many is not None: +                    raise NebPanic("& must be last argument") +                if param.name == "&": +                    many = Arg(param.name, TypeEnum.ANY) +                else: +                    newparams.append(param) +                    args.append(Arg(param.name, TypeEnum.ANY)) +                prev_type = False +            elif isinstance(param, Type) and not prev_type and not first: +                typ = TypeEnum.__getattr__(param.name[1:].upper()) +                if many is None: +                    args[-1].type_ = typ +                else: +                    many.type_ = typ +                prev_type = True +            else: +                raise NebPanic("invalid :func signature", param) +            first = False +        return newparams, args, many + +    def call(self, expr, env, ns): +        self.arity_check(expr.args[0], expr.args[1:]) +        evaluated_args = self.evaluate_args(expr.args[0], expr.args[1:], env, ns) +        this_env = Environment(env) +        for idx, param in enumerate(self.params): +            this_env.register(param.name, evaluated_args[idx]) + +        # if we got "many", wrap the rest in a list +        if self.many: +            this_env.register(self.many.name, List(evaluated_args[len(self.params):], True)) + +        return interpret(self.body, env=this_env, ns=ns) + +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, ns=None): +    ret = None +    for expr in exprs: +        ret = evaluate(expr, env, ns) +    return ret + +def evaluate(expr, env, ns=None): +    if isinstance(expr, Literal) or isinstance(expr, Function) or isinstance(expr, Type): +        return expr +    elif isinstance(expr, Symbol): +        if env.contains(expr.name): +            return evaluate(env.get(expr.name), env, ns) +        elif ns is not None and env.contains(f"{ns}/{expr.name}"): +            return evaluate(env.get(f"{ns}/{expr.name}"), env, ns) +        else: +            raise NebPanic(f"no such symbol: {expr}") + +    # 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, ns) +    elif ns is not None and env.contains(f"{ns}/{name}"): +        return env.get(f"{ns}/{name}").call(expr, env, ns) +    else: +        raise InterpretPanic(expr.args[0], "unable to evaluate") + +def interpretOr(symbol, args, env, ns): +    # or returns true for the first expression that returns true +    for arg in args: +        ev = evaluate(arg, env, ns) +        if not isinstance(ev, Bool): +            raise InterpretPanic(symbol, "requires :bool arguments") +        if ev.value == True: +            return ev +    return Bool(False) + +or_arg = Arg("arg", TypeEnum.BOOL, lazy=True) +GLOBALS.register("or", Builtin(interpretOr, [or_arg, or_arg], or_arg)) + +def interpretAnd(symbol, args, env, ns): +    # and returns false for the first expression that returns false +    for arg in args: +        ev = evaluate(arg, env, ns) +        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, ns): +    # 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) +GLOBALS.register("eq?", Builtin(interpretEq, [eq_arg, eq_arg])) + +def interpretGreaterThan(symbol, args, env, ns): +    return Bool(args[0].value > args[1].value) + +compare_arg = Arg("num", TypeEnum.NUMBER) +GLOBALS.register(">", Builtin(interpretGreaterThan, [compare_arg, compare_arg])) + +def interpretGreaterThanEqual(symbol, args, env, ns): +    return Bool(args[0].value >= args[1].value) + +GLOBALS.register(">=", Builtin(interpretGreaterThanEqual, [compare_arg, compare_arg])) + +def interpretLessThan(symbol, args, env, ns): +    return Bool(args[0].value < args[1].value) + +GLOBALS.register("<", Builtin(interpretLessThan, [compare_arg, compare_arg])) + +def interpretLessThanEqual(symbol, args, env, ns): +    return Bool(args[0].value <= args[1].value) + +GLOBALS.register("<=", Builtin(interpretLessThanEqual, [compare_arg, compare_arg])) + +def interpretAddition(symbol, args, env, ns): +    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) +GLOBALS.register("+", Builtin(interpretAddition, [term_arg], term_arg)) + +def interpretSubtraction(symbol, args, env, ns): +    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, ns): +    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) +GLOBALS.register("*", Builtin(interpretMultiplication, [factor_arg, factor_arg], factor_arg)) + +def interpretDivision(symbol, args, env, ns): +    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, ns): +    return Bool(not args[0].value) + +not_arg = Arg("not", TypeEnum.BOOL) +GLOBALS.register("not", Builtin(interpretNot, [not_arg])) + +def interpretIf(symbol, args, env, ns): +    if args[0].value: +        return evaluate(args[1], env, ns) +    elif len(args) == 3: +        return evaluate(args[2], env, ns) +    return List([]) + +cond = Arg("cond", TypeEnum.BOOL) +t_branch = Arg("t-branch", TypeEnum.ANY, lazy=True) +f_branch = Arg("f-branch", TypeEnum.ANY, optional=True, lazy=True) +GLOBALS.register("if", Builtin(interpretIf, [cond, t_branch, f_branch])) + +def interpretPrint(symbol, args, env, ns): +    print(args[0].value) +    return List([])  # print returns nothing + +GLOBALS.register("print", Builtin(interpretPrint, [Arg("arg", TypeEnum.STRING)])) + +def interpretDef(symbol, args, env, ns): + +    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, lazy=True) +def_val_arg = Arg("value", TypeEnum.ANY) +GLOBALS.register("def", Builtin(interpretDef, [def_name_arg, def_val_arg])) + +def interpretRedef(symbol, args, env, ns): +    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, ns): +    if len(args[0].args) != 0: +        func = UserFunction("<lambda>", args[0].args, args[1:]) +    else: +        func = UserFunction("<lambda>", [], args[1:]) +    return func + +lambda_args_arg = Arg("args", TypeEnum.ANY, lazy=True) +lambda_body_arg = Arg("body", TypeEnum.ANY, lazy=True) +GLOBALS.register("lambda", Builtin(interpretLambda, [lambda_args_arg, lambda_body_arg], lambda_body_arg)) + +def interpretToString(symbol, args, env, ns): +    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)])) + +def interpretConcat(symbol, args, env, ns): +    out = "" +    for arg in args: +        out += arg.value +    return String(out) + +string_arg = Arg("arg", TypeEnum.STRING) +GLOBALS.register("concat", Builtin(interpretConcat, [string_arg, string_arg], string_arg)) + +def interpretForCount(symbol, args, env, ns): +    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, ns) +    if ret is None: +        return List([]) +    return ret + +for_count_arg = Arg("count", TypeEnum.INT) +for_body_arg = Arg("body", TypeEnum.ANY, lazy=True) +GLOBALS.register("for-count", Builtin(interpretForCount, [for_count_arg, for_body_arg], for_body_arg)) + +def interpretForEach(symbol, args, env, ns): +    new_env = Environment(env) +    ret = None +    for item in args[0].args: +        new_env.register("_item_", evaluate(item, env, ns)) +        for arg in args[1:]: +            ret = evaluate(arg, new_env, ns) +    if ret is None: +        return List([]) +    return ret + +for_each_arg = Arg("list", TypeEnum.LIST) +GLOBALS.register("for-each", Builtin(interpretForEach, [for_each_arg, for_body_arg], for_body_arg)) + +def interpretPipe(symbol, args, env, ns): +    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, ns) +    if pipe is None: +        return List([]) +    return pipe + +# TODO +GLOBALS.register("|", Builtin(interpretPipe, 2)) + +def interpretBranch(symbol, args, env, ns): +    for arg in args: +        if len(arg.args) != 2: +            raise InterpretPanic(symbol, "each branch requires two expressions") +        cond = evaluate(arg.args[0], env, ns)  # 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, ns) +    return List([]) + +GLOBALS.register("branch", Builtin(interpretBranch, [for_body_arg], for_body_arg)) + +def interpretFunc(symbol, args, env, ns): +    if not isinstance(args[0], Symbol): +        raise InterpretPanic(symbol, "requires a :string name") +    name = args[0].name  # NOTE: we are not evaluating the name!! + +    if ns is not None: +        name = f"{ns}/{name}" + +    # compose a lambda +    func = interpretLambda(None, args[1:], env, ns) + +    env.register(name, func) +    return List([]) + +GLOBALS.register("func", Builtin(interpretFunc, [def_name_arg, lambda_args_arg, lambda_body_arg], lambda_body_arg)) + +def interpretReadLines(symbol, args, env, ns): +    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)])) + +def interpretStrip(symbol, args, env, ns): +    return String(args[0].value.strip()) + +GLOBALS.register("strip", Builtin(interpretStrip, [Arg("filename", TypeEnum.STRING)])) + +#  - string->int and string->float +def interpretStringToInt(symbol, args, env, ns): +    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)])) + +def interpretSplit(symbol, args, env, ns): +    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)], Arg("splitter", TypeEnum.STRING, optional=True))) +     +def interpretListLength(symbol, args, env, ns): +    return Int(len(args[0].args)) + +GLOBALS.register("list-length", Builtin(interpretListLength, [Arg("arg", TypeEnum.LIST)])) +     +def interpretFirst(symbol, args, env, ns): +    if len(args[0].args) == 0: +        raise InterpretPanic(symbol, "list is empty") +    return evaluate(args[0].args[0], env, ns) + +GLOBALS.register("first", Builtin(interpretFirst, [Arg("arg", TypeEnum.LIST, )])) + +def interpretRest(symbol, args, env, ns): +    # 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)])) + +def interpretMap(symbol, args, env, ns): +    func = args[0] +    if not isinstance(func, Function): +        raise InterpretPanic(symbol, "requires a :func as its first argument", func) +    lst = evaluate(args[1], env, ns) +    if not isinstance(lst, List): +        raise InterpretPanic(symbol, "requires a :list as its second argument", lst) +    out = [] +    for arg in lst.args: +        ev = func.call(List([func, arg]), env, ns) +        out.append(ev) +    return List(out, True) + +GLOBALS.register("map", Builtin(interpretMap, [Arg("func", TypeEnum.ANY), Arg("list", TypeEnum.LIST)])) + +def interpretZip(symbol, args, env, ns): +    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) +GLOBALS.register("zip", Builtin(interpretZip, [zip_arg, zip_arg])) + +def interpretList(symbol, args, env, ns): +    return List(args, True) + +GLOBALS.register("list", Builtin(interpretList, [], Arg("item", TypeEnum.ANY))) + +def interpretListReverse(symbol, args, env, ns): +    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)])) + +def interpretApply(symbol, args, env, ns): +    # 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, ns) + +GLOBALS.register("apply", Builtin(interpretApply, [Arg("func", TypeEnum.ANY, lazy=True), Arg("list", TypeEnum.LIST)])) + +def interpretGlob(symbol, args, env, ns): +    items = glob(args[0].value) +    return List([String(item) for item in items], True) + +GLOBALS.register("glob", Builtin(interpretGlob, [Arg("regex", TypeEnum.STRING)])) + +def interpretShell(symbol, args, env, ns): +    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)])) + +def interpretEmpty(symbol, args, env, ns): +    return Bool(len(args[0].args) == 0) + +GLOBALS.register("empty?", Builtin(interpretEmpty, [Arg("list", TypeEnum.LIST)])) + +def interpretShuf(symbol, args, env, ns): +    items = args[0].args[:] +    random.shuffle(items) +    return List(items, True) + +GLOBALS.register("shuf", Builtin(interpretShuf, [Arg("list", TypeEnum.LIST)])) + +def interpretIsList(symbol, args, env, ns): +    return Bool(isinstance(args[0], List)) + +GLOBALS.register("list?", Builtin(interpretIsList, [Arg("arg", TypeEnum.ANY)])) + +def interpretBlock(symbol, args, env, ns): +    ret = List([]) +    for arg in args: +        ret = evaluate(arg, env, ns) +    return ret + +block_arg = Arg("expr", TypeEnum.ANY, lazy=True) +GLOBALS.register("block", Builtin(interpretBlock, [block_arg], block_arg)) + +def interpretExit(symbol, args, env, ns): +    status = 0 if len(args) == 0 else args[0].value +    sys.exit(status) +    return List([]) + +exit_arg = Arg("status", TypeEnum.INT, optional=True) +GLOBALS.register("exit", Builtin(interpretExit, [exit_arg])) + +def interpretUnlink(symbol, args, env, ns): +    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)])) + +def interpretArgv(symbol, args, env, ns): +    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, ns): +    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) +in_list_arg = Arg("list", TypeEnum.LIST) +GLOBALS.register("in?", Builtin(interpretIn, [in_target_arg, in_list_arg])) + +def interpretLast(symbol, args, env, ns): +    if len(args[0].args) == 0: +        raise InterpretPanic("List is empty") +    return evaluate(args[0].args[-1], env, ns) + +GLOBALS.register("last", Builtin(interpretLast, [Arg("list", TypeEnum.LIST)])) + +def interpretJoin(symbol, args, env, ns): +    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) +join_string_arg = Arg("joiner", TypeEnum.STRING) +GLOBALS.register("join", Builtin(interpretJoin, [join_list_arg, join_string_arg])) + +def interpretWithWrite(symbol, args, env, ns): +    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, ns) +    return ret + +GLOBALS.register("with-write", Builtin(interpretWithWrite, [Arg("filename", TypeEnum.STRING)], Arg("exprs", TypeEnum.ANY, lazy=True))) + +def interpretWrite(symbol, args, env, ns): +    # 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), Arg("filename", TypeEnum.LIST)])) + +def interpretNewline(symbol, args, env, ns): +    return String("\n") + +GLOBALS.register("newline", Builtin(interpretNewline, [])) + +def interpretExists(symbol, args, env, ns): +    return Bool(Path(args[0].value).resolve().exists()) + +GLOBALS.register("exists?", Builtin(interpretExists, [Arg("filename", TypeEnum.STRING)])) + +def interpretFirstChar(symbol, args, env, ns): +    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)])) + +def interpretRestChar(symbol, args, env, ns): +    return String(args[0].value[1:]) + +GLOBALS.register("rest-char", Builtin(interpretRestChar, [Arg("string", TypeEnum.STRING)])) + +def interpretSlice(symbol, args, env, ns): +    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) +slice_idx_arg = Arg("idx", TypeEnum.INT) +slice_length_arg = Arg("length", TypeEnum.INT, optional=True) +GLOBALS.register("slice", Builtin(interpretSlice, [slice_list_arg, slice_idx_arg, slice_length_arg])) + +def interpretClear(symbol, args, env, ns): +    subprocess.run(["clear"]) +    return List([]) + +GLOBALS.register("clear", Builtin(interpretClear, [])) + +def interpretReadLine(symbol, args, env, ns): +    ret = input(args[0].value) +    return String(ret) + +GLOBALS.register("read-line", Builtin(interpretReadLine, [Arg("prompt", TypeEnum.STRING)])) + +def interpretReadChar(symbol, args, env, ns): +    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, ns): +    lst = args[0] +    val = args[1] +    items = lst.args[:] +    return List(items + [val], True) + +GLOBALS.register("append", Builtin(interpretAppend, [Arg("list", TypeEnum.LIST), Arg("item", TypeEnum.ANY)])) + +# TODO: this is actually for records/structs/whatever they're called +def interpretRemove(symbol, args, env, ns): +    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), Arg("key", TypeEnum.ANY)])) + +def interpretWhile(symbol, args, env, ns): +    cond = args[0] +    ret = List([]) +    while True: +        ev = evaluate(cond, env, ns) +        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, ns) +    return ret + +GLOBALS.register("while", Builtin(interpretWhile, [Arg("cond", TypeEnum.BOOL, lazy=True)], Arg("expr", TypeEnum.ANY, lazy=True))) + +def interpretUse(symbol, args, env, ns): +    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)])) + +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", TypeEnum.BOOL)])) + +def interpretHowTo(symbol, args, env, ns): +    if not isinstance(args[0], Symbol): +        raise InterpretPanic(symbol, "expects a symbol", args[0]) +    sym = env.get(args[0].name) +    print(sym.describe(args[0].name)) +    return List([]) + +GLOBALS.register("howto", Builtin(interpretHowTo, [Arg("symbol", TypeEnum.ANY, lazy=True)])) + +def interpretSymbols(symbol, args, env, ns): +    keys = list(env.environment.keys()) +    keys.sort() +    out = "" +    for idx, key in enumerate(keys): +        if idx % 6 == 0: +            print(out) +            out = f"{key}" +        else: +            out = f"{out} {key}" +    print(out) +    return List([]) + +GLOBALS.register("symbols", Builtin(interpretSymbols, [])) + +def interpretUseAs(symbol, args, env, ns): +    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)), ns=args[1].name) +    return List([]) + +GLOBALS.register("use-as", Builtin(interpretUseAs, [Arg("filename", TypeEnum.STRING), Arg("namespace", TypeEnum.ANY, lazy=True)])) + +def interpretFloor(symbol, args, env, ns): +    return Int(math.floor(args[0].value)) + +GLOBALS.register("floor", Builtin(interpretFloor, [Arg("floor", TypeEnum.NUMBER)])) + +def interpretFilter(symbol, args, env, ns): +    func = args[0] +    if not isinstance(func, Function): +        raise InterpretPanic(symbol, "requires a :func as its first argument", func) +    lst = args[1] +    out = [] +    for arg in lst.args: +        ev = func.call(List([func, arg]), env, ns) +        if not isinstance(ev, Bool): +            raise InterpretPanic(symbol, "function must return :bool", ev) +        if ev.value: +            out.append(arg) +    return List(out, True) + +GLOBALS.register("filter", Builtin(interpretFilter, [Arg("func", TypeEnum.ANY), Arg("list", TypeEnum.LIST)])) + +def interpretTypeOf(symbol, args, env, ns): +    return Type(f"{args[0].type_}") + +GLOBALS.register("typeof", Builtin(interpretTypeOf, [Arg("candidate", TypeEnum.ANY)])) | 
