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::Number;
9use kcl_api::Object;
10use kcl_api::ObjectId;
11use kcl_api::ObjectKind;
12use kcl_api::UnitAngle;
13use kcl_api::UnitLength;
14pub use kittycad_modeling_cmds::KclVersion;
15use serde::Deserialize;
16use serde::Serialize;
17use uuid::Uuid;
18
19use crate::CompilationIssue;
20use crate::ExecutorContext;
21use crate::KclErrorWithOutputs;
22use crate::MockConfig;
23use crate::NodePath;
24use crate::SegmentDragAnchor;
25use crate::SourceRange;
26use crate::collections::AhashIndexSet;
27use crate::engine::engine_manager::EngineManager;
28use crate::errors::KclError;
29use crate::errors::KclErrorDetails;
30use crate::errors::Severity;
31use crate::exec::DefaultPlanes;
32use crate::execution::Artifact;
33use crate::execution::ArtifactCommand;
34use crate::execution::ArtifactGraph;
35use crate::execution::ArtifactId;
36use crate::execution::ConstrainableLine2d;
37use crate::execution::EnvironmentRef;
38use crate::execution::ExecOutcome;
39use crate::execution::ExecutorSettings;
40use crate::execution::KclValue;
41use crate::execution::KclValueView;
42use crate::execution::OperationCallbackArgs;
43use crate::execution::OperationsByModule;
44use crate::execution::ProgramLookup;
45use crate::execution::SketchVarId;
46use crate::execution::UnsolvedSegment;
47use crate::execution::annotations;
48use crate::execution::cad_op::Operation;
49use crate::execution::id_generator::IdGenerator;
50#[cfg(test)]
51use crate::execution::memory::MemoryBackendKind;
52use crate::execution::memory::ProgramMemory;
53use crate::execution::memory::Stack;
54use crate::execution::sketch_solve::Solved;
55use crate::execution::types::NumericType;
56use crate::id::IncIdGenerator;
57use crate::modules::ModuleId;
58use crate::modules::ModuleInfo;
59use crate::modules::ModuleLoader;
60use crate::modules::ModulePath;
61use crate::modules::ModulePathExt as _;
62use crate::modules::ModuleRepr;
63use crate::modules::ModuleSource;
64use crate::parsing::ast::types::Annotation;
65use crate::parsing::ast::types::Node;
66use crate::parsing::ast::types::NodeRef;
67use crate::parsing::ast::types::Program;
68use crate::parsing::ast::types::TagNode;
69
70/// State for executing a program.
71#[derive(Debug, Clone)]
72pub struct ExecState {
73    pub(super) execution_callbacks: Option<std::sync::Arc<dyn crate::execution::ExecutionCallbacks>>,
74    pub(super) global: GlobalState,
75    pub(super) mod_local: ModuleState,
76}
77
78pub type ModuleInfoMap = IndexMap<ModuleId, ModuleInfo>;
79
80#[derive(Debug, Clone)]
81pub(super) struct GlobalState {
82    /// The deepest machine-executor call depth reached by executions sharing
83    /// this state: the root module, its callbacks, and module bodies executed
84    /// inline on it. Imported modules pre-executed in parallel run on cloned
85    /// state whose counter is dropped, so their depths are not aggregated
86    /// here. Used to survey real-world depth against the runaway guard's
87    /// limit; see `machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT`.
88    pub(crate) machine_depth_high_water: usize,
89    /// Map from source file absolute path to module ID.
90    pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
91    /// Map from module ID to source file.
92    pub id_to_source: IndexMap<ModuleId, ModuleSource>,
93    /// Map from module ID to module info.
94    pub module_infos: ModuleInfoMap,
95    /// `never` type uses in local imports, retained until their versions are validated.
96    pub never_type_ranges: IndexMap<ModuleId, Vec<SourceRange>>,
97    /// Module loader.
98    pub mod_loader: ModuleLoader,
99    /// Errors and warnings.
100    pub issues: Vec<CompilationIssue>,
101    /// If set, use this version only when deciding whether to emit
102    /// `deprecated_since` warnings. Runtime behavior still uses the version
103    /// declared by the KCL program.
104    pub deprecation_version_override: Option<String>,
105    /// The entry-point (root) module's declared kclVersion, or `None` when it
106    /// declares none. When this is KCL 3.0 or later, this single version
107    /// governs version-conditional runtime behavior for the whole execution --
108    /// every module and every function body. Otherwise (1.0, 2.0, or
109    /// undeclared) the legacy per-module lookup and its caller-version quirk
110    /// apply, see [`ExecState::legacy_caller_kcl_version`], and no imported
111    /// file may declare KCL 3.0 or later, see
112    /// [`ExecState::check_imported_module_kcl_version`]. Assigned
113    /// unconditionally at the start of every execution.
114    pub entry_point_kcl_version: Option<KclVersion>,
115    /// Global artifacts that represent the entire program.
116    pub artifacts: ArtifactState,
117    /// Artifacts for only the root module.
118    pub root_module_artifacts: ModuleArtifactState,
119    /// The segments that were edited that triggered this execution.
120    pub segment_ids_edited: AhashIndexSet<ObjectId>,
121    /// Segment-body drag anchors that temporarily pull a point on a segment toward the cursor.
122    pub drag_anchors: Vec<SegmentDragAnchor>,
123    /// True if this execution is sketch mode execution, executing a single
124    /// sketch block. Unlike [`ModuleState::sketch_mode`], this is constant for
125    /// the entire execution, including while executing the body of the sketch
126    /// block being edited.
127    pub sketch_mode: bool,
128    /// True when the engine being used for execution is CPU only with no graphical environment
129    pub geometry_only: bool,
130    /// Std's exported declarations skipped as not yet added. Set once by
131    /// `eval_prelude`, since root environments reach the prelude without an
132    /// import statement; the mock memory cache carries it like `module_infos`.
133    pub std_not_yet_added: IndexMap<String, NotYetAdded>,
134    /// Test-only: explain missing names for user declarations too, so tests
135    /// can cover the hint path while std declares nothing gated.
136    #[cfg(test)]
137    pub(crate) hint_all_not_yet_added: bool,
138}
139
140/// A declaration skipped because the program's KCL version predates its
141/// `added_in`, kept so a failed lookup of the name can explain why. Records
142/// flow like names: module scope, the module's outcome if exported, glob imports.
143#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
144pub struct NotYetAdded {
145    /// The KCL version the declaration is available from.
146    pub added_in: annotations::VersionConstraint,
147    /// Whether the declaration belongs to std. Only std declarations get the hint.
148    pub is_std: bool,
149}
150
151/// A record in a module's scope and whether the module exports it, by its own
152/// `export` or an `export import *`. Only exported records leave in the outcome.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub(crate) struct ScopedNotYetAdded {
155    pub item: NotYetAdded,
156    pub exported: bool,
157}
158
159impl GlobalState {
160    pub(crate) fn operations_by_module(&self) -> OperationsByModule {
161        let mut operations = OperationsByModule::default();
162        operations.insert(ModuleId::default(), self.root_module_artifacts.operations.clone());
163
164        for (module_id, module_info) in &self.module_infos {
165            match &module_info.repr {
166                ModuleRepr::Root => {}
167                ModuleRepr::Kcl(_, Some(outcome)) => {
168                    operations.insert(*module_id, outcome.artifacts.operations.clone());
169                }
170                ModuleRepr::Foreign(_, Some((_, artifacts))) => {
171                    operations.insert(*module_id, artifacts.operations.clone());
172                }
173                ModuleRepr::Kcl(_, None) | ModuleRepr::Foreign(_, None) | ModuleRepr::Dummy => {}
174            }
175        }
176
177        operations
178    }
179}
180
181#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
182pub(crate) enum ConstraintKey {
183    LineCircle([usize; 10]),
184    CircleCircle([usize; 12]),
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub(crate) enum TangencyMode {
189    LineCircle(ezpz::LineSide),
190    CircleCircle(ezpz::CircleSide),
191}
192
193#[derive(Debug, Clone, Copy, PartialEq, Eq)]
194pub(crate) enum ConstraintState {
195    Tangency(TangencyMode),
196}
197
198#[derive(Debug, Clone, Default)]
199pub(super) struct ArtifactState {
200    /// Internal map of UUIDs to exec artifacts.  This needs to persist across
201    /// executions to allow the graph building to refer to cached artifacts.
202    pub artifacts: IndexMap<ArtifactId, Artifact>,
203    /// Output artifact graph.
204    pub graph: ArtifactGraph,
205}
206
207/// Which stdlib edge function produced this refactor metadata (for lint/code mod).
208#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
209#[ts(export)]
210#[serde(rename_all = "camelCase")]
211pub enum EdgeRefactorStdlibFn {
212    GetOppositeEdge,
213    GetNextAdjacentEdge,
214    GetPreviousAdjacentEdge,
215    GetCommonEdge,
216    EdgeId,
217    DirectEdgeTag,
218}
219
220/// Metadata collected when a deprecated edge stdlib function runs, for refactor-to-edgeRefs lint/code mod.
221#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
222#[ts(export)]
223#[serde(rename_all = "camelCase")]
224pub struct EdgeRefactorMeta {
225    pub edge_id: Uuid,
226    pub face_ids: [Uuid; 2],
227    #[serde(default, skip_serializing_if = "Vec::is_empty")]
228    pub end_face_ids: Vec<Uuid>,
229    pub source_range: SourceRange,
230    pub stdlib_fn: EdgeRefactorStdlibFn,
231}
232
233/// Metadata for a deprecated edge stdlib function whose edge ID was resolved,
234/// but whose adjacent face IDs could not be recorded at the helper callsite.
235#[derive(Debug, Clone, PartialEq, Eq)]
236pub(crate) struct PendingEdgeRefactorMeta {
237    pub edge_id: Uuid,
238    pub source_range: SourceRange,
239    pub stdlib_fn: EdgeRefactorStdlibFn,
240}
241
242/// One tag entry in a fillet/chamfer call that used `tags` directly (for refactor to edgeRefs).
243#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
244#[ts(export)]
245#[serde(rename_all = "camelCase")]
246pub struct DirectTagFilletTagEntry {
247    pub tag_identifier: String,
248    pub edge_id: Uuid,
249    pub face_ids: [Uuid; 2],
250}
251
252/// Metadata for one fillet/chamfer call that used `tags` directly (no stdlib call).
253#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
254#[ts(export)]
255#[serde(rename_all = "camelCase")]
256pub struct DirectTagFilletMeta {
257    pub call_source_range: SourceRange,
258    pub tags: Vec<DirectTagFilletTagEntry>,
259}
260
261/// Information needed to rewrite one legacy `angle` call while preserving its
262/// currently solved directed-angle branch.
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
264#[ts(export)]
265#[serde(rename_all = "camelCase")]
266pub struct LegacyAngleRefactorMeta {
267    pub source_range: SourceRange,
268    pub sector: u8,
269    pub inverse: bool,
270}
271
272/// Unified metadata stream for Z0006 and future execution-backed refactors.
273#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
274#[ts(export)]
275#[serde(tag = "kind", content = "data", rename_all = "camelCase")]
276pub enum RefactorMetadata {
277    EdgeRefactor(Box<EdgeRefactorMeta>),
278    DirectTagFillet(DirectTagFilletMeta),
279    LegacyAngle(LegacyAngleRefactorMeta),
280}
281
282#[derive(Debug, Clone)]
283pub(crate) struct PendingLegacyAngleRefactorMeta {
284    pub source_range: SourceRange,
285    pub lines: [ConstrainableLine2d; 2],
286    pub desired_angle_radians: f64,
287}
288
289/// Artifact state for a single module.
290#[derive(Debug, Clone, Default, PartialEq, Serialize)]
291pub struct ModuleArtifactState {
292    /// Internal map of UUIDs to exec artifacts.
293    pub artifacts: IndexMap<ArtifactId, Artifact>,
294    /// Outgoing engine commands that have not yet been processed and integrated
295    /// into the artifact graph.
296    #[serde(skip)]
297    pub unprocessed_commands: Vec<ArtifactCommand>,
298    /// Outgoing engine commands.
299    pub commands: Vec<ArtifactCommand>,
300    /// Incoming engine commands.
301    #[cfg(feature = "snapshot-engine-responses")]
302    pub responses: IndexMap<Uuid, kittycad_modeling_cmds::websocket::WebSocketResponse>,
303    /// Operations that have been performed in execution order, for display in
304    /// the Feature Tree.
305    pub operations: Vec<Operation>,
306    /// [`ObjectId`] generator.
307    pub object_id_generator: IncIdGenerator<usize>,
308    /// Objects in the scene, created from execution.
309    pub scene_objects: Vec<Object>,
310    /// Map from source range to object ID for lookup of objects by their source
311    /// range.
312    pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
313    /// Map from artifact ID to object ID in the scene.
314    pub artifact_id_to_scene_object: IndexMap<ArtifactId, ObjectId>,
315    /// Solutions for sketch variables.
316    pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
317    /// Metadata collected during execution for refactor lint/code-mod paths (Z0006 and future).
318    pub refactor_metadata: Vec<RefactorMetadata>,
319    /// Deprecated edge helper callsites that may be completed by a downstream
320    /// operation that knows the target solid.
321    #[serde(skip)]
322    pub(crate) pending_edge_refactor_metadata: Vec<PendingEdgeRefactorMeta>,
323}
324
325#[derive(Debug, Clone)]
326pub(super) struct ModuleState {
327    /// The id of this module.
328    pub module_id: ModuleId,
329    /// The id generator for this module.
330    pub id_generator: IdGenerator,
331    pub stack: Stack,
332    /// The size of the call stack. This is used to prevent stack overflows with
333    /// recursive function calls. In general, this doesn't match `stack`'s size
334    /// since it's conservative in reclaiming frames between executions.
335    pub(super) call_stack_size: usize,
336    /// Live call depth of the machine executor within this module, for its
337    /// runaway-recursion guard. The machine's analog of `call_stack_size`.
338    pub(crate) machine_call_depth: usize,
339    /// The current value of the pipe operator returned from the previous
340    /// expression.  If we're not currently in a pipeline, this will be None.
341    pub pipe_value: Option<KclValue>,
342    /// The closest variable declaration being executed in any parent node in the AST.
343    /// This is used to provide better error messages, e.g. noticing when the user is trying
344    /// to use the variable `length` inside the RHS of its own definition, like `length = tan(length)`.
345    pub being_declared: Option<String>,
346    /// Present if we're currently executing inside a sketch block.
347    pub sketch_block: Option<SketchBlockState>,
348    /// Tracks if KCL being executed is currently inside a stdlib function or not.
349    /// This matters because e.g. we shouldn't emit artifacts from declarations declared inside a stdlib function.
350    pub inside_stdlib: bool,
351    /// The source range where we entered the standard library.
352    pub stdlib_entry_source_range: Option<SourceRange>,
353    /// Identifiers that have been exported from the current module.
354    pub module_exports: Vec<String>,
355    /// Not-yet-added records in scope here: this module's own plus those from
356    /// glob imports. Only the exported ones travel in the module's outcome.
357    pub not_yet_added: IndexMap<String, ScopedNotYetAdded>,
358    /// Settings specified from annotations.
359    pub settings: MetaSettings,
360    /// True if executing in sketch mode. Only a single sketch block will be
361    /// executed. All other code is ignored.
362    pub sketch_mode: bool,
363    /// True to do more costly analysis of whether the sketch block segments are
364    /// under-constrained. The only time we disable this is when a user is
365    /// dragging segments.
366    pub freedom_analysis: bool,
367    pub(super) explicit_length_units: bool,
368    pub(super) path: ModulePath,
369    /// Artifacts for only this module.
370    pub artifacts: ModuleArtifactState,
371    /// Sticky per-constraint state persisted across sketch-mode mock solves.
372    /// Maps from sketch block ID to a map for that sketch.
373    /// Then the inner map is per constraint (in that sketch block) to its state.
374    pub constraint_state: IndexMap<ObjectId, IndexMap<ConstraintKey, ConstraintState>>,
375
376    pub(super) allowed_warnings: Vec<&'static str>,
377    pub(super) denied_warnings: Vec<&'static str>,
378
379    /// Map from consumed solid values to information about the operation that
380    /// consumed them. Populated by operations that destroy their inputs so that
381    /// subsequent attempts to use a consumed solid produce a clear KCL-level
382    /// error rather than a cryptic engine error.
383    pub(super) consumed_solids: AHashMap<ConsumedSolidKey, ConsumedSolidInfo>,
384    /// Defensive map from consumed engine UUID to consumption info.
385    /// Rust code may create a `Solid` with a consumed `engine_id` and a
386    /// different `instance_id` that was not recorded in `consumed_solids`. When
387    /// the exact key lookup misses, this map lets us reject that solid by
388    /// `engine_id`, unless the key is a recorded operation output.
389    pub(super) consumed_solid_ids: AHashMap<Uuid, ConsumedSolidInfo>,
390    /// Region engine UUIDs consumed by successful modeling operations. Regions
391    /// use the KCL `Sketch` representation, so this state keeps stale Region
392    /// values from reaching an engine object that has become something else.
393    pub(super) consumed_regions: AHashMap<Uuid, ConsumedRegionInfo>,
394}
395
396/// Information about the operation that consumed a Region.
397#[derive(Debug, Clone, Copy)]
398pub(crate) struct ConsumedRegionInfo {
399    operation: ConsumedRegionOperation,
400}
401
402impl ConsumedRegionInfo {
403    pub(crate) fn new(operation: ConsumedRegionOperation) -> Self {
404        Self { operation }
405    }
406
407    pub(crate) fn operation(self) -> ConsumedRegionOperation {
408        self.operation
409    }
410}
411
412#[derive(Debug, Clone, Copy, PartialEq, Eq)]
413pub(crate) enum ConsumedRegionOperation {
414    Extrude,
415    Revolve,
416    Sweep,
417    Delete,
418}
419
420impl std::fmt::Display for ConsumedRegionOperation {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        match self {
423            Self::Extrude => f.write_str("extrude"),
424            Self::Revolve => f.write_str("revolve"),
425            Self::Sweep => f.write_str("sweep"),
426            Self::Delete => f.write_str("delete"),
427        }
428    }
429}
430
431/// Internal identity for one runtime KCL solid value.
432#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
433pub(crate) struct ConsumedSolidKey {
434    /// The engine body UUID.
435    engine_id: Uuid,
436    /// Distinguishes this KCL runtime instance from other values that may reuse
437    /// the same engine body UUID.
438    instance_id: Uuid,
439}
440
441impl ConsumedSolidKey {
442    pub(crate) fn new(engine_id: Uuid, instance_id: Uuid) -> Self {
443        Self { engine_id, instance_id }
444    }
445
446    pub(crate) fn engine_id(&self) -> Uuid {
447        self.engine_id
448    }
449
450    pub(crate) fn instance_id(&self) -> Uuid {
451        self.instance_id
452    }
453}
454
455/// Information about a solid value that was consumed by an operation.
456/// Stored in `ModuleState.consumed_solids` so subsequent attempts to use the
457/// solid produce a clear error pointing at the operation that consumed it.
458#[derive(Debug, Clone)]
459pub(crate) struct ConsumedSolidInfo {
460    /// The operation that consumed the solid.
461    operation: ConsumedSolidOperation,
462    /// First returned solid value, used only for replacement suggestions in
463    /// error messages. When present, this key is also included in
464    /// `returned_solid_keys`.
465    suggested_replacement_key: Option<ConsumedSolidKey>,
466    /// All solid values returned by that operation. This is used as the
467    /// allow-list for returned solids that reuse a consumed engine UUID.
468    returned_solid_keys: Vec<ConsumedSolidKey>,
469}
470
471impl ConsumedSolidInfo {
472    pub(crate) fn new(operation: ConsumedSolidOperation, returned_solid_keys: Vec<ConsumedSolidKey>) -> Self {
473        Self {
474            operation,
475            suggested_replacement_key: returned_solid_keys.first().copied(),
476            returned_solid_keys,
477        }
478    }
479
480    pub(crate) fn operation(&self) -> ConsumedSolidOperation {
481        self.operation
482    }
483
484    pub(crate) fn suggested_replacement_key(&self) -> Option<ConsumedSolidKey> {
485        self.suggested_replacement_key
486    }
487
488    pub(crate) fn should_report_reused_engine_id_as_consumed(&self, key: ConsumedSolidKey) -> bool {
489        !self.returned_solid_keys.contains(&key)
490    }
491}
492
493#[derive(Debug, Clone, Copy, PartialEq, Eq)]
494pub(crate) enum ConsumedSolidOperation {
495    Union,
496    Intersect,
497    Subtract,
498    Split,
499    JoinSurfaces,
500}
501
502impl ConsumedSolidOperation {
503    pub(crate) fn indefinite_article(self) -> &'static str {
504        match self {
505            Self::Intersect => "an",
506            Self::Union | Self::Subtract | Self::Split | Self::JoinSurfaces => "a",
507        }
508    }
509}
510
511impl std::fmt::Display for ConsumedSolidOperation {
512    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
513        match self {
514            Self::Union => f.write_str("union"),
515            Self::Intersect => f.write_str("intersect"),
516            Self::Subtract => f.write_str("subtract"),
517            Self::Split => f.write_str("split"),
518            Self::JoinSurfaces => f.write_str("joinSurfaces"),
519        }
520    }
521}
522
523#[derive(Debug, Clone, Default)]
524pub(crate) struct SketchBlockState {
525    pub sketch_vars: Vec<KclValue>,
526    pub sketch_id: Option<ObjectId>,
527    pub sketch_constraints: Vec<ObjectId>,
528    pub solver_constraints: Vec<ezpz::Constraint>,
529    pub solver_optional_constraints: Vec<ezpz::Constraint>,
530    pub needed_by_engine: Vec<UnsolvedSegment>,
531    pub segment_tags: IndexMap<ObjectId, TagNode>,
532    pub pending_legacy_angle_refactor_metadata: Vec<PendingLegacyAngleRefactorMeta>,
533}
534
535impl ExecState {
536    pub fn new(exec_context: &super::ExecutorContext) -> Self {
537        ExecState {
538            execution_callbacks: exec_context.execution_callbacks.clone(),
539            global: GlobalState::new(&exec_context.settings, Default::default()),
540            mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default(), false, true),
541        }
542    }
543
544    #[cfg(test)]
545    pub(crate) fn new_with_memory_backend(exec_context: &super::ExecutorContext, backend: MemoryBackendKind) -> Self {
546        ExecState {
547            execution_callbacks: exec_context.execution_callbacks.clone(),
548            global: GlobalState::new(&exec_context.settings, Default::default()),
549            mod_local: ModuleState::new(
550                ModulePath::Main,
551                ProgramMemory::new_with_backend(backend),
552                Default::default(),
553                false,
554                true,
555            ),
556        }
557    }
558
559    pub fn new_mock(exec_context: &super::ExecutorContext, mock_config: &MockConfig) -> Self {
560        let segment_ids_edited = mock_config.segment_ids_edited.clone();
561        let mut global = GlobalState::new(&exec_context.settings, segment_ids_edited);
562        global.drag_anchors = mock_config.drag_anchors.clone();
563        global.sketch_mode = mock_config.sketch_block_id.is_some();
564        ExecState {
565            execution_callbacks: exec_context.execution_callbacks.clone(),
566            global,
567            mod_local: ModuleState::new(
568                ModulePath::Main,
569                ProgramMemory::new(),
570                Default::default(),
571                mock_config.sketch_block_id.is_some(),
572                mock_config.freedom_analysis,
573            ),
574        }
575    }
576
577    #[cfg(test)]
578    pub(crate) fn new_mock_with_memory_backend(
579        exec_context: &super::ExecutorContext,
580        mock_config: &MockConfig,
581        backend: MemoryBackendKind,
582    ) -> Self {
583        let segment_ids_edited = mock_config.segment_ids_edited.clone();
584        let mut global = GlobalState::new(&exec_context.settings, segment_ids_edited);
585        global.drag_anchors = mock_config.drag_anchors.clone();
586        global.sketch_mode = mock_config.sketch_block_id.is_some();
587        ExecState {
588            execution_callbacks: exec_context.execution_callbacks.clone(),
589            global,
590            mod_local: ModuleState::new(
591                ModulePath::Main,
592                ProgramMemory::new_with_backend(backend),
593                Default::default(),
594                mock_config.sketch_block_id.is_some(),
595                mock_config.freedom_analysis,
596            ),
597        }
598    }
599
600    pub(super) fn reset(&mut self, exec_context: &super::ExecutorContext) {
601        let global = GlobalState::new(&exec_context.settings, Default::default());
602
603        *self = ExecState {
604            execution_callbacks: exec_context.execution_callbacks.clone(),
605            global,
606            mod_local: ModuleState::new(
607                self.mod_local.path.clone(),
608                ProgramMemory::new(),
609                Default::default(),
610                false,
611                true,
612            ),
613        };
614    }
615
616    /// Log a non-fatal error.
617    pub fn err(&mut self, e: CompilationIssue) {
618        self.global.issues.push(e);
619    }
620
621    /// Log a warning.
622    pub fn warn(&mut self, mut e: CompilationIssue, name: &'static str) {
623        debug_assert!(annotations::WARN_VALUES.contains(&name));
624
625        if self.mod_local.allowed_warnings.contains(&name) {
626            return;
627        }
628
629        if self.mod_local.denied_warnings.contains(&name) {
630            e.severity = Severity::Error;
631        } else {
632            e.severity = Severity::Warning;
633        }
634
635        self.global.issues.push(e);
636    }
637
638    pub fn warn_experimental(&mut self, feature_name: &str, source_range: SourceRange) {
639        let Some(severity) = self.mod_local.settings.experimental_features.severity() else {
640            return;
641        };
642        let error = CompilationIssue {
643            source_range,
644            message: format!("Use of {feature_name} is experimental and may change or be removed."),
645            suggestion: None,
646            severity,
647            tag: crate::errors::Tag::None,
648        };
649
650        self.global.issues.push(error);
651    }
652
653    pub fn clear_units_warnings(&mut self, source_range: &SourceRange) {
654        self.global.issues = std::mem::take(&mut self.global.issues)
655            .into_iter()
656            .filter(|e| {
657                e.severity != Severity::Warning
658                    || !source_range.contains_range(&e.source_range)
659                    || e.tag != crate::errors::Tag::UnknownNumericUnits
660            })
661            .collect();
662    }
663
664    pub fn issues(&self) -> &[CompilationIssue] {
665        &self.global.issues
666    }
667
668    pub(crate) fn deprecation_version(&self) -> &str {
669        self.global
670            .deprecation_version_override
671            .as_deref()
672            .unwrap_or(self.mod_local.settings.kcl_version.as_str())
673    }
674
675    /// The entry point's declared KCL version, or the default. `added_in`
676    /// gating uses this rather than [`Self::kcl_version`] because std modules
677    /// declare no version of their own.
678    pub(crate) fn entry_point_kcl_version(&self) -> KclVersion {
679        self.global.entry_point_kcl_version.unwrap_or_default()
680    }
681
682    /// Record that `key` was skipped in the current module because the
683    /// program's KCL version predates `added_in`.
684    pub(crate) fn record_not_yet_added(
685        &mut self,
686        key: String,
687        added_in: annotations::VersionConstraint,
688        exported: bool,
689    ) {
690        let item = NotYetAdded {
691            added_in,
692            is_std: matches!(self.mod_local.path, ModulePath::Std { .. }),
693        };
694        self.mod_local
695            .not_yet_added
696            .insert(key, ScopedNotYetAdded { item, exported });
697    }
698
699    /// Bring a glob-imported module's exported records into this scope, as
700    /// `import *` does for names. Records already here are left alone.
701    pub(crate) fn import_not_yet_added(&mut self, records: &IndexMap<String, NotYetAdded>, exported: bool) {
702        for (key, item) in records {
703            self.mod_local
704                .not_yet_added
705                .entry(key.clone())
706                .or_insert_with(|| ScopedNotYetAdded {
707                    item: item.clone(),
708                    exported,
709                });
710        }
711    }
712
713    /// The record for `key` in this module's scope, then in std's.
714    pub(crate) fn not_yet_added_in_scope(&self, key: &str) -> Option<&NotYetAdded> {
715        self.mod_local
716            .not_yet_added
717            .get(key)
718            .map(|scoped| &scoped.item)
719            .or_else(|| self.global.std_not_yet_added.get(key))
720    }
721
722    /// Append the version help to `err` if one of `keys` is a std declaration
723    /// skipped in the current scope, mirroring the not-yet-added parameter
724    /// message.
725    pub(crate) fn with_not_yet_added_hint(&self, keys: &[&str], err: KclError) -> KclError {
726        match keys.iter().find_map(|key| self.not_yet_added_in_scope(key)) {
727            Some(item) if self.hints_for(item) => self.not_yet_added_hint(item, err),
728            _ => err,
729        }
730    }
731
732    /// Like [`Self::with_not_yet_added_hint`], but consulting another module's
733    /// exported `records`.
734    pub(crate) fn with_not_yet_added_hint_from(
735        &self,
736        records: &IndexMap<String, NotYetAdded>,
737        keys: &[&str],
738        err: KclError,
739    ) -> KclError {
740        match keys.iter().find_map(|key| records.get(*key)) {
741            Some(item) if self.hints_for(item) => self.not_yet_added_hint(item, err),
742            _ => err,
743        }
744    }
745
746    /// Whether a failed lookup should mention `item`. Only std declarations
747    /// qualify, so a mismatch with the library is explained while user code
748    /// keeps the plain message; hints for user code would also have to follow
749    /// lexical scope, which these records do not.
750    fn hints_for(&self, item: &NotYetAdded) -> bool {
751        #[cfg(test)]
752        if self.global.hint_all_not_yet_added {
753            return true;
754        }
755        item.is_std
756    }
757
758    fn not_yet_added_hint(&self, item: &NotYetAdded, mut err: KclError) -> KclError {
759        let details = err.details_mut();
760        details.message = format!(
761            "{}; it was added in KCL {}, but this program uses KCL {}",
762            details.message,
763            item.added_in,
764            self.entry_point_kcl_version().as_str()
765        );
766        err
767    }
768
769    #[cfg(test)]
770    pub(crate) fn set_deprecation_version_override(&mut self, version: Option<&str>) {
771        self.global.deprecation_version_override = version.map(str::to_owned);
772    }
773
774    #[cfg(test)]
775    pub(crate) fn program_memory_for_tests(
776        &self,
777        main_ref: EnvironmentRef,
778    ) -> Result<IndexMap<String, KclValue>, KclError> {
779        self.mod_local.variables(main_ref)
780    }
781
782    /// Convert to execution outcome when running in WebAssembly.  We want to
783    /// reduce the amount of data that crosses the WASM boundary as much as
784    /// possible.
785    pub async fn into_exec_outcome(
786        self,
787        main_ref: EnvironmentRef,
788        ctx: &ExecutorContext,
789    ) -> Result<ExecOutcome, KclError> {
790        // Fields are opt-in so that we don't accidentally leak private internal
791        // state when we add more to ExecState.
792        let variables = self.mod_local.variables(main_ref)?;
793        #[cfg(test)]
794        let test_program_memory = variables.clone();
795        let variables = variables
796            .into_iter()
797            .map(|(key, value)| (key, KclValueView::from(value)))
798            .collect();
799        Ok(ExecOutcome {
800            variables,
801            filenames: self.global.filenames(),
802            from_cache: false,
803            operations: self.global.operations_by_module(),
804            artifact_graph: self.global.artifacts.graph,
805            scene_objects: self.global.root_module_artifacts.scene_objects,
806            source_range_to_object: self.global.root_module_artifacts.source_range_to_object,
807            var_solutions: self.global.root_module_artifacts.var_solutions,
808            refactor_metadata: self.global.root_module_artifacts.refactor_metadata.clone(),
809            issues: self.global.issues,
810            source_files: self.global.id_to_source,
811            default_planes: ctx.engine.get_default_planes().read().await.clone(),
812            #[cfg(test)]
813            test_program_memory,
814        })
815    }
816
817    #[cfg(feature = "snapshot-engine-responses")]
818    pub(crate) fn take_root_module_responses(
819        &mut self,
820    ) -> IndexMap<Uuid, kittycad_modeling_cmds::websocket::WebSocketResponse> {
821        std::mem::take(&mut self.global.root_module_artifacts.responses)
822    }
823
824    pub(crate) fn geometry_only(&self) -> bool {
825        self.global.geometry_only
826    }
827
828    pub(crate) fn stack(&self) -> &Stack {
829        &self.mod_local.stack
830    }
831
832    pub(crate) fn mut_stack(&mut self) -> &mut Stack {
833        &mut self.mod_local.stack
834    }
835
836    /// Increment the user-level call stack size, returning an error if it
837    /// exceeds the maximum.
838    pub(super) fn inc_call_stack_size(&mut self, range: SourceRange) -> Result<(), KclError> {
839        // If you change this, make sure to test in WebAssembly in the app since
840        // that's the limiting factor.
841        const LIMIT: usize = 50;
842        if self.mod_local.call_stack_size >= LIMIT {
843            return Err(KclError::new_max_call_stack(KclErrorDetails::new(
844                format!(
845                    "Call depth limit ({LIMIT}) exceeded. This usually means a function is recursing without a base case."
846                ),
847                vec![range],
848            )));
849        }
850        self.mod_local.call_stack_size += 1;
851        Ok(())
852    }
853
854    /// Decrement the user-level call stack size, returning an error if it would
855    /// go below zero.
856    pub(super) fn dec_call_stack_size(&mut self, range: SourceRange) -> Result<(), KclError> {
857        // Prevent underflow.
858        if self.mod_local.call_stack_size == 0 {
859            let message = "call stack size below zero".to_owned();
860            debug_assert!(false, "{message}");
861            return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
862        }
863        self.mod_local.call_stack_size -= 1;
864        Ok(())
865    }
866
867    /// The deepest machine-executor call depth reached in this execution.
868    /// The machine maintains the counter in all builds; today only the test
869    /// harnesses' depth survey reads it.
870    // Unused outside test builds, but kept available so release diagnostics
871    // can read the counter the machine already maintains.
872    #[allow(dead_code)]
873    pub(crate) fn machine_depth_high_water(&self) -> usize {
874        self.global.machine_depth_high_water
875    }
876
877    /// Returns true if we're executing in sketch mode for the current module.
878    /// In sketch mode, we still want to execute the prelude and other stdlib
879    /// modules as normal, so it can vary per module within a single overall
880    /// execution.
881    pub(crate) fn sketch_mode(&self) -> bool {
882        self.mod_local.sketch_mode
883            && match &self.mod_local.path {
884                ModulePath::Main => true,
885                ModulePath::Local { .. } => true,
886                ModulePath::Std { .. } => false,
887            }
888    }
889
890    /// Returns true if this execution is sketch mode execution, executing a
891    /// single sketch block. Unlike [`Self::sketch_mode`], this doesn't vary
892    /// during the execution.
893    pub(crate) fn is_sketch_mode_execution(&self) -> bool {
894        self.global.sketch_mode
895    }
896
897    pub fn next_object_id(&mut self) -> ObjectId {
898        ObjectId(self.mod_local.artifacts.object_id_generator.next_id())
899    }
900
901    pub fn peek_object_id(&self) -> ObjectId {
902        ObjectId(self.mod_local.artifacts.object_id_generator.peek_id())
903    }
904
905    pub(crate) fn constraint_state(&self, sketch_block_id: ObjectId, key: &ConstraintKey) -> Option<ConstraintState> {
906        let map = self.mod_local.constraint_state.get(&sketch_block_id)?;
907        map.get(key).copied()
908    }
909
910    pub(crate) fn set_constraint_state(
911        &mut self,
912        sketch_block_id: ObjectId,
913        key: ConstraintKey,
914        state: ConstraintState,
915    ) {
916        let map = self.mod_local.constraint_state.entry(sketch_block_id).or_default();
917        map.insert(key, state);
918    }
919
920    pub fn add_scene_object(&mut self, obj: Object, source_range: SourceRange) -> ObjectId {
921        let id = obj.id;
922        debug_assert!(
923            id.0 == self.mod_local.artifacts.scene_objects.len(),
924            "Adding scene object with ID {} but next ID is {}",
925            id.0,
926            self.mod_local.artifacts.scene_objects.len()
927        );
928        let artifact_id = obj.artifact_id;
929        self.mod_local.artifacts.scene_objects.push(obj);
930        self.mod_local.artifacts.source_range_to_object.insert(source_range, id);
931        self.mod_local
932            .artifacts
933            .artifact_id_to_scene_object
934            .insert(artifact_id, id);
935        id
936    }
937
938    /// Add a placeholder scene object. This is useful when we need to reserve
939    /// an ID before we have all the information to create the full object.
940    pub fn add_placeholder_scene_object(
941        &mut self,
942        id: ObjectId,
943        source_range: SourceRange,
944        node_path: Option<NodePath>,
945    ) -> ObjectId {
946        debug_assert!(id.0 == self.mod_local.artifacts.scene_objects.len());
947        self.mod_local
948            .artifacts
949            .scene_objects
950            .push(Object::placeholder(id, source_range, node_path));
951        self.mod_local.artifacts.source_range_to_object.insert(source_range, id);
952        id
953    }
954
955    /// Update a scene object. This is useful to replace a placeholder.
956    pub fn set_scene_object(&mut self, object: Object) {
957        let id = object.id;
958        let artifact_id = object.artifact_id;
959        self.mod_local.artifacts.scene_objects[id.0] = object;
960        self.mod_local
961            .artifacts
962            .artifact_id_to_scene_object
963            .insert(artifact_id, id);
964    }
965
966    pub fn scene_object_id_by_artifact_id(&self, artifact_id: ArtifactId) -> Option<ObjectId> {
967        self.mod_local
968            .artifacts
969            .artifact_id_to_scene_object
970            .get(&artifact_id)
971            .cloned()
972    }
973
974    pub fn segment_ids_edited_contains(&self, object_id: &ObjectId) -> bool {
975        self.global.segment_ids_edited.contains(object_id)
976    }
977
978    pub fn drag_anchor_target(&self, object_id: &ObjectId) -> Option<&kcl_api::Point2d<kcl_api::Number>> {
979        self.global
980            .drag_anchors
981            .iter()
982            .find(|anchor| &anchor.segment_id == object_id)
983            .map(|anchor| &anchor.target)
984    }
985
986    pub(super) fn is_in_sketch_block(&self) -> bool {
987        self.mod_local.sketch_block.is_some()
988    }
989
990    pub(crate) fn sketch_block_mut(&mut self) -> Option<&mut SketchBlockState> {
991        self.mod_local.sketch_block.as_mut()
992    }
993
994    pub(crate) fn sketch_block(&mut self) -> Option<&SketchBlockState> {
995        self.mod_local.sketch_block.as_ref()
996    }
997
998    pub fn next_uuid(&mut self) -> Uuid {
999        self.mod_local.id_generator.next_uuid()
1000    }
1001
1002    pub fn next_artifact_id(&mut self) -> ArtifactId {
1003        self.mod_local.id_generator.next_artifact_id()
1004    }
1005
1006    pub fn id_generator(&mut self) -> &mut IdGenerator {
1007        &mut self.mod_local.id_generator
1008    }
1009
1010    /// Record that a solid value has been consumed by a CSG boolean operation.
1011    pub(crate) fn mark_solid_consumed(&mut self, consumed_key: ConsumedSolidKey, info: ConsumedSolidInfo) {
1012        self.mod_local.consumed_solids.insert(consumed_key, info);
1013    }
1014
1015    /// Record that an engine body UUID has been consumed by a CSG boolean
1016    /// operation.
1017    pub(crate) fn mark_solid_id_consumed(&mut self, consumed_id: Uuid, info: ConsumedSolidInfo) {
1018        self.mod_local.consumed_solid_ids.insert(consumed_id, info);
1019    }
1020
1021    /// Look up whether a solid value was consumed by a previous CSG boolean
1022    /// operation.
1023    pub(crate) fn check_solid_consumed(&self, key: &ConsumedSolidKey) -> Option<&ConsumedSolidInfo> {
1024        self.mod_local.consumed_solids.get(key)
1025    }
1026
1027    /// Look up whether an engine body UUID was consumed by a previous CSG
1028    /// boolean operation.
1029    pub(crate) fn check_solid_id_consumed(&self, id: &Uuid) -> Option<&ConsumedSolidInfo> {
1030        self.mod_local.consumed_solid_ids.get(id)
1031    }
1032
1033    pub(crate) fn mark_region_consumed(&mut self, id: Uuid, info: ConsumedRegionInfo) {
1034        self.mod_local.consumed_regions.insert(id, info);
1035    }
1036
1037    pub(crate) fn check_region_consumed(&self, id: &Uuid) -> Option<ConsumedRegionInfo> {
1038        self.mod_local.consumed_regions.get(id).copied()
1039    }
1040
1041    /// Find the current variable containing a Region engine UUID. This runs
1042    /// only while constructing a diagnostic, so recursively searching arrays
1043    /// and objects is preferable to storing variable names in liveness state.
1044    pub(crate) fn find_var_name_for_region_id(&self, target_id: Uuid) -> Result<Option<String>, KclError> {
1045        fn contains_region_id(value: &KclValue, target_id: Uuid) -> bool {
1046            match value {
1047                KclValue::Sketch { value } => value.origin_sketch_id.is_some() && value.id == target_id,
1048                KclValue::HomArray { value, .. } | KclValue::Tuple { value, .. } => {
1049                    value.iter().any(|value| contains_region_id(value, target_id))
1050                }
1051                KclValue::Object { value, .. } => value.values().any(|value| contains_region_id(value, target_id)),
1052                _ => false,
1053            }
1054        }
1055
1056        self.mod_local
1057            .stack
1058            .find_var_name_in_all_envs(|value| contains_region_id(value, target_id))
1059    }
1060
1061    /// Follow direct replacement links until we find the latest known output.
1062    /// Used only on error paths so diagnostics can suggest the current solid.
1063    pub(crate) fn latest_consumed_output(
1064        &self,
1065        suggested_replacement_key: Option<ConsumedSolidKey>,
1066    ) -> Option<ConsumedSolidKey> {
1067        let mut latest = suggested_replacement_key?;
1068        let mut seen = AhashIndexSet::default();
1069
1070        while seen.insert(latest) {
1071            let Some(next) = self
1072                .mod_local
1073                .consumed_solids
1074                .get(&latest)
1075                .and_then(|info| info.suggested_replacement_key())
1076            else {
1077                break;
1078            };
1079            latest = next;
1080        }
1081
1082        Some(latest)
1083    }
1084
1085    /// Search the live environment for the name of a variable holding a Solid
1086    /// (or an array of Solids) whose value identity matches `target_key`. Used only on
1087    /// error paths to recover variable names for diagnostics.
1088    pub(crate) fn find_var_name_for_solid_key(&self, target_key: ConsumedSolidKey) -> Result<Option<String>, KclError> {
1089        fn contains_solid_key(value: &KclValue, target_key: ConsumedSolidKey) -> bool {
1090            match value {
1091                KclValue::Solid { value } => {
1092                    value.id == target_key.engine_id() && value.value_id == target_key.instance_id()
1093                }
1094                KclValue::HomArray { value, .. } => value.iter().any(|v| contains_solid_key(v, target_key)),
1095                _ => false,
1096            }
1097        }
1098        self.mod_local
1099            .stack
1100            .find_var_name_in_all_envs(|value| contains_solid_key(value, target_key))
1101    }
1102
1103    pub(crate) fn add_artifact(&mut self, artifact: Artifact) {
1104        let id = artifact.id();
1105        self.mod_local.artifacts.artifacts.insert(id, artifact);
1106    }
1107
1108    /// The declaring module and display name of every named view registered so
1109    /// far. `view::named` needs these to reject a name that a view declared by
1110    /// the same module already uses.
1111    ///
1112    /// Both artifact maps are scanned, because incremental re-execution divides
1113    /// the views between them:
1114    /// - a run that clears the scene empties `global.artifacts` beforehand, so
1115    ///   every view it can see is one the current run registered into
1116    ///   `mod_local.artifacts`;
1117    /// - a run that only appends statements to an unchanged prefix does not
1118    ///   re-execute that prefix, so the views the prefix declared stay in
1119    ///   `global.artifacts` from the previous run while the appended
1120    ///   declarations register into `mod_local.artifacts`.
1121    ///
1122    /// Reading one map alone would accept a duplicate name on one of those
1123    /// paths and reject it on the other, which an author would see as the same
1124    /// file being accepted while typed and rejected after an unrelated edit.
1125    /// Neither path can report a view against its own earlier registration: a
1126    /// re-executed declaration is only reached after `global.artifacts` was
1127    /// cleared, and an appended declaration has no earlier registration.
1128    pub(crate) fn registered_named_views(&self) -> impl Iterator<Item = (ModuleId, &str)> {
1129        self.mod_local
1130            .artifacts
1131            .artifacts
1132            .values()
1133            .chain(self.global.artifacts.artifacts.values())
1134            .filter_map(|artifact| match artifact {
1135                Artifact::NamedView(view) => Some((view.declaring_module_id, view.name.as_str())),
1136                _ => None,
1137            })
1138    }
1139
1140    pub(crate) fn artifact_mut(&mut self, id: ArtifactId) -> Option<&mut Artifact> {
1141        self.mod_local.artifacts.artifacts.get_mut(&id)
1142    }
1143
1144    pub(crate) fn is_sketch_block_path(&self, path_id: ArtifactId) -> bool {
1145        self.mod_local
1146            .artifacts
1147            .artifacts
1148            .values()
1149            .chain(self.global.artifacts.artifacts.values())
1150            .any(|artifact| {
1151                matches!(artifact, Artifact::SketchBlock(sketch_block) if sketch_block.path_id == Some(path_id))
1152            })
1153    }
1154
1155    pub(crate) fn push_op(&mut self, op: Operation) {
1156        let index = self.mod_local.artifacts.operations.len();
1157        self.mod_local.artifacts.operations.push(op);
1158        if let Some(operation) = self.mod_local.artifacts.operations.last().cloned()
1159            && let Some(callbacks) = &self.execution_callbacks
1160        {
1161            callbacks.on_operation(OperationCallbackArgs {
1162                module_id: self.mod_local.module_id,
1163                operation,
1164                index,
1165            });
1166        }
1167    }
1168
1169    pub(crate) fn push_command(&mut self, command: ArtifactCommand) {
1170        self.mod_local.artifacts.unprocessed_commands.push(command);
1171    }
1172
1173    pub(super) fn next_module_id(&self) -> ModuleId {
1174        ModuleId::from_usize(self.global.path_to_source_id.len())
1175    }
1176
1177    pub(super) fn id_for_module(&self, path: &ModulePath) -> Option<ModuleId> {
1178        self.global.path_to_source_id.get(path).cloned()
1179    }
1180
1181    pub(super) fn add_path_to_source_id(&mut self, path: ModulePath, id: ModuleId) {
1182        debug_assert!(!self.global.path_to_source_id.contains_key(&path));
1183        self.global.path_to_source_id.insert(path, id);
1184    }
1185
1186    pub(crate) fn add_root_module_contents(&mut self, program: &crate::Program) {
1187        let root_id = ModuleId::default();
1188        // Get the path for the root module.
1189        let path = self
1190            .global
1191            .path_to_source_id
1192            .iter()
1193            .find(|(_, v)| **v == root_id)
1194            .unwrap()
1195            .0
1196            .clone();
1197        self.add_id_to_source(
1198            root_id,
1199            ModuleSource {
1200                path,
1201                source: program.original_file_contents.to_string(),
1202            },
1203        );
1204    }
1205
1206    pub(super) fn add_id_to_source(&mut self, id: ModuleId, source: ModuleSource) {
1207        self.global.id_to_source.insert(id, source);
1208    }
1209
1210    pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
1211        debug_assert!(self.global.path_to_source_id.contains_key(&path));
1212        let module_info = ModuleInfo { id, repr, path };
1213        self.global.module_infos.insert(id, module_info);
1214    }
1215
1216    pub fn get_module(&mut self, id: ModuleId) -> Option<&ModuleInfo> {
1217        self.global.module_infos.get(&id)
1218    }
1219
1220    #[cfg(test)]
1221    pub(crate) fn modules(&self) -> &ModuleInfoMap {
1222        &self.global.module_infos
1223    }
1224
1225    #[cfg(test)]
1226    pub(crate) fn root_module_artifact_state(&self) -> &ModuleArtifactState {
1227        &self.global.root_module_artifacts
1228    }
1229
1230    /// Record metadata from a deprecated edge stdlib call for the Z0006 refactor.
1231    pub(crate) fn record_edge_refactor_meta(&mut self, meta: EdgeRefactorMeta) {
1232        self.mod_local
1233            .artifacts
1234            .refactor_metadata
1235            .push(RefactorMetadata::EdgeRefactor(Box::new(meta)));
1236    }
1237
1238    pub(crate) fn record_pending_edge_refactor_meta(&mut self, meta: PendingEdgeRefactorMeta) {
1239        self.mod_local.artifacts.pending_edge_refactor_metadata.push(meta);
1240    }
1241
1242    pub(crate) fn pending_edge_refactor_meta(
1243        &self,
1244        edge_id: Uuid,
1245        argument_source_range: SourceRange,
1246    ) -> Option<PendingEdgeRefactorMeta> {
1247        if let Some(pending) = self
1248            .mod_local
1249            .artifacts
1250            .pending_edge_refactor_metadata
1251            .iter()
1252            .find(|meta| meta.edge_id == edge_id && argument_source_range.contains_range(&meta.source_range))
1253        {
1254            return Some(pending.clone());
1255        }
1256
1257        // A helper assigned to a variable is outside the argument's source
1258        // range. Fall back to the edge ID only when it identifies one helper.
1259        let mut matches = self
1260            .mod_local
1261            .artifacts
1262            .pending_edge_refactor_metadata
1263            .iter()
1264            .filter(|meta| meta.edge_id == edge_id);
1265        let pending = matches.next()?.clone();
1266        matches.next().is_none().then_some(pending)
1267    }
1268
1269    pub(crate) fn record_edge_refactor_meta_from_pending(
1270        &mut self,
1271        edge_id: Uuid,
1272        source_range: SourceRange,
1273        face_ids: [Uuid; 2],
1274    ) -> bool {
1275        if self.mod_local.artifacts.refactor_metadata.iter().any(|meta| {
1276            matches!(
1277                meta,
1278                RefactorMetadata::EdgeRefactor(meta)
1279                    if meta.edge_id == edge_id && meta.source_range == source_range
1280            )
1281        }) {
1282            return true;
1283        }
1284
1285        let exact_pending_meta = self
1286            .mod_local
1287            .artifacts
1288            .pending_edge_refactor_metadata
1289            .iter()
1290            .find(|meta| meta.edge_id == edge_id && meta.source_range == source_range)
1291            .cloned();
1292
1293        let edge_pending_meta = || {
1294            let mut matches = self
1295                .mod_local
1296                .artifacts
1297                .pending_edge_refactor_metadata
1298                .iter()
1299                .filter(|meta| meta.edge_id == edge_id);
1300            let pending_meta = matches.next()?.clone();
1301            matches.next().is_none().then_some(pending_meta)
1302        };
1303
1304        let Some(pending_meta) = exact_pending_meta.or_else(edge_pending_meta) else {
1305            return false;
1306        };
1307
1308        self.record_edge_refactor_meta(EdgeRefactorMeta {
1309            edge_id,
1310            face_ids,
1311            end_face_ids: Vec::new(),
1312            source_range: pending_meta.source_range,
1313            stdlib_fn: pending_meta.stdlib_fn,
1314        });
1315
1316        true
1317    }
1318
1319    /// Record metadata from a fillet/chamfer call that used `tags` directly.
1320    pub(crate) fn record_direct_tag_fillet_meta(&mut self, meta: DirectTagFilletMeta) {
1321        self.mod_local
1322            .artifacts
1323            .refactor_metadata
1324            .push(RefactorMetadata::DirectTagFillet(meta));
1325    }
1326
1327    /// Refactor metadata collected when deprecated edge stdlib functions run (for tests and lint).
1328    pub fn edge_refactor_metadata(&self) -> Vec<EdgeRefactorMeta> {
1329        self.global
1330            .root_module_artifacts
1331            .refactor_metadata
1332            .iter()
1333            .filter_map(|m| match m {
1334                RefactorMetadata::EdgeRefactor(meta) => Some(meta.as_ref().clone()),
1335                RefactorMetadata::DirectTagFillet(_) | RefactorMetadata::LegacyAngle(_) => None,
1336            })
1337            .collect()
1338    }
1339
1340    /// Direct-tag fillet/chamfer metadata (for Z0006 code mod).
1341    pub fn direct_tag_fillet_metadata(&self) -> Vec<DirectTagFilletMeta> {
1342        self.global
1343            .root_module_artifacts
1344            .refactor_metadata
1345            .iter()
1346            .filter_map(|m| match m {
1347                RefactorMetadata::EdgeRefactor(_) | RefactorMetadata::LegacyAngle(_) => None,
1348                RefactorMetadata::DirectTagFillet(meta) => Some(meta.clone()),
1349            })
1350            .collect()
1351    }
1352
1353    pub fn current_default_units(&self) -> NumericType {
1354        NumericType::Default {
1355            len: self.length_unit(),
1356            angle: self.angle_unit(),
1357        }
1358    }
1359
1360    pub fn length_unit(&self) -> UnitLength {
1361        self.mod_local.settings.default_length_units
1362    }
1363
1364    pub fn angle_unit(&self) -> UnitAngle {
1365        self.mod_local.settings.default_angle_units
1366    }
1367
1368    pub(super) fn circular_import_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError {
1369        KclError::new_import_cycle(KclErrorDetails::new(
1370            format!(
1371                "circular import of modules is not allowed: {} -> {}",
1372                self.global
1373                    .mod_loader
1374                    .import_stack
1375                    .iter()
1376                    .map(|p| p.to_string_lossy())
1377                    .collect::<Vec<_>>()
1378                    .join(" -> "),
1379                path,
1380            ),
1381            vec![source_range],
1382        ))
1383    }
1384
1385    pub(crate) fn pipe_value(&self) -> Option<&KclValue> {
1386        self.mod_local.pipe_value.as_ref()
1387    }
1388
1389    pub(crate) fn error_with_outputs(
1390        &self,
1391        error: KclError,
1392        main_ref: Option<EnvironmentRef>,
1393        default_planes: Option<DefaultPlanes>,
1394    ) -> KclErrorWithOutputs {
1395        let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = self
1396            .global
1397            .path_to_source_id
1398            .iter()
1399            .map(|(k, v)| ((*v), k.clone()))
1400            .collect();
1401
1402        KclErrorWithOutputs::new(
1403            error,
1404            self.issues().to_vec(),
1405            main_ref
1406                .and_then(|main_ref| self.mod_local.variables(main_ref).ok())
1407                .unwrap_or_default(),
1408            self.global.operations_by_module(),
1409            Default::default(),
1410            self.global.artifacts.graph.clone(),
1411            self.global.root_module_artifacts.scene_objects.clone(),
1412            self.global.root_module_artifacts.source_range_to_object.clone(),
1413            self.global.root_module_artifacts.var_solutions.clone(),
1414            self.global.root_module_artifacts.refactor_metadata.clone(),
1415            module_id_to_module_path,
1416            self.global.id_to_source.clone(),
1417            default_planes,
1418        )
1419    }
1420
1421    pub(crate) fn build_program_lookup(
1422        &self,
1423        current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
1424    ) -> ProgramLookup {
1425        ProgramLookup::new(current, self.global.module_infos.clone())
1426    }
1427
1428    pub(crate) async fn build_artifact_graph(
1429        &mut self,
1430        engine: &Arc<EngineManager>,
1431        program: NodeRef<'_, crate::parsing::ast::types::Program>,
1432    ) -> Result<(), KclError> {
1433        let mut new_commands = Vec::new();
1434        let mut new_exec_artifacts = IndexMap::new();
1435        for module in self.global.module_infos.values_mut() {
1436            match &mut module.repr {
1437                ModuleRepr::Kcl(_, Some(outcome)) => {
1438                    new_commands.extend(outcome.artifacts.process_commands());
1439                    new_exec_artifacts.extend(outcome.artifacts.artifacts.clone());
1440                }
1441                ModuleRepr::Foreign(_, Some((_, module_artifacts))) => {
1442                    new_commands.extend(module_artifacts.process_commands());
1443                    new_exec_artifacts.extend(module_artifacts.artifacts.clone());
1444                }
1445                ModuleRepr::Root | ModuleRepr::Kcl(_, None) | ModuleRepr::Foreign(_, None) | ModuleRepr::Dummy => {}
1446            }
1447        }
1448        // Take from the module artifacts so that we don't try to process them
1449        // again next time due to execution caching.
1450        new_commands.extend(self.global.root_module_artifacts.process_commands());
1451        // Note: These will get re-processed, but since we're just adding them
1452        // to a map, it's fine.
1453        new_exec_artifacts.extend(self.global.root_module_artifacts.artifacts.clone());
1454        let new_responses = engine.take_responses().await;
1455
1456        // Move the artifacts into ExecState global to simplify cache
1457        // management.
1458        for (id, exec_artifact) in new_exec_artifacts {
1459            // Only insert if it wasn't already present. We don't want to
1460            // overwrite what was previously there. We haven't filled in node
1461            // paths yet.
1462            self.global.artifacts.artifacts.entry(id).or_insert(exec_artifact);
1463        }
1464
1465        let initial_graph = self.global.artifacts.graph.clone();
1466
1467        // Build the artifact graph.
1468        let programs = self.build_program_lookup(program.clone());
1469        let graph_result = crate::execution::artifact::build_artifact_graph(
1470            &new_commands,
1471            &new_responses,
1472            program,
1473            &mut self.global.artifacts.artifacts,
1474            initial_graph,
1475            &programs,
1476            &self.global.module_infos,
1477        );
1478
1479        #[cfg(feature = "snapshot-engine-responses")]
1480        {
1481            // Store engine responses for debugging.
1482            self.global.root_module_artifacts.responses.extend(new_responses);
1483        }
1484
1485        let artifact_graph = graph_result?;
1486        self.global.artifacts.graph = artifact_graph;
1487
1488        Ok(())
1489    }
1490
1491    /// The KCL version governing version-conditional runtime behavior.
1492    ///
1493    /// If the entry-point module declared kclVersion 3.0-preview (or later),
1494    /// that single version governs the entire execution -- all modules and
1495    /// all function bodies. Otherwise, falls back to the legacy per-module
1496    /// lookup; see [`Self::legacy_caller_kcl_version`].
1497    pub(crate) fn kcl_version(&self) -> KclVersion {
1498        match self.global.entry_point_kcl_version {
1499            Some(version) if version >= KclVersion::V3Preview => version,
1500            _ => self.legacy_caller_kcl_version(),
1501        }
1502    }
1503
1504    /// The legacy kclVersion lookup: the current module-local settings.
1505    ///
1506    /// Quirk (fixed when the entry point declares 3.0-preview or later):
1507    /// `mod_local` is swapped only around module top-level execution, never
1508    /// around function calls, so module-level code sees its own module's
1509    /// declared version, but a function body sees the CALLING module's
1510    /// version -- a function defined in a 1.0 module but called from a 2.0
1511    /// module observes 2.0 here.
1512    pub(crate) fn legacy_caller_kcl_version(&self) -> KclVersion {
1513        self.mod_local.settings.kcl_version
1514    }
1515
1516    /// Gate for behaviors introduced in KCL 3.0. True only when the entry-point
1517    /// module of this execution declares KCL 3.0 or later. This never looks at
1518    /// [`Self::legacy_caller_kcl_version()`] so that behavior never varies
1519    /// within a single execution.
1520    pub(crate) fn entry_point_version_is_v3_or_higher(&self) -> bool {
1521        self.global
1522            .entry_point_kcl_version
1523            .is_some_and(|v| v >= KclVersion::V3Preview)
1524    }
1525
1526    /// Record the entry-point program's declared kclVersion for this
1527    /// execution, or `None` when it declares no kclVersion. Must be assigned
1528    /// unconditionally at the start of every execution since the state may be
1529    /// reused across executions whose programs declare different versions.
1530    pub(crate) fn set_entry_point_kcl_version(&mut self, program: &crate::Program) -> Result<(), KclError> {
1531        let version = program.language_version()?;
1532        // Import diagnostics distinguish an explicit version from the default.
1533        self.global.entry_point_kcl_version = declared_kcl_version(&program.ast)?.map(|_| version);
1534        Ok(())
1535    }
1536
1537    /// KCL 3.0: the entry point's declared kclVersion decides which kclVersion
1538    /// an imported file may declare, so that KCL 3.0 semantics never apply to
1539    /// only part of a program.
1540    ///
1541    /// - When the entry point declares 3.0-preview or later, that version
1542    ///   governs the whole execution (see [`Self::kcl_version`]), and an
1543    ///   imported file may not declare a different one.
1544    /// - Otherwise (1.0, 2.0, or undeclared), the legacy per-module lookup
1545    ///   applies, and an imported file may not declare 3.0-preview or later,
1546    ///   which the legacy lookup would honor for that file only. Mixing 1.0
1547    ///   and 2.0 remains allowed, as it always has been.
1548    ///
1549    /// A file that declares no kclVersion does not cause a version mismatch:
1550    /// it runs under the version the lookup gives it. Only user files (local
1551    /// imports) are checked. Standard library modules are exempt: they ship
1552    /// with the interpreter, always run under the entry point's pinned
1553    /// version, and the user cannot edit them to resolve a mismatch. Foreign
1554    /// imports carry no KCL settings.
1555    ///
1556    /// `import_range` is the import statement when the check runs at the
1557    /// import site, which is included in the error.
1558    pub(crate) fn check_imported_module_kcl_version(
1559        &self,
1560        path: &ModulePath,
1561        program: &Node<Program>,
1562        import_range: Option<SourceRange>,
1563    ) -> Result<(), KclError> {
1564        if !path.is_local() {
1565            // stdlib is exempt from the restriction, and `Main` is the version
1566            // we're checking against.
1567            return Ok(());
1568        }
1569        let Some((declared, declared_range)) = declared_kcl_version(program)? else {
1570            return Ok(());
1571        };
1572        let entry_point_version = self.global.entry_point_kcl_version;
1573        let allowed = if self.entry_point_version_is_v3_or_higher() {
1574            Some(declared) == entry_point_version
1575        } else {
1576            declared < KclVersion::V3Preview
1577        };
1578        if allowed {
1579            return Ok(());
1580        }
1581
1582        // The root module's path is the executor's current file, which is
1583        // empty when execution was started without one.
1584        let entry_point = match self
1585            .global
1586            .module_infos
1587            .get(&ModuleId::default())
1588            .map(|info| &info.path)
1589        {
1590            Some(root @ ModulePath::Local { .. }) if !root.to_string().is_empty() => {
1591                format!("The entry point `{root}`")
1592            }
1593            _ => "The entry point".to_owned(),
1594        };
1595        let (entry_point_declares, fix) = match entry_point_version {
1596            Some(version) => (
1597                format!("declares kclVersion {}", version.as_str()),
1598                "Update the kclVersion setting in one of these files to match the other.",
1599            ),
1600            None => (
1601                "does not declare a kclVersion".to_owned(),
1602                "Declare the same kclVersion in the entry point, or update the setting in the imported file.",
1603            ),
1604        };
1605        let mut source_ranges = vec![declared_range];
1606        source_ranges.extend(import_range);
1607        Err(KclError::new_semantic(KclErrorDetails::new(
1608            format!(
1609                "Mixing KCL versions in a single program is not allowed. {entry_point} {entry_point_declares}, but the imported file `{path}` declares kclVersion {}. {fix}",
1610                declared.as_str(),
1611            ),
1612            source_ranges,
1613        )))
1614    }
1615
1616    /// Check an imported file before executing it. Version mismatches take
1617    /// priority over versioned syntax restrictions.
1618    /// The loader-assigned module ID identifies source and syntax metadata;
1619    /// an empty parsed program retains the default AST module ID.
1620    pub(crate) fn validate_imported_module(
1621        &self,
1622        path: &ModulePath,
1623        module_id: ModuleId,
1624        program: &Node<Program>,
1625        import_range: Option<SourceRange>,
1626    ) -> Result<(), KclError> {
1627        self.check_imported_module_kcl_version(path, program, import_range)?;
1628        if !path.is_local() {
1629            return Ok(());
1630        }
1631
1632        let ranges = self.global.never_type_ranges.get(&module_id).ok_or_else(|| {
1633            KclError::new_internal(KclErrorDetails::new(
1634                format!("Missing `never` type ranges for imported KCL module `{path}`"),
1635                import_range.into_iter().collect(),
1636            ))
1637        })?;
1638        crate::parsing::validate_never_type_ranges(
1639            ranges,
1640            crate::parsing::SyntaxSource::UserCode(self.entry_point_kcl_version()),
1641        )
1642        .map_err(|error| match import_range {
1643            Some(range) => error.add_import_location(&path.import_name(), range),
1644            None => error,
1645        })?;
1646        if !self.entry_point_version_is_v3_or_higher() {
1647            return Ok(());
1648        }
1649
1650        let source = self.global.id_to_source.get(&module_id).ok_or_else(|| {
1651            KclError::new_internal(KclErrorDetails::new(
1652                format!("Missing source for imported KCL module `{path}`"),
1653                import_range.into_iter().collect(),
1654            ))
1655        })?;
1656        let validation = crate::parsing::validate_use_keyword_source(&source.source, module_id)
1657            .and_then(|_| crate::parsing::validate_enum_keyword_source(&source.source, module_id))
1658            .and_then(|_| crate::parsing::validate_import_modifier_source(&source.source, module_id));
1659        validation.map_err(|error| match import_range {
1660            Some(range) => error.add_import_location(&path.import_name(), range),
1661            None => error,
1662        })
1663    }
1664}
1665
1666/// The kclVersion that a program's `@settings` annotations declare, with the
1667/// source range of the declaring property, or `None` when the program does not
1668/// declare one. The last declaration wins, as in
1669/// [`MetaSettings::update_from_annotation`], so this searches from the end and
1670/// stops at the first match.
1671pub(crate) fn declared_kcl_version(program: &Node<Program>) -> Result<Option<(KclVersion, SourceRange)>, KclError> {
1672    let Some(property) = program
1673        .inner_attrs
1674        .iter()
1675        .rev()
1676        .filter(|annotation| annotation.name() == Some(annotations::SETTINGS))
1677        .find_map(|annotation| {
1678            annotation
1679                .properties
1680                .as_deref()
1681                .unwrap_or_default()
1682                .iter()
1683                .rev()
1684                .find(|property| &*property.inner.key.name == annotations::SETTINGS_VERSION)
1685        })
1686    else {
1687        return Ok(None);
1688    };
1689    let value = annotations::expect_kcl_version(&property.inner.value)?;
1690    let version = value.parse::<KclVersion>().map_err(|err| {
1691        KclError::new_semantic(KclErrorDetails::new(err.to_string(), vec![property.as_source_range()]))
1692    })?;
1693    Ok(Some((version, property.as_source_range())))
1694}
1695
1696/// The effective kclVersion that a program will be run with. If there's an
1697/// error parsing, the default is used.
1698pub(crate) fn computed_kcl_version(program: &Node<Program>) -> KclVersion {
1699    match declared_kcl_version(program) {
1700        Ok(Some((version, _))) => version,
1701        Ok(None) => KclVersion::default(),
1702        Err(_) => KclVersion::default(),
1703    }
1704}
1705
1706impl GlobalState {
1707    fn new(settings: &ExecutorSettings, segment_ids_edited: AhashIndexSet<ObjectId>) -> Self {
1708        let mut global = GlobalState {
1709            machine_depth_high_water: 0,
1710            path_to_source_id: Default::default(),
1711            module_infos: Default::default(),
1712            never_type_ranges: Default::default(),
1713            artifacts: Default::default(),
1714            root_module_artifacts: Default::default(),
1715            mod_loader: Default::default(),
1716            issues: Default::default(),
1717            deprecation_version_override: None,
1718            entry_point_kcl_version: None,
1719            id_to_source: Default::default(),
1720            segment_ids_edited,
1721            drag_anchors: Vec::new(),
1722            sketch_mode: false,
1723            geometry_only: settings.geometry_only,
1724            std_not_yet_added: Default::default(),
1725            #[cfg(test)]
1726            hint_all_not_yet_added: false,
1727        };
1728
1729        let root_id = ModuleId::default();
1730        let root_path = settings.current_file.clone().unwrap_or_default();
1731        global.module_infos.insert(
1732            root_id,
1733            ModuleInfo {
1734                id: root_id,
1735                path: ModulePath::Local {
1736                    value: root_path.clone(),
1737                    original_import_path: None,
1738                },
1739                repr: ModuleRepr::Root,
1740            },
1741        );
1742        global.path_to_source_id.insert(
1743            ModulePath::Local {
1744                value: root_path,
1745                original_import_path: None,
1746            },
1747            root_id,
1748        );
1749        global
1750    }
1751
1752    pub(super) fn filenames(&self) -> IndexMap<ModuleId, ModulePath> {
1753        self.path_to_source_id.iter().map(|(k, v)| ((*v), k.clone())).collect()
1754    }
1755
1756    pub(super) fn get_source(&self, id: ModuleId) -> Option<&ModuleSource> {
1757        self.id_to_source.get(&id)
1758    }
1759}
1760
1761impl ArtifactState {
1762    pub fn cached_body_items(&self) -> usize {
1763        self.graph.item_count()
1764    }
1765
1766    pub(crate) fn clear(&mut self) {
1767        self.artifacts.clear();
1768        self.graph.clear();
1769    }
1770}
1771
1772impl ModuleArtifactState {
1773    pub fn legacy_angle_refactor_metadata(&self) -> Vec<LegacyAngleRefactorMeta> {
1774        self.refactor_metadata
1775            .iter()
1776            .filter_map(|metadata| match metadata {
1777                RefactorMetadata::LegacyAngle(metadata) => Some(*metadata),
1778                RefactorMetadata::EdgeRefactor(_) | RefactorMetadata::DirectTagFillet(_) => None,
1779            })
1780            .collect()
1781    }
1782
1783    pub(crate) fn clear(&mut self) {
1784        self.artifacts.clear();
1785        self.unprocessed_commands.clear();
1786        self.commands.clear();
1787        self.operations.clear();
1788        self.refactor_metadata.clear();
1789    }
1790
1791    pub(crate) fn restore_scene_objects(&mut self, scene_objects: &[Object]) {
1792        self.scene_objects = scene_objects.to_vec();
1793        self.object_id_generator = IncIdGenerator::new(self.scene_objects.len());
1794        self.source_range_to_object.clear();
1795        self.artifact_id_to_scene_object.clear();
1796
1797        for (expected_id, object) in self.scene_objects.iter().enumerate() {
1798            debug_assert_eq!(
1799                object.id.0, expected_id,
1800                "Restored cached scene object ID {} does not match its position {}",
1801                object.id.0, expected_id
1802            );
1803
1804            match &object.kind {
1805                ObjectKind::Wall(wall) => {
1806                    self.source_range_to_object.insert(wall.source.solid.range, object.id);
1807                }
1808                ObjectKind::Cap(cap) => {
1809                    self.source_range_to_object.insert(cap.source.solid.range, object.id);
1810                }
1811                _ => match &object.source {
1812                    kcl_api::SourceRef::Simple { range, node_path: _ } => {
1813                        self.source_range_to_object.insert(*range, object.id);
1814                    }
1815                    kcl_api::SourceRef::BackTrace { ranges } => {
1816                        // Don't map the entire backtrace, only the most specific
1817                        // range.
1818                        if let Some((range, _)) = ranges.first() {
1819                            self.source_range_to_object.insert(*range, object.id);
1820                        }
1821                    }
1822                },
1823            }
1824
1825            // Ignore placeholder artifacts.
1826            if object.artifact_id != ArtifactId::placeholder() {
1827                self.artifact_id_to_scene_object.insert(object.artifact_id, object.id);
1828            }
1829        }
1830    }
1831
1832    /// When self is a cached state, extend it with new state.
1833    pub(crate) fn extend(&mut self, other: ModuleArtifactState) {
1834        self.artifacts.extend(other.artifacts);
1835        self.unprocessed_commands.extend(other.unprocessed_commands);
1836        self.commands.extend(other.commands);
1837        self.operations.extend(other.operations);
1838        if other.scene_objects.len() > self.scene_objects.len() {
1839            self.scene_objects
1840                .extend(other.scene_objects[self.scene_objects.len()..].iter().cloned());
1841        }
1842        self.source_range_to_object.extend(other.source_range_to_object);
1843        self.artifact_id_to_scene_object
1844            .extend(other.artifact_id_to_scene_object);
1845        self.var_solutions.extend(other.var_solutions);
1846        self.refactor_metadata.extend(other.refactor_metadata);
1847    }
1848
1849    // Move unprocessed artifact commands so that we don't try to process them
1850    // again next time due to execution caching.  Returns a clone of the
1851    // commands that were moved.
1852    pub(crate) fn process_commands(&mut self) -> Vec<ArtifactCommand> {
1853        let unprocessed = std::mem::take(&mut self.unprocessed_commands);
1854        let new_module_commands = unprocessed.clone();
1855        self.commands.extend(unprocessed);
1856        new_module_commands
1857    }
1858
1859    pub(crate) fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
1860        debug_assert!(
1861            id.0 < self.scene_objects.len(),
1862            "Requested object ID {} but only have {} objects",
1863            id.0,
1864            self.scene_objects.len()
1865        );
1866        self.scene_objects.get(id.0)
1867    }
1868
1869    pub(crate) fn scene_object_by_id_mut(&mut self, id: ObjectId) -> Option<&mut Object> {
1870        debug_assert!(
1871            id.0 < self.scene_objects.len(),
1872            "Requested object ID {} but only have {} objects",
1873            id.0,
1874            self.scene_objects.len()
1875        );
1876        self.scene_objects.get_mut(id.0)
1877    }
1878}
1879
1880impl ModuleState {
1881    pub(super) fn new(
1882        path: ModulePath,
1883        memory: Arc<ProgramMemory>,
1884        module_id: Option<ModuleId>,
1885        sketch_mode: bool,
1886        freedom_analysis: bool,
1887    ) -> Self {
1888        let state_module_id = module_id.unwrap_or_default();
1889        ModuleState {
1890            module_id: state_module_id,
1891            id_generator: IdGenerator::new(module_id),
1892            stack: memory.new_stack(),
1893            call_stack_size: 0,
1894            machine_call_depth: 0,
1895            pipe_value: Default::default(),
1896            being_declared: Default::default(),
1897            sketch_block: Default::default(),
1898            stdlib_entry_source_range: Default::default(),
1899            module_exports: Default::default(),
1900            not_yet_added: Default::default(),
1901            explicit_length_units: false,
1902            path,
1903            settings: Default::default(),
1904            sketch_mode,
1905            freedom_analysis,
1906            artifacts: Default::default(),
1907            constraint_state: Default::default(),
1908            allowed_warnings: Vec::new(),
1909            denied_warnings: Vec::new(),
1910            consumed_solids: AHashMap::default(),
1911            consumed_solid_ids: AHashMap::default(),
1912            consumed_regions: AHashMap::default(),
1913            inside_stdlib: false,
1914        }
1915    }
1916
1917    pub(super) fn variables(&self, main_ref: EnvironmentRef) -> Result<IndexMap<String, KclValue>, KclError> {
1918        self.stack.find_all_in_env_owned(main_ref)
1919    }
1920}
1921
1922impl SketchBlockState {
1923    pub(crate) fn next_sketch_var_id(&self) -> SketchVarId {
1924        SketchVarId(self.sketch_vars.len())
1925    }
1926
1927    /// Given a solve outcome, return the solutions for the sketch variables and
1928    /// enough information to update them in the source.
1929    pub(crate) fn var_solutions(
1930        &self,
1931        solve_outcome: &Solved,
1932        solution_ty: NumericType,
1933        sketch_block_range: SourceRange,
1934    ) -> Result<Vec<(SourceRange, Option<NodePath>, Number)>, KclError> {
1935        self.sketch_vars
1936            .iter()
1937            .map(|v| {
1938                let Some(sketch_var) = v.as_sketch_var() else {
1939                    return Err(KclError::new_internal(KclErrorDetails::new(
1940                        "Expected sketch variable".to_owned(),
1941                        vec![sketch_block_range],
1942                    )));
1943                };
1944                let var_index = sketch_var.id.0;
1945                let solved_n = solve_outcome.final_values.get(var_index).ok_or_else(|| {
1946                    let message = format!("No solution for sketch variable with id {}", var_index);
1947                    debug_assert!(false, "{}", &message);
1948                    KclError::new_internal(KclErrorDetails::new(
1949                        message,
1950                        sketch_var.meta.iter().map(|m| m.source_range).collect(),
1951                    ))
1952                })?;
1953                let solved_value = Number {
1954                    value: *solved_n,
1955                    units: solution_ty.try_into().map_err(|_| {
1956                        KclError::new_internal(KclErrorDetails::new(
1957                            "Failed to convert numeric type to units".to_owned(),
1958                            vec![sketch_block_range],
1959                        ))
1960                    })?,
1961                };
1962                let Some(source_range) = sketch_var.meta.first().map(|m| m.source_range) else {
1963                    return Ok(None);
1964                };
1965                Ok(Some((source_range, sketch_var.node_path.clone(), solved_value)))
1966            })
1967            .filter_map(Result::transpose)
1968            .collect::<Result<Vec<_>, KclError>>()
1969    }
1970}
1971
1972#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS)]
1973#[ts(export)]
1974#[serde(rename_all = "camelCase")]
1975pub struct MetaSettings {
1976    pub default_length_units: UnitLength,
1977    pub default_angle_units: UnitAngle,
1978    pub experimental_features: annotations::WarningLevel,
1979    pub kcl_version: KclVersion,
1980}
1981
1982impl Default for MetaSettings {
1983    fn default() -> Self {
1984        MetaSettings {
1985            default_length_units: UnitLength::Millimeters,
1986            default_angle_units: UnitAngle::Degrees,
1987            experimental_features: annotations::WarningLevel::Deny,
1988            kcl_version: KclVersion::default(),
1989        }
1990    }
1991}
1992
1993impl MetaSettings {
1994    pub(crate) fn update_from_annotation(
1995        &mut self,
1996        annotation: &crate::parsing::ast::types::Node<Annotation>,
1997    ) -> Result<(bool, bool), KclError> {
1998        let properties = annotations::expect_properties(annotations::SETTINGS, annotation)?;
1999
2000        let mut updated_len = false;
2001        let mut updated_angle = false;
2002        for p in properties {
2003            match &*p.inner.key.name {
2004                annotations::SETTINGS_UNIT_LENGTH => {
2005                    let value = annotations::expect_ident(&p.inner.value)?;
2006                    let value = super::types::length_from_str(value, annotation.as_source_range())?;
2007                    self.default_length_units = value;
2008                    updated_len = true;
2009                }
2010                annotations::SETTINGS_UNIT_ANGLE => {
2011                    let value = annotations::expect_ident(&p.inner.value)?;
2012                    let value = super::types::angle_from_str(value, annotation.as_source_range())?;
2013                    self.default_angle_units = value;
2014                    updated_angle = true;
2015                }
2016                annotations::SETTINGS_VERSION => {
2017                    let value = annotations::expect_kcl_version(&p.inner.value)?;
2018                    self.kcl_version = value.parse::<KclVersion>().map_err(|err| {
2019                        KclError::new_semantic(KclErrorDetails::new(err.to_string(), p.as_source_ranges()))
2020                    })?;
2021                }
2022                annotations::SETTINGS_EXPERIMENTAL_FEATURES => {
2023                    let value = annotations::expect_ident(&p.inner.value)?;
2024                    let value = annotations::WarningLevel::from_str(value).map_err(|_| {
2025                        KclError::new_semantic(KclErrorDetails::new(
2026                            format!(
2027                                "Invalid value for {} settings property, expected one of: {}",
2028                                annotations::SETTINGS_EXPERIMENTAL_FEATURES,
2029                                annotations::WARN_LEVELS.join(", ")
2030                            ),
2031                            annotation.as_source_ranges(),
2032                        ))
2033                    })?;
2034                    self.experimental_features = value;
2035                }
2036                name => {
2037                    return Err(KclError::new_semantic(KclErrorDetails::new(
2038                        format!(
2039                            "Unexpected settings key: `{name}`; expected one of `{}`, `{}`",
2040                            annotations::SETTINGS_UNIT_LENGTH,
2041                            annotations::SETTINGS_UNIT_ANGLE
2042                        ),
2043                        vec![annotation.as_source_range()],
2044                    )));
2045                }
2046            }
2047        }
2048
2049        Ok((updated_len, updated_angle))
2050    }
2051}
2052
2053#[cfg(test)]
2054mod tests {
2055
2056    use kcl_api::Object;
2057    use kcl_api::ObjectId;
2058    use kcl_api::ObjectKind;
2059    use kcl_api::Plane;
2060    use kcl_api::SourceRef;
2061    use uuid::Uuid;
2062
2063    use super::KclVersion;
2064    use super::ModuleArtifactState;
2065    use crate::NodePath;
2066    use crate::NodePathExt;
2067    use crate::SourceRange;
2068    use crate::execution::ArtifactId;
2069
2070    #[test]
2071    fn declared_kcl_version_finds_the_setting_and_its_range() {
2072        let parse = |code: &str| crate::parsing::top_level_parse(code).unwrap();
2073
2074        assert_eq!(super::declared_kcl_version(&parse("x = 1\n")).unwrap(), None);
2075        assert_eq!(
2076            super::declared_kcl_version(&parse("@settings(defaultLengthUnit = in)\nx = 1\n")).unwrap(),
2077            None
2078        );
2079
2080        let code = "@settings(defaultLengthUnit = in, kclVersion = 2.0)\nx = 1\n";
2081        let (version, range) = super::declared_kcl_version(&parse(code)).unwrap().unwrap();
2082        assert_eq!(version, KclVersion::V2);
2083        let start = code.find("kclVersion").unwrap();
2084        assert_eq!((range.start(), range.end()), (start, start + "kclVersion = 2.0".len()));
2085
2086        let (version, _) = super::declared_kcl_version(&parse("@settings(kclVersion = \"3.0-preview\")\n"))
2087            .unwrap()
2088            .unwrap();
2089        assert_eq!(version, KclVersion::V3Preview);
2090
2091        // The last declaration wins, whether it is in a later annotation or
2092        // later within the same annotation.
2093        let code = "@settings(kclVersion = 1.0)\n@settings(defaultLengthUnit = in)\n@settings(kclVersion = 2.0, kclVersion = \"3.0-preview\")\n";
2094        let (version, range) = super::declared_kcl_version(&parse(code)).unwrap().unwrap();
2095        assert_eq!(version, KclVersion::V3Preview);
2096        let start = code.rfind("kclVersion").unwrap();
2097        assert_eq!(
2098            (range.start(), range.end()),
2099            (start, start + "kclVersion = \"3.0-preview\"".len())
2100        );
2101
2102        // An unknown version is an error located at the setting.
2103        let code = "@settings(kclVersion = 9.0)\n";
2104        let error = super::declared_kcl_version(&parse(code)).unwrap_err();
2105        let start = code.find("kclVersion").unwrap();
2106        assert_eq!(
2107            error.source_ranges().first().map(|range| (range.start(), range.end())),
2108            Some((start, start + "kclVersion = 9.0".len()))
2109        );
2110    }
2111
2112    #[test]
2113    fn kcl_version_serializes_as_canonical_setting_value() {
2114        assert_eq!(serde_json::to_string(&KclVersion::V1).unwrap(), r#""1.0""#);
2115        assert_eq!(serde_json::to_string(&KclVersion::V2).unwrap(), r#""2.0""#);
2116        assert_eq!(serde_json::to_string(&KclVersion::V3).unwrap(), r#""3.0""#);
2117        assert_eq!(
2118            serde_json::to_string(&KclVersion::V3Preview).unwrap(),
2119            r#""3.0-preview""#
2120        );
2121    }
2122
2123    #[test]
2124    fn restore_scene_objects_rebuilds_lookup_maps() {
2125        let plane_artifact_id = ArtifactId::new(Uuid::from_u128(1));
2126        let sketch_artifact_id = ArtifactId::new(Uuid::from_u128(2));
2127        let plane_range = SourceRange::from([1, 4, 0]);
2128        let plane_node_path = Some(NodePath::placeholder());
2129        let sketch_ranges = vec![
2130            (SourceRange::from([5, 9, 0]), None),
2131            (SourceRange::from([10, 12, 0]), None),
2132        ];
2133        let cached_objects = vec![
2134            Object {
2135                id: ObjectId(0),
2136                kind: ObjectKind::Plane(Plane::Object(ObjectId(0))),
2137                label: Default::default(),
2138                comments: Default::default(),
2139                artifact_id: plane_artifact_id,
2140                source: SourceRef::new(plane_range, plane_node_path),
2141            },
2142            Object {
2143                id: ObjectId(1),
2144                kind: ObjectKind::Nil,
2145                label: Default::default(),
2146                comments: Default::default(),
2147                artifact_id: sketch_artifact_id,
2148                source: SourceRef::BackTrace {
2149                    ranges: sketch_ranges.clone(),
2150                },
2151            },
2152            Object::placeholder(ObjectId(2), SourceRange::from([13, 14, 0]), None),
2153        ];
2154
2155        let mut artifacts = ModuleArtifactState::default();
2156        artifacts.restore_scene_objects(&cached_objects);
2157
2158        assert_eq!(artifacts.scene_objects, cached_objects);
2159        assert_eq!(
2160            artifacts.artifact_id_to_scene_object.get(&plane_artifact_id),
2161            Some(&ObjectId(0))
2162        );
2163        assert_eq!(
2164            artifacts.artifact_id_to_scene_object.get(&sketch_artifact_id),
2165            Some(&ObjectId(1))
2166        );
2167        assert_eq!(
2168            artifacts.artifact_id_to_scene_object.get(&ArtifactId::placeholder()),
2169            None
2170        );
2171        assert_eq!(artifacts.source_range_to_object.get(&plane_range), Some(&ObjectId(0)));
2172        assert_eq!(
2173            artifacts.source_range_to_object.get(&sketch_ranges[0].0),
2174            Some(&ObjectId(1))
2175        );
2176        // We don't map all the ranges in a backtrace.
2177        assert_eq!(artifacts.source_range_to_object.get(&sketch_ranges[1].0), None);
2178    }
2179}