1use std::cell::RefCell;
9use std::collections::HashMap;
10use std::sync::{Mutex, OnceLock};
11
12use ocas_core::arena::Arena;
13
14pub mod normalize;
15pub mod walk;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct Symbol(&'static str);
34
35impl Symbol {
36 pub fn new(name: &str) -> Self {
47 Self(intern(name))
48 }
49
50 pub fn as_str(&self) -> &str {
61 self.0
62 }
63}
64
65fn intern(name: &str) -> &'static str {
66 static TABLE: OnceLock<Mutex<HashMap<String, &'static str>>> = OnceLock::new();
67 let table = TABLE.get_or_init(|| Mutex::new(HashMap::new()));
68 let mut table = table.lock().expect("symbol interner lock poisoned");
69 table.entry(name.to_owned()).or_insert_with(|| {
70 let boxed = name.to_owned().into_boxed_str();
71 Box::leak(boxed)
72 })
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
94pub struct Atom<'a>(&'a AtomNode<'a>);
95
96impl<'a> Atom<'a> {
97 pub fn node(&self) -> &'a AtomNode<'a> {
111 self.0
112 }
113
114 pub fn children(&self) -> &'a [Atom<'a>] {
133 match self.node() {
134 AtomNode::Num(_) | AtomNode::Var(_) => &[],
135 AtomNode::Fun(_, args) | AtomNode::Add(args) | AtomNode::Mul(args) => args,
136 AtomNode::Pow(base, exp) => {
137 let _ = (base, exp);
142 &[]
143 }
144 }
145 }
146
147 pub fn binary_children(&self) -> Option<(Atom<'a>, Atom<'a>)> {
165 match self.node() {
166 AtomNode::Pow(base, exp) => Some((*base, *exp)),
167 _ => None,
168 }
169 }
170}
171
172#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
189pub enum AtomNode<'a> {
190 Num(i64),
192 Var(Symbol),
194 Fun(Symbol, &'a [Atom<'a>]),
196 Add(&'a [Atom<'a>]),
198 Mul(&'a [Atom<'a>]),
200 Pow(Atom<'a>, Atom<'a>),
202}
203
204pub struct AtomArena<'a> {
225 arena: &'a Arena,
226 cons_table: RefCell<HashMap<AtomNode<'a>, Atom<'a>>>,
227}
228
229impl<'a> AtomArena<'a> {
230 pub fn new(arena: &'a Arena) -> Self {
244 Self {
245 arena,
246 cons_table: RefCell::new(HashMap::new()),
247 }
248 }
249
250 fn intern(&self, candidate: AtomNode<'a>) -> Atom<'a> {
251 let mut table = self.cons_table.borrow_mut();
252 *table
253 .entry(candidate)
254 .or_insert_with(|| Atom(self.arena.allocate_with(|| candidate)))
255 }
256
257 pub fn num(&self, value: i64) -> Atom<'a> {
271 self.intern(AtomNode::Num(value))
272 }
273
274 pub fn var(&self, name: &str) -> Atom<'a> {
288 self.intern(AtomNode::Var(Symbol::new(name)))
289 }
290
291 pub fn fun(&self, name: &str, args: &[Atom<'a>]) -> Atom<'a> {
310 debug_assert!(!args.is_empty(), "Fun node requires at least one argument");
311 let slice = self.arena.allocate_slice(args);
312 self.intern(AtomNode::Fun(Symbol::new(name), slice))
313 }
314
315 pub fn add(&self, args: &[Atom<'a>]) -> Atom<'a> {
335 debug_assert!(!args.is_empty(), "Add node requires at least one argument");
336 let slice = self.arena.allocate_slice(args);
337 self.intern(AtomNode::Add(slice))
338 }
339
340 pub fn mul(&self, args: &[Atom<'a>]) -> Atom<'a> {
360 debug_assert!(!args.is_empty(), "Mul node requires at least one argument");
361 let slice = self.arena.allocate_slice(args);
362 self.intern(AtomNode::Mul(slice))
363 }
364
365 pub fn pow(&self, base: Atom<'a>, exp: Atom<'a>) -> Atom<'a> {
380 self.intern(AtomNode::Pow(base, exp))
381 }
382}
383
384impl std::fmt::Display for Atom<'_> {
385 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
386 match self.node() {
387 AtomNode::Num(n) => write!(f, "{n}"),
388 AtomNode::Var(s) => write!(f, "{}", s.as_str()),
389 AtomNode::Fun(name, args) => {
390 write!(f, "{}(", name.as_str())?;
391 for (i, arg) in args.iter().enumerate() {
392 if i > 0 {
393 write!(f, ", ")?;
394 }
395 write!(f, "{arg}")?;
396 }
397 write!(f, ")")
398 }
399 AtomNode::Add(args) => {
400 for (i, arg) in args.iter().enumerate() {
401 if i > 0 {
402 write!(f, " + ")?;
403 }
404 write_parenthesized(arg, f)?;
405 }
406 Ok(())
407 }
408 AtomNode::Mul(args) => {
409 for (i, arg) in args.iter().enumerate() {
410 if i > 0 {
411 write!(f, "*")?;
412 }
413 write_parenthesized(arg, f)?;
414 }
415 Ok(())
416 }
417 AtomNode::Pow(base, exp) => {
418 write_parenthesized(base, f)?;
419 write!(f, "^")?;
420 write_parenthesized(exp, f)
421 }
422 }
423 }
424}
425
426fn write_parenthesized(atom: &Atom<'_>, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
427 match atom.node() {
428 AtomNode::Num(_) | AtomNode::Var(_) => write!(f, "{atom}"),
429 _ => write!(f, "({atom})"),
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn construct_num() {
439 let arena = Arena::new();
440 let ctx = AtomArena::new(&arena);
441 let n = ctx.num(42);
442 assert_eq!(n.to_string(), "42");
443 assert!(matches!(n.node(), AtomNode::Num(42)));
444 }
445
446 #[test]
447 fn construct_var() {
448 let arena = Arena::new();
449 let ctx = AtomArena::new(&arena);
450 let x = ctx.var("x");
451 assert_eq!(x.to_string(), "x");
452 assert!(matches!(x.node(), AtomNode::Var(s) if s.as_str() == "x"));
453 }
454
455 #[test]
456 fn construct_add() {
457 let arena = Arena::new();
458 let ctx = AtomArena::new(&arena);
459 let x = ctx.var("x");
460 let y = ctx.var("y");
461 let sum = ctx.add(&[x, y]);
462 assert_eq!(sum.to_string(), "x + y");
463 }
464
465 #[test]
466 fn construct_mul() {
467 let arena = Arena::new();
468 let ctx = AtomArena::new(&arena);
469 let x = ctx.var("x");
470 let two = ctx.num(2);
471 let prod = ctx.mul(&[x, two]);
472 assert_eq!(prod.to_string(), "x*2");
473 }
474
475 #[test]
476 fn construct_fun() {
477 let arena = Arena::new();
478 let ctx = AtomArena::new(&arena);
479 let x = ctx.var("x");
480 let sin = ctx.fun("sin", &[x]);
481 assert_eq!(sin.to_string(), "sin(x)");
482 }
483
484 #[test]
485 fn fun_with_multiple_args() {
486 let arena = Arena::new();
487 let ctx = AtomArena::new(&arena);
488 let x = ctx.var("x");
489 let y = ctx.var("y");
490 let f = ctx.fun("f", &[x, y]);
491 assert_eq!(f.to_string(), "f(x, y)");
492 }
493
494 #[test]
495 fn children_returns_direct_subexpressions() {
496 let arena = Arena::new();
497 let ctx = AtomArena::new(&arena);
498 let x = ctx.var("x");
499 let y = ctx.var("y");
500 let sum = ctx.add(&[x, y]);
501 assert_eq!(sum.children(), &[x, y]);
502 assert_eq!(x.children(), &[]);
503 }
504
505 #[test]
506 fn nested_expression_prints_with_parentheses() {
507 let arena = Arena::new();
508 let ctx = AtomArena::new(&arena);
509 let x = ctx.var("x");
510 let y = ctx.var("y");
511 let sum = ctx.add(&[x, y]);
512 let two = ctx.num(2);
513 let squared = ctx.pow(sum, two);
514 assert_eq!(squared.to_string(), "(x + y)^2");
515 }
516
517 #[test]
518 fn atom_equality_uses_structure() {
519 let arena = Arena::new();
520 let ctx = AtomArena::new(&arena);
521 let x = ctx.var("x");
522 let y = ctx.var("y");
523 let a = ctx.add(&[x, y]);
524 let b = ctx.add(&[x, y]);
525 let c = ctx.add(&[y, x]);
526 assert_eq!(a, b);
527 assert_ne!(a, c);
528 }
529
530 #[test]
531 fn symbol_identity_is_preserved() {
532 let a = Symbol::new("x");
533 let b = Symbol::new("x");
534 let c = Symbol::new("y");
535 assert_eq!(a, b);
536 assert_ne!(a, c);
537 assert_eq!(a.as_str(), "x");
538 }
539
540 #[test]
541 fn atom_is_copyable() {
542 let arena = Arena::new();
543 let ctx = AtomArena::new(&arena);
544 let x = ctx.var("x");
545 let copied = x;
546 assert_eq!(x, copied);
547 }
548
549 #[test]
550 fn hash_consing_reuses_identical_nodes() {
551 let arena = Arena::new();
552 let ctx = AtomArena::new(&arena);
553 let x = ctx.var("x");
554 let y = ctx.var("y");
555 let a = ctx.add(&[x, y]);
556 let b = ctx.add(&[x, y]);
557 assert!(std::ptr::eq(a.node(), b.node()));
559 }
560
561 #[test]
562 fn hash_consing_distinguishes_different_nodes() {
563 let arena = Arena::new();
564 let ctx = AtomArena::new(&arena);
565 let x = ctx.var("x");
566 let y = ctx.var("y");
567 let a = ctx.add(&[x, y]);
568 let b = ctx.add(&[y, x]);
569 assert!(!std::ptr::eq(a.node(), b.node()));
570 }
571}
572
573#[cfg(test)]
574mod proptests {
575 use super::*;
576 use ocas_core::arena::Arena;
577 use proptest::prelude::*;
578
579 #[derive(Debug, Clone)]
581 enum PropExpr {
582 Num(i64),
583 Var(&'static str),
584 Fun(&'static str, Vec<PropExpr>),
585 Add(Vec<PropExpr>),
586 Mul(Vec<PropExpr>),
587 Pow(Box<PropExpr>, Box<PropExpr>),
588 }
589
590 fn build_atom<'a>(ctx: &AtomArena<'a>, expr: &PropExpr) -> Atom<'a> {
591 match expr {
592 PropExpr::Num(n) => ctx.num(*n),
593 PropExpr::Var(name) => ctx.var(name),
594 PropExpr::Fun(name, args) => {
595 let atoms: Vec<Atom<'a>> = args.iter().map(|a| build_atom(ctx, a)).collect();
596 ctx.fun(name, &atoms)
597 }
598 PropExpr::Add(args) => {
599 let atoms: Vec<Atom<'a>> = args.iter().map(|a| build_atom(ctx, a)).collect();
600 ctx.add(&atoms)
601 }
602 PropExpr::Mul(args) => {
603 let atoms: Vec<Atom<'a>> = args.iter().map(|a| build_atom(ctx, a)).collect();
604 ctx.mul(&atoms)
605 }
606 PropExpr::Pow(base, exp) => ctx.pow(build_atom(ctx, base), build_atom(ctx, exp)),
607 }
608 }
609
610 fn prop_expr() -> impl Strategy<Value = PropExpr> {
611 let leaf = prop_oneof![
612 (-100..100i64).prop_map(PropExpr::Num),
613 Just(PropExpr::Var("x")),
614 Just(PropExpr::Var("y")),
615 Just(PropExpr::Var("z")),
616 ];
617 leaf.prop_recursive(4, 64, 4, |inner| {
618 prop_oneof![
619 inner.clone().prop_map(|e| PropExpr::Fun("sin", vec![e])),
620 inner.clone().prop_map(|e| PropExpr::Fun("cos", vec![e])),
621 prop::collection::vec(inner.clone(), 1..4).prop_map(PropExpr::Add),
622 prop::collection::vec(inner.clone(), 1..4).prop_map(PropExpr::Mul),
623 (inner.clone(), inner.clone())
624 .prop_map(|(b, e)| PropExpr::Pow(Box::new(b), Box::new(e))),
625 ]
626 })
627 }
628
629 proptest! {
630 #[test]
631 fn normalize_is_idempotent(expr in prop_expr()) {
632 let arena = Arena::new();
633 let ctx = AtomArena::new(&arena);
634 let atom = build_atom(&ctx, &expr);
635 let once = normalize::normalize(&ctx, atom);
636 let twice = normalize::normalize(&ctx, once);
637 assert_eq!(once.to_string(), twice.to_string());
638 }
639
640 #[test]
641 fn add_identity(expr in prop_expr()) {
642 let arena = Arena::new();
643 let ctx = AtomArena::new(&arena);
644 let atom = build_atom(&ctx, &expr);
645 let zero = ctx.num(0);
646 let with_zero = ctx.add(&[atom, zero]);
647 let normalized = normalize::normalize(&ctx, with_zero);
648 assert_eq!(normalized.to_string(), normalize::normalize(&ctx, atom).to_string());
649 }
650
651 #[test]
652 fn mul_identity(expr in prop_expr()) {
653 let arena = Arena::new();
654 let ctx = AtomArena::new(&arena);
655 let atom = build_atom(&ctx, &expr);
656 let one = ctx.num(1);
657 let with_one = ctx.mul(&[atom, one]);
658 let normalized = normalize::normalize(&ctx, with_one);
659 assert_eq!(normalized.to_string(), normalize::normalize(&ctx, atom).to_string());
660 }
661 }
662}