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