use std::collections::BTreeSet;
use crate::dsl::ast::{Arg, Expr};
pub fn referenced_names(text: &str) -> BTreeSet<String> {
try_referenced_names(text).unwrap_or_default()
}
pub fn try_referenced_names(text: &str) -> Result<BTreeSet<String>, String> {
let tokens = crate::dsl::lexer::lex(text)?;
let expr = crate::dsl::parser::parse_expression(tokens)?;
let mut out = BTreeSet::new();
collect_expr_refs(&expr, &mut out);
Ok(out)
}
pub fn collect_expr_refs(expr: &Expr, out: &mut BTreeSet<String>) {
match expr {
Expr::Ident(name, _) => {
if name != "true" && name != "false" {
out.insert(name.clone());
}
}
Expr::Call(call) => {
for arg in &call.args {
let inner = match arg {
Arg::Positional(e) => e,
Arg::Named(_, e) => e,
};
collect_expr_refs(inner, out);
}
}
Expr::BinOp(lhs, _, rhs) => {
collect_expr_refs(lhs, out);
collect_expr_refs(rhs, out);
}
Expr::UnaryNeg(inner, _) | Expr::UnaryBitNot(inner, _)
| Expr::Cast(inner, _, _) => {
collect_expr_refs(inner, out);
}
Expr::ArrayLit(elems, _) => {
for e in elems {
collect_expr_refs(e, out);
}
}
Expr::FieldAccess { source, .. } => {
out.insert(source.clone());
}
Expr::StringLit(s, _) => {
collect_string_interpolation_refs(s, out);
}
Expr::IntLit(..) | Expr::FloatLit(..) => {}
}
}
pub fn collect_string_interpolation_refs(s: &str, out: &mut BTreeSet<String>) {
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] != '{' {
i += 1;
continue;
}
let body_start = i + 1;
let mut depth = 1;
let mut j = body_start;
while j < chars.len() && depth > 0 {
match chars[j] {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
if depth == 0 { break; }
j += 1;
}
if depth != 0 {
break; }
let body: String = chars[body_start..j].iter().collect();
if body.contains('{') {
collect_string_interpolation_refs(&body, out);
} else if is_plain_ident(&body) {
out.insert(body);
} else {
if let Ok(inner) = try_referenced_names(&body) {
out.extend(inner);
}
}
i = j + 1;
}
}
fn is_plain_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[cfg(test)]
mod tests {
use super::*;
fn names(text: &str) -> Vec<String> {
referenced_names(text).into_iter().collect()
}
#[test]
fn bare_identifier_is_a_reference() {
assert_eq!(names("eh_values"), vec!["eh_values"]);
}
#[test]
fn function_call_args_are_references_callee_is_not() {
assert_eq!(names("concat(nbo_v_values)"), vec!["nbo_v_values"]);
}
#[test]
fn nested_calls_collect_all_arg_idents() {
let got = names("mod(hash(cycle), p)");
assert_eq!(got, vec!["cycle", "p"]);
}
#[test]
fn string_literal_interpolation_refs() {
let got = names(r#""fknn_oat_{sm_lc}_m{mnc}""#);
assert_eq!(got, vec!["mnc", "sm_lc"]);
}
#[test]
fn string_literal_plain_text_has_no_refs() {
assert!(names(r#""just text""#).is_empty());
}
#[test]
fn field_access_references_source_wire() {
assert_eq!(names("base.vector"), vec!["base"]);
}
#[test]
fn arithmetic_operands_are_references() {
let got = names("a + b * c");
assert_eq!(got, vec!["a", "b", "c"]);
}
#[test]
fn numeric_and_bool_literals_are_not_references() {
assert!(names("1000").is_empty());
assert!(names("3.14").is_empty());
assert!(names("true").is_empty());
}
#[test]
fn unparseable_text_yields_no_refs_via_lenient_api() {
assert!(names("((((").is_empty());
assert!(try_referenced_names("((((").is_err());
}
}