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

Improve diagnostic messages for errors in elaborating record projections #504

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions fathom/src/core/semantics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ impl<'arena> Value<'arena> {
}
}

pub fn is_unit_type(&self) -> bool {
match self {
Value::RecordType(labels, _) => labels.is_empty(),
_ => false,
}
}

pub fn is_error(&self) -> bool {
matches!(self, Value::Stuck(Head::Prim(Prim::ReportedError), _))
}
Expand Down
52 changes: 33 additions & 19 deletions fathom/src/surface/elaboration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1597,14 +1597,24 @@ impl<'arena> Context<'arena> {
(term, r#type)
}
Term::Proj(range, head_expr, labels) => {
let head_range = head_expr.range();
let mut head_range = head_expr.range();
let (mut head_expr, mut head_type) = self.synth_and_insert_implicit_apps(head_expr);

'labels: for (label_range, proj_label) in *labels {
head_type = self.elim_env().force(&head_type);
match (&head_expr, head_type.as_ref()) {
match head_type.as_ref() {
r#type if r#type.is_unit_type() => {
self.push_message(Message::RecordProjUnit {
head_range: self.file_range(head_range),
head_type: self.pretty_value(&head_type),
label_range: self.file_range(*label_range),
label: *proj_label,
});
return self.synth_reported_error(*range);
}

// Ensure that the head of the projection is a record
(_, Value::RecordType(labels, types)) => {
Value::RecordType(labels, types) => {
let mut labels = labels.iter().copied();
let mut types = types.clone();

Expand All @@ -1618,9 +1628,9 @@ impl<'arena> Context<'arena> {
if *proj_label == label {
// The field was found. Update the head expression
// and continue elaborating the next projection.
head_range = ByteRange::merge(head_range, *label_range);
head_expr = core::Term::RecordProj(
self.file_range(ByteRange::merge(head_range, *label_range))
.into(),
self.file_range(head_range).into(),
self.scope.to_scope(head_expr),
*proj_label,
);
Expand All @@ -1636,28 +1646,32 @@ impl<'arena> Context<'arena> {
}
}
// Couldn't find the field in the record type.
// Fallthrough with an error.
self.push_message(Message::RecordProjNotFound {
head_range: self.file_range(head_range),
head_type: self.pretty_value(&head_type),
label_range: self.file_range(*label_range),
label: *proj_label,
suggested_label: suggest_name(*proj_label, labels),
});
return self.synth_reported_error(*range);
}
// There's been an error when elaborating the head of
// the projection, so avoid trying to elaborate any
// further to prevent cascading type errors.
(core::Term::Prim(_, Prim::ReportedError), _)
| (_, Value::Stuck(Head::Prim(Prim::ReportedError), _)) => {
_ if head_expr.is_error() || head_type.is_error() => {
return self.synth_reported_error(*range);
}
// The head expression was not a record type.
// Fallthrough with an error.
_ => {}
_ => {
self.push_message(Message::RecordProjNotRecord {
head_range: self.file_range(head_range),
head_type: self.pretty_value(&head_type),
label_range: self.file_range(*label_range),
label: *proj_label,
});
return self.synth_reported_error(*range);
}
}

self.push_message(Message::UnknownField {
head_range: self.file_range(head_range),
head_type: self.pretty_value(&head_type),
label_range: self.file_range(*label_range),
label: *proj_label,
suggested_label: suggest_name(*proj_label, labels.iter().map(|(_, l)| *l)),
});
return self.synth_reported_error(*range);
}

(head_expr, head_type)
Expand Down
54 changes: 48 additions & 6 deletions fathom/src/surface/elaboration/reporting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,19 @@ pub enum Message {
arg_range: FileRange,
arg_plicity: Plicity,
},
UnknownField {
RecordProjNotRecord {
head_range: FileRange,
head_type: String,
label_range: FileRange,
label: Symbol,
},
RecordProjUnit {
head_range: FileRange,
head_type: String,
label_range: FileRange,
label: Symbol,
},
RecordProjNotFound {
head_range: FileRange,
head_type: String,
label_range: FileRange,
Expand Down Expand Up @@ -205,18 +217,48 @@ impl Message {
secondary_label(head_range)
.with_message(format!("{head_plicity} function of type {head_type}")),
]),
Message::UnknownField {
Message::RecordProjNotRecord {
head_range,
head_type,
label_range,
label,
} => Diagnostic::error()
.with_message(format!(
"tried to access field `{}` of non-record expression",
label.resolve()
))
.with_labels(vec![
primary_label(head_range)
.with_message(format!("expression of type `{head_type}`")),
secondary_label(label_range).with_message("field access"),
]),
Message::RecordProjUnit {
head_range,
head_type,
label_range,
label,
} => Diagnostic::error()
.with_message(format!(
"tried to access field `{}` of empty record",
label.resolve()
))
.with_labels(vec![
primary_label(head_range)
.with_message(format!("expression of type `{head_type}`")),
secondary_label(label_range).with_message("field access"),
]),
Message::RecordProjNotFound {
head_range,
head_type,
label_range,
label,
suggested_label,
} => Diagnostic::error()
.with_message(format!("cannot find `{}` in expression", label.resolve()))
.with_message(format!("no field named `{}` in record", label.resolve()))
.with_labels(vec![
primary_label(label_range).with_message("unknown label"),
secondary_label(head_range)
.with_message(format!("expression of type {head_type}")),
primary_label(head_range)
.with_message(format!("expression of type `{head_type}`")),
secondary_label(label_range).with_message("unknown field"),
])
.with_notes(suggested_label.map_or(Vec::new(), |label| {
vec![format!("help: did you mean `{}`?", label.resolve())]
Expand Down
6 changes: 6 additions & 0 deletions tests/fail/elaboration/record-proj/field-not-found.fathom
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//~ exit-code = 1

let _ : Bool = {x=false}.y;
let _ : Bool = {x=false}.x.y;

{}
19 changes: 19 additions & 0 deletions tests/fail/elaboration/record-proj/field-not-found.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
stdout = ''
stderr = '''
error: no field named `y` in record
┌─ tests/fail/elaboration/record-proj/field-not-found.fathom:3:16
3 │ let _ : Bool = {x=false}.y;
│ ^^^^^^^^^ - unknown field
│ │
│ expression of type `{ x : Bool }`

error: tried to access field `y` of non-record expression
┌─ tests/fail/elaboration/record-proj/field-not-found.fathom:4:16
4 │ let _ : Bool = {x=false}.x.y;
│ ^^^^^^^^^^^ - field access
│ │
│ expression of type `Bool`

'''
5 changes: 5 additions & 0 deletions tests/fail/elaboration/record-proj/head-is-unit.fathom
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
//~ exit-code = 1

let _ : Bool = {}.foo;

{}
11 changes: 11 additions & 0 deletions tests/fail/elaboration/record-proj/head-is-unit.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
stdout = ''
stderr = '''
error: tried to access field `foo` of empty record
┌─ tests/fail/elaboration/record-proj/head-is-unit.fathom:3:16
3 │ let _ : Bool = {}.foo;
│ ^^ --- field access
│ │
│ expression of type `()`

'''
6 changes: 6 additions & 0 deletions tests/fail/elaboration/record-proj/head-not-record.fathom
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
//~ exit-code = 1

let _ : Bool = Bool.foo;
let _ : Bool = {x=false}.x.foo;

{}
19 changes: 19 additions & 0 deletions tests/fail/elaboration/record-proj/head-not-record.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
stdout = ''
stderr = '''
error: tried to access field `foo` of non-record expression
┌─ tests/fail/elaboration/record-proj/head-not-record.fathom:3:16
3 │ let _ : Bool = Bool.foo;
│ ^^^^ --- field access
│ │
│ expression of type `Type`

error: tried to access field `foo` of non-record expression
┌─ tests/fail/elaboration/record-proj/head-not-record.fathom:4:16
4 │ let _ : Bool = {x=false}.x.foo;
│ ^^^^^^^^^^^ --- field access
│ │
│ expression of type `Bool`

'''
3 changes: 0 additions & 3 deletions tests/fail/elaboration/unknown-field/record-literal.fathom

This file was deleted.

13 changes: 0 additions & 13 deletions tests/fail/elaboration/unknown-field/record-literal.snap

This file was deleted.

3 changes: 0 additions & 3 deletions tests/fail/elaboration/unknown-field/type.fathom

This file was deleted.

13 changes: 0 additions & 13 deletions tests/fail/elaboration/unknown-field/type.snap

This file was deleted.

3 changes: 0 additions & 3 deletions tests/fail/elaboration/unknown-field/unbound-head.fathom

This file was deleted.

9 changes: 0 additions & 9 deletions tests/fail/elaboration/unknown-field/unbound-head.snap

This file was deleted.

3 changes: 0 additions & 3 deletions tests/fail/elaboration/unknown-field/unit-literal.fathom

This file was deleted.

13 changes: 0 additions & 13 deletions tests/fail/elaboration/unknown-field/unit-literal.snap

This file was deleted.