inauguration 0.2.0

.in language and general compiler CLI (Core IR, hybrid SIL, staging, plugins)
Documentation
use crate::boundary_ir::{
    BoundaryLayout, BoundaryModule, BoundaryOwnership, BoundaryRepr, BoundaryTransfer,
    IN_ABI_VERSION,
};

const MANAGED_ABI_TYPES: &[&str] = &["Vec", "String", "Array", "seq", "string", "InBufU8"];

pub struct BoundaryVerifyReport {
    pub ok: bool,
    pub diagnostics: Vec<String>,
}

pub fn boundary_ir_verify(module: &BoundaryModule) -> BoundaryVerifyReport {
    let mut diagnostics = Vec::new();

    if module.abi_version != IN_ABI_VERSION {
        diagnostics.push(format!(
            "abi_version mismatch: expected {IN_ABI_VERSION}, got {}",
            module.abi_version
        ));
    }

    if module.module.is_empty() {
        diagnostics.push("module id is empty".to_string());
    }

    for layout in &module.layouts {
        verify_layout(layout, &mut diagnostics);
    }

    for symbol in &module.symbols {
        if symbol.name.is_empty() {
            diagnostics.push("symbol name is empty".to_string());
        }
        if symbol.signature_hash.is_empty() {
            diagnostics.push(format!("symbol `{}` missing signature_hash", symbol.name));
        }
        if matches!(symbol.ownership, BoundaryOwnership::Borrowed)
            && symbol.calling_convention != "c"
        {
            diagnostics.push(format!(
                "symbol `{}` borrowed ownership requires c calling convention",
                symbol.name
            ));
        }
    }

    if !module.layout_hash.is_empty() {
        let expected = module.compute_layout_hash();
        if module.layout_hash != expected {
            diagnostics.push("layout_hash does not match canonical layouts+symbols".to_string());
        }
    }

    BoundaryVerifyReport {
        ok: diagnostics.is_empty(),
        diagnostics,
    }
}

fn verify_layout(layout: &BoundaryLayout, diagnostics: &mut Vec<String>) {
    if layout.name.is_empty() {
        diagnostics.push("layout name is empty".to_string());
        return;
    }

    if layout.kind != "struct" && layout.kind != "enum" {
        diagnostics.push(format!(
            "layout `{}` unsupported kind `{}`",
            layout.name, layout.kind
        ));
    }

    if layout.size == 0 {
        diagnostics.push(format!("layout `{}` size must be non-zero", layout.name));
    }

    if layout.align == 0 || !layout.align.is_power_of_two() {
        diagnostics.push(format!(
            "layout `{}` align must be a power of two",
            layout.name
        ));
    }

    if layout.stride < layout.size {
        diagnostics.push(format!(
            "layout `{}` stride {} < size {}",
            layout.name, layout.stride, layout.size
        ));
    }

    if let Some(repr) = &layout.repr
        && matches!(repr, BoundaryRepr::Packed)
        && layout.align > 1
    {
        diagnostics.push(format!(
            "layout `{}` packed repr with align > 1",
            layout.name
        ));
    }

    let mut prev_end = 0u64;
    for field in &layout.fields {
        if field.offset < prev_end && prev_end > 0 {
            diagnostics.push(format!(
                "layout `{}` field `{}` overlaps prior field",
                layout.name, field.name
            ));
        }
        if is_managed_abi_type(&field.typ) {
            diagnostics.push(format!(
                "layout `{}` field `{}` uses managed type `{}` across ABI",
                layout.name, field.name, field.typ
            ));
        }
        if let Some(transfer) = &field.transfer
            && matches!(transfer, BoundaryTransfer::Borrow)
            && field.typ != "InSliceU8"
        {
            diagnostics.push(format!(
                "layout `{}` field `{}` borrow transfer requires InSliceU8",
                layout.name, field.name
            ));
        }
        prev_end = field.offset.saturating_add(size_hint_for_type(&field.typ));
    }
}

fn is_managed_abi_type(typ: &str) -> bool {
    let base = typ.split('<').next().unwrap_or(typ).trim();
    MANAGED_ABI_TYPES
        .iter()
        .any(|m| base.eq_ignore_ascii_case(m))
}

fn size_hint_for_type(typ: &str) -> u64 {
    match typ {
        "u8" | "i8" | "bool" => 1,
        "u16" | "i16" => 2,
        "u32" | "i32" | "float" => 4,
        "u64" | "i64" | "f64" | "InSliceU8" | "InBufU8" | "InBorrowToken" | "InArenaHandle" => 8,
        _ => 8,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::boundary_ir::{
        BoundaryField, BoundaryLayout, BoundaryOwnership, BoundaryRepr, BoundarySymbol,
    };

    fn sample_layout() -> BoundaryLayout {
        BoundaryLayout {
            name: "Person".to_string(),
            kind: "struct".to_string(),
            repr: Some(BoundaryRepr::C),
            size: 24,
            align: 8,
            stride: 24,
            fields: vec![
                BoundaryField {
                    name: "name".to_string(),
                    offset: 0,
                    typ: "InSliceU8".to_string(),
                    transfer: Some(BoundaryTransfer::Borrow),
                },
                BoundaryField {
                    name: "age".to_string(),
                    offset: 16,
                    typ: "u32".to_string(),
                    transfer: Some(BoundaryTransfer::Copy),
                },
            ],
        }
    }

    #[test]
    fn verify_accepts_valid_boundary_module() {
        let module = BoundaryModule {
            abi_version: IN_ABI_VERSION,
            module: "pkg.person".to_string(),
            layouts: vec![sample_layout()],
            symbols: vec![BoundarySymbol {
                name: "person_new".to_string(),
                signature_hash: "abc123".to_string(),
                ownership: BoundaryOwnership::ReturnsOwnedHandle,
                calling_convention: "c".to_string(),
            }],
            allocators: vec![],
            layout_hash: String::new(),
        }
        .with_layout_hash();
        let report = boundary_ir_verify(&module);
        assert!(report.ok, "{:?}", report.diagnostics);
    }

    #[test]
    fn verify_rejects_managed_container_field() {
        let mut layout = sample_layout();
        layout.fields.push(BoundaryField {
            name: "tags".to_string(),
            offset: 20,
            typ: "Vec".to_string(),
            transfer: None,
        });
        let module = BoundaryModule {
            abi_version: IN_ABI_VERSION,
            module: "pkg.person".to_string(),
            layouts: vec![layout],
            symbols: vec![],
            allocators: vec![],
            layout_hash: String::new(),
        };
        let report = boundary_ir_verify(&module);
        assert!(!report.ok);
        assert!(
            report
                .diagnostics
                .iter()
                .any(|d| d.contains("managed type"))
        );
    }

    #[test]
    fn verify_rejects_stale_layout_hash() {
        let module = BoundaryModule {
            abi_version: IN_ABI_VERSION,
            module: "pkg.person".to_string(),
            layouts: vec![sample_layout()],
            symbols: vec![],
            allocators: vec![],
            layout_hash: "blake3-deadbeef".to_string(),
        };
        let report = boundary_ir_verify(&module);
        assert!(!report.ok);
    }
}