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