use std::collections::HashSet;
#[derive(Debug, Clone, Default)]
pub struct LocalSymbolTable {
known_subs: HashSet<Box<str>>,
}
impl LocalSymbolTable {
pub fn scan_subs(input: &str) -> Self {
let mut known_subs = HashSet::new();
let bytes = input.as_bytes();
let len = bytes.len();
let mut i = 0;
while i < len {
match bytes[i] {
b'#' => {
while i < len && bytes[i] != b'\n' {
i += 1;
}
}
b'\'' => {
i += 1;
while i < len {
if bytes[i] == b'\\' && i + 1 < len {
i += 2; } else if bytes[i] == b'\'' {
i += 1;
break;
} else {
i += 1;
}
}
}
b'"' => {
i += 1;
while i < len {
if bytes[i] == b'\\' && i + 1 < len {
i += 2;
} else if bytes[i] == b'"' {
i += 1;
break;
} else {
i += 1;
}
}
}
b's' if i + 3 <= len && bytes[i..i + 3] == *b"sub" => {
let prev_ident = i > 0 && is_ident_byte(bytes[i - 1]);
let next_ident = i + 3 < len && is_ident_byte(bytes[i + 3]);
if !prev_ident && !next_ident {
let mut j = i + 3;
while j < len && matches!(bytes[j], b' ' | b'\t' | b'\r' | b'\n') {
j += 1;
}
if j < len && (bytes[j].is_ascii_alphabetic() || bytes[j] == b'_') {
let start = j;
while j < len && is_ident_byte(bytes[j]) {
j += 1;
}
known_subs.insert(input[start..j].into());
}
i = j;
} else {
i += 1;
}
}
_ => {
i += 1;
}
}
}
Self { known_subs }
}
pub fn is_known_sub(&self, name: &str) -> bool {
self.known_subs.contains(name)
}
pub fn len(&self) -> usize {
self.known_subs.len()
}
pub fn is_empty(&self) -> bool {
self.known_subs.is_empty()
}
}
fn is_ident_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_'
}
#[cfg(test)]
mod tests {
use super::LocalSymbolTable;
#[test]
fn scan_subs_empty_source_gives_empty_table() {
assert!(LocalSymbolTable::scan_subs("").is_empty());
}
#[test]
fn scan_subs_single_declaration_is_recognized() {
let st = LocalSymbolTable::scan_subs("sub foo { }");
assert!(st.is_known_sub("foo"));
assert_eq!(st.len(), 1);
}
#[test]
fn scan_subs_multiple_declarations_are_all_recognized() {
let src = "sub alpha { }\nsub beta { }\nsub gamma { }";
let st = LocalSymbolTable::scan_subs(src);
assert!(st.is_known_sub("alpha"));
assert!(st.is_known_sub("beta"));
assert!(st.is_known_sub("gamma"));
assert_eq!(st.len(), 3);
}
#[test]
fn scan_subs_forward_declaration_with_semicolon() {
let st = LocalSymbolTable::scan_subs("sub builder;");
assert!(st.is_known_sub("builder"));
}
#[test]
fn scan_subs_prototype_declaration() {
let st = LocalSymbolTable::scan_subs("sub transform ($$) { }");
assert!(st.is_known_sub("transform"));
}
#[test]
fn scan_subs_underscore_prefix() {
let st = LocalSymbolTable::scan_subs("sub _private { }");
assert!(st.is_known_sub("_private"));
}
#[test]
fn scan_subs_alphanumeric_name_with_digits() {
let st = LocalSymbolTable::scan_subs("sub process2 { }");
assert!(st.is_known_sub("process2"));
}
#[test]
fn scan_subs_skips_line_comment() {
let src = "# sub commented_out { }\nsub real_sub { }";
let st = LocalSymbolTable::scan_subs(src);
assert!(!st.is_known_sub("commented_out"), "should not register commented-out subs");
assert!(st.is_known_sub("real_sub"));
}
#[test]
fn scan_subs_inline_comment_after_declaration() {
let src = "sub real { } # sub fake { }";
let st = LocalSymbolTable::scan_subs(src);
assert!(st.is_known_sub("real"));
assert!(!st.is_known_sub("fake"));
}
#[test]
fn scan_subs_skips_single_quoted_string() {
let src = r#"my $x = 'sub in_string { }'; sub real { }"#;
let st = LocalSymbolTable::scan_subs(src);
assert!(!st.is_known_sub("in_string"), "should not register subs in string literals");
assert!(st.is_known_sub("real"));
}
#[test]
fn scan_subs_skips_double_quoted_string() {
let src = r#"my $x = "sub in_string { }"; sub real { }"#;
let st = LocalSymbolTable::scan_subs(src);
assert!(!st.is_known_sub("in_string"));
assert!(st.is_known_sub("real"));
}
#[test]
fn scan_subs_does_not_match_substring() {
let src = "my $x = substring(1, 2);";
let st = LocalSymbolTable::scan_subs(src);
assert!(!st.is_known_sub("string"), "should not match 'sub' inside 'substring'");
assert!(st.is_empty());
}
#[test]
fn scan_subs_does_not_match_sub_suffix() {
let src = "my $x = foosubfoo;";
let st = LocalSymbolTable::scan_subs(src);
assert!(st.is_empty());
}
#[test]
fn is_known_sub_unknown_name_returns_false() {
let st = LocalSymbolTable::scan_subs("sub foo { }");
assert!(!st.is_known_sub("bar"));
assert!(!st.is_known_sub(""));
}
#[test]
fn default_produces_empty_table() {
let st = LocalSymbolTable::default();
assert!(st.is_empty());
assert!(!st.is_known_sub("anything"));
}
}