#![allow(
clippy::module_name_repetitions,
clippy::too_many_lines,
clippy::too_many_arguments,
clippy::option_if_let_else,
clippy::match_same_arms,
clippy::missing_const_for_fn,
clippy::similar_names
)]
use super::{Grammar, Production, collect_field_names};
#[must_use]
pub(crate) fn sat(grammar: &Grammar, k: &str, name: &str) -> bool {
if k == name {
return true;
}
let mut visited = std::collections::HashSet::new();
dispatches_to(grammar, name, k, &mut visited)
}
fn dispatches_to<'g>(
grammar: &'g Grammar,
name: &'g str,
k: &str,
visited: &mut std::collections::HashSet<&'g str>,
) -> bool {
if !visited.insert(name) {
return false;
}
let Some(rule) = grammar.rules.get(name) else {
return false;
};
let is_dispatch =
name.starts_with('_') || grammar.supertypes.contains(name) || is_pure_choice_dispatch(rule);
if !is_dispatch {
return false;
}
dispatch_prod(grammar, rule, k, visited)
}
fn is_pure_choice_dispatch(prod: &Production) -> bool {
match prod {
Production::Choice { members } => {
!members.is_empty() && members.iter().all(is_pure_choice_dispatch)
}
Production::Symbol { .. } => true,
Production::Alias { named, .. } => *named,
Production::Token { content }
| Production::ImmediateToken { content }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. } => is_pure_choice_dispatch(content),
_ => false,
}
}
fn dispatch_prod<'g>(
grammar: &'g Grammar,
prod: &'g Production,
k: &str,
visited: &mut std::collections::HashSet<&'g str>,
) -> bool {
match prod {
Production::Symbol { name } => name == k || dispatches_to(grammar, name, k, visited),
Production::Alias { value, named, .. } => *named && value == k,
Production::Choice { members } => members
.iter()
.any(|m| dispatch_prod(grammar, m, k, visited)),
Production::Token { content }
| Production::ImmediateToken { content }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. }
| Production::Field { content, .. } => dispatch_prod(grammar, content, k, visited),
_ => false,
}
}
fn bare_symbol_name(prod: &Production) -> Option<&str> {
match prod {
Production::Symbol { name } => Some(name.as_str()),
Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Token { content }
| Production::ImmediateToken { content }
| Production::Reserved { content, .. } => bare_symbol_name(content),
_ => None,
}
}
fn slot_label<'a>(labels: &[&'a str], pos: usize) -> &'a str {
labels.get(pos).copied().unwrap_or("")
}
fn label_ok(slot: &str, field_ctx: Option<&str>) -> bool {
if slot.is_empty() {
return true;
}
match field_ctx {
Some(f) => slot == f,
None => slot == "child_of",
}
}
#[must_use]
pub(crate) fn match_demand<'g>(
grammar: &'g Grammar,
prod: &'g Production,
demand: &[&str],
labels: &[&str],
pos: usize,
field_ctx: Option<&str>,
visited: &mut Vec<(&'g str, usize)>,
) -> Vec<usize> {
match prod {
Production::Blank => vec![pos],
Production::String { .. } | Production::Pattern { .. } => vec![pos],
Production::Symbol { name } => {
let is_dispatch = name.starts_with('_') || grammar.supertypes.contains(name);
if is_dispatch {
if visited.contains(&(name.as_str(), pos)) {
return vec![];
}
if let Some(rule) = grammar.rules.get(name) {
visited.push((name.as_str(), pos));
let out = match_demand(grammar, rule, demand, labels, pos, field_ctx, visited);
visited.pop();
return out;
}
return vec![pos];
}
if !grammar.rules.contains_key(name) {
if let Some(k) = demand.get(pos) {
if sat(grammar, k, name) && label_ok(slot_label(labels, pos), field_ctx) {
return vec![pos + 1];
}
}
return vec![pos];
}
match demand.get(pos) {
Some(k)
if sat(grammar, k, name) && label_ok(slot_label(labels, pos), field_ctx) =>
{
vec![pos + 1]
}
_ => vec![],
}
}
Production::Alias { named, value, .. } => {
if *named && !value.is_empty() {
match demand.get(pos) {
Some(k)
if sat(grammar, k, value)
&& label_ok(slot_label(labels, pos), field_ctx) =>
{
vec![pos + 1]
}
_ => vec![],
}
} else {
vec![pos]
}
}
Production::Field { name, content } => match_demand(
grammar,
content,
demand,
labels,
pos,
Some(name.as_str()),
visited,
),
Production::Token { content }
| Production::ImmediateToken { content }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. } => {
match_demand(grammar, content, demand, labels, pos, field_ctx, visited)
}
Production::Seq { members } => {
let mut frontier = vec![pos];
for m in members {
let mut next: Vec<usize> = Vec::new();
for &p in &frontier {
for end in match_demand(grammar, m, demand, labels, p, field_ctx, visited) {
if !next.contains(&end) {
next.push(end);
}
}
}
if next.is_empty() {
return vec![];
}
frontier = next;
}
frontier
}
Production::Choice { members } => {
let mut out: Vec<usize> = Vec::new();
for m in members {
for end in match_demand(grammar, m, demand, labels, pos, field_ctx, visited) {
if !out.contains(&end) {
out.push(end);
}
}
}
out
}
Production::Optional { content } => {
let mut out = vec![pos];
for end in match_demand(grammar, content, demand, labels, pos, field_ctx, visited) {
if !out.contains(&end) {
out.push(end);
}
}
out
}
Production::Repeat { content } => closure(
grammar, content, demand, labels, pos, field_ctx, visited, true,
),
Production::Repeat1 { content } => closure(
grammar, content, demand, labels, pos, field_ctx, visited, false,
),
}
}
#[allow(clippy::too_many_arguments)]
fn closure<'g>(
grammar: &'g Grammar,
content: &'g Production,
demand: &[&str],
labels: &[&str],
pos: usize,
field_ctx: Option<&str>,
visited: &mut Vec<(&'g str, usize)>,
reflexive: bool,
) -> Vec<usize> {
let mut seen = if reflexive { vec![pos] } else { vec![] };
let mut frontier = vec![pos];
while let Some(p) = frontier.pop() {
for end in match_demand(grammar, content, demand, labels, p, field_ctx, visited) {
if end > p && !seen.contains(&end) {
seen.push(end);
frontier.push(end);
}
}
}
seen
}
#[allow(clippy::too_many_arguments)]
fn consumes_without_reentry<'g>(
grammar: &'g Grammar,
prod: &'g Production,
demand: &[&str],
labels: &[&str],
pos: usize,
field_ctx: Option<&str>,
blocked: &str,
visited: &mut Vec<(&'g str, usize)>,
) -> Vec<usize> {
let recur = |p: &'g Production, pos: usize, fc: Option<&str>, v: &mut Vec<(&'g str, usize)>| {
consumes_without_reentry(grammar, p, demand, labels, pos, fc, blocked, v)
};
match prod {
Production::Symbol { name } => {
let is_dispatch = name.starts_with('_') || grammar.supertypes.contains(name);
if is_dispatch {
if name == blocked {
return vec![];
}
if visited.contains(&(name.as_str(), pos)) {
return vec![];
}
if let Some(rule) = grammar.rules.get(name) {
visited.push((name.as_str(), pos));
let out = recur(rule, pos, field_ctx, visited);
visited.pop();
return out;
}
return vec![pos];
}
match_demand(grammar, prod, demand, labels, pos, field_ctx, visited)
}
Production::Field { name, content } => recur(content, pos, Some(name.as_str()), visited),
Production::Token { content }
| Production::ImmediateToken { content }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. } => recur(content, pos, field_ctx, visited),
Production::Seq { members } => {
let mut frontier = vec![pos];
for m in members {
let mut next: Vec<usize> = Vec::new();
for &p in &frontier {
for end in recur(m, p, field_ctx, visited) {
if !next.contains(&end) {
next.push(end);
}
}
}
if next.is_empty() {
return vec![];
}
frontier = next;
}
frontier
}
Production::Choice { members } => {
let mut out: Vec<usize> = Vec::new();
for m in members {
for end in recur(m, pos, field_ctx, visited) {
if !out.contains(&end) {
out.push(end);
}
}
}
out
}
Production::Optional { content } => {
let mut out = vec![pos];
for end in recur(content, pos, field_ctx, visited) {
if !out.contains(&end) {
out.push(end);
}
}
out
}
_ => match_demand(grammar, prod, demand, labels, pos, field_ctx, visited),
}
}
struct Candidates {
best_len: usize,
cands: Vec<usize>,
num_viable: usize,
viable: Vec<usize>,
}
struct FieldBinding<'p> {
literals: Vec<&'p str>,
binds_symbol: bool,
always: bool,
}
fn analyse_field<'g>(
grammar: &'g Grammar,
prod: &'g Production,
target: &str,
visited: &mut Vec<&'g str>,
) -> FieldBinding<'g> {
fn direct_literals<'g>(prod: &'g Production, out: &mut Vec<&'g str>) {
match prod {
Production::String { value } => out.push(value),
Production::Alias {
named: false,
value,
..
} if !value.is_empty() => out.push(value),
Production::Choice { members } => {
for m in members {
direct_literals(m, out);
}
}
Production::Token { content }
| Production::ImmediateToken { content }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. } => direct_literals(content, out),
_ => {}
}
}
fn binds_symbol(prod: &Production) -> bool {
match prod {
Production::Symbol { .. } | Production::Pattern { .. } => true,
Production::Choice { members } | Production::Seq { members } => {
members.iter().any(binds_symbol)
}
Production::Repeat { content }
| Production::Repeat1 { content }
| Production::Optional { content }
| Production::Alias { content, .. }
| Production::Token { content }
| Production::ImmediateToken { content }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. } => binds_symbol(content),
_ => false,
}
}
let empty = || FieldBinding {
literals: Vec::new(),
binds_symbol: false,
always: false,
};
match prod {
Production::Field { name, content } => {
if name == target {
let mut literals = Vec::new();
direct_literals(content, &mut literals);
FieldBinding {
literals,
binds_symbol: binds_symbol(content),
always: true,
}
} else {
analyse_field(grammar, content, target, visited)
}
}
Production::Symbol { name } => {
let is_dispatch = name.starts_with('_') || grammar.supertypes.contains(name);
if is_dispatch && !visited.contains(&name.as_str()) {
if let Some(rule) = grammar.rules.get(name) {
visited.push(name.as_str());
let fb = analyse_field(grammar, rule, target, visited);
visited.pop();
return fb;
}
}
empty()
}
Production::Seq { members } => {
let mut acc = empty();
for m in members {
let fb = analyse_field(grammar, m, target, visited);
acc.literals.extend(fb.literals);
acc.binds_symbol |= fb.binds_symbol;
acc.always |= fb.always;
}
acc
}
Production::Choice { members } => {
let mut acc = FieldBinding {
literals: Vec::new(),
binds_symbol: false,
always: !members.is_empty(),
};
for m in members {
let fb = analyse_field(grammar, m, target, visited);
acc.literals.extend(fb.literals);
acc.binds_symbol |= fb.binds_symbol;
acc.always &= fb.always;
}
acc
}
Production::Optional { content } | Production::Repeat { content } => {
let fb = analyse_field(grammar, content, target, visited);
FieldBinding {
literals: fb.literals,
binds_symbol: fb.binds_symbol,
always: false,
}
}
Production::Repeat1 { content }
| Production::Token { content }
| Production::ImmediateToken { content }
| Production::Alias { content, .. }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. } => analyse_field(grammar, content, target, visited),
_ => empty(),
}
}
fn field_value_consistent(
grammar: &Grammar,
alt: &Production,
field_constraints: &[(&str, &str)],
) -> bool {
let mut names = std::collections::HashSet::new();
collect_field_names(alt, &mut names);
for name in names {
let mut visited = Vec::new();
let fb = analyse_field(grammar, alt, name, &mut visited);
if fb.binds_symbol || fb.literals.is_empty() {
continue;
}
match field_constraints.iter().find(|(n, _)| *n == name) {
Some((_, value)) => {
if !fb.literals.iter().any(|l| l == value) {
return false;
}
}
None => {
if fb.always {
return false;
}
}
}
}
true
}
fn choice_candidates(
grammar: &Grammar,
alternatives: &[Production],
demand: &[&str],
labels: &[&str],
initial_field_ctx: Option<&str>,
field_constraints: &[(&str, &str)],
) -> Candidates {
let mut best_len = 0usize;
let mut cands: Vec<usize> = Vec::new();
let mut num_viable = 0usize;
let mut viable: Vec<usize> = Vec::new();
for (i, alt) in alternatives.iter().enumerate() {
let mut visited = Vec::new();
if match_demand(grammar, alt, demand, &[], 0, None, &mut visited)
.into_iter()
.max()
.is_none()
{
continue;
}
num_viable += 1;
if !field_value_consistent(grammar, alt, field_constraints) {
continue;
}
let mut lv = Vec::new();
let Some(max_end) =
match_demand(grammar, alt, demand, labels, 0, initial_field_ctx, &mut lv)
.into_iter()
.max()
else {
continue;
};
viable.push(i);
if max_end > best_len {
best_len = max_end;
cands = vec![i];
} else if max_end == best_len {
cands.push(i);
}
}
Candidates {
best_len,
cands,
num_viable,
viable,
}
}
fn reenters_repeat<'g>(
grammar: &'g Grammar,
prod: &'g Production,
self_rule: &str,
in_repeat: bool,
visited: &mut Vec<&'g str>,
) -> bool {
match prod {
Production::Symbol { name } => {
if in_repeat && name == self_rule {
return true;
}
if name.starts_with('_') && !visited.contains(&name.as_str()) {
if let Some(rule) = grammar.rules.get(name) {
visited.push(name.as_str());
let r = reenters_repeat(grammar, rule, self_rule, in_repeat, visited);
visited.pop();
return r;
}
}
false
}
Production::Repeat { content } | Production::Repeat1 { content } => {
reenters_repeat(grammar, content, self_rule, true, visited)
}
Production::Seq { members } | Production::Choice { members } => members
.iter()
.any(|m| reenters_repeat(grammar, m, self_rule, in_repeat, visited)),
Production::Optional { content }
| Production::Field { content, .. }
| Production::Token { content }
| Production::ImmediateToken { content }
| Production::Alias { content, .. }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. } => {
reenters_repeat(grammar, content, self_rule, in_repeat, visited)
}
_ => false,
}
}
fn alt_literals(prod: &Production) -> Vec<String> {
let mut out = Vec::new();
collect_literals(prod, &mut out);
out
}
fn alt_literals_resolved(grammar: &Grammar, prod: &Production) -> Vec<String> {
let inline = alt_literals(prod);
if !inline.is_empty() {
return inline;
}
let Some(name) = bare_symbol_name(prod) else {
return inline;
};
let mut out = Vec::new();
let mut visiting = Vec::new();
collect_leading_literals(grammar, name, &mut out, &mut visiting);
out
}
fn collect_leading_literals<'g>(
grammar: &'g Grammar,
name: &'g str,
out: &mut Vec<String>,
visiting: &mut Vec<&'g str>,
) {
if visiting.contains(&name) {
return;
}
let Some(rule) = grammar.rules.get(name) else {
return;
};
visiting.push(name);
leading_literals_of(grammar, rule, out, visiting);
visiting.pop();
}
fn leading_literals_of<'g>(
grammar: &'g Grammar,
prod: &'g Production,
out: &mut Vec<String>,
visiting: &mut Vec<&'g str>,
) {
match prod {
Production::String { value } => out.push(value.clone()),
Production::Seq { members } => {
if let Some(first) = members.first() {
leading_literals_of(grammar, first, out, visiting);
}
}
Production::Choice { members } => {
for m in members {
leading_literals_of(grammar, m, out, visiting);
}
}
Production::Repeat { content }
| Production::Repeat1 { content }
| Production::Optional { content }
| Production::Field { content, .. }
| Production::Token { content }
| Production::ImmediateToken { content }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. }
| Production::Alias { content, .. } => {
leading_literals_of(grammar, content, out, visiting);
}
Production::Symbol { name } => {
collect_leading_literals(grammar, name, out, visiting);
}
_ => {}
}
}
fn collect_literals(prod: &Production, out: &mut Vec<String>) {
match prod {
Production::String { value } => out.push(value.clone()),
Production::Seq { members } | Production::Choice { members } => {
for m in members {
collect_literals(m, out);
}
}
Production::Repeat { content }
| Production::Repeat1 { content }
| Production::Optional { content }
| Production::Field { content, .. }
| Production::Token { content }
| Production::ImmediateToken { content }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. } => collect_literals(content, out),
_ => {}
}
}
#[must_use]
pub(crate) fn select_choice_with_trace(
grammar: &Grammar,
alternatives: &[Production],
demand: &[&str],
labels: &[&str],
initial_field_ctx: Option<&str>,
field_constraints: &[(&str, &str)],
trace_tokens: &[String],
self_rule: Option<&str>,
positional_remaining: &str,
) -> Option<usize> {
let Candidates {
best_len,
cands,
num_viable,
viable,
} = choice_candidates(
grammar,
alternatives,
demand,
labels,
initial_field_ctx,
field_constraints,
);
if cands.is_empty() {
return None;
}
if let (Some(self_rule), Some(&child_kind)) = (self_rule, demand.first()) {
let direct_viable: Vec<usize> = viable
.iter()
.copied()
.filter(|&i| bare_symbol_name(&alternatives[i]) == Some(child_kind))
.collect();
if direct_viable.len() == 1 {
let d = direct_viable[0];
let winner_is_direct = cands
.iter()
.any(|&i| bare_symbol_name(&alternatives[i]) == Some(child_kind));
let winner_reenters = cands.iter().any(|&i| {
let mut v = Vec::new();
reenters_repeat(grammar, &alternatives[i], self_rule, false, &mut v)
});
if !winner_is_direct && !cands.contains(&d) && winner_reenters {
return Some(d);
}
}
}
if cands.len() == 1 && (best_len > 0 || num_viable == 1) {
return Some(cands[0]);
}
if let Some(self_rule) = self_rule {
let mut best_direct = 0usize;
let mut best_direct_alt: Option<usize> = None;
let mut direct_tied = false;
for &i in &cands {
let mut v = Vec::new();
let reach = consumes_without_reentry(
grammar,
&alternatives[i],
demand,
labels,
0,
initial_field_ctx,
self_rule,
&mut v,
)
.into_iter()
.max()
.unwrap_or(0);
if reach > best_direct {
best_direct = reach;
best_direct_alt = Some(i);
direct_tied = false;
} else if reach == best_direct && best_direct_alt.is_some() {
direct_tied = true;
}
}
if best_direct > 0 && !direct_tied {
if let Some(i) = best_direct_alt {
return Some(i);
}
}
}
if let Some(&child_kind) = demand.first() {
let direct: Vec<usize> = cands
.iter()
.copied()
.filter(|&i| bare_symbol_name(&alternatives[i]) == Some(child_kind))
.collect();
if direct.len() == 1 {
return Some(direct[0]);
}
}
let optional_with_blank =
best_len == 0 && alternatives.iter().any(|a| matches!(a, Production::Blank));
if !trace_tokens.is_empty() {
let mut best_overlap = 0usize;
let mut winner: Option<usize> = None;
let mut winner_count = 0usize;
for &i in &cands {
let lits = alt_literals_resolved(grammar, &alternatives[i]);
let overlap = lits.iter().filter(|l| trace_tokens.contains(l)).count();
if overlap > best_overlap {
best_overlap = overlap;
winner = Some(i);
winner_count = 1;
} else if overlap == best_overlap && overlap > 0 {
winner_count += 1;
}
}
if best_overlap > 0 && winner_count == 1 {
if optional_with_blank && !positional_remaining.is_empty() {
let literal_in_layout = winner.is_some_and(|i| {
alt_literals_resolved(grammar, &alternatives[i])
.iter()
.any(|l| positional_remaining.contains(l.as_str()))
});
if !literal_in_layout {
return alternatives
.iter()
.position(|a| matches!(a, Production::Blank));
}
}
return winner;
}
}
if best_len == 0 {
if let Some(b) = alternatives
.iter()
.position(|a| matches!(a, Production::Blank))
{
return Some(b);
}
}
None
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::emit_pretty::Grammar;
fn grammar(json: &str) -> Grammar {
Grammar::from_bytes("test", json.as_bytes()).expect("parse grammar")
}
fn sym(name: &str) -> serde_json::Value {
serde_json::json!({"type": "SYMBOL", "name": name})
}
fn str_(v: &str) -> serde_json::Value {
serde_json::json!({"type": "STRING", "value": v})
}
#[test]
fn picks_alternative_by_child_kind() {
let g = grammar(
&serde_json::json!({
"name": "test",
"rules": {
"expr": {"type": "CHOICE", "members": [
{"type": "SEQ", "members": [sym("number"), str_("+"), sym("number")]},
{"type": "SEQ", "members": [sym("string"), str_("~"), sym("string")]},
]},
"number": str_("0"),
"string": str_("s"),
}
})
.to_string(),
);
let alts = match &g.rules["expr"] {
Production::Choice { members } => members,
_ => panic!(),
};
assert_eq!(
select_choice_with_trace(
&g,
alts,
&["number", "number"],
&[],
None,
&[],
&[],
None,
""
),
Some(0)
);
assert_eq!(
select_choice_with_trace(
&g,
alts,
&["string", "string"],
&[],
None,
&[],
&[],
None,
""
),
Some(1)
);
}
#[test]
fn wrapper_does_not_steal_via_deep_reachability() {
let g = grammar(
&serde_json::json!({
"name": "test",
"rules": {
"declarator": {"type": "CHOICE", "members": [
sym("template_parameters"),
sym("identifier"),
]},
"template_parameters": {"type": "SEQ", "members": [
str_("<"), sym("int_literal"), str_(">")]},
"identifier": str_("x"),
"int_literal": str_("0"),
}
})
.to_string(),
);
let alts = match &g.rules["declarator"] {
Production::Choice { members } => members,
_ => panic!(),
};
assert_eq!(
select_choice_with_trace(&g, alts, &["identifier"], &[], None, &[], &[], None, ""),
Some(1)
);
assert_eq!(
select_choice_with_trace(&g, alts, &["int_literal"], &[], None, &[], &[], None, ""),
None
);
}
#[test]
fn supertype_member_satisfies_supertype_symbol() {
let g = grammar(
&serde_json::json!({
"name": "test",
"supertypes": ["_literal"],
"rules": {
"_literal": {"type": "CHOICE", "members": [sym("int"), sym("float")]},
"int": str_("0"),
"float": str_("0.0"),
}
})
.to_string(),
);
assert!(sat(&g, "int", "_literal"));
assert!(sat(&g, "float", "_literal"));
assert!(!sat(&g, "string", "_literal"));
assert!(sat(&g, "int", "int"));
}
#[test]
fn direct_bare_symbol_wins_tie_over_alias() {
let g = grammar(
&serde_json::json!({
"name": "test",
"rules": {
"_top_level_item": {"type": "CHOICE", "members": [
sym("function_definition"),
sym("declaration"),
{"type": "ALIAS", "named": true, "value": "declaration",
"content": sym("command_declaration")},
]},
"function_definition": str_("f"),
"declaration": str_("d"),
"command_declaration": str_("c"),
}
})
.to_string(),
);
let alts = match &g.rules["_top_level_item"] {
Production::Choice { members } => members,
_ => panic!(),
};
assert_eq!(
select_choice_with_trace(&g, alts, &["declaration"], &[], None, &[], &[], None, ""),
Some(1)
);
}
#[test]
fn ambiguous_alias_defers() {
let g = grammar(
&serde_json::json!({
"name": "test",
"rules": {
"site": {"type": "CHOICE", "members": [
{"type": "ALIAS", "named": true, "value": "binary",
"content": sym("_pow")},
{"type": "ALIAS", "named": true, "value": "binary",
"content": sym("command_binary")},
]},
"_pow": str_("**"),
"command_binary": str_("+"),
}
})
.to_string(),
);
let alts = match &g.rules["site"] {
Production::Choice { members } => members,
_ => panic!(),
};
assert_eq!(
select_choice_with_trace(&g, alts, &["binary"], &[], None, &[], &[], None, ""),
None
);
}
#[test]
fn optional_token_defaults_to_blank_unless_traced() {
let g = grammar(
&serde_json::json!({
"name": "test",
"rules": {
"opt_semi": {"type": "CHOICE", "members": [str_(";"), {"type": "BLANK"}]},
}
})
.to_string(),
);
let alts = match &g.rules["opt_semi"] {
Production::Choice { members } => members,
_ => panic!(),
};
assert_eq!(
select_choice_with_trace(&g, alts, &[], &[], None, &[], &[], None, ""),
Some(1)
);
assert_eq!(
select_choice_with_trace(&g, alts, &[], &[], None, &[], &[";".to_owned()], None, ""),
Some(0)
);
}
#[test]
fn trace_token_breaks_keyword_tie() {
let g = grammar(
&serde_json::json!({
"name": "test",
"rules": {
"jump": {"type": "CHOICE", "members": [
{"type": "SEQ", "members": [str_("return"), sym("expr")]},
{"type": "SEQ", "members": [str_("throw"), sym("expr")]},
]},
"expr": str_("e"),
}
})
.to_string(),
);
let alts = match &g.rules["jump"] {
Production::Choice { members } => members,
_ => panic!(),
};
assert_eq!(
select_choice_with_trace(&g, alts, &["expr"], &[], None, &[], &[], None, ""),
None
);
assert_eq!(
select_choice_with_trace(
&g,
alts,
&["expr"],
&[],
None,
&[],
&["return".to_owned()],
None,
""
),
Some(0)
);
assert_eq!(
select_choice_with_trace(
&g,
alts,
&["expr"],
&[],
None,
&[],
&["throw".to_owned()],
None,
""
),
Some(1)
);
assert_eq!(
select_choice_with_trace(
&g,
alts,
&["expr"],
&[],
None,
&[],
&["xyz".to_owned()],
None,
""
),
None
);
}
#[test]
fn unique_viable_blank_beats_rejected_optional() {
let g = grammar(
&serde_json::json!({
"name": "test",
"rules": {
"declarator": {"type": "SEQ", "members": [
{"type": "FIELD", "name": "name", "content": sym("identifier")},
{"type": "CHOICE", "members": [sym("_initializer"), {"type": "BLANK"}]},
]},
"_initializer": {"type": "SEQ", "members": [str_("="), sym("expression")]},
"identifier": str_("x"),
"expression": str_("e"),
}
})
.to_string(),
);
let alts = match &g.rules["declarator"] {
Production::Seq { members } => match &members[1] {
Production::Choice { members } => members,
_ => panic!(),
},
_ => panic!(),
};
assert_eq!(
select_choice_with_trace(&g, alts, &[], &[], None, &[], &[], None, ""),
Some(1)
);
assert_eq!(
select_choice_with_trace(&g, alts, &["expression"], &[], None, &[], &[], None, ""),
Some(0)
);
}
#[test]
fn repeat_does_not_force_swallow_of_mandatory() {
let g = grammar(
&serde_json::json!({
"name": "test",
"rules": {
"statements": {"type": "SEQ", "members": [
{"type": "REPEAT", "content": sym("stmt")},
sym("stmt"),
]},
"stmt": str_(";"),
}
})
.to_string(),
);
let mut v = Vec::new();
let ends = match_demand(
&g,
&g.rules["statements"],
&["stmt", "stmt"],
&[],
0,
None,
&mut v,
);
assert!(ends.contains(&2), "must fully consume 2 stmts: {ends:?}");
}
#[test]
fn field_value_rejects_contradicting_and_absent_literal() {
let g = grammar(
&serde_json::json!({
"name": "test",
"rules": {
"header": {"type": "CHOICE", "members": [
{"type": "FIELD", "name": "left", "content": sym("expr")},
{"type": "SEQ", "members": [
{"type": "FIELD", "name": "kind", "content": str_("var")},
{"type": "FIELD", "name": "left", "content": sym("expr")},
{"type": "CHOICE", "members": [
{"type": "SEQ", "members": [str_("="), sym("expr")]},
{"type": "BLANK"},
]},
]},
{"type": "SEQ", "members": [
{"type": "FIELD", "name": "kind", "content":
{"type": "CHOICE", "members": [str_("let"), str_("const")]}},
{"type": "FIELD", "name": "left", "content": sym("expr")},
]},
]},
"expr": str_("x"),
}
})
.to_string(),
);
let alts = match &g.rules["header"] {
Production::Choice { members } => members,
_ => panic!(),
};
assert_ne!(
select_choice_with_trace(
&g,
alts,
&["expr", "expr"],
&[],
None,
&[("kind", "const")],
&[],
None,
""
),
Some(1)
);
assert_eq!(
select_choice_with_trace(&g, alts, &["expr", "expr"], &[], None, &[], &[], None, ""),
Some(0)
);
}
}