use std::collections::{BTreeMap, HashSet};
use crate::core::ir::{ApiSurface, ErrorDef};
#[derive(Clone, Copy)]
pub enum ExceptionNaming {
Python,
Nominal,
}
pub struct ExceptionFamily {
pub error: ErrorDef,
pub parent: Option<String>,
}
pub fn validate_exception_hierarchy(api: &ApiSurface) -> anyhow::Result<()> {
let parents = exception_parents(api);
for (child, parent) in &parents {
anyhow::ensure!(
api.errors.iter().any(|error| error.name == *parent),
"unknown exception parent {parent}"
);
ancestors(child, &parents)?;
}
if !parents.is_empty() {
variant_owners(api, ExceptionNaming::Python)?;
variant_owners(api, ExceptionNaming::Nominal)?;
}
for naming in [ExceptionNaming::Python, ExceptionNaming::Nominal] {
for (class, members) in shared_exception_classes(&api.errors, naming) {
shared_introspection(&api.errors, &class, &members)?;
}
}
Ok(())
}
pub fn shared_introspection(
errors: &[ErrorDef],
class: &str,
members: &ClassFamilies,
) -> anyhow::Result<Vec<(crate::core::ir::MethodDef, bool)>> {
let declaring: Vec<&ErrorDef> = errors
.iter()
.filter(|error| members.declared_by.contains(&error.name))
.collect();
let mut union: Vec<(&crate::core::ir::MethodDef, &str)> = Vec::new();
for error in &declaring {
for method in lineage_methods(errors, &error.name) {
match union.iter().find(|(existing, _)| existing.name == method.name) {
Some((existing, first)) => anyhow::ensure!(
existing.return_type == method.return_type,
"incompatible exception introspection {} of shared class {class}: {first} and {} return \
different types",
method.name,
error.name
),
None => union.push((method, &error.name)),
}
}
}
Ok(union
.into_iter()
.map(|(method, _)| {
let everywhere = declaring.iter().all(|error| {
lineage_methods(errors, &error.name)
.iter()
.any(|declared| declared.name == method.name)
});
(method.clone(), !everywhere)
})
.collect())
}
fn ancestors<'a>(name: &'a str, parents: &'a BTreeMap<String, String>) -> anyhow::Result<Vec<&'a str>> {
let mut visited = HashSet::new();
let mut names = Vec::new();
let mut current = name;
loop {
anyhow::ensure!(visited.insert(current), "exception hierarchy cycle at {current}");
names.push(current);
let Some(parent) = parents.get(current) else {
return Ok(names);
};
current = parent;
}
}
pub fn exception_families(api: &ApiSurface, naming: ExceptionNaming) -> anyhow::Result<Vec<ExceptionFamily>> {
validate_exception_hierarchy(api)?;
let parents = exception_parents(api);
if parents.is_empty() {
return Ok(api
.errors
.iter()
.cloned()
.map(|error| ExceptionFamily { error, parent: None })
.collect());
}
let owners = variant_owners(api, naming)?;
let mut errors = api
.errors
.iter()
.map(|error| Ok((ancestors(&error.name, &parents)?.len(), error.clone())))
.collect::<anyhow::Result<Vec<_>>>()?;
errors.sort_by_key(|(depth, _)| *depth);
Ok(errors
.into_iter()
.map(|(_, mut error)| {
error
.variants
.retain(|variant| owners[&class_name(&variant.name, &error.name, naming)] == error.name);
ExceptionFamily {
parent: parents.get(&error.name).cloned(),
error,
}
})
.collect())
}
fn class_name(variant: &str, error: &str, naming: ExceptionNaming) -> String {
if matches!(naming, ExceptionNaming::Python) {
super::python_exception_name(variant, error)
} else {
format!("{variant}Exception")
}
}
fn variant_owners(api: &ApiSurface, naming: ExceptionNaming) -> anyhow::Result<BTreeMap<String, String>> {
let families = class_families(&api.errors, naming);
for (class, members) in &families {
let owner = &members.families[0];
let owner_error = api
.errors
.iter()
.find(|error| error.name == *owner)
.ok_or_else(|| anyhow::anyhow!("missing exception owner {owner} for shared class {class}"))?;
let layout = |error: &ErrorDef| {
error
.methods
.iter()
.map(|method| (method.name.clone(), method.return_type.clone()))
.collect::<Vec<_>>()
};
for error in api
.errors
.iter()
.filter(|error| members.declared_by.contains(&error.name))
.filter(|error| same_exception_lineage(&api.errors, &error.name, owner))
{
anyhow::ensure!(
layout(error) == layout(owner_error),
"incompatible exception introspection for {class} between {owner} and {}",
error.name
);
}
}
Ok(families
.into_iter()
.map(|(class, members)| (class, members.families[0].clone()))
.collect())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClassFamilies {
pub declared_by: Vec<String>,
pub families: Vec<String>,
}
fn class_families(errors: &[ErrorDef], naming: ExceptionNaming) -> BTreeMap<String, ClassFamilies> {
let parents = exception_parents_of(errors);
let mut declared: BTreeMap<String, Vec<String>> = BTreeMap::new();
for error in errors {
for variant in &error.variants {
let owners = declared
.entry(class_name(&variant.name, &error.name, naming))
.or_default();
if !owners.contains(&error.name) {
owners.push(error.name.clone());
}
}
}
declared
.into_iter()
.map(|(class, declared_by)| {
let families = declared_by
.iter()
.filter(|family| {
!declared_by
.iter()
.any(|other| other != *family && lineage(other, &parents).contains(&family.as_str()))
})
.cloned()
.collect();
(class, ClassFamilies { declared_by, families })
})
.collect()
}
pub fn shared_exception_classes(errors: &[ErrorDef], naming: ExceptionNaming) -> BTreeMap<String, ClassFamilies> {
class_families(errors, naming)
.into_iter()
.filter(|(_, members)| members.families.len() > 1)
.collect()
}
pub fn additional_exception_families(
errors: &[ErrorDef],
error: &str,
variant: &str,
naming: ExceptionNaming,
) -> Vec<String> {
let class = class_name(variant, error, naming);
shared_exception_classes(errors, naming)
.remove(&class)
.map(|members| members.families.into_iter().skip(1).collect())
.unwrap_or_default()
}
fn lineage<'a>(name: &'a str, parents: &'a BTreeMap<String, String>) -> Vec<&'a str> {
let mut names = Vec::new();
let mut current = name;
while !names.contains(¤t) {
names.push(current);
match parents.get(current) {
Some(parent) => current = parent,
None => break,
}
}
names
}
pub fn validate_family_interface_names(
api: &ApiSurface,
language: &str,
interface_name: impl Fn(&str) -> String,
) -> anyhow::Result<()> {
let mut taken: Vec<(String, String)> = Vec::new();
for typ in &api.types {
taken.push((typ.name.clone(), format!("type {}", typ.name)));
if typ.is_trait {
taken.push((format!("I{}", typ.name), format!("interface of trait {}", typ.name)));
taken.push((
format!("{}Interface", typ.name),
format!("interface of trait {}", typ.name),
));
}
}
for enum_def in &api.enums {
taken.push((enum_def.name.clone(), format!("enum {}", enum_def.name)));
}
for error in &api.errors {
taken.push((error.name.clone(), format!("error {}", error.name)));
taken.push((
format!("{}Exception", error.name),
format!("exception class of {}", error.name),
));
for variant in &error.variants {
for class in [
class_name(&variant.name, &error.name, ExceptionNaming::Python),
class_name(&variant.name, &error.name, ExceptionNaming::Nominal),
] {
taken.push((class, format!("exception class of {}::{}", error.name, variant.name)));
}
}
}
for error in &api.errors {
let name = interface_name(&error.name);
if let Some((_, owner)) = taken.iter().find(|(taken_name, _)| *taken_name == name) {
anyhow::bail!(
"{language} marker interface {name} of error family {} collides with the generated {owner}",
error.name
);
}
}
Ok(())
}
pub fn lineage_methods<'a>(errors: &'a [ErrorDef], family: &str) -> Vec<&'a crate::core::ir::MethodDef> {
let mut methods: Vec<&crate::core::ir::MethodDef> = Vec::new();
for member in exception_lineage(errors, family) {
for method in errors
.iter()
.filter(|error| error.name == member)
.flat_map(|error| &error.methods)
{
if methods.iter().all(|existing| existing.name != method.name) {
methods.push(method);
}
}
}
methods
}
pub fn exception_lineage(errors: &[ErrorDef], family: &str) -> Vec<String> {
let parents = exception_parents_of(errors);
lineage(family, &parents).into_iter().map(str::to_string).collect()
}
pub fn same_exception_lineage(errors: &[ErrorDef], a: &str, b: &str) -> bool {
let parents = exception_parents_of(errors);
lineage(a, &parents).contains(&b) || lineage(b, &parents).contains(&a)
}
fn exception_parents(api: &ApiSurface) -> BTreeMap<String, String> {
exception_parents_of(&api.errors)
}
fn exception_parents_of(errors: &[ErrorDef]) -> BTreeMap<String, String> {
errors
.iter()
.filter_map(|error| {
error
.exception_base
.as_ref()
.map(|parent| (error.name.clone(), parent.clone()))
})
.collect()
}