perl_lexer/
symbol_table.rs1use std::collections::HashSet;
21
22#[derive(Debug, Clone, Default)]
36pub struct LocalSymbolTable {
37 known_subs: HashSet<Box<str>>,
38}
39
40impl LocalSymbolTable {
41 pub fn scan_subs(input: &str) -> Self {
49 let mut known_subs = HashSet::new();
50 let bytes = input.as_bytes();
51 let len = bytes.len();
52 let mut i = 0;
53
54 while i < len {
55 match bytes[i] {
56 b'#' => {
58 while i < len && bytes[i] != b'\n' {
59 i += 1;
60 }
61 }
62
63 b'\'' => {
66 i += 1;
67 while i < len {
68 if bytes[i] == b'\\' && i + 1 < len {
69 i += 2; } else if bytes[i] == b'\'' {
71 i += 1;
72 break;
73 } else {
74 i += 1;
75 }
76 }
77 }
78
79 b'"' => {
81 i += 1;
82 while i < len {
83 if bytes[i] == b'\\' && i + 1 < len {
84 i += 2;
85 } else if bytes[i] == b'"' {
86 i += 1;
87 break;
88 } else {
89 i += 1;
90 }
91 }
92 }
93
94 b's' if i + 3 <= len && bytes[i..i + 3] == *b"sub" => {
96 let prev_ident = i > 0 && is_ident_byte(bytes[i - 1]);
98 let next_ident = i + 3 < len && is_ident_byte(bytes[i + 3]);
100
101 if !prev_ident && !next_ident {
102 let mut j = i + 3;
103 while j < len && matches!(bytes[j], b' ' | b'\t' | b'\r' | b'\n') {
105 j += 1;
106 }
107 if j < len && (bytes[j].is_ascii_alphabetic() || bytes[j] == b'_') {
109 let start = j;
110 while j < len && is_ident_byte(bytes[j]) {
111 j += 1;
112 }
113 known_subs.insert(input[start..j].into());
115 }
116 i = j;
117 } else {
118 i += 1;
119 }
120 }
121
122 _ => {
123 i += 1;
124 }
125 }
126 }
127
128 Self { known_subs }
129 }
130
131 pub fn is_known_sub(&self, name: &str) -> bool {
133 self.known_subs.contains(name)
134 }
135
136 pub fn len(&self) -> usize {
138 self.known_subs.len()
139 }
140
141 pub fn is_empty(&self) -> bool {
143 self.known_subs.is_empty()
144 }
145}
146
147fn is_ident_byte(b: u8) -> bool {
149 b.is_ascii_alphanumeric() || b == b'_'
150}
151
152#[cfg(test)]
153mod tests {
154 use super::LocalSymbolTable;
155
156 #[test]
159 fn scan_subs_empty_source_gives_empty_table() {
160 assert!(LocalSymbolTable::scan_subs("").is_empty());
161 }
162
163 #[test]
164 fn scan_subs_single_declaration_is_recognized() {
165 let st = LocalSymbolTable::scan_subs("sub foo { }");
166 assert!(st.is_known_sub("foo"));
167 assert_eq!(st.len(), 1);
168 }
169
170 #[test]
171 fn scan_subs_multiple_declarations_are_all_recognized() {
172 let src = "sub alpha { }\nsub beta { }\nsub gamma { }";
173 let st = LocalSymbolTable::scan_subs(src);
174 assert!(st.is_known_sub("alpha"));
175 assert!(st.is_known_sub("beta"));
176 assert!(st.is_known_sub("gamma"));
177 assert_eq!(st.len(), 3);
178 }
179
180 #[test]
181 fn scan_subs_forward_declaration_with_semicolon() {
182 let st = LocalSymbolTable::scan_subs("sub builder;");
183 assert!(st.is_known_sub("builder"));
184 }
185
186 #[test]
187 fn scan_subs_prototype_declaration() {
188 let st = LocalSymbolTable::scan_subs("sub transform ($$) { }");
189 assert!(st.is_known_sub("transform"));
190 }
191
192 #[test]
193 fn scan_subs_underscore_prefix() {
194 let st = LocalSymbolTable::scan_subs("sub _private { }");
195 assert!(st.is_known_sub("_private"));
196 }
197
198 #[test]
199 fn scan_subs_alphanumeric_name_with_digits() {
200 let st = LocalSymbolTable::scan_subs("sub process2 { }");
201 assert!(st.is_known_sub("process2"));
202 }
203
204 #[test]
207 fn scan_subs_skips_line_comment() {
208 let src = "# sub commented_out { }\nsub real_sub { }";
209 let st = LocalSymbolTable::scan_subs(src);
210 assert!(!st.is_known_sub("commented_out"), "should not register commented-out subs");
211 assert!(st.is_known_sub("real_sub"));
212 }
213
214 #[test]
215 fn scan_subs_inline_comment_after_declaration() {
216 let src = "sub real { } # sub fake { }";
217 let st = LocalSymbolTable::scan_subs(src);
218 assert!(st.is_known_sub("real"));
219 assert!(!st.is_known_sub("fake"));
220 }
221
222 #[test]
225 fn scan_subs_skips_single_quoted_string() {
226 let src = r#"my $x = 'sub in_string { }'; sub real { }"#;
227 let st = LocalSymbolTable::scan_subs(src);
228 assert!(!st.is_known_sub("in_string"), "should not register subs in string literals");
229 assert!(st.is_known_sub("real"));
230 }
231
232 #[test]
233 fn scan_subs_skips_double_quoted_string() {
234 let src = r#"my $x = "sub in_string { }"; sub real { }"#;
235 let st = LocalSymbolTable::scan_subs(src);
236 assert!(!st.is_known_sub("in_string"));
237 assert!(st.is_known_sub("real"));
238 }
239
240 #[test]
243 fn scan_subs_does_not_match_substring() {
244 let src = "my $x = substring(1, 2);";
246 let st = LocalSymbolTable::scan_subs(src);
247 assert!(!st.is_known_sub("string"), "should not match 'sub' inside 'substring'");
248 assert!(st.is_empty());
249 }
250
251 #[test]
252 fn scan_subs_does_not_match_sub_suffix() {
253 let src = "my $x = foosubfoo;";
254 let st = LocalSymbolTable::scan_subs(src);
255 assert!(st.is_empty());
256 }
257
258 #[test]
261 fn is_known_sub_unknown_name_returns_false() {
262 let st = LocalSymbolTable::scan_subs("sub foo { }");
263 assert!(!st.is_known_sub("bar"));
264 assert!(!st.is_known_sub(""));
265 }
266
267 #[test]
270 fn default_produces_empty_table() {
271 let st = LocalSymbolTable::default();
272 assert!(st.is_empty());
273 assert!(!st.is_known_sub("anything"));
274 }
275}