#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
#![allow(
clippy::disallowed_types,
reason = "always-empty file_paths map for these fixtures — brink_ir::determinism::LookupMap \
is pub(crate) and invisible to this external test-binary crate, same allow \
issue_1727_lambda_hir_minted_id.rs's own file doc carries for the identical reason"
)]
use std::collections::HashMap;
mod analysis_fixture;
use brink_analyzer::{AnalysisOptions, Dialect, ModuleMap, ResolvedModule};
use brink_format::DefinitionId;
use brink_ir::hir::lower_native;
use brink_ir::{Choice, ContentPart, FileId, HirFile, Stmt, SymbolManifest};
const FILE_A: FileId = FileId(0);
const FILE_B: FileId = FileId(1);
const SOURCE_A: &str = "\
var flag = true
flow start() {
Some prose {if flag {
{?
* (dup) A's choice text -> a_end
}
}} more.
-> a_end
}
flow a_end() {
Done A.
-> END
}
";
const SOURCE_B: &str = "\
var flag2 = true
flow start() {
Some prose {if flag2 {
{?
* (dup) B's choice text -> b_end
}
}} more.
-> b_end
}
flow b_end() {
Done B.
-> END
}
";
fn lower(file: FileId, source: &str) -> (HirFile, SymbolManifest) {
let parsed = brink_syntax_native::parse(source);
assert!(
parsed.errors().is_empty(),
"fixture must parse cleanly: {:?}",
parsed.errors()
);
let (hir, manifest, diags) = lower_native::lower(file, &parsed.tree());
assert!(
diags.is_empty(),
"unexpected lowering diagnostics: {diags:?}"
);
(hir, manifest)
}
fn module_map(files: &[FileId]) -> ModuleMap {
files
.iter()
.map(|&f| {
let name = if f == FILE_A { "story::a" } else { "story::b" };
(
f,
ResolvedModule {
name: name.to_string(),
declared: true,
was: None,
},
)
})
.collect()
}
fn analyze_and_stamp(files: Vec<(FileId, HirFile, SymbolManifest)>) -> Vec<HirFile> {
let file_ids: Vec<FileId> = files.iter().map(|(id, _, _)| *id).collect();
let inputs: Vec<(FileId, &HirFile, &SymbolManifest)> = files
.iter()
.map(|(id, hir, manifest)| (*id, hir, manifest))
.collect();
let opts = AnalysisOptions {
dialect: Dialect::Brink,
..AnalysisOptions::default()
};
let result = analysis_fixture::analyze_with_map(&inputs, &module_map(&file_ids), &opts, true);
let file_paths: HashMap<FileId, String> = HashMap::new();
let mut slice: Vec<(FileId, HirFile)> = files
.into_iter()
.map(|(id, hir, _manifest)| (id, hir))
.collect();
brink_ir::stamp_container_ids(&mut slice, &result.index, &file_paths);
slice.into_iter().map(|(_, hir)| hir).collect()
}
fn dup_choice(hir: &HirFile) -> &Choice {
let start = hir
.knots
.iter()
.find(|k| k.name.text == "start")
.expect("fixture must declare a `start` flow");
let Stmt::Content(content) = &start.body.stmts[0] else {
panic!(
"expected `start`'s first statement to be prose Content, got {:?}",
start.body.stmts[0]
);
};
let cond = content
.parts
.iter()
.find_map(|p| match p {
ContentPart::InlineConditional(c) => Some(c),
_ => None,
})
.expect(
"expected the mid-line `{if …}` to lower to a ContentPart::InlineConditional — if \
this fails, the fixture no longer exercises this issue's code path",
);
let branch = &cond.branches[0];
let cs = branch
.body
.stmts
.iter()
.find_map(|s| match s {
Stmt::ChoiceSet(cs) => Some(cs),
_ => None,
})
.expect("expected the conditional's branch body to contain a Stmt::ChoiceSet");
cs.choices
.first()
.expect("expected exactly one choice in the nested choice point")
}
fn dup_choice_id(hir: &HirFile) -> DefinitionId {
dup_choice(hir)
.container_id
.expect("stamp_container_ids must assign every choice a container_id")
}
#[test]
fn inline_conditional_labeled_choice_keeps_self_identity_when_a_coexisting_module_shares_the_label()
{
{
let (hir_a, manifest_a) = lower(FILE_A, SOURCE_A);
let (hir_b, manifest_b) = lower(FILE_B, SOURCE_B);
let inputs: Vec<(FileId, &HirFile, &SymbolManifest)> =
vec![(FILE_A, &hir_a, &manifest_a), (FILE_B, &hir_b, &manifest_b)];
let opts = AnalysisOptions {
dialect: Dialect::Brink,
..AnalysisOptions::default()
};
let result = analysis_fixture::analyze_with_map(
&inputs,
&module_map(&[FILE_A, FILE_B]),
&opts,
true,
);
assert_eq!(
result.index.by_name.get("start.dup").map(Vec::len),
Some(2),
"both files' `start.dup` labels must coexist in the index (M-2d) — if only one \
survives, this test's premise (a real cross-module label collision) does not hold"
);
}
let isolated_id_a = {
let (hir, manifest) = lower(FILE_A, SOURCE_A);
let [stamped] = analyze_and_stamp(vec![(FILE_A, hir, manifest)])
.try_into()
.unwrap_or_else(|_| panic!("expected exactly one stamped file"));
dup_choice_id(&stamped)
};
let isolated_id_b = {
let (hir, manifest) = lower(FILE_B, SOURCE_B);
let [stamped] = analyze_and_stamp(vec![(FILE_B, hir, manifest)])
.try_into()
.unwrap_or_else(|_| panic!("expected exactly one stamped file"));
dup_choice_id(&stamped)
};
let (hir_a, manifest_a) = lower(FILE_A, SOURCE_A);
let (hir_b, manifest_b) = lower(FILE_B, SOURCE_B);
let stamped = analyze_and_stamp(vec![
(FILE_A, hir_a, manifest_a),
(FILE_B, hir_b, manifest_b),
]);
let [stamped_a, stamped_b] = <[HirFile; 2]>::try_from(stamped)
.unwrap_or_else(|_| panic!("expected exactly two stamped files"));
let joint_id_a = dup_choice_id(&stamped_a);
let joint_id_b = dup_choice_id(&stamped_b);
assert_eq!(
joint_id_a, isolated_id_a,
"file A's own `(dup)` choice must keep the SAME DefinitionId it has in isolation — \
presence of file B's coexisting `start.dup` label must not change which id file A's \
own declaration resolves to (this is the #2215 gap: the InlineConditional lambda-\
stamping traversal used to call lookup_label_id with file: None, an unscoped lookup \
that can silently prefer the wrong file's entry)"
);
assert_eq!(
joint_id_b, isolated_id_b,
"file B's own `(dup)` choice must keep the SAME DefinitionId it has in isolation — same \
self-identity requirement as file A's, in the other direction"
);
assert_ne!(
joint_id_a, joint_id_b,
"file A's and file B's `(dup)` choices are genuinely distinct containers and must never \
collapse to the same DefinitionId — the exact #2197-class collision this issue tracks, \
reached this time through the InlineConditional lambda-stamping traversal instead of \
the primary weave walk"
);
}