Skip to main content

runmat_runtime/builtins/timing/
pause.rs

1//! MATLAB-compatible `pause` builtin that temporarily suspends execution.
2
3use once_cell::sync::Lazy;
4use runmat_builtins::{
5    BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
6    BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
7    BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
8    BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
9    BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
10    BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
11};
12use runmat_macros::runtime_builtin;
13use runmat_value::{CharArray, LogicalArray, Tensor, Value};
14use std::sync::RwLock;
15
16use crate::builtins::common::gpu_helpers;
17use crate::builtins::common::spec::{
18    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
19    ReductionNaN, ResidencyPolicy, ShapeRequirements,
20};
21use crate::builtins::common::tensor as tensor_utils;
22#[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
23use crate::builtins::plotting;
24use crate::builtins::timing::type_resolvers::pause_type;
25#[cfg(not(test))]
26use crate::interaction;
27use crate::{build_runtime_error, BuiltinResult, RuntimeError};
28
29#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::pause")]
30pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
31    name: "pause",
32    op_kind: GpuOpKind::Custom("timer"),
33    supported_precisions: &[],
34    broadcast: BroadcastSemantics::None,
35    provider_hooks: &[],
36    constant_strategy: ConstantStrategy::InlineLiteral,
37    residency: ResidencyPolicy::GatherImmediately,
38    nan_mode: ReductionNaN::Include,
39    two_pass_threshold: None,
40    workgroup_size: None,
41    accepts_nan_mode: false,
42    notes: "pause executes entirely on the host. Acceleration providers are never queried.",
43};
44
45#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::pause")]
46pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
47    name: "pause",
48    shape: ShapeRequirements::Any,
49    constant_strategy: ConstantStrategy::InlineLiteral,
50    elementwise: None,
51    reduction: None,
52    emits_nan: false,
53    notes: "pause suspends host execution and is excluded from fusion pipelines.",
54};
55
56static PAUSE_STATE: Lazy<RwLock<PauseState>> = Lazy::new(|| RwLock::new(PauseState::default()));
57
58#[cfg(test)]
59use std::sync::Mutex;
60#[cfg(test)]
61pub(crate) static TEST_GUARD: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
62
63#[derive(Debug, Clone, Copy)]
64struct PauseState {
65    enabled: bool,
66}
67
68impl Default for PauseState {
69    fn default() -> Self {
70        Self { enabled: true }
71    }
72}
73
74const BUILTIN_NAME: &str = "pause";
75
76const GPU_INPUT_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
77    id: "pause-gpu-input",
78    mode: BuiltinExtensionMode::RunMatOnly,
79    description: "pause with an explicit GPU-resident argument is a RunMat extension",
80    error_identifier: Some("RunMat:compatibility:PauseGpuInputExtension"),
81};
82
83pub const EXTENSIONS: [BuiltinExtensionDescriptor; 1] = [GPU_INPUT_EXTENSION];
84
85const INTEGER_DURATION_INPUTS: [BuiltinIntegerInputCapability; 1] =
86    [BuiltinIntegerInputCapability {
87        name: "duration",
88        classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
89        availability: BuiltinIntegerInputAvailability::Documented,
90        scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
91        notes: "The compatibility target explicitly lists every built-in integer class for the nonnegative real duration.",
92    }];
93
94pub const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
95    [BuiltinIntegerCapabilityDescriptor {
96        form: "pause(integer_duration)",
97        inputs: &INTEGER_DURATION_INPUTS,
98        computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
99        output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
100        overflow: BuiltinIntegerOverflowRule::NotApplicable,
101        backend: BuiltinIntegerBackendRule::HostOnly,
102        overload: BuiltinIntegerOverloadKind::ScalarOnly,
103        notes: "The integer scalar is validated from authoritative storage and crosses one explicit seconds/timer boundary; pause returns no integer data. Explicit resident arguments are independently gated before provider access, while internal automatic residency remains transparent.",
104    }];
105
106const PAUSE_OUTPUT_EMPTY: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
107    name: "out",
108    ty: BuiltinParamType::NumericArray,
109    arity: BuiltinParamArity::Required,
110    default: None,
111    description: "Empty array when pausing or changing state.",
112}];
113
114const PAUSE_OUTPUT_STATE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
115    name: "state",
116    ty: BuiltinParamType::StringScalar,
117    arity: BuiltinParamArity::Required,
118    default: None,
119    description: "Previous pause state ('on' or 'off').",
120}];
121
122const PAUSE_INPUTS_NONE: [BuiltinParamDescriptor; 0] = [];
123const PAUSE_INPUTS_DURATION: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
124    name: "duration",
125    ty: BuiltinParamType::Any,
126    arity: BuiltinParamArity::Required,
127    default: Some("0"),
128    description: "Duration scalar or command-like scalar value accepted by pause.",
129}];
130const PAUSE_INPUTS_COMMAND: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
131    name: "command",
132    ty: BuiltinParamType::StringScalar,
133    arity: BuiltinParamArity::Required,
134    default: None,
135    description: "One of 'on', 'off', or 'query'.",
136}];
137
138const PAUSE_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
139    BuiltinSignatureDescriptor {
140        label: "out = pause()",
141        inputs: &PAUSE_INPUTS_NONE,
142        outputs: &PAUSE_OUTPUT_EMPTY,
143    },
144    BuiltinSignatureDescriptor {
145        label: "out = pause(duration)",
146        inputs: &PAUSE_INPUTS_DURATION,
147        outputs: &PAUSE_OUTPUT_EMPTY,
148    },
149    BuiltinSignatureDescriptor {
150        label: "state = pause(command)",
151        inputs: &PAUSE_INPUTS_COMMAND,
152        outputs: &PAUSE_OUTPUT_STATE,
153    },
154];
155
156const PAUSE_ERROR_INVALID_ARG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
157    code: "RM.PAUSE.INVALID_ARG",
158    identifier: Some("RunMat:pause:InvalidInputArgument"),
159    when: "Input argument is malformed, unsupported, non-scalar where scalar is required, or a negative/non-finite duration.",
160    message: "pause: invalid input argument",
161};
162
163const PAUSE_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
164    code: "RM.PAUSE.TOO_MANY_INPUTS",
165    identifier: Some("RunMat:pause:TooManyInputs"),
166    when: "More than one input argument is supplied.",
167    message: "pause: too many input arguments",
168};
169
170const PAUSE_ERROR_STATE_LOCK: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
171    code: "RM.PAUSE.STATE_LOCK",
172    identifier: Some("RunMat:pause:StateLockFailed"),
173    when: "Internal pause-state lock cannot be acquired.",
174    message: "pause: failed to acquire pause state",
175};
176
177const PAUSE_ERROR_GATHER_FAILED: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
178    code: "RM.PAUSE.GPU_GATHER_FAILED",
179    identifier: Some("RunMat:pause:GpuGatherFailed"),
180    when: "Gathering a GPU argument to host fails during argument classification.",
181    message: "pause: failed to gather gpu input",
182};
183
184const PAUSE_ERRORS: [BuiltinErrorDescriptor; 4] = [
185    PAUSE_ERROR_INVALID_ARG,
186    PAUSE_ERROR_TOO_MANY_INPUTS,
187    PAUSE_ERROR_STATE_LOCK,
188    PAUSE_ERROR_GATHER_FAILED,
189];
190
191pub const PAUSE_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
192    signatures: &PAUSE_SIGNATURES,
193    output_mode: BuiltinOutputMode::Fixed,
194    completion_policy: BuiltinCompletionPolicy::Public,
195    errors: &PAUSE_ERRORS,
196};
197
198fn pause_error_with_message(
199    message: impl Into<String>,
200    error: &'static BuiltinErrorDescriptor,
201) -> RuntimeError {
202    let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
203    if let Some(identifier) = error.identifier {
204        builder = builder.with_identifier(identifier);
205    }
206    builder.build()
207}
208
209#[derive(Debug, Clone, Copy)]
210enum PauseArgument {
211    Wait(PauseWait),
212    SetState(bool),
213    Query,
214}
215
216#[derive(Debug, Clone, Copy)]
217enum PauseWait {
218    Default,
219    Seconds(f64),
220}
221
222/// Suspend execution according to MATLAB-compatible pause semantics.
223#[runtime_builtin(
224    name = "pause",
225    category = "timing",
226    summary = "Pause execution until keypress or specified duration elapses.",
227    keywords = "pause,sleep,wait,delay",
228    accel = "metadata",
229    sink = true,
230    type_resolver(pause_type),
231    descriptor(crate::builtins::timing::pause::PAUSE_DESCRIPTOR),
232    extensions(crate::builtins::timing::pause::EXTENSIONS),
233    integer_capabilities(crate::builtins::timing::pause::INTEGER_CAPABILITIES),
234    builtin_path = "crate::builtins::timing::pause"
235)]
236async fn pause_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
237    match args.len() {
238        0 => {
239            perform_wait(PauseWait::Default).await?;
240            Ok(empty_return_value())
241        }
242        1 => match classify_argument(&args[0]).await? {
243            PauseArgument::Wait(wait) => {
244                perform_wait(wait).await?;
245                Ok(empty_return_value())
246            }
247            PauseArgument::SetState(next_state) => {
248                let previous = set_pause_enabled(next_state)?;
249                Ok(state_value(previous))
250            }
251            PauseArgument::Query => {
252                let current = pause_enabled()?;
253                Ok(state_value(current))
254            }
255        },
256        _ => Err(pause_error_with_message(
257            PAUSE_ERROR_TOO_MANY_INPUTS.message,
258            &PAUSE_ERROR_TOO_MANY_INPUTS,
259        )),
260    }
261}
262
263async fn perform_wait(wait: PauseWait) -> Result<(), RuntimeError> {
264    if !pause_enabled()? {
265        return Ok(());
266    }
267
268    #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
269    {
270        // MATLAB semantics: `pause` gives the UI a chance to update.
271        // In RunMat Web/WASM this is an explicit flush boundary for plotting.
272        let handle = plotting::current_figure_handle();
273        // Present before the wait.
274        let _ = plotting::render_current_scene(handle.as_u32());
275    }
276
277    match wait {
278        PauseWait::Default => wait_for_key_press().await,
279        PauseWait::Seconds(seconds) => {
280            if seconds == 0.0 {
281                // `pause(0)` is a useful yield point in simulation loops.
282                #[cfg(target_arch = "wasm32")]
283                {
284                    return wasm_sleep_seconds(0.0).await;
285                }
286                #[cfg(not(target_arch = "wasm32"))]
287                {
288                    return Ok(());
289                }
290            }
291            sleep_seconds(seconds).await?;
292            #[cfg(all(target_arch = "wasm32", feature = "plot-web"))]
293            {
294                // Present again after the wait to ensure the compositor sees the most recent frame.
295                // Some browser/driver combinations appear to delay presentation unless we yield across
296                // a timer boundary.
297                let handle = plotting::current_figure_handle();
298                let _ = plotting::render_current_scene(handle.as_u32());
299            }
300            Ok(())
301        }
302    }
303}
304
305async fn wait_for_key_press() -> Result<(), RuntimeError> {
306    #[cfg(test)]
307    {
308        Ok(())
309    }
310    #[cfg(not(test))]
311    {
312        interaction::wait_for_key_async("").await
313    }
314}
315
316async fn sleep_seconds(seconds: f64) -> Result<(), RuntimeError> {
317    #[cfg(target_arch = "wasm32")]
318    {
319        wasm_sleep_seconds(seconds).await
320    }
321    #[cfg(not(target_arch = "wasm32"))]
322    {
323        // from_secs_f64 rejects NaN/±Inf; classify_argument filters those earlier.
324        let duration = std::time::Duration::from_secs_f64(seconds);
325        std::thread::sleep(duration);
326        Ok(())
327    }
328}
329
330#[cfg(target_arch = "wasm32")]
331async fn wasm_sleep_seconds(seconds: f64) -> Result<(), RuntimeError> {
332    use js_sys::{Function, Promise, Reflect};
333    use wasm_bindgen::JsCast;
334    use wasm_bindgen_futures::JsFuture;
335
336    // `pause` runs in both Window and WebWorker contexts; workers do not have `window`.
337    // Use the global `setTimeout` function instead.
338    let global = js_sys::global();
339    let set_timeout = Reflect::get(&global, &wasm_bindgen::JsValue::from_str("setTimeout"))
340        .map_err(|_| build_runtime_error("pause: setTimeout unavailable").build())?
341        .dyn_into::<Function>()
342        .map_err(|_| build_runtime_error("pause: setTimeout unavailable").build())?;
343
344    let millis = (seconds * 1000.0).max(0.0).round();
345    let millis_i32 = if millis > i32::MAX as f64 {
346        i32::MAX
347    } else {
348        millis as i32
349    };
350
351    let promise = Promise::new(&mut |resolve, _reject| {
352        let resolve: Function = resolve.unchecked_into();
353        let _ = set_timeout.call2(
354            &global,
355            &resolve.into(),
356            &wasm_bindgen::JsValue::from_f64(millis_i32 as f64),
357        );
358    });
359
360    let _ = JsFuture::from(promise)
361        .await
362        .map_err(|err| build_runtime_error(format!("pause: timer failed ({err:?})")).build())?;
363    Ok(())
364}
365
366async fn classify_argument(arg: &Value) -> Result<PauseArgument, RuntimeError> {
367    if matches!(arg, Value::GpuTensor(handle) if runmat_accelerate_api::handle_is_explicit(handle))
368    {
369        crate::compatibility::ensure_builtin_extension_enabled(&GPU_INPUT_EXTENSION, BUILTIN_NAME)?;
370    }
371    let host_value = gpu_helpers::gather_value_async(arg)
372        .await
373        .map_err(|e| pause_error_with_message(format!("pause: {e}"), &PAUSE_ERROR_GATHER_FAILED))?;
374    match host_value {
375        Value::String(text) => parse_command(&text),
376        Value::CharArray(ca) => {
377            if ca.rows == 0 || ca.data.is_empty() {
378                Ok(PauseArgument::Wait(PauseWait::Default))
379            } else if ca.rows == 1 {
380                let text: String = ca.data.iter().collect();
381                parse_command(&text)
382            } else {
383                Err(pause_error_with_message(
384                    PAUSE_ERROR_INVALID_ARG.message,
385                    &PAUSE_ERROR_INVALID_ARG,
386                ))
387            }
388        }
389        Value::StringArray(sa) => {
390            if sa.data.is_empty() {
391                Ok(PauseArgument::Wait(PauseWait::Default))
392            } else if sa.data.len() == 1 {
393                parse_command(&sa.data[0])
394            } else {
395                Err(pause_error_with_message(
396                    PAUSE_ERROR_INVALID_ARG.message,
397                    &PAUSE_ERROR_INVALID_ARG,
398                ))
399            }
400        }
401        Value::Num(value) => parse_numeric(value),
402        Value::Int(int_value) => parse_numeric(int_value.to_f64()),
403        Value::Bool(flag) => parse_numeric(if flag { 1.0 } else { 0.0 }),
404        Value::Tensor(tensor) => parse_tensor(tensor),
405        Value::LogicalArray(logical) => parse_logical(logical),
406        Value::GpuTensor(handle) => {
407            let tensor = gpu_helpers::gather_tensor_async(&handle).await?;
408            parse_tensor(tensor)
409        }
410        Value::Complex(_, _)
411        | Value::ComplexTensor(_)
412        | Value::Symbolic(_)
413        | Value::SymbolicArray(_)
414        | Value::SparseTensor(_)
415        | Value::Cell(_)
416        | Value::Struct(_)
417        | Value::ObjectArray(_)
418        | Value::Object(_)
419        | Value::HandleObject(_)
420        | Value::Listener(_)
421        | Value::FunctionHandle(_)
422        | Value::ExternalFunctionHandle(_)
423        | Value::MethodFunctionHandle(_)
424        | Value::BoundFunctionHandle { .. }
425        | Value::Closure(_)
426        | Value::ClassRef(_)
427        | Value::MException(_)
428        | Value::Future(_)
429        | Value::Task(_)
430        | Value::Pool(_)
431        | Value::Job(_)
432        | Value::Foreign(_)
433        | Value::OutputList(_) => Err(pause_error_with_message(
434            PAUSE_ERROR_INVALID_ARG.message,
435            &PAUSE_ERROR_INVALID_ARG,
436        )),
437    }
438}
439
440fn parse_command(raw: &str) -> Result<PauseArgument, RuntimeError> {
441    let trimmed = raw.trim();
442    if trimmed.is_empty() {
443        return Ok(PauseArgument::Wait(PauseWait::Default));
444    }
445    let lower = trimmed.to_ascii_lowercase();
446    match lower.as_str() {
447        "on" => Ok(PauseArgument::SetState(true)),
448        "off" => Ok(PauseArgument::SetState(false)),
449        "query" => Ok(PauseArgument::Query),
450        _ => Err(pause_error_with_message(
451            PAUSE_ERROR_INVALID_ARG.message,
452            &PAUSE_ERROR_INVALID_ARG,
453        )),
454    }
455}
456
457fn parse_numeric(value: f64) -> Result<PauseArgument, RuntimeError> {
458    if !value.is_finite() {
459        if value.is_sign_positive() {
460            return Ok(PauseArgument::Wait(PauseWait::Default));
461        }
462        return Err(pause_error_with_message(
463            PAUSE_ERROR_INVALID_ARG.message,
464            &PAUSE_ERROR_INVALID_ARG,
465        ));
466    }
467    if value < 0.0 {
468        return Err(pause_error_with_message(
469            PAUSE_ERROR_INVALID_ARG.message,
470            &PAUSE_ERROR_INVALID_ARG,
471        ));
472    }
473    Ok(PauseArgument::Wait(PauseWait::Seconds(value)))
474}
475
476fn parse_tensor(tensor: Tensor) -> Result<PauseArgument, RuntimeError> {
477    let len = tensor.len();
478    if len == 0 {
479        return Ok(PauseArgument::Wait(PauseWait::Default));
480    }
481    if len != 1 {
482        return Err(pause_error_with_message(
483            PAUSE_ERROR_INVALID_ARG.message,
484            &PAUSE_ERROR_INVALID_ARG,
485        ));
486    }
487    parse_numeric(tensor_utils::tensor_value_f64(&tensor, 0))
488}
489
490fn parse_logical(logical: LogicalArray) -> Result<PauseArgument, RuntimeError> {
491    if logical.data.is_empty() {
492        return Ok(PauseArgument::Wait(PauseWait::Default));
493    }
494    if logical.data.len() != 1 {
495        return Err(pause_error_with_message(
496            PAUSE_ERROR_INVALID_ARG.message,
497            &PAUSE_ERROR_INVALID_ARG,
498        ));
499    }
500    let scalar = if logical.data[0] != 0 { 1.0 } else { 0.0 };
501    parse_numeric(scalar)
502}
503
504fn empty_return_value() -> Value {
505    Value::Tensor(Tensor::zeros(vec![0, 0]))
506}
507
508fn state_value(enabled: bool) -> Value {
509    let text = if enabled { "on" } else { "off" };
510    Value::CharArray(CharArray::new_row(text))
511}
512
513fn pause_enabled() -> Result<bool, RuntimeError> {
514    PAUSE_STATE.read().map(|guard| guard.enabled).map_err(|_| {
515        pause_error_with_message(PAUSE_ERROR_STATE_LOCK.message, &PAUSE_ERROR_STATE_LOCK)
516    })
517}
518
519fn set_pause_enabled(next: bool) -> Result<bool, RuntimeError> {
520    let mut guard = PAUSE_STATE.write().map_err(|_| {
521        pause_error_with_message(PAUSE_ERROR_STATE_LOCK.message, &PAUSE_ERROR_STATE_LOCK)
522    })?;
523    let previous = guard.enabled;
524    guard.enabled = next;
525    Ok(previous)
526}
527
528#[cfg(test)]
529pub(crate) mod tests {
530    use super::*;
531    use crate::builtins::common::test_support;
532    use futures::executor::block_on;
533    use runmat_accelerate_api::HostTensorView;
534    use runmat_value::{IntValue, IntegerStorage, LogicalArray, Tensor};
535
536    #[cfg(feature = "wgpu")]
537    use runmat_accelerate::backend::wgpu::provider as wgpu_provider;
538
539    fn reset_state(enabled: bool) {
540        let mut guard = PAUSE_STATE.write().unwrap_or_else(|e| e.into_inner());
541        guard.enabled = enabled;
542    }
543
544    fn char_array_to_string(value: Value) -> String {
545        match value {
546            Value::CharArray(ca) if ca.rows == 1 => ca.data.iter().collect(),
547            other => panic!("expected char array, got {other:?}"),
548        }
549    }
550
551    fn assert_pause_error_identifier(err: crate::RuntimeError, identifier: &str) {
552        assert_eq!(
553            err.identifier(),
554            Some(identifier),
555            "message: {}",
556            err.message()
557        );
558    }
559
560    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
561    #[test]
562    fn query_returns_on_by_default() {
563        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
564        reset_state(true);
565        let result = block_on(pause_builtin(vec![Value::from("query")])).expect("pause query");
566        assert_eq!(char_array_to_string(result), "on");
567    }
568
569    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
570    #[test]
571    fn pause_off_returns_previous_state() {
572        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
573        reset_state(true);
574        let previous = block_on(pause_builtin(vec![Value::from("off")])).expect("pause off");
575        assert_eq!(char_array_to_string(previous), "on");
576        assert!(!pause_enabled().unwrap());
577    }
578
579    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
580    #[test]
581    fn pause_on_restores_state() {
582        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
583        reset_state(false);
584        let previous = block_on(pause_builtin(vec![Value::from("on")])).expect("pause on");
585        assert_eq!(char_array_to_string(previous), "off");
586        assert!(pause_enabled().unwrap());
587    }
588
589    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
590    #[test]
591    fn pause_default_returns_empty_tensor() {
592        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
593        reset_state(true);
594        let result = block_on(pause_builtin(Vec::new())).expect("pause()");
595        match result {
596            Value::Tensor(t) => assert_eq!(t.materialize_f64().len(), 0),
597            other => panic!("expected empty tensor, got {other:?}"),
598        }
599    }
600
601    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
602    #[test]
603    fn numeric_zero_is_accepted() {
604        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
605        reset_state(true);
606        let result = block_on(pause_builtin(vec![Value::Num(0.0)])).expect("pause(0)");
607        match result {
608            Value::Tensor(t) => assert_eq!(t.materialize_f64().len(), 0),
609            other => panic!("expected empty tensor, got {other:?}"),
610        }
611    }
612
613    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
614    #[test]
615    fn integer_scalar_is_accepted() {
616        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
617        reset_state(true);
618        let result =
619            block_on(pause_builtin(vec![Value::Int(IntValue::I32(0))])).expect("pause(int)");
620        match result {
621            Value::Tensor(t) => assert_eq!(t.materialize_f64().len(), 0),
622            other => panic!("expected empty tensor, got {other:?}"),
623        }
624    }
625
626    #[test]
627    fn pause_tensor_reads_typed_integer_storage_exactly() {
628        let tensor = Tensor::new_integer(IntegerStorage::U16(vec![2026]), vec![1, 1])
629            .expect("typed pause tensor");
630
631        match parse_tensor(tensor).expect("pause tensor") {
632            PauseArgument::Wait(PauseWait::Seconds(seconds)) => assert_eq!(seconds, 2026.0),
633            other => panic!("expected wait seconds, got {other:?}"),
634        }
635    }
636
637    #[test]
638    fn pause_accepts_every_integer_duration_class() {
639        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
640        reset_state(false);
641        for value in [
642            IntValue::I8(1),
643            IntValue::I16(1),
644            IntValue::I32(1),
645            IntValue::I64(1),
646            IntValue::U8(1),
647            IntValue::U16(1),
648            IntValue::U32(1),
649            IntValue::U64(1),
650        ] {
651            let result = block_on(pause_builtin(vec![Value::Int(value)]))
652                .expect("documented integer duration");
653            assert!(matches!(result, Value::Tensor(tensor) if tensor.is_empty()));
654        }
655        reset_state(true);
656    }
657
658    #[test]
659    fn pause_resident_argument_is_gated_before_provider_access() {
660        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
661        let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
662        let resident = Value::GpuTensor(
663            runmat_accelerate_api::GpuTensorHandle {
664                shape: vec![1, 1],
665                device_id: u32::MAX,
666                buffer_id: u64::MAX,
667                descriptor: Default::default(),
668            }
669            .with_provenance(runmat_accelerate_api::GpuHandleProvenance::Explicit),
670        );
671        let error = block_on(pause_builtin(vec![resident])).expect_err("resident pause input");
672
673        assert_eq!(
674            error.identifier(),
675            Some("RunMat:compatibility:PauseGpuInputExtension")
676        );
677    }
678
679    #[test]
680    fn pause_automatically_resident_duration_gathers_in_strict_mode() {
681        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
682        reset_state(false);
683        test_support::with_test_provider(|provider| {
684            let tensor = Tensor::new_integer(IntegerStorage::U16(vec![1]), vec![1, 1]).unwrap();
685            let handle = gpu_helpers::upload_tensor(provider, &tensor)
686                .expect("resident duration")
687                .with_provenance(runmat_accelerate_api::GpuHandleProvenance::Automatic);
688            let result = {
689                let _strict = crate::compatibility::push_runmat_extensions_enabled(false);
690                block_on(pause_builtin(vec![Value::GpuTensor(handle.clone())]))
691                    .expect("automatic duration gathers transparently")
692            };
693            assert!(matches!(result, Value::Tensor(tensor) if tensor.is_empty()));
694            runmat_accelerate_api::clear_handle_metadata(&handle);
695        });
696        reset_state(true);
697    }
698
699    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
700    #[test]
701    fn numeric_negative_zero_is_treated_as_zero() {
702        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
703        reset_state(true);
704        let result = block_on(pause_builtin(vec![Value::Num(-0.0)])).expect("pause(-0)");
705        match result {
706            Value::Tensor(t) => assert_eq!(t.materialize_f64().len(), 0),
707            other => panic!("expected empty tensor, got {other:?}"),
708        }
709    }
710
711    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
712    #[test]
713    fn negative_duration_raises_error() {
714        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
715        reset_state(true);
716        let err = block_on(pause_builtin(vec![Value::Num(-0.1)])).unwrap_err();
717        assert_pause_error_identifier(err, PAUSE_ERROR_INVALID_ARG.identifier.unwrap());
718    }
719
720    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
721    #[test]
722    fn non_scalar_tensor_is_rejected() {
723        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
724        reset_state(true);
725        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
726        let err = block_on(pause_builtin(vec![Value::Tensor(tensor)])).unwrap_err();
727        assert_pause_error_identifier(err, PAUSE_ERROR_INVALID_ARG.identifier.unwrap());
728    }
729
730    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
731    #[test]
732    fn empty_tensor_behaves_like_default_pause() {
733        let _guard = TEST_GUARD.lock().unwrap();
734        reset_state(true);
735        let empty = Tensor::zeros(vec![0, 0]);
736        let result = block_on(pause_builtin(vec![Value::Tensor(empty)])).expect("pause([])");
737        match result {
738            Value::Tensor(t) => assert_eq!(t.materialize_f64().len(), 0),
739            other => panic!("expected empty tensor, got {other:?}"),
740        }
741    }
742
743    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
744    #[test]
745    fn logical_scalar_is_accepted() {
746        let _guard = TEST_GUARD.lock().unwrap();
747        reset_state(true);
748        let logical = LogicalArray::new(vec![1u8], vec![1, 1]).unwrap();
749        let result =
750            block_on(pause_builtin(vec![Value::LogicalArray(logical)])).expect("pause(true)");
751        match result {
752            Value::Tensor(t) => assert_eq!(t.materialize_f64().len(), 0),
753            other => panic!("expected empty tensor, got {other:?}"),
754        }
755    }
756
757    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
758    #[test]
759    fn infinite_duration_behaves_like_default() {
760        let _guard = TEST_GUARD.lock().unwrap();
761        reset_state(true);
762        let result = block_on(pause_builtin(vec![Value::Num(f64::INFINITY)])).expect("pause(Inf)");
763        match result {
764            Value::Tensor(t) => assert_eq!(t.materialize_f64().len(), 0),
765            other => panic!("expected empty tensor, got {other:?}"),
766        }
767    }
768
769    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
770    #[test]
771    fn pause_gpu_duration_gathered() {
772        let _guard = TEST_GUARD.lock().unwrap();
773        reset_state(true);
774        test_support::with_test_provider(|provider| {
775            let tensor = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
776            let view = HostTensorView {
777                data: &tensor.materialize_f64(),
778                shape: &tensor.shape,
779            };
780            let handle = provider.upload(&view).expect("upload");
781            let result =
782                block_on(pause_builtin(vec![Value::GpuTensor(handle)])).expect("pause(gpuScalar)");
783            match result {
784                Value::Tensor(t) => assert_eq!(t.materialize_f64().len(), 0),
785                other => panic!("expected empty tensor, got {other:?}"),
786            }
787        });
788    }
789
790    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
791    #[test]
792    #[cfg(feature = "wgpu")]
793    fn pause_wgpu_duration_gathered() {
794        let _guard = TEST_GUARD.lock().unwrap();
795        reset_state(true);
796        if wgpu_provider::register_wgpu_provider(wgpu_provider::WgpuProviderOptions::default())
797            .is_err()
798        {
799            return;
800        }
801        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
802        let tensor = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
803        let view = HostTensorView {
804            data: &tensor.materialize_f64(),
805            shape: &tensor.shape,
806        };
807        let handle = provider.upload(&view).expect("upload");
808        let result =
809            block_on(pause_builtin(vec![Value::GpuTensor(handle)])).expect("pause(gpuScalar)");
810        match result {
811            Value::Tensor(t) => assert_eq!(t.materialize_f64().len(), 0),
812            other => panic!("expected empty tensor, got {other:?}"),
813        }
814    }
815
816    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
817    #[test]
818    fn invalid_command_raises_error() {
819        let _guard = TEST_GUARD.lock().unwrap();
820        reset_state(true);
821        let err = block_on(pause_builtin(vec![Value::from("invalid")])).unwrap_err();
822        assert_pause_error_identifier(err, PAUSE_ERROR_INVALID_ARG.identifier.unwrap());
823    }
824
825    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
826    #[test]
827    fn too_many_inputs_raises_error() {
828        let _guard = TEST_GUARD.lock().unwrap();
829        reset_state(true);
830        let err = block_on(pause_builtin(vec![Value::Num(0.0), Value::Num(0.0)])).unwrap_err();
831        assert_pause_error_identifier(err, PAUSE_ERROR_TOO_MANY_INPUTS.identifier.unwrap());
832    }
833}