use brink_syntax_native::SyntaxKind as N;
use brink_syntax_native::ast::{self, AstNode as _};
use brink_syntax_native::{SyntaxNode, SyntaxToken};
use crate::VisibilityMark;
use crate::hir::FileId;
use crate::hir::doc_block::DocPolicy;
use crate::provenance::NodeClass;
use crate::{
ConstDecl, Diagnostic, DiagnosticCode, ExternalDecl, ListDecl, ListMember, Name, ParamInfo,
StructDecl, StructFieldDecl, TypeExpr, VarDecl,
};
fn visibility_mark(is_pub: bool) -> Option<VisibilityMark> {
is_pub.then_some(VisibilityMark::Public)
}
use super::doc_comment::lower_doc_comment;
use super::expr::{lower_expr, lower_path};
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<SyntaxToken>) -> Option<Name> {
tok.map(|t| Name {
text: t.text().to_string(),
range: t.text_range(),
})
}
fn binding_annotation(annotation: Option<&ast::TypeAnnotation>) -> Option<TypeExpr> {
annotation.and_then(super::types::lower_type_annotation)
}
pub(super) fn lower_var_decl(
file_id: FileId,
node: &ast::VarDecl,
diags: &mut Vec<Diagnostic>,
) -> Option<VarDecl> {
let range = node.syntax().text_range();
let Some(name) = name_from(node.name_token()) else {
diags.push(diag(file_id, range, DiagnosticCode::E004));
return None;
};
let Some(value_node) = node.value() else {
diags.push(diag(file_id, range, DiagnosticCode::E005));
return None;
};
let value = lower_expr(file_id, &value_node, diags);
let doc = lower_doc_comment(file_id, node.doc(), DocPolicy::VALUE, diags);
Some(VarDecl {
ptr: native_provenance(file_id, NodeClass::VarDecl, node.syntax()),
name,
value,
is_local: false,
annotation: binding_annotation(node.type_annotation().as_ref()),
doc,
visibility: visibility_mark(node.is_pub()),
was: None,
})
}
pub(super) fn lower_const_decl(
file_id: FileId,
node: &ast::ConstDecl,
diags: &mut Vec<Diagnostic>,
) -> Option<ConstDecl> {
let range = node.syntax().text_range();
let Some(name) = name_from(node.name_token()) else {
diags.push(diag(file_id, range, DiagnosticCode::E006));
return None;
};
let Some(value_node) = node.value() else {
diags.push(diag(file_id, range, DiagnosticCode::E007));
return None;
};
let value = lower_expr(file_id, &value_node, diags);
let doc = lower_doc_comment(file_id, node.doc(), DocPolicy::VALUE, diags);
Some(ConstDecl {
ptr: native_provenance(file_id, NodeClass::ConstDecl, node.syntax()),
name,
value,
annotation: binding_annotation(node.type_annotation().as_ref()),
doc,
visibility: visibility_mark(node.is_pub()),
was: None,
})
}
pub(super) fn lower_flags_decl(
file_id: FileId,
node: &ast::FlagsDecl,
diags: &mut Vec<Diagnostic>,
) -> Option<ListDecl> {
let range = node.syntax().text_range();
let Some(name) = name_from(node.name_token()) else {
diags.push(diag(file_id, range, DiagnosticCode::E008));
return None;
};
let members: Vec<ListMember> = node
.member_list()
.into_iter()
.flat_map(|ml| ml.members().collect::<Vec<_>>())
.filter_map(|m| {
let member_name = name_from(m.name_token());
if member_name.is_none() {
diags.push(diag(file_id, m.syntax().text_range(), DiagnosticCode::E009));
}
member_name.map(|n| ListMember {
name: n,
value: None,
is_active: m.is_active(),
})
})
.collect();
let doc = lower_doc_comment(file_id, node.doc(), DocPolicy::VALUE, diags);
Some(ListDecl {
ptr: native_provenance(file_id, NodeClass::ListDecl, node.syntax()),
name,
members,
doc,
visibility: visibility_mark(node.is_pub()),
was: None,
})
}
pub(super) fn lower_struct_decl(
file_id: FileId,
node: &ast::StructDecl,
diags: &mut Vec<Diagnostic>,
) -> Option<StructDecl> {
let range = node.syntax().text_range();
let Some(name) = name_from(node.name_token()) else {
diags.push(diag(file_id, range, DiagnosticCode::E004));
return None;
};
let fields: Vec<StructFieldDecl> = node
.fields()
.filter_map(|f| {
let field_name = name_from(f.name_token());
let ty = binding_annotation(f.type_annotation().as_ref());
if let (Some(n), Some(ty)) = (field_name, ty) {
Some(StructFieldDecl { name: n, ty })
} else {
diags.push(diag(file_id, f.syntax().text_range(), DiagnosticCode::E003));
None
}
})
.collect();
let doc = lower_doc_comment(file_id, node.doc(), DocPolicy::VALUE, diags);
Some(StructDecl {
ptr: native_provenance(file_id, NodeClass::StructDecl, node.syntax()),
name,
fields,
doc,
visibility: visibility_mark(node.is_pub()),
})
}
pub(super) fn lower_extern_decl(
file_id: FileId,
node: &ast::ExternDecl,
diags: &mut Vec<Diagnostic>,
) -> Option<ExternalDecl> {
let range = node.syntax().text_range();
let Some(name) = name_from(node.name_token()) else {
diags.push(diag(file_id, range, DiagnosticCode::E010));
return None;
};
let params: Vec<ParamInfo> = node
.param_list()
.into_iter()
.flat_map(|pl| pl.params().collect::<Vec<_>>())
.filter_map(|p| {
p.name_token().map(|t| ParamInfo {
name: t.text().to_string(),
is_ref: false,
is_divert: false,
})
})
.collect();
#[expect(
clippy::cast_possible_truncation,
reason = "external params won't exceed 255, mirrors the ink lowering's own cast"
)]
let param_count = params.len() as u8;
let doc = lower_doc_comment(file_id, node.doc(), DocPolicy::EXTERNAL, diags);
Some(ExternalDecl {
ptr: native_provenance(file_id, NodeClass::ExternalDecl, node.syntax()),
name,
param_count,
params,
doc,
visibility: visibility_mark(node.is_pub()),
was: None,
})
}
pub(super) fn joined_path_text(path: &ast::Path) -> String {
lower_path(path)
.segments
.iter()
.map(|n| n.text.as_str())
.collect::<Vec<_>>()
.join("::")
}
pub(super) fn in_flattened_scope(node: &SyntaxNode) -> bool {
let Some(parent) = node.parent() else {
return true;
};
if parent.kind() == N::SOURCE_FILE {
return true;
}
if parent.kind() == N::BLOCK
&& let Some(grandparent) = parent.parent()
&& grandparent.kind() == N::MODULE_DECL
{
return in_flattened_scope(&grandparent);
}
false
}