├── Cargo.toml ├── ferric ├── src │ ├── core │ │ ├── mod.rs │ │ └── feoption.rs │ ├── distributions │ │ ├── mod.rs │ │ ├── distribution.rs │ │ ├── poisson.rs │ │ └── bernoulli.rs │ └── lib.rs ├── tests │ ├── ui.rs │ ├── ui │ │ ├── syntax_err_01.stderr │ │ ├── semantic_err_02.stderr │ │ ├── syntax_err_02.stderr │ │ ├── syntax_err_03.stderr │ │ ├── semantic_err_03.stderr │ │ ├── semantic_err_04.stderr │ │ ├── semantic_err_05.stderr │ │ ├── semantic_err_01.stderr │ │ ├── syntax_err_01.rs │ │ ├── syntax_err_03.rs │ │ ├── semantic_err_02.rs │ │ ├── semantic_err_03.rs │ │ ├── semantic_err_04.rs │ │ ├── semantic_err_05.rs │ │ ├── syntax_err_02.rs │ │ └── semantic_err_01.rs │ ├── checks.rs │ └── grass.rs ├── Cargo.toml └── examples │ └── grass.rs ├── .gitignore ├── LICENSE-APACHE ├── .vscode ├── coverage2 ├── coverage ├── tasks.json └── launch.json ├── ferric-macros ├── Cargo.toml └── src │ ├── lib.rs │ ├── parse.rs │ ├── analyze.rs │ └── codegen.rs ├── LICENSE-MIT ├── .github └── workflows │ └── ci.yml └── README.md /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "ferric-macros", 4 | "ferric", 5 | ] 6 | -------------------------------------------------------------------------------- /ferric/src/core/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | mod feoption; 3 | 4 | // Re-exports 5 | pub use self::feoption::FeOption; 6 | -------------------------------------------------------------------------------- /ferric/tests/ui.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use trybuild; 3 | 4 | #[test] 5 | fn ui() { 6 | let t = trybuild::TestCases::new(); 7 | t.compile_fail("tests/ui/*.rs"); 8 | t.pass("examples/*.rs"); 9 | } 10 | -------------------------------------------------------------------------------- /ferric/src/distributions/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | mod bernoulli; 3 | mod distribution; 4 | mod poisson; 5 | 6 | // Re-exports 7 | pub use self::bernoulli::Bernoulli; 8 | pub use self::distribution::Distribution; 9 | pub use self::poisson::Poisson; 10 | -------------------------------------------------------------------------------- /ferric/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | 3 | // re-export make_model from the ferric-macros crate 4 | pub use ferric_macros::make_model; 5 | 6 | // Public modules 7 | pub mod core; 8 | pub mod distributions; 9 | 10 | // re-export FeOption and its variants 11 | pub use self::core::FeOption; 12 | pub use FeOption::{Known, Null, Unknown}; 13 | -------------------------------------------------------------------------------- /ferric/tests/ui/syntax_err_01.stderr: -------------------------------------------------------------------------------- 1 | error: expected `mod` 2 | --> tests/ui/syntax_err_01.rs:5:5 3 | | 4 | 5 | modu grass; 5 | | ^^^^ 6 | 7 | error[E0433]: failed to resolve: use of undeclared crate or module `grass` 8 | --> tests/ui/syntax_err_01.rs:30:18 9 | | 10 | 30 | let _model = grass::Model { grass_wet: true }; 11 | | ^^^^^ use of undeclared crate or module `grass` 12 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_02.stderr: -------------------------------------------------------------------------------- 1 | error: duplicate query of variable `rain` 2 | --> tests/ui/semantic_err_02.rs:28:11 3 | | 4 | 28 | query rain; 5 | | ^^^^ 6 | 7 | error[E0433]: failed to resolve: use of undeclared crate or module `grass` 8 | --> tests/ui/semantic_err_02.rs:32:18 9 | | 10 | 32 | let _model = grass::Model { grass_wet: true }; 11 | | ^^^^^ use of undeclared crate or module `grass` 12 | -------------------------------------------------------------------------------- /ferric/tests/ui/syntax_err_02.stderr: -------------------------------------------------------------------------------- 1 | error: expected let | use | observe | query 2 | --> tests/ui/syntax_err_02.rs:8:5 3 | | 4 | 8 | + foo : bool ~ Bernoulli::new( 0.2 ); 5 | | ^ 6 | 7 | error[E0433]: failed to resolve: use of undeclared crate or module `grass` 8 | --> tests/ui/syntax_err_02.rs:32:18 9 | | 10 | 32 | let _model = grass::Model { grass_wet: true }; 11 | | ^^^^^ use of undeclared crate or module `grass` 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | .VSCodeCounter/ 13 | 14 | lcov.info 15 | coverage/ 16 | *.profraw 17 | 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /ferric/tests/ui/syntax_err_03.stderr: -------------------------------------------------------------------------------- 1 | error: expected let | use | observe | query 2 | --> tests/ui/syntax_err_03.rs:8:5 3 | | 4 | 8 | letu rain : bool ~ Bernoulli::new( 0.2 ); 5 | | ^^^^ 6 | 7 | error[E0433]: failed to resolve: use of undeclared crate or module `grass` 8 | --> tests/ui/syntax_err_03.rs:30:18 9 | | 10 | 30 | let _model = grass::Model { grass_wet: true }; 11 | | ^^^^^ use of undeclared crate or module `grass` 12 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_03.stderr: -------------------------------------------------------------------------------- 1 | error: undefined query variable `grass_wet2` 2 | --> tests/ui/semantic_err_03.rs:28:11 3 | | 4 | 28 | query grass_wet2; 5 | | ^^^^^^^^^^ 6 | 7 | error[E0433]: failed to resolve: use of undeclared crate or module `grass` 8 | --> tests/ui/semantic_err_03.rs:32:18 9 | | 10 | 32 | let _model = grass::Model { grass_wet: true }; 11 | | ^^^^^ use of undeclared crate or module `grass` 12 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_04.stderr: -------------------------------------------------------------------------------- 1 | error: undefined observed variable `grass_wet2` 2 | --> tests/ui/semantic_err_04.rs:28:13 3 | | 4 | 28 | observe grass_wet2; 5 | | ^^^^^^^^^^ 6 | 7 | error[E0433]: failed to resolve: use of undeclared crate or module `grass` 8 | --> tests/ui/semantic_err_04.rs:32:18 9 | | 10 | 32 | let _model = grass::Model { grass_wet: true }; 11 | | ^^^^^ use of undeclared crate or module `grass` 12 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_05.stderr: -------------------------------------------------------------------------------- 1 | error: duplicate observe of variable `grass_wet` 2 | --> tests/ui/semantic_err_05.rs:28:13 3 | | 4 | 28 | observe grass_wet; 5 | | ^^^^^^^^^ 6 | 7 | error[E0433]: failed to resolve: use of undeclared crate or module `grass` 8 | --> tests/ui/semantic_err_05.rs:32:18 9 | | 10 | 32 | let _model = grass::Model { grass_wet: true }; 11 | | ^^^^^ use of undeclared crate or module `grass` 12 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_01.stderr: -------------------------------------------------------------------------------- 1 | error: duplicate declaration of variable `sprinkler` 2 | --> tests/ui/semantic_err_01.rs:28:9 3 | | 4 | 28 | let sprinkler : bool ~ Bernoulli::new( 0.2 ); 5 | | ^^^^^^^^^ 6 | 7 | error[E0433]: failed to resolve: use of undeclared crate or module `grass` 8 | --> tests/ui/semantic_err_01.rs:32:18 9 | | 10 | 32 | let _model = grass::Model { grass_wet: true }; 11 | | ^^^^^ use of undeclared crate or module `grass` 12 | -------------------------------------------------------------------------------- /ferric/src/distributions/distribution.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | 3 | use rand::rngs::ThreadRng; 4 | use rand::Rng; 5 | 6 | /// A Distribution defined over a specific domain can be used to generate 7 | /// random values over that domain. 8 | /// For example, the Bernoulli distribution implements Distribution and 9 | /// it can be used to generate random booleans. 10 | pub trait Distribution 11 | where 12 | R: Rng + ?Sized, 13 | { 14 | type Domain; 15 | fn sample(&self, rng: &mut R) -> Self::Domain; 16 | } 17 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Copyright 2022 The Ferric AI Project Developers 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /.vscode/coverage2: -------------------------------------------------------------------------------- 1 | # source based coverage 2 | # cargo install grcov 3 | # rustup component add llvm-tools-preview 4 | export RUSTFLAGS="-Zinstrument-coverage" 5 | export LLVM_PROFILE_FILE="ferric-%p-%m.profraw" 6 | cargo clean 7 | rm lcov.info 8 | rm -f *.profraw 9 | rm -fr coverage 10 | cargo +nightly build --verbose $CARGO_OPTIONS 11 | cargo +nightly test --all-features --no-fail-fast --verbose $CARGO_OPTIONS -- --skip ui --skip check_formatting --skip check_clippy 12 | grcov . --llvm --binary-path ./target/debug -s . --ignore="/*" --ignore="ferric/examples/*" -t lcov --branch --ignore-not-existing > lcov.info 13 | genhtml -o coverage --branch-coverage --show-details --highlight --ignore-errors source --legend lcov.info 14 | cargo clean 15 | -------------------------------------------------------------------------------- /ferric-macros/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ferric-macros" 3 | version = "0.1.2" 4 | edition = "2021" 5 | authors = ["Nimar Arora "] 6 | license = "MIT OR Apache-2.0" 7 | description = "A Probablistic Programming Language with a declarative syntax for random variables." 8 | readme = "../README.md" 9 | repository = "https://github.com/ferric-ai/ferric" 10 | homepage = "https://ferric.ai" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | proc-macro2 = "1.0" 19 | quote = "1.0" 20 | syn = {features = ["full"], version="1.0"} 21 | rand = "0.8.0" 22 | 23 | [dev-dependencies] 24 | syn = {features = ["full", "extra-traits"], version="1.0"} 25 | -------------------------------------------------------------------------------- /ferric/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ferric" 3 | version = "0.1.2" 4 | edition = "2021" 5 | authors = ["Nimar Arora "] 6 | license = "MIT OR Apache-2.0" 7 | description = "A Probablistic Programming Language with a declarative syntax for random variables." 8 | readme = "../README.md" 9 | repository = "https://github.com/ferric-ai/ferric" 10 | homepage = "https://ferric.ai" 11 | keywords = ["probabilistic", "programming", "generative", "bayesian", "mcmc"] 12 | categories = ["mathematics", "science"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | ferric-macros = {path="../ferric-macros"} 18 | rand = "0.8" 19 | rand_distr = "0.4" 20 | 21 | [dev-dependencies] 22 | ferric = {path="../ferric"} 23 | trybuild = "1.0" 24 | -------------------------------------------------------------------------------- /ferric/tests/ui/syntax_err_01.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | 4 | make_model! { 5 | modu grass; 6 | use ferric::distributions::Bernoulli; 7 | 8 | let rain : bool ~ Bernoulli::new( 0.2 ); 9 | 10 | let sprinkler : bool ~ 11 | if rain { 12 | Bernoulli::new( 0.01 ) 13 | } else { 14 | Bernoulli::new( 0.4 ) 15 | }; 16 | 17 | let grass_wet : bool ~ Bernoulli::new( 18 | if sprinkler && rain { 0.99 } 19 | else if sprinkler && !rain { 0.9 } 20 | else if !sprinkler && rain { 0.8 } 21 | else { 0.0 } 22 | ); 23 | 24 | observe grass_wet; 25 | query rain; 26 | query sprinkler; 27 | } 28 | 29 | fn main() { 30 | let _model = grass::Model { grass_wet: true }; 31 | } 32 | -------------------------------------------------------------------------------- /ferric/tests/ui/syntax_err_03.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | 4 | make_model! { 5 | mod grass; 6 | use ferric::distributions::Bernoulli; 7 | 8 | letu rain : bool ~ Bernoulli::new( 0.2 ); 9 | 10 | let sprinkler : bool ~ 11 | if rain { 12 | Bernoulli::new( 0.01 ) 13 | } else { 14 | Bernoulli::new( 0.4 ) 15 | }; 16 | 17 | let grass_wet : bool ~ Bernoulli::new( 18 | if sprinkler && rain { 0.99 } 19 | else if sprinkler && !rain { 0.9 } 20 | else if !sprinkler && rain { 0.8 } 21 | else { 0.0 } 22 | ); 23 | 24 | observe grass_wet; 25 | query rain; 26 | query sprinkler; 27 | } 28 | 29 | fn main() { 30 | let _model = grass::Model { grass_wet: true }; 31 | } 32 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_02.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | 4 | make_model! { 5 | mod grass; 6 | use ferric::distributions::Bernoulli; 7 | 8 | let rain : bool ~ Bernoulli::new( 0.2 ); 9 | 10 | let sprinkler : bool ~ 11 | if rain { 12 | Bernoulli::new( 0.01 ) 13 | } else { 14 | Bernoulli::new( 0.4 ) 15 | }; 16 | 17 | let grass_wet : bool ~ Bernoulli::new( 18 | if sprinkler && rain { 0.99 } 19 | else if sprinkler && !rain { 0.9 } 20 | else if !sprinkler && rain { 0.8 } 21 | else { 0.0 } 22 | ); 23 | 24 | observe grass_wet; 25 | query rain; 26 | query sprinkler; 27 | 28 | query rain; 29 | } 30 | 31 | fn main() { 32 | let _model = grass::Model { grass_wet: true }; 33 | } 34 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_03.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | 4 | make_model! { 5 | mod grass; 6 | use ferric::distributions::Bernoulli; 7 | 8 | let rain : bool ~ Bernoulli::new( 0.2 ); 9 | 10 | let sprinkler : bool ~ 11 | if rain { 12 | Bernoulli::new( 0.01 ) 13 | } else { 14 | Bernoulli::new( 0.4 ) 15 | }; 16 | 17 | let grass_wet : bool ~ Bernoulli::new( 18 | if sprinkler && rain { 0.99 } 19 | else if sprinkler && !rain { 0.9 } 20 | else if !sprinkler && rain { 0.8 } 21 | else { 0.0 } 22 | ); 23 | 24 | observe grass_wet; 25 | query rain; 26 | query sprinkler; 27 | 28 | query grass_wet2; 29 | } 30 | 31 | fn main() { 32 | let _model = grass::Model { grass_wet: true }; 33 | } 34 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_04.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | 4 | make_model! { 5 | mod grass; 6 | use ferric::distributions::Bernoulli; 7 | 8 | let rain : bool ~ Bernoulli::new( 0.2 ); 9 | 10 | let sprinkler : bool ~ 11 | if rain { 12 | Bernoulli::new( 0.01 ) 13 | } else { 14 | Bernoulli::new( 0.4 ) 15 | }; 16 | 17 | let grass_wet : bool ~ Bernoulli::new( 18 | if sprinkler && rain { 0.99 } 19 | else if sprinkler && !rain { 0.9 } 20 | else if !sprinkler && rain { 0.8 } 21 | else { 0.0 } 22 | ); 23 | 24 | observe grass_wet; 25 | query rain; 26 | query sprinkler; 27 | 28 | observe grass_wet2; 29 | } 30 | 31 | fn main() { 32 | let _model = grass::Model { grass_wet: true }; 33 | } 34 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_05.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | 4 | make_model! { 5 | mod grass; 6 | use ferric::distributions::Bernoulli; 7 | 8 | let rain : bool ~ Bernoulli::new( 0.2 ); 9 | 10 | let sprinkler : bool ~ 11 | if rain { 12 | Bernoulli::new( 0.01 ) 13 | } else { 14 | Bernoulli::new( 0.4 ) 15 | }; 16 | 17 | let grass_wet : bool ~ Bernoulli::new( 18 | if sprinkler && rain { 0.99 } 19 | else if sprinkler && !rain { 0.9 } 20 | else if !sprinkler && rain { 0.8 } 21 | else { 0.0 } 22 | ); 23 | 24 | observe grass_wet; 25 | query rain; 26 | query sprinkler; 27 | 28 | observe grass_wet; 29 | } 30 | 31 | fn main() { 32 | let _model = grass::Model { grass_wet: true }; 33 | } 34 | -------------------------------------------------------------------------------- /ferric/tests/ui/syntax_err_02.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | 4 | make_model! { 5 | mod grass; 6 | use ferric::distributions::Bernoulli; 7 | 8 | + foo : bool ~ Bernoulli::new( 0.2 ); 9 | 10 | let rain : bool ~ Bernoulli::new( 0.2 ); 11 | 12 | let sprinkler : bool ~ 13 | if rain { 14 | Bernoulli::new( 0.01 ) 15 | } else { 16 | Bernoulli::new( 0.4 ) 17 | }; 18 | 19 | let grass_wet : bool ~ Bernoulli::new( 20 | if sprinkler && rain { 0.99 } 21 | else if sprinkler && !rain { 0.9 } 22 | else if !sprinkler && rain { 0.8 } 23 | else { 0.0 } 24 | ); 25 | 26 | observe grass_wet; 27 | query rain; 28 | query sprinkler; 29 | } 30 | 31 | fn main() { 32 | let _model = grass::Model { grass_wet: true }; 33 | } 34 | -------------------------------------------------------------------------------- /ferric/tests/ui/semantic_err_01.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | 4 | make_model! { 5 | mod grass; 6 | use ferric::distributions::Bernoulli; 7 | 8 | let rain : bool ~ Bernoulli::new( 0.2 ); 9 | 10 | let sprinkler : bool ~ 11 | if rain { 12 | Bernoulli::new( 0.01 ) 13 | } else { 14 | Bernoulli::new( 0.4 ) 15 | }; 16 | 17 | let grass_wet : bool ~ Bernoulli::new( 18 | if sprinkler && rain { 0.99 } 19 | else if sprinkler && !rain { 0.9 } 20 | else if !sprinkler && rain { 0.8 } 21 | else { 0.0 } 22 | ); 23 | 24 | observe grass_wet; 25 | query rain; 26 | query sprinkler; 27 | 28 | let sprinkler : bool ~ Bernoulli::new( 0.2 ); 29 | } 30 | 31 | fn main() { 32 | let _model = grass::Model { grass_wet: true }; 33 | } 34 | -------------------------------------------------------------------------------- /.vscode/coverage: -------------------------------------------------------------------------------- 1 | cargo clean 2 | rm lcov.info 3 | rm -fr coverage 4 | export CARGO_INCREMENTAL=0 5 | # removing "-Zpanic_abort_tests -Cpanic=abort" since they don't quite work 6 | export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off" 7 | export RUSTDOCFLAGS="-Cpanic=abort" 8 | # the following flag makes cargo build work with nightly mode and we don't need the `+nightly` flag any more 9 | export RUSTC_BOOTSTRAP=1 10 | cargo build 11 | cargo test --all-features --no-fail-fast 12 | # excluding coverage of all test files and unit test sections in files, also excluding panic! macros 13 | grcov . --binary-path ./target/debug -s . --ignore="/*" --ignore="ferric/examples/*" --ignore="ferric/tests/*" \ 14 | --excl-br-start "#\[test\]" --excl-start "#\[test\]" \ 15 | --excl-br-line "panic!" --excl-line "panic!" -t lcov --branch --ignore-not-existing > lcov.info 16 | genhtml -o coverage --branch-coverage --show-details --highlight --ignore-errors source --legend lcov.info 17 | cargo clean 18 | unset RUSTFLAGS 19 | unset RUSTDOCFLAGS 20 | unset RUSTC_BOOTSTRAP 21 | unset CARGO_INCREMENTAL 22 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 The Ferric AI Project Developers 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /ferric/tests/checks.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use std::process::Command; 3 | 4 | #[test] 5 | fn check_formatting() { 6 | let output = Command::new("cargo") 7 | .arg("fmt") 8 | .arg("--all") 9 | .arg("--") 10 | .arg("--check") 11 | .output() 12 | .expect("failed to check formatting"); 13 | 14 | println!("status: {}", output.status); 15 | println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); 16 | println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); 17 | 18 | assert!(output.status.success()); 19 | } 20 | 21 | #[test] 22 | fn check_clippy() { 23 | let output = Command::new("cargo") 24 | .arg("clippy") 25 | .arg("--workspace") 26 | .arg("--all-targets") 27 | .arg("--verbose") 28 | .output() 29 | .expect("failed to run clippy"); 30 | 31 | println!("status: {}", output.status); 32 | println!("stdout: {}", String::from_utf8_lossy(&output.stdout)); 33 | println!("stderr: {}", String::from_utf8_lossy(&output.stderr)); 34 | 35 | assert!(output.status.success()); 36 | } 37 | -------------------------------------------------------------------------------- /ferric-macros/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use proc_macro2::TokenStream; 3 | use syn::parse2; 4 | 5 | mod parse; 6 | use crate::parse::ModelAst; 7 | mod analyze; 8 | use crate::analyze::analyze; 9 | mod codegen; 10 | use crate::codegen::codegen; 11 | 12 | // 13 | // Parse statements such as the following 14 | // let rain ~ Bernoulli(0.2); 15 | // to build a Ferric model. 16 | // 17 | #[proc_macro] 18 | pub fn make_model(input: proc_macro::TokenStream) -> proc_macro::TokenStream { 19 | make_model_inner(TokenStream::from(input)).into() 20 | } 21 | 22 | fn make_model_inner(input: TokenStream) -> TokenStream { 23 | // parse the token stream into an AST (abstract syntax tree) 24 | let ast = parse2::(input); 25 | let ast = match ast { 26 | Ok(data) => data, 27 | Err(err) => { 28 | return err.to_compile_error(); 29 | } 30 | }; 31 | // analyze the AST and produce an IR (intermediate representation) 32 | let ir = analyze(ast); 33 | let ir = match ir { 34 | Ok(data) => data, 35 | Err(err) => { 36 | return err.to_compile_error(); 37 | } 38 | }; 39 | codegen(ir) 40 | } 41 | 42 | #[test] 43 | fn test_parse_error() { 44 | use quote::quote; 45 | make_model_inner(quote!(modu grass;)); 46 | } 47 | 48 | #[test] 49 | fn test_analyze_error() { 50 | use quote::quote; 51 | make_model_inner(quote!(mod grass; query rain;)); 52 | } 53 | -------------------------------------------------------------------------------- /ferric/tests/grass.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | 4 | // Test the simple Bayesian Network from Wikipedia: 5 | // https://en.wikipedia.org/wiki/Bayesian_network#Example 6 | // P( Rain | Grass Is Wet ) = .3577 7 | 8 | #[test] 9 | fn grass() { 10 | make_model! { 11 | mod grass; 12 | use ferric::distributions::Bernoulli; 13 | 14 | let rain : bool ~ Bernoulli::new( 0.2 ); 15 | 16 | let sprinkler : bool ~ 17 | if rain { 18 | Bernoulli::new( 0.01 ) 19 | } else { 20 | Bernoulli::new( 0.4 ) 21 | }; 22 | 23 | let grass_wet : bool ~ Bernoulli::new( 24 | if sprinkler && rain { 0.99 } 25 | else if sprinkler && !rain { 0.9 } 26 | else if !sprinkler && rain { 0.8 } 27 | else { 0.0 } 28 | ); 29 | 30 | query rain; 31 | observe grass_wet; 32 | }; 33 | 34 | let model = grass::Model { grass_wet: true }; 35 | let num_samples = 100000; 36 | let ans = 0.3577f64; 37 | let err = 5.0 * (ans * (1.0 - ans) / (num_samples as f64)).sqrt(); // 5 sigma error 38 | let post_rain = (model 39 | .sample_iter() 40 | .take(num_samples) 41 | .map(|x| x.rain as isize) 42 | .sum::() as f64) 43 | / (num_samples as f64); 44 | println!("post_rain is {}", post_rain); 45 | assert!(post_rain > (ans - err) && post_rain < (ans + err)); 46 | } 47 | -------------------------------------------------------------------------------- /ferric/examples/grass.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use ferric::make_model; 3 | use std::time::Instant; 4 | 5 | make_model! { 6 | mod grass; 7 | use ferric::distributions::Bernoulli; 8 | 9 | let rain : bool ~ Bernoulli::new( 0.2 ); 10 | 11 | let sprinkler : bool ~ 12 | if rain { 13 | Bernoulli::new( 0.01 ) 14 | } else { 15 | Bernoulli::new( 0.4 ) 16 | }; 17 | 18 | let grass_wet : bool ~ Bernoulli::new( 19 | if sprinkler && rain { 0.99 } 20 | else if sprinkler && !rain { 0.9 } 21 | else if !sprinkler && rain { 0.8 } 22 | else { 0.0 } 23 | ); 24 | 25 | observe grass_wet; 26 | query rain; 27 | query sprinkler; 28 | } 29 | 30 | fn main() { 31 | let model = grass::Model { grass_wet: true }; 32 | let mut num_rain = 0; 33 | let mut num_sprinkler = 0; 34 | let num_samples = 100000; 35 | let start = Instant::now(); 36 | for sample in model.sample_iter().take(num_samples) { 37 | if sample.rain { 38 | num_rain += 1; 39 | } 40 | if sample.sprinkler { 41 | num_sprinkler += 1; 42 | } 43 | } 44 | let num_samples = num_samples as f64; 45 | println!( 46 | "posterior: rain = {} sprinkler = {}. Elapsed {} millisec for {} samples", 47 | (num_rain as f64) / num_samples, 48 | (num_sprinkler as f64) / num_samples, 49 | start.elapsed().as_millis(), 50 | num_samples, 51 | ); 52 | } 53 | -------------------------------------------------------------------------------- /ferric/src/distributions/poisson.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use rand_distr::Distribution as Distribution2; 3 | use rand_distr::Poisson as Poisson2; 4 | 5 | use crate::distributions::Distribution; 6 | use rand::Rng; 7 | 8 | pub struct Poisson { 9 | /// Probability of success. 10 | rate: f64, 11 | } 12 | 13 | impl Poisson { 14 | pub fn new(rate: f64) -> Result { 15 | if rate <= 0f64 { 16 | Err(format! {"Poisson: illegal rate `{}` should be greater than 0", rate}) 17 | } else { 18 | Ok(Poisson { rate }) 19 | } 20 | } 21 | } 22 | 23 | impl Distribution for Poisson { 24 | type Domain = u64; 25 | fn sample(&self, rng: &mut R) -> u64 { 26 | Poisson2::new(self.rate).unwrap().sample(rng) as u64 27 | } 28 | } 29 | 30 | impl std::fmt::Display for Poisson { 31 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | write!(f, "Poisson {{ rate = {} }}", self.rate) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | use super::*; 39 | use rand::thread_rng; 40 | 41 | #[test] 42 | fn poisson_sample() { 43 | let mut rng = thread_rng(); 44 | let rate = 2.7f64; 45 | let dist = Poisson::new(rate).unwrap(); 46 | println!("dist = {}", dist); 47 | let mut total = 0u64; 48 | let trials = 10000; 49 | for _ in 0..trials { 50 | total += dist.sample(&mut rng); 51 | } 52 | let mean = (total as f64) / (trials as f64); 53 | let err = 5.0 * (rate / (trials as f64)).sqrt(); 54 | println!("empirical mean is {} 5sigma error is {}", mean, err); 55 | assert!((mean - 2.7).abs() < err); 56 | } 57 | 58 | #[test] 59 | #[should_panic] 60 | fn poisson_too_low() { 61 | let _dist = Poisson::new(-0.01).unwrap(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /ferric/src/distributions/bernoulli.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | 3 | use crate::distributions::Distribution; 4 | use rand::Rng; 5 | 6 | pub struct Bernoulli { 7 | /// Probability of success. 8 | p: f64, 9 | } 10 | 11 | impl Bernoulli { 12 | pub fn new(p: f64) -> Result { 13 | if !(0f64..=1f64).contains(&p) { 14 | Err(format! {"Bernoulli: illegal probability `{}` should be between 0 and 1", p}) 15 | } else { 16 | Ok(Bernoulli { p }) 17 | } 18 | } 19 | } 20 | 21 | impl Distribution for Bernoulli { 22 | type Domain = bool; 23 | fn sample(&self, rng: &mut R) -> bool { 24 | let val: f64 = rng.gen(); 25 | val < self.p 26 | } 27 | } 28 | 29 | impl std::fmt::Display for Bernoulli { 30 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 31 | write!(f, "Bernoulli {{ p = {} }}", self.p) 32 | } 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | use super::*; 38 | use rand::thread_rng; 39 | 40 | #[test] 41 | fn bernoulli_sample() { 42 | let mut rng = thread_rng(); 43 | let dist = Bernoulli::new(0.1).unwrap(); 44 | println!("dist = {}", dist); 45 | let mut succ = 0; 46 | let trials = 10000; 47 | for _ in 0..trials { 48 | if dist.sample(&mut rng) { 49 | succ += 1; 50 | } 51 | } 52 | let mean = (succ as f64) / (trials as f64); 53 | assert!((mean - 0.1).abs() < 0.01); 54 | // both of the following extreme distributios should be allowed 55 | let _dist2 = Bernoulli::new(0.0).unwrap(); 56 | let _dist3 = Bernoulli::new(1.0).unwrap(); 57 | } 58 | 59 | #[test] 60 | #[should_panic] 61 | fn bernoulli_too_low() { 62 | let _dist = Bernoulli::new(-0.01).unwrap(); 63 | } 64 | 65 | #[test] 66 | #[should_panic] 67 | fn bernoulli_too_high() { 68 | let _dist = Bernoulli::new(1.01).unwrap(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | test-versions: 7 | name: Test Ferric on Rust ${{ matrix.rust }} 8 | runs-on: ubuntu-latest 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | rust: [stable, beta] 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v2 16 | - name: Install toolchain 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | profile: minimal 20 | toolchain: ${{ matrix.rust }} 21 | override: true 22 | components: rustfmt, clippy 23 | - name: Run tests 24 | uses: actions-rs/cargo@v1 25 | with: 26 | command: test 27 | args: --all-features 28 | code-coverage: 29 | name: Generate Ferric Code Coverage on Rust nightly 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout code 33 | uses: actions/checkout@v2 34 | - name: Install toolchain 35 | uses: actions-rs/toolchain@v1 36 | with: 37 | profile: minimal 38 | toolchain: nightly 39 | override: true 40 | components: rustfmt, clippy, llvm-tools-preview 41 | - name: Run tests 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: test 45 | args: --all-features --no-fail-fast 46 | env: 47 | CARGO_INCREMENTAL: '0' 48 | RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 49 | RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off' 50 | - name: Download grcov v0.8.10 51 | run: | 52 | wget https://github.com/mozilla/grcov/releases/download/v0.8.10/grcov-x86_64-unknown-linux-gnu.tar.bz2 53 | tar xjf grcov-x86_64-unknown-linux-gnu.tar.bz2 54 | - name: Run grcov 55 | run: ./grcov . --binary-path ./target/debug -s . --ignore="/*" --ignore="ferric/examples/*" --ignore="ferric/tests/*" --excl-br-start "#\[test\]" --excl-start "#\[test\]" --excl-br-line "panic!" --excl-line "panic!" -t lcov --branch --ignore-not-existing > lcov.info 56 | - name: Coveralls upload 57 | uses: coverallsapp/github-action@master 58 | with: 59 | github-token: ${{ secrets.GITHUB_TOKEN }} 60 | path-to-lcov: ./lcov.info 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Github Actions Tests](https://github.com/ferric-ai/ferric/actions/workflows/ci.yml/badge.svg)](https://github.com/Ferric-AI/ferric/actions/workflows/ci.yml) 2 | [![crates.io](https://img.shields.io/crates/v/ferric.svg)](https://crates.io/crates/ferric) 3 | [![Coverage Status](https://coveralls.io/repos/github/Ferric-AI/ferric/badge.svg)](https://coveralls.io/github/Ferric-AI/ferric) 4 | 5 | # Ferric 6 | A Probabilistic Programming Language in Rust with a declarative syntax. 7 | 8 | ## Usage 9 | 10 | Add this to your `Cargo.toml`: 11 | 12 | ```toml 13 | [dependencies] 14 | ferric = "0.1" 15 | ``` 16 | 17 | ## Example 18 | 19 | ```rust 20 | use std::time::Instant; 21 | use ferric::make_model; 22 | 23 | make_model! { 24 | mod grass; 25 | use ferric::distributions::Bernoulli; 26 | 27 | let rain : bool ~ Bernoulli::new( 0.2 ); 28 | 29 | let sprinkler : bool ~ 30 | if rain { 31 | Bernoulli::new( 0.01 ) 32 | } else { 33 | Bernoulli::new( 0.4 ) 34 | }; 35 | 36 | let grass_wet : bool ~ Bernoulli::new( 37 | if sprinkler && rain { 0.99 } 38 | else if sprinkler && !rain { 0.9 } 39 | else if !sprinkler && rain { 0.8 } 40 | else { 0.0 } 41 | ); 42 | 43 | observe grass_wet; 44 | query rain; 45 | query sprinkler; 46 | } 47 | 48 | fn main() { 49 | let model = grass::Model {grass_wet: true}; 50 | let mut num_rain = 0; 51 | let mut num_sprinkler = 0; 52 | let num_samples = 100000; 53 | let start = Instant::now(); 54 | for sample in model.sample_iter().take(num_samples) { 55 | if sample.rain { 56 | num_rain += 1; 57 | } 58 | if sample.sprinkler { 59 | num_sprinkler += 1; 60 | } 61 | } 62 | let num_samples = num_samples as f64; 63 | println!( 64 | "posterior: rain = {} sprinkler = {}. Elapsed {} millisec for {} samples", 65 | (num_rain as f64) / num_samples, 66 | (num_sprinkler as f64) / num_samples, 67 | start.elapsed().as_millis(), 68 | num_samples, 69 | ); 70 | } 71 | ``` 72 | 73 | ## License 74 | 75 | Licensed under either of 76 | 77 | * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) 78 | * [MIT license](http://opensource.org/licenses/MIT) 79 | 80 | at your option. 81 | 82 | ### Contribution 83 | 84 | Unless you explicitly state otherwise, any contribution intentionally submitted 85 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 86 | dual licensed as above, without any additional terms or conditions. 87 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [{ 4 | "label": "cargo build", 5 | "type": "shell", 6 | "command": "cargo build", 7 | "args": [], 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | } 12 | }, 13 | { 14 | "label": "cargo fmt", 15 | "type": "shell", 16 | "command": "cargo fmt", 17 | "args": [], 18 | "group": { 19 | "kind": "build", 20 | "isDefault": true 21 | } 22 | }, 23 | { 24 | "label": "cargo test", 25 | "type": "shell", 26 | "command": "cargo test", 27 | "args": [], 28 | "group": { 29 | "kind": "build", 30 | "isDefault": true 31 | } 32 | }, 33 | { 34 | "label": "expand example", 35 | "type": "shell", 36 | "command": "cargo", 37 | "args": [ 38 | "expand", 39 | "--example", 40 | "${input:exampleName}", 41 | "--package", 42 | "ferric" 43 | ], 44 | "group": { 45 | "kind": "build", 46 | "isDefault": true 47 | } 48 | }, 49 | { 50 | "label": "run example", 51 | "type": "shell", 52 | "command": "cargo", 53 | "args": [ 54 | "run", 55 | "--release", 56 | "--example", 57 | "${input:exampleName}", 58 | "--package", 59 | "ferric" 60 | ], 61 | "group": { 62 | "kind": "build", 63 | "isDefault": true 64 | } 65 | }, 66 | { 67 | "label": "coverage", 68 | "type": "shell", 69 | "command": "${workspaceFolder}/.vscode/coverage", 70 | "group": { 71 | "kind": "build", 72 | "isDefault": true 73 | } 74 | }, 75 | { 76 | "label": "documentation", 77 | "type": "shell", 78 | "command": "cargo", 79 | "args": [ 80 | "doc", 81 | "--open", 82 | "--no-deps", 83 | ], 84 | "group": { 85 | "kind": "build", 86 | "isDefault": true 87 | } 88 | }, 89 | { 90 | "label": "run test", 91 | "type": "shell", 92 | "command": "cargo", 93 | "args": [ 94 | "test", 95 | "--test", 96 | "${input:testName}", 97 | "--package", 98 | "ferric" 99 | ], 100 | "group": { 101 | "kind": "build", 102 | "isDefault": true 103 | } 104 | }], 105 | "inputs": 106 | [ 107 | { 108 | "id": "exampleName", 109 | "description": "Example name without the file suffix", 110 | "default": "grass", 111 | "type": "promptString", 112 | }, 113 | { 114 | "id": "testName", 115 | "description": "Test name without the file suffix", 116 | "default": "grass", 117 | "type": "promptString", 118 | }, 119 | ], 120 | } 121 | -------------------------------------------------------------------------------- /ferric/src/core/feoption.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | //! Optional value of a Ferric random variable. 3 | //! 4 | //! Type [`FeOption`] represents an potential value for a random variable: 5 | //! every [`FeOption`] is either [`Known`] and contains a value, or [`Null`], or 6 | //! [`Unknown`]. Note that a [`Null`] value is technically a case where the value is known. 7 | //! 8 | #[derive(Copy, Clone)] 9 | pub enum FeOption { 10 | Null, 11 | Unknown, 12 | Known(T), 13 | } 14 | 15 | use FeOption::{Known, Null, Unknown}; 16 | 17 | impl FeOption { 18 | /// Returns `true` if the FeOption is a [`Null`] value. 19 | /// 20 | /// # Examples 21 | /// 22 | /// ``` 23 | /// # use ferric::*; 24 | /// let x: FeOption = Null; 25 | /// assert_eq!(x.is_null(), true); 26 | /// 27 | /// let x: FeOption = Known(2); 28 | /// assert_eq!(x.is_null(), false); 29 | /// 30 | /// let x: FeOption = Unknown; 31 | /// assert_eq!(x.is_null(), false); 32 | /// ``` 33 | #[inline] 34 | pub const fn is_null(&self) -> bool { 35 | matches!(*self, Null) 36 | } 37 | 38 | /// Returns `true` if the FeOption is an [`Unknown`] value. 39 | /// 40 | /// # Examples 41 | /// 42 | /// ``` 43 | /// # use ferric::*; 44 | /// let x: FeOption = Unknown; 45 | /// assert_eq!(x.is_unknown(), true); 46 | /// 47 | /// let x: FeOption = Known(2); 48 | /// assert_eq!(x.is_unknown(), false); 49 | /// 50 | /// let x: FeOption = Null; 51 | /// assert_eq!(x.is_unknown(), false); 52 | /// ``` 53 | #[inline] 54 | pub const fn is_unknown(&self) -> bool { 55 | matches!(*self, Unknown) 56 | } 57 | 58 | /// Returns `true` if the FeOption is a [`Known`] value. 59 | /// 60 | /// # Examples 61 | /// 62 | /// ``` 63 | /// # use ferric::*; 64 | /// let x: FeOption = Known(2); 65 | /// assert_eq!(x.is_known(), true); 66 | /// 67 | /// let x: FeOption = Null; 68 | /// assert_eq!(x.is_known(), false); 69 | /// 70 | /// let x: FeOption = Unknown; 71 | /// assert_eq!(x.is_known(), false); 72 | /// ``` 73 | #[inline] 74 | pub const fn is_known(&self) -> bool { 75 | matches!(*self, Known(_)) 76 | } 77 | 78 | /// Returns the contained [`Known`] value, consuming the `self` value. 79 | /// 80 | /// # Panics 81 | /// 82 | /// Panics if the self value equals [`Unknown`] or [`Null`]. 83 | /// 84 | /// # Examples 85 | /// 86 | /// ``` 87 | /// # use ferric::*; 88 | /// let x = Known("air"); 89 | /// assert_eq!(x.unwrap(), "air"); 90 | /// ``` 91 | /// 92 | /// ```should_panic 93 | /// # use ferric::*; 94 | /// let x: FeOption<&str> = Null; 95 | /// assert_eq!(x.unwrap(), "air"); // fails 96 | /// ``` 97 | /// 98 | /// ```should_panic 99 | /// # use ferric::*; 100 | /// let x: FeOption<&str> = Unknown; 101 | /// assert_eq!(x.unwrap(), "air"); // fails 102 | /// ``` 103 | #[inline] 104 | pub fn unwrap(self) -> T { 105 | match self { 106 | Known(val) => val, 107 | Null => panic!("called `FeOption::unwrap()` on a `Null` value"), 108 | Unknown => panic!("called `FeOption::unwrap()` on an `Unknown` value"), 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "lldb", 9 | "request": "launch", 10 | "name": "Debug example 'grass' package 'ferric-macros'", 11 | "cargo": { 12 | "args": [ 13 | "build", 14 | "--example=grass", 15 | "--package=ferric-macros" 16 | ], 17 | "filter": { 18 | "name": "grass", 19 | "kind": "example" 20 | } 21 | }, 22 | "args": [], 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | { 26 | "type": "lldb", 27 | "request": "launch", 28 | "name": "Debug unit tests in example 'grass' package 'ferric-macros'", 29 | "cargo": { 30 | "args": [ 31 | "test", 32 | "--no-run", 33 | "--example=grass", 34 | "--package=ferric-macros" 35 | ], 36 | "filter": { 37 | "name": "grass", 38 | "kind": "example" 39 | } 40 | }, 41 | "args": [], 42 | "cwd": "${workspaceFolder}" 43 | }, 44 | { 45 | "type": "lldb", 46 | "request": "launch", 47 | "name": "Debug integration test 'smoke' in package 'ferric-macros'", 48 | "cargo": { 49 | "args": [ 50 | "test", 51 | "--no-run", 52 | "--test=smoke", 53 | "--package=ferric-macros" 54 | ], 55 | "filter": { 56 | "name": "smoke", 57 | "kind": "test" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | }, 63 | { 64 | "type": "lldb", 65 | "request": "launch", 66 | "name": "Debug unit tests in library 'ferric'", 67 | "cargo": { 68 | "args": [ 69 | "test", 70 | "--no-run", 71 | "--lib", 72 | "--package=ferric" 73 | ], 74 | "filter": { 75 | "name": "ferric", 76 | "kind": "lib" 77 | } 78 | }, 79 | "args": [], 80 | "cwd": "${workspaceFolder}" 81 | }, 82 | { 83 | "type": "lldb", 84 | "request": "launch", 85 | "name": "Debug example 'grass' in package 'ferric'", 86 | "cargo": { 87 | "args": [ 88 | "build", 89 | "--example=grass", 90 | "--package=ferric" 91 | ], 92 | "filter": { 93 | "name": "grass", 94 | "kind": "example" 95 | } 96 | }, 97 | "args": [], 98 | "cwd": "${workspaceFolder}" 99 | }, 100 | { 101 | "type": "lldb", 102 | "request": "launch", 103 | "name": "Debug unit tests in example 'grass' package 'ferric'", 104 | "cargo": { 105 | "args": [ 106 | "test", 107 | "--no-run", 108 | "--example=grass", 109 | "--package=ferric" 110 | ], 111 | "filter": { 112 | "name": "grass", 113 | "kind": "example" 114 | } 115 | }, 116 | "args": [], 117 | "cwd": "${workspaceFolder}" 118 | }, 119 | { 120 | "type": "lldb", 121 | "request": "launch", 122 | "name": "Debug integration test 'grass' package 'ferric'", 123 | "cargo": { 124 | "args": [ 125 | "test", 126 | "--no-run", 127 | "--test=grass", 128 | "--package=ferric" 129 | ], 130 | "filter": { 131 | "name": "grass", 132 | "kind": "test" 133 | } 134 | }, 135 | "args": [], 136 | "cwd": "${workspaceFolder}" 137 | } 138 | ] 139 | } 140 | -------------------------------------------------------------------------------- /ferric-macros/src/parse.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use syn::parse::{Parse, ParseStream, Result}; 3 | use syn::{Error, Expr, Ident, Token}; 4 | 5 | /// StmtAst is the Abstract Syntax Tree representation of a single dependency statement. 6 | pub struct StmtAst { 7 | pub var_ident: Ident, 8 | pub type_ident: Ident, 9 | pub dependency: Expr, 10 | } 11 | 12 | /// ModelAst is the Abstract Syntax Tree representation of the model. 13 | /// This represents the output of the parse phase in a proc_macro pipeline. 14 | pub struct ModelAst { 15 | pub model_ident: Ident, 16 | pub use_exprs: Vec, 17 | pub stmts: Vec, 18 | pub queries: Vec, 19 | pub observes: Vec, 20 | } 21 | 22 | impl Parse for ModelAst { 23 | fn parse(input: ParseStream) -> Result { 24 | // mod model_name; 25 | input.parse::()?; 26 | let model_ident: Ident = input.parse()?; 27 | input.parse::()?; 28 | 29 | let mut stmts = Vec::::new(); 30 | let mut use_exprs = Vec::::new(); 31 | let mut queries = Vec::::new(); 32 | let mut observes = Vec::::new(); 33 | 34 | while !input.is_empty() { 35 | // let var_name ~ dep_expr; 36 | if input.peek(Token![let]) { 37 | input.parse::()?; 38 | let var_ident: Ident = input.parse()?; 39 | input.parse::()?; 40 | let type_ident: Ident = input.parse()?; 41 | input.parse::()?; 42 | let dependency: Expr = input.parse()?; 43 | input.parse::()?; 44 | stmts.push(StmtAst { 45 | var_ident, 46 | type_ident, 47 | dependency, 48 | }); 49 | } else if input.peek(Token![use]) { 50 | input.parse::()?; 51 | let use_expr: Expr = input.parse()?; 52 | input.parse::()?; 53 | use_exprs.push(use_expr); 54 | } else if input.peek(Ident) { 55 | let keyword: Ident = input.parse()?; 56 | match keyword.to_string().as_ref() { 57 | "observe" => { 58 | // observe var_name; 59 | let var_name: Ident = input.parse()?; 60 | input.parse::()?; 61 | observes.push(var_name); 62 | } 63 | "query" => { 64 | // query var_name; 65 | let var_name: Ident = input.parse()?; 66 | input.parse::()?; 67 | queries.push(var_name); 68 | } 69 | _ => { 70 | return Err(Error::new( 71 | keyword.span(), 72 | "expected let | use | observe | query", 73 | )); 74 | } 75 | } 76 | } else { 77 | return Err(input.error("expected let | use | observe | query")); 78 | } 79 | } 80 | Ok(ModelAst { 81 | model_ident, 82 | use_exprs, 83 | stmts, 84 | queries, 85 | observes, 86 | }) 87 | } 88 | } 89 | 90 | #[test] 91 | fn test_parse_errors() { 92 | use quote::quote; 93 | use syn::parse2; 94 | 95 | assert!(parse2::(quote!( 96 | modu grass; 97 | )) 98 | .is_err()); 99 | assert!(parse2::(quote!( 100 | mod grass; 101 | use ferric::distributions::Bernoulli; 102 | 103 | + foo : bool ~ Bernoulli::new( 0.2 ); 104 | )) 105 | .is_err()); 106 | assert!(parse2::(quote!( 107 | mod grass; 108 | use ferric::distributions::Bernoulli; 109 | 110 | letu rain : bool ~ Bernoulli::new( 0.2 ); 111 | )) 112 | .is_err()); 113 | } 114 | 115 | #[test] 116 | fn test_parse_output() { 117 | use quote::quote; 118 | use syn::{parse2, parse_quote}; 119 | 120 | let model_ast = parse2::(quote!( 121 | mod grass; 122 | use ferric::distributions::Bernoulli; 123 | 124 | let rain : bool ~ Bernoulli::new( 0.2 ); 125 | 126 | let sprinkler : bool ~ 127 | if rain { 128 | Bernoulli::new( 0.01 ) 129 | } else { 130 | Bernoulli::new( 0.4 ) 131 | }; 132 | 133 | let grass_wet : bool ~ Bernoulli::new( 134 | if sprinkler && rain { 0.99 } 135 | else if sprinkler && !rain { 0.9 } 136 | else if !sprinkler && rain { 0.8 } 137 | else { 0.0 } 138 | ); 139 | 140 | observe grass_wet; 141 | query rain; 142 | query sprinkler; 143 | )) 144 | .unwrap(); 145 | 146 | let exp_model_name: Ident = parse_quote!(grass); 147 | assert_eq!(model_ast.model_ident, exp_model_name); 148 | 149 | let exp_use_exprs: &Expr = &parse_quote!(ferric::distributions::Bernoulli); 150 | assert_eq!(model_ast.use_exprs[0], *exp_use_exprs); 151 | assert_eq!(model_ast.use_exprs.len(), 1); 152 | 153 | let exp_var_name: Ident = parse_quote!(rain); 154 | let exp_type_name: Ident = parse_quote!(bool); 155 | let exp_dependency: &Expr = &parse_quote!(Bernoulli::new(0.2)); 156 | assert_eq!(model_ast.stmts[0].var_ident, exp_var_name); 157 | assert_eq!(model_ast.stmts[0].type_ident, exp_type_name); 158 | assert_eq!(model_ast.stmts[0].dependency, *exp_dependency); 159 | assert_eq!(model_ast.stmts.len(), 3); 160 | 161 | let exp_queryies_0: Ident = parse_quote!(rain); 162 | let exp_queryies_1: Ident = parse_quote!(sprinkler); 163 | assert_eq!(model_ast.queries, [exp_queryies_0, exp_queryies_1]); 164 | 165 | let exp_observes_0: Ident = parse_quote!(grass_wet); 166 | assert_eq!(model_ast.observes, [exp_observes_0]); 167 | } 168 | -------------------------------------------------------------------------------- /ferric-macros/src/analyze.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use std::collections::HashMap; 3 | use syn::{Error, Expr, Ident}; 4 | 5 | use crate::parse::ModelAst; 6 | 7 | /// VariableIR is the Intermediate Representation of a random variable. 8 | pub struct VariableIR { 9 | pub var_ident: Ident, 10 | pub type_ident: Ident, 11 | pub dependency: Expr, 12 | pub is_queried: bool, 13 | pub is_observed: bool, 14 | } 15 | 16 | /// ModelIR is the Intermediate Representation of the model before code generation. 17 | /// This represents the output of the analyze phase in a proc_macro pipeline. 18 | pub struct ModelIR { 19 | pub model_ident: Ident, 20 | pub use_exprs: Vec, 21 | pub variables: HashMap, 22 | } 23 | 24 | pub fn analyze(ast: ModelAst) -> Result { 25 | let mut variables = HashMap::::new(); 26 | // analyze all the statements 27 | for stmt in ast.stmts.into_iter() { 28 | let var_name = stmt.var_ident.to_string(); 29 | // the variable shouldn't have been previously defined 30 | if variables.contains_key(&var_name) { 31 | return Err(Error::new( 32 | stmt.var_ident.span(), 33 | format!("duplicate declaration of variable `{}`", var_name), 34 | )); 35 | } 36 | let variable = VariableIR { 37 | var_ident: stmt.var_ident, 38 | dependency: stmt.dependency, 39 | type_ident: stmt.type_ident, 40 | is_queried: false, 41 | is_observed: false, 42 | }; 43 | variables.insert(var_name, variable); 44 | } 45 | // analyze the query statements 46 | for query in ast.queries.into_iter() { 47 | let var_name = query.to_string(); 48 | match variables.get_mut(&var_name) { 49 | None => { 50 | return Err(Error::new( 51 | query.span(), 52 | format!("undefined query variable `{}`", var_name), 53 | )) 54 | } 55 | Some(variable) => { 56 | if variable.is_queried { 57 | return Err(Error::new( 58 | query.span(), 59 | format!("duplicate query of variable `{}`", var_name), 60 | )); 61 | } else { 62 | variable.is_queried = true; 63 | } 64 | } 65 | } 66 | } 67 | // analyze the observe statements 68 | for obs in ast.observes.into_iter() { 69 | let var_name = obs.to_string(); 70 | match variables.get_mut(&var_name) { 71 | None => { 72 | return Err(Error::new( 73 | obs.span(), 74 | format!("undefined observed variable `{}`", var_name), 75 | )) 76 | } 77 | Some(variable) => { 78 | if variable.is_observed { 79 | return Err(Error::new( 80 | obs.span(), 81 | format!("duplicate observe of variable `{}`", var_name), 82 | )); 83 | } else { 84 | variable.is_observed = true; 85 | } 86 | } 87 | } 88 | } 89 | Ok(ModelIR { 90 | model_ident: ast.model_ident, 91 | use_exprs: ast.use_exprs, 92 | variables, 93 | }) 94 | } 95 | 96 | #[test] 97 | fn test_analyze_errors() { 98 | use quote::quote; 99 | use syn::parse2; 100 | 101 | // duplicate definition of variable 102 | assert!(analyze( 103 | parse2::(quote!( 104 | mod grass; 105 | use ferric::distributions::Bernoulli; 106 | let rain : bool ~ Bernoulli::new( 0.2 ); 107 | let rain : bool ~ Bernoulli::new( 0.2 ); 108 | )) 109 | .unwrap() 110 | ) 111 | .is_err()); 112 | 113 | // undefined query variable 114 | assert!(analyze( 115 | parse2::(quote!( 116 | mod grass; 117 | use ferric::distributions::Bernoulli; 118 | let rain : bool ~ Bernoulli::new( 0.2 ); 119 | query sprinkler; 120 | )) 121 | .unwrap() 122 | ) 123 | .is_err()); 124 | 125 | // duplicate query 126 | assert!(analyze( 127 | parse2::(quote!( 128 | mod grass; 129 | use ferric::distributions::Bernoulli; 130 | let rain : bool ~ Bernoulli::new( 0.2 ); 131 | query rain; 132 | query rain; 133 | )) 134 | .unwrap() 135 | ) 136 | .is_err()); 137 | 138 | // undefined observe 139 | assert!(analyze( 140 | parse2::(quote!( 141 | mod grass; 142 | use ferric::distributions::Bernoulli; 143 | let rain : bool ~ Bernoulli::new( 0.2 ); 144 | observe sprinkler; 145 | )) 146 | .unwrap() 147 | ) 148 | .is_err()); 149 | 150 | // duplicate observe 151 | assert!(analyze( 152 | parse2::(quote!( 153 | mod grass; 154 | use ferric::distributions::Bernoulli; 155 | let rain : bool ~ Bernoulli::new( 0.2 ); 156 | observe rain; 157 | observe rain; 158 | )) 159 | .unwrap() 160 | ) 161 | .is_err()); 162 | } 163 | 164 | #[test] 165 | fn test_analyze_output() { 166 | use quote::quote; 167 | use syn::{parse2, parse_quote}; 168 | 169 | let model_ast = parse2::(quote!( 170 | mod grass; 171 | use ferric::distributions::Bernoulli; 172 | 173 | let rain : bool ~ Bernoulli::new( 0.2 ); 174 | 175 | let sprinkler : bool ~ 176 | if rain { 177 | Bernoulli::new( 0.01 ) 178 | } else { 179 | Bernoulli::new( 0.4 ) 180 | }; 181 | 182 | let grass_wet : bool ~ Bernoulli::new( 183 | if sprinkler && rain { 0.99 } 184 | else if sprinkler && !rain { 0.9 } 185 | else if !sprinkler && rain { 0.8 } 186 | else { 0.0 } 187 | ); 188 | 189 | observe grass_wet; 190 | query rain; 191 | query sprinkler; 192 | )) 193 | .unwrap(); 194 | 195 | let model_ir = analyze(model_ast).unwrap(); 196 | 197 | let exp_model_name: Ident = parse_quote!(grass); 198 | assert_eq!(model_ir.model_ident, exp_model_name); 199 | 200 | let exp_use_exprs: &Expr = &parse_quote!(ferric::distributions::Bernoulli); 201 | assert_eq!(model_ir.use_exprs[0], *exp_use_exprs); 202 | assert_eq!(model_ir.use_exprs.len(), 1); 203 | 204 | let var = model_ir.variables.get(&String::from("rain")).unwrap(); 205 | let exp_var_name: Ident = parse_quote!(rain); 206 | let exp_type_name: Ident = parse_quote!(bool); 207 | let exp_dependency: &Expr = &parse_quote!(Bernoulli::new(0.2)); 208 | assert_eq!(var.var_ident, exp_var_name); 209 | assert_eq!(var.type_ident, exp_type_name); 210 | assert_eq!(var.dependency, *exp_dependency); 211 | assert!(var.is_queried); 212 | assert!(!var.is_observed); 213 | } 214 | -------------------------------------------------------------------------------- /ferric-macros/src/codegen.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2022 The Ferric AI Project Developers 2 | use proc_macro2::{Delimiter, Group, TokenStream, TokenTree}; 3 | use quote::{format_ident, quote}; 4 | use std::collections::HashMap; 5 | use syn::Ident; 6 | 7 | use crate::analyze::{ModelIR, VariableIR}; 8 | 9 | pub fn codegen(ir: ModelIR) -> TokenStream { 10 | let model_ident = ir.model_ident; 11 | 12 | // process all the use statements; 13 | let mut use_stmts = Vec::::new(); 14 | for use_expr in ir.use_exprs.iter() { 15 | let use_stmt = quote! {use #use_expr}; 16 | use_stmts.push(use_stmt); 17 | } 18 | // additional use statements needed for code generation 19 | use_stmts.push(quote! {use ferric::FeOption}); 20 | 21 | // for all let statements 22 | let mut var_idents = Vec::::new(); // var_ 23 | let mut var_type_idents = Vec::::new(); // 24 | let mut var_eval_idents = Vec::::new(); // eval_ 25 | let mut var_eval_dist_idents = Vec::::new(); // evaldist_ 26 | let mut eval_dist_exprs = Vec::::new(); // expression for evaldist_ 27 | 28 | // for all query statements 29 | let mut query_idents = Vec::::new(); // 30 | let mut query_type_idents = Vec::::new(); // 31 | let mut query_eval_var_idents = Vec::::new(); // eval_ 32 | 33 | // for all observe statements 34 | let mut obs_idents = Vec::::new(); // 35 | let mut obs_var_idents = Vec::::new(); // var_ 36 | let mut obs_type_idents = Vec::::new(); // 37 | let mut obs_obs_idents = Vec::::new(); // obs_ 38 | let mut obs_eval_idents = Vec::::new(); // eval_ 39 | 40 | // process all the variables in the model 41 | for variable in ir.variables.values() { 42 | let var_ident = format_ident!("var_{}", &variable.var_ident); 43 | var_idents.push(var_ident.clone()); 44 | var_type_idents.push(variable.type_ident.clone()); 45 | let eval_var = format_ident!("eval_{}", &variable.var_ident); 46 | var_eval_idents.push(eval_var.clone()); 47 | let eval_dist_var = format_ident!("evaldist_{}", &variable.var_ident); 48 | var_eval_dist_idents.push(eval_dist_var.clone()); 49 | let eval_dist_dep = &variable.dependency; 50 | let modified_eval_dist_dep = replace(quote! {#eval_dist_dep}, &ir.variables); 51 | eval_dist_exprs.push(modified_eval_dist_dep); 52 | // if this variable is being queried then we need to store these 53 | // generated tokens in query-specific lists 54 | if variable.is_queried { 55 | query_idents.push(variable.var_ident.clone()); 56 | query_type_idents.push(variable.type_ident.clone()); 57 | query_eval_var_idents.push(eval_var.clone()); 58 | } 59 | if variable.is_observed { 60 | obs_idents.push(variable.var_ident.clone()); 61 | obs_type_idents.push(variable.type_ident.clone()); 62 | obs_var_idents.push(var_ident.clone()); 63 | obs_obs_idents.push(format_ident!("obs_{}", &variable.var_ident)); 64 | obs_eval_idents.push(eval_var.clone()); 65 | } 66 | } 67 | 68 | quote! { 69 | pub mod #model_ident { 70 | #( 71 | #use_stmts; 72 | )* 73 | 74 | // all the queried variables are fields in the `Sample` struct 75 | pub struct Sample { 76 | #( 77 | pub #query_idents: #query_type_idents, 78 | )* 79 | } 80 | 81 | // all the observed variables are fields in the `Model` struct 82 | pub struct Model { 83 | #( 84 | pub #obs_idents: #obs_type_idents, 85 | )* 86 | } 87 | 88 | impl Model { 89 | pub fn sample_iter(&self) -> World { 90 | World::new( 91 | rand::thread_rng(), 92 | #( 93 | self.#obs_idents, 94 | )* 95 | ) 96 | } 97 | } 98 | 99 | pub struct World { 100 | rng: R, 101 | #(#var_idents: FeOption<#var_type_idents>, )* 102 | #(#obs_obs_idents: #obs_type_idents, )* 103 | } 104 | 105 | impl Iterator for World { 106 | type Item = Sample; 107 | 108 | fn next(&mut self) -> Option { 109 | Some(self.sample()) 110 | } 111 | } 112 | 113 | impl World { 114 | pub fn new(rng: R, #(#obs_idents: #obs_type_idents,)*) -> World { 115 | World { 116 | rng: rng, 117 | #(#var_idents: FeOption::Unknown, )* 118 | #(#obs_obs_idents: #obs_idents, )* 119 | } 120 | } 121 | 122 | pub fn reset(&mut self) { 123 | #( 124 | self.#var_idents = FeOption::Unknown; 125 | )* 126 | } 127 | 128 | pub fn sample(&mut self) -> Sample { 129 | loop { 130 | self.reset(); 131 | #( 132 | if self.#obs_obs_idents != self.#obs_eval_idents() { 133 | continue; 134 | } 135 | )* 136 | return Sample { 137 | #( 138 | #query_idents: self.#query_eval_var_idents(), 139 | )* 140 | }; 141 | } 142 | } 143 | 144 | #( 145 | pub fn #var_eval_idents(&mut self) -> #var_type_idents { 146 | if self.#var_idents.is_unknown() { 147 | let dist = self.#var_eval_dist_idents(); 148 | self.#var_idents = FeOption::Known(dist.sample(&mut self.rng)); 149 | } 150 | // TODO: handle the case when the value is Null 151 | self.#var_idents.unwrap() 152 | } 153 | 154 | pub fn #var_eval_dist_idents(&mut self) -> Box> { 155 | // TODO: handle errors in constructing the distribution object 156 | let dist = #eval_dist_exprs; 157 | Box::new(dist.unwrap()) 158 | } 159 | )* 160 | } 161 | } 162 | } 163 | } 164 | 165 | // replace all occurrences of `var_name` in the dependency expression with `self.eval_var_name()` 166 | fn replace(dep_tokens: TokenStream, variables: &HashMap) -> TokenStream { 167 | let dep_it = dep_tokens.into_iter(); 168 | 169 | dep_it 170 | .map(|tt| match tt { 171 | TokenTree::Ident(ref i) => { 172 | let i_name = &i.to_string(); 173 | if variables.contains_key(i_name) { 174 | let var_ident = Ident::new(&format!("eval_{}", i_name), i.span()); 175 | let stream = quote! {self.#var_ident()}; 176 | let mut group = Group::new(Delimiter::None, stream); 177 | group.set_span(i.span()); 178 | TokenTree::Group(group) 179 | } else { 180 | tt 181 | } 182 | } 183 | TokenTree::Group(ref g) => { 184 | let stream = replace(g.stream(), variables); 185 | let mut group = Group::new(g.delimiter(), stream); 186 | group.set_span(g.span()); 187 | TokenTree::Group(group) 188 | } 189 | other => other, 190 | }) 191 | .collect() 192 | } 193 | 194 | #[test] 195 | fn output_is_module_item() { 196 | use proc_macro2::Span; 197 | use syn::{parse2, parse_quote, ItemMod}; 198 | let ir = ModelIR { 199 | model_ident: Ident::new(&String::from("grass"), Span::call_site()), 200 | use_exprs: vec![parse_quote!(ferric::distributions::Bernoulli)], 201 | variables: HashMap::from([ 202 | ( 203 | String::from("rain"), 204 | VariableIR { 205 | var_ident: Ident::new(&String::from("rain"), Span::call_site()), 206 | type_ident: Ident::new(&String::from("bool"), Span::call_site()), 207 | dependency: parse_quote!(Bernoulli::new(0.2)), 208 | is_queried: true, 209 | is_observed: false, 210 | }, 211 | ), 212 | ( 213 | String::from("sprinkler"), 214 | VariableIR { 215 | var_ident: Ident::new(&String::from("sprinkler"), Span::call_site()), 216 | type_ident: Ident::new(&String::from("bool"), Span::call_site()), 217 | dependency: parse_quote!(if rain { 218 | Bernoulli::new(0.01) 219 | } else { 220 | Bernoulli::new(0.4) 221 | }), 222 | is_queried: false, 223 | is_observed: true, 224 | }, 225 | ), 226 | ]), 227 | }; 228 | let rust = codegen(ir); 229 | 230 | assert!(parse2::(rust).is_ok()); 231 | } 232 | --------------------------------------------------------------------------------