voltaic-parser 0.2.0

Hand-written recursive-descent parser for the Voltaic language.
Documentation
import crate::ParseError
import crate::block::bind_block
import crate::block::block_expr
import crate::block::case_of
import crate::block::modified_block
import crate::operand::bracketed
import crate::operand::byte_literal
import crate::operand::interpolated_string
import crate::operand::lambda
import crate::operand::macro_call
import crate::operand::method_call_syntax
import crate::operand::optional_chaining
import crate::operand::record_literal
import crate::operand::section
import crate::parser::Parser
import crate::signature::TypeStop
import crate::signature::capture_type
import crate::support::PREFIX_POWER
import crate::support::binary_power
import crate::token::rust_hatch
import crate::token::rust_string_body
import voltaic_ast::BinaryOp
import voltaic_ast::Expr
import voltaic_ast::Fixity
import voltaic_ast::Ident
import voltaic_ast::TypeExpr
import voltaic_ast::UnaryOp
import voltaic_diag::Code
import voltaic_lexer::Keyword
import voltaic_lexer::StrPart
import voltaic_lexer::TokenKind

export(crate) const ABOVE_AND_AND : u8 = 10

export(crate) application : &mut Parser -> Result<Expr, ParseError>
application p =
  mut head = (atom p)?
  while !.just_dedented p && starts_argument (&(.peek p).kind)
    mut args = Vec::new ()
    while starts_argument (&(.peek p).kind)
      .push (atom p)? args
    span = ((.span head).0, (.prev_span p).1)
    built = case head of
      Expr::Field { recv, name, .. } -> Expr::Dot { recv, name, args, span }
      other -> Expr::Call { callee = Box::new other, args, is_dollar = false, span }
    head := built
    loop
      case (postfix p head)? of
        Ok grown -> head := grown
        Err unchanged ->
          head := unchanged
          break
  Ok head

export(crate) atom : &mut Parser -> Result<Expr, ParseError>
atom p =
  mut expr = (prefix p)?
  loop
    case (postfix p expr)? of
      Ok grown -> expr := grown
      Err unchanged ->
        return Ok unchanged

export(crate) binary_op : &mut Parser -> Option<BinaryOp>
binary_op p =
  found = (binary_power (&(.peek p).kind))?
  .advance p
  Some found.0

export(crate) digit_precedence : &str -> Option<u8>
digit_precedence text = case text of
  "0" -> Some 0
  "1" -> Some 1
  "2" -> Some 2
  "3" -> Some 3
  "4" -> Some 4
  "5" -> Some 5
  "6" -> Some 6
  "7" -> Some 7
  "8" -> Some 8
  "9" -> Some 9
  _ -> None

export(crate) operator_chains : &Parser -> &Ident -> bool
operator_chains p spelling = case &(.peek p).kind of
  TokenKind::UserOp next -> next == &spelling.text
  _ -> false

export(crate) parse_expr : &mut Parser -> u8 -> Result<Expr, ParseError>
parse_expr p min =
  mut lhs = (application p)?
  loop
    if min == 0 && .check (&TokenKind::Colon) p
      .advance p
      ty = (capture_type p "expected a type after `:`" TypeStop::Greedy)?
      at = ((.span lhs).0, ty.span.1)
      lhs := Expr::Ascribe { inner = Box::new lhs, ty, span = at }
      continue
    if min <= 1 && .check (&TokenKind::Dollar) p
      .advance p
      arg = (parse_expr p 1)?
      at = ((.span lhs).0, (.span arg).1)
      lhs := Expr::Call { callee = Box::new lhs, args = vec! \-> arg, is_dollar = true, span = at }
      continue
    if user_rung p is Some declared
      if declared.2 < min
        return Ok lhs
      spelling = declared.0
      .advance p
      rhs = (parse_expr p declared.3)?
      span = ((.span lhs).0, (.span rhs).1)
      if declared.1 == Fixity::NonAssoc && operator_chains p (&spelling)
        return Err $ ParseError::with_help Code::NonAssociativeOperator (format! \-> "`#{spelling.text}` was declared `infix`, so it does not chain") "write the grouping — `(a #{spelling.text} b) #{spelling.text} c` — or declare it `infixl` or `infixr`" (.peek_span p)
      lhs := Expr::UserBinary { spelling, fixity = declared.1, precedence = declared.5, target = declared.4, lhs = Box::new lhs, rhs = Box::new rhs, span }
      continue
    guard binary_power (&(.peek p).kind) is Some rung else
      if &(.peek p).kind is TokenKind::UserOp spelling
        return Err $ ParseError::with_help Code::UndeclaredOperator (format! \-> "`#{spelling}` is not declared in this file") "declare it above its first use — `infixl 6 #{spelling} = Set::union`. An operator's scope is the file, which is plan 14 decision 4's refusal of name resolution" (.peek_span p)
      return Ok lhs
    if rung.1 < min
      return Ok lhs
    .advance p
    rhs = (parse_expr p rung.2)?
    span = ((.span lhs).0, (.span rhs).1)
    lhs := Expr::Binary { op = rung.0, lhs = Box::new lhs, rhs = Box::new rhs, span }

export(crate) parse_spine : &mut Parser -> Result<Vec<TypeExpr>, ParseError>
parse_spine p =
  mut spine = vec! \-> (capture_type p "expected a type after `::`" TypeStop::Greedy)?
  while .check (&TokenKind::Arrow) p
    .advance p
    .push (capture_type p "expected a type after `->`" TypeStop::Greedy)? spine
  Ok spine

#[allow(clippy::result_large_err)]
export(crate) postfix : &mut Parser -> Expr -> Result<Result<Expr, Expr>, ParseError>
postfix p lhs =
  if .just_dedented p
    return Ok (Err lhs)
  if lhs is Expr::Unit { .. }
    return Ok (Err lhs)
  start = (.span lhs).0

  if .check (&TokenKind::Question) p
    glued = .is_some_and (\t -> t.kind == TokenKind::Dot && !t.ws_before) (.peek_at 1 p)
    if glued
      at = .peek_span p
      return Err (optional_chaining (at.0, at.1 + 1))
    .advance p
    return Ok (Ok (Expr::Try { inner = Box::new lhs, span = (start, (.prev_span p).1) }))
  if .check (&TokenKind::LBracket) p
    .advance p
    .skip_layout p
    index = (parse_expr p 0)?
    .skip_layout p
    (.expect (&TokenKind::RBracket) "expected `]` to close the index" p)?
    return Ok (Ok (Expr::Index { recv = Box::new lhs, index = Box::new index, span = (start, (.prev_span p).1) }))
  if .check (&TokenKind::LBrace) p
    return Ok (Ok (record_literal p lhs)?)
  if .check (&TokenKind::Dot) p
    .advance p
    if &(.peek p).kind is TokenKind::Int digits
      index_name = Ident { text = .clone digits, span = .peek_span p }
      .advance p
      return Ok (Ok (Expr::Field { recv = Box::new lhs, name = index_name, span = (start, (.prev_span p).1) }))
    name = (.ident "expected a field name or a tuple index after `.`" p)?
    if .check (&TokenKind::LParen) p && !(.peek p).ws_before
      return Err (method_call_syntax (&name.text) (start, name.span.1))
    return Ok (Ok (Expr::Field { recv = Box::new lhs, name, span = (start, (.prev_span p).1) }))
  Ok $ Err lhs

export(crate) prefix : &mut Parser -> Result<Expr, ParseError>
prefix p =
  start = (.peek_span p).0
  if unary_op p is Some op
    operand = (parse_expr p PREFIX_POWER)?
    span = (start, (.span operand).1)
    return Ok (Expr::Unary { op, operand = Box::new operand, span })
  case .clone (.peek p).kind of
    TokenKind::Int text or TokenKind::Float text ->
      span = .peek_span p
      .advance p
      Ok (Expr::Num { text, span })
    TokenKind::Str { parts, raw } ->
      span = .peek_span p
      .advance p
      mut interpolates = false
      for part in &parts
        case part of
          StrPart::Interp { .. } -> interpolates := true
          _ -> ()
      if interpolates
        return interpolated_string (&parts) span
      if .starts_with "\"\"\"" raw
        mut joined = String::new ()
        for part in &parts
          case part of
            StrPart::Text text -> .push_str (.as_str text) joined
            StrPart::Interp { .. } ->
              unreachable! \-> "checked above"
        body = rust_string_body (&joined)
        return Ok (Expr::StrLit { value = format! \-> "\"#{body}\"", span })
      Ok (Expr::StrLit { value = raw, span })
    TokenKind::RawStr { text, hashes } ->
      span = .peek_span p
      .advance p
      pad = .repeat hashes "#"
      Ok $ Expr::RawLit { text = format! \-> "r#{pad}\"#{text}\"#{pad}", span }
    TokenKind::ByteStr text ->
      span = .peek_span p
      .advance p
      body = rust_string_body (&text)
      Ok $ Expr::RawLit { text = format! \-> "b\"#{body}\"", span }
    TokenKind::CStr text ->
      span = .peek_span p
      .advance p
      body = rust_string_body (&text)
      Ok $ Expr::RawLit { text = format! \-> "c\"#{body}\"", span }
    TokenKind::Byte value ->
      span = .peek_span p
      .advance p
      Ok (Expr::RawLit { text = byte_literal value, span })
    TokenKind::Kw (Keyword::Async) -> modified_block p Keyword::Async start
    TokenKind::Kw (Keyword::Unsafe) -> modified_block p Keyword::Unsafe start
    TokenKind::Kw (Keyword::Try) -> modified_block p Keyword::Try start
    TokenKind::Char value ->
      span = .peek_span p
      .advance p
      Ok (Expr::Char { value, span })
    TokenKind::Kw (Keyword::True) ->
      span = .peek_span p
      .advance p
      Ok (Expr::Bool { value = true, span })
    TokenKind::Kw (Keyword::False) ->
      span = .peek_span p
      .advance p
      Ok (Expr::Bool { value = false, span })
    TokenKind::At ->
      .advance p
      name = (.ident "expected a field name after `@`" p)?
      span = (start, name.span.1)
      Ok (Expr::SelfField { name, span })
    TokenKind::AtAt ->
      .advance p
      name = (.ident "expected a name after `@@`" p)?
      span = (start, name.span.1)
      Ok (Expr::SelfType { name, span })
    TokenKind::LBracket -> bracketed p
    TokenKind::LParen ->
      .advance p
      .skip_layout p
      if .check (&TokenKind::RParen) p
        .advance p
        return Ok (Expr::Unit { span = (start, (.prev_span p).1) })
      sectioned = (section p start)?
      if sectioned is Some found
        return Ok found
      inner = (parse_expr p 0)?
      .skip_layout p
      if .check (&TokenKind::Comma) p
        mut items = vec! \-> inner
        while .check (&TokenKind::Comma) p
          .advance p
          .skip_layout p
          if .check (&TokenKind::RParen) p
            break
          .push (parse_expr p 0)? items
          .skip_layout p
        (.expect (&TokenKind::RParen) "expected `)` to close the tuple" p)?
        return Ok (Expr::Tuple { items, span = (start, (.prev_span p).1) })
      (.expect (&TokenKind::RParen) "expected `)` to close the group" p)?
      Ok (Expr::Paren { inner = Box::new inner, span = (start, (.prev_span p).1) })
    TokenKind::Kw (Keyword::Rust) ->
      .advance p
      hatched = (rust_hatch p)?
      Ok (Expr::RustHatch { code = hatched.0, span = (start, (.prev_span p).1) })
    TokenKind::Backslash -> lambda p
    TokenKind::Kw (Keyword::Move) -> lambda p
    TokenKind::Kw (Keyword::If) -> .if_statement p
    TokenKind::Kw (Keyword::Do) -> bind_block p start
    TokenKind::Newline -> block_expr p None start
    TokenKind::Label name ->
      .advance p
      block_expr p (Some name) start
    TokenKind::Kw (Keyword::Case) -> case_of p
    TokenKind::Kw (Keyword::SelfValue) ->
      span = .peek_span p
      .advance p
      Ok $ Expr::Ident $ Ident { text = .to_string "self", span }
    TokenKind::Kw (Keyword::SelfType) ->
      span = .peek_span p
      .advance p
      Ok $ Expr::Ident $ Ident { text = .to_string "Self", span }
    TokenKind::Dot ->
      .advance p
      name = (.ident "expected a method name after `.`" p)?
      span = (start, name.span.1)
      Ok (Expr::MethodRef { name, span })
    TokenKind::Ident _ -> macro_call p
    _ ->
      Err $ ParseError::r#with Code::ExpectedExpression "expected an expression" (.peek_span p)

export(crate) starts_argument : &TokenKind -> bool
starts_argument kind = case kind of
  TokenKind::Int _ or TokenKind::Float _ or TokenKind::Str { .. } or TokenKind::RawStr { .. } or TokenKind::ByteStr _ or TokenKind::CStr _ or TokenKind::Char _ or TokenKind::Byte _ or TokenKind::Ident _ or TokenKind::LParen or TokenKind::At or TokenKind::AtAt -> true
  TokenKind::Kw (Keyword::True or Keyword::False or Keyword::SelfValue or Keyword::SelfType) -> true
  _ -> false

export(crate) unary_op : &mut Parser -> Option<UnaryOp>
unary_op p =
  op = case (.peek p).kind of
    TokenKind::Minus -> UnaryOp::Neg
    TokenKind::Bang -> UnaryOp::Not
    TokenKind::Star -> UnaryOp::Deref
    TokenKind::Amp ->
      .advance p
      if .eat_keyword Keyword::Mut p
        return Some UnaryOp::RefMut
      return Some UnaryOp::Ref
    _ ->
      return None
  .advance p
  Some op

export(crate) user_rung : &Parser -> Option<(Ident, Fixity, u8, u8, String, u8)>
user_rung p =
  guard &(.peek p).kind is TokenKind::UserOp text else
    return None
  declared = (.get (.as_str text) p.fixities)?
  fixity = declared.0
  precedence = declared.1
  powers = .powers precedence fixity
  spelling = Ident { text = .clone text, span = .peek_span p }
  Some (spelling, fixity, powers.0, powers.1, .clone declared.2, precedence)