1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
use crate::{expr::Expr, parser::util::recognize_string_template};

// #todo remove excessive clones.

// #todo find a better, more general name for this stage.

// #insight prune does not err.

// #insight prune strips unnecessary auxiliary exprs not needed for evaluation.

// #todo strip quoting of literals (nops)
// #todo consider only allowing the sigils, and not quot/unquot -> no, we need them to maintain the list/tree abstraction, it has to be syntax-sugar!
// #todo actually we could skip the `unquot`, think about it.

// #insight no need to convert Symbol to KeySymbol, just converting List -> Array works.

pub fn prune_fn(expr: Expr) -> Option<Expr> {
    // #todo use `extract` instead of `unpack`.
    let (unpacked_expr, annotations) = expr.extract();

    match unpacked_expr {
        Expr::Comment(..) => {
            // #todo move prune elsewhere.
            // Prune Comment expressions.
            None
        }
        Expr::TextSeparator => {
            // #todo remove TextSeparator.
            // #todo move prune elsewhere.
            // Prune TextSeparator expressions.
            None
        }
        Expr::String(str) => {
            // #insight
            // only apply the transformation, error checking happened in the
            // parsing stage.
            if str.contains("${") {
                match recognize_string_template(str) {
                    Ok(format_expr) => {
                        // #todo extract a helper function.
                        if let Some(annotations) = annotations {
                            Some(Expr::Annotated(Box::new(format_expr), annotations.clone()))
                        } else {
                            Some(format_expr)
                        }
                    }
                    Err(_) => {
                        // #todo what should be done here?
                        // #insight this state should not be valid.
                        panic!("invalid state");
                    }
                }
            } else {
                Some(expr)
            }
        }
        // #todo resolve quoting+interpolation here? i.e. quasiquoting
        // #todo maybe even resolve string interpolation here?
        _ => Some(expr),
    }
}

pub fn prune(expr: Expr) -> Option<Expr> {
    expr.filter_transform(&prune_fn)
}

#[cfg(test)]
mod tests {
    use assert_matches::assert_matches;

    use crate::{api::parse_string, expr::Expr, prune::prune};

    #[test]
    fn prune_removes_comments() {
        let input = "(do ; comment\n(let a [1 2 3 4]) ; a comment\n(writeln (+ 2 3)))";

        let expr = parse_string(input).unwrap();

        let expr = prune(expr).unwrap();

        let s = format!("{expr}");

        assert!(s.contains("(do (let a (Array 1 2 3 4)) (writeln (+ 2 3)))"));
    }

    #[test]
    fn prune_transforms_template_strings() {
        let input = r#"(let m "An amount: $110.00. Here is a number: ${num}, and another: ${another-num}")"#;
        let expr = parse_string(input).unwrap();

        let expr = prune(expr).unwrap();

        let Expr::List(exprs) = expr.unpack() else {
            panic!("assertion failed: invalid form")
        };

        // dbg!(exprs);

        let Expr::List(ref exprs) = exprs[2].unpack() else {
            panic!("assertion failed: invalid form")
        };

        assert_matches!(&exprs[0].unpack(), Expr::Symbol(s) if s == "format");
        assert_eq!(exprs.len(), 5);
    }
}