Drop type_limit

This commit is contained in:
Richard Feldman 2019-11-25 22:19:33 -05:00
parent 0d69d7e90f
commit 90d463873e
7 changed files with 239 additions and 244 deletions

View file

@ -1,10 +1,3 @@
// TODO reduce this limit by replacing more parser combinator functions with
// macros. It's the way these functions (but not the macros) interact that
//
// See https://bodil.lol/parser-combinators for more information; the parser
// is based on her design./ causes the need for higher limits.
#![type_length_limit = "3735510"]
pub mod can;
pub mod collections;
pub mod graph;

View file

@ -1,36 +0,0 @@
use bumpalo::collections::Vec;
use parse::ast::Spaceable;
use parse::blankspace::space0_around;
use parse::parser::{sep_by0, skip_first, skip_second, Parser};
use region::Located;
/// Parse zero or more elements between two braces (e.g. square braces).
/// Elements can be optionally surrounded by spaces, and are separated by a
/// delimiter (e.g comma-separated). Braces and delimiters get discarded.
pub fn collection<'a, Elem, OpeningBrace, ClosingBrace, Delimiter, S>(
opening_brace: OpeningBrace,
elem: Elem,
delimiter: Delimiter,
closing_brace: ClosingBrace,
min_indent: u16,
) -> impl Parser<'a, Vec<'a, Located<S>>>
where
OpeningBrace: Parser<'a, ()>,
Elem: Parser<'a, Located<S>>,
Elem: 'a,
Delimiter: Parser<'a, ()>,
S: Spaceable<'a>,
S: 'a,
ClosingBrace: Parser<'a, ()>,
{
// TODO allow trailing commas before the closing delimiter, *but* without
// losing any comments or newlines! This will require parsing them and then,
// if they are present, merging them into the final Spaceable.
skip_first(
opening_brace,
skip_second(
sep_by0(delimiter, space0_around(elem, min_indent)),
closing_brace,
),
)
}

View file

@ -2,13 +2,11 @@
pub mod parser;
pub mod ast;
pub mod blankspace;
pub mod collection;
pub mod ident;
pub mod keyword;
pub mod module;
pub mod number_literal;
pub mod problems;
pub mod record;
pub mod string_literal;
pub mod type_annotation;
@ -21,14 +19,12 @@ use parse::blankspace::{
space0, space0_after, space0_around, space0_before, space1, space1_after, space1_around,
space1_before,
};
use parse::collection::collection;
use parse::ident::{ident, lowercase_ident, variant_or_ident, Ident};
use parse::number_literal::number_literal;
use parse::parser::{
allocated, between, char, not, not_followed_by, optional, skip_first, skip_second, string,
then, unexpected, unexpected_eof, Either, Fail, FailReason, ParseResult, Parser, State,
allocated, char, not, not_followed_by, optional, string, then, unexpected, unexpected_eof,
Either, Fail, FailReason, ParseResult, Parser, State,
};
use parse::record::record;
use region::{Located, Region};
pub fn expr<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> {
@ -128,7 +124,7 @@ fn parse_expr<'a>(min_indent: u16, arena: &'a Bump, state: State<'a>) -> ParseRe
pub fn loc_parenthetical_expr<'a>(min_indent: u16) -> impl Parser<'a, Located<Expr<'a>>> {
then(
loc!(and!(
between(
between!(
char('('),
map_with_arena!(
space0_around(
@ -142,7 +138,7 @@ pub fn loc_parenthetical_expr<'a>(min_indent: u16) -> impl Parser<'a, Located<Ex
}
}
),
char(')'),
char(')')
),
optional(either!(
// There may optionally be function args after the ')'
@ -158,7 +154,7 @@ pub fn loc_parenthetical_expr<'a>(min_indent: u16) -> impl Parser<'a, Located<Ex
// as if there were any args they'd have consumed it anyway
// e.g. in `((foo bar) baz.blah)` the `.blah` will be consumed by the `baz` parser
either!(
one_or_more!(skip_first(char('.'), lowercase_ident())),
one_or_more!(skip_first!(char('.'), lowercase_ident())),
and!(space0(min_indent), equals_with_indent())
)
))
@ -364,10 +360,10 @@ pub fn loc_parenthetical_def<'a>(min_indent: u16) -> impl Parser<'a, Located<Exp
move |arena, state| {
let (loc_tuple, state) = loc!(and!(
space0_after(
between(
between!(
char('('),
space0_around(loc!(pattern(min_indent)), min_indent),
char(')'),
char(')')
),
min_indent,
),
@ -414,21 +410,21 @@ pub fn def<'a>(min_indent: u16) -> impl Parser<'a, Def<'a>> {
space0_after(loc_closure_param(min_indent), min_indent),
either!(
// Constant
skip_first(
skip_first!(
equals_for_def(),
// Spaces after the '=' (at a normal indentation level) and then the expr.
// The expr itself must be indented more than the pattern and '='
space0_before(
loc!(move |arena, state| parse_expr(indented_more, arena, state)),
min_indent,
),
)
),
// Annotation
skip_first(
skip_first!(
char(':'),
// Spaces after the ':' (at a normal indentation level) and then the type.
// The type itself must be indented more than the pattern and ':'
space0_before(type_annotation::located(indented_more), indented_more),
space0_before(type_annotation::located(indented_more), indented_more)
)
)
),
@ -440,7 +436,7 @@ pub fn def<'a>(min_indent: u16) -> impl Parser<'a, Def<'a>> {
// Type alias or custom type (uppercase ident followed by `:` or `:=` and type annotation)
map!(
and!(
skip_second(
skip_second!(
// TODO FIXME this may need special logic to parse the first part of the type,
// then parse the rest with increased indentation. The current implementation
// may not correctly handle scenarios like this:
@ -456,17 +452,17 @@ pub fn def<'a>(min_indent: u16) -> impl Parser<'a, Def<'a>> {
// This seems likely enough to be broken that it's worth trying to reproduce
// and then fix! (Or, if everything is somehow fine, delete this comment.)
space0_after(type_annotation::located(min_indent), min_indent),
char(':'),
char(':')
),
either!(
// Custom type
skip_first(
skip_first!(
// The `=` in `:=` (at this point we already consumed the `:`)
char('='),
one_or_more!(space0_before(
type_annotation::located(min_indent),
min_indent,
)),
))
),
// Alias
space0_before(type_annotation::located(min_indent), min_indent)
@ -556,13 +552,13 @@ fn parse_def_expr<'a>(
}
fn loc_function_arg<'a>(min_indent: u16) -> impl Parser<'a, Located<Expr<'a>>> {
skip_first(
skip_first!(
// If this is a reserved keyword ("if", "then", "case, "when"), then
// it is not a function argument!
not(reserved_keyword()),
// Don't parse operators, because they have a higher precedence than function application.
// If we encounter one, we're done parsing function args!
move |arena, state| loc_parse_function_arg(min_indent, arena, state),
move |arena, state| loc_parse_function_arg(min_indent, arena, state)
)
}
@ -598,7 +594,7 @@ fn reserved_keyword<'a>() -> impl Parser<'a, ()> {
fn closure<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> {
map_with_arena!(
skip_first(
skip_first!(
// All closures start with a '\' - e.g. (\x -> x + 1)
char('\\'),
// Once we see the '\', we're committed to parsing this as a closure.
@ -615,7 +611,7 @@ fn closure<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> {
// the "->" but that does not seem worthwhile.
one_or_more!(space1_after(loc_closure_param(min_indent), min_indent))
),
skip_first(
skip_first!(
// Parse the -> which separates params from body
string("->"),
// Parse the body
@ -625,9 +621,9 @@ fn closure<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> {
loc!(move |arena, state| parse_expr(min_indent, arena, state)),
min_indent,
)
),
)
)
)),
))
),
|arena: &'a Bump, opt_contents| match opt_contents {
None => Expr::MalformedClosure,
@ -654,10 +650,10 @@ fn parse_closure_param<'a>(
loc!(record_destructure(min_indent)),
// If you wrap it in parens, you can match any arbitrary pattern at all.
// e.g. \User.UserId userId -> ...
between(
between!(
char('('),
space0_around(loc!(pattern(min_indent)), min_indent),
char(')'),
char(')')
),
// The least common, but still allowed, e.g. \Foo -> ...
loc!(map!(unqualified_variant(), |name| {
@ -697,12 +693,12 @@ fn underscore_pattern<'a>() -> impl Parser<'a, Pattern<'a>> {
fn record_destructure<'a>(min_indent: u16) -> impl Parser<'a, Pattern<'a>> {
map!(
collection(
collection!(
char('{'),
loc!(ident_pattern()),
char(','),
char('}'),
min_indent,
min_indent
),
Pattern::RecordDestructure
)
@ -722,12 +718,12 @@ pub fn case_expr<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> {
case_with_indent(),
attempt!(
Attempting::CaseCondition,
skip_second(
skip_second!(
space1_around(
loc!(move |arena, state| parse_expr(min_indent, arena, state)),
min_indent,
),
string(keyword::WHEN),
string(keyword::WHEN)
)
)
),
@ -773,13 +769,13 @@ pub fn case_branches<'a>(
};
// Parse the first "->" and the expression after it.
let (loc_first_expr, mut state) = skip_first(
let (loc_first_expr, mut state) = skip_first!(
string("->"),
// The expr must be indented more than the pattern preceding it
space0_before(
loc!(move |arena, state| parse_expr(indented_more, arena, state)),
indented_more,
),
)
)
.parse(arena, state)?;
@ -799,12 +795,12 @@ pub fn case_branches<'a>(
}
},
),
skip_first(
skip_first!(
string("->"),
space1_before(
loc!(move |arena, state| parse_expr(min_indent, arena, state)),
min_indent,
),
)
)
);
@ -830,27 +826,27 @@ pub fn case_branches<'a>(
pub fn if_expr<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> {
map_with_arena!(
and!(
skip_first(
skip_first!(
string(keyword::IF),
space1_around(
loc!(move |arena, state| parse_expr(min_indent, arena, state)),
min_indent,
),
)
),
and!(
skip_first(
skip_first!(
string(keyword::THEN),
space1_around(
loc!(move |arena, state| parse_expr(min_indent, arena, state)),
min_indent,
),
)
),
skip_first(
skip_first!(
string(keyword::ELSE),
space1_before(
loc!(move |arena, state| parse_expr(min_indent, arena, state)),
min_indent,
),
)
)
)
),
@ -1086,12 +1082,12 @@ fn binop<'a>() -> impl Parser<'a, BinOp> {
}
pub fn list_literal<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> {
let elems = collection(
let elems = collection!(
char('['),
loc!(expr(min_indent)),
char(','),
char(']'),
min_indent,
min_indent
);
parser::attempt(
@ -1114,7 +1110,7 @@ pub fn record_literal<'a>(min_indent: u16) -> impl Parser<'a, Expr<'a>> {
and!(
attempt!(
Attempting::Record,
loc!(record(loc!(expr(min_indent)), min_indent))
loc!(record!(loc!(expr(min_indent)), min_indent))
),
optional(and!(space0(min_indent), equals_with_indent()))
),

View file

@ -5,13 +5,9 @@ use parse::ast::{
Module,
};
use parse::blankspace::{space1, space1_around};
use parse::collection::collection;
use parse::ident::unqualified_ident;
use parse::parse;
use parse::parser::{
self, char, loc, optional, skip_first, skip_second, string, unexpected, unexpected_eof, Parser,
State,
};
use parse::parser::{self, char, loc, optional, string, unexpected, unexpected_eof, Parser, State};
use region::Located;
pub fn module<'a>() -> impl Parser<'a, Module<'a>> {
@ -36,7 +32,7 @@ pub fn app_module<'a>() -> impl Parser<'a, Module<'a>> {
pub fn interface_header<'a>() -> impl Parser<'a, InterfaceHeader<'a>> {
parser::map(
and!(
skip_first(string("interface"), and!(space1(1), loc!(module_name()))),
skip_first!(string("interface"), and!(space1(1), loc!(module_name()))),
and!(exposes(), imports())
),
|(
@ -151,8 +147,8 @@ fn exposes<'a>() -> impl Parser<
),
> {
and!(
and!(skip_second(space1(1), string("exposes")), space1(1)),
collection(char('['), loc!(exposes_entry()), char(','), char(']'), 1)
and!(skip_second!(space1(1), string("exposes")), space1(1)),
collection!(char('['), loc!(exposes_entry()), char(','), char(']'), 1)
)
}
@ -165,8 +161,8 @@ fn imports<'a>() -> impl Parser<
),
> {
and!(
and!(skip_second(space1(1), string("imports")), space1(1)),
collection(char('['), loc!(imports_entry()), char(','), char(']'), 1)
and!(skip_second!(space1(1), string("imports")), space1(1)),
collection!(char('['), loc!(imports_entry()), char(','), char(']'), 1)
)
}
@ -182,9 +178,9 @@ fn imports_entry<'a>() -> impl Parser<'a, ImportsEntry<'a>> {
// e.g. `Task`
module_name(),
// e.g. `.{ Task, after}`
optional(skip_first(
optional(skip_first!(
char('.'),
collection(char('{'), loc!(exposes_entry()), char(','), char('}'), 1)
collection!(char('{'), loc!(exposes_entry()), char(','), char('}'), 1)
))
),
|arena,

View file

@ -394,21 +394,6 @@ pub fn string<'a>(keyword: &'static str) -> impl Parser<'a, ()> {
}
}
/// Parse everything between two braces (e.g. parentheses), skipping both braces
/// and keeping only whatever was parsed in between them.
pub fn between<'a, P, OpeningBrace, ClosingBrace, Val>(
opening_brace: OpeningBrace,
parser: P,
closing_brace: ClosingBrace,
) -> impl Parser<'a, Val>
where
OpeningBrace: Parser<'a, ()>,
P: Parser<'a, Val>,
ClosingBrace: Parser<'a, ()>,
{
skip_first(opening_brace, skip_second(parser, closing_brace))
}
/// Parse zero or more values separated by a delimiter (e.g. a comma) whose
/// values are discarded
pub fn sep_by0<'a, P, D, Val>(delimiter: D, parser: P) -> impl Parser<'a, Vec<'a, Val>>
@ -479,69 +464,6 @@ where
}
}
/// If the first one parses, ignore its output and move on to parse with the second one.
pub fn skip_first<'a, P1, P2, A, B>(p1: P1, p2: P2) -> impl Parser<'a, B>
where
P1: Parser<'a, A>,
P2: Parser<'a, B>,
{
move |arena: &'a Bump, state: State<'a>| {
let original_attempting = state.attempting;
match p1.parse(arena, state) {
Ok((_, state)) => match p2.parse(arena, state) {
Ok((out2, state)) => Ok((out2, state)),
Err((fail, state)) => Err((
Fail {
attempting: original_attempting,
..fail
},
state,
)),
},
Err((fail, state)) => Err((
Fail {
attempting: original_attempting,
..fail
},
state,
)),
}
}
}
/// If the first one parses, parse the second one; if it also parses, use the
/// output from the first one.
pub fn skip_second<'a, P1, P2, A, B>(p1: P1, p2: P2) -> impl Parser<'a, A>
where
P1: Parser<'a, A>,
P2: Parser<'a, B>,
{
move |arena: &'a Bump, state: State<'a>| {
let original_attempting = state.attempting;
match p1.parse(arena, state) {
Ok((out1, state)) => match p2.parse(arena, state) {
Ok((_, state)) => Ok((out1, state)),
Err((fail, state)) => Err((
Fail {
attempting: original_attempting,
..fail
},
state,
)),
},
Err((fail, state)) => Err((
Fail {
attempting: original_attempting,
..fail
},
state,
)),
}
}
}
pub fn optional<'a, P, T>(parser: P) -> impl Parser<'a, Option<T>>
where
P: Parser<'a, T>,
@ -585,6 +507,93 @@ macro_rules! loc {
};
}
/// If the first one parses, ignore its output and move on to parse with the second one.
#[macro_export]
macro_rules! skip_first {
($p1:expr, $p2:expr) => {
move |arena, state: $crate::parse::parser::State<'a>| {
use $crate::parse::parser::Fail;
let original_attempting = state.attempting;
match $p1.parse(arena, state) {
Ok((_, state)) => match $p2.parse(arena, state) {
Ok((out2, state)) => Ok((out2, state)),
Err((fail, state)) => Err((
Fail {
attempting: original_attempting,
..fail
},
state,
)),
},
Err((fail, state)) => Err((
Fail {
attempting: original_attempting,
..fail
},
state,
)),
}
}
};
}
/// If the first one parses, parse the second one; if it also parses, use the
/// output from the first one.
#[macro_export]
macro_rules! skip_second {
($p1:expr, $p2:expr) => {
move |arena, state: $crate::parse::parser::State<'a>| {
use $crate::parse::parser::Fail;
let original_attempting = state.attempting;
match $p1.parse(arena, state) {
Ok((out1, state)) => match $p2.parse(arena, state) {
Ok((_, state)) => Ok((out1, state)),
Err((fail, state)) => Err((
Fail {
attempting: original_attempting,
..fail
},
state,
)),
},
Err((fail, state)) => Err((
Fail {
attempting: original_attempting,
..fail
},
state,
)),
}
}
};
}
/// Parse zero or more elements between two braces (e.g. square braces).
/// Elements can be optionally surrounded by spaces, and are separated by a
/// delimiter (e.g comma-separated). Braces and delimiters get discarded.
#[macro_export]
macro_rules! collection {
($opening_brace:expr, $elem:expr, $delimiter:expr, $closing_brace:expr, $min_indent:expr) => {
// TODO allow trailing commas before the closing delimiter, *but* without
// losing any comments or newlines! This will require parsing them and then,
// if they are present, merging them into the final Spaceable.
skip_first!(
$opening_brace,
skip_second!(
$crate::parse::parser::sep_by0(
$delimiter,
$crate::parse::blankspace::space0_around($elem, $min_indent)
),
$closing_brace
)
)
};
}
#[macro_export]
macro_rules! and {
($p1:expr, $p2:expr) => {
@ -780,6 +789,70 @@ macro_rules! either {
};
}
/// Parse everything between two braces (e.g. parentheses), skipping both braces
/// and keeping only whatever was parsed in between them.
#[macro_export]
macro_rules! between {
($opening_brace:expr, $parser:expr, $closing_brace:expr) => {
skip_first!($opening_brace, skip_second!($parser, $closing_brace))
};
}
#[macro_export]
macro_rules! record_field {
($val_parser:expr, $min_indent:expr) => {
move |arena: &'a bumpalo::Bump,
state: $crate::parse::parser::State<'a>|
-> $crate::parse::parser::ParseResult<
'a,
$crate::parse::ast::AssignedField<'a, _>,
> {
use $crate::parse::ast::AssignedField::*;
use $crate::parse::blankspace::{space0, space0_before};
use $crate::parse::ident::lowercase_ident;
// You must have a field name, e.g. "email"
let (loc_label, state) = loc!(lowercase_ident()).parse(arena, state)?;
let (spaces, state) = space0($min_indent).parse(arena, state)?;
// Having a value is optional; both `{ email }` and `{ email: blah }` work.
// (This is true in both literals and types.)
let (opt_loc_val, state) = $crate::parse::parser::optional(skip_first!(
char(':'),
space0_before($val_parser, $min_indent)
))
.parse(arena, state)?;
let answer = match opt_loc_val {
Some(loc_val) => LabeledValue(loc_label, spaces, arena.alloc(loc_val)),
// If no value was provided, record it as a Var.
// Canonicalize will know what to do with a Var later.
None => {
if !spaces.is_empty() {
SpaceAfter(arena.alloc(LabelOnly(loc_label)), spaces)
} else {
LabelOnly(loc_label)
}
}
};
Ok((answer, state))
}
};
}
#[macro_export]
macro_rules! record {
($val_parser:expr, $min_indent:expr) => {
collection!(
char('{'),
loc!(record_field!($val_parser, $min_indent)),
char(','),
char('}'),
$min_indent
)
};
}
/// For some reason, some usages won't compile unless they use this instead of the macro version
#[inline(always)]
pub fn and<'a, P1, P2, A, B>(p1: P1, p2: P2) -> impl Parser<'a, (A, B)>

View file

@ -3,65 +3,40 @@ use bumpalo::Bump;
use parse::ast::AssignedField;
use parse::ast::Spaceable;
use parse::blankspace::{space0, space0_before};
use parse::collection::collection;
use parse::ident::lowercase_ident;
use parse::parser::{self, and, char, loc, optional, skip_first, Parser};
use parse::parser::{self, and, char, loc, optional, Parser};
use region::Located;
/// Parse a record - generally one of these two:
///
/// * Literal Value, e.g. { name: "foo", email: "blah@example.com" }
/// * Type Annotation, e.g. { name: String, email: String }
pub fn record<'a, P, S>(
val_parser: P,
min_indent: u16,
) -> impl Parser<'a, Vec<'a, Located<AssignedField<'a, S>>>>
where
P: Parser<'a, Located<S>>,
P: 'a,
S: Spaceable<'a>,
S: 'a,
{
collection(
char('{'),
loc(record_field(val_parser, min_indent)),
char(','),
char('}'),
min_indent,
)
}
// Parse a record - generally one of these two:
//
// * Literal Value, e.g. { name: "foo", email: "blah@example.com" }
// * Type Annotation, e.g. { name: String, email: String }
// pub fn record<'a, P, S>(
// val_parser: P,
// min_indent: u16,
// ) -> impl Parser<'a, Vec<'a, Located<AssignedField<'a, S>>>>
// where
// P: Parser<'a, Located<S>>,
// P: 'a,
// S: Spaceable<'a>,
// S: 'a,
// {
// collection!(
// char('{'),
// loc(record_field(val_parser, min_indent)),
// char(','),
// char('}'),
// min_indent
// )
// }
fn record_field<'a, P, S>(val_parser: P, min_indent: u16) -> impl Parser<'a, AssignedField<'a, S>>
where
P: Parser<'a, Located<S>>,
P: 'a,
S: Spaceable<'a>,
S: 'a,
{
use parse::ast::AssignedField::*;
parser::map_with_arena(
and(
// You must have a field name, e.g. "email"
loc!(lowercase_ident()),
and(
space0(min_indent),
// Having a value is optional; both `{ email }` and `{ email: blah }` work.
// (This is true in both literals and types.)
optional(skip_first(char(':'), space0_before(val_parser, min_indent))),
),
),
|arena: &'a Bump, (loc_label, (spaces, opt_loc_val))| match opt_loc_val {
Some(loc_val) => LabeledValue(loc_label, spaces, arena.alloc(loc_val)),
// If no value was provided, record it as a Var.
// Canonicalize will know what to do with a Var later.
None => {
if !spaces.is_empty() {
SpaceAfter(arena.alloc(LabelOnly(loc_label)), spaces)
} else {
LabelOnly(loc_label)
}
}
},
)
}
// fn record_field<'a, P, S>(val_parser: P, min_indent: u16) -> impl Parser<'a, AssignedField<'a, S>>
// where
// P: Parser<'a, Located<S>>,
// P: 'a,
// S: Spaceable<'a>,
// S: 'a,
// {
// use parse::ast::AssignedField::*;
// panic!("TODO");
// }

View file

@ -5,10 +5,8 @@ use collections::arena_join;
use parse::ast::{Attempting, TypeAnnotation};
use parse::blankspace::{space0_around, space1_before};
use parse::parser::{
between, char, optional, skip_first, string, unexpected, unexpected_eof, ParseResult, Parser,
State,
char, optional, string, unexpected, unexpected_eof, ParseResult, Parser, State,
};
use parse::record::record;
use region::Located;
pub fn located<'a>(min_indent: u16) -> impl Parser<'a, Located<TypeAnnotation<'a>>> {
@ -26,13 +24,13 @@ pub fn located<'a>(min_indent: u16) -> impl Parser<'a, Located<TypeAnnotation<'a
#[inline(always)]
fn loc_parenthetical_type<'a>(min_indent: u16) -> impl Parser<'a, Located<TypeAnnotation<'a>>> {
between(
between!(
char('('),
space0_around(
move |arena, state| located(min_indent).parse(arena, state),
min_indent,
),
char(')'),
char(')')
)
}
@ -42,14 +40,14 @@ fn record_type<'a>(min_indent: u16) -> impl Parser<'a, TypeAnnotation<'a>> {
map_with_arena!(
and!(
record(
record!(
move |arena, state| located(min_indent).parse(arena, state),
min_indent,
min_indent
),
optional(skip_first(
optional(skip_first!(
// This could be a record fragment, e.g. `{ name: String }...r`
string("..."),
move |arena, state| located(min_indent).parse(arena, state),
move |arena, state| located(min_indent).parse(arena, state)
))
),
|arena: &'a Bump, (rec, opt_bound_var)| match opt_bound_var {