Skip to content
This repository was archived by the owner on Apr 2, 2026. It is now read-only.

Commit d84d7e7

Browse files
committed
Added DelimitedBy::with_mismatched_end, improved handling of mismatched delimiter errors
1 parent e6894cd commit d84d7e7

9 files changed

Lines changed: 286 additions & 93 deletions

File tree

examples/json.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ fn parser<'a>() -> impl Parser<'a, &'a str, Json, extra::Err<Rich<'a, char>>> {
3535
.then(exp.or_not())
3636
.to_slice()
3737
.map(|s: &str| s.parse().unwrap())
38+
.labelled("number")
3839
.boxed();
3940

4041
let escape = just('\\')
@@ -68,6 +69,8 @@ fn parser<'a>() -> impl Parser<'a, &'a str, Json, extra::Err<Rich<'a, char>>> {
6869
.to_slice()
6970
.map(ToString::to_string)
7071
.delimited_by(just('"'), just('"'))
72+
.labelled("string")
73+
.as_context()
7174
.boxed();
7275

7376
let array = value
@@ -86,9 +89,16 @@ fn parser<'a>() -> impl Parser<'a, &'a str, Json, extra::Err<Rich<'a, char>>> {
8689
.recover_with(via_parser(end()))
8790
.recover_with(skip_then_retry_until(any().ignored(), end())),
8891
)
92+
.labelled("array")
93+
.as_context()
8994
.boxed();
9095

91-
let member = string.clone().then_ignore(just(':').padded()).then(value);
96+
let member = string
97+
.clone()
98+
.then_ignore(just(':').padded())
99+
.then(value)
100+
.labelled("object member")
101+
.as_context();
92102
let object = member
93103
.clone()
94104
.separated_by(just(',').padded().recover_with(skip_then_retry_until(
@@ -104,12 +114,14 @@ fn parser<'a>() -> impl Parser<'a, &'a str, Json, extra::Err<Rich<'a, char>>> {
104114
.recover_with(via_parser(end()))
105115
.recover_with(skip_then_retry_until(any().ignored(), end())),
106116
)
117+
.labelled("object")
118+
.as_context()
107119
.boxed();
108120

109121
choice((
110-
just("null").to(Json::Null),
111-
just("true").to(Json::Bool(true)),
112-
just("false").to(Json::Bool(false)),
122+
just("null").to(Json::Null).labelled("null"),
123+
just("true").to(Json::Bool(true)).labelled("boolean"),
124+
just("false").to(Json::Bool(false)).labelled("boolean"),
113125
number.map(Json::Num),
114126
string.map(Json::Str),
115127
array.map(Json::Array),
@@ -150,6 +162,11 @@ fn main() {
150162
.with_message(e.reason().to_string())
151163
.with_color(Color::Red),
152164
)
165+
.with_labels(e.contexts().next().map(|ctx| {
166+
Label::new(((), ctx.span().into_range()))
167+
.with_message(format!("while parsing this {}", ctx.pattern()))
168+
.with_color(Color::Yellow)
169+
}))
153170
.finish()
154171
.print(Source::from(&src))
155172
.unwrap()

examples/mini_ml.rs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ fn lexer<'src>(
7878
.repeated()
7979
.collect()
8080
.delimited_by(just('('), just(')'))
81-
.labelled("token tree")
81+
.with_mismatched_end(one_of("]})"))
82+
.labelled("expression")
8283
.as_context()
8384
.map(Token::Parens),
8485
))
@@ -137,7 +138,9 @@ fn parser<'tokens, 'src: 'tokens>() -> impl Parser<
137138
lhs,
138139
rhs: Box::new(rhs),
139140
then: Box::new(then),
140-
}),
141+
})
142+
.labelled("expression")
143+
.as_context(),
141144
));
142145

143146
choice((
@@ -183,8 +186,6 @@ fn parser<'tokens, 'src: 'tokens>() -> impl Parser<
183186
})
184187
.boxed(),
185188
])
186-
.labelled("expression")
187-
.as_context()
188189
})
189190
}
190191

@@ -257,8 +258,8 @@ impl Solver<'_> {
257258
format!("Type mismatch between {a_info} and {b_info}"),
258259
("mismatch occurred here".to_string(), span),
259260
vec![
260-
(format!("{a_info}"), self.vars[a.0].1),
261-
(format!("{b_info}"), self.vars[b.0].1),
261+
(format!("{a_info}"), self.vars[a.0].1, Color::Yellow),
262+
(format!("{b_info}"), self.vars[b.0].1, Color::Yellow),
262263
],
263264
self.src,
264265
),
@@ -408,7 +409,7 @@ impl<'src> Vm<'src> {
408409
fn failure(
409410
msg: String,
410411
label: (String, SimpleSpan),
411-
extra_labels: impl IntoIterator<Item = (String, SimpleSpan)>,
412+
extra_labels: impl IntoIterator<Item = (String, SimpleSpan, Color)>,
412413
src: &str,
413414
) -> ! {
414415
let fname = "example";
@@ -420,10 +421,10 @@ fn failure(
420421
.with_message(label.0)
421422
.with_color(Color::Red),
422423
)
423-
.with_labels(extra_labels.into_iter().map(|label2| {
424-
Label::new((fname, label2.1.into_range()))
425-
.with_message(label2.0)
426-
.with_color(Color::Yellow)
424+
.with_labels(extra_labels.into_iter().map(|(pat, span, col)| {
425+
Label::new((fname, span.into_range()))
426+
.with_message(pat)
427+
.with_color(col)
427428
}))
428429
.finish()
429430
.print(sources([(fname, src)]))
@@ -440,8 +441,24 @@ fn parse_failure(err: &Rich<impl fmt::Display>, src: &str) -> ! {
440441
.unwrap_or_else(|| "end of input".to_string()),
441442
*err.span(),
442443
),
443-
err.contexts()
444-
.map(|(l, s)| (format!("while parsing this {l}"), *s)),
444+
err.reason()
445+
.unclosed_delimiter()
446+
.map(|ud| {
447+
(
448+
"this delimiter was never closed".to_string(),
449+
*ud,
450+
Color::Blue,
451+
)
452+
})
453+
.into_iter()
454+
.chain(err.contexts().map(|ctx| {
455+
(
456+
format!("while parsing this {}", ctx.pattern()),
457+
*ctx.span(),
458+
Color::Yellow,
459+
)
460+
}))
461+
.take(1),
445462
src,
446463
)
447464
}

examples/nano_rust.rs

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,10 @@ where
300300
// Blocks are expressions but delimited with braces
301301
let block = expr
302302
.clone()
303+
// Empty blocks are permitted
304+
.or(empty().map_with(|(), e| (Expr::Value(Value::Null), e.span())))
303305
.delimited_by(just(Token::Ctrl('{')), just(Token::Ctrl('}')))
306+
.with_mismatched_end(one_of([Token::Ctrl(']'), Token::Ctrl(')')]))
304307
// Attempt to recover anything that looks like a block but contains errors
305308
.recover_with(via_parser(nested_delimiters(
306309
Token::Ctrl('{'),
@@ -310,7 +313,9 @@ where
310313
(Token::Ctrl('['), Token::Ctrl(']')),
311314
],
312315
|span| (Expr::Error, span),
313-
)));
316+
)))
317+
.labelled("block")
318+
.as_context();
314319

315320
let if_ = recursive(|if_| {
316321
just(Token::If)
@@ -343,30 +348,9 @@ where
343348
(Expr::Then(Box::new(a), Box::new(b)), e.span())
344349
});
345350

346-
let block_recovery = nested_delimiters(
347-
Token::Ctrl('{'),
348-
Token::Ctrl('}'),
349-
[
350-
(Token::Ctrl('('), Token::Ctrl(')')),
351-
(Token::Ctrl('['), Token::Ctrl(']')),
352-
],
353-
|span| (Expr::Error, span),
354-
);
355-
356351
block_chain
357-
.labelled("block")
358352
// Expressions, chained by semicolons, are statements
359353
.or(inline_expr.clone())
360-
.recover_with(skip_then_retry_until(
361-
block_recovery.ignored().or(any().ignored()),
362-
one_of([
363-
Token::Ctrl(';'),
364-
Token::Ctrl('}'),
365-
Token::Ctrl(')'),
366-
Token::Ctrl(']'),
367-
])
368-
.ignored(),
369-
))
370354
.foldl_with(
371355
just(Token::Ctrl(';')).ignore_then(expr.or_not()).repeated(),
372356
|a, b, e| {
@@ -395,27 +379,39 @@ fn funcs_parser<'tokens, 'src: 'tokens, I>() -> impl Parser<
395379
where
396380
I: ValueInput<'tokens, Token = Token<'src>, Span = Span>,
397381
{
398-
let ident = select! { Token::Ident(ident) => ident };
382+
let ident = select! { Token::Ident(ident) => ident }.labelled("identifier");
399383

400384
// Argument lists are just identifiers separated by commas, surrounded by parentheses
401385
let args = ident
402386
.separated_by(just(Token::Ctrl(',')))
403387
.allow_trailing()
404388
.collect()
405389
.delimited_by(just(Token::Ctrl('(')), just(Token::Ctrl(')')))
406-
.labelled("function args");
390+
.with_mismatched_end(one_of([Token::Ctrl(']'), Token::Ctrl('}')]))
391+
.labelled("argument list")
392+
.as_context()
393+
.recover_with(via_parser(nested_delimiters(
394+
Token::Ctrl('('),
395+
Token::Ctrl(')'),
396+
[
397+
(Token::Ctrl('{'), Token::Ctrl('}')),
398+
(Token::Ctrl('['), Token::Ctrl(']')),
399+
],
400+
|_| Vec::new(),
401+
)));
407402

408403
let func = just(Token::Fn)
409-
.ignore_then(
410-
ident
411-
.map_with(|name, e| (name, e.span()))
412-
.labelled("function name"),
413-
)
404+
.ignore_then(ident.map_with(|name, e| (name, e.span())))
414405
.then(args)
415406
.map_with(|start, e| (start, e.span()))
416407
.then(
417408
expr_parser()
409+
// Empty function bodies are permitted
410+
.or(empty().map_with(|(), e| (Expr::Value(Value::Null), e.span())))
418411
.delimited_by(just(Token::Ctrl('{')), just(Token::Ctrl('}')))
412+
.with_mismatched_end(one_of([Token::Ctrl(']'), Token::Ctrl(')')]))
413+
.labelled("function body")
414+
.as_context()
419415
// Attempt to recover anything that looks like a function body but contains errors
420416
.recover_with(via_parser(nested_delimiters(
421417
Token::Ctrl('{'),
@@ -610,9 +606,14 @@ fn main() {
610606
.with_message(e.reason().to_string())
611607
.with_color(Color::Red),
612608
)
613-
.with_labels(e.contexts().map(|(label, span)| {
614-
Label::new((filename.clone(), span.into_range()))
615-
.with_message(format!("while parsing this {label}"))
609+
.with_labels(e.reason().unclosed_delimiter().map(|ud| {
610+
Label::new((filename.clone(), ud.into_range()))
611+
.with_message("this delimiter was never closed")
612+
.with_color(Color::Blue)
613+
}))
614+
.with_labels(e.contexts().take(1).map(|ctx| {
615+
Label::new((filename.clone(), ctx.span().into_range()))
616+
.with_message(format!("while parsing this {}", ctx.pattern()))
616617
.with_color(Color::Yellow)
617618
}))
618619
.finish()

examples/sample.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"origin": 981339097,
1616
},
1717
"though": ttrue asasjk,
18-
"invalid": "\uDFFF",
18+
"invalid": "hello \uDFFF world",
1919
"activity": "value",
2020
"office": -342325541.1937506,
2121
"noise": false,

src/combinator.rs

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,40 +1445,86 @@ where
14451445
}
14461446

14471447
/// See [`Parser::delimited_by`].
1448-
pub struct DelimitedBy<A, B, C, OB, OC> {
1448+
pub struct DelimitedBy<A, B, C, OB, OC, D = crate::primitive::Never, OD = Infallible> {
14491449
pub(crate) parser: A,
14501450
pub(crate) start: B,
14511451
pub(crate) end: C,
1452+
pub(crate) invalid_end: D,
14521453
#[allow(dead_code)]
1453-
pub(crate) phantom: EmptyPhantom<(OB, OC)>,
1454+
pub(crate) phantom: EmptyPhantom<(OB, OC, OD)>,
14541455
}
14551456

1456-
impl<A: Copy, B: Copy, C: Copy, OB, OC> Copy for DelimitedBy<A, B, C, OB, OC> {}
1457-
impl<A: Clone, B: Clone, C: Clone, OB, OC> Clone for DelimitedBy<A, B, C, OB, OC> {
1457+
impl<A: Copy, B: Copy, C: Copy, OB, OC, D: Copy, OD> Copy for DelimitedBy<A, B, C, OB, OC, D, OD> {}
1458+
impl<A: Clone, B: Clone, C: Clone, OB, OC, D: Clone, OD> Clone
1459+
for DelimitedBy<A, B, C, OB, OC, D, OD>
1460+
{
14581461
fn clone(&self) -> Self {
14591462
Self {
14601463
parser: self.parser.clone(),
14611464
start: self.start.clone(),
14621465
end: self.end.clone(),
1466+
invalid_end: self.invalid_end.clone(),
1467+
phantom: EmptyPhantom::new(),
1468+
}
1469+
}
1470+
}
1471+
1472+
impl<A, B, C, OB, OC, D, OD> DelimitedBy<A, B, C, OB, OC, D, OD> {
1473+
/// Indicate that when the given mismatched pattern is found, errors should be marked as 'unclosed delimiters'.
1474+
///
1475+
/// This can result in improved error messages in certain cases.
1476+
pub fn with_mismatched_end<D2, OD2>(self, end: D2) -> DelimitedBy<A, B, C, OB, OC, D2, OD2> {
1477+
DelimitedBy {
1478+
parser: self.parser,
1479+
start: self.start,
1480+
end: self.end,
1481+
invalid_end: end,
14631482
phantom: EmptyPhantom::new(),
14641483
}
14651484
}
14661485
}
14671486

1468-
impl<'src, I, E, A, B, C, OA, OB, OC> Parser<'src, I, OA, E> for DelimitedBy<A, B, C, OB, OC>
1487+
impl<'src, I, E, A, B, C, OA, OB, OC, D, OD> Parser<'src, I, OA, E>
1488+
for DelimitedBy<A, B, C, OB, OC, D, OD>
14691489
where
14701490
I: Input<'src>,
14711491
E: ParserExtra<'src, I>,
14721492
A: Parser<'src, I, OA, E>,
14731493
B: Parser<'src, I, OB, E>,
14741494
C: Parser<'src, I, OC, E>,
1495+
D: Parser<'src, I, OD, E>,
1496+
I::Span: Clone,
14751497
{
14761498
#[inline(always)]
14771499
fn go<M: Mode>(&self, inp: &mut InputRef<'src, '_, I, E>) -> PResult<M, OA> {
1500+
let before_start = inp.cursor();
14781501
self.start.go::<Check>(inp)?;
1502+
let start_span = inp.span_since(&before_start);
14791503
let a = self.parser.go::<M>(inp)?;
1480-
self.end.go::<Check>(inp)?;
1481-
Ok(a)
1504+
// let before_errors = inp.errors.secondary.len();
1505+
let before_end = inp.save();
1506+
let old_alt = inp.take_alt();
1507+
let scope_span = inp.span_since(&before_start);
1508+
let end_res = self.end.go::<Check>(inp);
1509+
let res = if end_res.is_err() {
1510+
let mut new_alt = inp.take_alt().expect("error, but no alt");
1511+
inp.rewind(before_end);
1512+
if self.invalid_end.go::<Check>(inp).is_ok() {
1513+
new_alt
1514+
.err
1515+
.in_delimited(start_span.clone(), scope_span.clone());
1516+
}
1517+
inp.errors.alt = old_alt;
1518+
inp.add_alt_err(&new_alt.pos, new_alt.err);
1519+
Err(())
1520+
} else {
1521+
inp.errors.alt = old_alt;
1522+
Ok(a)
1523+
};
1524+
// for err in inp.errors.secondary_errors_since(before_errors) {
1525+
// err.err.in_delimited(start_span.clone(), scope_span.clone());
1526+
// }
1527+
res
14821528
}
14831529

14841530
go_extra!(OA);

0 commit comments

Comments
 (0)