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 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 reapply_settings = true;
103 }
104
105 if old.ast == new.ast {
108 return CacheResult::NoAction(reapply_settings);
109 }
110
111 let mut old_ast = old.ast.clone();
113 let mut new_ast = new.ast.clone();
114
115 old_ast.compute_digest();
118 new_ast.compute_digest();
119
120 if old_ast.digest == new_ast.digest {
122 return CacheResult::NoAction(reapply_settings);
123 }
124
125 if !old_ast
127 .inner_attrs
128 .iter()
129 .filter(annotations::is_significant)
130 .zip_longest(new_ast.inner_attrs.iter().filter(annotations::is_significant))
131 .all(|pair| {
132 match pair {
133 EitherOrBoth::Both(old, new) => {
134 let Annotation { name, properties, .. } = &old.inner;
137 let Annotation {
138 name: new_name,
139 properties: new_properties,
140 ..
141 } = &new.inner;
142
143 name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
144 && properties
145 .as_ref()
146 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
147 == new_properties
148 .as_ref()
149 .map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())
150 }
151 _ => false,
152 }
153 })
154 {
155 return CacheResult::ReExecute {
159 clear_scene: true,
160 reapply_settings: true,
161 program: new.ast.clone(),
162 };
163 }
164
165 generate_changed_program(old_ast, new_ast, reapply_settings)
167}
168
169fn generate_changed_program(old_ast: Node<Program>, mut new_ast: Node<Program>, reapply_settings: bool) -> CacheResult {
176 if !old_ast.body.iter().zip(new_ast.body.iter()).all(|(old, new)| {
177 let old_node: WalkNode = old.into();
178 let new_node: WalkNode = new.into();
179 old_node.digest() == new_node.digest()
180 }) {
181 return CacheResult::ReExecute {
187 clear_scene: true,
188 reapply_settings,
189 program: new_ast,
190 };
191 }
192
193 match new_ast.body.len().cmp(&old_ast.body.len()) {
197 std::cmp::Ordering::Less => {
198 CacheResult::ReExecute {
207 clear_scene: true,
208 reapply_settings,
209 program: new_ast,
210 }
211 }
212 std::cmp::Ordering::Greater => {
213 new_ast.body = new_ast.body[old_ast.body.len()..].to_owned();
221
222 CacheResult::ReExecute {
223 clear_scene: false,
224 reapply_settings,
225 program: new_ast,
226 }
227 }
228 std::cmp::Ordering::Equal => {
229 CacheResult::NoAction(reapply_settings)
239 }
240 }
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::execution::{parse_execute, ExecTestResults};
247
248 #[tokio::test(flavor = "multi_thread")]
249 async fn test_get_changed_program_same_code() {
250 let new = r#"// Remove the end face for the extrusion.
251firstSketch = startSketchOn('XY')
252 |> startProfileAt([-12, 12], %)
253 |> line(end = [24, 0])
254 |> line(end = [0, -24])
255 |> line(end = [-24, 0])
256 |> close()
257 |> extrude(length = 6)
258
259// Remove the end face for the extrusion.
260shell(firstSketch, faces = [END], thickness = 0.25)"#;
261
262 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(new).await.unwrap();
263
264 let result = get_changed_program(
265 CacheInformation {
266 ast: &program.ast,
267 settings: &exec_ctxt.settings,
268 },
269 CacheInformation {
270 ast: &program.ast,
271 settings: &exec_ctxt.settings,
272 },
273 )
274 .await;
275
276 assert_eq!(result, CacheResult::NoAction(false));
277 }
278
279 #[tokio::test(flavor = "multi_thread")]
280 async fn test_get_changed_program_same_code_changed_whitespace() {
281 let old = r#" // Remove the end face for the extrusion.
282firstSketch = startSketchOn('XY')
283 |> startProfileAt([-12, 12], %)
284 |> line(end = [24, 0])
285 |> line(end = [0, -24])
286 |> line(end = [-24, 0])
287 |> close()
288 |> extrude(length = 6)
289
290// Remove the end face for the extrusion.
291shell(firstSketch, faces = [END], thickness = 0.25) "#;
292
293 let new = r#"// Remove the end face for the extrusion.
294firstSketch = startSketchOn('XY')
295 |> startProfileAt([-12, 12], %)
296 |> line(end = [24, 0])
297 |> line(end = [0, -24])
298 |> line(end = [-24, 0])
299 |> close()
300 |> extrude(length = 6)
301
302// Remove the end face for the extrusion.
303shell(firstSketch, faces = [END], thickness = 0.25)"#;
304
305 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
306
307 let program_new = crate::Program::parse_no_errs(new).unwrap();
308
309 let result = get_changed_program(
310 CacheInformation {
311 ast: &program.ast,
312 settings: &exec_ctxt.settings,
313 },
314 CacheInformation {
315 ast: &program_new.ast,
316 settings: &exec_ctxt.settings,
317 },
318 )
319 .await;
320
321 assert_eq!(result, CacheResult::NoAction(false));
322 }
323
324 #[tokio::test(flavor = "multi_thread")]
325 async fn test_get_changed_program_same_code_changed_code_comment_start_of_program() {
326 let old = r#" // Removed the end face for the extrusion.
327firstSketch = startSketchOn('XY')
328 |> startProfileAt([-12, 12], %)
329 |> line(end = [24, 0])
330 |> line(end = [0, -24])
331 |> line(end = [-24, 0])
332 |> close()
333 |> extrude(length = 6)
334
335// Remove the end face for the extrusion.
336shell(firstSketch, faces = [END], thickness = 0.25) "#;
337
338 let new = r#"// Remove the end face for the extrusion.
339firstSketch = startSketchOn('XY')
340 |> startProfileAt([-12, 12], %)
341 |> line(end = [24, 0])
342 |> line(end = [0, -24])
343 |> line(end = [-24, 0])
344 |> close()
345 |> extrude(length = 6)
346
347// Remove the end face for the extrusion.
348shell(firstSketch, faces = [END], thickness = 0.25)"#;
349
350 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
351
352 let program_new = crate::Program::parse_no_errs(new).unwrap();
353
354 let result = get_changed_program(
355 CacheInformation {
356 ast: &program.ast,
357 settings: &exec_ctxt.settings,
358 },
359 CacheInformation {
360 ast: &program_new.ast,
361 settings: &exec_ctxt.settings,
362 },
363 )
364 .await;
365
366 assert_eq!(result, CacheResult::NoAction(false));
367 }
368
369 #[tokio::test(flavor = "multi_thread")]
370 async fn test_get_changed_program_same_code_changed_code_comments_attrs() {
371 let old = r#"@foo(whatever = whatever)
372@bar
373// Removed the end face for the extrusion.
374firstSketch = startSketchOn('XY')
375 |> startProfileAt([-12, 12], %)
376 |> line(end = [24, 0])
377 |> line(end = [0, -24])
378 |> line(end = [-24, 0]) // my thing
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 new = r#"@foo(whatever = 42)
386@baz
387// Remove the end face for the extrusion.
388firstSketch = startSketchOn('XY')
389 |> startProfileAt([-12, 12], %)
390 |> line(end = [24, 0])
391 |> line(end = [0, -24])
392 |> line(end = [-24, 0])
393 |> close()
394 |> extrude(length = 6)
395
396// Remove the end face for the extrusion.
397shell(firstSketch, faces = [END], thickness = 0.25)"#;
398
399 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old).await.unwrap();
400
401 let program_new = crate::Program::parse_no_errs(new).unwrap();
402
403 let result = get_changed_program(
404 CacheInformation {
405 ast: &program.ast,
406 settings: &exec_ctxt.settings,
407 },
408 CacheInformation {
409 ast: &program_new.ast,
410 settings: &exec_ctxt.settings,
411 },
412 )
413 .await;
414
415 assert_eq!(result, CacheResult::NoAction(false));
416 }
417
418 #[tokio::test(flavor = "multi_thread")]
420 async fn test_get_changed_program_same_code_but_different_grid_setting() {
421 let new = r#"// Remove the end face for the extrusion.
422firstSketch = startSketchOn('XY')
423 |> startProfileAt([-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 {
434 program, mut exec_ctxt, ..
435 } = parse_execute(new).await.unwrap();
436
437 exec_ctxt.settings.show_grid = !exec_ctxt.settings.show_grid;
439
440 let result = get_changed_program(
441 CacheInformation {
442 ast: &program.ast,
443 settings: &Default::default(),
444 },
445 CacheInformation {
446 ast: &program.ast,
447 settings: &exec_ctxt.settings,
448 },
449 )
450 .await;
451
452 assert_eq!(result, CacheResult::NoAction(true));
453 }
454
455 #[tokio::test(flavor = "multi_thread")]
457 async fn test_get_changed_program_same_code_but_different_edge_visiblity_setting() {
458 let new = r#"// Remove the end face for the extrusion.
459firstSketch = startSketchOn('XY')
460 |> startProfileAt([-12, 12], %)
461 |> line(end = [24, 0])
462 |> line(end = [0, -24])
463 |> line(end = [-24, 0])
464 |> close()
465 |> extrude(length = 6)
466
467// Remove the end face for the extrusion.
468shell(firstSketch, faces = [END], thickness = 0.25)"#;
469
470 let ExecTestResults {
471 program, mut exec_ctxt, ..
472 } = parse_execute(new).await.unwrap();
473
474 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
476
477 let result = get_changed_program(
478 CacheInformation {
479 ast: &program.ast,
480 settings: &Default::default(),
481 },
482 CacheInformation {
483 ast: &program.ast,
484 settings: &exec_ctxt.settings,
485 },
486 )
487 .await;
488
489 assert_eq!(result, CacheResult::NoAction(true));
490
491 let old_settings = exec_ctxt.settings.clone();
493 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
494
495 let result = get_changed_program(
496 CacheInformation {
497 ast: &program.ast,
498 settings: &old_settings,
499 },
500 CacheInformation {
501 ast: &program.ast,
502 settings: &exec_ctxt.settings,
503 },
504 )
505 .await;
506
507 assert_eq!(result, CacheResult::NoAction(true));
508
509 let old_settings = exec_ctxt.settings.clone();
511 exec_ctxt.settings.highlight_edges = !exec_ctxt.settings.highlight_edges;
512
513 let result = get_changed_program(
514 CacheInformation {
515 ast: &program.ast,
516 settings: &old_settings,
517 },
518 CacheInformation {
519 ast: &program.ast,
520 settings: &exec_ctxt.settings,
521 },
522 )
523 .await;
524
525 assert_eq!(result, CacheResult::NoAction(true));
526 }
527
528 #[tokio::test(flavor = "multi_thread")]
531 async fn test_get_changed_program_same_code_but_different_unit_setting_using_annotation() {
532 let old_code = r#"@settings(defaultLengthUnit = in)
533startSketchOn('XY')
534"#;
535 let new_code = r#"@settings(defaultLengthUnit = mm)
536startSketchOn('XY')
537"#;
538
539 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
540
541 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
542 new_program.compute_digest();
543
544 let result = get_changed_program(
545 CacheInformation {
546 ast: &program.ast,
547 settings: &exec_ctxt.settings,
548 },
549 CacheInformation {
550 ast: &new_program.ast,
551 settings: &exec_ctxt.settings,
552 },
553 )
554 .await;
555
556 assert_eq!(
557 result,
558 CacheResult::ReExecute {
559 clear_scene: true,
560 reapply_settings: true,
561 program: new_program.ast
562 }
563 );
564 }
565
566 #[tokio::test(flavor = "multi_thread")]
569 async fn test_get_changed_program_same_code_but_removed_unit_setting_using_annotation() {
570 let old_code = r#"@settings(defaultLengthUnit = in)
571startSketchOn('XY')
572"#;
573 let new_code = r#"
574startSketchOn('XY')
575"#;
576
577 let ExecTestResults { program, exec_ctxt, .. } = parse_execute(old_code).await.unwrap();
578
579 let mut new_program = crate::Program::parse_no_errs(new_code).unwrap();
580 new_program.compute_digest();
581
582 let result = get_changed_program(
583 CacheInformation {
584 ast: &program.ast,
585 settings: &exec_ctxt.settings,
586 },
587 CacheInformation {
588 ast: &new_program.ast,
589 settings: &exec_ctxt.settings,
590 },
591 )
592 .await;
593
594 assert_eq!(
595 result,
596 CacheResult::ReExecute {
597 clear_scene: true,
598 reapply_settings: true,
599 program: new_program.ast
600 }
601 );
602 }
603}