use crate::hir;
use crate::symbols::SymbolKind;
use crate::{AssignOp, Diagnostic, DiagnosticCode, InfixOp};
use super::context::LowerCtx;
use super::context::TypeMode;
use super::context::UfcsVerdict;
use super::expr::lower_expr;
use super::lir;
pub(super) fn lower_logic_block(lb: &hir::LogicBlock, ctx: &mut LowerCtx<'_>) -> Vec<lir::Stmt> {
use hir::LogicBlockScope as Scope;
match lb.scope {
Scope::Standalone => {
ctx.push_block_scope();
let out = lower_block_stmt_list(&lb.stmts, ctx);
ctx.pop_block_scope();
out
}
Scope::Opens => {
ctx.push_block_scope();
lower_block_stmt_list(&lb.stmts, ctx)
}
Scope::Continues => lower_block_stmt_list(&lb.stmts, ctx),
}
}
pub(super) fn lower_block_stmt_list(
stmts: &[hir::BlockStmt],
ctx: &mut LowerCtx<'_>,
) -> Vec<lir::Stmt> {
let mut out = Vec::new();
for stmt in stmts {
lower_block_stmt(stmt, ctx, &mut out);
}
out
}
fn block_stmt_provenance(stmt: &hir::BlockStmt, ctx: &LowerCtx<'_>) -> crate::Provenance {
match stmt {
hir::BlockStmt::TempDecl(decl) => decl.ptr,
hir::BlockStmt::Assignment(assign) => assign.ptr,
hir::BlockStmt::Return(ret) => ret.ptr.unwrap_or(ctx.current_stmt_provenance),
hir::BlockStmt::If(if_stmt) => if_stmt.ptr,
hir::BlockStmt::While(w) => w.ptr,
hir::BlockStmt::For(f) => f.ptr,
hir::BlockStmt::Break(ptr) | hir::BlockStmt::Continue(ptr) => *ptr,
hir::BlockStmt::ExprStmt(e) => crate::hir::expr_span(e)
.map_or(ctx.current_stmt_provenance, |r| {
ctx.provenance_at(r, crate::NodeClass::Expr)
}),
hir::BlockStmt::Await(a) => a.ptr,
}
}
fn lower_block_stmt(stmt: &hir::BlockStmt, ctx: &mut LowerCtx<'_>, out: &mut Vec<lir::Stmt>) {
let provenance = ctx.enter_stmt(block_stmt_provenance(stmt, ctx));
match stmt {
hir::BlockStmt::TempDecl(decl) => lower_block_temp_decl(decl, ctx, out),
hir::BlockStmt::Assignment(assign) => lower_block_assignment(assign, ctx, out),
hir::BlockStmt::Return(ret) => {
let value = ret.value.as_ref().map(|e| lower_expr(e, ctx));
out.push(lir::Stmt::new(
lir::StmtKind::Return {
value,
is_tunnel: ret.kind == hir::ReturnKind::TunnelRedirect,
args: Vec::new(),
},
provenance,
));
}
hir::BlockStmt::If(if_stmt) => {
let mut branches = Vec::new();
lower_if_branch(if_stmt, ctx, &mut branches);
out.push(lir::Stmt::new(
lir::StmtKind::Conditional(lir::Conditional {
kind: lir::CondKind::IfElse,
branches,
}),
provenance,
));
}
hir::BlockStmt::While(w) => {
if w.is_await {
super::stmts::emit_await_lowering_fence(ctx, w.ptr.text_range());
return;
}
ctx.push_block_scope();
let condition = match &w.binding {
Some(binding) => lower_bound_condition(&w.condition, binding, ctx),
None => lower_expr(&w.condition, ctx),
};
ctx.loop_depth += 1;
let body = lower_block_stmt_list(&w.body, ctx);
ctx.loop_depth -= 1;
ctx.pop_block_scope();
out.push(lir::Stmt::new(
lir::StmtKind::LogicWhile(lir::LogicWhile {
condition,
body,
post: Vec::new(),
}),
provenance,
));
}
hir::BlockStmt::For(f) => lower_for_stmt(f, ctx, out),
hir::BlockStmt::Break(ptr) => {
lower_loop_control(ptr, "break", lir::StmtKind::LogicBreak, ctx, out);
}
hir::BlockStmt::Continue(ptr) => {
lower_loop_control(ptr, "continue", lir::StmtKind::LogicContinue, ctx, out);
}
hir::BlockStmt::ExprStmt(expr) => {
if !try_lower_postfix_stmt(expr, ctx, out)
&& !try_lower_mutator_stmt(expr, ctx, out)
&& !try_lower_frame_local_auto_ref_stmt(expr, ctx, out)
{
out.push(lir::Stmt::new(
lir::StmtKind::ExprStmt(lower_expr(expr, ctx)),
provenance,
));
}
}
hir::BlockStmt::Await(a) => {
super::stmts::emit_await_lowering_fence(ctx, a.ptr.text_range());
}
}
}
fn lower_loop_control(
ptr: &crate::Provenance,
keyword: &str,
kind: lir::StmtKind,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) {
if ctx.loop_depth == 0 {
ctx.diagnostics.push(Diagnostic {
file: ctx.file,
range: ptr.text_range(),
message: format!(
"{}: `{keyword}` used outside any enclosing while/for loop",
DiagnosticCode::E057.title(),
),
code: DiagnosticCode::E057,
});
return;
}
out.push(lir::Stmt::new(kind, *ptr));
}
fn lower_if_branch(
if_stmt: &hir::IfStmt,
ctx: &mut LowerCtx<'_>,
branches: &mut Vec<lir::CondBranch>,
) {
ctx.push_block_scope();
let condition = Some(match &if_stmt.binding {
Some(binding) => lower_bound_condition(&if_stmt.condition, binding, ctx),
None => lower_expr(&if_stmt.condition, ctx),
});
let body = lower_block_stmt_list(&if_stmt.body, ctx);
ctx.pop_block_scope();
branches.push(lir::CondBranch { condition, body });
match &if_stmt.else_branch {
Some(hir::ElseBranch::ElseIf(inner)) => lower_if_branch(inner, ctx, branches),
Some(hir::ElseBranch::Else(else_body)) => {
ctx.push_block_scope();
let body = lower_block_stmt_list(else_body, ctx);
ctx.pop_block_scope();
branches.push(lir::CondBranch {
condition: None,
body,
});
}
None => {}
}
}
pub(super) fn lower_bound_condition(
condition: &hir::Expr,
binding: &crate::Name,
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
let value = lower_expr(condition, ctx);
let (slot, name) = declare_shadow_checked(&binding.text, binding.range, ctx);
ctx.as_binding_slots.insert(slot);
lir::ExprKind::OptionBind {
value: Box::new(value),
slot,
name,
}
.at(ctx.current_stmt_provenance)
}
fn declare_shadow_checked(
name: &str,
range: rowan::TextRange,
ctx: &mut LowerCtx<'_>,
) -> (u16, brink_format::NameId) {
if ctx.is_name_visible(name) {
ctx.diagnostics.push(Diagnostic {
file: ctx.file,
range,
message: format!(
"`{name}` shadows an already-visible temp — block-scoped `temp` \
declarations may shadow outer temps (docs/t1b-surface-spec.md §2), \
but double-check this is intentional"
),
code: DiagnosticCode::E054,
});
}
let slot = ctx.alloc_block_slot();
let name_id = ctx.names.intern(name);
ctx.declare_block_local(name.to_string(), slot);
(slot, name_id)
}
fn lower_block_temp_decl(decl: &hir::TempDecl, ctx: &mut LowerCtx<'_>, out: &mut Vec<lir::Stmt>) {
let value = decl.value.as_ref().map(|e| lower_expr(e, ctx));
let (slot, name) = declare_shadow_checked(&decl.name.text, decl.name.range, ctx);
ctx.record_temp_annotation(slot, decl.annotation.as_ref());
out.push(lir::Stmt::new(
lir::StmtKind::DeclareTemp {
slot,
name,
value,
synthetic: decl.synthetic,
},
ctx.current_stmt_provenance,
));
}
fn lower_block_assignment(
assign: &hir::Assignment,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) {
if try_lower_field_assignment(assign, ctx, out) {
return;
}
if let hir::Expr::Index(idx) = &assign.target {
lower_indexed_assignment(idx, assign.op, &assign.value, ctx, out);
return;
}
if let Some(target) = super::stmts::lower_assign_target(&assign.target, ctx) {
let value = lower_expr(&assign.value, ctx);
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target,
op: assign.op,
value,
},
ctx.current_stmt_provenance,
));
}
}
pub(super) fn try_lower_field_assignment(
assign: &hir::Assignment,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) -> bool {
if let hir::Expr::Path(path) = &assign.target
&& path.segments.len() > 1
&& let Some(info) = ctx.resolve_path(path.range)
&& matches!(
info.kind,
SymbolKind::Variable | SymbolKind::Constant | SymbolKind::Param | SymbolKind::Temp
)
{
if path.segments.len() > 2 {
emit_chained_field_write_diagnostic(path.range, ctx);
} else {
lower_single_level_field_write(path, info, assign.op, &assign.value, ctx, out);
}
return true;
}
if let hir::Expr::FieldAccess(fa) = &assign.target {
emit_chained_field_write_diagnostic(fa.ptr.text_range(), ctx);
return true;
}
false
}
pub(super) fn try_lower_indexed_assignment(
assign: &hir::Assignment,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) -> bool {
let hir::Expr::Index(idx) = &assign.target else {
return false;
};
lower_indexed_assignment(idx, assign.op, &assign.value, ctx, out);
true
}
fn emit_chained_field_write_diagnostic(range: rowan::TextRange, ctx: &mut LowerCtx<'_>) {
emit_field_projection_refusal(range, ctx, DiagnosticCode::E074.title());
}
pub(super) const FIELD_PROJECTION_MUTATOR_ARG: &str = "field-projection mutator argument (`pop(a.items)`) is not supported — copy the field into a \
temp, mutate it, and write it back";
pub(super) const FIELD_PROJECTION_POSTFIX_TARGET: &str = "field-projection increment/decrement target (`a.count++`) is not supported — copy the field \
into a temp, mutate it, and write it back";
pub(super) const FIELD_PROJECTION_IMPLICIT_REF_ARG: &str = "field-projection argument to a `ref` parameter (`f(a.items)`) is not supported without an \
explicit `ref` — write `f(ref a.items)` to pass a real projection";
fn emit_field_projection_refusal(range: rowan::TextRange, ctx: &mut LowerCtx<'_>, message: &str) {
ctx.diagnostics.push(Diagnostic {
file: ctx.file,
range,
message: message.to_string(),
code: DiagnosticCode::E074,
});
}
pub(super) fn reject_field_projection_index_root(
root_expr: &hir::Expr,
ctx: &mut LowerCtx<'_>,
message: Option<&str>,
) -> bool {
if let hir::Expr::Path(path) = root_expr
&& reject_field_projection_path(path, ctx, message)
{
return true;
}
if let hir::Expr::FieldAccess(fa) = root_expr {
emit_field_projection_refusal(
fa.ptr.text_range(),
ctx,
message.unwrap_or_else(|| DiagnosticCode::E074.title()),
);
return true;
}
false
}
pub(super) fn reject_field_projection_path(
path: &hir::Path,
ctx: &mut LowerCtx<'_>,
message: Option<&str>,
) -> bool {
if path.segments.len() > 1
&& let Some(info) = ctx.resolve_path(path.range)
&& matches!(
info.kind,
SymbolKind::Variable | SymbolKind::Constant | SymbolKind::Param | SymbolKind::Temp
)
{
emit_field_projection_refusal(
path.range,
ctx,
message.unwrap_or_else(|| DiagnosticCode::E074.title()),
);
return true;
}
false
}
fn lower_single_level_field_write(
path: &hir::Path,
head_info: &crate::symbols::SymbolInfo,
op: AssignOp,
value_expr: &hir::Expr,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) {
#[expect(
clippy::indexing_slicing,
reason = "caller already proved path.segments.len() == 2"
)]
let field_name = path.segments[1].text.clone();
#[expect(
clippy::indexing_slicing,
reason = "caller already proved path.segments.len() == 2"
)]
let head_name = path.segments[0].text.clone();
let (root_target, root_shape) = match head_info.kind {
SymbolKind::Variable | SymbolKind::Constant => {
if super::stmts::reject_const_write(head_info, path.range, ctx) {
return;
}
(
lir::AssignTarget::Global(head_info.id),
ctx.global_shape(head_info.id),
)
}
SymbolKind::Param | SymbolKind::Temp => {
let Some(slot) = ctx.temp_slot(&head_name) else {
return;
};
if super::stmts::reject_as_binding_write(slot, &head_name, path.range, ctx) {
return;
}
let name_id = ctx.names.intern(&head_name);
(lir::AssignTarget::Temp(slot, name_id), ctx.temp_shape(slot))
}
_ => return,
};
let static_offset = if ctx.structs.type_mode == TypeMode::Strict {
root_shape
.and_then(|d| ctx.structs.shapes.get_by_def(d))
.and_then(|shape| shape.field(&field_name))
.map(|(offset, _)| offset)
} else {
None
};
let field = ctx.names.intern(&field_name);
let rhs_value = lower_expr(value_expr, ctx);
let (rhs_slot, rhs_name) = declare_synthetic("__rhs", rhs_value, ctx, out);
let current = lir::ExprKind::RecordGet {
base: Box::new(get_expr_for_target(
&root_target,
ctx.current_stmt_provenance,
)),
field,
static_offset,
}
.at(ctx.current_stmt_provenance);
let (current_slot, current_name) = declare_synthetic("__current", current, ctx, out);
let rhs = if op == AssignOp::Set {
lir::ExprKind::GetTemp(rhs_slot, rhs_name).at(ctx.current_stmt_provenance)
} else {
let infix_op = if op == AssignOp::Sub {
InfixOp::Sub
} else {
InfixOp::Add
};
lir::ExprKind::Infix(
Box::new(
lir::ExprKind::GetTemp(current_slot, current_name).at(ctx.current_stmt_provenance),
),
infix_op,
Box::new(lir::ExprKind::GetTemp(rhs_slot, rhs_name).at(ctx.current_stmt_provenance)),
)
.at(ctx.current_stmt_provenance)
};
let (c_slot, c_name) = declare_synthetic(
"__c",
take_expr_for_target(&root_target, ctx.current_stmt_provenance),
ctx,
out,
);
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(c_slot, c_name),
op: AssignOp::Set,
value: lir::ExprKind::RecordSet {
base: Box::new(
lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance),
),
field,
static_offset,
value: Box::new(rhs),
}
.at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: root_target,
op: AssignOp::Set,
value: lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
}
fn flatten_index_chain(idx: &hir::IndexExpr) -> (&hir::Expr, Vec<&hir::Expr>) {
let mut indices_outer_first: Vec<&hir::Expr> = vec![&idx.index];
let mut cur_base = idx.base.as_ref();
while let hir::Expr::Index(inner) = cur_base {
indices_outer_first.push(&inner.index);
cur_base = inner.base.as_ref();
}
indices_outer_first.reverse();
(cur_base, indices_outer_first)
}
fn get_expr_for_target(target: &lir::AssignTarget, provenance: crate::Provenance) -> lir::Expr {
match target {
lir::AssignTarget::Global(id) => lir::ExprKind::GetGlobal(*id).at(provenance),
lir::AssignTarget::Temp(slot, name) => lir::ExprKind::GetTemp(*slot, *name).at(provenance),
}
}
fn take_expr_for_target(target: &lir::AssignTarget, provenance: crate::Provenance) -> lir::Expr {
match target {
lir::AssignTarget::Global(id) => lir::ExprKind::TakeGlobal(*id).at(provenance),
lir::AssignTarget::Temp(slot, name) => lir::ExprKind::TakeTemp(*slot, *name).at(provenance),
}
}
fn declare_synthetic(
prefix: &str,
value: lir::Expr,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) -> (u16, brink_format::NameId) {
let slot = ctx.alloc_block_slot();
let name = ctx.names.intern(prefix);
out.push(lir::Stmt::new(
lir::StmtKind::DeclareTemp {
slot,
name,
value: Some(value),
synthetic: false,
},
ctx.current_stmt_provenance,
));
(slot, name)
}
fn lower_indexed_assignment(
idx: &hir::IndexExpr,
op: AssignOp,
value_expr: &hir::Expr,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) {
let (root_expr, indices_hir) = flatten_index_chain(idx);
if reject_field_projection_index_root(root_expr, ctx, None) {
return;
}
let Some(root_target) = super::stmts::lower_assign_target(root_expr, ctx) else {
return;
};
let n = indices_hir.len();
if n == 0 {
return;
}
if n == 1 {
#[expect(
clippy::indexing_slicing,
reason = "n == 1 just proved indices_hir has exactly one element"
)]
let index_hir = indices_hir[0];
lower_flat_indexed_assignment(root_target, index_hir, op, value_expr, ctx, out);
return;
}
lower_chained_indexed_assignment(root_target, &indices_hir, op, value_expr, ctx, out);
}
fn lower_flat_indexed_assignment(
root_target: lir::AssignTarget,
index_hir: &hir::Expr,
op: AssignOp,
value_expr: &hir::Expr,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) {
let index_value = lower_expr(index_hir, ctx);
let (idx_slot, idx_name) = declare_synthetic("__idx", index_value, ctx, out);
let rhs_value = lower_expr(value_expr, ctx);
let (rhs_slot, rhs_name) = declare_synthetic("__rhs", rhs_value, ctx, out);
let rhs = if op == AssignOp::Set {
lir::ExprKind::GetTemp(rhs_slot, rhs_name).at(ctx.current_stmt_provenance)
} else {
let current = lir::ExprKind::Index {
base: Box::new(get_expr_for_target(
&root_target,
ctx.current_stmt_provenance,
)),
index: Box::new(
lir::ExprKind::GetTemp(idx_slot, idx_name).at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance);
let (current_slot, current_name) = declare_synthetic("__current", current, ctx, out);
let infix_op = if op == AssignOp::Sub {
InfixOp::Sub
} else {
InfixOp::Add
};
lir::ExprKind::Infix(
Box::new(
lir::ExprKind::GetTemp(current_slot, current_name).at(ctx.current_stmt_provenance),
),
infix_op,
Box::new(lir::ExprKind::GetTemp(rhs_slot, rhs_name).at(ctx.current_stmt_provenance)),
)
.at(ctx.current_stmt_provenance)
};
let (c_slot, c_name) = declare_synthetic(
"__c",
take_expr_for_target(&root_target, ctx.current_stmt_provenance),
ctx,
out,
);
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(c_slot, c_name),
op: AssignOp::Set,
value: lir::ExprKind::IndexSet {
base: Box::new(
lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance),
),
index: Box::new(
lir::ExprKind::GetTemp(idx_slot, idx_name).at(ctx.current_stmt_provenance),
),
value: Box::new(rhs),
}
.at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: root_target,
op: AssignOp::Set,
value: lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
}
fn lower_chained_indexed_assignment(
root_target: lir::AssignTarget,
indices_hir: &[&hir::Expr],
op: AssignOp,
value_expr: &hir::Expr,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) {
let n = indices_hir.len();
let idx_slots: Vec<(u16, brink_format::NameId)> = indices_hir
.iter()
.map(|e| {
let v = lower_expr(e, ctx);
declare_synthetic("__idx", v, ctx, out)
})
.collect();
let mut c_slots: Vec<(u16, brink_format::NameId)> = vec![declare_synthetic(
"__c",
get_expr_for_target(&root_target, ctx.current_stmt_provenance),
ctx,
out,
)];
for k in 0..n - 1 {
let base =
lir::ExprKind::GetTemp(c_slots[k].0, c_slots[k].1).at(ctx.current_stmt_provenance);
let index =
lir::ExprKind::GetTemp(idx_slots[k].0, idx_slots[k].1).at(ctx.current_stmt_provenance);
let read = lir::ExprKind::Index {
base: Box::new(base),
index: Box::new(index),
}
.at(ctx.current_stmt_provenance);
c_slots.push(declare_synthetic("__c", read, ctx, out));
}
let mut rhs = lower_expr(value_expr, ctx);
if op != AssignOp::Set {
let last_base = lir::ExprKind::GetTemp(c_slots[n - 1].0, c_slots[n - 1].1)
.at(ctx.current_stmt_provenance);
let last_index = lir::ExprKind::GetTemp(idx_slots[n - 1].0, idx_slots[n - 1].1)
.at(ctx.current_stmt_provenance);
let current = lir::ExprKind::Index {
base: Box::new(last_base),
index: Box::new(last_index),
}
.at(ctx.current_stmt_provenance);
let infix_op = if op == AssignOp::Sub {
InfixOp::Sub
} else {
InfixOp::Add
};
rhs = lir::ExprKind::Infix(Box::new(current), infix_op, Box::new(rhs))
.at(ctx.current_stmt_provenance);
}
{
let (slot, name) = c_slots[n - 1];
let base = lir::ExprKind::GetTemp(slot, name).at(ctx.current_stmt_provenance);
let index = lir::ExprKind::GetTemp(idx_slots[n - 1].0, idx_slots[n - 1].1)
.at(ctx.current_stmt_provenance);
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(slot, name),
op: AssignOp::Set,
value: lir::ExprKind::IndexSet {
base: Box::new(base),
index: Box::new(index),
value: Box::new(rhs),
}
.at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
}
for k in (0..n - 1).rev() {
let (slot, name) = c_slots[k];
let base = lir::ExprKind::GetTemp(slot, name).at(ctx.current_stmt_provenance);
let index =
lir::ExprKind::GetTemp(idx_slots[k].0, idx_slots[k].1).at(ctx.current_stmt_provenance);
let inner = lir::ExprKind::GetTemp(c_slots[k + 1].0, c_slots[k + 1].1)
.at(ctx.current_stmt_provenance);
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(slot, name),
op: AssignOp::Set,
value: lir::ExprKind::IndexSet {
base: Box::new(base),
index: Box::new(index),
value: Box::new(inner),
}
.at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
}
let (root_slot, root_name) = c_slots[0];
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: root_target,
op: AssignOp::Set,
value: lir::ExprKind::GetTemp(root_slot, root_name).at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
}
#[expect(
clippy::similar_names,
reason = "var_name/val_name are the ForStmt field names (k/v's HIR spelling, B2 #1461) — \
not a pair a rename would clarify"
)]
fn lower_for_stmt(f: &hir::ForStmt, ctx: &mut LowerCtx<'_>, out: &mut Vec<lir::Stmt>) {
let iterable = lower_expr(&f.iterable, ctx);
let snapshot_source = if f.val_name.is_some() {
let (container_slot, container_name) =
declare_synthetic("__for_container", iterable, ctx, out);
lir::ExprKind::GetTemp(container_slot, container_name).at(ctx.current_stmt_provenance)
} else {
iterable
};
let (snap_slot, snap_name) = declare_synthetic(
"__for_snapshot",
lir::ExprKind::CollectionKeys(Box::new(snapshot_source.clone()))
.at(ctx.current_stmt_provenance),
ctx,
out,
);
let (idx_slot, idx_name) = declare_synthetic(
"__for_idx",
lir::ExprKind::Int(0).at(ctx.current_stmt_provenance),
ctx,
out,
);
let condition = lir::ExprKind::Infix(
Box::new(lir::ExprKind::GetTemp(idx_slot, idx_name).at(ctx.current_stmt_provenance)),
InfixOp::Lt,
Box::new(
lir::ExprKind::CollectionLen(Box::new(
lir::ExprKind::GetTemp(snap_slot, snap_name).at(ctx.current_stmt_provenance),
))
.at(ctx.current_stmt_provenance),
),
)
.at(ctx.current_stmt_provenance);
ctx.push_block_scope();
let (var_slot, var_name) = declare_shadow_checked(&f.var_name.text, f.var_name.range, ctx);
let mut body = vec![lir::Stmt::new(
lir::StmtKind::DeclareTemp {
slot: var_slot,
name: var_name,
value: Some(
lir::ExprKind::Index {
base: Box::new(
lir::ExprKind::GetTemp(snap_slot, snap_name)
.at(ctx.current_stmt_provenance),
),
index: Box::new(
lir::ExprKind::GetTemp(idx_slot, idx_name).at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
),
synthetic: false,
},
ctx.current_stmt_provenance,
)];
if let Some(val_name) = &f.val_name {
let (val_slot, val_name_id) = declare_shadow_checked(&val_name.text, val_name.range, ctx);
body.push(lir::Stmt::new(
lir::StmtKind::DeclareTemp {
slot: val_slot,
name: val_name_id,
value: Some(
lir::ExprKind::Index {
base: Box::new(snapshot_source),
index: Box::new(
lir::ExprKind::GetTemp(var_slot, var_name)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
),
synthetic: false,
},
ctx.current_stmt_provenance,
));
}
ctx.loop_depth += 1;
body.extend(lower_block_stmt_list(&f.body, ctx));
ctx.loop_depth -= 1;
ctx.pop_block_scope();
let post = vec![lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(idx_slot, idx_name),
op: AssignOp::Add,
value: lir::ExprKind::Int(1).at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
)];
out.push(lir::Stmt::new(
lir::StmtKind::LogicWhile(lir::LogicWhile {
condition,
body,
post,
}),
ctx.current_stmt_provenance,
));
}
type LvalueContainerChain = (
lir::AssignTarget,
Vec<(u16, brink_format::NameId)>,
Vec<(u16, brink_format::NameId)>,
);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum MutatorKind {
Push,
Insert,
Remove,
RemoveAt,
Clear,
Shuffle,
Sort,
SortBy,
HeapPush,
}
impl MutatorKind {
#[cfg(test)]
const ALL: [Self; 9] = [
Self::Push,
Self::Insert,
Self::Remove,
Self::RemoveAt,
Self::Clear,
Self::Shuffle,
Self::Sort,
Self::SortBy,
Self::HeapPush,
];
#[cfg(test)]
const fn name(self) -> &'static str {
match self {
Self::Push => "push",
Self::Insert => "insert",
Self::Remove => "remove",
Self::RemoveAt => "remove_at",
Self::Clear => "clear",
Self::Shuffle => "shuffle",
Self::Sort => "sort",
Self::SortBy => "sort_by",
Self::HeapPush => "heap_push",
}
}
fn from_name(name: &str) -> Option<Self> {
match name {
"push" => Some(Self::Push),
"insert" => Some(Self::Insert),
"remove" => Some(Self::Remove),
"remove_at" => Some(Self::RemoveAt),
"clear" => Some(Self::Clear),
"shuffle" => Some(Self::Shuffle),
"sort" => Some(Self::Sort),
"sort_by" => Some(Self::SortBy),
"heap_push" => Some(Self::HeapPush),
_ => None,
}
}
fn expected_argc(self) -> usize {
match self {
Self::Clear | Self::Shuffle | Self::Sort => 1,
Self::Push | Self::Remove | Self::RemoveAt | Self::SortBy | Self::HeapPush => 2,
Self::Insert => 3,
}
}
fn signature(self) -> &'static str {
match self {
Self::Push => "push(container, value)",
Self::Insert => "insert(container, key_or_index, value)",
Self::Remove => "remove(map, key)",
Self::RemoveAt => "remove_at(array, index)",
Self::Clear => "clear(map)",
Self::Shuffle => "shuffle(array)",
Self::Sort => "sort(array)",
Self::SortBy => "sort_by(array, comparator)",
Self::HeapPush => "heap_push(array, value)",
}
}
}
fn is_lvalue_expr(expr: &hir::Expr) -> bool {
match expr {
hir::Expr::Path(_) => true,
hir::Expr::Index(idx) => is_lvalue_expr(&idx.base),
_ => false,
}
}
fn try_lower_seed_stmt(
path: &hir::Path,
args: &[hir::Expr],
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) -> bool {
if ctx.temp_slot("seed").is_some() || ctx.resolve_path(path.range).is_some() {
return false;
}
if args.len() != 1 {
ctx.diagnostics.push(Diagnostic {
file: ctx.file,
range: path.range,
message: format!(
"{}: `seed` expects 1 argument(s), got {} — expected signature: `seed(n)`",
DiagnosticCode::E058.title(),
args.len(),
),
code: DiagnosticCode::E058,
});
return true;
}
let arg = lower_expr(&args[0], ctx);
out.push(lir::Stmt::new(
lir::StmtKind::ExprStmt(
lir::ExprKind::CallBuiltin {
builtin: lir::BuiltinFn::SeedRandom,
args: vec![arg],
}
.at(ctx.current_stmt_provenance),
),
ctx.current_stmt_provenance,
));
true
}
pub(super) fn try_lower_postfix_stmt(
expr: &hir::Expr,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) -> bool {
let hir::Expr::Postfix(inner, op) = expr else {
return false;
};
let assign_op = match op {
crate::PostfixOp::Increment => AssignOp::Add,
crate::PostfixOp::Decrement => AssignOp::Sub,
};
if let hir::Expr::Index(idx) = inner.as_ref() {
lower_indexed_assignment(idx, assign_op, &hir::Expr::Int(1), ctx, out);
return true;
}
if reject_field_projection_index_root(inner, ctx, Some(FIELD_PROJECTION_POSTFIX_TARGET)) {
return true;
}
let Some(target) = super::stmts::lower_assign_target(inner, ctx) else {
return false;
};
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target,
op: assign_op,
value: lir::ExprKind::Int(1).at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
true
}
pub(super) fn try_lower_mutator_stmt(
expr: &hir::Expr,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) -> bool {
let hir::Expr::Call(path, args) = expr else {
return false;
};
let name = super::expr::path_to_string(path);
if name == "seed" && try_lower_seed_stmt(path, args, ctx, out) {
return true;
}
if name == "seed" {
return false;
}
if path.segments.len() > 1
&& let Some(UfcsVerdict::PreludeDesugar { name: verb }) =
ctx.tables.ufcs.get(ctx.file, path.range).cloned()
&& let Some(kind) = MutatorKind::from_name(&verb)
{
let receiver = super::expr::ufcs_receiver_path(path);
let mut desugared_args = Vec::with_capacity(args.len() + 1);
desugared_args.push(hir::Expr::Path(receiver));
desugared_args.extend(args.iter().cloned());
lower_mutator_call(kind, &verb, path, &desugared_args, ctx, out);
return true;
}
let Some(kind) = MutatorKind::from_name(&name) else {
return false;
};
if ctx.temp_slot(&name).is_some() || ctx.resolve_path(path.range).is_some() {
return false;
}
lower_mutator_call(kind, &name, path, args, ctx, out);
true
}
#[expect(
clippy::too_many_lines,
reason = "one straight-line RMW desugar (read/call/write-back); issue #3183's \
provenance stamping pushed it just over the line budget"
)]
pub(super) fn try_lower_frame_local_auto_ref_stmt(
expr: &hir::Expr,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) -> bool {
let hir::Expr::Call(path, args) = expr else {
return false;
};
let Some(UfcsVerdict::FreeFnAutoRef { target }) =
ctx.tables.ufcs.get(ctx.file, path.range).cloned()
else {
return false;
};
let receiver = super::expr::ufcs_receiver_path(path);
if receiver.segments.len() != 2 {
return false;
}
#[expect(
clippy::indexing_slicing,
reason = "just proved receiver.segments.len() == 2"
)]
let head_name = receiver.segments[0].text.clone();
#[expect(
clippy::indexing_slicing,
reason = "just proved receiver.segments.len() == 2"
)]
let field_name = receiver.segments[1].text.clone();
let Some(root_slot) = ctx.temp_slot(&head_name) else {
return false;
};
if ctx.as_binding_slots.contains(&root_slot) {
ctx.diagnostics.push(Diagnostic {
file: ctx.file,
range: path.range,
message: format!(
"{}: `{head_name}` is an `as` binding — it is immutable and cannot be passed \
by `ref`",
DiagnosticCode::E148.title(),
),
code: DiagnosticCode::E148,
});
return true;
}
let Some(target_info) = ctx.index.symbols.get(&target) else {
return false;
};
let head_name_id = ctx.names.intern(&head_name);
let field = ctx.names.intern(&field_name);
let root_target = lir::AssignTarget::Temp(root_slot, head_name_id);
let static_offset = if ctx.structs.type_mode == TypeMode::Strict {
ctx.temp_shape(root_slot)
.and_then(|d| ctx.structs.shapes.get_by_def(d))
.and_then(|shape| shape.field(&field_name))
.map(|(offset, _)| offset)
} else {
None
};
let current = lir::ExprKind::RecordGet {
base: Box::new(get_expr_for_target(
&root_target,
ctx.current_stmt_provenance,
)),
field,
static_offset,
}
.at(ctx.current_stmt_provenance);
let (recv_slot, recv_name) = declare_synthetic("__recv", current, ctx, out);
let rest_params = target_info.params.get(1..).unwrap_or(&[]);
let mut call_args = Vec::with_capacity(args.len() + 1);
call_args.push(lir::CallArg::RefTemp(recv_slot, recv_name));
call_args.extend(super::expr::lower_call_args(args, rest_params, ctx));
let call_expr = if target_info.kind == SymbolKind::External {
lir::ExprKind::CallExternal {
target,
#[expect(
clippy::cast_possible_truncation,
reason = "ink externals have <=255 params"
)]
arg_count: target_info.params.len() as u8,
args: call_args,
}
.at(ctx.current_stmt_provenance)
} else {
lir::ExprKind::Call {
target,
args: call_args,
}
.at(ctx.current_stmt_provenance)
};
out.push(lir::Stmt::new(
lir::StmtKind::ExprStmt(call_expr),
ctx.current_stmt_provenance,
));
let write_back = lir::ExprKind::RecordSet {
base: Box::new(take_expr_for_target(
&root_target,
ctx.current_stmt_provenance,
)),
field,
static_offset,
value: Box::new(
lir::ExprKind::GetTemp(recv_slot, recv_name).at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance);
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: root_target,
op: AssignOp::Set,
value: write_back,
},
ctx.current_stmt_provenance,
));
true
}
#[expect(
clippy::too_many_lines,
reason = "arity/lvalue checks plus the three lvalue-shape dispatches \
(bare variable, struct field, indexed chain) read better as \
one function than split across an arbitrary line boundary"
)]
fn lower_mutator_call(
kind: MutatorKind,
name: &str,
path: &hir::Path,
args: &[hir::Expr],
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) {
let expected = kind.expected_argc();
if args.len() != expected {
ctx.diagnostics.push(Diagnostic {
file: ctx.file,
range: path.range,
message: format!(
"{}: `{}` expects {expected} argument(s), got {} — expected signature: `{}`",
DiagnosticCode::E058.title(),
name,
args.len(),
kind.signature(),
),
code: DiagnosticCode::E058,
});
return;
}
let lvalue_expr = &args[0];
if !is_lvalue_expr(lvalue_expr) {
ctx.diagnostics.push(Diagnostic {
file: ctx.file,
range: path.range,
message: format!(
"{}: `{name}` mutates its first argument — bind it to a variable first",
DiagnosticCode::E055.title(),
),
code: DiagnosticCode::E055,
});
return;
}
if let hir::Expr::Path(path) = lvalue_expr {
if path.segments.len() > 1
&& let Some(info) = ctx.resolve_path(path.range)
&& matches!(
info.kind,
SymbolKind::Variable | SymbolKind::Constant | SymbolKind::Param | SymbolKind::Temp
)
{
if path.segments.len() > 2 {
emit_chained_field_write_diagnostic(path.range, ctx);
} else {
lower_field_mutator(kind, path, info, args, ctx, out);
}
return;
}
lower_bare_mutator(kind, lvalue_expr, args, ctx, out);
return;
}
let Some((root_target, idx_slots, c_slots)) =
lower_lvalue_container_chain(lvalue_expr, ctx, out)
else {
return;
};
let Some(&(last_slot, last_name)) = c_slots.last() else {
return;
};
let stmt_prov = ctx.current_stmt_provenance;
let container = move || lir::ExprKind::GetTemp(last_slot, last_name).at(stmt_prov);
let new_container = match kind {
MutatorKind::Push => {
let value = lower_expr(&args[1], ctx);
lir::ExprKind::CollectionInsert {
base: Box::new(container()),
key: Box::new(
lir::ExprKind::CollectionLen(Box::new(container()))
.at(ctx.current_stmt_provenance),
),
value: Box::new(value),
}
.at(ctx.current_stmt_provenance)
}
MutatorKind::Insert => {
let key = lower_expr(&args[1], ctx);
let value = lower_expr(&args[2], ctx);
lir::ExprKind::CollectionInsert {
base: Box::new(container()),
key: Box::new(key),
value: Box::new(value),
}
.at(ctx.current_stmt_provenance)
}
MutatorKind::Remove => {
let key = lower_expr(&args[1], ctx);
lir::ExprKind::CollectionRemove {
base: Box::new(container()),
key: Box::new(key),
}
.at(ctx.current_stmt_provenance)
}
MutatorKind::RemoveAt => {
let index = lower_expr(&args[1], ctx);
lir::ExprKind::SeqRemoveAt {
base: Box::new(container()),
index: Box::new(index),
}
.at(ctx.current_stmt_provenance)
}
MutatorKind::Clear => {
lir::ExprKind::MapClear(Box::new(container())).at(ctx.current_stmt_provenance)
}
MutatorKind::Shuffle => {
lir::ExprKind::RandShuffle(Box::new(container())).at(ctx.current_stmt_provenance)
}
MutatorKind::Sort => {
lir::ExprKind::SeqSorted(Box::new(container())).at(ctx.current_stmt_provenance)
}
MutatorKind::SortBy => lir::ExprKind::SeqSortedBy {
seq: Box::new(container()),
cmp: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
MutatorKind::HeapPush => lir::ExprKind::HeapPush {
seq: Box::new(container()),
value: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
};
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(last_slot, last_name),
op: AssignOp::Set,
value: new_container,
},
ctx.current_stmt_provenance,
));
writeback_lvalue_container_chain(
root_target,
&idx_slots,
&c_slots,
out,
ctx.current_stmt_provenance,
);
}
#[expect(
clippy::too_many_lines,
reason = "one dispatch arm per MutatorKind; issue #3183's provenance stamping \
pushed it just over the line budget, splitting would obscure the \
exhaustive dispatch"
)]
fn lower_bare_mutator(
kind: MutatorKind,
root_expr: &hir::Expr,
args: &[hir::Expr],
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) {
let Some(root_target) = super::stmts::lower_assign_target(root_expr, ctx) else {
return;
};
let arg_slots: Vec<(u16, brink_format::NameId)> = args[1..]
.iter()
.map(|a| {
let v = lower_expr(a, ctx);
declare_synthetic("__arg", v, ctx, out)
})
.collect();
let push_len = matches!(kind, MutatorKind::Push).then(|| {
declare_synthetic(
"__len",
lir::ExprKind::CollectionLen(Box::new(get_expr_for_target(
&root_target,
ctx.current_stmt_provenance,
)))
.at(ctx.current_stmt_provenance),
ctx,
out,
)
});
let (c_slot, c_name) = declare_synthetic(
"__c",
take_expr_for_target(&root_target, ctx.current_stmt_provenance),
ctx,
out,
);
let new_container = match kind {
MutatorKind::Push => {
let Some((len_slot, len_name)) = push_len else {
return;
};
lir::ExprKind::CollectionInsert {
base: Box::new(
lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance),
),
key: Box::new(
lir::ExprKind::GetTemp(len_slot, len_name).at(ctx.current_stmt_provenance),
),
value: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance)
}
MutatorKind::Insert => lir::ExprKind::CollectionInsert {
base: Box::new(lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance)),
key: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
value: Box::new(
lir::ExprKind::GetTemp(arg_slots[1].0, arg_slots[1].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
MutatorKind::Remove => lir::ExprKind::CollectionRemove {
base: Box::new(lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance)),
key: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
MutatorKind::RemoveAt => lir::ExprKind::SeqRemoveAt {
base: Box::new(lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance)),
index: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
MutatorKind::Clear => lir::ExprKind::MapClear(Box::new(
lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance),
))
.at(ctx.current_stmt_provenance),
MutatorKind::Shuffle => lir::ExprKind::RandShuffle(Box::new(
lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance),
))
.at(ctx.current_stmt_provenance),
MutatorKind::Sort => lir::ExprKind::SeqSorted(Box::new(
lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance),
))
.at(ctx.current_stmt_provenance),
MutatorKind::SortBy => lir::ExprKind::SeqSortedBy {
seq: Box::new(lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance)),
cmp: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
MutatorKind::HeapPush => lir::ExprKind::HeapPush {
seq: Box::new(lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance)),
value: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
};
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(c_slot, c_name),
op: AssignOp::Set,
value: new_container,
},
ctx.current_stmt_provenance,
));
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: root_target,
op: AssignOp::Set,
value: lir::ExprKind::TakeTemp(c_slot, c_name).at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
}
#[expect(
clippy::too_many_lines,
reason = "mirrors lower_bare_mutator's own per-MutatorKind RMW dispatch, \
plus the field-projection take/RecordSet/write-back steps —\
now twice, once to de-alias the field before the RMW runs and \
once to write the mutated field back — lower_single_level_field_write \
needs a single instance of for a plain field write; reads \
better as one function than split across an arbitrary line \
boundary"
)]
fn lower_field_mutator(
kind: MutatorKind,
path: &hir::Path,
head_info: &crate::symbols::SymbolInfo,
args: &[hir::Expr],
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) {
#[expect(
clippy::indexing_slicing,
reason = "caller already proved path.segments.len() == 2"
)]
let field_name = path.segments[1].text.clone();
#[expect(
clippy::indexing_slicing,
reason = "caller already proved path.segments.len() == 2"
)]
let head_name = path.segments[0].text.clone();
let (root_target, root_shape) = match head_info.kind {
SymbolKind::Variable | SymbolKind::Constant => {
if super::stmts::reject_const_write(head_info, path.range, ctx) {
return;
}
(
lir::AssignTarget::Global(head_info.id),
ctx.global_shape(head_info.id),
)
}
SymbolKind::Param | SymbolKind::Temp => {
let Some(slot) = ctx.temp_slot(&head_name) else {
return;
};
if super::stmts::reject_as_binding_write(slot, &head_name, path.range, ctx) {
return;
}
let name_id = ctx.names.intern(&head_name);
(lir::AssignTarget::Temp(slot, name_id), ctx.temp_shape(slot))
}
_ => return,
};
let static_offset = if ctx.structs.type_mode == TypeMode::Strict {
root_shape
.and_then(|d| ctx.structs.shapes.get_by_def(d))
.and_then(|shape| shape.field(&field_name))
.map(|(offset, _)| offset)
} else {
None
};
let field = ctx.names.intern(&field_name);
let arg_slots: Vec<(u16, brink_format::NameId)> = args[1..]
.iter()
.map(|a| {
let v = lower_expr(a, ctx);
declare_synthetic("__arg", v, ctx, out)
})
.collect();
let current = lir::ExprKind::RecordGet {
base: Box::new(get_expr_for_target(
&root_target,
ctx.current_stmt_provenance,
)),
field,
static_offset,
}
.at(ctx.current_stmt_provenance);
let (current_slot, current_name) = declare_synthetic("__current", current, ctx, out);
let stmt_prov = ctx.current_stmt_provenance;
let container = move || lir::ExprKind::GetTemp(current_slot, current_name).at(stmt_prov);
let push_len = matches!(kind, MutatorKind::Push).then(|| {
declare_synthetic(
"__len",
lir::ExprKind::CollectionLen(Box::new(container())).at(ctx.current_stmt_provenance),
ctx,
out,
)
});
let (dealias_slot, dealias_name) = declare_synthetic(
"__c",
take_expr_for_target(&root_target, ctx.current_stmt_provenance),
ctx,
out,
);
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(dealias_slot, dealias_name),
op: AssignOp::Set,
value: lir::ExprKind::RecordSet {
base: Box::new(
lir::ExprKind::TakeTemp(dealias_slot, dealias_name)
.at(ctx.current_stmt_provenance),
),
field,
static_offset,
value: Box::new(lir::ExprKind::Null.at(ctx.current_stmt_provenance)),
}
.at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: root_target.clone(),
op: AssignOp::Set,
value: lir::ExprKind::TakeTemp(dealias_slot, dealias_name)
.at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
let take_container =
|| lir::ExprKind::TakeTemp(current_slot, current_name).at(ctx.current_stmt_provenance);
let new_field = match kind {
MutatorKind::Push => {
let Some((len_slot, len_name)) = push_len else {
return;
};
lir::ExprKind::CollectionInsert {
base: Box::new(take_container()),
key: Box::new(
lir::ExprKind::GetTemp(len_slot, len_name).at(ctx.current_stmt_provenance),
),
value: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance)
}
MutatorKind::Insert => lir::ExprKind::CollectionInsert {
base: Box::new(take_container()),
key: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
value: Box::new(
lir::ExprKind::GetTemp(arg_slots[1].0, arg_slots[1].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
MutatorKind::Remove => lir::ExprKind::CollectionRemove {
base: Box::new(take_container()),
key: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
MutatorKind::RemoveAt => lir::ExprKind::SeqRemoveAt {
base: Box::new(take_container()),
index: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
MutatorKind::Clear => {
lir::ExprKind::MapClear(Box::new(take_container())).at(ctx.current_stmt_provenance)
}
MutatorKind::Shuffle => {
lir::ExprKind::RandShuffle(Box::new(take_container())).at(ctx.current_stmt_provenance)
}
MutatorKind::Sort => {
lir::ExprKind::SeqSorted(Box::new(take_container())).at(ctx.current_stmt_provenance)
}
MutatorKind::SortBy => lir::ExprKind::SeqSortedBy {
seq: Box::new(take_container()),
cmp: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
MutatorKind::HeapPush => lir::ExprKind::HeapPush {
seq: Box::new(take_container()),
value: Box::new(
lir::ExprKind::GetTemp(arg_slots[0].0, arg_slots[0].1)
.at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
};
let (new_slot, new_name) = declare_synthetic("__new", new_field, ctx, out);
let (writeback_slot, writeback_name) = declare_synthetic(
"__c",
take_expr_for_target(&root_target, ctx.current_stmt_provenance),
ctx,
out,
);
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(writeback_slot, writeback_name),
op: AssignOp::Set,
value: lir::ExprKind::RecordSet {
base: Box::new(
lir::ExprKind::TakeTemp(writeback_slot, writeback_name)
.at(ctx.current_stmt_provenance),
),
field,
static_offset,
value: Box::new(
lir::ExprKind::TakeTemp(new_slot, new_name).at(ctx.current_stmt_provenance),
),
}
.at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: root_target,
op: AssignOp::Set,
value: lir::ExprKind::TakeTemp(writeback_slot, writeback_name)
.at(ctx.current_stmt_provenance),
},
ctx.current_stmt_provenance,
));
}
fn lower_lvalue_container_chain(
lvalue: &hir::Expr,
ctx: &mut LowerCtx<'_>,
out: &mut Vec<lir::Stmt>,
) -> Option<LvalueContainerChain> {
let (root_expr, indices_hir) = match lvalue {
hir::Expr::Index(idx) => flatten_index_chain(idx),
hir::Expr::Path(_) => (lvalue, Vec::new()),
_ => return None,
};
if reject_field_projection_index_root(root_expr, ctx, None) {
return None;
}
let root_target = super::stmts::lower_assign_target(root_expr, ctx)?;
let idx_slots: Vec<(u16, brink_format::NameId)> = indices_hir
.iter()
.map(|e| {
let v = lower_expr(e, ctx);
declare_synthetic("__idx", v, ctx, out)
})
.collect();
let mut c_slots: Vec<(u16, brink_format::NameId)> = vec![declare_synthetic(
"__c",
get_expr_for_target(&root_target, ctx.current_stmt_provenance),
ctx,
out,
)];
for k in 0..idx_slots.len() {
let base =
lir::ExprKind::GetTemp(c_slots[k].0, c_slots[k].1).at(ctx.current_stmt_provenance);
let index =
lir::ExprKind::GetTemp(idx_slots[k].0, idx_slots[k].1).at(ctx.current_stmt_provenance);
let read = lir::ExprKind::Index {
base: Box::new(base),
index: Box::new(index),
}
.at(ctx.current_stmt_provenance);
c_slots.push(declare_synthetic("__c", read, ctx, out));
}
Some((root_target, idx_slots, c_slots))
}
fn writeback_lvalue_container_chain(
root_target: lir::AssignTarget,
idx_slots: &[(u16, brink_format::NameId)],
c_slots: &[(u16, brink_format::NameId)],
out: &mut Vec<lir::Stmt>,
provenance: crate::Provenance,
) {
for k in (0..idx_slots.len()).rev() {
let Some(&(slot, name)) = c_slots.get(k) else {
return;
};
let Some(&(next_slot, next_name)) = c_slots.get(k + 1) else {
return;
};
let base = lir::ExprKind::GetTemp(slot, name).at(provenance);
let index = lir::ExprKind::GetTemp(idx_slots[k].0, idx_slots[k].1).at(provenance);
let inner = lir::ExprKind::GetTemp(next_slot, next_name).at(provenance);
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: lir::AssignTarget::Temp(slot, name),
op: AssignOp::Set,
value: lir::ExprKind::IndexSet {
base: Box::new(base),
index: Box::new(index),
value: Box::new(inner),
}
.at(provenance),
},
provenance,
));
}
let Some(&(root_slot, root_name)) = c_slots.first() else {
return;
};
out.push(lir::Stmt::new(
lir::StmtKind::Assign {
target: root_target,
op: AssignOp::Set,
value: lir::ExprKind::GetTemp(root_slot, root_name).at(provenance),
},
provenance,
));
}
#[cfg(test)]
mod mutator_kind_tests {
use super::MutatorKind;
#[test]
fn every_mutator_name_is_a_real_t1b_stdlib_name() {
for kind in MutatorKind::ALL {
let name = kind.name();
assert_eq!(
MutatorKind::from_name(name),
Some(kind),
"`{name}` (MutatorKind::{kind:?}) should round-trip through from_name"
);
assert!(
super::super::expr::is_t1b_stdlib_name(name),
"`{name}` is recognized as a mutator but is_t1b_stdlib_name doesn't know it \
— MutatorKind::from_name has drifted out of the subset its own doc claims"
);
}
}
}