Skip to content

Commit

Permalink
re-organize crates similar to parry to get separate f32/f64 crates
Browse files Browse the repository at this point in the history
I removed the doc test in lib.rs because I couldn't figure out how
to have correct imports when the crate name is not a constant.
  • Loading branch information
bonsairobo committed Dec 22, 2023
1 parent 18a56ab commit 883ba47
Show file tree
Hide file tree
Showing 8 changed files with 108 additions and 96 deletions.
21 changes: 8 additions & 13 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
[package]
name = "vector-expr"
authors = ["Duncan Fairbanks <duncan.fairbanks@foresightmining.com>"]
version = "0.2.0"
edition = "2021"
description = "Vectorized expression parser and evaluator"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ForesightMiningSoftwareCorporation/vector_expr"
readme = "README.md"
[workspace]
members = ["crates/vector-expr", "crates/vector-expr-f64"]
resolver = "2"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[patch.crates-io]
vector-expr = { path = "crates/vector-expr" }
vector-expr-f64 = { path = "crates/vector-expr-f64" }

[dependencies]
[workspace.dependencies]
once_cell = "1.19.0"
pest = "2.7.5"
pest_derive = "2.7.5"

rayon = { version = "1", optional = true }
rayon = "1"
23 changes: 1 addition & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,6 @@ parallelism via the `rayon` feature).

## Example

```rust
use vector_expr::*;

fn binding_map(var_name: &str) -> BindingId {
match var_name {
"bar" => 0,
"baz" => 1,
"foo" => 2,
_ => unreachable!(),
}
}
let parsed = Expression::parse("2 * (foo + bar) * baz", &binding_map).unwrap();
let real = parsed.unwrap_real();

let bar = [1.0, 2.0, 3.0];
let baz = [4.0, 5.0, 6.0];
let foo = [7.0, 8.0, 9.0];
let bindings: &[&[f64]] = &[&bar, &baz, &foo];
let mut registers = Registers::new(3);
let output = real.evaluate(bindings, &mut registers);
assert_eq!(&output, &[64.0, 100.0, 144.0]);
```
See unit tests in `src/lib.rs`.

License: MIT OR Apache-2.0
25 changes: 25 additions & 0 deletions crates/vector-expr-f64/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "vector-expr-f64"
authors = ["Duncan Fairbanks <duncan.fairbanks@foresightmining.com>"]
version = "0.2.0"
edition = "2021"
description = "Vectorized expression parser and evaluator"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ForesightMiningSoftwareCorporation/vector_expr"
readme = "../../README.md"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "vector_expr_f64"
path = "../../src/lib.rs"

[features]
default = ["f64"]
f64 = []

[dependencies]
once_cell = { workspace = true }
pest = { workspace = true }
pest_derive = { workspace = true }
rayon = { workspace = true, optional = true }
26 changes: 26 additions & 0 deletions crates/vector-expr/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "vector-expr"
authors = ["Duncan Fairbanks <duncan.fairbanks@foresightmining.com>"]
version = "0.2.0"
edition = "2021"
description = "Vectorized expression parser and evaluator"
license = "MIT OR Apache-2.0"
repository = "https://github.com/ForesightMiningSoftwareCorporation/vector_expr"
readme = "../../README.md"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "vector_expr"
path = "../../src/lib.rs"

[features]
default = ["f32"]
f32 = []

[dependencies]
once_cell = { workspace = true }
pest = { workspace = true }
pest_derive = { workspace = true }
rayon = { workspace = true, optional = true }

43 changes: 24 additions & 19 deletions src/evaluate.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::real::Real;
use crate::{BoolExpression, RealExpression, StringExpression};

#[cfg(feature = "rayon")]
Expand All @@ -10,7 +11,7 @@ pub type StringId = u32;

impl BoolExpression {
/// Calculates the `bool`-valued results of the expression component-wise.
pub fn evaluate<R: AsRef<[f64]>, S: AsRef<[StringId]>>(
pub fn evaluate<R: AsRef<[Real]>, S: AsRef<[StringId]>>(
&self,
real_bindings: &[R],
string_bindings: &[S],
Expand All @@ -27,7 +28,7 @@ impl BoolExpression {
)
}

fn evaluate_recursive<R: AsRef<[f64]>, S: AsRef<[StringId]>>(
fn evaluate_recursive<R: AsRef<[Real]>, S: AsRef<[StringId]>>(
&self,
real_bindings: &[R],
string_bindings: &[S],
Expand Down Expand Up @@ -124,21 +125,25 @@ impl BoolExpression {
}

impl RealExpression {
pub fn evaluate_without_vars(&self, registers: &mut Registers) -> Vec<f64> {
pub fn evaluate_without_vars(&self, registers: &mut Registers) -> Vec<Real> {
self.evaluate::<[_; 0]>(&[], registers)
}

/// Calculates the real-valued results of the expression component-wise.
pub fn evaluate<R: AsRef<[f64]>>(&self, bindings: &[R], registers: &mut Registers) -> Vec<f64> {
pub fn evaluate<R: AsRef<[Real]>>(
&self,
bindings: &[R],
registers: &mut Registers,
) -> Vec<Real> {
validate_bindings(bindings, registers.register_length);
self.evaluate_recursive(bindings, registers)
}

fn evaluate_recursive<R: AsRef<[f64]>>(
fn evaluate_recursive<R: AsRef<[Real]>>(
&self,
bindings: &[R],
registers: &mut Registers,
) -> Vec<f64> {
) -> Vec<Real> {
match self {
Self::Add(lhs, rhs) => evaluate_binary_real_op(
|lhs, rhs| lhs + rhs,
Expand Down Expand Up @@ -200,13 +205,13 @@ fn validate_bindings<T, B: AsRef<[T]>>(input_bindings: &[B], expected_length: us
}
}

fn evaluate_binary_real_op<R: AsRef<[f64]>>(
op: fn(f64, f64) -> f64,
fn evaluate_binary_real_op<R: AsRef<[Real]>>(
op: fn(Real, Real) -> Real,
lhs: &RealExpression,
rhs: &RealExpression,
bindings: &[R],
registers: &mut Registers,
) -> Vec<f64> {
) -> Vec<Real> {
// Before doing recursive evaluation, we check first if we already have
// input values in our bindings. This avoids unnecessary copies.
let mut lhs_reg = None;
Expand Down Expand Up @@ -254,12 +259,12 @@ fn evaluate_binary_real_op<R: AsRef<[f64]>>(
output
}

fn evaluate_unary_real_op<R: AsRef<[f64]>>(
op: fn(f64) -> f64,
fn evaluate_unary_real_op<R: AsRef<[Real]>>(
op: fn(Real) -> Real,
only: &RealExpression,
bindings: &[R],
registers: &mut Registers,
) -> Vec<f64> {
) -> Vec<Real> {
// Before doing recursive evaluation, we check first if we already have
// input values in our bindings. This avoids unnecessary copies.
let mut only_reg = None;
Expand Down Expand Up @@ -287,8 +292,8 @@ fn evaluate_unary_real_op<R: AsRef<[f64]>>(
output
}

fn evaluate_real_comparison<R: AsRef<[f64]>>(
op: fn(f64, f64) -> bool,
fn evaluate_real_comparison<R: AsRef<[Real]>>(
op: fn(Real, Real) -> bool,
lhs: &RealExpression,
rhs: &RealExpression,
bindings: &[R],
Expand Down Expand Up @@ -402,7 +407,7 @@ fn evaluate_string_comparison<S: AsRef<[StringId]>>(
output
}

fn evaluate_binary_logic<R: AsRef<[f64]>, S: AsRef<[StringId]>>(
fn evaluate_binary_logic<R: AsRef<[Real]>, S: AsRef<[StringId]>>(
op: fn(bool, bool) -> bool,
lhs: &BoolExpression,
rhs: &BoolExpression,
Expand Down Expand Up @@ -451,7 +456,7 @@ fn evaluate_binary_logic<R: AsRef<[f64]>, S: AsRef<[StringId]>>(
output
}

fn evaluate_unary_logic<R: AsRef<[f64]>, S: AsRef<[StringId]>>(
fn evaluate_unary_logic<R: AsRef<[Real]>, S: AsRef<[StringId]>>(
op: fn(bool) -> bool,
only: &BoolExpression,
real_bindings: &[R],
Expand Down Expand Up @@ -489,7 +494,7 @@ fn evaluate_unary_logic<R: AsRef<[f64]>, S: AsRef<[StringId]>>(
/// calculations have finished.
pub struct Registers {
num_allocations: usize,
real_registers: Vec<Vec<f64>>,
real_registers: Vec<Vec<Real>>,
bool_registers: Vec<Vec<bool>>,
string_registers: Vec<Vec<StringId>>,
register_length: usize,
Expand All @@ -506,7 +511,7 @@ impl Registers {
}
}

fn recycle_real(&mut self, mut used: Vec<f64>) {
fn recycle_real(&mut self, mut used: Vec<Real>) {
used.clear();
self.real_registers.push(used);
}
Expand All @@ -521,7 +526,7 @@ impl Registers {
self.string_registers.push(used);
}

fn allocate_real(&mut self) -> Vec<f64> {
fn allocate_real(&mut self) -> Vec<Real> {
self.real_registers.pop().unwrap_or_else(|| {
self.num_allocations += 1;
Vec::with_capacity(self.register_length)
Expand Down
9 changes: 6 additions & 3 deletions src/expression.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use crate::real::Real;

/// Top-level parseable calculation.
#[derive(Clone, Debug)]
pub enum Expression {
Expand Down Expand Up @@ -29,7 +31,7 @@ pub enum BoolExpression {
StrNotEqual(StringExpression, StringExpression),
}

/// An `f64`-valued expression.
/// A `Real`-valued expression.
#[derive(Clone, Debug)]
pub enum RealExpression {
// Binary real ops.
Expand All @@ -43,7 +45,7 @@ pub enum RealExpression {
Neg(Box<RealExpression>),

// Constant.
Literal(f64),
Literal(Real),

// Input variable.
Binding(BindingId),
Expand All @@ -55,5 +57,6 @@ pub enum StringExpression {
Binding(BindingId),
}

/// Index into the `&[&[f64]]` bindings passed to expression evaluation.
/// Index into the `&[&[Real]]` or `&[&[StringId]]` bindings passed to
/// expression evaluation.
pub type BindingId = usize;
52 changes: 15 additions & 37 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,14 @@
//! Vectorized math expression parser/evaluator.
//!
//! # Why?
//!
//! Performance. Evaluation of math expressions involving many variables can
//! incur significant overhead from traversing the expression tree or performing
//! variable lookups. We amortize that cost by performing intermediate
//! operations on _vectors_ of input data at a time (with optional data
//! parallelism via the `rayon` feature).
//!
//! # Example
//!
//! ```rust
//! use vector_expr::*;
//!
//! fn binding_map(var_name: &str) -> BindingId {
//! match var_name {
//! "bar" => 0,
//! "baz" => 1,
//! "foo" => 2,
//! _ => unreachable!(),
//! }
//! }
//! let parsed = Expression::parse("2 * (foo + bar) * baz", binding_map).unwrap();
//! let real = parsed.unwrap_real();
//!
//! let bar = [1.0, 2.0, 3.0];
//! let baz = [4.0, 5.0, 6.0];
//! let foo = [7.0, 8.0, 9.0];
//! let bindings: &[&[f64]] = &[&bar, &baz, &foo];
//! let mut registers = Registers::new(3);
//! let output = real.evaluate(bindings, &mut registers);
//! assert_eq!(&output, &[64.0, 100.0, 144.0]);
//! ```
#![doc = include_str!("../README.md")]

mod real {
/// The scalar type used throughout this crate.
#[cfg(feature = "f64")]
pub type Real = f64;

/// The scalar type used throughout this crate.
#[cfg(feature = "f32")]
pub type Real = f32;
}

mod evaluate;
mod expression;
Expand All @@ -55,6 +32,7 @@ pub fn empty_binding_map(_var_name: &str) -> BindingId {

#[cfg(test)]
mod tests {
use super::real::Real;
use super::*;

#[test]
Expand Down Expand Up @@ -198,9 +176,9 @@ mod tests {
let real = parsed.unwrap_real();

const LEN: i32 = 10_000_000;
let x: Vec<_> = (0..LEN).map(|i| i as f64).collect();
let y: Vec<_> = (0..LEN).map(|i| (LEN - i) as f64).collect();
let z: Vec<_> = (0..LEN).map(|i| ((LEN / 2) - i) as f64).collect();
let x: Vec<_> = (0..LEN).map(|i| i as Real).collect();
let y: Vec<_> = (0..LEN).map(|i| (LEN - i) as Real).collect();
let z: Vec<_> = (0..LEN).map(|i| ((LEN / 2) - i) as Real).collect();
let bindings = &[x, y, z];

let mut registers = Registers::new(LEN as usize);
Expand Down
5 changes: 3 additions & 2 deletions src/parse.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::expression::{BindingId, BoolExpression, Expression, RealExpression};
use crate::real::Real;
use crate::StringExpression;
use once_cell::sync::Lazy;
use pest::iterators::Pairs;
Expand All @@ -8,7 +9,7 @@ use pest_derive::Parser;
use std::collections::HashSet;

#[derive(Parser)]
#[grammar = "grammar.pest"] // relative to project `src`
#[grammar = "../../src/grammar.pest"] // relative to workspace `src`
struct ExpressionParser;

// Boxed because error is much larger than Ok variant in most results.
Expand Down Expand Up @@ -97,7 +98,7 @@ fn parse_recursive(pairs: Pairs<Rule>, binding_map: &impl Fn(&str) -> BindingId)
Rule::string_expr => parse_recursive(pair.into_inner(), binding_map),
Rule::real_literal => {
let literal_str = pair.as_str();
if let Ok(value) = literal_str.parse::<f64>() {
if let Ok(value) = literal_str.parse::<Real>() {
return Expression::Real(RealExpression::Literal(value));
}
panic!("Unexpected literal: {}", literal_str)
Expand Down

0 comments on commit 883ba47

Please sign in to comment.