Skip to main content

lrpar/
ctbuilder.rs

1//! Build grammars at compile-time so that they can be statically included into a binary.
2
3use std::{
4    collections::{HashMap, HashSet},
5    env::{current_dir, var},
6    error::Error,
7    fmt::{self, Debug, Write as fmtWrite},
8    fs::{self, File, create_dir_all, read_to_string},
9    hash::Hash,
10    io::Write,
11    marker::PhantomData,
12    path::{Path, PathBuf},
13    sync::{LazyLock, Mutex},
14};
15
16use crate::{
17    LexerTypes, RTParserBuilder, RecoveryKind,
18    codegen::{
19        ParserBuildEnv, ParserBuildEnvArgs, ParserBuildEnvError, ParserCodegen, ParserSrcEnv,
20        ParserSrcEnvError,
21    },
22    diagnostics::{DiagnosticFormatter, SpannedDiagnosticFormatter},
23};
24
25#[cfg(feature = "_unstable_api")]
26use crate::unstable_api::UnstableApi;
27
28use cfgrammar::{
29    Location,
30    header::{Header, HeaderError, HeaderErrorKind, HeaderValue, Namespaced, Setting, Value},
31    markmap::{Entry, MergeBehavior},
32    yacc::{YaccGrammar, YaccKind, ast::ASTWithValidityInfo},
33};
34use filetime::FileTime;
35use lrtable::{StateGraph, StateTable, statetable::Conflicts};
36use num_traits::{AsPrimitive, PrimInt, Unsigned};
37use wincode::{SchemaRead, SchemaReadOwned, SchemaWrite};
38
39const RUST_FILE_EXT: &str = "rs";
40const WARNING: &str = "[Warning]";
41pub(crate) const ERROR: &str = "[Error]";
42
43static GENERATED_PATHS: LazyLock<Mutex<HashSet<PathBuf>>> =
44    LazyLock::new(|| Mutex::new(HashSet::new()));
45
46pub(crate) struct CTConflictsError<StorageT: Eq + Hash> {
47    pub(crate) conflicts_diagnostic: String,
48    #[cfg(test)]
49    #[cfg_attr(test, allow(dead_code))]
50    pub(crate) stable: StateTable<StorageT>,
51    pub(crate) phantom: PhantomData<StorageT>,
52}
53
54impl<StorageT> fmt::Display for CTConflictsError<StorageT>
55where
56    StorageT: 'static + Debug + Hash + PrimInt + Unsigned,
57    usize: AsPrimitive<StorageT>,
58{
59    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60        write!(f, "{}", self.conflicts_diagnostic)
61    }
62}
63
64impl<StorageT> fmt::Debug for CTConflictsError<StorageT>
65where
66    StorageT: 'static + Debug + Hash + PrimInt + Unsigned,
67    usize: AsPrimitive<StorageT>,
68{
69    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70        write!(f, "{}", self.conflicts_diagnostic)
71    }
72}
73
74impl<StorageT> Error for CTConflictsError<StorageT>
75where
76    StorageT: 'static + Debug + Hash + PrimInt + Unsigned,
77    usize: AsPrimitive<StorageT>,
78{
79}
80
81/// A string which uses `Display` for it's `Debug` impl.
82struct ErrorString(String);
83impl fmt::Display for ErrorString {
84    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
85        let ErrorString(s) = self;
86        write!(f, "{}", s)
87    }
88}
89impl fmt::Debug for ErrorString {
90    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
91        let ErrorString(s) = self;
92        write!(f, "{}", s)
93    }
94}
95impl Error for ErrorString {}
96
97/// Specify the visibility of the module generated by `CTBuilder`.
98#[derive(Clone, PartialEq, Eq, Debug)]
99#[non_exhaustive]
100pub enum Visibility {
101    /// Module-level visibility only.
102    Private,
103    /// `pub`
104    Public,
105    /// `pub(super)`
106    PublicSuper,
107    /// `pub(self)`
108    PublicSelf,
109    /// `pub(crate)`
110    PublicCrate,
111    /// `pub(in {arg})`
112    PublicIn(String),
113}
114
115/// Specifies the [Rust Edition] that will be emitted during code generation.
116///
117/// [Rust Edition]: https://doc.rust-lang.org/edition-guide/rust-2021/index.html
118#[derive(Clone, Copy, PartialEq, Eq, Debug)]
119#[non_exhaustive]
120pub enum RustEdition {
121    Rust2015,
122    Rust2018,
123    Rust2021,
124}
125
126/// Sets the underlying encoding algorithm for serialising the `ParserData` into the generated source files.
127///
128/// This correlates to a specific `Configuration` of [wincode::config](https://docs.rs/wincode/latest/wincode/config/index.html).
129#[non_exhaustive]
130#[derive(SchemaRead, SchemaWrite, Debug, Clone, Copy)]
131pub enum SerialisationFormat {
132    /// See [wincode::FixedInt](https://docs.rs/wincode/latest/wincode/int_encoding/struct.FixedInt.html)
133    FixedSizeInteger,
134    /// See [wincode::VarInt](https://docs.rs/wincode/latest/wincode/int_encoding/struct.VarInt.html)
135    VariableSizedInteger,
136}
137
138impl TryFrom<SerialisationFormat> for Value<Location> {
139    type Error = cfgrammar::header::HeaderError<Location>;
140    fn try_from(kind: SerialisationFormat) -> Result<Value<Location>, HeaderError<Location>> {
141        let from_loc = Location::Other("From<SerialisationFormat>".to_string());
142        Ok(match kind {
143            SerialisationFormat::FixedSizeInteger => Value::Setting(Setting::Unitary(Namespaced {
144                namespace: Some(("serialisationformat".to_string(), from_loc.clone())),
145                member: ("fixedsizeinteger".to_string(), from_loc),
146            })),
147            SerialisationFormat::VariableSizedInteger => {
148                Value::Setting(Setting::Unitary(Namespaced {
149                    namespace: Some(("serialisationformat".to_string(), from_loc.clone())),
150                    member: ("variablesizedinteger".to_string(), from_loc),
151                }))
152            }
153        })
154    }
155}
156
157impl<T: Clone + Debug> TryFrom<&Value<T>> for SerialisationFormat {
158    type Error = HeaderError<T>;
159    fn try_from(value: &Value<T>) -> Result<SerialisationFormat, HeaderError<T>> {
160        let mut err_locs = Vec::new();
161        match value {
162            // Finally handle enum values.
163            Value::Setting(Setting::Unitary(Namespaced {
164                namespace,
165                member: (enc_value, enc_value_loc),
166            })) => {
167                if let Some((ns, ns_loc)) = namespace
168                    && ns != "serialisationformat"
169                {
170                    err_locs.push(ns_loc.clone());
171                }
172                let encodings = [
173                    (
174                        "fixedsizeinteger".to_string(),
175                        SerialisationFormat::FixedSizeInteger,
176                    ),
177                    (
178                        "variablesizedinteger".to_string(),
179                        SerialisationFormat::VariableSizedInteger,
180                    ),
181                ];
182                let enc_found = encodings
183                    .iter()
184                    .find_map(|(enc_str, enc)| (enc_str == enc_value).then_some(enc));
185                if let Some(enc) = enc_found {
186                    if err_locs.is_empty() {
187                        Ok(*enc)
188                    } else {
189                        Err(HeaderError {
190                            kind: HeaderErrorKind::InvalidEntry("serialisation_format"),
191                            locations: err_locs,
192                        })
193                    }
194                } else {
195                    err_locs.push(enc_value_loc.clone());
196                    Err(HeaderError {
197                        kind: HeaderErrorKind::InvalidEntry("serialisation_format"),
198                        locations: err_locs,
199                    })
200                }
201            }
202            val => {
203                err_locs.push(val.primary_location().clone());
204                Err(HeaderError {
205                    kind: HeaderErrorKind::InvalidEntry("serialisation_format"),
206                    locations: err_locs,
207                })
208            }
209        }
210    }
211}
212
213// We export this for generated code to refer to.
214#[doc(hidden)]
215pub use wincode;
216
217/// A `CTParserBuilder` allows one to specify the criteria for building a statically generated
218/// parser.
219pub struct CTParserBuilder<'a, LexerTypesT: LexerTypes>
220where
221    LexerTypesT::StorageT: Eq + Hash,
222    usize: AsPrimitive<LexerTypesT::StorageT>,
223{
224    // Anything stored in here (except `output_path`, `conflicts`, and `error_on_conflict`) almost
225    // certainly needs to be included as part of the rebuild_cache function below so that, if it's
226    // changed, the grammar is rebuilt.
227    grammar_path: Option<PathBuf>,
228    // If specified rather than reading source from `grammar_path`, use this string directly
229    grammar_src: Option<String>,
230    // If specified along with `grammar_src`, use this rather than building an ast from `grammar_src`.
231    from_ast: Option<ASTWithValidityInfo>,
232    output_path: Option<PathBuf>,
233    mod_name: Option<&'a str>,
234    recoverer: Option<RecoveryKind>,
235    yacckind: Option<YaccKind>,
236    error_on_conflicts: bool,
237    warnings_are_errors: bool,
238    show_warnings: bool,
239    visibility: Visibility,
240    rust_edition: RustEdition,
241    inspect_rt: Option<
242        Box<
243            dyn for<'b> FnMut(
244                &'b mut Header<Location>,
245                RTParserBuilder<LexerTypesT::StorageT, LexerTypesT>,
246                &'b HashMap<String, LexerTypesT::StorageT>,
247                &PathBuf,
248            ) -> Result<(), Box<dyn Error>>,
249        >,
250    >,
251    serialisation_format: Option<SerialisationFormat>,
252    // test function for inspecting private state
253    #[cfg(test)]
254    inspect_callback: Option<Box<dyn Fn(RecoveryKind) -> Result<(), Box<dyn Error>>>>,
255    phantom: PhantomData<LexerTypesT>,
256}
257
258/// Defaults to `wincode::int_encoding::VarInt`.
259pub(crate) type FixIntConfig = wincode::config::Configuration;
260/// The default config with the last parameter set to `VarInt`
261pub(crate) type VarIntConfig = wincode::config::Configuration<
262    true,
263    4194304,
264    wincode::len::BincodeLen,
265    wincode::int_encoding::LittleEndian,
266    wincode::int_encoding::VarInt,
267>;
268
269impl<
270    'a,
271    StorageT: 'static
272        + Debug
273        + Hash
274        + PrimInt
275        + SchemaWrite<FixIntConfig, Src = StorageT>
276        + SchemaWrite<VarIntConfig, Src = StorageT>
277        + Unsigned,
278    LexerTypesT: LexerTypes<StorageT = StorageT>,
279> CTParserBuilder<'a, LexerTypesT>
280where
281    usize: AsPrimitive<StorageT>,
282{
283    /// Create a new `CTParserBuilder`.
284    ///
285    /// `StorageT` must be an unsigned integer type (e.g. `u8`, `u16`) which is:
286    ///   * big enough to index (separately) all the tokens, rules, productions in the grammar,
287    ///   * big enough to index the state table created from the grammar,
288    ///   * less than or equal in size to `u32`.
289    ///
290    /// In other words, if you have a grammar with 256 tokens, 256 rules, and 256 productions,
291    /// which creates a state table of 256 states you can safely specify `u8` here; but if any of
292    /// those counts becomes 257 or greater you will need to specify `u16`. If you are parsing
293    /// large files, the additional storage requirements of larger integer types can be noticeable,
294    /// and in such cases it can be worth specifying a smaller type. `StorageT` defaults to `u32`
295    /// if unspecified.
296    ///
297    /// # Examples
298    ///
299    /// ```text
300    /// CTParserBuilder::<DefaultLexerTypes<u8>>::new()
301    ///     .grammar_in_src_dir("grm.y")?
302    ///     .build()?;
303    /// ```
304    pub fn new() -> Self {
305        CTParserBuilder {
306            grammar_path: None,
307            grammar_src: None,
308            from_ast: None,
309            output_path: None,
310            mod_name: None,
311            recoverer: None,
312            yacckind: None,
313            error_on_conflicts: true,
314            warnings_are_errors: true,
315            show_warnings: true,
316            visibility: Visibility::Private,
317            rust_edition: RustEdition::Rust2021,
318            inspect_rt: None,
319            serialisation_format: None,
320            #[cfg(test)]
321            inspect_callback: None,
322            phantom: PhantomData,
323        }
324    }
325
326    /// Set the input grammar path to a file relative to this project's `src` directory. This will
327    /// also set the output path (i.e. you do not need to call [CTParserBuilder::output_path]).
328    ///
329    /// For example if `a/b.y` is passed as `inp` then [CTParserBuilder::build] will:
330    ///   * use `src/a/b.y` as the input file.
331    ///   * write output to a file which can then be imported by calling `lrpar_mod!("a/b.y")`.
332    ///   * create a module in that output file named `b_y`.
333    ///
334    /// You can override the output path and/or module name by calling [CTParserBuilder::output_path]
335    /// and/or [CTParserBuilder::mod_name], respectively, after calling this function.
336    ///
337    /// This is a convenience function that makes it easier to compile grammar files stored in a
338    /// project's `src/` directory: please see [CTParserBuilder::build] for additional constraints
339    /// and information about the generated files. Note also that each `.y` file can only be
340    /// processed once using this function: if you want to generate multiple grammars from a single
341    /// `.y` file, you will need to use [CTParserBuilder::output_path].
342    pub fn grammar_in_src_dir<P>(mut self, srcp: P) -> Result<Self, Box<dyn Error>>
343    where
344        P: AsRef<Path>,
345    {
346        if !srcp.as_ref().is_relative() {
347            return Err(format!(
348                "Grammar path '{}' must be a relative path.",
349                srcp.as_ref().to_str().unwrap_or("<invalid UTF-8>")
350            )
351            .into());
352        }
353
354        let mut grmp = current_dir()?;
355        grmp.push("src");
356        grmp.push(srcp.as_ref());
357        self.grammar_path = Some(grmp);
358
359        let mut outp = PathBuf::new();
360        outp.push(var("OUT_DIR").unwrap());
361        outp.push(srcp.as_ref().parent().unwrap().to_str().unwrap());
362        create_dir_all(&outp)?;
363        let mut leaf = srcp
364            .as_ref()
365            .file_name()
366            .unwrap()
367            .to_str()
368            .unwrap()
369            .to_owned();
370        write!(leaf, ".{}", RUST_FILE_EXT).ok();
371        outp.push(leaf);
372        Ok(self.output_path(outp))
373    }
374
375    /// If set, specifies that this grammar should be built from a pre-validated AST
376    /// instead of a `.y`` file. When this is specified, `grammar_path` will not be read.
377    #[cfg(feature = "_unstable_api")]
378    pub fn grammar_ast(mut self, valid_ast: ASTWithValidityInfo, _api_key: UnstableApi) -> Self {
379        self.from_ast = Some(valid_ast);
380        self
381    }
382
383    /// Set the input grammar path to `inp`. If specified, you must also call
384    /// [CTParserBuilder::output_path]. In general it is easier to use
385    /// [CTParserBuilder::grammar_in_src_dir].
386    pub fn grammar_path<P>(mut self, inp: P) -> Self
387    where
388        P: AsRef<Path>,
389    {
390        self.grammar_path = Some(inp.as_ref().to_owned());
391        self
392    }
393
394    #[cfg(feature = "_unstable_api")]
395    pub fn with_grammar_src(mut self, src: String, _api_key: UnstableApi) -> Self {
396        self.grammar_src = Some(src);
397        self
398    }
399
400    /// Set the output grammar path to `outp`. Note that there are no requirements on `outp`: the
401    /// file can exist anywhere you can create a valid [Path] to. However, if you wish to use
402    /// [crate::lrpar_mod!] you will need to make sure that `outp` is in
403    /// [std::env::var]`("OUT_DIR")` or one of its subdirectories.
404    pub fn output_path<P>(mut self, outp: P) -> Self
405    where
406        P: AsRef<Path>,
407    {
408        self.output_path = Some(outp.as_ref().to_owned());
409        self
410    }
411
412    /// Set the generated module name to `mod_name`. If no module name is specified,
413    /// [CTParserBuilder::build] will attempt to create a sensible default based on the grammar
414    /// filename.
415    pub fn mod_name(mut self, mod_name: &'a str) -> Self {
416        self.mod_name = Some(mod_name);
417        self
418    }
419
420    /// Set the visibility of the generated module to `vis`. Defaults to `Visibility::Private`.
421    pub fn visibility(mut self, vis: Visibility) -> Self {
422        self.visibility = vis;
423        self
424    }
425
426    /// Set the recoverer for this parser to `rk`. Defaults to `RecoveryKind::CPCTPlus`.
427    pub fn recoverer(mut self, rk: RecoveryKind) -> Self {
428        self.recoverer = Some(rk);
429        self
430    }
431
432    /// Set the `YaccKind` for this parser to `ak`.
433    pub fn yacckind(mut self, yk: YaccKind) -> Self {
434        self.yacckind = Some(yk);
435        self
436    }
437
438    /// If set to true, [CTParserBuilder::build] will return an error if the given grammar contains
439    /// any Shift/Reduce or Reduce/Reduce conflicts. Defaults to `true`.
440    pub fn error_on_conflicts(mut self, b: bool) -> Self {
441        self.error_on_conflicts = b;
442        self
443    }
444
445    /// If set to true, [CTParserBuilder::build] will return an error if the given grammar contains
446    /// any warnings. Defaults to `true`.
447    pub fn warnings_are_errors(mut self, b: bool) -> Self {
448        self.warnings_are_errors = b;
449        self
450    }
451
452    /// If set to true, [CTParserBuilder::build] will print warnings to stderr, or via cargo when
453    /// running under cargo. Defaults to `true`.
454    pub fn show_warnings(mut self, b: bool) -> Self {
455        self.show_warnings = b;
456        self
457    }
458
459    /// Sets the rust edition to be used for generated code. Defaults to the latest edition of
460    /// rust supported by grmtools.
461    pub fn rust_edition(mut self, edition: RustEdition) -> Self {
462        self.rust_edition = edition;
463        self
464    }
465
466    pub fn serialisation_format(mut self, serialisation_format: SerialisationFormat) -> Self {
467        self.serialisation_format = Some(serialisation_format);
468        self
469    }
470
471    #[cfg(test)]
472    pub fn inspect_recoverer(
473        mut self,
474        cb: Box<dyn for<'h, 'y> Fn(RecoveryKind) -> Result<(), Box<dyn Error>>>,
475    ) -> Self {
476        self.inspect_callback = Some(cb);
477        self
478    }
479
480    #[doc(hidden)]
481    pub fn inspect_rt(
482        mut self,
483        cb: Box<
484            dyn for<'b, 'y> FnMut(
485                &'b mut Header<Location>,
486                RTParserBuilder<'y, StorageT, LexerTypesT>,
487                &'b HashMap<String, StorageT>,
488                &PathBuf,
489            ) -> Result<(), Box<dyn Error>>,
490        >,
491    ) -> Self {
492        self.inspect_rt = Some(cb);
493        self
494    }
495
496    /// Statically compile the Yacc file specified by [CTParserBuilder::grammar_path()] into Rust,
497    /// placing the output into the file spec [CTParserBuilder::output_path()]. Note that three
498    /// additional files will be created with the same name as specified in [self.output_path] but
499    /// with the extensions `grm`, and `stable`, overwriting any existing files with those names.
500    ///
501    /// If `%parse-param` is not specified, the generated module follows the form:
502    ///
503    /// ```text
504    ///   mod <modname> {
505    ///     pub fn parse<'lexer, 'input: 'lexer>(lexer: &'lexer dyn NonStreamingLexer<...>)
506    ///       -> (Option<ActionT>, Vec<LexParseError<...>> { ... }
507    ///
508    ///     pub fn token_epp<'a>(tidx: ::cfgrammar::TIdx<StorageT>) -> ::std::option::Option<&'a str> {
509    ///       ...
510    ///     }
511    ///
512    ///     ...
513    ///   }
514    /// ```
515    ///
516    /// If `%parse-param x: t` is specified, the generated module follows the form:
517    ///
518    /// ```text
519    ///   mod <modname> {
520    ///     pub fn parse<'lexer, 'input: 'lexer>(lexer: &'lexer dyn NonStreamingLexer<...>, x: t)
521    ///       -> (Option<ActionT>, Vec<LexParseError<...>> { ... }
522    ///
523    ///     pub fn token_epp<'a>(tidx: ::cfgrammar::TIdx<StorageT>) -> ::std::option::Option<&'a str> {
524    ///       ...
525    ///     }
526    ///
527    ///     ...
528    ///   }
529    /// ```
530    ///
531    /// where:
532    ///  * `modname` is either:
533    ///    * the module name specified by [CTParserBuilder::mod_name()];
534    ///    * or, if no module name was explicitly specified, then for the file `/a/b/c.y` the
535    ///      module name is `c_y` (i.e. the file's leaf name, minus its extension, with a prefix of
536    ///      `_y`).
537    ///  * `ActionT` is either:
538    ///    * if the `yacckind` was set to `YaccKind::GrmTools` or
539    ///      `YaccKind::Original(YaccOriginalActionKind::UserAction)`, it is
540    ///      the return type of the `%start` rule;
541    ///    * or, if the `yacckind` was set to
542    ///      `YaccKind::Original(YaccOriginalActionKind::GenericParseTree)`, it
543    ///      is `Node<StorageT>` where the `Node` type is defined within your `lrpar_mod!`.
544    ///
545    /// # Panics
546    ///
547    /// If `StorageT` is not big enough to index the grammar's tokens, rules, or productions.
548    pub fn build(mut self) -> Result<CTParser<StorageT>, Box<dyn Error>> {
549        let grmp = self
550            .grammar_path
551            .as_ref()
552            .expect("grammar_path must be specified before processing.");
553        let outp = self
554            .output_path
555            .as_ref()
556            .expect("output_path must be specified before processing.");
557        let mut header = Header::new();
558
559        match header.entry("cfgrammar.yacckind".to_string()) {
560            Entry::Occupied(_) => unreachable!(),
561            Entry::Vacant(mut v) => match self.yacckind {
562                Some(YaccKind::Eco) => panic!("Eco compile-time grammar generation not supported."),
563                Some(yk) => {
564                    let yk_value = Value::try_from(yk)?;
565                    let mut o = v.insert_entry(HeaderValue(
566                        Location::Other("CTParserBuilder".to_string()),
567                        yk_value,
568                    ));
569                    o.set_merge_behavior(MergeBehavior::Ours);
570                }
571                None => {
572                    v.mark_required();
573                }
574            },
575        }
576        if let Some(recoverer) = self.recoverer {
577            match header.entry("lrpar.recoverer".to_string()) {
578                Entry::Occupied(_) => unreachable!(),
579                Entry::Vacant(v) => {
580                    let rk_value: Value<Location> = Value::try_from(recoverer)?;
581                    let mut o = v.insert_entry(HeaderValue(
582                        Location::Other("CTParserBuilder".to_string()),
583                        rk_value,
584                    ));
585                    o.set_merge_behavior(MergeBehavior::Ours);
586                }
587            }
588        }
589
590        if let Some(encoding) = self.serialisation_format {
591            match header.entry("lrpar.serialisation_format".to_string()) {
592                Entry::Occupied(_) => unreachable!(),
593                Entry::Vacant(v) => {
594                    let rk_value: Value<Location> = Value::try_from(encoding)?;
595                    let mut o = v.insert_entry(HeaderValue(
596                        Location::Other("CTParserBuilder".to_string()),
597                        rk_value,
598                    ));
599                    o.set_merge_behavior(MergeBehavior::Ours);
600                }
601            }
602        }
603
604        {
605            let mut lk = GENERATED_PATHS.lock().unwrap();
606            if lk.contains(outp.as_path()) {
607                return Err(format!("Generating two parsers to the same path ('{}') is not allowed: use CTParserBuilder::output_path (and, optionally, CTParserBuilder::mod_name) to differentiate them.", outp.to_str().unwrap()).into());
608            }
609            lk.insert(outp.clone());
610        }
611
612        let inc = if let Some(grammar_src) = &self.grammar_src {
613            grammar_src.clone()
614        } else {
615            read_to_string(grmp).map_err(|e| format!("When reading '{}': {e}", grmp.display()))?
616        };
617
618        let src_env = ParserSrcEnv::new_with_header(&inc, Some(grmp), header);
619        let yacc_diag = SpannedDiagnosticFormatter::new(&inc, grmp);
620        let build_args = ParserBuildEnvArgs::new()
621            .ast_with_validity_info(self.from_ast.as_ref())
622            .mod_name(self.mod_name)
623            .show_warnings(self.show_warnings)
624            .error_on_conflicts(self.error_on_conflicts)
625            .warnings_are_errors(self.warnings_are_errors)
626            .visibility(self.visibility.clone())
627            .rust_edition(self.rust_edition);
628        let mut build_env = src_env.build_env(build_args).map_err(|e| match e {
629            ParserSrcEnvError::GrmtoolsSectionParseError(es) => {
630                let mut out = String::new();
631                out.push_str(&format!(
632                    "\n{ERROR}{}\n",
633                    yacc_diag.file_location_msg(" parsing the `%grmtools` section", None)
634                ));
635                for e in es {
636                    out.push_str(&indent("     ", &yacc_diag.format_error(e).to_string()));
637                    out.push('\n');
638                }
639                Box::<dyn Error>::from(ErrorString(out))
640            }
641            e => e.to_string().into(),
642        })?;
643        // Temporarily we update self.yacckind and self.recoverer from the build_env
644        // Until codegen reads these variables from the build_env directly.
645        self.recoverer = Some(build_env.recoverer());
646        self.yacckind = Some(build_env.yacc_kind());
647
648        let warnings = build_env.ast_with_validity_info().ast().warnings();
649        if self.warnings_are_errors && !warnings.is_empty() {
650            let mut out = String::new();
651            out.push_str(&format!(
652                "\n{ERROR}{}\n",
653                yacc_diag.file_location_msg("", None)
654            ));
655            for e in warnings {
656                out.push_str(&format!(
657                    "{}\n",
658                    indent("     ", &yacc_diag.format_warning(e).to_string())
659                ));
660            }
661            return Err(ErrorString(out).into());
662        } else if !warnings.is_empty() {
663            for w in warnings {
664                let ws_loc = yacc_diag.file_location_msg("", None);
665                let ws = indent("     ", &yacc_diag.format_warning(w).to_string());
666                // Assume if this variable is set we are running under cargo.
667                if std::env::var("OUT_DIR").is_ok() && self.show_warnings {
668                    for line in ws_loc.lines().chain(ws.lines()) {
669                        println!("cargo:warning={}", line);
670                    }
671                } else if self.show_warnings {
672                    eprintln!("{}", ws_loc);
673                    eprintln!("{WARNING} {}", ws);
674                }
675            }
676        }
677
678        #[cfg(test)]
679        if let Some(cb) = &self.inspect_callback {
680            cb(build_env.recoverer())?;
681        }
682
683        let timestamp = env!("VERGEN_BUILD_TIMESTAMP");
684        let code_gen = build_env.code_generator(timestamp).map_err(|e| match e {
685            ParserBuildEnvError::YaccGrammarErrors(errs) => {
686                let mut out = String::new();
687                out.push_str(&format!(
688                    "\n{ERROR}{}\n",
689                    yacc_diag.file_location_msg("", None)
690                ));
691                for e in errs {
692                    out.push_str(&indent("     ", &yacc_diag.format_error(e).to_string()));
693                    out.push('\n');
694                }
695                ErrorString(out)
696            }
697            e => ErrorString(e.to_string()),
698        })?;
699        let grm = code_gen.grm();
700        let rule_ids = grm
701            .tokens_map()
702            .iter()
703            .map(|(&n, &i)| (n.to_owned(), i.as_storaget()))
704            .collect::<HashMap<_, _>>();
705
706        let cache = code_gen.cache_str(&build_env);
707
708        // We don't need to go through the full rigmarole of generating an output file if all of
709        // the following are true: the output file exists; it is newer than the input file; and the
710        // cache hasn't changed. The last of these might be surprising, but it's vital: we don't
711        // know, for example, what the IDs map might be from one run to the next, and it might
712        // change for reasons beyond lrpar's control. If it does change, that means that the lexer
713        // and lrpar would get out of sync, so we have to play it safe and regenerate in such
714        // cases.
715        if let Ok(ref inmd) = fs::metadata(grmp)
716            && let Ok(ref out_rs_md) = fs::metadata(outp)
717            && FileTime::from_last_modification_time(out_rs_md)
718                > FileTime::from_last_modification_time(inmd)
719            && let Ok(outc) = read_to_string(outp)
720        {
721            if outc.contains(&cache) {
722                let (grm, _, _) = code_gen.finish();
723
724                return Ok(CTParser {
725                    regenerated: false,
726                    rule_ids,
727                    yacc_grammar: grm,
728                    grammar_src: inc,
729                    grammar_path: self.grammar_path.unwrap(),
730                    conflicts: None,
731                });
732            } else {
733                #[cfg(grmtools_extra_checks)]
734                if std::env::var("CACHE_EXPECTED").is_ok() {
735                    eprintln!("outc: {}", outc);
736                    eprintln!("using cache: {}", cache,);
737                    // Primarily for use in the testsuite.
738                    panic!("The cache regenerated however, it was expected to match");
739                }
740            }
741        }
742
743        // At this point, we know we're going to generate fresh output; however, if something goes
744        // wrong in the process between now and us writing /out/blah.rs, rustc thinks that
745        // everything's gone swimmingly (even if build.rs errored!), and tries to carry on
746        // compilation, leading to weird errors. We therefore delete /out/blah.rs at this point,
747        // which means, at worse, the user gets a "file not found" error from rustc (which is less
748        // confusing than the alternatives).
749        fs::remove_file(outp).ok();
750
751        let stable = code_gen.stable();
752        if self.error_on_conflicts
753            && let Some(c) = stable.conflicts()
754        {
755            match (grm.expect(), grm.expectrr()) {
756                (Some(i), Some(j)) if i == c.sr_len() && j == c.rr_len() => (),
757                (Some(i), None) if i == c.sr_len() && 0 == c.rr_len() => (),
758                (None, Some(j)) if 0 == c.sr_len() && j == c.rr_len() => (),
759                (None, None) if 0 == c.rr_len() && 0 == c.sr_len() => (),
760                _ => {
761                    let conflicts_diagnostic = yacc_diag.format_conflicts::<LexerTypesT>(
762                        grm,
763                        build_env.ast_with_validity_info().ast(),
764                        c,
765                        code_gen.sgraph(),
766                        stable,
767                    );
768                    #[cfg(test)]
769                    let (_, _, stable) = code_gen.finish();
770                    return Err(Box::new(CTConflictsError {
771                        conflicts_diagnostic,
772                        phantom: PhantomData,
773                        #[cfg(test)]
774                        stable,
775                    }));
776                }
777            }
778        }
779
780        if let Some(ref mut inspector_rt) = self.inspect_rt {
781            let rt: RTParserBuilder<'_, StorageT, LexerTypesT> = RTParserBuilder::new(grm, stable);
782            let rt = if let Some(rk) = self.recoverer {
783                rt.recoverer(rk)
784            } else {
785                rt
786            };
787            inspector_rt(build_env.header_mut(), rt, &rule_ids, grmp)?
788        }
789
790        build_env
791            .check_unused_header_keys()
792            .map_err(|e| ErrorString(e.to_string()))?;
793
794        self.output_file(
795            &code_gen,
796            outp,
797            &format!("/* CACHE INFORMATION {} */\n", cache),
798            &build_env,
799        )?;
800        let (grm, sgraph, stable) = code_gen.finish();
801        let conflicts = if stable.conflicts().is_some() {
802            Some((sgraph, stable))
803        } else {
804            None
805        };
806        Ok(CTParser {
807            regenerated: true,
808            rule_ids,
809            yacc_grammar: grm,
810            grammar_src: inc,
811            grammar_path: self.grammar_path.unwrap(),
812            conflicts,
813        })
814    }
815
816    /// Given the filename `a/b.y` as input, statically compile the grammar `src/a/b.y` into a Rust
817    /// module which can then be imported using `lrpar_mod!("a/b.y")`. This is a convenience
818    /// function around [`process_file`](#method.process_file) which makes it easier to compile
819    /// grammar files stored in a project's `src/` directory: please see
820    /// [`process_file`](#method.process_file) for additional constraints and information about the
821    /// generated files.
822    #[deprecated(
823        since = "0.11.0",
824        note = "Please use grammar_in_src_dir(), build(), and token_map() instead"
825    )]
826    #[allow(deprecated)]
827    pub fn process_file_in_src(
828        &mut self,
829        srcp: &str,
830    ) -> Result<HashMap<String, StorageT>, Box<dyn Error>> {
831        let mut inp = current_dir()?;
832        inp.push("src");
833        inp.push(srcp);
834        let mut outp = PathBuf::new();
835        outp.push(var("OUT_DIR").unwrap());
836        outp.push(Path::new(srcp).parent().unwrap().to_str().unwrap());
837        create_dir_all(&outp)?;
838        let mut leaf = Path::new(srcp)
839            .file_name()
840            .unwrap()
841            .to_str()
842            .unwrap()
843            .to_owned();
844        write!(leaf, ".{}", RUST_FILE_EXT).ok();
845        outp.push(leaf);
846        self.process_file(inp, outp)
847    }
848
849    /// Statically compile the Yacc file `inp` into Rust, placing the output into the file `outp`.
850    /// Note that three additional files will be created with the same name as `outp` but with the
851    /// extensions `grm`, and `stable`, overwriting any existing files with those names.
852    ///
853    /// `outp` defines a module as follows:
854    ///
855    /// ```text
856    ///   mod modname {
857    ///     pub fn parse(lexemes: &::std::vec::Vec<::lrpar::Lexeme<StorageT>>) { ... }
858    ///         -> (::std::option::Option<ActionT>,
859    ///             ::std::vec::Vec<::lrpar::LexParseError<StorageT>>)> { ...}
860    ///
861    ///     pub fn token_epp<'a>(tidx: ::cfgrammar::TIdx<StorageT>) -> ::std::option::Option<&'a str> {
862    ///       ...
863    ///     }
864    ///
865    ///     ...
866    ///   }
867    /// ```
868    ///
869    /// where:
870    ///  * `modname` is either:
871    ///    * the module name specified [`mod_name`](#method.mod_name)
872    ///    * or, if no module name was explicitly specified, then for the file `/a/b/c.y` the
873    ///      module name is `c_y` (i.e. the file's leaf name, minus its extension, with a prefix of
874    ///      `_y`).
875    ///  * `ActionT` is either:
876    ///    * the `%actiontype` value given to the grammar
877    ///    * or, if the `yacckind` was set YaccKind::Original(YaccOriginalActionKind::UserAction),
878    ///      it is [`Node<StorageT>`](../parser/enum.Node.html)
879    ///
880    /// # Panics
881    ///
882    /// If `StorageT` is not big enough to index the grammar's tokens, rules, or
883    /// productions.
884    #[deprecated(
885        since = "0.11.0",
886        note = "Please use grammar_path(), output_path(), build(), and token_map() instead"
887    )]
888    pub fn process_file<P, Q>(
889        &mut self,
890        inp: P,
891        outp: Q,
892    ) -> Result<HashMap<String, StorageT>, Box<dyn Error>>
893    where
894        P: AsRef<Path>,
895        Q: AsRef<Path>,
896    {
897        self.grammar_path = Some(inp.as_ref().to_owned());
898        self.output_path = Some(outp.as_ref().to_owned());
899        let cl: CTParserBuilder<LexerTypesT> = CTParserBuilder {
900            grammar_path: self.grammar_path.clone(),
901            grammar_src: None,
902            from_ast: None,
903            output_path: self.output_path.clone(),
904            mod_name: self.mod_name,
905            recoverer: self.recoverer,
906            yacckind: self.yacckind,
907            error_on_conflicts: self.error_on_conflicts,
908            warnings_are_errors: self.warnings_are_errors,
909            show_warnings: self.show_warnings,
910            visibility: self.visibility.clone(),
911            rust_edition: self.rust_edition,
912            inspect_rt: None,
913            serialisation_format: self.serialisation_format,
914            #[cfg(test)]
915            inspect_callback: None,
916            phantom: PhantomData,
917        };
918        Ok(cl.build()?.rule_ids)
919    }
920
921    fn output_file<P: AsRef<Path>>(
922        &self,
923        code_gen: &ParserCodegen<LexerTypesT>,
924        outp_rs: P,
925        cache: &str,
926        build_env: &ParserBuildEnv<'_, LexerTypesT>,
927    ) -> Result<(), Box<dyn Error>> {
928        let outs = code_gen
929            .generate(build_env)
930            .map_err(|e| ErrorString(e.to_string()))?;
931        let mut f = File::create(outp_rs)?;
932        f.write_all(outs.as_bytes())?;
933        f.write_all(cache.as_bytes())?;
934        Ok(())
935    }
936}
937
938/// Bundles `YaccGrammar` + `StateTable` so that generated parsers can hold
939/// them in a `OnceLock` without naming `lrtable` directly.
940#[doc(hidden)]
941pub struct ParserData<StorageT: Eq + Hash> {
942    grm: YaccGrammar<StorageT>,
943    stable: StateTable<StorageT>,
944}
945
946impl<StorageT: Eq + Hash> ParserData<StorageT> {
947    pub fn grm(&self) -> &YaccGrammar<StorageT> {
948        &self.grm
949    }
950
951    pub fn stable(&self) -> &StateTable<StorageT> {
952        &self.stable
953    }
954}
955
956/// This function is called by generated files; it exists so that generated files don't require a
957/// direct dependency on bincode.
958#[doc(hidden)]
959pub fn _reconstitute<
960    C: wincode::config::Config + Clone + Copy,
961    StorageT: SchemaReadOwned<C, Dst = StorageT> + Eq + Hash + PrimInt + Unsigned + 'static,
962>(
963    grm_buf: &[u8],
964    stable_buf: &[u8],
965    config: C,
966) -> ParserData<StorageT> {
967    let grm: YaccGrammar<StorageT> = wincode::config::deserialize_from(grm_buf, config).unwrap();
968    let stable = wincode::config::deserialize_from(stable_buf, config).unwrap();
969    ParserData { grm, stable }
970}
971
972/// An interface to the result of [CTParserBuilder::build()].
973pub struct CTParser<StorageT = u32>
974where
975    StorageT: Eq + Hash,
976{
977    regenerated: bool,
978    rule_ids: HashMap<String, StorageT>,
979    yacc_grammar: YaccGrammar<StorageT>,
980    grammar_src: String,
981    grammar_path: PathBuf,
982    conflicts: Option<(StateGraph<StorageT>, StateTable<StorageT>)>,
983}
984
985impl<StorageT> CTParser<StorageT>
986where
987    StorageT: 'static + Debug + Hash + PrimInt + Unsigned,
988    usize: AsPrimitive<StorageT>,
989{
990    /// Returns `true` if this compile-time parser was regenerated or `false` if it was not.
991    pub fn regenerated(&self) -> bool {
992        self.regenerated
993    }
994
995    /// Returns a [HashMap] from lexeme string types to numeric types (e.g. `INT: 2`), suitable for
996    /// handing to a lexer to coordinate the IDs of lexer and parser.
997    pub fn token_map(&self) -> &HashMap<String, StorageT> {
998        &self.rule_ids
999    }
1000
1001    /// If there are any conflicts in the grammar, return a tuple which allows users to inspect and
1002    /// pretty print them; otherwise returns `None`. If the grammar was not regenerated, this will
1003    /// always return `None`, even if the grammar actually has conflicts.
1004    ///
1005    /// **Note: The conflicts feature is currently unstable and may change in the future.**
1006    #[allow(private_interfaces)]
1007    pub fn conflicts(
1008        &self,
1009        _: crate::unstable::UnstableApi,
1010    ) -> Option<(
1011        &YaccGrammar<StorageT>,
1012        &StateGraph<StorageT>,
1013        &StateTable<StorageT>,
1014        &Conflicts<StorageT>,
1015    )> {
1016        if let Some((sgraph, stable)) = &self.conflicts {
1017            return Some((
1018                &self.yacc_grammar,
1019                sgraph,
1020                stable,
1021                stable.conflicts().unwrap(),
1022            ));
1023        }
1024        None
1025    }
1026
1027    #[doc(hidden)]
1028    pub fn yacc_grammar(&self) -> &YaccGrammar<StorageT> {
1029        &self.yacc_grammar
1030    }
1031    #[doc(hidden)]
1032    pub fn grammar_src(&self) -> &str {
1033        &self.grammar_src
1034    }
1035    #[doc(hidden)]
1036    pub fn grammar_path(&self) -> &Path {
1037        self.grammar_path.as_path()
1038    }
1039}
1040
1041/// Indents a multi-line string and trims any trailing newline.
1042/// This currently assumes that indentation on blank lines does not matter.
1043///
1044/// The algorithm used by this function is:
1045/// 1. Prefix `s` with the indentation, indenting the first line.
1046/// 2. Trim any trailing newlines.
1047/// 3. Replace all newlines with `\n{indent}`` to indent all lines after the first.
1048///
1049/// It is plausible that we should a step 4, but currently do not:
1050/// 4. Replace all `\n{indent}\n` with `\n\n`
1051pub(crate) fn indent(indent: &str, s: &str) -> String {
1052    format!("{indent}{}\n", s.trim_end_matches('\n')).replace('\n', &format!("\n{}", indent))
1053}
1054
1055// Tests dealing with the filesystem not supported under wasm32
1056#[cfg(all(not(target_arch = "wasm32"), test))]
1057mod test {
1058    use std::{fs::File, io::Write, path::PathBuf};
1059
1060    use super::{CTConflictsError, CTParserBuilder};
1061    use crate::test_utils::TestLexerTypes;
1062    use cfgrammar::yacc::{YaccKind, YaccOriginalActionKind};
1063    use tempfile::TempDir;
1064
1065    #[test]
1066    fn test_conflicts() {
1067        let temp = TempDir::new().unwrap();
1068        let mut file_path = PathBuf::from(temp.as_ref());
1069        file_path.push("grm.y");
1070        let mut f = File::create(&file_path).unwrap();
1071        let _ = f.write_all(
1072            "%start A
1073%%
1074A : 'a' 'b' | B 'b';
1075B : 'a' | C;
1076C : 'a';"
1077                .as_bytes(),
1078        );
1079
1080        match CTParserBuilder::<TestLexerTypes>::new()
1081            .error_on_conflicts(false)
1082            .yacckind(YaccKind::Original(YaccOriginalActionKind::GenericParseTree))
1083            .grammar_path(file_path.to_str().unwrap())
1084            .output_path(file_path.with_extension("ignored"))
1085            .build()
1086            .unwrap()
1087            .conflicts(crate::unstable::UnstableApi)
1088        {
1089            Some((_, _, _, conflicts)) => {
1090                assert_eq!(conflicts.sr_len(), 1);
1091                assert_eq!(conflicts.rr_len(), 1);
1092            }
1093            None => panic!("Expected error data"),
1094        }
1095    }
1096
1097    #[test]
1098    fn test_conflicts_error() {
1099        let temp = TempDir::new().unwrap();
1100        let mut file_path = PathBuf::from(temp.as_ref());
1101        file_path.push("grm.y");
1102        let mut f = File::create(&file_path).unwrap();
1103        let _ = f.write_all(
1104            "%start A
1105%%
1106A : 'a' 'b' | B 'b';
1107B : 'a' | C;
1108C : 'a';"
1109                .as_bytes(),
1110        );
1111
1112        match CTParserBuilder::<TestLexerTypes>::new()
1113            .yacckind(YaccKind::Original(YaccOriginalActionKind::GenericParseTree))
1114            .grammar_path(file_path.to_str().unwrap())
1115            .output_path(file_path.with_extension("ignored"))
1116            .build()
1117        {
1118            Ok(_) => panic!("Expected error"),
1119            Err(e) => {
1120                let cs = e.downcast_ref::<CTConflictsError<u16>>();
1121                assert_eq!(cs.unwrap().stable.conflicts().unwrap().rr_len(), 1);
1122                assert_eq!(cs.unwrap().stable.conflicts().unwrap().sr_len(), 1);
1123            }
1124        }
1125    }
1126
1127    #[test]
1128    fn test_expect_error() {
1129        let temp = TempDir::new().unwrap();
1130        let mut file_path = PathBuf::from(temp.as_ref());
1131        file_path.push("grm.y");
1132        let mut f = File::create(&file_path).unwrap();
1133        let _ = f.write_all(
1134            "%start A
1135%expect 2
1136%%
1137A: 'a' 'b' | B 'b';
1138B: 'a';"
1139                .as_bytes(),
1140        );
1141
1142        match CTParserBuilder::<TestLexerTypes>::new()
1143            .yacckind(YaccKind::Original(YaccOriginalActionKind::GenericParseTree))
1144            .grammar_path(file_path.to_str().unwrap())
1145            .output_path(file_path.with_extension("ignored"))
1146            .build()
1147        {
1148            Ok(_) => panic!("Expected error"),
1149            Err(e) => {
1150                let cs = e.downcast_ref::<CTConflictsError<u16>>();
1151                assert_eq!(cs.unwrap().stable.conflicts().unwrap().rr_len(), 0);
1152                assert_eq!(cs.unwrap().stable.conflicts().unwrap().sr_len(), 1);
1153            }
1154        }
1155    }
1156
1157    #[test]
1158    fn test_expectrr_error() {
1159        let temp = TempDir::new().unwrap();
1160        let mut file_path = PathBuf::from(temp.as_ref());
1161        file_path.push("grm.y");
1162        let mut f = File::create(&file_path).unwrap();
1163        let _ = f.write_all(
1164            "%start A
1165%expect 1
1166%expect-rr 2
1167%%
1168A : 'a' 'b' | B 'b';
1169B : 'a' | C;
1170C : 'a';"
1171                .as_bytes(),
1172        );
1173
1174        match CTParserBuilder::<TestLexerTypes>::new()
1175            .yacckind(YaccKind::Original(YaccOriginalActionKind::GenericParseTree))
1176            .grammar_path(file_path.to_str().unwrap())
1177            .output_path(file_path.with_extension("ignored"))
1178            .build()
1179        {
1180            Ok(_) => panic!("Expected error"),
1181            Err(e) => {
1182                let cs = e.downcast_ref::<CTConflictsError<u16>>();
1183                assert_eq!(cs.unwrap().stable.conflicts().unwrap().rr_len(), 1);
1184                assert_eq!(cs.unwrap().stable.conflicts().unwrap().sr_len(), 1);
1185            }
1186        }
1187    }
1188
1189    #[test]
1190    /// Tests a yacc .y filename containing a dash character leading to an invalid rust identifier
1191    /// when that dash is subsequently used as the default `CTParserBuilder::mod_name`.
1192    fn test_invalid_identifier_in_derived_mod_name() {
1193        let temp = TempDir::new().unwrap();
1194        let mut file_path = PathBuf::from(temp.as_ref());
1195        file_path.push("contains-a-dash.y");
1196        let mut f = File::create(&file_path).unwrap();
1197        let _ = f.write_all(
1198            "%start A
1199%%
1200A : 'a';"
1201                .as_bytes(),
1202        );
1203        match CTParserBuilder::<TestLexerTypes>::new()
1204            .yacckind(YaccKind::Original(YaccOriginalActionKind::GenericParseTree))
1205            .grammar_path(file_path.to_str().unwrap())
1206            .output_path(file_path.with_extension("ignored"))
1207            .build()
1208        {
1209            Ok(_) => panic!("Expected error"),
1210            Err(e) => {
1211                let err_string = e.to_string();
1212                assert_eq!(
1213                    err_string,
1214                    "mod_name \'contains-a-dash_y\' is not a valid rust identifier due to 'unexpected token'"
1215                );
1216            }
1217        }
1218    }
1219
1220    #[cfg(test)]
1221    #[test]
1222    fn test_recoverer_header() -> Result<(), Box<dyn std::error::Error>> {
1223        use crate::RecoveryKind as RK;
1224        #[rustfmt::skip]
1225            let recovery_kinds = [
1226                //  Builder,          Header setting,     Expected result.
1227                // -----------       ------------------  -------------------
1228                (Some(RK::None),      Some(RK::None),     Some(RK::None)),
1229                (Some(RK::None),      Some(RK::CPCTPlus), Some(RK::None)),
1230                (Some(RK::CPCTPlus),  Some(RK::CPCTPlus), Some(RK::CPCTPlus)),
1231                (Some(RK::CPCTPlus),  Some(RK::None),     Some(RK::CPCTPlus)),
1232                (None,                Some(RK::CPCTPlus), Some(RK::CPCTPlus)),
1233                (None,                Some(RK::None),     Some(RK::None)),
1234                (None,                None,               Some(RK::CPCTPlus)),
1235                (Some(RK::None),      None,               Some(RK::None)),
1236                (Some(RK::CPCTPlus),  None,               Some(RK::CPCTPlus)),
1237            ];
1238
1239        for (i, (builder_arg, header_arg, expected_rk)) in
1240            recovery_kinds.iter().cloned().enumerate()
1241        {
1242            let y_src = if let Some(header_arg) = header_arg {
1243                format!(
1244                    "\
1245                    %grmtools{{yacckind: Original(NoAction), recoverer: {}}} \
1246                    %% \
1247                    start: ; \
1248                    ",
1249                    match header_arg {
1250                        RK::None => "RecoveryKind::None",
1251                        RK::CPCTPlus => "RecoveryKind::CPCTPlus",
1252                    }
1253                )
1254            } else {
1255                r#"
1256                    %grmtools{yacckind: Original(NoAction)}
1257                    %%
1258                    Start: ;
1259                    "#
1260                .to_string()
1261            };
1262            let out_dir = std::env::var("OUT_DIR").unwrap();
1263            let y_path = format!("{out_dir}/recoverykind_test_{i}.y");
1264            let y_out_path = format!("{y_path}.rs");
1265            std::fs::File::create(y_path.clone()).unwrap();
1266            std::fs::write(y_path.clone(), y_src).unwrap();
1267            let mut cp_builder = CTParserBuilder::<TestLexerTypes>::new();
1268            cp_builder = cp_builder
1269                .output_path(y_out_path.clone())
1270                .grammar_path(y_path.clone());
1271            cp_builder = if let Some(builder_arg) = builder_arg {
1272                cp_builder.recoverer(builder_arg)
1273            } else {
1274                cp_builder
1275            }
1276            .inspect_recoverer(Box::new(move |rk| {
1277                if matches!(
1278                    (rk, expected_rk),
1279                    (RK::None, Some(RK::None)) | (RK::CPCTPlus, Some(RK::CPCTPlus))
1280                ) {
1281                    Ok(())
1282                } else {
1283                    panic!("Unexpected recovery kind")
1284                }
1285            }));
1286            cp_builder.build()?;
1287        }
1288        Ok(())
1289    }
1290}