Skip to main content

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