1use crate::diagnostic::{DwCode, ExitTier};
53use std::cmp::Ordering;
54use std::path::{Path, PathBuf};
55
56pub const INDENT: usize = 2;
60
61#[derive(Debug, Clone, PartialEq)]
68pub enum Node {
69 Null,
70 Bool(bool),
71 Number(String),
73 Str(String),
75 Array(Vec<Node>),
77 Object(Vec<(String, Node)>),
79}
80
81#[derive(Debug, Clone, PartialEq)]
83pub struct ParseError {
84 pub code: DwCode,
86 pub line: usize,
88 pub col: usize,
90 pub message: String,
91}
92
93impl std::fmt::Display for ParseError {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 write!(f, "{}:{}: {}", self.line, self.col, self.message)
96 }
97}
98
99pub const DW_FMT_PARSE: DwCode = DwCode::new("DW0770", ExitTier::Build);
101pub const DW_FMT_DUPLICATE_KEY: DwCode = DwCode::new("DW0771", ExitTier::Build);
103pub const DW_FMT_NOT_EQUIVALENT: DwCode = DwCode::new("DW0772", ExitTier::Build);
106pub const DW_FMT_UNFORMATTED: DwCode = DwCode::new("DW0773", ExitTier::Build);
108pub const DW_FMT_NO_BINDING: DwCode = DwCode::new("DW0774", ExitTier::Build);
111
112struct Parser<'a> {
115 src: &'a [u8],
116 pos: usize,
117 line: usize,
118 col: usize,
119}
120
121impl<'a> Parser<'a> {
122 fn new(src: &'a str) -> Self {
123 Parser {
124 src: src.as_bytes(),
125 pos: 0,
126 line: 1,
127 col: 1,
128 }
129 }
130
131 fn err(&self, code: DwCode, message: impl Into<String>) -> ParseError {
132 ParseError {
133 code,
134 line: self.line,
135 col: self.col,
136 message: message.into(),
137 }
138 }
139
140 fn peek(&self) -> Option<u8> {
141 self.src.get(self.pos).copied()
142 }
143
144 fn bump(&mut self) -> Option<u8> {
147 let b = self.src.get(self.pos).copied()?;
148 self.pos += 1;
149 if b == b'\n' {
150 self.line += 1;
151 self.col = 1;
152 } else if b & 0xC0 != 0x80 {
153 self.col += 1;
154 }
155 Some(b)
156 }
157
158 fn skip_ws(&mut self) {
159 while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) {
160 self.bump();
161 }
162 }
163
164 fn expect(&mut self, want: u8) -> Result<(), ParseError> {
165 match self.peek() {
166 Some(b) if b == want => {
167 self.bump();
168 Ok(())
169 }
170 Some(b) => Err(self.err(
171 DW_FMT_PARSE,
172 format!(
173 "expected `{}`, found `{}`",
174 want as char,
175 escape_for_message(b)
176 ),
177 )),
178 None => Err(self.err(
179 DW_FMT_PARSE,
180 format!("expected `{}`, found end of file", want as char),
181 )),
182 }
183 }
184
185 fn literal(&mut self, word: &str, node: Node) -> Result<Node, ParseError> {
186 if self.src[self.pos..].starts_with(word.as_bytes()) {
187 for _ in 0..word.len() {
188 self.bump();
189 }
190 Ok(node)
191 } else {
192 Err(self.err(DW_FMT_PARSE, format!("expected `{word}`")))
193 }
194 }
195
196 fn value(&mut self) -> Result<Node, ParseError> {
197 self.skip_ws();
198 match self.peek() {
199 Some(b'{') => self.object(),
200 Some(b'[') => self.array(),
201 Some(b'"') => Ok(Node::Str(self.string()?)),
202 Some(b't') => self.literal("true", Node::Bool(true)),
203 Some(b'f') => self.literal("false", Node::Bool(false)),
204 Some(b'n') => self.literal("null", Node::Null),
205 Some(b'-' | b'0'..=b'9') => self.number(),
206 Some(b) => Err(self.err(
207 DW_FMT_PARSE,
208 format!("unexpected `{}`", escape_for_message(b)),
209 )),
210 None => Err(self.err(DW_FMT_PARSE, "unexpected end of file")),
211 }
212 }
213
214 fn object(&mut self) -> Result<Node, ParseError> {
215 self.expect(b'{')?;
216 let mut entries: Vec<(String, Node)> = Vec::new();
217 self.skip_ws();
218 if self.peek() == Some(b'}') {
219 self.bump();
220 return Ok(Node::Object(entries));
221 }
222 loop {
223 self.skip_ws();
224 let key_line = self.line;
225 let key_col = self.col;
226 let key = self.string()?;
227 if entries.iter().any(|(k, _)| *k == key) {
231 return Err(ParseError {
232 code: DW_FMT_DUPLICATE_KEY,
233 line: key_line,
234 col: key_col,
235 message: format!(
236 "duplicate object key `{key}`. JSON allows it and the compiler's \
237 parser silently keeps the LAST one, so one of these two values is \
238 already being discarded without a word. Formatting would make that \
239 loss permanent and invisible, so `delvec fmt` refuses: delete or \
240 rename whichever occurrence is wrong."
241 ),
242 });
243 }
244 self.skip_ws();
245 self.expect(b':')?;
246 let value = self.value()?;
247 entries.push((key, value));
248 self.skip_ws();
249 match self.peek() {
250 Some(b',') => {
251 self.bump();
252 self.skip_ws();
253 if self.peek() == Some(b'}') {
254 return Err(self.err(
255 DW_FMT_PARSE,
256 "trailing comma before `}` (JSON has no trailing commas)",
257 ));
258 }
259 }
260 Some(b'}') => {
261 self.bump();
262 return Ok(Node::Object(entries));
263 }
264 Some(b) => {
265 return Err(self.err(
266 DW_FMT_PARSE,
267 format!("expected `,` or `}}`, found `{}`", escape_for_message(b)),
268 ));
269 }
270 None => {
271 return Err(self.err(DW_FMT_PARSE, "unterminated object"));
272 }
273 }
274 }
275 }
276
277 fn array(&mut self) -> Result<Node, ParseError> {
278 self.expect(b'[')?;
279 let mut items = Vec::new();
280 self.skip_ws();
281 if self.peek() == Some(b']') {
282 self.bump();
283 return Ok(Node::Array(items));
284 }
285 loop {
286 items.push(self.value()?);
287 self.skip_ws();
288 match self.peek() {
289 Some(b',') => {
290 self.bump();
291 self.skip_ws();
292 if self.peek() == Some(b']') {
293 return Err(self.err(
294 DW_FMT_PARSE,
295 "trailing comma before `]` (JSON has no trailing commas)",
296 ));
297 }
298 }
299 Some(b']') => {
300 self.bump();
301 return Ok(Node::Array(items));
302 }
303 Some(b) => {
304 return Err(self.err(
305 DW_FMT_PARSE,
306 format!("expected `,` or `]`, found `{}`", escape_for_message(b)),
307 ));
308 }
309 None => {
310 return Err(self.err(DW_FMT_PARSE, "unterminated array"));
311 }
312 }
313 }
314 }
315
316 fn number(&mut self) -> Result<Node, ParseError> {
319 let start = self.pos;
320 if self.peek() == Some(b'-') {
321 self.bump();
322 }
323 match self.peek() {
324 Some(b'0') => {
325 self.bump();
326 }
327 Some(b'1'..=b'9') => {
328 while matches!(self.peek(), Some(b'0'..=b'9')) {
329 self.bump();
330 }
331 }
332 _ => return Err(self.err(DW_FMT_PARSE, "expected a digit after `-`")),
333 }
334 if self.peek() == Some(b'.') {
335 self.bump();
336 if !matches!(self.peek(), Some(b'0'..=b'9')) {
337 return Err(self.err(DW_FMT_PARSE, "expected a digit after `.`"));
338 }
339 while matches!(self.peek(), Some(b'0'..=b'9')) {
340 self.bump();
341 }
342 }
343 if matches!(self.peek(), Some(b'e' | b'E')) {
344 self.bump();
345 if matches!(self.peek(), Some(b'+' | b'-')) {
346 self.bump();
347 }
348 if !matches!(self.peek(), Some(b'0'..=b'9')) {
349 return Err(self.err(DW_FMT_PARSE, "expected a digit in the exponent"));
350 }
351 while matches!(self.peek(), Some(b'0'..=b'9')) {
352 self.bump();
353 }
354 }
355 let raw = std::str::from_utf8(&self.src[start..self.pos])
357 .expect("a JSON number literal is ASCII")
358 .to_string();
359 Ok(Node::Number(raw))
360 }
361
362 fn string(&mut self) -> Result<String, ParseError> {
363 self.expect(b'"')?;
364 let mut out = String::new();
365 loop {
366 let Some(b) = self.peek() else {
367 return Err(self.err(DW_FMT_PARSE, "unterminated string"));
368 };
369 match b {
370 b'"' => {
371 self.bump();
372 return Ok(out);
373 }
374 b'\\' => {
375 self.bump();
376 let Some(esc) = self.bump() else {
377 return Err(self.err(DW_FMT_PARSE, "unterminated escape"));
378 };
379 match esc {
380 b'"' => out.push('"'),
381 b'\\' => out.push('\\'),
382 b'/' => out.push('/'),
383 b'b' => out.push('\u{0008}'),
384 b'f' => out.push('\u{000c}'),
385 b'n' => out.push('\n'),
386 b'r' => out.push('\r'),
387 b't' => out.push('\t'),
388 b'u' => {
389 let hi = self.hex4()?;
390 let ch = if (0xD800..0xDC00).contains(&hi) {
391 if self.peek() != Some(b'\\') {
393 return Err(self.err(
394 DW_FMT_PARSE,
395 "lone high surrogate: `\\uD800`–`\\uDBFF` must be \
396 followed by a low surrogate escape",
397 ));
398 }
399 self.bump();
400 if self.peek() != Some(b'u') {
401 return Err(self.err(
402 DW_FMT_PARSE,
403 "lone high surrogate: expected `\\u` low surrogate",
404 ));
405 }
406 self.bump();
407 let lo = self.hex4()?;
408 if !(0xDC00..0xE000).contains(&lo) {
409 return Err(self.err(
410 DW_FMT_PARSE,
411 "high surrogate not followed by a low surrogate",
412 ));
413 }
414 let cp = 0x1_0000u32
415 + ((hi as u32 - 0xD800) << 10)
416 + (lo as u32 - 0xDC00);
417 char::from_u32(cp)
418 .ok_or_else(|| self.err(DW_FMT_PARSE, "invalid code point"))?
419 } else if (0xDC00..0xE000).contains(&hi) {
420 return Err(self.err(
421 DW_FMT_PARSE,
422 "lone low surrogate escape (`\\uDC00`–`\\uDFFF`)",
423 ));
424 } else {
425 char::from_u32(hi as u32)
426 .ok_or_else(|| self.err(DW_FMT_PARSE, "invalid code point"))?
427 };
428 out.push(ch);
429 }
430 other => {
431 return Err(self.err(
432 DW_FMT_PARSE,
433 format!("invalid escape `\\{}`", escape_for_message(other)),
434 ));
435 }
436 }
437 }
438 0x00..=0x1F => {
439 return Err(self.err(
440 DW_FMT_PARSE,
441 format!("raw control character U+{b:04X} in a string; escape it"),
442 ));
443 }
444 _ => {
445 let start = self.pos;
448 self.bump();
449 while matches!(self.peek(), Some(c) if c & 0xC0 == 0x80) {
450 self.bump();
451 }
452 out.push_str(
453 std::str::from_utf8(&self.src[start..self.pos])
454 .expect("the source was a &str"),
455 );
456 }
457 }
458 }
459 }
460
461 fn hex4(&mut self) -> Result<u16, ParseError> {
462 let mut v: u16 = 0;
463 for _ in 0..4 {
464 let Some(b) = self.bump() else {
465 return Err(self.err(DW_FMT_PARSE, "truncated `\\u` escape"));
466 };
467 let d = match b {
468 b'0'..=b'9' => b - b'0',
469 b'a'..=b'f' => b - b'a' + 10,
470 b'A'..=b'F' => b - b'A' + 10,
471 _ => {
472 return Err(self.err(
473 DW_FMT_PARSE,
474 format!(
475 "`\\u` escape needs 4 hex digits, found `{}`",
476 escape_for_message(b)
477 ),
478 ));
479 }
480 };
481 v = v * 16 + d as u16;
482 }
483 Ok(v)
484 }
485}
486
487fn escape_for_message(b: u8) -> String {
488 if (0x20..0x7F).contains(&b) {
489 (b as char).to_string()
490 } else {
491 format!("\\x{b:02x}")
492 }
493}
494
495pub fn parse(text: &str) -> Result<Node, ParseError> {
498 let mut p = Parser::new(text);
499 if p.src.starts_with(&[0xEF, 0xBB, 0xBF]) {
502 return Err(p.err(
503 DW_FMT_PARSE,
504 "file starts with a UTF-8 BOM; Delvewright JSON is plain UTF-8 with no BOM",
505 ));
506 }
507 let node = p.value()?;
508 p.skip_ws();
509 if p.pos != p.src.len() {
510 return Err(p.err(DW_FMT_PARSE, "trailing content after the top-level value"));
511 }
512 Ok(node)
513}
514
515pub fn canonical(node: &Node) -> String {
519 let mut out = String::new();
520 write_node(node, 0, &mut out);
521 out.push('\n');
522 out
523}
524
525fn write_node(node: &Node, depth: usize, out: &mut String) {
526 match node {
527 Node::Null => out.push_str("null"),
528 Node::Bool(true) => out.push_str("true"),
529 Node::Bool(false) => out.push_str("false"),
530 Node::Number(raw) => out.push_str(raw),
531 Node::Str(s) => write_string(s, out),
532 Node::Array(items) => {
533 if items.is_empty() {
534 out.push_str("[]");
535 return;
536 }
537 out.push_str("[\n");
538 for (i, item) in items.iter().enumerate() {
539 indent(depth + 1, out);
542 write_node(item, depth + 1, out);
543 if i + 1 < items.len() {
544 out.push(',');
545 }
546 out.push('\n');
547 }
548 indent(depth, out);
549 out.push(']');
550 }
551 Node::Object(entries) => {
552 if entries.is_empty() {
553 out.push_str("{}");
554 return;
555 }
556 let mut sorted: Vec<&(String, Node)> = entries.iter().collect();
560 sorted.sort_by(|a, b| cmp_key(&a.0, &b.0));
561 out.push_str("{\n");
562 for (i, (key, value)) in sorted.iter().enumerate() {
563 indent(depth + 1, out);
564 write_string(key, out);
565 out.push_str(": ");
566 write_node(value, depth + 1, out);
567 if i + 1 < sorted.len() {
568 out.push(',');
569 }
570 out.push('\n');
571 }
572 indent(depth, out);
573 out.push('}');
574 }
575 }
576}
577
578fn cmp_key(a: &str, b: &str) -> Ordering {
581 a.cmp(b)
582}
583
584fn indent(depth: usize, out: &mut String) {
585 for _ in 0..depth * INDENT {
586 out.push(' ');
587 }
588}
589
590fn write_string(s: &str, out: &mut String) {
593 out.push('"');
594 for ch in s.chars() {
595 match ch {
596 '"' => out.push_str("\\\""),
597 '\\' => out.push_str("\\\\"),
598 '\u{0008}' => out.push_str("\\b"),
599 '\u{000c}' => out.push_str("\\f"),
600 '\n' => out.push_str("\\n"),
601 '\r' => out.push_str("\\r"),
602 '\t' => out.push_str("\\t"),
603 c if (c as u32) < 0x20 => {
604 use std::fmt::Write as _;
605 let _ = write!(out, "\\u{:04x}", c as u32);
606 }
607 c => out.push(c),
608 }
609 }
610 out.push('"');
611}
612
613pub fn equivalent(before: &Node, after: &Node) -> Result<(), String> {
621 fn walk(a: &Node, b: &Node, path: &str) -> Result<(), String> {
622 match (a, b) {
623 (Node::Null, Node::Null) => Ok(()),
624 (Node::Bool(x), Node::Bool(y)) if x == y => Ok(()),
625 (Node::Number(x), Node::Number(y)) if x == y => Ok(()),
626 (Node::Str(x), Node::Str(y)) if x == y => Ok(()),
627 (Node::Array(x), Node::Array(y)) => {
628 if x.len() != y.len() {
629 return Err(format!(
630 "{path}: array length changed ({} → {})",
631 x.len(),
632 y.len()
633 ));
634 }
635 for (i, (xi, yi)) in x.iter().zip(y.iter()).enumerate() {
636 walk(xi, yi, &format!("{path}/{i}"))?;
639 }
640 Ok(())
641 }
642 (Node::Object(x), Node::Object(y)) => {
643 let mut xs: Vec<&(String, Node)> = x.iter().collect();
644 let mut ys: Vec<&(String, Node)> = y.iter().collect();
645 xs.sort_by(|p, q| cmp_key(&p.0, &q.0));
646 ys.sort_by(|p, q| cmp_key(&p.0, &q.0));
647 if xs.len() != ys.len() {
648 return Err(format!(
649 "{path}: object key count changed ({} → {})",
650 xs.len(),
651 ys.len()
652 ));
653 }
654 for (xe, ye) in xs.iter().zip(ys.iter()) {
655 if xe.0 != ye.0 {
656 return Err(format!("{path}: key `{}` became `{}`", xe.0, ye.0));
657 }
658 walk(&xe.1, &ye.1, &format!("{path}/{}", xe.0))?;
659 }
660 Ok(())
661 }
662 _ => Err(format!("{path}: value kind or content changed")),
663 }
664 }
665 walk(before, after, "")
666}
667
668pub fn format_text(text: &str) -> Result<String, ParseError> {
677 format_with(text, canonical)
678}
679
680fn format_with(text: &str, render: impl Fn(&Node) -> String) -> Result<String, ParseError> {
685 let mut before = parse(text)?;
686 stamp_version(&mut before);
687 let out = render(&before);
688 let after = parse(&out).map_err(|e| ParseError {
689 code: DW_FMT_NOT_EQUIVALENT,
690 line: e.line,
691 col: e.col,
692 message: format!(
693 "the formatter emitted JSON it cannot itself parse: {}",
694 e.message
695 ),
696 })?;
697 if let Err(why) = equivalent(&before, &after) {
698 return Err(ParseError {
699 code: DW_FMT_NOT_EQUIVALENT,
700 line: 1,
701 col: 1,
702 message: format!(
703 "internal error: formatting would change what this document means \
704 ({why}). Nothing was written. This is a compiler bug — arrays are \
705 ordered and must never be reordered; please report it."
706 ),
707 });
708 }
709 Ok(out)
710}
711
712fn stamp_version(node: &mut Node) {
719 if let Node::Object(fields) = node {
720 for (key, value) in fields.iter_mut() {
721 if key == "dsl_version"
722 && let Node::Str(v) = value
723 && v != crate::DSL_VERSION
724 {
725 *v = crate::DSL_VERSION.to_string();
726 }
727 }
728 }
729}
730
731pub const BUILD_OUTPUT_MARKER: &str = "manifest.json";
738
739pub fn discover(root: &Path) -> std::io::Result<Vec<PathBuf>> {
748 let mut out = Vec::new();
749 if root.is_file() {
750 out.push(root.to_path_buf());
751 return Ok(out);
752 }
753 walk(root, &mut out)?;
754 Ok(out)
755}
756
757fn walk(dir: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
758 if dir.join(BUILD_OUTPUT_MARKER).is_file() {
759 return Ok(());
760 }
761 let mut entries: Vec<PathBuf> = std::fs::read_dir(dir)?
762 .map(|e| e.map(|e| e.path()))
763 .collect::<Result<_, _>>()?;
764 entries.sort();
765 for path in entries {
766 let name = path
767 .file_name()
768 .and_then(|n| n.to_str())
769 .unwrap_or_default();
770 if name.starts_with('.') {
771 continue;
772 }
773 let meta = std::fs::symlink_metadata(&path)?;
777 if meta.is_dir() {
778 walk(&path, out)?;
779 } else if meta.is_file() && path.extension().and_then(|e| e.to_str()) == Some("json") {
780 out.push(path);
781 }
782 }
783 Ok(())
784}
785
786#[cfg(test)]
787mod tests {
788 use super::*;
789
790 #[test]
794 fn the_formatter_writes_the_one_dsl_version() {
795 let src = r#"{"dsl_version":"0.2.0","stage":"world","content":{"dsl_version":"0.2.0"}}"#;
796 let out = format_text(src).unwrap();
797 let stamped = format!(r#" "dsl_version": "{}","#, crate::DSL_VERSION);
798 assert!(out.contains(&stamped), "{out}");
799 assert!(
800 out.contains(r#" "dsl_version": "0.2.0""#),
801 "nested content stayed: {out}"
802 );
803 let already = format_text(&out).unwrap();
804 assert_eq!(already, out, "a stamped document is canonical");
805 }
806
807 #[test]
808 fn object_keys_are_sorted_arrays_are_not() {
809 let src = r#"{"b":1,"a":[3,1,2],"c":{"z":0,"y":0}}"#;
810 assert_eq!(
811 format_text(src).unwrap(),
812 "{\n \"a\": [\n 3,\n 1,\n 2\n ],\n \"b\": 1,\n \"c\": {\n \"y\": 0,\n \"z\": 0\n }\n}\n"
813 );
814 }
815
816 #[test]
817 fn number_literals_survive_verbatim() {
818 let src = r#"{"big":9007199254740993,"exact":1.50,"exp":1e3,"neg":-0.0}"#;
821 let out = format_text(src).unwrap();
822 assert!(out.contains("9007199254740993"), "{out}");
823 assert!(out.contains("1.50"), "{out}");
824 assert!(out.contains("1e3"), "{out}");
825 assert!(out.contains("-0.0"), "{out}");
826 }
827
828 #[test]
829 fn non_ascii_is_never_escaped_and_escapes_are_decoded() {
830 let src = r#"{"zh":"洞中公羊","emoji":"😀"}"#;
831 let out = format_text(src).unwrap();
832 assert!(out.contains("洞中公羊"), "{out}");
833 assert!(out.contains('\u{1F600}'), "{out}");
834 assert!(!out.contains("\\u"), "{out}");
835 }
836
837 #[test]
838 fn control_characters_keep_their_shortest_escape() {
839 let src = "{\"a\":\"x\\ny\\tz\\u0001\"}";
840 let out = format_text(src).unwrap();
841 assert_eq!(out, "{\n \"a\": \"x\\ny\\tz\\u0001\"\n}\n");
842 }
843
844 #[test]
845 fn idempotent() {
846 let src = r#"{"b":[{"q":1,"p":[2,1]}],"a":"é"}"#;
847 let once = format_text(src).unwrap();
848 assert_eq!(format_text(&once).unwrap(), once);
849 }
850
851 #[test]
852 fn duplicate_key_is_refused() {
853 let e = format_text(r#"{"a":1,"a":2}"#).unwrap_err();
854 assert_eq!(e.code, "DW0771");
855 }
856
857 #[test]
858 fn syntax_errors_are_located() {
859 let e = format_text("{\n \"a\": 1,\n}\n").unwrap_err();
860 assert_eq!(e.code, "DW0770");
861 assert_eq!(e.line, 3);
862 }
863
864 #[test]
865 fn empty_containers_stay_on_one_line() {
866 assert_eq!(
867 format_text(r#"{"a":[],"b":{}}"#).unwrap(),
868 "{\n \"a\": [],\n \"b\": {}\n}\n"
869 );
870 }
871
872 #[test]
876 fn the_guard_catches_a_renderer_that_sorts_arrays() {
877 fn sorting_render(node: &Node) -> String {
878 fn sabotage(n: &Node) -> Node {
879 match n {
880 Node::Array(items) => {
881 let mut v: Vec<Node> = items.iter().map(sabotage).collect();
882 v.sort_by_key(|n| match n {
883 Node::Number(raw) => raw.clone(),
884 Node::Str(s) => s.clone(),
885 _ => String::new(),
886 });
887 Node::Array(v)
888 }
889 Node::Object(e) => {
890 Node::Object(e.iter().map(|(k, v)| (k.clone(), sabotage(v))).collect())
891 }
892 other => other.clone(),
893 }
894 }
895 canonical(&sabotage(node))
896 }
897 let src = r#"{"objectives":["3","1","2"]}"#;
899 assert!(format_with(src, canonical).is_ok());
900 let e = format_with(src, sorting_render).unwrap_err();
901 assert_eq!(e.code, "DW0772");
902 assert!(e.message.contains("/objectives/0"), "{}", e.message);
903 }
904
905 #[test]
906 fn equivalent_catches_a_reordered_array() {
907 let a = parse("[1,2,3]").unwrap();
910 let b = parse("[3,2,1]").unwrap();
911 assert!(equivalent(&a, &b).is_err());
912 assert!(equivalent(&a, &a).is_ok());
913 }
914}