Skip to main content

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