mirror of
https://github.com/roc-lang/roc.git
synced 2025-10-03 00:24:34 +00:00
better indentation errors (use original location for error)
This commit is contained in:
parent
21efa8cd71
commit
19d3e43f09
5 changed files with 241 additions and 38 deletions
|
@ -444,6 +444,9 @@ where
|
||||||
let mut state = state;
|
let mut state = state;
|
||||||
let mut any_newlines = false;
|
let mut any_newlines = false;
|
||||||
|
|
||||||
|
let start_row = original_state.line;
|
||||||
|
let start_col = original_state.column;
|
||||||
|
|
||||||
let start_bytes_len = state.bytes.len();
|
let start_bytes_len = state.bytes.len();
|
||||||
|
|
||||||
while !state.bytes.is_empty() {
|
while !state.bytes.is_empty() {
|
||||||
|
@ -491,7 +494,13 @@ where
|
||||||
let progress =
|
let progress =
|
||||||
Progress::from_lengths(start_bytes_len, state.bytes.len());
|
Progress::from_lengths(start_bytes_len, state.bytes.len());
|
||||||
state = state
|
state = state
|
||||||
.check_indent_e(arena, min_indent, indent_problem)
|
.check_indent_e(
|
||||||
|
arena,
|
||||||
|
min_indent,
|
||||||
|
indent_problem,
|
||||||
|
start_row,
|
||||||
|
start_col,
|
||||||
|
)
|
||||||
.map_err(|(fail, _)| {
|
.map_err(|(fail, _)| {
|
||||||
(progress, fail, original_state.clone())
|
(progress, fail, original_state.clone())
|
||||||
})?
|
})?
|
||||||
|
@ -523,7 +532,13 @@ where
|
||||||
);
|
);
|
||||||
if any_newlines {
|
if any_newlines {
|
||||||
state = state
|
state = state
|
||||||
.check_indent_e(arena, min_indent, indent_problem)
|
.check_indent_e(
|
||||||
|
arena,
|
||||||
|
min_indent,
|
||||||
|
indent_problem,
|
||||||
|
start_row,
|
||||||
|
start_col,
|
||||||
|
)
|
||||||
.map_err(|(fail, _)| {
|
.map_err(|(fail, _)| {
|
||||||
(progress, fail, original_state.clone())
|
(progress, fail, original_state.clone())
|
||||||
})?;
|
})?;
|
||||||
|
@ -692,7 +707,13 @@ where
|
||||||
progress,
|
progress,
|
||||||
space_slice,
|
space_slice,
|
||||||
state
|
state
|
||||||
.check_indent_e(arena, min_indent, indent_problem)
|
.check_indent_e(
|
||||||
|
arena,
|
||||||
|
min_indent,
|
||||||
|
indent_problem,
|
||||||
|
start_row,
|
||||||
|
start_col,
|
||||||
|
)
|
||||||
.map_err(|(fail, _)| (progress, fail, original_state))?,
|
.map_err(|(fail, _)| (progress, fail, original_state))?,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -720,7 +741,7 @@ where
|
||||||
let progress = Progress::from_lengths(start_bytes_len, state.bytes.len());
|
let progress = Progress::from_lengths(start_bytes_len, state.bytes.len());
|
||||||
if any_newlines {
|
if any_newlines {
|
||||||
state = state
|
state = state
|
||||||
.check_indent_e(arena, min_indent, indent_problem)
|
.check_indent_e(arena, min_indent, indent_problem, start_row, start_col)
|
||||||
.map_err(|(fail, _)| (progress, fail, original_state))?;
|
.map_err(|(fail, _)| (progress, fail, original_state))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,12 +72,14 @@ impl<'a> State<'a> {
|
||||||
_arena: &'a Bump,
|
_arena: &'a Bump,
|
||||||
min_indent: u16,
|
min_indent: u16,
|
||||||
to_error: TE,
|
to_error: TE,
|
||||||
|
row: Row,
|
||||||
|
col: Col,
|
||||||
) -> Result<Self, (E, Self)>
|
) -> Result<Self, (E, Self)>
|
||||||
where
|
where
|
||||||
TE: Fn(Row, Col) -> E,
|
TE: Fn(Row, Col) -> E,
|
||||||
{
|
{
|
||||||
if self.indent_col < min_indent {
|
if self.indent_col < min_indent {
|
||||||
Err((to_error(self.line, self.column), self))
|
Err((to_error(row, col), self))
|
||||||
} else {
|
} else {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
@ -378,6 +380,7 @@ pub enum Type<'a> {
|
||||||
TStart(Row, Col),
|
TStart(Row, Col),
|
||||||
TEnd(Row, Col),
|
TEnd(Row, Col),
|
||||||
TSpace(BadInputError, Row, Col),
|
TSpace(BadInputError, Row, Col),
|
||||||
|
TFunctionArgument(Row, Col),
|
||||||
///
|
///
|
||||||
TIndentStart(Row, Col),
|
TIndentStart(Row, Col),
|
||||||
TIndentEnd(Row, Col),
|
TIndentEnd(Row, Col),
|
||||||
|
@ -1496,6 +1499,31 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn check_indent<'a, TE, E>(min_indent: u16, to_problem: TE) -> impl Parser<'a, (), E>
|
||||||
|
where
|
||||||
|
TE: Fn(Row, Col) -> E,
|
||||||
|
E: 'a,
|
||||||
|
{
|
||||||
|
move |_arena, state: State<'a>| {
|
||||||
|
dbg!(state.indent_col, min_indent);
|
||||||
|
if state.indent_col < min_indent {
|
||||||
|
Err((NoProgress, to_problem(state.line, state.column), state))
|
||||||
|
} else {
|
||||||
|
Ok((NoProgress, (), state))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! word1_check_indent {
|
||||||
|
($word:expr, $word_problem:expr, $min_indent:expr, $indent_problem:expr) => {
|
||||||
|
and!(
|
||||||
|
word1($word, $word_problem),
|
||||||
|
crate::parser::check_indent($min_indent, $indent_problem)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
fn in_context<'a, AddContext, P1, P2, Start, A, X, Y>(
|
fn in_context<'a, AddContext, P1, P2, Start, A, X, Y>(
|
||||||
add_context: AddContext,
|
add_context: AddContext,
|
||||||
|
|
|
@ -1,15 +1,13 @@
|
||||||
use crate::ast::{AssignedField, CommentOrNewline, Tag, TypeAnnotation};
|
use crate::ast::{AssignedField, CommentOrNewline, Tag, TypeAnnotation};
|
||||||
use crate::blankspace::{
|
use crate::blankspace::{space0_around_e, space0_before_e, space0_e};
|
||||||
space0, space0_around, space0_around_e, space0_before, space0_before_e, space0_e,
|
|
||||||
};
|
|
||||||
use crate::expr::{global_tag, private_tag};
|
use crate::expr::{global_tag, private_tag};
|
||||||
use crate::ident::join_module_parts;
|
use crate::ident::join_module_parts;
|
||||||
use crate::keyword;
|
use crate::keyword;
|
||||||
use crate::parser::{
|
use crate::parser::{
|
||||||
allocated, ascii_char, ascii_string, backtrackable, not_e, optional, peek_utf8_char_e,
|
allocated, backtrackable, not_e, optional, peek_utf8_char_e, specialize, specialize_ref, word1,
|
||||||
specialize, specialize_ref, word1, word2, BadInputError, Col, Either, ParseResult, Parser,
|
word2, BadInputError, Either, ParseResult, Parser,
|
||||||
Progress::{self, *},
|
Progress::{self, *},
|
||||||
Row, State, SyntaxError, TApply, TInParens, TRecord, TTagUnion, TVariable, Type,
|
State, SyntaxError, TApply, TInParens, TRecord, TTagUnion, TVariable, Type,
|
||||||
};
|
};
|
||||||
use bumpalo::collections::string::String;
|
use bumpalo::collections::string::String;
|
||||||
use bumpalo::collections::vec::Vec;
|
use bumpalo::collections::vec::Vec;
|
||||||
|
@ -54,20 +52,6 @@ fn tag_union_type<'a>(min_indent: u16) -> impl Parser<'a, TypeAnnotation<'a>, TT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_indent<'a, TE, E>(min_indent: u16, to_problem: TE) -> impl Parser<'a, (), E>
|
|
||||||
where
|
|
||||||
TE: Fn(Row, Col) -> E,
|
|
||||||
E: 'a,
|
|
||||||
{
|
|
||||||
move |_arena, state: State<'a>| {
|
|
||||||
if state.indent_col < min_indent {
|
|
||||||
Err((NoProgress, to_problem(state.line, state.column), state))
|
|
||||||
} else {
|
|
||||||
Ok((NoProgress, (), state))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
fn term<'a>(min_indent: u16) -> impl Parser<'a, Located<TypeAnnotation<'a>>, SyntaxError<'a>> {
|
fn term<'a>(min_indent: u16) -> impl Parser<'a, Located<TypeAnnotation<'a>>, SyntaxError<'a>> {
|
||||||
specialize(|x, _, _| SyntaxError::Type(x), term_help(min_indent))
|
specialize(|x, _, _| SyntaxError::Type(x), term_help(min_indent))
|
||||||
|
@ -247,7 +231,7 @@ fn record_type_field<'a>(
|
||||||
debug_assert_eq!(progress, MadeProgress);
|
debug_assert_eq!(progress, MadeProgress);
|
||||||
|
|
||||||
let (_, spaces, state) =
|
let (_, spaces, state) =
|
||||||
space0_e(min_indent, TRecord::Space, TRecord::IndentEnd).parse(arena, state)?;
|
debug!(space0_e(min_indent, TRecord::Space, TRecord::IndentEnd)).parse(arena, state)?;
|
||||||
|
|
||||||
// Having a value is optional; both `{ email }` and `{ email: blah }` work.
|
// Having a value is optional; both `{ email }` and `{ email: blah }` work.
|
||||||
// (This is true in both literals and types.)
|
// (This is true in both literals and types.)
|
||||||
|
@ -312,9 +296,11 @@ fn record_type<'a>(min_indent: u16) -> impl Parser<'a, TypeAnnotation<'a>, TReco
|
||||||
|
|
||||||
move |arena, state| {
|
move |arena, state| {
|
||||||
let (_, (fields, final_comments), state) = collection_trailing_sep_e!(
|
let (_, (fields, final_comments), state) = collection_trailing_sep_e!(
|
||||||
|
// word1_check_indent!(b'{', TRecord::Open, min_indent, TRecord::IndentOpen),
|
||||||
word1(b'{', TRecord::Open),
|
word1(b'{', TRecord::Open),
|
||||||
loc!(record_type_field(min_indent)),
|
loc!(record_type_field(min_indent)),
|
||||||
word1(b',', TRecord::End),
|
word1(b',', TRecord::End),
|
||||||
|
// word1_check_indent!(b'}', TRecord::End, min_indent, TRecord::IndentEnd),
|
||||||
word1(b'}', TRecord::End),
|
word1(b'}', TRecord::End),
|
||||||
min_indent,
|
min_indent,
|
||||||
TRecord::Open,
|
TRecord::Open,
|
||||||
|
@ -377,13 +363,20 @@ fn expression<'a>(min_indent: u16) -> impl Parser<'a, Located<TypeAnnotation<'a>
|
||||||
.parse(arena, state)?;
|
.parse(arena, state)?;
|
||||||
|
|
||||||
let (p2, rest, state) = zero_or_more!(skip_first!(
|
let (p2, rest, state) = zero_or_more!(skip_first!(
|
||||||
word1(b',', Type::TStart),
|
word1(b',', Type::TFunctionArgument),
|
||||||
space0_around_e(
|
one_of![
|
||||||
term_help(min_indent),
|
space0_around_e(
|
||||||
min_indent,
|
term_help(min_indent),
|
||||||
Type::TSpace,
|
min_indent,
|
||||||
Type::TIndentStart
|
Type::TSpace,
|
||||||
)
|
Type::TIndentStart
|
||||||
|
),
|
||||||
|
|_, state: State<'a>| Err((
|
||||||
|
NoProgress,
|
||||||
|
Type::TFunctionArgument(state.line, state.column),
|
||||||
|
state
|
||||||
|
))
|
||||||
|
]
|
||||||
))
|
))
|
||||||
.parse(arena, state)?;
|
.parse(arena, state)?;
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,24 @@ fn to_syntax_report<'a>(
|
||||||
|
|
||||||
report(doc)
|
report(doc)
|
||||||
}
|
}
|
||||||
|
SyntaxError::Eof(region) => {
|
||||||
|
let doc = alloc.stack(vec![alloc.reflow("End of Field"), alloc.region(*region)]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "PARSE PROBLEM".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SyntaxError::OutdentedTooFar => {
|
||||||
|
let doc = alloc.stack(vec![alloc.reflow("OutdentedTooFar")]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "PARSE PROBLEM".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Type(typ) => to_type_report(alloc, filename, &typ, 0, 0),
|
Type(typ) => to_type_report(alloc, filename, &typ, 0, 0),
|
||||||
_ => todo!("unhandled parse error: {:?}", parse_problem),
|
_ => todo!("unhandled parse error: {:?}", parse_problem),
|
||||||
}
|
}
|
||||||
|
@ -126,6 +144,43 @@ fn to_type_report<'a>(
|
||||||
}
|
}
|
||||||
Type::TApply(tapply, row, col) => to_tapply_report(alloc, filename, &tapply, *row, *col),
|
Type::TApply(tapply, row, col) => to_tapply_report(alloc, filename, &tapply, *row, *col),
|
||||||
|
|
||||||
|
Type::TFunctionArgument(row, col) => match what_is_next(alloc.src_lines, *row, *col) {
|
||||||
|
Next::Other(Some(',')) => {
|
||||||
|
let surroundings = Region::from_rows_cols(start_row, start_col, *row, *col);
|
||||||
|
let region = Region::from_row_col(*row, *col);
|
||||||
|
|
||||||
|
let doc = alloc.stack(vec![
|
||||||
|
alloc.reflow(r"I just started parsing a function argument type, but I encounterd two commas in a row:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.concat(vec![alloc.reflow("Try removing one of them.")]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "DOUBLE COMMA".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
},
|
||||||
|
|
||||||
|
Type::TStart(row, col) => {
|
||||||
|
let surroundings = Region::from_rows_cols(start_row, start_col, *row, *col);
|
||||||
|
let region = Region::from_row_col(*row, *col);
|
||||||
|
|
||||||
|
let doc = alloc.stack(vec![
|
||||||
|
alloc.reflow(r"I just started parsing a type, but I got stuck here:"),
|
||||||
|
alloc.region_with_subregion(surroundings, region),
|
||||||
|
alloc.note("I may be confused by indentation"),
|
||||||
|
]);
|
||||||
|
|
||||||
|
Report {
|
||||||
|
filename,
|
||||||
|
doc,
|
||||||
|
title: "UNFINISHED TYPE".to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Type::TIndentStart(row, col) => {
|
Type::TIndentStart(row, col) => {
|
||||||
let surroundings = Region::from_rows_cols(start_row, start_col, *row, *col);
|
let surroundings = Region::from_rows_cols(start_row, start_col, *row, *col);
|
||||||
let region = Region::from_row_col(*row, *col);
|
let region = Region::from_row_col(*row, *col);
|
||||||
|
@ -342,7 +397,7 @@ fn to_trecord_report<'a>(
|
||||||
}
|
}
|
||||||
|
|
||||||
TRecord::IndentEnd(row, col) => {
|
TRecord::IndentEnd(row, col) => {
|
||||||
match next_line_starts_with_close_curly(alloc.src_lines, row - 1) {
|
match next_line_starts_with_close_curly(alloc.src_lines, row.saturating_sub(1)) {
|
||||||
Some((curly_row, curly_col)) => {
|
Some((curly_row, curly_col)) => {
|
||||||
let surroundings =
|
let surroundings =
|
||||||
Region::from_rows_cols(start_row, start_col, curly_row, curly_col);
|
Region::from_rows_cols(start_row, start_col, curly_row, curly_col);
|
||||||
|
|
|
@ -4246,8 +4246,7 @@ mod test_reporting {
|
||||||
I am partway through parsing a record type, but I got stuck here:
|
I am partway through parsing a record type, but I got stuck here:
|
||||||
|
|
||||||
1│ f : {
|
1│ f : {
|
||||||
2│ foo : I64,
|
^
|
||||||
^
|
|
||||||
|
|
||||||
I was expecting to see a closing curly brace before this, so try
|
I was expecting to see a closing curly brace before this, so try
|
||||||
adding a } and see if that helps?
|
adding a } and see if that helps?
|
||||||
|
@ -4589,8 +4588,115 @@ mod test_reporting {
|
||||||
I just started parsing an inline type alias, but I got stuck here:
|
I just started parsing an inline type alias, but I got stuck here:
|
||||||
|
|
||||||
1│ f : I64 as
|
1│ f : I64 as
|
||||||
2│ f = 0
|
^
|
||||||
^
|
|
||||||
|
Note: I may be confused by indentation
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_double_comma() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
f : I64,,I64 -> I64
|
||||||
|
f = 0
|
||||||
|
|
||||||
|
f
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── DOUBLE COMMA ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I just started parsing a function argument type, but I encounterd two
|
||||||
|
commas in a row:
|
||||||
|
|
||||||
|
1│ f : I64,,I64 -> I64
|
||||||
|
^
|
||||||
|
|
||||||
|
Try removing one of them.
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_argument_no_arrow() {
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
f : I64, I64
|
||||||
|
f = 0
|
||||||
|
|
||||||
|
f
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── UNFINISHED TYPE ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I just started parsing a type, but I got stuck here:
|
||||||
|
|
||||||
|
1│ f : I64, I64
|
||||||
|
^
|
||||||
|
|
||||||
|
Note: I may be confused by indentation
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn type_argument_arrow_then_nothing() {
|
||||||
|
// TODO could do better by pointing out we're parsing a function type
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
f : I64, I64 ->
|
||||||
|
f = 0
|
||||||
|
|
||||||
|
f
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── UNFINISHED TYPE ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I just started parsing a type, but I got stuck here:
|
||||||
|
|
||||||
|
1│ f : I64, I64 ->
|
||||||
|
^
|
||||||
|
|
||||||
|
Note: I may be confused by indentation
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn foobar() {
|
||||||
|
// TODO fix error on new row
|
||||||
|
// we should make whitespace only consumed when it puts us in a validly-indented position
|
||||||
|
report_problem_as(
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
f : I64 ->
|
||||||
|
f = 0
|
||||||
|
|
||||||
|
f
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
indoc!(
|
||||||
|
r#"
|
||||||
|
── UNFINISHED TYPE ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
I just started parsing a type, but I got stuck here:
|
||||||
|
|
||||||
|
1│ f : I64 ->
|
||||||
|
^
|
||||||
|
|
||||||
Note: I may be confused by indentation
|
Note: I may be confused by indentation
|
||||||
"#
|
"#
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue