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