Skip to main content

runmat_vm/interpreter/
runner.rs

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    /// The caller retains the argument values as live workspace roots.
123    Borrowed,
124    /// The invocation owns the argument values and may release inputs the
125    /// function neither returns nor stores in another live root.
126    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            // A semantic call recursively drives another async interpreter.
303            // Polling it on the caller's small executor/test-thread stack makes
304            // ordinary class method chains consume the full native stack long
305            // before reaching a meaningful language recursion depth. Grow the
306            // stack only while polling the nested interpreter; values remain on
307            // the same thread and therefore retain their thread-confined GC
308            // semantics.
309            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        // Interpreter-to-interpreter calls borrow arguments from the caller's
888        // workspace. Those values remain live after the callee returns even if
889        // the callee overwrites its input slots.
890        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(&registry_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: &current_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}