mirror of
https://github.com/roc-lang/roc.git
synced 2025-09-27 13:59:08 +00:00
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:
parent
a4fc813ca3
commit
30955a1eb8
7 changed files with 110 additions and 10 deletions
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {:?}",
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue