use std::borrow::Cow;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use brink_format::{DefinitionId, DefinitionTag, NameId};
use rowan::TextRange;
use crate::FileId;
use crate::determinism::{LookupMap, LookupSet};
use crate::provenance::{NodeClass, Provenance};
use crate::symbols::{ResolutionMap, SymbolIndex, SymbolInfo};
use super::structs::{GlobalShapeMap, ShapeTable};
pub struct ResolutionLookup {
map: LookupMap<(FileId, TextRange), DefinitionId>,
}
impl ResolutionLookup {
pub fn build(resolutions: &ResolutionMap) -> Self {
let map = resolutions
.iter()
.map(|r| ((r.file, r.range), r.target))
.collect();
Self { map }
}
pub fn resolve(&self, file: FileId, range: TextRange) -> Option<DefinitionId> {
self.map.get(&(file, range)).copied()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum UfcsVerdict {
FieldCall,
FreeFnDesugar { target: DefinitionId },
FreeFnAutoRef { target: DefinitionId },
PreludeDesugar { name: String },
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct UfcsLookup {
map: LookupMap<(FileId, TextRange), UfcsVerdict>,
}
impl UfcsLookup {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_entries(entries: Vec<(FileId, TextRange, UfcsVerdict)>) -> Self {
Self {
map: entries.into_iter().map(|(f, r, v)| ((f, r), v)).collect(),
}
}
#[must_use]
pub fn get(&self, file: FileId, range: TextRange) -> Option<&UfcsVerdict> {
self.map.get(&(file, range))
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
#[must_use]
pub fn call_sites_for_target(&self, target: DefinitionId) -> Vec<(FileId, TextRange)> {
let mut sites: Vec<(FileId, TextRange)> = self
.map
.iter()
.filter_map(|(&(file, range), verdict)| match *verdict {
UfcsVerdict::FreeFnDesugar { target: t }
| UfcsVerdict::FreeFnAutoRef { target: t }
if t == target =>
{
Some((file, range))
}
_ => None,
})
.collect();
sites.sort_by_key(|&(file, range)| (file, range.start(), range.end()));
sites
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CoalesceShape {
PreserveOption,
Collapse,
#[default]
RuntimeCheck,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CoalesceLookup {
map: LookupMap<(FileId, TextRange), Vec<CoalesceShape>>,
}
impl CoalesceLookup {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_entries(entries: Vec<(FileId, TextRange, Vec<CoalesceShape>)>) -> Self {
Self {
map: entries.into_iter().map(|(f, r, v)| ((f, r), v)).collect(),
}
}
#[must_use]
pub fn get(&self, file: FileId, range: TextRange) -> Option<&[CoalesceShape]> {
self.map.get(&(file, range)).map(Vec::as_slice)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
}
#[derive(Debug, Clone, Copy)]
pub struct AnalyzerTables<'a> {
pub ufcs: &'a UfcsLookup,
pub coalesce: &'a CoalesceLookup,
}
pub struct NameTable {
map: LookupMap<String, NameId>,
entries: Vec<String>,
}
impl NameTable {
pub fn new() -> Self {
Self {
map: LookupMap::new(),
entries: Vec::new(),
}
}
pub fn intern(&mut self, name: &str) -> NameId {
if let Some(&id) = self.map.get(name) {
return id;
}
#[expect(
clippy::cast_possible_truncation,
reason = "name table won't exceed u16::MAX"
)]
let id = NameId(self.entries.len() as u16);
self.entries.push(name.to_string());
self.map.insert(name.to_string(), id);
id
}
pub fn into_entries(self) -> Vec<String> {
self.entries
}
pub fn from_entries(entries: Vec<String>) -> Self {
let map = entries
.iter()
.enumerate()
.map(|(i, s)| {
#[expect(
clippy::cast_possible_truncation,
reason = "name table won't exceed u16::MAX"
)]
(s.clone(), NameId(i as u16))
})
.collect();
Self { map, entries }
}
}
#[must_use]
pub fn root_definition_id() -> DefinitionId {
IdAllocator::new().alloc_address("")
}
pub struct IdAllocator {
used: LookupMap<String, DefinitionId>,
emitted_shared: crate::determinism::LookupSet<DefinitionId>,
emitted_bodied: crate::determinism::LookupSet<DefinitionId>,
seq_counter: usize,
path_prefix: String,
}
impl IdAllocator {
pub fn new() -> Self {
Self {
used: LookupMap::new(),
seq_counter: 0,
emitted_shared: crate::determinism::LookupSet::new(),
emitted_bodied: crate::determinism::LookupSet::new(),
path_prefix: String::new(),
}
}
pub fn set_path_prefix(&mut self, prefix: String) {
self.path_prefix = prefix;
}
pub fn alloc_address(&mut self, path: &str) -> DefinitionId {
let qualified = qualify_path(&self.path_prefix, path);
if let Some(&id) = self.used.get(qualified.as_ref()) {
return id;
}
let hash = hash_path(&qualified);
let id = DefinitionId::new(DefinitionTag::Address, hash);
self.used.insert(qualified.into_owned(), id);
id
}
#[must_use]
pub fn qualify_lambda_path(&self, path: &str) -> String {
qualify_path(&self.path_prefix, path).into_owned()
}
pub fn mark_shared_emitted(&mut self, id: DefinitionId) -> bool {
self.emitted_shared.insert(id)
}
pub fn mark_bodied_emitted(&mut self, id: DefinitionId) {
self.emitted_bodied.insert(id);
}
pub fn is_bodied_emitted(&self, id: DefinitionId) -> bool {
self.emitted_bodied.contains(&id)
}
pub fn next_seq_index(&mut self) -> usize {
let idx = self.seq_counter;
self.seq_counter += 1;
idx
}
pub fn reset_seq_counter(&mut self) {
self.seq_counter = 0;
}
}
fn qualify_path<'a>(prefix: &str, path: &'a str) -> Cow<'a, str> {
if prefix.is_empty() {
Cow::Borrowed(path)
} else if path.is_empty() {
Cow::Owned(prefix.to_string())
} else {
Cow::Owned(format!("{prefix}.{path}"))
}
}
fn hash_path(path: &str) -> u64 {
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
hasher.finish()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TypeMode {
#[default]
Gradual,
Strict,
}
pub struct StructCtx<'a> {
pub shapes: &'a ShapeTable,
pub global_shapes: &'a GlobalShapeMap,
pub type_mode: TypeMode,
}
pub struct LowerCtx<'a> {
pub file: FileId,
pub native: bool,
pub resolutions: &'a ResolutionLookup,
pub index: &'a SymbolIndex,
pub temps: &'a TempMap,
pub names: &'a mut NameTable,
pub ids: &'a mut IdAllocator,
pub scope_path: String,
pub is_root_content_scope: bool,
pub pending_children: Vec<super::lir::Container>,
pub visible_temps: LookupSet<String>,
pub file_paths: &'a LookupMap<FileId, String>,
pub root_id: brink_format::DefinitionId,
pub choice_gather_target: Option<brink_format::DefinitionId>,
pub next_block_slot: &'a mut u16,
pub block_scopes: Vec<Vec<(String, u16)>>,
pub as_binding_slots: crate::determinism::LookupSet<u16>,
pub block_scoped_temp_names: LookupSet<String>,
pub diagnostics: &'a mut Vec<crate::Diagnostic>,
pub loop_depth: u32,
pub structs: &'a StructCtx<'a>,
pub temp_shapes: LookupMap<u16, DefinitionId>,
pub tables: AnalyzerTables<'a>,
pub lifted: &'a mut Vec<crate::lir::types::Container>,
pub current_stmt_provenance: Provenance,
}
impl<'a> LowerCtx<'a> {
pub fn resolve_path(&self, range: TextRange) -> Option<&'a SymbolInfo> {
let id = self.resolutions.resolve(self.file, range)?;
self.index.symbols.get(&id)
}
pub fn resolve_id(&self, range: TextRange) -> Option<DefinitionId> {
self.resolutions.resolve(self.file, range)
}
pub fn enter_stmt(&mut self, provenance: Provenance) -> Provenance {
self.current_stmt_provenance = provenance;
provenance
}
#[must_use]
pub fn provenance_at(&self, range: TextRange, class: NodeClass) -> Provenance {
Provenance::new(
self.file,
range,
crate::provenance::KindToken {
class,
raw: crate::provenance::KindToken::SYNTHETIC_RAW,
},
)
}
pub fn temp_slot(&self, name: &str) -> Option<u16> {
if let Some(slot) = self.lookup_block_local(name) {
return Some(slot);
}
if self.visible_temps.contains(name) {
self.temps.get(name)
} else {
None
}
}
fn lookup_block_local(&self, name: &str) -> Option<u16> {
for frame in self.block_scopes.iter().rev() {
if let Some(&(_, slot)) = frame.iter().rev().find(|(n, _)| n == name) {
return Some(slot);
}
}
None
}
pub fn push_block_scope(&mut self) {
self.block_scopes.push(Vec::new());
}
pub fn pop_block_scope(&mut self) {
self.block_scopes.pop();
}
pub fn alloc_block_slot(&mut self) -> u16 {
let slot = *self.next_block_slot;
*self.next_block_slot += 1;
slot
}
pub fn is_name_visible(&self, name: &str) -> bool {
self.lookup_block_local(name).is_some() || self.visible_temps.contains(name)
}
pub fn declare_block_local(&mut self, name: String, slot: u16) {
self.block_scoped_temp_names.insert(name.clone());
if self.block_scopes.is_empty() {
self.block_scopes.push(Vec::new());
}
if let Some(frame) = self.block_scopes.last_mut() {
frame.push((name, slot));
}
}
pub fn temp_slot_raw(&self, name: &str) -> Option<u16> {
self.temps.get(name)
}
pub fn set_temp_shape(&mut self, slot: u16, shape_def: DefinitionId) {
self.temp_shapes.insert(slot, shape_def);
}
pub fn record_temp_annotation(&mut self, slot: u16, annotation: Option<&crate::hir::TypeExpr>) {
let shape = annotation
.and_then(|ann| self.resolutions.resolve(self.file, ann.range()))
.and_then(|id| self.structs.shapes.get_by_def(id));
if let Some(shape) = shape {
self.set_temp_shape(slot, shape.definition_id);
}
}
pub fn temp_shape(&self, slot: u16) -> Option<DefinitionId> {
self.temp_shapes.get(&slot).copied()
}
pub fn global_shape(&self, id: DefinitionId) -> Option<DefinitionId> {
self.structs.global_shapes.get(&id).copied()
}
pub fn qualify_label(&self, label: &str) -> String {
if self.scope_path.is_empty() {
label.to_string()
} else {
format!("{}.{label}", self.scope_path)
}
}
pub fn alloc_sequence_id(&mut self, counter: usize) -> DefinitionId {
let path = if self.scope_path.is_empty() {
format!("s-{counter}")
} else {
format!("{}.s-{counter}", self.scope_path)
};
self.ids.alloc_address(&path)
}
pub fn lookup_address_id(&self, label: &str) -> Option<DefinitionId> {
use crate::symbols::SymbolKind;
fn is_container(info: &SymbolInfo) -> bool {
matches!(
info.kind,
SymbolKind::Knot | SymbolKind::Stitch | SymbolKind::Label
)
}
let qualified = self.qualify_label(label);
self.index.by_name.get(&qualified).and_then(|ids| {
ids.iter()
.find(|&&id| {
self.index
.symbols
.get(&id)
.is_some_and(|info| is_container(info) && info.file == self.file)
})
.or_else(|| ids.first())
.copied()
})
}
}
#[derive(Debug, Clone, Default)]
pub struct TempMap {
slots: LookupMap<String, u16>,
}
impl TempMap {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, name: String, slot: u16) {
self.slots.insert(name, slot);
}
pub fn get(&self, name: &str) -> Option<u16> {
self.slots.get(name).copied()
}
pub fn total_slots(&self) -> u16 {
#[expect(
clippy::cast_possible_truncation,
reason = "temp count won't exceed u16::MAX"
)]
{
self.slots.len() as u16
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::symbols::ResolvedRef;
#[test]
fn name_table_deduplication() {
let mut table = NameTable::new();
let a = table.intern("hello");
let b = table.intern("world");
let c = table.intern("hello");
assert_eq!(a, c);
assert_ne!(a, b);
assert_eq!(table.into_entries(), vec!["hello", "world"]);
}
#[test]
fn resolution_lookup() {
let refs = vec![ResolvedRef {
file: FileId(0),
range: TextRange::new(10.into(), 15.into()),
target: DefinitionId::new(DefinitionTag::Address, 42),
}];
let lookup = ResolutionLookup::build(&refs);
assert_eq!(
lookup.resolve(FileId(0), TextRange::new(10.into(), 15.into())),
Some(DefinitionId::new(DefinitionTag::Address, 42))
);
assert_eq!(
lookup.resolve(FileId(1), TextRange::new(10.into(), 15.into())),
None
);
}
#[test]
fn id_allocator_stable() {
let mut alloc = IdAllocator::new();
let a = alloc.alloc_address("knot.c0");
let b = alloc.alloc_address("knot.c0");
assert_eq!(a, b);
let c = alloc.alloc_address("knot.c1");
assert_ne!(a, c);
}
#[test]
fn temp_map_slots() {
let mut map = TempMap::new();
map.insert("x".to_string(), 0);
map.insert("y".to_string(), 1);
assert_eq!(map.get("x"), Some(0));
assert_eq!(map.get("y"), Some(1));
assert_eq!(map.get("z"), None);
assert_eq!(map.total_slots(), 2);
}
}