Skip to content
← Back to Examples

Grammar Modularity

RustRubyconceptualbeginner

Demonstrate how to compose grammars from reusable modules in Rust and Ruby.

Key Concepts

  • grammar composition
  • module isolation
  • rule namespacing
  • reusable components

Why Parse This Format?

Large grammars benefit from modular organization. Splitting grammars into modules improves maintainability and enables reuse.

Use Cases

  • Language implementations with multiple syntax levels
  • Shared grammar components (expressions, identifiers)
  • Plugin architectures for extensible parsers
  • Testing grammar parts in isolation

Input Format

Composed language with module prefixes.

'a(' aaa ')' | 'b(' bbb ')' | 'c(' ccc ')'

Examples

a(aaa)VALID

Module A syntax

b(bbb)VALID

Module B syntax

c(ccc)VALID

Module C syntax

a(bbb)INVALID

Wrong module content

Output Format

Match indicating which module was used.

Structure

module

Which module matched (a, b, or c)

content

The matched content

Visual Overview

Grammar Modularity diagram

How the Code Works

This section explains how the parsing rules map to the format semantics.

Module Functions

fn build_module_a() -> Vec<(&'static str, Parslet)> {
    let mut rules = Vec::new();
    rules.push(("a_language", re(r"aaa")));
    rules
}

Each module is a function returning rules: Functions encapsulate related rules. Return type is explicit for clarity.

Grammar Composition

fn build_grammar() -> Grammar {
    let mut builder = GrammarBuilder::new();

    // Import rules from module A
    for (name, parslet) in build_module_a() {
        builder = builder.rule(name, parslet);
    }

    // Import rules from module B
    for (name, parslet) in build_module_b() {
        builder = builder.rule(name, parslet);
    }
    // ...
}

Modules are combined in the main builder: Iteration over module rules adds them to the combined grammar.

Root Rule with Choices

builder = builder.rule(
    "root",
    choice(vec![
        dynamic(seq(vec![
            dynamic(str("a(")),
            dynamic(ref_("a_language")),
            dynamic(str(")")),
        ])),
        // b_language, c_language...
    ]),
);

The root combines module entry points: Each alternative invokes a different module's rule.

Trait-Based Approach (Alternative)

trait GrammarModule {
    fn name(&self) -> &str;
    fn add_rules(&self, builder: &mut GrammarBuilder);
    fn entry_point(&self) -> &str;
}

struct ModuleA;
impl GrammarModule for ModuleA {
    fn name(&self) -> &str { "A" }
    fn add_rules(&self, builder: &mut GrammarBuilder) {
        builder.rule("a_language", re(r"aaa"));
    }
    fn entry_point(&self) -> &str { "a_language" }
}

Traits provide type-safe composition: Traits enable static dispatch and compile-time checks.

Macro-Based Approach (Alternative)

macro_rules! compose_grammar {
    (modules: [$($mod:ty),*], root: $root:expr) => {{
        let mut builder = GrammarBuilder::new();
        $(
            <$mod as GrammarModule>::add_rules(&$mod::default(), &mut builder);
        )*
        builder.root($root);
        builder.build()
    }};
}

Macros provide declarative composition: Macros generate boilerplate at compile time.

Macro-Based Approach (Alternative)

macro_rules! compose_grammar {
    (modules: [$($mod:ty),*], root: $root:expr) => {{
        let mut builder = GrammarBuilder::new();
        $(
            <$mod as GrammarModule>::add_rules(&$mod::default(), &mut builder);
        )*
        builder.root($root);
        builder.build()
    }};
}

Macros provide declarative composition: Macros generate boilerplate at compile time.

Source Code

Rust
//! Grammar Modularity Example
//!
//! Demonstrates how to compose grammars from modules in Rust.
//!
//! Run with: cargo run --example modularity --no-default-features

#![allow(clippy::print_literal)]

use parsanol::portable::{
    parser_dsl::{choice, dynamic, re, ref_, seq, str, GrammarBuilder},
    AstArena, Grammar, PortableParser,
};

/// Module A: Simple identifier language
fn build_module_a() -> Vec<(&'static str, parsanol::portable::parser_dsl::Re<'static>)> {
    vec![("a_language", re(r"aaa"))]
}

/// Module B: Another simple language
fn build_module_b() -> Vec<(&'static str, parsanol::portable::parser_dsl::Re<'static>)> {
    vec![("b_language", re(r"bbb"))]
}

/// Module C: Yet another language
fn build_module_c() -> Vec<(&'static str, parsanol::portable::parser_dsl::Re<'static>)> {
    vec![("c_language", re(r"ccc"))]
}

/// Build combined grammar from modules
fn build_grammar() -> Grammar {
    let mut builder = GrammarBuilder::new();

    // Import rules from module A
    for (name, parslet) in build_module_a() {
        builder = builder.rule(name, parslet);
    }

    // Import rules from module B
    for (name, parslet) in build_module_b() {
        builder = builder.rule(name, parslet);
    }

    // Import rules from module C
    for (name, parslet) in build_module_c() {
        builder = builder.rule(name, parslet);
    }

    // Whitespace
    builder = builder.rule("space", re(r"[ \t]*"));

    // Root rule combining all modules
    builder = builder.rule(
        "root",
        choice(vec![
            // a(aaa)
            dynamic(seq(vec![
                dynamic(str("a(")),
                dynamic(ref_("a_language")),
                dynamic(str(")")),
            ])),
            // b(bbb)
            dynamic(seq(vec![
                dynamic(str("b(")),
                dynamic(ref_("b_language")),
                dynamic(str(")")),
            ])),
            // c(ccc)
            dynamic(seq(vec![
                dynamic(str("c(")),
                dynamic(ref_("c_language")),
                dynamic(str(")")),
            ])),
        ]),
    );

    builder.build()
}

fn main() {
    println!("Grammar Modularity Example");
    println!("==========================");
    println!();

    println!("This example demonstrates how to compose grammars from modules.");
    println!();

    // Demonstrate module functions
    println!("Module A defines: a_language -> 'aaa'");
    println!("Module B defines: b_language -> 'bbb'");
    println!("Module C defines: c_language -> 'ccc'");
    println!();

    // Combined grammar
    println!("Combined grammar root rule:");
    println!("  root = 'a(' a_language ')'");
    println!("       | 'b(' b_language ')'");
    println!("       | 'c(' c_language ')'");
    println!();

    // Build and test
    let grammar = build_grammar();

    // Test inputs
    let inputs = [
        ("a(aaa)", true),
        ("b(bbb)", true),
        ("c(ccc)", true),
        ("a(aab)", false), // invalid
    ];

    println!("Test inputs:");
    for (input, should_match) in inputs {
        let mut arena = AstArena::for_input(input.len());
        let mut parser = PortableParser::new(&grammar, input, &mut arena);

        match parser.parse() {
            Ok(_) => {
                let status = if should_match { "OK" } else { "UNEXPECTED" };
                println!("  '{}' - parsed ({})", input, status);
            }
            Err(_) => {
                let status = if !should_match {
                    "OK (expected failure)"
                } else {
                    "FAIL"
                };
                println!("  '{}' - failed ({})", input, status);
            }
        }
    }
    println!();

    // Alternative: Trait-based modularity
    println!("--- Alternative: Trait-based Modularity ---");
    println!();
    println!("In Rust, you can also use traits for modularity:");
    println!();
    println!("  trait GrammarModule {{");
    println!("      fn add_rules(&self, builder: &mut GrammarBuilder);");
    println!("  }}");
    println!();
    println!("  impl GrammarModule for ModuleA {{");
    println!("      fn add_rules(&self, builder: &mut GrammarBuilder) {{");
    println!("          builder.rule(\"a_language\", re(r\"aaa\"));");
    println!("      }}");
    println!("  }}");
    println!();

    println!("This demonstrates approaches to grammar modularity in Rust:");
    println!("1. Function-based: Functions returning rule vectors");
    println!("2. Trait-based: Types implementing GrammarModule trait");
    println!("3. Direct composition: Combining rules in build_grammar()");
}
Ruby
# frozen_string_literal: true

$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"

require "pp"
require "parsanol/parslet"

# Demonstrates modular parsers, split out over many classes. Please look at
# ip_address.rb as well.

module ALanguage
  include Parsanol::Parslet

  # Parslet rules are really a special kind of method. Mix them into your
  # classes!
  rule(:a_language) { str("aaa") }
end

# Parslet parsers are parslet atoms as well. Create an instance and chain them
# to your other rules.
#
class BLanguage < Parsanol::Parser
  root :blang

  rule(:blang) { str("bbb") }
end

# Parslet atoms are really Ruby values, pass them around.
c_language = Parsanol.str("ccc")

class Language < Parsanol::Parser
  def initialize(c_language)
    @c_language = c_language
    super()
  end

  root :root

  include ALanguage

  rule(:root) do
    (str("a(") >> a_language >> str(")") >> space) |
      (str("b(") >> BLanguage.new >> str(")") >> space) |
      (str("c(") >> @c_language >> str(")") >> space)
  end
  rule(:space) { str(" ").maybe }
end

Language.new(c_language).parse("a(aaa)")
Language.new(c_language).parse("b(bbb)")
Language.new(c_language).parse("c(ccc)")