mirror of
https://github.com/roc-lang/roc.git
synced 2025-10-02 00:01:16 +00:00
add a bunch of hints
This commit is contained in:
parent
5e226ee5f4
commit
85e2cf4465
4 changed files with 267 additions and 19 deletions
|
@ -19,6 +19,7 @@ inlinable_string = "0.1.0"
|
||||||
im = "14" # im and im-rc should always have the same version!
|
im = "14" # im and im-rc should always have the same version!
|
||||||
im-rc = "14" # im and im-rc should always have the same version!
|
im-rc = "14" # im and im-rc should always have the same version!
|
||||||
distance = "0.4.0"
|
distance = "0.4.0"
|
||||||
|
bumpalo = { version = "3.2", features = ["collections"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
roc_constrain = { path = "../constrain" }
|
roc_constrain = { path = "../constrain" }
|
||||||
|
@ -30,4 +31,3 @@ maplit = "1.0.1"
|
||||||
indoc = "0.3.3"
|
indoc = "0.3.3"
|
||||||
quickcheck = "0.8"
|
quickcheck = "0.8"
|
||||||
quickcheck_macros = "0.8"
|
quickcheck_macros = "0.8"
|
||||||
bumpalo = { version = "3.2", features = ["collections"] }
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use crate::report::ReportText::{BinOp, Concat, Module, Region, Value};
|
use crate::report::ReportText::{BinOp, Concat, Module, Region, Value};
|
||||||
|
use bumpalo::Bump;
|
||||||
use roc_module::ident::TagName;
|
use roc_module::ident::TagName;
|
||||||
use roc_module::symbol::{Interns, ModuleId, Symbol};
|
use roc_module::symbol::{Interns, ModuleId, Symbol};
|
||||||
use roc_problem::can::PrecedenceProblem::BothNonAssociative;
|
use roc_problem::can::PrecedenceProblem::BothNonAssociative;
|
||||||
|
@ -527,11 +528,12 @@ impl ReportText {
|
||||||
src_lines: &[&str],
|
src_lines: &[&str],
|
||||||
interns: &Interns,
|
interns: &Interns,
|
||||||
) {
|
) {
|
||||||
|
let arena = Bump::new();
|
||||||
let alloc = BoxAllocator;
|
let alloc = BoxAllocator;
|
||||||
|
|
||||||
let err_msg = "<buffer is not a utf-8 encoded string>";
|
let err_msg = "<buffer is not a utf-8 encoded string>";
|
||||||
|
|
||||||
self.pretty::<_>(&alloc, subs, home, src_lines, interns)
|
self.pretty::<_>(&alloc, &arena, subs, home, src_lines, interns)
|
||||||
.1
|
.1
|
||||||
.render_raw(70, &mut CiWrite::new(buf))
|
.render_raw(70, &mut CiWrite::new(buf))
|
||||||
.expect(err_msg);
|
.expect(err_msg);
|
||||||
|
@ -547,11 +549,12 @@ impl ReportText {
|
||||||
interns: &Interns,
|
interns: &Interns,
|
||||||
palette: &Palette,
|
palette: &Palette,
|
||||||
) {
|
) {
|
||||||
|
let arena = Bump::new();
|
||||||
let alloc = BoxAllocator;
|
let alloc = BoxAllocator;
|
||||||
|
|
||||||
let err_msg = "<buffer is not a utf-8 encoded string>";
|
let err_msg = "<buffer is not a utf-8 encoded string>";
|
||||||
|
|
||||||
self.pretty::<_>(&alloc, subs, home, src_lines, interns)
|
self.pretty::<_>(&alloc, &arena, subs, home, src_lines, interns)
|
||||||
.1
|
.1
|
||||||
.render_raw(70, &mut ColorWrite::new(palette, buf))
|
.render_raw(70, &mut ColorWrite::new(palette, buf))
|
||||||
.expect(err_msg);
|
.expect(err_msg);
|
||||||
|
@ -562,6 +565,7 @@ impl ReportText {
|
||||||
pub fn pretty<'b, D>(
|
pub fn pretty<'b, D>(
|
||||||
self,
|
self,
|
||||||
alloc: &'b D,
|
alloc: &'b D,
|
||||||
|
arena: &'b Bump,
|
||||||
subs: &mut Subs,
|
subs: &mut Subs,
|
||||||
home: ModuleId,
|
home: ModuleId,
|
||||||
src_lines: &'b [&'b str],
|
src_lines: &'b [&'b str],
|
||||||
|
@ -656,14 +660,14 @@ impl ReportText {
|
||||||
.append(alloc.hardline())
|
.append(alloc.hardline())
|
||||||
.append(
|
.append(
|
||||||
error_type
|
error_type
|
||||||
.pretty(alloc, subs, home, src_lines, interns)
|
.pretty(alloc, arena, subs, home, src_lines, interns)
|
||||||
.indent(4)
|
.indent(4)
|
||||||
.annotate(Annotation::TypeBlock),
|
.annotate(Annotation::TypeBlock),
|
||||||
)
|
)
|
||||||
.append(alloc.hardline()),
|
.append(alloc.hardline()),
|
||||||
|
|
||||||
Indent(n, nested) => {
|
Indent(n, nested) => {
|
||||||
let rest = nested.pretty(alloc, subs, home, src_lines, interns);
|
let rest = nested.pretty(alloc, arena, subs, home, src_lines, interns);
|
||||||
alloc.nil().append(rest).indent(n)
|
alloc.nil().append(rest).indent(n)
|
||||||
}
|
}
|
||||||
Docs(_) => {
|
Docs(_) => {
|
||||||
|
@ -672,22 +676,22 @@ impl ReportText {
|
||||||
Concat(report_texts) => alloc.concat(
|
Concat(report_texts) => alloc.concat(
|
||||||
report_texts
|
report_texts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|rep| rep.pretty(alloc, subs, home, src_lines, interns)),
|
.map(|rep| rep.pretty(alloc, arena, subs, home, src_lines, interns)),
|
||||||
),
|
),
|
||||||
Stack(report_texts) => alloc
|
Stack(report_texts) => alloc
|
||||||
.intersperse(
|
.intersperse(
|
||||||
report_texts
|
report_texts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|rep| (rep.pretty(alloc, subs, home, src_lines, interns))),
|
.map(|rep| (rep.pretty(alloc, arena, subs, home, src_lines, interns))),
|
||||||
alloc.hardline(),
|
alloc.hardline(),
|
||||||
)
|
)
|
||||||
.append(alloc.hardline()),
|
.append(alloc.hardline()),
|
||||||
Intersperse { separator, items } => alloc.intersperse(
|
Intersperse { separator, items } => alloc.intersperse(
|
||||||
items
|
items
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|rep| (rep.pretty(alloc, subs, home, src_lines, interns)))
|
.map(|rep| (rep.pretty(alloc, arena, subs, home, src_lines, interns)))
|
||||||
.collect::<Vec<_>>(),
|
.collect::<Vec<_>>(),
|
||||||
separator.pretty(alloc, subs, home, src_lines, interns),
|
separator.pretty(alloc, arena, subs, home, src_lines, interns),
|
||||||
),
|
),
|
||||||
BinOp(bin_op) => alloc.text(bin_op.to_string()).annotate(Annotation::BinOp),
|
BinOp(bin_op) => alloc.text(bin_op.to_string()).annotate(Annotation::BinOp),
|
||||||
Region(region) => {
|
Region(region) => {
|
||||||
|
@ -780,12 +784,15 @@ impl ReportText {
|
||||||
Name(ident) => alloc
|
Name(ident) => alloc
|
||||||
.text(format!("{}", ident.as_inline_str()))
|
.text(format!("{}", ident.as_inline_str()))
|
||||||
.annotate(Annotation::Symbol),
|
.annotate(Annotation::Symbol),
|
||||||
TypeProblem(problem) => Self::type_problem_to_pretty(alloc, home, interns, problem),
|
TypeProblem(problem) => {
|
||||||
|
Self::type_problem_to_pretty(alloc, arena, home, interns, problem)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn type_problem_to_pretty<'b, D>(
|
fn type_problem_to_pretty<'b, D>(
|
||||||
alloc: &'b D,
|
alloc: &'b D,
|
||||||
|
arena: &'b Bump,
|
||||||
home: ModuleId,
|
home: ModuleId,
|
||||||
interns: &Interns,
|
interns: &Interns,
|
||||||
problem: crate::type_error::Problem,
|
problem: crate::type_error::Problem,
|
||||||
|
@ -827,6 +834,26 @@ impl ReportText {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
FieldsMissing(missing) => match missing.split_last() {
|
||||||
|
None => alloc.nil(),
|
||||||
|
Some((f1, [])) => Self::hint(alloc)
|
||||||
|
.append(alloc.reflow("Looks like the "))
|
||||||
|
.append(f1.as_str().to_owned())
|
||||||
|
.append(alloc.reflow(" field is missing.")),
|
||||||
|
Some((last, init)) => {
|
||||||
|
let separator = alloc.reflow(", ");
|
||||||
|
|
||||||
|
Self::hint(alloc)
|
||||||
|
.append(alloc.reflow("Looks like the "))
|
||||||
|
.append(
|
||||||
|
alloc
|
||||||
|
.intersperse(init.iter().map(|v| v.as_str().to_owned()), separator),
|
||||||
|
)
|
||||||
|
.append(alloc.reflow(" and "))
|
||||||
|
.append(alloc.text(last.as_str().to_owned()))
|
||||||
|
.append(alloc.reflow(" fields are missing."))
|
||||||
|
}
|
||||||
|
},
|
||||||
TagTypo(typo, possibilities_tn) => {
|
TagTypo(typo, possibilities_tn) => {
|
||||||
let possibilities = possibilities_tn
|
let possibilities = possibilities_tn
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -860,6 +887,59 @@ impl ReportText {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ArityMismatch(found, expected) => {
|
||||||
|
let line = if found < expected {
|
||||||
|
format!(
|
||||||
|
"It looks like it takes too few arguments. I was expecting {} more.",
|
||||||
|
expected - found
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!(
|
||||||
|
"It looks like it takes too many arguments. I'm seeing {} extra.",
|
||||||
|
found - expected
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Self::hint(alloc).append(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
BadRigidVar(x, tipe) => {
|
||||||
|
use ErrorType::*;
|
||||||
|
|
||||||
|
let bad_rigid_var = |name: &str, a_thing| {
|
||||||
|
let text = format!(
|
||||||
|
r#"The type annotation uses the type variable `{}` to say that this definition can produce any type of value. But in the body I see that it will only produce {} of a single specific type. Maybe change the type annotation to be more specific? Maybe change the code to be more general?"#,
|
||||||
|
name, a_thing
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::hint(alloc).append(alloc.reflow(arena.alloc(text)))
|
||||||
|
};
|
||||||
|
|
||||||
|
let bad_double_rigid = |a, b| {
|
||||||
|
let text = format!(
|
||||||
|
r#"Your type annotation uses {} and {} as separate type variables. Your code seems to be saying they are the same though. Maybe they should be the same your type annotation? Maybe your code uses them in a weird way?"#,
|
||||||
|
a, b
|
||||||
|
);
|
||||||
|
|
||||||
|
Self::hint(alloc).append(alloc.reflow(arena.alloc(text)))
|
||||||
|
};
|
||||||
|
|
||||||
|
match tipe {
|
||||||
|
Infinite | Error | FlexVar(_) => alloc.nil(),
|
||||||
|
RigidVar(y) => bad_double_rigid(x.as_str(), y.as_str()),
|
||||||
|
Function(_, _) => bad_rigid_var(x.as_str(), "a function value"),
|
||||||
|
Record(_, _) => bad_rigid_var(x.as_str(), "a record value"),
|
||||||
|
TagUnion(_, _) | RecursiveTagUnion(_, _, _) => {
|
||||||
|
bad_rigid_var(x.as_str(), "a tag value")
|
||||||
|
}
|
||||||
|
Alias(symbol, _, _) | Type(symbol, _) => bad_rigid_var(
|
||||||
|
x.as_str(),
|
||||||
|
&format!("a {} value", symbol.ident_string(interns)),
|
||||||
|
),
|
||||||
|
Boolean(_) => bad_rigid_var(x.as_str(), "a uniqueness attribute value"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1143,15 +1143,22 @@ fn to_diff(parens: Parens, type1: &ErrorType, type2: &ErrorType) -> Diff<ReportT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {
|
pair => {
|
||||||
// We hit none of the specific cases where we give more detailed information
|
// We hit none of the specific cases where we give more detailed information
|
||||||
let left = to_doc(Parens::Unnecessary, type1);
|
let left = to_doc(Parens::Unnecessary, type1);
|
||||||
let right = to_doc(Parens::Unnecessary, type2);
|
let right = to_doc(Parens::Unnecessary, type2);
|
||||||
|
|
||||||
|
let problems = match pair {
|
||||||
|
(RigidVar(x), other) | (other, RigidVar(x)) => {
|
||||||
|
vec![Problem::BadRigidVar(x.clone(), other.clone())]
|
||||||
|
}
|
||||||
|
_ => vec![],
|
||||||
|
};
|
||||||
|
|
||||||
Diff {
|
Diff {
|
||||||
left,
|
left,
|
||||||
right,
|
right,
|
||||||
status: Status::Similar,
|
status: Status::Different(problems),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1200,8 +1207,8 @@ fn diff_record(
|
||||||
let diff = to_diff(Parens::Unnecessary, t1, t2);
|
let diff = to_diff(Parens::Unnecessary, t1, t2);
|
||||||
|
|
||||||
Diff {
|
Diff {
|
||||||
left: (plain_text(field.as_str()), diff.left),
|
left: (field.clone(), plain_text(field.as_str()), diff.left),
|
||||||
right: (plain_text(field.as_str()), diff.right),
|
right: (field.clone(), plain_text(field.as_str()), diff.right),
|
||||||
status: diff.status,
|
status: diff.status,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -1262,7 +1269,7 @@ fn diff_record(
|
||||||
|
|
||||||
let ext_diff = ext_to_diff(ext1, ext2);
|
let ext_diff = ext_to_diff(ext1, ext2);
|
||||||
|
|
||||||
let mut fields_diff: Diff<Vec<(ReportText, ReportText)>> = Diff {
|
let mut fields_diff: Diff<Vec<(Lowercase, ReportText, ReportText)>> = Diff {
|
||||||
left: vec![],
|
left: vec![],
|
||||||
right: vec![],
|
right: vec![],
|
||||||
status: Status::Similar,
|
status: Status::Similar,
|
||||||
|
@ -1275,13 +1282,31 @@ fn diff_record(
|
||||||
}
|
}
|
||||||
|
|
||||||
if !all_fields_shared {
|
if !all_fields_shared {
|
||||||
fields_diff.left.extend(left.map(|(_, x, y)| (x, y)));
|
fields_diff.left.extend(left);
|
||||||
fields_diff.right.extend(right.map(|(_, x, y)| (x, y)));
|
fields_diff.right.extend(right);
|
||||||
fields_diff.status.merge(Status::Different(vec![]));
|
fields_diff.status.merge(Status::Different(vec![]));
|
||||||
}
|
}
|
||||||
|
|
||||||
let doc1 = report_text::record(fields_diff.left, ext_diff.left);
|
// sort fields for display
|
||||||
let doc2 = report_text::record(fields_diff.right, ext_diff.right);
|
fields_diff.left.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
fields_diff.right.sort_by(|a, b| a.0.cmp(&b.0));
|
||||||
|
|
||||||
|
let doc1 = report_text::record(
|
||||||
|
fields_diff
|
||||||
|
.left
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, b, c)| (b, c))
|
||||||
|
.collect(),
|
||||||
|
ext_diff.left,
|
||||||
|
);
|
||||||
|
let doc2 = report_text::record(
|
||||||
|
fields_diff
|
||||||
|
.right
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, b, c)| (b, c))
|
||||||
|
.collect(),
|
||||||
|
ext_diff.right,
|
||||||
|
);
|
||||||
|
|
||||||
fields_diff.status.merge(status);
|
fields_diff.status.merge(status);
|
||||||
|
|
||||||
|
|
|
@ -1624,4 +1624,147 @@ mod test_reporting {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_fields() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
x : { a : Int, b : Float, c : Bool }
|
||||||
|
x = { b: 4.0 }
|
||||||
|
|
||||||
|
x
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
Something is off with the body of the `x` definition:
|
||||||
|
|
||||||
|
2 ┆ x = { b: 4.0 }
|
||||||
|
┆ ^^^^^^^^^^
|
||||||
|
|
||||||
|
The body is a record of type:
|
||||||
|
|
||||||
|
{ b : Float }
|
||||||
|
|
||||||
|
But the type annotation on `x` says it should be:
|
||||||
|
|
||||||
|
{ a : Int, b : Float, c : Bool }
|
||||||
|
|
||||||
|
|
||||||
|
Hint: Looks like the c and a fields are missing.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_double_rigid() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
f : a, b -> a
|
||||||
|
f = \x, y -> if True then x else y
|
||||||
|
|
||||||
|
f
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
Something is off with the body of the `f` definition:
|
||||||
|
|
||||||
|
2 ┆ f = \x, y -> if True then x else y
|
||||||
|
┆ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
The body is an anonymous function of type:
|
||||||
|
|
||||||
|
a, a -> a
|
||||||
|
|
||||||
|
But the type annotation on `f` says it should be:
|
||||||
|
|
||||||
|
a, b -> a
|
||||||
|
|
||||||
|
|
||||||
|
Hint: Your type annotation uses a and b as separate type variables.
|
||||||
|
Your code seems to be saying they are the same though. Maybe they
|
||||||
|
should be the same your type annotation? Maybe your code uses them in
|
||||||
|
a weird way?
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_rigid_function() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
f : Bool -> msg
|
||||||
|
f = \_ -> Foo
|
||||||
|
|
||||||
|
f
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
Something is off with the body of the `f` definition:
|
||||||
|
|
||||||
|
2 ┆ f = \_ -> Foo
|
||||||
|
┆ ^^^^^^^^^
|
||||||
|
|
||||||
|
The body is an anonymous function of type:
|
||||||
|
|
||||||
|
Bool -> [ Foo ]a
|
||||||
|
|
||||||
|
But the type annotation on `f` says it should be:
|
||||||
|
|
||||||
|
Bool -> msg
|
||||||
|
|
||||||
|
|
||||||
|
Hint: The type annotation uses the type variable `msg` to say that
|
||||||
|
this definition can produce any type of value. But in the body I see
|
||||||
|
that it will only produce a tag value of a single specific type. Maybe
|
||||||
|
change the type annotation to be more specific? Maybe change the code
|
||||||
|
to be more general?
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_rigid_value() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
f : msg
|
||||||
|
f = 0x3
|
||||||
|
|
||||||
|
f
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
Something is off with the body of the `f` definition:
|
||||||
|
|
||||||
|
2 ┆ f = 0x3
|
||||||
|
┆ ^^^
|
||||||
|
|
||||||
|
The body is an integer of type:
|
||||||
|
|
||||||
|
Int
|
||||||
|
|
||||||
|
But the type annotation on `f` says it should be:
|
||||||
|
|
||||||
|
msg
|
||||||
|
|
||||||
|
|
||||||
|
Hint: The type annotation uses the type variable `msg` to say that
|
||||||
|
this definition can produce any type of value. But in the body I see
|
||||||
|
that it will only produce a Int value of a single specific type. Maybe
|
||||||
|
change the type annotation to be more specific? Maybe change the code
|
||||||
|
to be more general?
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue