Skip to content

Commit fd27679

Browse files
authored
feat!: Decouple expression hashing and equality (#277)
* feat!: Decouple expression hashing and equality BREAKING: Expressions no longer use hashing for implementing equality BREAKING: Expression equality no longer takes commutativity into account In #276, @Shadow53 noted > One thing that may be useful enough to pull into its own PR is the > change to not use hashing in the implementation of `PartialEq` for > `Expression`, which also helps speed things up. We originally put this together in #27 to ensure that equality held in the face of commutativity, e.g., `1 + x == x + 1`. In addition to the performance benefits of decoupling the hashing and equality implementations, it makes sense to remove any special status for commutative operations in light of all the work we're doing on expression simplification. If we wished to ensure expressions are `Eq` if and only if they represent the same mathematical expression, we'd have to have equality contingent upon simplification, which would be even more costly. * fix: Use Self::
1 parent 02c59f3 commit fd27679

File tree

3 files changed

+50
-113
lines changed

3 files changed

+50
-113
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

quil-rs/src/expression/mod.rs

Lines changed: 45 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313
// limitations under the License.
1414

1515
use crate::{
16-
hash::{hash_f64, hash_to_u64},
16+
hash::hash_f64,
17+
imag,
18+
instruction::MemoryReference,
1719
parser::{lex, parse_expression, ParseError},
1820
program::{disallow_leftover, ParseProgramError},
19-
{imag, instruction::MemoryReference, real},
21+
real,
2022
};
2123
use lexical::{format, to_string_with_options, WriteFloatOptions};
2224
use nom_locate::LocatedSpan;
@@ -53,13 +55,13 @@ pub enum Expression {
5355
Address(MemoryReference),
5456
FunctionCall(FunctionCallExpression),
5557
Infix(InfixExpression),
56-
Number(num_complex::Complex64),
58+
Number(Complex64),
5759
PiConstant,
5860
Prefix(PrefixExpression),
5961
Variable(String),
6062
}
6163

62-
#[derive(Clone, Debug)]
64+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
6365
pub struct FunctionCallExpression {
6466
pub function: ExpressionFunction,
6567
pub expression: Box<Expression>,
@@ -74,7 +76,7 @@ impl FunctionCallExpression {
7476
}
7577
}
7678

77-
#[derive(Clone, Debug)]
79+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
7880
pub struct InfixExpression {
7981
pub left: Box<Expression>,
8082
pub operator: InfixOperator,
@@ -91,7 +93,7 @@ impl InfixExpression {
9193
}
9294
}
9395

94-
#[derive(Clone, Debug)]
96+
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
9597
pub struct PrefixExpression {
9698
pub operator: PrefixOperator,
9799
pub expression: Box<Expression>,
@@ -106,86 +108,77 @@ impl PrefixExpression {
106108
}
107109
}
108110

111+
impl PartialEq for Expression {
112+
// Implemented by hand since we can't derive with f64s hidden inside.
113+
fn eq(&self, other: &Self) -> bool {
114+
match (self, other) {
115+
(Self::Address(left), Self::Address(right)) => left == right,
116+
(Self::Infix(left), Self::Infix(right)) => left == right,
117+
(Self::Number(left), Self::Number(right)) => left == right,
118+
(Self::Prefix(left), Self::Prefix(right)) => left == right,
119+
(Self::FunctionCall(left), Self::FunctionCall(right)) => left == right,
120+
(Self::Variable(left), Self::Variable(right)) => left == right,
121+
(Self::PiConstant, Self::PiConstant) => true,
122+
_ => false,
123+
}
124+
}
125+
}
126+
127+
// Implemented by hand since we can't derive with f64s hidden inside.
128+
impl Eq for Expression {}
129+
109130
impl Hash for Expression {
110131
// Implemented by hand since we can't derive with f64s hidden inside.
111-
// Also to understand when things should be the same, like with commutativity (`1 + 2 == 2 + 1`).
112-
// See https://github.com/rigetti/quil-rust/issues/27
113132
fn hash<H: Hasher>(&self, state: &mut H) {
114-
use std::cmp::{max_by_key, min_by_key};
115-
use Expression::*;
116133
match self {
117-
Address(m) => {
134+
Self::Address(m) => {
118135
"Address".hash(state);
119136
m.hash(state);
120137
}
121-
FunctionCall(FunctionCallExpression {
138+
Self::FunctionCall(FunctionCallExpression {
122139
function,
123140
expression,
124141
}) => {
125142
"FunctionCall".hash(state);
126143
function.hash(state);
127144
expression.hash(state);
128145
}
129-
Infix(InfixExpression {
146+
Self::Infix(InfixExpression {
130147
left,
131148
operator,
132149
right,
133150
}) => {
134151
"Infix".hash(state);
135152
operator.hash(state);
136-
match operator {
137-
InfixOperator::Plus | InfixOperator::Star => {
138-
// commutative, so put left & right in decreasing order by hash value
139-
let (a, b) = (
140-
min_by_key(&left, &right, hash_to_u64),
141-
max_by_key(&left, &right, hash_to_u64),
142-
);
143-
a.hash(state);
144-
b.hash(state);
145-
}
146-
_ => {
147-
left.hash(state);
148-
right.hash(state);
149-
}
150-
}
153+
left.hash(state);
154+
right.hash(state);
151155
}
152-
Number(n) => {
156+
Self::Number(n) => {
153157
"Number".hash(state);
154158
// Skip zero values (akin to `format_complex`).
155-
// Also, since f64 isn't hashable, use the u64 binary representation.
156-
// The docs claim this is rather portable: https://doc.rust-lang.org/std/primitive.f64.html#method.to_bits
157159
if n.re.abs() > 0f64 {
158160
hash_f64(n.re, state)
159161
}
160162
if n.im.abs() > 0f64 {
161163
hash_f64(n.im, state)
162164
}
163165
}
164-
PiConstant => {
166+
Self::PiConstant => {
165167
"PiConstant".hash(state);
166168
}
167-
Prefix(p) => {
169+
Self::Prefix(p) => {
168170
"Prefix".hash(state);
169171
p.operator.hash(state);
170172
p.expression.hash(state);
171173
}
172-
Variable(v) => {
174+
Self::Variable(v) => {
173175
"Variable".hash(state);
174176
v.hash(state);
175177
}
176178
}
177179
}
178180
}
179181

180-
impl PartialEq for Expression {
181-
// Partial equality by hash value
182-
fn eq(&self, other: &Self) -> bool {
183-
hash_to_u64(self) == hash_to_u64(other)
184-
}
185-
}
186-
187-
impl Eq for Expression {}
188-
189182
macro_rules! impl_expr_op {
190183
($name:ident, $name_assign:ident, $function:ident, $function_assign:ident, $operator:ident) => {
191184
impl $name for Expression {
@@ -215,11 +208,7 @@ impl_expr_op!(Mul, MulAssign, mul, mul_assign, Star);
215208
impl_expr_op!(Div, DivAssign, div, div_assign, Slash);
216209

217210
/// Compute the result of an infix expression where both operands are complex.
218-
fn calculate_infix(
219-
left: &num_complex::Complex64,
220-
operator: &InfixOperator,
221-
right: &num_complex::Complex64,
222-
) -> num_complex::Complex64 {
211+
fn calculate_infix(left: &Complex64, operator: &InfixOperator, right: &Complex64) -> Complex64 {
223212
use InfixOperator::*;
224213
match operator {
225214
Caret => left.powc(*right),
@@ -231,10 +220,7 @@ fn calculate_infix(
231220
}
232221

233222
/// Compute the result of a Quil-defined expression function where the operand is complex.
234-
fn calculate_function(
235-
function: &ExpressionFunction,
236-
argument: &num_complex::Complex64,
237-
) -> num_complex::Complex64 {
223+
fn calculate_function(function: &ExpressionFunction, argument: &Complex64) -> Complex64 {
238224
use ExpressionFunction::*;
239225
match function {
240226
Sine => argument.sin(),
@@ -323,9 +309,9 @@ impl Expression {
323309
/// ```
324310
pub fn evaluate(
325311
&self,
326-
variables: &HashMap<String, num_complex::Complex64>,
312+
variables: &HashMap<String, Complex64>,
327313
memory_references: &HashMap<&str, Vec<f64>>,
328-
) -> Result<num_complex::Complex64, EvaluationError> {
314+
) -> Result<Complex64, EvaluationError> {
329315
use Expression::*;
330316

331317
match self {
@@ -660,18 +646,11 @@ impl fmt::Display for InfixOperator {
660646

661647
#[cfg(test)]
662648
mod tests {
663-
use std::collections::{hash_map::DefaultHasher, HashSet};
664-
665-
use num_complex::Complex64;
666-
use proptest::prelude::*;
667-
668-
use crate::{
669-
expression::{EvaluationError, Expression, ExpressionFunction},
670-
real,
671-
reserved::ReservedToken,
672-
};
673-
674649
use super::*;
650+
use crate::hash::hash_to_u64;
651+
use crate::reserved::ReservedToken;
652+
use proptest::prelude::*;
653+
use std::collections::HashSet;
675654

676655
#[test]
677656
fn simplify_and_evaluate() {
@@ -862,21 +841,6 @@ mod tests {
862841
prop_assert_ne!(&first, &differing);
863842
}
864843

865-
#[test]
866-
fn eq_commutative(a in any::<f64>(), b in any::<f64>()) {
867-
let first = Expression::Infix(InfixExpression {
868-
left: Box::new(Expression::Number(real!(a))),
869-
operator: InfixOperator::Plus,
870-
right: Box::new(Expression::Number(real!(b))),
871-
} );
872-
let second = Expression::Infix(InfixExpression {
873-
left: Box::new(Expression::Number(real!(b))),
874-
operator: InfixOperator::Plus,
875-
right: Box::new(Expression::Number(real!(a))),
876-
});
877-
prop_assert_eq!(first, second);
878-
}
879-
880844
#[test]
881845
fn hash(a in any::<f64>(), b in any::<f64>()) {
882846
let first = Expression::Infix (InfixExpression {
@@ -892,36 +856,9 @@ mod tests {
892856
assert!(!set.contains(&differing))
893857
}
894858

895-
#[test]
896-
fn hash_commutative(a in any::<f64>(), b in any::<f64>()) {
897-
let first = Expression::Infix(InfixExpression {
898-
left: Box::new(Expression::Number(real!(a))),
899-
operator: InfixOperator::Plus,
900-
right: Box::new(Expression::Number(real!(b))),
901-
} );
902-
let second = Expression::Infix(InfixExpression {
903-
left: Box::new(Expression::Number(real!(b))),
904-
operator: InfixOperator::Plus,
905-
right: Box::new(Expression::Number(real!(a))),
906-
} );
907-
let mut set = HashSet::new();
908-
set.insert(first);
909-
assert!(set.contains(&second));
910-
}
911-
912859
#[test]
913860
fn eq_iff_hash_eq(x in arb_expr(), y in arb_expr()) {
914-
let h_x = {
915-
let mut s = DefaultHasher::new();
916-
x.hash(&mut s);
917-
s.finish()
918-
};
919-
let h_y = {
920-
let mut s = DefaultHasher::new();
921-
y.hash(&mut s);
922-
s.finish()
923-
};
924-
prop_assert_eq!(x == y, h_x == h_y);
861+
prop_assert_eq!(x == y, hash_to_u64(&x) == hash_to_u64(&y));
925862
}
926863

927864
#[test]

quil-rs/src/expression/simplification.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/// Complex machinery for simplifying [`Expression`]s.
22
use crate::{
33
expression::{
4-
format_complex, hash_to_u64, imag, is_small, real, Expression, ExpressionFunction,
5-
FunctionCallExpression, InfixExpression, InfixOperator, MemoryReference, PrefixExpression,
6-
PrefixOperator,
4+
format_complex, is_small, Expression, ExpressionFunction, FunctionCallExpression,
5+
InfixExpression, InfixOperator, MemoryReference, PrefixExpression, PrefixOperator,
76
},
8-
hash::hash_f64,
7+
hash::{hash_f64, hash_to_u64},
8+
imag, real,
99
};
1010
use egg::{define_language, rewrite as rw, Id, Language, RecExpr};
1111
use once_cell::sync::Lazy;

0 commit comments

Comments
 (0)