1use std::collections::HashMap;
5use std::fmt;
6use std::sync::Arc;
7
8use crate::core::distribution::*;
9use crate::error::FugueResult;
10
11use super::Value;
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub enum SiteType {
17 F64,
19 Bool,
21 U64,
23 Usize,
25 I64,
27}
28
29impl SiteType {
30 pub fn name(self) -> &'static str {
32 match self {
33 SiteType::F64 => "f64",
34 SiteType::Bool => "bool",
35 SiteType::U64 => "u64",
36 SiteType::Usize => "usize",
37 SiteType::I64 => "i64",
38 }
39 }
40}
41
42impl fmt::Display for SiteType {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 f.write_str(self.name())
45 }
46}
47
48pub enum HostDist {
54 F64(Box<dyn Distribution<f64>>),
56 Bool(Box<dyn Distribution<bool>>),
58 U64(Box<dyn Distribution<u64>>),
60 Usize(Box<dyn Distribution<usize>>),
62 I64(Box<dyn Distribution<i64>>),
64}
65
66impl HostDist {
67 pub fn site_type(&self) -> SiteType {
69 match self {
70 HostDist::F64(_) => SiteType::F64,
71 HostDist::Bool(_) => SiteType::Bool,
72 HostDist::U64(_) => SiteType::U64,
73 HostDist::Usize(_) => SiteType::Usize,
74 HostDist::I64(_) => SiteType::I64,
75 }
76 }
77}
78
79impl fmt::Debug for HostDist {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 write!(f, "HostDist::{:?}(..)", self.site_type())
82 }
83}
84
85#[derive(Clone, Copy, Debug, PartialEq, Eq)]
88pub enum Arity {
89 Exact(usize),
91 Between(usize, usize),
93 AtLeast(usize),
95}
96
97impl Arity {
98 pub fn accepts(self, n: usize) -> bool {
100 match self {
101 Arity::Exact(k) => n == k,
102 Arity::Between(lo, hi) => (lo..=hi).contains(&n),
103 Arity::AtLeast(k) => n >= k,
104 }
105 }
106}
107
108impl fmt::Display for Arity {
109 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110 match self {
111 Arity::Exact(k) => write!(f, "{k}"),
112 Arity::Between(lo, hi) => write!(f, "{lo}..={hi}"),
113 Arity::AtLeast(k) => write!(f, "at least {k}"),
114 }
115 }
116}
117
118type DistCtor = dyn Fn(&[Value]) -> Result<HostDist, String> + Send + Sync;
119type FuncImpl = dyn Fn(&[Value]) -> Result<Value, String> + Send + Sync;
120
121#[derive(Clone)]
123pub(crate) struct DistEntry {
124 pub(crate) site: SiteType,
125 pub(crate) arity: Arity,
126 pub(crate) ctor: Arc<DistCtor>,
127}
128
129#[derive(Clone)]
131pub(crate) struct FuncEntry {
132 pub(crate) arity: Arity,
133 pub(crate) f: Arc<FuncImpl>,
134}
135
136#[derive(Clone)]
182pub struct Registry {
183 dists: HashMap<String, DistEntry>,
184 funcs: HashMap<String, FuncEntry>,
185}
186
187impl Default for Registry {
188 fn default() -> Self {
189 Registry::new()
190 }
191}
192
193impl fmt::Debug for Registry {
194 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195 let mut dists: Vec<_> = self.dists.keys().collect();
196 let mut funcs: Vec<_> = self.funcs.keys().collect();
197 dists.sort();
198 funcs.sort();
199 f.debug_struct("Registry")
200 .field("distributions", &dists)
201 .field("functions", &funcs)
202 .finish()
203 }
204}
205
206impl Registry {
207 pub fn new() -> Registry {
209 let mut r = Registry::empty();
210 register_builtins(&mut r);
211 r
212 }
213
214 pub fn empty() -> Registry {
217 Registry {
218 dists: HashMap::new(),
219 funcs: HashMap::new(),
220 }
221 }
222
223 pub fn register_distribution<F>(
238 &mut self,
239 name: impl Into<String>,
240 site: SiteType,
241 arity: Arity,
242 ctor: F,
243 ) -> &mut Registry
244 where
245 F: Fn(&[Value]) -> Result<HostDist, String> + Send + Sync + 'static,
246 {
247 self.dists.insert(
248 name.into(),
249 DistEntry {
250 site,
251 arity,
252 ctor: Arc::new(ctor),
253 },
254 );
255 self
256 }
257
258 pub fn register_function<F>(
262 &mut self,
263 name: impl Into<String>,
264 arity: Arity,
265 f: F,
266 ) -> &mut Registry
267 where
268 F: Fn(&[Value]) -> Result<Value, String> + Send + Sync + 'static,
269 {
270 self.funcs.insert(
271 name.into(),
272 FuncEntry {
273 arity,
274 f: Arc::new(f),
275 },
276 );
277 self
278 }
279
280 pub fn has_distribution(&self, name: &str) -> bool {
282 self.dists.contains_key(name)
283 }
284
285 pub fn has_function(&self, name: &str) -> bool {
287 self.funcs.contains_key(name)
288 }
289
290 pub fn distribution_site_type(&self, name: &str) -> Option<SiteType> {
292 self.dists.get(name).map(|d| d.site)
293 }
294
295 pub(crate) fn dist(&self, name: &str) -> Option<&DistEntry> {
296 self.dists.get(name)
297 }
298
299 pub(crate) fn func(&self, name: &str) -> Option<&FuncEntry> {
300 self.funcs.get(name)
301 }
302}
303
304fn num(args: &[Value], i: usize, name: &str) -> Result<f64, String> {
310 args[i].as_f64().ok_or_else(|| {
311 format!(
312 "`{name}` argument {} must be a number, found {}",
313 i + 1,
314 args[i].type_name()
315 )
316 })
317}
318
319fn boxed_f64<D: Distribution<f64> + 'static>(r: FugueResult<D>) -> Result<HostDist, String> {
320 r.map(|d| HostDist::F64(Box::new(d)))
321 .map_err(|e| e.to_string())
322}
323
324fn register_builtins(r: &mut Registry) {
325 macro_rules! f64_dist {
326 ($name:literal, $ty:ident, 1) => {
327 r.register_distribution($name, SiteType::F64, Arity::Exact(1), |a| {
328 boxed_f64($ty::new(num(a, 0, $name)?))
329 });
330 };
331 ($name:literal, $ty:ident, 2) => {
332 r.register_distribution($name, SiteType::F64, Arity::Exact(2), |a| {
333 boxed_f64($ty::new(num(a, 0, $name)?, num(a, 1, $name)?))
334 });
335 };
336 ($name:literal, $ty:ident, 3) => {
337 r.register_distribution($name, SiteType::F64, Arity::Exact(3), |a| {
338 boxed_f64($ty::new(
339 num(a, 0, $name)?,
340 num(a, 1, $name)?,
341 num(a, 2, $name)?,
342 ))
343 });
344 };
345 }
346 f64_dist!("Normal", Normal, 2);
347 f64_dist!("Uniform", Uniform, 2);
348 f64_dist!("LogNormal", LogNormal, 2);
349 f64_dist!("Beta", Beta, 2);
350 f64_dist!("Gamma", Gamma, 2);
351 f64_dist!("InverseGamma", InverseGamma, 2);
352 f64_dist!("Cauchy", Cauchy, 2);
353 f64_dist!("Laplace", Laplace, 2);
354 f64_dist!("Weibull", Weibull, 2);
355 f64_dist!("Exponential", Exponential, 1);
356 f64_dist!("ChiSquared", ChiSquared, 1);
357 f64_dist!("StudentT", StudentT, 3);
358
359 r.register_distribution("Bernoulli", SiteType::Bool, Arity::Exact(1), |a| {
360 Bernoulli::new(num(a, 0, "Bernoulli")?)
361 .map(|d| HostDist::Bool(Box::new(d)))
362 .map_err(|e| e.to_string())
363 });
364 r.register_distribution("Poisson", SiteType::U64, Arity::Exact(1), |a| {
365 Poisson::new(num(a, 0, "Poisson")?)
366 .map(|d| HostDist::U64(Box::new(d)))
367 .map_err(|e| e.to_string())
368 });
369 r.register_distribution("Binomial", SiteType::U64, Arity::Exact(2), |a| {
370 let n = a[0]
371 .as_u64()
372 .ok_or_else(|| format!("Binomial n must be a non-negative integer, got {}", a[0]))?;
373 Binomial::new(n, num(a, 1, "Binomial")?)
374 .map(|d| HostDist::U64(Box::new(d)))
375 .map_err(|e| e.to_string())
376 });
377 r.register_distribution("Categorical", SiteType::Usize, Arity::AtLeast(1), |a| {
378 let probs = match a {
380 [Value::Arr(p)] => p.as_ref().clone(),
381 _ => (0..a.len())
382 .map(|i| num(a, i, "Categorical"))
383 .collect::<Result<Vec<f64>, String>>()?,
384 };
385 Categorical::new(probs)
386 .map(|d| HostDist::Usize(Box::new(d)))
387 .map_err(|e| e.to_string())
388 });
389 r.register_distribution("DiscreteUniform", SiteType::I64, Arity::Exact(2), |a| {
390 let bound = |i: usize| {
391 a[i].as_i64()
392 .ok_or_else(|| format!("DiscreteUniform bounds must be integers, got {}", a[i]))
393 };
394 DiscreteUniform::new(bound(0)?, bound(1)?)
395 .map(|d| HostDist::I64(Box::new(d)))
396 .map_err(|e| e.to_string())
397 });
398
399 macro_rules! math1 {
400 ($($name:literal => $f:expr),* $(,)?) => {$(
401 r.register_function($name, Arity::Exact(1), |a| {
402 let f: fn(f64) -> f64 = $f;
403 Ok(Value::F64(f(num(a, 0, $name)?)))
404 });
405 )*};
406 }
407 math1! {
408 "exp" => f64::exp,
409 "ln" => f64::ln,
410 "log" => f64::ln,
411 "sqrt" => f64::sqrt,
412 "abs" => f64::abs,
413 "floor" => f64::floor,
414 "sin" => f64::sin,
415 "cos" => f64::cos,
416 "tanh" => f64::tanh,
417 }
418 macro_rules! math2 {
419 ($($name:literal => $f:expr),* $(,)?) => {$(
420 r.register_function($name, Arity::Exact(2), |a| {
421 let f: fn(f64, f64) -> f64 = $f;
422 Ok(Value::F64(f(num(a, 0, $name)?, num(a, 1, $name)?)))
423 });
424 )*};
425 }
426 math2! {
427 "pow" => f64::powf,
428 "min" => f64::min,
429 "max" => f64::max,
430 }
431}
432
433#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn builtins_are_registered_with_their_site_types() {
439 let r = Registry::new();
440 for (name, site) in [
441 ("Normal", SiteType::F64),
442 ("StudentT", SiteType::F64),
443 ("Bernoulli", SiteType::Bool),
444 ("Poisson", SiteType::U64),
445 ("Binomial", SiteType::U64),
446 ("Categorical", SiteType::Usize),
447 ("DiscreteUniform", SiteType::I64),
448 ] {
449 assert_eq!(r.distribution_site_type(name), Some(site), "{name}");
450 }
451 assert_eq!(r.dists.len(), 17);
452 for f in [
453 "exp", "ln", "log", "sqrt", "abs", "floor", "sin", "cos", "tanh", "pow", "min", "max",
454 ] {
455 assert!(r.has_function(f), "{f}");
456 }
457 assert!(!Registry::empty().has_distribution("Normal"));
458 assert!(format!("{r:?}").contains("Normal"));
459 }
460
461 #[test]
462 fn builtin_constructors_validate_arguments() {
463 let r = Registry::new();
464 let build = |name: &str, args: &[Value]| (r.dist(name).unwrap().ctor)(args);
465 assert!(build("Normal", &[Value::F64(0.0), Value::F64(-1.0)]).is_err());
466 assert!(build("Normal", &[Value::from(vec![1.0]), Value::F64(1.0)]).is_err());
467 assert!(build("Binomial", &[Value::F64(2.5), Value::F64(0.5)]).is_err());
468 assert!(build("Binomial", &[Value::Int(-1), Value::F64(0.5)]).is_err());
469 assert!(build("DiscreteUniform", &[Value::F64(0.5), Value::Int(3)]).is_err());
470 let many = Value::from(vec![1.0 / 100.0; 100]);
471 match build("Categorical", &[many]) {
472 Ok(HostDist::Usize(_)) => {}
473 other => panic!("{other:?}"),
474 }
475 assert!(build("Categorical", &[Value::F64(0.5), Value::from(vec![0.5])]).is_err());
476 let exp = r.func("exp").unwrap();
477 assert_eq!((exp.f)(&[Value::Int(0)]), Ok(Value::F64(1.0)));
478 assert!((exp.f)(&[Value::from(vec![0.0])]).is_err());
479 assert!(Arity::Between(1, 2).accepts(2) && !Arity::Between(1, 2).accepts(3));
480 assert_eq!(Arity::AtLeast(1).to_string(), "at least 1");
481 }
482}