use crate::ParseError;
use crate::block::return_in_tail;
use crate::expr::digit_precedence;
use crate::expr::parse_expr;
use crate::expr::parse_spine;
use crate::parser::Parser;
use crate::pattern::pattern_alt;
use crate::signature::TypeStop;
use crate::signature::capture_type;
use crate::signature::signature_only;
use crate::token::flush_word;
use crate::token::token_text;
use crate::type_class::walk_member_for_tails;
use voltaic_ast::Block;
use voltaic_ast::ConstDecl;
use voltaic_ast::ExternBlock;
use voltaic_ast::Fixity;
use voltaic_ast::ForeignFn;
use voltaic_ast::Ident;
use voltaic_ast::ImportDecl;
use voltaic_ast::Item;
use voltaic_ast::ModuleDecl;
use voltaic_ast::OperatorDecl;
use voltaic_ast::PatternSynonym;
use voltaic_ast::TestDecl;
use voltaic_ast::TypeAlias;
use voltaic_ast::Visibility;
use voltaic_diag::Code;
use voltaic_lexer::Keyword;
use voltaic_lexer::TokenKind;
use voltaic_lexer::Trivia;
pub(crate) fn alias_target(p: &mut Parser, spelling: &Ident) -> Result<String, ParseError> {
let mut text = String::new();
loop {
let kind = &p.peek().kind;
let at_end = match kind {
TokenKind::Newline | TokenKind::Indent | TokenKind::Dedent | TokenKind::Eof => true,
_ => false,
};
if at_end {
break;
}
if p.peek().ws_before && !text.is_empty() {
text.push(' ');
}
text.push_str(&token_text(kind));
p.advance();
}
let trimmed = text.as_str().trim();
if trimmed.is_empty() {
return Err(ParseError::with_help(
Code::OperatorShape,
"expected a function for this operator to stand for",
"a declaration is an **alias**: `infixl 6 <+> = Set::union` makes `a <+> b` emit `Set::union(a, b)`. It declares no function of its own, because no Rust function can be named `<+>`",
spelling.span,
));
}
Ok(trimmed.to_string())
}
pub(crate) fn const_decl(
p: &mut Parser,
leading: Vec<Trivia>,
start: usize,
visibility: Visibility,
) -> Result<ConstDecl, ParseError> {
let is_static = p.check(&TokenKind::Kw(Keyword::Static));
p.advance();
let name = p.ident("expected a name after `const`")?;
p.expect(
&TokenKind::Colon,
"expected `:` and a type — doc 03 §13.4 requires one on a module-scope binding",
)?;
let ty = capture_type(p, "expected the binding's type", TypeStop::Greedy)?;
p.expect(
&TokenKind::Eq,
"expected `=` and a value — a module-scope binding must be initialised",
)?;
let value = parse_expr(p, 0)?;
Ok(ConstDecl {
name,
ty,
value,
is_static,
visibility,
span: (start, p.prev_span().1),
leading,
})
}
pub(crate) fn declares_an_operator(p: &Parser) -> bool {
let word_is_fixity = match &p.peek().kind {
TokenKind::Ident(text) => Fixity::from_keyword(text.as_str()).is_some(),
_ => false,
};
if !word_is_fixity {
return false;
}
let level_is_an_int = p.peek_at(1).is_some_and(|t| match &t.kind {
TokenKind::Int(_) => true,
_ => false,
});
let names_an_operator = p.peek_at(2).is_some_and(|t| match &t.kind {
TokenKind::UserOp(_) => true,
_ => false,
});
level_is_an_int && names_an_operator
}
pub(crate) fn extern_block(
p: &mut Parser,
leading: Vec<Trivia>,
start: usize,
) -> Result<ExternBlock, ParseError> {
p.expect(&TokenKind::Kw(Keyword::Extern), "expected `extern`")?;
let abi = match &p.peek().kind {
TokenKind::Str { raw, .. } => raw.clone(),
_ => {
return Err(ParseError::expected(
"expected an ABI string after `extern`, as in `extern \"C\"`",
p.peek_span(),
));
}
};
p.advance();
p.newline_with_trailing("expected a line break after the ABI")?;
let mut decls = Vec::new();
p.expect(
&TokenKind::Indent,
"expected indented `extern` declarations",
)?;
loop {
p.skip_separators();
if p.check(&TokenKind::Dedent) || p.at_eof() {
break;
}
let decl_leading = p.peek_leading();
let decl_start = p.peek_span().0;
let name = p.ident("expected a foreign function's name")?;
p.expect(
&TokenKind::Colon,
"expected `:` after the foreign function's name",
)?;
let spine = parse_spine(p)?;
let span = (decl_start, p.prev_span().1);
decls.push(ForeignFn {
name,
sig: signature_only(&spine, span),
span,
leading: decl_leading,
});
p.newline_with_trailing("expected a line break after the declaration")?;
}
p.expect(&TokenKind::Dedent, "expected the `extern` block to end")?;
Ok(ExternBlock {
abi,
decls,
span: (start, p.prev_span().1),
leading,
})
}
pub(crate) fn import_decl(
p: &mut Parser,
leading: Vec<Trivia>,
start: usize,
visibility: Visibility,
) -> Result<ImportDecl, ParseError> {
p.expect(&TokenKind::Kw(Keyword::Import), "expected `import`")?;
let path = import_path(p)?;
let alias = if p.check(&TokenKind::Kw(Keyword::As)) {
p.advance();
Some(p.ident("expected a name after `as`")?)
} else {
None
};
Ok(ImportDecl {
path,
alias,
visibility,
span: (start, p.prev_span().1),
leading,
})
}
pub(crate) fn import_path(p: &mut Parser) -> Result<String, ParseError> {
let start = p.peek_span();
let mut depth: i32 = 0;
let mut text = String::new();
loop {
let kind = &p.peek().kind;
let ends = match kind {
TokenKind::Newline
| TokenKind::Indent
| TokenKind::Dedent
| TokenKind::Eof
| TokenKind::Kw(Keyword::As) => true,
_ => false,
};
if depth == 0 && ends {
break;
}
match kind {
TokenKind::LBrace => depth += 1,
TokenKind::RBrace => depth -= 1,
_ => (),
};
if kind == &TokenKind::Colon {
return Err(ParseError::with_help(
Code::ExpectedName,
"`:` is not a path separator",
"an import path is separated by `::` — `import std::iter::once`. A single `:` reaches the generated `use` verbatim and is not Rust",
p.peek_span(),
));
}
let renames = match kind {
TokenKind::Kw(Keyword::As) => true,
_ => false,
};
if renames {
text.push_str(" as ");
p.advance();
continue;
}
text.push_str(&token_text(kind));
let separates = match kind {
TokenKind::Comma => true,
_ => false,
};
if separates {
text.push(' ');
}
p.advance();
}
if text.is_empty() {
return Err(ParseError::r#with(
Code::ExpectedName,
"expected a module path after `import`",
start,
));
}
Ok(text)
}
pub(crate) fn local_test(p: &mut Parser) -> Result<(), ParseError> {
if !p.check(&TokenKind::Kw(Keyword::Test)) {
return Ok(());
}
let keyword = p.peek_span();
let _dropped = test_decl(p, Vec::new(), keyword.0);
Err(ParseError::with_help(
Code::LocalTest,
"a `test` declaration belongs at module scope, and this one is inside a body",
"move it out to the left margin: rustc does not collect a nested `#[test]` — it warns `cannot test inner items` and the test silently never runs (plan 55 §1.5)",
keyword,
))
}
pub(crate) fn module_decl(
p: &mut Parser,
leading: Vec<Trivia>,
start: usize,
visibility: Visibility,
) -> Result<ModuleDecl, ParseError> {
p.expect(&TokenKind::Kw(Keyword::Module), "expected `module`")?;
let name = p.ident("expected the module's name")?;
let has_body =
p.check(&TokenKind::Newline) && p.peek_at(1).is_some_and(|t| t.kind == TokenKind::Indent);
if !has_body {
return Ok(ModuleDecl {
name,
items: None,
visibility,
span: (start, p.prev_span().1),
leading,
});
}
p.advance();
p.advance();
let items = p.items_until_dedent()?;
Ok(ModuleDecl {
name,
items: Some(items),
visibility,
span: (start, p.prev_span().1),
leading,
})
}
pub(crate) fn operator_decl(
p: &mut Parser,
leading: Vec<Trivia>,
start: usize,
) -> Result<OperatorDecl, ParseError> {
let word = p.ident("expected `infix`, `infixl` or `infixr`")?;
let Some(fixity) = Fixity::from_keyword(word.text.as_str()) else {
return Err(ParseError::r#with(
Code::OperatorShape,
"expected `infix`, `infixl` or `infixr`",
word.span,
));
};
let at_level = p.peek_span();
let TokenKind::Int(digits) = &p.peek().kind else {
return Err(ParseError::with_help(
Code::OperatorShape,
"expected a precedence after the fixity keyword",
"the scale is Haskell's 0 to 9 — `infixl 6 <+> = Set::union`",
at_level,
));
};
let Some(level) = digit_precedence(digits.as_str()) else {
return Err(ParseError::with_help(
Code::OperatorPrecedence,
"a declared precedence must be a single digit, 0 to 9",
"the scale is Haskell's, and `Fixity::powers` maps it onto the binding powers between `|>` and `+`. Every user operator binds looser than arithmetic, which is what keeps a familiar expression meaning what it looks like. No suffix and no separator — `infixl 6`, not `infixl 6u8`",
at_level,
));
};
p.advance();
let at_spelling = p.peek_span();
let TokenKind::UserOp(text) = &p.peek().kind else {
return Err(ParseError::with_help(
Code::OperatorShape,
"expected an assignable operator after the precedence",
"the admissible spellings are closed: `<+>`, `<->`, `<*>`, `</>`, `<%>`, `<|>`, `<&>`, `<^>`, `<=>`, `<~>`, `<$>`, `<?>`. `ASSIGNABLE_OPERATORS` records why",
at_spelling,
));
};
let spelling = Ident {
text: text.clone(),
span: at_spelling,
};
p.advance();
p.expect(&TokenKind::Eq, "expected `=` after the operator's spelling")?;
let target = alias_target(p, &spelling)?;
p.fixities
.insert(spelling.text.clone(), (fixity, level, target.clone()));
p.newline_with_trailing("expected a line break after the operator declaration")?;
Ok(OperatorDecl {
spelling,
fixity,
precedence: level,
target,
span: (start, p.prev_span().1),
leading,
})
}
pub(crate) fn pattern_synonym(
p: &mut Parser,
leading: Vec<Trivia>,
start: usize,
) -> Result<PatternSynonym, ParseError> {
p.advance();
let name = p.ident("expected a name for the pattern synonym")?;
let mut params = Vec::new();
while let Some(param) = p.try_ident() {
params.push(param);
}
p.expect(
&TokenKind::Eq,
"expected `=` after the pattern synonym's name",
)?;
let body = pattern_alt(p)?;
p.newline_with_trailing("expected a line break after the pattern synonym")?;
let declared: Vec<String> = params.iter().map(|i| i.text.clone()).collect();
for bound in body.binders() {
if !declared.contains(&bound) {
let message = format!(
"`{}` is not a parameter of this pattern synonym, so it would introduce a name the use site never wrote",
bound
);
return Err(ParseError::with_help(
Code::PatternSynonymShape,
message,
"a synonym's body may bind only its own parameters — `pattern Both a b = (Some a, Some b)` declares `a` and `b` and binds exactly those. A name from nowhere is what `docs/DECLINED.md` refuses `RecordWildCards` for",
name.span,
));
}
}
Ok(PatternSynonym {
name,
params,
body,
span: (start, p.prev_span().1),
leading,
})
}
pub(crate) fn retired_spelling(name: &Ident) -> Option<ParseError> {
let replacement = match name.text.as_str() {
"assert" => "`assert! \\-> cond`, or `assert! \\-> a == b` for a comparison",
"panic" => "`panic! \\-> \"message\"`",
"print" => "`println! \\-> \"#{x}\"`, or `eprintln!` to write to stderr",
"todo" => "`todo!`",
"unimplemented" => "`unimplemented!`",
"unreachable" => "`unreachable!`",
_ => {
return None;
}
};
let message = format!(
"`{}` is not a construct any more — macros are callable, so write {}",
name.text, replacement
);
Some(ParseError::with_help(
Code::RetiredSpelling,
message,
"`!` marks a macro's name and `\\->` opens each argument, so no bracket is needed: `println! \\-> \"#{total} rows\"`",
name.span,
))
}
pub(crate) fn rewritten(text: &str, var: &str, subject: &str) -> String {
let mut out = String::with_capacity(text.len());
let mut word = String::new();
for ch in text.chars() {
if ch.is_alphanumeric() || ch == '_' {
word.push(ch);
continue;
}
flush_word(&mut out, &mut word, var, subject);
out.push(ch);
}
flush_word(&mut out, &mut word, var, subject);
out
}
pub(crate) fn starts_a_declaration(p: &Parser) -> bool {
match p.peek().kind {
TokenKind::Kw(
Keyword::Data
| Keyword::Newtype
| Keyword::Union
| Keyword::Type
| Keyword::Class
| Keyword::Instance
| Keyword::Errors
| Keyword::Import
| Keyword::Const
| Keyword::Static,
) => true,
_ => false,
}
}
pub(crate) fn test_decl(
p: &mut Parser,
leading: Vec<Trivia>,
start: usize,
) -> Result<TestDecl, ParseError> {
p.expect(&TokenKind::Kw(Keyword::Test), "expected `test`")?;
let description = match &p.peek().kind {
TokenKind::Str { raw, .. } => raw.clone(),
_ => {
return Err(ParseError::expected(
"expected the test's description, as a string literal",
p.peek_span(),
));
}
};
p.advance();
p.newline_with_trailing("expected a line break after the description")?;
let body = p.indented_block(Code::EmptyBody, "expected an indented test body")?;
Ok(TestDecl {
description,
body,
span: (start, p.prev_span().1),
leading,
})
}
pub(crate) fn type_alias(
p: &mut Parser,
leading: Vec<Trivia>,
start: usize,
visibility: Visibility,
) -> Result<TypeAlias, ParseError> {
p.expect(&TokenKind::Kw(Keyword::Type), "expected `type`")?;
let name = p.ident("expected a name after `type`")?;
p.expect(&TokenKind::Eq, "expected `=` after the alias's name")?;
let ty = capture_type(p, "expected a type", TypeStop::Greedy)?;
Ok(TypeAlias {
name,
ty,
visibility,
span: (start, p.prev_span().1),
leading,
})
}
pub(crate) fn visibility(p: &mut Parser) -> Result<Visibility, ParseError> {
if !p.check(&TokenKind::Kw(Keyword::Export)) {
return Ok(Visibility::Private);
}
p.advance();
if !p.check(&TokenKind::LParen) {
return Ok(Visibility::Public);
}
p.advance();
let scope = if p.check(&TokenKind::Kw(Keyword::In)) {
p.advance();
let mut path = String::new();
while !p.check(&TokenKind::RParen) && !p.at_eof() {
path.push_str(&token_text(&p.peek().kind));
p.advance();
}
if path.is_empty() {
return Err(ParseError::expected(
"expected a module path after `in`",
p.peek_span(),
));
}
Visibility::In(path)
} else {
let word = token_text(&p.peek().kind);
p.advance();
match word.as_str() {
"crate" => Visibility::Crate,
"super" => Visibility::Super,
_ => {
return Err(ParseError::expected(
"expected `crate`, `super` or `in` inside `export(…)`",
p.prev_span(),
));
}
}
};
p.expect(&TokenKind::RParen, "expected `)` to close `export(…)`")?;
Ok(scope)
}
pub(crate) fn walk_item_for_tails(item: &Item, problems: &mut Vec<ParseError>) {
match item {
Item::Fn(decl) => {
for clause in &decl.clauses {
if let Some(block) = clause.body.plain() {
problems.extend(return_in_tail(block));
}
if !clause.wheres.is_empty() {
problems.extend(return_in_tail(&Block {
stmts: clause.wheres.clone(),
span: clause.span,
}));
}
}
}
Item::Class(decl) => {
for member in &decl.members {
walk_member_for_tails(member, problems);
}
}
Item::Instance(decl) => {
for member in &decl.members {
walk_member_for_tails(member, problems);
}
}
Item::Test(decl) => problems.extend(return_in_tail(&decl.body)),
Item::Module(decl) => {
for inner in decl.items.iter().flatten() {
walk_item_for_tails(inner, problems);
}
}
Item::Data(_)
| Item::Type(_)
| Item::Errors(_)
| Item::Import(_)
| Item::Const(_)
| Item::InlineRust(_)
| Item::Extern(_)
| Item::PatternSynonym(_)
| Item::DeriveTemplate(_)
| Item::OperatorDecl(_) => (),
};
();
}