espresso_logic/pla.rs
1//! PLA (Programmable Logic Array) format support
2//!
3//! This module handles PLA file I/O and provides `PLACover`, a dynamic cover type
4//! for working with PLA files where dimensions are not known at compile time.
5
6use std::io;
7use std::path::Path;
8
9use crate::cover::{Cube, CubeType, Minimizable, PLAType};
10
11/// Trait for types that support PLA serialization
12pub(crate) trait PLASerializable: Minimizable {
13 /// Write this cover to PLA format string
14 fn to_pla_string(&self, pla_type: PLAType) -> io::Result<String> {
15 let mut output = String::new();
16
17 // Write .type directive first for FD, FR, FDR (matching C output order)
18 match pla_type {
19 PLAType::FD => output.push_str(".type fd\n"),
20 PLAType::FR => output.push_str(".type fr\n"),
21 PLAType::FDR => output.push_str(".type fdr\n"),
22 PLAType::F => {} // F is default, no .type needed
23 }
24
25 // Write PLA header
26 output.push_str(&format!(".i {}\n", self.num_inputs()));
27 output.push_str(&format!(".o {}\n", self.num_outputs()));
28
29 // Filter cubes based on output type using cube_type field
30 let filtered_cubes: Vec<_> = self
31 .internal_cubes_iter()
32 .filter(|cube| {
33 match pla_type {
34 PLAType::F => cube.cube_type == CubeType::F,
35 PLAType::FD => cube.cube_type == CubeType::F || cube.cube_type == CubeType::D,
36 PLAType::FR => cube.cube_type == CubeType::F || cube.cube_type == CubeType::R,
37 PLAType::FDR => true, // All cubes
38 }
39 })
40 .collect();
41
42 // Add .p directive with filtered cube count
43 output.push_str(&format!(".p {}\n", filtered_cubes.len()));
44
45 // Write filtered cubes
46 for cube in filtered_cubes {
47 // Write inputs
48 for inp in cube.inputs.iter() {
49 output.push(match inp {
50 Some(false) => '0',
51 Some(true) => '1',
52 None => '-',
53 });
54 }
55
56 output.push(' ');
57
58 // Encode outputs based on cube type and output format
59 // With bool outputs: true = bit set in this cube, false = bit not set
60 match pla_type {
61 PLAType::F => {
62 // F-type: '1' for bit set, '0' for bit not set
63 for &out in cube.outputs.iter() {
64 output.push(if out { '1' } else { '0' });
65 }
66 }
67 PLAType::FD | PLAType::FDR | PLAType::FR => {
68 // Use cube_type to determine character for set/unset bits
69 let (set_char, unset_char) = match cube.cube_type {
70 CubeType::F => ('1', '~'), // F cube: 1=ON, ~=not in cube
71 CubeType::D => ('2', '~'), // D cube: 2=DC, ~=not in cube
72 CubeType::R => ('0', '~'), // R cube: 0=OFF, ~=not in cube
73 };
74
75 for &out in cube.outputs.iter() {
76 output.push(if out { set_char } else { unset_char });
77 }
78 }
79 }
80
81 output.push('\n');
82 }
83
84 // C version uses ".e" for F-type, ".end" for FD/FR/FDR types
85 match pla_type {
86 PLAType::F => output.push_str(".e\n"),
87 _ => output.push_str(".end\n"),
88 }
89 Ok(output)
90 }
91
92 /// Write this cover to a PLA file
93 fn to_pla_file<P: AsRef<Path>>(&self, path: P, pla_type: PLAType) -> io::Result<()> {
94 let content = self.to_pla_string(pla_type)?;
95 std::fs::write(path, content)
96 }
97}
98
99/// Parse a PLA format string into cube data
100///
101/// Returns (num_inputs, num_outputs, cubes, cover_type)
102pub(crate) fn parse_pla_content(content: &str) -> io::Result<(usize, usize, Vec<Cube>, PLAType)> {
103 let mut num_inputs: Option<usize> = None;
104 let mut num_outputs: Option<usize> = None;
105 let mut cubes = Vec::new();
106 // Default to FD_type to match C espresso behavior (main.c line 21)
107 // This causes '-' in outputs to be parsed as D cubes, not just don't-care bits
108 let mut pla_type = PLAType::FD;
109
110 let lines: Vec<&str> = content.lines().collect();
111 let mut i = 0;
112
113 while i < lines.len() {
114 let line = lines[i].trim();
115 i += 1;
116
117 // Skip empty lines and comments
118 if line.is_empty() || line.starts_with('#') {
119 continue;
120 }
121
122 // Parse directives
123 if line.starts_with('.') {
124 let parts: Vec<&str> = line.split_whitespace().collect();
125
126 match parts.first().copied() {
127 Some(".i") => {
128 let val: usize =
129 parts.get(1).and_then(|s| s.parse().ok()).ok_or_else(|| {
130 io::Error::new(io::ErrorKind::InvalidData, "Invalid .i directive")
131 })?;
132 num_inputs = Some(val);
133 }
134 Some(".o") => {
135 let val: usize =
136 parts.get(1).and_then(|s| s.parse().ok()).ok_or_else(|| {
137 io::Error::new(io::ErrorKind::InvalidData, "Invalid .o directive")
138 })?;
139 num_outputs = Some(val);
140 }
141 Some(".type") => {
142 if let Some(type_str) = parts.get(1) {
143 pla_type = match *type_str {
144 "f" => PLAType::F,
145 "fd" => PLAType::FD,
146 "fr" => PLAType::FR,
147 "fdr" => PLAType::FDR,
148 _ => PLAType::F,
149 };
150 }
151 }
152 Some(".e") => break,
153 Some(".ilb") | Some(".ob") | Some(".p") => {}
154 _ => {}
155 }
156 continue;
157 }
158
159 // Parse cube line(s) - supports both single-line and multi-line formats
160 // Some PLA files use | as separator between inputs and outputs
161 let (input_part, output_part) = if line.contains('|') {
162 let parts: Vec<&str> = line.splitn(2, '|').collect();
163 (
164 parts.first().copied().unwrap_or(""),
165 parts.get(1).copied().unwrap_or(""),
166 )
167 } else {
168 (line, "")
169 };
170
171 // Remove ALL whitespace to handle column-based formatting
172 // (e.g., files where inputs/outputs are formatted in columns with spaces)
173 let line_no_spaces: String = if !output_part.is_empty() {
174 // Format with |: remove spaces from each part separately
175 let inp = input_part
176 .chars()
177 .filter(|c| !c.is_whitespace())
178 .collect::<String>();
179 let out = output_part
180 .chars()
181 .filter(|c| !c.is_whitespace())
182 .collect::<String>();
183 format!("{}{}", inp, out)
184 } else {
185 // No |: remove all spaces from whole line
186 line.chars().filter(|c| !c.is_whitespace()).collect()
187 };
188
189 if line_no_spaces.is_empty() {
190 continue;
191 }
192
193 // Determine input and output strings based on declared dimensions
194 let (input_str, output_str) = if let (Some(ni), Some(no)) = (num_inputs, num_outputs) {
195 // We know the dimensions, so split at the boundary
196 if line_no_spaces.len() < ni + no {
197 // Line too short, might be continuation or malformed - try multi-line format
198 let parts: Vec<&str> = line.split_whitespace().collect();
199 if parts.is_empty() {
200 continue;
201 }
202
203 // Multi-line format: accumulate input lines, then get output line
204 let mut input_accumulator = parts[0].to_string();
205 let mut output_line = String::new();
206
207 // Look ahead to accumulate more input lines and find output
208 while i < lines.len() {
209 let next_line = lines[i].trim();
210
211 // Skip empty lines
212 if next_line.is_empty() || next_line.starts_with('#') {
213 i += 1;
214 continue;
215 }
216
217 // Stop at directives
218 if next_line.starts_with('.') {
219 break;
220 }
221
222 let next_parts: Vec<&str> = next_line.split_whitespace().collect();
223 if next_parts.is_empty() {
224 i += 1;
225 continue;
226 }
227
228 let part = next_parts[0];
229
230 // Check if this looks like an output line
231 // Output lines have exact length matching num_outputs and mostly 0/1/~
232 let is_output = part.len() == no;
233
234 if is_output {
235 output_line = part.to_string();
236 i += 1; // Consume this line
237 break;
238 } else {
239 // Accumulate more input
240 input_accumulator.push_str(part);
241 i += 1; // Consume this line
242 }
243 }
244
245 if output_line.is_empty() {
246 continue; // Skip malformed cubes
247 }
248
249 (input_accumulator, output_line)
250 } else {
251 // Line has enough characters - split at boundary
252 let (inp, out) = line_no_spaces.split_at(ni);
253 (inp.to_string(), out.to_string())
254 }
255 } else {
256 // Dimensions not yet known - use whitespace splitting as before
257 let parts: Vec<&str> = line.split_whitespace().collect();
258 if parts.len() < 2 {
259 continue; // Need at least inputs and outputs
260 }
261 (parts[0].to_string(), parts[1].to_string())
262 };
263
264 // Infer dimensions from first cube if not specified
265 if num_inputs.is_none() {
266 num_inputs = Some(input_str.len());
267 }
268 if num_outputs.is_none() {
269 num_outputs = Some(output_str.len());
270 }
271
272 let ni = num_inputs.unwrap();
273 let no = num_outputs.unwrap();
274
275 // Verify dimensions are consistent
276 if input_str.len() != ni || output_str.len() != no {
277 // Skip cubes with wrong dimensions (might be intermediate lines)
278 continue;
279 }
280
281 // Parse inputs
282 let mut inputs = Vec::with_capacity(ni);
283 for ch in input_str.chars() {
284 inputs.push(match ch {
285 '0' => Some(false),
286 '1' => Some(true),
287 '-' | '~' | 'x' | 'X' => None,
288 _ => {
289 return Err(io::Error::new(
290 io::ErrorKind::InvalidData,
291 format!("Invalid input character: '{}'", ch),
292 ))
293 }
294 });
295 }
296
297 // Parse outputs following Espresso C convention (cvrin.c lines 176-199)
298 // The C code creates separate F, D, R cubes from a single line:
299 // - '1' or '4' → bit set in F cube
300 // - '0' or '3' → bit set in R cube
301 // - '-' or '2' → bit set in D cube (if pla_type includes D_type)
302 // - '~' → does NOTHING (cvrin.c line 190: just breaks)
303 //
304 // Simplified: outputs are Vec<bool> where true = bit set in this cube
305 let mut f_outputs = Vec::with_capacity(no);
306 let mut d_outputs = Vec::with_capacity(no);
307 let mut r_outputs = Vec::with_capacity(no);
308 let mut has_f = false;
309 let mut has_d = false;
310 let mut has_r = false;
311
312 for ch in output_str.chars() {
313 match ch {
314 '1' | '4' if pla_type.has_f() => {
315 f_outputs.push(true); // Bit set in F cube
316 d_outputs.push(false); // Not in D cube
317 r_outputs.push(false); // Not in R cube
318 has_f = true;
319 }
320 '0' | '3' if pla_type.has_r() => {
321 f_outputs.push(false); // Not in F cube
322 d_outputs.push(false); // Not in D cube
323 r_outputs.push(true); // Bit set in R cube
324 has_r = true;
325 }
326 '-' | '2' if pla_type.has_d() => {
327 // Only '-' and '2' create D cubes, NOT '~'
328 f_outputs.push(false); // Not in F cube
329 d_outputs.push(true); // Bit set in D cube
330 r_outputs.push(false); // Not in R cube
331 has_d = true;
332 }
333 '~' | '-' | '2' => {
334 // '~' does nothing (C code line 190)
335 // If '-' or '2' but D_type not set, also do nothing
336 f_outputs.push(false);
337 d_outputs.push(false);
338 r_outputs.push(false);
339 }
340 '1' | '4' | '0' | '3' => {
341 // Type flag not set, don't set bits
342 f_outputs.push(false);
343 d_outputs.push(false);
344 r_outputs.push(false);
345 }
346 _ => {
347 return Err(io::Error::new(
348 io::ErrorKind::InvalidData,
349 format!("Invalid output character: '{}'", ch),
350 ))
351 }
352 }
353 }
354
355 // Add cubes only if they have meaningful outputs
356 if has_f {
357 cubes.push(Cube::new(inputs.clone(), f_outputs, CubeType::F));
358 }
359 if has_d {
360 cubes.push(Cube::new(inputs.clone(), d_outputs, CubeType::D));
361 }
362 if has_r {
363 cubes.push(Cube::new(inputs, r_outputs, CubeType::R));
364 }
365 }
366
367 // Verify we got dimensions
368 let ni = num_inputs.ok_or_else(|| {
369 io::Error::new(
370 io::ErrorKind::InvalidData,
371 "PLA file missing .i directive and no cubes to infer from",
372 )
373 })?;
374 let no = num_outputs.ok_or_else(|| {
375 io::Error::new(
376 io::ErrorKind::InvalidData,
377 "PLA file missing .o directive and no cubes to infer from",
378 )
379 })?;
380
381 Ok((ni, no, cubes, pla_type))
382}
383
384// ============================================================================
385// PLACover - Dynamic Cover for PLA Files
386// ============================================================================
387
388/// A cover with dynamic dimensions (from PLA files)
389///
390/// Use this when loading PLA files where dimensions are not known at compile time.
391/// Outputs are `Option<bool>`: Some(true)=1, Some(false)=0, None=don't-care
392#[derive(Clone)]
393pub struct PLACover {
394 num_inputs: usize,
395 num_outputs: usize,
396 /// Cubes with their type (F/D/R) and data
397 cubes: Vec<Cube>,
398 /// Cover type (F, FD, FR, or FDR)
399 cover_type: PLAType,
400}
401
402impl PLACover {
403 /// Create a new empty cover with specified dimensions
404 pub fn new(num_inputs: usize, num_outputs: usize) -> Self {
405 PLACover {
406 num_inputs,
407 num_outputs,
408 cubes: Vec::new(),
409 cover_type: PLAType::F,
410 }
411 }
412
413 /// Load a cover from a PLA format file
414 ///
415 /// The dimensions are determined from the PLA file.
416 pub fn from_pla_file<P: AsRef<Path>>(path: P) -> io::Result<Self> {
417 let content = std::fs::read_to_string(path)?;
418 Self::from_pla_content(&content)
419 }
420
421 /// Load a cover from PLA format string
422 ///
423 /// The dimensions are determined from the PLA content.
424 pub fn from_pla_content(content: &str) -> io::Result<Self> {
425 let (num_inputs, num_outputs, cubes, cover_type) = parse_pla_content(content)?;
426
427 Ok(PLACover {
428 num_inputs,
429 num_outputs,
430 cubes,
431 cover_type,
432 })
433 }
434}
435
436// Implement Minimizable for PLACover (Cover trait is auto-implemented via blanket impl)
437impl Minimizable for PLACover {
438 fn num_inputs(&self) -> usize {
439 self.num_inputs
440 }
441
442 fn num_outputs(&self) -> usize {
443 self.num_outputs
444 }
445
446 fn cover_type(&self) -> PLAType {
447 self.cover_type
448 }
449
450 fn internal_cubes_iter<'a>(&'a self) -> Box<dyn Iterator<Item = &'a Cube> + 'a> {
451 Box::new(self.cubes.iter())
452 }
453
454 fn set_cubes(&mut self, cubes: Vec<Cube>) {
455 self.cubes = cubes;
456 }
457}
458
459// Implement PLASerializable for PLACover
460impl PLASerializable for PLACover {}