Skip to main content

kcl_lib/execution/
state.rs

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