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