1pub(crate) mod ceil;
4pub(crate) mod fix;
5pub(crate) mod floor;
6pub(crate) mod rem;
7pub(crate) mod round;
8
9use runmat_accelerate_api::GpuTensorHandle;
10use runmat_builtins::{
11 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinIntegerBackendRule,
12 BuiltinIntegerCapabilityDescriptor, BuiltinIntegerComputationDomain,
13 BuiltinIntegerInputAvailability, BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule,
14 BuiltinIntegerOverflowRule, BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule,
15 BuiltinOutputMode, BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType,
16 BuiltinSignatureDescriptor,
17};
18use runmat_macros::runtime_builtin;
19use runmat_value::{ComplexTensor, NumericDType, Tensor, Value};
20
21use crate::builtins::common::broadcast::BroadcastPlan;
22use crate::builtins::common::spec::{
23 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, FusionError,
24 FusionExprContext, FusionKernelTemplate, GpuOpKind, ProviderHook, ReductionNaN,
25 ResidencyPolicy, ScalarType, ShapeRequirements,
26};
27use crate::builtins::common::{gpu_helpers, tensor};
28use crate::builtins::math::elementwise::integer_arithmetic::{
29 reject_integer_logical_operands, try_integer_remainder, IntegerRemainderOp,
30};
31use crate::builtins::math::type_resolvers::numeric_binary_type;
32use crate::{build_runtime_error, BuiltinResult, RuntimeError};
33
34#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::rounding")]
35pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
36 name: "mod",
37 op_kind: GpuOpKind::Elementwise,
38 supported_precisions: &[ScalarType::F32, ScalarType::F64],
39 broadcast: BroadcastSemantics::Matlab,
40 provider_hooks: &[
41 ProviderHook::Binary {
42 name: "elem_div",
43 commutative: false,
44 },
45 ProviderHook::Unary { name: "unary_floor" },
46 ProviderHook::Binary {
47 name: "elem_mul",
48 commutative: false,
49 },
50 ProviderHook::Binary {
51 name: "elem_sub",
52 commutative: false,
53 },
54 ],
55 constant_strategy: ConstantStrategy::InlineLiteral,
56 residency: ResidencyPolicy::NewHandle,
57 nan_mode: ReductionNaN::Include,
58 two_pass_threshold: None,
59 workgroup_size: None,
60 accepts_nan_mode: false,
61 notes:
62 "Native integer providers may execute exact mod directly; floating fallback gathers and reuploads when a dedicated semantically complete provider hook is unavailable.",
63};
64
65#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::rounding")]
66pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
67 name: "mod",
68 shape: ShapeRequirements::BroadcastCompatible,
69 constant_strategy: ConstantStrategy::InlineLiteral,
70 elementwise: Some(FusionKernelTemplate {
71 scalar_precisions: &[ScalarType::F32, ScalarType::F64],
72 wgsl_body: |ctx: &FusionExprContext| {
73 let a = ctx
74 .inputs
75 .first()
76 .ok_or(FusionError::MissingInput(0))?;
77 let b = ctx.inputs.get(1).ok_or(FusionError::MissingInput(1))?;
78 Ok(format!(
79 "select({a} - {b} * floor({a} / {b}), {a}, {b} == 0.0)"
80 ))
81 },
82 }),
83 reduction: None,
84 emits_nan: true,
85 notes: "Fusion applies a - b * floor(a / b), including the documented mod(a, 0) = a convention; providers may substitute specialised kernels when available.",
86};
87
88const BUILTIN_NAME: &str = "mod";
89
90const MOD_OUTPUT: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
91 name: "R",
92 ty: BuiltinParamType::NumericArray,
93 arity: BuiltinParamArity::Required,
94 default: None,
95 description: "Element-wise modulus result.",
96}];
97const MOD_INPUTS: [BuiltinParamDescriptor; 2] = [
98 BuiltinParamDescriptor {
99 name: "A",
100 ty: BuiltinParamType::Any,
101 arity: BuiltinParamArity::Required,
102 default: None,
103 description: "Real dividend input (numeric/logical/char).",
104 },
105 BuiltinParamDescriptor {
106 name: "B",
107 ty: BuiltinParamType::Any,
108 arity: BuiltinParamArity::Required,
109 default: None,
110 description: "Real divisor input (numeric/logical/char).",
111 },
112];
113const MOD_SIGNATURES: [BuiltinSignatureDescriptor; 1] = [BuiltinSignatureDescriptor {
114 label: "R = mod(A, B)",
115 inputs: &MOD_INPUTS,
116 outputs: &MOD_OUTPUT,
117}];
118const MOD_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
119 code: "RM.MOD.INVALID_INPUT",
120 identifier: Some("RunMat:mod:InvalidInput"),
121 when: "Inputs are complex or cannot be interpreted as real numeric, logical, or char operands.",
122 message: "mod: invalid input",
123};
124const MOD_ERROR_SIZE_MISMATCH: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
125 code: "RM.MOD.SIZE_MISMATCH",
126 identifier: Some("RunMat:mod:SizeMismatch"),
127 when: "Operands are not broadcast-compatible.",
128 message: "mod: array sizes are not compatible for broadcasting",
129};
130const MOD_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
131 code: "RM.MOD.INTERNAL",
132 identifier: Some("RunMat:mod:Internal"),
133 when: "Internal tensor conversion, allocation, or provider composition failed.",
134 message: "mod: internal error",
135};
136const MOD_ERRORS: [BuiltinErrorDescriptor; 3] = [
137 MOD_ERROR_INVALID_INPUT,
138 MOD_ERROR_SIZE_MISMATCH,
139 MOD_ERROR_INTERNAL,
140];
141pub const MOD_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
142 signatures: &MOD_SIGNATURES,
143 output_mode: BuiltinOutputMode::Fixed,
144 completion_policy: BuiltinCompletionPolicy::Public,
145 errors: &MOD_ERRORS,
146};
147
148const MOD_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 2] = [
149 BuiltinIntegerInputCapability {
150 name: "A",
151 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
152 availability: BuiltinIntegerInputAvailability::Documented,
153 scalar_double: BuiltinIntegerScalarDoubleRule::AllowedExceptWith64BitInteger,
154 notes: "A is an integer array or a compatible real scalar double.",
155 },
156 BuiltinIntegerInputCapability {
157 name: "B",
158 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
159 availability: BuiltinIntegerInputAvailability::Documented,
160 scalar_double: BuiltinIntegerScalarDoubleRule::AllowedExceptWith64BitInteger,
161 notes: "B is an integer array or a compatible real scalar double.",
162 },
163];
164
165pub const INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
166 [BuiltinIntegerCapabilityDescriptor {
167 form: "R = mod(A, B)",
168 inputs: &MOD_INTEGER_INPUTS,
169 computation_domain: BuiltinIntegerComputationDomain::ExactInteger,
170 output_class: BuiltinIntegerOutputClassRule::PreserveNondoubleInput,
171 overflow: BuiltinIntegerOverflowRule::NotApplicable,
172 backend: BuiltinIntegerBackendRule::HostAndGpu,
173 overload: BuiltinIntegerOverloadKind::BroadcastCompatible,
174 notes: "Native integer storage uses exact floor-remainder semantics; providers may execute exact integer mod directly or gather for a semantically complete fallback.",
175 }];
176
177fn mod_error_with_detail(
178 error: &'static BuiltinErrorDescriptor,
179 detail: impl AsRef<str>,
180) -> RuntimeError {
181 mod_error_with_message(format!("{}: {}", error.message, detail.as_ref()), error)
182}
183
184fn mod_error_with_message(
185 message: impl Into<String>,
186 error: &'static BuiltinErrorDescriptor,
187) -> RuntimeError {
188 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
189 if let Some(identifier) = error.identifier {
190 builder = builder.with_identifier(identifier);
191 }
192 builder.build()
193}
194
195#[runtime_builtin(
196 name = "mod",
197 category = "math/rounding",
198 summary = "MATLAB-compatible real modulus a - b .* floor(a./b) with broadcasting.",
199 keywords = "mod,modulus,remainder,gpu",
200 accel = "binary",
201 type_resolver(numeric_binary_type),
202 descriptor(crate::builtins::math::rounding::MOD_DESCRIPTOR),
203 integer_capabilities(crate::builtins::math::rounding::INTEGER_CAPABILITIES),
204 builtin_path = "crate::builtins::math::rounding"
205)]
206async fn mod_builtin(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
207 if matches!(&lhs, Value::Complex(_, _) | Value::ComplexTensor(_))
208 || matches!(&rhs, Value::Complex(_, _) | Value::ComplexTensor(_))
209 {
210 return Err(mod_error_with_detail(
211 &MOD_ERROR_INVALID_INPUT,
212 "inputs must be real",
213 ));
214 }
215 crate::builtins::common::validation::reject_typed_complex_integer(&lhs, BUILTIN_NAME)?;
216 crate::builtins::common::validation::reject_typed_complex_integer(&rhs, BUILTIN_NAME)?;
217 reject_integer_logical_operands(&lhs, &rhs, BUILTIN_NAME)
218 .map_err(|error| mod_error_with_detail(&MOD_ERROR_INVALID_INPUT, error))?;
219 match (lhs, rhs) {
220 (Value::GpuTensor(a), Value::GpuTensor(b)) => mod_gpu_pair(a, b).await,
221 (Value::GpuTensor(a), other) => {
222 let gathered = gpu_helpers::gather_tensor_async(&a).await?;
223 mod_host(Value::Tensor(gathered), other)
224 }
225 (other, Value::GpuTensor(b)) => {
226 let gathered = gpu_helpers::gather_tensor_async(&b).await?;
227 mod_host(other, Value::Tensor(gathered))
228 }
229 (left, right) => mod_host(left, right),
230 }
231}
232
233async fn mod_gpu_pair(a: GpuTensorHandle, b: GpuTensorHandle) -> BuiltinResult<Value> {
234 if runmat_accelerate_api::handle_integer_type(&a).is_some()
235 && runmat_accelerate_api::handle_integer_type(&b).is_some()
236 && a.device_id == b.device_id
237 {
238 if let Some(provider) = runmat_accelerate_api::provider_for_handle(&a) {
239 if let Ok(out) = provider.elem_mod(&a, &b).await {
240 return Ok(gpu_helpers::resident_gpu_value(out));
241 }
242 }
243 }
244 let left = gpu_helpers::gather_tensor_async(&a).await?;
245 let right = gpu_helpers::gather_tensor_async(&b).await?;
246 let result = mod_host(Value::Tensor(left), Value::Tensor(right))?;
247 if a.device_id == b.device_id {
248 if let Some(provider) = runmat_accelerate_api::provider_for_handle(&a) {
249 return upload_mod_result(provider, result);
250 }
251 }
252 Ok(result)
253}
254
255fn upload_mod_result(
256 provider: &dyn runmat_accelerate_api::AccelProvider,
257 value: Value,
258) -> BuiltinResult<Value> {
259 let tensor = match value {
260 Value::Tensor(tensor) => tensor,
261 Value::Num(value) => Tensor::new(vec![value], vec![1, 1])
262 .map_err(|err| mod_error_with_detail(&MOD_ERROR_INTERNAL, err))?,
263 other => return Ok(other),
264 };
265 let handle = gpu_helpers::upload_tensor(provider, &tensor)
266 .map_err(|err| mod_error_with_detail(&MOD_ERROR_INTERNAL, err))?;
267 Ok(gpu_helpers::resident_gpu_value(handle))
268}
269
270fn mod_host(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
271 if let Some(result) = try_integer_remainder(&lhs, &rhs, IntegerRemainderOp::Mod, BUILTIN_NAME)
272 .map_err(|error| mod_error_with_detail(&MOD_ERROR_INVALID_INPUT, error))?
273 {
274 return Ok(result);
275 }
276 if let Some(result) = scalar_mod_value(&lhs, &rhs) {
277 return Ok(result);
278 }
279 let left = value_into_numeric_array(lhs)?;
280 let right = value_into_numeric_array(rhs)?;
281 match align_numeric_arrays(left, right)? {
282 NumericPair::Real(a, b) => compute_mod_real(&a, &b),
283 NumericPair::Complex(a, b) => compute_mod_complex(&a, &b),
284 }
285}
286
287fn compute_mod_real(a: &Tensor, b: &Tensor) -> BuiltinResult<Value> {
288 let plan = BroadcastPlan::new(&a.shape, &b.shape)
289 .map_err(|err| mod_error_with_detail(&MOD_ERROR_SIZE_MISMATCH, err))?;
290 let dtype = if a.numeric_dtype() == NumericDType::F32 && b.numeric_dtype() == NumericDType::F32
291 {
292 NumericDType::F32
293 } else {
294 NumericDType::F64
295 };
296 if plan.is_empty() {
297 let tensor = Tensor::new_with_dtype(Vec::new(), plan.output_shape().to_vec(), dtype)
298 .map_err(|e| mod_error_with_detail(&MOD_ERROR_INTERNAL, e))?;
299 return Ok(tensor::tensor_into_value(tensor));
300 }
301 let mut result = vec![0.0f64; plan.len()];
302 for (out_idx, idx_a, idx_b) in plan.iter() {
303 let aval = tensor::tensor_value_f64(a, idx_a);
304 let bval = tensor::tensor_value_f64(b, idx_b);
305 result[out_idx] = mod_real_scalar(aval, bval);
306 }
307 let tensor = Tensor::new_with_dtype(result, plan.output_shape().to_vec(), dtype)
308 .map_err(|e| mod_error_with_detail(&MOD_ERROR_INTERNAL, e))?;
309 Ok(tensor::tensor_into_value(tensor))
310}
311
312fn compute_mod_complex(a: &ComplexTensor, b: &ComplexTensor) -> BuiltinResult<Value> {
313 let plan = BroadcastPlan::new(&a.shape, &b.shape)
314 .map_err(|err| mod_error_with_detail(&MOD_ERROR_SIZE_MISMATCH, err))?;
315 if plan.is_empty() {
316 let tensor = ComplexTensor::new(Vec::new(), plan.output_shape().to_vec())
317 .map_err(|e| mod_error_with_detail(&MOD_ERROR_INTERNAL, e))?;
318 return Ok(complex_tensor_into_value(tensor));
319 }
320 let mut result = vec![(0.0f64, 0.0f64); plan.len()];
321 for (out_idx, idx_a, idx_b) in plan.iter() {
322 let (ar, ai) = a.materialize_f64()[idx_a];
323 let (br, bi) = b.materialize_f64()[idx_b];
324 result[out_idx] = mod_complex_scalar(ar, ai, br, bi);
325 }
326 let tensor = ComplexTensor::new(result, plan.output_shape().to_vec())
327 .map_err(|e| mod_error_with_detail(&MOD_ERROR_INTERNAL, e))?;
328 Ok(complex_tensor_into_value(tensor))
329}
330
331fn mod_real_scalar(a: f64, b: f64) -> f64 {
332 if a.is_nan() || b.is_nan() {
333 return f64::NAN;
334 }
335 if b == 0.0 {
336 return a;
337 }
338 if !a.is_finite() && b.is_finite() {
339 return f64::NAN;
340 }
341 let quotient = (a / b).floor();
342 let mut remainder = a - b * quotient;
343 if remainder == 0.0 {
344 remainder = 0.0;
345 }
346 if b.is_infinite() && a.is_finite() {
347 if a == 0.0 {
349 return 0.0;
350 }
351 return if a.signum() == b.signum() { a } else { b };
352 }
353 if !remainder.is_finite() && !a.is_finite() {
354 return f64::NAN;
355 }
356 let same_sign = remainder == 0.0 || remainder.signum() == b.signum();
357 if !same_sign {
358 remainder += b;
359 }
360 if remainder == -0.0 {
361 remainder = 0.0;
362 }
363 remainder
364}
365
366fn mod_complex_scalar(ar: f64, ai: f64, br: f64, bi: f64) -> (f64, f64) {
367 if (ar.is_nan() || ai.is_nan()) || (br.is_nan() || bi.is_nan()) {
368 return (f64::NAN, f64::NAN);
369 }
370 if br == 0.0 && bi == 0.0 {
371 return (f64::NAN, f64::NAN);
372 }
373 if !ar.is_finite() || !ai.is_finite() {
374 return (f64::NAN, f64::NAN);
375 }
376 let (qr, qi) = complex_div(ar, ai, br, bi);
377 if !qr.is_finite() && !qi.is_finite() && br.is_finite() && bi.is_finite() {
378 return (f64::NAN, f64::NAN);
379 }
380 let (fr, fi) = (qr.floor(), qi.floor());
381 let (mulr, muli) = complex_mul(br, bi, fr, fi);
382 let (rr, ri) = (ar - mulr, ai - muli);
383 (normalize_zero(rr), normalize_zero(ri))
384}
385
386fn scalar_real_value(value: &Value) -> Option<f64> {
387 match value {
388 Value::Num(n) => Some(*n),
389 Value::Int(i) => Some(i.to_f64()),
390 Value::Bool(b) => Some(if *b { 1.0 } else { 0.0 }),
391 Value::Tensor(t) if tensor::is_scalar_tensor(t) => Some(tensor::tensor_value_f64(t, 0)),
392 Value::LogicalArray(l) if l.data.len() == 1 => Some(if l.data[0] != 0 { 1.0 } else { 0.0 }),
393 Value::CharArray(ca) if ca.rows * ca.cols == 1 => {
394 Some(ca.data.first().map(|&ch| ch as u32 as f64).unwrap_or(0.0))
395 }
396 _ => None,
397 }
398}
399
400fn scalar_complex_value(value: &Value) -> Option<(f64, f64)> {
401 match value {
402 Value::Complex(re, im) => Some((*re, *im)),
403 Value::ComplexTensor(ct) if tensor::complex_tensor_element_len(ct) == 1 => {
404 let value = tensor::complex_tensor_value_complex64(ct, 0);
405 Some((value.re, value.im))
406 }
407 _ => None,
408 }
409}
410
411fn scalar_mod_value(lhs: &Value, rhs: &Value) -> Option<Value> {
412 let left = scalar_complex_value(lhs).or_else(|| scalar_real_value(lhs).map(|v| (v, 0.0)))?;
413 let right = scalar_complex_value(rhs).or_else(|| scalar_real_value(rhs).map(|v| (v, 0.0)))?;
414 let (ar, ai) = left;
415 let (br, bi) = right;
416 if ai != 0.0 || bi != 0.0 {
417 let (re, im) = mod_complex_scalar(ar, ai, br, bi);
418 return Some(Value::Complex(re, im));
419 }
420 Some(Value::Num(mod_real_scalar(ar, br)))
421}
422
423fn normalize_zero(value: f64) -> f64 {
424 if value == -0.0 {
425 0.0
426 } else {
427 value
428 }
429}
430
431fn complex_mul(ar: f64, ai: f64, br: f64, bi: f64) -> (f64, f64) {
432 (ar * br - ai * bi, ar * bi + ai * br)
433}
434
435fn complex_div(ar: f64, ai: f64, br: f64, bi: f64) -> (f64, f64) {
436 let denom = br * br + bi * bi;
437 if denom == 0.0 {
438 return (f64::NAN, f64::NAN);
439 }
440 ((ar * br + ai * bi) / denom, (ai * br - ar * bi) / denom)
441}
442
443fn complex_tensor_into_value(tensor: ComplexTensor) -> Value {
444 if tensor::complex_tensor_element_len(&tensor) == 1 {
445 let value = tensor::complex_tensor_value_complex64(&tensor, 0);
446 Value::Complex(value.re, value.im)
447 } else {
448 Value::ComplexTensor(tensor)
449 }
450}
451
452fn value_into_numeric_array(value: Value) -> BuiltinResult<NumericArray> {
453 match value {
454 Value::Complex(re, im) => {
455 let tensor = ComplexTensor::new(vec![(re, im)], vec![1, 1])
456 .map_err(|e| mod_error_with_detail(&MOD_ERROR_INTERNAL, e))?;
457 Ok(NumericArray::Complex(tensor))
458 }
459 Value::ComplexTensor(ct) => Ok(NumericArray::Complex(ct)),
460 Value::CharArray(ca) => {
461 let data: Vec<f64> = ca.data.iter().map(|&ch| ch as u32 as f64).collect();
462 let tensor = Tensor::new(data, vec![ca.rows, ca.cols])
463 .map_err(|e| mod_error_with_detail(&MOD_ERROR_INTERNAL, e))?;
464 Ok(NumericArray::Real(tensor))
465 }
466 Value::String(_) | Value::StringArray(_) => Err(mod_error_with_detail(
467 &MOD_ERROR_INVALID_INPUT,
468 "expected numeric input, got string",
469 )),
470 Value::GpuTensor(_) => Err(mod_error_with_detail(
471 &MOD_ERROR_INTERNAL,
472 "internal error converting GPU tensor",
473 )),
474 other => {
475 let tensor = tensor::value_into_tensor_for(BUILTIN_NAME, other)
476 .map_err(|err| mod_error_with_detail(&MOD_ERROR_INVALID_INPUT, err))?;
477 Ok(NumericArray::Real(tensor))
478 }
479 }
480}
481
482enum NumericArray {
483 Real(Tensor),
484 Complex(ComplexTensor),
485}
486
487enum NumericPair {
488 Real(Tensor, Tensor),
489 Complex(ComplexTensor, ComplexTensor),
490}
491
492fn align_numeric_arrays(lhs: NumericArray, rhs: NumericArray) -> BuiltinResult<NumericPair> {
493 match (lhs, rhs) {
494 (NumericArray::Real(a), NumericArray::Real(b)) => Ok(NumericPair::Real(a, b)),
495 (left, right) => {
496 let lc = into_complex(left)?;
497 let rc = into_complex(right)?;
498 Ok(NumericPair::Complex(lc, rc))
499 }
500 }
501}
502
503fn into_complex(input: NumericArray) -> BuiltinResult<ComplexTensor> {
504 match input {
505 NumericArray::Real(t) => {
506 let shape = t.shape.clone();
507 let complex = t
508 .into_numeric_storage()
509 .map_err(|e| mod_error_with_detail(&MOD_ERROR_INTERNAL, e))?
510 .materialize_f64()
511 .into_iter()
512 .map(|real| (real, 0.0))
513 .collect();
514 ComplexTensor::new(complex, shape)
515 .map_err(|e| mod_error_with_detail(&MOD_ERROR_INTERNAL, e))
516 }
517 NumericArray::Complex(ct) => Ok(ct),
518 }
519}
520
521#[cfg(test)]
522pub(crate) mod tests {
523 use super::*;
524 use crate::builtins::common::test_support;
525 use crate::RuntimeError;
526 use futures::executor::block_on;
527 use runmat_builtins::{ResolveContext, Type};
528 use runmat_value::{CharArray, ComplexTensor, IntValue, IntegerStorage, LogicalArray, Tensor};
529
530 fn mod_builtin(lhs: Value, rhs: Value) -> BuiltinResult<Value> {
531 block_on(super::mod_builtin(lhs, rhs))
532 }
533
534 #[test]
535 fn mod_real_arrays_preserve_native_single_storage_including_empty() {
536 let lhs = Tensor::from_f32(vec![5.5, -5.5], vec![1, 2]).unwrap();
537 let rhs = Tensor::from_f32(vec![2.0, 2.0], vec![1, 2]).unwrap();
538 let output = compute_mod_real(&lhs, &rhs).unwrap();
539 let Value::Tensor(output) = output else {
540 panic!("expected native-single tensor")
541 };
542 assert_eq!(
543 output.into_numeric_storage().unwrap(),
544 runmat_value::NumericStorage::F32(vec![1.5, 0.5])
545 );
546
547 let lhs = Tensor::from_f32(Vec::new(), vec![0, 2]).unwrap();
548 let rhs = Tensor::from_f32(Vec::new(), vec![0, 2]).unwrap();
549 let Value::Tensor(output) = compute_mod_real(&lhs, &rhs).unwrap() else {
550 panic!("expected empty native-single tensor")
551 };
552 assert_eq!(
553 output.into_numeric_storage().unwrap(),
554 runmat_value::NumericStorage::F32(Vec::new())
555 );
556 }
557
558 fn assert_error_contains(error: RuntimeError, needle: &str) {
559 assert!(
560 error.message().contains(needle),
561 "unexpected error: {}",
562 error.message()
563 );
564 }
565
566 #[test]
567 fn mod_descriptor_signatures_cover_core_forms() {
568 let labels: Vec<&str> = MOD_DESCRIPTOR
569 .signatures
570 .iter()
571 .map(|sig| sig.label)
572 .collect();
573 assert!(labels.contains(&"R = mod(A, B)"));
574 }
575
576 #[test]
577 fn mod_type_preserves_tensor_shape() {
578 let out = numeric_binary_type(
579 &[
580 Type::Tensor {
581 shape: Some(vec![Some(2), Some(3)]),
582 },
583 Type::Tensor {
584 shape: Some(vec![Some(2), Some(3)]),
585 },
586 ],
587 &ResolveContext::new(Vec::new()),
588 );
589 assert_eq!(
590 out,
591 Type::Tensor {
592 shape: Some(vec![Some(2), Some(3)])
593 }
594 );
595 }
596
597 #[test]
598 fn mod_type_scalar_and_tensor_returns_tensor() {
599 let out = numeric_binary_type(
600 &[
601 Type::Num,
602 Type::Tensor {
603 shape: Some(vec![Some(4), Some(1)]),
604 },
605 ],
606 &ResolveContext::new(Vec::new()),
607 );
608 assert_eq!(
609 out,
610 Type::Tensor {
611 shape: Some(vec![Some(4), Some(1)])
612 }
613 );
614 }
615
616 #[test]
617 fn mod_type_scalar_returns_num() {
618 let out = numeric_binary_type(&[Type::Num, Type::Int], &ResolveContext::new(Vec::new()));
619 assert_eq!(out, Type::Num);
620 }
621
622 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
623 #[test]
624 fn mod_positive_values() {
625 let result = mod_builtin(Value::Num(17.0), Value::Num(5.0)).expect("mod");
626 match result {
627 Value::Num(v) => assert!((v - 2.0).abs() < 1e-12),
628 other => panic!("expected scalar result, got {other:?}"),
629 }
630 }
631
632 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
633 #[test]
634 fn mod_negative_divisor_keeps_sign() {
635 let tensor = Tensor::new(vec![-7.0, -3.0, 4.0, 9.0], vec![4, 1]).unwrap();
636 let divisor = Tensor::new(vec![-4.0], vec![1, 1]).unwrap();
637 let result =
638 mod_builtin(Value::Tensor(tensor), Value::Tensor(divisor)).expect("mod broadcast");
639 match result {
640 Value::Tensor(out) => {
641 assert_eq!(out.materialize_f64(), vec![-3.0, -3.0, 0.0, -3.0]);
642 }
643 other => panic!("expected tensor result, got {other:?}"),
644 }
645 }
646
647 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
648 #[test]
649 fn mod_negative_numerator_positive_divisor() {
650 let result = mod_builtin(Value::Num(-3.0), Value::Num(2.0)).expect("mod");
651 match result {
652 Value::Num(v) => assert!((v - 1.0).abs() < 1e-12),
653 other => panic!("expected scalar result, got {other:?}"),
654 }
655 }
656
657 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
658 #[test]
659 fn mod_zero_divisor_returns_dividend() {
660 let result = mod_builtin(Value::Num(3.0), Value::Num(0.0)).expect("mod");
661 match result {
662 Value::Num(v) => assert_eq!(v, 3.0),
663 other => panic!("expected dividend, got {other:?}"),
664 }
665 }
666
667 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
668 #[test]
669 fn mod_matrix_scalar_broadcast() {
670 let matrix = Tensor::new(vec![4.5, 7.1, -2.3, 0.4], vec![2, 2]).unwrap();
671 let result = mod_builtin(Value::Tensor(matrix), Value::Num(2.0)).expect("broadcast");
672 match result {
673 Value::Tensor(t) => {
674 assert_eq!(t.shape, vec![2, 2]);
675 let expected = [0.5, 1.1, 1.7, 0.4];
676 for (a, b) in t.materialize_f64().iter().zip(expected.iter()) {
677 assert!((a - b).abs() < 1e-12);
678 }
679 }
680 other => panic!("expected tensor result, got {other:?}"),
681 }
682 }
683
684 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
685 #[test]
686 fn mod_rejects_complex_operands() {
687 let complex =
688 ComplexTensor::new(vec![(3.0, 4.0), (-2.0, 5.0)], vec![1, 2]).expect("complex tensor");
689 let divisor = ComplexTensor::new(vec![(2.0, 1.0)], vec![1, 1]).expect("divisor");
690 assert!(mod_builtin(Value::ComplexTensor(complex), Value::ComplexTensor(divisor)).is_err());
691 assert!(mod_builtin(Value::Complex(2.0, 0.0), Value::Num(1.0)).is_err());
692 }
693
694 #[test]
695 fn mod_complex_scalar_helpers_read_typed_integer_complex_storage_without_mirror() {
696 let complex = ComplexTensor::new_integer(
697 runmat_value::IntegerComplexStorage::new(
698 IntegerStorage::I16(vec![9]),
699 IntegerStorage::I16(vec![-2]),
700 )
701 .expect("integer complex storage"),
702 vec![1, 1],
703 )
704 .expect("integer complex tensor");
705
706 assert_eq!(
707 scalar_complex_value(&Value::ComplexTensor(complex.clone())),
708 Some((9.0, -2.0))
709 );
710 assert_eq!(
711 complex_tensor_into_value(complex),
712 Value::Complex(9.0, -2.0)
713 );
714 }
715
716 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
717 #[test]
718 fn mod_char_array_support() {
719 let chars = CharArray::new("ABC".chars().collect(), 1, 3).unwrap();
720 let result = mod_builtin(Value::CharArray(chars), Value::Num(5.0)).expect("mod");
721 match result {
722 Value::Tensor(t) => assert_eq!(t.materialize_f64(), vec![0.0, 1.0, 2.0]),
723 other => panic!("expected tensor result, got {other:?}"),
724 }
725 }
726
727 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
728 #[test]
729 fn mod_string_input_errors() {
730 let err = mod_builtin(Value::from("abc"), Value::Num(3.0))
731 .expect_err("string inputs should error");
732 let identifier = err.identifier().map(str::to_string);
733 assert_error_contains(err, "expected numeric input");
734 assert_eq!(identifier.as_deref(), MOD_ERROR_INVALID_INPUT.identifier);
735 }
736
737 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
738 #[test]
739 fn mod_logical_array_support() {
740 let logical = LogicalArray::new(vec![1, 0, 1, 0], vec![2, 2]).unwrap();
741 let value =
742 mod_builtin(Value::LogicalArray(logical), Value::Num(2.0)).expect("logical mod");
743 match value {
744 Value::Tensor(t) => assert_eq!(t.materialize_f64(), vec![1.0, 0.0, 1.0, 0.0]),
745 other => panic!("expected tensor result, got {other:?}"),
746 }
747 }
748
749 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
750 #[test]
751 fn mod_vector_broadcasting() {
752 let lhs = Tensor::new(vec![1.0, 2.0], vec![2, 1]).unwrap();
753 let rhs = Tensor::new(vec![3.0, 4.0, 5.0], vec![1, 3]).unwrap();
754 let result = mod_builtin(Value::Tensor(lhs), Value::Tensor(rhs)).expect("vector broadcast");
755 match result {
756 Value::Tensor(t) => {
757 assert_eq!(t.shape, vec![2, 3]);
758 assert_eq!(t.materialize_f64(), vec![1.0, 2.0, 1.0, 2.0, 1.0, 2.0]);
759 }
760 other => panic!("expected tensor result, got {other:?}"),
761 }
762 }
763
764 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
765 #[test]
766 fn mod_nan_inputs_propagate() {
767 let result = mod_builtin(Value::Num(f64::NAN), Value::Num(3.0)).expect("mod");
768 match result {
769 Value::Num(v) => assert!(v.is_nan()),
770 other => panic!("expected NaN result, got {other:?}"),
771 }
772 }
773
774 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
775 #[test]
776 fn mod_gpu_pair_roundtrip() {
777 test_support::with_test_provider(|provider| {
778 let tensor = Tensor::new(vec![-5.0, -3.0, 0.0, 1.0, 6.0, 9.0], vec![3, 2]).unwrap();
779 let divisor = Tensor::new(vec![4.0, 4.0, 4.0, 4.0, 4.0, 4.0], vec![3, 2]).unwrap();
780 let a_view = runmat_accelerate_api::HostTensorView {
781 data: &tensor.materialize_f64(),
782 shape: &tensor.shape,
783 };
784 let b_view = runmat_accelerate_api::HostTensorView {
785 data: &divisor.materialize_f64(),
786 shape: &divisor.shape,
787 };
788 let a_handle = provider.upload(&a_view).expect("upload a");
789 let b_handle = provider.upload(&b_view).expect("upload b");
790 let result =
791 mod_builtin(Value::GpuTensor(a_handle), Value::GpuTensor(b_handle)).expect("mod");
792 let gathered = test_support::gather(result).expect("gather result");
793 assert_eq!(gathered.shape, vec![3, 2]);
794 assert_eq!(
795 gathered.materialize_f64(),
796 vec![3.0, 1.0, 0.0, 1.0, 2.0, 1.0]
797 );
798 });
799 }
800
801 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
802 #[test]
803 fn mod_int_scalar_preserves_exact_class() {
804 let result =
805 mod_builtin(Value::Int(IntValue::I32(-7)), Value::Int(IntValue::I32(4))).expect("mod");
806 match result {
807 Value::Int(IntValue::I32(v)) => assert_eq!(v, 1),
808 other => panic!("expected int32 scalar result, got {other:?}"),
809 }
810 }
811
812 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
813 #[test]
814 fn mod_scalar_fast_path_reads_typed_integer_storage_exactly() {
815 let lhs =
816 Tensor::new_integer(IntegerStorage::I16(vec![7]), vec![1, 1]).expect("lhs tensor");
817
818 assert_eq!(scalar_real_value(&Value::Tensor(lhs.clone())), Some(7.0));
819
820 let result = mod_builtin(Value::Tensor(lhs), Value::Num(4.0)).expect("mod");
821 match result {
822 Value::Int(IntValue::I16(v)) => assert_eq!(v, 3),
823 other => panic!("expected int16 scalar result, got {other:?}"),
824 }
825 }
826
827 #[test]
828 fn mod_dense_integer_arrays_preserve_exact_storage_without_mirror() {
829 let lhs = Tensor::new_integer(IntegerStorage::I64(vec![-7, 7]), vec![2, 1]).expect("lhs");
830 let rhs =
831 Tensor::new_integer(IntegerStorage::I64(vec![4, -4, 0]), vec![1, 3]).expect("rhs");
832
833 let result = mod_builtin(Value::Tensor(lhs), Value::Tensor(rhs)).expect("mod");
834 let Value::Tensor(result) = result else {
835 panic!("expected integer tensor");
836 };
837 assert_eq!(result.shape, vec![2, 3]);
838 assert_eq!(
839 result.integer_storage(),
840 Some(&IntegerStorage::I64(vec![1, 3, -3, -1, -7, 7]))
841 );
842
843 let lhs =
844 Tensor::new_integer(IntegerStorage::U64(vec![u64::MAX]), vec![1, 1]).expect("lhs");
845 assert_eq!(
846 mod_builtin(Value::Tensor(lhs), Value::Num(3.0)).expect("mod"),
847 Value::Int(IntValue::U64(0))
848 );
849 }
850
851 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
852 #[test]
853 #[cfg(feature = "wgpu")]
854 fn mod_wgpu_matches_cpu() {
855 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
856 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
857 );
858 let numer = Tensor::new(vec![-5.0, -3.25, 0.0, 1.75, 6.5, 9.0], vec![3, 2]).unwrap();
859 let denom = Tensor::new(vec![4.0, -2.5, 3.0, 3.0, 2.0, -5.0], vec![3, 2]).unwrap();
860 let cpu_value =
861 mod_host(Value::Tensor(numer.clone()), Value::Tensor(denom.clone())).expect("cpu mod");
862
863 let provider = runmat_accelerate_api::provider().expect("wgpu provider registered");
864 let numer_handle = provider
865 .upload(&runmat_accelerate_api::HostTensorView {
866 data: &numer.materialize_f64(),
867 shape: &numer.shape,
868 })
869 .expect("upload numer");
870 let denom_handle = provider
871 .upload(&runmat_accelerate_api::HostTensorView {
872 data: &denom.materialize_f64(),
873 shape: &denom.shape,
874 })
875 .expect("upload denom");
876
877 let gpu_value = block_on(mod_gpu_pair(numer_handle, denom_handle)).expect("gpu mod");
878 let gpu_tensor = test_support::gather(gpu_value).expect("gather gpu result");
879
880 let cpu_tensor = match cpu_value {
881 Value::Tensor(t) => t,
882 Value::Num(n) => Tensor::new(vec![n], vec![1, 1]).expect("scalar tensor"),
883 other => panic!("unexpected CPU result {other:?}"),
884 };
885
886 assert_eq!(gpu_tensor.shape, cpu_tensor.shape);
887 let tol = match provider.precision() {
888 runmat_accelerate_api::ProviderPrecision::F64 => 1e-12,
889 runmat_accelerate_api::ProviderPrecision::F32 => 1e-5,
890 };
891 for (gpu, cpu) in gpu_tensor
892 .materialize_f64()
893 .iter()
894 .zip(cpu_tensor.materialize_f64().iter())
895 {
896 assert!(
897 (gpu - cpu).abs() <= tol,
898 "|{gpu} - {cpu}| exceeded tolerance {tol}"
899 );
900 }
901 }
902}