Skip to main content

fugue/program/
mod.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/docs/program/README.md"))]
2
3mod ast;
4mod compile;
5mod data;
6mod interp;
7mod parse;
8mod registry;
9#[cfg(test)]
10mod tests;
11mod value;
12
13use std::fmt;
14use std::str::FromStr;
15use std::sync::{Arc, Mutex};
16
17use crate::core::model::{Model, ModelExt};
18
19pub use ast::{Addr, BinOp, DistCall, Expr, Program, Stmt, FORMAT_VERSION, MAX_NESTING};
20pub use data::Data;
21pub use registry::{Arity, HostDist, Registry, SiteType};
22pub use value::Value;
23
24/// Why a program could not be read, written or compiled.
25///
26/// Runtime problems are not errors: once a program compiles, building and
27/// running its model never fails — see the module docs' error policy.
28#[derive(Clone, Debug, PartialEq)]
29pub enum ProgramError {
30    /// The text front end rejected the source.
31    Syntax {
32        /// 1-based line of the offending token.
33        line: usize,
34        /// What was expected and what was found.
35        message: String,
36    },
37    /// The JSON front end could not decode the document.
38    Json(String),
39    /// The document's `fugue_program` version is not one this build reads.
40    UnsupportedVersion {
41        /// The version the document declares.
42        found: u64,
43        /// The version this build reads ([`FORMAT_VERSION`]).
44        supported: u32,
45    },
46    /// The program is well-formed but does not check: an unknown variable,
47    /// distribution or function, a wrong argument count, a `break` outside a
48    /// loop, a non-finite literal, a non-identifier name, or nesting beyond
49    /// [`MAX_NESTING`] (which the text front end, refusing as it reads,
50    /// reports as a [`Syntax`](ProgramError::Syntax) error with its line).
51    Check(String),
52    /// The data payload is malformed.
53    Data(String),
54}
55
56impl fmt::Display for ProgramError {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            ProgramError::Syntax { line, message } => write!(f, "line {line}: {message}"),
60            ProgramError::Json(m) => write!(f, "invalid program JSON: {m}"),
61            ProgramError::UnsupportedVersion { found, supported } => write!(
62                f,
63                "unsupported program format version {found}: this build of fugue reads \
64                 version {supported}"
65            ),
66            ProgramError::Check(m) | ProgramError::Data(m) => f.write_str(m),
67        }
68    }
69}
70
71impl std::error::Error for ProgramError {}
72
73impl Program {
74    /// Parse the text syntax (the grammar is in the [module docs](self)).
75    ///
76    /// Parsing needs no registry: names are resolved by
77    /// [`compile`](Program::compile), so an unknown distribution is a compile
78    /// error, not a syntax error.
79    ///
80    /// ```rust
81    /// use fugue::program::{Program, ProgramError};
82    ///
83    /// let p = Program::parse(
84    ///     r#"let mu <- sample(addr!("mu"), Normal::new(0.0, 1.0).unwrap());
85    ///        observe(addr!("y"), Normal(mu, 1.0), 0.5);
86    ///        pure(mu)"#,
87    /// )
88    /// .unwrap();
89    /// assert_eq!(p.body.len(), 2);
90    ///
91    /// match Program::parse("let x = 1.0;\nlet y = ;\npure(x)") {
92    ///     Err(ProgramError::Syntax { line, .. }) => assert_eq!(line, 2),
93    ///     other => panic!("{other:?}"),
94    /// }
95    /// ```
96    pub fn parse(src: &str) -> Result<Program, ProgramError> {
97        parse::parse_program(src)
98    }
99
100    /// Check the program against a registry and data, resolving every name,
101    /// and prepare it for building models.
102    ///
103    /// Everything that can be known before running is checked here: unknown
104    /// variables (including assignment to one), unknown distributions and
105    /// functions, argument counts, `break` outside a loop, and the structural
106    /// checks of [`to_json`](Program::to_json). The data's names are bound as
107    /// variables in an outer scope (a program's own `let` may shadow them).
108    ///
109    /// ```rust
110    /// use fugue::program::{Data, Program, Registry};
111    ///
112    /// let program = Program::parse("pure(y.len())").unwrap();
113    /// let registry = Registry::new();
114    /// assert!(program.compile(&registry, &Data::new()).is_err()); // unknown `y`
115    /// let compiled = program
116    ///     .compile(&registry, &Data::new().with("y", vec![1.0, 2.0]))
117    ///     .unwrap();
118    /// # let _ = compiled;
119    /// ```
120    pub fn compile(
121        &self,
122        registry: &Registry,
123        data: &Data,
124    ) -> Result<CompiledProgram, ProgramError> {
125        self.validate()?;
126        let mut resolver = compile::Resolver::new(registry);
127        let mut base: Vec<Value> = Vec::with_capacity(data.len());
128        for (name, value) in data.iter() {
129            let slot = resolver.bind(name);
130            debug_assert_eq!(slot, base.len());
131            base.push(value.clone());
132        }
133        resolver.push_scope();
134        let body = resolver.stmts(&self.body)?;
135        let ret = resolver.expr(&self.ret)?;
136        base.resize(resolver.slot_count(), Value::F64(f64::NAN));
137        Ok(CompiledProgram {
138            shared: Arc::new(interp::Shared {
139                body: body.into(),
140                ret,
141                warnings: Mutex::new(Vec::new()),
142            }),
143            base: base.into(),
144        })
145    }
146}
147
148impl FromStr for Program {
149    type Err = ProgramError;
150
151    /// Same as [`Program::parse`].
152    fn from_str(s: &str) -> Result<Program, ProgramError> {
153        Program::parse(s)
154    }
155}
156
157/// A checked program bound to its data, ready to build models.
158///
159/// [`build`](CompiledProgram::build) constructs a fresh single-use `Model`,
160/// so `|| compiled.build()` is the `model_fn` every fugue inference driver
161/// takes. A `CompiledProgram` is `Send + Sync`; clones share their warning
162/// buffer.
163///
164/// ```rust
165/// use fugue::program::{Data, Program, Registry, Value};
166/// use fugue::*;
167/// use rand::{rngs::StdRng, SeedableRng};
168///
169/// let program = Program::parse(
170///     r#"let p <- sample(addr!("p"), Beta(2.0, 2.0));
171///        for i in 0..flips.len() {
172///            observe(addr!("flip", i), Bernoulli(p), flips[i]);
173///        }
174///        pure(p > 0.5)"#,
175/// )
176/// .unwrap();
177/// let data = Data::from_json(r#"{"flips": [true, false, true, true]}"#).unwrap();
178/// let compiled = program.compile(&Registry::new(), &data).unwrap();
179///
180/// let mut rng = StdRng::seed_from_u64(7);
181/// let (favours_heads, trace) = runtime::handler::run(
182///     PriorHandler { rng: &mut rng, trace: Trace::default() },
183///     compiled.build(),
184/// );
185/// assert!(matches!(favours_heads, Value::Bool(_)));
186/// // Addresses are exactly what `addr!` builds in Rust.
187/// assert!(trace.get_f64(&addr!("p")).is_some());
188/// assert!(trace.log_likelihood.is_finite());
189///
190/// // Any inference driver takes it.
191/// let draws = adaptive_mcmc_chain(&mut rng, || compiled.build(), 200, 100);
192/// assert_eq!(draws.len(), 200);
193/// ```
194#[derive(Clone)]
195pub struct CompiledProgram {
196    shared: Arc<interp::Shared>,
197    base: Arc<[Value]>,
198}
199
200impl fmt::Debug for CompiledProgram {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        f.debug_struct("CompiledProgram")
203            .field("statements", &self.shared.body.len())
204            .field("slots", &self.base.len())
205            .finish_non_exhaustive()
206    }
207}
208
209impl CompiledProgram {
210    /// Build a fresh single-use model returning the program's value.
211    pub fn build(&self) -> Model<Value> {
212        interp::start(self.shared.clone(), &self.base)
213    }
214
215    /// Build a model returning the program's value as an `f64`: a number
216    /// converts, a bool reads as `1.0`/`0.0`, an array as `NaN` — the
217    /// playground interpreter's `Model<f64>` contract. For other result
218    /// types, map [`build`](CompiledProgram::build) with [`Value`]'s
219    /// accessors:
220    ///
221    /// ```rust
222    /// use fugue::program::{Data, Program, Registry};
223    /// use fugue::*;
224    ///
225    /// let compiled = Program::parse(r#"let k <- sample(addr!("k"), Poisson(3.0)); pure(k)"#)
226    ///     .unwrap()
227    ///     .compile(&Registry::new(), &Data::new())
228    ///     .unwrap();
229    /// let as_f64: Model<f64> = compiled.build_f64();
230    /// let as_u64: Model<u64> = compiled.build().map(|v| v.as_u64().unwrap_or(0));
231    /// # let _ = (as_f64, as_u64);
232    /// ```
233    pub fn build_f64(&self) -> Model<f64> {
234        self.build().map(|v| v.to_f64_lossy())
235    }
236
237    /// Drain the runtime warnings accumulated by this program's models so
238    /// far (each a soft error that was mapped to weight `-inf`). At most 64
239    /// are kept between drains.
240    ///
241    /// ```rust
242    /// use fugue::program::{Data, Program, Registry};
243    /// use fugue::*;
244    /// use rand::{rngs::StdRng, SeedableRng};
245    ///
246    /// let compiled = Program::parse(r#"let s <- sample(addr!("s"), Normal(0.0, -1.0)); pure(s)"#)
247    ///     .unwrap()
248    ///     .compile(&Registry::new(), &Data::new())
249    ///     .unwrap();
250    /// let (_, trace) = runtime::handler::run(
251    ///     PriorHandler { rng: &mut StdRng::seed_from_u64(1), trace: Trace::default() },
252    ///     compiled.build(),
253    /// );
254    /// assert_eq!(trace.total_log_weight(), f64::NEG_INFINITY);
255    /// let warnings = compiled.take_warnings();
256    /// assert!(warnings[0].starts_with("sample `s`"));
257    /// assert!(compiled.take_warnings().is_empty());
258    /// ```
259    pub fn take_warnings(&self) -> Vec<String> {
260        self.shared
261            .warnings
262            .lock()
263            .map(|mut w| std::mem::take(&mut *w))
264            .unwrap_or_default()
265    }
266}