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}