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;
5#[cfg(feature = "native-accel")]
6use crate::bytecode::instr::Instr;
7use crate::bytecode::program::FunctionBytecode;
8use crate::bytecode::{Bytecode, FunctionRegistry};
9use crate::compiler::{CompileError, Compiler};
10use crate::layout::derive_layout;
11#[cfg(feature = "native-accel")]
12use runmat_builtins::{builtin_functions, AccelTag, BuiltinSemanticKind};
13use runmat_hir::{EntrypointId, FunctionId, HirAssembly};
14use runmat_mir::MirAssembly;
15use runmat_mir::{MirRvalue, MirStmtKind, MirTerminatorKind};
16use std::collections::{HashMap, HashSet};
17
18pub fn compile(
19 hir: &HirAssembly,
20 mir: &MirAssembly,
21 entrypoint: EntrypointId,
22) -> Result<Bytecode, CompileError> {
23 let layout = derive_layout(hir, mir)
24 .map_err(|err| CompileError::new(format!("failed to derive VM layout: {err:?}")))?;
25 let mut c = Compiler::new(hir, mir, layout, entrypoint)?;
26 c.compile()?;
27 let bound_functions =
28 compile_semantic_functions(hir, mir, c.layout.as_ref().unwrap(), Some(entrypoint))?;
29 let function_registry = FunctionRegistry::new(bound_functions.clone());
30 let (var_names, initially_unassigned_slots) = c
31 .layout
32 .as_ref()
33 .and_then(|layout| {
34 let entrypoint_layout = layout.entrypoints.get(&entrypoint)?;
35 let function_layout = layout.functions.get(&entrypoint_layout.target)?;
36 Some((
37 entrypoint_layout
38 .exports
39 .iter()
40 .map(|export| (export.slot.0, export.name.clone()))
41 .collect(),
42 function_layout_initially_unassigned_slots(hir, function_layout),
43 ))
44 })
45 .unwrap_or_default();
46 let entrypoint_target = hir
47 .entrypoints
48 .iter()
49 .find(|candidate| candidate.id == entrypoint)
50 .map(|candidate| candidate.target);
51 #[cfg(feature = "native-accel")]
52 let mut fusion_metadata = derive_semantic_fusion_metadata(mir, entrypoint_target);
53 let instruction_windows = derive_semantic_fusion_instruction_windows(
54 &c.instructions,
55 &c.instr_spans,
56 &fusion_metadata.mir_fusion_candidate_groups,
57 );
58 fusion_metadata.instruction_window_count = instruction_windows.len();
59 fusion_metadata.instruction_windows = instruction_windows.clone();
60 #[cfg(feature = "native-accel")]
61 let (accel_graph, fusion_groups) = if fusion_metadata.mir_fusion_candidate_group_count == 0
62 || instruction_windows.is_empty()
63 {
64 (None, Vec::new())
65 } else {
66 let accel_graph = build_accel_graph(&c.instructions, &c.var_types);
67 let mut fusion_groups =
70 derive_semantic_fusion_groups_from_instruction_windows(&instruction_windows);
71 if !fusion_groups.is_empty() {
72 annotate_fusion_groups_with_stack_layout(
73 &c.instructions,
74 &accel_graph,
75 &mut fusion_groups,
76 );
77 fusion_groups.retain(|group| {
78 fusion_group_within_semantic_candidate_spans(
79 group,
80 &c.instr_spans,
81 &fusion_metadata.mir_fusion_candidate_groups,
82 )
83 });
84 }
85 (Some(accel_graph), fusion_groups)
89 };
90 let async_metadata = derive_semantic_async_metadata(mir, entrypoint_target);
91
92 let source_id = entrypoint_target
93 .and_then(|function_id| {
94 hir.functions
95 .iter()
96 .find(|function| function.id == function_id)
97 })
98 .and_then(|function| hir.modules.get(function.module.0))
99 .map(|module| module.source_id);
100
101 Ok(Bytecode {
102 instructions: c.instructions,
103 instr_spans: c.instr_spans,
104 call_arg_spans: c.call_arg_spans,
105 source_id,
106 var_count: c.var_count,
107 bound_functions,
108 function_registry,
109 var_types: c.var_types,
110 var_names,
111 initially_unassigned_slots,
112 layout: c.layout,
113 async_metadata,
114 #[cfg(feature = "native-accel")]
115 accel_graph,
116 #[cfg(feature = "native-accel")]
117 fusion_groups,
118 #[cfg(feature = "native-accel")]
119 fusion_metadata,
120 })
121}
122
123fn derive_semantic_async_metadata(
124 mir: &MirAssembly,
125 entrypoint_target: Option<FunctionId>,
126) -> crate::bytecode::AsyncMetadata {
127 let mut spawn_sites = Vec::new();
128 let mut await_sites = Vec::new();
129 let mut function_ids: Vec<_> = if let Some(function) = entrypoint_target {
130 vec![function]
131 } else {
132 mir.bodies.keys().copied().collect()
133 };
134 function_ids.sort_by_key(|id| id.0);
135 for function_id in function_ids {
136 let Some(body) = mir.bodies.get(&function_id) else {
137 continue;
138 };
139 for block in &body.blocks {
140 for (stmt_index, stmt) in block.statements.iter().enumerate() {
141 let value = match &stmt.kind {
142 MirStmtKind::Assign { value, .. }
143 | MirStmtKind::MultiAssign { value, .. }
144 | MirStmtKind::Expr(value) => value,
145 MirStmtKind::PlaceMutation(_)
146 | MirStmtKind::WorkspaceEffect { .. }
147 | MirStmtKind::EnvironmentEffect(_) => continue,
148 };
149 if matches!(value, MirRvalue::Spawn(_)) {
150 spawn_sites.push(crate::bytecode::SpawnSite {
151 function: body.function,
152 block: block.id,
153 stmt_index,
154 });
155 }
156 }
157 if let MirTerminatorKind::Await { resume, .. } = &block.terminator.kind {
158 await_sites.push(crate::bytecode::AwaitSite {
159 function: body.function,
160 block: block.id,
161 resume: *resume,
162 });
163 }
164 }
165 }
166 crate::bytecode::AsyncMetadata {
167 mir_spawn_site_count: spawn_sites.len(),
168 mir_spawn_sites: spawn_sites,
169 mir_await_site_count: await_sites.len(),
170 mir_await_sites: await_sites,
171 runtime_model: crate::bytecode::program::AsyncRuntimeModel::LazyFutureDescriptorLane,
172 }
173}
174
175#[cfg(feature = "native-accel")]
176fn derive_semantic_fusion_metadata(
177 mir: &MirAssembly,
178 entrypoint_target: Option<FunctionId>,
179) -> crate::bytecode::FusionMetadata {
180 let (mir_fusion_signal_count, mir_fusion_candidate_groups) =
181 derive_semantic_fusion_candidate_groups(mir, entrypoint_target);
182 crate::bytecode::FusionMetadata {
183 mir_fusion_signal_count,
184 mir_fusion_candidate_group_count: mir_fusion_candidate_groups.len(),
185 mir_fusion_candidate_groups,
186 instruction_window_count: 0,
187 instruction_windows: Vec::new(),
188 }
189}
190
191#[cfg(feature = "native-accel")]
192fn derive_semantic_fusion_candidate_groups(
193 mir: &MirAssembly,
194 entrypoint_target: Option<FunctionId>,
195) -> (usize, Vec<crate::bytecode::FusionCandidateGroup>) {
196 let mut signal_count = 0usize;
197 let mut groups = Vec::new();
198 let mut function_ids: Vec<_> = if let Some(function) = entrypoint_target {
199 vec![function]
200 } else {
201 mir.bodies.keys().copied().collect()
202 };
203 function_ids.sort_by_key(|id| id.0);
204 for function_id in function_ids {
205 let Some(body) = mir.bodies.get(&function_id) else {
206 continue;
207 };
208 for block in &body.blocks {
209 let mut run_len = 0usize;
210 for (stmt_index, stmt) in block.statements.iter().enumerate() {
211 let value = match &stmt.kind {
212 MirStmtKind::Assign { value, .. }
213 | MirStmtKind::MultiAssign { value, .. }
214 | MirStmtKind::Expr(value) => value,
215 MirStmtKind::PlaceMutation(_)
216 | MirStmtKind::WorkspaceEffect { .. }
217 | MirStmtKind::EnvironmentEffect(_) => {
218 if run_len >= 2 {
219 let stmt_start = stmt_index - run_len;
220 let stmt_end = stmt_index;
221 groups.push(crate::bytecode::FusionCandidateGroup {
222 id: groups.len(),
223 signal_count: run_len,
224 function: body.function,
225 block: block.id,
226 stmt_start,
227 stmt_end,
228 source_span: merge_stmt_run_span(block, stmt_start, stmt_end),
229 });
230 }
231 run_len = 0;
232 continue;
233 }
234 };
235 if rvalue_has_fusion_signal(value) {
236 signal_count += 1;
237 run_len += 1;
238 } else {
239 if run_len >= 2 {
240 let stmt_start = stmt_index - run_len;
241 let stmt_end = stmt_index;
242 groups.push(crate::bytecode::FusionCandidateGroup {
243 id: groups.len(),
244 signal_count: run_len,
245 function: body.function,
246 block: block.id,
247 stmt_start,
248 stmt_end,
249 source_span: merge_stmt_run_span(block, stmt_start, stmt_end),
250 });
251 }
252 run_len = 0;
253 }
254 }
255 if run_len >= 2 {
256 let stmt_start = block.statements.len() - run_len;
257 let stmt_end = block.statements.len();
258 groups.push(crate::bytecode::FusionCandidateGroup {
259 id: groups.len(),
260 signal_count: run_len,
261 function: body.function,
262 block: block.id,
263 stmt_start,
264 stmt_end,
265 source_span: merge_stmt_run_span(block, stmt_start, stmt_end),
266 });
267 }
268 }
269 }
270 (signal_count, groups)
271}
272
273#[cfg(feature = "native-accel")]
274fn merge_stmt_run_span(
275 block: &runmat_mir::BasicBlock,
276 stmt_start: usize,
277 stmt_end: usize,
278) -> runmat_hir::Span {
279 let mut iter = block.statements[stmt_start..stmt_end]
280 .iter()
281 .map(|stmt| stmt.span);
282 let Some(first) = iter.next() else {
283 return runmat_hir::Span::default();
284 };
285 iter.fold(first, runmat_hir::merge_span)
286}
287
288#[cfg(feature = "native-accel")]
289fn source_span_contains(outer: runmat_hir::Span, inner: runmat_hir::Span) -> bool {
290 outer.start <= inner.start && inner.end <= outer.end
291}
292
293#[cfg(all(feature = "native-accel", test))]
294fn candidates_touch_accel_capable_instruction(
295 instructions: &[Instr],
296 instr_spans: &[runmat_hir::Span],
297 candidate_groups: &[crate::bytecode::FusionCandidateGroup],
298) -> bool {
299 if instructions.is_empty() || instr_spans.is_empty() || candidate_groups.is_empty() {
300 return false;
301 }
302 instructions
303 .iter()
304 .enumerate()
305 .filter(|(index, _)| *index < instr_spans.len())
306 .any(|(index, instr)| {
307 let span = instr_spans[index];
308 candidate_groups
309 .iter()
310 .any(|candidate| source_span_contains(candidate.source_span, span))
311 && instr_is_accel_capable(instr)
312 })
313}
314
315#[cfg(all(feature = "native-accel", test))]
316fn instr_is_accel_capable(instr: &Instr) -> bool {
317 match instr {
318 Instr::Add
319 | Instr::Sub
320 | Instr::Mul
321 | Instr::RightDiv
322 | Instr::LeftDiv
323 | Instr::Pow
324 | Instr::Neg
325 | Instr::UPlus
326 | Instr::Transpose
327 | Instr::ConjugateTranspose
328 | Instr::ElemMul
329 | Instr::ElemDiv
330 | Instr::ElemPow
331 | Instr::ElemLeftDiv
332 | Instr::LessEqual
333 | Instr::Less
334 | Instr::Greater
335 | Instr::GreaterEqual
336 | Instr::Equal
337 | Instr::NotEqual => true,
338 Instr::CallBuiltinMulti(name, _, _) => builtin_functions()
339 .iter()
340 .find(|func| func.name == name.as_str())
341 .map(|func| {
342 func.accel_tags.iter().any(|tag| {
343 matches!(
344 tag,
345 AccelTag::Unary
346 | AccelTag::Elementwise
347 | AccelTag::Reduction
348 | AccelTag::MatMul
349 | AccelTag::Transpose
350 )
351 })
352 })
353 .unwrap_or(false),
354 _ => false,
355 }
356}
357
358#[cfg(feature = "native-accel")]
359fn fusion_group_within_semantic_candidate_spans(
360 group: &runmat_accelerate::fusion::FusionGroup,
361 instr_spans: &[runmat_hir::Span],
362 candidate_groups: &[crate::bytecode::FusionCandidateGroup],
363) -> bool {
364 if instr_spans.is_empty()
365 || group.span.start > group.span.end
366 || group.span.start >= instr_spans.len()
367 {
368 return false;
369 }
370 let end = group.span.end.min(instr_spans.len().saturating_sub(1));
371 let candidate_spans: Vec<_> = candidate_groups
372 .iter()
373 .map(|group| group.source_span)
374 .collect();
375 candidate_spans.iter().any(|candidate| {
376 instr_spans[group.span.start..=end]
377 .iter()
378 .all(|span| source_span_contains(*candidate, *span))
379 })
380}
381
382#[cfg(all(feature = "native-accel", test))]
383fn derive_semantic_fusion_groups_from_candidates(
384 instruction_windows: &[crate::bytecode::FusionInstructionWindow],
385 accel_graph: &runmat_accelerate::graph::AccelGraph,
386) -> Vec<runmat_accelerate::fusion::FusionGroup> {
387 let mut groups = Vec::new();
388 let mut assigned_nodes = HashSet::new();
389
390 for window in instruction_windows {
391 let nodes = accel_nodes_for_instruction_window(accel_graph, window, &assigned_nodes);
392 if nodes.is_empty() {
393 continue;
394 }
395 for node_id in &nodes {
396 assigned_nodes.insert(*node_id);
397 }
398 let kind = infer_semantic_fusion_kind(window.kind);
399 groups.push(runmat_accelerate::fusion::FusionGroup {
400 id: groups.len(),
401 kind,
402 nodes,
403 shape: runmat_accelerate::graph::ShapeInfo::Unknown,
404 span: window.span.clone(),
405 pattern: None,
406 stack_layout: None,
407 });
408 }
409
410 groups
411}
412
413#[cfg(all(feature = "native-accel", test))]
414fn derive_semantic_fusion_groups_preserving_unmapped_windows(
415 instruction_windows: &[crate::bytecode::FusionInstructionWindow],
416 accel_graph: &runmat_accelerate::graph::AccelGraph,
417) -> Vec<runmat_accelerate::fusion::FusionGroup> {
418 let mut groups = Vec::new();
419 let mut assigned_nodes = HashSet::new();
420
421 for window in instruction_windows {
422 let nodes = accel_nodes_for_instruction_window(accel_graph, window, &assigned_nodes);
423 for node_id in &nodes {
424 assigned_nodes.insert(*node_id);
425 }
426 groups.push(runmat_accelerate::fusion::FusionGroup {
427 id: groups.len(),
428 kind: infer_semantic_fusion_kind(window.kind),
429 nodes,
430 shape: runmat_accelerate::graph::ShapeInfo::Unknown,
431 span: window.span.clone(),
432 pattern: None,
433 stack_layout: None,
434 });
435 }
436
437 groups
438}
439
440#[cfg(feature = "native-accel")]
441fn derive_semantic_fusion_groups_from_instruction_windows(
442 instruction_windows: &[crate::bytecode::FusionInstructionWindow],
443) -> Vec<runmat_accelerate::fusion::FusionGroup> {
444 instruction_windows
445 .iter()
446 .enumerate()
447 .map(|(id, window)| runmat_accelerate::fusion::FusionGroup {
448 id,
449 kind: infer_semantic_fusion_kind(window.kind),
450 nodes: Vec::new(),
451 shape: runmat_accelerate::graph::ShapeInfo::Unknown,
452 span: window.span.clone(),
453 pattern: None,
454 stack_layout: None,
455 })
456 .collect()
457}
458
459#[cfg(all(feature = "native-accel", test))]
460fn accel_nodes_for_instruction_window(
461 accel_graph: &runmat_accelerate::graph::AccelGraph,
462 window: &crate::bytecode::FusionInstructionWindow,
463 assigned_nodes: &HashSet<runmat_accelerate::graph::NodeId>,
464) -> Vec<runmat_accelerate::graph::NodeId> {
465 let mut nodes: Vec<_> = accel_graph
466 .nodes
467 .iter()
468 .filter(|node| {
469 !assigned_nodes.contains(&node.id)
470 && accel_node_matches_semantic_window_kind(node, window.kind)
471 && accel_node_span_matches_instruction_window(node, window)
472 })
473 .map(|node| node.id)
474 .collect();
475 nodes.sort_unstable_by_key(|node_id| {
476 accel_graph
477 .node(*node_id)
478 .map(|node| (node.span.start, node.span.end, node.id))
479 .unwrap_or((usize::MAX, usize::MAX, *node_id))
480 });
481 nodes.dedup();
482 nodes
483}
484
485#[cfg(all(feature = "native-accel", test))]
486fn accel_node_span_matches_instruction_window(
487 node: &runmat_accelerate::graph::AccelNode,
488 window: &crate::bytecode::FusionInstructionWindow,
489) -> bool {
490 node.span.start >= window.span.start && node.span.end <= window.span.end
493}
494
495#[cfg(all(feature = "native-accel", test))]
496fn accel_node_has_semantic_signal(node: &runmat_accelerate::graph::AccelNode) -> bool {
497 node.tags.iter().any(|tag| {
498 matches!(
499 tag,
500 runmat_accelerate::graph::AccelGraphTag::Unary
501 | runmat_accelerate::graph::AccelGraphTag::Elementwise
502 | runmat_accelerate::graph::AccelGraphTag::Reduction
503 | runmat_accelerate::graph::AccelGraphTag::MatMul
504 | runmat_accelerate::graph::AccelGraphTag::Transpose
505 )
506 })
507}
508
509#[cfg(all(feature = "native-accel", test))]
510fn accel_node_matches_semantic_window_kind(
511 node: &runmat_accelerate::graph::AccelNode,
512 kind: crate::bytecode::FusionInstructionKind,
513) -> bool {
514 let has_semantic_signal = accel_node_has_semantic_signal(node);
515 if !has_semantic_signal {
516 return match kind {
519 crate::bytecode::FusionInstructionKind::Elementwise => matches!(
520 node.category,
521 runmat_accelerate::graph::AccelOpCategory::Elementwise
522 | runmat_accelerate::graph::AccelOpCategory::Transpose
523 ),
524 crate::bytecode::FusionInstructionKind::Reduction => {
525 matches!(
526 node.category,
527 runmat_accelerate::graph::AccelOpCategory::Reduction
528 )
529 }
530 crate::bytecode::FusionInstructionKind::Matmul => {
531 matches!(
532 node.category,
533 runmat_accelerate::graph::AccelOpCategory::MatMul
534 )
535 }
536 };
537 }
538 let has_reduction = node
539 .tags
540 .iter()
541 .any(|tag| matches!(tag, runmat_accelerate::graph::AccelGraphTag::Reduction));
542 let has_matmul = node
543 .tags
544 .iter()
545 .any(|tag| matches!(tag, runmat_accelerate::graph::AccelGraphTag::MatMul));
546 match kind {
547 crate::bytecode::FusionInstructionKind::Elementwise => !has_reduction && !has_matmul,
548 crate::bytecode::FusionInstructionKind::Reduction => !has_matmul,
549 crate::bytecode::FusionInstructionKind::Matmul => true,
550 }
551}
552
553#[cfg(feature = "native-accel")]
554fn instruction_within_semantic_candidate_span(
555 instruction_index: usize,
556 instr_spans: &[runmat_hir::Span],
557 candidate_span: runmat_hir::Span,
558) -> bool {
559 if instr_spans.is_empty() || instruction_index >= instr_spans.len() {
560 return false;
561 }
562 source_span_contains(candidate_span, instr_spans[instruction_index])
563}
564
565#[cfg(feature = "native-accel")]
566fn derive_semantic_fusion_instruction_windows(
567 instructions: &[Instr],
568 instr_spans: &[runmat_hir::Span],
569 candidate_groups: &[crate::bytecode::FusionCandidateGroup],
570) -> Vec<crate::bytecode::FusionInstructionWindow> {
571 if instructions.is_empty() || instr_spans.is_empty() || candidate_groups.is_empty() {
572 return Vec::new();
573 }
574
575 let mut windows = Vec::new();
576 let mut assigned_instructions = HashSet::new();
577
578 for candidate in candidate_groups {
579 let mut run_start: Option<usize> = None;
580 let mut run_kind: Option<crate::bytecode::FusionInstructionKind> = None;
581 for (index, instr) in instructions.iter().enumerate() {
582 if index >= instr_spans.len() || assigned_instructions.contains(&index) {
583 if let Some(start) = run_start.take() {
584 windows.push(crate::bytecode::FusionInstructionWindow {
585 span: runmat_accelerate::graph::InstrSpan {
586 start,
587 end: index.saturating_sub(1),
588 },
589 kind: run_kind
590 .unwrap_or(crate::bytecode::FusionInstructionKind::Elementwise),
591 });
592 run_kind = None;
593 }
594 continue;
595 }
596 if !instruction_within_semantic_candidate_span(
597 index,
598 instr_spans,
599 candidate.source_span,
600 ) {
601 if let Some(start) = run_start.take() {
602 windows.push(crate::bytecode::FusionInstructionWindow {
603 span: runmat_accelerate::graph::InstrSpan {
604 start,
605 end: index.saturating_sub(1),
606 },
607 kind: run_kind
608 .unwrap_or(crate::bytecode::FusionInstructionKind::Elementwise),
609 });
610 run_kind = None;
611 }
612 continue;
613 }
614 let Some(signal_kind) = instr_fusion_signal_kind(instr) else {
615 if let Some(start) = run_start.take() {
616 windows.push(crate::bytecode::FusionInstructionWindow {
617 span: runmat_accelerate::graph::InstrSpan {
618 start,
619 end: index.saturating_sub(1),
620 },
621 kind: run_kind
622 .unwrap_or(crate::bytecode::FusionInstructionKind::Elementwise),
623 });
624 run_kind = None;
625 }
626 continue;
627 };
628
629 if run_start.is_none() {
630 run_start = Some(index);
631 run_kind = Some(signal_kind);
632 } else if matches!(signal_kind, crate::bytecode::FusionInstructionKind::Matmul) {
633 run_kind = Some(crate::bytecode::FusionInstructionKind::Matmul);
634 } else if matches!(
635 signal_kind,
636 crate::bytecode::FusionInstructionKind::Reduction
637 ) && !matches!(
638 run_kind,
639 Some(crate::bytecode::FusionInstructionKind::Matmul)
640 ) {
641 run_kind = Some(crate::bytecode::FusionInstructionKind::Reduction);
642 }
643 assigned_instructions.insert(index);
644 }
645 if let Some(start) = run_start.take() {
646 windows.push(crate::bytecode::FusionInstructionWindow {
647 span: runmat_accelerate::graph::InstrSpan {
648 start,
649 end: instructions.len().saturating_sub(1),
650 },
651 kind: run_kind.unwrap_or(crate::bytecode::FusionInstructionKind::Elementwise),
652 });
653 }
654 }
655
656 windows
657}
658
659#[cfg(feature = "native-accel")]
660fn instr_fusion_signal_kind(instr: &Instr) -> Option<crate::bytecode::FusionInstructionKind> {
661 match instr {
662 Instr::Add
663 | Instr::Sub
664 | Instr::Mul
665 | Instr::RightDiv
666 | Instr::LeftDiv
667 | Instr::Pow
668 | Instr::Neg
669 | Instr::UPlus
670 | Instr::Transpose
671 | Instr::ConjugateTranspose
672 | Instr::ElemMul
673 | Instr::ElemDiv
674 | Instr::ElemPow
675 | Instr::ElemLeftDiv
676 | Instr::LessEqual
677 | Instr::Less
678 | Instr::Greater
679 | Instr::GreaterEqual
680 | Instr::Equal
681 | Instr::NotEqual => Some(crate::bytecode::FusionInstructionKind::Elementwise),
682 Instr::CallBuiltinMulti(name, _, _) => builtin_functions()
683 .iter()
684 .find(|func| func.name == name.as_str())
685 .and_then(|func| {
686 let has_matmul = func
687 .accel_tags
688 .iter()
689 .any(|tag| matches!(tag, AccelTag::MatMul));
690 if has_matmul {
691 return Some(crate::bytecode::FusionInstructionKind::Matmul);
692 }
693 let has_reduction = func
694 .accel_tags
695 .iter()
696 .any(|tag| matches!(tag, AccelTag::Reduction));
697 if has_reduction {
698 return Some(crate::bytecode::FusionInstructionKind::Reduction);
699 }
700 let has_elementwise = func.accel_tags.iter().any(|tag| {
701 matches!(
702 tag,
703 AccelTag::Unary | AccelTag::Elementwise | AccelTag::Transpose
704 )
705 });
706 has_elementwise.then_some(crate::bytecode::FusionInstructionKind::Elementwise)
707 }),
708 _ => None,
709 }
710}
711
712#[cfg(feature = "native-accel")]
713fn infer_semantic_fusion_kind(
714 kind_hint: crate::bytecode::FusionInstructionKind,
715) -> runmat_accelerate::fusion::FusionKind {
716 match kind_hint {
717 crate::bytecode::FusionInstructionKind::Matmul => {
718 runmat_accelerate::fusion::FusionKind::MatmulEpilogue
719 }
720 crate::bytecode::FusionInstructionKind::Reduction => {
721 runmat_accelerate::fusion::FusionKind::Reduction
722 }
723 crate::bytecode::FusionInstructionKind::Elementwise => {
724 runmat_accelerate::fusion::FusionKind::ElementwiseChain
725 }
726 }
727}
728
729#[cfg(feature = "native-accel")]
730fn rvalue_has_fusion_signal(value: &MirRvalue) -> bool {
731 match value {
732 MirRvalue::Unary(_, _) | MirRvalue::Binary(_, _, _) => true,
733 MirRvalue::Call(call) => matches!(
734 call.semantic_kind,
735 BuiltinSemanticKind::Elementwise
736 | BuiltinSemanticKind::Reduction
737 | BuiltinSemanticKind::LinearAlgebra
738 | BuiltinSemanticKind::ShapeTransform(_)
739 ),
740 MirRvalue::ShortCircuit { .. } => false,
741 MirRvalue::Use(_)
742 | MirRvalue::Range { .. }
743 | MirRvalue::Aggregate { .. }
744 | MirRvalue::StructLiteral { .. }
745 | MirRvalue::ObjectLiteral { .. }
746 | MirRvalue::Index { .. }
747 | MirRvalue::Member { .. }
748 | MirRvalue::DynamicMember { .. }
749 | MirRvalue::WorkspaceFirstStaticProperty { .. }
750 | MirRvalue::MetaClass(_)
751 | MirRvalue::Colon
752 | MirRvalue::End
753 | MirRvalue::Future { .. }
754 | MirRvalue::Spawn(_) => false,
755 }
756}
757
758pub fn compile_semantic_function_registry(
759 hir: &HirAssembly,
760 mir: &MirAssembly,
761) -> Result<HashMap<FunctionId, FunctionBytecode>, CompileError> {
762 let layout = derive_layout(hir, mir)
763 .map_err(|err| CompileError::new(format!("failed to derive VM layout: {err:?}")))?;
764 compile_semantic_functions(hir, mir, &layout, None)
765}
766
767fn compile_semantic_functions(
768 hir: &HirAssembly,
769 mir: &MirAssembly,
770 layout: &crate::layout::VmAssemblyLayout,
771 entrypoint: Option<EntrypointId>,
772) -> Result<HashMap<FunctionId, FunctionBytecode>, CompileError> {
773 let entry_target = entrypoint
774 .and_then(|entrypoint| layout.entrypoints.get(&entrypoint))
775 .map(|entry| entry.target);
776 let mut functions = HashMap::new();
777 for function in &hir.functions {
778 if Some(function.id) == entry_target {
779 continue;
780 }
781 let mut compiler = Compiler::new_for_function(hir, mir, layout.clone(), function.id)?;
782 compiler.compile()?;
783 let function_layout = layout.functions.get(&function.id).ok_or_else(|| {
784 CompileError::new(format!("missing VM layout for function {:?}", function.id))
785 })?;
786 let source_id = hir
787 .modules
788 .get(function.module.0)
789 .map(|module| module.source_id);
790 functions.insert(
791 function.id,
792 FunctionBytecode {
793 function: function.id,
794 display_name: function_layout.display_name.clone(),
795 private_owner_scope: function_layout.private_owner_scope.clone(),
796 source_id,
797 instructions: compiler.instructions,
798 instr_spans: compiler.instr_spans,
799 call_arg_spans: compiler.call_arg_spans,
800 var_count: compiler.var_count,
801 input_slots: function_layout
802 .frame_abi
803 .fixed_inputs
804 .iter()
805 .filter(|slot| Some(**slot) != function_layout.frame_abi.varargin)
806 .map(|slot| slot.0)
807 .collect(),
808 varargin_slot: function_layout.frame_abi.varargin.map(|slot| slot.0),
809 implicit_nargin_slot: function_layout.frame_abi.implicit_nargin.map(|slot| slot.0),
810 output_slots: function_layout
811 .frame_abi
812 .fixed_outputs
813 .iter()
814 .filter(|slot| Some(**slot) != function_layout.frame_abi.varargout)
815 .map(|slot| slot.0)
816 .collect(),
817 varargout_slot: function_layout.frame_abi.varargout.map(|slot| slot.0),
818 implicit_nargout_slot: function_layout
819 .frame_abi
820 .implicit_nargout
821 .map(|slot| slot.0),
822 capture_slots: function_layout
823 .captures
824 .iter()
825 .map(|capture| capture.slot.0)
826 .collect(),
827 var_names: function_layout_var_names(hir, function_layout)?,
828 initially_unassigned_slots: function_layout_initially_unassigned_slots(
829 hir,
830 function_layout,
831 ),
832 argument_validations: function
833 .argument_validations
834 .iter()
835 .filter_map(|validation| {
836 function_layout
837 .binding_slots
838 .get(&validation.binding)
839 .map(|slot| crate::bytecode::program::FunctionArgumentValidation {
840 input_slot: slot.0,
841 size: validation.size.as_ref().map(|size| {
842 crate::bytecode::program::FunctionArgSizeSpec {
843 rows: match size.rows {
844 runmat_hir::FunctionArgDim::Any => {
845 crate::bytecode::program::FunctionArgDim::Any
846 }
847 runmat_hir::FunctionArgDim::Exact(value) => {
848 crate::bytecode::program::FunctionArgDim::Exact(value)
849 }
850 },
851 cols: match size.cols {
852 runmat_hir::FunctionArgDim::Any => {
853 crate::bytecode::program::FunctionArgDim::Any
854 }
855 runmat_hir::FunctionArgDim::Exact(value) => {
856 crate::bytecode::program::FunctionArgDim::Exact(value)
857 }
858 },
859 }
860 }),
861 class_name: validation.class_name.clone(),
862 validators: validation
863 .validators
864 .iter()
865 .map(|validator| match validator {
866 runmat_hir::FunctionArgValidator::A(class_names) => {
867 crate::bytecode::program::FunctionArgValidator::A(class_names.clone())
868 }
869 runmat_hir::FunctionArgValidator::Column => {
870 crate::bytecode::program::FunctionArgValidator::Column
871 }
872 runmat_hir::FunctionArgValidator::Finite => {
873 crate::bytecode::program::FunctionArgValidator::Finite
874 }
875 runmat_hir::FunctionArgValidator::Float => {
876 crate::bytecode::program::FunctionArgValidator::Float
877 }
878 runmat_hir::FunctionArgValidator::Folder => {
879 crate::bytecode::program::FunctionArgValidator::Folder
880 }
881 runmat_hir::FunctionArgValidator::File => {
882 crate::bytecode::program::FunctionArgValidator::File
883 }
884 runmat_hir::FunctionArgValidator::NumericOrLogical => {
885 crate::bytecode::program::FunctionArgValidator::NumericOrLogical
886 }
887 runmat_hir::FunctionArgValidator::Numeric => {
888 crate::bytecode::program::FunctionArgValidator::Numeric
889 }
890 runmat_hir::FunctionArgValidator::Text => {
891 crate::bytecode::program::FunctionArgValidator::Text
892 }
893 runmat_hir::FunctionArgValidator::TextScalar => {
894 crate::bytecode::program::FunctionArgValidator::TextScalar
895 }
896 runmat_hir::FunctionArgValidator::NonzeroLengthText => {
897 crate::bytecode::program::FunctionArgValidator::NonzeroLengthText
898 }
899 runmat_hir::FunctionArgValidator::Nonempty => {
900 crate::bytecode::program::FunctionArgValidator::Nonempty
901 }
902 runmat_hir::FunctionArgValidator::ScalarOrEmpty => {
903 crate::bytecode::program::FunctionArgValidator::ScalarOrEmpty
904 }
905 runmat_hir::FunctionArgValidator::Real => {
906 crate::bytecode::program::FunctionArgValidator::Real
907 }
908 runmat_hir::FunctionArgValidator::Integer => {
909 crate::bytecode::program::FunctionArgValidator::Integer
910 }
911 runmat_hir::FunctionArgValidator::Vector => {
912 crate::bytecode::program::FunctionArgValidator::Vector
913 }
914 runmat_hir::FunctionArgValidator::Positive => {
915 crate::bytecode::program::FunctionArgValidator::Positive
916 }
917 runmat_hir::FunctionArgValidator::Negative => {
918 crate::bytecode::program::FunctionArgValidator::Negative
919 }
920 runmat_hir::FunctionArgValidator::Nonnegative => {
921 crate::bytecode::program::FunctionArgValidator::Nonnegative
922 }
923 runmat_hir::FunctionArgValidator::Nonmissing => {
924 crate::bytecode::program::FunctionArgValidator::Nonmissing
925 }
926 runmat_hir::FunctionArgValidator::NonNan => {
927 crate::bytecode::program::FunctionArgValidator::NonNan
928 }
929 runmat_hir::FunctionArgValidator::Nonzero => {
930 crate::bytecode::program::FunctionArgValidator::Nonzero
931 }
932 runmat_hir::FunctionArgValidator::Nonpositive => {
933 crate::bytecode::program::FunctionArgValidator::Nonpositive
934 }
935 runmat_hir::FunctionArgValidator::Nonsparse => {
936 crate::bytecode::program::FunctionArgValidator::Nonsparse
937 }
938 runmat_hir::FunctionArgValidator::Sparse => {
939 crate::bytecode::program::FunctionArgValidator::Sparse
940 }
941 runmat_hir::FunctionArgValidator::ValidVariableName => {
942 crate::bytecode::program::FunctionArgValidator::ValidVariableName
943 }
944 runmat_hir::FunctionArgValidator::UnderlyingType(class_names) => {
945 crate::bytecode::program::FunctionArgValidator::UnderlyingType(class_names.clone())
946 }
947 runmat_hir::FunctionArgValidator::Member(literals) => {
948 crate::bytecode::program::FunctionArgValidator::Member(
949 literals
950 .iter()
951 .map(|literal| match literal {
952 runmat_hir::FunctionArgValidationLiteral::Number(value) => {
953 crate::bytecode::program::FunctionArgValidationLiteral::Number(*value)
954 }
955 runmat_hir::FunctionArgValidationLiteral::Text(value) => {
956 crate::bytecode::program::FunctionArgValidationLiteral::Text(value.clone())
957 }
958 runmat_hir::FunctionArgValidationLiteral::Bool(value) => {
959 crate::bytecode::program::FunctionArgValidationLiteral::Bool(*value)
960 }
961 })
962 .collect(),
963 )
964 }
965 runmat_hir::FunctionArgValidator::InRange(lower, upper, inclusivity) => {
966 crate::bytecode::program::FunctionArgValidator::InRange(
967 *lower,
968 *upper,
969 crate::bytecode::program::FunctionArgRangeInclusivity {
970 lower: inclusivity.lower,
971 upper: inclusivity.upper,
972 },
973 )
974 }
975 runmat_hir::FunctionArgValidator::GreaterThanOrEqual(
976 threshold,
977 ) => crate::bytecode::program::FunctionArgValidator::GreaterThanOrEqual(*threshold),
978 runmat_hir::FunctionArgValidator::LessThanOrEqual(
979 threshold,
980 ) => crate::bytecode::program::FunctionArgValidator::LessThanOrEqual(*threshold),
981 runmat_hir::FunctionArgValidator::GreaterThan(
982 threshold,
983 ) => crate::bytecode::program::FunctionArgValidator::GreaterThan(*threshold),
984 runmat_hir::FunctionArgValidator::LessThan(
985 threshold,
986 ) => crate::bytecode::program::FunctionArgValidator::LessThan(*threshold),
987 })
988 .collect(),
989 default_value: validation.default_value.as_ref().map(|default| {
990 match default {
991 runmat_hir::FunctionArgDefaultValue::Number(value) => {
992 crate::bytecode::program::FunctionArgDefaultValue::Number(*value)
993 }
994 runmat_hir::FunctionArgDefaultValue::Bool(value) => {
995 crate::bytecode::program::FunctionArgDefaultValue::Bool(*value)
996 }
997 runmat_hir::FunctionArgDefaultValue::String(value) => {
998 crate::bytecode::program::FunctionArgDefaultValue::String(value.clone())
999 }
1000 runmat_hir::FunctionArgDefaultValue::EmptyArray => {
1001 crate::bytecode::program::FunctionArgDefaultValue::EmptyArray
1002 }
1003 }
1004 }),
1005 })
1006 })
1007 .collect(),
1008 },
1009 );
1010 }
1011 Ok(functions)
1012}
1013
1014fn function_layout_var_names(
1015 hir: &HirAssembly,
1016 function_layout: &crate::layout::VmFunctionLayout,
1017) -> Result<HashMap<usize, String>, CompileError> {
1018 let mut names = HashMap::new();
1019 for (binding, slot) in &function_layout.binding_slots {
1020 let hir_binding = hir.bindings.get(binding.0).ok_or_else(|| {
1021 CompileError::new(format!("missing HIR binding for VM slot {:?}", binding))
1022 })?;
1023 names.insert(slot.0, hir_binding.name.0.clone());
1024 }
1025 Ok(names)
1026}
1027
1028fn function_layout_initially_unassigned_slots(
1029 hir: &HirAssembly,
1030 function_layout: &crate::layout::VmFunctionLayout,
1031) -> HashSet<usize> {
1032 function_layout
1033 .binding_slots
1034 .iter()
1035 .filter_map(|(binding, slot)| {
1036 hir.bindings
1037 .get(binding.0)
1038 .is_some_and(|hir_binding| {
1039 matches!(hir_binding.role, runmat_hir::BindingRole::ExternalWorkspace)
1040 })
1041 .then_some(slot.0)
1042 })
1043 .collect()
1044}
1045
1046#[cfg(test)]
1047mod tests {
1048 use super::compile;
1049 use crate::Instr;
1050 use futures::executor::block_on;
1051 #[cfg(feature = "native-accel")]
1052 use runmat_accelerate::fusion::prepare_fusion_plan;
1053 use runmat_builtins::Value;
1054 use runmat_hir::{
1055 lower, AssignmentCreationPolicy, BuiltinId, CallableFallbackPolicy, CallableIdentity,
1056 DefPath, DefPathSegment, FunctionId, IndexResultContext, LoweringContext, MethodId,
1057 OperatorKind, PackageName, QualifiedName, RequestedOutputCount, SymbolName,
1058 };
1059 use runmat_mir::lowering::lower_assembly;
1060 use runmat_mir::{
1061 MirAggregateKind, MirCallee, MirConstant, MirIndexComponent, MirIndexPlan, MirOperand,
1062 MirOutputTarget, MirPlace, MirRvalue, MirStmtKind, MirTerminatorKind,
1063 };
1064 use std::collections::HashMap;
1065 use std::sync::Arc;
1066 use std::time::{SystemTime, UNIX_EPOCH};
1067
1068 fn unique_csv_temp_path(prefix: &str) -> std::path::PathBuf {
1069 let nanos = SystemTime::now()
1070 .duration_since(UNIX_EPOCH)
1071 .expect("system time before unix epoch")
1072 .as_nanos();
1073 std::env::temp_dir().join(format!(
1074 "runmat_{prefix}_{}_{}.csv",
1075 std::process::id(),
1076 nanos
1077 ))
1078 }
1079
1080 #[test]
1081 fn compile_attaches_derived_layout() {
1082 let ast = runmat_parser::parse("x = 1 + 2;").expect("parse");
1083 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
1084 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
1085 let entrypoint = hir.assembly.entrypoints[0].id;
1086
1087 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
1088
1089 let layout = bytecode.layout.as_ref().expect("layout");
1090 let entrypoint_layout = &layout.entrypoints[&entrypoint];
1091 let function_layout = &layout.functions[&entrypoint_layout.target];
1092 assert_eq!(bytecode.var_count, function_layout.local_count);
1093 assert_eq!(bytecode.var_types.len(), function_layout.local_count);
1094 }
1095
1096 #[test]
1097 fn compile_lowers_simple_assignment_arithmetic() {
1098 let ast = runmat_parser::parse("x = 1 + 2;").expect("parse");
1099 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
1100 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
1101 let entrypoint = hir.assembly.entrypoints[0].id;
1102
1103 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
1104
1105 assert_eq!(bytecode.instructions.len(), 4);
1106 assert!(matches!(bytecode.instructions[0], Instr::LoadConst(1.0)));
1107 assert!(matches!(bytecode.instructions[1], Instr::LoadConst(2.0)));
1108 assert!(matches!(bytecode.instructions[2], Instr::Add));
1109 assert!(matches!(bytecode.instructions[3], Instr::StoreVar(_)));
1110 }
1111
1112 #[cfg(feature = "native-accel")]
1113 #[test]
1114 fn compile_records_semantic_fusion_metadata() {
1115 let ast = runmat_parser::parse("x = 1 + 2; y = x * 3;").expect("parse");
1116 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
1117 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
1118 let entrypoint = hir.assembly.entrypoints[0].id;
1119
1120 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
1121
1122 assert!(
1123 bytecode.fusion_metadata.mir_fusion_signal_count > 0,
1124 "expected non-zero MIR fusion signal count"
1125 );
1126 assert!(
1127 bytecode.fusion_metadata.mir_fusion_candidate_group_count > 0,
1128 "expected non-zero MIR fusion candidate group count"
1129 );
1130 assert!(
1131 !bytecode
1132 .fusion_metadata
1133 .mir_fusion_candidate_groups
1134 .is_empty(),
1135 "expected non-empty MIR fusion candidate groups"
1136 );
1137 assert!(
1138 bytecode
1139 .fusion_metadata
1140 .mir_fusion_candidate_groups
1141 .iter()
1142 .all(|group| group.stmt_end > group.stmt_start),
1143 "expected candidate groups to carry non-empty statement spans"
1144 );
1145 assert!(
1146 bytecode
1147 .fusion_metadata
1148 .mir_fusion_candidate_groups
1149 .iter()
1150 .all(|group| group.source_span.end > group.source_span.start),
1151 "expected candidate groups to carry non-empty source spans"
1152 );
1153 assert!(
1154 bytecode.fusion_metadata.instruction_window_count > 0,
1155 "expected non-zero semantic instruction window count"
1156 );
1157 assert_eq!(
1158 bytecode.fusion_metadata.instruction_window_count,
1159 bytecode.fusion_metadata.instruction_windows.len(),
1160 "window count should match serialized semantic instruction window entries"
1161 );
1162 assert!(
1163 bytecode
1164 .fusion_metadata
1165 .instruction_windows
1166 .iter()
1167 .all(|window| window.span.end >= window.span.start),
1168 "expected semantic instruction windows to carry valid instruction spans"
1169 );
1170 }
1171
1172 #[cfg(feature = "native-accel")]
1173 #[test]
1174 fn compile_emits_semantic_window_scaffolds_and_runtime_plan_reconciles_nodes() {
1175 let ast = runmat_parser::parse("x = 1 + 2; y = x * 3;").expect("parse");
1176 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
1177 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
1178 let entrypoint = hir.assembly.entrypoints[0].id;
1179
1180 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
1181 let runtime_groups = bytecode.runtime_fusion_groups();
1182 let runtime_graph = bytecode.runtime_accel_graph_for_fusion(&runtime_groups);
1183 assert!(
1184 runtime_graph.is_some(),
1185 "expected runtime accel graph when semantic fusion candidates/windows exist"
1186 );
1187 assert!(
1188 !bytecode.fusion_groups.is_empty(),
1189 "expected semantic-window fusion scaffolds"
1190 );
1191 assert!(
1192 bytecode
1193 .fusion_groups
1194 .iter()
1195 .all(|group| group.nodes.is_empty()),
1196 "compile should not assign accel node IDs to semantic-window groups"
1197 );
1198
1199 let runtime_groups = if let Some(graph) = runtime_graph.as_ref() {
1200 bytecode.runtime_fusion_groups_for_graph(graph)
1201 } else {
1202 bytecode.fusion_groups.clone()
1203 };
1204 let runtime_plan = prepare_fusion_plan(
1205 runtime_graph.as_ref(),
1206 &runtime_groups,
1207 bytecode.fusion_metadata.mir_fusion_candidate_group_count,
1208 )
1209 .expect("runtime fusion planning should reconcile executable groups");
1210 assert!(
1211 runtime_plan
1212 .groups
1213 .iter()
1214 .any(|group| !group.group.nodes.is_empty()),
1215 "runtime fusion planning should reconcile node IDs from accel graph"
1216 );
1217 }
1218
1219 #[cfg(feature = "native-accel")]
1220 #[test]
1221 fn compile_keeps_multi_window_groups_node_empty_before_runtime_reconciliation() {
1222 let ast =
1223 runmat_parser::parse("a = 1 + 2; b = a * 3; marker = 'x'; c = b - 4; d = c ./ 2;")
1224 .expect("parse");
1225 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
1226 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
1227 let entrypoint = hir.assembly.entrypoints[0].id;
1228
1229 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
1230 assert!(
1231 bytecode.fusion_groups.len() >= 2,
1232 "expected multiple semantic-window fusion groups for split accel-capable runs"
1233 );
1234 assert!(
1235 bytecode
1236 .fusion_groups
1237 .iter()
1238 .all(|group| group.nodes.is_empty()),
1239 "compile-time semantic-window groups must remain node-empty"
1240 );
1241
1242 let runtime_groups = bytecode.runtime_fusion_groups();
1243 let runtime_graph = bytecode.runtime_accel_graph_for_fusion(&runtime_groups);
1244 let runtime_groups = if let Some(graph) = runtime_graph.as_ref() {
1245 bytecode.runtime_fusion_groups_for_graph(graph)
1246 } else {
1247 bytecode.fusion_groups.clone()
1248 };
1249 let runtime_plan = prepare_fusion_plan(
1250 runtime_graph.as_ref(),
1251 &runtime_groups,
1252 bytecode.fusion_metadata.mir_fusion_candidate_group_count,
1253 )
1254 .expect("runtime fusion planning should reconcile executable groups");
1255 assert!(
1256 runtime_plan
1257 .groups
1258 .iter()
1259 .any(|group| !group.group.nodes.is_empty()),
1260 "runtime reconciliation should assign accel nodes"
1261 );
1262 }
1263
1264 #[cfg(feature = "native-accel")]
1265 #[test]
1266 fn compile_semantically_gates_bytecode_fusion_groups() {
1267 let ast = runmat_parser::parse("x = 1;").expect("parse");
1268 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
1269 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
1270 let entrypoint = hir.assembly.entrypoints[0].id;
1271 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
1272 let runtime_graph =
1273 bytecode.runtime_accel_graph_for_fusion(&bytecode.runtime_fusion_groups());
1274
1275 assert_eq!(
1276 bytecode.fusion_metadata.mir_fusion_candidate_group_count, 0,
1277 "expected no semantic fusion candidate groups"
1278 );
1279 assert!(
1280 runtime_graph.is_none(),
1281 "expected runtime accel graph to be omitted when semantic candidate groups are absent"
1282 );
1283 assert!(
1284 bytecode.fusion_groups.is_empty(),
1285 "expected bytecode fusion groups to be gated off when semantic candidates are absent"
1286 );
1287 }
1288
1289 #[cfg(feature = "native-accel")]
1290 #[test]
1291 fn compile_omits_accel_graph_when_signals_exist_but_no_candidate_group() {
1292 let ast = runmat_parser::parse("x = 1 + 2;").expect("parse");
1293 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
1294 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
1295 let entrypoint = hir.assembly.entrypoints[0].id;
1296 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
1297 let runtime_graph =
1298 bytecode.runtime_accel_graph_for_fusion(&bytecode.runtime_fusion_groups());
1299
1300 assert!(
1301 bytecode.fusion_metadata.mir_fusion_signal_count > 0,
1302 "expected non-zero fusion signal count for arithmetic operation"
1303 );
1304 assert_eq!(
1305 bytecode.fusion_metadata.mir_fusion_candidate_group_count, 0,
1306 "expected no semantic candidate groups for a single-operation run"
1307 );
1308 assert!(
1309 runtime_graph.is_none(),
1310 "expected runtime accel graph omission to follow semantic candidate-group gating"
1311 );
1312 assert!(
1313 bytecode.fusion_groups.is_empty(),
1314 "expected no executable bytecode fusion groups without semantic candidates"
1315 );
1316 }
1317
1318 #[cfg(feature = "native-accel")]
1319 #[test]
1320 fn compile_omits_accel_graph_when_candidates_overlap_only_logical_ops() {
1321 let ast =
1322 runmat_parser::parse("a = true; b = false; c = a & b; d = c | a;").expect("parse");
1323 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
1324 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
1325 let entrypoint = hir.assembly.entrypoints[0].id;
1326 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
1327 let runtime_graph =
1328 bytecode.runtime_accel_graph_for_fusion(&bytecode.runtime_fusion_groups());
1329
1330 assert!(
1331 bytecode.fusion_metadata.mir_fusion_candidate_group_count > 0,
1332 "logical chain should still produce semantic candidate groups"
1333 );
1334 assert!(
1335 runtime_graph.is_none(),
1336 "expected runtime accel graph omission when candidate overlap is non-accelerable logical ops"
1337 );
1338 assert!(
1339 bytecode.fusion_groups.is_empty(),
1340 "expected no executable fusion groups for logical-only candidate overlap"
1341 );
1342 }
1343
1344 #[cfg(feature = "native-accel")]
1345 #[test]
1346 fn candidate_accel_capability_gate_rejects_logical_ops() {
1347 let instructions = vec![Instr::LogicalAnd, Instr::LogicalOr];
1348 let instr_spans = vec![
1349 runmat_hir::Span { start: 10, end: 20 },
1350 runmat_hir::Span { start: 21, end: 30 },
1351 ];
1352 let candidates = vec![crate::bytecode::FusionCandidateGroup {
1353 id: 0,
1354 signal_count: 2,
1355 function: runmat_hir::FunctionId(0),
1356 block: runmat_mir::BasicBlockId(0),
1357 stmt_start: 0,
1358 stmt_end: 2,
1359 source_span: runmat_hir::Span { start: 10, end: 30 },
1360 }];
1361 assert!(
1362 !super::candidates_touch_accel_capable_instruction(
1363 &instructions,
1364 &instr_spans,
1365 &candidates,
1366 ),
1367 "logical ops should not trigger accel-graph construction gate"
1368 );
1369 }
1370
1371 #[cfg(feature = "native-accel")]
1372 #[test]
1373 fn candidate_accel_capability_gate_accepts_binary_ops() {
1374 let instructions = vec![Instr::Add, Instr::ElemMul];
1375 let instr_spans = vec![
1376 runmat_hir::Span { start: 10, end: 20 },
1377 runmat_hir::Span { start: 21, end: 30 },
1378 ];
1379 let candidates = vec![crate::bytecode::FusionCandidateGroup {
1380 id: 0,
1381 signal_count: 2,
1382 function: runmat_hir::FunctionId(0),
1383 block: runmat_mir::BasicBlockId(0),
1384 stmt_start: 0,
1385 stmt_end: 2,
1386 source_span: runmat_hir::Span { start: 10, end: 30 },
1387 }];
1388 assert!(
1389 super::candidates_touch_accel_capable_instruction(
1390 &instructions,
1391 &instr_spans,
1392 &candidates,
1393 ),
1394 "elementwise arithmetic ops should trigger accel-graph construction gate"
1395 );
1396 }
1397
1398 #[cfg(feature = "native-accel")]
1399 #[test]
1400 fn candidate_accel_capability_gate_rejects_partial_span_overlap() {
1401 let instructions = vec![Instr::Add];
1402 let instr_spans = vec![runmat_hir::Span { start: 10, end: 20 }];
1403 let candidates = vec![crate::bytecode::FusionCandidateGroup {
1404 id: 0,
1405 signal_count: 1,
1406 function: runmat_hir::FunctionId(0),
1407 block: runmat_mir::BasicBlockId(0),
1408 stmt_start: 0,
1409 stmt_end: 1,
1410 source_span: runmat_hir::Span { start: 19, end: 25 },
1411 }];
1412 assert!(
1413 !super::candidates_touch_accel_capable_instruction(
1414 &instructions,
1415 &instr_spans,
1416 &candidates,
1417 ),
1418 "partial boundary overlap should not satisfy accel-capability semantic gate"
1419 );
1420 }
1421
1422 #[cfg(feature = "native-accel")]
1423 #[test]
1424 fn candidate_accel_capability_gate_accepts_reduction_builtin() {
1425 let instructions = vec![Instr::CallBuiltinMulti("sum".to_string(), 1, 1)];
1426 let instr_spans = vec![runmat_hir::Span { start: 10, end: 20 }];
1427 let candidates = vec![crate::bytecode::FusionCandidateGroup {
1428 id: 0,
1429 signal_count: 2,
1430 function: runmat_hir::FunctionId(0),
1431 block: runmat_mir::BasicBlockId(0),
1432 stmt_start: 0,
1433 stmt_end: 2,
1434 source_span: runmat_hir::Span { start: 10, end: 20 },
1435 }];
1436 assert!(
1437 super::candidates_touch_accel_capable_instruction(
1438 &instructions,
1439 &instr_spans,
1440 &candidates,
1441 ),
1442 "reduction builtin call should trigger accel-graph construction gate"
1443 );
1444 }
1445
1446 #[cfg(feature = "native-accel")]
1447 #[test]
1448 fn candidate_accel_capability_gate_rejects_control_assert_builtin() {
1449 let instructions = vec![Instr::CallBuiltinMulti("assert".to_string(), 1, 0)];
1450 let instr_spans = vec![runmat_hir::Span { start: 10, end: 20 }];
1451 let candidates = vec![crate::bytecode::FusionCandidateGroup {
1452 id: 0,
1453 signal_count: 2,
1454 function: runmat_hir::FunctionId(0),
1455 block: runmat_mir::BasicBlockId(0),
1456 stmt_start: 0,
1457 stmt_end: 2,
1458 source_span: runmat_hir::Span { start: 10, end: 20 },
1459 }];
1460 assert!(
1461 !super::candidates_touch_accel_capable_instruction(
1462 &instructions,
1463 &instr_spans,
1464 &candidates,
1465 ),
1466 "control/assertion builtin should not trigger accel-graph construction gate"
1467 );
1468 }
1469
1470 #[cfg(feature = "native-accel")]
1471 #[test]
1472 fn candidate_accel_capability_gate_rejects_sink_builtins() {
1473 for builtin in ["disp", "fprintf"] {
1474 let instructions = vec![Instr::CallBuiltinMulti(builtin.to_string(), 1, 0)];
1475 let instr_spans = vec![runmat_hir::Span { start: 10, end: 20 }];
1476 let candidates = vec![crate::bytecode::FusionCandidateGroup {
1477 id: 0,
1478 signal_count: 2,
1479 function: runmat_hir::FunctionId(0),
1480 block: runmat_mir::BasicBlockId(0),
1481 stmt_start: 0,
1482 stmt_end: 2,
1483 source_span: runmat_hir::Span { start: 10, end: 20 },
1484 }];
1485 assert!(
1486 !super::candidates_touch_accel_capable_instruction(
1487 &instructions,
1488 &instr_spans,
1489 &candidates,
1490 ),
1491 "sink builtin `{builtin}` should not trigger accel-graph construction gate"
1492 );
1493 }
1494 }
1495
1496 #[cfg(feature = "native-accel")]
1497 #[test]
1498 fn compile_scopes_semantic_fusion_metadata_to_entrypoint_target() {
1499 let source = "x = 1; function z = helper(a); t = a + 1; z = t * 2; end;";
1500 let ast = runmat_parser::parse(source).expect("parse");
1501 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
1502 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
1503 let entrypoint = hir.assembly.entrypoints[0].id;
1504 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
1505 let runtime_graph =
1506 bytecode.runtime_accel_graph_for_fusion(&bytecode.runtime_fusion_groups());
1507
1508 assert_eq!(
1509 bytecode.fusion_metadata.mir_fusion_signal_count, 0,
1510 "non-entrypoint helper MIR bodies should not drive entrypoint fusion signal metadata"
1511 );
1512 assert_eq!(
1513 bytecode.fusion_metadata.mir_fusion_candidate_group_count, 0,
1514 "non-entrypoint helper MIR bodies should not drive entrypoint fusion candidate metadata"
1515 );
1516 assert!(
1517 runtime_graph.is_none(),
1518 "entrypoint with no semantic candidates should omit runtime accel graph even if helper bodies are fusible"
1519 );
1520 assert!(
1521 bytecode.fusion_groups.is_empty(),
1522 "entrypoint with no semantic candidates should not emit executable fusion groups"
1523 );
1524 }
1525
1526 #[cfg(feature = "native-accel")]
1527 #[test]
1528 fn fusion_group_semantic_span_filter_requires_full_group_coverage() {
1529 let instr_spans = vec![
1530 runmat_hir::Span { start: 0, end: 2 },
1531 runmat_hir::Span { start: 2, end: 4 },
1532 runmat_hir::Span { start: 4, end: 6 },
1533 ];
1534 let group = runmat_accelerate::fusion::FusionGroup {
1535 id: 0,
1536 kind: runmat_accelerate::fusion::FusionKind::ElementwiseChain,
1537 nodes: vec![],
1538 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1539 span: runmat_accelerate::graph::InstrSpan { start: 1, end: 2 },
1540 pattern: None,
1541 stack_layout: None,
1542 };
1543 let fully_covering_candidates = vec![crate::bytecode::FusionCandidateGroup {
1544 id: 0,
1545 signal_count: 3,
1546 function: runmat_hir::FunctionId(0),
1547 block: runmat_mir::BasicBlockId(0),
1548 stmt_start: 0,
1549 stmt_end: 2,
1550 source_span: runmat_hir::Span { start: 2, end: 6 },
1551 }];
1552 let partially_covering_candidates = vec![crate::bytecode::FusionCandidateGroup {
1553 id: 0,
1554 signal_count: 2,
1555 function: runmat_hir::FunctionId(0),
1556 block: runmat_mir::BasicBlockId(0),
1557 stmt_start: 0,
1558 stmt_end: 1,
1559 source_span: runmat_hir::Span { start: 0, end: 3 },
1560 }];
1561 let non_overlapping_candidates = vec![crate::bytecode::FusionCandidateGroup {
1562 id: 0,
1563 signal_count: 2,
1564 function: runmat_hir::FunctionId(0),
1565 block: runmat_mir::BasicBlockId(0),
1566 stmt_start: 0,
1567 stmt_end: 1,
1568 source_span: runmat_hir::Span { start: 8, end: 10 },
1569 }];
1570
1571 assert!(
1572 super::fusion_group_within_semantic_candidate_spans(
1573 &group,
1574 &instr_spans,
1575 &fully_covering_candidates
1576 ),
1577 "expected full coverage when all instruction source spans intersect semantic candidate spans"
1578 );
1579 assert!(
1580 !super::fusion_group_within_semantic_candidate_spans(
1581 &group,
1582 &instr_spans,
1583 &partially_covering_candidates
1584 ),
1585 "expected group rejection when only part of the instruction span range intersects semantic candidate spans"
1586 );
1587 assert!(
1588 !super::fusion_group_within_semantic_candidate_spans(
1589 &group,
1590 &instr_spans,
1591 &non_overlapping_candidates
1592 ),
1593 "expected no overlap when instruction source spans are disjoint from semantic candidate spans"
1594 );
1595 }
1596
1597 #[cfg(feature = "native-accel")]
1598 #[test]
1599 fn fusion_group_semantic_span_filter_rejects_multi_candidate_union_coverage() {
1600 let instr_spans = vec![
1601 runmat_hir::Span { start: 0, end: 2 },
1602 runmat_hir::Span { start: 2, end: 4 },
1603 ];
1604 let group = runmat_accelerate::fusion::FusionGroup {
1605 id: 0,
1606 kind: runmat_accelerate::fusion::FusionKind::ElementwiseChain,
1607 nodes: vec![],
1608 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1609 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 1 },
1610 pattern: None,
1611 stack_layout: None,
1612 };
1613 let split_candidates = vec![
1614 crate::bytecode::FusionCandidateGroup {
1615 id: 0,
1616 signal_count: 1,
1617 function: runmat_hir::FunctionId(0),
1618 block: runmat_mir::BasicBlockId(0),
1619 stmt_start: 0,
1620 stmt_end: 1,
1621 source_span: runmat_hir::Span { start: 0, end: 2 },
1622 },
1623 crate::bytecode::FusionCandidateGroup {
1624 id: 1,
1625 signal_count: 1,
1626 function: runmat_hir::FunctionId(0),
1627 block: runmat_mir::BasicBlockId(0),
1628 stmt_start: 1,
1629 stmt_end: 2,
1630 source_span: runmat_hir::Span { start: 2, end: 4 },
1631 },
1632 ];
1633
1634 assert!(
1635 !super::fusion_group_within_semantic_candidate_spans(
1636 &group,
1637 &instr_spans,
1638 &split_candidates
1639 ),
1640 "expected rejection when bytecode group coverage requires unioning multiple semantic candidate spans"
1641 );
1642 }
1643
1644 #[cfg(feature = "native-accel")]
1645 #[test]
1646 fn candidates_build_fusion_groups_from_accel_graph_nodes() {
1647 let accel_graph = runmat_accelerate::graph::AccelGraph {
1648 nodes: vec![
1649 runmat_accelerate::graph::AccelNode {
1650 id: 0,
1651 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
1652 runmat_accelerate::graph::PrimitiveOp::Add,
1653 ),
1654 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
1655 inputs: vec![0, 0],
1656 outputs: vec![1],
1657 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
1658 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
1659 },
1660 runmat_accelerate::graph::AccelNode {
1661 id: 1,
1662 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
1663 runmat_accelerate::graph::PrimitiveOp::ElemMul,
1664 ),
1665 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
1666 inputs: vec![1, 0],
1667 outputs: vec![2],
1668 span: runmat_accelerate::graph::InstrSpan { start: 1, end: 1 },
1669 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
1670 },
1671 ],
1672 values: vec![
1673 runmat_accelerate::graph::ValueInfo {
1674 id: 0,
1675 origin: runmat_accelerate::graph::ValueOrigin::Variable {
1676 kind: runmat_accelerate::graph::VarKind::Global,
1677 index: 0,
1678 },
1679 ty: runmat_builtins::Type::Num,
1680 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1681 constant: None,
1682 },
1683 runmat_accelerate::graph::ValueInfo {
1684 id: 1,
1685 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
1686 node: 0,
1687 output: 0,
1688 },
1689 ty: runmat_builtins::Type::Num,
1690 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1691 constant: None,
1692 },
1693 runmat_accelerate::graph::ValueInfo {
1694 id: 2,
1695 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
1696 node: 1,
1697 output: 0,
1698 },
1699 ty: runmat_builtins::Type::Num,
1700 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1701 constant: None,
1702 },
1703 ],
1704 var_bindings: std::collections::HashMap::new(),
1705 node_bindings: std::collections::HashMap::new(),
1706 };
1707 let instr_spans = vec![
1708 runmat_hir::Span { start: 10, end: 11 },
1709 runmat_hir::Span { start: 11, end: 12 },
1710 ];
1711 let candidates = vec![crate::bytecode::FusionCandidateGroup {
1712 id: 0,
1713 signal_count: 2,
1714 function: runmat_hir::FunctionId(0),
1715 block: runmat_mir::BasicBlockId(0),
1716 stmt_start: 0,
1717 stmt_end: 2,
1718 source_span: runmat_hir::Span { start: 10, end: 12 },
1719 }];
1720
1721 let windows = super::derive_semantic_fusion_instruction_windows(
1722 &[Instr::Add, Instr::ElemMul],
1723 &instr_spans,
1724 &candidates,
1725 );
1726 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
1727 assert_eq!(groups.len(), 1, "expected one semantic-driven fusion group");
1728 assert_eq!(groups[0].nodes, vec![0, 1]);
1729 assert_eq!(
1730 groups[0].kind,
1731 runmat_accelerate::fusion::FusionKind::ElementwiseChain
1732 );
1733 assert_eq!(
1734 groups[0].shape,
1735 runmat_accelerate::graph::ShapeInfo::Unknown
1736 );
1737 }
1738
1739 #[cfg(feature = "native-accel")]
1740 #[test]
1741 fn windows_fallback_to_empty_node_groups_when_mapping_drops_all_nodes() {
1742 let windows = vec![crate::bytecode::FusionInstructionWindow {
1743 span: runmat_accelerate::graph::InstrSpan { start: 7, end: 9 },
1744 kind: crate::bytecode::FusionInstructionKind::Elementwise,
1745 }];
1746 let groups = super::derive_semantic_fusion_groups_from_instruction_windows(&windows);
1747 assert_eq!(
1748 groups.len(),
1749 1,
1750 "semantic windows fallback should preserve executable-group scaffolding even when graph mapping is unavailable"
1751 );
1752 assert_eq!(
1753 groups[0].nodes,
1754 Vec::<runmat_accelerate::graph::NodeId>::new()
1755 );
1756 assert_eq!(groups[0].span.start, 7);
1757 assert_eq!(groups[0].span.end, 9);
1758 assert_eq!(
1759 groups[0].kind,
1760 runmat_accelerate::fusion::FusionKind::ElementwiseChain
1761 );
1762 }
1763
1764 #[cfg(feature = "native-accel")]
1765 #[test]
1766 fn windows_preserve_unmapped_windows_alongside_mapped_groups() {
1767 let accel_graph = runmat_accelerate::graph::AccelGraph {
1768 nodes: vec![runmat_accelerate::graph::AccelNode {
1769 id: 0,
1770 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
1771 runmat_accelerate::graph::PrimitiveOp::Add,
1772 ),
1773 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
1774 inputs: vec![0, 0],
1775 outputs: vec![1],
1776 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
1777 tags: vec![],
1778 }],
1779 values: vec![
1780 runmat_accelerate::graph::ValueInfo {
1781 id: 0,
1782 origin: runmat_accelerate::graph::ValueOrigin::Variable {
1783 kind: runmat_accelerate::graph::VarKind::Global,
1784 index: 0,
1785 },
1786 ty: runmat_builtins::Type::Num,
1787 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1788 constant: None,
1789 },
1790 runmat_accelerate::graph::ValueInfo {
1791 id: 1,
1792 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
1793 node: 0,
1794 output: 0,
1795 },
1796 ty: runmat_builtins::Type::Num,
1797 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1798 constant: None,
1799 },
1800 ],
1801 var_bindings: std::collections::HashMap::new(),
1802 node_bindings: std::collections::HashMap::new(),
1803 };
1804 let windows = vec![
1805 crate::bytecode::FusionInstructionWindow {
1806 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
1807 kind: crate::bytecode::FusionInstructionKind::Elementwise,
1808 },
1809 crate::bytecode::FusionInstructionWindow {
1810 span: runmat_accelerate::graph::InstrSpan { start: 5, end: 5 },
1811 kind: crate::bytecode::FusionInstructionKind::Elementwise,
1812 },
1813 ];
1814 let groups = super::derive_semantic_fusion_groups_preserving_unmapped_windows(
1815 &windows,
1816 &accel_graph,
1817 );
1818 assert_eq!(groups.len(), 2);
1819 assert_eq!(groups[0].nodes, vec![0]);
1820 assert_eq!(
1821 groups[1].nodes,
1822 Vec::<runmat_accelerate::graph::NodeId>::new()
1823 );
1824 assert_eq!(groups[0].span.start, 0);
1825 assert_eq!(groups[1].span.start, 5);
1826 }
1827
1828 #[cfg(feature = "native-accel")]
1829 #[test]
1830 fn windows_map_accel_nodes_without_semantic_tags() {
1831 let accel_graph = runmat_accelerate::graph::AccelGraph {
1832 nodes: vec![runmat_accelerate::graph::AccelNode {
1833 id: 0,
1834 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
1835 runmat_accelerate::graph::PrimitiveOp::Add,
1836 ),
1837 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
1838 inputs: vec![0, 0],
1839 outputs: vec![1],
1840 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
1841 tags: vec![],
1842 }],
1843 values: vec![
1844 runmat_accelerate::graph::ValueInfo {
1845 id: 0,
1846 origin: runmat_accelerate::graph::ValueOrigin::Variable {
1847 kind: runmat_accelerate::graph::VarKind::Global,
1848 index: 0,
1849 },
1850 ty: runmat_builtins::Type::Num,
1851 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1852 constant: None,
1853 },
1854 runmat_accelerate::graph::ValueInfo {
1855 id: 1,
1856 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
1857 node: 0,
1858 output: 0,
1859 },
1860 ty: runmat_builtins::Type::Num,
1861 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1862 constant: None,
1863 },
1864 ],
1865 var_bindings: std::collections::HashMap::new(),
1866 node_bindings: std::collections::HashMap::new(),
1867 };
1868 let windows = vec![crate::bytecode::FusionInstructionWindow {
1869 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
1870 kind: crate::bytecode::FusionInstructionKind::Elementwise,
1871 }];
1872 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
1873 assert_eq!(
1874 groups.len(),
1875 1,
1876 "semantic windows should still map accel nodes when graph semantic tags are absent"
1877 );
1878 assert_eq!(groups[0].nodes, vec![0]);
1879 assert_eq!(
1880 groups[0].kind,
1881 runmat_accelerate::fusion::FusionKind::ElementwiseChain
1882 );
1883 }
1884
1885 #[cfg(feature = "native-accel")]
1886 #[test]
1887 fn windows_without_tags_reject_category_mismatch() {
1888 let accel_graph = runmat_accelerate::graph::AccelGraph {
1889 nodes: vec![runmat_accelerate::graph::AccelNode {
1890 id: 0,
1891 label: runmat_accelerate::graph::AccelNodeLabel::Builtin {
1892 name: "sum".to_string(),
1893 },
1894 category: runmat_accelerate::graph::AccelOpCategory::Reduction,
1895 inputs: vec![0],
1896 outputs: vec![1],
1897 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
1898 tags: vec![],
1899 }],
1900 values: vec![
1901 runmat_accelerate::graph::ValueInfo {
1902 id: 0,
1903 origin: runmat_accelerate::graph::ValueOrigin::Variable {
1904 kind: runmat_accelerate::graph::VarKind::Global,
1905 index: 0,
1906 },
1907 ty: runmat_builtins::Type::Num,
1908 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1909 constant: None,
1910 },
1911 runmat_accelerate::graph::ValueInfo {
1912 id: 1,
1913 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
1914 node: 0,
1915 output: 0,
1916 },
1917 ty: runmat_builtins::Type::Num,
1918 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1919 constant: None,
1920 },
1921 ],
1922 var_bindings: std::collections::HashMap::new(),
1923 node_bindings: std::collections::HashMap::new(),
1924 };
1925 let windows = vec![crate::bytecode::FusionInstructionWindow {
1926 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
1927 kind: crate::bytecode::FusionInstructionKind::Elementwise,
1928 }];
1929 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
1930 assert!(
1931 groups.is_empty(),
1932 "missing tags should not bypass category mismatch for semantic window mapping"
1933 );
1934 }
1935
1936 #[cfg(feature = "native-accel")]
1937 #[test]
1938 fn windows_reject_covering_node_span_at_compile_mapping_stage() {
1939 let accel_graph = runmat_accelerate::graph::AccelGraph {
1940 nodes: vec![runmat_accelerate::graph::AccelNode {
1941 id: 0,
1942 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
1943 runmat_accelerate::graph::PrimitiveOp::Add,
1944 ),
1945 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
1946 inputs: vec![0, 0],
1947 outputs: vec![1],
1948 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 2 },
1949 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
1950 }],
1951 values: vec![
1952 runmat_accelerate::graph::ValueInfo {
1953 id: 0,
1954 origin: runmat_accelerate::graph::ValueOrigin::Variable {
1955 kind: runmat_accelerate::graph::VarKind::Global,
1956 index: 0,
1957 },
1958 ty: runmat_builtins::Type::Num,
1959 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1960 constant: None,
1961 },
1962 runmat_accelerate::graph::ValueInfo {
1963 id: 1,
1964 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
1965 node: 0,
1966 output: 0,
1967 },
1968 ty: runmat_builtins::Type::Num,
1969 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
1970 constant: None,
1971 },
1972 ],
1973 var_bindings: std::collections::HashMap::new(),
1974 node_bindings: std::collections::HashMap::new(),
1975 };
1976 let windows = vec![crate::bytecode::FusionInstructionWindow {
1977 span: runmat_accelerate::graph::InstrSpan { start: 1, end: 1 },
1978 kind: crate::bytecode::FusionInstructionKind::Elementwise,
1979 }];
1980 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
1981 assert!(
1982 groups.is_empty(),
1983 "compile-time mapping should reject covering node spans and defer reconciliation to runtime sanitization"
1984 );
1985 }
1986
1987 #[cfg(feature = "native-accel")]
1988 #[test]
1989 fn windows_reject_overly_wide_covering_node_spans() {
1990 let accel_graph = runmat_accelerate::graph::AccelGraph {
1991 nodes: vec![runmat_accelerate::graph::AccelNode {
1992 id: 0,
1993 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
1994 runmat_accelerate::graph::PrimitiveOp::Add,
1995 ),
1996 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
1997 inputs: vec![0, 0],
1998 outputs: vec![1],
1999 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 4 },
2000 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
2001 }],
2002 values: vec![
2003 runmat_accelerate::graph::ValueInfo {
2004 id: 0,
2005 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2006 kind: runmat_accelerate::graph::VarKind::Global,
2007 index: 0,
2008 },
2009 ty: runmat_builtins::Type::Num,
2010 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2011 constant: None,
2012 },
2013 runmat_accelerate::graph::ValueInfo {
2014 id: 1,
2015 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2016 node: 0,
2017 output: 0,
2018 },
2019 ty: runmat_builtins::Type::Num,
2020 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2021 constant: None,
2022 },
2023 ],
2024 var_bindings: std::collections::HashMap::new(),
2025 node_bindings: std::collections::HashMap::new(),
2026 };
2027 let windows = vec![crate::bytecode::FusionInstructionWindow {
2028 span: runmat_accelerate::graph::InstrSpan { start: 2, end: 2 },
2029 kind: crate::bytecode::FusionInstructionKind::Elementwise,
2030 }];
2031 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2032 assert!(
2033 groups.is_empty(),
2034 "semantic windows should reject overly broad covering node spans"
2035 );
2036 }
2037
2038 #[cfg(feature = "native-accel")]
2039 #[test]
2040 fn windows_reject_partial_overlap_at_compile_mapping_stage() {
2041 let accel_graph = runmat_accelerate::graph::AccelGraph {
2042 nodes: vec![runmat_accelerate::graph::AccelNode {
2043 id: 0,
2044 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
2045 runmat_accelerate::graph::PrimitiveOp::Add,
2046 ),
2047 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
2048 inputs: vec![0, 0],
2049 outputs: vec![1],
2050 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 1 },
2051 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
2052 }],
2053 values: vec![
2054 runmat_accelerate::graph::ValueInfo {
2055 id: 0,
2056 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2057 kind: runmat_accelerate::graph::VarKind::Global,
2058 index: 0,
2059 },
2060 ty: runmat_builtins::Type::Num,
2061 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2062 constant: None,
2063 },
2064 runmat_accelerate::graph::ValueInfo {
2065 id: 1,
2066 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2067 node: 0,
2068 output: 0,
2069 },
2070 ty: runmat_builtins::Type::Num,
2071 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2072 constant: None,
2073 },
2074 ],
2075 var_bindings: std::collections::HashMap::new(),
2076 node_bindings: std::collections::HashMap::new(),
2077 };
2078 let windows = vec![crate::bytecode::FusionInstructionWindow {
2079 span: runmat_accelerate::graph::InstrSpan { start: 1, end: 2 },
2080 kind: crate::bytecode::FusionInstructionKind::Elementwise,
2081 }];
2082 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2083 assert!(
2084 groups.is_empty(),
2085 "compile-time mapping should reject partial overlap and defer reconciliation to runtime sanitization"
2086 );
2087 }
2088
2089 #[cfg(feature = "native-accel")]
2090 #[test]
2091 fn windows_reject_partial_overlap_with_large_boundary_shift() {
2092 let accel_graph = runmat_accelerate::graph::AccelGraph {
2093 nodes: vec![runmat_accelerate::graph::AccelNode {
2094 id: 0,
2095 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
2096 runmat_accelerate::graph::PrimitiveOp::Add,
2097 ),
2098 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
2099 inputs: vec![0, 0],
2100 outputs: vec![1],
2101 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 2 },
2102 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
2103 }],
2104 values: vec![
2105 runmat_accelerate::graph::ValueInfo {
2106 id: 0,
2107 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2108 kind: runmat_accelerate::graph::VarKind::Global,
2109 index: 0,
2110 },
2111 ty: runmat_builtins::Type::Num,
2112 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2113 constant: None,
2114 },
2115 runmat_accelerate::graph::ValueInfo {
2116 id: 1,
2117 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2118 node: 0,
2119 output: 0,
2120 },
2121 ty: runmat_builtins::Type::Num,
2122 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2123 constant: None,
2124 },
2125 ],
2126 var_bindings: std::collections::HashMap::new(),
2127 node_bindings: std::collections::HashMap::new(),
2128 };
2129 let windows = vec![crate::bytecode::FusionInstructionWindow {
2130 span: runmat_accelerate::graph::InstrSpan { start: 2, end: 3 },
2131 kind: crate::bytecode::FusionInstructionKind::Elementwise,
2132 }];
2133 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2134 assert!(
2135 groups.is_empty(),
2136 "semantic windows should reject partial overlap when boundary shift exceeds tolerance"
2137 );
2138 }
2139
2140 #[cfg(feature = "native-accel")]
2141 #[test]
2142 fn windows_reject_disjoint_gap_at_compile_mapping_stage() {
2143 let accel_graph = runmat_accelerate::graph::AccelGraph {
2144 nodes: vec![runmat_accelerate::graph::AccelNode {
2145 id: 0,
2146 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
2147 runmat_accelerate::graph::PrimitiveOp::Add,
2148 ),
2149 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
2150 inputs: vec![0, 0],
2151 outputs: vec![1],
2152 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2153 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
2154 }],
2155 values: vec![
2156 runmat_accelerate::graph::ValueInfo {
2157 id: 0,
2158 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2159 kind: runmat_accelerate::graph::VarKind::Global,
2160 index: 0,
2161 },
2162 ty: runmat_builtins::Type::Num,
2163 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2164 constant: None,
2165 },
2166 runmat_accelerate::graph::ValueInfo {
2167 id: 1,
2168 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2169 node: 0,
2170 output: 0,
2171 },
2172 ty: runmat_builtins::Type::Num,
2173 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2174 constant: None,
2175 },
2176 ],
2177 var_bindings: std::collections::HashMap::new(),
2178 node_bindings: std::collections::HashMap::new(),
2179 };
2180 let windows = vec![crate::bytecode::FusionInstructionWindow {
2181 span: runmat_accelerate::graph::InstrSpan { start: 1, end: 1 },
2182 kind: crate::bytecode::FusionInstructionKind::Elementwise,
2183 }];
2184 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2185 assert!(
2186 groups.is_empty(),
2187 "compile-time mapping should reject disjoint graph/window spans and leave reconciliation to runtime sanitization"
2188 );
2189 }
2190
2191 #[cfg(feature = "native-accel")]
2192 #[test]
2193 fn windows_reject_accel_nodes_with_large_disjoint_gap() {
2194 let accel_graph = runmat_accelerate::graph::AccelGraph {
2195 nodes: vec![runmat_accelerate::graph::AccelNode {
2196 id: 0,
2197 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
2198 runmat_accelerate::graph::PrimitiveOp::Add,
2199 ),
2200 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
2201 inputs: vec![0, 0],
2202 outputs: vec![1],
2203 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2204 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
2205 }],
2206 values: vec![
2207 runmat_accelerate::graph::ValueInfo {
2208 id: 0,
2209 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2210 kind: runmat_accelerate::graph::VarKind::Global,
2211 index: 0,
2212 },
2213 ty: runmat_builtins::Type::Num,
2214 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2215 constant: None,
2216 },
2217 runmat_accelerate::graph::ValueInfo {
2218 id: 1,
2219 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2220 node: 0,
2221 output: 0,
2222 },
2223 ty: runmat_builtins::Type::Num,
2224 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2225 constant: None,
2226 },
2227 ],
2228 var_bindings: std::collections::HashMap::new(),
2229 node_bindings: std::collections::HashMap::new(),
2230 };
2231 let windows = vec![crate::bytecode::FusionInstructionWindow {
2232 span: runmat_accelerate::graph::InstrSpan { start: 3, end: 3 },
2233 kind: crate::bytecode::FusionInstructionKind::Elementwise,
2234 }];
2235 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2236 assert!(
2237 groups.is_empty(),
2238 "semantic windows should reject accel-node mapping when disjoint span gap exceeds tolerance"
2239 );
2240 }
2241
2242 #[cfg(feature = "native-accel")]
2243 #[test]
2244 fn window_kind_is_not_overridden_by_graph_category() {
2245 let accel_graph = runmat_accelerate::graph::AccelGraph {
2246 nodes: vec![runmat_accelerate::graph::AccelNode {
2247 id: 0,
2248 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
2249 runmat_accelerate::graph::PrimitiveOp::Add,
2250 ),
2251 category: runmat_accelerate::graph::AccelOpCategory::Reduction,
2252 inputs: vec![0, 0],
2253 outputs: vec![1],
2254 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2255 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
2256 }],
2257 values: vec![
2258 runmat_accelerate::graph::ValueInfo {
2259 id: 0,
2260 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2261 kind: runmat_accelerate::graph::VarKind::Global,
2262 index: 0,
2263 },
2264 ty: runmat_builtins::Type::Num,
2265 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2266 constant: None,
2267 },
2268 runmat_accelerate::graph::ValueInfo {
2269 id: 1,
2270 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2271 node: 0,
2272 output: 0,
2273 },
2274 ty: runmat_builtins::Type::Num,
2275 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2276 constant: None,
2277 },
2278 ],
2279 var_bindings: std::collections::HashMap::new(),
2280 node_bindings: std::collections::HashMap::new(),
2281 };
2282 let instr_spans = vec![runmat_hir::Span { start: 10, end: 11 }];
2283 let candidates = vec![crate::bytecode::FusionCandidateGroup {
2284 id: 0,
2285 signal_count: 1,
2286 function: runmat_hir::FunctionId(0),
2287 block: runmat_mir::BasicBlockId(0),
2288 stmt_start: 0,
2289 stmt_end: 1,
2290 source_span: runmat_hir::Span { start: 10, end: 11 },
2291 }];
2292
2293 let windows = super::derive_semantic_fusion_instruction_windows(
2294 &[Instr::Add],
2295 &instr_spans,
2296 &candidates,
2297 );
2298 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2299 assert_eq!(groups.len(), 1, "expected one semantic fusion group");
2300 assert_eq!(
2301 groups[0].kind,
2302 runmat_accelerate::fusion::FusionKind::ElementwiseChain,
2303 "semantic instruction-window kind should drive fusion kind classification"
2304 );
2305 }
2306
2307 #[cfg(feature = "native-accel")]
2308 #[test]
2309 fn candidates_build_fusion_groups_from_transpose_nodes() {
2310 let accel_graph = runmat_accelerate::graph::AccelGraph {
2311 nodes: vec![runmat_accelerate::graph::AccelNode {
2312 id: 0,
2313 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
2314 runmat_accelerate::graph::PrimitiveOp::Transpose,
2315 ),
2316 category: runmat_accelerate::graph::AccelOpCategory::Transpose,
2317 inputs: vec![0],
2318 outputs: vec![1],
2319 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2320 tags: vec![runmat_accelerate::graph::AccelGraphTag::Transpose],
2321 }],
2322 values: vec![
2323 runmat_accelerate::graph::ValueInfo {
2324 id: 0,
2325 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2326 kind: runmat_accelerate::graph::VarKind::Global,
2327 index: 0,
2328 },
2329 ty: runmat_builtins::Type::Num,
2330 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2331 constant: None,
2332 },
2333 runmat_accelerate::graph::ValueInfo {
2334 id: 1,
2335 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2336 node: 0,
2337 output: 0,
2338 },
2339 ty: runmat_builtins::Type::Num,
2340 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2341 constant: None,
2342 },
2343 ],
2344 var_bindings: std::collections::HashMap::new(),
2345 node_bindings: std::collections::HashMap::new(),
2346 };
2347 let instr_spans = vec![runmat_hir::Span { start: 10, end: 11 }];
2348 let candidates = vec![crate::bytecode::FusionCandidateGroup {
2349 id: 0,
2350 signal_count: 1,
2351 function: runmat_hir::FunctionId(0),
2352 block: runmat_mir::BasicBlockId(0),
2353 stmt_start: 0,
2354 stmt_end: 1,
2355 source_span: runmat_hir::Span { start: 10, end: 11 },
2356 }];
2357
2358 let windows = super::derive_semantic_fusion_instruction_windows(
2359 &[Instr::Transpose],
2360 &instr_spans,
2361 &candidates,
2362 );
2363 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2364 assert_eq!(
2365 groups.len(),
2366 1,
2367 "expected transpose-tagged accel node to participate in semantic fusion-group mapping"
2368 );
2369 assert_eq!(groups[0].nodes, vec![0]);
2370 assert_eq!(
2371 groups[0].kind,
2372 runmat_accelerate::fusion::FusionKind::ElementwiseChain
2373 );
2374 }
2375
2376 #[cfg(feature = "native-accel")]
2377 #[test]
2378 fn elementwise_window_excludes_reduction_nodes() {
2379 let accel_graph = runmat_accelerate::graph::AccelGraph {
2380 nodes: vec![runmat_accelerate::graph::AccelNode {
2381 id: 0,
2382 label: runmat_accelerate::graph::AccelNodeLabel::Builtin {
2383 name: "sum".to_string(),
2384 },
2385 category: runmat_accelerate::graph::AccelOpCategory::Reduction,
2386 inputs: vec![0],
2387 outputs: vec![1],
2388 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2389 tags: vec![runmat_accelerate::graph::AccelGraphTag::Reduction],
2390 }],
2391 values: vec![
2392 runmat_accelerate::graph::ValueInfo {
2393 id: 0,
2394 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2395 kind: runmat_accelerate::graph::VarKind::Global,
2396 index: 0,
2397 },
2398 ty: runmat_builtins::Type::Num,
2399 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2400 constant: None,
2401 },
2402 runmat_accelerate::graph::ValueInfo {
2403 id: 1,
2404 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2405 node: 0,
2406 output: 0,
2407 },
2408 ty: runmat_builtins::Type::Num,
2409 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2410 constant: None,
2411 },
2412 ],
2413 var_bindings: std::collections::HashMap::new(),
2414 node_bindings: std::collections::HashMap::new(),
2415 };
2416 let windows = vec![crate::bytecode::FusionInstructionWindow {
2417 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2418 kind: crate::bytecode::FusionInstructionKind::Elementwise,
2419 }];
2420 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2421 assert!(
2422 groups.is_empty(),
2423 "elementwise semantic windows should not absorb reduction-tagged accel nodes"
2424 );
2425 }
2426
2427 #[cfg(feature = "native-accel")]
2428 #[test]
2429 fn reduction_window_accepts_reduction_nodes() {
2430 let accel_graph = runmat_accelerate::graph::AccelGraph {
2431 nodes: vec![runmat_accelerate::graph::AccelNode {
2432 id: 0,
2433 label: runmat_accelerate::graph::AccelNodeLabel::Builtin {
2434 name: "sum".to_string(),
2435 },
2436 category: runmat_accelerate::graph::AccelOpCategory::Reduction,
2437 inputs: vec![0],
2438 outputs: vec![1],
2439 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2440 tags: vec![runmat_accelerate::graph::AccelGraphTag::Reduction],
2441 }],
2442 values: vec![
2443 runmat_accelerate::graph::ValueInfo {
2444 id: 0,
2445 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2446 kind: runmat_accelerate::graph::VarKind::Global,
2447 index: 0,
2448 },
2449 ty: runmat_builtins::Type::Num,
2450 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2451 constant: None,
2452 },
2453 runmat_accelerate::graph::ValueInfo {
2454 id: 1,
2455 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2456 node: 0,
2457 output: 0,
2458 },
2459 ty: runmat_builtins::Type::Num,
2460 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2461 constant: None,
2462 },
2463 ],
2464 var_bindings: std::collections::HashMap::new(),
2465 node_bindings: std::collections::HashMap::new(),
2466 };
2467 let windows = vec![crate::bytecode::FusionInstructionWindow {
2468 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2469 kind: crate::bytecode::FusionInstructionKind::Reduction,
2470 }];
2471 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2472 assert_eq!(
2473 groups.len(),
2474 1,
2475 "reduction semantic windows should include reduction-tagged accel nodes"
2476 );
2477 assert_eq!(groups[0].nodes, vec![0]);
2478 assert_eq!(
2479 groups[0].kind,
2480 runmat_accelerate::fusion::FusionKind::Reduction
2481 );
2482 }
2483
2484 #[cfg(feature = "native-accel")]
2485 #[test]
2486 fn candidate_instruction_windows_split_on_non_accel_ops() {
2487 let instructions = vec![Instr::Add, Instr::LoadConst(1.0), Instr::ElemMul];
2488 let instr_spans = vec![
2489 runmat_hir::Span { start: 10, end: 11 },
2490 runmat_hir::Span { start: 11, end: 12 },
2491 runmat_hir::Span { start: 12, end: 13 },
2492 ];
2493 let candidates = vec![crate::bytecode::FusionCandidateGroup {
2494 id: 0,
2495 signal_count: 3,
2496 function: runmat_hir::FunctionId(0),
2497 block: runmat_mir::BasicBlockId(0),
2498 stmt_start: 0,
2499 stmt_end: 3,
2500 source_span: runmat_hir::Span { start: 10, end: 13 },
2501 }];
2502
2503 let windows = super::derive_semantic_fusion_instruction_windows(
2504 &instructions,
2505 &instr_spans,
2506 &candidates,
2507 );
2508 assert_eq!(
2509 windows.len(),
2510 2,
2511 "expected non-accel instruction boundary to split semantic instruction windows"
2512 );
2513 assert_eq!(windows[0].span.start, 0);
2514 assert_eq!(windows[0].span.end, 0);
2515 assert_eq!(windows[1].span.start, 2);
2516 assert_eq!(windows[1].span.end, 2);
2517 }
2518
2519 #[cfg(feature = "native-accel")]
2520 #[test]
2521 fn candidates_without_overlap_do_not_build_fusion_groups() {
2522 let accel_graph = runmat_accelerate::graph::AccelGraph {
2523 nodes: vec![runmat_accelerate::graph::AccelNode {
2524 id: 0,
2525 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
2526 runmat_accelerate::graph::PrimitiveOp::Add,
2527 ),
2528 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
2529 inputs: vec![0, 0],
2530 outputs: vec![1],
2531 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2532 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
2533 }],
2534 values: vec![
2535 runmat_accelerate::graph::ValueInfo {
2536 id: 0,
2537 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2538 kind: runmat_accelerate::graph::VarKind::Global,
2539 index: 0,
2540 },
2541 ty: runmat_builtins::Type::Num,
2542 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2543 constant: None,
2544 },
2545 runmat_accelerate::graph::ValueInfo {
2546 id: 1,
2547 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2548 node: 0,
2549 output: 0,
2550 },
2551 ty: runmat_builtins::Type::Num,
2552 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2553 constant: None,
2554 },
2555 ],
2556 var_bindings: std::collections::HashMap::new(),
2557 node_bindings: std::collections::HashMap::new(),
2558 };
2559 let instr_spans = vec![runmat_hir::Span { start: 10, end: 11 }];
2560 let candidates = vec![crate::bytecode::FusionCandidateGroup {
2561 id: 0,
2562 signal_count: 1,
2563 function: runmat_hir::FunctionId(0),
2564 block: runmat_mir::BasicBlockId(0),
2565 stmt_start: 0,
2566 stmt_end: 1,
2567 source_span: runmat_hir::Span {
2568 start: 100,
2569 end: 101,
2570 },
2571 }];
2572
2573 let windows = super::derive_semantic_fusion_instruction_windows(
2574 &[Instr::Add],
2575 &instr_spans,
2576 &candidates,
2577 );
2578 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2579 assert!(
2580 groups.is_empty(),
2581 "expected no semantic-driven fusion groups when candidate spans do not overlap instruction source spans"
2582 );
2583 }
2584
2585 #[cfg(feature = "native-accel")]
2586 #[test]
2587 fn candidates_with_partial_overlap_do_not_build_fusion_groups() {
2588 let accel_graph = runmat_accelerate::graph::AccelGraph {
2589 nodes: vec![runmat_accelerate::graph::AccelNode {
2590 id: 0,
2591 label: runmat_accelerate::graph::AccelNodeLabel::Primitive(
2592 runmat_accelerate::graph::PrimitiveOp::Add,
2593 ),
2594 category: runmat_accelerate::graph::AccelOpCategory::Elementwise,
2595 inputs: vec![0, 0],
2596 outputs: vec![1],
2597 span: runmat_accelerate::graph::InstrSpan { start: 0, end: 0 },
2598 tags: vec![runmat_accelerate::graph::AccelGraphTag::Elementwise],
2599 }],
2600 values: vec![
2601 runmat_accelerate::graph::ValueInfo {
2602 id: 0,
2603 origin: runmat_accelerate::graph::ValueOrigin::Variable {
2604 kind: runmat_accelerate::graph::VarKind::Global,
2605 index: 0,
2606 },
2607 ty: runmat_builtins::Type::Num,
2608 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2609 constant: None,
2610 },
2611 runmat_accelerate::graph::ValueInfo {
2612 id: 1,
2613 origin: runmat_accelerate::graph::ValueOrigin::NodeOutput {
2614 node: 0,
2615 output: 0,
2616 },
2617 ty: runmat_builtins::Type::Num,
2618 shape: runmat_accelerate::graph::ShapeInfo::Scalar,
2619 constant: None,
2620 },
2621 ],
2622 var_bindings: std::collections::HashMap::new(),
2623 node_bindings: std::collections::HashMap::new(),
2624 };
2625 let instr_spans = vec![runmat_hir::Span { start: 10, end: 20 }];
2626 let candidates = vec![crate::bytecode::FusionCandidateGroup {
2627 id: 0,
2628 signal_count: 1,
2629 function: runmat_hir::FunctionId(0),
2630 block: runmat_mir::BasicBlockId(0),
2631 stmt_start: 0,
2632 stmt_end: 1,
2633 source_span: runmat_hir::Span { start: 19, end: 25 },
2634 }];
2635
2636 let windows = super::derive_semantic_fusion_instruction_windows(
2637 &[Instr::Add],
2638 &instr_spans,
2639 &candidates,
2640 );
2641 let groups = super::derive_semantic_fusion_groups_from_candidates(&windows, &accel_graph);
2642 assert!(
2643 groups.is_empty(),
2644 "expected no semantic-driven fusion groups when candidate spans only partially overlap instruction spans"
2645 );
2646 }
2647
2648 #[test]
2649 fn compile_records_semantic_spawn_site_metadata() {
2650 let ast = runmat_parser::parse("fut = make(); task = spawn(fut);").expect("parse");
2651 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2652 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2653 let entrypoint = hir.assembly.entrypoints[0].id;
2654
2655 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2656
2657 assert!(
2658 bytecode.async_metadata.mir_spawn_site_count > 0,
2659 "expected non-zero spawn site count"
2660 );
2661 assert!(
2662 !bytecode.async_metadata.mir_spawn_sites.is_empty(),
2663 "expected spawn site metadata entries"
2664 );
2665 assert_eq!(
2666 bytecode.async_metadata.mir_spawn_site_count,
2667 bytecode.async_metadata.mir_spawn_sites.len(),
2668 "spawn site count should match listed sites"
2669 );
2670 let unique_sites = bytecode
2671 .async_metadata
2672 .mir_spawn_sites
2673 .iter()
2674 .map(|site| (site.function, site.block, site.stmt_index))
2675 .collect::<std::collections::HashSet<_>>();
2676 assert!(
2677 unique_sites.len() == bytecode.async_metadata.mir_spawn_sites.len(),
2678 "spawn site metadata entries should be distinct"
2679 );
2680 assert_eq!(
2681 bytecode.async_metadata.runtime_model,
2682 crate::bytecode::program::AsyncRuntimeModel::LazyFutureDescriptorLane,
2683 "semantic async metadata should surface the current lazy-future runtime model"
2684 );
2685 assert_eq!(
2686 bytecode.async_metadata.mir_await_site_count, 0,
2687 "spawn-only program should not report await sites"
2688 );
2689 assert!(
2690 bytecode.async_metadata.mir_await_sites.is_empty(),
2691 "spawn-only program should have an empty await-site list"
2692 );
2693 }
2694
2695 #[test]
2696 fn compile_scopes_spawn_site_metadata_to_entrypoint_target() {
2697 let source = "x = 1; function z = helper(a); fut = make(); z = spawn(fut); end;";
2698 let ast = runmat_parser::parse(source).expect("parse");
2699 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2700 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2701 let entrypoint = hir.assembly.entrypoints[0].id;
2702 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2703
2704 assert_eq!(
2705 bytecode.async_metadata.mir_spawn_site_count, 0,
2706 "spawn sites in non-entrypoint helper bodies should not be attributed to the entrypoint bytecode artifact"
2707 );
2708 assert!(
2709 bytecode.async_metadata.mir_spawn_sites.is_empty(),
2710 "spawn site list should be empty when only helper bodies contain spawn expressions"
2711 );
2712 assert_eq!(
2713 bytecode.async_metadata.mir_await_site_count, 0,
2714 "program without entrypoint await should not report await sites"
2715 );
2716 }
2717
2718 #[test]
2719 fn compile_records_semantic_await_site_metadata() {
2720 let source = "async function y = inc(x); y = x + 1; end; t = inc(2); z = await(t);";
2721 let ast = runmat_parser::parse(source).expect("parse");
2722 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2723 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2724 let entrypoint = hir.assembly.entrypoints[0].id;
2725
2726 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2727
2728 assert!(
2729 bytecode.async_metadata.mir_await_site_count > 0,
2730 "expected non-zero await site count"
2731 );
2732 assert!(
2733 !bytecode.async_metadata.mir_await_sites.is_empty(),
2734 "expected await site metadata entries"
2735 );
2736 assert_eq!(
2737 bytecode.async_metadata.mir_await_site_count,
2738 bytecode.async_metadata.mir_await_sites.len(),
2739 "await site count should match listed sites"
2740 );
2741 let unique_sites = bytecode
2742 .async_metadata
2743 .mir_await_sites
2744 .iter()
2745 .map(|site| (site.function, site.block, site.resume))
2746 .collect::<std::collections::HashSet<_>>();
2747 assert!(
2748 unique_sites.len() == bytecode.async_metadata.mir_await_sites.len(),
2749 "await site metadata entries should be distinct"
2750 );
2751 assert_eq!(
2752 bytecode.async_metadata.runtime_model,
2753 crate::bytecode::program::AsyncRuntimeModel::LazyFutureDescriptorLane,
2754 "semantic async metadata should surface the current lazy-future runtime model"
2755 );
2756 }
2757
2758 #[test]
2759 fn compile_scopes_await_site_metadata_to_entrypoint_target() {
2760 let source = "x = 1; async function z = helper(a); z = await(a); end;";
2761 let ast = runmat_parser::parse(source).expect("parse");
2762 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2763 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2764 let entrypoint = hir.assembly.entrypoints[0].id;
2765 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2766
2767 assert_eq!(
2768 bytecode.async_metadata.mir_await_site_count, 0,
2769 "await sites in non-entrypoint helper bodies should not be attributed to the entrypoint bytecode artifact"
2770 );
2771 assert!(
2772 bytecode.async_metadata.mir_await_sites.is_empty(),
2773 "await site list should be empty when only helper bodies contain await expressions"
2774 );
2775 }
2776
2777 #[test]
2778 fn compile_interprets_visible_assignment() {
2779 let ast = runmat_parser::parse("x = 1 + 2;").expect("parse");
2780 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2781 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2782 let entrypoint = hir.assembly.entrypoints[0].id;
2783
2784 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2785 let layout = bytecode.layout.as_ref().expect("layout");
2786 let export = &layout.entrypoints[&entrypoint].exports[0];
2787
2788 assert_eq!(bytecode.var_names[&export.slot.0], "x");
2789
2790 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
2791 assert_eq!(vars[export.slot.0], Value::Num(3.0));
2792 }
2793
2794 #[test]
2795 fn compile_interprets_builtin_assignment() {
2796 let ast = runmat_parser::parse("x = sqrt(9);").expect("parse");
2797 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2798 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2799 let entrypoint = hir.assembly.entrypoints[0].id;
2800
2801 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2802 let layout = bytecode.layout.as_ref().expect("layout");
2803 let export = &layout.entrypoints[&entrypoint].exports[0];
2804
2805 assert!(matches!(
2806 bytecode.instructions.as_slice(),
2807 [
2808 Instr::LoadConst(9.0),
2809 Instr::CallBuiltinMulti(name, 1, 1),
2810 Instr::StoreVar(_),
2811 ] if name == "sqrt"
2812 ));
2813
2814 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
2815 assert_eq!(vars[export.slot.0], Value::Num(3.0));
2816 }
2817
2818 #[test]
2819 fn compile_interprets_uigetfile_cancel_destructuring() {
2820 let ast =
2821 runmat_parser::parse("[file, path] = uigetfile('*.xlsx', 'Select a spreadsheet');")
2822 .expect("parse");
2823 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2824 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2825 let entrypoint = hir.assembly.entrypoints[0].id;
2826
2827 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2828 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
2829 let file_slot = bytecode
2830 .var_names
2831 .iter()
2832 .find_map(|(slot, name)| (name == "file").then_some(*slot))
2833 .expect("file slot");
2834 let path_slot = bytecode
2835 .var_names
2836 .iter()
2837 .find_map(|(slot, name)| (name == "path").then_some(*slot))
2838 .expect("path slot");
2839
2840 assert_eq!(vars[file_slot], Value::Num(0.0));
2841 assert_eq!(vars[path_slot], Value::Num(0.0));
2842 }
2843
2844 #[test]
2845 fn compile_interprets_uiputfile_cancel_destructuring() {
2846 let ast = runmat_parser::parse("[file, path] = uiputfile('*.xlsx', 'Save spreadsheet');")
2847 .expect("parse");
2848 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2849 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2850 let entrypoint = hir.assembly.entrypoints[0].id;
2851
2852 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2853 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
2854 let file_slot = bytecode
2855 .var_names
2856 .iter()
2857 .find_map(|(slot, name)| (name == "file").then_some(*slot))
2858 .expect("file slot");
2859 let path_slot = bytecode
2860 .var_names
2861 .iter()
2862 .find_map(|(slot, name)| (name == "path").then_some(*slot))
2863 .expect("path slot");
2864
2865 assert_eq!(vars[file_slot], Value::Num(0.0));
2866 assert_eq!(vars[path_slot], Value::Num(0.0));
2867 }
2868
2869 #[test]
2870 fn compile_interprets_matrix_literal_assignment() {
2871 let ast = runmat_parser::parse("x = [1 2; 3 4];").expect("parse");
2872 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2873 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2874 let entrypoint = hir.assembly.entrypoints[0].id;
2875
2876 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2877 let layout = bytecode.layout.as_ref().expect("layout");
2878 let export = &layout.entrypoints[&entrypoint].exports[0];
2879
2880 assert!(matches!(
2881 bytecode.instructions.as_slice(),
2882 [
2883 Instr::LoadConst(1.0),
2884 Instr::LoadConst(2.0),
2885 Instr::LoadConst(3.0),
2886 Instr::LoadConst(4.0),
2887 Instr::CreateMatrix(2, 2),
2888 Instr::StoreVar(_),
2889 ]
2890 ));
2891
2892 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
2893 let Value::Tensor(tensor) = &vars[export.slot.0] else {
2894 panic!("expected tensor");
2895 };
2896 assert_eq!(tensor.shape, vec![2, 2]);
2897 assert_eq!(tensor.data, vec![1.0, 3.0, 2.0, 4.0]);
2898 }
2899
2900 #[test]
2901 fn compile_interprets_simple_matrix_indexing() {
2902 let ast = runmat_parser::parse("x = [1 2; 3 4]; y = x(2, 1);").expect("parse");
2903 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2904 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2905 let entrypoint = hir.assembly.entrypoints[0].id;
2906
2907 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2908 let layout = bytecode.layout.as_ref().expect("layout");
2909 let y_export = layout.entrypoints[&entrypoint]
2910 .exports
2911 .iter()
2912 .find(|export| export.name == "y")
2913 .expect("y export");
2914
2915 assert!(bytecode
2916 .instructions
2917 .iter()
2918 .any(|instr| matches!(instr, Instr::Index(2))));
2919 assert!(!bytecode
2920 .instructions
2921 .iter()
2922 .any(|instr| matches!(instr, Instr::IndexSlice(2, 2, 0, 0))));
2923
2924 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
2925 assert_eq!(vars[y_export.slot.0], Value::Num(3.0));
2926 }
2927
2928 #[test]
2929 fn compile_interprets_simple_colon_slice() {
2930 let ast = runmat_parser::parse("x = [1 2; 3 4]; y = x(:, 2);").expect("parse");
2931 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2932 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2933 let entrypoint = hir.assembly.entrypoints[0].id;
2934
2935 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2936 let layout = bytecode.layout.as_ref().expect("layout");
2937 let y_export = layout.entrypoints[&entrypoint]
2938 .exports
2939 .iter()
2940 .find(|export| export.name == "y")
2941 .expect("y export");
2942
2943 assert!(bytecode
2944 .instructions
2945 .iter()
2946 .any(|instr| matches!(instr, Instr::IndexSlice(2, 1, 1, 0))));
2947
2948 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
2949 let Value::Tensor(tensor) = &vars[y_export.slot.0] else {
2950 panic!("expected tensor");
2951 };
2952 assert_eq!(tensor.shape, vec![2, 1]);
2953 assert_eq!(tensor.data, vec![2.0, 4.0]);
2954 }
2955
2956 #[test]
2957 fn compile_lowers_ambiguous_local_index_to_slice() {
2958 let ast =
2959 runmat_parser::parse("x = [10 20 30 40]; a = find([0 1 1]); idx = a + 1; y = x(idx);")
2960 .expect("parse");
2961 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2962 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2963 let entrypoint = hir.assembly.entrypoints[0].id;
2964
2965 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2966 let layout = bytecode.layout.as_ref().expect("layout");
2967 let y_export = layout.entrypoints[&entrypoint]
2968 .exports
2969 .iter()
2970 .find(|export| export.name == "y")
2971 .expect("y export");
2972
2973 assert!(bytecode
2974 .instructions
2975 .iter()
2976 .any(|instr| matches!(instr, Instr::IndexSlice(1, _, _, _))));
2977 assert!(!bytecode
2978 .instructions
2979 .iter()
2980 .any(|instr| matches!(instr, Instr::Index(1))));
2981
2982 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
2983 let Value::Tensor(tensor) = &vars[y_export.slot.0] else {
2984 panic!("expected tensor");
2985 };
2986 assert_eq!(tensor.shape, vec![2, 1]);
2987 assert_eq!(tensor.data, vec![30.0, 40.0]);
2988 }
2989
2990 #[test]
2991 fn compile_lowers_ambiguous_local_store_index_to_slice() {
2992 let ast =
2993 runmat_parser::parse("x = [10 20 30 40]; idx = [2 4]; x(idx) = [9 8];").expect("parse");
2994 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
2995 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
2996 let entrypoint = hir.assembly.entrypoints[0].id;
2997
2998 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
2999 let layout = bytecode.layout.as_ref().expect("layout");
3000 let x_export = layout.entrypoints[&entrypoint]
3001 .exports
3002 .iter()
3003 .find(|export| export.name == "x")
3004 .expect("x export");
3005
3006 assert!(bytecode
3007 .instructions
3008 .iter()
3009 .any(|instr| matches!(instr, Instr::StoreSlice(1, _, _, _))));
3010 assert!(!bytecode
3011 .instructions
3012 .iter()
3013 .any(|instr| matches!(instr, Instr::StoreIndex(1))));
3014
3015 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
3016 let Value::Tensor(tensor) = &vars[x_export.slot.0] else {
3017 panic!("expected tensor");
3018 };
3019 assert_eq!(tensor.shape, vec![1, 4]);
3020 assert_eq!(tensor.data, vec![10.0, 9.0, 30.0, 8.0]);
3021 }
3022
3023 #[test]
3024 fn compile_rejects_invalid_scalar_index_plan_with_identifier() {
3025 let ast = runmat_parser::parse("x = [1 2 3]; y = x(2);").expect("parse");
3026 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3027 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3028 let entrypoint = hir.assembly.entrypoints[0].id;
3029 let function = hir.assembly.entrypoints[0].target;
3030 let body = mir.bodies.get_mut(&function).expect("entry body");
3031
3032 let mut patched = false;
3033 for block in &mut body.blocks {
3034 for stmt in &mut block.statements {
3035 if let MirStmtKind::Assign {
3036 value: MirRvalue::Index { indexing, .. },
3037 ..
3038 } = &mut stmt.kind
3039 {
3040 indexing.plan = MirIndexPlan::Scalar;
3041 indexing.components = vec![MirIndexComponent::Colon];
3042 patched = true;
3043 break;
3044 }
3045 }
3046 if patched {
3047 break;
3048 }
3049 }
3050 assert!(patched, "expected indexed assignment in lowered MIR");
3051
3052 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3053 assert_eq!(
3054 err.identifier.as_deref(),
3055 Some("RunMat:MirScalarIndexPlanInvalid")
3056 );
3057 }
3058
3059 #[test]
3060 fn compile_rejects_invalid_slice_index_plan_with_identifier() {
3061 let ast = runmat_parser::parse("x = [1 2 3]; y = x(end);").expect("parse");
3062 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3063 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3064 let entrypoint = hir.assembly.entrypoints[0].id;
3065 let function = hir.assembly.entrypoints[0].target;
3066 let body = mir.bodies.get_mut(&function).expect("entry body");
3067
3068 let mut patched = false;
3069 for block in &mut body.blocks {
3070 for stmt in &mut block.statements {
3071 if let MirStmtKind::Assign {
3072 value: MirRvalue::Index { indexing, .. },
3073 ..
3074 } = &mut stmt.kind
3075 {
3076 indexing.plan = MirIndexPlan::Slice;
3077 indexing.components = vec![MirIndexComponent::End {
3078 dim: Some(0),
3079 offset: 1,
3080 }];
3081 patched = true;
3082 break;
3083 }
3084 }
3085 if patched {
3086 break;
3087 }
3088 }
3089 assert!(patched, "expected indexed assignment in lowered MIR");
3090
3091 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3092 assert_eq!(
3093 err.identifier.as_deref(),
3094 Some("RunMat:MirSliceIndexPlanInvalid")
3095 );
3096 }
3097
3098 #[test]
3099 fn compile_rejects_slice_plan_selector_dimension_beyond_mask_width() {
3100 let ast = runmat_parser::parse("x = [1 2 3]; y = x(1);").expect("parse");
3101 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3102 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3103 let entrypoint = hir.assembly.entrypoints[0].id;
3104 let function = hir.assembly.entrypoints[0].target;
3105 let body = mir.bodies.get_mut(&function).expect("entry body");
3106
3107 let mut patched = false;
3108 for block in &mut body.blocks {
3109 for stmt in &mut block.statements {
3110 if let MirStmtKind::Assign {
3111 value: MirRvalue::Index { indexing, .. },
3112 ..
3113 } = &mut stmt.kind
3114 {
3115 indexing.plan = MirIndexPlan::Slice;
3116 let seed = indexing.components.first().cloned().expect("seed selector");
3117 let mut components = vec![seed; 33];
3118 components[32] = MirIndexComponent::Colon;
3119 indexing.components = components;
3120 patched = true;
3121 break;
3122 }
3123 }
3124 if patched {
3125 break;
3126 }
3127 }
3128 assert!(patched, "expected indexed assignment in lowered MIR");
3129
3130 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3131 assert_eq!(
3132 err.identifier.as_deref(),
3133 Some("RunMat:MirSliceIndexPlanInvalid")
3134 );
3135 }
3136
3137 #[test]
3138 fn compile_rejects_slice_plan_end_dimension_beyond_mask_width() {
3139 let ast = runmat_parser::parse("x = [1 2 3]; y = x(1);").expect("parse");
3140 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3141 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3142 let entrypoint = hir.assembly.entrypoints[0].id;
3143 let function = hir.assembly.entrypoints[0].target;
3144 let body = mir.bodies.get_mut(&function).expect("entry body");
3145
3146 let mut patched = false;
3147 for block in &mut body.blocks {
3148 for stmt in &mut block.statements {
3149 if let MirStmtKind::Assign {
3150 value: MirRvalue::Index { indexing, .. },
3151 ..
3152 } = &mut stmt.kind
3153 {
3154 indexing.plan = MirIndexPlan::Slice;
3155 let seed = indexing.components.first().cloned().expect("seed selector");
3156 let mut components = vec![seed; 33];
3157 components[32] = MirIndexComponent::End {
3158 dim: Some(32),
3159 offset: 0,
3160 };
3161 indexing.components = components;
3162 patched = true;
3163 break;
3164 }
3165 }
3166 if patched {
3167 break;
3168 }
3169 }
3170 assert!(patched, "expected indexed assignment in lowered MIR");
3171
3172 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3173 assert_eq!(
3174 err.identifier.as_deref(),
3175 Some("RunMat:MirSliceIndexPlanInvalid")
3176 );
3177 }
3178
3179 #[test]
3180 fn compile_rejects_scalar_plan_with_range_expr_component_with_identifier() {
3181 let ast = runmat_parser::parse("x = [1 2 3 4]; y = x(1:end);").expect("parse");
3182 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3183 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3184 let entrypoint = hir.assembly.entrypoints[0].id;
3185 let function = hir.assembly.entrypoints[0].target;
3186 let body = mir.bodies.get_mut(&function).expect("entry body");
3187
3188 let mut patched = false;
3189 for block in &mut body.blocks {
3190 for stmt in &mut block.statements {
3191 if let MirStmtKind::Assign {
3192 value: MirRvalue::Index { indexing, .. },
3193 ..
3194 } = &mut stmt.kind
3195 {
3196 indexing.plan = MirIndexPlan::Scalar;
3197 patched = true;
3198 break;
3199 }
3200 }
3201 if patched {
3202 break;
3203 }
3204 }
3205 assert!(patched, "expected indexed assignment in lowered MIR");
3206
3207 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3208 assert_eq!(
3209 err.identifier.as_deref(),
3210 Some("RunMat:MirScalarIndexPlanInvalid")
3211 );
3212 }
3213
3214 #[test]
3215 fn compile_rejects_slice_plan_with_range_expr_component_with_identifier() {
3216 let ast = runmat_parser::parse("x = [1 2 3 4]; y = x(1:end);").expect("parse");
3217 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3218 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3219 let entrypoint = hir.assembly.entrypoints[0].id;
3220 let function = hir.assembly.entrypoints[0].target;
3221 let body = mir.bodies.get_mut(&function).expect("entry body");
3222
3223 let mut patched = false;
3224 for block in &mut body.blocks {
3225 for stmt in &mut block.statements {
3226 if let MirStmtKind::Assign {
3227 value: MirRvalue::Index { indexing, .. },
3228 ..
3229 } = &mut stmt.kind
3230 {
3231 indexing.plan = MirIndexPlan::Slice;
3232 patched = true;
3233 break;
3234 }
3235 }
3236 if patched {
3237 break;
3238 }
3239 }
3240 assert!(patched, "expected indexed assignment in lowered MIR");
3241
3242 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3243 assert_eq!(
3244 err.identifier.as_deref(),
3245 Some("RunMat:MirSliceIndexPlanInvalid")
3246 );
3247 }
3248
3249 #[test]
3250 fn compile_rejects_invalid_paren_cell_plan_with_identifier() {
3251 let ast = runmat_parser::parse("x = [1 2 3]; y = x(2);").expect("parse");
3252 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3253 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3254 let entrypoint = hir.assembly.entrypoints[0].id;
3255 let function = hir.assembly.entrypoints[0].target;
3256 let body = mir.bodies.get_mut(&function).expect("entry body");
3257
3258 let mut patched = false;
3259 for block in &mut body.blocks {
3260 for stmt in &mut block.statements {
3261 if let MirStmtKind::Assign {
3262 value: MirRvalue::Index { indexing, .. },
3263 ..
3264 } = &mut stmt.kind
3265 {
3266 indexing.plan = MirIndexPlan::Cell;
3267 patched = true;
3268 break;
3269 }
3270 }
3271 if patched {
3272 break;
3273 }
3274 }
3275 assert!(patched, "expected indexed assignment in lowered MIR");
3276
3277 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3278 assert_eq!(
3279 err.identifier.as_deref(),
3280 Some("RunMat:MirParenCellPlanInvalid")
3281 );
3282 }
3283
3284 #[test]
3285 fn compile_rejects_index_assignment_with_read_context_identifier() {
3286 let ast = runmat_parser::parse("x=[1,2,3]; x(1)=4;").expect("parse");
3287 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3288 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3289 let entrypoint = hir.assembly.entrypoints[0].id;
3290 let function = hir.assembly.entrypoints[0].target;
3291 let body = mir.bodies.get_mut(&function).expect("entry body");
3292
3293 let mut patched = false;
3294 for block in &mut body.blocks {
3295 for stmt in &mut block.statements {
3296 if let MirStmtKind::Assign {
3297 place: MirPlace::Index(_, indexing),
3298 ..
3299 } = &mut stmt.kind
3300 {
3301 indexing.result_context = IndexResultContext::ReadSingle;
3302 patched = true;
3303 break;
3304 }
3305 }
3306 if patched {
3307 break;
3308 }
3309 }
3310 assert!(patched, "expected indexed assignment place in lowered MIR");
3311
3312 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3313 assert_eq!(
3314 err.identifier.as_deref(),
3315 Some("RunMat:MirIndexContextInvalid")
3316 );
3317 }
3318
3319 #[test]
3320 fn compile_rejects_index_assignment_with_deletion_context_identifier() {
3321 let ast = runmat_parser::parse("x=[1,2,3]; x(1)=4;").expect("parse");
3322 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3323 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3324 let entrypoint = hir.assembly.entrypoints[0].id;
3325 let function = hir.assembly.entrypoints[0].target;
3326 let body = mir.bodies.get_mut(&function).expect("entry body");
3327
3328 let mut patched = false;
3329 for block in &mut body.blocks {
3330 for stmt in &mut block.statements {
3331 if let MirStmtKind::Assign {
3332 place: MirPlace::Index(_, indexing),
3333 ..
3334 } = &mut stmt.kind
3335 {
3336 indexing.result_context = IndexResultContext::DeletionTarget;
3337 patched = true;
3338 break;
3339 }
3340 }
3341 if patched {
3342 break;
3343 }
3344 }
3345 assert!(patched, "expected indexed assignment place in lowered MIR");
3346
3347 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3348 assert_eq!(
3349 err.identifier.as_deref(),
3350 Some("RunMat:MirDeletionContextWithoutDeleteInvalid")
3351 );
3352 }
3353
3354 #[test]
3355 fn compile_rejects_invalid_cell_expand_all_shape_with_identifier() {
3356 let ast = runmat_parser::parse("c = {1,2;3,4}; [a,b] = c{:,2};").expect("parse");
3357 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3358 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3359 let entrypoint = hir.assembly.entrypoints[0].id;
3360 let function = hir.assembly.entrypoints[0].target;
3361 let body = mir.bodies.get_mut(&function).expect("entry body");
3362
3363 let mut patched = false;
3364 for block in &mut body.blocks {
3365 for stmt in &mut block.statements {
3366 if let MirStmtKind::MultiAssign {
3367 value: MirRvalue::Index { indexing, .. },
3368 ..
3369 } = &mut stmt.kind
3370 {
3371 indexing.cell_expand_all = true;
3372 patched = true;
3373 break;
3374 }
3375 }
3376 if patched {
3377 break;
3378 }
3379 }
3380 assert!(
3381 patched,
3382 "expected multi-assign cell expansion in lowered MIR"
3383 );
3384
3385 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3386 assert_eq!(
3387 err.identifier.as_deref(),
3388 Some("RunMat:MirCellExpandPlanInvalid")
3389 );
3390 }
3391
3392 #[test]
3393 fn compile_rejects_non_offset_end_expr_in_call_arg_cell_expansion_with_identifier() {
3394 let ast = runmat_parser::parse("c = {10, 20, 30, 40}; x = feval(@max, c{end/2}, 0);")
3395 .expect("parse");
3396 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3397 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3398 let entrypoint = hir.assembly.entrypoints[0].id;
3399
3400 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3401 assert_eq!(
3402 err.identifier.as_deref(),
3403 Some("RunMat:MirCellExpandPlanInvalid")
3404 );
3405 }
3406
3407 #[test]
3408 fn compile_rejects_invalid_mir_aggregate_shape_with_identifier() {
3409 let ast = runmat_parser::parse("x = [1 2 3];").expect("parse");
3410 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3411 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3412 let entrypoint = hir.assembly.entrypoints[0].id;
3413 let function = hir.assembly.entrypoints[0].target;
3414 let body = mir.bodies.get_mut(&function).expect("entry body");
3415
3416 let mut patched = false;
3417 for block in &mut body.blocks {
3418 for stmt in &mut block.statements {
3419 if let MirStmtKind::Assign {
3420 value: MirRvalue::Aggregate { rows, cols: _, .. },
3421 ..
3422 } = &mut stmt.kind
3423 {
3424 *rows = 2;
3425 patched = true;
3426 break;
3427 }
3428 }
3429 if patched {
3430 break;
3431 }
3432 }
3433 assert!(patched, "expected aggregate assignment in lowered MIR");
3434
3435 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3436 assert_eq!(
3437 err.identifier.as_deref(),
3438 Some("RunMat:MirAggregateShapeInvalid")
3439 );
3440 }
3441
3442 #[test]
3443 fn compile_rejects_invalid_cell_index_component_with_identifier() {
3444 let ast = runmat_parser::parse("c = {1}; c{1} = 2;").expect("parse");
3445 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3446 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3447 let entrypoint = hir.assembly.entrypoints[0].id;
3448 let function = hir.assembly.entrypoints[0].target;
3449 let body = mir.bodies.get_mut(&function).expect("entry body");
3450
3451 let mut patched = false;
3452 for block in &mut body.blocks {
3453 for stmt in &mut block.statements {
3454 if let MirStmtKind::Assign {
3455 place: MirPlace::Index(_, indexing),
3456 ..
3457 } = &mut stmt.kind
3458 {
3459 if matches!(indexing.kind, runmat_hir::IndexKind::Brace) {
3460 indexing.components = vec![MirIndexComponent::Colon];
3461 patched = true;
3462 break;
3463 }
3464 }
3465 }
3466 if patched {
3467 break;
3468 }
3469 }
3470 assert!(
3471 patched,
3472 "expected brace index assignment place in lowered MIR"
3473 );
3474
3475 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3476 assert_eq!(
3477 err.identifier.as_deref(),
3478 Some("RunMat:MirCellIndexPlanInvalid")
3479 );
3480 }
3481
3482 #[test]
3483 fn compile_rejects_cell_assignment_colon_selector_from_source_with_identifier() {
3484 let ast = runmat_parser::parse("c = {1,2;3,4}; c{:,2} = 9;").expect("parse");
3485 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3486 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3487 let entrypoint = hir.assembly.entrypoints[0].id;
3488
3489 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3490 assert_eq!(
3491 err.identifier.as_deref(),
3492 Some("RunMat:MirCellIndexPlanInvalid")
3493 );
3494 }
3495
3496 #[test]
3497 fn compile_rejects_mismatched_cell_index_context_with_identifier() {
3498 let ast = runmat_parser::parse("c = {1}; c{1} = 2;").expect("parse");
3499 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3500 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3501 let entrypoint = hir.assembly.entrypoints[0].id;
3502 let function = hir.assembly.entrypoints[0].target;
3503 let body = mir.bodies.get_mut(&function).expect("entry body");
3504
3505 let mut patched = false;
3506 for block in &mut body.blocks {
3507 for stmt in &mut block.statements {
3508 if let MirStmtKind::Assign {
3509 place: MirPlace::Index(_, indexing),
3510 ..
3511 } = &mut stmt.kind
3512 {
3513 if matches!(indexing.kind, runmat_hir::IndexKind::Brace) {
3514 indexing.result_context = runmat_hir::IndexResultContext::ReadSingle;
3515 patched = true;
3516 break;
3517 }
3518 }
3519 }
3520 if patched {
3521 break;
3522 }
3523 }
3524 assert!(
3525 patched,
3526 "expected brace index assignment place in lowered MIR"
3527 );
3528
3529 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3530 assert_eq!(
3531 err.identifier.as_deref(),
3532 Some("RunMat:MirIndexContextInvalid")
3533 );
3534 }
3535
3536 #[test]
3537 fn compile_rejects_member_store_back_brace_index_with_read_context_identifier() {
3538 let ast = runmat_parser::parse("c = {struct('x', 1)}; c{1}.x = 2;").expect("parse");
3539 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3540 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3541 let entrypoint = hir.assembly.entrypoints[0].id;
3542 let function = hir.assembly.entrypoints[0].target;
3543 let body = mir.bodies.get_mut(&function).expect("entry body");
3544
3545 let mut patched = false;
3546 for block in &mut body.blocks {
3547 for stmt in &mut block.statements {
3548 if let MirStmtKind::Assign {
3549 place: MirPlace::Member(base, _),
3550 ..
3551 } = &mut stmt.kind
3552 {
3553 if let MirPlace::Index(_, indexing) = base.as_mut() {
3554 if matches!(indexing.kind, runmat_hir::IndexKind::Brace) {
3555 indexing.result_context = runmat_hir::IndexResultContext::ReadSingle;
3556 patched = true;
3557 break;
3558 }
3559 }
3560 }
3561 }
3562 if patched {
3563 break;
3564 }
3565 }
3566 assert!(
3567 patched,
3568 "expected member-over-brace assignment in lowered MIR"
3569 );
3570
3571 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3572 assert_eq!(
3573 err.identifier.as_deref(),
3574 Some("RunMat:MirIndexContextInvalid")
3575 );
3576 }
3577
3578 #[test]
3579 fn compile_rejects_member_store_back_brace_index_with_deletion_context_identifier() {
3580 let ast = runmat_parser::parse("c = {struct('x', 1)}; c{1}.x = 2;").expect("parse");
3581 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3582 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3583 let entrypoint = hir.assembly.entrypoints[0].id;
3584 let function = hir.assembly.entrypoints[0].target;
3585 let body = mir.bodies.get_mut(&function).expect("entry body");
3586
3587 let mut patched = false;
3588 for block in &mut body.blocks {
3589 for stmt in &mut block.statements {
3590 if let MirStmtKind::Assign {
3591 place: MirPlace::Member(base, _),
3592 ..
3593 } = &mut stmt.kind
3594 {
3595 if let MirPlace::Index(_, indexing) = base.as_mut() {
3596 if matches!(indexing.kind, runmat_hir::IndexKind::Brace) {
3597 indexing.result_context =
3598 runmat_hir::IndexResultContext::DeletionTarget;
3599 patched = true;
3600 break;
3601 }
3602 }
3603 }
3604 }
3605 if patched {
3606 break;
3607 }
3608 }
3609 assert!(
3610 patched,
3611 "expected member-over-brace assignment in lowered MIR"
3612 );
3613
3614 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3615 assert_eq!(
3616 err.identifier.as_deref(),
3617 Some("RunMat:MirIndexContextInvalid")
3618 );
3619 }
3620
3621 #[test]
3622 fn compile_rejects_member_store_back_paren_index_with_read_context_identifier() {
3623 let ast = runmat_parser::parse("s = struct('x', {1, 2}); s(1).x = 3;").expect("parse");
3624 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3625 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3626 let entrypoint = hir.assembly.entrypoints[0].id;
3627 let function = hir.assembly.entrypoints[0].target;
3628 let body = mir.bodies.get_mut(&function).expect("entry body");
3629
3630 let mut patched = false;
3631 for block in &mut body.blocks {
3632 for stmt in &mut block.statements {
3633 if let MirStmtKind::Assign {
3634 place: MirPlace::Member(base, _),
3635 ..
3636 } = &mut stmt.kind
3637 {
3638 if let MirPlace::Index(_, indexing) = base.as_mut() {
3639 if matches!(indexing.kind, runmat_hir::IndexKind::Paren) {
3640 indexing.result_context = runmat_hir::IndexResultContext::ReadSingle;
3641 patched = true;
3642 break;
3643 }
3644 }
3645 }
3646 }
3647 if patched {
3648 break;
3649 }
3650 }
3651 assert!(
3652 patched,
3653 "expected member-over-paren assignment in lowered MIR"
3654 );
3655
3656 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3657 assert_eq!(
3658 err.identifier.as_deref(),
3659 Some("RunMat:MirIndexContextInvalid")
3660 );
3661 }
3662
3663 #[test]
3664 fn compile_rejects_member_store_back_paren_index_with_deletion_context_identifier() {
3665 let ast = runmat_parser::parse("s = struct('x', {1, 2}); s(1).x = 3;").expect("parse");
3666 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3667 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
3668 let entrypoint = hir.assembly.entrypoints[0].id;
3669 let function = hir.assembly.entrypoints[0].target;
3670 let body = mir.bodies.get_mut(&function).expect("entry body");
3671
3672 let mut patched = false;
3673 for block in &mut body.blocks {
3674 for stmt in &mut block.statements {
3675 if let MirStmtKind::Assign {
3676 place: MirPlace::Member(base, _),
3677 ..
3678 } = &mut stmt.kind
3679 {
3680 if let MirPlace::Index(_, indexing) = base.as_mut() {
3681 if matches!(indexing.kind, runmat_hir::IndexKind::Paren) {
3682 indexing.result_context =
3683 runmat_hir::IndexResultContext::DeletionTarget;
3684 patched = true;
3685 break;
3686 }
3687 }
3688 }
3689 }
3690 if patched {
3691 break;
3692 }
3693 }
3694 assert!(
3695 patched,
3696 "expected member-over-paren assignment in lowered MIR"
3697 );
3698
3699 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
3700 assert_eq!(
3701 err.identifier.as_deref(),
3702 Some("RunMat:MirIndexContextInvalid")
3703 );
3704 }
3705
3706 #[test]
3707 fn compile_interprets_member_store_back_paren_assignment() {
3708 let ast = runmat_parser::parse(
3709 "s = struct('x', {1, 2}); s(2).x = 9; t = s(2); y = getfield(t, 'x');",
3710 )
3711 .expect("parse");
3712 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3713 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3714 let entrypoint = hir.assembly.entrypoints[0].id;
3715
3716 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
3717 let layout = bytecode.layout.as_ref().expect("layout");
3718 let y_export = layout.entrypoints[&entrypoint]
3719 .exports
3720 .iter()
3721 .find(|export| export.name == "y")
3722 .expect("y export");
3723
3724 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
3725 assert_eq!(vars[y_export.slot.0], Value::Num(9.0));
3726 }
3727
3728 #[test]
3729 fn compile_interprets_readtable_weekly_groupsummary_workflow() {
3730 let path = unique_csv_temp_path("readtable_weekly");
3731 std::fs::write(
3732 &path,
3733 "Date,Orders,Revenue\n2024-03-11,10,100\n2024-03-12,20,300\n2024-03-18,6,90\n",
3734 )
3735 .expect("write csv");
3736 let source = format!(
3737 "\
3738T = readtable('{}');\n\
3739T.Date = datetime(T.Date, 'InputFormat', 'yyyy-MM-dd');\n\
3740T.Week = dateshift(T.Date, 'start', 'week');\n\
3741weekly = groupsummary(T, 'Week', 'mean', {{'Orders', 'Revenue'}});\n\
3742weekly.Properties.VariableNames(end-1:end) = {{'AvgOrders', 'AvgRevenue'}};\n\
3743weekly = sortrows(weekly, 'Week');\n\
3744out = weekly.AvgRevenue;\n",
3745 path.to_string_lossy()
3746 );
3747 let ast = runmat_parser::parse(&source).expect("parse");
3748 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3749 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3750 let entrypoint = hir.assembly.entrypoints[0].id;
3751
3752 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
3753 let layout = bytecode.layout.as_ref().expect("layout");
3754 let out_export = layout.entrypoints[&entrypoint]
3755 .exports
3756 .iter()
3757 .find(|export| export.name == "out")
3758 .expect("out export");
3759
3760 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
3761 let Value::Tensor(tensor) = &vars[out_export.slot.0] else {
3762 panic!(
3763 "expected numeric AvgRevenue tensor, got {:?}",
3764 vars[out_export.slot.0]
3765 );
3766 };
3767 assert_eq!(tensor.shape, vec![2, 1]);
3768 assert_eq!(tensor.data, vec![200.0, 90.0]);
3769 let _ = std::fs::remove_file(&path);
3770 }
3771
3772 #[test]
3773 fn compile_interprets_imhist_uint8_workflow() {
3774 let ast = runmat_parser::parse(
3775 "\
3776img = uint8([0 1 1; 2 2 2]);\n\
3777[counts, bins] = imhist(img);\n\
3778[peak, idx] = max(counts);\n\
3779dominant = bins(idx);\n\
3780total = sum(counts);\n\
3781summary = [peak; dominant; total];\n",
3782 )
3783 .expect("parse");
3784 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3785 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3786 let entrypoint = hir.assembly.entrypoints[0].id;
3787
3788 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
3789 let layout = bytecode.layout.as_ref().expect("layout");
3790 let summary_export = layout.entrypoints[&entrypoint]
3791 .exports
3792 .iter()
3793 .find(|export| export.name == "summary")
3794 .expect("summary export");
3795
3796 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
3797 let Value::Tensor(tensor) = &vars[summary_export.slot.0] else {
3798 panic!(
3799 "expected numeric summary tensor, got {:?}",
3800 vars[summary_export.slot.0]
3801 );
3802 };
3803 assert_eq!(tensor.shape, vec![3, 1]);
3804 assert_eq!(tensor.data, vec![3.0, 2.0, 6.0]);
3805 }
3806
3807 #[test]
3808 fn compile_interprets_butter_filter_workflow() {
3809 let ast = runmat_parser::parse(
3810 "\
3811[b, a] = butter(2, 0.25, 'low');\n\
3812x = [1 zeros(1, 5)];\n\
3813y = filter(b, a, x);\n\
3814expected = [0.0976310729378175 0.2873096041807672 0.3359654745135361];\n\
3815err = max(abs(y(1:3) - expected));\n\
3816summary = [numel(b); numel(a); all(isfinite(y)); err < 1e-12];\n",
3817 )
3818 .expect("parse");
3819 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3820 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3821 let entrypoint = hir.assembly.entrypoints[0].id;
3822
3823 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
3824 let layout = bytecode.layout.as_ref().expect("layout");
3825 let summary_export = layout.entrypoints[&entrypoint]
3826 .exports
3827 .iter()
3828 .find(|export| export.name == "summary")
3829 .expect("summary export");
3830
3831 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
3832 let Value::Tensor(tensor) = &vars[summary_export.slot.0] else {
3833 panic!(
3834 "expected numeric summary tensor, got {:?}",
3835 vars[summary_export.slot.0]
3836 );
3837 };
3838 assert_eq!(tensor.shape, vec![4, 1]);
3839 assert_eq!(tensor.data, vec![3.0, 3.0, 1.0, 1.0]);
3840 }
3841
3842 #[test]
3843 fn compile_interprets_rref_rank_workflow() {
3844 let ast = runmat_parser::parse(
3845 "\
3846A = [1 2 3; 2 4 6; 1 1 1];\n\
3847rankA = rank(A);\n\
3848[R, p] = rref(A);\n\
3849expected = [1 0 -1; 0 1 2; 0 0 0];\n\
3850expected_p = [1 2];\n\
3851err = max(max(abs(R - expected)));\n\
3852pivot_ok = all(p == expected_p);\n\
3853summary = [rankA; double(err < 1e-12); numel(p); double(pivot_ok)];\n",
3854 )
3855 .expect("parse");
3856 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3857 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3858 let entrypoint = hir.assembly.entrypoints[0].id;
3859
3860 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
3861 let layout = bytecode.layout.as_ref().expect("layout");
3862 let summary_export = layout.entrypoints[&entrypoint]
3863 .exports
3864 .iter()
3865 .find(|export| export.name == "summary")
3866 .expect("summary export");
3867
3868 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
3869 let Value::Tensor(tensor) = &vars[summary_export.slot.0] else {
3870 panic!(
3871 "expected numeric summary tensor, got {:?}",
3872 vars[summary_export.slot.0]
3873 );
3874 };
3875 assert_eq!(tensor.shape, vec![4, 1]);
3876 assert_eq!(tensor.data, vec![2.0, 1.0, 2.0, 1.0]);
3877 }
3878
3879 #[test]
3880 fn compile_interprets_symbolic_limit_workflow() {
3881 let ast = runmat_parser::parse(
3882 "\
3883syms x h\n\
3884syms('z')\n\
3885f1 = limit(sin(x)/x, x, 0);\n\
3886f2 = limit((cos(x+h) - cos(x))/h, h, 0);\n\
3887f3 = limit(sin(z)/z + 1, z, 0);\n",
3888 )
3889 .expect("parse");
3890 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3891 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3892 let entrypoint = hir.assembly.entrypoints[0].id;
3893
3894 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
3895 let layout = bytecode.layout.as_ref().expect("layout");
3896 let entry_layout = &layout.entrypoints[&entrypoint];
3897 let f1_export = entry_layout
3898 .exports
3899 .iter()
3900 .find(|export| export.name == "f1")
3901 .expect("f1 export");
3902 let f2_export = entry_layout
3903 .exports
3904 .iter()
3905 .find(|export| export.name == "f2")
3906 .expect("f2 export");
3907 let f3_export = entry_layout
3908 .exports
3909 .iter()
3910 .find(|export| export.name == "f3")
3911 .expect("f3 export");
3912
3913 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
3914 assert!(matches!(&vars[f1_export.slot.0], Value::Symbolic(_)));
3915 assert_eq!(vars[f1_export.slot.0].to_string(), "1");
3916 assert_eq!(vars[f2_export.slot.0].to_string(), "-sin(x)");
3917 assert_eq!(vars[f3_export.slot.0].to_string(), "2");
3918 }
3919
3920 #[test]
3921 fn compile_interprets_symbolic_function_declaration_workflow() {
3922 let ast = runmat_parser::parse(
3923 "\
3924syms Y(X);\n\
3925a = 0;\n\
3926z = 0;\n\
3927applied = Y(a);\n\
3928reapplied = applied(1);\n\
3929cond = applied == z;\n\
3930dydx = diff(Y, X);\n\
3931eqn = diff(Y, X) == 2*Y + X;\n\
3932syms('F(P, Q)');\n\
3933probe = F(1, 2);\n",
3934 )
3935 .expect("parse");
3936 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3937 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3938 let entrypoint = hir.assembly.entrypoints[0].id;
3939
3940 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
3941 let layout = bytecode.layout.as_ref().expect("layout");
3942 let entry_layout = &layout.entrypoints[&entrypoint];
3943 let export = |name: &str| {
3944 entry_layout
3945 .exports
3946 .iter()
3947 .find(|export| export.name == name)
3948 .unwrap_or_else(|| panic!("{name} export"))
3949 .slot
3950 .0
3951 };
3952
3953 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
3954 assert_eq!(vars[export("Y")].to_string(), "Y(X)");
3955 assert_eq!(vars[export("X")].to_string(), "X");
3956 assert_eq!(vars[export("applied")].to_string(), "Y(0)");
3957 assert_eq!(vars[export("reapplied")].to_string(), "Y(0)");
3958 assert_eq!(vars[export("cond")].to_string(), "Y(0) == 0");
3959 assert_eq!(vars[export("dydx")].to_string(), "diff(Y(X), X)");
3960 assert_eq!(
3961 vars[export("eqn")].to_string(),
3962 "diff(Y(X), X) == 2*Y(X) + X"
3963 );
3964 assert_eq!(vars[export("F")].to_string(), "F(P, Q)");
3965 assert_eq!(vars[export("P")].to_string(), "P");
3966 assert_eq!(vars[export("Q")].to_string(), "Q");
3967 assert_eq!(vars[export("probe")].to_string(), "F(1, 2)");
3968 }
3969
3970 #[test]
3971 fn compile_rejects_symbolic_function_arity_mismatch() {
3972 let ast = runmat_parser::parse(
3973 "\
3974syms Y(X);\n\
3975bad = Y(1, 2);\n",
3976 )
3977 .expect("parse");
3978 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
3979 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
3980 let entrypoint = hir.assembly.entrypoints[0].id;
3981
3982 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
3983 let err = block_on(crate::interpret(&bytecode)).expect_err("arity mismatch should fail");
3984
3985 assert_eq!(
3986 err.identifier.as_deref(),
3987 Some("RunMat:SymbolicFunctionArity")
3988 );
3989 }
3990
3991 #[test]
3992 fn compile_interprets_symbolic_fractional_power_limit() {
3993 let ast = runmat_parser::parse(
3994 "\
3995syms x\n\
3996f = (cos(x)^(1/3) - 1) / x^2;\n\
3997L = limit(f, x, 0);\n",
3998 )
3999 .expect("parse");
4000 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4001 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
4002 let entrypoint = hir.assembly.entrypoints[0].id;
4003
4004 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
4005 let layout = bytecode.layout.as_ref().expect("layout");
4006 let l_export = layout.entrypoints[&entrypoint]
4007 .exports
4008 .iter()
4009 .find(|export| export.name == "L")
4010 .expect("L export");
4011
4012 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
4013 let Value::Symbolic(expr) = &vars[l_export.slot.0] else {
4014 panic!(
4015 "expected symbolic limit result, got {:?}",
4016 vars[l_export.slot.0]
4017 );
4018 };
4019 let result = expr.constant_value().expect("constant limit result");
4020 assert!((result + 1.0 / 6.0).abs() < 1e-12, "{result}");
4021 }
4022
4023 #[test]
4024 fn compile_interprets_scalar_symbolic_power() {
4025 let ast = runmat_parser::parse(
4026 "\
4027syms x\n\
4028a = x^2;\n\
4029b = 2^x;\n",
4030 )
4031 .expect("parse");
4032 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4033 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
4034 let entrypoint = hir.assembly.entrypoints[0].id;
4035
4036 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
4037 let layout = bytecode.layout.as_ref().expect("layout");
4038 let entry_layout = &layout.entrypoints[&entrypoint];
4039 let a_export = entry_layout
4040 .exports
4041 .iter()
4042 .find(|export| export.name == "a")
4043 .expect("a export");
4044 let b_export = entry_layout
4045 .exports
4046 .iter()
4047 .find(|export| export.name == "b")
4048 .expect("b export");
4049
4050 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
4051 assert!(matches!(&vars[a_export.slot.0], Value::Symbolic(_)));
4052 assert!(matches!(&vars[b_export.slot.0], Value::Symbolic(_)));
4053 assert_eq!(vars[a_export.slot.0].to_string(), "x^2");
4054 assert_eq!(vars[b_export.slot.0].to_string(), "2^x");
4055 }
4056
4057 #[test]
4058 fn compile_interprets_nonscalar_symbolic_power_operand() {
4059 let ast = runmat_parser::parse(
4060 "\
4061syms x\n\
4062y = x^[1 2; 3 4];\n",
4063 )
4064 .expect("parse");
4065 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4066 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
4067 let entrypoint = hir.assembly.entrypoints[0].id;
4068
4069 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
4070 let layout = bytecode.layout.as_ref().expect("layout");
4071 let entry_layout = &layout.entrypoints[&entrypoint];
4072 let y_export = entry_layout
4073 .exports
4074 .iter()
4075 .find(|export| export.name == "y")
4076 .expect("y export");
4077
4078 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
4079 match &vars[y_export.slot.0] {
4080 Value::SymbolicArray(array) => {
4081 assert_eq!(array.shape, vec![2, 2]);
4082 assert_eq!(
4083 array
4084 .data
4085 .iter()
4086 .map(ToString::to_string)
4087 .collect::<Vec<_>>(),
4088 vec!["x", "x^3", "x^2", "x^4"]
4089 );
4090 }
4091 other => panic!("expected symbolic array, got {other:?}"),
4092 }
4093 }
4094
4095 #[test]
4096 fn compile_rejects_dynamic_member_store_back_paren_index_with_read_context_identifier() {
4097 let ast =
4098 runmat_parser::parse("s = struct('x', {1, 2}); f = 'x'; s(1).(f) = 3;").expect("parse");
4099 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4100 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4101 let entrypoint = hir.assembly.entrypoints[0].id;
4102 let function = hir.assembly.entrypoints[0].target;
4103 let body = mir.bodies.get_mut(&function).expect("entry body");
4104
4105 let mut patched = false;
4106 for block in &mut body.blocks {
4107 for stmt in &mut block.statements {
4108 if let MirStmtKind::Assign {
4109 place: MirPlace::DynamicMember(base, _),
4110 ..
4111 } = &mut stmt.kind
4112 {
4113 if let MirPlace::Index(_, indexing) = base.as_mut() {
4114 if matches!(indexing.kind, runmat_hir::IndexKind::Paren) {
4115 indexing.result_context = runmat_hir::IndexResultContext::ReadSingle;
4116 patched = true;
4117 break;
4118 }
4119 }
4120 }
4121 }
4122 if patched {
4123 break;
4124 }
4125 }
4126 assert!(
4127 patched,
4128 "expected dynamic-member-over-paren assignment in lowered MIR"
4129 );
4130
4131 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4132 assert_eq!(
4133 err.identifier.as_deref(),
4134 Some("RunMat:MirIndexContextInvalid")
4135 );
4136 }
4137
4138 #[test]
4139 fn compile_rejects_dynamic_member_store_back_paren_index_with_deletion_context_identifier() {
4140 let ast =
4141 runmat_parser::parse("s = struct('x', {1, 2}); f = 'x'; s(1).(f) = 3;").expect("parse");
4142 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4143 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4144 let entrypoint = hir.assembly.entrypoints[0].id;
4145 let function = hir.assembly.entrypoints[0].target;
4146 let body = mir.bodies.get_mut(&function).expect("entry body");
4147
4148 let mut patched = false;
4149 for block in &mut body.blocks {
4150 for stmt in &mut block.statements {
4151 if let MirStmtKind::Assign {
4152 place: MirPlace::DynamicMember(base, _),
4153 ..
4154 } = &mut stmt.kind
4155 {
4156 if let MirPlace::Index(_, indexing) = base.as_mut() {
4157 if matches!(indexing.kind, runmat_hir::IndexKind::Paren) {
4158 indexing.result_context =
4159 runmat_hir::IndexResultContext::DeletionTarget;
4160 patched = true;
4161 break;
4162 }
4163 }
4164 }
4165 }
4166 if patched {
4167 break;
4168 }
4169 }
4170 assert!(
4171 patched,
4172 "expected dynamic-member-over-paren assignment in lowered MIR"
4173 );
4174
4175 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4176 assert_eq!(
4177 err.identifier.as_deref(),
4178 Some("RunMat:MirIndexContextInvalid")
4179 );
4180 }
4181
4182 #[test]
4183 fn compile_interprets_dynamic_member_store_back_paren_assignment() {
4184 let ast = runmat_parser::parse(
4185 "s = struct('x', {1, 2}); f = 'x'; s(2).(f) = 9; t = s(2); y = getfield(t, f);",
4186 )
4187 .expect("parse");
4188 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4189 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
4190 let entrypoint = hir.assembly.entrypoints[0].id;
4191
4192 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
4193 let layout = bytecode.layout.as_ref().expect("layout");
4194 let y_export = layout.entrypoints[&entrypoint]
4195 .exports
4196 .iter()
4197 .find(|export| export.name == "y")
4198 .expect("y export");
4199
4200 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
4201 assert_eq!(vars[y_export.slot.0], Value::Num(9.0));
4202 }
4203
4204 #[test]
4205 fn compile_rejects_dynamic_member_store_back_brace_index_with_read_context_identifier() {
4206 let ast =
4207 runmat_parser::parse("c = {struct('x', 1)}; f = 'x'; c{1}.(f) = 3;").expect("parse");
4208 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4209 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4210 let entrypoint = hir.assembly.entrypoints[0].id;
4211 let function = hir.assembly.entrypoints[0].target;
4212 let body = mir.bodies.get_mut(&function).expect("entry body");
4213
4214 let mut patched = false;
4215 for block in &mut body.blocks {
4216 for stmt in &mut block.statements {
4217 if let MirStmtKind::Assign {
4218 place: MirPlace::DynamicMember(base, _),
4219 ..
4220 } = &mut stmt.kind
4221 {
4222 if let MirPlace::Index(_, indexing) = base.as_mut() {
4223 if matches!(indexing.kind, runmat_hir::IndexKind::Brace) {
4224 indexing.result_context = runmat_hir::IndexResultContext::ReadSingle;
4225 patched = true;
4226 break;
4227 }
4228 }
4229 }
4230 }
4231 if patched {
4232 break;
4233 }
4234 }
4235 assert!(
4236 patched,
4237 "expected dynamic-member-over-brace assignment in lowered MIR"
4238 );
4239
4240 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4241 assert_eq!(
4242 err.identifier.as_deref(),
4243 Some("RunMat:MirIndexContextInvalid")
4244 );
4245 }
4246
4247 #[test]
4248 fn compile_rejects_dynamic_member_store_back_brace_index_with_deletion_context_identifier() {
4249 let ast =
4250 runmat_parser::parse("c = {struct('x', 1)}; f = 'x'; c{1}.(f) = 3;").expect("parse");
4251 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4252 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4253 let entrypoint = hir.assembly.entrypoints[0].id;
4254 let function = hir.assembly.entrypoints[0].target;
4255 let body = mir.bodies.get_mut(&function).expect("entry body");
4256
4257 let mut patched = false;
4258 for block in &mut body.blocks {
4259 for stmt in &mut block.statements {
4260 if let MirStmtKind::Assign {
4261 place: MirPlace::DynamicMember(base, _),
4262 ..
4263 } = &mut stmt.kind
4264 {
4265 if let MirPlace::Index(_, indexing) = base.as_mut() {
4266 if matches!(indexing.kind, runmat_hir::IndexKind::Brace) {
4267 indexing.result_context =
4268 runmat_hir::IndexResultContext::DeletionTarget;
4269 patched = true;
4270 break;
4271 }
4272 }
4273 }
4274 }
4275 if patched {
4276 break;
4277 }
4278 }
4279 assert!(
4280 patched,
4281 "expected dynamic-member-over-brace assignment in lowered MIR"
4282 );
4283
4284 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4285 assert_eq!(
4286 err.identifier.as_deref(),
4287 Some("RunMat:MirIndexContextInvalid")
4288 );
4289 }
4290
4291 #[test]
4292 fn compile_interprets_dynamic_member_store_back_brace_assignment() {
4293 let ast = runmat_parser::parse(
4294 "c = {struct('x', 1)}; f = 'x'; c{1}.(f) = 9; t = c{1}; y = getfield(t, f);",
4295 )
4296 .expect("parse");
4297 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4298 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
4299 let entrypoint = hir.assembly.entrypoints[0].id;
4300
4301 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
4302 let layout = bytecode.layout.as_ref().expect("layout");
4303 let y_export = layout.entrypoints[&entrypoint]
4304 .exports
4305 .iter()
4306 .find(|export| export.name == "y")
4307 .expect("y export");
4308
4309 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
4310 assert_eq!(vars[y_export.slot.0], Value::Num(9.0));
4311 }
4312
4313 #[test]
4314 fn compile_interprets_dynamic_member_nested_index_delete_store_back() {
4315 let ast = runmat_parser::parse(
4316 "s = struct(); s.x = {1, 2, 3}; f = 'x'; s(1).(f)(2) = []; z = getfield(s(1), f); y = z{2};",
4317 )
4318 .expect("parse");
4319 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4320 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
4321 let entrypoint = hir.assembly.entrypoints[0].id;
4322
4323 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
4324 let layout = bytecode.layout.as_ref().expect("layout");
4325 let y_export = layout.entrypoints[&entrypoint]
4326 .exports
4327 .iter()
4328 .find(|export| export.name == "y")
4329 .expect("y export");
4330
4331 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
4332 assert_eq!(vars[y_export.slot.0], Value::Num(3.0));
4333 }
4334
4335 #[test]
4336 fn compile_rejects_multi_assign_call_output_count_mismatch_with_identifier() {
4337 let ast = runmat_parser::parse("[a, b] = deal(1, 2);").expect("parse");
4338 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4339 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4340 let entrypoint = hir.assembly.entrypoints[0].id;
4341 let function = hir.assembly.entrypoints[0].target;
4342 let body = mir.bodies.get_mut(&function).expect("entry body");
4343
4344 let mut patched = false;
4345 for block in &mut body.blocks {
4346 for stmt in &mut block.statements {
4347 if let MirStmtKind::MultiAssign {
4348 value: MirRvalue::Call(call),
4349 ..
4350 } = &mut stmt.kind
4351 {
4352 call.requested_outputs = RequestedOutputCount::One;
4353 patched = true;
4354 break;
4355 }
4356 }
4357 if patched {
4358 break;
4359 }
4360 }
4361 assert!(patched, "expected multi-assign call in lowered MIR");
4362
4363 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4364 assert_eq!(
4365 err.identifier.as_deref(),
4366 Some("RunMat:MirMultiAssignOutputCountMismatch")
4367 );
4368 }
4369
4370 #[test]
4371 fn compile_rejects_multi_assign_index_target_context_mismatch_with_identifier() {
4372 let ast = runmat_parser::parse("a(1)=0; [x, b] = deal(1, 2);").expect("parse");
4373 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4374 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4375 let entrypoint = hir.assembly.entrypoints[0].id;
4376 let function = hir.assembly.entrypoints[0].target;
4377 let body = mir.bodies.get_mut(&function).expect("entry body");
4378
4379 let mut indexed_place_for_target: Option<MirPlace> = None;
4380 for block in &body.blocks {
4381 for stmt in &block.statements {
4382 if let MirStmtKind::Assign { place, .. } = &stmt.kind {
4383 let mut cloned = place.clone();
4384 if let MirPlace::Index(_, ref mut idx) = cloned {
4385 idx.result_context = IndexResultContext::ReadSingle;
4386 indexed_place_for_target = Some(cloned);
4387 }
4388 }
4389 if indexed_place_for_target.is_some() {
4390 break;
4391 }
4392 }
4393 if indexed_place_for_target.is_some() {
4394 break;
4395 }
4396 }
4397
4398 let mut patched = false;
4399 for block in &mut body.blocks {
4400 for stmt in &mut block.statements {
4401 if let MirStmtKind::MultiAssign { targets, .. } = &mut stmt.kind {
4402 let place = indexed_place_for_target
4403 .clone()
4404 .expect("expected indexed place in lowered MIR");
4405 if let Some(target) = targets.targets.first_mut() {
4406 *target = MirOutputTarget::Place(place);
4407 patched = true;
4408 }
4409 }
4410 if patched {
4411 break;
4412 }
4413 }
4414 if patched {
4415 break;
4416 }
4417 }
4418 assert!(
4419 patched,
4420 "expected indexed multi-assign output target in lowered MIR"
4421 );
4422
4423 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4424 assert_eq!(
4425 err.identifier.as_deref(),
4426 Some("RunMat:MirIndexContextInvalid")
4427 );
4428 }
4429
4430 #[test]
4431 fn compile_rejects_multi_assign_index_target_deletion_context_identifier() {
4432 let ast = runmat_parser::parse("a(1)=0; [x, b] = deal(1, 2);").expect("parse");
4433 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4434 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4435 let entrypoint = hir.assembly.entrypoints[0].id;
4436 let function = hir.assembly.entrypoints[0].target;
4437 let body = mir.bodies.get_mut(&function).expect("entry body");
4438
4439 let mut indexed_place_for_target: Option<MirPlace> = None;
4440 for block in &body.blocks {
4441 for stmt in &block.statements {
4442 if let MirStmtKind::Assign { place, .. } = &stmt.kind {
4443 let mut cloned = place.clone();
4444 if let MirPlace::Index(_, ref mut idx) = cloned {
4445 idx.result_context = IndexResultContext::DeletionTarget;
4446 indexed_place_for_target = Some(cloned);
4447 }
4448 }
4449 if indexed_place_for_target.is_some() {
4450 break;
4451 }
4452 }
4453 if indexed_place_for_target.is_some() {
4454 break;
4455 }
4456 }
4457
4458 let mut patched = false;
4459 for block in &mut body.blocks {
4460 for stmt in &mut block.statements {
4461 if let MirStmtKind::MultiAssign { targets, .. } = &mut stmt.kind {
4462 let place = indexed_place_for_target
4463 .clone()
4464 .expect("expected indexed place in lowered MIR");
4465 if let Some(target) = targets.targets.first_mut() {
4466 *target = MirOutputTarget::Place(place);
4467 patched = true;
4468 }
4469 }
4470 if patched {
4471 break;
4472 }
4473 }
4474 if patched {
4475 break;
4476 }
4477 }
4478 assert!(
4479 patched,
4480 "expected indexed multi-assign output target in lowered MIR"
4481 );
4482
4483 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4484 assert_eq!(
4485 err.identifier.as_deref(),
4486 Some("RunMat:MirIndexContextInvalid")
4487 );
4488 }
4489
4490 #[test]
4491 fn compile_rejects_nonempty_delete_rhs_with_identifier() {
4492 let ast = runmat_parser::parse("x = [1 2 3]; x(2) = [];").expect("parse");
4493 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4494 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4495 let entrypoint = hir.assembly.entrypoints[0].id;
4496 let function = hir.assembly.entrypoints[0].target;
4497 let body = mir.bodies.get_mut(&function).expect("entry body");
4498
4499 let mut patched = false;
4500 for block in &mut body.blocks {
4501 for stmt in &mut block.statements {
4502 if let MirStmtKind::Assign { place, value } = &mut stmt.kind {
4503 if matches!(place, MirPlace::Index(_, _)) {
4504 *value = MirRvalue::Aggregate {
4505 kind: MirAggregateKind::Tensor,
4506 rows: 1,
4507 cols: 1,
4508 elements: vec![MirOperand::Constant(MirConstant::Number(
4509 "1".to_string(),
4510 ))],
4511 };
4512 patched = true;
4513 break;
4514 }
4515 }
4516 }
4517 if patched {
4518 break;
4519 }
4520 }
4521 assert!(patched, "expected indexed delete assignment in lowered MIR");
4522
4523 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4524 assert_eq!(
4525 err.identifier.as_deref(),
4526 Some("RunMat:MirDeleteAssignmentRhsInvalid")
4527 );
4528 }
4529
4530 #[test]
4531 fn compile_rejects_delete_place_mismatch_with_identifier() {
4532 let ast = runmat_parser::parse("x = [1 2 3]; x(2) = [];").expect("parse");
4533 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4534 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4535 let entrypoint = hir.assembly.entrypoints[0].id;
4536 let function = hir.assembly.entrypoints[0].target;
4537 let body = mir.bodies.get_mut(&function).expect("entry body");
4538
4539 let mut patched = false;
4540 for block in &mut body.blocks {
4541 for stmt in &mut block.statements {
4542 if let MirStmtKind::Assign { place, .. } = &mut stmt.kind {
4543 if let MirPlace::Index(base, _) = place {
4544 *place = (**base).clone();
4545 patched = true;
4546 break;
4547 }
4548 }
4549 }
4550 if patched {
4551 break;
4552 }
4553 }
4554 assert!(patched, "expected indexed delete assignment in lowered MIR");
4555
4556 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4557 assert_eq!(
4558 err.identifier.as_deref(),
4559 Some("RunMat:MirDeleteAssignmentPlaceMismatch")
4560 );
4561 }
4562
4563 #[test]
4564 fn compile_rejects_delete_on_nonindexed_target_with_identifier() {
4565 let ast = runmat_parser::parse("x = [1 2 3]; x(2) = [];").expect("parse");
4566 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4567 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4568 let entrypoint = hir.assembly.entrypoints[0].id;
4569 let function = hir.assembly.entrypoints[0].target;
4570 let body = mir.bodies.get_mut(&function).expect("entry body");
4571
4572 let replacement = body
4573 .blocks
4574 .iter()
4575 .flat_map(|block| block.statements.iter())
4576 .find_map(|stmt| match &stmt.kind {
4577 MirStmtKind::Assign {
4578 place: MirPlace::Index(base, _),
4579 ..
4580 } => Some((**base).clone()),
4581 _ => None,
4582 })
4583 .expect("expected indexed delete assignment target");
4584
4585 let mut patched_assign = false;
4586 let mut patched_mutation = false;
4587 for block in &mut body.blocks {
4588 for stmt in &mut block.statements {
4589 match &mut stmt.kind {
4590 MirStmtKind::Assign { place, .. } => {
4591 if matches!(place, MirPlace::Index(_, _)) {
4592 *place = replacement.clone();
4593 patched_assign = true;
4594 }
4595 }
4596 MirStmtKind::PlaceMutation(mutation) => {
4597 if matches!(mutation.kind, runmat_hir::PlaceMutationKind::Delete) {
4598 mutation.place = replacement.clone();
4599 patched_mutation = true;
4600 }
4601 }
4602 _ => {}
4603 }
4604 }
4605 }
4606 assert!(
4607 patched_assign,
4608 "expected indexed delete assign stmt in lowered MIR"
4609 );
4610 assert!(
4611 patched_mutation,
4612 "expected delete place mutation stmt in lowered MIR"
4613 );
4614
4615 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4616 assert_eq!(
4617 err.identifier.as_deref(),
4618 Some("RunMat:MirDeleteAssignmentTargetInvalid")
4619 );
4620 }
4621
4622 #[test]
4623 fn compile_rejects_delete_on_brace_index_target_with_identifier() {
4624 let ast = runmat_parser::parse("x = [1 2 3]; x(2) = [];").expect("parse");
4625 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4626 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4627 let entrypoint = hir.assembly.entrypoints[0].id;
4628 let function = hir.assembly.entrypoints[0].target;
4629 let body = mir.bodies.get_mut(&function).expect("entry body");
4630
4631 let mut patched_assign = false;
4632 let mut patched_mutation = false;
4633 for block in &mut body.blocks {
4634 for stmt in &mut block.statements {
4635 match &mut stmt.kind {
4636 MirStmtKind::Assign {
4637 place: MirPlace::Index(_, indexing),
4638 ..
4639 } => {
4640 indexing.kind = runmat_hir::IndexKind::Brace;
4641 patched_assign = true;
4642 }
4643 MirStmtKind::PlaceMutation(mutation) => {
4644 if let MirPlace::Index(_, indexing) = &mut mutation.place {
4645 indexing.kind = runmat_hir::IndexKind::Brace;
4646 patched_mutation = true;
4647 }
4648 }
4649 _ => {}
4650 }
4651 }
4652 }
4653 assert!(
4654 patched_assign,
4655 "expected indexed delete assign stmt in lowered MIR"
4656 );
4657 assert!(
4658 patched_mutation,
4659 "expected delete place mutation stmt in lowered MIR"
4660 );
4661
4662 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4663 assert_eq!(
4664 err.identifier.as_deref(),
4665 Some("RunMat:MirDeleteAssignmentIndexKindInvalid")
4666 );
4667 }
4668
4669 #[test]
4670 fn compile_rejects_delete_with_nondeletion_index_context_with_identifier() {
4671 let ast = runmat_parser::parse("x = [1 2 3]; x(2) = [];").expect("parse");
4672 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4673 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4674 let entrypoint = hir.assembly.entrypoints[0].id;
4675 let function = hir.assembly.entrypoints[0].target;
4676 let body = mir.bodies.get_mut(&function).expect("entry body");
4677
4678 let mut patched_assign = false;
4679 let mut patched_mutation = false;
4680 for block in &mut body.blocks {
4681 for stmt in &mut block.statements {
4682 match &mut stmt.kind {
4683 MirStmtKind::Assign {
4684 place: MirPlace::Index(_, indexing),
4685 ..
4686 } => {
4687 indexing.result_context = IndexResultContext::AssignmentTarget;
4688 patched_assign = true;
4689 }
4690 MirStmtKind::PlaceMutation(mutation) => {
4691 if let MirPlace::Index(_, indexing) = &mut mutation.place {
4692 indexing.result_context = IndexResultContext::AssignmentTarget;
4693 patched_mutation = true;
4694 }
4695 }
4696 _ => {}
4697 }
4698 }
4699 }
4700 assert!(
4701 patched_assign,
4702 "expected indexed delete assign stmt in lowered MIR"
4703 );
4704 assert!(
4705 patched_mutation,
4706 "expected delete place mutation stmt in lowered MIR"
4707 );
4708
4709 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4710 assert_eq!(
4711 err.identifier.as_deref(),
4712 Some("RunMat:MirDeleteAssignmentContextInvalid")
4713 );
4714 }
4715
4716 #[test]
4717 fn compile_rejects_deletion_context_without_delete_with_identifier() {
4718 let ast = runmat_parser::parse("x = [1 2 3]; x(2) = 9;").expect("parse");
4719 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4720 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4721 let entrypoint = hir.assembly.entrypoints[0].id;
4722 let function = hir.assembly.entrypoints[0].target;
4723 let body = mir.bodies.get_mut(&function).expect("entry body");
4724
4725 let mut patched_assign = false;
4726 for block in &mut body.blocks {
4727 for stmt in &mut block.statements {
4728 if let MirStmtKind::Assign {
4729 place: MirPlace::Index(_, indexing),
4730 ..
4731 } = &mut stmt.kind
4732 {
4733 indexing.result_context = IndexResultContext::DeletionTarget;
4734 patched_assign = true;
4735 break;
4736 }
4737 }
4738 if patched_assign {
4739 break;
4740 }
4741 }
4742 assert!(
4743 patched_assign,
4744 "expected indexed non-delete assign stmt in lowered MIR"
4745 );
4746
4747 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4748 assert_eq!(
4749 err.identifier.as_deref(),
4750 Some("RunMat:MirDeletionContextWithoutDeleteInvalid")
4751 );
4752 }
4753
4754 #[test]
4755 fn compile_rejects_delete_with_nonexisting_creation_policy_with_identifier() {
4756 let ast = runmat_parser::parse("x = [1 2 3]; x(2) = [];").expect("parse");
4757 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4758 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4759 let entrypoint = hir.assembly.entrypoints[0].id;
4760 let function = hir.assembly.entrypoints[0].target;
4761 let body = mir.bodies.get_mut(&function).expect("entry body");
4762
4763 let mut patched = false;
4764 for block in &mut body.blocks {
4765 for stmt in &mut block.statements {
4766 if let MirStmtKind::PlaceMutation(mutation) = &mut stmt.kind {
4767 if matches!(mutation.kind, runmat_hir::PlaceMutationKind::Delete) {
4768 mutation.creation_policy = AssignmentCreationPolicy::CreateArrayByIndex;
4769 patched = true;
4770 break;
4771 }
4772 }
4773 }
4774 if patched {
4775 break;
4776 }
4777 }
4778 assert!(patched, "expected delete place mutation in lowered MIR");
4779
4780 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4781 assert_eq!(
4782 err.identifier.as_deref(),
4783 Some("RunMat:MirDeleteAssignmentCreationPolicyInvalid")
4784 );
4785 }
4786
4787 #[test]
4788 fn compile_rejects_invalid_read_index_context_with_identifier() {
4789 let ast = runmat_parser::parse("x = [1 2 3]; y = x(2);").expect("parse");
4790 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4791 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4792 let entrypoint = hir.assembly.entrypoints[0].id;
4793 let function = hir.assembly.entrypoints[0].target;
4794 let body = mir.bodies.get_mut(&function).expect("entry body");
4795
4796 let mut patched = false;
4797 for block in &mut body.blocks {
4798 for stmt in &mut block.statements {
4799 if let MirStmtKind::Assign {
4800 value: MirRvalue::Index { indexing, .. },
4801 ..
4802 } = &mut stmt.kind
4803 {
4804 indexing.result_context = IndexResultContext::AssignmentTarget;
4805 patched = true;
4806 break;
4807 }
4808 }
4809 if patched {
4810 break;
4811 }
4812 }
4813 assert!(patched, "expected indexed read assignment in lowered MIR");
4814
4815 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4816 assert_eq!(
4817 err.identifier.as_deref(),
4818 Some("RunMat:MirIndexContextInvalid")
4819 );
4820 }
4821
4822 #[test]
4823 fn compile_rejects_unsupported_mir_unary_operator_with_identifier() {
4824 let ast = runmat_parser::parse("x = -1;").expect("parse");
4825 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4826 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4827 let entrypoint = hir.assembly.entrypoints[0].id;
4828 let function = hir.assembly.entrypoints[0].target;
4829 let body = mir.bodies.get_mut(&function).expect("entry body");
4830
4831 let mut patched = false;
4832 for block in &mut body.blocks {
4833 for stmt in &mut block.statements {
4834 if let MirStmtKind::Assign {
4835 value: MirRvalue::Unary(op, _),
4836 ..
4837 } = &mut stmt.kind
4838 {
4839 *op = OperatorKind::Add;
4840 patched = true;
4841 break;
4842 }
4843 }
4844 if patched {
4845 break;
4846 }
4847 }
4848 assert!(patched, "expected unary assignment in lowered MIR");
4849
4850 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4851 assert_eq!(
4852 err.identifier.as_deref(),
4853 Some("RunMat:MirOperatorUnsupported")
4854 );
4855 }
4856
4857 #[test]
4858 fn compile_rejects_unsupported_mir_binary_operator_with_identifier() {
4859 let ast = runmat_parser::parse("x = 1 + 2;").expect("parse");
4860 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4861 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4862 let entrypoint = hir.assembly.entrypoints[0].id;
4863 let function = hir.assembly.entrypoints[0].target;
4864 let body = mir.bodies.get_mut(&function).expect("entry body");
4865
4866 let mut patched = false;
4867 for block in &mut body.blocks {
4868 for stmt in &mut block.statements {
4869 if let MirStmtKind::Assign {
4870 value: MirRvalue::Binary(_, op, _),
4871 ..
4872 } = &mut stmt.kind
4873 {
4874 *op = OperatorKind::Transpose;
4875 patched = true;
4876 break;
4877 }
4878 }
4879 if patched {
4880 break;
4881 }
4882 }
4883 assert!(patched, "expected binary assignment in lowered MIR");
4884
4885 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4886 assert_eq!(
4887 err.identifier.as_deref(),
4888 Some("RunMat:MirOperatorUnsupported")
4889 );
4890 }
4891
4892 #[test]
4893 fn compile_rejects_unknown_mir_builtin_id_with_identifier() {
4894 let ast = runmat_parser::parse("x = sin(1);").expect("parse");
4895 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4896 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4897 let entrypoint = hir.assembly.entrypoints[0].id;
4898 let function = hir.assembly.entrypoints[0].target;
4899 let body = mir.bodies.get_mut(&function).expect("entry body");
4900
4901 let mut patched = false;
4902 for block in &mut body.blocks {
4903 for stmt in &mut block.statements {
4904 if let MirStmtKind::Assign {
4905 value: MirRvalue::Call(call),
4906 ..
4907 } = &mut stmt.kind
4908 {
4909 call.callee = MirCallee::Static(CallableIdentity::Builtin(BuiltinId(
4910 "__not_a_builtin".into(),
4911 )));
4912 patched = true;
4913 break;
4914 }
4915 }
4916 if patched {
4917 break;
4918 }
4919 }
4920 assert!(patched, "expected call assignment in lowered MIR");
4921
4922 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4923 assert_eq!(err.identifier.as_deref(), Some("RunMat:MirBuiltinUnknown"));
4924 }
4925
4926 #[test]
4927 fn compile_rejects_invalid_mir_number_literal_with_identifier() {
4928 let ast = runmat_parser::parse("x = 1;").expect("parse");
4929 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4930 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4931 let entrypoint = hir.assembly.entrypoints[0].id;
4932 let function = hir.assembly.entrypoints[0].target;
4933 let body = mir.bodies.get_mut(&function).expect("entry body");
4934
4935 let mut patched = false;
4936 for block in &mut body.blocks {
4937 for stmt in &mut block.statements {
4938 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
4939 *value = MirRvalue::Use(MirOperand::Constant(MirConstant::Number(
4940 "not_a_number".into(),
4941 )));
4942 patched = true;
4943 break;
4944 }
4945 }
4946 if patched {
4947 break;
4948 }
4949 }
4950 assert!(patched, "expected assignment in lowered MIR");
4951
4952 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4953 assert_eq!(
4954 err.identifier.as_deref(),
4955 Some("RunMat:MirNumberLiteralInvalid")
4956 );
4957 }
4958
4959 #[test]
4960 fn compile_rejects_unknown_mir_constant_with_identifier() {
4961 let ast = runmat_parser::parse("x = pi;").expect("parse");
4962 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4963 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4964 let entrypoint = hir.assembly.entrypoints[0].id;
4965 let function = hir.assembly.entrypoints[0].target;
4966 let body = mir.bodies.get_mut(&function).expect("entry body");
4967
4968 let mut patched = false;
4969 for block in &mut body.blocks {
4970 for stmt in &mut block.statements {
4971 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
4972 *value = MirRvalue::Use(MirOperand::Constant(MirConstant::Symbol(
4973 runmat_hir::SymbolName("definitely_missing_constant".to_string()),
4974 )));
4975 patched = true;
4976 break;
4977 }
4978 }
4979 if patched {
4980 break;
4981 }
4982 }
4983 assert!(patched, "expected assignment in lowered MIR");
4984
4985 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
4986 assert_eq!(err.identifier.as_deref(), Some("RunMat:MirConstantUnknown"));
4987 }
4988
4989 #[test]
4990 fn compile_rejects_missing_mir_function_handle_runtime_name_with_identifier() {
4991 let ast = runmat_parser::parse("f = @sin;").expect("parse");
4992 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
4993 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
4994 let entrypoint = hir.assembly.entrypoints[0].id;
4995 let function = hir.assembly.entrypoints[0].target;
4996 let body = mir.bodies.get_mut(&function).expect("entry body");
4997
4998 let mut patched = false;
4999 for block in &mut body.blocks {
5000 for stmt in &mut block.statements {
5001 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5002 *value = MirRvalue::Use(MirOperand::FunctionHandle(
5003 CallableIdentity::ExternalName(QualifiedName(vec![
5004 SymbolName("pkg".to_string()),
5005 SymbolName(String::new()),
5006 SymbolName("broken".to_string()),
5007 ])),
5008 ));
5009 patched = true;
5010 break;
5011 }
5012 }
5013 if patched {
5014 break;
5015 }
5016 }
5017 assert!(patched, "expected assignment in lowered MIR");
5018
5019 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5020 assert_eq!(
5021 err.identifier.as_deref(),
5022 Some("RunMat:MirFunctionHandleNameMissing")
5023 );
5024 }
5025
5026 #[test]
5027 fn compile_rejects_single_segment_external_function_handle_name_with_identifier() {
5028 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5029 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5030 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5031 let entrypoint = hir.assembly.entrypoints[0].id;
5032 let function = hir.assembly.entrypoints[0].target;
5033 let body = mir.bodies.get_mut(&function).expect("entry body");
5034
5035 let mut patched = false;
5036 for block in &mut body.blocks {
5037 for stmt in &mut block.statements {
5038 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5039 *value =
5040 MirRvalue::Use(MirOperand::FunctionHandle(CallableIdentity::ExternalName(
5041 QualifiedName(vec![SymbolName("pkg".to_string())]),
5042 )));
5043 patched = true;
5044 break;
5045 }
5046 }
5047 if patched {
5048 break;
5049 }
5050 }
5051 assert!(patched, "expected assignment in lowered MIR");
5052
5053 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5054 assert_eq!(
5055 err.identifier.as_deref(),
5056 Some("RunMat:MirFunctionHandleNameMissing")
5057 );
5058 }
5059
5060 #[test]
5061 fn compile_rejects_empty_dynamic_function_handle_name_with_identifier() {
5062 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5063 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5064 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5065 let entrypoint = hir.assembly.entrypoints[0].id;
5066 let function = hir.assembly.entrypoints[0].target;
5067 let body = mir.bodies.get_mut(&function).expect("entry body");
5068
5069 let mut patched = false;
5070 for block in &mut body.blocks {
5071 for stmt in &mut block.statements {
5072 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5073 *value = MirRvalue::Use(MirOperand::FunctionHandle(
5074 CallableIdentity::DynamicName(SymbolName(String::new())),
5075 ));
5076 patched = true;
5077 break;
5078 }
5079 }
5080 if patched {
5081 break;
5082 }
5083 }
5084 assert!(patched, "expected assignment in lowered MIR");
5085
5086 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5087 assert_eq!(
5088 err.identifier.as_deref(),
5089 Some("RunMat:MirFunctionHandleNameMissing")
5090 );
5091 }
5092
5093 #[test]
5094 fn compile_rejects_whitespace_dynamic_function_handle_name_with_identifier() {
5095 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5096 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5097 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5098 let entrypoint = hir.assembly.entrypoints[0].id;
5099 let function = hir.assembly.entrypoints[0].target;
5100 let body = mir.bodies.get_mut(&function).expect("entry body");
5101
5102 let mut patched = false;
5103 for block in &mut body.blocks {
5104 for stmt in &mut block.statements {
5105 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5106 *value = MirRvalue::Use(MirOperand::FunctionHandle(
5107 CallableIdentity::DynamicName(SymbolName(" ".to_string())),
5108 ));
5109 patched = true;
5110 break;
5111 }
5112 }
5113 if patched {
5114 break;
5115 }
5116 }
5117 assert!(patched, "expected assignment in lowered MIR");
5118
5119 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5120 assert_eq!(
5121 err.identifier.as_deref(),
5122 Some("RunMat:MirFunctionHandleNameMissing")
5123 );
5124 }
5125
5126 #[test]
5127 fn compile_rejects_empty_builtin_function_handle_name_with_identifier() {
5128 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5129 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5130 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5131 let entrypoint = hir.assembly.entrypoints[0].id;
5132 let function = hir.assembly.entrypoints[0].target;
5133 let body = mir.bodies.get_mut(&function).expect("entry body");
5134
5135 let mut patched = false;
5136 for block in &mut body.blocks {
5137 for stmt in &mut block.statements {
5138 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5139 *value = MirRvalue::Use(MirOperand::FunctionHandle(CallableIdentity::Builtin(
5140 BuiltinId(String::new()),
5141 )));
5142 patched = true;
5143 break;
5144 }
5145 }
5146 if patched {
5147 break;
5148 }
5149 }
5150 assert!(patched, "expected assignment in lowered MIR");
5151
5152 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5153 assert_eq!(
5154 err.identifier.as_deref(),
5155 Some("RunMat:MirFunctionHandleNameMissing")
5156 );
5157 }
5158
5159 #[test]
5160 fn compile_rejects_whitespace_builtin_function_handle_name_with_identifier() {
5161 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5162 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5163 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5164 let entrypoint = hir.assembly.entrypoints[0].id;
5165 let function = hir.assembly.entrypoints[0].target;
5166 let body = mir.bodies.get_mut(&function).expect("entry body");
5167
5168 let mut patched = false;
5169 for block in &mut body.blocks {
5170 for stmt in &mut block.statements {
5171 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5172 *value = MirRvalue::Use(MirOperand::FunctionHandle(CallableIdentity::Builtin(
5173 BuiltinId(" ".to_string()),
5174 )));
5175 patched = true;
5176 break;
5177 }
5178 }
5179 if patched {
5180 break;
5181 }
5182 }
5183 assert!(patched, "expected assignment in lowered MIR");
5184
5185 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5186 assert_eq!(
5187 err.identifier.as_deref(),
5188 Some("RunMat:MirFunctionHandleNameMissing")
5189 );
5190 }
5191
5192 #[test]
5193 fn compile_lowers_method_function_handle_target_to_typed_instruction() {
5194 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5195 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5196 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5197 let entrypoint = hir.assembly.entrypoints[0].id;
5198 let function = hir.assembly.entrypoints[0].target;
5199 let body = mir.bodies.get_mut(&function).expect("entry body");
5200
5201 let mut patched = false;
5202 for block in &mut body.blocks {
5203 for stmt in &mut block.statements {
5204 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5205 *value = MirRvalue::Use(MirOperand::FunctionHandle(CallableIdentity::Method(
5206 MethodId("m".to_string()),
5207 )));
5208 patched = true;
5209 break;
5210 }
5211 }
5212 if patched {
5213 break;
5214 }
5215 }
5216 assert!(patched, "expected assignment in lowered MIR");
5217
5218 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile should succeed");
5219 assert!(bytecode.instructions.iter().any(|instr| matches!(
5220 instr,
5221 Instr::CreateMethodFunctionHandle(name) if name == "m"
5222 )));
5223 }
5224
5225 #[test]
5226 fn compile_lowers_struct_aggregate_literal_to_typed_instruction() {
5227 let ast = runmat_parser::parse("s = struct{a = 1, a = 2, b = 3};").expect("parse");
5228 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5229 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
5230 let entrypoint = hir.assembly.entrypoints[0].id;
5231 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile should succeed");
5232 assert!(bytecode.instructions.iter().any(|instr| match instr {
5233 Instr::CreateStructLiteral(fields) => {
5234 fields.as_slice() == ["a".to_string(), "a".to_string(), "b".to_string()]
5235 }
5236 _ => false,
5237 }));
5238 }
5239
5240 #[test]
5241 fn compile_lowers_object_aggregate_literal_to_typed_instruction() {
5242 let ast = runmat_parser::parse("p = ?Point{x = 1, y = 2};").expect("parse");
5243 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5244 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
5245 let entrypoint = hir.assembly.entrypoints[0].id;
5246 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile should succeed");
5247 assert!(bytecode.instructions.iter().any(|instr| match instr {
5248 Instr::CreateObjectLiteral { class_name, fields } => {
5249 class_name == "Point" && fields.as_slice() == ["x".to_string(), "y".to_string()]
5250 }
5251 _ => false,
5252 }));
5253 }
5254
5255 #[test]
5256 fn compile_rejects_whitespace_method_function_handle_name_with_identifier() {
5257 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5258 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5259 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5260 let entrypoint = hir.assembly.entrypoints[0].id;
5261 let function = hir.assembly.entrypoints[0].target;
5262 let body = mir.bodies.get_mut(&function).expect("entry body");
5263
5264 let mut patched = false;
5265 for block in &mut body.blocks {
5266 for stmt in &mut block.statements {
5267 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5268 *value = MirRvalue::Use(MirOperand::FunctionHandle(CallableIdentity::Method(
5269 MethodId(" ".to_string()),
5270 )));
5271 patched = true;
5272 break;
5273 }
5274 }
5275 if patched {
5276 break;
5277 }
5278 }
5279 assert!(patched, "expected assignment in lowered MIR");
5280
5281 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5282 assert_eq!(
5283 err.identifier.as_deref(),
5284 Some("RunMat:MirFunctionHandleNameMissing")
5285 );
5286 }
5287
5288 #[test]
5289 fn compile_rejects_empty_imported_module_function_handle_name_with_identifier() {
5290 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5291 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5292 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5293 let entrypoint = hir.assembly.entrypoints[0].id;
5294 let function = hir.assembly.entrypoints[0].target;
5295 let body = mir.bodies.get_mut(&function).expect("entry body");
5296
5297 let mut patched = false;
5298 for block in &mut body.blocks {
5299 for stmt in &mut block.statements {
5300 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5301 *value = MirRvalue::Use(MirOperand::FunctionHandle(
5302 CallableIdentity::Imported(DefPath {
5303 package: PackageName("pkg".to_string()),
5304 module: QualifiedName(vec![]),
5305 item: vec![DefPathSegment::Function(SymbolName("target".to_string()))],
5306 }),
5307 ));
5308 patched = true;
5309 break;
5310 }
5311 }
5312 if patched {
5313 break;
5314 }
5315 }
5316 assert!(patched, "expected assignment in lowered MIR");
5317
5318 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5319 assert_eq!(
5320 err.identifier.as_deref(),
5321 Some("RunMat:MirFunctionHandleNameMissing")
5322 );
5323 }
5324
5325 #[test]
5326 fn compile_rejects_imported_function_handle_missing_item_with_identifier() {
5327 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5328 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5329 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5330 let entrypoint = hir.assembly.entrypoints[0].id;
5331 let function = hir.assembly.entrypoints[0].target;
5332 let body = mir.bodies.get_mut(&function).expect("entry body");
5333
5334 let mut patched = false;
5335 for block in &mut body.blocks {
5336 for stmt in &mut block.statements {
5337 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5338 *value = MirRvalue::Use(MirOperand::FunctionHandle(
5339 CallableIdentity::Imported(DefPath {
5340 package: PackageName("pkg".to_string()),
5341 module: QualifiedName(vec![
5342 SymbolName("pkg".to_string()),
5343 SymbolName("mod".to_string()),
5344 ]),
5345 item: vec![],
5346 }),
5347 ));
5348 patched = true;
5349 break;
5350 }
5351 }
5352 if patched {
5353 break;
5354 }
5355 }
5356 assert!(patched, "expected assignment in lowered MIR");
5357
5358 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5359 assert_eq!(
5360 err.identifier.as_deref(),
5361 Some("RunMat:MirFunctionHandleNameMissing")
5362 );
5363 }
5364
5365 #[test]
5366 fn compile_rejects_imported_function_handle_mismatched_item_with_identifier() {
5367 let ast = runmat_parser::parse("f = @sin;").expect("parse");
5368 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5369 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5370 let entrypoint = hir.assembly.entrypoints[0].id;
5371 let function = hir.assembly.entrypoints[0].target;
5372 let body = mir.bodies.get_mut(&function).expect("entry body");
5373
5374 let mut patched = false;
5375 for block in &mut body.blocks {
5376 for stmt in &mut block.statements {
5377 if let MirStmtKind::Assign { value, .. } = &mut stmt.kind {
5378 *value = MirRvalue::Use(MirOperand::FunctionHandle(
5379 CallableIdentity::Imported(DefPath {
5380 package: PackageName("pkg".to_string()),
5381 module: QualifiedName(vec![
5382 SymbolName("pkg".to_string()),
5383 SymbolName("target".to_string()),
5384 ]),
5385 item: vec![DefPathSegment::Function(SymbolName(
5386 "different".to_string(),
5387 ))],
5388 }),
5389 ));
5390 patched = true;
5391 break;
5392 }
5393 }
5394 if patched {
5395 break;
5396 }
5397 }
5398 assert!(patched, "expected assignment in lowered MIR");
5399
5400 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5401 assert_eq!(
5402 err.identifier.as_deref(),
5403 Some("RunMat:MirFunctionHandleNameMissing")
5404 );
5405 }
5406
5407 #[test]
5408 fn compile_rejects_unsupported_mir_static_call_fallback_policy_with_identifier() {
5409 let ast = runmat_parser::parse("x = sin(1);").expect("parse");
5410 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5411 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5412 let entrypoint = hir.assembly.entrypoints[0].id;
5413 let function = hir.assembly.entrypoints[0].target;
5414 let body = mir.bodies.get_mut(&function).expect("entry body");
5415
5416 let mut patched = false;
5417 for block in &mut body.blocks {
5418 for stmt in &mut block.statements {
5419 if let MirStmtKind::Assign {
5420 value: MirRvalue::Call(call),
5421 ..
5422 } = &mut stmt.kind
5423 {
5424 call.callee = MirCallee::Static(CallableIdentity::Method(MethodId("m".into())));
5425 call.fallback_policy = CallableFallbackPolicy::ObjectDispatch;
5426 patched = true;
5427 break;
5428 }
5429 }
5430 if patched {
5431 break;
5432 }
5433 }
5434 assert!(patched, "expected call assignment in lowered MIR");
5435
5436 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5437 assert_eq!(
5438 err.identifier.as_deref(),
5439 Some("RunMat:MirCallFallbackPolicyUnsupported")
5440 );
5441 }
5442
5443 #[test]
5444 fn compile_rejects_static_call_with_mismatched_imported_identity_name_shape() {
5445 let ast = runmat_parser::parse("x = sin(1);").expect("parse");
5446 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5447 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5448 let entrypoint = hir.assembly.entrypoints[0].id;
5449 let function = hir.assembly.entrypoints[0].target;
5450 let body = mir.bodies.get_mut(&function).expect("entry body");
5451
5452 let mut patched = false;
5453 for block in &mut body.blocks {
5454 for stmt in &mut block.statements {
5455 if let MirStmtKind::Assign {
5456 value: MirRvalue::Call(call),
5457 ..
5458 } = &mut stmt.kind
5459 {
5460 call.callee = MirCallee::Static(CallableIdentity::Imported(DefPath {
5461 package: PackageName("pkg".to_string()),
5462 module: QualifiedName(vec![
5463 SymbolName("pkg".to_string()),
5464 SymbolName("target".to_string()),
5465 ]),
5466 item: vec![DefPathSegment::Function(SymbolName(
5467 "different".to_string(),
5468 ))],
5469 }));
5470 call.fallback_policy = CallableFallbackPolicy::RuntimeNameResolution;
5471 patched = true;
5472 break;
5473 }
5474 }
5475 if patched {
5476 break;
5477 }
5478 }
5479 assert!(patched, "expected call assignment in lowered MIR");
5480
5481 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5482 assert_eq!(
5483 err.identifier.as_deref(),
5484 Some("RunMat:MirCallTargetNameInvalid")
5485 );
5486 }
5487
5488 #[test]
5489 fn compile_rejects_static_call_with_single_segment_external_identity() {
5490 let ast = runmat_parser::parse("x = sin(1);").expect("parse");
5491 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5492 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5493 let entrypoint = hir.assembly.entrypoints[0].id;
5494 let function = hir.assembly.entrypoints[0].target;
5495 let body = mir.bodies.get_mut(&function).expect("entry body");
5496
5497 let mut patched = false;
5498 for block in &mut body.blocks {
5499 for stmt in &mut block.statements {
5500 if let MirStmtKind::Assign {
5501 value: MirRvalue::Call(call),
5502 ..
5503 } = &mut stmt.kind
5504 {
5505 call.callee =
5506 MirCallee::Static(CallableIdentity::ExternalName(QualifiedName(vec![
5507 SymbolName("sqrt".to_string()),
5508 ])));
5509 call.fallback_policy = CallableFallbackPolicy::ExternalBoundary;
5510 patched = true;
5511 break;
5512 }
5513 }
5514 if patched {
5515 break;
5516 }
5517 }
5518 assert!(patched, "expected call assignment in lowered MIR");
5519
5520 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5521 assert_eq!(
5522 err.identifier.as_deref(),
5523 Some("RunMat:MirCallTargetNameInvalid")
5524 );
5525 }
5526
5527 #[test]
5528 fn compile_rejects_static_call_with_method_identity_name_shape() {
5529 let ast = runmat_parser::parse("x = sin(1);").expect("parse");
5530 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5531 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5532 let entrypoint = hir.assembly.entrypoints[0].id;
5533 let function = hir.assembly.entrypoints[0].target;
5534 let body = mir.bodies.get_mut(&function).expect("entry body");
5535
5536 let mut patched = false;
5537 for block in &mut body.blocks {
5538 for stmt in &mut block.statements {
5539 if let MirStmtKind::Assign {
5540 value: MirRvalue::Call(call),
5541 ..
5542 } = &mut stmt.kind
5543 {
5544 call.callee = MirCallee::Static(CallableIdentity::Method(MethodId(
5545 "remote_inc".to_string(),
5546 )));
5547 call.fallback_policy = CallableFallbackPolicy::RuntimeNameResolution;
5548 patched = true;
5549 break;
5550 }
5551 }
5552 if patched {
5553 break;
5554 }
5555 }
5556 assert!(patched, "expected call assignment in lowered MIR");
5557
5558 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5559 assert_eq!(
5560 err.identifier.as_deref(),
5561 Some("RunMat:MirCallTargetNameInvalid")
5562 );
5563 }
5564
5565 #[test]
5566 fn compile_rejects_static_call_with_whitespace_dynamic_identity_name_shape() {
5567 let ast = runmat_parser::parse("x = sin(1);").expect("parse");
5568 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5569 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5570 let entrypoint = hir.assembly.entrypoints[0].id;
5571 let function = hir.assembly.entrypoints[0].target;
5572 let body = mir.bodies.get_mut(&function).expect("entry body");
5573
5574 let mut patched = false;
5575 for block in &mut body.blocks {
5576 for stmt in &mut block.statements {
5577 if let MirStmtKind::Assign {
5578 value: MirRvalue::Call(call),
5579 ..
5580 } = &mut stmt.kind
5581 {
5582 call.callee = MirCallee::Static(CallableIdentity::DynamicName(SymbolName(
5583 " ".to_string(),
5584 )));
5585 call.fallback_policy = CallableFallbackPolicy::RuntimeNameResolution;
5586 patched = true;
5587 break;
5588 }
5589 }
5590 if patched {
5591 break;
5592 }
5593 }
5594 assert!(patched, "expected call assignment in lowered MIR");
5595
5596 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5597 assert_eq!(
5598 err.identifier.as_deref(),
5599 Some("RunMat:MirCallTargetNameInvalid")
5600 );
5601 }
5602
5603 #[test]
5604 fn compile_rejects_multi_assign_static_call_with_invalid_name_shape() {
5605 let ast = runmat_parser::parse("[a, b] = max([1,2]);").expect("parse");
5606 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5607 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5608 let entrypoint = hir.assembly.entrypoints[0].id;
5609 let function = hir.assembly.entrypoints[0].target;
5610 let body = mir.bodies.get_mut(&function).expect("entry body");
5611
5612 let mut patched = false;
5613 for block in &mut body.blocks {
5614 for stmt in &mut block.statements {
5615 if let MirStmtKind::MultiAssign {
5616 value: MirRvalue::Call(call),
5617 ..
5618 } = &mut stmt.kind
5619 {
5620 call.callee = MirCallee::Static(CallableIdentity::Imported(DefPath {
5621 package: PackageName("pkg".to_string()),
5622 module: QualifiedName(vec![
5623 SymbolName("pkg".to_string()),
5624 SymbolName("target".to_string()),
5625 ]),
5626 item: vec![DefPathSegment::Function(SymbolName(
5627 "different".to_string(),
5628 ))],
5629 }));
5630 call.fallback_policy = CallableFallbackPolicy::RuntimeNameResolution;
5631 patched = true;
5632 break;
5633 }
5634 }
5635 if patched {
5636 break;
5637 }
5638 }
5639 assert!(patched, "expected multi-assign call in lowered MIR");
5640
5641 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5642 assert_eq!(
5643 err.identifier.as_deref(),
5644 Some("RunMat:MirCallTargetNameInvalid")
5645 );
5646 }
5647
5648 #[test]
5649 fn compile_rejects_multi_assign_static_call_with_method_identity_name_shape() {
5650 let ast = runmat_parser::parse("[a, b] = max([1,2]);").expect("parse");
5651 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5652 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5653 let entrypoint = hir.assembly.entrypoints[0].id;
5654 let function = hir.assembly.entrypoints[0].target;
5655 let body = mir.bodies.get_mut(&function).expect("entry body");
5656
5657 let mut patched = false;
5658 for block in &mut body.blocks {
5659 for stmt in &mut block.statements {
5660 if let MirStmtKind::MultiAssign {
5661 value: MirRvalue::Call(call),
5662 ..
5663 } = &mut stmt.kind
5664 {
5665 call.callee = MirCallee::Static(CallableIdentity::Method(MethodId(
5666 "remote_pair".to_string(),
5667 )));
5668 call.fallback_policy = CallableFallbackPolicy::RuntimeNameResolution;
5669 patched = true;
5670 break;
5671 }
5672 }
5673 if patched {
5674 break;
5675 }
5676 }
5677 assert!(patched, "expected multi-assign call in lowered MIR");
5678
5679 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5680 assert_eq!(
5681 err.identifier.as_deref(),
5682 Some("RunMat:MirCallTargetNameInvalid")
5683 );
5684 }
5685
5686 #[test]
5687 fn compile_rejects_unsupported_mir_method_call_fallback_policy_with_identifier() {
5688 let ast = runmat_parser::parse("obj = 1; obj.method(1);").expect("parse");
5689 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5690 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5691 let entrypoint = hir.assembly.entrypoints[0].id;
5692 let function = hir.assembly.entrypoints[0].target;
5693 let body = mir.bodies.get_mut(&function).expect("entry body");
5694
5695 let mut patched = false;
5696 for block in &mut body.blocks {
5697 for stmt in &mut block.statements {
5698 let maybe_call = match &mut stmt.kind {
5699 MirStmtKind::Assign {
5700 value: MirRvalue::Call(call),
5701 ..
5702 }
5703 | MirStmtKind::Expr(MirRvalue::Call(call)) => Some(call),
5704 _ => None,
5705 };
5706 if let Some(call) = maybe_call {
5707 if matches!(
5708 call.syntax,
5709 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
5710 ) {
5711 call.fallback_policy = CallableFallbackPolicy::ExternalBoundary;
5712 patched = true;
5713 break;
5714 }
5715 }
5716 }
5717 if patched {
5718 break;
5719 }
5720 }
5721 assert!(patched, "expected method call assignment in lowered MIR");
5722
5723 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5724 assert_eq!(
5725 err.identifier.as_deref(),
5726 Some("RunMat:MirMethodFallbackPolicyUnsupported")
5727 );
5728 }
5729
5730 #[test]
5731 fn compile_rejects_missing_mir_method_call_receiver_with_identifier() {
5732 let ast = runmat_parser::parse("obj = 1; obj.method(1);").expect("parse");
5733 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5734 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5735 let entrypoint = hir.assembly.entrypoints[0].id;
5736 let function = hir.assembly.entrypoints[0].target;
5737 let body = mir.bodies.get_mut(&function).expect("entry body");
5738
5739 let mut patched = false;
5740 for block in &mut body.blocks {
5741 for stmt in &mut block.statements {
5742 let maybe_call = match &mut stmt.kind {
5743 MirStmtKind::Assign {
5744 value: MirRvalue::Call(call),
5745 ..
5746 }
5747 | MirStmtKind::Expr(MirRvalue::Call(call)) => Some(call),
5748 _ => None,
5749 };
5750 if let Some(call) = maybe_call {
5751 if matches!(
5752 call.syntax,
5753 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
5754 ) {
5755 call.args.clear();
5756 patched = true;
5757 break;
5758 }
5759 }
5760 }
5761 if patched {
5762 break;
5763 }
5764 }
5765 assert!(patched, "expected method call in lowered MIR");
5766
5767 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5768 assert_eq!(
5769 err.identifier.as_deref(),
5770 Some("RunMat:MirMethodCallReceiverMissing")
5771 );
5772 }
5773
5774 #[test]
5775 fn compile_rejects_invalid_mir_method_call_callee_with_identifier() {
5776 let ast = runmat_parser::parse("obj = 1; obj.method(1);").expect("parse");
5777 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5778 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5779 let entrypoint = hir.assembly.entrypoints[0].id;
5780 let function = hir.assembly.entrypoints[0].target;
5781 let body = mir.bodies.get_mut(&function).expect("entry body");
5782
5783 let mut patched = false;
5784 for block in &mut body.blocks {
5785 for stmt in &mut block.statements {
5786 let maybe_call = match &mut stmt.kind {
5787 MirStmtKind::Assign {
5788 value: MirRvalue::Call(call),
5789 ..
5790 }
5791 | MirStmtKind::Expr(MirRvalue::Call(call)) => Some(call),
5792 _ => None,
5793 };
5794 if let Some(call) = maybe_call {
5795 if matches!(
5796 call.syntax,
5797 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
5798 ) {
5799 call.callee = MirCallee::Dynamic(MirOperand::Constant(
5800 MirConstant::Number("1".into()),
5801 ));
5802 patched = true;
5803 break;
5804 }
5805 }
5806 }
5807 if patched {
5808 break;
5809 }
5810 }
5811 assert!(patched, "expected method call in lowered MIR");
5812
5813 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5814 assert_eq!(
5815 err.identifier.as_deref(),
5816 Some("RunMat:MirMethodCallCalleeInvalid")
5817 );
5818 }
5819
5820 #[test]
5821 fn compile_rejects_imported_mir_method_call_callee_with_identifier() {
5822 let ast = runmat_parser::parse("obj = 1; obj.method(1);").expect("parse");
5823 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5824 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5825 let entrypoint = hir.assembly.entrypoints[0].id;
5826 let function = hir.assembly.entrypoints[0].target;
5827 let body = mir.bodies.get_mut(&function).expect("entry body");
5828
5829 let mut patched = false;
5830 for block in &mut body.blocks {
5831 for stmt in &mut block.statements {
5832 let maybe_call = match &mut stmt.kind {
5833 MirStmtKind::Assign {
5834 value: MirRvalue::Call(call),
5835 ..
5836 }
5837 | MirStmtKind::Expr(MirRvalue::Call(call)) => Some(call),
5838 _ => None,
5839 };
5840 if let Some(call) = maybe_call {
5841 if matches!(
5842 call.syntax,
5843 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
5844 ) {
5845 call.callee = MirCallee::Static(CallableIdentity::Imported(DefPath {
5846 package: PackageName("pkg".into()),
5847 module: QualifiedName(vec![
5848 SymbolName("pkg".into()),
5849 SymbolName("method".into()),
5850 ]),
5851 item: vec![DefPathSegment::Function(SymbolName("method".into()))],
5852 }));
5853 patched = true;
5854 break;
5855 }
5856 }
5857 }
5858 if patched {
5859 break;
5860 }
5861 }
5862 assert!(patched, "expected method call in lowered MIR");
5863
5864 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5865 assert_eq!(
5866 err.identifier.as_deref(),
5867 Some("RunMat:MirMethodCallCalleeInvalid")
5868 );
5869 }
5870
5871 #[test]
5872 fn compile_rejects_invalid_mir_multi_assign_method_call_callee_with_identifier() {
5873 let ast = runmat_parser::parse("obj = 1; [a, b] = obj.method(1);").expect("parse");
5874 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5875 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5876 let entrypoint = hir.assembly.entrypoints[0].id;
5877 let function = hir.assembly.entrypoints[0].target;
5878 let body = mir.bodies.get_mut(&function).expect("entry body");
5879
5880 let mut patched = false;
5881 for block in &mut body.blocks {
5882 for stmt in &mut block.statements {
5883 if let MirStmtKind::MultiAssign {
5884 value: MirRvalue::Call(call),
5885 ..
5886 } = &mut stmt.kind
5887 {
5888 if matches!(
5889 call.syntax,
5890 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
5891 ) {
5892 call.callee = MirCallee::Dynamic(MirOperand::Constant(
5893 MirConstant::Number("1".into()),
5894 ));
5895 patched = true;
5896 break;
5897 }
5898 }
5899 }
5900 if patched {
5901 break;
5902 }
5903 }
5904 assert!(patched, "expected multi-assign method call in lowered MIR");
5905
5906 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5907 assert_eq!(
5908 err.identifier.as_deref(),
5909 Some("RunMat:MirMethodCallCalleeInvalid")
5910 );
5911 }
5912
5913 #[test]
5914 fn compile_rejects_imported_mir_multi_assign_method_call_callee_with_identifier() {
5915 let ast = runmat_parser::parse("obj = 1; [a, b] = obj.method(1);").expect("parse");
5916 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5917 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5918 let entrypoint = hir.assembly.entrypoints[0].id;
5919 let function = hir.assembly.entrypoints[0].target;
5920 let body = mir.bodies.get_mut(&function).expect("entry body");
5921
5922 let mut patched = false;
5923 for block in &mut body.blocks {
5924 for stmt in &mut block.statements {
5925 if let MirStmtKind::MultiAssign {
5926 value: MirRvalue::Call(call),
5927 ..
5928 } = &mut stmt.kind
5929 {
5930 if matches!(
5931 call.syntax,
5932 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
5933 ) {
5934 call.callee = MirCallee::Static(CallableIdentity::Imported(DefPath {
5935 package: PackageName("pkg".into()),
5936 module: QualifiedName(vec![
5937 SymbolName("pkg".into()),
5938 SymbolName("method".into()),
5939 ]),
5940 item: vec![DefPathSegment::Function(SymbolName("method".into()))],
5941 }));
5942 patched = true;
5943 break;
5944 }
5945 }
5946 }
5947 if patched {
5948 break;
5949 }
5950 }
5951 assert!(patched, "expected multi-assign method call in lowered MIR");
5952
5953 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
5954 assert_eq!(
5955 err.identifier.as_deref(),
5956 Some("RunMat:MirMethodCallCalleeInvalid")
5957 );
5958 }
5959
5960 #[test]
5961 fn compile_rejects_multisegment_external_mir_method_call_callee_with_identifier() {
5962 let ast = runmat_parser::parse("obj = 1; obj.method(1);").expect("parse");
5963 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
5964 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
5965 let entrypoint = hir.assembly.entrypoints[0].id;
5966 let function = hir.assembly.entrypoints[0].target;
5967 let body = mir.bodies.get_mut(&function).expect("entry body");
5968
5969 let mut patched = false;
5970 for block in &mut body.blocks {
5971 for stmt in &mut block.statements {
5972 let maybe_call = match &mut stmt.kind {
5973 MirStmtKind::Assign {
5974 value: MirRvalue::Call(call),
5975 ..
5976 }
5977 | MirStmtKind::Expr(MirRvalue::Call(call)) => Some(call),
5978 _ => None,
5979 };
5980 if let Some(call) = maybe_call {
5981 if matches!(
5982 call.syntax,
5983 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
5984 ) {
5985 call.callee =
5986 MirCallee::Static(CallableIdentity::ExternalName(QualifiedName(vec![
5987 SymbolName("pkg".into()),
5988 SymbolName("method".into()),
5989 ])));
5990 patched = true;
5991 break;
5992 }
5993 }
5994 }
5995 if patched {
5996 break;
5997 }
5998 }
5999 assert!(patched, "expected method call in lowered MIR");
6000
6001 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
6002 assert_eq!(
6003 err.identifier.as_deref(),
6004 Some("RunMat:MirMethodCallCalleeInvalid")
6005 );
6006 }
6007
6008 #[test]
6009 fn compile_rejects_empty_method_name_mir_method_call_callee_with_identifier() {
6010 let ast = runmat_parser::parse("obj = 1; obj.method(1);").expect("parse");
6011 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6012 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
6013 let entrypoint = hir.assembly.entrypoints[0].id;
6014 let function = hir.assembly.entrypoints[0].target;
6015 let body = mir.bodies.get_mut(&function).expect("entry body");
6016
6017 let mut patched = false;
6018 for block in &mut body.blocks {
6019 for stmt in &mut block.statements {
6020 let maybe_call = match &mut stmt.kind {
6021 MirStmtKind::Assign {
6022 value: MirRvalue::Call(call),
6023 ..
6024 }
6025 | MirStmtKind::Expr(MirRvalue::Call(call)) => Some(call),
6026 _ => None,
6027 };
6028 if let Some(call) = maybe_call {
6029 if matches!(
6030 call.syntax,
6031 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
6032 ) {
6033 call.callee =
6034 MirCallee::Static(CallableIdentity::Method(MethodId(String::new())));
6035 patched = true;
6036 break;
6037 }
6038 }
6039 }
6040 if patched {
6041 break;
6042 }
6043 }
6044 assert!(patched, "expected method call in lowered MIR");
6045
6046 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
6047 assert_eq!(
6048 err.identifier.as_deref(),
6049 Some("RunMat:MirMethodCallCalleeInvalid")
6050 );
6051 }
6052
6053 #[test]
6054 fn compile_rejects_whitespace_method_name_mir_method_call_callee_with_identifier() {
6055 let ast = runmat_parser::parse("obj = 1; obj.method(1);").expect("parse");
6056 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6057 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
6058 let entrypoint = hir.assembly.entrypoints[0].id;
6059 let function = hir.assembly.entrypoints[0].target;
6060 let body = mir.bodies.get_mut(&function).expect("entry body");
6061
6062 let mut patched = false;
6063 for block in &mut body.blocks {
6064 for stmt in &mut block.statements {
6065 let maybe_call = match &mut stmt.kind {
6066 MirStmtKind::Assign {
6067 value: MirRvalue::Call(call),
6068 ..
6069 }
6070 | MirStmtKind::Expr(MirRvalue::Call(call)) => Some(call),
6071 _ => None,
6072 };
6073 if let Some(call) = maybe_call {
6074 if matches!(
6075 call.syntax,
6076 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
6077 ) {
6078 call.callee = MirCallee::Static(CallableIdentity::Method(MethodId(
6079 " ".to_string(),
6080 )));
6081 patched = true;
6082 break;
6083 }
6084 }
6085 }
6086 if patched {
6087 break;
6088 }
6089 }
6090 assert!(patched, "expected method call in lowered MIR");
6091
6092 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
6093 assert_eq!(
6094 err.identifier.as_deref(),
6095 Some("RunMat:MirMethodCallCalleeInvalid")
6096 );
6097 }
6098
6099 #[test]
6100 fn compile_rejects_whitespace_single_segment_external_mir_method_call_callee_with_identifier() {
6101 let ast = runmat_parser::parse("obj = 1; obj.method(1);").expect("parse");
6102 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6103 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
6104 let entrypoint = hir.assembly.entrypoints[0].id;
6105 let function = hir.assembly.entrypoints[0].target;
6106 let body = mir.bodies.get_mut(&function).expect("entry body");
6107
6108 let mut patched = false;
6109 for block in &mut body.blocks {
6110 for stmt in &mut block.statements {
6111 let maybe_call = match &mut stmt.kind {
6112 MirStmtKind::Assign {
6113 value: MirRvalue::Call(call),
6114 ..
6115 }
6116 | MirStmtKind::Expr(MirRvalue::Call(call)) => Some(call),
6117 _ => None,
6118 };
6119 if let Some(call) = maybe_call {
6120 if matches!(
6121 call.syntax,
6122 runmat_hir::CallSyntax::Method | runmat_hir::CallSyntax::DottedInvoke
6123 ) {
6124 call.callee =
6125 MirCallee::Static(CallableIdentity::ExternalName(QualifiedName(vec![
6126 SymbolName(" ".into()),
6127 ])));
6128 patched = true;
6129 break;
6130 }
6131 }
6132 }
6133 if patched {
6134 break;
6135 }
6136 }
6137 assert!(patched, "expected method call in lowered MIR");
6138
6139 let err = compile(&hir.assembly, &mir, entrypoint).expect_err("compile should fail");
6140 assert_eq!(
6141 err.identifier.as_deref(),
6142 Some("RunMat:MirMethodCallCalleeInvalid")
6143 );
6144 }
6145
6146 #[test]
6147 fn compile_lowers_statement_semantic_call_to_zero_outputs() {
6148 let ast =
6149 runmat_parser::parse("function y = f(x); y = nargout(); end; f(10);").expect("parse");
6150 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6151 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6152 let entrypoint = hir.assembly.entrypoints[0].id;
6153
6154 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6155
6156 assert!(bytecode
6157 .instructions
6158 .iter()
6159 .any(|instr| matches!(instr, Instr::CallSemanticFunctionMulti(_, _, 0))));
6160 }
6161
6162 #[test]
6163 fn compile_lowers_method_calls_with_explicit_object_dispatch_policy() {
6164 let ast = runmat_parser::parse("obj = 1; obj.method(1);").expect("parse");
6165 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6166 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6167 let entrypoint = hir.assembly.entrypoints[0].id;
6168
6169 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6170 assert!(bytecode.instructions.iter().any(|instr| matches!(
6171 instr,
6172 Instr::CallMethodOrMemberIndexMulti {
6173 fallback_policy: CallableFallbackPolicy::ObjectDispatch,
6174 ..
6175 }
6176 )));
6177 }
6178
6179 #[test]
6180 fn compile_interprets_simple_indexed_assignment() {
6181 let ast = runmat_parser::parse("x = [1 2; 3 4]; x(1, 2) = 9;").expect("parse");
6182 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6183 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6184 let entrypoint = hir.assembly.entrypoints[0].id;
6185
6186 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6187 let layout = bytecode.layout.as_ref().expect("layout");
6188 let x_export = layout.entrypoints[&entrypoint]
6189 .exports
6190 .iter()
6191 .find(|export| export.name == "x")
6192 .expect("x export");
6193
6194 assert!(bytecode
6195 .instructions
6196 .iter()
6197 .any(|instr| matches!(instr, Instr::StoreIndex(2))));
6198 assert!(!bytecode
6199 .instructions
6200 .iter()
6201 .any(|instr| matches!(instr, Instr::StoreSlice(2, 2, 0, 0))));
6202
6203 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6204 let Value::Tensor(tensor) = &vars[x_export.slot.0] else {
6205 panic!("expected tensor");
6206 };
6207 assert_eq!(tensor.shape, vec![2, 2]);
6208 assert_eq!(tensor.data, vec![1.0, 3.0, 9.0, 4.0]);
6209 }
6210
6211 #[test]
6212 fn compile_interprets_simple_slice_assignment() {
6213 let ast = runmat_parser::parse("x = [1 2; 3 4]; x(:, 2) = [9; 8];").expect("parse");
6214 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6215 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6216 let entrypoint = hir.assembly.entrypoints[0].id;
6217
6218 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6219 let layout = bytecode.layout.as_ref().expect("layout");
6220 let x_export = layout.entrypoints[&entrypoint]
6221 .exports
6222 .iter()
6223 .find(|export| export.name == "x")
6224 .expect("x export");
6225
6226 assert!(bytecode
6227 .instructions
6228 .iter()
6229 .any(|instr| matches!(instr, Instr::StoreSlice(2, 1, 1, 0))));
6230
6231 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6232 let Value::Tensor(tensor) = &vars[x_export.slot.0] else {
6233 panic!("expected tensor");
6234 };
6235 assert_eq!(tensor.shape, vec![2, 2]);
6236 assert_eq!(tensor.data, vec![1.0, 3.0, 9.0, 8.0]);
6237 }
6238
6239 #[test]
6240 fn compile_interprets_simple_cell_indexing() {
6241 let ast = runmat_parser::parse("c = {1, 2}; x = c{2};").expect("parse");
6242 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6243 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6244 let entrypoint = hir.assembly.entrypoints[0].id;
6245
6246 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6247 let layout = bytecode.layout.as_ref().expect("layout");
6248 let x_export = layout.entrypoints[&entrypoint]
6249 .exports
6250 .iter()
6251 .find(|export| export.name == "x")
6252 .expect("x export");
6253
6254 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6255 assert_eq!(vars[x_export.slot.0], Value::Num(2.0));
6256 }
6257
6258 #[test]
6259 fn compile_interprets_simple_cell_indexed_assignment() {
6260 let ast = runmat_parser::parse("c = {1, 2}; c{2} = 9; x = c{2};").expect("parse");
6261 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6262 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6263 let entrypoint = hir.assembly.entrypoints[0].id;
6264
6265 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6266 let layout = bytecode.layout.as_ref().expect("layout");
6267 let x_export = layout.entrypoints[&entrypoint]
6268 .exports
6269 .iter()
6270 .find(|export| export.name == "x")
6271 .expect("x export");
6272
6273 assert!(bytecode
6274 .instructions
6275 .iter()
6276 .any(|instr| matches!(instr, Instr::StoreIndexCell { num_indices: 1, .. })));
6277
6278 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6279 assert_eq!(vars[x_export.slot.0], Value::Num(9.0));
6280 }
6281
6282 #[test]
6283 fn compile_carries_cell_end_selector_metadata_for_reads() {
6284 let ast = runmat_parser::parse("c = {1, 2, 3}; x = c{end};").expect("parse");
6285 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6286 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6287 let entrypoint = hir.assembly.entrypoints[0].id;
6288
6289 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6290 let layout = bytecode.layout.as_ref().expect("layout");
6291 let x_export = layout.entrypoints[&entrypoint]
6292 .exports
6293 .iter()
6294 .find(|export| export.name == "x")
6295 .expect("x export");
6296
6297 assert!(bytecode.instructions.iter().any(|instr| {
6298 matches!(
6299 instr,
6300 Instr::IndexCell {
6301 num_indices: 1,
6302 end_offsets,
6303 ..
6304 } if end_offsets == &vec![(0, 0)]
6305 ) || matches!(
6306 instr,
6307 Instr::IndexCellList {
6308 num_indices: 1,
6309 end_offsets,
6310 ..
6311 } if end_offsets == &vec![(0, 0)]
6312 )
6313 }));
6314
6315 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6316 assert_eq!(vars[x_export.slot.0], Value::Num(3.0));
6317 }
6318
6319 #[test]
6320 fn compile_carries_cell_end_selector_metadata_for_stores() {
6321 let ast = runmat_parser::parse("c = {1, 2, 3}; c{end} = 9; x = c{3};").expect("parse");
6322 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6323 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6324 let entrypoint = hir.assembly.entrypoints[0].id;
6325
6326 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6327 let layout = bytecode.layout.as_ref().expect("layout");
6328 let x_export = layout.entrypoints[&entrypoint]
6329 .exports
6330 .iter()
6331 .find(|export| export.name == "x")
6332 .expect("x export");
6333
6334 assert!(bytecode.instructions.iter().any(|instr| matches!(
6335 instr,
6336 Instr::StoreIndexCell {
6337 num_indices: 1,
6338 end_offsets,
6339 ..
6340 } if end_offsets == &vec![(0, 0)]
6341 )));
6342
6343 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6344 assert_eq!(vars[x_export.slot.0], Value::Num(9.0));
6345 }
6346
6347 #[test]
6348 fn compile_carries_cell_end_offset_selector_metadata_in_semantic_function_reads() {
6349 let ast = runmat_parser::parse(
6350 "function y = tail_cell(c); y = c{end-1}; end; c = {1, 2, 3}; x = tail_cell(c);",
6351 )
6352 .expect("parse");
6353 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6354 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6355 let entrypoint = hir.assembly.entrypoints[0].id;
6356
6357 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6358 let function_id = bytecode
6359 .function_registry
6360 .resolve_name("tail_cell")
6361 .expect("tail_cell semantic function id");
6362 let function = bytecode
6363 .function_registry
6364 .get(function_id)
6365 .expect("tail_cell semantic bytecode");
6366 let read_offsets: Vec<Vec<(usize, isize)>> = function
6367 .instructions
6368 .iter()
6369 .filter_map(|instr| match instr {
6370 Instr::IndexCell {
6371 num_indices: 1,
6372 end_offsets,
6373 ..
6374 }
6375 | Instr::IndexCellList {
6376 num_indices: 1,
6377 end_offsets,
6378 ..
6379 } => Some(end_offsets.clone()),
6380 _ => None,
6381 })
6382 .collect();
6383 assert!(
6384 read_offsets.iter().any(|offsets| offsets == &vec![(0, -1)]),
6385 "expected semantic function end-1 metadata offset; actual offsets: {read_offsets:?}; instructions: {:?}",
6386 function.instructions
6387 );
6388
6389 let layout = bytecode.layout.as_ref().expect("layout");
6390 let x_export = layout.entrypoints[&entrypoint]
6391 .exports
6392 .iter()
6393 .find(|export| export.name == "x")
6394 .expect("x export");
6395 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6396 assert_eq!(vars[x_export.slot.0], Value::Num(2.0));
6397 }
6398
6399 #[test]
6400 fn compile_carries_cell_end_offset_selector_metadata_in_semantic_function_stores() {
6401 let ast = runmat_parser::parse(
6402 "function y = patch_cell(c, v); c{end-1} = v; y = c{2}; end; c = {1, 2, 3}; x = patch_cell(c, 9);",
6403 )
6404 .expect("parse");
6405 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6406 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6407 let entrypoint = hir.assembly.entrypoints[0].id;
6408
6409 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6410 let function_id = bytecode
6411 .function_registry
6412 .resolve_name("patch_cell")
6413 .expect("patch_cell semantic function id");
6414 let function = bytecode
6415 .function_registry
6416 .get(function_id)
6417 .expect("patch_cell semantic bytecode");
6418 let store_offsets: Vec<Vec<(usize, isize)>> = function
6419 .instructions
6420 .iter()
6421 .filter_map(|instr| match instr {
6422 Instr::StoreIndexCell {
6423 num_indices: 1,
6424 end_offsets,
6425 ..
6426 } => Some(end_offsets.clone()),
6427 _ => None,
6428 })
6429 .collect();
6430 assert!(
6431 store_offsets
6432 .iter()
6433 .any(|offsets| offsets == &vec![(0, -1)]),
6434 "expected semantic function end-1 metadata offset; actual offsets: {store_offsets:?}"
6435 );
6436
6437 let layout = bytecode.layout.as_ref().expect("layout");
6438 let x_export = layout.entrypoints[&entrypoint]
6439 .exports
6440 .iter()
6441 .find(|export| export.name == "x")
6442 .expect("x export");
6443 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6444 assert_eq!(vars[x_export.slot.0], Value::Num(9.0));
6445 }
6446
6447 #[test]
6448 fn compile_supports_general_cell_end_expression_reads() {
6449 let ast = runmat_parser::parse("c = {10, 20, 30, 40}; x = c{end/2};").expect("parse");
6450 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6451 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6452 let entrypoint = hir.assembly.entrypoints[0].id;
6453
6454 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6455 assert!(bytecode.instructions.iter().any(|instr| {
6456 matches!(
6457 instr,
6458 Instr::IndexCell {
6459 num_indices: 1,
6460 end_exprs,
6461 ..
6462 } if end_exprs.iter().any(|(pos, expr)| {
6463 *pos == 0
6464 && matches!(
6465 expr,
6466 crate::bytecode::EndExpr::Div(left, right)
6467 if matches!(left.as_ref(), crate::bytecode::EndExpr::End)
6468 && matches!(right.as_ref(), crate::bytecode::EndExpr::Const(v) if (*v - 2.0).abs() < f64::EPSILON)
6469 )
6470 })
6471 ) || matches!(
6472 instr,
6473 Instr::IndexCellList {
6474 num_indices: 1,
6475 end_exprs,
6476 ..
6477 } if end_exprs.iter().any(|(pos, expr)| {
6478 *pos == 0
6479 && matches!(
6480 expr,
6481 crate::bytecode::EndExpr::Div(left, right)
6482 if matches!(left.as_ref(), crate::bytecode::EndExpr::End)
6483 && matches!(right.as_ref(), crate::bytecode::EndExpr::Const(v) if (*v - 2.0).abs() < f64::EPSILON)
6484 )
6485 })
6486 )
6487 }));
6488 let layout = bytecode.layout.as_ref().expect("layout");
6489 let x_export = layout.entrypoints[&entrypoint]
6490 .exports
6491 .iter()
6492 .find(|export| export.name == "x")
6493 .expect("x export");
6494 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6495 assert_eq!(vars[x_export.slot.0], Value::Num(20.0));
6496 }
6497
6498 #[test]
6499 fn compile_rejects_fractional_cell_end_expression_read_index() {
6500 let ast = runmat_parser::parse("c = {10, 20, 30, 40, 50}; x = c{end/2};").expect("parse");
6501 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6502 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6503 let entrypoint = hir.assembly.entrypoints[0].id;
6504
6505 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6506 let err = block_on(crate::interpret(&bytecode))
6507 .expect_err("fractional cell end expression selector should fail");
6508 assert_eq!(err.identifier(), Some("RunMat:UnsupportedIndexType"));
6509 }
6510
6511 #[test]
6512 fn compile_supports_general_cell_end_expression_stores() {
6513 let ast = runmat_parser::parse("c = {1, 2, 3, 4}; c{floor(end/2)} = 9; x = c{2};")
6514 .expect("parse");
6515 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6516 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6517 let entrypoint = hir.assembly.entrypoints[0].id;
6518
6519 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6520 assert!(bytecode.instructions.iter().any(|instr| {
6521 matches!(
6522 instr,
6523 Instr::StoreIndexCell {
6524 num_indices: 1,
6525 end_exprs,
6526 ..
6527 } if end_exprs.iter().any(|(pos, expr)| {
6528 *pos == 0
6529 && matches!(
6530 expr,
6531 crate::bytecode::EndExpr::ResolvedCall { args, .. } if args.len() == 1
6532 )
6533 })
6534 )
6535 }));
6536 let layout = bytecode.layout.as_ref().expect("layout");
6537 let x_export = layout.entrypoints[&entrypoint]
6538 .exports
6539 .iter()
6540 .find(|export| export.name == "x")
6541 .expect("x export");
6542 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6543 assert_eq!(vars[x_export.slot.0], Value::Num(9.0));
6544 }
6545
6546 #[test]
6547 fn compile_supports_cell_brace_end_plus_one_growth_for_vectors() {
6548 let ast = runmat_parser::parse("c = {1, 2}; c{end+1} = 9; x = c{3};").expect("parse");
6549 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6550 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6551 let entrypoint = hir.assembly.entrypoints[0].id;
6552
6553 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6554 assert!(bytecode.instructions.iter().any(|instr| {
6555 matches!(
6556 instr,
6557 Instr::StoreIndexCell {
6558 num_indices: 1,
6559 end_offsets,
6560 ..
6561 } if end_offsets == &vec![(0, 1)]
6562 )
6563 }));
6564 let layout = bytecode.layout.as_ref().expect("layout");
6565 let x_export = layout.entrypoints[&entrypoint]
6566 .exports
6567 .iter()
6568 .find(|export| export.name == "x")
6569 .expect("x export");
6570 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6571 assert_eq!(vars[x_export.slot.0], Value::Num(9.0));
6572 }
6573
6574 #[test]
6575 fn compile_supports_cell_brace_linear_gap_growth_for_vectors() {
6576 let ast = runmat_parser::parse(
6577 "c = {1, 2}; c{5} = 9; a = isempty(c{3}); b = isempty(c{4}); x = c{5};",
6578 )
6579 .expect("parse");
6580 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6581 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6582 let entrypoint = hir.assembly.entrypoints[0].id;
6583
6584 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6585 let layout = bytecode.layout.as_ref().expect("layout");
6586 let c_export = layout.entrypoints[&entrypoint]
6587 .exports
6588 .iter()
6589 .find(|export| export.name == "c")
6590 .expect("c export");
6591 let a_export = layout.entrypoints[&entrypoint]
6592 .exports
6593 .iter()
6594 .find(|export| export.name == "a")
6595 .expect("a export");
6596 let b_export = layout.entrypoints[&entrypoint]
6597 .exports
6598 .iter()
6599 .find(|export| export.name == "b")
6600 .expect("b export");
6601 let x_export = layout.entrypoints[&entrypoint]
6602 .exports
6603 .iter()
6604 .find(|export| export.name == "x")
6605 .expect("x export");
6606 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6607 assert_eq!(vars[a_export.slot.0], Value::Bool(true));
6608 assert_eq!(vars[b_export.slot.0], Value::Bool(true));
6609 assert_eq!(vars[x_export.slot.0], Value::Num(9.0));
6610 match &vars[c_export.slot.0] {
6611 Value::Cell(ca) => {
6612 assert_eq!(ca.rows, 1);
6613 assert_eq!(ca.cols, 5);
6614 }
6615 other => panic!("expected cell export, got {other:?}"),
6616 }
6617 }
6618
6619 #[test]
6620 fn compile_supports_cell_brace_linear_end_plus_k_growth_for_vectors() {
6621 let ast = runmat_parser::parse("c = {1, 2}; c{end+3} = 9; a = isempty(c{3}); x = c{5};")
6622 .expect("parse");
6623 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6624 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6625 let entrypoint = hir.assembly.entrypoints[0].id;
6626
6627 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6628 assert!(bytecode.instructions.iter().any(|instr| {
6629 matches!(
6630 instr,
6631 Instr::StoreIndexCell {
6632 num_indices: 1,
6633 end_offsets,
6634 ..
6635 } if end_offsets == &vec![(0, 3)]
6636 )
6637 }));
6638 let layout = bytecode.layout.as_ref().expect("layout");
6639 let a_export = layout.entrypoints[&entrypoint]
6640 .exports
6641 .iter()
6642 .find(|export| export.name == "a")
6643 .expect("a export");
6644 let x_export = layout.entrypoints[&entrypoint]
6645 .exports
6646 .iter()
6647 .find(|export| export.name == "x")
6648 .expect("x export");
6649 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6650 assert_eq!(vars[a_export.slot.0], Value::Bool(true));
6651 assert_eq!(vars[x_export.slot.0], Value::Num(9.0));
6652 }
6653
6654 #[test]
6655 fn compile_linear_cell_growth_from_5_by_0_normalizes_to_row_vector() {
6656 let ast = runmat_parser::parse("c = cell(5,0); c{3} = 2; v = c{3};").expect("parse");
6657 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6658 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6659 let entrypoint = hir.assembly.entrypoints[0].id;
6660 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6661 let layout = bytecode.layout.as_ref().expect("layout");
6662 let c_export = layout.entrypoints[&entrypoint]
6663 .exports
6664 .iter()
6665 .find(|export| export.name == "c")
6666 .expect("c export");
6667 let v_export = layout.entrypoints[&entrypoint]
6668 .exports
6669 .iter()
6670 .find(|export| export.name == "v")
6671 .expect("v export");
6672 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6673 assert_eq!(vars[v_export.slot.0], Value::Num(2.0));
6674 match &vars[c_export.slot.0] {
6675 Value::Cell(ca) => {
6676 assert_eq!(ca.rows, 1);
6677 assert_eq!(ca.cols, 3);
6678 }
6679 other => panic!("expected cell export, got {other:?}"),
6680 }
6681 }
6682
6683 #[test]
6684 fn compile_linear_cell_growth_from_0_by_5_normalizes_to_row_vector() {
6685 let ast = runmat_parser::parse("c = cell(0,5); c{3} = 2; v = c{3};").expect("parse");
6686 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6687 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6688 let entrypoint = hir.assembly.entrypoints[0].id;
6689 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6690 let layout = bytecode.layout.as_ref().expect("layout");
6691 let c_export = layout.entrypoints[&entrypoint]
6692 .exports
6693 .iter()
6694 .find(|export| export.name == "c")
6695 .expect("c export");
6696 let v_export = layout.entrypoints[&entrypoint]
6697 .exports
6698 .iter()
6699 .find(|export| export.name == "v")
6700 .expect("v export");
6701 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6702 assert_eq!(vars[v_export.slot.0], Value::Num(2.0));
6703 match &vars[c_export.slot.0] {
6704 Value::Cell(ca) => {
6705 assert_eq!(ca.rows, 1);
6706 assert_eq!(ca.cols, 3);
6707 }
6708 other => panic!("expected cell export, got {other:?}"),
6709 }
6710 }
6711
6712 #[test]
6713 fn compile_rejects_cell_brace_end_plus_one_growth_for_matrix_linear_assignment() {
6714 let ast = runmat_parser::parse("c = {1, 2; 3, 4}; c{end+1} = 9;").expect("parse");
6715 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6716 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6717 let entrypoint = hir.assembly.entrypoints[0].id;
6718 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6719 let err = block_on(crate::interpret(&bytecode))
6720 .expect_err("matrix linear brace end+1 growth should be rejected");
6721 assert_eq!(err.identifier(), Some("RunMat:UnsupportedCellGrowth"));
6722 }
6723
6724 #[test]
6725 fn compile_supports_cell_brace_subscript_growth_with_empty_fillers() {
6726 let ast =
6727 runmat_parser::parse("c = {1, 2; 3, 4}; c{3,3} = 9; a = c{3,3}; b = isempty(c{2,3});")
6728 .expect("parse");
6729 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6730 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6731 let entrypoint = hir.assembly.entrypoints[0].id;
6732
6733 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6734 let layout = bytecode.layout.as_ref().expect("layout");
6735 let c_export = layout.entrypoints[&entrypoint]
6736 .exports
6737 .iter()
6738 .find(|export| export.name == "c")
6739 .expect("c export");
6740 let a_export = layout.entrypoints[&entrypoint]
6741 .exports
6742 .iter()
6743 .find(|export| export.name == "a")
6744 .expect("a export");
6745 let b_export = layout.entrypoints[&entrypoint]
6746 .exports
6747 .iter()
6748 .find(|export| export.name == "b")
6749 .expect("b export");
6750 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6751
6752 assert_eq!(vars[a_export.slot.0], Value::Num(9.0));
6753 assert_eq!(vars[b_export.slot.0], Value::Bool(true));
6754 match &vars[c_export.slot.0] {
6755 Value::Cell(ca) => {
6756 assert_eq!(ca.rows, 3);
6757 assert_eq!(ca.cols, 3);
6758 }
6759 other => panic!("expected cell export, got {other:?}"),
6760 }
6761 }
6762
6763 #[test]
6764 fn compile_supports_cell_brace_end_plus_one_subscript_growth() {
6765 let ast =
6766 runmat_parser::parse("c = {1, 2; 3, 4}; c{end,end+1} = 8; x = c{2,3};").expect("parse");
6767 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6768 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6769 let entrypoint = hir.assembly.entrypoints[0].id;
6770
6771 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6772 assert!(bytecode.instructions.iter().any(|instr| {
6773 matches!(
6774 instr,
6775 Instr::StoreIndexCell {
6776 num_indices: 2,
6777 end_offsets,
6778 ..
6779 } if end_offsets.contains(&(1, 1))
6780 )
6781 }));
6782 let layout = bytecode.layout.as_ref().expect("layout");
6783 let x_export = layout.entrypoints[&entrypoint]
6784 .exports
6785 .iter()
6786 .find(|export| export.name == "x")
6787 .expect("x export");
6788 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6789 assert_eq!(vars[x_export.slot.0], Value::Num(8.0));
6790 }
6791
6792 #[test]
6793 fn compile_supports_mixed_cell_colon_expansion() {
6794 let ast = runmat_parser::parse("c = {1,2;3,4}; [a,b] = c{:,2}; z = a + b;").expect("parse");
6795 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6796 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6797 let entrypoint = hir.assembly.entrypoints[0].id;
6798
6799 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6800 let layout = bytecode.layout.as_ref().expect("layout");
6801 let z_export = layout.entrypoints[&entrypoint]
6802 .exports
6803 .iter()
6804 .find(|export| export.name == "z")
6805 .expect("z export");
6806
6807 assert!(bytecode.instructions.iter().any(|instr| matches!(
6808 instr,
6809 Instr::IndexCellExpand {
6810 num_indices,
6811 out_count,
6812 ..
6813 } if *num_indices == 2 && *out_count == 2
6814 )));
6815
6816 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6817 assert_eq!(vars[z_export.slot.0], Value::Num(6.0));
6818 }
6819
6820 #[test]
6821 fn compile_3d_slice_roundtrip_uses_slice_expr_paths() {
6822 let ast = runmat_parser::parse(
6823 r#"
6824 A = reshape([1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24], 3, 4, 2);
6825 S = A(1:2, 2:3, end);
6826 A(1:2, 2:3, end) = S;
6827 "#,
6828 )
6829 .expect("parse");
6830 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6831 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6832 let entrypoint = hir.assembly.entrypoints[0].id;
6833
6834 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6835 let mut saw_index_expr = false;
6836 let mut saw_store_expr = false;
6837 for instr in &bytecode.instructions {
6838 if let Instr::IndexSliceExpr {
6839 dims,
6840 end_mask,
6841 end_numeric_exprs,
6842 range_dims,
6843 ..
6844 } = instr
6845 {
6846 if *dims == 3 {
6847 saw_index_expr = true;
6848 assert_eq!(*end_mask, 0);
6849 assert_eq!(end_numeric_exprs.len(), 1);
6850 assert_eq!(range_dims, &vec![0, 1]);
6851 }
6852 }
6853 if let Instr::StoreSliceExpr {
6854 dims,
6855 numeric_count,
6856 colon_mask,
6857 end_mask,
6858 range_dims,
6859 range_has_step,
6860 end_numeric_exprs,
6861 ..
6862 } = instr
6863 {
6864 if *dims == 3 {
6865 saw_store_expr = true;
6866 assert_eq!(*numeric_count, 1);
6867 assert_eq!(*colon_mask, 0);
6868 assert_eq!(*end_mask, 0);
6869 assert_eq!(range_dims, &vec![0, 1]);
6870 assert_eq!(range_has_step, &vec![false, false]);
6871 assert_eq!(end_numeric_exprs.len(), 1);
6872 }
6873 }
6874 }
6875 assert!(saw_index_expr);
6876 assert!(saw_store_expr);
6877
6878 let run = block_on(crate::interpret(&bytecode));
6879 assert!(
6880 run.is_ok(),
6881 "roundtrip script should interpret successfully: {run:?}"
6882 );
6883 }
6884
6885 #[test]
6886 fn compile_interprets_basic_if_statement() {
6887 let ast = runmat_parser::parse("if 1; x = 2; else; x = 3; end").expect("parse");
6888 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6889 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6890 let entrypoint = hir.assembly.entrypoints[0].id;
6891
6892 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6893 let layout = bytecode.layout.as_ref().expect("layout");
6894 let x_export = layout.entrypoints[&entrypoint]
6895 .exports
6896 .iter()
6897 .find(|export| export.name == "x")
6898 .expect("x export");
6899
6900 assert!(bytecode
6901 .instructions
6902 .iter()
6903 .any(|instr| matches!(instr, Instr::JumpIfFalse(_))));
6904
6905 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6906 assert_eq!(vars[x_export.slot.0], Value::Num(2.0));
6907 }
6908
6909 #[test]
6910 fn compile_interprets_basic_switch_statement() {
6911 let ast =
6912 runmat_parser::parse("switch 2; case 1; x = 1; case 2; x = 2; otherwise; x = 3; end")
6913 .expect("parse");
6914 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6915 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6916 let entrypoint = hir.assembly.entrypoints[0].id;
6917
6918 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6919 let layout = bytecode.layout.as_ref().expect("layout");
6920 let x_export = layout.entrypoints[&entrypoint]
6921 .exports
6922 .iter()
6923 .find(|export| export.name == "x")
6924 .expect("x export");
6925
6926 assert!(bytecode
6927 .instructions
6928 .iter()
6929 .any(|instr| matches!(instr, Instr::Equal)));
6930
6931 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6932 assert_eq!(vars[x_export.slot.0], Value::Num(2.0));
6933 }
6934
6935 #[test]
6936 fn compile_lowers_unreachable_terminator() {
6937 let ast = runmat_parser::parse("x = 1;").expect("parse");
6938 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
6939 let mut mir = lower_assembly(&hir.assembly).expect("lower MIR");
6940 let entrypoint = hir.assembly.entrypoints[0].id;
6941 let function = hir.assembly.entrypoints[0].target;
6942 let body = mir.bodies.get_mut(&function).expect("entry body");
6943 body.blocks.last_mut().expect("entry block").terminator.kind =
6944 MirTerminatorKind::Unreachable;
6945
6946 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6947
6948 assert!(bytecode
6949 .instructions
6950 .iter()
6951 .any(|instr| matches!(instr, Instr::Return)));
6952 }
6953
6954 #[test]
6955 fn compile_external_semantic_function_handle_keeps_identity() {
6956 let ast = runmat_parser::parse("h = @remote_inc; y = feval(h, 2);").expect("parse");
6957 let mut bound_functions = HashMap::new();
6958 bound_functions.insert("remote_inc".to_string(), FunctionId(9001));
6959 let context = LoweringContext::empty().with_bound_functions(&bound_functions);
6960 let hir = lower(&ast, &context).expect("lower HIR");
6961 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
6962 let entrypoint = hir.assembly.entrypoints[0].id;
6963
6964 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
6965 assert!(bytecode.instructions.iter().any(|instr| matches!(
6966 instr,
6967 Instr::CreateExternalBoundFunctionHandle(FunctionId(9001), name)
6968 if name == "remote_inc"
6969 )));
6970
6971 let _resolver_guard = runmat_runtime::user_functions::install_semantic_function_resolver(
6972 Some(Arc::new(|name| {
6973 if name == "remote_inc" {
6974 Some(9001)
6975 } else {
6976 None
6977 }
6978 })),
6979 );
6980 let _invoker_guard = runmat_runtime::user_functions::install_semantic_function_invoker(
6981 Some(Arc::new(|function, args, requested_outputs| {
6982 assert_eq!(function, 9001);
6983 assert_eq!(args, &[Value::Num(2.0)]);
6984 assert_eq!(requested_outputs, 1);
6985 Box::pin(async move { Ok(Value::Num(3.0)) })
6986 })),
6987 );
6988
6989 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
6990 assert!(vars
6991 .iter()
6992 .any(|value| matches!(value, Value::Num(n) if (*n - 3.0).abs() < 1e-12)));
6993 }
6994
6995 #[test]
6996 fn compile_external_semantic_direct_call_uses_host_invoker() {
6997 let ast = runmat_parser::parse("y = remote_inc(2);").expect("parse");
6998 let mut bound_functions = HashMap::new();
6999 bound_functions.insert("remote_inc".to_string(), FunctionId(9001));
7000 let context = LoweringContext::empty().with_bound_functions(&bound_functions);
7001 let hir = lower(&ast, &context).expect("lower HIR");
7002 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
7003 let entrypoint = hir.assembly.entrypoints[0].id;
7004
7005 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
7006 assert!(bytecode.instructions.iter().any(|instr| matches!(
7007 instr,
7008 Instr::CallFunctionMulti {
7009 identity: CallableIdentity::ExternalFunction {
7010 function: FunctionId(9001),
7011 ..
7012 },
7013 arg_count: 1,
7014 out_count: 1,
7015 ..
7016 }
7017 )));
7018
7019 let _resolver_guard = runmat_runtime::user_functions::install_semantic_function_resolver(
7020 Some(Arc::new(|name| {
7021 if name == "remote_inc" {
7022 Some(9001)
7023 } else {
7024 None
7025 }
7026 })),
7027 );
7028 let _invoker_guard = runmat_runtime::user_functions::install_semantic_function_invoker(
7029 Some(Arc::new(|function, args, requested_outputs| {
7030 assert_eq!(function, 9001);
7031 assert_eq!(args, &[Value::Num(2.0)]);
7032 assert_eq!(requested_outputs, 1);
7033 Box::pin(async move { Ok(Value::Num(3.0)) })
7034 })),
7035 );
7036
7037 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
7038 assert!(vars
7039 .iter()
7040 .any(|value| matches!(value, Value::Num(n) if (*n - 3.0).abs() < 1e-12)));
7041 }
7042
7043 #[test]
7044 fn compile_rejects_workspace_first_bound_function_fallback() {
7045 let ast = runmat_parser::parse("run(\"setup.m\"); y = remote_inc(2);").expect("parse");
7046 let mut bound_functions = HashMap::new();
7047 bound_functions.insert("remote_inc".to_string(), FunctionId(9001));
7048 let context = LoweringContext::empty().with_bound_functions(&bound_functions);
7049 let hir = lower(&ast, &context).expect("lower HIR");
7050 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
7051 let entrypoint = hir.assembly.entrypoints[0].id;
7052
7053 let err = compile(&hir.assembly, &mir, entrypoint)
7054 .expect_err("workspace-first bound function fallback should be rejected");
7055 assert_eq!(
7056 err.identifier.as_deref(),
7057 Some("RunMat:MirCallFallbackPolicyUnsupported")
7058 );
7059 }
7060
7061 #[test]
7062 fn compile_interprets_async_call_and_await_via_semantic_future_lane() {
7063 let source = "async function y = inc(x); y = x + 1; end; t = inc(2); z = await(t);";
7064 let ast = runmat_parser::parse(source).expect("parse");
7065 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
7066 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
7067 let entrypoint = hir.assembly.entrypoints[0].id;
7068
7069 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
7070 assert!(
7071 bytecode
7072 .instructions
7073 .iter()
7074 .any(|instr| matches!(instr, Instr::CreateSemanticFuture(FunctionId(_), 1, 1))),
7075 "expected async direct call lowering to create a semantic future descriptor"
7076 );
7077 let layout = bytecode.layout.as_ref().expect("layout");
7078 let z_export = layout.entrypoints[&entrypoint]
7079 .exports
7080 .iter()
7081 .find(|export| export.name == "z")
7082 .expect("z export");
7083
7084 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
7085 assert_eq!(vars[z_export.slot.0], Value::Num(3.0));
7086 }
7087
7088 #[test]
7089 fn compile_emits_explicit_spawn_instruction() {
7090 let source = "async function y = inc(x); y = x + 1; end; t = spawn(inc(2)); z = await(t);";
7091 let ast = runmat_parser::parse(source).expect("parse");
7092 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
7093 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
7094 let entrypoint = hir.assembly.entrypoints[0].id;
7095
7096 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
7097 assert!(
7098 bytecode
7099 .instructions
7100 .iter()
7101 .any(|instr| matches!(instr, Instr::Spawn)),
7102 "expected MIR spawn lowering to emit an explicit spawn instruction"
7103 );
7104 assert!(
7105 bytecode
7106 .instructions
7107 .iter()
7108 .any(|instr| matches!(instr, Instr::Await)),
7109 "expected MIR await lowering to emit an explicit await instruction"
7110 );
7111
7112 let layout = bytecode.layout.as_ref().expect("layout");
7113 let z_export = layout.entrypoints[&entrypoint]
7114 .exports
7115 .iter()
7116 .find(|export| export.name == "z")
7117 .expect("z export");
7118 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
7119 assert_eq!(vars[z_export.slot.0], Value::Num(3.0));
7120 }
7121
7122 #[test]
7123 fn compile_lowers_async_expansion_call_to_future_expand_instruction() {
7124 let source = "async function y = inc(x); y = x + 1; end; args = {2}; t = inc(args{:}); z = await(t);";
7125 let ast = runmat_parser::parse(source).expect("parse");
7126 let hir = lower(&ast, &LoweringContext::empty()).expect("lower HIR");
7127 let mir = lower_assembly(&hir.assembly).expect("lower MIR");
7128 let entrypoint = hir.assembly.entrypoints[0].id;
7129
7130 let bytecode = compile(&hir.assembly, &mir, entrypoint).expect("compile");
7131 assert!(
7132 bytecode.instructions.iter().any(|instr| matches!(
7133 instr,
7134 Instr::CreateSemanticFutureExpandMultiOutput(FunctionId(_), _, 1)
7135 )),
7136 "expected async expansion call lowering to create a semantic future expansion descriptor"
7137 );
7138 let layout = bytecode.layout.as_ref().expect("layout");
7139 let z_export = layout.entrypoints[&entrypoint]
7140 .exports
7141 .iter()
7142 .find(|export| export.name == "z")
7143 .expect("z export");
7144 let vars = block_on(crate::interpret(&bytecode)).expect("interpret");
7145 assert_eq!(vars[z_export.slot.0], Value::Num(3.0));
7146 }
7147}