// Logic for getting a type from an AST node
#include <ctype.h>
#include <gc.h>
#include <glob.h>
#include <signal.h>
#include <stdarg.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>

#include "ast.h"
#include "cordhelpers.h"
#include "environment.h"
#include "parse.h"
#include "stdlib/patterns.h"
#include "stdlib/text.h"
#include "stdlib/util.h"
#include "typecheck.h"
#include "types.h"

type_t *parse_type_ast(env_t *env, type_ast_t *ast)
{
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-default"
    switch (ast->tag) {
    case VarTypeAST: {
        const char *name = Match(ast, VarTypeAST)->name;
        type_t *t = Table$str_get(*env->types, name);
        if (t) return t;
        while (strchr(name, '.')) {
            char *module_name = GC_strndup(name, strcspn(name, "."));
            binding_t *b = get_binding(env, module_name);
            if (!b || b->type->tag != ModuleType)
                code_err(ast, "I don't know a module with the name '%s'", module_name);

            env_t *imported = Table$str_get(*env->imports, Match(b->type, ModuleType)->name);
            assert(imported);
            env = imported;
            name = strchr(name, '.') + 1;
            t = Table$str_get(*env->types, name);
            if (t) return t;
        }
        code_err(ast, "I don't know a type with the name '%s'", name);
    }
    case PointerTypeAST: {
        auto ptr = Match(ast, PointerTypeAST);
        type_t *pointed_t = parse_type_ast(env, ptr->pointed);
        if (pointed_t->tag == VoidType)
            code_err(ast, "Void pointers are not supported. You probably meant 'Memory' instead of 'Void'");
        return Type(PointerType, .pointed=pointed_t, .is_view=ptr->is_view);
    }
    case ArrayTypeAST: {
        type_ast_t *item_type = Match(ast, ArrayTypeAST)->item;
        type_t *item_t = parse_type_ast(env, item_type);
        if (!item_t) code_err(item_type, "I can't figure out what this type is.");
        if (has_view_memory(item_t))
            code_err(item_type, "Arrays can't have stack references because the array may outlive the stack frame.");
        if (type_size(item_t) > ARRAY_MAX_STRIDE)
            code_err(ast, "This array holds items that take up %ld bytes, but the maximum supported size is %ld bytes. Consider using an array of pointers instead.",
                     type_size(item_t), ARRAY_MAX_STRIDE);
        return Type(ArrayType, .item_type=item_t);
    }
    case SetTypeAST: {
        type_ast_t *item_type = Match(ast, SetTypeAST)->item;
        type_t *item_t = parse_type_ast(env, item_type);
        if (!item_t) code_err(item_type, "I can't figure out what this type is.");
        if (has_view_memory(item_t))
            code_err(item_type, "Sets can't have stack references because the array may outlive the stack frame.");
        if (type_size(item_t) > ARRAY_MAX_STRIDE)
            code_err(ast, "This set holds items that take up %ld bytes, but the maximum supported size is %ld bytes. Consider using an set of pointers instead.",
                     type_size(item_t), ARRAY_MAX_STRIDE);
        return Type(SetType, .item_type=item_t);
    }
    case ChannelTypeAST: {
        type_ast_t *item_type = Match(ast, ChannelTypeAST)->item;
        type_t *item_t = parse_type_ast(env, item_type);
        if (!item_t) code_err(item_type, "I can't figure out what this type is.");
        if (!can_send_over_channel(item_t))
            code_err(ast, "This item type can't be sent over a channel because it contains reference to memory that may not be thread-safe.");
        if (type_size(item_t) > ARRAY_MAX_STRIDE)
            code_err(ast, "This channel holds items that take up %ld bytes, but the maximum supported size is %ld bytes.",
                     type_size(item_t), ARRAY_MAX_STRIDE);
        return Type(ChannelType, .item_type=item_t);
    }
    case TableTypeAST: {
        type_ast_t *key_type_ast = Match(ast, TableTypeAST)->key;
        type_t *key_type = parse_type_ast(env, key_type_ast);
        if (!key_type) code_err(key_type_ast, "I can't figure out what type this is.");
        if (has_view_memory(key_type))
            code_err(key_type_ast, "Tables can't have stack references because the array may outlive the stack frame.");
        type_ast_t *val_type_ast = Match(ast, TableTypeAST)->value;
        type_t *val_type = parse_type_ast(env, val_type_ast);
        if (!val_type) code_err(val_type_ast, "I can't figure out what type this is.");
        if (has_view_memory(val_type))
            code_err(val_type_ast, "Tables can't have stack references because the array may outlive the stack frame.");
        return Type(TableType, .key_type=key_type, .value_type=val_type);
    }
    case FunctionTypeAST: {
        auto fn = Match(ast, FunctionTypeAST);
        type_t *ret_t = fn->ret ? parse_type_ast(env, fn->ret) : Type(VoidType);
        if (has_view_memory(ret_t))
            code_err(fn->ret, "Functions are not allowed to return stack references, because the reference may no longer exist on the stack.");
        arg_t *type_args = NULL;
        for (arg_ast_t *arg = fn->args; arg; arg = arg->next) {
            type_args = new(arg_t, .name=arg->name, .next=type_args);
            if (arg->type) {
                type_args->type = parse_type_ast(env, arg->type);
            } else {
                type_args->default_val = arg->value;
                type_args->type = get_type(env, arg->value);
            }
        }
        REVERSE_LIST(type_args);
        return Type(ClosureType, Type(FunctionType, .args=type_args, .ret=ret_t));
    }
    case OptionalTypeAST: {
        auto opt = Match(ast, OptionalTypeAST);
        type_t *t = parse_type_ast(env, opt->type);
        if (t->tag == VoidType || t->tag == AbortType || t->tag == ReturnType)
            code_err(ast, "Optional %T types are not supported.", t);
        return Type(OptionalType, .type=t);
    }
    case UnknownTypeAST: code_err(ast, "I don't know how to get this type");
    }
#pragma GCC diagnostic pop
    errx(1, "Unreachable");
}

PUREFUNC type_t *get_math_type(env_t *env, ast_t *ast, type_t *lhs_t, type_t *rhs_t)
{
    (void)env;
    switch (compare_precision(lhs_t, rhs_t)) {
    case NUM_PRECISION_EQUAL: case NUM_PRECISION_MORE: return lhs_t;
    case NUM_PRECISION_LESS: return rhs_t;
    default: code_err(ast, "Math operations between %T and %T are not supported", lhs_t, rhs_t);
    }
}

static env_t *load_module(env_t *env, ast_t *module_ast)
{
    auto use = Match(module_ast, Use);
    switch (use->what) {
    case USE_LOCAL: {
        const char *resolved_path = resolve_path(use->path, module_ast->file->filename, module_ast->file->filename);
        env_t *module_env = Table$str_get(*env->imports, resolved_path);
        if (module_env)
            return module_env;

        if (!resolved_path)
            code_err(module_ast, "No such file exists: \"%s\"", use->path);

        ast_t *ast = parse_file(resolved_path, NULL);
        if (!ast) errx(1, "Could not compile file %s", resolved_path);
        return load_module_env(env, ast);
    }
    case USE_MODULE: {
        glob_t tm_files;
        if (glob(heap_strf("~/.local/share/tomo/installed/%s/[!._0-9]*.tm", use->path), GLOB_TILDE, NULL, &tm_files) != 0)
            code_err(module_ast, "Could not find library");

        env_t *module_env = fresh_scope(env);
        Table$str_set(env->imports, use->path, module_env);
        char *libname_id = Text$as_c_string(
            Text$replace(Text$from_str(use->path), Pattern("{1+ !alphanumeric}"), Text("_"), Pattern(""), false));
        module_env->libname = libname_id;
        for (size_t i = 0; i < tm_files.gl_pathc; i++) {
            const char *filename = tm_files.gl_pathv[i];
            ast_t *ast = parse_file(filename, NULL);
            if (!ast) errx(1, "Could not compile file %s", filename);
            env_t *module_file_env = fresh_scope(module_env);
            char *file_prefix = file_base_id(filename);
            module_file_env->namespace = new(namespace_t, .name=file_prefix);
            env_t *subenv = load_module_env(module_file_env, ast);
            for (int64_t j = 0; j < subenv->locals->entries.length; j++) {
                struct {
                    const char *name; binding_t *binding;
                } *entry = subenv->locals->entries.data + j*subenv->locals->entries.stride;
                set_binding(module_env, entry->name, entry->binding);
            }
        }
        globfree(&tm_files);
        return module_env;
    }
    default: return NULL;
    }
}

void prebind_statement(env_t *env, ast_t *statement)
{
    switch (statement->tag) {
    case DocTest: {
        prebind_statement(env, Match(statement, DocTest)->expr);
        break;
    }
    case StructDef: {
        auto def = Match(statement, StructDef);
        if (get_binding(env, def->name))
            code_err(statement, "A %T called '%s' has already been defined", get_binding(env, def->name)->type, def->name);

        env_t *ns_env = namespace_env(env, def->name);
        type_t *type = Type(StructType, .name=def->name, .opaque=true, .env=ns_env); // placeholder
        Table$str_set(env->types, def->name, type);
        set_binding(env, def->name, new(binding_t, .type=Type(TypeInfoType, .name=def->name, .type=type, .env=ns_env),
                                        .code=CORD_all(namespace_prefix(env, env->namespace), def->name)));
        for (ast_list_t *stmt = def->namespace ? Match(def->namespace, Block)->statements : NULL; stmt; stmt = stmt->next)
            prebind_statement(ns_env, stmt->ast);
        break;
    }
    case EnumDef: {
        auto def = Match(statement, EnumDef);
        if (get_binding(env, def->name))
            code_err(statement, "A %T called '%s' has already been defined", get_binding(env, def->name)->type, def->name);

        env_t *ns_env = namespace_env(env, def->name);
        type_t *type = Type(EnumType, .name=def->name, .opaque=true, .env=ns_env); // placeholder
        Table$str_set(env->types, def->name, type);
        set_binding(env, def->name, new(binding_t, .type=Type(TypeInfoType, .name=def->name, .type=type, .env=ns_env),
                                        .code=CORD_all(namespace_prefix(env, env->namespace), def->name)));
        for (ast_list_t *stmt = def->namespace ? Match(def->namespace, Block)->statements : NULL; stmt; stmt = stmt->next)
            prebind_statement(ns_env, stmt->ast);
        break;
    }
    case LangDef: {
        auto def = Match(statement, LangDef);
        if (get_binding(env, def->name))
            code_err(statement, "A %T called '%s' has already been defined", get_binding(env, def->name)->type, def->name);

        env_t *ns_env = namespace_env(env, def->name);
        type_t *type = Type(TextType, .lang=def->name, .env=ns_env);
        Table$str_set(env->types, def->name, type);
        set_binding(env, def->name, new(binding_t, .type=Type(TypeInfoType, .name=def->name, .type=type, .env=ns_env),
                                        .code=CORD_all(namespace_prefix(env, env->namespace), def->name)));
        for (ast_list_t *stmt = def->namespace ? Match(def->namespace, Block)->statements : NULL; stmt; stmt = stmt->next)
            prebind_statement(ns_env, stmt->ast);
        break;
    }
    default: break;
    }
}

void bind_statement(env_t *env, ast_t *statement)
{
    switch (statement->tag) {
    case DocTest: {
        bind_statement(env, Match(statement, DocTest)->expr);
        break;
    }
    case Declare: {
        auto decl = Match(statement, Declare);
        const char *name = Match(decl->var, Var)->name;
        if (streq(name, "_")) // Explicit discard
            return;
        if (get_binding(env, name))
            code_err(decl->var, "A %T called '%s' has already been defined", get_binding(env, name)->type, name);
        bind_statement(env, decl->value);
        type_t *type = get_type(env, decl->value);
        if (!type)
            code_err(decl->value, "I couldn't figure out the type of this value");
        if (type->tag == FunctionType)
            type = Type(ClosureType, type);
        CORD prefix = namespace_prefix(env, env->namespace);
        CORD code = CORD_cat(prefix ? prefix : "$", name);
        set_binding(env, name, new(binding_t, .type=type, .code=code));
        break;
    }
    case FunctionDef: {
        auto def = Match(statement, FunctionDef);
        const char *name = Match(def->name, Var)->name;
        if (get_binding(env, name))
            code_err(def->name, "A %T called '%s' has already been defined", get_binding(env, name)->type, name);
        type_t *type = get_function_def_type(env, statement);
        CORD code = CORD_all(namespace_prefix(env, env->namespace), name);
        set_binding(env, name, new(binding_t, .type=type, .code=code));
        break;
    }
    case StructDef: {
        auto def = Match(statement, StructDef);
        env_t *ns_env = namespace_env(env, def->name);
        type_t *type = Table$str_get(*env->types, def->name);
        if (!type) code_err(statement, "Couldn't find type!");
        assert(type);
        arg_t *fields = NULL;
        for (arg_ast_t *field_ast = def->fields; field_ast; field_ast = field_ast->next) {
            type_t *field_t = get_arg_ast_type(env, field_ast);
            type_t *non_opt_field_t = field_t->tag == OptionalType ? Match(field_t, OptionalType)->type : field_t;
            if ((non_opt_field_t->tag == StructType && Match(non_opt_field_t, StructType)->opaque)
                || (non_opt_field_t->tag == EnumType && Match(non_opt_field_t, EnumType)->opaque)) {
                if (non_opt_field_t == type)
                    code_err(field_ast->type, "This is a recursive struct that would be infinitely large. Maybe you meant to use an optional '@%T?' pointer instead?", type);
                else
                    code_err(field_ast->type, "I'm still in the process of defining the fields of %T, so I don't know how to use it as a member."
                             "\nTry using a @%T pointer for this field.",
                             field_t, field_t);
            }
            fields = new(arg_t, .name=field_ast->name, .type=field_t, .default_val=field_ast->value, .next=fields);
        }
        REVERSE_LIST(fields);
        type->__data.StructType.fields = fields; // populate placeholder
        type->__data.StructType.opaque = false;
        
        for (ast_list_t *stmt = def->namespace ? Match(def->namespace, Block)->statements : NULL; stmt; stmt = stmt->next) {
            bind_statement(ns_env, stmt->ast);
        }
        break;
    }
    case EnumDef: {
        auto def = Match(statement, EnumDef);
        env_t *ns_env = namespace_env(env, def->name);
        type_t *type = Table$str_get(*env->types, def->name);
        assert(type);
        tag_t *tags = NULL;
        int64_t next_tag = 1;
        for (tag_ast_t *tag_ast = def->tags; tag_ast; tag_ast = tag_ast->next) {
            arg_t *fields = NULL;
            for (arg_ast_t *field_ast = tag_ast->fields; field_ast; field_ast = field_ast->next) {
                type_t *field_t = get_arg_ast_type(env, field_ast);
                type_t *non_opt_field_t = field_t->tag == OptionalType ? Match(field_t, OptionalType)->type : field_t;
                if ((non_opt_field_t->tag == StructType && Match(non_opt_field_t, StructType)->opaque)
                    || (non_opt_field_t->tag == EnumType && Match(non_opt_field_t, EnumType)->opaque)) {
                    if (non_opt_field_t == type)
                        code_err(field_ast->type, "This is a recursive enum that would be infinitely large. Maybe you meant to use an optional '@%T?' pointer instead?", type);
                    else
                        code_err(field_ast->type, "I'm still in the process of defining the fields of %T, so I don't know how to use it as a member."
                                 "\nTry using a @%T pointer for this field.",
                                 field_t, field_t);
                }
                fields = new(arg_t, .name=field_ast->name, .type=field_t, .default_val=field_ast->value, .next=fields);
            }
            REVERSE_LIST(fields);
            env_t *member_ns = namespace_env(env, heap_strf("%s$%s", def->name, tag_ast->name));
            type_t *tag_type = Type(StructType, .name=heap_strf("%s$%s", def->name, tag_ast->name), .fields=fields, .env=member_ns);
            tags = new(tag_t, .name=tag_ast->name, .tag_value=(next_tag++), .type=tag_type, .next=tags);
        }
        REVERSE_LIST(tags);
        type->__data.EnumType.tags = tags;
        type->__data.EnumType.opaque = false;

        for (tag_t *tag = tags; tag; tag = tag->next) {
            if (Match(tag->type, StructType)->fields) { // Constructor:
                type_t *constructor_t = Type(FunctionType, .args=Match(tag->type, StructType)->fields, .ret=type);
                set_binding(ns_env, tag->name, new(binding_t, .type=constructor_t, .code=CORD_all(namespace_prefix(env, env->namespace), def->name, "$tagged$", tag->name)));
            } else { // Empty singleton value:
                CORD code = CORD_all("(", namespace_prefix(env, env->namespace), def->name, "_t){", namespace_prefix(env, env->namespace), def->name, "$tag$", tag->name, "}");
                set_binding(ns_env, tag->name, new(binding_t, .type=type, .code=code));
            }
            Table$str_set(env->types, heap_strf("%s$%s", def->name, tag->name), tag->type);
        }
        
        for (ast_list_t *stmt = def->namespace ? Match(def->namespace, Block)->statements : NULL; stmt; stmt = stmt->next) {
            bind_statement(ns_env, stmt->ast);
        }
        break;
    }
    case LangDef: {
        auto def = Match(statement, LangDef);
        env_t *ns_env = namespace_env(env, def->name);
        type_t *type = Type(TextType, .lang=def->name, .env=ns_env);
        Table$str_set(env->types, def->name, type);

        set_binding(ns_env, "without_escaping",
                    new(binding_t, .type=Type(FunctionType, .args=new(arg_t, .name="text", .type=TEXT_TYPE), .ret=type),
                        .code=CORD_all("(", namespace_prefix(env, env->namespace), def->name, "_t)")));

        for (ast_list_t *stmt = def->namespace ? Match(def->namespace, Block)->statements : NULL; stmt; stmt = stmt->next)
            bind_statement(ns_env, stmt->ast);
        break;
    }
    case Use: {
        env_t *module_env = load_module(env, statement);
        if (!module_env) break;
        for (Table_t *bindings = module_env->locals; bindings != module_env->globals; bindings = bindings->fallback) {
            for (int64_t i = 1; i <= Table$length(*bindings); i++) {
                struct {const char *name; binding_t *binding; } *entry = Table$entry(*bindings, i);
                if (entry->name[0] == '_' || streq(entry->name, "main"))
                    continue;
                binding_t *b = Table$str_get(*env->locals, entry->name);
                if (!b)
                    Table$str_set(env->locals, entry->name, entry->binding);
                else if (b != entry->binding)
                    code_err(statement, "This module imports a symbol called '%s', which would clobber another variable", entry->name);
            }
        }
        for (int64_t i = 1; i <= Table$length(*module_env->types); i++) {
            struct {const char *name; type_t *type; } *entry = Table$entry(*module_env->types, i);
            if (entry->name[0] == '_')
                continue;
            if (Table$str_get(*env->types, entry->name))
                continue;

            Table$str_set(env->types, entry->name, entry->type);
        }

        ast_t *var = Match(statement, Use)->var;
        if (var) {
            type_t *type = get_type(env, statement);
            assert(type);
            set_binding(env, Match(var, Var)->name, new(binding_t, .type=type));
        }
        break;
    }
    case Extern: {
        auto ext = Match(statement, Extern);
        type_t *t = parse_type_ast(env, ext->type);
        if (t->tag == ClosureType)
            t = Match(t, ClosureType)->fn;
        set_binding(env, ext->name, new(binding_t, .type=t, .code=ext->name));
        break;
    }
    default: break;
    }
}

type_t *get_function_def_type(env_t *env, ast_t *ast)
{
    auto fn = Match(ast, FunctionDef);
    arg_t *args = NULL;
    env_t *scope = fresh_scope(env);
    for (arg_ast_t *arg = fn->args; arg; arg = arg->next) {
        type_t *t = arg->type ? parse_type_ast(env, arg->type) : get_type(env, arg->value);
        args = new(arg_t, .name=arg->name, .type=t, .default_val=arg->value, .next=args);
        set_binding(scope, arg->name, new(binding_t, .type=t));
    }
    REVERSE_LIST(args);

    type_t *ret = fn->ret_type ? parse_type_ast(scope, fn->ret_type) : Type(VoidType);
    if (has_view_memory(ret))
        code_err(ast, "Functions can't return stack references because the reference may outlive its stack frame.");
    return Type(FunctionType, .args=args, .ret=ret);
}

type_t *get_method_type(env_t *env, ast_t *self, const char *name)
{
    binding_t *b = get_namespace_binding(env, self, name);
    if (!b || !b->type)
        code_err(self, "No such method: %T:%s(...)", get_type(env, self), name);
    return b->type;
}

type_t *get_clause_type(env_t *env, type_t *subject_t, when_clause_t *clause)
{
    assert(subject_t->tag == EnumType);
    tag_t * const tags = Match(subject_t, EnumType)->tags;

    const char *tag_name = Match(clause->tag_name, Var)->name;
    type_t *tag_type = NULL;
    for (tag_t *tag = tags; tag; tag = tag->next) {
        if (streq(tag->name, tag_name)) {
            tag_type = tag->type;
            break;
        }
    }

    if (!tag_type)
        code_err(clause->tag_name, "There is no tag '%s' for the type %T", tag_name, subject_t);

    // Don't return early so we validate the tags
    if (!clause->args)
        return get_type(env, clause->body);

    env_t *scope = fresh_scope(env);
    auto tag_struct = Match(tag_type, StructType);
    if (!clause->args->next && tag_struct->fields && tag_struct->fields->next) {
        set_binding(scope, Match(clause->args->ast, Var)->name, new(binding_t, .type=tag_type));
    } else {
        ast_list_t *var = clause->args;
        arg_t *field = tag_struct->fields;
        while (var || field) {
            if (!var)
                code_err(clause->tag_name, "The field %T.%s.%s wasn't accounted for", subject_t, tag_name, field->name);
            if (!field)
                code_err(var->ast, "This is one more field than %T has", subject_t);
            set_binding(scope, Match(var->ast, Var)->name, new(binding_t, .type=field->type));
            var = var->next;
            field = field->next;
        }
    }
    return get_type(scope, clause->body);
}

type_t *get_type(env_t *env, ast_t *ast)
{
    if (!ast) return NULL;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wswitch-default"
    switch (ast->tag) {
    case Null: {
        type_t *t = parse_type_ast(env, Match(ast, Null)->type);
        return Type(OptionalType, .type=t);
    }
    case Bool: {
        return Type(BoolType);
    }
    case Int: {
        auto i = Match(ast, Int);
        switch (i->bits) {
        case IBITS_UNSPECIFIED: return Type(BigIntType);
        case IBITS_BYTE: return Type(ByteType);
        case IBITS8: return Type(IntType, .bits=TYPE_IBITS8);
        case IBITS16: return Type(IntType, .bits=TYPE_IBITS16);
        case IBITS32: return Type(IntType, .bits=TYPE_IBITS32);
        case IBITS64: return Type(IntType, .bits=TYPE_IBITS64);
        default: errx(1, "Invalid integer bits");
        }
    }
    case Num: {
        auto n = Match(ast, Num);
        switch (n->bits) {
        case NBITS_UNSPECIFIED: case NBITS64:
            return Type(NumType, .bits=TYPE_NBITS64);
        case NBITS32:
            return Type(NumType, .bits=TYPE_NBITS32);
        default: errx(1, "Invalid num bits");
        }
    }
    case HeapAllocate: {
        type_t *pointed = get_type(env, Match(ast, HeapAllocate)->value);
        if (has_view_memory(pointed))
            code_err(ast, "Stack references cannot be moved to the heap because they may outlive the stack frame they were created in.");
        return Type(PointerType, .pointed=pointed);
    }
    case Optional: {
        ast_t *value = Match(ast, Optional)->value;
        type_t *t = get_type(env, value);
        if (t->tag == OptionalType)
            code_err(ast, "This value is already optional, it can't be converted to optional");
        return Type(OptionalType, .type=t);
    }
    case NonOptional: {
        ast_t *value = Match(ast, NonOptional)->value;
        type_t *t = get_type(env, value);
        if (t->tag != OptionalType)
            code_err(value, "This value is not optional. Only optional values can use the '!' operator.");
        return Match(t, OptionalType)->type;
    }
    case TextLiteral: return TEXT_TYPE;
    case TextJoin: {
        const char *lang = Match(ast, TextJoin)->lang;
        if (lang) {
            binding_t *b = get_binding(env, lang);
            if (!b || b->type->tag != TypeInfoType || Match(b->type, TypeInfoType)->type->tag != TextType)
                code_err(ast, "There is no text language called '%s'", lang);
            return Match(get_binding(env, lang)->type, TypeInfoType)->type;
        } else {
            return TEXT_TYPE;
        }
    }
    case Var: {
        auto var = Match(ast, Var);
        binding_t *b = get_binding(env, var->name);
        if (b) return b->type;
        code_err(ast, "I don't know what \"%s\" refers to", var->name);
    }
    case Array: {
        auto array = Match(ast, Array);
        type_t *item_type = NULL;
        if (array->item_type) {
            item_type = parse_type_ast(env, array->item_type);
        } else if (array->items) {
            for (ast_list_t *item = array->items; item; item = item->next) {
                ast_t *item_ast = item->ast;
                env_t *scope = env;
                while (item_ast->tag == Comprehension) {
                    auto comp = Match(item_ast, Comprehension);
                    scope = for_scope(
                        scope, FakeAST(For, .iter=comp->iter, .vars=comp->vars));
                    item_ast = comp->expr;
                }
                type_t *t2 = get_type(scope, item_ast);
                type_t *merged = item_type ? type_or_type(item_type, t2) : t2;
                if (!merged)
                    code_err(item->ast,
                             "This array item has type %T, which is different from earlier array items which have type %T",
                             t2,  item_type);
                item_type = merged;
            }
        } else {
            code_err(ast, "I can't figure out what type this array has because it has no members or explicit type");
        }
        if (has_view_memory(item_type))
            code_err(ast, "Arrays cannot hold stack references, because the array may outlive the stack frame the reference was created in.");
        return Type(ArrayType, .item_type=item_type);
    }
    case Set: {
        auto set = Match(ast, Set);
        type_t *item_type = NULL;
        if (set->item_type) {
            item_type = parse_type_ast(env, set->item_type);
        } else {
            for (ast_list_t *item = set->items; item; item = item->next) {
                ast_t *item_ast = item->ast;
                env_t *scope = env;
                while (item_ast->tag == Comprehension) {
                    auto comp = Match(item_ast, Comprehension);
                    scope = for_scope(
                        scope, FakeAST(For, .iter=comp->iter, .vars=comp->vars));
                    item_ast = comp->expr;
                }

                type_t *this_item_type = get_type(scope, item_ast);
                type_t *item_merged = type_or_type(item_type, this_item_type);
                if (!item_merged)
                    code_err(item_ast,
                             "This set item has type %T, which is different from earlier set items which have type %T",
                             this_item_type, item_type);
                item_type = item_merged;
            }
        }
        if (has_view_memory(item_type))
            code_err(ast, "Sets cannot hold stack references because the set may outlive the reference's stack frame.");
        return Type(SetType, .item_type=item_type);
    }
    case Channel: {
        auto channel = Match(ast, Channel);
        type_t *item_type = parse_type_ast(env, channel->item_type);
        return Type(ChannelType, .item_type=item_type);
    }
    case Table: {
        auto table = Match(ast, Table);
        type_t *key_type = NULL, *value_type = NULL;
        if (table->key_type && table->value_type) {
            key_type = parse_type_ast(env, table->key_type);
            value_type = parse_type_ast(env, table->value_type);
        } else {
            for (ast_list_t *entry = table->entries; entry; entry = entry->next) {
                ast_t *entry_ast = entry->ast;
                env_t *scope = env;
                while (entry_ast->tag == Comprehension) {
                    auto comp = Match(entry_ast, Comprehension);
                    scope = for_scope(
                        scope, FakeAST(For, .iter=comp->iter, .vars=comp->vars));
                    entry_ast = comp->expr;
                }

                auto e = Match(entry_ast, TableEntry);
                type_t *key_t = get_type(scope, e->key);
                type_t *value_t = get_type(scope, e->value);

                type_t *key_merged = key_type ? type_or_type(key_type, key_t) : key_t;
                if (!key_merged)
                    code_err(entry->ast,
                             "This table entry has type %T, which is different from earlier table entries which have type %T",
                             key_t, key_type);
                key_type = key_merged;

                type_t *val_merged = value_type ? type_or_type(value_type, value_t) : value_t;
                if (!val_merged)
                    code_err(entry->ast,
                             "This table entry has type %T, which is different from earlier table entries which have type %T",
                             value_t, value_type);
                value_type = val_merged;
            }
        }
        if (has_view_memory(key_type) || has_view_memory(value_type))
            code_err(ast, "Tables cannot hold stack references because the table may outlive the reference's stack frame.");
        return Type(TableType, .key_type=key_type, .value_type=value_type);
    }
    case TableEntry: {
        code_err(ast, "Table entries should not be typechecked directly");
    }
    case Comprehension: {
        auto comp = Match(ast, Comprehension);
        env_t *scope = for_scope(env, FakeAST(For, .iter=comp->iter, .vars=comp->vars));
        if (comp->expr->tag == Comprehension) {
            return get_type(scope, comp->expr);
        } else if (comp->expr->tag == TableEntry) {
            auto e = Match(comp->expr, TableEntry);
            return Type(TableType, .key_type=get_type(scope, e->key), .value_type=get_type(scope, e->value));
        } else {
            return Type(ArrayType, .item_type=get_type(scope, comp->expr));
        }
    }
    case FieldAccess: {
        auto access = Match(ast, FieldAccess);
        type_t *fielded_t = get_type(env, access->fielded);
        if (fielded_t->tag == ModuleType) {
            const char *name = Match(fielded_t, ModuleType)->name;
            env_t *module_env = Table$str_get(*env->imports, name);
            if (!module_env) code_err(access->fielded, "I couldn't find the environment for the module %s", name);
            return get_type(module_env, WrapAST(ast, Var, access->field));
        } else if (fielded_t->tag == TypeInfoType) {
            auto info = Match(fielded_t, TypeInfoType);
            assert(info->env);
            binding_t *b = get_binding(info->env, access->field);
            if (!b) code_err(ast, "I couldn't find the field '%s' on this type", access->field);
            return b->type;
        }
        type_t *field_t = get_field_type(fielded_t, access->field);
        if (!field_t)
            code_err(ast, "%T objects don't have a field called '%s'", fielded_t, access->field);
        return field_t;
    }
    case Index: {
        auto indexing = Match(ast, Index);
        type_t *indexed_t = get_type(env, indexing->indexed);
        if (indexed_t->tag == OptionalType && !indexing->index)
            code_err(ast, "You're attempting to dereference a value whose type indicates it could be null");

        if (indexed_t->tag == PointerType && !indexing->index)
            return Match(indexed_t, PointerType)->pointed;

        type_t *value_t = value_type(indexed_t);
        if (value_t->tag == ArrayType) {
            if (!indexing->index) return indexed_t;
            type_t *index_t = get_type(env, indexing->index);
            if (index_t->tag == IntType || index_t->tag == BigIntType)
                return Match(value_t, ArrayType)->item_type;
            code_err(indexing->index, "I only know how to index lists using integers, not %T", index_t);
        } else if (value_t->tag == TableType) {
            code_err(ast, "Tables use the table:get(key) method, not square bracket indexing like table[key]");
        } else {
            code_err(ast, "I don't know how to index %T values", indexed_t);
        }
    }
    case FunctionCall: {
        auto call = Match(ast, FunctionCall);
        type_t *fn_type_t = get_type(env, call->fn);
        if (!fn_type_t)
            code_err(call->fn, "I couldn't find this function");

        if (fn_type_t->tag == TypeInfoType) {
            type_t *t = Match(fn_type_t, TypeInfoType)->type;
            if (t->tag == StructType || t->tag == IntType || t->tag == BigIntType || t->tag == NumType
                || t->tag == TextType || t->tag == CStringType || t->tag == DateTimeType)
                return t; // Constructor
            code_err(call->fn, "This is not a type that has a constructor");
        }
        if (fn_type_t->tag == ClosureType)
            fn_type_t = Match(fn_type_t, ClosureType)->fn;
        if (fn_type_t->tag != FunctionType)
            code_err(call->fn, "This isn't a function, it's a %T", fn_type_t);
        auto fn_type = Match(fn_type_t, FunctionType);
        return fn_type->ret;
    }
    case MethodCall: {
        auto call = Match(ast, MethodCall);
        type_t *self_value_t = value_type(get_type(env, call->self));
        switch (self_value_t->tag) {
        case ArrayType: {
            type_t *item_type = Match(self_value_t, ArrayType)->item_type;
            if (streq(call->name, "binary_search")) return INT_TYPE;
            else if (streq(call->name, "by")) return self_value_t;
            else if (streq(call->name, "clear")) return Type(VoidType);
            else if (streq(call->name, "counts")) return Type(TableType, .key_type=item_type, .value_type=INT_TYPE);
            else if (streq(call->name, "find")) return Type(OptionalType, .type=INT_TYPE);
            else if (streq(call->name, "first")) return Type(OptionalType, .type=INT_TYPE);
            else if (streq(call->name, "from")) return self_value_t;
            else if (streq(call->name, "has")) return Type(BoolType);
            else if (streq(call->name, "heap_pop")) return item_type;
            else if (streq(call->name, "heap_push")) return Type(VoidType);
            else if (streq(call->name, "heapify")) return Type(VoidType);
            else if (streq(call->name, "insert")) return Type(VoidType);
            else if (streq(call->name, "insert_all")) return Type(VoidType);
            else if (streq(call->name, "random")) return item_type;
            else if (streq(call->name, "remove_at")) return Type(VoidType);
            else if (streq(call->name, "remove_item")) return Type(VoidType);
            else if (streq(call->name, "reversed")) return self_value_t;
            else if (streq(call->name, "sample")) return self_value_t;
            else if (streq(call->name, "shuffle")) return Type(VoidType);
            else if (streq(call->name, "shuffled")) return self_value_t;
            else if (streq(call->name, "sort")) return Type(VoidType);
            else if (streq(call->name, "sorted")) return self_value_t;
            else if (streq(call->name, "to")) return self_value_t;
            else if (streq(call->name, "unique")) return Type(SetType, .item_type=item_type);
            else code_err(ast, "There is no '%s' method for arrays", call->name);
        }
        case SetType: {
            if (streq(call->name, "add")) return Type(VoidType);
            else if (streq(call->name, "add_all")) return Type(VoidType);
            else if (streq(call->name, "clear")) return Type(VoidType);
            else if (streq(call->name, "has")) return Type(BoolType);
            else if (streq(call->name, "is_subset_of")) return Type(BoolType);
            else if (streq(call->name, "is_superset_of")) return Type(BoolType);
            else if (streq(call->name, "overlap")) return self_value_t;
            else if (streq(call->name, "remove")) return Type(VoidType);
            else if (streq(call->name, "remove_all")) return Type(VoidType);
            else if (streq(call->name, "with")) return self_value_t;
            else if (streq(call->name, "without")) return self_value_t;
            else code_err(ast, "There is no '%s' method for sets", call->name);
        }
        case ChannelType: {
            if (streq(call->name, "clear")) return Type(VoidType);
            else if (streq(call->name, "get")) return Match(self_value_t, ChannelType)->item_type;
            else if (streq(call->name, "give")) return Type(VoidType);
            else if (streq(call->name, "give_all")) return Type(VoidType);
            else if (streq(call->name, "peek")) return Match(self_value_t, ChannelType)->item_type;
            else if (streq(call->name, "view")) return Type(ArrayType, .item_type=Match(self_value_t, ChannelType)->item_type);
            else code_err(ast, "There is no '%s' method for arrays", call->name);
        }
        case TableType: {
            auto table = Match(self_value_t, TableType);
            if (streq(call->name, "bump")) return Type(VoidType);
            else if (streq(call->name, "clear")) return Type(VoidType);
            else if (streq(call->name, "get")) return Type(OptionalType, .type=table->value_type);
            else if (streq(call->name, "has")) return Type(BoolType);
            else if (streq(call->name, "remove")) return Type(VoidType);
            else if (streq(call->name, "set")) return Type(VoidType);
            else if (streq(call->name, "sorted")) return self_value_t;
            code_err(ast, "There is no '%s' method for %T tables", call->name, self_value_t);
        }
        default: {
            type_t *fn_type_t = get_method_type(env, call->self, call->name);
            if (!fn_type_t)
                code_err(ast, "No such method!");
            if (fn_type_t->tag != FunctionType)
                code_err(ast, "This isn't a method, it's a %T", fn_type_t);
            auto fn_type = Match(fn_type_t, FunctionType);
            return fn_type->ret;
        }
        }
    }
    case Block: {
        auto block = Match(ast, Block);
        ast_list_t *last = block->statements;
        if (!last)
            return Type(VoidType);
        while (last->next)
            last = last->next;

        // Early out if the type is knowable without any context from the block:
        switch (last->ast->tag) {
        case UpdateAssign: case Assign: case Declare: case FunctionDef: case StructDef: case EnumDef: case LangDef:
            return Type(VoidType);
        default: break;
        }

        env_t *block_env = fresh_scope(env);
        for (ast_list_t *stmt = block->statements; stmt; stmt = stmt->next) {
            bind_statement(block_env, stmt->ast);
        }
        return get_type(block_env, last->ast);
    }
    case Extern: {
        return parse_type_ast(env, Match(ast, Extern)->type);
    }
    case Declare: case Assign: case DocTest: {
        return Type(VoidType);
    }
    case Use: {
        switch (Match(ast, Use)->what) {
        case USE_LOCAL:
            return Type(ModuleType, resolve_path(Match(ast, Use)->path, ast->file->filename, ast->file->filename));
        default:
            return Type(ModuleType, Match(ast, Use)->path);
        }
    }
    case Return: {
        ast_t *val = Match(ast, Return)->value;
        // Support unqualified enum return values:
        if (env->fn_ctx && env->fn_ctx->return_type && env->fn_ctx->return_type->tag == EnumType) {
            env = fresh_scope(env);
            auto enum_ = Match(env->fn_ctx->return_type, EnumType);
            env_t *ns_env = enum_->env;
            for (tag_t *tag = enum_->tags; tag; tag = tag->next) {
                if (get_binding(env, tag->name))
                    continue;
                binding_t *b = get_binding(ns_env, tag->name);
                assert(b);
                set_binding(env, tag->name, b);
            }
        }
        return Type(ReturnType, .ret=(val ? get_type(env, val) : Type(VoidType)));
    }
    case Stop: case Skip: {
        return Type(AbortType);
    }
    case Pass: case Defer: case PrintStatement: return Type(VoidType);
    case Negative: {
        ast_t *value = Match(ast, Negative)->value;
        type_t *t = get_type(env, value);
        if (t->tag == IntType || t->tag == NumType)
            return t;

        binding_t *b = get_namespace_binding(env, value, "negative");
        if (b && b->type->tag == FunctionType) {
            auto fn = Match(b->type, FunctionType);
            if (fn->args && type_eq(t, get_arg_type(env, fn->args)) && type_eq(t, fn->ret))
                return t;
        }

        code_err(ast, "I don't know how to get the negative value of type %T", t);
    }
    case Not: {
        type_t *t = get_type(env, Match(ast, Not)->value);
        if (t->tag == IntType || t->tag == NumType || t->tag == BoolType)
            return t;
        if (t->tag == OptionalType)
            return Type(BoolType);

        ast_t *value = Match(ast, Not)->value;
        binding_t *b = get_namespace_binding(env, value, "negated");
        if (b && b->type->tag == FunctionType) {
            auto fn = Match(b->type, FunctionType);
            if (fn->args && type_eq(t, get_arg_type(env, fn->args)) && type_eq(t, fn->ret))
                return t;
        }
        code_err(ast, "I only know how to get 'not' of boolean, numeric, and optional pointer types, not %T", t);
    }
    case BinaryOp: {
        auto binop = Match(ast, BinaryOp);
        type_t *lhs_t = get_type(env, binop->lhs),
               *rhs_t = get_type(env, binop->rhs);

#define binding_works(name, self, lhs_t, rhs_t, ret_t) \
        ({ binding_t *b = get_namespace_binding(env, self, name); \
         (b && b->type->tag == FunctionType && ({ auto fn = Match(b->type, FunctionType);  \
                                                (type_eq(fn->ret, ret_t) \
                                                 && (fn->args && type_eq(fn->args->type, lhs_t)) \
                                                 && (fn->args->next && can_promote(rhs_t, fn->args->next->type))); })); })
        // Check for a binop method like plus() etc:
        switch (binop->op) {
        case BINOP_MULT: {
            if (is_numeric_type(lhs_t) && binding_works("scaled_by", binop->rhs, rhs_t, lhs_t, rhs_t))
                return rhs_t;
            else if (is_numeric_type(rhs_t) && binding_works("scaled_by", binop->lhs, lhs_t, rhs_t, lhs_t))
                return lhs_t;
            else if (type_eq(lhs_t, rhs_t) && binding_works(binop_method_names[binop->op], binop->lhs, lhs_t, rhs_t, lhs_t))
                return lhs_t;
            break;
        }
        case BINOP_PLUS: case BINOP_MINUS: case BINOP_AND: case BINOP_OR: case BINOP_XOR: {
            if (type_eq(lhs_t, rhs_t) && binding_works(binop_method_names[binop->op], binop->lhs, lhs_t, rhs_t, lhs_t))
                return lhs_t;
            break;
        }
        case BINOP_DIVIDE: case BINOP_MOD: case BINOP_MOD1: {
            if (is_numeric_type(rhs_t) && binding_works(binop_method_names[binop->op], binop->lhs, lhs_t, rhs_t, lhs_t))
                return lhs_t;
            break;
        }
        case BINOP_LSHIFT: case BINOP_RSHIFT: {
            if (is_int_type(rhs_t) && binding_works(binop_method_names[binop->op], binop->lhs, lhs_t, rhs_t, lhs_t))
                return lhs_t;
            break;
        }
        case BINOP_POWER: {
            if (is_numeric_type(rhs_t) && binding_works(binop_method_names[binop->op], binop->lhs, lhs_t, rhs_t, lhs_t))
                return lhs_t;
            break;
        }
        default: break;
        }
#undef binding_works

        switch (binop->op) {
        case BINOP_AND: {
            if (lhs_t->tag == BoolType && rhs_t->tag == BoolType) {
                return lhs_t;
            } else if (lhs_t->tag == BoolType && (rhs_t->tag == AbortType || rhs_t->tag == ReturnType)) {
                return lhs_t;
            } else if (rhs_t->tag == AbortType || rhs_t->tag == ReturnType) {
                return lhs_t;
            } else if (rhs_t->tag == OptionalType) {
                if (can_promote(lhs_t, rhs_t))
                    return rhs_t;
            } else if (lhs_t->tag == PointerType && rhs_t->tag == PointerType) {
                auto lhs_ptr = Match(lhs_t, PointerType);
                auto rhs_ptr = Match(rhs_t, PointerType);
                if (type_eq(lhs_ptr->pointed, rhs_ptr->pointed))
                    return Type(PointerType, .pointed=lhs_ptr->pointed);
            } else if ((is_int_type(lhs_t) && is_int_type(rhs_t))
                       || (lhs_t->tag == ByteType && rhs_t->tag == ByteType)) {
                return get_math_type(env, ast, lhs_t, rhs_t);
            }
            code_err(ast, "I can't figure out the type of this `and` expression because the left side is a %T, but the right side is a %T",
                         lhs_t, rhs_t);
        }
        case BINOP_OR: {
            if (lhs_t->tag == BoolType && rhs_t->tag == BoolType) {
                return lhs_t;
            } else if (lhs_t->tag == BoolType && (rhs_t->tag == AbortType || rhs_t->tag == ReturnType)) {
                return lhs_t;
            } else if ((is_int_type(lhs_t) && is_int_type(rhs_t))
                       || (lhs_t->tag == ByteType && rhs_t->tag == ByteType)) {
                return get_math_type(env, ast, lhs_t, rhs_t);
            } else if (lhs_t->tag == OptionalType) {
                if (rhs_t->tag == AbortType || rhs_t->tag == ReturnType)
                    return Match(lhs_t, OptionalType)->type;
                if (can_promote(rhs_t, lhs_t))
                    return rhs_t;
            } else if (lhs_t->tag == PointerType) {
                auto lhs_ptr = Match(lhs_t, PointerType);
                if (rhs_t->tag == AbortType || rhs_t->tag == ReturnType) {
                    return Type(PointerType, .pointed=lhs_ptr->pointed);
                } else if (rhs_t->tag == PointerType) {
                    auto rhs_ptr = Match(rhs_t, PointerType);
                    if (type_eq(rhs_ptr->pointed, lhs_ptr->pointed))
                        return Type(PointerType, .pointed=lhs_ptr->pointed);
                }
            }
            code_err(ast, "I can't figure out the type of this `or` expression because the left side is a %T, but the right side is a %T",
                         lhs_t, rhs_t);
        }
        case BINOP_XOR: {
            if (lhs_t->tag == BoolType && rhs_t->tag == BoolType) {
                return lhs_t;
            } else if ((is_int_type(lhs_t) && is_int_type(rhs_t))
                       || (lhs_t->tag == ByteType && rhs_t->tag == ByteType)) {
                return get_math_type(env, ast, lhs_t, rhs_t);
            }

            code_err(ast, "I can't figure out the type of this `xor` expression because the left side is a %T, but the right side is a %T",
                         lhs_t, rhs_t);
        }
        case BINOP_CONCAT: {
            if (!type_eq(lhs_t, rhs_t))
                code_err(ast, "The type on the left side of this concatenation doesn't match the right side: %T vs. %T",
                             lhs_t, rhs_t);
            if (lhs_t->tag == ArrayType || lhs_t->tag == TextType)
                return lhs_t;

            code_err(ast, "Only array/text value types support concatenation, not %T", lhs_t);
        }
        case BINOP_EQ: case BINOP_NE: case BINOP_LT: case BINOP_LE: case BINOP_GT: case BINOP_GE: {
            if (!can_promote(lhs_t, rhs_t) && !can_promote(rhs_t, lhs_t))
                code_err(ast, "I can't compare these two different types: %T vs %T", lhs_t, rhs_t);
            return Type(BoolType);
        }
        case BINOP_CMP:
            return Type(IntType, .bits=TYPE_IBITS32);
        case BINOP_POWER: {
            type_t *result = get_math_type(env, ast, lhs_t, rhs_t);
            if (result->tag == NumType)
                return result;
            return Type(NumType, .bits=TYPE_NBITS64);
        }
        default: {
            return get_math_type(env, ast, lhs_t, rhs_t);
        }
        }
    }

    case Reduction: {
        auto reduction = Match(ast, Reduction);
        type_t *iter_t = get_type(env, reduction->iter);

        type_t *value_t;
        type_t *iter_value_t = value_type(iter_t);
        switch (iter_value_t->tag) {
        case BigIntType: case IntType: value_t = iter_value_t; break;
        case ArrayType: value_t = Match(iter_value_t, ArrayType)->item_type; break;
        case SetType: value_t = Match(iter_value_t, SetType)->item_type; break;
        case TableType: code_err(reduction->iter, "To do a reduction over a table, please specify either .keys or .values");
        case FunctionType: case ClosureType: {
            // Iterator function
            auto fn = iter_value_t->tag == ClosureType ?
                Match(Match(iter_value_t, ClosureType)->fn, FunctionType) : Match(iter_value_t, FunctionType);
            if (fn->args)
                code_err(reduction->iter, "I expected this iterator function to not take any arguments, but it's %T", iter_value_t);
            if (fn->ret->tag != OptionalType)
                code_err(reduction->iter, "I expected this iterator function to return an optional value, but it's %T", iter_value_t);
            value_t = Match(fn->ret, OptionalType)->type;
            break;
        }
        default: code_err(reduction->iter, "I don't know how to do a reduction over %T values", iter_t);
        }

        env_t *scope = fresh_scope(env);
        set_binding(scope, "$reduction", new(binding_t, .type=value_t));
        set_binding(scope, "$iter_value", new(binding_t, .type=value_t));
        type_t *t = get_type(scope, reduction->combination);
        if (!reduction->fallback)
            return t;
        type_t *fallback_t = get_type(env, reduction->fallback);
        if (fallback_t->tag == AbortType || fallback_t->tag == ReturnType)
            return t;
        else if (can_promote(fallback_t, t))
            return t;
        else if (can_promote(t, fallback_t))
            return fallback_t;
        else
            return NULL;
    }

    case UpdateAssign:
        return Type(VoidType);

    case Min: case Max: {
        // Unsafe! These types *should* have the same fields and this saves a lot of duplicate code:
        ast_t *lhs = ast->__data.Min.lhs, *rhs = ast->__data.Min.rhs;
        // Okay safe again

        type_t *lhs_t = get_type(env, lhs), *rhs_t = get_type(env, rhs);
        type_t *t = type_or_type(lhs_t, rhs_t);
        if (!t)
            code_err(ast, "The two sides of this operation are not compatible: %T vs %T", lhs_t, rhs_t);
        return t;
    }

    case Lambda: {
        auto lambda = Match(ast, Lambda);
        arg_t *args = NULL;
        env_t *scope = fresh_scope(env); // For now, just use closed variables in scope normally
        for (arg_ast_t *arg = lambda->args; arg; arg = arg->next) {
            type_t *t = arg->type ? parse_type_ast(env, arg->type) : get_type(env, arg->value);
            args = new(arg_t, .name=arg->name, .type=t, .next=args);
            set_binding(scope, arg->name, new(binding_t, .type=t));
        }
        REVERSE_LIST(args);

        type_t *ret = get_type(scope, lambda->body);
        if (ret->tag == ReturnType)
            ret = Match(ret, ReturnType)->ret;
        if (ret->tag == AbortType)
            ret = Type(VoidType);

        if (lambda->ret_type) {
            type_t *declared = parse_type_ast(env, lambda->ret_type);
            if (can_promote(ret, declared))
                ret = declared;
            else
                code_err(ast, "This function was declared to return a value of type %T, but actually returns a value of type %T",
                         declared, ret);
        }

        if (has_view_memory(ret))
            code_err(ast, "Functions can't return stack references because the reference may outlive its stack frame.");
        return Type(ClosureType, Type(FunctionType, .args=args, .ret=ret));
    }

    case FunctionDef: case StructDef: case EnumDef: case LangDef: {
        return Type(VoidType);
    }

    case If: {
        auto if_ = Match(ast, If);
        if (!if_->else_body)
            return Type(VoidType);

        env_t *truthy_scope = env;
        env_t *falsey_scope = env;
        if (if_->condition->tag == Declare) {
            type_t *condition_type = get_type(env, Match(if_->condition, Declare)->value);
            falsey_scope = fresh_scope(env);
            bind_statement(falsey_scope, if_->condition);

            const char *varname = Match(Match(if_->condition, Declare)->var, Var)->name;
            if (!streq(varname, "_")) {
                truthy_scope = fresh_scope(env);
                set_binding(truthy_scope, varname,
                            new(binding_t, .type=Match(condition_type, OptionalType)->type));
            }
        } else if (if_->condition->tag == Var) {
            type_t *condition_type = get_type(env, if_->condition);
            if (condition_type->tag == OptionalType) {
                truthy_scope = fresh_scope(env);
                const char *varname = Match(if_->condition, Var)->name;
                set_binding(truthy_scope, varname,
                            new(binding_t, .type=Match(condition_type, OptionalType)->type));
            }
        }

        type_t *true_t = get_type(truthy_scope, if_->body);
        type_t *false_t = get_type(falsey_scope, if_->else_body);
        type_t *t_either = type_or_type(true_t, false_t);
        if (!t_either)
            code_err(if_->else_body,
                     "I was expecting this block to have a %T value (based on earlier clauses), but it actually has a %T value.",
                     true_t, false_t);
        return t_either;
    }

    case When: {
        auto when = Match(ast, When);
        type_t *subject_t = get_type(env, when->subject);
        type_t *overall_t = NULL;
        if (subject_t->tag != EnumType)
            code_err(when->subject, "'when' statements are only for enum types and optional pointers, not %T", subject_t);

        tag_t * const tags = Match(subject_t, EnumType)->tags;

        typedef struct match_s {
            tag_t *tag;
            bool handled;
            struct match_s *next;
        } match_t;
        match_t *matches = NULL;
        for (tag_t *tag = tags; tag; tag = tag->next)
            matches = new(match_t, .tag=tag, .handled=false, .next=matches);

        for (when_clause_t *clause = when->clauses; clause; clause = clause->next) {
            const char *tag_name = Match(clause->tag_name, Var)->name;
            CORD valid_tags = CORD_EMPTY;
            for (match_t *m = matches; m; m = m->next) {
                if (streq(m->tag->name, tag_name)) {
                    if (m->handled)
                        code_err(clause->tag_name, "This tag was already handled earlier");
                    m->handled = true;
                    goto found_matching_tag;
                }
                if (valid_tags) valid_tags = CORD_cat(valid_tags, ", ");
                valid_tags = CORD_cat(valid_tags, m->tag->name);
            }

            code_err(clause->tag_name, "There is no tag '%s' for the type %T (valid tags: %s)",
                     tag_name, subject_t, CORD_to_char_star(valid_tags));
          found_matching_tag:;
        }

        for (when_clause_t *clause = when->clauses; clause; clause = clause->next) {
            type_t *clause_type = get_clause_type(env, subject_t, clause);
            type_t *merged = type_or_type(overall_t, clause_type);
            if (!merged)
                code_err(clause->body, "The type of this branch is %T, which conflicts with the earlier branch type of %T",
                         clause_type, overall_t);
            overall_t = merged;
        }

        if (when->else_body) {
            bool any_unhandled = false;
            for (match_t *m = matches; m; m = m->next) {
                if (!m->handled) {
                    any_unhandled = true;
                    break;
                }
            }
            // HACK: `while when ...` is handled by the parser adding an implicit
            // `else: stop`, which has an empty source code span.
            if (!any_unhandled && when->else_body->end > when->else_body->start)
                code_err(when->else_body, "This 'else' block will never run because every tag is handled");

            type_t *else_t = get_type(env, when->else_body);
            type_t *merged = type_or_type(overall_t, else_t);
            if (!merged)
                code_err(when->else_body,
                         "I was expecting this block to have a %T value (based on earlier clauses), but it actually has a %T value.",
                         overall_t, else_t);
            return merged;
        } else {
            CORD unhandled = CORD_EMPTY;
            for (match_t *m = matches; m; m = m->next) {
                if (!m->handled)
                    unhandled = unhandled ? CORD_all(unhandled, ", ", m->tag->name) : m->tag->name;
            }
            if (unhandled)
                code_err(ast, "This 'when' statement doesn't handle the tags: %s", CORD_to_const_char_star(unhandled));
            return overall_t;
        }
    }

    case While: case For: return Type(VoidType);
    case InlineCCode: {
        auto inline_code = Match(ast, InlineCCode);
        if (inline_code->type)
            return inline_code->type;
        type_ast_t *type_ast = inline_code->type_ast;
        return type_ast ? parse_type_ast(env, type_ast) : Type(VoidType);
    }
    case DateTime: return Type(DateTimeType);
    case Unknown: code_err(ast, "I can't figure out the type of: %W", ast);
    }
#pragma GCC diagnostic pop
    code_err(ast, "I can't figure out the type of: %W", ast);
}

PUREFUNC bool is_discardable(env_t *env, ast_t *ast)
{
    switch (ast->tag) {
    case UpdateAssign: case Assign: case Declare: case FunctionDef: case StructDef: case EnumDef:
    case LangDef: case Use:
        return true;
    default: break;
    }
    type_t *t = get_type(env, ast);
    return (t->tag == VoidType || t->tag == AbortType || t->tag == ReturnType);
}

type_t *get_arg_ast_type(env_t *env, arg_ast_t *arg)
{
    assert(arg->type || arg->value);
    if (arg->type)
        return parse_type_ast(env, arg->type);
    return get_type(env, arg->value);
}

type_t *get_arg_type(env_t *env, arg_t *arg)
{
    assert(arg->type || arg->default_val);
    if (arg->type) return arg->type;
    return get_type(env, arg->default_val);
}

PUREFUNC bool can_be_mutated(env_t *env, ast_t *ast)
{
    switch (ast->tag) {
    case Var: return true;
    case FieldAccess: {
        auto access = Match(ast, FieldAccess);
        type_t *fielded_type = get_type(env, access->fielded);
        if (fielded_type->tag == PointerType) {
            return true;
        } else if (fielded_type->tag == StructType) {
            return can_be_mutated(env, access->fielded);
        } else {
            return false;
        }
    }
    case Index: {
        auto index = Match(ast, Index);
        type_t *indexed_type = get_type(env, index->indexed);
        if (indexed_type->tag == PointerType)
            return !Match(indexed_type, PointerType)->is_view;
        return can_be_mutated(env, index->indexed);
    }
    default: return false;
    }
}

type_t *parse_type_string(env_t *env, const char *str)
{
    type_ast_t *ast = parse_type_str(str);
    return ast ? parse_type_ast(env, ast) : NULL;
}

PUREFUNC bool is_constant(env_t *env, ast_t *ast)
{
    switch (ast->tag) {
    case Bool: case Num: case Null: return true;
    case Int: {
        auto info = Match(ast, Int);
        if (info->bits == IBITS_UNSPECIFIED) {
            Int_t int_val = Int$from_text(Text$from_str(info->str));
            if (int_val.small == 0) return false; // Failed to parse
            return (Int$compare_value(int_val, I(BIGGEST_SMALL_INT)) <= 0);
        }
        return true;
    }
    case TextJoin: {
        auto text = Match(ast, TextJoin);
        if (!text->children) return true; // Empty string, OK
        if (text->children->next) return false; // Concatenation, not constant
        return is_constant(env, text->children->ast);
    }
    case TextLiteral: {
        CORD literal = Match(ast, TextLiteral)->cord; 
        CORD_pos i;
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wsign-conversion"
        CORD_FOR(i, literal) {
            if (!isascii(CORD_pos_fetch(i)))
                return false; // Non-ASCII requires grapheme logic, not constant
        }
#pragma GCC diagnostic pop
        return true; // Literal ASCII string, OK
    }
    case Not: return is_constant(env, Match(ast, Not)->value);
    case Negative: return is_constant(env, Match(ast, Negative)->value);
    case BinaryOp: {
        auto binop = Match(ast, BinaryOp);
        switch (binop->op) {
        case BINOP_UNKNOWN: case BINOP_POWER: case BINOP_CONCAT: case BINOP_MIN: case BINOP_MAX: case BINOP_CMP:
            return false;
        default:
            return is_constant(env, binop->lhs) && is_constant(env, binop->rhs);
        }
    }
    case Use: return true;
    case FunctionCall: {
        // Constructors are allowed:
        auto call = Match(ast, FunctionCall);
        if (call->fn->tag != Var) return false;
        binding_t *b = get_binding(env, Match(call->fn, Var)->name);
        if (b == NULL || b->type->tag != TypeInfoType) return false;
        for (arg_ast_t *arg = call->args; arg; arg = arg->next) {
            if (!is_constant(env, arg->value))
                return false;
        }
        return true;
    }
    case InlineCCode: return true;
    default: return false;
    }
}

// vim: ts=4 sw=0 et cino=L2,l1,(0,W4,m1,\:0
