pub mod air;
pub mod builder;
pub mod decision_tree;
pub mod interner;
pub mod stick_break_set;
pub mod tree;

use self::{
    air::Air,
    builder::{
        AssignmentProperties, CodeGenSpecialFuncs, CycleFunctionNames, HoistableFunction, Variant,
        cast_validator_args, convert_type_to_data, extract_constant, modify_cyclic_calls,
        modify_self_calls,
    },
    tree::{AirTree, TreePath},
};
use crate::{
    IdGenerator,
    ast::{
        AssignmentKind, BinOp, Bls12_381Point, Curve, DataTypeKey, DecoratorKind,
        FunctionAccessKey, Pattern, Span, TraceLevel, Tracing, TypedArg, TypedDataType,
        TypedFunction, TypedPattern, TypedValidator, UnOp,
    },
    builtins::PRELUDE,
    expr::TypedExpr,
    gen_uplc::{
        air::ExpectLevel,
        builder::{
            CodeGenFunction, erase_opaque_type_operations, get_generic_variant_name,
            get_src_code_by_span, known_data_to_type, monomorphize, wrap_validator_condition,
        },
    },
    line_numbers::LineNumbers,
    plutus_version::PlutusVersion,
    tipo::{
        ModuleValueConstructor, PatternConstructor, Type, TypeInfo, ValueConstructor,
        ValueConstructorVariant, check_replaceable_opaque_type, convert_opaque_type,
        find_and_replace_generics, get_arg_type_name, get_generic_id_and_type,
        lookup_data_type_by_tipo,
    },
};
use builder::{
    DISCARDED, get_constr_index_variant, introduce_name, introduce_pattern, pop_pattern,
    softcast_data_to_type_otherwise, unknown_data_to_type,
};
use decision_tree::{Assigned, CaseTest, DecisionTree, TreeGen, get_tipo_by_path};
use indexmap::IndexMap;
use interner::AirInterner;
use itertools::Itertools;
use petgraph::{Graph, algo};
use std::{collections::HashMap, rc::Rc};
use stick_break_set::{Builtins, TreeSet};
use tree::Fields;
use uplc::{
    ast::{Constant as UplcConstant, Name, NamedDeBruijn, Program, Term, Type as UplcType},
    builder::{CONSTR_FIELDS_EXPOSER, CONSTR_INDEX_EXPOSER, EXPECT_ON_LIST},
    builtins::DefaultFunction,
    machine::cost_model::ExBudget,
    optimize::{aiken_optimize_and_intern, interner::CodeGenInterner, shrinker::NO_INLINE},
};

type Otherwise = Option<AirTree>;

const DELAY_ERROR: fn() -> AirTree =
    || AirTree::anon_func(vec![], AirTree::error(Type::void(), false), true);

#[derive(Clone)]
pub struct CodeGenerator<'a> {
    #[allow(dead_code)]
    plutus_version: PlutusVersion,
    /// immutable index maps
    functions: IndexMap<&'a FunctionAccessKey, &'a TypedFunction>,
    constants: IndexMap<&'a FunctionAccessKey, &'a TypedExpr>,
    data_types: IndexMap<&'a DataTypeKey, &'a TypedDataType>,
    module_types: IndexMap<&'a str, &'a TypeInfo>,
    module_src: IndexMap<&'a str, &'a (String, LineNumbers)>,
    /// immutable option
    tracing: TraceLevel,
    /// mutable index maps that are reset
    defined_functions: IndexMap<FunctionAccessKey, ()>,
    special_functions: CodeGenSpecialFuncs,
    code_gen_functions: IndexMap<String, CodeGenFunction>,
    cyclic_functions:
        IndexMap<(FunctionAccessKey, Variant), (CycleFunctionNames, usize, FunctionAccessKey)>,
    /// mutable and reset as well
    interner: AirInterner,
    id_gen: IdGenerator,
}

impl<'a> CodeGenerator<'a> {
    pub fn data_types(&self) -> &IndexMap<&'a DataTypeKey, &'a TypedDataType> {
        &self.data_types
    }

    pub fn new(
        plutus_version: PlutusVersion,
        functions: IndexMap<&'a FunctionAccessKey, &'a TypedFunction>,
        constants: IndexMap<&'a FunctionAccessKey, &'a TypedExpr>,
        data_types: IndexMap<&'a DataTypeKey, &'a TypedDataType>,
        module_types: IndexMap<&'a str, &'a TypeInfo>,
        module_src: IndexMap<&'a str, &'a (String, LineNumbers)>,
        tracing: Tracing,
    ) -> Self {
        CodeGenerator {
            plutus_version,
            functions,
            constants,
            data_types,
            module_types,
            module_src,
            tracing: tracing.trace_level(true),
            defined_functions: IndexMap::new(),
            special_functions: CodeGenSpecialFuncs::new(),
            code_gen_functions: IndexMap::new(),
            cyclic_functions: IndexMap::new(),
            interner: AirInterner::new(),
            id_gen: IdGenerator::new(),
        }
    }

    pub fn reset(&mut self, reset_special_functions: bool) {
        self.code_gen_functions = IndexMap::new();
        self.defined_functions = IndexMap::new();
        self.cyclic_functions = IndexMap::new();
        self.interner = AirInterner::new();
        self.id_gen = IdGenerator::new();
        if reset_special_functions {
            self.special_functions = CodeGenSpecialFuncs::new();
        }
    }

    pub fn generate(&mut self, validator: &TypedValidator, module_name: &str) -> Program<Name> {
        let context_name = "__context__".to_string();
        let context_name_interned = introduce_name(&mut self.interner, &context_name);
        validator.params.iter().for_each(|arg| {
            arg.get_variable_name()
                .iter()
                .for_each(|arg_name| self.interner.intern(arg_name.to_string()))
        });

        let air_tree_fun = wrap_validator_condition(
            self.build(&validator.into_script_context_handler(), module_name, &[]),
            self.tracing,
        );

        let air_tree_fun = AirTree::anon_func(vec![context_name_interned], air_tree_fun, true);

        let validator_args_tree = AirTree::no_op(air_tree_fun);

        let full_tree = self.hoist_functions_to_validator(validator_args_tree);

        // optimizations on air tree

        let full_vec = full_tree.to_vec();

        let term = self.uplc_code_gen(full_vec);

        let term = cast_validator_args(term, &validator.params, &self.interner, &self.data_types);

        self.interner.pop_text(context_name);
        validator.params.iter().for_each(|arg| {
            arg.get_variable_name()
                .iter()
                .for_each(|arg_name| self.interner.pop_text(arg_name.to_string()))
        });

        self.finalize(term)
    }

    pub fn generate_raw(
        &mut self,
        body: &TypedExpr,
        args: &[TypedArg],
        module_name: &str,
    ) -> Program<Name> {
        args.iter().for_each(|arg| {
            arg.get_variable_name()
                .iter()
                .for_each(|arg_name| self.interner.intern(arg_name.to_string()))
        });

        let mut air_tree = self.build(body, module_name, &[]);

        air_tree = AirTree::no_op(air_tree);

        let full_tree = self.hoist_functions_to_validator(air_tree);

        // optimizations on air tree
        let full_vec = full_tree.to_vec();

        let mut term = self.uplc_code_gen(full_vec);

        term = if args.is_empty() {
            term
        } else {
            cast_validator_args(term, args, &self.interner, &self.data_types)
        };

        args.iter().for_each(|arg| {
            arg.get_variable_name()
                .iter()
                .for_each(|arg_name| self.interner.pop_text(arg_name.to_string()))
        });

        self.finalize(term)
    }

    fn new_program<T>(&self, term: Term<T>) -> Program<T> {
        let version = match self.plutus_version {
            PlutusVersion::V1 | PlutusVersion::V2 => (1, 0, 0),
            PlutusVersion::V3 => (1, 1, 0),
        };

        Program { version, term }
    }

    fn finalize(&mut self, mut term: Term<Name>) -> Program<Name> {
        term = self.special_functions.apply_used_functions(term);

        let program = aiken_optimize_and_intern(self.new_program(term));

        // This is very important to call here.
        // If this isn't done, re-using the same instance
        // of the generator will result in free unique errors
        // among other unpredictable things. In fact,
        // switching to a shared code generator caused some
        // instability issues and we fixed it by placing this
        // method here.
        self.reset(true);

        program
    }

    // TODO: pass mono types to build so monomorphization
    // happens as we build the AIR Tree rather than after
    fn build(
        &mut self,
        body: &TypedExpr,
        module_build_name: &str,
        context: &[TypedExpr],
    ) -> AirTree {
        if !context.is_empty() {
            let TypedExpr::Assignment {
                location,
                tipo,
                value,
                pattern,
                kind,
                comment,
            } = body
            else {
                panic!("Dangling expressions without an assignment")
            };

            let air_value = self.build(value, module_build_name, &[]);

            let otherwise_delayed = {
                let msg = match (self.tracing, kind) {
                    (TraceLevel::Silent, _) | (_, AssignmentKind::Let { .. }) => "".to_string(),
                    (TraceLevel::Compact, _) => {
                        match comment.as_ref().and_then(|s| s.split(":").next()) {
                            None => "".to_string(),
                            Some(label) => format!("<expected> {label}"),
                        }
                    }
                    (TraceLevel::Verbose, _) => match comment.as_ref() {
                        None => get_src_code_by_span(module_build_name, location, &self.module_src),
                        Some(comment) => format!("<expected> {comment}"),
                    },
                };

                let msg_func_name = msg.split_whitespace().join("");

                if msg_func_name.is_empty() {
                    None
                } else {
                    self.special_functions.insert_new_function(
                        msg_func_name.clone(),
                        Term::Error.delayed_trace(Term::string(msg)).delay(),
                        Type::void(),
                    );

                    Some(self.special_functions.use_function_tree(msg_func_name))
                }
            };

            // Intern vars from pattern here
            introduce_pattern(&mut self.interner, pattern);

            let (then, context) = context.split_first().unwrap();

            let then = self.build(then, module_build_name, context);

            let tree = self.assignment(
                pattern,
                air_value,
                then,
                tipo,
                AssignmentProperties {
                    value_type: value.tipo(),
                    kind: *kind,
                    remove_unused: kind.is_let(),
                    full_check: !tipo.is_data() && value.tipo().is_data() && kind.is_expect(),
                    otherwise: otherwise_delayed,
                },
            );

            // Now pop off interned pattern
            pop_pattern(&mut self.interner, pattern);

            tree
        } else {
            match body {
                TypedExpr::Assignment { .. } => {
                    panic!("Reached assignment with no dangling expressions")
                }
                TypedExpr::UInt { value, .. } => AirTree::int(value),
                TypedExpr::String { value, .. } => AirTree::string(value),
                TypedExpr::ByteArray { bytes, .. } => AirTree::byte_array(bytes.clone()),
                TypedExpr::Sequence { expressions, .. }
                | TypedExpr::Pipeline { expressions, .. } => {
                    let (expr, dangling_expressions) = expressions
                        .split_first()
                        .expect("Sequence or Pipeline should have at least one expression");
                    self.build(expr, module_build_name, dangling_expressions)
                }

                TypedExpr::Var {
                    constructor, name, ..
                } => match constructor.variant {
                    ValueConstructorVariant::LocalVariable { .. } => {
                        if name != CONSTR_INDEX_EXPOSER && name != CONSTR_FIELDS_EXPOSER {
                            AirTree::var(
                                constructor.clone(),
                                self.interner.lookup_interned(name),
                                "",
                            )
                        } else {
                            AirTree::var(constructor.clone(), name, "")
                        }
                    }
                    _ => AirTree::var(constructor.clone(), name, ""),
                },

                TypedExpr::Fn { args, body, .. } => {
                    let params = args
                        .iter()
                        .map(|arg| {
                            arg.get_variable_name()
                                .map(|arg| introduce_name(&mut self.interner, &arg.to_string()))
                                .unwrap_or_else(|| DISCARDED.to_string())
                        })
                        .collect_vec();

                    let anon =
                        AirTree::anon_func(params, self.build(body, module_build_name, &[]), false);

                    args.iter()
                        .filter_map(|arg| arg.get_variable_name())
                        .for_each(|arg| {
                            self.interner.pop_text(arg.to_string());
                        });

                    anon
                }

                TypedExpr::List {
                    tipo,
                    elements,
                    tail,
                    ..
                } => AirTree::list(
                    elements
                        .iter()
                        .map(|elem| self.build(elem, module_build_name, &[]))
                        .collect_vec(),
                    tipo.clone(),
                    tail.as_ref()
                        .map(|tail| self.build(tail, module_build_name, &[])),
                ),

                TypedExpr::Call {
                    tipo, fun, args, ..
                } => match fun.as_ref() {
                    TypedExpr::Var {
                        constructor:
                            ValueConstructor {
                                variant:
                                    ValueConstructorVariant::Record {
                                        name: constr_name, ..
                                    },
                                tipo: constr_tipo,
                                ..
                            },
                        ..
                    }
                    | TypedExpr::ModuleSelect {
                        constructor:
                            ModuleValueConstructor::Record {
                                name: constr_name,
                                tipo: constr_tipo,
                                ..
                            },
                        ..
                    } => {
                        let data_type = lookup_data_type_by_tipo(&self.data_types, tipo)
                            .unwrap_or_else(||
                                panic!(
                                    "Creating a record of type {:?} with no record definition. Known definitions: {:?}",
                                    tipo.to_pretty(0),
                                    self.data_types.keys()
                                )
                            );

                        let err = &format!("Missing constr variant {constr_name}");
                        let (constr_index, _) =
                            get_constr_index_variant(&data_type, constr_name).expect(err);

                        let constr_args = args
                            .iter()
                            .zip(constr_tipo.arg_types().unwrap())
                            .map(|(arg, tipo)| {
                                if tipo.is_data() {
                                    AirTree::cast_to_data(
                                        self.build(&arg.value, module_build_name, &[]),
                                        arg.value.tipo(),
                                    )
                                } else {
                                    self.build(&arg.value, module_build_name, &[])
                                }
                            })
                            .collect_vec();

                        let index = if data_type
                            .decorators
                            .iter()
                            .any(|dec| matches!(dec.kind, DecoratorKind::List))
                        {
                            None
                        } else {
                            Some(constr_index)
                        };

                        AirTree::create_constr(index, constr_tipo.clone(), constr_args)
                    }

                    TypedExpr::Var {
                        constructor:
                            ValueConstructor {
                                variant: ValueConstructorVariant::ModuleFn { builtin, .. },
                                ..
                            },
                        ..
                    } => {
                        let fun_arg_types = fun
                            .tipo()
                            .arg_types()
                            .expect("Expected a function type with arguments");

                        assert!(args.len() == fun_arg_types.len());

                        let func_args = args
                            .iter()
                            .zip(fun_arg_types)
                            .map(|(arg, arg_tipo)| {
                                let mut arg_val = self.build(&arg.value, module_build_name, &[]);
                                if arg_tipo.is_data() && !arg.value.tipo().is_data() {
                                    arg_val = AirTree::cast_to_data(arg_val, arg.value.tipo())
                                }
                                arg_val
                            })
                            .collect_vec();

                        if let Some(func) = builtin {
                            AirTree::builtin(*func, tipo.clone(), func_args)
                        } else {
                            AirTree::call(
                                self.build(fun.as_ref(), module_build_name, &[]),
                                tipo.clone(),
                                func_args,
                            )
                        }
                    }

                    TypedExpr::ModuleSelect {
                        module_name,
                        constructor: ModuleValueConstructor::Fn { name, .. },
                        ..
                    } => {
                        let type_info = self.module_types.get(module_name.as_str()).unwrap();
                        let value = type_info.values.get(name).unwrap();

                        let ValueConstructorVariant::ModuleFn { builtin, .. } = &value.variant
                        else {
                            unreachable!("Missing module function definition")
                        };

                        let fun_arg_types = fun
                            .tipo()
                            .arg_types()
                            .expect("Expected a function type with arguments");

                        assert!(args.len() == fun_arg_types.len());

                        let func_args = args
                            .iter()
                            .zip(fun_arg_types)
                            .map(|(arg, arg_tipo)| {
                                let mut arg_val = self.build(&arg.value, module_build_name, &[]);

                                if arg_tipo.is_data() && !arg.value.tipo().is_data() {
                                    arg_val = AirTree::cast_to_data(arg_val, arg.value.tipo())
                                }
                                arg_val
                            })
                            .collect_vec();

                        if let Some(func) = builtin {
                            AirTree::builtin(*func, tipo.clone(), func_args)
                        } else {
                            AirTree::call(
                                self.build(fun.as_ref(), module_build_name, &[]),
                                tipo.clone(),
                                func_args,
                            )
                        }
                    }
                    _ => {
                        let fun_arg_types = fun
                            .tipo()
                            .arg_types()
                            .expect("Expected a function type with arguments");

                        assert!(args.len() == fun_arg_types.len());

                        let func_args = args
                            .iter()
                            .zip(fun_arg_types)
                            .map(|(arg, arg_tipo)| {
                                let mut arg_val = self.build(&arg.value, module_build_name, &[]);
                                if arg_tipo.is_data() && !arg.value.tipo().is_data() {
                                    arg_val = AirTree::cast_to_data(arg_val, arg.value.tipo())
                                }
                                arg_val
                            })
                            .collect_vec();

                        AirTree::call(
                            self.build(fun.as_ref(), module_build_name, &[]),
                            tipo.clone(),
                            func_args,
                        )
                    }
                },
                TypedExpr::BinOp {
                    name,
                    left,
                    right,
                    tipo,
                    ..
                } => AirTree::binop(
                    *name,
                    tipo.clone(),
                    self.build(left, module_build_name, &[]),
                    self.build(right, module_build_name, &[]),
                    left.tipo(),
                    right.tipo(),
                ),

                TypedExpr::Trace {
                    tipo, then, text, ..
                } => AirTree::trace(
                    self.build(text, module_build_name, &[]),
                    tipo.clone(),
                    self.build(then, module_build_name, &[]),
                ),

                TypedExpr::When {
                    subject,
                    clauses,
                    tipo,
                    ..
                } => {
                    if clauses.is_empty() {
                        unreachable!("We should have one clause at least")
                    } else if clauses.len() == 1 {
                        let subject_val = self.build(subject, module_build_name, &[]);

                        let last_clause = &clauses[0];

                        // Intern vars from pattern here
                        introduce_pattern(&mut self.interner, &last_clause.pattern);

                        let clause_then = self.build(&last_clause.then, module_build_name, &[]);

                        let subject_type = subject.tipo();

                        let tree = self.assignment(
                            &last_clause.pattern,
                            subject_val,
                            clause_then,
                            &subject_type,
                            AssignmentProperties {
                                value_type: subject.tipo(),
                                kind: AssignmentKind::let_(),
                                remove_unused: false,
                                full_check: false,
                                otherwise: None,
                            },
                        );

                        // Now pop off interned pattern
                        pop_pattern(&mut self.interner, &last_clause.pattern);

                        tree
                    } else {
                        let subject_name = format!(
                            "__subject_var_span_{}_{}",
                            subject.location().start,
                            subject.location().end
                        );

                        self.interner.intern(subject_name.clone());

                        let subject_name_interned = self.interner.lookup_interned(&subject_name);

                        let wild_card = TypedPattern::Discard {
                            name: "".to_string(),
                            location: Span::empty(),
                        };

                        let tree_gen =
                            TreeGen::new(&mut self.interner, &self.data_types, &wild_card);

                        let tree = tree_gen.build_tree(&subject.tipo(), clauses);

                        let stick_set = TreeSet::new();

                        let clauses = self.handle_decision_tree(
                            &subject_name_interned,
                            subject.tipo(),
                            tipo.clone(),
                            module_build_name,
                            tree,
                            stick_set,
                        );

                        self.interner.pop_text(subject_name);

                        AirTree::let_assignment(
                            subject_name_interned,
                            self.build(subject, module_build_name, &[]),
                            clauses,
                        )
                    }
                }

                TypedExpr::If {
                    branches,
                    final_else,
                    tipo,
                    ..
                } => {
                    branches.iter().rfold(
                        self.build(final_else, module_build_name, &[]),
                        |acc, branch| {
                            let condition = self.build(&branch.condition, module_build_name, &[]);

                            match &branch.is {
                                Some((pattern, tipo)) => {
                                    introduce_pattern(&mut self.interner, pattern);
                                    self.interner.intern("acc_var".to_string());

                                    let body = self.build(&branch.body, module_build_name, &[]);

                                    let acc_var =
                                        self.interner.lookup_interned(&"acc_var".to_string());

                                    let tree = AirTree::let_assignment(
                                        &acc_var,
                                        // use anon function as a delay to avoid evaluating the acc
                                        AirTree::anon_func(vec![], acc, true),
                                        self.assignment(
                                            pattern,
                                            condition,
                                            body,
                                            tipo,
                                            AssignmentProperties {
                                                value_type: branch.condition.tipo(),
                                                kind: AssignmentKind::Expect { backpassing: () },
                                                remove_unused: false,
                                                full_check: true,
                                                otherwise: Some(AirTree::local_var(
                                                    &acc_var,
                                                    tipo.clone(),
                                                )),
                                            },
                                        ),
                                    );

                                    pop_pattern(&mut self.interner, pattern);
                                    self.interner.pop_text("acc_var".to_string());

                                    tree
                                }
                                None => AirTree::if_branch(
                                    tipo.clone(),
                                    condition,
                                    self.build(&branch.body, module_build_name, &[]),
                                    acc,
                                ),
                            }
                        },
                    )
                }
                TypedExpr::RecordAccess {
                    tipo,
                    index,
                    record,
                    ..
                } => {
                    assert!(
                        !record.tipo().is_pair(),
                        "illegal record access on a Pair. This should have been a pair-index access."
                    );

                    if check_replaceable_opaque_type(&record.tipo(), &self.data_types) {
                        self.build(record, module_build_name, &[])
                    } else {
                        let function_name = format!("__access_index_{}", *index);

                        if self.code_gen_functions.get(&function_name).is_none() {
                            let mut body = AirTree::local_var("__fields", Type::list(Type::data()));

                            for _ in 0..*index {
                                body = AirTree::builtin(
                                    DefaultFunction::TailList,
                                    Type::list(Type::data()),
                                    vec![body],
                                )
                            }

                            body = AirTree::builtin(
                                DefaultFunction::HeadList,
                                Type::data(),
                                vec![body],
                            );

                            self.code_gen_functions.insert(
                                function_name.clone(),
                                CodeGenFunction::Function {
                                    body,
                                    params: vec!["__fields".to_string()],
                                },
                            );
                        }

                        let err =
                            format!("Missing record data type for type: {:#?}", record.tipo());

                        let record_data_type =
                            lookup_data_type_by_tipo(self.data_types(), &record.tipo())
                                .expect(&err);

                        let list_of_fields = if record_data_type
                            .decorators
                            .iter()
                            .any(|dec| matches!(dec.kind, DecoratorKind::List))
                        {
                            self.build(record, module_build_name, &[])
                        } else {
                            AirTree::call(
                                self.special_functions
                                    .use_function_tree(CONSTR_FIELDS_EXPOSER.to_string()),
                                Type::list(Type::data()),
                                vec![self.build(record, module_build_name, &[])],
                            )
                        };

                        AirTree::index_access(function_name, tipo.clone(), list_of_fields)
                    }
                }

                TypedExpr::ModuleSelect {
                    tipo,
                    module_name,
                    constructor,
                    ..
                } => match constructor {
                    ModuleValueConstructor::Record {
                        name,
                        arity,
                        tipo,
                        field_map,
                        ..
                    } => {
                        let val_constructor = {
                            let data_type = lookup_data_type_by_tipo(&self.data_types, tipo);

                            ValueConstructor::public(
                                tipo.clone(),
                                ValueConstructorVariant::Record {
                                    name: name.clone(),
                                    arity: *arity,
                                    field_map: field_map.clone(),
                                    location: Span::empty(),
                                    module: module_name.clone(),
                                    constructors_count: data_type
                                        .expect("Created a module type without a definition?")
                                        .constructors
                                        .len()
                                        as u16,
                                },
                            )
                        };

                        AirTree::var(val_constructor, name, "")
                    }
                    ModuleValueConstructor::Fn { name, module, .. } => {
                        let func = self.functions.get(&FunctionAccessKey {
                            // NOTE: This is needed because we register prelude functions under an
                            // empty module name. This is to facilitate their access when used
                            // directly. Note that, if we weren't doing this particular
                            // transformation, we would need to do the other direction anyway:
                            //
                            //     if module_name.is_empty() { PRELUDE.to_string() } else { module_name.clone() }
                            //
                            // So either way, we need to take care of this.
                            module_name: if module_name == PRELUDE {
                                String::new()
                            } else {
                                module_name.clone()
                            },
                            function_name: name.clone(),
                        });

                        let type_info = self.module_types.get(module_name.as_str()).unwrap();

                        let value = type_info.values.get(name).unwrap();

                        if let Some(_func) = func {
                            AirTree::var(
                                ValueConstructor::public(tipo.clone(), value.variant.clone()),
                                format!("{module}_{name}"),
                                "",
                            )
                        } else {
                            let ValueConstructorVariant::ModuleFn {
                                builtin: Some(builtin),
                                ..
                            } = &value.variant
                            else {
                                unreachable!("Didn't find the function definition.")
                            };

                            AirTree::builtin(*builtin, tipo.clone(), vec![])
                        }
                    }
                    ModuleValueConstructor::Constant { module, name, .. } => {
                        let type_info = self.module_types.get(module_name.as_str()).unwrap();

                        let value = type_info.values.get(name).unwrap();

                        AirTree::var(
                            ValueConstructor::public(tipo.clone(), value.variant.clone()),
                            format!("{module}_{name}"),
                            "",
                        )
                    }
                },

                TypedExpr::Pair { tipo, fst, snd, .. } => AirTree::pair(
                    self.build(fst, module_build_name, &[]),
                    self.build(snd, module_build_name, &[]),
                    tipo.clone(),
                ),

                TypedExpr::Tuple { tipo, elems, .. } => AirTree::tuple(
                    elems
                        .iter()
                        .map(|elem| self.build(elem, module_build_name, &[]))
                        .collect_vec(),
                    tipo.clone(),
                ),

                TypedExpr::TupleIndex {
                    index, tuple, tipo, ..
                } => {
                    if tuple.tipo().is_pair() {
                        AirTree::pair_index(
                            *index,
                            tipo.clone(),
                            self.build(tuple, module_build_name, &[]),
                        )
                    } else {
                        let function_name = format!("__access_index_{}", *index);

                        if self.code_gen_functions.get(&function_name).is_none() {
                            let mut body = AirTree::local_var("__fields", Type::list(Type::data()));

                            for _ in 0..*index {
                                body = AirTree::builtin(
                                    DefaultFunction::TailList,
                                    Type::list(Type::data()),
                                    vec![body],
                                )
                            }

                            body = AirTree::builtin(
                                DefaultFunction::HeadList,
                                Type::data(),
                                vec![body],
                            );

                            self.code_gen_functions.insert(
                                function_name.clone(),
                                CodeGenFunction::Function {
                                    body,
                                    params: vec!["__fields".to_string()],
                                },
                            );
                        }

                        AirTree::index_access(
                            function_name,
                            tipo.clone(),
                            self.build(tuple, module_build_name, &[]),
                        )
                    }
                }

                TypedExpr::ErrorTerm { tipo, .. } => AirTree::error(tipo.clone(), false),

                TypedExpr::RecordUpdate {
                    tipo, spread, args, ..
                } => {
                    let mut index_types = vec![];
                    let mut update_args = vec![];

                    let mut highest_index = 0;

                    for arg in args
                        .iter()
                        .sorted_by(|arg1, arg2| arg1.index.cmp(&arg2.index))
                    {
                        let arg_val = self.build(&arg.value, module_build_name, &[]);

                        if arg.index > highest_index {
                            highest_index = arg.index;
                        }

                        index_types.push((arg.index, arg.value.tipo()));
                        update_args.push(arg_val);
                    }

                    AirTree::record_update(
                        index_types,
                        highest_index,
                        tipo.clone(),
                        self.build(spread, module_build_name, &[]),
                        update_args,
                    )
                }
                TypedExpr::UnOp { value, op, .. } => {
                    AirTree::unop(*op, self.build(value, module_build_name, &[]))
                }
                TypedExpr::CurvePoint { point, .. } => AirTree::curve(*point.as_ref()),
            }
        }
    }

    pub fn assignment(
        &mut self,
        pattern: &TypedPattern,
        value: AirTree,
        then: AirTree,
        tipo: &Rc<Type>,
        props: AssignmentProperties,
    ) -> AirTree {
        assert!(
            match &value {
                AirTree::Var { name, .. } if props.kind.is_let() => {
                    name != DISCARDED
                }
                _ => true,
            },
            "No discard expressions or let bindings should be in the tree at this point."
        );

        // Cast value to or from data so we don't have to worry from this point onward
        let assign_casted_value = |name, value, then| {
            if props.value_type.is_data() && props.kind.is_expect() && !tipo.is_data() {
                if let Some(otherwise) = props.otherwise.as_ref() {
                    AirTree::soft_cast_assignment(
                        name,
                        tipo.clone(),
                        value,
                        then,
                        otherwise.clone(),
                    )
                } else {
                    AirTree::let_assignment(
                        name,
                        AirTree::cast_from_data(value, tipo.clone(), true),
                        then,
                    )
                }
            } else if !props.value_type.is_data() && tipo.is_data() {
                AirTree::let_assignment(
                    name,
                    AirTree::cast_to_data(value, props.value_type.clone()),
                    then,
                )
            } else {
                AirTree::let_assignment(name, value, then)
            }
        };

        let otherwise = match &props.otherwise {
            Some(x) => x.clone(),
            // (delay (error ))
            None => DELAY_ERROR(),
        };

        match pattern {
            Pattern::Int {
                value: expected_int,
                location,
                ..
            } => {
                let name = format!(
                    "__expected_by_{}_span_{}_{}",
                    expected_int, location.start, location.end
                );

                let expect = AirTree::binop(
                    BinOp::Eq,
                    Type::bool(),
                    AirTree::int(expected_int),
                    AirTree::local_var(&name, Type::int()),
                    Type::int(),
                    Type::int(),
                );

                assign_casted_value(
                    name,
                    value,
                    AirTree::assert_bool(true, expect, then, otherwise),
                )
            }

            Pattern::ByteArray {
                value: expected_bytes,
                location,
                ..
            } => {
                let name = format!("__expected_bytes_span_{}_{}", location.start, location.end);

                let expect = AirTree::binop(
                    BinOp::Eq,
                    Type::bool(),
                    AirTree::byte_array(expected_bytes.clone()),
                    AirTree::local_var(&name, Type::byte_array()),
                    Type::byte_array(),
                    Type::byte_array(),
                );

                assign_casted_value(
                    name,
                    value,
                    AirTree::assert_bool(true, expect, then, otherwise),
                )
            }

            Pattern::Var { name, .. } => {
                let name = self.interner.lookup_interned(name);

                if props.full_check {
                    let mut index_map = IndexMap::new();

                    let non_opaque_tipo = convert_opaque_type(tipo, &self.data_types, true);

                    let val = AirTree::local_var(&name, tipo.clone());

                    if non_opaque_tipo.is_primitive() {
                        assign_casted_value(name.clone(), value, then)
                    } else {
                        assign_casted_value(
                            name,
                            value,
                            self.expect_type_assign(
                                &non_opaque_tipo,
                                val,
                                &mut index_map,
                                pattern.location(),
                                then,
                                props.otherwise.clone(),
                            ),
                        )
                    }
                } else {
                    assign_casted_value(name.clone(), value, then)
                }
            }

            Pattern::Assign { name, pattern, .. } => {
                let name = self.interner.lookup_interned(name);

                let inner_pattern = self.assignment(
                    pattern,
                    AirTree::local_var(&name, tipo.clone()),
                    then,
                    tipo,
                    AssignmentProperties {
                        value_type: tipo.clone(),
                        kind: props.kind,
                        remove_unused: props.remove_unused,
                        full_check: props.full_check,
                        otherwise: props.otherwise.clone(),
                    },
                );

                assign_casted_value(name, value, inner_pattern)
            }

            Pattern::Discard { name, .. } => {
                if props.full_check {
                    let name = format!("__discard_expect_{name}");

                    let name_interned = introduce_name(&mut self.interner, &name);

                    let mut index_map = IndexMap::new();

                    let non_opaque_tipo = convert_opaque_type(tipo, &self.data_types, true);

                    let val = AirTree::local_var(&name_interned, tipo.clone());

                    let tree = if non_opaque_tipo.is_primitive() {
                        assign_casted_value(name_interned, value, then)
                    } else {
                        assign_casted_value(
                            name_interned,
                            value,
                            self.expect_type_assign(
                                &non_opaque_tipo,
                                val,
                                &mut index_map,
                                pattern.location(),
                                then,
                                props.otherwise.clone(),
                            ),
                        )
                    };

                    self.interner.pop_text(name);

                    tree
                } else if !props.remove_unused {
                    //No need to intern, name not used
                    assign_casted_value(name.clone(), value, then)
                } else {
                    then
                }
            }

            Pattern::List { elements, tail, .. } => {
                assert!(tipo.is_list());
                assert!(props.kind.is_expect());

                let list_elem_types = tipo.get_inner_types();

                let list_elem_type = list_elem_types
                    .first()
                    .unwrap_or_else(|| unreachable!("No list element type?"));

                let mut elems = vec![];

                // If Some then push tail onto elems
                let then = match tail {
                    None => then,
                    Some(tail) => {
                        let (tail_name, tail_name_interned) = match tail.as_ref() {
                            Pattern::Var { name, .. } => {
                                (None, self.interner.lookup_interned(name))
                            }
                            // This Pattern one doesn't even make sense
                            Pattern::Assign { .. } => {
                                todo!("Has this ever been reached before?")
                            }
                            Pattern::Discard { name, .. } => {
                                if props.full_check {
                                    (
                                        Some(format!("__discard_{name}_tail")),
                                        introduce_name(
                                            &mut self.interner,
                                            &format!("__discard_{name}_tail"),
                                        ),
                                    )
                                } else {
                                    (None, DISCARDED.to_string())
                                }
                            }
                            _ => unreachable!(),
                        };

                        let val = AirTree::local_var(&tail_name_interned, tipo.clone());

                        let then = if tail_name_interned != DISCARDED {
                            self.assignment(
                                tail,
                                val,
                                then,
                                tipo,
                                AssignmentProperties {
                                    value_type: tipo.clone(),
                                    kind: props.kind,
                                    // The reason the top level of recursion might have remove_unused
                                    // false is to deal with expect _ = thing
                                    //                       next_thing
                                    remove_unused: true,
                                    full_check: props.full_check,
                                    otherwise: props.otherwise.clone(),
                                },
                            )
                        } else {
                            then
                        };

                        elems.push(tail_name_interned);

                        if let Some(tail_name) = tail_name {
                            self.interner.pop_text(tail_name);
                        }

                        then
                    }
                };

                let then = elements
                    .iter()
                    .enumerate()
                    .rfold(then, |then, (index, elem)| {
                        let (elem_name, elem_name_interned) = match elem {
                            Pattern::Var { name, .. } => {
                                (None, self.interner.lookup_interned(name))
                            }
                            Pattern::Assign { name, .. } => {
                                (None, self.interner.lookup_interned(name))
                            }
                            Pattern::Discard { name, .. } => {
                                if props.full_check {
                                    (
                                        Some(format!("__discard_{name}_{index}")),
                                        introduce_name(
                                            &mut self.interner,
                                            &format!("__discard_{name}_{index}"),
                                        ),
                                    )
                                } else {
                                    (None, DISCARDED.to_string())
                                }
                            }
                            _ => {
                                let name = format!(
                                    "elem_{}_span_{}_{}",
                                    index,
                                    elem.location().start,
                                    elem.location().end
                                );
                                let interned = introduce_name(&mut self.interner, &name);

                                (Some(name), interned)
                            }
                        };

                        let val = AirTree::local_var(&elem_name_interned, list_elem_type.clone());

                        let then = if elem_name_interned != DISCARDED {
                            self.assignment(
                                elem,
                                val,
                                then,
                                list_elem_type,
                                AssignmentProperties {
                                    value_type: list_elem_type.clone(),
                                    kind: props.kind,
                                    remove_unused: true,
                                    full_check: props.full_check,
                                    otherwise: props.otherwise.clone(),
                                },
                            )
                        } else {
                            then
                        };

                        elems.push(elem_name_interned);

                        if let Some(elem_name) = elem_name {
                            self.interner.pop_text(elem_name);
                        }

                        then
                    });

                elems.reverse();

                let name = format!(
                    "__List_span_{}_{}",
                    pattern.location().start,
                    pattern.location().end
                );

                let name_interned = introduce_name(&mut self.interner, &name);

                let casted_var = AirTree::local_var(&name_interned, tipo.clone());

                let tree = if elements.is_empty() {
                    assign_casted_value(
                        name_interned,
                        value,
                        AirTree::list_empty(casted_var, then, otherwise),
                    )
                } else {
                    assign_casted_value(
                        name_interned,
                        value,
                        AirTree::list_access(
                            elems,
                            tipo.clone(),
                            tail.is_some(),
                            casted_var,
                            if props.full_check {
                                ExpectLevel::Full
                            } else {
                                ExpectLevel::Items
                            },
                            then,
                            otherwise,
                        ),
                    )
                };

                self.interner.pop_text(name);

                tree
            }

            Pattern::Pair {
                fst,
                snd,
                location: _,
            } => {
                let mut type_map: IndexMap<usize, Rc<Type>> = IndexMap::new();

                for (index, arg) in tipo.get_inner_types().iter().enumerate() {
                    let field_type = arg.clone();
                    type_map.insert(index, field_type);
                }

                assert!(type_map.len() == 2);

                let mut fields = vec![];

                let then = [fst, snd]
                    .iter()
                    .enumerate()
                    .rfold(then, |then, (field_index, arg)| {
                        let (field_name, field_name_interned) = match arg.as_ref() {
                            Pattern::Var { name, .. } => {
                                (None, self.interner.lookup_interned(name))
                            }
                            Pattern::Assign { name, .. } => {
                                (None, self.interner.lookup_interned(name))
                            }
                            Pattern::Discard { name, .. } => {
                                if props.full_check {
                                    (
                                        Some(format!("__discard_{name}_{field_index}")),
                                        introduce_name(
                                            &mut self.interner,
                                            &format!("__discard_{name}_{field_index}"),
                                        ),
                                    )
                                } else {
                                    (None, DISCARDED.to_string())
                                }
                            }
                            _ => {
                                let name = format!(
                                    "field_{}_span_{}_{}",
                                    field_index,
                                    arg.location().start,
                                    arg.location().end
                                );
                                let interned = introduce_name(&mut self.interner, &name);

                                (Some(name), interned)
                            }
                        };

                        let arg_type = type_map.get(&field_index).unwrap_or_else(|| {
                            unreachable!("Missing type for field {} of Pair", field_index,)
                        });

                        let val = AirTree::local_var(&field_name_interned, arg_type.clone());

                        let then = if field_name_interned != DISCARDED {
                            self.assignment(
                                arg,
                                val,
                                then,
                                arg_type,
                                AssignmentProperties {
                                    value_type: arg_type.clone(),
                                    kind: props.kind,
                                    remove_unused: true,
                                    full_check: props.full_check,
                                    otherwise: props.otherwise.clone(),
                                },
                            )
                        } else {
                            then
                        };

                        fields.push((field_index, field_name_interned, arg_type.clone()));

                        if let Some(field_name) = field_name {
                            self.interner.pop_text(field_name);
                        }

                        then
                    });

                fields.reverse();

                // This `value` is either value param that was passed in or
                // local var
                let constructor_name = format!(
                    "Pair_span_{}_{}",
                    pattern.location().start,
                    pattern.location().end
                );

                let constructor_name_interned =
                    introduce_name(&mut self.interner, &constructor_name);

                let local_value = AirTree::local_var(&constructor_name_interned, tipo.clone());

                let then = AirTree::pair_access(
                    fields
                        .first()
                        .map(|x| {
                            if x.1 == DISCARDED {
                                None
                            } else {
                                Some(x.1.clone())
                            }
                        })
                        .unwrap(),
                    fields
                        .last()
                        .map(|x| {
                            if x.1 == DISCARDED {
                                None
                            } else {
                                Some(x.1.clone())
                            }
                        })
                        .unwrap(),
                    tipo.clone(),
                    local_value,
                    props.full_check,
                    then,
                    otherwise,
                );

                let tree = assign_casted_value(constructor_name_interned, value, then);

                self.interner.pop_text(constructor_name);

                tree
            }

            Pattern::Constructor {
                constructor: PatternConstructor::Record { name, .. },
                ..
            } if tipo.is_bool() => {
                assert!(props.kind.is_expect());

                let name_var = format!(
                    "__Bool_{}_{}",
                    pattern.location().start,
                    pattern.location().end
                );

                let local_var = AirTree::local_var(&name_var, tipo.clone());

                assign_casted_value(
                    name_var,
                    value,
                    AirTree::assert_bool(name == "True", local_var, then, otherwise),
                )
            }

            Pattern::Constructor { .. } if tipo.is_void() => {
                // Void type is checked when casting from data
                // So we just assign the value and move on
                assign_casted_value(DISCARDED.to_string(), value, then)
            }

            Pattern::Constructor {
                arguments,
                constructor: PatternConstructor::Record { name, field_map },
                tipo: constr_tipo,
                ..
            } => {
                // Constr execution branch
                let field_map = field_map.clone();

                let mut type_map: IndexMap<usize, Rc<Type>> = IndexMap::new();

                for (index, arg) in constr_tipo
                    .arg_types()
                    .expect("Mismatched type")
                    .iter()
                    .enumerate()
                {
                    let field_type = arg.clone();

                    type_map.insert(index, field_type);
                }

                assert!(
                    type_map.len() >= arguments.len(),
                    "type map len: {}, arguments len: {}; for constructor {:?}",
                    type_map.len(),
                    arguments.len(),
                    name,
                );

                let mut fields = vec![];

                let then = arguments
                    .iter()
                    .enumerate()
                    .rfold(then, |then, (index, arg)| {
                        let label = arg.label.clone().unwrap_or_default();

                        let field_index = if let Some(field_map) = &field_map {
                            *field_map.fields.get(&label).map(|x| &x.0).unwrap_or(&index)
                        } else {
                            index
                        };

                        let (field_name, field_name_interned) = match &arg.value {
                            Pattern::Var { name, .. } => {
                                (None, self.interner.lookup_interned(name))
                            }
                            Pattern::Assign { name, .. } => {
                                (None, self.interner.lookup_interned(name))
                            }
                            Pattern::Discard { name, .. } => {
                                if props.full_check {
                                    (
                                        Some(format!("__discard_{name}_{index}")),
                                        introduce_name(
                                            &mut self.interner,
                                            &format!("__discard_{name}_{index}"),
                                        ),
                                    )
                                } else {
                                    (None, DISCARDED.to_string())
                                }
                            }
                            _ => {
                                let name = format!(
                                    "field_{}_span_{}_{}",
                                    field_index,
                                    arg.value.location().start,
                                    arg.value.location().end
                                );
                                let interned = introduce_name(&mut self.interner, &name);

                                (Some(name), interned)
                            }
                        };

                        let arg_type = type_map.get(&field_index).unwrap_or_else(|| {
                            unreachable!(
                                "Missing type for field {} of constr {}",
                                field_index, name
                            )
                        });

                        let val = AirTree::local_var(&field_name_interned, arg_type.clone());

                        let then = if field_name_interned != DISCARDED {
                            self.assignment(
                                &arg.value,
                                val,
                                then,
                                arg_type,
                                AssignmentProperties {
                                    value_type: arg_type.clone(),
                                    kind: props.kind,
                                    remove_unused: true,
                                    full_check: props.full_check,
                                    otherwise: props.otherwise.clone(),
                                },
                            )
                        } else {
                            then
                        };

                        fields.push((field_index, field_name_interned, arg_type.clone()));

                        if let Some(field_name) = field_name {
                            self.interner.pop_text(field_name);
                        }

                        then
                    });

                fields.reverse();

                // This `value` is either value param that was passed in or
                // local var
                let constructor_name = format!(
                    "__constructor_{}_span_{}_{}",
                    name,
                    pattern.location().start,
                    pattern.location().end
                );

                let subject_name = format!(
                    "__subject_{}_span_{}_{}",
                    name,
                    pattern.location().start,
                    pattern.location().end
                );

                let constructor_name_interned =
                    introduce_name(&mut self.interner, &constructor_name);

                let subject_name_interned = introduce_name(&mut self.interner, &subject_name);

                let local_value = AirTree::local_var(&constructor_name_interned, tipo.clone());

                let data_type = lookup_data_type_by_tipo(&self.data_types, tipo)
                    .unwrap_or_else(|| unreachable!("Failed to find definition for {}", name));

                let list_decorator = data_type
                    .decorators
                    .iter()
                    .any(|dec| matches!(dec.kind, DecoratorKind::List));

                let then = if check_replaceable_opaque_type(tipo, &self.data_types) {
                    AirTree::let_assignment(&fields[0].1, local_value.clone(), then)
                } else {
                    AirTree::fields_expose(
                        fields,
                        local_value.clone(),
                        props.full_check,
                        then,
                        otherwise.clone(),
                        list_decorator,
                    )
                };

                let then = if props.kind.is_expect()
                    && !list_decorator
                    && (data_type.constructors.len() > 1
                        || props.full_check
                        // Never check is not needed in theory since never has 2 constr variants
                        || data_type.is_never())
                {
                    let (index, _) =
                        get_constr_index_variant(&data_type, name).unwrap_or_else(|| {
                            panic!("Found constructor type {name} with 0 matching constructors")
                        });

                    AirTree::when(
                        &subject_name_interned,
                        Type::void(),
                        tipo.clone(),
                        local_value,
                        AirTree::clause(
                            &subject_name_interned,
                            AirTree::int(index),
                            tipo.clone(),
                            then,
                            otherwise,
                        ),
                    )
                } else {
                    assert!(
                        data_type.constructors.len() == 1 || data_type.is_never(),
                        "attempted let-assignment on a type with more or less than 1 constructor: \nis_expect? {}\nfull_check? {}\ndata_type={data_type:#?}\n{}",
                        props.kind.is_expect(),
                        props.full_check,
                        name,
                    );

                    then
                };

                let tree = assign_casted_value(constructor_name_interned, value, then);

                self.interner.pop_text(constructor_name);
                self.interner.pop_text(subject_name);

                tree
            }

            Pattern::Tuple {
                elems, location, ..
            } => {
                let mut type_map: IndexMap<usize, Rc<Type>> = IndexMap::new();

                for (index, arg) in tipo.get_inner_types().iter().enumerate() {
                    let field_type = arg.clone();
                    type_map.insert(index, field_type);
                }

                assert!(type_map.len() == elems.len());

                let mut fields = vec![];

                let then = elems.iter().enumerate().rfold(then, |then, (index, arg)| {
                    let (tuple_name, tuple_name_interned) = match &arg {
                        Pattern::Var { name, .. } => (None, self.interner.lookup_interned(name)),
                        Pattern::Assign { name, .. } => (None, self.interner.lookup_interned(name)),
                        Pattern::Discard { name, .. } => {
                            if props.full_check {
                                (
                                    Some(format!("__discard_{name}_{index}")),
                                    introduce_name(
                                        &mut self.interner,
                                        &format!("__discard_{name}_{index}"),
                                    ),
                                )
                            } else {
                                (None, DISCARDED.to_string())
                            }
                        }
                        _ => {
                            let name = format!(
                                "tuple_{}_span_{}_{}",
                                index,
                                arg.location().start,
                                arg.location().end
                            );

                            let interned = introduce_name(&mut self.interner, &name);

                            (Some(name), interned)
                        }
                    };

                    let arg_type = type_map.get(&index).unwrap_or_else(|| {
                        unreachable!(
                            "Missing type for tuple index {} of tuple_span_{}_{}",
                            index, location.start, location.end
                        )
                    });

                    let val = AirTree::local_var(&tuple_name_interned, arg_type.clone());

                    let then = if DISCARDED != tuple_name_interned {
                        self.assignment(
                            arg,
                            val,
                            then,
                            arg_type,
                            AssignmentProperties {
                                value_type: arg_type.clone(),
                                kind: props.kind,
                                remove_unused: true,
                                full_check: props.full_check,
                                otherwise: props.otherwise.clone(),
                            },
                        )
                    } else {
                        then
                    };

                    fields.push(tuple_name_interned);

                    if let Some(tuple_name) = tuple_name {
                        self.interner.pop_text(tuple_name);
                    }

                    then
                });

                fields.reverse();

                // This `value` is either value param that was passed in or local var

                let name = format!(
                    "__Tuple_span_{}_{}",
                    pattern.location().start,
                    pattern.location().end
                );

                let name_interned = introduce_name(&mut self.interner, &name);

                let local_var = AirTree::local_var(&name_interned, tipo.clone());

                let tree = assign_casted_value(
                    name_interned,
                    value,
                    AirTree::tuple_access(
                        fields,
                        tipo.clone(),
                        local_var,
                        props.full_check,
                        then,
                        otherwise,
                    ),
                );

                self.interner.pop_text(name);

                tree
            }
        }
    }

    pub fn expect_type_assign(
        &mut self,
        tipo: &Rc<Type>,
        value: AirTree,
        defined_data_types: &mut IndexMap<String, u64>,
        location: Span,
        then: AirTree,
        otherwise: Otherwise,
    ) -> AirTree {
        assert!(
            tipo.get_generic_id().is_none(),
            "left-hand side of expect is generic: {}",
            tipo.to_pretty(0)
        );
        // Shouldn't be needed but still here just in case
        // this function is called from anywhere else besides assignment
        let tipo = &convert_opaque_type(tipo, &self.data_types, true);

        let uplc_type = tipo.get_uplc_type();

        match uplc_type {
            // primitives
            // Untyped Data
            Some(
                UplcType::Integer
                | UplcType::String
                | UplcType::Bool
                | UplcType::ByteString
                | UplcType::Unit
                | UplcType::Bls12_381G1Element
                | UplcType::Bls12_381G2Element
                | UplcType::Bls12_381MlResult
                | UplcType::Data,
            ) => then,

            // Map type
            Some(UplcType::List(_)) if tipo.is_map() => {
                assert!(!tipo.get_inner_types().is_empty());

                let inner_list_type = &tipo.get_inner_types()[0];
                let inner_pair_types = inner_list_type.get_inner_types();

                assert!(inner_pair_types.len() == 2);

                let map_name = format!("__map_span_{}_{}", location.start, location.end);
                let pair_name = format!("__pair_span_{}_{}", location.start, location.end);
                let fst_name = format!("__pair_fst_span_{}_{}", location.start, location.end);
                let snd_name = format!("__pair_snd_span_{}_{}", location.start, location.end);
                let curried_expect_on_list = "__curried_expect_on_list".to_string();
                let list = "__list".to_string();

                let map_name_interned = introduce_name(&mut self.interner, &map_name);
                let pair_name_interned = introduce_name(&mut self.interner, &pair_name);
                let fst_name_interned = introduce_name(&mut self.interner, &fst_name);
                let snd_name_interned = introduce_name(&mut self.interner, &snd_name);
                let curried_expect_on_list_interned =
                    introduce_name(&mut self.interner, &curried_expect_on_list);
                let list_interned = introduce_name(&mut self.interner, &list);

                let expect_snd = self.expect_type_assign(
                    &inner_pair_types[1],
                    AirTree::local_var(snd_name_interned.clone(), inner_pair_types[1].clone()),
                    defined_data_types,
                    location,
                    AirTree::call(
                        AirTree::local_var(&curried_expect_on_list_interned, Type::void()),
                        Type::void(),
                        vec![AirTree::builtin(
                            DefaultFunction::TailList,
                            Type::list(Type::data()),
                            vec![AirTree::local_var(&list_interned, tipo.clone())],
                        )],
                    ),
                    otherwise.clone(),
                );

                let expect_fst = self.expect_type_assign(
                    &inner_pair_types[0],
                    AirTree::local_var(fst_name_interned.clone(), inner_pair_types[0].clone()),
                    defined_data_types,
                    location,
                    expect_snd,
                    otherwise.clone(),
                );

                let unwrap_function = AirTree::anon_func(
                    vec![list_interned.clone(), curried_expect_on_list_interned],
                    AirTree::list_empty(
                        AirTree::local_var(&list_interned, tipo.clone()),
                        then,
                        AirTree::anon_func(
                            vec![],
                            AirTree::let_assignment(
                                &pair_name_interned,
                                AirTree::builtin(
                                    DefaultFunction::HeadList,
                                    Type::pair(Type::data(), Type::data()),
                                    vec![AirTree::local_var(list_interned, tipo.clone())],
                                ),
                                AirTree::pair_access(
                                    Some(fst_name_interned),
                                    Some(snd_name_interned),
                                    inner_list_type.clone(),
                                    AirTree::local_var(
                                        &pair_name_interned,
                                        inner_list_type.clone(),
                                    ),
                                    true,
                                    expect_fst,
                                    otherwise.unwrap_or_else(DELAY_ERROR),
                                ),
                            ),
                            true,
                        ),
                    ),
                    false,
                );

                let function = self.code_gen_functions.get(EXPECT_ON_LIST);

                // This function can be defined here on in the branch below
                if function.is_none() {
                    let expect_list_func = AirTree::expect_on_list2();
                    self.code_gen_functions.insert(
                        EXPECT_ON_LIST.to_string(),
                        CodeGenFunction::Function {
                            body: expect_list_func,
                            params: vec!["__list_to_check".to_string(), "__check_with".to_string()],
                        },
                    );
                }

                if let Some(counter) = defined_data_types.get_mut(EXPECT_ON_LIST) {
                    *counter += 1
                } else {
                    defined_data_types.insert(EXPECT_ON_LIST.to_string(), 1);
                }

                let func_call = AirTree::call(
                    AirTree::var(
                        ValueConstructor::public(
                            Type::void(),
                            ValueConstructorVariant::ModuleFn {
                                name: EXPECT_ON_LIST.to_string(),
                                field_map: None,
                                module: "".to_string(),
                                arity: 1,
                                location,
                                builtin: None,
                            },
                        ),
                        EXPECT_ON_LIST,
                        "",
                    ),
                    Type::void(),
                    vec![
                        AirTree::local_var(&map_name_interned, tipo.clone()),
                        unwrap_function,
                    ],
                );

                let tree = AirTree::let_assignment(map_name_interned, value, func_call);

                self.interner.pop_text(map_name);
                self.interner.pop_text(pair_name);
                self.interner.pop_text(fst_name);
                self.interner.pop_text(snd_name);
                self.interner.pop_text(curried_expect_on_list);
                self.interner.pop_text(list);

                tree
            }
            // Tuple type
            Some(UplcType::List(_)) if tipo.is_tuple() => {
                let tuple_inner_types = tipo.get_inner_types();

                assert!(!tuple_inner_types.is_empty());

                let tuple_name = format!("__tuple_span_{}_{}", location.start, location.end);

                let tuple_name_interned = introduce_name(&mut self.interner, &tuple_name);

                let mut tuple_expect_items = vec![];

                let then =
                    tuple_inner_types
                        .iter()
                        .enumerate()
                        .rfold(then, |then, (index, arg)| {
                            let tuple_index_name = format!(
                                "__tuple_index_{}_span_{}_{}",
                                index, location.start, location.end
                            );

                            let tuple_index_name_interned =
                                introduce_name(&mut self.interner, &tuple_index_name);

                            let expect_tuple_item = self.expect_type_assign(
                                arg,
                                AirTree::local_var(&tuple_index_name_interned, arg.clone()),
                                defined_data_types,
                                location,
                                then,
                                otherwise.clone(),
                            );

                            tuple_expect_items.push(tuple_index_name_interned);

                            self.interner.pop_text(tuple_index_name);

                            expect_tuple_item
                        });

                tuple_expect_items.reverse();

                let tuple_access = AirTree::tuple_access(
                    tuple_expect_items,
                    tipo.clone(),
                    AirTree::local_var(&tuple_name_interned, tipo.clone()),
                    true,
                    then,
                    otherwise.unwrap_or_else(DELAY_ERROR),
                );

                let tree = AirTree::let_assignment(tuple_name_interned, value, tuple_access);

                self.interner.pop_text(tuple_name);

                tree
            }
            // Regular List type
            Some(UplcType::List(_)) => {
                assert!(!tipo.get_inner_types().is_empty());

                let inner_list_type = &tipo.get_inner_types()[0];

                if inner_list_type.is_data() {
                    then
                } else {
                    let list_name = format!("__list_span_{}_{}", location.start, location.end);
                    let item_name = format!("__item_span_{}_{}", location.start, location.end);
                    let list = "__list".to_string();
                    let curried_func = "__curried_expect_on_list".to_string();

                    let list_name_interned = introduce_name(&mut self.interner, &list_name);
                    let item_name_interned = introduce_name(&mut self.interner, &item_name);
                    let list_interned = introduce_name(&mut self.interner, &list);
                    let curried_func_interned = introduce_name(&mut self.interner, &curried_func);

                    let unwrap_function = AirTree::anon_func(
                        vec![list_interned.clone(), curried_func_interned.clone()],
                        AirTree::list_empty(
                            AirTree::local_var(&list_interned, tipo.clone()),
                            then,
                            AirTree::anon_func(
                                vec![],
                                AirTree::let_assignment(
                                    &item_name_interned,
                                    AirTree::builtin(
                                        DefaultFunction::HeadList,
                                        Type::data(),
                                        vec![AirTree::local_var(&list_interned, tipo.clone())],
                                    ),
                                    AirTree::soft_cast_assignment(
                                        &item_name_interned,
                                        inner_list_type.clone(),
                                        AirTree::local_var(&item_name_interned, Type::data()),
                                        self.expect_type_assign(
                                            inner_list_type,
                                            AirTree::local_var(
                                                &item_name_interned,
                                                inner_list_type.clone(),
                                            ),
                                            defined_data_types,
                                            location,
                                            AirTree::call(
                                                AirTree::local_var(
                                                    curried_func_interned,
                                                    Type::void(),
                                                ),
                                                Type::void(),
                                                vec![AirTree::builtin(
                                                    DefaultFunction::TailList,
                                                    Type::list(Type::data()),
                                                    vec![AirTree::local_var(
                                                        list_interned,
                                                        tipo.clone(),
                                                    )],
                                                )],
                                            ),
                                            otherwise.clone(),
                                        ),
                                        otherwise.unwrap_or_else(DELAY_ERROR),
                                    ),
                                ),
                                true,
                            ),
                        ),
                        false,
                    );

                    let function = self.code_gen_functions.get(EXPECT_ON_LIST);

                    if function.is_none() {
                        let expect_list_func = AirTree::expect_on_list2();
                        self.code_gen_functions.insert(
                            EXPECT_ON_LIST.to_string(),
                            CodeGenFunction::Function {
                                body: expect_list_func,
                                params: vec![
                                    "__list_to_check".to_string(),
                                    "__check_with".to_string(),
                                ],
                            },
                        );
                    }

                    if let Some(counter) = defined_data_types.get_mut(EXPECT_ON_LIST) {
                        *counter += 1
                    } else {
                        defined_data_types.insert(EXPECT_ON_LIST.to_string(), 1);
                    }

                    let func_call = AirTree::call(
                        AirTree::var(
                            ValueConstructor::public(
                                Type::void(),
                                ValueConstructorVariant::ModuleFn {
                                    name: EXPECT_ON_LIST.to_string(),
                                    field_map: None,
                                    module: "".to_string(),
                                    arity: 1,
                                    location,
                                    builtin: None,
                                },
                            ),
                            EXPECT_ON_LIST,
                            "",
                        ),
                        Type::void(),
                        vec![
                            AirTree::local_var(&list_name_interned, tipo.clone()),
                            unwrap_function,
                        ],
                    );

                    let tree = AirTree::let_assignment(list_name_interned, value, func_call);

                    self.interner.pop_text(list_name);
                    self.interner.pop_text(item_name);
                    self.interner.pop_text(list);
                    self.interner.pop_text(curried_func);

                    tree
                }
            }
            // Pair type
            Some(UplcType::Pair(_, _)) => {
                let tuple_inner_types = tipo.get_inner_types();

                assert!(tuple_inner_types.len() == 2);

                let pair_name = format!("__pair_span_{}_{}", location.start, location.end);
                let fst_name = format!("__pair_fst_span_{}_{}", location.start, location.end);
                let snd_name = format!("__pair_snd_span_{}_{}", location.start, location.end);

                let pair_name_interned = introduce_name(&mut self.interner, &pair_name);
                let fst_name_interned = introduce_name(&mut self.interner, &fst_name);
                let snd_name_interned = introduce_name(&mut self.interner, &snd_name);

                let expect_snd = self.expect_type_assign(
                    &tuple_inner_types[1],
                    AirTree::local_var(snd_name_interned.clone(), tuple_inner_types[1].clone()),
                    defined_data_types,
                    location,
                    then,
                    otherwise.clone(),
                );

                let expect_fst = self.expect_type_assign(
                    &tuple_inner_types[0],
                    AirTree::local_var(fst_name_interned.clone(), tuple_inner_types[0].clone()),
                    defined_data_types,
                    location,
                    expect_snd,
                    otherwise.clone(),
                );

                let pair_access = AirTree::pair_access(
                    Some(fst_name_interned),
                    Some(snd_name_interned),
                    tipo.clone(),
                    AirTree::local_var(&pair_name_interned, tipo.clone()),
                    true,
                    expect_fst,
                    otherwise.unwrap_or_else(DELAY_ERROR),
                );

                let tree = AirTree::let_assignment(pair_name_interned, value, pair_access);

                self.interner.pop_text(pair_name);
                self.interner.pop_text(fst_name);
                self.interner.pop_text(snd_name);

                tree
            }

            // Constr type
            None => {
                let data_type =
                    lookup_data_type_by_tipo(&self.data_types, tipo).unwrap_or_else(|| {
                        unreachable!("We need a data type definition for type {:#?}", tipo)
                    });

                let data_type_variant = tipo
                    .get_inner_types()
                    .iter()
                    .map(|arg| get_arg_type_name(arg))
                    .join(DISCARDED);

                assert!(data_type.typed_parameters.len() == tipo.arg_types().unwrap().len());

                let mono_types: IndexMap<u64, Rc<Type>> = if !data_type.typed_parameters.is_empty()
                {
                    data_type
                        .typed_parameters
                        .iter()
                        .zip(tipo.arg_types().unwrap())
                        .flat_map(|item| get_generic_id_and_type(item.0, &item.1))
                        .collect()
                } else {
                    IndexMap::new()
                };

                let data_type_name = if otherwise.is_some() {
                    format!(
                        "__expect_{}_{}_otherwise",
                        data_type.name, data_type_variant
                    )
                } else {
                    format!("__expect_{}_{}", data_type.name, data_type_variant)
                };
                let function = self.code_gen_functions.get(&data_type_name);

                // mutate code_gen_funcs and defined_data_types in this if branch
                if function.is_none() && defined_data_types.get(&data_type_name).is_none() {
                    defined_data_types.insert(data_type_name.clone(), 1);

                    let var_then = AirTree::call(
                        AirTree::local_var("then_delayed", Type::void()),
                        Type::void(),
                        vec![],
                    );

                    let otherwise_delayed = otherwise
                        .as_ref()
                        .map(|_| AirTree::local_var("otherwise_delayed", Type::void()));

                    let is_never = data_type.is_never();

                    let list_decorator = data_type
                        .decorators
                        .iter()
                        .any(|dec| matches!(dec.kind, DecoratorKind::List));

                    let constr_clauses = data_type.constructors.iter().enumerate().rfold(
                        otherwise_delayed.clone().unwrap_or_else(DELAY_ERROR),
                        |acc, (index, constr)| {
                            // NOTE: For the Never type, we have an placeholder first constructor
                            // that must be ignored. The Never type is considered to have only one
                            // constructor starting at index 1 so it shouldn't be possible to
                            // cast from Data into the first constructor. There's virtually no
                            // constructor at index 0.
                            if is_never && index == 0 {
                                return acc;
                            }

                            let index = if let Some(tag) =
                                constr.decorators.iter().find_map(|d| match &d.kind {
                                    DecoratorKind::Tag { value, .. } => Some(value),
                                    _ => None,
                                }) {
                                tag.parse().unwrap()
                            } else {
                                index
                            };

                            let mut constr_args = vec![];

                            let constr_then = constr.arguments.iter().enumerate().rfold(
                                var_then.clone(),
                                |then, (index, arg)| {
                                    let arg_name =
                                        arg.label.clone().unwrap_or(format!("__field_{index}"));

                                    let arg_tipo =
                                        find_and_replace_generics(&arg.tipo, &mono_types);

                                    constr_args.push((index, arg_name.clone(), arg_tipo.clone()));

                                    self.expect_type_assign(
                                        &arg_tipo.clone(),
                                        AirTree::local_var(arg_name, arg_tipo),
                                        defined_data_types,
                                        location,
                                        then,
                                        otherwise_delayed.clone(),
                                    )
                                },
                            );
                            constr_args.reverse();

                            let then = if constr_args.is_empty() {
                                AirTree::fields_empty(
                                    AirTree::local_var(
                                        format!(
                                            "__constr_var_span_{}_{}",
                                            location.start, location.end
                                        ),
                                        tipo.clone(),
                                    ),
                                    constr_then,
                                    otherwise_delayed.clone().unwrap_or_else(DELAY_ERROR),
                                    list_decorator,
                                )
                            } else {
                                AirTree::fields_expose(
                                    constr_args,
                                    AirTree::local_var(
                                        format!(
                                            "__constr_var_span_{}_{}",
                                            location.start, location.end
                                        ),
                                        tipo.clone(),
                                    ),
                                    true,
                                    constr_then,
                                    otherwise_delayed.clone().unwrap_or_else(DELAY_ERROR),
                                    list_decorator,
                                )
                            };

                            if list_decorator {
                                AirTree::anon_func(vec![], then, true)
                            } else {
                                AirTree::anon_func(
                                    vec![],
                                    AirTree::clause(
                                        format!(
                                            "__subject_span_{}_{}",
                                            location.start, location.end
                                        ),
                                        AirTree::int(index),
                                        tipo.clone(),
                                        then,
                                        acc,
                                    ),
                                    true,
                                )
                            }
                        },
                    );

                    let when_expr = AirTree::when(
                        format!("__subject_span_{}_{}", location.start, location.end),
                        Type::void(),
                        tipo.clone(),
                        AirTree::local_var(
                            format!("__constr_var_span_{}_{}", location.start, location.end),
                            tipo.clone(),
                        ),
                        AirTree::call(constr_clauses, Type::void(), vec![]),
                    );

                    let func_body = AirTree::let_assignment(
                        format!("__constr_var_span_{}_{}", location.start, location.end),
                        AirTree::local_var("__param_0", tipo.clone()),
                        when_expr,
                    );

                    let code_gen_func = CodeGenFunction::Function {
                        body: func_body,
                        params: if otherwise.is_some() {
                            vec![
                                "__param_0".to_string(),
                                "then_delayed".to_string(),
                                "otherwise_delayed".to_string(),
                            ]
                        } else {
                            vec!["__param_0".to_string(), "then_delayed".to_string()]
                        },
                    };

                    self.code_gen_functions
                        .insert(data_type_name.clone(), code_gen_func);
                } else if defined_data_types.get(&data_type_name).is_none() {
                    defined_data_types.insert(data_type_name.to_string(), 1);
                }

                let args = if let Some(otherwise) = otherwise {
                    vec![value, AirTree::anon_func(vec![], then, true), otherwise]
                } else {
                    vec![value, AirTree::anon_func(vec![], then, true)]
                };

                let module_fn = ValueConstructorVariant::ModuleFn {
                    name: data_type_name.to_string(),
                    field_map: None,
                    module: "".to_string(),
                    arity: args.len(),
                    location,
                    builtin: None,
                };

                let func_var = AirTree::var(
                    ValueConstructor::public(tipo.clone(), module_fn),
                    data_type_name,
                    "",
                );

                AirTree::call(func_var, Type::void(), args)
            }
        }
    }

    fn handle_decision_tree(
        &mut self,
        subject_name: &String,
        subject_tipo: Rc<Type>,
        return_tipo: Rc<Type>,
        module_build_name: &str,
        tree: decision_tree::DecisionTree<'_>,
        mut stick_set: TreeSet,
    ) -> AirTree {
        match tree {
            DecisionTree::Switch {
                path,
                mut cases,
                default,
            } => {
                //Current path to test
                let current_tipo = get_tipo_by_path(subject_tipo.clone(), &path);
                let builtins_path = Builtins::new_from_path(subject_tipo.clone(), path);
                let current_subject_name = if builtins_path.is_empty() {
                    subject_name.clone()
                } else {
                    format!("{subject_name}_{builtins_path}")
                };

                // Transition process from previous to current
                let builtins_to_add = stick_set.diff_union_builtins(builtins_path.clone());

                // Previous path to apply the transition process too
                let prev_builtins = Builtins {
                    vec: builtins_path.vec[0..(builtins_path.len() - builtins_to_add.len())]
                        .to_vec(),
                };

                let prev_subject_name = if prev_builtins.is_empty() {
                    subject_name.clone()
                } else {
                    format!("{subject_name}_{prev_builtins}")
                };
                let prev_tipo = prev_builtins
                    .vec
                    .last()
                    .map_or(subject_tipo.clone(), |last| last.tipo());

                let data_type = lookup_data_type_by_tipo(&self.data_types, &current_tipo);

                let last_clause = if data_type
                    .as_ref()
                    .is_none_or(|d| d.constructors.len() != cases.len())
                {
                    *default.unwrap()
                } else {
                    cases.pop().unwrap().1
                };

                let last_clause = self.handle_decision_tree(
                    subject_name,
                    subject_tipo.clone(),
                    return_tipo.clone(),
                    module_build_name,
                    last_clause,
                    stick_set.clone(),
                );

                let test_subject_name = if data_type.is_some() {
                    format!("{}_index", current_subject_name.clone(),)
                } else {
                    current_subject_name.clone()
                };

                let clauses = cases.into_iter().rfold(last_clause, |acc, (case, then)| {
                    let case_air = self.handle_decision_tree(
                        subject_name,
                        subject_tipo.clone(),
                        return_tipo.clone(),
                        module_build_name,
                        then,
                        stick_set.clone(),
                    );

                    AirTree::clause(
                        test_subject_name.clone(),
                        case.get_air_pattern(current_tipo.clone()),
                        current_tipo.clone(),
                        case_air,
                        AirTree::anon_func(vec![], acc, true),
                    )
                });

                let when_air_clauses = AirTree::when(
                    test_subject_name,
                    return_tipo.clone(),
                    current_tipo.clone(),
                    AirTree::local_var(current_subject_name, current_tipo.clone()),
                    clauses,
                );

                builtins_to_add.produce_air(prev_subject_name, prev_tipo, when_air_clauses)
            }
            DecisionTree::ListSwitch {
                path,
                cases,
                tail_cases,
                default,
            } => {
                //Current path to test
                let current_tipo = get_tipo_by_path(subject_tipo.clone(), &path);
                let builtins_path = Builtins::new_from_path(subject_tipo.clone(), path);
                let current_subject_name = if builtins_path.is_empty() {
                    subject_name.clone()
                } else {
                    format!("{subject_name}_{builtins_path}")
                };

                // Transition process from previous to current
                let builtins_to_add = stick_set.diff_union_builtins(builtins_path.clone());

                // Previous path to apply the transition process too
                let prev_builtins = Builtins {
                    vec: builtins_path.vec[0..(builtins_path.len() - builtins_to_add.len())]
                        .to_vec(),
                };

                let prev_subject_name = if prev_builtins.is_empty() {
                    subject_name.clone()
                } else {
                    format!("{subject_name}_{prev_builtins}")
                };
                let prev_tipo = prev_builtins
                    .vec
                    .last()
                    .map_or(subject_tipo.clone(), |last| last.tipo());

                let longest_pattern = cases.iter().chain(tail_cases.iter()).fold(
                    0,
                    |longest, (case, _)| match case {
                        CaseTest::List(i) => {
                            if longest < *i {
                                *i
                            } else {
                                longest
                            }
                        }
                        CaseTest::ListWithTail(i) => {
                            if longest < *i {
                                *i - 1
                            } else {
                                longest
                            }
                        }
                        _ => unreachable!(),
                    },
                );

                let last_pattern = if tail_cases.is_empty() {
                    *default.as_ref().unwrap().clone()
                } else {
                    let tree = tail_cases.last().unwrap();

                    tree.1.clone()
                };

                let builtins_for_pattern = builtins_path.merge(Builtins::new_from_list_case(
                    CaseTest::List(longest_pattern),
                ));

                stick_set.diff_union_builtins(builtins_for_pattern.clone());

                let last_pattern = self.handle_decision_tree(
                    subject_name,
                    subject_tipo.clone(),
                    return_tipo.clone(),
                    module_build_name,
                    last_pattern,
                    stick_set.clone(),
                );

                let list_clauses = (0..=longest_pattern).rev().with_position().fold(
                    (builtins_for_pattern, last_pattern),
                    |(mut builtins_for_pattern, acc), list_item| match list_item {
                        itertools::Position::First(index) | itertools::Position::Only(index) => {
                            let (_, tree) = cases
                                .iter()
                                .chain(tail_cases.iter())
                                .find(|x| match x.0 {
                                    CaseTest::List(i) => i == index,
                                    CaseTest::ListWithTail(i) => i <= index,
                                    _ => unreachable!(),
                                })
                                .cloned()
                                .unwrap_or_else(|| {
                                    (CaseTest::Wild, *default.as_ref().unwrap().clone())
                                });

                            let tail_name = if builtins_for_pattern.is_empty() {
                                subject_name.clone()
                            } else {
                                format!("{subject_name}_{builtins_for_pattern}")
                            };

                            let then = self.handle_decision_tree(
                                subject_name,
                                subject_tipo.clone(),
                                return_tipo.clone(),
                                module_build_name,
                                tree,
                                stick_set.clone(),
                            );

                            let acc = AirTree::list_clause(
                                tail_name.clone(),
                                subject_tipo.clone(),
                                then,
                                AirTree::anon_func(vec![], acc, true),
                                None,
                            );

                            builtins_for_pattern.pop();

                            (builtins_for_pattern, acc)
                        }

                        itertools::Position::Middle(index) | itertools::Position::Last(index) => {
                            let (_, tree) = cases
                                .iter()
                                .chain(tail_cases.iter())
                                .find(|x| match x.0 {
                                    CaseTest::List(i) => i == index,
                                    CaseTest::ListWithTail(i) => i <= index,
                                    _ => unreachable!(),
                                })
                                .cloned()
                                .unwrap_or_else(|| {
                                    (CaseTest::Wild, *default.as_ref().unwrap().clone())
                                });

                            let tail_name = if builtins_for_pattern.is_empty() {
                                subject_name.clone()
                            } else {
                                format!("{subject_name}_{builtins_for_pattern}")
                            };

                            // TODO: change this in the future to use the Builtins to_string method
                            // to ensure future changes don't break things
                            let next_tail_name = Some(format!("{tail_name}_tail"));

                            let then = self.handle_decision_tree(
                                subject_name,
                                subject_tipo.clone(),
                                return_tipo.clone(),
                                module_build_name,
                                tree,
                                stick_set.clone(),
                            );

                            let acc = AirTree::list_clause(
                                tail_name.clone(),
                                subject_tipo.clone(),
                                then,
                                AirTree::anon_func(vec![], acc, true),
                                next_tail_name.map(|next| (tail_name, next)),
                            );

                            // since we iterate over the list cases in reverse
                            // We pop off a builtin to make it easier to get the name of
                            // prev_tested list case since each name is based off the builtins
                            builtins_for_pattern.pop();

                            (builtins_for_pattern, acc)
                        }
                    },
                );

                let when_list_cases = AirTree::when(
                    current_subject_name.clone(),
                    return_tipo.clone(),
                    current_tipo.clone(),
                    AirTree::local_var(current_subject_name, current_tipo.clone()),
                    list_clauses.1,
                );

                builtins_to_add.produce_air(prev_subject_name, prev_tipo, when_list_cases)
            }
            DecisionTree::HoistedLeaf(name, args) => {
                let air_args = args
                    .iter()
                    .map(|item| {
                        let current_tipo = get_tipo_by_path(subject_tipo.clone(), &item.path);

                        (
                            current_tipo.clone(),
                            AirTree::local_var(item.assigned.clone(), current_tipo),
                        )
                    })
                    .collect_vec();

                let then = AirTree::call(
                    AirTree::local_var(
                        name,
                        Type::function(
                            air_args.iter().map(|i| i.0.clone()).collect_vec(),
                            return_tipo.clone(),
                        ),
                    ),
                    Type::void(),
                    air_args.into_iter().map(|i| i.1).collect_vec(),
                );

                handle_assigns(subject_name, subject_tipo, &args, &mut stick_set, then)
            }
            DecisionTree::HoistThen {
                name,
                assigns,
                pattern,
                then,
            } => {
                let assign = AirTree::let_assignment(
                    name,
                    AirTree::anon_func(
                        assigns
                            .iter()
                            .map(|i| introduce_name(&mut self.interner, &i.assigned))
                            .collect_vec(),
                        // The one reason we have to pass in mutable self
                        // So we can build the TypedExpr into Air
                        self.build(then, module_build_name, &[]),
                        true,
                    ),
                    self.handle_decision_tree(
                        subject_name,
                        subject_tipo,
                        return_tipo,
                        module_build_name,
                        *pattern,
                        stick_set,
                    ),
                );

                assigns.into_iter().for_each(|x| {
                    self.interner.pop_text(x.assigned);
                });

                assign
            }
        }
    }

    fn hoist_functions_to_validator(&mut self, mut air_tree: AirTree) -> AirTree {
        let mut functions_to_hoist = IndexMap::new();
        let mut used_functions = vec![];
        let mut defined_functions = vec![];
        let mut hoisted_functions = vec![];
        let mut validator_hoistable;

        // TODO change subsequent tree traversals to be more like a stream.
        air_tree.traverse_tree_with(&mut |air_tree: &mut AirTree, _| {
            erase_opaque_type_operations(air_tree, &self.data_types);
        });

        self.find_function_vars_and_depth(
            &mut air_tree,
            &mut functions_to_hoist,
            &mut used_functions,
            &mut TreePath::new(),
            0,
            Fields::FirstField,
        );

        validator_hoistable = used_functions.clone();

        while let Some((key, variant_name)) = used_functions.pop() {
            defined_functions.push((key.clone(), variant_name.clone()));

            let function_variants = functions_to_hoist
                .get(&key)
                .unwrap_or_else(|| panic!("Missing Function Definition"));

            let (tree_path, function) = function_variants
                .get(&variant_name)
                .unwrap_or_else(|| panic!("Missing Function Variant Definition"));

            match function {
                HoistableFunction::Function { body, deps, params } => {
                    let mut hoist_body = body.clone();
                    let mut hoist_deps = deps.clone();
                    let params = params.clone();
                    let tree_path = tree_path.clone();

                    self.define_dependent_functions(
                        &mut hoist_body,
                        &mut functions_to_hoist,
                        &mut used_functions,
                        &defined_functions,
                        &mut hoist_deps,
                        tree_path,
                    );

                    let function_variants = functions_to_hoist
                        .get_mut(&key)
                        .unwrap_or_else(|| panic!("Missing Function Definition"));

                    let (_, function) = function_variants
                        .get_mut(&variant_name)
                        .expect("Missing Function Variant Definition");

                    *function = HoistableFunction::Function {
                        body: hoist_body,
                        deps: hoist_deps,
                        params,
                    };
                }
                HoistableFunction::Link(_) => todo!("Deal with Link later"),
                _ => unreachable!(),
            }
        }
        validator_hoistable.dedup();

        // First we need to sort functions by dependencies
        // here's also where we deal with mutual recursion

        // Mutual Recursion
        let inputs = functions_to_hoist
            .iter()
            .flat_map(|(function_name, val)| {
                val.into_iter()
                    .map(|(variant, (_, function))| {
                        if let HoistableFunction::Function { deps, .. } = function {
                            ((function_name.clone(), variant.clone()), deps)
                        } else {
                            todo!("Deal with Link later")
                        }
                    })
                    .collect_vec()
            })
            .collect_vec();

        let capacity = inputs.len();

        let mut graph = Graph::<(), ()>::with_capacity(capacity, capacity * 5);

        let mut indices = HashMap::with_capacity(capacity);
        let mut values = HashMap::with_capacity(capacity);

        for (value, _) in &inputs {
            let index = graph.add_node(());

            indices.insert(value.clone(), index);

            values.insert(index, value.clone());
        }

        for (value, deps) in inputs {
            if let Some(from_index) = indices.get(&value) {
                let deps = deps.iter().filter_map(|dep| indices.get(dep));

                for to_index in deps {
                    graph.add_edge(*from_index, *to_index, ());
                }
            }
        }

        let strong_connections = algo::tarjan_scc(&graph);

        for (index, connections) in strong_connections.into_iter().enumerate() {
            // If there's only one function, then it's only self recursive
            if connections.len() < 2 {
                continue;
            }

            let cyclic_function_names = connections
                .iter()
                .map(|index| values.get(index).unwrap())
                .collect_vec();

            // TODO: Maybe I could come up with a name based off the functions involved?
            let function_key = FunctionAccessKey {
                function_name: format!("__cyclic_function_{index}"),
                module_name: "".to_string(),
            };

            let mut path = TreePath::new();
            let mut cycle_of_functions = vec![];
            let mut cycle_deps = vec![];

            let function_list = cyclic_function_names
                .iter()
                .map(|(key, variant)| {
                    format!(
                        "{}{}{}",
                        key.module_name,
                        key.function_name,
                        if variant.is_empty() {
                            "".to_string()
                        } else {
                            format!("_{variant}")
                        }
                    )
                })
                .collect_vec();

            // By doing this any vars that "call" into a function in the cycle will be
            // redirected to call the cyclic function instead with the proper index
            for (index, (func_name, variant)) in cyclic_function_names.iter().enumerate() {
                self.cyclic_functions.insert(
                    (func_name.clone(), variant.clone()),
                    (function_list.clone(), index, function_key.clone()),
                );

                let (tree_path, func) = functions_to_hoist
                    .get_mut(func_name)
                    .expect("Missing Function Definition")
                    .get_mut(variant)
                    .expect("Missing Function Variant Definition");

                match func {
                    HoistableFunction::Function { params, body, deps } => {
                        cycle_of_functions.push((params.clone(), body.clone()));
                        cycle_deps.push(deps.clone());
                    }

                    _ => unreachable!(),
                }

                if !path.was_set() {
                    path = tree_path.clone();
                } else {
                    path = path.common_ancestor(tree_path);
                }

                // Here we change function to be link so all functions that depend on it know its a
                // cyclic function
                *func = HoistableFunction::CyclicLink(function_key.clone());
            }

            let cyclic_function = HoistableFunction::CyclicFunction {
                functions: cycle_of_functions,
                deps: cycle_deps
                    .into_iter()
                    .flatten()
                    .dedup()
                    // Make sure to filter out cyclic dependencies
                    .filter(|dependency| {
                        !cyclic_function_names.iter().any(|(func_name, variant)| {
                            func_name == &dependency.0 && variant == &dependency.1
                        })
                    })
                    .collect_vec(),
            };

            let mut cyclic_map = IndexMap::new();
            cyclic_map.insert("".to_string(), (path, cyclic_function));

            functions_to_hoist.insert(function_key, cyclic_map);
        }

        // Rest of code is for hoisting functions
        // TODO: replace with graph implementation of sorting
        let mut sorted_function_vec: Vec<(FunctionAccessKey, String)> = vec![];

        let functions_to_hoist_cloned = functions_to_hoist.clone();

        let mut sorting_attempts: u64 = 0;
        while let Some((generic_func, variant)) = validator_hoistable.pop() {
            assert!(
                sorting_attempts < 5_000_000_000,
                "Sorting dependency attempts exceeded"
            );

            let function_variants = functions_to_hoist_cloned
                .get(&generic_func)
                .unwrap_or_else(|| panic!("Missing Function Definition"));

            let (_, function) = function_variants
                .get(&variant)
                .unwrap_or_else(|| panic!("Missing Function Variant Definition"));

            match function {
                HoistableFunction::Function { deps, .. } => {
                    for (dep_generic_func, dep_variant) in deps.iter() {
                        if !(dep_generic_func == &generic_func && dep_variant == &variant) {
                            validator_hoistable
                                .insert(0, (dep_generic_func.clone(), dep_variant.clone()));

                            sorted_function_vec.retain(|(generic_func, variant)| {
                                !(generic_func == dep_generic_func && variant == dep_variant)
                            });
                        }
                    }

                    // Fix dependencies path to be updated to common ancestor
                    for (dep_key, dep_variant) in deps {
                        let (func_tree_path, _) = functions_to_hoist
                            .get(&generic_func)
                            .unwrap()
                            .get(&variant)
                            .unwrap()
                            .clone();

                        let (dep_path, _) = functions_to_hoist
                            .get_mut(dep_key)
                            .unwrap()
                            .get_mut(dep_variant)
                            .unwrap();

                        *dep_path = func_tree_path.common_ancestor(dep_path);
                    }
                    sorted_function_vec.push((generic_func, variant));
                }
                HoistableFunction::Link(_) => todo!("Deal with Link later"),
                HoistableFunction::CyclicLink(cyclic_name) => {
                    validator_hoistable.insert(0, (cyclic_name.clone(), "".to_string()));

                    sorted_function_vec.retain(|(generic_func, variant)| {
                        !(generic_func == cyclic_name && variant.is_empty())
                    });

                    let (func_tree_path, _) = functions_to_hoist
                        .get(&generic_func)
                        .unwrap()
                        .get(&variant)
                        .unwrap()
                        .clone();

                    let (dep_path, _) = functions_to_hoist
                        .get_mut(cyclic_name)
                        .unwrap()
                        .get_mut("")
                        .unwrap();

                    *dep_path = func_tree_path.common_ancestor(dep_path);
                }
                HoistableFunction::CyclicFunction { deps, .. } => {
                    for (dep_generic_func, dep_variant) in deps.iter() {
                        if !(dep_generic_func == &generic_func && dep_variant == &variant) {
                            validator_hoistable
                                .insert(0, (dep_generic_func.clone(), dep_variant.clone()));

                            sorted_function_vec.retain(|(generic_func, variant)| {
                                !(generic_func == dep_generic_func && variant == dep_variant)
                            });
                        }
                    }

                    // Fix dependencies path to be updated to common ancestor
                    for (dep_key, dep_variant) in deps {
                        let (func_tree_path, _) = functions_to_hoist
                            .get(&generic_func)
                            .unwrap()
                            .get(&variant)
                            .unwrap()
                            .clone();

                        let (dep_path, _) = functions_to_hoist
                            .get_mut(dep_key)
                            .unwrap()
                            .get_mut(dep_variant)
                            .unwrap();

                        *dep_path = func_tree_path.common_ancestor(dep_path);
                    }
                    sorted_function_vec.push((generic_func, variant));
                }
            }

            sorting_attempts += 1;
        }
        sorted_function_vec.dedup();

        // Now we need to hoist the functions to the top of the validator
        for (key, variant) in sorted_function_vec {
            if hoisted_functions
                .iter()
                .any(|(func_key, func_variant)| func_key == &key && func_variant == &variant)
            {
                continue;
            }

            let function_variants = functions_to_hoist
                .get(&key)
                .unwrap_or_else(|| panic!("Missing Function Definition"));

            let (tree_path, function) = function_variants
                .get(&variant)
                .unwrap_or_else(|| panic!("Missing Function Variant Definition"));

            self.hoist_function(
                &mut air_tree,
                tree_path,
                function,
                (&key, &variant),
                &functions_to_hoist,
                &mut hoisted_functions,
            );
        }

        air_tree
    }

    fn hoist_function(
        &mut self,
        air_tree: &mut AirTree,
        tree_path: &TreePath,
        function: &HoistableFunction,
        key_var: (&FunctionAccessKey, &String),
        functions_to_hoist: &IndexMap<
            FunctionAccessKey,
            IndexMap<String, (TreePath, HoistableFunction)>,
        >,
        hoisted_functions: &mut Vec<(FunctionAccessKey, String)>,
    ) {
        match function {
            HoistableFunction::Function {
                body,
                deps: func_deps,
                params,
            } => {
                let mut body = body.clone();

                let (key, variant) = key_var;

                // check for recursiveness
                let is_recursive = func_deps
                    .iter()
                    .any(|(dep_key, dep_variant)| dep_key == key && dep_variant == variant);

                // first grab dependencies
                let func_params = params;

                let deps = (tree_path, func_deps.clone());

                let recursive_nonstatics = if is_recursive {
                    modify_self_calls(&mut body, key, variant, func_params)
                } else {
                    func_params.clone()
                };

                let node_to_edit = air_tree.find_air_tree_node(tree_path);

                let defined_function = AirTree::define_func(
                    &key.function_name,
                    &key.module_name,
                    variant,
                    func_params.clone(),
                    is_recursive,
                    recursive_nonstatics,
                    body,
                    node_to_edit.clone(),
                );

                let defined_dependencies = self.hoist_dependent_functions(
                    deps,
                    (key, variant),
                    hoisted_functions,
                    functions_to_hoist,
                    defined_function,
                );

                // now hoist full function onto validator tree
                *node_to_edit = defined_dependencies;

                hoisted_functions.push((key.clone(), variant.clone()));
            }
            HoistableFunction::CyclicFunction {
                functions,
                deps: func_deps,
            } => {
                let (key, variant) = key_var;

                let deps = (tree_path, func_deps.clone());

                let mut functions = functions.clone();

                for (_, body) in functions.iter_mut() {
                    modify_cyclic_calls(body, key, &self.cyclic_functions);
                }

                let node_to_edit = air_tree.find_air_tree_node(tree_path);

                let cyclic_func = AirTree::define_cyclic_func(
                    &key.function_name,
                    &key.module_name,
                    variant,
                    functions,
                    node_to_edit.clone(),
                );

                let defined_dependencies = self.hoist_dependent_functions(
                    deps,
                    (key, variant),
                    hoisted_functions,
                    functions_to_hoist,
                    cyclic_func,
                );

                // now hoist full function onto validator tree
                *node_to_edit = defined_dependencies;

                hoisted_functions.push((key.clone(), variant.clone()));
            }
            HoistableFunction::Link(_) => {
                todo!("This should probably be unreachable when I get to it")
            }
            HoistableFunction::CyclicLink(_) => {
                unreachable!("Sorted functions should not contain cyclic links")
            }
        }
    }

    fn hoist_dependent_functions(
        &mut self,
        deps: (&TreePath, Vec<(FunctionAccessKey, String)>),
        func_key_variant: (&FunctionAccessKey, &Variant),
        hoisted_functions: &mut Vec<(FunctionAccessKey, String)>,
        functions_to_hoist: &IndexMap<
            FunctionAccessKey,
            IndexMap<String, (TreePath, HoistableFunction)>,
        >,
        air_tree: AirTree,
    ) -> AirTree {
        let (key, variant) = func_key_variant;
        let (func_path, func_deps) = deps;

        let mut deps_vec = func_deps;
        let mut sorted_dep_vec = vec![];

        while let Some(dep) = deps_vec.pop() {
            let function_variants = functions_to_hoist
                .get(&dep.0)
                .unwrap_or_else(|| panic!("Missing Function Definition"));

            let (_, function) = function_variants
                .get(&dep.1)
                .unwrap_or_else(|| panic!("Missing Function Variant Definition"));

            match function {
                HoistableFunction::Function { deps, .. } => {
                    for (dep_generic_func, dep_variant) in deps.iter() {
                        if !(dep_generic_func == &dep.0 && dep_variant == &dep.1) {
                            sorted_dep_vec.retain(|(generic_func, variant)| {
                                !(generic_func == dep_generic_func && variant == dep_variant)
                            });

                            deps_vec.insert(0, (dep_generic_func.clone(), dep_variant.clone()));
                        }
                    }

                    sorted_dep_vec.push((dep.0.clone(), dep.1.clone()));
                }
                HoistableFunction::CyclicFunction { deps, .. } => {
                    for (dep_generic_func, dep_variant) in deps.iter() {
                        if !(dep_generic_func == &dep.0 && dep_variant == &dep.1) {
                            sorted_dep_vec.retain(|(generic_func, variant)| {
                                !(generic_func == dep_generic_func && variant == dep_variant)
                            });

                            deps_vec.insert(0, (dep_generic_func.clone(), dep_variant.clone()));
                        }
                    }
                    sorted_dep_vec.push((dep.0.clone(), dep.1.clone()));
                }
                HoistableFunction::Link(_) => todo!("Deal with Link later"),
                HoistableFunction::CyclicLink(cyclic_func) => {
                    sorted_dep_vec.retain(|(generic_func, variant)| {
                        !(generic_func == cyclic_func && variant.is_empty())
                    });

                    deps_vec.insert(0, (cyclic_func.clone(), "".to_string()));
                }
            }
        }

        sorted_dep_vec.dedup();

        sorted_dep_vec
            .into_iter()
            .fold(air_tree, |then, (dep_key, dep_variant)| {
                if
                // if the dependency is the same as the function we're hoisting
                // or we hoisted it, then skip it
                hoisted_functions
                    .iter()
                    .any(|(generic, variant)| generic == &dep_key && variant == &dep_variant)
                    || (&dep_key == key && &dep_variant == variant)
                {
                    return then;
                }

                let dependency = functions_to_hoist
                    .get(&dep_key)
                    .unwrap_or_else(|| panic!("Missing Function Definition"));

                let (dep_path, dep_function) = dependency
                    .get(&dep_variant)
                    .unwrap_or_else(|| panic!("Missing Function Variant Definition"));

                // In the case of zero args, we need to hoist the dependency function to the top of the zero arg function
                // The dependency we are hoisting should have an equal path to the function we hoisted
                // if we are going to hoist it
                if &dep_path.common_ancestor(func_path) == func_path {
                    match dep_function.clone() {
                        HoistableFunction::Function {
                            body: mut dep_air_tree,
                            deps: dependency_deps,
                            params: dependent_params,
                        } => {
                            let is_dependent_recursive = dependency_deps
                                .iter()
                                .any(|(key, variant)| &dep_key == key && &dep_variant == variant);

                            let recursive_nonstatics = if is_dependent_recursive {
                                modify_self_calls(
                                    &mut dep_air_tree,
                                    &dep_key,
                                    &dep_variant,
                                    &dependent_params,
                                )
                            } else {
                                dependent_params.clone()
                            };

                            hoisted_functions.push((dep_key.clone(), dep_variant.clone()));

                            AirTree::define_func(
                                &dep_key.function_name,
                                &dep_key.module_name,
                                &dep_variant,
                                dependent_params,
                                is_dependent_recursive,
                                recursive_nonstatics,
                                dep_air_tree,
                                then,
                            )
                        }
                        HoistableFunction::CyclicFunction { functions, .. } => {
                            let mut functions = functions.clone();

                            for (_, body) in functions.iter_mut() {
                                modify_cyclic_calls(body, &dep_key, &self.cyclic_functions);
                            }

                            hoisted_functions.push((dep_key.clone(), dep_variant.clone()));

                            AirTree::define_cyclic_func(
                                &dep_key.function_name,
                                &dep_key.module_name,
                                &dep_variant,
                                functions,
                                then,
                            )
                        }
                        HoistableFunction::Link(_) => unreachable!(),
                        HoistableFunction::CyclicLink(_) => unreachable!(),
                    }
                } else {
                    then
                }
            })
    }

    fn define_dependent_functions(
        &mut self,
        air_tree: &mut AirTree,
        function_usage: &mut IndexMap<
            FunctionAccessKey,
            IndexMap<String, (TreePath, HoistableFunction)>,
        >,
        used_functions: &mut Vec<(FunctionAccessKey, String)>,
        defined_functions: &[(FunctionAccessKey, String)],
        current_function_deps: &mut Vec<(FunctionAccessKey, String)>,
        mut function_tree_path: TreePath,
    ) {
        let Some((depth, index)) = function_tree_path.pop() else {
            return;
        };

        function_tree_path.push(depth, index);

        self.find_function_vars_and_depth(
            air_tree,
            function_usage,
            current_function_deps,
            &mut function_tree_path,
            depth + 1,
            Fields::FirstField,
        );

        for (generic_function_key, variant_name) in current_function_deps.iter() {
            if !used_functions
                .iter()
                .any(|(key, name)| key == generic_function_key && name == variant_name)
                && !defined_functions
                    .iter()
                    .any(|(key, name)| key == generic_function_key && name == variant_name)
            {
                used_functions.push((generic_function_key.clone(), variant_name.clone()));
            }
        }
    }

    fn find_function_vars_and_depth(
        &mut self,
        air_tree: &mut AirTree,
        function_usage: &mut IndexMap<
            FunctionAccessKey,
            IndexMap<String, (TreePath, HoistableFunction)>,
        >,
        dependency_functions: &mut Vec<(FunctionAccessKey, String)>,
        path: &mut TreePath,
        current_depth: usize,
        depth_index: Fields,
    ) {
        air_tree.traverse_tree_with_path(
            path,
            current_depth,
            depth_index,
            &mut |air_tree, tree_path| {
                if let AirTree::Var {
                    constructor,
                    variant_name,
                    ..
                } = air_tree
                {
                    let ValueConstructorVariant::ModuleFn {
                        name: func_name,
                        module,
                        builtin: None,
                        ..
                    } = &constructor.variant
                    else {
                        return;
                    };

                    let function_var_tipo = &constructor.tipo;

                    let generic_function_key = FunctionAccessKey {
                        module_name: module.clone(),
                        function_name: func_name.clone(),
                    };

                    let function_def = self.functions.get(&generic_function_key);

                    let Some(function_def) = function_def else {
                        let code_gen_func = self
                            .code_gen_functions
                            .get(&generic_function_key.function_name)
                            .unwrap_or_else(|| {
                                panic!(
                                    "Missing function definition for {}. Known functions: {:?}",
                                    generic_function_key.function_name,
                                    self.functions.keys(),
                                )
                            });

                        if !dependency_functions
                            .iter()
                            .any(|(key, name)| key == &generic_function_key && name.is_empty())
                        {
                            dependency_functions
                                .push((generic_function_key.clone(), "".to_string()));
                        }

                        // Code gen functions are already monomorphized
                        if let Some(func_variants) = function_usage.get_mut(&generic_function_key) {
                            let (path, _) = func_variants.get_mut("").unwrap();
                            *path = path.common_ancestor(tree_path);
                        } else {
                            // Shortcut path for compiler generated functions
                            let CodeGenFunction::Function { body, params } = code_gen_func else {
                                unreachable!()
                            };

                            let mut function_variant_path = IndexMap::new();

                            let mut body = AirTree::no_op(body.clone());

                            body.traverse_tree_with(&mut |air_tree, _| {
                                erase_opaque_type_operations(air_tree, &self.data_types);
                            });

                            function_variant_path.insert(
                                "".to_string(),
                                (
                                    tree_path.clone(),
                                    HoistableFunction::Function {
                                        body,
                                        deps: vec![],
                                        params: params.clone(),
                                    },
                                ),
                            );

                            function_usage.insert(generic_function_key, function_variant_path);
                        }
                        return;
                    };

                    let mut function_var_types = function_var_tipo
                        .arg_types()
                        .unwrap_or_else(|| panic!("Expected a function tipo with arg types"));

                    function_var_types.push(
                        function_var_tipo
                            .return_type()
                            .unwrap_or_else(|| panic!("Should have return type")),
                    );

                    let mut function_def_types = function_def
                        .arguments
                        .iter()
                        .map(|arg| convert_opaque_type(&arg.tipo, &self.data_types, true))
                        .collect_vec();

                    function_def_types.push(convert_opaque_type(
                        &function_def.return_type,
                        &self.data_types,
                        true,
                    ));

                    let mono_types: IndexMap<u64, Rc<Type>> = if !function_def_types.is_empty() {
                        function_def_types
                            .iter()
                            .zip(function_var_types.iter())
                            .flat_map(|(func_tipo, var_tipo)| {
                                get_generic_id_and_type(func_tipo, var_tipo)
                            })
                            .collect()
                    } else {
                        IndexMap::new()
                    };

                    // Don't sort here. Mono types map is already in argument order.
                    let variant = mono_types
                        .iter()
                        .map(|(_, tipo)| get_generic_variant_name(tipo))
                        .join("");

                    variant_name.clone_from(&variant);

                    if !dependency_functions
                        .iter()
                        .any(|(key, name)| key == &generic_function_key && name == &variant)
                    {
                        dependency_functions.push((generic_function_key.clone(), variant.clone()));
                    }

                    if let Some(func_variants) = function_usage.get_mut(&generic_function_key) {
                        if let Some((path, _)) = func_variants.get_mut(&variant) {
                            *path = path.common_ancestor(tree_path);
                        } else {
                            let args = function_def.arguments.clone();

                            let params = args
                                .iter()
                                .map(|arg| {
                                    arg.arg_name
                                        .get_variable_name()
                                        .map(|arg| {
                                            introduce_name(&mut self.interner, &arg.to_string())
                                        })
                                        .unwrap_or_else(|| DISCARDED.to_string())
                                })
                                .collect_vec();

                            let mut function_air_tree_body = AirTree::no_op(self.build(
                                &function_def.body,
                                &generic_function_key.module_name,
                                &[],
                            ));

                            function_air_tree_body.traverse_tree_with(&mut |air_tree, _| {
                                erase_opaque_type_operations(air_tree, &self.data_types);
                                monomorphize(air_tree, &mono_types);
                            });

                            args.iter().for_each(|arg| {
                                arg.arg_name.get_variable_name().iter().for_each(|arg| {
                                    self.interner.pop_text(arg.to_string());
                                })
                            });

                            func_variants.insert(
                                variant,
                                (
                                    tree_path.clone(),
                                    HoistableFunction::Function {
                                        body: function_air_tree_body,
                                        deps: vec![],
                                        params,
                                    },
                                ),
                            );
                        }
                    } else {
                        let args = function_def.arguments.clone();

                        let params = args
                            .iter()
                            .map(|arg| {
                                arg.arg_name
                                    .get_variable_name()
                                    .map(|arg| introduce_name(&mut self.interner, &arg.to_string()))
                                    .unwrap_or_else(|| DISCARDED.to_string())
                            })
                            .collect_vec();

                        let mut function_air_tree_body = AirTree::no_op(self.build(
                            &function_def.body,
                            &generic_function_key.module_name,
                            &[],
                        ));

                        function_air_tree_body.traverse_tree_with(&mut |air_tree, _| {
                            erase_opaque_type_operations(air_tree, &self.data_types);
                            monomorphize(air_tree, &mono_types);
                        });

                        let mut function_variant_path = IndexMap::new();

                        args.iter().for_each(|arg| {
                            arg.arg_name
                                .get_variable_name()
                                .iter()
                                .for_each(|arg| self.interner.pop_text(arg.to_string()))
                        });

                        function_variant_path.insert(
                            variant,
                            (
                                tree_path.clone(),
                                HoistableFunction::Function {
                                    body: function_air_tree_body,
                                    deps: vec![],
                                    params,
                                },
                            ),
                        );

                        function_usage.insert(generic_function_key, function_variant_path);
                    }
                }
            },
        );
    }

    fn uplc_code_gen(&mut self, mut ir_stack: Vec<Air>) -> Term<Name> {
        let mut arg_stack: Vec<Term<Name>> = vec![];

        while let Some(air_element) = ir_stack.pop() {
            let arg = self.gen_uplc(air_element, &mut arg_stack);
            if let Some(arg) = arg {
                arg_stack.push(arg);
            }
        }
        assert!(arg_stack.len() == 1, "Expected one term on the stack");
        arg_stack.pop().unwrap()
    }

    fn gen_uplc(&mut self, ir: Air, arg_stack: &mut Vec<Term<Name>>) -> Option<Term<Name>> {
        match ir {
            Air::Int { value } => Some(Term::integer(value.parse().unwrap())),
            Air::String { value } => Some(Term::string(value)),
            Air::ByteArray { bytes } => Some(Term::byte_string(bytes)),
            Air::Bool { value } => Some(Term::bool(value)),
            Air::CurvePoint { point, .. } => match point {
                Curve::Bls12_381(Bls12_381Point::G1(g1)) => Some(Term::bls12_381_g1(g1)),
                Curve::Bls12_381(Bls12_381Point::G2(g2)) => Some(Term::bls12_381_g2(g2)),
            },
            Air::Var {
                name,
                constructor,
                variant_name,
            } => match &constructor.variant {
                ValueConstructorVariant::LocalVariable { .. } => Some(Term::Var(
                    Name {
                        text: name,
                        unique: 0.into(),
                    }
                    .into(),
                )),
                ValueConstructorVariant::ModuleConstant { module, name, .. } => {
                    let access_key = FunctionAccessKey {
                        module_name: module.clone(),
                        function_name: name.clone(),
                    };

                    let definition = self
                        .constants
                        .get(&access_key)
                        .unwrap_or_else(|| panic!("unknown constant {module}.{name}"));

                    let mut value =
                        AirTree::no_op(self.build(definition, &access_key.module_name, &[]));

                    value.traverse_tree_with(&mut |air_tree, _| {
                        erase_opaque_type_operations(air_tree, &self.data_types);
                    });

                    value = self.hoist_functions_to_validator(value);

                    let term = self.uplc_code_gen(value.to_vec());

                    let mut program =
                        self.new_program(self.special_functions.apply_used_functions(term));

                    let mut interner = CodeGenInterner::new();

                    interner.program(&mut program);

                    let eval_program: Program<NamedDeBruijn> =
                        program.clean_up_no_inlines().try_into().unwrap();

                    Some(
                        eval_program
                            .eval(ExBudget::max())
                            .result()
                            .unwrap_or_else(|e| panic!("Failed to evaluate constant: {e:#?}"))
                            .try_into()
                            .unwrap(),
                    )
                }
                ValueConstructorVariant::ModuleFn {
                    name: func_name,
                    module,
                    builtin,
                    ..
                } => {
                    if let Some(func) = builtin {
                        return self.gen_uplc(
                            Air::Builtin {
                                count: 0,
                                func: *func,
                                tipo: constructor.tipo,
                            },
                            arg_stack,
                        );
                    }

                    if let Some((names, index, cyclic_name)) = self.cyclic_functions.get(&(
                        FunctionAccessKey {
                            module_name: module.clone(),
                            function_name: func_name.clone(),
                        },
                        variant_name.clone(),
                    )) {
                        let cyclic_var_name = if cyclic_name.module_name.is_empty() {
                            cyclic_name.function_name.to_string()
                        } else {
                            format!("{}_{}", cyclic_name.module_name, cyclic_name.function_name)
                        };

                        let index_name = names[*index].clone();

                        let mut arg_var = Term::var(index_name.clone());

                        for name in names.iter().rev() {
                            arg_var = arg_var.lambda(name);
                        }

                        let term = Term::var(cyclic_var_name).apply(arg_var);

                        Some(term)
                    } else {
                        let name = if !module.is_empty() {
                            format!("{module}_{func_name}{variant_name}")
                        } else {
                            format!("{func_name}{variant_name}")
                        };

                        Some(Term::Var(
                            Name {
                                text: name,
                                unique: 0.into(),
                            }
                            .into(),
                        ))
                    }
                }
                ValueConstructorVariant::Record {
                    name: constr_name, ..
                } => {
                    if constructor.tipo.is_bool() {
                        Some(Term::bool(constr_name == "True"))
                    } else if constructor.tipo.is_void() {
                        Some(Term::Constant(UplcConstant::Unit.into()))
                    } else if constructor.is_pair() {
                        let args = constructor.tipo.arg_types().unwrap();
                        let mut args = args.iter();
                        let arg_left = args.next().unwrap();
                        let arg_right = args.next().unwrap();
                        Some(
                            Term::mk_pair_data()
                                .apply(convert_type_to_data(
                                    Term::var("left".to_string()),
                                    arg_left,
                                    &self.data_types,
                                ))
                                .apply(convert_type_to_data(
                                    Term::var("right".to_string()),
                                    arg_right,
                                    &self.data_types,
                                ))
                                .lambda("right".to_string())
                                .lambda("left".to_string()),
                        )
                    } else {
                        let data_type = crate::tipo::lookup_data_type_by_tipo(
                            &self.data_types,
                            &constructor.tipo,
                        )
                        .unwrap_or_else(|| {
                            panic!(
                                "could not find data-type definition for {} within known set: {:?}",
                                constructor.tipo.to_pretty(0),
                                self.data_types.keys()
                            )
                        });

                        let (constr_index, constr_type) =
                            get_constr_index_variant(&data_type, constr_name).unwrap();

                        let list_decorator = data_type
                            .decorators
                            .iter()
                            .any(|dec| matches!(dec.kind, DecoratorKind::List));

                        let mut term = Term::empty_list();

                        if constr_type.arguments.is_empty() {
                            if !list_decorator {
                                term = Term::constr_data()
                                    .apply(Term::integer(constr_index.into()))
                                    .apply(term);
                            }

                            let mut program = self.new_program(term);

                            let mut interner = CodeGenInterner::new();

                            interner.program(&mut program);

                            let eval_program: Program<NamedDeBruijn> =
                                program.clean_up_no_inlines().try_into().unwrap();

                            let evaluated_term: Term<NamedDeBruijn> = eval_program
                                .eval(ExBudget::default())
                                .result()
                                .expect("Evaluated a constant record and got an error");

                            term = evaluated_term.try_into().unwrap();
                        } else {
                            for (index, arg) in constructor
                                .tipo
                                .arg_types()
                                .unwrap()
                                .iter()
                                .enumerate()
                                .rev()
                            {
                                term = Term::mk_cons()
                                    .apply(convert_type_to_data(
                                        Term::var(format!("arg_{index}")),
                                        arg,
                                        &self.data_types,
                                    ))
                                    .apply(term);
                            }
                            if !list_decorator {
                                term = Term::constr_data()
                                    .apply(Term::integer(constr_index.into()))
                                    .apply(term);
                            }

                            for (index, _) in constr_type.arguments.iter().enumerate().rev() {
                                term = term.lambda(format!("arg_{index}"))
                            }
                        }
                        Some(term)
                    }
                }
            },
            Air::Void => Some(Term::Constant(UplcConstant::Unit.into())),
            Air::List { count, tipo, tail } => {
                let mut args = vec![];

                for _ in 0..count {
                    let arg = arg_stack.pop().unwrap();
                    args.push(arg);
                }
                let mut constants = vec![];
                for arg in &args {
                    let maybe_const = extract_constant(arg);
                    if let Some(c) = maybe_const {
                        constants.push(c);
                    }
                }

                if constants.len() == args.len() && !tail {
                    let list = if tipo.is_map() {
                        let mut convert_keys = vec![];
                        let mut convert_values = vec![];
                        for constant in constants {
                            match constant.as_ref() {
                                UplcConstant::ProtoPair(_, _, fst, snd) => {
                                    convert_keys.push(fst.clone());
                                    convert_values.push(snd.clone());
                                }
                                _ => unreachable!(),
                            }
                        }

                        let convert_keys = builder::convert_constants_to_data(convert_keys);
                        let convert_values = builder::convert_constants_to_data(convert_values);

                        Term::Constant(
                            UplcConstant::ProtoList(
                                UplcType::Pair(UplcType::Data.into(), UplcType::Data.into()),
                                convert_keys
                                    .into_iter()
                                    .zip(convert_values)
                                    .map(|(key, value)| {
                                        UplcConstant::ProtoPair(
                                            UplcType::Data,
                                            UplcType::Data,
                                            key.into(),
                                            value.into(),
                                        )
                                    })
                                    .collect_vec(),
                            )
                            .into(),
                        )
                    } else {
                        Term::Constant(
                            UplcConstant::ProtoList(
                                UplcType::Data,
                                builder::convert_constants_to_data(constants),
                            )
                            .into(),
                        )
                    };

                    Some(list)
                } else {
                    let mut term = if tail {
                        args.pop().unwrap()
                    } else if tipo.is_map() {
                        Term::empty_map()
                    } else {
                        Term::empty_list()
                    };

                    // move this down here since the constant list path doesn't need to do this
                    let list_element_type = tipo.get_inner_types()[0].clone();

                    for arg in args.into_iter().rev() {
                        let list_item = if tipo.is_map() {
                            arg
                        } else {
                            builder::convert_type_to_data(arg, &list_element_type, &self.data_types)
                        };
                        term = Term::mk_cons().apply(list_item).apply(term);
                    }
                    Some(term)
                }
            }
            Air::ListAccessor {
                names,
                tail,
                // TODO: rename tipo -
                // tipo here refers to the list type while the actual return
                // type is nothing since this is an assignment over some expression
                tipo,
                expect_level,
            } => {
                let value = arg_stack.pop().unwrap();

                let mut term = arg_stack.pop().unwrap();

                let otherwise = if matches!(expect_level, ExpectLevel::Full | ExpectLevel::Items) {
                    arg_stack.pop().unwrap()
                } else {
                    Term::Error.delay()
                };

                let list_id = self.id_gen.next();

                let mut id_list = vec![];
                id_list.push(list_id);

                names.iter().for_each(|_| {
                    id_list.push(self.id_gen.next());
                });

                let names_types = tipo
                    .get_inner_types()
                    .into_iter()
                    .cycle()
                    .take(names.len())
                    .zip(names)
                    .zip(id_list)
                    .map(|((tipo, name), id)| (name, tipo, id))
                    .collect_vec();

                term = builder::list_access_to_uplc(
                    &names_types,
                    tail,
                    term,
                    true,
                    expect_level,
                    otherwise,
                    &self.data_types,
                )
                .apply(value);

                Some(term)
            }
            Air::Fn {
                params,
                allow_inline,
            } => {
                let mut term = arg_stack.pop().unwrap();

                for param in params.iter().rev() {
                    term = term.lambda(param);
                }
                term = if allow_inline {
                    term
                } else {
                    term.lambda(NO_INLINE)
                };

                if params.is_empty() {
                    Some(term.delay())
                } else {
                    Some(term)
                }
            }
            Air::Call { count, .. } => {
                if count >= 1 {
                    let mut term = arg_stack.pop().unwrap();
                    for _ in 0..count {
                        let arg = arg_stack.pop().unwrap();

                        term = term.apply(arg);
                    }
                    Some(term)
                } else {
                    let term = arg_stack.pop().unwrap();
                    match term.pierce_no_inlines_ref() {
                        Term::Var(_) => Some(term.force()),
                        Term::Delay(inner_term) => Some(inner_term.as_ref().clone()),
                        Term::Apply { .. } => Some(term.force()),
                        _ => unreachable!(
                            "Shouldn't call anything other than var or apply\n{:#?}",
                            term
                        ),
                    }
                }
            }
            Air::Builtin { func, tipo, count } => {
                let mut arg_vec = vec![];
                for _ in 0..count {
                    arg_vec.push(arg_stack.pop().unwrap());
                }

                let ret_tipo = match tipo.as_ref() {
                    Type::Fn { ret, .. } => ret,
                    // In this case the Air Opcode only holds the return type and not the function type
                    _ => &tipo,
                };

                let term = match &func {
                    DefaultFunction::IfThenElse
                    | DefaultFunction::ChooseUnit
                    | DefaultFunction::Trace
                    | DefaultFunction::ChooseList
                    | DefaultFunction::ChooseData
                    | DefaultFunction::MkCons
                    | DefaultFunction::UnConstrData => {
                        builder::special_case_builtin(&func, tipo, count, arg_vec, &self.data_types)
                    }
                    DefaultFunction::FstPair | DefaultFunction::SndPair => {
                        builder::undata_builtin(&func, count, ret_tipo, arg_vec, &self.data_types)
                    }
                    DefaultFunction::HeadList if !tipo.is_pair() => {
                        builder::undata_builtin(&func, count, ret_tipo, arg_vec, &self.data_types)
                    }
                    _ => {
                        let mut term: Term<Name> = func.into();

                        term = builder::apply_builtin_forces(term, func.force_count());

                        if func.arg_is_unit() {
                            term = term.apply(Term::unit())
                        } else {
                            for arg in arg_vec {
                                term = term.apply(arg.clone());
                            }
                        }

                        term
                    }
                };

                Some(term)
            }
            Air::BinOp {
                name: mut op,
                // changed this to argument tipo
                left_tipo,
                right_tipo,
                ..
            } => {
                let mut left = arg_stack.pop().unwrap();
                let mut right = arg_stack.pop().unwrap();

                let uplc_type = left_tipo.get_uplc_type();

                // When operators are symmetric, favor putting constant first to allow currying
                // optimisation to kick-in more easily.
                if !left.is_constant() && right.is_constant() {
                    // If the operator is symmetric, it's safe to swap left and right
                    if op.is_symmetric() {
                        std::mem::swap(&mut left, &mut right);
                    // Special case for SubInt, which is easy to transform into a sum
                    } else if matches!(op, BinOp::SubInt)
                        && let Some(minus_right) = right.try_negate()
                    {
                        right = left;
                        left = minus_right;
                        op = BinOp::AddInt;
                    }
                }

                let term = match op {
                    BinOp::And => left.delayed_if_then_else(right, Term::bool(false)),
                    BinOp::Or => left.delayed_if_then_else(Term::bool(true), right),
                    BinOp::Eq | BinOp::NotEq => {
                        let builtin = match &uplc_type {
                            Some(UplcType::Integer) => Term::equals_integer(),
                            Some(UplcType::String) => Term::equals_string(),
                            Some(UplcType::ByteString) => Term::equals_bytestring(),
                            Some(UplcType::Bls12_381G1Element) => Term::bls12_381_g1_equal(),
                            Some(UplcType::Bls12_381G2Element) => Term::bls12_381_g2_equal(),
                            Some(UplcType::Bool | UplcType::Unit) => Term::unit(),
                            Some(UplcType::List(_) | UplcType::Pair(_, _) | UplcType::Data)
                            | None => Term::equals_data(),
                            Some(UplcType::Bls12_381MlResult) => {
                                panic!("ML Result equality is not supported")
                            }
                        };

                        let binop_eq = match uplc_type {
                            Some(UplcType::Bool) => {
                                if matches!(op, BinOp::Eq) {
                                    if left.is_true() {
                                        right
                                    } else if left.is_false() {
                                        right.if_then_else(Term::bool(false), Term::bool(true))
                                    } else {
                                        left.delayed_if_then_else(
                                            right.clone(),
                                            right.if_then_else(Term::bool(false), Term::bool(true)),
                                        )
                                    }
                                } else {
                                    if left.is_true() {
                                        right.if_then_else(Term::bool(false), Term::bool(true))
                                    } else if left.is_false() {
                                        right
                                    } else {
                                        left.delayed_if_then_else(
                                            right
                                                .clone()
                                                .if_then_else(Term::bool(false), Term::bool(true)),
                                            right.clone(),
                                        )
                                    }
                                }
                            }
                            Some(UplcType::List(_)) if left_tipo.is_map() => builtin
                                .apply(Term::map_data().apply(left))
                                .apply(Term::map_data().apply(right)),
                            Some(UplcType::List(_)) => builtin
                                .apply(Term::list_data().apply(left))
                                .apply(Term::list_data().apply(right)),
                            Some(UplcType::Pair(_, _)) => {
                                builtin
                                    .apply(Term::map_data().apply(
                                        Term::mk_cons().apply(left).apply(Term::empty_map()),
                                    ))
                                    .apply(Term::map_data().apply(
                                        Term::mk_cons().apply(right).apply(Term::empty_map()),
                                    ))
                            }
                            Some(
                                UplcType::Data
                                | UplcType::Bls12_381G1Element
                                | UplcType::Bls12_381G2Element
                                | UplcType::Bls12_381MlResult
                                | UplcType::Integer
                                | UplcType::String
                                | UplcType::ByteString,
                            ) => builtin.apply(left).apply(right),

                            None => {
                                let mut left = left;
                                let mut right = right;

                                let left_data_type =
                                    lookup_data_type_by_tipo(&self.data_types, &left_tipo);

                                let right_data_type =
                                    lookup_data_type_by_tipo(&self.data_types, &right_tipo);

                                if left_data_type
                                    .map(|d| {
                                        d.decorators
                                            .iter()
                                            .any(|dec| matches!(dec.kind, DecoratorKind::List))
                                    })
                                    .unwrap_or(false)
                                {
                                    left = Term::list_data().apply(left)
                                }

                                if right_data_type
                                    .map(|d| {
                                        d.decorators
                                            .iter()
                                            .any(|dec| matches!(dec.kind, DecoratorKind::List))
                                    })
                                    .unwrap_or(false)
                                {
                                    right = Term::list_data().apply(right)
                                }

                                builtin.apply(left).apply(right)
                            }
                            Some(UplcType::Unit) => {
                                left.choose_unit(right.choose_unit(Term::bool(true)))
                            }
                        };

                        if !left_tipo.is_bool() && matches!(op, BinOp::NotEq) {
                            binop_eq.if_then_else(Term::bool(false), Term::bool(true))
                        } else {
                            binop_eq
                        }
                    }
                    BinOp::LtInt => Term::Builtin(DefaultFunction::LessThanInteger)
                        .apply(left)
                        .apply(right),
                    BinOp::LtEqInt => Term::Builtin(DefaultFunction::LessThanEqualsInteger)
                        .apply(left)
                        .apply(right),
                    BinOp::GtEqInt => Term::Builtin(DefaultFunction::LessThanEqualsInteger)
                        .apply(right)
                        .apply(left),
                    BinOp::GtInt => Term::Builtin(DefaultFunction::LessThanInteger)
                        .apply(right)
                        .apply(left),
                    BinOp::AddInt => Term::add_integer().apply(left).apply(right),
                    BinOp::SubInt => Term::Builtin(DefaultFunction::SubtractInteger)
                        .apply(left)
                        .apply(right),
                    BinOp::MultInt => Term::Builtin(DefaultFunction::MultiplyInteger)
                        .apply(left)
                        .apply(right),
                    BinOp::DivInt => Term::Builtin(DefaultFunction::DivideInteger)
                        .apply(left)
                        .apply(right),
                    BinOp::ModInt => Term::Builtin(DefaultFunction::ModInteger)
                        .apply(left)
                        .apply(right),
                };
                Some(term)
            }
            Air::DefineFunc {
                func_name,
                module_name,
                variant_name,
                variant,
            } => {
                let func_name = if module_name.is_empty() {
                    format!("{func_name}{variant_name}")
                } else {
                    format!("{module_name}_{func_name}{variant_name}")
                };

                match variant {
                    air::FunctionVariants::Standard(params) => {
                        let mut func_body = arg_stack.pop().unwrap();

                        let term = arg_stack.pop().unwrap();

                        if params.is_empty() {
                            func_body = func_body.delay();
                        }

                        let func_body = params
                            .into_iter()
                            .rfold(func_body, |term, arg| term.lambda(arg))
                            .lambda(NO_INLINE);

                        Some(term.lambda(func_name).apply(func_body))
                    }
                    air::FunctionVariants::Recursive {
                        params,
                        recursive_nonstatic_params,
                    } => {
                        let mut func_body = arg_stack.pop().unwrap();

                        let term = arg_stack.pop().unwrap();

                        let no_statics = recursive_nonstatic_params == params;

                        if recursive_nonstatic_params.is_empty() || params.is_empty() {
                            func_body = func_body.delay();
                        }

                        let func_body = recursive_nonstatic_params
                            .iter()
                            .rfold(func_body, |term, arg| term.lambda(arg));

                        let func_body = func_body.lambda(func_name.clone());

                        if no_statics {
                            // If we don't have any recursive-static params, we can just emit the function as is
                            Some(
                                term.lambda(func_name.clone())
                                    .apply(
                                        Term::var(func_name.clone())
                                            .apply(Term::var(func_name.clone())),
                                    )
                                    .lambda(func_name)
                                    .apply(func_body.lambda(NO_INLINE)),
                            )
                        } else {
                            // If we have parameters that remain static in each recursive call,
                            // we can construct an *outer* function to take those in
                            // and simplify the recursive part to only accept the non-static arguments
                            let mut recursive_func_body =
                                Term::var(&func_name).apply(Term::var(&func_name));

                            if recursive_nonstatic_params.is_empty() {
                                recursive_func_body = recursive_func_body.force();
                            }

                            // Introduce a parameter for each parameter
                            // NOTE: we use recursive_nonstatic_params here because
                            // if this is recursive, those are the ones that need to be passed
                            // each time
                            for param in recursive_nonstatic_params.into_iter() {
                                recursive_func_body = recursive_func_body.apply(Term::var(param));
                            }

                            // Then construct an outer function with *all* parameters, not just the nonstatic ones.
                            let mut outer_func_body =
                                recursive_func_body.lambda(&func_name).apply(func_body);

                            // Now, add *all* parameters, so that other call sites don't know the difference
                            outer_func_body = params
                                .clone()
                                .into_iter()
                                .rfold(outer_func_body, |term, arg| term.lambda(arg));

                            // And finally, fold that definition into the rest of our program
                            Some(
                                term.lambda(&func_name)
                                    .apply(outer_func_body.lambda(NO_INLINE)),
                            )
                        }
                    }
                    air::FunctionVariants::Cyclic(contained_functions) => {
                        let mut cyclic_functions = vec![];

                        for params in contained_functions {
                            let func_body = arg_stack.pop().unwrap();

                            cyclic_functions.push((params, func_body));
                        }
                        let mut term = arg_stack.pop().unwrap();

                        let mut cyclic_body = Term::var("__chooser");

                        for (params, func_body) in cyclic_functions.into_iter() {
                            let mut function = func_body;
                            if params.is_empty() {
                                function = function.delay();
                            } else {
                                for param in params.iter().rev() {
                                    function = function.lambda(param);
                                }
                            }

                            // We basically Scott encode our function bodies and use the chooser function
                            // to determine which function body and params is run
                            // For example say there is a cycle of 3 function bodies
                            // Our choose function can look like this:
                            // \func1 -> \func2 -> \func3 -> func1
                            // In this case our chooser is a function that takes in 3 functions
                            // and returns the first one to run
                            cyclic_body = cyclic_body.apply(function)
                        }

                        term = term
                            .lambda(&func_name)
                            .apply(Term::var(&func_name).apply(Term::var(&func_name)))
                            .lambda(&func_name)
                            .apply(
                                cyclic_body
                                    .lambda("__chooser")
                                    .lambda(func_name)
                                    .lambda(NO_INLINE),
                            );

                        Some(term)
                    }
                }
            }

            Air::Let { name } => {
                let arg = arg_stack.pop().unwrap();

                let mut term = arg_stack.pop().unwrap();

                term = term.lambda(name).apply(arg);

                Some(term)
            }
            Air::CastFromData { tipo, full_cast } => {
                let mut term = arg_stack.pop().unwrap();

                term = if full_cast {
                    unknown_data_to_type(term, &tipo, &self.data_types)
                } else {
                    known_data_to_type(term, &tipo, &self.data_types)
                };

                if extract_constant(term.pierce_no_inlines_ref()).is_some() {
                    let mut program = self.new_program(term);

                    let mut interner = CodeGenInterner::new();

                    interner.program(&mut program);

                    let eval_program: Program<NamedDeBruijn> =
                        program.clean_up_no_inlines().try_into().unwrap();

                    let evaluated_term: Term<NamedDeBruijn> = eval_program
                        .eval(ExBudget::default())
                        .result()
                        .expect("Evaluated on unwrapping a data constant and got an error");

                    term = evaluated_term.try_into().unwrap();
                }

                Some(term)
            }
            Air::CastToData { tipo } => {
                let mut term = arg_stack.pop().unwrap();

                if extract_constant(term.pierce_no_inlines_ref()).is_some() {
                    term = builder::convert_type_to_data(term, &tipo, &self.data_types);

                    let mut program = self.new_program(term);

                    let mut interner = CodeGenInterner::new();

                    interner.program(&mut program);

                    let eval_program: Program<NamedDeBruijn> =
                        program.clean_up_no_inlines().try_into().unwrap();

                    let evaluated_term: Term<NamedDeBruijn> = eval_program
                        .eval(ExBudget::default())
                        .result()
                        .expect("Evaluated on wrapping a constant into data and got an error");

                    term = evaluated_term.try_into().unwrap();
                } else {
                    term = builder::convert_type_to_data(term, &tipo, &self.data_types);
                }

                Some(term)
            }
            Air::AssertBool { is_true } => {
                let value = arg_stack.pop().unwrap();

                let mut term = arg_stack.pop().unwrap();
                let otherwise = arg_stack.pop().unwrap();

                if is_true {
                    term = value.if_then_else(term.delay(), otherwise).force()
                } else {
                    term = value.if_then_else(otherwise, term.delay()).force()
                }
                Some(term)
            }
            Air::When {
                subject_name,
                // using subject type here
                subject_tipo: tipo,
                ..
            } => {
                let subject = arg_stack.pop().unwrap();

                let uplc_type = tipo.get_uplc_type();

                let subject = match uplc_type {
                    Some(
                        UplcType::Bool
                        | UplcType::Integer
                        | UplcType::String
                        | UplcType::ByteString
                        | UplcType::Unit
                        | UplcType::List(_)
                        | UplcType::Pair(_, _)
                        | UplcType::Bls12_381G1Element
                        | UplcType::Bls12_381G2Element
                        | UplcType::Bls12_381MlResult,
                    ) => subject,

                    Some(UplcType::Data) => subject,

                    None => {
                        let data_type = lookup_data_type_by_tipo(&self.data_types, &tipo)
                            .expect("Found constr with no data type?");

                        let list_decorator = data_type
                            .decorators
                            .iter()
                            .any(|dec| matches!(dec.kind, DecoratorKind::List));

                        if list_decorator {
                            subject
                        } else {
                            Term::var(CONSTR_INDEX_EXPOSER).apply(subject)
                        }
                    }
                };

                let mut term = arg_stack.pop().unwrap();

                term = term.lambda(subject_name).apply(subject);

                Some(term)
            }
            Air::Clause {
                subject_tipo: tipo,
                subject_name,
            } => {
                // clause to compare
                let clause = arg_stack.pop().unwrap();

                // the body to be run if the clause matches
                let body = arg_stack.pop().unwrap();

                // the next branch in the when expression
                // Expected to be delayed
                let term = arg_stack.pop().unwrap();

                assert!(matches!(term, Term::Delay(_) | Term::Var(_)));

                let other_clauses = term.clone();

                let body = if tipo.is_bool() {
                    if matches!(clause, Term::Constant(boolean) if matches!(boolean.as_ref(), UplcConstant::Bool(true)))
                    {
                        Term::var(subject_name)
                            .if_then_else(body.delay(), other_clauses)
                            .force()
                    } else {
                        Term::var(subject_name)
                            .if_then_else(other_clauses, body.delay())
                            .force()
                    }
                } else {
                    let uplc_type = tipo.get_uplc_type();

                    let condition = match uplc_type {
                        Some(
                            UplcType::Bool
                            | UplcType::Unit
                            | UplcType::List(_)
                            | UplcType::Pair(_, _)
                            | UplcType::Bls12_381MlResult,
                        ) => unreachable!("{:#?}", tipo),
                        Some(UplcType::Data) => unimplemented!(),
                        Some(UplcType::Integer) => Term::equals_integer()
                            .apply(clause)
                            .apply(Term::var(subject_name)),
                        Some(UplcType::String) => Term::equals_string()
                            .apply(clause)
                            .apply(Term::var(subject_name)),
                        Some(UplcType::ByteString) => Term::equals_bytestring()
                            .apply(clause)
                            .apply(Term::var(subject_name)),
                        Some(UplcType::Bls12_381G1Element) => Term::bls12_381_g1_equal()
                            .apply(clause)
                            .apply(Term::var(subject_name)),
                        Some(UplcType::Bls12_381G2Element) => Term::bls12_381_g2_equal()
                            .apply(clause)
                            .apply(Term::var(subject_name)),
                        None => Term::equals_integer()
                            .apply(clause)
                            .apply(Term::var(subject_name)),
                    };

                    condition.delay_true_if_then_else(body, other_clauses)
                };

                Some(body)
            }
            Air::ListClause {
                tail_name,
                next_tail_name,
                ..
            } => {
                // no longer need to pop off discard
                let body = arg_stack.pop().unwrap();
                let mut term = arg_stack.pop().unwrap();

                assert!(matches!(term, Term::Delay(_)));

                term = if let Some((current_tail, next_tail_name)) = next_tail_name {
                    term.force()
                        .lambda(next_tail_name)
                        .apply(Term::tail_list().apply(Term::var(current_tail.clone())))
                        .delay()
                } else {
                    term
                };

                term = Term::var(tail_name).delay_empty_choose_list(body, term);

                Some(term)
            }
            Air::If { .. } => {
                let condition = arg_stack.pop().unwrap();
                let then = arg_stack.pop().unwrap();
                let mut term = arg_stack.pop().unwrap();

                term = condition.delayed_if_then_else(then, term);

                Some(term)
            }
            Air::Constr { tag, count, tipo } => {
                let mut arg_vec = vec![];
                for _ in 0..count {
                    arg_vec.push(arg_stack.pop().unwrap());
                }

                let mut term = Term::empty_list();

                for (index, arg) in arg_vec.iter().enumerate().rev() {
                    term = Term::mk_cons()
                        .apply(builder::convert_type_to_data(
                            arg.clone(),
                            &tipo.arg_types().unwrap()[index],
                            &self.data_types,
                        ))
                        .apply(term);
                }

                if let Some(constr_index) = tag {
                    term = Term::constr_data()
                        .apply(Term::integer(constr_index.into()))
                        .apply(term);
                }

                if arg_vec.iter().all(|item| {
                    let maybe_const = extract_constant(item.pierce_no_inlines_ref());
                    maybe_const.is_some()
                }) {
                    let mut program = self.new_program(term);

                    let mut interner = CodeGenInterner::new();

                    interner.program(&mut program);

                    let eval_program: Program<NamedDeBruijn> =
                        program.clean_up_no_inlines().try_into().unwrap();

                    let evaluated_term: Term<NamedDeBruijn> = eval_program
                        .eval(ExBudget::default())
                        .result()
                        .expect("Evaluated a constant record with args and got an error");

                    term = evaluated_term.try_into().unwrap();
                }

                Some(term)
            }
            Air::FieldsExpose {
                indices,
                is_expect,
                list_decorator,
            } => {
                let mut id_list = vec![];

                let mut value = arg_stack.pop().unwrap();

                let mut term = arg_stack.pop().unwrap();

                let otherwise = if is_expect {
                    arg_stack.pop().unwrap()
                } else {
                    Term::Error.delay()
                };

                let list_id = self.id_gen.next();

                id_list.push(list_id);

                indices.iter().for_each(|_| {
                    id_list.push(self.id_gen.next());
                });

                let names_types = indices
                    .iter()
                    .cloned()
                    .zip(id_list)
                    .map(|(item, id)| (item.1, item.2, id))
                    .collect_vec();

                let named_indices = names_types
                    .iter()
                    .skip_while(|(name, _, _)| name == DISCARDED)
                    .collect_vec();

                if !named_indices.is_empty() || is_expect {
                    term = builder::list_access_to_uplc(
                        &names_types,
                        false,
                        term,
                        false,
                        is_expect.into(),
                        otherwise,
                        &self.data_types,
                    );

                    if !list_decorator {
                        value = Term::var(CONSTR_FIELDS_EXPOSER).apply(value);
                    }

                    term = term.apply(value);

                    Some(term)
                } else {
                    Some(term)
                }
            }
            Air::FieldsEmpty { list_decorator } => {
                let mut value = arg_stack.pop().unwrap();

                let mut term = arg_stack.pop().unwrap();
                let otherwise = arg_stack.pop().unwrap();

                if !list_decorator {
                    value = Term::var(CONSTR_FIELDS_EXPOSER).apply(value)
                }

                term = value.choose_list(term.delay(), otherwise).force();

                Some(term)
            }
            Air::ListEmpty => {
                let value = arg_stack.pop().unwrap();

                let mut term = arg_stack.pop().unwrap();
                let otherwise = arg_stack.pop().unwrap();

                term = value.choose_list(term.delay(), otherwise).force();

                Some(term)
            }
            Air::Tuple { count, tipo } => {
                let mut args = vec![];

                let tuple_sub_types = tipo.get_inner_types();

                for _ in 0..count {
                    let arg = arg_stack.pop().unwrap();
                    args.push(arg);
                }
                let mut constants = vec![];
                for arg in &args {
                    let maybe_const = extract_constant(arg);
                    if let Some(c) = maybe_const {
                        constants.push(c);
                    }
                }

                if constants.len() == args.len() {
                    let data_constants = builder::convert_constants_to_data(constants);

                    let term = Term::Constant(
                        UplcConstant::ProtoList(UplcType::Data, data_constants).into(),
                    );
                    Some(term)
                } else {
                    let mut term = Term::empty_list();
                    for (arg, tipo) in args.into_iter().zip(tuple_sub_types).rev() {
                        term = Term::mk_cons()
                            .apply(builder::convert_type_to_data(arg, &tipo, &self.data_types))
                            .apply(term);
                    }
                    Some(term)
                }
            }
            Air::Pair { tipo } => {
                let fst = arg_stack.pop().unwrap();
                let snd = arg_stack.pop().unwrap();

                match (extract_constant(&fst), extract_constant(&snd)) {
                    (Some(fst), Some(snd)) => {
                        let mut pair_fields = builder::convert_constants_to_data(vec![fst, snd]);
                        let term = Term::Constant(
                            UplcConstant::ProtoPair(
                                UplcType::Data,
                                UplcType::Data,
                                pair_fields.remove(0).into(),
                                pair_fields.remove(0).into(),
                            )
                            .into(),
                        );
                        Some(term)
                    }
                    _ => {
                        let term = Term::mk_pair_data()
                            .apply(builder::convert_type_to_data(
                                fst,
                                &tipo.get_inner_types()[0],
                                &self.data_types,
                            ))
                            .apply(builder::convert_type_to_data(
                                snd,
                                &tipo.get_inner_types()[1],
                                &self.data_types,
                            ));

                        Some(term)
                    }
                }
            }
            Air::RecordUpdate {
                highest_index,
                indices,
                tipo,
            } => {
                let tail_name_prefix = "__tail_index";

                let data_type =
                    lookup_data_type_by_tipo(&self.data_types, &tipo).unwrap_or_else(|| {
                        panic!("Attempted record update on an unknown type!\ntype: {tipo:#?}")
                    });

                assert!(
                    !data_type.is_never(),
                    "Attempted record update on a Never type.",
                );

                let list_decorator = data_type
                    .decorators
                    .iter()
                    .any(|dec| matches!(dec.kind, DecoratorKind::List));

                let constructor_field_count = data_type.constructors[0].arguments.len();
                let mut record = arg_stack.pop().unwrap();

                let mut args = IndexMap::new();
                let mut unchanged_field_indices = vec![];
                // plus 2 so we get one index higher than the record update index
                // then we add that and any other unchanged fields to an array to later create the
                // lambda bindings
                unchanged_field_indices.push(0);
                let mut prev_index = 0;

                for (index, tipo) in indices
                    .into_iter()
                    .sorted_by(|(index1, _), (index2, _)| index1.cmp(index2))
                {
                    let arg = arg_stack.pop().unwrap();
                    args.insert(index, (tipo.clone(), arg));

                    for field_index in (prev_index + 1)..index {
                        unchanged_field_indices.push(field_index);
                    }
                    prev_index = index;
                }

                unchanged_field_indices.push(prev_index + 1);

                let mut term = Term::var(format!("{tail_name_prefix}_{}", highest_index + 1));

                for current_index in (0..(highest_index + 1)).rev() {
                    let tail_name = format!("{tail_name_prefix}_{current_index}");

                    if let Some((tipo, arg)) = args.get(&current_index) {
                        term = Term::mk_cons()
                            .apply(builder::convert_type_to_data(
                                arg.clone(),
                                tipo,
                                &self.data_types,
                            ))
                            .apply(term);
                    } else {
                        term = Term::mk_cons()
                            .apply(Term::head_list().apply(Term::var(tail_name)))
                            .apply(term);
                    }
                }

                if !list_decorator {
                    term = Term::constr_data()
                        .apply(Term::integer(0.into()))
                        .apply(term);
                }

                if unchanged_field_indices.len() > 1 {
                    let (prev_index, rest_list) = unchanged_field_indices
                        .split_last()
                        .unwrap_or_else(|| panic!("WHAT HAPPENED"));

                    let mut prev_index = *prev_index;

                    for index in rest_list.iter().rev() {
                        let index = *index;
                        let suffix_tail = format!("{tail_name_prefix}_{prev_index}");
                        let tail = format!("{tail_name_prefix}_{index}");

                        let mut tail_list = Term::var(tail);

                        if index < prev_index {
                            tail_list = tail_list.repeat_tail_list(prev_index - index);

                            if prev_index == constructor_field_count {
                                term = term.lambda(suffix_tail).apply(Term::empty_list());
                            } else {
                                term = term.lambda(suffix_tail).apply(tail_list);
                            }
                        }
                        prev_index = index;
                    }
                }

                if !list_decorator {
                    record = Term::var(CONSTR_FIELDS_EXPOSER).apply(record)
                }

                term = term.lambda(format!("{tail_name_prefix}_0")).apply(record);

                Some(term)
            }
            Air::UnOp { op } => {
                let value = arg_stack.pop().unwrap();

                let term = match op {
                    UnOp::Not => value.if_then_else(Term::bool(false), Term::bool(true)),
                    UnOp::Negate => {
                        if let Term::Constant(c) = &value {
                            if let UplcConstant::Integer(i) = c.as_ref() {
                                Term::integer(-i)
                            } else {
                                Term::subtract_integer()
                                    .apply(Term::integer(0.into()))
                                    .apply(value)
                            }
                        } else {
                            Term::subtract_integer()
                                .apply(Term::integer(0.into()))
                                .apply(value)
                        }
                    }
                };

                Some(term)
            }
            Air::TupleAccessor {
                tipo,
                names,
                is_expect,
            } => {
                let inner_types = tipo.get_inner_types();
                let value = arg_stack.pop().unwrap();

                let mut term = arg_stack.pop().unwrap();
                let otherwise = if is_expect {
                    arg_stack.pop().unwrap()
                } else {
                    Term::Error.delay()
                };
                let list_id = self.id_gen.next();

                let mut id_list = vec![];
                id_list.push(list_id);

                names.iter().for_each(|_| {
                    id_list.push(self.id_gen.next());
                });

                let names_types = names
                    .into_iter()
                    .zip(inner_types)
                    .zip(id_list)
                    .map(|((name, tipo), id)| (name, tipo, id))
                    .collect_vec();

                term = builder::list_access_to_uplc(
                    &names_types,
                    false,
                    term,
                    false,
                    is_expect.into(),
                    otherwise,
                    &self.data_types,
                )
                .apply(value);

                Some(term)
            }
            Air::PairAccessor {
                fst,
                snd,
                tipo,
                is_expect,
            } => {
                let inner_types = tipo.get_inner_types();
                let value = arg_stack.pop().unwrap();

                let mut term = arg_stack.pop().unwrap();
                let otherwise = if is_expect {
                    arg_stack.pop().unwrap()
                } else {
                    Term::Error.delay()
                };

                let list_id = self.id_gen.next();

                if let Some(name) = snd {
                    let value = Term::snd_pair().apply(Term::var(format!("__pair_{list_id}")));
                    term = if is_expect {
                        if otherwise == Term::Error.delay() {
                            term.lambda(name).apply(unknown_data_to_type(
                                value,
                                &inner_types[1],
                                &self.data_types,
                            ))
                        } else {
                            softcast_data_to_type_otherwise(
                                value,
                                &name,
                                &inner_types[1],
                                term,
                                otherwise.clone(),
                                &self.data_types,
                            )
                        }
                    } else {
                        term.lambda(name).apply(known_data_to_type(
                            value,
                            &inner_types[1],
                            &self.data_types,
                        ))
                    }
                }

                if let Some(name) = fst {
                    let value = Term::fst_pair().apply(Term::var(format!("__pair_{list_id}")));
                    term = if is_expect {
                        if otherwise == Term::Error.delay() {
                            term.lambda(name).apply(unknown_data_to_type(
                                value,
                                &inner_types[0],
                                &self.data_types,
                            ))
                        } else {
                            softcast_data_to_type_otherwise(
                                value,
                                &name,
                                &inner_types[0],
                                term,
                                otherwise,
                                &self.data_types,
                            )
                        }
                    } else {
                        term.lambda(name).apply(known_data_to_type(
                            value,
                            &inner_types[0],
                            &self.data_types,
                        ))
                    }
                }

                term = term.lambda(format!("__pair_{list_id}")).apply(value);

                Some(term)
            }
            Air::Trace { .. } => {
                let text = arg_stack.pop().unwrap();

                let term = arg_stack.pop().unwrap();

                let term = term.delayed_trace(text);

                Some(term)
            }
            Air::ErrorTerm { validator, .. } => {
                if validator {
                    Some(Term::Error.apply(Term::Error.force()))
                } else {
                    Some(Term::Error)
                }
            }

            Air::NoOp => None,
            Air::SoftCastLet { name, tipo } => {
                let value = arg_stack.pop().unwrap();
                let then = arg_stack.pop().unwrap();
                let otherwise = arg_stack.pop().unwrap();

                if otherwise == Term::Error.delay() {
                    Some(then.lambda(name).apply(unknown_data_to_type(
                        value,
                        &tipo,
                        &self.data_types,
                    )))
                } else {
                    Some(softcast_data_to_type_otherwise(
                        value,
                        &name,
                        &tipo,
                        then,
                        otherwise,
                        &self.data_types,
                    ))
                }
            }
            Air::ExtractField { tipo } => {
                let arg = arg_stack.pop().unwrap();

                Some(known_data_to_type(
                    Term::head_list().apply(arg),
                    &tipo,
                    &self.data_types,
                ))
            }
        }
    }
}

fn handle_assigns(
    subject_name: &String,
    subject_tipo: Rc<Type>,
    assigns: &[Assigned],
    stick_set: &mut TreeSet,
    then: AirTree,
) -> AirTree {
    match assigns {
        [] => then,
        [assign, rest @ ..] => {
            let Assigned { path, assigned } = assign;

            let current_tipo = get_tipo_by_path(subject_tipo.clone(), path);
            let builtins_path = Builtins::new_from_path(subject_tipo.clone(), path.clone());
            let current_subject_name = if builtins_path.is_empty() {
                subject_name.clone()
            } else {
                format!("{subject_name}_{builtins_path}")
            };

            // Transition process from previous to current
            let builtins_to_add = stick_set.diff_union_builtins(builtins_path.clone());

            // Previous path to apply the transition process too
            let prev_builtins = Builtins {
                vec: builtins_path.vec[0..(builtins_path.len() - builtins_to_add.len())].to_vec(),
            };

            let prev_subject_name = if prev_builtins.is_empty() {
                subject_name.clone()
            } else {
                format!("{subject_name}_{prev_builtins}")
            };
            let prev_tipo = prev_builtins
                .vec
                .last()
                .map_or(subject_tipo.clone(), |last| last.tipo());

            let assignment = AirTree::let_assignment(
                assigned,
                AirTree::local_var(current_subject_name, current_tipo),
                handle_assigns(subject_name, subject_tipo, rest, stick_set, then),
            );

            builtins_to_add.produce_air(prev_subject_name, prev_tipo, assignment)
        }
    }
}
