use std::collections::{HashMap, HashSet};

use crate::{
    ast::{BinaryOp, MatchPatternKind},
    check::{CheckedFunction, CheckedProgram, TExpr, TExprKind, TMatchArm},
    diag::Diagnostic,
    std_runtime,
    types::Type,
};

pub fn emit(_file: &str, program: &CheckedProgram) -> Result<String, Vec<Diagnostic>> {
    let mut out = String::new();
    let layouts = struct_layouts(program);
    let enum_layouts = enum_layouts(program);
    let string_globals = StringGlobals::collect(program);
    let needs_process_runtime = needs_process_runtime(program);

    out.push_str("; Module generated by glagol\n");
    out.push_str(&format!("; Slovo module: {}\n\n", program.module));
    out.push_str("declare void @print_i32(i32)\n\n");
    out.push_str("declare void @print_i64(i64)\n\n");
    out.push_str("declare void @print_u32(i32)\n\n");
    out.push_str("declare void @print_u64(i64)\n\n");
    out.push_str("declare void @print_f64(double)\n\n");
    out.push_str("declare void @print_string(ptr)\n\n");
    out.push_str("declare void @print_bool(i1)\n\n");
    out.push_str("declare i32 @string_len(ptr)\n\n");
    out.push_str("declare ptr @__glagol_string_concat(ptr, ptr)\n\n");
    out.push_str("declare i64 @__glagol_string_byte_at_result(ptr, i32)\n\n");
    out.push_str("declare ptr @__glagol_string_slice_result(ptr, i32, i32)\n\n");
    out.push_str("declare i1 @__glagol_string_starts_with(ptr, ptr)\n\n");
    out.push_str("declare i1 @__glagol_string_ends_with(ptr, ptr)\n\n");
    out.push_str("declare i64 @__glagol_string_parse_i32_result(ptr)\n\n");
    out.push_str("declare i32 @__glagol_string_parse_i64_result(ptr, ptr)\n\n");
    out.push_str("declare i64 @__glagol_string_parse_u32_result(ptr)\n\n");
    out.push_str("declare i32 @__glagol_string_parse_u64_result(ptr, ptr)\n\n");
    out.push_str("declare i32 @__glagol_string_parse_f64_result(ptr, ptr)\n\n");
    out.push_str("declare i32 @__glagol_string_parse_bool_result(ptr, ptr)\n\n");
    out.push_str("declare ptr @__glagol_json_quote_string(ptr)\n\n");
    out.push_str("declare ptr @__glagol_json_parse_string_value_result(ptr)\n\n");
    out.push_str("declare i32 @__glagol_json_parse_bool_value_result(ptr, ptr)\n\n");
    out.push_str("declare i64 @__glagol_json_parse_i32_value_result(ptr)\n\n");
    out.push_str("declare i64 @__glagol_json_parse_u32_value_result(ptr)\n\n");
    out.push_str("declare i32 @__glagol_json_parse_i64_value_result(ptr, ptr)\n\n");
    out.push_str("declare i32 @__glagol_json_parse_u64_value_result(ptr, ptr)\n\n");
    out.push_str("declare i32 @__glagol_json_parse_f64_value_result(ptr, ptr)\n\n");
    out.push_str("declare ptr @__glagol_num_i32_to_string(i32)\n\n");
    out.push_str("declare ptr @__glagol_num_i64_to_string(i64)\n\n");
    out.push_str("declare ptr @__glagol_num_u32_to_string(i32)\n\n");
    out.push_str("declare ptr @__glagol_num_u64_to_string(i64)\n\n");
    out.push_str("declare ptr @__glagol_num_f64_to_string(double)\n\n");
    out.push_str("declare void @__glagol_io_eprint(ptr)\n\n");
    out.push_str("declare ptr @__glagol_io_read_stdin_result()\n\n");
    out.push_str("declare void @__glagol_process_init(i32, ptr)\n\n");
    out.push_str("declare i32 @__glagol_process_argc()\n\n");
    out.push_str("declare ptr @__glagol_process_arg(i32)\n\n");
    out.push_str("declare ptr @__glagol_process_arg_result(i32)\n\n");
    out.push_str("declare ptr @__glagol_env_get(ptr)\n\n");
    out.push_str("declare ptr @__glagol_env_get_result(ptr)\n\n");
    out.push_str("declare ptr @__glagol_fs_read_text(ptr)\n\n");
    out.push_str("declare ptr @__glagol_fs_read_text_result(ptr)\n\n");
    out.push_str("declare i32 @__glagol_fs_write_text(ptr, ptr)\n\n");
    out.push_str("declare i32 @__glagol_fs_write_text_result(ptr, ptr)\n\n");
    out.push_str("declare i1 @__glagol_fs_exists(ptr)\n\n");
    out.push_str("declare i1 @__glagol_fs_is_file(ptr)\n\n");
    out.push_str("declare i1 @__glagol_fs_is_dir(ptr)\n\n");
    out.push_str("declare i32 @__glagol_fs_remove_file_result(ptr)\n\n");
    out.push_str("declare i32 @__glagol_fs_create_dir_result(ptr)\n\n");
    out.push_str("declare i64 @__glagol_fs_open_text_read_result(ptr)\n\n");
    out.push_str("declare ptr @__glagol_fs_read_open_text_result(i32)\n\n");
    out.push_str("declare i32 @__glagol_fs_close_result(i32)\n\n");
    out.push_str("declare i64 @__glagol_net_tcp_connect_loopback_result(i32)\n\n");
    out.push_str("declare i64 @__glagol_net_tcp_listen_loopback_result(i32)\n\n");
    out.push_str("declare i64 @__glagol_net_tcp_bound_port_result(i32)\n\n");
    out.push_str("declare i64 @__glagol_net_tcp_accept_result(i32)\n\n");
    out.push_str("declare ptr @__glagol_net_tcp_read_all_result(i32)\n\n");
    out.push_str("declare i32 @__glagol_net_tcp_write_text_result(i32, ptr)\n\n");
    out.push_str("declare i32 @__glagol_net_tcp_close_result(i32)\n\n");
    out.push_str("declare i1 @__glagol_string_eq(ptr, ptr)\n\n");
    out.push_str("declare ptr @__glagol_vec_i32_empty()\n\n");
    out.push_str("declare ptr @__glagol_vec_i32_append(ptr, i32)\n\n");
    out.push_str("declare i32 @__glagol_vec_i32_len(ptr)\n\n");
    out.push_str("declare i32 @__glagol_vec_i32_index(ptr, i32)\n\n");
    out.push_str("declare i1 @__glagol_vec_i32_eq(ptr, ptr)\n\n");
    out.push_str("declare ptr @__glagol_vec_i64_empty()\n\n");
    out.push_str("declare ptr @__glagol_vec_i64_append(ptr, i64)\n\n");
    out.push_str("declare i32 @__glagol_vec_i64_len(ptr)\n\n");
    out.push_str("declare i64 @__glagol_vec_i64_index(ptr, i32)\n\n");
    out.push_str("declare i1 @__glagol_vec_i64_eq(ptr, ptr)\n\n");
    out.push_str("declare ptr @__glagol_vec_f64_empty()\n\n");
    out.push_str("declare ptr @__glagol_vec_f64_append(ptr, double)\n\n");
    out.push_str("declare i32 @__glagol_vec_f64_len(ptr)\n\n");
    out.push_str("declare double @__glagol_vec_f64_index(ptr, i32)\n\n");
    out.push_str("declare i1 @__glagol_vec_f64_eq(ptr, ptr)\n\n");
    out.push_str("declare ptr @__glagol_vec_bool_empty()\n\n");
    out.push_str("declare ptr @__glagol_vec_bool_append(ptr, i1)\n\n");
    out.push_str("declare i32 @__glagol_vec_bool_len(ptr)\n\n");
    out.push_str("declare i1 @__glagol_vec_bool_index(ptr, i32)\n\n");
    out.push_str("declare i1 @__glagol_vec_bool_eq(ptr, ptr)\n\n");
    out.push_str("declare ptr @__glagol_vec_string_empty()\n\n");
    out.push_str("declare ptr @__glagol_vec_string_append(ptr, ptr)\n\n");
    out.push_str("declare i32 @__glagol_vec_string_len(ptr)\n\n");
    out.push_str("declare ptr @__glagol_vec_string_index(ptr, i32)\n\n");
    out.push_str("declare i1 @__glagol_vec_string_eq(ptr, ptr)\n\n");
    out.push_str("declare i32 @__glagol_time_monotonic_ms()\n\n");
    out.push_str("declare void @__glagol_time_sleep_ms(i32)\n\n");
    out.push_str("declare i32 @__glagol_random_i32()\n\n");
    out.push_str("declare void @__glagol_array_bounds_trap()\n\n");
    out.push_str("declare void @__glagol_unwrap_some_trap()\n\n");
    out.push_str("declare void @__glagol_unwrap_ok_trap()\n\n");
    out.push_str("declare void @__glagol_unwrap_err_trap()\n\n");

    let mut declared_c_imports = HashSet::new();
    for import in &program.c_imports {
        if !declared_c_imports.insert(import.name.as_str()) {
            continue;
        }
        let params = import
            .params
            .iter()
            .map(|(_, ty)| ty.llvm())
            .collect::<Vec<_>>()
            .join(", ");
        out.push_str(&format!(
            "declare {} @{}({})\n\n",
            import.return_type.llvm(),
            import.name,
            params
        ));
    }

    if !string_globals.definitions.is_empty() {
        out.push_str(&string_globals.emit_definitions());
        out.push('\n');
    }

    for function in &program.functions {
        out.push_str(&emit_function(
            function.file.as_str(),
            function,
            &layouts,
            &enum_layouts,
            &string_globals,
            needs_process_runtime,
        )?);
        out.push('\n');
    }

    Ok(out)
}

#[derive(Clone)]
struct StructLayout {
    fields: Vec<(String, Type)>,
}

type StructLayouts = HashMap<String, StructLayout>;

#[derive(Clone)]
struct EnumLayout {
    payload_ty: Option<Type>,
}

type EnumLayouts = HashMap<String, EnumLayout>;

struct StringGlobals {
    names: HashMap<String, String>,
    definitions: Vec<StringGlobal>,
}

struct StringGlobal {
    name: String,
    value: String,
}

impl StringGlobals {
    fn collect(program: &CheckedProgram) -> Self {
        let mut globals = Self {
            names: HashMap::new(),
            definitions: Vec::new(),
        };

        for function in &program.functions {
            for expr in &function.body {
                globals.collect_expr(expr);
            }
        }

        globals
    }

    fn collect_expr(&mut self, expr: &TExpr) {
        match &expr.kind {
            TExprKind::String(value) => {
                self.intern(value);
            }
            TExprKind::EnumVariant { payload, .. } => {
                if let Some(payload) = payload {
                    self.collect_expr(payload);
                }
            }
            TExprKind::StructInit { fields, .. } => {
                for (_, value) in fields {
                    self.collect_expr(value);
                }
            }
            TExprKind::ArrayInit { elements } => {
                for element in elements {
                    self.collect_expr(element);
                }
            }
            TExprKind::OptionSome { value }
            | TExprKind::ResultOk { value }
            | TExprKind::ResultErr { value }
            | TExprKind::OptionIsSome { value }
            | TExprKind::OptionIsNone { value }
            | TExprKind::OptionUnwrapSome { value }
            | TExprKind::ResultIsOk { value, .. }
            | TExprKind::ResultIsErr { value, .. }
            | TExprKind::ResultUnwrapOk { value, .. }
            | TExprKind::ResultUnwrapErr { value, .. }
            | TExprKind::FieldAccess { value, .. } => self.collect_expr(value),
            TExprKind::Index { array, index } => {
                self.collect_expr(array);
                self.collect_expr(index);
            }
            TExprKind::Local { initializer, .. } => self.collect_expr(initializer),
            TExprKind::Set { expr, .. } => self.collect_expr(expr),
            TExprKind::Binary { left, right, .. } => {
                self.collect_expr(left);
                self.collect_expr(right);
            }
            TExprKind::If {
                condition,
                then_expr,
                else_expr,
            } => {
                self.collect_expr(condition);
                self.collect_expr(then_expr);
                self.collect_expr(else_expr);
            }
            TExprKind::Match { subject, arms } => {
                self.collect_expr(subject);
                for arm in arms {
                    for expr in &arm.body {
                        self.collect_expr(expr);
                    }
                }
            }
            TExprKind::While { condition, body } => {
                self.collect_expr(condition);
                for expr in body {
                    self.collect_expr(expr);
                }
            }
            TExprKind::Unsafe { body } => {
                for expr in body {
                    self.collect_expr(expr);
                }
            }
            TExprKind::Call { args, .. } => {
                for arg in args {
                    self.collect_expr(arg);
                }
            }
            TExprKind::Int(_)
            | TExprKind::Int64(_)
            | TExprKind::UInt32(_)
            | TExprKind::UInt64(_)
            | TExprKind::Float(_)
            | TExprKind::Bool(_)
            | TExprKind::Var(_)
            | TExprKind::OptionNone => {}
        }
    }

    fn intern(&mut self, value: &str) {
        if self.names.contains_key(value) {
            return;
        }

        let name = format!(".str.{}", self.definitions.len());
        self.names.insert(value.to_string(), name.clone());
        self.definitions.push(StringGlobal {
            name,
            value: value.to_string(),
        });
    }

    fn global_name(&self, value: &str) -> Option<&str> {
        self.names.get(value).map(String::as_str)
    }

    fn emit_definitions(&self) -> String {
        let mut out = String::new();

        for global in &self.definitions {
            out.push_str(&format!(
                "@{} = private unnamed_addr constant [{} x i8] c\"{}\", align 1\n",
                global.name,
                global.value.len() + 1,
                llvm_c_string(&global.value)
            ));
        }

        out
    }
}

fn llvm_c_string(value: &str) -> String {
    let mut out = String::new();

    for byte in value.bytes().chain(std::iter::once(0)) {
        match byte {
            b'"' => out.push_str("\\22"),
            b'\\' => out.push_str("\\5C"),
            b'\n' => out.push_str("\\0A"),
            b'\r' => out.push_str("\\0D"),
            b'\t' => out.push_str("\\09"),
            0x20..=0x7e => out.push(byte as char),
            _ => out.push_str(&format!("\\{:02X}", byte)),
        }
    }

    out
}

fn struct_layouts(program: &CheckedProgram) -> StructLayouts {
    program
        .structs
        .iter()
        .map(|decl| {
            (
                decl.name.clone(),
                StructLayout {
                    fields: decl.fields.clone(),
                },
            )
        })
        .collect()
}

fn enum_layouts(program: &CheckedProgram) -> EnumLayouts {
    program
        .enums
        .iter()
        .map(|decl| {
            (
                decl.name.clone(),
                EnumLayout {
                    payload_ty: decl
                        .variants
                        .iter()
                        .find_map(|variant| variant.payload_ty.clone()),
                },
            )
        })
        .collect()
}

fn needs_process_runtime(program: &CheckedProgram) -> bool {
    program
        .functions
        .iter()
        .flat_map(|function| function.body.iter())
        .any(expr_needs_process_runtime)
}

fn expr_needs_process_runtime(expr: &TExpr) -> bool {
    match &expr.kind {
        TExprKind::StructInit { fields, .. } => fields
            .iter()
            .any(|(_, value)| expr_needs_process_runtime(value)),
        TExprKind::ArrayInit { elements } => elements.iter().any(expr_needs_process_runtime),
        TExprKind::OptionSome { value }
        | TExprKind::ResultOk { value }
        | TExprKind::ResultErr { value }
        | TExprKind::OptionIsSome { value }
        | TExprKind::OptionIsNone { value }
        | TExprKind::OptionUnwrapSome { value }
        | TExprKind::ResultIsOk { value, .. }
        | TExprKind::ResultIsErr { value, .. }
        | TExprKind::ResultUnwrapOk { value, .. }
        | TExprKind::ResultUnwrapErr { value, .. }
        | TExprKind::FieldAccess { value, .. } => expr_needs_process_runtime(value),
        TExprKind::Index { array, index } => {
            expr_needs_process_runtime(array) || expr_needs_process_runtime(index)
        }
        TExprKind::Local { initializer, .. } => expr_needs_process_runtime(initializer),
        TExprKind::Set { expr, .. } => expr_needs_process_runtime(expr),
        TExprKind::Binary { left, right, .. } => {
            expr_needs_process_runtime(left) || expr_needs_process_runtime(right)
        }
        TExprKind::If {
            condition,
            then_expr,
            else_expr,
        } => {
            expr_needs_process_runtime(condition)
                || expr_needs_process_runtime(then_expr)
                || expr_needs_process_runtime(else_expr)
        }
        TExprKind::Match { subject, arms } => {
            expr_needs_process_runtime(subject)
                || arms
                    .iter()
                    .flat_map(|arm| arm.body.iter())
                    .any(expr_needs_process_runtime)
        }
        TExprKind::While { condition, body } => {
            expr_needs_process_runtime(condition) || body.iter().any(expr_needs_process_runtime)
        }
        TExprKind::Unsafe { body } => body.iter().any(expr_needs_process_runtime),
        TExprKind::Call { name, args } => {
            matches!(
                std_runtime::runtime_symbol(name),
                Some(
                    "__glagol_process_argc"
                        | "__glagol_process_arg"
                        | "__glagol_process_arg_result"
                )
            ) || args.iter().any(expr_needs_process_runtime)
        }
        TExprKind::Int(_)
        | TExprKind::Int64(_)
        | TExprKind::UInt32(_)
        | TExprKind::UInt64(_)
        | TExprKind::Float(_)
        | TExprKind::Bool(_)
        | TExprKind::String(_)
        | TExprKind::Var(_)
        | TExprKind::OptionNone => false,
        TExprKind::EnumVariant { payload, .. } => payload
            .as_deref()
            .map(expr_needs_process_runtime)
            .unwrap_or(false),
    }
}

fn emit_function(
    file: &str,
    function: &CheckedFunction,
    layouts: &StructLayouts,
    enum_layouts: &EnumLayouts,
    string_globals: &StringGlobals,
    needs_process_runtime: bool,
) -> Result<String, Vec<Diagnostic>> {
    validate_function_signature(file, function, layouts, enum_layouts)?;

    let mut gen = FunctionGen {
        next_reg: 0,
        next_block: 0,
        local_alloc_counts: HashMap::new(),
        current_block: "entry".to_string(),
        locals: HashMap::new(),
        lines: Vec::new(),
        file: file.to_string(),
        layouts,
        enum_layouts,
        string_globals,
    };

    for (name, ty) in &function.params {
        match ty {
            Type::Array(inner, len)
                if llvm_fixed_array_type(layouts, enum_layouts, inner, *len).is_some() =>
            {
                let llvm_ty = llvm_type(layouts, enum_layouts, ty).expect("validated array type");
                let ptr = gen.local_addr(name);
                gen.lines.push(format!("{} = alloca {}", ptr, llvm_ty));
                gen.lines
                    .push(format!("store {} %{}, ptr {}", llvm_ty, name, ptr));
                gen.locals.insert(
                    name.clone(),
                    LocalValue::Array {
                        ptr,
                        ty: ty.clone(),
                    },
                );
            }
            _ => {
                gen.locals
                    .insert(name.clone(), LocalValue::Value(format!("%{}", name)));
            }
        }
    }

    let is_entry_main =
        needs_process_runtime && function.name == "main" && function.params.is_empty();
    let params = if is_entry_main {
        "i32 %__glagol_argc, ptr %__glagol_argv".to_string()
    } else {
        function
            .params
            .iter()
            .map(|(name, ty)| {
                format!(
                    "{} %{}",
                    llvm_type(layouts, enum_layouts, ty).expect("validated type"),
                    name
                )
            })
            .collect::<Vec<_>>()
            .join(", ")
    };
    let return_ty =
        llvm_type(layouts, enum_layouts, &function.return_type).expect("validated return type");

    let mut out = String::new();
    out.push_str(&format!(
        "define {} @{}({}) {{\n",
        return_ty, function.name, params
    ));
    out.push_str("entry:\n");
    if is_entry_main {
        gen.lines.push(
            "call void @__glagol_process_init(i32 %__glagol_argc, ptr %__glagol_argv)".to_string(),
        );
    }

    let mut last = None;
    for expr in &function.body {
        match gen.emit_expr(expr) {
            Ok(value) => last = Some(value),
            Err(err) => return Err(vec![err]),
        }
    }

    for line in gen.lines {
        if !line.ends_with(':') {
            out.push_str("  ");
        }
        out.push_str(&line);
        out.push('\n');
    }

    match function.return_type {
        Type::Unit => out.push_str("  ret void\n"),
        _ => {
            let value = last.unwrap_or_else(|| "0".to_string());
            let return_ty = llvm_type(layouts, enum_layouts, &function.return_type)
                .expect("validated return type");
            out.push_str(&format!("  ret {} {}\n", return_ty, value));
        }
    }

    out.push_str("}\n");
    Ok(out)
}

fn validate_function_signature(
    file: &str,
    function: &CheckedFunction,
    layouts: &StructLayouts,
    enum_layouts: &EnumLayouts,
) -> Result<(), Vec<Diagnostic>> {
    let mut errors = Vec::new();
    let mut reported_types = HashSet::new();

    for (_, ty) in &function.params {
        if !is_backend_param_type_supported(ty, layouts, enum_layouts)
            && reported_types.insert(ty.to_string())
        {
            errors.push(unsupported_type(file, function, ty));
        }
    }

    if !is_backend_return_type_supported(&function.return_type, layouts, enum_layouts)
        && reported_types.insert(function.return_type.to_string())
    {
        errors.push(unsupported_type(file, function, &function.return_type));
    }

    if errors.is_empty() {
        Ok(())
    } else {
        Err(errors)
    }
}

fn is_backend_param_type_supported(
    ty: &Type,
    layouts: &StructLayouts,
    enum_layouts: &EnumLayouts,
) -> bool {
    match ty {
        Type::I32
        | Type::I64
        | Type::U32
        | Type::U64
        | Type::F64
        | Type::Bool
        | Type::Unit
        | Type::String => true,
        Type::Named(_) => true,
        Type::Array(inner, len) => {
            llvm_fixed_array_type(layouts, enum_layouts, inner, *len).is_some()
        }
        Type::Vec(inner) => {
            **inner == Type::I32
                || **inner == Type::I64
                || **inner == Type::F64
                || **inner == Type::Bool
                || **inner == Type::String
        }
        Type::Option(inner) => {
            **inner == Type::I32
                || **inner == Type::I64
                || **inner == Type::U32
                || **inner == Type::U64
                || **inner == Type::F64
                || **inner == Type::Bool
                || **inner == Type::String
        }
        Type::Result(ok, err) => result_type_supported(ok, err),
        _ => false,
    }
}

fn is_backend_return_type_supported(
    ty: &Type,
    layouts: &StructLayouts,
    enum_layouts: &EnumLayouts,
) -> bool {
    match ty {
        Type::I32
        | Type::I64
        | Type::U32
        | Type::U64
        | Type::F64
        | Type::Bool
        | Type::Unit
        | Type::String => true,
        Type::Named(_) => true,
        Type::Array(inner, len) => {
            llvm_fixed_array_type(layouts, enum_layouts, inner, *len).is_some()
        }
        Type::Vec(inner) => {
            **inner == Type::I32
                || **inner == Type::I64
                || **inner == Type::F64
                || **inner == Type::Bool
                || **inner == Type::String
        }
        Type::Option(inner) => {
            **inner == Type::I32
                || **inner == Type::I64
                || **inner == Type::U32
                || **inner == Type::U64
                || **inner == Type::F64
                || **inner == Type::Bool
                || **inner == Type::String
        }
        Type::Result(ok, err) => result_type_supported(ok, err),
        _ => false,
    }
}

fn llvm_type(layouts: &StructLayouts, enum_layouts: &EnumLayouts, ty: &Type) -> Option<String> {
    match ty {
        Type::Array(inner, len) => llvm_fixed_array_type(layouts, enum_layouts, inner, *len),
        Type::Named(name) => {
            if let Some(layout) = enum_layouts.get(name) {
                return Some(if let Some(payload_ty) = &layout.payload_ty {
                    format!(
                        "{{ i32, {} }}",
                        llvm_type(layouts, enum_layouts, payload_ty)?
                    )
                } else {
                    "i32".to_string()
                });
            }
            let Some(layout) = layouts.get(name) else {
                return Some("i32".to_string());
            };
            let fields = layout
                .fields
                .iter()
                .map(|(_, ty)| llvm_type(layouts, enum_layouts, ty))
                .collect::<Option<Vec<_>>>()?
                .join(", ");
            Some(format!("{{ {} }}", fields))
        }
        _ => Some(ty.llvm().to_string()),
    }
}

fn llvm_fixed_array_type(
    layouts: &StructLayouts,
    enum_layouts: &EnumLayouts,
    inner: &Type,
    len: usize,
) -> Option<String> {
    if len == 0 {
        return None;
    }
    let inner_ty = match inner {
        Type::I32 | Type::I64 | Type::U32 | Type::U64 | Type::F64 | Type::Bool | Type::String => {
            inner.llvm().to_string()
        }
        Type::Named(_) => llvm_type(layouts, enum_layouts, inner)?,
        _ => return None,
    };
    Some(format!("[{} x {}]", len, inner_ty))
}

fn format_f64_literal(value: f64) -> String {
    format!("{:.17}", value)
}

fn is_vec_i32_type(ty: &Type) -> bool {
    matches!(ty, Type::Vec(inner) if **inner == Type::I32)
}

fn is_vec_i64_type(ty: &Type) -> bool {
    matches!(ty, Type::Vec(inner) if **inner == Type::I64)
}

fn is_vec_f64_type(ty: &Type) -> bool {
    matches!(ty, Type::Vec(inner) if **inner == Type::F64)
}

fn is_vec_bool_type(ty: &Type) -> bool {
    matches!(ty, Type::Vec(inner) if **inner == Type::Bool)
}

fn is_vec_string_type(ty: &Type) -> bool {
    matches!(ty, Type::Vec(inner) if **inner == Type::String)
}

fn vec_eq_runtime_symbol(ty: &Type) -> Option<&'static str> {
    if is_vec_i32_type(ty) {
        Some("__glagol_vec_i32_eq")
    } else if is_vec_i64_type(ty) {
        Some("__glagol_vec_i64_eq")
    } else if is_vec_f64_type(ty) {
        Some("__glagol_vec_f64_eq")
    } else if is_vec_bool_type(ty) {
        Some("__glagol_vec_bool_eq")
    } else if is_vec_string_type(ty) {
        Some("__glagol_vec_string_eq")
    } else {
        None
    }
}

fn result_type_supported(ok: &Type, err: &Type) -> bool {
    matches!(
        (ok, err),
        (Type::I32, Type::I32)
            | (Type::I64, Type::I32)
            | (Type::U32, Type::I32)
            | (Type::U64, Type::I32)
            | (Type::F64, Type::I32)
            | (Type::Bool, Type::I32)
            | (Type::String, Type::I32)
    )
}

fn zero_value_for_type(ty: &Type) -> Option<String> {
    match ty {
        Type::I32 => Some("0".to_string()),
        Type::I64 => Some("0".to_string()),
        Type::U32 => Some("0".to_string()),
        Type::U64 => Some("0".to_string()),
        Type::F64 => Some("0.0".to_string()),
        Type::Bool => Some("0".to_string()),
        Type::String => Some("null".to_string()),
        Type::Array(_, _) | Type::Named(_) => Some("zeroinitializer".to_string()),
        _ => None,
    }
}

fn result_payload_index(ty: &Type, ok_payload: bool) -> Option<usize> {
    match ty {
        Type::Option(inner)
            if (**inner == Type::I32
                || **inner == Type::I64
                || **inner == Type::U32
                || **inner == Type::U64
                || **inner == Type::F64
                || **inner == Type::Bool
                || **inner == Type::String)
                && ok_payload =>
        {
            Some(1)
        }
        Type::Result(ok, err) if **ok == Type::I32 && **err == Type::I32 => Some(1),
        Type::Result(ok, err) if **ok == Type::I64 && **err == Type::I32 => {
            Some(if ok_payload { 1 } else { 2 })
        }
        Type::Result(ok, err) if **ok == Type::U32 && **err == Type::I32 => Some(1),
        Type::Result(ok, err) if **ok == Type::U64 && **err == Type::I32 => {
            Some(if ok_payload { 1 } else { 2 })
        }
        Type::Result(ok, err) if **ok == Type::F64 && **err == Type::I32 => {
            Some(if ok_payload { 1 } else { 2 })
        }
        Type::Result(ok, err) if **ok == Type::Bool && **err == Type::I32 => {
            Some(if ok_payload { 1 } else { 2 })
        }
        Type::Result(ok, err) if **ok == Type::String && **err == Type::I32 => {
            Some(if ok_payload { 1 } else { 2 })
        }
        _ => None,
    }
}

fn unsupported_type(file: &str, function: &CheckedFunction, ty: &Type) -> Diagnostic {
    Diagnostic::new(
        file,
        "UnsupportedBackendFeature",
        format!(
            "backend does not support `{}` in function `{}` signature",
            ty, function.name
        ),
    )
    .with_span(function.span)
    .hint(
        "keep this type in speculative examples until layout and LLVM ABI behavior are implemented",
    )
}

struct FunctionGen<'a> {
    next_reg: usize,
    next_block: usize,
    local_alloc_counts: HashMap<String, usize>,
    current_block: String,
    locals: HashMap<String, LocalValue>,
    lines: Vec<String>,
    file: String,
    layouts: &'a StructLayouts,
    enum_layouts: &'a EnumLayouts,
    string_globals: &'a StringGlobals,
}

#[derive(Clone)]
enum LocalValue {
    Value(String),
    Ptr { ptr: String, ty: Type },
    Array { ptr: String, ty: Type },
}

impl FunctionGen<'_> {
    fn reg(&mut self) -> String {
        let reg = format!("%{}", self.next_reg);
        self.next_reg += 1;
        reg
    }

    fn block(&mut self, prefix: &str) -> String {
        let block = format!("{}.{}", prefix, self.next_block);
        self.next_block += 1;
        block
    }

    fn local_addr(&mut self, name: &str) -> String {
        let count = self.local_alloc_counts.entry(name.to_string()).or_insert(0);
        let ptr = if *count == 0 {
            format!("%{}.addr", name)
        } else {
            format!("%{}.addr.{}", name, count)
        };
        *count += 1;
        ptr
    }

    fn label(&mut self, block: &str) {
        self.lines.push(format!("{}:", block));
        self.current_block = block.to_string();
    }

    fn unsupported(&self, expr: &TExpr, feature: &str) -> Diagnostic {
        Diagnostic::new(
            &self.file,
            "UnsupportedBackendFeature",
            format!("backend does not support {}", feature),
        )
        .with_span(expr.span)
        .hint("keep this form in speculative examples until LLVM lowering is implemented")
    }

    fn llvm_type(&self, expr: &TExpr, ty: &Type) -> Result<String, Diagnostic> {
        llvm_type(self.layouts, self.enum_layouts, ty).ok_or_else(|| {
            self.unsupported(expr, &format!("LLVM representation for type `{}`", ty))
        })
    }

    fn emit_numeric_widening_conversion(
        &mut self,
        expr: &TExpr,
        args: &[TExpr],
        instruction: &str,
        from_ty: &str,
        to_ty: &str,
    ) -> Result<String, Diagnostic> {
        let [arg] = args else {
            return Err(self.unsupported(expr, "malformed numeric widening conversion"));
        };
        let value = self.emit_expr(arg)?;
        let reg = self.reg();
        self.lines.push(format!(
            "{} = {} {} {} to {}",
            reg, instruction, from_ty, value, to_ty
        ));
        Ok(reg)
    }

    fn emit_i64_to_i32_result_conversion(
        &mut self,
        expr: &TExpr,
        args: &[TExpr],
    ) -> Result<String, Diagnostic> {
        let [arg] = args else {
            return Err(self.unsupported(expr, "malformed checked i64-to-i32 conversion"));
        };
        let value = self.emit_expr(arg)?;
        let above_min = self.reg();
        self.lines.push(format!(
            "{} = icmp sge i64 {}, -2147483648",
            above_min, value
        ));
        let below_max = self.reg();
        self.lines.push(format!(
            "{} = icmp sle i64 {}, 2147483647",
            below_max, value
        ));
        let in_range = self.reg();
        self.lines.push(format!(
            "{} = and i1 {}, {}",
            in_range, above_min, below_max
        ));
        let narrowed = self.reg();
        self.lines
            .push(format!("{} = trunc i64 {} to i32", narrowed, value));
        let payload = self.reg();
        self.lines.push(format!(
            "{} = select i1 {}, i32 {}, i32 1",
            payload, in_range, narrowed
        ));
        self.emit_tagged_i32_aggregate_from_values(expr, &in_range, &payload)
    }

    fn emit_f64_to_i32_result_conversion(
        &mut self,
        expr: &TExpr,
        args: &[TExpr],
    ) -> Result<String, Diagnostic> {
        let [arg] = args else {
            return Err(self.unsupported(expr, "malformed checked f64-to-i32 conversion"));
        };
        let value = self.emit_expr(arg)?;
        let above_min = self.reg();
        self.lines.push(format!(
            "{} = fcmp oge double {}, -2147483648.0",
            above_min, value
        ));
        let below_max = self.reg();
        self.lines.push(format!(
            "{} = fcmp ole double {}, 2147483647.0",
            below_max, value
        ));
        let in_range = self.reg();
        self.lines.push(format!(
            "{} = and i1 {}, {}",
            in_range, above_min, below_max
        ));

        let integral_block = self.block("f64.to_i32.integral");
        let exponent_block = self.block("f64.to_i32.exponent");
        let fraction_block = self.block("f64.to_i32.fraction");
        let convert_block = self.block("f64.to_i32.convert");
        let err_block = self.block("f64.to_i32.err");
        let merge_block = self.block("f64.to_i32.end");
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            in_range, integral_block, err_block
        ));

        self.label(&integral_block);
        let bits = self.reg();
        self.lines
            .push(format!("{} = bitcast double {} to i64", bits, value));
        let abs_bits = self.reg();
        self.lines.push(format!(
            "{} = and i64 {}, 9223372036854775807",
            abs_bits, bits
        ));
        let is_zero = self.reg();
        self.lines
            .push(format!("{} = icmp eq i64 {}, 0", is_zero, abs_bits));
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            is_zero, convert_block, exponent_block
        ));

        self.label(&exponent_block);
        let exponent = self.reg();
        self.lines
            .push(format!("{} = lshr i64 {}, 52", exponent, abs_bits));
        let exponent_ge_bias = self.reg();
        self.lines.push(format!(
            "{} = icmp uge i64 {}, 1023",
            exponent_ge_bias, exponent
        ));
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            exponent_ge_bias, fraction_block, err_block
        ));

        self.label(&fraction_block);
        let unbiased_exponent = self.reg();
        self.lines.push(format!(
            "{} = sub i64 {}, 1023",
            unbiased_exponent, exponent
        ));
        let fractional_width = self.reg();
        self.lines.push(format!(
            "{} = sub i64 52, {}",
            fractional_width, unbiased_exponent
        ));
        let fractional_one = self.reg();
        self.lines.push(format!(
            "{} = shl i64 1, {}",
            fractional_one, fractional_width
        ));
        let fractional_mask = self.reg();
        self.lines.push(format!(
            "{} = sub i64 {}, 1",
            fractional_mask, fractional_one
        ));
        let fractional_bits = self.reg();
        self.lines.push(format!(
            "{} = and i64 {}, {}",
            fractional_bits, abs_bits, fractional_mask
        ));
        let integral = self.reg();
        self.lines
            .push(format!("{} = icmp eq i64 {}, 0", integral, fractional_bits));
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            integral, convert_block, err_block
        ));

        self.label(&convert_block);
        let narrowed = self.reg();
        self.lines
            .push(format!("{} = fptosi double {} to i32", narrowed, value));
        let ok_result = self.emit_tagged_i32_aggregate_from_values(expr, "true", &narrowed)?;
        let ok_pred = self.current_block.clone();
        self.lines.push(format!("br label %{}", merge_block));

        self.label(&err_block);
        let err_result = self.emit_tagged_i32_aggregate_from_values(expr, "false", "1")?;
        let err_pred = self.current_block.clone();
        self.lines.push(format!("br label %{}", merge_block));

        self.label(&merge_block);
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let result = self.reg();
        self.lines.push(format!(
            "{} = phi {} [ {}, %{} ], [ {}, %{} ]",
            result, aggregate_ty, ok_result, ok_pred, err_result, err_pred
        ));
        Ok(result)
    }

    fn emit_f64_to_i64_result_conversion(
        &mut self,
        expr: &TExpr,
        args: &[TExpr],
    ) -> Result<String, Diagnostic> {
        let [arg] = args else {
            return Err(self.unsupported(expr, "malformed checked f64-to-i64 conversion"));
        };
        let value = self.emit_expr(arg)?;
        let above_min = self.reg();
        self.lines.push(format!(
            "{} = fcmp oge double {}, -9223372036854775808.0",
            above_min, value
        ));
        let below_max = self.reg();
        self.lines.push(format!(
            "{} = fcmp olt double {}, 9223372036854775808.0",
            below_max, value
        ));
        let in_range = self.reg();
        self.lines.push(format!(
            "{} = and i1 {}, {}",
            in_range, above_min, below_max
        ));

        let integral_block = self.block("f64.to_i64.integral");
        let exponent_block = self.block("f64.to_i64.exponent");
        let mantissa_block = self.block("f64.to_i64.mantissa");
        let fraction_block = self.block("f64.to_i64.fraction");
        let convert_block = self.block("f64.to_i64.convert");
        let err_block = self.block("f64.to_i64.err");
        let merge_block = self.block("f64.to_i64.end");
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            in_range, integral_block, err_block
        ));

        self.label(&integral_block);
        let bits = self.reg();
        self.lines
            .push(format!("{} = bitcast double {} to i64", bits, value));
        let abs_bits = self.reg();
        self.lines.push(format!(
            "{} = and i64 {}, 9223372036854775807",
            abs_bits, bits
        ));
        let is_zero = self.reg();
        self.lines
            .push(format!("{} = icmp eq i64 {}, 0", is_zero, abs_bits));
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            is_zero, convert_block, exponent_block
        ));

        self.label(&exponent_block);
        let exponent = self.reg();
        self.lines
            .push(format!("{} = lshr i64 {}, 52", exponent, abs_bits));
        let exponent_ge_bias = self.reg();
        self.lines.push(format!(
            "{} = icmp uge i64 {}, 1023",
            exponent_ge_bias, exponent
        ));
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            exponent_ge_bias, mantissa_block, err_block
        ));

        self.label(&mantissa_block);
        let unbiased_exponent = self.reg();
        self.lines.push(format!(
            "{} = sub i64 {}, 1023",
            unbiased_exponent, exponent
        ));
        let no_fractional_bits = self.reg();
        self.lines.push(format!(
            "{} = icmp uge i64 {}, 52",
            no_fractional_bits, unbiased_exponent
        ));
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            no_fractional_bits, convert_block, fraction_block
        ));

        self.label(&fraction_block);
        let fractional_width = self.reg();
        self.lines.push(format!(
            "{} = sub i64 52, {}",
            fractional_width, unbiased_exponent
        ));
        let fractional_one = self.reg();
        self.lines.push(format!(
            "{} = shl i64 1, {}",
            fractional_one, fractional_width
        ));
        let fractional_mask = self.reg();
        self.lines.push(format!(
            "{} = sub i64 {}, 1",
            fractional_mask, fractional_one
        ));
        let fractional_bits = self.reg();
        self.lines.push(format!(
            "{} = and i64 {}, {}",
            fractional_bits, abs_bits, fractional_mask
        ));
        let integral = self.reg();
        self.lines
            .push(format!("{} = icmp eq i64 {}, 0", integral, fractional_bits));
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            integral, convert_block, err_block
        ));

        self.label(&convert_block);
        let narrowed = self.reg();
        self.lines
            .push(format!("{} = fptosi double {} to i64", narrowed, value));
        let ok_result = self.emit_i64_result_aggregate(expr, "true", &narrowed, "0")?;
        let ok_pred = self.current_block.clone();
        self.lines.push(format!("br label %{}", merge_block));

        self.label(&err_block);
        let err_result = self.emit_i64_result_aggregate(expr, "false", "0", "1")?;
        let err_pred = self.current_block.clone();
        self.lines.push(format!("br label %{}", merge_block));

        self.label(&merge_block);
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let result = self.reg();
        self.lines.push(format!(
            "{} = phi {} [ {}, %{} ], [ {}, %{} ]",
            result, aggregate_ty, ok_result, ok_pred, err_result, err_pred
        ));
        Ok(result)
    }

    fn is_payload_enum_type(&self, ty: &Type) -> bool {
        matches!(
            ty,
            Type::Named(name)
                if self
                    .enum_layouts
                    .get(name)
                    .and_then(|layout| layout.payload_ty.as_ref())
                    .is_some()
        )
    }

    fn enum_payload_type<'b>(&'b self, ty: &Type) -> Option<&'b Type> {
        match ty {
            Type::Named(name) => self
                .enum_layouts
                .get(name)
                .and_then(|layout| layout.payload_ty.as_ref()),
            _ => None,
        }
    }

    fn emit_equality_for_type(
        &mut self,
        ty: &Type,
        left: &str,
        right: &str,
    ) -> Result<String, Diagnostic> {
        let reg = self.reg();
        match ty {
            Type::String => self.lines.push(format!(
                "{} = call i1 @__glagol_string_eq(ptr {}, ptr {})",
                reg, left, right
            )),
            Type::F64 => self
                .lines
                .push(format!("{} = fcmp oeq double {}, {}", reg, left, right)),
            Type::Bool => self
                .lines
                .push(format!("{} = icmp eq i1 {}, {}", reg, left, right)),
            Type::U64 => self
                .lines
                .push(format!("{} = icmp eq i64 {}, {}", reg, left, right)),
            Type::I64 => self
                .lines
                .push(format!("{} = icmp eq i64 {}, {}", reg, left, right)),
            Type::U32 => self
                .lines
                .push(format!("{} = icmp eq i32 {}, {}", reg, left, right)),
            Type::I32 => self
                .lines
                .push(format!("{} = icmp eq i32 {}, {}", reg, left, right)),
            _ => {
                return Err(Diagnostic::new(
                    &self.file,
                    "UnsupportedBackendFeature",
                    format!(
                        "backend does not support equality for enum payload type `{}`",
                        ty
                    ),
                ))
            }
        }
        Ok(reg)
    }

    fn enum_payload_zero_value(&self, ty: &Type) -> Result<String, Diagnostic> {
        zero_value_for_type(ty).ok_or_else(|| {
            Diagnostic::new(
                &self.file,
                "UnsupportedBackendFeature",
                format!("backend does not support enum payload type `{}`", ty),
            )
        })
    }

    fn enum_payload_llvm_type(&self, ty: &Type) -> Result<String, Diagnostic> {
        llvm_type(self.layouts, self.enum_layouts, ty).ok_or_else(|| {
            Diagnostic::new(
                &self.file,
                "UnsupportedBackendFeature",
                format!("backend does not support enum payload type `{}`", ty),
            )
        })
    }

    fn emit_payload_enum_equality(
        &mut self,
        reg: &str,
        ty: &Type,
        left_aggregate_ty: &str,
        left_value: &str,
        right_value: &str,
    ) -> Result<(), Diagnostic> {
        let left_tag = self.reg();
        self.lines.push(format!(
            "{} = extractvalue {} {}, 0",
            left_tag, left_aggregate_ty, left_value
        ));
        let right_tag = self.reg();
        self.lines.push(format!(
            "{} = extractvalue {} {}, 0",
            right_tag, left_aggregate_ty, right_value
        ));
        let tags_equal = self.reg();
        self.lines.push(format!(
            "{} = icmp eq i32 {}, {}",
            tags_equal, left_tag, right_tag
        ));
        let left_payload = self.reg();
        self.lines.push(format!(
            "{} = extractvalue {} {}, 1",
            left_payload, left_aggregate_ty, left_value
        ));
        let right_payload = self.reg();
        self.lines.push(format!(
            "{} = extractvalue {} {}, 1",
            right_payload, left_aggregate_ty, right_value
        ));
        let payloads_equal = self.emit_equality_for_type(ty, &left_payload, &right_payload)?;
        self.lines.push(format!(
            "{} = and i1 {}, {}",
            reg, tags_equal, payloads_equal
        ));
        Ok(())
    }

    fn emit_expr(&mut self, expr: &TExpr) -> Result<String, Diagnostic> {
        match &expr.kind {
            TExprKind::Int(value) => Ok(value.to_string()),
            TExprKind::Int64(value) => Ok(value.to_string()),
            TExprKind::UInt32(value) => Ok(value.to_string()),
            TExprKind::UInt64(value) => Ok(value.to_string()),
            TExprKind::Float(value) => Ok(format_f64_literal(*value)),
            TExprKind::Bool(value) => Ok(if *value {
                "1".to_string()
            } else {
                "0".to_string()
            }),
            TExprKind::String(value) => self
                .string_globals
                .global_name(value)
                .map(|name| format!("@{}", name))
                .ok_or_else(|| self.unsupported(expr, "string literal storage")),
            TExprKind::EnumVariant {
                enum_name,
                discriminant,
                payload,
                ..
            } => self.emit_enum_variant(expr, enum_name, *discriminant, payload.as_deref()),
            TExprKind::StructInit { name, fields } => self.emit_struct_init(expr, name, fields),
            TExprKind::ArrayInit { elements } => self.emit_array_init(expr, elements),
            TExprKind::OptionSome { value } => self.emit_option_aggregate(expr, true, Some(value)),
            TExprKind::OptionNone => self.emit_option_aggregate(expr, false, None),
            TExprKind::ResultOk { value } => self.emit_result_aggregate(expr, true, value),
            TExprKind::ResultErr { value } => self.emit_result_aggregate(expr, false, value),
            TExprKind::OptionIsSome { value } => self.emit_tag_observer(expr, value, false),
            TExprKind::OptionIsNone { value } => self.emit_tag_observer(expr, value, true),
            TExprKind::OptionUnwrapSome { value } => {
                self.emit_tag_checked_payload_access(expr, value, true, "__glagol_unwrap_some_trap")
            }
            TExprKind::ResultIsOk { value, .. } => self.emit_tag_observer(expr, value, false),
            TExprKind::ResultIsErr { value, .. } => self.emit_tag_observer(expr, value, true),
            TExprKind::ResultUnwrapOk { value, .. } => {
                self.emit_tag_checked_payload_access(expr, value, true, "__glagol_unwrap_ok_trap")
            }
            TExprKind::ResultUnwrapErr { value, .. } => {
                self.emit_tag_checked_payload_access(expr, value, false, "__glagol_unwrap_err_trap")
            }
            TExprKind::FieldAccess { value, field } => self.emit_field_access(expr, value, field),
            TExprKind::Index { array, index } => self.emit_index(expr, array, index),
            TExprKind::Var(name) => match self.locals.get(name).cloned() {
                Some(LocalValue::Value(value)) => Ok(value),
                Some(LocalValue::Ptr { ptr, ty }) => {
                    let reg = self.reg();
                    let llvm_ty = self.llvm_type(expr, &ty)?;
                    self.lines
                        .push(format!("{} = load {}, ptr {}", reg, llvm_ty, ptr));
                    Ok(reg)
                }
                Some(LocalValue::Array { ptr, ty }) => {
                    let reg = self.reg();
                    let llvm_ty = self.llvm_type(expr, &ty)?;
                    self.lines
                        .push(format!("{} = load {}, ptr {}", reg, llvm_ty, ptr));
                    Ok(reg)
                }
                None => Ok(format!("%{}", name)),
            },
            TExprKind::Local {
                mutable,
                name,
                initializer,
            } => match &initializer.ty {
                Type::I32 | Type::Bool | Type::I64 | Type::U32 | Type::U64 | Type::F64
                    if !*mutable =>
                {
                    self.emit_ssa_local(name, initializer)
                }
                Type::I32 | Type::Bool | Type::I64 | Type::U32 | Type::U64 | Type::F64 => {
                    self.emit_value_local(name, initializer)
                }
                Type::String => self.emit_value_local(name, initializer),
                Type::Vec(inner)
                    if (**inner == Type::I32
                        || **inner == Type::I64
                        || **inner == Type::F64
                        || **inner == Type::Bool
                        || **inner == Type::String) =>
                {
                    self.emit_value_local(name, initializer)
                }
                Type::Option(inner)
                    if (**inner == Type::I32
                        || **inner == Type::I64
                        || **inner == Type::U32
                        || **inner == Type::U64
                        || **inner == Type::F64
                        || **inner == Type::Bool
                        || **inner == Type::String) =>
                {
                    self.emit_value_local(name, initializer)
                }
                Type::Result(ok, err) if result_type_supported(ok, err) => {
                    self.emit_value_local(name, initializer)
                }
                Type::Named(struct_name) if self.layouts.contains_key(struct_name) => {
                    self.emit_value_local(name, initializer)
                }
                Type::Named(_) => self.emit_value_local(name, initializer),
                Type::Array(_, _) if !*mutable => self.emit_array_local(expr, name, initializer),
                _ => Err(self.unsupported(expr, "unsupported local variables")),
            },
            TExprKind::Set { name, expr: value } => {
                let rhs = self.emit_expr(value)?;
                let Some(LocalValue::Ptr { ptr, ty }) = self.locals.get(name).cloned() else {
                    return Err(self.unsupported(expr, "assignment to non-local values"));
                };
                let llvm_ty = self.llvm_type(expr, &ty)?;
                self.lines
                    .push(format!("store {} {}, ptr {}", llvm_ty, rhs, ptr));
                Ok("0".to_string())
            }
            TExprKind::Binary { op, left, right } => {
                let l = self.emit_expr(left)?;
                let r = self.emit_expr(right)?;
                let reg = self.reg();

                match op {
                    BinaryOp::Add if left.ty == Type::F64 => self
                        .lines
                        .push(format!("{} = fadd double {}, {}", reg, l, r)),
                    BinaryOp::Sub if left.ty == Type::F64 => self
                        .lines
                        .push(format!("{} = fsub double {}, {}", reg, l, r)),
                    BinaryOp::Mul if left.ty == Type::F64 => self
                        .lines
                        .push(format!("{} = fmul double {}, {}", reg, l, r)),
                    BinaryOp::Div if left.ty == Type::F64 => self
                        .lines
                        .push(format!("{} = fdiv double {}, {}", reg, l, r)),
                    BinaryOp::Rem if left.ty == Type::F64 => {
                        return Err(self.unsupported(expr, "f64 remainder"))
                    }
                    BinaryOp::BitAnd | BinaryOp::BitOr | BinaryOp::BitXor
                        if left.ty == Type::F64 =>
                    {
                        return Err(self.unsupported(expr, "f64 bitwise operation"))
                    }
                    BinaryOp::Add if left.ty == Type::I64 => {
                        self.lines.push(format!("{} = add i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Add if left.ty == Type::U64 => {
                        self.lines.push(format!("{} = add i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Sub if left.ty == Type::I64 => {
                        self.lines.push(format!("{} = sub i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Sub if left.ty == Type::U64 => {
                        self.lines.push(format!("{} = sub i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Mul if left.ty == Type::I64 => {
                        self.lines.push(format!("{} = mul i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Mul if left.ty == Type::U64 => {
                        self.lines.push(format!("{} = mul i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Div if left.ty == Type::I64 => {
                        self.lines.push(format!("{} = sdiv i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Div if left.ty == Type::U64 => {
                        self.lines.push(format!("{} = udiv i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Rem if left.ty == Type::I64 => {
                        self.lines.push(format!("{} = srem i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Rem if left.ty == Type::U64 => {
                        self.lines.push(format!("{} = urem i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::BitAnd if left.ty == Type::I64 => {
                        self.lines.push(format!("{} = and i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::BitAnd if left.ty == Type::U64 => {
                        self.lines.push(format!("{} = and i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::BitOr if left.ty == Type::I64 => {
                        self.lines.push(format!("{} = or i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::BitOr if left.ty == Type::U64 => {
                        self.lines.push(format!("{} = or i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::BitXor if left.ty == Type::I64 => {
                        self.lines.push(format!("{} = xor i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::BitXor if left.ty == Type::U64 => {
                        self.lines.push(format!("{} = xor i64 {}, {}", reg, l, r))
                    }
                    BinaryOp::Add => self.lines.push(format!("{} = add i32 {}, {}", reg, l, r)),
                    BinaryOp::Sub => self.lines.push(format!("{} = sub i32 {}, {}", reg, l, r)),
                    BinaryOp::Mul => self.lines.push(format!("{} = mul i32 {}, {}", reg, l, r)),
                    BinaryOp::Div if left.ty == Type::U32 => {
                        self.lines.push(format!("{} = udiv i32 {}, {}", reg, l, r))
                    }
                    BinaryOp::Div => self.lines.push(format!("{} = sdiv i32 {}, {}", reg, l, r)),
                    BinaryOp::Rem if left.ty == Type::U32 => {
                        self.lines.push(format!("{} = urem i32 {}, {}", reg, l, r))
                    }
                    BinaryOp::Rem => self.lines.push(format!("{} = srem i32 {}, {}", reg, l, r)),
                    BinaryOp::BitAnd => self.lines.push(format!("{} = and i32 {}, {}", reg, l, r)),
                    BinaryOp::BitOr => self.lines.push(format!("{} = or i32 {}, {}", reg, l, r)),
                    BinaryOp::BitXor => self.lines.push(format!("{} = xor i32 {}, {}", reg, l, r)),
                    BinaryOp::Eq if left.ty == Type::String && right.ty == Type::String => {
                        self.lines.push(format!(
                            "{} = call i1 @__glagol_string_eq(ptr {}, ptr {})",
                            reg, l, r
                        ))
                    }
                    BinaryOp::Eq
                        if vec_eq_runtime_symbol(&left.ty).is_some() && left.ty == right.ty =>
                    {
                        let symbol =
                            vec_eq_runtime_symbol(&left.ty).expect("vector equality symbol");
                        self.lines.push(format!(
                            "{} = call i1 @{}(ptr {}, ptr {})",
                            reg, symbol, l, r
                        ))
                    }
                    BinaryOp::Eq if self.is_payload_enum_type(&left.ty) => {
                        let left_ty = self.llvm_type(left, &left.ty)?;
                        let payload_ty = self
                            .enum_payload_type(&left.ty)
                            .ok_or_else(|| self.unsupported(expr, "enum payload metadata"))?
                            .clone();
                        self.emit_payload_enum_equality(&reg, &payload_ty, &left_ty, &l, &r)?;
                    }
                    BinaryOp::Eq if left.ty == Type::F64 => self
                        .lines
                        .push(format!("{} = fcmp oeq double {}, {}", reg, l, r)),
                    BinaryOp::Eq if left.ty == Type::Bool => self
                        .lines
                        .push(format!("{} = icmp eq i1 {}, {}", reg, l, r)),
                    BinaryOp::Eq if left.ty == Type::I64 => self
                        .lines
                        .push(format!("{} = icmp eq i64 {}, {}", reg, l, r)),
                    BinaryOp::Eq if left.ty == Type::U64 => self
                        .lines
                        .push(format!("{} = icmp eq i64 {}, {}", reg, l, r)),
                    BinaryOp::Eq => self
                        .lines
                        .push(format!("{} = icmp eq i32 {}, {}", reg, l, r)),
                    BinaryOp::Lt if left.ty == Type::F64 => self
                        .lines
                        .push(format!("{} = fcmp olt double {}, {}", reg, l, r)),
                    BinaryOp::Lt if left.ty == Type::I64 => self
                        .lines
                        .push(format!("{} = icmp slt i64 {}, {}", reg, l, r)),
                    BinaryOp::Lt if left.ty == Type::U64 => self
                        .lines
                        .push(format!("{} = icmp ult i64 {}, {}", reg, l, r)),
                    BinaryOp::Lt if left.ty == Type::U32 => self
                        .lines
                        .push(format!("{} = icmp ult i32 {}, {}", reg, l, r)),
                    BinaryOp::Lt => self
                        .lines
                        .push(format!("{} = icmp slt i32 {}, {}", reg, l, r)),
                    BinaryOp::Gt if left.ty == Type::F64 => self
                        .lines
                        .push(format!("{} = fcmp ogt double {}, {}", reg, l, r)),
                    BinaryOp::Gt if left.ty == Type::I64 => self
                        .lines
                        .push(format!("{} = icmp sgt i64 {}, {}", reg, l, r)),
                    BinaryOp::Gt if left.ty == Type::U64 => self
                        .lines
                        .push(format!("{} = icmp ugt i64 {}, {}", reg, l, r)),
                    BinaryOp::Gt if left.ty == Type::U32 => self
                        .lines
                        .push(format!("{} = icmp ugt i32 {}, {}", reg, l, r)),
                    BinaryOp::Gt => self
                        .lines
                        .push(format!("{} = icmp sgt i32 {}, {}", reg, l, r)),
                    BinaryOp::Le if left.ty == Type::F64 => self
                        .lines
                        .push(format!("{} = fcmp ole double {}, {}", reg, l, r)),
                    BinaryOp::Le if left.ty == Type::I64 => self
                        .lines
                        .push(format!("{} = icmp sle i64 {}, {}", reg, l, r)),
                    BinaryOp::Le if left.ty == Type::U64 => self
                        .lines
                        .push(format!("{} = icmp ule i64 {}, {}", reg, l, r)),
                    BinaryOp::Le if left.ty == Type::U32 => self
                        .lines
                        .push(format!("{} = icmp ule i32 {}, {}", reg, l, r)),
                    BinaryOp::Le => self
                        .lines
                        .push(format!("{} = icmp sle i32 {}, {}", reg, l, r)),
                    BinaryOp::Ge if left.ty == Type::F64 => self
                        .lines
                        .push(format!("{} = fcmp oge double {}, {}", reg, l, r)),
                    BinaryOp::Ge if left.ty == Type::I64 => self
                        .lines
                        .push(format!("{} = icmp sge i64 {}, {}", reg, l, r)),
                    BinaryOp::Ge if left.ty == Type::U64 => self
                        .lines
                        .push(format!("{} = icmp uge i64 {}, {}", reg, l, r)),
                    BinaryOp::Ge if left.ty == Type::U32 => self
                        .lines
                        .push(format!("{} = icmp uge i32 {}, {}", reg, l, r)),
                    BinaryOp::Ge => self
                        .lines
                        .push(format!("{} = icmp sge i32 {}, {}", reg, l, r)),
                }

                Ok(reg)
            }
            TExprKind::Call { name, args } => {
                match name.as_str() {
                    "std.num.i32_to_i64" => {
                        return self
                            .emit_numeric_widening_conversion(expr, args, "sext", "i32", "i64");
                    }
                    "std.num.i32_to_f64" => {
                        return self.emit_numeric_widening_conversion(
                            expr, args, "sitofp", "i32", "double",
                        );
                    }
                    "std.num.i64_to_f64" => {
                        return self.emit_numeric_widening_conversion(
                            expr, args, "sitofp", "i64", "double",
                        );
                    }
                    "std.num.i64_to_i32_result" => {
                        return self.emit_i64_to_i32_result_conversion(expr, args);
                    }
                    "std.num.f64_to_i32_result" => {
                        return self.emit_f64_to_i32_result_conversion(expr, args);
                    }
                    "std.num.f64_to_i64_result" => {
                        return self.emit_f64_to_i64_result_conversion(expr, args);
                    }
                    _ => {}
                }

                let mut arg_values = Vec::new();

                for arg in args {
                    let value = self.emit_expr(arg)?;
                    let arg_ty = self.llvm_type(arg, &arg.ty)?;
                    arg_values.push(format!("{} {}", arg_ty, value));
                }

                let arg_values = arg_values.join(", ");
                let callee = std_runtime::runtime_symbol(name).unwrap_or(name);

                if matches!(
                    callee,
                    "__glagol_process_arg_result"
                        | "__glagol_env_get_result"
                        | "__glagol_fs_read_text_result"
                        | "__glagol_fs_read_open_text_result"
                        | "__glagol_net_tcp_read_all_result"
                        | "__glagol_io_read_stdin_result"
                        | "__glagol_string_slice_result"
                        | "__glagol_json_parse_string_value_result"
                ) {
                    return self.emit_string_result_host_call(expr, callee, &arg_values);
                }

                if callee == "__glagol_fs_write_text_result"
                    || callee == "__glagol_fs_remove_file_result"
                    || callee == "__glagol_fs_create_dir_result"
                    || callee == "__glagol_fs_close_result"
                    || callee == "__glagol_net_tcp_write_text_result"
                    || callee == "__glagol_net_tcp_close_result"
                {
                    return self.emit_i32_result_status_call(expr, callee, &arg_values);
                }

                if callee == "__glagol_fs_open_text_read_result"
                    || callee == "__glagol_net_tcp_connect_loopback_result"
                    || callee == "__glagol_net_tcp_listen_loopback_result"
                    || callee == "__glagol_net_tcp_bound_port_result"
                    || callee == "__glagol_net_tcp_accept_result"
                    || callee == "__glagol_string_parse_i32_result"
                    || callee == "__glagol_json_parse_i32_value_result"
                    || callee == "__glagol_string_byte_at_result"
                {
                    return self.emit_i32_result_encoded_i64_call(expr, callee, &arg_values);
                }

                if callee == "__glagol_string_parse_u32_result"
                    || callee == "__glagol_json_parse_u32_value_result"
                {
                    return self.emit_i32_result_encoded_i64_call(expr, callee, &arg_values);
                }

                if callee == "__glagol_string_parse_i64_result"
                    || callee == "__glagol_json_parse_i64_value_result"
                {
                    return self.emit_i64_result_out_param_call(expr, callee, &arg_values);
                }

                if callee == "__glagol_string_parse_u64_result"
                    || callee == "__glagol_json_parse_u64_value_result"
                {
                    return self.emit_i64_result_out_param_call(expr, callee, &arg_values);
                }

                if callee == "__glagol_string_parse_f64_result"
                    || callee == "__glagol_json_parse_f64_value_result"
                {
                    return self.emit_f64_result_out_param_call(expr, callee, &arg_values);
                }

                if callee == "__glagol_string_parse_bool_result"
                    || callee == "__glagol_json_parse_bool_value_result"
                {
                    return self.emit_bool_result_out_param_call(expr, callee, &arg_values);
                }

                if expr.ty == Type::Unit {
                    self.lines
                        .push(format!("call void @{}({})", callee, arg_values));
                    Ok("0".to_string())
                } else {
                    let reg = self.reg();
                    let result_ty = self.llvm_type(expr, &expr.ty)?;
                    self.lines.push(format!(
                        "{} = call {} @{}({})",
                        reg, result_ty, callee, arg_values
                    ));
                    Ok(reg)
                }
            }
            TExprKind::If {
                condition,
                then_expr,
                else_expr,
            } => {
                let then_block = self.block("if.then");
                let else_block = self.block("if.else");
                let end_block = self.block("if.end");
                let cond = self.emit_expr(condition)?;

                self.lines.push(format!(
                    "br i1 {}, label %{}, label %{}",
                    cond, then_block, else_block
                ));

                self.label(&then_block);
                let then_value = self.emit_expr(then_expr)?;
                let then_pred = self.current_block.clone();
                self.lines.push(format!("br label %{}", end_block));

                self.label(&else_block);
                let else_value = self.emit_expr(else_expr)?;
                let else_pred = self.current_block.clone();
                self.lines.push(format!("br label %{}", end_block));

                self.label(&end_block);

                if expr.ty == Type::Unit {
                    Ok("0".to_string())
                } else {
                    let reg = self.reg();
                    let phi_ty = self.llvm_type(expr, &expr.ty)?;
                    self.lines.push(format!(
                        "{} = phi {} [ {}, %{} ], [ {}, %{} ]",
                        reg, phi_ty, then_value, then_pred, else_value, else_pred,
                    ));
                    Ok(reg)
                }
            }
            TExprKind::Match { subject, arms } => self.emit_match(expr, subject, arms),
            TExprKind::While { condition, body } => {
                let cond_block = self.block("while.cond");
                let body_block = self.block("while.body");
                let end_block = self.block("while.end");

                self.lines.push(format!("br label %{}", cond_block));

                self.label(&cond_block);
                let cond = self.emit_expr(condition)?;
                self.lines.push(format!(
                    "br i1 {}, label %{}, label %{}",
                    cond, body_block, end_block
                ));

                self.label(&body_block);
                for expr in body {
                    self.emit_expr(expr)?;
                }
                self.lines.push(format!("br label %{}", cond_block));

                self.label(&end_block);
                Ok("0".to_string())
            }
            TExprKind::Unsafe { body } => {
                let saved_locals = self.locals.clone();
                let mut value = "0".to_string();
                for expr in body {
                    value = self.emit_expr(expr)?;
                }
                self.locals = saved_locals;
                Ok(value)
            }
        }
    }

    fn emit_option_aggregate(
        &mut self,
        expr: &TExpr,
        tag: bool,
        payload: Option<&TExpr>,
    ) -> Result<String, Diagnostic> {
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let tag_value = if tag { "1" } else { "0" };
        let tagged = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} undef, i1 {}, 0",
            tagged, aggregate_ty, tag_value
        ));

        let payload_value = match payload {
            Some(payload) => self.emit_expr(payload)?,
            None => match &expr.ty {
                Type::Option(inner) => zero_value_for_type(inner).ok_or_else(|| {
                    self.unsupported(expr, "option constructor for unsupported payload type")
                })?,
                _ => return Err(self.unsupported(expr, "option constructor for non-option type")),
            },
        };
        let payload_ty = match &expr.ty {
            Type::Option(inner) if **inner == Type::I32 => "i32",
            Type::Option(inner) if **inner == Type::I64 => "i64",
            Type::Option(inner) if **inner == Type::U32 => "i32",
            Type::Option(inner) if **inner == Type::U64 => "i64",
            Type::Option(inner) if **inner == Type::F64 => "double",
            Type::Option(inner) if **inner == Type::Bool => "i1",
            Type::Option(inner) if **inner == Type::String => "ptr",
            _ => return Err(self.unsupported(expr, "option constructor for non-option type")),
        };

        let with_payload = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, {} {}, 1",
            with_payload, aggregate_ty, tagged, payload_ty, payload_value
        ));
        Ok(with_payload)
    }

    fn emit_tagged_i32_aggregate(
        &mut self,
        expr: &TExpr,
        tag: bool,
        payload: Option<&TExpr>,
    ) -> Result<String, Diagnostic> {
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let tag_value = if tag { "1" } else { "0" };
        let tagged = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} undef, i1 {}, 0",
            tagged, aggregate_ty, tag_value
        ));

        let payload_value = match payload {
            Some(payload) => self.emit_expr(payload)?,
            None => "0".to_string(),
        };

        let with_payload = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, i32 {}, 1",
            with_payload, aggregate_ty, tagged, payload_value
        ));
        Ok(with_payload)
    }

    fn emit_enum_variant(
        &mut self,
        expr: &TExpr,
        enum_name: &str,
        discriminant: i32,
        payload: Option<&TExpr>,
    ) -> Result<String, Diagnostic> {
        let Some(layout) = self.enum_layouts.get(enum_name) else {
            return Ok(discriminant.to_string());
        };
        let Some(payload_ty) = layout.payload_ty.as_ref() else {
            return Ok(discriminant.to_string());
        };

        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let tagged = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} undef, i32 {}, 0",
            tagged, aggregate_ty, discriminant
        ));

        let payload_value = match payload {
            Some(payload) => self.emit_expr(payload)?,
            None => self.enum_payload_zero_value(payload_ty)?,
        };
        let payload_llvm_ty = self.enum_payload_llvm_type(payload_ty)?;
        let with_payload = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, {} {}, 1",
            with_payload, aggregate_ty, tagged, payload_llvm_ty, payload_value
        ));
        Ok(with_payload)
    }

    fn emit_result_aggregate(
        &mut self,
        expr: &TExpr,
        is_ok: bool,
        payload: &TExpr,
    ) -> Result<String, Diagnostic> {
        match &expr.ty {
            Type::Result(ok, err) if **ok == Type::I32 && **err == Type::I32 => {
                self.emit_tagged_i32_aggregate(expr, is_ok, Some(payload))
            }
            Type::Result(ok, err) if **ok == Type::I64 && **err == Type::I32 => {
                let payload_value = self.emit_expr(payload)?;
                if is_ok {
                    self.emit_i64_result_aggregate(expr, "1", &payload_value, "0")
                } else {
                    self.emit_i64_result_aggregate(expr, "0", "0", &payload_value)
                }
            }
            Type::Result(ok, err) if **ok == Type::U32 && **err == Type::I32 => {
                self.emit_tagged_i32_aggregate(expr, is_ok, Some(payload))
            }
            Type::Result(ok, err) if **ok == Type::U64 && **err == Type::I32 => {
                let payload_value = self.emit_expr(payload)?;
                if is_ok {
                    self.emit_i64_result_aggregate(expr, "1", &payload_value, "0")
                } else {
                    self.emit_i64_result_aggregate(expr, "0", "0", &payload_value)
                }
            }
            Type::Result(ok, err) if **ok == Type::F64 && **err == Type::I32 => {
                let payload_value = self.emit_expr(payload)?;
                if is_ok {
                    self.emit_f64_result_aggregate(expr, "1", &payload_value, "0")
                } else {
                    self.emit_f64_result_aggregate(expr, "0", "0.0", &payload_value)
                }
            }
            Type::Result(ok, err) if **ok == Type::Bool && **err == Type::I32 => {
                let payload_value = self.emit_expr(payload)?;
                if is_ok {
                    self.emit_bool_result_aggregate(expr, "1", &payload_value, "0")
                } else {
                    self.emit_bool_result_aggregate(expr, "0", "0", &payload_value)
                }
            }
            Type::Result(ok, err) if **ok == Type::String && **err == Type::I32 => {
                let payload_value = self.emit_expr(payload)?;
                if is_ok {
                    self.emit_string_result_aggregate(expr, "1", &payload_value, "0")
                } else {
                    self.emit_string_result_aggregate(expr, "0", "null", &payload_value)
                }
            }
            Type::Result(_, _) => Err(self.unsupported(expr, "unsupported result payload types")),
            _ => Err(self.unsupported(expr, "result constructor for non-result type")),
        }
    }

    fn emit_string_result_aggregate(
        &mut self,
        expr: &TExpr,
        tag_value: &str,
        ok_value: &str,
        err_value: &str,
    ) -> Result<String, Diagnostic> {
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let tagged = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} undef, i1 {}, 0",
            tagged, aggregate_ty, tag_value
        ));

        let with_ok = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, ptr {}, 1",
            with_ok, aggregate_ty, tagged, ok_value
        ));

        let with_err = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, i32 {}, 2",
            with_err, aggregate_ty, with_ok, err_value
        ));
        Ok(with_err)
    }

    fn emit_i64_result_aggregate(
        &mut self,
        expr: &TExpr,
        tag_value: &str,
        ok_value: &str,
        err_value: &str,
    ) -> Result<String, Diagnostic> {
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let tagged = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} undef, i1 {}, 0",
            tagged, aggregate_ty, tag_value
        ));

        let with_ok = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, i64 {}, 1",
            with_ok, aggregate_ty, tagged, ok_value
        ));

        let with_err = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, i32 {}, 2",
            with_err, aggregate_ty, with_ok, err_value
        ));
        Ok(with_err)
    }

    fn emit_f64_result_aggregate(
        &mut self,
        expr: &TExpr,
        tag_value: &str,
        ok_value: &str,
        err_value: &str,
    ) -> Result<String, Diagnostic> {
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let tagged = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} undef, i1 {}, 0",
            tagged, aggregate_ty, tag_value
        ));

        let with_ok = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, double {}, 1",
            with_ok, aggregate_ty, tagged, ok_value
        ));

        let with_err = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, i32 {}, 2",
            with_err, aggregate_ty, with_ok, err_value
        ));
        Ok(with_err)
    }

    fn emit_bool_result_aggregate(
        &mut self,
        expr: &TExpr,
        tag_value: &str,
        ok_value: &str,
        err_value: &str,
    ) -> Result<String, Diagnostic> {
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let tagged = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} undef, i1 {}, 0",
            tagged, aggregate_ty, tag_value
        ));

        let with_ok = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, i1 {}, 1",
            with_ok, aggregate_ty, tagged, ok_value
        ));

        let with_err = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, i32 {}, 2",
            with_err, aggregate_ty, with_ok, err_value
        ));
        Ok(with_err)
    }

    fn emit_string_result_host_call(
        &mut self,
        expr: &TExpr,
        callee: &str,
        arg_values: &str,
    ) -> Result<String, Diagnostic> {
        let payload = self.reg();
        self.lines.push(format!(
            "{} = call ptr @{}({})",
            payload, callee, arg_values
        ));
        let is_ok = self.reg();
        self.lines
            .push(format!("{} = icmp ne ptr {}, null", is_ok, payload));
        let err_code = self.reg();
        self.lines
            .push(format!("{} = select i1 {}, i32 0, i32 1", err_code, is_ok));
        self.emit_string_result_aggregate(expr, &is_ok, &payload, &err_code)
    }

    fn emit_i32_result_status_call(
        &mut self,
        expr: &TExpr,
        callee: &str,
        arg_values: &str,
    ) -> Result<String, Diagnostic> {
        let status = self.reg();
        self.lines
            .push(format!("{} = call i32 @{}({})", status, callee, arg_values));
        let is_ok = self.reg();
        self.lines
            .push(format!("{} = icmp eq i32 {}, 0", is_ok, status));
        self.emit_tagged_i32_aggregate_from_values(expr, &is_ok, &status)
    }

    fn emit_i32_result_encoded_i64_call(
        &mut self,
        expr: &TExpr,
        callee: &str,
        arg_values: &str,
    ) -> Result<String, Diagnostic> {
        let encoded = self.reg();
        self.lines.push(format!(
            "{} = call i64 @{}({})",
            encoded, callee, arg_values
        ));
        let status64 = self.reg();
        self.lines
            .push(format!("{} = lshr i64 {}, 32", status64, encoded));
        let status = self.reg();
        self.lines
            .push(format!("{} = trunc i64 {} to i32", status, status64));
        let is_ok = self.reg();
        self.lines
            .push(format!("{} = icmp eq i32 {}, 0", is_ok, status));
        let payload = self.reg();
        self.lines
            .push(format!("{} = trunc i64 {} to i32", payload, encoded));
        self.emit_tagged_i32_aggregate_from_values(expr, &is_ok, &payload)
    }

    fn emit_i64_result_out_param_call(
        &mut self,
        expr: &TExpr,
        callee: &str,
        arg_values: &str,
    ) -> Result<String, Diagnostic> {
        let out_ptr = self.reg();
        self.lines.push(format!("{} = alloca i64", out_ptr));
        self.lines.push(format!("store i64 0, ptr {}", out_ptr));
        let status = self.reg();
        self.lines.push(format!(
            "{} = call i32 @{}({}, ptr {})",
            status, callee, arg_values, out_ptr
        ));
        let is_ok = self.reg();
        self.lines
            .push(format!("{} = icmp eq i32 {}, 0", is_ok, status));
        let ok_payload = self.reg();
        self.lines
            .push(format!("{} = load i64, ptr {}", ok_payload, out_ptr));
        let err_payload = self.reg();
        self.lines.push(format!(
            "{} = select i1 {}, i32 0, i32 1",
            err_payload, is_ok
        ));
        self.emit_i64_result_aggregate(expr, &is_ok, &ok_payload, &err_payload)
    }

    fn emit_f64_result_out_param_call(
        &mut self,
        expr: &TExpr,
        callee: &str,
        arg_values: &str,
    ) -> Result<String, Diagnostic> {
        let out_ptr = self.reg();
        self.lines.push(format!("{} = alloca double", out_ptr));
        self.lines
            .push(format!("store double 0.0, ptr {}", out_ptr));
        let status = self.reg();
        self.lines.push(format!(
            "{} = call i32 @{}({}, ptr {})",
            status, callee, arg_values, out_ptr
        ));
        let is_ok = self.reg();
        self.lines
            .push(format!("{} = icmp eq i32 {}, 0", is_ok, status));
        let ok_payload = self.reg();
        self.lines
            .push(format!("{} = load double, ptr {}", ok_payload, out_ptr));
        let err_payload = self.reg();
        self.lines.push(format!(
            "{} = select i1 {}, i32 0, i32 1",
            err_payload, is_ok
        ));
        self.emit_f64_result_aggregate(expr, &is_ok, &ok_payload, &err_payload)
    }

    fn emit_bool_result_out_param_call(
        &mut self,
        expr: &TExpr,
        callee: &str,
        arg_values: &str,
    ) -> Result<String, Diagnostic> {
        let out_ptr = self.reg();
        self.lines.push(format!("{} = alloca i1", out_ptr));
        self.lines.push(format!("store i1 0, ptr {}", out_ptr));
        let status = self.reg();
        self.lines.push(format!(
            "{} = call i32 @{}({}, ptr {})",
            status, callee, arg_values, out_ptr
        ));
        let is_ok = self.reg();
        self.lines
            .push(format!("{} = icmp eq i32 {}, 0", is_ok, status));
        let ok_payload = self.reg();
        self.lines
            .push(format!("{} = load i1, ptr {}", ok_payload, out_ptr));
        let err_payload = self.reg();
        self.lines.push(format!(
            "{} = select i1 {}, i32 0, i32 1",
            err_payload, is_ok
        ));
        self.emit_bool_result_aggregate(expr, &is_ok, &ok_payload, &err_payload)
    }

    fn emit_tagged_i32_aggregate_from_values(
        &mut self,
        expr: &TExpr,
        tag_value: &str,
        payload_value: &str,
    ) -> Result<String, Diagnostic> {
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let tagged = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} undef, i1 {}, 0",
            tagged, aggregate_ty, tag_value
        ));

        let with_payload = self.reg();
        self.lines.push(format!(
            "{} = insertvalue {} {}, i32 {}, 1",
            with_payload, aggregate_ty, tagged, payload_value
        ));
        Ok(with_payload)
    }

    fn emit_tag_observer(
        &mut self,
        _expr: &TExpr,
        value: &TExpr,
        invert: bool,
    ) -> Result<String, Diagnostic> {
        let aggregate = self.emit_expr(value)?;
        let aggregate_ty = self.llvm_type(value, &value.ty)?;
        let tag = self.reg();
        self.lines.push(format!(
            "{} = extractvalue {} {}, 0",
            tag, aggregate_ty, aggregate
        ));

        if invert {
            let inverted = self.reg();
            self.lines
                .push(format!("{} = xor i1 {}, true", inverted, tag));
            Ok(inverted)
        } else {
            Ok(tag)
        }
    }

    fn emit_tag_checked_payload_access(
        &mut self,
        _expr: &TExpr,
        value: &TExpr,
        expected_tag: bool,
        trap_fn: &str,
    ) -> Result<String, Diagnostic> {
        let aggregate = self.emit_expr(value)?;
        let aggregate_ty = self.llvm_type(value, &value.ty)?;
        let tag = self.reg();
        self.lines.push(format!(
            "{} = extractvalue {} {}, 0",
            tag, aggregate_ty, aggregate
        ));

        let valid = if expected_tag {
            tag
        } else {
            let valid = self.reg();
            self.lines
                .push(format!("{} = icmp eq i1 {}, 0", valid, tag));
            valid
        };

        let ok_block = self.block("unwrap.ok");
        let trap_block = self.block("unwrap.trap");
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            valid, ok_block, trap_block
        ));

        self.label(&trap_block);
        self.lines.push(format!("call void @{}()", trap_fn));
        self.lines.push("unreachable".to_string());

        self.label(&ok_block);
        let payload_index = result_payload_index(&value.ty, expected_tag)
            .ok_or_else(|| self.unsupported(value, "unsupported result payload access"))?;
        let payload = self.reg();
        self.lines.push(format!(
            "{} = extractvalue {} {}, {}",
            payload, aggregate_ty, aggregate, payload_index
        ));
        Ok(payload)
    }

    fn emit_match(
        &mut self,
        expr: &TExpr,
        subject: &TExpr,
        arms: &[TMatchArm],
    ) -> Result<String, Diagnostic> {
        if matches!(subject.ty, Type::Named(_)) {
            return self.emit_enum_match(expr, subject, arms);
        }

        let subject_value = self.emit_expr(subject)?;
        let aggregate_ty = self.llvm_type(subject, &subject.ty)?;
        let tag = self.reg();
        self.lines.push(format!(
            "{} = extractvalue {} {}, 0",
            tag, aggregate_ty, subject_value
        ));

        let (truthy_kind, falsy_kind, truthy_prefix, falsy_prefix) = match &subject.ty {
            Type::Option(inner)
                if **inner == Type::I32
                    || **inner == Type::I64
                    || **inner == Type::U32
                    || **inner == Type::U64
                    || **inner == Type::F64
                    || **inner == Type::Bool
                    || **inner == Type::String =>
            {
                (
                    MatchPatternKind::Some,
                    MatchPatternKind::None,
                    "match.some",
                    "match.none",
                )
            }
            Type::Result(ok, err) if result_type_supported(ok, err) => (
                MatchPatternKind::Ok,
                MatchPatternKind::Err,
                "match.ok",
                "match.err",
            ),
            _ => {
                return Err(self.unsupported(
                    expr,
                    "match subject types outside concrete option/result families",
                ))
            }
        };

        let truthy_arm = arms
            .iter()
            .find(|arm| arm.pattern == truthy_kind)
            .ok_or_else(|| self.unsupported(expr, "non-exhaustive match lowering"))?;
        let falsy_arm = arms
            .iter()
            .find(|arm| arm.pattern == falsy_kind)
            .ok_or_else(|| self.unsupported(expr, "non-exhaustive match lowering"))?;

        let truthy_block = self.block(truthy_prefix);
        let falsy_block = self.block(falsy_prefix);
        let end_block = self.block("match.end");

        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            tag, truthy_block, falsy_block
        ));

        self.label(&truthy_block);
        let truthy_value =
            self.emit_match_arm(truthy_arm, &subject.ty, &aggregate_ty, &subject_value)?;
        let truthy_pred = self.current_block.clone();
        self.lines.push(format!("br label %{}", end_block));

        self.label(&falsy_block);
        let falsy_value =
            self.emit_match_arm(falsy_arm, &subject.ty, &aggregate_ty, &subject_value)?;
        let falsy_pred = self.current_block.clone();
        self.lines.push(format!("br label %{}", end_block));

        self.label(&end_block);

        if expr.ty == Type::Unit {
            Ok("0".to_string())
        } else {
            let reg = self.reg();
            let phi_ty = self.llvm_type(expr, &expr.ty)?;
            self.lines.push(format!(
                "{} = phi {} [ {}, %{} ], [ {}, %{} ]",
                reg, phi_ty, truthy_value, truthy_pred, falsy_value, falsy_pred,
            ));
            Ok(reg)
        }
    }

    fn emit_enum_match(
        &mut self,
        expr: &TExpr,
        subject: &TExpr,
        arms: &[TMatchArm],
    ) -> Result<String, Diagnostic> {
        let subject_value = self.emit_expr(subject)?;
        let subject_llvm_ty = self.llvm_type(subject, &subject.ty)?;
        let switch_value = if self.is_payload_enum_type(&subject.ty) {
            let tag = self.reg();
            self.lines.push(format!(
                "{} = extractvalue {} {}, 0",
                tag, subject_llvm_ty, subject_value
            ));
            tag
        } else {
            subject_value.clone()
        };
        let end_block = self.block("match.end");
        let default_block = self.block("match.invalid");
        let mut arm_blocks = Vec::new();

        for arm in arms {
            let Some(discriminant) = arm.discriminant else {
                return Err(self.unsupported(expr, "enum match arm without discriminant"));
            };
            arm_blocks.push((discriminant, self.block("match.enum"), arm));
        }

        let cases = arm_blocks
            .iter()
            .map(|(discriminant, block, _)| format!("i32 {}, label %{}", discriminant, block))
            .collect::<Vec<_>>()
            .join("\n    ");
        self.lines.push(format!(
            "switch i32 {}, label %{} [\n    {}\n  ]",
            switch_value, default_block, cases
        ));

        self.label(&default_block);
        self.lines.push("unreachable".to_string());

        let mut values = Vec::new();
        for (_, block, arm) in arm_blocks {
            self.label(&block);
            let value = self.emit_match_arm(arm, &subject.ty, &subject_llvm_ty, &subject_value)?;
            let pred = self.current_block.clone();
            self.lines.push(format!("br label %{}", end_block));
            values.push((value, pred));
        }

        self.label(&end_block);
        if expr.ty == Type::Unit {
            Ok("0".to_string())
        } else {
            let reg = self.reg();
            let phi_ty = self.llvm_type(expr, &expr.ty)?;
            let incoming = values
                .into_iter()
                .map(|(value, pred)| format!("[ {}, %{} ]", value, pred))
                .collect::<Vec<_>>()
                .join(", ");
            self.lines
                .push(format!("{} = phi {} {}", reg, phi_ty, incoming));
            Ok(reg)
        }
    }

    fn emit_match_arm(
        &mut self,
        arm: &TMatchArm,
        subject_ty: &Type,
        aggregate_ty: &str,
        subject_value: &str,
    ) -> Result<String, Diagnostic> {
        let saved_locals = self.locals.clone();

        if let Some(binding) = &arm.binding {
            let payload_index = match subject_ty {
                Type::Named(name)
                    if self
                        .enum_layouts
                        .get(name)
                        .and_then(|layout| layout.payload_ty.as_ref())
                        .is_some() =>
                {
                    Some(1)
                }
                Type::Option(_) => Some(1),
                Type::Result(_, _) => {
                    result_payload_index(subject_ty, matches!(arm.pattern, MatchPatternKind::Ok))
                }
                _ => None,
            }
            .ok_or_else(|| {
                Diagnostic::new(
                    &self.file,
                    "UnsupportedBackendFeature",
                    "backend does not support this match payload binding",
                )
            })?;
            let payload = self.reg();
            self.lines.push(format!(
                "{} = extractvalue {} {}, {}",
                payload, aggregate_ty, subject_value, payload_index
            ));
            self.locals
                .insert(binding.clone(), LocalValue::Value(payload));
        }

        let mut value = "0".to_string();
        for expr in &arm.body {
            value = self.emit_expr(expr)?;
        }

        self.locals = saved_locals;
        Ok(value)
    }

    fn emit_struct_init(
        &mut self,
        expr: &TExpr,
        _name: &str,
        fields: &[(String, TExpr)],
    ) -> Result<String, Diagnostic> {
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let mut aggregate = "undef".to_string();

        for (index, (_, field_expr)) in fields.iter().enumerate() {
            let value = self.emit_expr(field_expr)?;
            let field_ty = self.llvm_type(field_expr, &field_expr.ty)?;
            let with_field = self.reg();
            self.lines.push(format!(
                "{} = insertvalue {} {}, {} {}, {}",
                with_field, aggregate_ty, aggregate, field_ty, value, index
            ));
            aggregate = with_field;
        }

        Ok(aggregate)
    }

    fn emit_array_init(&mut self, expr: &TExpr, elements: &[TExpr]) -> Result<String, Diagnostic> {
        let aggregate_ty = self.llvm_type(expr, &expr.ty)?;
        let mut aggregate = "undef".to_string();

        for (index, element) in elements.iter().enumerate() {
            let value = self.emit_expr(element)?;
            let element_ty = self.llvm_type(element, &element.ty)?;
            let with_element = self.reg();
            self.lines.push(format!(
                "{} = insertvalue {} {}, {} {}, {}",
                with_element, aggregate_ty, aggregate, element_ty, value, index
            ));
            aggregate = with_element;
        }

        Ok(aggregate)
    }

    fn emit_ssa_local(&mut self, name: &str, initializer: &TExpr) -> Result<String, Diagnostic> {
        let value = self.emit_expr(initializer)?;
        self.locals
            .insert(name.to_string(), LocalValue::Value(value));
        Ok("0".to_string())
    }

    fn emit_value_local(&mut self, name: &str, initializer: &TExpr) -> Result<String, Diagnostic> {
        let value = self.emit_expr(initializer)?;
        let llvm_ty = self.llvm_type(initializer, &initializer.ty)?;
        let ptr = self.local_addr(name);
        self.lines.push(format!("{} = alloca {}", ptr, llvm_ty));
        self.lines
            .push(format!("store {} {}, ptr {}", llvm_ty, value, ptr));
        self.locals.insert(
            name.to_string(),
            LocalValue::Ptr {
                ptr,
                ty: initializer.ty.clone(),
            },
        );
        Ok("0".to_string())
    }

    fn emit_array_local(
        &mut self,
        _expr: &TExpr,
        name: &str,
        initializer: &TExpr,
    ) -> Result<String, Diagnostic> {
        let Type::Array(inner, len) = &initializer.ty else {
            return Err(self.unsupported(initializer, "unsupported array locals"));
        };
        let llvm_ty = llvm_fixed_array_type(self.layouts, self.enum_layouts, inner, *len)
            .ok_or_else(|| self.unsupported(initializer, "unsupported array locals"))?;
        let ptr = self.local_addr(name);
        self.lines.push(format!("{} = alloca {}", ptr, llvm_ty));

        if let TExprKind::ArrayInit { elements } = &initializer.kind {
            for (index, element) in elements.iter().enumerate() {
                let value = self.emit_expr(element)?;
                let element_ty = self.llvm_type(element, &element.ty)?;
                let element_ptr = self.reg();
                self.lines.push(format!(
                    "{} = getelementptr inbounds {}, ptr {}, i32 0, i32 {}",
                    element_ptr, llvm_ty, ptr, index
                ));
                self.lines.push(format!(
                    "store {} {}, ptr {}",
                    element_ty, value, element_ptr
                ));
            }
        } else {
            let value = self.emit_expr(initializer)?;
            self.lines
                .push(format!("store {} {}, ptr {}", llvm_ty, value, ptr));
        }

        self.locals.insert(
            name.to_string(),
            LocalValue::Array {
                ptr,
                ty: initializer.ty.clone(),
            },
        );
        Ok("0".to_string())
    }

    fn emit_index(
        &mut self,
        expr: &TExpr,
        array: &TExpr,
        index: &TExpr,
    ) -> Result<String, Diagnostic> {
        if let (TExprKind::ArrayInit { elements }, TExprKind::Int(index)) =
            (&array.kind, &index.kind)
        {
            let index = usize::try_from(*index)
                .map_err(|_| self.unsupported(expr, "negative array indices"))?;
            if index >= elements.len() {
                return Err(self.unsupported(expr, "out-of-bounds array indices"));
            }

            let aggregate = self.emit_expr(array)?;
            let aggregate_ty = self.llvm_type(array, &array.ty)?;
            let value = self.reg();
            self.lines.push(format!(
                "{} = extractvalue {} {}, {}",
                value, aggregate_ty, aggregate, index
            ));
            return Ok(value);
        }

        let Type::Array(inner, len) = &array.ty else {
            return Err(self.unsupported(expr, "indexing non-array values"));
        };
        let llvm_array_ty = llvm_fixed_array_type(self.layouts, self.enum_layouts, inner, *len)
            .ok_or_else(|| self.unsupported(expr, "unsupported fixed array indexing"))?;
        let element_ty = self.llvm_type(expr, inner)?;

        let (array_ptr, len) = self.emit_array_address(expr, array)?;
        let index_value = self.emit_expr(index)?;
        self.emit_array_bounds_check(&index_value, len);

        let element_ptr = self.reg();
        self.lines.push(format!(
            "{} = getelementptr inbounds {}, ptr {}, i32 0, i32 {}",
            element_ptr, llvm_array_ty, array_ptr, index_value
        ));
        let value = self.reg();
        self.lines.push(format!(
            "{} = load {}, ptr {}",
            value, element_ty, element_ptr
        ));
        Ok(value)
    }

    fn emit_array_address(
        &mut self,
        _expr: &TExpr,
        array: &TExpr,
    ) -> Result<(String, usize), Diagnostic> {
        if let TExprKind::Var(name) = &array.kind {
            if let Some(LocalValue::Array { ptr, ty }) = self.locals.get(name).cloned() {
                let Type::Array(_, len) = ty else {
                    return Err(self.unsupported(array, "unsupported fixed array locals"));
                };
                return Ok((ptr, len));
            }
        }

        let Type::Array(inner, len) = &array.ty else {
            return Err(self.unsupported(array, "unsupported fixed array values"));
        };
        let value = self.emit_expr(array)?;
        let llvm_ty = llvm_fixed_array_type(self.layouts, self.enum_layouts, inner, *len)
            .ok_or_else(|| self.unsupported(array, "unsupported fixed array values"))?;
        let ptr = self.local_addr("array.tmp");
        self.lines.push(format!("{} = alloca {}", ptr, llvm_ty));
        self.lines
            .push(format!("store {} {}, ptr {}", llvm_ty, value, ptr));
        Ok((ptr, *len))
    }

    fn emit_array_bounds_check(&mut self, index_value: &str, len: usize) {
        let lower_ok = self.reg();
        self.lines
            .push(format!("{} = icmp sge i32 {}, 0", lower_ok, index_value));
        let upper_ok = self.reg();
        self.lines.push(format!(
            "{} = icmp slt i32 {}, {}",
            upper_ok, index_value, len
        ));
        let in_bounds = self.reg();
        self.lines
            .push(format!("{} = and i1 {}, {}", in_bounds, lower_ok, upper_ok));

        let ok_block = self.block("array.index.ok");
        let trap_block = self.block("array.index.trap");
        self.lines.push(format!(
            "br i1 {}, label %{}, label %{}",
            in_bounds, ok_block, trap_block
        ));

        self.label(&trap_block);
        self.lines
            .push("call void @__glagol_array_bounds_trap()".to_string());
        self.lines.push("unreachable".to_string());

        self.label(&ok_block);
    }

    fn emit_field_access(
        &mut self,
        expr: &TExpr,
        value: &TExpr,
        field: &str,
    ) -> Result<String, Diagnostic> {
        let Type::Named(struct_name) = &value.ty else {
            return Err(self.unsupported(expr, "field access on non-struct values"));
        };

        let Some(layout) = self.layouts.get(struct_name) else {
            return Err(self.unsupported(expr, "unknown struct layouts"));
        };

        let Some((index, (_, field_ty))) = layout
            .fields
            .iter()
            .enumerate()
            .find(|(_, (name, _))| name == field)
        else {
            return Err(self.unsupported(expr, "unknown struct fields"));
        };

        let aggregate = self.emit_expr(value)?;
        let aggregate_ty = self.llvm_type(value, &value.ty)?;
        let _field_ty = self.llvm_type(expr, field_ty)?;
        let result = self.reg();
        self.lines.push(format!(
            "{} = extractvalue {} {}, {}",
            result, aggregate_ty, aggregate, index
        ));
        Ok(result)
    }
}
