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