Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
yannham committed Mar 14, 2023
1 parent a1ec9ab commit b0eeb12
Show file tree
Hide file tree
Showing 8 changed files with 69 additions and 42 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +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 = {version = "0.3.2", features = ["enable_serde"] }
malachite-q = "0.3.2"

[dev-dependencies]
pretty_assertions = "1.3.0"
Expand Down
27 changes: 3 additions & 24 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 @@ -13,21 +14,14 @@ use crate::term::array::{self, Array};
use crate::term::record::Field;
use crate::term::{RichTerm, Term};

use malachite::Rational;

macro_rules! deserialize_number {
($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($type::try_from(&n).map_err(|_| {
RustDeserializationError::IllegalNumberConversion {
source: n,
target_repr: String::from(stringify!($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 All @@ -49,10 +43,6 @@ pub enum RustDeserializationError {
UnimplementedType {
occurred: String,
},
IllegalNumberConversion {
source: Rational,
target_repr: String,
},
InvalidRecordLength(usize),
InvalidArrayLength(usize),
Other(String),
Expand All @@ -69,12 +59,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(f64::try_from(&v).map_err(|_| {
RustDeserializationError::IllegalNumberConversion {
source: v,
target_repr: String::from("f64"),
}
})?),
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 Down Expand Up @@ -585,12 +570,6 @@ impl std::fmt::Display for RustDeserializationError {
RustDeserializationError::UnimplementedType { ref occurred } => {
write!(f, "unimplemented conversion from type: {occurred}")
}
RustDeserializationError::IllegalNumberConversion {
source,
target_repr,
} => {
write!(f, "couldn't represent Nickel's arbitrary precision number {source} as an {target_repr}")
}
RustDeserializationError::Other(ref err) => write!(f, "{err}"),
}
}
Expand Down
9 changes: 7 additions & 2 deletions src/parser/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
//! `0`, this is the end of the current interpolated expressions, and we leave the normal mode and
//! go back to string mode. In our example, this is the second `}`: at this point, the lexer knows
//! that the coming characters must be lexed as string tokens, and not as normal tokens.
use crate::parser::error::{LexicalError, ParseError};
use super::{
error::{LexicalError, ParseError},
utils::parse_rational,
};
use logos::Logos;
use malachite::Rational;
use std::ops::Range;
Expand Down Expand Up @@ -66,7 +69,9 @@ pub enum NormalToken<'input> {
// regex for checking identifiers at ../lsp/nls/src/requests/completion.rs
#[regex("_?[a-zA-Z][_a-zA-Z0-9-']*")]
Identifier(&'input str),
#[regex("[0-9]*\\.?[0-9]+", |lex| lex.slice().parse())]
// unwrap(): try_from_float_simplest only fails on NaN or infinity, but those values aren't
// representable as a number literal.
#[regex("[0-9]*\\.?[0-9]+", |lex| parse_rational(lex.slice()))]
NumLiteral(Rational),

// **IMPORTANT**
Expand Down
2 changes: 1 addition & 1 deletion src/parser/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ fn numbers() {
assert_eq!(parse_without_pos("22.0"), mk_term::integer(22));
assert_eq!(
parse_without_pos("22.22"),
Num(Rational::try_from(22.22).unwrap()).into()
Num(Rational::try_from_float_simplest(22.22).unwrap()).into()
);
assert_eq!(parse_without_pos("(22)"), mk_term::integer(22));
assert_eq!(parse_without_pos("((22))"), mk_term::integer(22));
Expand Down
15 changes: 15 additions & 0 deletions src/parser/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,27 @@ use crate::{
position::{RawSpan, TermPos},
term::{
make as mk_term,
Rational,
record::{Field, FieldMetadata, RecordAttrs, RecordData},
BinaryOp, LabeledType, LetMetadata, RichTerm, StrChunk, Term, TypeAnnotation, UnaryOp,
},
types::{TypeF, Types},
};

pub enum ParseRationalError {
ParseFloatError(String),
RationalConversionError,
}

pub fn parse_rational(slice: &str) -> Result<Rational, ParseRationalError> {
let as_f64 = slice
.parse::<f64>()
.map_err(|err| ParseRationalError::ParseFloatError(err.to_string()))?;

Rational::try_from_float_simplest(as_f64)
.map_err(|_| ParseRationalError::RationalConversionError)
}

/// Distinguish between the standard string opening delimiter `"`, the multi-line string
/// opening delimter `m%"`, and the symbolic string opening delimiter `s%"`.
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
Expand Down
53 changes: 39 additions & 14 deletions src/serialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ use crate::{
term::{
array::{Array, ArrayAttrs},
record::RecordData,
Integer, Rational, RichTerm, Term, TypeAnnotation,
Rational, RichTerm, Term, TypeAnnotation,
},
};

use serde::{
de::{Deserialize, Deserializer},
ser::{Error, Serialize, SerializeMap, SerializeSeq, Serializer},
ser::{Serialize, SerializeMap, SerializeSeq, Serializer},
};

use malachite::num::conversion::traits::IsInteger;

use std::{collections::HashMap, fmt, io, rc::Rc, str::FromStr};

/// Available export formats.
Expand Down Expand Up @@ -67,23 +69,46 @@ impl FromStr for ExportFormat {
}
}

/// Implicitly convert numbers to integers when possible, and serialize an exact representation.
/// Implicitly convert numbers to primitive integers when possible, and serialize an exact
/// representation. Note that `u128` and `i128` aren't supported for common configuration formats
/// in serde, so we rather pick `i64` and `u64`, even if the former couple theoretically allows for
/// a wider range of rationals to be exactly represented. We don't expect values to be that large
/// in practice anyway: using arbitrary precision rationals is directed toward not introducing rounding errors
/// when performing simple arithmetic operations over decimals numbers, mostly.
///
/// If the number isn't an integer, we approximate it by the nearest `f64` and serialize this
/// value. This may incur a loss of precision, but this is expected: we can't represent e.g. `1/3`
/// exactly in JSON anyway. What arbitrary precision rationals are supposed to fix over floats are
/// behaviors like `0.1 +
/// 0.2 != 0.3` happening within the evaluation of a Nickel program.
/// If the number doesn't fit into an `i64` or `u64`, we approximate it by the nearest `f64` and
/// serialize this value. This may incur a loss of precision, but this is expected: we can't
/// represent something like e.g. `1/3` exactly in JSON anyway.
pub fn serialize_num<S>(n: &Rational, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
if let Ok(n_as_integer) = Integer::try_from(n) {
n_as_integer.serialize(serializer)
} else {
let n_as_f64 = f64::rounding_from(n, RoundingMode::Nearest);
n_as_f64.serialize(serializer)
if n.is_integer() {
if *n < 0 {
if let Ok(n_as_integer) = i64::try_from(n) {
return n_as_integer.serialize(serializer);
}
} else {
if let Ok(n_as_uinteger) = u64::try_from(n) {
return n_as_uinteger.serialize(serializer);
}
}
}

f64::rounding_from(n, RoundingMode::Nearest).serialize(serializer)
}

/// Deserialize for an Array. Required to set the default attributes.
pub fn deserialize_num<'de, D>(deserializer: D) -> Result<Rational, D::Error>
where
D: Deserializer<'de>,
{
let as_f64 = f64::deserialize(deserializer)?;
Rational::try_from_float_simplest(as_f64).map_err(|_| {
serde::de::Error::custom(format!(
"couldn't conver {as_f64} to a Nickel number: Nickel doesn't support NaN nor infinity"
))
})
}

/// Serializer for annotated values.
Expand All @@ -108,7 +133,7 @@ where
.iter_serializable()
.collect::<Result<Vec<_>, _>>()
.map_err(|missing_def_err| {
Error::custom(format!(
serde::ser::Error::custom(format!(
"missing field definition for `{}`",
missing_def_err.id
))
Expand Down
1 change: 1 addition & 0 deletions src/term/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ pub enum Term {
Bool(bool),
/// A floating-point value.
#[serde(serialize_with = "crate::serialize::serialize_num")]
#[serde(deserialize_with = "crate::serialize::deserialize_num")]
Num(Rational),
/// A literal string.
Str(String),
Expand Down

0 comments on commit b0eeb12

Please # to comment.