1use crate::{Atom, AtomArena, AtomNode};
8
9pub fn normalize<'a>(ctx: &AtomArena<'a>, atom: Atom<'a>) -> Atom<'a> {
31 match atom.node() {
32 AtomNode::Num(_) | AtomNode::Var(_) => atom,
33 AtomNode::Fun(name, args) => {
34 let mut normalized: Vec<Atom<'a>> = args.iter().map(|a| normalize(ctx, *a)).collect();
35 if !matches!(name.as_str(), "Derivative" | "Integral") {
37 normalized.sort();
38 }
39 ctx.fun(name.as_str(), &normalized)
40 }
41 AtomNode::Add(args) => {
42 let normalized_children: Vec<Atom<'a>> =
46 args.iter().map(|a| normalize(ctx, *a)).collect();
47 let mut flat = Vec::new();
48 collect_add(&normalized_children, &mut flat);
49 let mut normalized = flat;
50 normalized.retain(|a| !matches!(a.node(), AtomNode::Num(0)));
54 normalized.sort();
55 merge_numbers(ctx, &mut normalized, true);
56 normalized.retain(|a| !matches!(a.node(), AtomNode::Num(0)));
57 if normalized.is_empty() {
58 ctx.num(0)
59 } else if normalized.len() == 1 {
60 normalized[0]
61 } else {
62 ctx.add(&normalized)
63 }
64 }
65 AtomNode::Mul(args) => {
66 let normalized_children: Vec<Atom<'a>> =
68 args.iter().map(|a| normalize(ctx, *a)).collect();
69 let mut flat = Vec::new();
70 collect_mul(&normalized_children, &mut flat);
71 let mut normalized = flat;
72 if normalized
73 .iter()
74 .any(|a| matches!(a.node(), AtomNode::Num(0)))
75 {
76 return ctx.num(0);
77 }
78 normalized.retain(|a| !matches!(a.node(), AtomNode::Num(1)));
82 normalized.sort();
83 merge_numbers(ctx, &mut normalized, false);
84 normalized.retain(|a| !matches!(a.node(), AtomNode::Num(1)));
85 if normalized.is_empty() {
86 ctx.num(1)
87 } else if normalized.len() == 1 {
88 normalized[0]
89 } else {
90 ctx.mul(&normalized)
91 }
92 }
93 AtomNode::Pow(base, exp) => {
94 let base = normalize(ctx, *base);
95 let exp = normalize(ctx, *exp);
96 ctx.pow(base, exp)
97 }
98 }
99}
100
101fn collect_add<'a>(args: &[Atom<'a>], out: &mut Vec<Atom<'a>>) {
102 for &arg in args {
103 match arg.node() {
104 AtomNode::Add(inner) => collect_add(inner, out),
105 _ => out.push(arg),
106 }
107 }
108}
109
110fn collect_mul<'a>(args: &[Atom<'a>], out: &mut Vec<Atom<'a>>) {
111 for &arg in args {
112 match arg.node() {
113 AtomNode::Mul(inner) => collect_mul(inner, out),
114 _ => out.push(arg),
115 }
116 }
117}
118
119fn merge_numbers<'a>(ctx: &AtomArena<'a>, args: &mut Vec<Atom<'a>>, is_add: bool) {
120 let count = args
121 .iter()
122 .take_while(|a| matches!(a.node(), AtomNode::Num(_)))
123 .count();
124
125 if count >= 2 {
126 let nums: Vec<i64> = args[0..count]
127 .iter()
128 .map(|a| match a.node() {
129 AtomNode::Num(n) => *n,
130 _ => unreachable!(),
131 })
132 .collect();
133 let merged = if is_add {
136 nums.into_iter().fold(0i64, |acc, n| acc.wrapping_add(n))
137 } else {
138 nums.into_iter().fold(1i64, |acc, n| acc.wrapping_mul(n))
139 };
140 args.drain(0..count);
141 args.insert(0, ctx.num(merged));
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148 use ocas_core::arena::Arena;
149
150 #[test]
151 fn normalize_leaves_atom_unchanged() {
152 let arena = Arena::new();
153 let ctx = AtomArena::new(&arena);
154 let x = ctx.var("x");
155 assert_eq!(normalize(&ctx, x).to_string(), "x");
156 }
157
158 #[test]
159 fn normalize_flattens_nested_add() {
160 let arena = Arena::new();
161 let ctx = AtomArena::new(&arena);
162 let x = ctx.var("x");
163 let y = ctx.var("y");
164 let z = ctx.var("z");
165 let inner = ctx.add(&[x, y]);
166 let outer = ctx.add(&[inner, z]);
167 assert_eq!(normalize(&ctx, outer).to_string(), "x + y + z");
168 }
169
170 #[test]
171 fn normalize_drops_zero_from_opposite_numerics() {
172 let arena = Arena::new();
176 let ctx = AtomArena::new(&arena);
177 let x = ctx.var("x");
178 let sinx = ctx.fun("sin", &[x]);
179 let a1 = ctx.add(&[ctx.num(93)]);
180 let a2 = ctx.add(&[ctx.num(-93)]);
181 let atom = ctx.add(&[a1, a2, sinx]);
182 assert_eq!(normalize(&ctx, atom).to_string(), "sin(x)");
183 }
184
185 #[test]
186 fn normalize_drops_unit_from_opposite_numerics() {
187 let arena = Arena::new();
190 let ctx = AtomArena::new(&arena);
191 let x = ctx.var("x");
192 let neg1 = ctx.num(-1);
193 let inner = ctx.mul(&[x, neg1]);
194 let atom = ctx.mul(&[neg1, inner]);
195 assert_eq!(normalize(&ctx, atom).to_string(), "x");
196 }
197
198 #[test]
199 fn normalize_sorts_arguments() {
200 let arena = Arena::new();
201 let ctx = AtomArena::new(&arena);
202 let x = ctx.var("x");
203 let y = ctx.var("y");
204 let z = ctx.var("z");
205 let expr = ctx.add(&[z, x, y]);
206 assert_eq!(normalize(&ctx, expr).to_string(), "x + y + z");
207 }
208
209 #[test]
210 fn normalize_merges_numeric_literals() {
211 let arena = Arena::new();
212 let ctx = AtomArena::new(&arena);
213 let one = ctx.num(1);
214 let two = ctx.num(2);
215 let x = ctx.var("x");
216 let expr = ctx.add(&[one, x, two]);
217 assert_eq!(normalize(&ctx, expr).to_string(), "3 + x");
218 }
219
220 #[test]
221 fn normalize_pow() {
222 let arena = Arena::new();
223 let ctx = AtomArena::new(&arena);
224 let x = ctx.var("x");
225 let two = ctx.num(2);
226 let pow = ctx.pow(x, two);
227 assert_eq!(normalize(&ctx, pow).to_string(), "x^2");
228 }
229
230 #[test]
231 fn normalize_sorts_fun_arguments() {
232 let arena = Arena::new();
233 let ctx = AtomArena::new(&arena);
234 let x = ctx.var("x");
235 let y = ctx.var("y");
236 let f = ctx.fun("f", &[y, x]);
237 assert_eq!(normalize(&ctx, f).to_string(), "f(x, y)");
238 }
239}