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