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