///|
/// `TraitStatements = *(Trait [WS])`.
fn Parser::parse_traits(self : Parser) -> Array[@ast.Trait] raise ParseError {
  let traits : Array[@ast.Trait] = []
  while self.at_kind(At) {
    traits.push(self.parse_trait())
  }
  traits
}

///|
/// `Trait = "@" ShapeId [TraitBody]`; `TraitBody = "(" [WS] [TraitStructure / TraitNode] ")"`.
/// A body is a structure when it starts with a key followed by `:`.
fn Parser::parse_trait(self : Parser) -> @ast.Trait raise ParseError {
  let at = self.expect_kind(At, "`@`")
  guard self.adjacent() && self.at_kind(Identifier) else {
    self.fail(
      "SM-PARSE-001",
      "UNEXPECTED TOKEN",
      "I was expecting a trait name right after `@` but found \{self.found()}",
      self.here(),
      example="@required",
    )
  }
  let id = self.parse_shape_id()
  self.push_context("the trait `@\{id.value}`", at.span)
  let mut end = id.span.end()
  let body : @ast.TraitBody? = if self.at_kind(LParen) {
    ignore(self.advance())
    self.enter()
    let b : @ast.TraitBody = if self.at_kind(RParen) {
      Structure([][:])
    } else if (self.at_kind(Identifier) || self.at_kind(String)) &&
      self.peek1() is Some(n) &&
      n.kind == Colon {
      Structure(self.parse_object_kvps(RParen, "a trait body")[:])
    } else {
      Node(self.parse_node_value())
    }
    let close = self.expect_kind(RParen, "`)` to close the trait body")
    self.leave()
    end = close.span.end()
    Some(b)
  } else {
    None
  }
  self.pop_context()
  { id, body, span: at.span.start.to(end), }
}

///|
/// Whether a 2.0-only construct must be reported here: the file is known to
/// be 1.0 and no such construct has been reported yet. Marks it reported.
fn Parser::claim_v2_report(self : Parser) -> Bool {
  guard !self.version_unknown &&
    self.version != Some(V2) &&
    !self.version_reported else {
    return false
  }
  self.version_reported = true
  true
}

///|
/// Whether a 1.0-only construct must be reported here: the file says
/// `$version: "2"` and no version mismatch has been reported yet. Marks
/// it reported.
fn Parser::claim_v1_report(self : Parser) -> Bool {
  guard self.version is Some(V2) && !self.version_reported else { return false }
  self.version_reported = true
  true
}

///|
/// An error for 2.0-only syntax in a 1.0 file. The messages are the
/// reference's. Reports nothing when `version_unknown`: a broken `$`
/// control statement might have been `$version: "2"`, so gating on 1.0
/// would be reporting an error we can't actually back up. Reports once per
/// file: after the first, later 2.0-only constructs pass silently.
fn Parser::require_v2(
  self : Parser,
  feature : String,
  span : @span.Span,
) -> Unit raise ParseError {
  guard self.claim_v2_report() else { return }
  let message = match feature {
    "mixins" => "Mixins can only be used with Smithy version 2 or later"
    "elision" => "Members can only elide targets in IDL version 2 or later"
    "resource_binding" =>
      "Structures can only be bound to resources with Smithy version 2 or later"
    "default" =>
      "@default assignment is only supported in IDL version 2 or later"
    "enum" => "enum shapes cannot be used in Smithy version 1.0"
    _ =>
      "Inlined operation inputs and outputs can only be used with Smithy version 2 or later"
  }
  self.fail(
    "SM-PARSE-007",
    "NEEDS SMITHY IDL 2",
    message,
    span,
    notes=[
      "This file is Smithy IDL 1.0: " +
      (if self.version is None {
        "it has no `$version` statement, so I read it as version 1.0."
      } else {
        "its `$version` says so."
      }),
    ],
    hint="Add `$version: \"2\"` as the first line of the file.",
  )
}