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