1use 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 static ref OLD_AST: Arc<RwLock<Option<OldAstState>>> = Default::default();
17 static ref PREV_MEMORY: Arc<RwLock<Option<(Stack, ModuleInfoMap)>>> = Default::default();
19}
20
21pub(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#[derive(Debug, Clone)]
54pub struct CacheInformation<'a> {
55 pub ast: &'a Node<Program>,
56 pub settings: &'a ExecutorSettings,
57}
58
59#[derive(Debug, Clone)]
61pub struct OldAstState {
62 pub ast: Node<Program>,
64 pub exec_state: ExecState,
66 pub settings: crate::execution::ExecutorSettings,
68 pub result_env: EnvironmentRef,
69}
70
71#[derive(Debug, Clone, PartialEq)]
73#[allow(clippy::large_enum_variant)]
74pub(super) enum CacheResult {
75 ReExecute {
76 clear_scene: bool,
78 reapply_settings: bool,
80 program: Node<Program>,
82 cached_body_items: usize,
85 },
86 CheckImportsOnly {
90 reapply_settings: bool,
92 ast: Node<Program>,
94 },
95 NoAction(bool),
97}
98
99pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
107 let mut reapply_settings = false;
108
109 if old.settings != new.settings {
112 reapply_settings = true;
115 }
116
117 if old.ast == new.ast {
120 if !old.ast.has_import_statements() {
124 return CacheResult::NoAction(reapply_settings);
125 }
126
127 return CacheResult::CheckImportsOnly {
129 reapply_settings,
130 ast: old.ast.clone(),
131 };
132 }
133
134 let mut old_ast = old.ast.clone();
136 let mut new_ast = new.ast.clone();
137
138 old_ast.compute_digest();
141 new_ast.compute_digest();
142
143 if old_ast.digest == new_ast.digest {
145 if !old.ast.has_import_statements() {
149 println!("No imports, no need to check.");
150 return CacheResult::NoAction(reapply_settings);
151 }
152
153 return CacheResult::CheckImportsOnly {
155 reapply_settings,
156 ast: old.ast.clone(),
157 };
158 }
159
160 if !old_ast
162 .inner_attrs
163 .iter()
164 .filter(annotations::is_significant)
165 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
166 .all(|pair| {
167 match pair {
168 EitherOrBoth::Both(old, new) => {
169 let Annotation { name, properties, .. } = &old.inner;
172 let Annotation {
173 name: new_name,
174 properties: new_properties,
175 ..
176 } = &new.inner;
177
178 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
179 && properties
180 .as_ref()
181 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
182 == new_properties
183 .as_ref()
184 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
185 }
186 _ => false,
187 }
188 })
189 {
190 return CacheResult::ReExecute {
194 clear_scene: true,
195 reapply_settings: true,
196 program: new.ast.clone(),
197 cached_body_items: 0,
198 };
199 }
200
201 generate_changed_program(old_ast, new_ast, reapply_settings)
203}
204
205fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
212 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
213 let old_node: WalkNode = old.into();
214 let new_node: WalkNode = new.into();
215 old_node.digest() == new_node.digest()
216 }) {
217 return CacheResult::ReExecute {
223 clear_scene: true,
224 reapply_settings,
225 program: new_ast,
226 cached_body_items: 0,
227 };
228 }
229
230 match new_ast.body.len().cmp(&old_ast.body.len()) {
234 std::cmp::Ordering::Less => {
235 CacheResult::ReExecute {
244 clear_scene: true,
245 reapply_settings,
246 program: new_ast,
247 cached_body_items: 0,
248 }
249 }
250 std::cmp::Ordering::Greater => {
251 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
259
260 CacheResult::ReExecute {
261 clear_scene: false,
262 reapply_settings,
263 program: new_ast,
264 cached_body_items: old_ast.body.len(),
265 }
266 }
267 std::cmp::Ordering::Equal => {
268 CacheResult::NoAction(reapply_settings)
278 }
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use pretty_assertions::assert_eq;
285
286 use super::*;
287 use crate::execution::{parse_execute, parse_execute_with_project_dir, ExecTestResults};
288
289 #[tokio::test(flavor = "multi_thread")]
290 async fn test_get_changed_program_same_code() {
291 let new = r#"// Remove the end face for the extrusion.
292firstSketch = startSketchOn(XY)
293 |> startProfile(at = [-12, 12])
294 |> line(end = [24, 0])
295 |> line(end = [0, -24])
296 |> line(end = [-24, 0])
297 |> close()
298 |> extrude(length = 6)
299
300// Remove the end face for the extrusion.
301shell(firstSketch, faces = [END], thickness = 0.25)"#;
302
303 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
304
305 let result = get_changed_program(
306 CacheInformation {
307 ast: &program.ast,
308 settings: &exec_ctxt.settings,
309 },
310 CacheInformation {
311 ast: &program.ast,
312 settings: &exec_ctxt.settings,
313 },
314 )
315 .await;
316
317 assert_eq!(result, CacheResult::NoAction(false));
318 }
319
320 #[tokio::test(flavor = "multi_thread")]
321 async fn test_get_changed_program_same_code_changed_whitespace() {
322 let old = r#" // Remove the end face for the extrusion.
323firstSketch = startSketchOn(XY)
324 |> startProfile(at = [-12, 12])
325 |> line(end = [24, 0])
326 |> line(end = [0, -24])
327 |> line(end = [-24, 0])
328 |> close()
329 |> extrude(length = 6)
330
331// Remove the end face for the extrusion.
332shell(firstSketch, faces = [END], thickness = 0.25) "#;
333
334 let new = r#"// Remove the end face for the extrusion.
335firstSketch = startSketchOn(XY)
336 |> startProfile(at = [-12, 12])
337 |> line(end = [24, 0])
338 |> line(end = [0, -24])
339 |> line(end = [-24, 0])
340 |> close()
341 |> extrude(length = 6)
342
343// Remove the end face for the extrusion.
344shell(firstSketch, faces = [END], thickness = 0.25)"#;
345
346 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
347
348 let program_new = crate::Program::parse_no_errs(new).unwrap();
349
350 let result = get_changed_program(
351 CacheInformation {
352 ast: &program.ast,
353 settings: &exec_ctxt.settings,
354 },
355 CacheInformation {
356 ast: &program_new.ast,
357 settings: &exec_ctxt.settings,
358 },
359 )
360 .await;
361
362 assert_eq!(result, CacheResult::NoAction(false));
363 }
364
365 #[tokio::test(flavor = "multi_thread")]
366 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
367 let old = r#" // Removed the end face for the extrusion.
368firstSketch = startSketchOn(XY)
369 |> startProfile(at = [-12, 12])
370 |> line(end = [24, 0])
371 |> line(end = [0, -24])
372 |> line(end = [-24, 0])
373 |> close()
374 |> extrude(length = 6)
375
376// Remove the end face for the extrusion.
377shell(firstSketch, faces = [END], thickness = 0.25) "#;
378
379 let new = r#"// Remove the end face for the extrusion.
380firstSketch = startSketchOn(XY)
381 |> startProfile(at = [-12, 12])
382 |> line(end = [24, 0])
383 |> line(end = [0, -24])
384 |> line(end = [-24, 0])
385 |> close()
386 |> extrude(length = 6)
387
388// Remove the end face for the extrusion.
389shell(firstSketch, faces = [END], thickness = 0.25)"#;
390
391 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
392
393 let program_new = crate::Program::parse_no_errs(new).unwrap();
394
395 let result = get_changed_program(
396 CacheInformation {
397 ast: &program.ast,
398 settings: &exec_ctxt.settings,
399 },
400 CacheInformation {
401 ast: &program_new.ast,
402 settings: &exec_ctxt.settings,
403 },
404 )
405 .await;
406
407 assert_eq!(result, CacheResult::NoAction(false));
408 }
409
410 #[tokio::test(flavor = "multi_thread")]
411 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
412 let old = r#"@foo(whatever = whatever)
413@bar
414// Removed the end face for the extrusion.
415firstSketch = startSketchOn(XY)
416 |> startProfile(at = [-12, 12])
417 |> line(end = [24, 0])
418 |> line(end = [0, -24])
419 |> line(end = [-24, 0]) // my thing
420 |> close()
421 |> extrude(length = 6)
422
423// Remove the end face for the extrusion.
424shell(firstSketch, faces = [END], thickness = 0.25) "#;
425
426 let new = r#"@foo(whatever = 42)
427@baz
428// Remove the end face for the extrusion.
429firstSketch = startSketchOn(XY)
430 |> startProfile(at = [-12, 12])
431 |> line(end = [24, 0])
432 |> line(end = [0, -24])
433 |> line(end = [-24, 0])
434 |> close()
435 |> extrude(length = 6)
436
437// Remove the end face for the extrusion.
438shell(firstSketch, faces = [END], thickness = 0.25)"#;
439
440 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
441
442 let program_new = crate::Program::parse_no_errs(new).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_new.ast,
451 settings: &exec_ctxt.settings,
452 },
453 )
454 .await;
455
456 assert_eq!(result, CacheResult::NoAction(false));
457 }
458
459 #[tokio::test(flavor = "multi_thread")]
461 async fn test_get_changed_program_same_code_but_different_grid_setting() {
462 let new = 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 ExecTestResults {
475 program, mut exec_ctxt, ..
476 } = parse_execute(new).await.unwrap();
477
478 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
480
481 let result = get_changed_program(
482 CacheInformation {
483 ast: &program.ast,
484 settings: &Default::default(),
485 },
486 CacheInformation {
487 ast: &program.ast,
488 settings: &exec_ctxt.settings,
489 },
490 )
491 .await;
492
493 assert_eq!(result, CacheResult::NoAction(true));
494 }
495
496 #[tokio::test(flavor = "multi_thread")]
498 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
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 {
512 program, mut exec_ctxt, ..
513 } = parse_execute(new).await.unwrap();
514
515 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
517
518 let result = get_changed_program(
519 CacheInformation {
520 ast: &program.ast,
521 settings: &Default::default(),
522 },
523 CacheInformation {
524 ast: &program.ast,
525 settings: &exec_ctxt.settings,
526 },
527 )
528 .await;
529
530 assert_eq!(result, CacheResult::NoAction(true));
531
532 let old_settings = exec_ctxt.settings.clone();
534 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
535
536 let result = get_changed_program(
537 CacheInformation {
538 ast: &program.ast,
539 settings: &old_settings,
540 },
541 CacheInformation {
542 ast: &program.ast,
543 settings: &exec_ctxt.settings,
544 },
545 )
546 .await;
547
548 assert_eq!(result, CacheResult::NoAction(true));
549
550 let old_settings = exec_ctxt.settings.clone();
552 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
553
554 let result = get_changed_program(
555 CacheInformation {
556 ast: &program.ast,
557 settings: &old_settings,
558 },
559 CacheInformation {
560 ast: &program.ast,
561 settings: &exec_ctxt.settings,
562 },
563 )
564 .await;
565
566 assert_eq!(result, CacheResult::NoAction(true));
567 }
568
569 #[tokio::test(flavor = "multi_thread")]
572 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
573 let old_code = r#"@settings(defaultLengthUnit = in)
574startSketchOn(XY)
575"#;
576 let new_code = r#"@settings(defaultLengthUnit = mm)
577startSketchOn(XY)
578"#;
579
580 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
581
582 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
583 new_program.compute_digest();
584
585 let result = get_changed_program(
586 CacheInformation {
587 ast: &program.ast,
588 settings: &exec_ctxt.settings,
589 },
590 CacheInformation {
591 ast: &new_program.ast,
592 settings: &exec_ctxt.settings,
593 },
594 )
595 .await;
596
597 assert_eq!(
598 result,
599 CacheResult::ReExecute {
600 clear_scene: true,
601 reapply_settings: true,
602 program: new_program.ast,
603 cached_body_items: 0,
604 }
605 );
606 }
607
608 #[tokio::test(flavor = "multi_thread")]
611 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
612 let old_code = r#"@settings(defaultLengthUnit = in)
613startSketchOn(XY)
614"#;
615 let new_code = r#"
616startSketchOn(XY)
617"#;
618
619 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
620
621 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
622 new_program.compute_digest();
623
624 let result = get_changed_program(
625 CacheInformation {
626 ast: &program.ast,
627 settings: &exec_ctxt.settings,
628 },
629 CacheInformation {
630 ast: &new_program.ast,
631 settings: &exec_ctxt.settings,
632 },
633 )
634 .await;
635
636 assert_eq!(
637 result,
638 CacheResult::ReExecute {
639 clear_scene: true,
640 reapply_settings: true,
641 program: new_program.ast,
642 cached_body_items: 0,
643 }
644 );
645 }
646
647 #[tokio::test(flavor = "multi_thread")]
648 async fn test_multi_file_no_changes_does_not_reexecute() {
649 let code = r#"import "toBeImported.kcl" as importedCube
650
651importedCube
652
653sketch001 = startSketchOn(XZ)
654profile001 = startProfile(sketch001, at = [-134.53, -56.17])
655 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
656 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
657 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
658 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
659 |> close()
660extrude001 = extrude(profile001, length = 100)
661sketch003 = startSketchOn(extrude001, face = seg02)
662sketch002 = startSketchOn(extrude001, face = seg01)
663"#;
664
665 let other_file = (
666 std::path::PathBuf::from("toBeImported.kcl"),
667 r#"sketch001 = startSketchOn(XZ)
668profile001 = startProfile(sketch001, at = [281.54, 305.81])
669 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
670 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
671 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
672 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
673 |> close()
674extrude(profile001, length = 100)"#
675 .to_string(),
676 );
677
678 let tmp_dir = std::env::temp_dir();
679 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
680
681 let tmp_file = tmp_dir.join(other_file.0);
683 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
684 std::fs::write(tmp_file, other_file.1).unwrap();
685
686 let ExecTestResults { program, exec_ctxt, .. } =
687 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
688 .await
689 .unwrap();
690
691 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
692 new_program.compute_digest();
693
694 let result = get_changed_program(
695 CacheInformation {
696 ast: &program.ast,
697 settings: &exec_ctxt.settings,
698 },
699 CacheInformation {
700 ast: &new_program.ast,
701 settings: &exec_ctxt.settings,
702 },
703 )
704 .await;
705
706 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
707 panic!("Expected CheckImportsOnly, got {:?}", result);
708 };
709
710 assert_eq!(reapply_settings, false);
711 }
712
713 #[tokio::test(flavor = "multi_thread")]
714 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
715 let code = r#"import "toBeImported.kcl" as importedCube
716
717importedCube
718
719sketch001 = startSketchOn(XZ)
720profile001 = startProfile(sketch001, at = [-134.53, -56.17])
721 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
722 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
723 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
724 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
725 |> close()
726extrude001 = extrude(profile001, length = 100)
727sketch003 = startSketchOn(extrude001, face = seg02)
728sketch002 = startSketchOn(extrude001, face = seg01)
729"#;
730
731 let other_file = (
732 std::path::PathBuf::from("toBeImported.kcl"),
733 r#"sketch001 = startSketchOn(XZ)
734profile001 = startProfile(sketch001, at = [281.54, 305.81])
735 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
736 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
737 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
738 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
739 |> close()
740extrude(profile001, length = 100)"#
741 .to_string(),
742 );
743
744 let other_file2 = (
745 std::path::PathBuf::from("toBeImported.kcl"),
746 r#"sketch001 = startSketchOn(XZ)
747profile001 = startProfile(sketch001, at = [281.54, 305.81])
748 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
749 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
750 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
751 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
752 |> close()
753extrude(profile001, length = 100)
754|> translate(z=100)
755"#
756 .to_string(),
757 );
758
759 let tmp_dir = std::env::temp_dir();
760 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
761
762 let tmp_file = tmp_dir.join(other_file.0);
764 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
765 std::fs::write(&tmp_file, other_file.1).unwrap();
766
767 let ExecTestResults { program, exec_ctxt, .. } =
768 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
769 .await
770 .unwrap();
771
772 std::fs::write(tmp_file, other_file2.1).unwrap();
774
775 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
776 new_program.compute_digest();
777
778 let result = get_changed_program(
779 CacheInformation {
780 ast: &program.ast,
781 settings: &exec_ctxt.settings,
782 },
783 CacheInformation {
784 ast: &new_program.ast,
785 settings: &exec_ctxt.settings,
786 },
787 )
788 .await;
789
790 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
791 panic!("Expected CheckImportsOnly, got {:?}", result);
792 };
793
794 assert_eq!(reapply_settings, false);
795 }
796}