Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggest wrapping value in Ok or Error if it makes sense #3663

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,43 @@

### Compiler

- The compiler can now suggest to wrap a value in an `Ok` or `Error` if that can
solve a type mismatch error:

```gleam
pub fn first(list: List(a)) -> Result(a, Nil) {
case number {
[] -> Error(Nil)
[first, ..rest] -> first
}
}
```

Results in the following error:

```txt
error: Type mismatch
┌─ /src/one/two.gleam:5:5
5 │ [first, ..rest] -> first
│ ^^^^^^^^^^^^^^^^^^^^^^^^
│ │
│ Did you mean to wrap this in an `Ok`?
Comment on lines +22 to +28
Copy link
Member

@lpil lpil Dec 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This error is wrong! Ok([first, ..rest] -> first) would not be valid.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The message is referring to the first bit but the way the two overlapping spans are rendered it's not very clear; do you think we should stop highlighting the entire case branch in this case? However that would mean that the error message is not very precise now:

[first, ..rest] -> first
                   ^^^^^ Did you mean to wrap this in an `Ok`?

This case clause was found to return a different type than the previous
one, but all case clauses must return the same type.

And it would be more confusing when the part on the right of -> is on its own line as the clause would not be displayed in the error!

  first
  ^^^^^ Did you mean to wrap this in an `Ok`?

This case clause was found to return a different type than the previous
one, but all case clauses must return the same type.

Copy link
Member

@lpil lpil Dec 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should continue to highlight the entire cause as the source of the type error, but a label about wrapping an expression should only point to the code that is to be wrapped.

If this is a lot of work then another option could be to not have this suggestion for case clauses.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tricky thing is it is already pointing to just the code that needs to be wrapped but the way the library renders it it's not immediately obvious. If the then clause were not on the same line what you'd see is this:

     case wibble {
       A -> Error("a")
  ->   wobble ->
 |     1
 |     ^ Did you mean to wrap this ...
 |
 incorrect branch

But when the two are on the same line the two look like this

wobble -> 1
^^^^^^^^^^^
          |
          Did you mean to wrap this
incorrect branch

The way it gets rendered is not the best, maybe I could try and remove it for branches when the then clause is on the same line or for case matches entirely

Copy link
Member

@lpil lpil Dec 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I see, that is annoying. Sounds good to me- changing printing sounds like it would be irritating.


This case clause was found to return a different type than the previous
one, but all case clauses must return the same type.

Expected type:

Result(a, Nil)

Found type:

a
```

([Giacomo Cavalieri](https://github.com/giacomocavalieri))

### Build tool

### Language server
Expand Down
2 changes: 0 additions & 2 deletions compiler-core/src/ast/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,6 @@ impl TypedExpr {
matches!(
self,
Self::Int{ value, .. } | Self::Float { value, .. } if NON_ZERO.get_or_init(||


Regex::new(r"[1-9]").expect("NON_ZERO regex")).is_match(value)
)
}
Expand Down
35 changes: 32 additions & 3 deletions compiler-core/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![allow(clippy::unwrap_used, clippy::expect_used)]
use crate::build::{Outcome, Runtime, Target};
use crate::diagnostic::{Diagnostic, ExtraLabel, Label, Location};
use crate::type_::collapse_links;
use crate::type_::error::{
MissingAnnotation, ModuleValueUsageContext, Named, UnknownField, UnknownTypeHint,
UnsafeRecordUpdateReason,
Expand All @@ -21,6 +22,7 @@ use std::collections::HashSet;
use std::fmt::{Debug, Display};
use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;
use termcolor::Buffer;
use thiserror::Error;
use vec1::Vec1;
Expand Down Expand Up @@ -1961,19 +1963,33 @@ But function expects:
text.push_str(&printer.print_type(expected));
text.push_str("\n\nFound type:\n\n ");
text.push_str(&printer.print_type(given));

let (main_message_location, main_message_text, extra_labels) = match situation {
// When the mismatch error comes from a case clause we want to highlight the
// entire branch (pattern included) when reporting the error; in addition,
// if the error could be resolved just by wrapping the value in an `Ok`
// or `Error` we want to add an additional label with this hint below the
// offending value.
Some(UnifyErrorSituation::CaseClauseMismatch{ clause_location }) => (clause_location, None, vec![]),
// In all other cases we just highlight the offending expression, optionally
// adding the wrapping hint if it makes sense.
Some(_) | None =>
(location, hint_wrap_value_in_result(expected, given), vec![])
};

Diagnostic {
title: "Type mismatch".into(),
text,
hint: None,
level: Level::Error,
location: Some(Location {
label: Label {
text: None,
span: *location,
text: main_message_text,
span: *main_message_location,
},
path: path.clone(),
src: src.clone(),
extra_labels: vec![],
extra_labels,
}),
}
}
Expand Down Expand Up @@ -3948,6 +3964,19 @@ fn hint_alternative_operator(op: &BinOp, given: &Type) -> Option<String> {
}
}

fn hint_wrap_value_in_result(expected: &Arc<Type>, given: &Arc<Type>) -> Option<String> {
let expected = collapse_links(expected.clone());
let (expected_ok_type, expected_error_type) = expected.result_types()?;

if given.same_as(expected_ok_type.as_ref()) {
Some("Did you mean to wrap this in an `Ok`?".into())
} else if given.same_as(expected_error_type.as_ref()) {
Some("Did you mean to wrap this in an `Error`?".into())
} else {
None
}
}

fn hint_numeric_message(alt: &str, type_: &str) -> String {
format!("the {alt} operator can be used with {type_}s\n")
}
Expand Down
103 changes: 103 additions & 0 deletions compiler-core/src/type_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,18 @@ impl Type {
}
}

pub fn result_types(&self) -> Option<(Arc<Type>, Arc<Type>)> {
match self {
Self::Named {
module, name, args, ..
} if "Result" == name && is_prelude_module(module) => {
Some((args.first().cloned()?, args.get(1).cloned()?))
}
Self::Var { type_ } => type_.borrow().result_types(),
Self::Named { .. } | Self::Tuple { .. } | Type::Fn { .. } => None,
}
}

pub fn is_unbound(&self) -> bool {
match self {
Self::Var { type_ } => type_.borrow().is_unbound(),
Expand Down Expand Up @@ -433,6 +445,90 @@ impl Type {
_ => None,
}
}

#[must_use]
/// Returns `true` is the two types are the same. This differs from the
/// standard `Eq` implementation as it also follows all links to check if
/// two types are really the same.
///
pub fn same_as(&self, other: &Self) -> bool {
match (self, other) {
(Type::Named { .. }, Type::Fn { .. } | Type::Tuple { .. }) => false,
(one @ Type::Named { .. }, Type::Var { type_ }) => {
type_.as_ref().borrow().same_as_other_type(one)
}
(Type::Named { .. }, Type::Named { .. }) => self == other,

(Type::Fn { .. }, Type::Named { .. } | Type::Tuple { .. }) => false,
(one @ Type::Fn { .. }, Type::Var { type_ }) => {
type_.as_ref().borrow().same_as_other_type(one)
}
(
Type::Fn { args, retrn },
Type::Fn {
args: other_args,
retrn: other_retrn,
},
) => {
args.len() == other_args.len()
&& args
.iter()
.zip(other_args)
.all(|(one, other)| one.same_as(other))
&& retrn.same_as(other_retrn)
}

(Type::Var { type_ }, other) => type_.as_ref().borrow().same_as_other_type(other),

(Type::Tuple { .. }, Type::Fn { .. } | Type::Named { .. }) => false,
(one @ Type::Tuple { .. }, Type::Var { type_ }) => {
type_.as_ref().borrow().same_as_other_type(one)
}
giacomocavalieri marked this conversation as resolved.
Show resolved Hide resolved
(Type::Tuple { elems }, Type::Tuple { elems: other_elems }) => {
elems.len() == other_elems.len()
&& elems
.iter()
.zip(other_elems)
.all(|(one, other)| one.same_as(other))
}
}
}
}

impl TypeVar {
#[must_use]
fn same_as_other_type(&self, other: &Type) -> bool {
match (self, other) {
(TypeVar::Unbound { .. }, _) => true,
(TypeVar::Link { type_ }, other) => type_.same_as(other),

(
TypeVar::Generic { .. },
Type::Named { .. } | Type::Fn { .. } | Type::Tuple { .. },
) => false,

(one @ TypeVar::Generic { .. }, Type::Var { type_ }) => {
one.same_as(&type_.as_ref().borrow())
}
}
}

#[must_use]
fn same_as(&self, other: &Self) -> bool {
match (self, other) {
(TypeVar::Unbound { .. }, _) | (_, TypeVar::Unbound { .. }) => true,
(TypeVar::Link { type_ }, TypeVar::Link { type_: other_type }) => {
type_.same_as(other_type)
}
(TypeVar::Link { type_ }, other @ TypeVar::Generic { .. }) => {
other.same_as_other_type(type_)
}
(TypeVar::Generic { id }, TypeVar::Generic { id: other_id }) => id == other_id,
(one @ TypeVar::Generic { .. }, TypeVar::Link { type_ }) => {
one.same_as_other_type(type_)
}
}
}
}

pub fn collapse_links(t: Arc<Type>) -> Arc<Type> {
Expand Down Expand Up @@ -970,6 +1066,13 @@ impl TypeVar {
}
}

pub fn result_types(&self) -> Option<(Arc<Type>, Arc<Type>)> {
match self {
TypeVar::Link { type_ } => type_.result_types(),
TypeVar::Unbound { .. } | TypeVar::Generic { .. } => None,
}
}

pub fn fn_types(&self) -> Option<(Vec<Arc<Type>>, Arc<Type>)> {
match self {
Self::Link { type_ } => type_.fn_types(),
Expand Down
26 changes: 18 additions & 8 deletions compiler-core/src/type_/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1329,12 +1329,16 @@ fn flip_unify_error_test() {
UnifyError::CouldNotUnify {
expected: crate::type_::int(),
given: crate::type_::float(),
situation: Some(UnifyErrorSituation::CaseClauseMismatch),
situation: Some(UnifyErrorSituation::CaseClauseMismatch {
clause_location: SrcSpan::default()
}),
},
flip_unify_error(UnifyError::CouldNotUnify {
expected: crate::type_::float(),
given: crate::type_::int(),
situation: Some(UnifyErrorSituation::CaseClauseMismatch),
situation: Some(UnifyErrorSituation::CaseClauseMismatch {
clause_location: SrcSpan::default()
}),
})
);
}
Expand Down Expand Up @@ -1364,15 +1368,19 @@ fn unify_enclosed_type_test() {
Err(UnifyError::CouldNotUnify {
expected: crate::type_::int(),
given: crate::type_::float(),
situation: Some(UnifyErrorSituation::CaseClauseMismatch)
situation: Some(UnifyErrorSituation::CaseClauseMismatch {
clause_location: SrcSpan::default()
})
}),
unify_enclosed_type(
crate::type_::int(),
crate::type_::float(),
Err(UnifyError::CouldNotUnify {
expected: crate::type_::string(),
given: crate::type_::bits(),
situation: Some(UnifyErrorSituation::CaseClauseMismatch)
situation: Some(UnifyErrorSituation::CaseClauseMismatch {
clause_location: SrcSpan::default()
})
})
)
);
Expand Down Expand Up @@ -1437,7 +1445,9 @@ pub fn unify_wrong_returns(
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UnifyErrorSituation {
/// Clauses in a case expression were found to return different types.
CaseClauseMismatch,
CaseClauseMismatch {
clause_location: SrcSpan,
},

/// A function was found to return a value that did not match its return
/// annotation.
Expand Down Expand Up @@ -1480,7 +1490,7 @@ pub enum FunctionsMismatchReason {
impl UnifyErrorSituation {
pub fn description(&self) -> Option<&'static str> {
match self {
Self::CaseClauseMismatch => Some(
Self::CaseClauseMismatch { clause_location: _ } => Some(
"This case clause was found to return a different type than the previous
one, but all case clauses must return the same type.",
),
Expand Down Expand Up @@ -1545,8 +1555,8 @@ impl UnifyError {
}
}

pub fn case_clause_mismatch(self) -> Self {
self.with_unify_error_situation(UnifyErrorSituation::CaseClauseMismatch)
pub fn case_clause_mismatch(self, clause_location: SrcSpan) -> Self {
self.with_unify_error_situation(UnifyErrorSituation::CaseClauseMismatch { clause_location })
}

pub fn list_element_mismatch(self) -> Self {
Expand Down
7 changes: 4 additions & 3 deletions compiler-core/src/type_/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1469,9 +1469,10 @@ impl<'a, 'b> ExprTyper<'a, 'b> {
let typed_clause = self.infer_clause(clause, &typed_subjects);
all_clauses_panic = all_clauses_panic && self.previous_panics;

if let Err(e) = unify(return_type.clone(), typed_clause.then.type_())
.map_err(|e| e.case_clause_mismatch().into_error(typed_clause.location()))
{
if let Err(e) = unify(return_type.clone(), typed_clause.then.type_()).map_err(|e| {
e.case_clause_mismatch(typed_clause.location)
.into_error(typed_clause.then.type_defining_location())
}) {
self.problems.error(e);
}
typed_clauses.push(typed_clause);
Expand Down
6 changes: 2 additions & 4 deletions compiler-core/src/type_/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use ecow::EcoString;
use im::HashMap;
use std::{collections::HashSet, sync::Arc};

use crate::type_::{collapse_links, Type, TypeVar};
use crate::type_::{Type, TypeVar};

/// This class keeps track of what names are used for modules in the current
/// scope, so they can be printed in errors, etc.
Expand Down Expand Up @@ -126,9 +126,7 @@ fn compare_arguments(arguments: &[Arc<Type>], parameters: &[Arc<Type>]) -> bool
arguments
.iter()
.zip(parameters)
.all(|(argument, parameter)| {
collapse_links(argument.clone()) == collapse_links(parameter.clone())
})
.all(|(argument, parameter)| argument.same_as(parameter))
}

impl Names {
Expand Down
Loading
Loading