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