use brink_syntax::ast::{self, AstNode};
use brink_syntax::{SyntaxKind, SyntaxNode};
use rowan::TextRange;
use super::context::LowerSink;
use crate::DiagnosticCode;
#[derive(Debug, Clone)]
pub(super) struct ParsedDirective {
pub name: String,
pub bare: bool,
pub dynamic: bool,
pub arg: Option<String>,
pub range: TextRange,
pub from_annotation: bool,
}
pub(super) enum TagLineClass {
Plain,
Directives(Vec<ParsedDirective>),
Mixed,
}
pub(super) fn parse_directive_tag(tag: &ast::Tag) -> Option<ParsedDirective> {
let mut text = String::new();
let mut dynamic = false;
let mut first = true;
for child in tag.syntax().children_with_tokens() {
match child {
rowan::NodeOrToken::Token(tok) => {
if first && tok.kind() == SyntaxKind::HASH {
first = false;
continue;
}
first = false;
text.push_str(tok.text());
}
rowan::NodeOrToken::Node(_) => {
first = false;
dynamic = true;
}
}
}
let trimmed = text.trim();
let rest = trimmed.strip_prefix('@')?;
let name: String = rest
.chars()
.take_while(|c| *c != '(' && !c.is_whitespace())
.collect();
let bare = !dynamic && rest.len() == name.len();
let after_name = rest.trim_start_matches(|c: char| c != '(');
let arg = after_name
.strip_prefix('(')
.and_then(|s| s.strip_suffix(')'))
.map(|s| s.trim().to_string());
Some(ParsedDirective {
name,
bare,
dynamic,
arg,
range: tag.syntax().text_range(),
from_annotation: false,
})
}
pub(super) fn parse_annotation_line(al: &ast::AnnotationLine) -> Option<ParsedDirective> {
let name = al.name_token()?.text().to_string();
let arg = al.arg_text();
Some(ParsedDirective {
bare: arg.is_none(),
dynamic: false,
arg,
name,
range: al.syntax().text_range(),
from_annotation: true,
})
}
pub(super) fn scan_tag_line(tl: &ast::TagLine) -> TagLineClass {
let mut directives = Vec::new();
let mut plain = 0usize;
if let Some(tags) = tl.tags() {
for tag in tags.tags() {
match parse_directive_tag(&tag) {
Some(d) => directives.push(d),
None => plain += 1,
}
}
}
if directives.is_empty() {
TagLineClass::Plain
} else if plain == 0 && directives.len() == 1 {
TagLineClass::Directives(directives)
} else {
TagLineClass::Mixed
}
}
fn is_trivia(kind: SyntaxKind) -> bool {
kind.is_trivia() || kind == SyntaxKind::NEWLINE
}
fn is_attachable_decl(kind: SyntaxKind) -> bool {
matches!(
kind,
SyntaxKind::VAR_DECL
| SyntaxKind::CONST_DECL
| SyntaxKind::LIST_DECL
| SyntaxKind::EXTERNAL_DECL
)
}
fn is_directive_line(node: &SyntaxNode) -> bool {
ast::TagLine::cast(node.clone())
.is_some_and(|tl| matches!(scan_tag_line(&tl), TagLineClass::Directives(_)))
}
pub(super) fn attached_declaration(tl: &ast::TagLine) -> Option<SyntaxNode> {
let mut cursor = tl.syntax().next_sibling_or_token();
while let Some(el) = cursor {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
return None;
}
cursor = tok.next_sibling_or_token();
}
rowan::NodeOrToken::Node(node) => {
if node.kind() == SyntaxKind::EMPTY_LINE || is_directive_line(&node) {
cursor = node.next_sibling_or_token();
continue;
}
return is_attachable_decl(node.kind()).then_some(node);
}
}
}
None
}
pub(super) fn in_leading_body_run(tl: &ast::TagLine) -> bool {
let Some(parent) = tl.syntax().parent() else {
return false;
};
if !matches!(
parent.kind(),
SyntaxKind::KNOT_BODY | SyntaxKind::STITCH_BODY
) {
return false;
}
let mut cursor = tl.syntax().prev_sibling_or_token();
while let Some(el) = cursor {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
return false;
}
cursor = tok.prev_sibling_or_token();
}
rowan::NodeOrToken::Node(node) => {
if !matches!(
node.kind(),
SyntaxKind::TAG_LINE | SyntaxKind::EMPTY_LINE | SyntaxKind::ANNOTATION_LINE
) {
return false;
}
cursor = node.prev_sibling_or_token();
}
}
}
true
}
pub(super) fn in_leading_annotation_run(al: &ast::AnnotationLine) -> bool {
let Some(parent) = al.syntax().parent() else {
return false;
};
if !matches!(
parent.kind(),
SyntaxKind::KNOT_BODY | SyntaxKind::STITCH_BODY
) {
return false;
}
let mut cursor = al.syntax().prev_sibling_or_token();
while let Some(el) = cursor {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
return false;
}
cursor = tok.prev_sibling_or_token();
}
rowan::NodeOrToken::Node(node) => {
if !matches!(
node.kind(),
SyntaxKind::TAG_LINE | SyntaxKind::EMPTY_LINE | SyntaxKind::ANNOTATION_LINE
) {
return false;
}
cursor = node.prev_sibling_or_token();
}
}
}
true
}
pub(super) fn handle_annotation_line(al: &ast::AnnotationLine, sink: &mut impl LowerSink) {
if !in_leading_annotation_run(al) {
sink.diagnose(al.syntax().text_range(), DiagnosticCode::E112);
}
}
pub(super) fn is_consumed_position(tl: &ast::TagLine) -> bool {
attached_declaration(tl).is_some()
|| in_leading_body_run(tl)
|| is_file_module_line(tl)
|| is_file_was_line(tl)
}
fn sole_directive(tl: &ast::TagLine) -> Option<ParsedDirective> {
match scan_tag_line(tl) {
TagLineClass::Directives(mut dirs) if dirs.len() == 1 => dirs.pop(),
_ => None,
}
}
pub(super) fn is_file_module_line(tl: &ast::TagLine) -> bool {
is_file_leading_line(tl, "module")
}
pub(super) fn is_file_was_line(tl: &ast::TagLine) -> bool {
is_file_leading_line(tl, "was")
}
fn is_file_leading_line(tl: &ast::TagLine, name: &str) -> bool {
let Some(dir) = sole_directive(tl) else {
return false;
};
if dir.name != name {
return false;
}
let Some(parent) = tl.syntax().parent() else {
return false;
};
if parent.kind() != SyntaxKind::SOURCE_FILE {
return false;
}
let mut cursor = tl.syntax().prev_sibling_or_token();
while let Some(el) = cursor {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
return false;
}
cursor = tok.prev_sibling_or_token();
}
rowan::NodeOrToken::Node(node) => {
if !matches!(node.kind(), SyntaxKind::TAG_LINE | SyntaxKind::EMPTY_LINE) {
return false;
}
cursor = node.prev_sibling_or_token();
}
}
}
true
}
pub(super) fn collect_visibility_directives(
source_file: &SyntaxNode,
) -> Vec<crate::VisibilityDirective> {
use crate::VisibilityMark;
let mut out = Vec::new();
for node in source_file.descendants() {
let Some(tl) = ast::TagLine::cast(node) else {
continue;
};
let Some(tags) = tl.tags() else {
continue;
};
for tag in tags.tags() {
if let Some(dir) = parse_directive_tag(&tag) {
let mark = match dir.name.as_str() {
"private" => VisibilityMark::Private,
"public" => VisibilityMark::Public,
_ => continue,
};
out.push(crate::VisibilityDirective {
mark,
range: dir.range,
});
}
}
}
out
}
pub(super) fn collect_was_directives(source_file: &SyntaxNode) -> Vec<TextRange> {
let mut out = Vec::new();
for node in source_file.descendants() {
let Some(tl) = ast::TagLine::cast(node) else {
continue;
};
let Some(tags) = tl.tags() else {
continue;
};
for tag in tags.tags() {
if let Some(dir) = parse_directive_tag(&tag)
&& dir.name == "was"
{
out.push(dir.range);
}
}
}
out
}
pub(super) fn file_module_declaration(
source_file: &SyntaxNode,
sink: &mut impl LowerSink,
) -> Option<(String, TextRange)> {
let mut found: Option<(String, TextRange)> = None;
for child in source_file.children() {
let Some(tl) = ast::TagLine::cast(child) else {
continue;
};
if !is_file_module_line(&tl) {
continue;
}
let Some(dir) = sole_directive(&tl) else {
continue;
};
if dir.dynamic {
sink.diagnose(dir.range, DiagnosticCode::E046);
continue;
}
let Some(name) = module_directive_name(&tl) else {
sink.diagnose(dir.range, DiagnosticCode::E086);
continue;
};
if found.is_some() {
sink.diagnose(dir.range, DiagnosticCode::E086);
continue;
}
found = Some((name, dir.range));
}
found
}
pub(super) fn file_module_was(
source_file: &SyntaxNode,
sink: &mut impl LowerSink,
) -> Option<(String, TextRange, bool)> {
let mut found: Option<(String, TextRange, bool)> = None;
for child in source_file.children() {
let Some(tl) = ast::TagLine::cast(child) else {
continue;
};
if !is_file_was_line(&tl) {
continue;
}
let Some(dir) = sole_directive(&tl) else {
continue;
};
if dir.dynamic {
sink.diagnose(dir.range, DiagnosticCode::E046);
continue;
}
let Some(name) = dir.arg.as_ref().filter(|n| !n.is_empty()) else {
sink.diagnose(dir.range, DiagnosticCode::E094);
continue;
};
if found.is_some() {
sink.diagnose(dir.range, DiagnosticCode::E048);
continue;
}
found = Some((name.clone(), dir.range, attached_declaration(&tl).is_some()));
}
found
}
fn module_directive_name(tl: &ast::TagLine) -> Option<String> {
let tags = tl.tags()?;
let tag = tags.tags().next()?;
let mut text = String::new();
let mut first = true;
for child in tag.syntax().children_with_tokens() {
if let rowan::NodeOrToken::Token(tok) = child {
if first && tok.kind() == SyntaxKind::HASH {
first = false;
continue;
}
first = false;
text.push_str(tok.text());
} else {
first = false;
}
}
let trimmed = text.trim();
let rest = trimmed.strip_prefix('@')?;
let after_name = rest.trim_start_matches(|c: char| c != '(');
let inner = after_name.strip_prefix('(')?.strip_suffix(')')?;
let name = inner.trim();
(!name.is_empty()).then(|| name.to_string())
}
pub(super) fn directives_before(node: &SyntaxNode) -> Vec<ParsedDirective> {
let mut collected: Vec<ParsedDirective> = Vec::new();
let mut cursor = node.prev_sibling_or_token();
while let Some(el) = cursor {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
break;
}
cursor = tok.prev_sibling_or_token();
}
rowan::NodeOrToken::Node(n) => {
if n.kind() == SyntaxKind::EMPTY_LINE {
cursor = n.prev_sibling_or_token();
continue;
}
let Some(tl) = ast::TagLine::cast(n.clone()) else {
break;
};
let TagLineClass::Directives(dirs) = scan_tag_line(&tl) else {
break;
};
for d in dirs.into_iter().rev() {
collected.insert(0, d);
}
cursor = n.prev_sibling_or_token();
}
}
}
collected
}
pub(super) fn leading_body_directives(body: &SyntaxNode) -> Vec<ParsedDirective> {
let mut collected = Vec::new();
for el in body.children_with_tokens() {
match el {
rowan::NodeOrToken::Token(tok) => {
if !is_trivia(tok.kind()) {
break;
}
}
rowan::NodeOrToken::Node(node) => {
if node.kind() == SyntaxKind::EMPTY_LINE {
continue;
}
if let Some(al) = ast::AnnotationLine::cast(node.clone()) {
collected.extend(parse_annotation_line(&al));
continue;
}
let Some(tl) = ast::TagLine::cast(node) else {
break;
};
if let TagLineClass::Directives(dirs) = scan_tag_line(&tl)
&& attached_declaration(&tl).is_none()
{
collected.extend(dirs);
}
}
}
}
collected
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum DirectiveTarget {
Var,
Const,
List,
External,
Knot,
Stitch,
}
impl DirectiveTarget {
fn supports_local(self) -> bool {
matches!(self, Self::Var | Self::Knot | Self::Stitch)
}
}
pub(super) fn apply_scope_directives(
dirs: &[ParsedDirective],
target: DirectiveTarget,
sink: &mut impl LowerSink,
) -> bool {
let mut is_local = false;
for d in dirs {
if d.from_annotation && d.name != "effects" {
sink.diagnose(d.range, DiagnosticCode::E111);
} else if d.name == "private" || d.name == "public" {
} else if d.name == "was" {
} else if d.name == "effects" {
if !matches!(target, DirectiveTarget::Knot | DirectiveTarget::Stitch) {
sink.diagnose(d.range, DiagnosticCode::E049);
}
} else if d.name == "module" {
} else if d.name != "local" {
if d.dynamic {
sink.diagnose(d.range, DiagnosticCode::E046);
} else {
sink.diagnose(d.range, DiagnosticCode::E044);
}
} else if !d.bare {
sink.diagnose(d.range, DiagnosticCode::E050);
} else if !target.supports_local() {
sink.diagnose(d.range, DiagnosticCode::E049);
} else if is_local {
sink.diagnose(d.range, DiagnosticCode::E048);
} else {
is_local = true;
}
}
is_local
}
pub(super) fn was_from_directives(
dirs: &[ParsedDirective],
sink: &mut impl LowerSink,
) -> Option<(String, TextRange)> {
let mut chosen: Option<(String, TextRange)> = None;
for d in dirs {
if d.name != "was" {
continue;
}
if d.dynamic {
sink.diagnose(d.range, DiagnosticCode::E046);
continue;
}
let Some(name) = d.arg.as_ref().filter(|n| !n.is_empty()) else {
sink.diagnose(d.range, DiagnosticCode::E094);
continue;
};
if chosen.is_some() {
sink.diagnose(d.range, DiagnosticCode::E048);
continue;
}
chosen = Some((name.clone(), d.range));
}
chosen
}
pub(super) fn effects_assertion_from_directives(
dirs: &[ParsedDirective],
sink: &mut impl LowerSink,
) -> Option<crate::EffectsAssertion> {
let mut chosen: Option<crate::EffectsAssertion> = None;
for d in dirs {
if d.name != "effects" {
continue;
}
if !d.from_annotation {
sink.diagnose(d.range, DiagnosticCode::E110);
}
if d.dynamic {
sink.diagnose(d.range, DiagnosticCode::E046);
continue;
}
let raw = if d.bare { None } else { d.arg.as_deref() };
let Some(raw) = raw else {
sink.diagnose(d.range, DiagnosticCode::E100);
continue;
};
let trimmed = raw.trim();
let parsed = if trimmed.is_empty() {
sink.diagnose(d.range, DiagnosticCode::E100);
None
} else if d.from_annotation {
parse_effects_paren_clauses(trimmed, d.range, sink)
} else {
parse_effects_clauses(trimmed, d.range, sink)
};
let Some(parsed) = parsed else {
continue; };
if chosen.is_some() {
sink.diagnose(d.range, DiagnosticCode::E048);
continue;
}
chosen = Some(parsed);
}
chosen
}
#[derive(Clone, Copy)]
enum EffectsClauseKind {
Reads,
Writes,
Calls,
}
fn parse_effects_paren_clauses(
text: &str,
range: TextRange,
sink: &mut impl LowerSink,
) -> Option<crate::EffectsAssertion> {
let mut pure = false;
let mut silent = false;
let mut total = false;
let mut reads = Vec::new();
let mut writes = Vec::new();
let mut calls = Vec::new();
let mut ok = true;
for piece in split_top_level_commas(text) {
let piece = piece.trim();
if piece.is_empty() {
continue;
}
if let Some(open) = piece.find('(') {
let Some(inner) = piece[open + 1..].strip_suffix(')') else {
ok = false; continue;
};
let target = match piece[..open].trim() {
"reads" => &mut reads,
"writes" => &mut writes,
"calls" => &mut calls,
_ => {
ok = false; continue;
}
};
for value in inner.split(',') {
let value = value.trim();
if value.is_empty() {
continue; }
if is_effects_ident(value) {
target.push(value.to_string());
} else {
ok = false;
}
}
} else {
match piece {
"pure" => pure = true,
"silent" => silent = true,
"total" => total = true,
_ => ok = false,
}
}
}
if pure && !(reads.is_empty() && writes.is_empty() && calls.is_empty()) {
ok = false;
}
if !ok {
sink.diagnose(range, DiagnosticCode::E101);
return None;
}
if !pure && !silent && !total && reads.is_empty() && writes.is_empty() && calls.is_empty() {
sink.diagnose(range, DiagnosticCode::E100);
return None;
}
Some(crate::EffectsAssertion {
pure,
silent,
total,
reads,
writes,
calls,
range,
})
}
fn split_top_level_commas(text: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut depth = 0usize;
let mut start = 0usize;
for (i, c) in text.char_indices() {
match c {
'(' => depth += 1,
')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
out.push(&text[start..i]);
start = i + 1;
}
_ => {}
}
}
out.push(&text[start..]);
out
}
fn parse_effects_clauses(
text: &str,
range: TextRange,
sink: &mut impl LowerSink,
) -> Option<crate::EffectsAssertion> {
let mut pure = false;
let mut silent = false;
let mut total = false;
let mut reads = Vec::new();
let mut writes = Vec::new();
let mut calls = Vec::new();
let mut current: Option<EffectsClauseKind> = None;
let mut ok = true;
let mut push = |kind: EffectsClauseKind, value: &str| -> bool {
if !is_effects_ident(value) {
return false;
}
match kind {
EffectsClauseKind::Reads => reads.push(value.to_string()),
EffectsClauseKind::Writes => writes.push(value.to_string()),
EffectsClauseKind::Calls => calls.push(value.to_string()),
}
true
};
for piece in text.split(',') {
let piece = piece.trim();
if piece.is_empty() {
continue;
}
if let Some((key, rest)) = piece.split_once(':') {
let key = key.trim();
let kind = match key {
"reads" => EffectsClauseKind::Reads,
"writes" => EffectsClauseKind::Writes,
"calls" => EffectsClauseKind::Calls,
_ => {
ok = false;
continue;
}
};
current = Some(kind);
let rest = rest.trim();
if !rest.is_empty() && !push(kind, rest) {
ok = false;
}
} else {
if current.is_none() {
match piece {
"pure" => {
pure = true;
continue;
}
"silent" => {
silent = true;
continue;
}
"total" => {
total = true;
continue;
}
_ => {}
}
}
let Some(kind) = current else {
ok = false;
continue;
};
if !push(kind, piece) {
ok = false;
}
}
}
if pure && !(reads.is_empty() && writes.is_empty() && calls.is_empty()) {
ok = false;
}
if !ok {
sink.diagnose(range, DiagnosticCode::E101);
return None;
}
if !pure && !silent && !total && reads.is_empty() && writes.is_empty() && calls.is_empty() {
sink.diagnose(range, DiagnosticCode::E100);
return None;
}
Some(crate::EffectsAssertion {
pure,
silent,
total,
reads,
writes,
calls,
range,
})
}
fn is_effects_ident(s: &str) -> bool {
let mut chars = s.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub(super) fn visibility_from_directives(
dirs: &[ParsedDirective],
sink: &mut impl LowerSink,
) -> Option<crate::VisibilityMark> {
use crate::VisibilityMark;
let mut chosen: Option<VisibilityMark> = None;
for d in dirs {
let mark = match d.name.as_str() {
"private" => VisibilityMark::Private,
"public" => VisibilityMark::Public,
_ => continue,
};
if d.dynamic {
sink.diagnose(d.range, DiagnosticCode::E046);
} else if !d.bare {
sink.diagnose(d.range, DiagnosticCode::E050);
} else if chosen.is_some() {
sink.diagnose(d.range, DiagnosticCode::E093);
} else {
chosen = Some(mark);
}
}
chosen
}