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 __rdx_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
);
}
}