Skip to content

Commit da28694

Browse files
committed
rename get_float macro to get_primitive_float
rename get_int macro to get_primitive_int rename compare_int to compare_numeric refactor NUM_LT, NUM_LTE, NUM_GT, NUM_GTE to use compare_numeric and removed helper macro updated equality docs implemented clojure-style = and == and not= implemented identical
1 parent 3563510 commit da28694

File tree

7 files changed

+166
-96
lines changed

7 files changed

+166
-96
lines changed

compile_state/src/state.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,6 @@ Example:
379379
(test::assert-equal 0 (+))
380380
(test::assert-equal 5 (+ 5))
381381
(test::assert-equal 10 (+ 5 5))
382-
(test::assert-equal 5 (+ 5.0))
383382
(test::assert-equal 6 (+ 1 5))
384383
(test::assert-equal 6.5 (+ 1 5.5))
385384
(test::assert-equal 7 (+ 1 2 4))
@@ -750,6 +749,8 @@ Example:
750749
(test::assert-true (not= 1.1 1.0))
751750
(test::assert-false (not= 1.1 1.1))
752751
(test::assert-true (not= 3 2 3))
752+
(test::assert-true (not= 1 1.0))
753+
(test::assert-true (not= 1e-30 1.0001e-30))
753754
"#),
754755
numlt: add_special(vm, "<", r#"Usage: (< val0 ... valN)
755756

doc/src/equality.md

Lines changed: 60 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,77 @@
11
# Equality
22

3-
When testing for equality in Slosh, there are several functions that can be used.
4-
Consider invocations like
5-
```slosh
6-
(= 1 2)
7-
```
8-
```slosh
9-
(== 1 2)
10-
```
11-
```slosh
12-
(identical? 1 2)
13-
```
14-
```slosh
15-
(assert-equal 1 2)
16-
```
17-
these are translated to different opcodes and equality implementations as listed below
18-
19-
- `equal?`
20-
21-
- lenient
22-
- `state.rs` defines special forms object with key `equal` mapped to the name `equal?`.
3+
The most common way to test equality is with `=`
4+
For numeric equality (IEEE) use `==`
5+
For bytewise equality use `identical?`
6+
7+
The behavior and names are based on Clojure's implementation. Read their docs here: https://clojure.org/guides/equality
8+
9+
- `=`
10+
- `(= 2 0x2)` is `true` (comparing an int to a byte)
11+
- `(= 2 2.0)` is `false` (comparing an int to a float)
12+
- `(= NaN NaN)` is `false`
13+
- `state.rs` defines special forms object with key `equal` mapped to the name `=`.
2314
- `compile.rs` matches on `env.specials().equal` and generates opcode `EQUAL`
2415
- `exec_loop.rs` maps opcode `EQUAL` to function `is_equal`
2516
- `vm.rs` `is_equal` converts each arg to a `Value` (in case it needs to be dereferenced) and calls `is_equal_pair`
2617
- `vm.rs` `is_equal_pair` does a complex test for equality
2718
- check if both args are Byte or Int and if so, converts both to `i64` with `Value::get_int` and compares with rust native `==`
2819
- check if both args are Byte or Int or Float and if so, converts both to `f64` with `Value::get_float`
2920
- then subtracts the second from the first and takes the absolute value and tests `diff == 0.0`
21+
- `==`
22+
23+
- `(== 1 1.0)` is `true` (comparing an int to a float)
24+
- `(== NaN NaN)` is `false`
25+
- returns true whenever `=` does, but also returns true for numbers that are numerically equal
26+
27+
- when comparing two floats, converts two both to `f64` and compares with native f64 `==`
28+
- does not use F56::PartialEq implementation
29+
30+
- `state.rs` defines special forms object with key `numeq` mapped to the name `==`.
31+
- `compile.rs` calls `compile_list` which calls `compile_special` which calls `compile_math` in `compile_math.rs`
32+
- `compile_math.rs` `compile_math` matches on `env.specials().numeq` and generates opcode `NUMEQ`
33+
- `exec_loop.rs` maps opcode `NUMEQ` to function `compare_numeric` and passes a comparator `|a,b| a == b`
34+
- `macros.rs` `compare_numeric`
35+
- checks if either argument is a `Float` and if so, converts both to `f64` with `get_primitive_float` macro and uses the comparator
36+
- checks if either argument is a `Int` and if so, converts both to `i64` with `get_primitive_int` macro and uses the comparator
37+
38+
- `identical?`
39+
40+
- uses `Value::PartialEq` implementation
41+
- `state.rs` defines special forms object with key `eq` mapped to the name `identical?`.
42+
- `compile.rs` matches on `env.specials().eq` and generates opcode `EQ`
43+
- `exec_loop.rs` maps opcode `EQ` to function `is_identical`
44+
- `vm.rs` `is_identical` converts each arg to a `Value` (in case it needs to be dereferenced) and compares `val1 == val2` which uses `Value::PartialEq` implementation
3045

3146
- `assert-equal`
3247

3348
- based on `equal?`
3449
- is a macro defined in core.slosh which checks if the arguments are `equal?` and throws an error if they are not
3550

36-
- `eq?`
51+
- `not=`
52+
- `(not= 1 2)` is equivalent to `(not (= 1 2))`
53+
- `(not= 1 1.0)` is `true` (comparing an int to a float)
54+
- `state.rs` defines special forms object with key `numneq` mapped to the name `not=`.
55+
- `compile_math.rs` converts `numneq` to opcode `NUMNEQ`
56+
- `exec_loop.rs` maps opcode `NUMNEQ` to a negation of the implementation of opcode `EQUAL` AKA `=`
3757

38-
- uses `Value::PartialEq` implementation
39-
- `state.rs` defines special forms object with key `eq` mapped to the name `eq?`.
40-
- `compile.rs` matches on `env.specials().eq` and generates opcode `EQ`
41-
- `exec_loop.rs` maps opcode `EQ` to function `is_eq`
42-
- `vm.rs` `is_eq` converts each arg to a `Value` and compares `val1 == val2` which uses `Value::PartialEq` implementation
58+
### Moving Forward
4359

44-
- `=`
45-
- numeric equality after converting to `i64` or `f64`
46-
- `state.rs` defines special forms object with key `numeq` mapped to the name `=`.
47-
- `compile.rs` calls `compile_list` which calls `compile_special` which calls `compile_math` in `compile_math.rs`
48-
- `compile_math.rs` `compile_math` matches on `env.specials().numeq` and generates opcode `NUMEQ`
49-
- `exec_loop.rs` maps opcode `NUMEQ` to function `compare_int` and passes two comparators
50-
- an integer comparator: `|a,b| a == b`
51-
- a float comparator: `|a: f64, b: f64| (a - b).abs() < f64::EPSILON`
52-
- `macros.rs` `compare_int`
53-
- checks if either argument is a `Float` and if so, converts both to `f64` with `get_float` macro and uses the float comparator
54-
- checks if either argument is a `Int` and if so, converts both to `i64` with `get_int` macro and uses the integer comparator
55-
- ultimately, ints are compared as i64 and floats are compared as f64 with a tolerance of `f64::EPSILON`
60+
- = should be converted to =?
61+
- =? should just be an alias for equal? which is the slower more intuitive equality check
62+
- eq? is the faster more strict equality check but it does spend a little extra time making sure to dereference captured heap values
63+
- eq? on floats should do an IEEE comparison
64+
- eq? should consider 1.0 equal to 1
65+
- equal? on floats should do a fuzzy comparison
66+
- if we want a bitwise comparison of floats we will implement it later
67+
68+
eq? does not do type coercion
69+
just do what clojure does
70+
numeq should be a bit fuzzy
71+
NaN != NaN is false and NaN == NaN is false
72+
73+
may 1 2024
74+
= (equal) loosy goosey (still use IEEE comparison)
75+
== (=?) (for numbers) (IEEE comparison)
76+
identical? (eq?) (bytewise equality)
77+
users who want rough epsilon equality can defn their own fn

slosh/tests/equality.slosh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env slosh
2+
3+
(assert-true (= 1 1))
4+
(assert-false (= 2 2.0))
5+
(assert-true (not= 2 2.0))
6+
(assert-true (== 2 2.0))
7+
(assert-true (== 2 2.000000000000000000000000000000000000000000001))
8+
(assert-false(== 1e-30 1.001e-30))

vm/src/value.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,18 @@ impl Value {
245245
matches!(&self, Value::Byte(_) | Value::Int(_) | Value::Float(_))
246246
}
247247

248+
pub fn is_float(&self) -> bool {
249+
matches!(self, Value::Float(_))
250+
}
251+
252+
pub fn not(&self) -> Value {
253+
match self {
254+
Value::False => Value::True,
255+
Value::Nil => Value::True,
256+
_ => Value::False,
257+
}
258+
}
259+
248260
pub fn get_int<ENV>(&self, _vm: &GVm<ENV>) -> VMResult<i64> {
249261
match &self {
250262
Value::Byte(b) => Ok(*b as i64),

vm/src/vm.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ impl<ENV> GVm<ENV> {
189189
Ok(val)
190190
}
191191

192-
fn is_eq(&self, reg1: u16, reg2: u16) -> VMResult<Value> {
192+
fn is_identical(&self, reg1: u16, reg2: u16) -> VMResult<Value> {
193193
let mut val = Value::False;
194194
if reg1 == reg2 {
195195
val = Value::True;
@@ -230,9 +230,14 @@ impl<ENV> GVm<ENV> {
230230
val = Value::True;
231231
}
232232
} else if val1.is_number() && val2.is_number() {
233-
let diff = (val1.get_float(self)? - val2.get_float(self)?).abs();
234-
if diff == 0.0 {
235-
val = Value::True;
233+
// compare two floats by converting to f64 and using native equality check (IEEE)
234+
if val1.is_float() && val2.is_float() {
235+
if val1.get_float(self)? == val2.get_float(self)? {
236+
val = Value::True;
237+
}
238+
} else {
239+
// we are comparing two numbers but they aren't both ints or both floats
240+
val = Value::False;
236241
}
237242
} else {
238243
match (val1, val2) {
@@ -391,6 +396,7 @@ impl<ENV> GVm<ENV> {
391396
Ok(val)
392397
}
393398

399+
/// test if the operands are = (more lenient than identical)
394400
fn is_equal(&self, reg1: u16, reg2: u16) -> VMResult<Value> {
395401
let mut val = Value::False;
396402
if reg1 == reg2 {
@@ -1577,4 +1583,29 @@ mod tests {
15771583
assert!(res.unwrap_err().to_string() == "[rt]: Divide by zero error.");
15781584
Ok(())
15791585
}
1586+
1587+
#[test]
1588+
fn test_equality() {
1589+
let vm = Vm::new();
1590+
1591+
let byte = Value::Byte(2);
1592+
let another_byte = Value::Byte(2);
1593+
let int = Value::Int([0, 0, 0, 0, 0, 0, 2]);
1594+
let another_int = Value::Int([0, 0, 0, 0, 0, 0, 2]);
1595+
let float = Value::Float(2.0.into());
1596+
let another_float = Value::Float(2.0000000000000000000000000000000000000000001.into());
1597+
1598+
// testing `=`
1599+
assert!(vm.is_equal_pair(byte, int).unwrap().is_true());
1600+
assert!(vm.is_equal_pair(byte, float).unwrap().is_false());
1601+
assert!(vm.is_equal_pair(int, float).unwrap().is_false());
1602+
1603+
// testing `identical?`
1604+
assert!(byte == another_byte);
1605+
assert!(int == another_int);
1606+
assert!(float == another_float);
1607+
assert!(byte != int);
1608+
assert!(byte != float);
1609+
assert!(int != float);
1610+
}
15801611
}

vm/src/vm/exec_loop.rs

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -805,7 +805,9 @@ impl<ENV> GVm<ENV> {
805805
}
806806
EQ => {
807807
let (dest, reg1, reg2) = decode3!(self.ip_ptr, wide);
808-
let val = self.is_eq(reg1, reg2).map_err(|e| (e, chunk.clone()))?;
808+
let val = self
809+
.is_identical(reg1, reg2)
810+
.map_err(|e| (e, chunk.clone()))?;
809811
set_register!(self, dest as usize, val);
810812
}
811813
EQUAL => {
@@ -948,30 +950,29 @@ impl<ENV> GVm<ENV> {
948950
SUB => binary_math!(self, chunk, self.ip_ptr, |a, b| a - b, wide),
949951
MUL => binary_math!(self, chunk, self.ip_ptr, |a, b| a * b, wide),
950952
DIV => div_math!(self, chunk, self.ip_ptr, wide),
951-
NUMEQ => compare_int!(
952-
self,
953-
chunk,
954-
self.ip_ptr,
955-
|a, b| a == b,
956-
|a: f64, b: f64| (a - b).abs() < f64::EPSILON,
957-
wide,
958-
true,
959-
false
960-
),
961-
NUMNEQ => compare_int!(
962-
self,
963-
chunk,
964-
self.ip_ptr,
965-
|a, b| a == b,
966-
|a: f64, b: f64| (a - b).abs() < f64::EPSILON,
967-
wide,
968-
true,
969-
true
970-
),
971-
NUMLT => compare!(self, chunk, self.ip_ptr, |a, b| a < b, wide, true),
972-
NUMLTE => compare!(self, chunk, self.ip_ptr, |a, b| a <= b, wide, true),
973-
NUMGT => compare!(self, chunk, self.ip_ptr, |a, b| a > b, wide, true),
974-
NUMGTE => compare!(self, chunk, self.ip_ptr, |a, b| a >= b, wide, true),
953+
NUMEQ => {
954+
compare_numeric!(self, chunk, self.ip_ptr, |a, b| a == b, wide)
955+
}
956+
NUMNEQ => {
957+
// TODO: #142 NUMNEQ is being hijacked temporarily to implement clojure `not=` which is a non-numeric comparison
958+
let (dest, reg1, reg2) = decode3!(self.ip_ptr, wide);
959+
let val = self.is_equal(reg1, reg2).map_err(|e| (e, chunk.clone()))?;
960+
set_register!(self, dest as usize, val.not());
961+
// This is what NUMNEQ would look like if it were a numeric comparison
962+
// compare_numeric!(self, chunk, self.ip_ptr, |a, b| a != b, wide)
963+
}
964+
NUMLT => {
965+
compare_numeric!(self, chunk, self.ip_ptr, |a, b| a < b, wide)
966+
}
967+
NUMLTE => {
968+
compare_numeric!(self, chunk, self.ip_ptr, |a, b| a <= b, wide)
969+
}
970+
NUMGT => {
971+
compare_numeric!(self, chunk, self.ip_ptr, |a, b| a > b, wide)
972+
}
973+
NUMGTE => {
974+
compare_numeric!(self, chunk, self.ip_ptr, |a, b| a >= b, wide)
975+
}
975976
INC => {
976977
let (dest, i) = decode2!(self.ip_ptr, wide);
977978
match self.register(dest as usize) {

0 commit comments

Comments
 (0)