pretty error messages for floats

This commit is contained in:
Folkert 2020-07-07 22:53:54 +02:00
parent 9d67b11c0d
commit edb79b8fce
5 changed files with 163 additions and 23 deletions

View file

@ -1,13 +1,12 @@
use crate::env::Env; use crate::env::Env;
use crate::expr::Expr; use crate::expr::Expr;
use roc_parse::ast::Base; use roc_parse::ast::Base;
use roc_problem::can::IntErrorKind;
use roc_problem::can::Problem; use roc_problem::can::Problem;
use roc_problem::can::RuntimeError::*; use roc_problem::can::RuntimeError::*;
use roc_problem::can::{FloatErrorKind, IntErrorKind};
use roc_region::all::Region; use roc_region::all::Region;
use roc_types::subs::VarStore; use roc_types::subs::VarStore;
use std::i64; use std::i64;
use std::num::ParseFloatError;
// TODO distinguish number parsing failures // TODO distinguish number parsing failures
// //
@ -17,7 +16,7 @@ use std::num::ParseFloatError;
#[inline(always)] #[inline(always)]
pub fn num_expr_from_result( pub fn num_expr_from_result(
var_store: &mut VarStore, var_store: &mut VarStore,
result: Result<i64, (&str, ParseIntError)>, result: Result<i64, (&str, IntErrorKind)>,
region: Region, region: Region,
env: &mut Env, env: &mut Env,
) -> Expr { ) -> Expr {
@ -27,7 +26,7 @@ pub fn num_expr_from_result(
// (Num *) compiles to Int if it doesn't // (Num *) compiles to Int if it doesn't
// get specialized to something else first, // get specialized to something else first,
// so use int's overflow bounds here. // so use int's overflow bounds here.
let runtime_error = InvalidInt(error.kind, Base::Decimal, region, raw.into()); let runtime_error = InvalidInt(error, Base::Decimal, region, raw.into());
env.problem(Problem::RuntimeError(runtime_error.clone())); env.problem(Problem::RuntimeError(runtime_error.clone()));
@ -39,7 +38,7 @@ pub fn num_expr_from_result(
#[inline(always)] #[inline(always)]
pub fn int_expr_from_result( pub fn int_expr_from_result(
var_store: &mut VarStore, var_store: &mut VarStore,
result: Result<i64, (&str, ParseIntError)>, result: Result<i64, (&str, IntErrorKind)>,
region: Region, region: Region,
base: Base, base: Base,
env: &mut Env, env: &mut Env,
@ -48,7 +47,7 @@ pub fn int_expr_from_result(
match result { match result {
Ok(int) => Expr::Int(var_store.fresh(), int), Ok(int) => Expr::Int(var_store.fresh(), int),
Err((raw, error)) => { Err((raw, error)) => {
let runtime_error = InvalidInt(error.kind, base, region, raw.into()); let runtime_error = InvalidInt(error, base, region, raw.into());
env.problem(Problem::RuntimeError(runtime_error.clone())); env.problem(Problem::RuntimeError(runtime_error.clone()));
@ -60,15 +59,15 @@ pub fn int_expr_from_result(
#[inline(always)] #[inline(always)]
pub fn float_expr_from_result( pub fn float_expr_from_result(
var_store: &mut VarStore, var_store: &mut VarStore,
result: Result<f64, (&str, ParseFloatError)>, result: Result<f64, (&str, FloatErrorKind)>,
region: Region, region: Region,
env: &mut Env, env: &mut Env,
) -> Expr { ) -> Expr {
// Float stores a variable to generate better error messages // Float stores a variable to generate better error messages
match result { match result {
Ok(float) => Expr::Float(var_store.fresh(), float), Ok(float) => Expr::Float(var_store.fresh(), float),
Err((raw, _error)) => { Err((raw, error)) => {
let runtime_error = FloatOutsideRange(raw.into(), region); let runtime_error = InvalidFloat(error, region, raw.into());
env.problem(Problem::RuntimeError(runtime_error.clone())); env.problem(Problem::RuntimeError(runtime_error.clone()));
@ -78,10 +77,10 @@ pub fn float_expr_from_result(
} }
#[inline(always)] #[inline(always)]
pub fn finish_parsing_int(raw: &str) -> Result<i64, (&str, ParseIntError)> { pub fn finish_parsing_int(raw: &str) -> Result<i64, (&str, IntErrorKind)> {
// Ignore underscores. // Ignore underscores.
let radix = 10; let radix = 10;
from_str_radix::<i64>(raw.replace("_", "").as_str(), radix).map_err(|e| (raw, e)) from_str_radix::<i64>(raw.replace("_", "").as_str(), radix).map_err(|e| (raw, e.kind))
} }
#[inline(always)] #[inline(always)]
@ -89,7 +88,7 @@ pub fn finish_parsing_base(
raw: &str, raw: &str,
base: Base, base: Base,
is_negative: bool, is_negative: bool,
) -> Result<i64, (&str, ParseIntError)> { ) -> Result<i64, (&str, IntErrorKind)> {
let radix = match base { let radix = match base {
Base::Hex => 16, Base::Hex => 16,
Base::Decimal => 10, Base::Decimal => 10,
@ -103,16 +102,22 @@ pub fn finish_parsing_base(
} else { } else {
from_str_radix::<i64>(raw.replace("_", "").as_str(), radix) from_str_radix::<i64>(raw.replace("_", "").as_str(), radix)
}) })
.map_err(|e| (raw, e)) .map_err(|e| (raw, e.kind))
} }
#[inline(always)] #[inline(always)]
pub fn finish_parsing_float(raw: &str) -> Result<f64, (&str, ParseFloatError)> { pub fn finish_parsing_float(raw: &str) -> Result<f64, (&str, FloatErrorKind)> {
// Ignore underscores. // Ignore underscores.
match raw.replace("_", "").parse::<f64>() { match raw.replace("_", "").parse::<f64>() {
Ok(float) if float.is_finite() => Ok(float), Ok(float) if float.is_finite() => Ok(float),
Ok(_float) => panic!("TODO handle infinite float literal"), Ok(float) => {
Err(e) => Err((raw, e)), if float.is_sign_positive() {
Err((raw, FloatErrorKind::PositiveInfinity))
} else {
Err((raw, FloatErrorKind::NegativeInfinity))
}
}
Err(_) => Err((raw, FloatErrorKind::Error)),
} }
} }
@ -150,7 +155,9 @@ macro_rules! doit {
} }
})*) })*)
} }
doit! { i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize } // We only need the i64 implementation, but libcore defines
// doit! { i8 i16 i32 i64 i128 isize u8 u16 u32 u64 u128 usize }
doit! { i64 }
fn from_str_radix<T: FromStrRadixHelper>(src: &str, radix: u32) -> Result<T, ParseIntError> { fn from_str_radix<T: FromStrRadixHelper>(src: &str, radix: u32) -> Result<T, ParseIntError> {
use self::IntErrorKind::*; use self::IntErrorKind::*;

View file

@ -16,7 +16,7 @@ mod test_can {
use bumpalo::Bump; use bumpalo::Bump;
use roc_can::expr::Expr::{self, *}; use roc_can::expr::Expr::{self, *};
use roc_can::expr::Recursive; use roc_can::expr::Recursive;
use roc_problem::can::{IntErrorKind, Problem, RuntimeError}; use roc_problem::can::{FloatErrorKind, IntErrorKind, Problem, RuntimeError};
use roc_region::all::Region; use roc_region::all::Region;
use std::{f64, i64}; use std::{f64, i64};
@ -112,7 +112,11 @@ mod test_can {
assert_can( assert_can(
&string.clone(), &string.clone(),
RuntimeError(RuntimeError::FloatOutsideRange(string.into(), region)), RuntimeError(RuntimeError::InvalidFloat(
FloatErrorKind::PositiveInfinity,
region,
string.into(),
)),
); );
} }
@ -123,7 +127,11 @@ mod test_can {
assert_can( assert_can(
&string.clone(), &string.clone(),
RuntimeError(RuntimeError::FloatOutsideRange(string.into(), region)), RuntimeError(RuntimeError::InvalidFloat(
FloatErrorKind::NegativeInfinity,
region,
string.into(),
)),
); );
} }

View file

@ -80,6 +80,17 @@ pub enum IntErrorKind {
Underflow, Underflow,
} }
/// Enum to store the various types of errors that can cause parsing a float to fail.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FloatErrorKind {
/// Probably an invalid digit
Error,
/// the literal is too small for f64
NegativeInfinity,
/// the literal is too large for f64
PositiveInfinity,
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum RuntimeError { pub enum RuntimeError {
Shadowing { Shadowing {
@ -104,7 +115,7 @@ pub enum RuntimeError {
InvalidPrecedence(PrecedenceProblem, Region), InvalidPrecedence(PrecedenceProblem, Region),
MalformedIdentifier(Box<str>, Region), MalformedIdentifier(Box<str>, Region),
MalformedClosure(Region), MalformedClosure(Region),
FloatOutsideRange(Box<str>, Region), InvalidFloat(FloatErrorKind, Region, Box<str>),
InvalidInt(IntErrorKind, Base, Region, Box<str>), InvalidInt(IntErrorKind, Base, Region, Box<str>),
CircularDef(Vec<Symbol>, Vec<(Region /* pattern */, Region /* expr */)>), CircularDef(Vec<Symbol>, Vec<(Region /* pattern */, Region /* expr */)>),

View file

@ -1,6 +1,6 @@
use roc_collections::all::MutSet; use roc_collections::all::MutSet;
use roc_problem::can::PrecedenceProblem::BothNonAssociative; use roc_problem::can::PrecedenceProblem::BothNonAssociative;
use roc_problem::can::{IntErrorKind, Problem, RuntimeError}; use roc_problem::can::{FloatErrorKind, IntErrorKind, Problem, RuntimeError};
use roc_region::all::Region; use roc_region::all::Region;
use std::path::PathBuf; use std::path::PathBuf;
@ -386,7 +386,50 @@ fn pretty_runtime_error<'b>(
todo!("malformed identifier, currently gives a parse error and thus is unreachable") todo!("malformed identifier, currently gives a parse error and thus is unreachable")
} }
RuntimeError::MalformedClosure(_) => todo!(""), RuntimeError::MalformedClosure(_) => todo!(""),
RuntimeError::FloatOutsideRange(_raw_str, _region) => todo!(""), RuntimeError::InvalidFloat(sign @ FloatErrorKind::PositiveInfinity, region, _raw_str)
| RuntimeError::InvalidFloat(sign @ FloatErrorKind::NegativeInfinity, region, _raw_str) => {
let hint = alloc
.hint()
.append(alloc.reflow("Learn more about number literals at TODO"));
let big_or_small = if let FloatErrorKind::PositiveInfinity = sign {
"big"
} else {
"small"
};
alloc.stack(vec![
alloc.concat(vec![
alloc.reflow("This float literal is too "),
alloc.text(big_or_small),
alloc.reflow(":"),
]),
alloc.region(region),
alloc.concat(vec![
alloc.reflow("Roc uses signed 64-bit floating points, allowing values between"),
alloc.text(format!("{:e}", f64::MIN)),
alloc.reflow(" and "),
alloc.text(format!("{:e}", f64::MAX)),
]),
hint,
])
}
RuntimeError::InvalidFloat(FloatErrorKind::Error, region, _raw_str) => {
let hint = alloc
.hint()
.append(alloc.reflow("Learn more about number literals at TODO"));
alloc.stack(vec![
alloc.concat(vec![
alloc.reflow("This float literal contains an invalid digit:"),
]),
alloc.region(region),
alloc.concat(vec![
alloc.reflow("Floating point literals can only contain the digits 0-9, or use scientific notation 10e4"),
]),
hint,
])
}
RuntimeError::InvalidInt(IntErrorKind::Empty, _base, _region, _raw_str) => { RuntimeError::InvalidInt(IntErrorKind::Empty, _base, _region, _raw_str) => {
unreachable!("would never parse an empty int literal") unreachable!("would never parse an empty int literal")
} }

View file

@ -3181,6 +3181,49 @@ mod test_reporting {
) )
} }
#[test]
fn float_out_of_range() {
report_problem_as(
&format!(
r#"
overflow = 1{:e}
underflow = -1{:e}
overflow + underflow
"#,
f64::MAX,
f64::MAX,
),
indoc!(
r#"
-- SYNTAX PROBLEM --------------------------------------------------------------
This float literal is too small:
3 underflow = -11.7976931348623157e308
^^^^^^^^^^^^^^^^^^^^^^^^
Roc uses signed 64-bit floating points, allowing values
between-1.7976931348623157e308 and 1.7976931348623157e308
Hint: Learn more about number literals at TODO
-- SYNTAX PROBLEM --------------------------------------------------------------
This float literal is too big:
2 overflow = 11.7976931348623157e308
^^^^^^^^^^^^^^^^^^^^^^^
Roc uses signed 64-bit floating points, allowing values
between-1.7976931348623157e308 and 1.7976931348623157e308
Hint: Learn more about number literals at TODO
"#
),
)
}
#[test] #[test]
fn integer_malformed() { fn integer_malformed() {
// the generated messages here are incorrect. Waiting for a rust nightly feature to land, // the generated messages here are incorrect. Waiting for a rust nightly feature to land,
@ -3250,4 +3293,32 @@ mod test_reporting {
), ),
) )
} }
#[test]
fn float_malformed() {
report_problem_as(
indoc!(
r#"
x = 3.0A
x
"#
),
indoc!(
r#"
-- SYNTAX PROBLEM --------------------------------------------------------------
This float literal contains an invalid digit:
1 x = 3.0A
^^^^
Floating point literals can only contain the digits 0-9, or use
scientific notation 10e4
Hint: Learn more about number literals at TODO
"#
),
)
}
} }