use brink_format::DefinitionId;
use crate::hir;
use crate::symbols::SymbolKind;
use super::blocks::{
FIELD_PROJECTION_IMPLICIT_REF_ARG, FIELD_PROJECTION_MUTATOR_ARG,
reject_field_projection_index_root, reject_field_projection_path,
};
use super::context::{self, LowerCtx};
use super::decls::list_def_to_global_var;
use super::lir;
#[expect(
clippy::cast_possible_truncation,
reason = "f64→f32 is intentional per ink spec"
)]
#[expect(
clippy::too_many_lines,
reason = "one match arm per hir::Expr variant — the point of this dispatch; \
issue #3183's provenance stamping pushed it just over the line \
budget, splitting would obscure the exhaustive dispatch"
)]
pub fn lower_expr(expr: &hir::Expr, ctx: &mut LowerCtx<'_>) -> lir::Expr {
match expr {
hir::Expr::Int(n) => lir::ExprKind::Int(*n).at(ctx.current_stmt_provenance),
hir::Expr::Float(bits) => {
lir::ExprKind::Float(bits.to_f64() as f32).at(ctx.current_stmt_provenance)
}
hir::Expr::Bool(b) => lir::ExprKind::Bool(*b).at(ctx.current_stmt_provenance),
hir::Expr::Null => lir::ExprKind::Null.at(ctx.current_stmt_provenance),
hir::Expr::String(s) => {
let parts = s
.parts
.iter()
.map(|p| match p {
hir::StringPart::Literal(t) => lir::StringPart::Literal(t.clone()),
hir::StringPart::Interpolation(e) => {
lir::StringPart::Interpolation(Box::new(lower_expr(e, ctx)))
}
})
.collect();
lir::ExprKind::String(lir::StringExpr { parts }).at(ctx.current_stmt_provenance)
}
hir::Expr::Path(path) => lower_path(path, ctx),
hir::Expr::DivertTarget(path) => {
if let Some(id) = ctx.resolve_id(path.range) {
lir::ExprKind::DivertTarget(id).at(ctx.current_stmt_provenance)
} else {
lir::ExprKind::Null.at(ctx.current_stmt_provenance)
}
}
hir::Expr::ListLiteral(paths) => {
let mut items = Vec::new();
let mut origins = Vec::new();
for path in paths {
if let Some(id) = ctx.resolve_id(path.range)
&& let Some(info) = ctx.index.symbols.get(&id)
{
if info.kind == SymbolKind::ListItem {
items.push(id);
if let Some(dot) = info.name.rfind('.') {
let list_name = &info.name[..dot];
if let Some(list_ids) = ctx.index.by_name.get(list_name) {
for &list_id in list_ids {
if ctx
.index
.symbols
.get(&list_id)
.is_some_and(|s| s.kind == SymbolKind::List)
&& !origins.contains(&list_id)
{
origins.push(list_id);
}
}
}
}
} else if info.kind == SymbolKind::List {
origins.push(id);
}
}
}
lir::ExprKind::ListLiteral { items, origins }.at(ctx.current_stmt_provenance)
}
hir::Expr::Prefix(op, inner) => {
lir::ExprKind::Prefix(*op, Box::new(lower_expr(inner, ctx)))
.at(ctx.current_stmt_provenance)
}
hir::Expr::Infix(ie) if ie.op == crate::InfixOp::Coalesce => {
lower_coalesce_chain(expr, ctx)
}
hir::Expr::Infix(ie) => lir::ExprKind::Infix(
Box::new(lower_expr(&ie.lhs, ctx)),
ie.op,
Box::new(lower_expr(&ie.rhs, ctx)),
)
.at(ie.ptr),
hir::Expr::Postfix(inner, op) => {
lir::ExprKind::Postfix(Box::new(lower_expr(inner, ctx)), *op)
.at(ctx.current_stmt_provenance)
}
hir::Expr::Call(path, args) => lower_call(path, args, ctx),
hir::Expr::ArrayLiteral(arr) => lower_array_literal(arr, ctx),
hir::Expr::MapLiteral(map) => lower_map_literal(map, ctx),
hir::Expr::Index(idx) => lir::ExprKind::Index {
base: Box::new(lower_expr(&idx.base, ctx)),
index: Box::new(lower_expr(&idx.index, ctx)),
}
.at(idx.ptr),
hir::Expr::Range(r) => lir::ExprKind::RangeMake {
start: Box::new(lower_expr(&r.start, ctx)),
end: Box::new(lower_expr(&r.end, ctx)),
inclusive: r.inclusive,
}
.at(r.ptr),
hir::Expr::StructLiteral(sl) => lower_struct_literal(sl, ctx),
hir::Expr::FieldAccess(fa) => lower_field_access(fa, ctx),
hir::Expr::FnLiteral(fl) => lower_fn_literal(fl, ctx),
hir::Expr::RefArg(ra) => lower_ref_arg_fence(ra, ctx),
hir::Expr::Lambda(l) => super::lambda::lower_lambda(l, ctx),
hir::Expr::Fragment(stmts) => {
let ambient = ctx.current_stmt_provenance;
let lowered = stmts
.iter()
.filter_map(|s| super::stmts::lower_stmt(s, ctx))
.collect();
ctx.current_stmt_provenance = ambient;
lir::ExprKind::Fragment(lowered).at(ambient)
}
}
}
fn lower_coalesce_chain(root: &hir::Expr, ctx: &mut LowerCtx<'_>) -> lir::Expr {
let spine = coalesce_chain_spine(root);
let shapes = crate::hir::expr_span(root)
.and_then(|range| ctx.tables.coalesce.get(ctx.file, range))
.filter(|shapes| shapes.len() == spine.len());
let mut steps = spine.iter().rev();
let Some(&(innermost_ptr, innermost_lhs, innermost_rhs)) = steps.next() else {
unreachable!("InfixOp::Coalesce always has a non-empty chain spine")
};
let mut fallbacks = vec![(innermost_ptr, innermost_rhs)];
fallbacks.extend(steps.map(|&(ptr, _, rhs)| (ptr, rhs)));
let mut acc = lower_expr(innermost_lhs, ctx);
for (index, (ptr, fallback)) in fallbacks.into_iter().enumerate() {
let rhs = lower_expr(fallback, ctx);
acc = lir::ExprKind::Coalesce {
lhs: Box::new(acc),
rhs: Box::new(rhs),
shape: shapes
.and_then(|shapes| shapes.get(index))
.copied()
.unwrap_or_default(),
}
.at(ptr);
}
acc
}
fn coalesce_chain_spine(root: &hir::Expr) -> Vec<(crate::Provenance, &hir::Expr, &hir::Expr)> {
let mut spine = Vec::new();
let mut cursor = root;
while let hir::Expr::Infix(ie) = cursor
&& ie.op == crate::InfixOp::Coalesce
{
spine.push((ie.ptr, ie.lhs.as_ref(), ie.rhs.as_ref()));
cursor = &ie.lhs;
}
spine
}
fn lower_ref_arg_fence(ra: &hir::RefArgExpr, ctx: &mut LowerCtx<'_>) -> lir::Expr {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: ra.ptr.text_range(),
message: format!(
"{}: path-projection ref-arguments (`ref {}`) have no runtime \
representation yet — grammar/HIR/analyzer support lands in T1e-1 \
(this compiler), lowering in T1e-2 (tracking #828)",
crate::DiagnosticCode::E099.title(),
crate::display_expr(&ra.operand),
),
code: crate::DiagnosticCode::E099,
});
lower_expr(&ra.operand, ctx);
lir::ExprKind::Null.at(ra.ptr)
}
fn lower_fn_literal(fl: &hir::FnLiteral, ctx: &mut LowerCtx<'_>) -> lir::Expr {
if let Some(info) = ctx.resolve_path(fl.target.range) {
let target = info.id;
let bound = lower_call_args(&fl.args, &info.params, ctx);
lir::ExprKind::MakeFnValue { target, bound }.at(fl.ptr)
} else {
for arg in &fl.args {
lower_expr(arg, ctx);
}
lir::ExprKind::Null.at(fl.ptr)
}
}
const CONSTRUCTION_FAULT_SHAPE_ID: u32 = u32::MAX;
fn lower_struct_literal(sl: &hir::StructLiteral, ctx: &mut LowerCtx<'_>) -> lir::Expr {
let structs = ctx.structs;
let file = ctx.file;
let shape = ctx
.resolutions
.resolve(file, sl.shape.range)
.and_then(|id| structs.shapes.get_by_def(id));
let Some(shape) = shape else {
for (_name, val) in &sl.fields {
lower_expr(val, ctx);
}
return reject_unresolved_struct_shape(sl.ptr, ctx);
};
let mut placed: Vec<Option<u16>> = vec![None; shape.fields.len()];
let mut prelude: Vec<(u16, brink_format::NameId, lir::Expr)> =
Vec::with_capacity(sl.fields.len());
let mut source_order: Vec<lir::Expr> = Vec::with_capacity(sl.fields.len());
let mut has_extra = false;
for (name, val) in &sl.fields {
let lowered = lower_expr(val, ctx);
match shape.field(&name.text) {
Some((offset, _)) => {
let slot = ctx.alloc_block_slot();
let name_id = ctx.names.intern("__field");
prelude.push((slot, name_id, lowered.clone()));
if let Some(p) = placed.get_mut(offset as usize) {
*p = Some(slot);
}
}
None => has_extra = true,
}
source_order.push(lowered);
}
let has_missing = placed.iter().any(Option::is_none);
if has_extra || has_missing {
return lir::ExprKind::RecordNew {
shape_id: CONSTRUCTION_FAULT_SHAPE_ID,
fields: source_order,
prelude: Vec::new(),
}
.at(sl.ptr);
}
lir::ExprKind::RecordNew {
shape_id: shape.id,
fields: placed
.into_iter()
.map(|slot| {
slot.map_or(lir::ExprKind::Null.at(ctx.current_stmt_provenance), |s| {
lir::ExprKind::GetTemp(s, ctx.names.intern("__field"))
.at(ctx.current_stmt_provenance)
})
})
.collect(),
prelude,
}
.at(sl.ptr)
}
fn lower_field_access(fa: &hir::FieldAccessExpr, ctx: &mut LowerCtx<'_>) -> lir::Expr {
let static_offset = static_offset_for(&fa.base, &fa.field.text, ctx);
let field = ctx.names.intern(&fa.field.text);
let base = lower_expr(&fa.base, ctx);
lir::ExprKind::RecordGet {
base: Box::new(base),
field,
static_offset,
}
.at(fa.ptr)
}
fn static_offset_for(base: &hir::Expr, field_name: &str, ctx: &LowerCtx<'_>) -> Option<u16> {
if ctx.structs.type_mode != crate::lir::TypeMode::Strict {
return None;
}
let shape_def = known_shape(base, ctx)?;
let shape = ctx.structs.shapes.get_by_def(shape_def)?;
shape.field(field_name).map(|(offset, _)| offset)
}
fn known_shape(expr: &hir::Expr, ctx: &LowerCtx<'_>) -> Option<DefinitionId> {
match expr {
hir::Expr::StructLiteral(sl) => ctx.resolutions.resolve(ctx.file, sl.shape.range),
hir::Expr::Path(path) => {
let name = path_to_string(path);
if let Some(slot) = ctx.temp_slot(&name) {
ctx.temp_shape(slot)
} else {
let info = ctx.resolve_path(path.range)?;
ctx.global_shape(info.id)
}
}
hir::Expr::FieldAccess(fa) => {
let base_shape = known_shape(&fa.base, ctx)?;
let shape = ctx.structs.shapes.get_by_def(base_shape)?;
let (_, nested) = shape.field(&fa.field.text)?;
nested
}
_ => None,
}
}
fn reject_unresolved_struct_shape(
provenance: crate::Provenance,
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: provenance.text_range(),
message: crate::DiagnosticCode::E073.title().to_string(),
code: crate::DiagnosticCode::E073,
});
lir::ExprKind::Null.at(provenance)
}
fn lower_array_literal(arr: &hir::ArrayLiteral, ctx: &mut LowerCtx<'_>) -> lir::Expr {
let folded: Option<Vec<lir::ConstValue>> = arr.elements.iter().map(try_const_fold).collect();
if let Some(items) = folded {
return lir::ExprKind::ConstLiteral(lir::ConstValue::Array(items)).at(arr.ptr);
}
lir::ExprKind::ArrayNew(arr.elements.iter().map(|e| lower_expr(e, ctx)).collect()).at(arr.ptr)
}
fn lower_map_literal(map: &hir::MapLiteral, ctx: &mut LowerCtx<'_>) -> lir::Expr {
let folded: Option<Vec<(lir::ConstMapKey, lir::ConstValue)>> = map
.entries
.iter()
.map(|(k, v)| {
let key = try_const_fold(k).and_then(const_value_to_map_key)?;
let value = try_const_fold(v)?;
Some((key, value))
})
.collect();
if let Some(entries) = folded {
return lir::ExprKind::ConstLiteral(lir::ConstValue::Map(entries)).at(map.ptr);
}
lir::ExprKind::MapNew(
map.entries
.iter()
.map(|(k, v)| (lower_expr(k, ctx), lower_expr(v, ctx)))
.collect(),
)
.at(map.ptr)
}
fn try_const_fold(expr: &hir::Expr) -> Option<lir::ConstValue> {
match expr {
hir::Expr::Int(n) => Some(lir::ConstValue::Int(*n)),
#[expect(
clippy::cast_possible_truncation,
reason = "f64->f32 is intentional per ink spec, matches lower_expr's Float arm"
)]
hir::Expr::Float(bits) => Some(lir::ConstValue::Float(bits.to_f64() as f32)),
hir::Expr::Bool(b) => Some(lir::ConstValue::Bool(*b)),
hir::Expr::Null => Some(lir::ConstValue::Null),
hir::Expr::String(s) => {
match s.parts.as_slice() {
[hir::StringPart::Literal(text)] => Some(lir::ConstValue::String(text.clone())),
[] => Some(lir::ConstValue::String(String::new())),
_ => None,
}
}
hir::Expr::ArrayLiteral(arr) => {
let items: Option<Vec<lir::ConstValue>> =
arr.elements.iter().map(try_const_fold).collect();
items.map(lir::ConstValue::Array)
}
hir::Expr::MapLiteral(map) => {
let entries: Option<Vec<(lir::ConstMapKey, lir::ConstValue)>> = map
.entries
.iter()
.map(|(k, v)| {
let key = try_const_fold(k).and_then(const_value_to_map_key)?;
let value = try_const_fold(v)?;
Some((key, value))
})
.collect();
entries.map(lir::ConstValue::Map)
}
_ => None,
}
}
pub(super) fn const_value_to_map_key(v: lir::ConstValue) -> Option<lir::ConstMapKey> {
match v {
lir::ConstValue::Int(n) => Some(lir::ConstMapKey::Int(n)),
lir::ConstValue::String(s) => Some(lir::ConstMapKey::Str(s)),
lir::ConstValue::Bool(b) => Some(lir::ConstMapKey::Bool(b)),
_ => None,
}
}
fn lower_ambiguous_dotted_path(
path: &hir::Path,
head_info: &crate::symbols::SymbolInfo,
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
let Some(head_name) = path.segments.first().map(|n| n.text.clone()) else {
return lir::ExprKind::Null.at(ctx.current_stmt_provenance);
};
let (mut expr, mut current_shape) = match head_info.kind {
SymbolKind::Variable | SymbolKind::Constant => (
lir::ExprKind::GetGlobal(head_info.id).at(ctx.current_stmt_provenance),
ctx.global_shape(head_info.id),
),
SymbolKind::Param | SymbolKind::Temp => {
let Some(slot) = ctx.temp_slot(&head_name) else {
return lir::ExprKind::Null.at(ctx.current_stmt_provenance);
};
let name_id = ctx.names.intern(&head_name);
(
lir::ExprKind::GetTemp(slot, name_id).at(ctx.current_stmt_provenance),
ctx.temp_shape(slot),
)
}
_ => return lir::ExprKind::Null.at(ctx.current_stmt_provenance),
};
for seg in &path.segments[1..] {
let shape_info = current_shape.and_then(|d| ctx.structs.shapes.get_by_def(d));
let static_offset = if ctx.structs.type_mode == crate::lir::TypeMode::Strict {
shape_info.and_then(|s| s.field(&seg.text)).map(|(o, _)| o)
} else {
None
};
let nested_shape = shape_info
.and_then(|s| s.field(&seg.text))
.and_then(|(_, nested)| nested);
let field = ctx.names.intern(&seg.text);
expr = lir::ExprKind::RecordGet {
base: Box::new(expr),
field,
static_offset,
}
.at(ctx.current_stmt_provenance);
current_shape = nested_shape;
}
expr
}
fn lower_path(path: &hir::Path, ctx: &mut LowerCtx<'_>) -> lir::Expr {
let name = path_to_string(path);
if let Some(slot) = ctx.temp_slot(&name) {
let name_id = ctx.names.intern(&name);
return lir::ExprKind::GetTemp(slot, name_id).at(ctx.current_stmt_provenance);
}
if let Some(info) = ctx.resolve_path(path.range) {
if path.segments.len() > 1
&& matches!(
info.kind,
SymbolKind::Variable | SymbolKind::Constant | SymbolKind::Param | SymbolKind::Temp
)
{
return lower_ambiguous_dotted_path(path, info, ctx);
}
if ctx.native && info.is_function_definition() {
return lir::ExprKind::MakeFnValue {
target: info.id,
bound: Vec::new(),
}
.at(ctx.current_stmt_provenance);
}
match info.kind {
SymbolKind::Variable | SymbolKind::Constant => {
lir::ExprKind::GetGlobal(info.id).at(ctx.current_stmt_provenance)
}
SymbolKind::List => {
lir::ExprKind::GetGlobal(list_def_to_global_var(info.id))
.at(ctx.current_stmt_provenance)
}
SymbolKind::ListItem => {
let origin = info
.name
.split_once('.')
.and_then(|(list_name, _)| {
ctx.index
.by_name
.get(list_name)
.and_then(|ids| {
ids.iter().find(|&&id| {
ctx.index
.symbols
.get(&id)
.is_some_and(|s| s.kind == SymbolKind::List)
})
})
.copied()
})
.into_iter()
.collect();
lir::ExprKind::ListLiteral {
items: vec![info.id],
origins: origin,
}
.at(ctx.current_stmt_provenance)
}
SymbolKind::Knot | SymbolKind::Stitch | SymbolKind::Label => {
lir::ExprKind::VisitCount(info.id).at(ctx.current_stmt_provenance)
}
SymbolKind::Temp if ctx.block_scoped_temp_names.contains(&name) => {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: path.range,
message: format!(
"{}: `{name}` was declared in a `~ {{ … }}` block that has already \
closed — block-scoped temps (docs/t1b-surface-spec.md §2) are only \
visible for the rest of their own block",
crate::DiagnosticCode::E082.title(),
),
code: crate::DiagnosticCode::E082,
});
lir::ExprKind::Null.at(ctx.current_stmt_provenance)
}
SymbolKind::Temp => {
if let Some(slot) = ctx.temp_slot_raw(&name) {
let name_id = ctx.names.intern(&name);
lir::ExprKind::GetTemp(slot, name_id).at(ctx.current_stmt_provenance)
} else {
use brink_format::DefinitionTag;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
name.hash(&mut hasher);
let global_id = DefinitionId::new(DefinitionTag::GlobalVar, hasher.finish());
lir::ExprKind::GetGlobal(global_id).at(ctx.current_stmt_provenance)
}
}
SymbolKind::External | SymbolKind::Param | SymbolKind::Struct => {
lir::ExprKind::Null.at(ctx.current_stmt_provenance)
}
}
} else if path.segments.len() == 1 && path.segments[0].text == "none" {
lir::ExprKind::OptionNone.at(ctx.current_stmt_provenance)
} else {
lir::ExprKind::Null.at(ctx.current_stmt_provenance)
}
}
#[expect(
clippy::too_many_lines,
reason = "one dispatch arm per call-target kind (builtin/ufcs/weighted/tower/…); \
issue #3183's provenance stamping pushed it just over the line \
budget, splitting would obscure the exhaustive dispatch"
)]
fn lower_call(path: &hir::Path, args: &[hir::Expr], ctx: &mut LowerCtx<'_>) -> lir::Expr {
let name = path_to_string(path);
if let Some(slot) = ctx.temp_slot(&name) {
let call_args = lower_call_args(args, &[], ctx);
let name_id = ctx.names.intern(&name);
return lir::ExprKind::CallVariableTemp {
slot,
name: name_id,
args: call_args,
}
.at(ctx.current_stmt_provenance);
}
if let Some(info) = ctx.resolve_path(path.range) {
if path.segments.len() > 1
&& matches!(
info.kind,
SymbolKind::Param | SymbolKind::Temp | SymbolKind::Variable | SymbolKind::Constant
)
{
return lower_ufcs_call(&name, path, args, ctx);
}
match info.kind {
SymbolKind::List => {
if args.is_empty() {
lir::ExprKind::ListLiteral {
items: Vec::new(),
origins: vec![info.id],
}
.at(ctx.current_stmt_provenance)
} else {
let list_name = info
.name
.split('.')
.next()
.unwrap_or(&info.name)
.to_string();
let name_expr = lir::ExprKind::String(lir::StringExpr {
parts: vec![lir::StringPart::Literal(list_name)],
})
.at(ctx.current_stmt_provenance);
let ordinal_expr = lower_expr(&args[0], ctx);
lir::ExprKind::CallBuiltin {
builtin: lir::BuiltinFn::ListFromInt,
args: vec![name_expr, ordinal_expr],
}
.at(ctx.current_stmt_provenance)
}
}
SymbolKind::External => {
let call_args = lower_call_args(args, &info.params, ctx);
lir::ExprKind::CallExternal {
target: info.id,
args: call_args,
#[expect(
clippy::cast_possible_truncation,
reason = "ink externals have <=255 params"
)]
arg_count: info.params.len() as u8,
}
.at(ctx.current_stmt_provenance)
}
SymbolKind::Variable | SymbolKind::Constant => {
let call_args = lower_call_args(args, &info.params, ctx);
lir::ExprKind::CallVariable {
target: info.id,
args: call_args,
}
.at(ctx.current_stmt_provenance)
}
SymbolKind::Knot => {
let call_args = lower_call_args(args, &info.params, ctx);
lir::ExprKind::Call {
target: info.id,
args: call_args,
}
.at(ctx.current_stmt_provenance)
}
SymbolKind::Temp if ctx.block_scoped_temp_names.contains(&name) => {
push_block_scoped_temp_call_refusal(&name, path.range, ctx)
}
kind @ (SymbolKind::Stitch
| SymbolKind::ListItem
| SymbolKind::Label
| SymbolKind::Param
| SymbolKind::Temp
| SymbolKind::Struct) => push_non_callable_refusal(&name, kind, path.range, ctx),
}
} else if let Some(builtin) = recognize_builtin(&name) {
let lir_args: Vec<lir::Expr> = args.iter().map(|a| lower_expr(a, ctx)).collect();
lir::ExprKind::CallBuiltin {
builtin,
args: lir_args,
}
.at(ctx.current_stmt_provenance)
} else if let Some(expr) = lower_t1b_stdlib_call(&name, args, path.range, ctx) {
expr
} else {
tracing::error!(
"ICE: unresolved call to `{name}` — analyzer marked as builtin but \
recognize_builtin()/lower_t1b_stdlib_call() both returned None and \
resolution map has no entry"
);
lir::ExprKind::Null.at(ctx.current_stmt_provenance)
}
}
fn push_block_scoped_temp_call_refusal(
name: &str,
range: rowan::TextRange,
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range,
message: format!(
"{}: `{name}` was declared in a `~ {{ … }}` block that has already \
closed — block-scoped temps (docs/t1b-surface-spec.md §2) are only \
visible for the rest of their own block",
crate::DiagnosticCode::E082.title(),
),
code: crate::DiagnosticCode::E082,
});
lir::ExprKind::Null.at(ctx.current_stmt_provenance)
}
fn push_non_callable_refusal(
name: &str,
kind: SymbolKind,
range: rowan::TextRange,
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
let message = match kind {
SymbolKind::Temp | SymbolKind::Param => format!(
"{}: `{name}` is used here before its declaration — it is not in scope at this call site",
crate::DiagnosticCode::E183.title(),
),
_ => format!(
"{}: `{name}` resolves to a {kind:?}, which cannot be called",
crate::DiagnosticCode::E183.title(),
),
};
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range,
message,
code: crate::DiagnosticCode::E183,
});
lir::ExprKind::Null.at(ctx.current_stmt_provenance)
}
fn push_ufcs_lowering_refusal(
name: &str,
range: rowan::TextRange,
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range,
message: format!(
"{}: `{name}` resolves as method-call syntax, but the compiler cannot \
lower it yet — spell the call explicitly as a free call for now",
crate::DiagnosticCode::E144.title(),
),
code: crate::DiagnosticCode::E144,
});
lir::ExprKind::Null.at(ctx.current_stmt_provenance)
}
fn lower_ufcs_call(
name: &str,
path: &hir::Path,
args: &[hir::Expr],
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
let Some(verdict) = ctx.tables.ufcs.get(ctx.file, path.range).cloned() else {
return push_ufcs_lowering_refusal(name, path.range, ctx);
};
match verdict {
context::UfcsVerdict::FieldCall => {
let callee = lower_expr(&hir::Expr::Path(path.clone()), ctx);
let call_args = args.iter().map(|a| lower_expr(a, ctx)).collect();
lir::ExprKind::CallValue {
callee: Box::new(callee),
args: call_args,
}
.at(ctx.current_stmt_provenance)
}
context::UfcsVerdict::FreeFnDesugar { target }
| context::UfcsVerdict::FreeFnAutoRef { target } => {
lower_ufcs_desugared_call(path, args, target, ctx)
}
context::UfcsVerdict::PreludeDesugar { name } => {
lower_ufcs_prelude_desugar(path, args, &name, ctx)
}
}
}
pub(super) fn ufcs_receiver_path(path: &hir::Path) -> hir::Path {
let receiver_segs = path.segments.split_last().map_or(&[][..], |(_, rest)| rest);
hir::Path {
segments: receiver_segs.to_vec(),
range: path.range,
crosses_module_wall: path.crosses_module_wall,
}
}
fn ufcs_receiver_arg(path: &hir::Path, auto_ref: bool) -> hir::Expr {
let receiver = hir::Expr::Path(ufcs_receiver_path(path));
if auto_ref {
hir::Expr::RefArg(hir::RefArgExpr {
ptr: crate::Provenance::synthetic(crate::NodeClass::RefArg, path.range),
operand: Box::new(receiver),
})
} else {
receiver
}
}
fn lower_ufcs_desugared_call(
path: &hir::Path,
args: &[hir::Expr],
target: brink_format::DefinitionId,
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
let Some(target_info) = ctx.index.symbols.get(&target) else {
return lir::ExprKind::Null.at(ctx.current_stmt_provenance);
};
let auto_ref = target_info.params.first().is_some_and(|p| p.is_ref);
let mut desugared_args = Vec::with_capacity(args.len() + 1);
desugared_args.push(ufcs_receiver_arg(path, auto_ref));
desugared_args.extend(args.iter().cloned());
let call_args = lower_call_args(&desugared_args, &target_info.params, ctx);
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)
}
}
fn lower_ufcs_prelude_desugar(
path: &hir::Path,
args: &[hir::Expr],
name: &str,
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
let receiver_path = ufcs_receiver_path(path);
if let Some(builtin) = recognize_builtin(name) {
let mut lowered = vec![lower_expr(&hir::Expr::Path(receiver_path), ctx)];
lowered.extend(args.iter().map(|a| lower_expr(a, ctx)));
return lir::ExprKind::CallBuiltin {
builtin,
args: lowered,
}
.at(ctx.current_stmt_provenance);
}
let mut desugared_args = Vec::with_capacity(args.len() + 1);
desugared_args.push(hir::Expr::Path(receiver_path));
desugared_args.extend(args.iter().cloned());
lower_t1b_stdlib_call(name, &desugared_args, path.range, ctx)
.unwrap_or_else(|| push_ufcs_lowering_refusal(name, path.range, ctx))
}
#[expect(
clippy::too_many_lines,
reason = "one-arm-per-stdlib-name dispatch; splitting would scatter the table"
)]
fn lower_t1b_stdlib_call(
name: &str,
args: &[hir::Expr],
call_range: rowan::TextRange,
ctx: &mut LowerCtx<'_>,
) -> Option<lir::Expr> {
if !is_t1b_stdlib_name(name) {
return None;
}
let arity_ok = |ctx: &mut LowerCtx<'_>, expected: usize| -> bool {
if args.len() == expected {
true
} else {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: call_range,
message: format!(
"{}: `{name}` expects {expected} argument(s), got {}",
crate::DiagnosticCode::E031.title(),
args.len(),
),
code: crate::DiagnosticCode::E031,
});
false
}
};
match name {
"len" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::CollectionLen(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"keys" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::CollectionKeys(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"values" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::CollectionValues(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"contains" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::CollectionContains {
container: Box::new(lower_expr(&args[0], ctx)),
needle: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"char_at" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::CharAt {
s: Box::new(lower_expr(&args[0], ctx)),
index: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"push" | "insert" | "remove" | "remove_at" | "clear" | "sort" | "sort_by" | "heap_push" => {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: call_range,
message: format!(
"{}: `{name}` mutates its first argument and returns nothing — it can \
only be used as a statement, not an expression",
crate::DiagnosticCode::E056.title(),
),
code: crate::DiagnosticCode::E056,
});
Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance))
}
"some" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::OptionSome(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"find" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::StrFind {
s: Box::new(lower_expr(&args[0], ctx)),
sub: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"index_of" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqIndexOf {
seq: Box::new(lower_expr(&args[0], ctx)),
needle: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"min" => {
if args.len() == 2 {
return Some(lower_tower_call(brink_format::TowerOp::Min, args, ctx));
}
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqMin(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"max" => {
if args.len() == 2 {
return Some(lower_tower_call(brink_format::TowerOp::Max, args, ctx));
}
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqMax(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"vec2" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::MakeVec2, args, ctx))
}
"vec3" => {
if !arity_ok(ctx, 3) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::MakeVec3, args, ctx))
}
"vec4" => {
if !arity_ok(ctx, 4) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::MakeVec4, args, ctx))
}
"quat" => {
if !arity_ok(ctx, 4) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::MakeQuat, args, ctx))
}
"mat2" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::MakeMat2, args, ctx))
}
"mat3" => {
if !arity_ok(ctx, 3) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::MakeMat3, args, ctx))
}
"mat4" => {
if !arity_ok(ctx, 4) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::MakeMat4, args, ctx))
}
"dot" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::Dot, args, ctx))
}
"cross" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::Cross, args, ctx))
}
"clamp" => {
if !arity_ok(ctx, 3) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::Clamp, args, ctx))
}
"lerp" => {
if !arity_ok(ctx, 3) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(lower_tower_call(brink_format::TowerOp::Lerp, args, ctx))
}
"first" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqFirst(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"last" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqLast(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"get" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::MapGetOpt {
map: Box::new(lower_expr(&args[0], ctx)),
key: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"contains_value" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::MapContainsValue {
map: Box::new(lower_expr(&args[0], ctx)),
value: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"pop" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
if reject_field_projection_index_root(&args[0], ctx, Some(FIELD_PROJECTION_MUTATOR_ARG))
{
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
if let Some(root) = super::stmts::lower_assign_target(&args[0], ctx) {
return Some(lir::ExprKind::SeqPop { root }.at(ctx.current_stmt_provenance));
}
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: call_range,
message: format!(
"{}: `pop` mutates its first argument — bind it to a variable first",
crate::DiagnosticCode::E055.title(),
),
code: crate::DiagnosticCode::E055,
});
Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance))
}
"int" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::ConvertInt(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"float" => match args.len() {
0 => Some(lir::ExprKind::RandFloat.at(ctx.current_stmt_provenance)),
1 => Some(
lir::ExprKind::ConvertFloat(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
),
n => {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: call_range,
message: format!(
"{}: `float` expects 0 arguments (random draw in [0,1)) or 1 \
argument (numeric conversion), got {n}",
crate::DiagnosticCode::E031.title(),
),
code: crate::DiagnosticCode::E031,
});
Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance))
}
},
"string" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::ConvertString(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"chance" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::RandChance(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"pick" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::RandPick(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"non_empty" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::RangeNonEmpty(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"weighted" => Some(lower_weighted_call(args, call_range, ctx)),
"roll" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::RandRoll(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"heap_peek" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::HeapPeek(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"heap_pop" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
if reject_field_projection_index_root(&args[0], ctx, Some(FIELD_PROJECTION_MUTATOR_ARG))
{
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
if let Some(root) = super::stmts::lower_assign_target(&args[0], ctx) {
return Some(lir::ExprKind::HeapPop { root }.at(ctx.current_stmt_provenance));
}
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: call_range,
message: format!(
"{}: `heap_pop` mutates its first argument — bind it to a variable first",
crate::DiagnosticCode::E055.title(),
),
code: crate::DiagnosticCode::E055,
});
Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance))
}
"sorted" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqSorted(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"sorted_by" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqSortedBy {
seq: Box::new(lower_expr(&args[0], ctx)),
cmp: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"map" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqMap {
seq: Box::new(lower_expr(&args[0], ctx)),
f: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"filter" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqFilter {
seq: Box::new(lower_expr(&args[0], ctx)),
pred: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"fold" => {
if !arity_ok(ctx, 3) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqFold {
seq: Box::new(lower_expr(&args[0], ctx)),
init: Box::new(lower_expr(&args[1], ctx)),
f: Box::new(lower_expr(&args[2], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"filter_map" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqFilterMap {
seq: Box::new(lower_expr(&args[0], ctx)),
f: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"each" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqEach {
seq: Box::new(lower_expr(&args[0], ctx)),
f: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"map_each" => {
if !arity_ok(ctx, 2) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::SeqMapEach {
seq: Box::new(lower_expr(&args[0], ctx)),
f: Box::new(lower_expr(&args[1], ctx)),
}
.at(ctx.current_stmt_provenance),
)
}
"shuffled" => {
if !arity_ok(ctx, 1) {
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
Some(
lir::ExprKind::RandShuffle(Box::new(lower_expr(&args[0], ctx)))
.at(ctx.current_stmt_provenance),
)
}
"shuffle" | "seed" => {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: call_range,
message: format!(
"{}: `{name}` returns nothing — it can only be used as a statement, \
not an expression",
crate::DiagnosticCode::E056.title(),
),
code: crate::DiagnosticCode::E056,
});
Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance))
}
"call" => {
if args.is_empty() {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: call_range,
message: format!(
"{}: `call` needs at least the callee function value \
(`call(f, args…)`)",
crate::DiagnosticCode::E031.title(),
),
code: crate::DiagnosticCode::E031,
});
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
let callee = lower_expr(&args[0], ctx);
let supplied = args[1..].iter().map(|a| lower_expr(a, ctx)).collect();
Some(
lir::ExprKind::CallValue {
callee: Box::new(callee),
args: supplied,
}
.at(ctx.current_stmt_provenance),
)
}
"bind" => {
if args.is_empty() {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: call_range,
message: format!(
"{}: `bind` needs at least the callee function value \
(`bind(f, args…)`)",
crate::DiagnosticCode::E031.title(),
),
code: crate::DiagnosticCode::E031,
});
return Some(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
let callee = lower_expr(&args[0], ctx);
let supplied = args[1..].iter().map(|a| lower_expr(a, ctx)).collect();
Some(
lir::ExprKind::BindValue {
callee: Box::new(callee),
args: supplied,
}
.at(ctx.current_stmt_provenance),
)
}
_ => None,
}
}
fn lower_weighted_call(
args: &[hir::Expr],
call_range: rowan::TextRange,
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
let refuse = |ctx: &mut LowerCtx<'_>, detail: &str| {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: call_range,
message: format!("{}: {detail}", crate::DiagnosticCode::E120.title()),
code: crate::DiagnosticCode::E120,
});
lir::ExprKind::Null.at(ctx.current_stmt_provenance)
};
if args.is_empty() {
return refuse(
ctx,
"a weighted table cannot be empty — construction is the validator \
(`weighted(weight, value, …)`)",
);
}
if !args.len().is_multiple_of(2) {
return refuse(
ctx,
"`weighted` takes weight/value pairs — got a dangling weight \
(`weighted(weight, value, …)`)",
);
}
let (arg_pairs, _) = args.as_chunks::<2>();
for pair in arg_pairs {
match &pair[0] {
hir::Expr::Int(w) if *w >= 1 => {}
hir::Expr::Int(w) => {
return refuse(
ctx,
&format!("weight {w} is not positive — weights are positive ints (v1)"),
);
}
hir::Expr::Prefix(hir::PrefixOp::Negate, inner)
if matches!(inner.as_ref(), hir::Expr::Int(_) | hir::Expr::Float(_)) =>
{
return refuse(
ctx,
"a negated literal weight is not positive — weights are positive ints (v1)",
);
}
hir::Expr::Float(_) => {
return refuse(ctx, "weights are positive ints (v1), got a float literal");
}
hir::Expr::Bool(_) => {
return refuse(ctx, "weights are positive ints (v1), got a bool literal");
}
hir::Expr::String(_) => {
return refuse(ctx, "weights are positive ints (v1), got a string literal");
}
_ => {}
}
}
let pairs = arg_pairs
.iter()
.map(|pair| (lower_expr(&pair[0], ctx), lower_expr(&pair[1], ctx)))
.collect();
lir::ExprKind::WeightedNew { pairs }.at(ctx.current_stmt_provenance)
}
pub fn is_t1b_stdlib_name(name: &str) -> bool {
matches!(
name,
"len"
| "keys"
| "values"
| "contains"
| "push"
| "insert"
| "remove"
| "remove_at"
| "int"
| "float"
| "string"
| "call"
| "bind"
| "char_at"
| "find"
| "index_of"
| "min"
| "max"
| "first"
| "last"
| "pop"
| "get"
| "contains_value"
| "clear"
| "some"
| "chance"
| "pick"
| "shuffle"
| "shuffled"
| "seed"
| "non_empty"
| "weighted"
| "roll"
| "heap_push"
| "heap_pop"
| "heap_peek"
| "sort"
| "sort_by"
| "sorted"
| "sorted_by"
| "vec2"
| "vec3"
| "vec4"
| "quat"
| "mat2"
| "mat3"
| "mat4"
| "dot"
| "cross"
| "clamp"
| "lerp"
| "map"
| "filter"
| "fold"
| "filter_map"
| "each"
| "map_each"
)
}
fn lower_tower_call(
op: brink_format::TowerOp,
args: &[hir::Expr],
ctx: &mut LowerCtx<'_>,
) -> lir::Expr {
lir::ExprKind::Tower {
op,
args: args.iter().map(|a| lower_expr(a, ctx)).collect(),
}
.at(ctx.current_stmt_provenance)
}
fn lower_ref_path_call_arg(
path: &hir::Path,
original: &hir::Expr,
ctx: &mut LowerCtx<'_>,
) -> lir::CallArg {
if reject_field_projection_path(path, ctx, Some(FIELD_PROJECTION_IMPLICIT_REF_ARG)) {
return lir::CallArg::Value(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
let name = path_to_string(path);
if let Some(slot) = ctx.temp_slot(&name) {
if ctx.as_binding_slots.contains(&slot) {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: path.range,
message: format!(
"{}: `{name}` is an `as` binding — it is immutable and cannot be passed \
by `ref`",
crate::DiagnosticCode::E148.title(),
),
code: crate::DiagnosticCode::E148,
});
return lir::CallArg::Value(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
let name_id = ctx.names.intern(&name);
return lir::CallArg::RefTemp(slot, name_id);
}
if let Some(info) = ctx.resolve_path(path.range) {
if info.kind == SymbolKind::Temp && ctx.block_scoped_temp_names.contains(&name) {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: path.range,
message: format!(
"{}: `{name}` was declared in a `~ {{ … }}` block that has already \
closed — block-scoped temps (docs/t1b-surface-spec.md §2) are only \
visible for the rest of their own block",
crate::DiagnosticCode::E082.title(),
),
code: crate::DiagnosticCode::E082,
});
return lir::CallArg::Value(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
if super::stmts::reject_const_write(info, path.range, ctx) {
return lir::CallArg::Value(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
if info.kind == SymbolKind::Temp
&& let Some(slot) = ctx.temp_slot_raw(&name)
{
let name_id = ctx.names.intern(&name);
return lir::CallArg::RefTemp(slot, name_id);
}
let id = if info.kind == SymbolKind::List {
list_def_to_global_var(info.id)
} else {
info.id
};
return lir::CallArg::RefGlobal(id);
}
lir::CallArg::Value(lower_expr(original, ctx))
}
pub(super) fn lower_call_args(
args: &[hir::Expr],
params: &[crate::symbols::ParamInfo],
ctx: &mut LowerCtx<'_>,
) -> Vec<lir::CallArg> {
args.iter()
.enumerate()
.map(|(i, arg)| {
let is_ref = params.get(i).is_some_and(|p| p.is_ref);
if is_ref {
match arg {
hir::Expr::Path(path) => lower_ref_path_call_arg(path, arg, ctx),
hir::Expr::RefArg(ra) => match ra.operand.as_ref() {
hir::Expr::Path(path) if path.segments.len() == 1 => {
lower_ref_path_call_arg(path, &ra.operand, ctx)
}
_ => lower_ref_projection_arg(ra, ctx),
},
_ => lir::CallArg::Value(lower_expr(arg, ctx)),
}
} else {
lir::CallArg::Value(lower_expr(arg, ctx))
}
})
.collect()
}
enum ProjSegmentSrc<'a> {
Field(&'a hir::Name),
Index(&'a hir::Expr),
}
fn decompose_projection(expr: &hir::Expr) -> Option<(&hir::Path, Vec<ProjSegmentSrc<'_>>)> {
match expr {
hir::Expr::Path(p) => {
let segments = p.segments[1..].iter().map(ProjSegmentSrc::Field).collect();
Some((p, segments))
}
hir::Expr::FieldAccess(fa) => {
let (root, mut segments) = decompose_projection(&fa.base)?;
segments.push(ProjSegmentSrc::Field(&fa.field));
Some((root, segments))
}
hir::Expr::Index(idx) => {
let (root, mut segments) = decompose_projection(&idx.base)?;
segments.push(ProjSegmentSrc::Index(&idx.index));
Some((root, segments))
}
_ => None,
}
}
fn lower_ref_projection_arg(ra: &hir::RefArgExpr, ctx: &mut LowerCtx<'_>) -> lir::CallArg {
let Some((root, src_segments)) = decompose_projection(&ra.operand) else {
return lir::CallArg::Value(lower_ref_arg_fence(ra, ctx));
};
let root_name = path_to_string(root);
if let Some(slot) = ctx.temp_slot(&root_name)
&& ctx.as_binding_slots.contains(&slot)
{
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: root.range,
message: format!(
"{}: `{root_name}` is an `as` binding — it is immutable and cannot be passed \
by `ref`",
crate::DiagnosticCode::E148.title(),
),
code: crate::DiagnosticCode::E148,
});
return lir::CallArg::Value(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
let Some(info) = ctx.resolve_path(root.range) else {
return lir::CallArg::Value(lower_ref_arg_fence(ra, ctx));
};
if matches!(info.kind, SymbolKind::Param | SymbolKind::Temp) {
ctx.diagnostics.push(crate::Diagnostic {
file: ctx.file,
range: root.range,
message: format!(
"{}: `{root_name}` is a temp/param — a frame-local projection receiver \
(`{root_name}.field`) is legal only when the call is its own statement, not \
nested inside a larger expression",
crate::DiagnosticCode::E143.title(),
),
code: crate::DiagnosticCode::E143,
});
return lir::CallArg::Value(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
if super::stmts::reject_const_write(info, root.range, ctx) {
return lir::CallArg::Value(lir::ExprKind::Null.at(ctx.current_stmt_provenance));
}
let root_id = if info.kind == SymbolKind::List {
list_def_to_global_var(info.id)
} else {
info.id
};
let segments = src_segments
.into_iter()
.map(|seg| match seg {
ProjSegmentSrc::Field(name) => lir::ExprKind::String(lir::StringExpr {
parts: vec![lir::StringPart::Literal(name.text.clone())],
})
.at(ctx.current_stmt_provenance),
ProjSegmentSrc::Index(index_expr) => lower_expr(index_expr, ctx),
})
.collect();
lir::CallArg::RefProjection {
root: root_id,
segments,
}
}
pub fn path_to_string(path: &hir::Path) -> String {
path.segments
.iter()
.map(|s| s.text.as_str())
.collect::<Vec<_>>()
.join(".")
}
#[must_use]
pub fn is_builtin_function(name: &str) -> bool {
recognize_builtin(name).is_some()
}
pub(crate) fn recognize_builtin(name: &str) -> Option<lir::BuiltinFn> {
match name {
"TURNS_SINCE" => Some(lir::BuiltinFn::TurnsSince),
"READ_COUNT" => Some(lir::BuiltinFn::ReadCount),
"TURNS" => Some(lir::BuiltinFn::Turns),
"CHOICE_COUNT" => Some(lir::BuiltinFn::ChoiceCount),
"RANDOM" => Some(lir::BuiltinFn::Random),
"SEED_RANDOM" => Some(lir::BuiltinFn::SeedRandom),
"INT" => Some(lir::BuiltinFn::CastToInt),
"FLOAT" => Some(lir::BuiltinFn::CastToFloat),
"FLOOR" => Some(lir::BuiltinFn::Floor),
"CEILING" => Some(lir::BuiltinFn::Ceiling),
"POW" => Some(lir::BuiltinFn::Pow),
"MIN" => Some(lir::BuiltinFn::Min),
"MAX" => Some(lir::BuiltinFn::Max),
"LIST_COUNT" => Some(lir::BuiltinFn::ListCount),
"LIST_MIN" => Some(lir::BuiltinFn::ListMin),
"LIST_MAX" => Some(lir::BuiltinFn::ListMax),
"LIST_ALL" => Some(lir::BuiltinFn::ListAll),
"LIST_INVERT" => Some(lir::BuiltinFn::ListInvert),
"LIST_RANGE" => Some(lir::BuiltinFn::ListRange),
"LIST_RANDOM" => Some(lir::BuiltinFn::ListRandom),
"LIST_VALUE" => Some(lir::BuiltinFn::ListValue),
"LIST_FROM_INT" => Some(lir::BuiltinFn::ListFromInt),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn builtin_recognition() {
assert_eq!(recognize_builtin("RANDOM"), Some(lir::BuiltinFn::Random));
assert_eq!(
recognize_builtin("TURNS_SINCE"),
Some(lir::BuiltinFn::TurnsSince)
);
assert_eq!(
recognize_builtin("LIST_COUNT"),
Some(lir::BuiltinFn::ListCount)
);
assert_eq!(recognize_builtin("random"), None);
assert_eq!(recognize_builtin("unknown"), None);
}
#[test]
fn builtin_recognition_turns() {
assert!(
recognize_builtin("TURNS").is_some(),
"TURNS() should be recognized as a built-in function"
);
}
fn synthetic_infix_prov() -> crate::Provenance {
crate::Provenance::synthetic(
crate::NodeClass::Infix,
rowan::TextRange::new(0.into(), 1.into()),
)
}
#[test]
fn coalesce_chain_spine_walks_the_left_spine_outermost_first() {
fn coalesce(lhs: hir::Expr, rhs: hir::Expr) -> hir::Expr {
hir::Expr::Infix(hir::InfixExpr::new(
synthetic_infix_prov(),
lhs,
crate::InfixOp::Coalesce,
rhs,
))
}
let chain = coalesce(
coalesce(hir::Expr::Int(1), hir::Expr::Int(2)),
hir::Expr::Int(3),
);
let spine = coalesce_chain_spine(&chain);
assert_eq!(spine.len(), 2, "two steps: `1 or 2`, then `… or 3`");
assert!(matches!(spine[0].2, hir::Expr::Int(3)));
assert!(matches!(spine[1].1, hir::Expr::Int(1)));
assert!(matches!(spine[1].2, hir::Expr::Int(2)));
let nested = coalesce(
hir::Expr::Int(1),
coalesce(hir::Expr::Int(2), hir::Expr::Int(3)),
);
assert_eq!(coalesce_chain_spine(&nested).len(), 1);
}
#[test]
fn coalesce_chain_spine_is_empty_for_a_non_coalescing_expr() {
assert!(coalesce_chain_spine(&hir::Expr::Int(1)).is_empty());
assert!(
coalesce_chain_spine(&hir::Expr::Infix(hir::InfixExpr::new(
synthetic_infix_prov(),
hir::Expr::Int(1),
crate::InfixOp::Or,
hir::Expr::Int(2),
)))
.is_empty()
);
}
}