Skip to main content

kcl_lib/execution/
state.rs

1use std::collections::BTreeMap;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use ahash::AHashMap;
6use anyhow::Result;
7use indexmap::IndexMap;
8use kcl_api::UnitAngle;
9use kcl_api::UnitLength;
10use serde::Deserialize;
11use serde::Serialize;
12use uuid::Uuid;
13
14use crate::CompilationIssue;
15use crate::ExecutorContext;
16use crate::KclErrorWithOutputs;
17use crate::MockConfig;
18use crate::NodePath;
19use crate::SegmentDragAnchor;
20use crate::SourceRange;
21use crate::collections::AhashIndexSet;
22use crate::engine::engine_manager::EngineManager;
23use crate::errors::KclError;
24use crate::errors::KclErrorDetails;
25use crate::errors::Severity;
26use crate::exec::DefaultPlanes;
27use crate::execution::Artifact;
28use crate::execution::ArtifactCommand;
29use crate::execution::ArtifactGraph;
30use crate::execution::ArtifactId;
31use crate::execution::EnvironmentRef;
32use crate::execution::ExecOutcome;
33use crate::execution::ExecutorSettings;
34use crate::execution::KclValue;
35use crate::execution::KclValueView;
36use crate::execution::OperationCallbackArgs;
37use crate::execution::OperationsByModule;
38use crate::execution::ProgramLookup;
39use crate::execution::SketchVarId;
40use crate::execution::UnsolvedSegment;
41use crate::execution::annotations;
42use crate::execution::cad_op::Operation;
43use crate::execution::id_generator::IdGenerator;
44#[cfg(test)]
45use crate::execution::memory::MemoryBackendKind;
46use crate::execution::memory::ProgramMemory;
47use crate::execution::memory::Stack;
48use crate::execution::sketch_solve::Solved;
49use crate::execution::types::NumericType;
50use crate::front::Number;
51use crate::front::Object;
52use crate::front::ObjectId;
53use crate::front::ObjectKind;
54use crate::id::IncIdGenerator;
55use crate::modules::ModuleId;
56use crate::modules::ModuleInfo;
57use crate::modules::ModuleLoader;
58use crate::modules::ModulePath;
59use crate::modules::ModuleRepr;
60use crate::modules::ModuleSource;
61use crate::parsing::ast::types::Annotation;
62use crate::parsing::ast::types::NodeRef;
63use crate::parsing::ast::types::TagNode;
64
65/// State for executing a program.
66#[derive(Debug, Clone)]
67pub struct ExecState {
68    pub(super) execution_callbacks: Option<std::sync::Arc<dyn crate::execution::ExecutionCallbacks>>,
69    pub(super) global: GlobalState,
70    pub(super) mod_local: ModuleState,
71}
72
73pub type ModuleInfoMap = IndexMap<ModuleId, ModuleInfo>;
74
75#[derive(Debug, Clone)]
76pub(super) struct GlobalState {
77    /// Map from source file absolute path to module ID.
78    pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
79    /// Map from module ID to source file.
80    pub id_to_source: IndexMap<ModuleId, ModuleSource>,
81    /// Map from module ID to module info.
82    pub module_infos: ModuleInfoMap,
83    /// Module loader.
84    pub mod_loader: ModuleLoader,
85    /// Errors and warnings.
86    pub issues: Vec<CompilationIssue>,
87    /// Global artifacts that represent the entire program.
88    pub artifacts: ArtifactState,
89    /// Artifacts for only the root module.
90    pub root_module_artifacts: ModuleArtifactState,
91    /// The segments that were edited that triggered this execution.
92    pub segment_ids_edited: AhashIndexSet<ObjectId>,
93    /// Segment-body drag anchors that temporarily pull a point on a segment toward the cursor.
94    pub drag_anchors: Vec<SegmentDragAnchor>,
95    /// True if this execution is sketch mode execution, executing a single
96    /// sketch block. Unlike [`ModuleState::sketch_mode`], this is constant for
97    /// the entire execution, including while executing the body of the sketch
98    /// block being edited.
99    pub sketch_mode: bool,
100}
101
102impl GlobalState {
103    pub(crate) fn operations_by_module(&self) -> OperationsByModule {
104        let mut operations = OperationsByModule::default();
105        operations.insert(ModuleId::default(), self.root_module_artifacts.operations.clone());
106
107        for (module_id, module_info) in &self.module_infos {
108            match &module_info.repr {
109                ModuleRepr::Root => {}
110                ModuleRepr::Kcl(_, Some(outcome)) => {
111                    operations.insert(*module_id, outcome.artifacts.operations.clone());
112                }
113                ModuleRepr::Foreign(_, Some((_, artifacts))) => {
114                    operations.insert(*module_id, artifacts.operations.clone());
115                }
116                ModuleRepr::Kcl(_, None) | ModuleRepr::Foreign(_, None) | ModuleRepr::Dummy => {}
117            }
118        }
119
120        operations
121    }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
125pub(crate) enum ConstraintKey {
126    LineCircle([usize; 10]),
127    CircleCircle([usize; 12]),
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub(crate) enum TangencyMode {
132    LineCircle(ezpz::LineSide),
133    CircleCircle(ezpz::CircleSide),
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
137pub(crate) enum ConstraintState {
138    Tangency(TangencyMode),
139}
140
141#[derive(Debug, Clone, Default)]
142pub(super) struct ArtifactState {
143    /// Internal map of UUIDs to exec artifacts.  This needs to persist across
144    /// executions to allow the graph building to refer to cached artifacts.
145    pub artifacts: IndexMap<ArtifactId, Artifact>,
146    /// Output artifact graph.
147    pub graph: ArtifactGraph,
148}
149
150/// Which stdlib edge function produced this refactor metadata (for lint/code mod).
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
152#[ts(export)]
153#[serde(rename_all = "camelCase")]
154pub enum EdgeRefactorStdlibFn {
155    GetOppositeEdge,
156    GetNextAdjacentEdge,
157    GetPreviousAdjacentEdge,
158    GetCommonEdge,
159    EdgeId,
160}
161
162/// Metadata collected when a deprecated edge stdlib function runs, for refactor-to-edgeRefs lint/code mod.
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
164#[ts(export)]
165#[serde(rename_all = "camelCase")]
166pub struct EdgeRefactorMeta {
167    pub edge_id: Uuid,
168    pub face_ids: [Uuid; 2],
169    #[serde(default, skip_serializing_if = "Vec::is_empty")]
170    pub end_face_ids: Vec<Uuid>,
171    pub source_range: SourceRange,
172    pub stdlib_fn: EdgeRefactorStdlibFn,
173}
174
175/// Metadata for a deprecated edge stdlib function whose edge ID was resolved,
176/// but whose adjacent face IDs could not be recorded at the helper callsite.
177#[derive(Debug, Clone, PartialEq, Eq)]
178pub(crate) struct PendingEdgeRefactorMeta {
179    pub edge_id: Uuid,
180    pub source_range: SourceRange,
181    pub stdlib_fn: EdgeRefactorStdlibFn,
182}
183
184/// One tag entry in a fillet/chamfer call that used `tags` directly (for refactor to edgeRefs).
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
186#[ts(export)]
187#[serde(rename_all = "camelCase")]
188pub struct DirectTagFilletTagEntry {
189    pub tag_identifier: String,
190    pub edge_id: Uuid,
191    pub face_ids: [Uuid; 2],
192}
193
194/// Metadata for one fillet/chamfer call that used `tags` directly (no stdlib call).
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
196#[ts(export)]
197#[serde(rename_all = "camelCase")]
198pub struct DirectTagFilletMeta {
199    pub call_source_range: SourceRange,
200    pub tags: Vec<DirectTagFilletTagEntry>,
201}
202
203/// Unified metadata stream for Z0006 and future execution-backed refactors.
204#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
205#[ts(export)]
206#[serde(tag = "kind", content = "data", rename_all = "camelCase")]
207pub enum RefactorMetadata {
208    EdgeRefactor(Box<EdgeRefactorMeta>),
209    DirectTagFillet(DirectTagFilletMeta),
210}
211
212/// Artifact state for a single module.
213#[derive(Debug, Clone, Default, PartialEq, Serialize)]
214pub struct ModuleArtifactState {
215    /// Internal map of UUIDs to exec artifacts.
216    pub artifacts: IndexMap<ArtifactId, Artifact>,
217    /// Outgoing engine commands that have not yet been processed and integrated
218    /// into the artifact graph.
219    #[serde(skip)]
220    pub unprocessed_commands: Vec<ArtifactCommand>,
221    /// Outgoing engine commands.
222    pub commands: Vec<ArtifactCommand>,
223    /// Incoming engine commands.
224    #[cfg(feature = "snapshot-engine-responses")]
225    pub responses: IndexMap<Uuid, kittycad_modeling_cmds::websocket::WebSocketResponse>,
226    /// Operations that have been performed in execution order, for display in
227    /// the Feature Tree.
228    pub operations: Vec<Operation>,
229    /// [`ObjectId`] generator.
230    pub object_id_generator: IncIdGenerator<usize>,
231    /// Objects in the scene, created from execution.
232    pub scene_objects: Vec<Object>,
233    /// Map from source range to object ID for lookup of objects by their source
234    /// range.
235    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
236    /// Map from artifact ID to object ID in the scene.
237    pub artifact_id_to_scene_object: IndexMap<ArtifactId, ObjectId>,
238    /// Solutions for sketch variables.
239    pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
240    /// Metadata collected during execution for refactor lint/code-mod paths (Z0006 and future).
241    pub refactor_metadata: Vec<RefactorMetadata>,
242    /// Deprecated edge helper callsites that may be completed by a downstream
243    /// operation that knows the target solid.
244    #[serde(skip)]
245    pub(crate) pending_edge_refactor_metadata: Vec<PendingEdgeRefactorMeta>,
246}
247
248#[derive(Debug, Clone)]
249pub(super) struct ModuleState {
250    /// The id of this module.
251    pub module_id: ModuleId,
252    /// The id generator for this module.
253    pub id_generator: IdGenerator,
254    pub stack: Stack,
255    /// The size of the call stack. This is used to prevent stack overflows with
256    /// recursive function calls. In general, this doesn't match `stack`'s size
257    /// since it's conservative in reclaiming frames between executions.
258    pub(super) call_stack_size: usize,
259    /// The current value of the pipe operator returned from the previous
260    /// expression.  If we're not currently in a pipeline, this will be None.
261    pub pipe_value: Option<KclValue>,
262    /// The closest variable declaration being executed in any parent node in the AST.
263    /// This is used to provide better error messages, e.g. noticing when the user is trying
264    /// to use the variable `length` inside the RHS of its own definition, like `length = tan(length)`.
265    pub being_declared: Option<String>,
266    /// Present if we're currently executing inside a sketch block.
267    pub sketch_block: Option<SketchBlockState>,
268    /// Tracks if KCL being executed is currently inside a stdlib function or not.
269    /// This matters because e.g. we shouldn't emit artifacts from declarations declared inside a stdlib function.
270    pub inside_stdlib: bool,
271    /// The source range where we entered the standard library.
272    pub stdlib_entry_source_range: Option<SourceRange>,
273    /// Identifiers that have been exported from the current module.
274    pub module_exports: Vec<String>,
275    /// Settings specified from annotations.
276    pub settings: MetaSettings,
277    /// True if executing in sketch mode. Only a single sketch block will be
278    /// executed. All other code is ignored.
279    pub sketch_mode: bool,
280    /// True to do more costly analysis of whether the sketch block segments are
281    /// under-constrained. The only time we disable this is when a user is
282    /// dragging segments.
283    pub freedom_analysis: bool,
284    pub(super) explicit_length_units: bool,
285    pub(super) path: ModulePath,
286    /// Artifacts for only this module.
287    pub artifacts: ModuleArtifactState,
288    /// Sticky per-constraint state persisted across sketch-mode mock solves.
289    /// Maps from sketch block ID to a map for that sketch.
290    /// Then the inner map is per constraint (in that sketch block) to its state.
291    pub constraint_state: IndexMap<ObjectId, IndexMap<ConstraintKey, ConstraintState>>,
292
293    pub(super) allowed_warnings: Vec<&'static str>,
294    pub(super) denied_warnings: Vec<&'static str>,
295
296    /// Map from consumed solid values to information about the operation that
297    /// consumed them. Populated by operations that destroy their inputs so that
298    /// subsequent attempts to use a consumed solid produce a clear KCL-level
299    /// error rather than a cryptic engine error.
300    pub(super) consumed_solids: AHashMap<ConsumedSolidKey, ConsumedSolidInfo>,
301    /// Defensive map from consumed engine UUID to consumption info.
302    /// Rust code may create a `Solid` with a consumed `engine_id` and a
303    /// different `instance_id` that was not recorded in `consumed_solids`. When
304    /// the exact key lookup misses, this map lets us reject that solid by
305    /// `engine_id`, unless the key is a recorded operation output.
306    pub(super) consumed_solid_ids: AHashMap<Uuid, ConsumedSolidInfo>,
307}
308
309/// Internal identity for one runtime KCL solid value.
310#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
311pub(crate) struct ConsumedSolidKey {
312    /// The engine body UUID.
313    engine_id: Uuid,
314    /// Distinguishes this KCL runtime instance from other values that may reuse
315    /// the same engine body UUID.
316    instance_id: Uuid,
317}
318
319impl ConsumedSolidKey {
320    pub(crate) fn new(engine_id: Uuid, instance_id: Uuid) -> Self {
321        Self { engine_id, instance_id }
322    }
323
324    pub(crate) fn engine_id(&self) -> Uuid {
325        self.engine_id
326    }
327
328    pub(crate) fn instance_id(&self) -> Uuid {
329        self.instance_id
330    }
331}
332
333/// Information about a solid value that was consumed by an operation.
334/// Stored in `ModuleState.consumed_solids` so subsequent attempts to use the
335/// solid produce a clear error pointing at the operation that consumed it.
336#[derive(Debug, Clone)]
337pub(crate) struct ConsumedSolidInfo {
338    /// The operation that consumed the solid.
339    operation: ConsumedSolidOperation,
340    /// First returned solid value, used only for replacement suggestions in
341    /// error messages. When present, this key is also included in
342    /// `returned_solid_keys`.
343    suggested_replacement_key: Option<ConsumedSolidKey>,
344    /// All solid values returned by that operation. This is used as the
345    /// allow-list for returned solids that reuse a consumed engine UUID.
346    returned_solid_keys: Vec<ConsumedSolidKey>,
347}
348
349impl ConsumedSolidInfo {
350    pub(crate) fn new(operation: ConsumedSolidOperation, returned_solid_keys: Vec<ConsumedSolidKey>) -> Self {
351        Self {
352            operation,
353            suggested_replacement_key: returned_solid_keys.first().copied(),
354            returned_solid_keys,
355        }
356    }
357
358    pub(crate) fn operation(&self) -> ConsumedSolidOperation {
359        self.operation
360    }
361
362    pub(crate) fn suggested_replacement_key(&self) -> Option<ConsumedSolidKey> {
363        self.suggested_replacement_key
364    }
365
366    pub(crate) fn should_report_reused_engine_id_as_consumed(&self, key: ConsumedSolidKey) -> bool {
367        !self.returned_solid_keys.contains(&key)
368    }
369}
370
371#[derive(Debug, Clone, Copy, PartialEq, Eq)]
372pub(crate) enum ConsumedSolidOperation {
373    Union,
374    Intersect,
375    Subtract,
376    Split,
377    JoinSurfaces,
378}
379
380impl ConsumedSolidOperation {
381    pub(crate) fn indefinite_article(self) -> &'static str {
382        match self {
383            Self::Intersect => "an",
384            Self::Union | Self::Subtract | Self::Split | Self::JoinSurfaces => "a",
385        }
386    }
387}
388
389impl std::fmt::Display for ConsumedSolidOperation {
390    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
391        match self {
392            Self::Union => f.write_str("union"),
393            Self::Intersect => f.write_str("intersect"),
394            Self::Subtract => f.write_str("subtract"),
395            Self::Split => f.write_str("split"),
396            Self::JoinSurfaces => f.write_str("joinSurfaces"),
397        }
398    }
399}
400
401#[derive(Debug, Clone, Default)]
402pub(crate) struct SketchBlockState {
403    pub sketch_vars: Vec<KclValue>,
404    pub sketch_id: Option<ObjectId>,
405    pub sketch_constraints: Vec<ObjectId>,
406    pub solver_constraints: Vec<ezpz::Constraint>,
407    pub solver_optional_constraints: Vec<ezpz::Constraint>,
408    pub needed_by_engine: Vec<UnsolvedSegment>,
409    pub segment_tags: IndexMap<ObjectId, TagNode>,
410}
411
412impl ExecState {
413    pub fn new(exec_context: &super::ExecutorContext) -> Self {
414        ExecState {
415            execution_callbacks: exec_context.execution_callbacks.clone(),
416            global: GlobalState::new(&exec_context.settings, Default::default()),
417            mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default(), false, true),
418        }
419    }
420
421    #[cfg(test)]
422    pub(crate) fn new_with_memory_backend(exec_context: &super::ExecutorContext, backend: MemoryBackendKind) -> Self {
423        ExecState {
424            execution_callbacks: exec_context.execution_callbacks.clone(),
425            global: GlobalState::new(&exec_context.settings, Default::default()),
426            mod_local: ModuleState::new(
427                ModulePath::Main,
428                ProgramMemory::new_with_backend(backend),
429                Default::default(),
430                false,
431                true,
432            ),
433        }
434    }
435
436    pub fn new_mock(exec_context: &super::ExecutorContext, mock_config: &MockConfig) -> Self {
437        let segment_ids_edited = mock_config.segment_ids_edited.clone();
438        let mut global = GlobalState::new(&exec_context.settings, segment_ids_edited);
439        global.drag_anchors = mock_config.drag_anchors.clone();
440        global.sketch_mode = mock_config.sketch_block_id.is_some();
441        ExecState {
442            execution_callbacks: exec_context.execution_callbacks.clone(),
443            global,
444            mod_local: ModuleState::new(
445                ModulePath::Main,
446                ProgramMemory::new(),
447                Default::default(),
448                mock_config.sketch_block_id.is_some(),
449                mock_config.freedom_analysis,
450            ),
451        }
452    }
453
454    #[cfg(test)]
455    pub(crate) fn new_mock_with_memory_backend(
456        exec_context: &super::ExecutorContext,
457        mock_config: &MockConfig,
458        backend: MemoryBackendKind,
459    ) -> Self {
460        let segment_ids_edited = mock_config.segment_ids_edited.clone();
461        let mut global = GlobalState::new(&exec_context.settings, segment_ids_edited);
462        global.drag_anchors = mock_config.drag_anchors.clone();
463        global.sketch_mode = mock_config.sketch_block_id.is_some();
464        ExecState {
465            execution_callbacks: exec_context.execution_callbacks.clone(),
466            global,
467            mod_local: ModuleState::new(
468                ModulePath::Main,
469                ProgramMemory::new_with_backend(backend),
470                Default::default(),
471                mock_config.sketch_block_id.is_some(),
472                mock_config.freedom_analysis,
473            ),
474        }
475    }
476
477    pub(super) fn reset(&mut self, exec_context: &super::ExecutorContext) {
478        let global = GlobalState::new(&exec_context.settings, Default::default());
479
480        *self = ExecState {
481            execution_callbacks: exec_context.execution_callbacks.clone(),
482            global,
483            mod_local: ModuleState::new(
484                self.mod_local.path.clone(),
485                ProgramMemory::new(),
486                Default::default(),
487                false,
488                true,
489            ),
490        };
491    }
492
493    /// Log a non-fatal error.
494    pub fn err(&mut self, e: CompilationIssue) {
495        self.global.issues.push(e);
496    }
497
498    /// Log a warning.
499    pub fn warn(&mut self, mut e: CompilationIssue, name: &'static str) {
500        debug_assert!(annotations::WARN_VALUES.contains(&name));
501
502        if self.mod_local.allowed_warnings.contains(&name) {
503            return;
504        }
505
506        if self.mod_local.denied_warnings.contains(&name) {
507            e.severity = Severity::Error;
508        } else {
509            e.severity = Severity::Warning;
510        }
511
512        self.global.issues.push(e);
513    }
514
515    pub fn warn_experimental(&mut self, feature_name: &str, source_range: SourceRange) {
516        let Some(severity) = self.mod_local.settings.experimental_features.severity() else {
517            return;
518        };
519        let error = CompilationIssue {
520            source_range,
521            message: format!("Use of {feature_name} is experimental and may change or be removed."),
522            suggestion: None,
523            severity,
524            tag: crate::errors::Tag::None,
525        };
526
527        self.global.issues.push(error);
528    }
529
530    pub fn clear_units_warnings(&mut self, source_range: &SourceRange) {
531        self.global.issues = std::mem::take(&mut self.global.issues)
532            .into_iter()
533            .filter(|e| {
534                e.severity != Severity::Warning
535                    || !source_range.contains_range(&e.source_range)
536                    || e.tag != crate::errors::Tag::UnknownNumericUnits
537            })
538            .collect();
539    }
540
541    pub fn issues(&self) -> &[CompilationIssue] {
542        &self.global.issues
543    }
544
545    /// Convert to execution outcome when running in WebAssembly.  We want to
546    /// reduce the amount of data that crosses the WASM boundary as much as
547    /// possible.
548    pub async fn into_exec_outcome(
549        self,
550        main_ref: EnvironmentRef,
551        ctx: &ExecutorContext,
552    ) -> Result<ExecOutcome, KclError> {
553        // Fields are opt-in so that we don't accidentally leak private internal
554        // state when we add more to ExecState.
555        let variables = self
556            .mod_local
557            .variables(main_ref)?
558            .into_iter()
559            .map(|(key, value)| (key, KclValueView::from(value)))
560            .collect();
561        Ok(ExecOutcome {
562            variables,
563            filenames: self.global.filenames(),
564            operations: self.global.operations_by_module(),
565            artifact_graph: self.global.artifacts.graph,
566            scene_objects: self.global.root_module_artifacts.scene_objects,
567            source_range_to_object: self.global.root_module_artifacts.source_range_to_object,
568            var_solutions: self.global.root_module_artifacts.var_solutions,
569            refactor_metadata: self.global.root_module_artifacts.refactor_metadata.clone(),
570            issues: self.global.issues,
571            default_planes: ctx.engine.get_default_planes().read().await.clone(),
572        })
573    }
574
575    #[cfg(feature = "snapshot-engine-responses")]
576    pub(crate) fn take_root_module_responses(
577        &mut self,
578    ) -> IndexMap<Uuid, kittycad_modeling_cmds::websocket::WebSocketResponse> {
579        std::mem::take(&mut self.global.root_module_artifacts.responses)
580    }
581
582    pub(crate) fn stack(&self) -> &Stack {
583        &self.mod_local.stack
584    }
585
586    pub(crate) fn mut_stack(&mut self) -> &mut Stack {
587        &mut self.mod_local.stack
588    }
589
590    /// Increment the user-level call stack size, returning an error if it
591    /// exceeds the maximum.
592    pub(super) fn inc_call_stack_size(&mut self, range: SourceRange) -> Result<(), KclError> {
593        // If you change this, make sure to test in WebAssembly in the app since
594        // that's the limiting factor.
595        if self.mod_local.call_stack_size >= 50 {
596            return Err(KclError::MaxCallStack {
597                details: KclErrorDetails::new("maximum call stack size exceeded".to_owned(), vec![range]),
598            });
599        }
600        self.mod_local.call_stack_size += 1;
601        Ok(())
602    }
603
604    /// Decrement the user-level call stack size, returning an error if it would
605    /// go below zero.
606    pub(super) fn dec_call_stack_size(&mut self, range: SourceRange) -> Result<(), KclError> {
607        // Prevent underflow.
608        if self.mod_local.call_stack_size == 0 {
609            let message = "call stack size below zero".to_owned();
610            debug_assert!(false, "{message}");
611            return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
612        }
613        self.mod_local.call_stack_size -= 1;
614        Ok(())
615    }
616
617    /// Returns true if we're executing in sketch mode for the current module.
618    /// In sketch mode, we still want to execute the prelude and other stdlib
619    /// modules as normal, so it can vary per module within a single overall
620    /// execution.
621    pub(crate) fn sketch_mode(&self) -> bool {
622        self.mod_local.sketch_mode
623            && match &self.mod_local.path {
624                ModulePath::Main => true,
625                ModulePath::Local { .. } => true,
626                ModulePath::Std { .. } => false,
627            }
628    }
629
630    /// Returns true if this execution is sketch mode execution, executing a
631    /// single sketch block. Unlike [`Self::sketch_mode`], this doesn't vary
632    /// during the execution.
633    pub(crate) fn is_sketch_mode_execution(&self) -> bool {
634        self.global.sketch_mode
635    }
636
637    pub fn next_object_id(&mut self) -> ObjectId {
638        ObjectId(self.mod_local.artifacts.object_id_generator.next_id())
639    }
640
641    pub fn peek_object_id(&self) -> ObjectId {
642        ObjectId(self.mod_local.artifacts.object_id_generator.peek_id())
643    }
644
645    pub(crate) fn constraint_state(&self, sketch_block_id: ObjectId, key: &ConstraintKey) -> Option<ConstraintState> {
646        let map = self.mod_local.constraint_state.get(&sketch_block_id)?;
647        map.get(key).copied()
648    }
649
650    pub(crate) fn set_constraint_state(
651        &mut self,
652        sketch_block_id: ObjectId,
653        key: ConstraintKey,
654        state: ConstraintState,
655    ) {
656        let map = self.mod_local.constraint_state.entry(sketch_block_id).or_default();
657        map.insert(key, state);
658    }
659
660    pub fn add_scene_object(&mut self, obj: Object, source_range: SourceRange) -> ObjectId {
661        let id = obj.id;
662        debug_assert!(
663            id.0 == self.mod_local.artifacts.scene_objects.len(),
664            "Adding scene object with ID {} but next ID is {}",
665            id.0,
666            self.mod_local.artifacts.scene_objects.len()
667        );
668        let artifact_id = obj.artifact_id;
669        self.mod_local.artifacts.scene_objects.push(obj);
670        self.mod_local.artifacts.source_range_to_object.insert(source_range, id);
671        self.mod_local
672            .artifacts
673            .artifact_id_to_scene_object
674            .insert(artifact_id, id);
675        id
676    }
677
678    /// Add a placeholder scene object. This is useful when we need to reserve
679    /// an ID before we have all the information to create the full object.
680    pub fn add_placeholder_scene_object(
681        &mut self,
682        id: ObjectId,
683        source_range: SourceRange,
684        node_path: Option<NodePath>,
685    ) -> ObjectId {
686        debug_assert!(id.0 == self.mod_local.artifacts.scene_objects.len());
687        self.mod_local
688            .artifacts
689            .scene_objects
690            .push(Object::placeholder(id, source_range, node_path));
691        self.mod_local.artifacts.source_range_to_object.insert(source_range, id);
692        id
693    }
694
695    /// Update a scene object. This is useful to replace a placeholder.
696    pub fn set_scene_object(&mut self, object: Object) {
697        let id = object.id;
698        let artifact_id = object.artifact_id;
699        self.mod_local.artifacts.scene_objects[id.0] = object;
700        self.mod_local
701            .artifacts
702            .artifact_id_to_scene_object
703            .insert(artifact_id, id);
704    }
705
706    pub fn scene_object_id_by_artifact_id(&self, artifact_id: ArtifactId) -> Option<ObjectId> {
707        self.mod_local
708            .artifacts
709            .artifact_id_to_scene_object
710            .get(&artifact_id)
711            .cloned()
712    }
713
714    pub fn segment_ids_edited_contains(&self, object_id: &ObjectId) -> bool {
715        self.global.segment_ids_edited.contains(object_id)
716    }
717
718    pub fn drag_anchor_target(&self, object_id: &ObjectId) -> Option<&crate::front::Point2d<crate::front::Number>> {
719        self.global
720            .drag_anchors
721            .iter()
722            .find(|anchor| &anchor.segment_id == object_id)
723            .map(|anchor| &anchor.target)
724    }
725
726    pub(super) fn is_in_sketch_block(&self) -> bool {
727        self.mod_local.sketch_block.is_some()
728    }
729
730    pub(crate) fn sketch_block_mut(&mut self) -> Option<&mut SketchBlockState> {
731        self.mod_local.sketch_block.as_mut()
732    }
733
734    pub(crate) fn sketch_block(&mut self) -> Option<&SketchBlockState> {
735        self.mod_local.sketch_block.as_ref()
736    }
737
738    pub fn next_uuid(&mut self) -> Uuid {
739        self.mod_local.id_generator.next_uuid()
740    }
741
742    pub fn next_artifact_id(&mut self) -> ArtifactId {
743        self.mod_local.id_generator.next_artifact_id()
744    }
745
746    pub fn id_generator(&mut self) -> &mut IdGenerator {
747        &mut self.mod_local.id_generator
748    }
749
750    /// Record that a solid value has been consumed by a CSG boolean operation.
751    pub(crate) fn mark_solid_consumed(&mut self, consumed_key: ConsumedSolidKey, info: ConsumedSolidInfo) {
752        self.mod_local.consumed_solids.insert(consumed_key, info);
753    }
754
755    /// Record that an engine body UUID has been consumed by a CSG boolean
756    /// operation.
757    pub(crate) fn mark_solid_id_consumed(&mut self, consumed_id: Uuid, info: ConsumedSolidInfo) {
758        self.mod_local.consumed_solid_ids.insert(consumed_id, info);
759    }
760
761    /// Look up whether a solid value was consumed by a previous CSG boolean
762    /// operation.
763    pub(crate) fn check_solid_consumed(&self, key: &ConsumedSolidKey) -> Option<&ConsumedSolidInfo> {
764        self.mod_local.consumed_solids.get(key)
765    }
766
767    /// Look up whether an engine body UUID was consumed by a previous CSG
768    /// boolean operation.
769    pub(crate) fn check_solid_id_consumed(&self, id: &Uuid) -> Option<&ConsumedSolidInfo> {
770        self.mod_local.consumed_solid_ids.get(id)
771    }
772
773    /// Follow direct replacement links until we find the latest known output.
774    /// Used only on error paths so diagnostics can suggest the current solid.
775    pub(crate) fn latest_consumed_output(
776        &self,
777        suggested_replacement_key: Option<ConsumedSolidKey>,
778    ) -> Option<ConsumedSolidKey> {
779        let mut latest = suggested_replacement_key?;
780        let mut seen = AhashIndexSet::default();
781
782        while seen.insert(latest) {
783            let Some(next) = self
784                .mod_local
785                .consumed_solids
786                .get(&latest)
787                .and_then(|info| info.suggested_replacement_key())
788            else {
789                break;
790            };
791            latest = next;
792        }
793
794        Some(latest)
795    }
796
797    /// Search the live environment for the name of a variable holding a Solid
798    /// (or an array of Solids) whose value identity matches `target_key`. Used only on
799    /// error paths to recover variable names for diagnostics.
800    pub(crate) fn find_var_name_for_solid_key(&self, target_key: ConsumedSolidKey) -> Result<Option<String>, KclError> {
801        fn contains_solid_key(value: &KclValue, target_key: ConsumedSolidKey) -> bool {
802            match value {
803                KclValue::Solid { value } => {
804                    value.id == target_key.engine_id() && value.value_id == target_key.instance_id()
805                }
806                KclValue::HomArray { value, .. } => value.iter().any(|v| contains_solid_key(v, target_key)),
807                _ => false,
808            }
809        }
810        self.mod_local
811            .stack
812            .find_var_name_in_all_envs(|value| contains_solid_key(value, target_key))
813    }
814
815    pub(crate) fn add_artifact(&mut self, artifact: Artifact) {
816        let id = artifact.id();
817        self.mod_local.artifacts.artifacts.insert(id, artifact);
818    }
819
820    pub(crate) fn artifact_mut(&mut self, id: ArtifactId) -> Option<&mut Artifact> {
821        self.mod_local.artifacts.artifacts.get_mut(&id)
822    }
823
824    pub(crate) fn push_op(&mut self, op: Operation) {
825        let index = self.mod_local.artifacts.operations.len();
826        self.mod_local.artifacts.operations.push(op);
827        if let Some(operation) = self.mod_local.artifacts.operations.last().cloned()
828            && let Some(callbacks) = &self.execution_callbacks
829        {
830            callbacks.on_operation(OperationCallbackArgs {
831                module_id: self.mod_local.module_id,
832                operation,
833                index,
834            });
835        }
836    }
837
838    pub(crate) fn push_command(&mut self, command: ArtifactCommand) {
839        self.mod_local.artifacts.unprocessed_commands.push(command);
840    }
841
842    pub(super) fn next_module_id(&self) -> ModuleId {
843        ModuleId::from_usize(self.global.path_to_source_id.len())
844    }
845
846    pub(super) fn id_for_module(&self, path: &ModulePath) -> Option<ModuleId> {
847        self.global.path_to_source_id.get(path).cloned()
848    }
849
850    pub(super) fn add_path_to_source_id(&mut self, path: ModulePath, id: ModuleId) {
851        debug_assert!(!self.global.path_to_source_id.contains_key(&path));
852        self.global.path_to_source_id.insert(path, id);
853    }
854
855    pub(crate) fn add_root_module_contents(&mut self, program: &crate::Program) {
856        let root_id = ModuleId::default();
857        // Get the path for the root module.
858        let path = self
859            .global
860            .path_to_source_id
861            .iter()
862            .find(|(_, v)| **v == root_id)
863            .unwrap()
864            .0
865            .clone();
866        self.add_id_to_source(
867            root_id,
868            ModuleSource {
869                path,
870                source: program.original_file_contents.to_string(),
871            },
872        );
873    }
874
875    pub(super) fn add_id_to_source(&mut self, id: ModuleId, source: ModuleSource) {
876        self.global.id_to_source.insert(id, source);
877    }
878
879    pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
880        debug_assert!(self.global.path_to_source_id.contains_key(&path));
881        let module_info = ModuleInfo { id, repr, path };
882        self.global.module_infos.insert(id, module_info);
883    }
884
885    pub fn get_module(&mut self, id: ModuleId) -> Option<&ModuleInfo> {
886        self.global.module_infos.get(&id)
887    }
888
889    #[cfg(test)]
890    pub(crate) fn modules(&self) -> &ModuleInfoMap {
891        &self.global.module_infos
892    }
893
894    #[cfg(test)]
895    pub(crate) fn root_module_artifact_state(&self) -> &ModuleArtifactState {
896        &self.global.root_module_artifacts
897    }
898
899    /// Record metadata from a deprecated edge stdlib call for the Z0006 refactor.
900    ///
901    /// This is intentionally collected unconditionally when artifact graph support is enabled.
902    /// The temporary feature flag only controls whether the lint/action is shown in the app.
903    pub(crate) fn record_edge_refactor_meta(&mut self, meta: EdgeRefactorMeta) {
904        self.mod_local
905            .artifacts
906            .refactor_metadata
907            .push(RefactorMetadata::EdgeRefactor(Box::new(meta)));
908    }
909
910    pub(crate) fn record_pending_edge_refactor_meta(&mut self, meta: PendingEdgeRefactorMeta) {
911        self.mod_local.artifacts.pending_edge_refactor_metadata.push(meta);
912    }
913
914    pub(crate) fn pending_edge_refactor_meta(
915        &self,
916        edge_id: Uuid,
917        argument_source_range: SourceRange,
918    ) -> Option<PendingEdgeRefactorMeta> {
919        if let Some(pending) = self
920            .mod_local
921            .artifacts
922            .pending_edge_refactor_metadata
923            .iter()
924            .find(|meta| meta.edge_id == edge_id && argument_source_range.contains_range(&meta.source_range))
925        {
926            return Some(pending.clone());
927        }
928
929        // A helper assigned to a variable is outside the argument's source
930        // range. Fall back to the edge ID only when it identifies one helper.
931        let mut matches = self
932            .mod_local
933            .artifacts
934            .pending_edge_refactor_metadata
935            .iter()
936            .filter(|meta| meta.edge_id == edge_id);
937        let pending = matches.next()?.clone();
938        matches.next().is_none().then_some(pending)
939    }
940
941    pub(crate) fn record_edge_refactor_meta_from_pending(
942        &mut self,
943        edge_id: Uuid,
944        source_range: SourceRange,
945        face_ids: [Uuid; 2],
946    ) -> bool {
947        if self.mod_local.artifacts.refactor_metadata.iter().any(|meta| {
948            matches!(
949                meta,
950                RefactorMetadata::EdgeRefactor(meta)
951                    if meta.edge_id == edge_id && meta.source_range == source_range
952            )
953        }) {
954            return true;
955        }
956
957        let exact_pending_meta = self
958            .mod_local
959            .artifacts
960            .pending_edge_refactor_metadata
961            .iter()
962            .find(|meta| meta.edge_id == edge_id && meta.source_range == source_range)
963            .cloned();
964
965        let edge_pending_meta = || {
966            let mut matches = self
967                .mod_local
968                .artifacts
969                .pending_edge_refactor_metadata
970                .iter()
971                .filter(|meta| meta.edge_id == edge_id);
972            let pending_meta = matches.next()?.clone();
973            matches.next().is_none().then_some(pending_meta)
974        };
975
976        let Some(pending_meta) = exact_pending_meta.or_else(edge_pending_meta) else {
977            return false;
978        };
979
980        self.record_edge_refactor_meta(EdgeRefactorMeta {
981            edge_id,
982            face_ids,
983            end_face_ids: Vec::new(),
984            source_range: pending_meta.source_range,
985            stdlib_fn: pending_meta.stdlib_fn,
986        });
987
988        true
989    }
990
991    /// Record metadata from a fillet/chamfer call that used `tags` directly.
992    ///
993    /// This is intentionally collected unconditionally when artifact graph support is enabled.
994    /// The temporary feature flag only controls whether the lint/action is shown in the app.
995    pub(crate) fn record_direct_tag_fillet_meta(&mut self, meta: DirectTagFilletMeta) {
996        self.mod_local
997            .artifacts
998            .refactor_metadata
999            .push(RefactorMetadata::DirectTagFillet(meta));
1000    }
1001
1002    /// Refactor metadata collected when deprecated edge stdlib functions run (for tests and lint).
1003    pub fn edge_refactor_metadata(&self) -> Vec<EdgeRefactorMeta> {
1004        self.global
1005            .root_module_artifacts
1006            .refactor_metadata
1007            .iter()
1008            .filter_map(|m| match m {
1009                RefactorMetadata::EdgeRefactor(meta) => Some(meta.as_ref().clone()),
1010                RefactorMetadata::DirectTagFillet(_) => None,
1011            })
1012            .collect()
1013    }
1014
1015    /// Direct-tag fillet/chamfer metadata (for Z0006 code mod).
1016    pub fn direct_tag_fillet_metadata(&self) -> Vec<DirectTagFilletMeta> {
1017        self.global
1018            .root_module_artifacts
1019            .refactor_metadata
1020            .iter()
1021            .filter_map(|m| match m {
1022                RefactorMetadata::EdgeRefactor(_) => None,
1023                RefactorMetadata::DirectTagFillet(meta) => Some(meta.clone()),
1024            })
1025            .collect()
1026    }
1027
1028    pub fn current_default_units(&self) -> NumericType {
1029        NumericType::Default {
1030            len: self.length_unit(),
1031            angle: self.angle_unit(),
1032        }
1033    }
1034
1035    pub fn length_unit(&self) -> UnitLength {
1036        self.mod_local.settings.default_length_units
1037    }
1038
1039    pub fn angle_unit(&self) -> UnitAngle {
1040        self.mod_local.settings.default_angle_units
1041    }
1042
1043    pub(super) fn circular_import_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError {
1044        KclError::new_import_cycle(KclErrorDetails::new(
1045            format!(
1046                "circular import of modules is not allowed: {} -> {}",
1047                self.global
1048                    .mod_loader
1049                    .import_stack
1050                    .iter()
1051                    .map(|p| p.to_string_lossy())
1052                    .collect::<Vec<_>>()
1053                    .join(" -> "),
1054                path,
1055            ),
1056            vec![source_range],
1057        ))
1058    }
1059
1060    pub(crate) fn pipe_value(&self) -> Option<&KclValue> {
1061        self.mod_local.pipe_value.as_ref()
1062    }
1063
1064    pub(crate) fn error_with_outputs(
1065        &self,
1066        error: KclError,
1067        main_ref: Option<EnvironmentRef>,
1068        default_planes: Option<DefaultPlanes>,
1069    ) -> KclErrorWithOutputs {
1070        let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = self
1071            .global
1072            .path_to_source_id
1073            .iter()
1074            .map(|(k, v)| ((*v), k.clone()))
1075            .collect();
1076
1077        KclErrorWithOutputs::new(
1078            error,
1079            self.issues().to_vec(),
1080            main_ref
1081                .and_then(|main_ref| self.mod_local.variables(main_ref).ok())
1082                .unwrap_or_default(),
1083            self.global.operations_by_module(),
1084            Default::default(),
1085            self.global.artifacts.graph.clone(),
1086            self.global.root_module_artifacts.scene_objects.clone(),
1087            self.global.root_module_artifacts.source_range_to_object.clone(),
1088            self.global.root_module_artifacts.var_solutions.clone(),
1089            self.global.root_module_artifacts.refactor_metadata.clone(),
1090            module_id_to_module_path,
1091            self.global.id_to_source.clone(),
1092            default_planes,
1093        )
1094    }
1095
1096    pub(crate) fn build_program_lookup(
1097        &self,
1098        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1099    ) -> ProgramLookup {
1100        ProgramLookup::new(current, self.global.module_infos.clone())
1101    }
1102
1103    pub(crate) async fn build_artifact_graph(
1104        &mut self,
1105        engine: &Arc<EngineManager>,
1106        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1107    ) -> Result<(), KclError> {
1108        let mut new_commands = Vec::new();
1109        let mut new_exec_artifacts = IndexMap::new();
1110        for module in self.global.module_infos.values_mut() {
1111            match &mut module.repr {
1112                ModuleRepr::Kcl(_, Some(outcome)) => {
1113                    new_commands.extend(outcome.artifacts.process_commands());
1114                    new_exec_artifacts.extend(outcome.artifacts.artifacts.clone());
1115                }
1116                ModuleRepr::Foreign(_, Some((_, module_artifacts))) => {
1117                    new_commands.extend(module_artifacts.process_commands());
1118                    new_exec_artifacts.extend(module_artifacts.artifacts.clone());
1119                }
1120                ModuleRepr::Root | ModuleRepr::Kcl(_, None) | ModuleRepr::Foreign(_, None) | ModuleRepr::Dummy => {}
1121            }
1122        }
1123        // Take from the module artifacts so that we don't try to process them
1124        // again next time due to execution caching.
1125        new_commands.extend(self.global.root_module_artifacts.process_commands());
1126        // Note: These will get re-processed, but since we're just adding them
1127        // to a map, it's fine.
1128        new_exec_artifacts.extend(self.global.root_module_artifacts.artifacts.clone());
1129        let new_responses = engine.take_responses().await;
1130
1131        // Move the artifacts into ExecState global to simplify cache
1132        // management.
1133        for (id, exec_artifact) in new_exec_artifacts {
1134            // Only insert if it wasn't already present. We don't want to
1135            // overwrite what was previously there. We haven't filled in node
1136            // paths yet.
1137            self.global.artifacts.artifacts.entry(id).or_insert(exec_artifact);
1138        }
1139
1140        let initial_graph = self.global.artifacts.graph.clone();
1141
1142        // Build the artifact graph.
1143        let programs = self.build_program_lookup(program.clone());
1144        let graph_result = crate::execution::artifact::build_artifact_graph(
1145            &new_commands,
1146            &new_responses,
1147            program,
1148            &mut self.global.artifacts.artifacts,
1149            initial_graph,
1150            &programs,
1151            &self.global.module_infos,
1152        );
1153
1154        #[cfg(feature = "snapshot-engine-responses")]
1155        {
1156            // Store engine responses for debugging.
1157            self.global.root_module_artifacts.responses.extend(new_responses);
1158        }
1159
1160        let artifact_graph = graph_result?;
1161        self.global.artifacts.graph = artifact_graph;
1162
1163        Ok(())
1164    }
1165
1166    pub(crate) fn kcl_version(&self) -> KclVersion {
1167        self.mod_local.settings.kcl_version.parse().unwrap_or_default()
1168    }
1169}
1170
1171#[derive(Default)]
1172pub enum KclVersion {
1173    #[default]
1174    V1,
1175    V2,
1176}
1177
1178impl FromStr for KclVersion {
1179    type Err = KclError;
1180
1181    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
1182        match s {
1183            "1" | "1.0" | "1.0.0" => Ok(Self::V1),
1184            "2" | "2.0" | "2.0.0" => Ok(Self::V2),
1185            other => Err(KclError::new_semantic(KclErrorDetails {
1186                source_ranges: Default::default(),
1187                backtrace: Default::default(),
1188                message: format!("Unrecognized version {other}. Valid versions are 1.0 and 2.0"),
1189            })),
1190        }
1191    }
1192}
1193
1194impl GlobalState {
1195    fn new(settings: &ExecutorSettings, segment_ids_edited: AhashIndexSet<ObjectId>) -> Self {
1196        let mut global = GlobalState {
1197            path_to_source_id: Default::default(),
1198            module_infos: Default::default(),
1199            artifacts: Default::default(),
1200            root_module_artifacts: Default::default(),
1201            mod_loader: Default::default(),
1202            issues: Default::default(),
1203            id_to_source: Default::default(),
1204            segment_ids_edited,
1205            drag_anchors: Vec::new(),
1206            sketch_mode: false,
1207        };
1208
1209        let root_id = ModuleId::default();
1210        let root_path = settings.current_file.clone().unwrap_or_default();
1211        global.module_infos.insert(
1212            root_id,
1213            ModuleInfo {
1214                id: root_id,
1215                path: ModulePath::Local {
1216                    value: root_path.clone(),
1217                    original_import_path: None,
1218                },
1219                repr: ModuleRepr::Root,
1220            },
1221        );
1222        global.path_to_source_id.insert(
1223            ModulePath::Local {
1224                value: root_path,
1225                original_import_path: None,
1226            },
1227            root_id,
1228        );
1229        global
1230    }
1231
1232    pub(super) fn filenames(&self) -> IndexMap<ModuleId, ModulePath> {
1233        self.path_to_source_id.iter().map(|(k, v)| ((*v), k.clone())).collect()
1234    }
1235
1236    pub(super) fn get_source(&self, id: ModuleId) -> Option<&ModuleSource> {
1237        self.id_to_source.get(&id)
1238    }
1239}
1240
1241impl ArtifactState {
1242    pub fn cached_body_items(&self) -> usize {
1243        self.graph.item_count()
1244    }
1245
1246    pub(crate) fn clear(&mut self) {
1247        self.artifacts.clear();
1248        self.graph.clear();
1249    }
1250}
1251
1252impl ModuleArtifactState {
1253    pub(crate) fn clear(&mut self) {
1254        self.artifacts.clear();
1255        self.unprocessed_commands.clear();
1256        self.commands.clear();
1257        self.operations.clear();
1258        self.refactor_metadata.clear();
1259    }
1260
1261    pub(crate) fn restore_scene_objects(&mut self, scene_objects: &[Object]) {
1262        self.scene_objects = scene_objects.to_vec();
1263        self.object_id_generator = IncIdGenerator::new(self.scene_objects.len());
1264        self.source_range_to_object.clear();
1265        self.artifact_id_to_scene_object.clear();
1266
1267        for (expected_id, object) in self.scene_objects.iter().enumerate() {
1268            debug_assert_eq!(
1269                object.id.0, expected_id,
1270                "Restored cached scene object ID {} does not match its position {}",
1271                object.id.0, expected_id
1272            );
1273
1274            match &object.kind {
1275                ObjectKind::Wall(wall) => {
1276                    self.source_range_to_object.insert(wall.source.solid.range, object.id);
1277                }
1278                ObjectKind::Cap(cap) => {
1279                    self.source_range_to_object.insert(cap.source.solid.range, object.id);
1280                }
1281                _ => match &object.source {
1282                    crate::front::SourceRef::Simple { range, node_path: _ } => {
1283                        self.source_range_to_object.insert(*range, object.id);
1284                    }
1285                    crate::front::SourceRef::BackTrace { ranges } => {
1286                        // Don't map the entire backtrace, only the most specific
1287                        // range.
1288                        if let Some((range, _)) = ranges.first() {
1289                            self.source_range_to_object.insert(*range, object.id);
1290                        }
1291                    }
1292                },
1293            }
1294
1295            // Ignore placeholder artifacts.
1296            if object.artifact_id != ArtifactId::placeholder() {
1297                self.artifact_id_to_scene_object.insert(object.artifact_id, object.id);
1298            }
1299        }
1300    }
1301
1302    /// When self is a cached state, extend it with new state.
1303    pub(crate) fn extend(&mut self, other: ModuleArtifactState) {
1304        self.artifacts.extend(other.artifacts);
1305        self.unprocessed_commands.extend(other.unprocessed_commands);
1306        self.commands.extend(other.commands);
1307        self.operations.extend(other.operations);
1308        if other.scene_objects.len() > self.scene_objects.len() {
1309            self.scene_objects
1310                .extend(other.scene_objects[self.scene_objects.len()..].iter().cloned());
1311        }
1312        self.source_range_to_object.extend(other.source_range_to_object);
1313        self.artifact_id_to_scene_object
1314            .extend(other.artifact_id_to_scene_object);
1315        self.var_solutions.extend(other.var_solutions);
1316        self.refactor_metadata.extend(other.refactor_metadata);
1317    }
1318
1319    // Move unprocessed artifact commands so that we don't try to process them
1320    // again next time due to execution caching.  Returns a clone of the
1321    // commands that were moved.
1322    pub(crate) fn process_commands(&mut self) -> Vec<ArtifactCommand> {
1323        let unprocessed = std::mem::take(&mut self.unprocessed_commands);
1324        let new_module_commands = unprocessed.clone();
1325        self.commands.extend(unprocessed);
1326        new_module_commands
1327    }
1328
1329    pub(crate) fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
1330        debug_assert!(
1331            id.0 < self.scene_objects.len(),
1332            "Requested object ID {} but only have {} objects",
1333            id.0,
1334            self.scene_objects.len()
1335        );
1336        self.scene_objects.get(id.0)
1337    }
1338
1339    pub(crate) fn scene_object_by_id_mut(&mut self, id: ObjectId) -> Option<&mut Object> {
1340        debug_assert!(
1341            id.0 < self.scene_objects.len(),
1342            "Requested object ID {} but only have {} objects",
1343            id.0,
1344            self.scene_objects.len()
1345        );
1346        self.scene_objects.get_mut(id.0)
1347    }
1348}
1349
1350impl ModuleState {
1351    pub(super) fn new(
1352        path: ModulePath,
1353        memory: Arc<ProgramMemory>,
1354        module_id: Option<ModuleId>,
1355        sketch_mode: bool,
1356        freedom_analysis: bool,
1357    ) -> Self {
1358        let state_module_id = module_id.unwrap_or_default();
1359        ModuleState {
1360            module_id: state_module_id,
1361            id_generator: IdGenerator::new(module_id),
1362            stack: memory.new_stack(),
1363            call_stack_size: 0,
1364            pipe_value: Default::default(),
1365            being_declared: Default::default(),
1366            sketch_block: Default::default(),
1367            stdlib_entry_source_range: Default::default(),
1368            module_exports: Default::default(),
1369            explicit_length_units: false,
1370            path,
1371            settings: Default::default(),
1372            sketch_mode,
1373            freedom_analysis,
1374            artifacts: Default::default(),
1375            constraint_state: Default::default(),
1376            allowed_warnings: Vec::new(),
1377            denied_warnings: Vec::new(),
1378            consumed_solids: AHashMap::default(),
1379            consumed_solid_ids: AHashMap::default(),
1380            inside_stdlib: false,
1381        }
1382    }
1383
1384    pub(super) fn variables(&self, main_ref: EnvironmentRef) -> Result<IndexMap<String, KclValue>, KclError> {
1385        self.stack.find_all_in_env_owned(main_ref)
1386    }
1387}
1388
1389impl SketchBlockState {
1390    pub(crate) fn next_sketch_var_id(&self) -> SketchVarId {
1391        SketchVarId(self.sketch_vars.len())
1392    }
1393
1394    /// Given a solve outcome, return the solutions for the sketch variables and
1395    /// enough information to update them in the source.
1396    pub(crate) fn var_solutions(
1397        &self,
1398        solve_outcome: &Solved,
1399        solution_ty: NumericType,
1400        sketch_block_range: SourceRange,
1401    ) -> Result<Vec<(SourceRange, Option<NodePath>, Number)>, KclError> {
1402        self.sketch_vars
1403            .iter()
1404            .map(|v| {
1405                let Some(sketch_var) = v.as_sketch_var() else {
1406                    return Err(KclError::new_internal(KclErrorDetails::new(
1407                        "Expected sketch variable".to_owned(),
1408                        vec![sketch_block_range],
1409                    )));
1410                };
1411                let var_index = sketch_var.id.0;
1412                let solved_n = solve_outcome.final_values.get(var_index).ok_or_else(|| {
1413                    let message = format!("No solution for sketch variable with id {}", var_index);
1414                    debug_assert!(false, "{}", &message);
1415                    KclError::new_internal(KclErrorDetails::new(
1416                        message,
1417                        sketch_var.meta.iter().map(|m| m.source_range).collect(),
1418                    ))
1419                })?;
1420                let solved_value = Number {
1421                    value: *solved_n,
1422                    units: solution_ty.try_into().map_err(|_| {
1423                        KclError::new_internal(KclErrorDetails::new(
1424                            "Failed to convert numeric type to units".to_owned(),
1425                            vec![sketch_block_range],
1426                        ))
1427                    })?,
1428                };
1429                let Some(source_range) = sketch_var.meta.first().map(|m| m.source_range) else {
1430                    return Ok(None);
1431                };
1432                Ok(Some((source_range, sketch_var.node_path.clone(), solved_value)))
1433            })
1434            .filter_map(Result::transpose)
1435            .collect::<Result<Vec<_>, KclError>>()
1436    }
1437}
1438
1439#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
1440#[ts(export)]
1441#[serde(rename_all = "camelCase")]
1442pub struct MetaSettings {
1443    pub default_length_units: UnitLength,
1444    pub default_angle_units: UnitAngle,
1445    pub experimental_features: annotations::WarningLevel,
1446    pub kcl_version: String,
1447}
1448
1449impl Default for MetaSettings {
1450    fn default() -> Self {
1451        MetaSettings {
1452            default_length_units: UnitLength::Millimeters,
1453            default_angle_units: UnitAngle::Degrees,
1454            experimental_features: annotations::WarningLevel::Deny,
1455            kcl_version: "1.0".to_owned(),
1456        }
1457    }
1458}
1459
1460impl MetaSettings {
1461    pub(crate) fn update_from_annotation(
1462        &mut self,
1463        annotation: &crate::parsing::ast::types::Node<Annotation>,
1464    ) -> Result<(bool, bool), KclError> {
1465        let properties = annotations::expect_properties(annotations::SETTINGS, annotation)?;
1466
1467        let mut updated_len = false;
1468        let mut updated_angle = false;
1469        for p in properties {
1470            match &*p.inner.key.name {
1471                annotations::SETTINGS_UNIT_LENGTH => {
1472                    let value = annotations::expect_ident(&p.inner.value)?;
1473                    let value = super::types::length_from_str(value, annotation.as_source_range())?;
1474                    self.default_length_units = value;
1475                    updated_len = true;
1476                }
1477                annotations::SETTINGS_UNIT_ANGLE => {
1478                    let value = annotations::expect_ident(&p.inner.value)?;
1479                    let value = super::types::angle_from_str(value, annotation.as_source_range())?;
1480                    self.default_angle_units = value;
1481                    updated_angle = true;
1482                }
1483                annotations::SETTINGS_VERSION => {
1484                    let value = annotations::expect_number(&p.inner.value)?;
1485                    self.kcl_version = value;
1486                }
1487                annotations::SETTINGS_EXPERIMENTAL_FEATURES => {
1488                    let value = annotations::expect_ident(&p.inner.value)?;
1489                    let value = annotations::WarningLevel::from_str(value).map_err(|_| {
1490                        KclError::new_semantic(KclErrorDetails::new(
1491                            format!(
1492                                "Invalid value for {} settings property, expected one of: {}",
1493                                annotations::SETTINGS_EXPERIMENTAL_FEATURES,
1494                                annotations::WARN_LEVELS.join(", ")
1495                            ),
1496                            annotation.as_source_ranges(),
1497                        ))
1498                    })?;
1499                    self.experimental_features = value;
1500                }
1501                name => {
1502                    return Err(KclError::new_semantic(KclErrorDetails::new(
1503                        format!(
1504                            "Unexpected settings key: `{name}`; expected one of `{}`, `{}`",
1505                            annotations::SETTINGS_UNIT_LENGTH,
1506                            annotations::SETTINGS_UNIT_ANGLE
1507                        ),
1508                        vec![annotation.as_source_range()],
1509                    )));
1510                }
1511            }
1512        }
1513
1514        Ok((updated_len, updated_angle))
1515    }
1516}
1517
1518#[cfg(test)]
1519mod tests {
1520    use uuid::Uuid;
1521
1522    use super::ModuleArtifactState;
1523    use crate::NodePath;
1524    use crate::NodePathExt;
1525    use crate::SourceRange;
1526    use crate::execution::ArtifactId;
1527    use crate::front::Object;
1528    use crate::front::ObjectId;
1529    use crate::front::ObjectKind;
1530    use crate::front::Plane;
1531    use crate::front::SourceRef;
1532
1533    #[test]
1534    fn restore_scene_objects_rebuilds_lookup_maps() {
1535        let plane_artifact_id = ArtifactId::new(Uuid::from_u128(1));
1536        let sketch_artifact_id = ArtifactId::new(Uuid::from_u128(2));
1537        let plane_range = SourceRange::from([1, 4, 0]);
1538        let plane_node_path = Some(NodePath::placeholder());
1539        let sketch_ranges = vec![
1540            (SourceRange::from([5, 9, 0]), None),
1541            (SourceRange::from([10, 12, 0]), None),
1542        ];
1543        let cached_objects = vec![
1544            Object {
1545                id: ObjectId(0),
1546                kind: ObjectKind::Plane(Plane::Object(ObjectId(0))),
1547                label: Default::default(),
1548                comments: Default::default(),
1549                artifact_id: plane_artifact_id,
1550                source: SourceRef::new(plane_range, plane_node_path),
1551            },
1552            Object {
1553                id: ObjectId(1),
1554                kind: ObjectKind::Nil,
1555                label: Default::default(),
1556                comments: Default::default(),
1557                artifact_id: sketch_artifact_id,
1558                source: SourceRef::BackTrace {
1559                    ranges: sketch_ranges.clone(),
1560                },
1561            },
1562            Object::placeholder(ObjectId(2), SourceRange::from([13, 14, 0]), None),
1563        ];
1564
1565        let mut artifacts = ModuleArtifactState::default();
1566        artifacts.restore_scene_objects(&cached_objects);
1567
1568        assert_eq!(artifacts.scene_objects, cached_objects);
1569        assert_eq!(
1570            artifacts.artifact_id_to_scene_object.get(&plane_artifact_id),
1571            Some(&ObjectId(0))
1572        );
1573        assert_eq!(
1574            artifacts.artifact_id_to_scene_object.get(&sketch_artifact_id),
1575            Some(&ObjectId(1))
1576        );
1577        assert_eq!(
1578            artifacts.artifact_id_to_scene_object.get(&ArtifactId::placeholder()),
1579            None
1580        );
1581        assert_eq!(artifacts.source_range_to_object.get(&plane_range), Some(&ObjectId(0)));
1582        assert_eq!(
1583            artifacts.source_range_to_object.get(&sketch_ranges[0].0),
1584            Some(&ObjectId(1))
1585        );
1586        // We don't map all the ranges in a backtrace.
1587        assert_eq!(artifacts.source_range_to_object.get(&sketch_ranges[1].0), None);
1588    }
1589}