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