Skip to main content

panproto_parse/
emit_pretty.rs

1#![allow(
2    clippy::module_name_repetitions,
3    clippy::too_many_lines,
4    clippy::too_many_arguments,
5    clippy::map_unwrap_or,
6    clippy::option_if_let_else,
7    clippy::elidable_lifetime_names,
8    clippy::items_after_statements,
9    clippy::needless_pass_by_value,
10    clippy::single_match_else,
11    clippy::manual_let_else,
12    clippy::match_same_arms,
13    clippy::missing_const_for_fn,
14    clippy::single_char_pattern,
15    clippy::naive_bytecount,
16    clippy::expect_used,
17    clippy::redundant_pub_crate,
18    clippy::used_underscore_binding,
19    clippy::redundant_field_names,
20    clippy::struct_field_names,
21    clippy::redundant_else,
22    clippy::similar_names
23)]
24
25//! De-novo source emission from a by-construction schema.
26//!
27//! [`AstParser::emit`] reconstructs source from byte-position fragments
28//! that the parser stored on the schema during `parse`. That works for
29//! edit pipelines (`parse → transform → emit`) but fails for schemas
30//! built by hand (`SchemaBuilder` with no parse history): they carry
31//! no `start-byte`, no `interstitial-N`, no `literal-value`, and the
32//! reconstructor returns `Err(EmitFailed { reason: "schema has no
33//! text fragments" })`.
34//!
35//! This module renders such schemas to source bytes by walking
36//! tree-sitter's `grammar.json` production rules. For each schema
37//! vertex of kind `K`, the walker looks up `K`'s production in the
38//! grammar and emits its body in order:
39//!
40//! - `STRING` nodes contribute literal token bytes directly.
41//! - `SYMBOL` and `FIELD` nodes recurse into the schema's children,
42//!   matching by edge kind (which is the tree-sitter field name).
43//! - `SEQ` emits its members in order.
44//! - `CHOICE` picks the alternative whose head `SYMBOL` matches an
45//!   actual child kind, or whose terminals appear in the rendered
46//!   prefix; falls back to the first non-`BLANK` alternative when no
47//!   alternative matches.
48//! - `REPEAT` and `REPEAT1` emit their content once per matching
49//!   child edge in declared order.
50//! - `OPTIONAL` emits its content iff a corresponding child edge or
51//!   constraint is populated.
52//! - `PATTERN` is a regex placeholder for variable-text terminals
53//!   (identifiers, numbers, quoted strings). The walker emits a
54//!   `literal-value` constraint when present and otherwise falls
55//!   back to a placeholder derived from the regex shape.
56//! - `BLANK`, `TOKEN`, `IMMEDIATE_TOKEN`, `ALIAS`, `PREC*` are
57//!   handled transparently (the inner content is emitted; the
58//!   wrapper is dropped).
59//!
60//! Whitespace and indentation come from a `FormatPolicy` applied
61//! during emission. The default policy inserts a single space between
62//! adjacent tokens, a newline after `;` / `}` / `{`, and tracks an
63//! indent counter on `{` / `}` boundaries.
64//!
65//! Output is *syntactically valid* for any grammar that ships
66//! `grammar.json`. Idiomatic formatting (rustfmt-style spacing rules,
67//! per-language conventions) is a polish layer that lives outside
68//! this module.
69
70use std::collections::BTreeMap;
71
72use panproto_schema::{Edge, Schema};
73use serde::Deserialize;
74
75use crate::error::ParseError;
76
77// ═══════════════════════════════════════════════════════════════════
78// Grammar JSON model
79// ═══════════════════════════════════════════════════════════════════
80
81/// A single tree-sitter production rule.
82///
83/// Mirrors the shape emitted by `tree-sitter generate`: every node has
84/// a `type` discriminator that selects a structural variant. The
85/// untyped subset (`PATTERN`, `STRING`, `SYMBOL`, `BLANK`) handles
86/// terminals; the structural subset (`SEQ`, `CHOICE`, `REPEAT`,
87/// `REPEAT1`, `OPTIONAL`, `FIELD`, `ALIAS`, `TOKEN`,
88/// `IMMEDIATE_TOKEN`, `PREC*`) builds composite productions.
89#[derive(Debug, Clone, Deserialize)]
90#[serde(tag = "type")]
91#[non_exhaustive]
92pub enum Production {
93    /// Concatenation of productions.
94    #[serde(rename = "SEQ")]
95    Seq {
96        /// Ordered members; each is emitted in turn.
97        members: Vec<Self>,
98    },
99    /// Alternation between productions.
100    #[serde(rename = "CHOICE")]
101    Choice {
102        /// Alternatives; the walker picks one based on the schema's
103        /// children and constraints.
104        members: Vec<Self>,
105    },
106    /// Zero-or-more repetition.
107    #[serde(rename = "REPEAT")]
108    Repeat {
109        /// The repeated body.
110        content: Box<Self>,
111    },
112    /// One-or-more repetition.
113    #[serde(rename = "REPEAT1")]
114    Repeat1 {
115        /// The repeated body.
116        content: Box<Self>,
117    },
118    /// Optional inclusion (zero or one).
119    ///
120    /// Tree-sitter usually emits `OPTIONAL` as `CHOICE { content,
121    /// BLANK }`, but recent generator versions also emit explicit
122    /// `OPTIONAL` nodes; both shapes are accepted.
123    #[serde(rename = "OPTIONAL")]
124    Optional {
125        /// The optional body.
126        content: Box<Self>,
127    },
128    /// Reference to another rule by name.
129    #[serde(rename = "SYMBOL")]
130    Symbol {
131        /// Name of the referenced rule (matches a vertex kind on the
132        /// schema side).
133        name: String,
134    },
135    /// Literal token bytes.
136    #[serde(rename = "STRING")]
137    String {
138        /// The literal token. Emitted verbatim.
139        value: String,
140    },
141    /// Regex-matched terminal.
142    ///
143    /// At parse time this matches arbitrary bytes; at emit time the
144    /// walker substitutes a `literal-value` constraint when present
145    /// and falls back to a placeholder otherwise.
146    #[serde(rename = "PATTERN")]
147    Pattern {
148        /// The original regex.
149        value: String,
150    },
151    /// The empty production. Emits nothing.
152    #[serde(rename = "BLANK")]
153    Blank,
154    /// Named field over a content production.
155    ///
156    /// The field `name` matches an edge kind on the schema side; the
157    /// walker resolves the corresponding child vertex and recurses
158    /// into `content` with that child as context.
159    #[serde(rename = "FIELD")]
160    Field {
161        /// Field name (matches edge kind).
162        name: String,
163        /// The contents of the field.
164        content: Box<Self>,
165    },
166    /// An aliased production.
167    ///
168    /// `value` records the parser-visible kind; the walker emits
169    /// `content` and ignores the alias rename.
170    #[serde(rename = "ALIAS")]
171    Alias {
172        /// The aliased content.
173        content: Box<Self>,
174        /// Whether the alias is a named node.
175        #[serde(default)]
176        named: bool,
177        /// The alias's surface name.
178        #[serde(default)]
179        value: String,
180    },
181    /// A token wrapper.
182    ///
183    /// Tree-sitter uses `TOKEN` to mark a sub-rule as a single
184    /// lexical token; the walker emits the inner content unchanged.
185    #[serde(rename = "TOKEN")]
186    Token {
187        /// The wrapped content.
188        content: Box<Self>,
189    },
190    /// An immediate-token wrapper (no preceding whitespace).
191    ///
192    /// Treated like [`Production::Token`] for emit purposes.
193    #[serde(rename = "IMMEDIATE_TOKEN")]
194    ImmediateToken {
195        /// The wrapped content.
196        content: Box<Self>,
197    },
198    /// Precedence wrapper.
199    #[serde(rename = "PREC")]
200    Prec {
201        /// Precedence value (numeric or string). Ignored at emit time.
202        #[allow(dead_code)]
203        value: serde_json::Value,
204        /// The wrapped content.
205        content: Box<Self>,
206    },
207    /// Left-associative precedence wrapper.
208    #[serde(rename = "PREC_LEFT")]
209    PrecLeft {
210        /// Precedence value. Ignored at emit time.
211        #[allow(dead_code)]
212        value: serde_json::Value,
213        /// The wrapped content.
214        content: Box<Self>,
215    },
216    /// Right-associative precedence wrapper.
217    #[serde(rename = "PREC_RIGHT")]
218    PrecRight {
219        /// Precedence value. Ignored at emit time.
220        #[allow(dead_code)]
221        value: serde_json::Value,
222        /// The wrapped content.
223        content: Box<Self>,
224    },
225    /// Dynamic precedence wrapper.
226    #[serde(rename = "PREC_DYNAMIC")]
227    PrecDynamic {
228        /// Precedence value. Ignored at emit time.
229        #[allow(dead_code)]
230        value: serde_json::Value,
231        /// The wrapped content.
232        content: Box<Self>,
233    },
234    /// Reserved-word wrapper (tree-sitter ≥ 0.25).
235    ///
236    /// Tree-sitter's `RESERVED` rule marks an inner production as a
237    /// reserved-word context: the parser excludes the listed identifiers
238    /// from being treated as the inner symbol. The `context_name`
239    /// metadata names the reserved-word set; the emitter does not need
240    /// it (we are walking schema → bytes, not enforcing reserved-word
241    /// constraints), so we emit the inner content unchanged, the same
242    /// way [`Production::Token`] and [`Production::ImmediateToken`] do.
243    #[serde(rename = "RESERVED")]
244    Reserved {
245        /// The wrapped content.
246        content: Box<Self>,
247        /// Name of the reserved-word context. Ignored at emit time.
248        #[allow(dead_code)]
249        #[serde(default)]
250        context_name: String,
251    },
252}
253
254/// A grammar's production-rule table, deserialized from `grammar.json`.
255///
256/// Only the fields the emitter consumes are decoded; precedences,
257/// conflicts, externals, and other parser-only metadata are ignored.
258#[derive(Debug, Clone, Deserialize)]
259#[non_exhaustive]
260pub struct Grammar {
261    /// Grammar name (e.g. `"rust"`, `"typescript"`).
262    #[allow(dead_code)]
263    pub name: String,
264    /// Map from rule name (a vertex kind on the schema side) to
265    /// production. Entries are kept in lexical order so iteration
266    /// is deterministic.
267    pub rules: BTreeMap<String, Production>,
268    /// Supertypes declared in the grammar's `supertypes` field. A
269    /// supertype is a rule whose body is a `CHOICE` of `SYMBOL`
270    /// references; tree-sitter parsers report a node's kind as one
271    /// of the subtypes (e.g. `identifier`, `typed_parameter`) rather
272    /// than the supertype name (`parameter`), so the emitter needs to
273    /// know that a child kind in a subtype set should match the
274    /// supertype name when a SYMBOL references it.
275    #[serde(default, deserialize_with = "deserialize_supertypes")]
276    pub supertypes: std::collections::HashSet<String>,
277    /// Tree-sitter `extras` rules: the named symbols (typically comments)
278    /// that tree-sitter skips at parse time but records as children of the
279    /// surrounding vertex. They appear nowhere in the production grammar,
280    /// so the rule walker cannot reconcile them against the cursor — the
281    /// emit pass therefore drains them as a side channel: at vertex entry
282    /// and between REPEAT iterations any leading extras-kind edges are
283    /// consumed and emitted directly. The set is populated at
284    /// `Grammar::from_bytes` by collecting every `SYMBOL { name }` and
285    /// named `ALIAS { value, named: true }` under the top-level `extras`
286    /// array. Pattern-only extras (e.g. `\s` whitespace) are not vertex
287    /// kinds and are excluded.
288    #[serde(default, deserialize_with = "deserialize_extras")]
289    pub extras: std::collections::HashSet<String>,
290    /// Precomputed subtyping closure: `subtypes[symbol_name]` is the
291    /// set of vertex kinds that satisfy a SYMBOL `symbol_name`
292    /// reference on the schema side.
293    ///
294    /// Built once at [`Grammar::from_bytes`] time by walking each
295    /// hidden rule (`_`-prefixed), declared supertype, and named
296    /// `ALIAS { value: K, ... }` production to its leaf SYMBOLs and
297    /// recording the closure. This replaces the prior heuristic
298    /// `kind_satisfies_symbol` that walked the rule body on every
299    /// query: lookups are now O(1) and the relation is exactly the
300    /// transitive closure of "is reachable via hidden / supertype /
301    /// alias dispatch", with no over-expansion through non-hidden
302    /// non-supertype rule references.
303    #[serde(skip)]
304    pub subtypes: std::collections::HashMap<String, std::collections::HashSet<String>>,
305    /// Precomputed Yield sets: `yield_sets[rule_name]` is the set of
306    /// concrete vertex kinds that can appear as the **first named
307    /// child** when that rule's production is taken.
308    ///
309    /// Defined inductively:
310    /// - `Yield(SYMBOL S)` where S is hidden/supertype = `Yield(rules[S])`
311    /// - `Yield(SYMBOL S)` where S is concrete = `{S}`
312    /// - `Yield(SEQ [M1, ...])` = `Yield(M1)` (only first member)
313    /// - `Yield(CHOICE [M1, ..., Mn])` = `⋃ Yield(Mi)`
314    /// - `Yield(OPTIONAL { c })` = `Yield(c) ∪ {ε}`
315    /// - `Yield(BLANK)` = `{ε}`
316    /// - Wrappers (PREC*, TOKEN, FIELD, REPEAT, etc.) = `Yield(content)`
317    /// - `Yield(STRING)` = `Yield(PATTERN)` = `∅`
318    /// - `Yield(ALIAS { value: V, named: true })` = `{V}`
319    ///
320    /// Epsilon is represented as the empty string `""`.
321    #[serde(skip)]
322    pub yield_sets: std::collections::HashMap<String, std::collections::HashSet<String>>,
323    /// Child kinds allowed per parent kind, derived from node-types.json.
324    /// Maps parent kind to the set of ALL named child kinds that tree-sitter's
325    /// parser can produce for that parent (from both `children.types` and
326    /// `fields.*.types`). Used by `augment_subtypes_from_node_types` to
327    /// close the grammar/parser divergence gap.
328    #[serde(skip)]
329    pub node_type_children: std::collections::HashMap<String, std::collections::HashSet<String>>,
330    /// Anonymous ALIAS values for external scanner tokens. Maps external
331    /// symbol name (e.g. `_ternary_qmark`) to the ALIAS value string
332    /// (e.g. `"?"`). Built by scanning grammar.json rule bodies for
333    /// `ALIAS { content: SYMBOL S, named: false, value: V }` where S
334    /// has no grammar rule.
335    #[serde(skip)]
336    pub external_alias_map: std::collections::HashMap<String, String>,
337    /// Rules whose `{`/`}` STRING tokens are inline delimiters (e.g.
338    /// string interpolation) rather than block scopes. Identified
339    /// structurally: a rule whose SEQ contains `{` and `}` but no
340    /// REPEAT/REPEAT1 between them.
341    #[serde(skip)]
342    pub inline_brace_rules: std::collections::HashSet<String>,
343}
344
345fn deserialize_supertypes<'de, D>(
346    deserializer: D,
347) -> Result<std::collections::HashSet<String>, D::Error>
348where
349    D: serde::Deserializer<'de>,
350{
351    let entries: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;
352    let mut out = std::collections::HashSet::new();
353    for entry in entries {
354        match entry {
355            serde_json::Value::String(s) => {
356                out.insert(s);
357            }
358            serde_json::Value::Object(map) => {
359                if let Some(serde_json::Value::String(name)) = map.get("name") {
360                    out.insert(name.clone());
361                }
362            }
363            _ => {}
364        }
365    }
366    Ok(out)
367}
368
369fn deserialize_extras<'de, D>(
370    deserializer: D,
371) -> Result<std::collections::HashSet<String>, D::Error>
372where
373    D: serde::Deserializer<'de>,
374{
375    let entries: Vec<serde_json::Value> = Vec::deserialize(deserializer)?;
376    let mut out = std::collections::HashSet::new();
377    for entry in entries {
378        if let serde_json::Value::Object(map) = entry {
379            let ty = map.get("type").and_then(serde_json::Value::as_str);
380            match ty {
381                // SYMBOL { name: K } — the extras rule is a named symbol
382                // (typically `line_comment` / `block_comment`). The kind
383                // K appears as a real child vertex on the schema side.
384                Some("SYMBOL") => {
385                    if let Some(serde_json::Value::String(name)) = map.get("name") {
386                        out.insert(name.clone());
387                    }
388                }
389                // ALIAS { content, value: V, named: true } — the extras
390                // rule renames its content; V is the kind on the schema.
391                Some("ALIAS") => {
392                    let named = map
393                        .get("named")
394                        .and_then(serde_json::Value::as_bool)
395                        .unwrap_or(false);
396                    if named {
397                        if let Some(serde_json::Value::String(value)) = map.get("value") {
398                            out.insert(value.clone());
399                        }
400                    }
401                }
402                // PATTERN / STRING / TOKEN entries describe inter-token
403                // whitespace and have no vertex-side representation.
404                _ => {}
405            }
406        }
407    }
408    Ok(out)
409}
410
411impl Grammar {
412    /// Parse a grammar's `grammar.json` bytes.
413    ///
414    /// Builds the subtyping closure as part of construction so every
415    /// downstream lookup is O(1). The closure is the least relation
416    /// containing `(K, K)` for every rule key `K` and closed under:
417    ///
418    /// - hidden-rule expansion: if `S` is hidden and a SYMBOL `S` may
419    ///   reach SYMBOL `K`, then `K ⊑ S`.
420    /// - supertype expansion: if `S` is in the grammar's supertypes
421    ///   block and `K` is one of `S`'s alternatives, then `K ⊑ S`.
422    /// - alias renaming: if a rule body contains
423    ///   `ALIAS { content: SYMBOL R, value: A, named: true }` where
424    ///   `R` reaches kind `K` (or `K = R` when no further hop), then
425    ///   `A ⊑ R` and `K ⊑ A`.
426    ///
427    /// # Errors
428    ///
429    /// Returns [`ParseError::EmitFailed`] when the bytes are not a
430    /// valid `grammar.json` document.
431    pub fn from_bytes(protocol: &str, bytes: &[u8]) -> Result<Self, ParseError> {
432        Self::from_bytes_with_node_types(protocol, bytes, None)
433    }
434
435    /// Parse a grammar from both `grammar.json` and optionally
436    /// `node-types.json` bytes.
437    ///
438    /// # Errors
439    ///
440    /// Returns [`ParseError::EmitFailed`] when `grammar_bytes` is
441    /// not a valid `grammar.json` document.
442    pub fn from_bytes_with_node_types(
443        protocol: &str,
444        grammar_bytes: &[u8],
445        node_types_bytes: Option<&[u8]>,
446    ) -> Result<Self, ParseError> {
447        let mut grammar: Self =
448            serde_json::from_slice(grammar_bytes).map_err(|e| ParseError::EmitFailed {
449                protocol: protocol.to_owned(),
450                reason: format!("grammar.json deserialization failed: {e}"),
451            })?;
452        grammar.subtypes = compute_subtype_closure(&grammar);
453        if let Some(nt_bytes) = node_types_bytes {
454            grammar.node_type_children = build_node_type_children(nt_bytes);
455            augment_subtypes_from_node_types(&mut grammar);
456        }
457        grammar.external_alias_map = build_external_alias_map(&grammar);
458        grammar.inline_brace_rules = identify_inline_brace_rules(&grammar);
459        grammar.yield_sets = compute_yield_sets(&grammar);
460        Ok(grammar)
461    }
462}
463
464/// Compute the subtyping relation as a forward-indexed map from a
465/// SYMBOL name to the set of vertex kinds that satisfy that SYMBOL.
466fn compute_subtype_closure(
467    grammar: &Grammar,
468) -> std::collections::HashMap<String, std::collections::HashSet<String>> {
469    use std::collections::{HashMap, HashSet};
470    // Edges of the "kind X satisfies SYMBOL Y" relation. `K ⊑ Y` is
471    // recorded whenever Y is reached by walking the grammar's
472    // ALIAS / hidden-rule / supertype dispatch from a position where
473    // K is the actual vertex kind.
474    let mut subtypes: HashMap<String, HashSet<String>> = HashMap::new();
475    for name in grammar.rules.keys() {
476        subtypes
477            .entry(name.clone())
478            .or_default()
479            .insert(name.clone());
480    }
481
482    // First pass: collect the immediate "satisfies" edges from each
483    // expandable rule (hidden, supertype) to the kinds reachable by
484    // walking its body, plus alias edges.
485    fn walk<'g>(
486        grammar: &'g Grammar,
487        production: &'g Production,
488        visited: &mut HashSet<&'g str>,
489        out: &mut HashSet<String>,
490    ) {
491        match production {
492            Production::Symbol { name } => {
493                // Direct subtype.
494                out.insert(name.clone());
495                // Continue expansion through hidden / supertype rules
496                // so the closure traverses pass-through dispatch.
497                let expand = name.starts_with('_') || grammar.supertypes.contains(name.as_str());
498                if expand && visited.insert(name.as_str()) {
499                    if let Some(rule) = grammar.rules.get(name) {
500                        walk(grammar, rule, visited, out);
501                    }
502                }
503            }
504            Production::Choice { members } | Production::Seq { members } => {
505                for m in members {
506                    walk(grammar, m, visited, out);
507                }
508            }
509            Production::Alias {
510                content,
511                named,
512                value,
513            } => {
514                if *named && !value.is_empty() {
515                    out.insert(value.clone());
516                }
517                walk(grammar, content, visited, out);
518            }
519            Production::Repeat { content }
520            | Production::Repeat1 { content }
521            | Production::Optional { content }
522            | Production::Field { content, .. }
523            | Production::Token { content }
524            | Production::ImmediateToken { content }
525            | Production::Prec { content, .. }
526            | Production::PrecLeft { content, .. }
527            | Production::PrecRight { content, .. }
528            | Production::PrecDynamic { content, .. }
529            | Production::Reserved { content, .. } => {
530                walk(grammar, content, visited, out);
531            }
532            _ => {}
533        }
534    }
535
536    for (name, rule) in &grammar.rules {
537        let expand = name.starts_with('_') || grammar.supertypes.contains(name.as_str());
538        if !expand {
539            continue;
540        }
541        let mut visited: HashSet<&str> = HashSet::new();
542        visited.insert(name.as_str());
543        let mut reachable: HashSet<String> = HashSet::new();
544        walk(grammar, rule, &mut visited, &mut reachable);
545        for kind in &reachable {
546            subtypes
547                .entry(kind.clone())
548                .or_default()
549                .insert(name.clone());
550        }
551    }
552
553    // Aliases: scan every rule body for ALIAS { content, value }
554    // declarations. The kinds reachable from `content` satisfy
555    // `value`, AND (by construction) `value` satisfies the
556    // surrounding rule. Walking the ENTIRE grammar once captures
557    // every alias site, irrespective of which rule introduces it.
558    fn collect_aliases<'g>(production: &'g Production, out: &mut Vec<(String, &'g Production)>) {
559        match production {
560            Production::Alias {
561                content,
562                named,
563                value,
564            } => {
565                if *named && !value.is_empty() {
566                    out.push((value.clone(), content.as_ref()));
567                }
568                collect_aliases(content, out);
569            }
570            Production::Choice { members } | Production::Seq { members } => {
571                for m in members {
572                    collect_aliases(m, out);
573                }
574            }
575            Production::Repeat { content }
576            | Production::Repeat1 { content }
577            | Production::Optional { content }
578            | Production::Field { content, .. }
579            | Production::Token { content }
580            | Production::ImmediateToken { content }
581            | Production::Prec { content, .. }
582            | Production::PrecLeft { content, .. }
583            | Production::PrecRight { content, .. }
584            | Production::PrecDynamic { content, .. }
585            | Production::Reserved { content, .. } => {
586                collect_aliases(content, out);
587            }
588            _ => {}
589        }
590    }
591    let mut aliases: Vec<(String, &Production)> = Vec::new();
592    for rule in grammar.rules.values() {
593        collect_aliases(rule, &mut aliases);
594    }
595    for (alias_value, content) in aliases {
596        let mut visited: HashSet<&str> = HashSet::new();
597        let mut reachable: HashSet<String> = HashSet::new();
598        walk(grammar, content, &mut visited, &mut reachable);
599        // Aliased value satisfies itself and is satisfied by every
600        // kind its content can reach.
601        subtypes
602            .entry(alias_value.clone())
603            .or_default()
604            .insert(alias_value.clone());
605        for kind in reachable {
606            subtypes
607                .entry(kind)
608                .or_default()
609                .insert(alias_value.clone());
610        }
611    }
612
613    // Transitive close: `K ⊑ A` and `A ⊑ B` implies `K ⊑ B`. Iterate
614    // a few rounds; the relation is small so a quick fixed-point
615    // suffices in practice.
616    for _ in 0..8 {
617        let snapshot = subtypes.clone();
618        let mut changed = false;
619        for (kind, supers) in &snapshot {
620            let extra: HashSet<String> = supers
621                .iter()
622                .flat_map(|s| snapshot.get(s).cloned().unwrap_or_default())
623                .collect();
624            let entry = subtypes.entry(kind.clone()).or_default();
625            for s in extra {
626                if entry.insert(s) {
627                    changed = true;
628                }
629            }
630        }
631        if !changed {
632            break;
633        }
634    }
635
636    subtypes
637}
638
639/// Compute the Yield set for every rule in the grammar.
640///
641/// `Yield(P)` is the set of concrete vertex kinds that can appear as
642/// the first named child when production P is taken. See the
643/// `Grammar::yield_sets` doc comment for the inductive definition.
644fn compute_yield_sets(
645    grammar: &Grammar,
646) -> std::collections::HashMap<String, std::collections::HashSet<String>> {
647    let mut cache: std::collections::HashMap<String, std::collections::HashSet<String>> =
648        std::collections::HashMap::new();
649    for (name, rule) in &grammar.rules {
650        if cache.contains_key(name) {
651            continue;
652        }
653        let mut visited = std::collections::HashSet::new();
654        let ys = yield_of_production(grammar, rule, &mut visited, &mut cache);
655        cache.insert(name.clone(), ys);
656    }
657    cache
658}
659
660/// Compute the Yield set of an arbitrary production node.
661///
662/// Uses `cache` (the partially-built `yield_sets` map) as
663/// memoization. `visited` tracks the current recursion path to
664/// detect cycles through hidden/supertype rules; a cycle returns ∅
665/// (a cycle that never passes through a concrete named symbol
666/// cannot produce a first child).
667fn yield_of_production(
668    grammar: &Grammar,
669    production: &Production,
670    visited: &mut std::collections::HashSet<String>,
671    cache: &mut std::collections::HashMap<String, std::collections::HashSet<String>>,
672) -> std::collections::HashSet<String> {
673    match production {
674        Production::Symbol { name } => {
675            if let Some(cached) = cache.get(name) {
676                return cached.clone();
677            }
678            let expand = name.starts_with('_') || grammar.supertypes.contains(name.as_str());
679            if expand {
680                if !visited.insert(name.clone()) {
681                    return std::collections::HashSet::new();
682                }
683                let result = if let Some(rule) = grammar.rules.get(name) {
684                    yield_of_production(grammar, rule, visited, cache)
685                } else {
686                    std::collections::HashSet::new()
687                };
688                visited.remove(name);
689                cache.insert(name.clone(), result.clone());
690                result
691            } else {
692                let mut set = std::collections::HashSet::new();
693                set.insert(name.clone());
694                set
695            }
696        }
697        Production::Alias {
698            content,
699            named,
700            value,
701        } => {
702            if *named && !value.is_empty() {
703                let mut set = std::collections::HashSet::new();
704                set.insert(value.clone());
705                set
706            } else {
707                yield_of_production(grammar, content, visited, cache)
708            }
709        }
710        Production::Seq { members } => {
711            if members.is_empty() {
712                let mut set = std::collections::HashSet::new();
713                set.insert(String::new());
714                set
715            } else {
716                // Walk the SEQ members left-to-right, returning the
717                // Yield of the first member that can produce a named
718                // child. STRING and PATTERN yield ∅ (anonymous
719                // tokens); skip them to reach the first named-child-
720                // producing position.  This handles hidden rules like
721                // `_initializer = SEQ ["=", FIELD { value, ... }]`
722                // where the leading "=" is a STRING.
723                for m in members {
724                    let ys = yield_of_production(grammar, m, visited, cache);
725                    if !ys.is_empty() {
726                        return ys;
727                    }
728                }
729                std::collections::HashSet::new()
730            }
731        }
732        Production::Choice { members } => {
733            let mut union = std::collections::HashSet::new();
734            for m in members {
735                union.extend(yield_of_production(grammar, m, visited, cache));
736            }
737            union
738        }
739        Production::Optional { content } => {
740            let mut set = yield_of_production(grammar, content, visited, cache);
741            set.insert(String::new());
742            set
743        }
744        Production::Blank => {
745            let mut set = std::collections::HashSet::new();
746            set.insert(String::new());
747            set
748        }
749        Production::String { .. } | Production::Pattern { .. } => std::collections::HashSet::new(),
750        Production::Repeat { content }
751        | Production::Repeat1 { content }
752        | Production::Field { content, .. }
753        | Production::Token { content }
754        | Production::ImmediateToken { content }
755        | Production::Prec { content, .. }
756        | Production::PrecLeft { content, .. }
757        | Production::PrecRight { content, .. }
758        | Production::PrecDynamic { content, .. }
759        | Production::Reserved { content, .. } => {
760            yield_of_production(grammar, content, visited, cache)
761        }
762    }
763}
764
765// ═══════════════════════════════════════════════════════════════════
766// node-types.json integration
767// ═══════════════════════════════════════════════════════════════════
768
769/// Parse node-types.json and build a map from parent kind to the set
770/// of all named child kinds the parser can produce for that parent.
771fn build_node_type_children(
772    nt_bytes: &[u8],
773) -> std::collections::HashMap<String, std::collections::HashSet<String>> {
774    let node_types: Vec<crate::theory_extract::NodeType> = match serde_json::from_slice(nt_bytes) {
775        Ok(v) => v,
776        Err(_) => return std::collections::HashMap::new(),
777    };
778    let mut map: std::collections::HashMap<String, std::collections::HashSet<String>> =
779        std::collections::HashMap::new();
780    for entry in &node_types {
781        if !entry.named {
782            continue;
783        }
784        let mut child_kinds = std::collections::HashSet::new();
785        for field_value in entry.fields.values() {
786            if let Some(types) = field_value.get("types").and_then(|t| t.as_array()) {
787                for t in types {
788                    if let (Some(name), Some(true)) = (
789                        t.get("type").and_then(|n| n.as_str()),
790                        t.get("named").and_then(serde_json::Value::as_bool),
791                    ) {
792                        child_kinds.insert(name.to_owned());
793                    }
794                }
795            }
796        }
797        if let Some(ref children) = entry.children {
798            for t in &children.types {
799                if t.named {
800                    child_kinds.insert(t.node_type.clone());
801                }
802            }
803        }
804        if !child_kinds.is_empty() {
805            map.insert(entry.node_type.clone(), child_kinds);
806        }
807    }
808    map
809}
810
811/// Augment `grammar.subtypes` with child-kind data from node-types.json.
812///
813/// For each parent kind P with node-type children, for each SYMBOL S
814/// referenced in P's grammar rule, for each child kind C in
815/// `node_type_children[P]`: if C does not already satisfy S, record
816/// C satisfies S. This closes the grammar/parser divergence where
817/// tree-sitter's parser produces child kinds not reachable from
818/// grammar.json's production rules.
819fn augment_subtypes_from_node_types(grammar: &mut Grammar) {
820    let pairs: Vec<(String, String)> = grammar
821        .node_type_children
822        .iter()
823        .flat_map(|(parent_kind, allowed_children)| {
824            let symbols: Vec<&str> = grammar
825                .rules
826                .get(parent_kind)
827                .map(|rule| referenced_symbols(rule))
828                .unwrap_or_default();
829            let mut out = Vec::new();
830            for sym_name in &symbols {
831                for child_kind in allowed_children {
832                    if !kind_satisfies_symbol(grammar, Some(child_kind), sym_name) {
833                        out.push((child_kind.clone(), (*sym_name).to_owned()));
834                    }
835                }
836            }
837            out
838        })
839        .collect();
840    for (child_kind, sym_name) in pairs {
841        grammar
842            .subtypes
843            .entry(child_kind)
844            .or_default()
845            .insert(sym_name);
846    }
847}
848
849/// Build a map from external scanner symbol names to their anonymous
850/// ALIAS values by walking every rule body in the grammar.
851fn build_external_alias_map(grammar: &Grammar) -> std::collections::HashMap<String, String> {
852    let mut map = std::collections::HashMap::new();
853    fn walk(
854        grammar: &Grammar,
855        prod: &Production,
856        map: &mut std::collections::HashMap<String, String>,
857    ) {
858        match prod {
859            Production::Alias {
860                content,
861                named,
862                value,
863            } => {
864                if !*named && !value.is_empty() {
865                    if let Production::Symbol { name } = content.as_ref() {
866                        if name.starts_with('_') && !grammar.rules.contains_key(name) {
867                            map.entry(name.clone()).or_insert_with(|| value.clone());
868                        }
869                    }
870                }
871                walk(grammar, content, map);
872            }
873            Production::Choice { members } | Production::Seq { members } => {
874                for m in members {
875                    walk(grammar, m, map);
876                }
877            }
878            Production::Repeat { content }
879            | Production::Repeat1 { content }
880            | Production::Optional { content }
881            | Production::Field { content, .. }
882            | Production::Token { content }
883            | Production::ImmediateToken { content }
884            | Production::Prec { content, .. }
885            | Production::PrecLeft { content, .. }
886            | Production::PrecRight { content, .. }
887            | Production::PrecDynamic { content, .. }
888            | Production::Reserved { content, .. } => walk(grammar, content, map),
889            _ => {}
890        }
891    }
892    for rule in grammar.rules.values() {
893        walk(grammar, rule, &mut map);
894    }
895    map
896}
897
898/// Identify rules whose `{`/`}` tokens are inline delimiters (e.g.
899/// interpolation) rather than block scopes. A rule is inline-brace
900/// iff its production SEQ contains both an opening brace token and
901/// `}`, and the members between them contain no REPEAT/REPEAT1
902/// (which would indicate a statement-list block).
903fn identify_inline_brace_rules(grammar: &Grammar) -> std::collections::HashSet<String> {
904    fn is_inline_brace_body(prod: &Production) -> bool {
905        match prod {
906            Production::Seq { members } => {
907                let open_idx = members.iter().position(|m| match m {
908                    Production::String { value } => value.contains('{'),
909                    _ => false,
910                });
911                let close_idx = members
912                    .iter()
913                    .rposition(|m| matches!(m, Production::String { value } if value == "}"));
914                if let (Some(open), Some(close)) = (open_idx, close_idx) {
915                    if open < close {
916                        let between = &members[open + 1..close];
917                        return !has_repeat(between);
918                    }
919                }
920                false
921            }
922            Production::Prec { content, .. }
923            | Production::PrecLeft { content, .. }
924            | Production::PrecRight { content, .. }
925            | Production::PrecDynamic { content, .. }
926            | Production::Token { content }
927            | Production::ImmediateToken { content }
928            | Production::Reserved { content, .. } => is_inline_brace_body(content),
929            _ => false,
930        }
931    }
932    fn has_repeat(members: &[Production]) -> bool {
933        members.iter().any(|m| match m {
934            Production::Repeat { .. } | Production::Repeat1 { .. } => true,
935            Production::Prec { content, .. }
936            | Production::PrecLeft { content, .. }
937            | Production::PrecRight { content, .. }
938            | Production::PrecDynamic { content, .. } => {
939                matches!(
940                    content.as_ref(),
941                    Production::Repeat { .. } | Production::Repeat1 { .. }
942                )
943            }
944            _ => false,
945        })
946    }
947    let mut result = std::collections::HashSet::new();
948    for (name, rule) in &grammar.rules {
949        if is_inline_brace_body(rule) {
950            result.insert(name.clone());
951        }
952    }
953    result
954}
955
956// ═══════════════════════════════════════════════════════════════════
957// Format policy
958// ═══════════════════════════════════════════════════════════════════
959
960/// Whitespace and indentation policy applied during emission.
961///
962/// The default policy inserts a single space between adjacent tokens,
963/// a newline after `;` / `}` / `{`, and tracks indent on `{` / `}`
964/// boundaries. Per-language overrides (idiomatic indent width,
965/// trailing-comma rules, blank-line conventions) can ride alongside
966/// this struct in a follow-up branch; today's defaults aim only for
967/// syntactic validity.
968#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
969pub struct FormatPolicy {
970    /// Number of spaces per indent level.
971    pub indent_width: usize,
972    /// Separator inserted between adjacent terminals that the lexer
973    /// would otherwise glue together (word ↔ word, operator ↔ operator).
974    /// Default is a single space.
975    pub separator: String,
976    /// Newline byte sequence emitted after `line_break_after` tokens
977    /// and at end-of-output. Default is `"\n"`.
978    pub newline: String,
979    /// Tokens after which the walker breaks to a new line.
980    pub line_break_after: Vec<String>,
981    /// Tokens that increase indent on emission.
982    pub indent_open: Vec<String>,
983    /// Tokens that decrease indent on emission.
984    pub indent_close: Vec<String>,
985}
986
987impl Default for FormatPolicy {
988    fn default() -> Self {
989        Self {
990            indent_width: 2,
991            separator: " ".to_owned(),
992            newline: "\n".to_owned(),
993            line_break_after: vec![";".into(), "{".into(), "}".into()],
994            indent_open: vec!["{".into()],
995            indent_close: vec!["}".into()],
996        }
997    }
998}
999
1000// ═══════════════════════════════════════════════════════════════════
1001// Emitter
1002// ═══════════════════════════════════════════════════════════════════
1003
1004/// Emit a by-construction schema to source bytes.
1005///
1006/// `protocol` is the grammar / language name (used in error messages
1007/// and to label the entry point).
1008///
1009/// The walker treats `schema.entries` as the ordered list of root
1010/// vertices, falling back to a deterministic by-id ordering when
1011/// `entries` is empty. Each root is emitted using the production
1012/// associated with its kind in `grammar.rules`.
1013///
1014/// # Errors
1015///
1016/// Returns [`ParseError::EmitFailed`] when:
1017///
1018/// - the schema has no vertices
1019/// - a root vertex's kind is not a grammar rule
1020/// - a `SYMBOL` reference points at a kind with no rule and no schema
1021///   child to resolve it to
1022/// - a required `FIELD` has no corresponding edge in the schema
1023pub fn emit_pretty(
1024    protocol: &str,
1025    schema: &Schema,
1026    grammar: &Grammar,
1027    policy: &FormatPolicy,
1028) -> Result<Vec<u8>, ParseError> {
1029    let roots = collect_roots(schema);
1030    if roots.is_empty() {
1031        return Err(ParseError::EmitFailed {
1032            protocol: protocol.to_owned(),
1033            reason: "schema has no entry vertices".to_owned(),
1034        });
1035    }
1036
1037    let mut out = Output::new(policy);
1038    for (i, root) in roots.iter().enumerate() {
1039        if i > 0 {
1040            out.newline();
1041        }
1042        emit_vertex(protocol, schema, grammar, root, &mut out)?;
1043    }
1044    Ok(out.finish())
1045}
1046
1047fn collect_roots(schema: &Schema) -> Vec<&panproto_gat::Name> {
1048    if !schema.entries.is_empty() {
1049        return schema
1050            .entries
1051            .iter()
1052            .filter(|name| schema.vertices.contains_key(*name))
1053            .collect();
1054    }
1055
1056    // Fallback: every vertex that is not the target of any structural edge
1057    // (sorted by id for determinism).
1058    let mut targets: std::collections::HashSet<&panproto_gat::Name> =
1059        std::collections::HashSet::new();
1060    for edge in schema.edges.keys() {
1061        targets.insert(&edge.tgt);
1062    }
1063    let mut roots: Vec<&panproto_gat::Name> = schema
1064        .vertices
1065        .keys()
1066        .filter(|name| !targets.contains(name))
1067        .collect();
1068    roots.sort();
1069    roots
1070}
1071
1072fn emit_vertex(
1073    protocol: &str,
1074    schema: &Schema,
1075    grammar: &Grammar,
1076    vertex_id: &panproto_gat::Name,
1077    out: &mut Output<'_>,
1078) -> Result<(), ParseError> {
1079    let vertex = schema
1080        .vertices
1081        .get(vertex_id)
1082        .ok_or_else(|| ParseError::EmitFailed {
1083            protocol: protocol.to_owned(),
1084            reason: format!("vertex '{vertex_id}' not found"),
1085        })?;
1086
1087    // Leaf shortcut: a vertex carrying a `literal-value` constraint
1088    // and no outgoing structural edges is a terminal token. Emit the
1089    // captured value directly. This handles identifiers, numeric
1090    // literals, and string literals that the parser stored as
1091    // `literal-value` even on by-construction schemas.
1092    if let Some(literal) = literal_value(schema, vertex_id) {
1093        if children_for(schema, vertex_id).is_empty() {
1094            out.token(literal);
1095            return Ok(());
1096        }
1097    }
1098
1099    let kind = vertex.kind.as_ref();
1100    let edges = children_for(schema, vertex_id);
1101    if let Some(rule) = grammar.rules.get(kind) {
1102        let old_suppress = out.suppress_brace_indent;
1103        if grammar.inline_brace_rules.contains(kind) {
1104            out.suppress_brace_indent = true;
1105        }
1106        let mut cursor = ChildCursor::new(&edges);
1107        emit_production(protocol, schema, grammar, vertex_id, rule, &mut cursor, out)?;
1108        // Drain any extras left after the rule walk completed; tree-sitter
1109        // may record trailing comments as children of the surrounding
1110        // vertex (i.e. after the last structural child the rule matched).
1111        drain_extras(protocol, schema, grammar, &mut cursor, out)?;
1112        out.suppress_brace_indent = old_suppress;
1113        return Ok(());
1114    }
1115
1116    // No rule for this kind. The parser produced it via an ALIAS
1117    // (tree-sitter's `alias($.something, $.actual_kind)`) or via an
1118    // external scanner (e.g. YAML's `document` root). Fall back to
1119    // walking the children directly so the inner content survives;
1120    // surrounding tokens — whose only source is the missing rule —
1121    // are necessarily absent.
1122    for edge in &edges {
1123        emit_vertex(protocol, schema, grammar, &edge.tgt, out)?;
1124    }
1125    Ok(())
1126}
1127
1128/// Linear cursor over a vertex's outgoing edges, used to thread
1129/// children through a production rule without double-consuming them.
1130struct ChildCursor<'a> {
1131    edges: &'a [&'a Edge],
1132    consumed: Vec<bool>,
1133}
1134
1135impl<'a> ChildCursor<'a> {
1136    fn new(edges: &'a [&'a Edge]) -> Self {
1137        Self {
1138            edges,
1139            consumed: vec![false; edges.len()],
1140        }
1141    }
1142
1143    /// Take the next unconsumed edge whose kind equals `field_name`.
1144    fn take_field(&mut self, field_name: &str) -> Option<&'a Edge> {
1145        for (i, edge) in self.edges.iter().enumerate() {
1146            if !self.consumed[i] && edge.kind.as_ref() == field_name {
1147                self.consumed[i] = true;
1148                return Some(edge);
1149            }
1150        }
1151        None
1152    }
1153
1154    /// Whether any unconsumed edge satisfies `predicate`. Used by the
1155    /// unit tests; the live emit path went through `has_matching` on
1156    /// each alternative until cursor-driven dispatch was rewritten to
1157    /// pick the first-unconsumed-edge's kind directly.
1158    #[cfg(test)]
1159    fn has_matching(&self, predicate: impl Fn(&Edge) -> bool) -> bool {
1160        self.edges
1161            .iter()
1162            .enumerate()
1163            .any(|(i, edge)| !self.consumed[i] && predicate(edge))
1164    }
1165
1166    /// Take the next unconsumed edge whose target vertex satisfies
1167    /// `predicate`. Returns the edge and the underlying production
1168    /// resolution path is the caller's job.
1169    fn take_matching(&mut self, predicate: impl Fn(&Edge) -> bool) -> Option<&'a Edge> {
1170        for (i, edge) in self.edges.iter().enumerate() {
1171            if !self.consumed[i] && predicate(edge) {
1172                self.consumed[i] = true;
1173                return Some(edge);
1174            }
1175        }
1176        None
1177    }
1178}
1179
1180thread_local! {
1181    static EMIT_DEPTH: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
1182    /// Set of `(vertex_id, rule_name)` pairs that are currently being
1183    /// walked by the recursion. A SYMBOL that resolves to a rule
1184    /// already on this stack closes a μ-binder cycle: in the
1185    /// coinductive reading, the rule walk at any vertex is the least
1186    /// fixed point of `body[μ X . body / X]`, which unfolds at most
1187    /// once, with the second visit returning the empty sequence (the
1188    /// unit of the free token monoid). Examples that trigger this:
1189    /// YAML's `stream` ⊃ `_b_blk_*` mutually-recursive chain, Rust's
1190    /// `_expression` ⊃ `binary_expression` ⊃ `_expression`.
1191    static EMIT_MU_FRAMES: std::cell::RefCell<std::collections::HashSet<(String, String)>> =
1192        std::cell::RefCell::new(std::collections::HashSet::new());
1193    /// The name of the FIELD whose body the walker is currently inside,
1194    /// or `None` at top level. Lets a SYMBOL nested arbitrarily deep
1195    /// in the field's content (under SEQ, CHOICE, REPEAT, OPTIONAL)
1196    /// consume from the *outer* cursor by edge-kind rather than from
1197    /// the child's own cursor by symbol-match. Without this, shapes
1198    /// like `field('args', commaSep1($.X))` — which expands to
1199    /// `FIELD(SEQ(SYMBOL X, REPEAT(SEQ(',', SYMBOL X))))` — emit only
1200    /// the first matched edge: the FIELD handler consumed one edge,
1201    /// the inner REPEAT searched the consumed child's cursor (which
1202    /// has no more sibling field edges), and the REPEAT broke after
1203    /// one iteration. Setting the context here so the inner SYMBOL
1204    /// pulls successive field-named edges from the outer cursor
1205    /// recovers every matched edge across arbitrary nesting.
1206    static EMIT_FIELD_CONTEXT: std::cell::RefCell<Option<String>> =
1207        const { std::cell::RefCell::new(None) };
1208}
1209
1210/// RAII guard that restores the prior `EMIT_FIELD_CONTEXT` value on drop.
1211struct FieldContextGuard(Option<String>);
1212
1213impl Drop for FieldContextGuard {
1214    fn drop(&mut self) {
1215        EMIT_FIELD_CONTEXT.with(|f| *f.borrow_mut() = self.0.take());
1216    }
1217}
1218
1219fn push_field_context(name: &str) -> FieldContextGuard {
1220    let prev = EMIT_FIELD_CONTEXT.with(|f| f.borrow_mut().replace(name.to_owned()));
1221    FieldContextGuard(prev)
1222}
1223
1224/// Clear the field context for the duration of a child-context walk.
1225/// The child's own production has its own FIELDs that set their own
1226/// context; the outer field hint must not leak into them.
1227fn clear_field_context() -> FieldContextGuard {
1228    let prev = EMIT_FIELD_CONTEXT.with(|f| f.borrow_mut().take());
1229    FieldContextGuard(prev)
1230}
1231
1232fn current_field_context() -> Option<String> {
1233    EMIT_FIELD_CONTEXT.with(|f| f.borrow().clone())
1234}
1235
1236/// Walk a rule at a vertex inside a μ-binder. The wrapping frame is
1237/// pushed before recursion and popped after, so any SYMBOL inside
1238/// `rule` that re-enters the same `(vertex_id, rule_name)` pair
1239/// returns the empty sequence (μ X . body unfolds once).
1240fn walk_in_mu_frame(
1241    protocol: &str,
1242    schema: &Schema,
1243    grammar: &Grammar,
1244    vertex_id: &panproto_gat::Name,
1245    rule_name: &str,
1246    rule: &Production,
1247    cursor: &mut ChildCursor<'_>,
1248    out: &mut Output<'_>,
1249) -> Result<(), ParseError> {
1250    let key = (vertex_id.to_string(), rule_name.to_owned());
1251    let inserted = EMIT_MU_FRAMES.with(|frames| frames.borrow_mut().insert(key.clone()));
1252    if !inserted {
1253        // We are already walking this rule at this vertex deeper in
1254        // the call stack. The coinductive μ-fixed-point reading
1255        // returns the empty sequence here; the surrounding
1256        // production resumes after the SYMBOL.
1257        return Ok(());
1258    }
1259    let result = emit_production(protocol, schema, grammar, vertex_id, rule, cursor, out);
1260    EMIT_MU_FRAMES.with(|frames| {
1261        frames.borrow_mut().remove(&key);
1262    });
1263    result
1264}
1265
1266fn emit_production(
1267    protocol: &str,
1268    schema: &Schema,
1269    grammar: &Grammar,
1270    vertex_id: &panproto_gat::Name,
1271    production: &Production,
1272    cursor: &mut ChildCursor<'_>,
1273    out: &mut Output<'_>,
1274) -> Result<(), ParseError> {
1275    let depth = EMIT_DEPTH.with(|d| {
1276        let v = d.get() + 1;
1277        d.set(v);
1278        v
1279    });
1280    if depth > 500 {
1281        EMIT_DEPTH.with(|d| d.set(d.get() - 1));
1282        return Err(ParseError::EmitFailed {
1283            protocol: protocol.to_owned(),
1284            reason: format!(
1285                "emit_production recursion >500 (likely a cyclic grammar; \
1286                     vertex='{vertex_id}')"
1287            ),
1288        });
1289    }
1290    drain_extras(protocol, schema, grammar, cursor, out)?;
1291    let result = emit_production_inner(
1292        protocol, schema, grammar, vertex_id, production, cursor, out,
1293    );
1294    EMIT_DEPTH.with(|d| d.set(d.get() - 1));
1295    result
1296}
1297
1298/// Consume and emit every leading edge on `cursor` whose target kind
1299/// is in `grammar.extras` (typically `line_comment` / `block_comment`).
1300/// Extras live outside the production grammar — tree-sitter skips them
1301/// at parse time and records them as children of the surrounding
1302/// vertex — so the rule walker cannot reconcile them against the
1303/// cursor. Draining them as a side channel preserves their content in
1304/// the output without confusing the structural matchers.
1305fn drain_extras(
1306    protocol: &str,
1307    schema: &Schema,
1308    grammar: &Grammar,
1309    cursor: &mut ChildCursor<'_>,
1310    out: &mut Output<'_>,
1311) -> Result<(), ParseError> {
1312    if grammar.extras.is_empty() {
1313        return Ok(());
1314    }
1315    loop {
1316        let next_extra: Option<usize> = cursor
1317            .edges
1318            .iter()
1319            .enumerate()
1320            .find(|(i, _)| !cursor.consumed[*i])
1321            .and_then(|(i, edge)| {
1322                let kind = schema.vertices.get(&edge.tgt).map(|v| v.kind.as_ref())?;
1323                if grammar.extras.contains(kind) {
1324                    Some(i)
1325                } else {
1326                    None
1327                }
1328            });
1329        let Some(idx) = next_extra else {
1330            return Ok(());
1331        };
1332        cursor.consumed[idx] = true;
1333        let target = &cursor.edges[idx].tgt;
1334        emit_vertex(protocol, schema, grammar, target, out)?;
1335    }
1336}
1337
1338fn emit_production_inner(
1339    protocol: &str,
1340    schema: &Schema,
1341    grammar: &Grammar,
1342    vertex_id: &panproto_gat::Name,
1343    production: &Production,
1344    cursor: &mut ChildCursor<'_>,
1345    out: &mut Output<'_>,
1346) -> Result<(), ParseError> {
1347    match production {
1348        Production::String { value } => {
1349            out.token(value);
1350            Ok(())
1351        }
1352        Production::Pattern { value } => {
1353            if let Some(literal) = literal_value(schema, vertex_id) {
1354                out.token(literal);
1355            } else if is_newline_like_pattern(value) {
1356                // Patterns like `\r?\n`, `\n`, `\r\n` are the structural
1357                // newline tokens grammars use to separate top-level
1358                // statements (csound's `_new_line`, ABC's line-end, etc.).
1359                // Emitting them through the placeholder fallback rendered
1360                // the bare `_` sentinel between siblings; route them to
1361                // the layout pass's line-break instead so the output
1362                // re-parses.
1363                out.newline();
1364            } else if is_whitespace_only_pattern(value) {
1365                // `\s+`, `[ \t]+` and friends are interstitial whitespace
1366                // tokens. Emit nothing: the layout pass inserts the
1367                // policy separator between adjacent Lits if needed.
1368            } else {
1369                out.token(&placeholder_for_pattern(value));
1370            }
1371            Ok(())
1372        }
1373        Production::Blank => Ok(()),
1374        Production::Symbol { name } => {
1375            // Inside a FIELD body, a SYMBOL consumes by field-name on
1376            // the outer cursor rather than searching by symbol-match.
1377            // This covers the simple `FIELD(SYMBOL X)` case as well as
1378            // every nesting under FIELD that contains SYMBOLs (SEQ,
1379            // CHOICE, REPEAT, OPTIONAL, ALIAS). Without the override,
1380            // shapes like `field('args', commaSep1($.X))` consume one
1381            // field edge in the FIELD handler and then the REPEAT
1382            // inside SEQ searches the consumed child's cursor — where
1383            // no sibling field edges sit — and breaks after one
1384            // iteration.
1385            if let Some(field) = current_field_context() {
1386                if let Some(edge) = cursor.take_field(&field) {
1387                    return emit_in_child_context(
1388                        protocol, schema, grammar, &edge.tgt, production, out,
1389                    );
1390                }
1391                // No matching field-named edge left on the outer
1392                // cursor. Surface nothing; the surrounding REPEAT /
1393                // OPTIONAL / CHOICE backtracks the literal tokens it
1394                // emitted on this iteration when it sees no progress.
1395                return Ok(());
1396            }
1397            if name.starts_with('_') {
1398                // Hidden rule: not a vertex kind on the schema side.
1399                // Inline-expand the rule body so its children take
1400                // edges from the current cursor, instead of trying to
1401                // take a single child edge that "satisfies" the
1402                // hidden rule and discarding the rest of the body
1403                // (which would drop tokens like `=` and the trailing
1404                // value SYMBOL inside e.g. TOML's `_inline_pair`).
1405                //
1406                // Wrapped in a μ-frame so a hidden rule that
1407                // references its own kind cyclically (or another
1408                // hidden rule that closes the cycle) unfolds once
1409                // and then collapses to the empty sequence at the
1410                // second visit, rather than blowing the stack.
1411                if let Some(rule) = grammar.rules.get(name) {
1412                    walk_in_mu_frame(
1413                        protocol, schema, grammar, vertex_id, name, rule, cursor, out,
1414                    )
1415                } else {
1416                    // External hidden rule (declared in the
1417                    // grammar's `externals` block, scanned by C code,
1418                    // not listed in `rules`). Heuristic fallback by
1419                    // name:
1420                    //
1421                    // - `_indent` / `*_indent`: open an indent block.
1422                    //   Indent-based grammars (Python, YAML, qvr)
1423                    //   declare an `_indent` external scanner before
1424                    //   the body of a block-bodied declaration; the
1425                    //   emitted output is unparseable without the
1426                    //   corresponding indentation jump.
1427                    // - `_dedent` / `*_dedent`: close the matching
1428                    //   indent block.
1429                    // - `_newline` / `*_line_ending` / `*_or_eof`:
1430                    //   universally newline-or-empty; emitting a
1431                    //   single newline is the right default for
1432                    //   grammars like TOML whose `pair` SEQ trails
1433                    //   into `_line_ending_or_eof`.
1434                    //
1435                    // Check the precomputed alias map first: if this
1436                    // external token appears as the content of an
1437                    // anonymous ALIAS elsewhere in the grammar, emit
1438                    // the alias value as the token text.
1439                    if let Some(alias_value) = grammar.external_alias_map.get(name) {
1440                        out.token(alias_value);
1441                        return Ok(());
1442                    }
1443                    if name == "_indent" || name.ends_with("_indent") {
1444                        out.indent_open();
1445                    } else if name == "_dedent" || name.ends_with("_dedent") {
1446                        out.indent_close();
1447                    } else if name.contains("line_ending")
1448                        || name.contains("newline")
1449                        || name.ends_with("_or_eof")
1450                    {
1451                        out.newline();
1452                    } else if name.contains("semicolon") {
1453                        out.token(";");
1454                    }
1455                    Ok(())
1456                }
1457            } else if let Some(edge) = { take_symbol_match(grammar, schema, cursor, name) } {
1458                // For supertype / hidden-rule dispatch the child's
1459                // own kind names the actual production to walk
1460                // (`child.kind` IS the subtype). For ALIAS the
1461                // dependent-optic context is carried by the
1462                // surrounding `Production::Alias` branch, which calls
1463                // `emit_aliased_child` directly; we don't reach here
1464                // for that case. So walking `grammar.rules[child.kind]`
1465                // via `emit_vertex` is correct: the dependent-optic
1466                // path is preserved at every site where it actually
1467                // diverges from `child.kind`.
1468                emit_vertex(protocol, schema, grammar, &edge.tgt, out)
1469            } else if vertex_id_kind(schema, vertex_id) == Some(name.as_str()) {
1470                let rule = grammar
1471                    .rules
1472                    .get(name)
1473                    .ok_or_else(|| ParseError::EmitFailed {
1474                        protocol: protocol.to_owned(),
1475                        reason: format!("no production for SYMBOL '{name}'"),
1476                    })?;
1477                // Self-reference (`X = ... SYMBOL X ...`): wrap in a
1478                // μ-frame so re-entry collapses to the empty sequence.
1479                walk_in_mu_frame(
1480                    protocol, schema, grammar, vertex_id, name, rule, cursor, out,
1481                )
1482            } else {
1483                // Named rule with no matching child: emit nothing and
1484                // let the surrounding CHOICE / OPTIONAL / REPEAT
1485                // resolve the absence.
1486                Ok(())
1487            }
1488        }
1489        Production::Seq { members } => {
1490            for member in members {
1491                emit_production(protocol, schema, grammar, vertex_id, member, cursor, out)?;
1492            }
1493            Ok(())
1494        }
1495        Production::Choice { members } => {
1496            if let Some(matched) =
1497                pick_choice_with_cursor(schema, grammar, vertex_id, cursor, members)
1498            {
1499                emit_production(protocol, schema, grammar, vertex_id, matched, cursor, out)
1500            } else {
1501                Ok(())
1502            }
1503        }
1504        Production::Repeat { content } | Production::Repeat1 { content } => {
1505            // Detect a "separator-leading SEQ" iteration body: SEQ whose
1506            // first member is a CHOICE containing BLANK (or an OPTIONAL),
1507            // i.e. the source-level separator between two iterations is
1508            // syntactically optional. When the chosen alternative for
1509            // that separator slot emits zero content tokens at runtime,
1510            // there was no source-level separator between this iteration
1511            // and the previous one; the layout pass must suppress its
1512            // policy separator to match the source's tight adjacency.
1513            //
1514            // Categorical reading: REPEAT body `B = SEQ(SEP, BODY)` is
1515            // the pullback of two halves. The bytes emitted in iteration
1516            // k+1 are a concatenation of `SEP_k+1` and `BODY_k+1`; if
1517            // `SEP_k+1` is the empty word, the concatenation of
1518            // `BODY_k` and `BODY_k+1` must remain a single contiguous
1519            // span. Hence the NoSpace marker.
1520            let separator_leading_seq: Option<&[Production]> = match content.as_ref() {
1521                Production::Seq { members } if members.len() >= 2 => {
1522                    let first = &members[0];
1523                    let is_separator_slot = match first {
1524                        Production::Choice { members } => {
1525                            members.iter().any(|m| matches!(m, Production::Blank))
1526                        }
1527                        Production::Optional { .. } => true,
1528                        _ => false,
1529                    };
1530                    if is_separator_slot {
1531                        Some(members.as_slice())
1532                    } else {
1533                        None
1534                    }
1535                }
1536                _ => None,
1537            };
1538
1539            let mut emitted_any = false;
1540            loop {
1541                let cursor_snap = cursor.consumed.clone();
1542                let out_snap = out.snapshot();
1543                let consumed_before = cursor.consumed.iter().filter(|&&c| c).count();
1544                let result: Result<(), ParseError> =
1545                    if let Some(seq_members) = separator_leading_seq {
1546                        // Emit the separator slot first and observe
1547                        // whether it contributed any Lit. If not, push
1548                        // a NoSpace marker before walking the remaining
1549                        // SEQ members. The OutputSnapshot here covers
1550                        // only the separator's emission window.
1551                        let pre_sep = out.snapshot();
1552                        let sep_result = emit_production(
1553                            protocol,
1554                            schema,
1555                            grammar,
1556                            vertex_id,
1557                            &seq_members[0],
1558                            cursor,
1559                            out,
1560                        );
1561                        match sep_result {
1562                            Err(e) => Err(e),
1563                            Ok(()) => {
1564                                if !out.lit_emitted_since(pre_sep) {
1565                                    out.no_space();
1566                                }
1567                                let mut rest_result = Ok(());
1568                                for member in &seq_members[1..] {
1569                                    rest_result = emit_production(
1570                                        protocol, schema, grammar, vertex_id, member, cursor, out,
1571                                    );
1572                                    if rest_result.is_err() {
1573                                        break;
1574                                    }
1575                                }
1576                                rest_result
1577                            }
1578                        }
1579                    } else {
1580                        emit_production(protocol, schema, grammar, vertex_id, content, cursor, out)
1581                    };
1582                let consumed_after = cursor.consumed.iter().filter(|&&c| c).count();
1583                if result.is_err() || consumed_after == consumed_before {
1584                    cursor.consumed = cursor_snap;
1585                    out.restore(out_snap);
1586                    break;
1587                }
1588                emitted_any = true;
1589            }
1590            if matches!(production, Production::Repeat1 { .. }) && !emitted_any {
1591                emit_production(protocol, schema, grammar, vertex_id, content, cursor, out)?;
1592            }
1593            Ok(())
1594        }
1595        Production::Optional { content } => {
1596            let cursor_snap = cursor.consumed.clone();
1597            let out_snap = out.snapshot();
1598            let consumed_before = cursor.consumed.iter().filter(|&&c| c).count();
1599            let result =
1600                emit_production(protocol, schema, grammar, vertex_id, content, cursor, out);
1601            // OPTIONAL is a backtracking site: if the inner production
1602            // errored *or* made no progress without leaving a witness
1603            // constraint, restore both cursor and output to their
1604            // pre-attempt state. Mirrors `Repeat`'s loop body.
1605            if result.is_err() {
1606                cursor.consumed = cursor_snap;
1607                out.restore(out_snap);
1608                return result;
1609            }
1610            let consumed_after = cursor.consumed.iter().filter(|&&c| c).count();
1611            if consumed_after == consumed_before
1612                && !has_relevant_constraint(content, schema, vertex_id)
1613            {
1614                cursor.consumed = cursor_snap;
1615                out.restore(out_snap);
1616            }
1617            Ok(())
1618        }
1619        Production::Field { name, content } => {
1620            // Set the field context for the duration of `content`'s
1621            // walk and emit the content against the *outer* cursor.
1622            // The SYMBOL handler picks up the context and pulls
1623            // successive `take_field(name)` edges as it encounters
1624            // SYMBOLs anywhere under `content` (under SEQ, CHOICE,
1625            // REPEAT, OPTIONAL, ALIAS — arbitrarily nested). This
1626            // subsumes the prior carve-outs for FIELD(REPEAT(...)),
1627            // FIELD(REPEAT1(...)), and the bare FIELD(SYMBOL ...)
1628            // case, and adds coverage for
1629            // `field('xs', commaSep1($.X))` which expands to
1630            // FIELD(SEQ(SYMBOL X, REPEAT(SEQ(',', SYMBOL X)))) and
1631            // any other shape where REPEAT/REPEAT1 sits inside SEQ /
1632            // CHOICE / OPTIONAL under a FIELD. A FIELD that wraps a
1633            // non-SYMBOL production (e.g. `field('op', '+')` or
1634            // `field('op', CHOICE(STRING ...))`) still works: STRING
1635            // handlers ignore the context and emit literals
1636            // directly, so the operator token survives the round
1637            // trip.
1638            let _guard = push_field_context(name);
1639            emit_production(protocol, schema, grammar, vertex_id, content, cursor, out)
1640        }
1641        Production::Alias {
1642            content,
1643            named,
1644            value,
1645        } => {
1646            // A named ALIAS rewrites the parser-visible kind to
1647            // `value`. If the cursor has an unconsumed child whose
1648            // kind matches that alias name, take it and emit the
1649            // child using the alias's INNER content as the rule
1650            // (e.g. `ALIAS { SYMBOL real_rule, value: "kind_x" }`
1651            // means a `kind_x` vertex on the schema should be walked
1652            // through `real_rule`'s body, not through whatever rule
1653            // happens to be keyed under `kind_x`). This is the
1654            // dependent-optic shape: the rule the emitter walks at a
1655            // child position is determined by the parent's chosen
1656            // alias, not by the child kind alone — without it,
1657            // grammars like YAML that introduce the same kind through
1658            // many ALIAS sites lose the parent context the moment
1659            // emit_vertex is called.
1660            if *named && !value.is_empty() {
1661                if let Some(edge) = cursor.take_matching(|edge| {
1662                    schema
1663                        .vertices
1664                        .get(&edge.tgt)
1665                        .map(|v| v.kind.as_ref() == value.as_str())
1666                        .unwrap_or(false)
1667                }) {
1668                    return emit_aliased_child(protocol, schema, grammar, &edge.tgt, content, out);
1669                }
1670            }
1671            // For anonymous aliases (named: false) whose content is an
1672            // external scanner token with no grammar rule (e.g.
1673            // JavaScript's `_ternary_qmark` aliased to `"?"`), emit the
1674            // alias value directly. The content's SYMBOL handler would
1675            // fall through the external-token heuristic and produce
1676            // nothing; the alias value IS the token text.
1677            if !*named && !value.is_empty() {
1678                if let Production::Symbol { name: sym } = content.as_ref() {
1679                    if sym.starts_with('_') && !grammar.rules.contains_key(sym) {
1680                        out.token(value);
1681                        return Ok(());
1682                    }
1683                }
1684            }
1685            emit_production(protocol, schema, grammar, vertex_id, content, cursor, out)
1686        }
1687        Production::Token { content }
1688        | Production::ImmediateToken { content }
1689        | Production::Prec { content, .. }
1690        | Production::PrecLeft { content, .. }
1691        | Production::PrecRight { content, .. }
1692        | Production::PrecDynamic { content, .. }
1693        | Production::Reserved { content, .. } => {
1694            emit_production(protocol, schema, grammar, vertex_id, content, cursor, out)
1695        }
1696    }
1697}
1698
1699/// Take the next cursor edge whose target vertex's kind matches the
1700/// SYMBOL `name` directly or via inline expansion of a hidden rule.
1701fn take_symbol_match<'a>(
1702    grammar: &Grammar,
1703    schema: &Schema,
1704    cursor: &mut ChildCursor<'a>,
1705    name: &str,
1706) -> Option<&'a Edge> {
1707    // Prefer non-field edges (`child_of`) to avoid consuming a
1708    // field-named edge that a later FIELD handler should claim.
1709    // Field-named edges (edge.kind != "child_of") are reserved for
1710    // the FIELD production that names them; consuming one here would
1711    // steal it from its intended handler (e.g. `as_pattern`'s
1712    // `alias` field edge consumed by the leading `expression`
1713    // SYMBOL instead of the trailing FIELD "alias" handler).
1714    if let Some(edge) = cursor.take_matching(|edge| {
1715        edge.kind.as_ref() == "child_of" && {
1716            let target_kind = schema.vertices.get(&edge.tgt).map(|v| v.kind.as_ref());
1717            kind_satisfies_symbol(grammar, target_kind, name)
1718        }
1719    }) {
1720        return Some(edge);
1721    }
1722    cursor.take_matching(|edge| {
1723        let target_kind = schema.vertices.get(&edge.tgt).map(|v| v.kind.as_ref());
1724        kind_satisfies_symbol(grammar, target_kind, name)
1725    })
1726}
1727
1728/// Decide whether a schema vertex of kind `target_kind` satisfies a
1729/// SYMBOL `name` reference in the grammar.
1730///
1731/// Operates as an O(1) lookup against the precomputed subtype
1732/// closure built at [`Grammar::from_bytes`]. The semantic content is
1733/// "K satisfies SYMBOL S iff K is reachable from S by walking the
1734/// grammar's hidden, supertype, and named-alias dispatch": this is
1735/// exactly the relation tree-sitter induces on `(parser-visible kind,
1736/// rule-position)` pairs.
1737fn kind_satisfies_symbol(grammar: &Grammar, target_kind: Option<&str>, name: &str) -> bool {
1738    let Some(target) = target_kind else {
1739        return false;
1740    };
1741    if target == name {
1742        return true;
1743    }
1744    grammar
1745        .subtypes
1746        .get(target)
1747        .is_some_and(|set| set.contains(name))
1748}
1749
1750/// Emit a child reached through an ALIAS production using the
1751/// alias's inner content as the rule, not `grammar.rules[child.kind]`.
1752///
1753/// This carries the dependent-optic context across the ALIAS edge:
1754/// at the parent rule's site we know which underlying production the
1755/// alias wraps (typically `SYMBOL real_rule`), and that's the
1756/// production that should drive the emit walk on the child's
1757/// children. Looking up `grammar.rules.get(child.kind)` instead would
1758/// either fail (the renamed kind has no top-level rule, e.g. YAML's
1759/// `block_mapping_pair`) or pick an arbitrary same-kinded rule from
1760/// elsewhere in the grammar.
1761///
1762/// Walk-context invariant. The dependent-optic shape of `emit_pretty`
1763/// says: the production walked at any vertex is determined by the
1764/// path from the root through the grammar, not by the vertex kind in
1765/// isolation. Two dispatch sites realise that invariant:
1766///
1767/// * [`emit_vertex`] looks up `grammar.rules[child.kind]` and walks
1768///   it. Correct for supertype / hidden-rule dispatch: the child's
1769///   kind on the schema IS the subtype tree-sitter selected, so its
1770///   top-level rule is the right production to walk.
1771/// * `emit_aliased_child` threads the parent rule's `Production`
1772///   directly (the inner `content` of `Production::Alias`) and walks
1773///   it on the child's children. Correct for ALIAS dispatch: the
1774///   child's kind on the schema is the alias's `value` (a renamed
1775///   kind that may have no top-level rule), and the production to
1776///   walk is the alias's content body, supplied by the parent.
1777///
1778/// Together these cover every site where the rule-walked-at-child
1779/// diverges from `grammar.rules[child.kind]`; the recursion site for
1780/// plain SYMBOL therefore correctly delegates to `emit_vertex`, and
1781/// we do not need a richer `WalkContext` value passed by reference.
1782/// The grammar dependency is the thread.
1783fn emit_aliased_child(
1784    protocol: &str,
1785    schema: &Schema,
1786    grammar: &Grammar,
1787    child_id: &panproto_gat::Name,
1788    content: &Production,
1789    out: &mut Output<'_>,
1790) -> Result<(), ParseError> {
1791    // Leaf shortcut: if the child has a literal-value and no
1792    // structural children, emit the captured text. Identifiers and
1793    // similar terminals reach here when an ALIAS wraps a SYMBOL that
1794    // resolves to a PATTERN.
1795    if let Some(literal) = literal_value(schema, child_id) {
1796        if children_for(schema, child_id).is_empty() {
1797            out.token(literal);
1798            return Ok(());
1799        }
1800    }
1801
1802    // Clear the enclosing FIELD context so it does not leak into the
1803    // aliased child's production walk. Without this, a FIELD("alias")
1804    // containing an ALIAS whose content is SYMBOL "expression" would
1805    // cause the inner SYMBOL handler to pull by field name "alias"
1806    // instead of by symbol match, failing to find the child edge.
1807    let _guard = clear_field_context();
1808
1809    // Resolve `content` to a rule when it's a SYMBOL (the dominant
1810    // shape: `ALIAS { content: SYMBOL real_rule, value: "kind_x" }`).
1811    if let Production::Symbol { name } = content {
1812        if let Some(rule) = grammar.rules.get(name) {
1813            let edges = children_for(schema, child_id);
1814            let mut cursor = ChildCursor::new(&edges);
1815            return emit_production(protocol, schema, grammar, child_id, rule, &mut cursor, out);
1816        }
1817    }
1818
1819    // Other ALIAS contents (CHOICE, SEQ, literals) walk in place.
1820    let edges = children_for(schema, child_id);
1821    let mut cursor = ChildCursor::new(&edges);
1822    emit_production(
1823        protocol,
1824        schema,
1825        grammar,
1826        child_id,
1827        content,
1828        &mut cursor,
1829        out,
1830    )
1831}
1832
1833fn emit_in_child_context(
1834    protocol: &str,
1835    schema: &Schema,
1836    grammar: &Grammar,
1837    child_id: &panproto_gat::Name,
1838    production: &Production,
1839    out: &mut Output<'_>,
1840) -> Result<(), ParseError> {
1841    // The child walks under its own production tree, with its own
1842    // FIELDs setting their own contexts. Clear the outer FIELD hint
1843    // so it does not leak through and cause sibling SYMBOLs inside
1844    // the child's body to mistakenly pull edges from the child's
1845    // cursor by the parent's field name.
1846    let _guard = clear_field_context();
1847    // If `production` is a structural wrapper (CHOICE / SEQ /
1848    // OPTIONAL / ...) whose referenced symbols cover the child's own
1849    // kind, the child IS the production's target node and the right
1850    // emit path is `emit_vertex(child)` (which honours the
1851    // literal-value leaf shortcut). Without this guard, FIELD(pattern,
1852    // CHOICE { _pattern, self }) on an identifier child walks the
1853    // CHOICE on the identifier's empty cursor, falls through to the
1854    // first non-BLANK alt, and loses the captured identifier text.
1855    if !matches!(production, Production::Symbol { .. }) {
1856        let child_kind = schema.vertices.get(child_id).map(|v| v.kind.as_ref());
1857        let symbols = referenced_symbols(production);
1858        if symbols
1859            .iter()
1860            .any(|s| kind_satisfies_symbol(grammar, child_kind, s) || child_kind == Some(s))
1861        {
1862            return emit_vertex(protocol, schema, grammar, child_id, out);
1863        }
1864    }
1865    match production {
1866        Production::Symbol { .. } => emit_vertex(protocol, schema, grammar, child_id, out),
1867        _ => {
1868            let edges = children_for(schema, child_id);
1869            let mut cursor = ChildCursor::new(&edges);
1870            emit_production(
1871                protocol,
1872                schema,
1873                grammar,
1874                child_id,
1875                production,
1876                &mut cursor,
1877                out,
1878            )
1879        }
1880    }
1881}
1882
1883fn pick_choice_with_cursor<'a>(
1884    schema: &Schema,
1885    grammar: &Grammar,
1886    vertex_id: &panproto_gat::Name,
1887    cursor: &ChildCursor<'_>,
1888    alternatives: &'a [Production],
1889) -> Option<&'a Production> {
1890    // Discriminator-driven dispatch (highest priority): when the
1891    // walker recorded a `chose-alt-fingerprint` constraint at parse
1892    // time, dispatch directly against that. This is the categorical
1893    // discriminator: it survives stripping of byte-position
1894    // constraints (so by-construction round-trips work) and is the
1895    // explicit witness of which CHOICE alternative the parser took.
1896    //
1897    // Falls back to the live `interstitial-*` substring blob when no
1898    // fingerprint is present (e.g. instances built by callers that
1899    // bypass the AstWalker). Both blobs are scored by the longest
1900    // STRING-literal token in an alternative that matches; the
1901    // length tiebreak prefers `&&` over `&`, `==` over `=`, etc.
1902    let constraint_blob = schema
1903        .constraints
1904        .get(vertex_id)
1905        .map(|cs| {
1906            let fingerprint: Option<&str> = cs
1907                .iter()
1908                .find(|c| c.sort.as_ref() == "chose-alt-fingerprint")
1909                .map(|c| c.value.as_str());
1910            if let Some(fp) = fingerprint {
1911                fp.to_owned()
1912            } else {
1913                cs.iter()
1914                    .filter(|c| {
1915                        let s = c.sort.as_ref();
1916                        s.starts_with("interstitial-") && !s.ends_with("-start-byte")
1917                    })
1918                    .map(|c| c.value.as_str())
1919                    .collect::<Vec<&str>>()
1920                    .join(" ")
1921            }
1922        })
1923        .unwrap_or_default();
1924    let child_kinds: Vec<&str> = schema
1925        .constraints
1926        .get(vertex_id)
1927        .and_then(|cs| {
1928            cs.iter()
1929                .find(|c| c.sort.as_ref() == "chose-alt-child-kinds")
1930                .map(|c| c.value.split_whitespace().collect())
1931        })
1932        .unwrap_or_default();
1933    // Cursor-exhaustion BLANK-preference: when all cursor edges have
1934    // been consumed AND `BLANK` is one of the alternatives, the only
1935    // alt that won't introduce a non-existent child is `BLANK`.
1936    //
1937    // This gate fires before the literal-blob discriminator because
1938    // the fingerprint is shared across every CHOICE position in the
1939    // vertex's rule body: a vertex like `sample_step` that ends in
1940    // `..., REPEAT(SEQ(",", arg)), CHOICE(",", BLANK)` records all of
1941    // its `","` interstitials in a single blob, so the literal-score
1942    // matcher would otherwise prefer `","` for the trailing CHOICE
1943    // even when the source had no trailing comma. By the time the
1944    // emitter reaches the trailing CHOICE, the REPEAT has consumed
1945    // every arg edge in cursor order; the residual unconsumed multiset
1946    // is empty; and the categorical reading of a CHOICE-with-BLANK at
1947    // a position with no remaining children is the no-op alternative.
1948    let any_unconsumed = cursor
1949        .edges
1950        .iter()
1951        .enumerate()
1952        .any(|(i, _)| !cursor.consumed[i]);
1953    let blank_present = alternatives.iter().any(|a| matches!(a, Production::Blank));
1954    if !any_unconsumed && blank_present {
1955        return alternatives.iter().find(|a| matches!(a, Production::Blank));
1956    }
1957    if !any_unconsumed && !blank_present {
1958        let mut visited = std::collections::HashSet::new();
1959        let mut yield_cache = grammar.yield_sets.clone();
1960        for alt in alternatives {
1961            let ys = yield_of_production(grammar, alt, &mut visited, &mut yield_cache);
1962            if ys.contains("") {
1963                return Some(alt);
1964            }
1965            visited.clear();
1966        }
1967    }
1968
1969    if !constraint_blob.is_empty() {
1970        // Primary score: literal-token match length. This dominates
1971        // alt selection so existing language tests that depend on
1972        // literal-only fingerprints keep working.
1973        // Secondary score (tiebreaker only): named-symbol kind match
1974        // count, read from the separate `chose-alt-child-kinds`
1975        // constraint (kept apart from the literal fingerprint so
1976        // identifiers like `:` in the kind list don't contaminate the
1977        // literal match). An alt that matches the recorded kinds is a
1978        // stronger witness than one whose only
1979        // overlap is literal punctuation.
1980        let mut best_literal: usize = 0;
1981        let mut best_symbols: usize = 0;
1982        let mut best_alt: Option<&Production> = None;
1983        let mut tied = false;
1984        for alt in alternatives {
1985            let strings = literal_strings(alt);
1986            if strings.is_empty() {
1987                continue;
1988            }
1989            let literal_score = strings
1990                .iter()
1991                .filter(|s| constraint_blob.contains(s.as_str()))
1992                .map(String::len)
1993                .sum::<usize>();
1994            if literal_score == 0 {
1995                continue;
1996            }
1997            // Symbol score is computed only as a tiebreaker among alts
1998            // whose literal-token coverage is the same; it never lifts
1999            // an alt above one with a strictly higher literal score.
2000            // Reads the `chose-alt-child-kinds` constraint (a separate
2001            // sequence the walker emits, kept apart from the literal
2002            // fingerprint to avoid cross-contamination).
2003            let symbol_score = if literal_score >= best_literal && !child_kinds.is_empty() {
2004                let symbols = referenced_symbols(alt);
2005                symbols
2006                    .iter()
2007                    .filter(|sym| {
2008                        let sym_str: &str = sym;
2009                        if child_kinds.contains(&sym_str) {
2010                            return true;
2011                        }
2012                        grammar.subtypes.get(sym_str).is_some_and(|sub_set| {
2013                            sub_set
2014                                .iter()
2015                                .any(|sub| child_kinds.contains(&sub.as_str()))
2016                        })
2017                    })
2018                    .count()
2019            } else {
2020                0
2021            };
2022            let better = literal_score > best_literal
2023                || (literal_score == best_literal && symbol_score > best_symbols);
2024            let same = literal_score == best_literal && symbol_score == best_symbols;
2025            if better {
2026                best_literal = literal_score;
2027                best_symbols = symbol_score;
2028                best_alt = Some(alt);
2029                tied = false;
2030            } else if same && best_alt.is_some() {
2031                tied = true;
2032            }
2033        }
2034        // Only commit to an alt when the fingerprint discriminates it
2035        // uniquely. A tie means the alts share the same literal token
2036        // set (e.g. JSON's `string = CHOICE { SEQ { '"', '"' }, SEQ {
2037        // '"', _string_content, '"' } }` — both alts contain just the
2038        // two `"` tokens). In that case fall through to cursor-based
2039        // dispatch, which uses the actual edge structure.
2040        if let Some(alt) = best_alt {
2041            if !tied {
2042                return Some(alt);
2043            }
2044        }
2045    }
2046
2047    // Cursor-driven dispatch via Yield-set preimage.
2048    //
2049    // For a CHOICE C = A1 | ... | An, Yield(Ai) is the set of vertex
2050    // kinds that can appear as the first named child when Ai is taken
2051    // (see `yield_of_production`). Given the first unconsumed cursor
2052    // edge with target kind K, select the first Ai (grammar order)
2053    // where K ∈ Yield(Ai). This is deterministic: grammar order is
2054    // the tiebreak, matching tree-sitter's own disambiguation.
2055    let first_unconsumed_kind: Option<&str> = cursor
2056        .edges
2057        .iter()
2058        .enumerate()
2059        .find(|(i, _)| !cursor.consumed[*i])
2060        .and_then(|(_, edge)| schema.vertices.get(&edge.tgt).map(|v| v.kind.as_ref()));
2061    if let Some(target_kind) = first_unconsumed_kind {
2062        // The subtype closure `subtypes[target_kind]` contains every
2063        // symbol name S such that a vertex of kind `target_kind` can
2064        // appear where the grammar says `SYMBOL S`. For a CHOICE
2065        // C = A1 | ... | An, the correct alternative is the one whose
2066        // top-level symbol is in `subtypes[target_kind]` (the target
2067        // kind IS a subtype of that symbol, so the symbol's rule body
2068        // dispatches to the target kind at parse time). This is an
2069        // O(1) set-membership check per alternative — no recursive
2070        // Yield computation needed.
2071        //
2072        // Preference order:
2073        //   1. Direct name match (target_kind == symbol name)
2074        //   2. Subtype match (symbol name ∈ subtypes[target_kind])
2075        //   3. Yield-set match (target_kind ∈ Yield(alt)) as fallback
2076        //      for non-SYMBOL alternatives (ALIAS, SEQ, etc.)
2077        let target_supers = grammar.subtypes.get(target_kind);
2078
2079        // Indented-form preference: when multiple alternatives match
2080        // the target kind (e.g. Python _suite where all three alts
2081        // produce `block`), prefer the alternative containing an
2082        // `_indent` SYMBOL. Check this BEFORE the standard passes
2083        // since they would pick the first match in grammar order.
2084        {
2085            let mut match_count = 0usize;
2086            let mut indent_alt_idx: Option<usize> = None;
2087            let mut visited = std::collections::HashSet::new();
2088            let mut yield_cache = grammar.yield_sets.clone();
2089            for (i, alt) in alternatives.iter().enumerate() {
2090                let ys = yield_of_production(grammar, alt, &mut visited, &mut yield_cache);
2091                if ys.contains(target_kind) {
2092                    match_count += 1;
2093                    if indent_alt_idx.is_none()
2094                        && referenced_symbols(alt)
2095                            .iter()
2096                            .any(|s| *s == "_indent" || s.ends_with("_indent"))
2097                    {
2098                        indent_alt_idx = Some(i);
2099                    }
2100                }
2101                visited.clear();
2102            }
2103            if match_count > 1 {
2104                if let Some(idx) = indent_alt_idx {
2105                    return Some(&alternatives[idx]);
2106                }
2107            }
2108        }
2109
2110        // Pass 1: direct name match
2111        for alt in alternatives {
2112            if let Production::Symbol { name } = alt {
2113                if name.as_str() == target_kind {
2114                    return Some(alt);
2115                }
2116            }
2117            if let Production::Alias {
2118                named: true, value, ..
2119            } = alt
2120            {
2121                if value.as_str() == target_kind {
2122                    return Some(alt);
2123                }
2124            }
2125        }
2126
2127        // Pass 2: subtype match (the target kind's supertype set
2128        // tells us which SYMBOL names it satisfies)
2129        if let Some(supers) = target_supers {
2130            for alt in alternatives {
2131                if let Production::Symbol { name } = alt {
2132                    if supers.contains(name.as_str()) {
2133                        return Some(alt);
2134                    }
2135                }
2136                if let Production::Alias {
2137                    named: true, value, ..
2138                } = alt
2139                {
2140                    if supers.contains(value.as_str()) {
2141                        return Some(alt);
2142                    }
2143                }
2144            }
2145        }
2146
2147        // Pass 3: Yield-set fallback for alternatives that are not
2148        // plain SYMBOLs or named ALIASes (e.g. SEQ, PREC wrappers
2149        // around SYMBOLs that the above passes don't unwrap).
2150        let mut visited = std::collections::HashSet::new();
2151        let mut yield_cache = grammar.yield_sets.clone();
2152        for alt in alternatives {
2153            let ys = yield_of_production(grammar, alt, &mut visited, &mut yield_cache);
2154            if ys.contains(target_kind) {
2155                return Some(alt);
2156            }
2157            visited.clear();
2158        }
2159    }
2160
2161    // FIELD dispatch: pick an alternative whose FIELD name matches an
2162    // unconsumed edge kind.
2163    let edge_kinds: Vec<&str> = cursor
2164        .edges
2165        .iter()
2166        .enumerate()
2167        .filter(|(i, _)| !cursor.consumed[*i])
2168        .map(|(_, e)| e.kind.as_ref())
2169        .collect();
2170    for alt in alternatives {
2171        if has_field_in(alt, &edge_kinds) {
2172            return Some(alt);
2173        }
2174    }
2175
2176    // No dispatch tier matched. The final selection follows the
2177    // categorical semantics of CHOICE-with-BLANK: BLANK represents ε
2178    // (produce nothing at this position). It is correct if and only
2179    // if no child remains to consume at this cursor position.
2180    //
2181    // When unconsumed non-extra children remain, selecting BLANK
2182    // would silently drop them. Select the first non-BLANK
2183    // alternative instead so the production walk can attempt to
2184    // consume them (the grammar rule may reference a symbol name
2185    // that doesn't exactly match the parse output's child kind,
2186    // e.g. Julia's macrocall_expression receives `argument_list`
2187    // children when grammar.json only references
2188    // `macro_argument_list`).
2189    let _ = (schema, vertex_id);
2190    if alternatives.iter().any(|a| matches!(a, Production::Blank)) {
2191        return alternatives.iter().find(|a| matches!(a, Production::Blank));
2192    }
2193    alternatives
2194        .iter()
2195        .find(|alt| !matches!(alt, Production::Blank))
2196}
2197
2198/// Collect every literal STRING token directly inside `production`
2199/// (without descending into SYMBOLs / hidden rules). Used to score
2200/// CHOICE alternatives against the parent vertex's interstitials so
2201/// the right operator / keyword form is picked when the schema
2202/// preserves interstitial fragments from a prior parse.
2203fn literal_strings(production: &Production) -> Vec<String> {
2204    let mut out = Vec::new();
2205    fn walk(p: &Production, out: &mut Vec<String>) {
2206        match p {
2207            Production::String { value } if !value.is_empty() => {
2208                out.push(value.clone());
2209            }
2210            Production::Choice { members } | Production::Seq { members } => {
2211                for m in members {
2212                    walk(m, out);
2213                }
2214            }
2215            Production::Repeat { content }
2216            | Production::Repeat1 { content }
2217            | Production::Optional { content }
2218            | Production::Field { content, .. }
2219            | Production::Alias { content, .. }
2220            | Production::Token { content }
2221            | Production::ImmediateToken { content }
2222            | Production::Prec { content, .. }
2223            | Production::PrecLeft { content, .. }
2224            | Production::PrecRight { content, .. }
2225            | Production::PrecDynamic { content, .. }
2226            | Production::Reserved { content, .. } => walk(content, out),
2227            _ => {}
2228        }
2229    }
2230    walk(production, &mut out);
2231    out
2232}
2233
2234/// Collect every SYMBOL name reachable from `production` without
2235/// crossing into nested rules. Used by `pick_choice_with_cursor` to
2236/// rank alternatives by "any SYMBOL inside this alt matches something
2237/// on the cursor", instead of just the first SYMBOL: a leading
2238/// optional like `attribute_item` then `parameter` is otherwise
2239/// rejected when only the parameter children are present.
2240fn referenced_symbols(production: &Production) -> Vec<&str> {
2241    let mut out = Vec::new();
2242    fn walk<'a>(p: &'a Production, out: &mut Vec<&'a str>) {
2243        match p {
2244            Production::Symbol { name } => out.push(name.as_str()),
2245            Production::Choice { members } | Production::Seq { members } => {
2246                for m in members {
2247                    walk(m, out);
2248                }
2249            }
2250            Production::Alias {
2251                content,
2252                named,
2253                value,
2254            } => {
2255                // A named ALIAS produces a child vertex whose kind is
2256                // the alias `value` (e.g. `ALIAS { content: STRING "=",
2257                // value: "punctuation", named: true }` introduces a
2258                // `punctuation` child). For cursor-driven dispatch to
2259                // recognise alts that emit such children, yield the
2260                // alias value as a referenced symbol. Anonymous aliases
2261                // do not introduce a named node and only need their
2262                // inner content's symbols.
2263                if *named && !value.is_empty() {
2264                    out.push(value.as_str());
2265                }
2266                walk(content, out);
2267            }
2268            Production::Repeat { content }
2269            | Production::Repeat1 { content }
2270            | Production::Optional { content }
2271            | Production::Field { content, .. }
2272            | Production::Token { content }
2273            | Production::ImmediateToken { content }
2274            | Production::Prec { content, .. }
2275            | Production::PrecLeft { content, .. }
2276            | Production::PrecRight { content, .. }
2277            | Production::PrecDynamic { content, .. }
2278            | Production::Reserved { content, .. } => walk(content, out),
2279            _ => {}
2280        }
2281    }
2282    walk(production, &mut out);
2283    out
2284}
2285
2286#[cfg(test)]
2287fn first_symbol(production: &Production) -> Option<&str> {
2288    match production {
2289        Production::Symbol { name } => Some(name),
2290        Production::Seq { members } => members.iter().find_map(first_symbol),
2291        Production::Choice { members } => members.iter().find_map(first_symbol),
2292        Production::Repeat { content }
2293        | Production::Repeat1 { content }
2294        | Production::Optional { content }
2295        | Production::Field { content, .. }
2296        | Production::Alias { content, .. }
2297        | Production::Token { content }
2298        | Production::ImmediateToken { content }
2299        | Production::Prec { content, .. }
2300        | Production::PrecLeft { content, .. }
2301        | Production::PrecRight { content, .. }
2302        | Production::PrecDynamic { content, .. }
2303        | Production::Reserved { content, .. } => first_symbol(content),
2304        _ => None,
2305    }
2306}
2307
2308fn has_field_in(production: &Production, edge_kinds: &[&str]) -> bool {
2309    match production {
2310        Production::Field { name, .. } => edge_kinds.contains(&name.as_str()),
2311        Production::Seq { members } | Production::Choice { members } => {
2312            members.iter().any(|m| has_field_in(m, edge_kinds))
2313        }
2314        Production::Repeat { content }
2315        | Production::Repeat1 { content }
2316        | Production::Optional { content }
2317        | Production::Alias { content, .. }
2318        | Production::Token { content }
2319        | Production::ImmediateToken { content }
2320        | Production::Prec { content, .. }
2321        | Production::PrecLeft { content, .. }
2322        | Production::PrecRight { content, .. }
2323        | Production::PrecDynamic { content, .. }
2324        | Production::Reserved { content, .. } => has_field_in(content, edge_kinds),
2325        _ => false,
2326    }
2327}
2328
2329fn has_relevant_constraint(
2330    production: &Production,
2331    schema: &Schema,
2332    vertex_id: &panproto_gat::Name,
2333) -> bool {
2334    let constraints = match schema.constraints.get(vertex_id) {
2335        Some(c) => c,
2336        None => return false,
2337    };
2338    fn walk(production: &Production, constraints: &[panproto_schema::Constraint]) -> bool {
2339        match production {
2340            Production::String { value } => constraints
2341                .iter()
2342                .any(|c| c.value == *value || c.sort.as_ref() == value),
2343            Production::Field { name, content } => {
2344                constraints.iter().any(|c| c.sort.as_ref() == name) || walk(content, constraints)
2345            }
2346            Production::Seq { members } | Production::Choice { members } => {
2347                members.iter().any(|m| walk(m, constraints))
2348            }
2349            Production::Repeat { content }
2350            | Production::Repeat1 { content }
2351            | Production::Optional { content }
2352            | Production::Alias { content, .. }
2353            | Production::Token { content }
2354            | Production::ImmediateToken { content }
2355            | Production::Prec { content, .. }
2356            | Production::PrecLeft { content, .. }
2357            | Production::PrecRight { content, .. }
2358            | Production::PrecDynamic { content, .. }
2359            | Production::Reserved { content, .. } => walk(content, constraints),
2360            _ => false,
2361        }
2362    }
2363    walk(production, constraints)
2364}
2365
2366fn children_for<'a>(schema: &'a Schema, vertex_id: &panproto_gat::Name) -> Vec<&'a Edge> {
2367    // Walk `outgoing` (insertion-ordered by SchemaBuilder via SmallVec
2368    // append) rather than the unordered `edges` HashMap so abstract
2369    // schemas under REPEAT(CHOICE(...)) preserve the order their edges
2370    // were inserted in. The previous implementation walked the HashMap
2371    // and sorted lexicographically by (kind, target id), which fused
2372    // interleaved children of the same kind into runs (e.g. a sequence
2373    // [symbol, punct, int, symbol, punct, int] became [symbol, symbol,
2374    // punct, punct, int, int] after the lex sort).
2375    let Some(edges) = schema.outgoing.get(vertex_id) else {
2376        return Vec::new();
2377    };
2378
2379    // Look up the canonical Edge reference (the key in `schema.edges`)
2380    // for each entry in `outgoing`. Falls back to the SmallVec entry if
2381    // the canonical key is missing, which would indicate index drift.
2382    let mut indexed: Vec<(usize, u32, &Edge)> = edges
2383        .iter()
2384        .enumerate()
2385        .map(|(i, e)| {
2386            let canonical = schema.edges.get_key_value(e).map_or(e, |(k, _)| k);
2387            let pos = schema.orderings.get(canonical).copied().unwrap_or(u32::MAX);
2388            (i, pos, canonical)
2389        })
2390        .collect();
2391
2392    // Stable sort by (explicit-ordering, insertion-index). Edges with
2393    // an explicit `orderings` entry come first in their declared order;
2394    // the remainder fall through in insertion order.
2395    indexed.sort_by_key(|(i, pos, _)| (*pos, *i));
2396    indexed.into_iter().map(|(_, _, e)| e).collect()
2397}
2398
2399fn vertex_id_kind<'a>(schema: &'a Schema, vertex_id: &panproto_gat::Name) -> Option<&'a str> {
2400    schema.vertices.get(vertex_id).map(|v| v.kind.as_ref())
2401}
2402
2403fn literal_value<'a>(schema: &'a Schema, vertex_id: &panproto_gat::Name) -> Option<&'a str> {
2404    schema
2405        .constraints
2406        .get(vertex_id)?
2407        .iter()
2408        .find(|c| c.sort.as_ref() == "literal-value")
2409        .map(|c| c.value.as_str())
2410}
2411
2412/// True iff `pattern` matches a (possibly optional / repeated) sequence
2413/// of carriage-return and newline characters only. Examples: `\r?\n`,
2414/// `\n`, `\r\n`, `\n+`, `\r?\n+`. Distinguishes structural newline
2415/// terminals from generic whitespace and from other patterns that
2416/// happen to contain a newline escape inside a larger class.
2417fn is_newline_like_pattern(pattern: &str) -> bool {
2418    if pattern.is_empty() {
2419        return false;
2420    }
2421    let mut chars = pattern.chars();
2422    let mut saw_newline_atom = false;
2423    while let Some(c) = chars.next() {
2424        match c {
2425            '\\' => match chars.next() {
2426                Some('n' | 'r') => saw_newline_atom = true,
2427                _ => return false,
2428            },
2429            '?' | '*' | '+' => {} // quantifiers on the previous atom
2430            _ => return false,
2431        }
2432    }
2433    saw_newline_atom
2434}
2435
2436/// True iff `pattern` matches a (possibly quantified) run of generic
2437/// whitespace characters: `\s+`, `[ \t]+`, ` +`, `\s*`. Such patterns
2438/// describe interstitial spacing rather than syntactic content, so the
2439/// pretty emitter can drop them and let the layout pass insert the
2440/// configured separator.
2441fn is_whitespace_only_pattern(pattern: &str) -> bool {
2442    if pattern.is_empty() {
2443        return false;
2444    }
2445    // Strip an outer quantifier suffix.
2446    let trimmed = pattern.trim_end_matches(['?', '*', '+']);
2447    if trimmed.is_empty() {
2448        return false;
2449    }
2450    // Bare `\s` / ` ` / `\t`.
2451    if matches!(trimmed, "\\s" | " " | "\\t") {
2452        return true;
2453    }
2454    // Character class containing only whitespace atoms.
2455    if let Some(inner) = trimmed.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
2456        let mut chars = inner.chars();
2457        let mut saw_atom = false;
2458        while let Some(c) = chars.next() {
2459            match c {
2460                '\\' => match chars.next() {
2461                    Some('s' | 't' | 'r' | 'n') => saw_atom = true,
2462                    _ => return false,
2463                },
2464                ' ' | '\t' => saw_atom = true,
2465                _ => return false,
2466            }
2467        }
2468        return saw_atom;
2469    }
2470    false
2471}
2472
2473fn placeholder_for_pattern(pattern: &str) -> String {
2474    // Heuristic placeholder for unconstrained PATTERN terminals.
2475    //
2476    // First handle the "the regex IS a literal escape" cases that
2477    // tree-sitter grammars use as separators (`\n`, `\r\n`, `;`,
2478    // etc.); emitting the matching character is always preferable
2479    // to a `_x` identifier-like placeholder when the surrounding
2480    // grammar expects a separator.
2481    let simple_lit = decode_simple_pattern_literal(pattern);
2482    if let Some(lit) = simple_lit {
2483        return lit;
2484    }
2485
2486    if pattern.contains("[0-9]") || pattern.contains("\\d") {
2487        "0".into()
2488    } else if pattern.contains("[a-zA-Z_]") || pattern.contains("\\w") {
2489        "_x".into()
2490    } else if pattern.contains('"') || pattern.contains('\'') {
2491        "\"\"".into()
2492    } else {
2493        "_".into()
2494    }
2495}
2496
2497/// Decode a tree-sitter PATTERN whose regex is a simple literal
2498/// (newline, semicolon, comma, etc.) to the byte sequence it matches.
2499/// Returns `None` for patterns with character classes, alternations,
2500/// or quantifiers; the caller falls back to the heuristic placeholder.
2501fn decode_simple_pattern_literal(pattern: &str) -> Option<String> {
2502    // Skip patterns containing regex metachars that would broaden the
2503    // match beyond a single literal byte sequence.
2504    if pattern
2505        .chars()
2506        .any(|c| matches!(c, '[' | ']' | '(' | ')' | '*' | '+' | '?' | '|' | '{' | '}'))
2507    {
2508        return None;
2509    }
2510    let mut out = String::new();
2511    let mut chars = pattern.chars();
2512    while let Some(c) = chars.next() {
2513        if c == '\\' {
2514            match chars.next() {
2515                Some('n') => out.push('\n'),
2516                Some('r') => out.push('\r'),
2517                Some('t') => out.push('\t'),
2518                Some('\\') => out.push('\\'),
2519                Some('/') => out.push('/'),
2520                Some(other) => out.push(other),
2521                None => return None,
2522            }
2523        } else {
2524            out.push(c);
2525        }
2526    }
2527    Some(out)
2528}
2529
2530// ═══════════════════════════════════════════════════════════════════
2531// Token list output with Spacing algebra
2532// ═══════════════════════════════════════════════════════════════════
2533//
2534// Emit produces a free monoid over `Token`. Layout (spaces, newlines,
2535// indentation) is a homomorphism `Vec<Token> -> Vec<u8>` parameterised
2536// by `FormatPolicy`. Separating the structural output from the layout
2537// decision means each phase has one job: emit walks the grammar and
2538// pushes tokens; layout is a single fold, locally driven by adjacent
2539// pairs and a depth counter. Snapshot/restore is just `tokens.len()`.
2540
2541#[derive(Clone)]
2542enum Token {
2543    /// A user-visible terminal contributed by the grammar.
2544    Lit(String),
2545    /// `indent_open` marker emitted when a `Lit` matched the policy's
2546    /// open list. Carried as a separate token so layout can decide to
2547    /// break + indent without re-scanning.
2548    IndentOpen,
2549    /// `indent_close` marker emitted before a closer-`Lit`.
2550    IndentClose,
2551    /// "Break a line here if not already at line start" — used after
2552    /// statements/declarations and after open braces.
2553    LineBreak,
2554    /// Suppress the next inter-Lit separator. Pushed by the REPEAT
2555    /// walker when an iteration's "separator slot" (a CHOICE-with-BLANK
2556    /// or OPTIONAL at SEQ position 0) emitted zero content tokens, so
2557    /// the categorical reading is "no source-level separator existed
2558    /// between these two sibling iterations of the body".
2559    NoSpace,
2560}
2561
2562struct Output<'a> {
2563    tokens: Vec<Token>,
2564    policy: &'a FormatPolicy,
2565    suppress_brace_indent: bool,
2566}
2567
2568#[derive(Clone)]
2569struct OutputSnapshot {
2570    tokens_len: usize,
2571}
2572
2573impl<'a> Output<'a> {
2574    fn new(policy: &'a FormatPolicy) -> Self {
2575        Self {
2576            tokens: Vec::new(),
2577            policy,
2578            suppress_brace_indent: false,
2579        }
2580    }
2581
2582    fn token(&mut self, value: &str) {
2583        if value.is_empty() {
2584            return;
2585        }
2586
2587        // A grammar STRING whose value is a newline (e.g. abc's `_NL = "\n"`
2588        // or any rule that uses `"\n"` as a structural line terminator)
2589        // must route through the layout's `LineBreak` channel. Emitting it
2590        // as a `Lit` leaves the newline character in the byte stream but
2591        // also makes `needs_space_between` insert the configured separator
2592        // between the newline and the following token, producing leading
2593        // spaces on every line after the first.
2594        if value == "\n" || value == "\r\n" || value == "\r" {
2595            self.tokens.push(Token::LineBreak);
2596            return;
2597        }
2598
2599        // A captured literal value (typically a vertex's `literal-value`
2600        // constraint covering the full source span of a terminal-like
2601        // rule, e.g. abc's `reference_number_line` matching `"X:1\n"`)
2602        // may contain trailing newlines. Splitting the trailing newlines
2603        // off as a `LineBreak` lets the layout pass treat the next Lit
2604        // as starting a new line; otherwise the next Lit pair would
2605        // trigger `needs_space_between` against the embedded `\n` and
2606        // insert the policy separator at column 0 of the new line.
2607        let trimmed = value.trim_end_matches(['\n', '\r']);
2608        let trailing_newlines = value.len() - trimmed.len();
2609        if trailing_newlines > 0 && !trimmed.is_empty() {
2610            if !self.suppress_brace_indent && self.policy.indent_close.iter().any(|t| t == trimmed)
2611            {
2612                self.tokens.push(Token::IndentClose);
2613            }
2614            self.tokens.push(Token::Lit(trimmed.to_owned()));
2615            if !self.suppress_brace_indent && self.policy.indent_open.iter().any(|t| t == trimmed) {
2616                self.tokens.push(Token::IndentOpen);
2617            } else if self.policy.line_break_after.iter().any(|t| t == trimmed) {
2618                // already emitting a LineBreak below for the trailing \n
2619            }
2620            self.tokens.push(Token::LineBreak);
2621            return;
2622        }
2623
2624        if !self.suppress_brace_indent && self.policy.indent_close.iter().any(|t| t == value) {
2625            self.tokens.push(Token::IndentClose);
2626        }
2627
2628        self.tokens.push(Token::Lit(value.to_owned()));
2629
2630        if !self.suppress_brace_indent && self.policy.indent_open.iter().any(|t| t == value) {
2631            self.tokens.push(Token::IndentOpen);
2632            self.tokens.push(Token::LineBreak);
2633        } else if self.policy.line_break_after.iter().any(|t| t == value)
2634            && !(self.suppress_brace_indent && (value == "{" || value == "}"))
2635        {
2636            self.tokens.push(Token::LineBreak);
2637        }
2638    }
2639
2640    fn newline(&mut self) {
2641        self.tokens.push(Token::LineBreak);
2642    }
2643
2644    /// Open an indent scope: subsequent `LineBreak`s render at the
2645    /// new depth until a matching `indent_close` pops it. Used by the
2646    /// external-token fallback to render indent-based grammars'
2647    /// `_indent` scanner outputs.
2648    fn indent_open(&mut self) {
2649        self.tokens.push(Token::IndentOpen);
2650        self.tokens.push(Token::LineBreak);
2651    }
2652
2653    /// Close one indent scope opened by `indent_open`.
2654    fn indent_close(&mut self) {
2655        self.tokens.push(Token::IndentClose);
2656    }
2657
2658    fn snapshot(&self) -> OutputSnapshot {
2659        OutputSnapshot {
2660            tokens_len: self.tokens.len(),
2661        }
2662    }
2663
2664    fn restore(&mut self, snap: OutputSnapshot) {
2665        self.tokens.truncate(snap.tokens_len);
2666    }
2667
2668    /// True iff at least one `Token::Lit` was pushed since `snap`.
2669    /// Control-only emissions (`LineBreak`, `IndentOpen` / `IndentClose`,
2670    /// `NoSpace`) do not count as content. Used by the REPEAT walker
2671    /// to detect that a "separator slot" CHOICE picked its BLANK
2672    /// alternative, so the next iteration's content can be marked
2673    /// tight against the previous iteration's content.
2674    fn lit_emitted_since(&self, snap: OutputSnapshot) -> bool {
2675        self.tokens[snap.tokens_len..]
2676            .iter()
2677            .any(|t| matches!(t, Token::Lit(_)))
2678    }
2679
2680    /// Push a marker that suppresses the next inter-Lit separator the
2681    /// layout pass would otherwise insert. Used to encode "no source-
2682    /// level separator was emitted between these two Lits" without
2683    /// having to make per-grammar adjacency decisions in the layout.
2684    fn no_space(&mut self) {
2685        self.tokens.push(Token::NoSpace);
2686    }
2687
2688    fn finish(self) -> Vec<u8> {
2689        layout(&self.tokens, self.policy)
2690    }
2691}
2692
2693/// Fold a token list into bytes. The algebra:
2694/// * adjacent `Lit`s get a single space iff `needs_space_between(a, b)`,
2695/// * `IndentOpen` / `IndentClose` adjust a depth counter,
2696/// * `LineBreak` writes `\n` if not already at line start, then the
2697///   next `Lit` writes `indent * indent_width` spaces of indent.
2698fn layout(tokens: &[Token], policy: &FormatPolicy) -> Vec<u8> {
2699    let mut bytes = Vec::new();
2700    let mut indent: usize = 0;
2701    let mut at_line_start = true;
2702    let mut last_lit: Option<&str> = None;
2703    // True iff, at the moment `last_lit` was emitted, the cursor was at a
2704    // position where the grammar expects an operand: start of stream / line,
2705    // just after an open paren / bracket / brace, just after a separator like
2706    // `,` or `;`, or just after a binary / assignment operator. Used by
2707    // `needs_space_between` to recognise `last_lit` as a tight unary prefix
2708    // (`f(-1.0)`) rather than a spaced binary operator (`a - b`).
2709    let mut last_was_in_operand_position = true;
2710    let mut expecting_operand = true;
2711    // Set when a `Token::NoSpace` marker is seen; cleared when the next
2712    // Lit consumes it. While set, suppress the policy separator that
2713    // would otherwise be inserted before the next Lit.
2714    let mut suppress_next_separator = false;
2715    let newline = policy.newline.as_bytes();
2716    let separator = policy.separator.as_bytes();
2717
2718    for tok in tokens {
2719        match tok {
2720            Token::IndentOpen => indent += 1,
2721            Token::IndentClose => {
2722                indent = indent.saturating_sub(1);
2723                if !at_line_start {
2724                    bytes.extend_from_slice(newline);
2725                    at_line_start = true;
2726                    expecting_operand = true;
2727                }
2728            }
2729            Token::LineBreak => {
2730                if !at_line_start {
2731                    bytes.extend_from_slice(newline);
2732                    at_line_start = true;
2733                    expecting_operand = true;
2734                }
2735            }
2736            Token::NoSpace => {
2737                suppress_next_separator = true;
2738            }
2739            Token::Lit(value) => {
2740                if at_line_start {
2741                    bytes.extend(std::iter::repeat_n(b' ', indent * policy.indent_width));
2742                } else if let Some(prev) = last_lit {
2743                    if !suppress_next_separator
2744                        && needs_space_between(prev, value, last_was_in_operand_position)
2745                    {
2746                        bytes.extend_from_slice(separator);
2747                    }
2748                }
2749                suppress_next_separator = false;
2750                bytes.extend_from_slice(value.as_bytes());
2751                at_line_start = false;
2752                last_was_in_operand_position = expecting_operand;
2753                expecting_operand = leaves_operand_position(value);
2754                last_lit = Some(value.as_str());
2755            }
2756        }
2757    }
2758
2759    if !at_line_start {
2760        bytes.extend_from_slice(newline);
2761    }
2762    bytes
2763}
2764
2765/// True iff emitting `tok` leaves the cursor in a position where the
2766/// grammar expects an operand next. Operand-introducing tokens are open
2767/// punctuation, separators, and operator-like strings; operand-terminating
2768/// tokens are identifiers, literals, and closing punctuation.
2769fn leaves_operand_position(tok: &str) -> bool {
2770    if tok.is_empty() {
2771        return true;
2772    }
2773    if is_punct_open(tok) {
2774        return true;
2775    }
2776    if matches!(tok, "," | ";") {
2777        return true;
2778    }
2779    if is_punct_close(tok) {
2780        return false;
2781    }
2782    if first_is_alnum_or_underscore(tok) || last_ends_with_alnum(tok) {
2783        return false;
2784    }
2785    // Pure punctuation/operator runs (`=`, `+`, `-`, `<=`, `>>`, …) leave
2786    // the cursor expecting another operand.
2787    true
2788}
2789
2790fn needs_space_between(last: &str, next: &str, expecting_operand: bool) -> bool {
2791    if last.is_empty() || next.is_empty() {
2792        return false;
2793    }
2794    if is_punct_open(last) || is_punct_open(next) {
2795        return false;
2796    }
2797    if is_punct_close(next) {
2798        return false;
2799    }
2800    if is_punct_close(last) && is_punct_punctuation(next) {
2801        return false;
2802    }
2803    if last == "." || next == "." {
2804        return false;
2805    }
2806    // Tight unary prefix: `last` is a sign/logical-not operator emitted
2807    // where the grammar expected an operand, so it glues to `next`.
2808    // `expecting_operand` here means: just before `last` was emitted,
2809    // the cursor expected an operand, which makes `last` a unary prefix.
2810    // Examples: `f(-1.0)`, `[ -2, 3 ]`, `return -x`, `a = !flag`.
2811    if expecting_operand && is_unary_prefix_operator(last) && first_is_operand_start(next) {
2812        return false;
2813    }
2814    if last_is_word_like(last) && first_is_word_like(next) {
2815        return true;
2816    }
2817    if last_ends_with_alnum(last) && first_is_alnum_or_underscore(next) {
2818        return true;
2819    }
2820    // Adjacent operator runs: keep them apart so the lexer doesn't glue
2821    // `>` and `=` into `>=` unintentionally.
2822    true
2823}
2824
2825fn is_unary_prefix_operator(s: &str) -> bool {
2826    matches!(s, "-" | "+" | "!" | "~")
2827}
2828
2829fn first_is_operand_start(s: &str) -> bool {
2830    s.chars()
2831        .next()
2832        .map(|c| c.is_alphanumeric() || c == '_' || c == '.' || c == '(')
2833        .unwrap_or(false)
2834}
2835
2836fn is_punct_open(s: &str) -> bool {
2837    matches!(s, "(" | "[" | "{" | "\"" | "'" | "`" | "@" | "#")
2838        || s.ends_with('{')
2839        || s.ends_with('(')
2840        || s.ends_with('[')
2841}
2842
2843fn is_punct_close(s: &str) -> bool {
2844    matches!(s, ")" | "]" | "}" | "," | ";" | ":" | "\"" | "'" | "`")
2845}
2846
2847fn is_punct_punctuation(s: &str) -> bool {
2848    matches!(s, "," | ";" | ":" | "." | ")" | "]" | "}")
2849}
2850
2851fn last_is_word_like(s: &str) -> bool {
2852    s.chars()
2853        .next_back()
2854        .map(|c| c.is_alphanumeric() || c == '_')
2855        .unwrap_or(false)
2856}
2857
2858fn first_is_word_like(s: &str) -> bool {
2859    s.chars()
2860        .next()
2861        .map(|c| c.is_alphanumeric() || c == '_')
2862        .unwrap_or(false)
2863}
2864
2865fn last_ends_with_alnum(s: &str) -> bool {
2866    s.chars()
2867        .next_back()
2868        .map(char::is_alphanumeric)
2869        .unwrap_or(false)
2870}
2871
2872fn first_is_alnum_or_underscore(s: &str) -> bool {
2873    s.chars()
2874        .next()
2875        .map(|c| c.is_alphanumeric() || c == '_')
2876        .unwrap_or(false)
2877}
2878
2879#[cfg(test)]
2880#[allow(clippy::unwrap_used)]
2881mod tests {
2882    use super::*;
2883
2884    #[test]
2885    fn parses_simple_grammar_json() {
2886        let bytes = br#"{
2887            "name": "tiny",
2888            "rules": {
2889                "program": {
2890                    "type": "SEQ",
2891                    "members": [
2892                        {"type": "STRING", "value": "hello"},
2893                        {"type": "STRING", "value": ";"}
2894                    ]
2895                }
2896            }
2897        }"#;
2898        let g = Grammar::from_bytes("tiny", bytes).expect("valid tiny grammar");
2899        assert!(g.rules.contains_key("program"));
2900    }
2901
2902    #[test]
2903    fn output_emits_punctuation_without_leading_space() {
2904        let policy = FormatPolicy::default();
2905        let mut out = Output::new(&policy);
2906        out.token("foo");
2907        out.token("(");
2908        out.token(")");
2909        out.token(";");
2910        let bytes = out.finish();
2911        let s = std::str::from_utf8(&bytes).expect("ascii output");
2912        assert!(s.starts_with("foo();"), "got {s:?}");
2913    }
2914
2915    #[test]
2916    fn grammar_from_bytes_rejects_malformed_input() {
2917        let result = Grammar::from_bytes("malformed", b"not json");
2918        let err = result.expect_err("malformed bytes must yield Err");
2919        let msg = err.to_string();
2920        assert!(
2921            msg.contains("malformed"),
2922            "error message should name the protocol: {msg:?}"
2923        );
2924    }
2925
2926    #[test]
2927    fn output_indents_after_open_brace() {
2928        let policy = FormatPolicy::default();
2929        let mut out = Output::new(&policy);
2930        out.token("fn");
2931        out.token("foo");
2932        out.token("(");
2933        out.token(")");
2934        out.token("{");
2935        out.token("body");
2936        out.token("}");
2937        let bytes = out.finish();
2938        let s = std::str::from_utf8(&bytes).expect("ascii output");
2939        assert!(s.contains("{\n"), "newline after opening brace: {s:?}");
2940        assert!(s.contains("body"), "body inside block: {s:?}");
2941        assert!(s.ends_with("}\n"), "newline after closing brace: {s:?}");
2942    }
2943
2944    #[test]
2945    fn output_no_space_between_word_and_dot() {
2946        let policy = FormatPolicy::default();
2947        let mut out = Output::new(&policy);
2948        out.token("foo");
2949        out.token(".");
2950        out.token("bar");
2951        let bytes = out.finish();
2952        let s = std::str::from_utf8(&bytes).expect("ascii output");
2953        assert!(s.starts_with("foo.bar"), "no space around dot: {s:?}");
2954    }
2955
2956    #[test]
2957    fn output_snapshot_restore_truncates_bytes() {
2958        let policy = FormatPolicy::default();
2959        let mut out = Output::new(&policy);
2960        out.token("keep");
2961        let snap = out.snapshot();
2962        out.token("drop");
2963        out.token("more");
2964        out.restore(snap);
2965        out.token("after");
2966        let bytes = out.finish();
2967        let s = std::str::from_utf8(&bytes).expect("ascii output");
2968        assert!(s.contains("keep"), "kept token survives: {s:?}");
2969        assert!(s.contains("after"), "post-restore token visible: {s:?}");
2970        assert!(!s.contains("drop"), "rolled-back token removed: {s:?}");
2971        assert!(!s.contains("more"), "rolled-back token removed: {s:?}");
2972    }
2973
2974    #[test]
2975    fn child_cursor_take_field_consumes_once() {
2976        let edges_owned: Vec<Edge> = vec![Edge {
2977            src: panproto_gat::Name::from("p"),
2978            tgt: panproto_gat::Name::from("c"),
2979            kind: panproto_gat::Name::from("name"),
2980            name: None,
2981        }];
2982        let edges: Vec<&Edge> = edges_owned.iter().collect();
2983        let mut cursor = ChildCursor::new(&edges);
2984        let first = cursor.take_field("name");
2985        let second = cursor.take_field("name");
2986        assert!(first.is_some(), "first take returns the edge");
2987        assert!(
2988            second.is_none(),
2989            "second take returns None (already consumed)"
2990        );
2991    }
2992
2993    #[test]
2994    fn child_cursor_take_matching_predicate() {
2995        let edges_owned: Vec<Edge> = vec![
2996            Edge {
2997                src: "p".into(),
2998                tgt: "c1".into(),
2999                kind: "child_of".into(),
3000                name: None,
3001            },
3002            Edge {
3003                src: "p".into(),
3004                tgt: "c2".into(),
3005                kind: "key".into(),
3006                name: None,
3007            },
3008        ];
3009        let edges: Vec<&Edge> = edges_owned.iter().collect();
3010        let mut cursor = ChildCursor::new(&edges);
3011        assert!(cursor.has_matching(|e| e.kind.as_ref() == "key"));
3012        let taken = cursor.take_matching(|e| e.kind.as_ref() == "key");
3013        assert!(taken.is_some());
3014        assert!(
3015            !cursor.has_matching(|e| e.kind.as_ref() == "key"),
3016            "consumed edge no longer matches"
3017        );
3018        assert!(
3019            cursor.has_matching(|e| e.kind.as_ref() == "child_of"),
3020            "the other edge is still available"
3021        );
3022    }
3023
3024    #[test]
3025    fn kind_satisfies_symbol_direct_match() {
3026        let bytes = br#"{
3027            "name": "tiny",
3028            "rules": {
3029                "x": {"type": "STRING", "value": "x"}
3030            }
3031        }"#;
3032        let g = Grammar::from_bytes("tiny", bytes).expect("valid grammar");
3033        assert!(kind_satisfies_symbol(&g, Some("x"), "x"));
3034        assert!(!kind_satisfies_symbol(&g, Some("y"), "x"));
3035        assert!(!kind_satisfies_symbol(&g, None, "x"));
3036    }
3037
3038    #[test]
3039    fn kind_satisfies_symbol_through_hidden_rule() {
3040        let bytes = br#"{
3041            "name": "tiny",
3042            "rules": {
3043                "_value": {
3044                    "type": "CHOICE",
3045                    "members": [
3046                        {"type": "SYMBOL", "name": "object"},
3047                        {"type": "SYMBOL", "name": "number"}
3048                    ]
3049                },
3050                "object": {"type": "STRING", "value": "{}"},
3051                "number": {"type": "PATTERN", "value": "[0-9]+"}
3052            }
3053        }"#;
3054        let g = Grammar::from_bytes("tiny", bytes).expect("valid grammar");
3055        assert!(
3056            kind_satisfies_symbol(&g, Some("number"), "_value"),
3057            "number is reachable from _value via CHOICE"
3058        );
3059        assert!(
3060            kind_satisfies_symbol(&g, Some("object"), "_value"),
3061            "object is reachable from _value via CHOICE"
3062        );
3063        assert!(
3064            !kind_satisfies_symbol(&g, Some("string"), "_value"),
3065            "string is NOT among the alternatives"
3066        );
3067    }
3068
3069    #[test]
3070    fn first_symbol_skips_string_terminals() {
3071        let prod: Production = serde_json::from_str(
3072            r#"{
3073                "type": "SEQ",
3074                "members": [
3075                    {"type": "STRING", "value": "{"},
3076                    {"type": "SYMBOL", "name": "body"},
3077                    {"type": "STRING", "value": "}"}
3078                ]
3079            }"#,
3080        )
3081        .expect("valid SEQ");
3082        assert_eq!(first_symbol(&prod), Some("body"));
3083    }
3084
3085    #[test]
3086    fn placeholder_for_pattern_routes_by_regex_class() {
3087        assert_eq!(placeholder_for_pattern("[0-9]+"), "0");
3088        assert_eq!(placeholder_for_pattern("[a-zA-Z_]\\w*"), "_x");
3089        assert_eq!(placeholder_for_pattern("\"[^\"]*\""), "\"\"");
3090        assert_eq!(placeholder_for_pattern("\\d+\\.\\d+"), "0");
3091    }
3092
3093    #[test]
3094    fn format_policy_default_breaks_after_semicolon() {
3095        let policy = FormatPolicy::default();
3096        assert!(policy.line_break_after.iter().any(|t| t == ";"));
3097        assert!(policy.indent_open.iter().any(|t| t == "{"));
3098        assert!(policy.indent_close.iter().any(|t| t == "}"));
3099        assert_eq!(policy.indent_width, 2);
3100    }
3101
3102    #[test]
3103    fn placeholder_decodes_literal_pattern_separators() {
3104        // PATTERN regexes that match a single literal byte sequence
3105        // (newline, semicolon, comma) emit the bytes verbatim instead
3106        // of falling through to the `_` catch-all.
3107        assert_eq!(placeholder_for_pattern("\\n"), "\n");
3108        assert_eq!(placeholder_for_pattern("\\r\\n"), "\r\n");
3109        assert_eq!(placeholder_for_pattern(";"), ";");
3110        // Patterns with character classes / alternation still route
3111        // through the heuristic.
3112        assert_eq!(placeholder_for_pattern("[0-9]+"), "0");
3113        assert_eq!(placeholder_for_pattern("a|b"), "_");
3114    }
3115
3116    #[test]
3117    fn supertypes_decode_from_grammar_json_strings() {
3118        // Tree-sitter older grammars list supertypes as bare strings.
3119        let bytes = br#"{
3120            "name": "tiny",
3121            "supertypes": ["expression"],
3122            "rules": {
3123                "expression": {
3124                    "type": "CHOICE",
3125                    "members": [
3126                        {"type": "SYMBOL", "name": "binary_expression"},
3127                        {"type": "SYMBOL", "name": "identifier"}
3128                    ]
3129                },
3130                "binary_expression": {"type": "STRING", "value": "x"},
3131                "identifier": {"type": "PATTERN", "value": "[a-z]+"}
3132            }
3133        }"#;
3134        let g = Grammar::from_bytes("tiny", bytes).expect("parse");
3135        assert!(g.supertypes.contains("expression"));
3136        // identifier matches the supertype `expression`.
3137        assert!(kind_satisfies_symbol(&g, Some("identifier"), "expression"));
3138        // unrelated kinds do not.
3139        assert!(!kind_satisfies_symbol(&g, Some("string"), "expression"));
3140    }
3141
3142    #[test]
3143    fn supertypes_decode_from_grammar_json_objects() {
3144        // Recent grammars list supertypes as `{type: SYMBOL, name: ...}`
3145        // entries instead of bare strings.
3146        let bytes = br#"{
3147            "name": "tiny",
3148            "supertypes": [{"type": "SYMBOL", "name": "stmt"}],
3149            "rules": {
3150                "stmt": {
3151                    "type": "CHOICE",
3152                    "members": [
3153                        {"type": "SYMBOL", "name": "while_stmt"},
3154                        {"type": "SYMBOL", "name": "if_stmt"}
3155                    ]
3156                },
3157                "while_stmt": {"type": "STRING", "value": "while"},
3158                "if_stmt": {"type": "STRING", "value": "if"}
3159            }
3160        }"#;
3161        let g = Grammar::from_bytes("tiny", bytes).expect("parse");
3162        assert!(g.supertypes.contains("stmt"));
3163        assert!(kind_satisfies_symbol(&g, Some("while_stmt"), "stmt"));
3164    }
3165
3166    #[test]
3167    fn alias_value_matches_kind() {
3168        // A named ALIAS rewrites the parser-visible kind to `value`;
3169        // `kind_satisfies_symbol` should accept that rewritten kind
3170        // when looking up the original SYMBOL.
3171        let bytes = br#"{
3172            "name": "tiny",
3173            "rules": {
3174                "_package_identifier": {
3175                    "type": "ALIAS",
3176                    "named": true,
3177                    "value": "package_identifier",
3178                    "content": {"type": "SYMBOL", "name": "identifier"}
3179                },
3180                "identifier": {"type": "PATTERN", "value": "[a-z]+"}
3181            }
3182        }"#;
3183        let g = Grammar::from_bytes("tiny", bytes).expect("parse");
3184        assert!(kind_satisfies_symbol(
3185            &g,
3186            Some("package_identifier"),
3187            "_package_identifier"
3188        ));
3189    }
3190
3191    #[test]
3192    fn referenced_symbols_walks_nested_seq() {
3193        let prod: Production = serde_json::from_str(
3194            r#"{
3195                "type": "SEQ",
3196                "members": [
3197                    {"type": "CHOICE", "members": [
3198                        {"type": "SYMBOL", "name": "attribute_item"},
3199                        {"type": "BLANK"}
3200                    ]},
3201                    {"type": "SYMBOL", "name": "parameter"},
3202                    {"type": "REPEAT", "content": {
3203                        "type": "SEQ",
3204                        "members": [
3205                            {"type": "STRING", "value": ","},
3206                            {"type": "SYMBOL", "name": "parameter"}
3207                        ]
3208                    }}
3209                ]
3210            }"#,
3211        )
3212        .expect("seq");
3213        let symbols = referenced_symbols(&prod);
3214        assert!(symbols.contains(&"attribute_item"));
3215        assert!(symbols.contains(&"parameter"));
3216    }
3217
3218    #[test]
3219    fn literal_strings_collects_choice_members() {
3220        let prod: Production = serde_json::from_str(
3221            r#"{
3222                "type": "CHOICE",
3223                "members": [
3224                    {"type": "STRING", "value": "+"},
3225                    {"type": "STRING", "value": "-"},
3226                    {"type": "STRING", "value": "*"}
3227                ]
3228            }"#,
3229        )
3230        .expect("choice");
3231        let strings = literal_strings(&prod);
3232        assert_eq!(strings, vec!["+", "-", "*"]);
3233    }
3234
3235    /// The ocaml and javascript grammars (tree-sitter ≥ 0.25) emit a
3236    /// `RESERVED` rule kind that an earlier deserialiser rejected
3237    /// with `unknown variant "RESERVED"`. Verify both that the bare
3238    /// variant deserialises and that a `RESERVED`-wrapped grammar is
3239    /// loadable end-to-end via [`Grammar::from_bytes`].
3240    #[test]
3241    fn reserved_variant_deserialises() {
3242        let prod: Production = serde_json::from_str(
3243            r#"{
3244                "type": "RESERVED",
3245                "content": {"type": "SYMBOL", "name": "_lowercase_identifier"},
3246                "context_name": "attribute_id"
3247            }"#,
3248        )
3249        .expect("RESERVED parses");
3250        match prod {
3251            Production::Reserved { content, .. } => match *content {
3252                Production::Symbol { name } => assert_eq!(name, "_lowercase_identifier"),
3253                other => panic!("expected inner SYMBOL, got {other:?}"),
3254            },
3255            other => panic!("expected RESERVED, got {other:?}"),
3256        }
3257    }
3258
3259    #[test]
3260    fn reserved_grammar_loads_end_to_end() {
3261        let bytes = br#"{
3262            "name": "tiny_reserved",
3263            "rules": {
3264                "program": {
3265                    "type": "RESERVED",
3266                    "content": {"type": "SYMBOL", "name": "ident"},
3267                    "context_name": "keywords"
3268                },
3269                "ident": {"type": "PATTERN", "value": "[a-z]+"}
3270            }
3271        }"#;
3272        let g = Grammar::from_bytes("tiny_reserved", bytes).expect("RESERVED-using grammar loads");
3273        assert!(g.rules.contains_key("program"));
3274    }
3275
3276    #[test]
3277    fn reserved_walker_helpers_recurse_into_content() {
3278        // The walker's helpers (first_symbol, has_field_in,
3279        // referenced_symbols, ...) all need to descend through
3280        // RESERVED into its content. If they bail at RESERVED, the
3281        // `pick_choice_with_cursor` heuristic ranks the alt below
3282        // alts that DO recurse, which produces wrong emit output
3283        // even when the deserialiser doesn't crash.
3284        let prod: Production = serde_json::from_str(
3285            r#"{
3286                "type": "RESERVED",
3287                "content": {
3288                    "type": "FIELD",
3289                    "name": "lhs",
3290                    "content": {"type": "SYMBOL", "name": "expr"}
3291                },
3292                "context_name": "ctx"
3293            }"#,
3294        )
3295        .expect("nested RESERVED parses");
3296        assert_eq!(first_symbol(&prod), Some("expr"));
3297        assert!(has_field_in(&prod, &["lhs"]));
3298        let symbols = referenced_symbols(&prod);
3299        assert!(symbols.contains(&"expr"));
3300    }
3301
3302    // -- Yield-set tests --
3303
3304    fn yield_of(grammar: &Grammar, prod: &Production) -> std::collections::HashSet<String> {
3305        let mut visited = std::collections::HashSet::new();
3306        let mut cache = grammar.yield_sets.clone();
3307        yield_of_production(grammar, prod, &mut visited, &mut cache)
3308    }
3309
3310    #[test]
3311    fn yield_set_seq_only_first_member() {
3312        let prod: Production = serde_json::from_str(
3313            r#"{
3314                "type": "SEQ",
3315                "members": [
3316                    {"type": "SYMBOL", "name": "identifier"},
3317                    {"type": "STRING", "value": "as"},
3318                    {"type": "SYMBOL", "name": "target"}
3319                ]
3320            }"#,
3321        )
3322        .expect("valid SEQ");
3323        let g = Grammar::from_bytes("test", b"{}").unwrap_or_else(|_| {
3324            serde_json::from_str::<Grammar>(r#"{"name":"t","rules":{}}"#).unwrap()
3325        });
3326        let ys = yield_of(&g, &prod);
3327        assert!(ys.contains("identifier"), "SEQ yields first member");
3328        assert!(
3329            !ys.contains("target"),
3330            "SEQ must NOT yield non-first members"
3331        );
3332    }
3333
3334    #[test]
3335    fn yield_set_choice_union() {
3336        let prod: Production = serde_json::from_str(
3337            r#"{
3338                "type": "CHOICE",
3339                "members": [
3340                    {"type": "SYMBOL", "name": "a"},
3341                    {"type": "SYMBOL", "name": "b"}
3342                ]
3343            }"#,
3344        )
3345        .expect("valid CHOICE");
3346        let g = serde_json::from_str::<Grammar>(r#"{"name":"t","rules":{}}"#).unwrap();
3347        let ys = yield_of(&g, &prod);
3348        assert_eq!(ys.len(), 2);
3349        assert!(ys.contains("a"));
3350        assert!(ys.contains("b"));
3351    }
3352
3353    #[test]
3354    fn yield_set_hidden_expansion() {
3355        let g = serde_json::from_str::<Grammar>(
3356            r#"{"name":"t","rules":{
3357                "_value": {
3358                    "type": "CHOICE",
3359                    "members": [
3360                        {"type": "SYMBOL", "name": "number"},
3361                        {"type": "SYMBOL", "name": "object"}
3362                    ]
3363                }
3364            }}"#,
3365        )
3366        .unwrap();
3367        let mut g = g;
3368        g.subtypes = compute_subtype_closure(&g);
3369        g.yield_sets = compute_yield_sets(&g);
3370        let sym: Production =
3371            serde_json::from_str(r#"{"type": "SYMBOL", "name": "_value"}"#).unwrap();
3372        let ys = yield_of(&g, &sym);
3373        assert!(
3374            ys.contains("number"),
3375            "hidden rule expands into its CHOICE members"
3376        );
3377        assert!(ys.contains("object"));
3378        assert!(
3379            !ys.contains("_value"),
3380            "hidden rule name is not in yield set"
3381        );
3382    }
3383
3384    #[test]
3385    fn yield_set_optional_includes_epsilon() {
3386        let prod: Production = serde_json::from_str(
3387            r#"{"type": "OPTIONAL", "content": {"type": "SYMBOL", "name": "x"}}"#,
3388        )
3389        .unwrap();
3390        let g = serde_json::from_str::<Grammar>(r#"{"name":"t","rules":{}}"#).unwrap();
3391        let ys = yield_of(&g, &prod);
3392        assert!(ys.contains("x"));
3393        assert!(ys.contains(""), "OPTIONAL includes epsilon");
3394    }
3395
3396    #[test]
3397    fn yield_set_alias_uses_value() {
3398        let prod: Production = serde_json::from_str(
3399            r#"{"type": "ALIAS", "content": {"type": "SYMBOL", "name": "real"},
3400                "named": true, "value": "alias_name"}"#,
3401        )
3402        .unwrap();
3403        let g = serde_json::from_str::<Grammar>(r#"{"name":"t","rules":{}}"#).unwrap();
3404        let ys = yield_of(&g, &prod);
3405        assert_eq!(ys.len(), 1);
3406        assert!(ys.contains("alias_name"), "named ALIAS yields its value");
3407    }
3408}