Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Operator Associativity

Operator associativity defines the direction in which operators of the same precedence are evaluated.

Left Associativity

Left associativity means operators are grouped from the left. For example, x * y * z is evaluated as (x * y) * z.

Consider this example grammar, which supports left-associative multiplication:

%start LeftAssociativity
%title "Operator associativity"
%comment "Shows the handling of operator associativity in `parol`"

%%

Literal : /[0-9]+/ ;

// ---------------------------------------------------------
// OPERATOR SYMBOLS
MulOp   : '*' ;

// ---------------------------------------------------------
// EXPRESSIONS
LeftAssociativity : Multiplication ;

Multiplication : Literal { MulOp Literal } ;

To try this grammar:

parol new --bin --path .\left_associativity --tree

Replace the generated dummy grammar with the example above. Set test.txt to:

5 * 6 * 2

Parse the text by running:

cargo run ./test.txt

from the root of the generated crate.

Parsing 5 * 6 * 2 produces this parse tree:

Parse Tree

At first glance, the parse tree may appear to impose right associativity (evaluated right to left). However, in parol, all repetitive grammar constructs are represented as vectors in AST types.

Example from the generated types in src/left_associativity_grammar_trait.rs:

#![allow(unused)]
fn main() {
/// Type derived for non-terminal Multiplication
pub struct Multiplication<'t> {
    pub literal: Box<Literal<'t>>,
    pub multiplication_list: Vec<MultiplicationList<'t>>,
}

/// Type derived for non-terminal MultiplicationList
pub struct MultiplicationList<'t> {
    pub mul_op: Box<MulOp<'t>>,
    pub literal: Box<Literal<'t>>,
}
}

Items in repetitions ({...}) are stored in vectors and can be processed in the desired direction. This behavior applies to all grammar repetitions.

It is up to your grammar processing to choose the evaluation direction. To implement left associativity, apply these changes to src/left_associativity_grammar.rs:

Replace the use statements at the top of the file:

#![allow(unused)]
fn main() {
use crate::left_associativity_grammar_trait::{
    LeftAssociativity, LeftAssociativityGrammarTrait, Literal,
};
use parol_runtime::Result;
#[allow(unused_imports)]
use parol_runtime::parol_macros::{bail, parol};
use std::fmt::{Debug, Display, Error, Formatter};
}

Add a result member to the struct:

#![allow(unused)]
fn main() {
pub struct LeftAssociativityGrammar<'t> {
    pub left_associativity: Option<LeftAssociativity<'t>>,
    pub result: u32,
}
}

Add these functions to the impl LeftAssociativityGrammar<'_> block:

#![allow(unused)]
fn main() {
fn number(literal: &Literal) -> Result<u32> {
    literal
        .literal
        .text()
        .parse::<u32>()
        .map_err(|e| parol!("'{}': {e}", literal.literal.text()))
}

fn process_operation(&mut self) -> Result<()> {
    if let Some(grammar) = &self.left_associativity {
        let init = Self::number(&grammar.multiplication.literal)?;
        self.result = grammar
            .multiplication
            .multiplication_list
            .iter()
            .try_fold(init, |acc, mul| -> Result<u32> {
                Ok(acc * Self::number(&mul.literal)?)
            })?;
        Ok(())
    } else {
        bail!("No valid parse result!")
    }
}
}

Update the Display implementation:

#![allow(unused)]
fn main() {
impl Display for LeftAssociativityGrammar<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::result::Result<(), Error> {
        match &self.left_associativity {
            Some(_) => writeln!(f, "{}", self.result),
            None => write!(f, "No parse result"),
        }
    }
}
}

Change the last line of the left_associativity function from:

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

to:

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

Run the parser again:

cargo run ./test.txt

Sample output:

Parsing took 0 milliseconds.
Success!
60

The parser correctly calculates the result: 60.

The key part is the process_operation function, which folds the multiplication results into result. The initial value is the first element (literal) of the Multiplication struct, matching the grammar structure:

Multiplication : Literal { MulOp Literal } ;

Right Associativity

Right associativity means operators are grouped from the right. For example, x ^ y ^ z is evaluated as x ^ (y ^ z).

Here is a grammar for right-associative potentiation:

%start RightAssociativity
%title "Operator associativity"
%comment "Shows the handling of operator associativity in `parol`"

%%

Literal : /[0-9]+/ ;

// ---------------------------------------------------------
// OPERATOR SYMBOLS
PowOp   : '^' ;

// ---------------------------------------------------------
// EXPRESSIONS
RightAssociativity : Potentiation ;

Potentiation : Literal { PowOp Literal } ;

To try this grammar:

parol new --bin --path .\right_associativity --tree

Replace the generated dummy grammar with the example above. Set test.txt to:

4 ^ 3 ^ 2

Parse the text by running:

cargo run ./test.txt

from the root of the generated crate.

Parsing 4 ^ 3 ^ 2 produces this parse tree:

Parse Tree

The parse tree structure is identical to the left-associative example. parol handles all repetitive constructs as vectors.

To implement right associativity, modify src/right_associativity_grammar.rs as in the left-associativity example, changing prefixes from Left/left_ to Right/right_.

Replace the process_operation function with:

#![allow(unused)]
fn main() {
fn process_operation(&mut self) -> Result<()> {
    if let Some(grammar) = &self.right_associativity {
        self.result = grammar
            .potentiation
            .potentiation_list
            .iter()
            .rev()
            .try_fold(1, |acc, mul| -> Result<u32> {
                Ok(Self::number(&mul.literal)?.pow(acc))
            })?;
        let last = Self::number(&grammar.potentiation.literal)?;
        self.result = last.pow(self.result);
        Ok(())
    } else {
        bail!("No valid parse result!")
    }
}
}

Here, the fold is performed in reverse order (.rev()), starting with 1, and the last operand is the single literal in the Potentiation struct.

Run the parser again:

cargo run ./test.txt

Sample output:

Parsing took 0 milliseconds.
Success!
262144

The parser correctly calculates the result: 262144.