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 itertools::{EitherOrBoth, Itertools};
6use tokio::sync::RwLock;
7
8use crate::{
9    execution::{annotations, memory::Stack, state::ModuleInfoMap, EnvironmentRef, ExecState, ExecutorSettings},
10    parsing::ast::types::{Annotation, Node, Program},
11    walk::Node as WalkNode,
12};
13
14lazy_static::lazy_static! {
15    /// A static mutable lock for updating the last successful execution state for the cache.
16    static ref OLD_AST: Arc<RwLock<Option<OldAstState>>> = Default::default();
17    // The last successful run's memory. Not cleared after an unssuccessful run.
18    static ref PREV_MEMORY: Arc<RwLock<Option<(Stack, ModuleInfoMap)>>> = Default::default();
19}
20
21/// Read the old ast memory from the lock.
22pub(crate) async fn read_old_ast() -> Option<OldAstState> {
23    let old_ast = OLD_AST.read().await;
24    old_ast.clone()
25}
26
27pub(super) async fn write_old_ast(old_state: OldAstState) {
28    let mut old_ast = OLD_AST.write().await;
29    *old_ast = Some(old_state);
30}
31
32pub(crate) async fn read_old_memory() -> Option<(Stack, ModuleInfoMap)> {
33    let old_mem = PREV_MEMORY.read().await;
34    old_mem.clone()
35}
36
37pub(super) async fn write_old_memory(mem: (Stack, ModuleInfoMap)) {
38    let mut old_mem = PREV_MEMORY.write().await;
39    *old_mem = Some(mem);
40}
41
42pub async fn bust_cache() {
43    let mut old_ast = OLD_AST.write().await;
44    *old_ast = None;
45}
46
47pub async fn clear_mem_cache() {
48    let mut old_mem = PREV_MEMORY.write().await;
49    *old_mem = None;
50}
51
52/// Information for the caching an AST and smartly re-executing it if we can.
53#[derive(Debug, Clone)]
54pub struct CacheInformation<'a> {
55    pub ast: &'a Node<Program>,
56    pub settings: &'a ExecutorSettings,
57}
58
59/// The old ast and program memory.
60#[derive(Debug, Clone)]
61pub struct OldAstState {
62    /// The ast.
63    pub ast: Node<Program>,
64    /// The exec state.
65    pub exec_state: ExecState,
66    /// The last settings used for execution.
67    pub settings: crate::execution::ExecutorSettings,
68    pub result_env: EnvironmentRef,
69}
70
71/// The result of a cache check.
72#[derive(Debug, Clone, PartialEq)]
73#[allow(clippy::large_enum_variant)]
74pub(super) enum CacheResult {
75    ReExecute {
76        /// Should we clear the scene and start over?
77        clear_scene: bool,
78        /// Do we need to reapply settings?
79        reapply_settings: bool,
80        /// The program that needs to be executed.
81        program: Node<Program>,
82    },
83    /// Argument is whether we need to reapply settings.
84    NoAction(bool),
85}
86
87/// Given an old ast, old program memory and new ast, find the parts of the code that need to be
88/// re-executed.
89/// This function should never error, because in the case of any internal error, we should just pop
90/// the cache.
91///
92/// Returns `None` when there are no changes to the program, i.e. it is
93/// fully cached.
94pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
95    let mut reapply_settings = false;
96
97    // If the settings are different we might need to bust the cache.
98    // We specifically do this before checking if they are the exact same.
99    if old.settings != new.settings {
100        // If anything else is different we may not need to re-execute, but rather just
101        // run the settings again.
102        reapply_settings = true;
103    }
104
105    // If the ASTs are the EXACT same we return None.
106    // We don't even need to waste time computing the digests.
107    if old.ast == new.ast {
108        return CacheResult::NoAction(reapply_settings);
109    }
110
111    // We have to clone just because the digests are stored inline :-(
112    let mut old_ast = old.ast.clone();
113    let mut new_ast = new.ast.clone();
114
115    // The digests should already be computed, but just in case we don't
116    // want to compare against none.
117    old_ast.compute_digest();
118    new_ast.compute_digest();
119
120    // Check if the digest is the same.
121    if old_ast.digest == new_ast.digest {
122        return CacheResult::NoAction(reapply_settings);
123    }
124
125    // Check if the annotations are different.
126    if !old_ast
127        .inner_attrs
128        .iter()
129        .filter(annotations::is_significant)
130        .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
131        .all(|pair| {
132            match pair {
133                EitherOrBoth::Both(old, new) => {
134                    // Compare annotations, ignoring source ranges.  Digests must
135                    // have been computed before this.
136                    let Annotation { name, properties, .. } = &old.inner;
137                    let Annotation {
138                        name: new_name,
139                        properties: new_properties,
140                        ..
141                    } = &new.inner;
142
143                    name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
144                        && properties
145                            .as_ref()
146                            .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
147                            == new_properties
148                                .as_ref()
149                                .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
150                }
151                _ => false,
152            }
153        })
154    {
155        // If any of the annotations are different at the beginning of the
156        // program, it's likely the settings, and we have to bust the cache and
157        // re-execute the whole thing.
158        return CacheResult::ReExecute {
159            clear_scene: true,
160            reapply_settings: true,
161            program: new.ast.clone(),
162        };
163    }
164
165    // Check if the changes were only to Non-code areas, like comments or whitespace.
166    generate_changed_program(old_ast, new_ast, reapply_settings)
167}
168
169/// Force-generate a new CacheResult, even if one shouldn't be made. The
170/// way in which this gets invoked should always be through
171/// [get_changed_program]. This is purely to contain the logic on
172/// how we construct a new [CacheResult].
173///
174/// Digests *must* be computed before calling this.
175fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
176    if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
177        let old_node: WalkNode = old.into();
178        let new_node: WalkNode = new.into();
179        old_node.digest() == new_node.digest()
180    }) {
181        // If any of the nodes are different in the stretch of body that
182        // overlaps, we have to bust cache and rebuild the scene. This
183        // means a single insertion or deletion will result in a cache
184        // bust.
185
186        return CacheResult::ReExecute {
187            clear_scene: true,
188            reapply_settings,
189            program: new_ast,
190        };
191    }
192
193    // otherwise the overlapping section of the ast bodies matches.
194    // Let's see what the rest of the slice looks like.
195
196    match new_ast.body.len().cmp(&old_ast.body.len()) {
197        std::cmp::Ordering::Less => {
198            // the new AST is shorter than the old AST -- statements
199            // were removed from the "current" code in the "new" code.
200            //
201            // Statements up until now match which means this is a
202            // "pure delete" of the remaining slice, when we get to
203            // supporting that.
204
205            // Cache bust time.
206            CacheResult::ReExecute {
207                clear_scene: true,
208                reapply_settings,
209                program: new_ast,
210            }
211        }
212        std::cmp::Ordering::Greater => {
213            // the new AST is longer than the old AST, which means
214            // statements were added to the new code we haven't previously
215            // seen.
216            //
217            // Statements up until now are the same, which means this
218            // is a "pure addition" of the remaining slice.
219
220            new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
221
222            CacheResult::ReExecute {
223                clear_scene: false,
224                reapply_settings,
225                program: new_ast,
226            }
227        }
228        std::cmp::Ordering::Equal => {
229            // currently unreachable, but let's pretend like the code
230            // above can do something meaningful here for when we get
231            // to diffing and yanking chunks of the program apart.
232
233            // We don't actually want to do anything here; so we're going
234            // to not clear and do nothing. Is this wrong? I don't think
235            // so but i think many things. This def needs to change
236            // when the code above changes.
237
238            CacheResult::NoAction(reapply_settings)
239        }
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use crate::execution::{parse_execute, ExecTestResults};
247
248    #[tokio::test(flavor = "multi_thread")]
249    async fn test_get_changed_program_same_code() {
250        let new = r#"// Remove the end face for the extrusion.
251firstSketch = startSketchOn('XY')
252  |> startProfileAt([-12, 12], %)
253  |> line(end = [24, 0])
254  |> line(end = [0, -24])
255  |> line(end = [-24, 0])
256  |> close()
257  |> extrude(length = 6)
258
259// Remove the end face for the extrusion.
260shell(firstSketch, faces = [END], thickness = 0.25)"#;
261
262        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
263
264        let result = get_changed_program(
265            CacheInformation {
266                ast: &program.ast,
267                settings: &exec_ctxt.settings,
268            },
269            CacheInformation {
270                ast: &program.ast,
271                settings: &exec_ctxt.settings,
272            },
273        )
274        .await;
275
276        assert_eq!(result, CacheResult::NoAction(false));
277    }
278
279    #[tokio::test(flavor = "multi_thread")]
280    async fn test_get_changed_program_same_code_changed_whitespace() {
281        let old = r#" // Remove the end face for the extrusion.
282firstSketch = startSketchOn('XY')
283  |> startProfileAt([-12, 12], %)
284  |> line(end = [24, 0])
285  |> line(end = [0, -24])
286  |> line(end = [-24, 0])
287  |> close()
288  |> extrude(length = 6)
289
290// Remove the end face for the extrusion.
291shell(firstSketch, faces = [END], thickness = 0.25) "#;
292
293        let new = r#"// Remove the end face for the extrusion.
294firstSketch = startSketchOn('XY')
295  |> startProfileAt([-12, 12], %)
296  |> line(end = [24, 0])
297  |> line(end = [0, -24])
298  |> line(end = [-24, 0])
299  |> close()
300  |> extrude(length = 6)
301
302// Remove the end face for the extrusion.
303shell(firstSketch, faces = [END], thickness = 0.25)"#;
304
305        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
306
307        let program_new = crate::Program::parse_no_errs(new).unwrap();
308
309        let result = get_changed_program(
310            CacheInformation {
311                ast: &program.ast,
312                settings: &exec_ctxt.settings,
313            },
314            CacheInformation {
315                ast: &program_new.ast,
316                settings: &exec_ctxt.settings,
317            },
318        )
319        .await;
320
321        assert_eq!(result, CacheResult::NoAction(false));
322    }
323
324    #[tokio::test(flavor = "multi_thread")]
325    async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
326        let old = r#" // Removed the end face for the extrusion.
327firstSketch = startSketchOn('XY')
328  |> startProfileAt([-12, 12], %)
329  |> line(end = [24, 0])
330  |> line(end = [0, -24])
331  |> line(end = [-24, 0])
332  |> close()
333  |> extrude(length = 6)
334
335// Remove the end face for the extrusion.
336shell(firstSketch, faces = [END], thickness = 0.25) "#;
337
338        let new = r#"// Remove the end face for the extrusion.
339firstSketch = startSketchOn('XY')
340  |> startProfileAt([-12, 12], %)
341  |> line(end = [24, 0])
342  |> line(end = [0, -24])
343  |> line(end = [-24, 0])
344  |> close()
345  |> extrude(length = 6)
346
347// Remove the end face for the extrusion.
348shell(firstSketch, faces = [END], thickness = 0.25)"#;
349
350        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
351
352        let program_new = crate::Program::parse_no_errs(new).unwrap();
353
354        let result = get_changed_program(
355            CacheInformation {
356                ast: &program.ast,
357                settings: &exec_ctxt.settings,
358            },
359            CacheInformation {
360                ast: &program_new.ast,
361                settings: &exec_ctxt.settings,
362            },
363        )
364        .await;
365
366        assert_eq!(result, CacheResult::NoAction(false));
367    }
368
369    #[tokio::test(flavor = "multi_thread")]
370    async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
371        let old = r#"@foo(whatever = whatever)
372@bar
373// Removed the end face for the extrusion.
374firstSketch = startSketchOn('XY')
375  |> startProfileAt([-12, 12], %)
376  |> line(end = [24, 0])
377  |> line(end = [0, -24])
378  |> line(end = [-24, 0]) // my thing
379  |> close()
380  |> extrude(length = 6)
381
382// Remove the end face for the extrusion.
383shell(firstSketch, faces = [END], thickness = 0.25) "#;
384
385        let new = r#"@foo(whatever = 42)
386@baz
387// Remove the end face for the extrusion.
388firstSketch = startSketchOn('XY')
389  |> startProfileAt([-12, 12], %)
390  |> line(end = [24, 0])
391  |> line(end = [0, -24])
392  |> line(end = [-24, 0])
393  |> close()
394  |> extrude(length = 6)
395
396// Remove the end face for the extrusion.
397shell(firstSketch, faces = [END], thickness = 0.25)"#;
398
399        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
400
401        let program_new = crate::Program::parse_no_errs(new).unwrap();
402
403        let result = get_changed_program(
404            CacheInformation {
405                ast: &program.ast,
406                settings: &exec_ctxt.settings,
407            },
408            CacheInformation {
409                ast: &program_new.ast,
410                settings: &exec_ctxt.settings,
411            },
412        )
413        .await;
414
415        assert_eq!(result, CacheResult::NoAction(false));
416    }
417
418    // Changing the grid settings with the exact same file should NOT bust the cache.
419    #[tokio::test(flavor = "multi_thread")]
420    async fn test_get_changed_program_same_code_but_different_grid_setting() {
421        let new = r#"// Remove the end face for the extrusion.
422firstSketch = startSketchOn('XY')
423  |> startProfileAt([-12, 12], %)
424  |> line(end = [24, 0])
425  |> line(end = [0, -24])
426  |> line(end = [-24, 0])
427  |> close()
428  |> extrude(length = 6)
429
430// Remove the end face for the extrusion.
431shell(firstSketch, faces = [END], thickness = 0.25)"#;
432
433        let ExecTestResults {
434            program, mut exec_ctxt, ..
435        } = parse_execute(new).await.unwrap();
436
437        // Change the settings.
438        exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
439
440        let result = get_changed_program(
441            CacheInformation {
442                ast: &program.ast,
443                settings: &Default::default(),
444            },
445            CacheInformation {
446                ast: &program.ast,
447                settings: &exec_ctxt.settings,
448            },
449        )
450        .await;
451
452        assert_eq!(result, CacheResult::NoAction(true));
453    }
454
455    // Changing the edge visibility settings with the exact same file should NOT bust the cache.
456    #[tokio::test(flavor = "multi_thread")]
457    async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() {
458        let new = r#"// Remove the end face for the extrusion.
459firstSketch = startSketchOn('XY')
460  |> startProfileAt([-12, 12], %)
461  |> line(end = [24, 0])
462  |> line(end = [0, -24])
463  |> line(end = [-24, 0])
464  |> close()
465  |> extrude(length = 6)
466
467// Remove the end face for the extrusion.
468shell(firstSketch, faces = [END], thickness = 0.25)"#;
469
470        let ExecTestResults {
471            program, mut exec_ctxt, ..
472        } = parse_execute(new).await.unwrap();
473
474        // Change the settings.
475        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
476
477        let result = get_changed_program(
478            CacheInformation {
479                ast: &program.ast,
480                settings: &Default::default(),
481            },
482            CacheInformation {
483                ast: &program.ast,
484                settings: &exec_ctxt.settings,
485            },
486        )
487        .await;
488
489        assert_eq!(result, CacheResult::NoAction(true));
490
491        // Change the settings back.
492        let old_settings = exec_ctxt.settings.clone();
493        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
494
495        let result = get_changed_program(
496            CacheInformation {
497                ast: &program.ast,
498                settings: &old_settings,
499            },
500            CacheInformation {
501                ast: &program.ast,
502                settings: &exec_ctxt.settings,
503            },
504        )
505        .await;
506
507        assert_eq!(result, CacheResult::NoAction(true));
508
509        // Change the settings back.
510        let old_settings = exec_ctxt.settings.clone();
511        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
512
513        let result = get_changed_program(
514            CacheInformation {
515                ast: &program.ast,
516                settings: &old_settings,
517            },
518            CacheInformation {
519                ast: &program.ast,
520                settings: &exec_ctxt.settings,
521            },
522        )
523        .await;
524
525        assert_eq!(result, CacheResult::NoAction(true));
526    }
527
528    // Changing the units settings using an annotation with the exact same file
529    // should bust the cache.
530    #[tokio::test(flavor = "multi_thread")]
531    async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
532        let old_code = r#"@settings(defaultLengthUnit = in)
533startSketchOn('XY')
534"#;
535        let new_code = r#"@settings(defaultLengthUnit = mm)
536startSketchOn('XY')
537"#;
538
539        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
540
541        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
542        new_program.compute_digest();
543
544        let result = get_changed_program(
545            CacheInformation {
546                ast: &program.ast,
547                settings: &exec_ctxt.settings,
548            },
549            CacheInformation {
550                ast: &new_program.ast,
551                settings: &exec_ctxt.settings,
552            },
553        )
554        .await;
555
556        assert_eq!(
557            result,
558            CacheResult::ReExecute {
559                clear_scene: true,
560                reapply_settings: true,
561                program: new_program.ast
562            }
563        );
564    }
565
566    // Removing the units settings using an annotation, when it was non-default
567    // units, with the exact same file should bust the cache.
568    #[tokio::test(flavor = "multi_thread")]
569    async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
570        let old_code = r#"@settings(defaultLengthUnit = in)
571startSketchOn('XY')
572"#;
573        let new_code = r#"
574startSketchOn('XY')
575"#;
576
577        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
578
579        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
580        new_program.compute_digest();
581
582        let result = get_changed_program(
583            CacheInformation {
584                ast: &program.ast,
585                settings: &exec_ctxt.settings,
586            },
587            CacheInformation {
588                ast: &new_program.ast,
589                settings: &exec_ctxt.settings,
590            },
591        )
592        .await;
593
594        assert_eq!(
595            result,
596            CacheResult::ReExecute {
597                clear_scene: true,
598                reapply_settings: true,
599                program: new_program.ast
600            }
601        );
602    }
603}