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(®istry, &Data::new()).is_err()); // unknown `y`
115 /// let compiled = program
116 /// .compile(®istry, &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}