1use log::trace;
4use num_complex::Complex64;
5use runmat_builtins::{
6 BuiltinCompletionPolicy, BuiltinDescriptor, BuiltinErrorDescriptor, BuiltinExtensionDescriptor,
7 BuiltinExtensionMode, BuiltinIntegerBackendRule, BuiltinIntegerCapabilityDescriptor,
8 BuiltinIntegerComputationDomain, BuiltinIntegerInputAvailability,
9 BuiltinIntegerInputCapability, BuiltinIntegerOutputClassRule, BuiltinIntegerOverflowRule,
10 BuiltinIntegerOverloadKind, BuiltinIntegerScalarDoubleRule, BuiltinOutputMode,
11 BuiltinParamArity, BuiltinParamDescriptor, BuiltinParamType, BuiltinSignatureDescriptor,
12};
13use runmat_macros::runtime_builtin;
14use runmat_value::{ComplexTensor, Tensor, Value};
15
16use crate::builtins::common::random_args::complex_tensor_into_value;
17use crate::builtins::common::spec::{
18 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
19 ProviderHook, ReductionNaN, ResidencyPolicy, ScalarType, ShapeRequirements,
20};
21use crate::builtins::common::{tensor, tensor::tensor_into_value};
22use crate::builtins::math::poly::type_resolvers::polyder_type;
23use crate::dispatcher;
24use crate::{build_runtime_error, BuiltinResult, RuntimeError};
25
26const EPS: f64 = 1.0e-12;
27const BUILTIN_NAME: &str = "polyder";
28
29const POLYDER_OUTPUT_D: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
30 name: "d",
31 ty: BuiltinParamType::Any,
32 arity: BuiltinParamArity::Required,
33 default: None,
34 description: "Derivative coefficient vector.",
35}];
36
37const POLYDER_OUTPUT_NUM_DEN: [BuiltinParamDescriptor; 2] = [
38 BuiltinParamDescriptor {
39 name: "num",
40 ty: BuiltinParamType::Any,
41 arity: BuiltinParamArity::Required,
42 default: None,
43 description: "Quotient-rule numerator coefficients.",
44 },
45 BuiltinParamDescriptor {
46 name: "den",
47 ty: BuiltinParamType::Any,
48 arity: BuiltinParamArity::Required,
49 default: None,
50 description: "Quotient-rule denominator coefficients.",
51 },
52];
53
54const POLYDER_INPUTS_SINGLE: [BuiltinParamDescriptor; 1] = [BuiltinParamDescriptor {
55 name: "p",
56 ty: BuiltinParamType::Any,
57 arity: BuiltinParamArity::Required,
58 default: None,
59 description: "Polynomial coefficient vector.",
60}];
61
62const POLYDER_INPUTS_BINARY: [BuiltinParamDescriptor; 2] = [
63 BuiltinParamDescriptor {
64 name: "a",
65 ty: BuiltinParamType::Any,
66 arity: BuiltinParamArity::Required,
67 default: None,
68 description: "First polynomial coefficient vector.",
69 },
70 BuiltinParamDescriptor {
71 name: "b",
72 ty: BuiltinParamType::Any,
73 arity: BuiltinParamArity::Required,
74 default: None,
75 description: "Second polynomial coefficient vector.",
76 },
77];
78
79const POLYDER_SIGNATURES: [BuiltinSignatureDescriptor; 3] = [
80 BuiltinSignatureDescriptor {
81 label: "d = polyder(p)",
82 inputs: &POLYDER_INPUTS_SINGLE,
83 outputs: &POLYDER_OUTPUT_D,
84 },
85 BuiltinSignatureDescriptor {
86 label: "d = polyder(a, b)",
87 inputs: &POLYDER_INPUTS_BINARY,
88 outputs: &POLYDER_OUTPUT_D,
89 },
90 BuiltinSignatureDescriptor {
91 label: "[num, den] = polyder(u, v)",
92 inputs: &POLYDER_INPUTS_BINARY,
93 outputs: &POLYDER_OUTPUT_NUM_DEN,
94 },
95];
96
97const POLYDER_ERROR_INVALID_ARGUMENT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
98 code: "RM.POLYDER.INVALID_ARGUMENT",
99 identifier: Some("RunMat:polyder:InvalidArgument"),
100 when: "Input arity/output mode combination is invalid.",
101 message: "polyder: invalid argument",
102};
103
104const POLYDER_ERROR_INVALID_INPUT: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
105 code: "RM.POLYDER.INVALID_INPUT",
106 identifier: Some("RunMat:polyder:InvalidInput"),
107 when: "Inputs cannot be interpreted as numeric coefficient vectors.",
108 message: "polyder: invalid input",
109};
110
111const POLYDER_ERROR_INTERNAL: BuiltinErrorDescriptor = BuiltinErrorDescriptor {
112 code: "RM.POLYDER.INTERNAL",
113 identifier: Some("RunMat:polyder:Internal"),
114 when: "Runtime fails while building derivative outputs or provider fallback paths.",
115 message: "polyder: internal runtime failure",
116};
117
118const POLYDER_ERRORS: [BuiltinErrorDescriptor; 3] = [
119 POLYDER_ERROR_INVALID_ARGUMENT,
120 POLYDER_ERROR_INVALID_INPUT,
121 POLYDER_ERROR_INTERNAL,
122];
123
124pub const POLYDER_DESCRIPTOR: BuiltinDescriptor = BuiltinDescriptor {
125 signatures: &POLYDER_SIGNATURES,
126 output_mode: BuiltinOutputMode::ByRequestedOutputCount,
127 completion_policy: BuiltinCompletionPolicy::Public,
128 errors: &POLYDER_ERRORS,
129};
130
131const POLYDER_INTEGER_COEFFICIENTS_EXTENSION: BuiltinExtensionDescriptor =
132 BuiltinExtensionDescriptor {
133 id: "polyder-integer-coefficients",
134 mode: BuiltinExtensionMode::RunMatOnly,
135 description: "polyder accepts typed-integer coefficient vectors as a RunMat extension",
136 error_identifier: Some("RunMat:compatibility:PolyderIntegerCoefficientsExtension"),
137 };
138pub const POLYDER_EXTENSIONS: [BuiltinExtensionDescriptor; 1] =
139 [POLYDER_INTEGER_COEFFICIENTS_EXTENSION];
140const POLYDER_INTEGER_INPUTS: [BuiltinIntegerInputCapability; 1] =
141 [BuiltinIntegerInputCapability {
142 name: "p, a, or b",
143 classes: &crate::builtins::common::integer_capability::ALL_INTEGER_CLASSES,
144 availability: BuiltinIntegerInputAvailability::RunMatOnly,
145 scalar_double: BuiltinIntegerScalarDoubleRule::NotApplicable,
146 notes: "The compatibility target documents single and double polynomial coefficients. RunMat admits typed integers only after exact conversion to the floating polynomial domain.",
147 }];
148pub const POLYDER_INTEGER_CAPABILITIES: [BuiltinIntegerCapabilityDescriptor; 1] =
149 [BuiltinIntegerCapabilityDescriptor {
150 form: "d = polyder(integer_p) or d = polyder(integer_a,b) or [q,d] = polyder(integer_a,b)",
151 inputs: &POLYDER_INTEGER_INPUTS,
152 computation_domain: BuiltinIntegerComputationDomain::FloatingPoint,
153 output_class: BuiltinIntegerOutputClassRule::FunctionSpecific,
154 overflow: BuiltinIntegerOverflowRule::Error,
155 backend: BuiltinIntegerBackendRule::GatherFallback,
156 overload: BuiltinIntegerOverloadKind::Multiple,
157 notes: "Every integer coefficient operand is checked before provider dispatch; derivative, convolution, and quotient arithmetic then use the selected floating precision domain.",
158 }];
159
160#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::math::poly::polyder")]
161pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
162 name: "polyder",
163 op_kind: GpuOpKind::Custom("polynomial-derivative"),
164 supported_precisions: &[ScalarType::F32, ScalarType::F64],
165 broadcast: BroadcastSemantics::None,
166 provider_hooks: &[
167 ProviderHook::Custom("polyder-single"),
168 ProviderHook::Custom("polyder-product"),
169 ProviderHook::Custom("polyder-quotient"),
170 ],
171 constant_strategy: ConstantStrategy::InlineLiteral,
172 residency: ResidencyPolicy::NewHandle,
173 nan_mode: ReductionNaN::Include,
174 two_pass_threshold: None,
175 workgroup_size: None,
176 accepts_nan_mode: false,
177 notes: "Runs on-device when providers expose polyder hooks; falls back to the host for complex coefficients or unsupported shapes.",
178};
179
180fn polyder_error(message: impl Into<String>) -> RuntimeError {
181 polyder_error_with(message, &POLYDER_ERROR_INVALID_INPUT)
182}
183
184fn polyder_argument_error(message: impl Into<String>) -> RuntimeError {
185 polyder_error_with(message, &POLYDER_ERROR_INVALID_ARGUMENT)
186}
187
188fn polyder_error_with(
189 message: impl Into<String>,
190 error: &'static BuiltinErrorDescriptor,
191) -> RuntimeError {
192 let mut builder = build_runtime_error(message).with_builtin(BUILTIN_NAME);
193 if let Some(identifier) = error.identifier {
194 builder = builder.with_identifier(identifier);
195 }
196 builder.build()
197}
198
199#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::math::poly::polyder")]
200pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
201 name: "polyder",
202 shape: ShapeRequirements::Any,
203 constant_strategy: ConstantStrategy::InlineLiteral,
204 elementwise: None,
205 reduction: None,
206 emits_nan: false,
207 notes: "Symbolic operation on coefficient vectors; fusion bypasses this builtin.",
208};
209
210#[runtime_builtin(
211 name = "polyder",
212 category = "math/poly",
213 summary = "Differentiate polynomials, products, and ratios.",
214 keywords = "polyder,polynomial,derivative,product,quotient",
215 type_resolver(polyder_type),
216 descriptor(crate::builtins::math::poly::polyder::POLYDER_DESCRIPTOR),
217 extensions(crate::builtins::math::poly::polyder::POLYDER_EXTENSIONS),
218 integer_capabilities(crate::builtins::math::poly::polyder::POLYDER_INTEGER_CAPABILITIES),
219 builtin_path = "crate::builtins::math::poly::polyder"
220)]
221async fn polyder_builtin(first: Value, rest: Vec<Value>) -> crate::BuiltinResult<Value> {
222 if rest.len() > 1 {
223 return Err(polyder_argument_error("polyder: too many input arguments"));
224 }
225 if let Some(out_count) = crate::output_count::current_output_count() {
226 if out_count <= 1 {
227 let result = match rest.len() {
228 0 => derivative_single(first).await,
229 1 => derivative_product(first, rest.into_iter().next().unwrap()).await,
230 _ => unreachable!("input count validated above"),
231 }?;
232 if out_count == 0 {
233 return Ok(Value::OutputList(Vec::new()));
234 }
235 return Ok(Value::OutputList(vec![result]));
236 }
237 if rest.len() != 1 {
238 return Err(polyder_argument_error(
239 "Not enough input arguments for quotient form.",
240 ));
241 }
242 let eval = evaluate_quotient(first, rest.into_iter().next().unwrap()).await?;
243 let outputs = vec![eval.numerator(), eval.denominator()];
244 return Ok(crate::output_count::output_list_with_padding(
245 out_count, outputs,
246 ));
247 }
248 match rest.len() {
249 0 => derivative_single(first).await,
250 1 => derivative_product(first, rest.into_iter().next().unwrap()).await,
251 _ => unreachable!("input count validated above"),
252 }
253}
254
255async fn try_gpu_derivative_single(value: &Value) -> BuiltinResult<Option<Value>> {
256 let Value::GpuTensor(handle) = value else {
257 return Ok(None);
258 };
259 let Some(provider) = runmat_accelerate_api::provider() else {
260 return Ok(None);
261 };
262 match provider.polyder_single(handle).await {
263 Ok(out) => Ok(Some(Value::GpuTensor(out))),
264 Err(err) => {
265 trace!("polyder: provider polyder_single fallback: {err}");
266 Ok(None)
267 }
268 }
269}
270
271async fn try_gpu_derivative_product(first: &Value, second: &Value) -> BuiltinResult<Option<Value>> {
272 match (first, second) {
273 (Value::GpuTensor(p), Value::GpuTensor(q)) => {
274 let Some(provider) = runmat_accelerate_api::provider() else {
275 return Ok(None);
276 };
277 match provider.polyder_product(p, q).await {
278 Ok(out) => Ok(Some(Value::GpuTensor(out))),
279 Err(err) => {
280 trace!("polyder: provider polyder_product fallback: {err}");
281 Ok(None)
282 }
283 }
284 }
285 _ => Ok(None),
286 }
287}
288
289async fn try_gpu_quotient(u: &Value, v: &Value) -> BuiltinResult<Option<PolyderEval>> {
290 match (u, v) {
291 (Value::GpuTensor(uh), Value::GpuTensor(vh)) => {
292 let Some(provider) = runmat_accelerate_api::provider() else {
293 return Ok(None);
294 };
295 match provider.polyder_quotient(uh, vh).await {
296 Ok(result) => Ok(Some(PolyderEval {
297 numerator: Value::GpuTensor(result.numerator),
298 denominator: Value::GpuTensor(result.denominator),
299 })),
300 Err(err) => {
301 trace!("polyder: provider polyder_quotient fallback: {err}");
302 Ok(None)
303 }
304 }
305 }
306 _ => Ok(None),
307 }
308}
309
310pub async fn evaluate_quotient(u: Value, v: Value) -> BuiltinResult<PolyderEval> {
312 gate_coefficient(&u).await?;
313 gate_coefficient(&v).await?;
314 if let Some(eval) = try_gpu_quotient(&u, &v).await? {
315 return Ok(eval);
316 }
317 let u_poly = parse_polynomial("polyder", "U", u).await?;
318 let v_poly = parse_polynomial("polyder", "V", v).await?;
319 let numerator = quotient_numerator(&u_poly, &v_poly)?;
320 let denominator = quotient_denominator(&v_poly)?;
321 Ok(PolyderEval {
322 numerator,
323 denominator,
324 })
325}
326
327#[derive(Clone)]
329pub struct PolyderEval {
330 numerator: Value,
331 denominator: Value,
332}
333
334impl PolyderEval {
335 pub fn numerator(&self) -> Value {
337 self.numerator.clone()
338 }
339
340 pub fn denominator(&self) -> Value {
342 self.denominator.clone()
343 }
344}
345
346pub async fn derivative_single(value: Value) -> BuiltinResult<Value> {
347 gate_coefficient(&value).await?;
348 if let Some(out) = try_gpu_derivative_single(&value).await? {
349 return Ok(out);
350 }
351 let poly = parse_polynomial("polyder", "P", value).await?;
352 differentiate_polynomial(&poly)
353}
354
355pub async fn derivative_product(first: Value, second: Value) -> BuiltinResult<Value> {
356 gate_coefficient(&first).await?;
357 gate_coefficient(&second).await?;
358 if let Some(out) = try_gpu_derivative_product(&first, &second).await? {
359 return Ok(out);
360 }
361 let p = parse_polynomial("polyder", "P", first).await?;
362 let q = parse_polynomial("polyder", "A", second).await?;
363 product_derivative(&p, &q)
364}
365
366async fn gate_coefficient(value: &Value) -> BuiltinResult<()> {
367 crate::builtins::common::validation::reject_typed_complex_integer(value, BUILTIN_NAME)?;
368 crate::builtins::common::validation::ensure_runmat_integer_f64_boundary(
369 value,
370 &POLYDER_INTEGER_COEFFICIENTS_EXTENSION,
371 BUILTIN_NAME,
372 "coefficient",
373 )
374 .await
375}
376
377fn quotient_numerator(u: &Polynomial, v: &Polynomial) -> BuiltinResult<Value> {
378 let du = raw_derivative(&u.coeffs);
379 let dv = raw_derivative(&v.coeffs);
380 let term1 = poly_convolve(&du, &v.coeffs);
381 let term2 = poly_convolve(&u.coeffs, &dv);
382 let mut numerator = poly_sub(&term1, &term2);
383 numerator = trim_leading_zeros(&numerator);
384 coeffs_to_value(&numerator, u.orientation)
385}
386
387fn quotient_denominator(v: &Polynomial) -> BuiltinResult<Value> {
388 let mut denominator = poly_convolve(&v.coeffs, &v.coeffs);
389 denominator = trim_leading_zeros(&denominator);
390 coeffs_to_value(&denominator, v.orientation)
391}
392
393fn differentiate_polynomial(poly: &Polynomial) -> BuiltinResult<Value> {
394 let mut coeffs = raw_derivative(&poly.coeffs);
395 coeffs = trim_leading_zeros(&coeffs);
396 coeffs_to_value(&coeffs, poly.orientation)
397}
398
399fn product_derivative(p: &Polynomial, q: &Polynomial) -> BuiltinResult<Value> {
400 let dp = raw_derivative(&p.coeffs);
401 let dq = raw_derivative(&q.coeffs);
402 let term1 = poly_convolve(&dp, &q.coeffs);
403 let term2 = poly_convolve(&p.coeffs, &dq);
404 let mut result = poly_add(&term1, &term2);
405 result = trim_leading_zeros(&result);
406 coeffs_to_value(&result, p.orientation)
407}
408
409fn raw_derivative(coeffs: &[Complex64]) -> Vec<Complex64> {
410 if coeffs.len() <= 1 {
411 return vec![Complex64::new(0.0, 0.0)];
412 }
413 let mut output = Vec::with_capacity(coeffs.len() - 1);
414 let mut power = coeffs.len() - 1;
415 for coeff in coeffs.iter().take(coeffs.len() - 1) {
416 output.push(*coeff * (power as f64));
417 power -= 1;
418 }
419 output
420}
421
422fn poly_convolve(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
423 if a.is_empty() || b.is_empty() {
424 return Vec::new();
425 }
426 let mut result = vec![Complex64::new(0.0, 0.0); a.len() + b.len() - 1];
427 for (i, &ai) in a.iter().enumerate() {
428 for (j, &bj) in b.iter().enumerate() {
429 result[i + j] += ai * bj;
430 }
431 }
432 result
433}
434
435fn poly_add(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
436 let len = a.len().max(b.len());
437 let mut result = vec![Complex64::new(0.0, 0.0); len];
438 for (idx, &value) in a.iter().enumerate() {
439 result[len - a.len() + idx] += value;
440 }
441 for (idx, &value) in b.iter().enumerate() {
442 result[len - b.len() + idx] += value;
443 }
444 result
445}
446
447fn poly_sub(a: &[Complex64], b: &[Complex64]) -> Vec<Complex64> {
448 let len = a.len().max(b.len());
449 let mut result = vec![Complex64::new(0.0, 0.0); len];
450 for (idx, &value) in a.iter().enumerate() {
451 result[len - a.len() + idx] += value;
452 }
453 for (idx, &value) in b.iter().enumerate() {
454 result[len - b.len() + idx] -= value;
455 }
456 result
457}
458
459fn trim_leading_zeros(coeffs: &[Complex64]) -> Vec<Complex64> {
460 let mut first = None;
461 for (idx, coeff) in coeffs.iter().enumerate() {
462 if coeff.norm() > EPS {
463 first = Some(idx);
464 break;
465 }
466 }
467 match first {
468 Some(idx) => coeffs[idx..].to_vec(),
469 None => vec![Complex64::new(0.0, 0.0)],
470 }
471}
472
473fn coeffs_to_value(coeffs: &[Complex64], orientation: Orientation) -> BuiltinResult<Value> {
474 if coeffs.iter().all(|c| c.im.abs() <= EPS) {
475 let data: Vec<f64> = coeffs.iter().map(|c| c.re).collect();
476 let shape = orientation.shape_for_len(data.len());
477 let tensor =
478 Tensor::new(data, shape).map_err(|e| polyder_error(format!("polyder: {e}")))?;
479 Ok(tensor_into_value(tensor))
480 } else {
481 let data: Vec<(f64, f64)> = coeffs.iter().map(|c| (c.re, c.im)).collect();
482 let shape = orientation.shape_for_len(data.len());
483 let tensor =
484 ComplexTensor::new(data, shape).map_err(|e| polyder_error(format!("polyder: {e}")))?;
485 Ok(complex_tensor_into_value(tensor))
486 }
487}
488
489async fn parse_polynomial(context: &str, label: &str, value: Value) -> BuiltinResult<Polynomial> {
490 let gathered = dispatcher::gather_if_needed_async(&value).await?;
491 let (coeffs, orientation) = match gathered {
492 Value::Tensor(tensor) => {
493 ensure_vector_shape(context, label, &tensor.shape)?;
494 let orientation = orientation_from_shape(&tensor.shape);
495 if tensor::tensor_element_len(&tensor) == 0 {
496 (vec![Complex64::new(0.0, 0.0)], orientation)
497 } else {
498 (
499 tensor::tensor_values_f64(&tensor)
500 .into_iter()
501 .map(|re| Complex64::new(re, 0.0))
502 .collect(),
503 orientation,
504 )
505 }
506 }
507 Value::ComplexTensor(tensor) => {
508 ensure_vector_shape(context, label, &tensor.shape)?;
509 let orientation = orientation_from_shape(&tensor.shape);
510 if tensor::complex_tensor_element_len(&tensor) == 0 {
511 (vec![Complex64::new(0.0, 0.0)], orientation)
512 } else {
513 (
514 tensor::complex_tensor_into_values_complex64(tensor),
515 orientation,
516 )
517 }
518 }
519 Value::LogicalArray(logical) => {
520 let tensor = tensor::logical_to_tensor(&logical).map_err(polyder_error)?;
521 ensure_vector_shape(context, label, &tensor.shape)?;
522 let orientation = orientation_from_shape(&tensor.shape);
523 if tensor.is_empty() {
524 (vec![Complex64::new(0.0, 0.0)], orientation)
525 } else {
526 (
527 tensor::tensor_values_f64(&tensor)
528 .into_iter()
529 .map(|re| Complex64::new(re, 0.0))
530 .collect(),
531 orientation,
532 )
533 }
534 }
535 Value::Num(n) => (vec![Complex64::new(n, 0.0)], Orientation::Scalar),
536 Value::Int(i) => (vec![Complex64::new(i.to_f64(), 0.0)], Orientation::Scalar),
537 Value::Bool(b) => (
538 vec![Complex64::new(if b { 1.0 } else { 0.0 }, 0.0)],
539 Orientation::Scalar,
540 ),
541 Value::Complex(re, im) => (vec![Complex64::new(re, im)], Orientation::Scalar),
542 other => {
543 return Err(polyder_error(format!(
544 "{context}: expected {label} to be a numeric vector, got {other:?}"
545 )));
546 }
547 };
548
549 Ok(Polynomial {
550 coeffs,
551 orientation,
552 })
553}
554
555fn ensure_vector_shape(context: &str, label: &str, shape: &[usize]) -> BuiltinResult<()> {
556 let non_unit = shape.iter().copied().filter(|&dim| dim > 1).count();
557 if non_unit <= 1 {
558 Ok(())
559 } else {
560 Err(polyder_error(format!(
561 "{context}: {label} must be a vector of coefficients"
562 )))
563 }
564}
565
566#[derive(Clone, Copy)]
567enum Orientation {
568 Scalar,
569 Row,
570 Column,
571}
572
573impl Orientation {
574 fn shape_for_len(self, len: usize) -> Vec<usize> {
575 if len <= 1 {
576 return vec![1, 1];
577 }
578 match self {
579 Orientation::Scalar => vec![1, len],
580 Orientation::Row => vec![1, len],
581 Orientation::Column => vec![len, 1],
582 }
583 }
584}
585
586fn orientation_from_shape(shape: &[usize]) -> Orientation {
587 for (idx, &dim) in shape.iter().enumerate() {
588 if dim != 1 {
589 return match idx {
590 0 => Orientation::Column,
591 1 => Orientation::Row,
592 _ => Orientation::Column,
593 };
594 }
595 }
596 Orientation::Scalar
597}
598
599#[derive(Clone)]
600struct Polynomial {
601 coeffs: Vec<Complex64>,
602 orientation: Orientation,
603}
604
605#[cfg(test)]
606pub(crate) mod tests {
607 use super::*;
608 use crate::builtins::common::test_support;
609 use futures::executor::block_on;
610 use runmat_value::{IntValue, IntegerComplexStorage, IntegerStorage, Tensor};
611
612 fn assert_error_contains(err: crate::RuntimeError, needle: &str) {
613 assert!(
614 err.message().contains(needle),
615 "expected error containing '{needle}', got '{}'",
616 err.message()
617 );
618 }
619
620 #[test]
621 fn polyder_descriptor_signatures_cover_core_forms() {
622 let labels: Vec<&str> = POLYDER_DESCRIPTOR
623 .signatures
624 .iter()
625 .map(|signature| signature.label)
626 .collect();
627 assert!(labels.contains(&"d = polyder(p)"));
628 assert!(labels.contains(&"d = polyder(a, b)"));
629 assert!(labels.contains(&"[num, den] = polyder(u, v)"));
630 }
631
632 #[test]
633 fn polyder_descriptor_errors_have_stable_codes() {
634 let codes: Vec<&str> = POLYDER_DESCRIPTOR
635 .errors
636 .iter()
637 .map(|error| error.code)
638 .collect();
639 assert!(codes.contains(&"RM.POLYDER.INVALID_ARGUMENT"));
640 assert!(codes.contains(&"RM.POLYDER.INVALID_INPUT"));
641 assert!(codes.contains(&"RM.POLYDER.INTERNAL"));
642 }
643
644 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
645 #[test]
646 fn derivative_of_cubic_polynomial_is_correct() {
647 let tensor = Tensor::new(vec![3.0, -2.0, 5.0, 7.0], vec![1, 4]).unwrap();
648 let result = derivative_single(Value::Tensor(tensor)).expect("polyder");
649 match result {
650 Value::Tensor(t) => {
651 assert_eq!(t.shape, vec![1, 3]);
652 assert!(t
653 .materialize_f64()
654 .iter()
655 .zip([9.0, -4.0, 5.0])
656 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
657 }
658 other => panic!("expected tensor result, got {other:?}"),
659 }
660 }
661
662 #[test]
663 fn derivative_typed_integer_coefficients_cross_double_boundary_exactly() {
664 let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
665 let tensor =
666 Tensor::new_integer(IntegerStorage::I16(vec![3, -2, 5, 7]), vec![1, 4]).unwrap();
667 let result = derivative_single(Value::Tensor(tensor)).expect("polyder");
668 match result {
669 Value::Tensor(t) => {
670 assert_eq!(t.shape, vec![1, 3]);
671 assert!(t
672 .materialize_f64()
673 .iter()
674 .zip([9.0, -4.0, 5.0])
675 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
676 assert!(t.integer_storage().is_none());
677 }
678 other => panic!("expected tensor result, got {other:?}"),
679 }
680 }
681
682 #[test]
683 fn derivative_typed_complex_integer_coefficients_reject_before_conversion() {
684 let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
685 let tensor = ComplexTensor::new_integer(
686 IntegerComplexStorage::new(
687 IntegerStorage::I16(vec![3, -2, 5, 7]),
688 IntegerStorage::I16(vec![1, 0, -1, 2]),
689 )
690 .expect("complex integer storage"),
691 vec![1, 4],
692 )
693 .expect("complex integer tensor");
694
695 let error = derivative_single(Value::ComplexTensor(tensor))
696 .expect_err("typed complex integer arithmetic must reject");
697 assert!(
698 error
699 .message()
700 .contains("complex numbers with integer types are not supported"),
701 "{error:?}"
702 );
703 }
704
705 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
706 #[test]
707 fn derivative_of_product_matches_manual_rule() {
708 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
709 let a = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
710 let result =
711 derivative_product(Value::Tensor(p), Value::Tensor(a)).expect("polyder product");
712 match result {
713 Value::Tensor(t) => {
714 assert_eq!(t.shape, vec![1, 3]);
715 assert!(t
716 .materialize_f64()
717 .iter()
718 .zip([3.0, 2.0, -2.0])
719 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
720 }
721 other => panic!("expected tensor result, got {other:?}"),
722 }
723 }
724
725 #[test]
726 fn product_typed_integer_coefficients_cross_double_boundary_exactly() {
727 let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
728 let p = Tensor::new_integer(IntegerStorage::I16(vec![1, 0, -2]), vec![1, 3]).unwrap();
729 let a = Tensor::new_integer(IntegerStorage::U16(vec![1, 1]), vec![1, 2]).unwrap();
730 let result =
731 derivative_product(Value::Tensor(p), Value::Tensor(a)).expect("polyder product");
732 match result {
733 Value::Tensor(t) => {
734 assert_eq!(t.shape, vec![1, 3]);
735 assert!(t
736 .materialize_f64()
737 .iter()
738 .zip([3.0, 2.0, -2.0])
739 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
740 assert!(t.integer_storage().is_none());
741 }
742 other => panic!("expected tensor result, got {other:?}"),
743 }
744 }
745
746 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
747 #[test]
748 fn quotient_rule_produces_expected_num_and_den() {
749 let u = Tensor::new(vec![1.0, 0.0, -4.0], vec![1, 3]).unwrap();
750 let v = Tensor::new(vec![1.0, -1.0], vec![1, 2]).unwrap();
751 let eval = evaluate_quotient(Value::Tensor(u), Value::Tensor(v)).expect("polyder quotient");
752 match eval.numerator() {
753 Value::Tensor(t) => {
754 assert_eq!(t.shape, vec![1, 3]);
755 assert!(t
756 .materialize_f64()
757 .iter()
758 .zip([1.0, -2.0, 4.0])
759 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
760 }
761 other => panic!("expected tensor numerator, got {other:?}"),
762 }
763 match eval.denominator() {
764 Value::Tensor(t) => {
765 assert_eq!(t.shape, vec![1, 3]);
766 assert!(t
767 .materialize_f64()
768 .iter()
769 .zip([1.0, -2.0, 1.0])
770 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
771 }
772 other => panic!("expected tensor denominator, got {other:?}"),
773 }
774 }
775
776 #[test]
777 fn quotient_typed_integer_coefficients_cross_double_boundary_exactly() {
778 let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
779 let u = Tensor::new_integer(IntegerStorage::I16(vec![1, 0, -4]), vec![1, 3]).unwrap();
780 let v = Tensor::new_integer(IntegerStorage::I16(vec![1, -1]), vec![1, 2]).unwrap();
781 let eval = evaluate_quotient(Value::Tensor(u), Value::Tensor(v)).expect("polyder quotient");
782 match eval.numerator() {
783 Value::Tensor(t) => {
784 assert_eq!(t.shape, vec![1, 3]);
785 assert!(t
786 .materialize_f64()
787 .iter()
788 .zip([1.0, -2.0, 4.0])
789 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
790 assert!(t.integer_storage().is_none());
791 }
792 other => panic!("expected tensor numerator, got {other:?}"),
793 }
794 match eval.denominator() {
795 Value::Tensor(t) => {
796 assert_eq!(t.shape, vec![1, 3]);
797 assert!(t
798 .materialize_f64()
799 .iter()
800 .zip([1.0, -2.0, 1.0])
801 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
802 assert!(t.integer_storage().is_none());
803 }
804 other => panic!("expected tensor denominator, got {other:?}"),
805 }
806 }
807
808 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
809 #[test]
810 fn column_vector_orientation_is_preserved() {
811 let tensor = Tensor::new(vec![1.0, 0.0, -3.0], vec![3, 1]).unwrap();
812 let result = derivative_single(Value::Tensor(tensor)).expect("polyder column");
813 match result {
814 Value::Tensor(t) => {
815 assert_eq!(t.shape, vec![2, 1]);
816 assert!(t
817 .materialize_f64()
818 .iter()
819 .zip([2.0, 0.0])
820 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
821 }
822 other => panic!("expected column tensor, got {other:?}"),
823 }
824 }
825
826 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
827 #[test]
828 fn complex_coefficients_are_supported() {
829 let tensor =
830 ComplexTensor::new(vec![(1.0, 2.0), (-3.0, 0.0), (0.0, 4.0)], vec![1, 3]).unwrap();
831 let result = derivative_single(Value::ComplexTensor(tensor)).expect("polyder complex");
832 match result {
833 Value::ComplexTensor(t) => {
834 assert_eq!(t.shape, vec![1, 2]);
835 let expected = [Complex64::new(2.0, 4.0), Complex64::new(-3.0, 0.0)];
836 assert!(t.materialize_f64().iter().zip(expected.iter()).all(
837 |((re, im), expected)| {
838 (re - expected.re).abs() < 1e-12 && (im - expected.im).abs() < 1e-12
839 }
840 ));
841 }
842 other => panic!("expected complex tensor, got {other:?}"),
843 }
844 }
845
846 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
847 #[test]
848 fn empty_polynomial_returns_zero() {
849 let tensor = Tensor::new(Vec::new(), vec![1, 0]).unwrap();
850 let result = derivative_single(Value::Tensor(tensor)).expect("polyder empty");
851 assert_eq!(result, Value::Num(0.0));
852 }
853
854 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
855 #[test]
856 fn rejects_matrix_input() {
857 let tensor = Tensor::new(vec![1.0, 2.0, 3.0, 4.0], vec![2, 2]).unwrap();
858 let err = derivative_single(Value::Tensor(tensor)).unwrap_err();
859 assert_error_contains(err, "vector of coefficients");
860 }
861
862 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
863 #[test]
864 fn rejects_string_input() {
865 let err = derivative_single(Value::String("abc".into())).unwrap_err();
866 assert_error_contains(err, "numeric vector");
867 }
868
869 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
870 #[test]
871 fn mixed_gpu_cpu_product_falls_back_to_host() {
872 test_support::with_test_provider(|provider| {
873 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
874 let q = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
875 let cpu_expected =
876 derivative_product(Value::Tensor(p.clone()), Value::Tensor(q.clone()))
877 .expect("cpu product");
878 let Value::Tensor(cpu_tensor) = cpu_expected else {
879 panic!("expected tensor result");
880 };
881
882 let view_p = runmat_accelerate_api::HostTensorView {
883 data: &p.materialize_f64(),
884 shape: &p.shape,
885 };
886 let handle_p = provider.upload(&view_p).expect("upload p");
887 let result = derivative_product(Value::GpuTensor(handle_p), Value::Tensor(q))
888 .expect("mixed product");
889 let Value::Tensor(host_tensor) = result else {
890 panic!("expected host tensor result");
891 };
892 assert_eq!(host_tensor.shape, cpu_tensor.shape);
893 assert!(host_tensor
894 .materialize_f64()
895 .iter()
896 .zip(cpu_tensor.materialize_f64().iter())
897 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
898 });
899 }
900
901 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
902 #[test]
903 fn builtin_rejects_too_many_inputs() {
904 let err = futures::executor::block_on(super::polyder_builtin(
905 Value::Num(1.0),
906 vec![Value::Num(2.0), Value::Num(3.0)],
907 ))
908 .unwrap_err();
909 assert_eq!(err.identifier(), POLYDER_ERROR_INVALID_ARGUMENT.identifier);
910 assert_error_contains(err, "too many input arguments");
911 }
912
913 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
914 #[test]
915 fn gpu_inputs_remain_on_device() {
916 test_support::with_test_provider(|provider| {
917 let tensor = Tensor::new(vec![2.0, 0.0, -5.0, 4.0], vec![1, 4]).unwrap();
918 let view = runmat_accelerate_api::HostTensorView {
919 data: &tensor.materialize_f64(),
920 shape: &tensor.shape,
921 };
922 let handle = provider.upload(&view).expect("upload");
923 let result = derivative_single(Value::GpuTensor(handle)).expect("polyder gpu");
924 let Value::GpuTensor(out_handle) = result else {
925 panic!("expected GPU tensor result");
926 };
927 let gathered = test_support::gather(Value::GpuTensor(out_handle)).expect("gather");
928 assert_eq!(gathered.shape, vec![1, 3]);
929 assert!(gathered
930 .materialize_f64()
931 .iter()
932 .zip([6.0, 0.0, -5.0])
933 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
934 });
935 }
936
937 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
938 #[test]
939 fn gpu_product_matches_cpu() {
940 test_support::with_test_provider(|provider| {
941 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
942 let q = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
943 let expected = derivative_product(Value::Tensor(p.clone()), Value::Tensor(q.clone()))
944 .expect("cpu product");
945 let Value::Tensor(expected_tensor) = expected else {
946 panic!("expected tensor output");
947 };
948
949 let view_p = runmat_accelerate_api::HostTensorView {
950 data: &p.materialize_f64(),
951 shape: &p.shape,
952 };
953 let view_q = runmat_accelerate_api::HostTensorView {
954 data: &q.materialize_f64(),
955 shape: &q.shape,
956 };
957 let handle_p = provider.upload(&view_p).expect("upload p");
958 let handle_q = provider.upload(&view_q).expect("upload q");
959 let gpu_result =
960 derivative_product(Value::GpuTensor(handle_p), Value::GpuTensor(handle_q))
961 .expect("gpu product");
962 let Value::GpuTensor(gpu_handle) = gpu_result else {
963 panic!("expected GPU tensor");
964 };
965 let gathered = test_support::gather(Value::GpuTensor(gpu_handle)).expect("gather");
966 assert_eq!(gathered.shape, expected_tensor.shape);
967 assert!(gathered
968 .materialize_f64()
969 .iter()
970 .zip(expected_tensor.materialize_f64().iter())
971 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
972 });
973 }
974
975 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
976 #[test]
977 fn gpu_quotient_matches_cpu() {
978 test_support::with_test_provider(|provider| {
979 let u = Tensor::new(vec![1.0, 0.0, -4.0], vec![1, 3]).unwrap();
980 let v = Tensor::new(vec![1.0, -1.0], vec![1, 2]).unwrap();
981 let expected = evaluate_quotient(Value::Tensor(u.clone()), Value::Tensor(v.clone()))
982 .expect("cpu quotient");
983 let Value::Tensor(expected_num) = expected.numerator() else {
984 panic!("expected tensor numerator");
985 };
986 let Value::Tensor(expected_den) = expected.denominator() else {
987 panic!("expected tensor denominator");
988 };
989
990 let view_u = runmat_accelerate_api::HostTensorView {
991 data: &u.materialize_f64(),
992 shape: &u.shape,
993 };
994 let view_v = runmat_accelerate_api::HostTensorView {
995 data: &v.materialize_f64(),
996 shape: &v.shape,
997 };
998 let handle_u = provider.upload(&view_u).expect("upload u");
999 let handle_v = provider.upload(&view_v).expect("upload v");
1000 let gpu_eval =
1001 evaluate_quotient(Value::GpuTensor(handle_u), Value::GpuTensor(handle_v))
1002 .expect("gpu quotient");
1003 let gpu_num = test_support::gather(gpu_eval.numerator()).expect("gather num");
1004 let gpu_den = test_support::gather(gpu_eval.denominator()).expect("gather den");
1005 assert_eq!(gpu_num.shape, expected_num.shape);
1006 assert_eq!(gpu_den.shape, expected_den.shape);
1007 assert!(gpu_num
1008 .materialize_f64()
1009 .iter()
1010 .zip(expected_num.materialize_f64().iter())
1011 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
1012 assert!(gpu_den
1013 .materialize_f64()
1014 .iter()
1015 .zip(expected_den.materialize_f64().iter())
1016 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
1017 });
1018 }
1019
1020 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1021 #[test]
1022 #[cfg(feature = "wgpu")]
1023 fn wgpu_polyder_single_matches_cpu() {
1024 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1025 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1026 );
1027 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1028 let tensor = Tensor::new(vec![3.0, -2.0, 5.0, 7.0], vec![1, 4]).unwrap();
1029 let expected = derivative_single(Value::Tensor(tensor.clone())).expect("cpu polyder");
1030 let Value::Tensor(expected_tensor) = expected else {
1031 panic!("expected tensor");
1032 };
1033 let view = runmat_accelerate_api::HostTensorView {
1034 data: &tensor.materialize_f64(),
1035 shape: &tensor.shape,
1036 };
1037 let handle = provider.upload(&view).expect("upload");
1038 let gpu_result = derivative_single(Value::GpuTensor(handle)).expect("gpu polyder");
1039 let gathered = test_support::gather(gpu_result).expect("gather");
1040 assert_eq!(gathered.shape, expected_tensor.shape);
1041 assert!(gathered
1042 .materialize_f64()
1043 .iter()
1044 .zip(expected_tensor.materialize_f64().iter())
1045 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
1046 }
1047
1048 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1049 #[test]
1050 #[cfg(feature = "wgpu")]
1051 fn wgpu_polyder_product_matches_cpu() {
1052 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1053 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1054 );
1055 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1056 let p = Tensor::new(vec![1.0, 0.0, -2.0], vec![1, 3]).unwrap();
1057 let q = Tensor::new(vec![1.0, 1.0], vec![1, 2]).unwrap();
1058 let expected = derivative_product(Value::Tensor(p.clone()), Value::Tensor(q.clone()))
1059 .expect("cpu product");
1060 let Value::Tensor(expected_tensor) = expected else {
1061 panic!("expected tensor");
1062 };
1063 let view_p = runmat_accelerate_api::HostTensorView {
1064 data: &p.materialize_f64(),
1065 shape: &p.shape,
1066 };
1067 let view_q = runmat_accelerate_api::HostTensorView {
1068 data: &q.materialize_f64(),
1069 shape: &q.shape,
1070 };
1071 let handle_p = provider.upload(&view_p).expect("upload p");
1072 let handle_q = provider.upload(&view_q).expect("upload q");
1073 let gpu_result = derivative_product(Value::GpuTensor(handle_p), Value::GpuTensor(handle_q))
1074 .expect("gpu product");
1075 let gathered = test_support::gather(gpu_result).expect("gather");
1076 assert_eq!(gathered.shape, expected_tensor.shape);
1077 assert!(gathered
1078 .materialize_f64()
1079 .iter()
1080 .zip(expected_tensor.materialize_f64().iter())
1081 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
1082 }
1083
1084 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1085 #[test]
1086 #[cfg(feature = "wgpu")]
1087 fn wgpu_polyder_quotient_matches_cpu() {
1088 let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
1089 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
1090 );
1091 let provider = runmat_accelerate_api::provider().expect("wgpu provider");
1092 let u = Tensor::new(vec![1.0, 0.0, -4.0], vec![1, 3]).unwrap();
1093 let v = Tensor::new(vec![1.0, -1.0], vec![1, 2]).unwrap();
1094 let expected = evaluate_quotient(Value::Tensor(u.clone()), Value::Tensor(v.clone()))
1095 .expect("cpu quotient");
1096 let expected_num = match expected.numerator() {
1097 Value::Tensor(t) => t,
1098 other => panic!("expected tensor numerator, got {other:?}"),
1099 };
1100 let expected_den = match expected.denominator() {
1101 Value::Tensor(t) => t,
1102 other => panic!("expected tensor denominator, got {other:?}"),
1103 };
1104 let view_u = runmat_accelerate_api::HostTensorView {
1105 data: &u.materialize_f64(),
1106 shape: &u.shape,
1107 };
1108 let view_v = runmat_accelerate_api::HostTensorView {
1109 data: &v.materialize_f64(),
1110 shape: &v.shape,
1111 };
1112 let handle_u = provider.upload(&view_u).expect("upload u");
1113 let handle_v = provider.upload(&view_v).expect("upload v");
1114 let gpu_eval = evaluate_quotient(Value::GpuTensor(handle_u), Value::GpuTensor(handle_v))
1115 .expect("gpu quotient");
1116 let gpu_num = test_support::gather(gpu_eval.numerator()).expect("gather num");
1117 let gpu_den = test_support::gather(gpu_eval.denominator()).expect("gather den");
1118 assert_eq!(gpu_num.shape, expected_num.shape);
1119 assert_eq!(gpu_den.shape, expected_den.shape);
1120 assert!(gpu_num
1121 .materialize_f64()
1122 .iter()
1123 .zip(expected_num.materialize_f64().iter())
1124 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
1125 assert!(gpu_den
1126 .materialize_f64()
1127 .iter()
1128 .zip(expected_den.materialize_f64().iter())
1129 .all(|(lhs, rhs)| (lhs - rhs).abs() < 1e-12));
1130 }
1131
1132 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
1133 #[test]
1134 fn derivative_promotes_integers() {
1135 let _extensions = crate::compatibility::push_runmat_extensions_enabled(true);
1136 let value = Value::Int(IntValue::I32(5));
1137 let result = derivative_single(value).expect("polyder int");
1138 assert_eq!(result, Value::Num(0.0));
1139 }
1140
1141 fn derivative_single(value: Value) -> BuiltinResult<Value> {
1142 block_on(super::derivative_single(value))
1143 }
1144
1145 fn derivative_product(first: Value, second: Value) -> BuiltinResult<Value> {
1146 block_on(super::derivative_product(first, second))
1147 }
1148
1149 fn evaluate_quotient(u: Value, v: Value) -> BuiltinResult<PolyderEval> {
1150 block_on(super::evaluate_quotient(u, v))
1151 }
1152}