voltaic-parser 0.2.0

Hand-written recursive-descent parser for the Voltaic language.
Documentation
use crate::ParseError;

use crate::block::block_expr;

use crate::expr::parse_expr;

use crate::item::rewritten;

use crate::parser::Parser;

use voltaic_ast::Expr;

use voltaic_ast::InlineRust;

use voltaic_ast::Signature;

use voltaic_ast::Stmt;

use voltaic_diag::Code;

use voltaic_lexer::Keyword;

use voltaic_lexer::Span;

use voltaic_lexer::Token;

use voltaic_lexer::TokenKind;

use voltaic_lexer::Trivia;

use voltaic_lexer::verbatim_triple;

pub(crate) fn expr_from_tokens(tokens: &[Token], span: Span) -> Result<Expr, ParseError> {
  let mut toks = tokens.to_vec();
  toks.push(Token {
    kind: TokenKind::Eof,
    span,
    ws_before: false,
    leading: Vec::new(),
    trailing: None,
  });
  let mut p = Parser::new(&toks);
  let expr = parse_expr(&mut p, 0)?;
  if !p.at_eof() {
    return Err(ParseError::r#with(
      voltaic_diag::Code::ExpectedToken,
      "an interpolation holds one expression; `}` was expected here",
      p.peek_span(),
    ));
  }
  Ok(expr)
}

pub(crate) fn flush_word(out: &mut String, word: &mut String, var: &str, subject: &str) {
  if word.is_empty() {
    return ();
  }
  if word == var {
    out.push_str(subject);
  } else {
    out.push_str(word);
  }
  word.clear();
  ();
}

pub(crate) fn inline_rust(
  p: &mut Parser,
  leading: Vec<Trivia>,
  start: usize,
) -> Result<InlineRust, ParseError> {
  p.expect(&TokenKind::Kw(Keyword::Rust), "expected `rust`")?;
  let hatched = rust_hatch(p)?;
  Ok(InlineRust {
    code: hatched.0,
    span: (start, p.prev_span().1),
    leading,
  })
}

pub(crate) fn is_word(ch: char) -> bool {
  ch.is_alphanumeric() || ch == '_'
}

pub(crate) fn reserved_word(p: &mut Parser) -> ParseError {
  debug_assert!(
    p.check(&TokenKind::Kw(Keyword::Do)),
    "`do` is the only reserved word with no construct"
  );
  let keyword = p.peek_span();
  p.advance();
  let labelled = match p.peek().kind {
    TokenKind::Label(_) => true,
    _ => false,
  };
  if labelled {
    p.advance();
  }
  if p.check(&TokenKind::Newline) {
    let _dropped = block_expr(p, None, keyword.0);
    ();
  }
  ParseError::with_help(
    Code::ReservedWord,
    "`do` is a reserved word and names no construct",
    "an indented block is an expression on its own, so drop the `do` and keep the block — `found = 'search` for the labelled form. The word stays reserved for plan 60's do-notation (plan 55 §1.7)",
    keyword,
  )
}

pub(crate) fn rust_hatch(p: &mut Parser) -> Result<(String, Span), ParseError> {
  let span = p.peek_span();
  if let TokenKind::RustBlock(code) = &p.peek().kind {
    let block_code = code.clone();
    p.advance();
    return Ok((block_code, span));
  }
  let TokenKind::Str { raw, .. } = &p.peek().kind else {
    return Err(ParseError::expected(
      "expected a `\"\"\"` block of Rust after `rust`, or an indented block on the lines under it",
      span,
    ));
  };
  let literal = raw.clone();
  p.advance();
  verbatim_triple(&literal).map(|code| (code, span)).ok_or_else(|| ParseError::expected("inline Rust needs a `\"\"\"` block or an indented one: a single-quoted string cannot hold Rust's own quotes", span))
}

pub(crate) fn rust_string_body(text: &str) -> String {
  let mut out = String::with_capacity(text.len());
  for ch in text.chars() {
    match ch {
      '\\' => out.push_str("\\\\"),
      '"' => out.push_str("\\\""),
      '\n' => out.push_str("\\n"),
      '\r' => out.push_str("\\r"),
      '\t' => out.push_str("\\t"),
      '\0' => out.push_str("\\0"),
      c if c.is_ascii_control() || c == '\u{7f}' => {
        let point = u32::from(c);
        out.push_str(&format!("\\x{:02X}", point))
      }
      _ => out.push(ch),
    };
  }
  out
}

pub(crate) fn scan_run(stmts: &[Stmt], in_tail: bool, problems: &mut Vec<ParseError>) {
  for pair in stmts.iter().enumerate() {
    let i = pair.0;
    let stmt = pair.1;
    if let Expr::Draw { .. } = &stmt.expr {
      let from = i + 1;
      let n = stmts.len();
      scan_run(&stmts[from..n], true, problems);
      return ();
    }
    if in_tail && let Expr::Return { .. } = &stmt.expr {
      problems.push(ParseError::r#with(Code::ReturnInCallbackTail, "`return` here returns from the closure `with` generates, not from the enclosing function — doc 03 §7.6. Use `?` to propagate, or restructure so the value is the callback's", stmt.span));
    }
    for nested in stmt.expr.statement_blocks() {
      scan_run(&nested.stmts, in_tail, problems);
    }
  }
  ();
}

pub(crate) fn substitute_var(sig: &mut Signature, var: &str, subject: &str) {
  for param in &mut sig.params {
    if let Some(ty) = &mut param.ty {
      ty.text = rewritten(&ty.text, var, subject);
    }
  }
  if let Some(ret) = &mut sig.ret {
    ret.text = rewritten(&ret.text, var, subject);
  }
  ();
}

pub(crate) fn token_text(kind: &TokenKind) -> String {
  match kind {
    TokenKind::Ident(t) => t.clone(),
    TokenKind::Kw(kw) => kw.text().to_string(),
    TokenKind::Int(t) | TokenKind::Float(t) => t.clone(),
    TokenKind::Char(c) => format!("{:?}", c),
    TokenKind::Byte(b) => {
      let value = *b;
      let ch = char::from(value);
      format!("b{:?}", ch)
    }
    TokenKind::Str { raw, .. } => raw.clone(),
    TokenKind::Label(name) => format!("'{}", name),
    TokenKind::ColonColon => "::".to_string(),
    TokenKind::Lt => "<".to_string(),
    TokenKind::Gt => ">".to_string(),
    TokenKind::Shr => ">>".to_string(),
    TokenKind::Compose => ">>>".to_string(),
    TokenKind::LParen => "(".to_string(),
    TokenKind::RParen => ")".to_string(),
    TokenKind::LBracket => "[".to_string(),
    TokenKind::RBracket => "]".to_string(),
    TokenKind::LBrace => "{".to_string(),
    TokenKind::RBrace => "}".to_string(),
    TokenKind::Comma => ",".to_string(),
    TokenKind::Semi => ";".to_string(),
    TokenKind::Amp => "&".to_string(),
    TokenKind::Star => "*".to_string(),
    TokenKind::Plus => "+".to_string(),
    TokenKind::Arrow => "->".to_string(),
    TokenKind::Colon => ":".to_string(),
    TokenKind::Eq => "=".to_string(),
    TokenKind::Bang => "!".to_string(),
    TokenKind::Dot => ".".to_string(),
    TokenKind::Question => "?".to_string(),
    TokenKind::FatArrow => "=>".to_string(),
    TokenKind::DrawArrow => "<-".to_string(),
    TokenKind::ComposeBack => "<<<".to_string(),
    TokenKind::Pipe => "|>".to_string(),
    TokenKind::Coalesce => "??".to_string(),
    TokenKind::CoalesceEq => "??=".to_string(),
    TokenKind::DotDotDot => "...".to_string(),
    TokenKind::DotDot => "..".to_string(),
    TokenKind::DotDotEq => "..=".to_string(),
    TokenKind::DotParen => ".(".to_string(),
    TokenKind::DotPlus => ".+".to_string(),
    TokenKind::DotMinus => ".-".to_string(),
    TokenKind::DotStar => ".*".to_string(),
    TokenKind::DotSlash => "./".to_string(),
    TokenKind::DotPercent => ".%".to_string(),
    TokenKind::AmpColon => "&:".to_string(),
    TokenKind::Backslash => "\\".to_string(),
    TokenKind::Dollar => "$".to_string(),
    TokenKind::At => "@".to_string(),
    TokenKind::AtAt => "@@".to_string(),
    TokenKind::ColonEq => ":=".to_string(),
    TokenKind::Minus => "-".to_string(),
    TokenKind::Slash => "/".to_string(),
    TokenKind::Percent => "%".to_string(),
    TokenKind::PlusEq => "+=".to_string(),
    TokenKind::MinusEq => "-=".to_string(),
    TokenKind::StarEq => "*=".to_string(),
    TokenKind::SlashEq => "/=".to_string(),
    TokenKind::PercentEq => "%=".to_string(),
    TokenKind::EqEq => "==".to_string(),
    TokenKind::Ne => "!=".to_string(),
    TokenKind::Le => "<=".to_string(),
    TokenKind::Ge => ">=".to_string(),
    TokenKind::AndAnd => "&&".to_string(),
    TokenKind::OrOr => "||".to_string(),
    TokenKind::Or => "|".to_string(),
    TokenKind::Caret => "^".to_string(),
    TokenKind::Tilde => "~".to_string(),
    TokenKind::Shl => "<<".to_string(),
    TokenKind::UserOp(text) => text.clone(),
    other => format!("{:?}", other),
  }
}

#[cfg(test)]
mod __vlt_tests {
  use super::*;

  #[test]
  fn respells_the_tokens_a_type_can_contain() {
    assert_eq!(token_text(&TokenKind::Ident("Vec".into())), "Vec");
    assert_eq!(token_text(&TokenKind::Lt), "<");
    assert_eq!(token_text(&TokenKind::Shr), ">>");
    assert_eq!(token_text(&TokenKind::Compose), ">>>");
    assert_eq!(token_text(&TokenKind::Amp), "&");
    assert_eq!(
      token_text(&TokenKind::Kw(voltaic_lexer::Keyword::Mut)),
      "mut"
    );
    assert_eq!(token_text(&TokenKind::Label("a".into())), "'a");
  }

  #[test]
  fn respells_every_operator_the_lexer_produces() {
    const SOURCE: &str = "... ..= ??= >>> <<< .. .( .+ .- .* ./ .% ?? |> &: :: := -> <- => == != <= >= && || += -= *= /= %= << >> ( ) [ ] { } @@ @ ? : \\\\ $ , . ; = + - * / % < > ! & | ^ ~";
    let toks = voltaic_lexer::lex(SOURCE).expect("the operator source should lex");
    let mut checked = 0;
    for tok in &toks {
      let skip = match tok.kind {
        TokenKind::Newline | TokenKind::Indent | TokenKind::Dedent | TokenKind::Eof => true,
        _ => false,
      };
      if skip {
        continue;
      }
      let slice = &SOURCE[tok.span.0..tok.span.1];
      let spelled = token_text(&tok.kind);
      assert_eq!(
        spelled.as_str(),
        slice,
        "{:?} respells as `{}` but its source is `{}` — an unlisted token answers with its own Debug name, which reaches generated Rust",
        tok.kind,
        spelled,
        slice
      );
      checked += 1;
    }
    assert!(
      checked > 50,
      "only {} operators were checked, so this test is not reading the source it thinks it is",
      checked
    );
  }
}