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