1use std::cell::{Cell, RefCell};
10use std::collections::HashMap;
11#[cfg(not(target_arch = "wasm32"))]
12use std::time::Duration;
13
14#[cfg(test)]
15use once_cell::sync::Lazy;
16#[cfg(test)]
17use std::sync::Mutex;
18
19use runmat_builtins::{
20 Access, BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
21 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
22 CellArray, ClassDef, HandleRef, MethodDef, ObjectInstance, PropertyDef, StructValue, Value,
23};
24use runmat_macros::runtime_builtin;
25
26use crate::builtins::common::spec::{
27 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
28 ReductionNaN, ResidencyPolicy, ShapeRequirements,
29};
30use crate::{build_runtime_error, BuiltinResult, RuntimeError};
31
32const TIMER_CLASS: &str = "timer";
33const BUILTIN_TIMER: &str = "timer";
34const TIMER_METHOD_START: &str = "__runmat_timer_start";
35const TIMER_METHOD_STARTAT: &str = "__runmat_timer_startat";
36const TIMER_METHOD_STOP: &str = "__runmat_timer_stop";
37const TIMER_METHOD_WAIT: &str = "__runmat_timer_wait";
38const TIMER_METHOD_DELETE: &str = "__runmat_timer_delete";
39
40const CALLBACK_PROPS: [&str; 4] = ["TimerFcn", "StartFcn", "StopFcn", "ErrorFcn"];
41const STRING_PROPS: [&str; 5] = [
42 "BusyMode",
43 "ExecutionMode",
44 "Name",
45 "Tag",
46 "ObjectVisibility",
47];
48const NUMERIC_PROPS: [&str; 3] = ["Period", "StartDelay", "TasksToExecute"];
49const READONLY_PROPS: [&str; 5] = [
50 "AveragePeriod",
51 "InstantPeriod",
52 "Running",
53 "TasksExecuted",
54 "Type",
55];
56
57thread_local! {
58 static TIMER_REGISTRY: RefCell<Vec<HandleRef>> = const { RefCell::new(Vec::new()) };
59 static TIMER_COUNTER: Cell<usize> = const { Cell::new(0) };
60}
61
62#[cfg(test)]
63static TIMER_TEST_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
64
65const TIMER_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
66 name: "t",
67 ty: BuiltinParamType::Any,
68 arity: BuiltinParamArity::Required,
69 default: None,
70 description: "Timer handle object.",
71}];
72
73const TIMER_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
74 name: "Name,Value",
75 ty: BuiltinParamType::Any,
76 arity: BuiltinParamArity::Variadic,
77 default: None,
78 description: "Timer properties such as TimerFcn, StartDelay, Period, ExecutionMode, Name, Tag, and UserData.",
79}];
80
81const TIMER_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
82 BuiltinSignatureDescriptor {
83 label: "t = timer",
84 inputs: &[],
85 outputs: &TIMER_OUTPUT,
86 },
87 BuiltinSignatureDescriptor {
88 label: "t = timer(Name, Value, ...)",
89 inputs: &TIMER_INPUTS,
90 outputs: &TIMER_OUTPUT,
91 },
92];
93
94const TIMERFIND_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
95 name: "timers",
96 ty: BuiltinParamType::Any,
97 arity: BuiltinParamArity::Required,
98 default: None,
99 description: "Matching timer handle, empty cell row, or cell row of timer handles.",
100}];
101
102const TIMERFIND_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
103 name: "Name,Value",
104 ty: BuiltinParamType::Any,
105 arity: BuiltinParamArity::Variadic,
106 default: None,
107 description: "Property filters matched by exact value.",
108}];
109
110const TIMERFIND_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
111 BuiltinSignatureDescriptor {
112 label: "timers = timerfind",
113 inputs: &[],
114 outputs: &TIMERFIND_OUTPUT,
115 },
116 BuiltinSignatureDescriptor {
117 label: "timers = timerfind(Name, Value, ...)",
118 inputs: &TIMERFIND_INPUTS,
119 outputs: &TIMERFIND_OUTPUT,
120 },
121];
122
123const TIMERFINDALL_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
124 BuiltinSignatureDescriptor {
125 label: "timers = timerfindall",
126 inputs: &[],
127 outputs: &TIMERFIND_OUTPUT,
128 },
129 BuiltinSignatureDescriptor {
130 label: "timers = timerfindall(Name, Value, ...)",
131 inputs: &TIMERFIND_INPUTS,
132 outputs: &TIMERFIND_OUTPUT,
133 },
134];
135
136const TIMER_METHOD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
137 name: "status",
138 ty: BuiltinParamType::NumericScalar,
139 arity: BuiltinParamArity::Optional,
140 default: None,
141 description: "Zero on success.",
142}];
143
144const TIMER_METHOD_INPUTS: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
145 name: "t",
146 ty: BuiltinParamType::Any,
147 arity: BuiltinParamArity::Required,
148 default: None,
149 description: "Timer handle or cell array of timer handles.",
150}];
151
152const TIMER_METHOD_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
153 label: "status = method(t)",
154 inputs: &TIMER_METHOD_INPUTS,
155 outputs: &TIMER_METHOD_OUTPUT,
156}];
157
158const TIMER_SETTER_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
159 name: "obj",
160 ty: BuiltinParamType::Any,
161 arity: BuiltinParamArity::Required,
162 default: None,
163 description: "Updated timer object.",
164}];
165
166const TIMER_SETTER_INPUTS: [BuiltinParamDescriptor; 2] = [
167 BuiltinParamDescriptor {
168 name: "obj",
169 ty: BuiltinParamType::Any,
170 arity: BuiltinParamArity::Required,
171 default: None,
172 description: "Timer object receiver.",
173 },
174 BuiltinParamDescriptor {
175 name: "value",
176 ty: BuiltinParamType::Any,
177 arity: BuiltinParamArity::Required,
178 default: None,
179 description: "Property value to validate and assign.",
180 },
181];
182
183const TIMER_SETTER_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
184 label: "obj = set.Property(obj, value)",
185 inputs: &TIMER_SETTER_INPUTS,
186 outputs: &TIMER_SETTER_OUTPUT,
187}];
188
189const TIMER_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
190 code: "RM.TIMER.INVALID_INPUT",
191 identifier: Some("RunMat:timer:InvalidInput"),
192 when: "A timer argument, property, or callback value has an unsupported type.",
193 message: "timer: invalid input",
194};
195
196const TIMER_ERROR_INVALID_PROPERTY: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
197 code: "RM.TIMER.INVALID_PROPERTY",
198 identifier: Some("RunMat:timer:InvalidProperty"),
199 when: "A timer property name is unknown, read-only, or has an invalid value.",
200 message: "timer: invalid property",
201};
202
203const TIMER_ERROR_INVALID_HANDLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
204 code: "RM.TIMER.INVALID_HANDLE",
205 identifier: Some("RunMat:timer:InvalidHandle"),
206 when: "A lifecycle method receives a non-timer or invalid timer handle.",
207 message: "timer: invalid timer handle",
208};
209
210const TIMER_ERROR_CALLBACK: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
211 code: "RM.TIMER.CALLBACK",
212 identifier: Some("RunMat:timer:CallbackFailed"),
213 when: "A timer callback fails.",
214 message: "timer: callback failed",
215};
216
217const TIMER_ERROR_GC: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
218 code: "RM.TIMER.GC",
219 identifier: Some("RunMat:timer:GcFailure"),
220 when: "A timer object cannot be allocated, rooted, read, or mutated.",
221 message: "timer: object storage failed",
222};
223
224const TIMER_ERRORS: [BuiltinErrorDescriptor; 5] = [
225 TIMER_ERROR_INVALID_INPUT,
226 TIMER_ERROR_INVALID_PROPERTY,
227 TIMER_ERROR_INVALID_HANDLE,
228 TIMER_ERROR_CALLBACK,
229 TIMER_ERROR_GC,
230];
231
232pub const TIMER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
233 signatures: &TIMER_SIGNATURES,
234 output_mode: BuiltinOutputMode::Fixed,
235 completion_policy: BuiltinCompletionPolicy::Public,
236 errors: &TIMER_ERRORS,
237};
238
239pub const TIMERFIND_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
240 signatures: &TIMERFIND_SIGNATURES,
241 output_mode: BuiltinOutputMode::Fixed,
242 completion_policy: BuiltinCompletionPolicy::Public,
243 errors: &TIMER_ERRORS,
244};
245
246pub const TIMERFINDALL_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
247 signatures: &TIMERFINDALL_SIGNATURES,
248 output_mode: BuiltinOutputMode::Fixed,
249 completion_policy: BuiltinCompletionPolicy::Public,
250 errors: &TIMER_ERRORS,
251};
252
253pub const TIMER_METHOD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
254 signatures: &TIMER_METHOD_SIGNATURES,
255 output_mode: BuiltinOutputMode::Fixed,
256 completion_policy: BuiltinCompletionPolicy::HiddenInternal,
257 errors: &TIMER_ERRORS,
258};
259
260pub const TIMER_SETTER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
261 signatures: &TIMER_SETTER_SIGNATURES,
262 output_mode: BuiltinOutputMode::Fixed,
263 completion_policy: BuiltinCompletionPolicy::MethodOnly,
264 errors: &TIMER_ERRORS,
265};
266
267#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::timer")]
268pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
269 name: "timer",
270 op_kind: GpuOpKind::Custom("timer"),
271 supported_precisions: &[],
272 broadcast: BroadcastSemantics::None,
273 provider_hooks: &[],
274 constant_strategy: ConstantStrategy::InlineLiteral,
275 residency: ResidencyPolicy::GatherImmediately,
276 nan_mode: ReductionNaN::Include,
277 two_pass_threshold: None,
278 workgroup_size: None,
279 accepts_nan_mode: false,
280 notes: "Timer handles are host control-flow objects. Timer callbacks may call GPU-capable builtins, but timer itself has no provider kernel.",
281};
282
283#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::timer")]
284pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
285 name: "timer",
286 shape: ShapeRequirements::Any,
287 constant_strategy: ConstantStrategy::InlineLiteral,
288 elementwise: None,
289 reduction: None,
290 emits_nan: false,
291 notes: "Timer object creation, discovery, and callback execution are side-effect boundaries and are excluded from fusion.",
292};
293
294#[runtime_builtin(
295 name = "timer",
296 category = "timing",
297 summary = "Create a MATLAB-compatible timer handle object.",
298 keywords = "timer,callback,start,stop,wait,timerfind",
299 descriptor(crate::builtins::timing::timer::TIMER_DESCRIPTOR),
300 builtin_path = "crate::builtins::timing::timer"
301)]
302pub async fn timer_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
303 ensure_timer_class_registered();
304 let mut object = default_timer_object(next_timer_name());
305 apply_name_value_pairs(&mut object, &args, true)?;
306 let target = runmat_gc::gc_allocate(Value::Object(object))
307 .map_err(|err| timer_error(&TIMER_ERROR_GC, format!("timer: {err}")))?;
308 runmat_gc::gc_add_root(target)
309 .map_err(|err| timer_error(&TIMER_ERROR_GC, format!("timer: {err}")))?;
310 let handle = HandleRef {
311 class_name: TIMER_CLASS.to_string(),
312 target,
313 valid: true,
314 };
315 TIMER_REGISTRY.with(|registry| registry.borrow_mut().push(handle.clone()));
316 Ok(Value::HandleObject(handle))
317}
318
319#[runtime_builtin(
320 name = "timerfind",
321 category = "timing",
322 summary = "Find visible timer objects by property value.",
323 keywords = "timerfind,timer,callback,handle",
324 descriptor(crate::builtins::timing::timer::TIMERFIND_DESCRIPTOR),
325 builtin_path = "crate::builtins::timing::timer"
326)]
327pub fn timerfind_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
328 find_timers(args, false)
329}
330
331#[runtime_builtin(
332 name = "timerfindall",
333 category = "timing",
334 summary = "Find all timer objects by property value.",
335 keywords = "timerfindall,timer,callback,handle",
336 descriptor(crate::builtins::timing::timer::TIMERFINDALL_DESCRIPTOR),
337 builtin_path = "crate::builtins::timing::timer"
338)]
339pub fn timerfindall_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
340 find_timers(args, true)
341}
342
343#[runtime_builtin(
344 name = "__runmat_timer_start",
345 category = "timing",
346 summary = "Start a timer object.",
347 keywords = "timer,start",
348 sink = true,
349 suppress_auto_output = true,
350 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
351 builtin_path = "crate::builtins::timing::timer"
352)]
353pub async fn timer_start_builtin(value: Value) -> BuiltinResult<Value> {
354 start_timer_values(&value, None).await?;
355 Ok(Value::Num(0.0))
356}
357
358#[runtime_builtin(
359 name = "__runmat_timer_startat",
360 category = "timing",
361 summary = "Start a timer object at a requested time.",
362 keywords = "timer,startat",
363 sink = true,
364 suppress_auto_output = true,
365 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
366 builtin_path = "crate::builtins::timing::timer"
367)]
368pub async fn timer_startat_builtin(value: Value, rest: Vec<Value>) -> BuiltinResult<Value> {
369 let delay = rest
370 .first()
371 .map(|value| numeric_scalar(value, "startat"))
372 .transpose()?
373 .filter(|seconds| *seconds > 0.0);
374 start_timer_values(&value, delay).await?;
375 Ok(Value::Num(0.0))
376}
377
378#[runtime_builtin(
379 name = "__runmat_timer_stop",
380 category = "timing",
381 summary = "Stop a timer object.",
382 keywords = "timer,stop",
383 sink = true,
384 suppress_auto_output = true,
385 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
386 builtin_path = "crate::builtins::timing::timer"
387)]
388pub async fn timer_stop_builtin(value: Value) -> BuiltinResult<Value> {
389 stop_timer_values(&value, true).await?;
390 Ok(Value::Num(0.0))
391}
392
393#[runtime_builtin(
394 name = "__runmat_timer_wait",
395 category = "timing",
396 summary = "Wait until timer objects finish.",
397 keywords = "timer,wait",
398 sink = true,
399 suppress_auto_output = true,
400 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
401 builtin_path = "crate::builtins::timing::timer"
402)]
403pub async fn timer_wait_builtin(value: Value) -> BuiltinResult<Value> {
404 wait_timer_values(&value).await?;
405 Ok(Value::Num(0.0))
406}
407
408#[runtime_builtin(
409 name = "__runmat_timer_delete",
410 category = "timing",
411 summary = "Delete a timer object.",
412 keywords = "timer,delete",
413 sink = true,
414 suppress_auto_output = true,
415 descriptor(crate::builtins::timing::timer::TIMER_METHOD_DESCRIPTOR),
416 builtin_path = "crate::builtins::timing::timer"
417)]
418pub async fn timer_delete_builtin(value: Value) -> BuiltinResult<Value> {
419 delete_timer_values(&value).await?;
420 Ok(Value::Num(0.0))
421}
422
423macro_rules! timer_setter_builtin {
424 ($fn_name:ident, $builtin_name:literal, $property:literal) => {
425 #[runtime_builtin(
426 name = $builtin_name,
427 category = "timing",
428 summary = "Validate and assign a timer property.",
429 keywords = "timer,set,property",
430 descriptor(crate::builtins::timing::timer::TIMER_SETTER_DESCRIPTOR),
431 builtin_path = "crate::builtins::timing::timer"
432 )]
433 pub async fn $fn_name(obj: Value, value: Value) -> BuiltinResult<Value> {
434 set_timer_property_object(obj, $property, value).await
435 }
436 };
437}
438
439timer_setter_builtin!(timer_set_timer_fcn_builtin, "set.TimerFcn", "TimerFcn");
440timer_setter_builtin!(timer_set_start_fcn_builtin, "set.StartFcn", "StartFcn");
441timer_setter_builtin!(timer_set_stop_fcn_builtin, "set.StopFcn", "StopFcn");
442timer_setter_builtin!(timer_set_error_fcn_builtin, "set.ErrorFcn", "ErrorFcn");
443timer_setter_builtin!(timer_set_period_builtin, "set.Period", "Period");
444timer_setter_builtin!(
445 timer_set_start_delay_builtin,
446 "set.StartDelay",
447 "StartDelay"
448);
449timer_setter_builtin!(
450 timer_set_tasks_to_execute_builtin,
451 "set.TasksToExecute",
452 "TasksToExecute"
453);
454timer_setter_builtin!(timer_set_busy_mode_builtin, "set.BusyMode", "BusyMode");
455timer_setter_builtin!(
456 timer_set_execution_mode_builtin,
457 "set.ExecutionMode",
458 "ExecutionMode"
459);
460timer_setter_builtin!(timer_set_name_builtin, "set.Name", "Name");
461timer_setter_builtin!(timer_set_tag_builtin, "set.Tag", "Tag");
462timer_setter_builtin!(
463 timer_set_object_visibility_builtin,
464 "set.ObjectVisibility",
465 "ObjectVisibility"
466);
467timer_setter_builtin!(timer_set_user_data_builtin, "set.UserData", "UserData");
468
469fn ensure_timer_class_registered() {
470 if runmat_builtins::get_class(TIMER_CLASS).is_some() {
471 return;
472 }
473
474 let mut methods = HashMap::new();
475 for (name, function_name) in [
476 ("start", TIMER_METHOD_START),
477 ("startat", TIMER_METHOD_STARTAT),
478 ("stop", TIMER_METHOD_STOP),
479 ("wait", TIMER_METHOD_WAIT),
480 ("delete", TIMER_METHOD_DELETE),
481 ] {
482 methods.insert(
483 name.to_string(),
484 MethodDef {
485 name: name.to_string(),
486 is_static: false,
487 is_abstract: false,
488 is_sealed: false,
489 access: Access::Public,
490 function_name: function_name.to_string(),
491 implicit_class_argument: None,
492 },
493 );
494 }
495
496 let mut properties = HashMap::new();
497 for name in CALLBACK_PROPS
498 .into_iter()
499 .chain(STRING_PROPS)
500 .chain(NUMERIC_PROPS)
501 .chain(READONLY_PROPS)
502 .chain(["UserData"])
503 {
504 properties.insert(
505 name.to_string(),
506 PropertyDef {
507 name: name.to_string(),
508 is_static: false,
509 is_constant: false,
510 is_dependent: is_timer_mutable_property(name),
511 get_access: Access::Public,
512 set_access: if READONLY_PROPS.contains(&name) {
513 Access::Private
514 } else {
515 Access::Public
516 },
517 default_value: None,
518 },
519 );
520 }
521
522 runmat_builtins::register_class(ClassDef {
523 name: TIMER_CLASS.to_string(),
524 parent: Some("handle".to_string()),
525 properties,
526 methods,
527 });
528}
529
530fn is_timer_mutable_property(name: &str) -> bool {
531 CALLBACK_PROPS.contains(&name)
532 || STRING_PROPS.contains(&name)
533 || NUMERIC_PROPS.contains(&name)
534 || name == "UserData"
535}
536
537fn default_timer_object(name: String) -> ObjectInstance {
538 let mut object = ObjectInstance::new(TIMER_CLASS.to_string());
539 for prop in CALLBACK_PROPS {
540 object
541 .properties
542 .insert(prop.to_string(), Value::String(String::new()));
543 }
544 object
545 .properties
546 .insert("Period".to_string(), Value::Num(1.0));
547 object
548 .properties
549 .insert("StartDelay".to_string(), Value::Num(0.0));
550 object
551 .properties
552 .insert("TasksToExecute".to_string(), Value::Num(1.0));
553 object
554 .properties
555 .insert("BusyMode".to_string(), Value::String("drop".to_string()));
556 object.properties.insert(
557 "ExecutionMode".to_string(),
558 Value::String("singleShot".to_string()),
559 );
560 object
561 .properties
562 .insert("Name".to_string(), Value::String(name));
563 object
564 .properties
565 .insert("Tag".to_string(), Value::String(String::new()));
566 object.properties.insert(
567 "ObjectVisibility".to_string(),
568 Value::String("on".to_string()),
569 );
570 object
571 .properties
572 .insert("UserData".to_string(), Value::Num(0.0));
573 object
574 .properties
575 .insert("AveragePeriod".to_string(), Value::Num(f64::NAN));
576 object
577 .properties
578 .insert("InstantPeriod".to_string(), Value::Num(f64::NAN));
579 object
580 .properties
581 .insert("Running".to_string(), Value::String("off".to_string()));
582 object
583 .properties
584 .insert("TasksExecuted".to_string(), Value::Num(0.0));
585 object
586 .properties
587 .insert("Type".to_string(), Value::String(TIMER_CLASS.to_string()));
588 object.properties.insert(
589 crate::HANDLE_VALID_FLAG_PROPERTY.to_string(),
590 Value::Bool(true),
591 );
592 object
593}
594
595fn next_timer_name() -> String {
596 TIMER_COUNTER.with(|counter| {
597 let next = counter.get() + 1;
598 counter.set(next);
599 format!("timer-{next}")
600 })
601}
602
603fn apply_name_value_pairs(
604 object: &mut ObjectInstance,
605 args: &[Value],
606 constructor: bool,
607) -> BuiltinResult<()> {
608 if !args.len().is_multiple_of(2) {
609 return Err(timer_error(
610 &TIMER_ERROR_INVALID_INPUT,
611 "timer: name-value arguments must appear in pairs",
612 ));
613 }
614 let mut idx = 0usize;
615 while idx < args.len() {
616 let name = canonical_property_name(&value_to_string(&args[idx])?)?;
617 if READONLY_PROPS.contains(&name.as_str()) {
618 return Err(timer_error(
619 &TIMER_ERROR_INVALID_PROPERTY,
620 format!("timer: property '{name}' is read-only"),
621 ));
622 }
623 let value = normalize_property_value(&name, args[idx + 1].clone())?;
624 if !constructor && running_is_on(object) && is_running_readonly_property(&name) {
625 return Err(timer_error(
626 &TIMER_ERROR_INVALID_PROPERTY,
627 format!("timer: property '{name}' cannot be changed while Running is on"),
628 ));
629 }
630 object.properties.insert(name, value);
631 idx += 2;
632 }
633 Ok(())
634}
635
636async fn set_timer_property_object(
637 obj: Value,
638 property_name: &'static str,
639 value: Value,
640) -> BuiltinResult<Value> {
641 let Value::Object(mut object) = obj else {
642 return Err(timer_error(
643 &TIMER_ERROR_INVALID_HANDLE,
644 format!("timer: set.{property_name} requires timer object receiver"),
645 ));
646 };
647 if object.class_name != TIMER_CLASS {
648 return Err(timer_error(
649 &TIMER_ERROR_INVALID_HANDLE,
650 format!(
651 "timer: set.{property_name} requires timer object receiver, got '{}'",
652 object.class_name
653 ),
654 ));
655 }
656 apply_name_value_pairs(
657 &mut object,
658 &[Value::String(property_name.to_string()), value],
659 false,
660 )?;
661 Ok(Value::Object(object))
662}
663
664fn canonical_property_name(name: &str) -> BuiltinResult<String> {
665 let trimmed = name.trim();
666 for prop in CALLBACK_PROPS
667 .into_iter()
668 .chain(STRING_PROPS)
669 .chain(NUMERIC_PROPS)
670 .chain(READONLY_PROPS)
671 .chain(["UserData"])
672 {
673 if prop.eq_ignore_ascii_case(trimmed) {
674 return Ok(prop.to_string());
675 }
676 }
677 Err(timer_error(
678 &TIMER_ERROR_INVALID_PROPERTY,
679 format!("timer: unknown property '{trimmed}'"),
680 ))
681}
682
683fn normalize_property_value(name: &str, value: Value) -> BuiltinResult<Value> {
684 match name {
685 "Period" => {
686 let seconds = numeric_scalar(&value, name)?;
687 if !seconds.is_finite() || seconds <= 0.001 {
688 return Err(timer_error(
689 &TIMER_ERROR_INVALID_PROPERTY,
690 "timer: Period must be a finite scalar greater than 0.001",
691 ));
692 }
693 Ok(Value::Num(seconds))
694 }
695 "StartDelay" => {
696 let seconds = numeric_scalar(&value, name)?;
697 if !seconds.is_finite() || seconds < 0.0 {
698 return Err(timer_error(
699 &TIMER_ERROR_INVALID_PROPERTY,
700 "timer: StartDelay must be a finite non-negative scalar",
701 ));
702 }
703 Ok(Value::Num(seconds))
704 }
705 "TasksToExecute" => {
706 let count = numeric_scalar(&value, name)?;
707 if !count.is_finite() || count < 1.0 || count.fract() != 0.0 {
708 return Err(timer_error(
709 &TIMER_ERROR_INVALID_PROPERTY,
710 "timer: TasksToExecute must be a positive integer scalar",
711 ));
712 }
713 Ok(Value::Num(count))
714 }
715 "BusyMode" => match_string_choice(&value, name, &["drop", "error", "queue"]),
716 "ExecutionMode" => match_string_choice(
717 &value,
718 name,
719 &["singleShot", "fixedRate", "fixedDelay", "fixedSpacing"],
720 ),
721 "ObjectVisibility" => match_string_choice(&value, name, &["on", "off"]),
722 "Name" | "Tag" => Ok(Value::String(value_to_string(&value)?)),
723 "TimerFcn" | "StartFcn" | "StopFcn" | "ErrorFcn" => normalize_callback(value, name),
724 "UserData" => Ok(value),
725 _ => Err(timer_error(
726 &TIMER_ERROR_INVALID_PROPERTY,
727 format!("timer: property '{name}' cannot be assigned"),
728 )),
729 }
730}
731
732fn is_running_readonly_property(name: &str) -> bool {
733 matches!(name, "BusyMode" | "ExecutionMode" | "StartDelay")
734}
735
736fn normalize_callback(value: Value, name: &str) -> BuiltinResult<Value> {
737 match value {
738 Value::String(_)
739 | Value::CharArray(_)
740 | Value::FunctionHandle(_)
741 | Value::ExternalFunctionHandle(_)
742 | Value::MethodFunctionHandle(_)
743 | Value::BoundFunctionHandle { .. }
744 | Value::Closure(_)
745 | Value::Cell(_) => Ok(value),
746 other => Err(timer_error(
747 &TIMER_ERROR_INVALID_PROPERTY,
748 format!("timer: {name} must be text, a function handle, or a callback cell array, got {other:?}"),
749 )),
750 }
751}
752
753fn match_string_choice(value: &Value, name: &str, choices: &[&str]) -> BuiltinResult<Value> {
754 let text = value_to_string(value)?;
755 choices
756 .iter()
757 .find(|choice| choice.eq_ignore_ascii_case(text.trim()))
758 .map(|choice| Value::String((*choice).to_string()))
759 .ok_or_else(|| {
760 timer_error(
761 &TIMER_ERROR_INVALID_PROPERTY,
762 format!("timer: invalid {name} value '{text}'"),
763 )
764 })
765}
766
767fn value_to_string(value: &Value) -> BuiltinResult<String> {
768 match value {
769 Value::String(text) => Ok(text.clone()),
770 Value::CharArray(chars) if chars.rows == 1 => Ok(chars.data.iter().collect()),
771 other => Err(timer_error(
772 &TIMER_ERROR_INVALID_INPUT,
773 format!("timer: expected string scalar or character row, got {other:?}"),
774 )),
775 }
776}
777
778fn numeric_scalar(value: &Value, name: &str) -> BuiltinResult<f64> {
779 match value {
780 Value::Num(value) => Ok(*value),
781 Value::Int(value) => Ok(value.to_f64()),
782 Value::Tensor(tensor) if tensor.data.len() == 1 => Ok(tensor.data[0]),
783 other => Err(timer_error(
784 &TIMER_ERROR_INVALID_INPUT,
785 format!("timer: {name} must be a numeric scalar, got {other:?}"),
786 )),
787 }
788}
789
790async fn start_timer_values(value: &Value, override_delay: Option<f64>) -> BuiltinResult<()> {
791 match value {
792 Value::HandleObject(handle) if is_timer_handle(handle) => {
793 start_one_timer(handle.clone(), override_delay).await
794 }
795 Value::Cell(cell) => {
796 for item in &cell.data {
797 Box::pin(start_timer_values(item, override_delay)).await?;
798 }
799 Ok(())
800 }
801 other => Err(timer_error(
802 &TIMER_ERROR_INVALID_HANDLE,
803 format!("timer: expected timer handle, got {other:?}"),
804 )),
805 }
806}
807
808async fn stop_timer_values(value: &Value, call_stop: bool) -> BuiltinResult<()> {
809 match value {
810 Value::HandleObject(handle) if is_timer_handle(handle) => {
811 stop_one_timer(handle, call_stop).await
812 }
813 Value::Cell(cell) => {
814 for item in &cell.data {
815 Box::pin(stop_timer_values(item, call_stop)).await?;
816 }
817 Ok(())
818 }
819 other => Err(timer_error(
820 &TIMER_ERROR_INVALID_HANDLE,
821 format!("timer: expected timer handle, got {other:?}"),
822 )),
823 }
824}
825
826async fn wait_timer_values(value: &Value) -> BuiltinResult<()> {
827 match value {
828 Value::HandleObject(handle) if is_timer_handle(handle) => {
829 if running_property(handle)? {
830 stop_one_timer(handle, true).await?;
831 }
832 Ok(())
833 }
834 Value::Cell(cell) => {
835 for item in &cell.data {
836 Box::pin(wait_timer_values(item)).await?;
837 }
838 Ok(())
839 }
840 other => Err(timer_error(
841 &TIMER_ERROR_INVALID_HANDLE,
842 format!("timer: expected timer handle, got {other:?}"),
843 )),
844 }
845}
846
847async fn delete_timer_values(value: &Value) -> BuiltinResult<()> {
848 match value {
849 Value::HandleObject(handle) if is_timer_handle(handle) => {
850 stop_one_timer(handle, false).await?;
851 crate::set_handle_valid(handle, false);
852 TIMER_REGISTRY.with(|registry| registry.borrow_mut().retain(|h| h != handle));
853 let _ = runmat_gc::gc_remove_root(handle.target);
854 Ok(())
855 }
856 Value::Cell(cell) => {
857 for item in &cell.data {
858 Box::pin(delete_timer_values(item)).await?;
859 }
860 Ok(())
861 }
862 other => Err(timer_error(
863 &TIMER_ERROR_INVALID_HANDLE,
864 format!("timer: expected timer handle, got {other:?}"),
865 )),
866 }
867}
868
869async fn start_one_timer(handle: HandleRef, override_delay: Option<f64>) -> BuiltinResult<()> {
870 ensure_valid_timer(&handle)?;
871 let timer_fcn = property(&handle, "TimerFcn")?;
872 if callback_is_empty(&timer_fcn) {
873 return Err(timer_error(
874 &TIMER_ERROR_INVALID_PROPERTY,
875 "timer: TimerFcn must be set before starting a timer",
876 ));
877 }
878 set_property(&handle, "Running", Value::String("on".to_string()))?;
879 set_property(&handle, "TasksExecuted", Value::Num(0.0))?;
880 let start_delay = override_delay.unwrap_or(numeric_property(&handle, "StartDelay")?);
881 sleep_seconds(start_delay);
882 if let Err(err) = run_callback(&handle, "StartFcn", "StartFcn").await {
883 let _ = stop_one_timer(&handle, false).await;
884 return Err(err);
885 }
886
887 let execution_mode = string_property(&handle, "ExecutionMode")?;
888 let tasks = if execution_mode.eq_ignore_ascii_case("singleShot") {
889 1usize
890 } else {
891 numeric_property(&handle, "TasksToExecute")?.max(1.0) as usize
892 };
893 let period = numeric_property(&handle, "Period")?;
894 let mut last_fire: Option<runmat_time::Instant> = None;
895 for idx in 0..tasks {
896 if idx > 0 {
897 sleep_seconds(period);
898 }
899 let now = runmat_time::Instant::now();
900 if let Some(last) = last_fire {
901 let instant_period = now.duration_since(last).as_secs_f64();
902 set_property(&handle, "InstantPeriod", Value::Num(instant_period))?;
903 update_average_period(&handle, instant_period, idx)?;
904 }
905 last_fire = Some(now);
906 match run_callback(&handle, "TimerFcn", "TimerFcn").await {
907 Ok(()) => {
908 let executed = numeric_property(&handle, "TasksExecuted")? + 1.0;
909 set_property(&handle, "TasksExecuted", Value::Num(executed))?;
910 }
911 Err(err) => {
912 let _ = run_callback(&handle, "ErrorFcn", "ErrorFcn").await;
913 let _ = stop_one_timer(&handle, true).await;
914 return Err(timer_error(&TIMER_ERROR_CALLBACK, err.message()));
915 }
916 }
917 }
918 stop_one_timer(&handle, true).await
919}
920
921async fn stop_one_timer(handle: &HandleRef, call_stop: bool) -> BuiltinResult<()> {
922 ensure_valid_timer(handle)?;
923 let was_running = running_property(handle)?;
924 set_property(handle, "Running", Value::String("off".to_string()))?;
925 if call_stop && was_running {
926 run_callback(handle, "StopFcn", "StopFcn").await?;
927 }
928 Ok(())
929}
930
931fn update_average_period(handle: &HandleRef, instant_period: f64, idx: usize) -> BuiltinResult<()> {
932 let current = numeric_property(handle, "AveragePeriod").unwrap_or(f64::NAN);
933 let average = if current.is_finite() {
934 (current * (idx as f64 - 1.0) + instant_period) / idx as f64
935 } else {
936 instant_period
937 };
938 set_property(handle, "AveragePeriod", Value::Num(average))
939}
940
941async fn run_callback(
942 handle: &HandleRef,
943 property_name: &str,
944 event_type: &str,
945) -> BuiltinResult<()> {
946 let callback = property(handle, property_name)?;
947 if callback_is_empty(&callback) {
948 return Ok(());
949 }
950 match callback {
951 Value::String(text) if !text.trim().is_empty() => {
952 return Err(timer_error(
953 &TIMER_ERROR_CALLBACK,
954 "timer: text callback execution requires dynamic-eval callback infrastructure; use a function handle callback",
955 ));
956 }
957 Value::CharArray(chars) if !chars.data.is_empty() => {
958 let text: String = chars.data.iter().collect();
959 if !text.trim().is_empty() {
960 return Err(timer_error(
961 &TIMER_ERROR_CALLBACK,
962 "timer: text callback execution requires dynamic-eval callback infrastructure; use a function handle callback",
963 ));
964 }
965 }
966 Value::Cell(cell) => {
967 let Some(function) = cell.data.first().cloned() else {
968 return Ok(());
969 };
970 let mut args = callback_base_args(handle, event_type);
971 args.extend(cell.data.iter().skip(1).cloned());
972 crate::call_feval_async_with_outputs(function, &args, 0).await?;
973 }
974 function @ (Value::FunctionHandle(_)
975 | Value::ExternalFunctionHandle(_)
976 | Value::MethodFunctionHandle(_)
977 | Value::BoundFunctionHandle { .. }
978 | Value::Closure(_)) => {
979 let args = callback_base_args(handle, event_type);
980 crate::call_feval_async_with_outputs(function, &args, 0).await?;
981 }
982 _ => {}
983 }
984 Ok(())
985}
986
987fn callback_base_args(handle: &HandleRef, event_type: &str) -> Vec<Value> {
988 vec![Value::HandleObject(handle.clone()), timer_event(event_type)]
989}
990
991fn timer_event(event_type: &str) -> Value {
992 let mut data = StructValue::new();
993 data.insert(
994 "time",
995 Value::Num(runmat_time::duration_since_epoch().as_secs_f64()),
996 );
997 let mut event = StructValue::new();
998 event.insert("Type", Value::String(event_type.to_string()));
999 event.insert("Data", Value::Struct(data));
1000 Value::Struct(event)
1001}
1002
1003fn callback_is_empty(value: &Value) -> bool {
1004 match value {
1005 Value::String(text) => text.trim().is_empty(),
1006 Value::CharArray(chars) => chars.data.iter().collect::<String>().trim().is_empty(),
1007 Value::Cell(cell) => cell.data.is_empty(),
1008 _ => false,
1009 }
1010}
1011
1012fn sleep_seconds(seconds: f64) {
1013 if seconds <= 0.0 || !seconds.is_finite() {
1014 return;
1015 }
1016 #[cfg(not(target_arch = "wasm32"))]
1017 {
1018 std::thread::sleep(Duration::from_secs_f64(seconds));
1019 }
1020 #[cfg(target_arch = "wasm32")]
1021 {
1022 let _ = seconds;
1023 }
1024}
1025
1026fn find_timers(args: Vec<Value>, include_invisible: bool) -> BuiltinResult<Value> {
1027 ensure_timer_class_registered();
1028 let filters = parse_filters(&args)?;
1029 let mut matches = Vec::new();
1030 TIMER_REGISTRY.with(|registry| {
1031 let mut registry = registry.borrow_mut();
1032 registry.retain(crate::is_handle_valid);
1033 for handle in registry.iter() {
1034 if !include_invisible
1035 && !string_property(handle, "ObjectVisibility")
1036 .map(|value| value.eq_ignore_ascii_case("on"))
1037 .unwrap_or(false)
1038 {
1039 continue;
1040 }
1041 if filters_match(handle, &filters).unwrap_or(false) {
1042 matches.push(Value::HandleObject(handle.clone()));
1043 }
1044 }
1045 });
1046 handles_to_result(matches)
1047}
1048
1049fn parse_filters(args: &[Value]) -> BuiltinResult<Vec<(String, Value)>> {
1050 if !args.len().is_multiple_of(2) {
1051 return Err(timer_error(
1052 &TIMER_ERROR_INVALID_INPUT,
1053 "timerfind: name-value arguments must appear in pairs",
1054 ));
1055 }
1056 let mut out = Vec::new();
1057 let mut idx = 0usize;
1058 while idx < args.len() {
1059 let name = canonical_property_name(&value_to_string(&args[idx])?)?;
1060 out.push((name, normalize_find_value(args[idx + 1].clone())));
1061 idx += 2;
1062 }
1063 Ok(out)
1064}
1065
1066fn normalize_find_value(value: Value) -> Value {
1067 match value {
1068 Value::CharArray(chars) if chars.rows == 1 => Value::String(chars.data.iter().collect()),
1069 other => other,
1070 }
1071}
1072
1073fn filters_match(handle: &HandleRef, filters: &[(String, Value)]) -> BuiltinResult<bool> {
1074 for (name, expected) in filters {
1075 let actual = normalize_find_value(property(handle, name)?);
1076 if !timer_values_equal(&actual, expected) {
1077 return Ok(false);
1078 }
1079 }
1080 Ok(true)
1081}
1082
1083fn timer_values_equal(lhs: &Value, rhs: &Value) -> bool {
1084 match (lhs, rhs) {
1085 (Value::String(a), Value::String(b)) => a == b,
1086 (Value::Num(a), Value::Num(b)) if a.is_nan() && b.is_nan() => true,
1087 (Value::Num(a), Value::Num(b)) => a == b,
1088 (Value::Bool(a), Value::Bool(b)) => a == b,
1089 _ => lhs == rhs,
1090 }
1091}
1092
1093fn handles_to_result(handles: Vec<Value>) -> BuiltinResult<Value> {
1094 if handles.len() == 1 {
1095 Ok(handles.into_iter().next().unwrap())
1096 } else {
1097 let len = handles.len();
1098 CellArray::new(handles, 1, len)
1099 .map(Value::Cell)
1100 .map_err(|err| timer_error(&TIMER_ERROR_INVALID_INPUT, err))
1101 }
1102}
1103
1104fn property(handle: &HandleRef, name: &str) -> BuiltinResult<Value> {
1105 ensure_valid_timer(handle)?;
1106 runmat_gc::gc_with_value(&handle.target, |target| {
1107 let Value::Object(object) = target else {
1108 return None;
1109 };
1110 object.properties.get(name).cloned()
1111 })
1112 .map_err(|err| timer_error(&TIMER_ERROR_GC, format!("timer: {err}")))?
1113 .ok_or_else(|| {
1114 timer_error(
1115 &TIMER_ERROR_INVALID_PROPERTY,
1116 format!("timer: missing property '{name}'"),
1117 )
1118 })
1119}
1120
1121fn set_property(handle: &HandleRef, name: &str, value: Value) -> BuiltinResult<()> {
1122 ensure_valid_timer(handle)?;
1123 let updated = runmat_gc::gc_with_value_mut(&handle.target, |target| {
1124 let Value::Object(object) = target else {
1125 return false;
1126 };
1127 object.properties.insert(name.to_string(), value);
1128 true
1129 })
1130 .map_err(|err| timer_error(&TIMER_ERROR_GC, format!("timer: {err}")))?;
1131 if updated {
1132 Ok(())
1133 } else {
1134 Err(timer_error(
1135 &TIMER_ERROR_INVALID_HANDLE,
1136 "timer: handle target is not an object",
1137 ))
1138 }
1139}
1140
1141fn string_property(handle: &HandleRef, name: &str) -> BuiltinResult<String> {
1142 match property(handle, name)? {
1143 Value::String(text) => Ok(text),
1144 Value::CharArray(chars) if chars.rows == 1 => Ok(chars.data.iter().collect()),
1145 other => Err(timer_error(
1146 &TIMER_ERROR_INVALID_PROPERTY,
1147 format!("timer: property '{name}' must be text, got {other:?}"),
1148 )),
1149 }
1150}
1151
1152fn numeric_property(handle: &HandleRef, name: &str) -> BuiltinResult<f64> {
1153 numeric_scalar(&property(handle, name)?, name)
1154}
1155
1156fn running_property(handle: &HandleRef) -> BuiltinResult<bool> {
1157 Ok(string_property(handle, "Running")?.eq_ignore_ascii_case("on"))
1158}
1159
1160fn running_is_on(object: &ObjectInstance) -> bool {
1161 matches!(
1162 object.properties.get("Running"),
1163 Some(Value::String(text)) if text.eq_ignore_ascii_case("on")
1164 )
1165}
1166
1167fn is_timer_handle(handle: &HandleRef) -> bool {
1168 handle.class_name == TIMER_CLASS
1169}
1170
1171fn ensure_valid_timer(handle: &HandleRef) -> BuiltinResult<()> {
1172 if is_timer_handle(handle) && crate::is_handle_valid(handle) {
1173 Ok(())
1174 } else {
1175 Err(timer_error(
1176 &TIMER_ERROR_INVALID_HANDLE,
1177 "timer: invalid or deleted timer handle",
1178 ))
1179 }
1180}
1181
1182fn timer_error(error: &'static BuiltinErrorDescriptor, detail: impl AsRef<str>) -> RuntimeError {
1183 let detail = detail.as_ref();
1184 let message = if detail.is_empty() {
1185 error.message.to_string()
1186 } else {
1187 detail.to_string()
1188 };
1189 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_TIMER);
1190 if let Some(identifier) = error.identifier {
1191 builder = builder.with_identifier(identifier);
1192 }
1193 builder.build()
1194}
1195
1196#[cfg(test)]
1197pub(crate) fn reset_timer_state_for_tests() {
1198 TIMER_REGISTRY.with(|registry| {
1199 for handle in registry.borrow().iter() {
1200 let _ = runmat_gc::gc_remove_root(handle.target);
1201 }
1202 registry.borrow_mut().clear();
1203 });
1204 TIMER_COUNTER.with(|counter| counter.set(0));
1205}
1206
1207#[cfg(test)]
1208mod tests {
1209 use super::*;
1210 use futures::executor::block_on;
1211 use runmat_builtins::CharArray;
1212 use std::sync::{Arc, Mutex};
1213
1214 #[test]
1215 fn timer_constructor_sets_defaults_and_name_values() {
1216 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1217 reset_timer_state_for_tests();
1218 let value = block_on(timer_builtin(vec![
1219 Value::String("Name".into()),
1220 Value::String("solver".into()),
1221 Value::String("Tag".into()),
1222 Value::String("client".into()),
1223 Value::String("StartDelay".into()),
1224 Value::Num(0.25),
1225 ]))
1226 .expect("timer");
1227 let Value::HandleObject(handle) = value else {
1228 panic!("expected timer handle");
1229 };
1230 assert_eq!(string_property(&handle, "Name").unwrap(), "solver");
1231 assert_eq!(string_property(&handle, "Tag").unwrap(), "client");
1232 assert_eq!(numeric_property(&handle, "StartDelay").unwrap(), 0.25);
1233 assert_eq!(string_property(&handle, "Running").unwrap(), "off");
1234 assert!(crate::is_handle_valid(&handle));
1235 }
1236
1237 #[test]
1238 fn timerfind_filters_visible_timers() {
1239 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1240 reset_timer_state_for_tests();
1241 let _hidden = block_on(timer_builtin(vec![
1242 Value::String("Name".into()),
1243 Value::String("hidden".into()),
1244 Value::String("ObjectVisibility".into()),
1245 Value::String("off".into()),
1246 ]))
1247 .expect("hidden timer");
1248 let visible = block_on(timer_builtin(vec![
1249 Value::String("Name".into()),
1250 Value::String("visible".into()),
1251 Value::String("Tag".into()),
1252 Value::String("batch".into()),
1253 ]))
1254 .expect("visible timer");
1255 let found = timerfind_builtin(vec![
1256 Value::String("Tag".into()),
1257 Value::String("batch".into()),
1258 ])
1259 .expect("timerfind");
1260 assert_eq!(found, visible);
1261 let all = timerfindall_builtin(vec![]).expect("timerfindall");
1262 let Value::Cell(cell) = all else {
1263 panic!("expected cell row of all timers");
1264 };
1265 assert_eq!((cell.rows, cell.cols), (1, 2));
1266 }
1267
1268 #[test]
1269 fn timer_start_runs_function_callback_and_updates_state() {
1270 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1271 reset_timer_state_for_tests();
1272 let calls = Arc::new(Mutex::new(0usize));
1273 let invoker_calls = calls.clone();
1274 let _guard = crate::user_functions::install_semantic_function_invoker(Some(Arc::new(
1275 move |_function, args, requested_outputs| {
1276 assert_eq!(requested_outputs, 0);
1277 let args = args.to_vec();
1278 let invoker_calls = Arc::clone(&invoker_calls);
1279 Box::pin(async move {
1280 assert_eq!(args.len(), 2);
1281 let Value::HandleObject(handle) = &args[0] else {
1282 panic!("expected timer handle");
1283 };
1284 assert!(is_timer_handle(handle));
1285 let Value::Struct(event) = &args[1] else {
1286 panic!("expected event struct");
1287 };
1288 assert!(event.fields.contains_key("Type"));
1289 *invoker_calls.lock().unwrap() += 1;
1290 Ok(Value::OutputList(Vec::new()))
1291 })
1292 },
1293 )));
1294 let timer = block_on(timer_builtin(vec![
1295 Value::String("TimerFcn".into()),
1296 Value::BoundFunctionHandle {
1297 name: "tick".into(),
1298 function: 1,
1299 },
1300 Value::String("ExecutionMode".into()),
1301 Value::String("fixedSpacing".into()),
1302 Value::String("TasksToExecute".into()),
1303 Value::Num(2.0),
1304 Value::String("Period".into()),
1305 Value::Num(0.002),
1306 ]))
1307 .expect("timer");
1308 block_on(timer_start_builtin(timer.clone())).expect("start");
1309 let Value::HandleObject(handle) = timer else {
1310 panic!("expected timer handle");
1311 };
1312 assert_eq!(*calls.lock().unwrap(), 2);
1313 assert_eq!(numeric_property(&handle, "TasksExecuted").unwrap(), 2.0);
1314 assert_eq!(string_property(&handle, "Running").unwrap(), "off");
1315 }
1316
1317 #[test]
1318 fn timer_delete_invalidates_and_removes_from_find_results() {
1319 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1320 reset_timer_state_for_tests();
1321 let timer = block_on(timer_builtin(vec![
1322 Value::String("TimerFcn".into()),
1323 Value::String("disp(1)".into()),
1324 ]))
1325 .expect("timer");
1326 let Value::HandleObject(handle) = timer.clone() else {
1327 panic!("expected timer handle");
1328 };
1329 block_on(timer_delete_builtin(timer)).expect("delete");
1330 assert!(!crate::is_handle_valid(&handle));
1331 let found = timerfindall_builtin(vec![]).expect("timerfindall");
1332 let Value::Cell(cell) = found else {
1333 panic!("expected empty cell");
1334 };
1335 assert_eq!((cell.rows, cell.cols), (1, 0));
1336 }
1337
1338 #[test]
1339 fn timer_rejects_invalid_property_values() {
1340 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1341 reset_timer_state_for_tests();
1342 let err = block_on(timer_builtin(vec![
1343 Value::CharArray(CharArray::new_row("Period")),
1344 Value::Num(0.0),
1345 ]))
1346 .unwrap_err();
1347 assert_eq!(
1348 err.identifier().map(str::to_string),
1349 Some("RunMat:timer:InvalidProperty".to_string())
1350 );
1351 }
1352
1353 #[test]
1354 fn timer_dependent_setters_validate_post_construction_values() {
1355 let _lock = TIMER_TEST_LOCK.lock().unwrap();
1356 reset_timer_state_for_tests();
1357 let timer = block_on(timer_builtin(vec![])).expect("timer");
1358 let Value::HandleObject(handle) = timer else {
1359 panic!("expected timer handle");
1360 };
1361 let object = runmat_gc::gc_clone_value(&handle.target).expect("timer object");
1362 let err = block_on(set_timer_property_object(
1363 object.clone(),
1364 "Period",
1365 Value::Num(0.0),
1366 ))
1367 .unwrap_err();
1368 assert_eq!(
1369 err.identifier().map(str::to_string),
1370 Some("RunMat:timer:InvalidProperty".to_string())
1371 );
1372
1373 let updated = block_on(set_timer_property_object(
1374 object,
1375 "Tag",
1376 Value::CharArray(CharArray::new_row("batch")),
1377 ))
1378 .expect("valid tag update");
1379 let Value::Object(updated) = updated else {
1380 panic!("expected updated timer object");
1381 };
1382 assert_eq!(
1383 updated.properties.get("Tag"),
1384 Some(&Value::String("batch".to_string()))
1385 );
1386 }
1387}