1use std::cmp::Ordering;
2
3use sha2::{Digest, Sha256};
4use unicode_normalization::UnicodeNormalization;
5
6use crate::{Atom, Document, Form, FormKind};
7
8pub fn canonicalize(document: &Document) -> Vec<u8> {
10 let mut forms = document.forms.iter().collect::<Vec<_>>();
11 if forms
12 .iter()
13 .any(|form| head_symbol(form) == Some("chappi-patch"))
14 {
15 forms.sort_by_key(|form| u8::from(head_symbol(form) != Some("chappi-patch")));
16 } else {
17 forms.sort_by(|left, right| compare_forms(left, right));
18 }
19
20 let mut output = String::new();
21 for form in forms {
22 write_form(&mut output, form);
23 output.push('\n');
24 }
25 output.into_bytes()
26}
27
28pub fn canonical_hash(document: &Document) -> [u8; 32] {
30 Sha256::digest(canonicalize(document)).into()
31}
32
33pub fn is_canonical_text(value: &str) -> bool {
35 value.nfc().eq(value.chars())
36}
37
38pub fn is_canonical_symbol(value: &str) -> bool {
40 is_canonical_text(value)
41 && is_symbol_literal(value)
42 && !is_integer_literal(value)
43 && !is_decimal_literal(value)
44}
45
46pub fn is_canonical_integer(value: &str) -> bool {
48 is_integer_literal(value) && normalize_integer(value) == value
49}
50
51pub fn is_canonical_decimal(value: &str) -> bool {
53 is_decimal_literal(value) && normalize_decimal(value) == value
54}
55
56pub fn atom_is_canonical(atom: &Atom) -> bool {
58 match atom {
59 Atom::Symbol(value) => is_canonical_symbol(value),
60 Atom::String(value) => is_canonical_text(value),
61 Atom::Integer(value) => is_canonical_integer(value),
62 Atom::Decimal(value) => is_canonical_decimal(value),
63 }
64}
65
66fn compare_forms(left: &Form, right: &Form) -> Ordering {
67 let rank = form_rank(left).cmp(&form_rank(right));
68 if rank != Ordering::Equal {
69 return rank;
70 }
71
72 let specific = match form_rank(left) {
73 1 => canonical_child(left, 1).cmp(&canonical_child(right, 1)),
74 2 => canonical_child(left, 1).cmp(&canonical_child(right, 1)),
75 3 => canonical_form(left).cmp(&canonical_form(right)),
76 4 => compare_focus(left, right),
77 _ => Ordering::Equal,
78 };
79 if specific == Ordering::Equal {
80 canonical_form(left).cmp(&canonical_form(right))
81 } else {
82 specific
83 }
84}
85
86fn form_rank(form: &Form) -> u8 {
87 match head_symbol(form) {
88 Some("chappi") => 0,
89 Some(head) if is_node_head(head) => 1,
90 Some("revision") => 2,
91 Some("rel") => 3,
92 Some("focus") => 4,
93 _ => 5,
94 }
95}
96
97fn is_node_head(head: &str) -> bool {
98 matches!(
99 head,
100 "entity"
101 | "claim"
102 | "evidence"
103 | "goal"
104 | "constraint"
105 | "question"
106 | "relationship"
107 | "desire"
108 | "attention"
109 | "association"
110 | "tension"
111 | "prediction"
112 | "perspective"
113 | "affect"
114 | "metacognition"
115 )
116}
117
118fn head_symbol(form: &Form) -> Option<&str> {
119 let FormKind::List(items) = &form.kind else {
120 return None;
121 };
122 let Some(Form {
123 kind: FormKind::Atom(Atom::Symbol(symbol)),
124 ..
125 }) = items.first()
126 else {
127 return None;
128 };
129 Some(symbol)
130}
131
132fn canonical_child(form: &Form, index: usize) -> Vec<u8> {
133 match &form.kind {
134 FormKind::List(items) => items.get(index).map(canonical_form).unwrap_or_default(),
135 FormKind::Atom(_) => Vec::new(),
136 }
137}
138
139fn compare_focus(left: &Form, right: &Form) -> Ordering {
140 let stream = canonical_child(left, 1).cmp(&canonical_child(right, 1));
141 if stream != Ordering::Equal {
142 return stream;
143 }
144 let sequence = focus_sequence(left);
145 let other_sequence = focus_sequence(right);
146 compare_integer_literals(sequence.as_deref(), other_sequence.as_deref())
147}
148
149fn focus_sequence(form: &Form) -> Option<String> {
150 let FormKind::List(items) = &form.kind else {
151 return None;
152 };
153 match items.get(2).map(|form| &form.kind) {
154 Some(FormKind::Atom(Atom::Integer(value))) => Some(normalize_integer(value)),
155 _ => None,
156 }
157}
158
159fn compare_integer_literals(left: Option<&str>, right: Option<&str>) -> Ordering {
160 match (left, right) {
161 (Some(left), Some(right)) => compare_normalized_integers(left, right),
162 (Some(_), None) => Ordering::Less,
163 (None, Some(_)) => Ordering::Greater,
164 (None, None) => Ordering::Equal,
165 }
166}
167
168fn compare_normalized_integers(left: &str, right: &str) -> Ordering {
169 let left_negative = left.starts_with('-');
170 let right_negative = right.starts_with('-');
171 match (left_negative, right_negative) {
172 (true, false) => Ordering::Less,
173 (false, true) => Ordering::Greater,
174 _ => {
175 let left_digits = left.strip_prefix('-').unwrap_or(left);
176 let right_digits = right.strip_prefix('-').unwrap_or(right);
177 let order = left_digits
178 .len()
179 .cmp(&right_digits.len())
180 .then_with(|| left_digits.cmp(right_digits));
181 if left_negative {
182 order.reverse()
183 } else {
184 order
185 }
186 }
187 }
188}
189
190fn canonical_form(form: &Form) -> Vec<u8> {
191 let mut output = String::new();
192 write_form(&mut output, form);
193 output.into_bytes()
194}
195
196fn write_form(output: &mut String, form: &Form) {
197 enum WriteItem<'a> {
198 Form(&'a Form),
199 Space,
200 CloseList,
201 }
202
203 let mut pending = vec![WriteItem::Form(form)];
204 while let Some(item) = pending.pop() {
205 match item {
206 WriteItem::Form(form) => match &form.kind {
207 FormKind::Atom(atom) => write_atom(output, atom),
208 FormKind::List(items) => {
209 output.push('(');
210 pending.push(WriteItem::CloseList);
211 for index in (0..items.len()).rev() {
212 pending.push(WriteItem::Form(&items[index]));
213 if index > 0 {
214 pending.push(WriteItem::Space);
215 }
216 }
217 }
218 },
219 WriteItem::Space => output.push(' '),
220 WriteItem::CloseList => output.push(')'),
221 }
222 }
223}
224
225fn write_atom(output: &mut String, atom: &Atom) {
226 match atom {
227 Atom::Symbol(value) => write_symbol(output, value),
228 Atom::Integer(value) if is_integer_literal(value) => {
229 output.push_str(&normalize_integer(value));
230 }
231 Atom::Decimal(value) if is_decimal_literal(value) => {
232 output.push_str(&normalize_decimal(value));
233 }
234 Atom::Integer(value) | Atom::Decimal(value) => write_string(output, value),
235 Atom::String(value) => write_string(output, value),
236 }
237}
238
239fn write_symbol(output: &mut String, value: &str) {
240 let value = value.nfc().collect::<String>();
241 if is_symbol_literal(&value) {
242 output.push_str(&value);
243 } else {
244 write_string(output, &value);
245 }
246}
247
248fn is_symbol_literal(value: &str) -> bool {
249 !value.is_empty()
250 && value.chars().all(|character| {
251 !character.is_control() && !matches!(character, ' ' | '\t' | '(' | ')' | '"' | ';')
252 })
253}
254
255fn is_integer_literal(value: &str) -> bool {
256 let digits = value.strip_prefix('-').unwrap_or(value);
257 !digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
258}
259
260fn is_decimal_literal(value: &str) -> bool {
261 let unsigned = value.strip_prefix('-').unwrap_or(value);
262 let Some((whole, fraction)) = unsigned.split_once('.') else {
263 return false;
264 };
265 !whole.is_empty()
266 && !fraction.is_empty()
267 && !fraction.contains('.')
268 && whole.bytes().all(|byte| byte.is_ascii_digit())
269 && fraction.bytes().all(|byte| byte.is_ascii_digit())
270}
271
272fn normalize_integer(value: &str) -> String {
273 let (negative, digits) = match value.strip_prefix('-') {
274 Some(digits) => (true, digits),
275 None => (false, value),
276 };
277 let digits = digits.trim_start_matches('0');
278 if digits.is_empty() {
279 "0".to_owned()
280 } else if negative {
281 format!("-{digits}")
282 } else {
283 digits.to_owned()
284 }
285}
286
287fn normalize_decimal(value: &str) -> String {
288 let (negative, unsigned) = match value.strip_prefix('-') {
289 Some(unsigned) => (true, unsigned),
290 None => (false, value),
291 };
292 let Some((whole, fraction)) = unsigned.split_once('.') else {
293 return value.to_owned();
294 };
295 let whole = whole.trim_start_matches('0');
296 let whole = if whole.is_empty() { "0" } else { whole };
297 let fraction = fraction.trim_end_matches('0');
298 if fraction.is_empty() {
299 return if negative && whole != "0" {
300 format!("-{whole}")
301 } else {
302 whole.to_owned()
303 };
304 }
305 let prefix = if negative { "-" } else { "" };
306 format!("{prefix}{whole}.{fraction}")
307}
308
309fn write_string(output: &mut String, value: &str) {
310 output.push('"');
311 for character in value.nfc() {
312 match character {
313 '"' => output.push_str("\\\""),
314 '\\' => output.push_str("\\\\"),
315 '\n' => output.push_str("\\n"),
316 '\r' => output.push_str("\\r"),
317 '\t' => output.push_str("\\t"),
318 character if character.is_control() => {
319 use std::fmt::Write as _;
320 write!(output, "\\u{{{:x}}}", character as u32)
321 .expect("writing to String cannot fail");
322 }
323 character => output.push(character),
324 }
325 }
326 output.push('"');
327}