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