Skip to main content

kcl_lib/std/
math.rs

1//! Functions related to mathematics.
2
3use anyhow::Result;
4
5use crate::CompilationError;
6use crate::errors::KclError;
7use crate::errors::KclErrorDetails;
8use crate::execution::ExecState;
9use crate::execution::KclValue;
10use crate::execution::annotations;
11use crate::execution::types::ArrayLen;
12use crate::execution::types::NumericType;
13use crate::execution::types::RuntimeType;
14use crate::std::args::Args;
15use crate::std::args::TyF64;
16
17/// Compute the remainder after dividing `num` by `div`.
18/// If `num` is negative, the result will be too.
19pub async fn rem(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
20    let n: TyF64 = args.get_unlabeled_kw_arg("number to divide", &RuntimeType::num_any(), exec_state)?;
21    let d: TyF64 = args.get_kw_arg("divisor", &RuntimeType::num_any(), exec_state)?;
22    let valid_d = d.n != 0.0;
23    if !valid_d {
24        exec_state.warn(
25            CompilationError::err(args.source_range, "Divisor cannot be 0".to_string()),
26            annotations::WARN_INVALID_MATH,
27        );
28    }
29
30    let (n, d, ty) = NumericType::combine_mod(n, d);
31    if ty == NumericType::Unknown {
32        exec_state.err(CompilationError::err(
33            args.source_range,
34            "Calling `rem` on numbers which have unknown or incompatible units.\n\nYou may need to add information about the type of the argument, for example:\n  using a numeric suffix: `42{ty}`\n  or using type ascription: `foo(): number({ty})`"
35        ));
36    }
37    let remainder = n % d;
38
39    Ok(args.make_user_val_from_f64_with_type(TyF64::new(remainder, ty)))
40}
41
42/// Compute the cosine of a number (in radians).
43pub async fn cos(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
44    let num: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::angle(), exec_state)?;
45    let num = num.to_radians(exec_state, args.source_range);
46    Ok(args.make_user_val_from_f64_with_type(TyF64::new(libm::cos(num), exec_state.current_default_units())))
47}
48
49/// Compute the sine of a number (in radians).
50pub async fn sin(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
51    let num: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::angle(), exec_state)?;
52    let num = num.to_radians(exec_state, args.source_range);
53    Ok(args.make_user_val_from_f64_with_type(TyF64::new(libm::sin(num), exec_state.current_default_units())))
54}
55
56/// Compute the tangent of a number (in radians).
57pub async fn tan(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
58    let num: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::angle(), exec_state)?;
59    let num = num.to_radians(exec_state, args.source_range);
60    Ok(args.make_user_val_from_f64_with_type(TyF64::new(libm::tan(num), exec_state.current_default_units())))
61}
62
63/// Compute the square root of a number.
64pub async fn sqrt(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
65    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
66
67    if input.n < 0.0 {
68        return Err(KclError::new_semantic(KclErrorDetails::new(
69            format!(
70                "Attempt to take square root (`sqrt`) of a number less than zero ({})",
71                input.n
72            ),
73            vec![args.source_range],
74        )));
75    }
76
77    let result = input.n.sqrt();
78
79    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
80}
81
82/// Compute the absolute value of a number.
83pub async fn abs(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
84    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
85    let result = input.n.abs();
86
87    Ok(args.make_user_val_from_f64_with_type(input.map_value(result)))
88}
89
90/// Round a number to the nearest integer.
91pub async fn round(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
92    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
93    let result = input.n.round();
94
95    Ok(args.make_user_val_from_f64_with_type(input.map_value(result)))
96}
97
98/// Compute the largest integer less than or equal to a number.
99pub async fn floor(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
100    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
101    let result = input.n.floor();
102
103    Ok(args.make_user_val_from_f64_with_type(input.map_value(result)))
104}
105
106/// Compute the smallest integer greater than or equal to a number.
107pub async fn ceil(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
108    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
109    let result = input.n.ceil();
110
111    Ok(args.make_user_val_from_f64_with_type(input.map_value(result)))
112}
113
114/// Compute the minimum of the given arguments.
115pub async fn min(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
116    let nums: Vec<TyF64> = args.get_unlabeled_kw_arg(
117        "input",
118        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Minimum(1)),
119        exec_state,
120    )?;
121    let (nums, ty) = NumericType::combine_eq_array(&nums);
122    if ty == NumericType::Unknown {
123        exec_state.warn(CompilationError::err(
124            args.source_range,
125            "Calling `min` on numbers which have unknown or incompatible units.\n\nYou may need to add information about the type of the argument, for example:\n  using a numeric suffix: `42{ty}`\n  or using type ascription: `foo(): number({ty})`",
126        ), annotations::WARN_UNKNOWN_UNITS);
127    }
128
129    let mut result = f64::MAX;
130    for num in nums {
131        if num < result {
132            result = num;
133        }
134    }
135
136    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, ty)))
137}
138
139/// Compute the maximum of the given arguments.
140pub async fn max(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
141    let nums: Vec<TyF64> = args.get_unlabeled_kw_arg(
142        "input",
143        &RuntimeType::Array(Box::new(RuntimeType::num_any()), ArrayLen::Minimum(1)),
144        exec_state,
145    )?;
146    let (nums, ty) = NumericType::combine_eq_array(&nums);
147    if ty == NumericType::Unknown {
148        exec_state.warn(CompilationError::err(
149            args.source_range,
150            "Calling `max` on numbers which have unknown or incompatible units.\n\nYou may need to add information about the type of the argument, for example:\n  using a numeric suffix: `42{ty}`\n  or using type ascription: `foo(): number({ty})`",
151        ), annotations::WARN_UNKNOWN_UNITS);
152    }
153
154    let mut result = f64::MIN;
155    for num in nums {
156        if num > result {
157            result = num;
158        }
159    }
160
161    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, ty)))
162}
163
164/// Compute the number to a power.
165pub async fn pow(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
166    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
167    let exp: TyF64 = args.get_kw_arg("exp", &RuntimeType::count(), exec_state)?;
168    let exp_is_int = exp.n.fract() == 0.0;
169    if input.n < 0.0 && !exp_is_int {
170        exec_state.warn(
171            CompilationError::err(
172                args.source_range,
173                format!(
174                    "Exponent must be an integer when input is negative, but it was {}",
175                    exp.n
176                ),
177            ),
178            annotations::WARN_INVALID_MATH,
179        );
180    }
181    let valid_input = !(input.n == 0.0 && exp.n < 0.0);
182    if !valid_input {
183        exec_state.warn(
184            CompilationError::err(args.source_range, "Input cannot be 0 when exp < 0".to_string()),
185            annotations::WARN_INVALID_MATH,
186        );
187    }
188    let result = input.n.powf(exp.n);
189
190    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
191}
192
193/// Compute the arccosine of a number (in radians).
194pub async fn acos(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
195    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::count(), exec_state)?;
196    let in_range = (-1.0..=1.0).contains(&input.n);
197    if !in_range {
198        exec_state.warn(
199            CompilationError::err(
200                args.source_range,
201                format!("The argument must be between -1 and 1, but it was {}", input.n),
202            ),
203            annotations::WARN_INVALID_MATH,
204        );
205    }
206    let result = libm::acos(input.n);
207
208    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::radians())))
209}
210
211/// Compute the arcsine of a number (in radians).
212pub async fn asin(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
213    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::count(), exec_state)?;
214    let in_range = (-1.0..=1.0).contains(&input.n);
215    if !in_range {
216        exec_state.warn(
217            CompilationError::err(
218                args.source_range,
219                format!("The argument must be between -1 and 1, but it was {}", input.n),
220            ),
221            annotations::WARN_INVALID_MATH,
222        );
223    }
224    let result = libm::asin(input.n);
225
226    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::radians())))
227}
228
229/// Compute the arctangent of a number (in radians).
230pub async fn atan(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
231    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::count(), exec_state)?;
232    let result = libm::atan(input.n);
233
234    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::radians())))
235}
236
237/// Compute the four quadrant arctangent of Y and X (in radians).
238pub async fn atan2(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
239    let y = args.get_kw_arg("y", &RuntimeType::length(), exec_state)?;
240    let x = args.get_kw_arg("x", &RuntimeType::length(), exec_state)?;
241    let (y, x, _) = NumericType::combine_eq_coerce(y, x, Some((exec_state, args.source_range)));
242    let result = libm::atan2(y, x);
243
244    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, NumericType::radians())))
245}
246
247/// Compute the logarithm of the number with respect to an arbitrary base.
248///
249/// The result might not be correctly rounded owing to implementation
250/// details; `log2()` can produce more accurate results for base 2,
251/// and `log10()` can produce more accurate results for base 10.
252pub async fn log(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
253    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
254    let base: TyF64 = args.get_kw_arg("base", &RuntimeType::count(), exec_state)?;
255    let valid_input = input.n > 0.0;
256    if !valid_input {
257        exec_state.warn(
258            CompilationError::err(args.source_range, format!("Input must be > 0, but it was {}", input.n)),
259            annotations::WARN_INVALID_MATH,
260        );
261    }
262    let valid_base = base.n > 0.0;
263    if !valid_base {
264        exec_state.warn(
265            CompilationError::err(args.source_range, format!("Base must be > 0, but it was {}", base.n)),
266            annotations::WARN_INVALID_MATH,
267        );
268    }
269    let base_not_1 = base.n != 1.0;
270    if !base_not_1 {
271        exec_state.warn(
272            CompilationError::err(args.source_range, "Base cannot be 1".to_string()),
273            annotations::WARN_INVALID_MATH,
274        );
275    }
276    let result = input.n.log(base.n);
277
278    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
279}
280
281/// Compute the base 2 logarithm of the number.
282pub async fn log2(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
283    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
284    let valid_input = input.n > 0.0;
285    if !valid_input {
286        exec_state.warn(
287            CompilationError::err(args.source_range, format!("Input must be > 0, but it was {}", input.n)),
288            annotations::WARN_INVALID_MATH,
289        );
290    }
291    let result = input.n.log2();
292
293    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
294}
295
296/// Compute the base 10 logarithm of the number.
297pub async fn log10(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
298    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
299    let valid_input = input.n > 0.0;
300    if !valid_input {
301        exec_state.warn(
302            CompilationError::err(args.source_range, format!("Input must be > 0, but it was {}", input.n)),
303            annotations::WARN_INVALID_MATH,
304        );
305    }
306    let result = input.n.log10();
307
308    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
309}
310
311/// Compute the natural logarithm of the number.
312pub async fn ln(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
313    let input: TyF64 = args.get_unlabeled_kw_arg("input", &RuntimeType::num_any(), exec_state)?;
314    let valid_input = input.n > 0.0;
315    if !valid_input {
316        exec_state.warn(
317            CompilationError::err(args.source_range, format!("Input must be > 0, but it was {}", input.n)),
318            annotations::WARN_INVALID_MATH,
319        );
320    }
321    let result = input.n.ln();
322
323    Ok(args.make_user_val_from_f64_with_type(TyF64::new(result, exec_state.current_default_units())))
324}
325
326/// Compute the length of the given leg.
327pub async fn leg_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
328    let hypotenuse: TyF64 = args.get_kw_arg("hypotenuse", &RuntimeType::length(), exec_state)?;
329    let leg: TyF64 = args.get_kw_arg("leg", &RuntimeType::length(), exec_state)?;
330    let (hypotenuse, leg, ty) = NumericType::combine_eq_coerce(hypotenuse, leg, Some((exec_state, args.source_range)));
331    let result = (hypotenuse.powi(2) - f64::min(hypotenuse.abs(), leg.abs()).powi(2)).sqrt();
332    Ok(KclValue::from_number_with_type(result, ty, vec![args.into()]))
333}
334
335/// Compute the angle of the given leg for x.
336pub async fn leg_angle_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
337    let hypotenuse: TyF64 = args.get_kw_arg("hypotenuse", &RuntimeType::length(), exec_state)?;
338    let leg: TyF64 = args.get_kw_arg("leg", &RuntimeType::length(), exec_state)?;
339    let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg, Some((exec_state, args.source_range)));
340    let valid_hypotenuse = hypotenuse > 0.0;
341    if !valid_hypotenuse {
342        exec_state.warn(
343            CompilationError::err(
344                args.source_range,
345                format!("Hypotenuse must be > 0, but it was {}", hypotenuse),
346            ),
347            annotations::WARN_INVALID_MATH,
348        );
349    }
350    let ratio = leg.min(hypotenuse) / hypotenuse;
351    let in_range = (-1.0..=1.0).contains(&ratio);
352    if !in_range {
353        exec_state.warn(
354            CompilationError::err(
355                args.source_range,
356                format!("The argument must be between -1 and 1, but it was {}", ratio),
357            ),
358            annotations::WARN_INVALID_MATH,
359        );
360    }
361    let result = libm::acos(ratio).to_degrees();
362    Ok(KclValue::from_number_with_type(
363        result,
364        NumericType::degrees(),
365        vec![args.into()],
366    ))
367}
368
369/// Compute the angle of the given leg for y.
370pub async fn leg_angle_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
371    let hypotenuse: TyF64 = args.get_kw_arg("hypotenuse", &RuntimeType::length(), exec_state)?;
372    let leg: TyF64 = args.get_kw_arg("leg", &RuntimeType::length(), exec_state)?;
373    let (hypotenuse, leg, _ty) = NumericType::combine_eq_coerce(hypotenuse, leg, Some((exec_state, args.source_range)));
374    let valid_hypotenuse = hypotenuse > 0.0;
375    if !valid_hypotenuse {
376        exec_state.warn(
377            CompilationError::err(
378                args.source_range,
379                format!("Hypotenuse must be > 0, but it was {}", hypotenuse),
380            ),
381            annotations::WARN_INVALID_MATH,
382        );
383    }
384    let ratio = leg.min(hypotenuse) / hypotenuse;
385    let in_range = (-1.0..=1.0).contains(&ratio);
386    if !in_range {
387        exec_state.warn(
388            CompilationError::err(
389                args.source_range,
390                format!("The argument must be between -1 and 1, but it was {}", ratio),
391            ),
392            annotations::WARN_INVALID_MATH,
393        );
394    }
395    let result = libm::asin(ratio).to_degrees();
396    Ok(KclValue::from_number_with_type(
397        result,
398        NumericType::degrees(),
399        vec![args.into()],
400    ))
401}