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