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
17pub 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
23pub(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
33pub(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
52pub(crate) fn map_result(done: Vec<KclValue>) -> KclValue {
54 KclValue::HomArray {
55 value: done,
56 ty: RuntimeType::any(),
57 }
58}
59
60pub(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 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
107pub 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
113pub(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
124pub(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
146pub(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 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 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 Ok(inner_concat(&left, &left_el_ty, &right, &RuntimeType::any()))
233 }
234 _ => 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 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 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
356fn 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}