1use super::*;
2use crate::fusion::FusionPlannerMetadata;
3use std::collections::{HashMap, HashSet};
4use std::path::{Path, PathBuf};
5
6fn entrypoint_target_function(
7 assembly: &runmat_hir::HirAssembly,
8) -> Option<runmat_hir::FunctionId> {
9 assembly
10 .entrypoints
11 .first()
12 .map(|entrypoint| entrypoint.target)
13}
14
15fn mir_local_fact_count_for_entrypoint(
16 analysis: &runmat_mir::analysis::AnalysisStore,
17 assembly: &runmat_hir::HirAssembly,
18) -> usize {
19 let Some(entrypoint_target) = entrypoint_target_function(assembly) else {
20 return analysis.mir_locals.len();
21 };
22 analysis
23 .mir_locals
24 .keys()
25 .filter(|key| key.function == entrypoint_target)
26 .count()
27}
28
29fn discover_source_catalog(
30 source_name: &str,
31) -> Option<runmat_config::project::DiscoveredSourceSymbols> {
32 use runmat_config::project::discover_source_symbols_from_source_name;
33
34 let source_path = PathBuf::from(source_name);
35 let cwd = if source_path.is_absolute() {
36 source_path
37 .parent()
38 .map(Path::to_path_buf)
39 .unwrap_or_else(|| PathBuf::from("."))
40 } else {
41 runmat_filesystem::current_dir().unwrap_or_else(|_| PathBuf::from("."))
42 };
43 discover_source_symbols_from_source_name(source_name, &cwd)
44 .ok()
45 .flatten()
46}
47
48fn function_output_arities(
49 registry: &runmat_vm::FunctionRegistry,
50) -> HashMap<runmat_hir::FunctionId, runmat_hir::FunctionOutputArity> {
51 registry
52 .functions
53 .iter()
54 .map(|(id, function)| {
55 (
56 *id,
57 runmat_hir::FunctionOutputArity::new(
58 function.output_slots.len(),
59 function.varargout_slot.is_some(),
60 ),
61 )
62 })
63 .collect()
64}
65
66fn source_lookup_cwd(source_name: &str) -> Option<PathBuf> {
67 let source_path = PathBuf::from(source_name);
68 if source_path.is_absolute() {
69 return source_path
70 .parent()
71 .map(Path::to_path_buf)
72 .or_else(|| Some(PathBuf::from(".")));
73 }
74 Some(runmat_filesystem::current_dir().unwrap_or_else(|_| PathBuf::from(".")))
75}
76
77fn resolved_source_path(source_name: &str, cwd: &Path) -> PathBuf {
78 crate::diagnostic_path::resolve_against_base(source_name, cwd)
79}
80
81fn path_to_source_name(path: &Path) -> String {
82 path.to_string_lossy().to_string()
83}
84
85fn is_class_source_body(stmts: &[runmat_parser::Stmt]) -> bool {
86 let has_classdef = stmts
87 .iter()
88 .any(|stmt| matches!(stmt, runmat_parser::Stmt::ClassDef { .. }));
89 if !has_classdef {
90 return false;
91 }
92 stmts.iter().all(|stmt| {
93 matches!(
94 stmt,
95 runmat_parser::Stmt::ClassDef { .. } | runmat_parser::Stmt::Function { .. }
96 )
97 })
98}
99
100fn is_function_source_body(stmts: &[runmat_parser::Stmt]) -> bool {
101 !stmts.is_empty()
102 && stmts
103 .iter()
104 .all(|stmt| matches!(stmt, runmat_parser::Stmt::Function { .. }))
105}
106
107fn package_class_name_from_path(source_path: &Path, root_dir: &Path) -> Option<String> {
108 let relative = source_path.strip_prefix(root_dir).ok()?;
109 let class_name = source_path.file_stem()?.to_str()?;
110 let mut package_segments = Vec::new();
111 if let Some(parent) = relative.parent() {
112 for component in parent.components() {
113 let segment = component.as_os_str().to_str()?;
114 if let Some(pkg) = segment.strip_prefix('+') {
115 if pkg.is_empty() {
116 return None;
117 }
118 package_segments.push(pkg.to_string());
119 } else if let Some(class) = segment.strip_prefix('@') {
120 if class.is_empty() {
121 return None;
122 }
123 package_segments.push(class.to_string());
124 } else {
125 return None;
126 }
127 }
128 }
129 if package_segments.is_empty() {
130 return None;
131 }
132 package_segments.push(class_name.to_string());
133 Some(package_segments.join("."))
134}
135
136fn qualify_companion_classdefs(stmts: &mut [runmat_parser::Stmt], qualified_name: &str) {
137 for stmt in stmts {
138 if let runmat_parser::Stmt::ClassDef { name, .. } = stmt {
139 if !name.contains('.') {
140 *name = qualified_name.to_string();
141 }
142 }
143 }
144}
145
146fn qualify_companion_functions(stmts: &mut [runmat_parser::Stmt], qualified_name: &str) {
147 for stmt in stmts {
148 if let runmat_parser::Stmt::Function { name, .. } = stmt {
149 if !name.contains('.') {
150 *name = qualified_name.to_string();
151 }
152 }
153 }
154}
155
156fn source_index_qualified_function_name(
157 source: &runmat_config::project::ProjectSourceFile,
158) -> Option<&str> {
159 source.function_qualified_name()
160}
161
162fn source_index_qualified_class_name(
163 source: &runmat_config::project::ProjectSourceFile,
164) -> Option<&str> {
165 source.class_definition_qualified_name()
166}
167
168fn is_private_dir(path: &Path) -> bool {
169 path.file_name()
170 .and_then(|name| name.to_str())
171 .is_some_and(|name| name.eq_ignore_ascii_case("private"))
172}
173
174fn private_parent_dir_for_source(path: &Path) -> Option<PathBuf> {
175 let private_dir = path.parent()?;
176 if !is_private_dir(private_dir) {
177 return None;
178 }
179 private_dir.parent().map(Path::to_path_buf)
180}
181
182fn source_paths_equivalent(left: &Path, right: &Path) -> bool {
183 if left == right {
184 return true;
185 }
186
187 #[cfg(not(target_arch = "wasm32"))]
188 {
189 if let (Ok(left), Ok(right)) = (std::fs::canonicalize(left), std::fs::canonicalize(right)) {
190 if left == right {
191 return true;
192 }
193 }
194 }
195
196 #[cfg(windows)]
197 {
198 windows_source_path_key(left) == windows_source_path_key(right)
199 }
200 #[cfg(not(windows))]
201 {
202 false
203 }
204}
205
206#[cfg(windows)]
207fn windows_source_path_key(path: &Path) -> String {
208 let mut text = path.to_string_lossy().replace('/', "\\");
209 if let Some(stripped) = text.strip_prefix(r"\\?\UNC\") {
210 text = format!(r"\\{stripped}");
211 } else if let Some(stripped) = text.strip_prefix(r"\\?\") {
212 text = stripped.to_string();
213 }
214 while text.ends_with('\\') && text.len() > 3 {
215 text.pop();
216 }
217 text.to_ascii_lowercase()
218}
219
220fn private_source_visible_to(primary_source_path: &Path, source_path: &Path) -> bool {
221 let Some(private_parent) = private_parent_dir_for_source(source_path) else {
222 return true;
223 };
224 primary_source_path
225 .parent()
226 .is_some_and(|caller_dir| source_paths_equivalent(caller_dir, &private_parent))
227}
228
229fn function_owner_scope_from_qualified_name(qualified_name: &str) -> String {
230 qualified_name
231 .rsplit_once('.')
232 .map(|(owner, _)| owner.to_string())
233 .unwrap_or_default()
234}
235
236fn function_leaf_name(name: &str) -> &str {
237 name.rsplit_once('.').map(|(_, leaf)| leaf).unwrap_or(name)
238}
239
240fn synthetic_private_function_name(owner_scope: &str, leaf_name: &str) -> String {
241 if owner_scope.is_empty() {
242 format!("__private__.{leaf_name}")
243 } else {
244 format!("{owner_scope}.__private__.{leaf_name}")
245 }
246}
247
248fn qualify_private_companion_functions(
249 stmts: &mut [runmat_parser::Stmt],
250 owner_scope: &str,
251 primary_visible: bool,
252) -> HashMap<String, String> {
253 let mut aliases = HashMap::new();
254 for stmt in stmts {
255 if let runmat_parser::Stmt::Function { name, .. } = stmt {
256 let leaf = function_leaf_name(name).to_string();
257 let display_name = if primary_visible {
258 leaf.clone()
259 } else {
260 synthetic_private_function_name(owner_scope, &leaf)
261 };
262 *name = display_name.clone();
263 aliases.insert(leaf, display_name);
264 }
265 }
266 aliases
267}
268
269#[derive(Default)]
270pub(super) struct CompanionSourceDiscovery {
271 pub statements: Vec<runmat_parser::Stmt>,
272 pub private_function_names: HashSet<String>,
273 pub private_function_owners: HashMap<String, String>,
274 pub private_function_aliases: HashMap<String, HashMap<String, String>>,
275 pub function_source_contexts: HashMap<String, SourceContextData>,
276 private_statement_flags: Vec<bool>,
277}
278
279#[derive(Clone)]
280pub(super) struct SourceContextData {
281 pub display_name: String,
282 pub fullpath_name: Option<String>,
283 pub text: String,
284}
285
286fn function_names_in_statements(stmts: &[runmat_parser::Stmt]) -> impl Iterator<Item = &str> {
287 stmts.iter().filter_map(|stmt| {
288 if let runmat_parser::Stmt::Function { name, .. } = stmt {
289 Some(name.as_str())
290 } else {
291 None
292 }
293 })
294}
295
296fn source_context_function_names_in_statements(stmts: &[runmat_parser::Stmt]) -> Vec<String> {
297 let mut names = Vec::new();
298 for stmt in stmts {
299 match stmt {
300 runmat_parser::Stmt::Function { name, .. } => names.push(name.clone()),
301 runmat_parser::Stmt::ClassDef {
302 name: class_name,
303 members,
304 ..
305 } => {
306 for member in members {
307 if let runmat_parser::ClassMember::Methods { body, .. } = member {
308 for stmt in body {
309 if let runmat_parser::Stmt::Function { name, .. } = stmt {
310 let display_name = if name.contains('.') {
311 name.clone()
312 } else {
313 format!("{class_name}.{name}")
314 };
315 names.push(display_name);
316 }
317 }
318 }
319 }
320 }
321 _ => {}
322 }
323 }
324 names
325}
326
327impl CompanionSourceDiscovery {
328 fn extend_body(
329 &mut self,
330 body: Vec<runmat_parser::Stmt>,
331 private_owner_scope: Option<&str>,
332 private_aliases: HashMap<String, String>,
333 source_context: Option<SourceContextData>,
334 ) {
335 if let Some(source_context) = source_context {
336 for function_name in source_context_function_names_in_statements(&body) {
337 self.function_source_contexts
338 .insert(function_name, source_context.clone());
339 }
340 }
341 let is_private = private_owner_scope.is_some();
342 if is_private {
343 let owner_scope = private_owner_scope.unwrap_or_default();
344 for function_name in function_names_in_statements(&body) {
345 self.private_function_names
346 .insert(function_name.to_string());
347 self.private_function_owners
348 .insert(function_name.to_string(), owner_scope.to_string());
349 }
350 if !private_aliases.is_empty() {
351 self.private_function_aliases
352 .entry(owner_scope.to_string())
353 .or_default()
354 .extend(private_aliases);
355 }
356 }
357 for stmt in body {
358 self.statements.push(stmt);
359 self.private_statement_flags.push(is_private);
360 }
361 }
362
363 fn apply_function_precedence(&mut self, primary_function_names: &HashSet<String>) {
364 let discovered_private_function_names: HashSet<String> = self
365 .statements
366 .iter()
367 .zip(self.private_statement_flags.iter())
368 .filter_map(|(stmt, is_private)| {
369 if !*is_private {
370 return None;
371 }
372 if let runmat_parser::Stmt::Function { name, .. } = stmt {
373 Some(name.clone())
374 } else {
375 None
376 }
377 })
378 .collect();
379
380 let old_statements = std::mem::take(&mut self.statements);
381 let old_private_flags = std::mem::take(&mut self.private_statement_flags);
382 self.private_function_names.clear();
383 self.private_function_owners.clear();
384 self.private_function_aliases.clear();
385
386 for (stmt, is_private) in old_statements.into_iter().zip(old_private_flags) {
387 let keep = match &stmt {
388 runmat_parser::Stmt::Function { name, .. } => {
389 !primary_function_names.contains(name)
390 && (is_private || !discovered_private_function_names.contains(name))
391 }
392 _ => true,
393 };
394 if !keep {
395 if let runmat_parser::Stmt::Function { name, .. } = &stmt {
396 self.function_source_contexts.remove(name);
397 }
398 continue;
399 }
400 if is_private {
401 if let runmat_parser::Stmt::Function { name, .. } = &stmt {
402 self.private_function_names.insert(name.clone());
403 let owner_scope = function_owner_scope_from_qualified_name(name);
404 let owner_scope = if let Some((owner, _)) = name.split_once(".__private__.") {
405 owner.to_string()
406 } else {
407 owner_scope
408 };
409 self.private_function_owners
410 .insert(name.clone(), owner_scope.clone());
411 self.private_function_aliases
412 .entry(owner_scope)
413 .or_default()
414 .insert(function_leaf_name(name).to_string(), name.clone());
415 }
416 }
417 self.statements.push(stmt);
418 self.private_statement_flags.push(is_private);
419 }
420 }
421}
422
423async fn discover_companion_from_composition_graph_async(
424 source_name: &str,
425 cwd: &Path,
426 primary_source_path: &Path,
427 compat_mode: runmat_parser::CompatMode,
428) -> Result<Option<CompanionSourceDiscovery>, String> {
429 use runmat_config::project::{
430 build_project_composition_graph_async, discover_project_symbols_from_source_name_async,
431 };
432 let options = ParserOptions::new(compat_mode);
433 let Some(discovered_symbols) =
434 discover_project_symbols_from_source_name_async(source_name, cwd)
435 .await
436 .map_err(|error| error.to_string())?
437 else {
438 return Ok(None);
439 };
440 let composition = build_project_composition_graph_async(&discovered_symbols.manifest_path)
441 .await
442 .map_err(|error| error.to_string())?;
443 let mut out = CompanionSourceDiscovery::default();
444 for package in composition.packages.values() {
445 for source in &package.source_index.files {
446 let file_path = package
447 .project_root
448 .join(&source.source_root)
449 .join(&source.relative_path);
450 let file_path =
451 runmat_runtime::builtins::io::repl_fs::pcode::prefer_pcode_source_path(&file_path)
452 .await;
453 if source_paths_equivalent(&file_path, primary_source_path) {
454 continue;
455 }
456 let Ok(contents) =
457 runmat_runtime::builtins::io::repl_fs::pcode::read_source_text_async(&file_path)
458 .await
459 else {
460 continue;
461 };
462 if !contents.contains("classdef") && !contents.contains("function") {
463 continue;
464 }
465 let Ok(program) = parse_with_options(&contents, options) else {
466 continue;
467 };
468 let is_class_source = is_class_source_body(&program.body);
469 let is_function_source = is_function_source_body(&program.body);
470 if !is_class_source && !is_function_source {
471 continue;
472 }
473 let mut body = program.body;
474 let private_owner_scope = source
475 .is_private
476 .then(|| function_owner_scope_from_qualified_name(&source.qualified_name));
477 let primary_visible_private =
478 source.is_private && private_source_visible_to(primary_source_path, &file_path);
479 let private_aliases = if let Some(owner_scope) = private_owner_scope.as_deref() {
480 qualify_private_companion_functions(&mut body, owner_scope, primary_visible_private)
481 } else {
482 HashMap::new()
483 };
484 if is_class_source {
485 if let Some(qualified) = source_index_qualified_class_name(source) {
486 qualify_companion_classdefs(&mut body, qualified);
487 } else if let Some(qualified) = package_class_name_from_path(&file_path, cwd) {
488 qualify_companion_classdefs(&mut body, &qualified);
489 }
490 } else if private_owner_scope.is_none() {
491 if let Some(qualified) = source_index_qualified_function_name(source) {
492 qualify_companion_functions(&mut body, qualified);
493 } else if let Some(qualified) = package_class_name_from_path(&file_path, cwd) {
494 qualify_companion_functions(&mut body, &qualified);
495 }
496 }
497 out.extend_body(
498 body,
499 private_owner_scope.as_deref(),
500 private_aliases,
501 Some(SourceContextData {
502 display_name: crate::diagnostic_path::display_path_from_base(&file_path, cwd),
503 fullpath_name: Some(path_to_source_name(&file_path)),
504 text: contents,
505 }),
506 );
507 }
508 }
509 Ok(Some(out))
510}
511
512pub(super) async fn discover_companion_source_statements_async(
513 source_name: &str,
514 compat_mode: runmat_parser::CompatMode,
515) -> Result<CompanionSourceDiscovery, String> {
516 let Some(cwd) = source_lookup_cwd(source_name) else {
517 return Ok(CompanionSourceDiscovery::default());
518 };
519 let primary_source_path = resolved_source_path(source_name, &cwd);
520 let primary_is_local_file = runmat_filesystem::metadata_async(&primary_source_path)
521 .await
522 .map(|metadata| metadata.is_file())
523 .unwrap_or(false);
524 if !primary_is_local_file {
525 return Ok(CompanionSourceDiscovery::default());
529 }
530 let Some(parent) = primary_source_path.parent() else {
531 return Ok(CompanionSourceDiscovery::default());
532 };
533 if let Some(out) = discover_companion_from_composition_graph_async(
534 source_name,
535 &cwd,
536 &primary_source_path,
537 compat_mode,
538 )
539 .await?
540 {
541 return Ok(out);
542 }
543 let mut out = CompanionSourceDiscovery::default();
544 let options = ParserOptions::new(compat_mode);
545 let source_index = runmat_config::project::build_loose_source_index_async(parent)
546 .await
547 .map_err(|error| error.to_string())?;
548 for source in source_index.files {
549 let path = parent.join(&source.source_root).join(&source.relative_path);
550 if source_paths_equivalent(&path, &primary_source_path) {
551 continue;
552 }
553 let Ok(contents) = runmat_filesystem::read_to_string_async(&path).await else {
554 continue;
555 };
556 if !contents.contains("classdef") && !contents.contains("function") {
557 continue;
558 }
559 let Ok(program) = parse_with_options(&contents, options) else {
560 continue;
561 };
562 let is_class_source = is_class_source_body(&program.body);
563 let is_function_source = is_function_source_body(&program.body);
564 if !is_class_source && !is_function_source {
565 continue;
566 }
567 let mut body = program.body;
568 let private_owner_scope = source
569 .is_private
570 .then(|| function_owner_scope_from_qualified_name(&source.qualified_name));
571 let primary_visible_private =
572 private_owner_scope.is_some() && private_source_visible_to(&primary_source_path, &path);
573 let private_aliases = if let Some(owner_scope) = private_owner_scope.as_deref() {
574 qualify_private_companion_functions(&mut body, owner_scope, primary_visible_private)
575 } else {
576 HashMap::new()
577 };
578 if is_class_source {
579 if let Some(qualified) = source.class_definition_qualified_name() {
580 qualify_companion_classdefs(&mut body, qualified);
581 }
582 } else if private_owner_scope.is_none() {
583 if let Some(qualified) = source.function_qualified_name() {
584 qualify_companion_functions(&mut body, qualified);
585 }
586 }
587 out.extend_body(
588 body,
589 private_owner_scope.as_deref(),
590 private_aliases,
591 Some(SourceContextData {
592 display_name: crate::diagnostic_path::display_path_from_base(&path, &cwd),
593 fullpath_name: Some(path_to_source_name(&path)),
594 text: contents,
595 }),
596 );
597 }
598 Ok(out)
599}
600
601impl RunMatSession {
602 #[cfg(test)]
603 pub(crate) fn compile_input_for_source_name(
604 &mut self,
605 source_name: &str,
606 input: &str,
607 ) -> std::result::Result<PreparedExecution, RunError> {
608 let previous_source_name = self.active_source_name.clone();
609 let previous_source_fullpath_name = self.active_source_fullpath_name.clone();
610 self.active_source_name = source_name.to_string();
611 self.active_source_fullpath_name = None;
612 let result = self.compile_input(input);
613 self.active_source_name = previous_source_name;
614 self.active_source_fullpath_name = previous_source_fullpath_name;
615 result
616 }
617
618 pub(crate) fn compile_input(
619 &mut self,
620 input: &str,
621 ) -> std::result::Result<PreparedExecution, RunError> {
622 let source_name = self.current_source_name().to_string();
623 let source_fullpath_name = self.current_source_fullpath_name().map(ToString::to_string);
624 let source_id = self.source_pool.intern_with_fullpath(
625 &source_name,
626 source_fullpath_name.as_deref(),
627 input,
628 );
629 let (
630 ast,
631 private_companion_function_names,
632 private_companion_function_owners,
633 private_companion_function_aliases,
634 companion_function_source_ids,
635 ) = {
636 let _span = info_span!("runtime.parse").entered();
637 let mut ast = parse_with_options(input, ParserOptions::new(self.compat_mode))?;
638 let primary_function_names = function_names_in_statements(&ast.body)
639 .map(ToString::to_string)
640 .collect::<HashSet<_>>();
641 let mut companion = self
642 .pending_companion_source_discovery
643 .take()
644 .unwrap_or_default();
645 companion.apply_function_precedence(&primary_function_names);
646 let private_companion_function_names =
647 std::mem::take(&mut companion.private_function_names);
648 let private_companion_function_owners =
649 std::mem::take(&mut companion.private_function_owners);
650 let private_companion_function_aliases =
651 std::mem::take(&mut companion.private_function_aliases);
652 let companion_function_source_ids =
653 std::mem::take(&mut companion.function_source_contexts)
654 .into_iter()
655 .map(|(function_name, source_context)| {
656 (
657 function_name,
658 self.source_pool.intern_with_fullpath(
659 &source_context.display_name,
660 source_context.fullpath_name.as_deref(),
661 &source_context.text,
662 ),
663 )
664 })
665 .collect::<HashMap<_, _>>();
666 if !companion.statements.is_empty() {
667 ast.body.append(&mut companion.statements);
668 }
669 (
670 ast,
671 private_companion_function_names,
672 private_companion_function_owners,
673 private_companion_function_aliases,
674 companion_function_source_ids,
675 )
676 };
677 let (lowering, analysis, mut bytecode) = {
678 let _span = info_span!("runtime.lower").entered();
679 let function_names = self.function_registry.names.clone();
680 let function_output_arities = function_output_arities(&self.function_registry);
681 let workspace_bindings = self.lowering_workspace_bindings();
682 let source_lookup_name = source_fullpath_name.as_deref().unwrap_or(&source_name);
683 let source_catalog = discover_source_catalog(source_lookup_name);
684 let known_project_symbols = source_catalog
685 .as_ref()
686 .map(|catalog| &catalog.symbols)
687 .cloned()
688 .unwrap_or_default();
689 let frontend = runmat_static_analysis::frontend::analyze_program_with_catalog(
690 &ast,
691 &LoweringContext::new(&workspace_bindings)
692 .with_bound_functions(&function_names)
693 .with_function_output_arities(&function_output_arities)
694 .with_known_project_symbols(&known_project_symbols)
695 .with_private_functions(
696 &private_companion_function_owners,
697 &private_companion_function_aliases,
698 )
699 .with_runmat_extensions_enabled(self.compat_mode.allows_runmat_extensions())
700 .with_top_level_await_enabled(self.top_level_await_enabled),
701 source_catalog.as_ref(),
702 );
703 if let Some(error) = frontend.lowering_failure {
704 return Err(error.into());
705 }
706 if let Some(error) = frontend.compile_failure {
707 return Err(error.into());
708 }
709 (
710 frontend
711 .lowering
712 .expect("canonical frontend returned no lowering without an error"),
713 frontend
714 .facts
715 .expect("canonical frontend returned no facts without an error"),
716 frontend
717 .bytecode
718 .expect("canonical frontend returned no bytecode without an error"),
719 )
720 };
721 bytecode.source_id = Some(source_id);
722 for function in bytecode.function_registry.functions.values_mut() {
723 function.source_id = companion_function_source_ids
724 .get(&function.display_name)
725 .copied()
726 .or(Some(source_id));
727 }
728 bytecode.bound_functions = bytecode.function_registry.functions.clone();
729 let (function_registry_after_success, next_semantic_function_id_after_success) = self
730 .prepare_session_semantic_function_registry(
731 &mut bytecode,
732 &private_companion_function_names,
733 );
734 Ok(PreparedExecution {
735 ast,
736 lowering,
737 analysis,
738 bytecode,
739 function_registry_after_success,
740 next_semantic_function_id_after_success,
741 })
742 }
743
744 pub(crate) fn populate_callstack(&self, error: &mut RuntimeError) {
745 if !error.context.call_stack.is_empty() || error.context.call_frames.is_empty() {
746 return;
747 }
748 let mut rendered = Vec::new();
749 if error.context.call_frames_elided > 0 {
750 rendered.push(format!(
751 "... {} frames elided ...",
752 error.context.call_frames_elided
753 ));
754 }
755 for frame in error.context.call_frames.iter().rev() {
756 let mut line = frame.function.clone();
757 if let (Some(source_id), Some((start, _end))) = (frame.source_id, frame.span) {
758 if let Some(source) = self.source_pool.get(SourceId(source_id)) {
759 let (line_num, col) = line_col_from_offset(&source.text, start);
760 line = format!("{} @ {}:{}:{}", frame.function, source.name, line_num, col);
761 }
762 }
763 rendered.push(line);
764 }
765 error.context.call_stack = rendered;
766 }
767
768 fn prepare_session_semantic_function_registry(
769 &self,
770 bytecode: &mut runmat_vm::Bytecode,
771 private_companion_function_names: &HashSet<String>,
772 ) -> (runmat_vm::FunctionRegistry, usize) {
773 let mut session_registry = self.function_registry.clone();
774 let mut execution_registry = session_registry.clone();
775 let mut next_semantic_function_id = self.next_semantic_function_id;
776 let current_registry = bytecode.function_registry();
777 if current_registry.functions.is_empty() {
778 bytecode.function_registry = session_registry.clone();
779 bytecode.bound_functions = bytecode.function_registry.functions.clone();
780 bind_semantic_function_references(bytecode);
781 return (session_registry, next_semantic_function_id);
782 }
783
784 let mut remap = HashMap::new();
785 let mut ids: Vec<_> = current_registry.functions.keys().copied().collect();
786 ids.sort_by_key(|id| id.0);
787 for old_id in ids {
788 let new_id = runmat_hir::FunctionId(next_semantic_function_id);
789 next_semantic_function_id += 1;
790 remap.insert(old_id, new_id);
791 }
792
793 for instr in &mut bytecode.instructions {
794 remap_semantic_function_instr(instr, &remap);
795 }
796 let name_remaps: Vec<(String, runmat_hir::FunctionId)> = current_registry
797 .names
798 .iter()
799 .map(|(name, function)| (name.clone(), *function))
800 .collect();
801
802 let mut replaced_sources = Vec::new();
803 for function in current_registry.functions.values() {
804 if private_companion_function_names.contains(&function.display_name) {
805 continue;
806 }
807 if let Some(existing_id) = session_registry.resolve_name(&function.display_name) {
808 if let Some(source_id) = session_registry
809 .get(existing_id)
810 .and_then(|existing| existing.source_id)
811 {
812 if !replaced_sources.contains(&source_id) {
813 replaced_sources.push(source_id);
814 }
815 }
816 }
817 }
818 for source_id in replaced_sources {
819 session_registry.remove_source(source_id);
820 }
821
822 for (old_id, function) in current_registry.functions {
823 let Some(new_id) = remap.get(&old_id).copied() else {
824 continue;
825 };
826 let mut function = function;
827 function.function = new_id;
828 function.source_id = function.source_id.or(bytecode.source_id);
829 for instr in &mut function.instructions {
830 remap_semantic_function_instr(instr, &remap);
831 }
832 let persist_function =
833 !private_companion_function_names.contains(&function.display_name);
834 execution_registry.insert_replacing_name(function.clone());
835 if persist_function {
836 session_registry.insert_replacing_name(function);
837 }
838 }
839 for (name, old_id) in name_remaps {
840 let Some(new_id) = remap.get(&old_id).copied() else {
841 continue;
842 };
843 execution_registry.names.insert(name.clone(), new_id);
844 if !private_companion_function_names.contains(&name) {
845 session_registry.names.insert(name, new_id);
846 }
847 }
848
849 bytecode.function_registry = execution_registry;
850 bytecode.bound_functions = bytecode.function_registry.functions.clone();
851 bind_semantic_function_references(bytecode);
852 (session_registry, next_semantic_function_id)
853 }
854
855 pub(crate) fn normalize_error_namespace(&self, error: &mut RuntimeError) {
856 let Some(identifier) = error.identifier.clone() else {
857 return;
858 };
859 let suffix = identifier
860 .split_once(':')
861 .map(|(_, suffix)| suffix)
862 .unwrap_or(identifier.as_str());
863 error.identifier = Some(format!("{}:{suffix}", self.error_namespace));
864 }
865
866 pub fn compile_fusion_plan(
868 &mut self,
869 input: &str,
870 ) -> std::result::Result<Option<FusionPlanSnapshot>, RunError> {
871 let prepared = self.compile_input(input)?;
872 let runtime_groups = prepared.bytecode.runtime_fusion_groups();
873 let (runtime_graph, runtime_graph_source) = prepared
874 .bytecode
875 .runtime_accel_graph_for_fusion_with_source(&runtime_groups);
876 Ok(build_fusion_snapshot(
877 &runtime_groups,
878 &prepared
879 .bytecode
880 .fusion_metadata
881 .mir_fusion_candidate_groups,
882 &prepared.bytecode.fusion_metadata.instruction_windows,
883 Some(FusionPlannerMetadata {
884 source: "semantic-mir-analysis".to_string(),
885 accel_graph_state: if runtime_graph.is_some() {
886 "present".to_string()
887 } else {
888 "missing".to_string()
889 },
890 accel_graph_source: runtime_graph_source.as_str().to_string(),
891 mir_local_fact_count: mir_local_fact_count_for_entrypoint(
892 &prepared.analysis,
893 &prepared.lowering.assembly,
894 ),
895 mir_diagnostic_count: prepared.analysis.diagnostics.len(),
896 mir_fusion_signal_count: prepared.bytecode.fusion_metadata.mir_fusion_signal_count,
897 mir_fusion_candidate_group_count: prepared
898 .bytecode
899 .fusion_metadata
900 .mir_fusion_candidate_group_count,
901 mir_semantic_instruction_window_count: prepared
902 .bytecode
903 .fusion_metadata
904 .instruction_window_count,
905 }),
906 ))
907 }
908
909 pub(crate) fn prepare_variable_array_for_execution(
910 &mut self,
911 bytecode: &runmat_vm::Bytecode,
912 updated_var_mapping: &HashMap<String, usize>,
913 debug_trace: bool,
914 ) {
915 let max_var_id = updated_var_mapping.values().copied().max().unwrap_or(0);
917 let required_len = std::cmp::max(bytecode.var_count, max_var_id + 1);
918 let mut new_variable_array = vec![Value::Num(0.0); required_len];
919 if debug_trace {
920 debug!(
921 bytecode_var_count = bytecode.var_count,
922 required_len, max_var_id, "[repl] prepare variable array"
923 );
924 }
925
926 for (var_name, &new_var_id) in updated_var_mapping {
928 if new_var_id < new_variable_array.len() {
929 if let Some(value) = self.workspace_values.get(var_name) {
930 if debug_trace {
931 debug!(
932 var_name,
933 var_id = new_var_id,
934 ?value,
935 "[repl] prepare set var"
936 );
937 }
938 new_variable_array[new_var_id] = value.clone();
939 }
940 } else if debug_trace {
941 debug!(
942 var_name,
943 var_id = new_var_id,
944 len = new_variable_array.len(),
945 "[repl] prepare skipping var"
946 );
947 }
948 }
949
950 self.variable_array = new_variable_array;
952 }
953}
954
955fn remap_semantic_function_instr(
956 instr: &mut runmat_vm::Instr,
957 remap: &HashMap<runmat_hir::FunctionId, runmat_hir::FunctionId>,
958) {
959 match instr {
960 runmat_vm::Instr::CreateSemanticClosure(function, _, _)
961 | runmat_vm::Instr::CreateBoundFunctionHandle(function, _)
962 | runmat_vm::Instr::CreateSemanticFuture(function, _, _)
963 | runmat_vm::Instr::CreateSemanticFutureExpandMultiOutput(function, _, _)
964 | runmat_vm::Instr::CallSemanticFunctionMulti(function, _, _)
965 | runmat_vm::Instr::CallSemanticFunctionMultiUsingOutputSlot(function, _, _)
966 | runmat_vm::Instr::CallSemanticFunctionExpandMultiOutput(function, _, _) => {
967 if let Some(new_id) = remap.get(function).copied() {
968 *function = new_id;
969 }
970 }
971 runmat_vm::Instr::CallSemanticNestedFunctionMulti { function, .. }
972 | runmat_vm::Instr::CallSemanticNestedFunctionMultiUsingOutputSlot { function, .. }
973 | runmat_vm::Instr::CallSemanticNestedFunctionExpandMultiOutput { function, .. } => {
974 if let Some(new_id) = remap.get(function).copied() {
975 *function = new_id;
976 }
977 }
978 runmat_vm::Instr::IndexSliceExpr {
979 range_start_exprs,
980 range_step_exprs,
981 range_end_exprs,
982 end_numeric_exprs,
983 ..
984 }
985 | runmat_vm::Instr::StoreSliceExpr {
986 range_start_exprs,
987 range_step_exprs,
988 range_end_exprs,
989 end_numeric_exprs,
990 ..
991 } => {
992 remap_optional_end_exprs(range_start_exprs, remap);
993 remap_optional_end_exprs(range_step_exprs, remap);
994 for expr in range_end_exprs {
995 remap_semantic_function_end_expr(expr, remap);
996 }
997 for (_, expr) in end_numeric_exprs {
998 remap_semantic_function_end_expr(expr, remap);
999 }
1000 }
1001 _ => {}
1002 }
1003}
1004
1005fn bind_semantic_function_references(bytecode: &mut runmat_vm::Bytecode) {
1006 let registry = bytecode.function_registry.clone();
1007 bind_semantic_callback_literals(bytecode, ®istry);
1008 for instr in &mut bytecode.instructions {
1009 match instr {
1010 runmat_vm::Instr::CreateFunctionHandle(name) => {
1011 if let Some(function) = registry.resolve_name(name) {
1012 *instr = runmat_vm::Instr::CreateBoundFunctionHandle(function, name.clone());
1013 }
1014 }
1015 runmat_vm::Instr::IndexSliceExpr {
1016 range_start_exprs,
1017 range_step_exprs,
1018 range_end_exprs,
1019 end_numeric_exprs,
1020 ..
1021 }
1022 | runmat_vm::Instr::StoreSliceExpr {
1023 range_start_exprs,
1024 range_step_exprs,
1025 range_end_exprs,
1026 end_numeric_exprs,
1027 ..
1028 } => {
1029 bind_optional_end_exprs(range_start_exprs, ®istry);
1030 bind_optional_end_exprs(range_step_exprs, ®istry);
1031 for expr in range_end_exprs {
1032 bind_semantic_function_end_expr(expr, ®istry);
1033 }
1034 for (_, expr) in end_numeric_exprs {
1035 bind_semantic_function_end_expr(expr, ®istry);
1036 }
1037 }
1038 _ => {}
1039 }
1040 }
1041}
1042
1043fn bind_semantic_callback_literals(
1044 bytecode: &mut runmat_vm::Bytecode,
1045 registry: &runmat_vm::FunctionRegistry,
1046) {
1047 let mut stack: Vec<usize> = Vec::new();
1048 let mut replacements = Vec::new();
1049
1050 for (pc, instr) in bytecode.instructions.iter().enumerate() {
1051 match instr {
1052 runmat_vm::Instr::CallBuiltinMulti(name, argc, _)
1053 if matches!(name.as_str(), "cellfun" | "arrayfun") && *argc > 0 =>
1054 {
1055 if stack.len() >= *argc {
1056 let producer = stack[stack.len() - *argc];
1057 if let Some((function, display_name)) =
1058 callback_literal(bytecode.instructions.get(producer), registry)
1059 {
1060 replacements.push((producer, function, display_name));
1061 }
1062 }
1063 }
1064 runmat_vm::Instr::CallFevalMulti(argc, _) => {
1065 let pops = *argc + 1;
1066 if stack.len() >= pops {
1067 let producer = stack[stack.len() - pops];
1068 if let Some((function, display_name)) =
1069 callback_literal(bytecode.instructions.get(producer), registry)
1070 {
1071 replacements.push((producer, function, display_name));
1072 }
1073 }
1074 }
1075 runmat_vm::Instr::CallFevalMultiUsingOutputSlot(argc, _) => {
1076 let pops = *argc + 1;
1077 if stack.len() >= pops {
1078 let producer = stack[stack.len() - pops];
1079 if let Some((function, display_name)) =
1080 callback_literal(bytecode.instructions.get(producer), registry)
1081 {
1082 replacements.push((producer, function, display_name));
1083 }
1084 }
1085 }
1086 runmat_vm::Instr::CallFevalExpandMultiOutput(_, _) => {
1087 if let Some(effect) = instr.stack_effect() {
1088 if stack.len() >= effect.pops {
1089 let producer = stack[stack.len() - effect.pops];
1090 if let Some((function, display_name)) =
1091 callback_literal(bytecode.instructions.get(producer), registry)
1092 {
1093 replacements.push((producer, function, display_name));
1094 }
1095 }
1096 }
1097 }
1098 runmat_vm::Instr::CallFevalExpandMultiOutputUsingOutputSlot(_, _) => {
1099 if let Some(effect) = instr.stack_effect() {
1100 if stack.len() >= effect.pops {
1101 let producer = stack[stack.len() - effect.pops];
1102 if let Some((function, display_name)) =
1103 callback_literal(bytecode.instructions.get(producer), registry)
1104 {
1105 replacements.push((producer, function, display_name));
1106 }
1107 }
1108 }
1109 }
1110 _ => {}
1111 }
1112
1113 let Some(effect) = instr.stack_effect() else {
1114 stack.clear();
1115 continue;
1116 };
1117 if effect.pops > stack.len() {
1118 stack.clear();
1119 } else {
1120 for _ in 0..effect.pops {
1121 stack.pop();
1122 }
1123 }
1124 for _ in 0..effect.pushes {
1125 stack.push(pc);
1126 }
1127 }
1128
1129 for (producer, function, display_name) in replacements {
1130 bytecode.instructions[producer] =
1131 runmat_vm::Instr::CreateBoundFunctionHandle(function, display_name);
1132 }
1133}
1134
1135fn callback_literal(
1136 instr: Option<&runmat_vm::Instr>,
1137 registry: &runmat_vm::FunctionRegistry,
1138) -> Option<(runmat_hir::FunctionId, String)> {
1139 let text = match instr? {
1140 runmat_vm::Instr::LoadString(text) | runmat_vm::Instr::LoadCharRow(text) => text,
1141 _ => return None,
1142 };
1143 let name = text.trim().strip_prefix('@').unwrap_or(text.trim()).trim();
1144 if name.is_empty() {
1145 return None;
1146 }
1147 registry
1148 .resolve_name(name)
1149 .map(|function| (function, name.to_string()))
1150}
1151
1152fn bind_optional_end_exprs(
1153 exprs: &mut [Option<runmat_vm::EndExpr>],
1154 registry: &runmat_vm::FunctionRegistry,
1155) {
1156 for expr in exprs.iter_mut().flatten() {
1157 bind_semantic_function_end_expr(expr, registry);
1158 }
1159}
1160
1161fn bind_semantic_function_end_expr(
1162 expr: &mut runmat_vm::EndExpr,
1163 registry: &runmat_vm::FunctionRegistry,
1164) {
1165 match expr {
1166 runmat_vm::EndExpr::ResolvedCall { identity, args, .. } => {
1167 if let runmat_hir::CallableIdentity::DynamicName(name) = identity {
1168 let dynamic_name = name.0.clone();
1169 if let Some(function) = registry.resolve_name(&dynamic_name) {
1170 *identity = runmat_hir::CallableIdentity::BoundFunction(function);
1171 }
1172 }
1173 for arg in args {
1174 bind_semantic_function_end_expr(arg, registry);
1175 }
1176 }
1177 runmat_vm::EndExpr::Add(lhs, rhs)
1178 | runmat_vm::EndExpr::Sub(lhs, rhs)
1179 | runmat_vm::EndExpr::Mul(lhs, rhs)
1180 | runmat_vm::EndExpr::Div(lhs, rhs)
1181 | runmat_vm::EndExpr::LeftDiv(lhs, rhs)
1182 | runmat_vm::EndExpr::Pow(lhs, rhs) => {
1183 bind_semantic_function_end_expr(lhs, registry);
1184 bind_semantic_function_end_expr(rhs, registry);
1185 }
1186 runmat_vm::EndExpr::Neg(inner)
1187 | runmat_vm::EndExpr::Pos(inner)
1188 | runmat_vm::EndExpr::Floor(inner)
1189 | runmat_vm::EndExpr::Ceil(inner)
1190 | runmat_vm::EndExpr::Round(inner)
1191 | runmat_vm::EndExpr::Fix(inner) => bind_semantic_function_end_expr(inner, registry),
1192 runmat_vm::EndExpr::End | runmat_vm::EndExpr::Const(_) | runmat_vm::EndExpr::Var(_) => {}
1193 }
1194}
1195
1196fn remap_optional_end_exprs(
1197 exprs: &mut [Option<runmat_vm::EndExpr>],
1198 remap: &HashMap<runmat_hir::FunctionId, runmat_hir::FunctionId>,
1199) {
1200 for expr in exprs.iter_mut().flatten() {
1201 remap_semantic_function_end_expr(expr, remap);
1202 }
1203}
1204
1205fn remap_semantic_function_end_expr(
1206 expr: &mut runmat_vm::EndExpr,
1207 remap: &HashMap<runmat_hir::FunctionId, runmat_hir::FunctionId>,
1208) {
1209 match expr {
1210 runmat_vm::EndExpr::ResolvedCall { identity, args, .. } => {
1211 match identity {
1212 runmat_hir::CallableIdentity::BoundFunction(function)
1213 | runmat_hir::CallableIdentity::AnonymousFunction(function) => {
1214 if let Some(new_id) = remap.get(function).copied() {
1215 *function = new_id;
1216 }
1217 }
1218 _ => {}
1219 }
1220 for arg in args {
1221 remap_semantic_function_end_expr(arg, remap);
1222 }
1223 }
1224 runmat_vm::EndExpr::Add(lhs, rhs)
1225 | runmat_vm::EndExpr::Sub(lhs, rhs)
1226 | runmat_vm::EndExpr::Mul(lhs, rhs)
1227 | runmat_vm::EndExpr::Div(lhs, rhs)
1228 | runmat_vm::EndExpr::LeftDiv(lhs, rhs)
1229 | runmat_vm::EndExpr::Pow(lhs, rhs) => {
1230 remap_semantic_function_end_expr(lhs, remap);
1231 remap_semantic_function_end_expr(rhs, remap);
1232 }
1233 runmat_vm::EndExpr::Neg(inner)
1234 | runmat_vm::EndExpr::Pos(inner)
1235 | runmat_vm::EndExpr::Floor(inner)
1236 | runmat_vm::EndExpr::Ceil(inner)
1237 | runmat_vm::EndExpr::Round(inner)
1238 | runmat_vm::EndExpr::Fix(inner) => remap_semantic_function_end_expr(inner, remap),
1239 runmat_vm::EndExpr::End | runmat_vm::EndExpr::Const(_) | runmat_vm::EndExpr::Var(_) => {}
1240 }
1241}
1242
1243#[cfg(test)]
1244mod source_discovery_tests {
1245 use super::*;
1246 use std::fs;
1247
1248 fn discovered_function_names(discovery: &CompanionSourceDiscovery) -> HashSet<String> {
1249 discovery
1250 .statements
1251 .iter()
1252 .filter_map(|statement| match statement {
1253 runmat_parser::Stmt::Function { name, .. } => Some(name.clone()),
1254 _ => None,
1255 })
1256 .collect()
1257 }
1258
1259 #[test]
1260 fn loose_runtime_companions_use_the_shared_matlab_source_index() {
1261 let _provider_guard = runmat_filesystem::provider_override_lock();
1262 let temp = tempfile::TempDir::new().unwrap();
1263 fs::create_dir_all(temp.path().join("+pkg")).unwrap();
1264 fs::create_dir_all(temp.path().join("private")).unwrap();
1265 fs::create_dir_all(temp.path().join("ordinary-nested")).unwrap();
1266 let main = temp.path().join("main.m");
1267 fs::write(&main, "value = helper(1);").unwrap();
1268 fs::write(
1269 temp.path().join("helper.m"),
1270 "function y = helper(x)\ny = x;\nend",
1271 )
1272 .unwrap();
1273 fs::write(
1274 temp.path().join("+pkg/tool.m"),
1275 "function y = tool(x)\ny = x;\nend",
1276 )
1277 .unwrap();
1278 fs::write(
1279 temp.path().join("private/secret.m"),
1280 "function y = secret(x)\ny = x;\nend",
1281 )
1282 .unwrap();
1283 fs::write(
1284 temp.path().join("ordinary-nested/hidden.m"),
1285 "function y = hidden(x)\ny = x;\nend",
1286 )
1287 .unwrap();
1288
1289 let discovery = futures::executor::block_on(discover_companion_source_statements_async(
1290 main.to_str().unwrap(),
1291 runmat_parser::CompatMode::RunMat,
1292 ))
1293 .expect("discover loose companion sources");
1294 let names = discovered_function_names(&discovery);
1295 assert!(names.contains("helper"));
1296 assert!(names.contains("pkg.tool"));
1297 assert!(names.contains("secret"));
1298 assert!(!names.iter().any(|name| name.ends_with("hidden")));
1299 }
1300}