Skip to main content

kcl_lib/execution/
fn_call.rs

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