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