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