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