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