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}