Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Use arbitrary precision rationals as the underlying representation of numbers #1182

Merged
merged 13 commits into from
Mar 17, 2023
Merged
315 changes: 254 additions & 61 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ pretty = "0.11.3"
comrak = { version = "0.16.0", optional = true, features = [] }
once_cell = "1.17.1"
typed-arena = "2.0.2"
malachite = {version = "0.3.2", features = ["enable_serde"] }
malachite-q = "0.3.2"

[dev-dependencies]
pretty_assertions = "1.3.0"
Expand Down
4 changes: 2 additions & 2 deletions benches/arrays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::rc::Rc;
use criterion::{criterion_main, Criterion};
use nickel_lang::term::{
array::{Array, ArrayAttrs},
RichTerm, Term,
Number, RichTerm, Term,
};
use nickel_lang_utilities::{ncl_bench_group, EvalMode};
use pprof::criterion::{Output, PProfProfiler};
Expand All @@ -20,7 +20,7 @@ fn ncl_random_array(len: usize) -> String {

for _ in 0..len {
acc = (a * acc + c) % m;
numbers.push(RichTerm::from(Term::Num(acc as f64)));
numbers.push(RichTerm::from(Term::Num(Number::from(acc))));
}

let xs = RichTerm::from(Term::Array(
Expand Down
16 changes: 14 additions & 2 deletions doc/manual/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,20 @@ There are four basic kinds of values in Nickel :
### Numeric values

Nickel has a support for numbers, positive and negative, with or without
decimals. Internally, those numbers are stored as 64-bits floating point
numbers, following the IEEE 754 standard.
decimals. Internally, those numbers are stored as arbitrary precision rational
numbers, meaning that basic arithmetic operations (addition, subtraction,
division and multiplication) don't incur rounding errors. Numbers are
deserialized as 64-bits floating point numbers.

Raising to a non-integer power is one operation which can incur rounding errors:
both operands need to be converted to the nearest 64-bits floating point
numbers, the power is computed as a 64-bits floating point number as well,
and converted back to an arbitrary precision rational number.

Numbers are serialized as integers whenever possible (when they fit exactly into
a 64-bits signed integer or a 64-bits unsigned integer), and as a 64 bits float
otherwise. The latter conversion might lose precision as well, for example when
serializing `1/3`.

Examples:

Expand Down
5 changes: 4 additions & 1 deletion doc/manual/typing.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,10 @@ Let us now have a quick tour of the type system. The basic types are:

- `Dyn`: the dynamic type. This is the type given to most expressions outside of
a typed block. A value of type `Dyn` can be pretty much anything.
- `Number`: the only number type. Currently implemented as a 64bits float.
- `Number`: the only number type. Currently implemented as arbitrary precision
rationals. Common arithmetic operations are exact. The power function using
a non-integer exponent is computed on floating point values, which
might incur rounding errors.
- `String`: a string, which must always be valid UTF8.
- `Bool`: a boolean, that is either `true` or `false`.
<!-- - `Lbl`: a contract label. You usually don't need to use it or worry about it,-->
Expand Down
42 changes: 13 additions & 29 deletions src/deserialize.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! Deserialization of an evaluated program to plain Rust types.

use malachite::{num::conversion::traits::RoundingFrom, rounding_modes::RoundingMode};
use std::collections::HashMap;
use std::iter::ExactSizeIterator;

Expand All @@ -20,24 +21,7 @@ macro_rules! deserialize_number {
V: Visitor<'de>,
{
match unwrap_term(self)? {
Term::Num(n) => visitor.$visit(n as $type),
other => Err(RustDeserializationError::InvalidType {
expected: "Number".to_string(),
occurred: RichTerm::from(other).to_string(),
}),
}
}
};
}

macro_rules! deserialize_number_round {
($method:ident, $type:tt, $visit:ident) => {
fn $method<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
match unwrap_term(self)? {
Term::Num(n) => visitor.$visit(n.round() as $type),
Term::Num(n) => visitor.$visit($type::rounding_from(&n, RoundingMode::Nearest)),
other => Err(RustDeserializationError::InvalidType {
expected: "Number".to_string(),
occurred: RichTerm::from(other).to_string(),
Expand Down Expand Up @@ -70,7 +54,7 @@ impl<'de> serde::Deserializer<'de> for RichTerm {
match unwrap_term(self)? {
Term::Null => visitor.visit_unit(),
Term::Bool(v) => visitor.visit_bool(v),
Term::Num(v) => visitor.visit_f64(v),
Term::Num(v) => visitor.visit_f64(f64::rounding_from(v, RoundingMode::Nearest)),
Term::Str(v) => visitor.visit_string(v),
Term::Enum(v) => visitor.visit_enum(EnumDeserializer {
variant: v.into_label(),
Expand All @@ -88,16 +72,16 @@ impl<'de> serde::Deserializer<'de> for RichTerm {
}
}

deserialize_number_round!(deserialize_i8, i8, visit_i8);
deserialize_number_round!(deserialize_i16, i16, visit_i16);
deserialize_number_round!(deserialize_i32, i32, visit_i32);
deserialize_number_round!(deserialize_i64, i64, visit_i64);
deserialize_number_round!(deserialize_i128, i128, visit_i128);
deserialize_number_round!(deserialize_u8, u8, visit_u8);
deserialize_number_round!(deserialize_u16, u16, visit_u16);
deserialize_number_round!(deserialize_u32, u32, visit_u32);
deserialize_number_round!(deserialize_u64, u64, visit_u64);
deserialize_number_round!(deserialize_u128, u128, visit_u128);
deserialize_number!(deserialize_i8, i8, visit_i8);
deserialize_number!(deserialize_i16, i16, visit_i16);
deserialize_number!(deserialize_i32, i32, visit_i32);
deserialize_number!(deserialize_i64, i64, visit_i64);
deserialize_number!(deserialize_i128, i128, visit_i128);
deserialize_number!(deserialize_u8, u8, visit_u8);
deserialize_number!(deserialize_u16, u16, visit_u16);
deserialize_number!(deserialize_u32, u32, visit_u32);
deserialize_number!(deserialize_u64, u64, visit_u64);
deserialize_number!(deserialize_u128, u128, visit_u128);
deserialize_number!(deserialize_f32, f32, visit_f32);
deserialize_number!(deserialize_f64, f64, visit_f64);

Expand Down
2 changes: 1 addition & 1 deletion src/eval/merge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ pub fn merge<C: Cache>(
}
}
(Term::Num(n1), Term::Num(n2)) => {
if (n1 - n2).abs() < f64::EPSILON {
if n1 == n2 {
Ok(Closure::atomic_closure(RichTerm::new(
Term::Num(n1),
pos_op.into_inherited(),
Expand Down
Loading