Skip to main content

kcl_lib/execution/
mod.rs

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