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