#![allow(
clippy::module_name_repetitions,
clippy::too_many_lines,
clippy::too_many_arguments,
clippy::map_unwrap_or,
clippy::option_if_let_else,
clippy::elidable_lifetime_names,
clippy::items_after_statements,
clippy::needless_pass_by_value,
clippy::single_match_else,
clippy::manual_let_else,
clippy::match_same_arms,
clippy::missing_const_for_fn,
clippy::single_char_pattern,
clippy::naive_bytecount,
clippy::expect_used,
clippy::redundant_pub_crate,
clippy::used_underscore_binding,
clippy::redundant_field_names,
clippy::struct_field_names,
clippy::redundant_else,
clippy::similar_names
)]
use std::collections::BTreeMap;
use panproto_schema::{Edge, Schema};
use serde::Deserialize;
use crate::error::ParseError;
#[derive(Debug, Clone, Deserialize)]
#[serde(tag = "type")]
#[non_exhaustive]
pub enum Production {
#[serde(rename = "SEQ")]
Seq {
members: Vec<Self>,
},
#[serde(rename = "CHOICE")]
Choice {
members: Vec<Self>,
},
#[serde(rename = "REPEAT")]
Repeat {
content: Box<Self>,
},
#[serde(rename = "REPEAT1")]
Repeat1 {
content: Box<Self>,
},
#[serde(rename = "OPTIONAL")]
Optional {
content: Box<Self>,
},
#[serde(rename = "SYMBOL")]
Symbol {
name: String,
},
#[serde(rename = "STRING")]
String {
value: String,
},
#[serde(rename = "PATTERN")]
Pattern {
value: String,
},
#[serde(rename = "BLANK")]
Blank,
#[serde(rename = "FIELD")]
Field {
name: String,
content: Box<Self>,
},
#[serde(rename = "ALIAS")]
Alias {
content: Box<Self>,
#[serde(default)]
named: bool,
#[serde(default)]
value: String,
},
#[serde(rename = "TOKEN")]
Token {
content: Box<Self>,
},
#[serde(rename = "IMMEDIATE_TOKEN")]
ImmediateToken {
content: Box<Self>,
},
#[serde(rename = "PREC")]
Prec {
#[allow(dead_code)]
value: serde_json::Value,
content: Box<Self>,
},
#[serde(rename = "PREC_LEFT")]
PrecLeft {
#[allow(dead_code)]
value: serde_json::Value,
content: Box<Self>,
},
#[serde(rename = "PREC_RIGHT")]
PrecRight {
#[allow(dead_code)]
value: serde_json::Value,
content: Box<Self>,
},
#[serde(rename = "PREC_DYNAMIC")]
PrecDynamic {
#[allow(dead_code)]
value: serde_json::Value,
content: Box<Self>,
},
#[serde(rename = "RESERVED")]
Reserved {
content: Box<Self>,
#[allow(dead_code)]
#[serde(default)]
context_name: String,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct Grammar {
#[allow(dead_code)]
pub name: String,
pub rules: BTreeMap<String, Production>,
#[serde(default, deserialize_with = "deserialize_supertypes")]
pub supertypes: std::collections::HashSet<String>,
#[serde(skip)]
pub subtypes: std::collections::HashMap<String, std::collections::HashSet<String>>,
}
fn deserialize_supertypes<'de, D>(
deserializer: D,
) -> Result<std::collections::HashSet<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
let entries: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;
let mut out = std::collections::HashSet::new();
for entry in entries {
match entry {
serde_json::Value::String(s) => {
out.insert(s);
}
serde_json::Value::Object(map) => {
if let Some(serde_json::Value::String(name)) = map.get("name") {
out.insert(name.clone());
}
}
_ => {}
}
}
Ok(out)
}
impl Grammar {
pub fn from_bytes(protocol: &str, bytes: &[u8]) -> Result<Self, ParseError> {
let mut grammar: Self =
serde_json::from_slice(bytes).map_err(|e| ParseError::EmitFailed {
protocol: protocol.to_owned(),
reason: format!("grammar.json deserialization failed: {e}"),
})?;
grammar.subtypes = compute_subtype_closure(&grammar);
Ok(grammar)
}
}
fn compute_subtype_closure(
grammar: &Grammar,
) -> std::collections::HashMap<String, std::collections::HashSet<String>> {
use std::collections::{HashMap, HashSet};
let mut subtypes: HashMap<String, HashSet<String>> = HashMap::new();
for name in grammar.rules.keys() {
subtypes
.entry(name.clone())
.or_default()
.insert(name.clone());
}
fn walk<'g>(
grammar: &'g Grammar,
production: &'g Production,
visited: &mut HashSet<&'g str>,
out: &mut HashSet<String>,
) {
match production {
Production::Symbol { name } => {
out.insert(name.clone());
let expand = name.starts_with('_') || grammar.supertypes.contains(name.as_str());
if expand && visited.insert(name.as_str()) {
if let Some(rule) = grammar.rules.get(name) {
walk(grammar, rule, visited, out);
}
}
}
Production::Choice { members } | Production::Seq { members } => {
for m in members {
walk(grammar, m, visited, out);
}
}
Production::Alias {
content,
named,
value,
} => {
if *named && !value.is_empty() {
out.insert(value.clone());
}
walk(grammar, content, visited, 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, .. } => {
walk(grammar, content, visited, out);
}
_ => {}
}
}
for (name, rule) in &grammar.rules {
let expand = name.starts_with('_') || grammar.supertypes.contains(name.as_str());
if !expand {
continue;
}
let mut visited: HashSet<&str> = HashSet::new();
visited.insert(name.as_str());
let mut reachable: HashSet<String> = HashSet::new();
walk(grammar, rule, &mut visited, &mut reachable);
for kind in &reachable {
subtypes
.entry(kind.clone())
.or_default()
.insert(name.clone());
}
}
fn collect_aliases<'g>(production: &'g Production, out: &mut Vec<(String, &'g Production)>) {
match production {
Production::Alias {
content,
named,
value,
} => {
if *named && !value.is_empty() {
out.push((value.clone(), content.as_ref()));
}
collect_aliases(content, out);
}
Production::Choice { members } | Production::Seq { members } => {
for m in members {
collect_aliases(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_aliases(content, out);
}
_ => {}
}
}
let mut aliases: Vec<(String, &Production)> = Vec::new();
for rule in grammar.rules.values() {
collect_aliases(rule, &mut aliases);
}
for (alias_value, content) in aliases {
let mut visited: HashSet<&str> = HashSet::new();
let mut reachable: HashSet<String> = HashSet::new();
walk(grammar, content, &mut visited, &mut reachable);
subtypes
.entry(alias_value.clone())
.or_default()
.insert(alias_value.clone());
for kind in reachable {
subtypes
.entry(kind)
.or_default()
.insert(alias_value.clone());
}
}
for _ in 0..8 {
let snapshot = subtypes.clone();
let mut changed = false;
for (kind, supers) in &snapshot {
let extra: HashSet<String> = supers
.iter()
.flat_map(|s| snapshot.get(s).cloned().unwrap_or_default())
.collect();
let entry = subtypes.entry(kind.clone()).or_default();
for s in extra {
if entry.insert(s) {
changed = true;
}
}
}
if !changed {
break;
}
}
subtypes
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FormatPolicy {
pub indent_width: usize,
pub separator: String,
pub newline: String,
pub line_break_after: Vec<String>,
pub indent_open: Vec<String>,
pub indent_close: Vec<String>,
}
impl Default for FormatPolicy {
fn default() -> Self {
Self {
indent_width: 2,
separator: " ".to_owned(),
newline: "\n".to_owned(),
line_break_after: vec![";".into(), "{".into(), "}".into()],
indent_open: vec!["{".into()],
indent_close: vec!["}".into()],
}
}
}
pub fn emit_pretty(
protocol: &str,
schema: &Schema,
grammar: &Grammar,
policy: &FormatPolicy,
) -> Result<Vec<u8>, ParseError> {
let roots = collect_roots(schema);
if roots.is_empty() {
return Err(ParseError::EmitFailed {
protocol: protocol.to_owned(),
reason: "schema has no entry vertices".to_owned(),
});
}
let mut out = Output::new(policy);
for (i, root) in roots.iter().enumerate() {
if i > 0 {
out.newline();
}
emit_vertex(protocol, schema, grammar, root, &mut out)?;
}
Ok(out.finish())
}
fn collect_roots(schema: &Schema) -> Vec<&panproto_gat::Name> {
if !schema.entries.is_empty() {
return schema
.entries
.iter()
.filter(|name| schema.vertices.contains_key(*name))
.collect();
}
let mut targets: std::collections::HashSet<&panproto_gat::Name> =
std::collections::HashSet::new();
for edge in schema.edges.keys() {
targets.insert(&edge.tgt);
}
let mut roots: Vec<&panproto_gat::Name> = schema
.vertices
.keys()
.filter(|name| !targets.contains(name))
.collect();
roots.sort();
roots
}
fn emit_vertex(
protocol: &str,
schema: &Schema,
grammar: &Grammar,
vertex_id: &panproto_gat::Name,
out: &mut Output<'_>,
) -> Result<(), ParseError> {
let vertex = schema
.vertices
.get(vertex_id)
.ok_or_else(|| ParseError::EmitFailed {
protocol: protocol.to_owned(),
reason: format!("vertex '{vertex_id}' not found"),
})?;
if let Some(literal) = literal_value(schema, vertex_id) {
if children_for(schema, vertex_id).is_empty() {
out.token(literal);
return Ok(());
}
}
let kind = vertex.kind.as_ref();
let edges = children_for(schema, vertex_id);
if let Some(rule) = grammar.rules.get(kind) {
let mut cursor = ChildCursor::new(&edges);
return emit_production(protocol, schema, grammar, vertex_id, rule, &mut cursor, out);
}
for edge in &edges {
emit_vertex(protocol, schema, grammar, &edge.tgt, out)?;
}
Ok(())
}
struct ChildCursor<'a> {
edges: &'a [&'a Edge],
consumed: Vec<bool>,
}
impl<'a> ChildCursor<'a> {
fn new(edges: &'a [&'a Edge]) -> Self {
Self {
edges,
consumed: vec![false; edges.len()],
}
}
fn take_field(&mut self, field_name: &str) -> Option<&'a Edge> {
for (i, edge) in self.edges.iter().enumerate() {
if !self.consumed[i] && edge.kind.as_ref() == field_name {
self.consumed[i] = true;
return Some(edge);
}
}
None
}
#[cfg(test)]
fn has_matching(&self, predicate: impl Fn(&Edge) -> bool) -> bool {
self.edges
.iter()
.enumerate()
.any(|(i, edge)| !self.consumed[i] && predicate(edge))
}
fn take_matching(&mut self, predicate: impl Fn(&Edge) -> bool) -> Option<&'a Edge> {
for (i, edge) in self.edges.iter().enumerate() {
if !self.consumed[i] && predicate(edge) {
self.consumed[i] = true;
return Some(edge);
}
}
None
}
}
thread_local! {
static EMIT_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
static EMIT_MU_FRAMES: std::cell::RefCell<std::collections::HashSet<(String, String)>> =
std::cell::RefCell::new(std::collections::HashSet::new());
static EMIT_FIELD_CONTEXT: std::cell::RefCell<Option<String>> =
const { std::cell::RefCell::new(None) };
}
struct FieldContextGuard(Option<String>);
impl Drop for FieldContextGuard {
fn drop(&mut self) {
EMIT_FIELD_CONTEXT.with(|f| *f.borrow_mut() = self.0.take());
}
}
fn push_field_context(name: &str) -> FieldContextGuard {
let prev = EMIT_FIELD_CONTEXT.with(|f| f.borrow_mut().replace(name.to_owned()));
FieldContextGuard(prev)
}
fn clear_field_context() -> FieldContextGuard {
let prev = EMIT_FIELD_CONTEXT.with(|f| f.borrow_mut().take());
FieldContextGuard(prev)
}
fn current_field_context() -> Option<String> {
EMIT_FIELD_CONTEXT.with(|f| f.borrow().clone())
}
fn walk_in_mu_frame(
protocol: &str,
schema: &Schema,
grammar: &Grammar,
vertex_id: &panproto_gat::Name,
rule_name: &str,
rule: &Production,
cursor: &mut ChildCursor<'_>,
out: &mut Output<'_>,
) -> Result<(), ParseError> {
let key = (vertex_id.to_string(), rule_name.to_owned());
let inserted = EMIT_MU_FRAMES.with(|frames| frames.borrow_mut().insert(key.clone()));
if !inserted {
return Ok(());
}
let result = emit_production(protocol, schema, grammar, vertex_id, rule, cursor, out);
EMIT_MU_FRAMES.with(|frames| {
frames.borrow_mut().remove(&key);
});
result
}
fn emit_production(
protocol: &str,
schema: &Schema,
grammar: &Grammar,
vertex_id: &panproto_gat::Name,
production: &Production,
cursor: &mut ChildCursor<'_>,
out: &mut Output<'_>,
) -> Result<(), ParseError> {
let depth = EMIT_DEPTH.with(|d| {
let v = d.get() + 1;
d.set(v);
v
});
if depth > 500 {
EMIT_DEPTH.with(|d| d.set(d.get() - 1));
return Err(ParseError::EmitFailed {
protocol: protocol.to_owned(),
reason: format!(
"emit_production recursion >500 (likely a cyclic grammar; \
vertex='{vertex_id}')"
),
});
}
let result = emit_production_inner(
protocol, schema, grammar, vertex_id, production, cursor, out,
);
EMIT_DEPTH.with(|d| d.set(d.get() - 1));
result
}
fn emit_production_inner(
protocol: &str,
schema: &Schema,
grammar: &Grammar,
vertex_id: &panproto_gat::Name,
production: &Production,
cursor: &mut ChildCursor<'_>,
out: &mut Output<'_>,
) -> Result<(), ParseError> {
match production {
Production::String { value } => {
out.token(value);
Ok(())
}
Production::Pattern { value } => {
if let Some(literal) = literal_value(schema, vertex_id) {
out.token(literal);
} else if is_newline_like_pattern(value) {
out.newline();
} else if is_whitespace_only_pattern(value) {
} else {
out.token(&placeholder_for_pattern(value));
}
Ok(())
}
Production::Blank => Ok(()),
Production::Symbol { name } => {
if let Some(field) = current_field_context() {
if let Some(edge) = cursor.take_field(&field) {
return emit_in_child_context(
protocol, schema, grammar, &edge.tgt, production, out,
);
}
return Ok(());
}
if name.starts_with('_') {
if let Some(rule) = grammar.rules.get(name) {
walk_in_mu_frame(
protocol, schema, grammar, vertex_id, name, rule, cursor, out,
)
} else {
if name == "_indent" || name.ends_with("_indent") {
out.indent_open();
} else if name == "_dedent" || name.ends_with("_dedent") {
out.indent_close();
} else if name.contains("line_ending")
|| name.contains("newline")
|| name.ends_with("_or_eof")
{
out.newline();
}
Ok(())
}
} else if let Some(edge) = take_symbol_match(grammar, schema, cursor, name) {
emit_vertex(protocol, schema, grammar, &edge.tgt, out)
} else if vertex_id_kind(schema, vertex_id) == Some(name.as_str()) {
let rule = grammar
.rules
.get(name)
.ok_or_else(|| ParseError::EmitFailed {
protocol: protocol.to_owned(),
reason: format!("no production for SYMBOL '{name}'"),
})?;
walk_in_mu_frame(
protocol, schema, grammar, vertex_id, name, rule, cursor, out,
)
} else {
Ok(())
}
}
Production::Seq { members } => {
for member in members {
emit_production(protocol, schema, grammar, vertex_id, member, cursor, out)?;
}
Ok(())
}
Production::Choice { members } => {
if let Some(matched) =
pick_choice_with_cursor(schema, grammar, vertex_id, cursor, members)
{
emit_production(protocol, schema, grammar, vertex_id, matched, cursor, out)
} else {
Ok(())
}
}
Production::Repeat { content } | Production::Repeat1 { content } => {
let mut emitted_any = false;
loop {
let cursor_snap = cursor.consumed.clone();
let out_snap = out.snapshot();
let consumed_before = cursor.consumed.iter().filter(|&&c| c).count();
let result =
emit_production(protocol, schema, grammar, vertex_id, content, cursor, out);
let consumed_after = cursor.consumed.iter().filter(|&&c| c).count();
if result.is_err() || consumed_after == consumed_before {
cursor.consumed = cursor_snap;
out.restore(out_snap);
break;
}
emitted_any = true;
}
if matches!(production, Production::Repeat1 { .. }) && !emitted_any {
emit_production(protocol, schema, grammar, vertex_id, content, cursor, out)?;
}
Ok(())
}
Production::Optional { content } => {
let cursor_snap = cursor.consumed.clone();
let out_snap = out.snapshot();
let consumed_before = cursor.consumed.iter().filter(|&&c| c).count();
let result =
emit_production(protocol, schema, grammar, vertex_id, content, cursor, out);
if result.is_err() {
cursor.consumed = cursor_snap;
out.restore(out_snap);
return result;
}
let consumed_after = cursor.consumed.iter().filter(|&&c| c).count();
if consumed_after == consumed_before
&& !has_relevant_constraint(content, schema, vertex_id)
{
cursor.consumed = cursor_snap;
out.restore(out_snap);
}
Ok(())
}
Production::Field { name, content } => {
let _guard = push_field_context(name);
emit_production(protocol, schema, grammar, vertex_id, content, cursor, out)
}
Production::Alias {
content,
named,
value,
} => {
if *named && !value.is_empty() {
if let Some(edge) = cursor.take_matching(|edge| {
schema
.vertices
.get(&edge.tgt)
.map(|v| v.kind.as_ref() == value.as_str())
.unwrap_or(false)
}) {
return emit_aliased_child(protocol, schema, grammar, &edge.tgt, content, out);
}
}
emit_production(protocol, schema, grammar, vertex_id, content, cursor, out)
}
Production::Token { content }
| Production::ImmediateToken { content }
| Production::Prec { content, .. }
| Production::PrecLeft { content, .. }
| Production::PrecRight { content, .. }
| Production::PrecDynamic { content, .. }
| Production::Reserved { content, .. } => {
emit_production(protocol, schema, grammar, vertex_id, content, cursor, out)
}
}
}
fn take_symbol_match<'a>(
grammar: &Grammar,
schema: &Schema,
cursor: &mut ChildCursor<'a>,
name: &str,
) -> Option<&'a Edge> {
cursor.take_matching(|edge| {
let target_kind = schema.vertices.get(&edge.tgt).map(|v| v.kind.as_ref());
kind_satisfies_symbol(grammar, target_kind, name)
})
}
fn kind_satisfies_symbol(grammar: &Grammar, target_kind: Option<&str>, name: &str) -> bool {
let Some(target) = target_kind else {
return false;
};
if target == name {
return true;
}
grammar
.subtypes
.get(target)
.is_some_and(|set| set.contains(name))
}
fn emit_aliased_child(
protocol: &str,
schema: &Schema,
grammar: &Grammar,
child_id: &panproto_gat::Name,
content: &Production,
out: &mut Output<'_>,
) -> Result<(), ParseError> {
if let Some(literal) = literal_value(schema, child_id) {
if children_for(schema, child_id).is_empty() {
out.token(literal);
return Ok(());
}
}
if let Production::Symbol { name } = content {
if let Some(rule) = grammar.rules.get(name) {
let edges = children_for(schema, child_id);
let mut cursor = ChildCursor::new(&edges);
return emit_production(protocol, schema, grammar, child_id, rule, &mut cursor, out);
}
}
let edges = children_for(schema, child_id);
let mut cursor = ChildCursor::new(&edges);
emit_production(
protocol,
schema,
grammar,
child_id,
content,
&mut cursor,
out,
)
}
fn emit_in_child_context(
protocol: &str,
schema: &Schema,
grammar: &Grammar,
child_id: &panproto_gat::Name,
production: &Production,
out: &mut Output<'_>,
) -> Result<(), ParseError> {
let _guard = clear_field_context();
if !matches!(production, Production::Symbol { .. }) {
let child_kind = schema.vertices.get(child_id).map(|v| v.kind.as_ref());
let symbols = referenced_symbols(production);
if symbols
.iter()
.any(|s| kind_satisfies_symbol(grammar, child_kind, s) || child_kind == Some(s))
{
return emit_vertex(protocol, schema, grammar, child_id, out);
}
}
match production {
Production::Symbol { .. } => emit_vertex(protocol, schema, grammar, child_id, out),
_ => {
let edges = children_for(schema, child_id);
let mut cursor = ChildCursor::new(&edges);
emit_production(
protocol,
schema,
grammar,
child_id,
production,
&mut cursor,
out,
)
}
}
}
fn pick_choice_with_cursor<'a>(
schema: &Schema,
grammar: &Grammar,
vertex_id: &panproto_gat::Name,
cursor: &ChildCursor<'_>,
alternatives: &'a [Production],
) -> Option<&'a Production> {
let constraint_blob = schema
.constraints
.get(vertex_id)
.map(|cs| {
let fingerprint: Option<&str> = cs
.iter()
.find(|c| c.sort.as_ref() == "chose-alt-fingerprint")
.map(|c| c.value.as_str());
if let Some(fp) = fingerprint {
fp.to_owned()
} else {
cs.iter()
.filter(|c| {
let s = c.sort.as_ref();
s.starts_with("interstitial-") && !s.ends_with("-start-byte")
})
.map(|c| c.value.as_str())
.collect::<Vec<&str>>()
.join(" ")
}
})
.unwrap_or_default();
let child_kinds: Vec<&str> = schema
.constraints
.get(vertex_id)
.and_then(|cs| {
cs.iter()
.find(|c| c.sort.as_ref() == "chose-alt-child-kinds")
.map(|c| c.value.split_whitespace().collect())
})
.unwrap_or_default();
if !constraint_blob.is_empty() {
let mut best_literal: usize = 0;
let mut best_symbols: usize = 0;
let mut best_alt: Option<&Production> = None;
let mut tied = false;
for alt in alternatives {
let strings = literal_strings(alt);
if strings.is_empty() {
continue;
}
let literal_score = strings
.iter()
.filter(|s| constraint_blob.contains(s.as_str()))
.map(String::len)
.sum::<usize>();
if literal_score == 0 {
continue;
}
let symbol_score = if literal_score >= best_literal && !child_kinds.is_empty() {
let symbols = referenced_symbols(alt);
symbols
.iter()
.filter(|sym| {
let sym_str: &str = sym;
if child_kinds.contains(&sym_str) {
return true;
}
grammar.subtypes.get(sym_str).is_some_and(|sub_set| {
sub_set
.iter()
.any(|sub| child_kinds.contains(&sub.as_str()))
})
})
.count()
} else {
0
};
let better = literal_score > best_literal
|| (literal_score == best_literal && symbol_score > best_symbols);
let same = literal_score == best_literal && symbol_score == best_symbols;
if better {
best_literal = literal_score;
best_symbols = symbol_score;
best_alt = Some(alt);
tied = false;
} else if same && best_alt.is_some() {
tied = true;
}
}
if let Some(alt) = best_alt {
if !tied {
return Some(alt);
}
}
}
let first_unconsumed_kind: Option<&str> = cursor
.edges
.iter()
.enumerate()
.find(|(i, _)| !cursor.consumed[*i])
.and_then(|(_, edge)| schema.vertices.get(&edge.tgt).map(|v| v.kind.as_ref()));
if let Some(target_kind) = first_unconsumed_kind {
for alt in alternatives {
let symbols = referenced_symbols(alt);
if !symbols.is_empty()
&& symbols
.iter()
.any(|s| kind_satisfies_symbol(grammar, Some(target_kind), s))
{
return Some(alt);
}
}
}
let edge_kinds: Vec<&str> = cursor
.edges
.iter()
.enumerate()
.filter(|(i, _)| !cursor.consumed[*i])
.map(|(_, e)| e.kind.as_ref())
.collect();
for alt in alternatives {
if has_field_in(alt, &edge_kinds) {
return Some(alt);
}
}
let _ = (schema, vertex_id);
if alternatives.iter().any(|a| matches!(a, Production::Blank)) {
return alternatives.iter().find(|a| matches!(a, Production::Blank));
}
alternatives
.iter()
.find(|alt| !matches!(alt, Production::Blank))
}
fn literal_strings(production: &Production) -> Vec<String> {
let mut out = Vec::new();
fn walk(p: &Production, out: &mut Vec<String>) {
match p {
Production::String { value } if !value.is_empty() => {
out.push(value.clone());
}
Production::Choice { members } | Production::Seq { members } => {
for m in members {
walk(m, out);
}
}
Production::Repeat { content }
| Production::Repeat1 { content }
| Production::Optional { content }
| Production::Field { 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, .. } => walk(content, out),
_ => {}
}
}
walk(production, &mut out);
out
}
fn referenced_symbols(production: &Production) -> Vec<&str> {
let mut out = Vec::new();
fn walk<'a>(p: &'a Production, out: &mut Vec<&'a str>) {
match p {
Production::Symbol { name } => out.push(name.as_str()),
Production::Choice { members } | Production::Seq { members } => {
for m in members {
walk(m, out);
}
}
Production::Alias {
content,
named,
value,
} => {
if *named && !value.is_empty() {
out.push(value.as_str());
}
walk(content, 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, .. } => walk(content, out),
_ => {}
}
}
walk(production, &mut out);
out
}
#[cfg(test)]
fn first_symbol(production: &Production) -> Option<&str> {
match production {
Production::Symbol { name } => Some(name),
Production::Seq { members } => members.iter().find_map(first_symbol),
Production::Choice { members } => members.iter().find_map(first_symbol),
Production::Repeat { content }
| Production::Repeat1 { content }
| Production::Optional { content }
| Production::Field { 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, .. } => first_symbol(content),
_ => None,
}
}
fn has_field_in(production: &Production, edge_kinds: &[&str]) -> bool {
match production {
Production::Field { name, .. } => edge_kinds.contains(&name.as_str()),
Production::Seq { members } | Production::Choice { members } => {
members.iter().any(|m| has_field_in(m, edge_kinds))
}
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, .. } => has_field_in(content, edge_kinds),
_ => false,
}
}
fn has_relevant_constraint(
production: &Production,
schema: &Schema,
vertex_id: &panproto_gat::Name,
) -> bool {
let constraints = match schema.constraints.get(vertex_id) {
Some(c) => c,
None => return false,
};
fn walk(production: &Production, constraints: &[panproto_schema::Constraint]) -> bool {
match production {
Production::String { value } => constraints
.iter()
.any(|c| c.value == *value || c.sort.as_ref() == value),
Production::Field { name, content } => {
constraints.iter().any(|c| c.sort.as_ref() == name) || walk(content, constraints)
}
Production::Seq { members } | Production::Choice { members } => {
members.iter().any(|m| walk(m, constraints))
}
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, .. } => walk(content, constraints),
_ => false,
}
}
walk(production, constraints)
}
fn children_for<'a>(schema: &'a Schema, vertex_id: &panproto_gat::Name) -> Vec<&'a Edge> {
let Some(edges) = schema.outgoing.get(vertex_id) else {
return Vec::new();
};
let mut indexed: Vec<(usize, u32, &Edge)> = edges
.iter()
.enumerate()
.map(|(i, e)| {
let canonical = schema.edges.get_key_value(e).map_or(e, |(k, _)| k);
let pos = schema.orderings.get(canonical).copied().unwrap_or(u32::MAX);
(i, pos, canonical)
})
.collect();
indexed.sort_by_key(|(i, pos, _)| (*pos, *i));
indexed.into_iter().map(|(_, _, e)| e).collect()
}
fn vertex_id_kind<'a>(schema: &'a Schema, vertex_id: &panproto_gat::Name) -> Option<&'a str> {
schema.vertices.get(vertex_id).map(|v| v.kind.as_ref())
}
fn literal_value<'a>(schema: &'a Schema, vertex_id: &panproto_gat::Name) -> Option<&'a str> {
schema
.constraints
.get(vertex_id)?
.iter()
.find(|c| c.sort.as_ref() == "literal-value")
.map(|c| c.value.as_str())
}
fn is_newline_like_pattern(pattern: &str) -> bool {
if pattern.is_empty() {
return false;
}
let mut chars = pattern.chars();
let mut saw_newline_atom = false;
while let Some(c) = chars.next() {
match c {
'\\' => match chars.next() {
Some('n' | 'r') => saw_newline_atom = true,
_ => return false,
},
'?' | '*' | '+' => {} _ => return false,
}
}
saw_newline_atom
}
fn is_whitespace_only_pattern(pattern: &str) -> bool {
if pattern.is_empty() {
return false;
}
let trimmed = pattern.trim_end_matches(['?', '*', '+']);
if trimmed.is_empty() {
return false;
}
if matches!(trimmed, "\\s" | " " | "\\t") {
return true;
}
if let Some(inner) = trimmed.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
let mut chars = inner.chars();
let mut saw_atom = false;
while let Some(c) = chars.next() {
match c {
'\\' => match chars.next() {
Some('s' | 't' | 'r' | 'n') => saw_atom = true,
_ => return false,
},
' ' | '\t' => saw_atom = true,
_ => return false,
}
}
return saw_atom;
}
false
}
fn placeholder_for_pattern(pattern: &str) -> String {
let simple_lit = decode_simple_pattern_literal(pattern);
if let Some(lit) = simple_lit {
return lit;
}
if pattern.contains("[0-9]") || pattern.contains("\\d") {
"0".into()
} else if pattern.contains("[a-zA-Z_]") || pattern.contains("\\w") {
"_x".into()
} else if pattern.contains('"') || pattern.contains('\'') {
"\"\"".into()
} else {
"_".into()
}
}
fn decode_simple_pattern_literal(pattern: &str) -> Option<String> {
if pattern
.chars()
.any(|c| matches!(c, '[' | ']' | '(' | ')' | '*' | '+' | '?' | '|' | '{' | '}'))
{
return None;
}
let mut out = String::new();
let mut chars = pattern.chars();
while let Some(c) = chars.next() {
if c == '\\' {
match chars.next() {
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some('\\') => out.push('\\'),
Some('/') => out.push('/'),
Some(other) => out.push(other),
None => return None,
}
} else {
out.push(c);
}
}
Some(out)
}
#[derive(Clone)]
enum Token {
Lit(String),
IndentOpen,
IndentClose,
LineBreak,
}
struct Output<'a> {
tokens: Vec<Token>,
policy: &'a FormatPolicy,
}
#[derive(Clone)]
struct OutputSnapshot {
tokens_len: usize,
}
impl<'a> Output<'a> {
fn new(policy: &'a FormatPolicy) -> Self {
Self {
tokens: Vec::new(),
policy,
}
}
fn token(&mut self, value: &str) {
if value.is_empty() {
return;
}
if self.policy.indent_close.iter().any(|t| t == value) {
self.tokens.push(Token::IndentClose);
}
self.tokens.push(Token::Lit(value.to_owned()));
if self.policy.indent_open.iter().any(|t| t == value) {
self.tokens.push(Token::IndentOpen);
self.tokens.push(Token::LineBreak);
} else if self.policy.line_break_after.iter().any(|t| t == value) {
self.tokens.push(Token::LineBreak);
}
}
fn newline(&mut self) {
self.tokens.push(Token::LineBreak);
}
fn indent_open(&mut self) {
self.tokens.push(Token::IndentOpen);
self.tokens.push(Token::LineBreak);
}
fn indent_close(&mut self) {
self.tokens.push(Token::IndentClose);
}
fn snapshot(&self) -> OutputSnapshot {
OutputSnapshot {
tokens_len: self.tokens.len(),
}
}
fn restore(&mut self, snap: OutputSnapshot) {
self.tokens.truncate(snap.tokens_len);
}
fn finish(self) -> Vec<u8> {
layout(&self.tokens, self.policy)
}
}
fn layout(tokens: &[Token], policy: &FormatPolicy) -> Vec<u8> {
let mut bytes = Vec::new();
let mut indent: usize = 0;
let mut at_line_start = true;
let mut last_lit: Option<&str> = None;
let mut last_was_in_operand_position = true;
let mut expecting_operand = true;
let newline = policy.newline.as_bytes();
let separator = policy.separator.as_bytes();
for tok in tokens {
match tok {
Token::IndentOpen => indent += 1,
Token::IndentClose => {
indent = indent.saturating_sub(1);
if !at_line_start {
bytes.extend_from_slice(newline);
at_line_start = true;
expecting_operand = true;
}
}
Token::LineBreak => {
if !at_line_start {
bytes.extend_from_slice(newline);
at_line_start = true;
expecting_operand = true;
}
}
Token::Lit(value) => {
if at_line_start {
bytes.extend(std::iter::repeat_n(b' ', indent * policy.indent_width));
} else if let Some(prev) = last_lit {
if needs_space_between(prev, value, last_was_in_operand_position) {
bytes.extend_from_slice(separator);
}
}
bytes.extend_from_slice(value.as_bytes());
at_line_start = false;
last_was_in_operand_position = expecting_operand;
expecting_operand = leaves_operand_position(value);
last_lit = Some(value.as_str());
}
}
}
if !at_line_start {
bytes.extend_from_slice(newline);
}
bytes
}
fn leaves_operand_position(tok: &str) -> bool {
if tok.is_empty() {
return true;
}
if is_punct_open(tok) {
return true;
}
if matches!(tok, "," | ";") {
return true;
}
if is_punct_close(tok) {
return false;
}
if first_is_alnum_or_underscore(tok) || last_ends_with_alnum(tok) {
return false;
}
true
}
fn needs_space_between(last: &str, next: &str, expecting_operand: bool) -> bool {
if last.is_empty() || next.is_empty() {
return false;
}
if is_punct_open(last) || is_punct_open(next) {
return false;
}
if is_punct_close(next) {
return false;
}
if is_punct_close(last) && is_punct_punctuation(next) {
return false;
}
if last == "." || next == "." {
return false;
}
if expecting_operand && is_unary_prefix_operator(last) && first_is_operand_start(next) {
return false;
}
if last_is_word_like(last) && first_is_word_like(next) {
return true;
}
if last_ends_with_alnum(last) && first_is_alnum_or_underscore(next) {
return true;
}
true
}
fn is_unary_prefix_operator(s: &str) -> bool {
matches!(s, "-" | "+" | "!" | "~")
}
fn first_is_operand_start(s: &str) -> bool {
s.chars()
.next()
.map(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '(')
.unwrap_or(false)
}
fn is_punct_open(s: &str) -> bool {
matches!(s, "(" | "[" | "{" | "\"" | "'" | "`")
}
fn is_punct_close(s: &str) -> bool {
matches!(s, ")" | "]" | "}" | "," | ";" | ":" | "\"" | "'" | "`")
}
fn is_punct_punctuation(s: &str) -> bool {
matches!(s, "," | ";" | ":" | "." | ")" | "]" | "}")
}
fn last_is_word_like(s: &str) -> bool {
s.chars()
.next_back()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false)
}
fn first_is_word_like(s: &str) -> bool {
s.chars()
.next()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false)
}
fn last_ends_with_alnum(s: &str) -> bool {
s.chars()
.next_back()
.map(char::is_alphanumeric)
.unwrap_or(false)
}
fn first_is_alnum_or_underscore(s: &str) -> bool {
s.chars()
.next()
.map(|c| c.is_alphanumeric() || c == '_')
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_simple_grammar_json() {
let bytes = br#"{
"name": "tiny",
"rules": {
"program": {
"type": "SEQ",
"members": [
{"type": "STRING", "value": "hello"},
{"type": "STRING", "value": ";"}
]
}
}
}"#;
let g = Grammar::from_bytes("tiny", bytes).expect("valid tiny grammar");
assert!(g.rules.contains_key("program"));
}
#[test]
fn output_emits_punctuation_without_leading_space() {
let policy = FormatPolicy::default();
let mut out = Output::new(&policy);
out.token("foo");
out.token("(");
out.token(")");
out.token(";");
let bytes = out.finish();
let s = std::str::from_utf8(&bytes).expect("ascii output");
assert!(s.starts_with("foo();"), "got {s:?}");
}
#[test]
fn grammar_from_bytes_rejects_malformed_input() {
let result = Grammar::from_bytes("malformed", b"not json");
let err = result.expect_err("malformed bytes must yield Err");
let msg = err.to_string();
assert!(
msg.contains("malformed"),
"error message should name the protocol: {msg:?}"
);
}
#[test]
fn output_indents_after_open_brace() {
let policy = FormatPolicy::default();
let mut out = Output::new(&policy);
out.token("fn");
out.token("foo");
out.token("(");
out.token(")");
out.token("{");
out.token("body");
out.token("}");
let bytes = out.finish();
let s = std::str::from_utf8(&bytes).expect("ascii output");
assert!(s.contains("{\n"), "newline after opening brace: {s:?}");
assert!(s.contains("body"), "body inside block: {s:?}");
assert!(s.ends_with("}\n"), "newline after closing brace: {s:?}");
}
#[test]
fn output_no_space_between_word_and_dot() {
let policy = FormatPolicy::default();
let mut out = Output::new(&policy);
out.token("foo");
out.token(".");
out.token("bar");
let bytes = out.finish();
let s = std::str::from_utf8(&bytes).expect("ascii output");
assert!(s.starts_with("foo.bar"), "no space around dot: {s:?}");
}
#[test]
fn output_snapshot_restore_truncates_bytes() {
let policy = FormatPolicy::default();
let mut out = Output::new(&policy);
out.token("keep");
let snap = out.snapshot();
out.token("drop");
out.token("more");
out.restore(snap);
out.token("after");
let bytes = out.finish();
let s = std::str::from_utf8(&bytes).expect("ascii output");
assert!(s.contains("keep"), "kept token survives: {s:?}");
assert!(s.contains("after"), "post-restore token visible: {s:?}");
assert!(!s.contains("drop"), "rolled-back token removed: {s:?}");
assert!(!s.contains("more"), "rolled-back token removed: {s:?}");
}
#[test]
fn child_cursor_take_field_consumes_once() {
let edges_owned: Vec<Edge> = vec![Edge {
src: panproto_gat::Name::from("p"),
tgt: panproto_gat::Name::from("c"),
kind: panproto_gat::Name::from("name"),
name: None,
}];
let edges: Vec<&Edge> = edges_owned.iter().collect();
let mut cursor = ChildCursor::new(&edges);
let first = cursor.take_field("name");
let second = cursor.take_field("name");
assert!(first.is_some(), "first take returns the edge");
assert!(
second.is_none(),
"second take returns None (already consumed)"
);
}
#[test]
fn child_cursor_take_matching_predicate() {
let edges_owned: Vec<Edge> = vec![
Edge {
src: "p".into(),
tgt: "c1".into(),
kind: "child_of".into(),
name: None,
},
Edge {
src: "p".into(),
tgt: "c2".into(),
kind: "key".into(),
name: None,
},
];
let edges: Vec<&Edge> = edges_owned.iter().collect();
let mut cursor = ChildCursor::new(&edges);
assert!(cursor.has_matching(|e| e.kind.as_ref() == "key"));
let taken = cursor.take_matching(|e| e.kind.as_ref() == "key");
assert!(taken.is_some());
assert!(
!cursor.has_matching(|e| e.kind.as_ref() == "key"),
"consumed edge no longer matches"
);
assert!(
cursor.has_matching(|e| e.kind.as_ref() == "child_of"),
"the other edge is still available"
);
}
#[test]
fn kind_satisfies_symbol_direct_match() {
let bytes = br#"{
"name": "tiny",
"rules": {
"x": {"type": "STRING", "value": "x"}
}
}"#;
let g = Grammar::from_bytes("tiny", bytes).expect("valid grammar");
assert!(kind_satisfies_symbol(&g, Some("x"), "x"));
assert!(!kind_satisfies_symbol(&g, Some("y"), "x"));
assert!(!kind_satisfies_symbol(&g, None, "x"));
}
#[test]
fn kind_satisfies_symbol_through_hidden_rule() {
let bytes = br#"{
"name": "tiny",
"rules": {
"_value": {
"type": "CHOICE",
"members": [
{"type": "SYMBOL", "name": "object"},
{"type": "SYMBOL", "name": "number"}
]
},
"object": {"type": "STRING", "value": "{}"},
"number": {"type": "PATTERN", "value": "[0-9]+"}
}
}"#;
let g = Grammar::from_bytes("tiny", bytes).expect("valid grammar");
assert!(
kind_satisfies_symbol(&g, Some("number"), "_value"),
"number is reachable from _value via CHOICE"
);
assert!(
kind_satisfies_symbol(&g, Some("object"), "_value"),
"object is reachable from _value via CHOICE"
);
assert!(
!kind_satisfies_symbol(&g, Some("string"), "_value"),
"string is NOT among the alternatives"
);
}
#[test]
fn first_symbol_skips_string_terminals() {
let prod: Production = serde_json::from_str(
r#"{
"type": "SEQ",
"members": [
{"type": "STRING", "value": "{"},
{"type": "SYMBOL", "name": "body"},
{"type": "STRING", "value": "}"}
]
}"#,
)
.expect("valid SEQ");
assert_eq!(first_symbol(&prod), Some("body"));
}
#[test]
fn placeholder_for_pattern_routes_by_regex_class() {
assert_eq!(placeholder_for_pattern("[0-9]+"), "0");
assert_eq!(placeholder_for_pattern("[a-zA-Z_]\\w*"), "_x");
assert_eq!(placeholder_for_pattern("\"[^\"]*\""), "\"\"");
assert_eq!(placeholder_for_pattern("\\d+\\.\\d+"), "0");
}
#[test]
fn format_policy_default_breaks_after_semicolon() {
let policy = FormatPolicy::default();
assert!(policy.line_break_after.iter().any(|t| t == ";"));
assert!(policy.indent_open.iter().any(|t| t == "{"));
assert!(policy.indent_close.iter().any(|t| t == "}"));
assert_eq!(policy.indent_width, 2);
}
#[test]
fn placeholder_decodes_literal_pattern_separators() {
assert_eq!(placeholder_for_pattern("\\n"), "\n");
assert_eq!(placeholder_for_pattern("\\r\\n"), "\r\n");
assert_eq!(placeholder_for_pattern(";"), ";");
assert_eq!(placeholder_for_pattern("[0-9]+"), "0");
assert_eq!(placeholder_for_pattern("a|b"), "_");
}
#[test]
fn supertypes_decode_from_grammar_json_strings() {
let bytes = br#"{
"name": "tiny",
"supertypes": ["expression"],
"rules": {
"expression": {
"type": "CHOICE",
"members": [
{"type": "SYMBOL", "name": "binary_expression"},
{"type": "SYMBOL", "name": "identifier"}
]
},
"binary_expression": {"type": "STRING", "value": "x"},
"identifier": {"type": "PATTERN", "value": "[a-z]+"}
}
}"#;
let g = Grammar::from_bytes("tiny", bytes).expect("parse");
assert!(g.supertypes.contains("expression"));
assert!(kind_satisfies_symbol(&g, Some("identifier"), "expression"));
assert!(!kind_satisfies_symbol(&g, Some("string"), "expression"));
}
#[test]
fn supertypes_decode_from_grammar_json_objects() {
let bytes = br#"{
"name": "tiny",
"supertypes": [{"type": "SYMBOL", "name": "stmt"}],
"rules": {
"stmt": {
"type": "CHOICE",
"members": [
{"type": "SYMBOL", "name": "while_stmt"},
{"type": "SYMBOL", "name": "if_stmt"}
]
},
"while_stmt": {"type": "STRING", "value": "while"},
"if_stmt": {"type": "STRING", "value": "if"}
}
}"#;
let g = Grammar::from_bytes("tiny", bytes).expect("parse");
assert!(g.supertypes.contains("stmt"));
assert!(kind_satisfies_symbol(&g, Some("while_stmt"), "stmt"));
}
#[test]
fn alias_value_matches_kind() {
let bytes = br#"{
"name": "tiny",
"rules": {
"_package_identifier": {
"type": "ALIAS",
"named": true,
"value": "package_identifier",
"content": {"type": "SYMBOL", "name": "identifier"}
},
"identifier": {"type": "PATTERN", "value": "[a-z]+"}
}
}"#;
let g = Grammar::from_bytes("tiny", bytes).expect("parse");
assert!(kind_satisfies_symbol(
&g,
Some("package_identifier"),
"_package_identifier"
));
}
#[test]
fn referenced_symbols_walks_nested_seq() {
let prod: Production = serde_json::from_str(
r#"{
"type": "SEQ",
"members": [
{"type": "CHOICE", "members": [
{"type": "SYMBOL", "name": "attribute_item"},
{"type": "BLANK"}
]},
{"type": "SYMBOL", "name": "parameter"},
{"type": "REPEAT", "content": {
"type": "SEQ",
"members": [
{"type": "STRING", "value": ","},
{"type": "SYMBOL", "name": "parameter"}
]
}}
]
}"#,
)
.expect("seq");
let symbols = referenced_symbols(&prod);
assert!(symbols.contains(&"attribute_item"));
assert!(symbols.contains(&"parameter"));
}
#[test]
fn literal_strings_collects_choice_members() {
let prod: Production = serde_json::from_str(
r#"{
"type": "CHOICE",
"members": [
{"type": "STRING", "value": "+"},
{"type": "STRING", "value": "-"},
{"type": "STRING", "value": "*"}
]
}"#,
)
.expect("choice");
let strings = literal_strings(&prod);
assert_eq!(strings, vec!["+", "-", "*"]);
}
#[test]
fn reserved_variant_deserialises() {
let prod: Production = serde_json::from_str(
r#"{
"type": "RESERVED",
"content": {"type": "SYMBOL", "name": "_lowercase_identifier"},
"context_name": "attribute_id"
}"#,
)
.expect("RESERVED parses");
match prod {
Production::Reserved { content, .. } => match *content {
Production::Symbol { name } => assert_eq!(name, "_lowercase_identifier"),
other => panic!("expected inner SYMBOL, got {other:?}"),
},
other => panic!("expected RESERVED, got {other:?}"),
}
}
#[test]
fn reserved_grammar_loads_end_to_end() {
let bytes = br#"{
"name": "tiny_reserved",
"rules": {
"program": {
"type": "RESERVED",
"content": {"type": "SYMBOL", "name": "ident"},
"context_name": "keywords"
},
"ident": {"type": "PATTERN", "value": "[a-z]+"}
}
}"#;
let g = Grammar::from_bytes("tiny_reserved", bytes).expect("RESERVED-using grammar loads");
assert!(g.rules.contains_key("program"));
}
#[test]
fn reserved_walker_helpers_recurse_into_content() {
let prod: Production = serde_json::from_str(
r#"{
"type": "RESERVED",
"content": {
"type": "FIELD",
"name": "lhs",
"content": {"type": "SYMBOL", "name": "expr"}
},
"context_name": "ctx"
}"#,
)
.expect("nested RESERVED parses");
assert_eq!(first_symbol(&prod), Some("expr"));
assert!(has_field_in(&prod, &["lhs"]));
let symbols = referenced_symbols(&prod);
assert!(symbols.contains(&"expr"));
}
}