llzk-sys 30.0.0

Rust bindings to the LLZK C API.
//! llzk-sys's build script.
//!
//! Performs the following sequence of steps:
//! - Build and link the vendored version of `llzk-lib`
//! - Generate the Rust bindings for LLZK's CAPI using [`bindgen`].
//! - Build and link the static functions defined in LLZK's CAPI.

use anyhow::{Context, Result};
use llzk_sys_build_support::{
    config_traits::{bindgen::BindgenConfig as _, cc::CCConfig as _},
    default::DefaultConfig,
    link_llzk,
    wrap_static_fns::WrapStaticFns,
};
use std::{
    env, fs,
    path::{Path, PathBuf},
};

const LLZK_MAJOR_VERSION: u8 = 1;

/// Default configuration of the build process.
///
/// Exported names on LLZK's CAPI are usually prefixed with `Llzk` or `llzk`. However, some names
/// in the CAPI are prefixed with `Mlir` or `mlir`. These include:
///
/// - LLZK's passes since its CAPI is generated with tablegen and that's the prefix used by the
///   tool.
/// - LLZK's functions for dialect handles and pass registration. These are also generated by
///   tablegen.
/// - Helper types whose corresponding C++ type is a MLIR type.
fn create_default_cfg() -> DefaultConfig<'static> {
    let mut passes = vec![
        "ArrayToScalar",
        "InlineIncludes",
        "Flattening",
        "RedundantOperationElimination",
        "RedundantReadAndWriteElimination",
        "UnusedDeclarationElimination",
        "MemberWriteValidator",
    ];
    let pcl_enabled = feature_is_enabled("pcl-backend");
    if pcl_enabled {
        passes.push("PCLLowering");
    }

    DefaultConfig::new(
        pcl_enabled,
        passes,
        &[
            "GetDialectHandle__llzk__.*",
            "Operation.*",
            "OpBuilder.*",
            "RegisterLLZK.*Passes",
            "RegisterPCL.*Passes",
        ],
        &[
            "OpBuilder",
            "OpBuilderListener",
            "Notify(Operation|Block)Inserted",
            "(Op|Block)BuilderInsertPoint",
            "ValueRange",
        ],
    )
}

fn run() -> Result<()> {
    let out_dir = env::var("OUT_DIR").context("OUT_DIR environment variable")?;
    let llzk_prefix = format!("LLZK_SYS_{LLZK_MAJOR_VERSION}0_PREFIX");
    let llzk_dir = env::var(&llzk_prefix).context(format!("{llzk_prefix} environment variable"))?;
    let default_cfg = create_default_cfg();
    default_cfg.emit_cargo_commands()?;
    let cfg = (
        &default_cfg,
        WrapStaticFns::new(Path::new(&out_dir)),
        link_llzk(PathBuf::from(llzk_dir))?,
    );
    let bindings_path = Path::new(&out_dir).join("bindings.rs");
    let source = cfg.generate()?.to_string();
    fs::write(bindings_path, suppress_missing_docs(&source))?;
    cfg.try_compile("llzk-sys-cc")
}

/// Inserts `#[allow(missing_docs)]` before items in the generated bindings that cannot
/// have doc comments added to them in this crate:
///
/// - `pub fn mlir*`: tablegen-generated dialect/pass registration functions.
/// - `pub type Llzk* = c_uint/i*_t/u*_t`: enum typedef aliases emitted by bindgen from the CAPI's
///   `typedef enum ... Name;` pattern, where bindgen documents the constants but not the alias.
/// - `pub struct <name>` where the name contains "bindgen": anonymous types emitted by bindgen.
/// - `impl <type>` where the type contains "bindgen": impl blocks for bindgen types (e.g.
///   `impl<T> __BindgenUnionField<T>`).
/// - `pub <field>:` where the field name contains "bindgen": padding/bitfield fields emitted by
///   bindgen (e.g. `__bindgen_padding_0`).
fn suppress_missing_docs(source: &str) -> String {
    let mut result = String::with_capacity(source.len());
    for line in source.lines() {
        let trimmed = line.trim_start();
        let indent = &line[..line.len() - trimmed.len()];
        let needs_allow = trimmed.starts_with("pub fn mlir")
            || trimmed
                .strip_prefix("pub type ")
                .and_then(|rest| rest.split_once('='))
                .is_some_and(|(name, ty)| {
                    let name = name.trim();
                    let ty = ty.trim().trim_end_matches(';');
                    name.starts_with("Llzk")
                        && matches!(
                            ty,
                            "::std::os::raw::c_uint"
                                | "::std::os::raw::c_int"
                                | "::std::os::raw::c_uchar"
                                | "::std::os::raw::c_ushort"
                                | "::std::os::raw::c_ulong"
                                | "::std::os::raw::c_ulonglong"
                                | "::std::os::raw::c_schar"
                                | "::std::os::raw::c_short"
                                | "::std::os::raw::c_long"
                                | "::std::os::raw::c_longlong"
                        )
                })
            || (trimmed.starts_with("impl") && trimmed.to_ascii_lowercase().contains("bindgen"))
            || trimmed
                .strip_prefix("pub struct ")
                .and_then(|rest| rest.split_whitespace().next())
                .is_some_and(|name| name.to_ascii_lowercase().contains("bindgen"))
            || trimmed
                .strip_prefix("pub ")
                .and_then(|rest| {
                    let colon = rest.find(':')?;
                    let ident = &rest[..colon];
                    // exclude `pub fn` / `pub unsafe fn` / tuple-struct fields (no space, no paren)
                    if ident.contains(' ') || ident.contains('(') {
                        return None;
                    }
                    Some(ident.to_ascii_lowercase().contains("bindgen"))
                })
                .unwrap_or(false);
        if needs_allow {
            result.push_str(indent);
            result.push_str("#[allow(missing_docs)]\n");
        }
        result.push_str(line);
        result.push('\n');
    }
    result
}

fn feature_is_enabled(feature: &str) -> bool {
    env::var_os(format!(
        "CARGO_FEATURE_{}",
        feature.to_uppercase().replace("-", "_")
    ))
    .is_some()
}

fn main() {
    if let Err(err) = run() {
        println!("cargo:error={err:#}");
        std::process::exit(1);
    }
}