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