kcl_lib/execution/
state.rs

1use std::sync::Arc;
2
3use anyhow::Result;
4use indexmap::IndexMap;
5#[cfg(feature = "artifact-graph")]
6use kittycad_modeling_cmds::websocket::WebSocketResponse;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use uuid::Uuid;
10
11#[cfg(feature = "artifact-graph")]
12use crate::execution::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
13use crate::{
14    errors::{KclError, KclErrorDetails, Severity},
15    exec::DefaultPlanes,
16    execution::{
17        annotations,
18        cad_op::Operation,
19        id_generator::IdGenerator,
20        memory::{ProgramMemory, Stack},
21        types::{self, NumericType},
22        EnvironmentRef, ExecOutcome, ExecutorSettings, KclValue, UnitAngle, UnitLen,
23    },
24    modules::{ModuleId, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, ModuleSource},
25    parsing::ast::types::{Annotation, NodeRef},
26    source_range::SourceRange,
27    CompilationError, EngineManager, ExecutorContext, KclErrorWithOutputs,
28};
29
30/// State for executing a program.
31#[derive(Debug, Clone)]
32pub struct ExecState {
33    pub(super) global: GlobalState,
34    pub(super) mod_local: ModuleState,
35}
36
37pub type ModuleInfoMap = IndexMap<ModuleId, ModuleInfo>;
38
39#[derive(Debug, Clone)]
40pub(super) struct GlobalState {
41    /// Map from source file absolute path to module ID.
42    pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
43    /// Map from module ID to source file.
44    pub id_to_source: IndexMap<ModuleId, ModuleSource>,
45    /// Map from module ID to module info.
46    pub module_infos: ModuleInfoMap,
47    /// Module loader.
48    pub mod_loader: ModuleLoader,
49    /// Errors and warnings.
50    pub errors: Vec<CompilationError>,
51    #[allow(dead_code)]
52    pub artifacts: ArtifactState,
53}
54
55#[cfg(feature = "artifact-graph")]
56#[derive(Debug, Clone, Default)]
57pub(super) struct ArtifactState {
58    /// Output map of UUIDs to artifacts.
59    pub artifacts: IndexMap<ArtifactId, Artifact>,
60    /// Output commands to allow building the artifact graph by the caller.
61    /// These are accumulated in the [`ExecutorContext`] but moved here for
62    /// convenience of the execution cache.
63    pub commands: Vec<ArtifactCommand>,
64    /// Responses from the engine for `artifact_commands`.  We need to cache
65    /// this so that we can build the artifact graph.  These are accumulated in
66    /// the [`ExecutorContext`] but moved here for convenience of the execution
67    /// cache.
68    pub responses: IndexMap<Uuid, WebSocketResponse>,
69    /// Output artifact graph.
70    pub graph: ArtifactGraph,
71    /// Operations that have been performed in execution order, for display in
72    /// the Feature Tree.
73    pub operations: Vec<Operation>,
74}
75
76#[cfg(not(feature = "artifact-graph"))]
77#[derive(Debug, Clone, Default)]
78pub(super) struct ArtifactState {}
79
80#[derive(Debug, Clone)]
81pub(super) struct ModuleState {
82    /// The id generator for this module.
83    pub id_generator: IdGenerator,
84    pub stack: Stack,
85    /// The current value of the pipe operator returned from the previous
86    /// expression.  If we're not currently in a pipeline, this will be None.
87    pub pipe_value: Option<KclValue>,
88    /// The closest variable declaration being executed in any parent node in the AST.
89    /// This is used to provide better error messages, e.g. noticing when the user is trying
90    /// to use the variable `length` inside the RHS of its own definition, like `length = tan(length)`.
91    /// TODO: Make this a reference.
92    pub being_declared: Option<String>,
93    /// Identifiers that have been exported from the current module.
94    pub module_exports: Vec<String>,
95    /// Settings specified from annotations.
96    pub settings: MetaSettings,
97    pub(super) explicit_length_units: bool,
98    pub(super) path: ModulePath,
99}
100
101impl ExecState {
102    pub fn new(exec_context: &super::ExecutorContext) -> Self {
103        ExecState {
104            global: GlobalState::new(&exec_context.settings),
105            mod_local: ModuleState::new(ModulePath::Main, ProgramMemory::new(), Default::default()),
106        }
107    }
108
109    pub(super) fn reset(&mut self, exec_context: &super::ExecutorContext) {
110        let global = GlobalState::new(&exec_context.settings);
111
112        *self = ExecState {
113            global,
114            mod_local: ModuleState::new(self.mod_local.path.clone(), ProgramMemory::new(), Default::default()),
115        };
116    }
117
118    /// Log a non-fatal error.
119    pub fn err(&mut self, e: CompilationError) {
120        self.global.errors.push(e);
121    }
122
123    /// Log a warning.
124    pub fn warn(&mut self, mut e: CompilationError) {
125        e.severity = Severity::Warning;
126        self.global.errors.push(e);
127    }
128
129    pub fn errors(&self) -> &[CompilationError] {
130        &self.global.errors
131    }
132
133    /// Convert to execution outcome when running in WebAssembly.  We want to
134    /// reduce the amount of data that crosses the WASM boundary as much as
135    /// possible.
136    pub async fn to_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
137        // Fields are opt-in so that we don't accidentally leak private internal
138        // state when we add more to ExecState.
139        ExecOutcome {
140            variables: self.mod_local.variables(main_ref),
141            filenames: self.global.filenames(),
142            #[cfg(feature = "artifact-graph")]
143            operations: self.global.artifacts.operations,
144            #[cfg(feature = "artifact-graph")]
145            artifact_commands: self.global.artifacts.commands,
146            #[cfg(feature = "artifact-graph")]
147            artifact_graph: self.global.artifacts.graph,
148            errors: self.global.errors,
149            default_planes: ctx.engine.get_default_planes().read().await.clone(),
150        }
151    }
152
153    pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
154        ExecOutcome {
155            variables: self.mod_local.variables(main_ref),
156            #[cfg(feature = "artifact-graph")]
157            operations: Default::default(),
158            #[cfg(feature = "artifact-graph")]
159            artifact_commands: Default::default(),
160            #[cfg(feature = "artifact-graph")]
161            artifact_graph: Default::default(),
162            errors: self.global.errors,
163            filenames: Default::default(),
164            default_planes: ctx.engine.get_default_planes().read().await.clone(),
165        }
166    }
167
168    pub(crate) fn stack(&self) -> &Stack {
169        &self.mod_local.stack
170    }
171
172    pub(crate) fn mut_stack(&mut self) -> &mut Stack {
173        &mut self.mod_local.stack
174    }
175
176    pub fn next_uuid(&mut self) -> Uuid {
177        self.mod_local.id_generator.next_uuid()
178    }
179
180    pub fn id_generator(&mut self) -> &mut IdGenerator {
181        &mut self.mod_local.id_generator
182    }
183
184    #[cfg(feature = "artifact-graph")]
185    pub(crate) fn add_artifact(&mut self, artifact: Artifact) {
186        let id = artifact.id();
187        self.global.artifacts.artifacts.insert(id, artifact);
188    }
189
190    pub(crate) fn push_op(&mut self, op: Operation) {
191        #[cfg(feature = "artifact-graph")]
192        self.global.artifacts.operations.push(op);
193        #[cfg(not(feature = "artifact-graph"))]
194        drop(op);
195    }
196
197    pub(super) fn next_module_id(&self) -> ModuleId {
198        ModuleId::from_usize(self.global.path_to_source_id.len())
199    }
200
201    pub(super) fn id_for_module(&self, path: &ModulePath) -> Option<ModuleId> {
202        self.global.path_to_source_id.get(path).cloned()
203    }
204
205    pub(super) fn add_path_to_source_id(&mut self, path: ModulePath, id: ModuleId) {
206        debug_assert!(!self.global.path_to_source_id.contains_key(&path));
207        self.global.path_to_source_id.insert(path.clone(), id);
208    }
209
210    pub(crate) fn add_root_module_contents(&mut self, program: &crate::Program) {
211        let root_id = ModuleId::default();
212        // Get the path for the root module.
213        let path = self
214            .global
215            .path_to_source_id
216            .iter()
217            .find(|(_, v)| **v == root_id)
218            .unwrap()
219            .0
220            .clone();
221        self.add_id_to_source(
222            root_id,
223            ModuleSource {
224                path,
225                source: program.original_file_contents.to_string(),
226            },
227        );
228    }
229
230    pub(super) fn add_id_to_source(&mut self, id: ModuleId, source: ModuleSource) {
231        self.global.id_to_source.insert(id, source.clone());
232    }
233
234    pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
235        debug_assert!(self.global.path_to_source_id.contains_key(&path));
236        let module_info = ModuleInfo { id, repr, path };
237        self.global.module_infos.insert(id, module_info);
238    }
239
240    pub fn get_module(&mut self, id: ModuleId) -> Option<&ModuleInfo> {
241        self.global.module_infos.get(&id)
242    }
243
244    pub fn current_default_units(&self) -> NumericType {
245        NumericType::Default {
246            len: self.length_unit(),
247            angle: self.angle_unit(),
248        }
249    }
250
251    pub fn length_unit(&self) -> UnitLen {
252        self.mod_local.settings.default_length_units
253    }
254
255    pub fn angle_unit(&self) -> UnitAngle {
256        self.mod_local.settings.default_angle_units
257    }
258
259    pub(super) fn circular_import_error(&self, path: &ModulePath, source_range: SourceRange) -> KclError {
260        KclError::new_import_cycle(KclErrorDetails::new(
261            format!(
262                "circular import of modules is not allowed: {} -> {}",
263                self.global
264                    .mod_loader
265                    .import_stack
266                    .iter()
267                    .map(|p| p.to_string_lossy())
268                    .collect::<Vec<_>>()
269                    .join(" -> "),
270                path,
271            ),
272            vec![source_range],
273        ))
274    }
275
276    pub(crate) fn pipe_value(&self) -> Option<&KclValue> {
277        self.mod_local.pipe_value.as_ref()
278    }
279
280    pub(crate) fn error_with_outputs(
281        &self,
282        error: KclError,
283        default_planes: Option<DefaultPlanes>,
284    ) -> KclErrorWithOutputs {
285        let module_id_to_module_path: IndexMap<ModuleId, ModulePath> = self
286            .global
287            .path_to_source_id
288            .iter()
289            .map(|(k, v)| ((*v), k.clone()))
290            .collect();
291
292        KclErrorWithOutputs::new(
293            error,
294            self.errors().to_vec(),
295            #[cfg(feature = "artifact-graph")]
296            self.global.artifacts.operations.clone(),
297            #[cfg(feature = "artifact-graph")]
298            self.global.artifacts.commands.clone(),
299            #[cfg(feature = "artifact-graph")]
300            self.global.artifacts.graph.clone(),
301            module_id_to_module_path,
302            self.global.id_to_source.clone(),
303            default_planes,
304        )
305    }
306
307    #[cfg(feature = "artifact-graph")]
308    pub(crate) async fn build_artifact_graph(
309        &mut self,
310        engine: &Arc<Box<dyn EngineManager>>,
311        program: NodeRef<'_, crate::parsing::ast::types::Program>,
312    ) -> Result<(), KclError> {
313        let new_commands = engine.take_artifact_commands().await;
314        let new_responses = engine.take_responses().await;
315        let initial_graph = self.global.artifacts.graph.clone();
316
317        // Build the artifact graph.
318        let graph_result = crate::execution::artifact::build_artifact_graph(
319            &new_commands,
320            &new_responses,
321            program,
322            &mut self.global.artifacts.artifacts,
323            initial_graph,
324        );
325        // Move the artifact commands and responses into ExecState to
326        // simplify cache management and error creation.
327        self.global.artifacts.commands.extend(new_commands);
328        self.global.artifacts.responses.extend(new_responses);
329
330        let artifact_graph = graph_result?;
331        self.global.artifacts.graph = artifact_graph;
332
333        Ok(())
334    }
335
336    #[cfg(not(feature = "artifact-graph"))]
337    pub(crate) async fn build_artifact_graph(
338        &mut self,
339        _engine: &Arc<Box<dyn EngineManager>>,
340        _program: NodeRef<'_, crate::parsing::ast::types::Program>,
341    ) -> Result<(), KclError> {
342        Ok(())
343    }
344}
345
346impl GlobalState {
347    fn new(settings: &ExecutorSettings) -> Self {
348        let mut global = GlobalState {
349            path_to_source_id: Default::default(),
350            module_infos: Default::default(),
351            artifacts: Default::default(),
352            mod_loader: Default::default(),
353            errors: Default::default(),
354            id_to_source: Default::default(),
355        };
356
357        let root_id = ModuleId::default();
358        let root_path = settings.current_file.clone().unwrap_or_default();
359        global.module_infos.insert(
360            root_id,
361            ModuleInfo {
362                id: root_id,
363                path: ModulePath::Local {
364                    value: root_path.clone(),
365                },
366                repr: ModuleRepr::Root,
367            },
368        );
369        global
370            .path_to_source_id
371            .insert(ModulePath::Local { value: root_path }, root_id);
372        global
373    }
374
375    pub(super) fn filenames(&self) -> IndexMap<ModuleId, ModulePath> {
376        self.path_to_source_id.iter().map(|(k, v)| ((*v), k.clone())).collect()
377    }
378
379    pub(super) fn get_source(&self, id: ModuleId) -> Option<&ModuleSource> {
380        self.id_to_source.get(&id)
381    }
382}
383
384#[cfg(feature = "artifact-graph")]
385impl ArtifactState {
386    pub fn cached_body_items(&self) -> usize {
387        self.graph.item_count
388    }
389}
390
391impl ModuleState {
392    pub(super) fn new(path: ModulePath, memory: Arc<ProgramMemory>, module_id: Option<ModuleId>) -> Self {
393        ModuleState {
394            id_generator: IdGenerator::new(module_id),
395            stack: memory.new_stack(),
396            pipe_value: Default::default(),
397            being_declared: Default::default(),
398            module_exports: Default::default(),
399            explicit_length_units: false,
400            path,
401            settings: MetaSettings {
402                default_length_units: Default::default(),
403                default_angle_units: Default::default(),
404                kcl_version: "0.1".to_owned(),
405            },
406        }
407    }
408
409    pub(super) fn variables(&self, main_ref: EnvironmentRef) -> IndexMap<String, KclValue> {
410        self.stack
411            .find_all_in_env(main_ref)
412            .map(|(k, v)| (k.clone(), v.clone()))
413            .collect()
414    }
415}
416
417#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, ts_rs::TS, JsonSchema)]
418#[ts(export)]
419#[serde(rename_all = "camelCase")]
420pub struct MetaSettings {
421    pub default_length_units: types::UnitLen,
422    pub default_angle_units: types::UnitAngle,
423    pub kcl_version: String,
424}
425
426impl MetaSettings {
427    pub(crate) fn update_from_annotation(
428        &mut self,
429        annotation: &crate::parsing::ast::types::Node<Annotation>,
430    ) -> Result<bool, KclError> {
431        let properties = annotations::expect_properties(annotations::SETTINGS, annotation)?;
432
433        let mut updated_len = false;
434        for p in properties {
435            match &*p.inner.key.name {
436                annotations::SETTINGS_UNIT_LENGTH => {
437                    let value = annotations::expect_ident(&p.inner.value)?;
438                    let value = types::UnitLen::from_str(value, annotation.as_source_range())?;
439                    self.default_length_units = value;
440                    updated_len = true;
441                }
442                annotations::SETTINGS_UNIT_ANGLE => {
443                    let value = annotations::expect_ident(&p.inner.value)?;
444                    let value = types::UnitAngle::from_str(value, annotation.as_source_range())?;
445                    self.default_angle_units = value;
446                }
447                annotations::SETTINGS_VERSION => {
448                    let value = annotations::expect_number(&p.inner.value)?;
449                    self.kcl_version = value;
450                }
451                name => {
452                    return Err(KclError::new_semantic(KclErrorDetails::new(
453                        format!(
454                            "Unexpected settings key: `{name}`; expected one of `{}`, `{}`",
455                            annotations::SETTINGS_UNIT_LENGTH,
456                            annotations::SETTINGS_UNIT_ANGLE
457                        ),
458                        vec![annotation.as_source_range()],
459                    )))
460                }
461            }
462        }
463
464        Ok(updated_len)
465    }
466}