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}