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            &ModuleRepr::Kcl(program.ast.clone(), None),
1048            &mut universe,
1049            exec_state,
1050        )
1051        .await
1052        .map_err(|err| {
1053            println!("Error: {err:?}");
1054            let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
1055                .global
1056                .path_to_source_id
1057                .iter()
1058                .map(|(k, v)| ((*v), k.clone()))
1059                .collect();
1060
1061            KclErrorWithOutputs::new(
1062                err,
1063                exec_state.errors().to_vec(),
1064                #[cfg(feature = "artifact-graph")]
1065                exec_state.global.operations.clone(),
1066                #[cfg(feature = "artifact-graph")]
1067                exec_state.global.artifact_commands.clone(),
1068                #[cfg(feature = "artifact-graph")]
1069                exec_state.global.artifact_graph.clone(),
1070                module_id_to_module_path,
1071                exec_state.global.id_to_source.clone(),
1072                default_planes,
1073            )
1074        })?;
1075
1076        Ok((universe, root_imports))
1077    }
1078
1079    /// Perform the execution of a program.  Accept all possible parameters and
1080    /// output everything.
1081    async fn inner_run(
1082        &self,
1083        program: &crate::Program,
1084        cached_body_items: usize,
1085        exec_state: &mut ExecState,
1086        preserve_mem: bool,
1087    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1088        let _stats = crate::log::LogPerfStats::new("Interpretation");
1089
1090        // Re-apply the settings, in case the cache was busted.
1091        self.engine
1092            .reapply_settings(&self.settings, Default::default(), exec_state.id_generator())
1093            .await
1094            .map_err(KclErrorWithOutputs::no_outputs)?;
1095
1096        let default_planes = self.engine.get_default_planes().read().await.clone();
1097        let result = self
1098            .execute_and_build_graph(&program.ast, cached_body_items, exec_state, preserve_mem)
1099            .await;
1100
1101        crate::log::log(format!(
1102            "Post interpretation KCL memory stats: {:#?}",
1103            exec_state.stack().memory.stats
1104        ));
1105        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1106
1107        let env_ref = result.map_err(|e| {
1108            let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = exec_state
1109                .global
1110                .path_to_source_id
1111                .iter()
1112                .map(|(k, v)| ((*v), k.clone()))
1113                .collect();
1114
1115            KclErrorWithOutputs::new(
1116                e,
1117                exec_state.errors().to_vec(),
1118                #[cfg(feature = "artifact-graph")]
1119                exec_state.global.operations.clone(),
1120                #[cfg(feature = "artifact-graph")]
1121                exec_state.global.artifact_commands.clone(),
1122                #[cfg(feature = "artifact-graph")]
1123                exec_state.global.artifact_graph.clone(),
1124                module_id_to_module_path,
1125                exec_state.global.id_to_source.clone(),
1126                default_planes.clone(),
1127            )
1128        })?;
1129
1130        if !self.is_mock() {
1131            let mut mem = exec_state.stack().deep_clone();
1132            mem.restore_env(env_ref);
1133            cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1134        }
1135        let session_data = self.engine.get_session_data().await;
1136
1137        Ok((env_ref, session_data))
1138    }
1139
1140    /// Execute an AST's program and build auxiliary outputs like the artifact
1141    /// graph.
1142    async fn execute_and_build_graph(
1143        &self,
1144        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1145        #[cfg_attr(not(feature = "artifact-graph"), expect(unused))] cached_body_items: usize,
1146        exec_state: &mut ExecState,
1147        preserve_mem: bool,
1148    ) -> Result<EnvironmentRef, KclError> {
1149        // Don't early return!  We need to build other outputs regardless of
1150        // whether execution failed.
1151
1152        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1153            .await?;
1154
1155        let exec_result = self
1156            .exec_module_body(
1157                program,
1158                exec_state,
1159                preserve_mem,
1160                ModuleId::default(),
1161                &ModulePath::Main,
1162            )
1163            .await;
1164
1165        // Ensure all the async commands completed.
1166        self.engine.ensure_async_commands_completed().await?;
1167
1168        // If we errored out and early-returned, there might be commands which haven't been executed
1169        // and should be dropped.
1170        self.engine.clear_queues().await;
1171
1172        #[cfg(feature = "artifact-graph")]
1173        {
1174            let new_commands = self.engine.take_artifact_commands().await;
1175            let new_responses = self.engine.take_responses().await;
1176            let initial_graph = exec_state.global.artifact_graph.clone();
1177
1178            // Build the artifact graph.
1179            let graph_result = build_artifact_graph(
1180                &new_commands,
1181                &new_responses,
1182                program,
1183                cached_body_items,
1184                &mut exec_state.global.artifacts,
1185                initial_graph,
1186            );
1187            // Move the artifact commands and responses into ExecState to
1188            // simplify cache management and error creation.
1189            exec_state.global.artifact_commands.extend(new_commands);
1190            exec_state.global.artifact_responses.extend(new_responses);
1191
1192            match graph_result {
1193                Ok(artifact_graph) => {
1194                    exec_state.global.artifact_graph = artifact_graph;
1195                    exec_result.map(|(_, env_ref, _)| env_ref)
1196                }
1197                Err(err) => {
1198                    // Prefer the exec error.
1199                    exec_result.and(Err(err))
1200                }
1201            }
1202        }
1203        #[cfg(not(feature = "artifact-graph"))]
1204        {
1205            exec_result.map(|(_, env_ref, _)| env_ref)
1206        }
1207    }
1208
1209    /// 'Import' std::prelude as the outermost scope.
1210    ///
1211    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1212    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1213        if exec_state.stack().memory.requires_std() {
1214            let id = self
1215                .open_module(
1216                    &ImportPath::Std {
1217                        path: vec!["std".to_owned(), "prelude".to_owned()],
1218                    },
1219                    &[],
1220                    exec_state,
1221                    source_range,
1222                )
1223                .await?;
1224            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1225
1226            exec_state.mut_stack().memory.set_std(module_memory);
1227        }
1228
1229        Ok(())
1230    }
1231
1232    /// Get a snapshot of the current scene.
1233    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1234        // Zoom to fit.
1235        self.engine
1236            .send_modeling_cmd(
1237                uuid::Uuid::new_v4(),
1238                crate::execution::SourceRange::default(),
1239                &ModelingCmd::from(mcmd::ZoomToFit {
1240                    object_ids: Default::default(),
1241                    animated: false,
1242                    padding: 0.1,
1243                }),
1244            )
1245            .await
1246            .map_err(KclErrorWithOutputs::no_outputs)?;
1247
1248        // Send a snapshot request to the engine.
1249        let resp = self
1250            .engine
1251            .send_modeling_cmd(
1252                uuid::Uuid::new_v4(),
1253                crate::execution::SourceRange::default(),
1254                &ModelingCmd::from(mcmd::TakeSnapshot {
1255                    format: ImageFormat::Png,
1256                }),
1257            )
1258            .await
1259            .map_err(KclErrorWithOutputs::no_outputs)?;
1260
1261        let OkWebSocketResponseData::Modeling {
1262            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1263        } = resp
1264        else {
1265            return Err(ExecError::BadPng(format!(
1266                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1267            )));
1268        };
1269        Ok(contents)
1270    }
1271
1272    /// Export the current scene as a CAD file.
1273    pub async fn export(
1274        &self,
1275        format: kittycad_modeling_cmds::format::OutputFormat3d,
1276    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1277        let resp = self
1278            .engine
1279            .send_modeling_cmd(
1280                uuid::Uuid::new_v4(),
1281                crate::SourceRange::default(),
1282                &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1283                    entity_ids: vec![],
1284                    format,
1285                }),
1286            )
1287            .await?;
1288
1289        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1290            return Err(KclError::Internal(crate::errors::KclErrorDetails::new(
1291                format!("Expected Export response, got {resp:?}",),
1292                vec![SourceRange::default()],
1293            )));
1294        };
1295
1296        Ok(files)
1297    }
1298
1299    /// Export the current scene as a STEP file.
1300    pub async fn export_step(
1301        &self,
1302        deterministic_time: bool,
1303    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1304        let files = self
1305            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1306                kittycad_modeling_cmds::format::step::export::Options {
1307                    coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1308                    created: if deterministic_time {
1309                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1310                            KclError::Internal(crate::errors::KclErrorDetails::new(
1311                                format!("Failed to parse date: {}", e),
1312                                vec![SourceRange::default()],
1313                            ))
1314                        })?)
1315                    } else {
1316                        None
1317                    },
1318                },
1319            ))
1320            .await?;
1321
1322        Ok(files)
1323    }
1324
1325    pub async fn close(&self) {
1326        self.engine.close().await;
1327    }
1328}
1329
1330#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS, JsonSchema)]
1331pub struct ArtifactId(Uuid);
1332
1333impl ArtifactId {
1334    pub fn new(uuid: Uuid) -> Self {
1335        Self(uuid)
1336    }
1337}
1338
1339impl From<Uuid> for ArtifactId {
1340    fn from(uuid: Uuid) -> Self {
1341        Self::new(uuid)
1342    }
1343}
1344
1345impl From<&Uuid> for ArtifactId {
1346    fn from(uuid: &Uuid) -> Self {
1347        Self::new(*uuid)
1348    }
1349}
1350
1351impl From<ArtifactId> for Uuid {
1352    fn from(id: ArtifactId) -> Self {
1353        id.0
1354    }
1355}
1356
1357impl From<&ArtifactId> for Uuid {
1358    fn from(id: &ArtifactId) -> Self {
1359        id.0
1360    }
1361}
1362
1363impl From<ModelingCmdId> for ArtifactId {
1364    fn from(id: ModelingCmdId) -> Self {
1365        Self::new(*id.as_ref())
1366    }
1367}
1368
1369impl From<&ModelingCmdId> for ArtifactId {
1370    fn from(id: &ModelingCmdId) -> Self {
1371        Self::new(*id.as_ref())
1372    }
1373}
1374
1375#[cfg(test)]
1376pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1377    parse_execute_with_project_dir(code, None).await
1378}
1379
1380#[cfg(test)]
1381pub(crate) async fn parse_execute_with_project_dir(
1382    code: &str,
1383    project_directory: Option<TypedPath>,
1384) -> Result<ExecTestResults, KclError> {
1385    let program = crate::Program::parse_no_errs(code)?;
1386
1387    let exec_ctxt = ExecutorContext {
1388        engine: Arc::new(Box::new(
1389            crate::engine::conn_mock::EngineConnection::new().await.map_err(|err| {
1390                KclError::Internal(crate::errors::KclErrorDetails::new(
1391                    format!("Failed to create mock engine connection: {}", err),
1392                    vec![SourceRange::default()],
1393                ))
1394            })?,
1395        )),
1396        fs: Arc::new(crate::fs::FileManager::new()),
1397        stdlib: Arc::new(crate::std::StdLib::new()),
1398        settings: ExecutorSettings {
1399            project_directory,
1400            ..Default::default()
1401        },
1402        context_type: ContextType::Mock,
1403    };
1404    let mut exec_state = ExecState::new(&exec_ctxt);
1405    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1406
1407    Ok(ExecTestResults {
1408        program,
1409        mem_env: result.0,
1410        exec_ctxt,
1411        exec_state,
1412    })
1413}
1414
1415#[cfg(test)]
1416#[derive(Debug)]
1417pub(crate) struct ExecTestResults {
1418    program: crate::Program,
1419    mem_env: EnvironmentRef,
1420    exec_ctxt: ExecutorContext,
1421    exec_state: ExecState,
1422}
1423
1424#[cfg(test)]
1425mod tests {
1426    use pretty_assertions::assert_eq;
1427
1428    use super::*;
1429    use crate::{errors::KclErrorDetails, execution::memory::Stack, ModuleId};
1430
1431    /// Convenience function to get a JSON value from memory and unwrap.
1432    #[track_caller]
1433    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1434        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1435    }
1436
1437    #[tokio::test(flavor = "multi_thread")]
1438    async fn test_execute_warn() {
1439        let text = "@blah";
1440        let result = parse_execute(text).await.unwrap();
1441        let errs = result.exec_state.errors();
1442        assert_eq!(errs.len(), 1);
1443        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1444        assert!(
1445            errs[0].message.contains("Unknown annotation"),
1446            "unexpected warning message: {}",
1447            errs[0].message
1448        );
1449    }
1450
1451    #[tokio::test(flavor = "multi_thread")]
1452    async fn test_execute_fn_definitions() {
1453        let ast = r#"fn def(@x) {
1454  return x
1455}
1456fn ghi(@x) {
1457  return x
1458}
1459fn jkl(@x) {
1460  return x
1461}
1462fn hmm(@x) {
1463  return x
1464}
1465
1466yo = 5 + 6
1467
1468abc = 3
1469identifierGuy = 5
1470part001 = startSketchOn(XY)
1471|> startProfile(at = [-1.2, 4.83])
1472|> line(end = [2.8, 0])
1473|> angledLine(angle = 100 + 100, length = 3.01)
1474|> angledLine(angle = abc, length = 3.02)
1475|> angledLine(angle = def(yo), length = 3.03)
1476|> angledLine(angle = ghi(2), length = 3.04)
1477|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1478|> close()
1479yo2 = hmm([identifierGuy + 5])"#;
1480
1481        parse_execute(ast).await.unwrap();
1482    }
1483
1484    #[tokio::test(flavor = "multi_thread")]
1485    async fn test_execute_with_pipe_substitutions_unary() {
1486        let ast = r#"myVar = 3
1487part001 = startSketchOn(XY)
1488  |> startProfile(at = [0, 0])
1489  |> line(end = [3, 4], tag = $seg01)
1490  |> line(end = [
1491  min([segLen(seg01), myVar]),
1492  -legLen(hypotenuse = segLen(seg01), leg = myVar)
1493])
1494"#;
1495
1496        parse_execute(ast).await.unwrap();
1497    }
1498
1499    #[tokio::test(flavor = "multi_thread")]
1500    async fn test_execute_with_pipe_substitutions() {
1501        let ast = r#"myVar = 3
1502part001 = startSketchOn(XY)
1503  |> startProfile(at = [0, 0])
1504  |> line(end = [3, 4], tag = $seg01)
1505  |> line(end = [
1506  min([segLen(seg01), myVar]),
1507  legLen(hypotenuse = segLen(seg01), leg = myVar)
1508])
1509"#;
1510
1511        parse_execute(ast).await.unwrap();
1512    }
1513
1514    #[tokio::test(flavor = "multi_thread")]
1515    async fn test_execute_with_inline_comment() {
1516        let ast = r#"baseThick = 1
1517armAngle = 60
1518
1519baseThickHalf = baseThick / 2
1520halfArmAngle = armAngle / 2
1521
1522arrExpShouldNotBeIncluded = [1, 2, 3]
1523objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1524
1525part001 = startSketchOn(XY)
1526  |> startProfile(at = [0, 0])
1527  |> yLine(endAbsolute = 1)
1528  |> xLine(length = 3.84) // selection-range-7ish-before-this
1529
1530variableBelowShouldNotBeIncluded = 3
1531"#;
1532
1533        parse_execute(ast).await.unwrap();
1534    }
1535
1536    #[tokio::test(flavor = "multi_thread")]
1537    async fn test_execute_with_function_literal_in_pipe() {
1538        let ast = r#"w = 20
1539l = 8
1540h = 10
1541
1542fn thing() {
1543  return -8
1544}
1545
1546firstExtrude = startSketchOn(XY)
1547  |> startProfile(at = [0,0])
1548  |> line(end = [0, l])
1549  |> line(end = [w, 0])
1550  |> line(end = [0, thing()])
1551  |> close()
1552  |> extrude(length = h)"#;
1553
1554        parse_execute(ast).await.unwrap();
1555    }
1556
1557    #[tokio::test(flavor = "multi_thread")]
1558    async fn test_execute_with_function_unary_in_pipe() {
1559        let ast = r#"w = 20
1560l = 8
1561h = 10
1562
1563fn thing(@x) {
1564  return -x
1565}
1566
1567firstExtrude = startSketchOn(XY)
1568  |> startProfile(at = [0,0])
1569  |> line(end = [0, l])
1570  |> line(end = [w, 0])
1571  |> line(end = [0, thing(8)])
1572  |> close()
1573  |> extrude(length = h)"#;
1574
1575        parse_execute(ast).await.unwrap();
1576    }
1577
1578    #[tokio::test(flavor = "multi_thread")]
1579    async fn test_execute_with_function_array_in_pipe() {
1580        let ast = r#"w = 20
1581l = 8
1582h = 10
1583
1584fn thing(@x) {
1585  return [0, -x]
1586}
1587
1588firstExtrude = startSketchOn(XY)
1589  |> startProfile(at = [0,0])
1590  |> line(end = [0, l])
1591  |> line(end = [w, 0])
1592  |> line(end = thing(8))
1593  |> close()
1594  |> extrude(length = h)"#;
1595
1596        parse_execute(ast).await.unwrap();
1597    }
1598
1599    #[tokio::test(flavor = "multi_thread")]
1600    async fn test_execute_with_function_call_in_pipe() {
1601        let ast = r#"w = 20
1602l = 8
1603h = 10
1604
1605fn other_thing(@y) {
1606  return -y
1607}
1608
1609fn thing(@x) {
1610  return other_thing(x)
1611}
1612
1613firstExtrude = startSketchOn(XY)
1614  |> startProfile(at = [0,0])
1615  |> line(end = [0, l])
1616  |> line(end = [w, 0])
1617  |> line(end = [0, thing(8)])
1618  |> close()
1619  |> extrude(length = h)"#;
1620
1621        parse_execute(ast).await.unwrap();
1622    }
1623
1624    #[tokio::test(flavor = "multi_thread")]
1625    async fn test_execute_with_function_sketch() {
1626        let ast = r#"fn box(h, l, w) {
1627 myBox = startSketchOn(XY)
1628    |> startProfile(at = [0,0])
1629    |> line(end = [0, l])
1630    |> line(end = [w, 0])
1631    |> line(end = [0, -l])
1632    |> close()
1633    |> extrude(length = h)
1634
1635  return myBox
1636}
1637
1638fnBox = box(h = 3, l = 6, w = 10)"#;
1639
1640        parse_execute(ast).await.unwrap();
1641    }
1642
1643    #[tokio::test(flavor = "multi_thread")]
1644    async fn test_get_member_of_object_with_function_period() {
1645        let ast = r#"fn box(@obj) {
1646 myBox = startSketchOn(XY)
1647    |> startProfile(at = obj.start)
1648    |> line(end = [0, obj.l])
1649    |> line(end = [obj.w, 0])
1650    |> line(end = [0, -obj.l])
1651    |> close()
1652    |> extrude(length = obj.h)
1653
1654  return myBox
1655}
1656
1657thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1658"#;
1659        parse_execute(ast).await.unwrap();
1660    }
1661
1662    #[tokio::test(flavor = "multi_thread")]
1663    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
1664    async fn test_object_member_starting_pipeline() {
1665        let ast = r#"
1666fn test2() {
1667  return {
1668    thing: startSketchOn(XY)
1669      |> startProfile(at = [0, 0])
1670      |> line(end = [0, 1])
1671      |> line(end = [1, 0])
1672      |> line(end = [0, -1])
1673      |> close()
1674  }
1675}
1676
1677x2 = test2()
1678
1679x2.thing
1680  |> extrude(length = 10)
1681"#;
1682        parse_execute(ast).await.unwrap();
1683    }
1684
1685    #[tokio::test(flavor = "multi_thread")]
1686    #[ignore] // ignore til we get loops
1687    async fn test_execute_with_function_sketch_loop_objects() {
1688        let ast = r#"fn box(obj) {
1689let myBox = startSketchOn(XY)
1690    |> startProfile(at = obj.start)
1691    |> line(end = [0, obj.l])
1692    |> line(end = [obj.w, 0])
1693    |> line(end = [0, -obj.l])
1694    |> close()
1695    |> extrude(length = obj.h)
1696
1697  return myBox
1698}
1699
1700for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1701  thisBox = box(var)
1702}"#;
1703
1704        parse_execute(ast).await.unwrap();
1705    }
1706
1707    #[tokio::test(flavor = "multi_thread")]
1708    #[ignore] // ignore til we get loops
1709    async fn test_execute_with_function_sketch_loop_array() {
1710        let ast = r#"fn box(h, l, w, start) {
1711 myBox = startSketchOn(XY)
1712    |> startProfile(at = [0,0])
1713    |> line(end = [0, l])
1714    |> line(end = [w, 0])
1715    |> line(end = [0, -l])
1716    |> close()
1717    |> extrude(length = h)
1718
1719  return myBox
1720}
1721
1722
1723for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1724  const thisBox = box(var[0], var[1], var[2], var[3])
1725}"#;
1726
1727        parse_execute(ast).await.unwrap();
1728    }
1729
1730    #[tokio::test(flavor = "multi_thread")]
1731    async fn test_get_member_of_array_with_function() {
1732        let ast = r#"fn box(@arr) {
1733 myBox =startSketchOn(XY)
1734    |> startProfile(at = arr[0])
1735    |> line(end = [0, arr[1]])
1736    |> line(end = [arr[2], 0])
1737    |> line(end = [0, -arr[1]])
1738    |> close()
1739    |> extrude(length = arr[3])
1740
1741  return myBox
1742}
1743
1744thisBox = box([[0,0], 6, 10, 3])
1745
1746"#;
1747        parse_execute(ast).await.unwrap();
1748    }
1749
1750    #[tokio::test(flavor = "multi_thread")]
1751    async fn test_function_cannot_access_future_definitions() {
1752        let ast = r#"
1753fn returnX() {
1754  // x shouldn't be defined yet.
1755  return x
1756}
1757
1758x = 5
1759
1760answer = returnX()"#;
1761
1762        let result = parse_execute(ast).await;
1763        let err = result.unwrap_err();
1764        assert_eq!(err.message(), "`x` is not defined");
1765    }
1766
1767    #[tokio::test(flavor = "multi_thread")]
1768    async fn test_override_prelude() {
1769        let text = "PI = 3.0";
1770        let result = parse_execute(text).await.unwrap();
1771        let errs = result.exec_state.errors();
1772        assert!(errs.is_empty());
1773    }
1774
1775    #[tokio::test(flavor = "multi_thread")]
1776    async fn type_aliases() {
1777        let text = r#"type MyTy = [number; 2]
1778fn foo(@x: MyTy) {
1779    return x[0]
1780}
1781
1782foo([0, 1])
1783
1784type Other = MyTy | Helix
1785"#;
1786        let result = parse_execute(text).await.unwrap();
1787        let errs = result.exec_state.errors();
1788        assert!(errs.is_empty());
1789    }
1790
1791    #[tokio::test(flavor = "multi_thread")]
1792    async fn test_cannot_shebang_in_fn() {
1793        let ast = r#"
1794fn foo() {
1795  #!hello
1796  return true
1797}
1798
1799foo
1800"#;
1801
1802        let result = parse_execute(ast).await;
1803        let err = result.unwrap_err();
1804        assert_eq!(
1805            err,
1806            KclError::Syntax(KclErrorDetails::new(
1807                "Unexpected token: #".to_owned(),
1808                vec![SourceRange::new(14, 15, ModuleId::default())],
1809            )),
1810        );
1811    }
1812
1813    #[tokio::test(flavor = "multi_thread")]
1814    async fn test_pattern_transform_function_cannot_access_future_definitions() {
1815        let ast = r#"
1816fn transform(@replicaId) {
1817  // x shouldn't be defined yet.
1818  scale = x
1819  return {
1820    translate = [0, 0, replicaId * 10],
1821    scale = [scale, 1, 0],
1822  }
1823}
1824
1825fn layer() {
1826  return startSketchOn(XY)
1827    |> circle( center= [0, 0], radius= 1, tag = $tag1)
1828    |> extrude(length = 10)
1829}
1830
1831x = 5
1832
1833// The 10 layers are replicas of each other, with a transform applied to each.
1834shape = layer() |> patternTransform(instances = 10, transform = transform)
1835"#;
1836
1837        let result = parse_execute(ast).await;
1838        let err = result.unwrap_err();
1839        assert_eq!(err.message(), "`x` is not defined",);
1840    }
1841
1842    // ADAM: Move some of these into simulation tests.
1843
1844    #[tokio::test(flavor = "multi_thread")]
1845    async fn test_math_execute_with_functions() {
1846        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
1847        let result = parse_execute(ast).await.unwrap();
1848        assert_eq!(
1849            5.0,
1850            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1851                .as_f64()
1852                .unwrap()
1853        );
1854    }
1855
1856    #[tokio::test(flavor = "multi_thread")]
1857    async fn test_math_execute() {
1858        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
1859        let result = parse_execute(ast).await.unwrap();
1860        assert_eq!(
1861            7.4,
1862            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1863                .as_f64()
1864                .unwrap()
1865        );
1866    }
1867
1868    #[tokio::test(flavor = "multi_thread")]
1869    async fn test_math_execute_start_negative() {
1870        let ast = r#"myVar = -5 + 6"#;
1871        let result = parse_execute(ast).await.unwrap();
1872        assert_eq!(
1873            1.0,
1874            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1875                .as_f64()
1876                .unwrap()
1877        );
1878    }
1879
1880    #[tokio::test(flavor = "multi_thread")]
1881    async fn test_math_execute_with_pi() {
1882        let ast = r#"myVar = PI * 2"#;
1883        let result = parse_execute(ast).await.unwrap();
1884        assert_eq!(
1885            std::f64::consts::TAU,
1886            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
1887                .as_f64()
1888                .unwrap()
1889        );
1890    }
1891
1892    #[tokio::test(flavor = "multi_thread")]
1893    async fn test_math_define_decimal_without_leading_zero() {
1894        let ast = r#"thing = .4 + 7"#;
1895        let result = parse_execute(ast).await.unwrap();
1896        assert_eq!(
1897            7.4,
1898            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
1899                .as_f64()
1900                .unwrap()
1901        );
1902    }
1903
1904    #[tokio::test(flavor = "multi_thread")]
1905    async fn test_zero_param_fn() {
1906        let ast = r#"sigmaAllow = 35000 // psi
1907leg1 = 5 // inches
1908leg2 = 8 // inches
1909fn thickness() { return 0.56 }
1910
1911bracket = startSketchOn(XY)
1912  |> startProfile(at = [0,0])
1913  |> line(end = [0, leg1])
1914  |> line(end = [leg2, 0])
1915  |> line(end = [0, -thickness()])
1916  |> line(end = [-leg2 + thickness(), 0])
1917"#;
1918        parse_execute(ast).await.unwrap();
1919    }
1920
1921    #[tokio::test(flavor = "multi_thread")]
1922    async fn test_unary_operator_not_succeeds() {
1923        let ast = r#"
1924fn returnTrue() { return !false }
1925t = true
1926f = false
1927notTrue = !t
1928notFalse = !f
1929c = !!true
1930d = !returnTrue()
1931
1932assertIs(!false, error = "expected to pass")
1933
1934fn check(x) {
1935  assertIs(!x, error = "expected argument to be false")
1936  return true
1937}
1938check(x = false)
1939"#;
1940        let result = parse_execute(ast).await.unwrap();
1941        assert_eq!(
1942            false,
1943            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
1944                .as_bool()
1945                .unwrap()
1946        );
1947        assert_eq!(
1948            true,
1949            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
1950                .as_bool()
1951                .unwrap()
1952        );
1953        assert_eq!(
1954            true,
1955            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
1956                .as_bool()
1957                .unwrap()
1958        );
1959        assert_eq!(
1960            false,
1961            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
1962                .as_bool()
1963                .unwrap()
1964        );
1965    }
1966
1967    #[tokio::test(flavor = "multi_thread")]
1968    async fn test_unary_operator_not_on_non_bool_fails() {
1969        let code1 = r#"
1970// Yup, this is null.
1971myNull = 0 / 0
1972notNull = !myNull
1973"#;
1974        assert_eq!(
1975            parse_execute(code1).await.unwrap_err().message(),
1976            "Cannot apply unary operator ! to non-boolean value: number(default units)",
1977        );
1978
1979        let code2 = "notZero = !0";
1980        assert_eq!(
1981            parse_execute(code2).await.unwrap_err().message(),
1982            "Cannot apply unary operator ! to non-boolean value: number(default units)",
1983        );
1984
1985        let code3 = r#"
1986notEmptyString = !""
1987"#;
1988        assert_eq!(
1989            parse_execute(code3).await.unwrap_err().message(),
1990            "Cannot apply unary operator ! to non-boolean value: string",
1991        );
1992
1993        let code4 = r#"
1994obj = { a = 1 }
1995notMember = !obj.a
1996"#;
1997        assert_eq!(
1998            parse_execute(code4).await.unwrap_err().message(),
1999            "Cannot apply unary operator ! to non-boolean value: number(default units)",
2000        );
2001
2002        let code5 = "
2003a = []
2004notArray = !a";
2005        assert_eq!(
2006            parse_execute(code5).await.unwrap_err().message(),
2007            "Cannot apply unary operator ! to non-boolean value: [any; 0]",
2008        );
2009
2010        let code6 = "
2011x = {}
2012notObject = !x";
2013        assert_eq!(
2014            parse_execute(code6).await.unwrap_err().message(),
2015            "Cannot apply unary operator ! to non-boolean value: {  }",
2016        );
2017
2018        let code7 = "
2019fn x() { return 1 }
2020notFunction = !x";
2021        let fn_err = parse_execute(code7).await.unwrap_err();
2022        // These are currently printed out as JSON objects, so we don't want to
2023        // check the full error.
2024        assert!(
2025            fn_err
2026                .message()
2027                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2028            "Actual error: {:?}",
2029            fn_err
2030        );
2031
2032        let code8 = "
2033myTagDeclarator = $myTag
2034notTagDeclarator = !myTagDeclarator";
2035        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2036        // These are currently printed out as JSON objects, so we don't want to
2037        // check the full error.
2038        assert!(
2039            tag_declarator_err
2040                .message()
2041                .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
2042            "Actual error: {:?}",
2043            tag_declarator_err
2044        );
2045
2046        let code9 = "
2047myTagDeclarator = $myTag
2048notTagIdentifier = !myTag";
2049        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2050        // These are currently printed out as JSON objects, so we don't want to
2051        // check the full error.
2052        assert!(
2053            tag_identifier_err
2054                .message()
2055                .starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
2056            "Actual error: {:?}",
2057            tag_identifier_err
2058        );
2059
2060        let code10 = "notPipe = !(1 |> 2)";
2061        assert_eq!(
2062            // TODO: We don't currently parse this, but we should.  It should be
2063            // a runtime error instead.
2064            parse_execute(code10).await.unwrap_err(),
2065            KclError::Syntax(KclErrorDetails::new(
2066                "Unexpected token: !".to_owned(),
2067                vec![SourceRange::new(10, 11, ModuleId::default())],
2068            ))
2069        );
2070
2071        let code11 = "
2072fn identity(x) { return x }
2073notPipeSub = 1 |> identity(!%))";
2074        assert_eq!(
2075            // TODO: We don't currently parse this, but we should.  It should be
2076            // a runtime error instead.
2077            parse_execute(code11).await.unwrap_err(),
2078            KclError::Syntax(KclErrorDetails::new(
2079                "Unexpected token: |>".to_owned(),
2080                vec![SourceRange::new(44, 46, ModuleId::default())],
2081            ))
2082        );
2083
2084        // TODO: Add these tests when we support these types.
2085        // let notNan = !NaN
2086        // let notInfinity = !Infinity
2087    }
2088
2089    #[tokio::test(flavor = "multi_thread")]
2090    async fn test_math_negative_variable_in_binary_expression() {
2091        let ast = r#"sigmaAllow = 35000 // psi
2092width = 1 // inch
2093
2094p = 150 // lbs
2095distance = 6 // inches
2096FOS = 2
2097
2098leg1 = 5 // inches
2099leg2 = 8 // inches
2100
2101thickness_squared = distance * p * FOS * 6 / sigmaAllow
2102thickness = 0.56 // inches. App does not support square root function yet
2103
2104bracket = startSketchOn(XY)
2105  |> startProfile(at = [0,0])
2106  |> line(end = [0, leg1])
2107  |> line(end = [leg2, 0])
2108  |> line(end = [0, -thickness])
2109  |> line(end = [-leg2 + thickness, 0])
2110"#;
2111        parse_execute(ast).await.unwrap();
2112    }
2113
2114    #[tokio::test(flavor = "multi_thread")]
2115    async fn test_execute_function_no_return() {
2116        let ast = r#"fn test(@origin) {
2117  origin
2118}
2119
2120test([0, 0])
2121"#;
2122        let result = parse_execute(ast).await;
2123        assert!(result.is_err());
2124        assert!(result.unwrap_err().to_string().contains("undefined"),);
2125    }
2126
2127    #[tokio::test(flavor = "multi_thread")]
2128    async fn test_math_doubly_nested_parens() {
2129        let ast = r#"sigmaAllow = 35000 // psi
2130width = 4 // inch
2131p = 150 // Force on shelf - lbs
2132distance = 6 // inches
2133FOS = 2
2134leg1 = 5 // inches
2135leg2 = 8 // inches
2136thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2137thickness = 0.32 // inches. App does not support square root function yet
2138bracket = startSketchOn(XY)
2139  |> startProfile(at = [0,0])
2140    |> line(end = [0, leg1])
2141  |> line(end = [leg2, 0])
2142  |> line(end = [0, -thickness])
2143  |> line(end = [-1 * leg2 + thickness, 0])
2144  |> line(end = [0, -1 * leg1 + thickness])
2145  |> close()
2146  |> extrude(length = width)
2147"#;
2148        parse_execute(ast).await.unwrap();
2149    }
2150
2151    #[tokio::test(flavor = "multi_thread")]
2152    async fn test_math_nested_parens_one_less() {
2153        let ast = r#" sigmaAllow = 35000 // psi
2154width = 4 // inch
2155p = 150 // Force on shelf - lbs
2156distance = 6 // inches
2157FOS = 2
2158leg1 = 5 // inches
2159leg2 = 8 // inches
2160thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2161thickness = 0.32 // inches. App does not support square root function yet
2162bracket = startSketchOn(XY)
2163  |> startProfile(at = [0,0])
2164    |> line(end = [0, leg1])
2165  |> line(end = [leg2, 0])
2166  |> line(end = [0, -thickness])
2167  |> line(end = [-1 * leg2 + thickness, 0])
2168  |> line(end = [0, -1 * leg1 + thickness])
2169  |> close()
2170  |> extrude(length = width)
2171"#;
2172        parse_execute(ast).await.unwrap();
2173    }
2174
2175    #[tokio::test(flavor = "multi_thread")]
2176    async fn test_fn_as_operand() {
2177        let ast = r#"fn f() { return 1 }
2178x = f()
2179y = x + 1
2180z = f() + 1
2181w = f() + f()
2182"#;
2183        parse_execute(ast).await.unwrap();
2184    }
2185
2186    #[tokio::test(flavor = "multi_thread")]
2187    async fn kcl_test_ids_stable_between_executions() {
2188        let code = r#"sketch001 = startSketchOn(XZ)
2189|> startProfile(at = [61.74, 206.13])
2190|> xLine(length = 305.11, tag = $seg01)
2191|> yLine(length = -291.85)
2192|> xLine(length = -segLen(seg01))
2193|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2194|> close()
2195|> extrude(length = 40.14)
2196|> shell(
2197    thickness = 3.14,
2198    faces = [seg01]
2199)
2200"#;
2201
2202        let ctx = crate::test_server::new_context(true, None).await.unwrap();
2203        let old_program = crate::Program::parse_no_errs(code).unwrap();
2204
2205        // Execute the program.
2206        if let Err(err) = ctx.run_with_caching(old_program).await {
2207            let report = err.into_miette_report_with_outputs(code).unwrap();
2208            let report = miette::Report::new(report);
2209            panic!("Error executing program: {:?}", report);
2210        }
2211
2212        // Get the id_generator from the first execution.
2213        let id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
2214
2215        let code = r#"sketch001 = startSketchOn(XZ)
2216|> startProfile(at = [62.74, 206.13])
2217|> xLine(length = 305.11, tag = $seg01)
2218|> yLine(length = -291.85)
2219|> xLine(length = -segLen(seg01))
2220|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2221|> close()
2222|> extrude(length = 40.14)
2223|> shell(
2224    faces = [seg01],
2225    thickness = 3.14,
2226)
2227"#;
2228
2229        // Execute a slightly different program again.
2230        let program = crate::Program::parse_no_errs(code).unwrap();
2231        // Execute the program.
2232        ctx.run_with_caching(program).await.unwrap();
2233
2234        let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
2235
2236        assert_eq!(id_generator, new_id_generator);
2237    }
2238
2239    #[tokio::test(flavor = "multi_thread")]
2240    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2241        let code = r#"sketch001 = startSketchOn(XZ)
2242|> startProfile(at = [61.74, 206.13])
2243|> xLine(length = 305.11, tag = $seg01)
2244|> yLine(length = -291.85)
2245|> xLine(length = -segLen(seg01))
2246|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2247|> close()
2248|> extrude(length = 40.14)
2249|> shell(
2250    thickness = 3.14,
2251    faces = [seg01]
2252)
2253"#;
2254
2255        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2256        let old_program = crate::Program::parse_no_errs(code).unwrap();
2257
2258        // Execute the program.
2259        ctx.run_with_caching(old_program.clone()).await.unwrap();
2260
2261        let settings_state = cache::read_old_ast().await.unwrap().settings;
2262
2263        // Ensure the settings are as expected.
2264        assert_eq!(settings_state, ctx.settings);
2265
2266        // Change a setting.
2267        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2268
2269        // Execute the program.
2270        ctx.run_with_caching(old_program.clone()).await.unwrap();
2271
2272        let settings_state = cache::read_old_ast().await.unwrap().settings;
2273
2274        // Ensure the settings are as expected.
2275        assert_eq!(settings_state, ctx.settings);
2276
2277        // Change a setting.
2278        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2279
2280        // Execute the program.
2281        ctx.run_with_caching(old_program).await.unwrap();
2282
2283        let settings_state = cache::read_old_ast().await.unwrap().settings;
2284
2285        // Ensure the settings are as expected.
2286        assert_eq!(settings_state, ctx.settings);
2287
2288        ctx.close().await;
2289    }
2290
2291    #[tokio::test(flavor = "multi_thread")]
2292    async fn mock_after_not_mock() {
2293        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2294        let program = crate::Program::parse_no_errs("x = 2").unwrap();
2295        let result = ctx.run_with_caching(program).await.unwrap();
2296        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2297
2298        let ctx2 = ExecutorContext::new_mock(None).await;
2299        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2300        let result = ctx2.run_mock(program2, true).await.unwrap();
2301        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2302
2303        ctx.close().await;
2304        ctx2.close().await;
2305    }
2306
2307    #[tokio::test(flavor = "multi_thread")]
2308    async fn read_tag_version() {
2309        let ast = r#"fn bar(@t) {
2310  return startSketchOn(XY)
2311    |> startProfile(at = [0,0])
2312    |> angledLine(
2313        angle = -60,
2314        length = segLen(t),
2315    )
2316    |> line(end = [0, 0])
2317    |> close()
2318}
2319
2320sketch = startSketchOn(XY)
2321  |> startProfile(at = [0,0])
2322  |> line(end = [0, 10])
2323  |> line(end = [10, 0], tag = $tag0)
2324  |> line(end = [0, 0])
2325
2326fn foo() {
2327  // tag0 tags an edge
2328  return bar(tag0)
2329}
2330
2331solid = sketch |> extrude(length = 10)
2332// tag0 tags a face
2333sketch2 = startSketchOn(solid, face = tag0)
2334  |> startProfile(at = [0,0])
2335  |> line(end = [0, 1])
2336  |> line(end = [1, 0])
2337  |> line(end = [0, 0])
2338
2339foo() |> extrude(length = 1)
2340"#;
2341        parse_execute(ast).await.unwrap();
2342    }
2343}