Skip to main content

kcl_lib/execution/
fn_call.rs

1use async_recursion::async_recursion;
2use indexmap::IndexMap;
3
4use crate::CompilationIssue;
5use crate::NodePath;
6use crate::SourceRange;
7use crate::errors::KclError;
8use crate::errors::KclErrorDetails;
9use crate::execution::BodyType;
10use crate::execution::ExecState;
11use crate::execution::ExecutorContext;
12use crate::execution::Geometry;
13use crate::execution::KclValue;
14use crate::execution::KclValueControlFlow;
15use crate::execution::Metadata;
16use crate::execution::Solid;
17use crate::execution::StatementKind;
18use crate::execution::TagEngineInfo;
19use crate::execution::TagIdentifier;
20use crate::execution::annotations;
21use crate::execution::cad_op::Group;
22use crate::execution::cad_op::OpArg;
23use crate::execution::cad_op::OpKclValue;
24use crate::execution::cad_op::Operation;
25use crate::execution::control_continue;
26use crate::execution::kcl_value::FunctionBody;
27use crate::execution::kcl_value::FunctionSource;
28use crate::execution::kcl_value::NamedParam;
29use crate::execution::memory;
30use crate::execution::types::RuntimeType;
31use crate::parsing::ast::types::CallExpressionKw;
32use crate::parsing::ast::types::Node;
33use crate::parsing::ast::types::Type;
34
35#[derive(Debug, Clone)]
36pub struct Args<Status: ArgsStatus = Desugared> {
37    /// Name of the function these args are being passed into.
38    pub fn_name: Option<String>,
39    /// Unlabeled keyword args. Currently only the first formal arg can be unlabeled.
40    /// If the argument was a local variable, then the first element of the tuple is its name
41    /// which may be used to treat this arg as a labelled arg.
42    pub unlabeled: Vec<(Option<String>, Arg)>,
43    /// Labeled args.
44    pub labeled: IndexMap<String, Arg>,
45    pub source_range: SourceRange,
46    pub node_path: Option<NodePath>,
47    pub ctx: ExecutorContext,
48    /// If this call happens inside a pipe (|>) expression, this holds the LHS of that |>.
49    /// Otherwise it's None.
50    pub pipe_value: Option<Arg>,
51    _status: std::marker::PhantomData<Status>,
52}
53
54pub trait ArgsStatus: std::fmt::Debug + Clone {}
55
56#[derive(Debug, Clone)]
57pub struct Sugary;
58impl ArgsStatus for Sugary {}
59
60// Invariants guaranteed by the `Desugared` status:
61// - There is either 0 or 1 unlabeled arguments
62// - Any lableled args are in the labeled map, and not the unlabeled Vec.
63// - The arguments match the type signature of the function exactly
64// - pipe_value.is_none()
65#[derive(Debug, Clone)]
66pub struct Desugared;
67impl ArgsStatus for Desugared {}
68
69impl Args<Sugary> {
70    /// Collect the given keyword arguments.
71    pub fn new(
72        labeled: IndexMap<String, Arg>,
73        unlabeled: Vec<(Option<String>, Arg)>,
74        source_range: SourceRange,
75        node_path: Option<NodePath>,
76        exec_state: &mut ExecState,
77        ctx: ExecutorContext,
78        fn_name: Option<String>,
79    ) -> Args<Sugary> {
80        Args {
81            fn_name,
82            labeled,
83            unlabeled,
84            source_range,
85            node_path,
86            ctx,
87            pipe_value: exec_state.pipe_value().map(|v| Arg::new(v.clone(), source_range)),
88            _status: std::marker::PhantomData,
89        }
90    }
91}
92
93impl<Status: ArgsStatus> Args<Status> {
94    /// How many arguments are there?
95    pub fn len(&self) -> usize {
96        self.labeled.len() + self.unlabeled.len()
97    }
98
99    /// Are there no arguments?
100    pub fn is_empty(&self) -> bool {
101        self.labeled.is_empty() && self.unlabeled.is_empty()
102    }
103}
104
105impl Args<Desugared> {
106    pub fn new_no_args(
107        source_range: SourceRange,
108        node_path: Option<NodePath>,
109        ctx: ExecutorContext,
110        fn_name: Option<String>,
111    ) -> Args {
112        Args {
113            fn_name,
114            unlabeled: Default::default(),
115            labeled: Default::default(),
116            source_range,
117            node_path,
118            ctx,
119            pipe_value: None,
120            _status: std::marker::PhantomData,
121        }
122    }
123
124    /// Get the unlabeled keyword argument. If not set, returns None.
125    pub(crate) fn unlabeled_kw_arg_unconverted(&self) -> Option<&Arg> {
126        self.unlabeled.first().map(|(_, a)| a)
127    }
128}
129
130#[derive(Debug, Clone)]
131pub struct Arg {
132    /// The evaluated argument.
133    pub value: KclValue,
134    /// The source range of the unevaluated argument.
135    pub source_range: SourceRange,
136}
137
138impl Arg {
139    pub fn new(value: KclValue, source_range: SourceRange) -> Self {
140        Self { value, source_range }
141    }
142
143    pub fn synthetic(value: KclValue) -> Self {
144        Self {
145            value,
146            source_range: SourceRange::synthetic(),
147        }
148    }
149
150    pub fn source_ranges(&self) -> Vec<SourceRange> {
151        vec![self.source_range]
152    }
153}
154
155impl Node<CallExpressionKw> {
156    #[async_recursion]
157    pub(super) async fn execute(
158        &self,
159        exec_state: &mut ExecState,
160        ctx: &ExecutorContext,
161    ) -> Result<KclValueControlFlow, KclError> {
162        let fn_name = &self.callee;
163        let callsite: SourceRange = self.into();
164
165        // Clone the function so that we can use a mutable reference to
166        // exec_state.
167        let func: KclValue = fn_name.get_result(exec_state, ctx).await?.clone();
168
169        let Some(fn_src) = func.as_function() else {
170            return Err(KclError::new_semantic(KclErrorDetails::new(
171                "cannot call this because it isn't a function".to_string(),
172                vec![callsite],
173            )));
174        };
175
176        // Build a hashmap from argument labels to the final evaluated values.
177        let mut fn_args = IndexMap::with_capacity(self.arguments.len());
178        let mut unlabeled = Vec::new();
179
180        // Evaluate the unlabeled first param, if any exists.
181        if let Some(ref arg_expr) = self.unlabeled {
182            let source_range = SourceRange::from(arg_expr.clone());
183            let metadata = Metadata { source_range };
184            let value_cf = ctx
185                .execute_expr(arg_expr, exec_state, &metadata, &[], StatementKind::Expression)
186                .await?;
187            let value = control_continue!(value_cf);
188
189            let label = arg_expr.ident_name().map(str::to_owned);
190
191            unlabeled.push((label, Arg::new(value, source_range)))
192        }
193
194        for arg_expr in &self.arguments {
195            let source_range = SourceRange::from(arg_expr.arg.clone());
196            let metadata = Metadata { source_range };
197            let value_cf = ctx
198                .execute_expr(&arg_expr.arg, exec_state, &metadata, &[], StatementKind::Expression)
199                .await?;
200            let value = control_continue!(value_cf);
201            let arg = Arg::new(value, source_range);
202            match &arg_expr.label {
203                Some(l) => {
204                    fn_args.insert(l.name.clone(), arg);
205                }
206                None => {
207                    unlabeled.push((arg_expr.arg.ident_name().map(str::to_owned), arg));
208                }
209            }
210        }
211
212        let args = Args::new(
213            fn_args,
214            unlabeled,
215            callsite,
216            self.node_path.clone(),
217            exec_state,
218            ctx.clone(),
219            Some(fn_name.name.name.clone()),
220        );
221
222        let return_value = fn_src
223            .call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite)
224            .await
225            .map_err(|e| {
226                // Add the call expression to the source ranges.
227                //
228                // TODO: Use the name that the function was defined
229                // with, not the identifier it was used with.
230                e.add_unwind_location(Some(fn_name.name.name.clone()), callsite)
231            })?;
232
233        let result = return_value.ok_or_else(move || {
234            let mut source_ranges: Vec<SourceRange> = vec![callsite];
235            // We want to send the source range of the original function.
236            if let KclValue::Function { meta, .. } = func {
237                source_ranges = meta.iter().map(|m| m.source_range).collect();
238            };
239            KclError::new_undefined_value(
240                KclErrorDetails::new(
241                    format!("Result of user-defined function {fn_name} is undefined"),
242                    source_ranges,
243                ),
244                None,
245            )
246        })?;
247
248        Ok(result)
249    }
250}
251
252impl FunctionSource {
253    pub(crate) async fn call_kw(
254        &self,
255        fn_name: Option<String>,
256        exec_state: &mut ExecState,
257        ctx: &ExecutorContext,
258        args: Args<Sugary>,
259        callsite: SourceRange,
260    ) -> Result<Option<KclValueControlFlow>, KclError> {
261        exec_state.inc_call_stack_size(callsite)?;
262
263        let result = self.inner_call_kw(fn_name, exec_state, ctx, args, callsite).await;
264
265        exec_state.dec_call_stack_size(callsite)?;
266        result
267    }
268
269    async fn inner_call_kw(
270        &self,
271        fn_name: Option<String>,
272        exec_state: &mut ExecState,
273        ctx: &ExecutorContext,
274        args: Args<Sugary>,
275        callsite: SourceRange,
276    ) -> Result<Option<KclValueControlFlow>, KclError> {
277        if self.deprecated {
278            exec_state.warn(
279                CompilationIssue::err(
280                    callsite,
281                    format!(
282                        "{} is deprecated, see the docs for a recommended replacement",
283                        match &fn_name {
284                            Some(n) => format!("`{n}`"),
285                            None => "This function".to_owned(),
286                        }
287                    ),
288                ),
289                annotations::WARN_DEPRECATED,
290            );
291        }
292        if self.experimental {
293            exec_state.warn_experimental(
294                &match &fn_name {
295                    Some(n) => format!("`{n}`"),
296                    None => "This function".to_owned(),
297                },
298                callsite,
299            );
300        }
301
302        let args = type_check_params_kw(fn_name.as_deref(), self, args, exec_state)?;
303
304        // Warn if experimental arguments are used after desugaring.
305        for (label, arg) in &args.labeled {
306            if let Some(param) = self.named_args.get(label.as_str())
307                && param.experimental
308            {
309                exec_state.warn_experimental(
310                    &match &fn_name {
311                        Some(f) => format!("`{f}({label})`"),
312                        None => label.to_owned(),
313                    },
314                    arg.source_range,
315                );
316            }
317        }
318
319        // Don't early return until the stack frame is popped!
320        self.body.prep_mem(exec_state);
321
322        // Some function calls might get added to the feature tree.
323        // We do this by adding an "operation".
324
325        // Don't add operations if the KCL code being executed is
326        // just the KCL stdlib calling other KCL stdlib,
327        // because the stdlib internals aren't relevant to users,
328        // that would just be pointless noise.
329        //
330        // Do add operations if the KCL being executed is
331        // user-defined, or the calling code is user-defined,
332        // because that's relevant to the user.
333        let would_trace_stdlib_internals = exec_state.mod_local.inside_stdlib && self.is_std();
334        // self.include_in_feature_tree is set by the KCL annotation `@(feature_tree = true)`.
335        let should_track_operation = !would_trace_stdlib_internals && self.include_in_feature_tree;
336        let op = if should_track_operation {
337            let op_labeled_args = args
338                .labeled
339                .iter()
340                .map(|(k, arg)| (k.clone(), OpArg::new(OpKclValue::from(&arg.value), arg.source_range)))
341                .collect();
342
343            // If you're calling a stdlib function, track that call as an operation.
344            if self.is_std() {
345                Some(Operation::StdLibCall {
346                    name: fn_name.clone().unwrap_or_else(|| "unknown function".to_owned()),
347                    unlabeled_arg: args
348                        .unlabeled_kw_arg_unconverted()
349                        .map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
350                    labeled_args: op_labeled_args,
351                    node_path: NodePath::placeholder(),
352                    source_range: callsite,
353                    stdlib_entry_source_range: exec_state.mod_local.stdlib_entry_source_range,
354                    is_error: false,
355                })
356            } else {
357                // Otherwise, you're calling a user-defined function, track that call as an operation.
358                exec_state.push_op(Operation::GroupBegin {
359                    group: Group::FunctionCall {
360                        name: fn_name.clone(),
361                        function_source_range: self.ast.as_source_range(),
362                        unlabeled_arg: args
363                            .unlabeled_kw_arg_unconverted()
364                            .map(|arg| OpArg::new(OpKclValue::from(&arg.value), arg.source_range)),
365                        labeled_args: op_labeled_args,
366                    },
367                    node_path: NodePath::placeholder(),
368                    source_range: callsite,
369                });
370
371                None
372            }
373        } else {
374            None
375        };
376
377        let is_calling_into_stdlib = match &self.body {
378            FunctionBody::Rust(_) => true,
379            FunctionBody::Kcl(_) => self.is_std(),
380        };
381        let is_crossing_into_stdlib = is_calling_into_stdlib && !exec_state.mod_local.inside_stdlib;
382        let is_crossing_out_of_stdlib = !is_calling_into_stdlib && exec_state.mod_local.inside_stdlib;
383        let stdlib_entry_source_range = if is_crossing_into_stdlib {
384            // When we're calling into the stdlib, for example calling hole(),
385            // track the location so that any further stdlib calls like
386            // subtract() can point to the hole() call. The frontend needs this.
387            Some(callsite)
388        } else if is_crossing_out_of_stdlib {
389            // When map() calls a user-defined function, and it calls extrude()
390            // for example, we want it to point the the extrude() call, not
391            // the map() call.
392            None
393        } else {
394            // When we're not crossing the stdlib boundary, keep the previous
395            // value.
396            exec_state.mod_local.stdlib_entry_source_range
397        };
398
399        let prev_inside_stdlib = std::mem::replace(&mut exec_state.mod_local.inside_stdlib, is_calling_into_stdlib);
400        let prev_stdlib_entry_source_range = std::mem::replace(
401            &mut exec_state.mod_local.stdlib_entry_source_range,
402            stdlib_entry_source_range,
403        );
404        // Do not early return via ? or something until we've
405        // - put this `prev_inside_stdlib` value back.
406        // - called the pop_env.
407        let result = match &self.body {
408            FunctionBody::Rust(f) => f(exec_state, args).await.map(Some),
409            FunctionBody::Kcl(_) => {
410                if let Err(e) = assign_args_to_params_kw(self, args, exec_state) {
411                    exec_state.mod_local.inside_stdlib = prev_inside_stdlib;
412                    exec_state.mut_stack().pop_env();
413                    return Err(e);
414                }
415
416                ctx.exec_block(&self.ast.body, exec_state, BodyType::Block)
417                    .await
418                    .map(|cf| {
419                        if let Some(cf) = cf
420                            && cf.is_some_return()
421                        {
422                            return Some(cf);
423                        }
424                        // Ignore the block's value and extract the return value
425                        // from memory.
426                        exec_state
427                            .stack()
428                            .get(memory::RETURN_NAME, self.ast.as_source_range())
429                            .ok()
430                            .cloned()
431                            .map(KclValue::continue_)
432                    })
433            }
434        };
435        exec_state.mod_local.inside_stdlib = prev_inside_stdlib;
436        exec_state.mod_local.stdlib_entry_source_range = prev_stdlib_entry_source_range;
437        exec_state.mut_stack().pop_env();
438
439        if should_track_operation {
440            if let Some(mut op) = op {
441                op.set_std_lib_call_is_error(result.is_err());
442                // Track call operation.  We do this after the call
443                // since things like patternTransform may call user code
444                // before running, and we will likely want to use the
445                // return value. The call takes ownership of the args,
446                // so we need to build the op before the call.
447                exec_state.push_op(op);
448            } else if !is_calling_into_stdlib {
449                exec_state.push_op(Operation::GroupEnd);
450            }
451        }
452
453        let mut result = match result {
454            Ok(Some(value)) => {
455                if value.is_some_return() {
456                    return Ok(Some(value));
457                } else {
458                    Ok(Some(value.into_value()))
459                }
460            }
461            Ok(None) => Ok(None),
462            Err(e) => Err(e),
463        };
464
465        if self.is_std()
466            && let Ok(Some(result)) = &mut result
467        {
468            update_memory_for_tags_of_geometry(result, exec_state)?;
469        }
470
471        coerce_result_type(result, self, exec_state).map(|r| r.map(KclValue::continue_))
472    }
473}
474
475impl FunctionBody {
476    fn prep_mem(&self, exec_state: &mut ExecState) {
477        match self {
478            FunctionBody::Rust(_) => exec_state.mut_stack().push_new_root_env(true),
479            FunctionBody::Kcl(memory) => exec_state.mut_stack().push_new_env_for_call(*memory),
480        }
481    }
482}
483
484fn originates_from_sketch_block(value: &KclValue) -> bool {
485    match value {
486        KclValue::Uuid { .. } => false,
487        KclValue::Bool { .. } => false,
488        KclValue::Number { .. } => false,
489        KclValue::String { .. } => false,
490        KclValue::SketchVar { .. } => true,
491        KclValue::SketchConstraint { .. } => true,
492        KclValue::Tuple { value, .. } => value.iter().all(originates_from_sketch_block),
493        KclValue::HomArray { value, .. } => value.iter().all(originates_from_sketch_block),
494        // TODO: sketch block result should return true.
495        KclValue::Object { value, .. } => value.values().all(originates_from_sketch_block),
496        KclValue::TagIdentifier(_) => false,
497        KclValue::TagDeclarator(_) => false,
498        KclValue::GdtAnnotation { .. } => false,
499        KclValue::Plane { .. } => false,
500        KclValue::Face { .. } => false,
501        KclValue::BoundedEdge { .. } => false,
502        KclValue::Segment { .. } => true,
503        KclValue::Sketch { value: sketch } => sketch.origin_sketch_id.is_some(),
504        KclValue::Solid { value: solid } => solid
505            .sketch()
506            .map(|sketch| sketch.origin_sketch_id.is_some())
507            .unwrap_or(false),
508        KclValue::Helix { .. } => false,
509        KclValue::ImportedGeometry(_) => false,
510        KclValue::Function { .. } => false,
511        KclValue::Module { .. } => false,
512        KclValue::Type { .. } => false,
513        KclValue::KclNone { .. } => false,
514    }
515}
516
517fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
518    let is_sketch_block = originates_from_sketch_block(&*result);
519    // If the return result is a sketch or solid, we want to update the
520    // memory for the tags of the group.
521    // TODO: This could probably be done in a better way, but as of now this was my only idea
522    // and it works.
523    match result {
524        KclValue::Sketch { value } if !is_sketch_block => {
525            for (name, tag) in value.tags.iter() {
526                if exec_state.stack().cur_frame_contains(name) {
527                    exec_state.mut_stack().update(name, |v, _| {
528                        v.as_mut_tag().unwrap().merge_info(tag);
529                    });
530                } else {
531                    exec_state
532                        .mut_stack()
533                        .add(
534                            name.to_owned(),
535                            KclValue::TagIdentifier(Box::new(tag.clone())),
536                            SourceRange::default(),
537                        )
538                        .unwrap();
539                }
540            }
541        }
542        KclValue::Solid { value } => {
543            let surfaces = value.value.clone();
544            if value.sketch_mut().is_none() {
545                // If the solid isn't based on a sketch, then it doesn't have a tag container,
546                // so there's nothing to do here.
547                return Ok(());
548            };
549            // Now that we know there's work to do (because there's a tag container),
550            // run some clones.
551            let solid_copies: Vec<Box<Solid>> = surfaces.iter().map(|_| value.clone()).collect();
552            // Get the tag container. We expect it to always succeed because we already checked
553            // for a tag container above.
554            let Some(sketch) = value.sketch_mut() else {
555                return Ok(());
556            };
557            for (v, mut solid_copy) in surfaces.iter().zip(solid_copies) {
558                if let Some(sketch) = solid_copy.sketch_mut() {
559                    sketch.tags.clear(); // Avoid recursive tags.
560                }
561                if let Some(tag) = v.get_tag() {
562                    // Get the past tag and update it.
563                    let mut is_part_of_sketch = false;
564                    let tag_id = if let Some(t) = sketch.tags.get(&tag.name) {
565                        is_part_of_sketch = true;
566                        let mut t = t.clone();
567                        let Some(info) = t.get_cur_info() else {
568                            return Err(KclError::new_internal(KclErrorDetails::new(
569                                format!("Tag {} does not have path info", tag.name),
570                                vec![tag.into()],
571                            )));
572                        };
573
574                        let mut info = info.clone();
575                        info.id = v.get_id();
576                        info.surface = Some(v.clone());
577                        info.geometry = Geometry::Solid(*solid_copy);
578                        t.info.push((exec_state.stack().current_epoch(), info));
579                        t
580                    } else {
581                        // It's probably a fillet or a chamfer.
582                        // Initialize it.
583                        TagIdentifier {
584                            value: tag.name.clone(),
585                            info: vec![(
586                                exec_state.stack().current_epoch(),
587                                TagEngineInfo {
588                                    id: v.get_id(),
589                                    surface: Some(v.clone()),
590                                    path: None,
591                                    geometry: Geometry::Solid(*solid_copy),
592                                },
593                            )],
594                            meta: vec![Metadata {
595                                source_range: tag.clone().into(),
596                            }],
597                        }
598                    };
599
600                    // update the sketch tags.
601                    sketch.merge_tags(Some(&tag_id).into_iter());
602
603                    if exec_state.stack().cur_frame_contains(&tag.name) {
604                        exec_state.mut_stack().update(&tag.name, |v, _| {
605                            v.as_mut_tag().unwrap().merge_info(&tag_id);
606                        });
607                    } else if !is_sketch_block || !is_part_of_sketch {
608                        // The above condition is saying that we add a tag to
609                        // the stack in either of these cases:
610                        //
611                        // 1. It originates from a legacy sketch v1.
612                        //
613                        // 2. It originates from a sketch block and it's not
614                        // part of the sketch. Instead, it's part of the solid,
615                        // as in tagging a cap face `extrude(tagEnd, tagStart)`
616                        // or chamfer face `chamfer(tag)`.
617                        exec_state
618                            .mut_stack()
619                            .add(
620                                tag.name.clone(),
621                                KclValue::TagIdentifier(Box::new(tag_id)),
622                                SourceRange::default(),
623                            )
624                            .unwrap();
625                    }
626                }
627            }
628
629            // Find the stale sketch in memory and update it.
630            if let Some(sketch) = value.sketch() {
631                if sketch.tags.is_empty() {
632                    return Ok(());
633                }
634                let sketch_tags: Vec<_> = sketch.tags.values().cloned().collect();
635                let sketches_to_update: Vec<_> = exec_state
636                    .stack()
637                    .find_keys_in_current_env(|v| match v {
638                        KclValue::Sketch { value: sk } => sk.original_id == sketch.original_id,
639                        _ => false,
640                    })
641                    .cloned()
642                    .collect();
643
644                for k in sketches_to_update {
645                    exec_state.mut_stack().update(&k, |v, _| {
646                        let sketch = v.as_mut_sketch().unwrap();
647                        sketch.merge_tags(sketch_tags.iter());
648                    });
649                }
650            }
651        }
652        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
653            for v in value {
654                update_memory_for_tags_of_geometry(v, exec_state)?;
655            }
656        }
657        _ => {}
658    }
659    Ok(())
660}
661
662fn type_err_str(expected: &Type, found: &KclValue, source_range: &SourceRange, exec_state: &mut ExecState) -> String {
663    fn strip_backticks(s: &str) -> &str {
664        let mut result = s;
665        if s.starts_with('`') {
666            result = &result[1..]
667        }
668        if s.ends_with('`') {
669            result = &result[..result.len() - 1]
670        }
671        result
672    }
673
674    let expected_human = expected.human_friendly_type();
675    let expected_ty = expected.to_string();
676    let expected_str =
677        if expected_human == expected_ty || expected_human == format!("a value with type `{expected_ty}`") {
678            format!("a value with type `{expected_ty}`")
679        } else {
680            format!("{expected_human} (`{expected_ty}`)")
681        };
682    let found_human = found.human_friendly_type();
683    let found_ty = found.principal_type_string();
684    let found_str = if found_human == found_ty || found_human == format!("a {}", strip_backticks(&found_ty)) {
685        format!("a value with type {found_ty}")
686    } else {
687        format!("{found_human} (with type {found_ty})")
688    };
689
690    let mut result = format!("{expected_str}, but found {found_str}.");
691
692    if found.is_unknown_number() {
693        exec_state.clear_units_warnings(source_range);
694        result.push_str("\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: mm` or `(a * b): deg`.");
695    }
696
697    result
698}
699
700fn type_check_params_kw(
701    fn_name: Option<&str>,
702    fn_def: &FunctionSource,
703    mut args: Args<Sugary>,
704    exec_state: &mut ExecState,
705) -> Result<Args<Desugared>, KclError> {
706    let fn_name = fn_name.or(args.fn_name.as_deref());
707    let mut result = Args::new_no_args(
708        args.source_range,
709        args.node_path.clone(),
710        args.ctx,
711        fn_name.map(|f| f.to_string()).or_else(|| args.fn_name.clone()),
712    );
713
714    // If it's possible the input arg was meant to be labelled and we probably don't want to use
715    // it as the input arg, then treat it as labelled.
716    if let Some((Some(label), _)) = args.unlabeled.first()
717        && args.unlabeled.len() == 1
718        && (fn_def.input_arg.is_none() || args.pipe_value.is_some())
719        && fn_def.named_args.iter().any(|p| p.0 == label)
720        && !args.labeled.contains_key(label)
721    {
722        let Some((label, arg)) = args.unlabeled.pop() else {
723            let message = "Expected unlabeled arg to be present".to_owned();
724            debug_assert!(false, "{}", &message);
725            return Err(KclError::new_internal(KclErrorDetails::new(
726                message,
727                vec![args.source_range],
728            )));
729        };
730        args.labeled.insert(label.unwrap(), arg);
731    }
732
733    // Apply the `a == a: a` shorthand by desugaring unlabeled args into labeled ones.
734    let (labeled_unlabeled, unlabeled_unlabeled) = args.unlabeled.into_iter().partition(|(l, _)| {
735        if let Some(l) = l
736            && fn_def.named_args.contains_key(l)
737            && !args.labeled.contains_key(l)
738        {
739            true
740        } else {
741            false
742        }
743    });
744    args.unlabeled = unlabeled_unlabeled;
745    for (l, arg) in labeled_unlabeled {
746        let previous = args.labeled.insert(l.unwrap(), arg);
747        debug_assert!(previous.is_none());
748    }
749
750    if let Some((name, ty)) = &fn_def.input_arg {
751        // Expecting an input arg
752
753        if args.unlabeled.is_empty() {
754            // No args provided
755
756            if let Some(pipe) = args.pipe_value {
757                // But there is a pipeline
758                result.unlabeled = vec![(None, pipe)];
759            } else if let Some(arg) = args.labeled.swap_remove(name) {
760                // Mistakenly labelled
761                exec_state.err(CompilationIssue::err(
762                    arg.source_range,
763                    format!(
764                        "{} expects an unlabeled first argument (`@{name}`), but it is labelled in the call. You might try removing the `{name} = `",
765                        fn_name
766                            .map(|n| format!("The function `{n}`"))
767                            .unwrap_or_else(|| "This function".to_owned()),
768                    ),
769                ));
770                result.unlabeled = vec![(Some(name.clone()), arg)];
771            } else {
772                // Just missing
773                return Err(KclError::new_argument(KclErrorDetails::new(
774                    "This function expects an unlabeled first parameter, but you haven't passed it one.".to_owned(),
775                    fn_def.ast.as_source_ranges(),
776                )));
777            }
778        } else if args.unlabeled.len() == 1
779            && let Some(unlabeled_arg) = args.unlabeled.pop()
780        {
781            let mut arg = unlabeled_arg.1;
782            if let Some(ty) = ty {
783                // Suppress warnings about types because they should only be
784                // warned about once for the function definition.
785                let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range, false, true)
786                    .map_err(|e| KclError::new_semantic(e.into()))?;
787                arg.value = arg.value.coerce(&rty, true, exec_state).map_err(|_| {
788                    KclError::new_argument(KclErrorDetails::new(
789                        format!(
790                            "The input argument of {} requires {}",
791                            fn_name
792                                .map(|n| format!("`{n}`"))
793                                .unwrap_or_else(|| "this function".to_owned()),
794                            type_err_str(ty, &arg.value, &arg.source_range, exec_state),
795                        ),
796                        vec![arg.source_range],
797                    ))
798                })?;
799            }
800            result.unlabeled = vec![(None, arg)]
801        } else {
802            // Multiple unlabelled args
803
804            // Try to un-spread args into an array
805            if let Some(Type::Array { len, .. }) = ty {
806                if len.satisfied(args.unlabeled.len(), false).is_none() {
807                    exec_state.err(CompilationIssue::err(
808                        args.source_range,
809                        format!(
810                            "{} expects an array input argument with {} elements",
811                            fn_name
812                                .map(|n| format!("The function `{n}`"))
813                                .unwrap_or_else(|| "This function".to_owned()),
814                            len.human_friendly_type(),
815                        ),
816                    ));
817                }
818
819                let source_range = SourceRange::merge(args.unlabeled.iter().map(|(_, a)| a.source_range));
820                exec_state.warn_experimental("array input arguments", source_range);
821                result.unlabeled = vec![(
822                    None,
823                    Arg {
824                        source_range,
825                        value: KclValue::HomArray {
826                            value: args.unlabeled.drain(..).map(|(_, a)| a.value).collect(),
827                            ty: RuntimeType::any(),
828                        },
829                    },
830                )]
831            }
832        }
833    }
834
835    // Either we didn't move the arg above, or we're not expecting one.
836    if !args.unlabeled.is_empty() {
837        // Not expecting an input arg, but found one or more
838        let actuals = args.labeled.keys();
839        let formals: Vec<_> = fn_def
840            .named_args
841            .keys()
842            .filter_map(|name| {
843                if actuals.clone().any(|a| a == name) {
844                    return None;
845                }
846
847                Some(format!("`{name}`"))
848            })
849            .collect();
850
851        let suggestion = if formals.is_empty() {
852            String::new()
853        } else {
854            format!("; suggested labels: {}", formals.join(", "))
855        };
856
857        let mut errors = args.unlabeled.iter().map(|(_, arg)| {
858            CompilationIssue::err(
859                arg.source_range,
860                format!("This argument needs a label, but it doesn't have one{suggestion}"),
861            )
862        });
863
864        let first = errors.next().unwrap();
865        errors.for_each(|e| exec_state.err(e));
866
867        return Err(KclError::new_argument(first.into()));
868    }
869
870    for (label, mut arg) in args.labeled {
871        match fn_def.named_args.get(&label) {
872            Some(NamedParam {
873                experimental: _,
874                default_value: def,
875                ty,
876            }) => {
877                // For optional args, passing None should be the same as not passing an arg.
878                if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
879                    if let Some(ty) = ty {
880                        // Suppress warnings about types because they should
881                        // only be warned about once for the function
882                        // definition.
883                        let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range, false, true)
884                            .map_err(|e| KclError::new_semantic(e.into()))?;
885                        arg.value = arg
886                                .value
887                                .coerce(
888                                    &rty,
889                                    true,
890                                    exec_state,
891                                )
892                                .map_err(|e| {
893                                    let mut message = format!(
894                                        "{label} requires {}",
895                                        type_err_str(ty, &arg.value, &arg.source_range, exec_state),
896                                    );
897                                    if let Some(ty) = e.explicit_coercion {
898                                        // TODO if we have access to the AST for the argument we could choose which example to suggest.
899                                        message = format!("{message}\n\nYou may need to add information about the type of the argument, for example:\n  using a numeric suffix: `42{ty}`\n  or using type ascription: `foo(): {ty}`");
900                                    }
901                                    KclError::new_argument(KclErrorDetails::new(
902                                        message,
903                                        vec![arg.source_range],
904                                    ))
905                                })?;
906                    }
907                    result.labeled.insert(label, arg);
908                }
909            }
910            None => {
911                exec_state.err(CompilationIssue::err(
912                    arg.source_range,
913                    format!(
914                        "`{label}` is not an argument of {}",
915                        fn_name
916                            .map(|n| format!("`{n}`"))
917                            .unwrap_or_else(|| "this function".to_owned()),
918                    ),
919                ));
920            }
921        }
922    }
923
924    Ok(result)
925}
926
927fn assign_args_to_params_kw(
928    fn_def: &FunctionSource,
929    args: Args<Desugared>,
930    exec_state: &mut ExecState,
931) -> Result<(), KclError> {
932    // Add the arguments to the memory.  A new call frame should have already
933    // been created.
934    let source_ranges = fn_def.ast.as_source_ranges();
935
936    for (name, param) in fn_def.named_args.iter() {
937        let arg = args.labeled.get(name);
938        match arg {
939            Some(arg) => {
940                exec_state.mut_stack().add(
941                    name.clone(),
942                    arg.value.clone(),
943                    arg.source_ranges().pop().unwrap_or(SourceRange::synthetic()),
944                )?;
945            }
946            None => match &param.default_value {
947                Some(default_val) => {
948                    let value = KclValue::from_default_param(default_val.clone(), exec_state);
949                    exec_state
950                        .mut_stack()
951                        .add(name.clone(), value, default_val.source_range())?;
952                }
953                None => {
954                    return Err(KclError::new_argument(KclErrorDetails::new(
955                        format!("This function requires a parameter {name}, but you haven't passed it one."),
956                        source_ranges,
957                    )));
958                }
959            },
960        }
961    }
962
963    if let Some((param_name, _)) = &fn_def.input_arg {
964        let Some(unlabeled) = args.unlabeled_kw_arg_unconverted() else {
965            debug_assert!(false, "Bad args");
966            return Err(KclError::new_internal(KclErrorDetails::new(
967                "Desugared arguments are inconsistent".to_owned(),
968                source_ranges,
969            )));
970        };
971        exec_state.mut_stack().add(
972            param_name.clone(),
973            unlabeled.value.clone(),
974            unlabeled.source_ranges().pop().unwrap_or(SourceRange::synthetic()),
975        )?;
976    }
977
978    Ok(())
979}
980
981fn coerce_result_type(
982    result: Result<Option<KclValue>, KclError>,
983    fn_def: &FunctionSource,
984    exec_state: &mut ExecState,
985) -> Result<Option<KclValue>, KclError> {
986    if let Ok(Some(val)) = result {
987        if let Some(ret_ty) = &fn_def.return_type {
988            // Suppress warnings about types because they should only be warned
989            // about once for the function definition.
990            let ty = RuntimeType::from_parsed(ret_ty.inner.clone(), exec_state, ret_ty.as_source_range(), false, true)
991                .map_err(|e| KclError::new_semantic(e.into()))?;
992            let val = val.coerce(&ty, true, exec_state).map_err(|_| {
993                KclError::new_type(KclErrorDetails::new(
994                    format!(
995                        "This function requires its result to be {}",
996                        type_err_str(ret_ty, &val, &(&val).into(), exec_state)
997                    ),
998                    ret_ty.as_source_ranges(),
999                ))
1000            })?;
1001            Ok(Some(val))
1002        } else {
1003            Ok(Some(val))
1004        }
1005    } else {
1006        result
1007    }
1008}
1009
1010#[cfg(test)]
1011mod test {
1012    use std::sync::Arc;
1013
1014    use super::*;
1015    use crate::execution::ContextType;
1016    use crate::execution::EnvironmentRef;
1017    use crate::execution::memory::Stack;
1018    use crate::execution::parse_execute;
1019    use crate::execution::types::NumericType;
1020    use crate::parsing::ast::types::DefaultParamVal;
1021    use crate::parsing::ast::types::FunctionExpression;
1022    use crate::parsing::ast::types::Identifier;
1023    use crate::parsing::ast::types::Parameter;
1024    use crate::parsing::ast::types::Program;
1025
1026    #[tokio::test(flavor = "multi_thread")]
1027    async fn test_assign_args_to_params() {
1028        // Set up a little framework for this test.
1029        fn mem(number: usize) -> KclValue {
1030            KclValue::Number {
1031                value: number as f64,
1032                ty: NumericType::count(),
1033                meta: Default::default(),
1034            }
1035        }
1036        fn ident(s: &'static str) -> Node<Identifier> {
1037            Node::no_src(Identifier {
1038                name: s.to_owned(),
1039                digest: None,
1040            })
1041        }
1042        fn opt_param(s: &'static str) -> Parameter {
1043            Parameter {
1044                experimental: false,
1045                identifier: ident(s),
1046                param_type: None,
1047                default_value: Some(DefaultParamVal::none()),
1048                labeled: true,
1049                digest: None,
1050            }
1051        }
1052        fn req_param(s: &'static str) -> Parameter {
1053            Parameter {
1054                experimental: false,
1055                identifier: ident(s),
1056                param_type: None,
1057                default_value: None,
1058                labeled: true,
1059                digest: None,
1060            }
1061        }
1062        fn additional_program_memory(items: &[(String, KclValue)]) -> Stack {
1063            let mut program_memory = Stack::new_for_tests();
1064            for (name, item) in items {
1065                program_memory
1066                    .add(name.clone(), item.clone(), SourceRange::default())
1067                    .unwrap();
1068            }
1069            program_memory
1070        }
1071        // Declare the test cases.
1072        for (test_name, params, args, expected) in [
1073            ("empty", Vec::new(), Vec::new(), Ok(additional_program_memory(&[]))),
1074            (
1075                "all params required, and all given, should be OK",
1076                vec![req_param("x")],
1077                vec![("x", mem(1))],
1078                Ok(additional_program_memory(&[("x".to_owned(), mem(1))])),
1079            ),
1080            (
1081                "all params required, none given, should error",
1082                vec![req_param("x")],
1083                vec![],
1084                Err(KclError::new_argument(KclErrorDetails::new(
1085                    "This function requires a parameter x, but you haven't passed it one.".to_owned(),
1086                    vec![SourceRange::default()],
1087                ))),
1088            ),
1089            (
1090                "all params optional, none given, should be OK",
1091                vec![opt_param("x")],
1092                vec![],
1093                Ok(additional_program_memory(&[("x".to_owned(), KclValue::none())])),
1094            ),
1095            (
1096                "mixed params, too few given",
1097                vec![req_param("x"), opt_param("y")],
1098                vec![],
1099                Err(KclError::new_argument(KclErrorDetails::new(
1100                    "This function requires a parameter x, but you haven't passed it one.".to_owned(),
1101                    vec![SourceRange::default()],
1102                ))),
1103            ),
1104            (
1105                "mixed params, minimum given, should be OK",
1106                vec![req_param("x"), opt_param("y")],
1107                vec![("x", mem(1))],
1108                Ok(additional_program_memory(&[
1109                    ("x".to_owned(), mem(1)),
1110                    ("y".to_owned(), KclValue::none()),
1111                ])),
1112            ),
1113            (
1114                "mixed params, maximum given, should be OK",
1115                vec![req_param("x"), opt_param("y")],
1116                vec![("x", mem(1)), ("y", mem(2))],
1117                Ok(additional_program_memory(&[
1118                    ("x".to_owned(), mem(1)),
1119                    ("y".to_owned(), mem(2)),
1120                ])),
1121            ),
1122        ] {
1123            // Run each test.
1124            let func_expr = Node::no_src(FunctionExpression {
1125                name: None,
1126                params,
1127                body: Program::empty(),
1128                return_type: None,
1129                digest: None,
1130            });
1131            let func_src = FunctionSource::kcl(
1132                Box::new(func_expr),
1133                EnvironmentRef::dummy(),
1134                crate::execution::kcl_value::KclFunctionSourceParams {
1135                    std_props: None,
1136                    experimental: false,
1137                    include_in_feature_tree: false,
1138                },
1139            );
1140            let labeled = args
1141                .iter()
1142                .map(|(name, value)| {
1143                    let arg = Arg::new(value.clone(), SourceRange::default());
1144                    ((*name).to_owned(), arg)
1145                })
1146                .collect::<IndexMap<_, _>>();
1147            let exec_ctxt = ExecutorContext {
1148                engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
1149                fs: Arc::new(crate::fs::FileManager::new()),
1150                settings: Default::default(),
1151                context_type: ContextType::Mock,
1152            };
1153            let mut exec_state = ExecState::new(&exec_ctxt);
1154            exec_state.mod_local.stack = Stack::new_for_tests();
1155
1156            let args = Args {
1157                fn_name: Some("test".to_owned()),
1158                labeled,
1159                unlabeled: Vec::new(),
1160                source_range: SourceRange::default(),
1161                node_path: None,
1162                ctx: exec_ctxt,
1163                pipe_value: None,
1164                _status: std::marker::PhantomData,
1165            };
1166
1167            let actual = assign_args_to_params_kw(&func_src, args, &mut exec_state).map(|_| exec_state.mod_local.stack);
1168            assert_eq!(
1169                actual, expected,
1170                "failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}"
1171            );
1172        }
1173    }
1174
1175    #[tokio::test(flavor = "multi_thread")]
1176    async fn type_check_user_args() {
1177        let program = r#"fn makeMessage(prefix: string, suffix: string) {
1178  return prefix + suffix
1179}
1180
1181msg1 = makeMessage(prefix = "world", suffix = " hello")
1182msg2 = makeMessage(prefix = 1, suffix = 3)"#;
1183        let err = parse_execute(program).await.unwrap_err();
1184        assert_eq!(
1185            err.message(),
1186            "prefix requires a value with type `string`, but found a value with type `number`.\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: mm` or `(a * b): deg`."
1187        )
1188    }
1189
1190    #[tokio::test(flavor = "multi_thread")]
1191    async fn map_closure_error_mentions_fn_name() {
1192        let program = r#"
1193arr = ["hello"]
1194map(array = arr, f = fn(@item: number) { return item })
1195"#;
1196        let err = parse_execute(program).await.unwrap_err();
1197        assert!(
1198            err.message().contains("map closure"),
1199            "expected map closure errors to include the closure name, got: {}",
1200            err.message()
1201        );
1202    }
1203
1204    #[tokio::test(flavor = "multi_thread")]
1205    async fn array_input_arg() {
1206        let ast = r#"fn f(@input: [mm]) { return 1 }
1207f([1, 2, 3])
1208f(1, 2, 3)
1209"#;
1210        parse_execute(ast).await.unwrap();
1211    }
1212}