Skip to main content

kcl_lib/std/
array.rs

1use indexmap::IndexMap;
2
3use crate::ExecutorContext;
4use crate::SourceRange;
5use crate::errors::KclError;
6use crate::errors::KclErrorDetails;
7use crate::execution::ControlFlowKind;
8use crate::execution::ExecState;
9use crate::execution::fn_call::Arg;
10use crate::execution::fn_call::Args;
11use crate::execution::kcl_value::FunctionSource;
12use crate::execution::kcl_value::KclValue;
13use crate::execution::types::RuntimeType;
14
15/// Apply a function to each element of an array.
16pub async fn map(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
17    let array: Vec<KclValue> = args.get_unlabeled_kw_arg("array", &RuntimeType::any_array(), exec_state)?;
18    let f: FunctionSource = args.get_kw_arg("f", &RuntimeType::function(), exec_state)?;
19    let new_array = inner_map(array, f, exec_state, &args).await?;
20    Ok(KclValue::HomArray {
21        value: new_array,
22        ty: RuntimeType::any(),
23    })
24}
25
26async fn inner_map(
27    array: Vec<KclValue>,
28    f: FunctionSource,
29    exec_state: &mut ExecState,
30    args: &Args,
31) -> Result<Vec<KclValue>, KclError> {
32    let mut new_array = Vec::with_capacity(array.len());
33    for elem in array {
34        let new_elem = call_map_closure(elem, &f, args.source_range, exec_state, &args.ctx).await?;
35        new_array.push(new_elem);
36    }
37    Ok(new_array)
38}
39
40async fn call_map_closure(
41    input: KclValue,
42    map_fn: &FunctionSource,
43    source_range: SourceRange,
44    exec_state: &mut ExecState,
45    ctxt: &ExecutorContext,
46) -> Result<KclValue, KclError> {
47    let args = Args::new(
48        Default::default(),
49        vec![(None, Arg::new(input, source_range))],
50        source_range,
51        exec_state,
52        ctxt.clone(),
53        Some("map closure".to_owned()),
54    );
55    let output = map_fn.call_kw(None, exec_state, ctxt, args, source_range).await?;
56    let source_ranges = vec![source_range];
57    let output = output.ok_or_else(|| {
58        KclError::new_semantic(KclErrorDetails::new(
59            "Map function must return a value".to_owned(),
60            source_ranges,
61        ))
62    })?;
63    let output = match output.control {
64        ControlFlowKind::Continue => output.into_value(),
65        ControlFlowKind::Exit => {
66            let message = "Early return inside map function is currently not supported".to_owned();
67            debug_assert!(false, "{}", &message);
68            return Err(KclError::new_internal(KclErrorDetails::new(
69                message,
70                vec![source_range],
71            )));
72        }
73    };
74    Ok(output)
75}
76
77/// For each item in an array, update a value.
78pub async fn reduce(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
79    let array: Vec<KclValue> = args.get_unlabeled_kw_arg("array", &RuntimeType::any_array(), exec_state)?;
80    let f: FunctionSource = args.get_kw_arg("f", &RuntimeType::function(), exec_state)?;
81    let initial: KclValue = args.get_kw_arg("initial", &RuntimeType::any(), exec_state)?;
82    inner_reduce(array, initial, f, exec_state, &args).await
83}
84
85async fn inner_reduce(
86    array: Vec<KclValue>,
87    initial: KclValue,
88    f: FunctionSource,
89    exec_state: &mut ExecState,
90    args: &Args,
91) -> Result<KclValue, KclError> {
92    let mut reduced = initial;
93    for elem in array {
94        reduced = call_reduce_closure(elem, reduced, &f, args.source_range, exec_state, &args.ctx).await?;
95    }
96
97    Ok(reduced)
98}
99
100async fn call_reduce_closure(
101    elem: KclValue,
102    accum: KclValue,
103    reduce_fn: &FunctionSource,
104    source_range: SourceRange,
105    exec_state: &mut ExecState,
106    ctxt: &ExecutorContext,
107) -> Result<KclValue, KclError> {
108    // Call the reduce fn for this repetition.
109    let mut labeled = IndexMap::with_capacity(1);
110    labeled.insert("accum".to_string(), Arg::new(accum, source_range));
111    let reduce_fn_args = Args::new(
112        labeled,
113        vec![(None, Arg::new(elem, source_range))],
114        source_range,
115        exec_state,
116        ctxt.clone(),
117        Some("reduce closure".to_owned()),
118    );
119    let transform_fn_return = reduce_fn
120        .call_kw(None, exec_state, ctxt, reduce_fn_args, source_range)
121        .await?;
122
123    // Unpack the returned transform object.
124    let source_ranges = vec![source_range];
125    let out = transform_fn_return.ok_or_else(|| {
126        KclError::new_semantic(KclErrorDetails::new(
127            "Reducer function must return a value".to_string(),
128            source_ranges.clone(),
129        ))
130    })?;
131    let out = match out.control {
132        ControlFlowKind::Continue => out.into_value(),
133        ControlFlowKind::Exit => {
134            let message = "Early return inside reduce function is currently not supported".to_owned();
135            debug_assert!(false, "{}", &message);
136            return Err(KclError::new_internal(KclErrorDetails::new(
137                message,
138                vec![source_range],
139            )));
140        }
141    };
142    Ok(out)
143}
144
145pub async fn push(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
146    let (mut array, ty) = args.get_unlabeled_kw_arg_array_and_type("array", exec_state)?;
147    let item: KclValue = args.get_kw_arg("item", &RuntimeType::any(), exec_state)?;
148
149    array.push(item);
150
151    Ok(KclValue::HomArray { value: array, ty })
152}
153
154pub async fn pop(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
155    let (mut array, ty) = args.get_unlabeled_kw_arg_array_and_type("array", exec_state)?;
156    if array.is_empty() {
157        return Err(KclError::new_semantic(KclErrorDetails::new(
158            "Cannot pop from an empty array".to_string(),
159            vec![args.source_range],
160        )));
161    }
162    array.pop();
163    Ok(KclValue::HomArray { value: array, ty })
164}
165
166pub async fn concat(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
167    let (left, left_el_ty) = args.get_unlabeled_kw_arg_array_and_type("array", exec_state)?;
168    let right_value: KclValue = args.get_kw_arg("items", &RuntimeType::any_array(), exec_state)?;
169
170    match right_value {
171        KclValue::HomArray {
172            value: right,
173            ty: right_el_ty,
174            ..
175        } => Ok(inner_concat(&left, &left_el_ty, &right, &right_el_ty)),
176        KclValue::Tuple { value: right, .. } => {
177            // Tuples are treated as arrays for concatenation.
178            Ok(inner_concat(&left, &left_el_ty, &right, &RuntimeType::any()))
179        }
180        // Any single value is a subtype of an array, so we can treat it as a
181        // single-element array.
182        _ => Ok(inner_concat(&left, &left_el_ty, &[right_value], &RuntimeType::any())),
183    }
184}
185
186fn inner_concat(
187    left: &[KclValue],
188    left_el_ty: &RuntimeType,
189    right: &[KclValue],
190    right_el_ty: &RuntimeType,
191) -> KclValue {
192    if left.is_empty() {
193        return KclValue::HomArray {
194            value: right.to_vec(),
195            ty: right_el_ty.clone(),
196        };
197    }
198    if right.is_empty() {
199        return KclValue::HomArray {
200            value: left.to_vec(),
201            ty: left_el_ty.clone(),
202        };
203    }
204    let mut new = left.to_vec();
205    new.extend_from_slice(right);
206    // Propagate the element type if we can.
207    let ty = if right_el_ty.subtype(left_el_ty) {
208        left_el_ty.clone()
209    } else if left_el_ty.subtype(right_el_ty) {
210        right_el_ty.clone()
211    } else {
212        RuntimeType::any()
213    };
214    KclValue::HomArray { value: new, ty }
215}
216
217pub async fn slice(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
218    let (array, ty) = args.get_unlabeled_kw_arg_array_and_type("array", exec_state)?;
219    let start: Option<i64> = args.get_kw_arg_opt("start", &RuntimeType::count(), exec_state)?;
220    let end: Option<i64> = args.get_kw_arg_opt("end", &RuntimeType::count(), exec_state)?;
221
222    if start.is_none() && end.is_none() {
223        return Err(KclError::new_semantic(KclErrorDetails::new(
224            "Either `start` or `end` must be provided".to_owned(),
225            vec![args.source_range],
226        )));
227    }
228
229    let Ok(len) = i64::try_from(array.len()) else {
230        return Err(KclError::new_semantic(KclErrorDetails::new(
231            format!("Array length {} exceeds maximum supported length", array.len()),
232            vec![args.source_range],
233        )));
234    };
235    let mut computed_start = start.unwrap_or(0);
236    let mut computed_end = end.unwrap_or(len);
237
238    // Negative indices count from the end.
239    if computed_start < 0 {
240        computed_start += len;
241    }
242    if computed_end < 0 {
243        computed_end += len;
244    }
245
246    fn empty_slice(ty: RuntimeType) -> KclValue {
247        KclValue::HomArray { value: Vec::new(), ty }
248    }
249
250    if computed_start < 0 {
251        computed_start = 0;
252    }
253    if computed_start >= len {
254        return Ok(empty_slice(ty));
255    }
256    if computed_end > len {
257        computed_end = len;
258    }
259    if computed_end < 0 {
260        return Ok(empty_slice(ty));
261    }
262
263    if computed_start >= computed_end {
264        return Ok(empty_slice(ty));
265    }
266
267    let Some(sliced) = array.get(computed_start as usize..computed_end as usize) else {
268        let message = "Failed to compute array slice".to_owned();
269        debug_assert!(false, "{message}");
270        return Err(KclError::new_internal(KclErrorDetails::new(
271            message,
272            vec![args.source_range],
273        )));
274    };
275    Ok(KclValue::HomArray {
276        value: sliced.to_vec(),
277        ty,
278    })
279}
280
281pub async fn flatten(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
282    let array_value: KclValue = args.get_unlabeled_kw_arg("array", &RuntimeType::any_array(), exec_state)?;
283    let mut flattened = Vec::new();
284
285    let (array, original_ty) = match array_value {
286        KclValue::HomArray { value, ty, .. } => (value, ty),
287        KclValue::Tuple { value, .. } => (value, RuntimeType::any()),
288        _ => (vec![array_value], RuntimeType::any()),
289    };
290    for elem in array {
291        match elem {
292            KclValue::HomArray { value, .. } => flattened.extend(value),
293            KclValue::Tuple { value, .. } => flattened.extend(value),
294            _ => flattened.push(elem),
295        }
296    }
297
298    let ty = infer_flattened_type(original_ty, &flattened);
299    Ok(KclValue::HomArray { value: flattened, ty })
300}
301
302/// Infer the type of a flattened array based on the original type and the
303/// types of the flattened values. Currently, we preserve the original type only
304/// if all flattened values have the same type as the original element type.
305/// Otherwise, we fall back to `any`.
306fn infer_flattened_type(original_ty: RuntimeType, values: &[KclValue]) -> RuntimeType {
307    for value in values {
308        if !value.has_type(&original_ty) {
309            return RuntimeType::any();
310        };
311    }
312
313    original_ty
314}