Merge pull request 'test(symclaw-skill): cover handlers_advanced via JSON API' (#10) from ci-doctor/coverage-20260518-201834 into master
Reviewed-on: #10
This commit is contained in:
@@ -0,0 +1,903 @@
|
||||
//! Symbol attributes, namespaces, and a global registry.
|
||||
//!
|
||||
//! Provides a thread-safe [`SymbolRegistry`] that associates [`SymbolAttributes`]
|
||||
//! (symmetric, antisymmetric, linear, real, positive, …) with symbol names.
|
||||
//! Attribute-aware normalization functions sort arguments of symmetric /
|
||||
//! antisymmetric functions and linearize where appropriate.
|
||||
|
||||
use crate::ast::Expr;
|
||||
use parking_lot::RwLock;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
// ── Core types ────────────────────────────────────────────────────────────
|
||||
|
||||
/// Attributes that can be attached to symbols.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct SymbolAttributes {
|
||||
/// Function is symmetric: f(a,b) = f(b,a), arguments auto-sorted.
|
||||
pub is_symmetric: bool,
|
||||
/// Function is antisymmetric: f(b,a) = -f(a,b), arguments auto-sorted with sign.
|
||||
pub is_antisymmetric: bool,
|
||||
/// Function is linear: f(a*x + b*y) = a*f(x) + b*f(y).
|
||||
pub is_linear: bool,
|
||||
/// Symbol is real-valued (not complex).
|
||||
pub is_real: bool,
|
||||
/// Symbol is positive.
|
||||
pub is_positive: bool,
|
||||
/// Symbol is integer-valued.
|
||||
pub is_integer: bool,
|
||||
/// Symbol is a constant (does not depend on any variable).
|
||||
pub is_constant: bool,
|
||||
/// Symbol is commutative (default true for most operations).
|
||||
pub is_commutative: bool,
|
||||
/// Custom tags for user-defined properties.
|
||||
pub tags: Vec<String>,
|
||||
/// Custom derivative rules: var → derivative expr string.
|
||||
pub derivative_rules: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// A namespace for organizing symbols.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct Namespace {
|
||||
/// Namespace name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// A fully qualified symbol: `namespace::name`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct QualifiedSymbol {
|
||||
/// Optional namespace prefix.
|
||||
pub namespace: Option<String>,
|
||||
/// Bare symbol name.
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
// ── Registry ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Global, thread-safe symbol registry.
|
||||
pub struct SymbolRegistry {
|
||||
attributes: HashMap<String, SymbolAttributes>,
|
||||
default_namespace: String,
|
||||
aliases: HashMap<String, String>,
|
||||
}
|
||||
|
||||
static GLOBAL_REGISTRY: LazyLock<RwLock<SymbolRegistry>> =
|
||||
LazyLock::new(|| RwLock::new(SymbolRegistry::new("global")));
|
||||
|
||||
impl SymbolRegistry {
|
||||
/// Create a new registry with the given default namespace.
|
||||
#[must_use]
|
||||
pub fn new(default_namespace: &str) -> Self {
|
||||
Self {
|
||||
attributes: HashMap::new(),
|
||||
default_namespace: default_namespace.to_string(),
|
||||
aliases: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a reference to the process-wide global registry.
|
||||
pub fn global() -> &'static RwLock<Self> {
|
||||
&GLOBAL_REGISTRY
|
||||
}
|
||||
|
||||
/// Define (or overwrite) a symbol's attributes.
|
||||
///
|
||||
/// # Errors
|
||||
/// Returns `Err` if the name is empty.
|
||||
pub fn define(&mut self, name: &str, attrs: SymbolAttributes) -> Result<(), String> {
|
||||
if name.is_empty() {
|
||||
return Err("symbol name must not be empty".into());
|
||||
}
|
||||
self.attributes.insert(name.to_string(), attrs);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retrieve attributes for `name` (returns defaults if not registered).
|
||||
#[must_use]
|
||||
pub fn get(&self, name: &str) -> SymbolAttributes {
|
||||
self.attributes.get(name).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
// ── Attribute queries ─────────────────────────────────────
|
||||
|
||||
/// Is the symbol marked symmetric?
|
||||
#[must_use]
|
||||
pub fn is_symmetric(&self, name: &str) -> bool {
|
||||
self.attributes.get(name).is_some_and(|a| a.is_symmetric)
|
||||
}
|
||||
/// Is the symbol marked antisymmetric?
|
||||
#[must_use]
|
||||
pub fn is_antisymmetric(&self, name: &str) -> bool {
|
||||
self.attributes
|
||||
.get(name)
|
||||
.is_some_and(|a| a.is_antisymmetric)
|
||||
}
|
||||
/// Is the symbol marked linear?
|
||||
#[must_use]
|
||||
pub fn is_linear(&self, name: &str) -> bool {
|
||||
self.attributes.get(name).is_some_and(|a| a.is_linear)
|
||||
}
|
||||
/// Is the symbol marked real?
|
||||
#[must_use]
|
||||
pub fn is_real(&self, name: &str) -> bool {
|
||||
self.attributes.get(name).is_some_and(|a| a.is_real)
|
||||
}
|
||||
/// Is the symbol marked positive?
|
||||
#[must_use]
|
||||
pub fn is_positive(&self, name: &str) -> bool {
|
||||
self.attributes.get(name).is_some_and(|a| a.is_positive)
|
||||
}
|
||||
/// Is the symbol marked integer?
|
||||
#[must_use]
|
||||
pub fn is_integer(&self, name: &str) -> bool {
|
||||
self.attributes.get(name).is_some_and(|a| a.is_integer)
|
||||
}
|
||||
/// Is the symbol marked constant?
|
||||
#[must_use]
|
||||
pub fn is_constant(&self, name: &str) -> bool {
|
||||
self.attributes.get(name).is_some_and(|a| a.is_constant)
|
||||
}
|
||||
|
||||
// ── Tags ──────────────────────────────────────────────────
|
||||
|
||||
/// Check whether a symbol carries a given tag.
|
||||
#[must_use]
|
||||
pub fn has_tag(&self, name: &str, tag: &str) -> bool {
|
||||
self.attributes
|
||||
.get(name)
|
||||
.is_some_and(|a| a.tags.iter().any(|t| t == tag))
|
||||
}
|
||||
|
||||
/// Add a tag to a symbol (creates default attrs if needed).
|
||||
pub fn add_tag(&mut self, name: &str, tag: &str) {
|
||||
self.attributes
|
||||
.entry(name.to_string())
|
||||
.or_default()
|
||||
.tags
|
||||
.push(tag.to_string());
|
||||
}
|
||||
|
||||
/// Get all tags for a symbol.
|
||||
#[must_use]
|
||||
pub fn get_tags(&self, name: &str) -> Vec<String> {
|
||||
self.attributes
|
||||
.get(name)
|
||||
.map(|a| a.tags.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// ── Namespaces ────────────────────────────────────────────
|
||||
|
||||
/// Set the default namespace.
|
||||
pub fn set_namespace(&mut self, ns: &str) {
|
||||
self.default_namespace = ns.to_string();
|
||||
}
|
||||
|
||||
/// Add a namespace alias (`alias` → `target`).
|
||||
pub fn add_alias(&mut self, alias: &str, target: &str) {
|
||||
self.aliases.insert(alias.to_string(), target.to_string());
|
||||
}
|
||||
|
||||
/// Resolve a potentially qualified name (`ns::name` or bare name).
|
||||
#[must_use]
|
||||
pub fn resolve(&self, name: &str) -> QualifiedSymbol {
|
||||
if let Some((ns, bare)) = name.split_once("::") {
|
||||
let resolved_ns = self.aliases.get(ns).map_or(ns, |s| s.as_str());
|
||||
QualifiedSymbol {
|
||||
namespace: Some(resolved_ns.to_string()),
|
||||
name: bare.to_string(),
|
||||
}
|
||||
} else {
|
||||
QualifiedSymbol {
|
||||
namespace: Some(self.default_namespace.clone()),
|
||||
name: name.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List all defined symbols and their attributes.
|
||||
#[must_use]
|
||||
pub fn all_symbols(&self) -> Vec<(String, SymbolAttributes)> {
|
||||
self.attributes
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Attribute-aware operations ────────────────────────────────────────────
|
||||
|
||||
/// Sort function arguments according to symbol attributes.
|
||||
///
|
||||
/// - Symmetric: sort canonically, sign = 1.
|
||||
/// - Antisymmetric: sort canonically, sign = (-1)^(number of transpositions).
|
||||
/// - Otherwise: unchanged, sign = 1.
|
||||
pub fn normalize_function(
|
||||
func_name: &str,
|
||||
args: &[Arc<Expr>],
|
||||
registry: &SymbolRegistry,
|
||||
) -> (Vec<Arc<Expr>>, i8) {
|
||||
let mut sorted = args.to_vec();
|
||||
if registry.is_symmetric(func_name) {
|
||||
crate::ast::canonical_sort(&mut sorted);
|
||||
return (sorted, 1);
|
||||
}
|
||||
if registry.is_antisymmetric(func_name) {
|
||||
let swaps = count_swaps(&mut sorted);
|
||||
let sign: i8 = if swaps.is_multiple_of(2) { 1 } else { -1 };
|
||||
return (sorted, sign);
|
||||
}
|
||||
// For non-attributed functions, still return unchanged.
|
||||
(sorted, 1)
|
||||
}
|
||||
|
||||
/// Bubble-sort `v` using canonical ordering, returning the number of swaps.
|
||||
fn count_swaps(v: &mut [Arc<Expr>]) -> usize {
|
||||
let mut swaps = 0usize;
|
||||
let n = v.len();
|
||||
for i in 0..n {
|
||||
for j in 0..n.saturating_sub(i + 1) {
|
||||
// Determine if v[j] should come after v[j+1] in canonical order.
|
||||
let mut pair = [v[j].clone(), v[j + 1].clone()];
|
||||
crate::ast::canonical_sort(&mut pair);
|
||||
if *pair[0] != *v[j] {
|
||||
v.swap(j, j + 1);
|
||||
swaps += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
swaps
|
||||
}
|
||||
|
||||
/// Linearize a function call if the function is marked linear.
|
||||
///
|
||||
/// Given `f(a*x + b*y)` with a single argument that is an `Add`, returns
|
||||
/// `a*f(x) + b*f(y)`. Returns `None` when linearization does not apply.
|
||||
pub fn linearize(func_name: &str, args: &[Arc<Expr>], registry: &SymbolRegistry) -> Option<Expr> {
|
||||
if !registry.is_linear(func_name) || args.len() != 1 {
|
||||
return None;
|
||||
}
|
||||
// Only linearize if the single argument is an Add.
|
||||
if let Expr::Add(terms) = args[0].as_ref() {
|
||||
let fid = crate::ast::FuncId::from_name(func_name)?;
|
||||
let mapped: Vec<Arc<Expr>> = terms
|
||||
.iter()
|
||||
.map(|term| {
|
||||
// Split coefficient: Mul([Num(c), rest…]) → c * f(rest)
|
||||
let (coeff, inner) = split_coeff(term);
|
||||
let call = Expr::Func(fid, vec![inner]);
|
||||
if let Some(c) = coeff {
|
||||
Arc::new(Expr::Mul(vec![Arc::new(c), Arc::new(call)]))
|
||||
} else {
|
||||
Arc::new(call)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if mapped.len() == 1 {
|
||||
return Some((*mapped[0]).clone());
|
||||
}
|
||||
return Some(Expr::Add(mapped));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Extract leading numeric coefficient from a term.
|
||||
/// Returns `(Some(Num(c)), rest)` or `(None, original)`.
|
||||
fn split_coeff(expr: &Arc<Expr>) -> (Option<Expr>, Arc<Expr>) {
|
||||
if let Expr::Mul(factors) = expr.as_ref()
|
||||
&& factors.len() >= 2
|
||||
&& let Expr::Num(_) = factors[0].as_ref()
|
||||
{
|
||||
let coeff = (*factors[0]).clone();
|
||||
let rest = if factors.len() == 2 {
|
||||
factors[1].clone()
|
||||
} else {
|
||||
Arc::new(Expr::Mul(factors[1..].to_vec()))
|
||||
};
|
||||
return (Some(coeff), rest);
|
||||
}
|
||||
(None, expr.clone())
|
||||
}
|
||||
|
||||
/// Apply all symbol attributes to normalize an expression (recursive).
|
||||
pub fn apply_attributes(expr: &Expr, registry: &SymbolRegistry) -> Expr {
|
||||
match expr {
|
||||
Expr::Func(fid, args) => {
|
||||
// Recursively normalize children first.
|
||||
let normed_args: Vec<Arc<Expr>> = args
|
||||
.iter()
|
||||
.map(|a| Arc::new(apply_attributes(a, registry)))
|
||||
.collect();
|
||||
|
||||
let fname = format!("{fid}");
|
||||
|
||||
// Try linearize first.
|
||||
if let Some(lin) = linearize(&fname, &normed_args, registry) {
|
||||
return lin;
|
||||
}
|
||||
|
||||
// Then normalize (symmetric / antisymmetric).
|
||||
let (sorted, sign) = normalize_function(&fname, &normed_args, registry);
|
||||
let func_expr = Expr::Func(*fid, sorted);
|
||||
if sign == -1 {
|
||||
Expr::Neg(Arc::new(func_expr))
|
||||
} else {
|
||||
func_expr
|
||||
}
|
||||
}
|
||||
Expr::Add(terms) => {
|
||||
let normed: Vec<Arc<Expr>> = terms
|
||||
.iter()
|
||||
.map(|t| Arc::new(apply_attributes(t, registry)))
|
||||
.collect();
|
||||
Expr::Add(normed)
|
||||
}
|
||||
Expr::Mul(factors) => {
|
||||
let normed: Vec<Arc<Expr>> = factors
|
||||
.iter()
|
||||
.map(|f| Arc::new(apply_attributes(f, registry)))
|
||||
.collect();
|
||||
Expr::Mul(normed)
|
||||
}
|
||||
Expr::Neg(inner) => Expr::Neg(Arc::new(apply_attributes(inner, registry))),
|
||||
Expr::Pow(base, exp) => Expr::Pow(
|
||||
Arc::new(apply_attributes(base, registry)),
|
||||
Arc::new(apply_attributes(exp, registry)),
|
||||
),
|
||||
other => other.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether an expression is real-valued given the registry.
|
||||
#[must_use]
|
||||
pub fn is_real(expr: &Expr, registry: &SymbolRegistry) -> bool {
|
||||
match expr {
|
||||
Expr::Num(_) | Expr::Float(_) => true,
|
||||
Expr::Sym(s) => registry.is_real(&s.as_str()),
|
||||
Expr::Add(terms) => terms.iter().all(|t| is_real(t, registry)),
|
||||
Expr::Mul(factors) => factors.iter().all(|f| is_real(f, registry)),
|
||||
Expr::Neg(inner) => is_real(inner, registry),
|
||||
Expr::Pow(base, exp) => is_real(base, registry) && is_real(exp, registry),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether an expression is positive given the registry.
|
||||
#[must_use]
|
||||
pub fn is_positive(expr: &Expr, registry: &SymbolRegistry) -> bool {
|
||||
match expr {
|
||||
Expr::Num(r) => *r > num_rational::Rational64::new(0, 1),
|
||||
Expr::Float(f) => f.into_inner() > 0.0,
|
||||
Expr::Sym(s) => registry.is_positive(&s.as_str()),
|
||||
Expr::Mul(factors) => factors.iter().all(|f| is_positive(f, registry)),
|
||||
Expr::Pow(base, _exp) => is_positive(base, registry),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Convenience functions ─────────────────────────────────────────────────
|
||||
|
||||
/// Define a symmetric function in the global registry.
|
||||
pub fn define_symmetric(name: &str) {
|
||||
SymbolRegistry::global()
|
||||
.write()
|
||||
.define(
|
||||
name,
|
||||
SymbolAttributes {
|
||||
is_symmetric: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("define_symmetric failed");
|
||||
}
|
||||
|
||||
/// Define an antisymmetric function in the global registry.
|
||||
pub fn define_antisymmetric(name: &str) {
|
||||
SymbolRegistry::global()
|
||||
.write()
|
||||
.define(
|
||||
name,
|
||||
SymbolAttributes {
|
||||
is_antisymmetric: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("define_antisymmetric failed");
|
||||
}
|
||||
|
||||
/// Define a linear function in the global registry.
|
||||
pub fn define_linear(name: &str) {
|
||||
SymbolRegistry::global()
|
||||
.write()
|
||||
.define(
|
||||
name,
|
||||
SymbolAttributes {
|
||||
is_linear: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("define_linear failed");
|
||||
}
|
||||
|
||||
/// Define a real variable in the global registry.
|
||||
pub fn define_real(name: &str) {
|
||||
SymbolRegistry::global()
|
||||
.write()
|
||||
.define(
|
||||
name,
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("define_real failed");
|
||||
}
|
||||
|
||||
/// Define a positive variable in the global registry.
|
||||
pub fn define_positive(name: &str) {
|
||||
SymbolRegistry::global()
|
||||
.write()
|
||||
.define(
|
||||
name,
|
||||
SymbolAttributes {
|
||||
is_positive: true,
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("define_positive failed");
|
||||
}
|
||||
|
||||
/// Define a constant in the global registry.
|
||||
pub fn define_constant(name: &str) {
|
||||
SymbolRegistry::global()
|
||||
.write()
|
||||
.define(
|
||||
name,
|
||||
SymbolAttributes {
|
||||
is_constant: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("define_constant failed");
|
||||
}
|
||||
|
||||
/// Define a symbol with multiple attributes at once in the global registry.
|
||||
pub fn define_symbol(name: &str, symmetric: bool, linear: bool, real: bool) {
|
||||
SymbolRegistry::global()
|
||||
.write()
|
||||
.define(
|
||||
name,
|
||||
SymbolAttributes {
|
||||
is_symmetric: symmetric,
|
||||
is_linear: linear,
|
||||
is_real: real,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.expect("define_symbol failed");
|
||||
}
|
||||
|
||||
// ── Built-ins ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// Register commonly-used mathematical symbols with sensible defaults.
|
||||
pub fn register_builtins(registry: &mut SymbolRegistry) {
|
||||
// Trig functions: real-valued for real input.
|
||||
for name in &["sin", "cos", "tan", "asin", "acos", "atan"] {
|
||||
let _ = registry.define(
|
||||
name,
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
// abs is real and positive.
|
||||
let _ = registry.define(
|
||||
"abs",
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
is_positive: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// exp is positive for real input.
|
||||
let _ = registry.define(
|
||||
"exp",
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
is_positive: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// ln is real (for positive input).
|
||||
let _ = registry.define(
|
||||
"ln",
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// dot product: symmetric and linear.
|
||||
let _ = registry.define(
|
||||
"dot",
|
||||
SymbolAttributes {
|
||||
is_symmetric: true,
|
||||
is_linear: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
// pi and e are real constants.
|
||||
for name in &["pi", "e"] {
|
||||
let _ = registry.define(
|
||||
name,
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
is_constant: true,
|
||||
is_positive: true,
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::ast::{Expr, FuncId};
|
||||
use crate::interner::Symbol;
|
||||
use num_rational::Rational64;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn sym(name: &str) -> Arc<Expr> {
|
||||
Arc::new(Expr::Sym(Symbol::new(name)))
|
||||
}
|
||||
|
||||
fn num(n: i64) -> Arc<Expr> {
|
||||
Arc::new(Expr::Num(Rational64::new(n, 1)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_symmetric_sort() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
reg.define(
|
||||
"f",
|
||||
SymbolAttributes {
|
||||
is_symmetric: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let (sorted, sign) = normalize_function("f", &[sym("b"), sym("a")], ®);
|
||||
assert_eq!(sign, 1);
|
||||
// After canonical sort, "a" should come before "b".
|
||||
assert_eq!(*sorted[0], Expr::Sym(Symbol::new("a")));
|
||||
assert_eq!(*sorted[1], Expr::Sym(Symbol::new("b")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_antisymmetric_sort_and_sign() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
reg.define(
|
||||
"f",
|
||||
SymbolAttributes {
|
||||
is_antisymmetric: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
// (b, a) requires 1 swap → sign = -1.
|
||||
let (sorted, sign) = normalize_function("f", &[sym("b"), sym("a")], ®);
|
||||
assert_eq!(sign, -1);
|
||||
assert_eq!(*sorted[0], Expr::Sym(Symbol::new("a")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_linearize() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
// Use a known FuncId name so from_name succeeds.
|
||||
reg.define(
|
||||
"sin",
|
||||
SymbolAttributes {
|
||||
is_linear: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// sin(2*x + 3*y) → 2*sin(x) + 3*sin(y)
|
||||
let two_x = Arc::new(Expr::Mul(vec![num(2), sym("x")]));
|
||||
let three_y = Arc::new(Expr::Mul(vec![num(3), sym("y")]));
|
||||
let sum = Arc::new(Expr::Add(vec![two_x, three_y]));
|
||||
let result = linearize("sin", &[sum], ®);
|
||||
assert!(result.is_some());
|
||||
if let Some(Expr::Add(terms)) = &result {
|
||||
assert_eq!(terms.len(), 2);
|
||||
} else {
|
||||
panic!("expected Add, got {result:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tags() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
reg.add_tag("x", "tensor");
|
||||
reg.add_tag("x", "rank2");
|
||||
assert!(reg.has_tag("x", "tensor"));
|
||||
assert!(!reg.has_tag("x", "scalar"));
|
||||
assert_eq!(reg.get_tags("x"), vec!["tensor", "rank2"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_namespace_default() {
|
||||
let reg = SymbolRegistry::new("math");
|
||||
let q = reg.resolve("gamma");
|
||||
assert_eq!(q.namespace, Some("math".into()));
|
||||
assert_eq!(q.name, "gamma");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_namespace_qualified() {
|
||||
let reg = SymbolRegistry::new("math");
|
||||
let q = reg.resolve("physics::gamma");
|
||||
assert_eq!(q.namespace, Some("physics".into()));
|
||||
assert_eq!(q.name, "gamma");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_real_propagation() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
reg.define(
|
||||
"x",
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
reg.define(
|
||||
"y",
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let expr = Expr::Add(vec![sym("x"), sym("y")]);
|
||||
assert!(is_real(&expr, ®));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_positive_mul() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
reg.define(
|
||||
"a",
|
||||
SymbolAttributes {
|
||||
is_positive: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
reg.define(
|
||||
"b",
|
||||
SymbolAttributes {
|
||||
is_positive: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let expr = Expr::Mul(vec![sym("a"), sym("b")]);
|
||||
assert!(is_positive(&expr, ®));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_constant() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
reg.define(
|
||||
"pi",
|
||||
SymbolAttributes {
|
||||
is_constant: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(reg.is_constant("pi"));
|
||||
assert!(!reg.is_constant("x"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_attributes_symmetric() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
// Use a built-in FuncId; mark it symmetric for this test.
|
||||
reg.define(
|
||||
"sin",
|
||||
SymbolAttributes {
|
||||
is_symmetric: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let expr = Expr::Func(FuncId::Sin, vec![sym("b"), sym("a")]);
|
||||
let normed = apply_attributes(&expr, ®);
|
||||
if let Expr::Func(_, args) = &normed {
|
||||
assert_eq!(*args[0], Expr::Sym(Symbol::new("a")));
|
||||
} else {
|
||||
panic!("expected Func");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_registry() {
|
||||
let reg = SymbolRegistry::global();
|
||||
{
|
||||
let mut w = reg.write();
|
||||
w.define(
|
||||
"test_global_sym",
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
{
|
||||
let r = reg.read();
|
||||
assert!(r.is_real("test_global_sym"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_attributes() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
reg.define(
|
||||
"f",
|
||||
SymbolAttributes {
|
||||
is_symmetric: true,
|
||||
is_linear: true,
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert!(reg.is_symmetric("f"));
|
||||
assert!(reg.is_linear("f"));
|
||||
assert!(reg.is_real("f"));
|
||||
assert!(!reg.is_antisymmetric("f"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_derivative_rule() {
|
||||
let mut attrs = SymbolAttributes::default();
|
||||
attrs.derivative_rules.insert("x".into(), "cos(x)".into());
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
reg.define("sin", attrs).unwrap();
|
||||
let a = reg.get("sin");
|
||||
assert_eq!(a.derivative_rules.get("x").unwrap(), "cos(x)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convenience_define_symmetric() {
|
||||
define_symmetric("conv_sym_test");
|
||||
let r = SymbolRegistry::global().read();
|
||||
assert!(r.is_symmetric("conv_sym_test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convenience_define_antisymmetric() {
|
||||
define_antisymmetric("conv_antisym_test");
|
||||
let r = SymbolRegistry::global().read();
|
||||
assert!(r.is_antisymmetric("conv_antisym_test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convenience_define_linear() {
|
||||
define_linear("conv_lin_test");
|
||||
let r = SymbolRegistry::global().read();
|
||||
assert!(r.is_linear("conv_lin_test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_convenience_define_real_positive_constant() {
|
||||
define_real("conv_real_test");
|
||||
define_positive("conv_pos_test");
|
||||
define_constant("conv_const_test");
|
||||
let r = SymbolRegistry::global().read();
|
||||
assert!(r.is_real("conv_real_test"));
|
||||
assert!(r.is_positive("conv_pos_test"));
|
||||
assert!(r.is_constant("conv_const_test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_define_symbol_multi() {
|
||||
define_symbol("conv_multi_test", true, true, true);
|
||||
let r = SymbolRegistry::global().read();
|
||||
assert!(r.is_symmetric("conv_multi_test"));
|
||||
assert!(r.is_linear("conv_multi_test"));
|
||||
assert!(r.is_real("conv_multi_test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builtins() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
register_builtins(&mut reg);
|
||||
assert!(reg.is_real("sin"));
|
||||
assert!(reg.is_positive("abs"));
|
||||
assert!(reg.is_symmetric("dot"));
|
||||
assert!(reg.is_constant("pi"));
|
||||
assert!(reg.is_positive("exp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_symbols() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
reg.define("a", SymbolAttributes::default()).unwrap();
|
||||
reg.define(
|
||||
"b",
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let all = reg.all_symbols();
|
||||
assert_eq!(all.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attribute_defaults() {
|
||||
let reg = SymbolRegistry::new("test");
|
||||
let attrs = reg.get("nonexistent");
|
||||
assert!(!attrs.is_symmetric);
|
||||
assert!(!attrs.is_real);
|
||||
assert!(attrs.tags.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_access() {
|
||||
use std::thread;
|
||||
let handles: Vec<_> = (0..4)
|
||||
.map(|i| {
|
||||
thread::spawn(move || {
|
||||
let name = format!("concurrent_{i}");
|
||||
let reg = SymbolRegistry::global();
|
||||
reg.write()
|
||||
.define(
|
||||
&name,
|
||||
SymbolAttributes {
|
||||
is_real: true,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
let r = reg.read();
|
||||
assert!(r.is_real(&name));
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
for h in handles {
|
||||
h.join().expect("thread panicked");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_name_error() {
|
||||
let mut reg = SymbolRegistry::new("test");
|
||||
assert!(reg.define("", SymbolAttributes::default()).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_namespace_alias() {
|
||||
let mut reg = SymbolRegistry::new("math");
|
||||
reg.add_alias("phys", "physics");
|
||||
let q = reg.resolve("phys::gamma");
|
||||
assert_eq!(q.namespace, Some("physics".into()));
|
||||
assert_eq!(q.name, "gamma");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user