use std::sync::Arc;
use brink_ir::{Block, Diagnostic, HirFile, Knot};
use brink_syntax::SegmentKind as SyntaxSegmentKind;
use rowan::TextSize;
use super::SourceFile;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, salsa::Update)]
pub(crate) enum SegmentKind {
Header,
Knot,
TopLevelStitch,
}
impl From<SyntaxSegmentKind> for SegmentKind {
fn from(kind: SyntaxSegmentKind) -> Self {
match kind {
SyntaxSegmentKind::Header => Self::Header,
SyntaxSegmentKind::Knot => Self::Knot,
SyntaxSegmentKind::TopLevelStitch => Self::TopLevelStitch,
}
}
}
pub(crate) fn segment_heap_size(fields: &(SegmentKind, String, Option<u32>, u32)) -> usize {
fields.1.capacity()
}
#[salsa::tracked(heap_size = segment_heap_size)]
pub(crate) struct FileSegment<'db> {
pub kind: SegmentKind,
#[returns(ref)]
pub text: String,
pub header_offset: Option<u32>,
#[tracked]
pub offset: u32,
}
#[salsa::tracked(returns(ref))]
pub(crate) fn file_segments_query(
db: &dyn salsa::Database,
file: SourceFile,
) -> Vec<FileSegment<'_>> {
let text = file.text(db);
brink_syntax::segment_file(text)
.into_iter()
.map(|seg| {
let start = u32::from(seg.range.start());
let slice =
&text[usize::from(seg.lowered_range.start())..usize::from(seg.lowered_range.end())];
FileSegment::new(
db,
SegmentKind::from(seg.kind),
slice.to_owned(),
seg.header_start.map(|h| u32::from(h) - start),
start,
)
})
.collect()
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct LoweredSegment {
pub knot_entries: Vec<(Option<Knot>, Vec<Diagnostic>)>,
pub top_level_knots: Vec<Knot>,
pub root_content: Block,
pub decl_hir: HirFile,
pub decl_diags: Vec<Diagnostic>,
pub content_diags: Vec<Diagnostic>,
pub parse_errors: Vec<Diagnostic>,
}
#[salsa::tracked(returns(ref))]
pub(crate) fn segment_lowered_query<'db>(
db: &'db dyn salsa::Database,
file: SourceFile,
segment: FileSegment<'db>,
) -> Arc<LoweredSegment> {
let file_id = file.file_id(db);
let parse = brink_syntax::parse(segment.text(db));
let tree = parse.tree();
let knot_entries: Vec<_> = tree
.knots()
.map(|knot_ast| brink_ir::lower_single_knot(file_id, &knot_ast))
.collect();
let (root_content, top_level_knots, content_diags) = brink_ir::lower_top_level(file_id, &tree);
let (decl_hir, decl_diags) = brink_ir::lower_declarations(file_id, &tree);
let parse_errors = parse
.errors()
.iter()
.map(|e| Diagnostic {
file: file_id,
range: e.range,
message: e.message.clone(),
code: brink_ir::DiagnosticCode::E037,
})
.collect();
Arc::new(LoweredSegment {
knot_entries,
top_level_knots,
root_content,
decl_hir,
decl_diags,
content_diags,
parse_errors,
})
}
pub(crate) fn assemble_lowered_file(
db: &dyn salsa::Database,
file: SourceFile,
) -> super::LoweredFile {
use brink_ir::hir::rebase::Rebase as _;
let file_id = file.file_id(db);
let segments = file_segments_query(db, file);
let products: Vec<(TextSize, &Arc<LoweredSegment>)> = segments
.iter()
.map(|seg| {
(
TextSize::from(seg.offset(db)),
segment_lowered_query(db, file, *seg),
)
})
.collect();
let Some(((first_delta, first_product), rest)) = products.split_first() else {
return super::lower_file(file_id, &brink_syntax::parse(""));
};
let mut hir = first_product.decl_hir.clone();
hir.rebase(*first_delta, file_id);
let mut diagnostics: Vec<Diagnostic> = {
let mut d = first_product.decl_diags.clone();
for diag in &mut d {
diag.rebase(*first_delta, file_id);
}
d
};
for (delta, product) in rest {
let mut decls = product.decl_hir.clone();
decls.rebase(*delta, file_id);
hir.variables.extend(decls.variables);
hir.constants.extend(decls.constants);
hir.lists.extend(decls.lists);
hir.structs.extend(decls.structs);
hir.externals.extend(decls.externals);
hir.includes.extend(decls.includes);
hir.imports.extend(decls.imports);
hir.visibility.extend(decls.visibility);
hir.was_directives.extend(decls.was_directives);
if decls.module.is_some() {
hir.module = decls.module;
}
let mut d = product.decl_diags.clone();
for diag in &mut d {
diag.rebase(*delta, file_id);
}
diagnostics.append(&mut d);
}
if let Some((delta, product)) = products.first() {
let mut root = product.root_content.clone();
root.rebase(*delta, file_id);
hir.root_content = root;
}
for (delta, product) in &products {
for (knot, _) in &product.knot_entries {
if let Some(knot) = knot {
let mut k = knot.clone();
k.rebase(*delta, file_id);
hir.knots.push(k);
}
}
}
for (delta, product) in &products {
for knot in &product.top_level_knots {
let mut k = knot.clone();
k.rebase(*delta, file_id);
hir.knots.push(k);
}
}
for (delta, product) in &products {
let mut d = product.content_diags.clone();
for diag in &mut d {
diag.rebase(*delta, file_id);
}
diagnostics.append(&mut d);
for (_, knot_diags) in &product.knot_entries {
let mut d = knot_diags.clone();
for diag in &mut d {
diag.rebase(*delta, file_id);
}
diagnostics.append(&mut d);
}
}
for (delta, product) in &products {
let mut d = product.parse_errors.clone();
for diag in &mut d {
diag.rebase(*delta, file_id);
}
diagnostics.append(&mut d);
}
let manifest = brink_ir::symbols::project_manifest(&hir);
diagnostics.extend(brink_analyzer::check_anonymous_stateful(file_id, &hir));
let file_len = segments.last().map_or(TextSize::from(0), |seg| {
TextSize::from(seg.offset(db)) + TextSize::of(seg.text(db).as_str())
});
let admission = brink_analyzer::validate_admission(file_id, &hir, &manifest, file_len);
super::LoweredFile {
hir,
manifest,
diagnostics,
admission,
}
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn segment_projection_query<'db>(
db: &'db dyn salsa::Database,
file: SourceFile,
segment: FileSegment<'db>,
) -> super::NoEqArc<brink_ir::hir::projection::ProjectionParts> {
let product = segment_lowered_query(db, file, segment);
let hir = fragment_hir(product);
super::NoEqArc(Arc::new(brink_ir::hir::projection::project_walk_parts(
&hir,
segment.text(db),
)))
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn projection_query(
db: &dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
) -> super::NoEqArc<brink_ir::hir::projection::Projection> {
use brink_ir::hir::projection as proj;
let source = file.text(db);
let lowered = super::lowered_query(db, project, file);
let decl_parts = proj::project_file_decl_parts(&lowered.hir, source);
let mut spans = decl_parts.spans;
let mut join_keys: Vec<(Option<proj::JoinKey>, TextSize)> = decl_parts
.join_keys
.into_iter()
.map(|k| (k, TextSize::from(0)))
.collect();
let mut option_paths: std::collections::BTreeMap<u32, Vec<u32>> = decl_parts.option_paths;
let mut handle_offset = decl_parts.handle_count;
let append = |parts: &proj::ProjectionParts,
delta: TextSize,
spans: &mut Vec<proj::ProjectedSpan>,
join_keys: &mut Vec<(Option<proj::JoinKey>, TextSize)>,
option_paths: &mut std::collections::BTreeMap<u32, Vec<u32>>,
handle_offset: &mut u32| {
for (span, key) in parts.spans.iter().zip(&parts.join_keys) {
let mut s = *span;
s.range += delta;
if let Some(h) = s.handle.as_mut() {
*h += *handle_offset;
}
spans.push(s);
join_keys.push((*key, delta));
}
for (handle, path) in &parts.option_paths {
option_paths.insert(handle + *handle_offset, path.clone());
}
*handle_offset += parts.handle_count;
};
if super::file_language(file.path(db)) == super::Language::Ink {
let segs = file_segments_query(db, file);
for pass_stitches in [false, true] {
for seg in segs {
let is_stitch = seg.kind(db) == SegmentKind::TopLevelStitch;
if is_stitch != pass_stitches {
continue;
}
let parts = &segment_projection_query(db, file, *seg).0;
append(
parts,
TextSize::from(seg.offset(db)),
&mut spans,
&mut join_keys,
&mut option_paths,
&mut handle_offset,
);
}
}
} else {
let parts = proj::project_walk_parts(&lowered.hir, source);
append(
&parts,
TextSize::from(0),
&mut spans,
&mut join_keys,
&mut option_paths,
&mut handle_offset,
);
}
let resolved = super::resolutions_index_query(db, project);
let file_id = file.file_id(db);
let mut decl_ids: std::collections::BTreeMap<(u32, u32), brink_format::DefinitionId> =
std::collections::BTreeMap::new();
for info in resolved.index.symbols.values() {
if info.file == file_id {
decl_ids.insert(proj::range_key(info.range), info.id);
}
}
let mut ref_targets: std::collections::BTreeMap<(u32, u32), brink_format::DefinitionId> =
std::collections::BTreeMap::new();
for r in &resolved.resolutions {
if r.file == file_id {
ref_targets.insert(proj::range_key(r.range), r.target);
}
}
for (span, (key, delta)) in spans.iter_mut().zip(&join_keys) {
match key {
Some(proj::JoinKey::Decl(r)) => {
span.def_id = decl_ids.get(&proj::range_key(*r + *delta)).copied();
}
Some(proj::JoinKey::Ref(r)) => {
span.target_id = ref_targets.get(&proj::range_key(*r + *delta)).copied();
}
None => {}
}
}
let lines = proj::build_line_stacks(&spans, source);
super::NoEqArc(Arc::new(proj::Projection {
spans,
lines,
option_paths,
}))
}
fn fragment_hir(product: &LoweredSegment) -> HirFile {
let mut hir = product.decl_hir.clone();
hir.knots = product
.knot_entries
.iter()
.filter_map(|(knot, _)| knot.clone())
.collect();
hir.knots.extend(product.top_level_knots.iter().cloned());
hir.root_content = product.root_content.clone();
hir
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn segment_line_contexts_query<'db>(
db: &'db dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
segment: FileSegment<'db>,
) -> super::NoEqArc<Vec<brink_ir::hir::line_context::LineContext>> {
use brink_ir::hir::{line_context as lc, projection as proj};
let product = segment_lowered_query(db, file, segment);
let text = segment.text(db);
let parse = brink_syntax::parse(text);
let root = parse.syntax();
let hir = fragment_hir(product);
let decl_parts = proj::project_file_decl_parts(&hir, text);
let walk = &segment_projection_query(db, file, segment).0;
let mut spans = decl_parts.spans;
spans.extend(walk.spans.iter().copied());
let mut option_paths = decl_parts.option_paths;
for (handle, path) in &walk.option_paths {
option_paths.insert(*handle, path.clone());
}
let lines = proj::build_line_stacks(&spans, text);
let projection = proj::Projection {
spans,
lines,
option_paths,
};
let contexts = match &super::resolved_dialect_query(db, project).0 {
Some(dialect) => lc::line_contexts_with_dialect(text, &root, &projection, dialect),
None => lc::line_contexts(text, &root, &projection),
};
super::NoEqArc(Arc::new(contexts))
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn line_contexts_query(
db: &dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
) -> super::NoEqArc<Vec<brink_ir::hir::line_context::LineContext>> {
use brink_ir::hir::line_context as lc;
let source = file.text(db);
if super::file_language(file.path(db)) != super::Language::Ink {
let projection = &projection_query(db, project, file).0;
let parse = super::parse_native_query(db, file);
let root = parse.syntax();
let contexts = match &super::resolved_dialect_query(db, project).0 {
Some(dialect) => {
lc::line_contexts_with_dialect_native(source, &root, projection, dialect)
}
None => lc::line_contexts_native(source, &root, projection),
};
return super::NoEqArc(Arc::new(contexts));
}
let owned = segment_owned_lines(db, file);
let mut out: Vec<lc::LineContext> = Vec::with_capacity(owned.total_lines);
for i in 0..owned.segments.len() {
out.extend(segment_line_contexts_slice(db, project, file, &owned, i));
}
super::NoEqArc(Arc::new(out))
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn file_resolution_kinds_query(
db: &dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
) -> super::NoEqArc<std::collections::BTreeMap<(u32, u32), u32>> {
use brink_ir::hir::projection::range_key;
let resolved = super::resolutions_index_query(db, project);
let file_id = file.file_id(db);
let mut map = std::collections::BTreeMap::new();
for rref in &resolved.resolutions {
if rref.file == file_id
&& let Some(info) = resolved.index.symbols.get(&rref.target)
{
map.insert(range_key(rref.range), info.kind.to_u32());
}
}
super::NoEqArc(Arc::new(map))
}
#[salsa::tracked(returns(ref))]
pub(crate) fn segment_resolution_kinds_query<'db>(
db: &'db dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
segment: FileSegment<'db>,
) -> Vec<(u32, u32, u32)> {
let map = &file_resolution_kinds_query(db, project, file).0;
let start = segment.offset(db);
let end = start + u32::try_from(segment.text(db).len()).unwrap_or(u32::MAX);
map.range((start, 0)..(end, u32::MAX))
.filter(|((_, e), _)| *e <= end)
.map(|((s, e), kind)| (s - start, e - start, *kind))
.collect()
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn segment_semantic_tokens_query<'db>(
db: &'db dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
segment: FileSegment<'db>,
) -> super::NoEqArc<Vec<brink_ir::semantic_tokens::RawToken>> {
let text = segment.text(db);
let parse = brink_syntax::parse(text);
let root = parse.syntax();
let kinds: std::collections::BTreeMap<(u32, u32), brink_ir::SymbolKind> =
segment_resolution_kinds_query(db, project, file, segment)
.iter()
.filter_map(|(s, e, k)| brink_ir::SymbolKind::from_u32(*k).map(|k| ((*s, *e), k)))
.collect();
super::NoEqArc(Arc::new(brink_ir::semantic_tokens::tokens_with_kinds(
text, &root, &kinds,
)))
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn segment_semantic_tokens_classifier_query<'db>(
db: &'db dyn salsa::Database,
file: SourceFile,
segment: FileSegment<'db>,
) -> super::NoEqArc<Vec<brink_ir::semantic_tokens::RawToken>> {
let _ = file;
let text = segment.text(db);
let parse = brink_syntax::parse(text);
let root = parse.syntax();
let kinds = std::collections::BTreeMap::new();
super::NoEqArc(Arc::new(brink_ir::semantic_tokens::tokens_with_kinds(
text, &root, &kinds,
)))
}
#[salsa::tracked(returns(ref), no_eq)]
pub(crate) fn semantic_tokens_query(
db: &dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
) -> super::NoEqArc<Vec<brink_ir::semantic_tokens::RawToken>> {
let source = file.text(db);
if super::file_language(file.path(db)) != super::Language::Ink {
let parse = super::parse_native_query(db, file);
let kinds: std::collections::BTreeMap<(u32, u32), brink_ir::SymbolKind> =
file_resolution_kinds_query(db, project, file)
.0
.iter()
.filter_map(|((s, e), k)| brink_ir::SymbolKind::from_u32(*k).map(|k| ((*s, *e), k)))
.collect();
return super::NoEqArc(Arc::new(
brink_ir::semantic_tokens::tokens_with_kinds_native(source, &parse.syntax(), &kinds),
));
}
let owned = segment_owned_lines(db, file);
let mut out: Vec<brink_ir::semantic_tokens::RawToken> = Vec::new();
for i in 0..owned.segments.len() {
let owned_from = owned.segments[i].owned_from;
for mut t in segment_semantic_tokens_slice(db, project, file, &owned, i) {
t.line += u32::try_from(owned_from).unwrap_or(u32::MAX);
out.push(t);
}
}
super::NoEqArc(Arc::new(out))
}
pub(crate) struct SegmentLines<'db> {
pub seg: FileSegment<'db>,
pub owned_from: usize,
pub seg_start_line: usize,
pub cut_col_utf16: u32,
}
pub(crate) struct OwnedLines<'db> {
pub segments: Vec<SegmentLines<'db>>,
pub total_lines: usize,
}
impl OwnedLines<'_> {
pub fn owned_to(&self, i: usize) -> usize {
self.segments
.get(i + 1)
.map_or(self.total_lines, |n| n.owned_from)
}
}
pub(crate) fn segment_owned_lines(db: &dyn salsa::Database, file: SourceFile) -> OwnedLines<'_> {
let source = file.text(db);
let mut line_starts: Vec<u32> = vec![0];
for (i, b) in source.bytes().enumerate() {
if b == b'\n' {
line_starts.push(u32::try_from(i + 1).unwrap_or(u32::MAX));
}
}
let total_lines = brink_ir::hir::line_context::line_count_for(source);
let line_of = |offset: u32| -> usize {
match line_starts.binary_search(&offset) {
Ok(l) => l,
Err(next) => next - 1,
}
};
let segs = file_segments_query(db, file);
let mut segments = Vec::with_capacity(segs.len());
for seg in segs {
let start = seg.offset(db);
let l = line_of(start);
let prefix = &source[line_starts[l] as usize..start as usize];
let only_trivia = prefix.chars().all(|c| c.is_whitespace() || c == '\u{feff}');
segments.push(SegmentLines {
seg: *seg,
owned_from: if only_trivia { l } else { l + 1 },
seg_start_line: l,
cut_col_utf16: u32::try_from(prefix.encode_utf16().count()).unwrap_or(u32::MAX),
});
}
OwnedLines {
segments,
total_lines,
}
}
pub(crate) fn segment_line_contexts_slice(
db: &dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
owned: &OwnedLines<'_>,
i: usize,
) -> Vec<brink_ir::hir::line_context::LineContext> {
let sl = &owned.segments[i];
let ctxs = &segment_line_contexts_query(db, project, file, sl.seg).0;
(sl.owned_from..owned.owned_to(i))
.map(|line| {
ctxs.get(line - sl.seg_start_line)
.cloned()
.unwrap_or_default()
})
.collect()
}
pub(crate) fn segment_semantic_tokens_slice(
db: &dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
owned: &OwnedLines<'_>,
i: usize,
) -> Vec<brink_ir::semantic_tokens::RawToken> {
segment_semantic_tokens_slice_with(db, project, file, owned, i, false)
}
pub(crate) fn segment_semantic_tokens_slice_with(
db: &dyn salsa::Database,
project: super::ProjectInput,
file: SourceFile,
owned: &OwnedLines<'_>,
i: usize,
classifier_only: bool,
) -> Vec<brink_ir::semantic_tokens::RawToken> {
let sl = &owned.segments[i];
let owned_from = sl.owned_from;
let owned_to = owned.owned_to(i);
let mut out: Vec<brink_ir::semantic_tokens::RawToken> = Vec::new();
let push_from = |sl: &SegmentLines<'_>, merge_boundary_only: bool, out: &mut Vec<_>| {
let tokens = if classifier_only {
&segment_semantic_tokens_classifier_query(db, file, sl.seg).0
} else {
&segment_semantic_tokens_query(db, project, file, sl.seg).0
};
for t in tokens.iter() {
let file_line = sl.seg_start_line + t.line as usize;
let in_window = file_line >= owned_from && file_line < owned_to;
let boundary = merge_boundary_only && t.line == 0;
if merge_boundary_only {
if !boundary || !in_window {
continue;
}
} else {
let self_owned = file_line >= sl.owned_from;
if !in_window || !self_owned {
continue;
}
}
let mut t = t.clone();
t.line = u32::try_from(file_line - owned_from).unwrap_or(u32::MAX);
if file_line == sl.seg_start_line {
t.start_char += sl.cut_col_utf16;
}
out.push(t);
}
};
push_from(sl, false, &mut out);
if let Some(next) = owned.segments.get(i + 1)
&& next.owned_from > next.seg_start_line
{
push_from(next, true, &mut out);
}
out
}
#[cfg(test)]
mod tests {
use salsa::plumbing::AsId;
use super::{FileSegment, SegmentKind, file_segments_query};
use crate::ProjectDb;
const BASE: &str = "\
VAR x = 1
Intro prose.
== alpha ==
Alpha body.
== beta ==
Beta body.
== gamma ==
Gamma body.
";
fn segments<'db>(db: &'db ProjectDb, path: &str) -> &'db [FileSegment<'db>] {
let id = db.file_id(path).expect("file is loaded");
let file = db.test_source_file(id).expect("source file exists");
file_segments_query(db.test_salsa(), file)
}
#[test]
fn segments_match_the_segmenter() {
let mut db = ProjectDb::new();
db.update_file("a.ink", BASE.to_owned());
let expected = brink_syntax::segment_file(BASE);
let got = segments(&db, "a.ink");
assert_eq!(got.len(), expected.len());
let salsa = db.test_salsa();
let mut pos = 0u32;
for (seg, exp) in got.iter().zip(&expected) {
assert_eq!(seg.kind(salsa), SegmentKind::from(exp.kind));
assert_eq!(seg.offset(salsa), u32::from(exp.range.start()));
assert_eq!(seg.offset(salsa), pos, "segments must tile the file");
assert_eq!(seg.text(salsa), &BASE[exp.range]);
pos += u32::try_from(seg.text(salsa).len()).unwrap_or(u32::MAX);
}
assert_eq!(pos, u32::try_from(BASE.len()).unwrap_or(u32::MAX));
}
#[test]
fn shift_edit_preserves_every_unedited_segment_identity() {
let mut db = ProjectDb::new();
db.update_file("a.ink", BASE.to_owned());
let before: Vec<salsa::Id> = segments(&db, "a.ink").iter().map(AsId::as_id).collect();
assert_eq!(before.len(), 4, "header + three knots");
let edited = BASE.replace("Beta body.\n", "Beta body.\nA second beta line.\n");
db.update_file("a.ink", edited.clone());
let after: Vec<salsa::Id> = segments(&db, "a.ink").iter().map(AsId::as_id).collect();
assert_eq!(before[0], after[0], "header identity must survive");
assert_eq!(before[1], after[1], "alpha identity must survive");
assert_ne!(before[2], after[2], "edited beta must get a new identity");
assert_eq!(
before[3], after[3],
"gamma shifted but its content is unchanged — identity must survive"
);
let gamma = segments(&db, "a.ink")[3];
let expected_offset =
u32::try_from(edited.find("== gamma ==").expect("gamma exists")).unwrap_or(u32::MAX);
assert_eq!(
gamma.offset(db.test_salsa()),
expected_offset,
"the tracked offset field must reflect the shifted position"
);
}
#[test]
fn duplicate_knots_are_distinct_and_stable() {
const DUP: &str =
"Intro.\n== twin ==\nSame body.\n== twin ==\nSame body.\n== tail ==\nTail body.\n";
let mut db = ProjectDb::new();
db.update_file("a.ink", DUP.to_owned());
let before: Vec<salsa::Id> = segments(&db, "a.ink").iter().map(AsId::as_id).collect();
assert_eq!(before.len(), 4);
assert_ne!(
before[1], before[2],
"identical twins are distinct segments"
);
let edited = DUP.replace("Tail body.\n", "Tail body, edited.\n");
db.update_file("a.ink", edited);
let after: Vec<salsa::Id> = segments(&db, "a.ink").iter().map(AsId::as_id).collect();
assert_eq!(
before[1], after[1],
"first twin stable across unrelated edit"
);
assert_eq!(
before[2], after[2],
"second twin stable across unrelated edit"
);
assert_ne!(before[3], after[3], "edited tail gets a new identity");
}
#[expect(
clippy::too_many_lines,
reason = "the roads-agree gate: one comparison block per parity surface \
(lowering, projection, contexts, tokens, delta reconstruction) — \
splitting them would obscure that they all run per fixture"
)]
fn assert_roads_agree(source: &str, label: &str) {
let mut db = ProjectDb::new();
let id = db.update_file("gate.ink", source.to_owned());
let file = db.test_source_file(id).expect("file exists");
let salsa = db.test_salsa();
let assembled = super::assemble_lowered_file(salsa, file);
let parse = brink_syntax::parse(source);
let oracle = crate::queries::lower_file(id, &parse);
assert_eq!(assembled.hir, oracle.hir, "HIR diverged: {label}");
assert_eq!(
assembled.manifest, oracle.manifest,
"manifest diverged: {label}"
);
assert_eq!(
assembled.admission, oracle.admission,
"admission diverged: {label}"
);
{
use brink_ir::hir::projection as proj;
let analysis = db.analysis();
let mut decl_ids = std::collections::BTreeMap::new();
for info in analysis.index.symbols.values() {
if info.file == id {
decl_ids.insert(proj::range_key(info.range), info.id);
}
}
let mut ref_targets = std::collections::BTreeMap::new();
for r in &analysis.resolutions {
if r.file == id {
ref_targets.insert(proj::range_key(r.range), r.target);
}
}
let oracle_projection =
proj::project_with_maps(&oracle.hir, source, &decl_ids, &ref_targets);
let assembled_projection = db.projection(id).expect("projection");
assert_eq!(
*assembled_projection, oracle_projection,
"projection diverged: {label}"
);
}
{
use brink_ir::hir::line_context as lc;
let root = brink_syntax::parse(source).syntax();
let projection = db.projection(id).expect("projection");
let oracle_contexts = lc::line_contexts(source, &root, &projection);
let assembled_contexts = db.line_contexts(id).expect("contexts");
assert_eq!(
*assembled_contexts, oracle_contexts,
"line contexts diverged: {label}"
);
}
{
use brink_ir::hir::projection::range_key;
let analysis = db.analysis();
let mut kinds = std::collections::BTreeMap::new();
for rref in &analysis.resolutions {
if rref.file == id
&& let Some(info) = analysis.index.symbols.get(&rref.target)
{
kinds.insert(range_key(rref.range), info.kind);
}
}
let root = brink_syntax::parse(source).syntax();
let oracle_tokens = brink_ir::semantic_tokens::tokens_with_kinds(source, &root, &kinds);
let assembled_tokens = db.semantic_tokens(id).expect("tokens");
assert_eq!(
*assembled_tokens, oracle_tokens,
"semantic tokens diverged: {label}"
);
}
if let Some((manifest, _total)) = db.segment_manifest(id) {
let mut contexts = Vec::new();
let mut tokens = Vec::new();
for (key, owned_from) in &manifest {
contexts.extend(db.segment_line_contexts_slice(id, key).expect("live key"));
for mut t in db.segment_semantic_tokens_slice(id, key).expect("live key") {
t.line += owned_from;
tokens.push(t);
}
}
assert_eq!(
contexts,
*db.line_contexts(id).expect("contexts"),
"delta-reconstructed contexts diverged: {label}"
);
assert_eq!(
tokens,
*db.semantic_tokens(id).expect("tokens"),
"delta-reconstructed tokens diverged: {label}"
);
}
let mut a = assembled.diagnostics.clone();
let mut b = oracle.diagnostics.clone();
let key = |d: &brink_ir::Diagnostic| {
(
d.file.0,
u32::from(d.range.start()),
u32::from(d.range.end()),
format!("{:?}", d.code),
d.message.clone(),
)
};
a.sort_by_key(key);
b.sort_by_key(key);
assert_eq!(a, b, "diagnostic multiset diverged: {label}");
}
#[test]
fn roads_agree_on_crafted_fixtures() {
let fixtures: &[(&str, &str)] = &[
("base", BASE),
("empty", ""),
("header only", "VAR x = 1\nJust prose.\n"),
(
"knot-nested globals",
"Intro.\n== alpha ==\nBody.\nVAR nested = 3\nMore body.\n== beta ==\n{nested}\n-> END\n",
),
(
"top-level stitch + knot stitch",
"= lobby\nStitch content.\n== alpha ==\nBody.\n= inner\nInner.\n-> END\n",
),
(
"doc blocks travel",
"/// About alpha.\n== alpha ==\nBody.\n/// About beta.\n/// @kind scene\n== beta ==\nBody.\n",
),
(
"module directives",
"#@module(alpha)\n#@was(old_alpha)\n== greet ==\nHello.\n-> END\n",
),
(
"orphan was before knot",
"#@was(ghost)\n== greet ==\nHello.\n-> END\n",
),
(
"todo notes both levels",
"TODO: top note\nProse.\n== alpha ==\nTODO: knot note\nBody.\n",
),
(
"parse errors in knot",
"== alpha ==\n{ unclosed conditional\nBody.\n== beta ==\nFine.\n-> END\n",
),
(
"block comment hiding header",
"Intro.\n/* dead\n== ghost ==\n*/\n== alpha ==\nBody.\n-> DONE\n",
),
(
"choices and weave",
"== alpha ==\n* [One] First.\n* [Two] Second. # aside\n- Gather.\n-> DONE\n",
),
];
for (label, source) in fixtures {
assert_roads_agree(source, label);
}
}
#[test]
fn dialect_line_contexts_agree_with_whole_file() {
use brink_ir::hir::line_context as lc;
let config = brink_ir::DialogueDialect::default();
let source = "Intro prose.
== alpha ==
@Alice:<>
Hello there.
Second dialogue line.
Plain narrative after blank.
== beta ==
@Bob:<>
Hi.
-> END
";
let mut db = ProjectDb::new();
db.set_dialect(Some(config));
let id = db.update_file("gate.ink", source.to_owned());
let root = brink_syntax::parse(source).syntax();
let projection = db.projection(id).expect("projection");
let dialect = std::sync::Arc::clone(db.resolved_dialect().expect("dialect compiles"));
let oracle = lc::line_contexts_with_dialect(source, &root, &projection, &dialect);
let assembled = db.line_contexts(id).expect("contexts");
assert_eq!(*assembled, oracle, "dialect contexts diverged");
assert!(
oracle.iter().any(|c| c.dialect.is_some()),
"fixture must actually exercise dialect classification"
);
}
#[test]
fn manifest_keys_survive_shift_edits() {
let mut db = ProjectDb::new();
db.update_file("a.ink", BASE.to_owned());
let id = db.file_id("a.ink").expect("loaded");
let (before, _) = db.segment_manifest(id).expect("manifest");
assert_eq!(before.len(), 4, "header + three knots");
let edited = BASE.replace("Beta body.\n", "Beta body.\nA second beta line.\n");
db.update_file("a.ink", edited);
let (after, _) = db.segment_manifest(id).expect("manifest");
assert_eq!(before[0].0, after[0].0, "header key survives");
assert_eq!(before[1].0, after[1].0, "alpha key survives");
assert_ne!(before[2].0, after[2].0, "edited beta gets a new key");
assert_eq!(
before[3].0, after[3].0,
"gamma shifted but unchanged — its key (and any cached slice) survives"
);
assert_ne!(
before[3].1, after[3].1,
"gamma's owned-from line DID move — the manifest carries the shift"
);
}
#[test]
fn roads_agree_across_the_tier_corpora() {
fn collect(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
collect(&path, out);
} else if path.extension().is_some_and(|e| e == "ink") {
out.push(path);
}
}
}
let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.join("tests");
let mut dirs = vec![root.join("tier1"), root.join("tier2"), root.join("tier3")];
if std::env::var("BRINK_SEGMENT_SWEEP_FULL").is_ok() {
dirs.push(root.join("tests_github"));
dirs.push(root.join("tests_patched"));
}
let mut files = Vec::new();
for dir in &dirs {
collect(dir, &mut files);
}
files.sort();
assert!(
files.len() >= 200,
"corpus discovery looks broken: {} files",
files.len()
);
for path in &files {
let Ok(source) = std::fs::read_to_string(path) else {
continue;
};
assert_roads_agree(&source, &path.display().to_string());
}
}
#[test]
fn knot_interior_edit_relowers_only_that_segment() {
fn product_ptrs(db: &ProjectDb, path: &str) -> Vec<*const super::LoweredSegment> {
let id = db.file_id(path).expect("loaded");
let file = db.test_source_file(id).expect("file");
let salsa = db.test_salsa();
super::file_segments_query(salsa, file)
.iter()
.map(|seg| std::sync::Arc::as_ptr(super::segment_lowered_query(salsa, file, *seg)))
.collect()
}
let mut db = ProjectDb::new();
db.update_file("a.ink", BASE.to_owned());
let before = product_ptrs(&db, "a.ink");
assert_eq!(before.len(), 4, "header + three knots");
let edited = BASE.replace("Beta body.\n", "Beta body.\nA second beta line.\n");
db.update_file("a.ink", edited);
let after = product_ptrs(&db, "a.ink");
assert_eq!(before[0], after[0], "header memo must be untouched");
assert_eq!(before[1], after[1], "alpha memo must be untouched");
assert_ne!(before[2], after[2], "edited beta must re-lower");
assert_eq!(
before[3], after[3],
"gamma shifted but content-unchanged — its memo must be untouched"
);
}
}