use std::collections::HashMap;
use crate::error::CompilerError;
use crate::location::Span;
use super::types::{IrEnum, IrFunction, IrImpl, IrLet, IrStruct, IrTrait};
use super::{EnumId, FunctionId, ImplId, IrImport, StructId, TraitId};
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct IrModule {
pub structs: Vec<IrStruct>,
pub traits: Vec<IrTrait>,
pub enums: Vec<IrEnum>,
pub impls: Vec<IrImpl>,
pub lets: Vec<IrLet>,
pub functions: Vec<IrFunction>,
pub imports: Vec<IrImport>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modules: Vec<IrModuleNode>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub file_table: Vec<std::path::PathBuf>,
#[serde(skip)]
struct_names: HashMap<String, StructId>,
#[serde(skip)]
trait_names: HashMap<String, TraitId>,
#[serde(skip)]
enum_names: HashMap<String, EnumId>,
#[serde(skip)]
function_names: HashMap<String, FunctionId>,
#[serde(skip)]
let_names: HashMap<String, usize>,
}
#[expect(
clippy::exhaustive_structs,
reason = "IR types are constructed directly by consumer code"
)]
#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct IrModuleNode {
pub name: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub structs: Vec<StructId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub traits: Vec<TraitId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enums: Vec<EnumId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub functions: Vec<FunctionId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub modules: Vec<Self>,
}
impl IrModule {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn get_struct(&self, id: StructId) -> Option<&IrStruct> {
self.structs.get(id.0 as usize)
}
#[must_use]
pub fn get_trait(&self, id: TraitId) -> Option<&IrTrait> {
self.traits.get(id.0 as usize)
}
#[must_use]
pub fn get_enum(&self, id: EnumId) -> Option<&IrEnum> {
self.enums.get(id.0 as usize)
}
#[must_use]
pub fn struct_id(&self, name: &str) -> Option<StructId> {
self.struct_names.get(name).copied()
}
#[must_use]
pub fn trait_id(&self, name: &str) -> Option<TraitId> {
self.trait_names.get(name).copied()
}
#[must_use]
pub fn enum_id(&self, name: &str) -> Option<EnumId> {
self.enum_names.get(name).copied()
}
#[expect(
clippy::result_large_err,
reason = "CompilerError is large by design; callers push errors into a Vec so allocation is bounded"
)]
pub(crate) fn add_struct(
&mut self,
name: String,
s: IrStruct,
) -> Result<StructId, CompilerError> {
let id = u32::try_from(self.structs.len())
.map(StructId)
.map_err(|_| CompilerError::TooManyDefinitions {
kind: "struct",
span: Span::default(),
})?;
self.struct_names.insert(name, id);
self.structs.push(s);
Ok(id)
}
#[expect(
clippy::result_large_err,
reason = "CompilerError is large by design; callers push errors into a Vec so allocation is bounded"
)]
pub(crate) fn add_trait(&mut self, name: String, t: IrTrait) -> Result<TraitId, CompilerError> {
let id = u32::try_from(self.traits.len()).map(TraitId).map_err(|_| {
CompilerError::TooManyDefinitions {
kind: "trait",
span: Span::default(),
}
})?;
self.trait_names.insert(name, id);
self.traits.push(t);
Ok(id)
}
#[expect(
clippy::result_large_err,
reason = "CompilerError is large by design; callers push errors into a Vec so allocation is bounded"
)]
pub(crate) fn add_enum(&mut self, name: String, e: IrEnum) -> Result<EnumId, CompilerError> {
let id = u32::try_from(self.enums.len()).map(EnumId).map_err(|_| {
CompilerError::TooManyDefinitions {
kind: "enum",
span: Span::default(),
}
})?;
self.enum_names.insert(name, id);
self.enums.push(e);
Ok(id)
}
pub(crate) fn struct_mut(&mut self, id: StructId) -> Option<&mut IrStruct> {
self.structs.get_mut(id.0 as usize)
}
pub(crate) fn trait_mut(&mut self, id: TraitId) -> Option<&mut IrTrait> {
self.traits.get_mut(id.0 as usize)
}
pub(crate) fn enum_mut(&mut self, id: EnumId) -> Option<&mut IrEnum> {
self.enums.get_mut(id.0 as usize)
}
#[expect(
clippy::result_large_err,
reason = "CompilerError is large by design; callers push errors into a Vec so allocation is bounded"
)]
pub(crate) fn add_impl(&mut self, i: IrImpl) -> Result<ImplId, CompilerError> {
let id = u32::try_from(self.impls.len()).map(ImplId).map_err(|_| {
CompilerError::TooManyDefinitions {
kind: "impl",
span: Span::default(),
}
})?;
self.impls.push(i);
Ok(id)
}
#[must_use]
pub(crate) fn next_impl_id(&self) -> Option<ImplId> {
u32::try_from(self.impls.len()).ok().map(ImplId)
}
#[must_use]
pub fn get_let(&self, name: &str) -> Option<&IrLet> {
self.let_names.get(name).and_then(|&idx| self.lets.get(idx))
}
#[must_use]
pub fn has_let(&self, name: &str) -> bool {
self.let_names.contains_key(name)
}
pub(crate) fn add_let(&mut self, l: IrLet) {
let idx = self.lets.len();
self.let_names.insert(l.name.clone(), idx);
self.lets.push(l);
}
#[must_use]
pub fn get_function(&self, id: FunctionId) -> Option<&IrFunction> {
self.functions.get(id.0 as usize)
}
#[must_use]
pub fn function_id(&self, name: &str) -> Option<FunctionId> {
self.function_names.get(name).copied()
}
#[must_use]
pub fn file_path(&self, file: crate::ir::FileId) -> Option<&std::path::PathBuf> {
if file.is_synthetic() {
return None;
}
let idx = (file.0.checked_sub(1))? as usize;
self.file_table.get(idx)
}
pub fn register_file(&mut self, path: std::path::PathBuf) -> crate::ir::FileId {
if let Some(idx) = self.file_table.iter().position(|p| p == &path) {
return crate::ir::FileId(u32::try_from(idx).unwrap_or(0).saturating_add(1));
}
self.file_table.push(path);
crate::ir::FileId(u32::try_from(self.file_table.len()).unwrap_or(1))
}
#[expect(
clippy::result_large_err,
reason = "CompilerError is large by design; callers push errors into a Vec so allocation is bounded"
)]
pub(crate) fn add_function(
&mut self,
name: String,
f: IrFunction,
) -> Result<FunctionId, CompilerError> {
let id = u32::try_from(self.functions.len())
.map(FunctionId)
.map_err(|_| CompilerError::TooManyDefinitions {
kind: "function",
span: Span::default(),
})?;
self.function_names.insert(name, id);
self.functions.push(f);
Ok(id)
}
pub fn rebuild_indices(&mut self) {
self.struct_names.clear();
for (idx, s) in self.structs.iter().enumerate() {
#[expect(
clippy::cast_possible_truncation,
reason = "checked by add_struct which errors before len reaches u32::MAX"
)]
let prev = self
.struct_names
.insert(s.name.clone(), StructId(idx as u32));
debug_assert!(
prev.is_none(),
"duplicate struct name `{}` in module; rebuild_indices requires unique names",
s.name
);
}
self.trait_names.clear();
for (idx, t) in self.traits.iter().enumerate() {
#[expect(
clippy::cast_possible_truncation,
reason = "checked by add_trait which errors before len reaches u32::MAX"
)]
let prev = self.trait_names.insert(t.name.clone(), TraitId(idx as u32));
debug_assert!(
prev.is_none(),
"duplicate trait name `{}` in module; rebuild_indices requires unique names",
t.name
);
}
self.enum_names.clear();
for (idx, e) in self.enums.iter().enumerate() {
#[expect(
clippy::cast_possible_truncation,
reason = "checked by add_enum which errors before len reaches u32::MAX"
)]
let prev = self.enum_names.insert(e.name.clone(), EnumId(idx as u32));
debug_assert!(
prev.is_none(),
"duplicate enum name `{}` in module; rebuild_indices requires unique names",
e.name
);
}
self.function_names.clear();
for (idx, f) in self.functions.iter().enumerate() {
#[expect(
clippy::cast_possible_truncation,
reason = "checked by add_function which errors before len reaches u32::MAX"
)]
let prev = self
.function_names
.insert(f.name.clone(), FunctionId(idx as u32));
debug_assert!(
prev.is_none() || cfg!(test),
"duplicate function name `{}` in module; rebuild_indices will shadow earlier entries",
f.name
);
}
self.let_names.clear();
for (idx, l) in self.lets.iter().enumerate() {
let prev = self.let_names.insert(l.name.clone(), idx);
debug_assert!(
prev.is_none(),
"duplicate let name `{}` in module; rebuild_indices requires unique names",
l.name
);
}
}
}