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