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, ResolvedType, 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()
}
#[must_use]
pub fn prelude_array_id(&self) -> Option<StructId> {
self.struct_id("Array")
}
#[must_use]
pub fn prelude_dictionary_id(&self) -> Option<StructId> {
self.struct_id("Dictionary")
}
#[must_use]
pub fn prelude_range_id(&self) -> Option<StructId> {
self.struct_id("Range")
}
#[must_use]
pub fn prelude_optional_id(&self) -> Option<EnumId> {
self.enum_id("Optional")
}
#[must_use]
pub fn is_prelude_struct(&self, id: StructId) -> bool {
Some(id) == self.prelude_array_id()
|| Some(id) == self.prelude_dictionary_id()
|| Some(id) == self.prelude_range_id()
}
#[must_use]
pub fn is_prelude_enum(&self, id: EnumId) -> bool {
Some(id) == self.prelude_optional_id()
}
pub fn user_structs(&self) -> impl Iterator<Item = &IrStruct> {
let array = self.prelude_array_id();
let dict = self.prelude_dictionary_id();
let range = self.prelude_range_id();
self.structs.iter().enumerate().filter_map(move |(i, s)| {
#[expect(
clippy::cast_possible_truncation,
reason = "struct count fits in u32 by construction (add_struct guards the cast)"
)]
let id = StructId(i as u32);
if Some(id) == array || Some(id) == dict || Some(id) == range {
None
} else {
Some(s)
}
})
}
pub fn user_enums(&self) -> impl Iterator<Item = &IrEnum> {
let optional = self.prelude_optional_id();
self.enums.iter().enumerate().filter_map(move |(i, e)| {
#[expect(
clippy::cast_possible_truncation,
reason = "enum count fits in u32 by construction (add_enum guards the cast)"
)]
let id = EnumId(i as u32);
if Some(id) == optional {
None
} else {
Some(e)
}
})
}
#[must_use]
pub fn array_element_ty<'a>(&self, ty: &'a ResolvedType) -> Option<&'a ResolvedType> {
let arr = self.prelude_array_id()?;
if let ResolvedType::Generic {
base: crate::ir::GenericBase::Struct(id),
args,
} = ty
{
if *id == arr && args.len() == 1 {
return args.first();
}
}
None
}
#[must_use]
pub fn dictionary_kv_ty<'a>(
&self,
ty: &'a ResolvedType,
) -> Option<(&'a ResolvedType, &'a ResolvedType)> {
let did = self.prelude_dictionary_id()?;
if let ResolvedType::Generic {
base: crate::ir::GenericBase::Struct(id),
args,
} = ty
{
if *id == did {
if let [k, v] = args.as_slice() {
return Some((k, v));
}
}
}
None
}
#[must_use]
pub fn range_element_ty<'a>(&self, ty: &'a ResolvedType) -> Option<&'a ResolvedType> {
let rid = self.prelude_range_id()?;
if let ResolvedType::Generic {
base: crate::ir::GenericBase::Struct(id),
args,
} = ty
{
if *id == rid && args.len() == 1 {
return args.first();
}
}
None
}
#[must_use]
pub fn optional_inner_ty<'a>(&self, ty: &'a ResolvedType) -> Option<&'a ResolvedType> {
let opt = self.prelude_optional_id()?;
if let ResolvedType::Generic {
base: crate::ir::GenericBase::Enum(id),
args,
} = ty
{
if *id == opt && args.len() == 1 {
return args.first();
}
}
None
}
#[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.entry(name).or_insert(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"
)]
self.function_names
.entry(f.name.clone())
.or_insert(FunctionId(idx as u32));
}
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
);
}
}
}