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