///|
/// The first lexical error the compiler's lexer would have reached before
/// the parser stopped: a scanner error diagnostic that starts before the
/// farthest token loaded.
fn first_lexical_error(
  stream : @scanner.TokenStream,
  farthest : Int32,
) -> @diagnostic.Diagnostic? {
  for d in stream.diagnostics {
    if d.code == "GL-SCAN-001" && d.span.start < farthest {
      return Some(d)
    }
  }
  None
}

///|
/// Run a snippet parse and keep its error as a value, so a lexical error
/// the lexer reached can be reported first.
fn[A] attempt_parse(f : () -> A? raise ParseError) -> Result[A?, ParseError] {
  try f() catch {
    e => Err(e)
  } noraise {
    v => Ok(v)
  }
}

///|
/// The compiler's `ensure_no_errors_or_remaining_input` for a snippet: a
/// lexical error first, then a missing result (`missing`), then leftover
/// tokens.
fn[A] Parser::finish_snippet(
  self : Parser,
  stream : @scanner.TokenStream,
  result : Result[A?, ParseError],
  missing : ParseErrorType,
) -> A raise ParseError {
  if first_lexical_error(stream, self.farthest) is Some(d) {
    parse_error(LexError(d), d.span)
  }
  let parsed = match result {
    Ok(v) => v
    Err(e) => raise e
  }
  guard parsed is Some(value) else { parse_error(missing, sp(0, 0)) }
  match self.next_token() {
    Some(t) =>
      parse_error(
        UnexpectedToken(
          token=t.kind,
          expected=["An import, const, type, or function."][:],
          hint=None,
        ),
        t.span,
      )
    None => value
  }
}

///|
/// Parse `text` as one type annotation (without `:`), for tests and tools.
///
/// ```mbt check
/// test {
///   let t = @parser.parse_type_text("List(a)")
///   debug_inspect(t.location(), content="{ start: 0, end: 7 }")
/// }
/// ```
pub fn parse_type_text(text : String) -> @ast.TypeAst raise ParseError {
  let stream = @scanner.tokenize_text(text)
  let p = Parser::new(stream, @dialect.Dialect::gleam_1_19())
  let result = attempt_parse(() => p.parse_type())
  p.finish_snippet(stream, result, ExpectedType)
}

///|
/// Parse `text` as one pattern.
pub fn parse_pattern_text(text : String) -> @ast.Pattern raise ParseError {
  let stream = @scanner.tokenize_text(text)
  let p = Parser::new(stream, @dialect.Dialect::gleam_1_19())
  let result = attempt_parse(() => p.parse_pattern())
  p.finish_snippet(stream, result, ExpectedPattern)
}

///|
/// Parse `text` as one constant value (the compiler's `parse_const_value`
/// test interface; a missing value is `ExpectedExpr` at `0..0`).
pub fn parse_constant_text(text : String) -> @ast.Constant raise ParseError {
  let stream = @scanner.tokenize_text(text)
  let p = Parser::new(stream, @dialect.Dialect::gleam_1_19())
  let result = attempt_parse(() => p.parse_const_value())
  p.finish_snippet(stream, result, ExpectedExpr)
}

///|
/// Parse `text` as one expression (a pipeline or an operator chain counts
/// as one); a missing expression is `ExpectedExpr` at `0..0`.
///
/// ```mbt check
/// test {
///   let e = @parser.parse_expression_text("1 + 2")
///   debug_inspect(e.location(), content="{ start: 0, end: 5 }")
/// }
/// ```
pub fn parse_expression_text(text : String) -> @ast.Expr raise ParseError {
  let stream = @scanner.tokenize_text(text)
  let p = Parser::new(stream, @dialect.Dialect::gleam_1_19())
  let result = attempt_parse(() => p.parse_expression())
  p.finish_snippet(stream, result, ExpectedExpr)
}

///|
/// Parse `text` as a statement sequence (a function body without braces),
/// the compiler's `parse_statement_sequence` test interface.
pub fn parse_statements_text(
  text : String,
) -> ArrayView[@ast.Statement] raise ParseError {
  let stream = @scanner.tokenize_text(text)
  let p = Parser::new(stream, @dialect.Dialect::gleam_1_19())
  let result = attempt_parse(() => p.parse_statement_sequence().map(r => r.0[:]))
  p.finish_snippet(stream, result, ExpectedExpr)
}