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
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(
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
34pub(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
54pub(crate) fn map_result(done: Vec<KclValue>) -> KclValue {
56 KclValue::HomArray {
57 value: done,
58 ty: RuntimeType::any(),
59 }
60}
61
62pub(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 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
109pub 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
115pub(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
126pub(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
149pub(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 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 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 Ok(inner_concat(&left, &left_el_ty, &right, &RuntimeType::any()))
236 }
237 _ => 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 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 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
359fn 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}