Skip to main content

kcl_lib/execution/
state.rs

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