espresso_logic/
lib.rs

1//! # Espresso Logic Minimizer
2//!
3//! This crate provides Rust bindings to the Espresso heuristic logic minimiser
4//! (Version 2.3), a classic tool from UC Berkeley for minimising Boolean functions.
5//!
6//! ## Overview
7//!
8//! Espresso takes a Boolean function represented as a sum-of-products (cover) and
9//! produces a minimal or near-minimal equivalent representation. It's particularly
10//! useful for:
11//!
12//! - Digital logic synthesis
13//! - PLA (Programmable Logic Array) minimisation
14//! - Boolean function simplification
15//! - Logic optimisation in CAD tools
16//!
17//! ## API Levels
18//!
19//! This crate provides **two API levels** to suit different needs:
20//!
21//! ### High-Level API (Recommended)
22//!
23//! The high-level API provides easy-to-use abstractions with automatic resource management:
24//!
25//! - **[`BoolExpr`]** - Boolean expressions with parsing, operators, and the `expr!` macro
26//! - **[`Cover`]** - Dynamic covers with automatic dimension management
27//!
28//! **Benefits:**
29//! - ✅ Automatic memory management
30//! - ✅ No manual dimension tracking
31//! - ✅ Thread-safe by design
32//! - ✅ Clean, idiomatic Rust API
33//!
34//! ### Low-Level API (Advanced)
35//!
36//! The low-level [`espresso`] module provides direct access to the C library:
37//!
38//! - **[`espresso::Espresso`]** - Direct Espresso instance management
39//! - **[`espresso::EspressoCover`]** - Raw cover with C memory control
40//!
41//! **When to use:**
42//! - **Access to intermediate covers** - Get ON-set (F), don't-care (D), and OFF-set (R) separately
43//! - **Custom don't-care/off-sets** - Provide your own D and R covers to `minimize()`
44//! - **Maximum performance** - Minimal overhead, direct C calls (~5-10% faster than high-level)
45//! - **Explicit instance control** - Manually manage when Espresso instances are created/destroyed
46//!
47//! **Note:** Algorithm configuration via [`EspressoConfig`] works with **both** APIs -
48//! it's not a reason to use the low-level API.
49//!
50//! **Important constraints:**
51//! - ⚠️ **All covers on a thread must use the same dimensions** until dropped
52//! - ⚠️ Requires manual dimension management
53//! - ⚠️ More complex error handling
54//!
55//! See the [`espresso`] module documentation for detailed usage and safety guidelines.
56//!
57//! ## Using the High-Level API
58//!
59//! ### 1. Boolean Expressions (Recommended for most use cases)
60//!
61//! The `expr!` macro provides three convenient styles:
62//!
63//! ```
64//! use espresso_logic::{BoolExpr, expr, Minimizable};
65//!
66//! # fn main() -> std::io::Result<()> {
67//! // Style 1: String literals (most concise - no declarations!)
68//! let xor = expr!("a" * !"b" + !"a" * "b");
69//! println!("{}", xor);  // Output: a * ~b + ~a * b (minimal parentheses!)
70//!
71//! // Style 2: Existing BoolExpr variables
72//! let a = BoolExpr::variable("a");
73//! let b = BoolExpr::variable("b");
74//! let c = BoolExpr::variable("c");
75//! let redundant = expr!(a * b + a * b * c);
76//!
77//! // Minimize it (returns a new minimized expression)
78//! let minimized = redundant.minimize()?;
79//! println!("Minimized: {}", minimized);  // Output: a * b
80//!
81//! // Check logical equivalence (create new instance for comparison)
82//! let redundant2 = expr!(a * b + a * b * c);
83//! assert!(redundant2.equivalent_to(&minimized));
84//! # Ok(())
85//! # }
86//! ```
87//!
88//! Parse expressions from strings:
89//!
90//! ```
91//! use espresso_logic::{BoolExpr, Minimizable};
92//!
93//! # fn main() -> std::io::Result<()> {
94//! // Parse using standard operators: +, *, ~, ! (or & and |)
95//! let expr = BoolExpr::parse("a * b + ~a * ~b")?;
96//!
97//! // Minimise using Espresso algorithm
98//! let minimised = expr.minimize()?;
99//! println!("Minimised: {}", minimised);
100//! # Ok(())
101//! # }
102//! ```
103//!
104//! #### Using Cover with Expressions
105//!
106//! For advanced use cases, the `Cover` type provides direct access to the cover
107//! representation and supports adding expressions:
108//!
109//! ```
110//! use espresso_logic::{BoolExpr, Cover, CoverType, Minimizable};
111//!
112//! # fn main() -> std::io::Result<()> {
113//! let a = BoolExpr::variable("a");
114//! let b = BoolExpr::variable("b");
115//! let expr = a.and(&b).or(&a.and(&b.not()));
116//!
117//! // Create cover and add expression
118//! let mut cover = Cover::new(CoverType::F);
119//! cover.add_expr(&expr, "output")?;
120//!
121//! // Access cover properties
122//! println!("Input variables: {:?}", cover.input_labels());
123//! println!("Number of cubes: {}", cover.num_cubes());
124//!
125//! // Minimize the cover
126//! cover = cover.minimize()?;
127//!
128//! // Convert back to expression
129//! let minimized = cover.to_expr("output")?;
130//! println!("Minimized: {}", minimized);
131//! # Ok(())
132//! # }
133//! ```
134//!
135//! ### 2. Manual Cube Construction
136//!
137//! Build covers by manually adding cubes (dimensions grow automatically):
138//!
139//! ```
140//! use espresso_logic::{Cover, CoverType, Minimizable};
141//!
142//! # fn main() -> std::io::Result<()> {
143//! // Create a cover (dimensions grow automatically)
144//! let mut cover = Cover::new(CoverType::F);
145//!
146//! // Build the ON-set (truth table)
147//! cover.add_cube(&[Some(false), Some(true)], &[Some(true)]);  // 01 -> 1
148//! cover.add_cube(&[Some(true), Some(false)], &[Some(true)]);  // 10 -> 1
149//!
150//! // Minimize (returns new instance)
151//! cover = cover.minimize()?;
152//!
153//! // Iterate over minimized cubes
154//! for (inputs, outputs) in cover.cubes_iter() {
155//!     println!("Cube: {:?} -> {:?}", inputs, outputs);
156//! }
157//! # Ok(())
158//! # }
159//! ```
160//!
161//! ### 3. PLA Files
162//!
163//! Covers can be read from and written to PLA format files (compatible with original Espresso):
164//!
165//! ```
166//! use espresso_logic::{Cover, CoverType, Minimizable, PLAReader, PLAWriter};
167//! # use std::io::Write;
168//!
169//! # fn main() -> std::io::Result<()> {
170//! # let mut temp = tempfile::NamedTempFile::new()?;
171//! # temp.write_all(b".i 2\n.o 1\n.p 1\n01 1\n.e\n")?;
172//! # temp.flush()?;
173//! # let input_path = temp.path();
174//! // Read from PLA file (PLAReader trait)
175//! let mut cover = Cover::from_pla_file(input_path)?;
176//!
177//! // Minimize
178//! cover = cover.minimize()?;
179//!
180//! # let output_file = tempfile::NamedTempFile::new()?;
181//! # let output_path = output_file.path();
182//! // Write to PLA file (PLAWriter trait)
183//! cover.to_pla_file(output_path, CoverType::F)?;
184//!
185//! // Or write directly to any Write implementation
186//! use std::io::{Write, BufReader};
187//! let mut buffer = Vec::new();
188//! cover.write_pla(&mut buffer, CoverType::F)?;
189//!
190//! // Similarly, you can read from any BufRead implementation
191//! let reader = BufReader::new(buffer.as_slice());
192//! let cover2 = Cover::from_pla_reader(reader)?;
193//! # Ok(())
194//! # }
195//! ```
196//!
197//! ## Cover Types
198//!
199//! The library supports different cover types for representing Boolean functions:
200//!
201//! - **F Type** - ON-set only (specifies where output is 1)
202//! - **FD Type** - ON-set + Don't-cares (most flexible)
203//! - **FR Type** - ON-set + OFF-set (specifies both 1s and 0s)
204//! - **FDR Type** - ON-set + Don't-cares + OFF-set (complete specification)
205//!
206//! ```
207//! use espresso_logic::{Cover, CoverType};
208//!
209//! # fn main() -> std::io::Result<()> {
210//! // F type (ON-set only)
211//! let mut f_cover = Cover::new(CoverType::F);
212//! f_cover.add_cube(&[Some(true), Some(true)], &[Some(true)]);
213//!
214//! // FD type (ON-set + Don't-cares)
215//! let mut fd_cover = Cover::new(CoverType::FD);
216//! fd_cover.add_cube(&[Some(true), Some(true)], &[Some(true)]);  // ON
217//! fd_cover.add_cube(&[Some(false), Some(false)], &[None]);      // Don't-care
218//! # Ok(())
219//! # }
220//! ```
221//!
222//! ## Thread Safety and Concurrency
223//!
224//! ### High-Level API ([`Cover`])
225//!
226//! [`Cover`] is `Send` and `Sync`, making it freely shareable across threads. The key
227//! advantage is that Espresso instances are created **lazily on-demand** - only when
228//! `.minimize()` is called, the thread-local Espresso instance is created for that thread.
229//!
230//! ```
231//! use espresso_logic::{Cover, CoverType, Minimizable};
232//! use std::thread;
233//!
234//! # fn main() -> std::io::Result<()> {
235//! // Covers can be freely moved between threads
236//! let handles: Vec<_> = (0..4).map(|_| {
237//!     thread::spawn(move || {
238//!         let mut cover = Cover::new(CoverType::F);
239//!         cover.add_cube(&[Some(false), Some(true)], &[Some(true)]);
240//!         cover.add_cube(&[Some(true), Some(false)], &[Some(true)]);
241//!         
242//!         // Creates thread-local Espresso instance on first minimize()
243//!         cover = cover.minimize()?;
244//!         Ok(cover.num_cubes())
245//!     })
246//! }).collect();
247//!
248//! for handle in handles {
249//!     let result: std::io::Result<usize> = handle.join().unwrap();
250//!     println!("Result: {} cubes", result?);
251//! }
252//! # Ok(())
253//! # }
254//! ```
255//!
256//! ### Low-Level API ([`espresso`])
257//!
258//! The low-level API uses C11 thread-local storage. Each thread gets its own independent
259//! Espresso instance and global state, but types are `!Send` and `!Sync`. See the
260//! [`espresso`] module for details on dimension constraints.
261//!
262//! ## Using the Low-Level API (Advanced)
263//!
264//! For maximum performance and fine-grained control, use the [`espresso`] module directly:
265//!
266//! ```
267//! use espresso_logic::espresso::{Espresso, EspressoCover, CubeType};
268//! use espresso_logic::EspressoConfig;
269//!
270//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
271//! // Explicit instance creation with custom config
272//! let mut config = EspressoConfig::default();
273//! config.single_expand = true;  // Faster mode
274//! let _esp = Espresso::new(2, 1, &config);
275//!
276//! // Create cover with raw cube data
277//! let cubes = [
278//!     (&[0, 1][..], &[1][..]),  // 01 -> 1
279//!     (&[1, 0][..], &[1][..]),  // 10 -> 1
280//! ];
281//! let cover = EspressoCover::from_cubes(&cubes, 2, 1)?;
282//!
283//! // Minimize and get all three covers (F, D, R)
284//! let (f_result, d_result, r_result) = cover.minimize(None, None);
285//!
286//! println!("ON-set: {} cubes", f_result.to_cubes(2, 1, CubeType::F).len());
287//! println!("Don't-care: {} cubes", d_result.to_cubes(2, 1, CubeType::F).len());
288//! println!("OFF-set: {} cubes", r_result.to_cubes(2, 1, CubeType::F).len());
289//! # Ok(())
290//! # }
291//! ```
292//!
293//! **⚠️ Important Constraint:** All covers on a thread must use the same dimensions until dropped:
294//!
295//! ```
296//! use espresso_logic::espresso::EspressoCover;
297//!
298//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
299//! // Works: same dimensions (2 inputs, 1 output)
300//! let cubes1 = [(&[0, 1][..], &[1][..])];
301//! let cover1 = EspressoCover::from_cubes(&cubes1, 2, 1)?;
302//! let cubes2 = [(&[1, 0][..], &[1][..])];
303//! let cover2 = EspressoCover::from_cubes(&cubes2, 2, 1)?;
304//!
305//! // Must drop before using different dimensions
306//! drop(cover1);
307//! drop(cover2);
308//!
309//! // Now 3 inputs works
310//! let cubes3 = [(&[0, 1, 0][..], &[1][..])];
311//! let cover3 = EspressoCover::from_cubes(&cubes3, 3, 1)?;
312//! # Ok(())
313//! # }
314//! ```
315//!
316//! See the [`espresso`] module documentation for detailed safety guidelines and usage patterns.
317//!
318//! # 📚 Comprehensive Guides
319//!
320//! See the [`doc`] module for embedded guides:
321//!
322//! - [`doc::examples`] - Complete usage examples for all features
323//! - [`doc::boolean_expressions`] - Boolean expression API deep dive
324//! - [`doc::pla_format`] - PLA file format specification
325//! - [`doc::cli`] - Command-line tool documentation
326
327// Public modules
328pub mod cover;
329pub mod error;
330pub mod espresso;
331pub mod expression;
332pub mod sys;
333
334// Documentation-only module
335#[cfg(doc)]
336pub mod examples;
337
338// Re-export high-level public API
339pub use cover::pla::{PLAReader, PLAWriter};
340pub use cover::{Cover, CoverType, Cube, CubeType, Dnf, Minimizable};
341pub use espresso::EspressoConfig;
342#[allow(deprecated)]
343pub use expression::Bdd;
344pub use expression::{BoolExpr, ExprNode};
345
346// Re-export procedural macro
347pub use espresso_logic_macros::expr;
348
349/// Comprehensive documentation guides
350///
351/// This module contains embedded guides from the `docs/` directory,
352/// making all comprehensive documentation available on docs.rs.
353///
354/// # Available Guides
355///
356/// - [`examples`](doc::examples) - Complete usage examples for all features
357/// - [`boolean_expressions`](doc::boolean_expressions) - Boolean expression API deep dive
358/// - [`pla_format`](doc::pla_format) - PLA file format specification
359/// - [`cli`](doc::cli) - Command-line tool documentation
360pub mod doc {
361    #[doc = include_str!("../docs/EXAMPLES.md")]
362    #[cfg(doc)]
363    pub mod examples {}
364
365    #[doc = include_str!("../docs/BOOLEAN_EXPRESSIONS.md")]
366    #[cfg(doc)]
367    pub mod boolean_expressions {}
368
369    #[doc = include_str!("../docs/PLA_FORMAT.md")]
370    #[cfg(doc)]
371    pub mod pla_format {}
372
373    #[doc = include_str!("../docs/CLI.md")]
374    #[cfg(doc)]
375    pub mod cli {}
376}