kcl_lib/std/
array.rs

1use kcl_derive_docs::stdlib;
2
3use super::{
4    args::{Arg, FromArgs},
5    Args,
6};
7use crate::{
8    errors::{KclError, KclErrorDetails},
9    execution::{
10        kcl_value::{FunctionSource, KclValue},
11        ExecState,
12    },
13    source_range::SourceRange,
14    ExecutorContext,
15};
16
17/// Apply a function to each element of an array.
18pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
19    let (array, f): (Vec<KclValue>, &FunctionSource) = FromArgs::from_args(&args, 0)?;
20    let meta = vec![args.source_range.into()];
21    let new_array = inner_map(array, f, exec_state, &args).await?;
22    Ok(KclValue::MixedArray { value: new_array, meta })
23}
24
25/// Apply a function to every element of a list.
26///
27/// Given a list like `[a, b, c]`, and a function like `f`, returns
28/// `[f(a), f(b), f(c)]`
29/// ```no_run
30/// r = 10 // radius
31/// fn drawCircle(id) {
32///   return startSketchOn("XY")
33///     |> circle( center= [id * 2 * r, 0], radius= r)
34/// }
35///
36/// // Call `drawCircle`, passing in each element of the array.
37/// // The outputs from each `drawCircle` form a new array,
38/// // which is the return value from `map`.
39/// circles = map(
40///   [1..3],
41///   drawCircle
42/// )
43/// ```
44/// ```no_run
45/// r = 10 // radius
46/// // Call `map`, using an anonymous function instead of a named one.
47/// circles = map(
48///   [1..3],
49///   fn(id) {
50///     return startSketchOn("XY")
51///       |> circle( center= [id * 2 * r, 0], radius= r)
52///   }
53/// )
54/// ```
55#[stdlib {
56    name = "map",
57}]
58async fn inner_map<'a>(
59    array: Vec<KclValue>,
60    map_fn: &'a FunctionSource,
61    exec_state: &mut ExecState,
62    args: &'a Args,
63) -> Result<Vec<KclValue>, KclError> {
64    let mut new_array = Vec::with_capacity(array.len());
65    for elem in array {
66        let new_elem = call_map_closure(elem, map_fn, args.source_range, exec_state, &args.ctx).await?;
67        new_array.push(new_elem);
68    }
69    Ok(new_array)
70}
71
72async fn call_map_closure(
73    input: KclValue,
74    map_fn: &FunctionSource,
75    source_range: SourceRange,
76    exec_state: &mut ExecState,
77    ctxt: &ExecutorContext,
78) -> Result<KclValue, KclError> {
79    let output = map_fn
80        .call(exec_state, ctxt, vec![Arg::synthetic(input)], source_range)
81        .await?;
82    let source_ranges = vec![source_range];
83    let output = output.ok_or_else(|| {
84        KclError::Semantic(KclErrorDetails {
85            message: "Map function must return a value".to_string(),
86            source_ranges,
87        })
88    })?;
89    Ok(output)
90}
91
92/// For each item in an array, update a value.
93pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
94    let (array, start, f): (Vec<KclValue>, KclValue, &FunctionSource) = FromArgs::from_args(&args, 0)?;
95    inner_reduce(array, start, f, exec_state, &args).await
96}
97
98/// Take a starting value. Then, for each element of an array, calculate the next value,
99/// using the previous value and the element.
100/// ```no_run
101/// // This function adds two numbers.
102/// fn add(a, b) { return a + b }
103///
104/// // This function adds an array of numbers.
105/// // It uses the `reduce` function, to call the `add` function on every
106/// // element of the `arr` parameter. The starting value is 0.
107/// fn sum(arr) { return reduce(arr, 0, add) }
108///
109/// /*
110/// The above is basically like this pseudo-code:
111/// fn sum(arr):
112///     sumSoFar = 0
113///     for i in arr:
114///         sumSoFar = add(sumSoFar, i)
115///     return sumSoFar
116/// */
117///
118/// // We use `assertEqual` to check that our `sum` function gives the
119/// // expected result. It's good to check your work!
120/// assertEqual(sum([1, 2, 3]), 6, 0.00001, "1 + 2 + 3 summed is 6")
121/// ```
122/// ```no_run
123/// // This example works just like the previous example above, but it uses
124/// // an anonymous `add` function as its parameter, instead of declaring a
125/// // named function outside.
126/// arr = [1, 2, 3]
127/// sum = reduce(arr, 0, (i, result_so_far) => { return i + result_so_far })
128///
129/// // We use `assertEqual` to check that our `sum` function gives the
130/// // expected result. It's good to check your work!
131/// assertEqual(sum, 6, 0.00001, "1 + 2 + 3 summed is 6")
132/// ```
133/// ```no_run
134/// // Declare a function that sketches a decagon.
135/// fn decagon(radius) {
136///   // Each side of the decagon is turned this many degrees from the previous angle.
137///   stepAngle = (1/10) * TAU
138///
139///   // Start the decagon sketch at this point.
140///   startOfDecagonSketch = startSketchOn('XY')
141///     |> startProfileAt([(cos(0)*radius), (sin(0) * radius)], %)
142///
143///   // Use a `reduce` to draw the remaining decagon sides.
144///   // For each number in the array 1..10, run the given function,
145///   // which takes a partially-sketched decagon and adds one more edge to it.
146///   fullDecagon = reduce([1..10], startOfDecagonSketch, fn(i, partialDecagon) {
147///       // Draw one edge of the decagon.
148///       x = cos(stepAngle * i) * radius
149///       y = sin(stepAngle * i) * radius
150///       return line(partialDecagon, end = [x, y])
151///   })
152///
153///   return fullDecagon
154///
155/// }
156///
157/// /*
158/// The `decagon` above is basically like this pseudo-code:
159/// fn decagon(radius):
160///     stepAngle = (1/10) * TAU
161///     plane = startSketchOn('XY')
162///     startOfDecagonSketch = startProfileAt([(cos(0)*radius), (sin(0) * radius)], plane)
163///
164///     // Here's the reduce part.
165///     partialDecagon = startOfDecagonSketch
166///     for i in [1..10]:
167///         x = cos(stepAngle * i) * radius
168///         y = sin(stepAngle * i) * radius
169///         partialDecagon = line(partialDecagon, end = [x, y])
170///     fullDecagon = partialDecagon // it's now full
171///     return fullDecagon
172/// */
173///
174/// // Use the `decagon` function declared above, to sketch a decagon with radius 5.
175/// decagon(5.0) |> close()
176/// ```
177#[stdlib {
178    name = "reduce",
179}]
180async fn inner_reduce<'a>(
181    array: Vec<KclValue>,
182    start: KclValue,
183    reduce_fn: &'a FunctionSource,
184    exec_state: &mut ExecState,
185    args: &'a Args,
186) -> Result<KclValue, KclError> {
187    let mut reduced = start;
188    for elem in array {
189        reduced = call_reduce_closure(elem, reduced, reduce_fn, args.source_range, exec_state, &args.ctx).await?;
190    }
191
192    Ok(reduced)
193}
194
195async fn call_reduce_closure(
196    elem: KclValue,
197    start: KclValue,
198    reduce_fn: &FunctionSource,
199    source_range: SourceRange,
200    exec_state: &mut ExecState,
201    ctxt: &ExecutorContext,
202) -> Result<KclValue, KclError> {
203    // Call the reduce fn for this repetition.
204    let reduce_fn_args = vec![Arg::synthetic(elem), Arg::synthetic(start)];
205    let transform_fn_return = reduce_fn.call(exec_state, ctxt, reduce_fn_args, source_range).await?;
206
207    // Unpack the returned transform object.
208    let source_ranges = vec![source_range];
209    let out = transform_fn_return.ok_or_else(|| {
210        KclError::Semantic(KclErrorDetails {
211            message: "Reducer function must return a value".to_string(),
212            source_ranges: source_ranges.clone(),
213        })
214    })?;
215    Ok(out)
216}
217
218/// Append an element to the end of an array.
219///
220/// Returns a new array with the element appended.
221///
222/// ```no_run
223/// arr = [1, 2, 3]
224/// new_arr = push(arr, 4)
225/// assertEqual(new_arr[3], 4, 0.00001, "4 was added to the end of the array")
226/// ```
227#[stdlib {
228    name = "push",
229}]
230async fn inner_push(mut array: Vec<KclValue>, elem: KclValue, args: &Args) -> Result<KclValue, KclError> {
231    // Unwrap the KclValues to JValues for manipulation
232    array.push(elem);
233    Ok(KclValue::MixedArray {
234        value: array,
235        meta: vec![args.source_range.into()],
236    })
237}
238
239pub async fn push(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
240    // Extract the array and the element from the arguments
241    let (val, elem): (KclValue, KclValue) = FromArgs::from_args(&args, 0)?;
242
243    let meta = vec![args.source_range];
244    let KclValue::MixedArray { value: array, meta: _ } = val else {
245        let actual_type = val.human_friendly_type();
246        return Err(KclError::Semantic(KclErrorDetails {
247            source_ranges: meta,
248            message: format!("You can't push to a value of type {actual_type}, only an array"),
249        }));
250    };
251    inner_push(array, elem, &args).await
252}
253
254/// Remove the last element from an array.
255///
256/// Returns a new array with the last element removed.
257///
258/// ```no_run
259/// arr = [1, 2, 3, 4]
260/// new_arr = pop(arr)
261/// assertEqual(new_arr[0], 1, 0.00001, "1 is the first element of the array")
262/// assertEqual(new_arr[1], 2, 0.00001, "2 is the second element of the array")
263/// assertEqual(new_arr[2], 3, 0.00001, "3 is the third element of the array")
264/// ```
265#[stdlib {
266    name = "pop",
267    keywords = true,
268    unlabeled_first = true,
269    args = {
270        array = { docs = "The array to pop from.  Must not be empty."},
271    }
272}]
273async fn inner_pop(array: Vec<KclValue>, args: &Args) -> Result<KclValue, KclError> {
274    if array.is_empty() {
275        return Err(KclError::Semantic(KclErrorDetails {
276            message: "Cannot pop from an empty array".to_string(),
277            source_ranges: vec![args.source_range],
278        }));
279    }
280
281    // Create a new array with all elements except the last one
282    let new_array = array[..array.len() - 1].to_vec();
283
284    Ok(KclValue::MixedArray {
285        value: new_array,
286        meta: vec![args.source_range.into()],
287    })
288}
289
290pub async fn pop(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
291    // Extract the array from the arguments
292    let val = args.get_unlabeled_kw_arg("array")?;
293
294    let meta = vec![args.source_range];
295    let KclValue::MixedArray { value: array, meta: _ } = val else {
296        let actual_type = val.human_friendly_type();
297        return Err(KclError::Semantic(KclErrorDetails {
298            source_ranges: meta,
299            message: format!("You can't pop from a value of type {actual_type}, only an array"),
300        }));
301    };
302
303    inner_pop(array, &args).await
304}