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_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    /// Compile the input and produce a fusion plan snapshot without executing.
893    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        // Create a new variable array of the correct size
942        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        // Populate with existing values based on the variable mapping
953        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        // Update our variable array and mapping
977        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, &registry);
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, &registry);
1056                bind_optional_end_exprs(range_step_exprs, &registry);
1057                for expr in range_end_exprs {
1058                    bind_semantic_function_end_expr(expr, &registry);
1059                }
1060                for (_, expr) in end_numeric_exprs {
1061                    bind_semantic_function_end_expr(expr, &registry);
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}