Skip to main content

kcl_lib/execution/
exec_ast.rs

1use std::collections::HashMap;
2
3use async_recursion::async_recursion;
4use indexmap::IndexMap;
5use kcl_ezpz::{Constraint, NonLinearSystemError};
6use kittycad_modeling_cmds as kcmc;
7
8#[cfg(feature = "artifact-graph")]
9use crate::front::{Object, ObjectKind};
10use crate::{
11    CompilationError, NodePath, SourceRange,
12    errors::{KclError, KclErrorDetails},
13    exec::Sketch,
14    execution::{
15        AbstractSegment, BodyType, ControlFlowKind, EarlyReturn, EnvironmentRef, ExecState, ExecutorContext, KclValue,
16        KclValueControlFlow, Metadata, ModelingCmdMeta, ModuleArtifactState, Operation, PreserveMem,
17        SKETCH_BLOCK_PARAM_ON, SKETCH_OBJECT_META, SKETCH_OBJECT_META_SKETCH, Segment, SegmentKind, SegmentRepr,
18        SketchConstraintKind, SketchSurface, StatementKind, TagIdentifier, UnsolvedExpr, UnsolvedSegment,
19        UnsolvedSegmentKind, annotations,
20        cad_op::OpKclValue,
21        control_continue, early_return,
22        fn_call::{Arg, Args},
23        kcl_value::{FunctionSource, KclFunctionSourceParams, TypeDef},
24        memory::{self, SKETCH_PREFIX},
25        sketch_solve::{
26            FreedomAnalysis, Solved, create_segment_scene_objects, normalize_to_solver_angle_unit,
27            normalize_to_solver_distance_unit, solver_numeric_type, substitute_sketch_var_in_segment,
28            substitute_sketch_vars,
29        },
30        state::{ModuleState, SketchBlockState},
31        types::{NumericType, PrimitiveType, RuntimeType},
32    },
33    front::{ObjectId, PointCtor},
34    modules::{ModuleExecutionOutcome, ModuleId, ModulePath, ModuleRepr},
35    parsing::ast::types::{
36        Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryOperator,
37        BinaryPart, BodyItem, CodeBlock, Expr, IfExpression, ImportPath, ImportSelector, ItemVisibility,
38        MemberExpression, Name, Node, ObjectExpression, PipeExpression, Program, SketchBlock, SketchVar, TagDeclarator,
39        Type, UnaryExpression, UnaryOperator,
40    },
41    std::{
42        args::{FromKclValue, TyF64},
43        shapes::SketchOrSurface,
44        sketch::ensure_sketch_plane_in_engine,
45        sketch2::create_segments_in_engine,
46    },
47};
48
49fn internal_err(message: impl Into<String>, range: impl Into<SourceRange>) -> KclError {
50    KclError::new_internal(KclErrorDetails::new(message.into(), vec![range.into()]))
51}
52
53fn sketch_on_cache_name(sketch_id: ObjectId) -> String {
54    format!("{SKETCH_PREFIX}{}_on", sketch_id.0)
55}
56
57#[cfg(feature = "artifact-graph")]
58fn default_plane_name_from_expr(expr: &Expr) -> Option<crate::engine::PlaneName> {
59    fn parse_name(name: &str, negative: bool) -> Option<crate::engine::PlaneName> {
60        use crate::engine::PlaneName;
61
62        match (name, negative) {
63            ("XY", false) => Some(PlaneName::Xy),
64            ("XY", true) => Some(PlaneName::NegXy),
65            ("XZ", false) => Some(PlaneName::Xz),
66            ("XZ", true) => Some(PlaneName::NegXz),
67            ("YZ", false) => Some(PlaneName::Yz),
68            ("YZ", true) => Some(PlaneName::NegYz),
69            _ => None,
70        }
71    }
72
73    match expr {
74        Expr::Name(name) => {
75            if !name.path.is_empty() {
76                return None;
77            }
78            parse_name(&name.name.name, false)
79        }
80        Expr::UnaryExpression(unary) => {
81            if unary.operator != UnaryOperator::Neg {
82                return None;
83            }
84            let crate::parsing::ast::types::BinaryPart::Name(name) = &unary.argument else {
85                return None;
86            };
87            if !name.path.is_empty() {
88                return None;
89            }
90            parse_name(&name.name.name, true)
91        }
92        _ => None,
93    }
94}
95
96#[cfg(feature = "artifact-graph")]
97fn sketch_on_frontend_plane(
98    arguments: &[crate::parsing::ast::types::LabeledArg],
99    on_object_id: crate::front::ObjectId,
100) -> crate::front::Plane {
101    for arg in arguments {
102        let Some(label) = &arg.label else {
103            continue;
104        };
105        if label.name != SKETCH_BLOCK_PARAM_ON {
106            continue;
107        }
108        if let Some(name) = default_plane_name_from_expr(&arg.arg) {
109            return crate::front::Plane::Default(name);
110        }
111        break;
112    }
113
114    crate::front::Plane::Object(on_object_id)
115}
116
117impl<'a> StatementKind<'a> {
118    fn expect_name(&self) -> &'a str {
119        match self {
120            StatementKind::Declaration { name } => name,
121            StatementKind::Expression => unreachable!(),
122        }
123    }
124}
125
126impl ExecutorContext {
127    /// Returns true if importing the prelude should be skipped.
128    async fn handle_annotations(
129        &self,
130        annotations: impl Iterator<Item = &Node<Annotation>>,
131        body_type: BodyType,
132        exec_state: &mut ExecState,
133    ) -> Result<bool, KclError> {
134        let mut no_prelude = false;
135        for annotation in annotations {
136            if annotation.name() == Some(annotations::SETTINGS) {
137                if matches!(body_type, BodyType::Root) {
138                    let (updated_len, updated_angle) =
139                        exec_state.mod_local.settings.update_from_annotation(annotation)?;
140                    if updated_len {
141                        exec_state.mod_local.explicit_length_units = true;
142                    }
143                    if updated_angle {
144                        exec_state.warn(
145                            CompilationError::err(
146                                annotation.as_source_range(),
147                                "Prefer to use explicit units for angles",
148                            ),
149                            annotations::WARN_ANGLE_UNITS,
150                        );
151                    }
152                } else {
153                    exec_state.err(CompilationError::err(
154                        annotation.as_source_range(),
155                        "Settings can only be modified at the top level scope of a file",
156                    ));
157                }
158            } else if annotation.name() == Some(annotations::NO_PRELUDE) {
159                if matches!(body_type, BodyType::Root) {
160                    no_prelude = true;
161                } else {
162                    exec_state.err(CompilationError::err(
163                        annotation.as_source_range(),
164                        "The standard library can only be skipped at the top level scope of a file",
165                    ));
166                }
167            } else if annotation.name() == Some(annotations::WARNINGS) {
168                // TODO we should support setting warnings for the whole project, not just one file
169                if matches!(body_type, BodyType::Root) {
170                    let props = annotations::expect_properties(annotations::WARNINGS, annotation)?;
171                    for p in props {
172                        match &*p.inner.key.name {
173                            annotations::WARN_ALLOW => {
174                                let allowed = annotations::many_of(
175                                    &p.inner.value,
176                                    &annotations::WARN_VALUES,
177                                    annotation.as_source_range(),
178                                )?;
179                                exec_state.mod_local.allowed_warnings = allowed;
180                            }
181                            annotations::WARN_DENY => {
182                                let denied = annotations::many_of(
183                                    &p.inner.value,
184                                    &annotations::WARN_VALUES,
185                                    annotation.as_source_range(),
186                                )?;
187                                exec_state.mod_local.denied_warnings = denied;
188                            }
189                            name => {
190                                return Err(KclError::new_semantic(KclErrorDetails::new(
191                                    format!(
192                                        "Unexpected warnings key: `{name}`; expected one of `{}`, `{}`",
193                                        annotations::WARN_ALLOW,
194                                        annotations::WARN_DENY,
195                                    ),
196                                    vec![annotation.as_source_range()],
197                                )));
198                            }
199                        }
200                    }
201                } else {
202                    exec_state.err(CompilationError::err(
203                        annotation.as_source_range(),
204                        "Warnings can only be customized at the top level scope of a file",
205                    ));
206                }
207            } else {
208                exec_state.warn(
209                    CompilationError::err(annotation.as_source_range(), "Unknown annotation"),
210                    annotations::WARN_UNKNOWN_ATTR,
211                );
212            }
213        }
214        Ok(no_prelude)
215    }
216
217    pub(super) async fn exec_module_body(
218        &self,
219        program: &Node<Program>,
220        exec_state: &mut ExecState,
221        preserve_mem: PreserveMem,
222        module_id: ModuleId,
223        path: &ModulePath,
224    ) -> Result<ModuleExecutionOutcome, (KclError, Option<EnvironmentRef>, Option<ModuleArtifactState>)> {
225        crate::log::log(format!("enter module {path} {}", exec_state.stack()));
226
227        // When executing only the new statements in incremental execution or
228        // mock executing for sketch mode, we need the scene objects that were
229        // created during the last execution, which are in the execution cache.
230        // The cache is read to create the initial module state. Depending on
231        // whether it's mock execution or engine execution, it's rehydrated
232        // differently, so we need to clone them from a different place. Then
233        // make sure the object ID generator matches the number of existing
234        // scene objects.
235        let mut local_state = ModuleState::new(
236            path.clone(),
237            exec_state.stack().memory.clone(),
238            Some(module_id),
239            exec_state.mod_local.sketch_mode,
240            exec_state.mod_local.freedom_analysis,
241        );
242        match preserve_mem {
243            PreserveMem::Always => {
244                #[cfg(feature = "artifact-graph")]
245                {
246                    use crate::id::IncIdGenerator;
247                    exec_state
248                        .mod_local
249                        .artifacts
250                        .scene_objects
251                        .clone_from(&exec_state.global.root_module_artifacts.scene_objects);
252                    exec_state.mod_local.artifacts.object_id_generator =
253                        IncIdGenerator::new(exec_state.global.root_module_artifacts.scene_objects.len());
254                }
255            }
256            PreserveMem::Normal => {
257                #[cfg(feature = "artifact-graph")]
258                {
259                    local_state
260                        .artifacts
261                        .scene_objects
262                        .clone_from(&exec_state.mod_local.artifacts.scene_objects);
263                }
264                std::mem::swap(&mut exec_state.mod_local, &mut local_state);
265            }
266        }
267
268        let no_prelude = self
269            .handle_annotations(program.inner_attrs.iter(), crate::execution::BodyType::Root, exec_state)
270            .await
271            .map_err(|err| (err, None, None))?;
272
273        if preserve_mem.normal() {
274            exec_state.mut_stack().push_new_root_env(!no_prelude);
275        }
276
277        let result = self
278            .exec_block(program, exec_state, crate::execution::BodyType::Root)
279            .await;
280
281        let env_ref = match preserve_mem {
282            PreserveMem::Always => exec_state.mut_stack().pop_and_preserve_env(),
283            PreserveMem::Normal => exec_state.mut_stack().pop_env(),
284        };
285        let module_artifacts = match preserve_mem {
286            PreserveMem::Always => std::mem::take(&mut exec_state.mod_local.artifacts),
287            PreserveMem::Normal => {
288                std::mem::swap(&mut exec_state.mod_local, &mut local_state);
289                local_state.artifacts
290            }
291        };
292
293        crate::log::log(format!("leave {path}"));
294
295        result
296            .map_err(|err| (err, Some(env_ref), Some(module_artifacts.clone())))
297            .map(|last_expr| ModuleExecutionOutcome {
298                last_expr: last_expr.map(|value_cf| value_cf.into_value()),
299                environment: env_ref,
300                exports: local_state.module_exports,
301                artifacts: module_artifacts,
302            })
303    }
304
305    /// Execute an AST's program.
306    #[async_recursion]
307    pub(super) async fn exec_block<'a, B>(
308        &'a self,
309        block: &'a B,
310        exec_state: &mut ExecState,
311        body_type: BodyType,
312    ) -> Result<Option<KclValueControlFlow>, KclError>
313    where
314        B: CodeBlock + Sync,
315    {
316        let mut last_expr = None;
317        // Iterate over the body of the program.
318        for statement in block.body() {
319            match statement {
320                BodyItem::ImportStatement(import_stmt) => {
321                    if exec_state.sketch_mode() {
322                        continue;
323                    }
324                    if !matches!(body_type, BodyType::Root) {
325                        return Err(KclError::new_semantic(KclErrorDetails::new(
326                            "Imports are only supported at the top-level of a file.".to_owned(),
327                            vec![import_stmt.into()],
328                        )));
329                    }
330
331                    let source_range = SourceRange::from(import_stmt);
332                    let attrs = &import_stmt.outer_attrs;
333                    let module_path = ModulePath::from_import_path(
334                        &import_stmt.path,
335                        &self.settings.project_directory,
336                        &exec_state.mod_local.path,
337                    )?;
338                    let module_id = self
339                        .open_module(&import_stmt.path, attrs, &module_path, exec_state, source_range)
340                        .await?;
341
342                    match &import_stmt.selector {
343                        ImportSelector::List { items } => {
344                            let (env_ref, module_exports) =
345                                self.exec_module_for_items(module_id, exec_state, source_range).await?;
346                            for import_item in items {
347                                // Extract the item from the module.
348                                let mem = &exec_state.stack().memory;
349                                let mut value = mem
350                                    .get_from(&import_item.name.name, env_ref, import_item.into(), 0)
351                                    .cloned();
352                                let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.name.name);
353                                let mut ty = mem.get_from(&ty_name, env_ref, import_item.into(), 0).cloned();
354                                let mod_name = format!("{}{}", memory::MODULE_PREFIX, import_item.name.name);
355                                let mut mod_value = mem.get_from(&mod_name, env_ref, import_item.into(), 0).cloned();
356
357                                if value.is_err() && ty.is_err() && mod_value.is_err() {
358                                    return Err(KclError::new_undefined_value(
359                                        KclErrorDetails::new(
360                                            format!("{} is not defined in module", import_item.name.name),
361                                            vec![SourceRange::from(&import_item.name)],
362                                        ),
363                                        None,
364                                    ));
365                                }
366
367                                // Check that the item is allowed to be imported (in at least one namespace).
368                                if value.is_ok() && !module_exports.contains(&import_item.name.name) {
369                                    value = Err(KclError::new_semantic(KclErrorDetails::new(
370                                        format!(
371                                            "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
372                                            import_item.name.name
373                                        ),
374                                        vec![SourceRange::from(&import_item.name)],
375                                    )));
376                                }
377
378                                if ty.is_ok() && !module_exports.contains(&ty_name) {
379                                    ty = Err(KclError::new_semantic(KclErrorDetails::new(
380                                        format!(
381                                            "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
382                                            import_item.name.name
383                                        ),
384                                        vec![SourceRange::from(&import_item.name)],
385                                    )));
386                                }
387
388                                if mod_value.is_ok() && !module_exports.contains(&mod_name) {
389                                    mod_value = Err(KclError::new_semantic(KclErrorDetails::new(
390                                        format!(
391                                            "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
392                                            import_item.name.name
393                                        ),
394                                        vec![SourceRange::from(&import_item.name)],
395                                    )));
396                                }
397
398                                if value.is_err() && ty.is_err() && mod_value.is_err() {
399                                    return value.map(|v| Some(v.continue_()));
400                                }
401
402                                // Add the item to the current module.
403                                if let Ok(value) = value {
404                                    exec_state.mut_stack().add(
405                                        import_item.identifier().to_owned(),
406                                        value,
407                                        SourceRange::from(&import_item.name),
408                                    )?;
409
410                                    if let ItemVisibility::Export = import_stmt.visibility {
411                                        exec_state
412                                            .mod_local
413                                            .module_exports
414                                            .push(import_item.identifier().to_owned());
415                                    }
416                                }
417
418                                if let Ok(ty) = ty {
419                                    let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.identifier());
420                                    exec_state.mut_stack().add(
421                                        ty_name.clone(),
422                                        ty,
423                                        SourceRange::from(&import_item.name),
424                                    )?;
425
426                                    if let ItemVisibility::Export = import_stmt.visibility {
427                                        exec_state.mod_local.module_exports.push(ty_name);
428                                    }
429                                }
430
431                                if let Ok(mod_value) = mod_value {
432                                    let mod_name = format!("{}{}", memory::MODULE_PREFIX, import_item.identifier());
433                                    exec_state.mut_stack().add(
434                                        mod_name.clone(),
435                                        mod_value,
436                                        SourceRange::from(&import_item.name),
437                                    )?;
438
439                                    if let ItemVisibility::Export = import_stmt.visibility {
440                                        exec_state.mod_local.module_exports.push(mod_name);
441                                    }
442                                }
443                            }
444                        }
445                        ImportSelector::Glob(_) => {
446                            let (env_ref, module_exports) =
447                                self.exec_module_for_items(module_id, exec_state, source_range).await?;
448                            for name in module_exports.iter() {
449                                let item = exec_state
450                                    .stack()
451                                    .memory
452                                    .get_from(name, env_ref, source_range, 0)
453                                    .map_err(|_err| {
454                                        internal_err(
455                                            format!("{name} is not defined in module (but was exported?)"),
456                                            source_range,
457                                        )
458                                    })?
459                                    .clone();
460                                exec_state.mut_stack().add(name.to_owned(), item, source_range)?;
461
462                                if let ItemVisibility::Export = import_stmt.visibility {
463                                    exec_state.mod_local.module_exports.push(name.clone());
464                                }
465                            }
466                        }
467                        ImportSelector::None { .. } => {
468                            let name = import_stmt.module_name().unwrap();
469                            let item = KclValue::Module {
470                                value: module_id,
471                                meta: vec![source_range.into()],
472                            };
473                            exec_state.mut_stack().add(
474                                format!("{}{}", memory::MODULE_PREFIX, name),
475                                item,
476                                source_range,
477                            )?;
478                        }
479                    }
480                    last_expr = None;
481                }
482                BodyItem::ExpressionStatement(expression_statement) => {
483                    if exec_state.sketch_mode() && sketch_mode_should_skip(&expression_statement.expression) {
484                        continue;
485                    }
486
487                    let metadata = Metadata::from(expression_statement);
488                    let value = self
489                        .execute_expr(
490                            &expression_statement.expression,
491                            exec_state,
492                            &metadata,
493                            &[],
494                            StatementKind::Expression,
495                        )
496                        .await?;
497
498                    let is_return = value.is_some_return();
499                    last_expr = Some(value);
500
501                    if is_return {
502                        break;
503                    }
504                }
505                BodyItem::VariableDeclaration(variable_declaration) => {
506                    if exec_state.sketch_mode() && sketch_mode_should_skip(&variable_declaration.declaration.init) {
507                        continue;
508                    }
509
510                    let var_name = variable_declaration.declaration.id.name.to_string();
511                    let source_range = SourceRange::from(&variable_declaration.declaration.init);
512                    let metadata = Metadata { source_range };
513
514                    let annotations = &variable_declaration.outer_attrs;
515
516                    // During the evaluation of the variable's RHS, set context that this is all happening inside a variable
517                    // declaration, for the given name. This helps improve user-facing error messages.
518                    let lhs = variable_declaration.inner.name().to_owned();
519                    let prev_being_declared = exec_state.mod_local.being_declared.take();
520                    exec_state.mod_local.being_declared = Some(lhs);
521                    let rhs_result = self
522                        .execute_expr(
523                            &variable_declaration.declaration.init,
524                            exec_state,
525                            &metadata,
526                            annotations,
527                            StatementKind::Declaration { name: &var_name },
528                        )
529                        .await;
530                    // Declaration over, so unset this context.
531                    exec_state.mod_local.being_declared = prev_being_declared;
532                    let rhs = rhs_result?;
533
534                    if rhs.is_some_return() {
535                        last_expr = Some(rhs);
536                        break;
537                    }
538                    let mut rhs = rhs.into_value();
539
540                    // Attach the variable name to unsolved segments as a tag.
541                    // While executing the body of a sketch block, the segments
542                    // won't have been solved yet.
543                    if let KclValue::Segment { value } = &mut rhs
544                        && let SegmentRepr::Unsolved { segment } = &mut value.repr
545                    {
546                        segment.tag = Some(TagIdentifier {
547                            value: variable_declaration.declaration.id.name.clone(),
548                            info: Default::default(),
549                            meta: vec![SourceRange::from(&variable_declaration.declaration.id).into()],
550                        });
551                    }
552                    let rhs = rhs; // Remove mutability.
553
554                    let should_bind_name =
555                        if let Some(fn_name) = variable_declaration.declaration.init.fn_declaring_name() {
556                            // Declaring a function with a name, so only bind
557                            // the variable name if it differs from the function
558                            // name.
559                            var_name != fn_name
560                        } else {
561                            // Not declaring a function, so we should bind the
562                            // variable name.
563                            true
564                        };
565                    if should_bind_name {
566                        exec_state
567                            .mut_stack()
568                            .add(var_name.clone(), rhs.clone(), source_range)?;
569                    }
570
571                    if let Some(sketch_block_state) = exec_state.mod_local.sketch_block.as_mut()
572                        && let KclValue::Segment { value } = &rhs
573                    {
574                        // Add segment to mapping so that we can tag it when
575                        // sending to the engine.
576                        let segment_object_id = match &value.repr {
577                            SegmentRepr::Unsolved { segment } => segment.object_id,
578                            SegmentRepr::Solved { segment } => segment.object_id,
579                        };
580                        sketch_block_state
581                            .segment_tags
582                            .entry(segment_object_id)
583                            .or_insert_with(|| {
584                                let id_node = &variable_declaration.declaration.id;
585                                Node::new(
586                                    TagDeclarator {
587                                        name: id_node.name.clone(),
588                                        digest: None,
589                                    },
590                                    id_node.start,
591                                    id_node.end,
592                                    id_node.module_id,
593                                )
594                            });
595                    }
596
597                    // Track operations, for the feature tree.
598                    // Don't track these operations if the KCL code being executed is in the stdlib,
599                    // because users shouldn't know about stdlib internals -- it's useless noise, to them.
600                    let should_show_in_feature_tree =
601                        !exec_state.mod_local.inside_stdlib && rhs.show_variable_in_feature_tree();
602                    if should_show_in_feature_tree {
603                        exec_state.push_op(Operation::VariableDeclaration {
604                            name: var_name.clone(),
605                            value: OpKclValue::from(&rhs),
606                            visibility: variable_declaration.visibility,
607                            node_path: NodePath::placeholder(),
608                            source_range,
609                        });
610                    }
611
612                    // Track exports.
613                    if let ItemVisibility::Export = variable_declaration.visibility {
614                        if matches!(body_type, BodyType::Root) {
615                            exec_state.mod_local.module_exports.push(var_name);
616                        } else {
617                            exec_state.err(CompilationError::err(
618                                variable_declaration.as_source_range(),
619                                "Exports are only supported at the top-level of a file. Remove `export` or move it to the top-level.",
620                            ));
621                        }
622                    }
623                    // Variable declaration can be the return value of a module.
624                    last_expr = matches!(body_type, BodyType::Root).then_some(rhs.continue_());
625                }
626                BodyItem::TypeDeclaration(ty) => {
627                    if exec_state.sketch_mode() {
628                        continue;
629                    }
630
631                    let metadata = Metadata::from(&**ty);
632                    let attrs = annotations::get_fn_attrs(&ty.outer_attrs, metadata.source_range)?.unwrap_or_default();
633                    match attrs.impl_ {
634                        annotations::Impl::Rust
635                        | annotations::Impl::RustConstrainable
636                        | annotations::Impl::RustConstraint => {
637                            let std_path = match &exec_state.mod_local.path {
638                                ModulePath::Std { value } => value,
639                                ModulePath::Local { .. } | ModulePath::Main => {
640                                    return Err(KclError::new_semantic(KclErrorDetails::new(
641                                        "User-defined types are not yet supported.".to_owned(),
642                                        vec![metadata.source_range],
643                                    )));
644                                }
645                            };
646                            let (t, props) = crate::std::std_ty(std_path, &ty.name.name);
647                            let value = KclValue::Type {
648                                value: TypeDef::RustRepr(t, props),
649                                meta: vec![metadata],
650                                experimental: attrs.experimental,
651                            };
652                            let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name);
653                            exec_state
654                                .mut_stack()
655                                .add(name_in_mem.clone(), value, metadata.source_range)
656                                .map_err(|_| {
657                                    KclError::new_semantic(KclErrorDetails::new(
658                                        format!("Redefinition of type {}.", ty.name.name),
659                                        vec![metadata.source_range],
660                                    ))
661                                })?;
662
663                            if let ItemVisibility::Export = ty.visibility {
664                                exec_state.mod_local.module_exports.push(name_in_mem);
665                            }
666                        }
667                        // Do nothing for primitive types, they get special treatment and their declarations are just for documentation.
668                        annotations::Impl::Primitive => {}
669                        annotations::Impl::Kcl | annotations::Impl::KclConstrainable => match &ty.alias {
670                            Some(alias) => {
671                                let value = KclValue::Type {
672                                    value: TypeDef::Alias(
673                                        RuntimeType::from_parsed(
674                                            alias.inner.clone(),
675                                            exec_state,
676                                            metadata.source_range,
677                                            attrs.impl_ == annotations::Impl::KclConstrainable,
678                                            false,
679                                        )
680                                        .map_err(|e| KclError::new_semantic(e.into()))?,
681                                    ),
682                                    meta: vec![metadata],
683                                    experimental: attrs.experimental,
684                                };
685                                let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name);
686                                exec_state
687                                    .mut_stack()
688                                    .add(name_in_mem.clone(), value, metadata.source_range)
689                                    .map_err(|_| {
690                                        KclError::new_semantic(KclErrorDetails::new(
691                                            format!("Redefinition of type {}.", ty.name.name),
692                                            vec![metadata.source_range],
693                                        ))
694                                    })?;
695
696                                if let ItemVisibility::Export = ty.visibility {
697                                    exec_state.mod_local.module_exports.push(name_in_mem);
698                                }
699                            }
700                            None => {
701                                return Err(KclError::new_semantic(KclErrorDetails::new(
702                                    "User-defined types are not yet supported.".to_owned(),
703                                    vec![metadata.source_range],
704                                )));
705                            }
706                        },
707                    }
708
709                    last_expr = None;
710                }
711                BodyItem::ReturnStatement(return_statement) => {
712                    if exec_state.sketch_mode() && sketch_mode_should_skip(&return_statement.argument) {
713                        continue;
714                    }
715
716                    let metadata = Metadata::from(return_statement);
717
718                    if matches!(body_type, BodyType::Root) {
719                        return Err(KclError::new_semantic(KclErrorDetails::new(
720                            "Cannot return from outside a function.".to_owned(),
721                            vec![metadata.source_range],
722                        )));
723                    }
724
725                    let value_cf = self
726                        .execute_expr(
727                            &return_statement.argument,
728                            exec_state,
729                            &metadata,
730                            &[],
731                            StatementKind::Expression,
732                        )
733                        .await?;
734                    if value_cf.is_some_return() {
735                        last_expr = Some(value_cf);
736                        break;
737                    }
738                    let value = value_cf.into_value();
739                    exec_state
740                        .mut_stack()
741                        .add(memory::RETURN_NAME.to_owned(), value, metadata.source_range)
742                        .map_err(|_| {
743                            KclError::new_semantic(KclErrorDetails::new(
744                                "Multiple returns from a single function.".to_owned(),
745                                vec![metadata.source_range],
746                            ))
747                        })?;
748                    last_expr = None;
749                }
750            }
751        }
752
753        if matches!(body_type, BodyType::Root) {
754            // Flush the batch queue.
755            exec_state
756                .flush_batch(
757                    ModelingCmdMeta::new(exec_state, self, block.to_source_range()),
758                    // True here tells the engine to flush all the end commands as well like fillets
759                    // and chamfers where the engine would otherwise eat the ID of the segments.
760                    true,
761                )
762                .await?;
763        }
764
765        Ok(last_expr)
766    }
767
768    pub async fn open_module(
769        &self,
770        path: &ImportPath,
771        attrs: &[Node<Annotation>],
772        resolved_path: &ModulePath,
773        exec_state: &mut ExecState,
774        source_range: SourceRange,
775    ) -> Result<ModuleId, KclError> {
776        match path {
777            ImportPath::Kcl { .. } => {
778                exec_state.global.mod_loader.cycle_check(resolved_path, source_range)?;
779
780                if let Some(id) = exec_state.id_for_module(resolved_path) {
781                    return Ok(id);
782                }
783
784                let id = exec_state.next_module_id();
785                // Add file path string to global state even if it fails to import
786                exec_state.add_path_to_source_id(resolved_path.clone(), id);
787                let source = resolved_path.source(&self.fs, source_range).await?;
788                exec_state.add_id_to_source(id, source.clone());
789                // TODO handle parsing errors properly
790                let parsed = crate::parsing::parse_str(&source.source, id).parse_errs_as_err()?;
791                exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
792
793                Ok(id)
794            }
795            ImportPath::Foreign { .. } => {
796                if let Some(id) = exec_state.id_for_module(resolved_path) {
797                    return Ok(id);
798                }
799
800                let id = exec_state.next_module_id();
801                let path = resolved_path.expect_path();
802                // Add file path string to global state even if it fails to import
803                exec_state.add_path_to_source_id(resolved_path.clone(), id);
804                let format = super::import::format_from_annotations(attrs, path, source_range)?;
805                let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
806                exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Foreign(geom, None));
807                Ok(id)
808            }
809            ImportPath::Std { .. } => {
810                if let Some(id) = exec_state.id_for_module(resolved_path) {
811                    return Ok(id);
812                }
813
814                let id = exec_state.next_module_id();
815                // Add file path string to global state even if it fails to import
816                exec_state.add_path_to_source_id(resolved_path.clone(), id);
817                let source = resolved_path.source(&self.fs, source_range).await?;
818                exec_state.add_id_to_source(id, source.clone());
819                let parsed = crate::parsing::parse_str(&source.source, id)
820                    .parse_errs_as_err()
821                    .unwrap();
822                exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
823                Ok(id)
824            }
825        }
826    }
827
828    pub(super) async fn exec_module_for_items(
829        &self,
830        module_id: ModuleId,
831        exec_state: &mut ExecState,
832        source_range: SourceRange,
833    ) -> Result<(EnvironmentRef, Vec<String>), KclError> {
834        let path = exec_state.global.module_infos[&module_id].path.clone();
835        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
836        // DON'T EARLY RETURN! We need to restore the module repr
837
838        let result = match &mut repr {
839            ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
840            ModuleRepr::Kcl(_, Some(outcome)) => Ok((outcome.environment, outcome.exports.clone())),
841            ModuleRepr::Kcl(program, cache) => self
842                .exec_module_from_ast(program, module_id, &path, exec_state, source_range, PreserveMem::Normal)
843                .await
844                .map(|outcome| {
845                    *cache = Some(outcome.clone());
846                    (outcome.environment, outcome.exports)
847                }),
848            ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
849                "Cannot import items from foreign modules".to_owned(),
850                vec![geom.source_range],
851            ))),
852            ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path),
853        };
854
855        exec_state.global.module_infos[&module_id].restore_repr(repr);
856        result
857    }
858
859    async fn exec_module_for_result(
860        &self,
861        module_id: ModuleId,
862        exec_state: &mut ExecState,
863        source_range: SourceRange,
864    ) -> Result<Option<KclValue>, KclError> {
865        let path = exec_state.global.module_infos[&module_id].path.clone();
866        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
867        // DON'T EARLY RETURN! We need to restore the module repr
868
869        let result = match &mut repr {
870            ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
871            ModuleRepr::Kcl(_, Some(outcome)) => Ok(outcome.last_expr.clone()),
872            ModuleRepr::Kcl(program, cached_items) => {
873                let result = self
874                    .exec_module_from_ast(program, module_id, &path, exec_state, source_range, PreserveMem::Normal)
875                    .await;
876                match result {
877                    Ok(outcome) => {
878                        let value = outcome.last_expr.clone();
879                        *cached_items = Some(outcome);
880                        Ok(value)
881                    }
882                    Err(e) => Err(e),
883                }
884            }
885            ModuleRepr::Foreign(_, Some((imported, _))) => Ok(imported.clone()),
886            ModuleRepr::Foreign(geom, cached) => {
887                let result = super::import::send_to_engine(geom.clone(), exec_state, self)
888                    .await
889                    .map(|geom| Some(KclValue::ImportedGeometry(geom)));
890
891                match result {
892                    Ok(val) => {
893                        *cached = Some((val.clone(), exec_state.mod_local.artifacts.clone()));
894                        Ok(val)
895                    }
896                    Err(e) => Err(e),
897                }
898            }
899            ModuleRepr::Dummy => unreachable!(),
900        };
901
902        exec_state.global.module_infos[&module_id].restore_repr(repr);
903
904        result
905    }
906
907    pub async fn exec_module_from_ast(
908        &self,
909        program: &Node<Program>,
910        module_id: ModuleId,
911        path: &ModulePath,
912        exec_state: &mut ExecState,
913        source_range: SourceRange,
914        preserve_mem: PreserveMem,
915    ) -> Result<ModuleExecutionOutcome, KclError> {
916        exec_state.global.mod_loader.enter_module(path);
917        let result = self
918            .exec_module_body(program, exec_state, preserve_mem, module_id, path)
919            .await;
920        exec_state.global.mod_loader.leave_module(path, source_range)?;
921
922        // TODO: ModuleArtifactState is getting dropped here when there's an
923        // error.  Should we propagate it for non-root modules?
924        result.map_err(|(err, _, _)| {
925            match err {
926                KclError::ImportCycle { .. } => {
927                    // It was an import cycle.  Keep the original message.
928                    err.override_source_ranges(vec![source_range])
929                }
930                KclError::EngineHangup { .. } | KclError::EngineInternal { .. } => {
931                    // Propagate this type of error. It's likely a transient
932                    // error that just needs to be retried.
933                    err.override_source_ranges(vec![source_range])
934                }
935                _ => {
936                    // TODO would be great to have line/column for the underlying error here
937                    KclError::new_semantic(KclErrorDetails::new(
938                        format!(
939                            "Error loading imported file ({path}). Open it to view more details.\n  {}",
940                            err.message()
941                        ),
942                        vec![source_range],
943                    ))
944                }
945            }
946        })
947    }
948
949    #[async_recursion]
950    pub(crate) async fn execute_expr<'a: 'async_recursion>(
951        &self,
952        init: &Expr,
953        exec_state: &mut ExecState,
954        metadata: &Metadata,
955        annotations: &[Node<Annotation>],
956        statement_kind: StatementKind<'a>,
957    ) -> Result<KclValueControlFlow, KclError> {
958        let item = match init {
959            Expr::None(none) => KclValue::from(none).continue_(),
960            Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), exec_state).continue_(),
961            Expr::TagDeclarator(tag) => tag.execute(exec_state).await?.continue_(),
962            Expr::Name(name) => {
963                let being_declared = exec_state.mod_local.being_declared.clone();
964                let value = name
965                    .get_result(exec_state, self)
966                    .await
967                    .map_err(|e| var_in_own_ref_err(e, &being_declared))?
968                    .clone();
969                if let KclValue::Module { value: module_id, meta } = value {
970                    self.exec_module_for_result(
971                        module_id,
972                        exec_state,
973                        metadata.source_range
974                        ).await?.map(|v| v.continue_())
975                        .unwrap_or_else(|| {
976                            exec_state.warn(CompilationError::err(
977                                metadata.source_range,
978                                "Imported module has no return value. The last statement of the module must be an expression, usually the Solid.",
979                            ),
980                        annotations::WARN_MOD_RETURN_VALUE);
981
982                            let mut new_meta = vec![metadata.to_owned()];
983                            new_meta.extend(meta);
984                            KclValue::KclNone {
985                                value: Default::default(),
986                                meta: new_meta,
987                            }.continue_()
988                        })
989                } else {
990                    value.continue_()
991                }
992            }
993            Expr::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, self).await?,
994            Expr::FunctionExpression(function_expression) => {
995                let attrs = annotations::get_fn_attrs(annotations, metadata.source_range)?;
996                let experimental = attrs.map(|a| a.experimental).unwrap_or_default();
997                let is_std = matches!(&exec_state.mod_local.path, ModulePath::Std { .. });
998
999                // Check the KCL @(feature_tree = ) annotation.
1000                let include_in_feature_tree = attrs.unwrap_or_default().include_in_feature_tree;
1001                let (mut closure, placeholder_env_ref) = if let Some(attrs) = attrs
1002                    && (attrs.impl_ == annotations::Impl::Rust
1003                        || attrs.impl_ == annotations::Impl::RustConstrainable
1004                        || attrs.impl_ == annotations::Impl::RustConstraint)
1005                {
1006                    if let ModulePath::Std { value: std_path } = &exec_state.mod_local.path {
1007                        let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name());
1008                        (
1009                            KclValue::Function {
1010                                value: Box::new(FunctionSource::rust(func, function_expression.clone(), props, attrs)),
1011                                meta: vec![metadata.to_owned()],
1012                            },
1013                            None,
1014                        )
1015                    } else {
1016                        return Err(KclError::new_semantic(KclErrorDetails::new(
1017                            "Rust implementation of functions is restricted to the standard library".to_owned(),
1018                            vec![metadata.source_range],
1019                        )));
1020                    }
1021                } else {
1022                    // Snapshotting memory here is crucial for semantics so that we close
1023                    // over variables. Variables defined lexically later shouldn't
1024                    // be available to the function body.
1025                    let (env_ref, placeholder_env_ref) = if function_expression.name.is_some() {
1026                        // Recursive function needs a snapshot that includes
1027                        // itself.
1028                        let dummy = EnvironmentRef::dummy();
1029                        (dummy, Some(dummy))
1030                    } else {
1031                        (exec_state.mut_stack().snapshot(), None)
1032                    };
1033                    (
1034                        KclValue::Function {
1035                            value: Box::new(FunctionSource::kcl(
1036                                function_expression.clone(),
1037                                env_ref,
1038                                KclFunctionSourceParams {
1039                                    is_std,
1040                                    experimental,
1041                                    include_in_feature_tree,
1042                                },
1043                            )),
1044                            meta: vec![metadata.to_owned()],
1045                        },
1046                        placeholder_env_ref,
1047                    )
1048                };
1049
1050                // If the function expression has a name, i.e. `fn name() {}`,
1051                // bind it in the current scope.
1052                if let Some(fn_name) = &function_expression.name {
1053                    // If we used a placeholder env ref for recursion, fix it up
1054                    // with the name recursively bound so that it's available in
1055                    // the function body.
1056                    if let Some(placeholder_env_ref) = placeholder_env_ref {
1057                        closure = exec_state.mut_stack().add_recursive_closure(
1058                            fn_name.name.to_owned(),
1059                            closure,
1060                            placeholder_env_ref,
1061                            metadata.source_range,
1062                        )?;
1063                    } else {
1064                        // Regular non-recursive binding.
1065                        exec_state
1066                            .mut_stack()
1067                            .add(fn_name.name.clone(), closure.clone(), metadata.source_range)?;
1068                    }
1069                }
1070
1071                closure.continue_()
1072            }
1073            Expr::CallExpressionKw(call_expression) => call_expression.execute(exec_state, self).await?,
1074            Expr::PipeExpression(pipe_expression) => pipe_expression.get_result(exec_state, self).await?,
1075            Expr::PipeSubstitution(pipe_substitution) => match statement_kind {
1076                StatementKind::Declaration { name } => {
1077                    let message = format!(
1078                        "you cannot declare variable {name} as %, because % can only be used in function calls"
1079                    );
1080
1081                    return Err(KclError::new_semantic(KclErrorDetails::new(
1082                        message,
1083                        vec![pipe_substitution.into()],
1084                    )));
1085                }
1086                StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
1087                    Some(x) => x.continue_(),
1088                    None => {
1089                        return Err(KclError::new_semantic(KclErrorDetails::new(
1090                            "cannot use % outside a pipe expression".to_owned(),
1091                            vec![pipe_substitution.into()],
1092                        )));
1093                    }
1094                },
1095            },
1096            Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
1097            Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?,
1098            Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
1099            Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state, self).await?,
1100            Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
1101            Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
1102            Expr::LabelledExpression(expr) => {
1103                let value_cf = self
1104                    .execute_expr(&expr.expr, exec_state, metadata, &[], statement_kind)
1105                    .await?;
1106                let value = control_continue!(value_cf);
1107                exec_state
1108                    .mut_stack()
1109                    .add(expr.label.name.clone(), value.clone(), init.into())?;
1110                // TODO this lets us use the label as a variable name, but not as a tag in most cases
1111                value.continue_()
1112            }
1113            Expr::AscribedExpression(expr) => expr.get_result(exec_state, self).await?,
1114            Expr::SketchBlock(expr) => expr.get_result(exec_state, self).await?,
1115            Expr::SketchVar(expr) => expr.get_result(exec_state, self).await?.continue_(),
1116        };
1117        Ok(item)
1118    }
1119}
1120
1121/// When executing in sketch mode, whether we should skip executing this
1122/// expression.
1123fn sketch_mode_should_skip(expr: &Expr) -> bool {
1124    match expr {
1125        Expr::SketchBlock(sketch_block) => !sketch_block.is_being_edited,
1126        _ => true,
1127    }
1128}
1129
1130/// If the error is about an undefined name, and that name matches the name being defined,
1131/// make the error message more specific.
1132fn var_in_own_ref_err(e: KclError, being_declared: &Option<String>) -> KclError {
1133    let KclError::UndefinedValue { name, mut details } = e else {
1134        return e;
1135    };
1136    // TODO after June 26th: replace this with a let-chain,
1137    // which will be available in Rust 1.88
1138    // https://rust-lang.github.io/rfcs/2497-if-let-chains.html
1139    if let (Some(name0), Some(name1)) = (&being_declared, &name)
1140        && name0 == name1
1141    {
1142        details.message = format!(
1143            "You can't use `{name0}` because you're currently trying to define it. Use a different variable here instead."
1144        );
1145    }
1146    KclError::UndefinedValue { details, name }
1147}
1148
1149impl Node<AscribedExpression> {
1150    #[async_recursion]
1151    pub(super) async fn get_result(
1152        &self,
1153        exec_state: &mut ExecState,
1154        ctx: &ExecutorContext,
1155    ) -> Result<KclValueControlFlow, KclError> {
1156        let metadata = Metadata {
1157            source_range: SourceRange::from(self),
1158        };
1159        let result = ctx
1160            .execute_expr(&self.expr, exec_state, &metadata, &[], StatementKind::Expression)
1161            .await?;
1162        let result = control_continue!(result);
1163        apply_ascription(&result, &self.ty, exec_state, self.into()).map(KclValue::continue_)
1164    }
1165}
1166
1167impl Node<SketchBlock> {
1168    pub(super) async fn get_result(
1169        &self,
1170        exec_state: &mut ExecState,
1171        ctx: &ExecutorContext,
1172    ) -> Result<KclValueControlFlow, KclError> {
1173        if exec_state.mod_local.sketch_block.is_some() {
1174            // Disallow nested sketch blocks for now.
1175            return Err(KclError::new_semantic(KclErrorDetails::new(
1176                "Cannot execute a sketch block from within another sketch block".to_owned(),
1177                vec![SourceRange::from(self)],
1178            )));
1179        }
1180
1181        let range = SourceRange::from(self);
1182
1183        // Evaluate arguments.
1184        let (sketch_id, sketch_surface) = match self.exec_arguments(exec_state, ctx).await {
1185            Ok(x) => x,
1186            Err(cf_error) => match cf_error {
1187                // Control flow needs to return early.
1188                EarlyReturn::Value(cf_value) => return Ok(cf_value),
1189                EarlyReturn::Error(err) => return Err(err),
1190            },
1191        };
1192        let on_object_id = if let Some(object_id) = sketch_surface.object_id() {
1193            object_id
1194        } else {
1195            let message = "The `on` argument should have an object after ensure_sketch_plane_in_engine".to_owned();
1196            debug_assert!(false, "{message}");
1197            return Err(internal_err(message, range));
1198        };
1199        #[cfg(not(feature = "artifact-graph"))]
1200        let _ = on_object_id;
1201        #[cfg(not(feature = "artifact-graph"))]
1202        let _ = sketch_id;
1203        #[cfg(feature = "artifact-graph")]
1204        let sketch_ctor_on = sketch_on_frontend_plane(&self.arguments, on_object_id);
1205        #[cfg(feature = "artifact-graph")]
1206        {
1207            use crate::execution::{Artifact, ArtifactId, CodeRef, SketchBlock};
1208
1209            let on_object = exec_state.mod_local.artifacts.scene_object_by_id(on_object_id);
1210
1211            // Get the plane artifact ID so that we can do an exclusive borrow.
1212            let plane_artifact_id = on_object.map(|object| object.artifact_id);
1213
1214            let artifact_id = ArtifactId::from(exec_state.next_uuid());
1215            // Create the sketch scene object and replace its placeholder.
1216            let sketch_scene_object = Object {
1217                id: sketch_id,
1218                kind: ObjectKind::Sketch(crate::frontend::sketch::Sketch {
1219                    args: crate::front::SketchCtor { on: sketch_ctor_on },
1220                    plane: on_object_id,
1221                    segments: Default::default(),
1222                    constraints: Default::default(),
1223                }),
1224                label: Default::default(),
1225                comments: Default::default(),
1226                artifact_id,
1227                source: range.into(),
1228            };
1229            exec_state.set_scene_object(sketch_scene_object);
1230
1231            // Create and add the sketch block artifact
1232            exec_state.add_artifact(Artifact::SketchBlock(SketchBlock {
1233                id: artifact_id,
1234                plane_id: plane_artifact_id,
1235                code_ref: CodeRef::placeholder(range),
1236                sketch_id,
1237            }));
1238        }
1239
1240        let (return_result, variables, sketch_block_state) = {
1241            // Don't early return until the stack frame is popped!
1242            self.prep_mem(exec_state.mut_stack().snapshot(), exec_state);
1243
1244            // Track that we're executing a sketch block.
1245            #[cfg(feature = "artifact-graph")]
1246            let initial_sketch_block_state = {
1247                SketchBlockState {
1248                    sketch_id: Some(sketch_id),
1249                    ..Default::default()
1250                }
1251            };
1252            #[cfg(not(feature = "artifact-graph"))]
1253            let initial_sketch_block_state = SketchBlockState::default();
1254
1255            let original_value = exec_state.mod_local.sketch_block.replace(initial_sketch_block_state);
1256
1257            // When executing the body of the sketch block, we no longer want to
1258            // skip any code.
1259            let original_sketch_mode = std::mem::replace(&mut exec_state.mod_local.sketch_mode, false);
1260
1261            // Load `sketch2::*` into the sketch block's parent scope, so calls
1262            // like `line(...)` resolve to sketch2 functions. Then execute the
1263            // user body in a child scope, so these aliases aren't included in
1264            // the returned sketch object.
1265            let (result, block_variables) = match self.load_sketch2_into_current_scope(exec_state, ctx, range).await {
1266                Ok(()) => {
1267                    let parent = exec_state.mut_stack().snapshot();
1268                    exec_state.mut_stack().push_new_env_for_call(parent);
1269                    let result = ctx.exec_block(&self.body, exec_state, BodyType::Block).await;
1270                    let block_variables = exec_state
1271                        .stack()
1272                        .find_all_in_current_env()
1273                        .map(|(name, value)| (name.clone(), value.clone()))
1274                        .collect::<IndexMap<_, _>>();
1275                    exec_state.mut_stack().pop_env();
1276                    (result, block_variables)
1277                }
1278                Err(err) => (Err(err), IndexMap::new()),
1279            };
1280
1281            exec_state.mod_local.sketch_mode = original_sketch_mode;
1282
1283            let sketch_block_state = std::mem::replace(&mut exec_state.mod_local.sketch_block, original_value);
1284
1285            // Pop the scope used for sketch2 aliases.
1286            exec_state.mut_stack().pop_env();
1287
1288            (result, block_variables, sketch_block_state)
1289        };
1290
1291        // Propagate errors.
1292        return_result?;
1293        let Some(sketch_block_state) = sketch_block_state else {
1294            debug_assert!(false, "Sketch block state should still be set to Some from just above");
1295            return Err(internal_err(
1296                "Sketch block state should still be set to Some from just above",
1297                self,
1298            ));
1299        };
1300        #[cfg(feature = "artifact-graph")]
1301        let mut sketch_block_state = sketch_block_state;
1302
1303        // Translate sketch variables and constraints to solver input.
1304        let constraints = sketch_block_state
1305            .solver_constraints
1306            .iter()
1307            .cloned()
1308            .map(kcl_ezpz::ConstraintRequest::highest_priority)
1309            .chain(
1310                // Optional constraints have a lower priority.
1311                sketch_block_state
1312                    .solver_optional_constraints
1313                    .iter()
1314                    .cloned()
1315                    .map(|c| kcl_ezpz::ConstraintRequest::new(c, 1)),
1316            )
1317            .collect::<Vec<_>>();
1318        let initial_guesses = sketch_block_state
1319            .sketch_vars
1320            .iter()
1321            .map(|v| {
1322                let Some(sketch_var) = v.as_sketch_var() else {
1323                    return Err(internal_err("Expected sketch variable", self));
1324                };
1325                let constraint_id = sketch_var.id.to_constraint_id(range)?;
1326                // Normalize units.
1327                let number_value = KclValue::Number {
1328                    value: sketch_var.initial_value,
1329                    ty: sketch_var.ty,
1330                    meta: sketch_var.meta.clone(),
1331                };
1332                let initial_guess_value = normalize_to_solver_distance_unit(
1333                    &number_value,
1334                    v.into(),
1335                    exec_state,
1336                    "sketch variable initial value",
1337                )?;
1338                let initial_guess = if let Some(n) = initial_guess_value.as_ty_f64() {
1339                    n.n
1340                } else {
1341                    let message = format!(
1342                        "Expected number after coercion, but found {}",
1343                        initial_guess_value.human_friendly_type()
1344                    );
1345                    debug_assert!(false, "{}", &message);
1346                    return Err(internal_err(message, self));
1347                };
1348                Ok((constraint_id, initial_guess))
1349            })
1350            .collect::<Result<Vec<_>, KclError>>()?;
1351        // Solve constraints.
1352        let config = kcl_ezpz::Config::default().with_max_iterations(50);
1353        let solve_result = if exec_state.mod_local.freedom_analysis {
1354            kcl_ezpz::solve_analysis(&constraints, initial_guesses.clone(), config).map(|outcome| {
1355                let freedom_analysis = FreedomAnalysis::from_ezpz_analysis(outcome.analysis, constraints.len());
1356                (outcome.outcome, Some(freedom_analysis))
1357            })
1358        } else {
1359            kcl_ezpz::solve(&constraints, initial_guesses.clone(), config).map(|outcome| (outcome, None))
1360        };
1361        // Build a combined list of all constraints (regular + optional) for conflict detection
1362        let num_required_constraints = sketch_block_state.solver_constraints.len();
1363        let all_constraints: Vec<kcl_ezpz::Constraint> = sketch_block_state
1364            .solver_constraints
1365            .iter()
1366            .cloned()
1367            .chain(sketch_block_state.solver_optional_constraints.iter().cloned())
1368            .collect();
1369
1370        let (solve_outcome, solve_analysis) = match solve_result {
1371            Ok((solved, freedom)) => {
1372                let outcome = Solved::from_ezpz_outcome(solved, &all_constraints, num_required_constraints);
1373                (outcome, freedom)
1374            }
1375            Err(failure) => {
1376                match &failure.error {
1377                    NonLinearSystemError::FaerMatrix { .. }
1378                    | NonLinearSystemError::Faer { .. }
1379                    | NonLinearSystemError::FaerSolve { .. }
1380                    | NonLinearSystemError::FaerSvd(..)
1381                    | NonLinearSystemError::DidNotConverge => {
1382                        // Constraint solver failed to find a solution. Build a
1383                        // solution that is the initial guesses.
1384                        exec_state.warn(
1385                            CompilationError::err(range, "Constraint solver failed to find a solution".to_owned()),
1386                            annotations::WARN_SOLVER,
1387                        );
1388                        let final_values = initial_guesses.iter().map(|(_, v)| *v).collect::<Vec<_>>();
1389                        (
1390                            Solved {
1391                                final_values,
1392                                iterations: Default::default(),
1393                                warnings: failure.warnings,
1394                                priority_solved: Default::default(),
1395                                variables_in_conflicts: Default::default(),
1396                            },
1397                            None,
1398                        )
1399                    }
1400                    NonLinearSystemError::EmptySystemNotAllowed
1401                    | NonLinearSystemError::WrongNumberGuesses { .. }
1402                    | NonLinearSystemError::MissingGuess { .. }
1403                    | NonLinearSystemError::NotFound(..) => {
1404                        // These indicate something's gone wrong in KCL or ezpz,
1405                        // it's not a user error. We should investigate this.
1406                        #[cfg(target_arch = "wasm32")]
1407                        web_sys::console::error_1(
1408                            &format!("Internal error from constraint solver: {}", &failure.error).into(),
1409                        );
1410                        return Err(internal_err(
1411                            format!("Internal error from constraint solver: {}", &failure.error),
1412                            self,
1413                        ));
1414                    }
1415                    _ => {
1416                        // Catch all error case so that it's not a breaking change to publish new errors.
1417                        return Err(internal_err(
1418                            format!("Error from constraint solver: {}", &failure.error),
1419                            self,
1420                        ));
1421                    }
1422                }
1423            }
1424        };
1425        #[cfg(not(feature = "artifact-graph"))]
1426        let _ = solve_analysis;
1427        // Propagate warnings.
1428        for warning in &solve_outcome.warnings {
1429            let message = if let Some(index) = warning.about_constraint.as_ref() {
1430                format!("{}; constraint index {}", &warning.content, index)
1431            } else {
1432                format!("{}", &warning.content)
1433            };
1434            exec_state.warn(CompilationError::err(range, message), annotations::WARN_SOLVER);
1435        }
1436        // Substitute solutions back into sketch variables.
1437        let sketch_engine_id = exec_state.next_uuid();
1438        let solution_ty = solver_numeric_type(exec_state);
1439        let mut solved_segments = Vec::with_capacity(sketch_block_state.needed_by_engine.len());
1440        for unsolved_segment in &sketch_block_state.needed_by_engine {
1441            solved_segments.push(substitute_sketch_var_in_segment(
1442                unsolved_segment.clone(),
1443                &sketch_surface,
1444                sketch_engine_id,
1445                None,
1446                &solve_outcome,
1447                solver_numeric_type(exec_state),
1448                solve_analysis.as_ref(),
1449            )?);
1450        }
1451        #[cfg(feature = "artifact-graph")]
1452        {
1453            // Store variable solutions so that the sketch refactoring API can
1454            // write them back to the source. When editing a sketch block, we
1455            // exit early so that the sketch block that we're editing is always
1456            // the last one. Therefore, we should overwrite any previous
1457            // solutions.
1458            exec_state.mod_local.artifacts.var_solutions =
1459                sketch_block_state.var_solutions(&solve_outcome, solution_ty, SourceRange::from(self))?;
1460        }
1461
1462        // Create scene objects after unknowns are solved.
1463        let scene_objects = create_segment_scene_objects(&solved_segments, range, exec_state)?;
1464
1465        // Build the sketch and send everything to the engine.
1466        let sketch = create_segments_in_engine(
1467            &sketch_surface,
1468            sketch_engine_id,
1469            &mut solved_segments,
1470            &sketch_block_state.segment_tags,
1471            ctx,
1472            exec_state,
1473            range,
1474        )
1475        .await?;
1476
1477        // Substitute solutions back into sketch variables. This time, collect
1478        // all the variables in the sketch block. The set of variables may have
1479        // overlap with the objects sent to the engine, but it isn't necessarily
1480        // the same.
1481        let variables = substitute_sketch_vars(
1482            variables,
1483            &sketch_surface,
1484            sketch_engine_id,
1485            sketch.as_ref(),
1486            &solve_outcome,
1487            solution_ty,
1488            solve_analysis.as_ref(),
1489        )?;
1490
1491        #[cfg(not(feature = "artifact-graph"))]
1492        drop(scene_objects);
1493        #[cfg(feature = "artifact-graph")]
1494        {
1495            let mut segment_object_ids = Vec::with_capacity(scene_objects.len());
1496            for scene_object in scene_objects {
1497                segment_object_ids.push(scene_object.id);
1498                // Fill in placeholder scene objects.
1499                exec_state.set_scene_object(scene_object);
1500            }
1501            // Update the sketch scene object with the segments.
1502            let Some(sketch_object) = exec_state.mod_local.artifacts.scene_object_by_id_mut(sketch_id) else {
1503                let message = format!("Sketch object not found after it was just created; id={:?}", sketch_id);
1504                debug_assert!(false, "{}", &message);
1505                return Err(internal_err(message, range));
1506            };
1507            let ObjectKind::Sketch(sketch) = &mut sketch_object.kind else {
1508                let message = format!(
1509                    "Expected Sketch object after it was just created to be a sketch kind; id={:?}, actual={:?}",
1510                    sketch_id, sketch_object
1511                );
1512                debug_assert!(
1513                    false,
1514                    "{}; scene_objects={:#?}",
1515                    &message, &exec_state.mod_local.artifacts.scene_objects
1516                );
1517                return Err(internal_err(message, range));
1518            };
1519            sketch.segments.extend(segment_object_ids);
1520            // Update the sketch scene object with constraints.
1521            sketch
1522                .constraints
1523                .extend(std::mem::take(&mut sketch_block_state.sketch_constraints));
1524
1525            // Push sketch solve operation
1526            exec_state.push_op(Operation::SketchSolve {
1527                sketch_id,
1528                node_path: NodePath::placeholder(),
1529                source_range: range,
1530            });
1531        }
1532
1533        let properties = self.sketch_properties(sketch, variables);
1534        let metadata = Metadata {
1535            source_range: SourceRange::from(self),
1536        };
1537        let return_value = KclValue::Object {
1538            value: properties,
1539            constrainable: Default::default(),
1540            meta: vec![metadata],
1541        };
1542        Ok(if self.is_being_edited {
1543            // When the sketch block is being edited, we exit the program
1544            // immediately.
1545            return_value.exit()
1546        } else {
1547            return_value.continue_()
1548        })
1549    }
1550
1551    /// Executes the arguments of the sketch block and returns the sketch ID and
1552    /// surface. The surface is the `on` argument, which is basically a Plane or
1553    /// Face.
1554    ///
1555    /// In sketch mode, the execution cache is used to look up the sketch
1556    /// surface.
1557    ///
1558    /// The sketch ID is generated in either case so that it's stable. But only
1559    /// a placeholder scene object is created for it.
1560    async fn exec_arguments(
1561        &self,
1562        exec_state: &mut ExecState,
1563        ctx: &ExecutorContext,
1564    ) -> Result<(ObjectId, SketchSurface), EarlyReturn> {
1565        let range = SourceRange::from(self);
1566
1567        if !exec_state.sketch_mode() {
1568            // Evaluate arguments.
1569            //
1570            // Sketch mode only executes the sketch block body. Arguments must
1571            // be evaluated in engine execution so that things like Planes and
1572            // Faces can be created in the engine.
1573            let mut labeled = IndexMap::new();
1574            for labeled_arg in &self.arguments {
1575                let source_range = SourceRange::from(labeled_arg.arg.clone());
1576                let metadata = Metadata { source_range };
1577                let value_cf = ctx
1578                    .execute_expr(&labeled_arg.arg, exec_state, &metadata, &[], StatementKind::Expression)
1579                    .await?;
1580                let value = early_return!(value_cf);
1581                let arg = Arg::new(value, source_range);
1582                match &labeled_arg.label {
1583                    Some(label) => {
1584                        labeled.insert(label.name.clone(), arg);
1585                    }
1586                    None => {
1587                        let name = labeled_arg.arg.ident_name();
1588                        if let Some(name) = name {
1589                            labeled.insert(name.to_owned(), arg);
1590                        } else {
1591                            return Err(KclError::new_semantic(KclErrorDetails::new(
1592                                "Arguments to sketch blocks must be either labeled or simple identifiers".to_owned(),
1593                                vec![SourceRange::from(&labeled_arg.arg)],
1594                            ))
1595                            .into());
1596                        }
1597                    }
1598                }
1599            }
1600            let mut args = Args::new_no_args(range, ctx.clone(), Some("sketch block".to_owned()));
1601            args.labeled = labeled;
1602
1603            let arg_on_value: KclValue =
1604                args.get_kw_arg(SKETCH_BLOCK_PARAM_ON, &RuntimeType::sketch_or_surface(), exec_state)?;
1605
1606            let Some(arg_on) = SketchOrSurface::from_kcl_val(&arg_on_value) else {
1607                let message =
1608                    "The `on` argument to a sketch block must be convertible to a sketch or surface.".to_owned();
1609                debug_assert!(false, "{message}");
1610                return Err(KclError::new_semantic(KclErrorDetails::new(message, vec![range])).into());
1611            };
1612            let mut sketch_surface = arg_on.into_sketch_surface();
1613
1614            // Ensure that the plane has an ObjectId. Always create an Object so
1615            // that we're consistent with IDs.
1616            match &mut sketch_surface {
1617                SketchSurface::Plane(plane) => {
1618                    // Ensure that it's been created in the engine.
1619                    ensure_sketch_plane_in_engine(plane, exec_state, ctx, range).await?;
1620                }
1621                SketchSurface::Face(_) => {
1622                    // All faces should already be created in the engine.
1623                }
1624            }
1625
1626            // Generate an ID for the sketch block. This must be done after
1627            // arguments so that we get the same result when the arguments are
1628            // cached. This must be done before the sketch block body so that no
1629            // matter how many IDs are generated due to objects in the body, the
1630            // sketch ID is always stable.
1631            let sketch_id = exec_state.next_object_id();
1632            #[cfg(feature = "artifact-graph")]
1633            exec_state.add_placeholder_scene_object(sketch_id, range);
1634            let on_cache_name = sketch_on_cache_name(sketch_id);
1635            // Store in memory so that it's cached.
1636            exec_state.mut_stack().add(on_cache_name, arg_on_value, range)?;
1637
1638            Ok((sketch_id, sketch_surface))
1639        } else {
1640            // In sketch mode, we can't re-evaluate arguments. Instead, look
1641            // them up from cache.
1642
1643            // Generate an ID for the sketch block. This must be done before the
1644            // sketch block body so that no matter how many IDs are generated
1645            // due to objects in the body, the sketch ID is always stable.
1646            let sketch_id = exec_state.next_object_id();
1647            #[cfg(feature = "artifact-graph")]
1648            exec_state.add_placeholder_scene_object(sketch_id, range);
1649            let on_cache_name = sketch_on_cache_name(sketch_id);
1650            let arg_on_value = exec_state.stack().get(&on_cache_name, range)?.clone();
1651
1652            let Some(arg_on) = SketchOrSurface::from_kcl_val(&arg_on_value) else {
1653                let message =
1654                    "The `on` argument to a sketch block must be convertible to a sketch or surface.".to_owned();
1655                debug_assert!(false, "{message}");
1656                return Err(KclError::new_semantic(KclErrorDetails::new(message, vec![range])).into());
1657            };
1658            let mut sketch_surface = arg_on.into_sketch_surface();
1659
1660            // Ensure that the plane has an ObjectId. Always create an Object so
1661            // that we're consistent with IDs.
1662            if sketch_surface.object_id().is_none() {
1663                #[cfg(not(feature = "artifact-graph"))]
1664                {
1665                    // Without artifact graph, we just create a new object ID.
1666                    // It will never be used for anything meaningful.
1667                    sketch_surface.set_object_id(exec_state.next_object_id());
1668                }
1669                #[cfg(feature = "artifact-graph")]
1670                {
1671                    // Look up the last object. Since this is where we would have
1672                    // created it in real execution, it will be the last object.
1673                    let Some(last_object) = exec_state.mod_local.artifacts.scene_objects.last() else {
1674                        return Err(internal_err(
1675                            "In sketch mode, the `on` plane argument must refer to an existing plane object.",
1676                            range,
1677                        )
1678                        .into());
1679                    };
1680                    sketch_surface.set_object_id(last_object.id);
1681                }
1682            }
1683
1684            Ok((sketch_id, sketch_surface))
1685        }
1686    }
1687
1688    async fn load_sketch2_into_current_scope(
1689        &self,
1690        exec_state: &mut ExecState,
1691        ctx: &ExecutorContext,
1692        source_range: SourceRange,
1693    ) -> Result<(), KclError> {
1694        let path = vec!["std".to_owned(), "sketch2".to_owned()];
1695        let resolved_path = ModulePath::from_std_import_path(&path)?;
1696        let module_id = ctx
1697            .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1698            .await?;
1699        let (env_ref, exports) = ctx.exec_module_for_items(module_id, exec_state, source_range).await?;
1700
1701        for name in exports {
1702            let value = exec_state
1703                .stack()
1704                .memory
1705                .get_from(&name, env_ref, source_range, 0)?
1706                .clone();
1707            exec_state.mut_stack().add(name, value, source_range)?;
1708        }
1709        Ok(())
1710    }
1711
1712    /// Augment the variables in the sketch block with properties that should be
1713    /// accessible on the returned sketch object. This includes metadata like
1714    /// the sketch so that the engine ID and surface can be accessed.
1715    pub(crate) fn sketch_properties(
1716        &self,
1717        sketch: Option<Sketch>,
1718        variables: HashMap<String, KclValue>,
1719    ) -> HashMap<String, KclValue> {
1720        let Some(sketch) = sketch else {
1721            // The sketch block did not produce a Sketch, so we cannot provide
1722            // it.
1723            return variables;
1724        };
1725
1726        let mut properties = variables;
1727
1728        let sketch_value = KclValue::Sketch {
1729            value: Box::new(sketch),
1730        };
1731        let mut meta_map = HashMap::with_capacity(1);
1732        meta_map.insert(SKETCH_OBJECT_META_SKETCH.to_owned(), sketch_value);
1733        let meta_value = KclValue::Object {
1734            value: meta_map,
1735            constrainable: false,
1736            meta: vec![Metadata {
1737                source_range: SourceRange::from(self),
1738            }],
1739        };
1740
1741        properties.insert(SKETCH_OBJECT_META.to_owned(), meta_value);
1742
1743        properties
1744    }
1745}
1746
1747impl SketchBlock {
1748    fn prep_mem(&self, parent: EnvironmentRef, exec_state: &mut ExecState) {
1749        exec_state.mut_stack().push_new_env_for_call(parent);
1750    }
1751}
1752
1753impl Node<SketchVar> {
1754    pub async fn get_result(&self, exec_state: &mut ExecState, _ctx: &ExecutorContext) -> Result<KclValue, KclError> {
1755        let Some(sketch_block_state) = &exec_state.mod_local.sketch_block else {
1756            return Err(KclError::new_semantic(KclErrorDetails::new(
1757                "Cannot use a sketch variable outside of a sketch block".to_owned(),
1758                vec![SourceRange::from(self)],
1759            )));
1760        };
1761        let id = sketch_block_state.next_sketch_var_id();
1762        let sketch_var = if let Some(initial) = &self.initial {
1763            KclValue::from_sketch_var_literal(initial, id, exec_state)
1764        } else {
1765            let metadata = Metadata {
1766                source_range: SourceRange::from(self),
1767            };
1768
1769            KclValue::SketchVar {
1770                value: Box::new(super::SketchVar {
1771                    id,
1772                    initial_value: 0.0,
1773                    ty: NumericType::default(),
1774                    meta: vec![metadata],
1775                }),
1776            }
1777        };
1778
1779        let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
1780            return Err(KclError::new_semantic(KclErrorDetails::new(
1781                "Cannot use a sketch variable outside of a sketch block".to_owned(),
1782                vec![SourceRange::from(self)],
1783            )));
1784        };
1785        sketch_block_state.sketch_vars.push(sketch_var.clone());
1786
1787        Ok(sketch_var)
1788    }
1789}
1790
1791fn apply_ascription(
1792    value: &KclValue,
1793    ty: &Node<Type>,
1794    exec_state: &mut ExecState,
1795    source_range: SourceRange,
1796) -> Result<KclValue, KclError> {
1797    let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into(), false, false)
1798        .map_err(|e| KclError::new_semantic(e.into()))?;
1799
1800    if matches!(&ty, &RuntimeType::Primitive(PrimitiveType::Number(..))) {
1801        exec_state.clear_units_warnings(&source_range);
1802    }
1803
1804    value.coerce(&ty, false, exec_state).map_err(|_| {
1805        let suggestion = if ty == RuntimeType::length() {
1806            ", you might try coercing to a fully specified numeric type such as `mm`"
1807        } else if ty == RuntimeType::angle() {
1808            ", you might try coercing to a fully specified numeric type such as `deg`"
1809        } else {
1810            ""
1811        };
1812        let ty_str = if let Some(ty) = value.principal_type() {
1813            format!("(with type `{ty}`) ")
1814        } else {
1815            String::new()
1816        };
1817        KclError::new_semantic(KclErrorDetails::new(
1818            format!(
1819                "could not coerce {} {ty_str}to type `{ty}`{suggestion}",
1820                value.human_friendly_type()
1821            ),
1822            vec![source_range],
1823        ))
1824    })
1825}
1826
1827impl BinaryPart {
1828    #[async_recursion]
1829    pub(super) async fn get_result(
1830        &self,
1831        exec_state: &mut ExecState,
1832        ctx: &ExecutorContext,
1833    ) -> Result<KclValueControlFlow, KclError> {
1834        match self {
1835            BinaryPart::Literal(literal) => Ok(KclValue::from_literal((**literal).clone(), exec_state).continue_()),
1836            BinaryPart::Name(name) => name.get_result(exec_state, ctx).await.cloned().map(KclValue::continue_),
1837            BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await,
1838            BinaryPart::CallExpressionKw(call_expression) => call_expression.execute(exec_state, ctx).await,
1839            BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await,
1840            BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state, ctx).await,
1841            BinaryPart::ArrayExpression(e) => e.execute(exec_state, ctx).await,
1842            BinaryPart::ArrayRangeExpression(e) => e.execute(exec_state, ctx).await,
1843            BinaryPart::ObjectExpression(e) => e.execute(exec_state, ctx).await,
1844            BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await,
1845            BinaryPart::AscribedExpression(e) => e.get_result(exec_state, ctx).await,
1846            BinaryPart::SketchVar(e) => e.get_result(exec_state, ctx).await.map(KclValue::continue_),
1847        }
1848    }
1849}
1850
1851impl Node<Name> {
1852    pub(super) async fn get_result<'a>(
1853        &self,
1854        exec_state: &'a mut ExecState,
1855        ctx: &ExecutorContext,
1856    ) -> Result<&'a KclValue, KclError> {
1857        let being_declared = exec_state.mod_local.being_declared.clone();
1858        self.get_result_inner(exec_state, ctx)
1859            .await
1860            .map_err(|e| var_in_own_ref_err(e, &being_declared))
1861    }
1862
1863    async fn get_result_inner<'a>(
1864        &self,
1865        exec_state: &'a mut ExecState,
1866        ctx: &ExecutorContext,
1867    ) -> Result<&'a KclValue, KclError> {
1868        if self.abs_path {
1869            return Err(KclError::new_semantic(KclErrorDetails::new(
1870                "Absolute paths (names beginning with `::` are not yet supported)".to_owned(),
1871                self.as_source_ranges(),
1872            )));
1873        }
1874
1875        let mod_name = format!("{}{}", memory::MODULE_PREFIX, self.name.name);
1876
1877        if self.path.is_empty() {
1878            let item_value = exec_state.stack().get(&self.name.name, self.into());
1879            if item_value.is_ok() {
1880                return item_value;
1881            }
1882            return exec_state.stack().get(&mod_name, self.into());
1883        }
1884
1885        let mut mem_spec: Option<(EnvironmentRef, Vec<String>)> = None;
1886        for p in &self.path {
1887            let value = match mem_spec {
1888                Some((env, exports)) => {
1889                    if !exports.contains(&p.name) {
1890                        return Err(KclError::new_semantic(KclErrorDetails::new(
1891                            format!("Item {} not found in module's exported items", p.name),
1892                            p.as_source_ranges(),
1893                        )));
1894                    }
1895
1896                    exec_state
1897                        .stack()
1898                        .memory
1899                        .get_from(&p.name, env, p.as_source_range(), 0)?
1900                }
1901                None => exec_state
1902                    .stack()
1903                    .get(&format!("{}{}", memory::MODULE_PREFIX, p.name), self.into())?,
1904            };
1905
1906            let KclValue::Module { value: module_id, .. } = value else {
1907                return Err(KclError::new_semantic(KclErrorDetails::new(
1908                    format!(
1909                        "Identifier in path must refer to a module, found {}",
1910                        value.human_friendly_type()
1911                    ),
1912                    p.as_source_ranges(),
1913                )));
1914            };
1915
1916            mem_spec = Some(
1917                ctx.exec_module_for_items(*module_id, exec_state, p.as_source_range())
1918                    .await?,
1919            );
1920        }
1921
1922        let (env, exports) = mem_spec.unwrap();
1923
1924        let item_exported = exports.contains(&self.name.name);
1925        let item_value = exec_state
1926            .stack()
1927            .memory
1928            .get_from(&self.name.name, env, self.name.as_source_range(), 0);
1929
1930        // Item is defined and exported.
1931        if item_exported && item_value.is_ok() {
1932            return item_value;
1933        }
1934
1935        let mod_exported = exports.contains(&mod_name);
1936        let mod_value = exec_state
1937            .stack()
1938            .memory
1939            .get_from(&mod_name, env, self.name.as_source_range(), 0);
1940
1941        // Module is defined and exported.
1942        if mod_exported && mod_value.is_ok() {
1943            return mod_value;
1944        }
1945
1946        // Neither item or module is defined.
1947        if item_value.is_err() && mod_value.is_err() {
1948            return item_value;
1949        }
1950
1951        // Either item or module is defined, but not exported.
1952        debug_assert!((item_value.is_ok() && !item_exported) || (mod_value.is_ok() && !mod_exported));
1953        Err(KclError::new_semantic(KclErrorDetails::new(
1954            format!("Item {} not found in module's exported items", self.name.name),
1955            self.name.as_source_ranges(),
1956        )))
1957    }
1958}
1959
1960impl Node<MemberExpression> {
1961    async fn get_result(
1962        &self,
1963        exec_state: &mut ExecState,
1964        ctx: &ExecutorContext,
1965    ) -> Result<KclValueControlFlow, KclError> {
1966        let meta = Metadata {
1967            source_range: SourceRange::from(self),
1968        };
1969        // TODO: The order of execution is wrong. We should execute the object
1970        // *before* the property.
1971        let property = Property::try_from(
1972            self.computed,
1973            self.property.clone(),
1974            exec_state,
1975            self.into(),
1976            ctx,
1977            &meta,
1978            &[],
1979            StatementKind::Expression,
1980        )
1981        .await?;
1982        let object_cf = ctx
1983            .execute_expr(&self.object, exec_state, &meta, &[], StatementKind::Expression)
1984            .await?;
1985        let object = control_continue!(object_cf);
1986
1987        // Check the property and object match -- e.g. ints for arrays, strs for objects.
1988        match (object, property, self.computed) {
1989            (KclValue::Segment { value: segment }, Property::String(property), false) => match property.as_str() {
1990                "at" => match &segment.repr {
1991                    SegmentRepr::Unsolved { segment } => {
1992                        match &segment.kind {
1993                            UnsolvedSegmentKind::Point { position, .. } => {
1994                                // TODO: assert that types of all elements are the same.
1995                                Ok(KclValue::HomArray {
1996                                    value: vec![
1997                                        KclValue::from_unsolved_expr(position[0].clone(), segment.meta.clone()),
1998                                        KclValue::from_unsolved_expr(position[1].clone(), segment.meta.clone()),
1999                                    ],
2000                                    ty: RuntimeType::any(),
2001                                }
2002                                .continue_())
2003                            }
2004                            _ => Err(KclError::new_undefined_value(
2005                                KclErrorDetails::new(
2006                                    format!("Property '{property}' not found in segment"),
2007                                    vec![self.clone().into()],
2008                                ),
2009                                None,
2010                            )),
2011                        }
2012                    }
2013                    SegmentRepr::Solved { segment } => {
2014                        match &segment.kind {
2015                            SegmentKind::Point { position, .. } => {
2016                                // TODO: assert that types of all elements are the same.
2017                                Ok(KclValue::array_from_point2d(
2018                                    [position[0].n, position[1].n],
2019                                    position[0].ty,
2020                                    segment.meta.clone(),
2021                                )
2022                                .continue_())
2023                            }
2024                            _ => Err(KclError::new_undefined_value(
2025                                KclErrorDetails::new(
2026                                    format!("Property '{property}' not found in segment"),
2027                                    vec![self.clone().into()],
2028                                ),
2029                                None,
2030                            )),
2031                        }
2032                    }
2033                },
2034                "start" => match &segment.repr {
2035                    SegmentRepr::Unsolved { segment } => match &segment.kind {
2036                        UnsolvedSegmentKind::Point { .. } => Err(KclError::new_undefined_value(
2037                            KclErrorDetails::new(
2038                                format!("Property '{property}' not found in point segment"),
2039                                vec![self.clone().into()],
2040                            ),
2041                            None,
2042                        )),
2043                        UnsolvedSegmentKind::Line {
2044                            start,
2045                            ctor,
2046                            start_object_id,
2047                            ..
2048                        } => Ok(KclValue::Segment {
2049                            value: Box::new(AbstractSegment {
2050                                repr: SegmentRepr::Unsolved {
2051                                    segment: Box::new(UnsolvedSegment {
2052                                        id: segment.id,
2053                                        object_id: *start_object_id,
2054                                        kind: UnsolvedSegmentKind::Point {
2055                                            position: start.clone(),
2056                                            ctor: Box::new(PointCtor {
2057                                                position: ctor.start.clone(),
2058                                            }),
2059                                        },
2060                                        tag: segment.tag.clone(),
2061                                        meta: segment.meta.clone(),
2062                                    }),
2063                                },
2064                                meta: segment.meta.clone(),
2065                            }),
2066                        }
2067                        .continue_()),
2068                        UnsolvedSegmentKind::Arc {
2069                            start,
2070                            ctor,
2071                            start_object_id,
2072                            ..
2073                        } => Ok(KclValue::Segment {
2074                            value: Box::new(AbstractSegment {
2075                                repr: SegmentRepr::Unsolved {
2076                                    segment: Box::new(UnsolvedSegment {
2077                                        id: segment.id,
2078                                        object_id: *start_object_id,
2079                                        kind: UnsolvedSegmentKind::Point {
2080                                            position: start.clone(),
2081                                            ctor: Box::new(PointCtor {
2082                                                position: ctor.start.clone(),
2083                                            }),
2084                                        },
2085                                        tag: segment.tag.clone(),
2086                                        meta: segment.meta.clone(),
2087                                    }),
2088                                },
2089                                meta: segment.meta.clone(),
2090                            }),
2091                        }
2092                        .continue_()),
2093                    },
2094                    SegmentRepr::Solved { segment } => match &segment.kind {
2095                        SegmentKind::Point { .. } => Err(KclError::new_undefined_value(
2096                            KclErrorDetails::new(
2097                                format!("Property '{property}' not found in point segment"),
2098                                vec![self.clone().into()],
2099                            ),
2100                            None,
2101                        )),
2102                        SegmentKind::Line {
2103                            start,
2104                            ctor,
2105                            start_object_id,
2106                            start_freedom,
2107                            ..
2108                        } => Ok(KclValue::Segment {
2109                            value: Box::new(AbstractSegment {
2110                                repr: SegmentRepr::Solved {
2111                                    segment: Box::new(Segment {
2112                                        id: segment.id,
2113                                        object_id: *start_object_id,
2114                                        kind: SegmentKind::Point {
2115                                            position: start.clone(),
2116                                            ctor: Box::new(PointCtor {
2117                                                position: ctor.start.clone(),
2118                                            }),
2119                                            freedom: *start_freedom,
2120                                        },
2121                                        surface: segment.surface.clone(),
2122                                        sketch_id: segment.sketch_id,
2123                                        sketch: None,
2124                                        tag: segment.tag.clone(),
2125                                        meta: segment.meta.clone(),
2126                                    }),
2127                                },
2128                                meta: segment.meta.clone(),
2129                            }),
2130                        }
2131                        .continue_()),
2132                        SegmentKind::Arc {
2133                            start,
2134                            ctor,
2135                            start_object_id,
2136                            start_freedom,
2137                            ..
2138                        } => Ok(KclValue::Segment {
2139                            value: Box::new(AbstractSegment {
2140                                repr: SegmentRepr::Solved {
2141                                    segment: Box::new(Segment {
2142                                        id: segment.id,
2143                                        object_id: *start_object_id,
2144                                        kind: SegmentKind::Point {
2145                                            position: start.clone(),
2146                                            ctor: Box::new(PointCtor {
2147                                                position: ctor.start.clone(),
2148                                            }),
2149                                            freedom: *start_freedom,
2150                                        },
2151                                        surface: segment.surface.clone(),
2152                                        sketch_id: segment.sketch_id,
2153                                        sketch: None,
2154                                        tag: segment.tag.clone(),
2155                                        meta: segment.meta.clone(),
2156                                    }),
2157                                },
2158                                meta: segment.meta.clone(),
2159                            }),
2160                        }
2161                        .continue_()),
2162                    },
2163                },
2164                "end" => match &segment.repr {
2165                    SegmentRepr::Unsolved { segment } => match &segment.kind {
2166                        UnsolvedSegmentKind::Point { .. } => Err(KclError::new_undefined_value(
2167                            KclErrorDetails::new(
2168                                format!("Property '{property}' not found in point segment"),
2169                                vec![self.clone().into()],
2170                            ),
2171                            None,
2172                        )),
2173                        UnsolvedSegmentKind::Line {
2174                            end,
2175                            ctor,
2176                            end_object_id,
2177                            ..
2178                        } => Ok(KclValue::Segment {
2179                            value: Box::new(AbstractSegment {
2180                                repr: SegmentRepr::Unsolved {
2181                                    segment: Box::new(UnsolvedSegment {
2182                                        id: segment.id,
2183                                        object_id: *end_object_id,
2184                                        kind: UnsolvedSegmentKind::Point {
2185                                            position: end.clone(),
2186                                            ctor: Box::new(PointCtor {
2187                                                position: ctor.end.clone(),
2188                                            }),
2189                                        },
2190                                        tag: segment.tag.clone(),
2191                                        meta: segment.meta.clone(),
2192                                    }),
2193                                },
2194                                meta: segment.meta.clone(),
2195                            }),
2196                        }
2197                        .continue_()),
2198                        UnsolvedSegmentKind::Arc {
2199                            end,
2200                            ctor,
2201                            end_object_id,
2202                            ..
2203                        } => Ok(KclValue::Segment {
2204                            value: Box::new(AbstractSegment {
2205                                repr: SegmentRepr::Unsolved {
2206                                    segment: Box::new(UnsolvedSegment {
2207                                        id: segment.id,
2208                                        object_id: *end_object_id,
2209                                        kind: UnsolvedSegmentKind::Point {
2210                                            position: end.clone(),
2211                                            ctor: Box::new(PointCtor {
2212                                                position: ctor.end.clone(),
2213                                            }),
2214                                        },
2215                                        tag: segment.tag.clone(),
2216                                        meta: segment.meta.clone(),
2217                                    }),
2218                                },
2219                                meta: segment.meta.clone(),
2220                            }),
2221                        }
2222                        .continue_()),
2223                    },
2224                    SegmentRepr::Solved { segment } => match &segment.kind {
2225                        SegmentKind::Point { .. } => Err(KclError::new_undefined_value(
2226                            KclErrorDetails::new(
2227                                format!("Property '{property}' not found in point segment"),
2228                                vec![self.clone().into()],
2229                            ),
2230                            None,
2231                        )),
2232                        SegmentKind::Line {
2233                            end,
2234                            ctor,
2235                            end_object_id,
2236                            end_freedom,
2237                            ..
2238                        } => Ok(KclValue::Segment {
2239                            value: Box::new(AbstractSegment {
2240                                repr: SegmentRepr::Solved {
2241                                    segment: Box::new(Segment {
2242                                        id: segment.id,
2243                                        object_id: *end_object_id,
2244                                        kind: SegmentKind::Point {
2245                                            position: end.clone(),
2246                                            ctor: Box::new(PointCtor {
2247                                                position: ctor.end.clone(),
2248                                            }),
2249                                            freedom: *end_freedom,
2250                                        },
2251                                        surface: segment.surface.clone(),
2252                                        sketch_id: segment.sketch_id,
2253                                        sketch: None,
2254                                        tag: segment.tag.clone(),
2255                                        meta: segment.meta.clone(),
2256                                    }),
2257                                },
2258                                meta: segment.meta.clone(),
2259                            }),
2260                        }
2261                        .continue_()),
2262                        SegmentKind::Arc {
2263                            end,
2264                            ctor,
2265                            end_object_id,
2266                            end_freedom,
2267                            ..
2268                        } => Ok(KclValue::Segment {
2269                            value: Box::new(AbstractSegment {
2270                                repr: SegmentRepr::Solved {
2271                                    segment: Box::new(Segment {
2272                                        id: segment.id,
2273                                        object_id: *end_object_id,
2274                                        kind: SegmentKind::Point {
2275                                            position: end.clone(),
2276                                            ctor: Box::new(PointCtor {
2277                                                position: ctor.end.clone(),
2278                                            }),
2279                                            freedom: *end_freedom,
2280                                        },
2281                                        surface: segment.surface.clone(),
2282                                        sketch_id: segment.sketch_id,
2283                                        sketch: None,
2284                                        tag: segment.tag.clone(),
2285                                        meta: segment.meta.clone(),
2286                                    }),
2287                                },
2288                                meta: segment.meta.clone(),
2289                            }),
2290                        }
2291                        .continue_()),
2292                    },
2293                },
2294                "center" => match &segment.repr {
2295                    SegmentRepr::Unsolved { segment } => match &segment.kind {
2296                        UnsolvedSegmentKind::Arc {
2297                            center,
2298                            ctor,
2299                            center_object_id,
2300                            ..
2301                        } => Ok(KclValue::Segment {
2302                            value: Box::new(AbstractSegment {
2303                                repr: SegmentRepr::Unsolved {
2304                                    segment: Box::new(UnsolvedSegment {
2305                                        id: segment.id,
2306                                        object_id: *center_object_id,
2307                                        kind: UnsolvedSegmentKind::Point {
2308                                            position: center.clone(),
2309                                            ctor: Box::new(PointCtor {
2310                                                position: ctor.center.clone(),
2311                                            }),
2312                                        },
2313                                        tag: segment.tag.clone(),
2314                                        meta: segment.meta.clone(),
2315                                    }),
2316                                },
2317                                meta: segment.meta.clone(),
2318                            }),
2319                        }
2320                        .continue_()),
2321                        _ => Err(KclError::new_undefined_value(
2322                            KclErrorDetails::new(
2323                                format!("Property '{property}' not found in segment"),
2324                                vec![self.clone().into()],
2325                            ),
2326                            None,
2327                        )),
2328                    },
2329                    SegmentRepr::Solved { segment } => match &segment.kind {
2330                        SegmentKind::Arc {
2331                            center,
2332                            ctor,
2333                            center_object_id,
2334                            center_freedom,
2335                            ..
2336                        } => Ok(KclValue::Segment {
2337                            value: Box::new(AbstractSegment {
2338                                repr: SegmentRepr::Solved {
2339                                    segment: Box::new(Segment {
2340                                        id: segment.id,
2341                                        object_id: *center_object_id,
2342                                        kind: SegmentKind::Point {
2343                                            position: center.clone(),
2344                                            ctor: Box::new(PointCtor {
2345                                                position: ctor.center.clone(),
2346                                            }),
2347                                            freedom: *center_freedom,
2348                                        },
2349                                        surface: segment.surface.clone(),
2350                                        sketch_id: segment.sketch_id,
2351                                        sketch: None,
2352                                        tag: segment.tag.clone(),
2353                                        meta: segment.meta.clone(),
2354                                    }),
2355                                },
2356                                meta: segment.meta.clone(),
2357                            }),
2358                        }
2359                        .continue_()),
2360                        _ => Err(KclError::new_undefined_value(
2361                            KclErrorDetails::new(
2362                                format!("Property '{property}' not found in segment"),
2363                                vec![self.clone().into()],
2364                            ),
2365                            None,
2366                        )),
2367                    },
2368                },
2369                other => Err(KclError::new_undefined_value(
2370                    KclErrorDetails::new(
2371                        format!("Property '{other}' not found in segment"),
2372                        vec![self.clone().into()],
2373                    ),
2374                    None,
2375                )),
2376            },
2377            (KclValue::Plane { value: plane }, Property::String(property), false) => match property.as_str() {
2378                "zAxis" => {
2379                    let (p, u) = plane.info.z_axis.as_3_dims();
2380                    Ok(KclValue::array_from_point3d(p, u.into(), vec![meta]).continue_())
2381                }
2382                "yAxis" => {
2383                    let (p, u) = plane.info.y_axis.as_3_dims();
2384                    Ok(KclValue::array_from_point3d(p, u.into(), vec![meta]).continue_())
2385                }
2386                "xAxis" => {
2387                    let (p, u) = plane.info.x_axis.as_3_dims();
2388                    Ok(KclValue::array_from_point3d(p, u.into(), vec![meta]).continue_())
2389                }
2390                "origin" => {
2391                    let (p, u) = plane.info.origin.as_3_dims();
2392                    Ok(KclValue::array_from_point3d(p, u.into(), vec![meta]).continue_())
2393                }
2394                other => Err(KclError::new_undefined_value(
2395                    KclErrorDetails::new(
2396                        format!("Property '{other}' not found in plane"),
2397                        vec![self.clone().into()],
2398                    ),
2399                    None,
2400                )),
2401            },
2402            (KclValue::Object { value: map, .. }, Property::String(property), false) => {
2403                if let Some(value) = map.get(&property) {
2404                    Ok(value.to_owned().continue_())
2405                } else {
2406                    Err(KclError::new_undefined_value(
2407                        KclErrorDetails::new(
2408                            format!("Property '{property}' not found in object"),
2409                            vec![self.clone().into()],
2410                        ),
2411                        None,
2412                    ))
2413                }
2414            }
2415            (KclValue::Object { .. }, Property::String(property), true) => {
2416                Err(KclError::new_semantic(KclErrorDetails::new(
2417                    format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
2418                    vec![self.clone().into()],
2419                )))
2420            }
2421            (KclValue::Object { value: map, .. }, p @ Property::UInt(i), _) => {
2422                if i == 0
2423                    && let Some(value) = map.get("x")
2424                {
2425                    return Ok(value.to_owned().continue_());
2426                }
2427                if i == 1
2428                    && let Some(value) = map.get("y")
2429                {
2430                    return Ok(value.to_owned().continue_());
2431                }
2432                if i == 2
2433                    && let Some(value) = map.get("z")
2434                {
2435                    return Ok(value.to_owned().continue_());
2436                }
2437                let t = p.type_name();
2438                let article = article_for(t);
2439                Err(KclError::new_semantic(KclErrorDetails::new(
2440                    format!("Only strings can be used as the property of an object, but you're using {article} {t}",),
2441                    vec![self.clone().into()],
2442                )))
2443            }
2444            (KclValue::HomArray { value: arr, .. }, Property::UInt(index), _) => {
2445                let value_of_arr = arr.get(index);
2446                if let Some(value) = value_of_arr {
2447                    Ok(value.to_owned().continue_())
2448                } else {
2449                    Err(KclError::new_undefined_value(
2450                        KclErrorDetails::new(
2451                            format!("The array doesn't have any item at index {index}"),
2452                            vec![self.clone().into()],
2453                        ),
2454                        None,
2455                    ))
2456                }
2457            }
2458            // Singletons and single-element arrays should be interchangeable, but only indexing by 0 should work.
2459            // This is kind of a silly property, but it's possible it occurs in generic code or something.
2460            (obj, Property::UInt(0), _) => Ok(obj.continue_()),
2461            (KclValue::HomArray { .. }, p, _) => {
2462                let t = p.type_name();
2463                let article = article_for(t);
2464                Err(KclError::new_semantic(KclErrorDetails::new(
2465                    format!("Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",),
2466                    vec![self.clone().into()],
2467                )))
2468            }
2469            (KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => {
2470                let Some(sketch) = value.sketch() else {
2471                    return Err(KclError::new_semantic(KclErrorDetails::new(
2472                        "This solid was created without a sketch, so `solid.sketch` is unavailable.".to_owned(),
2473                        vec![self.clone().into()],
2474                    )));
2475                };
2476                Ok(KclValue::Sketch {
2477                    value: Box::new(sketch.clone()),
2478                }
2479                .continue_())
2480            }
2481            (geometry @ KclValue::Solid { .. }, Property::String(prop), false) if prop == "tags" => {
2482                // This is a common mistake.
2483                Err(KclError::new_semantic(KclErrorDetails::new(
2484                    format!(
2485                        "Property `{prop}` not found on {}. You can get a solid's tags through its sketch, as in, `exampleSolid.sketch.tags`.",
2486                        geometry.human_friendly_type()
2487                    ),
2488                    vec![self.clone().into()],
2489                )))
2490            }
2491            (KclValue::Sketch { value: sk }, Property::String(prop), false) if prop == "tags" => Ok(KclValue::Object {
2492                meta: vec![Metadata {
2493                    source_range: SourceRange::from(self.clone()),
2494                }],
2495                value: sk
2496                    .tags
2497                    .iter()
2498                    .map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
2499                    .collect(),
2500                constrainable: false,
2501            }
2502            .continue_()),
2503            (geometry @ (KclValue::Sketch { .. } | KclValue::Solid { .. }), Property::String(property), false) => {
2504                Err(KclError::new_semantic(KclErrorDetails::new(
2505                    format!("Property `{property}` not found on {}", geometry.human_friendly_type()),
2506                    vec![self.clone().into()],
2507                )))
2508            }
2509            (being_indexed, _, false) => Err(KclError::new_semantic(KclErrorDetails::new(
2510                format!(
2511                    "Only objects can have members accessed with dot notation, but you're trying to access {}",
2512                    being_indexed.human_friendly_type()
2513                ),
2514                vec![self.clone().into()],
2515            ))),
2516            (being_indexed, _, true) => Err(KclError::new_semantic(KclErrorDetails::new(
2517                format!(
2518                    "Only arrays can be indexed, but you're trying to index {}",
2519                    being_indexed.human_friendly_type()
2520                ),
2521                vec![self.clone().into()],
2522            ))),
2523        }
2524    }
2525}
2526
2527impl Node<BinaryExpression> {
2528    pub(super) async fn get_result(
2529        &self,
2530        exec_state: &mut ExecState,
2531        ctx: &ExecutorContext,
2532    ) -> Result<KclValueControlFlow, KclError> {
2533        enum State {
2534            EvaluateLeft(Node<BinaryExpression>),
2535            FromLeft {
2536                node: Node<BinaryExpression>,
2537            },
2538            EvaluateRight {
2539                node: Node<BinaryExpression>,
2540                left: KclValue,
2541            },
2542            FromRight {
2543                node: Node<BinaryExpression>,
2544                left: KclValue,
2545            },
2546        }
2547
2548        let mut stack = vec![State::EvaluateLeft(self.clone())];
2549        let mut last_result: Option<KclValue> = None;
2550
2551        while let Some(state) = stack.pop() {
2552            match state {
2553                State::EvaluateLeft(node) => {
2554                    let left_part = node.left.clone();
2555                    match left_part {
2556                        BinaryPart::BinaryExpression(child) => {
2557                            stack.push(State::FromLeft { node });
2558                            stack.push(State::EvaluateLeft(*child));
2559                        }
2560                        part => {
2561                            let left_value = part.get_result(exec_state, ctx).await?;
2562                            let left_value = control_continue!(left_value);
2563                            stack.push(State::EvaluateRight { node, left: left_value });
2564                        }
2565                    }
2566                }
2567                State::FromLeft { node } => {
2568                    let Some(left_value) = last_result.take() else {
2569                        return Err(Self::missing_result_error(&node));
2570                    };
2571                    stack.push(State::EvaluateRight { node, left: left_value });
2572                }
2573                State::EvaluateRight { node, left } => {
2574                    let right_part = node.right.clone();
2575                    match right_part {
2576                        BinaryPart::BinaryExpression(child) => {
2577                            stack.push(State::FromRight { node, left });
2578                            stack.push(State::EvaluateLeft(*child));
2579                        }
2580                        part => {
2581                            let right_value = part.get_result(exec_state, ctx).await?;
2582                            let right_value = control_continue!(right_value);
2583                            let result = node.apply_operator(exec_state, ctx, left, right_value).await?;
2584                            last_result = Some(result);
2585                        }
2586                    }
2587                }
2588                State::FromRight { node, left } => {
2589                    let Some(right_value) = last_result.take() else {
2590                        return Err(Self::missing_result_error(&node));
2591                    };
2592                    let result = node.apply_operator(exec_state, ctx, left, right_value).await?;
2593                    last_result = Some(result);
2594                }
2595            }
2596        }
2597
2598        last_result
2599            .map(KclValue::continue_)
2600            .ok_or_else(|| Self::missing_result_error(self))
2601    }
2602
2603    async fn apply_operator(
2604        &self,
2605        exec_state: &mut ExecState,
2606        ctx: &ExecutorContext,
2607        left_value: KclValue,
2608        right_value: KclValue,
2609    ) -> Result<KclValue, KclError> {
2610        let mut meta = left_value.metadata();
2611        meta.extend(right_value.metadata());
2612
2613        // First check if we are doing string concatenation.
2614        if self.operator == BinaryOperator::Add
2615            && let (KclValue::String { value: left, .. }, KclValue::String { value: right, .. }) =
2616                (&left_value, &right_value)
2617        {
2618            return Ok(KclValue::String {
2619                value: format!("{left}{right}"),
2620                meta,
2621            });
2622        }
2623
2624        // Then check if we have solids.
2625        if self.operator == BinaryOperator::Add || self.operator == BinaryOperator::Or {
2626            if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
2627                let args = Args::new_no_args(self.into(), ctx.clone(), Some("union".to_owned()));
2628                let result = crate::std::csg::inner_union(
2629                    vec![*left.clone(), *right.clone()],
2630                    Default::default(),
2631                    exec_state,
2632                    args,
2633                )
2634                .await?;
2635                return Ok(result.into());
2636            }
2637        } else if self.operator == BinaryOperator::Sub {
2638            // Check if we have solids.
2639            if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
2640                let args = Args::new_no_args(self.into(), ctx.clone(), Some("subtract".to_owned()));
2641                let result = crate::std::csg::inner_subtract(
2642                    vec![*left.clone()],
2643                    vec![*right.clone()],
2644                    Default::default(),
2645                    exec_state,
2646                    args,
2647                )
2648                .await?;
2649                return Ok(result.into());
2650            }
2651        } else if self.operator == BinaryOperator::And
2652            && let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value)
2653        {
2654            // Check if we have solids.
2655            let args = Args::new_no_args(self.into(), ctx.clone(), Some("intersect".to_owned()));
2656            let result = crate::std::csg::inner_intersect(
2657                vec![*left.clone(), *right.clone()],
2658                Default::default(),
2659                exec_state,
2660                args,
2661            )
2662            .await?;
2663            return Ok(result.into());
2664        }
2665
2666        // Check if we are doing logical operations on booleans.
2667        if self.operator == BinaryOperator::Or || self.operator == BinaryOperator::And {
2668            let KclValue::Bool { value: left_value, .. } = left_value else {
2669                return Err(KclError::new_semantic(KclErrorDetails::new(
2670                    format!(
2671                        "Cannot apply logical operator to non-boolean value: {}",
2672                        left_value.human_friendly_type()
2673                    ),
2674                    vec![self.left.clone().into()],
2675                )));
2676            };
2677            let KclValue::Bool { value: right_value, .. } = right_value else {
2678                return Err(KclError::new_semantic(KclErrorDetails::new(
2679                    format!(
2680                        "Cannot apply logical operator to non-boolean value: {}",
2681                        right_value.human_friendly_type()
2682                    ),
2683                    vec![self.right.clone().into()],
2684                )));
2685            };
2686            let raw_value = match self.operator {
2687                BinaryOperator::Or => left_value || right_value,
2688                BinaryOperator::And => left_value && right_value,
2689                _ => unreachable!(),
2690            };
2691            return Ok(KclValue::Bool { value: raw_value, meta });
2692        }
2693
2694        // Check if we're doing equivalence in sketch mode.
2695        if self.operator == BinaryOperator::Eq && exec_state.mod_local.sketch_block.is_some() {
2696            match (&left_value, &right_value) {
2697                // Same sketch variables.
2698                (KclValue::SketchVar { value: left_value, .. }, KclValue::SketchVar { value: right_value, .. })
2699                    if left_value.id == right_value.id =>
2700                {
2701                    return Ok(KclValue::Bool { value: true, meta });
2702                }
2703                // Different sketch variables.
2704                (KclValue::SketchVar { .. }, KclValue::SketchVar { .. }) => {
2705                    // TODO: sketch-api: Collapse the two sketch variables into
2706                    // one constraint variable.
2707                    return Err(KclError::new_semantic(KclErrorDetails::new(
2708                        "TODO: Different sketch variables".to_owned(),
2709                        vec![self.into()],
2710                    )));
2711                }
2712                // One sketch variable, one number.
2713                (KclValue::SketchVar { value: var, .. }, input_number @ KclValue::Number { .. })
2714                | (input_number @ KclValue::Number { .. }, KclValue::SketchVar { value: var, .. }) => {
2715                    let number_value = normalize_to_solver_distance_unit(
2716                        input_number,
2717                        input_number.into(),
2718                        exec_state,
2719                        "fixed constraint value",
2720                    )?;
2721                    let Some(n) = number_value.as_ty_f64() else {
2722                        let message = format!(
2723                            "Expected number after coercion, but found {}",
2724                            number_value.human_friendly_type()
2725                        );
2726                        debug_assert!(false, "{}", &message);
2727                        return Err(internal_err(message, self));
2728                    };
2729                    let constraint = Constraint::Fixed(var.id.to_constraint_id(self.as_source_range())?, n.n);
2730                    let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
2731                        let message = "Being inside a sketch block should have already been checked above".to_owned();
2732                        debug_assert!(false, "{}", &message);
2733                        return Err(internal_err(message, self));
2734                    };
2735                    sketch_block_state.solver_constraints.push(constraint);
2736                    return Ok(KclValue::Bool { value: true, meta });
2737                }
2738                // One sketch constraint, one number.
2739                (KclValue::SketchConstraint { value: constraint }, input_number @ KclValue::Number { .. })
2740                | (input_number @ KclValue::Number { .. }, KclValue::SketchConstraint { value: constraint }) => {
2741                    let number_value = match constraint.kind {
2742                        // These constraint kinds expect the RHS to be an angle.
2743                        SketchConstraintKind::Angle { .. } => normalize_to_solver_angle_unit(
2744                            input_number,
2745                            input_number.into(),
2746                            exec_state,
2747                            "fixed constraint value",
2748                        )?,
2749                        // These constraint kinds expect the RHS to be a distance.
2750                        SketchConstraintKind::Distance { .. }
2751                        | SketchConstraintKind::Radius { .. }
2752                        | SketchConstraintKind::Diameter { .. }
2753                        | SketchConstraintKind::HorizontalDistance { .. }
2754                        | SketchConstraintKind::VerticalDistance { .. } => normalize_to_solver_distance_unit(
2755                            input_number,
2756                            input_number.into(),
2757                            exec_state,
2758                            "fixed constraint value",
2759                        )?,
2760                    };
2761                    let Some(n) = number_value.as_ty_f64() else {
2762                        let message = format!(
2763                            "Expected number after coercion, but found {}",
2764                            number_value.human_friendly_type()
2765                        );
2766                        debug_assert!(false, "{}", &message);
2767                        return Err(internal_err(message, self));
2768                    };
2769                    // Recast the number side of == to get the source expression text.
2770                    #[cfg(feature = "artifact-graph")]
2771                    let number_binary_part = if matches!(&left_value, KclValue::SketchConstraint { .. }) {
2772                        &self.right
2773                    } else {
2774                        &self.left
2775                    };
2776                    #[cfg(feature = "artifact-graph")]
2777                    let source = {
2778                        use crate::unparser::ExprContext;
2779                        let mut buf = String::new();
2780                        number_binary_part.recast(&mut buf, &Default::default(), 0, ExprContext::Other);
2781                        crate::frontend::sketch::ConstraintSource {
2782                            expr: buf,
2783                            is_literal: matches!(number_binary_part, BinaryPart::Literal(_)),
2784                        }
2785                    };
2786
2787                    match &constraint.kind {
2788                        SketchConstraintKind::Angle { line0, line1 } => {
2789                            let range = self.as_source_range();
2790                            // Line 0 is points A and B.
2791                            // Line 1 is points C and D.
2792                            let ax = line0.vars[0].x.to_constraint_id(range)?;
2793                            let ay = line0.vars[0].y.to_constraint_id(range)?;
2794                            let bx = line0.vars[1].x.to_constraint_id(range)?;
2795                            let by = line0.vars[1].y.to_constraint_id(range)?;
2796                            let cx = line1.vars[0].x.to_constraint_id(range)?;
2797                            let cy = line1.vars[0].y.to_constraint_id(range)?;
2798                            let dx = line1.vars[1].x.to_constraint_id(range)?;
2799                            let dy = line1.vars[1].y.to_constraint_id(range)?;
2800                            let solver_line0 = kcl_ezpz::datatypes::inputs::DatumLineSegment::new(
2801                                kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(ax, ay),
2802                                kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(bx, by),
2803                            );
2804                            let solver_line1 = kcl_ezpz::datatypes::inputs::DatumLineSegment::new(
2805                                kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(cx, cy),
2806                                kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(dx, dy),
2807                            );
2808                            let desired_angle = match n.ty {
2809                                NumericType::Known(crate::exec::UnitType::Angle(kcmc::units::UnitAngle::Degrees))
2810                                | NumericType::Default {
2811                                    len: _,
2812                                    angle: kcmc::units::UnitAngle::Degrees,
2813                                } => kcl_ezpz::datatypes::Angle::from_degrees(n.n),
2814                                NumericType::Known(crate::exec::UnitType::Angle(kcmc::units::UnitAngle::Radians))
2815                                | NumericType::Default {
2816                                    len: _,
2817                                    angle: kcmc::units::UnitAngle::Radians,
2818                                } => kcl_ezpz::datatypes::Angle::from_radians(n.n),
2819                                NumericType::Known(crate::exec::UnitType::Count)
2820                                | NumericType::Known(crate::exec::UnitType::GenericLength)
2821                                | NumericType::Known(crate::exec::UnitType::GenericAngle)
2822                                | NumericType::Known(crate::exec::UnitType::Length(_))
2823                                | NumericType::Unknown
2824                                | NumericType::Any => {
2825                                    let message = format!("Expected angle but found {:?}", n);
2826                                    debug_assert!(false, "{}", &message);
2827                                    return Err(internal_err(message, self));
2828                                }
2829                            };
2830                            let solver_constraint = Constraint::LinesAtAngle(
2831                                solver_line0,
2832                                solver_line1,
2833                                kcl_ezpz::datatypes::AngleKind::Other(desired_angle),
2834                            );
2835                            #[cfg(feature = "artifact-graph")]
2836                            let constraint_id = exec_state.next_object_id();
2837                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
2838                                let message =
2839                                    "Being inside a sketch block should have already been checked above".to_owned();
2840                                debug_assert!(false, "{}", &message);
2841                                return Err(internal_err(message, self));
2842                            };
2843                            sketch_block_state.solver_constraints.push(solver_constraint);
2844                            #[cfg(feature = "artifact-graph")]
2845                            {
2846                                use crate::{
2847                                    execution::{Artifact, CodeRef, SketchBlockConstraint, SketchBlockConstraintType},
2848                                    front::Angle,
2849                                };
2850
2851                                let Some(sketch_id) = sketch_block_state.sketch_id else {
2852                                    let message = "Sketch id missing for constraint artifact".to_owned();
2853                                    debug_assert!(false, "{}", &message);
2854                                    return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
2855                                };
2856                                let sketch_constraint = crate::front::Constraint::Angle(Angle {
2857                                    lines: vec![line0.object_id, line1.object_id],
2858                                    angle: n.try_into().map_err(|_| {
2859                                        internal_err("Failed to convert angle units numeric suffix:", range)
2860                                    })?,
2861                                    source,
2862                                });
2863                                sketch_block_state.sketch_constraints.push(constraint_id);
2864                                let artifact_id = exec_state.next_artifact_id();
2865                                exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
2866                                    id: artifact_id,
2867                                    sketch_id,
2868                                    constraint_id,
2869                                    constraint_type: SketchBlockConstraintType::from(&sketch_constraint),
2870                                    code_ref: CodeRef::placeholder(range),
2871                                }));
2872                                exec_state.add_scene_object(
2873                                    Object {
2874                                        id: constraint_id,
2875                                        kind: ObjectKind::Constraint {
2876                                            constraint: sketch_constraint,
2877                                        },
2878                                        label: Default::default(),
2879                                        comments: Default::default(),
2880                                        artifact_id,
2881                                        source: range.into(),
2882                                    },
2883                                    range,
2884                                );
2885                            }
2886                        }
2887                        SketchConstraintKind::Distance { points } => {
2888                            let range = self.as_source_range();
2889                            let p0 = &points[0];
2890                            let p1 = &points[1];
2891                            let solver_pt0 = kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(
2892                                p0.vars.x.to_constraint_id(range)?,
2893                                p0.vars.y.to_constraint_id(range)?,
2894                            );
2895                            let solver_pt1 = kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(
2896                                p1.vars.x.to_constraint_id(range)?,
2897                                p1.vars.y.to_constraint_id(range)?,
2898                            );
2899                            let solver_constraint = Constraint::Distance(solver_pt0, solver_pt1, n.n);
2900
2901                            #[cfg(feature = "artifact-graph")]
2902                            let constraint_id = exec_state.next_object_id();
2903                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
2904                                let message =
2905                                    "Being inside a sketch block should have already been checked above".to_owned();
2906                                debug_assert!(false, "{}", &message);
2907                                return Err(internal_err(message, self));
2908                            };
2909                            sketch_block_state.solver_constraints.push(solver_constraint);
2910                            #[cfg(feature = "artifact-graph")]
2911                            {
2912                                use crate::{
2913                                    execution::{Artifact, CodeRef, SketchBlockConstraint, SketchBlockConstraintType},
2914                                    front::Distance,
2915                                };
2916
2917                                let Some(sketch_id) = sketch_block_state.sketch_id else {
2918                                    let message = "Sketch id missing for constraint artifact".to_owned();
2919                                    debug_assert!(false, "{}", &message);
2920                                    return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
2921                                };
2922                                let sketch_constraint = crate::front::Constraint::Distance(Distance {
2923                                    points: vec![p0.object_id, p1.object_id],
2924                                    distance: n.try_into().map_err(|_| {
2925                                        internal_err("Failed to convert distance units numeric suffix:", range)
2926                                    })?,
2927                                    source,
2928                                });
2929                                sketch_block_state.sketch_constraints.push(constraint_id);
2930                                let artifact_id = exec_state.next_artifact_id();
2931                                exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
2932                                    id: artifact_id,
2933                                    sketch_id,
2934                                    constraint_id,
2935                                    constraint_type: SketchBlockConstraintType::from(&sketch_constraint),
2936                                    code_ref: CodeRef::placeholder(range),
2937                                }));
2938                                exec_state.add_scene_object(
2939                                    Object {
2940                                        id: constraint_id,
2941                                        kind: ObjectKind::Constraint {
2942                                            constraint: sketch_constraint,
2943                                        },
2944                                        label: Default::default(),
2945                                        comments: Default::default(),
2946                                        artifact_id,
2947                                        source: range.into(),
2948                                    },
2949                                    range,
2950                                );
2951                            }
2952                        }
2953                        SketchConstraintKind::Radius { points } | SketchConstraintKind::Diameter { points } => {
2954                            let range = self.as_source_range();
2955                            let center = &points[0];
2956                            let start = &points[1];
2957                            // Find the arc segment that has matching center and start to get its end point
2958                            let Some(sketch_block_state) = &exec_state.mod_local.sketch_block else {
2959                                return Err(internal_err(
2960                                    "Being inside a sketch block should have already been checked above",
2961                                    self,
2962                                ));
2963                            };
2964                            // Find the arc segment with matching center and start
2965                            let (constraint_name, is_diameter) = match &constraint.kind {
2966                                SketchConstraintKind::Radius { .. } => ("radius", false),
2967                                SketchConstraintKind::Diameter { .. } => ("diameter", true),
2968                                _ => unreachable!(),
2969                            };
2970                            let arc_segment = sketch_block_state
2971                                .needed_by_engine
2972                                .iter()
2973                                .find(|seg| {
2974                                    matches!(&seg.kind, UnsolvedSegmentKind::Arc {
2975                                        center_object_id,
2976                                        start_object_id,
2977                                        ..
2978                                    } if *center_object_id == center.object_id && *start_object_id == start.object_id)
2979                                })
2980                                .ok_or_else(|| {
2981                                    internal_err(
2982                                        format!("Could not find arc segment for {} constraint", constraint_name),
2983                                        range,
2984                                    )
2985                                })?;
2986                            let UnsolvedSegmentKind::Arc { end, .. } = &arc_segment.kind else {
2987                                return Err(internal_err("Expected arc segment", range));
2988                            };
2989                            // Extract end point coordinates
2990                            let (end_x_var, end_y_var) = match (&end[0], &end[1]) {
2991                                (UnsolvedExpr::Unknown(end_x), UnsolvedExpr::Unknown(end_y)) => (*end_x, *end_y),
2992                                _ => {
2993                                    return Err(internal_err(
2994                                        "Arc end point must have sketch vars in all coordinates",
2995                                        range,
2996                                    ));
2997                                }
2998                            };
2999                            let solver_arc = kcl_ezpz::datatypes::inputs::DatumCircularArc {
3000                                center: kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(
3001                                    center.vars.x.to_constraint_id(range)?,
3002                                    center.vars.y.to_constraint_id(range)?,
3003                                ),
3004                                start: kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(
3005                                    start.vars.x.to_constraint_id(range)?,
3006                                    start.vars.y.to_constraint_id(range)?,
3007                                ),
3008                                end: kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(
3009                                    end_x_var.to_constraint_id(range)?,
3010                                    end_y_var.to_constraint_id(range)?,
3011                                ),
3012                            };
3013                            // Use ArcRadius constraint from ezpz solver
3014                            // Diameter is twice the radius, so we divide by 2 before passing to the solver
3015                            let radius_value = if is_diameter { n.n / 2.0 } else { n.n };
3016                            let solver_constraint = Constraint::ArcRadius(solver_arc, radius_value);
3017
3018                            #[cfg(feature = "artifact-graph")]
3019                            let constraint_id = exec_state.next_object_id();
3020                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
3021                                let message =
3022                                    "Being inside a sketch block should have already been checked above".to_owned();
3023                                debug_assert!(false, "{}", &message);
3024                                return Err(internal_err(message, self));
3025                            };
3026                            sketch_block_state.solver_constraints.push(solver_constraint);
3027                            #[cfg(feature = "artifact-graph")]
3028                            {
3029                                use crate::execution::{
3030                                    Artifact, CodeRef, SketchBlockConstraint, SketchBlockConstraintType,
3031                                };
3032                                // Find the arc segment object ID from the sketch block state
3033
3034                                let arc_object_id = sketch_block_state
3035                                    .needed_by_engine
3036                                    .iter()
3037                                    .find(|seg| {
3038                                        matches!(&seg.kind, UnsolvedSegmentKind::Arc {
3039                                            center_object_id,
3040                                            start_object_id,
3041                                            ..
3042                                        } if *center_object_id == center.object_id && *start_object_id == start.object_id)
3043                                    })
3044                                    .map(|seg| seg.object_id)
3045                                    .ok_or_else(|| {
3046                                        internal_err(
3047                                            format!(
3048                                                "Could not find arc segment object ID for {} constraint",
3049                                                constraint_name
3050                                            ),
3051                                            range,
3052                                        )
3053                                    })?;
3054
3055                                let constraint = if is_diameter {
3056                                    use crate::frontend::sketch::Diameter;
3057                                    crate::front::Constraint::Diameter(Diameter {
3058                                        arc: arc_object_id,
3059                                        diameter: n.try_into().map_err(|_| {
3060                                            internal_err("Failed to convert diameter units numeric suffix:", range)
3061                                        })?,
3062                                        source,
3063                                    })
3064                                } else {
3065                                    use crate::frontend::sketch::Radius;
3066                                    crate::front::Constraint::Radius(Radius {
3067                                        arc: arc_object_id,
3068                                        radius: n.try_into().map_err(|_| {
3069                                            internal_err("Failed to convert radius units numeric suffix:", range)
3070                                        })?,
3071                                        source,
3072                                    })
3073                                };
3074                                sketch_block_state.sketch_constraints.push(constraint_id);
3075                                let Some(sketch_id) = sketch_block_state.sketch_id else {
3076                                    let message = "Sketch id missing for constraint artifact".to_owned();
3077                                    debug_assert!(false, "{}", &message);
3078                                    return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
3079                                };
3080                                let artifact_id = exec_state.next_artifact_id();
3081                                exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
3082                                    id: artifact_id,
3083                                    sketch_id,
3084                                    constraint_id,
3085                                    constraint_type: SketchBlockConstraintType::from(&constraint),
3086                                    code_ref: CodeRef::placeholder(range),
3087                                }));
3088                                exec_state.add_scene_object(
3089                                    Object {
3090                                        id: constraint_id,
3091                                        kind: ObjectKind::Constraint { constraint },
3092                                        label: Default::default(),
3093                                        comments: Default::default(),
3094                                        artifact_id,
3095                                        source: range.into(),
3096                                    },
3097                                    range,
3098                                );
3099                            }
3100                        }
3101                        SketchConstraintKind::HorizontalDistance { points } => {
3102                            let range = self.as_source_range();
3103                            let p0 = &points[0];
3104                            let p1 = &points[1];
3105                            let solver_pt0 = kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(
3106                                p0.vars.x.to_constraint_id(range)?,
3107                                p0.vars.y.to_constraint_id(range)?,
3108                            );
3109                            let solver_pt1 = kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(
3110                                p1.vars.x.to_constraint_id(range)?,
3111                                p1.vars.y.to_constraint_id(range)?,
3112                            );
3113                            // Horizontal distance: p1.x - p0.x = n
3114                            // Note: EZPZ's HorizontalDistance(p0, p1, d) means p0.x - p1.x = d
3115                            // So we swap the points to get p1.x - p0.x = n
3116                            let solver_constraint =
3117                                kcl_ezpz::Constraint::HorizontalDistance(solver_pt1, solver_pt0, n.n);
3118
3119                            #[cfg(feature = "artifact-graph")]
3120                            let constraint_id = exec_state.next_object_id();
3121                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
3122                                let message =
3123                                    "Being inside a sketch block should have already been checked above".to_owned();
3124                                debug_assert!(false, "{}", &message);
3125                                return Err(internal_err(message, self));
3126                            };
3127                            sketch_block_state.solver_constraints.push(solver_constraint);
3128                            #[cfg(feature = "artifact-graph")]
3129                            {
3130                                use crate::{
3131                                    execution::{Artifact, CodeRef, SketchBlockConstraint, SketchBlockConstraintType},
3132                                    front::Distance,
3133                                };
3134
3135                                let constraint = crate::front::Constraint::HorizontalDistance(Distance {
3136                                    points: vec![p0.object_id, p1.object_id],
3137                                    distance: n.try_into().map_err(|_| {
3138                                        internal_err("Failed to convert distance units numeric suffix:", range)
3139                                    })?,
3140                                    source,
3141                                });
3142                                sketch_block_state.sketch_constraints.push(constraint_id);
3143                                let Some(sketch_id) = sketch_block_state.sketch_id else {
3144                                    let message = "Sketch id missing for constraint artifact".to_owned();
3145                                    debug_assert!(false, "{}", &message);
3146                                    return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
3147                                };
3148                                let artifact_id = exec_state.next_artifact_id();
3149                                exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
3150                                    id: artifact_id,
3151                                    sketch_id,
3152                                    constraint_id,
3153                                    constraint_type: SketchBlockConstraintType::from(&constraint),
3154                                    code_ref: CodeRef::placeholder(range),
3155                                }));
3156                                exec_state.add_scene_object(
3157                                    Object {
3158                                        id: constraint_id,
3159                                        kind: ObjectKind::Constraint { constraint },
3160                                        label: Default::default(),
3161                                        comments: Default::default(),
3162                                        artifact_id,
3163                                        source: range.into(),
3164                                    },
3165                                    range,
3166                                );
3167                            }
3168                        }
3169                        SketchConstraintKind::VerticalDistance { points } => {
3170                            let range = self.as_source_range();
3171                            let p0 = &points[0];
3172                            let p1 = &points[1];
3173                            let solver_pt0 = kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(
3174                                p0.vars.x.to_constraint_id(range)?,
3175                                p0.vars.y.to_constraint_id(range)?,
3176                            );
3177                            let solver_pt1 = kcl_ezpz::datatypes::inputs::DatumPoint::new_xy(
3178                                p1.vars.x.to_constraint_id(range)?,
3179                                p1.vars.y.to_constraint_id(range)?,
3180                            );
3181                            // Vertical distance: p1.y - p0.y = n
3182                            // Note: EZPZ's VerticalDistance(p0, p1, d) means p0.y - p1.y = d
3183                            // So we swap the points to get p1.y - p0.y = n
3184                            let solver_constraint = kcl_ezpz::Constraint::VerticalDistance(solver_pt1, solver_pt0, n.n);
3185
3186                            #[cfg(feature = "artifact-graph")]
3187                            let constraint_id = exec_state.next_object_id();
3188                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
3189                                let message =
3190                                    "Being inside a sketch block should have already been checked above".to_owned();
3191                                debug_assert!(false, "{}", &message);
3192                                return Err(internal_err(message, self));
3193                            };
3194                            sketch_block_state.solver_constraints.push(solver_constraint);
3195                            #[cfg(feature = "artifact-graph")]
3196                            {
3197                                use crate::{
3198                                    execution::{Artifact, CodeRef, SketchBlockConstraint, SketchBlockConstraintType},
3199                                    front::Distance,
3200                                };
3201
3202                                let constraint = crate::front::Constraint::VerticalDistance(Distance {
3203                                    points: vec![p0.object_id, p1.object_id],
3204                                    distance: n.try_into().map_err(|_| {
3205                                        internal_err("Failed to convert distance units numeric suffix:", range)
3206                                    })?,
3207                                    source,
3208                                });
3209                                sketch_block_state.sketch_constraints.push(constraint_id);
3210                                let Some(sketch_id) = sketch_block_state.sketch_id else {
3211                                    let message = "Sketch id missing for constraint artifact".to_owned();
3212                                    debug_assert!(false, "{}", &message);
3213                                    return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
3214                                };
3215                                let artifact_id = exec_state.next_artifact_id();
3216                                exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
3217                                    id: artifact_id,
3218                                    sketch_id,
3219                                    constraint_id,
3220                                    constraint_type: SketchBlockConstraintType::from(&constraint),
3221                                    code_ref: CodeRef::placeholder(range),
3222                                }));
3223                                exec_state.add_scene_object(
3224                                    Object {
3225                                        id: constraint_id,
3226                                        kind: ObjectKind::Constraint { constraint },
3227                                        label: Default::default(),
3228                                        comments: Default::default(),
3229                                        artifact_id,
3230                                        source: range.into(),
3231                                    },
3232                                    range,
3233                                );
3234                            }
3235                        }
3236                    }
3237                    return Ok(KclValue::Bool { value: true, meta });
3238                }
3239                _ => {
3240                    return Err(KclError::new_semantic(KclErrorDetails::new(
3241                        format!(
3242                            "Cannot create an equivalence constraint between values of these types: {} and {}",
3243                            left_value.human_friendly_type(),
3244                            right_value.human_friendly_type()
3245                        ),
3246                        vec![self.into()],
3247                    )));
3248                }
3249            }
3250        }
3251
3252        let left = number_as_f64(&left_value, self.left.clone().into())?;
3253        let right = number_as_f64(&right_value, self.right.clone().into())?;
3254
3255        let value = match self.operator {
3256            BinaryOperator::Add => {
3257                let (l, r, ty) = NumericType::combine_eq_coerce(left, right, None);
3258                self.warn_on_unknown(&ty, "Adding", exec_state);
3259                KclValue::Number { value: l + r, meta, ty }
3260            }
3261            BinaryOperator::Sub => {
3262                let (l, r, ty) = NumericType::combine_eq_coerce(left, right, None);
3263                self.warn_on_unknown(&ty, "Subtracting", exec_state);
3264                KclValue::Number { value: l - r, meta, ty }
3265            }
3266            BinaryOperator::Mul => {
3267                let (l, r, ty) = NumericType::combine_mul(left, right);
3268                self.warn_on_unknown(&ty, "Multiplying", exec_state);
3269                KclValue::Number { value: l * r, meta, ty }
3270            }
3271            BinaryOperator::Div => {
3272                let (l, r, ty) = NumericType::combine_div(left, right);
3273                self.warn_on_unknown(&ty, "Dividing", exec_state);
3274                KclValue::Number { value: l / r, meta, ty }
3275            }
3276            BinaryOperator::Mod => {
3277                let (l, r, ty) = NumericType::combine_mod(left, right);
3278                self.warn_on_unknown(&ty, "Modulo of", exec_state);
3279                KclValue::Number { value: l % r, meta, ty }
3280            }
3281            BinaryOperator::Pow => KclValue::Number {
3282                value: left.n.powf(right.n),
3283                meta,
3284                ty: exec_state.current_default_units(),
3285            },
3286            BinaryOperator::Neq => {
3287                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
3288                self.warn_on_unknown(&ty, "Comparing", exec_state);
3289                KclValue::Bool { value: l != r, meta }
3290            }
3291            BinaryOperator::Gt => {
3292                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
3293                self.warn_on_unknown(&ty, "Comparing", exec_state);
3294                KclValue::Bool { value: l > r, meta }
3295            }
3296            BinaryOperator::Gte => {
3297                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
3298                self.warn_on_unknown(&ty, "Comparing", exec_state);
3299                KclValue::Bool { value: l >= r, meta }
3300            }
3301            BinaryOperator::Lt => {
3302                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
3303                self.warn_on_unknown(&ty, "Comparing", exec_state);
3304                KclValue::Bool { value: l < r, meta }
3305            }
3306            BinaryOperator::Lte => {
3307                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
3308                self.warn_on_unknown(&ty, "Comparing", exec_state);
3309                KclValue::Bool { value: l <= r, meta }
3310            }
3311            BinaryOperator::Eq => {
3312                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
3313                self.warn_on_unknown(&ty, "Comparing", exec_state);
3314                KclValue::Bool { value: l == r, meta }
3315            }
3316            BinaryOperator::And | BinaryOperator::Or => unreachable!(),
3317        };
3318
3319        Ok(value)
3320    }
3321
3322    fn missing_result_error(node: &Node<BinaryExpression>) -> KclError {
3323        internal_err("missing result while evaluating binary expression", node)
3324    }
3325
3326    fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
3327        if ty == &NumericType::Unknown {
3328            let sr = self.as_source_range();
3329            exec_state.clear_units_warnings(&sr);
3330            let mut err = CompilationError::err(
3331                sr,
3332                format!(
3333                    "{verb} numbers which have unknown or incompatible units.\nYou can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`."
3334                ),
3335            );
3336            err.tag = crate::errors::Tag::UnknownNumericUnits;
3337            exec_state.warn(err, annotations::WARN_UNKNOWN_UNITS);
3338        }
3339    }
3340}
3341
3342impl Node<UnaryExpression> {
3343    pub(super) async fn get_result(
3344        &self,
3345        exec_state: &mut ExecState,
3346        ctx: &ExecutorContext,
3347    ) -> Result<KclValueControlFlow, KclError> {
3348        match self.operator {
3349            UnaryOperator::Not => {
3350                let value = self.argument.get_result(exec_state, ctx).await?;
3351                let value = control_continue!(value);
3352                let KclValue::Bool {
3353                    value: bool_value,
3354                    meta: _,
3355                } = value
3356                else {
3357                    return Err(KclError::new_semantic(KclErrorDetails::new(
3358                        format!(
3359                            "Cannot apply unary operator ! to non-boolean value: {}",
3360                            value.human_friendly_type()
3361                        ),
3362                        vec![self.into()],
3363                    )));
3364                };
3365                let meta = vec![Metadata {
3366                    source_range: self.into(),
3367                }];
3368                let negated = KclValue::Bool {
3369                    value: !bool_value,
3370                    meta,
3371                };
3372
3373                Ok(negated.continue_())
3374            }
3375            UnaryOperator::Neg => {
3376                let value = self.argument.get_result(exec_state, ctx).await?;
3377                let value = control_continue!(value);
3378                let err = || {
3379                    KclError::new_semantic(KclErrorDetails::new(
3380                        format!(
3381                            "You can only negate numbers, planes, or lines, but this is a {}",
3382                            value.human_friendly_type()
3383                        ),
3384                        vec![self.into()],
3385                    ))
3386                };
3387                match &value {
3388                    KclValue::Number { value, ty, .. } => {
3389                        let meta = vec![Metadata {
3390                            source_range: self.into(),
3391                        }];
3392                        Ok(KclValue::Number {
3393                            value: -value,
3394                            meta,
3395                            ty: *ty,
3396                        }
3397                        .continue_())
3398                    }
3399                    KclValue::Plane { value } => {
3400                        let mut plane = value.clone();
3401                        if plane.info.x_axis.x != 0.0 {
3402                            plane.info.x_axis.x *= -1.0;
3403                        }
3404                        if plane.info.x_axis.y != 0.0 {
3405                            plane.info.x_axis.y *= -1.0;
3406                        }
3407                        if plane.info.x_axis.z != 0.0 {
3408                            plane.info.x_axis.z *= -1.0;
3409                        }
3410
3411                        plane.id = exec_state.next_uuid();
3412                        plane.object_id = None;
3413                        Ok(KclValue::Plane { value: plane }.continue_())
3414                    }
3415                    KclValue::Object {
3416                        value: values, meta, ..
3417                    } => {
3418                        // Special-case for negating line-like objects.
3419                        let Some(direction) = values.get("direction") else {
3420                            return Err(err());
3421                        };
3422
3423                        let direction = match direction {
3424                            KclValue::Tuple { value: values, meta } => {
3425                                let values = values
3426                                    .iter()
3427                                    .map(|v| match v {
3428                                        KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
3429                                            value: *value * -1.0,
3430                                            ty: *ty,
3431                                            meta: meta.clone(),
3432                                        }),
3433                                        _ => Err(err()),
3434                                    })
3435                                    .collect::<Result<Vec<_>, _>>()?;
3436
3437                                KclValue::Tuple {
3438                                    value: values,
3439                                    meta: meta.clone(),
3440                                }
3441                            }
3442                            KclValue::HomArray {
3443                                value: values,
3444                                ty: ty @ RuntimeType::Primitive(PrimitiveType::Number(_)),
3445                            } => {
3446                                let values = values
3447                                    .iter()
3448                                    .map(|v| match v {
3449                                        KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
3450                                            value: *value * -1.0,
3451                                            ty: *ty,
3452                                            meta: meta.clone(),
3453                                        }),
3454                                        _ => Err(err()),
3455                                    })
3456                                    .collect::<Result<Vec<_>, _>>()?;
3457
3458                                KclValue::HomArray {
3459                                    value: values,
3460                                    ty: ty.clone(),
3461                                }
3462                            }
3463                            _ => return Err(err()),
3464                        };
3465
3466                        let mut value = values.clone();
3467                        value.insert("direction".to_owned(), direction);
3468                        Ok(KclValue::Object {
3469                            value,
3470                            meta: meta.clone(),
3471                            constrainable: false,
3472                        }
3473                        .continue_())
3474                    }
3475                    _ => Err(err()),
3476                }
3477            }
3478            UnaryOperator::Plus => {
3479                let operand = self.argument.get_result(exec_state, ctx).await?;
3480                let operand = control_continue!(operand);
3481                match operand {
3482                    KclValue::Number { .. } | KclValue::Plane { .. } => Ok(operand.continue_()),
3483                    _ => Err(KclError::new_semantic(KclErrorDetails::new(
3484                        format!(
3485                            "You can only apply unary + to numbers or planes, but this is a {}",
3486                            operand.human_friendly_type()
3487                        ),
3488                        vec![self.into()],
3489                    ))),
3490                }
3491            }
3492        }
3493    }
3494}
3495
3496pub(crate) async fn execute_pipe_body(
3497    exec_state: &mut ExecState,
3498    body: &[Expr],
3499    source_range: SourceRange,
3500    ctx: &ExecutorContext,
3501) -> Result<KclValueControlFlow, KclError> {
3502    let Some((first, body)) = body.split_first() else {
3503        return Err(KclError::new_semantic(KclErrorDetails::new(
3504            "Pipe expressions cannot be empty".to_owned(),
3505            vec![source_range],
3506        )));
3507    };
3508    // Evaluate the first element in the pipeline.
3509    // They use the pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
3510    // they use the % from the parent. After all, this pipe expression hasn't been executed yet, so it doesn't have any % value
3511    // of its own.
3512    let meta = Metadata {
3513        source_range: SourceRange::from(first),
3514    };
3515    let output = ctx
3516        .execute_expr(first, exec_state, &meta, &[], StatementKind::Expression)
3517        .await?;
3518    let output = control_continue!(output);
3519
3520    // Now that we've evaluated the first child expression in the pipeline, following child expressions
3521    // should use the previous child expression for %.
3522    // This means there's no more need for the previous pipe_value from the parent AST node above this one.
3523    let previous_pipe_value = exec_state.mod_local.pipe_value.replace(output);
3524    // Evaluate remaining elements.
3525    let result = inner_execute_pipe_body(exec_state, body, ctx).await;
3526    // Restore the previous pipe value.
3527    exec_state.mod_local.pipe_value = previous_pipe_value;
3528
3529    result
3530}
3531
3532/// Execute the tail of a pipe expression.  exec_state.pipe_value must be set by
3533/// the caller.
3534#[async_recursion]
3535async fn inner_execute_pipe_body(
3536    exec_state: &mut ExecState,
3537    body: &[Expr],
3538    ctx: &ExecutorContext,
3539) -> Result<KclValueControlFlow, KclError> {
3540    for expression in body {
3541        if let Expr::TagDeclarator(_) = expression {
3542            return Err(KclError::new_semantic(KclErrorDetails::new(
3543                format!("This cannot be in a PipeExpression: {expression:?}"),
3544                vec![expression.into()],
3545            )));
3546        }
3547        let metadata = Metadata {
3548            source_range: SourceRange::from(expression),
3549        };
3550        let output = ctx
3551            .execute_expr(expression, exec_state, &metadata, &[], StatementKind::Expression)
3552            .await?;
3553        let output = control_continue!(output);
3554        exec_state.mod_local.pipe_value = Some(output);
3555    }
3556    // Safe to unwrap here, because pipe_value always has something pushed in when the `match first` executes.
3557    let final_output = exec_state.mod_local.pipe_value.take().unwrap();
3558    Ok(final_output.continue_())
3559}
3560
3561impl Node<TagDeclarator> {
3562    pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
3563        let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
3564            value: self.name.clone(),
3565            info: Vec::new(),
3566            meta: vec![Metadata {
3567                source_range: self.into(),
3568            }],
3569        }));
3570
3571        exec_state
3572            .mut_stack()
3573            .add(self.name.clone(), memory_item, self.into())?;
3574
3575        Ok(self.into())
3576    }
3577}
3578
3579impl Node<ArrayExpression> {
3580    #[async_recursion]
3581    pub(super) async fn execute(
3582        &self,
3583        exec_state: &mut ExecState,
3584        ctx: &ExecutorContext,
3585    ) -> Result<KclValueControlFlow, KclError> {
3586        let mut results = Vec::with_capacity(self.elements.len());
3587
3588        for element in &self.elements {
3589            let metadata = Metadata::from(element);
3590            // TODO: Carry statement kind here so that we know if we're
3591            // inside a variable declaration.
3592            let value = ctx
3593                .execute_expr(element, exec_state, &metadata, &[], StatementKind::Expression)
3594                .await?;
3595            let value = control_continue!(value);
3596
3597            results.push(value);
3598        }
3599
3600        Ok(KclValue::HomArray {
3601            value: results,
3602            ty: RuntimeType::Primitive(PrimitiveType::Any),
3603        }
3604        .continue_())
3605    }
3606}
3607
3608impl Node<ArrayRangeExpression> {
3609    #[async_recursion]
3610    pub(super) async fn execute(
3611        &self,
3612        exec_state: &mut ExecState,
3613        ctx: &ExecutorContext,
3614    ) -> Result<KclValueControlFlow, KclError> {
3615        let metadata = Metadata::from(&self.start_element);
3616        let start_val = ctx
3617            .execute_expr(
3618                &self.start_element,
3619                exec_state,
3620                &metadata,
3621                &[],
3622                StatementKind::Expression,
3623            )
3624            .await?;
3625        let start_val = control_continue!(start_val);
3626        let start = start_val
3627            .as_ty_f64()
3628            .ok_or(KclError::new_semantic(KclErrorDetails::new(
3629                format!(
3630                    "Expected number for range start but found {}",
3631                    start_val.human_friendly_type()
3632                ),
3633                vec![self.into()],
3634            )))?;
3635        let metadata = Metadata::from(&self.end_element);
3636        let end_val = ctx
3637            .execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
3638            .await?;
3639        let end_val = control_continue!(end_val);
3640        let end = end_val.as_ty_f64().ok_or(KclError::new_semantic(KclErrorDetails::new(
3641            format!(
3642                "Expected number for range end but found {}",
3643                end_val.human_friendly_type()
3644            ),
3645            vec![self.into()],
3646        )))?;
3647
3648        let (start, end, ty) = NumericType::combine_range(start, end, exec_state, self.as_source_range())?;
3649        let Some(start) = crate::try_f64_to_i64(start) else {
3650            return Err(KclError::new_semantic(KclErrorDetails::new(
3651                format!("Range start must be an integer, but found {start}"),
3652                vec![self.into()],
3653            )));
3654        };
3655        let Some(end) = crate::try_f64_to_i64(end) else {
3656            return Err(KclError::new_semantic(KclErrorDetails::new(
3657                format!("Range end must be an integer, but found {end}"),
3658                vec![self.into()],
3659            )));
3660        };
3661
3662        if end < start {
3663            return Err(KclError::new_semantic(KclErrorDetails::new(
3664                format!("Range start is greater than range end: {start} .. {end}"),
3665                vec![self.into()],
3666            )));
3667        }
3668
3669        let range: Vec<_> = if self.end_inclusive {
3670            (start..=end).collect()
3671        } else {
3672            (start..end).collect()
3673        };
3674
3675        let meta = vec![Metadata {
3676            source_range: self.into(),
3677        }];
3678
3679        Ok(KclValue::HomArray {
3680            value: range
3681                .into_iter()
3682                .map(|num| KclValue::Number {
3683                    value: num as f64,
3684                    ty,
3685                    meta: meta.clone(),
3686                })
3687                .collect(),
3688            ty: RuntimeType::Primitive(PrimitiveType::Number(ty)),
3689        }
3690        .continue_())
3691    }
3692}
3693
3694impl Node<ObjectExpression> {
3695    #[async_recursion]
3696    pub(super) async fn execute(
3697        &self,
3698        exec_state: &mut ExecState,
3699        ctx: &ExecutorContext,
3700    ) -> Result<KclValueControlFlow, KclError> {
3701        let mut object = HashMap::with_capacity(self.properties.len());
3702        for property in &self.properties {
3703            let metadata = Metadata::from(&property.value);
3704            let result = ctx
3705                .execute_expr(&property.value, exec_state, &metadata, &[], StatementKind::Expression)
3706                .await?;
3707            let result = control_continue!(result);
3708            object.insert(property.key.name.clone(), result);
3709        }
3710
3711        Ok(KclValue::Object {
3712            value: object,
3713            meta: vec![Metadata {
3714                source_range: self.into(),
3715            }],
3716            constrainable: false,
3717        }
3718        .continue_())
3719    }
3720}
3721
3722fn article_for<S: AsRef<str>>(s: S) -> &'static str {
3723    // '[' is included since it's an array.
3724    if s.as_ref().starts_with(['a', 'e', 'i', 'o', 'u', '[']) {
3725        "an"
3726    } else {
3727        "a"
3728    }
3729}
3730
3731fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
3732    v.as_ty_f64().ok_or_else(|| {
3733        let actual_type = v.human_friendly_type();
3734        KclError::new_semantic(KclErrorDetails::new(
3735            format!("Expected a number, but found {actual_type}",),
3736            vec![source_range],
3737        ))
3738    })
3739}
3740
3741impl Node<IfExpression> {
3742    #[async_recursion]
3743    pub(super) async fn get_result(
3744        &self,
3745        exec_state: &mut ExecState,
3746        ctx: &ExecutorContext,
3747    ) -> Result<KclValueControlFlow, KclError> {
3748        // Check the `if` branch.
3749        let cond_value = ctx
3750            .execute_expr(
3751                &self.cond,
3752                exec_state,
3753                &Metadata::from(self),
3754                &[],
3755                StatementKind::Expression,
3756            )
3757            .await?;
3758        let cond_value = control_continue!(cond_value);
3759        if cond_value.get_bool()? {
3760            let block_result = ctx.exec_block(&*self.then_val, exec_state, BodyType::Block).await?;
3761            // Block must end in an expression, so this has to be Some.
3762            // Enforced by the parser.
3763            // See https://github.com/KittyCAD/modeling-app/issues/4015
3764            return Ok(block_result.unwrap());
3765        }
3766
3767        // Check any `else if` branches.
3768        for else_if in &self.else_ifs {
3769            let cond_value = ctx
3770                .execute_expr(
3771                    &else_if.cond,
3772                    exec_state,
3773                    &Metadata::from(self),
3774                    &[],
3775                    StatementKind::Expression,
3776                )
3777                .await?;
3778            let cond_value = control_continue!(cond_value);
3779            if cond_value.get_bool()? {
3780                let block_result = ctx.exec_block(&*else_if.then_val, exec_state, BodyType::Block).await?;
3781                // Block must end in an expression, so this has to be Some.
3782                // Enforced by the parser.
3783                // See https://github.com/KittyCAD/modeling-app/issues/4015
3784                return Ok(block_result.unwrap());
3785            }
3786        }
3787
3788        // Run the final `else` branch.
3789        ctx.exec_block(&*self.final_else, exec_state, BodyType::Block)
3790            .await
3791            .map(|expr| expr.unwrap())
3792    }
3793}
3794
3795#[derive(Debug)]
3796enum Property {
3797    UInt(usize),
3798    String(String),
3799}
3800
3801impl Property {
3802    #[allow(clippy::too_many_arguments)]
3803    async fn try_from<'a>(
3804        computed: bool,
3805        value: Expr,
3806        exec_state: &mut ExecState,
3807        sr: SourceRange,
3808        ctx: &ExecutorContext,
3809        metadata: &Metadata,
3810        annotations: &[Node<Annotation>],
3811        statement_kind: StatementKind<'a>,
3812    ) -> Result<Self, KclError> {
3813        let property_sr = vec![sr];
3814        if !computed {
3815            let Expr::Name(identifier) = value else {
3816                // Should actually be impossible because the parser would reject it.
3817                return Err(KclError::new_semantic(KclErrorDetails::new(
3818                    "Object expressions like `obj.property` must use simple identifier names, not complex expressions"
3819                        .to_owned(),
3820                    property_sr,
3821                )));
3822            };
3823            return Ok(Property::String(identifier.to_string()));
3824        }
3825
3826        let prop_value = ctx
3827            .execute_expr(&value, exec_state, metadata, annotations, statement_kind)
3828            .await?;
3829        let prop_value = match prop_value.control {
3830            ControlFlowKind::Continue => prop_value.into_value(),
3831            ControlFlowKind::Exit => {
3832                let message = "Early return inside array brackets is currently not supported".to_owned();
3833                debug_assert!(false, "{}", &message);
3834                return Err(internal_err(message, sr));
3835            }
3836        };
3837        match prop_value {
3838            KclValue::Number { value, ty, meta: _ } => {
3839                if !matches!(
3840                    ty,
3841                    NumericType::Unknown
3842                        | NumericType::Default { .. }
3843                        | NumericType::Known(crate::exec::UnitType::Count)
3844                ) {
3845                    return Err(KclError::new_semantic(KclErrorDetails::new(
3846                        format!(
3847                            "{value} is not a valid index, indices must be non-dimensional numbers. If you're sure this is correct, you can add `: number(Count)` to tell KCL this number is an index"
3848                        ),
3849                        property_sr,
3850                    )));
3851                }
3852                if let Some(x) = crate::try_f64_to_usize(value) {
3853                    Ok(Property::UInt(x))
3854                } else {
3855                    Err(KclError::new_semantic(KclErrorDetails::new(
3856                        format!("{value} is not a valid index, indices must be whole numbers >= 0"),
3857                        property_sr,
3858                    )))
3859                }
3860            }
3861            _ => Err(KclError::new_semantic(KclErrorDetails::new(
3862                "Only numbers (>= 0) can be indexes".to_owned(),
3863                vec![sr],
3864            ))),
3865        }
3866    }
3867}
3868
3869impl Property {
3870    fn type_name(&self) -> &'static str {
3871        match self {
3872            Property::UInt(_) => "number",
3873            Property::String(_) => "string",
3874        }
3875    }
3876}
3877
3878impl Node<PipeExpression> {
3879    #[async_recursion]
3880    pub(super) async fn get_result(
3881        &self,
3882        exec_state: &mut ExecState,
3883        ctx: &ExecutorContext,
3884    ) -> Result<KclValueControlFlow, KclError> {
3885        execute_pipe_body(exec_state, &self.body, self.into(), ctx).await
3886    }
3887}
3888
3889#[cfg(test)]
3890mod test {
3891    use std::sync::Arc;
3892
3893    use tokio::io::AsyncWriteExt;
3894
3895    use super::*;
3896    use crate::{
3897        ExecutorSettings,
3898        errors::Severity,
3899        exec::UnitType,
3900        execution::{ContextType, parse_execute},
3901    };
3902
3903    #[tokio::test(flavor = "multi_thread")]
3904    async fn ascription() {
3905        let program = r#"
3906a = 42: number
3907b = a: number
3908p = {
3909  origin = { x = 0, y = 0, z = 0 },
3910  xAxis = { x = 1, y = 0, z = 0 },
3911  yAxis = { x = 0, y = 1, z = 0 },
3912  zAxis = { x = 0, y = 0, z = 1 }
3913}: Plane
3914arr1 = [42]: [number(cm)]
3915"#;
3916
3917        let result = parse_execute(program).await.unwrap();
3918        let mem = result.exec_state.stack();
3919        assert!(matches!(
3920            mem.memory
3921                .get_from("p", result.mem_env, SourceRange::default(), 0)
3922                .unwrap(),
3923            KclValue::Plane { .. }
3924        ));
3925        let arr1 = mem
3926            .memory
3927            .get_from("arr1", result.mem_env, SourceRange::default(), 0)
3928            .unwrap();
3929        if let KclValue::HomArray { value, ty } = arr1 {
3930            assert_eq!(value.len(), 1, "Expected Vec with specific length: found {value:?}");
3931            assert_eq!(
3932                *ty,
3933                RuntimeType::known_length(kittycad_modeling_cmds::units::UnitLength::Centimeters)
3934            );
3935            // Compare, ignoring meta.
3936            if let KclValue::Number { value, ty, .. } = &value[0] {
3937                // It should not convert units.
3938                assert_eq!(*value, 42.0);
3939                assert_eq!(
3940                    *ty,
3941                    NumericType::Known(UnitType::Length(kittycad_modeling_cmds::units::UnitLength::Centimeters))
3942                );
3943            } else {
3944                panic!("Expected a number; found {:?}", value[0]);
3945            }
3946        } else {
3947            panic!("Expected HomArray; found {arr1:?}");
3948        }
3949
3950        let program = r#"
3951a = 42: string
3952"#;
3953        let result = parse_execute(program).await;
3954        let err = result.unwrap_err();
3955        assert!(
3956            err.to_string()
3957                .contains("could not coerce a number (with type `number`) to type `string`"),
3958            "Expected error but found {err:?}"
3959        );
3960
3961        let program = r#"
3962a = 42: Plane
3963"#;
3964        let result = parse_execute(program).await;
3965        let err = result.unwrap_err();
3966        assert!(
3967            err.to_string()
3968                .contains("could not coerce a number (with type `number`) to type `Plane`"),
3969            "Expected error but found {err:?}"
3970        );
3971
3972        let program = r#"
3973arr = [0]: [string]
3974"#;
3975        let result = parse_execute(program).await;
3976        let err = result.unwrap_err();
3977        assert!(
3978            err.to_string().contains(
3979                "could not coerce an array of `number` with 1 value (with type `[any; 1]`) to type `[string]`"
3980            ),
3981            "Expected error but found {err:?}"
3982        );
3983
3984        let program = r#"
3985mixedArr = [0, "a"]: [number(mm)]
3986"#;
3987        let result = parse_execute(program).await;
3988        let err = result.unwrap_err();
3989        assert!(
3990            err.to_string().contains(
3991                "could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
3992            ),
3993            "Expected error but found {err:?}"
3994        );
3995
3996        let program = r#"
3997mixedArr = [0, "a"]: [mm]
3998"#;
3999        let result = parse_execute(program).await;
4000        let err = result.unwrap_err();
4001        assert!(
4002            err.to_string().contains(
4003                "could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
4004            ),
4005            "Expected error but found {err:?}"
4006        );
4007    }
4008
4009    #[tokio::test(flavor = "multi_thread")]
4010    async fn neg_plane() {
4011        let program = r#"
4012p = {
4013  origin = { x = 0, y = 0, z = 0 },
4014  xAxis = { x = 1, y = 0, z = 0 },
4015  yAxis = { x = 0, y = 1, z = 0 },
4016}: Plane
4017p2 = -p
4018"#;
4019
4020        let result = parse_execute(program).await.unwrap();
4021        let mem = result.exec_state.stack();
4022        match mem
4023            .memory
4024            .get_from("p2", result.mem_env, SourceRange::default(), 0)
4025            .unwrap()
4026        {
4027            KclValue::Plane { value } => {
4028                assert_eq!(value.info.x_axis.x, -1.0);
4029                assert_eq!(value.info.x_axis.y, 0.0);
4030                assert_eq!(value.info.x_axis.z, 0.0);
4031            }
4032            _ => unreachable!(),
4033        }
4034    }
4035
4036    #[tokio::test(flavor = "multi_thread")]
4037    async fn multiple_returns() {
4038        let program = r#"fn foo() {
4039  return 0
4040  return 42
4041}
4042
4043a = foo()
4044"#;
4045
4046        let result = parse_execute(program).await;
4047        assert!(result.unwrap_err().to_string().contains("return"));
4048    }
4049
4050    #[tokio::test(flavor = "multi_thread")]
4051    async fn load_all_modules() {
4052        // program a.kcl
4053        let program_a_kcl = r#"
4054export a = 1
4055"#;
4056        // program b.kcl
4057        let program_b_kcl = r#"
4058import a from 'a.kcl'
4059
4060export b = a + 1
4061"#;
4062        // program c.kcl
4063        let program_c_kcl = r#"
4064import a from 'a.kcl'
4065
4066export c = a + 2
4067"#;
4068
4069        // program main.kcl
4070        let main_kcl = r#"
4071import b from 'b.kcl'
4072import c from 'c.kcl'
4073
4074d = b + c
4075"#;
4076
4077        let main = crate::parsing::parse_str(main_kcl, ModuleId::default())
4078            .parse_errs_as_err()
4079            .unwrap();
4080
4081        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_load_all_modules").unwrap();
4082
4083        tokio::fs::File::create(tmpdir.path().join("main.kcl"))
4084            .await
4085            .unwrap()
4086            .write_all(main_kcl.as_bytes())
4087            .await
4088            .unwrap();
4089
4090        tokio::fs::File::create(tmpdir.path().join("a.kcl"))
4091            .await
4092            .unwrap()
4093            .write_all(program_a_kcl.as_bytes())
4094            .await
4095            .unwrap();
4096
4097        tokio::fs::File::create(tmpdir.path().join("b.kcl"))
4098            .await
4099            .unwrap()
4100            .write_all(program_b_kcl.as_bytes())
4101            .await
4102            .unwrap();
4103
4104        tokio::fs::File::create(tmpdir.path().join("c.kcl"))
4105            .await
4106            .unwrap()
4107            .write_all(program_c_kcl.as_bytes())
4108            .await
4109            .unwrap();
4110
4111        let exec_ctxt = ExecutorContext {
4112            engine: Arc::new(Box::new(
4113                crate::engine::conn_mock::EngineConnection::new()
4114                    .map_err(|err| {
4115                        internal_err(
4116                            format!("Failed to create mock engine connection: {err}"),
4117                            SourceRange::default(),
4118                        )
4119                    })
4120                    .unwrap(),
4121            )),
4122            fs: Arc::new(crate::fs::FileManager::new()),
4123            settings: ExecutorSettings {
4124                project_directory: Some(crate::TypedPath(tmpdir.path().into())),
4125                ..Default::default()
4126            },
4127            context_type: ContextType::Mock,
4128        };
4129        let mut exec_state = ExecState::new(&exec_ctxt);
4130
4131        exec_ctxt
4132            .run(
4133                &crate::Program {
4134                    ast: main.clone(),
4135                    original_file_contents: "".to_owned(),
4136                },
4137                &mut exec_state,
4138            )
4139            .await
4140            .unwrap();
4141    }
4142
4143    #[tokio::test(flavor = "multi_thread")]
4144    async fn user_coercion() {
4145        let program = r#"fn foo(x: Axis2d) {
4146  return 0
4147}
4148
4149foo(x = { direction = [0, 0], origin = [0, 0]})
4150"#;
4151
4152        parse_execute(program).await.unwrap();
4153
4154        let program = r#"fn foo(x: Axis3d) {
4155  return 0
4156}
4157
4158foo(x = { direction = [0, 0], origin = [0, 0]})
4159"#;
4160
4161        parse_execute(program).await.unwrap_err();
4162    }
4163
4164    #[tokio::test(flavor = "multi_thread")]
4165    async fn coerce_return() {
4166        let program = r#"fn foo(): number(mm) {
4167  return 42
4168}
4169
4170a = foo()
4171"#;
4172
4173        parse_execute(program).await.unwrap();
4174
4175        let program = r#"fn foo(): mm {
4176  return 42
4177}
4178
4179a = foo()
4180"#;
4181
4182        parse_execute(program).await.unwrap();
4183
4184        let program = r#"fn foo(): number(mm) {
4185  return { bar: 42 }
4186}
4187
4188a = foo()
4189"#;
4190
4191        parse_execute(program).await.unwrap_err();
4192
4193        let program = r#"fn foo(): mm {
4194  return { bar: 42 }
4195}
4196
4197a = foo()
4198"#;
4199
4200        parse_execute(program).await.unwrap_err();
4201    }
4202
4203    #[tokio::test(flavor = "multi_thread")]
4204    async fn test_sensible_error_when_missing_equals_in_kwarg() {
4205        for (i, call) in ["f(x=1,3,0)", "f(x=1,3,z)", "f(x=1,0,z=1)", "f(x=1, 3 + 4, z)"]
4206            .into_iter()
4207            .enumerate()
4208        {
4209            let program = format!(
4210                "fn foo() {{ return 0 }}
4211z = 0
4212fn f(x, y, z) {{ return 0 }}
4213{call}"
4214            );
4215            let err = parse_execute(&program).await.unwrap_err();
4216            let msg = err.message();
4217            assert!(
4218                msg.contains("This argument needs a label, but it doesn't have one"),
4219                "failed test {i}: {msg}"
4220            );
4221            assert!(msg.contains("`y`"), "failed test {i}, missing `y`: {msg}");
4222            if i == 0 {
4223                assert!(msg.contains("`z`"), "failed test {i}, missing `z`: {msg}");
4224            }
4225        }
4226    }
4227
4228    #[tokio::test(flavor = "multi_thread")]
4229    async fn default_param_for_unlabeled() {
4230        // Tests that the input param for myExtrude is taken from the pipeline value and same-name
4231        // keyword args.
4232        let ast = r#"fn myExtrude(@sk, length) {
4233  return extrude(sk, length)
4234}
4235sketch001 = startSketchOn(XY)
4236  |> circle(center = [0, 0], radius = 93.75)
4237  |> myExtrude(length = 40)
4238"#;
4239
4240        parse_execute(ast).await.unwrap();
4241    }
4242
4243    #[tokio::test(flavor = "multi_thread")]
4244    async fn dont_use_unlabelled_as_input() {
4245        // `length` should be used as the `length` argument to extrude, not the unlabelled input
4246        let ast = r#"length = 10
4247startSketchOn(XY)
4248  |> circle(center = [0, 0], radius = 93.75)
4249  |> extrude(length)
4250"#;
4251
4252        parse_execute(ast).await.unwrap();
4253    }
4254
4255    #[tokio::test(flavor = "multi_thread")]
4256    async fn ascription_in_binop() {
4257        let ast = r#"foo = tan(0): number(rad) - 4deg"#;
4258        parse_execute(ast).await.unwrap();
4259
4260        let ast = r#"foo = tan(0): rad - 4deg"#;
4261        parse_execute(ast).await.unwrap();
4262    }
4263
4264    #[tokio::test(flavor = "multi_thread")]
4265    async fn neg_sqrt() {
4266        let ast = r#"bad = sqrt(-2)"#;
4267
4268        let e = parse_execute(ast).await.unwrap_err();
4269        // Make sure we get a useful error message and not an engine error.
4270        assert!(e.message().contains("sqrt"), "Error message: '{}'", e.message());
4271    }
4272
4273    #[tokio::test(flavor = "multi_thread")]
4274    async fn non_array_fns() {
4275        let ast = r#"push(1, item = 2)
4276pop(1)
4277map(1, f = fn(@x) { return x + 1 })
4278reduce(1, f = fn(@x, accum) { return accum + x}, initial = 0)"#;
4279
4280        parse_execute(ast).await.unwrap();
4281    }
4282
4283    #[tokio::test(flavor = "multi_thread")]
4284    async fn non_array_indexing() {
4285        let good = r#"a = 42
4286good = a[0]
4287"#;
4288        let result = parse_execute(good).await.unwrap();
4289        let mem = result.exec_state.stack();
4290        let num = mem
4291            .memory
4292            .get_from("good", result.mem_env, SourceRange::default(), 0)
4293            .unwrap()
4294            .as_ty_f64()
4295            .unwrap();
4296        assert_eq!(num.n, 42.0);
4297
4298        let bad = r#"a = 42
4299bad = a[1]
4300"#;
4301
4302        parse_execute(bad).await.unwrap_err();
4303    }
4304
4305    #[tokio::test(flavor = "multi_thread")]
4306    async fn coerce_unknown_to_length() {
4307        let ast = r#"x = 2mm * 2mm
4308y = x: number(Length)"#;
4309        let e = parse_execute(ast).await.unwrap_err();
4310        assert!(
4311            e.message().contains("could not coerce"),
4312            "Error message: '{}'",
4313            e.message()
4314        );
4315
4316        let ast = r#"x = 2mm
4317y = x: number(Length)"#;
4318        let result = parse_execute(ast).await.unwrap();
4319        let mem = result.exec_state.stack();
4320        let num = mem
4321            .memory
4322            .get_from("y", result.mem_env, SourceRange::default(), 0)
4323            .unwrap()
4324            .as_ty_f64()
4325            .unwrap();
4326        assert_eq!(num.n, 2.0);
4327        assert_eq!(num.ty, NumericType::mm());
4328    }
4329
4330    #[tokio::test(flavor = "multi_thread")]
4331    async fn one_warning_unknown() {
4332        let ast = r#"
4333// Should warn once
4334a = PI * 2
4335// Should warn once
4336b = (PI * 2) / 3
4337// Should not warn
4338c = ((PI * 2) / 3): number(deg)
4339"#;
4340
4341        let result = parse_execute(ast).await.unwrap();
4342        assert_eq!(result.exec_state.errors().len(), 2);
4343    }
4344
4345    #[tokio::test(flavor = "multi_thread")]
4346    async fn non_count_indexing() {
4347        let ast = r#"x = [0, 0]
4348y = x[1mm]
4349"#;
4350        parse_execute(ast).await.unwrap_err();
4351
4352        let ast = r#"x = [0, 0]
4353y = 1deg
4354z = x[y]
4355"#;
4356        parse_execute(ast).await.unwrap_err();
4357
4358        let ast = r#"x = [0, 0]
4359y = x[0mm + 1]
4360"#;
4361        parse_execute(ast).await.unwrap_err();
4362    }
4363
4364    #[tokio::test(flavor = "multi_thread")]
4365    async fn getting_property_of_plane() {
4366        let ast = std::fs::read_to_string("tests/inputs/planestuff.kcl").unwrap();
4367        parse_execute(&ast).await.unwrap();
4368    }
4369
4370    #[cfg(feature = "artifact-graph")]
4371    #[tokio::test(flavor = "multi_thread")]
4372    async fn no_artifacts_from_within_hole_call() {
4373        // Test that executing stdlib KCL, like the `hole` function
4374        // (which is actually implemented in KCL not Rust)
4375        // does not generate artifacts from within the stdlib code,
4376        // only from the user code.
4377        let ast = std::fs::read_to_string("tests/inputs/sample_hole.kcl").unwrap();
4378        let out = parse_execute(&ast).await.unwrap();
4379
4380        // Get all the operations that occurred.
4381        let actual_operations = out.exec_state.global.root_module_artifacts.operations;
4382
4383        // There should be 5, for sketching the cube and applying the hole.
4384        // If the stdlib internal calls are being tracked, that's a bug,
4385        // and the actual number of operations will be something like 35.
4386        let expected = 5;
4387        assert_eq!(
4388            actual_operations.len(),
4389            expected,
4390            "expected {expected} operations, received {}:\n{actual_operations:#?}",
4391            actual_operations.len(),
4392        );
4393    }
4394
4395    #[cfg(feature = "artifact-graph")]
4396    #[tokio::test(flavor = "multi_thread")]
4397    async fn feature_tree_annotation_on_user_defined_kcl() {
4398        // The call to foo() should not generate an operation,
4399        // because its 'feature_tree' attribute has been set to false.
4400        let ast = std::fs::read_to_string("tests/inputs/feature_tree_annotation_on_user_defined_kcl.kcl").unwrap();
4401        let out = parse_execute(&ast).await.unwrap();
4402
4403        // Get all the operations that occurred.
4404        let actual_operations = out.exec_state.global.root_module_artifacts.operations;
4405
4406        let expected = 0;
4407        assert_eq!(
4408            actual_operations.len(),
4409            expected,
4410            "expected {expected} operations, received {}:\n{actual_operations:#?}",
4411            actual_operations.len(),
4412        );
4413    }
4414
4415    #[cfg(feature = "artifact-graph")]
4416    #[tokio::test(flavor = "multi_thread")]
4417    async fn no_feature_tree_annotation_on_user_defined_kcl() {
4418        // The call to foo() should generate an operation,
4419        // because @(feature_tree) defaults to true.
4420        let ast = std::fs::read_to_string("tests/inputs/no_feature_tree_annotation_on_user_defined_kcl.kcl").unwrap();
4421        let out = parse_execute(&ast).await.unwrap();
4422
4423        // Get all the operations that occurred.
4424        let actual_operations = out.exec_state.global.root_module_artifacts.operations;
4425
4426        let expected = 2;
4427        assert_eq!(
4428            actual_operations.len(),
4429            expected,
4430            "expected {expected} operations, received {}:\n{actual_operations:#?}",
4431            actual_operations.len(),
4432        );
4433        assert!(matches!(actual_operations[0], Operation::GroupBegin { .. }));
4434        assert!(matches!(actual_operations[1], Operation::GroupEnd));
4435    }
4436
4437    #[tokio::test(flavor = "multi_thread")]
4438    async fn custom_warning() {
4439        let warn = r#"
4440a = PI * 2
4441"#;
4442        let result = parse_execute(warn).await.unwrap();
4443        assert_eq!(result.exec_state.errors().len(), 1);
4444        assert_eq!(result.exec_state.errors()[0].severity, Severity::Warning);
4445
4446        let allow = r#"
4447@warnings(allow = unknownUnits)
4448a = PI * 2
4449"#;
4450        let result = parse_execute(allow).await.unwrap();
4451        assert_eq!(result.exec_state.errors().len(), 0);
4452
4453        let deny = r#"
4454@warnings(deny = [unknownUnits])
4455a = PI * 2
4456"#;
4457        let result = parse_execute(deny).await.unwrap();
4458        assert_eq!(result.exec_state.errors().len(), 1);
4459        assert_eq!(result.exec_state.errors()[0].severity, Severity::Error);
4460    }
4461
4462    #[tokio::test(flavor = "multi_thread")]
4463    async fn sketch_block_unqualified_functions_use_sketch2() {
4464        let ast = r#"
4465@settings(experimentalFeatures = allow)
4466s = sketch(on = XY) {
4467  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
4468  line2 = line(start = [var 1mm, var 0mm], end = [var 1mm, var 1mm])
4469  coincident([line1.end, line2.start])
4470}
4471"#;
4472        let result = parse_execute(ast).await.unwrap();
4473        let mem = result.exec_state.stack();
4474        let sketch_value = mem
4475            .memory
4476            .get_from("s", result.mem_env, SourceRange::default(), 0)
4477            .unwrap();
4478
4479        let KclValue::Object { value, .. } = sketch_value else {
4480            panic!("Expected sketch block to return an object, got {sketch_value:?}");
4481        };
4482
4483        assert!(value.contains_key("line1"));
4484        assert!(value.contains_key("line2"));
4485        // Ensure sketch2 aliases used during execution are not returned as
4486        // sketch block fields.
4487        assert!(!value.contains_key("line"));
4488        assert!(!value.contains_key("coincident"));
4489    }
4490
4491    #[tokio::test(flavor = "multi_thread")]
4492    async fn cannot_solid_extrude_an_open_profile() {
4493        // This should fail during mock execution, because KCL should catch
4494        // that the profile is not closed.
4495        let code = std::fs::read_to_string("tests/inputs/cannot_solid_extrude_an_open_profile.kcl").unwrap();
4496        let program = crate::Program::parse_no_errs(&code).expect("should parse");
4497        let exec_ctxt = ExecutorContext::new_mock(None).await;
4498        let mut exec_state = ExecState::new(&exec_ctxt);
4499
4500        let err = exec_ctxt.run(&program, &mut exec_state).await.unwrap_err().error;
4501        assert!(matches!(err, KclError::Semantic { .. }));
4502        exec_ctxt.close().await;
4503    }
4504}