1use runmat_hir::{
2 CallKind, CallSyntax, EnvironmentEffect as HirEnvironmentEffect, FunctionHandleTarget,
3 HirCallableRef, HirDiagnostic, HirDiagnosticSeverity, HirError, LoweringContext,
4 LoweringResult, Span,
5};
6use runmat_mir::{analysis::AnalysisStore, MirAssembly, MirStmtKind};
7use runmat_parser::{CompatMode, ParserOptions};
8use runmat_vm::CompileError;
9use serde::{Deserialize, Serialize};
10
11use crate::lints::shape::lint_shapes_from_mir;
12
13pub const DIAGNOSTIC_UNRESOLVED_FUNCTION: &str = "RM-RES0001";
14pub const DIAGNOSTIC_RUNTIME_DEPENDENT_RESOLUTION: &str = "RM-RES0002";
15pub const DIAGNOSTIC_RUNTIME_METHOD_DISPATCH: &str = "RM-RES0003";
16pub const DIAGNOSTIC_SOURCE_CATALOG: &str = "RM-CAT0001";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum ResolutionState {
21 Resolved,
22 Unresolved,
23 RuntimeDependent,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct ResolutionEvidence {
28 pub name: String,
29 pub span: Span,
30 pub state: ResolutionState,
31 pub reason: String,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub definition: Option<runmat_config::project::ProjectSymbolDefinition>,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum AnalysisCompleteness {
39 Complete,
40 Partial,
41 RuntimeDependent,
42 Unavailable,
43 NotApplicable,
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47pub struct AnalysisDomains {
48 pub syntax: AnalysisCompleteness,
49 pub name_resolution: AnalysisCompleteness,
50 pub definite_assignment: AnalysisCompleteness,
51 pub effects: AnalysisCompleteness,
52 pub types: AnalysisCompleteness,
53 pub shapes: AnalysisCompleteness,
54 pub async_safety: AnalysisCompleteness,
55}
56
57impl AnalysisDomains {
58 fn unavailable_after_syntax() -> Self {
59 Self {
60 syntax: AnalysisCompleteness::Complete,
61 name_resolution: AnalysisCompleteness::Unavailable,
62 definite_assignment: AnalysisCompleteness::Unavailable,
63 effects: AnalysisCompleteness::Unavailable,
64 types: AnalysisCompleteness::Unavailable,
65 shapes: AnalysisCompleteness::Unavailable,
66 async_safety: AnalysisCompleteness::Unavailable,
67 }
68 }
69
70 fn unavailable_after_hir() -> Self {
71 Self {
72 syntax: AnalysisCompleteness::Complete,
73 name_resolution: AnalysisCompleteness::Partial,
74 definite_assignment: AnalysisCompleteness::Unavailable,
75 effects: AnalysisCompleteness::Unavailable,
76 types: AnalysisCompleteness::Unavailable,
77 shapes: AnalysisCompleteness::Unavailable,
78 async_safety: AnalysisCompleteness::Unavailable,
79 }
80 }
81
82 fn completed_frontend() -> Self {
83 Self {
84 syntax: AnalysisCompleteness::Complete,
85 name_resolution: AnalysisCompleteness::Complete,
86 definite_assignment: AnalysisCompleteness::Complete,
87 effects: AnalysisCompleteness::Complete,
88 types: AnalysisCompleteness::Partial,
91 shapes: AnalysisCompleteness::Partial,
92 async_safety: AnalysisCompleteness::Complete,
93 }
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct ParseFailure {
99 pub message: String,
100 pub position: usize,
101 pub found_token: Option<String>,
102 pub expected: Option<String>,
103}
104
105#[derive(Debug, Clone)]
106pub struct FrontendAnalysis {
107 pub lowering: Option<LoweringResult>,
108 pub mir: Option<MirAssembly>,
109 pub facts: Option<AnalysisStore>,
110 pub diagnostics: Vec<HirDiagnostic>,
111 pub parse_failure: Option<ParseFailure>,
112 pub lowering_failure: Option<HirError>,
113 pub compile_failure: Option<CompileError>,
114 pub bytecode: Option<runmat_vm::Bytecode>,
115 pub resolution: Vec<ResolutionEvidence>,
116 pub domains: AnalysisDomains,
117}
118
119impl FrontendAnalysis {
120 pub fn has_errors(&self) -> bool {
121 self.diagnostics
122 .iter()
123 .any(|diagnostic| diagnostic.severity == HirDiagnosticSeverity::Error)
124 }
125
126 pub fn warning_count(&self) -> usize {
127 self.diagnostics
128 .iter()
129 .filter(|diagnostic| diagnostic.severity == HirDiagnosticSeverity::Warning)
130 .count()
131 }
132}
133
134pub fn analyze_source(
138 source: &str,
139 compat: CompatMode,
140 lowering_context: &LoweringContext<'_>,
141) -> FrontendAnalysis {
142 analyze_source_with_catalog(source, compat, lowering_context, None)
143}
144
145pub fn analyze_source_with_catalog(
146 source: &str,
147 compat: CompatMode,
148 lowering_context: &LoweringContext<'_>,
149 source_catalog: Option<&runmat_config::project::DiscoveredSourceSymbols>,
150) -> FrontendAnalysis {
151 let ast = match runmat_parser::parse_with_options(source, ParserOptions::new(compat)) {
152 Ok(ast) => ast,
153 Err(error) => {
154 let failure = ParseFailure {
155 message: error.message,
156 position: error.position,
157 found_token: error.found_token,
158 expected: error.expected,
159 };
160 let message = parse_failure_message(&failure);
161 let span = source_position_span(source, failure.position);
162 return FrontendAnalysis {
163 lowering: None,
164 mir: None,
165 facts: None,
166 diagnostics: vec![HirDiagnostic::new(
167 "RunMat:ParseError",
168 HirDiagnosticSeverity::Error,
169 message,
170 span,
171 )
172 .with_primary_label("syntax error occurs here")
173 .with_category("syntax")],
174 parse_failure: Some(failure),
175 lowering_failure: None,
176 compile_failure: None,
177 bytecode: None,
178 resolution: Vec::new(),
179 domains: AnalysisDomains::unavailable_after_syntax(),
180 };
181 }
182 };
183
184 analyze_program_with_catalog(&ast, lowering_context, source_catalog)
185}
186
187pub fn analyze_program_with_catalog(
193 ast: &runmat_parser::Program,
194 lowering_context: &LoweringContext<'_>,
195 source_catalog: Option<&runmat_config::project::DiscoveredSourceSymbols>,
196) -> FrontendAnalysis {
197 let lowering = match runmat_hir::lower(ast, lowering_context) {
198 Ok(lowering) => lowering,
199 Err(error) => {
200 let span = error.span.unwrap_or(Span { start: 0, end: 0 });
201 let code = error
202 .identifier
203 .clone()
204 .unwrap_or_else(|| "RunMat:LoweringError".to_string());
205 return FrontendAnalysis {
206 lowering: None,
207 mir: None,
208 facts: None,
209 diagnostics: vec![HirDiagnostic::new(
210 code,
211 HirDiagnosticSeverity::Error,
212 error.message.clone(),
213 span,
214 )
215 .with_primary_label("semantic error occurs here")
216 .with_category("semantic")],
217 parse_failure: None,
218 lowering_failure: Some(error),
219 compile_failure: None,
220 bytecode: None,
221 resolution: Vec::new(),
222 domains: AnalysisDomains::unavailable_after_hir(),
223 };
224 }
225 };
226
227 let mir = match runmat_mir::lowering::lower_assembly(&lowering.assembly) {
228 Ok(mir) => mir,
229 Err(error) => {
230 let span = error.span.unwrap_or(Span { start: 0, end: 0 });
231 let code = error
232 .identifier
233 .clone()
234 .unwrap_or_else(|| "RunMat:MirLoweringError".to_string());
235 return FrontendAnalysis {
236 lowering: Some(lowering),
237 mir: None,
238 facts: None,
239 diagnostics: vec![HirDiagnostic::new(
240 code,
241 HirDiagnosticSeverity::Error,
242 error.message.clone(),
243 span,
244 )
245 .with_primary_label("MIR lowering failed here")
246 .with_category("mir-lowering")],
247 parse_failure: None,
248 lowering_failure: Some(error),
249 compile_failure: None,
250 bytecode: None,
251 resolution: Vec::new(),
252 domains: AnalysisDomains::unavailable_after_hir(),
253 };
254 }
255 };
256
257 let facts = runmat_mir::analysis::analyze_assembly(&mir);
258 let mut diagnostics = facts
259 .diagnostics
260 .iter()
261 .filter(|diagnostic| diagnostic.code != "RM-MIR0009")
264 .cloned()
265 .collect::<Vec<_>>();
266 diagnostics.extend(lint_shapes_from_mir(&mir, &facts));
267
268 let environment_effects = environment_effects(&mir);
269 let resolution = call_resolution_diagnostics(&lowering, &environment_effects, source_catalog);
270 let runtime_dependent = resolution.runtime_dependent;
271 diagnostics.extend(resolution.diagnostics);
272
273 let compiled = compile_lowering(&lowering, &mir);
274 let compile_failure = compiled.as_ref().err().cloned();
275 if let Some(error) = &compile_failure {
276 diagnostics.push(
277 HirDiagnostic::new(
278 error
279 .identifier
280 .clone()
281 .unwrap_or_else(|| "RunMat:CompileError".to_string()),
282 HirDiagnosticSeverity::Error,
283 error.message.clone(),
284 error.span.unwrap_or(Span { start: 0, end: 0 }),
285 )
286 .with_primary_label("compilation failed here")
287 .with_category("compile"),
288 );
289 }
290
291 diagnostics.sort_by_key(|diagnostic| {
292 (
293 diagnostic.primary.span.start,
294 diagnostic.primary.span.end,
295 diagnostic.code.clone(),
296 )
297 });
298 diagnostics.dedup_by(|left, right| {
299 left.code == right.code
300 && left.primary.span == right.primary.span
301 && left.message == right.message
302 });
303
304 let mut domains = AnalysisDomains::completed_frontend();
305 if runtime_dependent {
306 domains.name_resolution = AnalysisCompleteness::RuntimeDependent;
307 }
308 FrontendAnalysis {
309 lowering: Some(lowering),
310 mir: Some(mir),
311 facts: Some(facts),
312 diagnostics,
313 parse_failure: None,
314 lowering_failure: None,
315 compile_failure,
316 bytecode: compiled.ok(),
317 resolution: resolution.evidence,
318 domains,
319 }
320}
321
322fn compile_lowering(
323 lowering: &LoweringResult,
324 mir: &MirAssembly,
325) -> Result<runmat_vm::Bytecode, CompileError> {
326 let Some(entrypoint) = lowering.assembly.entrypoints.first() else {
327 let bound_functions =
328 runmat_vm::compile_semantic_function_registry(&lowering.assembly, mir)?;
329 let function_registry = runmat_vm::FunctionRegistry::new(bound_functions.clone());
330 let mut bytecode = runmat_vm::Bytecode::empty();
331 bytecode.bound_functions = bound_functions;
332 bytecode.function_registry = function_registry;
333 return Ok(bytecode);
334 };
335 runmat_vm::compile(&lowering.assembly, mir, entrypoint.id)
336}
337
338fn parse_failure_message(failure: &ParseFailure) -> String {
339 let mut message = failure.message.clone();
340 if let Some(expected) = &failure.expected {
341 message.push_str(&format!("; expected {expected}"));
342 }
343 if let Some(found) = &failure.found_token {
344 message.push_str(&format!("; found `{found}`"));
345 }
346 message
347}
348
349fn source_position_span(source: &str, position: usize) -> Span {
350 let start = position.min(source.len());
351 let end = source[start..]
352 .chars()
353 .next()
354 .map(|character| start + character.len_utf8())
355 .unwrap_or(start);
356 Span { start, end }
357}
358
359#[derive(Debug, Clone)]
360struct EnvironmentEffect {
361 span: Span,
362 effect: HirEnvironmentEffect,
363}
364
365fn environment_effects(mir: &MirAssembly) -> Vec<EnvironmentEffect> {
366 let mut effects = Vec::new();
367 for body in mir.bodies.values() {
368 for block in &body.blocks {
369 for statement in &block.statements {
370 if let MirStmtKind::EnvironmentEffect(effect) = &statement.kind {
371 effects.push(EnvironmentEffect {
372 span: statement.span,
373 effect: effect.clone(),
374 });
375 }
376 }
377 }
378 }
379 effects.sort_by_key(|effect| effect.span.start);
380 effects
381}
382
383struct ResolutionDiagnostics {
384 diagnostics: Vec<HirDiagnostic>,
385 evidence: Vec<ResolutionEvidence>,
386 runtime_dependent: bool,
387}
388
389fn call_resolution_diagnostics(
390 lowering: &LoweringResult,
391 environment_effects: &[EnvironmentEffect],
392 source_catalog: Option<&runmat_config::project::DiscoveredSourceSymbols>,
393) -> ResolutionDiagnostics {
394 let mut diagnostics = Vec::new();
395 let mut evidence = Vec::new();
396 let mut runtime_dependent = false;
397 for call in &lowering.hir_index.calls {
398 let name = call
399 .name
400 .display_name()
401 .unwrap_or_else(|| "<dynamic call>".to_string());
402 if !matches!(call.callee, HirCallableRef::Unresolved(_)) {
403 if let Some(definition) = catalog_definition(source_catalog, &name) {
404 evidence.push(ResolutionEvidence {
405 name,
406 span: call.span,
407 state: ResolutionState::Resolved,
408 reason: "matched a statically indexed project source".to_string(),
409 definition: Some(definition.clone()),
410 });
411 }
412 continue;
413 }
414 if !matches!(call.kind, CallKind::Dynamic)
415 || !matches!(call.callee, HirCallableRef::Unresolved(_))
416 {
417 continue;
418 }
419 if matches!(call.syntax, CallSyntax::Method | CallSyntax::DottedInvoke) {
420 runtime_dependent = true;
421 evidence.push(ResolutionEvidence {
422 name,
423 span: call.span,
424 state: ResolutionState::RuntimeDependent,
425 reason: "method dispatch depends on the receiver's runtime class".to_string(),
426 definition: None,
427 });
428 diagnostics.push(
429 HirDiagnostic::new(
430 DIAGNOSTIC_RUNTIME_METHOD_DISPATCH,
431 HirDiagnosticSeverity::Warning,
432 "cannot determine which function is called here",
433 call.span,
434 )
435 .with_primary_label("the call target is selected at runtime")
436 .with_note(
437 "method dispatch depends on the runtime class of the receiver expression",
438 )
439 .with_category("call-resolution"),
440 );
441 continue;
442 }
443 let causal_effect = environment_effects
444 .iter()
445 .rev()
446 .find(|effect| effect.span.start < call.span.start);
447 if let Some(effect) = causal_effect {
448 runtime_dependent = true;
449 evidence.push(ResolutionEvidence {
450 name: name.clone(),
451 span: call.span,
452 state: ResolutionState::RuntimeDependent,
453 reason: "a preceding statement changes the runtime function lookup environment"
454 .to_string(),
455 definition: None,
456 });
457 let effect_label = match effect.effect {
458 HirEnvironmentEffect::PathMutation => {
459 "this changes where subsequent functions are loaded from"
460 }
461 HirEnvironmentEffect::WorkingDirectoryMutation => {
462 "this changes the base directory used for subsequent lookup"
463 }
464 HirEnvironmentEffect::FunctionCacheInvalidation => {
465 "this invalidates cached function lookup"
466 }
467 HirEnvironmentEffect::DynamicLookupInvalidation => {
468 "this changes subsequent dynamic lookup behavior"
469 }
470 };
471 diagnostics.push(
472 HirDiagnostic::new(
473 DIAGNOSTIC_RUNTIME_DEPENDENT_RESOLUTION,
474 HirDiagnosticSeverity::Warning,
475 format!("cannot resolve `{name}` after a runtime environment change"),
476 call.span,
477 )
478 .with_primary_label(format!(
479 "RunMat cannot determine which `{name}.m` will be used"
480 ))
481 .with_secondary(effect.span, effect_label)
482 .with_note(
483 "all statically known source roots were checked before classifying this call",
484 )
485 .with_category("call-resolution"),
486 );
487 } else {
488 evidence.push(ResolutionEvidence {
489 name: name.clone(),
490 span: call.span,
491 state: ResolutionState::Unresolved,
492 reason: "no matching builtin, local, imported, or indexed project source was found"
493 .to_string(),
494 definition: None,
495 });
496 diagnostics.push(
497 HirDiagnostic::new(
498 DIAGNOSTIC_UNRESOLVED_FUNCTION,
499 HirDiagnosticSeverity::Warning,
500 format!("cannot find function `{name}`"),
501 call.span,
502 )
503 .with_primary_label("not defined in this file or project")
504 .with_note(
505 "RunMat checked built-ins, local functions, imports, and every configured source root",
506 )
507 .with_help(format!(
508 "place `{name}.m` beside this source or in a source root configured by `runmat.toml`"
509 ))
510 .with_category("call-resolution"),
511 );
512 }
513 }
514 for handle in &lowering.hir_index.function_handles {
515 let FunctionHandleTarget::DynamicName(_) = &handle.target else {
516 continue;
517 };
518 let name = handle
519 .name
520 .display_name()
521 .unwrap_or_else(|| "<dynamic function handle>".to_string());
522 if let Some(effect) = environment_effects
523 .iter()
524 .rev()
525 .find(|effect| effect.span.start < handle.span.start)
526 {
527 runtime_dependent = true;
528 evidence.push(ResolutionEvidence {
529 name: name.clone(),
530 span: handle.span,
531 state: ResolutionState::RuntimeDependent,
532 reason: "a preceding statement changes the runtime function lookup environment"
533 .to_string(),
534 definition: None,
535 });
536 diagnostics.push(
537 HirDiagnostic::new(
538 DIAGNOSTIC_RUNTIME_DEPENDENT_RESOLUTION,
539 HirDiagnosticSeverity::Warning,
540 format!("cannot resolve function handle `@{name}` after a runtime environment change"),
541 handle.span,
542 )
543 .with_primary_label(format!(
544 "RunMat cannot determine which `{name}.m` this handle will reference"
545 ))
546 .with_secondary(
547 effect.span,
548 "this changes the function lookup environment used by the handle",
549 )
550 .with_category("call-resolution"),
551 );
552 } else {
553 evidence.push(ResolutionEvidence {
554 name: name.clone(),
555 span: handle.span,
556 state: ResolutionState::Unresolved,
557 reason: "no matching builtin, local, imported, or indexed project source was found"
558 .to_string(),
559 definition: None,
560 });
561 diagnostics.push(
562 HirDiagnostic::new(
563 DIAGNOSTIC_UNRESOLVED_FUNCTION,
564 HirDiagnosticSeverity::Warning,
565 format!("cannot find function referenced by `@{name}`"),
566 handle.span,
567 )
568 .with_primary_label("the named function is not defined in this file or project")
569 .with_note(
570 "RunMat checked built-ins, local functions, imports, and every configured source root",
571 )
572 .with_help(format!(
573 "place `{name}.m` beside this source or in a source root configured by `runmat.toml`"
574 ))
575 .with_category("call-resolution"),
576 );
577 }
578 }
579 ResolutionDiagnostics {
580 diagnostics,
581 evidence,
582 runtime_dependent,
583 }
584}
585
586fn catalog_definition<'a>(
587 catalog: Option<&'a runmat_config::project::DiscoveredSourceSymbols>,
588 name: &str,
589) -> Option<&'a runmat_config::project::ProjectSymbolDefinition> {
590 catalog?
591 .definitions
592 .iter()
593 .find(|definition| definition.name == name)
594}
595
596#[cfg(test)]
597mod tests {
598 use super::*;
599 use std::collections::HashSet;
600 use std::path::PathBuf;
601
602 fn analyze(source: &str) -> FrontendAnalysis {
603 analyze_source(source, CompatMode::default(), &LoweringContext::empty())
604 }
605
606 #[test]
607 fn reports_parse_failures_as_structured_errors() {
608 let analysis = analyze("x = ;");
609 assert!(analysis.has_errors());
610 assert_eq!(analysis.diagnostics[0].code, "RunMat:ParseError");
611 assert_eq!(
612 analysis.domains.name_resolution,
613 AnalysisCompleteness::Unavailable
614 );
615 }
616
617 #[test]
618 fn reports_unresolved_bare_calls_without_rejecting_compilation() {
619 let analysis = analyze("x = definitely_missing(42);");
620 assert!(analysis.compile_failure.is_none());
621 assert!(analysis
622 .diagnostics
623 .iter()
624 .any(|diagnostic| diagnostic.code == DIAGNOSTIC_UNRESOLVED_FUNCTION));
625 }
626
627 #[test]
628 fn resolves_known_project_functions() {
629 let symbols = HashSet::from(["helper".to_string()]);
630 let context = LoweringContext::empty().with_known_project_symbols(&symbols);
631 let analysis = analyze_source("x = helper(42);", CompatMode::default(), &context);
632 assert!(!analysis
633 .diagnostics
634 .iter()
635 .any(|diagnostic| diagnostic.code == DIAGNOSTIC_UNRESOLVED_FUNCTION));
636 }
637
638 #[test]
639 fn preserves_the_project_definition_that_justified_resolution() {
640 let symbols = HashSet::from(["helper".to_string()]);
641 let definition = runmat_config::project::ProjectSymbolDefinition {
642 name: "helper".to_string(),
643 qualified_name: "helper".to_string(),
644 source_path: PathBuf::from("src/helper.m"),
645 package_name: "demo".to_string(),
646 is_private: false,
647 };
648 let catalog = runmat_config::project::DiscoveredSourceSymbols {
649 manifest_path: Some(PathBuf::from("runmat.toml")),
650 project_root: PathBuf::from("."),
651 symbols: symbols.clone(),
652 definitions: vec![definition.clone()],
653 };
654 let context = LoweringContext::empty().with_known_project_symbols(&symbols);
655 let analysis = analyze_source_with_catalog(
656 "x = helper(42);",
657 CompatMode::default(),
658 &context,
659 Some(&catalog),
660 );
661 assert_eq!(
662 analysis.resolution,
663 vec![ResolutionEvidence {
664 name: "helper".to_string(),
665 span: analysis.resolution[0].span,
666 state: ResolutionState::Resolved,
667 reason: "matched a statically indexed project source".to_string(),
668 definition: Some(definition),
669 }]
670 );
671 }
672
673 #[test]
674 fn includes_mir_and_shape_diagnostics() {
675 let analysis = analyze("a = ones(2,3); b = ones(4,2); c = a * b;");
676 assert!(analysis
677 .diagnostics
678 .iter()
679 .any(|diagnostic| diagnostic.code == "lint.shape.matmul"));
680 }
681
682 #[test]
683 fn distinguishes_runtime_path_mutation_from_an_unresolved_static_call() {
684 let analysis = analyze("addpath('plugins'); value = selected_at_runtime(1);");
685 assert!(analysis
686 .diagnostics
687 .iter()
688 .any(|diagnostic| diagnostic.code == DIAGNOSTIC_RUNTIME_DEPENDENT_RESOLUTION));
689 assert_eq!(
690 analysis.domains.name_resolution,
691 AnalysisCompleteness::RuntimeDependent
692 );
693 }
694
695 #[test]
696 fn a_later_path_mutation_does_not_explain_an_earlier_missing_call() {
697 let analysis = analyze("value = still_missing(1); addpath('plugins');");
698 assert!(analysis
699 .diagnostics
700 .iter()
701 .any(|diagnostic| diagnostic.code == DIAGNOSTIC_UNRESOLVED_FUNCTION));
702 assert!(!analysis
703 .diagnostics
704 .iter()
705 .any(|diagnostic| diagnostic.code == DIAGNOSTIC_RUNTIME_DEPENDENT_RESOLUTION));
706 }
707
708 #[test]
709 fn unresolved_method_dispatch_is_reported_as_runtime_dependent() {
710 let analysis = analyze("receiver = struct(); value = receiver.compute(1);");
711 assert!(analysis
712 .diagnostics
713 .iter()
714 .any(|diagnostic| diagnostic.code == DIAGNOSTIC_RUNTIME_METHOD_DISPATCH));
715 }
716
717 #[test]
718 fn builtins_do_not_produce_missing_function_diagnostics() {
719 let analysis = analyze("value = sin(1);");
720 assert!(!analysis
721 .diagnostics
722 .iter()
723 .any(|diagnostic| diagnostic.category.as_deref() == Some("call-resolution")));
724 }
725
726 #[test]
727 fn unresolved_function_handles_use_the_same_resolution_policy() {
728 let missing = analyze("handle = @definitely_missing;");
729 assert!(missing
730 .diagnostics
731 .iter()
732 .any(|diagnostic| diagnostic.code == DIAGNOSTIC_UNRESOLVED_FUNCTION));
733
734 let symbols = HashSet::from(["project.helper".to_string()]);
735 let context = LoweringContext::empty().with_known_project_symbols(&symbols);
736 let resolved = analyze_source("handle = @project.helper;", CompatMode::default(), &context);
737 assert!(!resolved
738 .diagnostics
739 .iter()
740 .any(|diagnostic| diagnostic.code == DIAGNOSTIC_UNRESOLVED_FUNCTION));
741 }
742}