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