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