diff --git a/src/bin/antlr4-rust-gen.rs b/src/bin/antlr4-rust-gen.rs index 27768584..9a466f56 100644 --- a/src/bin/antlr4-rust-gen.rs +++ b/src/bin/antlr4-rust-gen.rs @@ -2589,6 +2589,15 @@ fn structural_rule_alternatives(rule: &Rule, vocabulary: &Vocabulary) -> Vec { + stable_accessor: bool, + /// Cardinality contributed by every enclosing quantifier *and* choice split. + enclosing_cardinality: embedded::ChildCardinality, + /// The same, but assuming each enclosing choice took this branch. + branch_local_cardinality: embedded::ChildCardinality, + choice_branch: &'a [(usize, usize)], + choice_arity: &'a [usize], + choice_spans: &'a [(usize, usize)], + group_spans: &'a [embedded::GroupSpan], + branch_spans: &'a [(usize, usize)], +} + fn collect_structural_context_refs_with_cardinality( elements: &[Element], refs: &mut Vec, - stable_accessor: bool, - enclosing_cardinality: embedded::ChildCardinality, + context: StructuralRefContext<'_>, vocabulary: &Vocabulary, ) { + let StructuralRefContext { + stable_accessor, + enclosing_cardinality, + branch_local_cardinality, + choice_branch, + choice_arity, + choice_spans, + group_spans, + branch_spans, + } = context; + // Tracks whether any terminal-bearing ref has been emitted on this path, so an + // element can record whether it is the first terminal. + // Only refs on *this* branch's path count: `refs` is shared across every branch + // of an enclosing choice, so terminals emitted for a sibling branch must not + // make this branch's elements look non-leading. `(x=A | x='a')` has each + // declaration first on its own path. + let mut seen_terminal = branch_local_cardinality.max.is_none_or(|max| max != 0) + && refs.iter().any(|existing| { + !existing.token_types.is_empty() + && existing.cardinality.max != Some(0) + && !existing.choice_branch.iter().any(|(choice, branch)| { + choice_branch + .iter() + .any(|(mine, my_branch)| choice == mine && branch != my_branch) + }) + }); for element in elements { let label = element.label.as_ref().map(|label| label.name.clone()); let is_list = element @@ -2679,6 +2736,13 @@ fn collect_structural_context_refs_with_cardinality( enclosing_cardinality, quantified_cardinality(embedded::ChildCardinality::ONE, element.quantifier), ); + // Same product, but against the cardinality this element would have if + // every enclosing choice took its branch — so a `min: 0` here comes only + // from a quantifier, never from branch membership. + let branch_local = multiply_child_cardinalities( + branch_local_cardinality, + quantified_cardinality(embedded::ChildCardinality::ONE, element.quantifier), + ); match &element.kind { ElementKind::RuleCall(call) => refs.push(embedded::ElementRef { label, @@ -2688,6 +2752,21 @@ fn collect_structural_context_refs_with_cardinality( is_list, cardinality, stable_accessor, + choice_branch: choice_branch.to_vec(), + choice_arity: choice_arity.to_vec(), + choice_spans: choice_spans.to_vec(), + group_spans: group_spans.to_vec(), + branch_spans: branch_spans.to_vec(), + leading_terminal: !seen_terminal, + span: Some(( + element.span.bytes.start as usize, + element.span.bytes.end as usize, + )), + branch_local_cardinality: branch_local, + group_local_cardinality: quantified_cardinality( + embedded::ChildCardinality::ONE, + element.quantifier, + ), }), ElementKind::Terminal(terminal) => { refs.push(embedded::ElementRef { @@ -2698,11 +2777,40 @@ fn collect_structural_context_refs_with_cardinality( is_list, cardinality, stable_accessor, + choice_branch: choice_branch.to_vec(), + choice_arity: choice_arity.to_vec(), + choice_spans: choice_spans.to_vec(), + group_spans: group_spans.to_vec(), + branch_spans: branch_spans.to_vec(), + leading_terminal: !seen_terminal, + span: Some(( + element.span.bytes.start as usize, + element.span.bytes.end as usize, + )), + branch_local_cardinality: branch_local, + group_local_cardinality: quantified_cardinality( + embedded::ChildCardinality::ONE, + element.quantifier, + ), }); } ElementKind::Block(block) => { let token_types = structural_block_token_types(block, vocabulary); - if !token_types.is_empty() { + // A token-only block collapses into a single group ref, which is + // what makes `x=(A | B)` one labeled token child. That only + // holds when the label sits *on* the group: when the block is + // unlabeled and the labels sit inside it (`(x=A)?`), collapsing + // would swallow them, so descend and let the inner refs carry + // their own labels. + // A block holding an action or predicate must also stay expanded even + // when it is token-only: collapsing it discards the per-branch spans + // that place the action, so `x=A? (A | B {$x.text})` could not tell + // that the action runs only where the sibling `A` cannot shadow `x`. + if !token_types.is_empty() + && (label.is_some() + || !(structural_block_labels_inside(block) + || structural_block_holds_action(block))) + { refs.push(embedded::ElementRef { label, target: String::new(), @@ -2711,7 +2819,26 @@ fn collect_structural_context_refs_with_cardinality( is_list, cardinality, stable_accessor, + choice_branch: choice_branch.to_vec(), + choice_arity: choice_arity.to_vec(), + choice_spans: choice_spans.to_vec(), + group_spans: group_spans.to_vec(), + branch_spans: branch_spans.to_vec(), + leading_terminal: !seen_terminal, + span: Some(( + element.span.bytes.start as usize, + element.span.bytes.end as usize, + )), + branch_local_cardinality: branch_local, + group_local_cardinality: quantified_cardinality( + embedded::ChildCardinality::ONE, + element.quantifier, + ), }); + // The collapsed group *is* a terminal child, so record it before + // skipping the rest of the loop body — otherwise the next element + // would also be classified as leading. + seen_terminal = true; continue; } if label.is_some() { @@ -2723,18 +2850,118 @@ fn collect_structural_context_refs_with_cardinality( is_list, cardinality, stable_accessor: false, + choice_branch: choice_branch.to_vec(), + choice_arity: choice_arity.to_vec(), + choice_spans: choice_spans.to_vec(), + group_spans: group_spans.to_vec(), + branch_spans: branch_spans.to_vec(), + leading_terminal: !seen_terminal, + span: Some(( + element.span.bytes.start as usize, + element.span.bytes.end as usize, + )), + branch_local_cardinality: branch_local, + group_local_cardinality: quantified_cardinality( + embedded::ChildCardinality::ONE, + element.quantifier, + ), }); } - let nested_stable = stable_accessor && block.alternatives.len() == 1; - for alternative in &block.alternatives { + // Refs from one alternative of a *choice* are present only when + // the parse took that branch, and the flattened CST does not + // record which. Clearing the lower bound states that honestly: + // a sibling alternative matching the same target then reads as + // inexact, so the occurrence-lookup guards in + // `context_label_accessor` reject exactly the layouts where + // positional access could resolve to another branch's child. + let branch_cardinality = if block.alternatives.len() > 1 { + embedded::ChildCardinality { + min: 0, + max: cardinality.max, + } + } else { + cardinality + }; + // Where the expanded refs start, so the terminal state can be read + // back off what the branches actually emitted (below). + let refs_before_block = refs.len(); + for (branch, alternative) in block.alternatives.iter().enumerate() { + // Tag each branch of a *choice* with `(block id, branch)` so + // consumers can tell mutually exclusive refs from sequential + // ones. A single-alternative block adds no exclusivity, so it + // keeps whatever tag it inherited. + let mut nested_branch = choice_branch.to_vec(); + let mut nested_arity = choice_arity.to_vec(); + let mut nested_spans = choice_spans.to_vec(); + // Every block counts here, single-alternative groups included. + let mut nested_branch_spans = branch_spans.to_vec(); + let mut nested_groups = group_spans.to_vec(); + nested_groups.push(embedded::GroupSpan { + start: block.span.bytes.start as usize, + end: block.span.bytes.end as usize, + // Only a quantifier that can yield nothing relaxes the + // elements inside. + optional: quantified_cardinality( + embedded::ChildCardinality::ONE, + element.quantifier, + ) + .min == 0, + // A star/plus group can run more than once, so even a group + // known to have run contributes an unfixed count. + repeated: quantified_cardinality( + embedded::ChildCardinality::ONE, + element.quantifier, + ) + .is_repeated(), + }); + if block.alternatives.len() > 1 { + nested_branch.push((block.syntax.index(), branch)); + nested_arity.push(block.alternatives.len()); + nested_spans.push(( + block.span.bytes.start as usize, + block.span.bytes.end as usize, + )); + nested_branch_spans.push(( + alternative.span.bytes.start as usize, + alternative.span.bytes.end as usize, + )); + } collect_structural_context_refs_with_cardinality( &alternative.elements, refs, - nested_stable, - cardinality, + StructuralRefContext { + stable_accessor, + enclosing_cardinality: branch_cardinality, + // Inside a branch the group's own quantifier still + // applies, but the choice split does not. + branch_local_cardinality: branch_local, + choice_branch: &nested_branch, + choice_arity: &nested_arity, + choice_spans: &nested_spans, + group_spans: &nested_groups, + branch_spans: &nested_branch_spans, + }, vocabulary, ); } + // An expanded block still matched terminals, and the collapsibility + // helper below cannot see them: `structural_block_token_types` + // returns empty for any block that is not one-element-per-branch, so + // `(B y=C? | )` reported no tokens and left the *following* element + // marked as leading. That false state then let a mixed + // token/literal merge through in `(B y=C? | ) x=A | x='a'`. + // + // Read it back off the refs the branches actually emitted instead. + // `leading_terminal` is a *claim* that the element is the first + // terminal — which is what makes a block-positional index and a + // same-type index agree at 0 — so it has to hold on every path. Any + // branch that *can* match a terminal falsifies it, hence `any` over + // `cardinality.max != Some(0)` rather than agreement across branches. + if refs[refs_before_block..].iter().any(|candidate| { + !candidate.token_types.is_empty() && candidate.cardinality.max != Some(0) + }) { + seen_terminal = true; + } } ElementKind::Set { inverted, elements } => { refs.push(embedded::ElementRef { @@ -2745,6 +2972,21 @@ fn collect_structural_context_refs_with_cardinality( is_list, cardinality, stable_accessor, + choice_branch: choice_branch.to_vec(), + choice_arity: choice_arity.to_vec(), + choice_spans: choice_spans.to_vec(), + group_spans: group_spans.to_vec(), + branch_spans: branch_spans.to_vec(), + leading_terminal: !seen_terminal, + span: Some(( + element.span.bytes.start as usize, + element.span.bytes.end as usize, + )), + branch_local_cardinality: branch_local, + group_local_cardinality: quantified_cardinality( + embedded::ChildCardinality::ONE, + element.quantifier, + ), }); } ElementKind::Range(..) if label.is_some() => refs.push(embedded::ElementRef { @@ -2755,12 +2997,30 @@ fn collect_structural_context_refs_with_cardinality( is_list, cardinality, stable_accessor: false, + choice_branch: choice_branch.to_vec(), + choice_arity: choice_arity.to_vec(), + choice_spans: choice_spans.to_vec(), + group_spans: group_spans.to_vec(), + branch_spans: branch_spans.to_vec(), + leading_terminal: !seen_terminal, + span: Some(( + element.span.bytes.start as usize, + element.span.bytes.end as usize, + )), + branch_local_cardinality: branch_local, + group_local_cardinality: quantified_cardinality( + embedded::ChildCardinality::ONE, + element.quantifier, + ), }), ElementKind::Range(..) | ElementKind::Action { .. } | ElementKind::Predicate { .. } | ElementKind::Epsilon => {} } + if !structural_element_token_types(element, vocabulary).is_empty() { + seen_terminal = true; + } } } @@ -2859,6 +3119,40 @@ fn structural_block_token_types(block: &Block, vocabulary: &Vocabulary) -> Vec bool { + block + .alternatives + .iter() + .flat_map(|alternative| &alternative.elements) + .any(|element| match &element.kind { + ElementKind::Action { .. } | ElementKind::Predicate { .. } => true, + ElementKind::Block(nested) => structural_block_holds_action(nested), + _ => false, + }) +} + +/// Whether any element anywhere inside `block` carries a label, i.e. the block +/// is a grouping wrapper around labeled elements (`(x=A)?`) rather than a +/// labeled token group (`x=(A | B)`). +/// +/// The search descends nested blocks: extra grouping levels (`((x=A))?`) are +/// syntactically inert, so a label buried under them must still prevent the +/// collapse that would discard it. +fn structural_block_labels_inside(block: &Block) -> bool { + block + .alternatives + .iter() + .flat_map(|alternative| &alternative.elements) + .any(|element| { + element.label.is_some() + || matches!(&element.kind, ElementKind::Block(nested) + if structural_block_labels_inside(nested)) + }) +} + fn structural_terminal_child_target( terminal: &Terminal, vocabulary: &Vocabulary, @@ -8941,6 +9235,15 @@ fn context_label_accessor( is_list, cardinality: embedded::ChildCardinality::ONE, stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: embedded::ChildCardinality::ONE, + group_local_cardinality: embedded::ChildCardinality::ONE, }; let mut selector = None; let mut cardinalities = Vec::with_capacity(alternatives.len()); @@ -9003,17 +9306,145 @@ fn context_label_selector( is_list: bool, ) -> Option { let first_position = matching[0].0; - let start = exact_target_cardinality(&alternative.refs[..first_position], target)?; + let labeled = matching[0].1; + // A sibling branch that matches the same target supplies a child at the very + // position this accessor reads, but on a parse where the label is unset — + // `(left=unary | right=unary)` would let `right()` return `left`'s child. + // Positional lookup cannot tell them apart, so decline. + // Another *declaration* of the same label is not an impostor — it binds the + // label too, and `context_label_accessor` already proved the declarations share + // one read. Only an unlabeled (or differently-labeled) sibling child can be + // mistaken for this label's. + let start = + exact_target_cardinality_on_path(&alternative.refs[..first_position], target, labeled)?; + // A sibling branch only collides when it can actually put a matching child at the + // position this accessor reads. `(A | A A x=A)` selects occurrence 2 while the + // sibling branch supplies at most one `A`, so `nth(2)` is safely empty there. + let sibling_supplies_target = alternative.refs.iter().any(|element| { + if element.label.as_deref() == Some(label) + || element.can_coexist_with(labeled) + || !context_ref_can_match_target(element, target) + || element.cardinality.max == Some(0) + { + return false; + } + let position = alternative + .refs + .iter() + .position(|candidate| std::ptr::eq(candidate, element)); + let reach = position.and_then(|position| { + let before = + exact_target_cardinality_on_path(&alternative.refs[..position], target, element)?; + // The highest occurrence this ref can occupy on its own path. + element + .cardinality + .max + .map(|max| before.saturating_add(max)) + }); + reach.is_none_or(|reach| reach > start) + }); + if sibling_supplies_target { + return None; + } if is_list { let has_unlabeled_target = alternative.refs[first_position..].iter().any(|element| { context_ref_can_match_target(element, target) && element.cardinality.max != Some(0) && element.label.as_deref() != Some(label) }); - return (!has_unlabeled_target).then_some(ContextLabelSelector::AllAfter(start)); + // A same-target ref *before* the label normally just shifts `start`, but if + // the two share a repeated group it recurs on every iteration and interleaves + // with the labeled children: `(A xs+=A)+` skips one `A` and then collects the + // second iteration's unlabeled prefix too. No `skip` can separate them. + let repeats_with_prefix = alternative.refs[..first_position].iter().any(|element| { + context_ref_can_match_target(element, target) + && element.cardinality.max != Some(0) + && element.label.as_deref() != Some(label) + && element.group_spans.iter().any(|group| { + // Shared and repeatable: `max` is not one, so the group can run + // more than once. + labeled.group_spans.contains(group) + && !matches!(element.cardinality.max, Some(0 | 1)) + }) + }); + if repeats_with_prefix { + return None; + } + // `AllAfter(start)` skips `start` children then takes the rest, so repeated + // declarations on one path are fine — the later ones fall inside the tail + // (`xs+=e (op xs+=e)*` skips 0 and collects every `e`). What it cannot serve + // is *mutually exclusive* declarations that begin at different offsets: + // `(A xs+=A | xs+=A)` needs skip 1 on one branch and 0 on the other. + let starts_agree = matching.iter().all(|(position, declaration)| { + if declaration.can_coexist_with(labeled) { + return true; + } + exact_target_cardinality_on_path(&alternative.refs[..*position], target, declaration) + == Some(start) + }); + return (!has_unlabeled_target && starts_agree) + .then_some(ContextLabelSelector::AllAfter(start)); } + // Several declarations can share one positional read when they are mutually + // exclusive and each sits at the same occurrence — `(x=A | x=B)` binds exactly + // one token on every parse, and the accessor's unioned token set selects it. if matching.len() != 1 { - return None; + let mutually_exclusive = matching.iter().enumerate().all(|(index, (_, left))| { + matching[index + 1..] + .iter() + .all(|(_, right)| !left.can_coexist_with(right)) + }); + let positions = matching + .iter() + .map(|(position, element)| { + exact_target_cardinality_on_path(&alternative.refs[..*position], target, element) + }) + .collect::>>()?; + let agreed = positions.first().copied()?; + if !mutually_exclusive || positions.iter().any(|position| *position != agreed) { + return None; + } + // Each declaration must also survive the single-label hazards: an *optional* + // one can be displaced by a following same-target child sliding into its + // slot, and the shared read cannot tell them apart either + // (`(x=A? B | x=B)` returns the unlabeled `B` when `x` is absent). + for (position, declaration) in matching { + // Optionality here means the *declaration's own* EBNF suffix — a `min: 0` + // that only reflects its branch possibly not being taken does not make it + // displaceable, since the read is chosen per branch anyway. + if declaration.branch_local_cardinality.min == 0 + && alternative.refs[position + 1..].iter().any(|following| { + context_ref_can_match_target(following, target) + && following.cardinality.max != Some(0) + && following.can_coexist_with(declaration) + }) + { + return None; + } + } + // A *repeated* scalar declaration (`x=A+`) is overwritten each iteration, so + // ANTLR exposes the last match — `nth` would pin the first. But `LastAfter` + // applies to every branch, so it is only sound when no branch has a matching + // child *after* its declaration: in `(x=A A B | x=A+ C)` the first branch's + // trailing unlabeled `A` would become the `last()`. + if matching + .iter() + .any(|(_, element)| element.cardinality.is_repeated()) + { + let followed = matching.iter().any(|(position, declaration)| { + alternative.refs[position + 1..].iter().any(|following| { + following.label.as_deref() != Some(label) + && context_ref_can_match_target(following, target) + && following.cardinality.max != Some(0) + && following.can_coexist_with(declaration) + }) + }); + if followed { + return None; + } + return Some(ContextLabelSelector::LastAfter(agreed)); + } + return Some(ContextLabelSelector::Nth(agreed)); } let element = matching[0].1; if !element.cardinality.is_repeated() { @@ -9058,13 +9489,81 @@ fn context_ref_can_match_target( .any(|token_type| target.token_types.contains(token_type)) } +/// Number of `target`-matching children that precede a ref *on the same parse +/// path* as `path`, or `None` when that number is not fixed. +fn exact_target_cardinality_on_path( + refs: &[embedded::ElementRef], + target: &embedded::ElementRef, + path: &embedded::ElementRef, +) -> Option { + // Filtering to one path and then demanding cross-branch agreement are mutually + // exclusive: the other branches were removed deliberately, so requiring them to + // contribute would reject `(A x=A | B)`, where the retained `A` still carries + // the choice's full arity of two. + // A ref in a branch `path` cannot reach never precedes it, so drop those + // before counting: in `(left=unary | right=unary)` the `left` ref must not + // shift `right` to occurrence 1. + let reachable = refs + .iter() + .filter(|element| element.can_coexist_with(path)) + .cloned() + .map(|mut element| { + // Drop the branch tags shared with `path`: on this path those branches + // are taken, so their refs count as plain sequential children rather + // than alternatives awaiting cross-branch agreement. + element.retain_choices(|choice| { + !path.choice_branch.iter().any(|&(taken, _)| taken == choice) + }); + // Their cardinality on this path is the branch-local one — and when the + // ref shares every optional group with the label, those groups are taken + // wherever the label is bound, so even their quantifiers are satisfied. + // `(A x=A)? EOF` has exactly one `A` before the label on every parse that + // binds it, though both figures otherwise report `0..1` from the `?`. + // + // A *repeated* group not shared with the label is the exception: knowing + // it ran says nothing about how many times, so its contribution stays + // unfixed. `((A B)+ x=A)?` has a variable run of `A` ahead of the label + // even though the outer `?` is satisfied. This mirrors `on_taken_group` + // in the action-resolution path. + let closed_repeated_group = element + .group_spans + .iter() + .any(|group| group.repeated && !path.group_spans.contains(group)); + let shares_optional_groups = !element.group_spans.is_empty() + && !closed_repeated_group + && element + .group_spans + .iter() + .filter(|group| group.optional) + .all(|group| path.group_spans.contains(group)); + let on_path = if shares_optional_groups { + element.group_local_cardinality + } else { + element.branch_local_cardinality + }; + element.cardinality = on_path; + // `exact_target_cardinality` judges exactness from the branch-local + // figure, so it has to see the same value. + element.branch_local_cardinality = on_path; + element + }) + .collect::>(); + exact_target_cardinality(&reachable, target) +} + fn exact_target_cardinality( refs: &[embedded::ElementRef], target: &embedded::ElementRef, ) -> Option { - refs.iter().try_fold(0_usize, |total, element| { + // Refs are grouped by their innermost choice so that an *exhaustive* choice + // counts once rather than per branch. `(a=A | b=A) x=A` always contributes + // exactly one `A` before `x`, even though each branch ref alone reports + // `0..1`; summing them independently would read as inexact and drop `x()`. + let mut total = 0_usize; + let mut choice_totals: BTreeMap<(usize, usize), Option> = BTreeMap::new(); + for element in refs { if !context_ref_can_match_target(element, target) { - return Some(total); + continue; } if !element.token_types.is_empty() && !element @@ -9075,8 +9574,102 @@ fn exact_target_cardinality( return None; } let exact = element.cardinality.max?; - (element.cardinality.min == exact).then(|| total.saturating_add(exact)) - }) + // A ref inside a choice reports `min: 0` because its *branch* may not be + // taken, but within that branch it contributes its branch-local count + // exactly. Judge exactness against that, so an *optional* choice + // (`(a=A | b=A)? x=A`) stays inexact — the group may yield nothing. + let local = element.branch_local_cardinality; + let contribution = (local.min == exact && local.max == Some(exact)).then_some(exact); + match element.choice_branch.last() { + // Sequential: its count adds directly. + None => total = total.saturating_add(contribution?), + // Inside a choice: accumulate per branch, compare branches after. + Some(&key) => { + let branch = choice_totals.entry(key).or_insert(Some(0)); + *branch = match (*branch, contribution) { + (Some(sum), Some(next)) => Some(sum.saturating_add(next)), + _ => None, + }; + } + } + } + // A choice is exact only when every one of its branches contributes the same + // count. Branches with no matching ref never entered the map above yet still + // contribute zero, so the full branch set is recovered from `refs`. + // + // Nested choices are folded innermost-first: once an inner choice agrees, its + // count is attributed to the *enclosing* branch that contains it, so + // `((a=A | b=A) | c=A) x=A` — where every path yields one `A` — stays exact. + // Arity comes from the recorded `choice_arity`, not from the branches seen: an + // *empty* alternative emits no ref, so `(a=A | )` would otherwise look like a + // one-branch choice that always yields an `A`. + let mut arity_of_choice: BTreeMap = BTreeMap::new(); + for element in refs { + for (&(choice, _), &arity) in element.choice_branch.iter().zip(&element.choice_arity) { + arity_of_choice.insert(choice, arity); + } + } + let mut branches_per_choice: BTreeMap> = BTreeMap::new(); + let mut depth_of_choice: BTreeMap = BTreeMap::new(); + for element in refs { + for (depth, &(choice, branch)) in element.choice_branch.iter().enumerate() { + branches_per_choice + .entry(choice) + .or_default() + .insert(branch); + depth_of_choice.insert(choice, depth); + } + } + // Ancestry per branch key, so an inner choice's total can be re-attributed. + let mut ancestry: BTreeMap<(usize, usize), Vec<(usize, usize)>> = BTreeMap::new(); + for element in refs { + if let Some(&key) = element.choice_branch.last() { + ancestry.insert(key, element.choice_branch.clone()); + } + } + let mut pending = choice_totals; + // Deepest choices first, so inner results roll up into their parents. + let mut choices = depth_of_choice + .iter() + .map(|(c, d)| (*d, *c)) + .collect::>(); + choices.sort_unstable_by_key(|(depth, _)| std::cmp::Reverse(*depth)); + for (_, choice) in choices { + let counts = pending + .iter() + .filter(|((candidate, _), _)| *candidate == choice) + .map(|((_, branch), count)| (*branch, *count)) + .collect::>(); + if counts.is_empty() { + continue; + } + let expected = arity_of_choice + .get(&choice) + .copied() + .unwrap_or_else(|| branches_per_choice.get(&choice).map_or(0, BTreeSet::len)); + let first = counts.first().and_then(|(_, count)| *count)?; + if counts.len() != expected || counts.iter().any(|(_, count)| *count != Some(first)) { + return None; + } + for (branch, _) in &counts { + pending.remove(&(choice, *branch)); + } + // Attribute this choice's agreed count to its own enclosing branch, if any. + let parent = counts.first().and_then(|(branch, _)| { + ancestry + .get(&(choice, *branch)) + .and_then(|chain| chain.split_last().map(|(_, rest)| rest.last().copied())) + .flatten() + }); + match parent { + Some(parent_key) => { + let slot = pending.entry(parent_key).or_insert(Some(0)); + *slot = slot.map(|sum| sum.saturating_add(first)); + } + None => total = total.saturating_add(first), + } + } + Some(total) } fn sum_child_cardinalities( @@ -14772,6 +15365,496 @@ mod tests { insta::assert_snapshot!("multi_alternative_label_shadowed_context", shadowed_context); } + /// Issue #201: labels nested inside an unlabeled grouping block, and a + /// single/list label pair on one rule, both reach the typed surface — while + /// layouts where positional lookup could resolve to another choice branch's + /// child still decline. + #[test] + fn grouped_and_mixed_same_rule_labels_emit_accessors_without_crossing_branches() { + let data = parser_fixture_data("multi-alternative-label/T.g4"); + let rendered = render_parser("TParser", &data).expect("parser should render"); + + let context = |name: &str| { + rendered + .split_once(&format!("impl<'a, State> {name}<'a, State> {{")) + .unwrap_or_else(|| panic!("{name} impl")) + .1 + .split_once(&format!("impl std::fmt::Display for {name}")) + .unwrap_or_else(|| panic!("{name} display impl")) + .0 + .to_owned() + }; + + // `(doc = IDENT)? (oneway = STAR | IN errors += unary ...)?`: the labels + // sit inside unlabeled grouping blocks, so collapsing each block into + // one token-group ref would swallow them. + insta::assert_snapshot!( + "multi_alternative_label_grouped_context", + context("GroupedContext") + ); + + // `name = unary ... errors += unary`: a single and a list label on the + // same rule must each resolve past the other's children. + insta::assert_snapshot!( + "multi_alternative_label_mixed_context", + context("MixedContext") + ); + + // A label buried under redundant grouping levels still reaches the + // surface — the collapse check descends nested blocks. + insta::assert_snapshot!( + "multi_alternative_label_nested_group_context", + context("NestedGroupContext") + ); + + // The three declining shapes are snapshotted whole rather than probed + // with `!contains`, so the absent accessor is visible alongside + // everything the context *does* expose: + // + // * `mixed_unbounded` — a variable count of the label's own target ahead + // of it leaves no fixed `.skip(N)`; + // * `branch_hazard` — only one branch supplies the label while its + // sibling matches the same target unlabeled, so `.nth(0)` could read + // the sibling's child; + // * `branch_rival` — rival labels on one target across branches must not + // read each other's child. + // An exhaustive choice keeps a following label's accessor (its prefix + // count is fixed at one however the choice branches), while a preceding + // *overlapping* token group does not (only some parses put a matching + // child ahead of the label). + insta::assert_snapshot!( + "multi_alternative_label_exhaustive_prefix_context", + context("ExhaustivePrefixContext") + ); + insta::assert_snapshot!( + "multi_alternative_label_overlapping_group_context", + context("OverlappingGroupContext") + ); + // Making that same choice optional removes the fixed position, so the + // following label loses its accessor — the branch-local cardinality is + // what distinguishes the two. + insta::assert_snapshot!( + "multi_alternative_label_optional_prefix_context", + context("OptionalPrefixContext") + ); + // One label over mutually exclusive branches merges into a single read; and + // restricting to the label's own path lets a sibling branch be ignored + // rather than demanded. + insta::assert_snapshot!( + "multi_alternative_label_merged_rivals_context", + context("MergedRivalsContext") + ); + // Repeated scalar declarations merge as a *last*-match read, since ANTLR + // overwrites a scalar label on every iteration. + insta::assert_snapshot!( + "multi_alternative_label_merged_repeats_context", + context("MergedRepeatsContext") + ); + insta::assert_snapshot!( + "multi_alternative_label_path_restricted_context", + context("PathRestrictedContext") + ); + // Two ways the on-path restriction can overstate what it knows, both + // declining as a result: + // + // * `closed_repeat_prefix` — sharing every *optional* group with the label + // proves those groups ran, but a closed `+` inside them ran an unknown + // number of times, so the prefix count stays unfixed; + // * `inner_choice_arity` — dropping the taken outer choice must drop its + // arity too, or the surviving three-way inner choice is read as an + // exhaustive two-way one and the prefix count is wrongly fixed at 1. + insta::assert_snapshot!( + "multi_alternative_label_closed_repeat_prefix_context", + context("ClosedRepeatPrefixContext") + ); + insta::assert_snapshot!( + "multi_alternative_label_inner_choice_arity_context", + context("InnerChoiceArityContext") + ); + // Nesting the exhaustive choice keeps the count fixed: the inner choice's + // agreed contribution rolls up into the outer branch. + insta::assert_snapshot!( + "multi_alternative_label_nested_exhaustive_prefix_context", + context("NestedExhaustivePrefixContext") + ); + + for (name, snapshot) in [ + ( + "MixedUnboundedContext", + "multi_alternative_label_mixed_unbounded_context", + ), + ( + "BranchHazardContext", + "multi_alternative_label_branch_hazard_context", + ), + ( + "BranchRivalContext", + "multi_alternative_label_branch_rival_context", + ), + ] { + insta::assert_snapshot!(snapshot, context(name)); + } + } + + /// Every label shape whose *resolution outcome* this module decides, kept in + /// one place so a change to any guard shows up as a diff here rather than as a + /// silent behaviour change in a grammar nobody tests. + /// + /// A label resolves only when the read `translate_element_read` emits provably + /// selects that label's own element. `resolve` means the grammar generates; + /// `decline` means resolution fails loudly (`cannot translate $x`), which is + /// always preferable to a read that returns some *other* child. Each entry + /// records why, because the two outcomes are easy to swap by accident — most + /// of these were originally over-rejections introduced while fixing a + /// miscompile, or vice versa. + #[test] + fn label_resolution_corpus_matches_expected_outcomes() { + // (fixture, label, resolves, why). `label` is the accessor/read the case + // turns on: rendering succeeds either way for a grammar without actions, so + // a declined *accessor* shows up as the method being absent rather than as + // a render error. + const CORPUS: &[(&str, &str, bool, &str)] = &[ + // Declines: the read would select a child the label never bound. + ( + "SiblingUnlabeledSameTarget", + "xs", + false, + "an action after a choice runs for every branch, so a sibling's token is not the label's", + ), + ( + "OptionalBlockFollowedByTerminal", + "x", + false, + "an absent optional block lets the follower occupy its index", + ), + ( + "ActionAfterNestedChoice", + "xs", + false, + "a list read would fold in the sibling branch's child", + ), + ( + "LiteralAliasDifferingOccurrence", + "x", + false, + "block and token reads index in different units, so occurrence 1 means different children", + ), + ( + "MergedDeclarationOptionalFollower", + "x", + false, + "an absent optional declaration lets the follower slide into the merged read", + ), + ( + "ListDeclarationsDifferingStart", + "xs", + false, + "one `AllAfter` skip cannot serve branches that begin at different offsets", + ), + ( + "InnerGroupClosedBeforeAction", + "x", + false, + "an inner group that closed before the action proves nothing about what matched", + ), + ( + "AliasDifferingOccurrenceInAlt", + "x", + false, + "block and token occurrences are comparable only at zero", + ), + ( + "DeclarationsDifferingOccurrence", + "x", + false, + "one positional read cannot serve declarations at different occurrences", + ), + ( + "DeclarationsDifferingRepetition", + "x", + false, + "a repeated declaration needs a last-match read the others do not", + ), + ( + "RepeatedBlockLabel", + "x", + false, + "a repeated block label exposes its last match, which a positional read cannot express", + ), + ( + "RepeatedSiblingSpansIndex", + "x", + false, + "a repeated sibling spans a range of terminal positions, not just its start", + ), + ( + "RepeatedMergeFollowedByMatch", + "x", + false, + "a shared last-match read would return a following unlabeled child on the non-repeated branch", + ), + ( + "InitScalarRuleLabel", + "x", + false, + "a scalar rule read lowers to `.expect(...)`, which panics at rule entry", + ), + ( + "AliasDeclarationsInChoice", + "x", + false, + "known limitation: alias declarations inside one nested choice still decline (see PR discussion)", + ), + ( + "FallbackReadSiblingAlternative", + "x", + false, + "a `last()` fallback can select any terminal, so a non-declaring alternative satisfies it", + ), + ( + "MixedModeLeadingTerminal", + "x", + false, + "mixed-mode occurrence zero coincides only when no terminal precedes either side", + ), + ( + "PrecedingSiblingBranch", + "x", + false, + "a same-target sibling *before* the label impersonates it as readily as one after", + ), + ( + "ExpandedBlockTerminalState", + "x", + false, + "an expanded block still matched a terminal, so what follows it is not leading", + ), + ( + "ListAliasAcrossModes", + "xs", + false, + "a list read has no form common to token and block mode, so aliases cannot merge", + ), + ( + "ListLabelWithoutIterator", + "xs", + false, + "a list label whose target names no rule or token type has no iterator read", + ), + ( + "InnerChoiceArityInAction", + "x", + false, + "dropping the taken outer choice must drop its arity, or a three-way inner choice reads as two-way", + ), + // Resolves: valid reads that must not be rejected. + ( + "ExhaustiveInnerChoiceInAction", + "x", + true, + "a genuinely exhaustive inner choice keeps its fixed prefix count of one", + ), + ( + "ActionInCollapsibleChoice", + "x", + true, + "a token-only choice holding an action keeps its branch spans", + ), + ( + "CollapsedBlockIsTerminal", + "x", + false, + "a collapsed token group is itself a terminal child, so what follows is not leading", + ), + ( + "UnrelatedLaterChoiceConfinement", + "x", + false, + "confinement to a later choice says nothing about an earlier sibling", + ), + ( + "ListPrefixRepeatsWithLabel", + "xs", + false, + "a same-target prefix sharing a repeated group interleaves with the labeled children", + ), + ( + "ClosedRepeatedGroupPrefix", + "x", + false, + "a closed repeated group still contributes an unfixed number of preceding children", + ), + ( + "RecoveredDeletedTokenIndex", + "x", + true, + "the positional block read skips deleted-token errors, so recovery cannot shift its index", + ), + ( + "ReassignedAfterAction", + "x", + true, + "a declaration after the action has not assigned the label yet, so it cannot conflict", + ), + ( + "ForwardBlockLabel", + "x", + true, + "a forward label's prefix is entirely in the action's future, so it cannot make the index inexact", + ), + ( + "OptionalGroupSharedWithLabel", + "x", + true, + "an optional group shared with the label is taken wherever the label is bound", + ), + ( + "SiblingBranchShorterThanOccurrence", + "x", + true, + "a sibling branch that cannot reach the selected occurrence is no collision", + ), + ( + "IdenticalDeclarationsInChoice", + "x", + true, + "isolating one declaration must not reclassify its twin as an impostor", + ), + ( + "MandatoryInnerGroupClosed", + "x", + true, + "a mandatory inner group relaxes nothing, so its closing before the action is irrelevant", + ), + ( + "InlineActionBeforeLabel", + "x", + true, + "an inline action sees no children, so a later unbounded run cannot poison it", + ), + ( + "SiblingDeclarationIrrelevant", + "x", + true, + "a branch-confined action never sees a sibling branch's declaration", + ), + ( + "NestedChoiceInsideConfinedBranch", + "x", + true, + "confinement to an outer branch does not restrict a nested choice inside it", + ), + ( + "ListAliasDeclarations", + "xs", + true, + "list declarations name one token type through two source forms", + ), + ( + "ChoicePrefixOutsideLabel", + "x", + true, + "a choice before the label contributes a fixed count when its branches agree", + ), + ( + "LiteralAliasSameOccurrence", + "x", + true, + "block and token reads coincide at occurrence zero", + ), + ( + "NestedChoiceSatisfiability", + "x", + true, + "nested choices fold rather than sum: the alternative builds one child", + ), + ( + "RepeatedScalarMerge", + "x", + true, + "a repeated scalar label exposes its last match", + ), + ( + "ActionInsideTakenGroup", + "x", + true, + "the enclosing group's quantifier is satisfied wherever the action runs", + ), + ( + "ActionOnlyBranch", + "x", + true, + "a branch holding only an action still identifies itself by span", + ), + ( + "InitActionBeforeChildren", + "xs", + true, + "an `@init` body runs before any child exists, so no read can be polluted", + ), + ( + "ExhaustiveChoicePrefix", + "x", + true, + "an exhaustive choice contributes a fixed count", + ), + ( + "NotSetLabelBeforeTerminal", + "t", + true, + "ANTLR's `Sets/ParserNotTokenWithLabel` shape", + ), + ( + "ConjuredLiteralLabel", + "x", + true, + "ANTLR's `ParserErrors/ConjuringUpToken` shape", + ), + ]; + + let mut wrong = Vec::new(); + for &(fixture, label, resolves, why) in CORPUS { + let data = parser_fixture_data(&format!("label-resolution/{fixture}.g4")); + let rendered = render_parser_with_options( + &format!("{fixture}Parser"), + &data, + ParserRenderOptions { + embedded: true, + ..ParserRenderOptions::default() + }, + ); + // Which signal reports the decision depends on how the fixture reads its + // label. A grammar whose *action* reads it fails to render outright when + // resolution declines; one that relies on the typed accessor renders + // either way, and the decision surfaces as the method's presence. + let source = fs::read_to_string( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests/fixtures/antlr4-rust-gen/label-resolution") + .join(format!("{fixture}.g4")), + ) + .expect("fixture should be readable"); + let reads_via_action = source.contains(&format!("${label}")); + let resolved = rendered.as_ref().is_ok_and(|parser| { + reads_via_action || parser.contains(&format!("pub fn {label}(")) + }); + if resolved != resolves { + let outcome = if resolves { "resolve" } else { "decline" }; + let error = rendered + .err() + .map(|error| error.to_string()) + .unwrap_or_default(); + wrong.push(format!( + " {fixture} (${label}): expected {outcome} — {why} {error}" + )); + } + } + assert!( + wrong.is_empty(), + "label resolution changed:\n{}", + wrong.join("\n") + ); + } + #[test] fn non_embedded_parser_action_disables_generated_rule() { let rendered = diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__left_recursive_label_alternatives.snap b/src/bin/snapshots/antlr4_rust_gen__tests__left_recursive_label_alternatives.snap index 308e790c..2a541e3f 100644 --- a/src/bin/snapshots/antlr4_rust_gen__tests__left_recursive_label_alternatives.snap +++ b/src/bin/snapshots/antlr4_rust_gen__tests__left_recursive_label_alternatives.snap @@ -25,6 +25,25 @@ expression: "model.rules[1].alts" ), }, stable_accessor: true, + choice_branch: [], + choice_arity: [], + choice_spans: [], + group_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, + group_spans: [], + branch_spans: [], + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, }, ElementRef { label: None, @@ -41,6 +60,30 @@ expression: "model.rules[1].alts" ), }, stable_accessor: true, + choice_branch: [], + choice_arity: [], + choice_spans: [], + group_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, + group_spans: [], + branch_spans: [], + leading_terminal: true, + span: Some( + ( + 135, + 139, + ), + ), + branch_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, }, ElementRef { label: Some( @@ -57,6 +100,30 @@ expression: "model.rules[1].alts" ), }, stable_accessor: true, + choice_branch: [], + choice_arity: [], + choice_spans: [], + group_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, + group_spans: [], + branch_spans: [], + leading_terminal: false, + span: Some( + ( + 146, + 147, + ), + ), + branch_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, }, ], children: { @@ -99,6 +166,25 @@ expression: "model.rules[1].alts" ), }, stable_accessor: true, + choice_branch: [], + choice_arity: [], + choice_spans: [], + group_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, + group_spans: [], + branch_spans: [], + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, }, ElementRef { label: None, @@ -115,6 +201,30 @@ expression: "model.rules[1].alts" ), }, stable_accessor: true, + choice_branch: [], + choice_arity: [], + choice_spans: [], + group_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, + group_spans: [], + branch_spans: [], + leading_terminal: true, + span: Some( + ( + 190, + 194, + ), + ), + branch_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, }, ElementRef { label: Some( @@ -131,6 +241,30 @@ expression: "model.rules[1].alts" ), }, stable_accessor: true, + choice_branch: [], + choice_arity: [], + choice_spans: [], + group_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, + group_spans: [], + branch_spans: [], + leading_terminal: false, + span: Some( + ( + 201, + 202, + ), + ), + branch_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, }, ], children: { @@ -173,6 +307,30 @@ expression: "model.rules[1].alts" ), }, stable_accessor: true, + choice_branch: [], + choice_arity: [], + choice_spans: [], + group_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, + group_spans: [], + branch_spans: [], + leading_terminal: true, + span: Some( + ( + 238, + 241, + ), + ), + branch_local_cardinality: ChildCardinality { + min: 1, + max: Some( + 1, + ), + }, }, ], children: { diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_branch_hazard_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_branch_hazard_context.snap new file mode 100644 index 00000000..4efe565f --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_branch_hazard_context.snap @@ -0,0 +1,44 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: context(name) +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary(&self) -> Result, MissingChildError> { + __rule_children(self.__node, 4) + .next() + .map(|node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + .ok_or_else(|| MissingChildError::new("BranchHazardContext", "unary")) + } + pub fn star_token(&self) -> Option> { + __token_children(self.__node, 8) + .next() + .map(TerminalNode::new) + } + pub fn num_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 14) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("BranchHazardContext", "NUM")) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_branch_rival_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_branch_rival_context.snap new file mode 100644 index 00000000..75897d14 --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_branch_rival_context.snap @@ -0,0 +1,39 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: context(name) +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary(&self) -> Result, MissingChildError> { + __rule_children(self.__node, 4) + .next() + .map(|node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + .ok_or_else(|| MissingChildError::new("BranchRivalContext", "unary")) + } + pub fn num_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 14) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("BranchRivalContext", "NUM")) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_closed_repeat_prefix_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_closed_repeat_prefix_context.snap new file mode 100644 index 00000000..eefdd3d2 --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_closed_repeat_prefix_context.snap @@ -0,0 +1,39 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"ClosedRepeatPrefixContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn star_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 8).map(TerminalNode::new) + } + pub fn ident_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 13).map(TerminalNode::new) + } + pub fn num_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 14) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("ClosedRepeatPrefixContext", "NUM")) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_exhaustive_prefix_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_exhaustive_prefix_context.snap new file mode 100644 index 00000000..d1a3e28d --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_exhaustive_prefix_context.snap @@ -0,0 +1,43 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"ExhaustivePrefixContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary_children(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn num_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 14) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("ExhaustivePrefixContext", "NUM")) + } + pub fn pick(&self) -> Result, MissingChildError> { + __rule_children(self.__node, 4) + .nth(1) + .map(|node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + .ok_or_else(|| MissingChildError::new("ExhaustivePrefixContext", "pick")) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_grouped_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_grouped_context.snap new file mode 100644 index 00000000..f20c3eb9 --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_grouped_context.snap @@ -0,0 +1,70 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"GroupedContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary_children(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn in_token(&self) -> Option> { + __token_children(self.__node, 7) + .next() + .map(TerminalNode::new) + } + pub fn star_token(&self) -> Option> { + __token_children(self.__node, 8) + .next() + .map(TerminalNode::new) + } + pub fn ident_token(&self) -> Option> { + __token_children(self.__node, 13) + .next() + .map(TerminalNode::new) + } + pub fn num_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 14) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("GroupedContext", "NUM")) + } + pub fn comma_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 15).map(TerminalNode::new) + } + pub fn doc(&self) -> Option> { + __labeled_token_children(self.__node, 13) + .nth(0) + .map(TerminalNode::new) + } + pub fn errors(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .skip(0) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn oneway(&self) -> Option> { + __labeled_token_children(self.__node, 8) + .nth(0) + .map(TerminalNode::new) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_inner_choice_arity_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_inner_choice_arity_context.snap new file mode 100644 index 00000000..4c128b39 --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_inner_choice_arity_context.snap @@ -0,0 +1,39 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"InnerChoiceArityContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary_children(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn param(&self) -> Option> { + __rule_children(self.__node, 8) + .next() + .map(|node| ParamContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn num_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 14).map(TerminalNode::new) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_merged_repeats_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_merged_repeats_context.snap new file mode 100644 index 00000000..b3eebada --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_merged_repeats_context.snap @@ -0,0 +1,44 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"MergedRepeatsContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn ident_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 13).map(TerminalNode::new) + } + pub fn num_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 14).map(TerminalNode::new) + } + pub fn lparen_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 16) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("MergedRepeatsContext", "LPAREN")) + } + pub fn last_pick(&self) -> Option> { + __labeled_token_children_matching(self.__node, &[13, 14]) + .skip(0).last() + .map(TerminalNode::new) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_merged_rivals_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_merged_rivals_context.snap new file mode 100644 index 00000000..f58de5bd --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_merged_rivals_context.snap @@ -0,0 +1,40 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"MergedRivalsContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn ident_token(&self) -> Option> { + __token_children(self.__node, 13) + .next() + .map(TerminalNode::new) + } + pub fn num_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 14).map(TerminalNode::new) + } + pub fn pick(&self) -> Option> { + __labeled_token_children_matching(self.__node, &[13, 14]) + .nth(0) + .map(TerminalNode::new) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_mixed_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_mixed_context.snap new file mode 100644 index 00000000..1eb721e7 --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_mixed_context.snap @@ -0,0 +1,66 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"MixedContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary_children(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn param_children(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 8) + .map(move |node| ParamContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn in_token(&self) -> Option> { + __token_children(self.__node, 7) + .next() + .map(TerminalNode::new) + } + pub fn comma_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 15).map(TerminalNode::new) + } + pub fn lparen_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 16) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("MixedContext", "LPAREN")) + } + pub fn rparen_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 17) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("MixedContext", "RPAREN")) + } + pub fn errors(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .skip(1) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn name(&self) -> Result, MissingChildError> { + __rule_children(self.__node, 4) + .nth(0) + .map(|node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + .ok_or_else(|| MissingChildError::new("MixedContext", "name")) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_mixed_unbounded_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_mixed_unbounded_context.snap new file mode 100644 index 00000000..30d42d55 --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_mixed_unbounded_context.snap @@ -0,0 +1,37 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: context(name) +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary_children(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn in_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 7) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("MixedUnboundedContext", "IN")) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_nested_exhaustive_prefix_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_nested_exhaustive_prefix_context.snap new file mode 100644 index 00000000..8927afdb --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_nested_exhaustive_prefix_context.snap @@ -0,0 +1,43 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"NestedExhaustivePrefixContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary_children(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn num_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 14) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("NestedExhaustivePrefixContext", "NUM")) + } + pub fn chosen(&self) -> Result, MissingChildError> { + __rule_children(self.__node, 4) + .nth(1) + .map(|node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + .ok_or_else(|| MissingChildError::new("NestedExhaustivePrefixContext", "chosen")) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_nested_group_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_nested_group_context.snap new file mode 100644 index 00000000..6743b5ac --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_nested_group_context.snap @@ -0,0 +1,43 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"NestedGroupContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn ident_token(&self) -> Option> { + __token_children(self.__node, 13) + .next() + .map(TerminalNode::new) + } + pub fn num_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 14) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("NestedGroupContext", "NUM")) + } + pub fn deep(&self) -> Option> { + __labeled_token_children(self.__node, 13) + .nth(0) + .map(TerminalNode::new) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_optional_prefix_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_optional_prefix_context.snap new file mode 100644 index 00000000..d81465c6 --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_optional_prefix_context.snap @@ -0,0 +1,37 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"OptionalPrefixContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary_children(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn num_token(&self) -> Result, MissingChildError> { + __token_children(self.__node, 14) + .next() + .map(TerminalNode::new) + .ok_or_else(|| MissingChildError::new("OptionalPrefixContext", "NUM")) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_overlapping_group_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_overlapping_group_context.snap new file mode 100644 index 00000000..19d96cbf --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_overlapping_group_context.snap @@ -0,0 +1,35 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"OverlappingGroupContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn ident_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 13).map(TerminalNode::new) + } + pub fn num_token(&self) -> Option> { + __token_children(self.__node, 14) + .next() + .map(TerminalNode::new) + } +} diff --git a/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_path_restricted_context.snap b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_path_restricted_context.snap new file mode 100644 index 00000000..6e00ba57 --- /dev/null +++ b/src/bin/snapshots/antlr4_rust_gen__tests__multi_alternative_label_path_restricted_context.snap @@ -0,0 +1,39 @@ +--- +source: src/bin/antlr4-rust-gen.rs +expression: "context(\"PathRestrictedContext\")" +--- + + pub fn child_count(&self) -> usize { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.child_count(), + __GeneratedRuleContext::Active { context, .. } => context.child_count(), + } + } + + pub fn start(&self) -> __GeneratedTokenView { + let token = match &self.__node { + __GeneratedRuleContext::Stored(node) => node.start(), + __GeneratedRuleContext::Active { context, tokens, .. } => context.start(tokens), + }; + __GeneratedTokenView { text: token.map(|token| token.text_or_empty().to_owned()).unwrap_or_default() } + } + + pub fn text(&self) -> String { + match &self.__node { + __GeneratedRuleContext::Stored(node) => node.text(), + __GeneratedRuleContext::Active { context, storage, tokens } => context.text(storage, tokens), + } + } + pub fn unary_children(&self) -> impl Iterator> + '_ { + __rule_children(self.__node, 4) + .map(move |node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } + pub fn num_tokens(&self) -> impl Iterator> + '_ { + __token_children(self.__node, 14).map(TerminalNode::new) + } + pub fn tail(&self) -> Option> { + __rule_children(self.__node, 4) + .nth(1) + .map(|node| UnaryContext::__from_child_node(node, self.__invocation_states.as_deref())) + } +} diff --git a/src/bin_support/embedded.rs b/src/bin_support/embedded.rs index 38be6215..600b4f54 100644 --- a/src/bin_support/embedded.rs +++ b/src/bin_support/embedded.rs @@ -14,7 +14,7 @@ //! references with labels (for `$label.attr` occurrence resolution), and //! `@members` bodies split into struct fields, impl items, and module items. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Write as _; use std::io; @@ -59,6 +59,24 @@ impl ChildCardinality { } } +/// Suffix marking a label that was temporarily renamed while resolving a *sibling* +/// declaration of the same label in isolation. Grammar labels are identifiers, so +/// this cannot collide with a real one. +const SIBLING_DECLARATION_SUFFIX: &str = " (sibling declaration)"; + +/// One enclosing block: its byte extent, and whether its own quantifier relaxes +/// the lower bound of the elements inside it. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct GroupSpan { + pub(crate) start: usize, + pub(crate) end: usize, + /// `true` for `(…)?` / `(…)*` — the group may contribute nothing. + pub(crate) optional: bool, + /// `true` for `(…)*` / `(…)+` — the group may run more than once, so the number + /// of children it contributes is not fixed even when it is known to have run. + pub(crate) repeated: bool, +} + /// One element reference inside an alternative: a rule ref, token ref, or a /// labeled sub-block, in source order. #[derive(Clone, Debug, Eq, PartialEq)] @@ -77,6 +95,109 @@ pub(crate) struct ElementRef { /// label accessor. Single-alternative EBNF groups preserve it; choices opt /// out because their flattened CST children do not retain the chosen path. pub(crate) stable_accessor: bool, + /// `(choice id, alternative index)` for every enclosing *multi*-alternative + /// block, outermost first. Two refs that share a choice id but sit in + /// different alternatives of it are mutually exclusive: no parse contains + /// both. Empty means the ref is on the rule's own sequential path. + /// + /// The whole ancestry is kept, not just the innermost choice: for + /// `((x=e | f) | e)` the labeled `x` and the trailing `e` are separated by + /// the *outer* choice, which an innermost-only tag would lose. + pub(crate) choice_branch: Vec<(usize, usize)>, + /// Alternative count of each choice named in `choice_branch`, in the same + /// order. Recorded at collection time because an *empty* alternative emits no + /// ref at all, so the branch count cannot be recovered from the refs alone — + /// `(a=A | )` would otherwise look like a one-branch choice that always + /// yields an `A`. + pub(crate) choice_arity: Vec, + /// Byte span of each enclosing choice *block*, in the same order as + /// `choice_branch`. An action lies inside a branch only when its offset falls + /// within the block's span — refs alone cannot tell `(A | xs+=A) {…}` (action + /// after the group) from `(A x=A {…} | B)` (action inside it), since both put + /// branch refs on either side of the action. + pub(crate) choice_spans: Vec<(usize, usize)>, + /// Cardinality with every *enclosing* quantifier and choice split treated as + /// satisfied — only this element's own EBNF suffix applies. An action inside + /// `(A x=A {…})?` runs only when the group matched, so on that path the + /// preceding `A` is exactly-once even though both `cardinality` and + /// `branch_local_cardinality` report `0..1` from the group's `?`. + pub(crate) group_local_cardinality: ChildCardinality, + /// Byte span and lower-bound-relaxing flag of *every* enclosing block, including + /// single-alternative groups that `choice_spans` omits. The flag says whether + /// that group's own quantifier is what made this element optional (`(…)?` or + /// `(…)*`), so a group known taken can have *its* contribution removed without + /// disturbing the others: in `((q) x=q {…})?` the outer `?` is satisfied when the + /// action runs while the inner `(q)` is mandatory and already closed. + pub(crate) group_spans: Vec, + /// Byte span of each enclosing choice *alternative* (the branch itself), in the + /// same order as `choice_branch`. Lets an action be attributed to the branch + /// whose text contains it, including a branch holding only actions or + /// predicates — such a branch emits no `ElementRef` at all, so ref spans alone + /// would attribute the action to a neighbouring branch. + pub(crate) branch_spans: Vec<(usize, usize)>, + /// Whether no terminal can precede this element on its own parse path. Only + /// then do a block read (which indexes every terminal child) and a token read + /// (which indexes only same-type children) provably agree, so this is what + /// makes a mixed-mode merge sound — occurrence zero alone is not enough, since + /// a token-mode zero can still sit at a non-zero terminal position. + pub(crate) leading_terminal: bool, + /// Byte span of the element in the grammar source, when known. A mid-rule + /// action executes at *its* source position, so only refs that start before + /// the action's offset have been matched when its body runs. + pub(crate) span: Option<(usize, usize)>, + /// Cardinality this element would have if every enclosing choice took the + /// branch containing it — i.e. with only the *quantifiers* applied, not the + /// `min: 0` that `choice_branch` membership imposes. + /// + /// `(a=A | b=A) x=A` and `(a=A | b=A)? x=A` give their branch refs the same + /// `cardinality` (`0..1`), yet the first choice always yields one `A` and the + /// second may yield none. Only this field separates them. + pub(crate) branch_local_cardinality: ChildCardinality, +} + +impl ElementRef { + /// Whether `self` and `other` can both appear in one parse. They cannot when + /// any choice encloses both in *different* alternatives. + pub(crate) fn can_coexist_with(&self, other: &Self) -> bool { + !self.choice_branch.iter().any(|(choice, branch)| { + other + .choice_branch + .iter() + .any(|(other_choice, other_branch)| { + choice == other_choice && branch != other_branch + }) + }) + } + + /// Drops the choices `discard` selects, keeping every parallel choice array + /// aligned with `choice_branch`. + /// + /// `choice_arity`, `choice_spans`, and `branch_spans` are indexed *by position* + /// in `choice_branch`, so removing an entry must remove the same position from + /// each. Truncating to the surviving length instead keeps the outermost + /// entries — which is wrong whenever an *outer* choice is the one dropped: + /// `((q | q | b) x=q | c)` would then read the outer choice's arity of 2 + /// against the surviving inner choice, mistaking a three-way choice for an + /// exhaustive two-way one. + pub(crate) fn retain_choices(&mut self, mut keep: impl FnMut(usize) -> bool) { + let mask = self + .choice_branch + .iter() + .map(|&(choice, _)| keep(choice)) + .collect::>(); + fn retain_by_mask(list: &mut Vec, mask: &[bool]) { + let mut index = 0; + list.retain(|_| { + let kept = mask.get(index).copied().unwrap_or(true); + index += 1; + kept + }); + } + retain_by_mask(&mut self.choice_branch, &mask); + retain_by_mask(&mut self.choice_arity, &mask); + retain_by_mask(&mut self.choice_spans, &mask); + retain_by_mask(&mut self.branch_spans, &mask); + } } /// One top-level alternative of a parser rule. @@ -358,26 +479,1002 @@ impl TranslationCtx<'_> { } /// Resolves a label to `(ref, occurrence-among-same-target-in-alt)`. + /// + /// Every read `translate_element_read` can emit is a *positional* query over + /// the flattened CST children — `nth(i)` for a single label, "all children of + /// this target" for a list label, "the last terminal child" for a block + /// label. None of those retain which grammar branch built a child, so a + /// label only resolves when its read provably selects the label's own + /// element and nothing else. When it cannot, this returns `None` and the + /// caller fails loudly rather than translating to a silently wrong read. + /// + /// The conditions that make a read unfaithful, by label kind: + /// + /// * **single** — a preceding ref with inexact cardinality (sibling branches + /// of a choice are mutually exclusive and report `min: 0`, so counting + /// them indexes past what the parse built), or an *optional* label with a + /// following same-target child that slides into its position when absent; + /// * **list** — any same-target child outside the label, since the read + /// cannot exclude it; + /// * **block** — a following terminal, which would become the `last()` the + /// read takes; + /// * **any kind** — a second declaration of the same label that this one + /// read cannot also serve. fn resolve_label(&self, label: &str) -> Option<(ElementRef, usize)> { let rule = self.rule(); - let alts: Vec<&AltModel> = self + if self.site == ActionSite::Init { + // An `@init` body runs at rule entry, before any child exists, so every + // read over children is empty and nothing can pollute it — no hazard + // applies. Most reads degrade gracefully (an iterator yields nothing, a + // `.text` read yields `""`), but a *scalar rule* label lowers to + // `.nth(i).expect("labeled rule child")`, which panics on every parse. + // Decline that one rather than emit code that cannot run. + let element = rule + .alts + .iter() + .flat_map(|alt| alt.refs.iter()) + .find(|element| element.label.as_deref() == Some(label))?; + let panics_when_absent = + !element.is_list && element.token_types.is_empty() && !element.target.is_empty(); + return (!panics_when_absent).then(|| (element.clone(), 0)); + } + if let Some((offset, alt)) = self .body_offset - .and_then(|offset| rule.alt_at(offset)) - .map_or_else(|| rule.alts.iter().collect(), |alt| vec![alt]); - for alt in alts { - let mut occurrence_by_target: BTreeMap<&str, usize> = BTreeMap::new(); - for element in &alt.refs { - let occurrence = occurrence_by_target - .entry(element.target.as_str()) - .or_insert(0); - let current = *occurrence; - *occurrence += 1; - if element.label.as_deref() == Some(label) { - return Some((element.clone(), current)); + .and_then(|offset| rule.alt_at(offset).map(|alt| (offset, alt))) + { + // A mid-rule action executes at its own source position, so only refs + // starting before it have been matched, and only branches enclosing + // that position can have run. + return Self::resolve_label_in_alt(alt, label, Some(offset)); + } + // `@after` / `@init` bodies are not scoped to an alternative, so the + // label may be declared in several. One read has to serve whichever + // alternative the parse took: taking the first match would emit that + // alternative's lookup and silently yield a default on the others. + let mut resolved: Option<(ElementRef, usize)> = None; + let mut non_declaring = Vec::new(); + for alt in &rule.alts { + let declares = alt + .refs + .iter() + .any(|element| element.label.as_deref() == Some(label)); + if !declares { + non_declaring.push(alt); + continue; + } + // An `@after` / `@init` body runs whichever branch the parse took, so + // a sibling branch's match *can* be the child present when the read + // executes — sibling exclusion does not apply here. + let candidate = Self::resolve_label_in_alt(alt, label, None)?; + if resolved + .as_ref() + .is_some_and(|existing| !Self::same_label_read(existing, &candidate)) + { + return None; + } + resolved = Some(candidate); + } + let (element, occurrence) = resolved?; + // An alternative that never declares the label leaves it unset, so the + // read must come up empty there. It will not if that alternative happens + // to build a child the read would select anyway (`r : x=A | A`), which + // would report a value for a label the parse never bound. + for alt in non_declaring { + if Self::alt_can_satisfy_read(alt, &element, occurrence) { + return None; + } + } + Some((element, occurrence)) + } + + /// Whether `alt` builds a child that the read for `element` would select, + /// even though `alt` does not declare the label. + fn alt_can_satisfy_read(alt: &AltModel, element: &ElementRef, occurrence: usize) -> bool { + // Route on `is_block`, matching `translate_element_read`: a *literal* label + // (`x='a'`) is block-mode yet keeps a non-empty source target, so keying on + // an empty target here would fall through to token-type matching while the + // read actually ignores token type entirely. + if element.is_block { + // A positional block read selects a terminal by index, so the + // alternative can satisfy it whenever it builds a terminal there. + return alt + .refs + .iter() + .filter(|candidate| { + !candidate.token_types.is_empty() && candidate.cardinality.max != Some(0) + }) + .any(|candidate| Self::can_occupy_terminal_index(alt, candidate, occurrence)); + } + // The read queries by token *type*, so a differently-spelled terminal with + // the same type is the same child (`A : 'a';` makes `A` and `'a'` one). + let same_read_target = |candidate: &ElementRef| { + if element.token_types.is_empty() || candidate.token_types.is_empty() { + return candidate.target == element.target; + } + candidate + .token_types + .iter() + .any(|token_type| element.token_types.contains(token_type)) + }; + // The most matching children *one parse* can build. Sequential refs add; + // branches of a choice are alternatives, so the widest branch wins. Nested + // choices must fold innermost-first — reducing each choice independently + // and then summing would double-count, rejecting valid reads such as + // `q x=q | ((q|b)|(q|c))` where the second alternative builds one `q`. + let available = Self::widest_child_count( + alt.refs + .iter() + .filter(|candidate| same_read_target(candidate)), + ); + // A list read selects any same-target child; a positional read needs one + // at `occurrence`. An unbounded count can always reach either. + available.is_none_or(|available| available > occurrence) + } + + /// Whether `candidate` can occupy terminal index `occurrence` on its own parse + /// path. A *repeated* candidate spans a range of positions rather than one, so + /// comparing a single index would miss it: in `C x=(A | B) | (D | E)+` the + /// repeated group starts at 0 yet also covers 1, where `x` reads. + fn can_occupy_terminal_index( + alt: &AltModel, + candidate: &ElementRef, + occurrence: usize, + ) -> bool { + // `usize::MAX` is the sentinel for "no fixed index, the read falls back to + // `last()`" — not a position. Any terminal the alternative builds can be that + // last child, so every candidate can occupy it. + if occurrence == usize::MAX { + return true; + } + let Some(start) = Self::exact_terminal_index(alt, candidate) else { + // No fixed start: the candidate could be anywhere. + return true; + }; + if start > occurrence { + return false; + } + // Unbounded repetition reaches every later index. + candidate + .cardinality + .max + .is_none_or(|max| start + max > occurrence) + } + + /// Index among terminal children at which `element` sits on its own parse + /// path, or `None` when that index is not fixed. + fn exact_terminal_index(alt: &AltModel, element: &ElementRef) -> Option { + let position = alt + .refs + .iter() + .position(|candidate| std::ptr::eq(candidate, element))?; + // `can_coexist_with` keeps everything a ref *after* the choice coexists + // with — both branches — so it does not by itself select one path. Strip the + // tags of choices the element is inside (those branches are taken on its + // path) and leave the rest tagged, so `exact_child_count` still demands + // cross-branch agreement for choices the element is not part of. + let on_path = alt.refs[..position] + .iter() + .filter(|candidate| { + !candidate.token_types.is_empty() && candidate.can_coexist_with(element) + }) + .cloned() + .map(|mut candidate| { + candidate.retain_choices(|choice| { + !element + .choice_branch + .iter() + .any(|&(taken, _)| taken == choice) + }); + if candidate.choice_branch.is_empty() { + candidate.cardinality = candidate.group_local_cardinality; + } + candidate + }) + .collect::>(); + Self::exact_child_count(on_path.iter(), false) + } + + /// Total children contributed by `refs`, or `None` when that total is not the + /// same on every parse. + /// + /// Refs are grouped by their enclosing choices and folded **innermost-first**: + /// once a choice's branches agree, its count is attributed to the enclosing + /// branch that contains it, so nested exhaustive choices + /// (`((a=A | b=A) | c=A)`) stay exact while nested *differing* ones + /// (`((a=A | b=B) C | D)`) correctly do not. + /// + /// `restricted_to_one_path` says the caller already filtered `refs` down to a + /// single parse path. Cross-branch agreement is then meaningless — the other + /// branches were removed on purpose — so each surviving branch simply counts. + fn exact_child_count<'a>( + refs: impl Iterator, + restricted_to_one_path: bool, + ) -> Option { + let refs = refs.collect::>(); + let mut total = 0_usize; + let mut per_branch: BTreeMap<(usize, usize), Option> = BTreeMap::new(); + // Arity is recorded, not observed: an *empty* alternative emits no ref, so + // `(a=A | )` would otherwise look like a one-branch choice. + let mut arity_of_choice: BTreeMap = BTreeMap::new(); + let mut depth_of_choice: BTreeMap = BTreeMap::new(); + let mut ancestry: BTreeMap<(usize, usize), Vec<(usize, usize)>> = BTreeMap::new(); + for candidate in &refs { + for (depth, (&(choice, branch), &arity)) in candidate + .choice_branch + .iter() + .zip(&candidate.choice_arity) + .enumerate() + { + arity_of_choice.insert(choice, arity); + // A choice's depth is where it sits in the ancestry; take the + // *shallowest* sighting, since that is its real nesting level + // (a deeper ref lists it at the same index, never a lower one). + depth_of_choice + .entry(choice) + .and_modify(|existing| *existing = (*existing).min(depth)) + .or_insert(depth); + ancestry.insert((choice, branch), candidate.choice_branch[..=depth].to_vec()); + } + } + for candidate in &refs { + let max = candidate.cardinality.max?; + // Within its own branch a ref contributes its branch-local count; the + // `min: 0` that branch membership imposes is not optionality. + let local = candidate.branch_local_cardinality; + let exact = (local.min == max && local.max == Some(max)).then_some(max); + match candidate.choice_branch.last() { + None => total = total.saturating_add(exact?), + Some(&key) => { + let slot = per_branch.entry(key).or_insert(Some(0)); + *slot = match (*slot, exact) { + (Some(sum), Some(next)) => Some(sum.saturating_add(next)), + _ => None, + }; + } + } + } + // Deepest choices first, so an inner result rolls up into its parent branch. + // The list is rebuilt from `per_branch` each pass, because folding an inner + // choice *creates* an entry for its parent that must then fold in turn. + let mut processed: BTreeSet = BTreeSet::new(); + // Deepest unprocessed choice still holding entries. Folding one creates an + // entry for its parent, so the candidate set is re-examined every pass. + while let Some(choice) = per_branch + .keys() + .map(|(choice, _)| *choice) + .filter(|choice| !processed.contains(choice)) + .max_by_key(|choice| depth_of_choice.get(choice).copied().unwrap_or(0)) + { + processed.insert(choice); + let counts = per_branch + .iter() + .filter(|((candidate, _), _)| *candidate == choice) + .map(|((_, branch), count)| (*branch, *count)) + .collect::>(); + if counts.is_empty() { + continue; + } + let agreed = if restricted_to_one_path { + // One path survives, so there is nothing to agree with. + counts.iter().try_fold(0_usize, |sum, (_, count)| { + Some(sum.saturating_add((*count)?)) + })? + } else { + let expected = arity_of_choice.get(&choice).copied()?; + let first = counts.first().and_then(|(_, count)| *count)?; + if counts.len() != expected || counts.iter().any(|(_, count)| *count != Some(first)) + { + return None; + } + first + }; + let parent = counts.first().and_then(|(branch, _)| { + ancestry.get(&(choice, *branch)).and_then(|chain| { + chain + .split_last() + .and_then(|(_, rest)| rest.last().copied()) + }) + }); + for (branch, _) in &counts { + per_branch.remove(&(choice, *branch)); + } + match parent { + Some(parent_key) => { + let slot = per_branch.entry(parent_key).or_insert(Some(0)); + *slot = slot.map(|sum| sum.saturating_add(agreed)); + } + None => total = total.saturating_add(agreed), + } + } + Some(total) + } + + /// Greatest number of children `refs` can contribute on any single parse, or + /// `None` when unbounded. Sequential refs add; branches of a choice are + /// alternatives, so the widest one wins. Choices fold innermost-first so a + /// nested choice's maximum lands in its enclosing branch rather than being + /// summed alongside it. + fn widest_child_count<'a>(refs: impl Iterator) -> Option { + let refs = refs.collect::>(); + let mut total = Some(0_usize); + let mut per_branch: BTreeMap<(usize, usize), Option> = BTreeMap::new(); + let mut depth_of_choice: BTreeMap = BTreeMap::new(); + let mut ancestry: BTreeMap<(usize, usize), Vec<(usize, usize)>> = BTreeMap::new(); + for candidate in &refs { + for (depth, &(choice, branch)) in candidate.choice_branch.iter().enumerate() { + depth_of_choice + .entry(choice) + .and_modify(|existing| *existing = (*existing).min(depth)) + .or_insert(depth); + ancestry.insert((choice, branch), candidate.choice_branch[..=depth].to_vec()); + } + } + let add = |slot: &mut Option, value: Option| { + *slot = match (*slot, value) { + (Some(total), Some(next)) => Some(total.saturating_add(next)), + _ => None, + }; + }; + for candidate in &refs { + match candidate.choice_branch.last() { + None => add(&mut total, candidate.cardinality.max), + Some(&key) => { + let slot = per_branch.entry(key).or_insert(Some(0)); + add(slot, candidate.cardinality.max); + } + } + } + let mut processed: BTreeSet = BTreeSet::new(); + while let Some(choice) = per_branch + .keys() + .map(|(choice, _)| *choice) + .filter(|choice| !processed.contains(choice)) + .max_by_key(|choice| depth_of_choice.get(choice).copied().unwrap_or(0)) + { + processed.insert(choice); + let counts = per_branch + .iter() + .filter(|((candidate, _), _)| *candidate == choice) + .map(|((_, branch), count)| (*branch, *count)) + .collect::>(); + if counts.is_empty() { + continue; + } + let widest = counts + .iter() + .try_fold(0_usize, |widest, (_, count)| Some(widest.max((*count)?))); + let parent = counts.first().and_then(|(branch, _)| { + ancestry.get(&(choice, *branch)).and_then(|chain| { + chain + .split_last() + .and_then(|(_, rest)| rest.last().copied()) + }) + }); + for (branch, _) in &counts { + per_branch.remove(&(choice, *branch)); + } + match parent { + Some(parent_key) => { + let slot = per_branch.entry(parent_key).or_insert(Some(0)); + add(slot, widest); + } + None => add(&mut total, widest), + } + } + total + } + + /// Whether the action sits inside the branch that separates `element` from + /// `candidate` — i.e. inside the label's own branch of the choice that makes the + /// two mutually exclusive. Only then can the candidate be dismissed: the action + /// cannot run on the branch that would supply it. + /// + /// Judged per choice rather than rule-wide, because an action confined to some + /// *unrelated* later choice says nothing about an earlier one. + fn action_inside_separating_branch( + element: &ElementRef, + candidate: &ElementRef, + action_branches: Option<&[(usize, usize)]>, + ) -> bool { + let Some(branches) = action_branches else { + // An unscoped body runs whatever branch matched. + return false; + }; + element.choice_branch.iter().any(|&(choice, branch)| { + // A choice that separates them... + candidate + .choice_branch + .iter() + .any(|&(other, other_branch)| other == choice && other_branch != branch) + // ...and whose label-side branch encloses the action. + && branches.contains(&(choice, branch)) + }) + } + + /// Whether two per-alternative resolutions lower to the same read, so one + /// translation can stand for both. The fields compared are exactly those + /// `translate_element_read` consumes to pick a read: list mode, block mode, + /// and the target it queries. Two block labels are equivalent regardless of + /// their token sets, because the block read ignores them. + fn same_label_read(left: &(ElementRef, usize), right: &(ElementRef, usize)) -> bool { + if left.0.is_list != right.0.is_list { + return false; + } + // Token-backed resolutions can be equivalent across source forms (`x=A` is + // token-mode, `x='a'` is block-mode) because both lower to the same + // `child_tokens(A)` query — but their occurrences are counted in different + // units: a block read indexes *every* terminal child, a token read only + // same-type children. `x='a' | A x=A` has both reporting 1 while meaning + // different children, and `A x='a' B | A x=A C` has them meaning the same + // child while reporting 1 and 1 only by coincidence. + // + // Rather than guess, mixed-mode pairs merge only when *neither* has anything + // ahead of it — occurrence zero in both systems *and* no preceding terminal + // of any type. Occurrence zero alone is not enough: in `B x=A | x='a'` the + // symbolic side reports same-token occurrence 0 while sitting at terminal + // position 1, and merging it with the literal's terminal 0 exposed `B`. + // Same-mode pairs compare directly. + if !left.0.token_types.is_empty() && left.0.token_types == right.0.token_types { + if left.0.is_block == right.0.is_block { + return left.1 == right.1; + } + // The mixed-mode merge works because both sides lower to the same + // *scalar* `nth(0)`. A list read has no such common form: token mode + // yields an iterator (`child_tokens(A)`), block mode a `String` — it + // resolves `target` against `ctx.token_types`, and a literal target + // (`xs+='a'`) is not a key there, so it falls through to the positional + // block read. Merging the two emitted `.collect()` on a `String`. + if left.0.is_list { + return false; + } + return left.1 == 0 + && right.1 == 0 + && left.0.leading_terminal + && right.0.leading_terminal; + } + if left.0.is_block != right.0.is_block { + return false; + } + // Block reads are positional now, so two block labels agree only when + // their terminal indices do: `x=(A | B) | C x=(A | B)` puts `x` at 0 and 1. + if left.0.is_block && right.0.is_block { + return left.1 == right.1; + } + left.1 == right.1 && left.0.target == right.0.target + } + + /// `action_offset` is the byte offset of a *mid-rule* action body, or `None` + /// for an `@after` / `@init` body that runs after the whole rule. + fn resolve_label_in_alt( + alt: &AltModel, + label: &str, + action_offset: Option, + ) -> Option<(ElementRef, usize)> { + let declarations = alt + .refs + .iter() + .filter(|element| element.label.as_deref() == Some(label)) + .collect::>(); + let element = *declarations.first()?; + // A renamed sibling declaration (see the isolation below) still counts as + // declaring the label: it binds it too, so it can never impersonate it. + let declares_label = |candidate: &ElementRef| { + candidate.label.as_deref().is_some_and(|name| { + name == label || name.strip_suffix(SIBLING_DECLARATION_SUFFIX) == Some(label) + }) + }; + // The generated read queries by rule index or *token type*, so a + // differently-spelled terminal with the same type is the same child as + // far as the read is concerned (`A : 'a';` makes `A` and `'a'` aliases). + let same_target = |candidate: &ElementRef| { + if candidate.cardinality.max == Some(0) { + return false; + } + // A token *group* has no target yet still contributes a child of the + // label's type when their sets overlap (`(xs+=A)? (A | B)`), so match + // on token types whenever both sides have them — empty target or not. + if element.token_types.is_empty() || candidate.token_types.is_empty() { + return !candidate.target.is_empty() && candidate.target == element.target; + } + candidate + .token_types + .iter() + .any(|token_type| element.token_types.contains(token_type)) + }; + // Whether `candidate` has been matched by the time the action body runs. + // A mid-rule action executes at its source position, so a ref that starts + // after it is still in the future and cannot affect the read; a ref in a + // branch the action does not sit inside cannot have run either. An + // `@after` body runs after everything, so every ref counts. + // The choice ancestry the action itself sits in, derived from spans: the + // action belongs to the innermost branch whose refs bracket its offset. + // Refs from any *other* branch of those choices cannot have run. + // The choice branches that syntactically *enclose* the action. A branch + // encloses it only when the branch has a ref before the action AND no + // sibling branch of the same choice has a ref after it — a sibling ref + // afterwards means the choice is still open, i.e. the action follows the + // whole group rather than sitting inside one branch. Nearest-preceding-ref + // alone gets `(A | xs+=A) {…}` wrong, marking the action as confined to the + // final branch when it actually runs for either. + // The choice branches that syntactically enclose the action: those whose + // *block* span contains the action's offset. Ref spans alone cannot decide + // this — `(A | xs+=A) {…}` and `(A x=A {…} | B)` both put branch refs on + // either side of the action — but the block's own extent can: in the first + // the action sits after the closing paren, in the second inside it. + let action_branches = action_offset.map(|offset| { + // For each enclosing choice, the *one* branch the action sits in: the + // branch whose own refs span the offset. Refs of an earlier sibling + // also precede the action and share the choice's block span, so + // collecting every preceding ref's tags would record mutually + // conflicting branches (`(B | C x=A? A {…})` would claim both). + // + // A branch contains the action when some ref of it starts before the + // offset and no ref of a *later* sibling does — source order means a + // later branch having started implies the action is past this one. + let mut chosen: Vec<(usize, usize)> = Vec::new(); + // Every (choice, branch) whose *branch text* contains the action. This + // reads the branch's own span rather than inferring from ref positions, + // so a branch holding only an action or predicate — which emits no + // `ElementRef` — is still identified (`x=A? (A | {$x.text})`). + for candidate in &alt.refs { + for ((&key, &(branch_start, branch_end)), &(choice_start, choice_end)) in candidate + .choice_branch + .iter() + .zip(&candidate.branch_spans) + .zip(&candidate.choice_spans) + { + let inside_choice = choice_start <= offset && offset < choice_end; + let inside_branch = branch_start <= offset && offset < branch_end; + if inside_choice && inside_branch && !chosen.contains(&key) { + chosen.push(key); + } + } + } + // A choice enclosing the action but with no branch claiming it means the + // action sits in a ref-free branch: nothing of that branch has matched, + // so record it as its own branch so siblings are excluded. + for candidate in &alt.refs { + for (&(choice, _), &(choice_start, choice_end)) in + candidate.choice_branch.iter().zip(&candidate.choice_spans) + { + if choice_start <= offset + && offset < choice_end + && !chosen + .iter() + .any(|&(chosen_choice, _)| chosen_choice == choice) + { + // usize::MAX marks "a branch with no refs of its own". + chosen.push((choice, usize::MAX)); + } + } + } + chosen + }); + let branch_confined = action_branches + .as_ref() + .is_some_and(|branches| !branches.is_empty()); + // A ref inside a group that also encloses the action has run: the action + // only executes when that group was taken. Its cardinality still reports + // `min: 0` from the group's `?`, so use the quantifier-free figure — + // `(A x=A {…})?` has exactly one `A` before the label whenever the action + // runs at all. + // + // *Every* group the ref sits in must enclose the action, not merely one: an + // inner group that closed before the action proves nothing, so + // `((q)? x=q {…})?` must not treat the inner `(q)?` as matched. + // A ref is exactly-once on the action's path when every group that *relaxed* + // its lower bound is one the action also sits inside — the action running + // proves those groups were taken. Groups that impose nothing (a mandatory + // `(…)`) are irrelevant whether or not they enclose the action, so requiring + // all of them to would reject `((q) x=q {…})?`, where the inner group is + // mandatory and already closed. + let on_taken_group = |candidate: &ElementRef| { + action_offset.is_some_and(|offset| { + let encloses = |group: &GroupSpan| group.start <= offset && offset < group.end; + // A *repeated* group that has closed still contributes an unknown + // number of children, so knowing it ran does not fix the count: + // `((A B)+ x=A {…})?` has a variable run of `A` before the label. + if candidate + .group_spans + .iter() + .any(|group| group.repeated && !encloses(group)) + { + return false; + } + let relaxing = candidate + .group_spans + .iter() + .filter(|group| group.optional) + .collect::>(); + !relaxing.is_empty() && relaxing.iter().all(|group| encloses(group)) + }) + }; + // Whether a ref can have run before the action, given that ancestry. An + // action after the whole choice (`(x=A | A) {$x}`) has no branch tag, so + // every branch counts; one written inside a branch excludes its siblings — + // including when the label itself is in another branch (`(e | xs+=e {…})`). + let on_action_path = |candidate: &ElementRef| { + action_branches.as_ref().is_none_or(|branches| { + !candidate.choice_branch.iter().any(|(choice, branch)| { + branches + .iter() + .any(|(a_choice, a_branch)| choice == a_choice && branch != a_branch) + }) + }) + }; + let matched_at_action = |candidate: &ElementRef| { + action_offset.is_none_or(|offset| { + let started = candidate.span.is_none_or(|(start, _)| start < offset); + started && on_action_path(candidate) + }) + }; + // Two declarations can share one read when the generated query is the + // same. For token-backed refs that is the token type, not the source form: + // `x=A` and `x='a'` differ in spelling and block-ness yet query alike. + let same_read_as_element = |candidate: &ElementRef| { + candidate.is_list == element.is_list + && if candidate.token_types.is_empty() || element.token_types.is_empty() { + candidate.target == element.target && candidate.is_block == element.is_block + } else { + candidate.token_types == element.token_types } + }; + + if element.is_list { + // `translate_element_read` lowers a list label to a per-target child + // iterator, which needs a rule or token *target*. A list over a token + // group (`xs+=(A | B)`) has none, so the read would fall through to + // the scalar block path and emit `.last()…collect()` — code that does + // not compile. Leave it unresolved instead. + if element.target.is_empty() { + return None; + } + // A list read yields every child of *one* query, so repeated declarations + // are the normal idiom (`xs+=e (op xs+=e)+`) only while they all name that + // same query. `xs+=A xs+=B` would iterate `A` alone and drop every `B`. + // The query is the token type for token-backed refs, not the spelling: + // `xs+=A B | xs+='a' C` binds one type through two source forms. + let same_query = |candidate: &ElementRef| { + if element.token_types.is_empty() || candidate.token_types.is_empty() { + candidate.target == element.target + } else { + candidate.token_types == element.token_types + } + }; + if declarations + .iter() + .any(|candidate| !same_query(candidate) || !candidate.is_list) + { + return None; + } + // What the read cannot express is exclusion, so the label resolves + // only when no *already-matched* same-target element sits outside it. + // A trailing `A` in `r : xs+=A {$xs} A;` has not been matched when the + // action runs, so it cannot pollute the iterator. + let exclusive = alt.refs.iter().all(|candidate| { + declares_label(candidate) + || !same_target(candidate) + || !matched_at_action(candidate) + }); + return exclusive.then(|| (element.clone(), 0)); + } + // Only declarations the action can actually observe constrain its read. Two + // rule it out: one in a sibling branch, which never runs alongside a + // branch-confined action (`(x=A {$x} | x=A+ B)`), and one *after* the action, + // which has not assigned the label yet (`x=A {$x} x=A` reads the first + // assignment unambiguously). + let relevant = declarations + .iter() + .copied() + .filter(|candidate| { + (!branch_confined || on_action_path(candidate)) && matched_at_action(candidate) + }) + .collect::>(); + let declarations = if relevant.is_empty() { + declarations + } else { + relevant + }; + let element = *declarations.first()?; + // A single label read is one positional lookup. Several declarations can + // still share it when each lowers to the same query — mutually exclusive + // branches holding `x=A` at the same occurrence do. What cannot be served + // is declarations that query differently (`(x=A | x=B)`), or that could + // both be present and so want different positions. + if declarations.iter().any(|candidate| { + !same_read_as_element(candidate) + || (!std::ptr::eq(*candidate, element) && candidate.can_coexist_with(element)) + }) { + return None; + } + // Deriving the read from the *first* declaration and probing the others + // property-by-property kept missing a dimension — occurrence and repetition + // among them. Instead resolve each declaration on its own and require the + // results to agree, so one read demonstrably serves every branch: + // `(A x=A B | x=A C)` wants occurrence 1 then 0, and `(x=A B | x=A+ C)` + // wants a first-match read then a last-match one. + if declarations.len() > 1 { + let mut resolutions = Vec::with_capacity(declarations.len()); + for candidate in &declarations { + let position = alt + .refs + .iter() + .position(|other| std::ptr::eq(other, *candidate))?; + let mut alone = alt.clone(); + // Isolate this declaration by *renaming* the others rather than + // clearing their labels. Clearing would reclassify a fellow + // declaration as an unlabeled impostor and trip the shadow check — + // `(x=A | x=A)` would reject itself. Renaming keeps them labeled, so + // `declares_label` still exempts them, while only one answers to + // `label`. + let shadow_name = format!("{label}{SIBLING_DECLARATION_SUFFIX}"); + for (index, ref_at) in alone.refs.iter_mut().enumerate() { + if index != position && ref_at.label.as_deref() == Some(label) { + ref_at.label = Some(shadow_name.clone()); + } + } + resolutions.push(Self::resolve_label_in_alt(&alone, label, action_offset)?); + } + let first = resolutions.first()?.clone(); + if resolutions + .iter() + .any(|resolution| !Self::same_label_read(resolution, &first)) + { + return None; + } + return Some(first); + } + // `element` is borrowed from `alt.refs`, so identity holds — but compare by + // value as a fallback, because the sibling-isolation rename above clones the + // alternative and a filtered `declarations` list can outlive that borrow. + let position = alt + .refs + .iter() + .position(|candidate| std::ptr::eq(candidate, element)) + .or_else(|| alt.refs.iter().position(|candidate| candidate == element))?; + let (before, after) = (&alt.refs[..position], &alt.refs[position + 1..]); + // `translate_element_read` routes on `is_block`, which covers labeled + // groups and *literal* terminals alike (`x='b'`), so the occurrence has to + // be computed the same way for both — keying on an empty target here would + // leave a literal label counting same-target children while its read walks + // every terminal. + if element.is_block { + // A *repeated* block label (`x=(A | B)+`) is overwritten each iteration, + // so ANTLR exposes the last match while a positional read pins the first. + // The non-block path already declines this; do the same rather than read + // the wrong iteration. + if element.cardinality.is_repeated() { + return None; } + // A block label has no single target to query, so its read walks the + // context's terminal children by position. The index is the number of + // terminals matched ahead of the block on this parse path — every + // terminal counts, not just ones sharing the block's token set, since + // each is a distinct child of the same context. + // `on_action_path` already narrowed these to one parse path (when the + // action is inside a branch), so branches that survive simply count. + // Confinement to one *outer* branch does not restrict a nested choice + // inside it: `((a=A | b=B) x=(C | D) {…} | E)` still has the `A`/`B` + // branches to reconcile, and calling the count path-restricted summed + // them. Strip the tags of choices the action is genuinely inside, then + // let any surviving tag force cross-branch agreement. + let counted = before + .iter() + .filter(|candidate| { + !candidate.token_types.is_empty() + && on_action_path(candidate) + // Only children already matched when the action runs affect + // its read. For a *forward* label (`A {$x.text} B? x=(C|D)`) + // the prefix is entirely in the future, so counting `B?` + // made the index inexact and fell back to `last()` — which + // returns the already-matched `A`. + && matched_at_action(candidate) + // A ref in a branch this label cannot reach never precedes it: + // in `(x=A | x='a')` the sibling declaration is not a prefix + // terminal of the literal's path. + && candidate.can_coexist_with(element) + }) + .cloned() + .map(|mut candidate| { + if let Some(branches) = action_branches.as_ref() { + candidate.retain_choices(|choice| { + !branches.iter().any(|&(taken, _)| taken == choice) + }); + } + candidate + }) + .collect::>(); + let restricted = counted + .iter() + .all(|candidate| candidate.choice_branch.is_empty()); + let terminals_before = Self::exact_child_count(counted.iter(), restricted); + // Without a fixed index the read falls back to the most recent + // terminal, which is only right when nothing has been matched since. + // A sibling branch that puts a terminal at the same index supplies the + // child this read selects on a parse where the label is unset: + // `((x=(A | B)) | C) {$x}` reads `C` on the `C` branch. + if let Some(index) = terminals_before { + // A sibling branch's terminal can only be mistaken for the label + // when the read actually runs on that branch. An action confined to + // the label's own branch never executes there, so the sibling is + // irrelevant — `(x=(A | B) {…} | C)` is safe even though `C` sits at + // the same index. + let sibling_at_index = !branch_confined + && alt.refs.iter().any(|candidate| { + // Another declaration of the same label binds it too, so it + // can never impersonate it — `(x=A | x='a')` is one label + // over two branches, not a label and an impostor. + !declares_label(candidate) + && !candidate.can_coexist_with(element) + && !candidate.token_types.is_empty() + && candidate.cardinality.max != Some(0) + && Self::can_occupy_terminal_index(alt, candidate, index) + }); + if sibling_at_index { + return None; + } + // ROOT J: a fixed index is not enough when the label is *optional* — + // `x=(A | B)? C {…}` puts `C` at index 0 whenever the block is + // absent, so the read would report it as the label's token. + let optional_here = if on_taken_group(element) { + element.group_local_cardinality.min == 0 + } else { + element.cardinality.min == 0 + }; + if optional_here + && after.iter().any(|candidate| { + !candidate.token_types.is_empty() + && candidate.cardinality.max != Some(0) + && matched_at_action(candidate) + }) + { + return None; + } + } + return terminals_before.map_or_else( + || { + let displaced = after.iter().any(|candidate| { + !candidate.token_types.is_empty() + && candidate.cardinality.max != Some(0) + && matched_at_action(candidate) + }); + (!displaced).then(|| (element.clone(), usize::MAX)) + }, + |index| Some((element.clone(), index)), + ); + } + + // A repeated single label (`(x=A)+`) is overwritten on every iteration, + // so ANTLR exposes the *latest* match. The read here is a fixed + // `nth(i)`, which would pin the first one; only the accessor path can + // express `.last()`. Leave it unresolved rather than read the wrong + // iteration. + if element.cardinality.is_repeated() { + return None; } - None + // Single label: count the children ahead of it that the read would also + // select, bailing as soon as one contributes an unfixed number. A token + // *group* has no target yet still produces a child of the label's token + // type when their sets overlap (`(A | B) x=A`), so it must be counted — + // and since only some of its members match, its contribution is not + // exact and the label declines. + let counts_toward_occurrence = |candidate: &ElementRef| { + if element.token_types.is_empty() || candidate.token_types.is_empty() { + return candidate.target == element.target; + } + candidate + .token_types + .iter() + .any(|token_type| element.token_types.contains(token_type)) + }; + // A token *group* only sometimes yields a matching child, so its count is + // exact only when every member is one the read selects. + if before.iter().any(|candidate| { + counts_toward_occurrence(candidate) + && !candidate.token_types.is_empty() + && !candidate + .token_types + .iter() + .all(|token_type| element.token_types.contains(token_type)) + }) { + return None; + } + // Count only children on the label's own parse path, and — when the action + // is confined to a branch — count the surviving branch as that path rather + // than demanding agreement from branches already filtered out. + let counted = before + .iter() + .filter(|candidate| { + // Only children already matched when the action runs can affect its + // read. An inline action *before* the label sees none of them, so a + // later unbounded run must not poison the count: + // `r : {$x.text} A* x=A EOF;` reads an empty list, whatever follows. + counts_toward_occurrence(candidate) + && matched_at_action(candidate) + && candidate.can_coexist_with(element) + }) + .cloned() + .map(|mut candidate| { + if on_taken_group(&candidate) { + // The enclosing group is taken on the action's path, so the + // group's own quantifier no longer relaxes this ref. + candidate.cardinality = candidate.group_local_cardinality; + candidate.branch_local_cardinality = candidate.group_local_cardinality; + } + // Choices the *label* is inside are settled on its path, so those + // tags carry no remaining alternation. Tags that survive belong to + // choices the label sits outside of, whose branches are genuinely + // alternative. + candidate.retain_choices(|choice| { + !element + .choice_branch + .iter() + .any(|&(taken, _)| taken == choice) + }); + candidate + }) + .collect::>(); + // `can_coexist_with` keeps every branch of a choice the label is outside of, + // so it does not by itself select one path: `(A B | A C) x=A` would sum both + // prefixes. Only claim path-restriction once no alternation remains. + let restricted = counted + .iter() + .all(|candidate| candidate.choice_branch.is_empty()); + let occurrence = Self::exact_child_count(counted.iter(), restricted)?; + // An optional label is displaced by a following same-target child that can + // slide into its position, whether that child is mandatory (`x=A? A`) or + // optional (`(pred x=A)? A?` — the follower may consume the only token). + // Two kinds cannot: a ref from a sibling branch of the same choice, since + // no parse contains both, and — for a mid-rule action — a ref that starts + // after the action and so has not been matched when the read runs. + // `matched_at_action` already folds in coexistence when the action is + // confined to the label's branch; applying it again here would also + // exclude siblings for an action that runs after the whole choice. + // Another *declaration* of the same label is not a shadow — it binds the + // label too, and the guard above already proved they share one read. + // The label's own `min: 0` may come from a group the action shares, in which + // case it is *not* optional relative to the action: `(A x=A {…})?` only runs + // the action when the group matched, so `x` is bound. + let element_optional_here = if on_taken_group(element) { + element.group_local_cardinality.min == 0 + } else { + element.cardinality.min == 0 + }; + // A sibling branch's child cannot slide into the label's slot *within a + // parse that bound the label* — the two never coexist. It is still a hazard + // when the read may run with the label unset, which is when the action is + // not inside the branch that separates them. That has to be judged per + // *choice*: a rule-wide flag would let an action confined to some unrelated + // later choice exempt an earlier sibling + // (`({false}? x=A | A) (B {$x} | C)`). + let excluded_by_confinement = |candidate: &ElementRef| { + !candidate.can_coexist_with(element) + && Self::action_inside_separating_branch( + element, + candidate, + action_branches.as_deref(), + ) + }; + let shadowed_when_absent = element_optional_here + && (after.iter().any(|candidate| { + !declares_label(candidate) + && same_target(candidate) + && matched_at_action(candidate) + && !excluded_by_confinement(candidate) + }) + // A same-target sibling *before* the label impersonates it just as well: + // in `(A | x=A) {$x}` the unlabeled `A` is the only child of its type on + // its own branch, so `child_tokens(A).nth(0)` reports it. Only + // non-coexisting refs matter here — a ref the label coexists with is a + // genuine prefix and is already folded into `occurrence`. + || before.iter().any(|candidate| { + !declares_label(candidate) + && same_target(candidate) + && matched_at_action(candidate) + && !candidate.can_coexist_with(element) + && !excluded_by_confinement(candidate) + })); + (!shadowed_when_absent).then_some((element.clone(), occurrence)) } } @@ -501,6 +1598,15 @@ fn translate_reference( is_list: false, cardinality: ChildCardinality::ONE, stable_accessor: false, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, }; let _ = target_rule; return translate_element_read(&element, usize::MAX, suffix, ctx, body); @@ -514,6 +1620,15 @@ fn translate_reference( is_list: false, cardinality: ChildCardinality::ONE, stable_accessor: false, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, }; return translate_element_read(&element, usize::MAX, suffix, ctx, body); } @@ -596,21 +1711,46 @@ fn translate_element_read( "__ctx.child_tokens(self.base.parse_tree_storage(), self.base.token_store(), {token_type})" )); } + // A list label whose target names neither a rule nor a token type has no + // iterator form: the block read below picks *one* terminal and renders it as + // a `String`, so falling through emitted `.collect()` on a `String` for + // `xs+='a'` (a literal is not a `token_types` key). Decline instead of + // generating code that does not compile. + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "cannot translate list label ${} in embedded action: {body}", + element.label.as_deref().unwrap_or_default() + ), + )); } if element.is_block { - // A labeled `(...)` block over tokens: `$myset.stop` / `$myset.text` - // read the token the block matched — the most recent terminal child. - // A bare `$myset` read denotes the Token object itself (Java prints - // `Token.toString()`), which is the same rendering as start/stop. + // A labeled `(...)` block over tokens: `$myset.stop` / `$myset.text` read + // the token the block matched. A bare `$myset` read denotes the Token + // object itself (Java prints `Token.toString()`), the same rendering as + // start/stop. + // + // The block has no target to query, so the read walks the context's + // terminal children and picks by *position*. It uses the *labeled* iterator, + // which skips deleted-token errors while keeping inserted missing ones — + // a grammar-derived index knows nothing about recovery, and a deleted token + // would otherwise shift every later position (see #235 for the same rule on + // token accessors): `occurrence` is the number of + // terminals matched ahead of the block on this parse path. `usize::MAX` + // means the position is not fixed, in which case the most recent terminal + // is the best available answer — the historical behaviour. + let pick = if occurrence == usize::MAX { + "last()".to_owned() + } else { + format!("nth({occurrence})") + }; return match suffix { - None | Some("stop" | "start") => Ok( - "__ctx.terminal_children(self.base.parse_tree_storage(), self.base.token_store()).last().map(|__t| __t.symbol().to_string()).unwrap_or_default()" - .to_owned(), - ), - Some("text") => Ok( - "__ctx.terminal_children(self.base.parse_tree_storage(), self.base.token_store()).last().map(|__t| __t.text().to_owned()).unwrap_or_default()" - .to_owned(), - ), + None | Some("stop" | "start") => Ok(format!( + "__ctx.labeled_terminal_children(self.base.parse_tree_storage(), self.base.token_store()).{pick}.map(|__t| __t.symbol().to_string()).unwrap_or_default()" + )), + Some("text") => Ok(format!( + "__ctx.labeled_terminal_children(self.base.parse_tree_storage(), self.base.token_store()).{pick}.map(|__t| __t.text().to_owned()).unwrap_or_default()" + )), _ => Err(io::Error::new( io::ErrorKind::InvalidData, format!("unsupported block-label read in embedded action: {body}"), @@ -854,6 +1994,15 @@ mod tests { is_list: false, cardinality: ChildCardinality::ONE, stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, }, ElementRef { label: Some("right".to_owned()), @@ -863,6 +2012,15 @@ mod tests { is_list: false, cardinality: ChildCardinality::ONE, stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, }, ], children: BTreeMap::from([( @@ -897,6 +2055,1339 @@ mod tests { ); } + /// A label preceded by a same-target ref from a *sibling* choice branch has + /// no fixed CST position: `r : (e | x=e) {$x...}` builds one `e` child, so + /// counting the flattened refs would emit `nth(1)` and silently read an + /// element the parse never produced. Such a label must stay unresolved and + /// surface as a translation error. + #[test] + fn inexact_preceding_refs_leave_labels_unresolved_instead_of_misindexing() { + let mut statement = rule("s"); + // `r : (e | x=e) {…}`: the two refs are *sequential* here, not branches of + // one choice — an unlabeled `e` genuinely precedes the label on the same + // path, which is what leaves its position unfixed. (The mutually exclusive + // spelling is covered by `sibling_branch_children_do_not_shadow_an_optional_label`.) + let branch_ref = |label: Option<&str>| ElementRef { + label: label.map(ToOwned::to_owned), + target: "e".to_owned(), + token_types: Vec::new(), + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 0, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + // Optional on its own path, so the count ahead of the label floats. + branch_local_cardinality: ChildCardinality { + min: 0, + max: Some(1), + }, + group_local_cardinality: ChildCardinality { + min: 0, + max: Some(1), + }, + }; + statement.alts.push(AltModel { + label: None, + span: (10, 20), + refs: vec![branch_ref(None), branch_ref(Some("x"))], + children: BTreeMap::from([( + "e".to_owned(), + ChildCardinality { + min: 1, + max: Some(1), + }, + )]), + leading_target: Some("e".to_owned()), + }); + let m = model(vec![statement, rule("e")]); + let toks = tokens(&[]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + + let error = translate_body("$x.text", &ctx).expect_err("must not translate"); + assert_eq!(error.kind(), io::ErrorKind::InvalidData); + assert!(error.to_string().contains("cannot translate $x"), "{error}"); + } + + /// An *optional* label with a following same-target child has no fixed + /// position either: in `r : ({false}? x=A)? A {$x...}` the mandatory `A` + /// slides into `nth(0)` whenever the optional group is skipped, so the + /// action would receive a value for an unset label. + #[test] + fn optional_labels_shadowed_by_a_following_child_stay_unresolved() { + let mut statement = rule("s"); + let token_ref = |label: Option<&str>, min| ElementRef { + label: label.map(ToOwned::to_owned), + target: "A".to_owned(), + token_types: vec![1], + is_block: false, + is_list: false, + cardinality: ChildCardinality { min, max: Some(1) }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + statement.alts.push(AltModel { + label: None, + span: (10, 20), + // `x=A?` then a mandatory `A`. + refs: vec![token_ref(Some("x"), 0), token_ref(None, 1)], + children: BTreeMap::new(), + leading_target: Some("A".to_owned()), + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + + let error = translate_body("$x.text", &ctx).expect_err("must not translate"); + assert!(error.to_string().contains("cannot translate $x"), "{error}"); + } + + /// Token groups carry no target, so they must not share one occurrence + /// bucket: an optional disjoint group ahead of a labeled group + /// (`r : (A | B)? x=(C | D) {$x...}`) must not poison it. Block-label reads + /// take the last terminal child and never consult the index at all. + #[test] + fn disjoint_token_groups_do_not_poison_a_later_block_label() { + let mut statement = rule("s"); + // `branch_local_cardinality` mirrors `cardinality` here: the optionality + // comes from the group's own `?`, not from choice membership. + let group_ref = |label: Option<&str>, token_types: Vec, min| ElementRef { + label: label.map(ToOwned::to_owned), + target: String::new(), + token_types, + is_block: true, + is_list: false, + cardinality: ChildCardinality { min, max: Some(1) }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality { min, max: Some(1) }, + group_local_cardinality: ChildCardinality { min, max: Some(1) }, + }; + statement.alts.push(AltModel { + label: None, + span: (10, 20), + refs: vec![ + group_ref(None, vec![1, 2], 0), + group_ref(Some("x"), vec![3, 4], 1), + ], + children: BTreeMap::new(), + leading_target: None, + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1), ("B", 2), ("C", 3), ("D", 4)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + + let translated = translate_body("$x.text", &ctx).expect("translates"); + assert!(translated.contains("terminal_children"), "{translated}"); + assert!(translated.contains(".last()"), "{translated}"); + } + + /// A list label ahead of a same-target single label still contributes + /// children, so it must go through the occurrence accounting rather than be + /// skipped: `r : xs+=e name=e` puts one `e` before `name` (exact, countable + /// → `nth(1)`), while `r : xs+=e+ name=e` puts an unbounded run there and + /// leaves no fixed position at all. + #[test] + fn list_refs_ahead_of_a_single_label_are_counted_then_poison_when_unbounded() { + let list_ref = |max| ElementRef { + label: Some("xs".to_owned()), + target: "e".to_owned(), + token_types: Vec::new(), + is_block: false, + is_list: true, + cardinality: ChildCardinality { min: 1, max }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let single_ref = ElementRef { + label: Some("name".to_owned()), + target: "e".to_owned(), + token_types: Vec::new(), + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let translate = |max| { + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (10, 20), + refs: vec![list_ref(max), single_ref.clone()], + children: BTreeMap::new(), + leading_target: Some("e".to_owned()), + }); + let m = model(vec![statement, rule("e")]); + let toks = tokens(&[]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + translate_body("$name.text", &ctx).map_err(|error| error.to_string()) + }; + + // Exactly one preceding `e`: the position is known. + let exact = translate(Some(1)).expect("exact list count still resolves"); + assert!(exact.contains(".nth(1)"), "{exact}"); + + // Unbounded run of `e` ahead of the label: no fixed index exists. + let error = translate(None).expect_err("unbounded list must not resolve"); + assert!(error.contains("cannot translate $name"), "{error}"); + } + + /// A list read yields *every* same-target child, so it can only stand for + /// the label when no same-target element sits outside it. In the `mixed` + /// shape (`name=e ... errors+=e`) a `$errors` read would fold in `name`'s + /// child, so the label must not resolve. + #[test] + fn list_labels_sharing_a_target_with_another_label_stay_unresolved() { + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (10, 20), + refs: vec![ + ElementRef { + label: Some("name".to_owned()), + target: "e".to_owned(), + token_types: Vec::new(), + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }, + ElementRef { + label: Some("errors".to_owned()), + target: "e".to_owned(), + token_types: Vec::new(), + is_block: false, + is_list: true, + cardinality: ChildCardinality { min: 0, max: None }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }, + ], + children: BTreeMap::new(), + leading_target: Some("e".to_owned()), + }); + let m = model(vec![statement, rule("e")]); + let toks = tokens(&[]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + + let error = translate_body("$errors", &ctx).expect_err("must not translate"); + assert!( + error.to_string().contains("cannot translate $errors"), + "{error}" + ); + + // `name` is still resolvable: it precedes the list, so its own index is + // fixed at 0 and the list contributes nothing ahead of it. + let name = translate_body("$name.text", &ctx).expect("translates"); + assert!(name.contains(".nth(0)"), "{name}"); + } + + /// A block label has no target to query, so its read walks the context's + /// terminal children by *position*: the count of terminals matched ahead of + /// the block. That is what makes `t=~'x' 'z' {$t.text}` read the token the + /// label bound rather than the trailing `'z'` the old `last()` picked + /// (issue #233). Where the count is not fixed, `last()` remains the fallback. + #[test] + fn block_labels_read_the_terminal_the_label_bound() { + let translate = |action_offset| { + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (0, 100), + refs: vec![ + ElementRef { + label: Some("x".to_owned()), + target: String::new(), + token_types: vec![1, 2], + is_block: true, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: Some((10, 20)), + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }, + ElementRef { + label: None, + target: "C".to_owned(), + token_types: vec![3], + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: Some((30, 31)), + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }, + ], + children: BTreeMap::new(), + leading_target: None, + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1), ("B", 2), ("C", 3)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(action_offset), + site: ActionSite::Body, + token_types: &toks, + }; + translate_body("$x.text", &ctx).map_err(|error| error.to_string()) + }; + + // The block is the first terminal either way, so the read is `nth(0)` and + // the trailing `C` cannot be mistaken for it — regardless of whether the + // action precedes or follows `C`. + for offset in [25, 40] { + let translated = translate(offset).expect("a fixed terminal position resolves"); + assert!(translated.contains("terminal_children"), "{translated}"); + assert!( + translated.contains(".nth(0)"), + "offset {offset}: {translated}" + ); + } + } + + /// A mid-rule action executes at its own source position, so refs written + /// after it have not been matched and cannot affect its read. Spans decide + /// this: `r : xs+=A {$xs} A;` iterates the sole child available at the action, + /// while the same refs read from `@after` see both and must decline. + #[test] + fn future_children_do_not_constrain_a_mid_rule_read() { + let token_ref = |label: Option<&str>, is_list, span| ElementRef { + label: label.map(ToOwned::to_owned), + target: "A".to_owned(), + token_types: vec![1], + is_block: false, + is_list, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: Some(span), + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (0, 100), + // `xs+=A {action at 20} A` + refs: vec![ + token_ref(Some("xs"), true, (10, 11)), + token_ref(None, false, (30, 31)), + ], + children: BTreeMap::new(), + leading_target: None, + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1)]); + let mid_rule = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(20), + site: ActionSite::Body, + token_types: &toks, + }; + let translated = translate_body("$xs", &mid_rule).expect("trailing A has not matched yet"); + assert!(translated.contains("child_tokens"), "{translated}"); + + // Read from `@after`, the trailing `A` has matched and would be iterated. + let after = TranslationCtx { + body_offset: None, + site: ActionSite::After, + ..mid_rule + }; + let error = translate_body("$xs", &after).expect_err("both children are present by then"); + assert!( + error.to_string().contains("cannot translate $xs"), + "{error}" + ); + } + + /// Several declarations of one label can share a single read when each lowers + /// to the same query and they are mutually exclusive: `(x=A e {$x} | x=A f + /// {$x})` resolves, while `x=A | x='a'` resolves too because token-backed + /// equivalence is by *type*, not by source form or block-ness. + #[test] + fn compatible_declarations_share_one_read() { + // Each declaration is mandatory *within its branch* — `min: 0` on + // `cardinality` would say the label is genuinely optional, which is a + // different (and displaceable) shape. + let decl = |branch, is_block, span| ElementRef { + label: Some("x".to_owned()), + target: if is_block { "'a'" } else { "A" }.to_owned(), + token_types: vec![1], + is_block, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: vec![(5, branch)], + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: Some(span), + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + // `separate_alts` models `x=… | x=…` written as *top-level* alternatives, + // which the collector emits as two `AltModel`s — the shape a real grammar + // produces. Both in one `AltModel` instead models a nested `(… | …)` choice. + let translate = |second: ElementRef, offset, separate_alts: bool| { + let mut statement = rule("s"); + if separate_alts { + for (index, declaration) in + [decl(0, false, (10, 11)), second].into_iter().enumerate() + { + statement.alts.push(AltModel { + label: None, + span: (index * 50, index * 50 + 50), + refs: vec![declaration], + children: BTreeMap::new(), + leading_target: None, + }); + } + } else { + statement.alts.push(AltModel { + label: None, + span: (0, 100), + refs: vec![decl(0, false, (10, 11)), second], + children: BTreeMap::new(), + leading_target: None, + }); + } + let m = model(vec![statement]); + let toks = tokens(&[("A", 1)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: offset, + site: if offset.is_some() { + ActionSite::Body + } else { + ActionSite::After + }, + token_types: &toks, + }; + translate_body("$x.text", &ctx).map_err(|error| error.to_string()) + }; + + // `(x=A e {action} | x=A f {action})`: same query, exclusive branches of one + // nested choice. + let translated = translate(decl(1, false, (30, 31)), Some(20), false) + .expect("identical reads in exclusive branches share one lookup"); + assert!(translated.contains(".nth(0)"), "{translated}"); + + // `r @after {…} : x=A | x='a';` — literal and symbolic forms of one token + // type, as *top-level* alternatives. Both resolve at occurrence zero, the + // index where the block and token coordinate systems coincide. + let aliased = translate(decl(1, true, (60, 63)), None, true) + .expect("token-type equivalence ignores source form"); + assert!(aliased.contains(".nth(0)"), "{aliased}"); + } + + /// A *literal* terminal label (`x='b'`) is `is_block` too, so it must take the + /// same positional terminal count as a labeled group — keying the count on an + /// empty target instead would leave it counting same-target children while its + /// read walks every terminal. This is ANTLR's + /// `ParserErrors/ConjuringUpToken` shape, where `'a'` precedes the label. + #[test] + fn literal_terminal_labels_count_every_preceding_terminal() { + let terminal = |label: Option<&str>, target: &str, token_type, span| ElementRef { + label: label.map(ToOwned::to_owned), + target: target.to_owned(), + token_types: vec![token_type], + // Literals and groups alike route through the block read. + is_block: true, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: Some(span), + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (0, 100), + // `'a' x='b' {action} 'c'` + refs: vec![ + terminal(None, "'a'", 1, (10, 13)), + terminal(Some("x"), "'b'", 2, (14, 17)), + terminal(None, "'c'", 3, (40, 43)), + ], + children: BTreeMap::new(), + leading_target: None, + }); + let m = model(vec![statement]); + let toks = tokens(&[]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(20), + site: ActionSite::Body, + token_types: &toks, + }; + + let translated = translate_body("$x", &ctx).expect("translates"); + assert!(translated.contains("terminal_children"), "{translated}"); + // `'a'` is terminal 0, so the label is terminal 1 — not `last()`, which + // would become `'c'` once that matched. + assert!(translated.contains(".nth(1)"), "{translated}"); + } + + /// An alternative that does not declare the label leaves it unset, so the + /// read has to come up empty there. `r : x=A | A` breaks that: the second + /// alternative builds an `A` the read would select, reporting a value for a + /// label the parse never bound. Conversely `r : x=A | B` is fine. + #[test] + fn unscoped_reads_reject_alternatives_that_would_satisfy_them_unbound() { + let token_ref = |label: Option<&str>, target: &str, token_type| ElementRef { + label: label.map(ToOwned::to_owned), + target: target.to_owned(), + token_types: vec![token_type], + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let translate = |second: ElementRef| { + let mut statement = rule("s"); + for (index, refs) in [vec![token_ref(Some("x"), "A", 1)], vec![second]] + .into_iter() + .enumerate() + { + statement.alts.push(AltModel { + label: None, + span: (index * 10, index * 10 + 10), + refs, + children: BTreeMap::new(), + leading_target: None, + }); + } + let m = model(vec![statement]); + let toks = tokens(&[("A", 1), ("B", 2)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: None, + site: ActionSite::After, + token_types: &toks, + }; + translate_body("$x.text", &ctx).map_err(|error| error.to_string()) + }; + + // `x=A | A`: the unlabeled `A` satisfies the read with `x` unset. + let error = translate(token_ref(None, "A", 1)) + .expect_err("an unbound label must not read another alternative's child"); + assert!(error.contains("cannot translate $x"), "{error}"); + + // `x=A | B`: nothing in the second alternative can be mistaken for `x`. + let translated = + translate(token_ref(None, "B", 2)).expect("disjoint alternative keeps the read"); + assert!(translated.contains(".nth(0)"), "{translated}"); + } + + /// A list read iterates one target, so every declaration of the label must + /// name that target: `xs+=A xs+=B` would iterate `A` and drop every `B`. + /// Equivalent *block* labels across alternatives (`x=(A | B) | x=(C | D)`) + /// conversely stay resolvable, because the block read ignores token sets. + #[test] + fn list_declarations_must_share_a_target_while_block_reads_ignore_token_sets() { + let list_ref = |target: &str, token_type| ElementRef { + label: Some("xs".to_owned()), + target: target.to_owned(), + token_types: vec![token_type], + is_block: false, + is_list: true, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (10, 20), + refs: vec![list_ref("A", 1), list_ref("B", 2)], + children: BTreeMap::new(), + leading_target: None, + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1), ("B", 2)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + let error = translate_body("$xs", &ctx).expect_err("mixed list targets must not resolve"); + assert!( + error.to_string().contains("cannot translate $xs"), + "{error}" + ); + + // Two block labels over different token sets lower to the same read. + let block_ref = |token_types: Vec| ElementRef { + label: Some("x".to_owned()), + target: String::new(), + token_types, + is_block: true, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let mut choice = rule("s"); + for (index, refs) in [vec![block_ref(vec![1, 2])], vec![block_ref(vec![3, 4])]] + .into_iter() + .enumerate() + { + choice.alts.push(AltModel { + label: None, + span: (index * 10, index * 10 + 10), + refs, + children: BTreeMap::new(), + leading_target: None, + }); + } + let m = model(vec![choice]); + let toks = tokens(&[("A", 1), ("B", 2), ("C", 3), ("D", 4)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: None, + site: ActionSite::After, + token_types: &toks, + }; + let translated = translate_body("$x.text", &ctx).expect("equivalent block reads resolve"); + assert!(translated.contains("terminal_children"), "{translated}"); + } + + /// Reads that `translate_element_read` cannot express must stay unresolved + /// rather than fall through to a different read: a list over a *token group* + /// (`xs+=(A | B)`) has no target to iterate and would emit + /// `.last()…collect()` — Rust that does not compile — and a *repeated* single + /// label (`(x=A)+`) is overwritten each iteration, so a fixed `nth(0)` pins + /// the first match where ANTLR exposes the latest. + #[test] + fn reads_the_translator_cannot_express_stay_unresolved() { + let translate = |element: ElementRef, read: &str| { + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (10, 20), + refs: vec![element], + children: BTreeMap::new(), + leading_target: None, + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1), ("B", 2)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + translate_body(read, &ctx).map_err(|error| error.to_string()) + }; + + let group_list = ElementRef { + label: Some("xs".to_owned()), + target: String::new(), + token_types: vec![1, 2], + is_block: true, + is_list: true, + cardinality: ChildCardinality { min: 1, max: None }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let error = translate(group_list, "$xs").expect_err("no target to iterate"); + assert!(error.contains("cannot translate $xs"), "{error}"); + + let repeated_single = ElementRef { + label: Some("x".to_owned()), + target: "A".to_owned(), + token_types: vec![1], + is_block: false, + is_list: false, + cardinality: ChildCardinality { min: 1, max: None }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let error = translate(repeated_single, "$x.text").expect_err("no last-occurrence read"); + assert!(error.contains("cannot translate $x"), "{error}"); + } + + /// Mutual exclusion needs the *whole* choice ancestry, not just the innermost + /// choice. In `((x=e | f) | e)` the label and the trailing `e` are separated + /// by the outer choice; keeping only the inner tag would make them look + /// independent and reject a valid action. + #[test] + fn nested_choices_keep_their_outer_branch_ancestry() { + let rule_ref = |label: Option<&str>, branches: Vec<(usize, usize)>, span| ElementRef { + label: label.map(ToOwned::to_owned), + target: "e".to_owned(), + token_types: Vec::new(), + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 0, + max: Some(1), + }, + stable_accessor: true, + choice_branch: branches, + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: Some(span), + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (0, 100), + refs: vec![ + // Outer choice 1 branch 0, then inner choice 2 branch 0. + rule_ref(Some("x"), vec![(1, 0), (2, 0)], (10, 11)), + // Outer choice 1 branch 1 — excluded by the *outer* choice alone. + rule_ref(None, vec![(1, 1)], (30, 31)), + ], + children: BTreeMap::new(), + leading_target: Some("e".to_owned()), + }); + let m = model(vec![statement, rule("e")]); + let toks = tokens(&[]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + + let translated = translate_body("$x.text", &ctx).expect("outer exclusion still applies"); + assert!(translated.contains(".nth(0)"), "{translated}"); + } + + /// Sibling exclusion is valid only for a *mid-rule* action, which is confined + /// to the branch it is written in. An `@after` body runs whichever branch the + /// parse took, so `r @after {$x.text} : (x=A | A) EOF;` must decline — the + /// unlabeled branch's `A` is present when the read executes. + #[test] + fn unscoped_bodies_treat_sibling_matches_as_hazards() { + let branch_ref = |label: Option<&str>, branch, span| ElementRef { + label: label.map(ToOwned::to_owned), + target: "A".to_owned(), + token_types: vec![1], + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 0, + max: Some(1), + }, + stable_accessor: true, + choice_branch: vec![(3, branch)], + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: Some(span), + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (0, 100), + refs: vec![ + branch_ref(Some("x"), 0, (10, 11)), + branch_ref(None, 1, (20, 21)), + ], + children: BTreeMap::new(), + leading_target: Some("A".to_owned()), + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1)]); + let after = TranslationCtx { + model: &m, + rule_index: 0, + // `@after`: unscoped, so any branch may have run. + body_offset: None, + site: ActionSite::After, + token_types: &toks, + }; + let error = translate_body("$x.text", &after).expect_err("sibling match is a hazard here"); + assert!(error.to_string().contains("cannot translate $x"), "{error}"); + + // The same refs read from a mid-rule action inside the labeled branch do + // resolve, since that action cannot run on the sibling branch. + let body = TranslationCtx { + body_offset: Some(15), + site: ActionSite::Body, + ..after + }; + let translated = + translate_body("$x.text", &body).expect("mid-rule action excludes sibling"); + assert!(translated.contains(".nth(0)"), "{translated}"); + } + + /// A token label's read queries by token *type*, so a differently-spelled + /// terminal with the same type is the same child: with `A : 'a';`, + /// `r : (xs+=A)? 'a' {$xs}` must decline because the mandatory `'a'` would be + /// iterated as an `xs` element. + #[test] + fn token_labels_compare_types_not_source_spelling() { + let mut statement = rule("s"); + statement.alts.push(AltModel { + label: None, + span: (10, 20), + refs: vec![ + ElementRef { + label: Some("xs".to_owned()), + target: "A".to_owned(), + token_types: vec![1], + is_block: false, + is_list: true, + cardinality: ChildCardinality { + min: 0, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }, + ElementRef { + label: None, + // Literal spelling differs; the token type is identical. + target: "'a'".to_owned(), + token_types: vec![1], + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }, + ], + children: BTreeMap::new(), + leading_target: None, + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + + let error = translate_body("$xs", &ctx).expect_err("aliased terminal is the same child"); + assert!( + error.to_string().contains("cannot translate $xs"), + "{error}" + ); + } + + /// A same-target ref in a *sibling* choice branch never coexists with the + /// label, so it cannot displace an optional one: `r : (x=A {$x.text} B | A C)` + /// must still translate. Only a certainly-matched following child shadows. + #[test] + fn sibling_branch_children_do_not_shadow_an_optional_label() { + let mut statement = rule("s"); + // Two branches of one choice: same choice id, different branch index. + let branch_ref = |label: Option<&str>, branch, span| ElementRef { + label: label.map(ToOwned::to_owned), + target: "A".to_owned(), + token_types: vec![1], + is_block: false, + is_list: false, + // Mutually exclusive branches both report `min: 0`. + cardinality: ChildCardinality { + min: 0, + max: Some(1), + }, + stable_accessor: true, + choice_branch: vec![(7, branch)], + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: Some(span), + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + statement.alts.push(AltModel { + label: None, + span: (0, 100), + // `(x=A {action} B | A C)`: the action sits inside branch 0. + refs: vec![ + branch_ref(Some("x"), 0, (10, 11)), + branch_ref(None, 1, (30, 31)), + ], + children: BTreeMap::new(), + leading_target: Some("A".to_owned()), + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + + let translated = translate_body("$x.text", &ctx).expect("translates"); + assert!(translated.contains(".nth(0)"), "{translated}"); + + // The sequential counterpart — `(pred x=A)? A?`, both on the rule's own + // path — *is* a hazard: the follower may consume the only token while the + // label is unset. Cardinality alone cannot tell these two apart, which is + // what `choice_branch` exists for. + let mut sequential = rule("s"); + let sequential_ref = |label: Option<&str>, span| ElementRef { + label: label.map(ToOwned::to_owned), + target: "A".to_owned(), + token_types: vec![1], + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 0, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: Some(span), + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + sequential.alts.push(AltModel { + label: None, + span: (0, 100), + // `(pred x=A)? A?` with the action last: both have matched by then. + refs: vec![ + sequential_ref(Some("x"), (10, 11)), + sequential_ref(None, (12, 13)), + ], + children: BTreeMap::new(), + leading_target: Some("A".to_owned()), + }); + let m = model(vec![sequential]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + let error = translate_body("$x.text", &ctx).expect_err("sequential follower shadows"); + assert!(error.to_string().contains("cannot translate $x"), "{error}"); + } + + /// A list label repeated within one alternative is the ordinary + /// comma-separated idiom (`xs+=e (op xs+=e)+`) — every declaration feeds the + /// same iteration, so repeats must not be mistaken for a conflict. This is + /// the shape of ANTLR's `ParserExec/ListLabelsOnRuleRefStartOfAlt` + /// descriptor, read from `@after` across alternatives that declare it plus + /// one that does not. + #[test] + fn repeated_list_declarations_across_alternatives_still_resolve() { + let list_ref = || ElementRef { + label: Some("args".to_owned()), + target: "e".to_owned(), + token_types: Vec::new(), + is_block: false, + is_list: true, + cardinality: ChildCardinality { min: 1, max: None }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let token_ref = ElementRef { + label: None, + target: "ID".to_owned(), + token_types: vec![1], + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let mut statement = rule("s"); + for (index, refs) in [ + // `args+=e (AND args+=e)+` — two declarations, one iteration. + vec![list_ref(), list_ref()], + // An alternative that never mentions the label at all. + vec![token_ref], + ] + .into_iter() + .enumerate() + { + statement.alts.push(AltModel { + label: None, + span: (index * 10, index * 10 + 10), + refs, + children: BTreeMap::new(), + leading_target: None, + }); + } + let m = model(vec![statement, rule("e")]); + let toks = tokens(&[("ID", 1)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: None, + site: ActionSite::After, + token_types: &toks, + }; + + let translated = translate_body("$args", &ctx).expect("list label resolves"); + assert!(translated.contains("child_rule_trees"), "{translated}"); + } + + /// `@after` / `@init` bodies are not scoped to an alternative, so one read + /// has to serve whichever alternative the parse took. `r : x=A | x=B` with an + /// `@after` read of `$x` would emit the `A` lookup and yield a default on the + /// `B` branch, while `r : x=A B | x=A C` resolves identically in both. + #[test] + fn unscoped_bodies_reject_labels_that_resolve_differently_per_alternative() { + let token_ref = |label: Option<&str>, target: &str, token_type| ElementRef { + label: label.map(ToOwned::to_owned), + target: target.to_owned(), + token_types: vec![token_type], + is_block: false, + is_list: false, + cardinality: ChildCardinality { + min: 1, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + let translate = |second: Vec| { + let mut statement = rule("s"); + for (index, refs) in [vec![token_ref(Some("x"), "A", 1)], second] + .into_iter() + .enumerate() + { + statement.alts.push(AltModel { + label: None, + span: (index * 10, index * 10 + 10), + refs, + children: BTreeMap::new(), + leading_target: None, + }); + } + let m = model(vec![statement]); + let toks = tokens(&[("A", 1), ("B", 2), ("C", 3)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + // `@after`: no offset, so no single owning alternative. + body_offset: None, + site: ActionSite::After, + token_types: &toks, + }; + translate_body("$x.text", &ctx).map_err(|error| error.to_string()) + }; + + // `x=A | x=B`: the two alternatives need different token lookups. + let error = translate(vec![token_ref(Some("x"), "B", 2)]) + .expect_err("conflicting per-alternative reads must not translate"); + assert!(error.contains("cannot translate $x"), "{error}"); + + // `x=A B | x=A C`: both resolve to the same `A` lookup at occurrence 0. + let agreed = translate(vec![token_ref(Some("x"), "A", 1), token_ref(None, "C", 3)]) + .expect("agreeing per-alternative reads still translate"); + assert!(agreed.contains(".nth(0)"), "{agreed}"); + } + + /// One label declared over disjoint targets (`r : (x=A | x=B)`) cannot be + /// served by a single positional read: picking the first declaration yields + /// an empty value whenever the parse took the other branch. + #[test] + fn labels_repeated_over_disjoint_targets_stay_unresolved() { + let mut statement = rule("s"); + let token_ref = |target: &str, token_type| ElementRef { + label: Some("x".to_owned()), + target: target.to_owned(), + token_types: vec![token_type], + is_block: false, + is_list: false, + // Mutually exclusive branches. + cardinality: ChildCardinality { + min: 0, + max: Some(1), + }, + stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, + }; + statement.alts.push(AltModel { + label: None, + span: (10, 20), + refs: vec![token_ref("A", 1), token_ref("B", 2)], + children: BTreeMap::new(), + leading_target: None, + }); + let m = model(vec![statement]); + let toks = tokens(&[("A", 1), ("B", 2)]); + let ctx = TranslationCtx { + model: &m, + rule_index: 0, + body_offset: Some(15), + site: ActionSite::Body, + token_types: &toks, + }; + + let error = translate_body("$x.text", &ctx).expect_err("must not translate"); + assert!(error.to_string().contains("cannot translate $x"), "{error}"); + } + #[test] fn translates_ctx_and_text() { let m = model(vec![rule("s")]); @@ -952,6 +3443,15 @@ mod tests { is_list: true, cardinality: ChildCardinality { min: 1, max: None }, stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, }, ElementRef { label: Some("ids".to_owned()), @@ -961,6 +3461,15 @@ mod tests { is_list: true, cardinality: ChildCardinality { min: 1, max: None }, stable_accessor: true, + choice_branch: Vec::new(), + choice_arity: Vec::new(), + choice_spans: Vec::new(), + group_spans: Vec::new(), + branch_spans: Vec::new(), + leading_terminal: true, + span: None, + branch_local_cardinality: ChildCardinality::ONE, + group_local_cardinality: ChildCardinality::ONE, }, ], children: BTreeMap::new(), diff --git a/src/snapshots/antlr4_runtime__tree__tests__terminal_children_raw_vs_labeled.snap b/src/snapshots/antlr4_runtime__tree__tests__terminal_children_raw_vs_labeled.snap new file mode 100644 index 00000000..a4792ae9 --- /dev/null +++ b/src/snapshots/antlr4_runtime__tree__tests__terminal_children_raw_vs_labeled.snap @@ -0,0 +1,15 @@ +--- +source: src/tree.rs +expression: "(raw, labeled)" +--- +( + [ + "x", + "", + "c", + ], + [ + "", + "c", + ], +) diff --git a/src/tree.rs b/src/tree.rs index 80a88dc3..85cfa13d 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -1163,6 +1163,35 @@ impl ParserRuleContext { }) } + /// Terminal children as a *grammar-positional* sequence: deleted input tokens + /// are skipped while inserted missing tokens are kept. + /// + /// A generated label read derives its index from the grammar, which knows + /// nothing about error recovery. A deleted token still occupies a child slot, so + /// indexing [`Self::terminal_children`] would shift every later position on + /// recovered input; an *inserted* token, by contrast, is the value ANTLR assigns + /// to the label and must stay. This matches the labeled-token accessors the + /// generator emits. + pub fn labeled_terminal_children<'a>( + &'a self, + storage: &'a ParseTreeStorage, + tokens: &'a TokenStore, + ) -> impl Iterator> + 'a { + self.child_nodes(storage, tokens) + .filter_map(|child| match child.kind() { + NodeKind::Terminal => child.as_terminal(), + NodeKind::Error => { + let terminal = child.as_error().map(ErrorNodeView::terminal)?; + let symbol = terminal.symbol(); + // Inserted missing tokens carry ANTLR's synthetic -1:-1 span; + // deleted input tokens retain real source boundaries. + (symbol.start() == usize::MAX && symbol.stop() == usize::MAX) + .then_some(terminal) + } + NodeKind::Rule => None, + }) + } + #[must_use] pub fn text(&self, storage: &ParseTreeStorage, tokens: &TokenStore) -> String { self.child_nodes(storage, tokens).map(Node::text).collect() @@ -1486,6 +1515,7 @@ fn stored_token_id(raw: u32) -> TokenId { } #[cfg(test)] +#[allow(clippy::disallowed_methods)] // insta assertion macros unwrap internal I/O. mod tests { use super::*; use crate::token::TokenSpec; @@ -1520,6 +1550,41 @@ mod tests { ); } + #[test] + fn labeled_terminal_children_keep_inserted_tokens_and_skip_deleted_ones() { + let mut tokens = TokenStore::new(None, ""); + let deleted = token(&mut tokens, 1, "x"); + let inserted = tokens + .push( + TokenSpec::explicit(2, "") + .with_span(usize::MAX, usize::MAX) + .with_byte_span(0, 0), + ) + .expect("test token should fit"); + let kept = token(&mut tokens, 3, "c"); + let mut storage = ParseTreeStorage::new(); + let deleted = storage.error(deleted); + let inserted = storage.error(inserted); + let kept = storage.terminal(kept); + let mut context = ParserRuleContext::new(0, -1); + storage.add_child(&mut context, deleted); + storage.add_child(&mut context, inserted); + storage.add_child(&mut context, kept); + + // `terminal_children` is the raw CST view: every error node counts, so a + // deleted token shifts the positions a grammar-derived index relies on. + let raw = context + .terminal_children(&storage, &tokens) + .map(|terminal| terminal.text().to_owned()) + .collect::>(); + let labeled = context + .labeled_terminal_children(&storage, &tokens) + .map(|terminal| terminal.text().to_owned()) + .collect::>(); + + insta::assert_debug_snapshot!("terminal_children_raw_vs_labeled", (raw, labeled)); + } + #[test] fn context_alt_number_does_not_change_public_tree_rendering() { let tokens = TokenStore::new(None, ""); diff --git a/tests/antlr4_rust_gen_cli.rs b/tests/antlr4_rust_gen_cli.rs index c1bd1f6d..ab36289a 100644 --- a/tests/antlr4_rust_gen_cli.rs +++ b/tests/antlr4_rust_gen_cli.rs @@ -914,10 +914,19 @@ mod multi_alternative_label_tests { use antlr4_runtime::{CommonTokenStream, InputStream, Parser as _}; fn parse(input: &str) -> antlr4_runtime::ParsedFile { + parse_rule(input, TParser::expr) + } + + fn parse_rule( + input: &str, + entry: impl FnOnce( + &mut TParser>, + ) -> Result, + ) -> antlr4_runtime::ParsedFile { let lexer = TLexer::new(InputStream::new(input)); let tokens = CommonTokenStream::new(lexer); let mut parser = TParser::new(tokens); - let root = parser.expr().expect("input should parse"); + let root = entry(&mut parser).expect("input should parse"); assert_eq!(parser.number_of_syntax_errors(), 0); parser.into_parsed_file(root) } @@ -951,6 +960,78 @@ mod multi_alternative_label_tests { let leaf = product.calc_children().next().expect("leaf calc"); assert!(leaf.op().is_none(), "primary alternative carries no operator"); } + + // Issue #201: labels nested inside an unlabeled grouping block reach the + // typed surface, and reading them agrees with the parsed text. + #[test] + fn labels_inside_grouping_blocks_read_their_own_children() { + let parsed = parse_rule("doc in a, b 7", TParser::grouped); + let grouped = parsed + .tree() + .as_rule() + .expect("grouped rule") + .downcast_ref::() + .expect("typed grouped context"); + assert_eq!(grouped.doc().expect("doc token").to_string(), "doc"); + assert!( + grouped.oneway().is_none(), + "the throws branch carries no oneway token" + ); + assert_eq!( + grouped + .errors() + .map(|error| error.text()) + .collect::>(), + ["a", "b"] + ); + + // The other branch of the same block, and both optionals absent. + let parsed = parse_rule("* 7", TParser::grouped); + let grouped = parsed + .tree() + .as_rule() + .expect("grouped rule") + .downcast_ref::() + .expect("typed grouped context"); + assert!(grouped.doc().is_none(), "absent optional reads as None"); + assert_eq!(grouped.oneway().expect("oneway token").to_string(), "*"); + assert_eq!(grouped.errors().count(), 0); + } + + // Issue #201: a single and a list label on the same rule must each resolve + // past the other's children rather than by caller-side positional guessing. + #[test] + fn single_and_list_labels_on_one_rule_stay_disjoint() { + let parsed = parse_rule("f ( x y ) in a, b", TParser::mixed); + let mixed = parsed + .tree() + .as_rule() + .expect("mixed rule") + .downcast_ref::() + .expect("typed mixed context"); + assert_eq!(mixed.name().expect("name label").text(), "f"); + assert_eq!( + mixed.errors().map(|error| error.text()).collect::>(), + ["a", "b"] + ); + // `name` is the sole unary outside the throws list, so the list accessor + // must not include it and the two must partition `unary_children`. + assert_eq!(mixed.unary_children().count(), 3); + + let parsed = parse_rule("f ( ) ", TParser::mixed); + let mixed = parsed + .tree() + .as_rule() + .expect("mixed rule") + .downcast_ref::() + .expect("typed mixed context"); + assert_eq!(mixed.name().expect("name label").text(), "f"); + assert_eq!( + mixed.errors().count(), + 0, + "the absent throws clause contributes no errors" + ); + } } "#, ); diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ActionAfterNestedChoice.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ActionAfterNestedChoice.g4 new file mode 100644 index 00000000..b0493570 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ActionAfterNestedChoice.g4 @@ -0,0 +1,3 @@ +grammar ActionAfterNestedChoice; +r : (A | xs+=A) { let _: Vec<_> = $xs.collect(); } EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ActionInCollapsibleChoice.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ActionInCollapsibleChoice.g4 new file mode 100644 index 00000000..d4555883 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ActionInCollapsibleChoice.g4 @@ -0,0 +1,3 @@ +grammar ActionInCollapsibleChoice; +r : x=A? (A | B { println!("{}", $x.text); }) EOF ; +A:'a'; B:'b'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ActionInsideTakenGroup.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ActionInsideTakenGroup.g4 new file mode 100644 index 00000000..b2ca42b4 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ActionInsideTakenGroup.g4 @@ -0,0 +1,3 @@ +grammar ActionInsideTakenGroup; +r : (A x=A { println!("{}", $x.text); })? EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ActionOnlyBranch.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ActionOnlyBranch.g4 new file mode 100644 index 00000000..a3e2932d --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ActionOnlyBranch.g4 @@ -0,0 +1,3 @@ +grammar ActionOnlyBranch; +r : x=A? (A | { println!("{}", $x.text); }) EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/AliasDeclarationsInChoice.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/AliasDeclarationsInChoice.g4 new file mode 100644 index 00000000..d3dddb02 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/AliasDeclarationsInChoice.g4 @@ -0,0 +1,5 @@ +grammar AliasDeclarationsInChoice; +r +@after { println!("{}", $x.text); } + : (x=A | x='a') EOF ; +A : 'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/AliasDifferingOccurrenceInAlt.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/AliasDifferingOccurrenceInAlt.g4 new file mode 100644 index 00000000..a235f7f4 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/AliasDifferingOccurrenceInAlt.g4 @@ -0,0 +1,5 @@ +grammar AliasDifferingOccurrenceInAlt; +r +@after { println!("{}", $x.text); } + : A x='a' B | A x=A C ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ChoicePrefixOutsideLabel.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ChoicePrefixOutsideLabel.g4 new file mode 100644 index 00000000..336971cf --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ChoicePrefixOutsideLabel.g4 @@ -0,0 +1,3 @@ +grammar ChoicePrefixOutsideLabel; +r : (A B | A C) x=A { println!("{}", $x.text); } ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ClosedRepeatedGroupPrefix.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ClosedRepeatedGroupPrefix.g4 new file mode 100644 index 00000000..9ba94718 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ClosedRepeatedGroupPrefix.g4 @@ -0,0 +1,3 @@ +grammar ClosedRepeatedGroupPrefix; +r : ((A B)+ x=A { println!("{}", $x.text); })? EOF ; +A : [ac]; B : 'b'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/CollapsedBlockIsTerminal.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/CollapsedBlockIsTerminal.g4 new file mode 100644 index 00000000..253b6ecf --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/CollapsedBlockIsTerminal.g4 @@ -0,0 +1,5 @@ +grammar CollapsedBlockIsTerminal; +r +@after { println!("{}", $x.text); } + : (B) x=A | x='a' ; +A:'a'; B:'b'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ConjuredLiteralLabel.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ConjuredLiteralLabel.g4 new file mode 100644 index 00000000..0a6d7c88 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ConjuredLiteralLabel.g4 @@ -0,0 +1,2 @@ +grammar ConjuredLiteralLabel; +a : 'a' x='b' { println!("conjured={}", $x); } 'c' ; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/DeclarationsDifferingOccurrence.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/DeclarationsDifferingOccurrence.g4 new file mode 100644 index 00000000..7ad9c08d --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/DeclarationsDifferingOccurrence.g4 @@ -0,0 +1,3 @@ +grammar DeclarationsDifferingOccurrence; +r : (A x=A B | x=A C) { println!("{}", $x.text); } ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/DeclarationsDifferingRepetition.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/DeclarationsDifferingRepetition.g4 new file mode 100644 index 00000000..97b3e216 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/DeclarationsDifferingRepetition.g4 @@ -0,0 +1,3 @@ +grammar DeclarationsDifferingRepetition; +r : (x=A B | x=A+ C) { println!("{}", $x.text); } ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ExhaustiveChoicePrefix.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ExhaustiveChoicePrefix.g4 new file mode 100644 index 00000000..58079590 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ExhaustiveChoicePrefix.g4 @@ -0,0 +1,3 @@ +grammar ExhaustiveChoicePrefix; +r : (a=A | b=A) x=A EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ExhaustiveInnerChoiceInAction.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ExhaustiveInnerChoiceInAction.g4 new file mode 100644 index 00000000..8fbce6de --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ExhaustiveInnerChoiceInAction.g4 @@ -0,0 +1,4 @@ +grammar ExhaustiveInnerChoiceInAction; +r : (((y=A | z=A) x=A {System.out.println($x.text);}) | C) EOF; +A : 'a'; +C : 'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ExpandedBlockTerminalState.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ExpandedBlockTerminalState.g4 new file mode 100644 index 00000000..8a196d4b --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ExpandedBlockTerminalState.g4 @@ -0,0 +1,5 @@ +grammar ExpandedBlockTerminalState; +r @after {System.out.println($x.text);} : (B y=C? | ) x=A | x='a'; +A : 'a'; +B : 'b'; +C : 'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/FallbackReadSiblingAlternative.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/FallbackReadSiblingAlternative.g4 new file mode 100644 index 00000000..5647f9c4 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/FallbackReadSiblingAlternative.g4 @@ -0,0 +1,5 @@ +grammar FallbackReadSiblingAlternative; +r +@after { println!("{}", $x.text); } + : A? ((x=(B|C))) | D ; +A:'a'; B:'b'; C:'c'; D:'d'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ForwardBlockLabel.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ForwardBlockLabel.g4 new file mode 100644 index 00000000..1afcc9a2 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ForwardBlockLabel.g4 @@ -0,0 +1,3 @@ +grammar ForwardBlockLabel; +r : A { println!("{}", $x.text); } B? x=(C|D) EOF ; +A:'a'; B:'b'; C:'c'; D:'d'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/IdenticalDeclarationsInChoice.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/IdenticalDeclarationsInChoice.g4 new file mode 100644 index 00000000..d4679d32 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/IdenticalDeclarationsInChoice.g4 @@ -0,0 +1,3 @@ +grammar IdenticalDeclarationsInChoice; +r : (x=A | x=A) { println!("{}", $x.text); } EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/InitActionBeforeChildren.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/InitActionBeforeChildren.g4 new file mode 100644 index 00000000..89df4a04 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/InitActionBeforeChildren.g4 @@ -0,0 +1,5 @@ +grammar InitActionBeforeChildren; +r +@init { let _: Vec<_> = $xs.collect(); } + : xs+=A A ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/InitScalarRuleLabel.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/InitScalarRuleLabel.g4 new file mode 100644 index 00000000..8dd87b31 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/InitScalarRuleLabel.g4 @@ -0,0 +1,6 @@ +grammar InitScalarRuleLabel; +r +@init { let _ = $x.ctx; } + : x=q EOF ; +q : A ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/InlineActionBeforeLabel.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/InlineActionBeforeLabel.g4 new file mode 100644 index 00000000..9b00b74e --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/InlineActionBeforeLabel.g4 @@ -0,0 +1,3 @@ +grammar InlineActionBeforeLabel; +r : { println!("{}", $x.text); } A* x=A EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/InnerChoiceArityInAction.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/InnerChoiceArityInAction.g4 new file mode 100644 index 00000000..68592bdb --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/InnerChoiceArityInAction.g4 @@ -0,0 +1,5 @@ +grammar InnerChoiceArityInAction; +r : (((y=A | z=A | B) x=A {System.out.println($x.text);}) | C) EOF; +A : 'a'; +B : 'b'; +C : 'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/InnerGroupClosedBeforeAction.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/InnerGroupClosedBeforeAction.g4 new file mode 100644 index 00000000..d0f232ed --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/InnerGroupClosedBeforeAction.g4 @@ -0,0 +1,4 @@ +grammar InnerGroupClosedBeforeAction; +r : ((q)? x=q { println!("{}", $x.text); })? EOF ; +q : A ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ListAliasAcrossModes.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ListAliasAcrossModes.g4 new file mode 100644 index 00000000..e0aecd5c --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ListAliasAcrossModes.g4 @@ -0,0 +1,3 @@ +grammar ListAliasAcrossModes; +r @after { let _: Vec<_> = $xs.collect(); } : xs+=A | xs+='a'; +A : 'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ListAliasDeclarations.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ListAliasDeclarations.g4 new file mode 100644 index 00000000..8f3f995c --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ListAliasDeclarations.g4 @@ -0,0 +1,3 @@ +grammar ListAliasDeclarations; +r : (xs+=A B | xs+='a' C) { let _: Vec<_> = $xs.collect(); } ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ListDeclarationsDifferingStart.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ListDeclarationsDifferingStart.g4 new file mode 100644 index 00000000..e84b4214 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ListDeclarationsDifferingStart.g4 @@ -0,0 +1,3 @@ +grammar ListDeclarationsDifferingStart; +r : (A xs+=A | xs+=A) EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ListLabelWithoutIterator.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ListLabelWithoutIterator.g4 new file mode 100644 index 00000000..7310860b --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ListLabelWithoutIterator.g4 @@ -0,0 +1,4 @@ +grammar ListLabelWithoutIterator; +r @after { let _: Vec<_> = $xs.collect(); } : xs+='a' | B xs+='a'; +A : 'a'; +B : 'b'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ListPrefixRepeatsWithLabel.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ListPrefixRepeatsWithLabel.g4 new file mode 100644 index 00000000..89f8873a --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ListPrefixRepeatsWithLabel.g4 @@ -0,0 +1,3 @@ +grammar ListPrefixRepeatsWithLabel; +r : (A xs+=A)+ EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/LiteralAliasDifferingOccurrence.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/LiteralAliasDifferingOccurrence.g4 new file mode 100644 index 00000000..365775bb --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/LiteralAliasDifferingOccurrence.g4 @@ -0,0 +1,5 @@ +grammar LiteralAliasDifferingOccurrence; +r +@after { println!("{}", $x.text); } + : B x='a' | C A x=A ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/LiteralAliasSameOccurrence.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/LiteralAliasSameOccurrence.g4 new file mode 100644 index 00000000..9f357c5c --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/LiteralAliasSameOccurrence.g4 @@ -0,0 +1,5 @@ +grammar LiteralAliasSameOccurrence; +r +@after { println!("{}", $x.text); } + : x=A | x='a' ; +A : 'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/MandatoryInnerGroupClosed.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/MandatoryInnerGroupClosed.g4 new file mode 100644 index 00000000..2ff3c78f --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/MandatoryInnerGroupClosed.g4 @@ -0,0 +1,4 @@ +grammar MandatoryInnerGroupClosed; +r : ((q) x=q { println!("{}", $x.text); })? EOF ; +q : A ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/MergedDeclarationOptionalFollower.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/MergedDeclarationOptionalFollower.g4 new file mode 100644 index 00000000..825acaf9 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/MergedDeclarationOptionalFollower.g4 @@ -0,0 +1,3 @@ +grammar MergedDeclarationOptionalFollower; +r : (x=A? B | x=B) EOF ; +A:'a'; B:'b'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/MixedModeLeadingTerminal.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/MixedModeLeadingTerminal.g4 new file mode 100644 index 00000000..e18f4482 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/MixedModeLeadingTerminal.g4 @@ -0,0 +1,5 @@ +grammar MixedModeLeadingTerminal; +r +@after { println!("{}", $x.text); } + : B x=A | x='a' ; +A:'a'; B:'b'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/NestedChoiceInsideConfinedBranch.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/NestedChoiceInsideConfinedBranch.g4 new file mode 100644 index 00000000..5c2cbf34 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/NestedChoiceInsideConfinedBranch.g4 @@ -0,0 +1,3 @@ +grammar NestedChoiceInsideConfinedBranch; +r : ((a=A | b=B) x=(C | D) { println!("{}", $x.text); } | E) EOF ; +A:'a'; B:'b'; C:'c'; D:'d'; E:'e'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/NestedChoiceSatisfiability.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/NestedChoiceSatisfiability.g4 new file mode 100644 index 00000000..7d12c4cf --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/NestedChoiceSatisfiability.g4 @@ -0,0 +1,8 @@ +grammar NestedChoiceSatisfiability; +r +@after { println!("{}", $x.text); } + : q x=q | ((q|b)|(q|c)) ; +q : A ; +b : B ; +c : C ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/NotSetLabelBeforeTerminal.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/NotSetLabelBeforeTerminal.g4 new file mode 100644 index 00000000..a9eee504 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/NotSetLabelBeforeTerminal.g4 @@ -0,0 +1,3 @@ +grammar NotSetLabelBeforeTerminal; +a : t=~'x' 'z' { println!("{}", $t.text); } ; +X : 'x' ; Z : 'z' ; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/OptionalBlockFollowedByTerminal.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/OptionalBlockFollowedByTerminal.g4 new file mode 100644 index 00000000..12ed9ff9 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/OptionalBlockFollowedByTerminal.g4 @@ -0,0 +1,3 @@ +grammar OptionalBlockFollowedByTerminal; +r : x=(A | B)? C { println!("{}", $x.text); } EOF ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/OptionalGroupSharedWithLabel.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/OptionalGroupSharedWithLabel.g4 new file mode 100644 index 00000000..4a526619 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/OptionalGroupSharedWithLabel.g4 @@ -0,0 +1,3 @@ +grammar OptionalGroupSharedWithLabel; +r : (A x=A)? EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/PrecedingSiblingBranch.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/PrecedingSiblingBranch.g4 new file mode 100644 index 00000000..d6907ec0 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/PrecedingSiblingBranch.g4 @@ -0,0 +1,3 @@ +grammar PrecedingSiblingBranch; +r : (A | x=A) {System.out.println($x.text);} EOF; +A : 'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/ReassignedAfterAction.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/ReassignedAfterAction.g4 new file mode 100644 index 00000000..6bfa8643 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/ReassignedAfterAction.g4 @@ -0,0 +1,3 @@ +grammar ReassignedAfterAction; +r : x=A { println!("{}", $x.text); } x=A EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/RecoveredDeletedTokenIndex.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/RecoveredDeletedTokenIndex.g4 new file mode 100644 index 00000000..293607e6 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/RecoveredDeletedTokenIndex.g4 @@ -0,0 +1,3 @@ +grammar RecoveredDeletedTokenIndex; +r : D A x=(B | C) { println!("{}", $x.text); } EOF ; +A:'a'; B:'b'; C:'c'; D:'d'; X:'x'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedBlockLabel.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedBlockLabel.g4 new file mode 100644 index 00000000..42c825a2 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedBlockLabel.g4 @@ -0,0 +1,3 @@ +grammar RepeatedBlockLabel; +r : x=(A | B)+ { println!("{}", $x.text); } EOF ; +A:'a'; B:'b'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedMergeFollowedByMatch.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedMergeFollowedByMatch.g4 new file mode 100644 index 00000000..318e25bc --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedMergeFollowedByMatch.g4 @@ -0,0 +1,3 @@ +grammar RepeatedMergeFollowedByMatch; +r : (x=A A B | x=A+ C) EOF ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedScalarMerge.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedScalarMerge.g4 new file mode 100644 index 00000000..cb892da2 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedScalarMerge.g4 @@ -0,0 +1,3 @@ +grammar RepeatedScalarMerge; +r : (x=A+ | x=B+) EOF ; +A:'a'; B:'b'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedSiblingSpansIndex.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedSiblingSpansIndex.g4 new file mode 100644 index 00000000..77eecec6 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/RepeatedSiblingSpansIndex.g4 @@ -0,0 +1,5 @@ +grammar RepeatedSiblingSpansIndex; +r +@after { println!("{}", $x.text); } + : C x=(A | B) | (D | E)+ ; +A:'a'; B:'b'; C:'c'; D:'d'; E:'e'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/SiblingBranchShorterThanOccurrence.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/SiblingBranchShorterThanOccurrence.g4 new file mode 100644 index 00000000..a27f2270 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/SiblingBranchShorterThanOccurrence.g4 @@ -0,0 +1,3 @@ +grammar SiblingBranchShorterThanOccurrence; +r : (A | A A x=A) EOF ; +A:'a'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/SiblingDeclarationIrrelevant.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/SiblingDeclarationIrrelevant.g4 new file mode 100644 index 00000000..79ebecbe --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/SiblingDeclarationIrrelevant.g4 @@ -0,0 +1,3 @@ +grammar SiblingDeclarationIrrelevant; +r : (x=A { println!("{}", $x.text); } | x=A+ B) EOF ; +A:'a'; B:'b'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/SiblingUnlabeledSameTarget.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/SiblingUnlabeledSameTarget.g4 new file mode 100644 index 00000000..d0b2a846 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/SiblingUnlabeledSameTarget.g4 @@ -0,0 +1,3 @@ +grammar SiblingUnlabeledSameTarget; +r : (B | C x=A? A { println!("{}", $x.text); }) EOF ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/label-resolution/UnrelatedLaterChoiceConfinement.g4 b/tests/fixtures/antlr4-rust-gen/label-resolution/UnrelatedLaterChoiceConfinement.g4 new file mode 100644 index 00000000..153c4160 --- /dev/null +++ b/tests/fixtures/antlr4-rust-gen/label-resolution/UnrelatedLaterChoiceConfinement.g4 @@ -0,0 +1,3 @@ +grammar UnrelatedLaterChoiceConfinement; +r : ({false}? x=A | A) (B { println!("{}", $x.text); } | C) EOF ; +A:'a'; B:'b'; C:'c'; diff --git a/tests/fixtures/antlr4-rust-gen/multi-alternative-label/T.g4 b/tests/fixtures/antlr4-rust-gen/multi-alternative-label/T.g4 index ffb70ddb..3851f0ef 100644 --- a/tests/fixtures/antlr4-rust-gen/multi-alternative-label/T.g4 +++ b/tests/fixtures/antlr4-rust-gen/multi-alternative-label/T.g4 @@ -27,6 +27,116 @@ unary | NUM ; +// Issue #201, shape 1: the labels sit *inside* an unlabeled grouping block, so +// collapsing the block into one token-group ref would swallow them. Mirrors +// avdl's `(doc=DocComment)?` and `(oneway=Oneway | Throws errors+=...)?`. +grouped + : (doc = IDENT)? (oneway = STAR | IN errors += unary (COMMA errors += unary)*)? NUM + ; + +// Issue #201, shape 2: `name` and `errors` label the same rule, mixing a single +// and a list label, so each accessor must resolve past the other's children. +// Mirrors avdl's `messageDeclaration`; `param` stands in for its +// `formalParameter`, keeping the `unary` count before `errors` exact. +mixed + : name = unary LPAREN param* RPAREN (IN errors += unary (COMMA errors += unary)*)? + ; + +// A variable count of the label's own target in front of it leaves no fixed +// `.skip(N)`, so the list accessor must be declined even though the labels +// themselves are unambiguous. +mixed_unbounded + : unary* IN errors += unary + ; + +param + : IDENT + ; + +// Only one branch of the choice supplies `pick`, and its sibling branch matches +// the same rule unlabeled at the same flattened position — `.nth(0)` could read +// the sibling's child, so no accessor may be emitted. +branch_hazard + : (pick = unary | STAR unary) NUM + ; + +// Same target labeled differently per branch: neither label may read the +// other branch's child. +branch_rival + : (left = unary | right = unary) NUM + ; + +// An *exhaustive* choice contributes exactly one `unary` however it branches, so +// the following label is reliably the second one and keeps its accessor. +exhaustive_prefix + : (first = unary | second = unary) pick = unary NUM + ; + +// A repeated scalar label over mutually exclusive branches: ANTLR overwrites a +// scalar on every iteration, so the merged read must select the *last* match. The +// trailing `LPAREN` is outside the accessor's token union, so nothing can become +// the `last()` ahead of the label — with a trailing `NUM` it could, and the merge +// would have to decline. +merged_repeats + : (last_pick = IDENT+ | last_pick = NUM+) LPAREN + ; + +// One label declared over mutually exclusive branches at the same occurrence: the +// accessor's unioned token set selects whichever token the parse bound, so a +// single positional read serves both. +merged_rivals + : (pick = IDENT | pick = NUM) NUM + ; + +// A label behind a sibling branch: restricting to the label's own path leaves one +// preceding `unary`, so the accessor is emitted at that fixed position. +path_restricted + : (unary tail = unary | NUM) NUM + ; + +// Nested exhaustive choices: every path still yields exactly one `unary` before +// the label, so the inner choice's agreed count rolls up into the outer branch +// and the accessor survives. +nested_exhaustive_prefix + : ((one = unary | two = unary) | three = unary) chosen = unary NUM + ; + +// The same choice made *optional*: it may now contribute no `unary` at all, so +// the following label has no fixed position and loses its accessor. Only the +// branch-local cardinality separates this from `exhaustive_prefix` above. +optional_prefix + : (early = unary | late = unary)? tail = unary NUM + ; + +// A preceding token group overlaps the label's token type, so only some parses +// put a matching child ahead of it — no fixed occurrence, so no accessor. +overlapping_group + : (IDENT | NUM) tail = IDENT + ; + +// Extra grouping levels are syntactically inert, so a label buried under them +// must still defeat the token-group collapse that would discard it. +nested_group + : ((deep = IDENT))? NUM + ; + +// The outer `?` is satisfied wherever the label binds, but the *inner* `+` is a +// closed repeated group: it ran an unknown number of times, so the count of +// matching children ahead of the label is not fixed and the accessor must go. +// Only the repeated-group carve-out separates this from `nested_group`'s +// all-shared-groups case. +closed_repeat_prefix + : ((IDENT STAR)+ tail = IDENT)? NUM + ; + +// Restricting to the label's path drops the *outer* choice, so the surviving +// inner choice must be read with its own arity of three. Treating it as an +// exhaustive two-way choice would fix the prefix count at one and emit +// `.nth(1)`, which reads nothing on the `param` path. +inner_choice_arity + : ((unary | unary | param) tail = unary | NUM) NUM + ; + LESS : '<' ; @@ -83,6 +193,20 @@ NUM : [0-9]+ ; +// Declared last so the issue #201 rules above do not renumber the token types +// the pre-existing context snapshots pin. +COMMA + : ',' + ; + +LPAREN + : '(' + ; + +RPAREN + : ')' + ; + WS : [ \t\r\n]+ -> skip ;