kcl_lib/execution/
mod.rs

1//! The executor for the AST.
2
3use std::sync::Arc;
4
5use anyhow::Result;
6#[cfg(feature = "artifact-graph")]
7pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketchOnFace, StartSketchOnPlane};
8use cache::GlobalState;
9pub use cache::{bust_cache, clear_mem_cache};
10#[cfg(feature = "artifact-graph")]
11pub use cad_op::{Group, Operation};
12pub use geometry::*;
13pub use id_generator::IdGenerator;
14pub(crate) use import::PreImportedGeometry;
15use indexmap::IndexMap;
16pub use kcl_value::{KclObjectFields, KclValue};
17use kcmc::{
18    each_cmd as mcmd,
19    ok_response::{output::TakeSnapshot, OkModelingCmdResponse},
20    websocket::{ModelingSessionData, OkWebSocketResponseData},
21    ImageFormat, ModelingCmd,
22};
23use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
24pub use memory::EnvironmentRef;
25use schemars::JsonSchema;
26use serde::{Deserialize, Serialize};
27pub use state::{ExecState, MetaSettings};
28use uuid::Uuid;
29
30use crate::{
31    engine::EngineManager,
32    errors::{KclError, KclErrorDetails},
33    execution::{
34        cache::{CacheInformation, CacheResult},
35        import_graph::{Universe, UniverseMap},
36        typed_path::TypedPath,
37        types::{UnitAngle, UnitLen},
38    },
39    fs::FileManager,
40    modules::{ModuleId, ModulePath, ModuleRepr},
41    parsing::ast::types::{Expr, ImportPath, NodeRef},
42    source_range::SourceRange,
43    CompilationError, ExecError, KclErrorWithOutputs,
44};
45
46pub(crate) mod annotations;
47#[cfg(feature = "artifact-graph")]
48mod artifact;
49pub(crate) mod cache;
50mod cad_op;
51mod exec_ast;
52pub mod fn_call;
53mod geometry;
54mod id_generator;
55mod import;
56mod import_graph;
57pub(crate) mod kcl_value;
58mod memory;
59mod state;
60pub mod typed_path;
61pub(crate) mod types;
62
63enum StatementKind<'a> {
64    Declaration { name: &'a str },
65    Expression,
66}
67
68/// Outcome of executing a program.  This is used in TS.
69#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
70#[ts(export)]
71#[serde(rename_all = "camelCase")]
72pub struct ExecOutcome {
73    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
74    pub variables: IndexMap<String, KclValue>,
75    /// Operations that have been performed in execution order, for display in
76    /// the Feature Tree.
77    #[cfg(feature = "artifact-graph")]
78    pub operations: Vec<Operation>,
79    /// Output commands to allow building the artifact graph by the caller.
80    #[cfg(feature = "artifact-graph")]
81    pub artifact_commands: Vec<ArtifactCommand>,
82    /// Output artifact graph.
83    #[cfg(feature = "artifact-graph")]
84    pub artifact_graph: ArtifactGraph,
85    /// Non-fatal errors and warnings.
86    pub errors: Vec<CompilationError>,
87    /// File Names in module Id array index order
88    pub filenames: IndexMap<ModuleId, ModulePath>,
89    /// The default planes.
90    pub default_planes: Option<DefaultPlanes>,
91}
92
93#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
94#[ts(export)]
95#[serde(rename_all = "camelCase")]
96pub struct DefaultPlanes {
97    pub xy: uuid::Uuid,
98    pub xz: uuid::Uuid,
99    pub yz: uuid::Uuid,
100    pub neg_xy: uuid::Uuid,
101    pub neg_xz: uuid::Uuid,
102    pub neg_yz: uuid::Uuid,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS, JsonSchema)]
106#[ts(export)]
107#[serde(tag = "type", rename_all = "camelCase")]
108pub struct TagIdentifier {
109    pub value: String,
110    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
111    // was written.
112    #[serde(skip)]
113    pub info: Vec<(usize, TagEngineInfo)>,
114    #[serde(skip)]
115    pub meta: Vec<Metadata>,
116}
117
118impl TagIdentifier {
119    /// Get the tag info for this tag at a specified epoch.
120    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
121        for (e, info) in self.info.iter().rev() {
122            if *e <= at_epoch {
123                return Some(info);
124            }
125        }
126
127        None
128    }
129
130    /// Get the most recent tag info for this tag.
131    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
132        self.info.last().map(|i| &i.1)
133    }
134
135    /// Add info from a different instance of this tag.
136    pub fn merge_info(&mut self, other: &TagIdentifier) {
137        assert_eq!(&self.value, &other.value);
138        for (oe, ot) in &other.info {
139            if let Some((e, t)) = self.info.last_mut() {
140                // If there is newer info, then skip this iteration.
141                if *e > *oe {
142                    continue;
143                }
144                // If we're in the same epoch, then overwrite.
145                if e == oe {
146                    *t = ot.clone();
147                    continue;
148                }
149            }
150            self.info.push((*oe, ot.clone()));
151        }
152    }
153}
154
155impl Eq for TagIdentifier {}
156
157impl std::fmt::Display for TagIdentifier {
158    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
159        write!(f, "{}", self.value)
160    }
161}
162
163impl std::str::FromStr for TagIdentifier {
164    type Err = KclError;
165
166    fn from_str(s: &str) -> Result<Self, Self::Err> {
167        Ok(Self {
168            value: s.to_string(),
169            info: Vec::new(),
170            meta: Default::default(),
171        })
172    }
173}
174
175impl Ord for TagIdentifier {
176    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
177        self.value.cmp(&other.value)
178    }
179}
180
181impl PartialOrd for TagIdentifier {
182    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
183        Some(self.cmp(other))
184    }
185}
186
187impl std::hash::Hash for TagIdentifier {
188    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
189        self.value.hash(state);
190    }
191}
192
193/// Engine information for a tag.
194#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
195#[ts(export)]
196#[serde(tag = "type", rename_all = "camelCase")]
197pub struct TagEngineInfo {
198    /// The id of the tagged object.
199    pub id: uuid::Uuid,
200    /// The sketch the tag is on.
201    pub sketch: uuid::Uuid,
202    /// The path the tag is on.
203    pub path: Option<Path>,
204    /// The surface information for the tag.
205    pub surface: Option<ExtrudeSurface>,
206}
207
208#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
209pub enum BodyType {
210    Root,
211    Block,
212}
213
214/// Metadata.
215#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
216#[ts(export)]
217#[serde(rename_all = "camelCase")]
218pub struct Metadata {
219    /// The source range.
220    pub source_range: SourceRange,
221}
222
223impl From<Metadata> for Vec<SourceRange> {
224    fn from(meta: Metadata) -> Self {
225        vec![meta.source_range]
226    }
227}
228
229impl From<SourceRange> for Metadata {
230    fn from(source_range: SourceRange) -> Self {
231        Self { source_range }
232    }
233}
234
235impl<T> From<NodeRef<'_, T>> for Metadata {
236    fn from(node: NodeRef<'_, T>) -> Self {
237        Self {
238            source_range: SourceRange::new(node.start, node.end, node.module_id),
239        }
240    }
241}
242
243impl From<&Expr> for Metadata {
244    fn from(expr: &Expr) -> Self {
245        Self {
246            source_range: SourceRange::from(expr),
247        }
248    }
249}
250
251/// The type of ExecutorContext being used
252#[derive(PartialEq, Debug, Default, Clone)]
253pub enum ContextType {
254    /// Live engine connection
255    #[default]
256    Live,
257
258    /// Completely mocked connection
259    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
260    /// actually make them.
261    Mock,
262
263    /// Handled by some other interpreter/conversion system
264    MockCustomForwarded,
265}
266
267/// The executor context.
268/// Cloning will return another handle to the same engine connection/session,
269/// as this uses `Arc` under the hood.
270#[derive(Debug, Clone)]
271pub struct ExecutorContext {
272    pub engine: Arc<Box<dyn EngineManager>>,
273    pub fs: Arc<FileManager>,
274    pub settings: ExecutorSettings,
275    pub context_type: ContextType,
276}
277
278/// The executor settings.
279#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
280#[ts(export)]
281pub struct ExecutorSettings {
282    /// Highlight edges of 3D objects?
283    pub highlight_edges: bool,
284    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
285    pub enable_ssao: bool,
286    /// Show grid?
287    pub show_grid: bool,
288    /// Should engine store this for replay?
289    /// If so, under what name?
290    pub replay: Option<String>,
291    /// The directory of the current project.  This is used for resolving import
292    /// paths.  If None is given, the current working directory is used.
293    pub project_directory: Option<TypedPath>,
294    /// This is the path to the current file being executed.
295    /// We use this for preventing cyclic imports.
296    pub current_file: Option<TypedPath>,
297}
298
299impl Default for ExecutorSettings {
300    fn default() -> Self {
301        Self {
302            highlight_edges: true,
303            enable_ssao: false,
304            show_grid: false,
305            replay: None,
306            project_directory: None,
307            current_file: None,
308        }
309    }
310}
311
312impl From<crate::settings::types::Configuration> for ExecutorSettings {
313    fn from(config: crate::settings::types::Configuration) -> Self {
314        Self {
315            highlight_edges: config.settings.modeling.highlight_edges.into(),
316            enable_ssao: config.settings.modeling.enable_ssao.into(),
317            show_grid: config.settings.modeling.show_scale_grid,
318            replay: None,
319            project_directory: None,
320            current_file: None,
321        }
322    }
323}
324
325impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
326    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
327        Self {
328            highlight_edges: config.settings.modeling.highlight_edges.into(),
329            enable_ssao: config.settings.modeling.enable_ssao.into(),
330            show_grid: Default::default(),
331            replay: None,
332            project_directory: None,
333            current_file: None,
334        }
335    }
336}
337
338impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
339    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
340        Self {
341            highlight_edges: modeling.highlight_edges.into(),
342            enable_ssao: modeling.enable_ssao.into(),
343            show_grid: modeling.show_scale_grid,
344            replay: None,
345            project_directory: None,
346            current_file: None,
347        }
348    }
349}
350
351impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
352    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
353        Self {
354            highlight_edges: modeling.highlight_edges.into(),
355            enable_ssao: modeling.enable_ssao.into(),
356            show_grid: Default::default(),
357            replay: None,
358            project_directory: None,
359            current_file: None,
360        }
361    }
362}
363
364impl ExecutorSettings {
365    /// Add the current file path to the executor settings.
366    pub fn with_current_file(&mut self, current_file: TypedPath) {
367        // We want the parent directory of the file.
368        if current_file.extension() == Some("kcl") {
369            self.current_file = Some(current_file.clone());
370            // Get the parent directory.
371            if let Some(parent) = current_file.parent() {
372                self.project_directory = Some(parent);
373            } else {
374                self.project_directory = Some(TypedPath::from(""));
375            }
376        } else {
377            self.project_directory = Some(current_file.clone());
378        }
379    }
380}
381
382impl ExecutorContext {
383    /// Create a new default executor context.
384    #[cfg(not(target_arch = "wasm32"))]
385    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
386        let (ws, _headers) = client
387            .modeling()
388            .commands_ws(
389                None,
390                None,
391                None,
392                if settings.enable_ssao {
393                    Some(kittycad::types::PostEffectType::Ssao)
394                } else {
395                    None
396                },
397                settings.replay.clone(),
398                if settings.show_grid { Some(true) } else { None },
399                None,
400                None,
401                None,
402                Some(false),
403            )
404            .await?;
405
406        let engine: Arc<Box<dyn EngineManager>> =
407            Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
408
409        Ok(Self {
410            engine,
411            fs: Arc::new(FileManager::new()),
412            settings,
413            context_type: ContextType::Live,
414        })
415    }
416
417    #[cfg(target_arch = "wasm32")]
418    pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
419        ExecutorContext {
420            engine,
421            fs,
422            settings,
423            context_type: ContextType::Live,
424        }
425    }
426
427    #[cfg(not(target_arch = "wasm32"))]
428    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
429        ExecutorContext {
430            engine: Arc::new(Box::new(
431                crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
432            )),
433            fs: Arc::new(FileManager::new()),
434            settings: settings.unwrap_or_default(),
435            context_type: ContextType::Mock,
436        }
437    }
438
439    #[cfg(target_arch = "wasm32")]
440    pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
441        ExecutorContext {
442            engine,
443            fs,
444            settings,
445            context_type: ContextType::Mock,
446        }
447    }
448
449    #[cfg(not(target_arch = "wasm32"))]
450    pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
451        ExecutorContext {
452            engine,
453            fs: Arc::new(FileManager::new()),
454            settings: Default::default(),
455            context_type: ContextType::MockCustomForwarded,
456        }
457    }
458
459    /// Create a new default executor context.
460    /// With a kittycad client.
461    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
462    /// variables.
463    /// But also allows for passing in a token and engine address directly.
464    #[cfg(not(target_arch = "wasm32"))]
465    pub async fn new_with_client(
466        settings: ExecutorSettings,
467        token: Option<String>,
468        engine_addr: Option<String>,
469    ) -> Result<Self> {
470        // Create the client.
471        let client = crate::engine::new_zoo_client(token, engine_addr)?;
472
473        let ctx = Self::new(&client, settings).await?;
474        Ok(ctx)
475    }
476
477    /// Create a new default executor context.
478    /// With the default kittycad client.
479    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
480    /// variables.
481    #[cfg(not(target_arch = "wasm32"))]
482    pub async fn new_with_default_client() -> Result<Self> {
483        // Create the client.
484        let ctx = Self::new_with_client(Default::default(), None, None).await?;
485        Ok(ctx)
486    }
487
488    /// For executing unit tests.
489    #[cfg(not(target_arch = "wasm32"))]
490    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
491        let ctx = ExecutorContext::new_with_client(
492            ExecutorSettings {
493                highlight_edges: true,
494                enable_ssao: false,
495                show_grid: false,
496                replay: None,
497                project_directory: None,
498                current_file: None,
499            },
500            None,
501            engine_addr,
502        )
503        .await?;
504        Ok(ctx)
505    }
506
507    pub fn is_mock(&self) -> bool {
508        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
509    }
510
511    /// Returns true if we should not send engine commands for any reason.
512    pub async fn no_engine_commands(&self) -> bool {
513        self.is_mock()
514    }
515
516    pub async fn send_clear_scene(
517        &self,
518        exec_state: &mut ExecState,
519        source_range: crate::execution::SourceRange,
520    ) -> Result<(), KclError> {
521        self.engine
522            .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
523            .await
524    }
525
526    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
527        cache::bust_cache().await;
528
529        // Execute an empty program to clear and reset the scene.
530        // We specifically want to be returned the objects after the scene is reset.
531        // Like the default planes so it is easier to just execute an empty program
532        // after the cache is busted.
533        let outcome = self.run_with_caching(crate::Program::empty()).await?;
534
535        Ok(outcome)
536    }
537
538    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
539        self.eval_prelude(exec_state, SourceRange::synthetic())
540            .await
541            .map_err(KclErrorWithOutputs::no_outputs)?;
542        exec_state.mut_stack().push_new_root_env(true);
543        Ok(())
544    }
545
546    pub async fn run_mock(
547        &self,
548        program: crate::Program,
549        use_prev_memory: bool,
550    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
551        assert!(self.is_mock());
552
553        let mut exec_state = ExecState::new(self);
554        if use_prev_memory {
555            match cache::read_old_memory().await {
556                Some(mem) => {
557                    *exec_state.mut_stack() = mem.0;
558                    exec_state.global.module_infos = mem.1;
559                }
560                None => self.prepare_mem(&mut exec_state).await?,
561            }
562        } else {
563            self.prepare_mem(&mut exec_state).await?
564        };
565
566        // Push a scope so that old variables can be overwritten (since we might be re-executing some
567        // part of the scene).
568        exec_state.mut_stack().push_new_env_for_scope();
569
570        let result = self.inner_run(&program, &mut exec_state, true).await?;
571
572        // Restore any temporary variables, then save any newly created variables back to
573        // memory in case another run wants to use them. Note this is just saved to the preserved
574        // memory, not to the exec_state which is not cached for mock execution.
575
576        let mut mem = exec_state.stack().clone();
577        let module_infos = exec_state.global.module_infos.clone();
578        let outcome = exec_state.to_mock_exec_outcome(result.0, self).await;
579
580        mem.squash_env(result.0);
581        cache::write_old_memory((mem, module_infos)).await;
582
583        Ok(outcome)
584    }
585
586    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
587        assert!(!self.is_mock());
588
589        let (program, exec_state, result) = match cache::read_old_ast().await {
590            Some(mut cached_state) => {
591                let old = CacheInformation {
592                    ast: &cached_state.main.ast,
593                    settings: &cached_state.settings,
594                };
595                let new = CacheInformation {
596                    ast: &program.ast,
597                    settings: &self.settings,
598                };
599
600                // Get the program that actually changed from the old and new information.
601                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
602                    CacheResult::ReExecute {
603                        clear_scene,
604                        reapply_settings,
605                        program: changed_program,
606                    } => {
607                        if reapply_settings
608                            && self
609                                .engine
610                                .reapply_settings(
611                                    &self.settings,
612                                    Default::default(),
613                                    &mut cached_state.main.exec_state.id_generator,
614                                )
615                                .await
616                                .is_err()
617                        {
618                            (true, program, None)
619                        } else {
620                            (
621                                clear_scene,
622                                crate::Program {
623                                    ast: changed_program,
624                                    original_file_contents: program.original_file_contents,
625                                },
626                                None,
627                            )
628                        }
629                    }
630                    CacheResult::CheckImportsOnly {
631                        reapply_settings,
632                        ast: changed_program,
633                    } => {
634                        if reapply_settings
635                            && self
636                                .engine
637                                .reapply_settings(
638                                    &self.settings,
639                                    Default::default(),
640                                    &mut cached_state.main.exec_state.id_generator,
641                                )
642                                .await
643                                .is_err()
644                        {
645                            (true, program, None)
646                        } else {
647                            // We need to check our imports to see if they changed.
648                            let mut new_exec_state = ExecState::new(self);
649                            let (new_universe, new_universe_map) =
650                                self.get_universe(&program, &mut new_exec_state).await?;
651
652                            let clear_scene = new_universe.keys().any(|key| {
653                                let id = new_universe[key].1;
654                                match (
655                                    cached_state.exec_state.get_source(id),
656                                    new_exec_state.global.get_source(id),
657                                ) {
658                                    (Some(s0), Some(s1)) => s0.source != s1.source,
659                                    _ => false,
660                                }
661                            });
662
663                            if !clear_scene {
664                                // Return early we don't need to clear the scene.
665                                return Ok(cached_state.into_exec_outcome(self).await);
666                            }
667
668                            (
669                                true,
670                                crate::Program {
671                                    ast: changed_program,
672                                    original_file_contents: program.original_file_contents,
673                                },
674                                Some((new_universe, new_universe_map, new_exec_state)),
675                            )
676                        }
677                    }
678                    CacheResult::NoAction(true) => {
679                        if self
680                            .engine
681                            .reapply_settings(
682                                &self.settings,
683                                Default::default(),
684                                &mut cached_state.main.exec_state.id_generator,
685                            )
686                            .await
687                            .is_ok()
688                        {
689                            // We need to update the old ast state with the new settings!!
690                            cache::write_old_ast(GlobalState::with_settings(
691                                cached_state.clone(),
692                                self.settings.clone(),
693                            ))
694                            .await;
695
696                            return Ok(cached_state.into_exec_outcome(self).await);
697                        }
698                        (true, program, None)
699                    }
700                    CacheResult::NoAction(false) => {
701                        return Ok(cached_state.into_exec_outcome(self).await);
702                    }
703                };
704
705                let (exec_state, result) = match import_check_info {
706                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
707                        // Clear the scene if the imports changed.
708                        self.send_clear_scene(&mut new_exec_state, Default::default())
709                            .await
710                            .map_err(KclErrorWithOutputs::no_outputs)?;
711
712                        let result = self
713                            .run_concurrent(
714                                &program,
715                                &mut new_exec_state,
716                                Some((new_universe, new_universe_map)),
717                                false,
718                            )
719                            .await;
720
721                        (new_exec_state, result)
722                    }
723                    None if clear_scene => {
724                        // Pop the execution state, since we are starting fresh.
725                        let mut exec_state = cached_state.reconstitute_exec_state();
726                        exec_state.reset(self);
727
728                        self.send_clear_scene(&mut exec_state, Default::default())
729                            .await
730                            .map_err(KclErrorWithOutputs::no_outputs)?;
731
732                        let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
733
734                        (exec_state, result)
735                    }
736                    None => {
737                        let mut exec_state = cached_state.reconstitute_exec_state();
738                        exec_state.mut_stack().restore_env(cached_state.main.result_env);
739
740                        let result = self.run_concurrent(&program, &mut exec_state, None, true).await;
741
742                        (exec_state, result)
743                    }
744                };
745
746                (program, exec_state, result)
747            }
748            None => {
749                let mut exec_state = ExecState::new(self);
750                self.send_clear_scene(&mut exec_state, Default::default())
751                    .await
752                    .map_err(KclErrorWithOutputs::no_outputs)?;
753
754                let result = self.run_concurrent(&program, &mut exec_state, None, false).await;
755
756                (program, exec_state, result)
757            }
758        };
759
760        if result.is_err() {
761            cache::bust_cache().await;
762        }
763
764        // Throw the error.
765        let result = result?;
766
767        // Save this as the last successful execution to the cache.
768        cache::write_old_ast(GlobalState::new(
769            exec_state.clone(),
770            self.settings.clone(),
771            program.ast,
772            result.0,
773        ))
774        .await;
775
776        let outcome = exec_state.to_exec_outcome(result.0, self).await;
777        Ok(outcome)
778    }
779
780    /// Perform the execution of a program.
781    ///
782    /// You can optionally pass in some initialization memory for partial
783    /// execution.
784    ///
785    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
786    pub async fn run(
787        &self,
788        program: &crate::Program,
789        exec_state: &mut ExecState,
790    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
791        self.run_concurrent(program, exec_state, None, false).await
792    }
793
794    /// Perform the execution of a program using a concurrent
795    /// execution model.
796    ///
797    /// You can optionally pass in some initialization memory for partial
798    /// execution.
799    ///
800    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
801    pub async fn run_concurrent(
802        &self,
803        program: &crate::Program,
804        exec_state: &mut ExecState,
805        universe_info: Option<(Universe, UniverseMap)>,
806        preserve_mem: bool,
807    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
808        // Reuse our cached universe if we have one.
809
810        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
811            (universe, universe_map)
812        } else {
813            self.get_universe(program, exec_state).await?
814        };
815
816        let default_planes = self.engine.get_default_planes().read().await.clone();
817
818        // Run the prelude to set up the engine.
819        self.eval_prelude(exec_state, SourceRange::synthetic())
820            .await
821            .map_err(KclErrorWithOutputs::no_outputs)?;
822
823        for modules in import_graph::import_graph(&universe, self)
824            .map_err(|err| exec_state.error_with_outputs(err, default_planes.clone()))?
825            .into_iter()
826        {
827            #[cfg(not(target_arch = "wasm32"))]
828            let mut set = tokio::task::JoinSet::new();
829
830            #[allow(clippy::type_complexity)]
831            let (results_tx, mut results_rx): (
832                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
833                tokio::sync::mpsc::Receiver<_>,
834            ) = tokio::sync::mpsc::channel(1);
835
836            for module in modules {
837                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
838                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
839                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
840                    )));
841                };
842                let module_id = *module_id;
843                let module_path = module_path.clone();
844                let source_range = SourceRange::from(import_stmt);
845
846                self.add_import_module_ops(
847                    exec_state,
848                    program,
849                    module_id,
850                    &module_path,
851                    source_range,
852                    &universe_map,
853                );
854
855                let repr = repr.clone();
856                let exec_state = exec_state.clone();
857                let exec_ctxt = self.clone();
858                let results_tx = results_tx.clone();
859
860                let exec_module = async |exec_ctxt: &ExecutorContext,
861                                         repr: &ModuleRepr,
862                                         module_id: ModuleId,
863                                         module_path: &ModulePath,
864                                         exec_state: &mut ExecState,
865                                         source_range: SourceRange|
866                       -> Result<ModuleRepr, KclError> {
867                    match repr {
868                        ModuleRepr::Kcl(program, _) => {
869                            let result = exec_ctxt
870                                .exec_module_from_ast(program, module_id, module_path, exec_state, source_range, false)
871                                .await;
872
873                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
874                        }
875                        ModuleRepr::Foreign(geom, _) => {
876                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_ctxt)
877                                .await
878                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
879
880                            result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
881                        }
882                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
883                            format!("Module {module_path} not found in universe"),
884                            vec![source_range],
885                        ))),
886                    }
887                };
888
889                #[cfg(target_arch = "wasm32")]
890                {
891                    wasm_bindgen_futures::spawn_local(async move {
892                        let mut exec_state = exec_state;
893                        let exec_ctxt = exec_ctxt;
894
895                        let result = exec_module(
896                            &exec_ctxt,
897                            &repr,
898                            module_id,
899                            &module_path,
900                            &mut exec_state,
901                            source_range,
902                        )
903                        .await;
904
905                        results_tx
906                            .send((module_id, module_path, result))
907                            .await
908                            .unwrap_or_default();
909                    });
910                }
911                #[cfg(not(target_arch = "wasm32"))]
912                {
913                    set.spawn(async move {
914                        let mut exec_state = exec_state;
915                        let exec_ctxt = exec_ctxt;
916
917                        let result = exec_module(
918                            &exec_ctxt,
919                            &repr,
920                            module_id,
921                            &module_path,
922                            &mut exec_state,
923                            source_range,
924                        )
925                        .await;
926
927                        results_tx
928                            .send((module_id, module_path, result))
929                            .await
930                            .unwrap_or_default();
931                    });
932                }
933            }
934
935            drop(results_tx);
936
937            while let Some((module_id, _, result)) = results_rx.recv().await {
938                match result {
939                    Ok(new_repr) => {
940                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
941
942                        match &mut repr {
943                            ModuleRepr::Kcl(_, cache) => {
944                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
945                                    unreachable!();
946                                };
947                                *cache = session_data;
948                            }
949                            ModuleRepr::Foreign(_, cache) => {
950                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
951                                    unreachable!();
952                                };
953                                *cache = session_data;
954                            }
955                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
956                        }
957
958                        exec_state.global.module_infos[&module_id].restore_repr(repr);
959                    }
960                    Err(e) => {
961                        return Err(exec_state.error_with_outputs(e, default_planes));
962                    }
963                }
964            }
965        }
966
967        self.inner_run(program, exec_state, preserve_mem).await
968    }
969
970    /// Get the universe & universe map of the program.
971    /// And see if any of the imports changed.
972    async fn get_universe(
973        &self,
974        program: &crate::Program,
975        exec_state: &mut ExecState,
976    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
977        exec_state.add_root_module_contents(program);
978
979        let mut universe = std::collections::HashMap::new();
980
981        let default_planes = self.engine.get_default_planes().read().await.clone();
982
983        let root_imports = import_graph::import_universe(
984            self,
985            &ModulePath::Main,
986            &ModuleRepr::Kcl(program.ast.clone(), None),
987            &mut universe,
988            exec_state,
989        )
990        .await
991        .map_err(|err| exec_state.error_with_outputs(err, default_planes))?;
992
993        Ok((universe, root_imports))
994    }
995
996    #[cfg(feature = "artifact-graph")]
997    fn add_import_module_ops(
998        &self,
999        exec_state: &mut ExecState,
1000        program: &crate::Program,
1001        module_id: ModuleId,
1002        module_path: &ModulePath,
1003        source_range: SourceRange,
1004        universe_map: &UniverseMap,
1005    ) {
1006        match module_path {
1007            ModulePath::Main => {
1008                // This should never happen.
1009            }
1010            ModulePath::Local { value, .. } => {
1011                // We only want to display the top-level module imports in
1012                // the Feature Tree, not transitive imports.
1013                if universe_map.contains_key(value) {
1014                    use crate::NodePath;
1015
1016                    let node_path = if source_range.is_top_level_module() {
1017                        let cached_body_items = exec_state.global.artifacts.cached_body_items();
1018                        NodePath::from_range(&program.ast, cached_body_items, source_range).unwrap_or_default()
1019                    } else {
1020                        // The frontend doesn't care about paths in
1021                        // files other than the top-level module.
1022                        NodePath::placeholder()
1023                    };
1024
1025                    exec_state.push_op(Operation::GroupBegin {
1026                        group: Group::ModuleInstance {
1027                            name: value.file_name().unwrap_or_default(),
1028                            module_id,
1029                        },
1030                        node_path,
1031                        source_range,
1032                    });
1033                    // Due to concurrent execution, we cannot easily
1034                    // group operations by module. So we leave the
1035                    // group empty and close it immediately.
1036                    exec_state.push_op(Operation::GroupEnd);
1037                }
1038            }
1039            ModulePath::Std { .. } => {
1040                // We don't want to display stdlib in the Feature Tree.
1041            }
1042        }
1043    }
1044
1045    #[cfg(not(feature = "artifact-graph"))]
1046    fn add_import_module_ops(
1047        &self,
1048        _exec_state: &mut ExecState,
1049        _program: &crate::Program,
1050        _module_id: ModuleId,
1051        _module_path: &ModulePath,
1052        _source_range: SourceRange,
1053        _universe_map: &UniverseMap,
1054    ) {
1055    }
1056
1057    /// Perform the execution of a program.  Accept all possible parameters and
1058    /// output everything.
1059    async fn inner_run(
1060        &self,
1061        program: &crate::Program,
1062        exec_state: &mut ExecState,
1063        preserve_mem: bool,
1064    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1065        let _stats = crate::log::LogPerfStats::new("Interpretation");
1066
1067        // Re-apply the settings, in case the cache was busted.
1068        self.engine
1069            .reapply_settings(&self.settings, Default::default(), exec_state.id_generator())
1070            .await
1071            .map_err(KclErrorWithOutputs::no_outputs)?;
1072
1073        let default_planes = self.engine.get_default_planes().read().await.clone();
1074        let result = self
1075            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1076            .await;
1077
1078        crate::log::log(format!(
1079            "Post interpretation KCL memory stats: {:#?}",
1080            exec_state.stack().memory.stats
1081        ));
1082        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1083
1084        let env_ref = result.map_err(|e| exec_state.error_with_outputs(e, default_planes))?;
1085
1086        if !self.is_mock() {
1087            let mut mem = exec_state.stack().deep_clone();
1088            mem.restore_env(env_ref);
1089            cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1090        }
1091        let session_data = self.engine.get_session_data().await;
1092
1093        Ok((env_ref, session_data))
1094    }
1095
1096    /// Execute an AST's program and build auxiliary outputs like the artifact
1097    /// graph.
1098    async fn execute_and_build_graph(
1099        &self,
1100        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1101        exec_state: &mut ExecState,
1102        preserve_mem: bool,
1103    ) -> Result<EnvironmentRef, KclError> {
1104        // Don't early return!  We need to build other outputs regardless of
1105        // whether execution failed.
1106
1107        // Because of execution caching, we may start with operations from a
1108        // previous run.
1109        #[cfg(feature = "artifact-graph")]
1110        let start_op = exec_state.global.artifacts.operations.len();
1111
1112        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1113            .await?;
1114
1115        let exec_result = self
1116            .exec_module_body(
1117                program,
1118                exec_state,
1119                preserve_mem,
1120                ModuleId::default(),
1121                &ModulePath::Main,
1122            )
1123            .await;
1124
1125        #[cfg(feature = "artifact-graph")]
1126        {
1127            // Fill in NodePath for operations.
1128            let cached_body_items = exec_state.global.artifacts.cached_body_items();
1129            for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
1130                match op {
1131                    Operation::StdLibCall {
1132                        node_path,
1133                        source_range,
1134                        ..
1135                    }
1136                    | Operation::GroupBegin {
1137                        node_path,
1138                        source_range,
1139                        ..
1140                    } => {
1141                        node_path.fill_placeholder(program, cached_body_items, *source_range);
1142                    }
1143                    Operation::GroupEnd => {}
1144                }
1145            }
1146        }
1147
1148        // Ensure all the async commands completed.
1149        self.engine.ensure_async_commands_completed().await?;
1150
1151        // If we errored out and early-returned, there might be commands which haven't been executed
1152        // and should be dropped.
1153        self.engine.clear_queues().await;
1154
1155        match exec_state.build_artifact_graph(&self.engine, program).await {
1156            Ok(_) => exec_result.map(|(_, env_ref, _)| env_ref),
1157            Err(err) => exec_result.and(Err(err)),
1158        }
1159    }
1160
1161    /// 'Import' std::prelude as the outermost scope.
1162    ///
1163    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1164    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1165        if exec_state.stack().memory.requires_std() {
1166            let path = vec!["std".to_owned(), "prelude".to_owned()];
1167            let resolved_path = ModulePath::from_std_import_path(&path)?;
1168            let id = self
1169                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1170                .await?;
1171            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1172
1173            exec_state.mut_stack().memory.set_std(module_memory);
1174        }
1175
1176        Ok(())
1177    }
1178
1179    /// Get a snapshot of the current scene.
1180    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1181        // Zoom to fit.
1182        self.engine
1183            .send_modeling_cmd(
1184                uuid::Uuid::new_v4(),
1185                crate::execution::SourceRange::default(),
1186                &ModelingCmd::from(mcmd::ZoomToFit {
1187                    object_ids: Default::default(),
1188                    animated: false,
1189                    padding: 0.1,
1190                }),
1191            )
1192            .await
1193            .map_err(KclErrorWithOutputs::no_outputs)?;
1194
1195        // Send a snapshot request to the engine.
1196        let resp = self
1197            .engine
1198            .send_modeling_cmd(
1199                uuid::Uuid::new_v4(),
1200                crate::execution::SourceRange::default(),
1201                &ModelingCmd::from(mcmd::TakeSnapshot {
1202                    format: ImageFormat::Png,
1203                }),
1204            )
1205            .await
1206            .map_err(KclErrorWithOutputs::no_outputs)?;
1207
1208        let OkWebSocketResponseData::Modeling {
1209            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1210        } = resp
1211        else {
1212            return Err(ExecError::BadPng(format!(
1213                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1214            )));
1215        };
1216        Ok(contents)
1217    }
1218
1219    /// Export the current scene as a CAD file.
1220    pub async fn export(
1221        &self,
1222        format: kittycad_modeling_cmds::format::OutputFormat3d,
1223    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1224        let resp = self
1225            .engine
1226            .send_modeling_cmd(
1227                uuid::Uuid::new_v4(),
1228                crate::SourceRange::default(),
1229                &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1230                    entity_ids: vec![],
1231                    format,
1232                }),
1233            )
1234            .await?;
1235
1236        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1237            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1238                format!("Expected Export response, got {resp:?}",),
1239                vec![SourceRange::default()],
1240            )));
1241        };
1242
1243        Ok(files)
1244    }
1245
1246    /// Export the current scene as a STEP file.
1247    pub async fn export_step(
1248        &self,
1249        deterministic_time: bool,
1250    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1251        let files = self
1252            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1253                kittycad_modeling_cmds::format::step::export::Options {
1254                    coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1255                    created: if deterministic_time {
1256                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1257                            KclError::new_internal(crate::errors::KclErrorDetails::new(
1258                                format!("Failed to parse date: {}", e),
1259                                vec![SourceRange::default()],
1260                            ))
1261                        })?)
1262                    } else {
1263                        None
1264                    },
1265                },
1266            ))
1267            .await?;
1268
1269        Ok(files)
1270    }
1271
1272    pub async fn close(&self) {
1273        self.engine.close().await;
1274    }
1275}
1276
1277#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS, JsonSchema)]
1278pub struct ArtifactId(Uuid);
1279
1280impl ArtifactId {
1281    pub fn new(uuid: Uuid) -> Self {
1282        Self(uuid)
1283    }
1284}
1285
1286impl From<Uuid> for ArtifactId {
1287    fn from(uuid: Uuid) -> Self {
1288        Self::new(uuid)
1289    }
1290}
1291
1292impl From<&Uuid> for ArtifactId {
1293    fn from(uuid: &Uuid) -> Self {
1294        Self::new(*uuid)
1295    }
1296}
1297
1298impl From<ArtifactId> for Uuid {
1299    fn from(id: ArtifactId) -> Self {
1300        id.0
1301    }
1302}
1303
1304impl From<&ArtifactId> for Uuid {
1305    fn from(id: &ArtifactId) -> Self {
1306        id.0
1307    }
1308}
1309
1310impl From<ModelingCmdId> for ArtifactId {
1311    fn from(id: ModelingCmdId) -> Self {
1312        Self::new(*id.as_ref())
1313    }
1314}
1315
1316impl From<&ModelingCmdId> for ArtifactId {
1317    fn from(id: &ModelingCmdId) -> Self {
1318        Self::new(*id.as_ref())
1319    }
1320}
1321
1322#[cfg(test)]
1323pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1324    parse_execute_with_project_dir(code, None).await
1325}
1326
1327#[cfg(test)]
1328pub(crate) async fn parse_execute_with_project_dir(
1329    code: &str,
1330    project_directory: Option<TypedPath>,
1331) -> Result<ExecTestResults, KclError> {
1332    let program = crate::Program::parse_no_errs(code)?;
1333
1334    let exec_ctxt = ExecutorContext {
1335        engine: Arc::new(Box::new(
1336            crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
1337                KclError::new_internal(crate::errors::KclErrorDetails::new(
1338                    format!("Failed to create mock engine connection: {}", err),
1339                    vec![SourceRange::default()],
1340                ))
1341            })?,
1342        )),
1343        fs: Arc::new(crate::fs::FileManager::new()),
1344        settings: ExecutorSettings {
1345            project_directory,
1346            ..Default::default()
1347        },
1348        context_type: ContextType::Mock,
1349    };
1350    let mut exec_state = ExecState::new(&exec_ctxt);
1351    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1352
1353    Ok(ExecTestResults {
1354        program,
1355        mem_env: result.0,
1356        exec_ctxt,
1357        exec_state,
1358    })
1359}
1360
1361#[cfg(test)]
1362#[derive(Debug)]
1363pub(crate) struct ExecTestResults {
1364    program: crate::Program,
1365    mem_env: EnvironmentRef,
1366    exec_ctxt: ExecutorContext,
1367    exec_state: ExecState,
1368}
1369
1370#[cfg(test)]
1371mod tests {
1372    use pretty_assertions::assert_eq;
1373
1374    use super::*;
1375    use crate::{errors::KclErrorDetails, execution::memory::Stack, ModuleId};
1376
1377    /// Convenience function to get a JSON value from memory and unwrap.
1378    #[track_caller]
1379    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1380        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1381    }
1382
1383    #[tokio::test(flavor = "multi_thread")]
1384    async fn test_execute_warn() {
1385        let text = "@blah";
1386        let result = parse_execute(text).await.unwrap();
1387        let errs = result.exec_state.errors();
1388        assert_eq!(errs.len(), 1);
1389        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1390        assert!(
1391            errs[0].message.contains("Unknown annotation"),
1392            "unexpected warning message: {}",
1393            errs[0].message
1394        );
1395    }
1396
1397    #[tokio::test(flavor = "multi_thread")]
1398    async fn test_execute_fn_definitions() {
1399        let ast = r#"fn def(@x) {
1400  return x
1401}
1402fn ghi(@x) {
1403  return x
1404}
1405fn jkl(@x) {
1406  return x
1407}
1408fn hmm(@x) {
1409  return x
1410}
1411
1412yo = 5 + 6
1413
1414abc = 3
1415identifierGuy = 5
1416part001 = startSketchOn(XY)
1417|> startProfile(at = [-1.2, 4.83])
1418|> line(end = [2.8, 0])
1419|> angledLine(angle = 100 + 100, length = 3.01)
1420|> angledLine(angle = abc, length = 3.02)
1421|> angledLine(angle = def(yo), length = 3.03)
1422|> angledLine(angle = ghi(2), length = 3.04)
1423|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1424|> close()
1425yo2 = hmm([identifierGuy + 5])"#;
1426
1427        parse_execute(ast).await.unwrap();
1428    }
1429
1430    #[tokio::test(flavor = "multi_thread")]
1431    async fn test_execute_with_pipe_substitutions_unary() {
1432        let ast = r#"myVar = 3
1433part001 = startSketchOn(XY)
1434  |> startProfile(at = [0, 0])
1435  |> line(end = [3, 4], tag = $seg01)
1436  |> line(end = [
1437  min([segLen(seg01), myVar]),
1438  -legLen(hypotenuse = segLen(seg01), leg = myVar)
1439])
1440"#;
1441
1442        parse_execute(ast).await.unwrap();
1443    }
1444
1445    #[tokio::test(flavor = "multi_thread")]
1446    async fn test_execute_with_pipe_substitutions() {
1447        let ast = r#"myVar = 3
1448part001 = startSketchOn(XY)
1449  |> startProfile(at = [0, 0])
1450  |> line(end = [3, 4], tag = $seg01)
1451  |> line(end = [
1452  min([segLen(seg01), myVar]),
1453  legLen(hypotenuse = segLen(seg01), leg = myVar)
1454])
1455"#;
1456
1457        parse_execute(ast).await.unwrap();
1458    }
1459
1460    #[tokio::test(flavor = "multi_thread")]
1461    async fn test_execute_with_inline_comment() {
1462        let ast = r#"baseThick = 1
1463armAngle = 60
1464
1465baseThickHalf = baseThick / 2
1466halfArmAngle = armAngle / 2
1467
1468arrExpShouldNotBeIncluded = [1, 2, 3]
1469objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1470
1471part001 = startSketchOn(XY)
1472  |> startProfile(at = [0, 0])
1473  |> yLine(endAbsolute = 1)
1474  |> xLine(length = 3.84) // selection-range-7ish-before-this
1475
1476variableBelowShouldNotBeIncluded = 3
1477"#;
1478
1479        parse_execute(ast).await.unwrap();
1480    }
1481
1482    #[tokio::test(flavor = "multi_thread")]
1483    async fn test_execute_with_function_literal_in_pipe() {
1484        let ast = r#"w = 20
1485l = 8
1486h = 10
1487
1488fn thing() {
1489  return -8
1490}
1491
1492firstExtrude = startSketchOn(XY)
1493  |> startProfile(at = [0,0])
1494  |> line(end = [0, l])
1495  |> line(end = [w, 0])
1496  |> line(end = [0, thing()])
1497  |> close()
1498  |> extrude(length = h)"#;
1499
1500        parse_execute(ast).await.unwrap();
1501    }
1502
1503    #[tokio::test(flavor = "multi_thread")]
1504    async fn test_execute_with_function_unary_in_pipe() {
1505        let ast = r#"w = 20
1506l = 8
1507h = 10
1508
1509fn thing(@x) {
1510  return -x
1511}
1512
1513firstExtrude = startSketchOn(XY)
1514  |> startProfile(at = [0,0])
1515  |> line(end = [0, l])
1516  |> line(end = [w, 0])
1517  |> line(end = [0, thing(8)])
1518  |> close()
1519  |> extrude(length = h)"#;
1520
1521        parse_execute(ast).await.unwrap();
1522    }
1523
1524    #[tokio::test(flavor = "multi_thread")]
1525    async fn test_execute_with_function_array_in_pipe() {
1526        let ast = r#"w = 20
1527l = 8
1528h = 10
1529
1530fn thing(@x) {
1531  return [0, -x]
1532}
1533
1534firstExtrude = startSketchOn(XY)
1535  |> startProfile(at = [0,0])
1536  |> line(end = [0, l])
1537  |> line(end = [w, 0])
1538  |> line(end = thing(8))
1539  |> close()
1540  |> extrude(length = h)"#;
1541
1542        parse_execute(ast).await.unwrap();
1543    }
1544
1545    #[tokio::test(flavor = "multi_thread")]
1546    async fn test_execute_with_function_call_in_pipe() {
1547        let ast = r#"w = 20
1548l = 8
1549h = 10
1550
1551fn other_thing(@y) {
1552  return -y
1553}
1554
1555fn thing(@x) {
1556  return other_thing(x)
1557}
1558
1559firstExtrude = startSketchOn(XY)
1560  |> startProfile(at = [0,0])
1561  |> line(end = [0, l])
1562  |> line(end = [w, 0])
1563  |> line(end = [0, thing(8)])
1564  |> close()
1565  |> extrude(length = h)"#;
1566
1567        parse_execute(ast).await.unwrap();
1568    }
1569
1570    #[tokio::test(flavor = "multi_thread")]
1571    async fn test_execute_with_function_sketch() {
1572        let ast = r#"fn box(h, l, w) {
1573 myBox = startSketchOn(XY)
1574    |> startProfile(at = [0,0])
1575    |> line(end = [0, l])
1576    |> line(end = [w, 0])
1577    |> line(end = [0, -l])
1578    |> close()
1579    |> extrude(length = h)
1580
1581  return myBox
1582}
1583
1584fnBox = box(h = 3, l = 6, w = 10)"#;
1585
1586        parse_execute(ast).await.unwrap();
1587    }
1588
1589    #[tokio::test(flavor = "multi_thread")]
1590    async fn test_get_member_of_object_with_function_period() {
1591        let ast = r#"fn box(@obj) {
1592 myBox = startSketchOn(XY)
1593    |> startProfile(at = obj.start)
1594    |> line(end = [0, obj.l])
1595    |> line(end = [obj.w, 0])
1596    |> line(end = [0, -obj.l])
1597    |> close()
1598    |> extrude(length = obj.h)
1599
1600  return myBox
1601}
1602
1603thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1604"#;
1605        parse_execute(ast).await.unwrap();
1606    }
1607
1608    #[tokio::test(flavor = "multi_thread")]
1609    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
1610    async fn test_object_member_starting_pipeline() {
1611        let ast = r#"
1612fn test2() {
1613  return {
1614    thing: startSketchOn(XY)
1615      |> startProfile(at = [0, 0])
1616      |> line(end = [0, 1])
1617      |> line(end = [1, 0])
1618      |> line(end = [0, -1])
1619      |> close()
1620  }
1621}
1622
1623x2 = test2()
1624
1625x2.thing
1626  |> extrude(length = 10)
1627"#;
1628        parse_execute(ast).await.unwrap();
1629    }
1630
1631    #[tokio::test(flavor = "multi_thread")]
1632    #[ignore] // ignore til we get loops
1633    async fn test_execute_with_function_sketch_loop_objects() {
1634        let ast = r#"fn box(obj) {
1635let myBox = startSketchOn(XY)
1636    |> startProfile(at = obj.start)
1637    |> line(end = [0, obj.l])
1638    |> line(end = [obj.w, 0])
1639    |> line(end = [0, -obj.l])
1640    |> close()
1641    |> extrude(length = obj.h)
1642
1643  return myBox
1644}
1645
1646for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1647  thisBox = box(var)
1648}"#;
1649
1650        parse_execute(ast).await.unwrap();
1651    }
1652
1653    #[tokio::test(flavor = "multi_thread")]
1654    #[ignore] // ignore til we get loops
1655    async fn test_execute_with_function_sketch_loop_array() {
1656        let ast = r#"fn box(h, l, w, start) {
1657 myBox = startSketchOn(XY)
1658    |> startProfile(at = [0,0])
1659    |> line(end = [0, l])
1660    |> line(end = [w, 0])
1661    |> line(end = [0, -l])
1662    |> close()
1663    |> extrude(length = h)
1664
1665  return myBox
1666}
1667
1668
1669for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1670  const thisBox = box(var[0], var[1], var[2], var[3])
1671}"#;
1672
1673        parse_execute(ast).await.unwrap();
1674    }
1675
1676    #[tokio::test(flavor = "multi_thread")]
1677    async fn test_get_member_of_array_with_function() {
1678        let ast = r#"fn box(@arr) {
1679 myBox =startSketchOn(XY)
1680    |> startProfile(at = arr[0])
1681    |> line(end = [0, arr[1]])
1682    |> line(end = [arr[2], 0])
1683    |> line(end = [0, -arr[1]])
1684    |> close()
1685    |> extrude(length = arr[3])
1686
1687  return myBox
1688}
1689
1690thisBox = box([[0,0], 6, 10, 3])
1691
1692"#;
1693        parse_execute(ast).await.unwrap();
1694    }
1695
1696    #[tokio::test(flavor = "multi_thread")]
1697    async fn test_function_cannot_access_future_definitions() {
1698        let ast = r#"
1699fn returnX() {
1700  // x shouldn't be defined yet.
1701  return x
1702}
1703
1704x = 5
1705
1706answer = returnX()"#;
1707
1708        let result = parse_execute(ast).await;
1709        let err = result.unwrap_err();
1710        assert_eq!(err.message(), "`x` is not defined");
1711    }
1712
1713    #[tokio::test(flavor = "multi_thread")]
1714    async fn test_override_prelude() {
1715        let text = "PI = 3.0";
1716        let result = parse_execute(text).await.unwrap();
1717        let errs = result.exec_state.errors();
1718        assert!(errs.is_empty());
1719    }
1720
1721    #[tokio::test(flavor = "multi_thread")]
1722    async fn type_aliases() {
1723        let text = r#"type MyTy = [number; 2]
1724fn foo(@x: MyTy) {
1725    return x[0]
1726}
1727
1728foo([0, 1])
1729
1730type Other = MyTy | Helix
1731"#;
1732        let result = parse_execute(text).await.unwrap();
1733        let errs = result.exec_state.errors();
1734        assert!(errs.is_empty());
1735    }
1736
1737    #[tokio::test(flavor = "multi_thread")]
1738    async fn test_cannot_shebang_in_fn() {
1739        let ast = r#"
1740fn foo() {
1741  #!hello
1742  return true
1743}
1744
1745foo
1746"#;
1747
1748        let result = parse_execute(ast).await;
1749        let err = result.unwrap_err();
1750        assert_eq!(
1751            err,
1752            KclError::new_syntax(KclErrorDetails::new(
1753                "Unexpected token: #".to_owned(),
1754                vec![SourceRange::new(14, 15, ModuleId::default())],
1755            )),
1756        );
1757    }
1758
1759    #[tokio::test(flavor = "multi_thread")]
1760    async fn test_pattern_transform_function_cannot_access_future_definitions() {
1761        let ast = r#"
1762fn transform(@replicaId) {
1763  // x shouldn't be defined yet.
1764  scale = x
1765  return {
1766    translate = [0, 0, replicaId * 10],
1767    scale = [scale, 1, 0],
1768  }
1769}
1770
1771fn layer() {
1772  return startSketchOn(XY)
1773    |> circle( center= [0, 0], radius= 1, tag = $tag1)
1774    |> extrude(length = 10)
1775}
1776
1777x = 5
1778
1779// The 10 layers are replicas of each other, with a transform applied to each.
1780shape = layer() |> patternTransform(instances = 10, transform = transform)
1781"#;
1782
1783        let result = parse_execute(ast).await;
1784        let err = result.unwrap_err();
1785        assert_eq!(err.message(), "`x` is not defined",);
1786    }
1787
1788    // ADAM: Move some of these into simulation tests.
1789
1790    #[tokio::test(flavor = "multi_thread")]
1791    async fn test_math_execute_with_functions() {
1792        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1793        let result = parse_execute(ast).await.unwrap();
1794        assert_eq!(
1795            5.0,
1796            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1797                .as_f64()
1798                .unwrap()
1799        );
1800    }
1801
1802    #[tokio::test(flavor = "multi_thread")]
1803    async fn test_math_execute() {
1804        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1805        let result = parse_execute(ast).await.unwrap();
1806        assert_eq!(
1807            7.4,
1808            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1809                .as_f64()
1810                .unwrap()
1811        );
1812    }
1813
1814    #[tokio::test(flavor = "multi_thread")]
1815    async fn test_math_execute_start_negative() {
1816        let ast = r#"myVar = -5 + 6"#;
1817        let result = parse_execute(ast).await.unwrap();
1818        assert_eq!(
1819            1.0,
1820            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1821                .as_f64()
1822                .unwrap()
1823        );
1824    }
1825
1826    #[tokio::test(flavor = "multi_thread")]
1827    async fn test_math_execute_with_pi() {
1828        let ast = r#"myVar = PI * 2"#;
1829        let result = parse_execute(ast).await.unwrap();
1830        assert_eq!(
1831            std::f64::consts::TAU,
1832            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1833                .as_f64()
1834                .unwrap()
1835        );
1836    }
1837
1838    #[tokio::test(flavor = "multi_thread")]
1839    async fn test_math_define_decimal_without_leading_zero() {
1840        let ast = r#"thing = .4 + 7"#;
1841        let result = parse_execute(ast).await.unwrap();
1842        assert_eq!(
1843            7.4,
1844            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1845                .as_f64()
1846                .unwrap()
1847        );
1848    }
1849
1850    #[tokio::test(flavor = "multi_thread")]
1851    async fn test_zero_param_fn() {
1852        let ast = r#"sigmaAllow = 35000 // psi
1853leg1 = 5 // inches
1854leg2 = 8 // inches
1855fn thickness() { return 0.56 }
1856
1857bracket = startSketchOn(XY)
1858  |> startProfile(at = [0,0])
1859  |> line(end = [0, leg1])
1860  |> line(end = [leg2, 0])
1861  |> line(end = [0, -thickness()])
1862  |> line(end = [-leg2 + thickness(), 0])
1863"#;
1864        parse_execute(ast).await.unwrap();
1865    }
1866
1867    #[tokio::test(flavor = "multi_thread")]
1868    async fn test_unary_operator_not_succeeds() {
1869        let ast = r#"
1870fn returnTrue() { return !false }
1871t = true
1872f = false
1873notTrue = !t
1874notFalse = !f
1875c = !!true
1876d = !returnTrue()
1877
1878assertIs(!false, error = "expected to pass")
1879
1880fn check(x) {
1881  assertIs(!x, error = "expected argument to be false")
1882  return true
1883}
1884check(x = false)
1885"#;
1886        let result = parse_execute(ast).await.unwrap();
1887        assert_eq!(
1888            false,
1889            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
1890                .as_bool()
1891                .unwrap()
1892        );
1893        assert_eq!(
1894            true,
1895            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
1896                .as_bool()
1897                .unwrap()
1898        );
1899        assert_eq!(
1900            true,
1901            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
1902                .as_bool()
1903                .unwrap()
1904        );
1905        assert_eq!(
1906            false,
1907            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
1908                .as_bool()
1909                .unwrap()
1910        );
1911    }
1912
1913    #[tokio::test(flavor = "multi_thread")]
1914    async fn test_unary_operator_not_on_non_bool_fails() {
1915        let code1 = r#"
1916// Yup, this is null.
1917myNull = 0 / 0
1918notNull = !myNull
1919"#;
1920        assert_eq!(
1921            parse_execute(code1).await.unwrap_err().message(),
1922            "Cannot apply unary operator ! to non-boolean value: number(default units)",
1923        );
1924
1925        let code2 = "notZero = !0";
1926        assert_eq!(
1927            parse_execute(code2).await.unwrap_err().message(),
1928            "Cannot apply unary operator ! to non-boolean value: number(default units)",
1929        );
1930
1931        let code3 = r#"
1932notEmptyString = !""
1933"#;
1934        assert_eq!(
1935            parse_execute(code3).await.unwrap_err().message(),
1936            "Cannot apply unary operator ! to non-boolean value: string",
1937        );
1938
1939        let code4 = r#"
1940obj = { a = 1 }
1941notMember = !obj.a
1942"#;
1943        assert_eq!(
1944            parse_execute(code4).await.unwrap_err().message(),
1945            "Cannot apply unary operator ! to non-boolean value: number(default units)",
1946        );
1947
1948        let code5 = "
1949a = []
1950notArray = !a";
1951        assert_eq!(
1952            parse_execute(code5).await.unwrap_err().message(),
1953            "Cannot apply unary operator ! to non-boolean value: [any; 0]",
1954        );
1955
1956        let code6 = "
1957x = {}
1958notObject = !x";
1959        assert_eq!(
1960            parse_execute(code6).await.unwrap_err().message(),
1961            "Cannot apply unary operator ! to non-boolean value: {  }",
1962        );
1963
1964        let code7 = "
1965fn x() { return 1 }
1966notFunction = !x";
1967        let fn_err = parse_execute(code7).await.unwrap_err();
1968        // These are currently printed out as JSON objects, so we don't want to
1969        // check the full error.
1970        assert!(
1971            fn_err
1972                .message()
1973                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
1974            "Actual error: {:?}",
1975            fn_err
1976        );
1977
1978        let code8 = "
1979myTagDeclarator = $myTag
1980notTagDeclarator = !myTagDeclarator";
1981        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
1982        // These are currently printed out as JSON objects, so we don't want to
1983        // check the full error.
1984        assert!(
1985            tag_declarator_err
1986                .message()
1987                .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
1988            "Actual error: {:?}",
1989            tag_declarator_err
1990        );
1991
1992        let code9 = "
1993myTagDeclarator = $myTag
1994notTagIdentifier = !myTag";
1995        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
1996        // These are currently printed out as JSON objects, so we don't want to
1997        // check the full error.
1998        assert!(
1999            tag_identifier_err
2000                .message()
2001                .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
2002            "Actual error: {:?}",
2003            tag_identifier_err
2004        );
2005
2006        let code10 = "notPipe = !(1 |> 2)";
2007        assert_eq!(
2008            // TODO: We don't currently parse this, but we should.  It should be
2009            // a runtime error instead.
2010            parse_execute(code10).await.unwrap_err(),
2011            KclError::new_syntax(KclErrorDetails::new(
2012                "Unexpected token: !".to_owned(),
2013                vec![SourceRange::new(10, 11, ModuleId::default())],
2014            ))
2015        );
2016
2017        let code11 = "
2018fn identity(x) { return x }
2019notPipeSub = 1 |> identity(!%))";
2020        assert_eq!(
2021            // TODO: We don't currently parse this, but we should.  It should be
2022            // a runtime error instead.
2023            parse_execute(code11).await.unwrap_err(),
2024            KclError::new_syntax(KclErrorDetails::new(
2025                "There was an unexpected `!`. Try removing it.".to_owned(),
2026                vec![SourceRange::new(56, 57, ModuleId::default())],
2027            ))
2028        );
2029
2030        // TODO: Add these tests when we support these types.
2031        // let notNan = !NaN
2032        // let notInfinity = !Infinity
2033    }
2034
2035    #[tokio::test(flavor = "multi_thread")]
2036    async fn test_math_negative_variable_in_binary_expression() {
2037        let ast = r#"sigmaAllow = 35000 // psi
2038width = 1 // inch
2039
2040p = 150 // lbs
2041distance = 6 // inches
2042FOS = 2
2043
2044leg1 = 5 // inches
2045leg2 = 8 // inches
2046
2047thickness_squared = distance * p * FOS * 6 / sigmaAllow
2048thickness = 0.56 // inches. App does not support square root function yet
2049
2050bracket = startSketchOn(XY)
2051  |> startProfile(at = [0,0])
2052  |> line(end = [0, leg1])
2053  |> line(end = [leg2, 0])
2054  |> line(end = [0, -thickness])
2055  |> line(end = [-leg2 + thickness, 0])
2056"#;
2057        parse_execute(ast).await.unwrap();
2058    }
2059
2060    #[tokio::test(flavor = "multi_thread")]
2061    async fn test_execute_function_no_return() {
2062        let ast = r#"fn test(@origin) {
2063  origin
2064}
2065
2066test([0, 0])
2067"#;
2068        let result = parse_execute(ast).await;
2069        assert!(result.is_err());
2070        assert!(result.unwrap_err().to_string().contains("undefined"),);
2071    }
2072
2073    #[tokio::test(flavor = "multi_thread")]
2074    async fn test_math_doubly_nested_parens() {
2075        let ast = r#"sigmaAllow = 35000 // psi
2076width = 4 // inch
2077p = 150 // Force on shelf - lbs
2078distance = 6 // inches
2079FOS = 2
2080leg1 = 5 // inches
2081leg2 = 8 // inches
2082thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2083thickness = 0.32 // inches. App does not support square root function yet
2084bracket = startSketchOn(XY)
2085  |> startProfile(at = [0,0])
2086    |> line(end = [0, leg1])
2087  |> line(end = [leg2, 0])
2088  |> line(end = [0, -thickness])
2089  |> line(end = [-1 * leg2 + thickness, 0])
2090  |> line(end = [0, -1 * leg1 + thickness])
2091  |> close()
2092  |> extrude(length = width)
2093"#;
2094        parse_execute(ast).await.unwrap();
2095    }
2096
2097    #[tokio::test(flavor = "multi_thread")]
2098    async fn test_math_nested_parens_one_less() {
2099        let ast = r#" sigmaAllow = 35000 // psi
2100width = 4 // inch
2101p = 150 // Force on shelf - lbs
2102distance = 6 // inches
2103FOS = 2
2104leg1 = 5 // inches
2105leg2 = 8 // inches
2106thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2107thickness = 0.32 // inches. App does not support square root function yet
2108bracket = startSketchOn(XY)
2109  |> startProfile(at = [0,0])
2110    |> line(end = [0, leg1])
2111  |> line(end = [leg2, 0])
2112  |> line(end = [0, -thickness])
2113  |> line(end = [-1 * leg2 + thickness, 0])
2114  |> line(end = [0, -1 * leg1 + thickness])
2115  |> close()
2116  |> extrude(length = width)
2117"#;
2118        parse_execute(ast).await.unwrap();
2119    }
2120
2121    #[tokio::test(flavor = "multi_thread")]
2122    async fn test_fn_as_operand() {
2123        let ast = r#"fn f() { return 1 }
2124x = f()
2125y = x + 1
2126z = f() + 1
2127w = f() + f()
2128"#;
2129        parse_execute(ast).await.unwrap();
2130    }
2131
2132    #[tokio::test(flavor = "multi_thread")]
2133    async fn kcl_test_ids_stable_between_executions() {
2134        let code = r#"sketch001 = startSketchOn(XZ)
2135|> startProfile(at = [61.74, 206.13])
2136|> xLine(length = 305.11, tag = $seg01)
2137|> yLine(length = -291.85)
2138|> xLine(length = -segLen(seg01))
2139|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2140|> close()
2141|> extrude(length = 40.14)
2142|> shell(
2143    thickness = 3.14,
2144    faces = [seg01]
2145)
2146"#;
2147
2148        let ctx = crate::test_server::new_context(true, None).await.unwrap();
2149        let old_program = crate::Program::parse_no_errs(code).unwrap();
2150
2151        // Execute the program.
2152        if let Err(err) = ctx.run_with_caching(old_program).await {
2153            let report = err.into_miette_report_with_outputs(code).unwrap();
2154            let report = miette::Report::new(report);
2155            panic!("Error executing program: {:?}", report);
2156        }
2157
2158        // Get the id_generator from the first execution.
2159        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2160
2161        let code = r#"sketch001 = startSketchOn(XZ)
2162|> startProfile(at = [62.74, 206.13])
2163|> xLine(length = 305.11, tag = $seg01)
2164|> yLine(length = -291.85)
2165|> xLine(length = -segLen(seg01))
2166|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2167|> close()
2168|> extrude(length = 40.14)
2169|> shell(
2170    faces = [seg01],
2171    thickness = 3.14,
2172)
2173"#;
2174
2175        // Execute a slightly different program again.
2176        let program = crate::Program::parse_no_errs(code).unwrap();
2177        // Execute the program.
2178        ctx.run_with_caching(program).await.unwrap();
2179
2180        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2181
2182        assert_eq!(id_generator, new_id_generator);
2183    }
2184
2185    #[tokio::test(flavor = "multi_thread")]
2186    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2187        let code = r#"sketch001 = startSketchOn(XZ)
2188|> startProfile(at = [61.74, 206.13])
2189|> xLine(length = 305.11, tag = $seg01)
2190|> yLine(length = -291.85)
2191|> xLine(length = -segLen(seg01))
2192|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2193|> close()
2194|> extrude(length = 40.14)
2195|> shell(
2196    thickness = 3.14,
2197    faces = [seg01]
2198)
2199"#;
2200
2201        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2202        let old_program = crate::Program::parse_no_errs(code).unwrap();
2203
2204        // Execute the program.
2205        ctx.run_with_caching(old_program.clone()).await.unwrap();
2206
2207        let settings_state = cache::read_old_ast().await.unwrap().settings;
2208
2209        // Ensure the settings are as expected.
2210        assert_eq!(settings_state, ctx.settings);
2211
2212        // Change a setting.
2213        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2214
2215        // Execute the program.
2216        ctx.run_with_caching(old_program.clone()).await.unwrap();
2217
2218        let settings_state = cache::read_old_ast().await.unwrap().settings;
2219
2220        // Ensure the settings are as expected.
2221        assert_eq!(settings_state, ctx.settings);
2222
2223        // Change a setting.
2224        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2225
2226        // Execute the program.
2227        ctx.run_with_caching(old_program).await.unwrap();
2228
2229        let settings_state = cache::read_old_ast().await.unwrap().settings;
2230
2231        // Ensure the settings are as expected.
2232        assert_eq!(settings_state, ctx.settings);
2233
2234        ctx.close().await;
2235    }
2236
2237    #[tokio::test(flavor = "multi_thread")]
2238    async fn mock_after_not_mock() {
2239        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2240        let program = crate::Program::parse_no_errs("x = 2").unwrap();
2241        let result = ctx.run_with_caching(program).await.unwrap();
2242        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2243
2244        let ctx2 = ExecutorContext::new_mock(None).await;
2245        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2246        let result = ctx2.run_mock(program2, true).await.unwrap();
2247        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2248
2249        ctx.close().await;
2250        ctx2.close().await;
2251    }
2252
2253    #[tokio::test(flavor = "multi_thread")]
2254    async fn read_tag_version() {
2255        let ast = r#"fn bar(@t) {
2256  return startSketchOn(XY)
2257    |> startProfile(at = [0,0])
2258    |> angledLine(
2259        angle = -60,
2260        length = segLen(t),
2261    )
2262    |> line(end = [0, 0])
2263    |> close()
2264}
2265
2266sketch = startSketchOn(XY)
2267  |> startProfile(at = [0,0])
2268  |> line(end = [0, 10])
2269  |> line(end = [10, 0], tag = $tag0)
2270  |> line(end = [0, 0])
2271
2272fn foo() {
2273  // tag0 tags an edge
2274  return bar(tag0)
2275}
2276
2277solid = sketch |> extrude(length = 10)
2278// tag0 tags a face
2279sketch2 = startSketchOn(solid, face = tag0)
2280  |> startProfile(at = [0,0])
2281  |> line(end = [0, 1])
2282  |> line(end = [1, 0])
2283  |> line(end = [0, 0])
2284
2285foo() |> extrude(length = 1)
2286"#;
2287        parse_execute(ast).await.unwrap();
2288    }
2289}