1use std::fmt;
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
64pub struct ContentHash(pub [u8; 32]);
65
66impl ContentHash {
67 #[must_use]
69 pub fn to_hex(&self) -> String {
70 crate::hex::encode(&self.0)
71 }
72}
73
74impl fmt::Display for ContentHash {
75 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.write_str(&self.to_hex())
77 }
78}
79
80#[derive(Debug, Clone, PartialEq)]
82pub enum SExpr {
83 Symbol(String),
85 String(String),
87 Integer(i64),
89 Float(f64),
91 Bool(bool),
93 Nil,
95 List(Vec<SExpr>),
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
101pub enum SExprError {
102 Parse(String),
104 Shape(String),
106 MissingField(String),
108 UnknownVariant(String),
110}
111
112impl fmt::Display for SExprError {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114 match self {
115 Self::Parse(s) => write!(f, "sexpr parse: {s}"),
116 Self::Shape(s) => write!(f, "sexpr shape: {s}"),
117 Self::MissingField(s) => write!(f, "sexpr missing field: {s}"),
118 Self::UnknownVariant(s) => write!(f, "sexpr unknown variant: {s}"),
119 }
120 }
121}
122
123impl std::error::Error for SExprError {}
124
125impl SExpr {
126 #[must_use]
128 pub fn emit(&self) -> String {
129 let mut out = String::new();
130 self.emit_into(&mut out);
131 out
132 }
133
134 #[must_use]
148 pub fn content_hash(&self) -> ContentHash {
149 let emitted = self.emit();
150 let hash = blake3::hash(emitted.as_bytes());
151 ContentHash(*hash.as_bytes())
152 }
153
154 fn emit_into(&self, out: &mut String) {
155 match self {
156 Self::Symbol(s) => out.push_str(s),
157 Self::String(s) => {
158 out.push('"');
159 for c in s.chars() {
160 match c {
161 '"' => out.push_str("\\\""),
162 '\\' => out.push_str("\\\\"),
163 '\n' => out.push_str("\\n"),
164 '\t' => out.push_str("\\t"),
165 c => out.push(c),
166 }
167 }
168 out.push('"');
169 }
170 Self::Integer(i) => out.push_str(&i.to_string()),
171 Self::Float(fl) => {
172 let s = fl.to_string();
174 if s.contains('.')
175 || s.contains('e')
176 || s.contains('E')
177 || s == "inf"
178 || s == "-inf"
179 || s == "NaN"
180 {
181 out.push_str(&s);
182 } else {
183 out.push_str(&s);
184 out.push_str(".0");
185 }
186 }
187 Self::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
188 Self::Nil => out.push_str("nil"),
189 Self::List(items) => {
190 out.push('(');
191 for (i, item) in items.iter().enumerate() {
192 if i > 0 {
193 out.push(' ');
194 }
195 item.emit_into(out);
196 }
197 out.push(')');
198 }
199 }
200 }
201
202 pub fn parse(source: &str) -> Result<Self, SExprError> {
208 let mut chars = source.chars().peekable();
209 skip_ws_and_comments(&mut chars);
210 let val = read_one(&mut chars)?;
211 skip_ws_and_comments(&mut chars);
212 if chars.peek().is_some() {
213 return Err(SExprError::Parse("trailing content after value".into()));
214 }
215 Ok(val)
216 }
217
218 pub fn as_list(&self) -> Result<&[SExpr], SExprError> {
223 match self {
224 Self::List(items) => Ok(items),
225 other => Err(SExprError::Shape(format!("expected list, got {other:?}"))),
226 }
227 }
228
229 pub fn as_symbol(&self) -> Result<&str, SExprError> {
234 match self {
235 Self::Symbol(s) => Ok(s),
236 other => Err(SExprError::Shape(format!("expected symbol, got {other:?}"))),
237 }
238 }
239
240 pub fn as_str(&self) -> Result<&str, SExprError> {
245 match self {
246 Self::String(s) => Ok(s),
247 other => Err(SExprError::Shape(format!("expected string, got {other:?}"))),
248 }
249 }
250}
251
252fn skip_ws_and_comments(chars: &mut std::iter::Peekable<std::str::Chars>) {
255 loop {
256 match chars.peek().copied() {
257 Some(c) if c.is_whitespace() => {
258 chars.next();
259 }
260 Some(';') => {
261 for c in chars.by_ref() {
262 if c == '\n' {
263 break;
264 }
265 }
266 }
267 _ => break,
268 }
269 }
270}
271
272fn read_one(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<SExpr, SExprError> {
273 skip_ws_and_comments(chars);
274 match chars.peek().copied() {
275 None => Err(SExprError::Parse("unexpected EOF".into())),
276 Some('(') => read_list(chars),
277 Some('"') => read_string(chars),
278 Some(_) => read_atom(chars),
279 }
280}
281
282fn read_list(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<SExpr, SExprError> {
283 debug_assert_eq!(chars.next(), Some('('));
284 let mut items = Vec::new();
285 loop {
286 skip_ws_and_comments(chars);
287 match chars.peek().copied() {
288 None => return Err(SExprError::Parse("unterminated list".into())),
289 Some(')') => {
290 chars.next();
291 return Ok(SExpr::List(items));
292 }
293 Some(_) => items.push(read_one(chars)?),
294 }
295 }
296}
297
298fn read_string(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<SExpr, SExprError> {
299 debug_assert_eq!(chars.next(), Some('"'));
300 let mut buf = String::new();
301 loop {
302 match chars.next() {
303 None => return Err(SExprError::Parse("unterminated string".into())),
304 Some('"') => return Ok(SExpr::String(buf)),
305 Some('\\') => match chars.next() {
306 Some('n') => buf.push('\n'),
307 Some('t') => buf.push('\t'),
308 Some('"') => buf.push('"'),
309 Some('\\') => buf.push('\\'),
310 Some(c) => {
311 return Err(SExprError::Parse(format!("unknown escape: \\{c}")));
312 }
313 None => return Err(SExprError::Parse("dangling \\ at EOF".into())),
314 },
315 Some(c) => buf.push(c),
316 }
317 }
318}
319
320fn read_atom(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<SExpr, SExprError> {
321 let mut buf = String::new();
322 while let Some(&c) = chars.peek() {
323 if c.is_whitespace() || c == '(' || c == ')' || c == ';' || c == '"' {
324 break;
325 }
326 buf.push(c);
327 chars.next();
328 }
329 if buf.is_empty() {
330 return Err(SExprError::Parse("empty atom".into()));
331 }
332 match buf.as_str() {
334 "true" => return Ok(SExpr::Bool(true)),
335 "false" => return Ok(SExpr::Bool(false)),
336 "nil" => return Ok(SExpr::Nil),
337 _ => {}
338 }
339 if !buf.contains('.') && !buf.contains('e') && !buf.contains('E') {
341 if let Ok(i) = buf.parse::<i64>() {
342 return Ok(SExpr::Integer(i));
343 }
344 }
345 if let Ok(f) = buf.parse::<f64>() {
346 return Ok(SExpr::Float(f));
347 }
348 Ok(SExpr::Symbol(buf))
349}
350
351pub trait ToSExpr {
359 fn to_sexpr(&self) -> SExpr;
360
361 #[must_use]
366 fn content_hash(&self) -> ContentHash {
367 self.to_sexpr().content_hash()
368 }
369}
370
371pub trait FromSExpr: Sized {
373 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError>;
376}
377
378impl ToSExpr for String {
381 fn to_sexpr(&self) -> SExpr {
382 SExpr::String(self.clone())
383 }
384}
385
386impl FromSExpr for String {
387 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
388 Ok(s.as_str()?.to_string())
389 }
390}
391
392impl ToSExpr for bool {
393 fn to_sexpr(&self) -> SExpr {
394 SExpr::Bool(*self)
395 }
396}
397
398impl ToSExpr for i64 {
399 fn to_sexpr(&self) -> SExpr {
400 SExpr::Integer(*self)
401 }
402}
403
404impl FromSExpr for i64 {
405 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
406 match s {
407 SExpr::Integer(i) => Ok(*i),
408 other => Err(SExprError::Shape(format!(
409 "expected integer, got {other:?}"
410 ))),
411 }
412 }
413}
414
415impl ToSExpr for f64 {
416 fn to_sexpr(&self) -> SExpr {
417 SExpr::Float(*self)
418 }
419}
420
421impl FromSExpr for f64 {
422 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
423 match s {
424 SExpr::Float(f) => Ok(*f),
425 SExpr::Integer(i) => Ok(*i as f64), other => Err(SExprError::Shape(format!("expected float, got {other:?}"))),
427 }
428 }
429}
430
431impl FromSExpr for bool {
432 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
433 match s {
434 SExpr::Bool(b) => Ok(*b),
435 other => Err(SExprError::Shape(format!("expected bool, got {other:?}"))),
436 }
437 }
438}
439
440impl<T: ToSExpr> ToSExpr for Option<T> {
441 fn to_sexpr(&self) -> SExpr {
442 match self {
443 Some(v) => v.to_sexpr(),
444 None => SExpr::Nil,
445 }
446 }
447}
448
449impl<T: FromSExpr> FromSExpr for Option<T> {
450 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
451 match s {
452 SExpr::Nil => Ok(None),
453 other => Ok(Some(T::from_sexpr(other)?)),
454 }
455 }
456}
457
458impl<T: ToSExpr> ToSExpr for Vec<T> {
459 fn to_sexpr(&self) -> SExpr {
460 let mut items = Vec::with_capacity(self.len() + 1);
461 items.push(SExpr::Symbol("list".into()));
462 for v in self {
463 items.push(v.to_sexpr());
464 }
465 SExpr::List(items)
466 }
467}
468
469impl<T: FromSExpr> FromSExpr for Vec<T> {
470 fn from_sexpr(s: &SExpr) -> Result<Self, SExprError> {
471 let items = s.as_list()?;
472 let Some((head, rest)) = items.split_first() else {
473 return Err(SExprError::Shape("expected (list ...) form, got ()".into()));
474 };
475 let tag = head.as_symbol()?;
476 if tag != "list" {
477 return Err(SExprError::Shape(format!(
478 "expected list head 'list', got '{tag}'"
479 )));
480 }
481 rest.iter().map(T::from_sexpr).collect()
482 }
483}
484
485#[must_use]
489pub fn struct_expr(name: &str, fields: Vec<(&str, SExpr)>) -> SExpr {
490 let mut items = Vec::with_capacity(fields.len() + 1);
491 items.push(SExpr::Symbol(name.to_string()));
492 for (k, v) in fields {
493 items.push(SExpr::List(vec![SExpr::Symbol(format!(":{k}")), v]));
494 }
495 SExpr::List(items)
496}
497
498pub fn parse_struct<'a>(
504 s: &'a SExpr,
505 expected_name: &str,
506) -> Result<std::collections::BTreeMap<String, &'a SExpr>, SExprError> {
507 let items = s.as_list()?;
508 let (head, rest) = items
509 .split_first()
510 .ok_or_else(|| SExprError::Shape(format!("expected ({expected_name} …), got ()")))?;
511 let name = head.as_symbol()?;
512 if name != expected_name {
513 return Err(SExprError::Shape(format!(
514 "expected '{expected_name}', got '{name}'"
515 )));
516 }
517 let mut out = std::collections::BTreeMap::new();
518 for field in rest {
519 let pair = field.as_list()?;
520 if pair.len() != 2 {
521 return Err(SExprError::Shape(format!(
522 "expected (:field value) pair, got {pair:?}"
523 )));
524 }
525 let key = pair[0].as_symbol()?;
526 if !key.starts_with(':') {
527 return Err(SExprError::Shape(format!(
528 "expected keyword (:name), got '{key}'"
529 )));
530 }
531 out.insert(key[1..].to_string(), &pair[1]);
532 }
533 Ok(out)
534}
535
536pub fn take_field<'a>(
541 fields: &std::collections::BTreeMap<String, &'a SExpr>,
542 name: &str,
543) -> Result<&'a SExpr, SExprError> {
544 fields
545 .get(name)
546 .copied()
547 .ok_or_else(|| SExprError::MissingField(name.to_string()))
548}
549
550#[cfg(test)]
553mod tests {
554 use super::*;
555
556 #[test]
557 fn emit_symbol() {
558 assert_eq!(SExpr::Symbol("hello".into()).emit(), "hello");
559 }
560
561 #[test]
562 fn emit_string_with_escapes() {
563 assert_eq!(SExpr::String("a\"b\nc".into()).emit(), "\"a\\\"b\\nc\"",);
564 }
565
566 #[test]
567 fn emit_integer() {
568 assert_eq!(SExpr::Integer(42).emit(), "42");
569 assert_eq!(SExpr::Integer(-7).emit(), "-7");
570 }
571
572 #[test]
573 fn emit_float_adds_decimal_if_missing() {
574 assert_eq!(SExpr::Float(1.0).emit(), "1.0");
575 assert_eq!(SExpr::Float(2.75).emit(), "2.75");
576 }
577
578 #[test]
579 fn emit_bool() {
580 assert_eq!(SExpr::Bool(true).emit(), "true");
581 assert_eq!(SExpr::Bool(false).emit(), "false");
582 }
583
584 #[test]
585 fn emit_nil() {
586 assert_eq!(SExpr::Nil.emit(), "nil");
587 }
588
589 #[test]
590 fn emit_empty_list() {
591 assert_eq!(SExpr::List(vec![]).emit(), "()");
592 }
593
594 #[test]
595 fn emit_nested_list() {
596 let s = SExpr::List(vec![
597 SExpr::Symbol("foo".into()),
598 SExpr::Integer(1),
599 SExpr::List(vec![SExpr::Symbol("bar".into()), SExpr::String("x".into())]),
600 ]);
601 assert_eq!(s.emit(), "(foo 1 (bar \"x\"))");
602 }
603
604 #[test]
605 fn parse_primitives() {
606 assert_eq!(SExpr::parse("42").unwrap(), SExpr::Integer(42));
607 assert_eq!(SExpr::parse("2.75").unwrap(), SExpr::Float(2.75));
608 assert_eq!(SExpr::parse("true").unwrap(), SExpr::Bool(true));
609 assert_eq!(SExpr::parse("false").unwrap(), SExpr::Bool(false));
610 assert_eq!(SExpr::parse("nil").unwrap(), SExpr::Nil);
611 assert_eq!(
612 SExpr::parse("hello").unwrap(),
613 SExpr::Symbol("hello".into())
614 );
615 assert_eq!(SExpr::parse("\"hi\"").unwrap(), SExpr::String("hi".into()));
616 }
617
618 #[test]
619 fn parse_string_escapes() {
620 assert_eq!(
621 SExpr::parse("\"a\\nb\"").unwrap(),
622 SExpr::String("a\nb".into())
623 );
624 assert_eq!(
625 SExpr::parse("\"a\\\"b\"").unwrap(),
626 SExpr::String("a\"b".into())
627 );
628 }
629
630 #[test]
631 fn parse_nested_list() {
632 let s = SExpr::parse("(foo 1 (bar \"x\"))").unwrap();
633 assert_eq!(
634 s,
635 SExpr::List(vec![
636 SExpr::Symbol("foo".into()),
637 SExpr::Integer(1),
638 SExpr::List(vec![SExpr::Symbol("bar".into()), SExpr::String("x".into()),]),
639 ])
640 );
641 }
642
643 #[test]
644 fn parse_rejects_unterminated_list() {
645 assert!(matches!(SExpr::parse("(foo"), Err(SExprError::Parse(_))));
646 }
647
648 #[test]
649 fn parse_rejects_trailing_content() {
650 assert!(matches!(SExpr::parse("1 2"), Err(SExprError::Parse(_))));
651 }
652
653 #[test]
654 fn parse_tolerates_comments() {
655 let s = SExpr::parse("; leading\n(a ; mid\n b)\n; trailing").unwrap();
656 assert_eq!(
657 s,
658 SExpr::List(vec![SExpr::Symbol("a".into()), SExpr::Symbol("b".into())])
659 );
660 }
661
662 #[test]
663 fn round_trip_string() {
664 let s = "hello \"world\"\n".to_string();
665 let round = String::from_sexpr(&s.to_sexpr()).unwrap();
666 assert_eq!(s, round);
667 }
668
669 #[test]
670 fn round_trip_bool() {
671 assert_eq!(bool::from_sexpr(&true.to_sexpr()).unwrap(), true);
672 assert_eq!(bool::from_sexpr(&false.to_sexpr()).unwrap(), false);
673 }
674
675 #[test]
676 fn round_trip_option_some_and_none() {
677 let some: Option<String> = Some("hi".into());
678 let none: Option<String> = None;
679 assert_eq!(
680 Option::<String>::from_sexpr(&some.to_sexpr()).unwrap(),
681 some
682 );
683 assert_eq!(
684 Option::<String>::from_sexpr(&none.to_sexpr()).unwrap(),
685 none
686 );
687 }
688
689 #[test]
690 fn round_trip_vec() {
691 let v: Vec<String> = vec!["a".into(), "b".into()];
692 let s = v.to_sexpr();
693 assert_eq!(s.emit(), "(list \"a\" \"b\")");
694 assert_eq!(Vec::<String>::from_sexpr(&s).unwrap(), v);
695 }
696
697 #[test]
698 fn round_trip_empty_vec() {
699 let v: Vec<String> = vec![];
700 let s = v.to_sexpr();
701 assert_eq!(s.emit(), "(list)");
702 assert_eq!(Vec::<String>::from_sexpr(&s).unwrap(), v);
703 }
704
705 #[test]
706 fn struct_expr_and_parse_struct_round_trip() {
707 let s = struct_expr(
708 "attribute",
709 vec![
710 ("api-name", "foo".to_string().to_sexpr()),
711 ("required", true.to_sexpr()),
712 ],
713 );
714 assert_eq!(s.emit(), "(attribute (:api-name \"foo\") (:required true))");
715
716 let parsed = parse_struct(&s, "attribute").unwrap();
717 assert_eq!(parsed.len(), 2);
718 assert!(parsed.contains_key("api-name"));
719 assert!(parsed.contains_key("required"));
720 }
721
722 #[test]
723 fn parse_struct_errors_on_wrong_name() {
724 let s = struct_expr("attribute", vec![]);
725 let err = parse_struct(&s, "resource").unwrap_err();
726 assert!(matches!(err, SExprError::Shape(_)));
727 }
728
729 #[test]
730 fn take_field_missing_errors_cleanly() {
731 let s = struct_expr("x", vec![]);
732 let map = parse_struct(&s, "x").unwrap();
733 let err = take_field(&map, "foo").unwrap_err();
734 assert!(matches!(err, SExprError::MissingField(_)));
735 }
736
737 #[test]
738 fn float_and_integer_distinguished_on_parse() {
739 assert_eq!(SExpr::parse("1").unwrap(), SExpr::Integer(1));
740 assert_eq!(SExpr::parse("1.0").unwrap(), SExpr::Float(1.0));
741 }
742
743 #[test]
746 fn content_hash_is_64_hex_chars() {
747 let h = SExpr::Integer(42).content_hash();
748 assert_eq!(h.to_hex().len(), 64);
749 assert!(h.to_hex().chars().all(|c| c.is_ascii_hexdigit()));
750 assert_eq!(h.to_string(), h.to_hex());
751 }
752
753 #[test]
754 fn content_hash_is_deterministic() {
755 let s = SExpr::List(vec![
756 SExpr::Symbol("foo".into()),
757 SExpr::Integer(1),
758 SExpr::String("bar".into()),
759 ]);
760 assert_eq!(s.content_hash(), s.content_hash());
761 }
762
763 #[test]
764 fn content_hash_differs_for_different_values() {
765 let a = SExpr::Integer(1).content_hash();
766 let b = SExpr::Integer(2).content_hash();
767 assert_ne!(a, b);
768 }
769
770 #[test]
771 fn content_hash_matches_emission_hash() {
772 let s = SExpr::String("hello world".into());
775 let expected = {
776 let emitted = s.emit();
777 let h = blake3::hash(emitted.as_bytes());
778 ContentHash(*h.as_bytes())
779 };
780 assert_eq!(s.content_hash(), expected);
781 }
782
783 #[test]
784 fn to_sexpr_blanket_content_hash() {
785 let s = "hello".to_string();
788 assert_eq!(s.content_hash(), s.to_sexpr().content_hash());
789 }
790
791 #[test]
792 fn content_hash_stable_across_clones() {
793 let s = SExpr::List(vec![SExpr::Symbol("x".into()), SExpr::Integer(42)]);
794 let clone = s.clone();
795 assert_eq!(s.content_hash(), clone.content_hash());
796 }
797}