Skip to main content

kcl_lib/execution/
cache.rs

1//! Functions for helping with caching an ast and finding the parts the changed.
2
3use std::sync::Arc;
4
5use indexmap::IndexMap;
6use itertools::EitherOrBoth;
7use itertools::Itertools;
8use tokio::sync::RwLock;
9
10use crate::ExecOutcome;
11use crate::ExecutorContext;
12use crate::KclVersion;
13use crate::SourceRange;
14use crate::errors::KclError;
15use crate::execution::ConstraintKey;
16use crate::execution::ConstraintState;
17use crate::execution::EnvironmentRef;
18use crate::execution::ExecutorSettings;
19use crate::execution::KclValue;
20use crate::execution::KclValueView;
21use crate::execution::annotations;
22use crate::execution::memory::Stack;
23use crate::execution::state::ModuleInfoMap;
24use crate::execution::state::NotYetAdded;
25use crate::execution::state::{self as exec_state};
26use crate::front::Object;
27use crate::front::ObjectId;
28use crate::modules::ModuleId;
29use crate::modules::ModulePath;
30use crate::modules::ModuleSource;
31use crate::parsing::ast::types::Annotation;
32use crate::parsing::ast::types::Node;
33use crate::parsing::ast::types::Program;
34use crate::walk::Node as WalkNode;
35
36lazy_static::lazy_static! {
37    /// A static mutable lock for updating the last successful execution state for the cache.
38    static ref OLD_AST: Arc<RwLock<Option<GlobalState>>> = Default::default();
39    // The last successful run's memory. Not cleared after an unsuccessful run.
40    static ref PREV_MEMORY: Arc<RwLock<Option<SketchModeState>>> = Default::default();
41}
42
43/// Read the old ast memory from the lock.
44pub(super) async fn read_old_ast() -> Option<GlobalState> {
45    let old_ast = OLD_AST.read().await;
46    old_ast.clone()
47}
48
49pub(super) async fn write_old_ast(old_state: GlobalState) {
50    let mut old_ast = OLD_AST.write().await;
51    *old_ast = Some(old_state);
52}
53
54pub(crate) async fn read_old_memory() -> Option<SketchModeState> {
55    let old_mem = PREV_MEMORY.read().await;
56    old_mem.clone()
57}
58
59pub(crate) async fn write_old_memory(mem: SketchModeState) {
60    let mut old_mem = PREV_MEMORY.write().await;
61    *old_mem = Some(mem);
62}
63
64pub async fn bust_cache() {
65    let mut old_ast = OLD_AST.write().await;
66    *old_ast = None;
67}
68
69pub async fn clear_mem_cache() {
70    let mut old_mem = PREV_MEMORY.write().await;
71    *old_mem = None;
72}
73
74/// Information for the caching an AST and smartly re-executing it if we can.
75#[derive(Debug, Clone)]
76pub struct CacheInformation<'a> {
77    pub ast: &'a Node<Program>,
78    pub settings: &'a ExecutorSettings,
79}
80
81/// The cached state of the whole program.
82#[derive(Debug, Clone)]
83pub(super) struct GlobalState {
84    pub(super) main: ModuleState,
85    /// The exec state.
86    pub(super) exec_state: exec_state::GlobalState,
87    /// The last settings used for execution.
88    pub(super) settings: ExecutorSettings,
89}
90
91impl GlobalState {
92    pub fn new(
93        state: exec_state::ExecState,
94        settings: ExecutorSettings,
95        ast: Node<Program>,
96        result_env: EnvironmentRef,
97    ) -> Self {
98        Self {
99            main: ModuleState {
100                ast,
101                exec_state: state.mod_local,
102                result_env,
103            },
104            exec_state: state.global,
105            settings,
106        }
107    }
108
109    pub fn with_settings(mut self, settings: ExecutorSettings) -> GlobalState {
110        self.settings = settings;
111        self
112    }
113
114    pub fn reconstitute_exec_state(&self, ctx: &ExecutorContext) -> exec_state::ExecState {
115        exec_state::ExecState {
116            execution_callbacks: ctx.execution_callbacks.clone(),
117            global: self.exec_state.clone(),
118            mod_local: self.main.exec_state.clone(),
119        }
120    }
121
122    pub async fn into_exec_outcome(self, ctx: &ExecutorContext) -> Result<ExecOutcome, KclError> {
123        // Fields are opt-in so that we don't accidentally leak private internal
124        // state when we add more to ExecState.
125        let variables = self.main.exec_state.variables(self.main.result_env)?;
126        #[cfg(test)]
127        let test_program_memory = variables.clone();
128        let variables = variables
129            .into_iter()
130            .map(|(key, value)| (key, KclValueView::from(value)))
131            .collect();
132        Ok(ExecOutcome {
133            variables,
134            filenames: self.exec_state.filenames(),
135            operations: self.exec_state.operations_by_module(),
136            artifact_graph: self.exec_state.artifacts.graph,
137            scene_objects: self.exec_state.root_module_artifacts.scene_objects,
138            source_range_to_object: self.exec_state.root_module_artifacts.source_range_to_object,
139            var_solutions: self.exec_state.root_module_artifacts.var_solutions,
140            refactor_metadata: self.exec_state.root_module_artifacts.refactor_metadata.clone(),
141            issues: self.exec_state.issues,
142            source_files: self.exec_state.id_to_source,
143            default_planes: ctx.engine.get_default_planes().read().await.clone(),
144            #[cfg(test)]
145            test_program_memory,
146        })
147    }
148
149    pub fn mock_memory_state(&self) -> Result<SketchModeState, KclError> {
150        let mut stack = self.main.exec_state.stack.deep_clone()?;
151        stack.restore_env(self.main.result_env)?;
152
153        Ok(SketchModeState {
154            stack,
155            module_infos: self.exec_state.module_infos.clone(),
156            path_to_source_id: self.exec_state.path_to_source_id.clone(),
157            id_to_source: self.exec_state.id_to_source.clone(),
158            never_type_ranges: self.exec_state.never_type_ranges.clone(),
159            constraint_state: self.main.exec_state.constraint_state.clone(),
160            scene_objects: self.exec_state.root_module_artifacts.scene_objects.clone(),
161            std_not_yet_added: self.exec_state.std_not_yet_added.clone(),
162            kcl_version: self.exec_state.entry_point_kcl_version.unwrap_or_default(),
163        })
164    }
165}
166
167/// Per-module cached state
168#[derive(Debug, Clone)]
169pub(super) struct ModuleState {
170    /// The AST of the module.
171    pub(super) ast: Node<Program>,
172    /// The ExecState of the module.
173    pub(super) exec_state: exec_state::ModuleState,
174    /// The memory env for the module.
175    pub(super) result_env: EnvironmentRef,
176}
177
178/// Cached state for sketch mode.
179#[derive(Debug, Clone)]
180pub(crate) struct SketchModeState {
181    /// The stack of the main module.
182    pub stack: Stack,
183    /// The module info map.
184    pub module_infos: ModuleInfoMap,
185    /// Map from source file path to module ID.
186    pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
187    /// Map from module ID to source file contents.
188    pub id_to_source: IndexMap<ModuleId, ModuleSource>,
189    /// Deferred `never` type uses in imported local modules.
190    pub never_type_ranges: IndexMap<ModuleId, Vec<SourceRange>>,
191    /// Sticky per-constraint state persisted across sketch-mode mock solves.
192    pub constraint_state: IndexMap<ObjectId, IndexMap<ConstraintKey, ConstraintState>>,
193    /// The scene objects.
194    pub scene_objects: Vec<Object>,
195    /// See `GlobalState::std_not_yet_added`. Restored because a run reusing
196    /// this memory skips the prelude.
197    pub std_not_yet_added: IndexMap<String, NotYetAdded>,
198    /// The effective kclVersion (declared, or the default) of the program that
199    /// wrote this memory; see [`Self::reusable_for`].
200    pub kcl_version: KclVersion,
201}
202
203impl SketchModeState {
204    /// Whether a program with the effective `kcl_version` may reuse this
205    /// memory. Memory from another version keeps bindings, module outcomes,
206    /// and a prelude this program must not see, and the LSP worker reuses
207    /// memory with no other invalidation. Only the version counts: after other
208    /// settings changes the frontend may execute a single sketch, which needs
209    /// this memory and cannot rebuild it.
210    pub(crate) fn reusable_for(&self, kcl_version: KclVersion) -> bool {
211        self.kcl_version == kcl_version
212    }
213}
214
215/// Read a named value from the previous sketch-mode execution.
216#[doc(hidden)]
217pub async fn read_old_memory_var(name: &str) -> Option<KclValue> {
218    let memory = read_old_memory().await?;
219    memory.stack.get(name, SourceRange::default()).ok()
220}
221
222#[cfg(test)]
223impl SketchModeState {
224    pub(crate) fn new_for_tests() -> Self {
225        Self {
226            stack: Stack::new_for_tests(),
227            module_infos: ModuleInfoMap::default(),
228            path_to_source_id: Default::default(),
229            id_to_source: Default::default(),
230            never_type_ranges: Default::default(),
231            constraint_state: Default::default(),
232            scene_objects: Vec::new(),
233            std_not_yet_added: Default::default(),
234            kcl_version: KclVersion::default(),
235        }
236    }
237}
238
239/// The result of a cache check.
240#[derive(Debug, Clone, PartialEq)]
241#[allow(clippy::large_enum_variant)]
242pub(super) enum CacheResult {
243    ReExecute {
244        /// Should we clear the scene and start over?
245        clear_scene: bool,
246        /// Do we need to reapply settings?
247        reapply_settings: bool,
248        /// The program that needs to be executed.
249        program: Node<Program>,
250    },
251    /// Check only the imports, and not the main program.
252    /// Before sending this we already checked the main program and it is the same.
253    /// And we made sure the import statements > 0.
254    CheckImportsOnly {
255        /// Argument is whether we need to reapply settings.
256        reapply_settings: bool,
257        /// The ast of the main file, which did not change.
258        ast: Node<Program>,
259    },
260    /// Argument is whether we need to reapply settings.
261    NoAction(bool),
262}
263
264/// Given an old ast, old program memory and new ast, find the parts of the code that need to be
265/// re-executed.
266/// This function should never error, because in the case of any internal error, we should just pop
267/// the cache.
268///
269/// Returns `None` when there are no changes to the program, i.e. it is
270/// fully cached.
271pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
272    let mut reapply_settings = false;
273
274    // If the settings are different we might need to bust the cache.
275    // We specifically do this before checking if they are the exact same.
276    if old.settings != new.settings {
277        // If anything else is different we may not need to re-execute, but rather just
278        // run the settings again.
279        reapply_settings = true;
280    }
281
282    // If the ASTs are the EXACT same we return None.
283    // We don't even need to waste time computing the digests.
284    if old.ast == new.ast {
285        // First we need to make sure an imported file didn't change it's ast.
286        // We know they have the same imports because the ast is the same.
287        // If we have no imports, we can skip this.
288        if !old.ast.has_import_statements() {
289            return CacheResult::NoAction(reapply_settings);
290        }
291
292        // Tell the CacheResult we need to check all the imports, but the main ast is the same.
293        return CacheResult::CheckImportsOnly {
294            reapply_settings,
295            ast: old.ast.clone(),
296        };
297    }
298
299    // We have to clone just because the digests are stored inline :-(
300    let mut old_ast = old.ast.clone();
301    let mut new_ast = new.ast.clone();
302
303    // The digests should already be computed, but just in case we don't
304    // want to compare against none.
305    old_ast.compute_digest();
306    new_ast.compute_digest();
307
308    // Check if the digest is the same.
309    if old_ast.digest == new_ast.digest {
310        // First we need to make sure an imported file didn't change it's ast.
311        // We know they have the same imports because the ast is the same.
312        // If we have no imports, we can skip this.
313        if !old.ast.has_import_statements() {
314            return CacheResult::NoAction(reapply_settings);
315        }
316
317        // Tell the CacheResult we need to check all the imports, but the main ast is the same.
318        return CacheResult::CheckImportsOnly {
319            reapply_settings,
320            ast: old.ast.clone(),
321        };
322    }
323
324    // Check if the block annotations like @settings() are different.
325    if !old_ast
326        .inner_attrs
327        .iter()
328        .filter(annotations::is_significant)
329        .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
330        .all(|pair| {
331            match pair {
332                EitherOrBoth::Both(old, new) => {
333                    // Compare annotations, ignoring source ranges.  Digests must
334                    // have been computed before this.
335                    let Annotation { name, properties, .. } = &old.inner;
336                    let Annotation {
337                        name: new_name,
338                        properties: new_properties,
339                        ..
340                    } = &new.inner;
341
342                    name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
343                        && properties
344                            .as_ref()
345                            .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
346                            == new_properties
347                                .as_ref()
348                                .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
349                }
350                _ => false,
351            }
352        })
353    {
354        // If any of the annotations are different at the beginning of the
355        // program, it's likely the settings, and we have to bust the cache and
356        // re-execute the whole thing.
357        return CacheResult::ReExecute {
358            clear_scene: true,
359            reapply_settings: true,
360            program: new.ast.clone(),
361        };
362    }
363
364    // Check if the changes were only to Non-code areas, like comments or whitespace.
365    generate_changed_program(old_ast, new_ast, reapply_settings)
366}
367
368/// Force-generate a new CacheResult, even if one shouldn't be made. The
369/// way in which this gets invoked should always be through
370/// [get_changed_program]. This is purely to contain the logic on
371/// how we construct a new [CacheResult].
372///
373/// A CacheResult's program may be a *diff* of only the parts that need
374/// to be executed (only in the case of "pure additions" at time of writing.).
375/// This diff-based AST should not be persisted or used anywhere beyond the execution flow,
376/// as it will be incomplete.
377///
378/// Digests *must* be computed before calling this.
379fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
380    if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
381        let old_node: WalkNode = old.into();
382        let new_node: WalkNode = new.into();
383        old_node.digest() == new_node.digest()
384    }) {
385        // If any of the nodes are different in the stretch of body that
386        // overlaps, we have to bust cache and rebuild the scene. This
387        // means a single insertion or deletion will result in a cache
388        // bust.
389
390        return CacheResult::ReExecute {
391            clear_scene: true,
392            reapply_settings,
393            program: new_ast,
394        };
395    }
396
397    // otherwise the overlapping section of the ast bodies matches.
398    // Let's see what the rest of the slice looks like.
399
400    match new_ast.body.len().cmp(&old_ast.body.len()) {
401        std::cmp::Ordering::Less => {
402            // the new AST is shorter than the old AST -- statements
403            // were removed from the "current" code in the "new" code.
404            //
405            // Statements up until now match which means this is a
406            // "pure delete" of the remaining slice, when we get to
407            // supporting that.
408
409            // Cache bust time.
410            CacheResult::ReExecute {
411                clear_scene: true,
412                reapply_settings,
413                program: new_ast,
414            }
415        }
416        std::cmp::Ordering::Greater => {
417            // the new AST is longer than the old AST, which means
418            // statements were added to the new code we haven't previously
419            // seen.
420            //
421            // Statements up until now are the same, which means this
422            // is a "pure addition" of the remaining slice.
423
424            new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
425
426            CacheResult::ReExecute {
427                clear_scene: false,
428                reapply_settings,
429                program: new_ast,
430            }
431        }
432        std::cmp::Ordering::Equal => {
433            // currently unreachable, but let's pretend like the code
434            // above can do something meaningful here for when we get
435            // to diffing and yanking chunks of the program apart.
436
437            // We don't actually want to do anything here; so we're going
438            // to not clear and do nothing. Is this wrong? I don't think
439            // so but i think many things. This def needs to change
440            // when the code above changes.
441
442            CacheResult::NoAction(reapply_settings)
443        }
444    }
445}
446
447#[cfg(test)]
448mod tests {
449    use pretty_assertions::assert_eq;
450
451    use super::*;
452    use crate::execution::ExecTestResults;
453    use crate::execution::parse_execute;
454    use crate::execution::parse_execute_with_project_dir;
455
456    #[tokio::test(flavor = "multi_thread")]
457    async fn test_get_changed_program_same_code() {
458        let new = r#"// Remove the end face for the extrusion.
459firstSketch = startSketchOn(XY)
460  |> startProfile(at = [-12, 12])
461  |> line(end = [24, 0])
462  |> line(end = [0, -24])
463  |> line(end = [-24, 0])
464  |> close()
465  |> extrude(length = 6)
466
467// Remove the end face for the extrusion.
468shell(firstSketch, faces = [END], thickness = 0.25)"#;
469
470        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
471
472        let result = get_changed_program(
473            CacheInformation {
474                ast: &program.ast,
475                settings: &exec_ctxt.settings,
476            },
477            CacheInformation {
478                ast: &program.ast,
479                settings: &exec_ctxt.settings,
480            },
481        )
482        .await;
483
484        assert_eq!(result, CacheResult::NoAction(false));
485        exec_ctxt.close().await;
486    }
487
488    #[tokio::test(flavor = "multi_thread")]
489    async fn test_get_changed_program_same_code_changed_whitespace() {
490        let old = r#" // Remove the end face for the extrusion.
491firstSketch = startSketchOn(XY)
492  |> startProfile(at = [-12, 12])
493  |> line(end = [24, 0])
494  |> line(end = [0, -24])
495  |> line(end = [-24, 0])
496  |> close()
497  |> extrude(length = 6)
498
499// Remove the end face for the extrusion.
500shell(firstSketch, faces = [END], thickness = 0.25) "#;
501
502        let new = r#"// Remove the end face for the extrusion.
503firstSketch = startSketchOn(XY)
504  |> startProfile(at = [-12, 12])
505  |> line(end = [24, 0])
506  |> line(end = [0, -24])
507  |> line(end = [-24, 0])
508  |> close()
509  |> extrude(length = 6)
510
511// Remove the end face for the extrusion.
512shell(firstSketch, faces = [END], thickness = 0.25)"#;
513
514        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
515
516        let program_new = crate::Program::parse_no_errs(new).unwrap();
517
518        let result = get_changed_program(
519            CacheInformation {
520                ast: &program.ast,
521                settings: &exec_ctxt.settings,
522            },
523            CacheInformation {
524                ast: &program_new.ast,
525                settings: &exec_ctxt.settings,
526            },
527        )
528        .await;
529
530        assert_eq!(result, CacheResult::NoAction(false));
531        exec_ctxt.close().await;
532    }
533
534    #[tokio::test(flavor = "multi_thread")]
535    async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
536        let old = r#" // Removed the end face for the extrusion.
537firstSketch = startSketchOn(XY)
538  |> startProfile(at = [-12, 12])
539  |> line(end = [24, 0])
540  |> line(end = [0, -24])
541  |> line(end = [-24, 0])
542  |> close()
543  |> extrude(length = 6)
544
545// Remove the end face for the extrusion.
546shell(firstSketch, faces = [END], thickness = 0.25) "#;
547
548        let new = r#"// Remove the end face for the extrusion.
549firstSketch = startSketchOn(XY)
550  |> startProfile(at = [-12, 12])
551  |> line(end = [24, 0])
552  |> line(end = [0, -24])
553  |> line(end = [-24, 0])
554  |> close()
555  |> extrude(length = 6)
556
557// Remove the end face for the extrusion.
558shell(firstSketch, faces = [END], thickness = 0.25)"#;
559
560        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
561
562        let program_new = crate::Program::parse_no_errs(new).unwrap();
563
564        let result = get_changed_program(
565            CacheInformation {
566                ast: &program.ast,
567                settings: &exec_ctxt.settings,
568            },
569            CacheInformation {
570                ast: &program_new.ast,
571                settings: &exec_ctxt.settings,
572            },
573        )
574        .await;
575
576        assert_eq!(result, CacheResult::NoAction(false));
577        exec_ctxt.close().await;
578    }
579
580    #[tokio::test(flavor = "multi_thread")]
581    async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
582        let old = r#"@foo(whatever = whatever)
583@bar
584// Removed the end face for the extrusion.
585firstSketch = startSketchOn(XY)
586  |> startProfile(at = [-12, 12])
587  |> line(end = [24, 0])
588  |> line(end = [0, -24])
589  |> line(end = [-24, 0]) // my thing
590  |> close()
591  |> extrude(length = 6)
592
593// Remove the end face for the extrusion.
594shell(firstSketch, faces = [END], thickness = 0.25) "#;
595
596        let new = r#"@foo(whatever = 42)
597@baz
598// Remove the end face for the extrusion.
599firstSketch = startSketchOn(XY)
600  |> startProfile(at = [-12, 12])
601  |> line(end = [24, 0])
602  |> line(end = [0, -24])
603  |> line(end = [-24, 0])
604  |> close()
605  |> extrude(length = 6)
606
607// Remove the end face for the extrusion.
608shell(firstSketch, faces = [END], thickness = 0.25)"#;
609
610        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
611
612        let program_new = crate::Program::parse_no_errs(new).unwrap();
613
614        let result = get_changed_program(
615            CacheInformation {
616                ast: &program.ast,
617                settings: &exec_ctxt.settings,
618            },
619            CacheInformation {
620                ast: &program_new.ast,
621                settings: &exec_ctxt.settings,
622            },
623        )
624        .await;
625
626        assert_eq!(result, CacheResult::NoAction(false));
627        exec_ctxt.close().await;
628    }
629
630    // Changing the grid settings with the exact same file should NOT bust the cache.
631    #[tokio::test(flavor = "multi_thread")]
632    async fn test_get_changed_program_same_code_but_different_grid_setting() {
633        let new = r#"// Remove the end face for the extrusion.
634firstSketch = startSketchOn(XY)
635  |> startProfile(at = [-12, 12])
636  |> line(end = [24, 0])
637  |> line(end = [0, -24])
638  |> line(end = [-24, 0])
639  |> close()
640  |> extrude(length = 6)
641
642// Remove the end face for the extrusion.
643shell(firstSketch, faces = [END], thickness = 0.25)"#;
644
645        let ExecTestResults {
646            program, mut exec_ctxt, ..
647        } = parse_execute(new).await.unwrap();
648
649        // Change the settings.
650        exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
651
652        let result = get_changed_program(
653            CacheInformation {
654                ast: &program.ast,
655                settings: &Default::default(),
656            },
657            CacheInformation {
658                ast: &program.ast,
659                settings: &exec_ctxt.settings,
660            },
661        )
662        .await;
663
664        assert_eq!(result, CacheResult::NoAction(true));
665        exec_ctxt.close().await;
666    }
667
668    // Changing the edge visibility settings with the exact same file should NOT bust the cache.
669    #[tokio::test(flavor = "multi_thread")]
670    async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
671        let new = r#"// Remove the end face for the extrusion.
672firstSketch = startSketchOn(XY)
673  |> startProfile(at = [-12, 12])
674  |> line(end = [24, 0])
675  |> line(end = [0, -24])
676  |> line(end = [-24, 0])
677  |> close()
678  |> extrude(length = 6)
679
680// Remove the end face for the extrusion.
681shell(firstSketch, faces = [END], thickness = 0.25)"#;
682
683        let ExecTestResults {
684            program, mut exec_ctxt, ..
685        } = parse_execute(new).await.unwrap();
686
687        // Change the settings.
688        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
689
690        let result = get_changed_program(
691            CacheInformation {
692                ast: &program.ast,
693                settings: &Default::default(),
694            },
695            CacheInformation {
696                ast: &program.ast,
697                settings: &exec_ctxt.settings,
698            },
699        )
700        .await;
701
702        assert_eq!(result, CacheResult::NoAction(true));
703
704        // Change the settings back.
705        let old_settings = exec_ctxt.settings.clone();
706        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
707
708        let result = get_changed_program(
709            CacheInformation {
710                ast: &program.ast,
711                settings: &old_settings,
712            },
713            CacheInformation {
714                ast: &program.ast,
715                settings: &exec_ctxt.settings,
716            },
717        )
718        .await;
719
720        assert_eq!(result, CacheResult::NoAction(true));
721
722        // Change the settings back.
723        let old_settings = exec_ctxt.settings.clone();
724        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
725
726        let result = get_changed_program(
727            CacheInformation {
728                ast: &program.ast,
729                settings: &old_settings,
730            },
731            CacheInformation {
732                ast: &program.ast,
733                settings: &exec_ctxt.settings,
734            },
735        )
736        .await;
737
738        assert_eq!(result, CacheResult::NoAction(true));
739        exec_ctxt.close().await;
740    }
741
742    // Changing the units settings using an annotation with the exact same file
743    // should bust the cache.
744    #[tokio::test(flavor = "multi_thread")]
745    async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
746        let old_code = r#"@settings(defaultLengthUnit = in)
747startSketchOn(XY)
748"#;
749        let new_code = r#"@settings(defaultLengthUnit = mm)
750startSketchOn(XY)
751"#;
752
753        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
754
755        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
756        new_program.compute_digest();
757
758        let result = get_changed_program(
759            CacheInformation {
760                ast: &program.ast,
761                settings: &exec_ctxt.settings,
762            },
763            CacheInformation {
764                ast: &new_program.ast,
765                settings: &exec_ctxt.settings,
766            },
767        )
768        .await;
769
770        assert_eq!(
771            result,
772            CacheResult::ReExecute {
773                clear_scene: true,
774                reapply_settings: true,
775                program: new_program.ast,
776            }
777        );
778        exec_ctxt.close().await;
779    }
780
781    // Removing the units settings using an annotation, when it was non-default
782    // units, with the exact same file should bust the cache.
783    #[tokio::test(flavor = "multi_thread")]
784    async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
785        let old_code = r#"@settings(defaultLengthUnit = in)
786startSketchOn(XY)
787"#;
788        let new_code = r#"
789startSketchOn(XY)
790"#;
791
792        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
793
794        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
795        new_program.compute_digest();
796
797        let result = get_changed_program(
798            CacheInformation {
799                ast: &program.ast,
800                settings: &exec_ctxt.settings,
801            },
802            CacheInformation {
803                ast: &new_program.ast,
804                settings: &exec_ctxt.settings,
805            },
806        )
807        .await;
808
809        assert_eq!(
810            result,
811            CacheResult::ReExecute {
812                clear_scene: true,
813                reapply_settings: true,
814                program: new_program.ast,
815            }
816        );
817        exec_ctxt.close().await;
818    }
819
820    #[tokio::test(flavor = "multi_thread")]
821    async fn test_multi_file_no_changes_does_not_reexecute() {
822        let code = r#"import "toBeImported.kcl" as importedCube
823
824importedCube
825
826sketch001 = startSketchOn(XZ)
827profile001 = startProfile(sketch001, at = [-134.53, -56.17])
828  |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
829  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
830  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
831  |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
832  |> close()
833extrude001 = extrude(profile001, length = 100)
834sketch003 = startSketchOn(extrude001, face = seg02)
835sketch002 = startSketchOn(extrude001, face = seg01)
836"#;
837
838        let other_file = (
839            std::path::PathBuf::from("toBeImported.kcl"),
840            r#"sketch001 = startSketchOn(XZ)
841profile001 = startProfile(sketch001, at = [281.54, 305.81])
842  |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
843  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
844  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
845  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
846  |> close()
847extrude(profile001, length = 100)"#
848                .to_string(),
849        );
850
851        let tmp_dir = std::env::temp_dir();
852        let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
853
854        // Create a temporary file for each of the other files.
855        let tmp_file = tmp_dir.join(other_file.0);
856        std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
857        std::fs::write(tmp_file, other_file.1).unwrap();
858
859        let ExecTestResults { program, exec_ctxt, .. } =
860            parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
861                .await
862                .unwrap();
863
864        let mut new_program = crate::Program::parse_no_errs(code).unwrap();
865        new_program.compute_digest();
866
867        let result = get_changed_program(
868            CacheInformation {
869                ast: &program.ast,
870                settings: &exec_ctxt.settings,
871            },
872            CacheInformation {
873                ast: &new_program.ast,
874                settings: &exec_ctxt.settings,
875            },
876        )
877        .await;
878
879        let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
880            panic!("Expected CheckImportsOnly, got {result:?}");
881        };
882
883        assert_eq!(reapply_settings, false);
884        exec_ctxt.close().await;
885    }
886
887    #[tokio::test(flavor = "multi_thread")]
888    async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
889        let code = r#"import "toBeImported.kcl" as importedCube
890
891importedCube
892
893sketch001 = startSketchOn(XZ)
894profile001 = startProfile(sketch001, at = [-134.53, -56.17])
895  |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
896  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
897  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
898  |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
899  |> close()
900extrude001 = extrude(profile001, length = 100)
901sketch003 = startSketchOn(extrude001, face = seg02)
902sketch002 = startSketchOn(extrude001, face = seg01)
903"#;
904
905        let other_file = (
906            std::path::PathBuf::from("toBeImported.kcl"),
907            r#"sketch001 = startSketchOn(XZ)
908profile001 = startProfile(sketch001, at = [281.54, 305.81])
909  |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
910  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
911  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
912  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
913  |> close()
914extrude(profile001, length = 100)"#
915                .to_string(),
916        );
917
918        let other_file2 = (
919            std::path::PathBuf::from("toBeImported.kcl"),
920            r#"sketch001 = startSketchOn(XZ)
921profile001 = startProfile(sketch001, at = [281.54, 305.81])
922  |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
923  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
924  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
925  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
926  |> close()
927extrude(profile001, length = 100)
928|> translate(z=100) 
929"#
930            .to_string(),
931        );
932
933        let tmp_dir = std::env::temp_dir();
934        let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
935
936        // Create a temporary file for each of the other files.
937        let tmp_file = tmp_dir.join(other_file.0);
938        std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
939        std::fs::write(&tmp_file, other_file.1).unwrap();
940
941        let ExecTestResults { program, exec_ctxt, .. } =
942            parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
943                .await
944                .unwrap();
945
946        // Change the other file.
947        std::fs::write(tmp_file, other_file2.1).unwrap();
948
949        let mut new_program = crate::Program::parse_no_errs(code).unwrap();
950        new_program.compute_digest();
951
952        let result = get_changed_program(
953            CacheInformation {
954                ast: &program.ast,
955                settings: &exec_ctxt.settings,
956            },
957            CacheInformation {
958                ast: &new_program.ast,
959                settings: &exec_ctxt.settings,
960            },
961        )
962        .await;
963
964        let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
965            panic!("Expected CheckImportsOnly, got {result:?}");
966        };
967
968        assert_eq!(reapply_settings, false);
969        exec_ctxt.close().await;
970    }
971
972    #[tokio::test(flavor = "multi_thread")]
973    async fn test_get_changed_program_added_outer_attribute() {
974        let old_code = r#"import "tests/inputs/cube.step"
975"#;
976        let new_code = r#"@(coords = opengl)
977import "tests/inputs/cube.step"
978"#;
979
980        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
981
982        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
983        new_program.compute_digest();
984
985        let result = get_changed_program(
986            CacheInformation {
987                ast: &program.ast,
988                settings: &exec_ctxt.settings,
989            },
990            CacheInformation {
991                ast: &new_program.ast,
992                settings: &exec_ctxt.settings,
993            },
994        )
995        .await;
996
997        assert_eq!(
998            result,
999            CacheResult::ReExecute {
1000                clear_scene: true,
1001                reapply_settings: false,
1002                program: new_program.ast,
1003            }
1004        );
1005        exec_ctxt.close().await;
1006    }
1007
1008    #[tokio::test(flavor = "multi_thread")]
1009    async fn test_get_changed_program_different_outer_attribute() {
1010        let old_code = r#"@(coords = vulkan)
1011import "tests/inputs/cube.step"
1012"#;
1013        let new_code = r#"@(coords = opengl)
1014import "tests/inputs/cube.step"
1015"#;
1016
1017        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
1018
1019        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
1020        new_program.compute_digest();
1021
1022        let result = get_changed_program(
1023            CacheInformation {
1024                ast: &program.ast,
1025                settings: &exec_ctxt.settings,
1026            },
1027            CacheInformation {
1028                ast: &new_program.ast,
1029                settings: &exec_ctxt.settings,
1030            },
1031        )
1032        .await;
1033
1034        assert_eq!(
1035            result,
1036            CacheResult::ReExecute {
1037                clear_scene: true,
1038                reapply_settings: false,
1039                program: new_program.ast,
1040            }
1041        );
1042        exec_ctxt.close().await;
1043    }
1044}