1use crate::scope::Namespace;
16pub use crate::scope::SymbolKind;
17
18pub type ScopeId = usize;
21
22pub type SymbolId = usize;
24
25pub type FileId = usize;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ScopeKind {
32 Global,
34 Function,
36 Block,
38}
39
40#[derive(Debug, Clone, PartialEq)]
42pub struct Symbol {
43 pub name: String,
44 pub kind: SymbolKind,
45 pub file: FileId,
46 pub decl: Option<(u32, u32)>,
47 pub scope: ScopeId,
48 pub params: Vec<String>,
49 pub type_annotation: Option<String>,
50 pub container: Option<SymbolId>,
53 pub type_ref: Option<SymbolId>,
56 pub exported: bool,
57 pub module: Option<ScopeId>,
59}
60
61impl Symbol {
62 pub(crate) fn new(
63 name: &str,
64 kind: SymbolKind,
65 decl: Option<(u32, u32)>,
66 scope: ScopeId,
67 ) -> Self {
68 Symbol {
69 name: name.to_string(),
70 kind,
71 file: 0,
72 decl,
73 scope,
74 params: Vec::new(),
75 type_annotation: None,
76 container: None,
77 type_ref: None,
78 exported: false,
79 module: None,
80 }
81 }
82
83 pub(crate) fn with_params(mut self, params: Vec<String>) -> Self {
84 self.params = params;
85 self
86 }
87
88 pub(crate) fn with_type(mut self, type_annotation: Option<String>) -> Self {
89 self.type_annotation = type_annotation;
90 self
91 }
92
93 pub(crate) fn with_type_ref(mut self, type_ref: Option<SymbolId>) -> Self {
94 self.type_ref = type_ref;
95 self
96 }
97}
98
99#[derive(Debug, Clone)]
100struct ScopeData {
101 parent: Option<ScopeId>,
102 kind: ScopeKind,
103 file: FileId,
104 symbols: Vec<SymbolId>,
105}
106
107#[derive(Debug, Clone)]
109struct Occurrence {
110 file: FileId,
111 line: u32,
112 column: u32,
113 symbol: SymbolId,
114}
115
116#[derive(Debug, Clone)]
119pub struct SymbolTable {
120 symbols: Vec<Symbol>,
121 scopes: Vec<ScopeData>,
122 occurrences: Vec<Occurrence>,
123 files: Vec<String>,
124 file_roots: Vec<ScopeId>,
125}
126
127impl SymbolTable {
128 pub const GLOBAL: ScopeId = 0;
130 pub const MAIN: FileId = 0;
132
133 pub(crate) fn new() -> Self {
134 SymbolTable {
135 symbols: Vec::new(),
136 scopes: vec![ScopeData {
137 parent: None,
138 kind: ScopeKind::Global,
139 file: Self::MAIN,
140 symbols: Vec::new(),
141 }],
142 occurrences: Vec::new(),
143 files: vec![String::new()],
144 file_roots: vec![Self::GLOBAL],
145 }
146 }
147
148 pub(crate) fn add_file(&mut self, path: &str) -> (FileId, ScopeId) {
149 let file = self.files.len();
150 self.files.push(path.to_string());
151 let root = self.scopes.len();
152 self.scopes.push(ScopeData {
153 parent: None,
154 kind: ScopeKind::Global,
155 file,
156 symbols: Vec::new(),
157 });
158 self.file_roots.push(root);
159 (file, root)
160 }
161
162 pub(crate) fn file_by_path(&self, path: &str) -> Option<FileId> {
163 self.files.iter().position(|p| p == path)
164 }
165
166 pub fn file_path(&self, file: FileId) -> &str {
167 &self.files[file]
168 }
169
170 pub fn file_root(&self, file: FileId) -> ScopeId {
171 self.file_roots[file]
172 }
173
174 pub fn files(&self) -> impl Iterator<Item = (FileId, &str)> {
175 self.files
176 .iter()
177 .enumerate()
178 .map(|(id, path)| (id, path.as_str()))
179 }
180
181 pub fn scope_file(&self, scope: ScopeId) -> FileId {
182 self.scopes[scope].file
183 }
184
185 pub(crate) fn open_scope(&mut self, parent: ScopeId, kind: ScopeKind) -> ScopeId {
186 let id = self.scopes.len();
187 let file = self.scopes[parent].file;
188 self.scopes.push(ScopeData {
189 parent: Some(parent),
190 kind,
191 file,
192 symbols: Vec::new(),
193 });
194 id
195 }
196
197 pub(crate) fn set_module(&mut self, id: SymbolId, scope: ScopeId) {
198 self.symbols[id].module = Some(scope);
199 }
200
201 pub(crate) fn mark_exported(&mut self, id: SymbolId) {
202 self.symbols[id].exported = true;
203 }
204
205 pub(crate) fn declare(&mut self, symbol: Symbol) -> SymbolId {
207 let scope = symbol.scope;
208 let id = self.symbols.len();
209 self.symbols.push(symbol);
210 self.scopes[scope].symbols.push(id);
211 id
212 }
213
214 pub(crate) fn declared_locally(&self, scope: ScopeId, name: &str) -> bool {
217 self.scopes[scope]
218 .symbols
219 .iter()
220 .any(|&id| self.symbols[id].name == name)
221 }
222
223 pub(crate) fn declared_locally_in(
226 &self,
227 scope: ScopeId,
228 name: &str,
229 namespace: Namespace,
230 ) -> bool {
231 self.scopes[scope].symbols.iter().any(|&id| {
232 let symbol = &self.symbols[id];
233 symbol.name == name && symbol.kind.namespace() == namespace
234 })
235 }
236
237 pub(crate) fn record_use(&mut self, file: FileId, pos: Option<(u32, u32)>, symbol: SymbolId) {
240 if let Some((line, column)) = pos {
241 self.occurrences.push(Occurrence {
242 file,
243 line,
244 column,
245 symbol,
246 });
247 }
248 }
249
250 pub(crate) fn declare_member(
253 &mut self,
254 owner: SymbolId,
255 name: &str,
256 decl: Option<(u32, u32)>,
257 type_annotation: Option<String>,
258 ) {
259 let scope = self.symbols[owner].scope;
260 let file = self.symbols[owner].file;
261 let mut member = Symbol::new(name, SymbolKind::Var, decl, scope);
262 member.file = file;
263 member.type_annotation = type_annotation;
264 member.container = Some(owner);
265 self.symbols.push(member);
266 }
267
268 pub fn symbols(&self) -> &[Symbol] {
272 &self.symbols
273 }
274
275 pub fn scope_kind(&self, scope: ScopeId) -> ScopeKind {
277 self.scopes[scope].kind
278 }
279
280 pub fn symbols_in(&self, scope: ScopeId) -> impl Iterator<Item = &Symbol> {
282 self.scopes[scope]
283 .symbols
284 .iter()
285 .map(|&id| &self.symbols[id])
286 }
287
288 pub fn members_of(&self, owner: SymbolId) -> impl Iterator<Item = &Symbol> {
291 self.symbols
292 .iter()
293 .filter(move |s| s.container == Some(owner))
294 }
295
296 pub fn member_id(&self, owner: SymbolId, name: &str) -> Option<SymbolId> {
298 self.symbols
299 .iter()
300 .position(|s| s.container == Some(owner) && s.name == name)
301 }
302
303 pub fn exported_id(&self, scope: ScopeId, name: &str) -> Option<SymbolId> {
306 self.scopes[scope]
307 .symbols
308 .iter()
309 .rev()
310 .find(|&&id| self.symbols[id].exported && self.symbols[id].name == name)
311 .copied()
312 }
313
314 pub fn declaration_location(&self, id: SymbolId) -> Option<(FileId, u32, u32)> {
317 let symbol = &self.symbols[id];
318 symbol.decl.map(|(line, col)| (symbol.file, line, col))
319 }
320
321 pub fn symbol(&self, id: SymbolId) -> &Symbol {
323 &self.symbols[id]
324 }
325
326 pub fn resolve(&self, scope: ScopeId, name: &str) -> Option<&Symbol> {
328 self.resolve_id(scope, name).map(|id| &self.symbols[id])
329 }
330
331 pub fn resolve_id(&self, scope: ScopeId, name: &str) -> Option<SymbolId> {
334 let mut current = Some(scope);
335 while let Some(id) = current {
336 let data = &self.scopes[id];
337 if let Some(&sym) = data
338 .symbols
339 .iter()
340 .rev()
341 .find(|&&s| self.symbols[s].name == name)
342 {
343 return Some(sym);
344 }
345 current = data.parent;
346 }
347 None
348 }
349
350 pub fn references(&self, symbol: SymbolId) -> impl Iterator<Item = (FileId, u32, u32)> + '_ {
351 self.occurrences
352 .iter()
353 .filter(move |o| o.symbol == symbol)
354 .map(|o| (o.file, o.line, o.column))
355 }
356
357 pub fn occurrences_in_file(
358 &self,
359 file: FileId,
360 ) -> impl Iterator<Item = (u32, u32, SymbolId)> + '_ {
361 self.occurrences
362 .iter()
363 .filter(move |o| o.file == file)
364 .map(|o| (o.line, o.column, o.symbol))
365 }
366
367 pub fn symbol_at(&self, file: FileId, line: u32, column: u32) -> Option<SymbolId> {
370 if let Some(occ) = self
371 .occurrences
372 .iter()
373 .find(|o| o.file == file && o.line == line && o.column == column)
374 {
375 return Some(occ.symbol);
376 }
377 self.symbols
378 .iter()
379 .position(|s| s.file == file && s.decl == Some((line, column)))
380 }
381
382 pub fn find_declaration_at(&self, file: FileId, line: u32, column: u32) -> Option<&Symbol> {
384 self.symbols
385 .iter()
386 .find(|s| s.file == file && s.decl == Some((line, column)))
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use crate::{analyze_with_symbols, Diagnostic, SymbolKind, SymbolTable};
393 use pine_core::{DefaultPineOutput, FileResolver};
394 use pine_interpreter::Value;
395 use pine_parser::Parser;
396 use std::collections::HashMap;
397
398 const MAIN: crate::FileId = SymbolTable::MAIN;
399
400 fn table(source: &str) -> SymbolTable {
401 analyze(source, None).1
402 }
403
404 fn analyze(source: &str, loader: Option<&FileResolver>) -> (Vec<Diagnostic>, SymbolTable) {
407 let program = Parser::parse_source(source).unwrap();
408 let builtins: HashMap<String, Value<DefaultPineOutput>> = HashMap::new();
409 analyze_with_symbols(
410 &program,
411 &builtins,
412 loader.map(|l| l as &dyn crate::LibraryLoader),
413 )
414 }
415
416 fn libs(files: &[(&str, &str)]) -> FileResolver {
418 let mut resolver = FileResolver::new();
419 for (path, source) in files {
420 resolver.add(path, source);
421 }
422 resolver
423 }
424
425 #[test]
426 fn records_kinds_locations_and_params() {
427 let table =
430 table("//@version=5\nindicator(\"t\")\nsum(a, b) => a + b\ntotal = sum(1, 2)\n");
431
432 let sum = table.resolve(SymbolTable::GLOBAL, "sum").unwrap();
433 assert_eq!(sum.kind, SymbolKind::Function);
434 assert_eq!(sum.params, vec!["a", "b"]);
435 assert_eq!(sum.decl, Some((3, 1)));
436
437 let total = table.resolve(SymbolTable::GLOBAL, "total").unwrap();
438 assert_eq!(total.kind, SymbolKind::Var);
439 assert_eq!(total.decl, Some((4, 1)));
440
441 assert!(table.resolve(SymbolTable::GLOBAL, "a").is_none());
443 assert!(table.symbols().iter().any(|s| s.name == "a"));
444 }
445
446 #[test]
447 fn records_type_members_and_lookups() {
448 let table = table(
449 "//@version=5\nindicator(\"t\")\ntype Point\n float x\n float y = 0.0\np = Point.new()\n",
450 );
451
452 let point = table.resolve(SymbolTable::GLOBAL, "Point").unwrap();
453 assert_eq!(point.kind, SymbolKind::Type);
454
455 assert!(table.resolve(SymbolTable::GLOBAL, "x").is_none());
457 let point_id = table
458 .symbols()
459 .iter()
460 .position(|s| s.name == "Point")
461 .unwrap();
462 let members: Vec<_> = table
463 .members_of(point_id)
464 .map(|m| m.name.as_str())
465 .collect();
466 assert_eq!(members, vec!["x", "y"]);
467
468 let x = table.find_declaration_at(MAIN, 4, 11).unwrap();
470 assert_eq!(x.name, "x");
471 assert_eq!(x.type_annotation.as_deref(), Some("float"));
472 }
473
474 #[test]
475 fn records_uses_for_references_and_hover() {
476 let table = table("//@version=5\nindicator(\"t\")\nx = 1\ny = x + x\n");
478 let x_id = table.symbols().iter().position(|s| s.name == "x").unwrap();
479
480 let mut refs: Vec<_> = table.references(x_id).collect();
482 refs.sort();
483 assert_eq!(refs, vec![(MAIN, 4, 5), (MAIN, 4, 9)]);
484
485 assert_eq!(table.symbol_at(MAIN, 4, 5), Some(x_id));
487 assert_eq!(table.symbol_at(MAIN, 4, 9), Some(x_id));
488 assert_eq!(table.symbol_at(MAIN, 3, 1), Some(x_id));
490
491 assert!(table.symbol_at(MAIN, 2, 1).is_none());
493 }
494
495 #[test]
496 fn resolves_members_of_a_typed_variable() {
497 let table = table(
500 "//@version=5\nindicator(\"t\")\ntype Point\n float x\n float y\np = Point.new()\nv = p.x\n",
501 );
502
503 let point_id = table
504 .symbols()
505 .iter()
506 .position(|s| s.name == "Point")
507 .unwrap();
508 let x_id = table.member_id(point_id, "x").unwrap();
509
510 assert!(table.resolve(SymbolTable::GLOBAL, "x").is_none());
512 let refs: Vec<_> = table.references(x_id).collect();
513 assert_eq!(refs, vec![(MAIN, 7, 7)]);
514 assert_eq!(table.symbol_at(MAIN, 7, 7), Some(x_id));
515 }
516
517 #[test]
518 fn resolves_enum_cases() {
519 let table = table(
521 "//@version=5\nindicator(\"t\")\nenum Signal\n buy\n sell\ns = Signal.buy\n",
522 );
523
524 let signal_id = table
525 .symbols()
526 .iter()
527 .position(|s| s.name == "Signal")
528 .unwrap();
529 let buy_id = table.member_id(signal_id, "buy").unwrap();
530
531 assert_eq!(table.symbol_at(MAIN, 6, 12), Some(buy_id));
532 }
533
534 #[test]
535 fn imports_record_the_alias_but_not_its_members_without_a_loader() {
536 let table =
538 table("//@version=5\nindicator(\"t\")\nimport foo/bar/1 as lib\nx = lib.calc(1)\n");
539
540 let lib = table.resolve(SymbolTable::GLOBAL, "lib").unwrap();
543 assert_eq!(lib.kind, SymbolKind::Import);
544 let lib_id = table
545 .symbols()
546 .iter()
547 .position(|s| s.name == "lib")
548 .unwrap();
549 assert_eq!(
550 table.references(lib_id).collect::<Vec<_>>(),
551 vec![(MAIN, 4, 5)]
552 );
553
554 assert_eq!(table.symbol_at(MAIN, 4, 9), None);
557 }
558
559 #[test]
560 fn resolves_a_library_export_across_files() {
561 let loader = libs(&[("lib", "//@version=5\nexport add(a, b) => a + b\n")]);
562 let (_diags, table) = analyze(
563 "//@version=5\nindicator(\"t\")\nimport lib as l\nx = l.add(1, 2)\n",
564 Some(&loader),
565 );
566
567 let add_id = table
569 .symbols()
570 .iter()
571 .position(|s| s.name == "add" && s.exported)
572 .unwrap();
573 let (file, _, _) = table.declaration_location(add_id).unwrap();
575 assert_ne!(file, MAIN);
576 assert_eq!(table.file_path(file), "lib");
577 let refs: Vec<_> = table.references(add_id).collect();
579 assert_eq!(refs.len(), 1);
580 let (rf, rl, rc) = refs[0];
581 assert_eq!(rf, MAIN);
582 assert_eq!(table.symbol_at(rf, rl, rc), Some(add_id));
583 }
584
585 #[test]
586 fn resolves_a_field_of_a_variable_typed_from_a_library() {
587 let loader = libs(&[(
588 "geo",
589 "//@version=5\nexport type Point\n float x\n float y\n",
590 )]);
591 let (_diags, table) = analyze(
592 "//@version=5\nindicator(\"t\")\nimport geo as g\np = g.Point.new()\nv = p.x\n",
593 Some(&loader),
594 );
595
596 let point_id = table
597 .symbols()
598 .iter()
599 .position(|s| s.name == "Point" && s.exported)
600 .unwrap();
601 let x_id = table.member_id(point_id, "x").unwrap();
602 let refs: Vec<_> = table.references(x_id).collect();
604 assert_eq!(refs.len(), 1);
605 assert_eq!(refs[0].0, MAIN);
606 }
607
608 #[test]
609 fn resolves_an_enum_case_from_a_library() {
610 let loader = libs(&[(
611 "sig",
612 "//@version=5\nexport enum Direction\n up\n down\n",
613 )]);
614 let (_diags, table) = analyze(
615 "//@version=5\nindicator(\"t\")\nimport sig as s\nd = s.Direction.up\n",
616 Some(&loader),
617 );
618
619 let dir_id = table
620 .symbols()
621 .iter()
622 .position(|s| s.name == "Direction" && s.exported)
623 .unwrap();
624 let up_id = table.member_id(dir_id, "up").unwrap();
625 assert_eq!(table.references(up_id).count(), 1);
626 }
627
628 #[test]
629 fn a_library_use_of_its_own_import_resolves() {
630 let loader = libs(&[
634 ("b", "//@version=5\nexport helper() => 42\n"),
635 (
636 "a",
637 "//@version=5\nimport b as bb\nexport compute() => bb.helper()\n",
638 ),
639 ]);
640 let (_diags, table) = analyze(
641 "//@version=5\nindicator(\"t\")\nimport a as x\ny = x.compute()\n",
642 Some(&loader),
643 );
644
645 let compute = table
647 .symbols()
648 .iter()
649 .position(|s| s.name == "compute")
650 .unwrap();
651 assert_eq!(table.references(compute).count(), 1);
652
653 let helper = table
655 .symbols()
656 .iter()
657 .position(|s| s.name == "helper")
658 .unwrap();
659 let (decl_file, _, _) = table.declaration_location(helper).unwrap();
660 let refs: Vec<_> = table.references(helper).collect();
661 assert_eq!(refs.len(), 1);
662 let (use_file, _, _) = refs[0];
663 assert_ne!(use_file, MAIN);
664 assert_eq!(table.file_path(use_file), "a");
665 assert_eq!(table.file_path(decl_file), "b");
666 }
667
668 #[test]
669 fn import_cycles_terminate() {
670 let loader = libs(&[
673 ("a", "//@version=5\nimport b as bb\nexport fa() => 1\n"),
674 ("b", "//@version=5\nimport a as aa\nexport fb() => 2\n"),
675 ]);
676 let (_diags, table) = analyze(
677 "//@version=5\nindicator(\"t\")\nimport a as x\ny = x.fa()\n",
678 Some(&loader),
679 );
680 let fa_id = table.symbols().iter().position(|s| s.name == "fa").unwrap();
681 assert_eq!(table.references(fa_id).count(), 1);
682
683 let loader = libs(&[(
685 "selfimp",
686 "//@version=5\nimport selfimp as me\nexport g() => 1\n",
687 )]);
688 let (_diags, table) = analyze(
689 "//@version=5\nindicator(\"t\")\nimport selfimp as s\ny = s.g()\n",
690 Some(&loader),
691 );
692 let g_id = table.symbols().iter().position(|s| s.name == "g").unwrap();
693 assert_eq!(table.references(g_id).count(), 1);
694 }
695
696 #[test]
697 fn a_use_binds_to_the_innermost_shadowing_declaration() {
698 let table = table("//@version=5\nindicator(\"t\")\nx = 1\nf(x) => x + 1\ny = x + 1\n");
702
703 let symbols = table.symbols();
704 let global_x = symbols
706 .iter()
707 .position(|s| s.name == "x" && s.scope == SymbolTable::GLOBAL)
708 .unwrap();
709 let param_x = symbols
710 .iter()
711 .position(|s| s.name == "x" && s.scope != SymbolTable::GLOBAL)
712 .unwrap();
713 assert_ne!(global_x, param_x);
714
715 assert_eq!(table.symbol_at(MAIN, 4, 9), Some(param_x));
718 assert_eq!(table.symbol_at(MAIN, 5, 5), Some(global_x));
719
720 assert_eq!(
722 table.references(param_x).collect::<Vec<_>>(),
723 vec![(MAIN, 4, 9)]
724 );
725 assert_eq!(
726 table.references(global_x).collect::<Vec<_>>(),
727 vec![(MAIN, 5, 5)]
728 );
729 }
730}