oxidd_parser/
load_file.rs

1//! Convenience functions etc. to load a [`Problem`] from file
2
3// spell-checker:ignore termcolor
4
5use std::fmt;
6use std::path::Path;
7
8use codespan_reporting::diagnostic::{Diagnostic, Label};
9use codespan_reporting::files::SimpleFile;
10use codespan_reporting::term::termcolor::ColorChoice;
11use codespan_reporting::term::termcolor::{StandardStream, WriteColor};
12use codespan_reporting::term::{emit, Config};
13use nom::error::{ContextError, ErrorKind, FromExternalError, ParseError};
14use nom::Offset;
15
16use crate::ParseOptions;
17use crate::Problem;
18use crate::{aiger, dimacs, nnf};
19
20/// File types
21///
22/// Every file type is associated with a parser and (possibly multiple)
23/// extensions
24#[non_exhaustive]
25#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
26pub enum FileType {
27    /// DIMACS CNF/SAT formats
28    ///
29    /// Extensions: `.cnf`, `.sat`, `.dimacs`
30    DIMACS,
31    /// c2d negation normal form
32    ///
33    /// Extensions: `.nnf`
34    NNF,
35    /// AIGER
36    ///
37    /// Extensions: `.aag`, `.aig`
38    AIGER,
39}
40
41impl FileType {
42    /// Guess the file type based on the given path
43    pub fn from_path(path: &Path) -> Option<Self> {
44        let ext = path.extension()?;
45        match ext.as_encoded_bytes() {
46            b"cnf" | b"sat" | b"dimacs" => Some(FileType::DIMACS),
47            b"nnf" => Some(FileType::NNF),
48            b"aag" | b"aig" => Some(FileType::AIGER),
49            _ => None,
50        }
51    }
52}
53
54struct ParserReport<I>(Vec<(I, ParserError)>);
55
56enum ParserError {
57    Nom(ErrorKind),
58    Char(char),
59    Context(&'static str),
60    External(String),
61}
62
63impl<I> ParseError<I> for ParserReport<I> {
64    fn from_error_kind(input: I, kind: ErrorKind) -> Self {
65        ParserReport(vec![(input, ParserError::Nom(kind))])
66    }
67
68    fn append(input: I, kind: ErrorKind, mut other: Self) -> Self {
69        other.0.push((input, ParserError::Nom(kind)));
70        other
71    }
72
73    fn from_char(input: I, c: char) -> Self {
74        ParserReport(vec![(input, ParserError::Char(c))])
75    }
76}
77
78impl<I> ContextError<I> for ParserReport<I> {
79    fn add_context(input: I, ctx: &'static str, mut other: Self) -> Self {
80        match other.0[0].1 {
81            ParserError::Context(_) => {}
82            // Assume that the context is a better description
83            _ => other.0.clear(),
84        }
85        other.0.push((input, ParserError::Context(ctx)));
86        other
87    }
88}
89
90impl<I, S: ToString> FromExternalError<I, S> for ParserReport<I> {
91    fn from_external_error(input: I, _kind: ErrorKind, e: S) -> Self {
92        Self(vec![(input, ParserError::External(e.to_string()))])
93    }
94}
95
96/// Parse `input` using the parser for the given `file_type`, emitting errors or
97/// warnings to `writer`
98///
99/// `file_id` is an identifier for the file used for error reporting. `config`
100/// configures how diagnostics are rendered.
101///
102/// If you simply want to parse a file with error reporting to stderr, you are
103/// probably looking for [`load_file()`].
104pub fn parse<S: AsRef<str> + Clone + fmt::Display>(
105    input: &[u8],
106    file_type: FileType,
107    parse_options: &ParseOptions,
108    file_id: S,
109    writer: &mut dyn WriteColor,
110    config: &Config,
111) -> Option<Problem> {
112    let parse_result = match file_type {
113        FileType::DIMACS => dimacs::parse::<ParserReport<_>>(parse_options)(input),
114        FileType::NNF => nnf::parse::<ParserReport<_>>(parse_options)(input),
115        FileType::AIGER => aiger::parse::<ParserReport<_>>(parse_options)(input),
116    };
117    let errors = match parse_result {
118        Ok((rest, problem)) => {
119            debug_assert!(rest.is_empty());
120            return Some(problem);
121        }
122        Err(nom::Err::Error(e) | nom::Err::Failure(e)) => e.0,
123        Err(nom::Err::Incomplete(_)) => unreachable!("only using complete parsers"),
124    };
125
126    let range = move |span: &[u8]| {
127        let offset = input.offset(span);
128        let end = offset + span.len();
129        if end >= input.len() {
130            offset..offset
131        } else {
132            offset..end
133        }
134    };
135
136    let mut labels = Vec::with_capacity(errors.len());
137    let mut errors = errors.into_iter().map(|(span, err)| {
138        (
139            span,
140            match err {
141                ParserError::Nom(e) => format!("Expected {}", e.description()),
142                ParserError::Char(c) => format!("Expected '{c}'"),
143                ParserError::Context(msg) => msg.to_string(),
144                ParserError::External(s) => s,
145            },
146        )
147    });
148    let (span, error) = errors.next().unwrap();
149    labels.push(Label::primary((), range(span)).with_message(error.to_string()));
150    for (span, error) in errors {
151        labels.push(Label::secondary((), range(span)).with_message(error.to_string()));
152    }
153
154    let diagnostic = Diagnostic::error()
155        .with_message("parsing failed")
156        .with_labels(labels);
157
158    let file = SimpleFile::new(file_id, String::from_utf8_lossy(input));
159    emit(writer, config, &file, &diagnostic).ok();
160
161    None
162}
163
164/// Load and parse the file at `path`, reporting errors to stderr
165///
166/// Returns `Some(problem)` on success, `None` on error.
167pub fn load_file(path: impl AsRef<Path>, options: &ParseOptions) -> Option<Problem> {
168    let path = path.as_ref();
169    let file_type = match FileType::from_path(path) {
170        Some(t) => t,
171        None => {
172            eprintln!(
173                "error: could not determine file type of '{}'",
174                path.display()
175            );
176            return None;
177        }
178    };
179
180    let src = match std::fs::read(path) {
181        Ok(src) => src,
182        Err(err) => {
183            eprintln!("error: could not read '{}' ({err})", path.display());
184            return None;
185        }
186    };
187
188    let config = codespan_reporting::term::Config::default();
189    let writer = StandardStream::stderr(ColorChoice::Auto);
190    let mut write_lock = writer.lock();
191
192    parse(
193        &src,
194        file_type,
195        options,
196        path.to_string_lossy(),
197        &mut write_lock,
198        &config,
199    )
200}