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