Skip to main content

runmat_core/session/
compile.rs

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        // Text/REPL source labels are not paths and must not inherit whatever
526        // manifest happens to be visible through the process-global cwd. Path
527        // executions resolve to a real primary file before reaching here.
528        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    /// Compile the input and produce a fusion plan snapshot without executing.
867    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        // Create a new variable array of the correct size
916        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        // Populate with existing values based on the variable mapping
927        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        // Update our variable array and mapping
951        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, &registry);
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, &registry);
1030                bind_optional_end_exprs(range_step_exprs, &registry);
1031                for expr in range_end_exprs {
1032                    bind_semantic_function_end_expr(expr, &registry);
1033                }
1034                for (_, expr) in end_numeric_exprs {
1035                    bind_semantic_function_end_expr(expr, &registry);
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}