///|
/// `; «...»`: a group sequence written without line or column sensitivity,
/// spliced into the enclosing sequence.
fn Parser::parse_splice(
  self : Parser,
  t : @lexer.Token,
  a : Advance,
  sg : GState,
) -> GroupsResult raise @err.ShrubberyError {
  let next_t = self.tok(a.rest)
  self.check_same_line(t, next_t, sg.count)
  let (group_commenting, b) = self.next_of_commenting(
    a.rest + 1,
    a.last_line,
    a.delta,
    a.raw.push(next_t),
    sg.count,
  )
  let inner = self.parse_groups(b.rest, {
    depth: sg.depth + 1,
    count: false,
    closer: Delim("\u{BB}", next_t),
    paren_immed: NotImmed,
    column: None,
    bar_column: None,
    check_column: false,
    bar_closes: false,
    bar_closes_line: None,
    block_mode: NoBlock,
    can_empty: false,
    comma_time: false,
    sequence_mode: AnyNumber,
    last_line: b.last_line,
    delta: b.delta,
    commenting: group_commenting,
    tail_commenting: None,
    raw: b.raw,
    variant: sg.variant,
  })
  if sg.paren_immed.is_immed() && inner.groups.length() != 1 {
    self.fail(t, MultiGroupSpliceNotAllowed)
  }
  let more = self.parse_groups(inner.rest, {
    ..sg,
    comma_time: sg.paren_immed.is_immed(),
    check_column: self.next_line(inner.rest, inner.end_line, sg.count),
    column: Some(sg.column.unwrap_or(t.column())),
    bar_column: None,
    last_line: inner.end_line,
    delta: inner.end_delta,
    commenting: None,
    tail_commenting: None,
    block_mode: next_group_block_mode(sg.block_mode),
    can_empty: false,
    raw: inner.tail_raw,
  })
  let groups = inner.groups
  for g in more.groups {
    groups.push(g)
  }
  { ..more, groups, }
}

///|
/// A group that begins with `|`: one alternative of a sequence.
fn Parser::parse_bar_group(
  self : Parser,
  t : @lexer.Token,
  i : Int,
  sg : GState,
  column : @column.Column,
) -> GroupsResult raise @err.ShrubberyError {
  let line = t.line()
  // A `|` that belongs to an enclosing construct ends this sequence rather
  // than starting an alternative here.
  if sg.bar_closes || line_eq_plus(line, sg.bar_closes_line, sg.delta) {
    return {
      groups: [],
      rest: i,
      end_line: sg.last_line,
      end_delta: sg.delta,
      end_token: None,
      tail_commenting: sg.tail_commenting,
      tail_raw: sg.raw,
    }
  }
  let in_block = sg.block_mode is Inside
  let after_colon = sg.block_mode.opened_by_colon() is Some(_)
  if !in_block && !after_colon {
    match sg.block_mode.opened_by_colon() {
      Some(colon) => self.fail(colon, UnnecessaryColonBeforeBar)
      None => self.fail(t, MisplacedBar)
    }
    // In recovery mode, treat it as an ordinary group so parsing continues.
    return self.parse_one_group(t, i, sg, column, (_, _) => ())
  }
  let same_line = !sg.count ||
    sg.last_line is None ||
    line_eq_plus(line, sg.last_line, 0)
  if sg.check_column {
    let ok = match sg.block_mode {
      Opened(_, parent) => self.col_eq(t, column, parent.half_next())
      _ =>
        same_line ||
        (match sg.column {
          Some(c) => self.col_eq(t, column, c)
          None => true
        })
    }
    if !ok {
      self.fail(t, WrongIndentation(missing_colon_hint=false))
    }
  }
  if !(sg.block_mode is Opened(_)) {
    // A `|` may not start left of the column its group started at, even when
    // it continues a line -- otherwise a later line could belong either to the
    // alternative or to whatever encloses it.
    match sg.column {
      Some(parent) =>
        if sg.count && self.col_lt(t, column, parent) {
          self.fail(t, AltBeforeGroupColumn)
        }
      None => ()
    }
  }
  let pre_raw = sg.raw
  let commenting = match sg.commenting {
    Some(c) => Some(c)
    None => sg.tail_commenting
  }
  let block = self.parse_block(
    depth=sg.depth,
    Some(t),
    i + 1,
    count=sg.count,
    line=Some(line),
    closer=Col(column.next()),
    bar_closes=!sg.count,
    bar_closes_line=if sg.count { Some(line - sg.delta) } else { None },
    delta=sg.delta,
    raw=if commenting is Some(_) { RNil } else { pre_raw },
    variant=sg.variant,
  )
  let rest = self.parse_groups(block.rest, {
    ..sg,
    column: if same_line {
      sg.column
    } else {
      Some(column)
    },
    bar_column: None,
    check_column: self.next_line(block.rest, block.end_line, sg.count),
    last_line: block.end_line,
    delta: block.end_delta,
    comma_time: sg.paren_immed.is_immed(),
    commenting: None,
    tail_commenting: block.tail_commenting,
    raw: match commenting {
      Some(_) =>
        pre_raw.push_text(group_source(block.items)).then(block.tail_raw)
      None => block.tail_raw
    },
  })
  match commenting {
    Some(_) => rest
    None => {
      // `parse_block` answers with the block as a single item; it becomes the
      // alternative, and `tag_as_block` will collect the run of them.
      let bar_block = if block.items.length() > 0 {
        Some(block.items[0])
      } else {
        None
      }
      let groups = [
        (
          {
            items: block.items,
            bar: bar_block,
            span: span_over(block.items, t.span),
            meta: @raw.Meta::new(),
          } : PendingGroup),
      ]
      for g in rest.groups {
        groups.push(g)
      }
      { ..rest, groups, }
    }
  }
}

///|
/// An ordinary group, then the rest of the sequence.
fn Parser::parse_one_group(
  self : Parser,
  t : @lexer.Token,
  i : Int,
  sg : GState,
  column : @column.Column,
  check_column : (@lexer.Token, @column.Column) -> Unit raise @err.ShrubberyError,
) -> GroupsResult raise @err.ShrubberyError {
  check_column(t, column)
  let use_column = sg.column.unwrap_or(column)
  let line = t.line()
  let pre_raw = sg.raw
  let commenting = match sg.commenting {
    Some(c) => Some(c)
    None => sg.tail_commenting
  }
  if sg.sequence_mode is NoMore && commenting is None {
    self.fail(t, SecondGroupAfterAt)
  }
  let g = self.parse_group(i, {
    depth: sg.depth,
    count: sg.count,
    line: Some(line),
    column: Some(use_column),
    bar_column: sg.bar_column,
    operator_column: None,
    bar_closes: sg.bar_closes,
    bar_closes_line: sg.bar_closes_line,
    block_mode: sg.block_mode,
    can_empty: sg.can_empty,
    delta: sg.delta,
    raw: RNil,
    at_mode: None,
    variant: sg.variant,
  })
  // A group that consumed nothing must not be followed by "the rest of the
  // sequence": `parse_groups` would dispatch on the same token and arrive back
  // here, for ever. It cannot happen while the first mistake is raised, because
  // the mistake comes first -- but recovery mode raises nothing, so a token no
  // arm consumes spins until the stack is gone. The way to get one is an `@`
  // body whose closer failed to lex, which strands the body's content at group
  // level where `AtContent` ends a group without consuming it.
  //
  // Handing the token back is the conservative answer: whatever opened this
  // sequence sees it next and reports it, or the sequence ends.
  if g.rest == i {
    return {
      groups: [],
      rest: i,
      end_line: g.end_line,
      end_delta: g.end_delta,
      end_token: None,
      tail_commenting: g.tail_commenting,
      tail_raw: pre_raw.then(g.tail_raw),
    }
  }
  let rest = self.parse_groups(g.rest, {
    ..sg,
    column: Some(use_column),
    check_column: self.next_line(g.rest, g.end_line, sg.count),
    last_line: g.end_line,
    comma_time: sg.paren_immed.is_immed(),
    bar_closes_line: next_block_close_line(
      sg.bar_closes_line,
      Some(line),
      g.end_line,
    ),
    bar_column: None,
    delta: g.end_delta,
    commenting: None,
    tail_commenting: g.tail_commenting,
    raw: match commenting {
      Some(_) => pre_raw.push_text(group_source(g.items)).then(g.tail_raw)
      None => g.tail_raw
    },
    sequence_mode: if commenting is None && sg.sequence_mode is OneOnly {
      NoMore
    } else {
      sg.sequence_mode
    },
  })
  match commenting {
    Some(_) => rest
    None => {
      // Whatever whitespace and comments led up to this group belong to the
      // GROUP, not to its first term: the reference attaches a prefix to the
      // enclosing group whenever the term it would otherwise land on is first.
      let meta = @raw.Meta::new()
      attach_meta_prefix(meta, pre_raw)
      let groups = [
        (
          { items: g.items, bar: None, span: span_over(g.items, t.span), meta, } :
          PendingGroup),
      ]
      for x in rest.groups {
        groups.push(x)
      }
      { ..rest, groups, }
    }
  }
}

///|
/// The source text of a group's items, for rendering a commented-out group
/// back into the raw text around it.
fn group_source(items : Array[@ast.Node]) -> @raw.Raw {
  let buf = StringBuilder()
  for n in items {
    n.write_source(buf)
  }
  Str(buf.to_string())
}

///|
/// A block that ends on the line a `|` was waiting for moves that expectation
/// to the block's own last line.
fn next_block_close_line(bcl : Int?, pre_line : Int?, post_line : Int?) -> Int? {
  match (bcl, pre_line) {
    (Some(a), Some(b)) => if a == b { post_line } else { bcl }
    _ => bcl
  }
}