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