kcl_lib/std/
array.rs

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