1use 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#[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 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 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}