1use runmat_builtins::{
7 BuiltinExtensionDescriptor, BuiltinExtensionMode, BuiltinIntegerBackendRule,
8 BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
9 BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
10 BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
11};
12use runmat_time::Instant;
13use std::cmp::Ordering;
14
15use runmat_builtins::{
16 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinOutputMode,
17 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
18};
19use runmat_macros::runtime_builtin;
20use runmat_value::Value;
21
22use crate::builtins::common::gpu_helpers::gather_value_async;
23use crate::builtins::common::spec::{
24 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
25 ReductionNaN, ResidencyPolicy, ShapeRequirements,
26};
27use crate::builtins::common::tensor;
28use crate::builtins::timing::type_resolvers::timeit_type;
29
30const TARGET_BATCH_SECONDS: f64 = 0.005;
31const MAX_BATCH_SECONDS: f64 = 0.25;
32const LOOP_COUNT_LIMIT: usize = 1 << 20;
33const MIN_SAMPLE_COUNT: usize = 7;
34const MAX_SAMPLE_COUNT: usize = 21;
35const BUILTIN_NAME: &str = "timeit";
36
37const INTEGER_NUM_OUTPUTS_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
38 id: "timeit-typed-integer-num-outputs",
39 mode: BuiltinExtensionMode::RunMatOnly,
40 description: "timeit with a typed-integer numOutputs value is a RunMat extension",
41 error_identifier: Some("RunMat:compatibility:TimeitIntegerNumOutputsExtension"),
42};
43const EXPLICIT_GPU_NUM_OUTPUTS_EXTENSION: BuiltinExtensionDescriptor = BuiltinExtensionDescriptor {
44 id: "timeit-explicit-gpu-num-outputs",
45 mode: BuiltinExtensionMode::RunMatOnly,
46 description: "timeit with an explicit gpuArray numOutputs value is a RunMat extension",
47 error_identifier: Some("RunMat:compatibility:TimeitExplicitGpuNumOutputsExtension"),
48};
49pub const TIMEIT_EXTENSIONS: [BuiltinExtensionDescriptor; 2] = [
50 INTEGER_NUM_OUTPUTS_EXTENSION,
51 EXPLICIT_GPU_NUM_OUTPUTS_EXTENSION,
52];
53
54const TIMEIT_INTEGER_NUM_OUTPUTS_INPUT: [BuiltinIntegerInputCapability; 1] =
55 [BuiltinIntegerInputCapability {
56 name: "numOutputs",
57 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
58 availability: BuiltinIntegerInputAvailability::RunMatOnly,
59 scalar_double: BuiltinIntegerScalarDoubleRule::Allowed,
60 notes: "The public contract specifies an integer-valued output count without publishing native integer classes; ordinary integer-valued double input remains the compatibility form.",
61 }];
62pub const TIMEIT_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
63 [BuiltinIntegerCapabilityDescriptor {
64 form: "t = timeit(f, typed_integer_numOutputs)",
65 inputs: &TIMEIT_INTEGER_NUM_OUTPUTS_INPUT,
66 computation_domain: BuiltinIntegerComputationDomain::Structural,
67 output_class: BuiltinIntegerOutputClassRule::Double,
68 overflow: BuiltinIntegerOverflowRule::Error,
69 backend: BuiltinIntegerBackendRule::GatherFallback,
70 overload: BuiltinIntegerOverloadKind::StructuralParameter,
71 notes: "Admitted native integers are decoded exactly as a nonnegative platform output count. Automatic residency gathers transparently; explicit gpuArray intent is independently gated.",
72 }];
73
74const TIMEIT_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
75 name: "t",
76 ty: BuiltinParamType::NumericScalar,
77 arity: BuiltinParamArity::Required,
78 default: None,
79 description: "Median execution time per invocation in seconds.",
80}];
81
82const TIMEIT_INPUTS_ONE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
83 name: "f",
84 ty: BuiltinParamType::Any,
85 arity: BuiltinParamArity::Required,
86 default: None,
87 description: "Zero-input function handle to benchmark.",
88}];
89
90const TIMEIT_INPUTS_TWO: [BuiltinParamDescriptor; 2] = [
91 BuiltinParamDescriptor {
92 name: "f",
93 ty: BuiltinParamType::Any,
94 arity: BuiltinParamArity::Required,
95 default: None,
96 description: "Zero-input function handle to benchmark.",
97 },
98 BuiltinParamDescriptor {
99 name: "numOutputs",
100 ty: BuiltinParamType::IntegerScalar,
101 arity: BuiltinParamArity::Optional,
102 default: Some("1"),
103 description: "Requested output count for invoking the benchmarked handle.",
104 },
105];
106
107const TIMEIT_SIGNATURES: [BuiltinSignatureDescriptor; 2] = [
108 BuiltinSignatureDescriptor {
109 label: "t = timeit(f)",
110 inputs: &TIMEIT_INPUTS_ONE,
111 outputs: &TIMEIT_OUTPUT,
112 },
113 BuiltinSignatureDescriptor {
114 label: "t = timeit(f, numOutputs)",
115 inputs: &TIMEIT_INPUTS_TWO,
116 outputs: &TIMEIT_OUTPUT,
117 },
118];
119
120const TIMEIT_ERROR_TOO_MANY_INPUTS: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
121 code: "RM.TIMEIT.TOO_MANY_INPUTS",
122 identifier: Some("RunMat:timeit:TooManyInputs"),
123 when: "More than two input arguments are supplied.",
124 message: "timeit: too many input arguments",
125};
126
127const TIMEIT_ERROR_NUM_OUTPUTS_SCALAR: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
128 code: "RM.TIMEIT.NUM_OUTPUTS_SCALAR",
129 identifier: Some("RunMat:timeit:NumOutputsScalar"),
130 when: "numOutputs is not a scalar numeric/integer value.",
131 message: "timeit: numOutputs must be a scalar numeric value",
132};
133
134const TIMEIT_ERROR_NUM_OUTPUTS_FINITE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
135 code: "RM.TIMEIT.NUM_OUTPUTS_FINITE",
136 identifier: Some("RunMat:timeit:NumOutputsFinite"),
137 when: "numOutputs is NaN or infinite.",
138 message: "timeit: numOutputs must be finite",
139};
140
141const TIMEIT_ERROR_NUM_OUTPUTS_NONNEG: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
142 code: "RM.TIMEIT.NUM_OUTPUTS_NONNEGATIVE",
143 identifier: Some("RunMat:timeit:NumOutputsNonnegative"),
144 when: "numOutputs is negative.",
145 message: "timeit: numOutputs must be a nonnegative integer",
146};
147
148const TIMEIT_ERROR_NUM_OUTPUTS_INTEGER: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
149 code: "RM.TIMEIT.NUM_OUTPUTS_INTEGER",
150 identifier: Some("RunMat:timeit:NumOutputsInteger"),
151 when: "numOutputs has a non-integer numeric value.",
152 message: "timeit: numOutputs must be an integer value",
153};
154
155const TIMEIT_ERROR_EMPTY_HANDLE: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
156 code: "RM.TIMEIT.EMPTY_HANDLE",
157 identifier: Some("RunMat:timeit:EmptyFunctionHandle"),
158 when: "A function-handle string or payload is empty after trimming.",
159 message: "timeit: empty function handle string",
160};
161
162const TIMEIT_ERROR_EXPECTS_AT_HANDLE_STRING: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
163 code: "RM.TIMEIT.EXPECTS_AT_HANDLE_STRING",
164 identifier: Some("RunMat:timeit:ExpectedAtHandleString"),
165 when: "A string/char function handle does not begin with '@'.",
166 message: "timeit: expected a function handle string beginning with '@'",
167};
168
169const TIMEIT_ERROR_HANDLE_KIND: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
170 code: "RM.TIMEIT.HANDLE_KIND",
171 identifier: Some("RunMat:timeit:HandleKind"),
172 when: "Function handle argument is not a scalar string/char or callable handle value.",
173 message: "timeit: function handle must be a string scalar or function handle",
174};
175
176const TIMEIT_ERROR_FIRST_ARG_KIND: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
177 code: "RM.TIMEIT.FIRST_ARG_KIND",
178 identifier: Some("RunMat:timeit:FirstArgKind"),
179 when: "First argument is not a function handle value.",
180 message: "timeit: first argument must be a function handle",
181};
182
183const TIMEIT_ERRORS: [BuiltinErrorDescriptor; 9] = [
184 TIMEIT_ERROR_TOO_MANY_INPUTS,
185 TIMEIT_ERROR_NUM_OUTPUTS_SCALAR,
186 TIMEIT_ERROR_NUM_OUTPUTS_FINITE,
187 TIMEIT_ERROR_NUM_OUTPUTS_NONNEG,
188 TIMEIT_ERROR_NUM_OUTPUTS_INTEGER,
189 TIMEIT_ERROR_EMPTY_HANDLE,
190 TIMEIT_ERROR_EXPECTS_AT_HANDLE_STRING,
191 TIMEIT_ERROR_HANDLE_KIND,
192 TIMEIT_ERROR_FIRST_ARG_KIND,
193];
194
195pub const TIMEIT_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
196 signatures: &TIMEIT_SIGNATURES,
197 output_mode: BuiltinOutputMode::Fixed,
198 completion_policy: BuiltinCompletionPolicy::Public,
199 errors: &TIMEIT_ERRORS,
200};
201
202fn timeit_error_with_message(
203 message: impl Into<String>,
204 error: &'static BuiltinErrorDescriptor,
205) -> crate::RuntimeError {
206 let mut builder = crate::build_runtime_error(message).with_builtin(BUILTIN_NAME);
207 if let Some(identifier) = error.identifier {
208 builder = builder.with_identifier(identifier);
209 }
210 builder.build()
211}
212
213#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::timing::timeit")]
214pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
215 name: "timeit",
216 op_kind: GpuOpKind::Custom("timer"),
217 supported_precisions: &[],
218 broadcast: BroadcastSemantics::None,
219 provider_hooks: &[],
220 constant_strategy: ConstantStrategy::InlineLiteral,
221 residency: ResidencyPolicy::GatherImmediately,
222 nan_mode: ReductionNaN::Include,
223 two_pass_threshold: None,
224 workgroup_size: None,
225 accepts_nan_mode: false,
226 notes: "Host-side helper; GPU kernels execute only if invoked by the timed function.",
227};
228
229#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::timing::timeit")]
230pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
231 name: "timeit",
232 shape: ShapeRequirements::Any,
233 constant_strategy: ConstantStrategy::InlineLiteral,
234 elementwise: None,
235 reduction: None,
236 emits_nan: false,
237 notes: "Timing helper; excluded from fusion planning.",
238};
239
240#[runtime_builtin(
241 name = "timeit",
242 category = "timing",
243 summary = "Measure runtime of zero-argument function handles using repeated execution.",
244 keywords = "timeit,benchmark,timing,performance,gpu",
245 accel = "helper",
246 type_resolver(timeit_type),
247 descriptor(crate::builtins::timing::timeit::TIMEIT_DESCRIPTOR),
248 extensions(crate::builtins::timing::timeit::TIMEIT_EXTENSIONS),
249 integer_capabilities(crate::builtins::timing::timeit::TIMEIT_INTEGER_CAPABILITIES),
250 builtin_path = "crate::builtins::timing::timeit"
251)]
252async fn timeit_builtin(func: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
253 let requested_outputs = parse_num_outputs(&rest).await?;
254 let callable = prepare_callable(func, requested_outputs)?;
255
256 callable.invoke().await?;
258
259 let loop_count = determine_loop_count(&callable).await?;
260 let samples = collect_samples(&callable, loop_count).await?;
261 if samples.is_empty() {
262 return Ok(Value::Num(0.0));
263 }
264
265 Ok(Value::Num(compute_median(samples)))
266}
267
268async fn parse_num_outputs(rest: &[Value]) -> Result<Option<usize>, crate::RuntimeError> {
269 match rest.len() {
270 0 => Ok(None),
271 1 => {
272 let value = &rest[0];
273 if crate::builtins::common::validation::value_contains_explicit_gpu(value) {
274 crate::compatibility::ensure_builtin_extension_enabled(
275 &EXPLICIT_GPU_NUM_OUTPUTS_EXTENSION,
276 BUILTIN_NAME,
277 )?;
278 }
279 if crate::builtins::common::validation::value_contains_native_integer_class(value) {
280 crate::compatibility::ensure_builtin_extension_enabled(
281 &INTEGER_NUM_OUTPUTS_EXTENSION,
282 BUILTIN_NAME,
283 )?;
284 }
285 let value = gather_value_async(value).await.map_err(|error| {
286 timeit_error_with_message(error.message(), &TIMEIT_ERROR_NUM_OUTPUTS_SCALAR)
287 })?;
288 parse_non_negative_integer(&value).map(Some)
289 }
290 _ => Err(timeit_error_with_message(
291 TIMEIT_ERROR_TOO_MANY_INPUTS.message,
292 &TIMEIT_ERROR_TOO_MANY_INPUTS,
293 )),
294 }
295}
296
297fn parse_non_negative_integer(value: &Value) -> Result<usize, crate::RuntimeError> {
298 if let Some(integer) = tensor::scalar_integer_value(value) {
299 return integer.try_to_usize().ok_or_else(|| {
300 timeit_error_with_message(
301 TIMEIT_ERROR_NUM_OUTPUTS_NONNEG.message,
302 &TIMEIT_ERROR_NUM_OUTPUTS_NONNEG,
303 )
304 });
305 }
306 match value {
307 Value::Num(n) => {
308 if !n.is_finite() {
309 return Err(timeit_error_with_message(
310 TIMEIT_ERROR_NUM_OUTPUTS_FINITE.message,
311 &TIMEIT_ERROR_NUM_OUTPUTS_FINITE,
312 ));
313 }
314 if *n < 0.0 {
315 return Err(timeit_error_with_message(
316 TIMEIT_ERROR_NUM_OUTPUTS_NONNEG.message,
317 &TIMEIT_ERROR_NUM_OUTPUTS_NONNEG,
318 ));
319 }
320 let rounded = n.round();
321 if (rounded - n).abs() > f64::EPSILON {
322 return Err(timeit_error_with_message(
323 TIMEIT_ERROR_NUM_OUTPUTS_INTEGER.message,
324 &TIMEIT_ERROR_NUM_OUTPUTS_INTEGER,
325 ));
326 }
327 if !fits_platform_usize(rounded) {
328 return Err(timeit_error_with_message(
329 TIMEIT_ERROR_NUM_OUTPUTS_NONNEG.message,
330 &TIMEIT_ERROR_NUM_OUTPUTS_NONNEG,
331 ));
332 }
333 Ok(rounded as usize)
334 }
335 _ => Err(timeit_error_with_message(
336 TIMEIT_ERROR_NUM_OUTPUTS_SCALAR.message,
337 &TIMEIT_ERROR_NUM_OUTPUTS_SCALAR,
338 )),
339 }
340}
341
342fn fits_platform_usize(value: f64) -> bool {
343 value < usize::MAX as f64 || (usize::BITS < 64 && value == usize::MAX as f64)
344}
345
346async fn determine_loop_count(callable: &TimeitCallable) -> Result<usize, crate::RuntimeError> {
347 let mut loops = 1usize;
348 loop {
349 let elapsed = run_batch(callable, loops).await?;
350 if elapsed >= TARGET_BATCH_SECONDS
351 || elapsed >= MAX_BATCH_SECONDS
352 || loops >= LOOP_COUNT_LIMIT
353 {
354 return Ok(loops);
355 }
356 loops = loops.saturating_mul(2);
357 if loops == 0 {
358 return Ok(LOOP_COUNT_LIMIT);
359 }
360 }
361}
362
363async fn collect_samples(
364 callable: &TimeitCallable,
365 loop_count: usize,
366) -> Result<Vec<f64>, crate::RuntimeError> {
367 let mut samples = Vec::with_capacity(MIN_SAMPLE_COUNT);
368 while samples.len() < MIN_SAMPLE_COUNT {
369 let elapsed = run_batch(callable, loop_count).await?;
370 let per_iter = elapsed / loop_count as f64;
371 samples.push(per_iter);
372 if samples.len() >= MAX_SAMPLE_COUNT || elapsed >= MAX_BATCH_SECONDS {
373 break;
374 }
375 }
376 Ok(samples)
377}
378
379async fn run_batch(
380 callable: &TimeitCallable,
381 loop_count: usize,
382) -> Result<f64, crate::RuntimeError> {
383 let start = Instant::now();
384 for _ in 0..loop_count {
385 let value = callable.invoke().await?;
386 drop(value);
387 }
388 Ok(start.elapsed().as_secs_f64())
389}
390
391fn compute_median(mut samples: Vec<f64>) -> f64 {
392 if samples.is_empty() {
393 return 0.0;
394 }
395 samples.sort_by(|a, b| match (a.is_nan(), b.is_nan()) {
396 (true, true) => Ordering::Equal,
397 (true, false) => Ordering::Greater,
398 (false, true) => Ordering::Less,
399 (false, false) => a.partial_cmp(b).unwrap_or_else(|| {
400 if a < b {
401 Ordering::Less
402 } else {
403 Ordering::Greater
404 }
405 }),
406 });
407 let mid = samples.len() / 2;
408 if samples.len() % 2 == 1 {
409 samples[mid]
410 } else {
411 (samples[mid - 1] + samples[mid]) * 0.5
412 }
413}
414
415#[derive(Clone, Debug)]
416struct TimeitCallable {
417 handle: Value,
418 num_outputs: Option<usize>,
419}
420
421impl TimeitCallable {
422 async fn invoke(&self) -> Result<Value, crate::RuntimeError> {
423 let requested_outputs = self.num_outputs.unwrap_or(1);
424 let value =
425 crate::call_feval_async_with_outputs(self.handle.clone(), &[], requested_outputs)
426 .await?;
427 drop(value);
428 Ok(Value::Num(0.0))
429 }
430}
431
432fn prepare_callable(
433 func: Value,
434 num_outputs: Option<usize>,
435) -> Result<TimeitCallable, crate::RuntimeError> {
436 fn normalize_name(name: &str) -> Result<String, crate::RuntimeError> {
437 let trimmed = name.trim();
438 if trimmed.is_empty() {
439 Err(timeit_error_with_message(
440 TIMEIT_ERROR_EMPTY_HANDLE.message,
441 &TIMEIT_ERROR_EMPTY_HANDLE,
442 ))
443 } else {
444 Ok(trimmed.to_string())
445 }
446 }
447
448 fn canonicalize_text_handle(handle: String) -> Value {
449 let name = handle.strip_prefix('@').unwrap_or(handle.as_str());
450 handle_for_name(name).unwrap_or(Value::String(handle))
451 }
452
453 match func {
454 Value::String(text) => parse_handle_string(&text).map(|handle| TimeitCallable {
455 handle: canonicalize_text_handle(handle),
456 num_outputs,
457 }),
458 Value::CharArray(arr) => {
459 if arr.rows != 1 {
460 Err(timeit_error_with_message(
461 TIMEIT_ERROR_HANDLE_KIND.message,
462 &TIMEIT_ERROR_HANDLE_KIND,
463 ))
464 } else {
465 let text: String = arr.data.iter().collect();
466 parse_handle_string(&text).map(|handle| TimeitCallable {
467 handle: canonicalize_text_handle(handle),
468 num_outputs,
469 })
470 }
471 }
472 Value::StringArray(sa) => {
473 if sa.data.len() == 1 {
474 parse_handle_string(&sa.data[0]).map(|handle| TimeitCallable {
475 handle: canonicalize_text_handle(handle),
476 num_outputs,
477 })
478 } else {
479 Err(timeit_error_with_message(
480 TIMEIT_ERROR_HANDLE_KIND.message,
481 &TIMEIT_ERROR_HANDLE_KIND,
482 ))
483 }
484 }
485 Value::FunctionHandle(name) => {
486 let normalized = normalize_name(&name)?;
487 Ok(TimeitCallable {
488 handle: handle_for_name(&normalized)
489 .unwrap_or_else(|| Value::String(format!("@{normalized}"))),
490 num_outputs,
491 })
492 }
493 Value::ExternalFunctionHandle(name) => {
494 let normalized = normalize_name(&name)?;
495 Ok(TimeitCallable {
496 handle: if crate::is_well_formed_qualified_name(&normalized) {
497 handle_for_name(&normalized)
498 .unwrap_or_else(|| Value::ExternalFunctionHandle(normalized))
499 } else {
500 Value::ExternalFunctionHandle(normalized)
501 },
502 num_outputs,
503 })
504 }
505 Value::BoundFunctionHandle { name, function } => {
506 let normalized = normalize_name(&name)?;
507 Ok(TimeitCallable {
508 handle: Value::BoundFunctionHandle {
509 name: normalized,
510 function,
511 },
512 num_outputs,
513 })
514 }
515 Value::Closure(mut closure) => Ok(TimeitCallable {
516 handle: {
517 if closure.bound_function.is_none() {
518 if let Some(function) = crate::user_functions::resolve_semantic_function_by_name(
519 &closure.function_name,
520 ) {
521 closure.bound_function = Some(function);
522 }
523 }
524 Value::Closure(closure)
525 },
526 num_outputs,
527 }),
528 other => Err(timeit_error_with_message(
529 format!("timeit: first argument must be a function handle, got {other:?}"),
530 &TIMEIT_ERROR_FIRST_ARG_KIND,
531 )),
532 }
533}
534
535fn handle_for_name(name: &str) -> Option<Value> {
536 let function = crate::user_functions::resolve_semantic_function_by_name(name)?;
537 Some(Value::BoundFunctionHandle {
538 name: name.to_string(),
539 function,
540 })
541}
542
543fn parse_handle_string(text: &str) -> Result<String, crate::RuntimeError> {
544 let trimmed = text.trim();
545 if let Some(rest) = trimmed.strip_prefix('@') {
546 if rest.trim().is_empty() {
547 Err(timeit_error_with_message(
548 TIMEIT_ERROR_EMPTY_HANDLE.message,
549 &TIMEIT_ERROR_EMPTY_HANDLE,
550 ))
551 } else {
552 Ok(format!("@{}", rest.trim()))
553 }
554 } else {
555 Err(timeit_error_with_message(
556 TIMEIT_ERROR_EXPECTS_AT_HANDLE_STRING.message,
557 &TIMEIT_ERROR_EXPECTS_AT_HANDLE_STRING,
558 ))
559 }
560}
561
562#[cfg(test)]
563pub(crate) mod tests {
564 use super::*;
565
566 #[test]
567 fn output_count_preserves_representable_uint64() {
568 assert_eq!(
569 parse_non_negative_integer(&Value::Int(IntValue::U64(u64::MAX))).ok(),
570 usize::try_from(u64::MAX).ok()
571 );
572 assert!(parse_non_negative_integer(&Value::Int(IntValue::I64(-1))).is_err());
573 }
574
575 #[test]
576 fn output_count_reads_typed_integer_scalar_storage_exactly() {
577 let count = Tensor::new_integer(IntegerStorage::U16(vec![2]), vec![1, 1])
578 .expect("typed output count");
579
580 assert_eq!(
581 parse_non_negative_integer(&Value::Tensor(count)).unwrap(),
582 2
583 );
584
585 let negative = Tensor::new_integer(IntegerStorage::I16(vec![-1]), vec![1, 1])
586 .expect("negative output count");
587 assert!(parse_non_negative_integer(&Value::Tensor(negative)).is_err());
588 }
589
590 #[test]
591 fn output_count_rejects_unrepresentable_double_boundary() {
592 let boundary = if usize::BITS == 64 {
593 usize::MAX as f64
594 } else {
595 (usize::MAX as f64) + 1.0
596 };
597 assert!(parse_non_negative_integer(&Value::Num(boundary)).is_err());
598 }
599
600 use futures::executor::block_on;
601 use runmat_value::{Closure, IntValue, IntegerStorage, Tensor};
602 use std::sync::atomic::{AtomicUsize, Ordering};
603 use std::sync::Arc;
604
605 const TIMEIT_HELPER_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
606 name: "y",
607 ty: BuiltinParamType::NumericScalar,
608 arity: BuiltinParamArity::Required,
609 default: None,
610 description: "Helper scalar return value.",
611 }];
612
613 const TIMEIT_HELPER_SIGNATURES: [BuiltinSignatureDescriptor; 1] =
614 [BuiltinSignatureDescriptor {
615 label: "y = __timeit_helper()",
616 inputs: &[],
617 outputs: &TIMEIT_HELPER_OUTPUT,
618 }];
619
620 const TIMEIT_HELPER_ERRORS: [BuiltinErrorDescriptor; 0] = [];
621
622 pub const TIMEIT_TEST_HELPER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
623 signatures: &TIMEIT_HELPER_SIGNATURES,
624 output_mode: BuiltinOutputMode::Fixed,
625 completion_policy: BuiltinCompletionPolicy::HiddenInternal,
626 errors: &TIMEIT_HELPER_ERRORS,
627 };
628
629 static COUNTER_DEFAULT: AtomicUsize = AtomicUsize::new(0);
630 static COUNTER_NUM_OUTPUTS: AtomicUsize = AtomicUsize::new(0);
631 static COUNTER_INVALID: AtomicUsize = AtomicUsize::new(0);
632 static COUNTER_ZERO_OUTPUTS: AtomicUsize = AtomicUsize::new(0);
633
634 #[runtime_builtin(
635 name = "__timeit_helper_counter_default",
636 type_resolver(crate::builtins::timing::type_resolvers::timeit_type),
637 descriptor(crate::builtins::timing::timeit::tests::TIMEIT_TEST_HELPER_DESCRIPTOR),
638 builtin_path = "crate::builtins::timing::timeit::tests"
639 )]
640 async fn helper_counter_default() -> crate::BuiltinResult<Value> {
641 COUNTER_DEFAULT.fetch_add(1, Ordering::SeqCst);
642 Ok(Value::Num(1.0))
643 }
644
645 #[runtime_builtin(
646 name = "__timeit_helper_counter_outputs",
647 type_resolver(crate::builtins::timing::type_resolvers::timeit_type),
648 descriptor(crate::builtins::timing::timeit::tests::TIMEIT_TEST_HELPER_DESCRIPTOR),
649 builtin_path = "crate::builtins::timing::timeit::tests"
650 )]
651 async fn helper_counter_outputs() -> crate::BuiltinResult<Value> {
652 COUNTER_NUM_OUTPUTS.fetch_add(1, Ordering::SeqCst);
653 Ok(Value::Num(1.0))
654 }
655
656 #[runtime_builtin(
657 name = "__timeit_helper_counter_invalid",
658 type_resolver(crate::builtins::timing::type_resolvers::timeit_type),
659 descriptor(crate::builtins::timing::timeit::tests::TIMEIT_TEST_HELPER_DESCRIPTOR),
660 builtin_path = "crate::builtins::timing::timeit::tests"
661 )]
662 async fn helper_counter_invalid() -> crate::BuiltinResult<Value> {
663 COUNTER_INVALID.fetch_add(1, Ordering::SeqCst);
664 Ok(Value::Num(1.0))
665 }
666
667 #[runtime_builtin(
668 name = "__timeit_helper_zero_outputs",
669 type_resolver(crate::builtins::timing::type_resolvers::timeit_type),
670 descriptor(crate::builtins::timing::timeit::tests::TIMEIT_TEST_HELPER_DESCRIPTOR),
671 builtin_path = "crate::builtins::timing::timeit::tests"
672 )]
673 async fn helper_counter_zero_outputs() -> crate::BuiltinResult<Value> {
674 COUNTER_ZERO_OUTPUTS.fetch_add(1, Ordering::SeqCst);
675 Ok(Value::Num(0.0))
676 }
677
678 fn default_handle() -> Value {
679 Value::String("@__timeit_helper_counter_default".to_string())
680 }
681
682 fn assert_timeit_error_contains(err: &crate::RuntimeError, needle: &str) {
683 let message = err.message().to_ascii_lowercase();
684 assert!(
685 message.contains(&needle.to_ascii_lowercase()),
686 "unexpected error text: {}",
687 err.message()
688 );
689 }
690
691 fn assert_timeit_error_identifier(err: &crate::RuntimeError, identifier: &'static str) {
692 assert_eq!(err.identifier(), Some(identifier), "{}", err.message());
693 }
694
695 fn outputs_handle() -> Value {
696 Value::String("@__timeit_helper_counter_outputs".to_string())
697 }
698
699 fn invalid_handle() -> Value {
700 Value::String("@__timeit_helper_counter_invalid".to_string())
701 }
702
703 fn zero_outputs_handle() -> Value {
704 Value::String("@__timeit_helper_zero_outputs".to_string())
705 }
706
707 #[test]
708 fn timeit_test_helper_descriptor_is_attached_shape() {
709 assert_eq!(
710 TIMEIT_TEST_HELPER_DESCRIPTOR.signatures[0].label,
711 "y = __timeit_helper()"
712 );
713 }
714
715 #[test]
716 fn timeit_declares_integer_output_count_compatibility_metadata() {
717 let builtin = runmat_builtins::builtin_function_by_name("timeit").unwrap();
718 assert_eq!(builtin.integer_capabilities.len(), 1);
719 assert_eq!(builtin.extensions.len(), 2);
720 assert_eq!(builtin.integer_capabilities[0].inputs[0].classes.len(), 8);
721 }
722
723 #[test]
724 fn timeit_accepts_external_function_handle() {
725 let callable = prepare_callable(
726 Value::ExternalFunctionHandle("pkg.callback".to_string()),
727 Some(2),
728 )
729 .expect("timeit should accept external function handle");
730 assert_eq!(
731 callable.handle,
732 Value::ExternalFunctionHandle("pkg.callback".to_string())
733 );
734 assert_eq!(callable.num_outputs, Some(2));
735 }
736
737 #[test]
738 fn timeit_rejects_empty_function_handle_name_value() {
739 let err = prepare_callable(Value::FunctionHandle(" ".to_string()), None)
740 .expect_err("timeit should reject empty function-handle payload name");
741 assert_timeit_error_contains(&err, "empty function handle");
742 assert_timeit_error_identifier(&err, TIMEIT_ERROR_EMPTY_HANDLE.identifier.unwrap());
743 }
744
745 #[test]
746 fn timeit_rejects_empty_external_function_handle_name_value() {
747 let err = prepare_callable(Value::ExternalFunctionHandle(" ".to_string()), None)
748 .expect_err("timeit should reject empty external function-handle payload name");
749 assert_timeit_error_contains(&err, "empty function handle");
750 assert_timeit_error_identifier(&err, TIMEIT_ERROR_EMPTY_HANDLE.identifier.unwrap());
751 }
752
753 #[test]
754 fn timeit_trims_function_handle_name_for_semantic_resolution() {
755 let _resolver_guard =
756 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
757 (name == "__timeit_helper_counter_default").then_some(188)
758 })));
759 let callable = prepare_callable(
760 Value::FunctionHandle(" __timeit_helper_counter_default ".to_string()),
761 None,
762 )
763 .expect("timeit should normalize function-handle payload name");
764 assert_eq!(
765 callable.handle,
766 Value::BoundFunctionHandle {
767 name: "__timeit_helper_counter_default".to_string(),
768 function: 188,
769 }
770 );
771 }
772
773 #[test]
774 fn timeit_callable_invoke_honors_multi_requested_outputs() {
775 let _invoker_guard = crate::user_functions::install_semantic_function_invoker(Some(
776 Arc::new(|function, args, requested_outputs| {
777 assert_eq!(function, 612);
778 assert!(args.is_empty());
779 assert_eq!(requested_outputs, 3);
780 Box::pin(async {
781 Ok(Value::OutputList(vec![
782 Value::Num(1.0),
783 Value::Num(2.0),
784 Value::Num(3.0),
785 ]))
786 })
787 }),
788 ));
789
790 let callable = prepare_callable(
791 Value::BoundFunctionHandle {
792 name: "function_target".to_string(),
793 function: 612,
794 },
795 Some(3),
796 )
797 .expect("timeit should accept semantic callback handles");
798
799 let invoked = block_on(callable.invoke()).expect("timeit callable invoke should succeed");
800 assert_eq!(invoked, Value::Num(0.0));
801 }
802
803 #[test]
804 fn timeit_string_handle_prefers_semantic_resolver_identity() {
805 let _resolver_guard =
806 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
807 (name == "__timeit_helper_counter_default").then_some(87)
808 })));
809 let callable = prepare_callable(
810 Value::String("@__timeit_helper_counter_default".to_string()),
811 None,
812 )
813 .expect("timeit should accept string function handle");
814 assert_eq!(
815 callable.handle,
816 Value::BoundFunctionHandle {
817 name: "__timeit_helper_counter_default".to_string(),
818 function: 87,
819 }
820 );
821 }
822
823 #[test]
824 fn timeit_char_handle_prefers_semantic_resolver_identity() {
825 let _resolver_guard =
826 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
827 (name == "__timeit_helper_counter_default").then_some(88)
828 })));
829 let callable = prepare_callable(
830 Value::CharArray(runmat_value::CharArray::new_row(
831 "@__timeit_helper_counter_default",
832 )),
833 None,
834 )
835 .expect("timeit should accept char function handle");
836 assert_eq!(
837 callable.handle,
838 Value::BoundFunctionHandle {
839 name: "__timeit_helper_counter_default".to_string(),
840 function: 88,
841 }
842 );
843 }
844
845 #[test]
846 fn timeit_external_function_handle_prefers_semantic_resolver_identity() {
847 let _resolver_guard =
848 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
849 (name == "pkg.callback").then_some(86)
850 })));
851 let callable = prepare_callable(
852 Value::ExternalFunctionHandle("pkg.callback".to_string()),
853 Some(2),
854 )
855 .expect("timeit should accept external function handle");
856 assert_eq!(
857 callable.handle,
858 Value::BoundFunctionHandle {
859 name: "pkg.callback".to_string(),
860 function: 86,
861 }
862 );
863 assert_eq!(callable.num_outputs, Some(2));
864 }
865
866 #[test]
867 fn timeit_accepts_semantic_function_handle() {
868 let callable = prepare_callable(
869 Value::BoundFunctionHandle {
870 name: "function_target".to_string(),
871 function: 41,
872 },
873 Some(1),
874 )
875 .expect("timeit should accept semantic function handle");
876 assert_eq!(
877 callable.handle,
878 Value::BoundFunctionHandle {
879 name: "function_target".to_string(),
880 function: 41,
881 }
882 );
883 assert_eq!(callable.num_outputs, Some(1));
884 }
885
886 #[test]
887 fn timeit_name_only_closure_prefers_semantic_resolver_identity() {
888 let _resolver_guard =
889 crate::user_functions::install_semantic_function_resolver(Some(Arc::new(|name| {
890 (name == "__timeit_helper_counter_default").then_some(89)
891 })));
892 let callable = prepare_callable(
893 Value::Closure(Closure {
894 function_name: "__timeit_helper_counter_default".to_string(),
895 bound_function: None,
896 captures: vec![Value::Num(9.0)],
897 }),
898 None,
899 )
900 .expect("timeit should accept closure callback");
901 assert_eq!(
902 callable.handle,
903 Value::Closure(Closure {
904 function_name: "__timeit_helper_counter_default".to_string(),
905 bound_function: Some(89),
906 captures: vec![Value::Num(9.0)],
907 })
908 );
909 }
910
911 #[test]
912 fn timeit_name_only_closure_without_resolver_keeps_name_shaped_identity() {
913 let callable = prepare_callable(
914 Value::Closure(Closure {
915 function_name: "__timeit_helper_counter_default".to_string(),
916 bound_function: None,
917 captures: vec![Value::Num(9.0)],
918 }),
919 None,
920 )
921 .expect("timeit should accept closure callback");
922 assert_eq!(
923 callable.handle,
924 Value::Closure(Closure {
925 function_name: "__timeit_helper_counter_default".to_string(),
926 bound_function: None,
927 captures: vec![Value::Num(9.0)],
928 })
929 );
930 }
931
932 #[test]
933 fn timeit_external_function_handle_surfaces_undefined_function() {
934 let err = block_on(timeit_builtin(
935 Value::ExternalFunctionHandle("pkg.missing_callback".to_string()),
936 Vec::new(),
937 ))
938 .expect_err("unresolved external callback should fail");
939 assert_eq!(err.identifier(), Some("RunMat:UndefinedFunction"));
940 }
941
942 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
943 #[test]
944 fn timeit_measures_time() {
945 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
946 COUNTER_DEFAULT.store(0, Ordering::SeqCst);
947 let result = block_on(timeit_builtin(default_handle(), Vec::new())).expect("timeit");
948 match result {
949 Value::Num(v) => assert!(v >= 0.0),
950 other => panic!("expected numeric result, got {other:?}"),
951 }
952 assert!(
953 COUNTER_DEFAULT.load(Ordering::SeqCst) >= MIN_SAMPLE_COUNT,
954 "expected at least {} invocations",
955 MIN_SAMPLE_COUNT
956 );
957 }
958
959 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
960 #[test]
961 fn timeit_accepts_num_outputs_argument() {
962 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
963 COUNTER_NUM_OUTPUTS.store(0, Ordering::SeqCst);
964 let args = vec![Value::Int(IntValue::I32(3))];
965 let _ = block_on(timeit_builtin(outputs_handle(), args)).expect("timeit numOutputs");
966 assert!(
967 COUNTER_NUM_OUTPUTS.load(Ordering::SeqCst) >= MIN_SAMPLE_COUNT,
968 "expected at least {} invocations",
969 MIN_SAMPLE_COUNT
970 );
971 }
972
973 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
974 #[test]
975 fn timeit_supports_zero_outputs() {
976 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
977 COUNTER_ZERO_OUTPUTS.store(0, Ordering::SeqCst);
978 let args = vec![Value::Int(IntValue::I32(0))];
979 let _ = block_on(timeit_builtin(zero_outputs_handle(), args)).expect("timeit zero outputs");
980 assert!(
981 COUNTER_ZERO_OUTPUTS.load(Ordering::SeqCst) >= MIN_SAMPLE_COUNT,
982 "expected at least {} invocations",
983 MIN_SAMPLE_COUNT
984 );
985 }
986
987 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
988 #[test]
989 #[cfg(feature = "wgpu")]
990 fn timeit_runs_with_wgpu_provider_registered() {
991 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
992 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
993 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
994 );
995 let result =
996 block_on(timeit_builtin(default_handle(), Vec::new())).expect("timeit with wgpu");
997 match result {
998 Value::Num(v) => assert!(v >= 0.0),
999 other => panic!("expected numeric result, got {other:?}"),
1000 }
1001 }
1002
1003 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1004 #[test]
1005 fn timeit_rejects_non_function_input() {
1006 let err = block_on(timeit_builtin(Value::Num(1.0), Vec::new())).unwrap_err();
1007 assert_timeit_error_contains(&err, "function");
1008 assert_timeit_error_identifier(&err, TIMEIT_ERROR_FIRST_ARG_KIND.identifier.unwrap());
1009 }
1010
1011 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1012 #[test]
1013 fn timeit_rejects_invalid_num_outputs() {
1014 COUNTER_INVALID.store(0, Ordering::SeqCst);
1015 let err = block_on(timeit_builtin(invalid_handle(), vec![Value::Num(-1.0)])).unwrap_err();
1016 assert_timeit_error_contains(&err, "nonnegative");
1017 assert_timeit_error_identifier(&err, TIMEIT_ERROR_NUM_OUTPUTS_NONNEG.identifier.unwrap());
1018 assert_eq!(COUNTER_INVALID.load(Ordering::SeqCst), 0);
1019 }
1020
1021 #[test]
1022 fn timeit_gates_typed_integer_output_counts_before_invoking_the_callback() {
1023 let _compat = crate::compatibility::push_runmat_extensions_enabled(false);
1024 COUNTER_NUM_OUTPUTS.store(0, Ordering::SeqCst);
1025 let err = block_on(timeit_builtin(
1026 outputs_handle(),
1027 vec![Value::Int(IntValue::I32(3))],
1028 ))
1029 .expect_err("strict compatibility must reject native integer output counts");
1030 assert_eq!(
1031 err.identifier(),
1032 INTEGER_NUM_OUTPUTS_EXTENSION.error_identifier
1033 );
1034 assert_eq!(COUNTER_NUM_OUTPUTS.load(Ordering::SeqCst), 0);
1035 }
1036
1037 #[test]
1038 fn timeit_decodes_typed_integer_output_counts_without_an_f64_boundary() {
1039 let _compat = crate::compatibility::push_runmat_extensions_enabled(true);
1040 assert_eq!(
1041 block_on(parse_num_outputs(&[Value::Int(IntValue::U64(
1042 usize::MAX as u64
1043 ))]))
1044 .expect("platform-sized integer should decode exactly"),
1045 Some(usize::MAX)
1046 );
1047 assert!(block_on(parse_num_outputs(&[Value::Int(IntValue::I64(-1))])).is_err());
1048 }
1049
1050 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1051 #[test]
1052 fn timeit_rejects_extra_arguments() {
1053 let err = block_on(timeit_builtin(
1054 default_handle(),
1055 vec![Value::from(1.0), Value::from(2.0)],
1056 ))
1057 .unwrap_err();
1058 assert_timeit_error_contains(&err, "too many");
1059 assert_timeit_error_identifier(&err, TIMEIT_ERROR_TOO_MANY_INPUTS.identifier.unwrap());
1060 }
1061}