///|
/// An `@` at the head of a term.
///
/// The `@` itself contributes nothing to the tree — it becomes the shape of
/// the call around what follows — so this arm's job is to decide what KIND of
/// form is starting and hand `parse_group` the right mode for it.
fn Parser::parse_at(
  self : Parser,
  t : @lexer.Token,
  i : Int,
  s : PState,
) -> GroupResult raise @err.ShrubberyError {
  self.check_block_mode(t, s)
  match s.at_mode {
    Some(am) if am.stop_at_at =>
      return {
        items: [],
        rest: i,
        end_line: s.line,
        end_delta: s.delta,
        tail_commenting: None,
        tail_raw: s.raw,
      }
    _ => ()
  }
  if self.at_end(i + 1) {
    self.fail(t, MissingTermAfterAt)
    return {
      items: [],
      rest: i + 1,
      end_line: s.line,
      end_delta: s.delta,
      tail_commenting: None,
      tail_raw: s.raw.push(t),
    }
  }
  let next_t = self.tok(i + 1)
  self.check_same_line(t, next_t, s.count)
  let stop_at_next_at = match s.at_mode {
    Some(am) => am.stop_at_next_at
    None => false
  }
  let raw_with_at = s.raw.push(t)
  match next_t.kind {
    Opener => {
      // `@(«...»)` splices its command and takes no arguments. The `(` is
      // dropped here and the `»` handler consumes the matching `)`.
      if next_t.text == "(" &&
        !self.at_end(i + 2) &&
        self.tok(i + 2).kind is Opener &&
        self.tok(i + 2).text == "\u{AB}" {
        return self.parse_group(i + 2, {
          ..s,
          raw: raw_with_at.push(next_t),
          at_mode: Some(
            AtMode::new(initial=true, splice=true, stop_at_at=stop_at_next_at),
          ),
        })
      }
      self.parse_group(i + 1, {
        ..s,
        raw: raw_with_at,
        at_mode: Some(AtMode::new(initial=true, stop_at_at=stop_at_next_at)),
      })
    }
    Identifier => {
      // `@a.b.c`: identifier and operator alternating, with no space between,
      // is one command. The pairs are collected here so that the call is built
      // around the whole chain rather than around its first name.
      let rev_prefix : Array[@ast.Node] = []
      let mut j = i + 1
      let mut raw = raw_with_at
      for ;; {
        if j + 2 < self.toks.length() &&
          self.tok(j + 1).kind is Operator &&
          is_identifier_like(self.tok(j + 2)) {
          let id = leaf_of(self.tok(j))
          id.meta.prefix = raw.to_raw()
          let op = leaf_of(self.tok(j + 1))
          // Source order: the chain reads `a . b . c`.
          rev_prefix.push(id)
          rev_prefix.push(op)
          j = j + 2
          raw = RNil
          continue
        }
        break
      }
      self.parse_group(j, {
        ..s,
        raw,
        at_mode: Some(
          AtMode::new(initial=true, rev_prefix~, stop_at_at=stop_at_next_at),
        ),
      })
    }
    Literal(_) | Operator | Keyword | SExp(_) =>
      self.parse_group(i + 1, {
        ..s,
        raw: raw_with_at,
        at_mode: Some(AtMode::new(initial=true, stop_at_at=stop_at_next_at)),
      })
    AtOpener =>
      self.keep(
        i,
        s,
        at_mode=Some(AtMode::new(stop_at_at=stop_at_next_at)),
        suffix=false,
      )
    Whitespace => {
      self.fail(next_t, WhitespaceAfterAt)
      {
        items: [],
        rest: i + 1,
        end_line: s.line,
        end_delta: s.delta,
        tail_commenting: None,
        tail_raw: raw_with_at,
      }
    }
    _ => {
      // A token the LEXER already rejected reports its own error rather than a
      // complaint about the `@`. The reference reads the term after an `@`
      // through the lexer, so `@` immediately before a `}` is a read error at
      // the `}`; only a token that lexed cleanly and is merely in the wrong
      // place is "invalid after `@`".
      match next_t.kind {
        Fail(kind) => self.fail(next_t, kind)
        _ => self.fail(next_t, InvalidAfterAt)
      }
      {
        items: [],
        rest: i + 1,
        end_line: s.line,
        end_delta: s.delta,
        tail_commenting: None,
        tail_raw: raw_with_at,
      }
    }
  }
}

///|
fn is_identifier_like(t : @lexer.Token) -> Bool {
  match t.kind {
    Identifier | Keyword => true
    _ => false
  }
}