voltaic-parser 0.2.0

Hand-written recursive-descent parser for the Voltaic language.
Documentation
import crate::ParseError
import crate::expr::ABOVE_AND_AND
import crate::expr::parse_expr
import crate::item::local_test
import crate::item::starts_a_declaration
import crate::item::walk_item_for_tails
import crate::operand::assign_tail
import crate::operand::opens_an_over
import crate::operand::over_update
import crate::operand::try_await
import crate::parser::Parser
import crate::pattern::pattern_alt
import crate::signature::TypeStop
import crate::signature::capture_type
import crate::token::reserved_word
import crate::token::scan_run
import voltaic_ast::Block
import voltaic_ast::CaseArm
import voltaic_ast::Condition
import voltaic_ast::ConditionOperand
import voltaic_ast::Expr
import voltaic_ast::Program
import voltaic_ast::Qualifier
import voltaic_ast::Stmt
import voltaic_diag::Code
import voltaic_lexer::Keyword
import voltaic_lexer::Span
import voltaic_lexer::TokenKind

names_the_retired_until : &Parser -> bool
names_the_retired_until p = case &(.peek p).kind of
  TokenKind::Ident text | text == "until" -> true
  _ -> false

export(crate) arm_body : &mut Parser -> Result<Expr, ParseError>
arm_body p =
  start = (.peek_span p).0
  value = (parse_expr p 0)?
  assign_tail p value start

export(crate) bind_block : &mut Parser -> usize -> Result<Expr, ParseError>
bind_block p start =
  names_a_method = .is_some_and (\t -> t.kind == TokenKind::Dot) (.peek_at 1 p)
  if !names_a_method
    return Err $ reserved_word p
  .advance p
  .advance p
  method = (.ident "expected a method name after `do .`" p)?
  body = (.statement_block "expected an indented `do` body" p)?
  Ok (Expr::BindBlock { method, body, span = (start, (.prev_span p).1) })

export(crate) block_expr : &mut Parser -> Option<String> -> usize -> Result<Expr, ParseError>
block_expr p label start =
  body = (.statement_block "expected an indented block" p)?
  Ok (Expr::Block { label, body, span = (start, (.prev_span p).1) })

export(crate) case_arm : &mut Parser -> Result<CaseArm, ParseError>
case_arm p =
  start = (.peek_span p).0
  pattern = (pattern_alt p)?
  ty = if .check (&TokenKind::Colon) p
    .advance p
    Some (capture_type p "expected a type after `:` in a `case` pattern" TypeStop::Greedy)?
  else
    None
  arm_guard = if .check (&TokenKind::Or) p
    .advance p
    Some (condition p)?
  else
    None
  (.expect (&TokenKind::Arrow) "expected `->` after a `case` pattern" p)?
  body = (arm_body p)?
  (.newline_with_trailing "expected a line break after a `case` arm" p)?
  Ok (CaseArm { pattern, ty, r#guard = arm_guard, body, span = (start, (.prev_span p).1) })

export(crate) case_of : &mut Parser -> Result<Expr, ParseError>
case_of p =
  start = (.peek_span p).0
  (.expect (&TokenKind::Kw Keyword::Case) "expected `case`" p)?
  scrutinee = (parse_expr p 0)?
  (.expect (&TokenKind::Kw Keyword::Of) "expected `of` after the scrutinee" p)?
  (.expect (&TokenKind::Newline) "expected a line break after `of`" p)?
  (.expect_with Code::EmptyBody (&TokenKind::Indent) "expected indented `case` arms" p)?
  mut arms = Vec::new ()
  loop
    .skip_separators p
    if .check (&TokenKind::Dedent) p || .at_eof p
      break
    .push (case_arm p)? arms
  (.expect (&TokenKind::Dedent) "expected the `case` arms to end" p)?
  if .is_empty arms
    return Err (ParseError::r#with Code::EmptyBody "a `case` must have at least one arm" (start, (.prev_span p).1))
  Ok (Expr::Case { scrutinee = Box::new scrutinee, arms, span = (start, (.prev_span p).1) })

export(crate) check_callback_tails : &Program -> Vec<ParseError>
check_callback_tails program =
  mut found = Vec::new ()
  for item in &program.items
    walk_item_for_tails item (&mut found)
  found

export(crate) condition : &mut Parser -> Result<Condition, ParseError>
condition p =
  if !holds_a_pattern_test p false (&TokenKind::Kw Keyword::Is) && holds_a_pattern_test p false (&TokenKind::Eq)
    return Err (ParseError::r#with Code::ExpectedToken (.to_string "a pattern test is written `scrutinee is pattern` — plan 60 moved it off `=`, which now means definition and nothing else") (.peek_span p))
  ladder_condition p

export(crate) ladder_condition : &mut Parser -> Result<Condition, ParseError>
ladder_condition p =
  if !holds_a_pattern_test p false (&TokenKind::Kw Keyword::Is)
    return Ok (Condition::plain (parse_expr p 0)?)
  mut operands = vec! \-> (condition_operand p)?
  while .check (&TokenKind::AndAnd) p
    .advance p
    .push (condition_operand p)? operands
  if .check (&TokenKind::OrOr) p
    return Err (ParseError::r#with Code::ExpectedToken "a condition holding a pattern test joins with `&&` only: Rust's let-chains do not admit `||`. Split the condition, or use a `case`" (.peek_span p))
  Ok $ Condition { operands }

export(crate) condition_operand : &mut Parser -> Result<ConditionOperand, ParseError>
condition_operand p =
  if !holds_a_pattern_test p true (&TokenKind::Kw Keyword::Is)
    return Ok (ConditionOperand::Expr (parse_expr p ABOVE_AND_AND)?)
  source = (parse_expr p ABOVE_AND_AND)?
  (.expect (&TokenKind::Kw Keyword::Is) "expected `is` before the pattern" p)?
  Ok (ConditionOperand::Pattern { pattern = (pattern_alt p)?, source })

export(crate) control_flow : &mut Parser -> Result<Option<Expr>, ParseError>
control_flow p =
  start = (.peek_span p).0
  label = case &(.peek p).kind of
    TokenKind::Label text ->
      owned = .clone text
      .advance p
      Some owned
    _ -> None
  if names_the_retired_until p
    return Err (ParseError::with_help Code::RetiredSpelling (.to_string "`until` is no longer a loop keyword") "write `while !(…)` with the condition negated, or a `loop` with a `break`. Row 99 wave 2 deleted it: `until` lowered to exactly that and could not take a pattern test, which is the one thing `while` can do and it could not" (.peek_span p))
  if .check (&TokenKind::Kw Keyword::While) p
    .advance p
    cond = (condition p)?
    body = (.statement_block "expected an indented loop body" p)?
    return Ok (Some (Expr::While { condition = cond, body, label, span = (start, (.prev_span p).1) }))
  if .check (&TokenKind::Kw Keyword::For) p
    .advance p
    binder = (.ident "expected a name after `for`" p)?
    (.expect (&TokenKind::Kw Keyword::In) "expected `in` after the binder — doc 30 §3.2 writes a `for` with Rust's own word" p)?
    source = Box::new (parse_expr p 0)?
    body = (.statement_block "expected an indented loop body" p)?
    return Ok (Some (Expr::For { binder, source, body, label, span = (start, (.prev_span p).1) }))
  if .check (&TokenKind::Kw Keyword::Loop) p
    .advance p
    body = (.statement_block "expected an indented loop body" p)?
    return Ok (Some (Expr::Loop { body, label, span = (start, (.prev_span p).1) }))
  if .is_some label && .check (&TokenKind::Newline) p
    return .map Some (block_expr p label start)
  if label is Some named
    message = format! \-> "expected `while`, `until`, `for`, `loop` or an indented block after the label `'#{named}`"
    return Err (ParseError::r#with Code::ExpectedToken message (.peek_span p))
  if .check (&TokenKind::Kw Keyword::Break) p
    .advance p
    break_label = case &(.peek p).kind of
      TokenKind::Label text ->
        owned = .clone text
        .advance p
        Some owned
      _ -> None
    value = if .at_statement_end p
      None
    else
      Some $ Box::new (parse_expr p 0)?
    return Ok (Some (Expr::Break { label = break_label, value, span = (start, (.prev_span p).1) }))
  if .check (&TokenKind::Kw Keyword::Continue) p
    .advance p
    continue_label = case &(.peek p).kind of
      TokenKind::Label text ->
        owned = .clone text
        .advance p
        Some owned
      _ -> None
    return Ok (Some (Expr::Continue { label = continue_label, span = (start, (.prev_span p).1) }))
  if .check (&TokenKind::Kw Keyword::If) p
    return .map Some (.if_statement p)
  if .check (&TokenKind::Kw Keyword::Guard) p
    .advance p
    source = Box::new (parse_expr p 0)?
    (.expect (&TokenKind::Kw Keyword::Is) "expected `is` before a `guard` pattern" p)?
    pattern = (pattern_alt p)?
    (.expect (&TokenKind::Kw Keyword::Else) "expected `else` — a `guard` without one is a binding, and let-else requires it" p)?
    fallback = (.statement_block "expected an indented `else` body after `guard`" p)?
    return Ok (Some (Expr::Guard { pattern, source, r#otherwise = fallback, span = (start, (.prev_span p).1) }))
  Ok None

export(crate) holds_a_pattern_test : &Parser -> bool -> &TokenKind -> bool
holds_a_pattern_test p one_operand marker =
  mut depth : i32 = 0
  mut i = 0
  while .peek_at i p is Some tok
    case &tok.kind of
      kind | depth == 0 && kind == marker ->
        return true
      TokenKind::LParen or TokenKind::LBracket or TokenKind::LBrace -> depth += 1
      TokenKind::RParen or TokenKind::RBracket or TokenKind::RBrace ->
        depth -= 1
        if depth < 0
          return false
      TokenKind::AndAnd or TokenKind::OrOr | depth == 0 && one_operand ->
        return false
      TokenKind::Kw (Keyword::Else) | depth == 0 ->
        return false
      TokenKind::Arrow | depth == 0 ->
        return false
      TokenKind::Newline or TokenKind::Indent or TokenKind::Dedent or TokenKind::Eof ->
        return false
      _ -> ()
    i += 1
  false

export(crate) modified_block : &mut Parser -> Keyword -> usize -> Result<Expr, ParseError>
modified_block p kw start =
  .advance p
  word = .text kw
  body = (.statement_block (&format! \-> "expected an indented `#{word}` body") p)?
  span = (start, (.prev_span p).1)
  case kw of
    Keyword::Async -> Ok (Expr::AsyncBlock { body, span })
    Keyword::Unsafe -> Ok (Expr::UnsafeBlock { body, span })
    Keyword::Try -> Ok (Expr::TryBlock { body, span })
    _ ->
      unreachable! \-> "only `try`, `async` and `unsafe` modify a block"

export(crate) qualifiers : &mut Parser -> Result<Vec<Qualifier>, ParseError>
qualifiers p =
  mut found = Vec::new ()
  loop
    named = case (.peek p).kind of
      TokenKind::Ident _ -> true
      _ -> false
    is_draw = named && .is_some_and (\t -> t.kind == TokenKind::Kw Keyword::In) (.peek_at 1 p)
    if is_draw
      name = (.ident "expected a name before `in`" p)?
      .advance p
      source = (parse_expr p 0)?
      .push (Qualifier::Draw { name, source }) found
    else
      .push (Qualifier::Guard (parse_expr p 0)?) found
    .skip_layout p
    if !.check (&TokenKind::Comma) p
      break
    .advance p
    .skip_layout p
  Ok found

export(crate) reaches_a_draw_arrow : &Parser -> bool
reaches_a_draw_arrow p =
  mut depth : i32 = 0
  mut i = 0
  while .peek_at i p is Some tok
    case &tok.kind of
      TokenKind::DrawArrow | depth == 0 ->
        return true
      TokenKind::LParen or TokenKind::LBracket or TokenKind::LBrace -> depth += 1
      TokenKind::RParen or TokenKind::RBracket or TokenKind::RBrace ->
        depth -= 1
        if depth < 0
          return false
      TokenKind::Eq | depth == 0 ->
        return false
      TokenKind::Newline or TokenKind::Indent or TokenKind::Dedent or TokenKind::Eof ->
        return false
      _ -> ()
    i += 1
  false

export(crate) reaches_a_fat_arrow : &Parser -> Option<Span>
reaches_a_fat_arrow p =
  mut depth : i32 = 0
  mut i = 0
  while .peek_at i p is Some tok
    case &tok.kind of
      TokenKind::FatArrow | depth == 0 ->
        return Some tok.span
      TokenKind::LParen or TokenKind::LBracket or TokenKind::LBrace -> depth += 1
      TokenKind::RParen or TokenKind::RBracket or TokenKind::RBrace ->
        depth -= 1
        if depth < 0
          return None
      TokenKind::Eq | depth == 0 ->
        return None
      TokenKind::Newline or TokenKind::Indent or TokenKind::Dedent or TokenKind::Eof ->
        return None
      _ -> ()
    i += 1
  None

export(crate) return_in_tail : &Block -> Vec<ParseError>
return_in_tail block =
  mut found = Vec::new ()
  scan_run (&block.stmts) false (&mut found)
  found

export(super) stmt : &mut Parser -> Result<Stmt, ParseError>
stmt p =
  leading = .peek_leading p
  start = (.peek_span p).0
  (local_test p)?
  if starts_a_declaration p
    declared = (.item p)?
    end = (.prev_span p).1
    trailing = if .check (&TokenKind::Newline) p
      taken = .clone (.peek p).trailing
      .advance p
      taken
    else
      None
    wrapped = Expr::Item { item = Box::new declared, span = (start, end) }
    return Ok (Stmt { expr = wrapped, span = (start, end), leading = Vec::new (), trailing })
  if opens_an_over p
    found = (over_update p start)?
    end = (.prev_span p).1
    trailing = (.newline_with_trailing "expected a line break after the statement" p)?
    return Ok (Stmt { expr = found, span = (start, end), leading, trailing })
  flow = (control_flow p)?
  if flow is Some found
    end = (.prev_span p).1
    trailing = (.newline_with_trailing "expected a line break after the statement" p)?
    return Ok (Stmt { expr = found, span = (start, end), leading, trailing })
  expr = if .check (&TokenKind::Kw Keyword::Return) p
    .advance p
    value = if .at_statement_end p
      None
    else
      Some $ Box::new (parse_expr p 0)?
    Expr::Return { value, span = (start, (.prev_span p).1) }
  else if (.try_draw p)? is Some drawn
    drawn
  else if (try_await p)? is Some awaited
    awaited
  else if (.try_binding p)? is Some bound
    Expr::Binding $ Box::new bound
  else
    value = (parse_expr p 0)?
    (assign_tail p value start)?
  end = (.prev_span p).1
  trailing = (.newline_with_trailing "expected a line break after the statement" p)?
  Ok (Stmt { expr, span = (start, end), leading, trailing })