1use crate::accel::fusion as accel_fusion;
2use crate::accel::residency as accel_residency;
3use crate::bytecode::{Bytecode, FunctionRegistry, Instr};
4use crate::interpreter::api::{InterpreterOutcome, InterpreterState};
5use crate::interpreter::dispatch::{self as interp_dispatch, DispatchDecision};
6use crate::interpreter::engine as interp_engine;
7use crate::interpreter::errors::{attach_span_from_pc, mex, set_vm_pc};
8use crate::interpreter::timing::InterpreterTiming;
9use crate::runtime::call_stack::attach_call_frames;
10use crate::runtime::globals as runtime_globals;
11use crate::runtime::workspace::{
12 refresh_workspace_state, workspace_assign, workspace_clear, workspace_lookup, workspace_remove,
13 workspace_snapshot,
14};
15use runmat_builtins::{CellArray, Value};
16use runmat_runtime::builtins::common::validation as arg_validation;
17use runmat_runtime::{
18 user_functions,
19 workspace::{self as runtime_workspace, WorkspaceResolver},
20 RuntimeError,
21};
22use std::cell::RefCell;
23use std::collections::{HashMap, HashSet};
24use std::sync::Arc;
25use std::sync::Once;
26use tracing::{debug, info_span};
27
28#[cfg(not(target_arch = "wasm32"))]
29use std::future::Future;
30
31#[cfg(feature = "native-accel")]
32use runmat_accelerate::{
33 activate_fusion_plan, active_group_plan_clone, deactivate_fusion_plan, set_current_pc,
34};
35
36#[cfg(feature = "native-accel")]
37struct FusionPlanGuard;
38
39#[cfg(feature = "native-accel")]
40impl Drop for FusionPlanGuard {
41 fn drop(&mut self) {
42 deactivate_fusion_plan();
43 }
44}
45
46type VmResult<T> = Result<T, RuntimeError>;
47runmat_thread_local::runmat_thread_local! {
48 static CALL_COUNTS: RefCell<Vec<(usize, usize)>> = const { RefCell::new(Vec::new()) };
49}
50
51fn sync_initial_vars(initial: &mut Vec<Value>, vars: &[Value]) {
52 initial.clear();
53 initial.extend_from_slice(vars);
54}
55
56fn ensure_workspace_resolver_registered() {
57 static REGISTER: Once = Once::new();
58 REGISTER.call_once(|| {
59 runtime_workspace::register_workspace_resolver(WorkspaceResolver {
60 lookup: workspace_lookup,
61 snapshot: workspace_snapshot,
62 globals: runtime_globals::workspace_global_names,
63 assign: Some(workspace_assign),
64 clear: Some(workspace_clear),
65 remove: Some(workspace_remove),
66 });
67 });
68}
69
70fn ensure_wasm_builtins_registered() {
71 #[cfg(target_arch = "wasm32")]
72 {
73 static REGISTER: Once = Once::new();
74 REGISTER.call_once(|| {
75 runmat_runtime::builtins::wasm_registry::register_all();
76 });
77 }
78}
79
80#[cfg(feature = "native-accel")]
81fn clear_residency(value: &Value) {
82 if let Err(err) = accel_residency::clear_value(value) {
83 log::warn!("failed to clear GPU residency: {err}");
84 }
85}
86
87pub async fn invoke_semantic_function_value(
88 function: usize,
89 args: &[Value],
90 requested_outputs: usize,
91 function_registry: &FunctionRegistry,
92) -> Result<Value, RuntimeError> {
93 let (value, _) = invoke_semantic_function_value_with_input_residency(
94 function,
95 args,
96 requested_outputs,
97 function_registry,
98 InputResidency::Transferred,
99 )
100 .await?;
101 Ok(value)
102}
103
104pub(crate) async fn invoke_semantic_function_value_with_capture_updates(
105 function: usize,
106 args: &[Value],
107 requested_outputs: usize,
108 function_registry: &FunctionRegistry,
109) -> Result<(Value, Vec<Value>), RuntimeError> {
110 invoke_semantic_function_value_with_input_residency(
111 function,
112 args,
113 requested_outputs,
114 function_registry,
115 InputResidency::Borrowed,
116 )
117 .await
118}
119
120#[derive(Clone, Copy)]
121enum InputResidency {
122 Borrowed,
124 Transferred,
127}
128
129async fn invoke_semantic_function_value_with_input_residency(
130 function: usize,
131 args: &[Value],
132 requested_outputs: usize,
133 function_registry: &FunctionRegistry,
134 input_residency: InputResidency,
135) -> Result<(Value, Vec<Value>), RuntimeError> {
136 let function_id = runmat_hir::FunctionId(function);
137 let func = function_registry.get(function_id).ok_or_else(|| {
138 let message = format!("Undefined semantic function: {function}");
139 mex("UndefinedSemanticFunction", &message)
140 })?;
141 if args.len() < func.capture_slots.len() {
142 let message = format!(
143 "semantic function {} received too few arguments",
144 func.display_name
145 );
146 return Err(mex("SemanticFunctionArity", &message));
147 }
148 let runtime_arg_count = args.len() - func.capture_slots.len();
149 if runtime_arg_count > func.input_slots.len() && func.varargin_slot.is_none() {
150 let message = format!(
151 "semantic function {} expected {} inputs, got {}",
152 func.display_name,
153 func.input_slots.len(),
154 runtime_arg_count
155 );
156 return Err(mex("TooManyInputs", &message));
157 }
158 if requested_outputs > func.output_slots.len() && func.varargout_slot.is_none() {
159 let message = format!(
160 "semantic function {} expected {} outputs, got {}",
161 func.display_name,
162 func.output_slots.len(),
163 requested_outputs
164 );
165 return Err(mex("TooManyOutputs", &message));
166 }
167
168 let mut vars = vec![Value::Num(0.0); func.var_count];
169 let mut missing_input_slots = HashSet::new();
170 for (slot, value) in func.capture_slots.iter().zip(args.iter()) {
171 if *slot < vars.len() {
172 vars[*slot] = value.clone();
173 }
174 }
175 for (slot, value) in func
176 .input_slots
177 .iter()
178 .take(runtime_arg_count)
179 .zip(args.iter().skip(func.capture_slots.len()))
180 {
181 if *slot < vars.len() {
182 vars[*slot] = value.clone();
183 }
184 }
185 let default_values_by_slot: HashMap<usize, Value> = func
186 .argument_validations
187 .iter()
188 .filter_map(|validation| {
189 validation.default_value.as_ref().map(|value| {
190 let lowered = match value {
191 crate::bytecode::program::FunctionArgDefaultValue::Number(value) => {
192 Value::Num(*value)
193 }
194 crate::bytecode::program::FunctionArgDefaultValue::Bool(value) => {
195 Value::Bool(*value)
196 }
197 crate::bytecode::program::FunctionArgDefaultValue::String(value) => {
198 Value::String(value.clone())
199 }
200 crate::bytecode::program::FunctionArgDefaultValue::EmptyArray => Value::Tensor(
201 runmat_builtins::Tensor::new(Vec::new(), vec![0, 0])
202 .expect("empty default tensor"),
203 ),
204 };
205 (validation.input_slot, lowered)
206 })
207 })
208 .collect();
209 if runtime_arg_count < func.input_slots.len() {
210 for slot in func.input_slots.iter().skip(runtime_arg_count) {
211 if let Some(default_value) = default_values_by_slot.get(slot) {
212 if *slot < vars.len() {
213 vars[*slot] = default_value.clone();
214 }
215 } else {
216 missing_input_slots.insert(*slot);
217 }
218 }
219 }
220 validate_function_arguments(func, &vars, &missing_input_slots)?;
221 if let Some(slot) = func.varargin_slot {
222 let fixed_count = func.input_slots.len();
223 let rest = if runtime_arg_count > fixed_count {
224 args[func.capture_slots.len() + fixed_count..].to_vec()
225 } else {
226 Vec::new()
227 };
228 let cols = rest.len();
229 let cell = CellArray::new(rest, 1, cols)
230 .map_err(|err| mex("VararginPack", &format!("varargin: {err}")))?;
231 if slot < vars.len() {
232 vars[slot] = Value::Cell(cell);
233 }
234 }
235 if let Some(slot) = func.varargout_slot {
236 if slot < vars.len() {
237 let cell = CellArray::new(Vec::new(), 1, 0)
238 .map_err(|err| mex("VarargoutPack", &format!("varargout: {err}")))?;
239 vars[slot] = Value::Cell(cell);
240 }
241 }
242 if let Some(slot) = func.implicit_nargin_slot {
243 if slot < vars.len() {
244 vars[slot] = Value::Num(runtime_arg_count as f64);
245 }
246 }
247 if let Some(slot) = func.implicit_nargout_slot {
248 if slot < vars.len() {
249 vars[slot] = Value::Num(requested_outputs as f64);
250 }
251 }
252
253 let _active_semantic_function_guard =
254 user_functions::push_active_semantic_function(function_id.0);
255 let mut bytecode = Bytecode::with_instructions(func.instructions.clone(), func.var_count);
256 bytecode.instr_spans = func.instr_spans.clone();
257 bytecode.call_arg_spans = func.call_arg_spans.clone();
258 bytecode.source_id = func.source_id;
259 bytecode.var_names = func.var_names.clone();
260 let mut initially_unassigned_slots = func.initially_unassigned_slots.clone();
261 for slot in &func.capture_slots {
262 initially_unassigned_slots.remove(slot);
263 }
264 for slot in func.input_slots.iter().take(runtime_arg_count) {
265 initially_unassigned_slots.remove(slot);
266 }
267 for slot in func.input_slots.iter().skip(runtime_arg_count) {
268 if default_values_by_slot.contains_key(slot) {
269 initially_unassigned_slots.remove(slot);
270 }
271 }
272 if let Some(slot) = func.varargin_slot {
273 initially_unassigned_slots.remove(&slot);
274 }
275 if let Some(slot) = func.varargout_slot {
276 initially_unassigned_slots.remove(&slot);
277 }
278 if let Some(slot) = func.implicit_nargin_slot {
279 initially_unassigned_slots.remove(&slot);
280 }
281 if let Some(slot) = func.implicit_nargout_slot {
282 initially_unassigned_slots.remove(&slot);
283 }
284 bytecode.initially_unassigned_slots = initially_unassigned_slots;
285 bytecode.bound_functions = function_registry.functions.clone();
286 bytecode.function_registry = function_registry.clone();
287 let result_vars = {
288 let future = interpret_function_with_counts(
289 &bytecode,
290 vars,
291 &func.display_name,
292 requested_outputs,
293 runtime_arg_count,
294 missing_input_slots,
295 );
296 #[cfg(target_arch = "wasm32")]
297 {
298 future.await?
299 }
300 #[cfg(not(target_arch = "wasm32"))]
301 {
302 const SEMANTIC_CALL_STACK_BYTES: usize = 16 * 1024 * 1024;
310 let mut future = Box::pin(future);
311 futures::future::poll_fn(move |context| {
312 stacker::grow(SEMANTIC_CALL_STACK_BYTES, || future.as_mut().poll(context))
313 })
314 .await?
315 }
316 };
317 let output_values = collect_semantic_outputs(func, &result_vars, requested_outputs)?;
318 let updated_captures = func
319 .capture_slots
320 .iter()
321 .map(|slot| result_vars.get(*slot).cloned().unwrap_or(Value::Num(0.0)))
322 .collect::<Vec<_>>();
323 #[cfg(feature = "native-accel")]
324 clear_semantic_function_temp_residency(
325 &result_vars,
326 args,
327 &output_values,
328 &updated_captures,
329 input_residency,
330 );
331 Ok((
332 output_value(output_values, requested_outputs),
333 updated_captures,
334 ))
335}
336
337fn validate_function_arguments(
338 func: &crate::bytecode::program::FunctionBytecode,
339 vars: &[Value],
340 missing_input_slots: &HashSet<usize>,
341) -> Result<(), RuntimeError> {
342 for validation in &func.argument_validations {
343 if missing_input_slots.contains(&validation.input_slot) {
344 continue;
345 }
346 let Some(input_index) = func
347 .input_slots
348 .iter()
349 .position(|slot| *slot == validation.input_slot)
350 else {
351 continue;
352 };
353 let value = vars
354 .get(validation.input_slot)
355 .ok_or_else(|| mex("InvalidInputSlot", "function argument slot out of bounds"))?;
356
357 if let Some(size) = &validation.size {
358 let (rows, cols) = arg_validation::value_shape_2d(value);
359 if !dim_matches(&size.rows, rows) || !dim_matches(&size.cols, cols) {
360 return Err(mex(
361 "ArgumentValidationSize",
362 &format!(
363 "Function '{}' argument #{} failed size validation",
364 func.display_name,
365 input_index + 1
366 ),
367 ));
368 }
369 }
370
371 if let Some(class_name) = &validation.class_name {
372 if !arg_validation::value_matches_class(value, class_name) {
373 return Err(mex(
374 "ArgumentValidationClass",
375 &format!(
376 "Function '{}' argument #{} failed class validation (expected {})",
377 func.display_name,
378 input_index + 1,
379 class_name
380 ),
381 ));
382 }
383 }
384 for validator in &validation.validators {
385 match validator {
386 crate::bytecode::program::FunctionArgValidator::A(class_names) => {
387 if !arg_validation::must_be_a(value, class_names.clone())? {
388 return Err(mex(
389 "ArgumentValidationFunction",
390 &format!(
391 "Function '{}' argument #{} failed mustBeA validation",
392 func.display_name,
393 input_index + 1
394 ),
395 ));
396 }
397 }
398 crate::bytecode::program::FunctionArgValidator::Column => {
399 if !arg_validation::value_is_column(value) {
400 return Err(mex(
401 "ArgumentValidationFunction",
402 &format!(
403 "Function '{}' argument #{} failed mustBeColumn validation",
404 func.display_name,
405 input_index + 1
406 ),
407 ));
408 }
409 }
410 crate::bytecode::program::FunctionArgValidator::Finite => {
411 if !arg_validation::value_is_finite(value) {
412 return Err(mex(
413 "ArgumentValidationFunction",
414 &format!(
415 "Function '{}' argument #{} failed mustBeFinite validation",
416 func.display_name,
417 input_index + 1
418 ),
419 ));
420 }
421 }
422 crate::bytecode::program::FunctionArgValidator::Float => {
423 if !arg_validation::value_is_float(value) {
424 return Err(mex(
425 "ArgumentValidationFunction",
426 &format!(
427 "Function '{}' argument #{} failed mustBeFloat validation",
428 func.display_name,
429 input_index + 1
430 ),
431 ));
432 }
433 }
434 crate::bytecode::program::FunctionArgValidator::Folder => {
435 if arg_validation::dispatch_validator("mustBeFolder", vec![value.clone()])
436 .is_err()
437 {
438 return Err(mex(
439 "ArgumentValidationFunction",
440 &format!(
441 "Function '{}' argument #{} failed mustBeFolder validation",
442 func.display_name,
443 input_index + 1
444 ),
445 ));
446 }
447 }
448 crate::bytecode::program::FunctionArgValidator::File => {
449 if arg_validation::dispatch_validator("mustBeFile", vec![value.clone()])
450 .is_err()
451 {
452 return Err(mex(
453 "ArgumentValidationFunction",
454 &format!(
455 "Function '{}' argument #{} failed mustBeFile validation",
456 func.display_name,
457 input_index + 1
458 ),
459 ));
460 }
461 }
462 crate::bytecode::program::FunctionArgValidator::NumericOrLogical => {
463 if !arg_validation::value_is_numeric_or_logical(value) {
464 return Err(mex(
465 "ArgumentValidationFunction",
466 &format!(
467 "Function '{}' argument #{} failed mustBeNumericOrLogical validation",
468 func.display_name,
469 input_index + 1
470 ),
471 ));
472 }
473 }
474 crate::bytecode::program::FunctionArgValidator::Numeric => {
475 if !arg_validation::value_is_numeric(value) {
476 return Err(mex(
477 "ArgumentValidationFunction",
478 &format!(
479 "Function '{}' argument #{} failed mustBeNumeric validation",
480 func.display_name,
481 input_index + 1
482 ),
483 ));
484 }
485 }
486 crate::bytecode::program::FunctionArgValidator::Text => {
487 if !arg_validation::value_is_text(value) {
488 return Err(mex(
489 "ArgumentValidationFunction",
490 &format!(
491 "Function '{}' argument #{} failed mustBeText validation",
492 func.display_name,
493 input_index + 1
494 ),
495 ));
496 }
497 }
498 crate::bytecode::program::FunctionArgValidator::TextScalar => {
499 if !arg_validation::value_is_text_scalar(value) {
500 return Err(mex(
501 "ArgumentValidationFunction",
502 &format!(
503 "Function '{}' argument #{} failed mustBeTextScalar validation",
504 func.display_name,
505 input_index + 1
506 ),
507 ));
508 }
509 }
510 crate::bytecode::program::FunctionArgValidator::NonzeroLengthText => {
511 if !arg_validation::value_is_nonzero_length_text(value) {
512 return Err(mex(
513 "ArgumentValidationFunction",
514 &format!(
515 "Function '{}' argument #{} failed mustBeNonzeroLengthText validation",
516 func.display_name,
517 input_index + 1
518 ),
519 ));
520 }
521 }
522 crate::bytecode::program::FunctionArgValidator::Nonempty => {
523 if arg_validation::value_is_empty(value) {
524 return Err(mex(
525 "ArgumentValidationFunction",
526 &format!(
527 "Function '{}' argument #{} failed mustBeNonempty validation",
528 func.display_name,
529 input_index + 1
530 ),
531 ));
532 }
533 }
534 crate::bytecode::program::FunctionArgValidator::ScalarOrEmpty => {
535 if !arg_validation::value_is_scalar_or_empty(value) {
536 return Err(mex(
537 "ArgumentValidationFunction",
538 &format!(
539 "Function '{}' argument #{} failed mustBeScalarOrEmpty validation",
540 func.display_name,
541 input_index + 1
542 ),
543 ));
544 }
545 }
546 crate::bytecode::program::FunctionArgValidator::Real => {
547 if !arg_validation::value_is_real(value) {
548 return Err(mex(
549 "ArgumentValidationFunction",
550 &format!(
551 "Function '{}' argument #{} failed mustBeReal validation",
552 func.display_name,
553 input_index + 1
554 ),
555 ));
556 }
557 }
558 crate::bytecode::program::FunctionArgValidator::Integer => {
559 if !arg_validation::value_is_integer(value) {
560 return Err(mex(
561 "ArgumentValidationFunction",
562 &format!(
563 "Function '{}' argument #{} failed mustBeInteger validation",
564 func.display_name,
565 input_index + 1
566 ),
567 ));
568 }
569 }
570 crate::bytecode::program::FunctionArgValidator::Vector => {
571 if !arg_validation::value_is_vector(value)? {
572 return Err(mex(
573 "ArgumentValidationFunction",
574 &format!(
575 "Function '{}' argument #{} failed mustBeVector validation",
576 func.display_name,
577 input_index + 1
578 ),
579 ));
580 }
581 }
582 crate::bytecode::program::FunctionArgValidator::Positive => {
583 if !arg_validation::value_is_positive(value) {
584 return Err(mex(
585 "ArgumentValidationFunction",
586 &format!(
587 "Function '{}' argument #{} failed mustBePositive validation",
588 func.display_name,
589 input_index + 1
590 ),
591 ));
592 }
593 }
594 crate::bytecode::program::FunctionArgValidator::Negative => {
595 if !arg_validation::value_is_negative(value) {
596 return Err(mex(
597 "ArgumentValidationFunction",
598 &format!(
599 "Function '{}' argument #{} failed mustBeNegative validation",
600 func.display_name,
601 input_index + 1
602 ),
603 ));
604 }
605 }
606 crate::bytecode::program::FunctionArgValidator::Nonnegative => {
607 if !arg_validation::value_is_nonnegative(value) {
608 return Err(mex(
609 "ArgumentValidationFunction",
610 &format!(
611 "Function '{}' argument #{} failed mustBeNonnegative validation",
612 func.display_name,
613 input_index + 1
614 ),
615 ));
616 }
617 }
618 crate::bytecode::program::FunctionArgValidator::Nonmissing => {
619 if !arg_validation::value_is_nonmissing(value) {
620 return Err(mex(
621 "ArgumentValidationFunction",
622 &format!(
623 "Function '{}' argument #{} failed mustBeNonmissing validation",
624 func.display_name,
625 input_index + 1
626 ),
627 ));
628 }
629 }
630 crate::bytecode::program::FunctionArgValidator::NonNan => {
631 if !arg_validation::value_is_non_nan(value) {
632 return Err(mex(
633 "ArgumentValidationFunction",
634 &format!(
635 "Function '{}' argument #{} failed mustBeNonNan validation",
636 func.display_name,
637 input_index + 1
638 ),
639 ));
640 }
641 }
642 crate::bytecode::program::FunctionArgValidator::Nonzero => {
643 if !arg_validation::value_is_nonzero(value) {
644 return Err(mex(
645 "ArgumentValidationFunction",
646 &format!(
647 "Function '{}' argument #{} failed mustBeNonzero validation",
648 func.display_name,
649 input_index + 1
650 ),
651 ));
652 }
653 }
654 crate::bytecode::program::FunctionArgValidator::Nonpositive => {
655 if !arg_validation::value_is_nonpositive(value) {
656 return Err(mex(
657 "ArgumentValidationFunction",
658 &format!(
659 "Function '{}' argument #{} failed mustBeNonpositive validation",
660 func.display_name,
661 input_index + 1
662 ),
663 ));
664 }
665 }
666 crate::bytecode::program::FunctionArgValidator::Nonsparse => {
667 if matches!(value, Value::SparseTensor(_)) {
668 return Err(mex(
669 "ArgumentValidationFunction",
670 &format!(
671 "Function '{}' argument #{} failed mustBeNonsparse validation",
672 func.display_name,
673 input_index + 1
674 ),
675 ));
676 }
677 }
678 crate::bytecode::program::FunctionArgValidator::Sparse => {
679 if !matches!(value, Value::SparseTensor(_)) {
680 return Err(mex(
681 "ArgumentValidationFunction",
682 &format!(
683 "Function '{}' argument #{} failed mustBeSparse validation",
684 func.display_name,
685 input_index + 1
686 ),
687 ));
688 }
689 }
690 crate::bytecode::program::FunctionArgValidator::ValidVariableName => {
691 if !arg_validation::isvarname_value(value) {
692 return Err(mex(
693 "ArgumentValidationFunction",
694 &format!(
695 "Function '{}' argument #{} failed mustBeValidVariableName validation",
696 func.display_name,
697 input_index + 1
698 ),
699 ));
700 }
701 }
702 crate::bytecode::program::FunctionArgValidator::UnderlyingType(class_names) => {
703 if !arg_validation::value_underlying_type_matches(value, class_names.clone())? {
704 return Err(mex(
705 "ArgumentValidationFunction",
706 &format!(
707 "Function '{}' argument #{} failed mustBeUnderlyingType validation",
708 func.display_name,
709 input_index + 1
710 ),
711 ));
712 }
713 }
714 crate::bytecode::program::FunctionArgValidator::Member(literals) => {
715 let allowed: Vec<_> = literals.iter().map(validation_literal_to_atom).collect();
716 if !arg_validation::value_is_member_atoms(value, &allowed)? {
717 return Err(mex(
718 "ArgumentValidationFunction",
719 &format!(
720 "Function '{}' argument #{} failed mustBeMember validation",
721 func.display_name,
722 input_index + 1
723 ),
724 ));
725 }
726 }
727 crate::bytecode::program::FunctionArgValidator::InRange(
728 lower,
729 upper,
730 inclusivity,
731 ) => {
732 if !arg_validation::value_is_in_range(
733 value,
734 *lower,
735 *upper,
736 arg_validation::RangeInclusivity {
737 lower: inclusivity.lower,
738 upper: inclusivity.upper,
739 },
740 ) {
741 return Err(mex(
742 "ArgumentValidationFunction",
743 &format!(
744 "Function '{}' argument #{} failed mustBeInRange validation",
745 func.display_name,
746 input_index + 1
747 ),
748 ));
749 }
750 }
751 crate::bytecode::program::FunctionArgValidator::GreaterThanOrEqual(threshold) => {
752 if !arg_validation::value_is_greater_than_or_equal(value, *threshold) {
753 return Err(mex(
754 "ArgumentValidationFunction",
755 &format!(
756 "Function '{}' argument #{} failed mustBeGreaterThanOrEqual validation",
757 func.display_name,
758 input_index + 1
759 ),
760 ));
761 }
762 }
763 crate::bytecode::program::FunctionArgValidator::LessThanOrEqual(threshold) => {
764 if !arg_validation::value_is_less_than_or_equal(value, *threshold) {
765 return Err(mex(
766 "ArgumentValidationFunction",
767 &format!(
768 "Function '{}' argument #{} failed mustBeLessThanOrEqual validation",
769 func.display_name,
770 input_index + 1
771 ),
772 ));
773 }
774 }
775 crate::bytecode::program::FunctionArgValidator::GreaterThan(threshold) => {
776 if !arg_validation::value_is_greater_than(value, *threshold) {
777 return Err(mex(
778 "ArgumentValidationFunction",
779 &format!(
780 "Function '{}' argument #{} failed mustBeGreaterThan validation",
781 func.display_name,
782 input_index + 1
783 ),
784 ));
785 }
786 }
787 crate::bytecode::program::FunctionArgValidator::LessThan(threshold) => {
788 if !arg_validation::value_is_less_than(value, *threshold) {
789 return Err(mex(
790 "ArgumentValidationFunction",
791 &format!(
792 "Function '{}' argument #{} failed mustBeLessThan validation",
793 func.display_name,
794 input_index + 1
795 ),
796 ));
797 }
798 }
799 }
800 }
801 }
802 Ok(())
803}
804
805fn validation_literal_to_atom(
806 literal: &crate::bytecode::program::FunctionArgValidationLiteral,
807) -> arg_validation::ValidationAtom {
808 match literal {
809 crate::bytecode::program::FunctionArgValidationLiteral::Number(value) => {
810 arg_validation::ValidationAtom::Number(*value)
811 }
812 crate::bytecode::program::FunctionArgValidationLiteral::Text(value) => {
813 arg_validation::ValidationAtom::Text(value.clone())
814 }
815 crate::bytecode::program::FunctionArgValidationLiteral::Bool(value) => {
816 arg_validation::ValidationAtom::Bool(*value)
817 }
818 }
819}
820
821fn dim_matches(dim: &crate::bytecode::program::FunctionArgDim, actual: usize) -> bool {
822 match dim {
823 crate::bytecode::program::FunctionArgDim::Any => true,
824 crate::bytecode::program::FunctionArgDim::Exact(expected) => *expected == actual,
825 }
826}
827
828fn collect_semantic_outputs(
829 func: &crate::bytecode::program::FunctionBytecode,
830 result_vars: &[Value],
831 requested_outputs: usize,
832) -> Result<Vec<Value>, RuntimeError> {
833 let mut values = Vec::with_capacity(requested_outputs.max(1));
834 for slot in func.output_slots.iter().take(requested_outputs) {
835 values.push(result_vars.get(*slot).cloned().unwrap_or(Value::Num(0.0)));
836 }
837 if values.len() < requested_outputs {
838 if let Some(slot) = func.varargout_slot {
839 let available = match result_vars.get(slot) {
840 Some(Value::Cell(cell)) => {
841 let expanded = crate::call::shared::expand_all_cell(cell)?;
842 let available = expanded.len();
843 for value in expanded {
844 if values.len() >= requested_outputs {
845 break;
846 }
847 values.push(value);
848 }
849 available
850 }
851 _ => 0,
852 };
853 if values.len() < requested_outputs {
854 let need = requested_outputs - func.output_slots.len();
855 let message = format!(
856 "Function '{}' returned {available} varargout values, {need} requested",
857 func.display_name
858 );
859 return Err(mex("VarargoutMismatch", &message));
860 }
861 }
862 }
863 while values.len() < requested_outputs {
864 values.push(Value::Num(0.0));
865 }
866 Ok(values)
867}
868
869fn output_value(output_values: Vec<Value>, requested_outputs: usize) -> Value {
870 match requested_outputs {
871 0 => Value::OutputList(Vec::new()),
872 1 => output_values.into_iter().next().unwrap_or(Value::Num(0.0)),
873 _ => Value::OutputList(output_values.into_iter().take(requested_outputs).collect()),
874 }
875}
876
877#[cfg(feature = "native-accel")]
878fn clear_semantic_function_temp_residency(
879 result_vars: &[Value],
880 args: &[Value],
881 output_values: &[Value],
882 updated_captures: &[Value],
883 input_residency: InputResidency,
884) {
885 let mut keep_values = output_values.to_vec();
886 if matches!(input_residency, InputResidency::Borrowed) {
887 keep_values.extend(args.iter().cloned());
891 }
892 keep_values.extend(updated_captures.iter().cloned());
893 keep_values.extend(runtime_globals::collect_thread_roots());
894 let keep = Value::OutputList(keep_values);
895 for value in result_vars {
896 if let Err(err) = accel_residency::clear_value_excluding(value, &keep) {
897 log::warn!("failed to clear temporary semantic function GPU residency: {err}");
898 }
899 }
900}
901
902pub async fn interpret_with_vars(
903 bytecode: &Bytecode,
904 initial_vars: &mut Vec<Value>,
905 current_function_name: Option<&str>,
906) -> VmResult<InterpreterOutcome> {
907 runmat_runtime::data::with_tx_registry_scope(interpret_with_vars_inner(
908 bytecode,
909 initial_vars,
910 current_function_name,
911 ))
912 .await
913}
914
915async fn interpret_with_vars_inner(
916 bytecode: &Bytecode,
917 initial_vars: &mut Vec<Value>,
918 current_function_name: Option<&str>,
919) -> VmResult<InterpreterOutcome> {
920 let _debug_frame_guard = runmat_runtime::debug_context::push_frame(
921 current_function_name.unwrap_or("<main>"),
922 bytecode.source_id,
923 bytecode_frame_span(bytecode),
924 );
925 let call_counts = CALL_COUNTS.with(|cc| cc.borrow().clone());
926 let state = Box::new(InterpreterState::new(
927 bytecode.clone(),
928 initial_vars,
929 current_function_name,
930 call_counts,
931 ));
932 match Box::pin(run_interpreter(state, initial_vars)).await {
933 Ok(outcome) => Ok(outcome),
934 Err(err) => {
935 let err = attach_span_from_pc(bytecode, err);
936 let current_name = current_function_name.unwrap_or("<main>");
937 Err(attach_call_frames(bytecode, current_name, err))
938 }
939 }
940}
941
942fn bytecode_frame_span(bytecode: &Bytecode) -> Option<(usize, usize)> {
943 bytecode
944 .instr_spans
945 .first()
946 .map(|span| (span.start, span.end))
947}
948
949async fn run_interpreter(
950 state: Box<InterpreterState>,
951 initial_vars: &mut Vec<Value>,
952) -> VmResult<InterpreterOutcome> {
953 let state = *state;
954 Box::pin(run_interpreter_inner(state, initial_vars)).await
955}
956
957async fn run_interpreter_inner(
958 state: InterpreterState,
959 initial_vars: &mut Vec<Value>,
960) -> VmResult<InterpreterOutcome> {
961 let run_span = info_span!(
962 "interpreter.run",
963 function = state.current_function_name.as_str()
964 );
965 let _run_guard = run_span.enter();
966 ensure_wasm_builtins_registered();
967 ensure_workspace_resolver_registered();
968 #[cfg(feature = "native-accel")]
969 activate_fusion_plan(state.fusion_plan.clone());
970 #[cfg(feature = "native-accel")]
971 let _fusion_guard = FusionPlanGuard;
972 let InterpreterState {
973 mut stack,
974 mut vars,
975 mut pc,
976 mut context,
977 mut try_stack,
978 mut last_exception,
979 mut imports,
980 mut global_aliases,
981 mut persistent_aliases,
982 mut missing_input_slots,
983 current_function_name,
984 call_counts,
985 initial_assigned_var_count,
986 #[cfg(feature = "native-accel")]
987 fusion_plan: _,
988 #[cfg(feature = "native-accel")]
989 fusion_accel_graph,
990 bytecode,
991 } = state;
992 let _source_context_guard =
993 runmat_runtime::source_context::replace_current_source_id(bytecode.source_id);
994 let _arity_call_counts_guard =
995 runmat_runtime::builtins::introspection::arity_check::replace_call_counts(
996 call_counts.clone(),
997 );
998 let function_registry = Arc::new(bytecode.function_registry());
999 let previous_semantic_invoker = user_functions::current_semantic_function_invoker();
1000 let registry_for_function_invoker = Arc::clone(&function_registry);
1001 let _semantic_function_guard =
1002 user_functions::install_semantic_function_invoker(Some(Arc::new(
1003 move |function: usize, args: &[Value], requested_outputs: usize| {
1004 let args = args.to_vec();
1005 let previous_invoker = previous_semantic_invoker.clone();
1006 let function_registry = Arc::clone(®istry_for_function_invoker);
1007 Box::pin(async move {
1008 let local_function = function_registry
1009 .get(runmat_hir::FunctionId(function))
1010 .is_some();
1011 if !local_function {
1012 if let Some(invoker) = previous_invoker {
1013 return invoker(function, &args, requested_outputs).await;
1014 }
1015 }
1016 invoke_semantic_function_value_with_capture_updates(
1017 function,
1018 &args,
1019 requested_outputs,
1020 &function_registry,
1021 )
1022 .await
1023 .map(|(value, _)| value)
1024 })
1025 },
1026 )));
1027 let previous_semantic_resolver = user_functions::current_semantic_function_resolver();
1028 let registry_for_function_resolver = Arc::clone(&function_registry);
1029 let _semantic_resolver_guard =
1030 user_functions::install_semantic_function_resolver(Some(Arc::new(move |name: &str| {
1031 if let Some(active_function) = user_functions::current_active_semantic_function() {
1032 if let Some(function) =
1033 registry_for_function_resolver.get(runmat_hir::FunctionId(active_function))
1034 {
1035 if let Some(scoped_function) = registry_for_function_resolver
1036 .resolve_name_in_private_scope(&function.private_owner_scope, name)
1037 {
1038 return Some(scoped_function.0);
1039 }
1040 }
1041 }
1042 if let Some(function) = registry_for_function_resolver.resolve_name(name) {
1043 return Some(function.0);
1044 }
1045 previous_semantic_resolver
1046 .as_ref()
1047 .and_then(|resolver| resolver(name))
1048 })));
1049 let mut source_function_catalog = function_registry
1050 .functions
1051 .values()
1052 .filter_map(|function| {
1053 function.source_id.map(
1054 |source_id| runmat_runtime::user_functions::SourceFunctionInfo {
1055 source_id,
1056 name: function.display_name.clone(),
1057 function: function.function.0,
1058 },
1059 )
1060 })
1061 .collect::<Vec<_>>();
1062 source_function_catalog.sort_by_key(|info| info.function);
1063 let _source_function_catalog_guard =
1064 user_functions::install_source_function_catalog(Some(Arc::new(source_function_catalog)));
1065 CALL_COUNTS.with(|cc| {
1066 *cc.borrow_mut() = call_counts.clone();
1067 });
1068 let _workspace_guard = interp_engine::prepare_workspace_guard(
1069 &bytecode.var_names,
1070 &mut vars,
1071 initial_assigned_var_count,
1072 &bytecode.initially_unassigned_slots,
1073 );
1074 let thread_roots: Vec<Value> = runtime_globals::collect_thread_roots();
1075 let mut _gc_context = interp_engine::create_gc_context(&stack, &vars, thread_roots)?;
1076 let debug_stack = interp_engine::debug_stack_enabled();
1077 let mut interpreter_timing = InterpreterTiming::new();
1078 while pc < bytecode.instructions.len() {
1079 set_vm_pc(pc);
1080 #[cfg(feature = "native-accel")]
1081 set_current_pc(pc);
1082 if let Err(err) = interp_engine::check_cancelled() {
1083 #[cfg(feature = "native-accel")]
1084 {
1085 for value in &stack {
1086 clear_residency(value);
1087 }
1088 for value in &vars {
1089 clear_residency(value);
1090 }
1091 }
1092 return Err(err);
1093 }
1094 #[cfg(feature = "native-accel")]
1095 if let (Some(plan), Some(graph)) = (active_group_plan_clone(), fusion_accel_graph.as_ref())
1096 {
1097 if plan.group.span.start == pc {
1098 #[cfg(feature = "native-accel")]
1099 {
1100 interp_engine::note_fusion_gate(
1101 &mut interpreter_timing,
1102 &plan,
1103 &bytecode,
1104 pc,
1105 accel_fusion::fusion_span_has_vm_barrier(
1106 &bytecode.instructions,
1107 &plan.group.span,
1108 ),
1109 accel_fusion::fusion_span_live_result_count(
1110 &bytecode.instructions,
1111 &plan.group.span,
1112 ),
1113 );
1114 }
1115 let span = plan.group.span.clone();
1116 let has_barrier =
1117 accel_fusion::fusion_span_has_vm_barrier(&bytecode.instructions, &span);
1118 let _fusion_span = info_span!(
1119 "fusion.execute",
1120 span_start = plan.group.span.start,
1121 span_end = plan.group.span.end,
1122 kind = ?plan.group.kind
1123 )
1124 .entered();
1125 if !has_barrier {
1126 match accel_fusion::try_execute_fusion_group(
1127 &plan,
1128 graph,
1129 &mut stack,
1130 &mut vars,
1131 &mut context,
1132 )
1133 .await
1134 {
1135 Ok(result) => {
1136 stack.push(result);
1137 pc = plan.group.span.end + 1;
1138 continue;
1139 }
1140 Err(err) => {
1141 log::debug!("fusion fallback at pc {}: {}", pc, err);
1142 }
1143 }
1144 } else {
1145 interp_engine::note_fusion_skip(pc, &span);
1146 }
1147 }
1148 }
1149 interp_engine::note_pre_dispatch(
1150 &mut interpreter_timing,
1151 debug_stack,
1152 pc,
1153 &bytecode.instructions[pc],
1154 stack.len(),
1155 );
1156 let call_counts_snapshot = CALL_COUNTS.with(|cc| cc.borrow().clone());
1157 let store_var_global_aliases = match &bytecode.instructions[pc] {
1158 Instr::StoreVar(_) => Some(global_aliases.clone()),
1159 _ => None,
1160 };
1161 let store_local_global_aliases = match &bytecode.instructions[pc] {
1162 Instr::StoreLocal(_) => Some(global_aliases.clone()),
1163 _ => None,
1164 };
1165 let mut clear_value_residency = |value: &Value| {
1166 #[cfg(feature = "native-accel")]
1167 clear_residency(value);
1168 };
1169 let mut store_var_before_overwrite = |_current: &Value, _incoming: &Value| {};
1170 let mut store_var_after_store = |stored_index: usize, stored_value: &Value| {
1171 if let Some(ref aliases) = store_var_global_aliases {
1172 runtime_globals::update_global_store(stored_index, stored_value, aliases);
1173 }
1174 };
1175 let mut store_local_before_local_overwrite = |_current: &Value, _incoming: &Value| {};
1176 let mut store_local_before_var_overwrite = |_current: &Value, _incoming: &Value| {};
1177 let mut store_local_after_store = |stored_offset: usize, stored_value: &Value| {
1178 if let Some(ref aliases) = store_local_global_aliases {
1179 runtime_globals::update_global_store(stored_offset, stored_value, aliases);
1180 }
1181 };
1182 let mut store_local_after_fallback_store =
1183 |func_name: &str, stored_offset: usize, stored_value: &Value| {
1184 if let Some(ref aliases) = store_local_global_aliases {
1185 runtime_globals::update_global_store(stored_offset, stored_value, aliases);
1186 }
1187 runtime_globals::update_persistent_local_store(
1188 func_name,
1189 stored_offset,
1190 stored_value,
1191 );
1192 };
1193 let dispatch_result = interp_dispatch::dispatch_instruction(
1194 interp_dispatch::DispatchMeta {
1195 instr: &bytecode.instructions[pc],
1196 var_names: &bytecode.var_names,
1197 function_registry: &function_registry,
1198 source_id: bytecode.source_id,
1199 call_arg_spans: bytecode.call_arg_spans.get(pc).cloned().flatten(),
1200 call_counts: &call_counts_snapshot,
1201 current_function_name: ¤t_function_name,
1202 },
1203 interp_dispatch::DispatchState {
1204 stack: &mut stack,
1205 vars: &mut vars,
1206 context: &mut context,
1207 try_stack: &mut try_stack,
1208 last_exception: &mut last_exception,
1209 imports: &mut imports,
1210 global_aliases: &mut global_aliases,
1211 persistent_aliases: &mut persistent_aliases,
1212 missing_input_slots: &mut missing_input_slots,
1213 pc: &mut pc,
1214 },
1215 interp_dispatch::DispatchHooks {
1216 clear_value_residency: &mut clear_value_residency,
1217 store_var_before_overwrite: &mut store_var_before_overwrite,
1218 store_var_after_store: &mut store_var_after_store,
1219 store_local_before_local_overwrite: &mut store_local_before_local_overwrite,
1220 store_local_before_var_overwrite: &mut store_local_before_var_overwrite,
1221 store_local_after_store: &mut store_local_after_store,
1222 store_local_after_fallback_store: &mut store_local_after_fallback_store,
1223 },
1224 )
1225 .await;
1226 let dispatch_result = match dispatch_result {
1227 Ok(result) => result,
1228 Err(err) => match interp_dispatch::redirect_exception_to_catch(
1229 err,
1230 &mut try_stack,
1231 &mut vars,
1232 &mut last_exception,
1233 &mut pc,
1234 refresh_workspace_state,
1235 ) {
1236 interp_dispatch::ExceptionHandling::Caught => {
1237 continue;
1238 }
1239 interp_dispatch::ExceptionHandling::Uncaught(err) => return Err(*err),
1240 },
1241 };
1242 if let Some(decision) = dispatch_result {
1243 match decision {
1244 interp_dispatch::DispatchHandled::Generic(DispatchDecision::ContinueLoop) => {
1245 continue;
1246 }
1247 interp_dispatch::DispatchHandled::Generic(DispatchDecision::FallThrough) => {
1248 pc += 1;
1249 continue;
1250 }
1251 interp_dispatch::DispatchHandled::Generic(DispatchDecision::Return) => {
1252 interpreter_timing.flush_host_span("return", None);
1253 break;
1254 }
1255 interp_dispatch::DispatchHandled::ReturnValue(DispatchDecision::ContinueLoop)
1256 | interp_dispatch::DispatchHandled::Return(DispatchDecision::ContinueLoop) => {
1257 continue;
1258 }
1259 interp_dispatch::DispatchHandled::ReturnValue(DispatchDecision::Return) => {
1260 interpreter_timing.flush_host_span("return_value", None);
1261 break;
1262 }
1263 interp_dispatch::DispatchHandled::Return(DispatchDecision::Return) => {
1264 interpreter_timing.flush_host_span("return", None);
1265 break;
1266 }
1267 interp_dispatch::DispatchHandled::ReturnValue(DispatchDecision::FallThrough)
1268 | interp_dispatch::DispatchHandled::Return(DispatchDecision::FallThrough) => {
1269 pc += 1;
1270 continue;
1271 }
1272 }
1273 }
1274 match bytecode.instructions[pc].clone() {
1275 Instr::EmitStackTop { .. }
1276 | Instr::EmitVar { .. }
1277 | Instr::AndAnd(_)
1278 | Instr::OrOr(_)
1279 | Instr::JumpIfFalse(_)
1280 | Instr::Jump(_)
1281 | Instr::LoadConst(_)
1282 | Instr::LoadComplex(_, _)
1283 | Instr::LoadBool(_)
1284 | Instr::LoadString(_)
1285 | Instr::LoadCharRow(_)
1286 | Instr::LoadLocal(_)
1287 | Instr::LoadVar(_)
1288 | Instr::LoadVarForIndexAssignment(_)
1289 | Instr::StoreVar(_)
1290 | Instr::StoreLocal(_)
1291 | Instr::Swap
1292 | Instr::Pop
1293 | Instr::EnterTry(_, _)
1294 | Instr::PopTry
1295 | Instr::ReturnValue
1296 | Instr::Return
1297 | Instr::EnterScope(_)
1298 | Instr::LoadMember(_)
1299 | Instr::LoadMemberOrInit(_)
1300 | Instr::LoadMemberDynamic
1301 | Instr::LoadMemberDynamicOrInit
1302 | Instr::StoreMember(_)
1303 | Instr::StoreMemberOrInit(_)
1304 | Instr::StoreMemberDynamic
1305 | Instr::StoreMemberDynamicOrInit
1306 | Instr::Index(_)
1307 | Instr::IndexSlice(_, _, _, _)
1308 | Instr::IndexSliceExpr { .. }
1309 | Instr::IndexCell { .. }
1310 | Instr::IndexCellExpand { .. }
1311 | Instr::IndexCellList { .. }
1312 | Instr::StoreIndex(_)
1313 | Instr::StoreIndexCell { .. }
1314 | Instr::StoreIndexDelete(_)
1315 | Instr::StoreIndexCellDelete { .. }
1316 | Instr::StoreSlice(_, _, _, _)
1317 | Instr::StoreSliceDelete(_, _, _, _)
1318 | Instr::StoreSliceExpr { .. }
1319 | Instr::StoreSliceExprDelete { .. }
1320 | Instr::CallMethodOrMemberIndexMulti { .. }
1321 | Instr::CallMethodOrMemberIndexExpandMultiOutput { .. }
1322 | Instr::LoadMethod(_)
1323 | Instr::CreateFunctionHandle(_)
1324 | Instr::CreateExternalFunctionHandle(_)
1325 | Instr::CreateMethodFunctionHandle(_)
1326 | Instr::CreateBoundFunctionHandle(_, _)
1327 | Instr::CreateExternalBoundFunctionHandle(_, _)
1328 | Instr::CreateClosure(_, _)
1329 | Instr::CreateSemanticClosure(_, _, _)
1330 | Instr::LoadStaticProperty(_, _)
1331 | Instr::LoadWorkspaceFirstStaticProperty { .. }
1332 | Instr::RegisterClass { .. }
1333 | Instr::CallFevalMulti(_, _)
1334 | Instr::CallFevalMultiUsingOutputSlot(_, _)
1335 | Instr::CallFevalExpandMultiOutput(_, _)
1336 | Instr::CallFevalExpandMultiOutputUsingOutputSlot(_, _)
1337 | Instr::CreateSemanticFuture(_, _, _)
1338 | Instr::CreateSemanticFutureExpandMultiOutput(_, _, _)
1339 | Instr::Spawn
1340 | Instr::Await
1341 | Instr::CallBuiltinMulti(_, _, _)
1342 | Instr::CallBuiltinMultiUsingOutputSlot(_, _, _)
1343 | Instr::CallSuperConstructorMulti { .. }
1344 | Instr::CallSuperMethodMulti { .. }
1345 | Instr::CallSemanticFunctionMulti(_, _, _)
1346 | Instr::CallSemanticFunctionMultiUsingOutputSlot(_, _, _)
1347 | Instr::CallSemanticNestedFunctionMulti { .. }
1348 | Instr::CallSemanticNestedFunctionMultiUsingOutputSlot { .. }
1349 | Instr::CallFunctionMulti { .. }
1350 | Instr::CallFunctionMultiUsingOutputSlot { .. }
1351 | Instr::CallFunctionExpandMultiOutput { .. }
1352 | Instr::CallWorkspaceFirstMulti { .. }
1353 | Instr::CallWorkspaceFirstMultiUsingOutputSlot { .. }
1354 | Instr::CallWorkspaceFirstExpandMultiOutput { .. }
1355 | Instr::CallWorkspaceFirstExpandMultiOutputUsingOutputSlot { .. }
1356 | Instr::CallSemanticFunctionExpandMultiOutput(_, _, _)
1357 | Instr::CallSemanticNestedFunctionExpandMultiOutput { .. }
1358 | Instr::CallBuiltinExpandMultiOutput(_, _, _)
1359 | Instr::CallSuperConstructorExpandMultiOutput { .. }
1360 | Instr::CallSuperMethodExpandMultiOutput { .. }
1361 | Instr::ExitScope(_)
1362 | Instr::RegisterImport { .. }
1363 | Instr::DeclareGlobal(_)
1364 | Instr::DeclareGlobalNamed(_, _)
1365 | Instr::DeclarePersistent(_)
1366 | Instr::DeclarePersistentNamed(_, _)
1367 | Instr::CreateCell2D(_, _)
1368 | Instr::CreateStructLiteral(_)
1369 | Instr::CreateObjectLiteral { .. }
1370 | Instr::Add
1371 | Instr::Sub
1372 | Instr::Mul
1373 | Instr::ElemMul
1374 | Instr::ElemDiv
1375 | Instr::ElemPow
1376 | Instr::ElemLeftDiv
1377 | Instr::Neg
1378 | Instr::UPlus
1379 | Instr::Transpose
1380 | Instr::ConjugateTranspose
1381 | Instr::Pow
1382 | Instr::RightDiv
1383 | Instr::LeftDiv
1384 | Instr::LessEqual
1385 | Instr::Less
1386 | Instr::Greater
1387 | Instr::GreaterEqual
1388 | Instr::Equal
1389 | Instr::NotEqual
1390 | Instr::LogicalNot
1391 | Instr::LogicalAnd
1392 | Instr::LogicalOr
1393 | Instr::Unpack(_)
1394 | Instr::CreateMatrix(_, _)
1395 | Instr::CreateMatrixDynamic(_)
1396 | Instr::CreateRange(_)
1397 | Instr::PackToRow(_)
1398 | Instr::PackToCol(_) => unreachable!("handled by dispatch_instruction"),
1399 Instr::StochasticEvolution => {
1400 let steps_value = stack
1401 .pop()
1402 .ok_or(mex("StackUnderflow", "stack underflow"))?;
1403 let scale_value = stack
1404 .pop()
1405 .ok_or(mex("StackUnderflow", "stack underflow"))?;
1406 let drift_value = stack
1407 .pop()
1408 .ok_or(mex("StackUnderflow", "stack underflow"))?;
1409 let state_value = stack
1410 .pop()
1411 .ok_or(mex("StackUnderflow", "stack underflow"))?;
1412 let evolved =
1413 crate::accel::idioms::stochastic_evolution::execute_stochastic_evolution(
1414 state_value,
1415 drift_value,
1416 scale_value,
1417 steps_value,
1418 )
1419 .await?;
1420 stack.push(evolved);
1421 }
1422 }
1423 if debug_stack {
1424 debug!(pc, stack_len = stack.len(), "[vm] after exec");
1425 }
1426 pc += 1;
1427 }
1428 interpreter_timing.flush_host_span("loop_complete", None);
1429 #[cfg(feature = "native-accel")]
1430 {
1431 let mut live_values = Vec::with_capacity(vars.len() + context.locals.len());
1432 live_values.extend(vars.iter().cloned());
1433 live_values.extend(context.locals.iter().cloned());
1434 live_values.extend(runtime_globals::collect_thread_roots());
1435 let live_values = Value::OutputList(live_values);
1436 for value in &stack {
1437 if let Err(err) = accel_residency::clear_value_excluding(value, &live_values) {
1438 log::warn!("failed to clear stack GPU residency: {err}");
1439 }
1440 }
1441 }
1442 sync_initial_vars(initial_vars, &vars);
1443 Ok(InterpreterOutcome::Completed(vars))
1444}
1445
1446pub async fn interpret(bytecode: &Bytecode) -> Result<Vec<Value>, RuntimeError> {
1447 let mut vars = vec![Value::Num(0.0); bytecode.var_count];
1448 match interpret_with_vars(bytecode, &mut vars, Some("<main>")).await {
1449 Ok(InterpreterOutcome::Completed(values)) => Ok(values),
1450 Err(e) => Err(e),
1451 }
1452}
1453
1454pub async fn interpret_function(
1455 bytecode: &Bytecode,
1456 vars: Vec<Value>,
1457) -> Result<Vec<Value>, RuntimeError> {
1458 interpret_function_with_counts(bytecode, vars, "<anonymous>", 0, 0, HashSet::new()).await
1459}
1460
1461pub async fn interpret_function_with_counts(
1462 bytecode: &Bytecode,
1463 vars: Vec<Value>,
1464 name: &str,
1465 out_count: usize,
1466 in_count: usize,
1467 missing_input_slots: HashSet<usize>,
1468) -> Result<Vec<Value>, RuntimeError> {
1469 let mut vars = vars;
1470 CALL_COUNTS.with(|cc| {
1471 cc.borrow_mut().push((in_count, out_count));
1472 });
1473 let call_counts = CALL_COUNTS.with(|cc| cc.borrow().clone());
1474 let mut state = InterpreterState::new(bytecode.clone(), &mut vars, Some(name), call_counts);
1475 state.missing_input_slots = missing_input_slots;
1476 let _debug_frame_guard = runmat_runtime::debug_context::push_frame(
1477 name,
1478 bytecode.source_id,
1479 bytecode_frame_span(bytecode),
1480 );
1481 let res = Box::pin(run_interpreter(Box::new(state), &mut vars)).await;
1482 CALL_COUNTS.with(|cc| {
1483 cc.borrow_mut().pop();
1484 });
1485 let res = match res {
1486 Ok(InterpreterOutcome::Completed(values)) => Ok(values),
1487 Err(e) => Err(e),
1488 }?;
1489 runtime_globals::persist_declared_for_bytecode(bytecode, name, &vars);
1490 Ok(res)
1491}
1492
1493#[cfg(test)]
1494mod tests {
1495 use super::{
1496 collect_semantic_outputs, interpret_with_vars, output_value, run_interpreter_inner,
1497 };
1498 use crate::bytecode::program::{Bytecode, FunctionBytecode};
1499 use crate::bytecode::Instr;
1500 use crate::interpreter::api::InterpreterState;
1501 use futures::executor::block_on;
1502 use runmat_builtins::{
1503 CellArray, Closure, HandleRef, ObjectInstance, StructValue, Tensor, Value,
1504 };
1505 use runmat_hir::FunctionId;
1506 use runmat_runtime::builtins::common::validation::{
1507 value_is_empty, value_is_greater_than, value_is_greater_than_or_equal, value_is_integer,
1508 value_is_less_than, value_is_less_than_or_equal, value_is_negative, value_is_nonnegative,
1509 value_is_nonpositive, value_is_nonzero, value_is_numeric_or_logical, value_is_positive,
1510 value_is_real, value_is_scalar_or_empty, value_is_text,
1511 };
1512 use std::collections::{HashMap, HashSet};
1513 use std::sync::{atomic::AtomicBool, Arc};
1514 #[cfg(feature = "native-accel")]
1515 use {
1516 once_cell::sync::Lazy,
1517 runmat_accelerate::simple_provider::InProcessProvider,
1518 runmat_accelerate_api::{AccelProvider, HostTensorView, ThreadProviderGuard},
1519 };
1520
1521 #[cfg(feature = "native-accel")]
1522 static TEST_PROVIDER: Lazy<InProcessProvider> = Lazy::new(InProcessProvider::new);
1523
1524 #[cfg(feature = "native-accel")]
1525 fn upload_provider_handle(
1526 data: Vec<f64>,
1527 shape: Vec<usize>,
1528 ) -> runmat_accelerate_api::GpuTensorHandle {
1529 TEST_PROVIDER
1530 .upload(&HostTensorView {
1531 data: &data,
1532 shape: &shape,
1533 })
1534 .expect("upload should succeed")
1535 }
1536
1537 fn test_function(varargout_slot: Option<usize>) -> FunctionBytecode {
1538 FunctionBytecode {
1539 function: FunctionId(0),
1540 display_name: "f".into(),
1541 private_owner_scope: String::new(),
1542 source_id: None,
1543 instructions: vec![Instr::Return],
1544 instr_spans: Vec::new(),
1545 call_arg_spans: Vec::new(),
1546 var_count: 1,
1547 input_slots: Vec::new(),
1548 varargin_slot: None,
1549 implicit_nargin_slot: None,
1550 output_slots: Vec::new(),
1551 varargout_slot,
1552 implicit_nargout_slot: None,
1553 capture_slots: Vec::new(),
1554 var_names: HashMap::new(),
1555 initially_unassigned_slots: HashSet::new(),
1556 argument_validations: Vec::new(),
1557 }
1558 }
1559
1560 #[test]
1561 fn collect_outputs_zero_requested_does_not_consume_varargout() {
1562 let func = test_function(Some(0));
1563 let varargout = CellArray::new(vec![Value::Num(7.0)], 1, 1).expect("cell");
1564 let result_vars = vec![Value::Cell(varargout)];
1565 let outputs = collect_semantic_outputs(&func, &result_vars, 0).expect("collect");
1566 assert!(outputs.is_empty());
1567 }
1568
1569 #[test]
1570 fn collect_outputs_one_requested_reads_varargout() {
1571 let func = test_function(Some(0));
1572 let varargout = CellArray::new(vec![Value::Num(7.0)], 1, 1).expect("cell");
1573 let result_vars = vec![Value::Cell(varargout)];
1574 let outputs = collect_semantic_outputs(&func, &result_vars, 1).expect("collect");
1575 assert_eq!(outputs, vec![Value::Num(7.0)]);
1576 }
1577
1578 #[test]
1579 fn output_value_zero_requested_is_empty_output_list() {
1580 let value = output_value(vec![Value::Num(1.0)], 0);
1581 assert_eq!(value, Value::OutputList(Vec::new()));
1582 }
1583
1584 #[test]
1585 fn output_value_multi_requested_returns_output_list() {
1586 let value = output_value(vec![Value::Num(1.0), Value::Num(2.0)], 2);
1587 assert_eq!(
1588 value,
1589 Value::OutputList(vec![Value::Num(1.0), Value::Num(2.0)])
1590 );
1591 }
1592
1593 #[test]
1594 fn numeric_or_logical_validator_accepts_expected_domains() {
1595 assert!(value_is_numeric_or_logical(&Value::Num(1.0)));
1596 assert!(value_is_numeric_or_logical(&Value::Bool(true)));
1597 assert!(value_is_numeric_or_logical(&Value::Complex(1.0, 2.0)));
1598 let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1599 assert!(value_is_numeric_or_logical(&Value::Tensor(tensor)));
1600 assert!(!value_is_numeric_or_logical(&Value::String(
1601 "x".to_string()
1602 )));
1603 assert!(!value_is_numeric_or_logical(&Value::CharArray(
1604 runmat_builtins::CharArray::new("x".chars().collect(), 1, 1).expect("char")
1605 )));
1606 }
1607
1608 #[test]
1609 fn text_validator_accepts_string_char_vector_and_cellstr() {
1610 assert!(value_is_text(&Value::String("x".to_string())));
1611 assert!(value_is_text(&Value::CharArray(
1612 runmat_builtins::CharArray::new("abc".chars().collect(), 1, 3).expect("char")
1613 )));
1614 assert!(value_is_text(&Value::Cell(
1615 CellArray::new(
1616 vec![
1617 Value::CharArray(
1618 runmat_builtins::CharArray::new("a".chars().collect(), 1, 1).expect("char"),
1619 ),
1620 Value::String("b".to_string()),
1621 ],
1622 1,
1623 2,
1624 )
1625 .expect("cell"),
1626 )));
1627 assert!(!value_is_text(&Value::Num(1.0)));
1628 }
1629
1630 #[test]
1631 fn nonempty_validator_rejects_empty_arrays_and_cells() {
1632 let empty_num = Tensor::new(Vec::new(), vec![0, 0]).expect("empty tensor");
1633 assert!(value_is_empty(&Value::Tensor(empty_num)));
1634 let empty_char =
1635 runmat_builtins::CharArray::new(Vec::new(), 1, 0).expect("empty char array");
1636 assert!(value_is_empty(&Value::CharArray(empty_char)));
1637 let empty_cell = CellArray::new(Vec::new(), 0, 0).expect("empty cell");
1638 assert!(value_is_empty(&Value::Cell(empty_cell)));
1639 assert!(!value_is_empty(&Value::String("".to_string())));
1640 assert!(!value_is_empty(&Value::Num(1.0)));
1641 }
1642
1643 #[test]
1644 fn scalar_or_empty_validator_accepts_scalar_or_empty_shapes() {
1645 assert!(value_is_scalar_or_empty(&Value::Num(1.0)));
1646 assert!(value_is_scalar_or_empty(&Value::Bool(true)));
1647 let empty_num = Tensor::new(Vec::new(), vec![0, 0]).expect("empty tensor");
1648 assert!(value_is_scalar_or_empty(&Value::Tensor(empty_num)));
1649 let matrix = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("matrix");
1650 assert!(!value_is_scalar_or_empty(&Value::Tensor(matrix)));
1651 }
1652
1653 #[test]
1654 fn real_validator_rejects_imaginary_values() {
1655 assert!(value_is_real(&Value::Num(1.0)));
1656 assert!(value_is_real(&Value::Complex(1.0, 0.0)));
1657 assert!(!value_is_real(&Value::Complex(1.0, 2.0)));
1658 let complex_real = runmat_builtins::ComplexTensor::new(vec![(1.0, 0.0)], vec![1, 1])
1659 .expect("complex tensor");
1660 let complex_imag = runmat_builtins::ComplexTensor::new(vec![(1.0, 2.0)], vec![1, 1])
1661 .expect("complex tensor");
1662 assert!(value_is_real(&Value::ComplexTensor(complex_real)));
1663 assert!(!value_is_real(&Value::ComplexTensor(complex_imag)));
1664 }
1665
1666 #[test]
1667 fn integer_validator_accepts_integer_valued_numeric_and_logical_inputs() {
1668 assert!(value_is_integer(&Value::Int(
1669 runmat_builtins::IntValue::I64(3)
1670 )));
1671 assert!(value_is_integer(&Value::Num(3.0)));
1672 assert!(!value_is_integer(&Value::Num(3.5)));
1673 let tensor = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1674 assert!(value_is_integer(&Value::Tensor(tensor)));
1675 let non_integer = Tensor::new(vec![1.0, 2.5], vec![1, 2]).expect("tensor");
1676 assert!(!value_is_integer(&Value::Tensor(non_integer)));
1677 assert!(value_is_integer(&Value::Bool(true)));
1678 assert!(value_is_integer(&Value::LogicalArray(
1679 runmat_builtins::LogicalArray::new(vec![0, 1], vec![1, 2]).expect("logical array")
1680 )));
1681 }
1682
1683 #[test]
1684 fn positive_validator_rejects_zero_and_negative_values() {
1685 assert!(value_is_positive(&Value::Num(1.0)));
1686 assert!(!value_is_positive(&Value::Num(0.0)));
1687 assert!(!value_is_positive(&Value::Num(-1.0)));
1688 assert!(value_is_positive(&Value::Int(
1689 runmat_builtins::IntValue::I64(2)
1690 )));
1691 assert!(!value_is_positive(&Value::Int(
1692 runmat_builtins::IntValue::I64(0)
1693 )));
1694 let positive = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1695 assert!(value_is_positive(&Value::Tensor(positive)));
1696 let mixed = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
1697 assert!(!value_is_positive(&Value::Tensor(mixed)));
1698 }
1699
1700 #[test]
1701 fn negative_validator_rejects_zero_and_positive_values() {
1702 assert!(value_is_negative(&Value::Num(-1.0)));
1703 assert!(!value_is_negative(&Value::Num(0.0)));
1704 assert!(!value_is_negative(&Value::Num(1.0)));
1705 assert!(value_is_negative(&Value::Int(
1706 runmat_builtins::IntValue::I64(-2)
1707 )));
1708 let ok = Tensor::new(vec![-1.0, -2.0], vec![1, 2]).expect("tensor");
1709 assert!(value_is_negative(&Value::Tensor(ok)));
1710 let bad = Tensor::new(vec![-1.0, 0.0], vec![1, 2]).expect("tensor");
1711 assert!(!value_is_negative(&Value::Tensor(bad)));
1712 }
1713
1714 #[test]
1715 fn nonnegative_validator_accepts_zero_and_positive_values() {
1716 assert!(value_is_nonnegative(&Value::Num(0.0)));
1717 assert!(value_is_nonnegative(&Value::Num(2.0)));
1718 assert!(!value_is_nonnegative(&Value::Num(-1.0)));
1719 assert!(value_is_nonnegative(&Value::Int(
1720 runmat_builtins::IntValue::I64(0)
1721 )));
1722 let ok = Tensor::new(vec![0.0, 1.0], vec![1, 2]).expect("tensor");
1723 assert!(value_is_nonnegative(&Value::Tensor(ok)));
1724 let bad = Tensor::new(vec![0.0, -1.0], vec![1, 2]).expect("tensor");
1725 assert!(!value_is_nonnegative(&Value::Tensor(bad)));
1726 }
1727
1728 #[test]
1729 fn nonzero_validator_rejects_zero_values() {
1730 assert!(value_is_nonzero(&Value::Num(1.0)));
1731 assert!(!value_is_nonzero(&Value::Num(0.0)));
1732 assert!(value_is_nonzero(&Value::Int(
1733 runmat_builtins::IntValue::I64(2)
1734 )));
1735 assert!(!value_is_nonzero(&Value::Int(
1736 runmat_builtins::IntValue::I64(0)
1737 )));
1738 assert!(value_is_nonzero(&Value::Complex(0.0, 1.0)));
1739 assert!(!value_is_nonzero(&Value::Complex(0.0, 0.0)));
1740 let ok = Tensor::new(vec![1.0, 2.0], vec![1, 2]).expect("tensor");
1741 assert!(value_is_nonzero(&Value::Tensor(ok)));
1742 let bad = Tensor::new(vec![1.0, 0.0], vec![1, 2]).expect("tensor");
1743 assert!(!value_is_nonzero(&Value::Tensor(bad)));
1744 }
1745
1746 #[test]
1747 fn nonpositive_validator_accepts_zero_and_negative_values() {
1748 assert!(value_is_nonpositive(&Value::Num(0.0)));
1749 assert!(value_is_nonpositive(&Value::Num(-2.0)));
1750 assert!(!value_is_nonpositive(&Value::Num(1.0)));
1751 assert!(value_is_nonpositive(&Value::Int(
1752 runmat_builtins::IntValue::I64(0)
1753 )));
1754 let ok = Tensor::new(vec![0.0, -1.0], vec![1, 2]).expect("tensor");
1755 assert!(value_is_nonpositive(&Value::Tensor(ok)));
1756 let bad = Tensor::new(vec![0.0, 1.0], vec![1, 2]).expect("tensor");
1757 assert!(!value_is_nonpositive(&Value::Tensor(bad)));
1758 }
1759
1760 #[test]
1761 fn greater_than_or_equal_validator_uses_numeric_threshold() {
1762 assert!(value_is_greater_than_or_equal(&Value::Num(2.0), 0.0));
1763 assert!(value_is_greater_than_or_equal(&Value::Num(0.0), 0.0));
1764 assert!(!value_is_greater_than_or_equal(&Value::Num(-1.0), 0.0));
1765 }
1766
1767 #[test]
1768 fn less_than_or_equal_validator_uses_numeric_threshold() {
1769 assert!(value_is_less_than_or_equal(&Value::Num(-1.0), 0.0));
1770 assert!(value_is_less_than_or_equal(&Value::Num(0.0), 0.0));
1771 assert!(!value_is_less_than_or_equal(&Value::Num(1.0), 0.0));
1772 }
1773
1774 #[test]
1775 fn greater_than_and_less_than_validators_use_numeric_threshold() {
1776 assert!(value_is_greater_than(&Value::Num(2.0), 1.0));
1777 assert!(!value_is_greater_than(&Value::Num(1.0), 1.0));
1778 assert!(value_is_less_than(&Value::Num(-2.0), -1.0));
1779 assert!(!value_is_less_than(&Value::Num(-1.0), -1.0));
1780 }
1781
1782 #[cfg(feature = "native-accel")]
1783 #[test]
1784 fn cancellation_clears_gpu_residency_for_live_values() {
1785 use runmat_accelerate::fusion_residency;
1786 use runmat_accelerate_api::GpuTensorHandle;
1787
1788 let handle = GpuTensorHandle {
1789 shape: vec![1, 1],
1790 device_id: 0,
1791 buffer_id: 777_001,
1792 };
1793 fusion_residency::mark(&handle);
1794 assert!(fusion_residency::is_resident(&handle));
1795
1796 let mut vars = vec![Value::GpuTensor(handle.clone())];
1797 let bytecode = Bytecode::with_instructions(vec![Instr::Return], vars.len());
1798 let cancelled = Arc::new(AtomicBool::new(true));
1799 let _interrupt_guard = runmat_runtime::interrupt::replace_interrupt(Some(cancelled));
1800
1801 let err = block_on(interpret_with_vars(&bytecode, &mut vars, Some("<main>")))
1802 .expect_err("cancelled execution should return error");
1803 assert_eq!(err.identifier(), Some("RunMat:ExecutionCancelled"));
1804 assert!(
1805 !fusion_residency::is_resident(&handle),
1806 "cancelled execution should clear residency marks for live GPU handles"
1807 );
1808 }
1809
1810 #[cfg(feature = "native-accel")]
1811 #[test]
1812 fn completion_clears_stack_only_gpu_residency() {
1813 use runmat_accelerate::fusion_residency;
1814 use runmat_accelerate_api::GpuTensorHandle;
1815
1816 let handle = GpuTensorHandle {
1817 shape: vec![1, 1],
1818 device_id: 0,
1819 buffer_id: 777_002,
1820 };
1821 fusion_residency::mark(&handle);
1822 assert!(fusion_residency::is_resident(&handle));
1823
1824 let bytecode = Bytecode::with_instructions(Vec::new(), 1);
1825 let mut seed_vars = vec![Value::Num(0.0)];
1826 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
1827 state.stack.push(Value::GpuTensor(handle.clone()));
1828 state.vars = vec![Value::Num(0.0)];
1829
1830 let mut result_vars = vec![Value::Num(0.0)];
1831 let outcome = block_on(run_interpreter_inner(state, &mut result_vars))
1832 .expect("interpreter should complete");
1833 assert!(matches!(
1834 outcome,
1835 crate::interpreter::api::InterpreterOutcome::Completed(_)
1836 ));
1837 assert!(
1838 !fusion_residency::is_resident(&handle),
1839 "completion should clear residency marks for stack-only GPU handles"
1840 );
1841 }
1842
1843 #[cfg(feature = "native-accel")]
1844 #[test]
1845 fn pop_releases_stack_only_provider_handle() {
1846 use runmat_accelerate::fusion_residency;
1847
1848 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
1849 let handle = upload_provider_handle(vec![9.0], vec![1]);
1850 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
1851 fusion_residency::mark(&handle);
1852
1853 let bytecode = Bytecode::with_instructions(vec![Instr::Pop, Instr::Return], 1);
1854 let mut seed_vars = vec![Value::Num(0.0)];
1855 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
1856 state.stack.push(Value::GpuTensor(handle.clone()));
1857 state.vars = vec![Value::Num(0.0)];
1858
1859 let mut result_vars = vec![Value::Num(0.0)];
1860 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
1861 .expect("interpreter should complete");
1862 assert!(
1863 !fusion_residency::is_resident(&handle),
1864 "pop should clear residency for stack-only handles"
1865 );
1866 assert!(
1867 block_on(TEST_PROVIDER.download(&handle)).is_err(),
1868 "pop should release provider storage for stack-only handles"
1869 );
1870 }
1871
1872 #[cfg(feature = "native-accel")]
1873 #[test]
1874 fn pop_preserves_provider_handle_when_still_live_in_vars() {
1875 use runmat_accelerate::fusion_residency;
1876
1877 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
1878 let handle = upload_provider_handle(vec![11.0], vec![1]);
1879 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
1880 fusion_residency::mark(&handle);
1881
1882 let bytecode = Bytecode::with_instructions(vec![Instr::Pop, Instr::Return], 1);
1883 let mut seed_vars = vec![Value::GpuTensor(handle.clone())];
1884 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
1885 state.stack.push(Value::GpuTensor(handle.clone()));
1886 state.vars = vec![Value::GpuTensor(handle.clone())];
1887
1888 let mut result_vars = vec![Value::GpuTensor(handle.clone())];
1889 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
1890 .expect("interpreter should complete");
1891 assert!(
1892 fusion_residency::is_resident(&handle),
1893 "pop should preserve residency for handles still referenced by vars"
1894 );
1895 assert!(
1896 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
1897 "pop should not release provider storage for handles still referenced by vars"
1898 );
1899 fusion_residency::clear(&handle);
1900 let _ = TEST_PROVIDER.free(&handle);
1901 }
1902
1903 #[cfg(feature = "native-accel")]
1904 #[test]
1905 fn exit_scope_releases_local_only_provider_handle() {
1906 use runmat_accelerate::fusion_residency;
1907
1908 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
1909 let handle = upload_provider_handle(vec![15.0], vec![1]);
1910 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
1911 fusion_residency::mark(&handle);
1912
1913 let bytecode = Bytecode::with_instructions(vec![Instr::ExitScope(1), Instr::Return], 1);
1914 let mut seed_vars = vec![Value::Num(0.0)];
1915 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
1916 state.context.locals.push(Value::GpuTensor(handle.clone()));
1917 state.vars = vec![Value::Num(0.0)];
1918
1919 let mut result_vars = vec![Value::Num(0.0)];
1920 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
1921 .expect("exit scope should complete");
1922 assert!(
1923 !fusion_residency::is_resident(&handle),
1924 "exit scope should clear residency for local-only handles"
1925 );
1926 assert!(
1927 block_on(TEST_PROVIDER.download(&handle)).is_err(),
1928 "exit scope should release provider storage for local-only handles"
1929 );
1930 }
1931
1932 #[cfg(feature = "native-accel")]
1933 #[test]
1934 fn exit_scope_preserves_provider_handle_when_still_live_in_vars() {
1935 use runmat_accelerate::fusion_residency;
1936
1937 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
1938 let handle = upload_provider_handle(vec![17.0], vec![1]);
1939 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
1940 fusion_residency::mark(&handle);
1941
1942 let bytecode = Bytecode::with_instructions(vec![Instr::ExitScope(1), Instr::Return], 1);
1943 let mut seed_vars = vec![Value::GpuTensor(handle.clone())];
1944 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
1945 state.context.locals.push(Value::GpuTensor(handle.clone()));
1946 state.vars = vec![Value::GpuTensor(handle.clone())];
1947
1948 let mut result_vars = vec![Value::GpuTensor(handle.clone())];
1949 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
1950 .expect("exit scope should complete");
1951 assert!(
1952 fusion_residency::is_resident(&handle),
1953 "exit scope should preserve residency for handles still referenced by vars"
1954 );
1955 assert!(
1956 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
1957 "exit scope should not release provider storage for handles still referenced by vars"
1958 );
1959 fusion_residency::clear(&handle);
1960 let _ = TEST_PROVIDER.free(&handle);
1961 }
1962
1963 #[cfg(feature = "native-accel")]
1964 #[test]
1965 fn exit_scope_releases_nested_handle_object_local_provider_handle() {
1966 use runmat_accelerate::fusion_residency;
1967
1968 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
1969 let handle = upload_provider_handle(vec![18.0], vec![1]);
1970 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
1971 fusion_residency::mark(&handle);
1972
1973 let bytecode = Bytecode::with_instructions(vec![Instr::ExitScope(1), Instr::Return], 1);
1974 let mut seed_vars = vec![Value::Num(0.0)];
1975 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
1976 let mut payload = StructValue::new();
1977 payload
1978 .fields
1979 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
1980 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
1981 state.context.locals.push(Value::HandleObject(HandleRef {
1982 class_name: "Payload".to_string(),
1983 target,
1984 valid: true,
1985 }));
1986 state.vars = vec![Value::Num(0.0)];
1987
1988 let mut result_vars = vec![Value::Num(0.0)];
1989 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
1990 .expect("exit scope should complete for nested handle-object local");
1991 assert!(
1992 !fusion_residency::is_resident(&handle),
1993 "exit scope should clear residency for nested handle-object local-only handles"
1994 );
1995 assert!(
1996 block_on(TEST_PROVIDER.download(&handle)).is_err(),
1997 "exit scope should release provider storage for nested handle-object local-only handles"
1998 );
1999 }
2000
2001 #[cfg(feature = "native-accel")]
2002 #[test]
2003 fn exit_scope_preserves_nested_handle_object_provider_handle_when_still_live_in_vars() {
2004 use runmat_accelerate::fusion_residency;
2005
2006 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2007 let handle = upload_provider_handle(vec![20.0], vec![1]);
2008 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2009 fusion_residency::mark(&handle);
2010
2011 let bytecode = Bytecode::with_instructions(vec![Instr::ExitScope(1), Instr::Return], 1);
2012 let mut seed_vars = vec![Value::Num(0.0)];
2013 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2014 let mut payload = StructValue::new();
2015 payload
2016 .fields
2017 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2018 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
2019 let local_value = Value::HandleObject(HandleRef {
2020 class_name: "Payload".to_string(),
2021 target,
2022 valid: true,
2023 });
2024 state.context.locals.push(local_value.clone());
2025 state.vars = vec![local_value.clone()];
2026
2027 let mut result_vars = vec![local_value];
2028 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2029 .expect("exit scope should complete for aliased nested handle-object local");
2030 assert!(
2031 fusion_residency::is_resident(&handle),
2032 "exit scope should preserve residency for nested handle-object handles still referenced by vars"
2033 );
2034 assert!(
2035 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2036 "exit scope should not release provider storage for nested handle-object handles still referenced by vars"
2037 );
2038 fusion_residency::clear(&handle);
2039 let _ = TEST_PROVIDER.free(&handle);
2040 }
2041
2042 #[cfg(feature = "native-accel")]
2043 #[test]
2044 fn store_var_overwrite_preserves_provider_handle_when_shared_in_other_var() {
2045 use runmat_accelerate::fusion_residency;
2046
2047 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2048 let handle = upload_provider_handle(vec![19.0], vec![1]);
2049 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2050 fusion_residency::mark(&handle);
2051
2052 let bytecode = Bytecode::with_instructions(vec![Instr::StoreVar(0), Instr::Return], 2);
2053 let mut seed_vars = vec![
2054 Value::GpuTensor(handle.clone()),
2055 Value::GpuTensor(handle.clone()),
2056 ];
2057 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2058 state.stack.push(Value::Num(0.0));
2059 state.vars = vec![
2060 Value::GpuTensor(handle.clone()),
2061 Value::GpuTensor(handle.clone()),
2062 ];
2063
2064 let mut result_vars = state.vars.clone();
2065 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2066 .expect("store var should complete");
2067 assert!(
2068 fusion_residency::is_resident(&handle),
2069 "store var overwrite should preserve residency for handles still live in other vars"
2070 );
2071 assert!(
2072 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2073 "store var overwrite should not release provider storage for handles still live in other vars"
2074 );
2075 fusion_residency::clear(&handle);
2076 let _ = TEST_PROVIDER.free(&handle);
2077 }
2078
2079 #[cfg(feature = "native-accel")]
2080 #[test]
2081 fn store_var_overwrite_preserves_provider_handle_when_shared_in_local() {
2082 use runmat_accelerate::fusion_residency;
2083
2084 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2085 let handle = upload_provider_handle(vec![20.0], vec![1]);
2086 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2087 fusion_residency::mark(&handle);
2088
2089 let bytecode = Bytecode::with_instructions(vec![Instr::StoreVar(0), Instr::Return], 1);
2090 let mut seed_vars = vec![Value::GpuTensor(handle.clone())];
2091 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2092 state.stack.push(Value::Num(0.0));
2093 state.vars = vec![Value::GpuTensor(handle.clone())];
2094 state.context.locals.push(Value::GpuTensor(handle.clone()));
2095
2096 let mut result_vars = state.vars.clone();
2097 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2098 .expect("store var should complete when alias lives in locals");
2099 assert!(
2100 fusion_residency::is_resident(&handle),
2101 "store var overwrite should preserve residency for handles still live in locals"
2102 );
2103 assert!(
2104 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2105 "store var overwrite should not release provider storage for handles still live in locals"
2106 );
2107 fusion_residency::clear(&handle);
2108 let _ = TEST_PROVIDER.free(&handle);
2109 }
2110
2111 #[cfg(feature = "native-accel")]
2112 #[test]
2113 fn store_var_overwrite_releases_nested_handle_object_provider_handle_when_unaliased() {
2114 use runmat_accelerate::fusion_residency;
2115
2116 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2117 let handle = upload_provider_handle(vec![22.0], vec![1]);
2118 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2119 fusion_residency::mark(&handle);
2120
2121 let bytecode = Bytecode::with_instructions(vec![Instr::StoreVar(0), Instr::Return], 1);
2122 let mut seed_vars = vec![Value::Num(0.0)];
2123 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2124 let mut payload = StructValue::new();
2125 payload
2126 .fields
2127 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2128 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
2129 state.vars = vec![Value::HandleObject(HandleRef {
2130 class_name: "Payload".to_string(),
2131 target,
2132 valid: true,
2133 })];
2134 state.stack.push(Value::Num(0.0));
2135
2136 let mut result_vars = state.vars.clone();
2137 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2138 .expect("store var overwrite should complete for nested handle-object value");
2139 assert!(
2140 !fusion_residency::is_resident(&handle),
2141 "store var overwrite should clear residency for nested handle-object handles when unaliased"
2142 );
2143 assert!(
2144 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2145 "store var overwrite should release provider storage for nested handle-object handles when unaliased"
2146 );
2147 }
2148
2149 #[cfg(feature = "native-accel")]
2150 #[test]
2151 fn store_var_overwrite_preserves_nested_handle_object_provider_handle_when_shared_in_other_var()
2152 {
2153 use runmat_accelerate::fusion_residency;
2154
2155 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2156 let handle = upload_provider_handle(vec![24.0], vec![1]);
2157 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2158 fusion_residency::mark(&handle);
2159
2160 let bytecode = Bytecode::with_instructions(vec![Instr::StoreVar(0), Instr::Return], 2);
2161 let mut seed_vars = vec![Value::Num(0.0), Value::Num(0.0)];
2162 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2163 let mut payload = StructValue::new();
2164 payload
2165 .fields
2166 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2167 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
2168 let nested = Value::HandleObject(HandleRef {
2169 class_name: "Payload".to_string(),
2170 target,
2171 valid: true,
2172 });
2173 state.vars = vec![nested.clone(), nested.clone()];
2174 state.stack.push(Value::Num(0.0));
2175
2176 let mut result_vars = state.vars.clone();
2177 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2178 .expect("store var overwrite should complete for aliased nested handle-object values");
2179 assert!(
2180 fusion_residency::is_resident(&handle),
2181 "store var overwrite should preserve residency for nested handle-object handles still live in other vars"
2182 );
2183 assert!(
2184 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2185 "store var overwrite should not release provider storage for nested handle-object handles still live in other vars"
2186 );
2187 fusion_residency::clear(&handle);
2188 let _ = TEST_PROVIDER.free(&handle);
2189 }
2190
2191 #[cfg(feature = "native-accel")]
2192 #[test]
2193 fn store_var_overwrite_preserves_nested_handle_object_provider_handle_when_shared_in_local() {
2194 use runmat_accelerate::fusion_residency;
2195
2196 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2197 let handle = upload_provider_handle(vec![27.0], vec![1]);
2198 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2199 fusion_residency::mark(&handle);
2200
2201 let bytecode = Bytecode::with_instructions(vec![Instr::StoreVar(0), Instr::Return], 1);
2202 let mut seed_vars = vec![Value::Num(0.0)];
2203 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2204 let mut payload = StructValue::new();
2205 payload
2206 .fields
2207 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2208 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
2209 let nested = Value::HandleObject(HandleRef {
2210 class_name: "Payload".to_string(),
2211 target,
2212 valid: true,
2213 });
2214 state.vars = vec![nested.clone()];
2215 state.stack.push(Value::Num(0.0));
2216 state.context.locals.push(nested);
2217
2218 let mut result_vars = state.vars.clone();
2219 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2220 .expect("store var overwrite should complete when alias lives in locals");
2221 assert!(
2222 fusion_residency::is_resident(&handle),
2223 "store var overwrite should preserve residency for nested handle-object handles still live in locals"
2224 );
2225 assert!(
2226 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2227 "store var overwrite should not release provider storage for nested handle-object handles still live in locals"
2228 );
2229 fusion_residency::clear(&handle);
2230 let _ = TEST_PROVIDER.free(&handle);
2231 }
2232
2233 #[cfg(feature = "native-accel")]
2234 #[test]
2235 fn store_local_overwrite_preserves_provider_handle_when_shared_in_var() {
2236 use runmat_accelerate::fusion_residency;
2237
2238 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2239 let handle = upload_provider_handle(vec![23.0], vec![1]);
2240 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2241 fusion_residency::mark(&handle);
2242
2243 let bytecode = Bytecode::with_instructions(vec![Instr::StoreLocal(0), Instr::Return], 1);
2244 let mut seed_vars = vec![Value::GpuTensor(handle.clone())];
2245 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2246 state.stack.push(Value::Num(0.0));
2247 state.vars = vec![Value::GpuTensor(handle.clone())];
2248 state
2249 .context
2250 .call_stack
2251 .push(crate::bytecode::program::CallFrame {
2252 function_name: "<local>".to_string(),
2253 return_address: 0,
2254 locals_start: 0,
2255 locals_count: 1,
2256 expected_outputs: 0,
2257 });
2258 state.context.locals.push(Value::GpuTensor(handle.clone()));
2259
2260 let mut result_vars = state.vars.clone();
2261 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2262 .expect("store local should complete");
2263 assert!(
2264 fusion_residency::is_resident(&handle),
2265 "store local overwrite should preserve residency for handles still live in vars"
2266 );
2267 assert!(
2268 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2269 "store local overwrite should not release provider storage for handles still live in vars"
2270 );
2271 fusion_residency::clear(&handle);
2272 let _ = TEST_PROVIDER.free(&handle);
2273 }
2274
2275 #[cfg(feature = "native-accel")]
2276 #[test]
2277 fn store_local_overwrite_preserves_provider_handle_when_shared_in_other_local() {
2278 use runmat_accelerate::fusion_residency;
2279
2280 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2281 let handle = upload_provider_handle(vec![24.0], vec![1]);
2282 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2283 fusion_residency::mark(&handle);
2284
2285 let bytecode = Bytecode::with_instructions(vec![Instr::StoreLocal(0), Instr::Return], 1);
2286 let mut seed_vars = vec![Value::Num(0.0)];
2287 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2288 state.stack.push(Value::Num(0.0));
2289 state.vars = vec![Value::Num(0.0)];
2290 state
2291 .context
2292 .call_stack
2293 .push(crate::bytecode::program::CallFrame {
2294 function_name: "<local>".to_string(),
2295 return_address: 0,
2296 locals_start: 0,
2297 locals_count: 2,
2298 expected_outputs: 0,
2299 });
2300 state.context.locals.push(Value::GpuTensor(handle.clone()));
2301 state.context.locals.push(Value::GpuTensor(handle.clone()));
2302
2303 let mut result_vars = state.vars.clone();
2304 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2305 .expect("store local should complete when alias lives in other local");
2306 assert!(
2307 fusion_residency::is_resident(&handle),
2308 "store local overwrite should preserve residency for handles still live in other locals"
2309 );
2310 assert!(
2311 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2312 "store local overwrite should not release provider storage for handles still live in other locals"
2313 );
2314 fusion_residency::clear(&handle);
2315 let _ = TEST_PROVIDER.free(&handle);
2316 }
2317
2318 #[cfg(feature = "native-accel")]
2319 #[test]
2320 fn store_local_overwrite_releases_provider_handle_when_unaliased() {
2321 use runmat_accelerate::fusion_residency;
2322
2323 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2324 let handle = upload_provider_handle(vec![25.0], vec![1]);
2325 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2326 fusion_residency::mark(&handle);
2327
2328 let bytecode = Bytecode::with_instructions(vec![Instr::StoreLocal(0), Instr::Return], 1);
2329 let mut seed_vars = vec![Value::Num(0.0)];
2330 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2331 state.stack.push(Value::Num(0.0));
2332 state.vars = vec![Value::Num(0.0)];
2333 state
2334 .context
2335 .call_stack
2336 .push(crate::bytecode::program::CallFrame {
2337 function_name: "<local>".to_string(),
2338 return_address: 0,
2339 locals_start: 0,
2340 locals_count: 1,
2341 expected_outputs: 0,
2342 });
2343 state.context.locals.push(Value::GpuTensor(handle.clone()));
2344
2345 let mut result_vars = state.vars.clone();
2346 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2347 .expect("store local overwrite should complete");
2348 assert!(
2349 !fusion_residency::is_resident(&handle),
2350 "store local overwrite should clear residency for unaliased local handles"
2351 );
2352 assert!(
2353 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2354 "store local overwrite should release provider storage for unaliased local handles"
2355 );
2356 }
2357
2358 #[cfg(feature = "native-accel")]
2359 #[test]
2360 fn store_local_overwrite_releases_nested_handle_object_provider_handle_when_unaliased() {
2361 use runmat_accelerate::fusion_residency;
2362
2363 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2364 let handle = upload_provider_handle(vec![26.0], vec![1]);
2365 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2366 fusion_residency::mark(&handle);
2367
2368 let bytecode = Bytecode::with_instructions(vec![Instr::StoreLocal(0), Instr::Return], 1);
2369 let mut seed_vars = vec![Value::Num(0.0)];
2370 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2371 let mut payload = StructValue::new();
2372 payload
2373 .fields
2374 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2375 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
2376 state.stack.push(Value::Num(0.0));
2377 state.vars = vec![Value::Num(0.0)];
2378 state
2379 .context
2380 .call_stack
2381 .push(crate::bytecode::program::CallFrame {
2382 function_name: "<local>".to_string(),
2383 return_address: 0,
2384 locals_start: 0,
2385 locals_count: 1,
2386 expected_outputs: 0,
2387 });
2388 state.context.locals.push(Value::HandleObject(HandleRef {
2389 class_name: "Payload".to_string(),
2390 target,
2391 valid: true,
2392 }));
2393
2394 let mut result_vars = state.vars.clone();
2395 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2396 .expect("store local overwrite should complete for nested handle-object value");
2397 assert!(
2398 !fusion_residency::is_resident(&handle),
2399 "store local overwrite should clear residency for nested handle-object handles when unaliased"
2400 );
2401 assert!(
2402 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2403 "store local overwrite should release provider storage for nested handle-object handles when unaliased"
2404 );
2405 }
2406
2407 #[cfg(feature = "native-accel")]
2408 #[test]
2409 fn store_local_overwrite_preserves_nested_handle_object_provider_handle_when_shared_in_var() {
2410 use runmat_accelerate::fusion_residency;
2411
2412 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2413 let handle = upload_provider_handle(vec![28.0], vec![1]);
2414 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2415 fusion_residency::mark(&handle);
2416
2417 let bytecode = Bytecode::with_instructions(vec![Instr::StoreLocal(0), Instr::Return], 1);
2418 let mut seed_vars = vec![Value::Num(0.0)];
2419 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2420 let mut payload = StructValue::new();
2421 payload
2422 .fields
2423 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2424 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
2425 let local_value = Value::HandleObject(HandleRef {
2426 class_name: "Payload".to_string(),
2427 target,
2428 valid: true,
2429 });
2430 state.stack.push(Value::Num(0.0));
2431 state.vars = vec![local_value.clone()];
2432 state
2433 .context
2434 .call_stack
2435 .push(crate::bytecode::program::CallFrame {
2436 function_name: "<local>".to_string(),
2437 return_address: 0,
2438 locals_start: 0,
2439 locals_count: 1,
2440 expected_outputs: 0,
2441 });
2442 state.context.locals.push(local_value);
2443
2444 let mut result_vars = state.vars.clone();
2445 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2446 .expect("store local overwrite should complete for aliased nested handle-object value");
2447 assert!(
2448 fusion_residency::is_resident(&handle),
2449 "store local overwrite should preserve residency for nested handle-object handles still live in vars"
2450 );
2451 assert!(
2452 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2453 "store local overwrite should not release provider storage for nested handle-object handles still live in vars"
2454 );
2455 fusion_residency::clear(&handle);
2456 let _ = TEST_PROVIDER.free(&handle);
2457 }
2458
2459 #[cfg(feature = "native-accel")]
2460 #[test]
2461 fn store_local_overwrite_preserves_nested_handle_object_provider_alias() {
2462 use runmat_accelerate::fusion_residency;
2463
2464 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2465 let handle = upload_provider_handle(vec![30.0], vec![1]);
2466 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2467 fusion_residency::mark(&handle);
2468
2469 let bytecode = Bytecode::with_instructions(vec![Instr::StoreLocal(0), Instr::Return], 1);
2470 let mut seed_vars = vec![Value::Num(0.0)];
2471 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2472 let mut payload = StructValue::new();
2473 payload
2474 .fields
2475 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2476 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
2477 let nested = Value::HandleObject(HandleRef {
2478 class_name: "Payload".to_string(),
2479 target,
2480 valid: true,
2481 });
2482 state.stack.push(Value::Num(0.0));
2483 state.vars = vec![Value::Num(0.0)];
2484 state
2485 .context
2486 .call_stack
2487 .push(crate::bytecode::program::CallFrame {
2488 function_name: "<local>".to_string(),
2489 return_address: 0,
2490 locals_start: 0,
2491 locals_count: 2,
2492 expected_outputs: 0,
2493 });
2494 state.context.locals.push(nested.clone());
2495 state.context.locals.push(nested);
2496
2497 let mut result_vars = state.vars.clone();
2498 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2499 .expect("store local overwrite should complete when alias lives in other local");
2500 assert!(
2501 fusion_residency::is_resident(&handle),
2502 "store local overwrite should preserve residency for nested handle-object handles still live in other locals"
2503 );
2504 assert!(
2505 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2506 "store local overwrite should not release provider storage for nested handle-object handles still live in other locals"
2507 );
2508 fusion_residency::clear(&handle);
2509 let _ = TEST_PROVIDER.free(&handle);
2510 }
2511
2512 #[cfg(feature = "native-accel")]
2513 #[test]
2514 fn spawn_await_completion_releases_stack_only_provider_handle() {
2515 use runmat_accelerate::fusion_residency;
2516
2517 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2518 let handle = upload_provider_handle(vec![21.0], vec![1]);
2519 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2520 fusion_residency::mark(&handle);
2521
2522 let bytecode =
2523 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2524 let mut seed_vars = vec![Value::Num(0.0)];
2525 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2526 state.stack.push(Value::GpuTensor(handle.clone()));
2527 state.vars = vec![Value::Num(0.0)];
2528
2529 let mut result_vars = vec![Value::Num(0.0)];
2530 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2531 .expect("spawn/await flow should complete");
2532 assert!(
2533 !fusion_residency::is_resident(&handle),
2534 "spawn/await completion should clear residency for stack-only handle"
2535 );
2536 assert!(
2537 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2538 "spawn/await completion should release provider storage for stack-only handle"
2539 );
2540 }
2541
2542 #[cfg(feature = "native-accel")]
2543 #[test]
2544 fn spawn_await_completion_preserves_provider_handle_when_still_live_in_vars() {
2545 use runmat_accelerate::fusion_residency;
2546
2547 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2548 let handle = upload_provider_handle(vec![31.0], vec![1]);
2549 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2550 fusion_residency::mark(&handle);
2551
2552 let bytecode =
2553 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2554 let mut seed_vars = vec![Value::GpuTensor(handle.clone())];
2555 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2556 state.stack.push(Value::GpuTensor(handle.clone()));
2557 state.vars = vec![Value::GpuTensor(handle.clone())];
2558
2559 let mut result_vars = vec![Value::GpuTensor(handle.clone())];
2560 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2561 .expect("spawn/await flow should complete");
2562 assert!(
2563 fusion_residency::is_resident(&handle),
2564 "spawn/await completion should preserve residency for live-var handle"
2565 );
2566 assert!(
2567 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2568 "spawn/await completion should not release provider storage for live-var handle"
2569 );
2570 fusion_residency::clear(&handle);
2571 let _ = TEST_PROVIDER.free(&handle);
2572 }
2573
2574 #[cfg(feature = "native-accel")]
2575 #[test]
2576 fn spawn_pop_releases_stack_only_provider_handle() {
2577 use runmat_accelerate::fusion_residency;
2578
2579 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2580 let handle = upload_provider_handle(vec![41.0], vec![1]);
2581 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2582 fusion_residency::mark(&handle);
2583
2584 let bytecode =
2585 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Pop, Instr::Return], 1);
2586 let mut seed_vars = vec![Value::Num(0.0)];
2587 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2588 state.stack.push(Value::GpuTensor(handle.clone()));
2589 state.vars = vec![Value::Num(0.0)];
2590
2591 let mut result_vars = vec![Value::Num(0.0)];
2592 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2593 .expect("spawn/pop should complete");
2594 assert!(
2595 !fusion_residency::is_resident(&handle),
2596 "spawn/pop should clear residency for dropped spawned task payload"
2597 );
2598 assert!(
2599 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2600 "spawn/pop should release provider storage for dropped spawned task payload"
2601 );
2602 }
2603
2604 #[cfg(feature = "native-accel")]
2605 #[test]
2606 fn spawn_pop_preserves_provider_handle_when_payload_still_live_in_vars() {
2607 use runmat_accelerate::fusion_residency;
2608
2609 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2610 let handle = upload_provider_handle(vec![51.0], vec![1]);
2611 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2612 fusion_residency::mark(&handle);
2613
2614 let bytecode =
2615 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Pop, Instr::Return], 1);
2616 let mut seed_vars = vec![Value::GpuTensor(handle.clone())];
2617 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2618 state.stack.push(Value::GpuTensor(handle.clone()));
2619 state.vars = vec![Value::GpuTensor(handle.clone())];
2620
2621 let mut result_vars = vec![Value::GpuTensor(handle.clone())];
2622 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2623 .expect("spawn/pop should complete");
2624 assert!(
2625 fusion_residency::is_resident(&handle),
2626 "spawn/pop should preserve residency for spawned payload handles still referenced by vars"
2627 );
2628 assert!(
2629 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2630 "spawn/pop should not release provider storage for spawned payload handles still referenced by vars"
2631 );
2632 fusion_residency::clear(&handle);
2633 let _ = TEST_PROVIDER.free(&handle);
2634 }
2635
2636 #[cfg(feature = "native-accel")]
2637 #[test]
2638 fn spawn_pop_preserves_provider_handle_when_payload_still_live_in_locals() {
2639 use runmat_accelerate::fusion_residency;
2640
2641 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2642 let handle = upload_provider_handle(vec![56.0], vec![1]);
2643 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2644 fusion_residency::mark(&handle);
2645
2646 let bytecode =
2647 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Pop, Instr::Return], 1);
2648 let mut seed_vars = vec![Value::Num(0.0)];
2649 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2650 state.stack.push(Value::GpuTensor(handle.clone()));
2651 state.vars = vec![Value::Num(0.0)];
2652 state.context.locals.push(Value::GpuTensor(handle.clone()));
2653
2654 let mut result_vars = vec![Value::Num(0.0)];
2655 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2656 .expect("spawn/pop should complete");
2657 assert!(
2658 fusion_residency::is_resident(&handle),
2659 "spawn/pop should preserve residency for spawned payload handles still referenced by locals"
2660 );
2661 assert!(
2662 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2663 "spawn/pop should not release provider storage for spawned payload handles still referenced by locals"
2664 );
2665 fusion_residency::clear(&handle);
2666 let _ = TEST_PROVIDER.free(&handle);
2667 }
2668
2669 #[cfg(feature = "native-accel")]
2670 #[test]
2671 fn spawn_pop_releases_nested_closure_captured_provider_handle() {
2672 use runmat_accelerate::fusion_residency;
2673
2674 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2675 let handle = upload_provider_handle(vec![61.0], vec![1]);
2676 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2677 fusion_residency::mark(&handle);
2678
2679 let bytecode =
2680 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Pop, Instr::Return], 1);
2681 let mut seed_vars = vec![Value::Num(0.0)];
2682 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2683 state.stack.push(Value::Closure(Closure {
2684 function_name: "worker".to_string(),
2685 bound_function: None,
2686 captures: vec![Value::GpuTensor(handle.clone())],
2687 }));
2688 state.vars = vec![Value::Num(0.0)];
2689
2690 let mut result_vars = vec![Value::Num(0.0)];
2691 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2692 .expect("spawn/pop should complete for closure payload");
2693 assert!(
2694 !fusion_residency::is_resident(&handle),
2695 "spawn/pop should clear residency for nested closure-captured payload handles"
2696 );
2697 assert!(
2698 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2699 "spawn/pop should release provider storage for nested closure-captured payload handles"
2700 );
2701 }
2702
2703 #[cfg(feature = "native-accel")]
2704 #[test]
2705 fn spawn_await_completion_releases_nested_output_list_provider_handle() {
2706 use runmat_accelerate::fusion_residency;
2707
2708 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2709 let handle = upload_provider_handle(vec![71.0], vec![1]);
2710 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2711 fusion_residency::mark(&handle);
2712
2713 let bytecode =
2714 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2715 let mut seed_vars = vec![Value::Num(0.0)];
2716 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2717 state
2718 .stack
2719 .push(Value::OutputList(vec![Value::GpuTensor(handle.clone())]));
2720 state.vars = vec![Value::Num(0.0)];
2721
2722 let mut result_vars = vec![Value::Num(0.0)];
2723 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2724 .expect("spawn/await flow should complete for nested output payload");
2725 assert!(
2726 !fusion_residency::is_resident(&handle),
2727 "spawn/await completion should clear residency for nested output-list payload handles"
2728 );
2729 assert!(
2730 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2731 "spawn/await completion should release provider storage for nested output-list payload handles"
2732 );
2733 }
2734
2735 #[cfg(feature = "native-accel")]
2736 #[test]
2737 fn spawn_await_completion_releases_nested_struct_provider_handle() {
2738 use runmat_accelerate::fusion_residency;
2739
2740 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2741 let handle = upload_provider_handle(vec![81.0], vec![1]);
2742 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2743 fusion_residency::mark(&handle);
2744
2745 let bytecode =
2746 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2747 let mut seed_vars = vec![Value::Num(0.0)];
2748 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2749 let mut payload = StructValue::new();
2750 payload
2751 .fields
2752 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2753 state.stack.push(Value::Struct(payload));
2754 state.vars = vec![Value::Num(0.0)];
2755
2756 let mut result_vars = vec![Value::Num(0.0)];
2757 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2758 .expect("spawn/await flow should complete for nested struct payload");
2759 assert!(
2760 !fusion_residency::is_resident(&handle),
2761 "spawn/await completion should clear residency for nested struct payload handles"
2762 );
2763 assert!(
2764 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2765 "spawn/await completion should release provider storage for nested struct payload handles"
2766 );
2767 }
2768
2769 #[cfg(feature = "native-accel")]
2770 #[test]
2771 fn spawn_await_completion_releases_nested_object_property_provider_handle() {
2772 use runmat_accelerate::fusion_residency;
2773
2774 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2775 let handle = upload_provider_handle(vec![91.0], vec![1]);
2776 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2777 fusion_residency::mark(&handle);
2778
2779 let bytecode =
2780 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2781 let mut seed_vars = vec![Value::Num(0.0)];
2782 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2783 let mut payload = ObjectInstance::new("Payload".to_string());
2784 payload
2785 .properties
2786 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2787 state.stack.push(Value::Object(payload));
2788 state.vars = vec![Value::Num(0.0)];
2789
2790 let mut result_vars = vec![Value::Num(0.0)];
2791 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2792 .expect("spawn/await flow should complete for nested object payload");
2793 assert!(
2794 !fusion_residency::is_resident(&handle),
2795 "spawn/await completion should clear residency for nested object-property payload handles"
2796 );
2797 assert!(
2798 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2799 "spawn/await completion should release provider storage for nested object-property payload handles"
2800 );
2801 }
2802
2803 #[cfg(feature = "native-accel")]
2804 #[test]
2805 fn spawn_await_completion_preserves_nested_object_property_handle_when_alias_live() {
2806 use runmat_accelerate::fusion_residency;
2807
2808 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2809 let handle = upload_provider_handle(vec![101.0], vec![1]);
2810 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2811 fusion_residency::mark(&handle);
2812
2813 let bytecode =
2814 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2815 let mut seed_vars = vec![Value::Num(0.0)];
2816 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2817 let mut payload = ObjectInstance::new("Payload".to_string());
2818 payload
2819 .properties
2820 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2821 state.stack.push(Value::Object(payload.clone()));
2822 state.vars = vec![Value::Object(payload.clone())];
2823
2824 let mut result_vars = vec![Value::Object(payload)];
2825 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2826 .expect("spawn/await flow should complete for aliased nested object payload");
2827 assert!(
2828 fusion_residency::is_resident(&handle),
2829 "spawn/await completion should preserve residency for nested object handles still referenced by vars"
2830 );
2831 assert!(
2832 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2833 "spawn/await completion should not release provider storage for nested object handles still referenced by vars"
2834 );
2835 fusion_residency::clear(&handle);
2836 let _ = TEST_PROVIDER.free(&handle);
2837 }
2838
2839 #[cfg(feature = "native-accel")]
2840 #[test]
2841 fn spawn_await_completion_releases_nested_cell_provider_handle() {
2842 use runmat_accelerate::fusion_residency;
2843
2844 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2845 let handle = upload_provider_handle(vec![111.0], vec![1]);
2846 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2847 fusion_residency::mark(&handle);
2848
2849 let bytecode =
2850 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2851 let mut seed_vars = vec![Value::Num(0.0)];
2852 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2853 let payload =
2854 CellArray::new(vec![Value::GpuTensor(handle.clone())], 1, 1).expect("cell payload");
2855 state.stack.push(Value::Cell(payload));
2856 state.vars = vec![Value::Num(0.0)];
2857
2858 let mut result_vars = vec![Value::Num(0.0)];
2859 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2860 .expect("spawn/await flow should complete for nested cell payload");
2861 assert!(
2862 !fusion_residency::is_resident(&handle),
2863 "spawn/await completion should clear residency for nested cell payload handles"
2864 );
2865 assert!(
2866 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2867 "spawn/await completion should release provider storage for nested cell payload handles"
2868 );
2869 }
2870
2871 #[cfg(feature = "native-accel")]
2872 #[test]
2873 fn spawn_await_completion_preserves_nested_cell_handle_when_alias_live() {
2874 use runmat_accelerate::fusion_residency;
2875
2876 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2877 let handle = upload_provider_handle(vec![121.0], vec![1]);
2878 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2879 fusion_residency::mark(&handle);
2880
2881 let bytecode =
2882 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2883 let mut seed_vars = vec![Value::Num(0.0)];
2884 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2885 let payload =
2886 CellArray::new(vec![Value::GpuTensor(handle.clone())], 1, 1).expect("cell payload");
2887 state.stack.push(Value::Cell(payload.clone()));
2888 state.vars = vec![Value::Cell(payload.clone())];
2889
2890 let mut result_vars = vec![Value::Cell(payload)];
2891 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2892 .expect("spawn/await flow should complete for aliased nested cell payload");
2893 assert!(
2894 fusion_residency::is_resident(&handle),
2895 "spawn/await completion should preserve residency for nested cell handles still referenced by vars"
2896 );
2897 assert!(
2898 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2899 "spawn/await completion should not release provider storage for nested cell handles still referenced by vars"
2900 );
2901 fusion_residency::clear(&handle);
2902 let _ = TEST_PROVIDER.free(&handle);
2903 }
2904
2905 #[cfg(feature = "native-accel")]
2906 #[test]
2907 fn spawn_await_completion_releases_nested_handle_object_target_provider_handle() {
2908 use runmat_accelerate::fusion_residency;
2909
2910 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2911 let handle = upload_provider_handle(vec![131.0], vec![1]);
2912 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2913 fusion_residency::mark(&handle);
2914
2915 let bytecode =
2916 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2917 let mut seed_vars = vec![Value::Num(0.0)];
2918 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2919 let mut payload = StructValue::new();
2920 payload
2921 .fields
2922 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2923 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
2924 let task_payload = Value::HandleObject(HandleRef {
2925 class_name: "Payload".to_string(),
2926 target,
2927 valid: true,
2928 });
2929 state.stack.push(task_payload);
2930 state.vars = vec![Value::Num(0.0)];
2931
2932 let mut result_vars = vec![Value::Num(0.0)];
2933 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2934 .expect("spawn/await flow should complete for nested handle-object payload");
2935 assert!(
2936 !fusion_residency::is_resident(&handle),
2937 "spawn/await completion should clear residency for nested handle-object target handles"
2938 );
2939 assert!(
2940 block_on(TEST_PROVIDER.download(&handle)).is_err(),
2941 "spawn/await completion should release provider storage for nested handle-object target handles"
2942 );
2943 }
2944
2945 #[cfg(feature = "native-accel")]
2946 #[test]
2947 fn spawn_await_completion_preserves_nested_handle_object_target_handle_when_alias_live() {
2948 use runmat_accelerate::fusion_residency;
2949
2950 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2951 let handle = upload_provider_handle(vec![141.0], vec![1]);
2952 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2953 fusion_residency::mark(&handle);
2954
2955 let bytecode =
2956 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2957 let mut seed_vars = vec![Value::Num(0.0)];
2958 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
2959 let mut payload = StructValue::new();
2960 payload
2961 .fields
2962 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
2963 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
2964 let task_payload = Value::HandleObject(HandleRef {
2965 class_name: "Payload".to_string(),
2966 target,
2967 valid: true,
2968 });
2969 state.stack.push(task_payload.clone());
2970 state.vars = vec![task_payload.clone()];
2971
2972 let mut result_vars = vec![task_payload];
2973 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
2974 .expect("spawn/await flow should complete for aliased nested handle-object payload");
2975 assert!(
2976 fusion_residency::is_resident(&handle),
2977 "spawn/await completion should preserve residency for nested handle-object target handles still referenced by vars"
2978 );
2979 assert!(
2980 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
2981 "spawn/await completion should not release provider storage for nested handle-object target handles still referenced by vars"
2982 );
2983 fusion_residency::clear(&handle);
2984 let _ = TEST_PROVIDER.free(&handle);
2985 }
2986
2987 #[cfg(feature = "native-accel")]
2988 #[test]
2989 fn spawn_await_preserves_nested_handle_object_target_alias() {
2990 use runmat_accelerate::fusion_residency;
2991
2992 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
2993 let handle = upload_provider_handle(vec![146.0], vec![1]);
2994 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
2995 fusion_residency::mark(&handle);
2996
2997 let bytecode =
2998 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Await, Instr::Return], 1);
2999 let mut seed_vars = vec![Value::Num(0.0)];
3000 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3001 let mut payload = StructValue::new();
3002 payload
3003 .fields
3004 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
3005 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
3006 let task_payload = Value::HandleObject(HandleRef {
3007 class_name: "Payload".to_string(),
3008 target,
3009 valid: true,
3010 });
3011 state.stack.push(task_payload.clone());
3012 state.vars = vec![Value::Num(0.0)];
3013 state.context.locals.push(task_payload.clone());
3014
3015 let mut result_vars = vec![Value::Num(0.0)];
3016 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
3017 .expect("spawn/await flow should complete for aliased nested handle-object payload");
3018 assert!(
3019 fusion_residency::is_resident(&handle),
3020 "spawn/await completion should preserve residency for nested handle-object target handles still referenced by locals"
3021 );
3022 assert!(
3023 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
3024 "spawn/await completion should not release provider storage for nested handle-object target handles still referenced by locals"
3025 );
3026 fusion_residency::clear(&handle);
3027 let _ = TEST_PROVIDER.free(&handle);
3028 }
3029
3030 #[cfg(feature = "native-accel")]
3031 #[test]
3032 fn spawn_pop_releases_nested_handle_object_target_provider_handle() {
3033 use runmat_accelerate::fusion_residency;
3034
3035 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
3036 let handle = upload_provider_handle(vec![151.0], vec![1]);
3037 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
3038 fusion_residency::mark(&handle);
3039
3040 let bytecode =
3041 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Pop, Instr::Return], 1);
3042 let mut seed_vars = vec![Value::Num(0.0)];
3043 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3044 let mut payload = StructValue::new();
3045 payload
3046 .fields
3047 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
3048 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
3049 state.stack.push(Value::HandleObject(HandleRef {
3050 class_name: "Payload".to_string(),
3051 target,
3052 valid: true,
3053 }));
3054 state.vars = vec![Value::Num(0.0)];
3055
3056 let mut result_vars = vec![Value::Num(0.0)];
3057 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
3058 .expect("spawn/pop flow should complete for nested handle-object payload");
3059 assert!(
3060 !fusion_residency::is_resident(&handle),
3061 "spawn/pop should clear residency for nested handle-object target handles"
3062 );
3063 assert!(
3064 block_on(TEST_PROVIDER.download(&handle)).is_err(),
3065 "spawn/pop should release provider storage for nested handle-object target handles"
3066 );
3067 }
3068
3069 #[cfg(feature = "native-accel")]
3070 #[test]
3071 fn spawn_pop_preserves_nested_handle_object_target_handle_when_alias_live() {
3072 use runmat_accelerate::fusion_residency;
3073
3074 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
3075 let handle = upload_provider_handle(vec![161.0], vec![1]);
3076 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
3077 fusion_residency::mark(&handle);
3078
3079 let bytecode =
3080 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Pop, Instr::Return], 1);
3081 let mut seed_vars = vec![Value::Num(0.0)];
3082 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3083 let mut payload = StructValue::new();
3084 payload
3085 .fields
3086 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
3087 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
3088 let task_payload = Value::HandleObject(HandleRef {
3089 class_name: "Payload".to_string(),
3090 target,
3091 valid: true,
3092 });
3093 state.stack.push(task_payload.clone());
3094 state.vars = vec![task_payload.clone()];
3095
3096 let mut result_vars = vec![task_payload];
3097 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
3098 .expect("spawn/pop flow should complete for aliased nested handle-object payload");
3099 assert!(
3100 fusion_residency::is_resident(&handle),
3101 "spawn/pop should preserve residency for nested handle-object target handles still referenced by vars"
3102 );
3103 assert!(
3104 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
3105 "spawn/pop should not release provider storage for nested handle-object target handles still referenced by vars"
3106 );
3107 fusion_residency::clear(&handle);
3108 let _ = TEST_PROVIDER.free(&handle);
3109 }
3110
3111 #[cfg(feature = "native-accel")]
3112 #[test]
3113 fn spawn_pop_preserves_nested_handle_object_target_handle_when_alias_live_in_locals() {
3114 use runmat_accelerate::fusion_residency;
3115
3116 let _provider_guard = ThreadProviderGuard::set(Some(&*TEST_PROVIDER));
3117 let handle = upload_provider_handle(vec![166.0], vec![1]);
3118 assert!(block_on(TEST_PROVIDER.download(&handle)).is_ok());
3119 fusion_residency::mark(&handle);
3120
3121 let bytecode =
3122 Bytecode::with_instructions(vec![Instr::Spawn, Instr::Pop, Instr::Return], 1);
3123 let mut seed_vars = vec![Value::Num(0.0)];
3124 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3125 let mut payload = StructValue::new();
3126 payload
3127 .fields
3128 .insert("nested".to_string(), Value::GpuTensor(handle.clone()));
3129 let target = runmat_gc::gc_allocate(Value::Struct(payload)).expect("gc allocate payload");
3130 let task_payload = Value::HandleObject(HandleRef {
3131 class_name: "Payload".to_string(),
3132 target,
3133 valid: true,
3134 });
3135 state.stack.push(task_payload.clone());
3136 state.vars = vec![Value::Num(0.0)];
3137 state.context.locals.push(task_payload);
3138
3139 let mut result_vars = vec![Value::Num(0.0)];
3140 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
3141 .expect("spawn/pop flow should complete for aliased nested handle-object payload");
3142 assert!(
3143 fusion_residency::is_resident(&handle),
3144 "spawn/pop should preserve residency for nested handle-object target handles still referenced by locals"
3145 );
3146 assert!(
3147 block_on(TEST_PROVIDER.download(&handle)).is_ok(),
3148 "spawn/pop should not release provider storage for nested handle-object target handles still referenced by locals"
3149 );
3150 fusion_residency::clear(&handle);
3151 let _ = TEST_PROVIDER.free(&handle);
3152 }
3153
3154 #[test]
3155 fn await_passes_through_non_spawn_value_operand() {
3156 let bytecode =
3157 Bytecode::with_instructions(vec![Instr::Await, Instr::StoreVar(0), Instr::Return], 1);
3158 let mut seed_vars = vec![Value::Num(0.0)];
3159 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3160 state.stack.push(Value::Num(7.0));
3161 state.vars = vec![Value::Num(0.0)];
3162
3163 let mut result_vars = vec![Value::Num(0.0)];
3164 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
3165 .expect("await should pass through non-task operand");
3166 assert_eq!(result_vars[0], Value::Num(7.0));
3167 }
3168
3169 #[test]
3170 fn await_succeeds_after_spawn_handle_self_reassignment() {
3171 let bytecode = Bytecode::with_instructions(
3172 vec![
3173 Instr::Spawn,
3174 Instr::StoreVar(0),
3175 Instr::LoadVar(0),
3176 Instr::StoreVar(0),
3177 Instr::LoadVar(0),
3178 Instr::Await,
3179 Instr::StoreVar(0),
3180 Instr::Return,
3181 ],
3182 1,
3183 );
3184 let mut seed_vars = vec![Value::Num(0.0)];
3185 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3186 state.stack.push(Value::Num(9.0));
3187 state.vars = vec![Value::Num(0.0)];
3188
3189 let mut result_vars = vec![Value::Num(0.0)];
3190 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
3191 .expect("await should still succeed after self-reassignment of spawn handle");
3192 assert_eq!(result_vars[0], Value::Num(9.0));
3193 }
3194
3195 #[test]
3196 fn await_succeeds_after_overwriting_one_spawn_handle_alias() {
3197 let bytecode = Bytecode::with_instructions(
3198 vec![
3199 Instr::Spawn,
3200 Instr::StoreVar(0),
3201 Instr::LoadVar(0),
3202 Instr::StoreVar(1),
3203 Instr::LoadConst(0.0),
3204 Instr::StoreVar(0),
3205 Instr::LoadVar(1),
3206 Instr::Await,
3207 Instr::StoreVar(0),
3208 Instr::Return,
3209 ],
3210 2,
3211 );
3212 let mut seed_vars = vec![Value::Num(0.0), Value::Num(0.0)];
3213 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3214 state.stack.push(Value::Num(9.0));
3215 state.vars = vec![Value::Num(0.0), Value::Num(0.0)];
3216
3217 let mut result_vars = vec![Value::Num(0.0), Value::Num(0.0)];
3218 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
3219 .expect("await should succeed when another alias still carries the spawn task handle");
3220 assert_eq!(result_vars[0], Value::Num(9.0));
3221 }
3222
3223 #[test]
3224 fn await_succeeds_after_overwriting_one_local_spawn_handle_alias() {
3225 let bytecode = Bytecode::with_instructions(
3226 vec![
3227 Instr::Spawn,
3228 Instr::StoreLocal(0),
3229 Instr::LoadLocal(0),
3230 Instr::StoreLocal(1),
3231 Instr::LoadConst(0.0),
3232 Instr::StoreLocal(0),
3233 Instr::LoadLocal(1),
3234 Instr::Await,
3235 Instr::StoreVar(0),
3236 Instr::Return,
3237 ],
3238 1,
3239 );
3240 let mut seed_vars = vec![Value::Num(0.0)];
3241 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3242 state.stack.push(Value::Num(9.0));
3243 state.vars = vec![Value::Num(0.0)];
3244 state
3245 .context
3246 .call_stack
3247 .push(crate::bytecode::program::CallFrame {
3248 function_name: "<local>".to_string(),
3249 return_address: 0,
3250 locals_start: 0,
3251 locals_count: 2,
3252 expected_outputs: 0,
3253 });
3254 state.context.locals = vec![Value::Num(0.0), Value::Num(0.0)];
3255
3256 let mut result_vars = vec![Value::Num(0.0)];
3257 let _ = block_on(run_interpreter_inner(state, &mut result_vars)).expect(
3258 "await should succeed when another local alias still carries the spawn task handle",
3259 );
3260 assert_eq!(result_vars[0], Value::Num(9.0));
3261 }
3262
3263 #[test]
3264 fn await_succeeds_after_overwriting_var_alias_when_local_spawn_handle_alias_live() {
3265 let bytecode = Bytecode::with_instructions(
3266 vec![
3267 Instr::Spawn,
3268 Instr::StoreLocal(0),
3269 Instr::LoadLocal(0),
3270 Instr::StoreVar(0),
3271 Instr::LoadConst(0.0),
3272 Instr::StoreVar(0),
3273 Instr::LoadLocal(0),
3274 Instr::Await,
3275 Instr::StoreVar(0),
3276 Instr::Return,
3277 ],
3278 1,
3279 );
3280 let mut seed_vars = vec![Value::Num(0.0)];
3281 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3282 state.stack.push(Value::Num(9.0));
3283 state.vars = vec![Value::Num(0.0)];
3284 state
3285 .context
3286 .call_stack
3287 .push(crate::bytecode::program::CallFrame {
3288 function_name: "<local>".to_string(),
3289 return_address: 0,
3290 locals_start: 0,
3291 locals_count: 1,
3292 expected_outputs: 0,
3293 });
3294 state.context.locals = vec![Value::Num(0.0)];
3295
3296 let mut result_vars = vec![Value::Num(0.0)];
3297 let _ = block_on(run_interpreter_inner(state, &mut result_vars)).expect(
3298 "await should succeed when var alias is overwritten but local alias still carries the spawn task handle",
3299 );
3300 assert_eq!(result_vars[0], Value::Num(9.0));
3301 }
3302
3303 #[test]
3304 fn await_succeeds_after_scope_exit_when_var_alias_keeps_spawn_task_id_live() {
3305 let mut task = runmat_builtins::StructValue::new();
3306 task.fields.insert(
3307 "__runmat_spawn_kind".to_string(),
3308 Value::String("task".to_string()),
3309 );
3310 task.fields.insert(
3311 "__runmat_spawn_id".to_string(),
3312 Value::Int(runmat_builtins::IntValue::U64(23)),
3313 );
3314 task.fields
3315 .insert("__runmat_spawn_payload".to_string(), Value::Num(4.0));
3316 let task_value = Value::Struct(task);
3317
3318 let bytecode = Bytecode::with_instructions(
3319 vec![
3320 Instr::ExitScope(1),
3321 Instr::LoadVar(0),
3322 Instr::Await,
3323 Instr::Return,
3324 ],
3325 1,
3326 );
3327 let mut seed_vars = vec![task_value.clone()];
3328 let mut state = InterpreterState::new(bytecode, &mut seed_vars, Some("<main>"), Vec::new());
3329 state.context.locals.push(task_value);
3330 state.context.spawned_task_ids.insert(23);
3331 state.vars = seed_vars.clone();
3332
3333 let mut result_vars = seed_vars.clone();
3334 let _ = block_on(run_interpreter_inner(state, &mut result_vars))
3335 .expect("await should succeed when var alias keeps the spawn task id live");
3336 assert!(
3337 matches!(result_vars[0], Value::Struct(_)),
3338 "await in this sequence does not overwrite var0"
3339 );
3340 }
3341}