Vanilla mode

Although the auto-generation mode (switch -g, --auto_generate) is the recommended way to use parol you can alternatively work in vanilla mode.

That means that parol skips generating AST types for you and it generates only a trait with semantic actions for each production of the expanded grammar instead of semantic actions for each non-terminal.

This means that you gain more control although you may loose some comfort.

Basically it is a matter of taste what mode you use. But keep in mind that growing complexity can have an impact on the maintainability of your software.

So although you may loose full speed and give up some control you obtain maintainability when using the auto-generation mode.

Actually parol itself was build in the simple mode at the first stages of its development (before version 0.9.3). But the implementation of new features required more and more changes in the grammar and showed the vulnerability of the existing implementation to changes in the input grammar.

Anyway, this chapter is dedicated to the way parol functions without auto-generation.

You may have a look at example list that uses the vanilla mode and actually shows how easy it is to work this way.

We will elaborate this by implementing a list example in an alternative way.

%start List
%title "A possibly empty comma separated list of integers"
%comment "A trailing comma is allowed."

%%

List: Items TrailingComma^;
Items: Num {","^ Num} | ;
Num: "0|[1-9][0-9]*";
TrailingComma: [","^];

Let's generate a new binary crate:

You can try this grammar by calling

parol new --bin --path ./vanilla_list --tree

Open the generated crate and substitute the generated dummy grammar by the one above. Open the build.rs and delete the line 11:

#![allow(unused)]
fn main() {
        .enable_auto_generation()
}

For the sake of completeness delete the -g from the CLI equivalent in the comment at the beginning of main.

Also change the test.txt to the content

1, 2, 3, 4, 5, 6,

Now you can parse this text by calling

cargo run ./test.txt

This will actually result in a bunch of errors because parol new generated the source for the new crate in the spirit of auto-generation mode.

But fortunately it is easy to correct the errors and create the basis for our vanilla mode crate.

Replace the content of vanilla_list_grammar.rs with the following lines

#![allow(unused)]
fn main() {
use parol_runtime::parol_macros::parol;
use parol_runtime::parser::ParseTreeType;
use parol_runtime::Result;
use std::fmt::{Debug, Display, Error, Formatter};

use crate::vanilla_list_grammar_trait::VanillaListGrammarTrait;

///
/// The value range for the supported list elements
///
pub type DefinitionRange = usize;

///
/// Data structure that implements the semantic actions for our list grammar
///
#[derive(Debug, Default)]
pub struct VanillaListGrammar {
    pub numbers: Vec<DefinitionRange>,
}

impl VanillaListGrammar {
    pub fn new() -> Self {
        VanillaListGrammar::default()
    }

    fn push(&mut self, item: DefinitionRange) {
        self.numbers.push(item)
    }
}

impl Display for VanillaListGrammar {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
        writeln!(
            f,
            "[{}]",
            self.numbers
                .iter()
                .map(|e| format!("{}", e))
                .collect::<Vec<String>>()
                .join(", ")
        )
    }
}

impl VanillaListGrammarTrait for VanillaListGrammar {}
}

Now you should be able to run the parser

$ cargo run ./test.txt
Finished dev [unoptimized + debuginfo] target(s) in 0.23s
     Running `target\debug\vanilla_list.exe .\test.txt`
Parsing took 3 milliseconds.
Success!
[]

Also some warnings should occur. But we resolve them soon.

What we see here is that the parser accepts the input but doesn't collect the list items for us immediately (there are no list items in between [ and ]). The parser functions as an acceptor but without any processing.

We need to do this on our own.

To be able to 'hook' into the right production we need to examine the expanded grammar more closely than we had to in the auto-generation mode.

So open the generated file vanilla_list-exp-par and look for the production where a Num token is accepted:

/* 5 */ Num: "0|[1-9][0-9]*";

Then we need to implement the semantic action for exactly this production number 5. We find the trait function to implement in the file src\vanilla_list_grammar_trait.rs and copy it into the impl block at the end of the file src\vanilla_list_grammar.rs:

#![allow(unused)]
fn main() {
impl VanillaListGrammarTrait for VanillaListGrammar {
    /// Semantic action for production 5:
    ///
    /// Num: "0|[1-9][0-9]*";
    ///
    fn num(&mut self, _num: &ParseTreeType) -> Result<()> {
        Ok(())
    }
}
}

Here we can implement our handling:

#![allow(unused)]
fn main() {
    /// Semantic action for production 5:
    ///
    /// Num: "0|[1-9][0-9]*";
    ///
    fn num(&mut self, num: &ParseTreeType) -> Result<()> {
        let symbol = num.text()?;
        let number = symbol
            .parse::<DefinitionRange>()
            .map_err(|e| parol!("num: Parse error: {e}"))?;
        self.push(number);
        Ok(())
    }
}

Now run the parser again

$ cargo run ./test.txt
    Finished dev [unoptimized + debuginfo] target(s) in 1.54s
     Running `target\debug\vanilla_list.exe .\test.txt`
Parsing took 4 milliseconds.
Success!
[1, 2, 3, 4, 5, 6]

Yep! This worked fine.

Note that you can`t use user defined types for your ATS types in vanilla mode because no AST types are generated at all. But actually you opted in to build the AST types on your own when you disable auto-generation mode.