use brink_ir::hir::visit::{self, HirVisitor};
use brink_ir::hir::{Choice, Sequence, SequenceType};
use brink_ir::{Diagnostic, DiagnosticCode, FileId, HirFile};
fn diag(file: FileId, range: rowan::TextRange, message: String) -> Diagnostic {
Diagnostic {
file,
range,
message,
code: DiagnosticCode::E157,
}
}
#[must_use]
pub fn check(file_id: FileId, hir: &HirFile) -> Vec<Diagnostic> {
let mut v = AnonymousStatefulVisitor {
file: file_id,
diagnostics: Vec::new(),
};
visit::visit(hir, &mut v);
v.diagnostics
}
struct AnonymousStatefulVisitor {
file: FileId,
diagnostics: Vec<Diagnostic>,
}
impl HirVisitor for AnonymousStatefulVisitor {
fn enter_choice(&mut self, choice: &Choice) {
if is_anonymous_once_only(choice) {
self.diagnostics.push(diag(
self.file,
choice.ptr.text_range(),
"this once-only choice has no name, so its 'already chosen' \
state is anonymous — inserting or removing an earlier \
sibling in the same weave block shifts its compiled \
identity and makes it reappear as if never chosen; give it \
a stable identity with `(label)`, which also anchors \
everything inside its body"
.to_owned(),
));
}
}
fn enter_sequence(&mut self, seq: &Sequence) {
if is_stateful_sequence(seq) {
self.diagnostics.push(diag(
self.file,
seq.ptr.text_range(),
"this sequence's position state is anonymous, so inserting \
or removing an earlier sibling in the same weave block \
shifts its compiled identity and makes it restart from its \
first branch; place it under a `(label)`ed choice or \
block, or in its own stably-named stitch, so nothing can \
renumber it"
.to_owned(),
));
}
}
}
fn is_anonymous_once_only(choice: &Choice) -> bool {
!choice.is_sticky && !choice.is_fallback && choice.label.is_none()
}
fn is_stateful_sequence(seq: &Sequence) -> bool {
seq.branches.len() >= 2 || seq.kind.contains(SequenceType::ONCE)
}
#[cfg(test)]
mod tests {
use super::*;
use brink_ir::FileId;
use brink_syntax::parse;
fn lower_ink(src: &str) -> HirFile {
let parsed = parse(src);
let tree = parsed.tree();
let (hir, _, _) = brink_ir::hir::lower::lower(FileId(0), &tree);
hir
}
fn lower_native(src: &str) -> HirFile {
let parsed = brink_syntax_native::parse(src);
let tree = parsed.tree();
let (hir, _, _) = brink_ir::hir::lower_native::lower(FileId(0), &tree);
hir
}
fn codes(diags: &[Diagnostic]) -> Vec<DiagnosticCode> {
diags.iter().map(|d| d.code).collect()
}
#[test]
fn unlabeled_once_only_choice_is_flagged() {
let hir = lower_ink("=== knot ===\n* [pick] -> DONE\n");
let diags = check(FileId(0), &hir);
assert_eq!(codes(&diags), vec![DiagnosticCode::E157], "{diags:?}");
}
#[test]
fn labeled_once_only_choice_is_not_flagged() {
let hir = lower_ink("=== knot ===\n* (mine) [pick] -> DONE\n");
let diags = check(FileId(0), &hir);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn sticky_choice_is_never_flagged() {
let hir = lower_ink("=== knot ===\n+ [pick] -> DONE\n");
let diags = check(FileId(0), &hir);
assert!(diags.is_empty(), "a `+` choice carries no state: {diags:?}");
}
#[test]
fn multi_branch_sequence_is_flagged() {
let hir = lower_ink("=== knot ===\n{a|b|c}\n-> DONE\n");
let diags = check(FileId(0), &hir);
assert_eq!(codes(&diags), vec![DiagnosticCode::E157], "{diags:?}");
}
#[test]
fn single_branch_stopping_sequence_is_not_flagged() {
let hir = lower_ink("=== knot ===\n{$a}\n-> DONE\n");
let diags = check(FileId(0), &hir);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn single_branch_once_only_alternation_is_flagged() {
let hir = lower_ink("=== knot ===\n{!a}\n-> DONE\n");
let diags = check(FileId(0), &hir);
assert_eq!(codes(&diags), vec![DiagnosticCode::E157], "{diags:?}");
}
#[test]
fn cycle_sequence_with_two_branches_is_flagged() {
let hir = lower_ink("=== knot ===\n{&a|b}\n-> DONE\n");
let diags = check(FileId(0), &hir);
assert_eq!(codes(&diags), vec![DiagnosticCode::E157], "{diags:?}");
}
#[test]
fn story_with_no_stateful_anonymous_constructs_is_clean() {
let hir = lower_ink("=== knot ===\nHello.\n-> DONE\n");
let diags = check(FileId(0), &hir);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn native_unlabeled_once_only_choice_is_flagged() {
let hir = lower_native("flow f() {\n {?\n * [Look] You look around.\n }\n}\n");
let diags = check(FileId(0), &hir);
assert_eq!(codes(&diags), vec![DiagnosticCode::E157], "{diags:?}");
}
#[test]
fn native_labeled_once_only_choice_is_not_flagged() {
let hir = lower_native("flow f() {\n {?\n * (mine) [Look] You look around.\n }\n}\n");
let diags = check(FileId(0), &hir);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn native_sticky_choice_is_never_flagged() {
let hir =
lower_native("flow f() {\n {?\n + (again) [Look again] Still a garden.\n }\n}\n");
let diags = check(FileId(0), &hir);
assert!(diags.is_empty(), "{diags:?}");
}
#[test]
fn native_fallback_choice_is_never_flagged() {
let hir = lower_native(
"flow f() {\n {?\n * [Look] You look around.\n + (again) [Look again] Still a garden.\n else { Nothing left to do. }\n }\n}\n",
);
let diags = check(FileId(0), &hir);
assert_eq!(
codes(&diags),
vec![DiagnosticCode::E157],
"only the unlabeled once-only `Look` choice, never the sticky \
`Look again` or the fallback: {diags:?}"
);
}
}