kcl_lib/execution/
mod.rs

1//! The executor for the AST.
2
3#[cfg(feature = "artifact-graph")]
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7use anyhow::Result;
8#[cfg(feature = "artifact-graph")]
9pub use artifact::{
10    Artifact, ArtifactCommand, ArtifactGraph, CodeRef, SketchBlock, StartSketchOnFace, StartSketchOnPlane,
11};
12use cache::GlobalState;
13pub use cache::{bust_cache, clear_mem_cache};
14#[cfg(feature = "artifact-graph")]
15pub use cad_op::Group;
16pub use cad_op::Operation;
17pub use geometry::*;
18pub use id_generator::IdGenerator;
19pub(crate) use import::PreImportedGeometry;
20use indexmap::IndexMap;
21pub use kcl_value::{KclObjectFields, KclValue};
22use kcmc::{
23    ImageFormat, ModelingCmd, each_cmd as mcmd,
24    ok_response::{OkModelingCmdResponse, output::TakeSnapshot},
25    websocket::{ModelingSessionData, OkWebSocketResponseData},
26};
27use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
28pub use memory::EnvironmentRef;
29pub(crate) use modeling::ModelingCmdMeta;
30use serde::{Deserialize, Serialize};
31pub(crate) use sketch_solve::normalize_to_solver_unit;
32pub(crate) use state::ModuleArtifactState;
33pub use state::{ExecState, MetaSettings};
34use uuid::Uuid;
35
36use crate::{
37    CompilationError, ExecError, KclErrorWithOutputs, SourceRange,
38    engine::{EngineManager, GridScaleBehavior},
39    errors::{KclError, KclErrorDetails},
40    execution::{
41        cache::{CacheInformation, CacheResult},
42        import_graph::{Universe, UniverseMap},
43        typed_path::TypedPath,
44    },
45    front::{Object, ObjectId},
46    fs::FileManager,
47    modules::{ModuleExecutionOutcome, ModuleId, ModulePath, ModuleRepr},
48    parsing::ast::types::{Expr, ImportPath, NodeRef},
49};
50#[cfg(feature = "artifact-graph")]
51use crate::{collections::AhashIndexSet, front::Number};
52
53pub(crate) mod annotations;
54#[cfg(feature = "artifact-graph")]
55mod artifact;
56pub(crate) mod cache;
57mod cad_op;
58mod exec_ast;
59pub mod fn_call;
60#[cfg(test)]
61#[cfg(feature = "artifact-graph")]
62mod freedom_analysis_tests;
63mod geometry;
64mod id_generator;
65mod import;
66mod import_graph;
67pub(crate) mod kcl_value;
68mod memory;
69mod modeling;
70mod sketch_solve;
71mod state;
72pub mod typed_path;
73pub(crate) mod types;
74
75/// Convenience macro for handling control flow in execution by returning early
76/// if it is some kind of early return or stripping off the control flow
77/// otherwise.
78macro_rules! control_continue {
79    ($control_flow:expr) => {{
80        let cf = $control_flow;
81        if cf.is_some_return() {
82            return Ok(cf);
83        } else {
84            cf.into_value()
85        }
86    }};
87}
88// Expose the macro to other modules.
89pub(crate) use control_continue;
90
91#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
92pub enum ControlFlowKind {
93    #[default]
94    Continue,
95    Exit,
96}
97
98impl ControlFlowKind {
99    /// Returns true if this is any kind of early return.
100    pub fn is_some_return(&self) -> bool {
101        match self {
102            ControlFlowKind::Continue => false,
103            ControlFlowKind::Exit => true,
104        }
105    }
106}
107
108#[must_use = "You should always handle the control flow value when it is returned"]
109#[derive(Debug, Clone, PartialEq, Serialize)]
110pub struct KclValueControlFlow {
111    /// Use [control_continue] or [Self::into_value] to get the value.
112    value: KclValue,
113    pub control: ControlFlowKind,
114}
115
116impl KclValue {
117    pub(crate) fn continue_(self) -> KclValueControlFlow {
118        KclValueControlFlow {
119            value: self,
120            control: ControlFlowKind::Continue,
121        }
122    }
123
124    pub(crate) fn exit(self) -> KclValueControlFlow {
125        KclValueControlFlow {
126            value: self,
127            control: ControlFlowKind::Exit,
128        }
129    }
130}
131
132impl KclValueControlFlow {
133    /// Returns true if this is any kind of early return.
134    pub fn is_some_return(&self) -> bool {
135        self.control.is_some_return()
136    }
137
138    pub(crate) fn into_value(self) -> KclValue {
139        self.value
140    }
141}
142
143pub(crate) enum StatementKind<'a> {
144    Declaration { name: &'a str },
145    Expression,
146}
147
148#[derive(Debug, Clone, Copy)]
149pub enum PreserveMem {
150    Normal,
151    Always,
152}
153
154impl PreserveMem {
155    fn normal(self) -> bool {
156        match self {
157            PreserveMem::Normal => true,
158            PreserveMem::Always => false,
159        }
160    }
161}
162
163/// Outcome of executing a program.  This is used in TS.
164#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
165#[ts(export)]
166#[serde(rename_all = "camelCase")]
167pub struct ExecOutcome {
168    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
169    pub variables: IndexMap<String, KclValue>,
170    /// Operations that have been performed in execution order, for display in
171    /// the Feature Tree.
172    #[cfg(feature = "artifact-graph")]
173    pub operations: Vec<Operation>,
174    /// Output artifact graph.
175    #[cfg(feature = "artifact-graph")]
176    pub artifact_graph: ArtifactGraph,
177    /// Objects in the scene, created from execution.
178    #[cfg(feature = "artifact-graph")]
179    #[serde(skip)]
180    pub scene_objects: Vec<Object>,
181    /// Map from source range to object ID for lookup of objects by their source
182    /// range.
183    #[cfg(feature = "artifact-graph")]
184    #[serde(skip)]
185    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
186    #[cfg(feature = "artifact-graph")]
187    #[serde(skip)]
188    pub var_solutions: Vec<(SourceRange, Number)>,
189    /// Non-fatal errors and warnings.
190    pub errors: Vec<CompilationError>,
191    /// File Names in module Id array index order
192    pub filenames: IndexMap<ModuleId, ModulePath>,
193    /// The default planes.
194    pub default_planes: Option<DefaultPlanes>,
195}
196
197impl ExecOutcome {
198    pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
199        #[cfg(feature = "artifact-graph")]
200        {
201            debug_assert!(
202                id.0 < self.scene_objects.len(),
203                "Requested object ID {} but only have {} objects",
204                id.0,
205                self.scene_objects.len()
206            );
207            self.scene_objects.get(id.0)
208        }
209        #[cfg(not(feature = "artifact-graph"))]
210        {
211            let _ = id;
212            None
213        }
214    }
215}
216
217/// Configuration for mock execution.
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub struct MockConfig {
220    pub use_prev_memory: bool,
221    /// The `ObjectId` of the sketch block to execute for sketch mode. Only the
222    /// specified sketch block will be executed. All other code is ignored.
223    pub sketch_block_id: Option<ObjectId>,
224    /// True to do more costly analysis of whether the sketch block segments are
225    /// under-constrained.
226    pub freedom_analysis: bool,
227    /// The segments that were edited that triggered this execution.
228    #[cfg(feature = "artifact-graph")]
229    pub segment_ids_edited: AhashIndexSet<ObjectId>,
230}
231
232impl Default for MockConfig {
233    fn default() -> Self {
234        Self {
235            // By default, use previous memory. This is usually what you want.
236            use_prev_memory: true,
237            sketch_block_id: None,
238            freedom_analysis: true,
239            #[cfg(feature = "artifact-graph")]
240            segment_ids_edited: AhashIndexSet::default(),
241        }
242    }
243}
244
245impl MockConfig {
246    /// Create a new mock config for sketch mode.
247    pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
248        Self {
249            sketch_block_id: Some(sketch_block_id),
250            ..Default::default()
251        }
252    }
253
254    #[must_use]
255    pub(crate) fn no_freedom_analysis(mut self) -> Self {
256        self.freedom_analysis = false;
257        self
258    }
259}
260
261#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
262#[ts(export)]
263#[serde(rename_all = "camelCase")]
264pub struct DefaultPlanes {
265    pub xy: uuid::Uuid,
266    pub xz: uuid::Uuid,
267    pub yz: uuid::Uuid,
268    pub neg_xy: uuid::Uuid,
269    pub neg_xz: uuid::Uuid,
270    pub neg_yz: uuid::Uuid,
271}
272
273#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
274#[ts(export)]
275#[serde(tag = "type", rename_all = "camelCase")]
276pub struct TagIdentifier {
277    pub value: String,
278    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
279    // was written.
280    #[serde(skip)]
281    pub info: Vec<(usize, TagEngineInfo)>,
282    #[serde(skip)]
283    pub meta: Vec<Metadata>,
284}
285
286impl TagIdentifier {
287    /// Get the tag info for this tag at a specified epoch.
288    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
289        for (e, info) in self.info.iter().rev() {
290            if *e <= at_epoch {
291                return Some(info);
292            }
293        }
294
295        None
296    }
297
298    /// Get the most recent tag info for this tag.
299    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
300        self.info.last().map(|i| &i.1)
301    }
302
303    /// Add info from a different instance of this tag.
304    pub fn merge_info(&mut self, other: &TagIdentifier) {
305        assert_eq!(&self.value, &other.value);
306        for (oe, ot) in &other.info {
307            if let Some((e, t)) = self.info.last_mut() {
308                // If there is newer info, then skip this iteration.
309                if *e > *oe {
310                    continue;
311                }
312                // If we're in the same epoch, then overwrite.
313                if e == oe {
314                    *t = ot.clone();
315                    continue;
316                }
317            }
318            self.info.push((*oe, ot.clone()));
319        }
320    }
321}
322
323impl Eq for TagIdentifier {}
324
325impl std::fmt::Display for TagIdentifier {
326    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327        write!(f, "{}", self.value)
328    }
329}
330
331impl std::str::FromStr for TagIdentifier {
332    type Err = KclError;
333
334    fn from_str(s: &str) -> Result<Self, Self::Err> {
335        Ok(Self {
336            value: s.to_string(),
337            info: Vec::new(),
338            meta: Default::default(),
339        })
340    }
341}
342
343impl Ord for TagIdentifier {
344    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
345        self.value.cmp(&other.value)
346    }
347}
348
349impl PartialOrd for TagIdentifier {
350    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
351        Some(self.cmp(other))
352    }
353}
354
355impl std::hash::Hash for TagIdentifier {
356    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
357        self.value.hash(state);
358    }
359}
360
361/// Engine information for a tag.
362#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
363#[ts(export)]
364#[serde(tag = "type", rename_all = "camelCase")]
365pub struct TagEngineInfo {
366    /// The id of the tagged object.
367    pub id: uuid::Uuid,
368    /// The sketch the tag is on.
369    pub sketch: uuid::Uuid,
370    /// The path the tag is on.
371    pub path: Option<Path>,
372    /// The surface information for the tag.
373    pub surface: Option<ExtrudeSurface>,
374}
375
376#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
377pub enum BodyType {
378    Root,
379    Block,
380}
381
382/// Metadata.
383#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
384#[ts(export)]
385#[serde(rename_all = "camelCase")]
386pub struct Metadata {
387    /// The source range.
388    pub source_range: SourceRange,
389}
390
391impl From<Metadata> for Vec<SourceRange> {
392    fn from(meta: Metadata) -> Self {
393        vec![meta.source_range]
394    }
395}
396
397impl From<SourceRange> for Metadata {
398    fn from(source_range: SourceRange) -> Self {
399        Self { source_range }
400    }
401}
402
403impl<T> From<NodeRef<'_, T>> for Metadata {
404    fn from(node: NodeRef<'_, T>) -> Self {
405        Self {
406            source_range: SourceRange::new(node.start, node.end, node.module_id),
407        }
408    }
409}
410
411impl From<&Expr> for Metadata {
412    fn from(expr: &Expr) -> Self {
413        Self {
414            source_range: SourceRange::from(expr),
415        }
416    }
417}
418
419impl Metadata {
420    pub fn to_source_ref(meta: &[Metadata]) -> crate::front::SourceRef {
421        if meta.len() == 1 {
422            let meta = &meta[0];
423            return crate::front::SourceRef::Simple {
424                range: meta.source_range,
425            };
426        }
427        crate::front::SourceRef::BackTrace {
428            ranges: meta.iter().map(|m| m.source_range).collect(),
429        }
430    }
431}
432
433/// The type of ExecutorContext being used
434#[derive(PartialEq, Debug, Default, Clone)]
435pub enum ContextType {
436    /// Live engine connection
437    #[default]
438    Live,
439
440    /// Completely mocked connection
441    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
442    /// actually make them.
443    Mock,
444
445    /// Handled by some other interpreter/conversion system
446    MockCustomForwarded,
447}
448
449/// The executor context.
450/// Cloning will return another handle to the same engine connection/session,
451/// as this uses `Arc` under the hood.
452#[derive(Debug, Clone)]
453pub struct ExecutorContext {
454    pub engine: Arc<Box<dyn EngineManager>>,
455    pub fs: Arc<FileManager>,
456    pub settings: ExecutorSettings,
457    pub context_type: ContextType,
458}
459
460/// The executor settings.
461#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
462#[ts(export)]
463pub struct ExecutorSettings {
464    /// Highlight edges of 3D objects?
465    pub highlight_edges: bool,
466    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
467    pub enable_ssao: bool,
468    /// Show grid?
469    pub show_grid: bool,
470    /// Should engine store this for replay?
471    /// If so, under what name?
472    pub replay: Option<String>,
473    /// The directory of the current project.  This is used for resolving import
474    /// paths.  If None is given, the current working directory is used.
475    pub project_directory: Option<TypedPath>,
476    /// This is the path to the current file being executed.
477    /// We use this for preventing cyclic imports.
478    pub current_file: Option<TypedPath>,
479    /// Whether or not to automatically scale the grid when user zooms.
480    pub fixed_size_grid: bool,
481}
482
483impl Default for ExecutorSettings {
484    fn default() -> Self {
485        Self {
486            highlight_edges: true,
487            enable_ssao: false,
488            show_grid: false,
489            replay: None,
490            project_directory: None,
491            current_file: None,
492            fixed_size_grid: true,
493        }
494    }
495}
496
497impl From<crate::settings::types::Configuration> for ExecutorSettings {
498    fn from(config: crate::settings::types::Configuration) -> Self {
499        Self::from(config.settings)
500    }
501}
502
503impl From<crate::settings::types::Settings> for ExecutorSettings {
504    fn from(settings: crate::settings::types::Settings) -> Self {
505        Self {
506            highlight_edges: settings.modeling.highlight_edges.into(),
507            enable_ssao: settings.modeling.enable_ssao.into(),
508            show_grid: settings.modeling.show_scale_grid,
509            replay: None,
510            project_directory: None,
511            current_file: None,
512            fixed_size_grid: settings.modeling.fixed_size_grid,
513        }
514    }
515}
516
517impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
518    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
519        Self::from(config.settings.modeling)
520    }
521}
522
523impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
524    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
525        Self {
526            highlight_edges: modeling.highlight_edges.into(),
527            enable_ssao: modeling.enable_ssao.into(),
528            show_grid: modeling.show_scale_grid,
529            replay: None,
530            project_directory: None,
531            current_file: None,
532            fixed_size_grid: true,
533        }
534    }
535}
536
537impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
538    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
539        Self {
540            highlight_edges: modeling.highlight_edges.into(),
541            enable_ssao: modeling.enable_ssao.into(),
542            show_grid: Default::default(),
543            replay: None,
544            project_directory: None,
545            current_file: None,
546            fixed_size_grid: true,
547        }
548    }
549}
550
551impl ExecutorSettings {
552    /// Add the current file path to the executor settings.
553    pub fn with_current_file(&mut self, current_file: TypedPath) {
554        // We want the parent directory of the file.
555        if current_file.extension() == Some("kcl") {
556            self.current_file = Some(current_file.clone());
557            // Get the parent directory.
558            if let Some(parent) = current_file.parent() {
559                self.project_directory = Some(parent);
560            } else {
561                self.project_directory = Some(TypedPath::from(""));
562            }
563        } else {
564            self.project_directory = Some(current_file);
565        }
566    }
567}
568
569impl ExecutorContext {
570    /// Create a new default executor context.
571    #[cfg(not(target_arch = "wasm32"))]
572    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
573        let (ws, _headers) = client
574            .modeling()
575            .commands_ws(
576                None,
577                None,
578                None,
579                if settings.enable_ssao {
580                    Some(kittycad::types::PostEffectType::Ssao)
581                } else {
582                    None
583                },
584                settings.replay.clone(),
585                if settings.show_grid { Some(true) } else { None },
586                None,
587                None,
588                None,
589                Some(false),
590            )
591            .await?;
592
593        let engine: Arc<Box<dyn EngineManager>> =
594            Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
595
596        Ok(Self {
597            engine,
598            fs: Arc::new(FileManager::new()),
599            settings,
600            context_type: ContextType::Live,
601        })
602    }
603
604    #[cfg(target_arch = "wasm32")]
605    pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
606        ExecutorContext {
607            engine,
608            fs,
609            settings,
610            context_type: ContextType::Live,
611        }
612    }
613
614    #[cfg(not(target_arch = "wasm32"))]
615    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
616        ExecutorContext {
617            engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
618            fs: Arc::new(FileManager::new()),
619            settings: settings.unwrap_or_default(),
620            context_type: ContextType::Mock,
621        }
622    }
623
624    #[cfg(target_arch = "wasm32")]
625    pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
626        ExecutorContext {
627            engine,
628            fs,
629            settings,
630            context_type: ContextType::Mock,
631        }
632    }
633
634    #[cfg(not(target_arch = "wasm32"))]
635    pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
636        ExecutorContext {
637            engine,
638            fs: Arc::new(FileManager::new()),
639            settings: Default::default(),
640            context_type: ContextType::MockCustomForwarded,
641        }
642    }
643
644    /// Create a new default executor context.
645    /// With a kittycad client.
646    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
647    /// variables.
648    /// But also allows for passing in a token and engine address directly.
649    #[cfg(not(target_arch = "wasm32"))]
650    pub async fn new_with_client(
651        settings: ExecutorSettings,
652        token: Option<String>,
653        engine_addr: Option<String>,
654    ) -> Result<Self> {
655        // Create the client.
656        let client = crate::engine::new_zoo_client(token, engine_addr)?;
657
658        let ctx = Self::new(&client, settings).await?;
659        Ok(ctx)
660    }
661
662    /// Create a new default executor context.
663    /// With the default kittycad client.
664    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
665    /// variables.
666    #[cfg(not(target_arch = "wasm32"))]
667    pub async fn new_with_default_client() -> Result<Self> {
668        // Create the client.
669        let ctx = Self::new_with_client(Default::default(), None, None).await?;
670        Ok(ctx)
671    }
672
673    /// For executing unit tests.
674    #[cfg(not(target_arch = "wasm32"))]
675    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
676        let ctx = ExecutorContext::new_with_client(
677            ExecutorSettings {
678                highlight_edges: true,
679                enable_ssao: false,
680                show_grid: false,
681                replay: None,
682                project_directory: None,
683                current_file: None,
684                fixed_size_grid: false,
685            },
686            None,
687            engine_addr,
688        )
689        .await?;
690        Ok(ctx)
691    }
692
693    pub fn is_mock(&self) -> bool {
694        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
695    }
696
697    /// Returns true if we should not send engine commands for any reason.
698    pub async fn no_engine_commands(&self) -> bool {
699        self.is_mock()
700    }
701
702    pub async fn send_clear_scene(
703        &self,
704        exec_state: &mut ExecState,
705        source_range: crate::execution::SourceRange,
706    ) -> Result<(), KclError> {
707        // Ensure artifacts are cleared so that we don't accumulate them across
708        // runs.
709        exec_state.mod_local.artifacts.clear();
710        exec_state.global.root_module_artifacts.clear();
711        exec_state.global.artifacts.clear();
712
713        self.engine
714            .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
715            .await
716    }
717
718    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
719        cache::bust_cache().await;
720
721        // Execute an empty program to clear and reset the scene.
722        // We specifically want to be returned the objects after the scene is reset.
723        // Like the default planes so it is easier to just execute an empty program
724        // after the cache is busted.
725        let outcome = self.run_with_caching(crate::Program::empty()).await?;
726
727        Ok(outcome)
728    }
729
730    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
731        self.eval_prelude(exec_state, SourceRange::synthetic())
732            .await
733            .map_err(KclErrorWithOutputs::no_outputs)?;
734        exec_state.mut_stack().push_new_root_env(true);
735        Ok(())
736    }
737
738    pub async fn run_mock(
739        &self,
740        program: &crate::Program,
741        mock_config: &MockConfig,
742    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
743        assert!(
744            self.is_mock(),
745            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
746        );
747
748        let use_prev_memory = mock_config.use_prev_memory;
749        let mut exec_state = ExecState::new_mock(self, mock_config);
750        if use_prev_memory {
751            match cache::read_old_memory().await {
752                Some(mem) => {
753                    *exec_state.mut_stack() = mem.stack;
754                    exec_state.global.module_infos = mem.module_infos;
755                    #[cfg(feature = "artifact-graph")]
756                    {
757                        let len = mock_config
758                            .sketch_block_id
759                            .map(|sketch_block_id| sketch_block_id.0)
760                            .unwrap_or(0);
761                        if let Some(scene_objects) = mem.scene_objects.get(0..len) {
762                            exec_state.global.root_module_artifacts.scene_objects = scene_objects.to_vec();
763                        } else {
764                            let message = format!(
765                                "Cached scene objects length {} is less than expected length from cached object ID generator {}",
766                                mem.scene_objects.len(),
767                                len
768                            );
769                            debug_assert!(false, "{message}");
770                            return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
771                                KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
772                            )));
773                        }
774                    }
775                }
776                None => self.prepare_mem(&mut exec_state).await?,
777            }
778        } else {
779            self.prepare_mem(&mut exec_state).await?
780        };
781
782        // Push a scope so that old variables can be overwritten (since we might be re-executing some
783        // part of the scene).
784        exec_state.mut_stack().push_new_env_for_scope();
785
786        let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
787
788        // Restore any temporary variables, then save any newly created variables back to
789        // memory in case another run wants to use them. Note this is just saved to the preserved
790        // memory, not to the exec_state which is not cached for mock execution.
791
792        let mut stack = exec_state.stack().clone();
793        let module_infos = exec_state.global.module_infos.clone();
794        #[cfg(feature = "artifact-graph")]
795        let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
796        #[cfg(not(feature = "artifact-graph"))]
797        let scene_objects = Default::default();
798        let outcome = exec_state.into_exec_outcome(result.0, self).await;
799
800        stack.squash_env(result.0);
801        let state = cache::SketchModeState {
802            stack,
803            module_infos,
804            scene_objects,
805        };
806        cache::write_old_memory(state).await;
807
808        Ok(outcome)
809    }
810
811    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
812        assert!(!self.is_mock());
813        let grid_scale = if self.settings.fixed_size_grid {
814            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
815        } else {
816            GridScaleBehavior::ScaleWithZoom
817        };
818
819        let original_program = program.clone();
820
821        let (_program, exec_state, result) = match cache::read_old_ast().await {
822            Some(mut cached_state) => {
823                let old = CacheInformation {
824                    ast: &cached_state.main.ast,
825                    settings: &cached_state.settings,
826                };
827                let new = CacheInformation {
828                    ast: &program.ast,
829                    settings: &self.settings,
830                };
831
832                // Get the program that actually changed from the old and new information.
833                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
834                    CacheResult::ReExecute {
835                        clear_scene,
836                        reapply_settings,
837                        program: changed_program,
838                    } => {
839                        if reapply_settings
840                            && self
841                                .engine
842                                .reapply_settings(
843                                    &self.settings,
844                                    Default::default(),
845                                    &mut cached_state.main.exec_state.id_generator,
846                                    grid_scale,
847                                )
848                                .await
849                                .is_err()
850                        {
851                            (true, program, None)
852                        } else {
853                            (
854                                clear_scene,
855                                crate::Program {
856                                    ast: changed_program,
857                                    original_file_contents: program.original_file_contents,
858                                },
859                                None,
860                            )
861                        }
862                    }
863                    CacheResult::CheckImportsOnly {
864                        reapply_settings,
865                        ast: changed_program,
866                    } => {
867                        let mut reapply_failed = false;
868                        if reapply_settings {
869                            if self
870                                .engine
871                                .reapply_settings(
872                                    &self.settings,
873                                    Default::default(),
874                                    &mut cached_state.main.exec_state.id_generator,
875                                    grid_scale,
876                                )
877                                .await
878                                .is_ok()
879                            {
880                                cache::write_old_ast(GlobalState::with_settings(
881                                    cached_state.clone(),
882                                    self.settings.clone(),
883                                ))
884                                .await;
885                            } else {
886                                reapply_failed = true;
887                            }
888                        }
889
890                        if reapply_failed {
891                            (true, program, None)
892                        } else {
893                            // We need to check our imports to see if they changed.
894                            let mut new_exec_state = ExecState::new(self);
895                            let (new_universe, new_universe_map) =
896                                self.get_universe(&program, &mut new_exec_state).await?;
897
898                            let clear_scene = new_universe.values().any(|value| {
899                                let id = value.1;
900                                match (
901                                    cached_state.exec_state.get_source(id),
902                                    new_exec_state.global.get_source(id),
903                                ) {
904                                    (Some(s0), Some(s1)) => s0.source != s1.source,
905                                    _ => false,
906                                }
907                            });
908
909                            if !clear_scene {
910                                // Return early we don't need to clear the scene.
911                                return Ok(cached_state.into_exec_outcome(self).await);
912                            }
913
914                            (
915                                true,
916                                crate::Program {
917                                    ast: changed_program,
918                                    original_file_contents: program.original_file_contents,
919                                },
920                                Some((new_universe, new_universe_map, new_exec_state)),
921                            )
922                        }
923                    }
924                    CacheResult::NoAction(true) => {
925                        if self
926                            .engine
927                            .reapply_settings(
928                                &self.settings,
929                                Default::default(),
930                                &mut cached_state.main.exec_state.id_generator,
931                                grid_scale,
932                            )
933                            .await
934                            .is_ok()
935                        {
936                            // We need to update the old ast state with the new settings!!
937                            cache::write_old_ast(GlobalState::with_settings(
938                                cached_state.clone(),
939                                self.settings.clone(),
940                            ))
941                            .await;
942
943                            return Ok(cached_state.into_exec_outcome(self).await);
944                        }
945                        (true, program, None)
946                    }
947                    CacheResult::NoAction(false) => {
948                        return Ok(cached_state.into_exec_outcome(self).await);
949                    }
950                };
951
952                let (exec_state, result) = match import_check_info {
953                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
954                        // Clear the scene if the imports changed.
955                        self.send_clear_scene(&mut new_exec_state, Default::default())
956                            .await
957                            .map_err(KclErrorWithOutputs::no_outputs)?;
958
959                        let result = self
960                            .run_concurrent(
961                                &program,
962                                &mut new_exec_state,
963                                Some((new_universe, new_universe_map)),
964                                PreserveMem::Normal,
965                            )
966                            .await;
967
968                        (new_exec_state, result)
969                    }
970                    None if clear_scene => {
971                        // Pop the execution state, since we are starting fresh.
972                        let mut exec_state = cached_state.reconstitute_exec_state();
973                        exec_state.reset(self);
974
975                        self.send_clear_scene(&mut exec_state, Default::default())
976                            .await
977                            .map_err(KclErrorWithOutputs::no_outputs)?;
978
979                        let result = self
980                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
981                            .await;
982
983                        (exec_state, result)
984                    }
985                    None => {
986                        let mut exec_state = cached_state.reconstitute_exec_state();
987                        exec_state.mut_stack().restore_env(cached_state.main.result_env);
988
989                        let result = self
990                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
991                            .await;
992
993                        (exec_state, result)
994                    }
995                };
996
997                (program, exec_state, result)
998            }
999            None => {
1000                let mut exec_state = ExecState::new(self);
1001                self.send_clear_scene(&mut exec_state, Default::default())
1002                    .await
1003                    .map_err(KclErrorWithOutputs::no_outputs)?;
1004
1005                let result = self
1006                    .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
1007                    .await;
1008
1009                (program, exec_state, result)
1010            }
1011        };
1012
1013        if result.is_err() {
1014            cache::bust_cache().await;
1015        }
1016
1017        // Throw the error.
1018        let result = result?;
1019
1020        // Save this as the last successful execution to the cache.
1021        // Gotcha: `CacheResult::ReExecute.program` may be diff-based, do not save that AST
1022        // the last-successful AST. Instead, save in the full AST passed in.
1023        cache::write_old_ast(GlobalState::new(
1024            exec_state.clone(),
1025            self.settings.clone(),
1026            original_program.ast,
1027            result.0,
1028        ))
1029        .await;
1030
1031        let outcome = exec_state.into_exec_outcome(result.0, self).await;
1032        Ok(outcome)
1033    }
1034
1035    /// Perform the execution of a program.
1036    ///
1037    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1038    pub async fn run(
1039        &self,
1040        program: &crate::Program,
1041        exec_state: &mut ExecState,
1042    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1043        self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
1044            .await
1045    }
1046
1047    /// Perform the execution of a program using a concurrent
1048    /// execution model.
1049    ///
1050    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
1051    pub async fn run_concurrent(
1052        &self,
1053        program: &crate::Program,
1054        exec_state: &mut ExecState,
1055        universe_info: Option<(Universe, UniverseMap)>,
1056        preserve_mem: PreserveMem,
1057    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1058        // Reuse our cached universe if we have one.
1059
1060        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1061            (universe, universe_map)
1062        } else {
1063            self.get_universe(program, exec_state).await?
1064        };
1065
1066        let default_planes = self.engine.get_default_planes().read().await.clone();
1067
1068        // Run the prelude to set up the engine.
1069        self.eval_prelude(exec_state, SourceRange::synthetic())
1070            .await
1071            .map_err(KclErrorWithOutputs::no_outputs)?;
1072
1073        for modules in import_graph::import_graph(&universe, self)
1074            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1075            .into_iter()
1076        {
1077            #[cfg(not(target_arch = "wasm32"))]
1078            let mut set = tokio::task::JoinSet::new();
1079
1080            #[allow(clippy::type_complexity)]
1081            let (results_tx, mut results_rx): (
1082                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1083                tokio::sync::mpsc::Receiver<_>,
1084            ) = tokio::sync::mpsc::channel(1);
1085
1086            for module in modules {
1087                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1088                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1089                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1090                    )));
1091                };
1092                let module_id = *module_id;
1093                let module_path = module_path.clone();
1094                let source_range = SourceRange::from(import_stmt);
1095                // Clone before mutating.
1096                let module_exec_state = exec_state.clone();
1097
1098                self.add_import_module_ops(
1099                    exec_state,
1100                    &program.ast,
1101                    module_id,
1102                    &module_path,
1103                    source_range,
1104                    &universe_map,
1105                );
1106
1107                let repr = repr.clone();
1108                let exec_ctxt = self.clone();
1109                let results_tx = results_tx.clone();
1110
1111                let exec_module = async |exec_ctxt: &ExecutorContext,
1112                                         repr: &ModuleRepr,
1113                                         module_id: ModuleId,
1114                                         module_path: &ModulePath,
1115                                         exec_state: &mut ExecState,
1116                                         source_range: SourceRange|
1117                       -> Result<ModuleRepr, KclError> {
1118                    match repr {
1119                        ModuleRepr::Kcl(program, _) => {
1120                            let result = exec_ctxt
1121                                .exec_module_from_ast(
1122                                    program,
1123                                    module_id,
1124                                    module_path,
1125                                    exec_state,
1126                                    source_range,
1127                                    PreserveMem::Normal,
1128                                )
1129                                .await;
1130
1131                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1132                        }
1133                        ModuleRepr::Foreign(geom, _) => {
1134                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1135                                .await
1136                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1137
1138                            result.map(|val| {
1139                                ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1140                            })
1141                        }
1142                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1143                            format!("Module {module_path} not found in universe"),
1144                            vec![source_range],
1145                        ))),
1146                    }
1147                };
1148
1149                #[cfg(target_arch = "wasm32")]
1150                {
1151                    wasm_bindgen_futures::spawn_local(async move {
1152                        let mut exec_state = module_exec_state;
1153                        let exec_ctxt = exec_ctxt;
1154
1155                        let result = exec_module(
1156                            &exec_ctxt,
1157                            &repr,
1158                            module_id,
1159                            &module_path,
1160                            &mut exec_state,
1161                            source_range,
1162                        )
1163                        .await;
1164
1165                        results_tx
1166                            .send((module_id, module_path, result))
1167                            .await
1168                            .unwrap_or_default();
1169                    });
1170                }
1171                #[cfg(not(target_arch = "wasm32"))]
1172                {
1173                    set.spawn(async move {
1174                        let mut exec_state = module_exec_state;
1175                        let exec_ctxt = exec_ctxt;
1176
1177                        let result = exec_module(
1178                            &exec_ctxt,
1179                            &repr,
1180                            module_id,
1181                            &module_path,
1182                            &mut exec_state,
1183                            source_range,
1184                        )
1185                        .await;
1186
1187                        results_tx
1188                            .send((module_id, module_path, result))
1189                            .await
1190                            .unwrap_or_default();
1191                    });
1192                }
1193            }
1194
1195            drop(results_tx);
1196
1197            while let Some((module_id, _, result)) = results_rx.recv().await {
1198                match result {
1199                    Ok(new_repr) => {
1200                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1201
1202                        match &mut repr {
1203                            ModuleRepr::Kcl(_, cache) => {
1204                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
1205                                    unreachable!();
1206                                };
1207                                *cache = session_data;
1208                            }
1209                            ModuleRepr::Foreign(_, cache) => {
1210                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
1211                                    unreachable!();
1212                                };
1213                                *cache = session_data;
1214                            }
1215                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1216                        }
1217
1218                        exec_state.global.module_infos[&module_id].restore_repr(repr);
1219                    }
1220                    Err(e) => {
1221                        return Err(exec_state.error_with_outputs(e, None, default_planes));
1222                    }
1223                }
1224            }
1225        }
1226
1227        // Since we haven't technically started executing the root module yet,
1228        // the operations corresponding to the imports will be missing unless we
1229        // track them here.
1230        exec_state
1231            .global
1232            .root_module_artifacts
1233            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1234
1235        self.inner_run(program, exec_state, preserve_mem).await
1236    }
1237
1238    /// Get the universe & universe map of the program.
1239    /// And see if any of the imports changed.
1240    async fn get_universe(
1241        &self,
1242        program: &crate::Program,
1243        exec_state: &mut ExecState,
1244    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1245        exec_state.add_root_module_contents(program);
1246
1247        let mut universe = std::collections::HashMap::new();
1248
1249        let default_planes = self.engine.get_default_planes().read().await.clone();
1250
1251        let root_imports = import_graph::import_universe(
1252            self,
1253            &ModulePath::Main,
1254            &ModuleRepr::Kcl(program.ast.clone(), None),
1255            &mut universe,
1256            exec_state,
1257        )
1258        .await
1259        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1260
1261        Ok((universe, root_imports))
1262    }
1263
1264    #[cfg(not(feature = "artifact-graph"))]
1265    fn add_import_module_ops(
1266        &self,
1267        _exec_state: &mut ExecState,
1268        _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1269        _module_id: ModuleId,
1270        _module_path: &ModulePath,
1271        _source_range: SourceRange,
1272        _universe_map: &UniverseMap,
1273    ) {
1274    }
1275
1276    #[cfg(feature = "artifact-graph")]
1277    fn add_import_module_ops(
1278        &self,
1279        exec_state: &mut ExecState,
1280        program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1281        module_id: ModuleId,
1282        module_path: &ModulePath,
1283        source_range: SourceRange,
1284        universe_map: &UniverseMap,
1285    ) {
1286        match module_path {
1287            ModulePath::Main => {
1288                // This should never happen.
1289            }
1290            ModulePath::Local {
1291                value,
1292                original_import_path,
1293            } => {
1294                // We only want to display the top-level module imports in
1295                // the Feature Tree, not transitive imports.
1296                if universe_map.contains_key(value) {
1297                    use crate::NodePath;
1298
1299                    let node_path = if source_range.is_top_level_module() {
1300                        let cached_body_items = exec_state.global.artifacts.cached_body_items();
1301                        NodePath::from_range(
1302                            &exec_state.build_program_lookup(program.clone()),
1303                            cached_body_items,
1304                            source_range,
1305                        )
1306                        .unwrap_or_default()
1307                    } else {
1308                        // The frontend doesn't care about paths in
1309                        // files other than the top-level module.
1310                        NodePath::placeholder()
1311                    };
1312
1313                    let name = match original_import_path {
1314                        Some(value) => value.to_string_lossy(),
1315                        None => value.file_name().unwrap_or_default(),
1316                    };
1317                    exec_state.push_op(Operation::GroupBegin {
1318                        group: Group::ModuleInstance { name, module_id },
1319                        node_path,
1320                        source_range,
1321                    });
1322                    // Due to concurrent execution, we cannot easily
1323                    // group operations by module. So we leave the
1324                    // group empty and close it immediately.
1325                    exec_state.push_op(Operation::GroupEnd);
1326                }
1327            }
1328            ModulePath::Std { .. } => {
1329                // We don't want to display stdlib in the Feature Tree.
1330            }
1331        }
1332    }
1333
1334    /// Perform the execution of a program.  Accept all possible parameters and
1335    /// output everything.
1336    async fn inner_run(
1337        &self,
1338        program: &crate::Program,
1339        exec_state: &mut ExecState,
1340        preserve_mem: PreserveMem,
1341    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1342        let _stats = crate::log::LogPerfStats::new("Interpretation");
1343
1344        // Re-apply the settings, in case the cache was busted.
1345        let grid_scale = if self.settings.fixed_size_grid {
1346            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1347        } else {
1348            GridScaleBehavior::ScaleWithZoom
1349        };
1350        self.engine
1351            .reapply_settings(
1352                &self.settings,
1353                Default::default(),
1354                exec_state.id_generator(),
1355                grid_scale,
1356            )
1357            .await
1358            .map_err(KclErrorWithOutputs::no_outputs)?;
1359
1360        let default_planes = self.engine.get_default_planes().read().await.clone();
1361        let result = self
1362            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1363            .await;
1364
1365        crate::log::log(format!(
1366            "Post interpretation KCL memory stats: {:#?}",
1367            exec_state.stack().memory.stats
1368        ));
1369        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1370
1371        let env_ref = result.map_err(|(err, env_ref)| exec_state.error_with_outputs(err, env_ref, default_planes))?;
1372
1373        if !self.is_mock() {
1374            let mut stack = exec_state.stack().deep_clone();
1375            stack.restore_env(env_ref);
1376            let state = cache::SketchModeState {
1377                stack,
1378                module_infos: exec_state.global.module_infos.clone(),
1379                #[cfg(feature = "artifact-graph")]
1380                scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
1381                #[cfg(not(feature = "artifact-graph"))]
1382                scene_objects: Default::default(),
1383            };
1384            cache::write_old_memory(state).await;
1385        }
1386        let session_data = self.engine.get_session_data().await;
1387
1388        Ok((env_ref, session_data))
1389    }
1390
1391    /// Execute an AST's program and build auxiliary outputs like the artifact
1392    /// graph.
1393    async fn execute_and_build_graph(
1394        &self,
1395        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1396        exec_state: &mut ExecState,
1397        preserve_mem: PreserveMem,
1398    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1399        // Don't early return!  We need to build other outputs regardless of
1400        // whether execution failed.
1401
1402        // Because of execution caching, we may start with operations from a
1403        // previous run.
1404        #[cfg(feature = "artifact-graph")]
1405        let start_op = exec_state.global.root_module_artifacts.operations.len();
1406
1407        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1408            .await
1409            .map_err(|e| (e, None))?;
1410
1411        let exec_result = self
1412            .exec_module_body(
1413                program,
1414                exec_state,
1415                preserve_mem,
1416                ModuleId::default(),
1417                &ModulePath::Main,
1418            )
1419            .await
1420            .map(
1421                |ModuleExecutionOutcome {
1422                     environment: env_ref,
1423                     artifacts: module_artifacts,
1424                     ..
1425                 }| {
1426                    // We need to extend because it may already have operations from
1427                    // imports.
1428                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1429                    env_ref
1430                },
1431            )
1432            .map_err(|(err, env_ref, module_artifacts)| {
1433                if let Some(module_artifacts) = module_artifacts {
1434                    // We need to extend because it may already have operations
1435                    // from imports.
1436                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1437                }
1438                (err, env_ref)
1439            });
1440
1441        #[cfg(feature = "artifact-graph")]
1442        {
1443            // Fill in NodePath for operations.
1444            let programs = &exec_state.build_program_lookup(program.clone());
1445            let cached_body_items = exec_state.global.artifacts.cached_body_items();
1446            for op in exec_state
1447                .global
1448                .root_module_artifacts
1449                .operations
1450                .iter_mut()
1451                .skip(start_op)
1452            {
1453                op.fill_node_paths(programs, cached_body_items);
1454            }
1455            for module in exec_state.global.module_infos.values_mut() {
1456                if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1457                    for op in &mut outcome.artifacts.operations {
1458                        op.fill_node_paths(programs, cached_body_items);
1459                    }
1460                }
1461            }
1462        }
1463
1464        // Ensure all the async commands completed.
1465        self.engine.ensure_async_commands_completed().await.map_err(|e| {
1466            match &exec_result {
1467                Ok(env_ref) => (e, Some(*env_ref)),
1468                // Prefer the execution error.
1469                Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1470            }
1471        })?;
1472
1473        // If we errored out and early-returned, there might be commands which haven't been executed
1474        // and should be dropped.
1475        self.engine.clear_queues().await;
1476
1477        match exec_state.build_artifact_graph(&self.engine, program).await {
1478            Ok(_) => exec_result,
1479            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1480        }
1481    }
1482
1483    /// 'Import' std::prelude as the outermost scope.
1484    ///
1485    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1486    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1487        if exec_state.stack().memory.requires_std() {
1488            #[cfg(feature = "artifact-graph")]
1489            let initial_ops = exec_state.mod_local.artifacts.operations.len();
1490
1491            let path = vec!["std".to_owned(), "prelude".to_owned()];
1492            let resolved_path = ModulePath::from_std_import_path(&path)?;
1493            let id = self
1494                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1495                .await?;
1496            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1497
1498            exec_state.mut_stack().memory.set_std(module_memory);
1499
1500            // Operations generated by the prelude are not useful, so clear them
1501            // out.
1502            //
1503            // TODO: Should we also clear them out of each module so that they
1504            // don't appear in test output?
1505            #[cfg(feature = "artifact-graph")]
1506            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1507        }
1508
1509        Ok(())
1510    }
1511
1512    /// Get a snapshot of the current scene.
1513    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1514        // Zoom to fit.
1515        self.engine
1516            .send_modeling_cmd(
1517                uuid::Uuid::new_v4(),
1518                crate::execution::SourceRange::default(),
1519                &ModelingCmd::from(
1520                    mcmd::ZoomToFit::builder()
1521                        .object_ids(Default::default())
1522                        .animated(false)
1523                        .padding(0.1)
1524                        .build(),
1525                ),
1526            )
1527            .await
1528            .map_err(KclErrorWithOutputs::no_outputs)?;
1529
1530        // Send a snapshot request to the engine.
1531        let resp = self
1532            .engine
1533            .send_modeling_cmd(
1534                uuid::Uuid::new_v4(),
1535                crate::execution::SourceRange::default(),
1536                &ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
1537            )
1538            .await
1539            .map_err(KclErrorWithOutputs::no_outputs)?;
1540
1541        let OkWebSocketResponseData::Modeling {
1542            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1543        } = resp
1544        else {
1545            return Err(ExecError::BadPng(format!(
1546                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1547            )));
1548        };
1549        Ok(contents)
1550    }
1551
1552    /// Export the current scene as a CAD file.
1553    pub async fn export(
1554        &self,
1555        format: kittycad_modeling_cmds::format::OutputFormat3d,
1556    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1557        let resp = self
1558            .engine
1559            .send_modeling_cmd(
1560                uuid::Uuid::new_v4(),
1561                crate::SourceRange::default(),
1562                &kittycad_modeling_cmds::ModelingCmd::Export(
1563                    kittycad_modeling_cmds::Export::builder()
1564                        .entity_ids(vec![])
1565                        .format(format)
1566                        .build(),
1567                ),
1568            )
1569            .await?;
1570
1571        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1572            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1573                format!("Expected Export response, got {resp:?}",),
1574                vec![SourceRange::default()],
1575            )));
1576        };
1577
1578        Ok(files)
1579    }
1580
1581    /// Export the current scene as a STEP file.
1582    pub async fn export_step(
1583        &self,
1584        deterministic_time: bool,
1585    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1586        let files = self
1587            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1588                kittycad_modeling_cmds::format::step::export::Options {
1589                    coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1590                    created: if deterministic_time {
1591                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1592                            KclError::new_internal(crate::errors::KclErrorDetails::new(
1593                                format!("Failed to parse date: {e}"),
1594                                vec![SourceRange::default()],
1595                            ))
1596                        })?)
1597                    } else {
1598                        None
1599                    },
1600                },
1601            ))
1602            .await?;
1603
1604        Ok(files)
1605    }
1606
1607    pub async fn close(&self) {
1608        self.engine.close().await;
1609    }
1610}
1611
1612#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1613pub struct ArtifactId(Uuid);
1614
1615impl ArtifactId {
1616    pub fn new(uuid: Uuid) -> Self {
1617        Self(uuid)
1618    }
1619
1620    /// A placeholder artifact ID that will be filled in later.
1621    pub fn placeholder() -> Self {
1622        Self(Uuid::nil())
1623    }
1624
1625    /// The constraint artifact ID is a special. They don't need to be
1626    /// represented in the artifact graph.
1627    pub fn constraint() -> Self {
1628        Self(Uuid::nil())
1629    }
1630}
1631
1632impl From<Uuid> for ArtifactId {
1633    fn from(uuid: Uuid) -> Self {
1634        Self::new(uuid)
1635    }
1636}
1637
1638impl From<&Uuid> for ArtifactId {
1639    fn from(uuid: &Uuid) -> Self {
1640        Self::new(*uuid)
1641    }
1642}
1643
1644impl From<ArtifactId> for Uuid {
1645    fn from(id: ArtifactId) -> Self {
1646        id.0
1647    }
1648}
1649
1650impl From<&ArtifactId> for Uuid {
1651    fn from(id: &ArtifactId) -> Self {
1652        id.0
1653    }
1654}
1655
1656impl From<ModelingCmdId> for ArtifactId {
1657    fn from(id: ModelingCmdId) -> Self {
1658        Self::new(*id.as_ref())
1659    }
1660}
1661
1662impl From<&ModelingCmdId> for ArtifactId {
1663    fn from(id: &ModelingCmdId) -> Self {
1664        Self::new(*id.as_ref())
1665    }
1666}
1667
1668#[cfg(test)]
1669pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1670    parse_execute_with_project_dir(code, None).await
1671}
1672
1673#[cfg(test)]
1674pub(crate) async fn parse_execute_with_project_dir(
1675    code: &str,
1676    project_directory: Option<TypedPath>,
1677) -> Result<ExecTestResults, KclError> {
1678    let program = crate::Program::parse_no_errs(code)?;
1679
1680    let exec_ctxt = ExecutorContext {
1681        engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1682            |err| {
1683                KclError::new_internal(crate::errors::KclErrorDetails::new(
1684                    format!("Failed to create mock engine connection: {err}"),
1685                    vec![SourceRange::default()],
1686                ))
1687            },
1688        )?)),
1689        fs: Arc::new(crate::fs::FileManager::new()),
1690        settings: ExecutorSettings {
1691            project_directory,
1692            ..Default::default()
1693        },
1694        context_type: ContextType::Mock,
1695    };
1696    let mut exec_state = ExecState::new(&exec_ctxt);
1697    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1698
1699    Ok(ExecTestResults {
1700        program,
1701        mem_env: result.0,
1702        exec_ctxt,
1703        exec_state,
1704    })
1705}
1706
1707#[cfg(test)]
1708#[derive(Debug)]
1709pub(crate) struct ExecTestResults {
1710    program: crate::Program,
1711    mem_env: EnvironmentRef,
1712    exec_ctxt: ExecutorContext,
1713    exec_state: ExecState,
1714}
1715
1716/// There are several places where we want to traverse a KCL program or find a symbol in it,
1717/// but because KCL modules can import each other, we need to traverse multiple programs.
1718/// This stores multiple programs, keyed by their module ID for quick access.
1719#[cfg(feature = "artifact-graph")]
1720pub struct ProgramLookup {
1721    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1722}
1723
1724#[cfg(feature = "artifact-graph")]
1725impl ProgramLookup {
1726    // TODO: Could this store a reference to KCL programs instead of owning them?
1727    // i.e. take &state::ModuleInfoMap instead?
1728    pub fn new(
1729        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1730        module_infos: state::ModuleInfoMap,
1731    ) -> Self {
1732        let mut programs = IndexMap::with_capacity(module_infos.len());
1733        for (id, info) in module_infos {
1734            if let ModuleRepr::Kcl(program, _) = info.repr {
1735                programs.insert(id, program);
1736            }
1737        }
1738        programs.insert(ModuleId::default(), current);
1739        Self { programs }
1740    }
1741
1742    pub fn program_for_module(
1743        &self,
1744        module_id: ModuleId,
1745    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1746        self.programs.get(&module_id)
1747    }
1748}
1749
1750#[cfg(test)]
1751mod tests {
1752    use pretty_assertions::assert_eq;
1753
1754    use super::*;
1755    use crate::{
1756        ModuleId,
1757        errors::{KclErrorDetails, Severity},
1758        exec::NumericType,
1759        execution::{memory::Stack, types::RuntimeType},
1760    };
1761
1762    /// Convenience function to get a JSON value from memory and unwrap.
1763    #[track_caller]
1764    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1765        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1766    }
1767
1768    #[tokio::test(flavor = "multi_thread")]
1769    async fn test_execute_warn() {
1770        let text = "@blah";
1771        let result = parse_execute(text).await.unwrap();
1772        let errs = result.exec_state.errors();
1773        assert_eq!(errs.len(), 1);
1774        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1775        assert!(
1776            errs[0].message.contains("Unknown annotation"),
1777            "unexpected warning message: {}",
1778            errs[0].message
1779        );
1780    }
1781
1782    #[tokio::test(flavor = "multi_thread")]
1783    async fn test_execute_fn_definitions() {
1784        let ast = r#"fn def(@x) {
1785  return x
1786}
1787fn ghi(@x) {
1788  return x
1789}
1790fn jkl(@x) {
1791  return x
1792}
1793fn hmm(@x) {
1794  return x
1795}
1796
1797yo = 5 + 6
1798
1799abc = 3
1800identifierGuy = 5
1801part001 = startSketchOn(XY)
1802|> startProfile(at = [-1.2, 4.83])
1803|> line(end = [2.8, 0])
1804|> angledLine(angle = 100 + 100, length = 3.01)
1805|> angledLine(angle = abc, length = 3.02)
1806|> angledLine(angle = def(yo), length = 3.03)
1807|> angledLine(angle = ghi(2), length = 3.04)
1808|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1809|> close()
1810yo2 = hmm([identifierGuy + 5])"#;
1811
1812        parse_execute(ast).await.unwrap();
1813    }
1814
1815    #[tokio::test(flavor = "multi_thread")]
1816    async fn test_execute_with_pipe_substitutions_unary() {
1817        let ast = r#"myVar = 3
1818part001 = startSketchOn(XY)
1819  |> startProfile(at = [0, 0])
1820  |> line(end = [3, 4], tag = $seg01)
1821  |> line(end = [
1822  min([segLen(seg01), myVar]),
1823  -legLen(hypotenuse = segLen(seg01), leg = myVar)
1824])
1825"#;
1826
1827        parse_execute(ast).await.unwrap();
1828    }
1829
1830    #[tokio::test(flavor = "multi_thread")]
1831    async fn test_execute_with_pipe_substitutions() {
1832        let ast = r#"myVar = 3
1833part001 = startSketchOn(XY)
1834  |> startProfile(at = [0, 0])
1835  |> line(end = [3, 4], tag = $seg01)
1836  |> line(end = [
1837  min([segLen(seg01), myVar]),
1838  legLen(hypotenuse = segLen(seg01), leg = myVar)
1839])
1840"#;
1841
1842        parse_execute(ast).await.unwrap();
1843    }
1844
1845    #[tokio::test(flavor = "multi_thread")]
1846    async fn test_execute_with_inline_comment() {
1847        let ast = r#"baseThick = 1
1848armAngle = 60
1849
1850baseThickHalf = baseThick / 2
1851halfArmAngle = armAngle / 2
1852
1853arrExpShouldNotBeIncluded = [1, 2, 3]
1854objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1855
1856part001 = startSketchOn(XY)
1857  |> startProfile(at = [0, 0])
1858  |> yLine(endAbsolute = 1)
1859  |> xLine(length = 3.84) // selection-range-7ish-before-this
1860
1861variableBelowShouldNotBeIncluded = 3
1862"#;
1863
1864        parse_execute(ast).await.unwrap();
1865    }
1866
1867    #[tokio::test(flavor = "multi_thread")]
1868    async fn test_execute_with_function_literal_in_pipe() {
1869        let ast = r#"w = 20
1870l = 8
1871h = 10
1872
1873fn thing() {
1874  return -8
1875}
1876
1877firstExtrude = startSketchOn(XY)
1878  |> startProfile(at = [0,0])
1879  |> line(end = [0, l])
1880  |> line(end = [w, 0])
1881  |> line(end = [0, thing()])
1882  |> close()
1883  |> extrude(length = h)"#;
1884
1885        parse_execute(ast).await.unwrap();
1886    }
1887
1888    #[tokio::test(flavor = "multi_thread")]
1889    async fn test_execute_with_function_unary_in_pipe() {
1890        let ast = r#"w = 20
1891l = 8
1892h = 10
1893
1894fn thing(@x) {
1895  return -x
1896}
1897
1898firstExtrude = startSketchOn(XY)
1899  |> startProfile(at = [0,0])
1900  |> line(end = [0, l])
1901  |> line(end = [w, 0])
1902  |> line(end = [0, thing(8)])
1903  |> close()
1904  |> extrude(length = h)"#;
1905
1906        parse_execute(ast).await.unwrap();
1907    }
1908
1909    #[tokio::test(flavor = "multi_thread")]
1910    async fn test_execute_with_function_array_in_pipe() {
1911        let ast = r#"w = 20
1912l = 8
1913h = 10
1914
1915fn thing(@x) {
1916  return [0, -x]
1917}
1918
1919firstExtrude = startSketchOn(XY)
1920  |> startProfile(at = [0,0])
1921  |> line(end = [0, l])
1922  |> line(end = [w, 0])
1923  |> line(end = thing(8))
1924  |> close()
1925  |> extrude(length = h)"#;
1926
1927        parse_execute(ast).await.unwrap();
1928    }
1929
1930    #[tokio::test(flavor = "multi_thread")]
1931    async fn test_execute_with_function_call_in_pipe() {
1932        let ast = r#"w = 20
1933l = 8
1934h = 10
1935
1936fn other_thing(@y) {
1937  return -y
1938}
1939
1940fn thing(@x) {
1941  return other_thing(x)
1942}
1943
1944firstExtrude = startSketchOn(XY)
1945  |> startProfile(at = [0,0])
1946  |> line(end = [0, l])
1947  |> line(end = [w, 0])
1948  |> line(end = [0, thing(8)])
1949  |> close()
1950  |> extrude(length = h)"#;
1951
1952        parse_execute(ast).await.unwrap();
1953    }
1954
1955    #[tokio::test(flavor = "multi_thread")]
1956    async fn test_execute_with_function_sketch() {
1957        let ast = r#"fn box(h, l, w) {
1958 myBox = startSketchOn(XY)
1959    |> startProfile(at = [0,0])
1960    |> line(end = [0, l])
1961    |> line(end = [w, 0])
1962    |> line(end = [0, -l])
1963    |> close()
1964    |> extrude(length = h)
1965
1966  return myBox
1967}
1968
1969fnBox = box(h = 3, l = 6, w = 10)"#;
1970
1971        parse_execute(ast).await.unwrap();
1972    }
1973
1974    #[tokio::test(flavor = "multi_thread")]
1975    async fn test_get_member_of_object_with_function_period() {
1976        let ast = r#"fn box(@obj) {
1977 myBox = startSketchOn(XY)
1978    |> startProfile(at = obj.start)
1979    |> line(end = [0, obj.l])
1980    |> line(end = [obj.w, 0])
1981    |> line(end = [0, -obj.l])
1982    |> close()
1983    |> extrude(length = obj.h)
1984
1985  return myBox
1986}
1987
1988thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1989"#;
1990        parse_execute(ast).await.unwrap();
1991    }
1992
1993    #[tokio::test(flavor = "multi_thread")]
1994    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
1995    async fn test_object_member_starting_pipeline() {
1996        let ast = r#"
1997fn test2() {
1998  return {
1999    thing: startSketchOn(XY)
2000      |> startProfile(at = [0, 0])
2001      |> line(end = [0, 1])
2002      |> line(end = [1, 0])
2003      |> line(end = [0, -1])
2004      |> close()
2005  }
2006}
2007
2008x2 = test2()
2009
2010x2.thing
2011  |> extrude(length = 10)
2012"#;
2013        parse_execute(ast).await.unwrap();
2014    }
2015
2016    #[tokio::test(flavor = "multi_thread")]
2017    #[ignore] // ignore til we get loops
2018    async fn test_execute_with_function_sketch_loop_objects() {
2019        let ast = r#"fn box(obj) {
2020let myBox = startSketchOn(XY)
2021    |> startProfile(at = obj.start)
2022    |> line(end = [0, obj.l])
2023    |> line(end = [obj.w, 0])
2024    |> line(end = [0, -obj.l])
2025    |> close()
2026    |> extrude(length = obj.h)
2027
2028  return myBox
2029}
2030
2031for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
2032  thisBox = box(var)
2033}"#;
2034
2035        parse_execute(ast).await.unwrap();
2036    }
2037
2038    #[tokio::test(flavor = "multi_thread")]
2039    #[ignore] // ignore til we get loops
2040    async fn test_execute_with_function_sketch_loop_array() {
2041        let ast = r#"fn box(h, l, w, start) {
2042 myBox = startSketchOn(XY)
2043    |> startProfile(at = [0,0])
2044    |> line(end = [0, l])
2045    |> line(end = [w, 0])
2046    |> line(end = [0, -l])
2047    |> close()
2048    |> extrude(length = h)
2049
2050  return myBox
2051}
2052
2053
2054for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
2055  const thisBox = box(var[0], var[1], var[2], var[3])
2056}"#;
2057
2058        parse_execute(ast).await.unwrap();
2059    }
2060
2061    #[tokio::test(flavor = "multi_thread")]
2062    async fn test_get_member_of_array_with_function() {
2063        let ast = r#"fn box(@arr) {
2064 myBox =startSketchOn(XY)
2065    |> startProfile(at = arr[0])
2066    |> line(end = [0, arr[1]])
2067    |> line(end = [arr[2], 0])
2068    |> line(end = [0, -arr[1]])
2069    |> close()
2070    |> extrude(length = arr[3])
2071
2072  return myBox
2073}
2074
2075thisBox = box([[0,0], 6, 10, 3])
2076
2077"#;
2078        parse_execute(ast).await.unwrap();
2079    }
2080
2081    #[tokio::test(flavor = "multi_thread")]
2082    async fn test_function_cannot_access_future_definitions() {
2083        let ast = r#"
2084fn returnX() {
2085  // x shouldn't be defined yet.
2086  return x
2087}
2088
2089x = 5
2090
2091answer = returnX()"#;
2092
2093        let result = parse_execute(ast).await;
2094        let err = result.unwrap_err();
2095        assert_eq!(err.message(), "`x` is not defined");
2096    }
2097
2098    #[tokio::test(flavor = "multi_thread")]
2099    async fn test_override_prelude() {
2100        let text = "PI = 3.0";
2101        let result = parse_execute(text).await.unwrap();
2102        let errs = result.exec_state.errors();
2103        assert!(errs.is_empty());
2104    }
2105
2106    #[tokio::test(flavor = "multi_thread")]
2107    async fn type_aliases() {
2108        let text = r#"@settings(experimentalFeatures = allow)
2109type MyTy = [number; 2]
2110fn foo(@x: MyTy) {
2111    return x[0]
2112}
2113
2114foo([0, 1])
2115
2116type Other = MyTy | Helix
2117"#;
2118        let result = parse_execute(text).await.unwrap();
2119        let errs = result.exec_state.errors();
2120        assert!(errs.is_empty());
2121    }
2122
2123    #[tokio::test(flavor = "multi_thread")]
2124    async fn test_cannot_shebang_in_fn() {
2125        let ast = r#"
2126fn foo() {
2127  #!hello
2128  return true
2129}
2130
2131foo
2132"#;
2133
2134        let result = parse_execute(ast).await;
2135        let err = result.unwrap_err();
2136        assert_eq!(
2137            err,
2138            KclError::new_syntax(KclErrorDetails::new(
2139                "Unexpected token: #".to_owned(),
2140                vec![SourceRange::new(14, 15, ModuleId::default())],
2141            )),
2142        );
2143    }
2144
2145    #[tokio::test(flavor = "multi_thread")]
2146    async fn test_pattern_transform_function_cannot_access_future_definitions() {
2147        let ast = r#"
2148fn transform(@replicaId) {
2149  // x shouldn't be defined yet.
2150  scale = x
2151  return {
2152    translate = [0, 0, replicaId * 10],
2153    scale = [scale, 1, 0],
2154  }
2155}
2156
2157fn layer() {
2158  return startSketchOn(XY)
2159    |> circle( center= [0, 0], radius= 1, tag = $tag1)
2160    |> extrude(length = 10)
2161}
2162
2163x = 5
2164
2165// The 10 layers are replicas of each other, with a transform applied to each.
2166shape = layer() |> patternTransform(instances = 10, transform = transform)
2167"#;
2168
2169        let result = parse_execute(ast).await;
2170        let err = result.unwrap_err();
2171        assert_eq!(err.message(), "`x` is not defined",);
2172    }
2173
2174    // ADAM: Move some of these into simulation tests.
2175
2176    #[tokio::test(flavor = "multi_thread")]
2177    async fn test_math_execute_with_functions() {
2178        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2179        let result = parse_execute(ast).await.unwrap();
2180        assert_eq!(
2181            5.0,
2182            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2183                .as_f64()
2184                .unwrap()
2185        );
2186    }
2187
2188    #[tokio::test(flavor = "multi_thread")]
2189    async fn test_math_execute() {
2190        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2191        let result = parse_execute(ast).await.unwrap();
2192        assert_eq!(
2193            7.4,
2194            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2195                .as_f64()
2196                .unwrap()
2197        );
2198    }
2199
2200    #[tokio::test(flavor = "multi_thread")]
2201    async fn test_math_execute_start_negative() {
2202        let ast = r#"myVar = -5 + 6"#;
2203        let result = parse_execute(ast).await.unwrap();
2204        assert_eq!(
2205            1.0,
2206            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2207                .as_f64()
2208                .unwrap()
2209        );
2210    }
2211
2212    #[tokio::test(flavor = "multi_thread")]
2213    async fn test_math_execute_with_pi() {
2214        let ast = r#"myVar = PI * 2"#;
2215        let result = parse_execute(ast).await.unwrap();
2216        assert_eq!(
2217            std::f64::consts::TAU,
2218            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2219                .as_f64()
2220                .unwrap()
2221        );
2222    }
2223
2224    #[tokio::test(flavor = "multi_thread")]
2225    async fn test_math_define_decimal_without_leading_zero() {
2226        let ast = r#"thing = .4 + 7"#;
2227        let result = parse_execute(ast).await.unwrap();
2228        assert_eq!(
2229            7.4,
2230            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2231                .as_f64()
2232                .unwrap()
2233        );
2234    }
2235
2236    #[tokio::test(flavor = "multi_thread")]
2237    async fn pass_std_to_std() {
2238        let ast = r#"sketch001 = startSketchOn(XY)
2239profile001 = circle(sketch001, center = [0, 0], radius = 2)
2240extrude001 = extrude(profile001, length = 5)
2241extrudes = patternLinear3d(
2242  extrude001,
2243  instances = 3,
2244  distance = 5,
2245  axis = [1, 1, 0],
2246)
2247clone001 = map(extrudes, f = clone)
2248"#;
2249        parse_execute(ast).await.unwrap();
2250    }
2251
2252    #[tokio::test(flavor = "multi_thread")]
2253    async fn test_array_reduce_nested_array() {
2254        let code = r#"
2255fn id(@el, accum)  { return accum }
2256
2257answer = reduce([], initial=[[[0,0]]], f=id)
2258"#;
2259        let result = parse_execute(code).await.unwrap();
2260        assert_eq!(
2261            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2262            KclValue::HomArray {
2263                value: vec![KclValue::HomArray {
2264                    value: vec![KclValue::HomArray {
2265                        value: vec![
2266                            KclValue::Number {
2267                                value: 0.0,
2268                                ty: NumericType::default(),
2269                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2270                            },
2271                            KclValue::Number {
2272                                value: 0.0,
2273                                ty: NumericType::default(),
2274                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2275                            }
2276                        ],
2277                        ty: RuntimeType::any(),
2278                    }],
2279                    ty: RuntimeType::any(),
2280                }],
2281                ty: RuntimeType::any(),
2282            }
2283        );
2284    }
2285
2286    #[tokio::test(flavor = "multi_thread")]
2287    async fn test_zero_param_fn() {
2288        let ast = r#"sigmaAllow = 35000 // psi
2289leg1 = 5 // inches
2290leg2 = 8 // inches
2291fn thickness() { return 0.56 }
2292
2293bracket = startSketchOn(XY)
2294  |> startProfile(at = [0,0])
2295  |> line(end = [0, leg1])
2296  |> line(end = [leg2, 0])
2297  |> line(end = [0, -thickness()])
2298  |> line(end = [-leg2 + thickness(), 0])
2299"#;
2300        parse_execute(ast).await.unwrap();
2301    }
2302
2303    #[tokio::test(flavor = "multi_thread")]
2304    async fn test_unary_operator_not_succeeds() {
2305        let ast = r#"
2306fn returnTrue() { return !false }
2307t = true
2308f = false
2309notTrue = !t
2310notFalse = !f
2311c = !!true
2312d = !returnTrue()
2313
2314assertIs(!false, error = "expected to pass")
2315
2316fn check(x) {
2317  assertIs(!x, error = "expected argument to be false")
2318  return true
2319}
2320check(x = false)
2321"#;
2322        let result = parse_execute(ast).await.unwrap();
2323        assert_eq!(
2324            false,
2325            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2326                .as_bool()
2327                .unwrap()
2328        );
2329        assert_eq!(
2330            true,
2331            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2332                .as_bool()
2333                .unwrap()
2334        );
2335        assert_eq!(
2336            true,
2337            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2338                .as_bool()
2339                .unwrap()
2340        );
2341        assert_eq!(
2342            false,
2343            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2344                .as_bool()
2345                .unwrap()
2346        );
2347    }
2348
2349    #[tokio::test(flavor = "multi_thread")]
2350    async fn test_unary_operator_not_on_non_bool_fails() {
2351        let code1 = r#"
2352// Yup, this is null.
2353myNull = 0 / 0
2354notNull = !myNull
2355"#;
2356        assert_eq!(
2357            parse_execute(code1).await.unwrap_err().message(),
2358            "Cannot apply unary operator ! to non-boolean value: a number",
2359        );
2360
2361        let code2 = "notZero = !0";
2362        assert_eq!(
2363            parse_execute(code2).await.unwrap_err().message(),
2364            "Cannot apply unary operator ! to non-boolean value: a number",
2365        );
2366
2367        let code3 = r#"
2368notEmptyString = !""
2369"#;
2370        assert_eq!(
2371            parse_execute(code3).await.unwrap_err().message(),
2372            "Cannot apply unary operator ! to non-boolean value: a string",
2373        );
2374
2375        let code4 = r#"
2376obj = { a = 1 }
2377notMember = !obj.a
2378"#;
2379        assert_eq!(
2380            parse_execute(code4).await.unwrap_err().message(),
2381            "Cannot apply unary operator ! to non-boolean value: a number",
2382        );
2383
2384        let code5 = "
2385a = []
2386notArray = !a";
2387        assert_eq!(
2388            parse_execute(code5).await.unwrap_err().message(),
2389            "Cannot apply unary operator ! to non-boolean value: an empty array",
2390        );
2391
2392        let code6 = "
2393x = {}
2394notObject = !x";
2395        assert_eq!(
2396            parse_execute(code6).await.unwrap_err().message(),
2397            "Cannot apply unary operator ! to non-boolean value: an object",
2398        );
2399
2400        let code7 = "
2401fn x() { return 1 }
2402notFunction = !x";
2403        let fn_err = parse_execute(code7).await.unwrap_err();
2404        // These are currently printed out as JSON objects, so we don't want to
2405        // check the full error.
2406        assert!(
2407            fn_err
2408                .message()
2409                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2410            "Actual error: {fn_err:?}"
2411        );
2412
2413        let code8 = "
2414myTagDeclarator = $myTag
2415notTagDeclarator = !myTagDeclarator";
2416        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2417        // These are currently printed out as JSON objects, so we don't want to
2418        // check the full error.
2419        assert!(
2420            tag_declarator_err
2421                .message()
2422                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2423            "Actual error: {tag_declarator_err:?}"
2424        );
2425
2426        let code9 = "
2427myTagDeclarator = $myTag
2428notTagIdentifier = !myTag";
2429        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2430        // These are currently printed out as JSON objects, so we don't want to
2431        // check the full error.
2432        assert!(
2433            tag_identifier_err
2434                .message()
2435                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2436            "Actual error: {tag_identifier_err:?}"
2437        );
2438
2439        let code10 = "notPipe = !(1 |> 2)";
2440        assert_eq!(
2441            // TODO: We don't currently parse this, but we should.  It should be
2442            // a runtime error instead.
2443            parse_execute(code10).await.unwrap_err(),
2444            KclError::new_syntax(KclErrorDetails::new(
2445                "Unexpected token: !".to_owned(),
2446                vec![SourceRange::new(10, 11, ModuleId::default())],
2447            ))
2448        );
2449
2450        let code11 = "
2451fn identity(x) { return x }
2452notPipeSub = 1 |> identity(!%))";
2453        assert_eq!(
2454            // TODO: We don't currently parse this, but we should.  It should be
2455            // a runtime error instead.
2456            parse_execute(code11).await.unwrap_err(),
2457            KclError::new_syntax(KclErrorDetails::new(
2458                "There was an unexpected `!`. Try removing it.".to_owned(),
2459                vec![SourceRange::new(56, 57, ModuleId::default())],
2460            ))
2461        );
2462
2463        // TODO: Add these tests when we support these types.
2464        // let notNan = !NaN
2465        // let notInfinity = !Infinity
2466    }
2467
2468    #[tokio::test(flavor = "multi_thread")]
2469    async fn test_start_sketch_on_invalid_kwargs() {
2470        let current_dir = std::env::current_dir().unwrap();
2471        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2472        let mut code = std::fs::read_to_string(&path).unwrap();
2473        assert_eq!(
2474            parse_execute(&code).await.unwrap_err().message(),
2475            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2476        );
2477
2478        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2479        code = std::fs::read_to_string(&path).unwrap();
2480
2481        assert_eq!(
2482            parse_execute(&code).await.unwrap_err().message(),
2483            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2484        );
2485
2486        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2487        code = std::fs::read_to_string(&path).unwrap();
2488
2489        assert_eq!(
2490            parse_execute(&code).await.unwrap_err().message(),
2491            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2492        );
2493
2494        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2495        code = std::fs::read_to_string(&path).unwrap();
2496
2497        assert_eq!(
2498            parse_execute(&code).await.unwrap_err().message(),
2499            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2500        );
2501
2502        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2503        code = std::fs::read_to_string(&path).unwrap();
2504
2505        assert_eq!(
2506            parse_execute(&code).await.unwrap_err().message(),
2507            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2508        );
2509    }
2510
2511    #[tokio::test(flavor = "multi_thread")]
2512    async fn test_math_negative_variable_in_binary_expression() {
2513        let ast = r#"sigmaAllow = 35000 // psi
2514width = 1 // inch
2515
2516p = 150 // lbs
2517distance = 6 // inches
2518FOS = 2
2519
2520leg1 = 5 // inches
2521leg2 = 8 // inches
2522
2523thickness_squared = distance * p * FOS * 6 / sigmaAllow
2524thickness = 0.56 // inches. App does not support square root function yet
2525
2526bracket = startSketchOn(XY)
2527  |> startProfile(at = [0,0])
2528  |> line(end = [0, leg1])
2529  |> line(end = [leg2, 0])
2530  |> line(end = [0, -thickness])
2531  |> line(end = [-leg2 + thickness, 0])
2532"#;
2533        parse_execute(ast).await.unwrap();
2534    }
2535
2536    #[tokio::test(flavor = "multi_thread")]
2537    async fn test_execute_function_no_return() {
2538        let ast = r#"fn test(@origin) {
2539  origin
2540}
2541
2542test([0, 0])
2543"#;
2544        let result = parse_execute(ast).await;
2545        assert!(result.is_err());
2546        assert!(result.unwrap_err().to_string().contains("undefined"));
2547    }
2548
2549    #[tokio::test(flavor = "multi_thread")]
2550    async fn test_max_stack_size_exceeded_error() {
2551        let ast = r#"
2552fn forever(@n) {
2553  return 1 + forever(n)
2554}
2555
2556forever(1)
2557"#;
2558        let result = parse_execute(ast).await;
2559        let err = result.unwrap_err();
2560        assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
2561    }
2562
2563    #[tokio::test(flavor = "multi_thread")]
2564    async fn test_math_doubly_nested_parens() {
2565        let ast = r#"sigmaAllow = 35000 // psi
2566width = 4 // inch
2567p = 150 // Force on shelf - lbs
2568distance = 6 // inches
2569FOS = 2
2570leg1 = 5 // inches
2571leg2 = 8 // inches
2572thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2573thickness = 0.32 // inches. App does not support square root function yet
2574bracket = startSketchOn(XY)
2575  |> startProfile(at = [0,0])
2576    |> line(end = [0, leg1])
2577  |> line(end = [leg2, 0])
2578  |> line(end = [0, -thickness])
2579  |> line(end = [-1 * leg2 + thickness, 0])
2580  |> line(end = [0, -1 * leg1 + thickness])
2581  |> close()
2582  |> extrude(length = width)
2583"#;
2584        parse_execute(ast).await.unwrap();
2585    }
2586
2587    #[tokio::test(flavor = "multi_thread")]
2588    async fn test_math_nested_parens_one_less() {
2589        let ast = r#" sigmaAllow = 35000 // psi
2590width = 4 // inch
2591p = 150 // Force on shelf - lbs
2592distance = 6 // inches
2593FOS = 2
2594leg1 = 5 // inches
2595leg2 = 8 // inches
2596thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2597thickness = 0.32 // inches. App does not support square root function yet
2598bracket = startSketchOn(XY)
2599  |> startProfile(at = [0,0])
2600    |> line(end = [0, leg1])
2601  |> line(end = [leg2, 0])
2602  |> line(end = [0, -thickness])
2603  |> line(end = [-1 * leg2 + thickness, 0])
2604  |> line(end = [0, -1 * leg1 + thickness])
2605  |> close()
2606  |> extrude(length = width)
2607"#;
2608        parse_execute(ast).await.unwrap();
2609    }
2610
2611    #[tokio::test(flavor = "multi_thread")]
2612    async fn test_fn_as_operand() {
2613        let ast = r#"fn f() { return 1 }
2614x = f()
2615y = x + 1
2616z = f() + 1
2617w = f() + f()
2618"#;
2619        parse_execute(ast).await.unwrap();
2620    }
2621
2622    #[tokio::test(flavor = "multi_thread")]
2623    async fn kcl_test_ids_stable_between_executions() {
2624        let code = r#"sketch001 = startSketchOn(XZ)
2625|> startProfile(at = [61.74, 206.13])
2626|> xLine(length = 305.11, tag = $seg01)
2627|> yLine(length = -291.85)
2628|> xLine(length = -segLen(seg01))
2629|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2630|> close()
2631|> extrude(length = 40.14)
2632|> shell(
2633    thickness = 3.14,
2634    faces = [seg01]
2635)
2636"#;
2637
2638        let ctx = crate::test_server::new_context(true, None).await.unwrap();
2639        let old_program = crate::Program::parse_no_errs(code).unwrap();
2640
2641        // Execute the program.
2642        if let Err(err) = ctx.run_with_caching(old_program).await {
2643            let report = err.into_miette_report_with_outputs(code).unwrap();
2644            let report = miette::Report::new(report);
2645            panic!("Error executing program: {report:?}");
2646        }
2647
2648        // Get the id_generator from the first execution.
2649        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2650
2651        let code = r#"sketch001 = startSketchOn(XZ)
2652|> startProfile(at = [62.74, 206.13])
2653|> xLine(length = 305.11, tag = $seg01)
2654|> yLine(length = -291.85)
2655|> xLine(length = -segLen(seg01))
2656|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2657|> close()
2658|> extrude(length = 40.14)
2659|> shell(
2660    faces = [seg01],
2661    thickness = 3.14,
2662)
2663"#;
2664
2665        // Execute a slightly different program again.
2666        let program = crate::Program::parse_no_errs(code).unwrap();
2667        // Execute the program.
2668        ctx.run_with_caching(program).await.unwrap();
2669
2670        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2671
2672        assert_eq!(id_generator, new_id_generator);
2673    }
2674
2675    #[tokio::test(flavor = "multi_thread")]
2676    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2677        let code = r#"sketch001 = startSketchOn(XZ)
2678|> startProfile(at = [61.74, 206.13])
2679|> xLine(length = 305.11, tag = $seg01)
2680|> yLine(length = -291.85)
2681|> xLine(length = -segLen(seg01))
2682|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2683|> close()
2684|> extrude(length = 40.14)
2685|> shell(
2686    thickness = 3.14,
2687    faces = [seg01]
2688)
2689"#;
2690
2691        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2692        let old_program = crate::Program::parse_no_errs(code).unwrap();
2693
2694        // Execute the program.
2695        ctx.run_with_caching(old_program.clone()).await.unwrap();
2696
2697        let settings_state = cache::read_old_ast().await.unwrap().settings;
2698
2699        // Ensure the settings are as expected.
2700        assert_eq!(settings_state, ctx.settings);
2701
2702        // Change a setting.
2703        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2704
2705        // Execute the program.
2706        ctx.run_with_caching(old_program.clone()).await.unwrap();
2707
2708        let settings_state = cache::read_old_ast().await.unwrap().settings;
2709
2710        // Ensure the settings are as expected.
2711        assert_eq!(settings_state, ctx.settings);
2712
2713        // Change a setting.
2714        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2715
2716        // Execute the program.
2717        ctx.run_with_caching(old_program).await.unwrap();
2718
2719        let settings_state = cache::read_old_ast().await.unwrap().settings;
2720
2721        // Ensure the settings are as expected.
2722        assert_eq!(settings_state, ctx.settings);
2723
2724        ctx.close().await;
2725    }
2726
2727    #[tokio::test(flavor = "multi_thread")]
2728    async fn mock_after_not_mock() {
2729        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2730        let program = crate::Program::parse_no_errs("x = 2").unwrap();
2731        let result = ctx.run_with_caching(program).await.unwrap();
2732        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2733
2734        let ctx2 = ExecutorContext::new_mock(None).await;
2735        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2736        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
2737        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2738
2739        ctx.close().await;
2740        ctx2.close().await;
2741    }
2742
2743    #[cfg(feature = "artifact-graph")]
2744    #[tokio::test(flavor = "multi_thread")]
2745    async fn mock_has_stable_ids() {
2746        let ctx = ExecutorContext::new_mock(None).await;
2747        let mock_config = MockConfig {
2748            use_prev_memory: false,
2749            ..Default::default()
2750        };
2751        let code = "sk = startSketchOn(XY)
2752        |> startProfile(at = [0, 0])";
2753        let program = crate::Program::parse_no_errs(code).unwrap();
2754        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
2755        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2756        assert!(!ids.is_empty(), "IDs should not be empty");
2757
2758        let ctx2 = ExecutorContext::new_mock(None).await;
2759        let program2 = crate::Program::parse_no_errs(code).unwrap();
2760        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
2761        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2762
2763        assert_eq!(ids, ids2, "Generated IDs should match");
2764        ctx.close().await;
2765        ctx2.close().await;
2766    }
2767
2768    #[cfg(feature = "artifact-graph")]
2769    #[tokio::test(flavor = "multi_thread")]
2770    async fn sim_sketch_mode_real_mock_real() {
2771        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2772        let code = r#"sketch001 = startSketchOn(XY)
2773profile001 = startProfile(sketch001, at = [0, 0])
2774  |> line(end = [10, 0])
2775  |> line(end = [0, 10])
2776  |> line(end = [-10, 0])
2777  |> line(end = [0, -10])
2778  |> close()
2779"#;
2780        let program = crate::Program::parse_no_errs(code).unwrap();
2781        let result = ctx.run_with_caching(program).await.unwrap();
2782        assert_eq!(result.operations.len(), 1);
2783
2784        let mock_ctx = ExecutorContext::new_mock(None).await;
2785        let mock_program = crate::Program::parse_no_errs(code).unwrap();
2786        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
2787        assert_eq!(mock_result.operations.len(), 1);
2788
2789        let code2 = code.to_owned()
2790            + r#"
2791extrude001 = extrude(profile001, length = 10)
2792"#;
2793        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
2794        let result = ctx.run_with_caching(program2).await.unwrap();
2795        assert_eq!(result.operations.len(), 2);
2796
2797        ctx.close().await;
2798        mock_ctx.close().await;
2799    }
2800
2801    #[tokio::test(flavor = "multi_thread")]
2802    async fn read_tag_version() {
2803        let ast = r#"fn bar(@t) {
2804  return startSketchOn(XY)
2805    |> startProfile(at = [0,0])
2806    |> angledLine(
2807        angle = -60,
2808        length = segLen(t),
2809    )
2810    |> line(end = [0, 0])
2811    |> close()
2812}
2813
2814sketch = startSketchOn(XY)
2815  |> startProfile(at = [0,0])
2816  |> line(end = [0, 10])
2817  |> line(end = [10, 0], tag = $tag0)
2818  |> line(endAbsolute = [0, 0])
2819
2820fn foo() {
2821  // tag0 tags an edge
2822  return bar(tag0)
2823}
2824
2825solid = sketch |> extrude(length = 10)
2826// tag0 tags a face
2827sketch2 = startSketchOn(solid, face = tag0)
2828  |> startProfile(at = [0,0])
2829  |> line(end = [0, 1])
2830  |> line(end = [1, 0])
2831  |> line(end = [0, 0])
2832
2833foo() |> extrude(length = 1)
2834"#;
2835        parse_execute(ast).await.unwrap();
2836    }
2837
2838    #[tokio::test(flavor = "multi_thread")]
2839    async fn experimental() {
2840        let code = r#"
2841startSketchOn(XY)
2842  |> startProfile(at = [0, 0], tag = $start)
2843  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2844"#;
2845        let result = parse_execute(code).await.unwrap();
2846        let errors = result.exec_state.errors();
2847        assert_eq!(errors.len(), 1);
2848        assert_eq!(errors[0].severity, Severity::Error);
2849        let msg = &errors[0].message;
2850        assert!(msg.contains("experimental"), "found {msg}");
2851
2852        let code = r#"@settings(experimentalFeatures = allow)
2853startSketchOn(XY)
2854  |> startProfile(at = [0, 0], tag = $start)
2855  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2856"#;
2857        let result = parse_execute(code).await.unwrap();
2858        let errors = result.exec_state.errors();
2859        assert!(errors.is_empty());
2860
2861        let code = r#"@settings(experimentalFeatures = warn)
2862startSketchOn(XY)
2863  |> startProfile(at = [0, 0], tag = $start)
2864  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2865"#;
2866        let result = parse_execute(code).await.unwrap();
2867        let errors = result.exec_state.errors();
2868        assert_eq!(errors.len(), 1);
2869        assert_eq!(errors[0].severity, Severity::Warning);
2870        let msg = &errors[0].message;
2871        assert!(msg.contains("experimental"), "found {msg}");
2872
2873        let code = r#"@settings(experimentalFeatures = deny)
2874startSketchOn(XY)
2875  |> startProfile(at = [0, 0], tag = $start)
2876  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2877"#;
2878        let result = parse_execute(code).await.unwrap();
2879        let errors = result.exec_state.errors();
2880        assert_eq!(errors.len(), 1);
2881        assert_eq!(errors[0].severity, Severity::Error);
2882        let msg = &errors[0].message;
2883        assert!(msg.contains("experimental"), "found {msg}");
2884
2885        let code = r#"@settings(experimentalFeatures = foo)
2886startSketchOn(XY)
2887  |> startProfile(at = [0, 0], tag = $start)
2888  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2889"#;
2890        parse_execute(code).await.unwrap_err();
2891    }
2892}