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}