kcl_lib/execution/
mod.rs

1//! The executor for the AST.
2
3#[cfg(feature = "artifact-graph")]
4use std::collections::BTreeMap;
5use std::sync::Arc;
6
7use anyhow::Result;
8#[cfg(feature = "artifact-graph")]
9pub use artifact::{
10    Artifact, ArtifactCommand, ArtifactGraph, CodeRef, SketchBlock, StartSketchOnFace, StartSketchOnPlane,
11};
12use cache::GlobalState;
13pub use cache::{bust_cache, clear_mem_cache};
14#[cfg(feature = "artifact-graph")]
15pub use cad_op::Group;
16pub use cad_op::Operation;
17pub use geometry::*;
18pub use id_generator::IdGenerator;
19pub(crate) use import::PreImportedGeometry;
20use indexmap::IndexMap;
21pub use kcl_value::{KclObjectFields, KclValue};
22use kcmc::{
23    ImageFormat, ModelingCmd, each_cmd as mcmd,
24    ok_response::{OkModelingCmdResponse, output::TakeSnapshot},
25    websocket::{ModelingSessionData, OkWebSocketResponseData},
26};
27use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
28pub use memory::EnvironmentRef;
29pub(crate) use modeling::ModelingCmdMeta;
30use serde::{Deserialize, Serialize};
31pub(crate) use sketch_solve::normalize_to_solver_unit;
32pub(crate) use state::ModuleArtifactState;
33pub use state::{ExecState, MetaSettings};
34use uuid::Uuid;
35
36use crate::{
37    CompilationError, ExecError, KclErrorWithOutputs, SourceRange,
38    engine::{EngineManager, GridScaleBehavior},
39    errors::{KclError, KclErrorDetails},
40    execution::{
41        cache::{CacheInformation, CacheResult},
42        import_graph::{Universe, UniverseMap},
43        typed_path::TypedPath,
44    },
45    fs::FileManager,
46    modules::{ModuleExecutionOutcome, ModuleId, ModulePath, ModuleRepr},
47    parsing::ast::types::{Expr, ImportPath, NodeRef},
48};
49#[cfg(feature = "artifact-graph")]
50use crate::{
51    collections::AhashIndexSet,
52    front::{Number, Object, ObjectId},
53};
54
55pub(crate) mod annotations;
56#[cfg(feature = "artifact-graph")]
57mod artifact;
58pub(crate) mod cache;
59mod cad_op;
60mod exec_ast;
61pub mod fn_call;
62mod geometry;
63mod id_generator;
64mod import;
65mod import_graph;
66pub(crate) mod kcl_value;
67mod memory;
68mod modeling;
69mod sketch_solve;
70mod state;
71pub mod typed_path;
72pub(crate) mod types;
73
74/// Convenience macro for handling control flow in execution by returning early
75/// if it is some kind of early return or stripping off the control flow
76/// otherwise.
77macro_rules! control_continue {
78    ($control_flow:expr) => {{
79        let cf = $control_flow;
80        if cf.is_some_return() {
81            return Ok(cf);
82        } else {
83            cf.into_value()
84        }
85    }};
86}
87// Expose the macro to other modules.
88pub(crate) use control_continue;
89
90#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
91pub enum ControlFlowKind {
92    #[default]
93    Continue,
94    Exit,
95}
96
97impl ControlFlowKind {
98    /// Returns true if this is any kind of early return.
99    pub fn is_some_return(&self) -> bool {
100        match self {
101            ControlFlowKind::Continue => false,
102            ControlFlowKind::Exit => true,
103        }
104    }
105}
106
107#[must_use = "You should always handle the control flow value when it is returned"]
108#[derive(Debug, Clone, PartialEq, Serialize)]
109pub struct KclValueControlFlow {
110    /// Use [control_continue] or [Self::into_value] to get the value.
111    value: KclValue,
112    pub control: ControlFlowKind,
113}
114
115impl KclValue {
116    pub(crate) fn continue_(self) -> KclValueControlFlow {
117        KclValueControlFlow {
118            value: self,
119            control: ControlFlowKind::Continue,
120        }
121    }
122
123    pub(crate) fn exit(self) -> KclValueControlFlow {
124        KclValueControlFlow {
125            value: self,
126            control: ControlFlowKind::Exit,
127        }
128    }
129}
130
131impl KclValueControlFlow {
132    /// Returns true if this is any kind of early return.
133    pub fn is_some_return(&self) -> bool {
134        self.control.is_some_return()
135    }
136
137    pub(crate) fn into_value(self) -> KclValue {
138        self.value
139    }
140}
141
142pub(crate) enum StatementKind<'a> {
143    Declaration { name: &'a str },
144    Expression,
145}
146
147#[derive(Debug, Clone, Copy)]
148pub enum PreserveMem {
149    Normal,
150    Always,
151}
152
153impl PreserveMem {
154    fn normal(self) -> bool {
155        match self {
156            PreserveMem::Normal => true,
157            PreserveMem::Always => false,
158        }
159    }
160}
161
162/// Outcome of executing a program.  This is used in TS.
163#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
164#[ts(export)]
165#[serde(rename_all = "camelCase")]
166pub struct ExecOutcome {
167    /// Variables in the top-level of the root module. Note that functions will have an invalid env ref.
168    pub variables: IndexMap<String, KclValue>,
169    /// Operations that have been performed in execution order, for display in
170    /// the Feature Tree.
171    #[cfg(feature = "artifact-graph")]
172    pub operations: Vec<Operation>,
173    /// Output artifact graph.
174    #[cfg(feature = "artifact-graph")]
175    pub artifact_graph: ArtifactGraph,
176    /// Objects in the scene, created from execution.
177    #[cfg(feature = "artifact-graph")]
178    #[serde(skip)]
179    pub scene_objects: Vec<Object>,
180    /// Map from source range to object ID for lookup of objects by their source
181    /// range.
182    #[cfg(feature = "artifact-graph")]
183    #[serde(skip)]
184    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
185    #[cfg(feature = "artifact-graph")]
186    #[serde(skip)]
187    pub var_solutions: Vec<(SourceRange, Number)>,
188    /// Non-fatal errors and warnings.
189    pub errors: Vec<CompilationError>,
190    /// File Names in module Id array index order
191    pub filenames: IndexMap<ModuleId, ModulePath>,
192    /// The default planes.
193    pub default_planes: Option<DefaultPlanes>,
194}
195
196/// Configuration for mock execution.
197#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct MockConfig {
199    pub use_prev_memory: bool,
200    /// True to do more costly analysis of whether the sketch block segments are
201    /// under-constrained.
202    pub freedom_analysis: bool,
203    /// The segments that were edited that triggered this execution.
204    #[cfg(feature = "artifact-graph")]
205    pub segment_ids_edited: AhashIndexSet<ObjectId>,
206}
207
208impl Default for MockConfig {
209    fn default() -> Self {
210        Self {
211            // By default, use previous memory. This is usually what you want.
212            use_prev_memory: true,
213            freedom_analysis: true,
214            #[cfg(feature = "artifact-graph")]
215            segment_ids_edited: AhashIndexSet::default(),
216        }
217    }
218}
219
220impl MockConfig {
221    #[must_use]
222    pub(crate) fn no_freedom_analysis(mut self) -> Self {
223        self.freedom_analysis = false;
224        self
225    }
226}
227
228#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
229#[ts(export)]
230#[serde(rename_all = "camelCase")]
231pub struct DefaultPlanes {
232    pub xy: uuid::Uuid,
233    pub xz: uuid::Uuid,
234    pub yz: uuid::Uuid,
235    pub neg_xy: uuid::Uuid,
236    pub neg_xz: uuid::Uuid,
237    pub neg_yz: uuid::Uuid,
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
241#[ts(export)]
242#[serde(tag = "type", rename_all = "camelCase")]
243pub struct TagIdentifier {
244    pub value: String,
245    // Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
246    // was written.
247    #[serde(skip)]
248    pub info: Vec<(usize, TagEngineInfo)>,
249    #[serde(skip)]
250    pub meta: Vec<Metadata>,
251}
252
253impl TagIdentifier {
254    /// Get the tag info for this tag at a specified epoch.
255    pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
256        for (e, info) in self.info.iter().rev() {
257            if *e <= at_epoch {
258                return Some(info);
259            }
260        }
261
262        None
263    }
264
265    /// Get the most recent tag info for this tag.
266    pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
267        self.info.last().map(|i| &i.1)
268    }
269
270    /// Add info from a different instance of this tag.
271    pub fn merge_info(&mut self, other: &TagIdentifier) {
272        assert_eq!(&self.value, &other.value);
273        for (oe, ot) in &other.info {
274            if let Some((e, t)) = self.info.last_mut() {
275                // If there is newer info, then skip this iteration.
276                if *e > *oe {
277                    continue;
278                }
279                // If we're in the same epoch, then overwrite.
280                if e == oe {
281                    *t = ot.clone();
282                    continue;
283                }
284            }
285            self.info.push((*oe, ot.clone()));
286        }
287    }
288}
289
290impl Eq for TagIdentifier {}
291
292impl std::fmt::Display for TagIdentifier {
293    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294        write!(f, "{}", self.value)
295    }
296}
297
298impl std::str::FromStr for TagIdentifier {
299    type Err = KclError;
300
301    fn from_str(s: &str) -> Result<Self, Self::Err> {
302        Ok(Self {
303            value: s.to_string(),
304            info: Vec::new(),
305            meta: Default::default(),
306        })
307    }
308}
309
310impl Ord for TagIdentifier {
311    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
312        self.value.cmp(&other.value)
313    }
314}
315
316impl PartialOrd for TagIdentifier {
317    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
318        Some(self.cmp(other))
319    }
320}
321
322impl std::hash::Hash for TagIdentifier {
323    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
324        self.value.hash(state);
325    }
326}
327
328/// Engine information for a tag.
329#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
330#[ts(export)]
331#[serde(tag = "type", rename_all = "camelCase")]
332pub struct TagEngineInfo {
333    /// The id of the tagged object.
334    pub id: uuid::Uuid,
335    /// The sketch the tag is on.
336    pub sketch: uuid::Uuid,
337    /// The path the tag is on.
338    pub path: Option<Path>,
339    /// The surface information for the tag.
340    pub surface: Option<ExtrudeSurface>,
341}
342
343#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
344pub enum BodyType {
345    Root,
346    Block,
347}
348
349/// Metadata.
350#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
351#[ts(export)]
352#[serde(rename_all = "camelCase")]
353pub struct Metadata {
354    /// The source range.
355    pub source_range: SourceRange,
356}
357
358impl From<Metadata> for Vec<SourceRange> {
359    fn from(meta: Metadata) -> Self {
360        vec![meta.source_range]
361    }
362}
363
364impl From<SourceRange> for Metadata {
365    fn from(source_range: SourceRange) -> Self {
366        Self { source_range }
367    }
368}
369
370impl<T> From<NodeRef<'_, T>> for Metadata {
371    fn from(node: NodeRef<'_, T>) -> Self {
372        Self {
373            source_range: SourceRange::new(node.start, node.end, node.module_id),
374        }
375    }
376}
377
378impl From<&Expr> for Metadata {
379    fn from(expr: &Expr) -> Self {
380        Self {
381            source_range: SourceRange::from(expr),
382        }
383    }
384}
385
386impl Metadata {
387    pub fn to_source_ref(meta: &[Metadata]) -> crate::front::SourceRef {
388        if meta.len() == 1 {
389            let meta = &meta[0];
390            return crate::front::SourceRef::Simple {
391                range: meta.source_range,
392            };
393        }
394        crate::front::SourceRef::BackTrace {
395            ranges: meta.iter().map(|m| m.source_range).collect(),
396        }
397    }
398}
399
400/// The type of ExecutorContext being used
401#[derive(PartialEq, Debug, Default, Clone)]
402pub enum ContextType {
403    /// Live engine connection
404    #[default]
405    Live,
406
407    /// Completely mocked connection
408    /// Mock mode is only for the Design Studio when they just want to mock engine calls and not
409    /// actually make them.
410    Mock,
411
412    /// Handled by some other interpreter/conversion system
413    MockCustomForwarded,
414}
415
416/// The executor context.
417/// Cloning will return another handle to the same engine connection/session,
418/// as this uses `Arc` under the hood.
419#[derive(Debug, Clone)]
420pub struct ExecutorContext {
421    pub engine: Arc<Box<dyn EngineManager>>,
422    pub fs: Arc<FileManager>,
423    pub settings: ExecutorSettings,
424    pub context_type: ContextType,
425}
426
427/// The executor settings.
428#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
429#[ts(export)]
430pub struct ExecutorSettings {
431    /// Highlight edges of 3D objects?
432    pub highlight_edges: bool,
433    /// Whether or not Screen Space Ambient Occlusion (SSAO) is enabled.
434    pub enable_ssao: bool,
435    /// Show grid?
436    pub show_grid: bool,
437    /// Should engine store this for replay?
438    /// If so, under what name?
439    pub replay: Option<String>,
440    /// The directory of the current project.  This is used for resolving import
441    /// paths.  If None is given, the current working directory is used.
442    pub project_directory: Option<TypedPath>,
443    /// This is the path to the current file being executed.
444    /// We use this for preventing cyclic imports.
445    pub current_file: Option<TypedPath>,
446    /// Whether or not to automatically scale the grid when user zooms.
447    pub fixed_size_grid: bool,
448}
449
450impl Default for ExecutorSettings {
451    fn default() -> Self {
452        Self {
453            highlight_edges: true,
454            enable_ssao: false,
455            show_grid: false,
456            replay: None,
457            project_directory: None,
458            current_file: None,
459            fixed_size_grid: true,
460        }
461    }
462}
463
464impl From<crate::settings::types::Configuration> for ExecutorSettings {
465    fn from(config: crate::settings::types::Configuration) -> Self {
466        Self::from(config.settings)
467    }
468}
469
470impl From<crate::settings::types::Settings> for ExecutorSettings {
471    fn from(settings: crate::settings::types::Settings) -> Self {
472        Self {
473            highlight_edges: settings.modeling.highlight_edges.into(),
474            enable_ssao: settings.modeling.enable_ssao.into(),
475            show_grid: settings.modeling.show_scale_grid,
476            replay: None,
477            project_directory: None,
478            current_file: None,
479            fixed_size_grid: settings.modeling.fixed_size_grid,
480        }
481    }
482}
483
484impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
485    fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
486        Self::from(config.settings.modeling)
487    }
488}
489
490impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
491    fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
492        Self {
493            highlight_edges: modeling.highlight_edges.into(),
494            enable_ssao: modeling.enable_ssao.into(),
495            show_grid: modeling.show_scale_grid,
496            replay: None,
497            project_directory: None,
498            current_file: None,
499            fixed_size_grid: true,
500        }
501    }
502}
503
504impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
505    fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
506        Self {
507            highlight_edges: modeling.highlight_edges.into(),
508            enable_ssao: modeling.enable_ssao.into(),
509            show_grid: Default::default(),
510            replay: None,
511            project_directory: None,
512            current_file: None,
513            fixed_size_grid: true,
514        }
515    }
516}
517
518impl ExecutorSettings {
519    /// Add the current file path to the executor settings.
520    pub fn with_current_file(&mut self, current_file: TypedPath) {
521        // We want the parent directory of the file.
522        if current_file.extension() == Some("kcl") {
523            self.current_file = Some(current_file.clone());
524            // Get the parent directory.
525            if let Some(parent) = current_file.parent() {
526                self.project_directory = Some(parent);
527            } else {
528                self.project_directory = Some(TypedPath::from(""));
529            }
530        } else {
531            self.project_directory = Some(current_file);
532        }
533    }
534}
535
536impl ExecutorContext {
537    /// Create a new default executor context.
538    #[cfg(not(target_arch = "wasm32"))]
539    pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
540        let (ws, _headers) = client
541            .modeling()
542            .commands_ws(
543                None,
544                None,
545                None,
546                if settings.enable_ssao {
547                    Some(kittycad::types::PostEffectType::Ssao)
548                } else {
549                    None
550                },
551                settings.replay.clone(),
552                if settings.show_grid { Some(true) } else { None },
553                None,
554                None,
555                None,
556                Some(false),
557            )
558            .await?;
559
560        let engine: Arc<Box<dyn EngineManager>> =
561            Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
562
563        Ok(Self {
564            engine,
565            fs: Arc::new(FileManager::new()),
566            settings,
567            context_type: ContextType::Live,
568        })
569    }
570
571    #[cfg(target_arch = "wasm32")]
572    pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
573        ExecutorContext {
574            engine,
575            fs,
576            settings,
577            context_type: ContextType::Live,
578        }
579    }
580
581    #[cfg(not(target_arch = "wasm32"))]
582    pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
583        ExecutorContext {
584            engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
585            fs: Arc::new(FileManager::new()),
586            settings: settings.unwrap_or_default(),
587            context_type: ContextType::Mock,
588        }
589    }
590
591    #[cfg(target_arch = "wasm32")]
592    pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
593        ExecutorContext {
594            engine,
595            fs,
596            settings,
597            context_type: ContextType::Mock,
598        }
599    }
600
601    #[cfg(not(target_arch = "wasm32"))]
602    pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
603        ExecutorContext {
604            engine,
605            fs: Arc::new(FileManager::new()),
606            settings: Default::default(),
607            context_type: ContextType::MockCustomForwarded,
608        }
609    }
610
611    /// Create a new default executor context.
612    /// With a kittycad client.
613    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
614    /// variables.
615    /// But also allows for passing in a token and engine address directly.
616    #[cfg(not(target_arch = "wasm32"))]
617    pub async fn new_with_client(
618        settings: ExecutorSettings,
619        token: Option<String>,
620        engine_addr: Option<String>,
621    ) -> Result<Self> {
622        // Create the client.
623        let client = crate::engine::new_zoo_client(token, engine_addr)?;
624
625        let ctx = Self::new(&client, settings).await?;
626        Ok(ctx)
627    }
628
629    /// Create a new default executor context.
630    /// With the default kittycad client.
631    /// This allows for passing in `ZOO_API_TOKEN` and `ZOO_HOST` as environment
632    /// variables.
633    #[cfg(not(target_arch = "wasm32"))]
634    pub async fn new_with_default_client() -> Result<Self> {
635        // Create the client.
636        let ctx = Self::new_with_client(Default::default(), None, None).await?;
637        Ok(ctx)
638    }
639
640    /// For executing unit tests.
641    #[cfg(not(target_arch = "wasm32"))]
642    pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
643        let ctx = ExecutorContext::new_with_client(
644            ExecutorSettings {
645                highlight_edges: true,
646                enable_ssao: false,
647                show_grid: false,
648                replay: None,
649                project_directory: None,
650                current_file: None,
651                fixed_size_grid: false,
652            },
653            None,
654            engine_addr,
655        )
656        .await?;
657        Ok(ctx)
658    }
659
660    pub fn is_mock(&self) -> bool {
661        self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
662    }
663
664    /// Returns true if we should not send engine commands for any reason.
665    pub async fn no_engine_commands(&self) -> bool {
666        self.is_mock()
667    }
668
669    pub async fn send_clear_scene(
670        &self,
671        exec_state: &mut ExecState,
672        source_range: crate::execution::SourceRange,
673    ) -> Result<(), KclError> {
674        // Ensure artifacts are cleared so that we don't accumulate them across
675        // runs.
676        exec_state.mod_local.artifacts.clear();
677        exec_state.global.root_module_artifacts.clear();
678        exec_state.global.artifacts.clear();
679
680        self.engine
681            .clear_scene(&mut exec_state.mod_local.id_generator, source_range)
682            .await
683    }
684
685    pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
686        cache::bust_cache().await;
687
688        // Execute an empty program to clear and reset the scene.
689        // We specifically want to be returned the objects after the scene is reset.
690        // Like the default planes so it is easier to just execute an empty program
691        // after the cache is busted.
692        let outcome = self.run_with_caching(crate::Program::empty()).await?;
693
694        Ok(outcome)
695    }
696
697    async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
698        self.eval_prelude(exec_state, SourceRange::synthetic())
699            .await
700            .map_err(KclErrorWithOutputs::no_outputs)?;
701        exec_state.mut_stack().push_new_root_env(true);
702        Ok(())
703    }
704
705    pub async fn run_mock(
706        &self,
707        program: &crate::Program,
708        mock_config: &MockConfig,
709    ) -> Result<ExecOutcome, KclErrorWithOutputs> {
710        assert!(
711            self.is_mock(),
712            "To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
713        );
714
715        let use_prev_memory = mock_config.use_prev_memory;
716        #[cfg(not(feature = "artifact-graph"))]
717        let mut exec_state = ExecState::new(self);
718        #[cfg(feature = "artifact-graph")]
719        let mut exec_state = ExecState::new_sketch_mode(self, mock_config);
720        if use_prev_memory {
721            match cache::read_old_memory().await {
722                Some(mem) => {
723                    *exec_state.mut_stack() = mem.0;
724                    exec_state.global.module_infos = mem.1;
725                }
726                None => self.prepare_mem(&mut exec_state).await?,
727            }
728        } else {
729            self.prepare_mem(&mut exec_state).await?
730        };
731
732        // Push a scope so that old variables can be overwritten (since we might be re-executing some
733        // part of the scene).
734        exec_state.mut_stack().push_new_env_for_scope();
735
736        let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
737
738        // Restore any temporary variables, then save any newly created variables back to
739        // memory in case another run wants to use them. Note this is just saved to the preserved
740        // memory, not to the exec_state which is not cached for mock execution.
741
742        let mut mem = exec_state.stack().clone();
743        let module_infos = exec_state.global.module_infos.clone();
744        let outcome = exec_state.into_exec_outcome(result.0, self).await;
745
746        mem.squash_env(result.0);
747        cache::write_old_memory((mem, module_infos)).await;
748
749        Ok(outcome)
750    }
751
752    pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
753        assert!(!self.is_mock());
754        let grid_scale = if self.settings.fixed_size_grid {
755            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
756        } else {
757            GridScaleBehavior::ScaleWithZoom
758        };
759
760        let original_program = program.clone();
761
762        let (_program, exec_state, result) = match cache::read_old_ast().await {
763            Some(mut cached_state) => {
764                let old = CacheInformation {
765                    ast: &cached_state.main.ast,
766                    settings: &cached_state.settings,
767                };
768                let new = CacheInformation {
769                    ast: &program.ast,
770                    settings: &self.settings,
771                };
772
773                // Get the program that actually changed from the old and new information.
774                let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
775                    CacheResult::ReExecute {
776                        clear_scene,
777                        reapply_settings,
778                        program: changed_program,
779                    } => {
780                        if reapply_settings
781                            && self
782                                .engine
783                                .reapply_settings(
784                                    &self.settings,
785                                    Default::default(),
786                                    &mut cached_state.main.exec_state.id_generator,
787                                    grid_scale,
788                                )
789                                .await
790                                .is_err()
791                        {
792                            (true, program, None)
793                        } else {
794                            (
795                                clear_scene,
796                                crate::Program {
797                                    ast: changed_program,
798                                    original_file_contents: program.original_file_contents,
799                                },
800                                None,
801                            )
802                        }
803                    }
804                    CacheResult::CheckImportsOnly {
805                        reapply_settings,
806                        ast: changed_program,
807                    } => {
808                        let mut reapply_failed = false;
809                        if reapply_settings {
810                            if self
811                                .engine
812                                .reapply_settings(
813                                    &self.settings,
814                                    Default::default(),
815                                    &mut cached_state.main.exec_state.id_generator,
816                                    grid_scale,
817                                )
818                                .await
819                                .is_ok()
820                            {
821                                cache::write_old_ast(GlobalState::with_settings(
822                                    cached_state.clone(),
823                                    self.settings.clone(),
824                                ))
825                                .await;
826                            } else {
827                                reapply_failed = true;
828                            }
829                        }
830
831                        if reapply_failed {
832                            (true, program, None)
833                        } else {
834                            // We need to check our imports to see if they changed.
835                            let mut new_exec_state = ExecState::new(self);
836                            let (new_universe, new_universe_map) =
837                                self.get_universe(&program, &mut new_exec_state).await?;
838
839                            let clear_scene = new_universe.values().any(|value| {
840                                let id = value.1;
841                                match (
842                                    cached_state.exec_state.get_source(id),
843                                    new_exec_state.global.get_source(id),
844                                ) {
845                                    (Some(s0), Some(s1)) => s0.source != s1.source,
846                                    _ => false,
847                                }
848                            });
849
850                            if !clear_scene {
851                                // Return early we don't need to clear the scene.
852                                return Ok(cached_state.into_exec_outcome(self).await);
853                            }
854
855                            (
856                                true,
857                                crate::Program {
858                                    ast: changed_program,
859                                    original_file_contents: program.original_file_contents,
860                                },
861                                Some((new_universe, new_universe_map, new_exec_state)),
862                            )
863                        }
864                    }
865                    CacheResult::NoAction(true) => {
866                        if self
867                            .engine
868                            .reapply_settings(
869                                &self.settings,
870                                Default::default(),
871                                &mut cached_state.main.exec_state.id_generator,
872                                grid_scale,
873                            )
874                            .await
875                            .is_ok()
876                        {
877                            // We need to update the old ast state with the new settings!!
878                            cache::write_old_ast(GlobalState::with_settings(
879                                cached_state.clone(),
880                                self.settings.clone(),
881                            ))
882                            .await;
883
884                            return Ok(cached_state.into_exec_outcome(self).await);
885                        }
886                        (true, program, None)
887                    }
888                    CacheResult::NoAction(false) => {
889                        return Ok(cached_state.into_exec_outcome(self).await);
890                    }
891                };
892
893                let (exec_state, result) = match import_check_info {
894                    Some((new_universe, new_universe_map, mut new_exec_state)) => {
895                        // Clear the scene if the imports changed.
896                        self.send_clear_scene(&mut new_exec_state, Default::default())
897                            .await
898                            .map_err(KclErrorWithOutputs::no_outputs)?;
899
900                        let result = self
901                            .run_concurrent(
902                                &program,
903                                &mut new_exec_state,
904                                Some((new_universe, new_universe_map)),
905                                PreserveMem::Normal,
906                            )
907                            .await;
908
909                        (new_exec_state, result)
910                    }
911                    None if clear_scene => {
912                        // Pop the execution state, since we are starting fresh.
913                        let mut exec_state = cached_state.reconstitute_exec_state();
914                        exec_state.reset(self);
915
916                        self.send_clear_scene(&mut exec_state, Default::default())
917                            .await
918                            .map_err(KclErrorWithOutputs::no_outputs)?;
919
920                        let result = self
921                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
922                            .await;
923
924                        (exec_state, result)
925                    }
926                    None => {
927                        let mut exec_state = cached_state.reconstitute_exec_state();
928                        exec_state.mut_stack().restore_env(cached_state.main.result_env);
929
930                        let result = self
931                            .run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
932                            .await;
933
934                        (exec_state, result)
935                    }
936                };
937
938                (program, exec_state, result)
939            }
940            None => {
941                let mut exec_state = ExecState::new(self);
942                self.send_clear_scene(&mut exec_state, Default::default())
943                    .await
944                    .map_err(KclErrorWithOutputs::no_outputs)?;
945
946                let result = self
947                    .run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
948                    .await;
949
950                (program, exec_state, result)
951            }
952        };
953
954        if result.is_err() {
955            cache::bust_cache().await;
956        }
957
958        // Throw the error.
959        let result = result?;
960
961        // Save this as the last successful execution to the cache.
962        // Gotcha: `CacheResult::ReExecute.program` may be diff-based, do not save that AST
963        // the last-successful AST. Instead, save in the full AST passed in.
964        cache::write_old_ast(GlobalState::new(
965            exec_state.clone(),
966            self.settings.clone(),
967            original_program.ast,
968            result.0,
969        ))
970        .await;
971
972        let outcome = exec_state.into_exec_outcome(result.0, self).await;
973        Ok(outcome)
974    }
975
976    /// Perform the execution of a program.
977    ///
978    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
979    pub async fn run(
980        &self,
981        program: &crate::Program,
982        exec_state: &mut ExecState,
983    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
984        self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
985            .await
986    }
987
988    /// Perform the execution of a program using a concurrent
989    /// execution model.
990    ///
991    /// To access non-fatal errors and warnings, extract them from the `ExecState`.
992    pub async fn run_concurrent(
993        &self,
994        program: &crate::Program,
995        exec_state: &mut ExecState,
996        universe_info: Option<(Universe, UniverseMap)>,
997        preserve_mem: PreserveMem,
998    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
999        // Reuse our cached universe if we have one.
1000
1001        let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
1002            (universe, universe_map)
1003        } else {
1004            self.get_universe(program, exec_state).await?
1005        };
1006
1007        let default_planes = self.engine.get_default_planes().read().await.clone();
1008
1009        // Run the prelude to set up the engine.
1010        self.eval_prelude(exec_state, SourceRange::synthetic())
1011            .await
1012            .map_err(KclErrorWithOutputs::no_outputs)?;
1013
1014        for modules in import_graph::import_graph(&universe, self)
1015            .map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
1016            .into_iter()
1017        {
1018            #[cfg(not(target_arch = "wasm32"))]
1019            let mut set = tokio::task::JoinSet::new();
1020
1021            #[allow(clippy::type_complexity)]
1022            let (results_tx, mut results_rx): (
1023                tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
1024                tokio::sync::mpsc::Receiver<_>,
1025            ) = tokio::sync::mpsc::channel(1);
1026
1027            for module in modules {
1028                let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
1029                    return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
1030                        KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
1031                    )));
1032                };
1033                let module_id = *module_id;
1034                let module_path = module_path.clone();
1035                let source_range = SourceRange::from(import_stmt);
1036                // Clone before mutating.
1037                let module_exec_state = exec_state.clone();
1038
1039                self.add_import_module_ops(
1040                    exec_state,
1041                    &program.ast,
1042                    module_id,
1043                    &module_path,
1044                    source_range,
1045                    &universe_map,
1046                );
1047
1048                let repr = repr.clone();
1049                let exec_ctxt = self.clone();
1050                let results_tx = results_tx.clone();
1051
1052                let exec_module = async |exec_ctxt: &ExecutorContext,
1053                                         repr: &ModuleRepr,
1054                                         module_id: ModuleId,
1055                                         module_path: &ModulePath,
1056                                         exec_state: &mut ExecState,
1057                                         source_range: SourceRange|
1058                       -> Result<ModuleRepr, KclError> {
1059                    match repr {
1060                        ModuleRepr::Kcl(program, _) => {
1061                            let result = exec_ctxt
1062                                .exec_module_from_ast(
1063                                    program,
1064                                    module_id,
1065                                    module_path,
1066                                    exec_state,
1067                                    source_range,
1068                                    PreserveMem::Normal,
1069                                )
1070                                .await;
1071
1072                            result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
1073                        }
1074                        ModuleRepr::Foreign(geom, _) => {
1075                            let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
1076                                .await
1077                                .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1078
1079                            result.map(|val| {
1080                                ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
1081                            })
1082                        }
1083                        ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
1084                            format!("Module {module_path} not found in universe"),
1085                            vec![source_range],
1086                        ))),
1087                    }
1088                };
1089
1090                #[cfg(target_arch = "wasm32")]
1091                {
1092                    wasm_bindgen_futures::spawn_local(async move {
1093                        let mut exec_state = module_exec_state;
1094                        let exec_ctxt = exec_ctxt;
1095
1096                        let result = exec_module(
1097                            &exec_ctxt,
1098                            &repr,
1099                            module_id,
1100                            &module_path,
1101                            &mut exec_state,
1102                            source_range,
1103                        )
1104                        .await;
1105
1106                        results_tx
1107                            .send((module_id, module_path, result))
1108                            .await
1109                            .unwrap_or_default();
1110                    });
1111                }
1112                #[cfg(not(target_arch = "wasm32"))]
1113                {
1114                    set.spawn(async move {
1115                        let mut exec_state = module_exec_state;
1116                        let exec_ctxt = exec_ctxt;
1117
1118                        let result = exec_module(
1119                            &exec_ctxt,
1120                            &repr,
1121                            module_id,
1122                            &module_path,
1123                            &mut exec_state,
1124                            source_range,
1125                        )
1126                        .await;
1127
1128                        results_tx
1129                            .send((module_id, module_path, result))
1130                            .await
1131                            .unwrap_or_default();
1132                    });
1133                }
1134            }
1135
1136            drop(results_tx);
1137
1138            while let Some((module_id, _, result)) = results_rx.recv().await {
1139                match result {
1140                    Ok(new_repr) => {
1141                        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1142
1143                        match &mut repr {
1144                            ModuleRepr::Kcl(_, cache) => {
1145                                let ModuleRepr::Kcl(_, session_data) = new_repr else {
1146                                    unreachable!();
1147                                };
1148                                *cache = session_data;
1149                            }
1150                            ModuleRepr::Foreign(_, cache) => {
1151                                let ModuleRepr::Foreign(_, session_data) = new_repr else {
1152                                    unreachable!();
1153                                };
1154                                *cache = session_data;
1155                            }
1156                            ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
1157                        }
1158
1159                        exec_state.global.module_infos[&module_id].restore_repr(repr);
1160                    }
1161                    Err(e) => {
1162                        return Err(exec_state.error_with_outputs(e, None, default_planes));
1163                    }
1164                }
1165            }
1166        }
1167
1168        // Since we haven't technically started executing the root module yet,
1169        // the operations corresponding to the imports will be missing unless we
1170        // track them here.
1171        exec_state
1172            .global
1173            .root_module_artifacts
1174            .extend(std::mem::take(&mut exec_state.mod_local.artifacts));
1175
1176        self.inner_run(program, exec_state, preserve_mem).await
1177    }
1178
1179    /// Get the universe & universe map of the program.
1180    /// And see if any of the imports changed.
1181    async fn get_universe(
1182        &self,
1183        program: &crate::Program,
1184        exec_state: &mut ExecState,
1185    ) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
1186        exec_state.add_root_module_contents(program);
1187
1188        let mut universe = std::collections::HashMap::new();
1189
1190        let default_planes = self.engine.get_default_planes().read().await.clone();
1191
1192        let root_imports = import_graph::import_universe(
1193            self,
1194            &ModulePath::Main,
1195            &ModuleRepr::Kcl(program.ast.clone(), None),
1196            &mut universe,
1197            exec_state,
1198        )
1199        .await
1200        .map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
1201
1202        Ok((universe, root_imports))
1203    }
1204
1205    #[cfg(not(feature = "artifact-graph"))]
1206    fn add_import_module_ops(
1207        &self,
1208        _exec_state: &mut ExecState,
1209        _program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1210        _module_id: ModuleId,
1211        _module_path: &ModulePath,
1212        _source_range: SourceRange,
1213        _universe_map: &UniverseMap,
1214    ) {
1215    }
1216
1217    #[cfg(feature = "artifact-graph")]
1218    fn add_import_module_ops(
1219        &self,
1220        exec_state: &mut ExecState,
1221        program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1222        module_id: ModuleId,
1223        module_path: &ModulePath,
1224        source_range: SourceRange,
1225        universe_map: &UniverseMap,
1226    ) {
1227        match module_path {
1228            ModulePath::Main => {
1229                // This should never happen.
1230            }
1231            ModulePath::Local {
1232                value,
1233                original_import_path,
1234            } => {
1235                // We only want to display the top-level module imports in
1236                // the Feature Tree, not transitive imports.
1237                if universe_map.contains_key(value) {
1238                    use crate::NodePath;
1239
1240                    let node_path = if source_range.is_top_level_module() {
1241                        let cached_body_items = exec_state.global.artifacts.cached_body_items();
1242                        NodePath::from_range(
1243                            &exec_state.build_program_lookup(program.clone()),
1244                            cached_body_items,
1245                            source_range,
1246                        )
1247                        .unwrap_or_default()
1248                    } else {
1249                        // The frontend doesn't care about paths in
1250                        // files other than the top-level module.
1251                        NodePath::placeholder()
1252                    };
1253
1254                    let name = match original_import_path {
1255                        Some(value) => value.to_string_lossy(),
1256                        None => value.file_name().unwrap_or_default(),
1257                    };
1258                    exec_state.push_op(Operation::GroupBegin {
1259                        group: Group::ModuleInstance { name, module_id },
1260                        node_path,
1261                        source_range,
1262                    });
1263                    // Due to concurrent execution, we cannot easily
1264                    // group operations by module. So we leave the
1265                    // group empty and close it immediately.
1266                    exec_state.push_op(Operation::GroupEnd);
1267                }
1268            }
1269            ModulePath::Std { .. } => {
1270                // We don't want to display stdlib in the Feature Tree.
1271            }
1272        }
1273    }
1274
1275    /// Perform the execution of a program.  Accept all possible parameters and
1276    /// output everything.
1277    async fn inner_run(
1278        &self,
1279        program: &crate::Program,
1280        exec_state: &mut ExecState,
1281        preserve_mem: PreserveMem,
1282    ) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
1283        let _stats = crate::log::LogPerfStats::new("Interpretation");
1284
1285        // Re-apply the settings, in case the cache was busted.
1286        let grid_scale = if self.settings.fixed_size_grid {
1287            GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
1288        } else {
1289            GridScaleBehavior::ScaleWithZoom
1290        };
1291        self.engine
1292            .reapply_settings(
1293                &self.settings,
1294                Default::default(),
1295                exec_state.id_generator(),
1296                grid_scale,
1297            )
1298            .await
1299            .map_err(KclErrorWithOutputs::no_outputs)?;
1300
1301        let default_planes = self.engine.get_default_planes().read().await.clone();
1302        let result = self
1303            .execute_and_build_graph(&program.ast, exec_state, preserve_mem)
1304            .await;
1305
1306        crate::log::log(format!(
1307            "Post interpretation KCL memory stats: {:#?}",
1308            exec_state.stack().memory.stats
1309        ));
1310        crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
1311
1312        let env_ref = result.map_err(|(err, env_ref)| exec_state.error_with_outputs(err, env_ref, default_planes))?;
1313
1314        if !self.is_mock() {
1315            let mut mem = exec_state.stack().deep_clone();
1316            mem.restore_env(env_ref);
1317            cache::write_old_memory((mem, exec_state.global.module_infos.clone())).await;
1318        }
1319        let session_data = self.engine.get_session_data().await;
1320
1321        Ok((env_ref, session_data))
1322    }
1323
1324    /// Execute an AST's program and build auxiliary outputs like the artifact
1325    /// graph.
1326    async fn execute_and_build_graph(
1327        &self,
1328        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1329        exec_state: &mut ExecState,
1330        preserve_mem: PreserveMem,
1331    ) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
1332        // Don't early return!  We need to build other outputs regardless of
1333        // whether execution failed.
1334
1335        // Because of execution caching, we may start with operations from a
1336        // previous run.
1337        #[cfg(feature = "artifact-graph")]
1338        let start_op = exec_state.global.root_module_artifacts.operations.len();
1339
1340        self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
1341            .await
1342            .map_err(|e| (e, None))?;
1343
1344        let exec_result = self
1345            .exec_module_body(
1346                program,
1347                exec_state,
1348                preserve_mem,
1349                ModuleId::default(),
1350                &ModulePath::Main,
1351            )
1352            .await
1353            .map(
1354                |ModuleExecutionOutcome {
1355                     environment: env_ref,
1356                     artifacts: module_artifacts,
1357                     ..
1358                 }| {
1359                    // We need to extend because it may already have operations from
1360                    // imports.
1361                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1362                    env_ref
1363                },
1364            )
1365            .map_err(|(err, env_ref, module_artifacts)| {
1366                if let Some(module_artifacts) = module_artifacts {
1367                    // We need to extend because it may already have operations
1368                    // from imports.
1369                    exec_state.global.root_module_artifacts.extend(module_artifacts);
1370                }
1371                (err, env_ref)
1372            });
1373
1374        #[cfg(feature = "artifact-graph")]
1375        {
1376            // Fill in NodePath for operations.
1377            let programs = &exec_state.build_program_lookup(program.clone());
1378            let cached_body_items = exec_state.global.artifacts.cached_body_items();
1379            for op in exec_state
1380                .global
1381                .root_module_artifacts
1382                .operations
1383                .iter_mut()
1384                .skip(start_op)
1385            {
1386                op.fill_node_paths(programs, cached_body_items);
1387            }
1388            for module in exec_state.global.module_infos.values_mut() {
1389                if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
1390                    for op in &mut outcome.artifacts.operations {
1391                        op.fill_node_paths(programs, cached_body_items);
1392                    }
1393                }
1394            }
1395        }
1396
1397        // Ensure all the async commands completed.
1398        self.engine.ensure_async_commands_completed().await.map_err(|e| {
1399            match &exec_result {
1400                Ok(env_ref) => (e, Some(*env_ref)),
1401                // Prefer the execution error.
1402                Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
1403            }
1404        })?;
1405
1406        // If we errored out and early-returned, there might be commands which haven't been executed
1407        // and should be dropped.
1408        self.engine.clear_queues().await;
1409
1410        match exec_state.build_artifact_graph(&self.engine, program).await {
1411            Ok(_) => exec_result,
1412            Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
1413        }
1414    }
1415
1416    /// 'Import' std::prelude as the outermost scope.
1417    ///
1418    /// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
1419    async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
1420        if exec_state.stack().memory.requires_std() {
1421            #[cfg(feature = "artifact-graph")]
1422            let initial_ops = exec_state.mod_local.artifacts.operations.len();
1423
1424            let path = vec!["std".to_owned(), "prelude".to_owned()];
1425            let resolved_path = ModulePath::from_std_import_path(&path)?;
1426            let id = self
1427                .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
1428                .await?;
1429            let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
1430
1431            exec_state.mut_stack().memory.set_std(module_memory);
1432
1433            // Operations generated by the prelude are not useful, so clear them
1434            // out.
1435            //
1436            // TODO: Should we also clear them out of each module so that they
1437            // don't appear in test output?
1438            #[cfg(feature = "artifact-graph")]
1439            exec_state.mod_local.artifacts.operations.truncate(initial_ops);
1440        }
1441
1442        Ok(())
1443    }
1444
1445    /// Get a snapshot of the current scene.
1446    pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
1447        // Zoom to fit.
1448        self.engine
1449            .send_modeling_cmd(
1450                uuid::Uuid::new_v4(),
1451                crate::execution::SourceRange::default(),
1452                &ModelingCmd::from(mcmd::ZoomToFit {
1453                    object_ids: Default::default(),
1454                    animated: false,
1455                    padding: 0.1,
1456                }),
1457            )
1458            .await
1459            .map_err(KclErrorWithOutputs::no_outputs)?;
1460
1461        // Send a snapshot request to the engine.
1462        let resp = self
1463            .engine
1464            .send_modeling_cmd(
1465                uuid::Uuid::new_v4(),
1466                crate::execution::SourceRange::default(),
1467                &ModelingCmd::from(mcmd::TakeSnapshot {
1468                    format: ImageFormat::Png,
1469                }),
1470            )
1471            .await
1472            .map_err(KclErrorWithOutputs::no_outputs)?;
1473
1474        let OkWebSocketResponseData::Modeling {
1475            modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
1476        } = resp
1477        else {
1478            return Err(ExecError::BadPng(format!(
1479                "Instead of a TakeSnapshot response, the engine returned {resp:?}"
1480            )));
1481        };
1482        Ok(contents)
1483    }
1484
1485    /// Export the current scene as a CAD file.
1486    pub async fn export(
1487        &self,
1488        format: kittycad_modeling_cmds::format::OutputFormat3d,
1489    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1490        let resp = self
1491            .engine
1492            .send_modeling_cmd(
1493                uuid::Uuid::new_v4(),
1494                crate::SourceRange::default(),
1495                &kittycad_modeling_cmds::ModelingCmd::Export(kittycad_modeling_cmds::Export {
1496                    entity_ids: vec![],
1497                    format,
1498                }),
1499            )
1500            .await?;
1501
1502        let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
1503            return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
1504                format!("Expected Export response, got {resp:?}",),
1505                vec![SourceRange::default()],
1506            )));
1507        };
1508
1509        Ok(files)
1510    }
1511
1512    /// Export the current scene as a STEP file.
1513    pub async fn export_step(
1514        &self,
1515        deterministic_time: bool,
1516    ) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
1517        let files = self
1518            .export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
1519                kittycad_modeling_cmds::format::step::export::Options {
1520                    coords: *kittycad_modeling_cmds::coord::KITTYCAD,
1521                    created: if deterministic_time {
1522                        Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
1523                            KclError::new_internal(crate::errors::KclErrorDetails::new(
1524                                format!("Failed to parse date: {e}"),
1525                                vec![SourceRange::default()],
1526                            ))
1527                        })?)
1528                    } else {
1529                        None
1530                    },
1531                },
1532            ))
1533            .await?;
1534
1535        Ok(files)
1536    }
1537
1538    pub async fn close(&self) {
1539        self.engine.close().await;
1540    }
1541}
1542
1543#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
1544pub struct ArtifactId(Uuid);
1545
1546impl ArtifactId {
1547    pub fn new(uuid: Uuid) -> Self {
1548        Self(uuid)
1549    }
1550
1551    /// A placeholder artifact ID that will be filled in later.
1552    pub fn placeholder() -> Self {
1553        Self(Uuid::nil())
1554    }
1555
1556    /// The constraint artifact ID is a special. They don't need to be
1557    /// represented in the artifact graph.
1558    pub fn constraint() -> Self {
1559        Self(Uuid::nil())
1560    }
1561}
1562
1563impl From<Uuid> for ArtifactId {
1564    fn from(uuid: Uuid) -> Self {
1565        Self::new(uuid)
1566    }
1567}
1568
1569impl From<&Uuid> for ArtifactId {
1570    fn from(uuid: &Uuid) -> Self {
1571        Self::new(*uuid)
1572    }
1573}
1574
1575impl From<ArtifactId> for Uuid {
1576    fn from(id: ArtifactId) -> Self {
1577        id.0
1578    }
1579}
1580
1581impl From<&ArtifactId> for Uuid {
1582    fn from(id: &ArtifactId) -> Self {
1583        id.0
1584    }
1585}
1586
1587impl From<ModelingCmdId> for ArtifactId {
1588    fn from(id: ModelingCmdId) -> Self {
1589        Self::new(*id.as_ref())
1590    }
1591}
1592
1593impl From<&ModelingCmdId> for ArtifactId {
1594    fn from(id: &ModelingCmdId) -> Self {
1595        Self::new(*id.as_ref())
1596    }
1597}
1598
1599#[cfg(test)]
1600pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
1601    parse_execute_with_project_dir(code, None).await
1602}
1603
1604#[cfg(test)]
1605pub(crate) async fn parse_execute_with_project_dir(
1606    code: &str,
1607    project_directory: Option<TypedPath>,
1608) -> Result<ExecTestResults, KclError> {
1609    let program = crate::Program::parse_no_errs(code)?;
1610
1611    let exec_ctxt = ExecutorContext {
1612        engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
1613            |err| {
1614                KclError::new_internal(crate::errors::KclErrorDetails::new(
1615                    format!("Failed to create mock engine connection: {err}"),
1616                    vec![SourceRange::default()],
1617                ))
1618            },
1619        )?)),
1620        fs: Arc::new(crate::fs::FileManager::new()),
1621        settings: ExecutorSettings {
1622            project_directory,
1623            ..Default::default()
1624        },
1625        context_type: ContextType::Mock,
1626    };
1627    let mut exec_state = ExecState::new(&exec_ctxt);
1628    let result = exec_ctxt.run(&program, &mut exec_state).await?;
1629
1630    Ok(ExecTestResults {
1631        program,
1632        mem_env: result.0,
1633        exec_ctxt,
1634        exec_state,
1635    })
1636}
1637
1638#[cfg(test)]
1639#[derive(Debug)]
1640pub(crate) struct ExecTestResults {
1641    program: crate::Program,
1642    mem_env: EnvironmentRef,
1643    exec_ctxt: ExecutorContext,
1644    exec_state: ExecState,
1645}
1646
1647/// There are several places where we want to traverse a KCL program or find a symbol in it,
1648/// but because KCL modules can import each other, we need to traverse multiple programs.
1649/// This stores multiple programs, keyed by their module ID for quick access.
1650#[cfg(feature = "artifact-graph")]
1651pub struct ProgramLookup {
1652    programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
1653}
1654
1655#[cfg(feature = "artifact-graph")]
1656impl ProgramLookup {
1657    // TODO: Could this store a reference to KCL programs instead of owning them?
1658    // i.e. take &state::ModuleInfoMap instead?
1659    pub fn new(
1660        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1661        module_infos: state::ModuleInfoMap,
1662    ) -> Self {
1663        let mut programs = IndexMap::with_capacity(module_infos.len());
1664        for (id, info) in module_infos {
1665            if let ModuleRepr::Kcl(program, _) = info.repr {
1666                programs.insert(id, program);
1667            }
1668        }
1669        programs.insert(ModuleId::default(), current);
1670        Self { programs }
1671    }
1672
1673    pub fn program_for_module(
1674        &self,
1675        module_id: ModuleId,
1676    ) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
1677        self.programs.get(&module_id)
1678    }
1679}
1680
1681#[cfg(test)]
1682mod tests {
1683    use pretty_assertions::assert_eq;
1684
1685    use super::*;
1686    use crate::{
1687        ModuleId,
1688        errors::{KclErrorDetails, Severity},
1689        exec::NumericType,
1690        execution::{memory::Stack, types::RuntimeType},
1691    };
1692
1693    /// Convenience function to get a JSON value from memory and unwrap.
1694    #[track_caller]
1695    fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
1696        memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
1697    }
1698
1699    #[tokio::test(flavor = "multi_thread")]
1700    async fn test_execute_warn() {
1701        let text = "@blah";
1702        let result = parse_execute(text).await.unwrap();
1703        let errs = result.exec_state.errors();
1704        assert_eq!(errs.len(), 1);
1705        assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
1706        assert!(
1707            errs[0].message.contains("Unknown annotation"),
1708            "unexpected warning message: {}",
1709            errs[0].message
1710        );
1711    }
1712
1713    #[tokio::test(flavor = "multi_thread")]
1714    async fn test_execute_fn_definitions() {
1715        let ast = r#"fn def(@x) {
1716  return x
1717}
1718fn ghi(@x) {
1719  return x
1720}
1721fn jkl(@x) {
1722  return x
1723}
1724fn hmm(@x) {
1725  return x
1726}
1727
1728yo = 5 + 6
1729
1730abc = 3
1731identifierGuy = 5
1732part001 = startSketchOn(XY)
1733|> startProfile(at = [-1.2, 4.83])
1734|> line(end = [2.8, 0])
1735|> angledLine(angle = 100 + 100, length = 3.01)
1736|> angledLine(angle = abc, length = 3.02)
1737|> angledLine(angle = def(yo), length = 3.03)
1738|> angledLine(angle = ghi(2), length = 3.04)
1739|> angledLine(angle = jkl(yo) + 2, length = 3.05)
1740|> close()
1741yo2 = hmm([identifierGuy + 5])"#;
1742
1743        parse_execute(ast).await.unwrap();
1744    }
1745
1746    #[tokio::test(flavor = "multi_thread")]
1747    async fn test_execute_with_pipe_substitutions_unary() {
1748        let ast = r#"myVar = 3
1749part001 = startSketchOn(XY)
1750  |> startProfile(at = [0, 0])
1751  |> line(end = [3, 4], tag = $seg01)
1752  |> line(end = [
1753  min([segLen(seg01), myVar]),
1754  -legLen(hypotenuse = segLen(seg01), leg = myVar)
1755])
1756"#;
1757
1758        parse_execute(ast).await.unwrap();
1759    }
1760
1761    #[tokio::test(flavor = "multi_thread")]
1762    async fn test_execute_with_pipe_substitutions() {
1763        let ast = r#"myVar = 3
1764part001 = startSketchOn(XY)
1765  |> startProfile(at = [0, 0])
1766  |> line(end = [3, 4], tag = $seg01)
1767  |> line(end = [
1768  min([segLen(seg01), myVar]),
1769  legLen(hypotenuse = segLen(seg01), leg = myVar)
1770])
1771"#;
1772
1773        parse_execute(ast).await.unwrap();
1774    }
1775
1776    #[tokio::test(flavor = "multi_thread")]
1777    async fn test_execute_with_inline_comment() {
1778        let ast = r#"baseThick = 1
1779armAngle = 60
1780
1781baseThickHalf = baseThick / 2
1782halfArmAngle = armAngle / 2
1783
1784arrExpShouldNotBeIncluded = [1, 2, 3]
1785objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
1786
1787part001 = startSketchOn(XY)
1788  |> startProfile(at = [0, 0])
1789  |> yLine(endAbsolute = 1)
1790  |> xLine(length = 3.84) // selection-range-7ish-before-this
1791
1792variableBelowShouldNotBeIncluded = 3
1793"#;
1794
1795        parse_execute(ast).await.unwrap();
1796    }
1797
1798    #[tokio::test(flavor = "multi_thread")]
1799    async fn test_execute_with_function_literal_in_pipe() {
1800        let ast = r#"w = 20
1801l = 8
1802h = 10
1803
1804fn thing() {
1805  return -8
1806}
1807
1808firstExtrude = startSketchOn(XY)
1809  |> startProfile(at = [0,0])
1810  |> line(end = [0, l])
1811  |> line(end = [w, 0])
1812  |> line(end = [0, thing()])
1813  |> close()
1814  |> extrude(length = h)"#;
1815
1816        parse_execute(ast).await.unwrap();
1817    }
1818
1819    #[tokio::test(flavor = "multi_thread")]
1820    async fn test_execute_with_function_unary_in_pipe() {
1821        let ast = r#"w = 20
1822l = 8
1823h = 10
1824
1825fn thing(@x) {
1826  return -x
1827}
1828
1829firstExtrude = startSketchOn(XY)
1830  |> startProfile(at = [0,0])
1831  |> line(end = [0, l])
1832  |> line(end = [w, 0])
1833  |> line(end = [0, thing(8)])
1834  |> close()
1835  |> extrude(length = h)"#;
1836
1837        parse_execute(ast).await.unwrap();
1838    }
1839
1840    #[tokio::test(flavor = "multi_thread")]
1841    async fn test_execute_with_function_array_in_pipe() {
1842        let ast = r#"w = 20
1843l = 8
1844h = 10
1845
1846fn thing(@x) {
1847  return [0, -x]
1848}
1849
1850firstExtrude = startSketchOn(XY)
1851  |> startProfile(at = [0,0])
1852  |> line(end = [0, l])
1853  |> line(end = [w, 0])
1854  |> line(end = thing(8))
1855  |> close()
1856  |> extrude(length = h)"#;
1857
1858        parse_execute(ast).await.unwrap();
1859    }
1860
1861    #[tokio::test(flavor = "multi_thread")]
1862    async fn test_execute_with_function_call_in_pipe() {
1863        let ast = r#"w = 20
1864l = 8
1865h = 10
1866
1867fn other_thing(@y) {
1868  return -y
1869}
1870
1871fn thing(@x) {
1872  return other_thing(x)
1873}
1874
1875firstExtrude = startSketchOn(XY)
1876  |> startProfile(at = [0,0])
1877  |> line(end = [0, l])
1878  |> line(end = [w, 0])
1879  |> line(end = [0, thing(8)])
1880  |> close()
1881  |> extrude(length = h)"#;
1882
1883        parse_execute(ast).await.unwrap();
1884    }
1885
1886    #[tokio::test(flavor = "multi_thread")]
1887    async fn test_execute_with_function_sketch() {
1888        let ast = r#"fn box(h, l, w) {
1889 myBox = startSketchOn(XY)
1890    |> startProfile(at = [0,0])
1891    |> line(end = [0, l])
1892    |> line(end = [w, 0])
1893    |> line(end = [0, -l])
1894    |> close()
1895    |> extrude(length = h)
1896
1897  return myBox
1898}
1899
1900fnBox = box(h = 3, l = 6, w = 10)"#;
1901
1902        parse_execute(ast).await.unwrap();
1903    }
1904
1905    #[tokio::test(flavor = "multi_thread")]
1906    async fn test_get_member_of_object_with_function_period() {
1907        let ast = r#"fn box(@obj) {
1908 myBox = startSketchOn(XY)
1909    |> startProfile(at = obj.start)
1910    |> line(end = [0, obj.l])
1911    |> line(end = [obj.w, 0])
1912    |> line(end = [0, -obj.l])
1913    |> close()
1914    |> extrude(length = obj.h)
1915
1916  return myBox
1917}
1918
1919thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
1920"#;
1921        parse_execute(ast).await.unwrap();
1922    }
1923
1924    #[tokio::test(flavor = "multi_thread")]
1925    #[ignore] // https://github.com/KittyCAD/modeling-app/issues/3338
1926    async fn test_object_member_starting_pipeline() {
1927        let ast = r#"
1928fn test2() {
1929  return {
1930    thing: startSketchOn(XY)
1931      |> startProfile(at = [0, 0])
1932      |> line(end = [0, 1])
1933      |> line(end = [1, 0])
1934      |> line(end = [0, -1])
1935      |> close()
1936  }
1937}
1938
1939x2 = test2()
1940
1941x2.thing
1942  |> extrude(length = 10)
1943"#;
1944        parse_execute(ast).await.unwrap();
1945    }
1946
1947    #[tokio::test(flavor = "multi_thread")]
1948    #[ignore] // ignore til we get loops
1949    async fn test_execute_with_function_sketch_loop_objects() {
1950        let ast = r#"fn box(obj) {
1951let myBox = startSketchOn(XY)
1952    |> startProfile(at = obj.start)
1953    |> line(end = [0, obj.l])
1954    |> line(end = [obj.w, 0])
1955    |> line(end = [0, -obj.l])
1956    |> close()
1957    |> extrude(length = obj.h)
1958
1959  return myBox
1960}
1961
1962for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
1963  thisBox = box(var)
1964}"#;
1965
1966        parse_execute(ast).await.unwrap();
1967    }
1968
1969    #[tokio::test(flavor = "multi_thread")]
1970    #[ignore] // ignore til we get loops
1971    async fn test_execute_with_function_sketch_loop_array() {
1972        let ast = r#"fn box(h, l, w, start) {
1973 myBox = startSketchOn(XY)
1974    |> startProfile(at = [0,0])
1975    |> line(end = [0, l])
1976    |> line(end = [w, 0])
1977    |> line(end = [0, -l])
1978    |> close()
1979    |> extrude(length = h)
1980
1981  return myBox
1982}
1983
1984
1985for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
1986  const thisBox = box(var[0], var[1], var[2], var[3])
1987}"#;
1988
1989        parse_execute(ast).await.unwrap();
1990    }
1991
1992    #[tokio::test(flavor = "multi_thread")]
1993    async fn test_get_member_of_array_with_function() {
1994        let ast = r#"fn box(@arr) {
1995 myBox =startSketchOn(XY)
1996    |> startProfile(at = arr[0])
1997    |> line(end = [0, arr[1]])
1998    |> line(end = [arr[2], 0])
1999    |> line(end = [0, -arr[1]])
2000    |> close()
2001    |> extrude(length = arr[3])
2002
2003  return myBox
2004}
2005
2006thisBox = box([[0,0], 6, 10, 3])
2007
2008"#;
2009        parse_execute(ast).await.unwrap();
2010    }
2011
2012    #[tokio::test(flavor = "multi_thread")]
2013    async fn test_function_cannot_access_future_definitions() {
2014        let ast = r#"
2015fn returnX() {
2016  // x shouldn't be defined yet.
2017  return x
2018}
2019
2020x = 5
2021
2022answer = returnX()"#;
2023
2024        let result = parse_execute(ast).await;
2025        let err = result.unwrap_err();
2026        assert_eq!(err.message(), "`x` is not defined");
2027    }
2028
2029    #[tokio::test(flavor = "multi_thread")]
2030    async fn test_override_prelude() {
2031        let text = "PI = 3.0";
2032        let result = parse_execute(text).await.unwrap();
2033        let errs = result.exec_state.errors();
2034        assert!(errs.is_empty());
2035    }
2036
2037    #[tokio::test(flavor = "multi_thread")]
2038    async fn type_aliases() {
2039        let text = r#"@settings(experimentalFeatures = allow)
2040type MyTy = [number; 2]
2041fn foo(@x: MyTy) {
2042    return x[0]
2043}
2044
2045foo([0, 1])
2046
2047type Other = MyTy | Helix
2048"#;
2049        let result = parse_execute(text).await.unwrap();
2050        let errs = result.exec_state.errors();
2051        assert!(errs.is_empty());
2052    }
2053
2054    #[tokio::test(flavor = "multi_thread")]
2055    async fn test_cannot_shebang_in_fn() {
2056        let ast = r#"
2057fn foo() {
2058  #!hello
2059  return true
2060}
2061
2062foo
2063"#;
2064
2065        let result = parse_execute(ast).await;
2066        let err = result.unwrap_err();
2067        assert_eq!(
2068            err,
2069            KclError::new_syntax(KclErrorDetails::new(
2070                "Unexpected token: #".to_owned(),
2071                vec![SourceRange::new(14, 15, ModuleId::default())],
2072            )),
2073        );
2074    }
2075
2076    #[tokio::test(flavor = "multi_thread")]
2077    async fn test_pattern_transform_function_cannot_access_future_definitions() {
2078        let ast = r#"
2079fn transform(@replicaId) {
2080  // x shouldn't be defined yet.
2081  scale = x
2082  return {
2083    translate = [0, 0, replicaId * 10],
2084    scale = [scale, 1, 0],
2085  }
2086}
2087
2088fn layer() {
2089  return startSketchOn(XY)
2090    |> circle( center= [0, 0], radius= 1, tag = $tag1)
2091    |> extrude(length = 10)
2092}
2093
2094x = 5
2095
2096// The 10 layers are replicas of each other, with a transform applied to each.
2097shape = layer() |> patternTransform(instances = 10, transform = transform)
2098"#;
2099
2100        let result = parse_execute(ast).await;
2101        let err = result.unwrap_err();
2102        assert_eq!(err.message(), "`x` is not defined",);
2103    }
2104
2105    // ADAM: Move some of these into simulation tests.
2106
2107    #[tokio::test(flavor = "multi_thread")]
2108    async fn test_math_execute_with_functions() {
2109        let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
2110        let result = parse_execute(ast).await.unwrap();
2111        assert_eq!(
2112            5.0,
2113            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2114                .as_f64()
2115                .unwrap()
2116        );
2117    }
2118
2119    #[tokio::test(flavor = "multi_thread")]
2120    async fn test_math_execute() {
2121        let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
2122        let result = parse_execute(ast).await.unwrap();
2123        assert_eq!(
2124            7.4,
2125            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2126                .as_f64()
2127                .unwrap()
2128        );
2129    }
2130
2131    #[tokio::test(flavor = "multi_thread")]
2132    async fn test_math_execute_start_negative() {
2133        let ast = r#"myVar = -5 + 6"#;
2134        let result = parse_execute(ast).await.unwrap();
2135        assert_eq!(
2136            1.0,
2137            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2138                .as_f64()
2139                .unwrap()
2140        );
2141    }
2142
2143    #[tokio::test(flavor = "multi_thread")]
2144    async fn test_math_execute_with_pi() {
2145        let ast = r#"myVar = PI * 2"#;
2146        let result = parse_execute(ast).await.unwrap();
2147        assert_eq!(
2148            std::f64::consts::TAU,
2149            mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
2150                .as_f64()
2151                .unwrap()
2152        );
2153    }
2154
2155    #[tokio::test(flavor = "multi_thread")]
2156    async fn test_math_define_decimal_without_leading_zero() {
2157        let ast = r#"thing = .4 + 7"#;
2158        let result = parse_execute(ast).await.unwrap();
2159        assert_eq!(
2160            7.4,
2161            mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
2162                .as_f64()
2163                .unwrap()
2164        );
2165    }
2166
2167    #[tokio::test(flavor = "multi_thread")]
2168    async fn pass_std_to_std() {
2169        let ast = r#"sketch001 = startSketchOn(XY)
2170profile001 = circle(sketch001, center = [0, 0], radius = 2)
2171extrude001 = extrude(profile001, length = 5)
2172extrudes = patternLinear3d(
2173  extrude001,
2174  instances = 3,
2175  distance = 5,
2176  axis = [1, 1, 0],
2177)
2178clone001 = map(extrudes, f = clone)
2179"#;
2180        parse_execute(ast).await.unwrap();
2181    }
2182
2183    #[tokio::test(flavor = "multi_thread")]
2184    async fn test_array_reduce_nested_array() {
2185        let code = r#"
2186fn id(@el, accum)  { return accum }
2187
2188answer = reduce([], initial=[[[0,0]]], f=id)
2189"#;
2190        let result = parse_execute(code).await.unwrap();
2191        assert_eq!(
2192            mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
2193            KclValue::HomArray {
2194                value: vec![KclValue::HomArray {
2195                    value: vec![KclValue::HomArray {
2196                        value: vec![
2197                            KclValue::Number {
2198                                value: 0.0,
2199                                ty: NumericType::default(),
2200                                meta: vec![SourceRange::new(69, 70, Default::default()).into()],
2201                            },
2202                            KclValue::Number {
2203                                value: 0.0,
2204                                ty: NumericType::default(),
2205                                meta: vec![SourceRange::new(71, 72, Default::default()).into()],
2206                            }
2207                        ],
2208                        ty: RuntimeType::any(),
2209                    }],
2210                    ty: RuntimeType::any(),
2211                }],
2212                ty: RuntimeType::any(),
2213            }
2214        );
2215    }
2216
2217    #[tokio::test(flavor = "multi_thread")]
2218    async fn test_zero_param_fn() {
2219        let ast = r#"sigmaAllow = 35000 // psi
2220leg1 = 5 // inches
2221leg2 = 8 // inches
2222fn thickness() { return 0.56 }
2223
2224bracket = startSketchOn(XY)
2225  |> startProfile(at = [0,0])
2226  |> line(end = [0, leg1])
2227  |> line(end = [leg2, 0])
2228  |> line(end = [0, -thickness()])
2229  |> line(end = [-leg2 + thickness(), 0])
2230"#;
2231        parse_execute(ast).await.unwrap();
2232    }
2233
2234    #[tokio::test(flavor = "multi_thread")]
2235    async fn test_unary_operator_not_succeeds() {
2236        let ast = r#"
2237fn returnTrue() { return !false }
2238t = true
2239f = false
2240notTrue = !t
2241notFalse = !f
2242c = !!true
2243d = !returnTrue()
2244
2245assertIs(!false, error = "expected to pass")
2246
2247fn check(x) {
2248  assertIs(!x, error = "expected argument to be false")
2249  return true
2250}
2251check(x = false)
2252"#;
2253        let result = parse_execute(ast).await.unwrap();
2254        assert_eq!(
2255            false,
2256            mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
2257                .as_bool()
2258                .unwrap()
2259        );
2260        assert_eq!(
2261            true,
2262            mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
2263                .as_bool()
2264                .unwrap()
2265        );
2266        assert_eq!(
2267            true,
2268            mem_get_json(result.exec_state.stack(), result.mem_env, "c")
2269                .as_bool()
2270                .unwrap()
2271        );
2272        assert_eq!(
2273            false,
2274            mem_get_json(result.exec_state.stack(), result.mem_env, "d")
2275                .as_bool()
2276                .unwrap()
2277        );
2278    }
2279
2280    #[tokio::test(flavor = "multi_thread")]
2281    async fn test_unary_operator_not_on_non_bool_fails() {
2282        let code1 = r#"
2283// Yup, this is null.
2284myNull = 0 / 0
2285notNull = !myNull
2286"#;
2287        assert_eq!(
2288            parse_execute(code1).await.unwrap_err().message(),
2289            "Cannot apply unary operator ! to non-boolean value: a number",
2290        );
2291
2292        let code2 = "notZero = !0";
2293        assert_eq!(
2294            parse_execute(code2).await.unwrap_err().message(),
2295            "Cannot apply unary operator ! to non-boolean value: a number",
2296        );
2297
2298        let code3 = r#"
2299notEmptyString = !""
2300"#;
2301        assert_eq!(
2302            parse_execute(code3).await.unwrap_err().message(),
2303            "Cannot apply unary operator ! to non-boolean value: a string",
2304        );
2305
2306        let code4 = r#"
2307obj = { a = 1 }
2308notMember = !obj.a
2309"#;
2310        assert_eq!(
2311            parse_execute(code4).await.unwrap_err().message(),
2312            "Cannot apply unary operator ! to non-boolean value: a number",
2313        );
2314
2315        let code5 = "
2316a = []
2317notArray = !a";
2318        assert_eq!(
2319            parse_execute(code5).await.unwrap_err().message(),
2320            "Cannot apply unary operator ! to non-boolean value: an empty array",
2321        );
2322
2323        let code6 = "
2324x = {}
2325notObject = !x";
2326        assert_eq!(
2327            parse_execute(code6).await.unwrap_err().message(),
2328            "Cannot apply unary operator ! to non-boolean value: an object",
2329        );
2330
2331        let code7 = "
2332fn x() { return 1 }
2333notFunction = !x";
2334        let fn_err = parse_execute(code7).await.unwrap_err();
2335        // These are currently printed out as JSON objects, so we don't want to
2336        // check the full error.
2337        assert!(
2338            fn_err
2339                .message()
2340                .starts_with("Cannot apply unary operator ! to non-boolean value: "),
2341            "Actual error: {fn_err:?}"
2342        );
2343
2344        let code8 = "
2345myTagDeclarator = $myTag
2346notTagDeclarator = !myTagDeclarator";
2347        let tag_declarator_err = parse_execute(code8).await.unwrap_err();
2348        // These are currently printed out as JSON objects, so we don't want to
2349        // check the full error.
2350        assert!(
2351            tag_declarator_err
2352                .message()
2353                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
2354            "Actual error: {tag_declarator_err:?}"
2355        );
2356
2357        let code9 = "
2358myTagDeclarator = $myTag
2359notTagIdentifier = !myTag";
2360        let tag_identifier_err = parse_execute(code9).await.unwrap_err();
2361        // These are currently printed out as JSON objects, so we don't want to
2362        // check the full error.
2363        assert!(
2364            tag_identifier_err
2365                .message()
2366                .starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
2367            "Actual error: {tag_identifier_err:?}"
2368        );
2369
2370        let code10 = "notPipe = !(1 |> 2)";
2371        assert_eq!(
2372            // TODO: We don't currently parse this, but we should.  It should be
2373            // a runtime error instead.
2374            parse_execute(code10).await.unwrap_err(),
2375            KclError::new_syntax(KclErrorDetails::new(
2376                "Unexpected token: !".to_owned(),
2377                vec![SourceRange::new(10, 11, ModuleId::default())],
2378            ))
2379        );
2380
2381        let code11 = "
2382fn identity(x) { return x }
2383notPipeSub = 1 |> identity(!%))";
2384        assert_eq!(
2385            // TODO: We don't currently parse this, but we should.  It should be
2386            // a runtime error instead.
2387            parse_execute(code11).await.unwrap_err(),
2388            KclError::new_syntax(KclErrorDetails::new(
2389                "There was an unexpected `!`. Try removing it.".to_owned(),
2390                vec![SourceRange::new(56, 57, ModuleId::default())],
2391            ))
2392        );
2393
2394        // TODO: Add these tests when we support these types.
2395        // let notNan = !NaN
2396        // let notInfinity = !Infinity
2397    }
2398
2399    #[tokio::test(flavor = "multi_thread")]
2400    async fn test_start_sketch_on_invalid_kwargs() {
2401        let current_dir = std::env::current_dir().unwrap();
2402        let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
2403        let mut code = std::fs::read_to_string(&path).unwrap();
2404        assert_eq!(
2405            parse_execute(&code).await.unwrap_err().message(),
2406            "You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
2407        );
2408
2409        path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
2410        code = std::fs::read_to_string(&path).unwrap();
2411
2412        assert_eq!(
2413            parse_execute(&code).await.unwrap_err().message(),
2414            "`alignAxis` is required if `normalToFace` is specified.".to_owned(),
2415        );
2416
2417        path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
2418        code = std::fs::read_to_string(&path).unwrap();
2419
2420        assert_eq!(
2421            parse_execute(&code).await.unwrap_err().message(),
2422            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2423        );
2424
2425        path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
2426        code = std::fs::read_to_string(&path).unwrap();
2427
2428        assert_eq!(
2429            parse_execute(&code).await.unwrap_err().message(),
2430            "`normalToFace` is required if `alignAxis` is specified.".to_owned(),
2431        );
2432
2433        path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
2434        code = std::fs::read_to_string(&path).unwrap();
2435
2436        assert_eq!(
2437            parse_execute(&code).await.unwrap_err().message(),
2438            "`normalToFace` is required if `normalOffset` is specified.".to_owned(),
2439        );
2440    }
2441
2442    #[tokio::test(flavor = "multi_thread")]
2443    async fn test_math_negative_variable_in_binary_expression() {
2444        let ast = r#"sigmaAllow = 35000 // psi
2445width = 1 // inch
2446
2447p = 150 // lbs
2448distance = 6 // inches
2449FOS = 2
2450
2451leg1 = 5 // inches
2452leg2 = 8 // inches
2453
2454thickness_squared = distance * p * FOS * 6 / sigmaAllow
2455thickness = 0.56 // inches. App does not support square root function yet
2456
2457bracket = startSketchOn(XY)
2458  |> startProfile(at = [0,0])
2459  |> line(end = [0, leg1])
2460  |> line(end = [leg2, 0])
2461  |> line(end = [0, -thickness])
2462  |> line(end = [-leg2 + thickness, 0])
2463"#;
2464        parse_execute(ast).await.unwrap();
2465    }
2466
2467    #[tokio::test(flavor = "multi_thread")]
2468    async fn test_execute_function_no_return() {
2469        let ast = r#"fn test(@origin) {
2470  origin
2471}
2472
2473test([0, 0])
2474"#;
2475        let result = parse_execute(ast).await;
2476        assert!(result.is_err());
2477        assert!(result.unwrap_err().to_string().contains("undefined"));
2478    }
2479
2480    #[tokio::test(flavor = "multi_thread")]
2481    async fn test_max_stack_size_exceeded_error() {
2482        let ast = r#"
2483fn forever(@n) {
2484  return 1 + forever(n)
2485}
2486
2487forever(1)
2488"#;
2489        let result = parse_execute(ast).await;
2490        let err = result.unwrap_err();
2491        assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
2492    }
2493
2494    #[tokio::test(flavor = "multi_thread")]
2495    async fn test_math_doubly_nested_parens() {
2496        let ast = r#"sigmaAllow = 35000 // psi
2497width = 4 // inch
2498p = 150 // Force on shelf - lbs
2499distance = 6 // inches
2500FOS = 2
2501leg1 = 5 // inches
2502leg2 = 8 // inches
2503thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
2504thickness = 0.32 // inches. App does not support square root function yet
2505bracket = startSketchOn(XY)
2506  |> startProfile(at = [0,0])
2507    |> line(end = [0, leg1])
2508  |> line(end = [leg2, 0])
2509  |> line(end = [0, -thickness])
2510  |> line(end = [-1 * leg2 + thickness, 0])
2511  |> line(end = [0, -1 * leg1 + thickness])
2512  |> close()
2513  |> extrude(length = width)
2514"#;
2515        parse_execute(ast).await.unwrap();
2516    }
2517
2518    #[tokio::test(flavor = "multi_thread")]
2519    async fn test_math_nested_parens_one_less() {
2520        let ast = r#" sigmaAllow = 35000 // psi
2521width = 4 // inch
2522p = 150 // Force on shelf - lbs
2523distance = 6 // inches
2524FOS = 2
2525leg1 = 5 // inches
2526leg2 = 8 // inches
2527thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
2528thickness = 0.32 // inches. App does not support square root function yet
2529bracket = startSketchOn(XY)
2530  |> startProfile(at = [0,0])
2531    |> line(end = [0, leg1])
2532  |> line(end = [leg2, 0])
2533  |> line(end = [0, -thickness])
2534  |> line(end = [-1 * leg2 + thickness, 0])
2535  |> line(end = [0, -1 * leg1 + thickness])
2536  |> close()
2537  |> extrude(length = width)
2538"#;
2539        parse_execute(ast).await.unwrap();
2540    }
2541
2542    #[tokio::test(flavor = "multi_thread")]
2543    async fn test_fn_as_operand() {
2544        let ast = r#"fn f() { return 1 }
2545x = f()
2546y = x + 1
2547z = f() + 1
2548w = f() + f()
2549"#;
2550        parse_execute(ast).await.unwrap();
2551    }
2552
2553    #[tokio::test(flavor = "multi_thread")]
2554    async fn kcl_test_ids_stable_between_executions() {
2555        let code = r#"sketch001 = startSketchOn(XZ)
2556|> startProfile(at = [61.74, 206.13])
2557|> xLine(length = 305.11, tag = $seg01)
2558|> yLine(length = -291.85)
2559|> xLine(length = -segLen(seg01))
2560|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2561|> close()
2562|> extrude(length = 40.14)
2563|> shell(
2564    thickness = 3.14,
2565    faces = [seg01]
2566)
2567"#;
2568
2569        let ctx = crate::test_server::new_context(true, None).await.unwrap();
2570        let old_program = crate::Program::parse_no_errs(code).unwrap();
2571
2572        // Execute the program.
2573        if let Err(err) = ctx.run_with_caching(old_program).await {
2574            let report = err.into_miette_report_with_outputs(code).unwrap();
2575            let report = miette::Report::new(report);
2576            panic!("Error executing program: {report:?}");
2577        }
2578
2579        // Get the id_generator from the first execution.
2580        let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2581
2582        let code = r#"sketch001 = startSketchOn(XZ)
2583|> startProfile(at = [62.74, 206.13])
2584|> xLine(length = 305.11, tag = $seg01)
2585|> yLine(length = -291.85)
2586|> xLine(length = -segLen(seg01))
2587|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2588|> close()
2589|> extrude(length = 40.14)
2590|> shell(
2591    faces = [seg01],
2592    thickness = 3.14,
2593)
2594"#;
2595
2596        // Execute a slightly different program again.
2597        let program = crate::Program::parse_no_errs(code).unwrap();
2598        // Execute the program.
2599        ctx.run_with_caching(program).await.unwrap();
2600
2601        let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
2602
2603        assert_eq!(id_generator, new_id_generator);
2604    }
2605
2606    #[tokio::test(flavor = "multi_thread")]
2607    async fn kcl_test_changing_a_setting_updates_the_cached_state() {
2608        let code = r#"sketch001 = startSketchOn(XZ)
2609|> startProfile(at = [61.74, 206.13])
2610|> xLine(length = 305.11, tag = $seg01)
2611|> yLine(length = -291.85)
2612|> xLine(length = -segLen(seg01))
2613|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
2614|> close()
2615|> extrude(length = 40.14)
2616|> shell(
2617    thickness = 3.14,
2618    faces = [seg01]
2619)
2620"#;
2621
2622        let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
2623        let old_program = crate::Program::parse_no_errs(code).unwrap();
2624
2625        // Execute the program.
2626        ctx.run_with_caching(old_program.clone()).await.unwrap();
2627
2628        let settings_state = cache::read_old_ast().await.unwrap().settings;
2629
2630        // Ensure the settings are as expected.
2631        assert_eq!(settings_state, ctx.settings);
2632
2633        // Change a setting.
2634        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2635
2636        // Execute the program.
2637        ctx.run_with_caching(old_program.clone()).await.unwrap();
2638
2639        let settings_state = cache::read_old_ast().await.unwrap().settings;
2640
2641        // Ensure the settings are as expected.
2642        assert_eq!(settings_state, ctx.settings);
2643
2644        // Change a setting.
2645        ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
2646
2647        // Execute the program.
2648        ctx.run_with_caching(old_program).await.unwrap();
2649
2650        let settings_state = cache::read_old_ast().await.unwrap().settings;
2651
2652        // Ensure the settings are as expected.
2653        assert_eq!(settings_state, ctx.settings);
2654
2655        ctx.close().await;
2656    }
2657
2658    #[tokio::test(flavor = "multi_thread")]
2659    async fn mock_after_not_mock() {
2660        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2661        let program = crate::Program::parse_no_errs("x = 2").unwrap();
2662        let result = ctx.run_with_caching(program).await.unwrap();
2663        assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
2664
2665        let ctx2 = ExecutorContext::new_mock(None).await;
2666        let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
2667        let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
2668        assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
2669
2670        ctx.close().await;
2671        ctx2.close().await;
2672    }
2673
2674    #[cfg(feature = "artifact-graph")]
2675    #[tokio::test(flavor = "multi_thread")]
2676    async fn mock_has_stable_ids() {
2677        let ctx = ExecutorContext::new_mock(None).await;
2678        let mock_config = MockConfig {
2679            use_prev_memory: false,
2680            ..Default::default()
2681        };
2682        let code = "sk = startSketchOn(XY)
2683        |> startProfile(at = [0, 0])";
2684        let program = crate::Program::parse_no_errs(code).unwrap();
2685        let result = ctx.run_mock(&program, &mock_config).await.unwrap();
2686        let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2687        assert!(!ids.is_empty(), "IDs should not be empty");
2688
2689        let ctx2 = ExecutorContext::new_mock(None).await;
2690        let program2 = crate::Program::parse_no_errs(code).unwrap();
2691        let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
2692        let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
2693
2694        assert_eq!(ids, ids2, "Generated IDs should match");
2695        ctx.close().await;
2696        ctx2.close().await;
2697    }
2698
2699    #[cfg(feature = "artifact-graph")]
2700    #[tokio::test(flavor = "multi_thread")]
2701    async fn sim_sketch_mode_real_mock_real() {
2702        let ctx = ExecutorContext::new_with_default_client().await.unwrap();
2703        let code = r#"sketch001 = startSketchOn(XY)
2704profile001 = startProfile(sketch001, at = [0, 0])
2705  |> line(end = [10, 0])
2706  |> line(end = [0, 10])
2707  |> line(end = [-10, 0])
2708  |> line(end = [0, -10])
2709  |> close()
2710"#;
2711        let program = crate::Program::parse_no_errs(code).unwrap();
2712        let result = ctx.run_with_caching(program).await.unwrap();
2713        assert_eq!(result.operations.len(), 1);
2714
2715        let mock_ctx = ExecutorContext::new_mock(None).await;
2716        let mock_program = crate::Program::parse_no_errs(code).unwrap();
2717        let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
2718        assert_eq!(mock_result.operations.len(), 1);
2719
2720        let code2 = code.to_owned()
2721            + r#"
2722extrude001 = extrude(profile001, length = 10)
2723"#;
2724        let program2 = crate::Program::parse_no_errs(&code2).unwrap();
2725        let result = ctx.run_with_caching(program2).await.unwrap();
2726        assert_eq!(result.operations.len(), 2);
2727
2728        ctx.close().await;
2729        mock_ctx.close().await;
2730    }
2731
2732    #[tokio::test(flavor = "multi_thread")]
2733    async fn read_tag_version() {
2734        let ast = r#"fn bar(@t) {
2735  return startSketchOn(XY)
2736    |> startProfile(at = [0,0])
2737    |> angledLine(
2738        angle = -60,
2739        length = segLen(t),
2740    )
2741    |> line(end = [0, 0])
2742    |> close()
2743}
2744
2745sketch = startSketchOn(XY)
2746  |> startProfile(at = [0,0])
2747  |> line(end = [0, 10])
2748  |> line(end = [10, 0], tag = $tag0)
2749  |> line(endAbsolute = [0, 0])
2750
2751fn foo() {
2752  // tag0 tags an edge
2753  return bar(tag0)
2754}
2755
2756solid = sketch |> extrude(length = 10)
2757// tag0 tags a face
2758sketch2 = startSketchOn(solid, face = tag0)
2759  |> startProfile(at = [0,0])
2760  |> line(end = [0, 1])
2761  |> line(end = [1, 0])
2762  |> line(end = [0, 0])
2763
2764foo() |> extrude(length = 1)
2765"#;
2766        parse_execute(ast).await.unwrap();
2767    }
2768
2769    #[tokio::test(flavor = "multi_thread")]
2770    async fn experimental() {
2771        let code = r#"
2772startSketchOn(XY)
2773  |> startProfile(at = [0, 0], tag = $start)
2774  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2775"#;
2776        let result = parse_execute(code).await.unwrap();
2777        let errors = result.exec_state.errors();
2778        assert_eq!(errors.len(), 1);
2779        assert_eq!(errors[0].severity, Severity::Error);
2780        let msg = &errors[0].message;
2781        assert!(msg.contains("experimental"), "found {msg}");
2782
2783        let code = r#"@settings(experimentalFeatures = allow)
2784startSketchOn(XY)
2785  |> startProfile(at = [0, 0], tag = $start)
2786  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2787"#;
2788        let result = parse_execute(code).await.unwrap();
2789        let errors = result.exec_state.errors();
2790        assert!(errors.is_empty());
2791
2792        let code = r#"@settings(experimentalFeatures = warn)
2793startSketchOn(XY)
2794  |> startProfile(at = [0, 0], tag = $start)
2795  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2796"#;
2797        let result = parse_execute(code).await.unwrap();
2798        let errors = result.exec_state.errors();
2799        assert_eq!(errors.len(), 1);
2800        assert_eq!(errors[0].severity, Severity::Warning);
2801        let msg = &errors[0].message;
2802        assert!(msg.contains("experimental"), "found {msg}");
2803
2804        let code = r#"@settings(experimentalFeatures = deny)
2805startSketchOn(XY)
2806  |> startProfile(at = [0, 0], tag = $start)
2807  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2808"#;
2809        let result = parse_execute(code).await.unwrap();
2810        let errors = result.exec_state.errors();
2811        assert_eq!(errors.len(), 1);
2812        assert_eq!(errors[0].severity, Severity::Error);
2813        let msg = &errors[0].message;
2814        assert!(msg.contains("experimental"), "found {msg}");
2815
2816        let code = r#"@settings(experimentalFeatures = foo)
2817startSketchOn(XY)
2818  |> startProfile(at = [0, 0], tag = $start)
2819  |> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
2820"#;
2821        parse_execute(code).await.unwrap_err();
2822    }
2823}