use brink_syntax_native::SyntaxKind as N;
use brink_syntax_native::SyntaxNode;
use brink_syntax_native::ast::{self, AstNode as _};
use crate::Provenance;
use crate::hir::FileId;
use crate::provenance::NodeClass;
use crate::{
Assignment, Block, BlockStmt, Content, ContentPart, Diagnostic, DiagnosticCode, Divert,
DivertPath, DivertTarget, ElseBranch, Expr, IfStmt, LogicBlock, LogicBlockScope, Name, Return,
ReturnKind, SpanAttr, SpanPart, Stmt, StringPart, Tag, TempDecl, TunnelCall,
};
use super::choice::lower_choice_point;
use super::cond::{lower_alternation, lower_conditional};
use super::element::Elements;
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_block(
file_id: FileId,
block: &ast::Block,
elements: &mut Elements,
diags: &mut Vec<Diagnostic>,
) -> Block {
let items: Vec<SyntaxNode> = block.items().collect();
let stmts = lower_items(file_id, &items, 0, elements, diags);
let tail = crate::tail_from_stmts(&stmts);
Block {
label: None,
stmts,
container_id: None,
tail,
}
}
pub(super) fn lower_stmt_block_as_body(
file_id: FileId,
block: &ast::StmtBlock,
elements: &mut Elements,
diags: &mut Vec<Diagnostic>,
) -> Block {
let items: Vec<SyntaxNode> = block.items().collect();
let mut stmts = lower_code_ground_items(file_id, &items, elements, diags);
if let [Stmt::LogicBlock(lb)] | [Stmt::LogicBlock(lb), Stmt::EndOfLine] = stmts.as_mut_slice() {
lb.ptr = native_provenance(file_id, NodeClass::LogicBlock, block.syntax());
}
let tail = crate::tail_from_stmts(&stmts);
Block {
label: None,
stmts,
container_id: None,
tail,
}
}
fn lower_code_ground_items(
file_id: FileId,
items: &[SyntaxNode],
elements: &mut Elements,
diags: &mut Vec<Diagnostic>,
) -> Vec<Stmt> {
let mut stmts = Vec::new();
let mut run: Vec<&SyntaxNode> = Vec::new();
let mut run_start: Option<SyntaxNode> = None;
for item in items {
if item.kind() == N::PROSE_LINE {
flush_code_ground_run(file_id, &mut run, &mut run_start, diags, &mut stmts);
if let Some(pl) = ast::ProseLine::cast(item.clone()) {
if let Some(cl) = pl.content_line() {
if let Some(label) = cl.label() {
diags.push(diag(
file_id,
label.syntax().text_range(),
DiagnosticCode::E129,
));
}
stmts.extend(lower_content_line_body(file_id, &cl, elements, diags));
} else {
diags.push(diag(file_id, item.text_range(), DiagnosticCode::E129));
}
} else {
diags.push(diag(file_id, item.text_range(), DiagnosticCode::E129));
}
continue;
}
if run.is_empty() {
run_start = Some(item.clone());
}
run.push(item);
}
flush_code_ground_run(file_id, &mut run, &mut run_start, diags, &mut stmts);
mark_split_logic_block_scopes(&mut stmts);
stmts
}
fn mark_split_logic_block_scopes(stmts: &mut [Stmt]) {
let count = stmts
.iter()
.filter(|s| matches!(s, Stmt::LogicBlock(_)))
.count();
if count < 2 {
return;
}
let mut seen = 0usize;
for stmt in stmts.iter_mut() {
if let Stmt::LogicBlock(lb) = stmt {
lb.scope = if seen == 0 {
crate::LogicBlockScope::Opens
} else {
crate::LogicBlockScope::Continues
};
seen += 1;
}
}
}
fn flush_code_ground_run(
file_id: FileId,
run: &mut Vec<&SyntaxNode>,
run_start: &mut Option<SyntaxNode>,
diags: &mut Vec<Diagnostic>,
stmts: &mut Vec<Stmt>,
) {
if run.is_empty() {
return;
}
let block_stmts: Vec<_> = run
.drain(..)
.filter_map(|item| super::control_flow::lower_block_item(file_id, item, diags))
.collect();
if !block_stmts.is_empty()
&& let Some(anchor) = run_start.take()
{
let needs_eol = block_stmts_contain_call(&block_stmts);
stmts.push(Stmt::LogicBlock(LogicBlock {
ptr: native_provenance(file_id, NodeClass::LogicBlock, &anchor),
stmts: block_stmts,
scope: crate::LogicBlockScope::Standalone,
}));
if needs_eol {
stmts.push(Stmt::EndOfLine);
}
}
*run_start = None;
}
pub(super) fn lower_items(
file_id: FileId,
items: &[SyntaxNode],
start: usize,
elements: &mut Elements,
diags: &mut Vec<Diagnostic>,
) -> Vec<Stmt> {
let mut stmts = Vec::new();
let mut i = start;
while i < items.len() {
let node = &items[i];
if node.kind() == N::CONTENT_LINE
&& let Some(cl) = ast::ContentLine::cast(node.clone())
&& let Some(label) = cl.label().and_then(|l| name_from(l.name_token()))
{
let mut inner = lower_content_line_body(file_id, &cl, elements, diags);
inner.extend(lower_items(file_id, items, i + 1, elements, diags));
let inner_tail = crate::tail_from_stmts(&inner);
stmts.push(Stmt::LabeledBlock(Box::new(Block {
label: Some(label),
stmts: inner,
container_id: None,
tail: inner_tail,
})));
return stmts;
}
if node.kind() == N::CHOICE_POINT {
if let Some(cp) = ast::ChoicePoint::cast(node.clone()) {
let continuation = lower_continuation(file_id, items, i + 1, elements, diags);
stmts.extend(lower_choice_point(
file_id,
&cp,
continuation,
elements,
diags,
));
}
return stmts;
}
let (item_stmts, consumed) =
lower_one_item(file_id, node, &items[i + 1..], elements, diags);
stmts.extend(item_stmts);
i += 1 + consumed;
}
stmts
}
fn lower_continuation(
file_id: FileId,
items: &[SyntaxNode],
start: usize,
elements: &mut Elements,
diags: &mut Vec<Diagnostic>,
) -> Block {
if let Some(node) = items.get(start)
&& node.kind() == N::CONTENT_LINE
&& let Some(cl) = ast::ContentLine::cast(node.clone())
&& let Some(label) = cl.label().and_then(|l| name_from(l.name_token()))
{
let mut stmts = lower_content_line_body(file_id, &cl, elements, diags);
stmts.extend(lower_items(file_id, items, start + 1, elements, diags));
let tail = crate::tail_from_stmts(&stmts);
return Block {
label: Some(label),
stmts,
container_id: None,
tail,
};
}
let stmts = lower_items(file_id, items, start, elements, diags);
let tail = crate::tail_from_stmts(&stmts);
Block {
label: None,
stmts,
container_id: None,
tail,
}
}
#[expect(
clippy::too_many_lines,
reason = "one match arm per body-item node kind; splitting would obscure the dispatch"
)]
fn lower_one_item(
file_id: FileId,
node: &SyntaxNode,
following: &[SyntaxNode],
elements: &mut Elements,
diags: &mut Vec<Diagnostic>,
) -> (Vec<Stmt>, usize) {
if let Some((claimed, consumed)) =
super::element::try_claim(file_id, node, elements, following, diags)
{
return (claimed, consumed);
}
if let Some((dispatched, consumed)) =
super::element::try_dispatch(file_id, node, elements, following, diags)
{
return (dispatched, consumed);
}
let stmts = match node.kind() {
N::CONTENT_LINE => {
let Some(cl) = ast::ContentLine::cast(node.clone()) else {
return (Vec::new(), 0);
};
lower_content_line_body(file_id, &cl, elements, diags)
}
N::SCENE_STITCH => {
let heading = node.children().find(|n| n.kind() == N::SCENE_HEADING);
let body_items: Vec<SyntaxNode> = node
.children()
.find(|n| n.kind() == N::SCENE_BODY)
.map(|b| b.children().collect())
.unwrap_or_default();
let Some((claimed, consumed)) = heading
.as_ref()
.and_then(|h| super::element::try_claim(file_id, h, elements, &body_items, diags))
else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E129));
return (Vec::new(), 0);
};
let mut stmts = claimed;
stmts.extend(lower_items(file_id, &body_items, consumed, elements, diags));
stmts
}
N::TAG_LINE => {
let Some(tl) = ast::TagLine::cast(node.clone()) else {
return (Vec::new(), 0);
};
let tags: Vec<Tag> = tl
.tags()
.map(|t| lower_tag(file_id, &t, diags))
.collect();
if tags.is_empty() {
Vec::new()
} else {
vec![
Stmt::Content(Content {
ptr: None,
parts: Vec::new(),
tags,
}),
Stmt::EndOfLine,
]
}
}
N::DIVERT_STMT | N::TUNNEL_CALL => lower_divert_like(file_id, node, diags)
.into_iter()
.collect(),
N::LOGIC_LINE => {
let Some(ll) = ast::LogicLine::cast(node.clone()) else {
return (Vec::new(), 0);
};
lower_logic_line(file_id, &ll, diags)
}
N::RETURN_STMT => vec![Stmt::Return(Return {
ptr: Some(native_provenance(file_id, NodeClass::Return, node)),
kind: ReturnKind::Explicit,
value: lower_return_value(file_id, node, diags),
onwards_args: Vec::new(),
})],
N::RETURN_REDIRECT => lower_return_redirect(file_id, node, diags),
N::CONDITIONAL_BLOCK => {
let Some(cb) = ast::ConditionalBlock::cast(node.clone()) else {
return (Vec::new(), 0);
};
vec![Stmt::Conditional(lower_conditional(file_id, &cb, elements, diags))]
}
N::ALTERNATION_BLOCK => {
let Some(ab) = ast::AlternationBlock::cast(node.clone()) else {
return (Vec::new(), 0);
};
vec![Stmt::Sequence(lower_alternation(file_id, &ab, elements, diags, true))]
}
N::FLOW_DECL
| N::FN_DECL
| N::VAR_DECL
| N::CONST_DECL
| N::FLAGS_DECL
| N::STRUCT_DECL
| N::EXTERN_DECL
| N::USE_DECL
| N::IMPORT_DECL
| N::MODULE_DECL
| N::ERROR
| N::DOC_COMMENT => Vec::new(),
N::ANNOTATION_LINE => {
super::annotation::handle_line(file_id, node, diags);
Vec::new()
}
_ => {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E129));
Vec::new()
}
};
(stmts, 0)
}
fn lower_logic_line(
file_id: FileId,
ll: &ast::LogicLine,
diags: &mut Vec<Diagnostic>,
) -> Vec<Stmt> {
let range = ll.syntax().text_range();
if let Some(block) = ll.stmt_block() {
let stmts = super::control_flow::lower_stmt_block(file_id, &block, diags);
let needs_eol = block_stmts_contain_call(&stmts);
let mut out = vec![Stmt::LogicBlock(LogicBlock {
ptr: native_provenance(file_id, NodeClass::LogicBlock, block.syntax()),
stmts,
scope: LogicBlockScope::Standalone,
})];
if needs_eol {
out.push(Stmt::EndOfLine);
}
return out;
}
if let Some(until_stmt) = ll.until_stmt() {
return vec![Stmt::Await(super::control_flow::lower_until_stmt(
file_id,
&until_stmt,
diags,
))];
}
if let Some(let_stmt) = ll.let_stmt() {
return lower_logic_line_temp_decl(file_id, &let_stmt, diags).map_or_else(Vec::new, |td| {
let needs_eol = td.value.as_ref().is_some_and(expr_contains_call);
let mut out = vec![Stmt::TempDecl(td)];
if needs_eol {
out.push(Stmt::EndOfLine);
}
out
});
}
if let Some(assign) = ll.assign_stmt() {
return lower_logic_line_assignment(file_id, &assign, diags).map_or_else(Vec::new, |a| {
let needs_eol = expr_contains_call(&a.value);
let mut out = vec![Stmt::Assignment(a)];
if needs_eol {
out.push(Stmt::EndOfLine);
}
out
});
}
if let Some(expr_stmt) = ll.expr_stmt() {
return lower_logic_line_expr_stmt(file_id, &expr_stmt, diags);
}
diags.push(diag(file_id, range, DiagnosticCode::E129));
Vec::new()
}
fn lower_logic_line_temp_decl(
file_id: FileId,
temp: &ast::LetStmt,
diags: &mut Vec<Diagnostic>,
) -> Option<TempDecl> {
super::control_flow::lower_temp_decl(file_id, temp, diags)
}
fn lower_logic_line_assignment(
file_id: FileId,
assign: &ast::AssignStmt,
diags: &mut Vec<Diagnostic>,
) -> Option<Assignment> {
super::control_flow::lower_assignment(file_id, assign, diags)
}
fn lower_logic_line_expr_stmt(
file_id: FileId,
stmt: &ast::ExprStmt,
diags: &mut Vec<Diagnostic>,
) -> Vec<Stmt> {
let range = stmt.syntax().text_range();
let Some(expr_node) = stmt.expr() else {
diags.push(diag(file_id, range, DiagnosticCode::E015));
return Vec::new();
};
let expr = lower_expr(file_id, &expr_node, diags);
let needs_eol = expr_contains_call(&expr);
let mut out = vec![Stmt::ExprStmt(expr)];
if needs_eol {
out.push(Stmt::EndOfLine);
}
out
}
fn expr_contains_call(expr: &Expr) -> bool {
match expr {
Expr::Call(..) => true,
Expr::Prefix(_, inner) | Expr::Postfix(inner, _) => expr_contains_call(inner),
Expr::Infix(ie) => expr_contains_call(&ie.lhs) || expr_contains_call(&ie.rhs),
Expr::String(s) => s
.parts
.iter()
.any(|p| matches!(p, StringPart::Interpolation(e) if expr_contains_call(e))),
_ => false,
}
}
fn block_stmts_contain_call(stmts: &[BlockStmt]) -> bool {
stmts.iter().any(|s| match s {
BlockStmt::TempDecl(td) => td.value.as_ref().is_some_and(expr_contains_call),
BlockStmt::Assignment(a) => expr_contains_call(&a.value),
BlockStmt::Return(r) => r.value.as_ref().is_some_and(expr_contains_call),
BlockStmt::ExprStmt(e) => expr_contains_call(e),
BlockStmt::Await(a) => a.condition.as_ref().is_some_and(expr_contains_call),
BlockStmt::If(i) => if_stmt_contains_call(i),
BlockStmt::While(w) => {
expr_contains_call(&w.condition) || block_stmts_contain_call(&w.body)
}
BlockStmt::For(f) => expr_contains_call(&f.iterable) || block_stmts_contain_call(&f.body),
BlockStmt::Break(_) | BlockStmt::Continue(_) => false,
})
}
fn if_stmt_contains_call(i: &IfStmt) -> bool {
expr_contains_call(&i.condition)
|| block_stmts_contain_call(&i.body)
|| match &i.else_branch {
Some(ElseBranch::ElseIf(inner)) => if_stmt_contains_call(inner),
Some(ElseBranch::Else(stmts)) => block_stmts_contain_call(stmts),
None => false,
}
}
pub(super) fn fixup_return_kind(is_function: bool, block: &mut Block) {
for stmt in &mut block.stmts {
match stmt {
Stmt::Return(r) => {
if !is_function && r.kind == ReturnKind::Explicit && r.value.is_none() {
r.kind = ReturnKind::TunnelRedirect;
}
}
Stmt::ChoiceSet(cs) => {
for choice in &mut cs.choices {
fixup_return_kind(is_function, &mut choice.body);
}
fixup_return_kind(is_function, &mut cs.continuation);
}
Stmt::LabeledBlock(b) => fixup_return_kind(is_function, b),
Stmt::Conditional(c) => {
for branch in &mut c.branches {
fixup_return_kind(is_function, &mut branch.body);
}
}
Stmt::Sequence(s) => {
for branch in &mut s.branches {
fixup_return_kind(is_function, &mut branch.body);
}
}
Stmt::LogicBlock(lb) => fixup_return_kind_in_block_stmts(is_function, &mut lb.stmts),
Stmt::Content(_)
| Stmt::Divert(_)
| Stmt::TunnelCall(_)
| Stmt::ThreadStart(_)
| Stmt::TempDecl(_)
| Stmt::Assignment(_)
| Stmt::ExprStmt(_)
| Stmt::EndOfLine
| Stmt::Await(_)
| Stmt::AttachElement(_)
| Stmt::EndElementRun => {}
}
}
block.recompute_tail();
}
fn fixup_return_kind_in_block_stmts(is_function: bool, stmts: &mut [BlockStmt]) {
for stmt in stmts {
match stmt {
BlockStmt::Return(r) => {
if !is_function && r.kind == ReturnKind::Explicit && r.value.is_none() {
r.kind = ReturnKind::TunnelRedirect;
}
}
BlockStmt::If(i) => fixup_return_kind_in_if_stmt(is_function, i),
BlockStmt::While(w) => fixup_return_kind_in_block_stmts(is_function, &mut w.body),
BlockStmt::For(f) => fixup_return_kind_in_block_stmts(is_function, &mut f.body),
BlockStmt::TempDecl(_)
| BlockStmt::Assignment(_)
| BlockStmt::Break(_)
| BlockStmt::Continue(_)
| BlockStmt::ExprStmt(_)
| BlockStmt::Await(_) => {}
}
}
}
fn fixup_return_kind_in_if_stmt(is_function: bool, i: &mut IfStmt) {
fixup_return_kind_in_block_stmts(is_function, &mut i.body);
match &mut i.else_branch {
Some(ElseBranch::ElseIf(inner)) => fixup_return_kind_in_if_stmt(is_function, inner),
Some(ElseBranch::Else(stmts)) => fixup_return_kind_in_block_stmts(is_function, stmts),
None => {}
}
}
pub(super) fn apply_implicit_done(block: &mut Block) {
if !matches!(block.tail, crate::Tail::Unit) {
return;
}
block.stmts.push(Stmt::Divert(Divert {
ptr: None,
target: DivertTarget {
path: DivertPath::Done,
args: Vec::new(),
},
}));
block.recompute_tail();
}
fn lower_content_line_body(
file_id: FileId,
cl: &ast::ContentLine,
elements: &mut Elements,
diags: &mut Vec<Diagnostic>,
) -> Vec<Stmt> {
let line_prov = native_provenance(file_id, NodeClass::Content, cl.syntax());
let children: Vec<SyntaxNode> = cl
.syntax()
.children()
.filter(|n| n.kind() != N::LABEL)
.collect();
lower_content_run(file_id, &children, Some(line_prov), elements, diags, true)
}
pub(super) fn lower_content_run(
file_id: FileId,
items: &[SyntaxNode],
line_prov: Option<Provenance>,
elements: &mut Elements,
diags: &mut Vec<Diagnostic>,
trailing_eol: bool,
) -> Vec<Stmt> {
let mut out = Vec::new();
let mut parts: Vec<ContentPart> = Vec::new();
let mut tags: Vec<Tag> = Vec::new();
let mut i = 0;
while i < items.len() {
let node = &items[i];
match node.kind() {
N::TEXT => {
push_text(&mut parts, node);
i += 1;
}
N::ESCAPE => {
push_escape(&mut parts, node);
i += 1;
}
N::INTERPOLATION => {
parts.push(lower_interpolation(file_id, node, diags));
i += 1;
}
N::SPAN => {
parts.push(lower_span(file_id, node, elements, diags));
i += 1;
}
N::GLUE_NODE => {
parts.push(ContentPart::Glue);
i += 1;
}
N::TAG => {
if let Some(t) = ast::Tag::cast(node.clone()) {
tags.push(lower_tag(file_id, &t, diags));
}
i += 1;
}
N::DIVERT_STMT | N::TUNNEL_CALL => {
flush_content(&mut parts, &mut tags, &mut out, None, false);
out.extend(lower_divert_like(file_id, node, diags));
i += 1;
}
N::CHOICE_POINT => {
flush_content(&mut parts, &mut tags, &mut out, None, false);
if let Some(cp) = ast::ChoicePoint::cast(node.clone()) {
let stmts = lower_content_run(
file_id,
&items[i + 1..],
line_prov,
elements,
diags,
true,
);
let tail = crate::tail_from_stmts(&stmts);
let continuation = Block {
label: None,
stmts,
container_id: None,
tail,
};
out.extend(lower_choice_point(
file_id,
&cp,
continuation,
elements,
diags,
));
}
return out;
}
N::CONDITIONAL_BLOCK => {
if let Some(cb) = ast::ConditionalBlock::cast(node.clone()) {
parts.push(ContentPart::InlineConditional(lower_conditional(
file_id, &cb, elements, diags,
)));
}
i += 1;
}
N::ALTERNATION_BLOCK => {
if let Some(ab) = ast::AlternationBlock::cast(node.clone()) {
parts.push(ContentPart::InlineSequence(lower_alternation(
file_id, &ab, elements, diags, false,
)));
}
i += 1;
}
N::ERROR => {
i += 1;
}
_ => {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E129));
i += 1;
}
}
}
flush_content(&mut parts, &mut tags, &mut out, line_prov, trailing_eol);
out
}
fn flush_content(
parts: &mut Vec<ContentPart>,
tags: &mut Vec<Tag>,
out: &mut Vec<Stmt>,
ptr: Option<Provenance>,
allow_eol: bool,
) {
if parts.is_empty() && tags.is_empty() {
return;
}
let ends_glue = matches!(parts.last(), Some(ContentPart::Glue));
out.push(Stmt::Content(Content {
ptr,
parts: std::mem::take(parts),
tags: std::mem::take(tags),
}));
if allow_eol && !ends_glue {
out.push(Stmt::EndOfLine);
}
}
pub(super) fn push_text(parts: &mut Vec<ContentPart>, node: &SyntaxNode) {
push_literal(parts, &node.text().to_string());
}
pub(super) fn push_escape(parts: &mut Vec<ContentPart>, node: &SyntaxNode) {
if let Some(escaped) = node
.children_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
.nth(1)
{
push_literal(parts, escaped.text());
}
}
fn push_literal(parts: &mut Vec<ContentPart>, s: &str) {
if s.is_empty() {
return;
}
if let Some(ContentPart::Text(last)) = parts.last_mut() {
last.push_str(s);
} else {
parts.push(ContentPart::Text(s.to_string()));
}
}
pub(super) fn lower_span(
file_id: FileId,
node: &SyntaxNode,
elements: &mut Elements,
diags: &mut Vec<Diagnostic>,
) -> ContentPart {
let mut name = String::new();
let mut attrs = Vec::new();
let mut children: Vec<ContentPart> = Vec::new();
for child in node.children() {
match child.kind() {
N::SPAN_NAME => name = child.text().to_string(),
N::SPAN_ATTR => attrs.push(lower_span_attr(file_id, &child)),
N::TEXT => push_text(&mut children, &child),
N::ESCAPE => push_escape(&mut children, &child),
N::INTERPOLATION => children.push(lower_interpolation(file_id, &child, diags)),
N::GLUE_NODE => children.push(ContentPart::Glue),
N::SPAN => children.push(lower_span(file_id, &child, elements, diags)),
N::CONDITIONAL_BLOCK => {
if let Some(cb) = ast::ConditionalBlock::cast(child) {
children.push(ContentPart::InlineConditional(lower_conditional(
file_id, &cb, elements, diags,
)));
}
}
N::ALTERNATION_BLOCK => {
if let Some(ab) = ast::AlternationBlock::cast(child) {
children.push(ContentPart::InlineSequence(lower_alternation(
file_id, &ab, elements, diags, false,
)));
}
}
N::ERROR => {}
_ => diags.push(diag(file_id, child.text_range(), DiagnosticCode::E129)),
}
}
ContentPart::Span(SpanPart {
ptr: native_provenance(file_id, NodeClass::Span, node),
name,
attrs,
children,
})
}
fn lower_span_attr(file_id: FileId, node: &SyntaxNode) -> SpanAttr {
let mut name = String::new();
let mut value = String::new();
for el in node.children_with_tokens() {
match el {
rowan::NodeOrToken::Token(t) if t.kind() == N::IDENT => {
name = t.text().to_string();
}
rowan::NodeOrToken::Node(n) if n.kind() == N::SPAN_ATTR_VALUE => {
value = attr_value_text(&n);
}
_ => {}
}
}
SpanAttr {
ptr: native_provenance(file_id, NodeClass::SpanAttr, node),
name,
value,
}
}
fn attr_value_text(node: &SyntaxNode) -> String {
let mut s = String::new();
for tok in node
.children_with_tokens()
.filter_map(rowan::NodeOrToken::into_token)
{
match tok.kind() {
N::STRING_TEXT => s.push_str(tok.text()),
N::STRING_ESCAPE => s.push_str(super::expr::unescape_string_token(tok.text())),
_ => {}
}
}
s
}
pub(super) fn lower_interpolation(
file_id: FileId,
node: &SyntaxNode,
diags: &mut Vec<Diagnostic>,
) -> ContentPart {
if let Some(inner) = node.children().next() {
ContentPart::Interpolation(lower_expr(file_id, &inner, diags))
} else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E015));
ContentPart::Interpolation(Expr::Null)
}
}
pub(super) fn lower_tag(file_id: FileId, t: &ast::Tag, diags: &mut Vec<Diagnostic>) -> Tag {
let trimmed = t.text();
if let Some(rest) = trimmed.strip_prefix('@') {
diags.push(directive_like_tag_diagnostic(
file_id,
t.syntax().text_range(),
rest,
));
}
let parts = if trimmed.is_empty() {
Vec::new()
} else {
vec![ContentPart::Text(trimmed)]
};
Tag {
parts,
ptr: native_provenance(file_id, NodeClass::Tag, t.syntax()),
}
}
fn directive_like_tag_diagnostic(file: FileId, range: rowan::TextRange, rest: &str) -> Diagnostic {
let name: String = rest
.chars()
.take_while(|c| *c != '(' && !c.is_whitespace())
.collect();
let message = match name.as_str() {
"was" | "effects" => format!(
"`#@{name}` is the ink-dialect directive-tag spelling; native's equivalent is the \
`@[{name}(…)]` annotation — this tag has no directive effect here and compiles as \
literal runtime tag content"
),
"module" | "public" | "private" | "local" => format!(
"`#@{name}` is an ink-dialect compiler-directive spelling (`module`/`public`/\
`private`/`local`/`was`/`effects`); native has no directive channel and no \
`{name}` equivalent — this tag has no directive effect here and compiles as \
literal runtime tag content"
),
"allow" => "`#@allow` has no directive meaning in either dialect — ink's directive \
recognizer only knows `module`/`public`/`private`/`local`/`was`/`effects`; \
native's `@[allow(…)]` annotation is an unrelated diagnostic-suppression channel \
— this tag compiles as literal runtime tag content"
.to_string(),
_ => format!(
"`#@{name}` has the shape of an ink-dialect compiler-directive tag, but native has \
no directive channel and ink has no `{name}` directive either — this tag compiles \
as literal runtime tag content"
),
};
Diagnostic {
file,
range,
message,
code: DiagnosticCode::E172,
}
}
pub(super) fn lower_divert_like(
file_id: FileId,
node: &SyntaxNode,
diags: &mut Vec<Diagnostic>,
) -> Option<Stmt> {
match node.kind() {
N::DIVERT_STMT => {
let target = ast::DivertStmt::cast(node.clone())
.and_then(|d| d.target())
.and_then(|t| lower_divert_target(file_id, &t, diags))?;
Some(Stmt::Divert(Divert {
ptr: Some(native_provenance(file_id, NodeClass::Divert, node)),
target,
}))
}
N::TUNNEL_CALL => {
let target = ast::TunnelCall::cast(node.clone())
.and_then(|t| t.target())
.and_then(|t| lower_divert_target(file_id, &t, diags))?;
Some(Stmt::TunnelCall(TunnelCall {
ptr: native_provenance(file_id, NodeClass::TunnelCall, node),
targets: vec![target],
}))
}
_ => None,
}
}
fn lower_divert_target(
file_id: FileId,
t: &ast::DivertTarget,
diags: &mut Vec<Diagnostic>,
) -> Option<DivertTarget> {
let path = if t.is_end() {
DivertPath::End
} else if t.is_done() {
DivertPath::Done
} else if let Some(p) = t.path() {
DivertPath::Path(super::expr::lower_path(&p))
} else {
diags.push(diag(file_id, t.syntax().text_range(), DiagnosticCode::E012));
return None;
};
let args = t
.call_args()
.into_iter()
.flat_map(|al| al.syntax().children().collect::<Vec<_>>())
.map(|arg_node| lower_expr(file_id, &arg_node, diags))
.collect();
Some(DivertTarget { path, args })
}
fn lower_return_value(
file_id: FileId,
node: &SyntaxNode,
diags: &mut Vec<Diagnostic>,
) -> Option<Expr> {
ast::ReturnStmt::cast(node.clone())
.and_then(|n| n.value())
.map(|v| lower_expr(file_id, &v, diags))
}
fn lower_return_redirect(
file_id: FileId,
node: &SyntaxNode,
diags: &mut Vec<Diagnostic>,
) -> Vec<Stmt> {
let Some(target) = ast::ReturnRedirect::cast(node.clone())
.and_then(|r| r.target())
.and_then(|t| lower_divert_target(file_id, &t, diags))
else {
diags.push(diag(file_id, node.text_range(), DiagnosticCode::E012));
return Vec::new();
};
match target.path {
DivertPath::Path(p) => vec![Stmt::Return(Return {
ptr: Some(native_provenance(file_id, NodeClass::Return, node)),
kind: ReturnKind::TunnelRedirect,
value: Some(Expr::DivertTarget(p)),
onwards_args: target.args,
})],
DivertPath::Done => vec![Stmt::Divert(Divert {
ptr: Some(native_provenance(file_id, NodeClass::Divert, node)),
target: DivertTarget {
path: DivertPath::Done,
args: Vec::new(),
},
})],
DivertPath::End => vec![Stmt::Divert(Divert {
ptr: Some(native_provenance(file_id, NodeClass::Divert, node)),
target: DivertTarget {
path: DivertPath::End,
args: Vec::new(),
},
})],
}
}