Skip to main content

kcl_lib/std/
math.rs

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