Skip to main content

limen_codegen/
lib.rs

1// Copyright © 2025–present Arlo Louis Byrne (idky137)
2// SPDX-License-Identifier: Apache-2.0
3//
4// Licensed under the Apache License, Version 2.0.
5// See the LICENSE-APACHE file in the project root for license terms.
6
7#![warn(missing_docs)]
8#![deny(unsafe_code)]
9//! # limen-codegen — reusable generator for Limen graphs
10//!
11//! This crate turns a compact, declarative graph DSL into fully-typed Rust
12//! implementations that conform to the `limen-core` graph, node, edge, and
13//! policy traits. The emitted code always includes a single concrete graph
14//! structure, with an optional std-only scoped execution API.
15//!
16//! It is designed to be used in **two** ways:
17//!
18//! 1. **Proc-macro mode** (recommended for quick iteration):
19//!
20//!    - Add `limen-build` (proc-macro crate) and `limen-core` to your `Cargo.toml`.
21//!    - Write the DSL inline in your code using the `define_graph! { ... }` macro.
22//!    - The macro forwards its token stream to `limen-codegen::expand_tokens(..)`.
23//!
24//!    ```rust,ignore
25//!    use limen_build::define_graph;
26//!
27//!    define_graph! {
28//!        pub struct MyGraph;
29//!
30//!        nodes {
31//!            0: {
32//!                ty: my_crate::nodes::MySourceNode,
33//!                in_ports: 0,
34//!                out_ports: 1,
35//!                in_payload: (),
36//!                out_payload: u32,
37//!                name: Some("src"),
38//!                // See "Ingress edges" below for rules:
39//!                // Only valid for source nodes (in_ports == 0, out_ports > 0).
40//!                ingress_policy: my_crate::policies::Q32_POLICY
41//!            },
42//!            1: {
43//!                ty: my_crate::nodes::MyMapNode,
44//!                in_ports: 1,
45//!                out_ports: 1,
46//!                in_payload: u32,
47//!                out_payload: u32,
48//!                name: Some("map")
49//!            },
50//!            2: {
51//!                ty: my_crate::nodes::MySinkNode,
52//!                in_ports: 1,
53//!                out_ports: 0,
54//!                in_payload: u32,
55//!                out_payload: (),
56//!                name: Some("sink")
57//!            },
58//!        }
59//!
60//!        edges {
61//!            0: {
62//!                ty: limen_core::edge::bench::TestSpscRingBuf<8>,
63//!                payload: u32,
64//!                manager: limen_core::memory::static_manager::StaticMemoryManager<u32, 8>,
65//!                from: (0, 0),
66//!                to: (1, 0),
67//!                policy: my_crate::policies::EDGE_POLICY,
68//!                name: Some("src->map")
69//!            },
70//!            1: {
71//!                ty: limen_core::edge::bench::TestSpscRingBuf<8>,
72//!                payload: u32,
73//!                manager: limen_core::memory::static_manager::StaticMemoryManager<u32, 8>,
74//!                from: (1, 0),
75//!                to: (2, 0),
76//!                policy: my_crate::policies::EDGE_POLICY,
77//!                name: Some("map->sink")
78//!            },
79//!        }
80//!
81//!        concurrent;
82//!    }
83//!    ```
84//!
85//! The trailing `concurrent;` keyword does not generate a separate graph type.
86//! It adds the std-only `ScopedGraphApi` implementation for the same graph.
87//!
88//! 2. **Build-script mode** (recommended when proc-macros slow down the language server or you want to inspect/generated source):
89//!
90//!    - Add `limen-codegen` (this crate) and `limen-core` to your `Cargo.toml`.
91//!    - Put your DSL in a file (for example, `src/my_graph.limen`).
92//!    - In `build.rs`, call `expand_str_to_file(..)` to emit pretty-printed Rust
93//!      into `OUT_DIR`, then `include!()` it from your library or binary.
94//!
95//!    ```rust,ignore
96//!    // build.rs
97//!    fn main() {
98//!        let spec = std::fs::read_to_string("src/my_graph.limen").unwrap();
99//!        let out = std::env::var("OUT_DIR").unwrap();
100//!        let dest = std::path::Path::new(&out).join("my_graph.rs");
101//!        limen_codegen::expand_str_to_file(&spec, &dest).unwrap();
102//!        println!("cargo:rerun-if-changed=src/my_graph.limen");
103//!    }
104//!    ```
105//!
106//!    ```rust,ignore
107//!    // lib.rs or main.rs
108//!    include!(concat!(env!("OUT_DIR"), "/my_graph.rs"));
109//!    ```
110//!
111//!    You can also build the graph programmatically in `build.rs` using
112//!    [`builder::GraphBuilder`] instead of writing the DSL as a string:
113//!
114//!    ```rust,ignore
115//!    use limen_codegen::builder::{Edge, GraphBuilder, GraphVisibility, Node};
116//!    use limen_core::policy::{AdmissionPolicy, EdgePolicy, OverBudgetAction, QueueCaps};
117//!
118//!    fn main() {
119//!        let edge_policy = EdgePolicy::new(
120//!            QueueCaps::new(8, 6, None, None),
121//!            AdmissionPolicy::DropNewest,
122//!            OverBudgetAction::Drop,
123//!        );
124//!
125//!        GraphBuilder::new("MyGraph", GraphVisibility::Public)
126//!            .node(
127//!                Node::new(0)
128//!                    .ty::<my_crate::nodes::MySource>()
129//!                    .in_ports(0)
130//!                    .out_ports(1)
131//!                    .in_payload::<()>()
132//!                    .out_payload::<u32>()
133//!                    .name(Some("src"))
134//!                    .ingress_policy(edge_policy),
135//!            )
136//!            .node(
137//!                Node::new(1)
138//!                    .ty::<my_crate::nodes::MyMap>()
139//!                    .in_ports(1)
140//!                    .out_ports(1)
141//!                    .in_payload::<u32>()
142//!                    .out_payload::<u32>()
143//!                    .name(Some("map")),
144//!            )
145//!            .node(
146//!                Node::new(2)
147//!                    .ty::<my_crate::nodes::MySink>()
148//!                    .in_ports(1)
149//!                    .out_ports(0)
150//!                    .in_payload::<u32>()
151//!                    .out_payload::<()>()
152//!                    .name(Some("sink")),
153//!            )
154//!            .edge(
155//!                Edge::new(0)
156//!                    .ty::<my_crate::queues::MyQueue<u32, 8>>()
157//!                    .payload::<u32>()
158//!                    .manager_ty::<my_crate::memory::MyMemoryManager<u32>>()
159//!                    .from(0, 0)
160//!                    .to(1, 0)
161//!                    .policy(edge_policy)
162//!                    .name(Some("src->map")),
163//!            )
164//!            .edge(
165//!                Edge::new(1)
166//!                    .ty::<my_crate::queues::MyQueue<u32, 8>>()
167//!                    .payload::<u32>()
168//!                    .manager_ty::<my_crate::memory::MyMemoryManager<u32>>()
169//!                    .from(1, 0)
170//!                    .to(2, 0)
171//!                    .policy(edge_policy)
172//!                    .name(Some("map->sink")),
173//!            )
174//!            .concurrent(false)
175//!            .finish()
176//!            .write("my_graph")
177//!            .unwrap();
178//!    }
179//!    ```
180//!
181//!    Set `.concurrent(true)` to additionally emit the std-only
182//!    `ScopedGraphApi` implementation for the same graph type.
183//!
184//! ## What gets generated
185//!
186//! Each invocation emits a single concrete graph type.
187//!
188//! - The generated graph struct stores:
189//!   - `nodes`: a tuple of `NodeLink<..>` (one per node),
190//!   - `edges`: a tuple of `EdgeLink<..>` (one per **real** edge; see ingress below),
191//!   - `managers`: a tuple of memory manager instances (one per real edge).
192//!
193//! - It also implements `GraphApi` for the concrete type, plus the per-index helper
194//!   traits (`GraphNodeAccess`, `GraphEdgeAccess`, `GraphNodeTypes`,
195//!   `GraphNodeContextBuilder`) that wire the graph into the Limen runtime APIs.
196//!
197//! - When `concurrent = false` (default), codegen emits the graph structure and
198//!   the core `GraphApi` / node-access / context-builder impls only.
199//!
200//! - When `concurrent = true`, codegen additionally emits a std-only
201//!   `ScopedGraphApi` implementation for that same graph type, behind
202//!   `#[cfg(feature = "std")]` in the downstream crate.
203//!
204//! ### Feature flag note
205//! The std-only scoped execution code is emitted behind `#[cfg(feature = "std")]`
206//! **in the generated file**.
207//! This crate (`limen-codegen`) does not define or forward a `std` feature; you control it in
208//! the crate that **compiles** the generated code.
209//!
210//! ## DSL: shape and types
211//!
212//! The DSL defines one graph per block:
213//!
214//! - A visibility and a struct name: `pub struct MyGraph;`
215//! - A `nodes { ... }` section: numbered nodes, each with type and I/O shape.
216//! - An `edges { ... }` section: numbered edges, each with its queue type, payload type, endpoints, and policy.
217//!
218//! **Node fields** (all required unless marked optional):
219//!
220//! - `ty: <TypePath>` — Concrete node implementation type.
221//! - `in_ports: <usize>` — Number of input ports (constant).
222//! - `out_ports: <usize>` — Number of output ports (constant).
223//! - `in_payload: <Type>` — Type received on each input port.
224//! - `out_payload: <Type>` — Type emitted on each output port.
225//! - `name: <Option<Expr>>` — Optional human-friendly identifier (for descriptors).
226//! - `ingress_policy: <Expr>` — **Optional** policy that creates a *synthetic* ingress edge
227//!   for this node. See **Ingress edges** below.
228//!
229//! **Edge fields** (all required unless marked optional):
230//!
231//! - `ty: <TypePath>` — Queue implementation type for this edge (for example, `TestSpscRingBuf<8>`).
232//! - `payload: <Type>` — Payload carried on this edge (must match node `out_payload` / `in_payload`).
233//! - `manager: <TypePath>` — Memory manager implementation for this edge (for example,
234//!   `StaticMemoryManager<P, DEPTH>` for `no_std` or `ConcurrentMemoryManager<P>` for concurrent graphs).
235//! - `from: (<usize>, <usize>)` — `(node_index, out_port_index)`.
236//! - `to: (<usize>, <usize>)` — `(node_index, in_port_index)`.
237//! - `policy: <Expr>` — Policy value used to compute occupancy and admission.
238//! - `name: <Option<Expr>>` — Optional human-friendly identifier (for descriptors).
239//!
240//! ### Important rules and assumptions
241//!
242//! 0. **Two edge classes**  
243//!    - *Ingress* edges are **synthetic** and created only for source nodes that
244//!      specify `ingress_policy`. They occupy the lowest global edge indices.
245//!    - *Real* edges are those declared in `edges { ... }` and are stored in the graph.
246//!
247//! 1. **Contiguous indices**  
248//!    - Node indices must be contiguous `0..nodes.len()` with no gaps.
249//!    - Edge indices must be contiguous `0..edges.len()` with no gaps.
250//!
251//! 2. **Port bounds**  
252//!    - For every edge, `from_port < from_node.out_ports` and `to_port < to_node.in_ports`.
253//!
254//! 3. **Payload compatibility**  
255//!    - For every edge, `edge.payload == from_node.out_payload == to_node.in_payload` (token-level equality).
256//!
257//! 4. **Queue uniformity per node**  
258//!    - All inbound edges to the same node must have an identical queue type.
259//!    - All outbound edges from the same node must have an identical queue type.
260//!    - This allows the generator to infer a single `InQ` and `OutQ` type per node.
261//!
262//! 5. **Manager uniformity per node**
263//!    - All inbound edges to the same node must have an identical manager type.
264//!    - All outbound edges from the same node must have an identical manager type.
265//!    - This allows the generator to infer a single `InM` and `OutM` type per node.
266//!
267//! 6. **Ingress edges (synthetic)**  
268//!    - If a node specifies `ingress_policy`, a *synthetic* ingress edge is created for that node.
269//!    - Ingress edges do **not** live in the real `edges` tuple and do **not** carry data;
270//!      they exist to expose external ingress occupancy via the node’s source interface.
271//!    - **Assumption:** `ingress_policy` may only be specified for **source nodes**
272//!      (`in_ports == 0` and `out_ports > 0`) that implement the source interface in `limen-core`.
273//!      These ingress edges occupy the lowest global edge indices `[0..ingress_count)`.
274//!
275//! 7. **Dependency on `limen-core`**  
276//!    - Generated code references the `limen_core` crate (note the underscore), which must be
277//!      available to the downstream crate. Ensure your Cargo manifest includes a dependency on
278//!      `limen-core` (the hyphenated package name maps to the `limen_core` crate identifier).
279//!
280//! ## Programmatic entry points (when not using the proc macro)
281//!
282//! All of the following:
283//! - parse the DSL (from tokens or string),
284//! - validate its structure and typing,
285//! - and emit the graph plus any optional scoped API selected by the input AST.
286//!
287//! - [`expand_tokens`]: parse+validate+emit from a token stream (used by the proc macro).
288//! - [`expand_str_to_tokens`]: parse+validate+emit from a `&str` DSL (for build scripts or tests).
289//! - [`expand_str_to_string`]: same as above, but pretty-prints to a Rust source string.
290//! - [`expand_str_to_file`]: same as above, writes to a path (creating parent directories if needed).
291//! - [`expand_ast_to_tokens`], [`expand_ast_to_file`]: like the above, but take a typed AST
292//!   (for use with the `builder` module so you can write graphs as normal Rust).
293//!
294//! Each entry point emits the single graph type, plus the optional std-only
295//! scoped execution API determined by the `emit_concurrent` flag on the input AST.
296//!
297//! All entry points perform **validation** before emitting code. Errors are returned as
298//! [`CodegenError`], with precise messages for parsing, validation, pretty-print, or I/O failures.
299
300/// Internal: Abstract syntax tree for the DSL (consumed by parsing, validation, and emission).
301mod ast;
302/// Optional: typed, LS-friendly graph builder (no proc-macro, no big strings).
303pub mod builder;
304/// Internal: Code emission — turns a validated AST into a `TokenStream` of Rust code.
305mod gen;
306/// Internal: DSL parser — converts the `define_graph!` body (or a string) into an AST.
307mod parse;
308/// Internal: Structural and semantic checks for a well-formed graph.
309mod validate;
310
311use proc_macro2::TokenStream as TokenStream2;
312use std::path::{Path, PathBuf};
313
314/// Errors that can occur while expanding the graph DSL into Rust code.
315#[derive(thiserror::Error, Debug)]
316pub enum CodegenError {
317    /// The DSL could not be parsed into a valid AST.
318    #[error("parse error: {0}")]
319    Parse(#[from] syn::Error),
320
321    /// The AST failed semantic validation (for example, non-contiguous indices,
322    /// port bound violations, payload mismatches, or queue non-uniformity).
323    #[error("validation error: {0}")]
324    Validate(String),
325
326    /// I/O failure while reading or writing generated code.
327    #[error("io error: {0}")]
328    Io(#[from] std::io::Error),
329
330    /// Pretty-printing (token → formatted Rust source) failed.
331    #[error("prettyprint failed: {0}")]
332    Pretty(String),
333}
334
335/// Validate and emit Rust code from a typed `ast::GraphDef`.
336///
337/// This is the low-level entry used by [`builder::GraphBuilder`] after it has
338/// constructed the AST programmatically.  The graph is validated before emission;
339/// if validation fails a [`CodegenError::Validate`] is returned.
340///
341/// # Errors
342/// Returns [`CodegenError::Validate`] if the graph is structurally or semantically invalid.
343pub fn expand_ast_to_tokens(g: ast::GraphDef) -> Result<TokenStream2, CodegenError> {
344    validate::validate_definition(&g).map_err(CodegenError::Validate)?;
345    Ok(gen::emit(&g))
346}
347
348/// Try to pretty-print tokens; if that fails, fall back to raw `.to_string()`.
349fn tokens_to_string_pretty_or_raw(tokens: &TokenStream2) -> String {
350    match syn::parse2::<syn::File>(tokens.clone()) {
351        Ok(file) => prettyplease::unparse(&file),
352        Err(_) => tokens.to_string(),
353    }
354}
355
356/// Write tokens to a file, preferring pretty-printing with fallback to raw.
357pub fn write_tokens_pretty_or_raw<P: AsRef<std::path::Path>>(
358    tokens: &TokenStream2,
359    dest: P,
360) -> Result<std::path::PathBuf, CodegenError> {
361    let s = tokens_to_string_pretty_or_raw(tokens);
362    let p = dest.as_ref().to_path_buf();
363    if let Some(parent) = p.parent() {
364        std::fs::create_dir_all(parent)?;
365    }
366    std::fs::write(&p, s)?;
367    Ok(p)
368}
369
370/// Parse, validate, and emit Rust code from a proc-macro input token stream.
371///
372/// Emits the graph selected by the DSL, plus the optional std-only
373/// `ScopedGraphApi` implementation when the trailing `concurrent;`
374/// keyword is present.
375///
376/// This is the entry used by `limen-build::define_graph! { ... }`.
377///
378/// # Parameters
379/// - `input`: Tokens containing exactly one graph DSL definition (see crate-level docs).
380///
381/// # Returns
382/// - `Ok(TokenStream2)`: The generated Rust code for the graph type and its trait implementations.
383/// - `Err(CodegenError)`: If parsing, validation, or emission fails.
384///
385/// # Errors
386/// Returns:
387/// - [`CodegenError::Parse`] if the input tokens are not a well-formed graph DSL.
388/// - [`CodegenError::Validate`] if the graph is structurally or semantically invalid.
389pub fn expand_tokens(input: TokenStream2) -> Result<TokenStream2, CodegenError> {
390    let g = syn::parse2::<ast::GraphDef>(input)?;
391    validate::validate_definition(&g).map_err(CodegenError::Validate)?;
392    Ok(gen::emit(&g))
393}
394
395/// Parse, validate, and emit Rust code from a DSL string (build script helper).
396///
397/// Emits the graph, plus the optional std-only `ScopedGraphApi`
398/// implementation selected by the `concurrent` keyword in the DSL.
399///
400/// Typical usage is inside `build.rs`, or in tests that snapshot generated code.
401///
402/// # Parameters
403/// - `spec`: The graph DSL as a UTF-8 string. It must contain exactly one graph definition.
404///
405/// # Returns
406/// - `Ok(TokenStream2)`: The generated Rust code for the graph type and its trait implementations.
407/// - `Err(CodegenError)`: If parsing, validation, or emission fails.
408///
409/// # Errors
410/// Returns:
411/// - [`CodegenError::Parse`] if the string is not a well-formed graph DSL.
412/// - [`CodegenError::Validate`] if the graph is structurally or semantically invalid.
413pub fn expand_str_to_tokens(spec: &str) -> Result<TokenStream2, CodegenError> {
414    let g = syn::parse_str::<ast::GraphDef>(spec)?;
415    validate::validate_definition(&g).map_err(CodegenError::Validate)?;
416    Ok(gen::emit(&g))
417}
418
419/// Parse, validate, emit, and **pretty-print** the Rust code for a DSL string.
420///
421/// This is convenient when you want stable, human-readable source for inspection
422/// or to write to disk with [`expand_str_to_file`].
423///
424/// # Parameters
425/// - `spec`: The graph DSL as a UTF-8 string. It must contain exactly one graph definition.
426///
427/// # Returns
428/// - `Ok(String)`: Formatted Rust source for the generated graph.
429/// - `Err(CodegenError)`: If parsing, validation, emission, or pretty-printing fails.
430///
431/// # Errors
432/// Returns:
433/// - [`CodegenError::Parse`] or [`CodegenError::Validate`] as above.
434/// - [`CodegenError::Pretty`] if formatting the generated tokens as a Rust file fails.
435pub fn expand_str_to_string(spec: &str) -> Result<String, CodegenError> {
436    let tokens = expand_str_to_tokens(spec)?;
437    Ok(tokens_to_string_pretty_or_raw(&tokens))
438}
439
440/// Parse, validate, emit, pretty-print, and **write** the Rust code for a DSL
441/// string to `dest`. Parent directories are created if needed, and writes are
442/// performed atomically.
443///
444/// This helper creates parent directories if needed, writes atomically to `dest`, and returns
445/// the resolved path. It is ideal for use in `build.rs`, where you can later `include!()` the file.
446///
447/// # Parameters
448/// - `spec`: The graph DSL as a UTF-8 string. It must contain exactly one graph definition.
449/// - `dest`: Destination filesystem path for the generated Rust source file.
450///
451/// # Returns
452/// - `Ok(PathBuf)`: The absolute path that was written.
453/// - `Err(CodegenError)`: If parsing, validation, emission, pretty-printing, or I/O fails.
454///
455/// # Errors
456/// Returns:
457/// - [`CodegenError::Parse`] or [`CodegenError::Validate`] as above.
458/// - [`CodegenError::Pretty`] if formatting the generated tokens as a Rust file fails.
459/// - [`CodegenError::Io`] if filesystem operations fail (for example, permission denied).
460pub fn expand_str_to_file<P: AsRef<Path>>(spec: &str, dest: P) -> Result<PathBuf, CodegenError> {
461    let tokens = expand_str_to_tokens(spec)?;
462    write_tokens_pretty_or_raw(&tokens, dest)
463}
464
465/// Validate, emit, pretty-print, and **write** a typed `ast::GraphDef` to `dest`.
466///
467/// Combines [`expand_ast_to_tokens`] with [`write_tokens_pretty_or_raw`].
468/// Parent directories are created if needed.
469///
470/// # Parameters
471/// - `g`: The graph AST, typically produced by [`builder::GraphBuilder`].
472/// - `dest`: Destination filesystem path for the generated Rust source file.
473///
474/// # Returns
475/// - `Ok(PathBuf)`: The absolute path that was written.
476/// - `Err(CodegenError)`: If validation, emission, or I/O fails.
477///
478/// # Errors
479/// Returns [`CodegenError::Validate`], or [`CodegenError::Io`] if filesystem
480/// operations fail (for example, permission denied or out of disk space).
481pub fn expand_ast_to_file<P: AsRef<Path>>(
482    g: ast::GraphDef,
483    dest: P,
484) -> Result<PathBuf, CodegenError> {
485    let tokens = expand_ast_to_tokens(g)?;
486    write_tokens_pretty_or_raw(&tokens, dest)
487}