use brink_ir::hir::visit::{self, HirVisitor};
use brink_ir::{
BlockStmt, Diagnostic, DiagnosticCode, ElseBranch, Expr, FileId, HirFile, IfStmt, Knot,
ResolutionMap, Stitch, Stmt,
};
use crate::determinism::LookupSet;
pub use brink_project_config::Dialect;
pub fn check(
files: &[(FileId, &HirFile)],
resolutions: &ResolutionMap,
dialect: Dialect,
) -> Vec<Diagnostic> {
let resolved: LookupSet<(FileId, rowan::TextRange)> =
resolutions.iter().map(|r| (r.file, r.range)).collect();
let mut out = Vec::new();
for &(file, hir) in files {
let mut v = GateVisitor {
file,
dialect,
resolved: &resolved,
diagnostics: &mut out,
};
visit::visit(hir, &mut v);
for var in &hir.variables {
if let Some(ann) = &var.annotation {
v.flag(ann.range(), "type annotation");
}
}
for c in &hir.constants {
if let Some(ann) = &c.annotation {
v.flag(ann.range(), "type annotation");
}
}
for s in &hir.structs {
v.flag(s.ptr.text_range(), "`STRUCT` declaration");
}
if let Some(module) = &hir.module {
v.flag(module.range, "`#@module` declaration");
}
for import in &hir.imports {
v.flag(import.range, "`IMPORT` statement");
}
for vis in &hir.visibility {
let name = match vis.mark {
brink_ir::VisibilityMark::Private => "`#@private` directive",
brink_ir::VisibilityMark::Public => "`#@public` directive",
};
v.flag(vis.range, name);
}
for range in &hir.was_directives {
v.flag(*range, "`#@was` directive");
}
}
out
}
struct GateVisitor<'a> {
file: FileId,
dialect: Dialect,
resolved: &'a LookupSet<(FileId, rowan::TextRange)>,
diagnostics: &'a mut Vec<Diagnostic>,
}
impl GateVisitor<'_> {
fn flag(&mut self, range: rowan::TextRange, construct: &str) {
if self.dialect != Dialect::StrictInk {
return;
}
self.diagnostics.push(Diagnostic {
file: self.file,
range,
message: format!(
"{construct} is a brink extension — this project compiles \
strict ink (dialect = brink to enable)"
),
code: DiagnosticCode::E051,
});
}
fn flag_params(&mut self, params: &[brink_ir::Param]) {
for p in params {
if let Some(ann) = &p.annotation {
self.flag(ann.range(), "type annotation");
}
}
}
fn flag_block_stmts(&mut self, stmts: &[BlockStmt]) {
for s in stmts {
match s {
BlockStmt::TempDecl(t) => {
if let Some(ann) = &t.annotation {
self.flag(ann.range(), "type annotation");
}
}
BlockStmt::If(i) => self.flag_if_stmt(i),
BlockStmt::While(w) => {
if w.is_await {
self.flag(w.ptr.text_range(), "`await` suspension point");
}
self.flag_block_stmts(&w.body);
}
BlockStmt::For(f) => self.flag_block_stmts(&f.body),
BlockStmt::Await(a) => {
self.flag(a.ptr.text_range(), "`await` suspension point");
}
BlockStmt::Assignment(_)
| BlockStmt::Return(_)
| BlockStmt::ExprStmt(_)
| BlockStmt::Break(_)
| BlockStmt::Continue(_) => {}
}
}
}
fn flag_if_stmt(&mut self, i: &IfStmt) {
self.flag_block_stmts(&i.body);
match &i.else_branch {
Some(ElseBranch::ElseIf(inner)) => self.flag_if_stmt(inner),
Some(ElseBranch::Else(stmts)) => self.flag_block_stmts(stmts),
None => {}
}
}
}
fn is_t1b_stdlib_call_name(name: &str) -> bool {
crate::resolve::is_t1b_stdlib_name(name)
}
impl HirVisitor for GateVisitor<'_> {
fn visit_exprs(&self) -> bool {
true
}
fn enter_knot(&mut self, knot: &Knot) {
self.flag_params(&knot.params);
if let Some(ret) = &knot.return_type {
self.flag(ret.range(), "type annotation");
}
if let Some(assertion) = &knot.effects_assertion {
self.flag(assertion.range, "`@[effects(…)]` assertion");
}
}
fn enter_stitch(&mut self, stitch: &Stitch) {
self.flag_params(&stitch.params);
if let Some(ret) = &stitch.return_type {
self.flag(ret.range(), "type annotation");
}
if let Some(assertion) = &stitch.effects_assertion {
self.flag(assertion.range, "`@[effects(…)]` assertion");
}
}
fn enter_stmt(&mut self, stmt: &Stmt) {
match stmt {
Stmt::LogicBlock(lb) => {
self.flag(lb.ptr.text_range(), "`~ { … }` multi-line logic block");
self.flag_block_stmts(&lb.stmts);
}
Stmt::TempDecl(t) => {
if let Some(ann) = &t.annotation {
self.flag(ann.range(), "type annotation");
}
}
Stmt::Await(a) => self.flag(a.ptr.text_range(), "`await` suspension point"),
_ => {}
}
}
fn enter_expr(&mut self, expr: &Expr) {
match expr {
Expr::ArrayLiteral(a) => self.flag(a.ptr.text_range(), "`#[…]` array literal"),
Expr::MapLiteral(m) => self.flag(m.ptr.text_range(), "`#{…}` map literal"),
Expr::Call(path, _args) => {
let Some(name) = path.segments.first().map(|s| s.text.as_str()) else {
return;
};
if is_t1b_stdlib_call_name(name)
&& !self.resolved.contains(&(self.file, path.range))
{
self.flag(path.range, &format!("`{name}` stdlib function"));
}
}
Expr::Path(p) => {
if let [seg] = p.segments.as_slice()
&& seg.text == "none"
&& !self.resolved.contains(&(self.file, p.range))
{
self.flag(p.range, "`none` Option literal");
}
}
Expr::Index(i) => self.flag(i.ptr.text_range(), "postfix indexing `[…]`"),
Expr::StructLiteral(sl) => {
self.flag(sl.ptr.text_range(), "struct construction literal");
}
Expr::FieldAccess(fa) => {
self.flag(fa.ptr.text_range(), "postfix field access `.field`");
}
Expr::FnLiteral(fl) => {
self.flag(fl.ptr.text_range(), "`#fn(…)` function-value creation");
}
Expr::RefArg(ra) => {
self.flag(ra.ptr.text_range(), "`ref` path-projection expression");
}
Expr::Range(r) => {
self.flag(r.ptr.text_range(), "`..`/`..=` range literal");
}
_ => {}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use brink_syntax::parse;
fn lower_src(src: &str) -> HirFile {
let parsed = parse(src);
let tree = parsed.tree();
let (hir, _, _) = brink_ir::hir::lower::lower(FileId(0), &tree);
hir
}
fn no_resolutions() -> ResolutionMap {
ResolutionMap::new()
}
#[test]
fn strict_ink_flags_block() {
let hir = lower_src("~ {\ntemp x = 0\n}\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("brink extension"));
}
#[test]
fn brink_dialect_does_not_flag_block_since_it_lowers_in_t1b_2() {
let hir = lower_src("~ {\ntemp x = 0\n}\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_module_directive() {
let hir = lower_src("#@module(quest)\nHi\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("#@module"));
}
#[test]
fn brink_dialect_allows_module_directive() {
let hir = lower_src("#@module(quest)\nHi\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_effects_directive_on_knot() {
let hir = lower_src("== guard ==\n@[effects(pure)]\nHalt!\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("effects"));
}
#[test]
fn strict_ink_flags_effects_directive_on_stitch() {
let hir = lower_src("== guard ==\nHalt!\n= mood\n@[effects(reads(gold))]\ngrumpy\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn brink_dialect_allows_effects_directive() {
let hir = lower_src("== guard ==\n#@effects(reads: gold, calls: audio)\nHalt!\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_array_literal_in_ordinary_logic_line() {
let hir = lower_src("~ x = #[1, 2, 3]\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn strict_ink_flags_map_literal() {
let hir = lower_src("~ x = #{}\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn strict_ink_flags_indexing() {
let hir = lower_src("~ x = a[0]\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn strict_ink_flags_indexed_assignment() {
let hir = lower_src("~ a[0] = 5\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn plain_ink_produces_no_dialect_diagnostics() {
let hir = lower_src("~ x = 5\nHello world\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert!(diags.is_empty(), "no extension syntax used: {diags:?}");
}
#[test]
fn nested_extension_inside_block_is_flagged_alongside_the_block() {
let hir = lower_src("~ {\ntemp x = a[0]\n}\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 2, "{diags:?}");
assert!(diags.iter().all(|d| d.code == DiagnosticCode::E051));
}
#[test]
fn strict_ink_flags_unresolved_stdlib_call() {
let hir = lower_src("~ x = len(a)\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("len"));
}
#[test]
fn brink_dialect_does_not_flag_unresolved_stdlib_call() {
let hir = lower_src("~ x = len(a)\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn resolved_stdlib_name_call_is_never_flagged_in_either_dialect() {
let hir = lower_src("~ x = len(a)\n");
let Some(Stmt::Assignment(assign)) = hir.root_content.stmts.first() else {
unreachable!("lower_src(\"~ x = len(a)\\n\") always lowers to an Assignment")
};
let Expr::Call(path, _) = &assign.value else {
unreachable!("assignment value is always the len(a) call")
};
let call_range = path.range;
let resolutions = vec![brink_ir::ResolvedRef {
file: FileId(0),
range: call_range,
target: brink_format::DefinitionId::new(brink_format::DefinitionTag::Address, 1),
}];
let diags = check(&[(FileId(0), &hir)], &resolutions, Dialect::StrictInk);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_unresolved_option_verb_calls() {
for src in [
"~ x = find(s, \"a\")\n",
"~ x = get(m, \"k\")\n",
"~ x = min(a)\n",
"~ x = some(1)\n",
] {
let hir = lower_src(src);
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert!(
diags.iter().any(|d| d.code == DiagnosticCode::E051),
"expected E051 for {src:?}, got {diags:?}"
);
}
}
#[test]
fn strict_ink_flags_unresolved_rand_verb_calls() {
for src in [
"~ x = float()
",
"~ x = chance(0.5)
",
"~ x = pick(a)
",
"~ x = shuffled(a)
",
"~ shuffle(a)
",
"~ seed(42)
",
] {
let hir = lower_src(src);
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert!(
diags.iter().any(|d| d.code == DiagnosticCode::E051),
"expected E051 for {src:?}, got {diags:?}"
);
}
}
#[test]
fn brink_dialect_does_not_flag_the_rand_surface() {
let hir = lower_src(
"~ x = chance(0.5)
~ y = pick(a)
~ shuffle(a)
~ seed(42)
~ z = float()
",
);
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_a_bare_unresolved_none() {
let hir = lower_src("~ x = none\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("none"), "{diags:?}");
}
#[test]
fn brink_dialect_does_not_flag_the_option_surface() {
let hir = lower_src("~ x = find(s, \"a\")\n~ y = none\n~ z = some(1)\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn resolved_none_reference_is_never_flagged() {
let hir = lower_src("~ x = none\n");
let Some(Stmt::Assignment(assign)) = hir.root_content.stmts.first() else {
unreachable!("~ x = none lowers to an Assignment")
};
let Expr::Path(p) = &assign.value else {
unreachable!("assignment value is the bare none path")
};
let resolutions = vec![brink_ir::ResolvedRef {
file: FileId(0),
range: p.range,
target: brink_format::DefinitionId::new(brink_format::DefinitionTag::Address, 1),
}];
let diags = check(&[(FileId(0), &hir)], &resolutions, Dialect::StrictInk);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_param_annotation() {
let hir = lower_src("=== heal(hp: int) ===\n~ return hp\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn strict_ink_flags_return_type_annotation() {
let hir = lower_src("=== function heal(hp): int ===\n~ return hp\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn strict_ink_flags_stitch_return_type_annotation() {
let hir = lower_src("=== camp ===\n= fire(logs): int\n~ return logs\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn strict_ink_flags_both_param_and_return_annotations_separately() {
let hir = lower_src("=== function heal(hp: int): int ===\n~ return hp\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 2, "{diags:?}");
assert!(diags.iter().all(|d| d.code == DiagnosticCode::E051));
}
#[test]
fn strict_ink_flags_var_annotation() {
let hir = lower_src("VAR gold: int = 100\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn strict_ink_flags_const_annotation() {
let hir = lower_src("CONST speed: float = 0.5\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn brink_dialect_does_not_flag_const_annotation() {
let hir = lower_src("CONST speed: float = 0.5\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_temp_ascription() {
let hir = lower_src("~ temp name: string = \"a\"\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn strict_ink_flags_temp_ascription_inside_a_block() {
let hir = lower_src("~ {\ntemp x: int = 1\n}\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 2, "{diags:?}");
assert!(diags.iter().all(|d| d.code == DiagnosticCode::E051));
}
#[test]
fn brink_dialect_does_not_flag_type_annotations() {
let hir = lower_src("=== function heal(hp: int): int ===\n~ return hp\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn unannotated_declarations_produce_no_type_diagnostics() {
let hir = lower_src(
"=== heal(hp) ===\nVAR gold = 100\nCONST speed = 0.5\n~ temp t = 1\n~ return hp\n",
);
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_fn_literal() {
let hir = lower_src("~ f = #fn(heal, hp)\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("#fn"), "{diags:?}");
}
#[test]
fn strict_ink_flags_fn_literal_nested_in_a_call_argument() {
let hir = lower_src("~ x = apply(#fn(heal, hp), 5)\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn brink_dialect_does_not_flag_fn_literal_at_the_gate() {
let hir = lower_src("~ f = #fn(heal, hp)\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_range_literal() {
let hir = lower_src("~ x = 1..=6\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("range"), "{diags:?}");
}
#[test]
fn brink_dialect_does_not_flag_range_literal() {
let hir = lower_src("~ x = 0..10\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_ref_expr() {
let hir = lower_src("~ x = alter(ref gold, 5)\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("ref"), "{diags:?}");
}
#[test]
fn brink_dialect_does_not_flag_ref_expr_at_the_gate() {
let hir = lower_src("~ x = alter(ref gold, 5)\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_struct_decl() {
let hir = lower_src("STRUCT Point = #{x: float, y: float}\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
}
#[test]
fn brink_dialect_does_not_flag_struct_decl() {
let hir = lower_src("STRUCT Point = #{x: float, y: float}\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_struct_literal() {
let hir = lower_src("STRUCT Point = #{x: float, y: float}\n~ p = Point#{x: 1.0, y: 2.0}\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 2, "{diags:?}");
assert!(diags.iter().all(|d| d.code == DiagnosticCode::E051));
}
#[test]
fn strict_ink_flags_field_access() {
let hir =
lower_src("STRUCT Point = #{x: float, y: float}\n~ x = Point#{x: 1.0, y: 2.0}.x\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 3, "{diags:?}");
assert!(diags.iter().all(|d| d.code == DiagnosticCode::E051));
}
#[test]
fn brink_dialect_does_not_flag_struct_literal_or_field_access() {
let hir =
lower_src("STRUCT Point = #{x: float, y: float}\n~ x = Point#{x: 1.0, y: 2.0}.x\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn strict_ink_flags_import() {
let hir = lower_src("IMPORT quest_3\nHi\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("IMPORT"));
}
#[test]
fn strict_ink_flags_visibility_directive() {
let hir = lower_src("#@private\nVAR secret = 0\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::StrictInk);
assert_eq!(diags.len(), 1, "{diags:?}");
assert_eq!(diags[0].code, DiagnosticCode::E051);
assert!(diags[0].message.contains("#@private"));
}
#[test]
fn brink_dialect_does_not_flag_module_surface() {
let hir = lower_src("IMPORT quest_3\n#@private\nVAR secret = 0\nHi\n");
let diags = check(&[(FileId(0), &hir)], &no_resolutions(), Dialect::Brink);
assert!(diags.is_empty(), "{diags:?}");
}
}