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