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