Skip to main content

runmat_vm/bytecode/
program.rs

1#[cfg(feature = "native-accel")]
2use crate::accel::graph::build_accel_graph;
3#[cfg(feature = "native-accel")]
4use crate::accel::stack_layout::annotate_fusion_groups_with_stack_layout;
5use crate::bytecode::instr::Instr;
6use crate::layout::VmAssemblyLayout;
7#[cfg(feature = "native-accel")]
8use runmat_accelerate::graph::AccelGraph;
9#[cfg(feature = "native-accel")]
10use runmat_accelerate::FusionGroup;
11use runmat_builtins::{Type, Value};
12use runmat_hir::FunctionId;
13use serde::{Deserialize, Serialize};
14use std::collections::{HashMap, HashSet};
15
16#[derive(Debug, Clone)]
17pub struct CallFrame {
18    pub function_name: String,
19    pub return_address: usize,
20    pub locals_start: usize,
21    pub locals_count: usize,
22    pub expected_outputs: usize,
23}
24
25#[derive(Debug)]
26pub struct ExecutionContext {
27    pub call_stack: Vec<CallFrame>,
28    pub locals: Vec<Value>,
29    pub instruction_pointer: usize,
30    pub spawned_task_ids: HashSet<u64>,
31    pub next_spawn_task_id: u64,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct FunctionBytecode {
36    pub function: FunctionId,
37    pub display_name: String,
38    #[serde(default)]
39    pub private_owner_scope: String,
40    #[serde(default)]
41    pub source_id: Option<runmat_hir::SourceId>,
42    pub instructions: Vec<Instr>,
43    #[serde(default)]
44    pub instr_spans: Vec<runmat_hir::Span>,
45    #[serde(default)]
46    pub call_arg_spans: Vec<Option<Vec<runmat_hir::Span>>>,
47    pub var_count: usize,
48    pub input_slots: Vec<usize>,
49    #[serde(default)]
50    pub varargin_slot: Option<usize>,
51    #[serde(default)]
52    pub implicit_nargin_slot: Option<usize>,
53    pub output_slots: Vec<usize>,
54    #[serde(default)]
55    pub varargout_slot: Option<usize>,
56    #[serde(default)]
57    pub implicit_nargout_slot: Option<usize>,
58    pub capture_slots: Vec<usize>,
59    #[serde(default)]
60    pub var_names: HashMap<usize, String>,
61    #[serde(default)]
62    pub argument_validations: Vec<FunctionArgumentValidation>,
63}
64
65impl Default for FunctionBytecode {
66    fn default() -> Self {
67        Self {
68            function: FunctionId(0),
69            display_name: String::new(),
70            private_owner_scope: String::new(),
71            source_id: None,
72            instructions: Vec::new(),
73            instr_spans: Vec::new(),
74            call_arg_spans: Vec::new(),
75            var_count: 0,
76            input_slots: Vec::new(),
77            varargin_slot: None,
78            implicit_nargin_slot: None,
79            output_slots: Vec::new(),
80            varargout_slot: None,
81            implicit_nargout_slot: None,
82            capture_slots: Vec::new(),
83            var_names: HashMap::new(),
84            argument_validations: Vec::new(),
85        }
86    }
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub enum FunctionArgDim {
91    Any,
92    Exact(usize),
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
96pub struct FunctionArgSizeSpec {
97    pub rows: FunctionArgDim,
98    pub cols: FunctionArgDim,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct FunctionArgumentValidation {
103    pub input_slot: usize,
104    pub size: Option<FunctionArgSizeSpec>,
105    pub class_name: Option<String>,
106    #[serde(default)]
107    pub validators: Vec<FunctionArgValidator>,
108    #[serde(default)]
109    pub default_value: Option<FunctionArgDefaultValue>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub enum FunctionArgValidator {
114    Finite,
115    NumericOrLogical,
116    Text,
117    Nonempty,
118    ScalarOrEmpty,
119    Real,
120    Integer,
121    Positive,
122    Negative,
123    Nonnegative,
124    Nonzero,
125    Nonpositive,
126    GreaterThanOrEqual(f64),
127    LessThanOrEqual(f64),
128    GreaterThan(f64),
129    LessThan(f64),
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub enum FunctionArgDefaultValue {
134    Number(f64),
135    Bool(bool),
136    String(String),
137    EmptyArray,
138}
139
140#[derive(Debug, Clone, Default, Serialize, Deserialize)]
141pub struct FunctionRegistry {
142    pub functions: HashMap<FunctionId, FunctionBytecode>,
143    #[serde(default)]
144    pub names: HashMap<String, FunctionId>,
145    #[serde(default)]
146    pub source_functions: HashMap<runmat_hir::SourceId, Vec<FunctionId>>,
147}
148
149impl FunctionRegistry {
150    pub fn new(functions: HashMap<FunctionId, FunctionBytecode>) -> Self {
151        let mut names = HashMap::new();
152        let mut source_functions: HashMap<runmat_hir::SourceId, Vec<FunctionId>> = HashMap::new();
153        let mut ids: Vec<_> = functions.keys().copied().collect();
154        ids.sort_by_key(|id| id.0);
155        for id in ids {
156            if let Some(function) = functions.get(&id) {
157                names.entry(function.display_name.clone()).or_insert(id);
158                if let Some(source_id) = function.source_id {
159                    source_functions.entry(source_id).or_default().push(id);
160                }
161            }
162        }
163        Self {
164            functions,
165            names,
166            source_functions,
167        }
168    }
169
170    pub fn get(&self, function: FunctionId) -> Option<&FunctionBytecode> {
171        self.functions.get(&function)
172    }
173
174    pub fn resolve_name(&self, name: &str) -> Option<FunctionId> {
175        self.names.get(name).copied()
176    }
177
178    pub fn resolve_name_in_private_scope(
179        &self,
180        private_owner_scope: &str,
181        name: &str,
182    ) -> Option<FunctionId> {
183        if private_owner_scope.is_empty() || name.contains('.') {
184            return None;
185        }
186        let scoped_name = format!("{private_owner_scope}.__private__.{name}");
187        self.names.get(&scoped_name).copied()
188    }
189
190    pub fn insert_replacing_name(&mut self, function: FunctionBytecode) {
191        if let Some(previous) = self
192            .names
193            .insert(function.display_name.clone(), function.function)
194        {
195            self.remove(previous);
196        }
197        let function_id = function.function;
198        if let Some(source_id) = function.source_id {
199            let functions = self.source_functions.entry(source_id).or_default();
200            if !functions.contains(&function_id) {
201                functions.push(function_id);
202            }
203        }
204        self.functions.insert(function_id, function);
205    }
206
207    pub fn remove(&mut self, function: FunctionId) -> Option<FunctionBytecode> {
208        let removed = self.functions.remove(&function)?;
209        if self.names.get(&removed.display_name) == Some(&function) {
210            self.names.remove(&removed.display_name);
211        }
212        if let Some(source_id) = removed.source_id {
213            if let Some(functions) = self.source_functions.get_mut(&source_id) {
214                functions.retain(|id| *id != function);
215                if functions.is_empty() {
216                    self.source_functions.remove(&source_id);
217                }
218            }
219        }
220        Some(removed)
221    }
222
223    pub fn remove_source(&mut self, source: runmat_hir::SourceId) -> Vec<FunctionBytecode> {
224        let ids = self.source_functions.remove(&source).unwrap_or_default();
225        let mut removed = Vec::new();
226        for id in ids {
227            if let Some(function) = self.functions.remove(&id) {
228                if self.names.get(&function.display_name) == Some(&id) {
229                    self.names.remove(&function.display_name);
230                }
231                removed.push(function);
232            }
233        }
234        removed
235    }
236
237    pub fn functions_for_source(&self, source: runmat_hir::SourceId) -> &[FunctionId] {
238        self.source_functions
239            .get(&source)
240            .map(Vec::as_slice)
241            .unwrap_or(&[])
242    }
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct Bytecode {
247    pub instructions: Vec<Instr>,
248    #[serde(default)]
249    pub instr_spans: Vec<runmat_hir::Span>,
250    #[serde(default)]
251    pub call_arg_spans: Vec<Option<Vec<runmat_hir::Span>>>,
252    #[serde(default)]
253    pub source_id: Option<runmat_hir::SourceId>,
254    pub var_count: usize,
255    #[serde(default)]
256    pub bound_functions: HashMap<FunctionId, FunctionBytecode>,
257    #[serde(default)]
258    pub function_registry: FunctionRegistry,
259    #[serde(default)]
260    pub var_types: Vec<Type>,
261    #[serde(default)]
262    pub var_names: HashMap<usize, String>,
263    #[serde(default)]
264    pub layout: Option<VmAssemblyLayout>,
265    #[serde(default)]
266    pub async_metadata: AsyncMetadata,
267    #[cfg(feature = "native-accel")]
268    #[serde(default)]
269    pub accel_graph: Option<AccelGraph>,
270    #[cfg(feature = "native-accel")]
271    #[serde(default)]
272    pub fusion_groups: Vec<FusionGroup>,
273    #[cfg(feature = "native-accel")]
274    #[serde(default)]
275    pub fusion_metadata: FusionMetadata,
276}
277
278#[derive(Debug, Clone, Default, Serialize, Deserialize)]
279pub struct AsyncMetadata {
280    pub mir_spawn_site_count: usize,
281    pub mir_spawn_sites: Vec<SpawnSite>,
282    pub mir_await_site_count: usize,
283    pub mir_await_sites: Vec<AwaitSite>,
284    #[serde(default)]
285    pub runtime_model: AsyncRuntimeModel,
286}
287
288#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
289pub enum AsyncRuntimeModel {
290    LazyFutureDescriptorLane,
291}
292
293impl Default for AsyncRuntimeModel {
294    fn default() -> Self {
295        Self::LazyFutureDescriptorLane
296    }
297}
298
299impl AsyncRuntimeModel {
300    pub fn as_str(self) -> &'static str {
301        match self {
302            Self::LazyFutureDescriptorLane => "lazy_future_descriptor_lane",
303        }
304    }
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct SpawnSite {
309    pub function: runmat_hir::FunctionId,
310    pub block: runmat_mir::BasicBlockId,
311    pub stmt_index: usize,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct AwaitSite {
316    pub function: runmat_hir::FunctionId,
317    pub block: runmat_mir::BasicBlockId,
318    pub resume: runmat_mir::BasicBlockId,
319}
320
321#[cfg(feature = "native-accel")]
322#[derive(Debug, Clone, Default, Serialize, Deserialize)]
323pub struct FusionMetadata {
324    pub mir_fusion_signal_count: usize,
325    pub mir_fusion_candidate_group_count: usize,
326    pub mir_fusion_candidate_groups: Vec<FusionCandidateGroup>,
327    pub instruction_window_count: usize,
328    #[serde(default)]
329    pub instruction_windows: Vec<FusionInstructionWindow>,
330}
331
332#[cfg(feature = "native-accel")]
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct FusionCandidateGroup {
335    pub id: usize,
336    pub signal_count: usize,
337    pub function: runmat_hir::FunctionId,
338    pub block: runmat_mir::BasicBlockId,
339    pub stmt_start: usize,
340    pub stmt_end: usize,
341    #[serde(default)]
342    pub source_span: runmat_hir::Span,
343}
344
345#[cfg(feature = "native-accel")]
346#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
347pub enum FusionInstructionKind {
348    Elementwise,
349    Reduction,
350    Matmul,
351}
352
353#[cfg(feature = "native-accel")]
354#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
355pub struct FusionInstructionWindow {
356    pub span: runmat_accelerate::graph::InstrSpan,
357    pub kind: FusionInstructionKind,
358}
359
360#[cfg(feature = "native-accel")]
361#[derive(Debug, Clone, Copy, PartialEq, Eq)]
362pub enum RuntimeAccelGraphSource {
363    NotMaterialized,
364    RuntimeMaterializedFromInstructions,
365}
366
367#[cfg(feature = "native-accel")]
368impl RuntimeAccelGraphSource {
369    pub fn as_str(self) -> &'static str {
370        match self {
371            Self::NotMaterialized => "not_materialized",
372            Self::RuntimeMaterializedFromInstructions => "runtime_materialized_from_instructions",
373        }
374    }
375}
376
377impl Bytecode {
378    pub fn empty() -> Self {
379        Self {
380            instructions: Vec::new(),
381            instr_spans: Vec::new(),
382            call_arg_spans: Vec::new(),
383            source_id: None,
384            var_count: 0,
385            bound_functions: HashMap::new(),
386            function_registry: FunctionRegistry::default(),
387            var_types: Vec::new(),
388            var_names: HashMap::new(),
389            layout: None,
390            async_metadata: AsyncMetadata::default(),
391            #[cfg(feature = "native-accel")]
392            accel_graph: None,
393            #[cfg(feature = "native-accel")]
394            fusion_groups: Vec::new(),
395            #[cfg(feature = "native-accel")]
396            fusion_metadata: FusionMetadata::default(),
397        }
398    }
399
400    pub fn with_instructions(instructions: Vec<Instr>, var_count: usize) -> Self {
401        let instr_spans = vec![runmat_hir::Span::default(); instructions.len()];
402        let call_arg_spans = vec![None; instructions.len()];
403        Self {
404            instructions,
405            instr_spans,
406            call_arg_spans,
407            var_count,
408            ..Self::empty()
409        }
410    }
411
412    pub fn function_registry(&self) -> FunctionRegistry {
413        if self.function_registry.functions.is_empty() && !self.bound_functions.is_empty() {
414            return FunctionRegistry::new(self.bound_functions.clone());
415        }
416        self.function_registry.clone()
417    }
418
419    #[cfg(feature = "native-accel")]
420    pub fn runtime_fusion_groups(&self) -> Vec<FusionGroup> {
421        let metadata_present = self.fusion_metadata.mir_fusion_signal_count > 0
422            || self.fusion_metadata.mir_fusion_candidate_group_count > 0
423            || !self.fusion_metadata.mir_fusion_candidate_groups.is_empty()
424            || self.fusion_metadata.instruction_window_count > 0
425            || !self.fusion_metadata.instruction_windows.is_empty();
426
427        if !metadata_present {
428            return self.fusion_groups.clone();
429        }
430
431        if self.fusion_metadata.mir_fusion_candidate_group_count == 0
432            || self.fusion_metadata.instruction_windows.is_empty()
433        {
434            return Vec::new();
435        }
436        self.fusion_metadata
437            .instruction_windows
438            .iter()
439            .enumerate()
440            .map(|(id, window)| FusionGroup {
441                id,
442                kind: match window.kind {
443                    FusionInstructionKind::Elementwise => {
444                        runmat_accelerate::fusion::FusionKind::ElementwiseChain
445                    }
446                    FusionInstructionKind::Reduction => {
447                        runmat_accelerate::fusion::FusionKind::Reduction
448                    }
449                    FusionInstructionKind::Matmul => {
450                        runmat_accelerate::fusion::FusionKind::MatmulEpilogue
451                    }
452                },
453                nodes: Vec::new(),
454                shape: runmat_accelerate::graph::ShapeInfo::Unknown,
455                span: window.span.clone(),
456                pattern: None,
457                stack_layout: None,
458            })
459            .collect()
460    }
461
462    #[cfg(feature = "native-accel")]
463    pub fn runtime_fusion_groups_for_graph(&self, graph: &AccelGraph) -> Vec<FusionGroup> {
464        let mut groups = self.runtime_fusion_groups();
465        if groups.is_empty() {
466            return groups;
467        }
468        if groups.iter().any(|group| group.stack_layout.is_none()) {
469            annotate_fusion_groups_with_stack_layout(&self.instructions, graph, &mut groups);
470        }
471        groups
472    }
473
474    #[cfg(feature = "native-accel")]
475    pub fn runtime_accel_graph_for_fusion(
476        &self,
477        runtime_groups: &[FusionGroup],
478    ) -> Option<AccelGraph> {
479        self.runtime_accel_graph_for_fusion_with_source(runtime_groups)
480            .0
481    }
482
483    #[cfg(feature = "native-accel")]
484    pub fn runtime_accel_graph_for_fusion_with_source(
485        &self,
486        runtime_groups: &[FusionGroup],
487    ) -> (Option<AccelGraph>, RuntimeAccelGraphSource) {
488        if runtime_groups.is_empty() || self.fusion_metadata.mir_fusion_candidate_group_count == 0 {
489            return (None, RuntimeAccelGraphSource::NotMaterialized);
490        }
491        (
492            Some(build_accel_graph(&self.instructions, &self.var_types)),
493            RuntimeAccelGraphSource::RuntimeMaterializedFromInstructions,
494        )
495    }
496}
497
498#[cfg(test)]
499mod function_registry_tests {
500    use super::{FunctionBytecode, FunctionRegistry};
501    use crate::Instr;
502    use runmat_hir::FunctionId;
503    use std::collections::HashMap;
504
505    fn test_function(id: usize, display_name: &str, private_owner_scope: &str) -> FunctionBytecode {
506        FunctionBytecode {
507            function: FunctionId(id),
508            display_name: display_name.to_string(),
509            private_owner_scope: private_owner_scope.to_string(),
510            source_id: None,
511            instructions: vec![Instr::Return],
512            instr_spans: Vec::new(),
513            call_arg_spans: Vec::new(),
514            var_count: 0,
515            input_slots: Vec::new(),
516            varargin_slot: None,
517            implicit_nargin_slot: None,
518            output_slots: Vec::new(),
519            varargout_slot: None,
520            implicit_nargout_slot: None,
521            capture_slots: Vec::new(),
522            var_names: HashMap::new(),
523            argument_validations: Vec::new(),
524        }
525    }
526
527    #[test]
528    fn function_registry_resolves_private_name_in_owner_scope() {
529        let mut functions = HashMap::new();
530        functions.insert(FunctionId(1), test_function(1, "helper", ""));
531        functions.insert(FunctionId(2), test_function(2, "C.__private__.helper", "C"));
532        let registry = FunctionRegistry::new(functions);
533
534        assert_eq!(
535            registry.resolve_name("helper"),
536            Some(FunctionId(1)),
537            "unscoped lookup should keep ordinary name resolution"
538        );
539        assert_eq!(
540            registry.resolve_name_in_private_scope("C", "helper"),
541            Some(FunctionId(2)),
542            "class owner scope should prefer its synthetic private helper"
543        );
544        assert_eq!(
545            registry.resolve_name_in_private_scope("", "helper"),
546            None,
547            "empty owner scope should not expose synthetic private helpers"
548        );
549        assert_eq!(
550            registry.resolve_name_in_private_scope("C", "pkg.helper"),
551            None,
552            "qualified names should not be rewritten as private-folder aliases"
553        );
554    }
555}
556
557#[cfg(all(test, feature = "native-accel"))]
558mod tests {
559    use super::{Bytecode, FusionInstructionKind, FusionInstructionWindow};
560    use runmat_accelerate::graph::InstrSpan;
561    use runmat_accelerate::graph::{AccelNodeLabel, PrimitiveOp};
562
563    #[test]
564    fn runtime_fusion_groups_fallback_to_semantic_windows_when_bytecode_groups_are_empty() {
565        let mut bytecode = Bytecode::empty();
566        bytecode.fusion_metadata.mir_fusion_candidate_group_count = 1;
567        bytecode.fusion_metadata.instruction_windows = vec![FusionInstructionWindow {
568            span: InstrSpan { start: 2, end: 4 },
569            kind: FusionInstructionKind::Elementwise,
570        }];
571
572        let groups = bytecode.runtime_fusion_groups();
573        assert_eq!(groups.len(), 1);
574        assert_eq!(groups[0].span.start, 2);
575        assert_eq!(groups[0].span.end, 4);
576        assert!(groups[0].nodes.is_empty());
577        assert_eq!(
578            groups[0].kind,
579            runmat_accelerate::fusion::FusionKind::ElementwiseChain
580        );
581    }
582
583    #[test]
584    fn runtime_fusion_groups_use_semantic_windows_when_metadata_is_present() {
585        let mut bytecode = Bytecode::empty();
586        bytecode.fusion_groups = vec![runmat_accelerate::fusion::FusionGroup {
587            id: 7,
588            kind: runmat_accelerate::fusion::FusionKind::ElementwiseChain,
589            nodes: vec![1],
590            shape: runmat_accelerate::graph::ShapeInfo::Unknown,
591            span: InstrSpan { start: 5, end: 5 },
592            pattern: None,
593            stack_layout: None,
594        }];
595        bytecode.fusion_metadata.mir_fusion_candidate_group_count = 1;
596        bytecode.fusion_metadata.instruction_windows = vec![FusionInstructionWindow {
597            span: InstrSpan { start: 10, end: 20 },
598            kind: FusionInstructionKind::Elementwise,
599        }];
600
601        let groups = bytecode.runtime_fusion_groups();
602        assert_eq!(groups.len(), 1);
603        assert_eq!(groups[0].id, 0);
604        assert!(groups[0].nodes.is_empty());
605        assert_eq!(groups[0].span.start, 10);
606        assert_eq!(groups[0].span.end, 20);
607    }
608
609    #[test]
610    fn runtime_fusion_groups_ignore_stale_compile_groups_when_semantic_candidates_are_empty() {
611        let mut bytecode = Bytecode::empty();
612        bytecode.fusion_groups = vec![runmat_accelerate::fusion::FusionGroup {
613            id: 7,
614            kind: runmat_accelerate::fusion::FusionKind::ElementwiseChain,
615            nodes: vec![1],
616            shape: runmat_accelerate::graph::ShapeInfo::Unknown,
617            span: InstrSpan { start: 5, end: 5 },
618            pattern: None,
619            stack_layout: None,
620        }];
621        bytecode.fusion_metadata.mir_fusion_signal_count = 2;
622        bytecode.fusion_metadata.mir_fusion_candidate_group_count = 0;
623
624        let groups = bytecode.runtime_fusion_groups();
625        assert!(
626            groups.is_empty(),
627            "semantic metadata should gate runtime fusion groups when no candidates exist"
628        );
629    }
630
631    #[test]
632    fn runtime_fusion_groups_fallback_to_existing_bytecode_groups_without_semantic_metadata() {
633        let mut bytecode = Bytecode::empty();
634        bytecode.fusion_groups = vec![runmat_accelerate::fusion::FusionGroup {
635            id: 7,
636            kind: runmat_accelerate::fusion::FusionKind::ElementwiseChain,
637            nodes: vec![1],
638            shape: runmat_accelerate::graph::ShapeInfo::Unknown,
639            span: InstrSpan { start: 5, end: 5 },
640            pattern: None,
641            stack_layout: None,
642        }];
643
644        let groups = bytecode.runtime_fusion_groups();
645        assert_eq!(groups.len(), 1);
646        assert_eq!(groups[0].id, 7);
647        assert_eq!(groups[0].nodes, vec![1]);
648        assert_eq!(groups[0].span.start, 5);
649        assert_eq!(groups[0].span.end, 5);
650    }
651
652    #[test]
653    fn runtime_accel_graph_materializes_when_semantic_groups_exist_and_compile_graph_is_missing() {
654        let mut bytecode = Bytecode::empty();
655        bytecode.instructions = vec![crate::Instr::Add];
656        bytecode.var_types = vec![
657            runmat_builtins::Type::Num,
658            runmat_builtins::Type::Num,
659            runmat_builtins::Type::Num,
660        ];
661        bytecode.fusion_metadata.mir_fusion_candidate_group_count = 1;
662        bytecode.fusion_metadata.instruction_windows = vec![FusionInstructionWindow {
663            span: InstrSpan { start: 0, end: 0 },
664            kind: FusionInstructionKind::Elementwise,
665        }];
666
667        let runtime_groups = bytecode.runtime_fusion_groups();
668        let (graph, source) = bytecode.runtime_accel_graph_for_fusion_with_source(&runtime_groups);
669        assert!(
670            graph.is_some(),
671            "runtime graph should be materialized when semantic runtime groups exist and compile graph is missing"
672        );
673        assert_eq!(
674            source,
675            super::RuntimeAccelGraphSource::RuntimeMaterializedFromInstructions
676        );
677    }
678
679    #[test]
680    fn runtime_accel_graph_materializes_when_semantic_groups_exist_and_compile_graph_is_present() {
681        let mut bytecode = Bytecode::empty();
682        bytecode.instructions = vec![
683            crate::Instr::LoadVar(0),
684            crate::Instr::LoadVar(1),
685            crate::Instr::Add,
686        ];
687        bytecode.var_types = vec![
688            runmat_builtins::Type::Num,
689            runmat_builtins::Type::Num,
690            runmat_builtins::Type::Num,
691        ];
692        bytecode.accel_graph = Some(crate::accel::graph::build_accel_graph(
693            &bytecode.instructions,
694            &bytecode.var_types,
695        ));
696        bytecode.fusion_metadata.mir_fusion_candidate_group_count = 1;
697        bytecode.fusion_metadata.instruction_windows = vec![FusionInstructionWindow {
698            span: InstrSpan { start: 2, end: 2 },
699            kind: FusionInstructionKind::Elementwise,
700        }];
701
702        let runtime_groups = bytecode.runtime_fusion_groups();
703        let (graph, source) = bytecode.runtime_accel_graph_for_fusion_with_source(&runtime_groups);
704        assert!(
705            graph.is_some(),
706            "runtime graph should still be materialized when compile graph metadata is present"
707        );
708        assert_eq!(
709            source,
710            super::RuntimeAccelGraphSource::RuntimeMaterializedFromInstructions
711        );
712    }
713
714    #[test]
715    fn runtime_accel_graph_ignores_stale_compile_graph_metadata() {
716        let mut bytecode = Bytecode::empty();
717        bytecode.instructions = vec![
718            crate::Instr::LoadVar(0),
719            crate::Instr::LoadVar(1),
720            crate::Instr::Add,
721        ];
722        bytecode.var_types = vec![
723            runmat_builtins::Type::Num,
724            runmat_builtins::Type::Num,
725            runmat_builtins::Type::Num,
726        ];
727
728        let stale_graph = crate::accel::graph::build_accel_graph(
729            &[
730                crate::Instr::LoadVar(0),
731                crate::Instr::LoadVar(1),
732                crate::Instr::Mul,
733            ],
734            &bytecode.var_types,
735        );
736        bytecode.accel_graph = Some(stale_graph);
737        bytecode.fusion_metadata.mir_fusion_candidate_group_count = 1;
738        bytecode.fusion_metadata.instruction_windows = vec![FusionInstructionWindow {
739            span: InstrSpan { start: 2, end: 2 },
740            kind: FusionInstructionKind::Elementwise,
741        }];
742
743        let runtime_groups = bytecode.runtime_fusion_groups();
744        let (graph, source) = bytecode.runtime_accel_graph_for_fusion_with_source(&runtime_groups);
745        let graph =
746            graph.expect("runtime graph should be materialized from active bytecode instructions");
747        assert!(
748            graph
749                .nodes
750                .iter()
751                .any(|node| matches!(node.label, AccelNodeLabel::Primitive(PrimitiveOp::Add))),
752            "runtime graph should reflect active bytecode instructions"
753        );
754        assert!(
755            !graph
756                .nodes
757                .iter()
758                .any(|node| matches!(node.label, AccelNodeLabel::Primitive(PrimitiveOp::Mul))),
759            "stale compile graph metadata should not be reused at runtime"
760        );
761        assert_eq!(
762            source,
763            super::RuntimeAccelGraphSource::RuntimeMaterializedFromInstructions
764        );
765    }
766
767    #[test]
768    fn runtime_accel_graph_is_not_materialized_when_runtime_groups_are_empty() {
769        let bytecode = Bytecode::empty();
770        let (graph, source) = bytecode.runtime_accel_graph_for_fusion_with_source(&[]);
771        assert!(
772            graph.is_none(),
773            "runtime graph materialization should remain gated when semantic runtime groups are absent"
774        );
775        assert_eq!(source, super::RuntimeAccelGraphSource::NotMaterialized);
776    }
777}