Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 46 additions & 8 deletions src/bin/antlr4-rust-gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8214,19 +8214,47 @@ fn context_label_accessor(
.filter(|element| element.label.as_deref() == Some(label.as_str()))
.collect::<Vec<_>>();
let first = declarations.first()?;
// Token-backed declarations may carry different token sets per alternative
// (`calc op=('*'|'/'|'%') calc | calc op=('+'|'-') calc`); ANTLR still
Comment thread
tinovyatkin marked this conversation as resolved.
Resolved
// declares a single `Token op` field, so union the sets and let the
// per-alternative selector checks below reject layouts where one
// occurrence lookup cannot serve every alternative. Rule references keep
// requiring one shared target.
let union_token_sets = declarations
.iter()
.all(|element| !element.token_types.is_empty());
if (first.target.is_empty() && first.token_types.is_empty())
|| declarations.iter().any(|element| {
!element.stable_accessor
|| !same_context_ref_target(element, first)
|| element.is_list != first.is_list
|| !(union_token_sets || same_context_ref_target(element, first))
})
{
return None;
}

let target = first.target.clone();
let token_types = first.token_types.clone();
let is_list = first.is_list;
let shared_target = declarations
.iter()
.all(|element| element.target == first.target);
let reference = embedded::ElementRef {
label: None,
target: if shared_target {
first.target.clone()
} else {
String::new()
},
token_types: declarations
.iter()
.flat_map(|element| element.token_types.iter().copied())
.collect::<BTreeSet<_>>()
.into_iter()
.collect(),
Comment thread
tinovyatkin marked this conversation as resolved.
Resolved
is_block: first.is_block,
is_list,
cardinality: embedded::ChildCardinality::ONE,
stable_accessor: true,
};
let mut selector = None;
let mut cardinalities = Vec::with_capacity(alternatives.len());
for alternative in alternatives {
Expand All @@ -8241,7 +8269,7 @@ fn context_label_accessor(
alternative
.refs
.iter()
.filter(|element| context_ref_can_match_target(element, first))
.filter(|element| context_ref_can_match_target(element, &reference))
.map(|element| element.cardinality),
);
if target_cardinality.max != Some(0) {
Expand All @@ -8252,7 +8280,7 @@ fn context_label_accessor(
}

let alternative_selector =
context_label_selector(alternative, &matching, &label, first, is_list)?;
context_label_selector(alternative, &matching, &label, &reference, is_list)?;
if selector.is_some_and(|existing| existing != alternative_selector) {
return None;
}
Expand All @@ -8273,8 +8301,8 @@ fn context_label_accessor(
}
Some(ContextLabelAccessor {
source_name: label,
target,
token_types,
target: reference.target,
token_types: reference.token_types,
cardinality,
selector: selector?,
})
Expand Down Expand Up @@ -8302,7 +8330,17 @@ fn context_label_selector(
}
let element = matching[0].1;
if !element.cardinality.is_repeated() {
return Some(ContextLabelSelector::Nth(start));
// An optional labeled occurrence (`x=A? C` with `C` in the accessor's
// token set) must not be shadowed by a following match sliding into
// its position when it is absent.
let shadowed_when_absent = element.cardinality.min == 0
&& alternative.refs[first_position + 1..]
.iter()
.any(|following| {
context_ref_can_match_target(following, target)
&& following.cardinality.max != Some(0)
});
return (!shadowed_when_absent).then_some(ContextLabelSelector::Nth(start));
}
let has_following_target = alternative.refs[first_position + 1..]
.iter()
Expand Down
71 changes: 71 additions & 0 deletions tests/antlr4_rust_gen_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,77 @@ mod grouped_token_tests {
);
}

#[test]
fn token_group_label_shared_across_alternatives_unions_the_sets() {
let temp = temporary_directory("multi-alternative-label");
let grammar = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/antlr4-rust-gen/multi-alternative-label/T.g4");
let out = temp.path().join("generated");

let output = run_antlr4_rust_gen(&[
grammar.as_os_str(),
OsStr::new("--out-dir"),
out.as_os_str(),
]);
assert!(
output.status.success(),
"stdout: {}\nstderr: {}",
utf8(&output.stdout),
utf8(&output.stderr)
);
assert_generated_project(
temp.path(),
&["t_lexer.rs", "t_parser.rs"],
r#"
#[cfg(test)]
mod multi_alternative_label_tests {
use super::t_lexer::TLexer;
use super::t_parser::*;
use antlr4_runtime::{CommonTokenStream, InputStream, Parser as _};

fn parse(input: &str) -> 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");
assert_eq!(parser.number_of_syntax_errors(), 0);
parser.into_parsed_file(root)
}

#[test]
fn op_label_is_available_on_every_alternative_group() {
let parsed = parse("a * b + c < d");
let expr = parsed
.tree()
.as_rule()
.expect("expr rule")
.downcast_ref::<ExprContext>()
.expect("typed expr context");
let relation = expr.relation().expect("relation child");
assert_eq!(relation.op().expect("relation operator").to_string(), "<");

let calc = relation
.relation_children()
.next()
.expect("left relation")
.calc()
.expect("calc child");
assert_eq!(calc.op().expect("additive operator").to_string(), "+");

let product = calc.calc_children().next().expect("left calc");
assert_eq!(
product.op().expect("multiplicative operator").to_string(),
"*"
);

let leaf = product.calc_children().next().expect("leaf calc");
assert!(leaf.op().is_none(), "primary alternative carries no operator");
}
}
"#,
);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

#[test]
fn compile_parse_tree_pattern_matches_and_binds_against_generated_parser() {
let temp = temporary_directory("tree-pattern-compile");
Expand Down
81 changes: 81 additions & 0 deletions tests/fixtures/antlr4-rust-gen/multi-alternative-label/T.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
grammar T;

expr
: relation EOF
;

relation
: calc
| relation op = ('<' | '<=' | '>=' | '>' | '==' | '!=' | 'in') relation
;

calc
: unary
| calc op = ('*' | '/' | '%') calc
| calc op = ('+' | '-') calc
;

unary
: IDENT
| NUM
;

LESS
: '<'
;

LESS_EQUALS
: '<='
;

GREATER_EQUALS
: '>='
;

GREATER
: '>'
;

EQUALS
: '=='
;

NOT_EQUALS
: '!='
;

IN
: 'in'
;

STAR
: '*'
;

SLASH
: '/'
;

PERCENT
: '%'
;

PLUS
: '+'
;

MINUS
: '-'
;

IDENT
: [a-zA-Z_] [a-zA-Z0-9_]*
;

NUM
: [0-9]+
;

WS
: [ \t\r\n]+ -> skip
;
Loading