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 {}