1use crate::{Atom, AtomArena, AtomNode};
8
9pub fn preserves_argument_order(name: &str) -> bool {
22 matches!(
23 name,
24 "Derivative" | "Integral" | "EllipticF" | "EllipticE" | "EllipticPi" | "Ei"
25 )
26}
27
28pub fn normalize<'a>(ctx: &AtomArena<'a>, atom: Atom<'a>) -> Atom<'a> {
50 match atom.node() {
51 AtomNode::Num(_) | AtomNode::Var(_) => atom,
52 AtomNode::Fun(name, args) => {
53 let mut normalized: Vec<Atom<'a>> = args.iter().map(|a| normalize(ctx, *a)).collect();
54 if !preserves_argument_order(name.as_str()) {
56 normalized.sort();
57 }
58 ctx.fun(name.as_str(), &normalized)
59 }
60 AtomNode::Add(args) => {
61 let normalized_children: Vec<Atom<'a>> =
65 args.iter().map(|a| normalize(ctx, *a)).collect();
66 let mut flat = Vec::new();
67 collect_add(&normalized_children, &mut flat);
68 let mut normalized = flat;
69 normalized.retain(|a| !matches!(a.node(), AtomNode::Num(0)));
73 normalized.sort();
74 merge_numbers(ctx, &mut normalized, true);
75 normalized.retain(|a| !matches!(a.node(), AtomNode::Num(0)));
76 if normalized.is_empty() {
77 ctx.num(0)
78 } else if normalized.len() == 1 {
79 normalized[0]
80 } else {
81 ctx.add(&normalized)
82 }
83 }
84 AtomNode::Mul(args) => {
85 let normalized_children: Vec<Atom<'a>> =
87 args.iter().map(|a| normalize(ctx, *a)).collect();
88 let mut flat = Vec::new();
89 collect_mul(&normalized_children, &mut flat);
90 let mut normalized = flat;
91 if normalized
92 .iter()
93 .any(|a| matches!(a.node(), AtomNode::Num(0)))
94 {
95 return ctx.num(0);
96 }
97 normalized.retain(|a| !matches!(a.node(), AtomNode::Num(1)));
101 normalized.sort();
102 merge_numbers(ctx, &mut normalized, false);
103 normalized.retain(|a| !matches!(a.node(), AtomNode::Num(1)));
104 if normalized.is_empty() {
105 ctx.num(1)
106 } else if normalized.len() == 1 {
107 normalized[0]
108 } else {
109 ctx.mul(&normalized)
110 }
111 }
112 AtomNode::Pow(base, exp) => {
113 let base = normalize(ctx, *base);
114 let exp = normalize(ctx, *exp);
115 if let AtomNode::Num(e) = exp.node() {
118 if *e == 0 {
119 return ctx.num(1);
120 }
121 if *e == 1 {
122 return base;
123 }
124 if let AtomNode::Num(b) = base.node() {
125 if *b == 0 {
126 if *e > 0 {
127 return ctx.num(0);
128 }
129 } else if *b == 1 {
130 return ctx.num(1);
131 } else if *e > 0 {
132 if let Ok(e32) = u32::try_from(*e)
133 && let Some(v) = b.checked_pow(e32)
134 {
135 return ctx.num(v);
136 }
137 } else if *e == -1 && *b == -1 {
138 return ctx.num(-1);
139 }
140 }
141 }
142 if let AtomNode::Num(n) = exp.node()
147 && let AtomNode::Pow(inner_base, inner_exp) = base.node()
148 {
149 let merged = normalize(ctx, ctx.mul(&[*inner_exp, ctx.num(*n)]));
150 return ctx.pow(*inner_base, merged);
151 }
152 ctx.pow(base, exp)
153 }
154 }
155}
156
157fn collect_add<'a>(args: &[Atom<'a>], out: &mut Vec<Atom<'a>>) {
158 for &arg in args {
159 match arg.node() {
160 AtomNode::Add(inner) => collect_add(inner, out),
161 _ => out.push(arg),
162 }
163 }
164}
165
166fn collect_mul<'a>(args: &[Atom<'a>], out: &mut Vec<Atom<'a>>) {
167 for &arg in args {
168 match arg.node() {
169 AtomNode::Mul(inner) => collect_mul(inner, out),
170 _ => out.push(arg),
171 }
172 }
173}
174
175fn merge_numbers<'a>(ctx: &AtomArena<'a>, args: &mut Vec<Atom<'a>>, is_add: bool) {
176 let count = args
177 .iter()
178 .take_while(|a| matches!(a.node(), AtomNode::Num(_)))
179 .count();
180
181 if count >= 2 {
182 let nums: Vec<i64> = args[0..count]
183 .iter()
184 .map(|a| match a.node() {
185 AtomNode::Num(n) => *n,
186 _ => unreachable!(),
187 })
188 .collect();
189 let merged = if is_add {
192 nums.into_iter().fold(0i64, |acc, n| acc.wrapping_add(n))
193 } else {
194 nums.into_iter().fold(1i64, |acc, n| acc.wrapping_mul(n))
195 };
196 args.drain(0..count);
197 args.insert(0, ctx.num(merged));
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use ocas_core::arena::Arena;
205
206 #[test]
207 fn normalize_leaves_atom_unchanged() {
208 let arena = Arena::new();
209 let ctx = AtomArena::new(&arena);
210 let x = ctx.var("x");
211 assert_eq!(normalize(&ctx, x).to_string(), "x");
212 }
213
214 #[test]
215 fn normalize_flattens_nested_add() {
216 let arena = Arena::new();
217 let ctx = AtomArena::new(&arena);
218 let x = ctx.var("x");
219 let y = ctx.var("y");
220 let z = ctx.var("z");
221 let inner = ctx.add(&[x, y]);
222 let outer = ctx.add(&[inner, z]);
223 assert_eq!(normalize(&ctx, outer).to_string(), "x + y + z");
224 }
225
226 #[test]
227 fn normalize_drops_zero_from_opposite_numerics() {
228 let arena = Arena::new();
232 let ctx = AtomArena::new(&arena);
233 let x = ctx.var("x");
234 let sinx = ctx.fun("sin", &[x]);
235 let a1 = ctx.add(&[ctx.num(93)]);
236 let a2 = ctx.add(&[ctx.num(-93)]);
237 let atom = ctx.add(&[a1, a2, sinx]);
238 assert_eq!(normalize(&ctx, atom).to_string(), "sin(x)");
239 }
240
241 #[test]
242 fn normalize_drops_unit_from_opposite_numerics() {
243 let arena = Arena::new();
246 let ctx = AtomArena::new(&arena);
247 let x = ctx.var("x");
248 let neg1 = ctx.num(-1);
249 let inner = ctx.mul(&[x, neg1]);
250 let atom = ctx.mul(&[neg1, inner]);
251 assert_eq!(normalize(&ctx, atom).to_string(), "x");
252 }
253
254 #[test]
255 fn normalize_sorts_arguments() {
256 let arena = Arena::new();
257 let ctx = AtomArena::new(&arena);
258 let x = ctx.var("x");
259 let y = ctx.var("y");
260 let z = ctx.var("z");
261 let expr = ctx.add(&[z, x, y]);
262 assert_eq!(normalize(&ctx, expr).to_string(), "x + y + z");
263 }
264
265 #[test]
266 fn normalize_merges_numeric_literals() {
267 let arena = Arena::new();
268 let ctx = AtomArena::new(&arena);
269 let one = ctx.num(1);
270 let two = ctx.num(2);
271 let x = ctx.var("x");
272 let expr = ctx.add(&[one, x, two]);
273 assert_eq!(normalize(&ctx, expr).to_string(), "3 + x");
274 }
275
276 #[test]
277 fn normalize_pow() {
278 let arena = Arena::new();
279 let ctx = AtomArena::new(&arena);
280 let x = ctx.var("x");
281 let two = ctx.num(2);
282 let pow = ctx.pow(x, two);
283 assert_eq!(normalize(&ctx, pow).to_string(), "x^2");
284 }
285
286 #[test]
287 fn normalize_sorts_fun_arguments() {
288 let arena = Arena::new();
289 let ctx = AtomArena::new(&arena);
290 let x = ctx.var("x");
291 let y = ctx.var("y");
292 let f = ctx.fun("f", &[y, x]);
293 assert_eq!(normalize(&ctx, f).to_string(), "f(x, y)");
294 }
295
296 #[test]
297 fn normalize_preserves_elliptic_argument_order() {
298 let arena = Arena::new();
299 let ctx = AtomArena::new(&arena);
300 let x = ctx.var("x");
301 let y = ctx.var("y");
302 let f = ctx.fun("EllipticF", &[x, y]);
304 assert_eq!(normalize(&ctx, f).to_string(), "EllipticF(x, y)");
305 let g = ctx.fun("EllipticF", &[y, x]);
306 assert_eq!(normalize(&ctx, g).to_string(), "EllipticF(y, x)");
307 assert_eq!(
309 normalize(&ctx, normalize(&ctx, g)).to_string(),
310 "EllipticF(y, x)"
311 );
312 let p = ctx.fun("EllipticPi", &[y, x, ctx.num(1)]);
314 assert_eq!(normalize(&ctx, p).to_string(), "EllipticPi(y, x, 1)");
315 let ei = ctx.fun("Ei", &[ctx.num(1), x]);
317 assert_eq!(normalize(&ctx, ei).to_string(), "Ei(1, x)");
318 let h = ctx.fun("sin", &[y]);
320 assert_eq!(normalize(&ctx, h).to_string(), "sin(y)");
321 }
322}