use brink_syntax_native::SyntaxKind as N;
use brink_syntax_native::ast::{self, AstNode as _};
use brink_syntax_native::{SyntaxNode, SyntaxToken};
use super::provenance::native_provenance;
use crate::hir::FileId;
use crate::hir::construct::{ConstructForm, ConstructTarget};
use crate::provenance::NodeClass;
use crate::{
Diagnostic, DiagnosticCode, Expr, FloatBits, InfixExpr, InfixOp, Name, Path, PrefixOp,
};
use crate::{StringExpr, StringPart};
pub(super) fn lower_expr(file_id: FileId, node: &SyntaxNode, diags: &mut Vec<Diagnostic>) -> Expr {
match node.kind() {
N::INTEGER_LIT => {
let lit = ast::IntegerLit::cast(node.clone()).and_then(|n| n.value());
if let Some(v) = lit {
#[expect(
clippy::cast_possible_truncation,
reason = "brink integers are 32-bit, mirrors ink's IntegerLit lowering"
)]
Expr::Int(v as i32)
} else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
Expr::Int(0)
}
}
N::FLOAT_LIT => {
let lit = ast::FloatLit::cast(node.clone()).and_then(|n| n.value());
if let Some(v) = lit {
Expr::Float(FloatBits::from_f64(v))
} else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
Expr::Float(FloatBits::from_f64(0.0))
}
}
N::BOOLEAN_LIT => {
let lit = ast::BooleanLit::cast(node.clone()).and_then(|n| n.value());
if let Some(v) = lit {
Expr::Bool(v)
} else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
Expr::Bool(false)
}
}
N::STRING_LIT => lower_string_lit(file_id, node, diags),
N::PATH_EXPR => {
let path = ast::PathExpr::cast(node.clone()).and_then(|n| n.path());
if let Some(p) = path {
Expr::Path(lower_path(&p))
} else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
Expr::Null
}
}
N::PAREN_EXPR => {
let inner = ast::ParenExpr::cast(node.clone()).and_then(|n| n.inner());
if let Some(inner_node) = inner {
lower_expr(file_id, &inner_node, diags)
} else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
Expr::Null
}
}
N::PREFIX_EXPR => lower_prefix(file_id, node, diags),
N::INFIX_EXPR => lower_infix(file_id, node, diags),
N::CALL_EXPR => lower_call(file_id, node, diags),
N::CONSTRUCT_LITERAL => lower_construct(file_id, node, diags),
N::LAMBDA_EXPR => super::lambda::lower_lambda(file_id, node, diags),
N::ARRAY_LITERAL => lower_array_literal(file_id, node, diags),
N::STMT_BLOCK => {
if let Some(sb) = ast::StmtBlock::cast(node.clone()) {
let _ = super::control_flow::lower_stmt_block(file_id, &sb, diags);
}
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E129));
Expr::Null
}
_ => {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E129));
Expr::Null
}
}
}
fn diag(file: FileId, range: rowan::TextRange, code: DiagnosticCode) -> Diagnostic {
Diagnostic {
file,
range,
message: code.title().to_string(),
code,
}
}
pub(super) fn lower_path(path: &ast::Path) -> Path {
let range = path.syntax().text_range();
let segments: Vec<Name> = path
.segments()
.map(|t| Name {
text: t.text().to_string(),
range: t.text_range(),
})
.collect();
Path {
segments,
range,
crosses_module_wall: path.crosses_module_wall(),
}
}
fn lower_prefix(file_id: FileId, node: &SyntaxNode, diags: &mut Vec<Diagnostic>) -> Expr {
let Some(prefix) = ast::PrefixExpr::cast(node.clone()) else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
return Expr::Null;
};
let op = prefix.op_token().as_ref().and_then(prefix_op);
let Some(operand) = prefix.operand() else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
return Expr::Null;
};
let inner = lower_expr(file_id, &operand, diags);
if let Some(op) = op {
Expr::Prefix(op, Box::new(inner))
} else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E016));
inner
}
}
fn prefix_op(tok: &SyntaxToken) -> Option<PrefixOp> {
match tok.kind() {
N::MINUS => Some(PrefixOp::Negate),
N::BANG => Some(PrefixOp::Not),
_ => None,
}
}
fn lower_infix(file_id: FileId, node: &SyntaxNode, diags: &mut Vec<Diagnostic>) -> Expr {
let Some(infix) = ast::InfixExpr::cast(node.clone()) else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
return Expr::Null;
};
let (Some(lhs_node), Some(rhs_node)) = (infix.lhs(), infix.rhs()) else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
return Expr::Null;
};
let lhs = lower_expr(file_id, &lhs_node, diags);
let rhs = lower_expr(file_id, &rhs_node, diags);
let op = if infix.is_double_pipe() {
Some(InfixOp::Or)
} else {
infix.op_token().as_ref().and_then(infix_op)
};
if let Some(op) = op {
let ptr = native_provenance(file_id, NodeClass::Infix, node);
Expr::Infix(InfixExpr::new(ptr, lhs, op, rhs))
} else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E016));
lhs
}
}
fn infix_op(tok: &SyntaxToken) -> Option<InfixOp> {
match tok.kind() {
N::PLUS => Some(InfixOp::Add),
N::MINUS => Some(InfixOp::Sub),
N::STAR => Some(InfixOp::Mul),
N::SLASH => Some(InfixOp::Div),
N::PERCENT => Some(InfixOp::Mod),
N::EQ_EQ => Some(InfixOp::Eq),
N::BANG_EQ => Some(InfixOp::NotEq),
N::LT => Some(InfixOp::Lt),
N::GT => Some(InfixOp::Gt),
N::LT_EQ => Some(InfixOp::LtEq),
N::GT_EQ => Some(InfixOp::GtEq),
N::AMP_AMP => Some(InfixOp::And),
N::KW_OR => Some(InfixOp::Coalesce),
_ => None,
}
}
fn lower_construct(file_id: FileId, node: &SyntaxNode, diags: &mut Vec<Diagnostic>) -> Expr {
let Some(lit) = ast::ConstructLiteral::cast(node.clone()) else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
return Expr::Null;
};
let Some(type_path) = lit.type_path() else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
return Expr::Null;
};
let path = lower_path(&type_path);
let segments: Vec<String> = path.segments.iter().map(|s| s.text.clone()).collect();
let entries: Vec<ast::ConstructEntry> = lit.entries().collect();
let target = ConstructTarget::lookup(&segments);
let expected = target.map_or(ConstructForm::Pair, ConstructTarget::form);
if !entries_match_form(&entries, expected) {
diags.push(form_mismatch(file_id, node, &segments, expected));
return Expr::Null;
}
match target {
Some(ConstructTarget::Map) => Expr::MapLiteral(crate::MapLiteral {
ptr: native_provenance(file_id, NodeClass::MapLiteral, node),
entries: entries
.iter()
.map(|e| {
let at = e.syntax().text_range();
(
lower_entry_part(file_id, e.key().as_ref(), at, diags),
lower_entry_part(file_id, e.value().as_ref(), at, diags),
)
})
.collect(),
}),
Some(ConstructTarget::Flags) => {
let mut items = Vec::with_capacity(entries.len());
for entry in &entries {
let member = entry
.value()
.and_then(ast::PathExpr::cast)
.and_then(|p| p.path());
let Some(p) = member else {
diags.push(diag(
file_id,
entry.syntax().text_range(),
DiagnosticCode::E139,
));
return Expr::Null;
};
items.push(lower_path(&p));
}
Expr::ListLiteral(items)
}
Some(ConstructTarget::Weighted) => {
let mut args = Vec::with_capacity(entries.len() * 2);
for entry in &entries {
let at = entry.syntax().text_range();
args.push(lower_entry_part(file_id, entry.key().as_ref(), at, diags));
args.push(lower_entry_part(file_id, entry.value().as_ref(), at, diags));
}
Expr::Call(
Path {
segments: vec![Name {
text: "weighted".to_string(),
range: type_path.syntax().text_range(),
}],
range: type_path.syntax().text_range(),
crosses_module_wall: false,
},
args,
)
}
None => {
let mut fields = Vec::with_capacity(entries.len());
for entry in &entries {
let Some(name) = entry.key().and_then(|k| bare_field_name(&k)) else {
diags.push(diag(
file_id,
entry.syntax().text_range(),
DiagnosticCode::E139,
));
return Expr::Null;
};
let at = entry.syntax().text_range();
fields.push((
name,
lower_entry_part(file_id, entry.value().as_ref(), at, diags),
));
}
Expr::StructLiteral(crate::StructLiteral {
ptr: native_provenance(file_id, NodeClass::StructLiteral, node),
shape: Name {
text: segments.last().cloned().unwrap_or_default(),
range: type_path.syntax().text_range(),
},
fields,
})
}
}
}
fn entries_match_form(entries: &[ast::ConstructEntry], expected: ConstructForm) -> bool {
entries
.iter()
.all(|e| e.is_pair() == (expected == ConstructForm::Pair))
}
fn form_mismatch(
file: FileId,
node: &SyntaxNode,
segments: &[String],
expected: ConstructForm,
) -> Diagnostic {
let name = segments.last().map_or("<unnamed>", String::as_str);
Diagnostic {
file,
range: node.text_range(),
message: format!(
"`{name} {{ … }}` constructs from {} entries",
expected.label()
),
code: DiagnosticCode::E139,
}
}
fn lower_entry_part(
file_id: FileId,
part: Option<&SyntaxNode>,
fallback: rowan::TextRange,
diags: &mut Vec<Diagnostic>,
) -> Expr {
let Some(n) = part else {
diags.push(diag(file_id, fallback, DiagnosticCode::E015));
return Expr::Null;
};
lower_expr(file_id, n, diags)
}
fn bare_field_name(key: &SyntaxNode) -> Option<Name> {
let path = ast::PathExpr::cast(key.clone())?.path()?;
let lowered = lower_path(&path);
match lowered.segments.as_slice() {
[only] => Some(only.clone()),
_ => None,
}
}
fn lower_array_literal(file_id: FileId, node: &SyntaxNode, diags: &mut Vec<Diagnostic>) -> Expr {
let Some(lit) = ast::ArrayLiteral::cast(node.clone()) else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
return Expr::Null;
};
let elements = lit
.elements()
.map(|el| lower_expr(file_id, &el, diags))
.collect();
Expr::ArrayLiteral(crate::ArrayLiteral {
ptr: native_provenance(file_id, NodeClass::ArrayLiteral, node),
elements,
})
}
fn lower_call(file_id: FileId, node: &SyntaxNode, diags: &mut Vec<Diagnostic>) -> Expr {
let Some(call) = ast::CallExpr::cast(node.clone()) else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E017));
return Expr::Null;
};
let Some(callee) = call.callee() else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E017));
return Expr::Null;
};
let path = lower_path(&callee);
let args: Vec<Expr> = call
.arg_list()
.into_iter()
.flat_map(|al| al.syntax().children().collect::<Vec<_>>())
.map(|arg_node| lower_expr(file_id, &arg_node, diags))
.collect();
Expr::Call(path, args)
}
fn lower_string_lit(file_id: FileId, node: &SyntaxNode, diags: &mut Vec<Diagnostic>) -> Expr {
let mut parts: Vec<StringPart> = Vec::new();
let mut literal = String::new();
for el in node.children_with_tokens() {
match el {
rowan::NodeOrToken::Token(t) => match t.kind() {
N::STRING_TEXT => literal.push_str(t.text()),
N::STRING_ESCAPE => literal.push_str(unescape_string_token(t.text())),
_ => {}
},
rowan::NodeOrToken::Node(n) if n.kind() == N::INTERPOLATION => {
if !literal.is_empty() {
parts.push(StringPart::Literal(std::mem::take(&mut literal)));
}
if let Some(inner) = n.children().next() {
let inner_expr = lower_expr(file_id, &inner, diags);
parts.push(StringPart::Interpolation(Box::new(inner_expr)));
} else {
diags.push(diag(file_id, n.text_range(), DiagnosticCode::E015));
}
}
rowan::NodeOrToken::Node(_) => {}
}
}
if !literal.is_empty() || parts.is_empty() {
parts.push(StringPart::Literal(literal));
}
Expr::String(StringExpr { parts })
}
pub(super) fn unescape_string_token(raw: &str) -> &'static str {
match raw {
"\\n" => "\n",
"\\t" => "\t",
"\\\\" => "\\",
"\\\"" => "\"",
_ => "",
}
}