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}