Skip to main content

prima_core/
simplify.rs

1use num_bigint::BigInt;
2use num_rational::BigRational;
3
4use crate::builtins::BuiltinSymbols;
5use crate::expr_pool::{ExprData, ExprId, ExprPool};
6use crate::number::Number;
7
8/// Full simplification (spec §8.3 levels 2/3): fold recursively, then apply rules on demand —
9/// `Pow(sqrt(x), 2) → x`, Euler's `e^{iθ} → cos + i·sin`, constant folding for `sin/cos/exp/log/ln/abs/sqrt`,
10/// `Pow(x, 1/2) → \sqrt{x}`. Simplification never changes the mathematical value.
11pub fn simplify(pool: &ExprPool, builtins: &BuiltinSymbols, id: ExprId) -> ExprId {
12    simplify_at(pool, builtins, id, 3)
13}
14
15/// Simplification gated by a level (spec §8.3/§13.2): a lower `simplify_level` reduces how many
16/// rules are applied, while intern-time level-0/1 canonicalization (`0*x→0`, `1*x→x`, constant
17/// merging) always holds. Level `>= 2` enables the symbolic rules (builtin constant folding,
18/// `sqrt`-elimination, Euler's formula); level `3` is a superset for future rationalization rules.
19pub fn simplify_at(pool: &ExprPool, builtins: &BuiltinSymbols, id: ExprId, level: u8) -> ExprId {
20    let node = match pool.get(id) {
21        Some(n) => n,
22        None => return id,
23    };
24    match node {
25        ExprData::Add(items) => {
26            if items.is_empty() {
27                return id;
28            }
29            let mut acc = simplify_at(pool, builtins, items[0], level);
30            for &it in &items[1..] {
31                let s = simplify_at(pool, builtins, it, level);
32                acc = pool.add2(acc, s);
33            }
34            acc
35        }
36        ExprData::Mul(items) => {
37            if items.is_empty() {
38                return id;
39            }
40            let mut acc = simplify_at(pool, builtins, items[0], level);
41            for &it in &items[1..] {
42                let s = simplify_at(pool, builtins, it, level);
43                acc = pool.mul2(acc, s);
44            }
45            acc
46        }
47        ExprData::Pow { base, exp } => {
48            let b = simplify_at(pool, builtins, base, level);
49            let e = simplify_at(pool, builtins, exp, level);
50            if level >= 2 {
51                if let Some(ExprData::Apply { f, args }) = pool.get(b)
52                    && f == pool.symbol(builtins.sqrt)
53                    && args.len() == 1
54                    && e == pool.integer(2)
55                {
56                    return simplify_at(pool, builtins, args[0], level);
57                }
58                if b == pool.symbol(builtins.e)
59                    && let Some(r) = euler(pool, builtins, e)
60                {
61                    return r;
62                }
63            }
64            pool.pow2(b, e)
65        }
66        ExprData::Apply { f, args } => {
67            let mut new_args = Vec::with_capacity(args.len());
68            for &a in args.iter() {
69                new_args.push(simplify_at(pool, builtins, a, level));
70            }
71            if level >= 2
72                && let Some(r) = apply_rule(pool, builtins, f, &new_args)
73            {
74                return r;
75            }
76            pool.apply(f, &new_args)
77        }
78        _ => id,
79    }
80}
81
82fn apply_rule(
83    pool: &ExprPool,
84    builtins: &BuiltinSymbols,
85    f: ExprId,
86    args: &[ExprId],
87) -> Option<ExprId> {
88    if args.len() != 1 {
89        return None;
90    }
91    let arg = args[0];
92    if f == pool.symbol(builtins.sqrt) {
93        if let Some(n) = pool.const_number(arg)
94            && let Some(s) = n.sqrt()
95        {
96            return Some(pool.number(&s));
97        }
98        return None;
99    }
100    let sin = pool.symbol(builtins.sin);
101    let cos = pool.symbol(builtins.cos);
102    let tan = pool.symbol(builtins.tan);
103    if f == sin || f == cos || f == tan {
104        if let Some((c, s)) = trig_of_angle(pool, builtins, arg) {
105            if f == sin {
106                return Some(pool.number(&s));
107            }
108            if f == cos {
109                return Some(pool.number(&c));
110            }
111            if !c.is_zero() {
112                return Some(pool.number(&(s / c)));
113            }
114        }
115        return None;
116    }
117    if f == pool.symbol(builtins.exp) {
118        if arg == pool.integer(0) {
119            return Some(pool.integer(1));
120        }
121        return None;
122    }
123    if f == pool.symbol(builtins.log) || f == pool.symbol(builtins.ln) {
124        if arg == pool.integer(1) {
125            return Some(pool.integer(0));
126        }
127        if arg == pool.symbol(builtins.e) {
128            return Some(pool.integer(1));
129        }
130        return None;
131    }
132    if f == pool.symbol(builtins.abs) {
133        if let Some(n) = pool.const_number(arg) {
134            return Some(pool.number(&n.abs()));
135        }
136        return None;
137    }
138    None
139}
140
141/// An angle of the form `k·\pi`: extract the rational coefficient k, then consult the exact trig table (spec §7 built-in symbol simplification).
142fn trig_of_angle(
143    pool: &ExprPool,
144    builtins: &BuiltinSymbols,
145    expr: ExprId,
146) -> Option<(Number, Number)> {
147    let c = rational_pi_coefficient(pool, builtins, expr)?;
148    exact_trig(&c)
149}
150
151fn rational_pi_coefficient(
152    pool: &ExprPool,
153    builtins: &BuiltinSymbols,
154    expr: ExprId,
155) -> Option<BigRational> {
156    let node = pool.get(expr)?;
157    match node {
158        ExprData::Symbol(s) if s == builtins.pi => {
159            Some(BigRational::new(BigInt::from(1), BigInt::from(1)))
160        }
161        ExprData::Integer(_) | ExprData::Rational(_) | ExprData::Real(_) => {
162            let n = pool.const_number(expr)?;
163            if n.is_zero() {
164                Some(BigRational::new(BigInt::from(0), BigInt::from(1)))
165            } else {
166                None
167            }
168        }
169        ExprData::Mul(items) => {
170            let mut coeff: Option<BigRational> = None;
171            let mut found_pi = false;
172            for &it in items.iter() {
173                match pool.get(it)? {
174                    ExprData::Symbol(s) if s == builtins.pi => found_pi = true,
175                    ExprData::Integer(_) | ExprData::Rational(_) => {
176                        let c = match pool.const_number(it)? {
177                            Number::Integer(i) => BigRational::from_integer(i),
178                            Number::Rational(r) => r,
179                            _ => return None,
180                        };
181                        coeff = Some(match coeff {
182                            Some(acc) => acc * c,
183                            None => c,
184                        });
185                    }
186                    _ => return None,
187                }
188            }
189            if !found_pi {
190                return None;
191            }
192            Some(coeff.unwrap_or_else(|| BigRational::new(BigInt::from(1), BigInt::from(1))))
193        }
194        _ => None,
195    }
196}
197
198fn exact_trig(c: &BigRational) -> Option<(Number, Number)> {
199    let two = BigRational::new(BigInt::from(2), BigInt::from(1));
200    let mut c = c % two.clone();
201    if c < BigRational::new(BigInt::from(0), BigInt::from(1)) {
202        c += two;
203    }
204    let zero = BigRational::new(BigInt::from(0), BigInt::from(1));
205    let half = BigRational::new(BigInt::from(1), BigInt::from(2));
206    let one = BigRational::new(BigInt::from(1), BigInt::from(1));
207    let three_halves = BigRational::new(BigInt::from(3), BigInt::from(2));
208    if c == zero {
209        Some((Number::from(1), Number::from(0)))
210    } else if c == half {
211        Some((Number::from(0), Number::from(1)))
212    } else if c == one {
213        Some((Number::from(-1), Number::from(0)))
214    } else if c == three_halves {
215        Some((Number::from(0), Number::from(-1)))
216    } else {
217        None
218    }
219}
220
221/// Euler's formula (spec §7.4): fold `e^{iθ}` into `cosθ + i·sinθ` using the exact trig values of θ.
222fn euler(pool: &ExprPool, builtins: &BuiltinSymbols, z: ExprId) -> Option<ExprId> {
223    let i = pool.symbol(builtins.i);
224    let theta = match pool.get(z)? {
225        ExprData::Symbol(s) if s == builtins.i => return None,
226        ExprData::Mul(items) => {
227            let mut theta_items = Vec::new();
228            let mut has_i = false;
229            for &it in items.iter() {
230                if it == i {
231                    has_i = true;
232                } else {
233                    theta_items.push(it);
234                }
235            }
236            if !has_i || theta_items.is_empty() {
237                return None;
238            }
239            let mut acc = theta_items[0];
240            for &it in &theta_items[1..] {
241                acc = pool.mul2(acc, it);
242            }
243            acc
244        }
245        _ => return None,
246    };
247    let (c, s) = trig_of_angle(pool, builtins, theta)?;
248    if s == Number::from(0) {
249        Some(pool.number(&c))
250    } else if c == Number::from(0) && s == Number::from(1) {
251        Some(i)
252    } else if c == Number::from(0) && s == Number::from(-1) {
253        Some(pool.mul2(pool.integer(-1), i))
254    } else {
255        None
256    }
257}