diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5be269fef5..62f3786f82 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: [ master ] pull_request: - + jobs: rustfmt: name: Check formatting @@ -56,11 +56,18 @@ jobs: strategy: matrix: rust: [beta, nightly] + features: [[], ['polonius-souffle']] steps: - uses: actions/checkout@v2 with: fetch-depth: 1 + - name: Install Souffle + if: contains(matrix.features, 'polonius-souffle') + run: | + wget https://github.com/ecstatic-morse/souffle/releases/download/2.0.2-alpha/souffle-2.0.2-Linux.deb + sudo apt install ./souffle-2.0.2-Linux.deb + - name: Install rust toolchain uses: actions-rs/toolchain@v1 with: @@ -69,7 +76,11 @@ jobs: override: true - name: Build polonius - run: cargo build + run: cargo build --features "${{ join(matrix.features) }}" + + - name: Execute tests for all crates in the workspace (except `polonius-souffle`) + run: cargo test --all --exclude polonius-souffle --features "${{ join(matrix.features) }}" - - name: Execute tests for all crates in the workspace - run: cargo test --all + - name: Execute tests for `polonius-souffle` + if: contains(matrix.features, 'polonius-souffle') + run: cargo test -p polonius-souffle diff --git a/Cargo.lock b/Cargo.lock index 7a380e2ad8..9535829f59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,72 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "cc" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "cxx" +version = "1.0.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8431240a7c50efe7610cbddd167ea5737a053947940fc815f72cd2864c0af36b" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6843a1e1fa9cffc5ba1780c4aed9561f20b86706e6a0e380e9fd066c0164f2c" +dependencies = [ + "cc", + "codespan-reporting", + "lazy_static", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1396df99aac70a2b73cc0f1bb333adb83d71dfa17cfa31a5467dc933071c5ef0" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ac15d8552d84daf53d8a195bf45f2ca529d099465228146eb59e75bbbfdde5c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "datafrog" version = "2.0.1" @@ -20,12 +80,54 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "fixedbitset" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86d4de0081402f5e88cdac65c8dcdcc73118c1a7a465e2a05f0da05843a8ea33" +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "is_executable" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8" +dependencies = [ + "winapi", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2a5ac8f984bfcf3a823267e5fde638acc3325f6496633a5da6bb6eb2171e103" + +[[package]] +name = "link-cplusplus" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1becd27d473556dc610b8afa1636ef90747b574a84553bc11e82371d5ef2d1" +dependencies = [ + "cc", +] + [[package]] name = "log" version = "0.4.14" @@ -67,6 +169,7 @@ dependencies = [ "pico-args", "polonius-engine", "polonius-parser", + "polonius-souffle", "rustc-hash", ] @@ -76,15 +179,133 @@ version = "0.13.0" dependencies = [ "datafrog", "log", + "polonius-facts", + "polonius-souffle", "rustc-hash", ] +[[package]] +name = "polonius-facts" +version = "0.1.0" + [[package]] name = "polonius-parser" version = "0.5.0" +[[package]] +name = "polonius-souffle" +version = "0.1.0" +dependencies = [ + "cc", + "cxx", + "cxx-build", + "glob", + "is_executable", + "log", + "polonius-facts", + "which", +] + +[[package]] +name = "proc-macro2" +version = "1.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +dependencies = [ + "proc-macro2", +] + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "scratch" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e114536316b51a5aa7a0e59fc49661fd263c5507dd08bd28de052e57626ce69" + +[[package]] +name = "syn" +version = "1.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "which" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9" +dependencies = [ + "either", + "lazy_static", + "libc", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml index a5123f12c9..ab8508aa82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,20 +4,22 @@ version = "0.7.0" authors = ["The Rust Project Developers", "Polonius Developers"] description = "Core definition for the Rust borrow checker" license = "Apache-2.0/MIT" -repository = "https://github.com/rust-lang/polonius" +repository = "https://github.com/rust-lang-nursery/polonius" readme = "README.md" keywords = ["compiler", "borrowck", "datalog"] edition = "2018" +resolver = "2" [dev-dependencies] diff = "0.1.0" polonius-parser = { path = "./polonius-parser" } [dependencies] -rustc-hash = "1.0.0" -polonius-engine = { path = "./polonius-engine" } -log = "0.4" -petgraph = "0.4.13" -pico-args = "0.2" +rustc-hash = "1.0.0" +polonius-engine = { path = "polonius-engine" } +polonius-souffle = { path = "polonius-souffle", optional = true } +log = "0.4" +petgraph = "0.4.13" +pico-args = "0.2" [workspace] diff --git a/polonius-engine/Cargo.toml b/polonius-engine/Cargo.toml index a975e91b9c..0ee4ae2d99 100644 --- a/polonius-engine/Cargo.toml +++ b/polonius-engine/Cargo.toml @@ -7,8 +7,11 @@ license = "Apache-2.0/MIT" repository = "https://github.com/rust-lang-nursery/polonius" readme = "README.md" keywords = ["compiler", "borrowck", "datalog"] +resolver = "2" [dependencies] datafrog = "2.0.0" +polonius-facts = { path = "../polonius-facts" } +polonius-souffle = { path = "../polonius-souffle", optional = true } rustc-hash = "1.0.0" log = "0.4" diff --git a/polonius-engine/src/facts.rs b/polonius-engine/src/facts.rs index 442ba186cc..e217e2f26d 100644 --- a/polonius-engine/src/facts.rs +++ b/polonius-engine/src/facts.rs @@ -1,129 +1 @@ -use std::fmt::Debug; -use std::hash::Hash; - -/// The "facts" which are the basis of the NLL borrow analysis. -#[derive(Clone, Debug)] -pub struct AllFacts { - /// `loan_issued_at(origin, loan, point)` indicates that the `loan` was "issued" - /// at the given `point`, creating a reference with the `origin`. - /// Effectively, `origin` may refer to data from `loan` starting at `point` (this is usually - /// the point *after* a borrow rvalue). - pub loan_issued_at: Vec<(T::Origin, T::Loan, T::Point)>, - - /// `universal_region(origin)` -- this is a "free region" within fn body - pub universal_region: Vec, - - /// `cfg_edge(point1, point2)` for each edge `point1 -> point2` in the control flow - pub cfg_edge: Vec<(T::Point, T::Point)>, - - /// `loan_killed_at(loan, point)` when some prefix of the path borrowed at `loan` - /// is assigned at `point`. - /// Indicates that the path borrowed by the `loan` has changed in some way that the loan no - /// longer needs to be tracked. (In particular, mutations to the path that was borrowed - /// no longer invalidate the loan) - pub loan_killed_at: Vec<(T::Loan, T::Point)>, - - /// `subset_base(origin1, origin2, point)` when we require `origin1@point: origin2@point`. - /// Indicates that `origin1 <= origin2` -- i.e., the set of loans in `origin1` are a subset - /// of those in `origin2`. - pub subset_base: Vec<(T::Origin, T::Origin, T::Point)>, - - /// `loan_invalidated_at(point, loan)` indicates that the `loan` is invalidated by some action - /// taking place at `point`; if any origin that references this loan is live, this is an error. - pub loan_invalidated_at: Vec<(T::Point, T::Loan)>, - - /// `var_used_at(var, point)` when the variable `var` is used for anything - /// but a drop at `point` - pub var_used_at: Vec<(T::Variable, T::Point)>, - - /// `var_defined_at(var, point)` when the variable `var` is overwritten at `point` - pub var_defined_at: Vec<(T::Variable, T::Point)>, - - /// `var_dropped_at(var, point)` when the variable `var` is used in a drop at `point` - pub var_dropped_at: Vec<(T::Variable, T::Point)>, - - /// `use_of_var_derefs_origin(variable, origin)`: References with the given - /// `origin` may be dereferenced when the `variable` is used. - /// - /// In rustc, we generate this whenever the type of the variable includes the - /// given origin. - pub use_of_var_derefs_origin: Vec<(T::Variable, T::Origin)>, - - /// `drop_of_var_derefs_origin(var, origin)` when the type of `var` includes - /// the `origin` and uses it when dropping - pub drop_of_var_derefs_origin: Vec<(T::Variable, T::Origin)>, - - /// `child_path(child, parent)` when the path `child` is the direct child of - /// `parent`, e.g. `child_path(x.y, x)`, but not `child_path(x.y.z, x)`. - pub child_path: Vec<(T::Path, T::Path)>, - - /// `path_is_var(path, var)` the root path `path` starting in variable `var`. - pub path_is_var: Vec<(T::Path, T::Variable)>, - - /// `path_assigned_at_base(path, point)` when the `path` was initialized at point - /// `point`. This fact is only emitted for a prefix `path`, and not for the - /// implicit initialization of all of `path`'s children. E.g. a statement like - /// `x.y = 3` at `point` would give the fact `path_assigned_at_base(x.y, point)` (but - /// neither `path_assigned_at_base(x.y.z, point)` nor `path_assigned_at_base(x, point)`). - pub path_assigned_at_base: Vec<(T::Path, T::Point)>, - - /// `path_moved_at_base(path, point)` when the `path` was moved at `point`. The - /// same logic is applied as for `path_assigned_at_base` above. - pub path_moved_at_base: Vec<(T::Path, T::Point)>, - - /// `path_accessed_at_base(path, point)` when the `path` was accessed at point - /// `point`. The same logic as for `path_assigned_at_base` and `path_moved_at_base` applies. - pub path_accessed_at_base: Vec<(T::Path, T::Point)>, - - /// These reflect the `'a: 'b` relations that are either declared by the user on function - /// declarations or which are inferred via implied bounds. - /// For example: `fn foo<'a, 'b: 'a, 'c>(x: &'c &'a u32)` would have two entries: - /// - one for the user-supplied subset `'b: 'a` - /// - and one for the `'a: 'c` implied bound from the `x` parameter, - /// (note that the transitive relation `'b: 'c` is not necessarily included - /// explicitly, but rather inferred by polonius). - pub known_placeholder_subset: Vec<(T::Origin, T::Origin)>, - - /// `placeholder(origin, loan)` describes a placeholder `origin`, with its associated - /// placeholder `loan`. - pub placeholder: Vec<(T::Origin, T::Loan)>, -} - -impl Default for AllFacts { - fn default() -> Self { - AllFacts { - loan_issued_at: Vec::default(), - universal_region: Vec::default(), - cfg_edge: Vec::default(), - loan_killed_at: Vec::default(), - subset_base: Vec::default(), - loan_invalidated_at: Vec::default(), - var_used_at: Vec::default(), - var_defined_at: Vec::default(), - var_dropped_at: Vec::default(), - use_of_var_derefs_origin: Vec::default(), - drop_of_var_derefs_origin: Vec::default(), - child_path: Vec::default(), - path_is_var: Vec::default(), - path_assigned_at_base: Vec::default(), - path_moved_at_base: Vec::default(), - path_accessed_at_base: Vec::default(), - known_placeholder_subset: Vec::default(), - placeholder: Vec::default(), - } - } -} - -pub trait Atom: - From + Into + Copy + Clone + Debug + Eq + Ord + Hash + 'static -{ - fn index(self) -> usize; -} - -pub trait FactTypes: Copy + Clone + Debug { - type Origin: Atom; - type Loan: Atom; - type Point: Atom; - type Variable: Atom; - type Path: Atom; -} +pub use polonius_facts::*; diff --git a/polonius-engine/src/lib.rs b/polonius-engine/src/lib.rs index 0926be8f59..cd6e9b91f6 100644 --- a/polonius-engine/src/lib.rs +++ b/polonius-engine/src/lib.rs @@ -3,6 +3,7 @@ extern crate datafrog; #[macro_use] extern crate log; +extern crate polonius_facts; extern crate rustc_hash; mod facts; @@ -13,4 +14,5 @@ pub use facts::AllFacts; pub use facts::Atom; pub use facts::FactTypes; pub use output::Algorithm; +pub use output::Engine; pub use output::Output; diff --git a/polonius-engine/src/output/liveness.rs b/polonius-engine/src/output/liveness.rs index 1b4b4ce53f..6b5a96a775 100644 --- a/polonius-engine/src/output/liveness.rs +++ b/polonius-engine/src/output/liveness.rs @@ -157,12 +157,12 @@ pub(super) fn compute_live_origins( pub(super) fn make_universal_regions_live( origin_live_on_entry: &mut Vec<(T::Origin, T::Point)>, cfg_node: &BTreeSet, - universal_regions: &[T::Origin], + universal_regions: &[(T::Origin,)], ) { debug!("make_universal_regions_live()"); origin_live_on_entry.reserve(universal_regions.len() * cfg_node.len()); - for &origin in universal_regions.iter() { + for &(origin,) in universal_regions.iter() { for &point in cfg_node.iter() { origin_live_on_entry.push((origin, point)); } diff --git a/polonius-engine/src/output/mod.rs b/polonius-engine/src/output/mod.rs index b840e4bec8..9d03573d5f 100644 --- a/polonius-engine/src/output/mod.rs +++ b/polonius-engine/src/output/mod.rs @@ -12,6 +12,7 @@ use datafrog::Relation; use rustc_hash::{FxHashMap, FxHashSet}; use std::borrow::Cow; use std::collections::{BTreeMap, BTreeSet}; +use std::convert::TryInto; use crate::facts::{AllFacts, Atom, FactTypes}; @@ -21,13 +22,19 @@ mod liveness; mod location_insensitive; mod naive; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Engine { + Souffle, + Datafrog, +} + #[derive(Debug, Clone, Copy)] pub enum Algorithm { /// Simple rules, but slower to execute - Naive, + Naive(Engine), /// Optimized variant of the rules - DatafrogOpt, + DatafrogOpt(Engine), /// Fast to compute, but imprecise: there can be false-positives /// but no false-negatives. Tailored for quick "early return" situations. @@ -42,33 +49,57 @@ pub enum Algorithm { Hybrid, } +impl Default for Algorithm { + fn default() -> Self { + Self::Naive(Engine::Datafrog) + } +} + impl Algorithm { /// Optimized variants that ought to be equivalent to "naive" - pub const OPTIMIZED: &'static [Algorithm] = &[Algorithm::DatafrogOpt]; + pub const OPTIMIZED: &'static [Algorithm] = &[Algorithm::DatafrogOpt(Engine::Datafrog)]; pub fn variants() -> [&'static str; 5] { [ - "Naive", - "DatafrogOpt", + "Naive(-Souffle)", + "DatafrogOpt(-Souffle)", "LocationInsensitive", "Compare", "Hybrid", ] } + + pub fn engine(&self) -> Engine { + match *self { + Self::Naive(eng) | Self::DatafrogOpt(eng) => eng, + Self::LocationInsensitive | Self::Compare | Self::Hybrid => Engine::Datafrog, + } + } + + pub fn souffle_name(&self) -> Option<&'static str> { + let ret = match self { + Self::Naive(Engine::Souffle) => "naive", + Self::DatafrogOpt(Engine::Souffle) => "opt", + + _ => return None, + }; + + Some(ret) + } } impl ::std::str::FromStr for Algorithm { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_ref() { - "naive" => Ok(Algorithm::Naive), - "datafrogopt" => Ok(Algorithm::DatafrogOpt), + "naive" => Ok(Algorithm::Naive(Engine::Datafrog)), + "naive-souffle" => Ok(Algorithm::Naive(Engine::Souffle)), + "datafrogopt" => Ok(Algorithm::DatafrogOpt(Engine::Datafrog)), + "datafrogopt-souffle" => Ok(Algorithm::DatafrogOpt(Engine::Souffle)), "locationinsensitive" => Ok(Algorithm::LocationInsensitive), "compare" => Ok(Algorithm::Compare), "hybrid" => Ok(Algorithm::Hybrid), - _ => Err(String::from( - "valid values: Naive, DatafrogOpt, LocationInsensitive, Compare, Hybrid", - )), + _ => Err(format!("valid values: {}", Self::variants().join(", "))), } } } @@ -157,6 +188,39 @@ impl Output { /// partial results can also be stored in the context, so that the following /// variant can use it to prune its own input data pub fn compute(all_facts: &AllFacts, algorithm: Algorithm, dump_enabled: bool) -> Self { + #[cfg(not(feature = "polonius-souffle"))] + if algorithm.engine() == Engine::Souffle { + panic!("Polonius built without `--feature polonius-souffle`") + } + + #[cfg(feature = "polonius-souffle")] + if algorithm.engine() == Engine::Souffle { + let name = algorithm + .souffle_name() + .expect("Algorithm does not have Soufflé version"); + let facts = polonius_souffle::run_from_facts(name, &all_facts); + + let mut out = Output::new(dump_enabled); + + for tuple in facts.get("errors").unwrap().iter() { + let [loan, location]: [u32; 2] = tuple.try_into().unwrap(); + out.errors + .entry(T::Point::from(location)) + .or_default() + .push(T::Loan::from(loan)); + } + + for tuple in facts.get("subset_errors").unwrap().iter() { + let [origin1, origin2, location]: [u32; 3] = tuple.try_into().unwrap(); + out.subset_errors + .entry(T::Point::from(location)) + .or_default() + .insert((T::Origin::from(origin1), T::Origin::from(origin2))); + } + + return out; + } + let mut result = Output::new(dump_enabled); // TODO: remove all the cloning thereafter, but that needs to be done in concert with rustc @@ -255,7 +319,7 @@ impl Output { all_facts .universal_region .iter() - .map(|&origin| (origin, ())), + .map(|&origin| (origin.0, ())), ); let placeholder_loan = Relation::from_iter( @@ -299,8 +363,8 @@ impl Output { (potential_errors, potential_subset_errors) } - Algorithm::Naive => naive::compute(&ctx, &mut result), - Algorithm::DatafrogOpt => datafrog_opt::compute(&ctx, &mut result), + Algorithm::Naive(_) => naive::compute(&ctx, &mut result), + Algorithm::DatafrogOpt(_) => datafrog_opt::compute(&ctx, &mut result), Algorithm::Hybrid => { // Execute the fast `LocationInsensitive` computation as a pre-pass: // if it finds no possible errors, we don't need to do the more complex @@ -560,16 +624,9 @@ fn compare_errors( mod tests { use super::*; - impl Atom for usize { - fn index(self) -> usize { - self - } - } + type Idx = u32; - fn compare( - errors1: &FxHashMap>, - errors2: &FxHashMap>, - ) -> bool { + fn compare(errors1: &FxHashMap>, errors2: &FxHashMap>) -> bool { let diff1 = compare_errors(errors1, errors2); let diff2 = compare_errors(errors2, errors1); assert_eq!(diff1, diff2); diff --git a/polonius-facts/Cargo.toml b/polonius-facts/Cargo.toml new file mode 100644 index 0000000000..974392cb1e --- /dev/null +++ b/polonius-facts/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "polonius-facts" +version = "0.1.0" +edition = "2018" +resolver = "2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/polonius-facts/src/lib.rs b/polonius-facts/src/lib.rs new file mode 100644 index 0000000000..2da5c61ada --- /dev/null +++ b/polonius-facts/src/lib.rs @@ -0,0 +1,133 @@ +use std::fmt::Debug; +use std::hash::Hash; + +/// The "facts" which are the basis of the NLL borrow analysis. +#[derive(Clone, Debug)] +pub struct AllFacts { + /// `loan_issued_at(origin, loan, point)` indicates that the `loan` was "issued" + /// at the given `point`, creating a reference with the `origin`. + /// Effectively, `origin` may refer to data from `loan` starting at `point` (this is usually + /// the point *after* a borrow rvalue). + pub loan_issued_at: Vec<(T::Origin, T::Loan, T::Point)>, + + /// `universal_region(origin)` -- this is a "free region" within fn body + pub universal_region: Vec<(T::Origin,)>, + + /// `cfg_edge(point1, point2)` for each edge `point1 -> point2` in the control flow + pub cfg_edge: Vec<(T::Point, T::Point)>, + + /// `loan_killed_at(loan, point)` when some prefix of the path borrowed at `loan` + /// is assigned at `point`. + /// Indicates that the path borrowed by the `loan` has changed in some way that the loan no + /// longer needs to be tracked. (In particular, mutations to the path that was borrowed + /// no longer invalidate the loan) + pub loan_killed_at: Vec<(T::Loan, T::Point)>, + + /// `subset_base(origin1, origin2, point)` when we require `origin1@point: origin2@point`. + /// Indicates that `origin1 <= origin2` -- i.e., the set of loans in `origin1` are a subset + /// of those in `origin2`. + pub subset_base: Vec<(T::Origin, T::Origin, T::Point)>, + + /// `loan_invalidated_at(point, loan)` indicates that the `loan` is invalidated by some action + /// taking place at `point`; if any origin that references this loan is live, this is an error. + pub loan_invalidated_at: Vec<(T::Point, T::Loan)>, + + /// `var_used_at(var, point)` when the variable `var` is used for anything + /// but a drop at `point` + pub var_used_at: Vec<(T::Variable, T::Point)>, + + /// `var_defined_at(var, point)` when the variable `var` is overwritten at `point` + pub var_defined_at: Vec<(T::Variable, T::Point)>, + + /// `var_dropped_at(var, point)` when the variable `var` is used in a drop at `point` + pub var_dropped_at: Vec<(T::Variable, T::Point)>, + + /// `use_of_var_derefs_origin(variable, origin)`: References with the given + /// `origin` may be dereferenced when the `variable` is used. + /// + /// In rustc, we generate this whenever the type of the variable includes the + /// given origin. + pub use_of_var_derefs_origin: Vec<(T::Variable, T::Origin)>, + + /// `drop_of_var_derefs_origin(var, origin)` when the type of `var` includes + /// the `origin` and uses it when dropping + pub drop_of_var_derefs_origin: Vec<(T::Variable, T::Origin)>, + + /// `child_path(child, parent)` when the path `child` is the direct child of + /// `parent`, e.g. `child_path(x.y, x)`, but not `child_path(x.y.z, x)`. + pub child_path: Vec<(T::Path, T::Path)>, + + /// `path_is_var(path, var)` the root path `path` starting in variable `var`. + pub path_is_var: Vec<(T::Path, T::Variable)>, + + /// `path_assigned_at_base(path, point)` when the `path` was initialized at point + /// `point`. This fact is only emitted for a prefix `path`, and not for the + /// implicit initialization of all of `path`'s children. E.g. a statement like + /// `x.y = 3` at `point` would give the fact `path_assigned_at_base(x.y, point)` (but + /// neither `path_assigned_at_base(x.y.z, point)` nor `path_assigned_at_base(x, point)`). + pub path_assigned_at_base: Vec<(T::Path, T::Point)>, + + /// `path_moved_at_base(path, point)` when the `path` was moved at `point`. The + /// same logic is applied as for `path_assigned_at_base` above. + pub path_moved_at_base: Vec<(T::Path, T::Point)>, + + /// `path_accessed_at_base(path, point)` when the `path` was accessed at point + /// `point`. The same logic as for `path_assigned_at_base` and `path_moved_at_base` applies. + pub path_accessed_at_base: Vec<(T::Path, T::Point)>, + + /// These reflect the `'a: 'b` relations that are either declared by the user on function + /// declarations or which are inferred via implied bounds. + /// For example: `fn foo<'a, 'b: 'a, 'c>(x: &'c &'a u32)` would have two entries: + /// - one for the user-supplied subset `'b: 'a` + /// - and one for the `'a: 'c` implied bound from the `x` parameter, + /// (note that the transitive relation `'b: 'c` is not necessarily included + /// explicitly, but rather inferred by polonius). + pub known_placeholder_subset: Vec<(T::Origin, T::Origin)>, + + /// `placeholder(origin, loan)` describes a placeholder `origin`, with its associated + /// placeholder `loan`. + pub placeholder: Vec<(T::Origin, T::Loan)>, +} + +impl Default for AllFacts { + fn default() -> Self { + AllFacts { + loan_issued_at: Vec::default(), + universal_region: Vec::default(), + cfg_edge: Vec::default(), + loan_killed_at: Vec::default(), + subset_base: Vec::default(), + loan_invalidated_at: Vec::default(), + var_used_at: Vec::default(), + var_defined_at: Vec::default(), + var_dropped_at: Vec::default(), + use_of_var_derefs_origin: Vec::default(), + drop_of_var_derefs_origin: Vec::default(), + child_path: Vec::default(), + path_is_var: Vec::default(), + path_assigned_at_base: Vec::default(), + path_moved_at_base: Vec::default(), + path_accessed_at_base: Vec::default(), + known_placeholder_subset: Vec::default(), + placeholder: Vec::default(), + } + } +} + +pub trait Atom: From + Into + Copy + Clone + Debug + Eq + Ord + Hash + 'static { + fn index(self) -> usize; +} + +impl Atom for u32 { + fn index(self) -> usize { + self as usize + } +} + +pub trait FactTypes: Copy + Clone + Debug { + type Origin: Atom; + type Loan: Atom; + type Point: Atom; + type Variable: Atom; + type Path: Atom; +} diff --git a/polonius-souffle/Cargo.toml b/polonius-souffle/Cargo.toml new file mode 100644 index 0000000000..f466fca8aa --- /dev/null +++ b/polonius-souffle/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "polonius-souffle" +version = "0.1.0" +edition = "2018" +authors = ["The Rust Project Developers", "Polonius Developers"] +description = "Core definition for the Rust borrow checker" +license = "Apache-2.0/MIT" +repository = "https://github.com/rust-lang-nursery/polonius" +readme = "README.md" +keywords = ["compiler", "borrowck", "datalog"] +build = "build.rs" +resolver = "2" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cxx = "1.0.50" +log = "0.4" +polonius-facts = { path = "../polonius-facts" } + +[build-dependencies] +cc = "1.0.69" +cxx-build = "1.0.50" +glob = "0.3.0" +is_executable = "1.0.1" +which = "4.2.1" diff --git a/polonius-souffle/build.rs b/polonius-souffle/build.rs new file mode 100644 index 0000000000..112f91e3e6 --- /dev/null +++ b/polonius-souffle/build.rs @@ -0,0 +1,145 @@ +use std::collections::HashSet; +use std::error::Error; +use std::fs; +use std::io::{BufWriter, Write}; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use glob::glob; +use which::which; + +const RULES_DIR: &str = "../rules"; +const CXX_BRIDGE: &str = "src/ffi.rs"; + +type Result = ::std::result::Result>; + +/// Gets the filename for each "top-level" rulest +fn get_rulesets() -> Vec { + let result: std::result::Result, _> = + glob(&format!("{}/*.dl", RULES_DIR)).unwrap().collect(); + result.unwrap() +} +fn print_rerun_if_changed() { + // Rerun if any C++ file changes + for file in glob("shims/*").unwrap() { + println!("cargo:rerun-if-changed={}", file.unwrap().to_string_lossy()); + } + + // Rerun if any datalog file changes. + for file in glob(&format!("{}/**/*.dl", RULES_DIR)).unwrap() { + println!("cargo:rerun-if-changed={}", file.unwrap().to_string_lossy()); + } + + // Rerun if our CXX bindings change. + println!("cargo:rerun-if-changed={}", CXX_BRIDGE); +} + +fn main() -> Result<()> { + print_rerun_if_changed(); + + if which("souffle").is_err() { + eprintln!("`souffle` not in PATH. Is it installed?"); + return Err("missing `souffle`".into()); + } + + let mut cpp_filenames = vec![]; + let mut known_stems = HashSet::new(); + for ruleset in get_rulesets() { + // Get the common name for this ruleset. + let stem = ruleset.file_stem().unwrap().to_str().unwrap(); + + // Check that name for duplicates + // + // Souffle uses a single, global registry for datalog programs, indexed by string. + if !known_stems.insert(stem.to_owned()) { + eprintln!("Multiple datalog files named `{}`", stem); + return Err("Duplicate filenames".into()); + } + + let cpp_filename = souffle_generate(&ruleset, stem)?; + cpp_filenames.push(cpp_filename); + } + + odr_use_generate(&known_stems)?; + + let mut cc = cxx_build::bridge(CXX_BRIDGE); + + for file in cpp_filenames { + cc.file(&file); + } + + cc.cpp(true) + .define("__EMBEDDED_SOUFFLE__", None) + .include("./shims") + .flag("-std=c++17") + .flag("-Wno-unused-parameter") + .flag("-Wno-parentheses") + .try_compile("souffle")?; + + Ok(()) +} + +fn odr_use_func_name(stem: &str) -> String { + format!("odr_use_{}_global", stem) +} + +/// Uses Souffle to generate a C++ file for evaluating the given datalog program. +/// +/// Returns the filename for the generated C code, as well as the name of a generated function that +/// will trigger the global initializers in that translation unit. +fn souffle_generate(datalog_filename: &Path, stem: &str) -> Result { + let mut cpp_filename = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + cpp_filename.push(datalog_filename.with_extension("cpp").file_name().unwrap()); + + eprintln!("Generating code for {:?}...", &datalog_filename); + + let result = Command::new("souffle") + .arg("--generate") + .arg(&cpp_filename) + .arg(&datalog_filename) + .status()?; + + if !result.success() { + return Err("Invalid datalog".into()); + } + + let mut generated_cpp = fs::OpenOptions::new().append(true).open(&cpp_filename)?; + writeln!( + generated_cpp, + r#" + extern "C" + void {}() {{}}"#, + odr_use_func_name(stem) + )?; + + Ok(cpp_filename) +} + +// HACK: Souffle adds datalog programs to the registry in the initializer of a global +// variable (whose name begins with `__factory_Sf`). That global variable is eligible for +// deferred initialization, so we need to force its initializer to run before we do a lookup in +// the registry (which happens in a different translation unit from the generated code). +// +// We accomplish this by defining a single, no-op function in each generated C++ file, and calling +// it on the Rust side before doing any meaningful work. By the C++ standard, this forces global +// initializers for anything in the that translation unit to run, since calling the function is an +// ODR-use of something in the same translation unit. We also define a helper function, +// `odr_use_all`, which calls the no-op function in every known module. +fn odr_use_generate(known_stems: &HashSet) -> Result<()> { + let mut odr_use_filename = PathBuf::from(std::env::var("OUT_DIR").unwrap()); + odr_use_filename.push("odr_use.rs"); + + let mut odr_use = BufWriter::new(fs::File::create(odr_use_filename)?); + writeln!(odr_use, r#"extern "C" {{"#)?; + for stem in known_stems { + writeln!(odr_use, "fn {}();", odr_use_func_name(stem))?; + } + writeln!(odr_use, r#"}}"#)?; + + writeln!(odr_use, "fn odr_use_all() {{")?; + for stem in known_stems { + writeln!(odr_use, "unsafe {{ {}(); }}", odr_use_func_name(stem))?; + } + writeln!(odr_use, "}}")?; + Ok(()) +} diff --git a/polonius-souffle/shims/shims.hpp b/polonius-souffle/shims/shims.hpp new file mode 100644 index 0000000000..02cc6be12c --- /dev/null +++ b/polonius-souffle/shims/shims.hpp @@ -0,0 +1,104 @@ +#pragma once + +#include +#include +#include + +#include + +#include "polonius-souffle/src/ffi.rs.h" +#include "rust/cxx.h" + +#ifndef __EMBEDDED_SOUFFLE__ +#error "Build script must define __EMBEDDED_SOUFFLE__" +#endif + +/* + * Wrappers around generated souffle functions. From Rust, we cannot safely + * call functions that accept or return things with move constructors by-value + * (e.g. `std::string`). The usual fix is to move them into a `unique_ptr` + * first, or to pass a borrowed version (e.g. `cxx::Str`) from the Rust side + * and copy it here. + */ + +namespace souffle { + +std::unique_ptr +ProgramFactory_newInstance(const std::string &name) { + std::unique_ptr prog(ProgramFactory::newInstance(name)); + return prog; +} + +void load_all(SouffleProgram &foo, const std::string &name) { + foo.loadAll(name); +} + +void print_all(SouffleProgram &foo, const std::string &name) { + foo.printAll(name); +} + +void get_output_relations(const SouffleProgram &prog, rust::Vec &out) { + auto relations = prog.getOutputRelations(); + for (auto rel : relations) { + RelationPtr ptr{}; + ptr.ptr = rel; + out.push_back(ptr); + } +} + +void get_all_relations(const SouffleProgram &prog, rust::Vec &out) { + auto relations = prog.getAllRelations(); + for (auto rel : relations) { + RelationPtr ptr{}; + ptr.ptr = rel; + out.push_back(ptr); + } +} + +std::unique_ptr get_name(const Relation& rel) { + return std::make_unique(rel.getName()); +} + +// Fact loading + +// This function is copied from the member function on `Program`. +// +// We cannot use the original because it requires a reference to both a +// `Program` and a `Relation`, which have the same lifetime on the Rust side. +template +void insert(const std::tuple &t, Relation *rel) { + souffle::tuple t1(rel); + SouffleProgram::tuple_insert::add(t, t1); + rel->insert(t1); +} + +void insert_tuple1(Relation &rel, Tuple1 r) { + insert(std::make_tuple(r.a), &rel); +} + +void insert_tuple2(Relation &rel, Tuple2 r) { + insert(std::make_tuple(r.a, r.b), &rel); +} + +void insert_tuple3(Relation &rel, Tuple3 r) { + insert(std::make_tuple(r.a, r.b, r.c), &rel); +} + +void insert_tuple4(Relation &rel, Tuple4 r) { + insert(std::make_tuple(r.a, r.b, r.c, r.d), &rel); +} + +DynTuples dump_tuples(const Relation &rel) { + DynTuples out{}; + out.arity = rel.getArity(); + + for (auto tuple : rel) { + for (unsigned i = 0; i < out.arity; ++i) { + out.data.push_back(tuple[i]); + } + } + + return out; +} + +} // namespace bridge diff --git a/polonius-souffle/src/facts.rs b/polonius-souffle/src/facts.rs new file mode 100644 index 0000000000..4997d6dffb --- /dev/null +++ b/polonius-souffle/src/facts.rs @@ -0,0 +1,77 @@ +use std::convert::TryInto; +use std::pin::Pin; + +use log::warn; +use polonius_facts::{AllFacts, FactTypes}; + +use crate::ffi::{self, InsertIntoRelation}; + +fn insert_facts(mut rel: Pin<&mut ffi::Relation>, name: &str, facts: &[T]) +where + T: Copy + InsertIntoRelation, +{ + debug_assert_eq!(std::mem::size_of::() % std::mem::size_of::(), 0); + let datafrog_arity = std::mem::size_of::() / std::mem::size_of::(); + + let souffle_arity: usize = rel.getArity().try_into().unwrap(); + + if souffle_arity != datafrog_arity { + panic!( + r#"Arity mismatch for "{}". souffle={}, datafrog={}"#, + name, souffle_arity, datafrog_arity + ); + } + + for &fact in facts { + fact.insert_into_relation(rel.as_mut()); + } +} + +macro_rules! load_facts { + ($prog:ident, $facts:ident; $( $f:ident ),* $(,)?) => { + // Exhaustive matching, since new facts must be reflected below as well. + let AllFacts { + $( ref $f ),* + } = $facts; + $( + let name = stringify!($f); + let rel = $prog.as_mut().relation_mut(name); + if let Some(rel) = rel { + insert_facts(rel, name, $f); + } else { + warn!("Relation named `{}` not found. Skipping...", name); + } + )* + } +} + +pub fn insert_all_facts(mut prog: Pin<&mut ffi::Program>, facts: &AllFacts) +where + T: FactTypes, + T::Origin: Into, + T::Loan: Into, + T::Point: Into, + T::Variable: Into, + T::Path: Into, +{ + load_facts!(prog, facts; + loan_issued_at, + universal_region, + cfg_edge, + loan_killed_at, + subset_base, + loan_invalidated_at, + var_used_at, + var_defined_at, + var_dropped_at, + use_of_var_derefs_origin, + drop_of_var_derefs_origin, + child_path, + path_is_var, + path_assigned_at_base, + path_moved_at_base, + path_accessed_at_base, + known_placeholder_subset, + placeholder, + ); +} diff --git a/polonius-souffle/src/ffi.rs b/polonius-souffle/src/ffi.rs new file mode 100644 index 0000000000..0d0c606ff1 --- /dev/null +++ b/polonius-souffle/src/ffi.rs @@ -0,0 +1,271 @@ +use core::fmt; +use std::pin::Pin; + +use cxx::let_cxx_string; + +pub use self::ffi::{ + load_all, print_all, DynTuples, Program, Relation, Tuple1, Tuple2, Tuple3, Tuple4, +}; + +#[cxx::bridge(namespace = "souffle")] +mod ffi { + struct Tuple1 { + a: u32, + } + + struct Tuple2 { + a: u32, + b: u32, + } + + struct Tuple3 { + a: u32, + b: u32, + c: u32, + } + + struct Tuple4 { + a: u32, + b: u32, + c: u32, + d: u32, + } + + /// A list of tuples whose arity is only known at runtime. + #[derive(Default)] + struct DynTuples { + data: Vec, + arity: usize, + } + + /// A pointer to a relation. + /// + /// Needed to work around the fact that does not allow for types like `Vec<*const T>` in + /// signatures. + struct RelationPtr { + ptr: *const Relation, + } + + unsafe extern "C++" { + include!("souffle/SouffleInterface.h"); + include!("polonius-souffle/shims/shims.hpp"); + + #[cxx_name = "SouffleProgram"] + type Program; + + fn ProgramFactory_newInstance(s: &CxxString) -> UniquePtr; + + fn load_all(prog: Pin<&mut Program>, dir: &CxxString); + fn print_all(prog: Pin<&mut Program>, dir: &CxxString); + fn getRelation(self: &Program, relation: &CxxString) -> *mut Relation; + fn run(self: Pin<&mut Program>); + fn get_output_relations(prog: &Program, relations: &mut Vec); + fn get_all_relations(prog: &Program, relations: &mut Vec); + + type Relation; + + fn size(self: &Relation) -> usize; + // fn getSignature(self: &Relation) -> UniquePtr; + fn getArity(self: &Relation) -> u32; + fn get_name(rel: &Relation) -> UniquePtr; + + fn insert_tuple1(rel: Pin<&mut Relation>, t: Tuple1); + fn insert_tuple2(rel: Pin<&mut Relation>, t: Tuple2); + fn insert_tuple3(rel: Pin<&mut Relation>, t: Tuple3); + fn insert_tuple4(rel: Pin<&mut Relation>, t: Tuple4); + + fn dump_tuples(rel: &Relation) -> DynTuples; + } +} + +impl fmt::Debug for DynTuples { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.iter()).finish() + } +} + +impl Relation { + pub fn name(&self) -> String { + let s = ffi::get_name(self); + s.as_ref() + .expect("Relation must have a name") + .to_string_lossy() + .into_owned() + } + + pub fn tuples(&self) -> DynTuples { + ffi::dump_tuples(self) + } +} + +// Rust wrappers + +// See `build.rs` for background. +include!(concat!(env!("OUT_DIR"), "/odr_use.rs")); + +impl Program { + pub fn new(name: &str) -> cxx::UniquePtr { + odr_use_all(); + let_cxx_string!(name = name); + ffi::ProgramFactory_newInstance(&*name) + } + + pub fn relation(&self, name: &str) -> Option<&Relation> { + let_cxx_string!(name = name); + let rel = self.getRelation(&*name); + + // SAFETY: `getRelation` returns a valid pointer or NULL. + unsafe { rel.as_ref() } + } + + pub fn relation_mut(self: Pin<&mut Self>, name: &str) -> Option> { + let_cxx_string!(name = name); + let rel = self.getRelation(&*name); + + // SAFETY: `relation_mut` returns a valid pointer or NULL. The returned reference has the + // same lifetime as `self`, so multiple mutable references to the same `Relation` are + // impossible. The mutable reference is never made available outside the `Pin`, so it + // cannot be moved by the caller. + unsafe { rel.as_mut().map(|x| Pin::new_unchecked(x)) } + } + + pub fn output_relations(&self) -> impl Iterator { + let mut relations = vec![]; + ffi::get_output_relations(self, &mut relations); + + relations.into_iter().map(|ptr| unsafe { &*ptr.ptr }) + } + + pub fn relations(&self) -> impl Iterator { + let mut relations = vec![]; + ffi::get_all_relations(self, &mut relations); + + relations.into_iter().map(|ptr| unsafe { &*ptr.ptr }) + } +} + +impl DynTuples { + pub fn iter(&self) -> std::slice::ChunksExact<'_, u32> { + self.data.chunks_exact(self.arity) + } +} + +// Tuples + +pub trait InsertIntoRelation { + fn insert_into_relation(self, rel: Pin<&mut Relation>); +} + +impl> InsertIntoRelation for (A,) { + fn insert_into_relation(self, rel: Pin<&mut Relation>) { + ffi::insert_tuple1(rel, self.into_tuple()) + } +} +impl, B: Into> InsertIntoRelation for (A, B) { + fn insert_into_relation(self, rel: Pin<&mut Relation>) { + ffi::insert_tuple2(rel, self.into_tuple()) + } +} + +impl, B: Into, C: Into> InsertIntoRelation for (A, B, C) { + fn insert_into_relation(self, rel: Pin<&mut Relation>) { + ffi::insert_tuple3(rel, self.into_tuple()) + } +} + +impl, B: Into, C: Into, D: Into> InsertIntoRelation for (A, B, C, D) { + fn insert_into_relation(self, rel: Pin<&mut Relation>) { + ffi::insert_tuple4(rel, self.into_tuple()) + } +} + +impl Tuple1 { + pub fn insert_into_relation(self, rel: Pin<&mut Relation>) { + ffi::insert_tuple1(rel, self) + } +} + +impl Tuple2 { + pub fn insert_into_relation(self, rel: Pin<&mut Relation>) { + ffi::insert_tuple2(rel, self) + } +} + +impl Tuple3 { + pub fn insert_into_relation(self, rel: Pin<&mut Relation>) { + ffi::insert_tuple3(rel, self) + } +} + +impl Tuple4 { + pub fn insert_into_relation(self, rel: Pin<&mut Relation>) { + ffi::insert_tuple4(rel, self) + } +} + +// Conversion method into FFI tuples. +// +// `From` or `Into` would be better, but this helps type deduction inside the fact loading macro. +pub trait IntoTuple { + fn into_tuple(self) -> T; +} + +impl> IntoTuple for (A,) { + fn into_tuple(self) -> Tuple1 { + Tuple1 { a: self.0.into() } + } +} + +impl, B: Into> IntoTuple for (A, B) { + fn into_tuple(self) -> Tuple2 { + Tuple2 { + a: self.0.into(), + b: self.1.into(), + } + } +} + +impl, B: Into, C: Into> IntoTuple for (A, B, C) { + fn into_tuple(self) -> Tuple3 { + Tuple3 { + a: self.0.into(), + b: self.1.into(), + c: self.2.into(), + } + } +} + +impl, B: Into, C: Into, D: Into> IntoTuple for (A, B, C, D) { + fn into_tuple(self) -> Tuple4 { + Tuple4 { + a: self.0.into(), + b: self.1.into(), + c: self.2.into(), + d: self.3.into(), + } + } +} + +impl> From for (A,) { + fn from(t: Tuple1) -> Self { + (t.a.into(),) + } +} + +impl, B: From> From for (A, B) { + fn from(t: Tuple2) -> Self { + (t.a.into(), t.b.into()) + } +} + +impl, B: From, C: From> From for (A, B, C) { + fn from(t: Tuple3) -> Self { + (t.a.into(), t.b.into(), t.c.into()) + } +} + +impl, B: From, C: From, D: From> From for (A, B, C, D) { + fn from(t: Tuple4) -> Self { + (t.a.into(), t.b.into(), t.c.into(), t.d.into()) + } +} diff --git a/polonius-souffle/src/lib.rs b/polonius-souffle/src/lib.rs new file mode 100644 index 0000000000..3831e1928b --- /dev/null +++ b/polonius-souffle/src/lib.rs @@ -0,0 +1,44 @@ +pub mod facts; +mod ffi; + +pub use ffi::{DynTuples, Program}; + +use std::collections::HashMap; +use std::path::Path; + +use cxx::let_cxx_string; +use polonius_facts::{AllFacts, FactTypes}; + +pub fn run_from_dir(prog: &str, facts_dir: &Path) { + let_cxx_string!(facts = facts_dir.to_string_lossy().as_bytes()); + let_cxx_string!(empty = ""); + + let mut prog = Program::new(&prog); + let mut prog = prog.as_mut().expect("Wrong program name"); + ffi::load_all(prog.as_mut(), &facts); + prog.as_mut().run(); + ffi::print_all(prog.as_mut(), &empty); +} + +pub fn run_from_facts(prog: &str, facts: &AllFacts) -> HashMap +where + T: FactTypes, +{ + let mut prog = Program::new(prog); + let mut prog = prog.as_mut().expect("Wrong program name"); + + facts::insert_all_facts(prog.as_mut(), facts); + + prog.as_mut().run(); + + let output_relations: HashMap<_, _> = prog + .relations() + .map(|rel| { + let s = rel.name(); + let tuples = rel.tuples(); + (s, tuples) + }) + .collect(); + + output_relations +} diff --git a/rules/literate.py b/rules/literate.py new file mode 100755 index 0000000000..2e36efacde --- /dev/null +++ b/rules/literate.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 + +import re +import sys + +ident = re.compile('[?_a-zA-Z][_a-zA-Z]*') + + +def datalog_to_markdown(dl, out): + lines = dl.readlines() + + header_depth = 1 + comments = [] + code = [] + + for line in lines: + # An empty line ends the section + if line.isspace(): + if comments or code: + write_section(out, comments, code) + comments = [] + code = [] + + continue + + # Preprocessor directives + if line.startswith('#'): + continue + + # A comment + if line.startswith('//'): + assert len(code) == 0 + + line = line.strip('/') + line = line.removeprefix(' ') + + comments.append(line) + + old_len = len(line) + leading_pound_signs = old_len - len(line.lstrip('#')) + if leading_pound_signs: + header_depth = leading_pound_signs + + continue + + # Add a section header for datalog declarations + words = line.split() + if words[0] == '.type' or words[0] == '.decl': + title = ident.match(words[1]).group() + # h = '#' * header_depth + h = '####' + comments = [f'{h} `{title}`\n', '\n'] + comments + + code.append(line) + + write_section(out, comments, code) + + +def write_section(out, comments, code): + for line in comments: + out.write(line) + + if comments and not comments[-1].isspace(): + out.write('\n') + + for line in code: + out.write('\t') + out.write(line) + + if code and not code[-1].isspace(): + out.write('\n') + + +if __name__ == '__main__': + infile = sys.argv[1] + with open(infile) as infile: + datalog_to_markdown(infile, sys.stdout) + diff --git a/rules/mkbook.sh b/rules/mkbook.sh new file mode 100755 index 0000000000..c28eb657e5 --- /dev/null +++ b/rules/mkbook.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# `cd` into the directory where this script lives +cd "$(dirname "${BASH_SOURCE[0]}")" + +for f in $(find . -name '*.dl'); do + echo $f + name=$(basename -- $f) + ./literate.py "$f" > "../../book/src/rules/${name%.*}.md" +done diff --git a/rules/naive.dl b/rules/naive.dl new file mode 100644 index 0000000000..4e4e8d7b0e --- /dev/null +++ b/rules/naive.dl @@ -0,0 +1,126 @@ +#include "shared/atoms.dl" +#include "shared/cfg.dl" +#include "shared/atoms.dl" +#include "shared/liveness.dl" + +// # Loans + +// ## Inputs + +// Indicates that the given loan `Loan` was "issued" at the given node `Node`, +// creating a reference with the origin `Origin`. +.decl loan_issued_at(origin: Origin, loan: Loan, node: Node) +.input loan_issued_at + + +// Indicates that the path borrowed by the loan `Loan` has changed in some way +// that the loan no longer needs to be tracked. (in particular, mutations to the +// path that was borrowed no longer invalidate the loan.) +.decl loan_killed_at(loan: Loan, node: Node) +.input loan_killed_at + + +// Indicates that the loan is "invalidated" by some action tha takes place at the +// given node; if any origin that references this loan is live, that is an error +// +// FIXME: These are reversed for some reason??? +.decl loan_invalidated_at(node: Node, loan: Loan) +.input loan_invalidated_at + +.decl loan_invalidated_at_fixed(loan: Loan, node: Node) + +loan_invalidated_at_fixed(loan, node) :- loan_invalidated_at(node, loan). + + +// Indicates that `O1 <= O2` -- i.e., the set of loans in O1 are a subset of those +// in O2. +.decl subset_base(origin1: Origin, origin2: Origin, n: Node) +.input subset_base + + +// Declares a "placeholder origin" and loan. These are the named lifetimes that +// appear on function declarations and the like (e.g., the `'a` in `fn +// foo<'a>(...)`). +.decl placeholder(o: Origin, loan: Loan) +.input placeholder + +.decl placeholder_origin(o: Origin) + +placeholder_origin(o) :- placeholder(o, _). + + +// Declares a known subset relation between two placeholder origins. For example, +// `fn foo<'a, 'b: 'a>()` would have a relation to `'b: 'a`. This is not +// transitive. +.decl known_placeholder_subset(origin1: Origin, origin2: Origin) +.input known_placeholder_subset + + +// ## Relations + +.decl loan_live_on_entry(loan: Loan, node: Node) + +loan_live_on_entry(loan, node) :- + origin_contains_loan_on_entry(origin, loan, node), + (origin_live_on_entry(origin, node); placeholder_origin(origin)). + + +.decl subset(origin1: Origin, origin2: Origin, node: Node) + +subset(origin1, origin2, node) :- + subset_base(origin1, origin2, node). + +subset(origin1, origin3, node) :- + subset(origin1, origin2, node), + subset(origin2, origin3, node), + origin1 != origin3. + +subset(origin1, origin2, targetNode) :- + subset(origin1, origin2, sourceNode), + cfg_edge(sourceNode, targetNode), + (origin_live_on_entry(origin1, targetNode); placeholder_origin(origin1)), + (origin_live_on_entry(origin2, targetNode); placeholder_origin(origin2)). + + +// Formerly `requires` +.decl origin_contains_loan_on_entry(origin: Origin, loan: Loan, node: Node) + +origin_contains_loan_on_entry(origin, loan, node) :- + loan_issued_at(origin, loan, node). + +origin_contains_loan_on_entry(origin2, loan, node) :- + origin_contains_loan_on_entry(origin1, loan, node), + subset(origin1, origin2, node). + +origin_contains_loan_on_entry(origin, loan, targetNode) :- + origin_contains_loan_on_entry(origin, loan, sourceNode), + !loan_killed_at(loan, sourceNode), + cfg_edge(sourceNode, targetNode), + (origin_live_on_entry(origin, targetNode); placeholder_origin(origin)). + +// Formerly `borrow_live_at` +.decl loan_live_at(loan: Loan, node: Node) + +loan_live_at(loan, node) :- + origin_contains_loan_on_entry(origin, loan, node), + origin_live_on_entry(origin, node). + + +// ## Error reporting + +.decl errors(l: Loan, n: Node) +.output errors + +errors(loan, node) :- + loan_invalidated_at_fixed(loan, node), + loan_live_at(loan, node). + + +.decl subset_errors(origin1: Origin, origin2: Origin, node: Node) +.output subset_errors + +subset_errors(origin1, origin2, node) :- + subset(origin1, origin2, node), + placeholder_origin(origin1), + placeholder_origin(origin2), + !known_placeholder_subset(origin1, origin2). diff --git a/rules/opt.dl b/rules/opt.dl new file mode 100644 index 0000000000..d925a0e1b2 --- /dev/null +++ b/rules/opt.dl @@ -0,0 +1,233 @@ +#include "shared/atoms.dl" +#include "shared/cfg.dl" +#include "shared/liveness.dl" + +// # Loans (optimized) + +// Indicates that the given loan `Loan` was "issued" at the given node `Node`, +// creating a reference with the origin `Origin`. +.decl loan_issued_at(origin: Origin, loan: Loan, node: Node) +.input loan_issued_at + + +// Indicates that the path borrowed by the loan `Loan` has changed in some way +// that the loan no longer needs to be tracked. (in particular, mutations to the +// path that was borrowed no longer invalidate the loan.) +.decl loan_killed_at(loan: Loan, node: Node) +.input loan_killed_at + + +// Indicates that the loan is "invalidated" by some action tha takes place at the +// given node; if any origin that references this loan is live, that is an error +// +// FIXME: These are reversed for some reason??? +.decl loan_invalidated_at(node: Node, loan: Loan) +.input loan_invalidated_at + +.decl loan_invalidated_at_fixed(loan: Loan, node: Node) + +loan_invalidated_at_fixed(loan, node) :- loan_invalidated_at(node, loan). + + + +// Indicates that `O1 <= O2` -- i.e., the set of loans in O1 are a subset of those +// in O2. +.decl subset_base(origin1: Origin, origin2: Origin, n: Node) +.input subset_base + + +// Declares a "placeholder origin" and loan. These are the named lifetimes that +// appear on function declarations and the like (e.g., the `'a` in `fn +// foo<'a>(...)`). +.decl placeholder(o: Origin, l: Loan) +.input placeholder + +.decl placeholder_origin(o: Origin) +placeholder_origin(o) :- placeholder(o, _). + +// Declares a known subset relation between two placeholder origins. For example, +// `fn foo<'a, 'b: 'a>()` would have a relation to `'b: 'a`. This is not +// transitive. +.decl known_placeholder_subset(origin1: Origin, origin2: Origin) +.input known_placeholder_subset + + +.decl origin_live_on_entry_or_placeholder(origin: Origin, point: Node) inline + +origin_live_on_entry_or_placeholder(origin, point) :- origin_live_on_entry(origin, point). +origin_live_on_entry_or_placeholder(origin, point) :- placeholder_origin(origin), cfg_node(point). + + +// The origins `origin1` and `origin2` are "live to dead" +// on the edge `point1 -> point2` if: +// +// - In `point1`, `origin1` <= `origin2` +// - In `point2`, `origin1` is live but `origin2` is dead. +// +// In that case, `point2` would like to add all the +// live things reachable from `origin2` to `origin1`. +.decl live_to_dying_regions(origin1: Origin, origin2: Origin, point1: Node, point2: Node) + +live_to_dying_regions(origin1, origin2, point1, point2) :- + subset(origin1, origin2, point1), + cfg_edge(point1, point2), + origin_live_on_entry_or_placeholder(origin1, point2), + !origin_live_on_entry_or_placeholder(origin2, point2). + + +// The `origin` requires `loan`, but the `origin` goes dead +// along the edge `point1 -> point2`. +.decl dying_region_requires(origin: Origin, point1: Node, point2: Node, loan: Loan) + +dying_region_requires(origin, point1, point2, loan) :- + requires(origin, loan, point1), + !loan_killed_at(loan, point1), + cfg_edge(point1, point2), + !origin_live_on_entry_or_placeholder(origin, point2). + + +// Contains dead origins where we are interested +// in computing the transitive closure of things they +// can reach. +.decl dying_can_reach_origins(origin: Origin, point1: Node, point2: Node) + +dying_can_reach_origins(origin, point1, point2) :- + dying_region_requires(origin, point1, point2, _loan). + +dying_can_reach_origins(origin2, point1, point2) :- + live_to_dying_regions(_, origin2, point1, point2). + + +// Indicates that `origin1`, which is dead +// in `point2`, can reach `origin2` in `point1`. +// +// This is effectively the transitive subset +// relation, but we try to limit it to origins +// that are dying on the edge `point1 -> point2`. +.decl dying_can_reach(origin1: Origin, origin2: Origin, point1: Node, point2: Node) + +dying_can_reach(origin1, origin2, point1, point2) :- + dying_can_reach_origins(origin1, point1, point2), + subset(origin1, origin2, point1). + +// This is the "transitive closure" rule, but +// note that we only apply it with the +// "intermediate" `origin2` is dead at `point2`. +dying_can_reach(origin1, origin3, point1, point2) :- + dying_can_reach(origin1, origin2, point1, point2), + !origin_live_on_entry_or_placeholder(origin2, point2), + subset(origin2, origin3, point1). + + +// Indicates that, along the edge `point1 -> point2`, the dead (in `point2`) +// `origin1` can reach the live (in `point2`) `origin2` via a subset +// relation. This is a subset of the full `dying_can_reach` +// relation where we filter down to those cases where `origin2` is +// live in `point2`. +.decl dying_can_reach_live(origin1: Origin, origin2: Origin, point1: Node, point2: Node) + +dying_can_reach_live(origin1, origin2, point1, point2) :- + dying_can_reach(origin1, origin2, point1, point2), + origin_live_on_entry_or_placeholder(origin2, point2). + + +// Indicates a "borrow region" `origin` at `point` which is not live on +// entry to `point`. +.decl dead_borrow_region_can_reach_root(origin: Origin, point: Node, loan: Loan) + +dead_borrow_region_can_reach_root(origin, point, loan) :- + loan_issued_at(origin, loan, point), + !origin_live_on_entry_or_placeholder(origin, point). + + +.decl dead_borrow_region_can_reach_dead(origin: Origin, point: Node, loan: Loan) + +dead_borrow_region_can_reach_dead(origin, point, loan) :- + dead_borrow_region_can_reach_root(origin, point, loan). + +dead_borrow_region_can_reach_dead(origin2, point, loan) :- + dead_borrow_region_can_reach_dead(origin1, point, loan), + subset(origin1, origin2, point), + !origin_live_on_entry_or_placeholder(origin2, point). + + +.decl subset(origin1: Origin, origin2: Origin, point: Node) +.output subset + +subset(origin1, origin2, point) :- subset_base(origin1, origin2, point). + +// Carry `origin1 <= origin2` from `point1` into `point2` if both `origin1` and +// `origin2` are live in `point2`. +subset(origin1, origin2, point2) :- + subset(origin1, origin2, point1), + cfg_edge(point1, point2), + origin_live_on_entry_or_placeholder(origin1, point2), + origin_live_on_entry_or_placeholder(origin2, point2). + +subset(origin1, origin3, point2) :- + live_to_dying_regions(origin1, origin2, point1, point2), + dying_can_reach_live(origin2, origin3, point1, point2), + origin1 != origin3. + + +.decl requires(origin: Origin, loan: Loan, point: Node) + +requires(origin, loan, point) :- + loan_issued_at(origin, loan, point). + +// Communicate a `origin1 requires loan` relation across +// an edge `point1 -> point2` where `origin1` is dead in `point2`; in +// that case, for each origin `origin2` live in `point2` +// where `origin1 <= origin2` in `point1`, we add `origin2 requires loan` +// to `point2`. +requires(origin2, loan, point2) :- + dying_region_requires(origin1, point1, point2, loan), + dying_can_reach_live(origin1, origin2, point1, point2). + +requires(origin, loan, point2) :- + requires(origin, loan, point1), + !loan_killed_at(loan, point1), + cfg_edge(point1, point2), + origin_live_on_entry_or_placeholder(origin, point2). + + +.decl borrow_live_at(loan: Loan, point: Node) + +borrow_live_at(loan, point) :- + requires(origin, loan, point), + origin_live_on_entry_or_placeholder(origin, point). + +borrow_live_at(loan, point) :- + dead_borrow_region_can_reach_dead(origin1, point, loan), + subset(origin1, origin2, point), + origin_live_on_entry_or_placeholder(origin2, point). + + +.decl errors(loan: Loan, point: Node) +.output errors + +errors(loan, point) :- + loan_invalidated_at_fixed(loan, point), + borrow_live_at(loan, point). + +// All subset relationships whose left-hand side is a placeholder origin. +.decl subset_placeholder(origin1: Origin, origin2: Origin, point: Node) + +subset_placeholder(Origin1, Origin2, Point) :- + subset(Origin1, Origin2, Point), + placeholder_origin(Origin1). + +// We compute the transitive closure of the placeholder origins, so we +// maintain the invariant from the rule above that `Origin1` is a placeholder origin. +subset_placeholder(Origin1, Origin3, Point) :- + subset_placeholder(Origin1, Origin2, Point), + subset(Origin2, Origin3, Point). + +.decl subset_errors(origin1: Origin, origin2: Origin, point: Node) +.output subset_errors + +subset_errors(Origin1, Origin2, Point) :- + Origin1 != Origin2, + subset_placeholder(Origin1, Origin2, Point), + placeholder_origin(Origin2), + !known_placeholder_subset(Origin1, Origin2). diff --git a/rules/shared/atoms.dl b/rules/shared/atoms.dl new file mode 100644 index 0000000000..2aad3b7fd1 --- /dev/null +++ b/rules/shared/atoms.dl @@ -0,0 +1,75 @@ +#pragma once + +// # Atoms +// +// We'll use this snippet of Rust code to illustrate the various kinds of +// atoms that can exist. +// +// ```rust +// let x = (vec![22], vec![44]); +// let y = &x.1; +// let z = x.0; +// drop(y); +// ``` + +// A **variable** represents a user variable defined by the Rust source +// code. In our snippet, `x`, `y`, and `z` are variables. Other kinds of +// variables include parameters. +.type Var <: unsigned + +// A **path** indicates a path through memory to a memory location -- +// these roughly correspond to **places** in MIR, although we only +// support a subset of the full places (that is, every MIR place maps to +// a Path, but sometimes a single Path maps back to multiple MIR places). +// +// Each path begins with a variable (e.g., `x`) but can be extended with +// fields (e.g., `x.1`), with an "index" (e.g., `x[]`) or with a deref `*x`. +// Note that the index paths (`x[]`) don't track the actual index that was +// accessed, since the borrow check treats all indices as equivalent. +// +// The grammar for paths would thus look something like this: +// +// ``` +// Path = Variable +// | Path "." Field // field access +// | Path "[" "]" // index +// | "*" Path +// ``` +// +// Each path has a distinct atom associated with it. So there would be an +// atom P1 for the path `x` and another atom P2 for the path `x.0`. +// These atoms are related to one another through the `path_parent` +// relation. +.type Path <: unsigned + + +// Nodes are, well, *nodes* in the control-flow graph. They are related +// to one another by the `cfg_edge` relation. +// +// For each statement (resp. terminator) S in the MIR, there are actually +// two associated nodes. One represents the "start" of S -- before S has +// begun executing -- the other is called the "mid node" -- which +// represents the point where S "takes effect". Each start node has +// exactly one successor, the mid node. +.type Node <: unsigned + + +.type Block <: unsigned + + +.type Location = [ block: Block, stmt: unsigned, mid: symbol ] + + +// A **loan** represents some borrow that occurs in the source. Each +// loan has an associated path that was borrowed along with a mutability. +// So, in our example, there would be a single loan, for the `&x.1` +// expression. +.type Loan <: unsigned + + +// An **origin** is what it typically called in Rust a **lifetime**. In +// Polonius, an **origin** refers to the set of loans from which a +// reference may have been created. +.type Origin <: unsigned + + diff --git a/rules/shared/cfg.dl b/rules/shared/cfg.dl new file mode 100644 index 0000000000..9738ab8f53 --- /dev/null +++ b/rules/shared/cfg.dl @@ -0,0 +1,38 @@ +#pragma once +#include "atoms.dl" + +// # Control-flow graph + +// ## Nodes + +// Indicates that an edge exists between `source` and `target` +.decl cfg_edge(sourceNode: Node, targetNode: Node) +.input cfg_edge + +// Enumerates all nodes (note that this approach implies that a single node +// graph is essentially not a thing). +.decl cfg_node(p: Node) + +cfg_node(p) :- cfg_edge(p, _). +cfg_node(p) :- cfg_edge(_, p). + +// ## Basic Blocks + +.decl bb_edge(src: Block, targ: Block) +.input bb_edge + +.decl node_is_loc(node: Node, loc: Location) +.input node_is_loc + + +.decl precedes_in_block(loc1: Location, loc2: Location) inline + +precedes_in_block([bb, stmt1, mid1], [bb, stmt2, mid2]) :- + stmt2 > stmt1; (stmt1 = stmt2, mid2 = "Mid", mid1 = "Start"). + + +.decl succeeds_in_block(loc1: Location, loc2: Location) inline + +succeeds_in_block([bb, stmt1, mid1], [bb, stmt2, mid2]) :- + stmt2 > stmt1; (stmt1 = stmt2, mid1 = "Mid", mid2 = "Start"). + diff --git a/rules/shared/init.dl b/rules/shared/init.dl new file mode 100644 index 0000000000..36e85640ee --- /dev/null +++ b/rules/shared/init.dl @@ -0,0 +1,76 @@ +#pragma once +#include "atoms.dl" +#include "paths.dl" + +// # Initialization + +// ## Relations + +// Here we compute the set of paths that *may* contain a value on exit from +// each given node in the CFG. This is used later as part of the liveness +// analysis. In particular, if a value has been moved, then its drop is a +// no-op. +// +// This is not used to compute move errors -- it would be too "optimistic", +// since it only computes if a value *may* be initialized. See the next section +// on computing *uninitialization*. +.decl path_maybe_initialized_on_exit(path: Path, node: Node) brie + +path_maybe_initialized_on_exit(path, node) :- + path_assigned_at(path, node). + +path_maybe_initialized_on_exit(path, targetNode) :- + path_maybe_initialized_on_exit(path, sourceNode), + cfg_edge(sourceNode, targetNode), + !path_moved_at(path, targetNode). + + +// We also compute which **variables** may be initialized (or at least partly +// initialized). Drops for variables that are not even partly initialized are +// known to be a no-op. +.decl var_maybe_partly_initialized_on_exit(var: Var, node: Node) + +var_maybe_partly_initialized_on_exit(var, node) :- + path_maybe_initialized_on_exit(path, node), + path_begins_with_var(path, var). + + +// Here we compute the set of paths that are maybe *uninitialized* on exit from +// a node. Naturally, it would be illegal to access a path that is maybe +// uninitialized. +// +// We compute "maybe uninitialized" because it is easier than computing "must +// be initialized" (though they are equivalent), since the latter requires +// intersection, which is not available in "core datalog". It may make sense -- +// as an optimization -- to try and convert to intersection, although it is +// debatable which will result in more tuples overall. +.decl path_maybe_uninitialized_on_exit(path: Path, node: Node) brie + +path_maybe_uninitialized_on_exit(path, node) :- + path_moved_at(path, node). + +path_maybe_uninitialized_on_exit(path, targetNode) :- + path_maybe_uninitialized_on_exit(path, sourceNode), + cfg_edge(sourceNode, targetNode), + !path_assigned_at(path, targetNode). + + +.decl path_maybe_accessed_later(path: Path, node: Node) brie + +path_maybe_accessed_later(path, node) :- + path_accessed_at(path, node). + +path_maybe_accessed_later(path, src) :- + path_maybe_accessed_later(path, dst), + cfg_edge(src, dst). + + +// ## Errors + +.decl move_errors(path: Path, node: Node) +.output move_errors + +move_errors(path, targetNode) :- + path_maybe_uninitialized_on_exit(path, sourceNode), + cfg_edge(sourceNode, targetNode), + path_accessed_at(path, targetNode). diff --git a/rules/shared/liveness.dl b/rules/shared/liveness.dl new file mode 100644 index 0000000000..3bb2310031 --- /dev/null +++ b/rules/shared/liveness.dl @@ -0,0 +1,106 @@ +#pragma once + +#include "atoms.dl" +#include "init.dl" + +// # Liveness analysis +// +// The role of the liveness computation is to figure out, for each cfg node, +// which variables may be accessed at some point in the future. We also +// distinguish between variables that may be accessed in general and those that +// may only be dropped. This is because a "full access" may potentially +// dereference any reference found in the variable, whereas a drop is more +// limited in its effects. +// +// One interesting wrinkle around drops is that we also need to consider the +// initialization state of each variable. This is because `Drop` statements can +// be added for variables which are never initialized, or whose values have +// been moved. Such statements are considered no-ops in MIR. + +// ## Inputs + +// Variable is used at the given CFG node +.decl var_used_at(variable: Var, node: Node) +.input var_used_at + + +// Variable is defined (overwritten) at the given CFG node +.decl var_defined_at(variable: Var, node: Node) +.input var_defined_at + + +// Variable is dropped at this cfg node +.decl var_dropped_at(variable: Var, node: Node) +.input var_dropped_at + + +// References with the given origin may be +// dereferenced when the variable is used. +// +// In rustc, we generate this whenever the +// type of the variable includes the given +// origin. +.decl use_of_var_derefs_origin(variable: Var, origin: Origin) +.input use_of_var_derefs_origin + + +// References with the given origin may be +// dereferenced when the variable is dropped + +// In rustc, we generate this by examining the type +// and taking into account various +// unstable attributes. It is always a subset +// of `use_of_var_derefs_origin`. +.decl drop_of_var_derefs_origin(variable: Var, origin: Origin) +.input drop_of_var_derefs_origin + + +// ## Relations + +// Variables that are live on entry. +.decl var_live_on_entry(var: Var, node: Node) + +var_live_on_entry(var, node) :- + var_used_at(var, node). + +var_live_on_entry(var, sourceNode) :- + var_live_on_entry(var, targetNode), + cfg_edge(sourceNode, targetNode), + !var_defined_at(var, sourceNode). + + +// Variables that are "drop live" on entry. +// +// The initial rule is that, when a variable is dropped, that makes it +// drop-live -- unless we know that the variable is fully uninitialized, in +// which case the drop is a no-op. +// +// **Optimization:** In rustc, we compute drop-live only up to the point where +// something becomes "use-live". We could do the same here by adding some `!` +// checks against `var_live_on_entry`, though it would require stratification +// in the datalog (not a problem). +.decl var_drop_live_on_entry(var: Var, node: Node) + +var_drop_live_on_entry(var, targetNode) :- + var_dropped_at(var, targetNode), + cfg_edge(sourceNode, targetNode), + var_maybe_partly_initialized_on_exit(var, sourceNode). + +var_drop_live_on_entry(var, sourceNode) :- + var_drop_live_on_entry(var, targetNode), + cfg_edge(sourceNode, targetNode), + !var_defined_at(var, sourceNode), + var_maybe_partly_initialized_on_exit(var, sourceNode). + + +// An origin is live at the node N if some reference with that origin may be +// dereferenced in the future. +.decl origin_live_on_entry(origin: Origin, node: Node) + +origin_live_on_entry(origin, node) :- + var_live_on_entry(var, node), + use_of_var_derefs_origin(var, origin). + +origin_live_on_entry(origin, node) :- + var_drop_live_on_entry(var, node), + drop_of_var_derefs_origin(var, origin). diff --git a/rules/shared/paths.dl b/rules/shared/paths.dl new file mode 100644 index 0000000000..8b5eb008ca --- /dev/null +++ b/rules/shared/paths.dl @@ -0,0 +1,104 @@ +#include "atoms.dl" + +// # Move Paths + +// ## Inputs + +// Relates the path to the variable it begins with; +// so `path_begins_with_var(a.b, a)` is true, and so +// forth. +.decl path_is_var(path: Path, var: Var) +.input path_is_var + +// True if Child is a direct "subpath" of Parent. +// e.g. `child(a.b, a)` would be true, but not +// `child(a, a.b.c)`. +.decl child_path(parent: Path, child: Path) +.input child_path + +// Indicates that `Path` is assigned a value +// at the point `Node`. +// +// Important: This includes a tuple for each +// argument to the function, indicating that it is +// initialized on entry. +.decl path_assigned_at_base(parent: Path, node: Node) +.input path_assigned_at_base + +// Indicates that the value in `Path` is moved +// at the point `Node`. +// +// Important: This includes a tuple for each +// local variable in the MIR, indicating that it is +// "moved" (uninitialized) on entry. +.decl path_moved_at_base(path: Path, node: Node) +.input path_moved_at_base + +// Indicates that the value in `Path` is accessed +// at the point `Node`. +.decl path_accessed_at_base(path: Path, node: Node) +.input path_accessed_at_base + +// ## Relations + +// The reverse of `child_path` +// +// FIXME: generate this fact directly. +.decl parent_path(parent: Path, child: Path) + +parent_path(parent, child) :- child_path(child, parent). + + +// Computes the transitive closure over paths +.decl ancestor_path(ancestor: Path, descendant: Path) + +ancestor_path(parent, child) :- + parent_path(parent, child). + +ancestor_path(parent, grandchild) :- + ancestor_path(parent, child), + parent_path(child, grandchild). + +.decl path_assigned_at(path: Path, node: Node) + +path_assigned_at(path, node) :- + path_assigned_at_base(path, node). + +// If you initialize the path `a`, you also initialize `a.b` +path_assigned_at(childPath, node) :- + path_assigned_at(path, node), + ancestor_path(path, childPath). + + +.decl path_moved_at(path: Path, node: Node) + +path_moved_at(path, node) :- + path_moved_at_base(path, node). + +// If you move the path `a`, you also move `a.b` +path_moved_at(childPath, node) :- + path_moved_at(path, node), + ancestor_path(path, childPath). + + +.decl path_accessed_at(path: Path, node: Node) + +path_accessed_at(path, node) :- + path_accessed_at_base(path, node). + +// If you access the path `a`, you also access `a.b` +path_accessed_at(childPath, node) :- + path_accessed_at(path, node), + ancestor_path(path, childPath). + + +// True if `var` is the base of `path` (e.g. `path_begins_with_var(a.b.c, a)`) +.decl path_begins_with_var(path: Path, var: Var) + +path_begins_with_var(path, var) :- + path_is_var(path, var). + +path_begins_with_var(path, var) :- + path_is_var(root, var), + ancestor_path(root, path). + diff --git a/src/cli.rs b/src/cli.rs index 2d1e23ce94..a8b43f4153 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,10 +1,10 @@ use log::{error, Level, LevelFilter, Metadata, Record, SetLoggerError}; use pico_args as pico; -use polonius_engine::Algorithm; +use polonius_engine::{Algorithm, Engine}; use std::env; use std::error; use std::fmt; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::exit; use std::str::FromStr; use std::time::{Duration, Instant}; @@ -42,12 +42,6 @@ impl fmt::Display for Error { } } -macro_rules! attempt { - ($($tokens:tt)*) => { - (|| Ok({ $($tokens)* }))() - }; -} - pub fn main(opt: Options) -> Result<(), Error> { let output_directory = opt .output_directory @@ -61,49 +55,95 @@ pub fn main(opt: Options) -> Result<(), Error> { for facts_dir in &opt.fact_dirs { let tables = &mut intern::InternerTables::new(); - let result: Result<(Duration, AllFacts, Output), Error> = attempt! { - let verbose = opt.verbose; - let all_facts = tab_delim::load_tab_delimited_facts(tables, &Path::new(&facts_dir)) - .map_err(|e| Error(e.to_string()))?; - let algorithm = opt.algorithm; - let graphviz_output = graphviz_file.is_some() || liveness_graph_file.is_some(); - let (duration, output) = - timed(|| Output::compute(&all_facts, algorithm, verbose || graphviz_output)); - (duration, all_facts, output) - }; + let verbose = opt.verbose; + let algorithm = opt.algorithm; + let graphviz_output = graphviz_file.is_some() || liveness_graph_file.is_some(); + let all_facts = tab_delim::load_tab_delimited_facts(tables, &Path::new(&facts_dir)); - match result { - Ok((duration, all_facts, output)) => { - println!("--------------------------------------------------"); - println!("Directory: {}", facts_dir); - if !opt.skip_timing { - let seconds = duration.as_secs() as f64; - let millis = f64::from(duration.subsec_nanos()) * 0.000_000_001_f64; - println!("Time: {:0.3}s", seconds + millis); - } - if opt.show_tuples { - dump::dump_output(&output, &output_directory, tables) - .expect("Failed to write output"); - } - if let Some(ref graphviz_file) = graphviz_file { - dump::graphviz(&output, &all_facts, graphviz_file, tables) - .expect("Failed to write GraphViz"); - } - if let Some(ref liveness_graph_file) = liveness_graph_file { - dump::liveness_graph(&output, &all_facts, liveness_graph_file, tables) - .expect("Failed to write liveness graph"); - } + let all_facts = match all_facts { + Ok(x) => x, + Err(e) => { + error!("`{}`: {}", facts_dir, e); + continue; } + }; - Err(error) => { - error!("`{}`: {}", facts_dir, error); + println!("--------------------------------------------------"); + println!("Directory: {}", facts_dir); + + // Don't use `Output` for the `Souffle` algorthm variants, since we would have to serialize + // all the intermediate tuples to nested hash tables. It's easier to use our custom `dump`. + if algorithm.engine() == Engine::Souffle { + let name = algorithm + .souffle_name() + .expect("Algorithm does not have Soufflé version"); + + if let Err(e) = run_polonius_souffle(&all_facts, &output_directory, tables, name, &opt) + { + error!("`{}`: {}", facts_dir, e); } + + continue; + } + + let (duration, output) = + timed(|| Output::compute(&all_facts, algorithm, verbose || graphviz_output)); + if !opt.skip_timing { + let seconds = duration.as_secs() as f64; + let millis = f64::from(duration.subsec_nanos()) * 0.000_000_001_f64; + println!("Time: {:0.3}s", seconds + millis); + } + if opt.show_tuples { + dump::dump_output(&output, &output_directory, tables).expect("Failed to write output"); + } + if let Some(ref graphviz_file) = graphviz_file { + dump::graphviz(&output, &all_facts, graphviz_file, tables) + .expect("Failed to write GraphViz"); + } + if let Some(ref liveness_graph_file) = liveness_graph_file { + dump::liveness_graph(&output, &all_facts, liveness_graph_file, tables) + .expect("Failed to write liveness graph"); } } Ok(()) } +#[cfg(feature = "polonius-souffle")] +fn run_polonius_souffle( + all_facts: &AllFacts, + output_directory: &Option, + tables: &intern::InternerTables, + souffle_name: &str, + opt: &Options, +) -> Result<(), Error> { + // FIXME This time includes loading/unloading tuples across the FFI boundary. + let (duration, output) = timed(|| polonius_souffle::run_from_facts(souffle_name, &all_facts)); + if !opt.skip_timing { + let seconds = duration.as_secs() as f64; + let millis = f64::from(duration.subsec_nanos()) * 0.000_000_001_f64; + println!("Time: {:0.3}s", seconds + millis); + } + if opt.show_tuples { + dump::dump_souffle_output(&output, &output_directory, tables, opt.verbose) + .expect("Failed to write output"); + } + Ok(()) +} + +#[cfg(not(feature = "polonius-souffle"))] +fn run_polonius_souffle( + _: &AllFacts, + _: &Option, + _: &intern::InternerTables, + _: &str, + _: &Options, +) -> Result<(), Error> { + Err(Error( + "`polonius` was compiled without Soufflé backend (`--feature polonius-souffle`)".into(), + )) +} + fn timed(op: impl FnOnce() -> T) -> (Duration, T) { let start = Instant::now(); let output = op(); @@ -162,7 +202,7 @@ ARGS: // 2) parse args let options = Options { - algorithm: arg_from_str(&mut args, "-a")?.unwrap_or(Algorithm::Naive), + algorithm: arg_from_str(&mut args, "-a")?.unwrap_or(Algorithm::default()), show_tuples: args.contains("--show-tuples"), skip_timing: args.contains("--skip-timing"), verbose: args.contains(["-v", "--verbose"]), diff --git a/src/dump.rs b/src/dump.rs index cf7120b917..ad989758e9 100644 --- a/src/dump.rs +++ b/src/dump.rs @@ -15,6 +15,62 @@ use std::path::PathBuf; pub(crate) type Output = PoloniusEngineOutput; +#[cfg(feature = "polonius-souffle")] +pub(crate) fn dump_souffle_output( + output: &HashMap, + output_dir: &Option, + intern: &InternerTables, + dump_enabled: bool, +) -> io::Result<()> { + macro_rules! souffle_dump { + ($output:ident, $output_dir:ident, $intern:ident; $($name:ident : ($($ty:ident),*) ),* $(,)?) => { + $({ + let name = stringify!($name); + if let Some(tuples) = $output.get(name) { + let (_, mut w) = writer_for($output_dir, name)?; + + for tuple in tuples.iter() { + let mut fields = tuple.iter().copied(); + let mut sep = ""; + + $( + let table = $ty::table($intern); + let field = fields.next().expect("wrong arity"); + let unterned = table.untern(field.into()); + write!(w, "{}{}", sep, unterned)?; + sep = "\t"; + )* + + let _ = sep; + assert!(fields.next().is_none(), "wrong arity"); + writeln!(w, "")?; + } + } + })* + } + } + + souffle_dump!(output, output_dir, intern; + subset_errors: (Origin, Origin, Point), + errors: (Loan, Point), + move_errors: (Path, Point), + ); + + if dump_enabled { + souffle_dump!(output, output_dir, intern; + origin_contains_loan_at: (Origin, Loan, Point), + origin_live_on_entry: (Origin, Point), + loan_invalidated_at: (Point, Loan), // FIXME: This is backward + loan_live_at: (Loan, Point), + subset_anywhere: (Origin, Origin), + + // TODO: complete this list + ); + } + + Ok(()) +} + pub(crate) fn dump_output( output: &Output, output_dir: &Option, @@ -56,29 +112,29 @@ pub(crate) fn dump_output( ]; } return Ok(()); +} - fn writer_for( - out_dir: &Option, - name: &str, - ) -> io::Result<(Option, Box)> { - // create a writer for the provided output. - // If we have an output directory use that, otherwise just dump to stdout - use std::fs; - - Ok(match out_dir { - Some(dir) => { - fs::create_dir_all(&dir)?; - let mut of = dir.join(name); - of.set_extension("facts"); - (None, Box::new(fs::File::create(of)?)) - } - None => { - let mut stdout = io::stdout(); - write!(&mut stdout, "# {}\n", name)?; - (Some(name.to_string()), Box::new(stdout)) - } - }) - } +fn writer_for( + out_dir: &Option, + name: &str, +) -> io::Result<(Option, Box)> { + // create a writer for the provided output. + // If we have an output directory use that, otherwise just dump to stdout + use std::fs; + + Ok(match out_dir { + Some(dir) => { + fs::create_dir_all(&dir)?; + let mut of = dir.join(name); + of.set_extension("facts"); + (None, Box::new(fs::File::create(of)?)) + } + None => { + let mut stdout = io::stdout(); + write!(&mut stdout, "# {}\n", name)?; + (Some(name.to_string()), Box::new(stdout)) + } + }) } trait OutputDump { diff --git a/src/facts.rs b/src/facts.rs index 74f57ff6cb..7b23ac89ac 100644 --- a/src/facts.rs +++ b/src/facts.rs @@ -20,12 +20,24 @@ macro_rules! index_type { } } + impl From for $t { + fn from(index: u32) -> $t { + $t { index } + } + } + impl Into for $t { fn into(self) -> usize { self.index as usize } } + impl Into for $t { + fn into(self) -> u32 { + self.index + } + } + impl polonius_engine::Atom for $t { fn index(self) -> usize { self.into() diff --git a/src/program.rs b/src/program.rs index 8e0de05978..b76c0d5e72 100644 --- a/src/program.rs +++ b/src/program.rs @@ -14,7 +14,7 @@ use crate::intern::InternerTables; #[derive(Default)] struct Facts { loan_issued_at: BTreeSet<(Origin, Loan, Point)>, - universal_region: BTreeSet, + universal_region: BTreeSet<(Origin,)>, cfg_edge: BTreeSet<(Point, Point)>, loan_killed_at: BTreeSet<(Loan, Point)>, subset_base: BTreeSet<(Origin, Origin, Point)>, @@ -72,7 +72,7 @@ pub(crate) fn parse_from_program( input .placeholders .iter() - .map(|placeholder| tables.origins.intern(&placeholder.origin)), + .map(|placeholder| (tables.origins.intern(&placeholder.origin),)), ); // facts: placeholder(Origin, Loan) @@ -296,7 +296,7 @@ mod tests { let universal_regions: Vec<_> = facts .universal_region .iter() - .map(|origin| tables.origins.untern(*origin)) + .map(|origin| tables.origins.untern(origin.0)) .collect(); assert_eq!(universal_regions, ["'a", "'b", "'c"]); diff --git a/src/tab_delim.rs b/src/tab_delim.rs index be8f3e9718..61882719f5 100644 --- a/src/tab_delim.rs +++ b/src/tab_delim.rs @@ -90,6 +90,19 @@ where } } +impl<'input, T> FromTabDelimited<'input> for (T,) +where + &'input str: InternTo, +{ + fn parse( + tables: &mut InternerTables, + inputs: &mut dyn Iterator, + ) -> Option { + let input = inputs.next()?; + Some((InternTo::intern(tables, input),)) + } +} + impl<'input, T> FromTabDelimited<'input> for T where &'input str: InternTo, diff --git a/src/test.rs b/src/test.rs index 30d94bdf34..06975195e2 100644 --- a/src/test.rs +++ b/src/test.rs @@ -9,13 +9,20 @@ use crate::test_util::{ assert_checkers_match, assert_equal, assert_outputs_match, location_insensitive_checker_for, naive_checker_for, opt_checker_for, }; -use polonius_engine::Algorithm; +use polonius_engine::{Algorithm, Engine}; use rustc_hash::FxHashMap; use std::error::Error; use std::path::Path; fn test_facts(all_facts: &AllFacts, algorithms: &[Algorithm]) { - let naive = Output::compute(all_facts, Algorithm::Naive, true); + let naive = Output::compute(all_facts, Algorithm::Naive(Engine::Datafrog), true); + + #[cfg(feature = "polonius-souffle")] + { + let souffle_naive = Output::compute(all_facts, Algorithm::Naive(Engine::Souffle), false); + assert_equal(&naive.errors, &souffle_naive.errors); + assert_equal(&naive.subset_errors, &souffle_naive.subset_errors); + } // Check that the "naive errors" are a subset of the "insensitive // ones". @@ -49,7 +56,7 @@ fn test_facts(all_facts: &AllFacts, algorithms: &[Algorithm]) { for (naive_point, naive_origins) in &naive.subset_errors { // Potential location-insensitive errors don't have a meaningful location, and use 0 // as a default when debugging. - match insensitive.subset_errors.get(&0.into()) { + match insensitive.subset_errors.get(&0u32.into()) { Some(insensitive_origins) => { for &(origin1, origin2) in naive_origins { if !insensitive_origins.contains(&(origin1, origin2)) { @@ -82,6 +89,17 @@ fn test_facts(all_facts: &AllFacts, algorithms: &[Algorithm]) { assert_equal(&naive.errors, &opt.errors); assert_equal(&naive.subset_errors, &opt.subset_errors); assert_equal(&naive.move_errors, &opt.move_errors); + + #[cfg(feature = "polonius-souffle")] + if matches!( + optimized_algorithm, + Algorithm::DatafrogOpt(Engine::Datafrog) + ) { + let souffle_opt = + Output::compute(all_facts, Algorithm::DatafrogOpt(Engine::Souffle), false); + assert_equal(&opt.errors, &souffle_opt.errors); + assert_equal(&opt.subset_errors, &souffle_opt.subset_errors); + } } // The hybrid algorithm gets the same errors as the naive version @@ -111,7 +129,7 @@ macro_rules! tests { #[test] fn datafrog_opt() -> Result<(), Box> { - test_fn($dir, $fn, Algorithm::DatafrogOpt) + test_fn($dir, $fn, Algorithm::DatafrogOpt(Engine::Datafrog)) } } )* @@ -152,8 +170,8 @@ fn test_insensitive_errors() -> Result<(), Box> { let insensitive = Output::compute(&all_facts, Algorithm::LocationInsensitive, false); let mut expected = FxHashMap::default(); - expected.insert(Point::from(24), vec![Loan::from(1)]); - expected.insert(Point::from(50), vec![Loan::from(2)]); + expected.insert(Point::from(24u32), vec![Loan::from(1u32)]); + expected.insert(Point::from(50u32), vec![Loan::from(2u32)]); assert_equal(&insensitive.errors, &expected); Ok(()) @@ -168,7 +186,7 @@ fn test_sensitive_passes_issue_47680() -> Result<(), Box> { .join("main"); let tables = &mut intern::InternerTables::new(); let all_facts = tab_delim::load_tab_delimited_facts(tables, &facts_dir)?; - let sensitive = Output::compute(&all_facts, Algorithm::DatafrogOpt, false); + let sensitive = Output::compute(&all_facts, Algorithm::DatafrogOpt(Engine::Datafrog), false); assert!(sensitive.errors.is_empty()); @@ -201,7 +219,7 @@ fn no_subset_symmetries_exist() -> Result<(), Box> { false }; - let naive = Output::compute(&all_facts, Algorithm::Naive, true); + let naive = Output::compute(&all_facts, Algorithm::Naive(Engine::Datafrog), true); assert!(!subset_symmetries_exist(&naive)); // FIXME: the issue-47680 dataset is suboptimal here as DatafrogOpt does not @@ -209,7 +227,7 @@ fn no_subset_symmetries_exist() -> Result<(), Box> { // that the assert in verbose mode didn't trigger. Therefore, switch to this dataset // whenever it's fast enough to be enabled in tests, or somehow create a test facts program // or reduce it from clap. - let opt = Output::compute(&all_facts, Algorithm::DatafrogOpt, true); + let opt = Output::compute(&all_facts, Algorithm::DatafrogOpt(Engine::Datafrog), true); assert!(!subset_symmetries_exist(&opt)); Ok(()) } @@ -323,8 +341,8 @@ fn smoke_test_errors() { let facts = tab_delim::load_tab_delimited_facts(tables, &facts_dir).expect("facts"); let location_insensitive = Output::compute(&facts, Algorithm::LocationInsensitive, true); - let naive = Output::compute(&facts, Algorithm::Naive, true); - let opt = Output::compute(&facts, Algorithm::DatafrogOpt, true); + let naive = Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true); + let opt = Output::compute(&facts, Algorithm::DatafrogOpt(Engine::Datafrog), true); // We have to find errors with every analysis assert!( @@ -399,7 +417,8 @@ fn var_live_in_single_block() { let mut tables = intern::InternerTables::new(); let facts = parse_from_program(program, &mut tables).expect("Parsing failure"); - let liveness = Output::compute(&facts, Algorithm::Naive, true).var_live_on_entry; + let liveness = + Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true).var_live_on_entry; println!("Registered liveness data: {:?}", liveness); for (point, variables) in liveness.iter() { println!("{:?} has live variables: {:?}", point, variables); @@ -433,7 +452,8 @@ fn var_live_in_successor_propagates_to_predecessor() { let mut tables = intern::InternerTables::new(); let facts = parse_from_program(program, &mut tables).expect("Parsing failure"); - let liveness = Output::compute(&facts, Algorithm::Naive, true).var_live_on_entry; + let liveness = + Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true).var_live_on_entry; println!("Registered liveness data: {:?}", liveness); println!("CFG: {:?}", facts.cfg_edge); for (point, variables) in liveness.iter() { @@ -441,7 +461,7 @@ fn var_live_in_successor_propagates_to_predecessor() { assert_eq!(variables.len(), 1); } - assert!(!liveness.get(&0.into()).unwrap().is_empty()); + assert!(!liveness.get(&0u32.into()).unwrap().is_empty()); } #[test] @@ -470,12 +490,12 @@ fn var_live_in_successor_killed_by_reassignment() { let mut tables = intern::InternerTables::new(); let facts = parse_from_program(program, &mut tables).expect("Parsing failure"); - let result = Output::compute(&facts, Algorithm::Naive, true); + let result = Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true); println!("result: {:#?}", result); let liveness = result.var_live_on_entry; println!("CFG: {:#?}", facts.cfg_edge); - let first_defined: Point = 3.into(); // Mid(B1[0]) + let first_defined: Point = 3u32.into(); // Mid(B1[0]) for (&point, variables) in liveness.iter() { println!( @@ -486,10 +506,10 @@ fn var_live_in_successor_killed_by_reassignment() { ); } - let live_at_start = liveness.get(&0.into()); + let live_at_start = liveness.get(&0u32.into()); assert_eq!( - liveness.get(&0.into()), + liveness.get(&0u32.into()), None, "{:?} were live at start!", live_at_start.and_then(|var| Some(tables.variables.untern_vec(var))), @@ -531,11 +551,11 @@ fn var_drop_used_simple() { let mut tables = intern::InternerTables::new(); let facts = parse_from_program(program, &mut tables).expect("Parsing failure"); - let result = Output::compute(&facts, Algorithm::Naive, true); + let result = Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true); println!("result: {:#?}", result); let liveness = result.var_drop_live_on_entry; println!("CFG: {:#?}", facts.cfg_edge); - let first_defined: Point = 3.into(); // Mid(B1[0]) + let first_defined: Point = 3u32.into(); // Mid(B1[0]) for (&point, variables) in liveness.iter() { println!( @@ -546,10 +566,10 @@ fn var_drop_used_simple() { ); } - let live_at_start = liveness.get(&0.into()); + let live_at_start = liveness.get(&0u32.into()); assert_eq!( - liveness.get(&0.into()), + liveness.get(&0u32.into()), None, "{:?} were live at start!", live_at_start.and_then(|var| Some(tables.variables.untern_vec(var))), @@ -573,7 +593,7 @@ fn var_drop_used_simple() { fn illegal_subset_error() { let program = r" placeholders { 'a, 'b } - + block B0 { // creates a transitive `'b: 'a` subset loan_issued_at('x, L0), @@ -638,7 +658,7 @@ fn transitive_known_subset() { let program = r" placeholders { 'a, 'b, 'c } known_subsets { 'a: 'b, 'b: 'c } - + block B0 { loan_issued_at('x, L0), outlives('a: 'x), @@ -670,7 +690,7 @@ fn transitive_illegal_subset_error() { let program = r" placeholders { 'a, 'b, 'c } known_subsets { 'a: 'b } - + block B0 { // this transitive `'a: 'b` subset is already known loan_issued_at('x, L0), @@ -679,7 +699,7 @@ fn transitive_illegal_subset_error() { // creates unknown transitive subsets: // - `'b: 'c` - // - and therefore `'a: 'c` + // - and therefore `'a: 'c` loan_issued_at('y, L1), outlives('b: 'y), outlives('y: 'c); @@ -721,7 +741,7 @@ fn successes_in_subset_relations_dataset() { let tables = &mut intern::InternerTables::new(); let facts = tab_delim::load_tab_delimited_facts(tables, &facts_dir).expect("facts"); - let naive = Output::compute(&facts, Algorithm::Naive, true); + let naive = Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true); assert!(naive.errors.is_empty()); assert!(naive.subset_errors.is_empty()); @@ -729,7 +749,7 @@ fn successes_in_subset_relations_dataset() { assert!(insensitive.errors.is_empty()); assert!(insensitive.subset_errors.is_empty()); - let opt = Output::compute(&facts, Algorithm::DatafrogOpt, true); + let opt = Output::compute(&facts, Algorithm::DatafrogOpt(Engine::Datafrog), true); assert!(opt.errors.is_empty()); assert!(opt.subset_errors.is_empty()); } @@ -746,16 +766,16 @@ fn errors_in_subset_relations_dataset() { let facts = tab_delim::load_tab_delimited_facts(tables, &facts_dir).expect("facts"); // this function has no illegal access errors, but one subset error, over 3 points - let naive = Output::compute(&facts, Algorithm::Naive, true); + let naive = Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true); assert!(naive.errors.is_empty()); assert_eq!(naive.subset_errors.len(), 3); let expected_subset_error = { // in this dataset, `'a` is interned as `'1` - let origin_a = Origin::from(1); + let origin_a = Origin::from(1u32); // `'b` is interned as `'2` - let origin_b = Origin::from(2); + let origin_b = Origin::from(2u32); // and `'b` should flow into `'a` (origin_b, origin_a) @@ -782,7 +802,7 @@ fn errors_in_subset_relations_dataset() { assert!(insensitive_subset_errors.contains(&expected_subset_error)); // And the optimized analysis results should be the same as the naive one's. - let opt = Output::compute(&facts, Algorithm::Naive, true); + let opt = Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true); assert_outputs_match(&naive, &opt); } @@ -802,7 +822,7 @@ fn successes_in_move_errors_dataset() { let tables = &mut intern::InternerTables::new(); let facts = tab_delim::load_tab_delimited_facts(tables, &facts_dir).expect("facts"); - let naive = Output::compute(&facts, Algorithm::Naive, true); + let naive = Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true); assert!(naive.errors.is_empty()); assert!(naive.subset_errors.is_empty()); assert!(naive.move_errors.is_empty()); @@ -812,7 +832,7 @@ fn successes_in_move_errors_dataset() { assert!(insensitive.subset_errors.is_empty()); assert!(insensitive.move_errors.is_empty()); - let opt = Output::compute(&facts, Algorithm::DatafrogOpt, true); + let opt = Output::compute(&facts, Algorithm::DatafrogOpt(Engine::Datafrog), true); assert!(opt.errors.is_empty()); assert!(opt.subset_errors.is_empty()); assert!(opt.move_errors.is_empty()); @@ -829,7 +849,7 @@ fn basic_move_error() { let tables = &mut intern::InternerTables::new(); let facts = tab_delim::load_tab_delimited_facts(tables, &facts_dir).expect("facts"); - let result = Output::compute(&facts, Algorithm::Naive, true); + let result = Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true); assert!(result.errors.is_empty()); assert!(result.subset_errors.is_empty()); @@ -853,7 +873,7 @@ fn conditional_init() { let tables = &mut intern::InternerTables::new(); let facts = tab_delim::load_tab_delimited_facts(tables, &facts_dir).expect("facts"); - let result = Output::compute(&facts, Algorithm::Naive, true); + let result = Output::compute(&facts, Algorithm::Naive(Engine::Datafrog), true); assert!(result.errors.is_empty()); assert!(result.subset_errors.is_empty()); diff --git a/src/test_util.rs b/src/test_util.rs index f0f9c11e66..6ffd0d1cfe 100644 --- a/src/test_util.rs +++ b/src/test_util.rs @@ -1,6 +1,6 @@ #![cfg(test)] -use polonius_engine::{Algorithm, AllFacts, Output}; +use polonius_engine::{Algorithm, AllFacts, Engine, Output}; use std::fmt::Debug; use crate::facts::LocalFacts; @@ -76,7 +76,7 @@ pub(crate) fn check_program( } pub(crate) fn naive_checker_for(program: &str) -> FactChecker { - check_program(program, Algorithm::Naive, true) + check_program(program, Algorithm::Naive(Engine::Datafrog), true) } pub(crate) fn location_insensitive_checker_for(program: &str) -> FactChecker { @@ -84,7 +84,7 @@ pub(crate) fn location_insensitive_checker_for(program: &str) -> FactChecker { } pub(crate) fn opt_checker_for(program: &str) -> FactChecker { - check_program(program, Algorithm::DatafrogOpt, true) + check_program(program, Algorithm::DatafrogOpt(Engine::Datafrog), true) } pub(crate) fn assert_checkers_match(checker_a: &FactChecker, checker_b: &FactChecker) {