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:
redclawsystems
2026-05-19 04:39:49 +00:00
commit f4b75db2ee
291 changed files with 130230 additions and 0 deletions
+903
View File
@@ -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")], &reg);
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")], &reg);
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], &reg);
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, &reg));
}
#[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, &reg));
}
#[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, &reg);
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");
}
}