mod blocks;
mod chunk;
mod content;
mod context;
mod decls;
mod expr;
mod lambda;
pub mod recognize;
mod stmts;
mod structs;
mod temps;
use brink_format::CountingFlags;
use rowan::TextRange;
use crate::FileId;
use crate::determinism::{LookupMap, LookupSet};
use crate::hir;
use crate::provenance::{NodeClass, Provenance};
use crate::symbols::{ResolutionMap, SymbolIndex};
use super::types as lir;
use context::{LowerCtx, NameTable, ResolutionLookup, TempMap};
pub use chunk::ScopeChunk;
pub use context::{
AnalyzerTables, CoalesceLookup, CoalesceShape, TypeMode, UfcsLookup, UfcsVerdict,
};
pub use structs::{StructFieldEntry, StructShapeData, StructShapeEntry, build_struct_shape_data};
pub use expr::{is_builtin_function, is_t1b_stdlib_name};
#[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: &LookupMap<FileId, String>,
) -> (Option<lir::Program>, Vec<crate::Diagnostic>) {
lower_to_program_with_type_mode(
files,
index,
resolutions,
file_paths,
context::TypeMode::Gradual,
context::AnalyzerTables {
ufcs: &context::UfcsLookup::new(),
coalesce: &context::CoalesceLookup::new(),
},
)
}
#[expect(
clippy::implicit_hasher,
reason = "internal API, no need to generalize"
)]
pub fn lower_to_program_with_type_mode(
files: &[(FileId, &hir::HirFile)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
file_paths: &LookupMap<FileId, String>,
type_mode: context::TypeMode,
tables: context::AnalyzerTables<'_>,
) -> (Option<lir::Program>, Vec<crate::Diagnostic>) {
let prelude = build_prelude(files, index, resolutions, file_paths, type_mode, tables);
let resolutions = ResolutionLookup::build(resolutions);
let struct_ctx = prelude.struct_ctx();
let prelude_files = prelude.files();
let (root_chunks, root_temp_slots) = lower_root_content_chunks(
&prelude_files,
&resolutions,
index,
prelude.root_id,
file_paths,
&struct_ctx,
tables,
);
let mut lir_diagnostics = prelude.decl_diagnostics.clone();
let mut ordered_chunks = Vec::new();
let mut root_iter = root_chunks.into_iter();
for &(file_id, hir_file) in &prelude_files {
if let Some((chunk, diags)) = root_iter.next() {
ordered_chunks.push(chunk);
lir_diagnostics.extend(diags);
}
for knot in &hir_file.knots {
let (chunk, diags) = lower_knot_chunk(
hir_file,
knot,
index,
&resolutions,
file_paths,
&struct_ctx,
prelude.root_id,
file_id,
tables,
);
ordered_chunks.push(chunk);
lir_diagnostics.extend(diags);
}
}
let program = assemble_program(&prelude, ordered_chunks, root_temp_slots, index, file_paths);
(Some(program), lir_diagnostics)
}
pub struct LirPrelude {
normalized: Vec<(FileId, hir::HirFile)>,
root_id: brink_format::DefinitionId,
globals: Vec<lir::GlobalDef>,
lists: Vec<lir::ListDef>,
list_items: Vec<lir::ListItemDef>,
externals: Vec<lir::ExternalDef>,
shape_table: structs::ShapeTable,
global_shapes: structs::GlobalShapeMap,
name_seed: Vec<String>,
type_mode: context::TypeMode,
private_defs: Vec<brink_format::DefinitionId>,
aliases: Vec<brink_format::AliasEntry>,
pub decl_diagnostics: Vec<crate::Diagnostic>,
lifted: Vec<lir::Container>,
}
impl LirPrelude {
#[must_use]
pub fn files(&self) -> Vec<(FileId, &hir::HirFile)> {
self.normalized.iter().map(|(id, h)| (*id, h)).collect()
}
#[must_use]
pub fn root_id(&self) -> brink_format::DefinitionId {
self.root_id
}
fn struct_ctx(&self) -> context::StructCtx<'_> {
context::StructCtx {
shapes: &self.shape_table,
global_shapes: &self.global_shapes,
type_mode: self.type_mode,
}
}
}
#[derive(Clone)]
pub struct PreludeDecls {
root_id: brink_format::DefinitionId,
globals: Vec<lir::GlobalDef>,
lists: Vec<lir::ListDef>,
list_items: Vec<lir::ListItemDef>,
externals: Vec<lir::ExternalDef>,
shape_table: structs::ShapeTable,
global_shapes: structs::GlobalShapeMap,
name_seed: Vec<String>,
type_mode: context::TypeMode,
private_defs: Vec<brink_format::DefinitionId>,
aliases: Vec<brink_format::AliasEntry>,
decl_diagnostics: Vec<crate::Diagnostic>,
lifted: Vec<lir::Container>,
}
impl PreludeDecls {
#[must_use]
pub fn empty(type_mode: context::TypeMode) -> Self {
Self {
root_id: context::root_definition_id(),
globals: Vec::new(),
lists: Vec::new(),
list_items: Vec::new(),
externals: Vec::new(),
shape_table: structs::ShapeTable::default(),
global_shapes: structs::GlobalShapeMap::default(),
name_seed: Vec::new(),
type_mode,
private_defs: Vec::new(),
aliases: Vec::new(),
decl_diagnostics: Vec::new(),
lifted: Vec::new(),
}
}
}
#[must_use]
#[expect(
clippy::implicit_hasher,
reason = "internal API, no need to generalize"
)]
pub fn build_prelude_decls(
files: &[(FileId, &hir::HirFile)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
file_paths: &LookupMap<FileId, String>,
type_mode: context::TypeMode,
tables: context::AnalyzerTables<'_>,
) -> PreludeDecls {
let resolutions_lookup = ResolutionLookup::build(resolutions);
let mut names = NameTable::new();
let root_id = context::root_definition_id();
let mut decl_diagnostics = Vec::new();
let shape_table = structs::build_shape_table(
files,
&mut names,
index,
&resolutions_lookup,
&mut decl_diagnostics,
);
let global_shapes =
structs::build_global_shape_map(files, index, &resolutions_lookup, &shape_table);
let mut ids = context::IdAllocator::new();
let mut lifted: Vec<lir::Container> = Vec::new();
let struct_ctx = context::StructCtx {
shapes: &shape_table,
global_shapes: &global_shapes,
type_mode,
};
let mut lambda_ctx = decls::GlobalLambdaCtx {
ids: &mut ids,
lifted: &mut lifted,
file_paths,
structs: &struct_ctx,
tables,
root_id,
};
let mut globals = decls::collect_globals(
files,
index,
&mut names,
&resolutions_lookup,
&shape_table,
&mut decl_diagnostics,
&mut lambda_ctx,
);
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, &mut decl_diagnostics);
let name_seed = names.into_entries();
let mut private_defs: Vec<brink_format::DefinitionId> = index
.symbols
.iter()
.filter(|(_, info)| info.visibility == crate::symbols::Visibility::Private)
.map(|(id, _)| *id)
.collect();
private_defs.sort_by_key(|id| id.to_raw());
let mut aliases = index.aliases.clone();
aliases.sort_unstable();
PreludeDecls {
root_id,
globals,
lists,
list_items,
externals,
shape_table,
global_shapes,
name_seed,
type_mode,
private_defs,
aliases,
decl_diagnostics,
lifted,
}
}
#[must_use]
pub fn assemble_prelude(
decls: PreludeDecls,
normalized: Vec<(FileId, hir::HirFile)>,
) -> LirPrelude {
LirPrelude {
normalized,
root_id: decls.root_id,
globals: decls.globals,
lists: decls.lists,
list_items: decls.list_items,
externals: decls.externals,
shape_table: decls.shape_table,
global_shapes: decls.global_shapes,
name_seed: decls.name_seed,
type_mode: decls.type_mode,
private_defs: decls.private_defs,
aliases: decls.aliases,
decl_diagnostics: decls.decl_diagnostics,
lifted: decls.lifted,
}
}
#[must_use]
#[expect(
clippy::implicit_hasher,
reason = "internal API, no need to generalize"
)]
pub fn build_prelude(
files: &[(FileId, &hir::HirFile)],
index: &SymbolIndex,
resolutions: &ResolutionMap,
file_paths: &LookupMap<FileId, String>,
type_mode: context::TypeMode,
tables: context::AnalyzerTables<'_>,
) -> LirPrelude {
let mut normalized: Vec<(FileId, hir::HirFile)> = files
.iter()
.map(|(id, hir_file)| (*id, (*hir_file).clone()))
.collect();
hir::stamp_container_ids(&mut normalized, index, file_paths);
for (_, h) in &mut normalized {
hir::normalize_file(h);
}
let normalized_refs: Vec<(FileId, &hir::HirFile)> =
normalized.iter().map(|(id, h)| (*id, h)).collect();
let decls = build_prelude_decls(
&normalized_refs,
index,
resolutions,
file_paths,
type_mode,
tables,
);
assemble_prelude(decls, normalized)
}
#[must_use]
fn lower_root_content_chunks(
files: &[(FileId, &hir::HirFile)],
resolutions: &ResolutionLookup,
index: &SymbolIndex,
root_id: brink_format::DefinitionId,
file_paths: &LookupMap<FileId, String>,
struct_ctx: &context::StructCtx<'_>,
tables: context::AnalyzerTables<'_>,
) -> (Vec<(chunk::ScopeChunk, Vec<crate::Diagnostic>)>, u16) {
let mut chunks = Vec::new();
let mut ids = context::IdAllocator::new();
let _ = ids.alloc_address("");
let root_blocks: Vec<&hir::Block> = files.iter().map(|(_, hir)| &hir.root_content).collect();
let temp_map = temps::alloc_temps(&[], &[], &root_blocks);
let mut block_slot = temp_map.total_slots();
let last_chunk = files.len().saturating_sub(1);
for (chunk_index, &(file_id, hir_file)) in files.iter().enumerate() {
let mut local_names = NameTable::new();
let mut diagnostics = Vec::new();
ids.set_path_prefix(hir::root_content_scope_path(
file_paths.get(&file_id).map(String::as_str),
));
let mut lifted: Vec<lir::Container> = Vec::new();
let (stmts, mut block_children) = {
let mut ctx = make_ctx(
file_id,
hir_file.native,
resolutions,
index,
&temp_map,
&mut local_names,
&mut ids,
root_id,
String::new(),
true,
&[],
file_paths,
&mut block_slot,
&mut diagnostics,
struct_ctx,
tables,
&mut lifted,
);
let mut cc = 0;
let mut gc = 0;
ctx.ids.reset_seq_counter();
lower_block_with_children(&hir_file.root_content, &mut ctx, &mut cc, &mut gc)
};
if chunk_index == last_chunk {
attach_root_final_gather(&mut block_children, &mut ids);
}
block_children.append(&mut lifted);
chunks.push((
chunk::ScopeChunk::root_content(stmts, block_children, local_names.into_entries()),
diagnostics,
));
}
(chunks, block_slot)
}
#[expect(clippy::too_many_arguments)]
#[must_use]
fn lower_knot_chunk(
hir_file: &hir::HirFile,
knot: &hir::Knot,
index: &SymbolIndex,
resolutions: &ResolutionLookup,
file_paths: &LookupMap<FileId, String>,
struct_ctx: &context::StructCtx<'_>,
root_id: brink_format::DefinitionId,
file_id: FileId,
tables: context::AnalyzerTables<'_>,
) -> (chunk::ScopeChunk, Vec<crate::Diagnostic>) {
let mut local_names = NameTable::new();
let mut ids = context::IdAllocator::new();
let _ = ids.alloc_address("");
ids.set_path_prefix(hir::root_content_scope_path(
file_paths.get(&file_id).map(String::as_str),
));
let mut diagnostics = Vec::new();
let mut lifted = Vec::new();
let knot_container = lower_knot(
file_id,
hir_file,
knot,
resolutions,
index,
&mut local_names,
&mut ids,
root_id,
file_paths,
&mut diagnostics,
struct_ctx,
tables,
&mut lifted,
);
(
chunk::ScopeChunk::knot(knot_container, lifted, local_names.into_entries()),
diagnostics,
)
}
pub struct ChunkLoweringCtx {
resolutions: ResolutionLookup,
shapes: structs::ShapeTable,
global_shapes: structs::GlobalShapeMap,
file_paths: LookupMap<FileId, String>,
type_mode: context::TypeMode,
}
impl ChunkLoweringCtx {
#[must_use]
pub fn new(
resolutions: &ResolutionMap,
shape_data: &StructShapeData,
file_paths: LookupMap<FileId, String>,
type_mode: context::TypeMode,
) -> Self {
let mut throwaway = NameTable::new();
let shapes = structs::rebuild_shape_table(shape_data, &mut throwaway);
let global_shapes = structs::rebuild_global_shape_map(shape_data);
Self {
resolutions: ResolutionLookup::build(resolutions),
shapes,
global_shapes,
file_paths,
type_mode,
}
}
}
#[must_use]
pub fn lower_knot_chunk_incremental(
hir_file: &hir::HirFile,
knot: &hir::Knot,
index: &SymbolIndex,
ctx: &ChunkLoweringCtx,
file_id: FileId,
tables: context::AnalyzerTables<'_>,
) -> (chunk::ScopeChunk, Vec<crate::Diagnostic>) {
let struct_ctx = context::StructCtx {
shapes: &ctx.shapes,
global_shapes: &ctx.global_shapes,
type_mode: ctx.type_mode,
};
lower_knot_chunk(
hir_file,
knot,
index,
&ctx.resolutions,
&ctx.file_paths,
&struct_ctx,
context::root_definition_id(),
file_id,
tables,
)
}
#[expect(
clippy::implicit_hasher,
reason = "internal API called only by brink-db"
)]
#[must_use]
pub fn lower_root_content_for_prelude(
prelude: &LirPrelude,
index: &SymbolIndex,
resolutions: &ResolutionMap,
file_paths: &LookupMap<FileId, String>,
tables: context::AnalyzerTables<'_>,
) -> (Vec<(chunk::ScopeChunk, Vec<crate::Diagnostic>)>, u16) {
let resolutions = ResolutionLookup::build(resolutions);
let struct_ctx = prelude.struct_ctx();
lower_root_content_chunks(
&prelude.files(),
&resolutions,
index,
prelude.root_id,
file_paths,
&struct_ctx,
tables,
)
}
#[must_use]
#[expect(
clippy::implicit_hasher,
reason = "internal API, no need to generalize"
)]
pub fn assemble_program(
prelude: &LirPrelude,
chunks: Vec<chunk::ScopeChunk>,
root_temp_slots: u16,
_index: &SymbolIndex,
file_paths: &LookupMap<FileId, String>,
) -> lir::Program {
let mut names = NameTable::from_entries(prelude.name_seed.clone());
let (mut root_body, mut root_children) = chunk::assemble_scopes(chunks, &mut names);
root_children.extend(prelude.lifted.iter().cloned());
let ends_with_divert = root_body
.last()
.is_some_and(|s| matches!(&s.kind, lir::StmtKind::Divert(_)));
if !ends_with_divert {
let provenance = root_body.last().map_or_else(
|| crate::Provenance::synthetic(crate::NodeClass::Stmt, TextRange::empty(0.into())),
|s| s.provenance,
);
root_body.push(lir::Stmt::new(
lir::StmtKind::Divert(lir::Divert {
target: lir::DivertTarget::Done,
args: Vec::new(),
}),
provenance,
));
}
let mut root = lir::Container {
id: prelude.root_id,
provenance: Provenance::synthetic(NodeClass::Knot, TextRange::empty(0.into())),
name: None,
kind: lir::ContainerKind::Root,
params: Vec::new(),
body: root_body,
children: root_children,
counting_flags: CountingFlags::empty(),
temp_slot_count: root_temp_slots,
labeled: false,
inline: false,
is_function: false,
local: false,
};
apply_counting_flags(&mut root, &prelude.globals);
let struct_shapes = structs::struct_shape_defs(&prelude.shape_table);
lir::Program {
root,
globals: prelude.globals.clone(),
lists: prelude.lists.clone(),
list_items: prelude.list_items.clone(),
externals: prelude.externals.clone(),
name_table: names.into_entries(),
struct_shapes,
private_defs: prelude.private_defs.clone(),
aliases: prelude.aliases.clone(),
file_paths: file_paths.iter().map(|(k, v)| (*k, v.clone())).collect(),
}
}
#[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: &LookupMap<FileId, String>,
diagnostics: &mut Vec<crate::Diagnostic>,
structs: &context::StructCtx<'_>,
tables: context::AnalyzerTables<'_>,
lifted: &mut Vec<lir::Container>,
) -> lir::Container {
let knot_name = &knot.name.text;
let knot_id = lookup_container_id(index, file_id, 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 params = lower_params(&knot.params, names, &temp_map);
let mut block_slot = temp_map.total_slots();
let knot_param_names: Vec<&str> = knot.params.iter().map(|p| p.name.text.as_str()).collect();
let mut ctx = make_ctx(
file_id,
hir_file.native,
resolutions,
index,
&temp_map,
names,
ids,
root_id,
knot_name.clone(),
false,
&knot_param_names,
file_paths,
&mut block_slot,
diagnostics,
structs,
tables,
lifted,
);
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);
drop(ctx);
for stitch in &knot.stitches {
children.push(lower_stitch(
file_id,
hir_file.native,
knot,
stitch,
&temp_map,
resolutions,
index,
names,
ids,
root_id,
file_paths,
&mut block_slot,
diagnostics,
structs,
tables,
lifted,
));
}
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::new(
lir::StmtKind::Divert(lir::Divert {
target: lir::DivertTarget::Address(first_stitch.id),
args: Vec::new(),
}),
knot.ptr,
));
}
lir::Container {
id: knot_id,
provenance: knot.ptr,
name: Some(knot_name.clone()),
kind: lir::ContainerKind::Knot,
params,
body: final_body,
children,
counting_flags: CountingFlags::empty(),
temp_slot_count: block_slot,
labeled: false,
inline: false,
is_function: knot.is_function,
local: knot.is_local,
}
}
#[expect(clippy::too_many_arguments)]
fn lower_stitch(
file_id: FileId,
native: bool,
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: &LookupMap<FileId, String>,
block_slot: &mut u16,
diagnostics: &mut Vec<crate::Diagnostic>,
structs: &context::StructCtx<'_>,
tables: context::AnalyzerTables<'_>,
lifted: &mut Vec<lir::Container>,
) -> lir::Container {
let stitch_name = &stitch.name.text;
let stitch_path = format!("{}.{stitch_name}", knot.name.text);
let stitch_id = lookup_container_id(index, file_id, &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,
native,
resolutions,
index,
temp_map,
names,
ids,
root_id,
stitch_path,
false,
&stitch_param_names,
file_paths,
block_slot,
diagnostics,
structs,
tables,
lifted,
);
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,
provenance: stitch.ptr,
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,
local: stitch.is_local,
}
}
fn try_lower_variant_line(
content: &hir::Content,
ctx: &mut LowerCtx<'_>,
stmt_prov: Provenance,
) -> Option<(lir::StmtKind, Vec<lir::Container>)> {
let en = match recognize::enumerate_variant_contents(content) {
Ok(Some(en)) => en,
Ok(None) => return None,
Err(breach) => {
for part in &content.parts {
if matches!(part, hir::ContentPart::InlineSequence(_)) {
let _ = ctx.ids.next_seq_index();
}
}
let range = content
.ptr
.map_or_else(|| stmt_prov.text_range(), |p| p.text_range());
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range,
message: format!(
"{}: this line's alternatives enumerate to {} whole-line variants, over \
the {} cap — each variant is a real line-table entry, a translation \
unit, and a VO slot, so the product is bounded; split the line or move \
an alternative onto its own line",
crate::DiagnosticCode::E191.title(),
breach.product,
breach.cap,
),
code: crate::DiagnosticCode::E191,
});
return None;
}
};
let mut alt_ids = Vec::with_capacity(en.alts.len());
for alt in &en.alts {
let hir::ContentPart::InlineSequence(seq) = &content.parts[alt.part_idx] else {
return None;
};
alt_ids.push((seq.counter_id.or(seq.container_id)?, seq.ptr));
}
let mut variants = Vec::with_capacity(en.variants.len());
for v in &en.variants {
let Some(emission) = recognize::try_recognize(v, ctx) else {
debug_assert!(
false,
"claims_variant_line admitted a variant try_recognize refuses: {v:?}"
);
return None;
};
variants.push(emission);
}
let mut alts = Vec::with_capacity(en.alts.len());
let mut stubs = Vec::with_capacity(en.alts.len());
for (alt, (id, ptr)) in en.alts.iter().zip(alt_ids) {
let seq_idx = ctx.ids.next_seq_index();
if !ctx.ids.is_bodied_emitted(id) && ctx.ids.mark_shared_emitted(id) {
stubs.push(lir::Container {
id,
provenance: ptr,
name: Some(format!("s-{seq_idx}")),
kind: lir::ContainerKind::Sequence,
params: Vec::new(),
body: Vec::new(),
children: Vec::new(),
counting_flags: CountingFlags::VISITS,
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
local: false,
});
}
alts.push(lir::VariantAltEmission {
container_id: id,
kind: alt.kind,
branch_count: alt.branch_count,
});
}
Some((
lir::StmtKind::EmitLineVariants(lir::VariantLineEmission {
alts,
dims: en.dims,
variants,
}),
stubs,
))
}
#[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;
let mut pending_split_scope = false;
while pos < block.stmts.len() {
let stmt = &block.stmts[pos];
let stmt_prov = ctx.enter_stmt(stmts::stmt_provenance(stmt, ctx));
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::new(
lir::StmtKind::ChoiceSet(lir::ChoiceSet {
choices,
gather_target,
}),
stmt_prov,
));
children.append(&mut choice_children);
let gather_container = build_continuation_container(
&cs.continuation,
ctx,
gather_target,
*gather_counter - 1,
choice_counter,
gather_counter,
stmt_prov,
);
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::new(
lir::StmtKind::EnterContainer(wrapper_id),
stmt_prov,
));
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.kind,
lir::StmtKind::Divert(d) if matches!(
d.target,
lir::DivertTarget::Done
| lir::DivertTarget::End
| lir::DivertTarget::Address(_)
)
) || matches!(&s.kind, lir::StmtKind::ChoiceSet(_))
});
if !ends_terminal {
inner_stmts.push(lir::Stmt::new(
lir::StmtKind::Divert(lir::Divert {
target: lir::DivertTarget::Address(gather_id),
args: Vec::new(),
}),
stmt_prov,
));
}
}
children.push(lir::Container {
id: wrapper_id,
provenance: stmt_prov,
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,
local: 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)| {
ctx.push_block_scope();
let condition = match (b.condition.as_ref(), b.binding.as_ref()) {
(Some(e), Some(binding)) => {
Some(blocks::lower_bound_condition(e, binding, ctx))
}
(Some(e), None) => Some(expr::lower_expr(e, ctx)),
(None, _) => None,
};
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,
provenance: b.ptr,
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,
local: false,
};
children.push(branch_container);
ctx.pop_block_scope();
lir::CondBranch {
condition,
body: vec![lir::Stmt::new(
lir::StmtKind::EnterContainer(branch_id),
b.ptr,
)],
}
})
.collect();
ctx.scope_path = old_scope;
stmts.push(lir::Stmt::new(
lir::StmtKind::Conditional(lir::Conditional { kind, branches }),
stmt_prov,
));
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);
if seq.container_id.is_some() && seq.counter_id.is_none() {
ctx.ids.mark_bodied_emitted(wrapper_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.body, ctx, &mut bc, &mut gc);
let branch_id = b.body.container_id.unwrap_or(ctx.root_id);
let branch_container = lir::Container {
id: branch_id,
provenance: b.ptr,
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,
local: false,
};
wrapper_children.push(branch_container);
vec![lir::Stmt::new(
lir::StmtKind::EnterContainer(branch_id),
b.ptr,
)]
})
.collect();
ctx.scope_path = old_scope;
let wrapper = lir::Container {
id: wrapper_id,
provenance: stmt_prov,
name: Some(display_name),
kind: lir::ContainerKind::Sequence,
params: Vec::new(),
body: vec![lir::Stmt::new(
lir::StmtKind::Sequence(lir::Sequence {
kind: seq.kind,
branches,
counter: seq.counter_id,
}),
stmt_prov,
)],
children: wrapper_children,
counting_flags: content::sequence_counting_flags(seq),
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
local: false,
};
children.push(wrapper);
stmts.push(lir::Stmt::new(
lir::StmtKind::EnterContainer(wrapper_id),
stmt_prov,
));
pos += 1;
}
hir::Stmt::Content(content) => {
if let Some((kind, mut stubs)) = try_lower_variant_line(content, ctx, stmt_prov) {
stmts.push(lir::Stmt::new(kind, stmt_prov));
children.append(&mut stubs);
}
else if let Some(emission) = recognize::try_recognize(content, ctx) {
stmts.push(lir::Stmt::new(lir::StmtKind::EmitLine(emission), stmt_prov));
}
else if let Some((leading, emission, trailing)) =
recognize::try_recognize_with_glue(content, ctx)
{
if leading {
stmts.push(lir::Stmt::new(
lir::StmtKind::EmitContent(lir::Content {
parts: vec![lir::ContentPart::Glue],
tags: vec![],
source_location: None,
}),
stmt_prov,
));
}
stmts.push(lir::Stmt::new(lir::StmtKind::EmitLine(emission), stmt_prov));
if trailing {
stmts.push(lir::Stmt::new(
lir::StmtKind::EmitContent(lir::Content {
parts: vec![lir::ContentPart::Glue],
tags: vec![],
source_location: None,
}),
stmt_prov,
));
}
}
else {
stmts.push(lir::Stmt::new(
lir::StmtKind::EmitContent(content::lower_content(content, ctx)),
stmt_prov,
));
}
children.append(&mut ctx.pending_children);
pos += 1;
}
hir::Stmt::LogicBlock(lb) => {
if matches!(lb.scope, hir::LogicBlockScope::Opens) {
pending_split_scope = true;
}
stmts.extend(blocks::lower_logic_block(lb, ctx));
pos += 1;
}
hir::Stmt::Assignment(assign)
if blocks::try_lower_field_assignment(assign, ctx, &mut stmts) =>
{
children.append(&mut ctx.pending_children);
pos += 1;
}
hir::Stmt::Assignment(assign)
if blocks::try_lower_indexed_assignment(assign, ctx, &mut stmts) =>
{
children.append(&mut ctx.pending_children);
pos += 1;
}
hir::Stmt::ExprStmt(expr) if blocks::try_lower_postfix_stmt(expr, ctx, &mut stmts) => {
children.append(&mut ctx.pending_children);
pos += 1;
}
hir::Stmt::ExprStmt(expr) if blocks::try_lower_mutator_stmt(expr, ctx, &mut stmts) => {
children.append(&mut ctx.pending_children);
pos += 1;
}
hir::Stmt::ExprStmt(expr)
if blocks::try_lower_frame_local_auto_ref_stmt(expr, ctx, &mut stmts) =>
{
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;
}
}
ctx.current_stmt_provenance = stmt_prov;
}
if pending_split_scope {
ctx.pop_block_scope();
}
(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,
provenance: Provenance,
) -> 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() {
let body = if ctx.is_root_content_scope {
let target = ctx
.choice_gather_target
.map_or(lir::DivertTarget::Done, lir::DivertTarget::Address);
vec![lir::Stmt::new(
lir::StmtKind::Divert(lir::Divert {
target,
args: Vec::new(),
}),
provenance,
)]
} else {
Vec::new()
};
return lir::Container {
id,
provenance,
name: Some(display_name),
kind: lir::ContainerKind::Gather,
params: Vec::new(),
body,
children: Vec::new(),
counting_flags: CountingFlags::empty(),
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
local: false,
};
}
let (body, children) =
lower_block_with_children(continuation, ctx, choice_counter, gather_counter);
lir::Container {
id,
provenance,
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,
local: false,
}
}
fn union_source_location(
a: Option<&brink_format::SourceLocation>,
b: Option<&brink_format::SourceLocation>,
) -> Option<brink_format::SourceLocation> {
match (a, b) {
(None, None) => None,
(Some(loc), None) | (None, Some(loc)) => Some(loc.clone()),
(Some(a), Some(b)) if a.file == b.file => Some(brink_format::SourceLocation {
file: a.file.clone(),
range_start: a.range_start.min(b.range_start),
range_end: a.range_end.max(b.range_end),
}),
(Some(a), Some(_)) => Some(a.clone()),
}
}
#[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);
ctx.push_block_scope();
let condition = match (choice.condition.as_ref(), choice.binding.as_ref()) {
(Some(cond_hir), Some(binding)) => {
Some(blocks::lower_bound_condition(cond_hir, binding, ctx))
}
(Some(cond_hir), None) => Some(expr::lower_expr(cond_hir, ctx)),
(None, _) => None,
};
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 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;
ctx.pop_block_scope();
let mut body: Vec<lir::Stmt> = Vec::new();
{
let mut output_parts = Vec::new();
let mut output_tags = Vec::new();
let mut output_source_location = None;
if let Some(ref sc) = start_content {
output_parts.extend(sc.parts.clone());
output_tags.extend(sc.tags.clone());
output_source_location.clone_from(&sc.source_location);
}
if let Some(ref ic) = inner_content {
output_parts.extend(ic.parts.clone());
output_tags.extend(ic.tags.clone());
output_source_location =
union_source_location(output_source_location.as_ref(), ic.source_location.as_ref());
}
if !output_parts.is_empty() || !output_tags.is_empty() {
body.push(lir::Stmt::new(
lir::StmtKind::ChoiceOutput {
content: lir::Content {
parts: output_parts,
tags: output_tags,
source_location: output_source_location,
},
emission: output_emission.clone(),
},
choice.ptr,
));
}
}
body.extend(body_stmts);
let ends_with_terminal = body.last().is_some_and(|s| {
matches!(
&s.kind,
lir::StmtKind::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.kind, lir::StmtKind::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::new(lir::StmtKind::Divert(divert), choice.ptr));
}
}
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,
provenance: choice.ptr,
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,
local: 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,
native: bool,
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,
is_root_content_scope: bool,
param_names: &[&str],
file_paths: &'a LookupMap<FileId, String>,
next_block_slot: &'a mut u16,
diagnostics: &'a mut Vec<crate::Diagnostic>,
structs: &'a context::StructCtx<'a>,
tables: context::AnalyzerTables<'a>,
lifted: &'a mut Vec<lir::Container>,
) -> LowerCtx<'a> {
LowerCtx {
file,
native,
resolutions,
index,
temps,
names,
ids,
scope_path,
is_root_content_scope,
pending_children: Vec::new(),
visible_temps: param_names.iter().map(|s| (*s).to_string()).collect(),
file_paths,
root_id,
choice_gather_target: None,
next_block_slot,
block_scopes: Vec::new(),
as_binding_slots: LookupSet::new(),
block_scoped_temp_names: LookupSet::new(),
diagnostics,
loop_depth: 0,
structs,
temp_shapes: LookupMap::new(),
tables,
lifted,
current_stmt_provenance: Provenance::synthetic(NodeClass::Stmt, TextRange::empty(0.into())),
}
}
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,
file: FileId,
name: &str,
) -> Option<brink_format::DefinitionId> {
use crate::symbols::SymbolKind;
fn is_container(info: &crate::symbols::SymbolInfo) -> bool {
matches!(
info.kind,
SymbolKind::Knot | SymbolKind::Stitch | SymbolKind::Label
)
}
index.by_name.get(name).and_then(|ids| {
ids.iter()
.find(|&&id| {
index
.symbols
.get(&id)
.is_some_and(|info| is_container(info) && info.file == file)
})
.or_else(|| {
ids.iter()
.find(|&&id| index.symbols.get(&id).is_some_and(is_container))
})
.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, false);
}
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],
in_local_scope: bool,
) {
let in_local_scope = in_local_scope || container.local;
if in_local_scope
&& matches!(
container.kind,
lir::ContainerKind::Knot | lir::ContainerKind::Stitch
)
{
container.counting_flags |= CountingFlags::VISITS;
}
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, in_local_scope);
}
}
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.kind {
lir::StmtKind::EmitContent(content) | lir::StmtKind::ChoiceOutput { content, .. } => {
collect_counting_refs_content(content, visit_ids, turns_ids);
}
lir::StmtKind::EmitLine(emission) | lir::StmtKind::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::StmtKind::Assign { value: e, .. }
| lir::StmtKind::DeclareTemp { value: Some(e), .. }
| lir::StmtKind::Return { value: Some(e), .. }
| lir::StmtKind::ExprStmt(e) => {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
lir::StmtKind::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::StmtKind::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::StmtKind::Sequence(seq) => {
for branch in &seq.branches {
collect_counting_refs(branch, visit_ids, turns_ids);
}
}
lir::StmtKind::Divert(d) => {
for arg in &d.args {
collect_counting_refs_call_arg(arg, visit_ids, turns_ids);
}
}
lir::StmtKind::TunnelCall(tc) => {
for t in &tc.targets {
for arg in &t.args {
collect_counting_refs_call_arg(arg, visit_ids, turns_ids);
}
}
}
lir::StmtKind::ThreadStart(ts) => {
for arg in &ts.args {
collect_counting_refs_call_arg(arg, 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_call_arg(
arg: &lir::CallArg,
visit_ids: &mut Vec<brink_format::DefinitionId>,
turns_ids: &mut Vec<brink_format::DefinitionId>,
) {
match arg {
lir::CallArg::Value(e) => collect_counting_refs_expr(e, visit_ids, turns_ids),
lir::CallArg::RefProjection { segments, .. } => {
for seg in segments {
collect_counting_refs_expr(seg, visit_ids, turns_ids);
}
}
lir::CallArg::RefGlobal(_) | lir::CallArg::RefTemp(_, _) => {}
}
}
fn collect_counting_refs_expr(
expr: &lir::Expr,
visit_ids: &mut Vec<brink_format::DefinitionId>,
turns_ids: &mut Vec<brink_format::DefinitionId>,
) {
match &expr.kind {
lir::ExprKind::VisitCount(id) => visit_ids.push(*id),
lir::ExprKind::DivertTarget(id) => {
visit_ids.push(*id);
turns_ids.push(*id);
}
lir::ExprKind::CallBuiltin {
builtin: lir::BuiltinFn::TurnsSince,
args,
} => {
for a in args {
if let lir::ExprKind::DivertTarget(id) = &a.kind {
turns_ids.push(*id);
}
collect_counting_refs_expr(a, visit_ids, turns_ids);
}
}
lir::ExprKind::Prefix(_, inner) | lir::ExprKind::Postfix(inner, _) => {
collect_counting_refs_expr(inner, visit_ids, turns_ids);
}
lir::ExprKind::Infix(lhs, _, rhs) | lir::ExprKind::Coalesce { lhs, rhs, shape: _ } => {
collect_counting_refs_expr(lhs, visit_ids, turns_ids);
collect_counting_refs_expr(rhs, visit_ids, turns_ids);
}
lir::ExprKind::Call { args, .. } | lir::ExprKind::CallExternal { args, .. } => {
for arg in args {
collect_counting_refs_call_arg(arg, visit_ids, turns_ids);
}
}
lir::ExprKind::CallBuiltin { args, .. } => {
for a in args {
collect_counting_refs_expr(a, visit_ids, turns_ids);
}
}
lir::ExprKind::String(s) => {
for p in &s.parts {
if let lir::StringPart::Interpolation(e) = p {
collect_counting_refs_expr(e, visit_ids, turns_ids);
}
}
}
_ => {}
}
}
const ROOT_TERMINUS_NAME: &str = "g-final";
fn attach_root_final_gather(children: &mut Vec<lir::Container>, ids: &mut context::IdAllocator) {
let terminus_id = ids.alloc_address("#root-terminus");
if !patch_root_loose_end(children, terminus_id) {
return;
}
let provenance = Provenance::synthetic(NodeClass::Stmt, TextRange::empty(0.into()));
children.push(lir::Container {
id: terminus_id,
provenance,
name: Some(ROOT_TERMINUS_NAME.to_string()),
kind: lir::ContainerKind::Gather,
params: Vec::new(),
body: vec![lir::Stmt::new(
lir::StmtKind::Divert(lir::Divert {
target: lir::DivertTarget::Done,
args: Vec::new(),
}),
provenance,
)],
children: Vec::new(),
counting_flags: CountingFlags::empty(),
temp_slot_count: 0,
labeled: false,
inline: false,
is_function: false,
local: false,
});
}
fn patch_root_loose_end(
children: &mut [lir::Container],
terminus: brink_format::DefinitionId,
) -> bool {
let Some(gather) = children
.last_mut()
.filter(|c| c.kind == lir::ContainerKind::Gather)
else {
return false;
};
if gather
.body
.last()
.is_some_and(|s| matches!(&s.kind, lir::StmtKind::ChoiceSet(_)))
{
return patch_root_loose_end(&mut gather.children, terminus);
}
if gather.inline {
return false;
}
let ends_terminal = gather.body.last().is_some_and(|s| {
matches!(
&s.kind,
lir::StmtKind::Divert(d)
if matches!(
d.target,
lir::DivertTarget::End
| lir::DivertTarget::Done
| lir::DivertTarget::Address(_)
)
)
});
if ends_terminal {
return false;
}
let provenance = gather.provenance;
gather.body.push(lir::Stmt::new(
lir::StmtKind::Divert(lir::Divert {
target: lir::DivertTarget::Address(terminus),
args: Vec::new(),
}),
provenance,
));
true
}
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.kind, lir::StmtKind::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.kind,
lir::StmtKind::Divert(d)
if matches!(
d.target,
lir::DivertTarget::End
| lir::DivertTarget::Done
| lir::DivertTarget::Address(_)
)
)
});
if gather_body_ends_terminal {
return;
}
let provenance = gather.provenance;
gather
.body
.push(lir::Stmt::new(lir::StmtKind::Divert(divert), provenance));
}