Skip to content

Commit 23a5826

Browse files
Kmeakinbrendanzab
andcommitted
Suggest a similar name when reporting UnboundName or UnknownField
Co-authored-by: Brendan Zabarauskas <[email protected]>
1 parent 5932033 commit 23a5826

File tree

8 files changed

+71
-7
lines changed

8 files changed

+71
-7
lines changed

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fathom/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ codespan-reporting = "0.11.1"
2424
fxhash = "0.2"
2525
itertools = "0.10"
2626
lalrpop-util = "0.19.5"
27+
levenshtein = "1.0.5"
2728
logos = "0.12"
2829
pretty = "0.11.2"
2930
rpds = "0.12.0"

fathom/src/surface/elaboration.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,18 @@ pub struct Context<'interner, 'arena> {
286286
messages: Vec<Message>,
287287
}
288288

289+
fn suggest_name(
290+
interner: &StringInterner,
291+
name: StringId,
292+
candidates: impl Iterator<Item = StringId>,
293+
) -> Option<StringId> {
294+
let name = interner.resolve(name).unwrap();
295+
candidates.min_by_key(|candidate| {
296+
let candidate = interner.resolve(*candidate).unwrap();
297+
levenshtein::levenshtein(name, candidate)
298+
})
299+
}
300+
289301
impl<'interner, 'arena> Context<'interner, 'arena> {
290302
/// Construct a new elaboration context, backed by the supplied arena.
291303
pub fn new(
@@ -1338,9 +1350,19 @@ impl<'interner, 'arena> Context<'interner, 'arena> {
13381350
return (core::Term::Prim(file_range.into(), prim), r#type.clone());
13391351
}
13401352

1353+
let candidates = self
1354+
.local_env
1355+
.names
1356+
.iter()
1357+
.flatten()
1358+
.copied()
1359+
.chain(self.item_env.names.iter().copied());
1360+
let suggestion = suggest_name(&self.interner.borrow(), *name, candidates);
1361+
13411362
self.push_message(Message::UnboundName {
13421363
range: file_range,
13431364
name: *name,
1365+
suggestion,
13441366
});
13451367
self.synth_reported_error(*range)
13461368
}
@@ -1654,11 +1676,17 @@ impl<'interner, 'arena> Context<'interner, 'arena> {
16541676
}
16551677

16561678
let head_type = self.pretty_print_value(&head_type);
1679+
let suggestion = suggest_name(
1680+
&self.interner.borrow(),
1681+
*proj_label,
1682+
labels.iter().map(|(_, label)| *label),
1683+
);
16571684
self.push_message(Message::UnknownField {
16581685
head_range: self.file_range(head_range),
16591686
head_type,
16601687
label_range: self.file_range(*label_range),
16611688
label: *proj_label,
1689+
suggestion,
16621690
});
16631691
return self.synth_reported_error(*range);
16641692
}

fathom/src/surface/elaboration/reporting.rs

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ pub enum Message {
1616
UnboundName {
1717
range: FileRange,
1818
name: StringId,
19+
suggestion: Option<StringId>,
1920
},
2021
RefutablePattern {
2122
pattern_range: FileRange,
@@ -47,6 +48,7 @@ pub enum Message {
4748
head_type: String,
4849
label_range: FileRange,
4950
label: StringId,
51+
suggestion: Option<StringId>,
5052
},
5153
MismatchedFieldLabels {
5254
range: FileRange,
@@ -144,14 +146,25 @@ impl Message {
144146
let secondary_label = |range: &FileRange| Label::secondary(range.file_id(), *range);
145147

146148
match self {
147-
Message::UnboundName { range, name } => {
149+
Message::UnboundName {
150+
range,
151+
name,
152+
suggestion,
153+
} => {
148154
let interner = interner.borrow();
149155
let name = interner.resolve(*name).unwrap();
150156

151-
Diagnostic::error()
157+
let mut diagnostic = Diagnostic::error()
152158
.with_message(format!("cannot find `{name}` in scope"))
153-
.with_labels(vec![primary_label(range).with_message("unbound name")])
154-
// TODO: list suggestions
159+
.with_labels(vec![primary_label(range).with_message("unbound name")]);
160+
161+
if let Some(suggestion) = suggestion {
162+
diagnostic = diagnostic.with_notes(vec![format!(
163+
"help: did you mean `{}`?",
164+
interner.resolve(*suggestion).unwrap()
165+
)])
166+
}
167+
diagnostic
155168
}
156169
Message::RefutablePattern { pattern_range } => Diagnostic::error()
157170
.with_message("refutable patterns found in binding")
@@ -208,18 +221,25 @@ impl Message {
208221
head_type,
209222
label_range,
210223
label,
224+
suggestion,
211225
} => {
212226
let interner = interner.borrow();
213227
let label = interner.resolve(*label).unwrap();
214228

215-
Diagnostic::error()
229+
let mut diagnostic = Diagnostic::error()
216230
.with_message(format!("cannot find `{label}` in expression"))
217231
.with_labels(vec![
218232
primary_label(label_range).with_message("unknown label"),
219233
secondary_label(head_range)
220234
.with_message(format!("expression of type {head_type}")),
221-
])
222-
// TODO: list suggestions
235+
]);
236+
if let Some(suggestion) = suggestion {
237+
diagnostic = diagnostic.with_notes(vec![format!(
238+
"help: did you mean `{}`?",
239+
interner.resolve(*suggestion).unwrap()
240+
)]);
241+
}
242+
diagnostic
223243
}
224244
Message::MismatchedFieldLabels {
225245
range,

tests/fail/elaboration/unification/mismatch/fun-literal-param-ann.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@ error: cannot find `a` in scope
1414
1515
3fun A => fun (a : Type) => a : fun (A : Type) -> A -> A
1616
│ ^ unbound name
17+
18+
= help: did you mean `A`?
1719

1820
'''

tests/fail/elaboration/unknown-field/record-literal.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ error: cannot find `goodbye` in expression
77
-------------- ^^^^^^^ unknown label
88
│ │
99
expression of type { hello : () }
10+
11+
= help: did you mean `goodbye`?
1012

1113
'''

tests/fail/elaboration/unknown-field/type.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ error: cannot find `foo` in expression
77
---- ^^^ unknown label
88
│ │
99
expression of type Type
10+
11+
= help: did you mean `foo`?
1012

1113
'''

tests/fail/elaboration/unknown-field/unit-literal.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ error: cannot find `goodbye` in expression
77
-- ^^^^^^^ unknown label
88
│ │
99
expression of type ()
10+
11+
= help: did you mean `goodbye`?
1012

1113
'''

0 commit comments

Comments
 (0)