runmat_runtime/builtins/timing/
pause.rs

1//! MATLAB-compatible `pause` builtin that temporarily suspends execution.
2
3use once_cell::sync::Lazy;
4use runmat_builtins::{CharArray, LogicalArray, Tensor, Value};
5use runmat_macros::runtime_builtin;
6#[cfg(not(test))]
7use std::io::{self, IsTerminal, Read};
8use std::sync::RwLock;
9use std::thread;
10use std::time::Duration;
11
12use crate::builtins::common::gpu_helpers;
13use crate::builtins::common::spec::{
14    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
15    ReductionNaN, ResidencyPolicy, ShapeRequirements,
16};
17#[cfg(feature = "doc_export")]
18use crate::register_builtin_doc_text;
19use crate::{register_builtin_fusion_spec, register_builtin_gpu_spec};
20
21#[cfg(feature = "doc_export")]
22pub const DOC_MD: &str = r#"---
23title: "pause"
24category: "timing"
25keywords: ["pause", "sleep", "wait", "delay", "press any key", "execution"]
26summary: "Suspend execution until the user presses a key or a specified time elapses."
27references: []
28gpu_support:
29  elementwise: false
30  reduction: false
31  precisions: []
32  broadcasting: "none"
33  notes: "pause executes entirely on the host CPU. GPU providers are never consulted and no residency changes occur."
34fusion:
35  elementwise: false
36  reduction: false
37  max_inputs: 1
38  constants: "inline"
39requires_feature: null
40tested:
41  unit: "builtins::timing::pause::tests"
42  integration: "builtins::timing::pause::tests::pause_gpu_duration_gathered"
43---
44
45# What does the `pause` function do in MATLAB / RunMat?
46`pause` suspends execution and mirrors MATLAB's timing semantics:
47
48- `pause` with no inputs waits for keyboard input (press any key) while pause mode is `on`.
49- `pause(t)` delays execution for `t` seconds (non-negative numeric scalar). `t = Inf` behaves like `pause` with no arguments.
50- `pause('on')` and `pause('off')` enable or disable pausing globally, returning the previous state (`'on'` or `'off'`).
51- `pause('query')` reports the current state (`'on'` or `'off'`).
52- `pause([])` is treated as `pause` with no arguments.
53- When pause mode is `off`, delays and key waits complete immediately.
54
55Invalid usages (negative times, non-scalar numeric inputs, or unknown strings) raise `MATLAB:pause:InvalidInputArgument`, matching MATLAB diagnostics.
56
57## GPU Execution and Residency
58`pause` never runs on the GPU. When you pass GPU-resident values (for example, `pause(gpuArray(0.5))`), RunMat automatically gathers them to the host before evaluating the delay. No residency changes occur otherwise, and acceleration providers do not receive any callbacks.
59
60## Examples of using the `pause` function in MATLAB / RunMat
61
62### Pausing for a fixed duration
63```matlab
64tic;
65pause(0.05);     % wait 50 milliseconds
66elapsed = toc;
67```
68
69### Waiting for user input mid-script
70```matlab
71disp("Press any key to continue the demo...");
72pause;           % waits until the user presses a key (while pause is 'on')
73```
74
75### Temporarily disabling pauses in automated runs
76```matlab
77state = pause('off');   % returns previous state so it can be restored
78cleanup = onCleanup(@() pause(state));  % ensure state is restored
79pause(1.0);             % returns immediately because pause is disabled
80```
81
82### Querying the current pause mode
83```matlab
84current = pause('query');   % returns 'on' or 'off'
85```
86
87### Using empty input to rely on the default behaviour
88```matlab
89pause([]);   % equivalent to calling pause with no arguments
90```
91
92## FAQ
93
941. **Does `pause` block forever when standard input is not interactive?** No. When RunMat detects a non-interactive standard input (for example, during automated tests), `pause` completes immediately even in `'on'` mode.
952. **What happens if I call `pause` with a negative duration?** RunMat raises `MATLAB:pause:InvalidInputArgument`, matching MATLAB.
963. **Does `pause` accept logical or integer values?** Yes. Logical and integer inputs are converted to doubles before evaluating the delay.
974. **Can I force pausing off globally?** Use `pause('off')` to disable pauses. Record the return value so you can restore the prior state with `pause(previousState)`.
985. **Does `pause('query')` change the pause state?** No. It simply reports the current mode (`'on'` or `'off'`).
996. **Is `pause` affected by GPU fusion or auto-offload?** No. The builtin runs on the host regardless of fusion plans or acceleration providers.
1007. **What is the default pause state?** `'on'`. Every RunMat session starts with pausing enabled.
1018. **Can I pass a gpuArray as the duration?** Yes. RunMat gathers the scalar duration to the host before evaluating the delay.
102
103## See Also
104[tic](./tic), [toc](./toc), [timeit](./timeit)
105
106## Source & Feedback
107- Implementation: [`crates/runmat-runtime/src/builtins/timing/pause.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/timing/pause.rs)
108- Found a behavioural difference? [Open an issue](https://github.com/runmat-org/runmat/issues/new/choose) with a minimal repro.
109"#;
110
111pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
112    name: "pause",
113    op_kind: GpuOpKind::Custom("timer"),
114    supported_precisions: &[],
115    broadcast: BroadcastSemantics::None,
116    provider_hooks: &[],
117    constant_strategy: ConstantStrategy::InlineLiteral,
118    residency: ResidencyPolicy::GatherImmediately,
119    nan_mode: ReductionNaN::Include,
120    two_pass_threshold: None,
121    workgroup_size: None,
122    accepts_nan_mode: false,
123    notes: "pause executes entirely on the host. Acceleration providers are never queried.",
124};
125
126register_builtin_gpu_spec!(GPU_SPEC);
127
128pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
129    name: "pause",
130    shape: ShapeRequirements::Any,
131    constant_strategy: ConstantStrategy::InlineLiteral,
132    elementwise: None,
133    reduction: None,
134    emits_nan: false,
135    notes: "pause suspends host execution and is excluded from fusion pipelines.",
136};
137
138register_builtin_fusion_spec!(FUSION_SPEC);
139
140#[cfg(feature = "doc_export")]
141register_builtin_doc_text!("pause", DOC_MD);
142
143static PAUSE_STATE: Lazy<RwLock<PauseState>> = Lazy::new(|| RwLock::new(PauseState::default()));
144
145#[cfg(test)]
146use std::sync::Mutex;
147#[cfg(test)]
148pub(crate) static TEST_GUARD: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
149
150#[derive(Debug, Clone, Copy)]
151struct PauseState {
152    enabled: bool,
153}
154
155impl Default for PauseState {
156    fn default() -> Self {
157        Self { enabled: true }
158    }
159}
160
161const ERR_INVALID_ARG: &str = "MATLAB:pause:InvalidInputArgument";
162const ERR_TOO_MANY_INPUTS: &str = "MATLAB:pause:TooManyInputs";
163const ERR_STATE_LOCK: &str = "pause: failed to acquire pause state";
164
165#[derive(Debug, Clone, Copy)]
166enum PauseArgument {
167    Wait(PauseWait),
168    SetState(bool),
169    Query,
170}
171
172#[derive(Debug, Clone, Copy)]
173enum PauseWait {
174    Default,
175    Seconds(f64),
176}
177
178/// Suspend execution according to MATLAB-compatible pause semantics.
179#[runtime_builtin(
180    name = "pause",
181    category = "timing",
182    summary = "Suspend execution until a key press or specified duration.",
183    keywords = "pause,sleep,wait,delay",
184    accel = "metadata",
185    sink = true
186)]
187fn pause_builtin(args: Vec<Value>) -> Result<Value, String> {
188    match args.len() {
189        0 => {
190            perform_wait(PauseWait::Default)?;
191            Ok(empty_return_value())
192        }
193        1 => match classify_argument(&args[0])? {
194            PauseArgument::Wait(wait) => {
195                perform_wait(wait)?;
196                Ok(empty_return_value())
197            }
198            PauseArgument::SetState(next_state) => {
199                let previous = set_pause_enabled(next_state)?;
200                Ok(state_value(previous))
201            }
202            PauseArgument::Query => {
203                let current = pause_enabled()?;
204                Ok(state_value(current))
205            }
206        },
207        _ => Err(ERR_TOO_MANY_INPUTS.to_string()),
208    }
209}
210
211fn perform_wait(wait: PauseWait) -> Result<(), String> {
212    if !pause_enabled()? {
213        return Ok(());
214    }
215
216    match wait {
217        PauseWait::Default => wait_for_key_press(),
218        PauseWait::Seconds(seconds) => {
219            if seconds == 0.0 {
220                return Ok(());
221            }
222            // from_secs_f64 rejects NaN/±Inf; classify_argument filters those earlier.
223            let duration = Duration::from_secs_f64(seconds);
224            thread::sleep(duration);
225            Ok(())
226        }
227    }
228}
229
230#[cfg(test)]
231fn wait_for_key_press() -> Result<(), String> {
232    // During crate-level tests we treat pause as non-interactive so unit tests
233    // never block waiting for a key press on an interactive terminal.
234    Ok(())
235}
236
237#[cfg(not(test))]
238fn wait_for_key_press() -> Result<(), String> {
239    let stdin = io::stdin();
240    if !stdin.is_terminal() {
241        thread::sleep(Duration::from_millis(1));
242        return Ok(());
243    }
244
245    let mut handle = stdin.lock();
246    let mut buffer = [0u8; 1];
247    loop {
248        match handle.read(&mut buffer) {
249            Ok(0) => return Ok(()),
250            Ok(_) => return Ok(()),
251            Err(err) if err.kind() == io::ErrorKind::Interrupted => continue,
252            Err(err) => return Err(format!("pause: failed to read from stdin: {err}")),
253        }
254    }
255}
256
257fn classify_argument(arg: &Value) -> Result<PauseArgument, String> {
258    let host_value = gpu_helpers::gather_value(arg).map_err(|e| format!("pause: {e}"))?;
259    match host_value {
260        Value::String(text) => parse_command(&text),
261        Value::CharArray(ca) => {
262            if ca.rows == 0 || ca.data.is_empty() {
263                Ok(PauseArgument::Wait(PauseWait::Default))
264            } else if ca.rows == 1 {
265                let text: String = ca.data.iter().collect();
266                parse_command(&text)
267            } else {
268                Err(ERR_INVALID_ARG.to_string())
269            }
270        }
271        Value::StringArray(sa) => {
272            if sa.data.is_empty() {
273                Ok(PauseArgument::Wait(PauseWait::Default))
274            } else if sa.data.len() == 1 {
275                parse_command(&sa.data[0])
276            } else {
277                Err(ERR_INVALID_ARG.to_string())
278            }
279        }
280        Value::Num(value) => parse_numeric(value),
281        Value::Int(int_value) => parse_numeric(int_value.to_f64()),
282        Value::Bool(flag) => parse_numeric(if flag { 1.0 } else { 0.0 }),
283        Value::Tensor(tensor) => parse_tensor(tensor),
284        Value::LogicalArray(logical) => parse_logical(logical),
285        Value::GpuTensor(handle) => {
286            let tensor = gpu_helpers::gather_tensor(&handle)?;
287            parse_tensor(tensor)
288        }
289        Value::Complex(_, _)
290        | Value::ComplexTensor(_)
291        | Value::Cell(_)
292        | Value::Struct(_)
293        | Value::Object(_)
294        | Value::HandleObject(_)
295        | Value::Listener(_)
296        | Value::FunctionHandle(_)
297        | Value::Closure(_)
298        | Value::ClassRef(_)
299        | Value::MException(_) => Err(ERR_INVALID_ARG.to_string()),
300    }
301}
302
303fn parse_command(raw: &str) -> Result<PauseArgument, String> {
304    let trimmed = raw.trim();
305    if trimmed.is_empty() {
306        return Ok(PauseArgument::Wait(PauseWait::Default));
307    }
308    let lower = trimmed.to_ascii_lowercase();
309    match lower.as_str() {
310        "on" => Ok(PauseArgument::SetState(true)),
311        "off" => Ok(PauseArgument::SetState(false)),
312        "query" => Ok(PauseArgument::Query),
313        _ => Err(ERR_INVALID_ARG.to_string()),
314    }
315}
316
317fn parse_numeric(value: f64) -> Result<PauseArgument, String> {
318    if !value.is_finite() {
319        if value.is_sign_positive() {
320            return Ok(PauseArgument::Wait(PauseWait::Default));
321        }
322        return Err(ERR_INVALID_ARG.to_string());
323    }
324    if value < 0.0 {
325        return Err(ERR_INVALID_ARG.to_string());
326    }
327    Ok(PauseArgument::Wait(PauseWait::Seconds(value)))
328}
329
330fn parse_tensor(tensor: Tensor) -> Result<PauseArgument, String> {
331    if tensor.data.is_empty() {
332        return Ok(PauseArgument::Wait(PauseWait::Default));
333    }
334    if tensor.data.len() != 1 {
335        return Err(ERR_INVALID_ARG.to_string());
336    }
337    parse_numeric(tensor.data[0])
338}
339
340fn parse_logical(logical: LogicalArray) -> Result<PauseArgument, String> {
341    if logical.data.is_empty() {
342        return Ok(PauseArgument::Wait(PauseWait::Default));
343    }
344    if logical.data.len() != 1 {
345        return Err(ERR_INVALID_ARG.to_string());
346    }
347    let scalar = if logical.data[0] != 0 { 1.0 } else { 0.0 };
348    parse_numeric(scalar)
349}
350
351fn empty_return_value() -> Value {
352    Value::Tensor(Tensor::zeros(vec![0, 0]))
353}
354
355fn state_value(enabled: bool) -> Value {
356    let text = if enabled { "on" } else { "off" };
357    Value::CharArray(CharArray::new_row(text))
358}
359
360fn pause_enabled() -> Result<bool, String> {
361    PAUSE_STATE
362        .read()
363        .map(|guard| guard.enabled)
364        .map_err(|_| ERR_STATE_LOCK.to_string())
365}
366
367fn set_pause_enabled(next: bool) -> Result<bool, String> {
368    let mut guard = PAUSE_STATE
369        .write()
370        .map_err(|_| ERR_STATE_LOCK.to_string())?;
371    let previous = guard.enabled;
372    guard.enabled = next;
373    Ok(previous)
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379    use crate::builtins::common::test_support;
380    use runmat_accelerate_api::HostTensorView;
381    use runmat_builtins::{IntValue, LogicalArray, Tensor};
382
383    #[cfg(feature = "wgpu")]
384    use runmat_accelerate::backend::wgpu::provider as wgpu_provider;
385
386    fn reset_state(enabled: bool) {
387        let mut guard = PAUSE_STATE.write().unwrap_or_else(|e| e.into_inner());
388        guard.enabled = enabled;
389    }
390
391    fn char_array_to_string(value: Value) -> String {
392        match value {
393            Value::CharArray(ca) if ca.rows == 1 => ca.data.iter().collect(),
394            other => panic!("expected char array, got {other:?}"),
395        }
396    }
397
398    #[test]
399    fn query_returns_on_by_default() {
400        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
401        reset_state(true);
402        let result = pause_builtin(vec![Value::from("query")]).expect("pause query");
403        assert_eq!(char_array_to_string(result), "on");
404    }
405
406    #[test]
407    fn pause_off_returns_previous_state() {
408        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
409        reset_state(true);
410        let previous = pause_builtin(vec![Value::from("off")]).expect("pause off");
411        assert_eq!(char_array_to_string(previous), "on");
412        assert!(!pause_enabled().unwrap());
413    }
414
415    #[test]
416    fn pause_on_restores_state() {
417        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
418        reset_state(false);
419        let previous = pause_builtin(vec![Value::from("on")]).expect("pause on");
420        assert_eq!(char_array_to_string(previous), "off");
421        assert!(pause_enabled().unwrap());
422    }
423
424    #[test]
425    fn pause_default_returns_empty_tensor() {
426        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
427        reset_state(true);
428        let result = pause_builtin(Vec::new()).expect("pause()");
429        match result {
430            Value::Tensor(t) => assert_eq!(t.data.len(), 0),
431            other => panic!("expected empty tensor, got {other:?}"),
432        }
433    }
434
435    #[test]
436    fn numeric_zero_is_accepted() {
437        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
438        reset_state(true);
439        let result = pause_builtin(vec![Value::Num(0.0)]).expect("pause(0)");
440        match result {
441            Value::Tensor(t) => assert_eq!(t.data.len(), 0),
442            other => panic!("expected empty tensor, got {other:?}"),
443        }
444    }
445
446    #[test]
447    fn integer_scalar_is_accepted() {
448        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
449        reset_state(true);
450        let result = pause_builtin(vec![Value::Int(IntValue::I32(0))]).expect("pause(int)");
451        match result {
452            Value::Tensor(t) => assert_eq!(t.data.len(), 0),
453            other => panic!("expected empty tensor, got {other:?}"),
454        }
455    }
456
457    #[test]
458    fn numeric_negative_zero_is_treated_as_zero() {
459        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
460        reset_state(true);
461        let result = pause_builtin(vec![Value::Num(-0.0)]).expect("pause(-0)");
462        match result {
463            Value::Tensor(t) => assert_eq!(t.data.len(), 0),
464            other => panic!("expected empty tensor, got {other:?}"),
465        }
466    }
467
468    #[test]
469    fn negative_duration_raises_error() {
470        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
471        reset_state(true);
472        let err = pause_builtin(vec![Value::Num(-0.1)]).unwrap_err();
473        assert_eq!(err, ERR_INVALID_ARG);
474    }
475
476    #[test]
477    fn non_scalar_tensor_is_rejected() {
478        let _guard = TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner());
479        reset_state(true);
480        let tensor = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
481        let err = pause_builtin(vec![Value::Tensor(tensor)]).unwrap_err();
482        assert_eq!(err, ERR_INVALID_ARG);
483    }
484
485    #[test]
486    fn empty_tensor_behaves_like_default_pause() {
487        let _guard = TEST_GUARD.lock().unwrap();
488        reset_state(true);
489        let empty = Tensor::zeros(vec![0, 0]);
490        let result = pause_builtin(vec![Value::Tensor(empty)]).expect("pause([])");
491        match result {
492            Value::Tensor(t) => assert_eq!(t.data.len(), 0),
493            other => panic!("expected empty tensor, got {other:?}"),
494        }
495    }
496
497    #[test]
498    fn logical_scalar_is_accepted() {
499        let _guard = TEST_GUARD.lock().unwrap();
500        reset_state(true);
501        let logical = LogicalArray::new(vec![1u8], vec![1, 1]).unwrap();
502        let result = pause_builtin(vec![Value::LogicalArray(logical)]).expect("pause(true)");
503        match result {
504            Value::Tensor(t) => assert_eq!(t.data.len(), 0),
505            other => panic!("expected empty tensor, got {other:?}"),
506        }
507    }
508
509    #[test]
510    fn infinite_duration_behaves_like_default() {
511        let _guard = TEST_GUARD.lock().unwrap();
512        reset_state(true);
513        let result = pause_builtin(vec![Value::Num(f64::INFINITY)]).expect("pause(Inf)");
514        match result {
515            Value::Tensor(t) => assert_eq!(t.data.len(), 0),
516            other => panic!("expected empty tensor, got {other:?}"),
517        }
518    }
519
520    #[test]
521    fn pause_gpu_duration_gathered() {
522        let _guard = TEST_GUARD.lock().unwrap();
523        reset_state(true);
524        test_support::with_test_provider(|provider| {
525            let tensor = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
526            let view = HostTensorView {
527                data: &tensor.data,
528                shape: &tensor.shape,
529            };
530            let handle = provider.upload(&view).expect("upload");
531            let result = pause_builtin(vec![Value::GpuTensor(handle)]).expect("pause(gpuScalar)");
532            match result {
533                Value::Tensor(t) => assert_eq!(t.data.len(), 0),
534                other => panic!("expected empty tensor, got {other:?}"),
535            }
536        });
537    }
538
539    #[test]
540    #[cfg(feature = "wgpu")]
541    fn pause_wgpu_duration_gathered() {
542        let _guard = TEST_GUARD.lock().unwrap();
543        reset_state(true);
544        if wgpu_provider::register_wgpu_provider(wgpu_provider::WgpuProviderOptions::default())
545            .is_err()
546        {
547            return;
548        }
549        let provider = runmat_accelerate_api::provider().expect("wgpu provider");
550        let tensor = Tensor::new(vec![0.0], vec![1, 1]).unwrap();
551        let view = HostTensorView {
552            data: &tensor.data,
553            shape: &tensor.shape,
554        };
555        let handle = provider.upload(&view).expect("upload");
556        let result = pause_builtin(vec![Value::GpuTensor(handle)]).expect("pause(gpuScalar)");
557        match result {
558            Value::Tensor(t) => assert_eq!(t.data.len(), 0),
559            other => panic!("expected empty tensor, got {other:?}"),
560        }
561    }
562
563    #[test]
564    fn invalid_command_raises_error() {
565        let _guard = TEST_GUARD.lock().unwrap();
566        reset_state(true);
567        let err = pause_builtin(vec![Value::from("invalid")]).unwrap_err();
568        assert_eq!(err, ERR_INVALID_ARG);
569    }
570
571    #[test]
572    #[cfg(feature = "doc_export")]
573    fn doc_examples_present() {
574        let _guard = TEST_GUARD.lock().unwrap();
575        let blocks = test_support::doc_examples(DOC_MD);
576        assert!(!blocks.is_empty());
577    }
578}