kcl_lib/execution/
fn_call.rs

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