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 = if choice.label.is_some() {
format!("#lbl:{choice_id}")
} else {
format!("{scope_path}.c-{}", *choice_counter - 1)
};
let mut child_choice_counter = 0;
let mut child_gather_counter = 0;
let mut child_seq_counter = 0;
for body_stmt in &mut choice.body.stmts {
stamp_stmt(
body_stmt,
file,
&child_scope,
label_scope,
index,
&mut child_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;
let child_scope = format!("#lbl:{label_id}");
let mut child_seq = 0;
let mut child_choice = 0;
let mut child_gather = 0;
for s in &mut block.stmts {
stamp_stmt(
s,
file,
&child_scope,
label_scope,
index,
&mut child_seq,
&mut child_choice,
&mut child_gather,
);
}
} else {
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 bseq = 0;
let mut bc = 0;
let mut gc = 0;
for s in &mut branch.body.stmts {
stamp_stmt(
s,
file,
&branch_path,
label_scope,
index,
&mut bseq,
&mut bc,
&mut gc,
);
}
}
}
hir::Stmt::Content(content) => {
for part in &mut content.parts {
stamp_inline_part(
part,
file,
scope_path,
label_scope,
index,
seq_counter,
choice_counter,
gather_counter,
);
}
for part in &mut content.parts {
if !matches!(
part,
hir::ContentPart::InlineSequence(_) | hir::ContentPart::InlineConditional(_)
) {
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);
}
}
}
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);
}
}
}
}
#[expect(
clippy::too_many_arguments,
reason = "same threaded walk state as `stamp_stmt`, same #2197 tradeoff"
)]
fn stamp_inline_part(
part: &mut hir::ContentPart,
file: FileId,
scope_path: &str,
label_scope: &str,
index: &SymbolIndex,
seq_counter: &mut usize,
choice_counter: &mut usize,
gather_counter: &mut usize,
) {
match part {
hir::ContentPart::InlineSequence(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
} else {
format!("{scope_path}.{display_name}")
};
seq.container_id = Some(alloc_address(&child_scope));
for (branch_idx, branch) in seq.branches.iter_mut().enumerate() {
let branch_path = format!("{child_scope}.{branch_idx}");
branch.body.container_id = Some(alloc_address(&branch_path));
let mut bseq = 0;
let mut bc = 0;
let mut gc = 0;
for s in &mut branch.body.stmts {
stamp_stmt(
s,
file,
&branch_path,
label_scope,
index,
&mut bseq,
&mut bc,
&mut gc,
);
}
}
}
hir::ContentPart::InlineConditional(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}")
};
branch.container_id = Some(alloc_address(&branch_scope));
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,
);
}
}
}
_ => {}
}
}
#[must_use]
pub(crate) fn derive_id(base: DefinitionId, kind: &str, salt: u64) -> DefinitionId {
let mut hasher = DefaultHasher::new();
Hash::hash(&base, &mut hasher);
Hash::hash(kind, &mut hasher);
Hash::hash(&salt, &mut hasher);
DefinitionId::new(DefinitionTag::Address, hasher.finish())
}
pub(crate) fn rederive_cloned_parts(parts: &mut [hir::ContentPart], salt: u64) {
rederive_parts_inner(parts, salt);
}
fn rederive_cloned_sequence(seq: &mut hir::Sequence, salt: u64) {
if let Some(id) = seq.container_id {
seq.counter_id.get_or_insert(id);
seq.container_id = Some(derive_id(id, "clone", salt));
}
for branch in &mut seq.branches {
if let Some(id) = branch.body.container_id {
branch.body.container_id = Some(derive_id(id, "clone", salt));
}
rederive_block_inner(&mut branch.body, salt);
}
}
fn rederive_parts_inner(parts: &mut [hir::ContentPart], salt: u64) {
if salt == 0 {
return;
}
for part in parts {
match part {
hir::ContentPart::InlineSequence(seq) => rederive_cloned_sequence(seq, salt),
hir::ContentPart::InlineConditional(cond) => {
if let hir::CondKind::Switch(e) = &mut cond.kind {
rederive_cloned_expr(e, salt);
}
for branch in &mut cond.branches {
if let Some(id) = branch.container_id {
branch.container_id = Some(derive_id(id, "clone", salt));
}
if let Some(c) = &mut branch.condition {
rederive_cloned_expr(c, salt);
}
rederive_block_inner(&mut branch.body, salt);
}
}
hir::ContentPart::Interpolation(e) => rederive_cloned_expr(e, salt),
hir::ContentPart::Span(span) => {
rederive_parts_inner(&mut span.children, salt);
}
hir::ContentPart::Text(_) | hir::ContentPart::Glue | hir::ContentPart::Spring => {}
}
}
}
fn rederive_block_inner(block: &mut hir::Block, salt: u64) {
if let Some(id) = block.container_id {
block.container_id = Some(derive_id(id, "clone", salt));
}
for stmt in &mut block.stmts {
rederive_stmt_inner(stmt, salt);
}
}
fn rederive_stmt_inner(stmt: &mut hir::Stmt, salt: u64) {
match stmt {
hir::Stmt::ChoiceSet(cs) => rederive_choice_set(cs, salt),
hir::Stmt::LabeledBlock(block) => {
for s in &mut block.stmts {
rederive_stmt_inner(s, salt);
}
}
hir::Stmt::Conditional(cond) => {
for branch in &mut cond.branches {
if let Some(id) = branch.container_id {
branch.container_id = Some(derive_id(id, "clone", salt));
}
rederive_block_inner(&mut branch.body, salt);
}
}
hir::Stmt::Sequence(seq) => rederive_cloned_sequence(seq, salt),
hir::Stmt::Content(content) => {
rederive_parts_inner(&mut content.parts, salt);
for tag in &mut content.tags {
rederive_parts_inner(&mut tag.parts, salt);
}
}
hir::Stmt::Divert(d) => {
for a in &mut d.target.args {
rederive_cloned_expr(a, salt);
}
}
hir::Stmt::TunnelCall(t) => {
for target in &mut t.targets {
for a in &mut target.args {
rederive_cloned_expr(a, salt);
}
}
}
hir::Stmt::ThreadStart(t) => {
for a in &mut t.target.args {
rederive_cloned_expr(a, salt);
}
}
hir::Stmt::TempDecl(t) => {
if let Some(e) = &mut t.value {
rederive_cloned_expr(e, salt);
}
}
hir::Stmt::Assignment(a) => {
rederive_cloned_expr(&mut a.target, salt);
rederive_cloned_expr(&mut a.value, salt);
}
hir::Stmt::Return(r) => {
if let Some(e) = &mut r.value {
rederive_cloned_expr(e, salt);
}
for a in &mut r.onwards_args {
rederive_cloned_expr(a, salt);
}
}
hir::Stmt::ExprStmt(e) | hir::Stmt::AttachElement(e) => {
rederive_cloned_expr(e, salt);
}
hir::Stmt::LogicBlock(lb) => {
for s in &mut lb.stmts {
rederive_cloned_block_stmt(s, salt);
}
}
hir::Stmt::Await(a) => {
if let Some(c) = &mut a.condition {
rederive_cloned_expr(c, salt);
}
}
hir::Stmt::EndOfLine | hir::Stmt::EndElementRun => {}
}
}
fn rederive_cloned_block_stmt(stmt: &mut hir::BlockStmt, salt: u64) {
match stmt {
hir::BlockStmt::TempDecl(t) => {
if let Some(e) = &mut t.value {
rederive_cloned_expr(e, salt);
}
}
hir::BlockStmt::Assignment(a) => {
rederive_cloned_expr(&mut a.target, salt);
rederive_cloned_expr(&mut a.value, salt);
}
hir::BlockStmt::Return(r) => {
if let Some(e) = &mut r.value {
rederive_cloned_expr(e, salt);
}
for a in &mut r.onwards_args {
rederive_cloned_expr(a, salt);
}
}
hir::BlockStmt::If(i) => rederive_cloned_if(i, salt),
hir::BlockStmt::While(w) => {
rederive_cloned_expr(&mut w.condition, salt);
for s in &mut w.body {
rederive_cloned_block_stmt(s, salt);
}
}
hir::BlockStmt::For(f) => {
rederive_cloned_expr(&mut f.iterable, salt);
for s in &mut f.body {
rederive_cloned_block_stmt(s, salt);
}
}
hir::BlockStmt::ExprStmt(e) => rederive_cloned_expr(e, salt),
_ => {}
}
}
fn rederive_choice_set(cs: &mut hir::ChoiceSet, salt: u64) {
if let Some(id) = cs.gather_id {
let derived = derive_id(id, "clone", salt);
cs.gather_id = Some(derived);
cs.continuation.container_id = Some(derived);
}
for choice in &mut cs.choices {
if let Some(id) = choice.container_id {
choice.container_id = Some(derive_id(id, "clone", salt));
}
if let Some(c) = &mut choice.condition {
rederive_cloned_expr(c, salt);
}
rederive_block_inner(&mut choice.body, salt);
}
for s in &mut cs.continuation.stmts {
rederive_stmt_inner(s, salt);
}
}
fn rederive_cloned_if(i: &mut hir::IfStmt, salt: u64) {
rederive_cloned_expr(&mut i.condition, salt);
for s in &mut i.body {
rederive_cloned_block_stmt(s, salt);
}
match &mut i.else_branch {
Some(hir::ElseBranch::ElseIf(nested)) => rederive_cloned_if(nested, salt),
Some(hir::ElseBranch::Else(stmts)) => {
for s in stmts {
rederive_cloned_block_stmt(s, salt);
}
}
None => {}
}
}
fn rederive_cloned_expr(expr: &mut hir::Expr, salt: u64) {
match expr {
hir::Expr::Lambda(l) => {
if let Some(id) = l.container_id {
l.container_id = Some(derive_id(id, "clone", salt));
}
match &mut l.body {
hir::LambdaBody::Expr(e) => rederive_cloned_expr(e, salt),
hir::LambdaBody::Block { stmts, tail } => {
for s in stmts.iter_mut() {
rederive_cloned_block_stmt(s, salt);
}
if let Some(t) = tail {
rederive_cloned_expr(t, salt);
}
}
}
}
hir::Expr::Prefix(_, inner) | hir::Expr::Postfix(inner, _) => {
rederive_cloned_expr(inner, salt);
}
hir::Expr::Infix(ie) => {
rederive_cloned_expr(&mut ie.lhs, salt);
rederive_cloned_expr(&mut ie.rhs, salt);
}
hir::Expr::Call(_, args) => {
for a in args {
rederive_cloned_expr(a, salt);
}
}
hir::Expr::ArrayLiteral(a) => {
for e in &mut a.elements {
rederive_cloned_expr(e, salt);
}
}
hir::Expr::MapLiteral(m) => {
for (k, v) in &mut m.entries {
rederive_cloned_expr(k, salt);
rederive_cloned_expr(v, salt);
}
}
hir::Expr::Index(idx) => {
rederive_cloned_expr(&mut idx.base, salt);
rederive_cloned_expr(&mut idx.index, salt);
}
hir::Expr::Range(r) => {
rederive_cloned_expr(&mut r.start, salt);
rederive_cloned_expr(&mut r.end, salt);
}
hir::Expr::StructLiteral(sl) => {
for (_, v) in &mut sl.fields {
rederive_cloned_expr(v, salt);
}
}
hir::Expr::FieldAccess(fa) => rederive_cloned_expr(&mut fa.base, salt),
hir::Expr::FnLiteral(fl) => {
for a in &mut fl.args {
rederive_cloned_expr(a, salt);
}
}
hir::Expr::RefArg(ra) => rederive_cloned_expr(&mut ra.operand, salt),
hir::Expr::String(s) => {
for part in &mut s.parts {
if let hir::StringPart::Interpolation(inner) = part {
rederive_cloned_expr(inner, salt);
}
}
}
hir::Expr::Fragment(stmts) => {
for s in stmts {
rederive_stmt_inner(s, salt);
}
}
hir::Expr::Path(_)
| hir::Expr::DivertTarget(_)
| hir::Expr::ListLiteral(_)
| hir::Expr::Int(_)
| hir::Expr::Float(_)
| hir::Expr::Bool(_)
| hir::Expr::Null => {}
}
}
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}")
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::panic)]
use super::*;
use crate::hir::{ContentPart, Stmt};
fn stamped(src: &str) -> hir::HirFile {
let parse = brink_syntax::parse(src);
let tree = parse.tree();
let (root_content, _top_knots, _d1) = crate::hir::lower::lower_top_level(FileId(0), &tree);
let (mut hir_file, _d2) = crate::hir::lower::lower_declarations(FileId(0), &tree);
hir_file.root_content = root_content;
let mut index = SymbolIndex::default();
let label_id = alloc_address("opt");
index.by_name.insert("opt".to_owned(), vec![label_id]);
index.symbols.insert(
label_id,
SymbolInfo {
kind: SymbolKind::Label,
file: FileId(0),
range: rowan::TextRange::default(),
id: label_id,
name: "opt".to_owned(),
params: Vec::new(),
detail: None,
scope: None,
param_detail: None,
module: None,
visibility: crate::symbols::Visibility::Public,
},
);
let mut files = [(FileId(0), hir_file)];
stamp_container_ids(&mut files, &index, &LookupMap::default());
let [(_, out)] = files;
out
}
fn as_choice_set(stmt: &Stmt) -> &hir::ChoiceSet {
match stmt {
Stmt::ChoiceSet(cs) => cs,
other => panic!("expected ChoiceSet, got {other:?}"),
}
}
fn first_inline_cond_branch_id(block: &hir::Block) -> DefinitionId {
for stmt in &block.stmts {
if let Stmt::Content(c) = stmt {
for part in &c.parts {
if let ContentPart::InlineConditional(cond) = part {
return cond.branches[0]
.container_id
.expect("branch must be stamped");
}
}
}
}
panic!("no inline conditional found in block");
}
#[test]
fn sequence_branch_choices_get_distinct_ids() {
let hir_file = stamped("{stopping:\n- one\n * choice A\n- two\n * choice B\n}\n");
let seq = hir_file
.root_content
.stmts
.iter()
.find_map(|s| match s {
Stmt::Sequence(seq) => Some(seq),
_ => None,
})
.expect("block sequence must lower");
let ids: Vec<DefinitionId> = seq
.branches
.iter()
.map(|b| {
let cs = b
.body
.stmts
.iter()
.find_map(|s| match s {
Stmt::ChoiceSet(cs) => Some(cs),
_ => None,
})
.expect("branch must hold a choice set");
cs.choices[0].container_id.expect("choice must be stamped")
})
.collect();
assert!(
ids.len() >= 2,
"fixture must produce two branches, got {}",
ids.len()
);
assert_ne!(
ids[0], ids[1],
"choices in two branches of one sequence must never share an id"
);
}
#[test]
fn sibling_choice_body_ids_survive_edit_in_other_body() {
let before = stamped("* first\n {x: a | b}\n* second\n {y: c | d}\n");
let after = stamped("* first\n {z: e | f}\n {x: a | b}\n* second\n {y: c | d}\n");
let second_branch_id = |hf: &hir::HirFile| {
let cs = as_choice_set(&hf.root_content.stmts[0]);
first_inline_cond_branch_id(&cs.choices[1].body)
};
assert_eq!(
second_branch_id(&before),
second_branch_id(&after),
"an insertion in `first`'s body must not renumber `second`'s body"
);
}
#[test]
fn label_insulates_choice_subtree_from_earlier_siblings() {
let before = stamped("* (opt) labeled\n {p: q | r}\n");
let after = stamped("* padding\n* (opt) labeled\n {p: q | r}\n");
let labeled = |hf: &hir::HirFile| {
let cs = as_choice_set(&hf.root_content.stmts[0]);
let choice = cs
.choices
.iter()
.find(|c| c.label.is_some())
.expect("labeled choice present");
(
choice.container_id.expect("stamped"),
first_inline_cond_branch_id(&choice.body),
)
};
assert_eq!(
labeled(&before),
labeled(&after),
"a label must insulate the choice AND its subtree from sibling insertions"
);
let before_anon = stamped("* labeled\n {p: q | r}\n");
let after_anon = stamped("* padding\n* labeled\n {p: q | r}\n");
let anon_choice_id = |hf: &hir::HirFile, idx: usize| {
as_choice_set(&hf.root_content.stmts[0]).choices[idx]
.container_id
.expect("stamped")
};
assert_ne!(
anon_choice_id(&before_anon, 0),
anon_choice_id(&after_anon, 1),
"an unlabeled choice's id is positional and must shift — the E157 exposure"
);
}
}