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, 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>>> = 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> {
33    let old_mem = PREV_MEMORY.read().await;
34    old_mem.clone()
35}
36
37pub(super) async fn write_old_memory(mem: Stack) {
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 the units are different we need to re-execute the whole thing.
101        if old.settings.units != new.settings.units {
102            return CacheResult::ReExecute {
103                clear_scene: true,
104                reapply_settings: true,
105                program: new.ast.clone(),
106            };
107        }
108
109        // If anything else is different we may not need to re-execute, but rather just
110        // run the settings again.
111        reapply_settings = true;
112    }
113
114    // If the ASTs are the EXACT same we return None.
115    // We don't even need to waste time computing the digests.
116    if old.ast == new.ast {
117        return CacheResult::NoAction(reapply_settings);
118    }
119
120    // We have to clone just because the digests are stored inline :-(
121    let mut old_ast = old.ast.clone();
122    let mut new_ast = new.ast.clone();
123
124    // The digests should already be computed, but just in case we don't
125    // want to compare against none.
126    old_ast.compute_digest();
127    new_ast.compute_digest();
128
129    // Check if the digest is the same.
130    if old_ast.digest == new_ast.digest {
131        return CacheResult::NoAction(reapply_settings);
132    }
133
134    // Check if the annotations are different.
135    if !old_ast
136        .inner_attrs
137        .iter()
138        .filter(annotations::is_significant)
139        .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
140        .all(|pair| {
141            match pair {
142                EitherOrBoth::Both(old, new) => {
143                    // Compare annotations, ignoring source ranges.  Digests must
144                    // have been computed before this.
145                    let Annotation { name, properties, .. } = &old.inner;
146                    let Annotation {
147                        name: new_name,
148                        properties: new_properties,
149                        ..
150                    } = &new.inner;
151
152                    name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
153                        && properties
154                            .as_ref()
155                            .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
156                            == new_properties
157                                .as_ref()
158                                .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
159                }
160                _ => false,
161            }
162        })
163    {
164        // If any of the annotations are different at the beginning of the
165        // program, it's likely the settings, and we have to bust the cache and
166        // re-execute the whole thing.
167        return CacheResult::ReExecute {
168            clear_scene: true,
169            reapply_settings: true,
170            program: new.ast.clone(),
171        };
172    }
173
174    // Check if the changes were only to Non-code areas, like comments or whitespace.
175    generate_changed_program(old_ast, new_ast, reapply_settings)
176}
177
178/// Force-generate a new CacheResult, even if one shouldn't be made. The
179/// way in which this gets invoked should always be through
180/// [get_changed_program]. This is purely to contain the logic on
181/// how we construct a new [CacheResult].
182///
183/// Digests *must* be computed before calling this.
184fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
185    if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
186        let old_node: WalkNode = old.into();
187        let new_node: WalkNode = new.into();
188        old_node.digest() == new_node.digest()
189    }) {
190        // If any of the nodes are different in the stretch of body that
191        // overlaps, we have to bust cache and rebuild the scene. This
192        // means a single insertion or deletion will result in a cache
193        // bust.
194
195        return CacheResult::ReExecute {
196            clear_scene: true,
197            reapply_settings,
198            program: new_ast,
199        };
200    }
201
202    // otherwise the overlapping section of the ast bodies matches.
203    // Let's see what the rest of the slice looks like.
204
205    match new_ast.body.len().cmp(&old_ast.body.len()) {
206        std::cmp::Ordering::Less => {
207            // the new AST is shorter than the old AST -- statements
208            // were removed from the "current" code in the "new" code.
209            //
210            // Statements up until now match which means this is a
211            // "pure delete" of the remaining slice, when we get to
212            // supporting that.
213
214            // Cache bust time.
215            CacheResult::ReExecute {
216                clear_scene: true,
217                reapply_settings,
218                program: new_ast,
219            }
220        }
221        std::cmp::Ordering::Greater => {
222            // the new AST is longer than the old AST, which means
223            // statements were added to the new code we haven't previously
224            // seen.
225            //
226            // Statements up until now are the same, which means this
227            // is a "pure addition" of the remaining slice.
228
229            new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
230
231            CacheResult::ReExecute {
232                clear_scene: false,
233                reapply_settings,
234                program: new_ast,
235            }
236        }
237        std::cmp::Ordering::Equal => {
238            // currently unreachable, but let's pretend like the code
239            // above can do something meaningful here for when we get
240            // to diffing and yanking chunks of the program apart.
241
242            // We don't actually want to do anything here; so we're going
243            // to not clear and do nothing. Is this wrong? I don't think
244            // so but i think many things. This def needs to change
245            // when the code above changes.
246
247            CacheResult::NoAction(reapply_settings)
248        }
249    }
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255    use crate::execution::{parse_execute, ExecTestResults};
256
257    #[tokio::test(flavor = "multi_thread")]
258    async fn test_get_changed_program_same_code() {
259        let new = r#"// Remove the end face for the extrusion.
260firstSketch = startSketchOn('XY')
261  |> startProfileAt([-12, 12], %)
262  |> line(end = [24, 0])
263  |> line(end = [0, -24])
264  |> line(end = [-24, 0])
265  |> close()
266  |> extrude(length = 6)
267
268// Remove the end face for the extrusion.
269shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
270
271        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
272
273        let result = get_changed_program(
274            CacheInformation {
275                ast: &program.ast,
276                settings: &exec_ctxt.settings,
277            },
278            CacheInformation {
279                ast: &program.ast,
280                settings: &exec_ctxt.settings,
281            },
282        )
283        .await;
284
285        assert_eq!(result, CacheResult::NoAction(false));
286    }
287
288    #[tokio::test(flavor = "multi_thread")]
289    async fn test_get_changed_program_same_code_changed_whitespace() {
290        let old = r#" // Remove the end face for the extrusion.
291firstSketch = startSketchOn('XY')
292  |> startProfileAt([-12, 12], %)
293  |> line(end = [24, 0])
294  |> line(end = [0, -24])
295  |> line(end = [-24, 0])
296  |> close()
297  |> extrude(length = 6)
298
299// Remove the end face for the extrusion.
300shell(firstSketch, faces = ['end'], thickness = 0.25) "#;
301
302        let new = r#"// Remove the end face for the extrusion.
303firstSketch = startSketchOn('XY')
304  |> startProfileAt([-12, 12], %)
305  |> line(end = [24, 0])
306  |> line(end = [0, -24])
307  |> line(end = [-24, 0])
308  |> close()
309  |> extrude(length = 6)
310
311// Remove the end face for the extrusion.
312shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
313
314        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
315
316        let program_new = crate::Program::parse_no_errs(new).unwrap();
317
318        let result = get_changed_program(
319            CacheInformation {
320                ast: &program.ast,
321                settings: &exec_ctxt.settings,
322            },
323            CacheInformation {
324                ast: &program_new.ast,
325                settings: &exec_ctxt.settings,
326            },
327        )
328        .await;
329
330        assert_eq!(result, CacheResult::NoAction(false));
331    }
332
333    #[tokio::test(flavor = "multi_thread")]
334    async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
335        let old = r#" // Removed the end face for the extrusion.
336firstSketch = startSketchOn('XY')
337  |> startProfileAt([-12, 12], %)
338  |> line(end = [24, 0])
339  |> line(end = [0, -24])
340  |> line(end = [-24, 0])
341  |> close()
342  |> extrude(length = 6)
343
344// Remove the end face for the extrusion.
345shell(firstSketch, faces = ['end'], thickness = 0.25) "#;
346
347        let new = r#"// Remove the end face for the extrusion.
348firstSketch = startSketchOn('XY')
349  |> startProfileAt([-12, 12], %)
350  |> line(end = [24, 0])
351  |> line(end = [0, -24])
352  |> line(end = [-24, 0])
353  |> close()
354  |> extrude(length = 6)
355
356// Remove the end face for the extrusion.
357shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
358
359        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
360
361        let program_new = crate::Program::parse_no_errs(new).unwrap();
362
363        let result = get_changed_program(
364            CacheInformation {
365                ast: &program.ast,
366                settings: &exec_ctxt.settings,
367            },
368            CacheInformation {
369                ast: &program_new.ast,
370                settings: &exec_ctxt.settings,
371            },
372        )
373        .await;
374
375        assert_eq!(result, CacheResult::NoAction(false));
376    }
377
378    #[tokio::test(flavor = "multi_thread")]
379    async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
380        let old = r#"@foo(whatever = whatever)
381@bar
382// Removed the end face for the extrusion.
383firstSketch = startSketchOn('XY')
384  |> startProfileAt([-12, 12], %)
385  |> line(end = [24, 0])
386  |> line(end = [0, -24])
387  |> line(end = [-24, 0]) // my thing
388  |> close()
389  |> extrude(length = 6)
390
391// Remove the end face for the extrusion.
392shell(firstSketch, faces = ['end'], thickness = 0.25) "#;
393
394        let new = r#"@foo(whatever = 42)
395@baz
396// Remove the end face for the extrusion.
397firstSketch = startSketchOn('XY')
398  |> startProfileAt([-12, 12], %)
399  |> line(end = [24, 0])
400  |> line(end = [0, -24])
401  |> line(end = [-24, 0])
402  |> close()
403  |> extrude(length = 6)
404
405// Remove the end face for the extrusion.
406shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
407
408        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
409
410        let program_new = crate::Program::parse_no_errs(new).unwrap();
411
412        let result = get_changed_program(
413            CacheInformation {
414                ast: &program.ast,
415                settings: &exec_ctxt.settings,
416            },
417            CacheInformation {
418                ast: &program_new.ast,
419                settings: &exec_ctxt.settings,
420            },
421        )
422        .await;
423
424        assert_eq!(result, CacheResult::NoAction(false));
425    }
426
427    // Changing the units with the exact same file should bust the cache.
428    #[tokio::test(flavor = "multi_thread")]
429    async fn test_get_changed_program_same_code_but_different_units() {
430        let new = r#"// Remove the end face for the extrusion.
431firstSketch = startSketchOn('XY')
432  |> startProfileAt([-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 {
443            program, mut exec_ctxt, ..
444        } = parse_execute(new).await.unwrap();
445
446        // Change the settings to cm.
447        exec_ctxt.settings.units = crate::UnitLength::Cm;
448
449        let result = get_changed_program(
450            CacheInformation {
451                ast: &program.ast,
452                settings: &Default::default(),
453            },
454            CacheInformation {
455                ast: &program.ast,
456                settings: &exec_ctxt.settings,
457            },
458        )
459        .await;
460
461        assert_eq!(
462            result,
463            CacheResult::ReExecute {
464                clear_scene: true,
465                reapply_settings: true,
466                program: program.ast
467            }
468        );
469    }
470
471    // Changing the grid settings with the exact same file should NOT bust the cache.
472    #[tokio::test(flavor = "multi_thread")]
473    async fn test_get_changed_program_same_code_but_different_grid_setting() {
474        let new = r#"// Remove the end face for the extrusion.
475firstSketch = startSketchOn('XY')
476  |> startProfileAt([-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 {
487            program, mut exec_ctxt, ..
488        } = parse_execute(new).await.unwrap();
489
490        // Change the settings.
491        exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
492
493        let result = get_changed_program(
494            CacheInformation {
495                ast: &program.ast,
496                settings: &Default::default(),
497            },
498            CacheInformation {
499                ast: &program.ast,
500                settings: &exec_ctxt.settings,
501            },
502        )
503        .await;
504
505        assert_eq!(result, CacheResult::NoAction(true));
506    }
507
508    // Changing the edge visibility settings with the exact same file should NOT bust the cache.
509    #[tokio::test(flavor = "multi_thread")]
510    async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() {
511        let new = r#"// Remove the end face for the extrusion.
512firstSketch = startSketchOn('XY')
513  |> startProfileAt([-12, 12], %)
514  |> line(end = [24, 0])
515  |> line(end = [0, -24])
516  |> line(end = [-24, 0])
517  |> close()
518  |> extrude(length = 6)
519
520// Remove the end face for the extrusion.
521shell(firstSketch, faces = ['end'], thickness = 0.25)"#;
522
523        let ExecTestResults {
524            program, mut exec_ctxt, ..
525        } = parse_execute(new).await.unwrap();
526
527        // Change the settings.
528        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
529
530        let result = get_changed_program(
531            CacheInformation {
532                ast: &program.ast,
533                settings: &Default::default(),
534            },
535            CacheInformation {
536                ast: &program.ast,
537                settings: &exec_ctxt.settings,
538            },
539        )
540        .await;
541
542        assert_eq!(result, CacheResult::NoAction(true));
543
544        // Change the settings back.
545        let old_settings = exec_ctxt.settings.clone();
546        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
547
548        let result = get_changed_program(
549            CacheInformation {
550                ast: &program.ast,
551                settings: &old_settings,
552            },
553            CacheInformation {
554                ast: &program.ast,
555                settings: &exec_ctxt.settings,
556            },
557        )
558        .await;
559
560        assert_eq!(result, CacheResult::NoAction(true));
561
562        // Change the settings back.
563        let old_settings = exec_ctxt.settings.clone();
564        exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
565
566        let result = get_changed_program(
567            CacheInformation {
568                ast: &program.ast,
569                settings: &old_settings,
570            },
571            CacheInformation {
572                ast: &program.ast,
573                settings: &exec_ctxt.settings,
574            },
575        )
576        .await;
577
578        assert_eq!(result, CacheResult::NoAction(true));
579    }
580
581    // Changing the units settings using an annotation with the exact same file
582    // should bust the cache.
583    #[tokio::test(flavor = "multi_thread")]
584    async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
585        let old_code = r#"@settings(defaultLengthUnit = in)
586startSketchOn('XY')
587"#;
588        let new_code = r#"@settings(defaultLengthUnit = mm)
589startSketchOn('XY')
590"#;
591
592        let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
593
594        let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
595        new_program.compute_digest();
596
597        let result = get_changed_program(
598            CacheInformation {
599                ast: &program.ast,
600                settings: &exec_ctxt.settings,
601            },
602            CacheInformation {
603                ast: &new_program.ast,
604                settings: &exec_ctxt.settings,
605            },
606        )
607        .await;
608
609        assert_eq!(
610            result,
611            CacheResult::ReExecute {
612                clear_scene: true,
613                reapply_settings: true,
614                program: new_program.ast
615            }
616        );
617    }
618}