use brink_syntax_native::SyntaxKind as N;
use brink_syntax_native::SyntaxNode;
use brink_syntax_native::ast::{self, AstNode as _};
use crate::hir::FileId;
use crate::provenance::NodeClass;
use crate::{
AssignOp, Assignment, AwaitStmt, BlockStmt, Diagnostic, DiagnosticCode, ElseBranch, ForStmt,
IfStmt, Name, Return, ReturnKind, TempDecl, WhileStmt,
};
use super::expr::lower_expr;
use super::provenance::native_provenance;
fn diag(file: FileId, range: rowan::TextRange, code: DiagnosticCode) -> Diagnostic {
Diagnostic {
file,
range,
message: code.title().to_string(),
code,
}
}
fn name_from(tok: Option<brink_syntax_native::SyntaxToken>) -> Option<Name> {
tok.map(|t| Name {
text: t.text().to_string(),
range: t.text_range(),
})
}
pub(super) fn lower_as_binding(
file_id: FileId,
binding: Option<&ast::AsBinding>,
condition: &crate::Expr,
diags: &mut Vec<Diagnostic>,
) -> Option<Name> {
let binding = binding?;
if let crate::Expr::Infix(ie) = condition
&& matches!(ie.op, crate::InfixOp::And | crate::InfixOp::Or)
{
diags.push(diag(
file_id,
binding.syntax().text_range(),
DiagnosticCode::E145,
));
return None;
}
name_from(binding.name_token())
}
pub fn lower_stmt_block(
file_id: FileId,
block: &ast::StmtBlock,
diags: &mut Vec<Diagnostic>,
) -> Vec<BlockStmt> {
block
.items()
.filter_map(|item| lower_block_item(file_id, &item, diags))
.collect()
}
pub(super) fn lower_stmt_block_stmts(
file_id: FileId,
block: &ast::StmtBlock,
diags: &mut Vec<Diagnostic>,
) -> Vec<BlockStmt> {
let tail = block.tail();
block
.items()
.filter(|item| Some(item) != tail.as_ref())
.filter_map(|item| lower_block_item(file_id, &item, diags))
.collect()
}
pub(super) fn lower_block_item(
file_id: FileId,
item: &SyntaxNode,
diags: &mut Vec<Diagnostic>,
) -> Option<BlockStmt> {
match item.kind() {
N::LET_STMT => ast::LetStmt::cast(item.clone())
.and_then(|n| lower_temp_decl(file_id, &n, diags))
.map(BlockStmt::TempDecl),
N::ASSIGN_STMT => ast::AssignStmt::cast(item.clone())
.and_then(|n| lower_assignment(file_id, &n, diags))
.map(BlockStmt::Assignment),
N::EXPR_STMT => {
ast::ExprStmt::cast(item.clone()).and_then(|n| lower_expr_stmt(file_id, &n, diags))
}
N::IF_STMT => ast::IfStmt::cast(item.clone())
.and_then(|n| lower_if_stmt(file_id, &n, diags).map(BlockStmt::If)),
N::WHILE_STMT => ast::WhileStmt::cast(item.clone())
.and_then(|n| lower_while_stmt(file_id, &n, diags).map(BlockStmt::While)),
N::FOR_STMT => ast::ForStmt::cast(item.clone())
.and_then(|n| lower_for_stmt(file_id, &n, diags).map(BlockStmt::For)),
N::UNTIL_STMT => ast::UntilStmt::cast(item.clone())
.map(|n| BlockStmt::Await(lower_until_stmt(file_id, &n, diags))),
N::RETURN_STMT => ast::ReturnStmt::cast(item.clone())
.map(|n| BlockStmt::Return(lower_return_stmt(file_id, &n, diags))),
N::BREAK_STMT => ast::BreakStmt::cast(item.clone())
.map(|n| BlockStmt::Break(native_provenance(file_id, NodeClass::Break, n.syntax()))),
N::CONTINUE_STMT => ast::ContinueStmt::cast(item.clone()).map(|n| {
BlockStmt::Continue(native_provenance(file_id, NodeClass::Continue, n.syntax()))
}),
_ => {
diags.push(diag(file_id, item.text_range(), DiagnosticCode::E129));
None
}
}
}
pub(super) fn lower_temp_decl(
file_id: FileId,
temp: &ast::LetStmt,
diags: &mut Vec<Diagnostic>,
) -> Option<TempDecl> {
let range = temp.syntax().text_range();
let Some(name) = name_from(temp.name_token()) else {
diags.push(diag(file_id, range, DiagnosticCode::E014));
return None;
};
let value = temp.value().map(|v| lower_expr(file_id, &v, diags));
Some(TempDecl {
ptr: native_provenance(file_id, NodeClass::TempDecl, temp.syntax()),
name,
value,
annotation: temp
.type_annotation()
.as_ref()
.and_then(super::types::lower_type_annotation),
synthetic: false,
})
}
pub(super) fn lower_assignment(
file_id: FileId,
assign: &ast::AssignStmt,
diags: &mut Vec<Diagnostic>,
) -> Option<Assignment> {
let range = assign.syntax().text_range();
let Some(place) = assign.place() else {
diags.push(diag(file_id, range, DiagnosticCode::E014));
return None;
};
let Some(value_node) = assign.value() else {
diags.push(diag(file_id, range, DiagnosticCode::E014));
return None;
};
let target = crate::Expr::Path(super::expr::lower_path(&place));
let value = lower_expr(file_id, &value_node, diags);
let op = assign
.op_token()
.map_or(AssignOp::Set, |tok| match tok.kind() {
N::PLUS_EQ => AssignOp::Add,
N::MINUS_EQ => AssignOp::Sub,
_ => AssignOp::Set,
});
Some(Assignment {
ptr: native_provenance(file_id, NodeClass::Assignment, assign.syntax()),
target,
op,
value,
})
}
fn lower_expr_stmt(
file_id: FileId,
stmt: &ast::ExprStmt,
diags: &mut Vec<Diagnostic>,
) -> Option<BlockStmt> {
let range = stmt.syntax().text_range();
let Some(expr_node) = stmt.expr() else {
diags.push(diag(file_id, range, DiagnosticCode::E015));
return None;
};
Some(BlockStmt::ExprStmt(lower_expr(file_id, &expr_node, diags)))
}
fn lower_if_stmt(
file_id: FileId,
if_stmt: &ast::IfStmt,
diags: &mut Vec<Diagnostic>,
) -> Option<IfStmt> {
let range = if_stmt.syntax().text_range();
let Some(cond_node) = if_stmt.condition() else {
diags.push(diag(file_id, range, DiagnosticCode::E015));
return None;
};
let condition = lower_expr(file_id, &cond_node, diags);
let binding = lower_as_binding(file_id, if_stmt.as_binding().as_ref(), &condition, diags);
let body = if_stmt
.body()
.map(|b| lower_stmt_block(file_id, &b, diags))
.unwrap_or_default();
let else_branch = match if_stmt.else_clause() {
None => None,
Some(clause) => Some(lower_else_clause(file_id, &clause, diags)?),
};
Some(IfStmt {
ptr: native_provenance(file_id, NodeClass::If, if_stmt.syntax()),
condition,
binding,
body,
else_branch,
})
}
fn lower_else_clause(
file_id: FileId,
clause: &ast::ElseClause,
diags: &mut Vec<Diagnostic>,
) -> Option<ElseBranch> {
if let Some(nested_if) = clause.if_stmt() {
Some(ElseBranch::ElseIf(Box::new(lower_if_stmt(
file_id, &nested_if, diags,
)?)))
} else {
let body = clause
.body()
.map(|b| lower_stmt_block(file_id, &b, diags))
.unwrap_or_default();
Some(ElseBranch::Else(body))
}
}
fn lower_while_stmt(
file_id: FileId,
w: &ast::WhileStmt,
diags: &mut Vec<Diagnostic>,
) -> Option<WhileStmt> {
let range = w.syntax().text_range();
let Some(cond_node) = w.condition() else {
diags.push(diag(file_id, range, DiagnosticCode::E015));
return None;
};
let condition = lower_expr(file_id, &cond_node, diags);
let binding = lower_as_binding(file_id, w.as_binding().as_ref(), &condition, diags);
let body = w
.body()
.map(|b| lower_stmt_block(file_id, &b, diags))
.unwrap_or_default();
Some(WhileStmt {
ptr: native_provenance(file_id, NodeClass::While, w.syntax()),
condition,
binding,
body,
is_await: false,
})
}
#[expect(
clippy::similar_names,
reason = "var_name/val_name are the ForStmt field names (k/v's HIR spelling, B2 #1461) — \
not a pair a rename would clarify"
)]
fn lower_for_stmt(
file_id: FileId,
f: &ast::ForStmt,
diags: &mut Vec<Diagnostic>,
) -> Option<ForStmt> {
let range = f.syntax().text_range();
let Some(var_name) = name_from(f.name_token()) else {
diags.push(diag(file_id, range, DiagnosticCode::E014));
return None;
};
let val_name = name_from(f.val_name_token());
let Some(iterable_node) = f.iterable() else {
diags.push(diag(file_id, range, DiagnosticCode::E015));
return None;
};
let iterable = lower_expr(file_id, &iterable_node, diags);
let body = f
.body()
.map(|b| lower_stmt_block(file_id, &b, diags))
.unwrap_or_default();
Some(ForStmt {
ptr: native_provenance(file_id, NodeClass::For, f.syntax()),
var_name,
val_name,
iterable,
body,
})
}
pub(super) fn lower_until_stmt(
file_id: FileId,
u: &ast::UntilStmt,
diags: &mut Vec<Diagnostic>,
) -> AwaitStmt {
let condition = u.condition().map(|n| lower_expr(file_id, &n, diags));
AwaitStmt {
ptr: native_provenance(file_id, NodeClass::Await, u.syntax()),
condition,
}
}
fn lower_return_stmt(
file_id: FileId,
ret: &ast::ReturnStmt,
diags: &mut Vec<Diagnostic>,
) -> Return {
let value = ret.value().map(|n| lower_expr(file_id, &n, diags));
Return {
ptr: Some(native_provenance(file_id, NodeClass::Return, ret.syntax())),
kind: ReturnKind::Explicit,
value,
onwards_args: Vec::new(),
}
}