1use runmat_builtins::{
10 BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
11 BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
12 BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
13};
14use runmat_types::MemberAccess;
15use runmat_value::NumericScalar;
16use std::cell::{Cell, RefCell};
17use std::collections::HashMap;
18#[cfg(not(target_arch = "wasm32"))]
19use std::time::Duration;
20
21#[cfg(test)]
22use once_cell::sync::Lazy;
23#[cfg(test)]
24use std::sync::Mutex;
25
26use runmat_builtins::{
27 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
28 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
29};
30use runmat_macros::runtime_builtin;
31use runmat_value::{CellArray, HandleRef, IntValue, ObjectInstance, StructValue, Value};
32
33use crate::builtins::common::gpu_helpers::gather_value_async;
34use crate::builtins::common::spec::{
35 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
36 ReductionNaN, ResidencyPolicy, ShapeRequirements,
37};
38use crate::builtins::common::tensor;
39use crate::{build_runtime_error, BuiltinResult, RuntimeError};
40
41const TIMER_CLASS: &str = "timer";
42const BUILTIN_TIMER: &str = "timer";
43const TIMER_METHOD_START: &str = "__runmat_timer_start";
44const TIMER_METHOD_STARTAT: &str = "__runmat_timer_startat";
45const TIMER_METHOD_STOP: &str = "__runmat_timer_stop";
46const TIMER_METHOD_WAIT: &str = "__runmat_timer_wait";
47const TIMER_METHOD_DELETE: &str = "__runmat_timer_delete";
48
49const CALLBACK_PROPS: [&str; 4] = ["TimerFcn", "StartFcn", "StopFcn", "ErrorFcn"];
50const STRING_PROPS: [&str; 5] = [
51 "BusyMode",
52 "ExecutionMode",
53 "Name",
54 "Tag",
55 "ObjectVisibility",
56];
57const NUMERIC_PROPS: [&str; 3] = ["Period", "StartDelay", "TasksToExecute"];
58const READONLY_PROPS: [&str; 5] = [
59 "AveragePeriod",
60 "InstantPeriod",
61 "Running",
62 "TasksExecuted",
63 "Type",
64];
65
66thread_local! {
67 static TIMER_REGISTRY: RefCell<Vec<HandleRef>> = const { RefCell::new(Vec::new()) };
68 static TIMER_COUNTER: Cell<usize> = const { Cell::new(0) };
69}
70
71#[cfg(test)]
72static TIMER_TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
73
74const TIMER_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
75 name: "t",
76 ty: BuiltinParamType::Any,
77 arity: BuiltinParamArity::Required,
78 default: None,
79 description: "Timer handle object.",
80}];
81
82const TIMER_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
83 name: "Name,Value",
84 ty: BuiltinParamType::Any,
85 arity: BuiltinParamArity::Variadic,
86 default: None,
87 description: "Timer properties such as TimerFcn, StartDelay, Period, ExecutionMode, Name, Tag, and UserData.",
88}];
89
90const TIMER_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
91 BuiltinSignatureDescriptor {
92 label: "t = timer",
93 inputs: &[],
94 outputs: &TIMER_OUTPUT,
95 },
96 BuiltinSignatureDescriptor {
97 label: "t = timer(Name, Value, ...)",
98 inputs: &TIMER_INPUTS,
99 outputs: &TIMER_OUTPUT,
100 },
101];
102
103const TIMERFIND_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
104 name: "timers",
105 ty: BuiltinParamType::Any,
106 arity: BuiltinParamArity::Required,
107 default: None,
108 description: "Matching timer handle, empty cell row, or cell row of timer handles.",
109}];
110
111const TIMERFIND_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
112 name: "Name,Value",
113 ty: BuiltinParamType::Any,
114 arity: BuiltinParamArity::Variadic,
115 default: None,
116 description: "Property filters matched by exact value.",
117}];
118
119const TIMERFIND_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
120 BuiltinSignatureDescriptor {
121 label: "timers = timerfind",
122 inputs: &[],
123 outputs: &TIMERFIND_OUTPUT,
124 },
125 BuiltinSignatureDescriptor {
126 label: "timers = timerfind(Name, Value, ...)",
127 inputs: &TIMERFIND_INPUTS,
128 outputs: &TIMERFIND_OUTPUT,
129 },
130];
131
132const TIMERFINDALL_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
133 BuiltinSignatureDescriptor {
134 label: "timers = timerfindall",
135 inputs: &[],
136 outputs: &TIMERFIND_OUTPUT,
137 },
138 BuiltinSignatureDescriptor {
139 label: "timers = timerfindall(Name, Value, ...)",
140 inputs: &TIMERFIND_INPUTS,
141 outputs: &TIMERFIND_OUTPUT,
142 },
143];
144
145const TIMER_METHOD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
146 name: "status",
147 ty: BuiltinParamType::NumericScalar,
148 arity: BuiltinParamArity::Optional,
149 default: None,
150 description: "Zero on success.",
151}];
152
153const TIMER_METHOD_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
154 name: "t",
155 ty: BuiltinParamType::Any,
156 arity: BuiltinParamArity::Required,
157 default: None,
158 description: "Timer handle or cell array of timer handles.",
159}];
160
161const TIMER_METHOD_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
162 label: "status = method(t)",
163 inputs: &TIMER_METHOD_INPUTS,
164 outputs: &TIMER_METHOD_OUTPUT,
165}];
166
167const TIMER_SETTER_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
168 name: "obj",
169 ty: BuiltinParamType::Any,
170 arity: BuiltinParamArity::Required,
171 default: None,
172 description: "Updated timer object.",
173}];
174
175const TIMER_SETTER_INPUTS: [BuiltinParamDescriptor; 2] = [
176 BuiltinParamDescriptor {
177 name: "obj",
178 ty: BuiltinParamType::Any,
179 arity: BuiltinParamArity::Required,
180 default: None,
181 description: "Timer object receiver.",
182 },
183 BuiltinParamDescriptor {
184 name: "value",
185 ty: BuiltinParamType::Any,
186 arity: BuiltinParamArity::Required,
187 default: None,
188 description: "Property value to validate and assign.",
189 },
190];
191
192const TIMER_SETTER_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
193 label: "obj = set.Property(obj, value)",
194 inputs: &TIMER_SETTER_INPUTS,
195 outputs: &TIMER_SETTER_OUTPUT,
196}];
197
198const TIMER_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
199 code: "RM.TIMER.INVALID_INPUT",
200 identifier: Some("RunMat:timer:InvalidInput"),
201 when: "A timer argument, property, or callback value has an unsupported type.",
202 message: "timer: invalid input",
203};
204
205const TIMER_ERROR_INVALID_PROPERTY: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
206 code: "RM.TIMER.INVALID_PROPERTY",
207 identifier: Some("RunMat:timer:InvalidProperty"),
208 when: "A timer property name is unknown, read-only, or has an invalid value.",
209 message: "timer: invalid property",
210};
211
212const TIMER_ERROR_INVALID_HANDLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
213 code: "RM.TIMER.INVALID_HANDLE",
214 identifier: Some("RunMat:timer:InvalidHandle"),
215 when: "A lifecycle method receives a non-timer or invalid timer handle.",
216 message: "timer: invalid timer handle",
217};
218
219const TIMER_ERROR_CALLBACK: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
220 code: "RM.TIMER.CALLBACK",
221 identifier: Some("RunMat:timer:CallbackFailed"),
222 when: "A timer callback fails.",
223 message: "timer: callback failed",
224};
225
226const TIMER_ERROR_GC: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
227 code: "RM.TIMER.GC",
228 identifier: Some("RunMat:timer:GcFailure"),
229 when: "A timer object cannot be allocated, rooted, read, or mutated.",
230 message: "timer: object storage failed",
231};
232
233const TIMER_ERRORS: [BuiltinErrorDescriptor; 5] = [
234 TIMER_ERROR_INVALID_INPUT,
235 TIMER_ERROR_INVALID_PROPERTY,
236 TIMER_ERROR_INVALID_HANDLE,
237 TIMER_ERROR_CALLBACK,
238 TIMER_ERROR_GC,
239];
240
241const TIMER_EXPLICIT_GPU_NUMERIC_PROPERTY_EXTENSION: runmat_builtins::BuiltinExtensionDescriptor =
242 runmat_builtins::BuiltinExtensionDescriptor {
243 id: "timer-explicit-gpu-numeric-property",
244 mode: runmat_builtins::BuiltinExtensionMode::RunMatOnly,
245 description: "timer with an explicitly GPU-resident numeric property is a RunMat extension",
246 error_identifier: Some("RunMat:compatibility:TimerExplicitGpuNumericPropertyExtension"),
247 };
248pub const TIMER_EXTENSIONS: [runmat_builtins::BuiltinExtensionDescriptor; 1] =
249 [TIMER_EXPLICIT_GPU_NUMERIC_PROPERTY_EXTENSION];
250const TIMERFIND_EXPLICIT_GPU_NUMERIC_FILTER_EXTENSION: runmat_builtins::BuiltinExtensionDescriptor =
251 runmat_builtins::BuiltinExtensionDescriptor {
252 id: "timerfind-explicit-gpu-numeric-filter",
253 mode: runmat_builtins::BuiltinExtensionMode::RunMatOnly,
254 description: "timerfind or timerfindall with an explicitly GPU-resident numeric filter is a RunMat extension",
255 error_identifier: Some("RunMat:compatibility:TimerfindExplicitGpuNumericFilterExtension"),
256 };
257pub const TIMERFIND_EXTENSIONS: [runmat_builtins::BuiltinExtensionDescriptor; 1] =
258 [TIMERFIND_EXPLICIT_GPU_NUMERIC_FILTER_EXTENSION];
259
260const TIMER_INTEGER_PROPERTY_INPUT: [BuiltinIntegerInputCapability; 1] =
261 [BuiltinIntegerInputCapability {
262 name: "Period, StartDelay, or TasksToExecute",
263 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
264 availability: BuiltinIntegerInputAvailability::Documented,
265 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
266 notes: "Numeric timer properties accept native integer scalars. TasksToExecute remains an exact structural integer; duration properties cross one checked seconds boundary.",
267 }];
268const TIMER_INTEGER_USER_DATA_INPUT: [BuiltinIntegerInputCapability; 1] =
269 [BuiltinIntegerInputCapability {
270 name: "UserData",
271 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
272 availability: BuiltinIntegerInputAvailability::Documented,
273 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
274 notes: "UserData accepts arbitrary values and preserves native integer class, shape, and payload without numeric conversion.",
275 }];
276pub const TIMER_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 2] = [
277 BuiltinIntegerCapabilityDescriptor {
278 form: "t = timer(..., numeric_property, integer_value, ...)",
279 inputs: &TIMER_INTEGER_PROPERTY_INPUT,
280 computation_domain: BuiltinIntegerComputationDomain::Structural,
281 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
282 overflow: BuiltinIntegerOverflowRule::Error,
283 backend: BuiltinIntegerBackendRule::GatherFallback,
284 overload: BuiltinIntegerOverloadKind::StructuralParameter,
285 notes: "TasksToExecute is decoded directly from authoritative integer storage. Period and StartDelay require an exactly representable binary64 seconds value.",
286 },
287 BuiltinIntegerCapabilityDescriptor {
288 form: "t = timer(..., \"UserData\", integer_value, ...)",
289 inputs: &TIMER_INTEGER_USER_DATA_INPUT,
290 computation_domain: BuiltinIntegerComputationDomain::Structural,
291 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
292 overflow: BuiltinIntegerOverflowRule::Error,
293 backend: BuiltinIntegerBackendRule::FunctionSpecific,
294 overload: BuiltinIntegerOverloadKind::StructuralParameter,
295 notes: "The timer object preserves the original integer class and value.",
296 },
297];
298
299const TIMERFIND_INTEGER_FILTER_INPUT: [BuiltinIntegerInputCapability; 1] =
300 [BuiltinIntegerInputCapability {
301 name: "property value",
302 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
303 availability: BuiltinIntegerInputAvailability::Documented,
304 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
305 notes: "Property filters compare integer values exactly, including signed values and values wider than binary64's exact integer range.",
306 }];
307pub const TIMERFIND_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
308 [BuiltinIntegerCapabilityDescriptor {
309 form: "timers = timerfind[all](..., property, integer_value, ...)",
310 inputs: &TIMERFIND_INTEGER_FILTER_INPUT,
311 computation_domain: BuiltinIntegerComputationDomain::Structural,
312 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
313 overflow: BuiltinIntegerOverflowRule::Error,
314 backend: BuiltinIntegerBackendRule::GatherFallback,
315 overload: BuiltinIntegerOverloadKind::StructuralParameter,
316 notes: "Native integer filters are normalized to an exact scalar representation and never routed through f64.",
317 }];
318
319pub const TIMER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
320 signatures: &TIMER_SIGNATURES,
321 output_mode: BuiltinOutputMode::Fixed,
322 completion_policy: BuiltinCompletionPolicy::Public,
323 errors: &TIMER_ERRORS,
324};
325
326pub const TIMERFIND_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
327 signatures: &TIMERFIND_SIGNATURES,
328 output_mode: BuiltinOutputMode::Fixed,
329 completion_policy: BuiltinCompletionPolicy::Public,
330 errors: &TIMER_ERRORS,
331};
332
333pub const TIMERFINDALL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
334 signatures: &TIMERFINDALL_SIGNATURES,
335 output_mode: BuiltinOutputMode::Fixed,
336 completion_policy: BuiltinCompletionPolicy::Public,
337 errors: &TIMER_ERRORS,
338};
339
340pub const TIMER_METHOD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
341 signatures: &TIMER_METHOD_SIGNATURES,
342 output_mode: BuiltinOutputMode::Fixed,
343 completion_policy: BuiltinCompletionPolicy::HiddenInternal,
344 errors: &TIMER_ERRORS,
345};
346
347pub const TIMER_SETTER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
348 signatures: &TIMER_SETTER_SIGNATURES,
349 output_mode: BuiltinOutputMode::Fixed,
350 completion_policy: BuiltinCompletionPolicy::MethodOnly,
351 errors: &TIMER_ERRORS,
352};
353
354#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::timer")]
355pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
356 name: "timer",
357 op_kind: GpuOpKind::Custom("timer"),
358 supported_precisions: &[],
359 broadcast: BroadcastSemantics::None,
360 provider_hooks: &[],
361 constant_strategy: ConstantStrategy::InlineLiteral,
362 residency: ResidencyPolicy::GatherImmediately,
363 nan_mode: ReductionNaN::Include,
364 two_pass_threshold: None,
365 workgroup_size: None,
366 accepts_nan_mode: false,
367 notes: "Timer handles are host control-flow objects. Timer callbacks may call GPU-capable builtins, but timer itself has no provider kernel.",
368};
369
370#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::timer")]
371pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
372 name: "timer",
373 shape: ShapeRequirements::Any,
374 constant_strategy: ConstantStrategy::InlineLiteral,
375 elementwise: None,
376 reduction: None,
377 emits_nan: false,
378 notes: "Timer object creation, discovery, and callback execution are side-effect boundaries and are excluded from fusion.",
379};
380
381#[runtime_builtin(
382 name = "timer",
383 category = "timing",
384 summary = "Create a timer handle object for scheduled callbacks.",
385 keywords = "timer,callback,start,stop,wait,timerfind",
386 descriptor(crate::builtins::timing::timer::TIMER_DESCRIPTOR),
387 extensions(crate::builtins::timing::timer::TIMER_EXTENSIONS),
388 integer_capabilities(crate::builtins::timing::timer::TIMER_INTEGER_CAPABILITIES),
389 builtin_path = "crate::builtins::timing::timer"
390)]
391pub async fn timer_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
392 ensure_timer_class_registered();
393 let args = prepare_timer_name_value_args(&args, false).await?;
394 let mut object = default_timer_object(next_timer_name());
395 apply_name_value_pairs(&mut object, &args, true)?;
396 let target = runmat_gc::gc_allocate(Value::Object(object))
397 .map_err(|err| timer_error(&TIMER_ERROR_GC, format!("timer: {err}")))?;
398 runmat_gc::gc_add_root(target)
399 .map_err(|err| timer_error(&TIMER_ERROR_GC, format!("timer: {err}")))?;
400 let handle = HandleRef {
401 class_name: TIMER_CLASS.to_string(),
402 target,
403 valid: true,
404 };
405 TIMER_REGISTRY.with(|registry| registry.borrow_mut().push(handle.clone()));
406 Ok(Value::HandleObject(handle))
407}
408
409#[runtime_builtin(
410 name = "timerfind",
411 category = "timing",
412 summary = "Find visible timer objects by property value.",
413 keywords = "timerfind,timer,callback,handle",
414 descriptor(crate::builtins::timing::timer::TIMERFIND_DESCRIPTOR),
415 extensions(crate::builtins::timing::timer::TIMERFIND_EXTENSIONS),
416 integer_capabilities(crate::builtins::timing::timer::TIMERFIND_INTEGER_CAPABILITIES),
417 builtin_path = "crate::builtins::timing::timer"
418)]
419pub async fn timerfind_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
420 find_timers(prepare_timer_name_value_args(&args, true).await?, false)
421}
422
423#[runtime_builtin(
424 name = "timerfindall",
425 category = "timing",
426 summary = "Find all timer objects by property value.",
427 keywords = "timerfindall,timer,callback,handle",
428 descriptor(crate::builtins::timing::timer::TIMERFINDALL_DESCRIPTOR),
429 extensions(crate::builtins::timing::timer::TIMERFIND_EXTENSIONS),
430 integer_capabilities(crate::builtins::timing::timer::TIMERFIND_INTEGER_CAPABILITIES),
431 builtin_path = "crate::builtins::timing::timer"
432)]
433pub async fn timerfindall_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
434 find_timers(prepare_timer_name_value_args(&args, true).await?, true)
435}
436
437#[runtime_builtin(
438 name = "__runmat_timer_start",
439 category = "timing",
440 summary = "Start a timer object.",
441 keywords = "timer,start",
442 sink = true,
443 suppress_auto_output = true,
444 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
445 builtin_path = "crate::builtins::timing::timer"
446)]
447pub async fn timer_start_builtin(value: Value) -> BuiltinResult<Value> {
448 start_timer_values(&value, None).await?;
449 Ok(Value::Num(0.0))
450}
451
452#[runtime_builtin(
453 name = "__runmat_timer_startat",
454 category = "timing",
455 summary = "Start a timer object at a requested time.",
456 keywords = "timer,startat",
457 sink = true,
458 suppress_auto_output = true,
459 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
460 builtin_path = "crate::builtins::timing::timer"
461)]
462pub async fn timer_startat_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
463 let prepared = match rest.first() {
464 Some(delay) => {
465 prepare_timer_name_value_args(
466 &[Value::String("StartDelay".to_string()), delay.clone()],
467 false,
468 )
469 .await?
470 }
471 None => Vec::new(),
472 };
473 let delay = prepared
474 .get(1)
475 .map(|value| numeric_scalar(value, "startat"))
476 .transpose()?
477 .filter(|seconds| *seconds > 0.0);
478 start_timer_values(&value, delay).await?;
479 Ok(Value::Num(0.0))
480}
481
482#[runtime_builtin(
483 name = "__runmat_timer_stop",
484 category = "timing",
485 summary = "Stop a timer object.",
486 keywords = "timer,stop",
487 sink = true,
488 suppress_auto_output = true,
489 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
490 builtin_path = "crate::builtins::timing::timer"
491)]
492pub async fn timer_stop_builtin(value: Value) -> BuiltinResult<Value> {
493 stop_timer_values(&value, true).await?;
494 Ok(Value::Num(0.0))
495}
496
497#[runtime_builtin(
498 name = "__runmat_timer_wait",
499 category = "timing",
500 summary = "Wait until timer objects finish.",
501 keywords = "timer,wait",
502 sink = true,
503 suppress_auto_output = true,
504 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
505 builtin_path = "crate::builtins::timing::timer"
506)]
507pub async fn timer_wait_builtin(value: Value) -> BuiltinResult<Value> {
508 wait_timer_values(&value).await?;
509 Ok(Value::Num(0.0))
510}
511
512#[runtime_builtin(
513 name = "__runmat_timer_delete",
514 category = "timing",
515 summary = "Delete a timer object.",
516 keywords = "timer,delete",
517 sink = true,
518 suppress_auto_output = true,
519 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
520 builtin_path = "crate::builtins::timing::timer"
521)]
522pub async fn timer_delete_builtin(value: Value) -> BuiltinResult<Value> {
523 delete_timer_values(&value).await?;
524 Ok(Value::Num(0.0))
525}
526
527macro_rules! timer_setter_builtin {
528 ($fn_name:ident, $builtin_name:literal, $property:literal) => {
529 #[runtime_builtin(
530 name = $builtin_name,
531 category = "timing",
532 summary = "Validate and assign a timer property.",
533 keywords = "timer,set,property",
534 descriptor(crate::builtins::timing::timer::TIMER_SETTER_DESCRIPTOR),
535 builtin_path = "crate::builtins::timing::timer"
536 )]
537 pub async fn $fn_name(obj: Value, value: Value) -> BuiltinResult<Value> {
538 set_timer_property_object(obj, $property, value).await
539 }
540 };
541}
542
543timer_setter_builtin!(timer_set_timer_fcn_builtin, "set.TimerFcn", "TimerFcn");
544timer_setter_builtin!(timer_set_start_fcn_builtin, "set.StartFcn", "StartFcn");
545timer_setter_builtin!(timer_set_stop_fcn_builtin, "set.StopFcn", "StopFcn");
546timer_setter_builtin!(timer_set_error_fcn_builtin, "set.ErrorFcn", "ErrorFcn");
547timer_setter_builtin!(timer_set_period_builtin, "set.Period", "Period");
548timer_setter_builtin!(
549 timer_set_start_delay_builtin,
550 "set.StartDelay",
551 "StartDelay"
552);
553timer_setter_builtin!(
554 timer_set_tasks_to_execute_builtin,
555 "set.TasksToExecute",
556 "TasksToExecute"
557);
558timer_setter_builtin!(timer_set_busy_mode_builtin, "set.BusyMode", "BusyMode");
559timer_setter_builtin!(
560 timer_set_execution_mode_builtin,
561 "set.ExecutionMode",
562 "ExecutionMode"
563);
564timer_setter_builtin!(timer_set_name_builtin, "set.Name", "Name");
565timer_setter_builtin!(timer_set_tag_builtin, "set.Tag", "Tag");
566timer_setter_builtin!(
567 timer_set_object_visibility_builtin,
568 "set.ObjectVisibility",
569 "ObjectVisibility"
570);
571timer_setter_builtin!(timer_set_user_data_builtin, "set.UserData", "UserData");
572
573fn ensure_timer_class_registered() {
574 if crate::class_registry::get_class(TIMER_CLASS).is_some() {
575 return;
576 }
577
578 let mut methods = HashMap::new();
579 for (name, function_name) in [
580 ("start", TIMER_METHOD_START),
581 ("startat", TIMER_METHOD_STARTAT),
582 ("stop", TIMER_METHOD_STOP),
583 ("wait", TIMER_METHOD_WAIT),
584 ("delete", TIMER_METHOD_DELETE),
585 ] {
586 methods.insert(
587 name.to_string(),
588 crate::class_registry::RuntimeMethod {
589 name: name.to_string(),
590 is_static: false,
591 is_abstract: false,
592 is_sealed: false,
593 access: MemberAccess::Public,
594 function_name: function_name.to_string(),
595 implicit_class_argument: None,
596 },
597 );
598 }
599
600 let mut properties = HashMap::new();
601 for name in CALLBACK_PROPS
602 .into_iter()
603 .chain(STRING_PROPS)
604 .chain(NUMERIC_PROPS)
605 .chain(READONLY_PROPS)
606 .chain(["UserData"])
607 {
608 properties.insert(
609 name.to_string(),
610 crate::class_registry::RuntimeProperty {
611 name: name.to_string(),
612 is_static: false,
613 is_constant: false,
614 is_dependent: is_timer_mutable_property(name),
615 get_access: MemberAccess::Public,
616 set_access: if READONLY_PROPS.contains(&name) {
617 MemberAccess::Private
618 } else {
619 MemberAccess::Public
620 },
621 default_value: None,
622 },
623 );
624 }
625
626 crate::class_registry::register_class(crate::class_registry::RuntimeClass {
627 name: TIMER_CLASS.to_string(),
628 parent: Some("handle".to_string()),
629 properties,
630 methods,
631 });
632}
633
634fn is_timer_mutable_property(name: &str) -> bool {
635 CALLBACK_PROPS.contains(&name)
636 || STRING_PROPS.contains(&name)
637 || NUMERIC_PROPS.contains(&name)
638 || name == "UserData"
639}
640
641fn default_timer_object(name: String) -> ObjectInstance {
642 let mut object = ObjectInstance::new(TIMER_CLASS.to_string());
643 for prop in CALLBACK_PROPS {
644 object
645 .properties
646 .insert(prop.to_string(), Value::String(String::new()));
647 }
648 object
649 .properties
650 .insert("Period".to_string(), Value::Num(1.0));
651 object
652 .properties
653 .insert("StartDelay".to_string(), Value::Num(0.0));
654 object
655 .properties
656 .insert("TasksToExecute".to_string(), Value::Num(1.0));
657 object
658 .properties
659 .insert("BusyMode".to_string(), Value::String("drop".to_string()));
660 object.properties.insert(
661 "ExecutionMode".to_string(),
662 Value::String("singleShot".to_string()),
663 );
664 object
665 .properties
666 .insert("Name".to_string(), Value::String(name));
667 object
668 .properties
669 .insert("Tag".to_string(), Value::String(String::new()));
670 object.properties.insert(
671 "ObjectVisibility".to_string(),
672 Value::String("on".to_string()),
673 );
674 object
675 .properties
676 .insert("UserData".to_string(), Value::Num(0.0));
677 object
678 .properties
679 .insert("AveragePeriod".to_string(), Value::Num(f64::NAN));
680 object
681 .properties
682 .insert("InstantPeriod".to_string(), Value::Num(f64::NAN));
683 object
684 .properties
685 .insert("Running".to_string(), Value::String("off".to_string()));
686 object
687 .properties
688 .insert("TasksExecuted".to_string(), Value::Num(0.0));
689 object
690 .properties
691 .insert("Type".to_string(), Value::String(TIMER_CLASS.to_string()));
692 object.properties.insert(
693 crate::HANDLE_VALID_FLAG_PROPERTY.to_string(),
694 Value::Bool(true),
695 );
696 object
697}
698
699fn next_timer_name() -> String {
700 TIMER_COUNTER.with(|counter| {
701 let next = counter.get() + 1;
702 counter.set(next);
703 format!("timer-{next}")
704 })
705}
706
707fn apply_name_value_pairs(
708 object: &mut ObjectInstance,
709 args: &[Value],
710 constructor: bool,
711) -> BuiltinResult<()> {
712 if !args.len().is_multiple_of(2) {
713 return Err(timer_error(
714 &TIMER_ERROR_INVALID_INPUT,
715 "timer: name-value arguments must appear in pairs",
716 ));
717 }
718 let mut idx = 0usize;
719 while idx < args.len() {
720 let name = canonical_property_name(&value_to_string(&args[idx])?)?;
721 if READONLY_PROPS.contains(&name.as_str()) {
722 return Err(timer_error(
723 &TIMER_ERROR_INVALID_PROPERTY,
724 format!("timer: property '{name}' is read-only"),
725 ));
726 }
727 let value = normalize_property_value(&name, args[idx + 1].clone())?;
728 if !constructor && running_is_on(object) && is_running_readonly_property(&name) {
729 return Err(timer_error(
730 &TIMER_ERROR_INVALID_PROPERTY,
731 format!("timer: property '{name}' cannot be changed while Running is on"),
732 ));
733 }
734 object.properties.insert(name, value);
735 idx += 2;
736 }
737 Ok(())
738}
739
740async fn set_timer_property_object(
741 obj: Value,
742 property_name: &'static str,
743 value: Value,
744) -> BuiltinResult<Value> {
745 let Value::Object(mut object) = obj else {
746 return Err(timer_error(
747 &TIMER_ERROR_INVALID_HANDLE,
748 format!("timer: set.{property_name} requires timer object receiver"),
749 ));
750 };
751 if object.class_name != TIMER_CLASS {
752 return Err(timer_error(
753 &TIMER_ERROR_INVALID_HANDLE,
754 format!(
755 "timer: set.{property_name} requires timer object receiver, got '{}'",
756 object.class_name
757 ),
758 ));
759 }
760 let args =
761 prepare_timer_name_value_args(&[Value::String(property_name.to_string()), value], false)
762 .await?;
763 apply_name_value_pairs(&mut object, &args, false)?;
764 Ok(Value::Object(object))
765}
766
767async fn prepare_timer_name_value_args(args: &[Value], finding: bool) -> BuiltinResult<Vec<Value>> {
768 if !args.len().is_multiple_of(2) {
769 let name = if finding { "timerfind" } else { "timer" };
770 return Err(timer_error(
771 &TIMER_ERROR_INVALID_INPUT,
772 format!("{name}: property filters must appear in name-value pairs"),
773 ));
774 }
775 let mut prepared = Vec::with_capacity(args.len());
776 for pair in args.chunks_exact(2) {
777 let property = canonical_property_name(&value_to_string(&pair[0])?)?;
778 prepared.push(pair[0].clone());
779 if NUMERIC_PROPS.contains(&property.as_str()) {
780 if crate::builtins::common::validation::value_contains_explicit_gpu(&pair[1]) {
781 let extension = if finding {
782 &TIMERFIND_EXPLICIT_GPU_NUMERIC_FILTER_EXTENSION
783 } else {
784 &TIMER_EXPLICIT_GPU_NUMERIC_PROPERTY_EXTENSION
785 };
786 crate::compatibility::ensure_builtin_extension_enabled(
787 extension,
788 if finding { "timerfind" } else { "timer" },
789 )?;
790 }
791 prepared.push(
792 gather_value_async(&pair[1])
793 .await
794 .map_err(|error| timer_error(&TIMER_ERROR_INVALID_INPUT, error.message()))?,
795 );
796 } else {
797 prepared.push(pair[1].clone());
798 }
799 }
800 Ok(prepared)
801}
802
803fn canonical_property_name(name: &str) -> BuiltinResult<String> {
804 let trimmed = name.trim();
805 for prop in CALLBACK_PROPS
806 .into_iter()
807 .chain(STRING_PROPS)
808 .chain(NUMERIC_PROPS)
809 .chain(READONLY_PROPS)
810 .chain(["UserData"])
811 {
812 if prop.eq_ignore_ascii_case(trimmed) {
813 return Ok(prop.to_string());
814 }
815 }
816 Err(timer_error(
817 &TIMER_ERROR_INVALID_PROPERTY,
818 format!("timer: unknown property '{trimmed}'"),
819 ))
820}
821
822fn normalize_property_value(name: &str, value: Value) -> BuiltinResult<Value> {
823 match name {
824 "Period" => {
825 let seconds = numeric_scalar(&value, name)?;
826 if !seconds.is_finite() || seconds <= 0.001 {
827 return Err(timer_error(
828 &TIMER_ERROR_INVALID_PROPERTY,
829 "timer: Period must be a finite scalar greater than 0.001",
830 ));
831 }
832 Ok(Value::Num(seconds))
833 }
834 "StartDelay" => {
835 let seconds = numeric_scalar(&value, name)?;
836 if !seconds.is_finite() || seconds < 0.0 {
837 return Err(timer_error(
838 &TIMER_ERROR_INVALID_PROPERTY,
839 "timer: StartDelay must be a finite non-negative scalar",
840 ));
841 }
842 Ok(Value::Num(seconds))
843 }
844 "TasksToExecute" => {
845 parse_tasks_to_execute_value(&value)?;
846 if let Some(integer) = tensor::scalar_integer_value(&value) {
847 Ok(Value::Int(integer))
848 } else {
849 Ok(Value::Num(numeric_scalar(&value, name)?.round()))
850 }
851 }
852 "BusyMode" => match_string_choice(&value, name, &["drop", "error", "queue"]),
853 "ExecutionMode" => match_string_choice(
854 &value,
855 name,
856 &["singleShot", "fixedRate", "fixedDelay", "fixedSpacing"],
857 ),
858 "ObjectVisibility" => match_string_choice(&value, name, &["on", "off"]),
859 "Name" | "Tag" => Ok(Value::String(value_to_string(&value)?)),
860 "TimerFcn" | "StartFcn" | "StopFcn" | "ErrorFcn" => normalize_callback(value, name),
861 "UserData" => Ok(value),
862 _ => Err(timer_error(
863 &TIMER_ERROR_INVALID_PROPERTY,
864 format!("timer: property '{name}' cannot be assigned"),
865 )),
866 }
867}
868
869fn is_running_readonly_property(name: &str) -> bool {
870 matches!(name, "BusyMode" | "ExecutionMode" | "StartDelay")
871}
872
873fn normalize_callback(value: Value, name: &str) -> BuiltinResult<Value> {
874 match value {
875 Value::String(_)
876 | Value::CharArray(_)
877 | Value::FunctionHandle(_)
878 | Value::ExternalFunctionHandle(_)
879 | Value::MethodFunctionHandle(_)
880 | Value::BoundFunctionHandle { .. }
881 | Value::Closure(_)
882 | Value::Cell(_) => Ok(value),
883 other => Err(timer_error(
884 &TIMER_ERROR_INVALID_PROPERTY,
885 format!("timer: {name} must be text, a function handle, or a callback cell array, got {other:?}"),
886 )),
887 }
888}
889
890fn match_string_choice(value: &Value, name: &str, choices: &[&str]) -> BuiltinResult<Value> {
891 let text = value_to_string(value)?;
892 choices
893 .iter()
894 .find(|choice| choice.eq_ignore_ascii_case(text.trim()))
895 .map(|choice| Value::String((*choice).to_string()))
896 .ok_or_else(|| {
897 timer_error(
898 &TIMER_ERROR_INVALID_PROPERTY,
899 format!("timer: invalid {name} value '{text}'"),
900 )
901 })
902}
903
904fn value_to_string(value: &Value) -> BuiltinResult<String> {
905 match value {
906 Value::String(text) => Ok(text.clone()),
907 Value::CharArray(chars) if chars.rows == 1 => Ok(chars.data.iter().collect()),
908 other => Err(timer_error(
909 &TIMER_ERROR_INVALID_INPUT,
910 format!("timer: expected string scalar or character row, got {other:?}"),
911 )),
912 }
913}
914
915fn numeric_scalar(value: &Value, name: &str) -> BuiltinResult<f64> {
916 if let Some(integer) = tensor::scalar_integer_value(value) {
917 let exact = match integer {
918 IntValue::I8(value) => i128::from(value),
919 IntValue::I16(value) => i128::from(value),
920 IntValue::I32(value) => i128::from(value),
921 IntValue::I64(value) => i128::from(value),
922 IntValue::U8(value) => i128::from(value),
923 IntValue::U16(value) => i128::from(value),
924 IntValue::U32(value) => i128::from(value),
925 IntValue::U64(value) => i128::from(value),
926 };
927 const MAX_EXACT_INTEGER: i128 = 1_i128 << 53;
928 if !(-MAX_EXACT_INTEGER..=MAX_EXACT_INTEGER).contains(&exact) {
929 return Err(timer_error(
930 &TIMER_ERROR_INVALID_PROPERTY,
931 format!("timer: {name} must be exactly representable as double seconds"),
932 ));
933 }
934 return Ok(exact as f64);
935 }
936 match value {
937 Value::Num(value) => Ok(*value),
938 Value::Tensor(value) if tensor::is_scalar_tensor(value) => {
939 Ok(tensor::tensor_value_f64(value, 0))
940 }
941 other => Err(timer_error(
942 &TIMER_ERROR_INVALID_INPUT,
943 format!("timer: {name} must be a numeric scalar, got {other:?}"),
944 )),
945 }
946}
947
948fn parse_tasks_to_execute_value(value: &Value) -> BuiltinResult<usize> {
949 if let Some(integer) = tensor::scalar_integer_value(value) {
950 return parse_tasks_to_execute_integer(&integer);
951 }
952
953 let count = numeric_scalar(value, "TasksToExecute")?;
954 if !count.is_finite() || count < 1.0 || count.fract() != 0.0 {
955 return Err(timer_error(
956 &TIMER_ERROR_INVALID_PROPERTY,
957 "timer: TasksToExecute must be a positive integer scalar",
958 ));
959 }
960 if !fits_platform_usize(count) {
961 return Err(timer_error(
962 &TIMER_ERROR_INVALID_PROPERTY,
963 "timer: TasksToExecute exceeds platform limits",
964 ));
965 }
966 Ok(count as usize)
967}
968
969fn parse_tasks_to_execute_integer(value: &IntValue) -> BuiltinResult<usize> {
970 let count = value.try_to_usize().ok_or_else(|| {
971 timer_error(
972 &TIMER_ERROR_INVALID_PROPERTY,
973 "timer: TasksToExecute must be a positive integer scalar",
974 )
975 })?;
976 if count == 0 {
977 return Err(timer_error(
978 &TIMER_ERROR_INVALID_PROPERTY,
979 "timer: TasksToExecute must be a positive integer scalar",
980 ));
981 }
982 Ok(count)
983}
984
985fn fits_platform_usize(value: f64) -> bool {
986 value < usize::MAX as f64 || (usize::BITS < 64 && value == usize::MAX as f64)
987}
988
989async fn start_timer_values(value: &Value, override_delay: Option<f64>) -> BuiltinResult<()> {
990 match value {
991 Value::HandleObject(handle) if is_timer_handle(handle) => {
992 start_one_timer(handle.clone(), override_delay).await
993 }
994 Value::Cell(cell) => {
995 for item in &cell.data {
996 Box::pin(start_timer_values(item, override_delay)).await?;
997 }
998 Ok(())
999 }
1000 other => Err(timer_error(
1001 &TIMER_ERROR_INVALID_HANDLE,
1002 format!("timer: expected timer handle, got {other:?}"),
1003 )),
1004 }
1005}
1006
1007async fn stop_timer_values(value: &Value, call_stop: bool) -> BuiltinResult<()> {
1008 match value {
1009 Value::HandleObject(handle) if is_timer_handle(handle) => {
1010 stop_one_timer(handle, call_stop).await
1011 }
1012 Value::Cell(cell) => {
1013 for item in &cell.data {
1014 Box::pin(stop_timer_values(item, call_stop)).await?;
1015 }
1016 Ok(())
1017 }
1018 other => Err(timer_error(
1019 &TIMER_ERROR_INVALID_HANDLE,
1020 format!("timer: expected timer handle, got {other:?}"),
1021 )),
1022 }
1023}
1024
1025async fn wait_timer_values(value: &Value) -> BuiltinResult<()> {
1026 match value {
1027 Value::HandleObject(handle) if is_timer_handle(handle) => {
1028 if running_property(handle)? {
1029 stop_one_timer(handle, true).await?;
1030 }
1031 Ok(())
1032 }
1033 Value::Cell(cell) => {
1034 for item in &cell.data {
1035 Box::pin(wait_timer_values(item)).await?;
1036 }
1037 Ok(())
1038 }
1039 other => Err(timer_error(
1040 &TIMER_ERROR_INVALID_HANDLE,
1041 format!("timer: expected timer handle, got {other:?}"),
1042 )),
1043 }
1044}
1045
1046async fn delete_timer_values(value: &Value) -> BuiltinResult<()> {
1047 match value {
1048 Value::HandleObject(handle) if is_timer_handle(handle) => {
1049 stop_one_timer(handle, false).await?;
1050 crate::set_handle_valid(handle, false);
1051 TIMER_REGISTRY.with(|registry| registry.borrow_mut().retain(|h| h != handle));
1052 let _ = runmat_gc::gc_remove_root(handle.target);
1053 Ok(())
1054 }
1055 Value::Cell(cell) => {
1056 for item in &cell.data {
1057 Box::pin(delete_timer_values(item)).await?;
1058 }
1059 Ok(())
1060 }
1061 other => Err(timer_error(
1062 &TIMER_ERROR_INVALID_HANDLE,
1063 format!("timer: expected timer handle, got {other:?}"),
1064 )),
1065 }
1066}
1067
1068async fn start_one_timer(handle: HandleRef, override_delay: Option<f64>) -> BuiltinResult<()> {
1069 ensure_valid_timer(&handle)?;
1070 let timer_fcn = property(&handle, "TimerFcn")?;
1071 if callback_is_empty(&timer_fcn) {
1072 return Err(timer_error(
1073 &TIMER_ERROR_INVALID_PROPERTY,
1074 "timer: TimerFcn must be set before starting a timer",
1075 ));
1076 }
1077 set_property(&handle, "Running", Value::String("on".to_string()))?;
1078 set_property(&handle, "TasksExecuted", Value::Num(0.0))?;
1079 let start_delay = override_delay.unwrap_or(numeric_property(&handle, "StartDelay")?);
1080 sleep_seconds(start_delay);
1081 if let Err(err) = run_callback(&handle, "StartFcn", "StartFcn").await {
1082 let _ = stop_one_timer(&handle, false).await;
1083 return Err(err);
1084 }
1085
1086 let execution_mode = string_property(&handle, "ExecutionMode")?;
1087 let tasks = if execution_mode.eq_ignore_ascii_case("singleShot") {
1088 1usize
1089 } else {
1090 parse_tasks_to_execute_value(&property(&handle, "TasksToExecute")?)?
1091 };
1092 let period = numeric_property(&handle, "Period")?;
1093 let mut last_fire: Option<runmat_time::Instant> = None;
1094 for idx in 0..tasks {
1095 if idx > 0 {
1096 sleep_seconds(period);
1097 }
1098 let now = runmat_time::Instant::now();
1099 if let Some(last) = last_fire {
1100 let instant_period = now.duration_since(last).as_secs_f64();
1101 set_property(&handle, "InstantPeriod", Value::Num(instant_period))?;
1102 update_average_period(&handle, instant_period, idx)?;
1103 }
1104 last_fire = Some(now);
1105 match run_callback(&handle, "TimerFcn", "TimerFcn").await {
1106 Ok(()) => {
1107 let executed = numeric_property(&handle, "TasksExecuted")? + 1.0;
1108 set_property(&handle, "TasksExecuted", Value::Num(executed))?;
1109 }
1110 Err(err) => {
1111 let _ = run_callback(&handle, "ErrorFcn", "ErrorFcn").await;
1112 let _ = stop_one_timer(&handle, true).await;
1113 return Err(timer_error(&TIMER_ERROR_CALLBACK, err.message()));
1114 }
1115 }
1116 }
1117 stop_one_timer(&handle, true).await
1118}
1119
1120async fn stop_one_timer(handle: &HandleRef, call_stop: bool) -> BuiltinResult<()> {
1121 ensure_valid_timer(handle)?;
1122 let was_running = running_property(handle)?;
1123 set_property(handle, "Running", Value::String("off".to_string()))?;
1124 if call_stop && was_running {
1125 run_callback(handle, "StopFcn", "StopFcn").await?;
1126 }
1127 Ok(())
1128}
1129
1130fn update_average_period(handle: &HandleRef, instant_period: f64, idx: usize) -> BuiltinResult<()> {
1131 let current = numeric_property(handle, "AveragePeriod").unwrap_or(f64::NAN);
1132 let average = if current.is_finite() {
1133 (current * (idx as f64 - 1.0) + instant_period) / idx as f64
1134 } else {
1135 instant_period
1136 };
1137 set_property(handle, "AveragePeriod", Value::Num(average))
1138}
1139
1140async fn run_callback(
1141 handle: &HandleRef,
1142 property_name: &str,
1143 event_type: &str,
1144) -> BuiltinResult<()> {
1145 let callback = property(handle, property_name)?;
1146 if callback_is_empty(&callback) {
1147 return Ok(());
1148 }
1149 match callback {
1150 Value::String(text) if !text.trim().is_empty() => {
1151 return Err(timer_error(
1152 &TIMER_ERROR_CALLBACK,
1153 "timer: text callback execution requires dynamic-eval callback infrastructure; use a function handle callback",
1154 ));
1155 }
1156 Value::CharArray(chars) if !chars.data.is_empty() => {
1157 let text: String = chars.data.iter().collect();
1158 if !text.trim().is_empty() {
1159 return Err(timer_error(
1160 &TIMER_ERROR_CALLBACK,
1161 "timer: text callback execution requires dynamic-eval callback infrastructure; use a function handle callback",
1162 ));
1163 }
1164 }
1165 Value::Cell(cell) => {
1166 let Some(function) = cell.data.first().cloned() else {
1167 return Ok(());
1168 };
1169 let mut args = callback_base_args(handle, event_type);
1170 args.extend(cell.data.iter().skip(1).cloned());
1171 crate::call_feval_async_with_outputs(function, &args, 0).await?;
1172 }
1173 function @ (Value::FunctionHandle(_)
1174 | Value::ExternalFunctionHandle(_)
1175 | Value::MethodFunctionHandle(_)
1176 | Value::BoundFunctionHandle { .. }
1177 | Value::Closure(_)) => {
1178 let args = callback_base_args(handle, event_type);
1179 crate::call_feval_async_with_outputs(function, &args, 0).await?;
1180 }
1181 _ => {}
1182 }
1183 Ok(())
1184}
1185
1186fn callback_base_args(handle: &HandleRef, event_type: &str) -> Vec<Value> {
1187 vec![Value::HandleObject(handle.clone()), timer_event(event_type)]
1188}
1189
1190fn timer_event(event_type: &str) -> Value {
1191 let mut data = StructValue::new();
1192 data.insert(
1193 "time",
1194 Value::Num(runmat_time::duration_since_epoch().as_secs_f64()),
1195 );
1196 let mut event = StructValue::new();
1197 event.insert("Type", Value::String(event_type.to_string()));
1198 event.insert("Data", Value::Struct(data));
1199 Value::Struct(event)
1200}
1201
1202fn callback_is_empty(value: &Value) -> bool {
1203 match value {
1204 Value::String(text) => text.trim().is_empty(),
1205 Value::CharArray(chars) => chars.data.iter().collect::<String>().trim().is_empty(),
1206 Value::Cell(cell) => cell.data.is_empty(),
1207 _ => false,
1208 }
1209}
1210
1211fn sleep_seconds(seconds: f64) {
1212 if seconds <= 0.0 || !seconds.is_finite() {
1213 return;
1214 }
1215 #[cfg(not(target_arch = "wasm32"))]
1216 {
1217 std::thread::sleep(Duration::from_secs_f64(seconds));
1218 }
1219 #[cfg(target_arch = "wasm32")]
1220 {
1221 let _ = seconds;
1222 }
1223}
1224
1225fn find_timers(args: Vec<Value>, include_invisible: bool) -> BuiltinResult<Value> {
1226 ensure_timer_class_registered();
1227 let filters = parse_filters(&args)?;
1228 let mut matches = Vec::new();
1229 TIMER_REGISTRY.with(|registry| {
1230 let mut registry = registry.borrow_mut();
1231 registry.retain(crate::is_handle_valid);
1232 for handle in registry.iter() {
1233 if !include_invisible
1234 && !string_property(handle, "ObjectVisibility")
1235 .map(|value| value.eq_ignore_ascii_case("on"))
1236 .unwrap_or(false)
1237 {
1238 continue;
1239 }
1240 if filters_match(handle, &filters).unwrap_or(false) {
1241 matches.push(Value::HandleObject(handle.clone()));
1242 }
1243 }
1244 });
1245 handles_to_result(matches)
1246}
1247
1248fn parse_filters(args: &[Value]) -> BuiltinResult<Vec<(String, Value)>> {
1249 if !args.len().is_multiple_of(2) {
1250 return Err(timer_error(
1251 &TIMER_ERROR_INVALID_INPUT,
1252 "timerfind: name-value arguments must appear in pairs",
1253 ));
1254 }
1255 let mut out = Vec::new();
1256 let mut idx = 0usize;
1257 while idx < args.len() {
1258 let name = canonical_property_name(&value_to_string(&args[idx])?)?;
1259 out.push((name, normalize_find_value(args[idx + 1].clone())));
1260 idx += 2;
1261 }
1262 Ok(out)
1263}
1264
1265fn normalize_find_value(value: Value) -> Value {
1266 if let Some(integer) = tensor::scalar_integer_value(&value) {
1267 return Value::Int(integer);
1268 }
1269 match value {
1270 Value::CharArray(chars) if chars.rows == 1 => Value::String(chars.data.iter().collect()),
1271 other => other,
1272 }
1273}
1274
1275fn filters_match(handle: &HandleRef, filters: &[(String, Value)]) -> BuiltinResult<bool> {
1276 for (name, expected) in filters {
1277 let actual = normalize_find_value(property(handle, name)?);
1278 if !timer_values_equal(&actual, expected) {
1279 return Ok(false);
1280 }
1281 }
1282 Ok(true)
1283}
1284
1285fn timer_values_equal(lhs: &Value, rhs: &Value) -> bool {
1286 match (lhs, rhs) {
1287 (Value::String(a), Value::String(b)) => a == b,
1288 (Value::Num(a), Value::Num(b)) if a.is_nan() && b.is_nan() => true,
1289 (Value::Num(a), Value::Num(b)) => a == b,
1290 (Value::Int(a), Value::Int(b)) => a == b,
1291 (Value::Int(a), Value::Num(b)) | (Value::Num(b), Value::Int(a)) => {
1292 crate::builtins::logical::rel::integer_comparison::compare_numeric_scalars_exact(
1293 NumericScalar::from(a.clone()),
1294 NumericScalar::F64(*b),
1295 ) == Some(std::cmp::Ordering::Equal)
1296 }
1297 (Value::Bool(a), Value::Bool(b)) => a == b,
1298 _ => lhs == rhs,
1299 }
1300}
1301
1302fn handles_to_result(handles: Vec<Value>) -> BuiltinResult<Value> {
1303 if handles.len() == 1 {
1304 Ok(handles.into_iter().next().unwrap())
1305 } else {
1306 let len = handles.len();
1307 CellArray::new(handles, 1, len)
1308 .map(Value::Cell)
1309 .map_err(|err| timer_error(&TIMER_ERROR_INVALID_INPUT, err))
1310 }
1311}
1312
1313fn property(handle: &HandleRef, name: &str) -> BuiltinResult<Value> {
1314 ensure_valid_timer(handle)?;
1315 runmat_gc::gc_with_value(&handle.target, |target| {
1316 let Value::Object(object) = target else {
1317 return None;
1318 };
1319 object.properties.get(name).cloned()
1320 })
1321 .map_err(|err| timer_error(&TIMER_ERROR_GC, format!("timer: {err}")))?
1322 .ok_or_else(|| {
1323 timer_error(
1324 &TIMER_ERROR_INVALID_PROPERTY,
1325 format!("timer: missing property '{name}'"),
1326 )
1327 })
1328}
1329
1330fn set_property(handle: &HandleRef, name: &str, value: Value) -> BuiltinResult<()> {
1331 ensure_valid_timer(handle)?;
1332 let updated = runmat_gc::gc_with_value_mut(&handle.target, |target| {
1333 let Value::Object(object) = target else {
1334 return false;
1335 };
1336 object.properties.insert(name.to_string(), value);
1337 true
1338 })
1339 .map_err(|err| timer_error(&TIMER_ERROR_GC, format!("timer: {err}")))?;
1340 if updated {
1341 Ok(())
1342 } else {
1343 Err(timer_error(
1344 &TIMER_ERROR_INVALID_HANDLE,
1345 "timer: handle target is not an object",
1346 ))
1347 }
1348}
1349
1350fn string_property(handle: &HandleRef, name: &str) -> BuiltinResult<String> {
1351 match property(handle, name)? {
1352 Value::String(text) => Ok(text),
1353 Value::CharArray(chars) if chars.rows == 1 => Ok(chars.data.iter().collect()),
1354 other => Err(timer_error(
1355 &TIMER_ERROR_INVALID_PROPERTY,
1356 format!("timer: property '{name}' must be text, got {other:?}"),
1357 )),
1358 }
1359}
1360
1361fn numeric_property(handle: &HandleRef, name: &str) -> BuiltinResult<f64> {
1362 numeric_scalar(&property(handle, name)?, name)
1363}
1364
1365fn running_property(handle: &HandleRef) -> BuiltinResult<bool> {
1366 Ok(string_property(handle, "Running")?.eq_ignore_ascii_case("on"))
1367}
1368
1369fn running_is_on(object: &ObjectInstance) -> bool {
1370 matches!(
1371 object.properties.get("Running"),
1372 Some(Value::String(text)) if text.eq_ignore_ascii_case("on")
1373 )
1374}
1375
1376fn is_timer_handle(handle: &HandleRef) -> bool {
1377 handle.class_name == TIMER_CLASS
1378}
1379
1380fn ensure_valid_timer(handle: &HandleRef) -> BuiltinResult<()> {
1381 if is_timer_handle(handle) && crate::is_handle_valid(handle) {
1382 Ok(())
1383 } else {
1384 Err(timer_error(
1385 &TIMER_ERROR_INVALID_HANDLE,
1386 "timer: invalid or deleted timer handle",
1387 ))
1388 }
1389}
1390
1391fn timer_error(error: &'static BuiltinErrorDescriptor, detail: impl AsRef<str>) -> RuntimeError {
1392 let detail = detail.as_ref();
1393 let message = if detail.is_empty() {
1394 error.message.to_string()
1395 } else {
1396 detail.to_string()
1397 };
1398 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_TIMER);
1399 if let Some(identifier) = error.identifier {
1400 builder = builder.with_identifier(identifier);
1401 }
1402 builder.build()
1403}
1404
1405#[cfg(test)]
1406pub(crate) fn reset_timer_state_for_tests() {
1407 TIMER_REGISTRY.with(|registry| {
1408 for handle in registry.borrow().iter() {
1409 let _ = runmat_gc::gc_remove_root(handle.target);
1410 }
1411 registry.borrow_mut().clear();
1412 });
1413 TIMER_COUNTER.with(|counter| counter.set(0));
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::*;
1419 use futures::executor::block_on;
1420 use runmat_value::{CharArray, IntegerStorage, Tensor};
1421 use std::sync::{Arc, Mutex};
1422
1423 #[test]
1424 fn timer_constructor_sets_defaults_and_name_values() {
1425 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1426 reset_timer_state_for_tests();
1427 let value = block_on(timer_builtin(vec![
1428 Value::String("Name".into()),
1429 Value::String("solver".into()),
1430 Value::String("Tag".into()),
1431 Value::String("client".into()),
1432 Value::String("StartDelay".into()),
1433 Value::Num(0.25),
1434 ]))
1435 .expect("timer");
1436 let Value::HandleObject(handle) = value else {
1437 panic!("expected timer handle");
1438 };
1439 assert_eq!(string_property(&handle, "Name").unwrap(), "solver");
1440 assert_eq!(string_property(&handle, "Tag").unwrap(), "client");
1441 assert_eq!(numeric_property(&handle, "StartDelay").unwrap(), 0.25);
1442 assert_eq!(string_property(&handle, "Running").unwrap(), "off");
1443 assert!(crate::is_handle_valid(&handle));
1444 }
1445
1446 #[test]
1447 fn timer_family_declares_exact_integer_property_metadata() {
1448 for (name, capabilities) in [("timer", 2), ("timerfind", 1), ("timerfindall", 1)] {
1449 let builtin = runmat_builtins::builtin_function_by_name(name).unwrap();
1450 assert_eq!(builtin.integer_capabilities.len(), capabilities, "{name}");
1451 assert_eq!(builtin.extensions.len(), 1, "{name}");
1452 assert!(builtin
1453 .integer_capabilities
1454 .iter()
1455 .all(|capability| capability
1456 .inputs
1457 .iter()
1458 .all(|input| input.classes.len() == 8)));
1459 }
1460 }
1461
1462 #[test]
1463 fn timerfind_filters_visible_timers() {
1464 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1465 reset_timer_state_for_tests();
1466 let _hidden = block_on(timer_builtin(vec![
1467 Value::String("Name".into()),
1468 Value::String("hidden".into()),
1469 Value::String("ObjectVisibility".into()),
1470 Value::String("off".into()),
1471 ]))
1472 .expect("hidden timer");
1473 let visible = block_on(timer_builtin(vec![
1474 Value::String("Name".into()),
1475 Value::String("visible".into()),
1476 Value::String("Tag".into()),
1477 Value::String("batch".into()),
1478 ]))
1479 .expect("visible timer");
1480 let found = block_on(timerfind_builtin(vec![
1481 Value::String("Tag".into()),
1482 Value::String("batch".into()),
1483 ]))
1484 .expect("timerfind");
1485 assert_eq!(found, visible);
1486 let all = block_on(timerfindall_builtin(vec![])).expect("timerfindall");
1487 let Value::Cell(cell) = all else {
1488 panic!("expected cell row of all timers");
1489 };
1490 assert_eq!((cell.rows, cell.cols), (1, 2));
1491 }
1492
1493 #[test]
1494 fn timer_start_runs_function_callback_and_updates_state() {
1495 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1496 reset_timer_state_for_tests();
1497 let calls = Arc::new(Mutex::new(0usize));
1498 let invoker_calls = calls.clone();
1499 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1500 move |_function, args, requested_outputs| {
1501 assert_eq!(requested_outputs, 0);
1502 let args = args.to_vec();
1503 let invoker_calls = Arc::clone(&invoker_calls);
1504 Box::pin(async move {
1505 assert_eq!(args.len(), 2);
1506 let Value::HandleObject(handle) = &args[0] else {
1507 panic!("expected timer handle");
1508 };
1509 assert!(is_timer_handle(handle));
1510 let Value::Struct(event) = &args[1] else {
1511 panic!("expected event struct");
1512 };
1513 assert!(event.fields.contains_key("Type"));
1514 *invoker_calls.lock().unwrap() += 1;
1515 Ok(Value::OutputList(Vec::new()))
1516 })
1517 },
1518 )));
1519 let timer = block_on(timer_builtin(vec![
1520 Value::String("TimerFcn".into()),
1521 Value::BoundFunctionHandle {
1522 name: "tick".into(),
1523 function: 1,
1524 },
1525 Value::String("ExecutionMode".into()),
1526 Value::String("fixedSpacing".into()),
1527 Value::String("TasksToExecute".into()),
1528 Value::Num(2.0),
1529 Value::String("Period".into()),
1530 Value::Num(0.002),
1531 ]))
1532 .expect("timer");
1533 block_on(timer_start_builtin(timer.clone())).expect("start");
1534 let Value::HandleObject(handle) = timer else {
1535 panic!("expected timer handle");
1536 };
1537 assert_eq!(*calls.lock().unwrap(), 2);
1538 assert_eq!(numeric_property(&handle, "TasksExecuted").unwrap(), 2.0);
1539 assert_eq!(string_property(&handle, "Running").unwrap(), "off");
1540 }
1541
1542 #[test]
1543 fn timer_delete_invalidates_and_removes_from_find_results() {
1544 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1545 reset_timer_state_for_tests();
1546 let timer = block_on(timer_builtin(vec![
1547 Value::String("TimerFcn".into()),
1548 Value::String("disp(1)".into()),
1549 ]))
1550 .expect("timer");
1551 let Value::HandleObject(handle) = timer.clone() else {
1552 panic!("expected timer handle");
1553 };
1554 block_on(timer_delete_builtin(timer)).expect("delete");
1555 assert!(!crate::is_handle_valid(&handle));
1556 let found = block_on(timerfindall_builtin(vec![])).expect("timerfindall");
1557 let Value::Cell(cell) = found else {
1558 panic!("expected empty cell");
1559 };
1560 assert_eq!((cell.rows, cell.cols), (1, 0));
1561 }
1562
1563 #[test]
1564 fn timer_rejects_invalid_property_values() {
1565 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1566 reset_timer_state_for_tests();
1567 let err = block_on(timer_builtin(vec![
1568 Value::CharArray(CharArray::new_row("Period")),
1569 Value::Num(0.0),
1570 ]))
1571 .unwrap_err();
1572 assert_eq!(
1573 err.identifier().map(str::to_string),
1574 Some("RunMat:timer:InvalidProperty".to_string())
1575 );
1576 }
1577
1578 #[test]
1579 fn timer_numeric_scalar_reads_typed_integer_storage_exactly() {
1580 let tensor = Tensor::new_integer(IntegerStorage::U16(vec![2026]), vec![1, 1])
1581 .expect("typed timer scalar");
1582
1583 assert_eq!(
1584 numeric_scalar(&Value::Tensor(tensor), "StartDelay").expect("numeric scalar"),
1585 2026.0
1586 );
1587
1588 let wide = Tensor::new_integer(IntegerStorage::U64(vec![(1_u64 << 53) + 1]), vec![1, 1])
1589 .expect("wide typed timer scalar");
1590 assert!(numeric_scalar(&Value::Tensor(wide), "StartDelay").is_err());
1591 }
1592
1593 #[test]
1594 fn timer_tasks_to_execute_preserves_typed_integer_storage_exactly() {
1595 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1596 reset_timer_state_for_tests();
1597 let calls = Arc::new(Mutex::new(0usize));
1598 let invoker_calls = calls.clone();
1599 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1600 move |_function, _args, _requested_outputs| {
1601 let invoker_calls = Arc::clone(&invoker_calls);
1602 Box::pin(async move {
1603 *invoker_calls.lock().unwrap() += 1;
1604 Ok(Value::OutputList(Vec::new()))
1605 })
1606 },
1607 )));
1608
1609 let tasks =
1610 Tensor::new_integer(IntegerStorage::U16(vec![2]), vec![1, 1]).expect("typed tasks");
1611 let timer = block_on(timer_builtin(vec![
1612 Value::String("TimerFcn".into()),
1613 Value::BoundFunctionHandle {
1614 name: "tick".into(),
1615 function: 1,
1616 },
1617 Value::String("ExecutionMode".into()),
1618 Value::String("fixedSpacing".into()),
1619 Value::String("TasksToExecute".into()),
1620 Value::Tensor(tasks),
1621 Value::String("Period".into()),
1622 Value::Num(0.002),
1623 ]))
1624 .expect("timer");
1625 let Value::HandleObject(handle) = timer.clone() else {
1626 panic!("expected timer handle");
1627 };
1628 assert_eq!(
1629 property(&handle, "TasksToExecute").unwrap(),
1630 Value::Int(IntValue::U16(2))
1631 );
1632 assert_eq!(numeric_property(&handle, "TasksToExecute").unwrap(), 2.0);
1633
1634 let found = block_on(timerfind_builtin(vec![
1635 Value::String("TasksToExecute".into()),
1636 Value::Num(2.0),
1637 ]))
1638 .expect("timerfind");
1639 assert_eq!(found, timer);
1640
1641 block_on(timer_start_builtin(Value::HandleObject(handle))).expect("start");
1642 assert_eq!(*calls.lock().unwrap(), 2);
1643 }
1644
1645 #[test]
1646 fn timer_tasks_to_execute_rejects_invalid_integer_bounds() {
1647 let negative =
1648 Tensor::new_integer(IntegerStorage::I16(vec![-1]), vec![1, 1]).expect("negative tasks");
1649 assert!(parse_tasks_to_execute_value(&Value::Tensor(negative)).is_err());
1650
1651 let zero =
1652 Tensor::new_integer(IntegerStorage::U16(vec![0]), vec![1, 1]).expect("zero tasks");
1653 assert!(parse_tasks_to_execute_value(&Value::Tensor(zero)).is_err());
1654
1655 let boundary = if usize::BITS == 64 {
1656 usize::MAX as f64
1657 } else {
1658 (usize::MAX as f64) + 1.0
1659 };
1660 assert!(parse_tasks_to_execute_value(&Value::Num(boundary)).is_err());
1661 }
1662
1663 #[test]
1664 fn timerfind_compares_signed_and_wide_integer_user_data_exactly() {
1665 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1666 reset_timer_state_for_tests();
1667 let negative = block_on(timer_builtin(vec![
1668 Value::String("UserData".into()),
1669 Value::Int(IntValue::I64(-7)),
1670 ]))
1671 .expect("negative integer user data");
1672 let wide = block_on(timer_builtin(vec![
1673 Value::String("UserData".into()),
1674 Value::Int(IntValue::U64((1_u64 << 53) + 1)),
1675 ]))
1676 .expect("wide integer user data");
1677
1678 assert_eq!(
1679 block_on(timerfind_builtin(vec![
1680 Value::String("UserData".into()),
1681 Value::Num(-7.0),
1682 ]))
1683 .expect("signed filter"),
1684 negative
1685 );
1686 assert_eq!(
1687 block_on(timerfind_builtin(vec![
1688 Value::String("UserData".into()),
1689 Value::Int(IntValue::U64((1_u64 << 53) + 1)),
1690 ]))
1691 .expect("wide exact filter"),
1692 wide
1693 );
1694 let no_match = block_on(timerfind_builtin(vec![
1695 Value::String("UserData".into()),
1696 Value::Num(((1_u64 << 53) + 1) as f64),
1697 ]))
1698 .expect("rounded double filter");
1699 assert!(matches!(no_match, Value::Cell(cell) if cell.data.is_empty()));
1700 }
1701
1702 #[test]
1703 fn timer_dependent_setters_validate_post_construction_values() {
1704 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1705 reset_timer_state_for_tests();
1706 let timer = block_on(timer_builtin(vec![])).expect("timer");
1707 let Value::HandleObject(handle) = timer else {
1708 panic!("expected timer handle");
1709 };
1710 let object = runmat_gc::gc_clone_value(&handle.target).expect("timer object");
1711 let err = block_on(set_timer_property_object(
1712 object.clone(),
1713 "Period",
1714 Value::Num(0.0),
1715 ))
1716 .unwrap_err();
1717 assert_eq!(
1718 err.identifier().map(str::to_string),
1719 Some("RunMat:timer:InvalidProperty".to_string())
1720 );
1721
1722 let updated = block_on(set_timer_property_object(
1723 object,
1724 "Tag",
1725 Value::CharArray(CharArray::new_row("batch")),
1726 ))
1727 .expect("valid tag update");
1728 let Value::Object(updated) = updated else {
1729 panic!("expected updated timer object");
1730 };
1731 assert_eq!(
1732 updated.properties.get("Tag"),
1733 Some(&Value::String("batch".to_string()))
1734 );
1735 }
1736}