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