diff --git a/ast/src/constrain.rs b/ast/src/constrain.rs index 80ff70491b..57d44ca2f0 100644 --- a/ast/src/constrain.rs +++ b/ast/src/constrain.rs @@ -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); diff --git a/compiler/can/src/expr.rs b/compiler/can/src/expr.rs index 098ad7926b..db2e005206 100644 --- a/compiler/can/src/expr.rs +++ b/compiler/can/src/expr.rs @@ -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) -> (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); diff --git a/compiler/constrain/src/expr.rs b/compiler/constrain/src/expr.rs index 11d8c4cabe..eac7d8f1fc 100644 --- a/compiler/constrain/src/expr.rs +++ b/compiler/constrain/src/expr.rs @@ -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, diff --git a/compiler/module/src/operator.rs b/compiler/module/src/operator.rs index 14ba2de17b..99f638d428 100644 --- a/compiler/module/src/operator.rs +++ b/compiler/module/src/operator.rs @@ -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)] diff --git a/compiler/types/src/types.rs b/compiler/types/src/types.rs index 3ccefd5fd3..1e3b7543cc 100644 --- a/compiler/types/src/types.rs +++ b/compiler/types/src/types.rs @@ -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), + CallResult(Option, CalledVia), LowLevelOpResult(LowLevel), ForeignCall, TagApply { diff --git a/reporting/src/error/type.rs b/reporting/src/error/type.rs index 7b8049aa14..1b5c28d43d 100644 --- a/reporting/src/error/type.rs +++ b/reporting/src/error/type.rs @@ -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 {:?}", diff --git a/reporting/tests/test_reporting.rs b/reporting/tests/test_reporting.rs index 5210c72396..0b5a40bd25 100644 --- a/reporting/tests/test_reporting.rs +++ b/reporting/tests/test_reporting.rs @@ -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(