1use 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#[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 let handle = plotting::current_figure_handle();
273 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 #[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 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 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 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}