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)));
51 normalized.sort();
52 merge_numbers(ctx, &mut normalized, true);
53 if normalized.is_empty() {
54 ctx.num(0)
55 } else if normalized.len() == 1 {
56 normalized[0]
57 } else {
58 ctx.add(&normalized)
59 }
60 }
61 AtomNode::Mul(args) => {
62 let normalized_children: Vec<Atom<'a>> =
64 args.iter().map(|a| normalize(ctx, *a)).collect();
65 let mut flat = Vec::new();
66 collect_mul(&normalized_children, &mut flat);
67 let mut normalized = flat;
68 if normalized
69 .iter()
70 .any(|a| matches!(a.node(), AtomNode::Num(0)))
71 {
72 return ctx.num(0);
73 }
74 normalized.retain(|a| !matches!(a.node(), AtomNode::Num(1)));
75 normalized.sort();
76 merge_numbers(ctx, &mut normalized, false);
77 if normalized.is_empty() {
78 ctx.num(1)
79 } else if normalized.len() == 1 {
80 normalized[0]
81 } else {
82 ctx.mul(&normalized)
83 }
84 }
85 AtomNode::Pow(base, exp) => {
86 let base = normalize(ctx, *base);
87 let exp = normalize(ctx, *exp);
88 ctx.pow(base, exp)
89 }
90 }
91}
92
93fn collect_add<'a>(args: &[Atom<'a>], out: &mut Vec<Atom<'a>>) {
94 for &arg in args {
95 match arg.node() {
96 AtomNode::Add(inner) => collect_add(inner, out),
97 _ => out.push(arg),
98 }
99 }
100}
101
102fn collect_mul<'a>(args: &[Atom<'a>], out: &mut Vec<Atom<'a>>) {
103 for &arg in args {
104 match arg.node() {
105 AtomNode::Mul(inner) => collect_mul(inner, out),
106 _ => out.push(arg),
107 }
108 }
109}
110
111fn merge_numbers<'a>(ctx: &AtomArena<'a>, args: &mut Vec<Atom<'a>>, is_add: bool) {
112 let count = args
113 .iter()
114 .take_while(|a| matches!(a.node(), AtomNode::Num(_)))
115 .count();
116
117 if count >= 2 {
118 let nums: Vec<i64> = args[0..count]
119 .iter()
120 .map(|a| match a.node() {
121 AtomNode::Num(n) => *n,
122 _ => unreachable!(),
123 })
124 .collect();
125 let merged = if is_add {
128 nums.into_iter().fold(0i64, |acc, n| acc.wrapping_add(n))
129 } else {
130 nums.into_iter().fold(1i64, |acc, n| acc.wrapping_mul(n))
131 };
132 args.drain(0..count);
133 args.insert(0, ctx.num(merged));
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use ocas_core::arena::Arena;
141
142 #[test]
143 fn normalize_leaves_atom_unchanged() {
144 let arena = Arena::new();
145 let ctx = AtomArena::new(&arena);
146 let x = ctx.var("x");
147 assert_eq!(normalize(&ctx, x).to_string(), "x");
148 }
149
150 #[test]
151 fn normalize_flattens_nested_add() {
152 let arena = Arena::new();
153 let ctx = AtomArena::new(&arena);
154 let x = ctx.var("x");
155 let y = ctx.var("y");
156 let z = ctx.var("z");
157 let inner = ctx.add(&[x, y]);
158 let outer = ctx.add(&[inner, z]);
159 assert_eq!(normalize(&ctx, outer).to_string(), "x + y + z");
160 }
161
162 #[test]
163 fn normalize_sorts_arguments() {
164 let arena = Arena::new();
165 let ctx = AtomArena::new(&arena);
166 let x = ctx.var("x");
167 let y = ctx.var("y");
168 let z = ctx.var("z");
169 let expr = ctx.add(&[z, x, y]);
170 assert_eq!(normalize(&ctx, expr).to_string(), "x + y + z");
171 }
172
173 #[test]
174 fn normalize_merges_numeric_literals() {
175 let arena = Arena::new();
176 let ctx = AtomArena::new(&arena);
177 let one = ctx.num(1);
178 let two = ctx.num(2);
179 let x = ctx.var("x");
180 let expr = ctx.add(&[one, x, two]);
181 assert_eq!(normalize(&ctx, expr).to_string(), "3 + x");
182 }
183
184 #[test]
185 fn normalize_pow() {
186 let arena = Arena::new();
187 let ctx = AtomArena::new(&arena);
188 let x = ctx.var("x");
189 let two = ctx.num(2);
190 let pow = ctx.pow(x, two);
191 assert_eq!(normalize(&ctx, pow).to_string(), "x^2");
192 }
193
194 #[test]
195 fn normalize_sorts_fun_arguments() {
196 let arena = Arena::new();
197 let ctx = AtomArena::new(&arena);
198 let x = ctx.var("x");
199 let y = ctx.var("y");
200 let f = ctx.fun("f", &[y, x]);
201 assert_eq!(normalize(&ctx, f).to_string(), "f(x, y)");
202 }
203}