voltaic-parser 0.2.0

Hand-written recursive-descent parser for the Voltaic language.
Documentation
import crate::parse
import voltaic_ast::BinaryOp
import voltaic_ast::Program
import voltaic_diag::Code
import voltaic_diag::Span
import voltaic_lexer::Keyword
import voltaic_lexer::LexError
import voltaic_lexer::Token
import voltaic_lexer::TokenKind

export data ParseError { export code Code, export message String, export span Span, export help Option<String> }
  deriving (Debug, Clone, PartialEq, Eq)

instance ParseError
  export(crate) expected : impl Into<String> -> Span -> Self
  expected message span =
    ParseError { code = Code::ExpectedToken, message = .into message, span, help = None }

  export(crate) r#with : Code -> impl Into<String> -> Span -> Self
  r#with code message span =
    ParseError { code, message = .into message, span, help = None }

  export(crate) with_help : Code -> impl Into<String> -> impl Into<String> -> Span -> Self
  with_help code message help span =
    ParseError { code, message = .into message, span, help = Some (.into help) }

export data ParseFailure
  = Lex LexError
  | Parse Vec<ParseError>
  deriving (Debug, Clone, PartialEq, Eq)

export const MAX_NESTING : usize = 16

export(crate) nesting_depth : &[Token] -> usize
nesting_depth toks =
  mut depth : usize = 0
  mut deepest : usize = 0
  for token in toks
    case token.kind of
      TokenKind::LParen or TokenKind::LBracket or TokenKind::LBrace ->
        depth += 1
        deepest := .max depth deepest
      TokenKind::RParen or TokenKind::RBracket or TokenKind::RBrace ->
        depth := .saturating_sub 1 depth
      _ -> ()
  deepest

export(crate) above_bitor : u8
above_bitor =
  found = .expect "`|` is in the precedence table" $ binary_power (&TokenKind::Or)
  found.1 + 1

export(crate) binary_power : &TokenKind -> Option<(BinaryOp, u8, u8)>
binary_power kind =
  op = case kind of
    TokenKind::Pipe -> BinaryOp::Pipeline
    TokenKind::Coalesce -> BinaryOp::Coalesce
    TokenKind::Kw (Keyword::Catch) -> BinaryOp::Catch
    TokenKind::DotDot -> BinaryOp::Range
    TokenKind::DotDotEq -> BinaryOp::RangeInclusive
    TokenKind::OrOr -> BinaryOp::Or
    TokenKind::AndAnd -> BinaryOp::And
    TokenKind::EqEq -> BinaryOp::Eq
    TokenKind::Ne -> BinaryOp::Ne
    TokenKind::Lt -> BinaryOp::Lt
    TokenKind::Gt -> BinaryOp::Gt
    TokenKind::Le -> BinaryOp::Le
    TokenKind::Ge -> BinaryOp::Ge
    TokenKind::Or -> BinaryOp::BitOr
    TokenKind::Caret -> BinaryOp::BitXor
    TokenKind::Amp -> BinaryOp::BitAnd
    TokenKind::Shl -> BinaryOp::Shl
    TokenKind::Shr -> BinaryOp::Shr
    TokenKind::Plus -> BinaryOp::Add
    TokenKind::Minus -> BinaryOp::Sub
    TokenKind::Star -> BinaryOp::Mul
    TokenKind::Slash -> BinaryOp::Div
    TokenKind::Percent -> BinaryOp::Rem
    TokenKind::Compose -> BinaryOp::Compose
    TokenKind::ComposeBack -> BinaryOp::ComposeBack
    _ ->
      return None
  left = .power op
  Some (op, left, left + 1)

#[cfg(test)]
power : TokenKind -> u8
power kind = (.expect "should have a precedence" (binary_power (&kind))).1

test "precedence follows rust"
  assert! \-> power TokenKind::Star > power TokenKind::Plus
  assert! \-> power TokenKind::Plus > power TokenKind::Shl
  assert! \-> power TokenKind::Shl > power TokenKind::Amp
  assert! \-> power TokenKind::Amp > power TokenKind::Caret
  assert! \-> power TokenKind::Caret > power TokenKind::Or
  assert! \-> power TokenKind::Or > power TokenKind::EqEq
  assert! \-> power TokenKind::EqEq > power TokenKind::AndAnd
  assert! \-> power TokenKind::AndAnd > power TokenKind::OrOr

test "the new operators sit where doc 03 places them"
  assert! \-> power TokenKind::Coalesce < power TokenKind::OrOr
  assert! \-> power TokenKind::Pipe < power TokenKind::Coalesce

test "unary and postfix bind tighter than any binary"
  assert! \-> crate::support::PREFIX_POWER > power TokenKind::Star

test "binary operators are left associative"
  for kind in [TokenKind::Plus, TokenKind::Star, TokenKind::Pipe]
    rung = .expect "should have a precedence" $ binary_power (&kind)
    assert_eq! \-> rung.2 \-> rung.1 + 1 \-> "#{kind:?} should be left-associative"

test "a non operator has no precedence"
  assert! \-> .is_none (binary_power (&TokenKind::Ident (.into "x")))
  assert! \-> .is_none (binary_power (&TokenKind::Newline))

test "the powers come from the operator"
  for kind in [TokenKind::Pipe, TokenKind::Coalesce, TokenKind::OrOr, TokenKind::EqEq, TokenKind::Plus, TokenKind::Percent]
    rung = .expect "should have a precedence" $ binary_power (&kind)
    assert_eq! \-> rung.1 \-> .power rung.0 \-> "#{kind:?} reports a power of its own"
    assert_eq! \-> rung.2 \-> .power rung.0 + 1

test "maps tokens to their operators"
  assert_eq! \-> .map (\pair -> pair.0) (binary_power (&TokenKind::Plus)) \-> Some BinaryOp::Add
  assert_eq! \-> .map (\pair -> pair.0) (binary_power (&TokenKind::DotDot)) \-> Some BinaryOp::Range \-> "`..` is exclusive (doc 03 ยง4.2)"

export(crate) const PREFIX_POWER : u8 = 25

export parse_source : &str -> Result<Program, ParseFailure>
parse_source src =
  toks = (.map_err ParseFailure::Lex (voltaic_lexer::lex src))?
  .map_err ParseFailure::Parse $ parse (&toks) (.len src)