This crate provides basic tools for generation of text strings and other sequences using context-free grammars.
The following example demonstrates random generation of a short sentence based on a sequence of input symbols and a set of rules, which may be applied to the symbol sequence until it is fully expanded.
use branchy::{
Symbol,
Rule,
ExpanderBuilder
};
let input = vec![Symbol::Nonterminal("person"), Symbol::Terminal("comes from a"), Symbol::Nonterminal("location")];
let rules = vec![
Rule::new("person", vec![Symbol::Nonterminal("name")]),
Rule::new(
"person",
vec![Symbol::Nonterminal("name"), Symbol::Terminal("the"), Symbol::Nonterminal("occupation")]
),
Rule::new("name", vec![Symbol::Terminal("Alice")]),
Rule::new("name", vec![Symbol::Terminal("Bob")]),
Rule::new("occupation", vec![Symbol::Terminal("blacksmith")]),
Rule::new("occupation", vec![Symbol::Terminal("baker")]),
Rule::new("location", vec![Symbol::Nonterminal("size"), Symbol::Nonterminal("settlement_type")]),
Rule::new("size", vec![Symbol::Terminal("small")]),
Rule::new("size", vec![Symbol::Terminal("big")]),
Rule::new("settlement_type", vec![Symbol::Terminal("village")]),
Rule::new("settlement_type", vec![Symbol::Terminal("town")])
];
let mut expander = ExpanderBuilder::from(rules).build();
let expansion_result = expander.expand(input);
assert!(expansion_result.is_ok());
let expanded_string = expansion_result.unwrap().join(" ");
println!("{}", expanded_string);
When run, this example prints out a sentence, similar to the ones below:
Alice the blacksmith comes from a big village
Bob comes from a small town
Bob the baker comes from a big town
As you can see, both the input sequence and the rules of the grammar are described in terms of
Nonterminal
(ones that can be further expanded)
and Terminal
symbols.
All of the rules have a non-terminal symbol value on their left-hand side and a sequence which
may contain both Nonterminal
and
Terminal
symbols on their right-hand side.
The "magic" happens in Expander
's expand()
method,
which repeatedly selects and applies matching rules until the sequence is fully expanded
(i.e contains only terminal symbols).
By default, UniformRandomRuleSelector
is used
to select rules while expanding, therefore the result is randomized. As we'll see below,
this can be changed, if needed, via ExpanderBuilder
.
When constructing an Expander
, you can provide your own
rule selector via ExpanderBuilder
's
with_rule_selector()
method.
The following example defines a custom rule selector, which always chooses the first
matching rule, and then uses it in generation of a short phrase.
As you can see, rule selectors need to implement at least the
select_matching_rule()
method
from the RuleSelector
trait.
use branchy::{
Symbol,
Rule,
ExpanderBuilder,
RuleSelector
};
struct AlwaysFirstRuleSelector;
impl<Nt, T> RuleSelector<Nt, T> for AlwaysFirstRuleSelector {
fn select_matching_rule<'a>(&self, matching_rules: &[&'a Rule<Nt, T>]) -> Option<&'a Rule<Nt, T>> {
if matching_rules.is_empty() {
None
} else {
Some(matching_rules[0])
}
}
}
let input = vec![Symbol::Terminal("Have a"), Symbol::Nonterminal("adjective"), Symbol::Nonterminal("time_of_day")];
let mut expander = ExpanderBuilder::new()
.with_new_rule("adjective", vec![Symbol::Terminal("wonderful")])
.with_new_rule("adjective", vec![Symbol::Terminal("great")])
.with_new_rule("time_of_day", vec![Symbol::Terminal("afternoon")])
.with_new_rule("time_of_day", vec![Symbol::Terminal("evening")])
.with_rule_selector(AlwaysFirstRuleSelector)
.build();
let expanded_string = expander.expand(input).unwrap().join(" ");
assert_eq!(expanded_string, "Have a wonderful afternoon");
This example also sets the rules of the grammar directly on ExpanderBuilder
via the with_new_rule()
method. See the documentation
of ExpanderBuilder
for more helper methods.
To help you debug your grammars, branchy
provides the ExpansionLogger
trait,
which you can implement in order to be notified of the progress of the expansion and the steps it takes.
For example, in order to log the rules selected at each step of the expansion, you can implement the
on_nonterm_expanded()
method. The following example
writes a message via println!()
on every step.
use branchy::{
Symbol,
Rule,
ExpanderBuilder,
ExpansionLogger,
TerminalValue,
NonterminalValue
};
struct StdOutLogger;
impl<Nt, T> ExpansionLogger<Nt, T> for StdOutLogger
where Nt: NonterminalValue + std::fmt::Debug,
T: TerminalValue + std::fmt::Debug
{
fn on_nonterm_expanded(&mut self, expanded_nonterm_value: &Nt, rule: &Rule<Nt, T>) {
println!("expanded {:?} to {:?}", expanded_nonterm_value, rule.replacement);
}
}
let input = vec![
Symbol::Terminal("There is a"),
Symbol::Nonterminal("site_description"),
Symbol::Terminal("to the"),
Symbol::Nonterminal("direction"),
Symbol::Terminal("of the town.")
];
let rules = vec![
Rule::new("site_description", vec![Symbol::Nonterminal("adjective"), Symbol::Nonterminal("site")]),
Rule::new("adjective", vec![Symbol::Terminal("huge")]),
Rule::new("adjective", vec![Symbol::Terminal("dark")]),
Rule::new("site", vec![Symbol::Terminal("forest")]),
Rule::new("site", vec![Symbol::Terminal("cave")]),
Rule::new("direction", vec![Symbol::Terminal("north")]),
Rule::new("direction", vec![Symbol::Terminal("east")])
];
let mut expander = ExpanderBuilder::from(rules)
.with_logger(StdOutLogger)
.build();
expander.expand(input).unwrap();
This example produces output similar to the following:
expanded "site_description" to [Nonterminal("adjective"), Nonterminal("site")]
expanded "adjective" to [Terminal("dark")]
expanded "site" to [Terminal("cave")]
expanded "direction" to [Terminal("east")]
Even though the primary use-case for branchy
is generating text strings, it can be used for
grammars producing other kinds of sequences. Any type implementing Clone + PartialEq
can be
used for values of non-terminal symbols and any type implementing Clone
can be used for
terminals. See NonterminalValue
and
TerminalValue
traits.