Take syntactic sugar into account when reporting errors

Previously, a program like

```roc
word = "word"

if True then 1 else "\(word) is a word"
```

would report an error like

```
── TYPE MISMATCH ───────────────────────────────────────────────────────────────

This `if` has an `else` branch with a different type from its `then` branch:

3│  if True then 1 else "\(word) is a word"
                        ^^^^^^^^^^^^^^^^^^

This concat all produces:

    Str

but the `then` branch has the type:

    Num a

I need all branches in an `if` to have the same type!
```

but this is a little bit confusing, since the user shouldn't have to
know (or care) that string interpolations are equivalent to
concatenations under the current implementation.

Indeed we should make this fully transparent. We now word the error
message by taking into account the way calls are made. To support the
case shown above, we introduce the `CalledVia::Sugar` variant to
represent the fact that some calls may be the result of desugaring the
surface syntax.

This commit also demonstrates the usage of `CalledVia` to produce better
error messages where we use binary comparison operators like `<`. There
are more improvements we can make here for all `CalledVia` variants, but
this is a good starting point to demonstrate the usage of the new
procedure.

Closes #1714
This commit is contained in:
ayazhafiz 2021-11-17 23:54:46 -05:00
parent a4fc813ca3
commit 30955a1eb8
7 changed files with 110 additions and 10 deletions

View file

@ -277,7 +277,7 @@ pub fn constrain_expr<'a>(
expr_id: expr_node_id,
closure_var,
fn_var,
..
called_via,
} => {
// The expression that evaluates to the function being called, e.g. `foo` in
// (foo) bar baz
@ -349,7 +349,7 @@ pub fn constrain_expr<'a>(
region,
);
let category = Category::CallResult(opt_symbol);
let category = Category::CallResult(opt_symbol, *called_via);
let mut and_constraints = BumpVec::with_capacity_in(4, arena);

View file

@ -12,7 +12,7 @@ use crate::scope::Scope;
use roc_collections::all::{ImSet, MutMap, MutSet, SendMap};
use roc_module::ident::{ForeignSymbol, Lowercase, TagName};
use roc_module::low_level::LowLevel;
use roc_module::operator::CalledVia;
use roc_module::operator::{CalledVia, Sugar};
use roc_module::symbol::Symbol;
use roc_parse::ast::{self, EscapedChar, StrLiteral};
use roc_parse::pattern::PatternType::*;
@ -1711,7 +1711,7 @@ fn desugar_str_segments(var_store: &mut VarStore, segments: Vec<StrSegment>) ->
(var_store.fresh(), loc_new_expr),
(var_store.fresh(), loc_expr),
],
CalledVia::Space,
CalledVia::Sugar(Sugar::StringInterpolation),
);
loc_expr = Located::new(0, 0, 0, 0, expr);

View file

@ -254,7 +254,7 @@ pub fn constrain_expr(
exists(vec![*elem_var], And(constraints))
}
}
Call(boxed, loc_args, _application_style) => {
Call(boxed, loc_args, called_via) => {
let (fn_var, loc_fn, closure_var, ret_var) = &**boxed;
// The expression that evaluates to the function being called, e.g. `foo` in
// (foo) bar baz
@ -317,7 +317,7 @@ pub fn constrain_expr(
region,
);
let category = Category::CallResult(opt_symbol);
let category = Category::CallResult(opt_symbol, *called_via);
exists(
vars,

View file

@ -12,6 +12,15 @@ pub enum CalledVia {
/// Calling with a unary operator, e.g. (!foo bar baz) or (-foo bar baz)
UnaryOp(UnaryOp),
/// This call is the result of some desugaring, e.g. "\(first) \(last)" is
/// transformed into first ++ last.
Sugar(Sugar),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Sugar {
StringInterpolation,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]

View file

@ -5,6 +5,7 @@ use crate::subs::{
use roc_collections::all::{ImMap, ImSet, Index, MutSet, SendMap};
use roc_module::ident::{ForeignSymbol, Ident, Lowercase, TagName};
use roc_module::low_level::LowLevel;
use roc_module::operator::CalledVia;
use roc_module::symbol::{Interns, ModuleId, Symbol};
use roc_region::all::{Located, Region};
use std::fmt;
@ -1134,7 +1135,7 @@ pub enum Reason {
#[derive(PartialEq, Debug, Clone)]
pub enum Category {
Lookup(Symbol),
CallResult(Option<Symbol>),
CallResult(Option<Symbol>, CalledVia),
LowLevelOpResult(LowLevel),
ForeignCall,
TagApply {

View file

@ -1,6 +1,7 @@
use roc_can::expected::{Expected, PExpected};
use roc_collections::all::{Index, MutSet, SendMap};
use roc_module::ident::{Ident, IdentStr, Lowercase, TagName};
use roc_module::operator::{BinOp, CalledVia, Sugar};
use roc_module::symbol::Symbol;
use roc_region::all::{Located, Region};
use roc_solve::solve;
@ -1043,13 +1044,26 @@ fn add_category<'b>(
alloc.record_field(field.to_owned()),
alloc.text(" is a:"),
]),
CallResult(Some(symbol)) => alloc.concat(vec![
CallResult(
Some(symbol),
CalledVia::BinOp(
BinOp::Equals
| BinOp::NotEquals
| BinOp::LessThan
| BinOp::GreaterThan
| BinOp::LessThanOrEq
| BinOp::GreaterThanOrEq,
),
) => alloc.concat(vec![alloc.text("This comparison produces:")]),
CallResult(Some(symbol), CalledVia::Sugar(Sugar::StringInterpolation)) => {
alloc.concat(vec![alloc.text("This string interpolation produces:")])
}
CallResult(Some(symbol), _) => alloc.concat(vec![
alloc.text("This "),
alloc.symbol_foreign_qualified(*symbol),
alloc.text(" call produces:"),
]),
CallResult(None) => alloc.concat(vec![this_is, alloc.text(":")]),
CallResult(None, _) => alloc.concat(vec![this_is, alloc.text(":")]),
LowLevelOpResult(op) => {
panic!(
"Compiler bug: invalid return type from low-level op {:?}",

View file

@ -5551,6 +5551,82 @@ mod test_reporting {
)
}
#[test]
// https://github.com/rtfeldman/roc/issues/1714
fn interpolate_concat_is_transparent_1714() {
report_problem_as(
indoc!(
r#"
greeting = "Privet"
if True then 1 else "\(greeting), World!"
"#,
),
indoc!(
r#"
TYPE MISMATCH
This `if` has an `else` branch with a different type from its `then` branch:
3 if True then 1 else "\(greeting), World!"
^^^^^^^^^^^^^^^^^^^^^
This string interpolation produces:
Str
but the `then` branch has the type:
Num a
I need all branches in an `if` to have the same type!
"#
),
)
}
macro_rules! comparison_binop_transparency_tests {
($($op:expr, $name:ident),* $(,)?) => {
$(
#[test]
fn $name() {
report_problem_as(
&format!(r#"if True then "abc" else 1 {} 2"#, $op),
&format!(
r#"── TYPE MISMATCH ───────────────────────────────────────────────────────────────
This `if` has an `else` branch with a different type from its `then` branch:
1 if True then "abc" else 1 {} 2
^^{}^^
This comparison produces:
Bool
but the `then` branch has the type:
Str
I need all branches in an `if` to have the same type!
"#,
$op, "^".repeat($op.len())
),
)
}
)*
}
}
comparison_binop_transparency_tests! {
"<", lt_binop_is_transparent,
">", gt_binop_is_transparent,
"==", eq_binop_is_transparent,
"!=", neq_binop_is_transparent,
"<=", leq_binop_is_transparent,
">=", geq_binop_is_transparent,
}
#[test]
fn keyword_record_field_access() {
report_problem_as(