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