use brink_format::NameId;
use super::context::NameTable;
use super::lir;
#[derive(Clone, Default)]
pub struct ScopeChunk {
pub body: Vec<lir::Stmt>,
pub children: Vec<lir::Container>,
pub local_names: Vec<String>,
}
impl ScopeChunk {
pub fn root_content(
body: Vec<lir::Stmt>,
children: Vec<lir::Container>,
local_names: Vec<String>,
) -> Self {
Self {
body,
children,
local_names,
}
}
pub fn knot(
container: lir::Container,
lifted: Vec<lir::Container>,
local_names: Vec<String>,
) -> Self {
let mut children = Vec::with_capacity(1 + lifted.len());
children.push(container);
children.extend(lifted);
Self {
body: Vec::new(),
children,
local_names,
}
}
}
pub(super) fn assemble_scopes(
chunks: Vec<ScopeChunk>,
names: &mut NameTable,
) -> (Vec<lir::Stmt>, Vec<lir::Container>) {
let mut body = Vec::new();
let mut children = Vec::new();
for mut chunk in chunks {
let map: Vec<NameId> = chunk.local_names.iter().map(|s| names.intern(s)).collect();
remap_stmts(&mut chunk.body, &map);
for c in &mut chunk.children {
remap_container(c, &map);
}
body.extend(chunk.body);
children.append(&mut chunk.children);
}
(body, children)
}
fn relocate(id: &mut NameId, map: &[NameId]) {
if let Some(assembled) = map.get(id.0 as usize) {
*id = *assembled;
}
}
fn remap_container(c: &mut lir::Container, map: &[NameId]) {
for p in &mut c.params {
relocate(&mut p.name, map);
}
remap_stmts(&mut c.body, map);
for child in &mut c.children {
remap_container(child, map);
}
}
fn remap_stmts(stmts: &mut [lir::Stmt], map: &[NameId]) {
for s in stmts {
remap_stmt(s, map);
}
}
fn remap_stmt(stmt: &mut lir::Stmt, map: &[NameId]) {
use lir::StmtKind;
match &mut stmt.kind {
StmtKind::EmitContent(content) => remap_content(content, map),
StmtKind::EmitLine(emission) | StmtKind::EvalLine(emission) => {
remap_emission(emission, map);
}
StmtKind::EmitLineVariants(v) => {
for emission in &mut v.variants {
remap_emission(emission, map);
}
}
StmtKind::ChoiceOutput { content, emission } => {
remap_content(content, map);
if let Some(e) = emission {
remap_emission(e, map);
}
}
StmtKind::Divert(d) => remap_divert(d, map),
StmtKind::TunnelCall(t) => {
for target in &mut t.targets {
remap_divert_target(&mut target.target, map);
remap_call_args(&mut target.args, map);
}
}
StmtKind::ThreadStart(t) => {
remap_divert_target(&mut t.target, map);
remap_call_args(&mut t.args, map);
}
StmtKind::DeclareTemp {
slot: _,
name,
value,
synthetic: _,
} => {
relocate(name, map);
if let Some(v) = value {
remap_expr(v, map);
}
}
StmtKind::Assign {
target,
op: _,
value,
} => {
remap_assign_target(target, map);
remap_expr(value, map);
}
StmtKind::Return {
value,
is_tunnel: _,
args,
} => {
if let Some(v) = value {
remap_expr(v, map);
}
remap_call_args(args, map);
}
StmtKind::ChoiceSet(cs) => {
for choice in &mut cs.choices {
remap_choice(choice, map);
}
}
StmtKind::Conditional(cond) => remap_conditional(cond, map),
StmtKind::Sequence(seq) => remap_sequence(seq, map),
StmtKind::ExprStmt(e) | StmtKind::AttachElement(e) => remap_expr(e, map),
StmtKind::LogicWhile(w) => {
remap_expr(&mut w.condition, map);
remap_stmts(&mut w.body, map);
remap_stmts(&mut w.post, map);
}
StmtKind::EnterContainer(_)
| StmtKind::EndOfLine
| StmtKind::LogicBreak
| StmtKind::LogicContinue
| StmtKind::EndElementRun => {}
}
}
fn remap_divert(d: &mut lir::Divert, map: &[NameId]) {
remap_divert_target(&mut d.target, map);
remap_call_args(&mut d.args, map);
}
fn remap_divert_target(t: &mut lir::DivertTarget, map: &[NameId]) {
use lir::DivertTarget;
match t {
DivertTarget::VariableTemp(_, name) => relocate(name, map),
DivertTarget::Address(_)
| DivertTarget::Variable(_)
| DivertTarget::Done
| DivertTarget::End => {}
}
}
fn remap_call_args(args: &mut [lir::CallArg], map: &[NameId]) {
use lir::CallArg;
for arg in args {
match arg {
CallArg::Value(e) => remap_expr(e, map),
CallArg::RefTemp(_, name) => relocate(name, map),
CallArg::RefGlobal(_) => {}
CallArg::RefProjection { segments, .. } => {
for seg in segments {
remap_expr(seg, map);
}
}
}
}
}
fn remap_assign_target(t: &mut lir::AssignTarget, map: &[NameId]) {
use lir::AssignTarget;
match t {
AssignTarget::Temp(_, name) => relocate(name, map),
AssignTarget::Global(_) => {}
}
}
fn remap_choice(choice: &mut lir::Choice, map: &[NameId]) {
if let Some(c) = &mut choice.condition {
remap_expr(c, map);
}
for content in [
&mut choice.start_content,
&mut choice.choice_only_content,
&mut choice.inner_content,
]
.into_iter()
.flatten()
{
remap_content(content, map);
}
if let Some(e) = &mut choice.display_emission {
remap_emission(e, map);
}
if let Some(e) = &mut choice.output_emission {
remap_emission(e, map);
}
remap_tags(&mut choice.tags, map);
}
fn remap_conditional(cond: &mut lir::Conditional, map: &[NameId]) {
if let lir::CondKind::Switch(e) = &mut cond.kind {
remap_expr(e, map);
}
for branch in &mut cond.branches {
if let Some(c) = &mut branch.condition {
remap_expr(c, map);
}
remap_stmts(&mut branch.body, map);
}
}
fn remap_sequence(seq: &mut lir::Sequence, map: &[NameId]) {
for branch in &mut seq.branches {
remap_stmts(branch, map);
}
}
fn remap_emission(emission: &mut lir::ContentEmission, map: &[NameId]) {
if let lir::RecognizedLine::Template { slot_exprs, .. } = &mut emission.line {
for e in slot_exprs {
remap_expr(e, map);
}
}
remap_tags(&mut emission.tags, map);
}
fn remap_content(content: &mut lir::Content, map: &[NameId]) {
remap_content_parts(&mut content.parts, map);
remap_tags(&mut content.tags, map);
}
fn remap_tags(tags: &mut [Vec<lir::ContentPart>], map: &[NameId]) {
for tag in tags {
remap_content_parts(tag, map);
}
}
fn remap_content_parts(parts: &mut [lir::ContentPart], map: &[NameId]) {
use lir::ContentPart;
for part in parts {
match part {
ContentPart::Interpolation(e) => remap_expr(e, map),
ContentPart::InlineConditional(cond) => remap_conditional(cond, map),
ContentPart::InlineSequence(seq) => remap_sequence(seq, map),
ContentPart::Text(_)
| ContentPart::Glue
| ContentPart::Spring
| ContentPart::EnterSequence(_) => {}
}
}
}
#[expect(
clippy::too_many_lines,
reason = "exhaustive per-variant Expr walk — one arm per variant is the point"
)]
fn remap_expr(expr: &mut lir::Expr, map: &[NameId]) {
use lir::ExprKind as Expr;
match &mut expr.kind {
Expr::GetTemp(_, name) | Expr::TakeTemp(_, name) => relocate(name, map),
Expr::String(s) => remap_string(s, map),
Expr::Prefix(_, e)
| Expr::Postfix(e, _)
| Expr::OptionSome(e)
| Expr::SeqMin(e)
| Expr::SeqMax(e)
| Expr::SeqFirst(e)
| Expr::SeqLast(e)
| Expr::MapClear(e)
| Expr::RandChance(e)
| Expr::RandPick(e)
| Expr::RandShuffle(e)
| Expr::SeqSorted(e)
| Expr::RangeNonEmpty(e)
| Expr::RandRoll(e)
| Expr::HeapPeek(e) => remap_expr(e, map),
Expr::SeqSortedBy { seq, cmp: second }
| Expr::HeapPush { seq, value: second }
| Expr::SeqMap { seq, f: second }
| Expr::SeqFilter { seq, pred: second }
| Expr::SeqFilterMap { seq, f: second }
| Expr::SeqEach { seq, f: second }
| Expr::SeqMapEach { seq, f: second } => {
remap_expr(seq, map);
remap_expr(second, map);
}
Expr::SeqFold { seq, init, f } => {
remap_expr(seq, map);
remap_expr(init, map);
remap_expr(f, map);
}
Expr::RangeMake { start, end, .. } => {
remap_expr(start, map);
remap_expr(end, map);
}
Expr::Infix(l, _, r)
| Expr::Coalesce {
lhs: l,
rhs: r,
shape: _,
} => {
remap_expr(l, map);
remap_expr(r, map);
}
Expr::Call { target: _, args }
| Expr::CallVariable { target: _, args }
| Expr::CallExternal {
target: _, args, ..
} => remap_call_args(args, map),
Expr::CallVariableTemp {
slot: _,
name,
args,
} => {
relocate(name, map);
remap_call_args(args, map);
}
Expr::CallBuiltin { builtin: _, args } | Expr::Tower { op: _, args } => {
for e in args {
remap_expr(e, map);
}
}
Expr::MakeFnValue { target: _, bound } => remap_call_args(bound, map),
Expr::CallValue { callee, args } | Expr::BindValue { callee, args } => {
remap_expr(callee, map);
for e in args {
remap_expr(e, map);
}
}
Expr::ArrayNew(elems) => {
for e in elems {
remap_expr(e, map);
}
}
Expr::MapNew(pairs) | Expr::WeightedNew { pairs } => {
for (k, v) in pairs {
remap_expr(k, map);
remap_expr(v, map);
}
}
Expr::Index { base, index } | Expr::SeqRemoveAt { base, index } => {
remap_expr(base, map);
remap_expr(index, map);
}
Expr::OptionBind { value, name, .. } => {
remap_expr(value, map);
relocate(name, map);
}
Expr::IndexSet { base, index, value } => {
remap_expr(base, map);
remap_expr(index, map);
remap_expr(value, map);
}
Expr::CollectionLen(e)
| Expr::CollectionKeys(e)
| Expr::CollectionValues(e)
| Expr::ConvertInt(e)
| Expr::ConvertFloat(e)
| Expr::ConvertString(e) => {
remap_expr(e, map);
}
Expr::CollectionContains { container, needle } => {
remap_expr(container, map);
remap_expr(needle, map);
}
Expr::CollectionInsert { base, key, value } => {
remap_expr(base, map);
remap_expr(key, map);
remap_expr(value, map);
}
Expr::CollectionRemove { base, key } => {
remap_expr(base, map);
remap_expr(key, map);
}
Expr::CharAt { s, index } => {
remap_expr(s, map);
remap_expr(index, map);
}
Expr::StrFind { s, sub } => {
remap_expr(s, map);
remap_expr(sub, map);
}
Expr::SeqIndexOf { seq, needle } => {
remap_expr(seq, map);
remap_expr(needle, map);
}
Expr::MapGetOpt { map: m, key } => {
remap_expr(m, map);
remap_expr(key, map);
}
Expr::MapContainsValue { map: m, value } => {
remap_expr(m, map);
remap_expr(value, map);
}
Expr::SeqPop { root } | Expr::HeapPop { root } => remap_assign_target(root, map),
Expr::RecordNew {
shape_id: _,
fields,
prelude,
} => {
for e in fields {
remap_expr(e, map);
}
for (_slot, name, e) in prelude {
relocate(name, map);
remap_expr(e, map);
}
}
Expr::RecordGet {
base,
field,
static_offset: _,
} => {
remap_expr(base, map);
relocate(field, map);
}
Expr::RecordSet {
base,
field,
static_offset: _,
value,
} => {
remap_expr(base, map);
relocate(field, map);
remap_expr(value, map);
}
Expr::Fragment(stmts) => remap_stmts(stmts, map),
Expr::Int(_)
| Expr::Float(_)
| Expr::Bool(_)
| Expr::Null
| Expr::GetGlobal(_)
| Expr::TakeGlobal(_)
| Expr::VisitCount(_)
| Expr::DivertTarget(_)
| Expr::ListLiteral { .. }
| Expr::ConstLiteral(_)
| Expr::OptionNone
| Expr::RandFloat => {}
}
}
fn remap_string(s: &mut lir::StringExpr, map: &[NameId]) {
for part in &mut s.parts {
if let lir::StringPart::Interpolation(e) = part {
remap_expr(e, map);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use brink_format::NameId;
fn test_provenance() -> crate::Provenance {
crate::Provenance::synthetic(crate::NodeClass::Stmt, rowan::TextRange::empty(0.into()))
}
#[test]
fn relocate_maps_local_to_assembled() {
let map = vec![NameId(5), NameId(2), NameId(9)];
let mut id = NameId(1);
relocate(&mut id, &map);
assert_eq!(id, NameId(2));
}
#[test]
fn remap_rewrites_nested_name_ids() {
let map = vec![NameId(7), NameId(3)];
let mut expr = lir::ExprKind::Infix(
Box::new(lir::ExprKind::GetTemp(0, NameId(0)).at(test_provenance())),
crate::InfixOp::Add,
Box::new(
lir::ExprKind::RecordGet {
base: Box::new(lir::ExprKind::GetTemp(1, NameId(0)).at(test_provenance())),
field: NameId(1),
static_offset: None,
}
.at(test_provenance()),
),
)
.at(test_provenance());
remap_expr(&mut expr, &map);
let (l, r) = match &expr.kind {
lir::ExprKind::Infix(l, _, r) => Some((l, r)),
_ => None,
}
.expect("expected infix");
assert!(matches!(l.kind, lir::ExprKind::GetTemp(0, NameId(7))));
let (base, field) = match &r.kind {
lir::ExprKind::RecordGet { base, field, .. } => Some((base, field)),
_ => None,
}
.expect("expected record get");
assert!(matches!(base.kind, lir::ExprKind::GetTemp(1, NameId(7))));
assert_eq!(*field, NameId(3));
}
#[test]
fn assemble_dedups_against_existing_table() {
let mut names = NameTable::new();
let pre = names.intern("existing");
let chunk = ScopeChunk::root_content(
vec![lir::Stmt::new(
lir::StmtKind::DeclareTemp {
slot: 0,
name: NameId(1), value: Some(lir::ExprKind::GetTemp(0, NameId(0)).at(test_provenance())), synthetic: false,
},
test_provenance(),
)],
Vec::new(),
vec!["existing".to_string(), "fresh".to_string()],
);
let (mut body, children) = assemble_scopes(vec![chunk], &mut names);
assert!(children.is_empty());
let entries = names.into_entries();
assert_eq!(entries, vec!["existing".to_string(), "fresh".to_string()]);
assert_eq!(pre, NameId(0));
let (name, value) = match body.remove(0).kind {
lir::StmtKind::DeclareTemp { name, value, .. } => Some((name, value)),
_ => None,
}
.expect("expected declare temp");
assert_eq!(name, NameId(1)); assert!(matches!(
value.map(|v| v.kind),
Some(lir::ExprKind::GetTemp(0, NameId(0)))
)); }
}