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

use crate::{
    ast::{
        BinaryOp, CImportDecl, EnumDecl, EnumVariantDecl, Expr, ExprKind, Function, MatchArm,
        MatchPattern, MatchPatternKind, Param, Program, StructDecl, StructField, StructInitField,
        Test, TypeAliasDecl,
    },
    diag::Diagnostic,
    reserved::{
        is_unsupported_generic_standard_library_call, unsupported_generic_function,
        unsupported_generic_standard_library_call, unsupported_generic_type_alias,
        unsupported_reserved_type_diagnostic,
    },
    sexpr::{Atom, SExpr, SExprKind},
    token::Span,
    types::Type,
};

pub fn lower_program(file: &str, forms: &[SExpr]) -> Result<Program, Vec<Diagnostic>> {
    lower_program_with_imported_names(file, forms, &[])
}

pub fn lower_program_with_imported_names(
    file: &str,
    forms: &[SExpr],
    imported_names: &[String],
) -> Result<Program, Vec<Diagnostic>> {
    let mut module = None;
    let mut type_aliases = Vec::new();
    let mut alias_names = HashMap::new();
    let mut enums = Vec::new();
    let mut enum_names = imported_names.iter().cloned().collect::<HashSet<_>>();
    let mut structs = Vec::new();
    let mut struct_names = HashSet::new();
    let mut c_imports = Vec::new();
    let mut functions = Vec::new();
    let mut tests = Vec::new();
    let mut test_names = HashMap::new();
    let mut errors = Vec::new();

    for form in forms {
        match list_head(form) {
            Some("enum") => match lower_enum(file, form) {
                Ok(enum_decl) => {
                    enum_names.insert(enum_decl.name.clone());
                    enums.push(enum_decl);
                }
                Err(mut errs) => errors.append(&mut errs),
            },
            Some("struct") => match lower_struct(file, form) {
                Ok(struct_decl) => {
                    struct_names.insert(struct_decl.name.clone());
                    structs.push(struct_decl);
                }
                Err(mut errs) => errors.append(&mut errs),
            },
            Some("type") => match lower_type_alias(file, form) {
                Ok(alias) => match alias_names.entry(alias.name.clone()) {
                    Entry::Vacant(entry) => {
                        entry.insert(alias.name_span);
                        type_aliases.push(alias);
                    }
                    Entry::Occupied(entry) => errors.push(
                        Diagnostic::new(
                            file,
                            "DuplicateTypeAlias",
                            format!("duplicate type alias `{}`", alias.name),
                        )
                        .with_span(alias.name_span)
                        .related("original type alias", *entry.get()),
                    ),
                },
                Err(mut errs) => errors.append(&mut errs),
            },
            _ => {}
        }
    }

    for alias in &type_aliases {
        if is_reserved_type_name(&alias.name) {
            errors.push(
                Diagnostic::new(
                    file,
                    "TypeAliasNameConflict",
                    format!(
                        "type alias `{}` conflicts with a built-in type name",
                        alias.name
                    ),
                )
                .with_span(alias.name_span)
                .hint("choose an alias name distinct from built-in type names"),
            );
        }
        if let Some(struct_decl) = structs.iter().find(|decl| decl.name == alias.name) {
            errors.push(
                Diagnostic::new(
                    file,
                    "TypeAliasNameConflict",
                    format!("type alias `{}` conflicts with a struct", alias.name),
                )
                .with_span(alias.name_span)
                .related("struct declaration", struct_decl.name_span),
            );
        }
        if let Some(enum_decl) = enums.iter().find(|decl| decl.name == alias.name) {
            errors.push(
                Diagnostic::new(
                    file,
                    "TypeAliasNameConflict",
                    format!("type alias `{}` conflicts with an enum", alias.name),
                )
                .with_span(alias.name_span)
                .related("enum declaration", enum_decl.name_span),
            );
        }
    }

    for form in forms {
        match list_head(form) {
            Some("module") => match lower_module(file, form) {
                Ok(name) => module = Some(name),
                Err(err) => errors.push(err),
            },
            Some("enum") => {}
            Some("struct") => {}
            Some("type") => {}
            Some("import_c") => match lower_c_import(file, form) {
                Ok(import) => c_imports.push(import),
                Err(mut errs) => errors.append(&mut errs),
            },
            Some("fn") => match lower_function(file, form, &struct_names, &enum_names) {
                Ok(function) => functions.push(function),
                Err(mut errs) => errors.append(&mut errs),
            },
            Some("test") => match lower_test(file, form, &struct_names, &enum_names) {
                Ok(test) => match test_names.entry(test.name.clone()) {
                    Entry::Vacant(entry) => {
                        entry.insert(test.name_span);
                        tests.push(test);
                    }
                    Entry::Occupied(entry) => errors.push(
                        Diagnostic::new(
                            file,
                            "DuplicateTestName",
                            format!("duplicate test name `{}`", test.name),
                        )
                        .with_span(test.name_span)
                        .hint("test names must be unique within a module")
                        .related("original test name", *entry.get()),
                    ),
                },
                Err(mut errs) => errors.append(&mut errs),
            },
            Some(other) => errors.push(
                Diagnostic::new(
                    file,
                    "UnknownTopLevelForm",
                    format!("unknown top-level form `{}`", other),
                )
                .with_span(form.span),
            ),
            None => errors.push(
                Diagnostic::new(file, "ExpectedTopLevelForm", "expected top-level form")
                    .with_span(form.span),
            ),
        }
    }

    if errors.is_empty() {
        Ok(Program {
            module: module.unwrap_or_else(|| "main".to_string()),
            type_aliases,
            enums,
            structs,
            c_imports,
            functions,
            tests,
        })
    } else {
        Err(errors)
    }
}

pub fn print_program(program: &Program) -> String {
    let mut output = String::new();

    output.push_str("program ");
    output.push_str(&program.module);
    output.push('\n');

    for alias in &program.type_aliases {
        output.push_str("  type ");
        output.push_str(&alias.name);
        output.push_str(" = ");
        output.push_str(&alias.target.to_string());
        output.push('\n');
    }

    for enum_decl in &program.enums {
        output.push_str("  enum ");
        output.push_str(&enum_decl.name);
        output.push('\n');
        for variant in &enum_decl.variants {
            output.push_str("    variant ");
            output.push_str(&variant.name);
            if let Some(payload_ty) = &variant.payload_ty {
                output.push(' ');
                output.push_str(&payload_ty.to_string());
            }
            output.push('\n');
        }
    }

    for struct_decl in &program.structs {
        output.push_str("  struct ");
        output.push_str(&struct_decl.name);
        output.push('\n');
        for field in &struct_decl.fields {
            output.push_str("    field ");
            output.push_str(&field.name);
            output.push_str(": ");
            output.push_str(&field.ty.to_string());
            output.push('\n');
        }
    }

    for import in &program.c_imports {
        output.push_str("  import_c ");
        output.push_str(&import.name);
        output.push('(');
        for (index, param) in import.params.iter().enumerate() {
            if index > 0 {
                output.push_str(", ");
            }
            output.push_str(&param.name);
            output.push_str(": ");
            output.push_str(&param.ty.to_string());
        }
        output.push_str(") -> ");
        output.push_str(&import.return_type.to_string());
        output.push('\n');
    }

    for function in &program.functions {
        output.push_str("  fn ");
        output.push_str(&function.name);
        output.push('(');
        for (index, param) in function.params.iter().enumerate() {
            if index > 0 {
                output.push_str(", ");
            }
            output.push_str(&param.name);
            output.push_str(": ");
            output.push_str(&param.ty.to_string());
        }
        output.push_str(") -> ");
        output.push_str(&function.return_type.to_string());
        output.push('\n');

        for expr in &function.body {
            write_expr(expr, 2, &mut output);
        }
    }

    for test in &program.tests {
        output.push_str("  test \"");
        for ch in test.name.chars() {
            output.extend(ch.escape_default());
        }
        output.push_str("\"\n");
        for expr in &test.body {
            write_expr(expr, 2, &mut output);
        }
    }

    output
}

fn write_expr(expr: &Expr, indent: usize, output: &mut String) {
    output.push_str(&"  ".repeat(indent));

    match &expr.kind {
        ExprKind::Int(value) => {
            output.push_str("int ");
            output.push_str(&value.to_string());
            output.push('\n');
        }
        ExprKind::Int64(value) => {
            output.push_str("i64 ");
            output.push_str(&value.to_string());
            output.push('\n');
        }
        ExprKind::UInt32(value) => {
            output.push_str("u32 ");
            output.push_str(&value.to_string());
            output.push('\n');
        }
        ExprKind::UInt64(value) => {
            output.push_str("u64 ");
            output.push_str(&value.to_string());
            output.push('\n');
        }
        ExprKind::Float(value) => {
            output.push_str("float ");
            output.push_str(&value.to_string());
            output.push('\n');
        }
        ExprKind::Bool(value) => {
            output.push_str("bool ");
            output.push_str(&value.to_string());
            output.push('\n');
        }
        ExprKind::String(value) => {
            output.push_str("string \"");
            for ch in value.chars() {
                output.extend(ch.escape_default());
            }
            output.push_str("\"\n");
        }
        ExprKind::Var(name) => {
            output.push_str("var ");
            output.push_str(name);
            output.push('\n');
        }
        ExprKind::StructInit { name, fields, .. } => {
            output.push_str("construct ");
            output.push_str(name);
            output.push('\n');
            for field in fields {
                output.push_str(&"  ".repeat(indent + 1));
                output.push_str("field ");
                output.push_str(&field.name);
                output.push('\n');
                write_expr(&field.expr, indent + 2, output);
            }
        }
        ExprKind::ArrayInit {
            elem_ty, elements, ..
        } => {
            output.push_str("array ");
            output.push_str(&elem_ty.to_string());
            output.push('\n');
            for element in elements {
                write_expr(element, indent + 1, output);
            }
        }
        ExprKind::OptionSome {
            payload_ty, value, ..
        } => {
            output.push_str("some ");
            output.push_str(&payload_ty.to_string());
            output.push('\n');
            write_expr(value, indent + 1, output);
        }
        ExprKind::OptionNone { payload_ty, .. } => {
            output.push_str("none ");
            output.push_str(&payload_ty.to_string());
            output.push('\n');
        }
        ExprKind::ResultOk {
            ok_ty,
            err_ty,
            value,
            ..
        } => {
            output.push_str("ok ");
            output.push_str(&ok_ty.to_string());
            output.push(' ');
            output.push_str(&err_ty.to_string());
            output.push('\n');
            write_expr(value, indent + 1, output);
        }
        ExprKind::ResultErr {
            ok_ty,
            err_ty,
            value,
            ..
        } => {
            output.push_str("err ");
            output.push_str(&ok_ty.to_string());
            output.push(' ');
            output.push_str(&err_ty.to_string());
            output.push('\n');
            write_expr(value, indent + 1, output);
        }
        ExprKind::OptionIsSome { value } => {
            output.push_str("is_some\n");
            write_expr(value, indent + 1, output);
        }
        ExprKind::OptionIsNone { value } => {
            output.push_str("is_none\n");
            write_expr(value, indent + 1, output);
        }
        ExprKind::OptionUnwrapSome { value } => {
            output.push_str("unwrap_some\n");
            write_expr(value, indent + 1, output);
        }
        ExprKind::ResultIsOk { source_name, value } => {
            output.push_str(source_name);
            output.push('\n');
            write_expr(value, indent + 1, output);
        }
        ExprKind::ResultIsErr { source_name, value } => {
            output.push_str(source_name);
            output.push('\n');
            write_expr(value, indent + 1, output);
        }
        ExprKind::ResultUnwrapOk { source_name, value } => {
            output.push_str(source_name);
            output.push('\n');
            write_expr(value, indent + 1, output);
        }
        ExprKind::ResultUnwrapErr { source_name, value } => {
            output.push_str(source_name);
            output.push('\n');
            write_expr(value, indent + 1, output);
        }
        ExprKind::EnumVariant {
            enum_name,
            variant,
            args,
            ..
        } => {
            output.push_str("enum-variant ");
            output.push_str(enum_name);
            output.push('.');
            output.push_str(variant);
            output.push('\n');
            for arg in args {
                write_expr(arg, indent + 1, output);
            }
        }
        ExprKind::FieldAccess { value, field, .. } => {
            output.push_str("field-access ");
            output.push_str(field);
            output.push('\n');
            write_expr(value, indent + 1, output);
        }
        ExprKind::Index { array, index } => {
            output.push_str("index\n");
            write_expr(array, indent + 1, output);
            write_expr(index, indent + 1, output);
        }
        ExprKind::Local {
            mutable,
            name,
            ty,
            expr,
            ..
        } => {
            output.push_str(if *mutable { "local var " } else { "local let " });
            output.push_str(name);
            output.push_str(": ");
            output.push_str(&ty.to_string());
            output.push('\n');
            write_expr(expr, indent + 1, output);
        }
        ExprKind::Set { name, expr, .. } => {
            output.push_str("set ");
            output.push_str(name);
            output.push('\n');
            write_expr(expr, indent + 1, output);
        }
        ExprKind::Binary { op, left, right } => {
            output.push_str("binary ");
            output.push_str(binary_op_name(*op));
            output.push('\n');
            write_expr(left, indent + 1, output);
            write_expr(right, indent + 1, output);
        }
        ExprKind::If {
            condition,
            then_expr,
            else_expr,
        } => {
            output.push_str("if\n");
            write_expr(condition, indent + 1, output);
            write_expr(then_expr, indent + 1, output);
            write_expr(else_expr, indent + 1, output);
        }
        ExprKind::Match { subject, arms } => {
            output.push_str("match\n");
            output.push_str(&"  ".repeat(indent + 1));
            output.push_str("subject\n");
            write_expr(subject, indent + 2, output);
            for arm in arms {
                output.push_str(&"  ".repeat(indent + 1));
                output.push_str("arm ");
                output.push_str(&match_pattern_name(&arm.pattern.kind));
                if let Some(binding) = &arm.pattern.binding {
                    output.push(' ');
                    output.push_str(binding);
                }
                output.push('\n');
                for expr in &arm.body {
                    write_expr(expr, indent + 2, output);
                }
            }
        }
        ExprKind::While { condition, body } => {
            output.push_str("while\n");
            write_expr(condition, indent + 1, output);
            for expr in body {
                write_expr(expr, indent + 1, output);
            }
        }
        ExprKind::Unsafe { body } => {
            output.push_str("unsafe\n");
            for expr in body {
                write_expr(expr, indent + 1, output);
            }
        }
        ExprKind::Call { name, args, .. } => {
            output.push_str("call ");
            output.push_str(name);
            output.push('\n');
            for arg in args {
                write_expr(arg, indent + 1, output);
            }
        }
    }
}

pub fn match_pattern_name(kind: &MatchPatternKind) -> String {
    match kind {
        MatchPatternKind::Some => "some".to_string(),
        MatchPatternKind::None => "none".to_string(),
        MatchPatternKind::Ok => "ok".to_string(),
        MatchPatternKind::Err => "err".to_string(),
        MatchPatternKind::EnumVariant { enum_name, variant } => {
            format!("{}.{}", enum_name, variant)
        }
    }
}

pub fn binary_op_name(op: BinaryOp) -> &'static str {
    match op {
        BinaryOp::Add => "+",
        BinaryOp::Sub => "-",
        BinaryOp::Mul => "*",
        BinaryOp::Div => "/",
        BinaryOp::Rem => "%",
        BinaryOp::BitAnd => "bit_and",
        BinaryOp::BitOr => "bit_or",
        BinaryOp::BitXor => "bit_xor",
        BinaryOp::Eq => "=",
        BinaryOp::Lt => "<",
        BinaryOp::Gt => ">",
        BinaryOp::Le => "<=",
        BinaryOp::Ge => ">=",
    }
}

fn lower_module(file: &str, form: &SExpr) -> Result<String, Diagnostic> {
    let items = expect_list(form).ok_or_else(|| {
        Diagnostic::new(file, "ExpectedList", "expected module list").with_span(form.span)
    })?;

    if items.len() != 2 && items.len() != 3 {
        return Err(Diagnostic::new(
            file,
            "InvalidModule",
            "module form must be `(module name)` or `(module name (export ...))`",
        )
        .with_span(form.span));
    }

    if let Some(export_form) = items.get(2) {
        let export_items = expect_list(export_form).ok_or_else(|| {
            Diagnostic::new(
                file,
                "InvalidExport",
                "export list must be `(export name...)`",
            )
            .with_span(export_form.span)
        })?;
        if !matches!(export_items.first().and_then(expect_ident), Some("export")) {
            return Err(Diagnostic::new(
                file,
                "InvalidExport",
                "module option must be an export list",
            )
            .with_span(export_form.span));
        }
        let mut seen = HashMap::new();
        for item in &export_items[1..] {
            let Some(name) = expect_ident(item) else {
                return Err(Diagnostic::new(
                    file,
                    "InvalidExport",
                    "exported name must be an identifier",
                )
                .with_span(item.span));
            };
            if seen.insert(name.to_string(), item.span).is_some() {
                return Err(Diagnostic::new(
                    file,
                    "DuplicateName",
                    format!("duplicate exported name `{}`", name),
                )
                .with_span(item.span));
            }
        }
    }

    expect_ident(&items[1])
        .map(|s| s.to_string())
        .ok_or_else(|| {
            Diagnostic::new(
                file,
                "InvalidModuleName",
                "module name must be an identifier",
            )
            .with_span(items[1].span)
        })
}

fn lower_type_alias(file: &str, form: &SExpr) -> Result<TypeAliasDecl, Vec<Diagnostic>> {
    let mut errors = Vec::new();
    let Some(items) = expect_list(form) else {
        return Err(vec![Diagnostic::new(
            file,
            "MalformedTypeAlias",
            "type alias form must be a list",
        )
        .with_span(form.span)]);
    };

    if matches!(items.get(2).and_then(list_head), Some("type_params")) {
        return Err(vec![unsupported_generic_type_alias(
            file,
            items.get(2).map_or(form.span, |item| item.span),
        )]);
    }

    if items.len() != 3 {
        return Err(vec![Diagnostic::new(
            file,
            "MalformedTypeAlias",
            "type alias form must be `(type Alias TargetType)`",
        )
        .with_span(form.span)
        .expected("(type Alias TargetType)")]);
    }

    let name = match expect_ident(&items[1]) {
        Some(name) => name.to_string(),
        None => {
            errors.push(
                Diagnostic::new(
                    file,
                    "InvalidTypeAliasName",
                    "type alias name must be an identifier",
                )
                .with_span(items[1].span),
            );
            "<error>".to_string()
        }
    };

    let target = match lower_type(&items[2]) {
        Some(ty) => ty,
        None => {
            errors.push(invalid_type_diagnostic(
                file,
                &items[2],
                "InvalidTypeAliasTarget",
                "type alias target type is invalid",
                Some("concrete type"),
                None,
            ));
            Type::Unit
        }
    };

    if errors.is_empty() {
        Ok(TypeAliasDecl {
            name,
            name_span: items[1].span,
            target,
            target_span: items[2].span,
            span: form.span,
        })
    } else {
        Err(errors)
    }
}

fn lower_struct(file: &str, form: &SExpr) -> Result<StructDecl, Vec<Diagnostic>> {
    let mut errors = Vec::new();
    let Some(items) = expect_list(form) else {
        return Err(vec![Diagnostic::new(
            file,
            "ExpectedList",
            "expected struct list",
        )
        .with_span(form.span)]);
    };

    if items.len() < 2 {
        return Err(vec![Diagnostic::new(
            file,
            "MalformedStructForm",
            "struct form must be `(struct Name (field type)...)`",
        )
        .with_span(form.span)]);
    }

    let name = match expect_ident(&items[1]) {
        Some(name) => name.to_string(),
        None => {
            errors.push(
                Diagnostic::new(
                    file,
                    "InvalidStructName",
                    "struct name must be an identifier",
                )
                .with_span(items[1].span),
            );
            "<error>".to_string()
        }
    };

    let mut fields = Vec::new();
    for item in &items[2..] {
        let Some(pair) = expect_list(item) else {
            errors.push(
                Diagnostic::new(
                    file,
                    "InvalidStructField",
                    "struct field must be `(name type)`",
                )
                .with_span(item.span),
            );
            continue;
        };

        if pair.len() != 2 {
            errors.push(
                Diagnostic::new(
                    file,
                    "InvalidStructField",
                    "struct field must be `(name type)`",
                )
                .with_span(item.span),
            );
            continue;
        }

        let Some(field_name) = expect_ident(&pair[0]) else {
            errors.push(
                Diagnostic::new(
                    file,
                    "InvalidStructFieldName",
                    "struct field name must be an identifier",
                )
                .with_span(pair[0].span),
            );
            continue;
        };

        let Some(ty) = lower_type(&pair[1]) else {
            errors.push(invalid_type_diagnostic(
                file,
                &pair[1],
                "InvalidStructFieldType",
                "invalid struct field type",
                None,
                None,
            ));
            continue;
        };

        fields.push(StructField {
            name: field_name.to_string(),
            name_span: pair[0].span,
            ty,
            ty_span: pair[1].span,
        });
    }

    if errors.is_empty() {
        Ok(StructDecl {
            name,
            name_span: items[1].span,
            fields,
            span: form.span,
        })
    } else {
        Err(errors)
    }
}

fn lower_enum(file: &str, form: &SExpr) -> Result<EnumDecl, Vec<Diagnostic>> {
    let mut errors = Vec::new();
    let Some(items) = expect_list(form) else {
        return Err(vec![Diagnostic::new(
            file,
            "ExpectedList",
            "expected enum list",
        )
        .with_span(form.span)]);
    };

    if items.len() < 2 {
        return Err(vec![Diagnostic::new(
            file,
            "MalformedEnumForm",
            "enum form must be `(enum Name Variant...)`",
        )
        .with_span(form.span)]);
    }

    let name = match expect_ident(&items[1]) {
        Some(name) => name.to_string(),
        None => {
            errors.push(
                Diagnostic::new(file, "InvalidEnumName", "enum name must be an identifier")
                    .with_span(items[1].span),
            );
            "<error>".to_string()
        }
    };

    let mut variants = Vec::new();
    for item in &items[2..] {
        match expect_ident(item) {
            Some(name) => variants.push(EnumVariantDecl {
                name: name.to_string(),
                name_span: item.span,
                payload_ty: None,
                payload_ty_span: None,
            }),
            None => {
                let Some(variant_items) = expect_list(item) else {
                    errors.push(
                        Diagnostic::new(
                            file,
                            "InvalidEnumVariant",
                            "enum variants must be identifiers or unary payload forms",
                        )
                        .with_span(item.span)
                        .expected("Variant or (Variant i32)"),
                    );
                    continue;
                };

                if variant_items.len() != 2 {
                    errors.push(
                        Diagnostic::new(
                            file,
                            "InvalidEnumVariant",
                            "enum payload variants must be unary forms",
                        )
                        .with_span(item.span)
                        .expected("(Variant i32)"),
                    );
                    continue;
                }

                let Some(name) = expect_ident(&variant_items[0]) else {
                    errors.push(
                        Diagnostic::new(
                            file,
                            "InvalidEnumVariant",
                            "enum variant name must be an identifier",
                        )
                        .with_span(variant_items[0].span),
                    );
                    continue;
                };

                let Some(payload_ty) = lower_type(&variant_items[1]) else {
                    errors.push(invalid_type_diagnostic(
                        file,
                        &variant_items[1],
                        "InvalidEnumVariant",
                        "enum variant payload type is invalid",
                        Some("i32"),
                        None,
                    ));
                    continue;
                };

                variants.push(EnumVariantDecl {
                    name: name.to_string(),
                    name_span: variant_items[0].span,
                    payload_ty: Some(payload_ty),
                    payload_ty_span: Some(variant_items[1].span),
                });
            }
        }
    }

    if errors.is_empty() {
        Ok(EnumDecl {
            name,
            name_span: items[1].span,
            variants,
            span: form.span,
        })
    } else {
        Err(errors)
    }
}

fn lower_function(
    file: &str,
    form: &SExpr,
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Function, Vec<Diagnostic>> {
    let mut errors = Vec::new();
    let Some(items) = expect_list(form) else {
        return Err(vec![Diagnostic::new(
            file,
            "ExpectedList",
            "expected function list",
        )
        .with_span(form.span)]);
    };

    if items.len() < 6 {
        return Err(vec![Diagnostic::new(
            file,
            "InvalidFunction",
            "function form is incomplete",
        )
        .with_span(form.span)
        .hint("expected `(fn name ((arg Type) ...) -> ReturnType body...)`")]);
    }

    if matches!(items.get(2).and_then(list_head), Some("type_params")) {
        return Err(vec![unsupported_generic_function(file, items[2].span)]);
    }

    let name = match expect_ident(&items[1]) {
        Some(name) => name.to_string(),
        None => {
            errors.push(
                Diagnostic::new(
                    file,
                    "InvalidFunctionName",
                    "function name must be an identifier",
                )
                .with_span(items[1].span),
            );
            "<error>".to_string()
        }
    };

    let params = match lower_params(file, &items[2]) {
        Ok(params) => params,
        Err(mut errs) => {
            errors.append(&mut errs);
            Vec::new()
        }
    };

    if !matches!(items[3].kind, SExprKind::Atom(Atom::Arrow)) {
        errors.push(
            Diagnostic::new(file, "ExpectedArrow", "expected `->` in function signature")
                .with_span(items[3].span),
        );
    }

    let return_type = match lower_type(&items[4]) {
        Some(Type::Unit) => {
            errors.push(unsupported_unit_return_signature(file, items[4].span));
            Type::Unit
        }
        Some(ty) => ty,
        None => {
            errors.push(invalid_type_diagnostic(
                file,
                &items[4],
                "InvalidReturnType",
                "invalid return type",
                None,
                None,
            ));
            Type::Unit
        }
    };

    let mut body = Vec::new();
    for expr in &items[5..] {
        match lower_expr(file, expr, struct_names, enum_names) {
            Ok(expr) => body.push(expr),
            Err(err) => errors.push(err),
        }
    }

    if errors.is_empty() {
        Ok(Function {
            name,
            params,
            return_type,
            return_type_span: items[4].span,
            body,
            span: form.span,
        })
    } else {
        Err(errors)
    }
}

fn lower_c_import(file: &str, form: &SExpr) -> Result<CImportDecl, Vec<Diagnostic>> {
    let mut errors = Vec::new();
    let Some(items) = expect_list(form) else {
        return Err(vec![Diagnostic::new(
            file,
            "MalformedCImport",
            "`import_c` declaration must be a list",
        )
        .with_span(form.span)]);
    };

    if items.len() != 5 {
        return Err(vec![Diagnostic::new(
            file,
            "MalformedCImport",
            "`import_c` form must be `(import_c name ((arg i32)...) -> ReturnType)`",
        )
        .with_span(form.span)
        .expected("(import_c name ((arg i32)...) -> i32)")]);
    }

    let name = match expect_ident(&items[1]) {
        Some(name) => name.to_string(),
        None => {
            errors.push(
                Diagnostic::new(
                    file,
                    "MalformedCImport",
                    "C import name must be an identifier",
                )
                .with_span(items[1].span),
            );
            "<error>".to_string()
        }
    };
    if name != "<error>" && !is_c_symbol_name(&name) {
        errors.push(
            Diagnostic::new(
                file,
                "MalformedCImport",
                "C import name must be a C symbol identifier",
            )
            .with_span(items[1].span)
            .expected("ASCII letter or `_`, followed by ASCII letters, digits, or `_`")
            .found(name.clone()),
        );
    }

    let params = match lower_c_import_params(file, &items[2]) {
        Ok(params) => params,
        Err(mut errs) => {
            errors.append(&mut errs);
            Vec::new()
        }
    };

    if !matches!(items[3].kind, SExprKind::Atom(Atom::Arrow)) {
        errors.push(
            Diagnostic::new(
                file,
                "MalformedCImport",
                "expected `->` in C import signature",
            )
            .with_span(items[3].span),
        );
    }

    let return_type = match lower_type(&items[4]) {
        Some(ty) => ty,
        None => {
            errors.push(invalid_type_diagnostic(
                file,
                &items[4],
                "MalformedCImport",
                "invalid C import return type",
                None,
                None,
            ));
            Type::Unit
        }
    };

    for param in &params {
        if param.ty != Type::I32 {
            errors.push(
                Diagnostic::new(
                    file,
                    "UnsupportedCImportType",
                    "C import parameters support only `i32` in exp-6",
                )
                .with_span(param.ty_span)
                .expected(Type::I32.to_string())
                .found(param.ty.to_string()),
            );
        }
    }

    if return_type != Type::I32 && return_type != Type::Unit {
        errors.push(
            Diagnostic::new(
                file,
                "UnsupportedCImportType",
                "C import return type must be `i32` or `unit` in exp-6",
            )
            .with_span(items[4].span)
            .expected("i32 or unit")
            .found(return_type.to_string()),
        );
    }

    if errors.is_empty() {
        Ok(CImportDecl {
            name,
            name_span: items[1].span,
            params,
            return_type,
            return_type_span: items[4].span,
            span: form.span,
        })
    } else {
        Err(errors)
    }
}

fn lower_c_import_params(file: &str, form: &SExpr) -> Result<Vec<Param>, Vec<Diagnostic>> {
    let mut errors = Vec::new();
    let Some(items) = expect_list(form) else {
        return Err(vec![Diagnostic::new(
            file,
            "MalformedCImport",
            "C import parameters must be a list",
        )
        .with_span(form.span)]);
    };

    let mut params = Vec::new();
    let mut seen = HashMap::<String, Span>::new();
    for item in items {
        let Some(pair) = expect_list(item) else {
            errors.push(
                Diagnostic::new(
                    file,
                    "MalformedCImport",
                    "C import parameter must be `(name Type)`",
                )
                .with_span(item.span),
            );
            continue;
        };
        if pair.len() != 2 {
            errors.push(
                Diagnostic::new(
                    file,
                    "MalformedCImport",
                    "C import parameter must be `(name Type)`",
                )
                .with_span(item.span),
            );
            continue;
        }
        let Some(name) = expect_ident(&pair[0]) else {
            errors.push(
                Diagnostic::new(
                    file,
                    "MalformedCImport",
                    "C import parameter name must be an identifier",
                )
                .with_span(pair[0].span),
            );
            continue;
        };
        if let Some(original) = seen.insert(name.to_string(), pair[0].span) {
            errors.push(
                Diagnostic::new(
                    file,
                    "DuplicateName",
                    format!("duplicate C import parameter `{}`", name),
                )
                .with_span(pair[0].span)
                .related("original parameter", original),
            );
        }
        let Some(ty) = lower_type(&pair[1]) else {
            errors.push(invalid_type_diagnostic(
                file,
                &pair[1],
                "MalformedCImport",
                "invalid C import parameter type",
                None,
                None,
            ));
            continue;
        };
        params.push(Param {
            name: name.to_string(),
            name_span: pair[0].span,
            ty,
            ty_span: pair[1].span,
        });
    }

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

fn is_c_symbol_name(value: &str) -> bool {
    let mut chars = value.chars();
    let Some(first) = chars.next() else {
        return false;
    };
    (first == '_' || first.is_ascii_alphabetic())
        && chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}

fn lower_test(
    file: &str,
    form: &SExpr,
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Test, Vec<Diagnostic>> {
    let mut errors = Vec::new();
    let Some(items) = expect_list(form) else {
        return Err(vec![Diagnostic::new(
            file,
            "ExpectedList",
            "expected test list",
        )
        .with_span(form.span)]);
    };

    if items.len() < 3 {
        return Err(vec![Diagnostic::new(
            file,
            "MalformedTestForm",
            "test form must be `(test \"name\" body...)`",
        )
        .with_span(form.span)
        .hint("use one string name followed by a bool result expression")]);
    }

    let name = match expect_string(&items[1]) {
        Some(name) if valid_test_name(name) => name.to_string(),
        Some(_) => {
            errors.push(
                Diagnostic::new(
                    file,
                    "InvalidTestName",
                    "test name must be non-empty printable ASCII without quotes, backslashes, or newlines",
                )
                    .with_span(items[1].span)
                    .expected("non-empty printable ASCII without quotes, backslashes, or newlines"),
            );
            "<error>".to_string()
        }
        None => {
            errors.push(
                Diagnostic::new(
                    file,
                    "InvalidTestName",
                    "test name must be a string literal",
                )
                .with_span(items[1].span)
                .expected("string"),
            );
            "<error>".to_string()
        }
    };

    let mut body = Vec::new();
    for item in &items[2..] {
        match lower_expr(file, item, struct_names, enum_names) {
            Ok(expr) => body.push(expr),
            Err(err) => {
                errors.push(err);
            }
        }
    }

    if body.len() > 1 {
        for expr in &body[..body.len() - 1] {
            if !matches!(
                expr.kind,
                ExprKind::Local { .. } | ExprKind::Set { .. } | ExprKind::While { .. }
            ) {
                errors.push(
                    Diagnostic::new(
                        file,
                        "MalformedTestForm",
                        "test body forms before the final expression must be local declarations, assignments, or while loops",
                    )
                    .with_span(expr.span)
                    .hint("use `(let name i32 expr)`, `(var name i32 expr)`, `(set name expr)`, or `(while condition body...)` before the final bool expression"),
                );
            }
        }
    }

    if errors.is_empty() {
        Ok(Test {
            name,
            name_span: items[1].span,
            body,
            span: form.span,
        })
    } else {
        Err(errors)
    }
}

fn lower_params(file: &str, form: &SExpr) -> Result<Vec<Param>, Vec<Diagnostic>> {
    let mut errors = Vec::new();
    let Some(items) = expect_list(form) else {
        return Err(vec![Diagnostic::new(
            file,
            "InvalidParams",
            "parameters must be a list",
        )
        .with_span(form.span)]);
    };

    let mut params = Vec::new();

    for item in items {
        let Some(pair) = expect_list(item) else {
            errors.push(
                Diagnostic::new(file, "InvalidParam", "parameter must be `(name Type)`")
                    .with_span(item.span),
            );
            continue;
        };

        if pair.len() != 2 {
            errors.push(
                Diagnostic::new(file, "InvalidParam", "parameter must be `(name Type)`")
                    .with_span(item.span),
            );
            continue;
        }

        let Some(name) = expect_ident(&pair[0]) else {
            errors.push(
                Diagnostic::new(
                    file,
                    "InvalidParamName",
                    "parameter name must be an identifier",
                )
                .with_span(pair[0].span),
            );
            continue;
        };

        let Some(ty) = lower_type(&pair[1]) else {
            errors.push(invalid_type_diagnostic(
                file,
                &pair[1],
                "InvalidParamType",
                "invalid parameter type",
                None,
                None,
            ));
            continue;
        };

        if ty == Type::Unit {
            errors.push(unsupported_unit_parameter_signature(file, pair[1].span));
            continue;
        }

        params.push(Param {
            name: name.to_string(),
            name_span: pair[0].span,
            ty,
            ty_span: pair[1].span,
        });
    }

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

fn unsupported_unit_parameter_signature(file: &str, span: crate::token::Span) -> Diagnostic {
    Diagnostic::new(
        file,
        "UnsupportedUnitSignatureType",
        "function parameter type `unit` is unsupported",
    )
    .with_span(span)
    .expected("non-unit function parameter type")
    .found(Type::Unit.to_string())
    .hint("`unit` is reserved for compiler/runtime unit-producing forms")
}

fn unsupported_unit_return_signature(file: &str, span: crate::token::Span) -> Diagnostic {
    Diagnostic::new(
        file,
        "UnsupportedUnitSignatureType",
        "function return type `unit` is unsupported",
    )
    .with_span(span)
    .expected("non-unit function return type")
    .found(Type::Unit.to_string())
    .hint("`unit` is reserved for compiler/runtime unit-producing forms")
}

fn is_reserved_type_name(name: &str) -> bool {
    matches!(
        name,
        "i32"
            | "i64"
            | "u32"
            | "u64"
            | "f64"
            | "bool"
            | "unit"
            | "string"
            | "ptr"
            | "slice"
            | "option"
            | "result"
            | "array"
            | "vec"
            | "map"
            | "set"
    )
}

fn lower_type(form: &SExpr) -> Option<Type> {
    match &form.kind {
        SExprKind::Atom(Atom::Ident(name)) => match name.as_str() {
            "i32" => Some(Type::I32),
            "i64" => Some(Type::I64),
            "u32" => Some(Type::U32),
            "u64" => Some(Type::U64),
            "f64" => Some(Type::F64),
            "bool" => Some(Type::Bool),
            "unit" => Some(Type::Unit),
            "string" => Some(Type::String),
            other => Some(Type::Named(other.to_string())),
        },
        SExprKind::List(items) if !items.is_empty() => {
            let head = expect_ident(&items[0])?;
            match head {
                "ptr" if items.len() == 2 => Some(Type::Ptr(Box::new(lower_type(&items[1])?))),
                "slice" if items.len() == 2 => Some(Type::Slice(Box::new(lower_type(&items[1])?))),
                "option" if items.len() == 2 => {
                    Some(Type::Option(Box::new(lower_type(&items[1])?)))
                }
                "result" if items.len() == 3 => Some(Type::Result(
                    Box::new(lower_type(&items[1])?),
                    Box::new(lower_type(&items[2])?),
                )),
                "array" if items.len() == 3 => {
                    let inner = lower_type(&items[1])?;
                    let n = match items[2].kind {
                        SExprKind::Atom(Atom::Int(n)) if n >= 0 => n as usize,
                        _ => return None,
                    };
                    Some(Type::Array(Box::new(inner), n))
                }
                "vec" if items.len() == 2 => Some(Type::Vec(Box::new(lower_type(&items[1])?))),
                _ => None,
            }
        }
        _ => None,
    }
}

fn lower_expr(
    file: &str,
    form: &SExpr,
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    match &form.kind {
        SExprKind::Atom(Atom::Int(value)) => {
            let value = i32::try_from(*value).map_err(|_| {
                Diagnostic::new(
                    file,
                    "IntegerOutOfRange",
                    "integer literal is outside the supported i32 range",
                )
                .with_span(form.span)
                .expected("i32")
                .found(value.to_string())
            })?;

            Ok(Expr {
                kind: ExprKind::Int(value),
                span: form.span,
            })
        }
        SExprKind::Atom(Atom::I64(value)) => Ok(Expr {
            kind: ExprKind::Int64(*value),
            span: form.span,
        }),
        SExprKind::Atom(Atom::U32(value)) => Ok(Expr {
            kind: ExprKind::UInt32(*value),
            span: form.span,
        }),
        SExprKind::Atom(Atom::U64(value)) => Ok(Expr {
            kind: ExprKind::UInt64(*value),
            span: form.span,
        }),
        SExprKind::Atom(Atom::String(value)) => Ok(Expr {
            kind: ExprKind::String(value.clone()),
            span: form.span,
        }),
        SExprKind::Atom(Atom::Float(value)) => {
            if !value.is_finite() {
                return Err(Diagnostic::new(
                    file,
                    "UnsupportedFloatLiteral",
                    "f64 literals must be finite in exp-20",
                )
                .with_span(form.span)
                .expected("finite f64 literal")
                .found(value.to_string())
                .hint("NaN and infinity semantics remain deferred"));
            }

            Ok(Expr {
                kind: ExprKind::Float(*value),
                span: form.span,
            })
        }
        SExprKind::Atom(Atom::Ident(name)) if name == "true" => Ok(Expr {
            kind: ExprKind::Bool(true),
            span: form.span,
        }),
        SExprKind::Atom(Atom::Ident(name)) if name == "false" => Ok(Expr {
            kind: ExprKind::Bool(false),
            span: form.span,
        }),
        SExprKind::Atom(Atom::Ident(name)) => Ok(Expr {
            kind: ExprKind::Var(name.clone()),
            span: form.span,
        }),
        SExprKind::List(items) if items.is_empty() => {
            Err(Diagnostic::new(file, "EmptyForm", "empty form").with_span(form.span))
        }
        SExprKind::List(items) => {
            let Some(head) = expect_ident(&items[0]) else {
                return Err(Diagnostic::new(
                    file,
                    "InvalidCall",
                    "form head must be an identifier",
                )
                .with_span(items[0].span));
            };

            match head {
                "+" | "-" | "*" | "/" | "%" | "bit_and" | "bit_or" | "bit_xor" | "=" | "<"
                | ">" | "<=" | ">=" => {
                    if items.len() != 3 {
                        return Err(Diagnostic::new(
                            file,
                            "InvalidBinary",
                            "binary operator expects exactly two operands",
                        )
                        .with_span(form.span));
                    }

                    let op = match head {
                        "+" => BinaryOp::Add,
                        "-" => BinaryOp::Sub,
                        "*" => BinaryOp::Mul,
                        "/" => BinaryOp::Div,
                        "%" => BinaryOp::Rem,
                        "bit_and" => BinaryOp::BitAnd,
                        "bit_or" => BinaryOp::BitOr,
                        "bit_xor" => BinaryOp::BitXor,
                        "=" => BinaryOp::Eq,
                        "<" => BinaryOp::Lt,
                        ">" => BinaryOp::Gt,
                        "<=" => BinaryOp::Le,
                        ">=" => BinaryOp::Ge,
                        _ => unreachable!(),
                    };

                    Ok(Expr {
                        kind: ExprKind::Binary {
                            op,
                            left: Box::new(lower_expr(file, &items[1], struct_names, enum_names)?),
                            right: Box::new(lower_expr(file, &items[2], struct_names, enum_names)?),
                        },
                        span: form.span,
                    })
                }
                "and" | "or" => {
                    if items.len() != 3 {
                        return Err(Diagnostic::new(
                            file,
                            "InvalidLogical",
                            "logical operator expects exactly two operands",
                        )
                        .with_span(form.span));
                    }

                    let left = lower_expr(file, &items[1], struct_names, enum_names)?;
                    let right = lower_expr(file, &items[2], struct_names, enum_names)?;
                    let literal = Expr {
                        kind: ExprKind::Bool(head == "or"),
                        span: form.span,
                    };
                    let (then_expr, else_expr) = if head == "and" {
                        (right, literal)
                    } else {
                        (literal, right)
                    };

                    Ok(Expr {
                        kind: ExprKind::If {
                            condition: Box::new(left),
                            then_expr: Box::new(then_expr),
                            else_expr: Box::new(else_expr),
                        },
                        span: form.span,
                    })
                }
                "not" => {
                    if items.len() != 2 {
                        return Err(Diagnostic::new(
                            file,
                            "InvalidLogical",
                            "logical not expects exactly one operand",
                        )
                        .with_span(form.span));
                    }

                    Ok(Expr {
                        kind: ExprKind::If {
                            condition: Box::new(lower_expr(
                                file,
                                &items[1],
                                struct_names,
                                enum_names,
                            )?),
                            then_expr: Box::new(Expr {
                                kind: ExprKind::Bool(false),
                                span: form.span,
                            }),
                            else_expr: Box::new(Expr {
                                kind: ExprKind::Bool(true),
                                span: form.span,
                            }),
                        },
                        span: form.span,
                    })
                }
                "." => lower_field_access(file, form, items, struct_names, enum_names),
                "array" => lower_array_init(file, form, items, struct_names, enum_names),
                "some" => lower_option_some(file, form, items, struct_names, enum_names),
                "none" => lower_option_none(file, form, items),
                "ok" => lower_result_ok(file, form, items, struct_names, enum_names),
                "err" => lower_result_err(file, form, items, struct_names, enum_names),
                "is_some" | "is_none" | "is_ok" | "is_err" | "std.result.is_ok"
                | "std.result.is_err" => {
                    lower_unary_observer(file, form, items, struct_names, enum_names, head)
                }
                "unwrap_some"
                | "unwrap_ok"
                | "unwrap_err"
                | "std.result.unwrap_ok"
                | "std.result.unwrap_err" => {
                    lower_unary_payload_access(file, form, items, struct_names, enum_names, head)
                }
                "index" => lower_index(file, form, items, struct_names, enum_names),
                "if" => {
                    if items.len() != 4 {
                        return Err(Diagnostic::new(
                            file,
                            "MalformedIfForm",
                            "`if` expects condition, then expression, else expression",
                        )
                        .with_span(form.span)
                        .hint("use `(if condition then-expression else-expression)`"));
                    }

                    Ok(Expr {
                        kind: ExprKind::If {
                            condition: Box::new(lower_expr(
                                file,
                                &items[1],
                                struct_names,
                                enum_names,
                            )?),
                            then_expr: Box::new(lower_expr(
                                file,
                                &items[2],
                                struct_names,
                                enum_names,
                            )?),
                            else_expr: Box::new(lower_expr(
                                file,
                                &items[3],
                                struct_names,
                                enum_names,
                            )?),
                        },
                        span: form.span,
                    })
                }
                "match" => lower_match(file, form, items, struct_names, enum_names),
                "let" | "var" => {
                    lower_local(file, form, items, head == "var", struct_names, enum_names)
                }
                "set" => lower_set(file, form, items, struct_names, enum_names),
                "while" => lower_while(file, form, items, struct_names, enum_names),
                "unsafe" => lower_unsafe(file, form, items, struct_names, enum_names),
                name if struct_names.contains(name) => {
                    lower_struct_init(file, form, items, name, struct_names, enum_names)
                }
                name if qualified_enum_name(name)
                    .map(|(enum_name, _)| {
                        enum_names.contains(enum_name) || starts_uppercase(enum_name)
                    })
                    .unwrap_or(false) =>
                {
                    let (enum_name, variant) = qualified_enum_name(name).expect("qualified enum");
                    let mut args = Vec::new();
                    for item in &items[1..] {
                        args.push(lower_expr(file, item, struct_names, enum_names)?);
                    }
                    Ok(Expr {
                        kind: ExprKind::EnumVariant {
                            enum_name: enum_name.to_string(),
                            variant: variant.to_string(),
                            name_span: items[0].span,
                            args,
                        },
                        span: form.span,
                    })
                }
                name if is_unsupported_generic_standard_library_call(name) => Err(
                    unsupported_generic_standard_library_call(file, items[0].span, name),
                ),
                name => {
                    let mut args = Vec::new();
                    for item in &items[1..] {
                        args.push(lower_expr(file, item, struct_names, enum_names)?);
                    }
                    Ok(Expr {
                        kind: ExprKind::Call {
                            name: name.to_string(),
                            name_span: items[0].span,
                            args,
                        },
                        span: form.span,
                    })
                }
            }
        }
        _ => Err(
            Diagnostic::new(file, "InvalidExpression", "invalid expression").with_span(form.span),
        ),
    }
}

fn lower_match(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() < 2 {
        return Err(
            Diagnostic::new(file, "MalformedMatchPattern", "`match` expects a subject")
                .with_span(form.span)
                .expected("(match subject (pattern body...) (pattern body...))"),
        );
    }

    let subject = Box::new(lower_expr(file, &items[1], struct_names, enum_names)?);
    let mut arms = Vec::new();

    for item in &items[2..] {
        let Some(arm_items) = expect_list(item) else {
            return Err(Diagnostic::new(
                file,
                "MalformedMatchPattern",
                "match arm must be a list containing a pattern and body",
            )
            .with_span(item.span)
            .expected("((some payload) body...)"));
        };

        if arm_items.len() < 2 {
            return Err(Diagnostic::new(
                file,
                "MalformedMatchPattern",
                "match arm must contain a pattern and at least one body expression",
            )
            .with_span(item.span)
            .expected("((some payload) body...)"));
        }

        let pattern = lower_match_pattern(file, &arm_items[0])?;
        let mut body = Vec::new();
        for body_expr in &arm_items[1..] {
            body.push(lower_expr(file, body_expr, struct_names, enum_names)?);
        }

        arms.push(MatchArm {
            pattern,
            body,
            span: item.span,
        });
    }

    Ok(Expr {
        kind: ExprKind::Match { subject, arms },
        span: form.span,
    })
}

fn lower_match_pattern(file: &str, form: &SExpr) -> Result<MatchPattern, Diagnostic> {
    let Some(items) = expect_list(form) else {
        return Err(Diagnostic::new(
            file,
            "MalformedMatchPattern",
            "match arm pattern must be a list",
        )
        .with_span(form.span)
        .expected("(some binding), (none), (ok binding), or (err binding)"));
    };

    let Some(head) = items.first().and_then(expect_ident) else {
        return Err(Diagnostic::new(
            file,
            "MalformedMatchPattern",
            "match arm pattern head must be an identifier",
        )
        .with_span(form.span)
        .expected("(some binding), (none), (ok binding), or (err binding)"));
    };

    match head {
        "some" | "ok" | "err" => {
            if items.len() != 2 {
                return Err(Diagnostic::new(
                    file,
                    "MalformedMatchPattern",
                    format!("`{}` match pattern requires one payload binding", head),
                )
                .with_span(form.span)
                .expected(format!("({} binding)", head)));
            }

            let Some(binding) = expect_ident(&items[1]) else {
                return Err(Diagnostic::new(
                    file,
                    "MalformedMatchPattern",
                    "match payload binding must be an identifier",
                )
                .with_span(items[1].span)
                .expected("identifier"));
            };

            let kind = match head {
                "some" => MatchPatternKind::Some,
                "ok" => MatchPatternKind::Ok,
                "err" => MatchPatternKind::Err,
                _ => unreachable!("known payload match pattern"),
            };

            Ok(MatchPattern {
                kind,
                binding: Some(binding.to_string()),
                binding_span: Some(items[1].span),
                span: form.span,
            })
        }
        "none" => {
            if items.len() != 1 {
                return Err(Diagnostic::new(
                    file,
                    "MalformedMatchPattern",
                    "`none` match pattern does not take a payload binding",
                )
                .with_span(form.span)
                .expected("(none)"));
            }

            Ok(MatchPattern {
                kind: MatchPatternKind::None,
                binding: None,
                binding_span: None,
                span: form.span,
            })
        }
        name if qualified_enum_name(name).is_some() => {
            if items.len() > 2 {
                return Err(Diagnostic::new(
                    file,
                    "InvalidEnumMatchArm",
                    "enum match patterns support at most one payload binding",
                )
                .with_span(form.span)
                .expected("(Name.Variant) or (Name.Variant binding)"));
            }

            let (enum_name, variant) = qualified_enum_name(name).expect("qualified enum pattern");
            let binding = if items.len() == 2 {
                let Some(binding) = expect_ident(&items[1]) else {
                    return Err(Diagnostic::new(
                        file,
                        "MalformedMatchPattern",
                        "match payload binding must be an identifier",
                    )
                    .with_span(items[1].span)
                    .expected("identifier"));
                };
                Some(binding.to_string())
            } else {
                None
            };
            Ok(MatchPattern {
                kind: MatchPatternKind::EnumVariant {
                    enum_name: enum_name.to_string(),
                    variant: variant.to_string(),
                },
                binding,
                binding_span: items.get(1).map(|item| item.span),
                span: form.span,
            })
        }
        _ => Err(Diagnostic::new(
            file,
            "MalformedMatchPattern",
            format!("unsupported match pattern `{}`", head),
        )
        .with_span(form.span)
        .expected("(some binding), (none), (ok binding), or (err binding)")),
    }
}

fn qualified_enum_name(name: &str) -> Option<(&str, &str)> {
    let (enum_name, variant) = name.split_once('.')?;
    if enum_name.is_empty() || variant.is_empty() || variant.contains('.') {
        return None;
    }
    Some((enum_name, variant))
}

fn starts_uppercase(name: &str) -> bool {
    name.chars().next().is_some_and(char::is_uppercase)
}

fn lower_unary_observer(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
    name: &str,
) -> Result<Expr, Diagnostic> {
    if items.len() != 2 {
        return Err(Diagnostic::new(
            file,
            "MalformedObservationForm",
            format!("`{}` expects exactly one value", name),
        )
        .with_span(form.span)
        .hint(format!("use `({} value)`", name)));
    }

    let value = Box::new(lower_expr(file, &items[1], struct_names, enum_names)?);
    let kind = match name {
        "is_some" => ExprKind::OptionIsSome { value },
        "is_none" => ExprKind::OptionIsNone { value },
        "is_ok" | "std.result.is_ok" => ExprKind::ResultIsOk {
            source_name: name.to_string(),
            value,
        },
        "is_err" | "std.result.is_err" => ExprKind::ResultIsErr {
            source_name: name.to_string(),
            value,
        },
        _ => unreachable!("unknown observer"),
    };

    Ok(Expr {
        kind,
        span: form.span,
    })
}

fn lower_unary_payload_access(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
    name: &str,
) -> Result<Expr, Diagnostic> {
    if items.len() != 2 {
        return Err(Diagnostic::new(
            file,
            "MalformedUnwrapForm",
            format!("`{}` expects exactly one value", name),
        )
        .with_span(form.span)
        .hint(format!("use `({} value)`", name)));
    }

    let value = Box::new(lower_expr(file, &items[1], struct_names, enum_names)?);
    let kind = match name {
        "unwrap_some" => ExprKind::OptionUnwrapSome { value },
        "unwrap_ok" | "std.result.unwrap_ok" => ExprKind::ResultUnwrapOk {
            source_name: name.to_string(),
            value,
        },
        "unwrap_err" | "std.result.unwrap_err" => ExprKind::ResultUnwrapErr {
            source_name: name.to_string(),
            value,
        },
        _ => unreachable!("unknown payload access"),
    };

    Ok(Expr {
        kind,
        span: form.span,
    })
}

fn lower_unsafe(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() < 2 {
        return Err(Diagnostic::new(
            file,
            "MalformedUnsafeForm",
            "`unsafe` block must contain a final expression",
        )
        .with_span(form.span)
        .expected("(unsafe body-form... final-expression)")
        .hint(
            "use `(unsafe expression)` or add supported body forms before the final expression",
        ));
    }

    let mut body = Vec::new();
    for item in &items[1..] {
        body.push(lower_expr(file, item, struct_names, enum_names)?);
    }

    Ok(Expr {
        kind: ExprKind::Unsafe { body },
        span: form.span,
    })
}

fn lower_field_access(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() != 3 {
        return Err(Diagnostic::new(
            file,
            "MalformedFieldAccess",
            "field access must be `(. value field)`",
        )
        .with_span(form.span));
    }

    let Some(field) = expect_ident(&items[2]) else {
        return Err(
            Diagnostic::new(file, "InvalidFieldName", "field name must be an identifier")
                .with_span(items[2].span),
        );
    };

    Ok(Expr {
        kind: ExprKind::FieldAccess {
            value: Box::new(lower_expr(file, &items[1], struct_names, enum_names)?),
            field: field.to_string(),
            field_span: items[2].span,
        },
        span: form.span,
    })
}

fn lower_array_init(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() < 2 {
        return Err(Diagnostic::new(
            file,
            "MalformedArrayConstructor",
            "array constructor must be `(array TYPE value...)`",
        )
        .with_span(form.span)
        .hint(
            "use `(array i32 1 2 3)`, `(array bool true false)`, or `(array string \"a\" \"b\")`",
        ));
    }

    let Some(elem_ty) = lower_type(&items[1]) else {
        return Err(invalid_type_diagnostic(
            file,
            &items[1],
            "InvalidArrayElementType",
            "array constructor element type is invalid",
            None,
            Some("fixed arrays use direct scalar `i32`, `i64`, `f64`, `bool`, `string`, known enum, or known non-recursive struct elements"),
        ));
    };

    let mut elements = Vec::new();
    for item in &items[2..] {
        elements.push(lower_expr(file, item, struct_names, enum_names)?);
    }

    Ok(Expr {
        kind: ExprKind::ArrayInit {
            elem_ty,
            elem_ty_span: items[1].span,
            elements,
        },
        span: form.span,
    })
}

fn lower_option_some(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() != 3 {
        return Err(Diagnostic::new(
            file,
            "MalformedOptionConstructor",
            "`some` constructor must be `(some i32 value)`",
        )
        .with_span(form.span)
        .hint("use `(some i32 value)`"));
    }

    let Some(payload_ty) = lower_type(&items[1]) else {
        return Err(invalid_type_diagnostic(
            file,
            &items[1],
            "InvalidOptionPayloadType",
            "option constructor payload type is invalid",
            None,
            Some("first-pass options use `i32` payloads"),
        ));
    };

    Ok(Expr {
        kind: ExprKind::OptionSome {
            payload_ty,
            payload_ty_span: items[1].span,
            value: Box::new(lower_expr(file, &items[2], struct_names, enum_names)?),
        },
        span: form.span,
    })
}

fn lower_option_none(file: &str, form: &SExpr, items: &[SExpr]) -> Result<Expr, Diagnostic> {
    if items.len() != 2 {
        return Err(Diagnostic::new(
            file,
            "MalformedOptionConstructor",
            "`none` constructor must be `(none i32)`",
        )
        .with_span(form.span)
        .hint("use `(none i32)`"));
    }

    let Some(payload_ty) = lower_type(&items[1]) else {
        return Err(invalid_type_diagnostic(
            file,
            &items[1],
            "InvalidOptionPayloadType",
            "option constructor payload type is invalid",
            None,
            Some("first-pass options use `i32` payloads"),
        ));
    };

    Ok(Expr {
        kind: ExprKind::OptionNone {
            payload_ty,
            payload_ty_span: items[1].span,
        },
        span: form.span,
    })
}

fn lower_result_ok(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() != 4 {
        return Err(Diagnostic::new(
            file,
            "MalformedResultConstructor",
            "`ok` constructor must be `(ok i32 i32 value)`",
        )
        .with_span(form.span)
        .hint("use `(ok i32 i32 value)`"));
    }

    let Some(ok_ty) = lower_type(&items[1]) else {
        return Err(invalid_type_diagnostic(
            file,
            &items[1],
            "InvalidResultPayloadType",
            "result constructor ok type is invalid",
            None,
            Some("first-pass results use `i32` payloads"),
        ));
    };
    let Some(err_ty) = lower_type(&items[2]) else {
        return Err(invalid_type_diagnostic(
            file,
            &items[2],
            "InvalidResultPayloadType",
            "result constructor err type is invalid",
            None,
            Some("first-pass results use `i32` payloads"),
        ));
    };

    Ok(Expr {
        kind: ExprKind::ResultOk {
            ok_ty,
            ok_ty_span: items[1].span,
            err_ty,
            err_ty_span: items[2].span,
            value: Box::new(lower_expr(file, &items[3], struct_names, enum_names)?),
        },
        span: form.span,
    })
}

fn lower_result_err(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() != 4 {
        return Err(Diagnostic::new(
            file,
            "MalformedResultConstructor",
            "`err` constructor must be `(err i32 i32 value)`",
        )
        .with_span(form.span)
        .hint("use `(err i32 i32 value)`"));
    }

    let Some(ok_ty) = lower_type(&items[1]) else {
        return Err(invalid_type_diagnostic(
            file,
            &items[1],
            "InvalidResultPayloadType",
            "result constructor ok type is invalid",
            None,
            Some("first-pass results use `i32` payloads"),
        ));
    };
    let Some(err_ty) = lower_type(&items[2]) else {
        return Err(invalid_type_diagnostic(
            file,
            &items[2],
            "InvalidResultPayloadType",
            "result constructor err type is invalid",
            None,
            Some("first-pass results use `i32` payloads"),
        ));
    };

    Ok(Expr {
        kind: ExprKind::ResultErr {
            ok_ty,
            ok_ty_span: items[1].span,
            err_ty,
            err_ty_span: items[2].span,
            value: Box::new(lower_expr(file, &items[3], struct_names, enum_names)?),
        },
        span: form.span,
    })
}

fn lower_index(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() != 3 {
        return Err(Diagnostic::new(
            file,
            "MalformedIndex",
            "index expression must be `(index array-expr index-expr)`",
        )
        .with_span(form.span)
        .hint("use `(index values 0)`"));
    }

    Ok(Expr {
        kind: ExprKind::Index {
            array: Box::new(lower_expr(file, &items[1], struct_names, enum_names)?),
            index: Box::new(lower_expr(file, &items[2], struct_names, enum_names)?),
        },
        span: form.span,
    })
}

fn lower_struct_init(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    name: &str,
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    let mut fields = Vec::new();

    for item in &items[1..] {
        let Some(pair) = expect_list(item) else {
            return Err(Diagnostic::new(
                file,
                "MalformedStructConstructor",
                "struct constructor fields must be `(field value)`",
            )
            .with_span(item.span));
        };

        if pair.len() != 2 {
            return Err(Diagnostic::new(
                file,
                "MalformedStructConstructor",
                "struct constructor fields must be `(field value)`",
            )
            .with_span(item.span));
        }

        let Some(field_name) = expect_ident(&pair[0]) else {
            return Err(Diagnostic::new(
                file,
                "InvalidStructConstructorField",
                "struct constructor field name must be an identifier",
            )
            .with_span(pair[0].span));
        };

        fields.push(StructInitField {
            name: field_name.to_string(),
            name_span: pair[0].span,
            expr: lower_expr(file, &pair[1], struct_names, enum_names)?,
        });
    }

    Ok(Expr {
        kind: ExprKind::StructInit {
            name: name.to_string(),
            name_span: items[0].span,
            fields,
        },
        span: form.span,
    })
}

fn lower_local(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    mutable: bool,
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    let keyword = if mutable { "var" } else { "let" };
    if items.len() != 4 {
        return Err(Diagnostic::new(
            file,
            "InvalidLocalDeclaration",
            format!("`{}` must be `({} name i32 expr)`", keyword, keyword),
        )
        .with_span(form.span));
    }

    let Some(name) = expect_ident(&items[1]) else {
        return Err(
            Diagnostic::new(file, "InvalidLocalName", "local name must be an identifier")
                .with_span(items[1].span),
        );
    };

    let Some(ty) = lower_type(&items[2]) else {
        return Err(invalid_type_diagnostic(
            file,
            &items[2],
            "InvalidLocalType",
            "invalid local type",
            None,
            None,
        ));
    };

    Ok(Expr {
        kind: ExprKind::Local {
            mutable,
            name: name.to_string(),
            name_span: items[1].span,
            ty,
            expr: Box::new(lower_expr(file, &items[3], struct_names, enum_names)?),
        },
        span: form.span,
    })
}

fn lower_set(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() != 3 {
        return Err(
            Diagnostic::new(file, "InvalidSet", "`set` must be `(set name expr)`")
                .with_span(form.span),
        );
    }

    let Some(name) = expect_ident(&items[1]) else {
        return Err(Diagnostic::new(
            file,
            "InvalidSetTarget",
            "`set` target must be an identifier",
        )
        .with_span(items[1].span));
    };

    Ok(Expr {
        kind: ExprKind::Set {
            name: name.to_string(),
            name_span: items[1].span,
            expr: Box::new(lower_expr(file, &items[2], struct_names, enum_names)?),
        },
        span: form.span,
    })
}

fn lower_while(
    file: &str,
    form: &SExpr,
    items: &[SExpr],
    struct_names: &HashSet<String>,
    enum_names: &HashSet<String>,
) -> Result<Expr, Diagnostic> {
    if items.len() < 2 {
        return Err(
            Diagnostic::new(file, "MalformedWhileForm", "`while` must have a condition")
                .with_span(form.span)
                .hint("use `(while condition body...)`"),
        );
    }

    if items.len() < 3 {
        return Err(Diagnostic::new(
            file,
            "EmptyWhileBody",
            "`while` must have at least one body form",
        )
        .with_span(form.span)
        .hint("provide at least one unit-producing loop body form"));
    }

    let mut body = Vec::new();
    for item in &items[2..] {
        body.push(lower_expr(file, item, struct_names, enum_names)?);
    }

    Ok(Expr {
        kind: ExprKind::While {
            condition: Box::new(lower_expr(file, &items[1], struct_names, enum_names)?),
            body,
        },
        span: form.span,
    })
}

fn invalid_type_diagnostic(
    file: &str,
    form: &SExpr,
    fallback_code: &'static str,
    fallback_message: impl Into<String>,
    fallback_expected: Option<&str>,
    fallback_hint: Option<&str>,
) -> Diagnostic {
    if let Some(diagnostic) = unsupported_reserved_type_diagnostic(file, form) {
        return diagnostic;
    }

    let mut diagnostic =
        Diagnostic::new(file, fallback_code, fallback_message).with_span(form.span);
    if let Some(expected) = fallback_expected {
        diagnostic = diagnostic.expected(expected);
    }
    if let Some(hint) = fallback_hint {
        diagnostic = diagnostic.hint(hint);
    }
    diagnostic
}

fn list_head(form: &SExpr) -> Option<&str> {
    let items = expect_list(form)?;
    let first = items.first()?;
    expect_ident(first)
}

fn expect_list(form: &SExpr) -> Option<&[SExpr]> {
    match &form.kind {
        SExprKind::List(items) => Some(items),
        _ => None,
    }
}

fn expect_ident(form: &SExpr) -> Option<&str> {
    match &form.kind {
        SExprKind::Atom(Atom::Ident(name)) => Some(name.as_str()),
        _ => None,
    }
}

fn expect_string(form: &SExpr) -> Option<&str> {
    match &form.kind {
        SExprKind::Atom(Atom::String(value)) => Some(value.as_str()),
        _ => None,
    }
}

fn valid_test_name(name: &str) -> bool {
    !name.is_empty()
        && name
            .bytes()
            .all(|byte| matches!(byte, 0x20..=0x7e) && byte != b'"' && byte != b'\\')
}
