Scope Atoms
Create isolated capture contexts with scope atoms. Captures made inside a scope are discarded when the scope exits, preventing capture pollution.
Key Concepts
- capture isolation
- scope nesting
- recursive parsing
- automatic capture cleanup
Why Parse This Format?
Scopes prevent inner captures from polluting outer contexts. Essential for parsing nested structures where each level has its own context.
Use Cases
- Parsing nested configuration with per-section captures
- Recursive descent parsing without capture accumulation
- Processing repeated structures with isolated state
- Preventing capture name collisions
Input Format
Nested structures where inner contexts should be isolated.
[section1]
key=value
[section2]
key=valueExamples
[database]
host=localhost
port=5432
[server]
port=8080VALIDINI-style config with sections
Output Format
Only captures from outer scope are preserved.
Structure
sectionSection name persists to outer scope
keyKeys are isolated within each section scope
valueValues are isolated within each section scope
How the Code Works
This section explains how the parsing rules map to the format semantics.
Why Scopes?
let grammar = GrammarBuilder::new()
.rule("items", sequence([
capture("temp", str("a")),
capture("temp", str("b")),
capture("temp", str("c")),
]))
.build();
// Result: captures = ["temp" = "c"] (last wins)Without scopes, captures accumulate across the entire parse: With scopes, inner captures are automatically discarded: let grammar = GrammarBuilder::new() .rule("outer", sequence([ capture("outer_name", str("prefix")), scope(sequence([ capture("inner_name", re(r"[a-z]+")), // Discarded ])), ])) .build(); // Result: captures = ["outer_name" = "prefix"]
Scope Isolation
scope(sequence([
capture("key", re(r"[a-z]+")),
str("="),
capture("value", re(r"[^\n]+")),
]))Captures inside `scope()` are: - Available during parsing for inner rules - Automatically discarded when scope exits - Never visible to outer capture inspection
Nested Scopes
let grammar = GrammarBuilder::new()
.rule("nested", sequence([
capture("level", str("L1")), // Persisted
scope(sequence([
capture("level", str("L2")), // Discarded
scope(sequence([
capture("level", str("L3")), // Discarded
])),
])),
]))
.build();
// Result: captures = ["level" = "L1"]Scopes can be nested. Only the outermost captures persist:
INI Configuration Example
let grammar = GrammarBuilder::new()
.rule("section", sequence([
str("["),
capture("section", re(r"[a-zA-Z_]+")), // Persists
str("]\n"),
scope(repeat("kv_pair", 0, None)), // Key-values isolated
]))
.build();Parse sections with isolated key-value pairs: Each section's `key` and `value` captures are isolated.
Recursive Parsing
let grammar = GrammarBuilder::new()
.rule("paren", sequence([
str("("),
scope(choice([
capture("content", re(r"[^()]+")),
recursive("paren"), // Each level has own scope
])),
str(")"),
]))
.build();Scopes are essential for recursive structures: Each nesting level has isolated captures.
Recursive Parsing
let grammar = GrammarBuilder::new()
.rule("paren", sequence([
str("("),
scope(choice([
capture("content", re(r"[^()]+")),
recursive("paren"), // Each level has own scope
])),
str(")"),
]))
.build();Scopes are essential for recursive structures: Each nesting level has isolated captures.
Source Code
//! Scope Atoms Example
//!
//! Demonstrates how to create isolated capture contexts with scope atoms.
//! Captures made inside a scope are discarded when the scope exits.
//!
//! Run with: cargo run --example scopes --no-default-features
#![allow(clippy::print_literal)]
use parsanol::portable::{
parser_dsl::{capture, dynamic, re, scope, seq, str, GrammarBuilder},
AstArena, Atom, Grammar, PortableParser,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("Scope Atoms Example");
println!("===================\n");
// =========================================================================
// Example 1: Basic Scope Isolation
// =========================================================================
println!("--- Example 1: Basic Scope Isolation ---\n");
// Without scope: captures accumulate, last value wins
let grammar = GrammarBuilder::new()
.rule(
"items",
seq(vec![
dynamic(capture("temp", dynamic(str("a")))),
dynamic(str("b")),
dynamic(capture("temp", dynamic(str("c")))),
]),
)
.build();
let input = "abc";
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let result = parser.parse_from_pos(0)?;
println!(" Without scope:");
println!(" Capture names: {:?}", result.capture_names());
println!(" 'temp' value: {:?}", result.get_capture("temp", input)); // "c" (last wins)
// With scope: inner captures are discarded
let grammar = GrammarBuilder::new()
.rule(
"outer",
seq(vec![
dynamic(capture("outer_name", dynamic(str("prefix")))),
dynamic(str(" ")),
dynamic(scope(seq(vec![
dynamic(capture("inner_name", dynamic(re(r"[a-z]+")))),
dynamic(str(" ")),
dynamic(capture("inner_value", dynamic(re(r"\d+")))),
]))),
dynamic(str(" ")),
dynamic(capture("outer_value", dynamic(str("suffix")))),
]),
)
.build();
let input = "prefix hello 123 suffix";
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let result = parser.parse_from_pos(0)?;
println!("\n With scope:");
println!(" Capture names: {:?}", result.capture_names());
println!(
" 'outer_name': {:?}",
result.get_capture("outer_name", input)
);
println!(
" 'outer_value': {:?}",
result.get_capture("outer_value", input)
);
// Note: inner_name and inner_value are NOT in the result
println!(" (inner_name and inner_value are discarded)");
// =========================================================================
// Example 2: Nested Scopes
// =========================================================================
println!("\n--- Example 2: Nested Scopes ---\n");
let grammar = GrammarBuilder::new()
.rule(
"outer",
seq(vec![
dynamic(capture("level", dynamic(str("L1")))),
dynamic(str(" ")),
dynamic(scope(seq(vec![
dynamic(capture("level", dynamic(str("L2")))),
dynamic(str(" ")),
dynamic(scope(seq(vec![dynamic(capture(
"level",
dynamic(str("L3")),
))]))),
]))),
]),
)
.build();
let input = "L1 L2 L3";
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let result = parser.parse_from_pos(0)?;
println!(" Nested scopes - only L1 persists:");
println!(" Capture names: {:?}", result.capture_names());
println!(
" 'level' value: {:?}",
result.get_capture("level", input)
);
// =========================================================================
// Example 3: Recursive Structure with Scopes (Raw Grammar API)
// =========================================================================
println!("\n--- Example 3: Recursive Structure with Scopes ---\n");
// Parse balanced parentheses with per-level captures
let mut grammar = Grammar::new();
// Inner content (can be text or nested parens)
let inner_text = grammar.add_atom(Atom::Re {
pattern: r"[^()]+".into(),
});
// Capture text at current level (scoped)
let text_capture = grammar.add_atom(Atom::Capture {
name: "text".into(),
atom: inner_text,
});
// Scope wraps the content to isolate captures
let content = grammar.add_atom(Atom::Scope { atom: text_capture });
// Full structure - create atoms first to avoid borrow issues
let open_paren = grammar.add_atom(Atom::Str {
pattern: "(".into(),
});
let close_paren = grammar.add_atom(Atom::Str {
pattern: ")".into(),
});
let paren_content = grammar.add_atom(Atom::Sequence {
atoms: vec![open_paren, content, close_paren],
});
grammar.root = paren_content;
let input = "(hello)";
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let result = parser.parse_from_pos(0)?;
println!(" Parsed: {}", input);
println!(" Capture names: {:?}", result.capture_names());
// Note: "text" capture is discarded because it's inside a scope
println!(" ('text' capture is discarded because it's inside a scope)");
// =========================================================================
// Example 4: Scope for Memory Cleanup
// =========================================================================
println!("\n--- Example 4: Scope for Memory Cleanup ---\n");
// Processing repeated structures - each gets its own scope
let grammar = GrammarBuilder::new()
.rule(
"item",
scope(seq(vec![
dynamic(capture("id", dynamic(re(r"\d+")))),
dynamic(str(":")),
dynamic(capture("name", dynamic(re(r"[a-zA-Z]+")))),
])),
)
.rule("items", seq(vec![dynamic(str("item"))]))
.build();
let input = "item";
println!(" Processing repeated items with scoped captures");
println!(" Input: {}", input);
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
let result = parser.parse_from_pos(0)?;
println!(" Final captures: {:?}", result.capture_names());
println!(" (id and name captures are discarded after each item)");
// =========================================================================
// Example 5: Accessing Capture State via Parser
// =========================================================================
println!("\n--- Example 5: Accessing Capture State ---\n");
let grammar = GrammarBuilder::new()
.rule(
"outer",
seq(vec![
dynamic(capture("name", dynamic(str("outer")))),
dynamic(scope(dynamic(capture("inner", dynamic(str("inner")))))),
]),
)
.build();
let input = "outerinner";
let mut arena = AstArena::for_input(input.len());
let mut parser = PortableParser::new(&grammar, input, &mut arena);
parser.parse()?;
// Access capture state directly from parser
let captures = parser.capture_state();
let names: Vec<_> = captures.names().collect();
println!(" Capture names from parser: {:?}", names);
if let Some(value) = captures.get("name") {
println!(" 'name' capture: {:?}", value.get_text(input));
}
// Note: "inner" is not available because it was in a scope
println!(" ('inner' capture is not available - it was in a scope)");
// =========================================================================
// Summary
// =========================================================================
println!("\n--- Benefits of Scope Atoms ---");
println!("* Prevent capture pollution from nested parsing");
println!("* Each recursion level has its own capture state");
println!("* Automatic cleanup when scope exits");
println!("* Memory bounded during parse");
println!("* Essential for parsing nested structures");
println!("\n--- Performance Notes ---");
println!("* Scope push/pop is O(c_scope) where c_scope = captures in scope");
println!("* Each nesting level adds ~2%% overhead");
println!("* Use scopes liberally - they're cheap");
println!("\n--- DSL Helper ---");
println!(" scope(parslet) // Wraps parslet in isolated capture context");
println!("\n--- API Summary ---");
println!(" scope(inner) -> isolates captures");
println!(" result.get_capture(name) -> access captures (inner ones excluded)");
Ok(())
}
# frozen_string_literal: true
# Scope Atoms Example
#
# Demonstrates how to create isolated capture contexts with scope atoms.
# Captures made inside a scope are discarded when the scope exits.
$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
require "parsanol/parslet"
include Parsanol::Parslet
puts "Scope Atoms Example"
puts "===================\n"
# ===========================================================================
# Example 1: Basic Scope Isolation
# ===========================================================================
puts "--- Example 1: Basic Scope Isolation ---\n"
# Without scope: captures accumulate, last value wins
parser = str("a").capture(:temp) >> str("b") >> str("c").capture(:temp)
input = "abc"
result = parser.parse(input)
puts " Without scope:"
puts " 'temp' value: #{result[:temp].inspect}" # "c" (last wins)
# With scope: inner captures are discarded
parser = str("prefix").capture(:outer) >> str(" ") >>
scope { str("inner").capture(:inner) } >>
str(" ") >> str("suffix").capture(:outer_end)
input = "prefix inner suffix"
result = parser.parse(input)
puts "\n With scope:"
puts " 'outer': #{result[:outer].inspect}"
puts " 'outer_end': #{result[:outer_end].inspect}"
puts " 'inner': #{begin
result[:inner].inspect
rescue StandardError
'nil'
end}" # Not present
# ===========================================================================
# Example 2: Nested Scopes
# ===========================================================================
puts "\n--- Example 2: Nested Scopes ---\n"
parser = str("L1").capture(:level) >> str(" ") >>
scope do
str("L2").capture(:level) >> str(" ") >>
scope { str("L3").capture(:level) }
end
input = "L1 L2 L3"
result = parser.parse(input)
puts " Nested scopes - only L1 persists:"
puts " 'level' value: #{result[:level].inspect}" # "L1"
# ===========================================================================
# Example 3: INI Configuration Parsing
# ===========================================================================
puts "\n--- Example 3: INI Configuration Parsing ---\n"
class IniParser < Parsanol::Parser
include Parsanol::Parslet
rule(:newline) { str("\n") | str("\r\n") }
rule(:whitespace) { match('[ \t]*') }
rule(:section_header) do
str("[") >> match("[a-zA-Z_]+").capture(:section) >> str("]") >> whitespace >> newline
end
rule(:kv_pair) do
match("[a-zA-Z_]+").capture(:key) >> str("=") >> match('[^\r\n]+').capture(:value) >> newline
end
rule(:section) { section_header >> scope { kv_pair.repeat(1) } }
rule(:config) { section.repeat(1) }
root :config
end
input = "[database]\nhost=localhost\nport=5432\n\n[server]\nport=8080\ndebug=true\n"
puts " Input:\n#{input}"
parser = IniParser.new
result = parser.parse(input)
puts " Outer captures: #{result.keys}"
puts " (key/value captures are discarded after each section)"
# ===========================================================================
# Example 4: Scope for Memory Cleanup
# ===========================================================================
puts "\n--- Example 4: Scope for Memory Cleanup ---\n"
# Processing repeated structures - each gets its own scope
class ItemParser < Parsanol::Parser
include Parsanol::Parslet
rule(:item) do
scope do
match('\d+').capture(:id) >> str(":") >> match("[a-zA-Z]+").capture(:name)
end
end
rule(:items) { str("item") >> item.repeat(1) }
root :items
end
input = "item123:apple456:banana789:cherry"
puts " Processing repeated items with scoped captures"
puts " Input: #{input}"
parser = ItemParser.new
result = parser.parse(input)
puts " Final captures: #{result.keys}"
puts " (id and name captures are discarded after each item)"
# ===========================================================================
# Example 5: Scope with Dynamic
# ===========================================================================
puts "\n--- Example 5: Scope with Dynamic ---\n"
# Scope preserves outer captures for dynamic blocks
parser = str("a").capture(:a) >>
scope { str("b").capture(:a) } >>
dynamic { |_s, c| str(c.captures[:a]) }
begin
parser.parse("aba")
puts " Parses 'aba' - scope preserved outer capture 'a'"
rescue StandardError
puts " Exception - scope isolation working"
end
# ===========================================================================
# Summary
# ===========================================================================
puts "\n--- Benefits of Scope Atoms ---"
puts "* Prevent capture pollution from nested parsing"
puts "* Each recursion level has its own capture state"
puts "* Automatic cleanup when scope exits"
puts "* Memory bounded during parse"
puts "* Essential for parsing nested structures"
puts "\n--- Performance Notes ---"
puts "* Scope push/pop is O(c_scope) where c_scope = captures in scope"
puts "* Each nesting level adds ~2% overhead"
puts "* Use scopes liberally - they're cheap"
puts "\n--- DSL Helper ---"
puts " scope { parslet } // Wraps parslet in isolated capture context"
puts "\n--- API Summary ---"
puts " scope { inner } -> isolates captures"
puts " result[:name] -> access captures (inner ones excluded)"