mod content;
mod context;
mod decls;
mod expr;
mod recognize;
mod stmts;
mod temps;
use std::collections::HashMap;
use brink_format::CountingFlags;
use crate::FileId;
use crate::hir;
use crate::symbols::{ResolutionMap, SymbolIndex};
use super::types as lir;
use context::{LowerCtx, NameTable, ResolutionLookup, TempMap};
#[expect(
clippy::implicit_hasher,
reason = "internal API, no need to generalize"
)]
pub fn lower_to_program(
files: &[(FileId, &hir::HirFile)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
file_paths: &HashMap<FileId, String>,
) -> (lir::Program, Vec<crate::Diagnostic>) {
let mut normalized: Vec<(FileId, hir::HirFile)> = files
.iter()
.map(|(id, hir_file)| {
let mut h = (*hir_file).clone();
hir::normalize_file(&mut h);
(*id, h)
})
.collect();
hir::stamp_container_ids(&mut normalized, index);
let files: Vec<(FileId, &hir::HirFile)> = normalized.iter().map(|(id, h)| (*id, h)).collect();
let files = &files;
let resolutions = ResolutionLookup::build(resolutions);
let mut names = NameTable::new();
let mut ids = context::IdAllocator::new();
let root_id = ids.alloc_address("");
let mut lir_diagnostics = Vec::new();
let mut globals =
decls::collect_globals(files, index, &mut names, &resolutions, &mut lir_diagnostics);
let (lists, list_items, list_globals) = decls::collect_lists(files, index, &mut names);
globals.extend(list_globals);
let externals = decls::collect_externals(files, index, &mut names);
let root = lower_root(
files,
&resolutions,
index,
&mut names,
root_id,
&mut ids,
file_paths,
);
let mut root = root;
apply_counting_flags(&mut root, &globals);
(
lir::Program {
root,
globals,
lists,
list_items,
externals,
name_table: names.into_entries(),
},
lir_diagnostics,
)
}
fn lower_root(
files: &[(FileId, &hir::HirFile)],
resolutions: &ResolutionLookup,
index: &SymbolIndex,
names: &mut NameTable,
root_id: brink_format::DefinitionId,
ids: &mut context::IdAllocator,
file_paths: &HashMap<FileId, String>,
) -> lir::Container {
let mut body = Vec::new();
let mut children = Vec::new();
let root_blocks: Vec<&hir::Block> = files.iter().map(|(_, hir)| &hir.root_content).collect();
let temp_map = temps::alloc_temps(&[], &[], &root_blocks);
for &(file_id, hir_file) in files {
let mut ctx = make_ctx(
file_id,
resolutions,
index,
&temp_map,
names,
ids,
root_id,
String::new(),
&[],
file_paths,
);
let mut cc = 0;
let mut gc = 0;
ctx.ids.reset_seq_counter();
let (stmts, mut block_children) =
lower_block_with_children(&hir_file.root_content, &mut ctx, &mut cc, &mut gc);
body.extend(stmts);
children.append(&mut block_children);
for knot in &hir_file.knots {
children.push(lower_knot(
file_id,
hir_file,
knot,
resolutions,
index,
names,
ids,
root_id,
file_paths,
));
}
}
let ends_with_divert = body
.last()
.is_some_and(|s| matches!(s, lir::Stmt::Divert(_)));
if !ends_with_divert {
body.push(lir::Stmt::Divert(lir::Divert {
target: lir::DivertTarget::Done,
args: Vec::new(),
}));
}
lir::Container {
id: root_id,
name: None,
kind: lir::ContainerKind::Root,
params: Vec::new(),
body,
children,
counting_flags: CountingFlags::empty(),
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
}
}
#[expect(clippy::too_many_arguments)]
fn lower_knot(
file_id: FileId,
_hir_file: &hir::HirFile,
knot: &hir::Knot,
resolutions: &ResolutionLookup,
index: &SymbolIndex,
names: &mut NameTable,
ids: &mut context::IdAllocator,
root_id: brink_format::DefinitionId,
file_paths: &HashMap<FileId, String>,
) -> lir::Container {
let knot_name = &knot.name.text;
let knot_id = lookup_container_id(index, knot_name).unwrap_or(root_id);
let mut scope_blocks: Vec<&hir::Block> = vec![&knot.body];
for stitch in &knot.stitches {
scope_blocks.push(&stitch.body);
}
let temp_map = temps::alloc_temps(&knot.params, &knot.stitches, &scope_blocks);
let temp_count = temp_map.total_slots();
let params = lower_params(&knot.params, names, &temp_map);
let knot_param_names: Vec<&str> = knot.params.iter().map(|p| p.name.text.as_str()).collect();
let mut ctx = make_ctx(
file_id,
resolutions,
index,
&temp_map,
names,
ids,
root_id,
knot_name.clone(),
&knot_param_names,
file_paths,
);
let mut cc = 0;
let mut gc = 0;
ctx.ids.reset_seq_counter();
let (body, mut children) = lower_block_with_children(&knot.body, &mut ctx, &mut cc, &mut gc);
for stitch in &knot.stitches {
children.push(lower_stitch(
file_id,
knot,
stitch,
&temp_map,
resolutions,
index,
names,
ids,
root_id,
file_paths,
));
}
let mut final_body = body;
if final_body.is_empty()
&& !knot.stitches.is_empty()
&& let Some(first_stitch) = children
.iter()
.find(|c| c.kind == lir::ContainerKind::Stitch)
{
final_body.push(lir::Stmt::Divert(lir::Divert {
target: lir::DivertTarget::Address(first_stitch.id),
args: Vec::new(),
}));
}
lir::Container {
id: knot_id,
name: Some(knot_name.clone()),
kind: lir::ContainerKind::Knot,
params,
body: final_body,
children,
counting_flags: CountingFlags::empty(),
temp_slot_count: temp_count,
labeled: false,
inline: false,
is_function: knot.is_function,
}
}
#[expect(clippy::too_many_arguments)]
fn lower_stitch(
file_id: FileId,
knot: &hir::Knot,
stitch: &hir::Stitch,
temp_map: &TempMap,
resolutions: &ResolutionLookup,
index: &SymbolIndex,
names: &mut NameTable,
ids: &mut context::IdAllocator,
root_id: brink_format::DefinitionId,
file_paths: &HashMap<FileId, String>,
) -> lir::Container {
let stitch_name = &stitch.name.text;
let stitch_path = format!("{}.{stitch_name}", knot.name.text);
let stitch_id = lookup_container_id(index, &stitch_path).unwrap_or(root_id);
let params = lower_params(&stitch.params, names, temp_map);
let stitch_param_names: Vec<&str> =
stitch.params.iter().map(|p| p.name.text.as_str()).collect();
let mut ctx = make_ctx(
file_id,
resolutions,
index,
temp_map,
names,
ids,
root_id,
stitch_path,
&stitch_param_names,
file_paths,
);
let mut cc = 0;
let mut gc = 0;
ctx.ids.reset_seq_counter();
let (body, children) = lower_block_with_children(&stitch.body, &mut ctx, &mut cc, &mut gc);
lir::Container {
id: stitch_id,
name: Some(stitch_name.clone()),
kind: lir::ContainerKind::Stitch,
params,
body,
children,
counting_flags: CountingFlags::empty(),
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
}
}
#[expect(clippy::too_many_lines)]
fn lower_block_with_children(
block: &hir::Block,
ctx: &mut LowerCtx<'_>,
choice_counter: &mut usize,
gather_counter: &mut usize,
) -> (Vec<lir::Stmt>, Vec<lir::Container>) {
let mut stmts = Vec::new();
let mut children = Vec::new();
let mut pos = 0;
while pos < block.stmts.len() {
let stmt = &block.stmts[pos];
match stmt {
hir::Stmt::ChoiceSet(cs) => {
let gather_target = cs.gather_id;
*gather_counter += 1;
let mut choice_children = Vec::new();
let choices: Vec<lir::Choice> = cs
.choices
.iter()
.map(|choice| {
let (lir_choice, child) =
lower_choice_with_child(choice, ctx, choice_counter, gather_target);
if let Some(c) = child {
choice_children.push(c);
}
lir_choice
})
.collect();
stmts.push(lir::Stmt::ChoiceSet(lir::ChoiceSet {
choices,
gather_target,
}));
children.append(&mut choice_children);
let gather_container = build_continuation_container(
&cs.continuation,
ctx,
gather_target,
*gather_counter - 1,
choice_counter,
gather_counter,
);
children.push(gather_container);
pos += 1;
}
hir::Stmt::LabeledBlock(labeled) => {
let wrapper_id = labeled.container_id.unwrap_or(ctx.root_id);
*gather_counter += 1;
stmts.push(lir::Stmt::EnterContainer(wrapper_id));
let display_name = labeled
.label
.as_ref()
.map_or_else(|| format!("g-{}", *gather_counter - 1), |l| l.text.clone());
let labeled_flag = labeled
.label
.as_ref()
.is_some_and(|label| ctx.lookup_address_id(&label.text).is_some());
let (mut inner_stmts, inner_children) =
lower_block_with_children(labeled, ctx, choice_counter, gather_counter);
if let Some(gather_id) = ctx.choice_gather_target {
let ends_terminal = inner_stmts.last().is_some_and(|s| {
matches!(
s,
lir::Stmt::Divert(d) if matches!(
d.target,
lir::DivertTarget::Done
| lir::DivertTarget::End
| lir::DivertTarget::Address(_)
)
) || matches!(s, lir::Stmt::ChoiceSet(_))
});
if !ends_terminal {
inner_stmts.push(lir::Stmt::Divert(lir::Divert {
target: lir::DivertTarget::Address(gather_id),
args: Vec::new(),
}));
}
}
children.push(lir::Container {
id: wrapper_id,
name: Some(display_name),
kind: lir::ContainerKind::Gather,
params: Vec::new(),
body: inner_stmts,
children: inner_children,
counting_flags: CountingFlags::empty(),
temp_slot_count: 0,
labeled: labeled_flag,
inline: true,
is_function: false,
});
pos += 1;
}
hir::Stmt::Conditional(cond) => {
let kind = match &cond.kind {
hir::CondKind::InitialCondition => lir::CondKind::InitialCondition,
hir::CondKind::IfElse => lir::CondKind::IfElse,
hir::CondKind::Switch(expr) => {
lir::CondKind::Switch(expr::lower_expr(expr, ctx))
}
};
let cond_idx = ctx.ids.next_seq_index();
let cond_scope = format!("b-{cond_idx}");
let old_scope = ctx.scope_path.clone();
let branches = cond
.branches
.iter()
.enumerate()
.map(|(branch_idx, b)| {
let condition = b.condition.as_ref().map(|e| expr::lower_expr(e, ctx));
let branch_scope = if old_scope.is_empty() {
format!("{cond_scope}.{branch_idx}")
} else {
format!("{old_scope}.{cond_scope}.{branch_idx}")
};
ctx.scope_path = branch_scope;
let (body, branch_children) =
lower_block_with_children(&b.body, ctx, choice_counter, gather_counter);
let branch_id = b.container_id.unwrap_or(ctx.root_id);
let branch_container = lir::Container {
id: branch_id,
name: Some(format!("{branch_idx}")),
kind: lir::ContainerKind::ConditionalBranch,
params: Vec::new(),
body,
children: branch_children,
counting_flags: CountingFlags::empty(),
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
};
children.push(branch_container);
lir::CondBranch {
condition,
body: vec![lir::Stmt::EnterContainer(branch_id)],
}
})
.collect();
ctx.scope_path = old_scope;
stmts.push(lir::Stmt::Conditional(lir::Conditional { kind, branches }));
pos += 1;
}
hir::Stmt::Sequence(seq) => {
let seq_idx = ctx.ids.next_seq_index();
let wrapper_id = seq.container_id.unwrap_or(ctx.root_id);
let display_name = format!("s-{seq_idx}");
let old_scope = ctx.scope_path.clone();
ctx.scope_path = if old_scope.is_empty() {
display_name.clone()
} else {
format!("{old_scope}.{display_name}")
};
let mut wrapper_children = Vec::new();
let branches: Vec<Vec<lir::Stmt>> = seq
.branches
.iter()
.enumerate()
.map(|(branch_idx, b)| {
let mut bc = 0;
let mut gc = 0;
let (body, branch_children) =
lower_block_with_children(b, ctx, &mut bc, &mut gc);
let branch_id = b.container_id.unwrap_or(ctx.root_id);
let branch_container = lir::Container {
id: branch_id,
name: Some(format!("{branch_idx}")),
kind: lir::ContainerKind::SequenceBranch,
params: Vec::new(),
body,
children: branch_children,
counting_flags: CountingFlags::empty(),
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
};
wrapper_children.push(branch_container);
vec![lir::Stmt::EnterContainer(branch_id)]
})
.collect();
ctx.scope_path = old_scope;
let wrapper = lir::Container {
id: wrapper_id,
name: Some(display_name),
kind: lir::ContainerKind::Sequence,
params: Vec::new(),
body: vec![lir::Stmt::Sequence(lir::Sequence {
kind: seq.kind,
branches,
})],
children: wrapper_children,
counting_flags: CountingFlags::VISITS | CountingFlags::COUNT_START_ONLY,
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
};
children.push(wrapper);
stmts.push(lir::Stmt::EnterContainer(wrapper_id));
pos += 1;
}
hir::Stmt::Content(content) => {
if let Some(emission) = recognize::try_recognize(content, ctx) {
stmts.push(lir::Stmt::EmitLine(emission));
}
else if let Some((leading, emission, trailing)) =
recognize::try_recognize_with_glue(content, ctx)
{
if leading {
stmts.push(lir::Stmt::EmitContent(lir::Content {
parts: vec![lir::ContentPart::Glue],
tags: vec![],
}));
}
stmts.push(lir::Stmt::EmitLine(emission));
if trailing {
stmts.push(lir::Stmt::EmitContent(lir::Content {
parts: vec![lir::ContentPart::Glue],
tags: vec![],
}));
}
}
else {
stmts.push(lir::Stmt::EmitContent(content::lower_content(content, ctx)));
}
children.append(&mut ctx.pending_children);
pos += 1;
}
_ => {
if let Some(s) = stmts::lower_stmt(stmt, ctx) {
stmts.push(s);
}
children.append(&mut ctx.pending_children);
pos += 1;
}
}
}
(stmts, children)
}
fn build_continuation_container(
continuation: &hir::Block,
ctx: &mut LowerCtx<'_>,
gather_id: Option<brink_format::DefinitionId>,
gather_index: usize,
choice_counter: &mut usize,
gather_counter: &mut usize,
) -> lir::Container {
let id = gather_id.unwrap_or(ctx.root_id);
let display_name = continuation
.label
.as_ref()
.map_or_else(|| format!("g-{gather_index}"), |l| l.text.clone());
let labeled = continuation
.label
.as_ref()
.is_some_and(|label| ctx.lookup_address_id(&label.text).is_some());
if continuation.stmts.is_empty() && continuation.label.is_none() {
return lir::Container {
id,
name: Some(display_name),
kind: lir::ContainerKind::Gather,
params: Vec::new(),
body: vec![lir::Stmt::Divert(lir::Divert {
target: lir::DivertTarget::Done,
args: Vec::new(),
})],
children: Vec::new(),
counting_flags: CountingFlags::empty(),
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
};
}
let (body, children) =
lower_block_with_children(continuation, ctx, choice_counter, gather_counter);
lir::Container {
id,
name: Some(display_name),
kind: lir::ContainerKind::Gather,
params: Vec::new(),
body,
children,
counting_flags: CountingFlags::empty(),
temp_slot_count: 0,
labeled,
inline: false,
is_function: false,
}
}
#[expect(clippy::too_many_lines, reason = "choice lowering has many parts")]
fn lower_choice_with_child(
choice: &hir::Choice,
ctx: &mut LowerCtx<'_>,
choice_counter: &mut usize,
gather_target: Option<brink_format::DefinitionId>,
) -> (lir::Choice, Option<lir::Container>) {
*choice_counter += 1;
let target = choice.container_id.unwrap_or(ctx.root_id);
let start_content = choice
.start_content
.as_ref()
.map(|c| content::lower_content(c, ctx));
let choice_only_content = choice
.bracket_content
.as_ref()
.map(|c| content::lower_content(c, ctx));
let inner_content = choice
.inner_content
.as_ref()
.map(|c| content::lower_content(c, ctx));
let display_hir = recognize::compose_hir_content_opt(
choice.start_content.as_ref(),
choice.bracket_content.as_ref(),
);
let output_hir = recognize::compose_hir_content_opt(
choice.start_content.as_ref(),
choice.inner_content.as_ref(),
);
let display_ws = display_hir
.as_ref()
.is_some_and(recognize::starts_with_whitespace_only_text);
let output_ws = output_hir
.as_ref()
.is_some_and(recognize::starts_with_whitespace_only_text);
let display_emission = if display_ws {
None
} else {
display_hir
.as_ref()
.and_then(|c| recognize::try_recognize(c, ctx))
};
let output_emission = if output_ws {
None
} else {
output_hir
.as_ref()
.and_then(|c| recognize::try_recognize(c, ctx))
};
let condition = choice.condition.as_ref().map(|e| expr::lower_expr(e, ctx));
let tags: Vec<Vec<lir::ContentPart>> = choice
.tags
.iter()
.map(|t| content::lower_content_parts_pub(&t.parts, ctx))
.collect();
let old_scope = ctx.scope_path.clone();
let old_gather_target = ctx.choice_gather_target;
ctx.scope_path = format!("{}.c{}", old_scope, *choice_counter - 1);
ctx.choice_gather_target = gather_target;
let mut cc = 0;
let mut gc = 0;
let (body_stmts, mut children) = lower_block_with_children(&choice.body, ctx, &mut cc, &mut gc);
ctx.scope_path = old_scope;
ctx.choice_gather_target = old_gather_target;
let mut body: Vec<lir::Stmt> = Vec::new();
{
let mut output_parts = Vec::new();
let mut output_tags = Vec::new();
if let Some(ref sc) = start_content {
output_parts.extend(sc.parts.clone());
output_tags.extend(sc.tags.clone());
}
if let Some(ref ic) = inner_content {
output_parts.extend(ic.parts.clone());
output_tags.extend(ic.tags.clone());
}
if !output_parts.is_empty() || !output_tags.is_empty() {
body.push(lir::Stmt::ChoiceOutput {
content: lir::Content {
parts: output_parts,
tags: output_tags,
},
emission: output_emission.clone(),
});
}
}
body.extend(body_stmts);
let ends_with_terminal = body.last().is_some_and(|s| {
matches!(
s,
lir::Stmt::Divert(d) if matches!(d.target, lir::DivertTarget::Done | lir::DivertTarget::End)
)
});
if !ends_with_terminal && let Some(gather_id) = gather_target {
let body_ends_with_choice_set = body
.last()
.is_some_and(|s| matches!(s, lir::Stmt::ChoiceSet(_)));
let divert = lir::Divert {
target: lir::DivertTarget::Address(gather_id),
args: Vec::new(),
};
if body_ends_with_choice_set {
patch_innermost_gather(&mut children, divert);
} else {
body.push(lir::Stmt::Divert(divert));
}
}
let labeled = choice
.label
.as_ref()
.is_some_and(|label| ctx.lookup_address_id(&label.text).is_some());
let child_name = format!("c-{}", *choice_counter - 1);
let child = lir::Container {
id: target,
name: Some(child_name),
kind: lir::ContainerKind::ChoiceTarget,
params: Vec::new(),
body,
children,
counting_flags: if choice.is_sticky {
CountingFlags::empty()
} else {
CountingFlags::VISITS | CountingFlags::COUNT_START_ONLY
},
temp_slot_count: 0,
labeled,
inline: false,
is_function: false,
};
let lir_choice = lir::Choice {
is_sticky: choice.is_sticky,
is_fallback: choice.is_fallback,
condition,
start_content,
choice_only_content,
inner_content,
display_emission,
output_emission,
target,
tags,
};
(lir_choice, Some(child))
}
#[expect(clippy::too_many_arguments)]
fn make_ctx<'a>(
file: FileId,
resolutions: &'a ResolutionLookup,
index: &'a SymbolIndex,
temps: &'a TempMap,
names: &'a mut NameTable,
ids: &'a mut context::IdAllocator,
root_id: brink_format::DefinitionId,
scope_path: String,
param_names: &[&str],
file_paths: &'a HashMap<FileId, String>,
) -> LowerCtx<'a> {
LowerCtx {
file,
resolutions,
index,
temps,
names,
ids,
scope_path,
pending_children: Vec::new(),
visible_temps: param_names.iter().map(|s| (*s).to_string()).collect(),
file_paths,
root_id,
choice_gather_target: None,
}
}
fn lower_params(
params: &[hir::Param],
names: &mut NameTable,
temp_map: &TempMap,
) -> Vec<lir::Param> {
params
.iter()
.map(|p| {
let name = names.intern(&p.name.text);
let slot = temp_map.get(&p.name.text).unwrap_or(0);
lir::Param {
name,
slot,
is_ref: p.is_ref,
is_divert: p.is_divert,
}
})
.collect()
}
fn lookup_container_id(index: &SymbolIndex, name: &str) -> Option<brink_format::DefinitionId> {
use crate::symbols::SymbolKind;
index.by_name.get(name).and_then(|ids| {
ids.iter()
.find(|&&id| {
index.symbols.get(&id).is_some_and(|info| {
matches!(
info.kind,
SymbolKind::Knot | SymbolKind::Stitch | SymbolKind::Label
)
})
})
.copied()
})
}
fn apply_counting_flags(root: &mut lir::Container, globals: &[lir::GlobalDef]) {
let mut visit_ids = Vec::new();
let mut turns_ids = Vec::new();
collect_counting_refs_tree(root, &mut visit_ids, &mut turns_ids);
for g in globals {
if let lir::ConstValue::DivertTarget(id) = &g.default {
visit_ids.push(*id);
turns_ids.push(*id);
}
}
apply_counting_flags_tree(root, &visit_ids, &turns_ids);
}
fn collect_counting_refs_tree(
container: &lir::Container,
visit_ids: &mut Vec<brink_format::DefinitionId>,
turns_ids: &mut Vec<brink_format::DefinitionId>,
) {
collect_counting_refs(&container.body, visit_ids, turns_ids);
for child in &container.children {
collect_counting_refs_tree(child, visit_ids, turns_ids);
}
}
fn apply_counting_flags_tree(
container: &mut lir::Container,
visit_ids: &[brink_format::DefinitionId],
turns_ids: &[brink_format::DefinitionId],
) {
if visit_ids.contains(&container.id) {
container.counting_flags |= CountingFlags::VISITS;
if container.labeled {
container.counting_flags |= CountingFlags::COUNT_START_ONLY;
}
}
if turns_ids.contains(&container.id) {
container.counting_flags |= CountingFlags::TURNS;
}
for child in &mut container.children {
apply_counting_flags_tree(child, visit_ids, turns_ids);
}
}
fn collect_counting_refs(
stmts: &[lir::Stmt],
visit_ids: &mut Vec<brink_format::DefinitionId>,
turns_ids: &mut Vec<brink_format::DefinitionId>,
) {
for stmt in stmts {
match stmt {
lir::Stmt::EmitContent(content) | lir::Stmt::ChoiceOutput { content, .. } => {
collect_counting_refs_content(content, visit_ids, turns_ids);
}
lir::Stmt::EmitLine(emission) | lir::Stmt::EvalLine(emission) => {
if let lir::RecognizedLine::Template { slot_exprs, .. } = &emission.line {
for e in slot_exprs {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
}
for tag in &emission.tags {
for part in tag {
if let lir::ContentPart::Interpolation(e) = part {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
}
}
}
lir::Stmt::Assign { value: e, .. }
| lir::Stmt::DeclareTemp { value: Some(e), .. }
| lir::Stmt::Return { value: Some(e), .. }
| lir::Stmt::ExprStmt(e) => {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
lir::Stmt::ChoiceSet(cs) => {
for choice in &cs.choices {
if let Some(ref cond) = choice.condition {
collect_counting_refs_expr(cond, visit_ids, turns_ids);
}
if let Some(ref c) = choice.start_content {
collect_counting_refs_content(c, visit_ids, turns_ids);
}
if let Some(ref c) = choice.choice_only_content {
collect_counting_refs_content(c, visit_ids, turns_ids);
}
if let Some(ref c) = choice.inner_content {
collect_counting_refs_content(c, visit_ids, turns_ids);
}
for emission in choice
.display_emission
.iter()
.chain(choice.output_emission.iter())
{
if let lir::RecognizedLine::Template { slot_exprs, .. } = &emission.line {
for e in slot_exprs {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
}
}
}
}
lir::Stmt::Conditional(cond) => {
for branch in &cond.branches {
if let Some(ref e) = branch.condition {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
collect_counting_refs(&branch.body, visit_ids, turns_ids);
}
}
lir::Stmt::Sequence(seq) => {
for branch in &seq.branches {
collect_counting_refs(branch, visit_ids, turns_ids);
}
}
lir::Stmt::Divert(d) => {
for arg in &d.args {
if let lir::CallArg::Value(e) = arg {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
}
}
lir::Stmt::TunnelCall(tc) => {
for t in &tc.targets {
for arg in &t.args {
if let lir::CallArg::Value(e) = arg {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
}
}
}
lir::Stmt::ThreadStart(ts) => {
for arg in &ts.args {
if let lir::CallArg::Value(e) = arg {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
}
}
_ => {}
}
}
}
fn collect_counting_refs_content(
content: &lir::Content,
visit_ids: &mut Vec<brink_format::DefinitionId>,
turns_ids: &mut Vec<brink_format::DefinitionId>,
) {
for part in &content.parts {
match part {
lir::ContentPart::Interpolation(e) => {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
lir::ContentPart::InlineConditional(cond) => {
for branch in &cond.branches {
if let Some(ref e) = branch.condition {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
collect_counting_refs(&branch.body, visit_ids, turns_ids);
}
}
lir::ContentPart::InlineSequence(seq) => {
for branch in &seq.branches {
collect_counting_refs(branch, visit_ids, turns_ids);
}
}
_ => {}
}
}
}
fn collect_counting_refs_expr(
expr: &lir::Expr,
visit_ids: &mut Vec<brink_format::DefinitionId>,
turns_ids: &mut Vec<brink_format::DefinitionId>,
) {
match expr {
lir::Expr::VisitCount(id) => visit_ids.push(*id),
lir::Expr::DivertTarget(id) => {
visit_ids.push(*id);
turns_ids.push(*id);
}
lir::Expr::CallBuiltin {
builtin: lir::BuiltinFn::TurnsSince,
args,
} => {
for a in args {
if let lir::Expr::DivertTarget(id) = a {
turns_ids.push(*id);
}
collect_counting_refs_expr(a, visit_ids, turns_ids);
}
}
lir::Expr::Prefix(_, inner) | lir::Expr::Postfix(inner, _) => {
collect_counting_refs_expr(inner, visit_ids, turns_ids);
}
lir::Expr::Infix(lhs, _, rhs) => {
collect_counting_refs_expr(lhs, visit_ids, turns_ids);
collect_counting_refs_expr(rhs, visit_ids, turns_ids);
}
lir::Expr::Call { args, .. } | lir::Expr::CallExternal { args, .. } => {
for arg in args {
if let lir::CallArg::Value(e) = arg {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
}
}
lir::Expr::CallBuiltin { args, .. } => {
for a in args {
collect_counting_refs_expr(a, visit_ids, turns_ids);
}
}
lir::Expr::String(s) => {
for p in &s.parts {
if let lir::StringPart::Interpolation(e) = p {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
}
}
_ => {}
}
}
fn patch_innermost_gather(children: &mut [lir::Container], divert: lir::Divert) {
let Some(gather) = children
.last_mut()
.filter(|c| c.kind == lir::ContainerKind::Gather)
else {
return;
};
let gather_body_ends_with_choice_set = gather
.body
.last()
.is_some_and(|s| matches!(s, lir::Stmt::ChoiceSet(_)));
if gather_body_ends_with_choice_set {
patch_innermost_gather(&mut gather.children, divert);
return;
}
let gather_body_ends_terminal = gather.body.last().is_some_and(|s| {
matches!(
s,
lir::Stmt::Divert(d)
if matches!(
d.target,
lir::DivertTarget::End
| lir::DivertTarget::Done
| lir::DivertTarget::Address(_)
)
)
});
if gather_body_ends_terminal {
let last_idx = gather.body.len() - 1;
gather.body[last_idx] = lir::Stmt::Divert(divert);
} else {
gather.body.push(lir::Stmt::Divert(divert));
}
}