use brink_format::{DefinitionId, DefinitionTag};
use rowan::TextRange;
use crate::determinism::LookupMap;
use crate::symbols::{SymbolIndex, SymbolKind, is_reserved_root_module};
use crate::{Diagnostic, DiagnosticCode, FileId, hir};
use super::context::{
AnalyzerTables, IdAllocator, NameTable, ResolutionLookup, StructCtx, TempMap,
};
use super::expr::const_value_to_map_key;
use super::lambda;
use super::lir;
use super::structs::ShapeTable;
pub fn collect_globals(
files: &[(FileId, &hir::HirFile)],
index: &SymbolIndex,
names: &mut NameTable,
resolutions: &ResolutionLookup,
shapes: &ShapeTable,
diagnostics: &mut Vec<Diagnostic>,
lambda_ctx: &mut GlobalLambdaCtx<'_>,
) -> Vec<lir::GlobalDef> {
let mut const_values: LookupMap<DefinitionId, lir::ConstValue> = LookupMap::new();
let mut globals = Vec::new();
for &(file_id, hir_file) in files {
lambda_ctx.ids.set_path_prefix(hir::root_content_scope_path(
lambda_ctx.file_paths.get(&file_id).map(String::as_str),
));
for cst in &hir_file.constants {
if let Some(id) = lookup_global_or_diagnose(
index,
file_id,
&cst.name.text,
SymbolKind::Constant,
cst.name.range,
diagnostics,
) {
let name = names.intern(&cst.name.text);
let env = ConstEvalEnv {
index,
resolutions,
file: file_id,
const_values: &const_values,
shapes,
native: hir_file.native,
};
let default = eval_decl_default(
&cst.value,
cst.ptr.text_range(),
env,
names,
lambda_ctx,
diagnostics,
);
const_values.insert(id, default.clone());
globals.push(lir::GlobalDef {
id,
name,
mutable: false,
default,
local: false,
});
}
}
}
for &(file_id, hir_file) in files {
lambda_ctx.ids.set_path_prefix(hir::root_content_scope_path(
lambda_ctx.file_paths.get(&file_id).map(String::as_str),
));
for var in &hir_file.variables {
if let Some(id) = lookup_global_or_diagnose(
index,
file_id,
&var.name.text,
SymbolKind::Variable,
var.name.range,
diagnostics,
) {
let name = names.intern(&var.name.text);
let env = ConstEvalEnv {
index,
resolutions,
file: file_id,
const_values: &const_values,
shapes,
native: hir_file.native,
};
let default = eval_decl_default(
&var.value,
var.ptr.text_range(),
env,
names,
lambda_ctx,
diagnostics,
);
globals.push(lir::GlobalDef {
id,
name,
mutable: true,
default,
local: var.is_local,
});
}
}
}
globals
}
fn eval_decl_default(
value: &hir::Expr,
decl_range: TextRange,
env: ConstEvalEnv<'_>,
names: &mut NameTable,
lambda_ctx: &mut GlobalLambdaCtx<'_>,
diagnostics: &mut Vec<Diagnostic>,
) -> lir::ConstValue {
if !is_const_foldable_decl_default(value, env.index, env.resolutions, env.file) {
diagnostics.push(Diagnostic {
file: env.file,
range: decl_range,
message: DiagnosticCode::E083.title().to_string(),
code: DiagnosticCode::E083,
});
}
if let hir::Expr::Lambda(l) = value {
eval_const_lambda(
l,
env.file,
env.native,
env.index,
env.resolutions,
names,
lambda_ctx,
diagnostics,
)
} else {
eval_const_expr(value, env, diagnostics)
}
}
pub struct GlobalLambdaCtx<'a> {
pub ids: &'a mut IdAllocator,
pub lifted: &'a mut Vec<lir::Container>,
pub file_paths: &'a LookupMap<FileId, String>,
pub structs: &'a StructCtx<'a>,
pub tables: AnalyzerTables<'a>,
pub root_id: brink_format::DefinitionId,
}
#[expect(
clippy::too_many_arguments,
reason = "mirrors GlobalLambdaCtx's own field count; a context struct already absorbed the growth"
)]
fn eval_const_lambda(
l: &hir::LambdaExpr,
file: FileId,
native: bool,
index: &SymbolIndex,
resolutions: &ResolutionLookup,
names: &mut NameTable,
lambda_ctx: &mut GlobalLambdaCtx<'_>,
diagnostics: &mut Vec<Diagnostic>,
) -> lir::ConstValue {
let empty_temps = TempMap::new();
let mut next_block_slot = 0u16;
let mut ctx = super::make_ctx(
file,
native,
resolutions,
index,
&empty_temps,
names,
lambda_ctx.ids,
lambda_ctx.root_id,
String::new(),
false,
&[],
lambda_ctx.file_paths,
&mut next_block_slot,
diagnostics,
lambda_ctx.structs,
lambda_ctx.tables,
lambda_ctx.lifted,
);
match lambda::lower_lambda(l, &mut ctx).kind {
lir::ExprKind::MakeFnValue { target, bound } if bound.is_empty() => {
lir::ConstValue::FnRef(target)
}
_ => {
diagnostics.push(Diagnostic {
file,
range: l.ptr.text_range(),
message: DiagnosticCode::E083.title().to_string(),
code: DiagnosticCode::E083,
});
lir::ConstValue::Null
}
}
}
pub fn collect_lists(
files: &[(FileId, &hir::HirFile)],
index: &SymbolIndex,
names: &mut NameTable,
) -> (
Vec<lir::ListDef>,
Vec<lir::ListItemDef>,
Vec<lir::GlobalDef>,
) {
let mut lists = Vec::new();
let mut items = Vec::new();
let mut list_globals = Vec::new();
for &(file_id, hir_file) in files {
for list_decl in &hir_file.lists {
let Some(list_id) =
lookup_global(index, file_id, &list_decl.name.text, SymbolKind::List)
else {
continue;
};
let list_name = names.intern(&list_decl.name.text);
let mut list_items = Vec::new();
let mut active_item_ids = Vec::new();
let mut next_ordinal = 1i32;
for member in &list_decl.members {
let ordinal = member.value.unwrap_or(next_ordinal);
next_ordinal = ordinal + 1;
let qualified = format!("{}.{}", list_decl.name.text, member.name.text);
let item_name = names.intern(&qualified);
if let Some(item_id) =
lookup_global(index, file_id, &qualified, SymbolKind::ListItem)
{
list_items.push((item_name, ordinal));
items.push(lir::ListItemDef {
id: item_id,
name: item_name,
origin: list_id,
ordinal,
});
if member.is_active {
active_item_ids.push(item_id);
}
}
}
lists.push(lir::ListDef {
id: list_id,
name: list_name,
items: list_items,
});
let global_id = list_def_to_global_var(list_id);
list_globals.push(lir::GlobalDef {
id: global_id,
name: list_name,
mutable: true,
default: lir::ConstValue::List {
items: active_item_ids,
origins: vec![list_id],
},
local: false,
});
}
}
(lists, items, list_globals)
}
pub fn list_def_to_global_var(list_id: DefinitionId) -> DefinitionId {
DefinitionId::new(DefinitionTag::GlobalVar, list_id.hash())
}
pub fn collect_externals(
files: &[(FileId, &hir::HirFile)],
index: &SymbolIndex,
names: &mut NameTable,
diagnostics: &mut Vec<Diagnostic>,
) -> Vec<lir::ExternalDef> {
let mut externals = Vec::new();
for &(file_id, hir_file) in files {
for ext in &hir_file.externals {
if let Some(id) = lookup_global_or_diagnose(
index,
file_id,
&ext.name.text,
SymbolKind::External,
ext.name.range,
diagnostics,
) {
let name = names.intern(&ext.name.text);
let fallback = lookup_global(index, file_id, &ext.name.text, SymbolKind::Knot);
externals.push(lir::ExternalDef {
id,
name,
arg_count: ext.param_count,
fallback,
});
}
}
}
externals
}
pub(super) fn lookup_global(
index: &SymbolIndex,
file: FileId,
name: &str,
kind: SymbolKind,
) -> Option<DefinitionId> {
index.by_name.get(name).and_then(|ids| {
ids.iter()
.find(|&&id| {
index
.symbols
.get(&id)
.is_some_and(|info| info.kind == kind && info.file == file)
})
.or_else(|| {
ids.iter().find(|&&id| {
index.symbols.get(&id).is_some_and(|info| {
info.kind == kind
&& !info.module.as_deref().is_some_and(is_reserved_root_module)
})
})
})
.copied()
})
}
fn lookup_global_or_diagnose(
index: &SymbolIndex,
file: FileId,
name: &str,
kind: SymbolKind,
range: rowan::TextRange,
diagnostics: &mut Vec<Diagnostic>,
) -> Option<DefinitionId> {
let id = lookup_global(index, file, name, kind);
if id.is_none() {
diagnostics.push(Diagnostic {
file,
range,
message: DiagnosticCode::E184.title().to_string(),
code: DiagnosticCode::E184,
});
}
id
}
#[derive(Clone, Copy)]
pub struct ConstEvalEnv<'a> {
pub index: &'a SymbolIndex,
pub resolutions: &'a ResolutionLookup,
pub file: FileId,
pub const_values: &'a LookupMap<DefinitionId, lir::ConstValue>,
pub shapes: &'a ShapeTable,
pub native: bool,
}
#[expect(
clippy::cast_possible_truncation,
reason = "f64→f32 is intentional per ink spec"
)]
pub fn eval_const_expr(
expr: &hir::Expr,
env: ConstEvalEnv<'_>,
diagnostics: &mut Vec<Diagnostic>,
) -> lir::ConstValue {
let ConstEvalEnv {
index,
resolutions,
file,
const_values,
native,
..
} = env;
match expr {
hir::Expr::Int(n) => lir::ConstValue::Int(*n),
hir::Expr::Float(bits) => lir::ConstValue::Float(bits.to_f64() as f32),
hir::Expr::Bool(b) => lir::ConstValue::Bool(*b),
hir::Expr::String(s) => eval_const_string(s, file, diagnostics),
hir::Expr::Prefix(hir::PrefixOp::Negate, inner) => {
match eval_const_expr(inner, env, diagnostics) {
lir::ConstValue::Int(n) => lir::ConstValue::Int(-n),
lir::ConstValue::Float(f) => lir::ConstValue::Float(-f),
_ => lir::ConstValue::Null,
}
}
hir::Expr::Prefix(hir::PrefixOp::Not, inner) => {
match eval_const_expr(inner, env, diagnostics) {
lir::ConstValue::Bool(b) => lir::ConstValue::Bool(!b),
lir::ConstValue::Int(n) => lir::ConstValue::Bool(n == 0),
lir::ConstValue::Float(f) => lir::ConstValue::Bool(f == 0.0),
lir::ConstValue::Null => lir::ConstValue::Bool(true),
_ => lir::ConstValue::Null,
}
}
hir::Expr::Infix(ie) => {
let l = eval_const_expr(&ie.lhs, env, diagnostics);
let r = eval_const_expr(&ie.rhs, env, diagnostics);
eval_const_infix(&l, ie.op, &r)
}
hir::Expr::Path(path) => {
fold_path_ref(path, file, resolutions, index, const_values, native)
}
hir::Expr::DivertTarget(path) => {
if let Some(id) = resolutions.resolve(file, path.range) {
lir::ConstValue::DivertTarget(id)
} else {
lir::ConstValue::Null
}
}
hir::Expr::ListLiteral(paths) => {
let mut items = Vec::new();
let mut origins = Vec::new();
for path in paths {
if let Some(id) = resolutions.resolve(file, path.range)
&& let Some(info) = 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) = index.by_name.get(list_name) {
for &list_id in list_ids {
if 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::ConstValue::List { items, origins }
}
hir::Expr::ArrayLiteral(arr) => eval_const_array_literal(arr, env, diagnostics),
hir::Expr::MapLiteral(map) => eval_const_map_literal(map, env, diagnostics),
hir::Expr::StructLiteral(sl) => eval_const_struct_literal(sl, env, diagnostics),
hir::Expr::FnLiteral(fl) => eval_const_fn_literal(fl, env, diagnostics),
_ => lir::ConstValue::Null,
}
}
fn fold_path_ref(
path: &hir::Path,
file: FileId,
resolutions: &ResolutionLookup,
index: &SymbolIndex,
const_values: &LookupMap<DefinitionId, lir::ConstValue>,
native: bool,
) -> lir::ConstValue {
let Some(id) = resolutions.resolve(file, path.range) else {
return lir::ConstValue::Null;
};
let Some(info) = index.symbols.get(&id) else {
return lir::ConstValue::Null;
};
if native && info.is_function_definition() {
return lir::ConstValue::FnRef(id);
}
match info.kind {
SymbolKind::ListItem => lir::ConstValue::List {
items: vec![id],
origins: vec![],
},
SymbolKind::Constant => const_values
.get(&id)
.cloned()
.unwrap_or(lir::ConstValue::Null),
SymbolKind::Variable => lir::ConstValue::Null,
_ => lir::ConstValue::DivertTarget(id),
}
}
fn eval_const_array_literal(
arr: &hir::ArrayLiteral,
env: ConstEvalEnv<'_>,
diagnostics: &mut Vec<Diagnostic>,
) -> lir::ConstValue {
let ConstEvalEnv {
index,
resolutions,
file,
..
} = env;
let mut items = Vec::with_capacity(arr.elements.len());
for e in &arr.elements {
if !is_const_foldable_kind(e, index, resolutions, file) {
diagnostics.push(Diagnostic {
file,
range: arr.ptr.text_range(),
message: DiagnosticCode::E077.title().to_string(),
code: DiagnosticCode::E077,
});
}
items.push(eval_const_expr(e, env, diagnostics));
}
lir::ConstValue::Array(items)
}
fn eval_const_map_literal(
map: &hir::MapLiteral,
env: ConstEvalEnv<'_>,
diagnostics: &mut Vec<Diagnostic>,
) -> lir::ConstValue {
let ConstEvalEnv {
index,
resolutions,
file,
..
} = env;
let mut entries = Vec::with_capacity(map.entries.len());
for (k, v) in &map.entries {
if !is_const_foldable_kind(v, index, resolutions, file) {
diagnostics.push(Diagnostic {
file,
range: map.ptr.text_range(),
message: DiagnosticCode::E077.title().to_string(),
code: DiagnosticCode::E077,
});
}
let key_val = eval_const_expr(k, env, diagnostics);
let value = eval_const_expr(v, env, diagnostics);
match const_value_to_map_key(key_val) {
Some(key) => entries.push((key, value)),
None => {
diagnostics.push(Diagnostic {
file,
range: map.ptr.text_range(),
message: DiagnosticCode::E076.title().to_string(),
code: DiagnosticCode::E076,
});
}
}
}
lir::ConstValue::Map(entries)
}
fn eval_const_struct_literal(
sl: &hir::StructLiteral,
env: ConstEvalEnv<'_>,
diagnostics: &mut Vec<Diagnostic>,
) -> lir::ConstValue {
let ConstEvalEnv {
index,
resolutions,
file,
shapes,
..
} = env;
let shape = resolutions
.resolve(file, sl.shape.range)
.and_then(|id| shapes.get_by_def(id));
let Some(shape) = shape else {
diagnostics.push(Diagnostic {
file,
range: sl.ptr.text_range(),
message: DiagnosticCode::E073.title().to_string(),
code: DiagnosticCode::E073,
});
return lir::ConstValue::Null;
};
let mut placed: Vec<Option<lir::ConstValue>> = vec![None; shape.fields.len()];
let mut has_extra = false;
for (name, value) in &sl.fields {
if !is_const_foldable_kind(value, index, resolutions, file) {
diagnostics.push(Diagnostic {
file,
range: sl.ptr.text_range(),
message: DiagnosticCode::E077.title().to_string(),
code: DiagnosticCode::E077,
});
}
let folded = eval_const_expr(value, env, diagnostics);
match shape.field(&name.text) {
Some((offset, _)) => {
if let Some(slot) = placed.get_mut(offset as usize) {
*slot = Some(folded);
}
}
None => has_extra = true,
}
}
if has_extra || placed.iter().any(Option::is_none) {
diagnostics.push(Diagnostic {
file,
range: sl.ptr.text_range(),
message: DiagnosticCode::E075.title().to_string(),
code: DiagnosticCode::E075,
});
return lir::ConstValue::Null;
}
lir::ConstValue::Record {
shape_id: shape.id,
fields: placed
.into_iter()
.map(|v| v.unwrap_or(lir::ConstValue::Null))
.collect(),
}
}
fn eval_const_fn_literal(
fl: &hir::FnLiteral,
env: ConstEvalEnv<'_>,
diagnostics: &mut Vec<Diagnostic>,
) -> lir::ConstValue {
let ConstEvalEnv {
index,
resolutions,
file,
..
} = env;
let Some(target_id) = resolutions.resolve(file, fl.target.range) else {
return lir::ConstValue::Null;
};
let Some(target_info) = index.symbols.get(&target_id) else {
return lir::ConstValue::Null;
};
if fl.args.is_empty() {
return lir::ConstValue::FnRef(target_id);
}
let mut bound = Vec::with_capacity(fl.args.len());
for (i, arg) in fl.args.iter().enumerate() {
let param = target_info.params.get(i);
let name = param.map_or_else(String::new, |p| p.name.clone());
let is_ref = param.is_some_and(|p| p.is_ref);
if is_ref {
let Some(cell) = (match arg {
hir::Expr::Path(p) => resolutions.resolve(file, p.range),
_ => None,
}) else {
return lir::ConstValue::Null;
};
bound.push(lir::ConstClosureEntry::Ref { name, cell });
} else {
if !is_const_foldable_kind(arg, index, resolutions, file) {
diagnostics.push(Diagnostic {
file,
range: fl.ptr.text_range(),
message: DiagnosticCode::E077.title().to_string(),
code: DiagnosticCode::E077,
});
}
let value = eval_const_expr(arg, env, diagnostics);
bound.push(lir::ConstClosureEntry::Val { name, value });
}
}
lir::ConstValue::Closure {
target: target_id,
env: bound,
}
}
fn is_const_foldable_kind(
expr: &hir::Expr,
index: &SymbolIndex,
resolutions: &ResolutionLookup,
file: FileId,
) -> bool {
match expr {
hir::Expr::Int(_)
| hir::Expr::Float(_)
| hir::Expr::Bool(_)
| hir::Expr::String(_)
| hir::Expr::Null
| hir::Expr::DivertTarget(_)
| hir::Expr::ListLiteral(_)
| hir::Expr::ArrayLiteral(_)
| hir::Expr::MapLiteral(_)
| hir::Expr::StructLiteral(_) => true,
hir::Expr::Path(path) => !matches!(
resolutions
.resolve(file, path.range)
.and_then(|id| index.symbols.get(&id)),
Some(info) if info.kind == SymbolKind::Variable
),
hir::Expr::Prefix(_, inner) => is_const_foldable_kind(inner, index, resolutions, file),
hir::Expr::Infix(ie) => {
is_const_foldable_kind(&ie.lhs, index, resolutions, file)
&& is_const_foldable_kind(&ie.rhs, index, resolutions, file)
}
hir::Expr::Postfix(..)
| hir::Expr::Call(..)
| hir::Expr::Index(_)
| hir::Expr::FieldAccess(_)
| hir::Expr::FnLiteral(_)
| hir::Expr::RefArg(_)
| hir::Expr::Range(_)
| hir::Expr::Lambda(_)
| hir::Expr::Fragment(_) => false,
}
}
fn is_const_foldable_decl_default(
expr: &hir::Expr,
index: &SymbolIndex,
resolutions: &ResolutionLookup,
file: FileId,
) -> bool {
match expr {
hir::Expr::Int(_)
| hir::Expr::Float(_)
| hir::Expr::Bool(_)
| hir::Expr::String(_)
| hir::Expr::Null
| hir::Expr::DivertTarget(_)
| hir::Expr::ListLiteral(_)
| hir::Expr::ArrayLiteral(_)
| hir::Expr::MapLiteral(_)
| hir::Expr::StructLiteral(_)
| hir::Expr::FnLiteral(_)
| hir::Expr::Lambda(_) => true,
hir::Expr::Path(path) => !matches!(
resolutions
.resolve(file, path.range)
.and_then(|id| index.symbols.get(&id)),
Some(info) if info.kind == SymbolKind::Variable
),
hir::Expr::Prefix(_, inner) => {
is_const_foldable_decl_default(inner, index, resolutions, file)
}
hir::Expr::Infix(ie) => {
is_const_foldable_decl_default(&ie.lhs, index, resolutions, file)
&& is_const_foldable_decl_default(&ie.rhs, index, resolutions, file)
}
hir::Expr::Postfix(..)
| hir::Expr::Call(..)
| hir::Expr::Index(_)
| hir::Expr::FieldAccess(_)
| hir::Expr::RefArg(_)
| hir::Expr::Range(_)
| hir::Expr::Fragment(_) => false,
}
}
fn eval_const_string(
s: &hir::StringExpr,
file: FileId,
diagnostics: &mut Vec<Diagnostic>,
) -> lir::ConstValue {
let mut has_interpolation = false;
let text: String = s
.parts
.iter()
.filter_map(|p| match p {
hir::StringPart::Literal(t) => Some(t.as_str()),
hir::StringPart::Interpolation(_) => {
has_interpolation = true;
None
}
})
.collect();
if has_interpolation {
diagnostics.push(Diagnostic {
file,
range: rowan::TextRange::default(),
message: DiagnosticCode::E030.title().to_string(),
code: DiagnosticCode::E030,
});
}
lir::ConstValue::String(text)
}
fn eval_const_infix(
lhs: &lir::ConstValue,
op: hir::InfixOp,
rhs: &lir::ConstValue,
) -> lir::ConstValue {
use hir::InfixOp;
use lir::ConstValue;
if matches!(op, InfixOp::Has | InfixOp::HasNot | InfixOp::Intersect) {
return ConstValue::Null;
}
if op == InfixOp::Add
&& let (ConstValue::String(a), ConstValue::String(b)) = (lhs, rhs)
{
return ConstValue::String(format!("{a}{b}"));
}
match (lhs, rhs) {
(ConstValue::Int(a), ConstValue::Int(b)) => eval_int_infix(*a, op, *b),
(ConstValue::Float(a), ConstValue::Float(b)) => {
eval_float_infix(f64::from(*a), op, f64::from(*b))
}
(ConstValue::Int(a), ConstValue::Float(b)) => {
eval_float_infix(f64::from(*a), op, f64::from(*b))
}
(ConstValue::Float(a), ConstValue::Int(b)) => {
eval_float_infix(f64::from(*a), op, f64::from(*b))
}
(ConstValue::Bool(a), ConstValue::Bool(b)) => eval_bool_infix(*a, op, *b),
_ => ConstValue::Null,
}
}
fn eval_int_infix(a: i32, op: hir::InfixOp, b: i32) -> lir::ConstValue {
use hir::InfixOp;
use lir::ConstValue;
match op {
InfixOp::Add => ConstValue::Int(a.wrapping_add(b)),
InfixOp::Sub => ConstValue::Int(a.wrapping_sub(b)),
InfixOp::Mul => ConstValue::Int(a.wrapping_mul(b)),
InfixOp::Div => {
if b == 0 {
ConstValue::Null
} else {
ConstValue::Int(a.wrapping_div(b))
}
}
InfixOp::Mod => {
if b == 0 {
ConstValue::Null
} else {
ConstValue::Int(a.wrapping_rem(b))
}
}
InfixOp::Eq => ConstValue::Bool(a == b),
InfixOp::NotEq => ConstValue::Bool(a != b),
InfixOp::Lt => ConstValue::Bool(a < b),
InfixOp::Gt => ConstValue::Bool(a > b),
InfixOp::LtEq => ConstValue::Bool(a <= b),
InfixOp::GtEq => ConstValue::Bool(a >= b),
InfixOp::And => ConstValue::Bool(a != 0 && b != 0),
InfixOp::Or => ConstValue::Bool(a != 0 || b != 0),
_ => ConstValue::Null,
}
}
#[expect(
clippy::cast_possible_truncation,
clippy::float_cmp,
reason = "f64→f32 is intentional per ink spec; ink uses exact float comparison"
)]
fn eval_float_infix(a: f64, op: hir::InfixOp, b: f64) -> lir::ConstValue {
use hir::InfixOp;
use lir::ConstValue;
match op {
InfixOp::Add => ConstValue::Float((a + b) as f32),
InfixOp::Sub => ConstValue::Float((a - b) as f32),
InfixOp::Mul => ConstValue::Float((a * b) as f32),
InfixOp::Div => ConstValue::Float((a / b) as f32),
InfixOp::Mod => ConstValue::Float((a % b) as f32),
InfixOp::Eq => ConstValue::Bool(a == b),
InfixOp::NotEq => ConstValue::Bool(a != b),
InfixOp::Lt => ConstValue::Bool(a < b),
InfixOp::Gt => ConstValue::Bool(a > b),
InfixOp::LtEq => ConstValue::Bool(a <= b),
InfixOp::GtEq => ConstValue::Bool(a >= b),
InfixOp::And => ConstValue::Bool(a != 0.0 && b != 0.0),
InfixOp::Or => ConstValue::Bool(a != 0.0 || b != 0.0),
_ => ConstValue::Null,
}
}
fn eval_bool_infix(a: bool, op: hir::InfixOp, b: bool) -> lir::ConstValue {
use hir::InfixOp;
use lir::ConstValue;
match op {
InfixOp::And => ConstValue::Bool(a && b),
InfixOp::Or => ConstValue::Bool(a || b),
InfixOp::Eq => ConstValue::Bool(a == b),
InfixOp::NotEq => ConstValue::Bool(a != b),
_ => ConstValue::Null,
}
}
#[cfg(test)]
mod tests {
use brink_format::DefinitionTag;
use rowan::TextRange;
use super::*;
use crate::symbols::{SymbolInfo, Visibility};
fn insert(
index: &mut SymbolIndex,
id: DefinitionId,
file: FileId,
name: &str,
kind: SymbolKind,
module: Option<&str>,
) {
index.symbols.insert(
id,
SymbolInfo {
kind,
file,
range: TextRange::default(),
id,
name: name.to_string(),
params: Vec::new(),
detail: None,
scope: None,
param_detail: None,
module: module.map(str::to_string),
visibility: Visibility::Public,
},
);
index.by_name.entry(name.to_string()).or_default().push(id);
}
#[test]
fn extern_only_no_fallback_does_not_bind_a_std_mounts_fn_of_the_same_name() {
let mut index = SymbolIndex::default();
let project_file = FileId(0);
let std_file = FileId(1);
let project_extern = DefinitionId::new(DefinitionTag::ExternalFn, 1);
insert(
&mut index,
project_extern,
project_file,
"scene_entered",
SymbolKind::External,
Some("story::story"),
);
let std_extern = DefinitionId::new(DefinitionTag::ExternalFn, 2);
insert(
&mut index,
std_extern,
std_file,
"scene_entered",
SymbolKind::External,
Some("std::conventions::screenplay"),
);
let std_fallback = DefinitionId::new(DefinitionTag::Address, 3);
insert(
&mut index,
std_fallback,
std_file,
"scene_entered",
SymbolKind::Knot,
Some("std::conventions::screenplay"),
);
assert_eq!(
lookup_global(&index, project_file, "scene_entered", SymbolKind::External),
Some(project_extern)
);
assert_eq!(
lookup_global(&index, project_file, "scene_entered", SymbolKind::Knot),
None,
"an extern with no project-side fallback must not silently bind \
a same-named fn from a mounted std:: module"
);
}
#[test]
fn extern_only_no_fallback_still_finds_a_non_std_sibling_fallback() {
let mut index = SymbolIndex::default();
let project_file = FileId(0);
let sibling_file = FileId(1);
let project_extern = DefinitionId::new(DefinitionTag::ExternalFn, 10);
insert(
&mut index,
project_extern,
project_file,
"narrate",
SymbolKind::External,
None,
);
let sibling_fallback = DefinitionId::new(DefinitionTag::Address, 11);
insert(
&mut index,
sibling_fallback,
sibling_file,
"narrate",
SymbolKind::Knot,
None,
);
assert_eq!(
lookup_global(&index, project_file, "narrate", SymbolKind::Knot),
Some(sibling_fallback),
"a non-std cross-file fallback pair must still resolve, unchanged \
from pre-#2197 behavior"
);
}
fn hir_for(src: &str) -> hir::HirFile {
let parsed = brink_syntax::parse(src);
let (hir, _manifest, _diag) = hir::lower(FileId(0), &parsed.tree());
hir
}
#[test]
fn lookup_global_or_diagnose_pushes_e184_when_every_surviving_candidate_is_std_declared() {
for kind in [SymbolKind::Constant, SymbolKind::Variable] {
let mut index = SymbolIndex::default();
let ghost_file = FileId(7);
let std_file = FileId(9);
let std_def_id = DefinitionId::new(DefinitionTag::GlobalVar, 1);
insert(
&mut index,
std_def_id,
std_file,
"MAX_HP",
kind,
Some("std::x"),
);
let mut diagnostics = Vec::new();
let range = TextRange::new(3.into(), 9.into());
let id = lookup_global_or_diagnose(
&index,
ghost_file,
"MAX_HP",
kind,
range,
&mut diagnostics,
);
assert_eq!(
id, None,
"the declaration still can't resolve its own identity for kind {kind:?}"
);
assert_eq!(
diagnostics.len(),
1,
"the unresolvable lookup must raise exactly one diagnostic for kind {kind:?}"
);
assert_eq!(diagnostics[0].code, DiagnosticCode::E184);
assert_eq!(diagnostics[0].file, ghost_file);
assert_eq!(diagnostics[0].range, range);
}
}
#[test]
fn lookup_global_or_diagnose_pushes_nothing_when_the_lookup_succeeds() {
let mut index = SymbolIndex::default();
let project_file = FileId(0);
let expected_id = DefinitionId::new(DefinitionTag::GlobalVar, 2);
insert(
&mut index,
expected_id,
project_file,
"score",
SymbolKind::Variable,
None,
);
let mut diagnostics = Vec::new();
let id = lookup_global_or_diagnose(
&index,
project_file,
"score",
SymbolKind::Variable,
TextRange::default(),
&mut diagnostics,
);
assert_eq!(id, Some(expected_id));
assert!(
diagnostics.is_empty(),
"an ordinary successful lookup must not raise E184: {diagnostics:?}"
);
}
#[test]
fn collect_externals_reports_e184_when_every_surviving_candidate_is_std_declared() {
let ghost_file = FileId(7);
let ghost_hir = hir_for("EXTERNAL scene_entered(title, slug)\nHello.\n-> END\n");
let mut index = SymbolIndex::default();
let std_file = FileId(9);
let std_def_id = DefinitionId::new(DefinitionTag::ExternalFn, 1);
insert(
&mut index,
std_def_id,
std_file,
"scene_entered",
SymbolKind::External,
Some("std::conventions::screenplay"),
);
let mut names = NameTable::new();
let mut diagnostics = Vec::new();
let externals = collect_externals(
&[(ghost_file, &ghost_hir)],
&index,
&mut names,
&mut diagnostics,
);
assert!(
externals.is_empty(),
"the external still can't resolve its own identity, so it still \
contributes no ExternalDef — E184 makes the drop loud, not \
stops it from happening"
);
assert_eq!(
diagnostics.len(),
1,
"the unresolvable self-declaration lookup must raise exactly one diagnostic"
);
assert_eq!(diagnostics[0].code, DiagnosticCode::E184);
assert_eq!(diagnostics[0].file, ghost_file);
assert_eq!(
diagnostics[0].range, ghost_hir.externals[0].name.range,
"reported at the external's own name span"
);
}
}