Grammar Modularity
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)VALIDModule A syntax
b(bbb)VALIDModule B syntax
c(ccc)VALIDModule C syntax
a(bbb)INVALIDWrong module content
Output Format
Match indicating which module was used.
Structure
moduleWhich module matched (a, b, or c)
contentThe matched content
Visual Overview
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
//! 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()");
}
# 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)")