1use 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 static ref OLD_AST: Arc<RwLock<Option<OldAstState>>> = Default::default();
17 static ref PREV_MEMORY: Arc<RwLock<Option<Stack>>> = 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> {
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#[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 NoAction(bool),
85}
86
87pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInformation<'_>) -> CacheResult {
95 let mut reapply_settings = false;
96
97 if old.settings != new.settings {
100 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 reapply_settings = true;
112 }
113
114 if old.ast == new.ast {
117 return CacheResult::NoAction(reapply_settings);
118 }
119
120 let mut old_ast = old.ast.clone();
122 let mut new_ast = new.ast.clone();
123
124 old_ast.compute_digest();
127 new_ast.compute_digest();
128
129 if old_ast.digest == new_ast.digest {
131 return CacheResult::NoAction(reapply_settings);
132 }
133
134 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 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 return CacheResult::ReExecute {
168 clear_scene: true,
169 reapply_settings: true,
170 program: new.ast.clone(),
171 };
172 }
173
174 generate_changed_program(old_ast, new_ast, reapply_settings)
176}
177
178fn 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 return CacheResult::ReExecute {
196 clear_scene: true,
197 reapply_settings,
198 program: new_ast,
199 };
200 }
201
202 match new_ast.body.len().cmp(&old_ast.body.len()) {
206 std::cmp::Ordering::Less => {
207 CacheResult::ReExecute {
216 clear_scene: true,
217 reapply_settings,
218 program: new_ast,
219 }
220 }
221 std::cmp::Ordering::Greater => {
222 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 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 #[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 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 #[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 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 #[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 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 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 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 #[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}