Skip to main content

runmat_runtime/builtins/timing/
timer.rs

1//! MATLAB-compatible timer handle objects.
2//!
3//! Timer objects are handle objects with MATLAB timer properties. RunMat keeps
4//! timers in a per-thread registry so `timerfind` can discover them after the
5//! constructor returns. Lifecycle methods execute callbacks on the current VM
6//! path rather than a background event thread; this keeps callback execution
7//! deterministic across native and wasm hosts.
8
9use 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}