ridl-ir 0.6.0

The RIDL intermediate representation: the typl surface plus the ridl interaction layer, generated from a versioned protobuf schema.
Documentation
//! The lowered codegen model — the artifact a codegen backend reads instead
//! of the raw IR (ADR-0020 decisions 8 and 9).
//!
//! The model carries, once and resolved, every fact the four in-tree backends
//! re-derive from `ridl.ir.v2` for themselves today: the scalar classes, the
//! pinned name transforms and the two compositions of them the wire backends
//! make, the resolution of every type reference over the packages the
//! lowering was handed, the transitive typl rules (the init of typl §5.8 and
//! the closure `derives.rs` walks), the tombstones in their slots, the
//! FlatBuffers projection whole, the interaction facts the descriptors and
//! the frame need, and the narrow contract-clause translation. The design
//! note is `docs/wip/2026-09-22-codegen-model-design.md`.
//!
//! **The home is this crate** (that note's D-11): the model is the projection
//! facts generalized — it carries what [`crate::name`] produces and what
//! [`crate::projection`] produces and adds the resolution and the typl rules
//! the backends still derive — and `ridl-ir` is the crate every consumer of
//! the model already depends on, so no edge in the dependency graph moves and
//! ADR-0020 decision 9 holds as it does today. The module moves to a crate of
//! its own if the lowering ever needs a dependency this crate must not carry
//! under `--no-default-features` on `wasm32`, or if a consumer needs the
//! model's types without the IR's; the `.proto` file and the package name do
//! not change with that move, so a plugin notices nothing.
//!
//! The canonical encoding is canonical protobuf JSON, in the dialect ADR-0014
//! decision 14 fixes for the IR and through the same generated impls and the
//! same reader guards ([`to_json_pretty`], [`from_json`]). Binary and
//! prototext are derived, as they are for the IR.
//!
//! The backend contract over the model — [`Backend`], the request and
//! response messages of `proto/ridl/codegen/v1/plugin.proto`, their JSON on
//! the pipe, and the path rule every host applies — is the `contract`
//! submodule (ADR-0020 decision 9; `docs/design/codegen-plugins.md`).

use crate::v2::{MAX_JSON_NESTING, read_json, render_json};

pub mod v1 {
    //! The generated types of `ridl.codegen.v1`: the model
    //! (`proto/ridl/codegen/v1/model.proto`), the deployment section of a
    //! request (`proto/ridl/codegen/v1/deployment.proto`) and the backend
    //! contract's request and response over them
    //! (`proto/ridl/codegen/v1/plugin.proto`).

    include!(concat!(env!("OUT_DIR"), "/ridl.codegen.v1.rs"));

    // The canonical protobuf JSON serde impls, generated by pbjson-build in
    // `build.rs` from the same schema compilation as the types above. The
    // private module scopes one lint allowance to generated code, for the
    // reason `v2`'s own `serde_impls` module states.
    #[expect(clippy::useless_borrows_in_formatting)]
    mod serde_impls {
        use super::*;

        include!(concat!(env!("OUT_DIR"), "/ridl.codegen.v1.serde.rs"));
    }
}

mod bindings;
mod clauses;
mod contract;
mod deployment;
pub mod depth;
mod facts;
mod flatbuffers;
mod lower;
mod names;
mod resolve;
mod unbounded;

pub use contract::{
    Backend, GENERATED_MARKER_PREFIX, ModelBackend, RawIr, SCHEMA, check_path, comment_preamble,
    error, generated_marker, has_error, header_control_character, normalise_header,
    request_from_json, request_to_json, response_from_json, response_to_json, text_file,
};
pub use deployment::lower_deployment;
pub use lower::lower;
pub use unbounded::attribute as fb_unbounded;

/// The error [`to_json_pretty`] and [`to_text_format`] return, on the two
/// paths [`crate::v2::SerializeError`] has and for the same causes — the
/// model is written in the same dialect, through the same machinery.
#[derive(Debug)]
pub enum SerializeError {
    /// Canonical protobuf JSON: the one error path the pbjson-generated
    /// `Serialize` impl has is an `i32` enum field holding a discriminant
    /// outside the schema.
    Json(serde_json::Error),
    /// Prototext: the transcode into the dynamic message goes through the
    /// wire encoding, whose decoder enforces prost's fixed recursion limit.
    Text(prost::DecodeError),
}

impl std::fmt::Display for SerializeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Json(source) => write!(
                f,
                "cannot render the codegen model as canonical protobuf JSON: {source}; the known \
                 cause is an enum field holding a discriminant outside the schema"
            ),
            Self::Text(source) => write!(
                f,
                "cannot render the codegen model as prototext: {source}; the known cause is \
                 composite nesting deeper than the transcoding decoder's recursion limit"
            ),
        }
    }
}

impl SerializeError {
    /// The IR's error, re-labelled as the model's: the model and the request
    /// are written through the IR's own machinery, so the causes are the
    /// same two.
    fn from_v2(error: crate::v2::SerializeError) -> Self {
        match error {
            crate::v2::SerializeError::Json(source) => Self::Json(source),
            crate::v2::SerializeError::Text(source) => Self::Text(source),
        }
    }
}

impl std::error::Error for SerializeError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::Json(source) => Some(source),
            Self::Text(source) => Some(source),
        }
    }
}

/// Renders a lowered model as pretty-printed canonical protobuf JSON — the
/// `--emit codegen-model` artifact. The codegen request carries the same JSON
/// value as its `model`, nested one indentation level deeper by
/// `request_to_json`.
pub fn to_json_pretty(model: &v1::Model) -> Result<String, SerializeError> {
    render_json(model).map_err(SerializeError::from_v2)
}

/// Reads a lowered model from canonical protobuf JSON — the inverse of
/// [`to_json_pretty`], under the guards [`crate::v2::from_json`] states: the
/// input's nesting is measured first and refused past the same ceiling, and
/// the parse runs on the same explicitly sized stack.
///
/// The bound a reader of this schema must provision is 264 JSON levels, from
/// the map shape at the front end's own type-depth limit (design note §6.2),
/// so the ceiling of 1,000 leaves a factor of 3.8.
pub fn from_json(text: &str) -> Result<v1::Model, serde_json::Error> {
    read_json(text)
}

/// The nesting ceiling [`from_json`] enforces, in JSON bracket levels — the
/// IR's, because the two encodings are one dialect with one reader.
pub const MAX_JSON_NESTING_LEVELS: usize = MAX_JSON_NESTING;

/// Renders a lowered model in the protobuf text format — the inspection
/// encoding, derived from the same descriptor pool the IR's prototext uses.
pub fn to_text_format(model: &v1::Model) -> Result<String, SerializeError> {
    crate::v2::render_text_for(crate::v2::codegen_model_descriptor(), model)
        .map_err(SerializeError::from_v2)
}

/// Encodes a lowered model in the protobuf binary wire format — the derived
/// compact encoding, whose reader stops 100 message levels below the root.
pub fn to_binary(model: &v1::Model) -> Vec<u8> {
    prost::Message::encode_to_vec(model)
}

/// Decodes a lowered model from the protobuf binary wire format.
pub fn from_binary(bytes: &[u8]) -> Result<v1::Model, prost::DecodeError> {
    prost::Message::decode(bytes)
}

#[cfg(test)]
mod tests;