use super::scan::Token;
const ACTIVITY_WRAPPER_SUFFIX: &str = "_activity";
pub(super) fn find_open_brace(tokens: &[Token], start: usize, end: usize) -> Option<usize> {
(start..end.min(tokens.len())).find(|&index| matches!(tokens[index], Token::OpenBrace))
}
pub(super) fn match_brace(tokens: &[Token], open: usize, end: usize) -> Option<usize> {
let mut depth = 0_i32;
let upper = end.min(tokens.len());
for (index, token) in tokens.iter().enumerate().take(upper).skip(open) {
match token {
Token::OpenBrace => depth += 1,
Token::CloseBrace => {
depth -= 1;
if depth == 0 {
return Some(index);
}
}
_ => {}
}
}
None
}
pub(super) fn end_of_call(tokens: &[Token], call: usize, end: usize) -> usize {
let upper = end.min(tokens.len());
let mut index = call + 1;
if index >= upper {
return upper;
}
if !matches!(tokens[index], Token::OpenParen) {
return index;
}
let mut depth = 0_i32;
while index < upper {
match tokens[index] {
Token::OpenParen => depth += 1,
Token::CloseParen => {
depth -= 1;
if depth == 0 {
return index + 1;
}
}
_ => {}
}
index += 1;
}
upper
}
pub(super) fn run_activity_name(args: &[Token]) -> Option<String> {
let mut index = 0;
if matches!(args.first(), Some(Token::OpenParen)) {
index = 1;
}
if let Some(Token::Qualified { left: _, right }) = args.get(index) {
if let Some(name) = right.strip_suffix(ACTIVITY_WRAPPER_SUFFIX) {
if !name.is_empty() {
return Some(name.to_owned());
}
}
}
None
}
pub(super) fn first_string_literal(args: &[Token]) -> Option<String> {
let mut depth = 0_i32;
for token in args {
match token {
Token::OpenParen => depth += 1,
Token::CloseParen => {
depth -= 1;
if depth <= 0 {
return None;
}
}
Token::StringLiteral(literal) if depth >= 1 => return Some(literal.clone()),
_ => {}
}
}
None
}
pub(super) fn fan_out_members(args: &[Token]) -> Vec<String> {
let mut members = Vec::new();
let mut depth = 0_i32;
let mut started = false;
for token in args {
match token {
Token::OpenParen => {
depth += 1;
started = true;
}
Token::CloseParen => {
depth -= 1;
if started && depth <= 0 {
break;
}
}
Token::Qualified { right, .. } if depth >= 1 => {
if let Some(name) = right.strip_suffix(ACTIVITY_WRAPPER_SUFFIX) {
if !name.is_empty() {
members.push(name.to_owned());
}
}
}
_ => {}
}
}
members
}
pub(super) fn first_argument_reference(args: &[Token]) -> Option<String> {
let mut depth = 0_i32;
for token in args {
match token {
Token::OpenParen => depth += 1,
Token::CloseParen => {
depth -= 1;
if depth <= 0 {
return None;
}
}
Token::Ident(word) if depth >= 1 => return Some(word.clone()),
Token::Qualified { left, right } if depth >= 1 => {
return Some(format!("{left}.{right}"));
}
_ => {}
}
}
None
}
pub(super) fn last_identifier_argument(
tokens: &[Token],
start: usize,
end: usize,
) -> Option<String> {
let upper = end.min(tokens.len());
let mut depth = 0_i32;
let mut last: Option<String> = None;
for token in tokens.iter().take(upper).skip(start) {
match token {
Token::OpenParen => depth += 1,
Token::CloseParen => depth -= 1,
Token::Ident(word) if depth == 1 => last = Some(word.clone()),
Token::Qualified { .. } if depth == 1 => last = None,
_ => {}
}
}
last
}
pub(super) fn leading_local_call(tokens: &[Token], start: usize, end: usize) -> Option<String> {
let upper = end.min(tokens.len());
if start >= upper {
return None;
}
if let Token::Ident(name) = &tokens[start] {
if matches!(tokens.get(start + 1), Some(Token::OpenParen)) {
return Some(name.clone());
}
}
None
}
pub(super) fn snippet(tokens: &[Token]) -> String {
let mut out = String::new();
for token in tokens {
if !out.is_empty() {
out.push(' ');
}
match token {
Token::Ident(word) => out.push_str(word),
Token::Qualified { left, right } => {
out.push_str(left);
out.push('.');
out.push_str(right);
}
Token::OpenParen => out.push('('),
Token::CloseParen => out.push(')'),
Token::OpenBrace => out.push('{'),
Token::CloseBrace => out.push('}'),
Token::Arrow => out.push_str("->"),
Token::Comma => out.push(','),
Token::StringLiteral(literal) => {
out.push('"');
out.push_str(literal);
out.push('"');
}
Token::Other(c) => out.push(*c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::structure::scan::tokenise;
#[test]
fn match_brace_honours_nesting() {
let tokens = tokenise("{ a { b } c }");
let close = match_brace(&tokens, 0, tokens.len())
.unwrap_or_else(|| unreachable!("the source is brace-balanced"));
assert!(matches!(tokens[close], Token::CloseBrace));
assert_eq!(close, tokens.len() - 1);
}
#[test]
fn end_of_call_skips_balanced_args() {
let tokens = tokenise("run(wrappers.a_activity(x)) next");
let after = end_of_call(&tokens, 0, tokens.len());
assert_eq!(tokens.get(after), Some(&Token::Ident("next".to_owned())));
}
#[test]
fn run_activity_name_strips_suffix() {
let tokens = tokenise("(wrappers.reserve_inventory_activity(input))");
assert_eq!(
run_activity_name(&tokens),
Some("reserve_inventory".to_owned())
);
}
#[test]
fn last_identifier_argument_reads_the_define_entry() {
let tokens = tokenise("(\"gate\", codecs.a(), codecs.b(), codecs.c(), execute)");
assert_eq!(
last_identifier_argument(&tokens, 0, tokens.len()),
Some("execute".to_owned())
);
}
#[test]
fn last_identifier_argument_rejects_a_qualified_last_argument() {
let tokens = tokenise("(\"gate\", codecs.a(), helpers.execute)");
assert_eq!(last_identifier_argument(&tokens, 0, tokens.len()), None);
}
#[test]
fn last_identifier_argument_ignores_nested_call_arguments() {
let tokens = tokenise("(\"gate\", wrap(input), execute)");
assert_eq!(
last_identifier_argument(&tokens, 0, tokens.len()),
Some("execute".to_owned())
);
}
#[test]
fn leading_local_call_detects_helper() {
let tokens = tokenise("charge_payment(input, reservation)");
assert_eq!(
leading_local_call(&tokens, 0, tokens.len()),
Some("charge_payment".to_owned())
);
}
#[test]
fn leading_local_call_rejects_qualified() {
let tokens = tokenise("decode.run(raw_input, decode.string)");
assert_eq!(leading_local_call(&tokens, 0, tokens.len()), None);
}
}