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