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 },
83 CheckImportsOnly {
87 reapply_settings: bool,
89 ast: Node<Program>,
91 },
92 NoAction(bool),
94}
95
96pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
104 let mut reapply_settings = false;
105
106 if old.settings != new.settings {
109 reapply_settings = true;
112 }
113
114 if old.ast == new.ast {
117 if !old.ast.has_import_statements() {
121 return CacheResult::NoAction(reapply_settings);
122 }
123
124 return CacheResult::CheckImportsOnly {
126 reapply_settings,
127 ast: old.ast.clone(),
128 };
129 }
130
131 let mut old_ast = old.ast.clone();
133 let mut new_ast = new.ast.clone();
134
135 old_ast.compute_digest();
138 new_ast.compute_digest();
139
140 if old_ast.digest == new_ast.digest {
142 if !old.ast.has_import_statements() {
146 println!("No imports, no need to check.");
147 return CacheResult::NoAction(reapply_settings);
148 }
149
150 return CacheResult::CheckImportsOnly {
152 reapply_settings,
153 ast: old.ast.clone(),
154 };
155 }
156
157 if !old_ast
159 .inner_attrs
160 .iter()
161 .filter(annotations::is_significant)
162 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
163 .all(|pair| {
164 match pair {
165 EitherOrBoth::Both(old, new) => {
166 let Annotation { name, properties, .. } = &old.inner;
169 let Annotation {
170 name: new_name,
171 properties: new_properties,
172 ..
173 } = &new.inner;
174
175 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
176 && properties
177 .as_ref()
178 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
179 == new_properties
180 .as_ref()
181 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
182 }
183 _ => false,
184 }
185 })
186 {
187 return CacheResult::ReExecute {
191 clear_scene: true,
192 reapply_settings: true,
193 program: new.ast.clone(),
194 };
195 }
196
197 generate_changed_program(old_ast, new_ast, reapply_settings)
199}
200
201fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
208 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
209 let old_node: WalkNode = old.into();
210 let new_node: WalkNode = new.into();
211 old_node.digest() == new_node.digest()
212 }) {
213 return CacheResult::ReExecute {
219 clear_scene: true,
220 reapply_settings,
221 program: new_ast,
222 };
223 }
224
225 match new_ast.body.len().cmp(&old_ast.body.len()) {
229 std::cmp::Ordering::Less => {
230 CacheResult::ReExecute {
239 clear_scene: true,
240 reapply_settings,
241 program: new_ast,
242 }
243 }
244 std::cmp::Ordering::Greater => {
245 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
253
254 CacheResult::ReExecute {
255 clear_scene: false,
256 reapply_settings,
257 program: new_ast,
258 }
259 }
260 std::cmp::Ordering::Equal => {
261 CacheResult::NoAction(reapply_settings)
271 }
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use pretty_assertions::assert_eq;
278
279 use super::*;
280 use crate::execution::{parse_execute, parse_execute_with_project_dir, ExecTestResults};
281
282 #[tokio::test(flavor = "multi_thread")]
283 async fn test_get_changed_program_same_code() {
284 let new = r#"// Remove the end face for the extrusion.
285firstSketch = startSketchOn(XY)
286 |> startProfile(at = [-12, 12])
287 |> line(end = [24, 0])
288 |> line(end = [0, -24])
289 |> line(end = [-24, 0])
290 |> close()
291 |> extrude(length = 6)
292
293// Remove the end face for the extrusion.
294shell(firstSketch, faces = [END], thickness = 0.25)"#;
295
296 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
297
298 let result = get_changed_program(
299 CacheInformation {
300 ast: &program.ast,
301 settings: &exec_ctxt.settings,
302 },
303 CacheInformation {
304 ast: &program.ast,
305 settings: &exec_ctxt.settings,
306 },
307 )
308 .await;
309
310 assert_eq!(result, CacheResult::NoAction(false));
311 }
312
313 #[tokio::test(flavor = "multi_thread")]
314 async fn test_get_changed_program_same_code_changed_whitespace() {
315 let old = r#" // Remove the end face for the extrusion.
316firstSketch = startSketchOn(XY)
317 |> startProfile(at = [-12, 12])
318 |> line(end = [24, 0])
319 |> line(end = [0, -24])
320 |> line(end = [-24, 0])
321 |> close()
322 |> extrude(length = 6)
323
324// Remove the end face for the extrusion.
325shell(firstSketch, faces = [END], thickness = 0.25) "#;
326
327 let new = r#"// Remove the end face for the extrusion.
328firstSketch = startSketchOn(XY)
329 |> startProfile(at = [-12, 12])
330 |> line(end = [24, 0])
331 |> line(end = [0, -24])
332 |> line(end = [-24, 0])
333 |> close()
334 |> extrude(length = 6)
335
336// Remove the end face for the extrusion.
337shell(firstSketch, faces = [END], thickness = 0.25)"#;
338
339 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
340
341 let program_new = crate::Program::parse_no_errs(new).unwrap();
342
343 let result = get_changed_program(
344 CacheInformation {
345 ast: &program.ast,
346 settings: &exec_ctxt.settings,
347 },
348 CacheInformation {
349 ast: &program_new.ast,
350 settings: &exec_ctxt.settings,
351 },
352 )
353 .await;
354
355 assert_eq!(result, CacheResult::NoAction(false));
356 }
357
358 #[tokio::test(flavor = "multi_thread")]
359 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
360 let old = r#" // Removed the end face for the extrusion.
361firstSketch = startSketchOn(XY)
362 |> startProfile(at = [-12, 12])
363 |> line(end = [24, 0])
364 |> line(end = [0, -24])
365 |> line(end = [-24, 0])
366 |> close()
367 |> extrude(length = 6)
368
369// Remove the end face for the extrusion.
370shell(firstSketch, faces = [END], thickness = 0.25) "#;
371
372 let new = r#"// Remove the end face for the extrusion.
373firstSketch = startSketchOn(XY)
374 |> startProfile(at = [-12, 12])
375 |> line(end = [24, 0])
376 |> line(end = [0, -24])
377 |> line(end = [-24, 0])
378 |> close()
379 |> extrude(length = 6)
380
381// Remove the end face for the extrusion.
382shell(firstSketch, faces = [END], thickness = 0.25)"#;
383
384 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
385
386 let program_new = crate::Program::parse_no_errs(new).unwrap();
387
388 let result = get_changed_program(
389 CacheInformation {
390 ast: &program.ast,
391 settings: &exec_ctxt.settings,
392 },
393 CacheInformation {
394 ast: &program_new.ast,
395 settings: &exec_ctxt.settings,
396 },
397 )
398 .await;
399
400 assert_eq!(result, CacheResult::NoAction(false));
401 }
402
403 #[tokio::test(flavor = "multi_thread")]
404 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
405 let old = r#"@foo(whatever = whatever)
406@bar
407// Removed the end face for the extrusion.
408firstSketch = startSketchOn(XY)
409 |> startProfile(at = [-12, 12])
410 |> line(end = [24, 0])
411 |> line(end = [0, -24])
412 |> line(end = [-24, 0]) // my thing
413 |> close()
414 |> extrude(length = 6)
415
416// Remove the end face for the extrusion.
417shell(firstSketch, faces = [END], thickness = 0.25) "#;
418
419 let new = r#"@foo(whatever = 42)
420@baz
421// Remove the end face for the extrusion.
422firstSketch = startSketchOn(XY)
423 |> startProfile(at = [-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 { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
434
435 let program_new = crate::Program::parse_no_errs(new).unwrap();
436
437 let result = get_changed_program(
438 CacheInformation {
439 ast: &program.ast,
440 settings: &exec_ctxt.settings,
441 },
442 CacheInformation {
443 ast: &program_new.ast,
444 settings: &exec_ctxt.settings,
445 },
446 )
447 .await;
448
449 assert_eq!(result, CacheResult::NoAction(false));
450 }
451
452 #[tokio::test(flavor = "multi_thread")]
454 async fn test_get_changed_program_same_code_but_different_grid_setting() {
455 let new = r#"// Remove the end face for the extrusion.
456firstSketch = startSketchOn(XY)
457 |> startProfile(at = [-12, 12])
458 |> line(end = [24, 0])
459 |> line(end = [0, -24])
460 |> line(end = [-24, 0])
461 |> close()
462 |> extrude(length = 6)
463
464// Remove the end face for the extrusion.
465shell(firstSketch, faces = [END], thickness = 0.25)"#;
466
467 let ExecTestResults {
468 program, mut exec_ctxt, ..
469 } = parse_execute(new).await.unwrap();
470
471 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
473
474 let result = get_changed_program(
475 CacheInformation {
476 ast: &program.ast,
477 settings: &Default::default(),
478 },
479 CacheInformation {
480 ast: &program.ast,
481 settings: &exec_ctxt.settings,
482 },
483 )
484 .await;
485
486 assert_eq!(result, CacheResult::NoAction(true));
487 }
488
489 #[tokio::test(flavor = "multi_thread")]
491 async fn test_get_changed_program_same_code_but_different_edge_visibility_setting() {
492 let new = r#"// Remove the end face for the extrusion.
493firstSketch = startSketchOn(XY)
494 |> startProfile(at = [-12, 12])
495 |> line(end = [24, 0])
496 |> line(end = [0, -24])
497 |> line(end = [-24, 0])
498 |> close()
499 |> extrude(length = 6)
500
501// Remove the end face for the extrusion.
502shell(firstSketch, faces = [END], thickness = 0.25)"#;
503
504 let ExecTestResults {
505 program, mut exec_ctxt, ..
506 } = parse_execute(new).await.unwrap();
507
508 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
510
511 let result = get_changed_program(
512 CacheInformation {
513 ast: &program.ast,
514 settings: &Default::default(),
515 },
516 CacheInformation {
517 ast: &program.ast,
518 settings: &exec_ctxt.settings,
519 },
520 )
521 .await;
522
523 assert_eq!(result, CacheResult::NoAction(true));
524
525 let old_settings = exec_ctxt.settings.clone();
527 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
528
529 let result = get_changed_program(
530 CacheInformation {
531 ast: &program.ast,
532 settings: &old_settings,
533 },
534 CacheInformation {
535 ast: &program.ast,
536 settings: &exec_ctxt.settings,
537 },
538 )
539 .await;
540
541 assert_eq!(result, CacheResult::NoAction(true));
542
543 let old_settings = exec_ctxt.settings.clone();
545 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
546
547 let result = get_changed_program(
548 CacheInformation {
549 ast: &program.ast,
550 settings: &old_settings,
551 },
552 CacheInformation {
553 ast: &program.ast,
554 settings: &exec_ctxt.settings,
555 },
556 )
557 .await;
558
559 assert_eq!(result, CacheResult::NoAction(true));
560 }
561
562 #[tokio::test(flavor = "multi_thread")]
565 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
566 let old_code = r#"@settings(defaultLengthUnit = in)
567startSketchOn(XY)
568"#;
569 let new_code = r#"@settings(defaultLengthUnit = mm)
570startSketchOn(XY)
571"#;
572
573 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
574
575 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
576 new_program.compute_digest();
577
578 let result = get_changed_program(
579 CacheInformation {
580 ast: &program.ast,
581 settings: &exec_ctxt.settings,
582 },
583 CacheInformation {
584 ast: &new_program.ast,
585 settings: &exec_ctxt.settings,
586 },
587 )
588 .await;
589
590 assert_eq!(
591 result,
592 CacheResult::ReExecute {
593 clear_scene: true,
594 reapply_settings: true,
595 program: new_program.ast
596 }
597 );
598 }
599
600 #[tokio::test(flavor = "multi_thread")]
603 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
604 let old_code = r#"@settings(defaultLengthUnit = in)
605startSketchOn(XY)
606"#;
607 let new_code = r#"
608startSketchOn(XY)
609"#;
610
611 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
612
613 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
614 new_program.compute_digest();
615
616 let result = get_changed_program(
617 CacheInformation {
618 ast: &program.ast,
619 settings: &exec_ctxt.settings,
620 },
621 CacheInformation {
622 ast: &new_program.ast,
623 settings: &exec_ctxt.settings,
624 },
625 )
626 .await;
627
628 assert_eq!(
629 result,
630 CacheResult::ReExecute {
631 clear_scene: true,
632 reapply_settings: true,
633 program: new_program.ast
634 }
635 );
636 }
637
638 #[tokio::test(flavor = "multi_thread")]
639 async fn test_multi_file_no_changes_does_not_reexecute() {
640 let code = r#"import "toBeImported.kcl" as importedCube
641
642importedCube
643
644sketch001 = startSketchOn(XZ)
645profile001 = startProfile(sketch001, at = [-134.53, -56.17])
646 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
647 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
648 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
649 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
650 |> close()
651extrude001 = extrude(profile001, length = 100)
652sketch003 = startSketchOn(extrude001, face = seg02)
653sketch002 = startSketchOn(extrude001, face = seg01)
654"#;
655
656 let other_file = (
657 std::path::PathBuf::from("toBeImported.kcl"),
658 r#"sketch001 = startSketchOn(XZ)
659profile001 = startProfile(sketch001, at = [281.54, 305.81])
660 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
661 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
662 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
663 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
664 |> close()
665extrude(profile001, length = 100)"#
666 .to_string(),
667 );
668
669 let tmp_dir = std::env::temp_dir();
670 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
671
672 let tmp_file = tmp_dir.join(other_file.0);
674 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
675 std::fs::write(tmp_file, other_file.1).unwrap();
676
677 let ExecTestResults { program, exec_ctxt, .. } =
678 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
679 .await
680 .unwrap();
681
682 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
683 new_program.compute_digest();
684
685 let result = get_changed_program(
686 CacheInformation {
687 ast: &program.ast,
688 settings: &exec_ctxt.settings,
689 },
690 CacheInformation {
691 ast: &new_program.ast,
692 settings: &exec_ctxt.settings,
693 },
694 )
695 .await;
696
697 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
698 panic!("Expected CheckImportsOnly, got {:?}", result);
699 };
700
701 assert_eq!(reapply_settings, false);
702 }
703
704 #[tokio::test(flavor = "multi_thread")]
705 async fn test_cache_multi_file_only_other_file_changes_should_reexecute() {
706 let code = r#"import "toBeImported.kcl" as importedCube
707
708importedCube
709
710sketch001 = startSketchOn(XZ)
711profile001 = startProfile(sketch001, at = [-134.53, -56.17])
712 |> angledLine(angle = 0, length = 79.05, tag = $rectangleSegmentA001)
713 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 76.28)
714 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001), tag = $seg01)
715 |> line(endAbsolute = [profileStartX(%), profileStartY(%)], tag = $seg02)
716 |> close()
717extrude001 = extrude(profile001, length = 100)
718sketch003 = startSketchOn(extrude001, face = seg02)
719sketch002 = startSketchOn(extrude001, face = seg01)
720"#;
721
722 let other_file = (
723 std::path::PathBuf::from("toBeImported.kcl"),
724 r#"sketch001 = startSketchOn(XZ)
725profile001 = startProfile(sketch001, at = [281.54, 305.81])
726 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
727 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
728 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
729 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
730 |> close()
731extrude(profile001, length = 100)"#
732 .to_string(),
733 );
734
735 let other_file2 = (
736 std::path::PathBuf::from("toBeImported.kcl"),
737 r#"sketch001 = startSketchOn(XZ)
738profile001 = startProfile(sketch001, at = [281.54, 305.81])
739 |> angledLine(angle = 0, length = 123.43, tag = $rectangleSegmentA001)
740 |> angledLine(angle = segAng(rectangleSegmentA001) - 90, length = 85.99)
741 |> angledLine(angle = segAng(rectangleSegmentA001), length = -segLen(rectangleSegmentA001))
742 |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
743 |> close()
744extrude(profile001, length = 100)
745|> translate(z=100)
746"#
747 .to_string(),
748 );
749
750 let tmp_dir = std::env::temp_dir();
751 let tmp_dir = tmp_dir.join(uuid::Uuid::new_v4().to_string());
752
753 let tmp_file = tmp_dir.join(other_file.0);
755 std::fs::create_dir_all(tmp_file.parent().unwrap()).unwrap();
756 std::fs::write(&tmp_file, other_file.1).unwrap();
757
758 let ExecTestResults { program, exec_ctxt, .. } =
759 parse_execute_with_project_dir(code, Some(crate::TypedPath(tmp_dir)))
760 .await
761 .unwrap();
762
763 std::fs::write(tmp_file, other_file2.1).unwrap();
765
766 let mut new_program = crate::Program::parse_no_errs(code).unwrap();
767 new_program.compute_digest();
768
769 let result = get_changed_program(
770 CacheInformation {
771 ast: &program.ast,
772 settings: &exec_ctxt.settings,
773 },
774 CacheInformation {
775 ast: &new_program.ast,
776 settings: &exec_ctxt.settings,
777 },
778 )
779 .await;
780
781 let CacheResult::CheckImportsOnly { reapply_settings, .. } = result else {
782 panic!("Expected CheckImportsOnly, got {:?}", result);
783 };
784
785 assert_eq!(reapply_settings, false);
786 }
787}