use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use brink_format::{DefinitionId, DefinitionTag};
use crate::FileId;
use crate::determinism::LookupMap;
use crate::hir;
use crate::symbols::{SymbolIndex, SymbolInfo, SymbolKind};
#[must_use]
pub fn root_content_scope_path(file_path: Option<&str>) -> String {
match file_path {
Some(path) if !path.is_empty() => format!("#file:{path}"),
_ => String::new(),
}
}
#[expect(
clippy::implicit_hasher,
reason = "internal API, no need to generalize"
)]
pub fn stamp_container_ids(
files: &mut [(FileId, hir::HirFile)],
index: &SymbolIndex,
file_paths: &LookupMap<FileId, String>,
) {
for (file_id, hir_file) in files {
let mut seq = 0;
let root_scope = root_content_scope_path(file_paths.get(file_id).map(String::as_str));
stamp_block(
&mut hir_file.root_content,
*file_id,
&root_scope,
"",
index,
&mut seq,
);
for cst in &mut hir_file.constants {
stamp_lambdas_in_expr(&mut cst.value, *file_id, &root_scope, "", index);
}
for var in &mut hir_file.variables {
stamp_lambdas_in_expr(&mut var.value, *file_id, &root_scope, "", index);
}
for knot in &mut hir_file.knots {
let knot_path = &knot.name.text;
let knot_scope = qualify(&root_scope, knot_path);
let mut seq = 0;
stamp_block(
&mut knot.body,
*file_id,
&knot_scope,
knot_path,
index,
&mut seq,
);
for stitch in &mut knot.stitches {
let stitch_path = format!("{knot_path}.{}", stitch.name.text);
let stitch_scope = qualify(&root_scope, &stitch_path);
let mut seq = 0;
stamp_block(
&mut stitch.body,
*file_id,
&stitch_scope,
&stitch_path,
index,
&mut seq,
);
}
}
}
}
fn stamp_block(
block: &mut hir::Block,
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
seq_counter: &mut usize,
) {
let mut choice_counter = 0usize;
let mut gather_counter = 0usize;
for stmt in &mut block.stmts {
stamp_stmt(
stmt,
file,
scope_path,
label_scope,
index,
seq_counter,
&mut choice_counter,
&mut gather_counter,
);
}
}
#[expect(
clippy::too_many_lines,
reason = "structural match over all statement types"
)]
#[expect(
clippy::too_many_arguments,
reason = "issue #2197 added `file` for label self-identity; a context struct isn't worth it \
for one more threaded parameter"
)]
fn stamp_stmt(
stmt: &mut hir::Stmt,
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
seq_counter: &mut usize,
choice_counter: &mut usize,
gather_counter: &mut usize,
) {
match stmt {
hir::Stmt::ChoiceSet(cs) => {
let gather_id = if let Some(ref label) = cs.continuation.label {
let label_path = qualify(label_scope, &label.text);
lookup_label_id(index, file, &label_path)
.unwrap_or_else(|| alloc_address(&format!("{scope_path}.g-{gather_counter}")))
} else {
alloc_address(&format!("{scope_path}.g-{gather_counter}"))
};
cs.gather_id = Some(gather_id);
cs.continuation.container_id = Some(gather_id);
*gather_counter += 1;
for choice in &mut cs.choices {
let choice_id = if let Some(ref label) = choice.label {
let label_path = qualify(label_scope, &label.text);
lookup_label_id(index, file, &label_path).unwrap_or_else(|| {
alloc_address(&format!("{scope_path}.c-{choice_counter}"))
})
} else {
alloc_address(&format!("{scope_path}.c-{choice_counter}"))
};
choice.container_id = Some(choice_id);
*choice_counter += 1;
if let Some(cond) = &mut choice.condition {
stamp_lambdas_in_expr(cond, file, scope_path, label_scope, index);
}
for c in [
&mut choice.start_content,
&mut choice.bracket_content,
&mut choice.inner_content,
]
.into_iter()
.flatten()
{
stamp_lambdas_in_content(c, file, scope_path, label_scope, index);
}
for tag in &mut choice.tags {
for part in &mut tag.parts {
stamp_lambdas_in_content_part(part, file, scope_path, label_scope, index);
}
}
let child_scope = format!("{scope_path}.c-{}", *choice_counter - 1);
let mut child_choice_counter = 0;
let mut child_gather_counter = 0;
for body_stmt in &mut choice.body.stmts {
stamp_stmt(
body_stmt,
file,
&child_scope,
label_scope,
index,
seq_counter,
&mut child_choice_counter,
&mut child_gather_counter,
);
}
}
for cont_stmt in &mut cs.continuation.stmts {
stamp_stmt(
cont_stmt,
file,
scope_path,
label_scope,
index,
seq_counter,
choice_counter,
gather_counter,
);
}
}
hir::Stmt::LabeledBlock(block) => {
if block.label.is_some() {
let label_path = block
.label
.as_ref()
.map(|l| qualify(label_scope, &l.text))
.unwrap_or_default();
let label_id = lookup_label_id(index, file, &label_path)
.unwrap_or_else(|| alloc_address(&label_path));
block.container_id = Some(label_id);
*gather_counter += 1;
}
for s in &mut block.stmts {
stamp_stmt(
s,
file,
scope_path,
label_scope,
index,
seq_counter,
choice_counter,
gather_counter,
);
}
}
hir::Stmt::Conditional(cond) => {
let cond_idx = *seq_counter;
*seq_counter += 1;
let cond_scope = format!("b-{cond_idx}");
if let hir::CondKind::Switch(e) = &mut cond.kind {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
for (branch_idx, branch) in cond.branches.iter_mut().enumerate() {
let branch_scope = if scope_path.is_empty() {
format!("{cond_scope}.{branch_idx}")
} else {
format!("{scope_path}.{cond_scope}.{branch_idx}")
};
let branch_id = alloc_address(&branch_scope);
branch.container_id = Some(branch_id);
if let Some(bc) = &mut branch.condition {
stamp_lambdas_in_expr(bc, file, scope_path, label_scope, index);
}
for s in &mut branch.body.stmts {
stamp_stmt(
s,
file,
&branch_scope,
label_scope,
index,
seq_counter,
choice_counter,
gather_counter,
);
}
}
}
hir::Stmt::Sequence(seq) => {
let seq_idx = *seq_counter;
*seq_counter += 1;
let display_name = format!("s-{seq_idx}");
let child_scope = if scope_path.is_empty() {
display_name.clone()
} else {
format!("{scope_path}.{display_name}")
};
let wrapper_id = alloc_address(&child_scope);
seq.container_id = Some(wrapper_id);
for (branch_idx, branch) in seq.branches.iter_mut().enumerate() {
let branch_path = if child_scope.is_empty() {
format!("{branch_idx}")
} else {
format!("{child_scope}.{branch_idx}")
};
let branch_id = alloc_address(&branch_path);
branch.body.container_id = Some(branch_id);
let mut bc = 0;
let mut gc = 0;
for s in &mut branch.body.stmts {
stamp_stmt(
s,
file,
&child_scope,
label_scope,
index,
seq_counter,
&mut bc,
&mut gc,
);
}
}
}
hir::Stmt::Content(content) => {
stamp_lambdas_in_content(content, file, scope_path, label_scope, index);
}
hir::Stmt::Divert(d) => {
for a in &mut d.target.args {
stamp_lambdas_in_expr(a, file, scope_path, label_scope, index);
}
}
hir::Stmt::TunnelCall(t) => {
for target in &mut t.targets {
for a in &mut target.args {
stamp_lambdas_in_expr(a, file, scope_path, label_scope, index);
}
}
}
hir::Stmt::ThreadStart(t) => {
for a in &mut t.target.args {
stamp_lambdas_in_expr(a, file, scope_path, label_scope, index);
}
}
hir::Stmt::TempDecl(t) => {
if let Some(e) = &mut t.value {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
}
hir::Stmt::Assignment(a) => {
stamp_lambdas_in_expr(&mut a.target, file, scope_path, label_scope, index);
stamp_lambdas_in_expr(&mut a.value, file, scope_path, label_scope, index);
}
hir::Stmt::Return(r) => {
if let Some(e) = &mut r.value {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
for a in &mut r.onwards_args {
stamp_lambdas_in_expr(a, file, scope_path, label_scope, index);
}
}
hir::Stmt::ExprStmt(e) | hir::Stmt::AttachElement(e) => {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
hir::Stmt::EndOfLine | hir::Stmt::EndElementRun => {}
hir::Stmt::LogicBlock(lb) => {
stamp_lambdas_in_block_stmts(&mut lb.stmts, file, scope_path, label_scope, index);
}
hir::Stmt::Await(a) => {
if let Some(c) = &mut a.condition {
stamp_lambdas_in_expr(c, file, scope_path, label_scope, index);
}
}
}
}
fn stamp_lambdas_in_expr(
expr: &mut hir::Expr,
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
) {
match expr {
hir::Expr::Lambda(l) => stamp_lambda(l, file, scope_path, label_scope, index),
hir::Expr::Prefix(_, inner) | hir::Expr::Postfix(inner, _) => {
stamp_lambdas_in_expr(inner, file, scope_path, label_scope, index);
}
hir::Expr::Infix(ie) => {
stamp_lambdas_in_expr(&mut ie.lhs, file, scope_path, label_scope, index);
stamp_lambdas_in_expr(&mut ie.rhs, file, scope_path, label_scope, index);
}
hir::Expr::Call(_, args) => {
for a in args {
stamp_lambdas_in_expr(a, file, scope_path, label_scope, index);
}
}
hir::Expr::ArrayLiteral(a) => {
for e in &mut a.elements {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
}
hir::Expr::MapLiteral(m) => {
for (k, v) in &mut m.entries {
stamp_lambdas_in_expr(k, file, scope_path, label_scope, index);
stamp_lambdas_in_expr(v, file, scope_path, label_scope, index);
}
}
hir::Expr::Index(idx) => {
stamp_lambdas_in_expr(&mut idx.base, file, scope_path, label_scope, index);
stamp_lambdas_in_expr(&mut idx.index, file, scope_path, label_scope, index);
}
hir::Expr::Range(r) => {
stamp_lambdas_in_expr(&mut r.start, file, scope_path, label_scope, index);
stamp_lambdas_in_expr(&mut r.end, file, scope_path, label_scope, index);
}
hir::Expr::StructLiteral(sl) => {
for (_, v) in &mut sl.fields {
stamp_lambdas_in_expr(v, file, scope_path, label_scope, index);
}
}
hir::Expr::FieldAccess(fa) => {
stamp_lambdas_in_expr(&mut fa.base, file, scope_path, label_scope, index);
}
hir::Expr::FnLiteral(fl) => {
for a in &mut fl.args {
stamp_lambdas_in_expr(a, file, scope_path, label_scope, index);
}
}
hir::Expr::RefArg(ra) => {
stamp_lambdas_in_expr(&mut ra.operand, file, scope_path, label_scope, index);
}
hir::Expr::String(s) => {
for part in &mut s.parts {
if let hir::StringPart::Interpolation(inner) = part {
stamp_lambdas_in_expr(inner, file, scope_path, label_scope, index);
}
}
}
hir::Expr::Fragment(stmts) => {
let mut seq = 0;
let mut cc = 0;
let mut gc = 0;
for s in stmts {
stamp_stmt(
s,
file,
scope_path,
label_scope,
index,
&mut seq,
&mut cc,
&mut gc,
);
}
}
hir::Expr::Path(_)
| hir::Expr::DivertTarget(_)
| hir::Expr::ListLiteral(_)
| hir::Expr::Int(_)
| hir::Expr::Float(_)
| hir::Expr::Bool(_)
| hir::Expr::Null => {}
}
}
fn stamp_lambda(
l: &mut hir::LambdaExpr,
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
) {
let offset = u32::from(l.ptr.text_range().start());
let own_path = qualify(scope_path, &format!("#lambda-{offset}"));
l.container_id = Some(alloc_address(&own_path));
match &mut l.body {
hir::LambdaBody::Expr(e) => stamp_lambdas_in_expr(e, file, &own_path, label_scope, index),
hir::LambdaBody::Block { stmts, tail } => {
stamp_lambdas_in_block_stmts(stmts, file, &own_path, label_scope, index);
if let Some(t) = tail {
stamp_lambdas_in_expr(t, file, &own_path, label_scope, index);
}
}
}
}
fn stamp_lambdas_in_block_stmts(
stmts: &mut [hir::BlockStmt],
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
) {
for s in stmts {
stamp_lambdas_in_block_stmt(s, file, scope_path, label_scope, index);
}
}
fn stamp_lambdas_in_block_stmt(
stmt: &mut hir::BlockStmt,
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
) {
match stmt {
hir::BlockStmt::TempDecl(t) => {
if let Some(e) = &mut t.value {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
}
hir::BlockStmt::Assignment(a) => {
stamp_lambdas_in_expr(&mut a.target, file, scope_path, label_scope, index);
stamp_lambdas_in_expr(&mut a.value, file, scope_path, label_scope, index);
}
hir::BlockStmt::Return(r) => {
if let Some(e) = &mut r.value {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
for a in &mut r.onwards_args {
stamp_lambdas_in_expr(a, file, scope_path, label_scope, index);
}
}
hir::BlockStmt::If(i) => stamp_lambdas_in_if_stmt(i, file, scope_path, label_scope, index),
hir::BlockStmt::While(w) => {
stamp_lambdas_in_expr(&mut w.condition, file, scope_path, label_scope, index);
stamp_lambdas_in_block_stmts(&mut w.body, file, scope_path, label_scope, index);
}
hir::BlockStmt::For(f) => {
stamp_lambdas_in_expr(&mut f.iterable, file, scope_path, label_scope, index);
stamp_lambdas_in_block_stmts(&mut f.body, file, scope_path, label_scope, index);
}
hir::BlockStmt::ExprStmt(e) => {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
hir::BlockStmt::Await(a) => {
if let Some(c) = &mut a.condition {
stamp_lambdas_in_expr(c, file, scope_path, label_scope, index);
}
}
hir::BlockStmt::Break(_) | hir::BlockStmt::Continue(_) => {}
}
}
fn stamp_lambdas_in_if_stmt(
i: &mut hir::IfStmt,
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
) {
stamp_lambdas_in_expr(&mut i.condition, file, scope_path, label_scope, index);
stamp_lambdas_in_block_stmts(&mut i.body, file, scope_path, label_scope, index);
match &mut i.else_branch {
Some(hir::ElseBranch::ElseIf(nested)) => {
stamp_lambdas_in_if_stmt(nested, file, scope_path, label_scope, index);
}
Some(hir::ElseBranch::Else(stmts)) => {
stamp_lambdas_in_block_stmts(stmts, file, scope_path, label_scope, index);
}
None => {}
}
}
fn stamp_lambdas_in_content(
content: &mut hir::Content,
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
) {
for part in &mut content.parts {
stamp_lambdas_in_content_part(part, file, scope_path, label_scope, index);
}
for tag in &mut content.tags {
for part in &mut tag.parts {
stamp_lambdas_in_content_part(part, file, scope_path, label_scope, index);
}
}
}
fn stamp_lambdas_in_content_part(
part: &mut hir::ContentPart,
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
) {
match part {
hir::ContentPart::Interpolation(e) => {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
hir::ContentPart::InlineConditional(cond) => {
if let hir::CondKind::Switch(e) = &mut cond.kind {
stamp_lambdas_in_expr(e, file, scope_path, label_scope, index);
}
for b in &mut cond.branches {
if let Some(c) = &mut b.condition {
stamp_lambdas_in_expr(c, file, scope_path, label_scope, index);
}
let mut seq = 0;
let mut cc = 0;
let mut gc = 0;
for s in &mut b.body.stmts {
stamp_stmt(
s,
file,
scope_path,
label_scope,
index,
&mut seq,
&mut cc,
&mut gc,
);
}
}
}
hir::ContentPart::InlineSequence(seq) => {
for b in &mut seq.branches {
let mut sc = 0;
let mut cc = 0;
let mut gc = 0;
for s in &mut b.body.stmts {
stamp_stmt(
s,
file,
scope_path,
label_scope,
index,
&mut sc,
&mut cc,
&mut gc,
);
}
}
}
hir::ContentPart::Span(span) => {
for child in &mut span.children {
stamp_lambdas_in_content_part(child, file, scope_path, label_scope, index);
}
}
hir::ContentPart::Text(_) | hir::ContentPart::Glue | hir::ContentPart::Spring => {}
}
}
fn alloc_address(path: &str) -> DefinitionId {
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
DefinitionId::new(DefinitionTag::Address, hasher.finish())
}
fn lookup_label_id(index: &SymbolIndex, file: FileId, name: &str) -> Option<DefinitionId> {
fn is_container(info: &SymbolInfo) -> bool {
matches!(
info.kind,
SymbolKind::Knot | SymbolKind::Stitch | SymbolKind::Label
)
}
index.by_name.get(name).and_then(|ids| {
if let Some(id) = ids.iter().find(|&&id| {
index
.symbols
.get(&id)
.is_some_and(|info| is_container(info) && info.file == file)
}) {
return Some(*id);
}
ids.iter()
.find(|&&id| index.symbols.get(&id).is_some_and(is_container))
.copied()
})
}
fn qualify(scope_path: &str, name: &str) -> String {
if scope_path.is_empty() {
name.to_string()
} else {
format!("{scope_path}.{name}")
}
}