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/// A CacheResult's program may be a *diff* of only the parts that need
266/// to be executed (only in the case of "pure additions" at time of writing.).
267/// This diff-based AST should not be persisted or used anywhere beyond the execution flow,
268/// as it will be incomplete.
269///
270/// Digests *must* be computed before calling this.
271fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
272    if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
273        let old_node: WalkNode = old.into();
274        let new_node: WalkNode = new.into();
275        old_node.digest() == new_node.digest()
276    }) {
277        // If any of the nodes are different in the stretch of body that
278        // overlaps, we have to bust cache and rebuild the scene. This
279        // means a single insertion or deletion will result in a cache
280        // bust.
281
282        return CacheResult::ReExecute {
283            clear_scene: true,
284            reapply_settings,
285            program: new_ast,
286        };
287    }
288
289    // otherwise the overlapping section of the ast bodies matches.
290    // Let's see what the rest of the slice looks like.
291
292    match new_ast.body.len().cmp(&old_ast.body.len()) {
293        std::cmp::Ordering::Less => {
294            // the new AST is shorter than the old AST -- statements
295            // were removed from the "current" code in the "new" code.
296            //
297            // Statements up until now match which means this is a
298            // "pure delete" of the remaining slice, when we get to
299            // supporting that.
300
301            // Cache bust time.
302            CacheResult::ReExecute {
303                clear_scene: true,
304                reapply_settings,
305                program: new_ast,
306            }
307        }
308        std::cmp::Ordering::Greater => {
309            // the new AST is longer than the old AST, which means
310            // statements were added to the new code we haven't previously
311            // seen.
312            //
313            // Statements up until now are the same, which means this
314            // is a "pure addition" of the remaining slice.
315
316            new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
317
318            CacheResult::ReExecute {
319                clear_scene: false,
320                reapply_settings,
321                program: new_ast,
322            }
323        }
324        std::cmp::Ordering::Equal => {
325            // currently unreachable, but let's pretend like the code
326            // above can do something meaningful here for when we get
327            // to diffing and yanking chunks of the program apart.
328
329            // We don't actually want to do anything here; so we're going
330            // to not clear and do nothing. Is this wrong? I don't think
331            // so but i think many things. This def needs to change
332            // when the code above changes.
333
334            CacheResult::NoAction(reapply_settings)
335        }
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use pretty_assertions::assert_eq;
342
343    use super::*;
344    use crate::execution::{ExecTestResults, parse_execute, parse_execute_with_project_dir};
345
346    #[tokio::test(flavor = "multi_thread")]
347    async fn test_get_changed_program_same_code() {
348        let new = r#"// Remove the end face for the extrusion.
349firstSketch = startSketchOn(XY)
350  |> startProfile(at = [-12, 12])
351  |> line(end = [24, 0])
352  |> line(end = [0, -24])
353  |> line(end = [-24, 0])
354  |> close()
355  |> extrude(length = 6)
356
357// Remove the end face for the extrusion.
358shell(firstSketch, faces = [END], thickness = 0.25)"#;
359
360        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
361
362        let result = get_changed_program(
363            CacheInformation {
364                ast: &program.ast,
365                settings: &exec_ctxt.settings,
366            },
367            CacheInformation {
368                ast: &program.ast,
369                settings: &exec_ctxt.settings,
370            },
371        )
372        .await;
373
374        assert_eq!(result, CacheResult::NoAction(false));
375        exec_ctxt.close().await;
376    }
377
378    #[tokio::test(flavor = "multi_thread")]
379    async fn test_get_changed_program_same_code_changed_whitespace() {
380        let old = r#" // Remove the end face for the extrusion.
381firstSketch = startSketchOn(XY)
382  |> startProfile(at = [-12, 12])
383  |> line(end = [24, 0])
384  |> line(end = [0, -24])
385  |> line(end = [-24, 0])
386  |> close()
387  |> extrude(length = 6)
388
389// Remove the end face for the extrusion.
390shell(firstSketch, faces = [END], thickness = 0.25) "#;
391
392        let new = r#"// Remove the end face for the extrusion.
393firstSketch = startSketchOn(XY)
394  |> startProfile(at = [-12, 12])
395  |> line(end = [24, 0])
396  |> line(end = [0, -24])
397  |> line(end = [-24, 0])
398  |> close()
399  |> extrude(length = 6)
400
401// Remove the end face for the extrusion.
402shell(firstSketch, faces = [END], thickness = 0.25)"#;
403
404        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
405
406        let program_new = crate::Program::parse_no_errs(new).unwrap();
407
408        let result = get_changed_program(
409            CacheInformation {
410                ast: &program.ast,
411                settings: &exec_ctxt.settings,
412            },
413            CacheInformation {
414                ast: &program_new.ast,
415                settings: &exec_ctxt.settings,
416            },
417        )
418        .await;
419
420        assert_eq!(result, CacheResult::NoAction(false));
421        exec_ctxt.close().await;
422    }
423
424    #[tokio::test(flavor = "multi_thread")]
425    async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
426        let old = r#" // Removed the end face for the extrusion.
427firstSketch = startSketchOn(XY)
428  |> startProfile(at = [-12, 12])
429  |> line(end = [24, 0])
430  |> line(end = [0, -24])
431  |> line(end = [-24, 0])
432  |> close()
433  |> extrude(length = 6)
434
435// Remove the end face for the extrusion.
436shell(firstSketch, faces = [END], thickness = 0.25) "#;
437
438        let new = r#"// Remove the end face for the extrusion.
439firstSketch = startSketchOn(XY)
440  |> startProfile(at = [-12, 12])
441  |> line(end = [24, 0])
442  |> line(end = [0, -24])
443  |> line(end = [-24, 0])
444  |> close()
445  |> extrude(length = 6)
446
447// Remove the end face for the extrusion.
448shell(firstSketch, faces = [END], thickness = 0.25)"#;
449
450        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
451
452        let program_new = crate::Program::parse_no_errs(new).unwrap();
453
454        let result = get_changed_program(
455            CacheInformation {
456                ast: &program.ast,
457                settings: &exec_ctxt.settings,
458            },
459            CacheInformation {
460                ast: &program_new.ast,
461                settings: &exec_ctxt.settings,
462            },
463        )
464        .await;
465
466        assert_eq!(result, CacheResult::NoAction(false));
467        exec_ctxt.close().await;
468    }
469
470    #[tokio::test(flavor = "multi_thread")]
471    async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
472        let old = r#"@foo(whatever = whatever)
473@bar
474// Removed the end face for the extrusion.
475firstSketch = startSketchOn(XY)
476  |> startProfile(at = [-12, 12])
477  |> line(end = [24, 0])
478  |> line(end = [0, -24])
479  |> line(end = [-24, 0]) // my thing
480  |> close()
481  |> extrude(length = 6)
482
483// Remove the end face for the extrusion.
484shell(firstSketch, faces = [END], thickness = 0.25) "#;
485
486        let new = r#"@foo(whatever = 42)
487@baz
488// Remove the end face for the extrusion.
489firstSketch = startSketchOn(XY)
490  |> startProfile(at = [-12, 12])
491  |> line(end = [24, 0])
492  |> line(end = [0, -24])
493  |> line(end = [-24, 0])
494  |> close()
495  |> extrude(length = 6)
496
497// Remove the end face for the extrusion.
498shell(firstSketch, faces = [END], thickness = 0.25)"#;
499
500        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
501
502        let program_new = crate::Program::parse_no_errs(new).unwrap();
503
504        let result = get_changed_program(
505            CacheInformation {
506                ast: &program.ast,
507                settings: &exec_ctxt.settings,
508            },
509            CacheInformation {
510                ast: &program_new.ast,
511                settings: &exec_ctxt.settings,
512            },
513        )
514        .await;
515
516        assert_eq!(result, CacheResult::NoAction(false));
517        exec_ctxt.close().await;
518    }
519
520    // Changing the grid settings with the exact same file should NOT bust the cache.
521    #[tokio::test(flavor = "multi_thread")]
522    async fn test_get_changed_program_same_code_but_different_grid_setting() {
523        let new = r#"// Remove the end face for the extrusion.
524firstSketch = startSketchOn(XY)
525  |> startProfile(at = [-12, 12])
526  |> line(end = [24, 0])
527  |> line(end = [0, -24])
528  |> line(end = [-24, 0])
529  |> close()
530  |> extrude(length = 6)
531
532// Remove the end face for the extrusion.
533shell(firstSketch, faces = [END], thickness = 0.25)"#;
534
535        let ExecTestResults {
536            program, mut exec_ctxt, ..
537        } = parse_execute(new).await.unwrap();
538
539        // Change the settings.
540        exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
541
542        let result = get_changed_program(
543            CacheInformation {
544                ast: &program.ast,
545                settings: &Default::default(),
546            },
547            CacheInformation {
548                ast: &program.ast,
549                settings: &exec_ctxt.settings,
550            },
551        )
552        .await;
553
554        assert_eq!(result, CacheResult::NoAction(true));
555        exec_ctxt.close().await;
556    }
557
558    // Changing the edge visibility settings with the exact same file should NOT bust the cache.
559    #[tokio::test(flavor = "multi_thread")]
560    async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
561        let new = r#"// Remove the end face for the extrusion.
562firstSketch = startSketchOn(XY)
563  |> startProfile(at = [-12, 12])
564  |> line(end = [24, 0])
565  |> line(end = [0, -24])
566  |> line(end = [-24, 0])
567  |> close()
568  |> extrude(length = 6)
569
570// Remove the end face for the extrusion.
571shell(firstSketch, faces = [END], thickness = 0.25)"#;
572
573        let ExecTestResults {
574            program, mut exec_ctxt, ..
575        } = parse_execute(new).await.unwrap();
576
577        // Change the settings.
578        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
579
580        let result = get_changed_program(
581            CacheInformation {
582                ast: &program.ast,
583                settings: &Default::default(),
584            },
585            CacheInformation {
586                ast: &program.ast,
587                settings: &exec_ctxt.settings,
588            },
589        )
590        .await;
591
592        assert_eq!(result, CacheResult::NoAction(true));
593
594        // Change the settings back.
595        let old_settings = exec_ctxt.settings.clone();
596        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
597
598        let result = get_changed_program(
599            CacheInformation {
600                ast: &program.ast,
601                settings: &old_settings,
602            },
603            CacheInformation {
604                ast: &program.ast,
605                settings: &exec_ctxt.settings,
606            },
607        )
608        .await;
609
610        assert_eq!(result, CacheResult::NoAction(true));
611
612        // Change the settings back.
613        let old_settings = exec_ctxt.settings.clone();
614        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
615
616        let result = get_changed_program(
617            CacheInformation {
618                ast: &program.ast,
619                settings: &old_settings,
620            },
621            CacheInformation {
622                ast: &program.ast,
623                settings: &exec_ctxt.settings,
624            },
625        )
626        .await;
627
628        assert_eq!(result, CacheResult::NoAction(true));
629        exec_ctxt.close().await;
630    }
631
632    // Changing the units settings using an annotation with the exact same file
633    // should bust the cache.
634    #[tokio::test(flavor = "multi_thread")]
635    async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
636        let old_code = r#"@settings(defaultLengthUnit = in)
637startSketchOn(XY)
638"#;
639        let new_code = r#"@settings(defaultLengthUnit = mm)
640startSketchOn(XY)
641"#;
642
643        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
644
645        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
646        new_program.compute_digest();
647
648        let result = get_changed_program(
649            CacheInformation {
650                ast: &program.ast,
651                settings: &exec_ctxt.settings,
652            },
653            CacheInformation {
654                ast: &new_program.ast,
655                settings: &exec_ctxt.settings,
656            },
657        )
658        .await;
659
660        assert_eq!(
661            result,
662            CacheResult::ReExecute {
663                clear_scene: true,
664                reapply_settings: true,
665                program: new_program.ast,
666            }
667        );
668        exec_ctxt.close().await;
669    }
670
671    // Removing the units settings using an annotation, when it was non-default
672    // units, with the exact same file should bust the cache.
673    #[tokio::test(flavor = "multi_thread")]
674    async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
675        let old_code = r#"@settings(defaultLengthUnit = in)
676startSketchOn(XY)
677"#;
678        let new_code = r#"
679startSketchOn(XY)
680"#;
681
682        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
683
684        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
685        new_program.compute_digest();
686
687        let result = get_changed_program(
688            CacheInformation {
689                ast: &program.ast,
690                settings: &exec_ctxt.settings,
691            },
692            CacheInformation {
693                ast: &new_program.ast,
694                settings: &exec_ctxt.settings,
695            },
696        )
697        .await;
698
699        assert_eq!(
700            result,
701            CacheResult::ReExecute {
702                clear_scene: true,
703                reapply_settings: true,
704                program: new_program.ast,
705            }
706        );
707        exec_ctxt.close().await;
708    }
709
710    #[tokio::test(flavor = "multi_thread")]
711    async fn test_multi_file_no_changes_does_not_reexecute() {
712        let code = r#"import "toBeImported.kcl" as importedCube
713
714importedCube
715
716sketch001 = startSketchOn(XZ)
717profile001 = startProfile(sketch001, at = [-134.53, -56.17])
718  |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
719  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
720  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
721  |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
722  |> close()
723extrude001 = extrude(profile001, length = 100)
724sketch003 = startSketchOn(extrude001, face = seg02)
725sketch002 = startSketchOn(extrude001, face = seg01)
726"#;
727
728        let other_file = (
729            std::path::PathBuf::from("toBeImported.kcl"),
730            r#"sketch001 = startSketchOn(XZ)
731profile001 = startProfile(sketch001, at = [281.54, 305.81])
732  |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
733  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
734  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
735  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
736  |> close()
737extrude(profile001, length = 100)"#
738                .to_string(),
739        );
740
741        let tmp_dir = std::env::temp_dir();
742        let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
743
744        // Create a temporary file for each of the other files.
745        let tmp_file = tmp_dir.join(other_file.0);
746        std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
747        std::fs::write(tmp_file, other_file.1).unwrap();
748
749        let ExecTestResults { program, exec_ctxt, .. } =
750            parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
751                .await
752                .unwrap();
753
754        let mut new_program = crate::Program::parse_no_errs(code).unwrap();
755        new_program.compute_digest();
756
757        let result = get_changed_program(
758            CacheInformation {
759                ast: &program.ast,
760                settings: &exec_ctxt.settings,
761            },
762            CacheInformation {
763                ast: &new_program.ast,
764                settings: &exec_ctxt.settings,
765            },
766        )
767        .await;
768
769        let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
770            panic!("Expected CheckImportsOnly, got {result:?}");
771        };
772
773        assert_eq!(reapply_settings, false);
774        exec_ctxt.close().await;
775    }
776
777    #[tokio::test(flavor = "multi_thread")]
778    async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
779        let code = r#"import "toBeImported.kcl" as importedCube
780
781importedCube
782
783sketch001 = startSketchOn(XZ)
784profile001 = startProfile(sketch001, at = [-134.53, -56.17])
785  |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
786  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
787  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
788  |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
789  |> close()
790extrude001 = extrude(profile001, length = 100)
791sketch003 = startSketchOn(extrude001, face = seg02)
792sketch002 = startSketchOn(extrude001, face = seg01)
793"#;
794
795        let other_file = (
796            std::path::PathBuf::from("toBeImported.kcl"),
797            r#"sketch001 = startSketchOn(XZ)
798profile001 = startProfile(sketch001, at = [281.54, 305.81])
799  |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
800  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
801  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
802  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
803  |> close()
804extrude(profile001, length = 100)"#
805                .to_string(),
806        );
807
808        let other_file2 = (
809            std::path::PathBuf::from("toBeImported.kcl"),
810            r#"sketch001 = startSketchOn(XZ)
811profile001 = startProfile(sketch001, at = [281.54, 305.81])
812  |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
813  |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
814  |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
815  |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
816  |> close()
817extrude(profile001, length = 100)
818|> translate(z=100) 
819"#
820            .to_string(),
821        );
822
823        let tmp_dir = std::env::temp_dir();
824        let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
825
826        // Create a temporary file for each of the other files.
827        let tmp_file = tmp_dir.join(other_file.0);
828        std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
829        std::fs::write(&tmp_file, other_file.1).unwrap();
830
831        let ExecTestResults { program, exec_ctxt, .. } =
832            parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
833                .await
834                .unwrap();
835
836        // Change the other file.
837        std::fs::write(tmp_file, other_file2.1).unwrap();
838
839        let mut new_program = crate::Program::parse_no_errs(code).unwrap();
840        new_program.compute_digest();
841
842        let result = get_changed_program(
843            CacheInformation {
844                ast: &program.ast,
845                settings: &exec_ctxt.settings,
846            },
847            CacheInformation {
848                ast: &new_program.ast,
849                settings: &exec_ctxt.settings,
850            },
851        )
852        .await;
853
854        let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
855            panic!("Expected CheckImportsOnly, got {result:?}");
856        };
857
858        assert_eq!(reapply_settings, false);
859        exec_ctxt.close().await;
860    }
861}