Skip to main content

kcl_lib/execution/
fn_call.rs

1use async_recursion::async_recursion;
2use indexmap::IndexMap;
3use kcl_api::Group;
4use kcl_api::OpArg;
5
6use crate::CompilationIssue;
7use crate::NodePath;
8use crate::NodePathExt;
9use crate::SourceRange;
10use crate::errors::KclError;
11use crate::errors::KclErrorDetails;
12use crate::execution::BodyType;
13use crate::execution::ExecState;
14use crate::execution::ExecutorContext;
15use crate::execution::Geometry;
16use crate::execution::KclValue;
17use crate::execution::KclValueControlFlow;
18use crate::execution::Metadata;
19use crate::execution::Solid;
20use crate::execution::StatementKind;
21use crate::execution::TagEngineInfo;
22use crate::execution::TagIdentifier;
23use crate::execution::annotations;
24use crate::execution::cad_op::Operation;
25use crate::execution::cad_op::op_from_kcl_value;
26use crate::execution::control_continue;
27use crate::execution::kcl_value::FunctionBody;
28use crate::execution::kcl_value::FunctionSource;
29use crate::execution::kcl_value::NamedParam;
30use crate::execution::memory;
31use crate::execution::types::CoercionMode;
32use crate::execution::types::RuntimeType;
33use crate::parsing::ast::types::CallExpressionKw;
34use crate::parsing::ast::types::Node;
35use crate::parsing::ast::types::Type;
36use crate::std::ConsumedSolidArgCheck;
37use crate::std::RegionBehavior;
38use crate::std::StaleRegionPolicy;
39use crate::std::region_consumption::PendingRegionConsumption;
40use crate::std::region_consumption::prepare_region_consumption;
41use crate::std::region_consumption::record_consumed_regions;
42use crate::std::region_consumption::validate_region_args_not_consumed;
43use crate::std::region_consumption::warn_if_region_args_consumed;
44use crate::std::solid_consumption::validate_value_not_consumed;
45use crate::std::solid_consumption::warn_if_value_consumed_for_deprecated_call;
46
47#[derive(Debug, Clone)]
48pub struct Args<Status: ArgsStatus = Desugared> {
49    /// Name of the function these args are being passed into.
50    pub fn_name: Option<String>,
51    /// Unlabeled keyword args. Currently only the first formal arg can be unlabeled.
52    /// If the argument was a local variable, then the first element of the tuple is its name
53    /// which may be used to treat this arg as a labelled arg.
54    pub unlabeled: Vec<(Option<String>, Arg)>,
55    /// Labeled args.
56    pub labeled: IndexMap<String, Arg>,
57    pub source_range: SourceRange,
58    pub node_path: Option<NodePath>,
59    pub ctx: ExecutorContext,
60    /// If this call happens inside a pipe (|>) expression, this holds the LHS of that |>.
61    /// Otherwise it's None.
62    pub pipe_value: Option<Arg>,
63    _status: std::marker::PhantomData<Status>,
64}
65
66pub trait ArgsStatus: std::fmt::Debug + Clone {}
67
68#[derive(Debug, Clone)]
69pub struct Sugary;
70impl ArgsStatus for Sugary {}
71
72// Invariants guaranteed by the `Desugared` status:
73// - There is either 0 or 1 unlabeled arguments
74// - Any lableled args are in the labeled map, and not the unlabeled Vec.
75// - The arguments match the type signature of the function exactly
76// - pipe_value.is_none()
77#[derive(Debug, Clone)]
78pub struct Desugared;
79impl ArgsStatus for Desugared {}
80
81impl Args<Sugary> {
82    /// Collect the given keyword arguments.
83    pub fn new(
84        labeled: IndexMap<String, Arg>,
85        unlabeled: Vec<(Option<String>, Arg)>,
86        source_range: SourceRange,
87        node_path: Option<NodePath>,
88        exec_state: &mut ExecState,
89        ctx: ExecutorContext,
90        fn_name: Option<String>,
91    ) -> Args<Sugary> {
92        Args {
93            fn_name,
94            labeled,
95            unlabeled,
96            source_range,
97            node_path,
98            ctx,
99            pipe_value: exec_state.pipe_value().map(|v| Arg::new(v.clone(), source_range)),
100            _status: std::marker::PhantomData,
101        }
102    }
103}
104
105impl<Status: ArgsStatus> Args<Status> {
106    /// How many arguments are there?
107    pub fn len(&self) -> usize {
108        self.labeled.len() + self.unlabeled.len()
109    }
110
111    /// Are there no arguments?
112    pub fn is_empty(&self) -> bool {
113        self.labeled.is_empty() && self.unlabeled.is_empty()
114    }
115}
116
117impl Args<Desugared> {
118    pub fn new_no_args(
119        source_range: SourceRange,
120        node_path: Option<NodePath>,
121        ctx: ExecutorContext,
122        fn_name: Option<String>,
123    ) -> Args {
124        Args {
125            fn_name,
126            unlabeled: Default::default(),
127            labeled: Default::default(),
128            source_range,
129            node_path,
130            ctx,
131            pipe_value: None,
132            _status: std::marker::PhantomData,
133        }
134    }
135
136    /// Get the unlabeled keyword argument. If not set, returns None.
137    pub(crate) fn unlabeled_kw_arg_unconverted(&self) -> Option<&Arg> {
138        self.unlabeled.first().map(|(_, a)| a)
139    }
140}
141
142#[derive(Debug, Clone)]
143pub struct Arg {
144    /// The evaluated argument.
145    pub value: KclValue,
146    /// The source range of the unevaluated argument.
147    pub source_range: SourceRange,
148}
149
150impl Arg {
151    pub fn new(value: KclValue, source_range: SourceRange) -> Self {
152        Self { value, source_range }
153    }
154
155    pub fn synthetic(value: KclValue) -> Self {
156        Self {
157            value,
158            source_range: SourceRange::synthetic(),
159        }
160    }
161
162    pub fn source_ranges(&self) -> Vec<SourceRange> {
163        vec![self.source_range]
164    }
165}
166
167impl Node<CallExpressionKw> {
168    #[async_recursion]
169    pub(super) async fn execute(
170        &self,
171        exec_state: &mut ExecState,
172        ctx: &ExecutorContext,
173    ) -> Result<KclValueControlFlow, KclError> {
174        let fn_name = &self.callee;
175        let callsite: SourceRange = self.into();
176
177        // Resolve the function before evaluating arguments so calls can mutate
178        // exec_state without holding a memory borrow.
179        let func: KclValue = fn_name.get_result(exec_state, ctx).await?;
180
181        let Some(fn_src) = func.as_function() else {
182            return Err(KclError::new_semantic(KclErrorDetails::new(
183                "cannot call this because it isn't a function".to_string(),
184                vec![callsite],
185            )));
186        };
187
188        // Build a hashmap from argument labels to the final evaluated values.
189        let mut fn_args = IndexMap::with_capacity(self.arguments.len());
190        let mut unlabeled = Vec::new();
191
192        // Evaluate the unlabeled first param, if any exists.
193        if let Some(ref arg_expr) = self.unlabeled {
194            let source_range = SourceRange::from(arg_expr.clone());
195            let metadata = Metadata { source_range };
196            let value_cf = ctx
197                .execute_expr(arg_expr, exec_state, &metadata, &[], StatementKind::Expression)
198                .await?;
199            let value = control_continue!(value_cf);
200
201            let label = arg_expr.ident_name().map(str::to_owned);
202
203            unlabeled.push((label, Arg::new(value, source_range)))
204        }
205
206        for arg_expr in &self.arguments {
207            let source_range = SourceRange::from(arg_expr.arg.clone());
208            let metadata = Metadata { source_range };
209            let value_cf = ctx
210                .execute_expr(&arg_expr.arg, exec_state, &metadata, &[], StatementKind::Expression)
211                .await?;
212            let value = control_continue!(value_cf);
213            let arg = Arg::new(value, source_range);
214            match &arg_expr.label {
215                Some(l) => {
216                    fn_args.insert(l.name.clone(), arg);
217                }
218                None => {
219                    unlabeled.push((arg_expr.arg.ident_name().map(str::to_owned), arg));
220                }
221            }
222        }
223
224        let args = Args::new(
225            fn_args,
226            unlabeled,
227            callsite,
228            self.node_path.clone(),
229            exec_state,
230            ctx.clone(),
231            Some(fn_name.name.name.clone()),
232        );
233
234        let return_value = fn_src
235            .call_kw(Some(fn_name.to_string()), exec_state, ctx, args, callsite)
236            .await
237            .map_err(|e| {
238                // Add the call expression to the source ranges.
239                //
240                // TODO: Use the name that the function was defined
241                // with, not the identifier it was used with.
242                e.add_unwind_location(Some(fn_name.to_string()), callsite)
243            })?;
244
245        let result = return_value.ok_or_else(move || {
246            let mut source_ranges: Vec<SourceRange> = vec![callsite];
247            // We want to send the source range of the original function.
248            if let KclValue::Function { meta, .. } = func {
249                source_ranges = meta.iter().map(|m| m.source_range).collect();
250            };
251            KclError::new_undefined_value(
252                KclErrorDetails::new(
253                    format!("Result of user-defined function {fn_name} is undefined"),
254                    source_ranges,
255                ),
256                None,
257            )
258        })?;
259
260        Ok(result)
261    }
262}
263
264/// Guidance included in deprecation warnings for sketch v1 stdlib functions.
265/// The warning must stand on its own: a human or AI agent reading it should
266/// learn what replaces the function and where to find conversion examples
267/// without any other context.
268const SKETCH_V1_MIGRATION_HELP: &str = "It is part of the legacy sketch API (sketch v1), which is replaced by the sketch-solve API.
269
270See https://zoo.dev/docs/kcl-book/sketch2d_constraints.html for an introduction to sketch-solve with examples.
271
272Draw profiles inside a `sketch(on = XY) { ... }` block using segment functions with absolute points, e.g. `line(start = [0, 0], end = [4, 3])`, optionally marking values as adjustable with `var` and constraining them with constraint functions like `coincident()` or `horizontal()`. ";
273
274/// Migration guidance for a deprecated stdlib function, when it has a
275/// dedicated replacement story beyond its docs page.
276fn migration_help(fn_src: &FunctionSource) -> Option<&'static str> {
277    let name = &fn_src.std_props.as_ref()?.name;
278    // Every deprecated function in std::sketch is part of sketch v1, which
279    // sketch-solve replaces in KCL 2.0.
280    name.starts_with("std::sketch::").then_some(SKETCH_V1_MIGRATION_HELP)
281}
282
283impl FunctionSource {
284    pub(crate) async fn call_kw(
285        &self,
286        fn_name: Option<String>,
287        exec_state: &mut ExecState,
288        ctx: &ExecutorContext,
289        args: Args<Sugary>,
290        callsite: SourceRange,
291    ) -> Result<Option<KclValueControlFlow>, KclError> {
292        exec_state.inc_call_stack_size(callsite)?;
293
294        let result = self.inner_call_kw(fn_name, exec_state, ctx, args, callsite).await;
295
296        exec_state.dec_call_stack_size(callsite)?;
297        result
298    }
299
300    async fn inner_call_kw(
301        &self,
302        fn_name: Option<String>,
303        exec_state: &mut ExecState,
304        ctx: &ExecutorContext,
305        args: Args<Sugary>,
306        callsite: SourceRange,
307    ) -> Result<Option<KclValueControlFlow>, KclError> {
308        let (state, args) = self.call_setup(&fn_name, exec_state, args, callsite)?;
309        // Do not early return via ? or something until we've called
310        // call_finish (or call_abort_on_arg_binding_failure), so that the
311        // ambient flags are restored and the callee env is popped.
312        let result = match &self.body {
313            FunctionBody::Rust(f) => f(exec_state, args).await.map(Some),
314            FunctionBody::Kcl(_) => {
315                if let Err(e) = assign_args_to_params_kw(self, args, exec_state) {
316                    return Err(Self::call_abort_on_arg_binding_failure(state, e, exec_state));
317                }
318
319                let block_result = ctx.exec_block(&self.ast.body, exec_state, BodyType::Block).await;
320                self.kcl_body_result(block_result, exec_state)
321            }
322        };
323        self.call_finish(state, result, exec_state)
324    }
325
326    /// The first half of a function call: warnings, argument type checking,
327    /// operation setup, pushing the callee environment, and stdlib
328    /// ambient-flag tracking. After this succeeds, the callee environment is
329    /// pushed and the ambient flags are set, so every path must reach
330    /// [`Self::call_finish`] (or [`Self::call_abort_on_arg_binding_failure`])
331    /// to balance them. The recursive executor keeps the returned [`CallState`]
332    /// across the body await; the machine executor parks it in a call-boundary
333    /// continuation.
334    pub(super) fn call_setup(
335        &self,
336        fn_name: &Option<String>,
337        exec_state: &mut ExecState,
338        args: Args<Sugary>,
339        callsite: SourceRange,
340    ) -> Result<(CallState, Args), KclError> {
341        // The KCL stdlib is allowed to use deprecated sketch1 functions inside.
342        let warn_on_deprecated_usage = !exec_state.mod_local.inside_stdlib;
343        if warn_on_deprecated_usage {
344            let subject = match &fn_name {
345                Some(n) => format!("`{n}`"),
346                None => "This function".to_owned(),
347            };
348            let message = if self.deprecated {
349                Some(match migration_help(self) {
350                    Some(help) => format!("{subject} is deprecated. {help}"),
351                    None => format!("{subject} is deprecated, see the docs for a recommended replacement"),
352                })
353            } else if let Some(since) = &self.deprecated_since
354                && annotations::version_ge(exec_state.deprecation_version(), since)
355            {
356                Some(match migration_help(self) {
357                    Some(help) => format!("{subject} is deprecated as of KCL {since}. {help}"),
358                    None => {
359                        format!(
360                            "{subject} is deprecated as of KCL {since}. See the docs for a recommended replacement."
361                        )
362                    }
363                })
364            } else {
365                None
366            };
367            if let Some(message) = message {
368                let mut issue = CompilationIssue::err(callsite, message);
369                issue.tag = crate::errors::Tag::Deprecated;
370                exec_state.warn(issue, annotations::WARN_DEPRECATED);
371            }
372        }
373        if self.experimental {
374            exec_state.warn_experimental(
375                &match &fn_name {
376                    Some(n) => format!("`{n}`"),
377                    None => "This function".to_owned(),
378                },
379                callsite,
380            );
381        }
382
383        let args = type_check_params_kw(fn_name.as_deref(), self, args, exec_state)?;
384        let face_tag_names = face_tag_names_for_call(self, &args);
385        let pending_region_consumption = prepare_region_consumption(
386            self.std_props
387                .as_ref()
388                .map_or(RegionBehavior::WarnOnConsumed, |props| props.region_behavior),
389            &args,
390            exec_state,
391        )?;
392
393        // Warn if experimental or deprecated arguments are used after desugaring.
394        for (label, arg) in &args.labeled {
395            let Some(param) = self.named_args.get(label.as_str()) else {
396                continue;
397            };
398            if param.experimental {
399                exec_state.warn_experimental(
400                    &match &fn_name {
401                        Some(f) => format!("`{f}({label})`"),
402                        None => label.to_owned(),
403                    },
404                    arg.source_range,
405                );
406            }
407            // `deprecated` deprecates the parameter for all versions, whereas
408            // `deprecated_since` only deprecates it at or after a given version.
409            let deprecation_suffix = if !warn_on_deprecated_usage {
410                None
411            } else if param.deprecated {
412                Some("is deprecated, see the docs for a recommended replacement".to_owned())
413            } else if let Some(since) = &param.deprecated_since
414                && annotations::version_ge(exec_state.deprecation_version(), since)
415            {
416                Some(format!(
417                    "is deprecated as of KCL {since}. See the docs for a recommended replacement."
418                ))
419            } else {
420                None
421            };
422            if let Some(suffix) = deprecation_suffix {
423                let qualified = match &fn_name {
424                    Some(f) => format!("`{f}({label})`"),
425                    None => format!("`{label}`"),
426                };
427                let mut issue = CompilationIssue::err(arg.source_range, format!("{qualified} {suffix}"));
428                issue.tag = crate::errors::Tag::Deprecated;
429                exec_state.warn(issue, annotations::WARN_DEPRECATED);
430            }
431        }
432
433        // Don't early return until the stack frame is popped!
434        self.body.prep_mem(exec_state)?;
435
436        // Some function calls might get added to the feature tree.
437        // We do this by adding an "operation".
438
439        // Don't add operations if the KCL code being executed is
440        // just the KCL stdlib calling other KCL stdlib,
441        // because the stdlib internals aren't relevant to users,
442        // that would just be pointless noise.
443        //
444        // Do add operations if the KCL being executed is
445        // user-defined, or the calling code is user-defined,
446        // because that's relevant to the user.
447        let would_trace_stdlib_internals = exec_state.mod_local.inside_stdlib && self.is_std();
448        // self.include_in_feature_tree is set by the KCL annotation `@(feature_tree = true)`.
449        let should_track_operation = !would_trace_stdlib_internals && self.include_in_feature_tree;
450        let op = if should_track_operation {
451            let op_labeled_args = args
452                .labeled
453                .iter()
454                .map(|(k, arg)| (k.clone(), OpArg::new(op_from_kcl_value(&arg.value), arg.source_range)))
455                .collect();
456
457            // If you're calling a stdlib function, track that call as an operation.
458            if self.is_std() {
459                Some(Operation::StdLibCall {
460                    name: fn_name.clone().unwrap_or_else(|| "unknown function".to_owned()),
461                    unlabeled_arg: args
462                        .unlabeled_kw_arg_unconverted()
463                        .map(|arg| OpArg::new(op_from_kcl_value(&arg.value), arg.source_range)),
464                    labeled_args: op_labeled_args,
465                    node_path: NodePath::placeholder(),
466                    source_range: callsite,
467                    stdlib_entry_source_range: exec_state.mod_local.stdlib_entry_source_range,
468                    is_error: false,
469                })
470            } else {
471                // Otherwise, you're calling a user-defined function, track that call as an operation.
472                exec_state.push_op(Operation::GroupBegin {
473                    group: Group::FunctionCall {
474                        name: fn_name.clone(),
475                        function_source_range: self.ast.as_source_range(),
476                        unlabeled_arg: args
477                            .unlabeled_kw_arg_unconverted()
478                            .map(|arg| OpArg::new(op_from_kcl_value(&arg.value), arg.source_range)),
479                        labeled_args: op_labeled_args,
480                    },
481                    node_path: NodePath::placeholder(),
482                    source_range: callsite,
483                });
484
485                None
486            }
487        } else {
488            None
489        };
490
491        let is_calling_into_stdlib = match &self.body {
492            FunctionBody::Rust(_) => true,
493            FunctionBody::Kcl(_) => self.is_std(),
494        };
495        let is_crossing_into_stdlib = is_calling_into_stdlib && !exec_state.mod_local.inside_stdlib;
496        let is_crossing_out_of_stdlib = !is_calling_into_stdlib && exec_state.mod_local.inside_stdlib;
497        let stdlib_entry_source_range = if is_crossing_into_stdlib {
498            // When we're calling into the stdlib, for example calling hole(),
499            // track the location so that any further stdlib calls like
500            // subtract() can point to the hole() call. The frontend needs this.
501            Some(callsite)
502        } else if is_crossing_out_of_stdlib {
503            // When map() calls a user-defined function, and it calls extrude()
504            // for example, we want it to point the the extrude() call, not
505            // the map() call.
506            None
507        } else {
508            // When we're not crossing the stdlib boundary, keep the previous
509            // value.
510            exec_state.mod_local.stdlib_entry_source_range
511        };
512
513        let prev_inside_stdlib = std::mem::replace(&mut exec_state.mod_local.inside_stdlib, is_calling_into_stdlib);
514        let prev_stdlib_entry_source_range = std::mem::replace(
515            &mut exec_state.mod_local.stdlib_entry_source_range,
516            stdlib_entry_source_range,
517        );
518
519        Ok((
520            CallState {
521                prev_inside_stdlib,
522                prev_stdlib_entry_source_range,
523                op,
524                should_track_operation,
525                is_calling_into_stdlib,
526                face_tag_names,
527                pending_region_consumption,
528            },
529            args,
530        ))
531    }
532
533    /// Compute a KCL function body's result while the callee environment is
534    /// still pushed: an `Exit` passes through untouched; otherwise the
535    /// function's result is the `__return` value recorded in the callee
536    /// environment, if any.
537    ///
538    /// NOTE: a `return` statement does NOT stop the body -- it records
539    /// `__return` and execution continues to the following statements (see
540    /// exec_block's ReturnStatement arm). The block's own trailing value is
541    /// deliberately ignored here; only `__return` counts.
542    pub(super) fn kcl_body_result(
543        &self,
544        block_result: Result<Option<KclValueControlFlow>, KclError>,
545        exec_state: &mut ExecState,
546    ) -> Result<Option<KclValueControlFlow>, KclError> {
547        block_result.map(|cf| {
548            if let Some(cf) = cf
549                && cf.is_some_return()
550            {
551                return Some(cf);
552            }
553            // Ignore the block's value and extract the return value
554            // from memory.
555            exec_state
556                .stack()
557                .get(memory::RETURN_NAME, self.ast.as_source_range())
558                .ok()
559                .map(KclValue::continue_)
560        })
561    }
562
563    /// The failure path when binding a KCL function's arguments fails, before
564    /// the body ran: restore `inside_stdlib` and pop the callee environment.
565    /// Deliberately asymmetric with [`Self::call_finish`] -- it does not
566    /// restore `stdlib_entry_source_range` and does not finalize the
567    /// operation -- preserving the recursive executor's historical behavior
568    /// exactly.
569    pub(super) fn call_abort_on_arg_binding_failure(
570        state: CallState,
571        e: KclError,
572        exec_state: &mut ExecState,
573    ) -> KclError {
574        exec_state.mod_local.inside_stdlib = state.prev_inside_stdlib;
575        match exec_state.mut_stack().pop_env() {
576            Ok(_) => e,
577            Err(pop_err) => pop_err,
578        }
579    }
580
581    /// The second half of a function call: restore the ambient stdlib flags,
582    /// pop the callee environment, finalize the operation, and then -- for
583    /// normal completions only -- apply tag updates and return-type coercion.
584    /// `Exit` control flow bypasses tags and coercion (it terminates the whole
585    /// evaluation rather than completing this function normally), and errors
586    /// skip them too; both still restore ambient state and finalize the
587    /// operation.
588    pub(super) fn call_finish(
589        &self,
590        state: CallState,
591        result: Result<Option<KclValueControlFlow>, KclError>,
592        exec_state: &mut ExecState,
593    ) -> Result<Option<KclValueControlFlow>, KclError> {
594        let CallState {
595            prev_inside_stdlib,
596            prev_stdlib_entry_source_range,
597            op,
598            should_track_operation,
599            is_calling_into_stdlib,
600            face_tag_names,
601            pending_region_consumption,
602        } = state;
603        exec_state.mod_local.inside_stdlib = prev_inside_stdlib;
604        exec_state.mod_local.stdlib_entry_source_range = prev_stdlib_entry_source_range;
605        exec_state.mut_stack().pop_env()?;
606
607        if result.is_ok()
608            && let Some(pending_region_consumption) = pending_region_consumption
609        {
610            record_consumed_regions(exec_state, pending_region_consumption);
611        }
612
613        if should_track_operation {
614            if let Some(mut op) = op {
615                op.set_std_lib_call_is_error(result.is_err());
616                // Track call operation.  We do this after the call
617                // since things like patternTransform may call user code
618                // before running, and we will likely want to use the
619                // return value. The call takes ownership of the args,
620                // so we need to build the op before the call.
621                exec_state.push_op(op);
622            } else if !is_calling_into_stdlib {
623                exec_state.push_op(Operation::GroupEnd);
624            }
625        }
626
627        let mut result = match result {
628            Ok(Some(value)) => {
629                if value.is_some_return() {
630                    // `Exit` terminates the whole evaluation rather than completing this
631                    // function normally, so it bypasses return-type validation, including
632                    // the `never` contract.
633                    return Ok(Some(value));
634                } else {
635                    Ok(Some(value.into_value()))
636                }
637            }
638            Ok(None) => Ok(None),
639            Err(e) => Err(e),
640        };
641
642        if self.is_std()
643            && let Ok(Some(result)) = &mut result
644        {
645            update_memory_for_tags_of_geometry(result, exec_state)?;
646            if !face_tag_names.is_empty() {
647                attach_face_tags_to_geometry(result, exec_state, &face_tag_names);
648            }
649        }
650
651        coerce_result_type(result, self, exec_state).map(|r| r.map(KclValue::continue_))
652    }
653}
654
655/// State captured by [`FunctionSource::call_setup`] that
656/// [`FunctionSource::call_finish`] needs to complete the call: the ambient
657/// flags to restore, the deferred operation, and details of what was called.
658#[derive(Debug)]
659pub(super) struct CallState {
660    prev_inside_stdlib: bool,
661    prev_stdlib_entry_source_range: Option<SourceRange>,
662    /// Deferred stdlib-call operation, pushed by call_finish.
663    op: Option<Operation>,
664    should_track_operation: bool,
665    is_calling_into_stdlib: bool,
666    face_tag_names: Vec<String>,
667    pending_region_consumption: Option<PendingRegionConsumption>,
668}
669
670impl FunctionBody {
671    fn prep_mem(&self, exec_state: &mut ExecState) -> Result<(), KclError> {
672        match self {
673            FunctionBody::Rust(_) => exec_state.mut_stack().push_new_root_env(true),
674            FunctionBody::Kcl(memory) => exec_state.mut_stack().push_new_env_for_call(*memory),
675        }
676    }
677}
678
679/// Whether `value` may have come from a legacy (v1) sketch rather than a
680/// sketch block, which is what gates the legacy tag-memory updates in
681/// `update_memory_for_tags_of_geometry`.
682///
683/// Anything that is not a sketch or a solid answers `false`: a number or an
684/// enum variant is not a sketch of either generation, so it must not pull in
685/// legacy behavior. The match stays exhaustive so that adding a `KclValue`
686/// variant forces an explicit answer here instead of inheriting one.
687fn might_be_legacy_sketch(value: &KclValue) -> bool {
688    match value {
689        KclValue::Uuid { .. } => false,
690        KclValue::Bool { .. } => false,
691        KclValue::Number { .. } => false,
692        KclValue::String { .. } => false,
693        KclValue::Enum { .. } => false,
694        KclValue::SketchVar { .. } => false,
695        KclValue::SketchConstraint { .. } => false,
696        KclValue::Tuple { value, .. } => value.iter().any(might_be_legacy_sketch),
697        KclValue::HomArray { value, .. } => value.iter().any(might_be_legacy_sketch),
698        // TODO: sketch block result should return false.
699        KclValue::Object { value, .. } => value.values().any(might_be_legacy_sketch),
700        KclValue::TagIdentifier(_) => false,
701        KclValue::TagDeclarator(_) => false,
702        KclValue::GdtAnnotation { .. } => false,
703        KclValue::CameraView { .. } => false,
704        KclValue::NamedView { .. } => false,
705        KclValue::Plane { .. } => false,
706        KclValue::Face { .. } => false,
707        KclValue::BoundedEdge { .. } => false,
708        KclValue::Segment { .. } => false,
709        KclValue::Sketch { value: sketch } => sketch.origin_sketch_id.is_none(),
710        // A solid with no sketch has no tag container, so the caller returns
711        // early without consulting this answer; `true` keeps it the exact
712        // negation of the previous `originates_from_sketch_block`.
713        KclValue::Solid { value: solid } => solid
714            .sketch()
715            .map(|sketch| sketch.origin_sketch_id.is_none())
716            .unwrap_or(true),
717        KclValue::Helix { .. } => false,
718        KclValue::ImportedGeometry(_) => false,
719        KclValue::Function { .. } => false,
720        KclValue::Module { .. } => false,
721        KclValue::Type { .. } => false,
722        KclValue::KclNone { .. } => false,
723    }
724}
725
726fn face_tag_names_for_call(fn_def: &FunctionSource, args: &Args<Desugared>) -> Vec<String> {
727    let Some(std_props) = &fn_def.std_props else {
728        return Vec::new();
729    };
730
731    if !std_function_allows_face_tags(&std_props.name) {
732        return Vec::new();
733    }
734
735    args.labeled
736        .iter()
737        .filter(|(label, _)| matches!(label.as_str(), "tag" | "tagStart" | "tagEnd"))
738        .filter_map(|(_, arg)| match &arg.value {
739            KclValue::TagDeclarator(tag) => Some(tag.name.clone()),
740            _ => None,
741        })
742        .collect()
743}
744
745fn std_function_allows_face_tags(std_fn_name: &str) -> bool {
746    matches!(
747        std_fn_name,
748        "std::sketch::extrude"
749            | "std::solid::chamfer"
750            | "std::solid::fillet"
751            | "std::sketch::sweep"
752            | "std::sketch::loft"
753            | "std::sketch::revolve"
754    )
755}
756
757fn attach_face_tags_to_geometry(result: &mut KclValue, exec_state: &ExecState, tag_names: &[String]) {
758    match result {
759        KclValue::Solid { value } => attach_face_tags_to_solid(value, exec_state, tag_names),
760        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
761            for v in value {
762                attach_face_tags_to_geometry(v, exec_state, tag_names);
763            }
764        }
765        _ => {}
766    }
767}
768
769fn attach_face_tags_to_solid(solid: &mut Solid, exec_state: &ExecState, tag_names: &[String]) {
770    let surfaces = solid.value.clone();
771    for surface in surfaces {
772        let Some(tag) = surface.get_tag() else {
773            continue;
774        };
775        if !tag_names.iter().any(|tag_name| tag_name == &tag.name) {
776            continue;
777        }
778
779        let tag_id = solid
780            .sketch()
781            .and_then(|sketch| sketch.tags.get(&tag.name))
782            .cloned()
783            .unwrap_or_else(|| {
784                let mut solid_copy = solid.clone();
785                clear_tags_from_solid_copy(&mut solid_copy);
786                TagIdentifier {
787                    value: tag.name.clone(),
788                    info: vec![(
789                        exec_state.stack().current_epoch(),
790                        TagEngineInfo {
791                            id: surface.get_id(),
792                            surface: Some(surface.clone()),
793                            path: None,
794                            geometry: Geometry::Solid(solid_copy),
795                        },
796                    )],
797                    meta: vec![Metadata {
798                        source_range: tag.clone().into(),
799                    }],
800                }
801            });
802
803        match solid.faces.get_mut(&tag.name) {
804            Some(existing_tag) => existing_tag.merge_info(&tag_id),
805            None => {
806                solid.faces.insert(tag.name.clone(), tag_id);
807            }
808        }
809    }
810}
811
812fn clear_tags_from_solid_copy(solid: &mut Solid) {
813    if let Some(sketch) = solid.sketch_mut() {
814        sketch.tags.clear(); // Avoid recursive tags.
815    }
816    solid.faces.clear();
817}
818
819fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut ExecState) -> Result<(), KclError> {
820    let might_be_legacy = might_be_legacy_sketch(&*result);
821    // If the return result is a sketch or solid, we want to update the
822    // memory for the tags of the group.
823    // TODO: This could probably be done in a better way, but as of now this was my only idea
824    // and it works.
825    match result {
826        KclValue::Sketch { value } if might_be_legacy => {
827            for (name, tag) in value.tags.iter() {
828                if exec_state.stack().cur_frame_contains(name)? {
829                    exec_state.mut_stack().update(name, |v, _| {
830                        if let Some(existing_tag) = v.as_mut_tag() {
831                            existing_tag.merge_info(tag);
832                        }
833                    })?;
834                } else {
835                    exec_state.mut_stack().add(
836                        name.to_owned(),
837                        KclValue::TagIdentifier(Box::new(tag.clone())),
838                        SourceRange::default(),
839                    )?;
840                }
841            }
842        }
843        KclValue::Solid { value } => {
844            let surfaces = value.value.clone();
845            if value.sketch_mut().is_none() {
846                // If the solid isn't based on a sketch, then it doesn't have a tag container,
847                // so there's nothing to do here.
848                return Ok(());
849            };
850            // Now that we know there's work to do (because there's a tag container),
851            // run some clones.
852            let solid_copies: Vec<Box<Solid>> = surfaces.iter().map(|_| value.clone()).collect();
853            // Get the tag container. We expect it to always succeed because we already checked
854            // for a tag container above.
855            let Some(sketch) = value.sketch_mut() else {
856                return Ok(());
857            };
858            for (v, mut solid_copy) in surfaces.iter().zip(solid_copies) {
859                clear_tags_from_solid_copy(&mut solid_copy);
860                if let Some(tag) = v.get_tag() {
861                    // Get the past tag and update it.
862                    let mut is_part_of_sketch = false;
863                    let tag_id = if let Some(t) = sketch.tags.get(&tag.name) {
864                        is_part_of_sketch = true;
865                        let mut t = t.clone();
866                        let Some(info) = t.get_cur_info() else {
867                            return Err(KclError::new_internal(KclErrorDetails::new(
868                                format!("Tag {} does not have path info", tag.name),
869                                vec![tag.into()],
870                            )));
871                        };
872
873                        let mut info = info.clone();
874                        info.id = v.get_id();
875                        info.surface = Some(v.clone());
876                        info.geometry = Geometry::Solid(*solid_copy);
877                        t.info.push((exec_state.stack().current_epoch(), info));
878                        t
879                    } else {
880                        // It's probably a fillet or a chamfer.
881                        // Initialize it.
882                        TagIdentifier {
883                            value: tag.name.clone(),
884                            info: vec![(
885                                exec_state.stack().current_epoch(),
886                                TagEngineInfo {
887                                    id: v.get_id(),
888                                    surface: Some(v.clone()),
889                                    path: None,
890                                    geometry: Geometry::Solid(*solid_copy),
891                                },
892                            )],
893                            meta: vec![Metadata {
894                                source_range: tag.clone().into(),
895                            }],
896                        }
897                    };
898
899                    // update the sketch tags.
900                    sketch.merge_tags(Some(&tag_id).into_iter());
901
902                    if exec_state.stack().cur_frame_contains(&tag.name)? {
903                        exec_state.mut_stack().update(&tag.name, |v, _| {
904                            if let Some(existing_tag) = v.as_mut_tag() {
905                                existing_tag.merge_info(&tag_id);
906                            }
907                        })?;
908                    } else if might_be_legacy || !is_part_of_sketch {
909                        // The above condition is saying that we add a tag to
910                        // the stack in either of these cases:
911                        //
912                        // 1. It originates from a legacy sketch v1.
913                        //
914                        // 2. It originates from a sketch block and it's not
915                        // part of the sketch. Instead, it's part of the solid,
916                        // as in tagging a cap face `extrude(tagEnd, tagStart)`
917                        // or chamfer face `chamfer(tag)`.
918                        exec_state.mut_stack().add(
919                            tag.name.clone(),
920                            KclValue::TagIdentifier(Box::new(tag_id)),
921                            SourceRange::default(),
922                        )?;
923                    }
924                }
925            }
926
927            // Find the stale sketch in memory and update it.
928            if let Some(sketch) = value.sketch() {
929                if sketch.tags.is_empty() {
930                    return Ok(());
931                }
932                let sketch_tags: Vec<_> = sketch.tags.values().cloned().collect();
933                let sketches_to_update: Vec<_> = exec_state.stack().find_keys_in_current_env(|v| match v {
934                    KclValue::Sketch { value: sk } => sk.original_id == sketch.original_id,
935                    _ => false,
936                })?;
937
938                for k in sketches_to_update {
939                    exec_state.mut_stack().update(&k, |v, _| {
940                        if let Some(sketch) = v.as_mut_sketch() {
941                            sketch.merge_tags(sketch_tags.iter());
942                        }
943                    })?;
944                }
945            }
946        }
947        KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
948            for v in value {
949                update_memory_for_tags_of_geometry(v, exec_state)?;
950            }
951        }
952        _ => {}
953    }
954    Ok(())
955}
956
957fn type_err_str(expected: &Type, found: &KclValue, source_range: &SourceRange, exec_state: &mut ExecState) -> String {
958    fn strip_backticks(s: &str) -> &str {
959        let mut result = s;
960        if s.starts_with('`') {
961            result = &result[1..]
962        }
963        if s.ends_with('`') {
964            result = &result[..result.len() - 1]
965        }
966        result
967    }
968
969    let expected_human = expected.human_friendly_type();
970    let expected_ty = expected.to_string();
971    let expected_str =
972        if expected_human == expected_ty || expected_human == format!("a value with type `{expected_ty}`") {
973            format!("a value with type `{expected_ty}`")
974        } else {
975            format!("{expected_human} (`{expected_ty}`)")
976        };
977    let found_human = found.human_friendly_type();
978    let found_ty = found.principal_type_string();
979    let found_str = if found_human == found_ty || found_human == format!("a {}", strip_backticks(&found_ty)) {
980        format!("a value with type {found_ty}")
981    } else {
982        format!("{found_human} (with type {found_ty})")
983    };
984
985    let mut result = format!("{expected_str}, but found {found_str}.");
986
987    if found.is_unknown_number() {
988        exec_state.clear_units_warnings(source_range);
989        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`.");
990    }
991
992    result
993}
994
995/// Build the error message for a labeled argument whose label doesn't match any
996/// parameter of the callee. Shared between keyword function calls and sketch
997/// blocks so the wording stays consistent.
998pub(crate) fn unexpected_kw_arg_message(label: &str, callee_name: Option<&str>) -> String {
999    format!(
1000        "`{label}` is not an argument of {}",
1001        callee_name
1002            .map(|n| format!("`{n}`"))
1003            .unwrap_or_else(|| "this function".to_owned()),
1004    )
1005}
1006
1007/// Fetch the definition-time resolution of a type written in a function
1008/// signature.
1009///
1010/// [`FunctionSource::resolve_signature_types`] runs whenever a function
1011/// declaration executes, so a written type without a stored resolution is a
1012/// bug in KCL, not in the user's program.
1013fn resolved_signature_type<'a>(
1014    resolved: Option<&'a RuntimeType>,
1015    written: &Type,
1016    source_range: SourceRange,
1017) -> Result<&'a RuntimeType, KclError> {
1018    resolved.ok_or_else(|| {
1019        KclError::new_internal(KclErrorDetails::new(
1020            format!(
1021                "The type `{written}` in this function's signature was not resolved when the function was declared. This is a bug in KCL and not in your code, please report this to Zoo."
1022            ),
1023            vec![source_range],
1024        ))
1025    })
1026}
1027
1028fn type_check_params_kw(
1029    fn_name: Option<&str>,
1030    fn_def: &FunctionSource,
1031    mut args: Args<Sugary>,
1032    exec_state: &mut ExecState,
1033) -> Result<Args<Desugared>, KclError> {
1034    let fn_name = fn_name.or(args.fn_name.as_deref());
1035    let mut result = Args::new_no_args(
1036        args.source_range,
1037        args.node_path.clone(),
1038        args.ctx,
1039        fn_name.map(|f| f.to_string()).or_else(|| args.fn_name.clone()),
1040    );
1041
1042    // If it's possible the input arg was meant to be labelled and we probably don't want to use
1043    // it as the input arg, then treat it as labelled.
1044    if let Some((Some(label), _)) = args.unlabeled.first()
1045        && args.unlabeled.len() == 1
1046        && (fn_def.input_arg.is_none() || args.pipe_value.is_some())
1047        && fn_def.named_args.iter().any(|p| p.0 == label)
1048        && !args.labeled.contains_key(label)
1049    {
1050        let Some((label, arg)) = args.unlabeled.pop() else {
1051            let message = "Expected unlabeled arg to be present".to_owned();
1052            debug_assert!(false, "{}", &message);
1053            return Err(KclError::new_internal(KclErrorDetails::new(
1054                message,
1055                vec![args.source_range],
1056            )));
1057        };
1058        args.labeled.insert(label.unwrap(), arg);
1059    }
1060
1061    // Apply the `a == a: a` shorthand by desugaring unlabeled args into labeled ones.
1062    let (labeled_unlabeled, unlabeled_unlabeled) = args.unlabeled.into_iter().partition(|(l, _)| {
1063        if let Some(l) = l
1064            && fn_def.named_args.contains_key(l)
1065            && !args.labeled.contains_key(l)
1066        {
1067            true
1068        } else {
1069            false
1070        }
1071    });
1072    args.unlabeled = unlabeled_unlabeled;
1073    for (l, arg) in labeled_unlabeled {
1074        let previous = args.labeled.insert(l.unwrap(), arg);
1075        debug_assert!(previous.is_none());
1076    }
1077
1078    if let Some((name, ty)) = &fn_def.input_arg {
1079        // Expecting an input arg
1080
1081        if args.unlabeled.is_empty() {
1082            // No args provided
1083
1084            if let Some(pipe) = args.pipe_value {
1085                // But there is a pipeline
1086                result.unlabeled = vec![(None, pipe)];
1087            } else if let Some(arg) = args.labeled.swap_remove(name) {
1088                // Mistakenly labelled
1089                exec_state.err(CompilationIssue::err(
1090                    arg.source_range,
1091                    format!(
1092                        "{} expects an unlabeled first argument (`@{name}`), but it is labelled in the call. You might try removing the `{name} = `",
1093                        fn_name
1094                            .map(|n| format!("The function `{n}`"))
1095                            .unwrap_or_else(|| "This function".to_owned()),
1096                    ),
1097                ));
1098                result.unlabeled = vec![(Some(name.clone()), arg)];
1099            } else {
1100                // Just missing
1101                return Err(KclError::new_argument(KclErrorDetails::new(
1102                    "This function expects an unlabeled first parameter, but you haven't passed it one.".to_owned(),
1103                    fn_def.ast.as_source_ranges(),
1104                )));
1105            }
1106        } else if args.unlabeled.len() == 1
1107            && let Some(unlabeled_arg) = args.unlabeled.pop()
1108        {
1109            let mut arg = unlabeled_arg.1;
1110            if let Some(ty) = ty {
1111                let rty = resolved_signature_type(fn_def.resolved_input_ty.as_ref(), ty, arg.source_range)?;
1112                arg.value = arg
1113                    .value
1114                    .coerce(rty, CoercionMode::implicit(), exec_state)
1115                    .map_err(|_| {
1116                        KclError::new_argument(KclErrorDetails::new(
1117                            format!(
1118                                "The input argument of {} requires {}",
1119                                fn_name
1120                                    .map(|n| format!("`{n}`"))
1121                                    .unwrap_or_else(|| "this function".to_owned()),
1122                                type_err_str(ty, &arg.value, &arg.source_range, exec_state),
1123                            ),
1124                            vec![arg.source_range],
1125                        ))
1126                    })?;
1127            }
1128            result.unlabeled = vec![(None, arg)]
1129        } else {
1130            // Multiple unlabelled args
1131
1132            // Try to un-spread args into an array
1133            if let Some(Type::Array { len, .. }) = ty {
1134                if len.satisfied(args.unlabeled.len(), false).is_none() {
1135                    exec_state.err(CompilationIssue::err(
1136                        args.source_range,
1137                        format!(
1138                            "{} expects an array input argument with {} elements",
1139                            fn_name
1140                                .map(|n| format!("The function `{n}`"))
1141                                .unwrap_or_else(|| "This function".to_owned()),
1142                            len.human_friendly_type(),
1143                        ),
1144                    ));
1145                }
1146
1147                let source_range = SourceRange::merge(args.unlabeled.iter().map(|(_, a)| a.source_range));
1148                exec_state.warn_experimental("array input arguments", source_range);
1149                result.unlabeled = vec![(
1150                    None,
1151                    Arg {
1152                        source_range,
1153                        value: KclValue::HomArray {
1154                            value: args.unlabeled.drain(..).map(|(_, a)| a.value).collect(),
1155                            ty: RuntimeType::any(),
1156                        },
1157                    },
1158                )]
1159            }
1160        }
1161    }
1162
1163    // Either we didn't move the arg above, or we're not expecting one.
1164    if !args.unlabeled.is_empty() {
1165        // Not expecting an input arg, but found one or more
1166        let actuals = args.labeled.keys();
1167        let formals: Vec<_> = fn_def
1168            .named_args
1169            .keys()
1170            .filter_map(|name| {
1171                if actuals.clone().any(|a| a == name) {
1172                    return None;
1173                }
1174
1175                Some(format!("`{name}`"))
1176            })
1177            .collect();
1178
1179        let suggestion = if formals.is_empty() {
1180            String::new()
1181        } else {
1182            format!("; suggested labels: {}", formals.join(", "))
1183        };
1184
1185        let mut errors = args.unlabeled.iter().map(|(_, arg)| {
1186            CompilationIssue::err(
1187                arg.source_range,
1188                format!("This argument needs a label, but it doesn't have one{suggestion}"),
1189            )
1190        });
1191
1192        let first = errors.next().unwrap();
1193        errors.for_each(|e| exec_state.err(e));
1194
1195        return Err(KclError::new_argument(first.into()));
1196    }
1197
1198    for (label, mut arg) in args.labeled {
1199        match fn_def.named_args.get(&label) {
1200            Some(NamedParam {
1201                experimental: _,
1202                deprecated: _,
1203                deprecated_since: _,
1204                default_value: def,
1205                ty,
1206                resolved_ty,
1207            }) => {
1208                // For optional args, passing None should be the same as not passing an arg.
1209                if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
1210                    if let Some(ty) = ty {
1211                        let rty = resolved_signature_type(resolved_ty.as_ref(), ty, arg.source_range)?;
1212                        arg.value = arg
1213                                .value
1214                                .coerce(
1215                                    rty,
1216                                    CoercionMode::implicit(),
1217                                    exec_state,
1218                                )
1219                                .map_err(|e| {
1220                                    let mut message = format!(
1221                                        "{label} requires {}",
1222                                        type_err_str(ty, &arg.value, &arg.source_range, exec_state),
1223                                    );
1224                                    if let Some(ty) = e.explicit_coercion {
1225                                        // TODO if we have access to the AST for the argument we could choose which example to suggest.
1226                                        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}`");
1227                                    }
1228                                    KclError::new_argument(KclErrorDetails::new(
1229                                        message,
1230                                        vec![arg.source_range],
1231                                    ))
1232                                })?;
1233                    }
1234                    result.labeled.insert(label, arg);
1235                }
1236            }
1237            None => {
1238                exec_state.err(CompilationIssue::err(
1239                    arg.source_range,
1240                    unexpected_kw_arg_message(&label, fn_name),
1241                ));
1242            }
1243        }
1244    }
1245
1246    let consumed_solid_arg_check = fn_def
1247        .std_props
1248        .as_ref()
1249        .map_or(ConsumedSolidArgCheck::Error, |props| props.consumed_solid_arg_check);
1250    if matches!(fn_def.body, FunctionBody::Rust(_))
1251        && let Some(props) = fn_def.std_props.as_ref()
1252    {
1253        match props.region_behavior.stale_region_policy() {
1254            Some(StaleRegionPolicy::Error) => validate_region_args_not_consumed(&result, exec_state)?,
1255            Some(StaleRegionPolicy::Warning) => {
1256                warn_if_region_args_consumed(&result, exec_state, &props.name)?;
1257            }
1258            None => {}
1259        }
1260    }
1261    match consumed_solid_arg_check {
1262        ConsumedSolidArgCheck::Error => {
1263            result
1264                .unlabeled
1265                .iter()
1266                .map(|(_, arg)| arg)
1267                .chain(result.labeled.values())
1268                .try_for_each(|arg| validate_value_not_consumed(&arg.value, exec_state, arg.source_range))?;
1269        }
1270        ConsumedSolidArgCheck::WarnDeprecated => {
1271            let std_fn_name = fn_def
1272                .std_props
1273                .as_ref()
1274                .map(|props| props.name.as_str())
1275                .unwrap_or("function");
1276            for arg in result
1277                .unlabeled
1278                .iter()
1279                .map(|(_, arg)| arg)
1280                .chain(result.labeled.values())
1281            {
1282                warn_if_value_consumed_for_deprecated_call(&arg.value, exec_state, arg.source_range, std_fn_name)?;
1283            }
1284        }
1285    }
1286
1287    Ok(result)
1288}
1289
1290pub(super) fn assign_args_to_params_kw(
1291    fn_def: &FunctionSource,
1292    args: Args<Desugared>,
1293    exec_state: &mut ExecState,
1294) -> Result<(), KclError> {
1295    // Add the arguments to the memory.  A new call frame should have already
1296    // been created.
1297    let source_ranges = fn_def.ast.as_source_ranges();
1298
1299    for (name, param) in fn_def.named_args.iter() {
1300        let arg = args.labeled.get(name);
1301        match arg {
1302            Some(arg) => {
1303                exec_state.mut_stack().add(
1304                    name.clone(),
1305                    arg.value.clone(),
1306                    arg.source_ranges().pop().unwrap_or(SourceRange::synthetic()),
1307                )?;
1308            }
1309            None => match &param.default_value {
1310                Some(default_val) => {
1311                    let value = KclValue::from_default_param(default_val.clone(), exec_state);
1312                    exec_state
1313                        .mut_stack()
1314                        .add(name.clone(), value, default_val.source_range())?;
1315                }
1316                None => {
1317                    return Err(KclError::new_argument(KclErrorDetails::new(
1318                        format!("This function requires a parameter {name}, but you haven't passed it one."),
1319                        source_ranges,
1320                    )));
1321                }
1322            },
1323        }
1324    }
1325
1326    if let Some((param_name, _)) = &fn_def.input_arg {
1327        let Some(unlabeled) = args.unlabeled_kw_arg_unconverted() else {
1328            debug_assert!(false, "Bad args");
1329            return Err(KclError::new_internal(KclErrorDetails::new(
1330                "Desugared arguments are inconsistent".to_owned(),
1331                source_ranges,
1332            )));
1333        };
1334        exec_state.mut_stack().add(
1335            param_name.clone(),
1336            unlabeled.value.clone(),
1337            unlabeled.source_ranges().pop().unwrap_or(SourceRange::synthetic()),
1338        )?;
1339    }
1340
1341    Ok(())
1342}
1343
1344fn coerce_result_type(
1345    result: Result<Option<KclValue>, KclError>,
1346    fn_def: &FunctionSource,
1347    exec_state: &mut ExecState,
1348) -> Result<Option<KclValue>, KclError> {
1349    let result = result?;
1350
1351    let Some(ret_ty) = &fn_def.return_type else {
1352        return Ok(result);
1353    };
1354
1355    let ty = resolved_signature_type(
1356        fn_def.resolved_return_ty.as_ref(),
1357        &ret_ty.inner,
1358        ret_ty.as_source_range(),
1359    )?;
1360
1361    // `never` describes the absence of normal completion, so either successful
1362    // result shape violates the function's declared contract.
1363    if ty.subtype(&RuntimeType::never()) {
1364        let message = if result.is_some() {
1365            "This function returned a value, but its return type is `never`. A function with return type `never` must stop evaluation abnormally. You may want to use `fail(...)` to stop evaluation and provide a message."
1366        } else {
1367            "This function completed without returning a value, but its return type is `never`. A function with return type `never` must stop evaluation abnormally. You may want to use `fail(...)` to stop evaluation and provide a message."
1368        };
1369        return Err(KclError::new_type(KclErrorDetails::new(
1370            message.to_owned(),
1371            ret_ty.as_source_ranges(),
1372        )));
1373    }
1374
1375    let Some(val) = result else {
1376        return Ok(None);
1377    };
1378
1379    let val = val.coerce(ty, CoercionMode::implicit(), exec_state).map_err(|_| {
1380        KclError::new_type(KclErrorDetails::new(
1381            format!(
1382                "This function requires its result to be {}",
1383                type_err_str(ret_ty, &val, &(&val).into(), exec_state)
1384            ),
1385            ret_ty.as_source_ranges(),
1386        ))
1387    })?;
1388    Ok(Some(val))
1389}
1390
1391#[cfg(test)]
1392mod test {
1393    use std::sync::Arc;
1394
1395    use super::*;
1396    use crate::engine::engine_manager::EngineManager;
1397    use crate::errors::Severity;
1398    use crate::execution::ContextType;
1399    use crate::execution::EnvironmentRef;
1400    use crate::execution::ExecTestResults;
1401    use crate::execution::memory::Stack;
1402    use crate::execution::parse_execute;
1403    use crate::execution::types::NumericType;
1404    use crate::execution::types::NumericTypeExt;
1405    use crate::parsing::ast::types::DefaultParamVal;
1406    use crate::parsing::ast::types::FunctionExpression;
1407    use crate::parsing::ast::types::Identifier;
1408    use crate::parsing::ast::types::Parameter;
1409    use crate::parsing::ast::types::Program;
1410
1411    fn source_texts<'a>(program: &'a str, error: &KclError) -> Vec<&'a str> {
1412        error
1413            .source_ranges()
1414            .into_iter()
1415            .map(|range| &program[range.start()..range.end()])
1416            .collect()
1417    }
1418
1419    fn get_var(result: &ExecTestResults, name: &str) -> KclValue {
1420        result
1421            .exec_state
1422            .stack()
1423            .memory
1424            .get_from_owned(name, result.mem_env, SourceRange::default(), 0)
1425            .unwrap_or_else(|err| panic!("expected variable `{name}` to exist: {err:?}"))
1426    }
1427
1428    fn var_exists(result: &ExecTestResults, name: &str) -> bool {
1429        result
1430            .exec_state
1431            .stack()
1432            .memory
1433            .get_from_owned(name, result.mem_env, SourceRange::default(), 0)
1434            .is_ok()
1435    }
1436
1437    fn assert_vars_are_tags(result: &ExecTestResults, names: &[&str]) {
1438        for name in names {
1439            assert!(
1440                matches!(get_var(result, name), KclValue::TagIdentifier(_)),
1441                "expected variable `{name}` to be a tag identifier"
1442            );
1443        }
1444    }
1445
1446    fn assert_vars_are_missing(result: &ExecTestResults, names: &[&str]) {
1447        for name in names {
1448            assert!(!var_exists(result, name), "expected variable `{name}` to be absent");
1449        }
1450    }
1451
1452    fn assert_body_face_tags(result: &ExecTestResults, expected: &[&str], unexpected: &[&str]) {
1453        let body = get_var(result, "body");
1454        let KclValue::Solid { value: body } = body else {
1455            panic!("expected `body` to be a solid");
1456        };
1457
1458        for tag in expected {
1459            assert!(body.faces.contains_key(*tag), "expected body.faces to contain `{tag}`");
1460        }
1461
1462        for tag in unexpected {
1463            assert!(
1464                !body.faces.contains_key(*tag),
1465                "expected body.faces not to contain sketch tag `{tag}`"
1466            );
1467        }
1468    }
1469
1470    fn deprecated_solid_tag_access_warnings(result: &ExecTestResults) -> Vec<&CompilationIssue> {
1471        result
1472            .exec_state
1473            .issues()
1474            .iter()
1475            .filter(|issue| issue.message.contains("Accessing solid-created face"))
1476            .collect()
1477    }
1478
1479    #[tokio::test(flavor = "multi_thread")]
1480    async fn test_assign_args_to_params() {
1481        // Set up a little framework for this test.
1482        fn mem(number: usize) -> KclValue {
1483            KclValue::Number {
1484                value: number as f64,
1485                ty: NumericType::count(),
1486                meta: Default::default(),
1487            }
1488        }
1489        fn ident(s: &'static str) -> Node<Identifier> {
1490            Node::no_src(Identifier {
1491                name: s.to_owned(),
1492                digest: None,
1493            })
1494        }
1495        fn opt_param(s: &'static str) -> Parameter {
1496            Parameter {
1497                experimental: false,
1498                deprecated: false,
1499                deprecated_since: None,
1500                identifier: ident(s),
1501                param_type: None,
1502                default_value: Some(DefaultParamVal::none()),
1503                labeled: true,
1504                digest: None,
1505            }
1506        }
1507        fn req_param(s: &'static str) -> Parameter {
1508            Parameter {
1509                experimental: false,
1510                deprecated: false,
1511                deprecated_since: None,
1512                identifier: ident(s),
1513                param_type: None,
1514                default_value: None,
1515                labeled: true,
1516                digest: None,
1517            }
1518        }
1519        fn additional_program_memory(items: &[(String, KclValue)]) -> Stack {
1520            let mut program_memory = Stack::new_for_tests();
1521            for (name, item) in items {
1522                program_memory
1523                    .add(name.clone(), item.clone(), SourceRange::default())
1524                    .unwrap();
1525            }
1526            program_memory
1527        }
1528        // Declare the test cases.
1529        for (test_name, params, args, expected) in [
1530            ("empty", Vec::new(), Vec::new(), Ok(additional_program_memory(&[]))),
1531            (
1532                "all params required, and all given, should be OK",
1533                vec![req_param("x")],
1534                vec![("x", mem(1))],
1535                Ok(additional_program_memory(&[("x".to_owned(), mem(1))])),
1536            ),
1537            (
1538                "all params required, none given, should error",
1539                vec![req_param("x")],
1540                vec![],
1541                Err(KclError::new_argument(KclErrorDetails::new(
1542                    "This function requires a parameter x, but you haven't passed it one.".to_owned(),
1543                    vec![SourceRange::default()],
1544                ))),
1545            ),
1546            (
1547                "all params optional, none given, should be OK",
1548                vec![opt_param("x")],
1549                vec![],
1550                Ok(additional_program_memory(&[("x".to_owned(), KclValue::none())])),
1551            ),
1552            (
1553                "mixed params, too few given",
1554                vec![req_param("x"), opt_param("y")],
1555                vec![],
1556                Err(KclError::new_argument(KclErrorDetails::new(
1557                    "This function requires a parameter x, but you haven't passed it one.".to_owned(),
1558                    vec![SourceRange::default()],
1559                ))),
1560            ),
1561            (
1562                "mixed params, minimum given, should be OK",
1563                vec![req_param("x"), opt_param("y")],
1564                vec![("x", mem(1))],
1565                Ok(additional_program_memory(&[
1566                    ("x".to_owned(), mem(1)),
1567                    ("y".to_owned(), KclValue::none()),
1568                ])),
1569            ),
1570            (
1571                "mixed params, maximum given, should be OK",
1572                vec![req_param("x"), opt_param("y")],
1573                vec![("x", mem(1)), ("y", mem(2))],
1574                Ok(additional_program_memory(&[
1575                    ("x".to_owned(), mem(1)),
1576                    ("y".to_owned(), mem(2)),
1577                ])),
1578            ),
1579        ] {
1580            // Run each test.
1581            let func_expr = Node::no_src(FunctionExpression {
1582                name: None,
1583                params,
1584                body: Program::empty(),
1585                return_type: None,
1586                digest: None,
1587            });
1588            let func_src = FunctionSource::kcl(
1589                crate::parsing::ast::types::BoxNode::new(func_expr),
1590                EnvironmentRef::dummy(),
1591                crate::execution::kcl_value::KclFunctionSourceParams {
1592                    std_props: None,
1593                    experimental: false,
1594                    include_in_feature_tree: false,
1595                },
1596            );
1597            let labeled = args
1598                .iter()
1599                .map(|(name, value)| {
1600                    let arg = Arg::new(value.clone(), SourceRange::default());
1601                    ((*name).to_owned(), arg)
1602                })
1603                .collect::<IndexMap<_, _>>();
1604            let exec_ctxt = ExecutorContext {
1605                engine: Arc::new(EngineManager::new_mock()),
1606                engine_batch: crate::engine::EngineBatchContext::default(),
1607                fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
1608                settings: Default::default(),
1609                context_type: ContextType::Mock,
1610                execution_callbacks: Default::default(),
1611                executor_kind: crate::execution::machine::ExecutorKind::resolve(),
1612                machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
1613            };
1614            let mut exec_state = ExecState::new(&exec_ctxt);
1615            exec_state.mod_local.stack = Stack::new_for_tests();
1616
1617            let args = Args {
1618                fn_name: Some("test".to_owned()),
1619                labeled,
1620                unlabeled: Vec::new(),
1621                source_range: SourceRange::default(),
1622                node_path: None,
1623                ctx: exec_ctxt,
1624                pipe_value: None,
1625                _status: std::marker::PhantomData,
1626            };
1627
1628            let actual = assign_args_to_params_kw(&func_src, args, &mut exec_state).map(|_| exec_state.mod_local.stack);
1629            assert_eq!(
1630                actual, expected,
1631                "failed test '{test_name}':\ngot {actual:?}\nbut expected\n{expected:?}"
1632            );
1633        }
1634    }
1635
1636    #[tokio::test(flavor = "multi_thread")]
1637    async fn type_check_user_args() {
1638        let program = r#"fn makeMessage(prefix: string, suffix: string) {
1639  return prefix + suffix
1640}
1641
1642msg1 = makeMessage(prefix = "world", suffix = " hello")
1643msg2 = makeMessage(prefix = 1, suffix = 3)"#;
1644        let err = parse_execute(program).await.unwrap_err();
1645        assert_eq!(
1646            err.message(),
1647            "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`."
1648        )
1649    }
1650
1651    #[tokio::test(flavor = "multi_thread")]
1652    async fn never_function_cannot_return_a_value() {
1653        let program = r#"@settings(experimentalFeatures = allow)
1654fn bad(): never {
1655  return 42
1656}
1657
1658bad()
1659"#;
1660        let err = parse_execute(program).await.unwrap_err();
1661
1662        assert!(matches!(&err, KclError::Type { .. }));
1663        assert_eq!(
1664            err.message(),
1665            "This function returned a value, but its return type is `never`. A function with return type `never` must stop evaluation abnormally. You may want to use `fail(...)` to stop evaluation and provide a message."
1666        );
1667    }
1668
1669    #[tokio::test(flavor = "multi_thread")]
1670    async fn never_function_cannot_fall_through() {
1671        let program = r#"@settings(experimentalFeatures = allow)
1672fn alsoBad(): never {
1673  x = 42
1674}
1675
1676alsoBad()
1677"#;
1678        let err = parse_execute(program).await.unwrap_err();
1679
1680        assert!(matches!(&err, KclError::Type { .. }));
1681        assert_eq!(
1682            err.message(),
1683            "This function completed without returning a value, but its return type is `never`. A function with return type `never` must stop evaluation abnormally. You may want to use `fail(...)` to stop evaluation and provide a message."
1684        );
1685    }
1686
1687    #[tokio::test(flavor = "multi_thread")]
1688    async fn never_union_function_cannot_return_a_value() {
1689        let program = r#"@settings(experimentalFeatures = allow)
1690fn bad(): never | never {
1691  return 42
1692}
1693
1694bad()
1695"#;
1696        let err = parse_execute(program).await.unwrap_err();
1697
1698        assert!(matches!(&err, KclError::Type { .. }));
1699        assert_eq!(
1700            err.message(),
1701            "This function returned a value, but its return type is `never`. A function with return type `never` must stop evaluation abnormally. You may want to use `fail(...)` to stop evaluation and provide a message."
1702        );
1703    }
1704
1705    #[tokio::test(flavor = "multi_thread")]
1706    async fn never_union_function_cannot_fall_through() {
1707        let program = r#"@settings(experimentalFeatures = allow)
1708fn alsoBad(): never | never {
1709  x = 42
1710}
1711
1712alsoBad()
1713"#;
1714        let err = parse_execute(program).await.unwrap_err();
1715
1716        assert!(matches!(&err, KclError::Type { .. }));
1717        assert_eq!(
1718            err.message(),
1719            "This function completed without returning a value, but its return type is `never`. A function with return type `never` must stop evaluation abnormally. You may want to use `fail(...)` to stop evaluation and provide a message."
1720        );
1721    }
1722
1723    #[tokio::test(flavor = "multi_thread")]
1724    async fn never_function_contract_is_path_dependent() {
1725        let function = r#"@settings(experimentalFeatures = allow)
1726fn failOrReturn(@shouldFail: bool): never {
1727  return if shouldFail {
1728    fail("requested failure")
1729  } else {
1730    42
1731  }
1732}
1733"#;
1734
1735        let err = parse_execute(&format!("{function}\nfailOrReturn(true)\n"))
1736            .await
1737            .unwrap_err();
1738        assert!(matches!(&err, KclError::UserDefined { .. }));
1739        assert_eq!(err.message(), "requested failure");
1740
1741        let err = parse_execute(&format!("{function}\nfailOrReturn(false)\n"))
1742            .await
1743            .unwrap_err();
1744        assert!(matches!(&err, KclError::Type { .. }));
1745        assert_eq!(
1746            err.message(),
1747            "This function returned a value, but its return type is `never`. A function with return type `never` must stop evaluation abnormally. You may want to use `fail(...)` to stop evaluation and provide a message."
1748        );
1749    }
1750
1751    #[tokio::test(flavor = "multi_thread")]
1752    async fn never_type_alias_contract_is_path_dependent() {
1753        let function = r#"@settings(experimentalFeatures = allow)
1754type impossible = never
1755fn failOrReturn(@shouldFail: bool): impossible {
1756  return if shouldFail {
1757    fail("requested failure")
1758  } else {
1759    42
1760  }
1761}
1762"#;
1763
1764        let err = parse_execute(&format!("{function}\nfailOrReturn(true)\n"))
1765            .await
1766            .unwrap_err();
1767        assert!(matches!(&err, KclError::UserDefined { .. }));
1768        assert_eq!(err.message(), "requested failure");
1769
1770        let err = parse_execute(&format!("{function}\nfailOrReturn(false)\n"))
1771            .await
1772            .unwrap_err();
1773        assert!(matches!(&err, KclError::Type { .. }));
1774        assert_eq!(
1775            err.message(),
1776            "This function returned a value, but its return type is `never`. A function with return type `never` must stop evaluation abnormally. You may want to use `fail(...)` to stop evaluation and provide a message."
1777        );
1778    }
1779
1780    #[tokio::test(flavor = "multi_thread")]
1781    async fn union_with_never_can_return_a_value_or_fail() {
1782        let function = r#"@settings(experimentalFeatures = allow)
1783fn stringOrFail(@shouldFail: bool): string | never {
1784  return if shouldFail {
1785    fail("requested failure")
1786  } else {
1787    "ok"
1788  }
1789}
1790"#;
1791
1792        let result = parse_execute(&format!("{function}\nresult = stringOrFail(false)\n"))
1793            .await
1794            .unwrap();
1795        let KclValue::String { value, .. } = get_var(&result, "result") else {
1796            panic!("expected `result` to be a string")
1797        };
1798        assert_eq!(value, "ok");
1799
1800        let err = parse_execute(&format!("{function}\nstringOrFail(true)\n"))
1801            .await
1802            .unwrap_err();
1803        assert!(matches!(&err, KclError::UserDefined { .. }));
1804        assert_eq!(err.message(), "requested failure");
1805    }
1806
1807    #[tokio::test(flavor = "multi_thread")]
1808    async fn fail_reports_user_defined_message_and_callsite_once() {
1809        let program = r#"@settings(experimentalFeatures = allow)
1810fail("custom failure")
1811"#;
1812
1813        let err = parse_execute(program).await.unwrap_err();
1814
1815        assert!(matches!(&err, KclError::UserDefined { .. }));
1816        assert_eq!(err.message(), "custom failure");
1817        assert_eq!(err.get_message(), "user-defined: custom failure");
1818        assert_eq!(serde_json::to_value(&err).unwrap()["kind"], "user_defined");
1819        assert_eq!(source_texts(program, &err), [r#"fail("custom failure")"#]);
1820        assert_eq!(err.backtrace().len(), 1);
1821    }
1822
1823    #[tokio::test(flavor = "multi_thread")]
1824    async fn fail_unwinds_through_nested_never_functions_once() {
1825        let program = r#"@settings(experimentalFeatures = allow)
1826fn inner(): never {
1827  fail("nested failure")
1828}
1829
1830fn outer(): never {
1831  inner()
1832}
1833
1834outer()
1835"#;
1836
1837        let err = parse_execute(program).await.unwrap_err();
1838
1839        assert!(matches!(&err, KclError::UserDefined { .. }));
1840        assert_eq!(err.message(), "nested failure");
1841        assert_eq!(
1842            source_texts(program, &err),
1843            [r#"fail("nested failure")"#, "inner()", "outer()"]
1844        );
1845        assert_eq!(
1846            err.backtrace()
1847                .iter()
1848                .map(|item| item.fn_name.as_deref())
1849                .collect::<Vec<_>>(),
1850            [Some("inner"), Some("outer"), None]
1851        );
1852    }
1853
1854    #[tokio::test(flavor = "multi_thread")]
1855    async fn fail_is_valid_in_a_function_with_a_value_return_type() {
1856        let function = r#"@settings(experimentalFeatures = allow)
1857fn valueOrFail(@shouldFail: bool): number {
1858  return if shouldFail {
1859    fail("no value")
1860  } else {
1861    42
1862  }
1863}
1864"#;
1865
1866        parse_execute(&format!("{function}\nresult = valueOrFail(false)\n"))
1867            .await
1868            .unwrap();
1869
1870        let err = parse_execute(&format!("{function}\nvalueOrFail(true)\n"))
1871            .await
1872            .unwrap_err();
1873        assert!(matches!(&err, KclError::UserDefined { .. }));
1874        assert_eq!(err.message(), "no value");
1875    }
1876
1877    #[tokio::test(flavor = "multi_thread")]
1878    async fn never_function_with_fail_or_fallthrough_is_path_dependent() {
1879        let function = r#"@settings(experimentalFeatures = allow)
1880fn failOrFallThrough(@shouldFail: bool): never {
1881  result = if shouldFail {
1882    fail("requested failure")
1883  } else {
1884    42
1885  }
1886}
1887"#;
1888
1889        let err = parse_execute(&format!("{function}\nfailOrFallThrough(true)\n"))
1890            .await
1891            .unwrap_err();
1892        assert!(matches!(&err, KclError::UserDefined { .. }));
1893        assert_eq!(err.message(), "requested failure");
1894
1895        let err = parse_execute(&format!("{function}\nfailOrFallThrough(false)\n"))
1896            .await
1897            .unwrap_err();
1898        assert!(matches!(&err, KclError::Type { .. }));
1899        assert_eq!(
1900            err.message(),
1901            "This function completed without returning a value, but its return type is `never`. A function with return type `never` must stop evaluation abnormally. You may want to use `fail(...)` to stop evaluation and provide a message."
1902        );
1903    }
1904
1905    #[tokio::test(flavor = "multi_thread")]
1906    async fn fail_argument_evaluation_errors_take_precedence() {
1907        let program = r#"@settings(experimentalFeatures = allow)
1908fn stop(): never {
1909  fail(missingMessage)
1910}
1911
1912stop()
1913"#;
1914
1915        let err = parse_execute(program).await.unwrap_err();
1916
1917        assert!(matches!(&err, KclError::UndefinedValue { .. }));
1918        assert_eq!(err.message(), "`missingMessage` is not defined");
1919        assert_eq!(source_texts(program, &err), ["missingMessage", "stop()"]);
1920    }
1921
1922    #[tokio::test(flavor = "multi_thread")]
1923    async fn fail_rejects_invalid_message_arguments_before_invocation() {
1924        for program in [
1925            "@settings(experimentalFeatures = allow)\nfail()\n",
1926            "@settings(experimentalFeatures = allow)\nfail(42)\n",
1927        ] {
1928            let err = parse_execute(program).await.unwrap_err();
1929            assert!(matches!(&err, KclError::Argument { .. }), "{err:?}");
1930        }
1931    }
1932
1933    #[tokio::test(flavor = "multi_thread")]
1934    async fn map_closure_error_mentions_fn_name() {
1935        let program = r#"
1936arr = ["hello"]
1937map(array = arr, f = fn(@item: number) { return item })
1938"#;
1939        let err = parse_execute(program).await.unwrap_err();
1940        assert!(
1941            err.message().contains("map closure"),
1942            "expected map closure errors to include the closure name, got: {}",
1943            err.message()
1944        );
1945    }
1946
1947    #[tokio::test(flavor = "multi_thread")]
1948    async fn array_input_arg() {
1949        let ast = r#"fn f(@input: [mm]) { return 1 }
1950f([1, 2, 3])
1951f(1, 2, 3)
1952"#;
1953        parse_execute(ast).await.unwrap();
1954    }
1955
1956    #[tokio::test(flavor = "multi_thread")]
1957    async fn extrude_tagged_body_gets_face_tags_and_keeps_legacy_bindings() {
1958        let program = r#"@settings(kclVersion = 2.0)
1959profile = sketch(on = XY) {
1960  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
1961  line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
1962  line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
1963  line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
1964  coincident([line1.end, line2.start])
1965  coincident([line2.end, line3.start])
1966  coincident([line3.end, line4.start])
1967  coincident([line4.end, line1.start])
1968}
1969region1 = region(point = [5mm, 5mm], sketch = profile)
1970
1971body = extrude(region1, length = 5mm, tagStart = $bottom, tagEnd = $top)
1972bottomFromBody = body.faces.bottom
1973topFromBody = body.faces.top
1974lineFromSketch = region1.tags.line1
1975legacyBottom = bottom
1976legacyTop = top
1977"#;
1978
1979        let result = parse_execute(program).await.unwrap();
1980        assert_body_face_tags(&result, &["bottom", "top"], &["line1"]);
1981        assert_vars_are_tags(
1982            &result,
1983            &[
1984                "bottom",
1985                "top",
1986                "bottomFromBody",
1987                "topFromBody",
1988                "lineFromSketch",
1989                "legacyBottom",
1990                "legacyTop",
1991            ],
1992        );
1993        assert_vars_are_missing(&result, &["line1"]);
1994    }
1995
1996    #[tokio::test(flavor = "multi_thread")]
1997    async fn extrude_without_tag_arguments_does_not_get_face_tags() {
1998        let program = r#"@settings(kclVersion = 2.0)
1999profile = sketch(on = XY) {
2000  line1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2001  line2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
2002  line3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
2003  line4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
2004  coincident([line1.end, line2.start])
2005  coincident([line2.end, line3.start])
2006  coincident([line3.end, line4.start])
2007  coincident([line4.end, line1.start])
2008}
2009region1 = region(point = [5mm, 5mm], sketch = profile)
2010
2011body = extrude(region1, length = 5mm)
2012"#;
2013
2014        let result = parse_execute(program).await.unwrap();
2015        let body = get_var(&result, "body");
2016        let KclValue::Solid { value: body } = body else {
2017            panic!("expected `body` to be a solid");
2018        };
2019
2020        assert!(
2021            body.faces.is_empty(),
2022            "body faces should only be populated for tagged calls"
2023        );
2024    }
2025
2026    #[tokio::test(flavor = "multi_thread")]
2027    async fn revolve_tagged_body_gets_face_tags() {
2028        let program = r#"@settings(kclVersion = 2.0)
2029profile = sketch(on = XY) {
2030  side = line(start = [var 5mm, var 0mm], end = [var 5mm, var 10mm])
2031  line2 = line(start = [var 5mm, var 10mm], end = [var 6mm, var 10mm])
2032  line3 = line(start = [var 6mm, var 10mm], end = [var 6mm, var 0mm])
2033  line4 = line(start = [var 6mm, var 0mm], end = [var 5mm, var 0mm])
2034  coincident([side.end, line2.start])
2035  coincident([line2.end, line3.start])
2036  coincident([line3.end, line4.start])
2037  coincident([line4.end, side.start])
2038}
2039region1 = region(point = [5.5mm, 5mm], sketch = profile)
2040
2041body = revolve(region1, axis = Y, angle = 90deg, tagStart = $startCap, tagEnd = $endCap)
2042startFromBody = body.faces.startCap
2043endFromBody = body.faces.endCap
2044sideFromSketch = region1.tags.side
2045legacyStart = startCap
2046legacyEnd = endCap
2047"#;
2048
2049        let result = parse_execute(program).await.unwrap();
2050        assert_body_face_tags(&result, &["startCap", "endCap"], &["side"]);
2051        assert_vars_are_tags(
2052            &result,
2053            &[
2054                "startCap",
2055                "endCap",
2056                "startFromBody",
2057                "endFromBody",
2058                "sideFromSketch",
2059                "legacyStart",
2060                "legacyEnd",
2061            ],
2062        );
2063        assert_vars_are_missing(&result, &["side"]);
2064    }
2065
2066    #[tokio::test(flavor = "multi_thread")]
2067    async fn sweep_tagged_body_gets_face_tags() {
2068        let program = r#"@settings(kclVersion = 2.0)
2069profile = sketch(on = XZ) {
2070  edge1 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 0mm])
2071  edge2 = line(start = [var 2mm, var 0mm], end = [var 2mm, var 2mm])
2072  edge3 = line(start = [var 2mm, var 2mm], end = [var 0mm, var 2mm])
2073  edge4 = line(start = [var 0mm, var 2mm], end = [var 0mm, var 0mm])
2074  coincident([edge1.end, edge2.start])
2075  coincident([edge2.end, edge3.start])
2076  coincident([edge3.end, edge4.start])
2077  coincident([edge4.end, edge1.start])
2078}
2079profileRegion = region(point = [1mm, 1mm], sketch = profile)
2080
2081pathSketch = sketch(on = offsetPlane(YZ, offset = -2mm)) {
2082  pathLine = line(start = [var 0mm, var 0mm], end = [var 0mm, var 5mm])
2083}
2084
2085body = sweep(profileRegion, path = pathSketch.pathLine, tagStart = $startCap, tagEnd = $endCap)
2086startFromBody = body.faces.startCap
2087endFromBody = body.faces.endCap
2088edgeFromSketch = profileRegion.tags.edge1
2089pathFromSketch = pathSketch.pathLine
2090legacyStart = startCap
2091legacyEnd = endCap
2092"#;
2093
2094        let result = parse_execute(program).await.unwrap();
2095        assert_body_face_tags(&result, &["startCap", "endCap"], &["edge1", "pathLine"]);
2096        assert_vars_are_tags(
2097            &result,
2098            &[
2099                "startCap",
2100                "endCap",
2101                "startFromBody",
2102                "endFromBody",
2103                "edgeFromSketch",
2104                "legacyStart",
2105                "legacyEnd",
2106            ],
2107        );
2108        assert_vars_are_missing(&result, &["edge1", "pathLine"]);
2109    }
2110
2111    #[tokio::test(flavor = "multi_thread")]
2112    async fn loft_tagged_body_gets_face_tags() {
2113        let program = r#"@settings(kclVersion = 2.0)
2114lowerProfile = sketch(on = XY) {
2115  edge1 = line(start = [var 0mm, var 0mm], end = [var 6mm, var 0mm])
2116  edge2 = line(start = [var 6mm, var 0mm], end = [var 6mm, var 4mm])
2117  edge3 = line(start = [var 6mm, var 4mm], end = [var 0mm, var 4mm])
2118  edge4 = line(start = [var 0mm, var 4mm], end = [var 0mm, var 0mm])
2119  coincident([edge1.end, edge2.start])
2120  coincident([edge2.end, edge3.start])
2121  coincident([edge3.end, edge4.start])
2122  coincident([edge4.end, edge1.start])
2123}
2124lowerRegion = region(point = [3mm, 2mm], sketch = lowerProfile)
2125
2126upperProfile = sketch(on = offsetPlane(XY, offset = 8mm)) {
2127  edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
2128  edge6 = line(start = [var 5mm, var 1mm], end = [var 4mm, var 3mm])
2129  edge7 = line(start = [var 4mm, var 3mm], end = [var 2mm, var 3mm])
2130  edge8 = line(start = [var 2mm, var 3mm], end = [var 1mm, var 1mm])
2131  coincident([edge5.end, edge6.start])
2132  coincident([edge6.end, edge7.start])
2133  coincident([edge7.end, edge8.start])
2134  coincident([edge8.end, edge5.start])
2135}
2136upperRegion = region(point = [3mm, 2mm], sketch = upperProfile)
2137
2138body = loft([lowerRegion, upperRegion], tagStart = $startCap, tagEnd = $endCap)
2139startFromBody = body.faces.startCap
2140endFromBody = body.faces.endCap
2141edgeFromSketch = lowerRegion.tags.edge1
2142legacyStart = startCap
2143legacyEnd = endCap
2144"#;
2145
2146        let result = parse_execute(program).await.unwrap();
2147        assert_body_face_tags(&result, &["startCap", "endCap"], &["edge1"]);
2148        assert_vars_are_tags(
2149            &result,
2150            &[
2151                "startCap",
2152                "endCap",
2153                "startFromBody",
2154                "endFromBody",
2155                "edgeFromSketch",
2156                "legacyStart",
2157                "legacyEnd",
2158            ],
2159        );
2160        assert_vars_are_missing(&result, &["edge1"]);
2161    }
2162
2163    #[tokio::test(flavor = "multi_thread")]
2164    async fn chamfer_tagged_body_gets_face_tags() {
2165        let program = r#"@settings(kclVersion = 2.0)
2166profile = sketch(on = XY) {
2167  edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2168  edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
2169  edge3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
2170  edge4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
2171  coincident([edge1.end, edge2.start])
2172  coincident([edge2.end, edge3.start])
2173  coincident([edge3.end, edge4.start])
2174  coincident([edge4.end, edge1.start])
2175}
2176profileRegion = region(point = [5mm, 5mm], sketch = profile)
2177
2178base = extrude(profileRegion, length = 5mm, tagEnd = $top)
2179body = chamfer(base, tags = getCommonEdge(faces = [profileRegion.tags.edge1, top]), length = 1mm, tag = $chamferFace)
2180chamferFromBody = body.faces.chamferFace
2181topFromBody = body.faces.top
2182edgeFromSketch = profileRegion.tags.edge1
2183legacyChamfer = chamferFace
2184legacyTop = top
2185"#;
2186
2187        let result = parse_execute(program).await.unwrap();
2188        assert_body_face_tags(&result, &["top", "chamferFace"], &["edge1"]);
2189        assert_vars_are_tags(
2190            &result,
2191            &[
2192                "top",
2193                "chamferFace",
2194                "chamferFromBody",
2195                "topFromBody",
2196                "edgeFromSketch",
2197                "legacyChamfer",
2198                "legacyTop",
2199            ],
2200        );
2201        assert_vars_are_missing(&result, &["edge1"]);
2202    }
2203
2204    #[tokio::test(flavor = "multi_thread")]
2205    async fn fillet_tagged_body_gets_face_tags() {
2206        let program = r#"@settings(kclVersion = 2.0)
2207profile = sketch(on = XY) {
2208  edge1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
2209  edge2 = line(start = [var 10mm, var 0mm], end = [var 10mm, var 10mm])
2210  edge3 = line(start = [var 10mm, var 10mm], end = [var 0mm, var 10mm])
2211  edge4 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
2212  coincident([edge1.end, edge2.start])
2213  coincident([edge2.end, edge3.start])
2214  coincident([edge3.end, edge4.start])
2215  coincident([edge4.end, edge1.start])
2216}
2217profileRegion = region(point = [5mm, 5mm], sketch = profile)
2218
2219base = extrude(profileRegion, length = 5mm, tagEnd = $top)
2220body = fillet(base, tags = getCommonEdge(faces = [profileRegion.tags.edge1, top]), radius = 1mm, tag = $filletFace)
2221filletFromBody = body.faces.filletFace
2222topFromBody = body.faces.top
2223edgeFromSketch = profileRegion.tags.edge1
2224legacyFillet = filletFace
2225legacyTop = top
2226"#;
2227
2228        let result = parse_execute(program).await.unwrap();
2229        assert_body_face_tags(&result, &["top", "filletFace"], &["edge1"]);
2230        assert_vars_are_tags(
2231            &result,
2232            &[
2233                "top",
2234                "filletFace",
2235                "filletFromBody",
2236                "topFromBody",
2237                "edgeFromSketch",
2238                "legacyFillet",
2239                "legacyTop",
2240            ],
2241        );
2242        assert_vars_are_missing(&result, &["edge1"]);
2243    }
2244
2245    #[tokio::test(flavor = "multi_thread")]
2246    async fn accessing_body_tag_through_body_sketch_tags_warns() {
2247        let program = r#"@settings(kclVersion = 2.0)
2248profile = startSketchOn(XY)
2249  |> startProfile(at = [0, 0])
2250  |> line(end = [10, 0], tag = $line1)
2251  |> line(end = [0, 10])
2252  |> line(end = [-10, 0])
2253  |> close()
2254
2255body = extrude(profile, length = 5, tagEnd = $top)
2256topFromSketch = body.sketch.tags.top
2257topFromBody = body.faces.top
2258"#;
2259
2260        let result = parse_execute(program).await.unwrap();
2261        assert!(matches!(get_var(&result, "topFromSketch"), KclValue::TagIdentifier(_)));
2262        assert!(matches!(get_var(&result, "topFromBody"), KclValue::TagIdentifier(_)));
2263
2264        let warnings = deprecated_solid_tag_access_warnings(&result);
2265        assert_eq!(warnings.len(), 1, "expected one deprecation warning, got {warnings:#?}");
2266        assert_eq!(warnings[0].severity, Severity::Warning);
2267        assert!(warnings[0].message.contains("`top`"), "found {}", warnings[0].message);
2268        assert!(
2269            warnings[0].message.contains("Accessing solid-created face `top` through sketch tags is deprecated. Use the body's faces instead, e.g. `body.faces.top`."),
2270            "found {}",
2271            warnings[0].message
2272        );
2273    }
2274
2275    #[tokio::test(flavor = "multi_thread")]
2276    async fn accessing_sketch_path_tag_through_body_sketch_tags_does_not_warn() {
2277        let program = r#"@settings(kclVersion = 2.0)
2278profile = startSketchOn(XY)
2279  |> startProfile(at = [0, 0])
2280  |> line(end = [10, 0], tag = $line1)
2281  |> line(end = [0, 10])
2282  |> line(end = [-10, 0])
2283  |> close()
2284
2285body = extrude(profile, length = 5, tagEnd = $top)
2286lineFromSketch = body.sketch.tags.line1
2287"#;
2288
2289        let result = parse_execute(program).await.unwrap();
2290        assert!(matches!(get_var(&result, "lineFromSketch"), KclValue::TagIdentifier(_)));
2291        let warnings = deprecated_solid_tag_access_warnings(&result);
2292        assert!(
2293            warnings.is_empty(),
2294            "sketch path tags should not get body-tag deprecation warnings: {warnings:#?}"
2295        );
2296    }
2297
2298    #[tokio::test(flavor = "multi_thread")]
2299    async fn accessing_body_tag_through_sketch_block_region_tags_warns() {
2300        let program = r#"@settings(kclVersion = 2.0)
2301profile = sketch(on = XY) {
2302  line1 = line(start = [0, 0], end = [10, 0])
2303  line2 = line(start = [10, 0], end = [10, 10])
2304  line3 = line(start = [10, 10], end = [0, 10])
2305  line4 = line(start = [0, 10], end = [0, 0])
2306}
2307
2308profileRegion = region(point = [1, 1], sketch = profile)
2309body = extrude(profileRegion, length = 5, tagEnd = $top)
2310topFromRegion = profileRegion.tags.top
2311"#;
2312
2313        let result = parse_execute(program).await.unwrap();
2314        assert!(matches!(get_var(&result, "topFromRegion"), KclValue::TagIdentifier(_)));
2315
2316        let warnings = deprecated_solid_tag_access_warnings(&result);
2317        assert_eq!(warnings.len(), 1, "expected one deprecation warning, got {warnings:#?}");
2318        assert_eq!(warnings[0].severity, Severity::Warning);
2319        assert!(warnings[0].message.contains("`top`"), "found {}", warnings[0].message);
2320    }
2321
2322    fn deprecation_warnings(result: &ExecTestResults) -> Vec<&CompilationIssue> {
2323        result
2324            .exec_state
2325            .issues()
2326            .iter()
2327            .filter(|issue| issue.message.contains("is deprecated"))
2328            .collect()
2329    }
2330
2331    #[tokio::test(flavor = "multi_thread")]
2332    async fn passing_param_deprecated_for_all_versions_warns() {
2333        // `@(deprecated = true)` deprecates the parameter regardless of the KCL
2334        // version, so even on the latest version the call should warn.
2335        let program = r#"@settings(kclVersion = 2.0)
2336fn f(
2337  @a: number,
2338  @(deprecated = true)
2339  oldArg?: number,
2340) {
2341  return a
2342}
2343x = f(1, oldArg = 2)
2344"#;
2345
2346        let result = parse_execute(program).await.unwrap();
2347        let warnings = deprecation_warnings(&result);
2348        assert_eq!(warnings.len(), 1, "expected one deprecation warning, got {warnings:#?}");
2349        assert_eq!(warnings[0].severity, Severity::Warning);
2350        assert_eq!(warnings[0].tag, crate::errors::Tag::Deprecated);
2351        assert!(
2352            warnings[0].message.contains("`f(oldArg)` is deprecated"),
2353            "found {}",
2354            warnings[0].message
2355        );
2356    }
2357
2358    #[tokio::test(flavor = "multi_thread")]
2359    async fn not_passing_deprecated_param_does_not_warn() {
2360        let program = r#"fn f(
2361  @a: number,
2362  @(deprecated = true)
2363  oldArg?: number,
2364) {
2365  return a
2366}
2367x = f(1)
2368"#;
2369
2370        let result = parse_execute(program).await.unwrap();
2371        let warnings = deprecation_warnings(&result);
2372        assert!(
2373            warnings.is_empty(),
2374            "unused deprecated parameter should not warn: {warnings:#?}"
2375        );
2376    }
2377
2378    #[tokio::test(flavor = "multi_thread")]
2379    async fn deprecated_calls_inside_kcl_stdlib_do_not_warn() {
2380        let program = include_str!("../../tests/cube_with_hole/input.kcl");
2381
2382        let result = parse_execute(program).await.unwrap();
2383        let warnings = deprecation_warnings(&result);
2384        assert!(
2385            warnings.is_empty(),
2386            "KCL stdlib internals should not emit deprecation warnings: {warnings:#?}"
2387        );
2388    }
2389
2390    #[tokio::test(flavor = "multi_thread")]
2391    async fn deprecated_stdlib_call_from_user_code_still_warns() {
2392        let program = r#"@settings(kclVersion = 2.0)
2393plane = startSketchOn(XY)
2394"#;
2395
2396        let result = parse_execute(program).await.unwrap();
2397        let warnings = deprecation_warnings(&result);
2398        assert_eq!(warnings.len(), 1, "expected one deprecation warning, got {warnings:#?}");
2399        assert!(
2400            warnings[0].message.contains("`startSketchOn` is deprecated"),
2401            "found {}",
2402            warnings[0].message
2403        );
2404        assert_eq!(warnings[0].tag, crate::errors::Tag::Deprecated);
2405    }
2406
2407    #[tokio::test(flavor = "multi_thread")]
2408    async fn deprecation_version_override_does_not_change_program_version() {
2409        let program = crate::Program::parse_no_errs(
2410            r#"@settings(kclVersion = 1.0)
2411plane = startSketchOn(XY)
2412"#,
2413        )
2414        .unwrap();
2415        let exec_ctxt = ExecutorContext {
2416            engine: Arc::new(EngineManager::new_mock()),
2417            engine_batch: crate::engine::EngineBatchContext::default(),
2418            fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
2419            settings: Default::default(),
2420            context_type: ContextType::Mock,
2421            execution_callbacks: Default::default(),
2422            executor_kind: crate::execution::machine::ExecutorKind::resolve(),
2423            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
2424        };
2425        let mut exec_state = ExecState::new(&exec_ctxt);
2426        exec_state.set_deprecation_version_override(Some("2.0"));
2427
2428        exec_ctxt.run(&program, &mut exec_state).await.unwrap();
2429
2430        assert_eq!(exec_state.mod_local.settings.kcl_version, crate::KclVersion::V1);
2431        let warnings = exec_state
2432            .issues()
2433            .iter()
2434            .filter(|issue| issue.tag == crate::errors::Tag::Deprecated)
2435            .collect::<Vec<_>>();
2436        assert_eq!(warnings.len(), 1, "expected one deprecation warning, got {warnings:#?}");
2437    }
2438
2439    #[tokio::test(flavor = "multi_thread")]
2440    async fn deprecated_sketch_v1_warning_explains_sketch_solve() {
2441        // Sketch v1 deprecation warnings must be self-contained: they should
2442        // say what replaces the function and link the conversion docs so both
2443        // humans and AI agents can act on the warning alone.
2444        let program = r#"@settings(kclVersion = 2.0)
2445exampleSketch = startSketchOn(XZ)
2446  |> startProfile(at = [0, 0])
2447  |> line(end = [10, 0])
2448"#;
2449
2450        let result = parse_execute(program).await.unwrap();
2451        let warnings = deprecation_warnings(&result);
2452        assert_eq!(
2453            warnings.len(),
2454            3,
2455            "expected one warning per sketch v1 call, got {warnings:#?}"
2456        );
2457        for warning in warnings {
2458            assert!(
2459                warning.message.contains("sketch-solve"),
2460                "expected sketch-solve context in {}",
2461                warning.message
2462            );
2463            assert!(
2464                warning
2465                    .message
2466                    .contains("https://zoo.dev/docs/kcl-book/sketch2d_constraints.html"),
2467                "expected docs URL in {}",
2468                warning.message
2469            );
2470        }
2471    }
2472}