use std::cell::RefCell;
use std::sync::{Mutex, OnceLock};
use ocas_core::FastHashMap;
use ocas_core::arena::Arena;
pub mod normalize;
pub mod tensor;
pub mod walk;
pub mod workspace;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Symbol(&'static str);
impl Symbol {
pub fn new(name: &str) -> Self {
Self(intern(name))
}
pub fn as_str(&self) -> &str {
self.0
}
}
fn intern(name: &str) -> &'static str {
static TABLE: OnceLock<Mutex<FastHashMap<String, &'static str>>> = OnceLock::new();
let table = TABLE.get_or_init(|| Mutex::new(FastHashMap::default()));
let mut table = table.lock().expect("symbol interner lock poisoned");
table.entry(name.to_owned()).or_insert_with(|| {
let boxed = name.to_owned().into_boxed_str();
Box::leak(boxed)
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Atom<'a>(&'a AtomNode<'a>);
impl<'a> Atom<'a> {
pub fn node(&self) -> &'a AtomNode<'a> {
self.0
}
pub fn children(&self) -> &'a [Atom<'a>] {
match self.node() {
AtomNode::Num(_) | AtomNode::Var(_) => &[],
AtomNode::Fun(_, args) | AtomNode::Add(args) | AtomNode::Mul(args) => args,
AtomNode::Pow(base, exp) => {
let _ = (base, exp);
&[]
}
}
}
pub fn binary_children(&self) -> Option<(Atom<'a>, Atom<'a>)> {
match self.node() {
AtomNode::Pow(base, exp) => Some((*base, *exp)),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum AtomNode<'a> {
Num(i64),
Var(Symbol),
Fun(Symbol, &'a [Atom<'a>]),
Add(&'a [Atom<'a>]),
Mul(&'a [Atom<'a>]),
Pow(Atom<'a>, Atom<'a>),
}
pub struct AtomArena<'a> {
arena: &'a Arena,
cons_table: RefCell<FastHashMap<AtomNode<'a>, Atom<'a>>>,
}
impl<'a> AtomArena<'a> {
pub fn new(arena: &'a Arena) -> Self {
Self {
arena,
cons_table: RefCell::new(FastHashMap::default()),
}
}
fn intern(&self, candidate: AtomNode<'a>) -> Atom<'a> {
let mut table = self.cons_table.borrow_mut();
*table
.entry(candidate)
.or_insert_with(|| Atom(self.arena.allocate_with(|| candidate)))
}
pub fn num(&self, value: i64) -> Atom<'a> {
self.intern(AtomNode::Num(value))
}
pub fn var(&self, name: &str) -> Atom<'a> {
self.intern(AtomNode::Var(Symbol::new(name)))
}
pub fn fun(&self, name: &str, args: &[Atom<'a>]) -> Atom<'a> {
debug_assert!(!args.is_empty(), "Fun node requires at least one argument");
let slice = self.arena.allocate_slice(args);
self.intern(AtomNode::Fun(Symbol::new(name), slice))
}
pub fn add(&self, args: &[Atom<'a>]) -> Atom<'a> {
debug_assert!(!args.is_empty(), "Add node requires at least one argument");
let slice = self.arena.allocate_slice(args);
self.intern(AtomNode::Add(slice))
}
pub fn mul(&self, args: &[Atom<'a>]) -> Atom<'a> {
debug_assert!(!args.is_empty(), "Mul node requires at least one argument");
let slice = self.arena.allocate_slice(args);
self.intern(AtomNode::Mul(slice))
}
pub fn pow(&self, base: Atom<'a>, exp: Atom<'a>) -> Atom<'a> {
self.intern(AtomNode::Pow(base, exp))
}
pub fn slice(&self, atoms: &[Atom<'a>]) -> &'a [Atom<'a>] {
self.arena.allocate_slice(atoms)
}
}
impl std::fmt::Display for Atom<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.node() {
AtomNode::Num(n) => write!(f, "{n}"),
AtomNode::Var(s) => write!(f, "{}", s.as_str()),
AtomNode::Fun(name, args) => {
write!(f, "{}(", name.as_str())?;
for (i, arg) in args.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{arg}")?;
}
write!(f, ")")
}
AtomNode::Add(args) => {
for (i, arg) in args.iter().enumerate() {
if i > 0 {
write!(f, " + ")?;
}
write_parenthesized(arg, f)?;
}
Ok(())
}
AtomNode::Mul(args) => {
for (i, arg) in args.iter().enumerate() {
if i > 0 {
write!(f, "*")?;
}
write_parenthesized(arg, f)?;
}
Ok(())
}
AtomNode::Pow(base, exp) => {
write_parenthesized(base, f)?;
write!(f, "^")?;
write_parenthesized(exp, f)
}
}
}
}
fn write_parenthesized(atom: &Atom<'_>, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match atom.node() {
AtomNode::Num(_) | AtomNode::Var(_) => write!(f, "{atom}"),
_ => write!(f, "({atom})"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn construct_num() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let n = ctx.num(42);
assert_eq!(n.to_string(), "42");
assert!(matches!(n.node(), AtomNode::Num(42)));
}
#[test]
fn construct_var() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
assert_eq!(x.to_string(), "x");
assert!(matches!(x.node(), AtomNode::Var(s) if s.as_str() == "x"));
}
#[test]
fn construct_add() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let sum = ctx.add(&[x, y]);
assert_eq!(sum.to_string(), "x + y");
}
#[test]
fn construct_mul() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let two = ctx.num(2);
let prod = ctx.mul(&[x, two]);
assert_eq!(prod.to_string(), "x*2");
}
#[test]
fn construct_fun() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let sin = ctx.fun("sin", &[x]);
assert_eq!(sin.to_string(), "sin(x)");
}
#[test]
fn fun_with_multiple_args() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let f = ctx.fun("f", &[x, y]);
assert_eq!(f.to_string(), "f(x, y)");
}
#[test]
fn children_returns_direct_subexpressions() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let sum = ctx.add(&[x, y]);
assert_eq!(sum.children(), &[x, y]);
assert_eq!(x.children(), &[]);
}
#[test]
fn nested_expression_prints_with_parentheses() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let sum = ctx.add(&[x, y]);
let two = ctx.num(2);
let squared = ctx.pow(sum, two);
assert_eq!(squared.to_string(), "(x + y)^2");
}
#[test]
fn atom_equality_uses_structure() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let a = ctx.add(&[x, y]);
let b = ctx.add(&[x, y]);
let c = ctx.add(&[y, x]);
assert_eq!(a, b);
assert_ne!(a, c);
}
#[test]
fn symbol_identity_is_preserved() {
let a = Symbol::new("x");
let b = Symbol::new("x");
let c = Symbol::new("y");
assert_eq!(a, b);
assert_ne!(a, c);
assert_eq!(a.as_str(), "x");
}
#[test]
fn atom_is_copyable() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let copied = x;
assert_eq!(x, copied);
}
#[test]
fn hash_consing_reuses_identical_nodes() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let a = ctx.add(&[x, y]);
let b = ctx.add(&[x, y]);
assert!(std::ptr::eq(a.node(), b.node()));
}
#[test]
fn hash_consing_distinguishes_different_nodes() {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let x = ctx.var("x");
let y = ctx.var("y");
let a = ctx.add(&[x, y]);
let b = ctx.add(&[y, x]);
assert!(!std::ptr::eq(a.node(), b.node()));
}
}
#[cfg(test)]
mod proptests {
use super::*;
use ocas_core::arena::Arena;
use proptest::prelude::*;
#[derive(Debug, Clone)]
enum PropExpr {
Num(i64),
Var(&'static str),
Fun(&'static str, Vec<PropExpr>),
Add(Vec<PropExpr>),
Mul(Vec<PropExpr>),
Pow(Box<PropExpr>, Box<PropExpr>),
}
fn build_atom<'a>(ctx: &AtomArena<'a>, expr: &PropExpr) -> Atom<'a> {
match expr {
PropExpr::Num(n) => ctx.num(*n),
PropExpr::Var(name) => ctx.var(name),
PropExpr::Fun(name, args) => {
let atoms: Vec<Atom<'a>> = args.iter().map(|a| build_atom(ctx, a)).collect();
ctx.fun(name, &atoms)
}
PropExpr::Add(args) => {
let atoms: Vec<Atom<'a>> = args.iter().map(|a| build_atom(ctx, a)).collect();
ctx.add(&atoms)
}
PropExpr::Mul(args) => {
let atoms: Vec<Atom<'a>> = args.iter().map(|a| build_atom(ctx, a)).collect();
ctx.mul(&atoms)
}
PropExpr::Pow(base, exp) => ctx.pow(build_atom(ctx, base), build_atom(ctx, exp)),
}
}
fn prop_expr() -> impl Strategy<Value = PropExpr> {
let leaf = prop_oneof![
(-100..100i64).prop_map(PropExpr::Num),
Just(PropExpr::Var("x")),
Just(PropExpr::Var("y")),
Just(PropExpr::Var("z")),
];
leaf.prop_recursive(4, 64, 4, |inner| {
prop_oneof![
inner.clone().prop_map(|e| PropExpr::Fun("sin", vec![e])),
inner.clone().prop_map(|e| PropExpr::Fun("cos", vec![e])),
prop::collection::vec(inner.clone(), 1..4).prop_map(PropExpr::Add),
prop::collection::vec(inner.clone(), 1..4).prop_map(PropExpr::Mul),
(inner.clone(), inner.clone())
.prop_map(|(b, e)| PropExpr::Pow(Box::new(b), Box::new(e))),
]
})
}
proptest! {
#[test]
fn normalize_is_idempotent(expr in prop_expr()) {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let atom = build_atom(&ctx, &expr);
let once = normalize::normalize(&ctx, atom);
let twice = normalize::normalize(&ctx, once);
assert_eq!(once.to_string(), twice.to_string());
}
#[test]
fn add_identity(expr in prop_expr()) {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let atom = build_atom(&ctx, &expr);
let zero = ctx.num(0);
let with_zero = ctx.add(&[atom, zero]);
let normalized = normalize::normalize(&ctx, with_zero);
assert_eq!(normalized.to_string(), normalize::normalize(&ctx, atom).to_string());
}
#[test]
fn mul_identity(expr in prop_expr()) {
let arena = Arena::new();
let ctx = AtomArena::new(&arena);
let atom = build_atom(&ctx, &expr);
let one = ctx.num(1);
let with_one = ctx.mul(&[atom, one]);
let normalized = normalize::normalize(&ctx, with_one);
assert_eq!(normalized.to_string(), normalize::normalize(&ctx, atom).to_string());
}
}
}