Skip to content

Commit c43c8f9

Browse files
author
Graham Enos
committed
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.
1 parent d022baa commit c43c8f9

File tree

3 files changed

+50
-111
lines changed

3 files changed

+50
-111
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 & 106 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,50 +108,52 @@ 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+
Expression::Address(m) => {
118135
"Address".hash(state);
119136
m.hash(state);
120137
}
121-
FunctionCall(FunctionCallExpression {
138+
Expression::FunctionCall(FunctionCallExpression {
122139
function,
123140
expression,
124141
}) => {
125142
"FunctionCall".hash(state);
126143
function.hash(state);
127144
expression.hash(state);
128145
}
129-
Infix(InfixExpression {
146+
Expression::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+
Expression::Number(n) => {
153157
"Number".hash(state);
154158
// Skip zero values (akin to `format_complex`).
155159
// Also, since f64 isn't hashable, use the u64 binary representation.
@@ -161,31 +165,22 @@ impl Hash for Expression {
161165
hash_f64(n.im, state)
162166
}
163167
}
164-
PiConstant => {
168+
Expression::PiConstant => {
165169
"PiConstant".hash(state);
166170
}
167-
Prefix(p) => {
171+
Expression::Prefix(p) => {
168172
"Prefix".hash(state);
169173
p.operator.hash(state);
170174
p.expression.hash(state);
171175
}
172-
Variable(v) => {
176+
Expression::Variable(v) => {
173177
"Variable".hash(state);
174178
v.hash(state);
175179
}
176180
}
177181
}
178182
}
179183

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-
189184
macro_rules! impl_expr_op {
190185
($name:ident, $name_assign:ident, $function:ident, $function_assign:ident, $operator:ident) => {
191186
impl $name for Expression {
@@ -215,11 +210,7 @@ impl_expr_op!(Mul, MulAssign, mul, mul_assign, Star);
215210
impl_expr_op!(Div, DivAssign, div, div_assign, Slash);
216211

217212
/// 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 {
213+
fn calculate_infix(left: &Complex64, operator: &InfixOperator, right: &Complex64) -> Complex64 {
223214
use InfixOperator::*;
224215
match operator {
225216
Caret => left.powc(*right),
@@ -231,10 +222,7 @@ fn calculate_infix(
231222
}
232223

233224
/// 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 {
225+
fn calculate_function(function: &ExpressionFunction, argument: &Complex64) -> Complex64 {
238226
use ExpressionFunction::*;
239227
match function {
240228
Sine => argument.sin(),
@@ -323,9 +311,9 @@ impl Expression {
323311
/// ```
324312
pub fn evaluate(
325313
&self,
326-
variables: &HashMap<String, num_complex::Complex64>,
314+
variables: &HashMap<String, Complex64>,
327315
memory_references: &HashMap<&str, Vec<f64>>,
328-
) -> Result<num_complex::Complex64, EvaluationError> {
316+
) -> Result<Complex64, EvaluationError> {
329317
use Expression::*;
330318

331319
match self {
@@ -660,18 +648,11 @@ impl fmt::Display for InfixOperator {
660648

661649
#[cfg(test)]
662650
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-
674651
use super::*;
652+
use crate::hash::hash_to_u64;
653+
use crate::reserved::ReservedToken;
654+
use proptest::prelude::*;
655+
use std::collections::HashSet;
675656

676657
#[test]
677658
fn simplify_and_evaluate() {
@@ -862,21 +843,6 @@ mod tests {
862843
prop_assert_ne!(&first, &differing);
863844
}
864845

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-
880846
#[test]
881847
fn hash(a in any::<f64>(), b in any::<f64>()) {
882848
let first = Expression::Infix (InfixExpression {
@@ -892,36 +858,9 @@ mod tests {
892858
assert!(!set.contains(&differing))
893859
}
894860

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-
912861
#[test]
913862
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);
863+
prop_assert_eq!(x == y, hash_to_u64(&x) == hash_to_u64(&y));
925864
}
926865

927866
#[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)