inauguration 0.2.0

.in language and general compiler CLI (Core IR, hybrid SIL, staging, plugins)
Documentation
use crate::boundary_ir::{
    BoundaryField, BoundaryLayout, BoundaryModule, BoundaryOwnership, BoundaryRepr, BoundarySymbol,
    BoundaryTransfer, CompileArtifact, IN_ABI_VERSION,
};
use crate::boundary_verify::boundary_ir_verify;
use crate::compiler::tree_front;
use crate::core_ir::{Decl, Typ, UnifiedModule};
use crate::parser_registry::ParserId;
use std::path::Path;

pub fn parse_ecmascript_artifact(
    parser_id: ParserId,
    path: &Path,
) -> Result<CompileArtifact, String> {
    let src = std::fs::read_to_string(path).map_err(|e| format!("read {}: {e}", path.display()))?;
    parse_ecmascript_artifact_source(parser_id, path, &src)
}

pub fn parse_ecmascript_artifact_source(
    parser_id: ParserId,
    path: &Path,
    src: &str,
) -> Result<CompileArtifact, String> {
    let semantic = tree_front::parse_polyglot_file(parser_id, path)?;
    let module_id = path
        .file_stem()
        .and_then(|s| s.to_str())
        .unwrap_or(parser_id.as_str());
    let boundary = extract_ecmascript_boundary(src)
        .or_else(|| boundary_from_semantic(parser_id, module_id, &semantic));
    Ok(if let Some(boundary) = boundary {
        CompileArtifact::with_boundary(semantic, boundary)
    } else {
        CompileArtifact::from_semantic(semantic)
    })
}

pub fn extract_ecmascript_boundary(src: &str) -> Option<BoundaryModule> {
    if let Some(line) = src.lines().next() {
        let trimmed = line.trim();
        let payload = trimmed
            .strip_prefix("//? in_boundary")
            .or_else(|| trimmed.strip_prefix("// in_boundary"))?;
        let module: BoundaryModule = serde_json::from_str(payload.trim()).ok()?;
        return Some(if module.layout_hash.is_empty() {
            module.with_layout_hash()
        } else {
            module
        });
    }
    None
}

fn boundary_from_semantic(
    parser_id: ParserId,
    module_id: &str,
    semantic: &UnifiedModule,
) -> Option<BoundaryModule> {
    let mut layouts = Vec::new();
    let mut symbols = Vec::new();

    for decl in &semantic.decls {
        collect_boundary_decl(decl, &mut layouts, &mut symbols);
    }

    if layouts.is_empty() && symbols.is_empty() {
        return None;
    }

    let boundary = BoundaryModule {
        abi_version: IN_ABI_VERSION,
        module: format!("{}.{module_id}", parser_id.as_str()),
        layouts,
        symbols,
        allocators: vec![],
        layout_hash: String::new(),
    }
    .with_layout_hash();
    let report = boundary_ir_verify(&boundary);
    if !report.ok {
        return None;
    }
    Some(boundary)
}

fn collect_boundary_decl(
    decl: &Decl,
    layouts: &mut Vec<BoundaryLayout>,
    symbols: &mut Vec<BoundarySymbol>,
) {
    match decl {
        Decl::Class {
            name,
            fields,
            methods,
            ..
        } => {
            if let Some(layout) = class_layout(name, fields) {
                layouts.push(layout);
            }
            for method in methods {
                collect_boundary_decl(method, layouts, symbols);
            }
        }
        Decl::Function {
            name, params, ret, ..
        } => symbols.push(function_symbol(name, params, ret)),
        _ => {}
    }
}

fn class_layout(name: &str, fields: &[(String, Typ)]) -> Option<BoundaryLayout> {
    if name.is_empty() || fields.is_empty() {
        return None;
    }
    let mut offset = 0u64;
    let mut out = Vec::new();
    for (field_name, typ) in fields {
        let abi_type = abi_type_name(typ);
        out.push(BoundaryField {
            name: field_name.clone(),
            offset,
            typ: abi_type.to_string(),
            transfer: Some(BoundaryTransfer::Copy),
        });
        offset = offset.saturating_add(size_hint(abi_type));
    }
    let size = offset.max(8);
    Some(BoundaryLayout {
        name: name.to_string(),
        kind: "struct".to_string(),
        repr: Some(BoundaryRepr::C),
        size,
        align: 8,
        stride: size,
        fields: out,
    })
}

fn function_symbol(name: &str, params: &[(String, Typ)], ret: &Typ) -> BoundarySymbol {
    let mut signature = String::new();
    signature.push_str(name);
    signature.push('(');
    for (idx, (_, typ)) in params.iter().enumerate() {
        if idx > 0 {
            signature.push(',');
        }
        signature.push_str(abi_type_name(typ));
    }
    signature.push_str(")->");
    signature.push_str(abi_type_name(ret));
    BoundarySymbol {
        name: name.to_string(),
        signature_hash: format!("blake3-{}", blake3::hash(signature.as_bytes()).to_hex()),
        ownership: BoundaryOwnership::ReturnsOwnedHandle,
        calling_convention: "c".to_string(),
    }
}

fn abi_type_name(typ: &Typ) -> &'static str {
    match typ {
        Typ::Bool => "bool",
        Typ::Float => "f64",
        Typ::String => "InSliceU8",
        Typ::Void => "void",
        Typ::Int => "i64",
        Typ::Named(name) if name == "number" => "i64",
        Typ::Named(name) if name == "boolean" => "bool",
        Typ::Named(name) if name == "string" => "InSliceU8",
        Typ::Named(name) if name == "void" => "void",
        _ => "i64",
    }
}

fn size_hint(typ: &str) -> u64 {
    match typ {
        "bool" => 1,
        "f64" | "i64" | "InSliceU8" => 8,
        _ => 8,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn extracts_inline_boundary_json() {
        let src = r#"//? in_boundary {"abi_version":1,"module":"sample.js","layouts":[{"name":"Point","kind":"struct","repr":"c","size":8,"align":4,"stride":8,"fields":[{"name":"x","offset":0,"type":"i32","transfer":"copy"},{"name":"y","offset":4,"type":"i32","transfer":"copy"}]}],"symbols":[{"name":"point_new","signature_hash":"point_new_v1","ownership":"returns-owned-handle","calling_convention":"c"}]}
function main() {}
"#;
        let boundary = extract_ecmascript_boundary(src).expect("boundary");
        assert_eq!(boundary.module, "sample.js");
        assert_eq!(boundary.layouts.len(), 1);
        assert_eq!(boundary.symbols[0].name, "point_new");
        assert!(boundary.layout_hash.starts_with("blake3-"));
    }
}