///|
/// Parse ONE group: a run of terms, optionally ending in a block and then
/// alternatives.
///
/// Answers with the group's items rather than a node, because a block and its
/// alternatives are appended into the group by `parse_block` and the caller is
/// what knows where the group starts.
fn Parser::parse_group(
self : Parser,
start : Int,
s : PState,
) -> GroupResult raise @err.ShrubberyError {
let done = (i : Int, s : PState) => {
(
{
items: [],
rest: i,
end_line: s.line,
end_delta: s.delta,
tail_commenting: None,
tail_raw: s.raw,
} : GroupResult)
}
let mut i = start
let mut s = s
for ;; {
if self.at_end(i) {
return done(i, s)
}
let t = self.tok(i)
let line = t.line()
if s.count && line_gt(line, s.line) {
// A new line. Either it continues this group -- an operator, or a `|`
// that opens alternatives -- or the group is over.
match t.kind {
Whitespace | Comment => {
s = { ..s, line: Some(line), delta: 0, raw: s.raw.push(t), }
i = i + 1
continue
}
Closer | AtContent | AtCloser => return done(i, s)
_ => ()
}
let (group_commenting, use_t, use_i, ll, d, r) = self.own_line_group_comment(
i,
s.line,
s.delta,
s.raw,
s.count,
)
let column = use_t.column()
let more_indented = match s.column {
Some(c) => self.col_gt(use_t, column, c)
None => false
}
if !more_indented {
// Leaving the group comments in place is deliberate: not consuming
// inspected tokens would normally be worrying, but they are parsed at
// most once more.
return done(i, s)
}
if use_t.kind is BarOperator {
let want = s.bar_column.unwrap_or(s.column.unwrap_or(@column.zero))
if !self.col_eq(use_t, column, want.half_next()) {
self.fail(use_t, WrongIndentation(missing_colon_hint=false))
}
// Straight to `parse_block`, not through `parse_alts_block`: on this
// path the `|` opens alternatives for a group that has already been
// continued onto later lines, so neither the operator-column check nor
// the "cannot start before the group's column" check applies -- the
// column was just checked against the group's own, above.
return self.parse_block(
depth=s.depth,
None,
use_i,
count=s.count,
block_mode=Some(Inside),
line=Some(use_t.line()),
closer=if s.count { Col(column) } else { Any },
bar_closes=false,
bar_closes_line=None,
drop_empty=true,
delta=d,
raw=r,
group_commenting~,
variant=s.variant,
)
}
let continues_operator = use_t.kind is Operator &&
(match s.operator_column {
None => true
Some(oc) => self.col_eq(use_t, column, oc)
}) &&
(s.variant.indented_operator_continue)(use_t.text)
if continues_operator {
match group_commenting {
Some(c) => self.fail(c, MisplacedGroupComment)
None => ()
}
s = { ..s, line: ll, delta: d, raw: r, }
return self.keep(use_i, s, operator_column=Some(column))
}
if use_t.kind is Opener && use_t.text == "\u{AB}" {
match group_commenting {
Some(c) => self.fail(c, MisplacedGroupComment)
None => ()
}
// Fall through so that the `«` is reported where it stands.
s = { ..s, line: Some(line), }
continue
}
self.fail(use_t, WrongIndentation(missing_colon_hint=true))
return done(i, s)
}
// Same line.
match t.kind {
Closer | CommaOperator | SemicolonOperator | AtContent | AtCloser =>
return done(i, s)
Identifier | Keyword | Literal(_) | Operator | SExp(_) =>
return self.keep(i, s)
BlockOperator => {
self.check_block_mode(t, s)
let parent_column = s.bar_column.unwrap_or(
s.column.unwrap_or(@column.zero),
)
return self.parse_block(
depth=s.depth,
Some(t),
i + 1,
count=s.count,
line=Some(line),
closer=if s.count { Col(parent_column.half_next()) } else { Any },
delta=s.delta,
raw=s.raw,
bar_closes=s.bar_closes && s.bar_closes_line is None,
bar_closes_line=s.bar_closes_line,
can_empty=s.can_empty,
could_empty_if_start=true,
parent_column=Some(parent_column),
variant=s.variant,
)
}
BarOperator =>
return self.parse_alts_block(
t,
i,
s,
delta=s.delta,
raw=s.raw,
done_at=i,
)
Opener => return self.parse_opener(t, i, s)
Whitespace | Comment | ContinueOperator => {
let a = self.next_of(i, s.line, s.delta, s.raw, s.count)
s = { ..s, line: a.last_line, delta: a.delta, raw: a.raw, }
i = a.rest
continue
}
GroupComment => {
// Misplaced, unless it is commenting out a `|` on the same line.
let a = self.next_of(i + 1, Some(line), s.delta, s.raw.push(t), s.count)
if !self.at_end(a.rest) &&
self.tok(a.rest).kind is BarOperator &&
!self.next_line(a.rest, a.last_line, s.count) {
return self.parse_alts_block(
self.tok(a.rest),
a.rest,
s,
delta=a.delta,
raw=a.raw,
done_at=i,
group_commenting=Some(t),
)
}
self.fail(t, MisplacedGroupComment)
i = i + 1
continue
}
At => return self.parse_at(t, i, s)
AtComment => {
self.fail(t, AtCommentOutsideBody)
return done(i, s)
}
AtOpener => return self.keep(i, s)
Fail(kind) => {
self.fail(t, kind)
i = i + 1
continue
}
_ => return done(i, s)
}
}
}
///|
/// Nothing may join a group after a `»` has closed its block.
fn Parser::check_block_mode(
self : Parser,
t : @lexer.Token,
s : PState,
) -> Unit raise @err.ShrubberyError {
if s.block_mode is End {
self.fail(t, NoTermsAfterGuillemet)
}
}
///|
/// Take one token as a term and carry on with the rest of the group.
fn Parser::keep(
self : Parser,
i : Int,
s : PState,
operator_column? : @column.Column? = None,
at_mode? : AtMode? = None,
suffix? : Bool = true,
) -> GroupResult raise @err.ShrubberyError {
let t = self.tok(i)
self.check_block_mode(t, s)
// A `#{...}` escape can span lines, so where the token ENDS is decided by
// where the next one starts rather than by the token's own line.
let post_line = if i + 1 < self.toks.length() {
self.tok(i + 1).line()
} else {
t.line()
}
// The default PRESERVES the state's operator column rather than clearing it:
// a group continued by an operator on one line must be continued at the same
// column on every later line, and forgetting it here would accept
// `1 / + 2 / ⟨further right⟩ + 3` -- which the reference rejects.
let operator_column = match operator_column {
Some(_) => operator_column
None => s.operator_column
}
let (suffix_raw, suffix_i, suffix_line, suffix_delta) = if suffix {
self.suffix_comments(i + 1, Some(post_line), s.delta)
} else {
(RNil, i + 1, Some(post_line), s.delta)
}
let am = match at_mode {
Some(_) => at_mode
None => s.at_mode
}
let cont = self.continue_at(
am,
false,
suffix_i,
suffix_line,
suffix_delta,
s.count,
s.variant,
s.depth,
)
let rest = self.parse_group(cont.rest, {
..s,
line: cont.line,
delta: cont.delta,
raw: RNil,
block_mode: NoBlock,
can_empty: false,
operator_column,
at_mode: cont.at_mode,
})
let elem = leaf_of(t)
attach_prefix(elem, s.raw)
elem.meta.suffix = elem.meta.suffix.combine(suffix_raw.to_raw())
let items = [elem]
for x in rest.items {
items.push(x)
}
{ ..rest, items: (cont.adjust)(items), }
}
///|
/// A `|` that opens or continues a sequence of alternatives.
/// `done_at` is where the CALLER was looking, which is not always where the
/// bar is: a `#//` before a `|` is inspected from one token earlier, and if
/// this decides the `|` belongs to an enclosing construct, the `#//` has to be
/// left in the stream for that construct to find. Returning the bar's position
/// instead silently swallows it.
fn Parser::parse_alts_block(
self : Parser,
t : @lexer.Token,
i : Int,
s : PState,
delta~ : Int,
raw~ : RawList,
done_at~ : Int,
group_commenting? : @lexer.Token? = None,
) -> GroupResult raise @err.ShrubberyError {
let line = t.line()
if s.bar_closes || line_eq_plus(line, s.bar_closes_line, s.delta) {
return {
items: [],
rest: done_at,
end_line: s.line,
end_delta: s.delta,
tail_commenting: None,
tail_raw: s.raw,
}
}
// No block-mode check here: a `|` is allowed after a `:`.
match s.operator_column {
Some(oc) =>
if !self.col_eq(t, t.column(), oc.half_next()) {
self.fail(t, WrongIndentation(missing_colon_hint=false))
}
None => ()
}
match s.column {
Some(c) =>
if s.count && self.col_lt(t, t.column(), c) {
self.fail(t, AltBeforeGroupColumn)
}
None => ()
}
// The `|` is NOT consumed here. `parse_block` starts its group sequence AT
// the bar, and the bar branch of `parse_groups` is what takes it -- which is
// how the sequence comes to have the bar's own column, and how a second `|`
// at that column is recognised as a sibling rather than as a mistake.
self.parse_block(
depth=s.depth,
None,
i,
count=s.count,
block_mode=Some(Inside),
line=Some(line),
closer=if s.count { Col(t.column()) } else { Any },
bar_closes=false,
bar_closes_line=None,
drop_empty=true,
delta~,
raw~,
group_commenting~,
variant=s.variant,
)
}