1use std::collections::{HashMap, HashSet};
11
12use pine_ast::{Argument, ExportItem, Expr, FunctionParam, Literal, Loc, Program, Stmt};
13use pine_core::{LibraryLoader, PineOutput};
14use pine_interpreter::{BuiltinSignature, Value};
15use pine_parser::Parser;
16
17use crate::scope::{is_global_only, Namespace, SymbolKind};
18use crate::symbols::{FileId, ScopeId, ScopeKind, Symbol, SymbolId, SymbolTable};
19use pine_diagnostics::Diagnostic;
20
21pub struct Analyzer<'a, O: PineOutput> {
22 diagnostics: Vec<Diagnostic>,
23 loop_depth: u32,
25 builtins: &'a HashMap<String, Value<O>>,
27 declarations: u32,
29 functions: HashMap<String, (usize, usize)>,
31 user_types: HashSet<String>,
33 fn_stack: Vec<String>,
35 call_edges: Vec<CallEdge>,
37 symbols: SymbolTable,
39 scope_ids: Vec<ScopeId>,
40 loader: Option<&'a dyn LibraryLoader>,
42}
43
44struct FileState {
46 scope_ids: Vec<ScopeId>,
47 loop_depth: u32,
48 functions: HashMap<String, (usize, usize)>,
49 user_types: HashSet<String>,
50 declarations: u32,
51 fn_stack: Vec<String>,
52 call_edges: Vec<CallEdge>,
53}
54
55type CallEdge = (String, String, Option<(u32, u32)>);
57
58const SCRIPT_DECLARATIONS: &[&str] = &["study", "indicator", "strategy", "library"];
60
61const BUILTIN_TYPES: &[&str] = &[
63 "int", "float", "bool", "string", "color", "line", "linefill", "label", "box", "table",
64 "polyline", "array", "matrix", "map",
65];
66
67fn callee_name(callee: &Expr) -> String {
69 match callee {
70 Expr::Variable { name, .. } => name.clone(),
71 Expr::MemberAccess { object, member, .. } => match object.as_ref() {
72 Expr::Variable {
73 name: namespace, ..
74 } => format!("{namespace}.{member}"),
75 _ => member.clone(),
76 },
77 _ => String::new(),
78 }
79}
80
81fn reaches(start: &str, target: &str, adjacency: &HashMap<&str, Vec<&str>>) -> bool {
83 if start == target {
84 return true;
85 }
86 let mut stack = vec![start];
87 let mut seen = HashSet::new();
88 while let Some(node) = stack.pop() {
89 if !seen.insert(node) {
90 continue;
91 }
92 if let Some(callees) = adjacency.get(node) {
93 for &callee in callees {
94 if callee == target {
95 return true;
96 }
97 stack.push(callee);
98 }
99 }
100 }
101 false
102}
103
104fn type_names(annotation: &str) -> impl Iterator<Item = &str> {
107 annotation
108 .split(['<', '>', ',', '[', ']', ' '])
109 .filter(|name| !name.is_empty())
110}
111
112fn describe_literal(literal: &Literal) -> &'static str {
114 match literal {
115 Literal::Int(_) | Literal::Number(_) => "a number",
116 Literal::String(_) => "a string",
117 Literal::Bool(_) => "a bool",
118 Literal::HexColor(_) => "a color",
119 Literal::Na => "na",
120 }
121}
122
123impl<'a, O: PineOutput> Analyzer<'a, O> {
124 pub fn new(
125 builtins: &'a HashMap<String, Value<O>>,
126 loader: Option<&'a dyn LibraryLoader>,
127 ) -> Self {
128 Self {
129 diagnostics: Vec::new(),
130 loop_depth: 0,
131 builtins,
132 declarations: 0,
133 functions: HashMap::new(),
134 user_types: HashSet::new(),
135 fn_stack: Vec::new(),
136 call_edges: Vec::new(),
137 symbols: SymbolTable::new(),
138 scope_ids: vec![SymbolTable::GLOBAL],
139 loader,
140 }
141 }
142
143 fn current_scope(&self) -> ScopeId {
146 *self.scope_ids.last().expect("scope stack is never empty")
147 }
148
149 fn current_file(&self) -> FileId {
150 self.symbols.scope_file(self.current_scope())
151 }
152
153 fn current_lib(&self) -> Option<String> {
154 let file = self.current_file();
155 (file != SymbolTable::MAIN).then(|| self.symbols.file_path(file).to_string())
156 }
157
158 fn enter_scope(&mut self, kind: ScopeKind) {
160 let child = self.symbols.open_scope(self.current_scope(), kind);
161 self.scope_ids.push(child);
162 }
163
164 fn exit_scope(&mut self) {
166 self.scope_ids.pop();
167 }
168
169 fn record(&mut self, mut symbol: Symbol) -> SymbolId {
171 symbol.file = self.current_file();
172 self.symbols.declare(symbol)
173 }
174
175 fn resolve(&self, name: &str) -> Option<SymbolKind> {
177 self.symbols
178 .resolve(self.current_scope(), name)
179 .map(|symbol| symbol.kind)
180 }
181
182 fn record_use(&mut self, name: &str, loc: Loc) {
184 let scope = self.current_scope();
185 if let Some(id) = self.symbols.resolve_id(scope, name) {
186 let file = self.current_file();
187 self.symbols.record_use(file, loc.position(), id);
188 }
189 }
190
191 fn infer_var_type(
193 &self,
194 type_annotation: Option<&String>,
195 initializer: Option<&Expr>,
196 ) -> Option<SymbolId> {
197 if let Some(annotation) = type_annotation {
198 let base = annotation.trim_end_matches("[]");
199 if let Some(id) = self.symbols.resolve_id(self.current_scope(), base) {
200 if matches!(
201 self.symbols.symbol(id).kind,
202 SymbolKind::Type | SymbolKind::Enum
203 ) {
204 return Some(id);
205 }
206 }
207 }
208 if let Some(Expr::Call { callee, .. }) = initializer {
210 if let Expr::MemberAccess { object, member, .. } = callee.as_ref() {
211 if member == "new" {
212 if let Some(id) = self.expr_type(object) {
213 if self.symbols.symbol(id).kind == SymbolKind::Type {
214 return Some(id);
215 }
216 }
217 }
218 }
219 }
220 None
221 }
222
223 fn owner_type(&self, id: SymbolId) -> Option<SymbolId> {
225 let symbol = self.symbols.symbol(id);
226 match symbol.kind {
227 SymbolKind::Type | SymbolKind::Enum => Some(id),
228 SymbolKind::Var => symbol.type_ref,
229 _ => None,
230 }
231 }
232
233 fn expr_type(&self, expr: &Expr) -> Option<SymbolId> {
236 let id = match expr {
237 Expr::Variable { name, .. } => self.symbols.resolve_id(self.current_scope(), name)?,
238 Expr::MemberAccess { object, member, .. } => self.resolve_member(object, member)?,
239 _ => return None,
240 };
241 self.owner_type(id)
242 }
243
244 fn resolve_member(&self, object: &Expr, member: &str) -> Option<SymbolId> {
247 if let Some(module) = self.alias_module(object) {
248 return self.symbols.exported_id(module, member);
249 }
250 let owner = self.expr_type(object)?;
251 self.symbols.member_id(owner, member)
252 }
253
254 fn alias_module(&self, object: &Expr) -> Option<ScopeId> {
256 let Expr::Variable { name, .. } = object else {
257 return None;
258 };
259 let id = self.symbols.resolve_id(self.current_scope(), name)?;
260 let symbol = self.symbols.symbol(id);
261 (symbol.kind == SymbolKind::Import)
262 .then_some(symbol.module)
263 .flatten()
264 }
265
266 fn is_builtin(&self, name: &str) -> bool {
267 self.builtins.contains_key(name)
268 }
269
270 fn builtin_signature(&self, callee: &Expr) -> Option<BuiltinSignature> {
273 let value = match callee {
274 Expr::Variable { name, .. } => {
275 if self.resolve(name).is_some() {
276 return None;
277 }
278 self.builtins.get(name)?.clone()
279 }
280 Expr::MemberAccess { object, member, .. } => {
281 let Expr::Variable {
282 name: namespace, ..
283 } = object.as_ref()
284 else {
285 return None;
286 };
287 if self.resolve(namespace).is_some() {
288 return None;
289 }
290 match self.builtins.get(namespace)? {
291 Value::Object { fields, .. } => fields.borrow().get(member)?.clone(),
292 _ => return None,
293 }
294 }
295 _ => return None,
296 };
297
298 match value {
299 Value::BuiltinFunction(builtin) if !builtin.signature.params.is_empty() => {
300 Some(builtin.signature)
301 }
302 _ => None,
303 }
304 }
305
306 fn check_builtin_args(
309 &mut self,
310 name: &str,
311 signature: &BuiltinSignature,
312 args: &[Argument],
313 pos: Option<(u32, u32)>,
314 ) {
315 let positional = args
316 .iter()
317 .filter(|arg| matches!(arg, Argument::Positional(_)))
318 .count();
319
320 if let Some(max) = signature.max_positional() {
321 if positional > max {
322 self.emit(
323 "too-many-arguments",
324 pos,
325 format!("`{name}` takes at most {max} arguments, found {positional}"),
326 );
327 }
328 }
329
330 let mut index = 0;
331 for arg in args {
332 let (param, value) = match arg {
333 Argument::Positional(value) => {
334 let param = signature.positional(index);
335 index += 1;
336 (param, value)
337 }
338 Argument::Named { name: label, value } => match signature.named(label) {
339 Some(param) => (Some(param), value),
340 None => {
341 self.emit(
342 "unknown-argument",
343 pos,
344 format!("`{name}` has no argument named `{label}`"),
345 );
346 continue;
347 }
348 },
349 };
350
351 let (Some(param), Expr::Literal(literal)) = (param, value) else {
354 continue;
355 };
356 if !param.ty.accepts(literal) {
357 let found = describe_literal(literal);
358 let expected = param.ty.describe();
359 let label = param.name.clone();
360 self.emit(
361 "argument-type",
362 pos,
363 format!("`{name}` expects {expected} for `{label}`, found {found}"),
364 );
365 }
366 }
367
368 let required = signature
371 .params
372 .iter()
373 .filter(|param| param.required)
374 .count();
375 if args.len() < required {
376 self.emit(
377 "too-few-arguments",
378 pos,
379 format!(
380 "`{name}` requires at least {required} arguments, found {}",
381 args.len()
382 ),
383 );
384 }
385 }
386
387 fn run_file(&mut self, program: &Program) {
389 for stmt in &program.statements {
391 match stmt {
392 Stmt::TypeDecl { name, .. } | Stmt::EnumDecl { name, .. } => {
393 self.user_types.insert(name.clone());
394 }
395 _ => {}
396 }
397 }
398 for stmt in &program.statements {
399 self.check_stmt(stmt);
400 }
401 self.detect_recursion();
402 }
403
404 fn enter_file(&mut self, root: ScopeId) -> FileState {
406 FileState {
407 scope_ids: std::mem::replace(&mut self.scope_ids, vec![root]),
408 loop_depth: std::mem::take(&mut self.loop_depth),
409 functions: std::mem::take(&mut self.functions),
410 user_types: std::mem::take(&mut self.user_types),
411 declarations: std::mem::take(&mut self.declarations),
412 fn_stack: std::mem::take(&mut self.fn_stack),
413 call_edges: std::mem::take(&mut self.call_edges),
414 }
415 }
416
417 fn exit_file(&mut self, saved: FileState) {
418 self.scope_ids = saved.scope_ids;
419 self.loop_depth = saved.loop_depth;
420 self.functions = saved.functions;
421 self.user_types = saved.user_types;
422 self.declarations = saved.declarations;
423 self.fn_stack = saved.fn_stack;
424 self.call_edges = saved.call_edges;
425 }
426
427 fn resolve_import(&mut self, path: &str) -> Option<(FileId, ScopeId)> {
430 if let Some(file) = self.symbols.file_by_path(path) {
431 return Some((file, self.symbols.file_root(file)));
432 }
433 let loader = self.loader?;
434 let source = match loader.load_library(path) {
435 Ok(source) => source,
436 Err(err) => {
437 self.emit(
438 "import-error",
439 None,
440 format!("cannot load library `{path}`: {err}"),
441 );
442 return None;
443 }
444 };
445 let program = match Parser::parse_source(&source) {
446 Ok(program) => program,
447 Err(err) => {
448 self.emit(
449 "import-parse-error",
450 None,
451 format!("cannot parse library `{path}`: {err}"),
452 );
453 return None;
454 }
455 };
456 let (file, root) = self.symbols.add_file(path);
457 let saved = self.enter_file(root);
458 self.run_file(&program);
459 self.exit_file(saved);
460 Some((file, root))
461 }
462
463 pub fn analyze(mut self, program: &Program) -> Vec<Diagnostic> {
465 self.run_file(program);
466 self.diagnostics
467 }
468
469 pub fn into_analysis(mut self, program: &Program) -> (Vec<Diagnostic>, SymbolTable) {
472 self.run_file(program);
473 (self.diagnostics, self.symbols)
474 }
475
476 fn detect_recursion(&mut self) {
478 let cycles: Vec<CallEdge> = {
479 let mut adjacency: HashMap<&str, Vec<&str>> = HashMap::new();
480 for (caller, callee, _) in &self.call_edges {
481 adjacency.entry(caller).or_default().push(callee);
482 }
483 self.call_edges
484 .iter()
485 .filter(|(caller, callee, _)| reaches(callee, caller, &adjacency))
486 .cloned()
487 .collect()
488 };
489 for (caller, callee, pos) in cycles {
490 let message = if caller == callee {
491 format!("`{caller}` calls itself; Pine does not allow recursion")
492 } else {
493 format!("`{caller}` and `{callee}` call each other; Pine does not allow recursion")
494 };
495 self.emit("recursion", pos, message);
496 }
497 }
498
499 fn emit(&mut self, rule: &'static str, pos: Option<(u32, u32)>, message: impl Into<String>) {
500 self.diagnostics
501 .push(Diagnostic::error(rule, pos, message).in_file(self.current_lib()));
502 }
503
504 fn warn(&mut self, rule: &'static str, pos: Option<(u32, u32)>, message: impl Into<String>) {
505 self.diagnostics
506 .push(Diagnostic::warning(rule, pos, message).in_file(self.current_lib()));
507 }
508
509 fn check_shadow(&mut self, name: &str) {
511 if self.is_builtin(name) {
512 self.warn(
513 "shadows-builtin",
514 None,
515 format!("declaration of `{name}` shadows a built-in"),
516 );
517 }
518 }
519
520 fn check_type_annotation(&mut self, annotation: Option<&String>) {
523 let Some(annotation) = annotation else {
524 return;
525 };
526 for name in type_names(annotation) {
527 if !BUILTIN_TYPES.contains(&name) && !self.user_types.contains(name) {
528 self.emit("unknown-type", None, format!("unknown type `{name}`"));
529 return;
530 }
531 }
532 }
533
534 fn check_call_arity(
536 &mut self,
537 name: &str,
538 supplied: usize,
539 required: usize,
540 total: usize,
541 pos: Option<(u32, u32)>,
542 ) {
543 if supplied < required {
544 self.emit(
545 "too-few-arguments",
546 pos,
547 format!("`{name}` requires at least {required} arguments, found {supplied}"),
548 );
549 } else if supplied > total {
550 self.emit(
551 "too-many-arguments",
552 pos,
553 format!("`{name}` takes at most {total} arguments, found {supplied}"),
554 );
555 }
556 }
557
558 fn analyze_function(
561 &mut self,
562 name: &str,
563 loc: Loc,
564 params: &[FunctionParam],
565 body: &[Stmt],
566 ) -> SymbolId {
567 let scope = self.current_scope();
568 if self
569 .symbols
570 .declared_locally_in(scope, name, Namespace::Value)
571 {
572 self.emit(
573 "duplicate-declaration",
574 None,
575 format!("`{name}` is already declared in this scope"),
576 );
577 }
578 let id = self.record(
579 Symbol::new(name, SymbolKind::Function, loc.position(), scope)
580 .with_params(params.iter().map(|p| p.name.clone()).collect()),
581 );
582 let required = params.iter().filter(|p| p.default_value.is_none()).count();
584 self.functions
585 .insert(name.to_string(), (required, params.len()));
586 for param in params {
587 self.check_type_annotation(param.type_annotation.as_ref());
588 }
589 self.fn_stack.push(name.to_string());
590 self.function_body(
591 params.iter().map(|p| {
592 (
593 p.name.as_str(),
594 p.default_value.as_ref(),
595 p.loc,
596 p.type_annotation.as_ref(),
597 )
598 }),
599 body,
600 );
601 self.fn_stack.pop();
602 id
603 }
604
605 fn declare(&mut self, name: &str, kind: SymbolKind, loc: Loc) -> SymbolId {
607 let scope = self.current_scope();
608 if self
609 .symbols
610 .declared_locally_in(scope, name, kind.namespace())
611 {
612 self.emit(
613 "duplicate-declaration",
614 None,
615 format!("`{name}` is already declared in this scope"),
616 );
617 }
618 self.record(Symbol::new(name, kind, loc.position(), scope))
619 }
620
621 fn block(&mut self, body: &[Stmt]) {
623 self.enter_scope(ScopeKind::Block);
624 for stmt in body {
625 self.check_stmt(stmt);
626 }
627 self.exit_scope();
628 }
629
630 fn loop_body(&mut self, body: &[Stmt]) {
632 self.loop_depth += 1;
633 for stmt in body {
634 self.check_stmt(stmt);
635 }
636 self.loop_depth -= 1;
637 }
638
639 fn function_body<'p>(
641 &mut self,
642 params: impl Iterator<Item = (&'p str, Option<&'p Expr>, Loc, Option<&'p String>)>,
643 body: &[Stmt],
644 ) {
645 self.enter_scope(ScopeKind::Function);
646 let saved_loop_depth = self.loop_depth;
647 self.loop_depth = 0;
648 let scope = self.current_scope();
649 for (name, default, loc, type_annotation) in params {
650 if let Some(default) = default {
651 self.check_expr(default);
652 }
653 self.check_shadow(name);
654 if self.symbols.declared_locally(scope, name) {
655 self.emit(
656 "duplicate-parameter",
657 None,
658 format!("parameter `{name}` is declared more than once"),
659 );
660 }
661 self.record(
662 Symbol::new(name, SymbolKind::Var, loc.position(), scope)
663 .with_type(type_annotation.cloned()),
664 );
665 }
666 for stmt in body {
667 self.check_stmt(stmt);
668 }
669 self.loop_depth = saved_loop_depth;
670 self.exit_scope();
671 }
672
673 fn check_stmt(&mut self, stmt: &Stmt) {
674 match stmt {
675 Stmt::VarDecl {
676 name,
677 initializer,
678 type_annotation,
679 loc,
680 ..
681 } => {
682 self.check_type_annotation(type_annotation.as_ref());
683 self.check_shadow(name);
684 if let Some(Expr::Function { params, body }) = initializer {
685 self.analyze_function(name, *loc, params, body);
687 } else {
688 if let Some(init) = initializer {
691 self.check_expr(init);
692 }
693 let scope = self.current_scope();
694 if self
695 .symbols
696 .declared_locally_in(scope, name, Namespace::Value)
697 {
698 self.emit(
699 "duplicate-declaration",
700 None,
701 format!(
702 "`{name}` is already declared in this scope (use `:=` to reassign)"
703 ),
704 );
705 }
706 let type_ref =
707 self.infer_var_type(type_annotation.as_ref(), initializer.as_ref());
708 self.record(
709 Symbol::new(name, SymbolKind::Var, loc.position(), scope)
710 .with_type(type_annotation.clone())
711 .with_type_ref(type_ref),
712 );
713 }
714 }
715 Stmt::Assignment { target, value } => {
716 self.check_expr(value);
717 self.check_assign_target(target);
718 }
719 Stmt::TupleAssignment {
720 names, value, loc, ..
721 } => {
722 self.check_expr(value);
723 let scope = self.current_scope();
724 for name in names {
725 if name == "_" {
728 continue;
729 }
730 self.check_shadow(name);
731 if self
732 .symbols
733 .declared_locally_in(scope, name, Namespace::Value)
734 {
735 self.emit(
736 "duplicate-declaration",
737 None,
738 format!("`{name}` is already declared in this scope"),
739 );
740 }
741 self.record(Symbol::new(name, SymbolKind::Var, loc.position(), scope));
742 }
743 }
744 Stmt::Expression(expr) => self.check_expr(expr),
745 Stmt::If {
746 condition,
747 then_branch,
748 else_if_branches,
749 else_branch,
750 } => {
751 self.check_expr(condition);
752 self.block(then_branch);
753 for (cond, body) in else_if_branches {
754 self.check_expr(cond);
755 self.block(body);
756 }
757 if let Some(body) = else_branch {
758 self.block(body);
759 }
760 }
761 Stmt::For {
762 var_name,
763 from,
764 to,
765 body,
766 loc,
767 } => {
768 self.check_expr(from);
769 self.check_expr(to);
770 self.enter_scope(ScopeKind::Block);
771 self.check_shadow(var_name);
772 let scope = self.current_scope();
773 self.record(Symbol::new(
774 var_name,
775 SymbolKind::Var,
776 loc.position(),
777 scope,
778 ));
779 self.loop_body(body);
780 self.exit_scope();
781 }
782 Stmt::ForIn {
783 index_var,
784 item_var,
785 collection,
786 body,
787 loc,
788 } => {
789 self.check_expr(collection);
790 self.enter_scope(ScopeKind::Block);
791 let scope = self.current_scope();
792 if let Some(idx) = index_var {
793 self.check_shadow(idx);
794 self.record(Symbol::new(idx, SymbolKind::Var, loc.position(), scope));
795 }
796 self.check_shadow(item_var);
797 self.record(Symbol::new(
798 item_var,
799 SymbolKind::Var,
800 loc.position(),
801 scope,
802 ));
803 self.loop_body(body);
804 self.exit_scope();
805 }
806 Stmt::While { condition, body } => {
807 self.check_expr(condition);
808 self.enter_scope(ScopeKind::Block);
809 self.loop_body(body);
810 self.exit_scope();
811 }
812 Stmt::Break => self.check_loop_keyword("break"),
813 Stmt::Continue => self.check_loop_keyword("continue"),
814 Stmt::FunctionDecl {
815 name,
816 params,
817 body,
818 export,
819 loc,
820 } => {
821 self.check_shadow(name);
822 let id = self.analyze_function(name, *loc, params, body);
823 if *export {
824 self.symbols.mark_exported(id);
825 }
826 }
827 Stmt::MethodDecl {
828 name,
829 params,
830 body,
831 export,
832 loc,
833 } => {
834 let scope = self.current_scope();
836 let id = self.record(
837 Symbol::new(name, SymbolKind::Function, loc.position(), scope)
838 .with_params(params.iter().map(|p| p.name.clone()).collect()),
839 );
840 if *export {
841 self.symbols.mark_exported(id);
842 }
843 for param in params {
844 self.check_type_annotation(param.type_annotation.as_ref());
845 }
846 self.function_body(
847 params.iter().map(|p| {
848 (
849 p.name.as_str(),
850 p.default_value.as_ref(),
851 p.loc,
852 p.type_annotation.as_ref(),
853 )
854 }),
855 body,
856 );
857 }
858 Stmt::TypeDecl {
859 name,
860 fields,
861 export,
862 loc,
863 } => {
864 let owner = self.declare(name, SymbolKind::Type, *loc);
865 if *export {
866 self.symbols.mark_exported(owner);
867 }
868 for field in fields {
869 self.check_type_annotation(Some(&field.type_annotation));
870 self.symbols.declare_member(
871 owner,
872 &field.name,
873 field.loc.position(),
874 Some(field.type_annotation.clone()),
875 );
876 }
877 }
878 Stmt::EnumDecl {
879 name,
880 fields,
881 export,
882 loc,
883 } => {
884 let owner = self.declare(name, SymbolKind::Enum, *loc);
885 if *export {
886 self.symbols.mark_exported(owner);
887 }
888 for field in fields {
889 self.symbols
890 .declare_member(owner, &field.name, field.loc.position(), None);
891 }
892 }
893 Stmt::Import { path, alias, loc } => {
894 let id = self.declare(alias, SymbolKind::Import, *loc);
895 if let Some((_, root)) = self.resolve_import(path) {
896 self.symbols.set_module(id, root);
897 }
898 }
899 Stmt::Export { item } => {
901 let name = match item {
902 ExportItem::Function(name) | ExportItem::Type(name) => name,
903 };
904 if let Some(id) = self.symbols.resolve_id(self.current_scope(), name) {
905 self.symbols.mark_exported(id);
906 }
907 }
908 }
909 }
910
911 fn check_loop_keyword(&mut self, keyword: &str) {
912 if self.loop_depth == 0 {
913 self.emit(
914 "break-outside-loop",
915 None,
916 format!("`{keyword}` is only valid inside a loop"),
917 );
918 }
919 }
920
921 fn check_assign_target(&mut self, target: &Expr) {
923 match target {
924 Expr::Variable { name, loc } => match self.resolve(name) {
925 Some(SymbolKind::Var) => self.record_use(name, *loc),
926 Some(other) => self.emit(
927 "invalid-assignment",
928 None,
929 format!("cannot assign to `{name}`, it is a {}", other.noun()),
930 ),
931 None if self.is_builtin(name) => self.emit(
932 "reassign-builtin",
933 None,
934 format!("cannot reassign built-in `{name}`"),
935 ),
936 None => self.emit(
937 "invalid-assignment",
938 None,
939 format!(
940 "cannot assign to undeclared variable `{name}` (declare it with `=` first)"
941 ),
942 ),
943 },
944 other => self.check_expr(other),
946 }
947 }
948
949 fn check_expr(&mut self, expr: &Expr) {
950 match expr {
951 Expr::Variable { name, loc } => {
952 if self.resolve(name).is_none() && !self.is_builtin(name) {
953 self.emit(
954 "undeclared-variable",
955 None,
956 format!("undeclared variable `{name}`"),
957 );
958 } else {
959 self.record_use(name, *loc);
960 }
961 }
962 Expr::Call {
963 callee, args, loc, ..
964 } => {
965 if let Expr::Variable {
966 name: fname,
967 loc: fname_loc,
968 } = callee.as_ref()
969 {
970 let pos = loc.position();
971 self.record_use(fname, *fname_loc);
972 if is_global_only(fname) && self.current_scope() != SymbolTable::GLOBAL {
973 self.emit(
974 "global-scope-required",
975 pos,
976 format!("`{fname}` may only be called in the global scope"),
977 );
978 }
979 if SCRIPT_DECLARATIONS.contains(&fname.as_str()) {
980 self.declarations += 1;
981 if self.declarations > 1 {
982 self.emit(
983 "duplicate-declaration",
984 pos,
985 "a script may only have one indicator/strategy/library declaration",
986 );
987 }
988 }
989 match self.resolve(fname) {
990 Some(SymbolKind::Function) => {
991 if let Some(caller) = self.fn_stack.last() {
994 self.call_edges.push((caller.clone(), fname.clone(), pos));
995 }
996 if let Some(&(required, total)) = self.functions.get(fname) {
997 self.check_call_arity(fname, args.len(), required, total, pos);
998 }
999 }
1000 Some(kind @ (SymbolKind::Var | SymbolKind::Type | SymbolKind::Enum)) => {
1002 self.emit(
1003 "not-callable",
1004 pos,
1005 format!("`{fname}` is a {}, not a function", kind.noun()),
1006 );
1007 }
1008 Some(SymbolKind::Import) => {}
1010 None => {
1011 if !self.is_builtin(fname) {
1012 self.emit(
1013 "unknown-function",
1014 pos,
1015 format!("unknown function `{fname}`"),
1016 );
1017 }
1018 }
1019 }
1020 } else {
1021 self.check_expr(callee);
1022 }
1023 if let Some(signature) = self.builtin_signature(callee) {
1024 let name = callee_name(callee);
1025 self.check_builtin_args(&name, &signature, args, loc.position());
1026 }
1027 for arg in args {
1028 match arg {
1029 Argument::Positional(e) => self.check_expr(e),
1030 Argument::Named { value, .. } => self.check_expr(value),
1031 }
1032 }
1033 }
1034 Expr::Binary { left, right, .. } => {
1035 self.check_expr(left);
1036 self.check_expr(right);
1037 }
1038 Expr::Unary { expr, .. } => self.check_expr(expr),
1039 Expr::Index { expr, index } => {
1040 self.check_expr(expr);
1041 self.check_expr(index);
1042 }
1043 Expr::MemberAccess {
1045 object,
1046 member,
1047 member_loc,
1048 } => {
1049 self.check_expr(object);
1050 if let Some(id) = self.resolve_member(object, member) {
1051 let file = self.current_file();
1052 self.symbols.record_use(file, member_loc.position(), id);
1053 }
1054 }
1055 Expr::Ternary {
1056 condition,
1057 then_expr,
1058 else_expr,
1059 } => {
1060 self.check_expr(condition);
1061 self.check_expr(then_expr);
1062 self.check_expr(else_expr);
1063 }
1064 Expr::IfExpr {
1065 condition,
1066 then_expr,
1067 else_if_branches,
1068 else_expr,
1069 } => {
1070 self.check_expr(condition);
1071 self.check_expr(then_expr);
1072 for (cond, e) in else_if_branches {
1073 self.check_expr(cond);
1074 self.check_expr(e);
1075 }
1076 if let Some(e) = else_expr {
1077 self.check_expr(e);
1078 }
1079 }
1080 Expr::Switch { value, cases } => {
1081 self.check_expr(value);
1082 for (pattern, result) in cases {
1083 self.check_expr(pattern);
1084 self.check_expr(result);
1085 }
1086 }
1087 Expr::Array(elements) => {
1088 for e in elements {
1089 self.check_expr(e);
1090 }
1091 }
1092 Expr::Function { params, body } => {
1094 self.function_body(
1095 params.iter().map(|p| {
1096 (
1097 p.name.as_str(),
1098 p.default_value.as_ref(),
1099 p.loc,
1100 p.type_annotation.as_ref(),
1101 )
1102 }),
1103 body,
1104 );
1105 }
1106 Expr::Literal(_) => {}
1107 }
1108 }
1109}