#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
use brink_ir::hir::lower_native;
use brink_ir::{DiagnosticCode, EffectsAssertion, FileId, HirFile};
fn lower(src: &str) -> (HirFile, Vec<brink_ir::Diagnostic>) {
let parse = brink_syntax_native::parse(src);
assert!(
parse.errors().is_empty(),
"fixture must parse cleanly: {:?}",
parse.errors()
);
let (hir, _manifest, diags) = lower_native::lower(FileId(0), &parse.tree());
(hir, diags)
}
fn codes(diags: &[brink_ir::Diagnostic]) -> Vec<DiagnosticCode> {
diags.iter().map(|d| d.code).collect()
}
fn knot_assertion(src: &str) -> EffectsAssertion {
let (hir, diags) = lower(src);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
hir.knots[0]
.effects_assertion
.clone()
.expect("the declaration carries an `@[effects(…)]` assertion")
}
#[test]
fn effects_above_a_fn_lowers_instead_of_failing() {
let a = knot_assertion("@[effects(pure)]\nfn heal(amount) {\n return;\n}\n");
assert!(a.pure);
assert!(!a.silent && !a.total);
assert!(a.reads.is_empty() && a.writes.is_empty() && a.calls.is_empty());
}
#[test]
fn effects_above_a_flow_carries_flags_and_every_clause() {
let a = knot_assertion(
"@[effects(silent, total, reads(gold, hp), writes(mood), calls(sfx))]\n\
flow garden() {\n Petals.\n}\n",
);
assert!(!a.pure);
assert!(a.silent && a.total);
assert_eq!(a.reads, ["gold", "hp"]);
assert_eq!(a.writes, ["mood"]);
assert_eq!(a.calls, ["sfx"]);
}
#[test]
fn effects_above_a_nested_flow_attaches_to_the_stitch() {
let (hir, diags) = lower(
"flow garden() {\n\
\x20 Petals.\n\
\x20 @[effects(total)]\n\
\x20 flow gate() {\n Creak.\n }\n\
}\n",
);
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
assert!(
hir.knots[0].effects_assertion.is_none(),
"the enclosing knot must not absorb its stitch's annotation"
);
let stitch = &hir.knots[0].stitches[0];
assert_eq!(stitch.name.text, "gate");
assert!(stitch.effects_assertion.as_ref().expect("present").total);
}
#[test]
fn doc_comments_and_blank_lines_do_not_break_attachment() {
let (hir, diags) = lower("@[effects(pure)]\n\n/// Heals.\nfn heal() {\n return;\n}\n");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
assert!(
hir.knots[0]
.effects_assertion
.as_ref()
.expect("present")
.pure
);
assert!(hir.knots[0].doc.is_some(), "the `///` doc still attaches");
}
#[test]
fn an_unannotated_declaration_has_no_assertion() {
let (hir, diags) = lower("fn heal() {\n return;\n}\n");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
assert!(hir.knots[0].effects_assertion.is_none());
}
#[test]
fn an_empty_assertion_is_e100() {
for src in [
"@[effects]\nfn heal() {\n return;\n}\n",
"@[effects()]\nfn heal() {\n return;\n}\n",
] {
let (hir, diags) = lower(src);
assert_eq!(codes(&diags), vec![DiagnosticCode::E100], "{src:?}");
assert!(hir.knots[0].effects_assertion.is_none(), "{src:?}");
}
}
#[test]
fn malformed_arguments_are_e101() {
for src in [
"@[effects(clean)]\nfn heal() {\n return;\n}\n",
"@[effects(touches(gold))]\nfn heal() {\n return;\n}\n",
"@[effects(pure, reads(gold))]\nfn heal() {\n return;\n}\n",
"@[effects(80)]\nfn heal() {\n return;\n}\n",
"@[effects(reads(80))]\nfn heal() {\n return;\n}\n",
"@[effects(story::old)]\nfn heal() {\n return;\n}\n",
"@[effects(reads(writes(gold)))]\nfn heal() {\n return;\n}\n",
] {
let (hir, diags) = lower(src);
assert_eq!(codes(&diags), vec![DiagnosticCode::E101], "{src:?}");
assert!(hir.knots[0].effects_assertion.is_none(), "{src:?}");
}
}
#[test]
fn a_duplicate_assertion_is_e048_and_the_first_wins() {
let (hir, diags) = lower("@[effects(pure)]\n@[effects(silent)]\nfn heal() {\n return;\n}\n");
assert_eq!(codes(&diags), vec![DiagnosticCode::E048]);
let a = hir.knots[0].effects_assertion.as_ref().expect("present");
assert!(a.pure && !a.silent);
}
#[test]
fn an_unknown_annotation_name_is_e111() {
for src in [
"@[bogus]\nfn heal() {\n return;\n}\n",
"@[local]\nvar hp = 10\n",
] {
let (_hir, diags) = lower(src);
assert_eq!(codes(&diags), vec![DiagnosticCode::E111], "{src:?}");
}
}
#[test]
fn element_and_style_are_recognized_names_not_e111() {
let (_hir, diags) = lower("@[element()]\nfn heal() {\n return;\n}\n");
assert_eq!(codes(&diags), vec![DiagnosticCode::E159]);
let (_hir, diags) =
lower("@[element(args = \"^ready$\")]\n@[style()]\nfn heal() {\n return;\n}\n");
assert_eq!(codes(&diags), vec![DiagnosticCode::E161]);
}
#[test]
fn a_misplaced_annotation_is_e112() {
for src in [
"@[effects(pure)]\nvar hp = 10\n",
"@[effects(pure)]\nstruct Npc {\n hp: int\n}\n",
"fn heal() {\n return;\n}\n@[effects(pure)]\n",
"flow garden() {\n @[effects(pure)]\n Petals.\n}\n",
"flow garden() {\n @[was(story::old)]\n Petals.\n}\n",
] {
let (_hir, diags) = lower(src);
assert_eq!(codes(&diags), vec![DiagnosticCode::E112], "{src:?}");
}
}
#[test]
fn the_file_level_was_record_is_still_consumed() {
let (hir, diags) = lower("@[was(\"story::old::path\")]\nflow main() {\n Hi.\n}\n");
assert!(diags.is_empty(), "unexpected diagnostics: {diags:?}");
assert_eq!(
hir.module.expect("module record").was.expect("was").0,
"story::old::path"
);
}
#[test]
fn an_annotation_line_never_lowers_to_content() {
let (hir, _diags) = lower(
"flow garden() {\n\
\x20 @[effects(pure)]\n\
\x20 @[bogus]\n\
\x20 Petals.\n\
}\n",
);
let rendered = format!("{:?}", hir.knots[0].body);
assert!(
!rendered.contains("effects") && !rendered.contains("bogus"),
"annotation text leaked into the body: {rendered}"
);
}