Skip to main content

kcl_lib/std/
array.rs

1use indexmap::IndexMap;
2
3use crate::ExecutorContext;
4use crate::NodePath;
5use crate::SourceRange;
6use crate::errors::KclError;
7use crate::errors::KclErrorDetails;
8use crate::execution::ExecState;
9use crate::execution::KclValueControlFlow;
10use crate::execution::control_continue;
11use crate::execution::fn_call::Arg;
12use crate::execution::fn_call::Args;
13use crate::execution::kcl_value::FunctionSource;
14use crate::execution::kcl_value::KclValue;
15use crate::execution::types::RuntimeType;
16
17/// Apply a function to each element of an array.
18pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValueControlFlow, KclError> {
19    let (array, f) = map_parse_args(&args, exec_state)?;
20    inner_map(array, f, exec_state, &args).await
21}
22
23/// Parse map's arguments. Shared by the recursive executor's `map` and the
24/// machine executor's resumable entry.
25pub(crate) fn map_parse_args(
26    args: &Args,
27    exec_state: &mut ExecState,
28) -> Result<(Vec<KclValue>, FunctionSource), KclError> {
29    let array: Vec<KclValue> = args.get_unlabeled_kw_arg("array", &RuntimeType::any_array(), exec_state)?;
30    let f: FunctionSource = args.get_kw_arg("f", &RuntimeType::function(), exec_state)?;
31    Ok((array, f))
32}
33
34/// Build the per-element callback arguments for map. Shared by both
35/// executors.
36pub(crate) fn map_callback_args(
37    input: KclValue,
38    source_range: SourceRange,
39    node_path: Option<NodePath>,
40    exec_state: &mut ExecState,
41    ctxt: &ExecutorContext,
42) -> Args<crate::execution::fn_call::Sugary> {
43    Args::new(
44        Default::default(),
45        vec![(None, Arg::new(input, source_range))],
46        source_range,
47        node_path,
48        exec_state,
49        ctxt.clone(),
50        Some("map closure".to_owned()),
51    )
52}
53
54/// The result of map's whole iteration. Shared by both executors.
55pub(crate) fn map_result(done: Vec<KclValue>) -> KclValue {
56    KclValue::HomArray {
57        value: done,
58        ty: RuntimeType::any(),
59    }
60}
61
62/// Error when a map callback produces no value. Shared by both executors.
63pub(crate) fn map_missing_value_error(source_range: SourceRange) -> KclError {
64    KclError::new_semantic(KclErrorDetails::new(
65        "Map function must return a value".to_owned(),
66        vec![source_range],
67    ))
68}
69
70async fn inner_map(
71    array: Vec<KclValue>,
72    f: FunctionSource,
73    exec_state: &mut ExecState,
74    args: &Args,
75) -> Result<KclValueControlFlow, KclError> {
76    let mut new_array = Vec::with_capacity(array.len());
77    for elem in array {
78        let new_elem_cf = call_map_closure(
79            elem,
80            &f,
81            args.source_range,
82            args.node_path.clone(),
83            exec_state,
84            &args.ctx,
85        )
86        .await?;
87        // If the callback exited, e.g. by calling exit(), stop mapping, and
88        // propagate the exit so that it terminates the enclosing module.
89        let new_elem = control_continue!(new_elem_cf);
90        new_array.push(new_elem);
91    }
92    Ok(map_result(new_array).continue_())
93}
94
95async fn call_map_closure(
96    input: KclValue,
97    map_fn: &FunctionSource,
98    source_range: SourceRange,
99    node_path: Option<NodePath>,
100    exec_state: &mut ExecState,
101    ctxt: &ExecutorContext,
102) -> Result<KclValueControlFlow, KclError> {
103    let args = map_callback_args(input, source_range, node_path, exec_state, ctxt);
104    let output = map_fn.call_kw(None, exec_state, ctxt, args, source_range).await?;
105    let output = output.ok_or_else(|| map_missing_value_error(source_range))?;
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<KclValueControlFlow, KclError> {
111    let (array, f, initial) = reduce_parse_args(&args, exec_state)?;
112    inner_reduce(array, initial, f, exec_state, &args).await
113}
114
115/// Parse reduce's arguments. Shared by both executors.
116pub(crate) fn reduce_parse_args(
117    args: &Args,
118    exec_state: &mut ExecState,
119) -> Result<(Vec<KclValue>, FunctionSource, KclValue), KclError> {
120    let array: Vec<KclValue> = args.get_unlabeled_kw_arg("array", &RuntimeType::any_array(), exec_state)?;
121    let f: FunctionSource = args.get_kw_arg("f", &RuntimeType::function(), exec_state)?;
122    let initial: KclValue = args.get_kw_arg("initial", &RuntimeType::any(), exec_state)?;
123    Ok((array, f, initial))
124}
125
126/// Build the per-element callback arguments for reduce. Shared by both
127/// executors.
128pub(crate) fn reduce_callback_args(
129    elem: KclValue,
130    accum: KclValue,
131    source_range: SourceRange,
132    node_path: Option<NodePath>,
133    exec_state: &mut ExecState,
134    ctxt: &ExecutorContext,
135) -> Args<crate::execution::fn_call::Sugary> {
136    let mut labeled = IndexMap::with_capacity(1);
137    labeled.insert("accum".to_string(), Arg::new(accum, source_range));
138    Args::new(
139        labeled,
140        vec![(None, Arg::new(elem, source_range))],
141        source_range,
142        node_path,
143        exec_state,
144        ctxt.clone(),
145        Some("reduce closure".to_owned()),
146    )
147}
148
149/// Error when a reduce callback produces no value. Shared by both executors.
150pub(crate) fn reduce_missing_value_error(source_range: SourceRange) -> KclError {
151    KclError::new_semantic(KclErrorDetails::new(
152        "Reducer function must return a value".to_string(),
153        vec![source_range],
154    ))
155}
156
157async fn inner_reduce(
158    array: Vec<KclValue>,
159    initial: KclValue,
160    f: FunctionSource,
161    exec_state: &mut ExecState,
162    args: &Args,
163) -> Result<KclValueControlFlow, KclError> {
164    let mut reduced = initial;
165    for elem in array {
166        let reduced_cf = call_reduce_closure(
167            elem,
168            reduced,
169            &f,
170            args.source_range,
171            args.node_path.clone(),
172            exec_state,
173            &args.ctx,
174        )
175        .await?;
176        // If the callback exited, e.g. by calling exit(), stop reducing, and
177        // propagate the exit so that it terminates the enclosing module.
178        reduced = control_continue!(reduced_cf);
179    }
180
181    Ok(reduced.continue_())
182}
183
184async fn call_reduce_closure(
185    elem: KclValue,
186    accum: KclValue,
187    reduce_fn: &FunctionSource,
188    source_range: SourceRange,
189    node_path: Option<NodePath>,
190    exec_state: &mut ExecState,
191    ctxt: &ExecutorContext,
192) -> Result<KclValueControlFlow, KclError> {
193    // Call the reduce fn for this repetition.
194    let reduce_fn_args = reduce_callback_args(elem, accum, source_range, node_path, exec_state, ctxt);
195    let transform_fn_return = reduce_fn
196        .call_kw(None, exec_state, ctxt, reduce_fn_args, source_range)
197        .await?;
198    let out = transform_fn_return.ok_or_else(|| reduce_missing_value_error(source_range))?;
199    Ok(out)
200}
201
202pub async fn push(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
203    let (mut array, ty) = args.get_unlabeled_kw_arg_array_and_type("array", exec_state)?;
204    let item: KclValue = args.get_kw_arg("item", &RuntimeType::any(), exec_state)?;
205
206    array.push(item);
207
208    Ok(KclValue::HomArray { value: array, ty })
209}
210
211pub async fn pop(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
212    let (mut array, ty) = args.get_unlabeled_kw_arg_array_and_type("array", exec_state)?;
213    if array.is_empty() {
214        return Err(KclError::new_semantic(KclErrorDetails::new(
215            "Cannot pop from an empty array".to_string(),
216            vec![args.source_range],
217        )));
218    }
219    array.pop();
220    Ok(KclValue::HomArray { value: array, ty })
221}
222
223pub async fn concat(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
224    let (left, left_el_ty) = args.get_unlabeled_kw_arg_array_and_type("array", exec_state)?;
225    let right_value: KclValue = args.get_kw_arg("items", &RuntimeType::any_array(), exec_state)?;
226
227    match right_value {
228        KclValue::HomArray {
229            value: right,
230            ty: right_el_ty,
231            ..
232        } => Ok(inner_concat(&left, &left_el_ty, &right, &right_el_ty)),
233        KclValue::Tuple { value: right, .. } => {
234            // Tuples are treated as arrays for concatenation.
235            Ok(inner_concat(&left, &left_el_ty, &right, &RuntimeType::any()))
236        }
237        // Any single value is a subtype of an array, so we can treat it as a
238        // single-element array.
239        _ => Ok(inner_concat(&left, &left_el_ty, &[right_value], &RuntimeType::any())),
240    }
241}
242
243fn inner_concat(
244    left: &[KclValue],
245    left_el_ty: &RuntimeType,
246    right: &[KclValue],
247    right_el_ty: &RuntimeType,
248) -> KclValue {
249    if left.is_empty() {
250        return KclValue::HomArray {
251            value: right.to_vec(),
252            ty: right_el_ty.clone(),
253        };
254    }
255    if right.is_empty() {
256        return KclValue::HomArray {
257            value: left.to_vec(),
258            ty: left_el_ty.clone(),
259        };
260    }
261    let mut new = left.to_vec();
262    new.extend_from_slice(right);
263    // Propagate the element type if we can.
264    let ty = if right_el_ty.subtype(left_el_ty) {
265        left_el_ty.clone()
266    } else if left_el_ty.subtype(right_el_ty) {
267        right_el_ty.clone()
268    } else {
269        RuntimeType::any()
270    };
271    KclValue::HomArray { value: new, ty }
272}
273
274pub async fn slice(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
275    let (array, ty) = args.get_unlabeled_kw_arg_array_and_type("array", exec_state)?;
276    let start: Option<i64> = args.get_kw_arg_opt("start", &RuntimeType::count(), exec_state)?;
277    let end: Option<i64> = args.get_kw_arg_opt("end", &RuntimeType::count(), exec_state)?;
278
279    if start.is_none() && end.is_none() {
280        return Err(KclError::new_semantic(KclErrorDetails::new(
281            "Either `start` or `end` must be provided".to_owned(),
282            vec![args.source_range],
283        )));
284    }
285
286    let Ok(len) = i64::try_from(array.len()) else {
287        return Err(KclError::new_semantic(KclErrorDetails::new(
288            format!("Array length {} exceeds maximum supported length", array.len()),
289            vec![args.source_range],
290        )));
291    };
292    let mut computed_start = start.unwrap_or(0);
293    let mut computed_end = end.unwrap_or(len);
294
295    // Negative indices count from the end.
296    if computed_start < 0 {
297        computed_start += len;
298    }
299    if computed_end < 0 {
300        computed_end += len;
301    }
302
303    fn empty_slice(ty: RuntimeType) -> KclValue {
304        KclValue::HomArray { value: Vec::new(), ty }
305    }
306
307    if computed_start < 0 {
308        computed_start = 0;
309    }
310    if computed_start >= len {
311        return Ok(empty_slice(ty));
312    }
313    if computed_end > len {
314        computed_end = len;
315    }
316    if computed_end < 0 {
317        return Ok(empty_slice(ty));
318    }
319
320    if computed_start >= computed_end {
321        return Ok(empty_slice(ty));
322    }
323
324    let Some(sliced) = array.get(computed_start as usize..computed_end as usize) else {
325        let message = "Failed to compute array slice".to_owned();
326        debug_assert!(false, "{message}");
327        return Err(KclError::new_internal(KclErrorDetails::new(
328            message,
329            vec![args.source_range],
330        )));
331    };
332    Ok(KclValue::HomArray {
333        value: sliced.to_vec(),
334        ty,
335    })
336}
337
338pub async fn flatten(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
339    let array_value: KclValue = args.get_unlabeled_kw_arg("array", &RuntimeType::any_array(), exec_state)?;
340    let mut flattened = Vec::new();
341
342    let (array, original_ty) = match array_value {
343        KclValue::HomArray { value, ty, .. } => (value, ty),
344        KclValue::Tuple { value, .. } => (value, RuntimeType::any()),
345        _ => (vec![array_value], RuntimeType::any()),
346    };
347    for elem in array {
348        match elem {
349            KclValue::HomArray { value, .. } => flattened.extend(value),
350            KclValue::Tuple { value, .. } => flattened.extend(value),
351            _ => flattened.push(elem),
352        }
353    }
354
355    let ty = infer_flattened_type(original_ty, &flattened);
356    Ok(KclValue::HomArray { value: flattened, ty })
357}
358
359/// Infer the type of a flattened array based on the original type and the
360/// types of the flattened values. Currently, we preserve the original type only
361/// if all flattened values have the same type as the original element type.
362/// Otherwise, we fall back to `any`.
363fn infer_flattened_type(original_ty: RuntimeType, values: &[KclValue]) -> RuntimeType {
364    for value in values {
365        if !value.has_type(&original_ty) {
366            return RuntimeType::any();
367        };
368    }
369
370    original_ty
371}
372
373#[cfg(test)]
374mod tests {
375    use crate::errors::Severity;
376    use crate::errors::Tag;
377    use crate::execution::KclValueView;
378    use crate::execution::MockConfig;
379
380    #[tokio::test(flavor = "multi_thread")]
381    async fn flatten_consumed_solid_reports_deprecation_warning() {
382        let code = r#"
383targetSketch = sketch(on = XY) {
384  line1 = line(start = [var -10, var -10], end = [var 10, var -10])
385  line2 = line(start = [var 10, var -10], end = [var 10, var 10])
386  line3 = line(start = [var 10, var 10], end = [var -10, var 10])
387  line4 = line(start = [var -10, var 10], end = [var -10, var -10])
388  coincident([line1.end, line2.start])
389  coincident([line2.end, line3.start])
390  coincident([line3.end, line4.start])
391  coincident([line4.end, line1.start])
392  equalLength([line1, line2, line3, line4])
393}
394
395target = extrude(region(point = [0, 0], sketch = targetSketch), length = 20)
396
397toolSketch = sketch(on = XY) {
398  line1 = line(start = [var -2, var -2], end = [var 2, var -2])
399  line2 = line(start = [var 2, var -2], end = [var 2, var 2])
400  line3 = line(start = [var 2, var 2], end = [var -2, var 2])
401  line4 = line(start = [var -2, var 2], end = [var -2, var -2])
402  coincident([line1.end, line2.start])
403  coincident([line2.end, line3.start])
404  coincident([line3.end, line4.start])
405  coincident([line4.end, line1.start])
406  equalLength([line1, line2, line3, line4])
407}
408
409tool = extrude(region(point = [0, 0], sketch = toolSketch), length = 4)
410
411result = subtract(target, tools = [tool])
412flattened = flatten([[target]])
413"#;
414
415        let ctx = crate::ExecutorContext::new_mock(None).await;
416        let program = crate::Program::parse_no_errs(code).unwrap();
417        let result = ctx.run_mock(&program, &MockConfig::default()).await;
418        ctx.close().await;
419
420        match result {
421            Ok(outcome) => {
422                let flattened = outcome.variables.get("flattened").unwrap();
423                let KclValueView::HomArray { value, .. } = flattened else {
424                    panic!("expected `flattened` to be an array, got: {flattened:?}");
425                };
426                assert_eq!(value.len(), 1);
427                assert!(
428                    outcome.issues.iter().any(|issue| {
429                        issue.severity == Severity::Warning
430                            && issue.tag == Tag::Deprecated
431                            && issue
432                                .message
433                                .contains("Calling `flatten` with a consumed solid is deprecated")
434                            && issue
435                                .message
436                                .contains("`target` was already consumed by a `subtract` operation")
437                    }),
438                    "expected flatten consumed-solid deprecation warning, got: {:#?}",
439                    outcome.issues
440                );
441            }
442            Err(err) => {
443                let message = err.error.message();
444                assert!(
445                    message.contains("`target` was already consumed by a `subtract` operation"),
446                    "{message}"
447                );
448                panic!("flatten should warn for consumed-solid validation, but failed with: {message}");
449            }
450        }
451    }
452}