use crate::model::Ast;
use crate::node::AnyId;
use crate::node::type_::TypeKind;
use alloc::collections::BTreeSet;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;
use zerodds_ast_core::NodeId;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InvariantViolation {
DanglingReference {
from: NodeId,
detail: &'static str,
},
NonPositiveArrayDimension {
node: NodeId,
value: i128,
},
IncompatibleCaseLabel {
case: NodeId,
discriminator: NodeId,
detail: &'static str,
},
DuplicateMemberName {
aggregate: NodeId,
name: String,
},
DuplicateEnumeratorName {
type_: NodeId,
name: String,
},
DuplicateMemberId {
aggregate: NodeId,
id: u32,
first: NodeId,
second: NodeId,
},
MultipleDefaultCases {
union: NodeId,
},
NameCollidesOnlyInCase {
scope: NodeId,
first: String,
second: String,
},
}
impl fmt::Display for InvariantViolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::DanglingReference { from, detail } => {
write!(f, "node {from:?} has a dangling reference: {detail}")
}
Self::NonPositiveArrayDimension { node, value } => {
write!(
f,
"array {node:?} has dimension {value}, which must be positive"
)
}
Self::IncompatibleCaseLabel {
case,
discriminator,
detail,
} => write!(
f,
"case {case:?} does not fit discriminator {discriminator:?}: {detail}"
),
Self::DuplicateMemberName { aggregate, name } => {
write!(f, "aggregate {aggregate:?} declares `{name}` twice")
}
Self::DuplicateEnumeratorName { type_, name } => {
write!(f, "type {type_:?} declares `{name}` twice")
}
Self::DuplicateMemberId {
aggregate,
id,
first,
second,
} => write!(
f,
"aggregate {aggregate:?}: members {first:?} and {second:?} both claim id {id}"
),
Self::MultipleDefaultCases { union } => {
write!(f, "union {union:?} declares more than one default case")
}
Self::NameCollidesOnlyInCase {
scope,
first,
second,
} => write!(
f,
"scope {scope:?} declares `{first}` and `{second}`, which differ only in case"
),
}
}
}
impl core::error::Error for InvariantViolation {}
pub fn check(ast: &Ast, touched: &[NodeId]) -> Vec<InvariantViolation> {
let mut out = Vec::new();
let mut seen: BTreeSet<NodeId> = BTreeSet::new();
for node in touched {
if !seen.insert(*node) {
continue;
}
let Some(any) = ast.resolve_node_id(*node) else {
continue;
};
let subject = match any {
AnyId::Type(id, _) => Some(id),
AnyId::Member(id, _) => ast.member(id).ok().and_then(|m| m.parent.as_type()),
AnyId::Case(id, _) => ast.case(id).ok().map(|c| c.parent),
AnyId::Enumerator(id, _) => ast.enumerator(id).ok().map(|e| e.parent),
AnyId::BitValue(id, _) => ast.bit_value(id).ok().map(|b| b.parent),
AnyId::Bitfield(id, _) => ast.bitfield(id).ok().map(|b| b.parent),
_ => None,
};
if let Some(type_id) = subject {
if let Ok(node) = ast.type_(type_id) {
let type_node_id = node.node_id;
if seen.insert(type_node_id) || any.kind() == crate::NodeKind::Type {
check_type(ast, type_id, type_node_id, &mut out);
}
}
}
}
out.dedup();
out
}
fn check_type(
ast: &Ast,
id: zerodds_ast_core::Id<crate::TypeNode>,
node_id: NodeId,
out: &mut Vec<InvariantViolation>,
) {
let Ok(node) = ast.type_(id) else {
return;
};
for reference in node.kind.type_refs() {
if let Some(target) = reference.resolved() {
if ast.type_(target).is_err() {
out.push(InvariantViolation::DanglingReference {
from: node_id,
detail: "referenced type is not live",
});
}
}
}
match &node.kind {
TypeKind::Array { dims, .. } => {
for dim in dims {
if let Ok(value) = ast.eval_integral(*dim) {
if value <= 0 {
out.push(InvariantViolation::NonPositiveArrayDimension {
node: node_id,
value,
});
}
}
}
}
TypeKind::Structure { members, .. } | TypeKind::Valuetype { state: members, .. } => {
check_member_names(ast, node_id, &node.name, members, out);
check_member_ids(ast, node_id, members, out);
}
TypeKind::Enumeration { enumerators } => {
let mut names: Vec<&str> = Vec::new();
for enumerator in enumerators {
let Ok(e) = ast.enumerator(*enumerator) else {
continue;
};
if names.contains(&e.name.as_str()) {
out.push(InvariantViolation::DuplicateEnumeratorName {
type_: node_id,
name: e.name.clone(),
});
} else {
names.push(&e.name);
}
}
}
TypeKind::Bitmask { bits } => {
let mut names: Vec<&str> = Vec::new();
for bit in bits {
let Ok(b) = ast.bit_value(*bit) else {
continue;
};
if names.contains(&b.name.as_str()) {
out.push(InvariantViolation::DuplicateEnumeratorName {
type_: node_id,
name: b.name.clone(),
});
} else {
names.push(&b.name);
}
}
}
TypeKind::Union {
discriminator,
cases,
..
} => {
let mut defaults = 0usize;
for case in cases {
let Ok(c) = ast.case(*case) else {
continue;
};
defaults += c.labels.iter().filter(|l| l.is_default()).count();
}
if defaults > 1 {
out.push(InvariantViolation::MultipleDefaultCases { union: node_id });
}
let mut elements = Vec::with_capacity(cases.len());
for case in cases {
let Ok(c) = ast.case(*case) else {
continue;
};
elements.push(c.member);
}
check_member_names(ast, node_id, &node.name, &elements, out);
if let Some(disc) = discriminator.resolved() {
check_case_labels(ast, node_id, disc, cases, out);
}
}
_ => {}
}
}
fn check_member_names(
ast: &Ast,
aggregate: NodeId,
aggregate_name: &str,
members: &[zerodds_ast_core::Id<crate::MemberNode>],
out: &mut Vec<InvariantViolation>,
) {
let mut names: Vec<&str> = Vec::new();
names.push(aggregate_name);
for member in members {
let Ok(m) = ast.member(*member) else {
continue;
};
if names.contains(&m.name.as_str()) {
out.push(InvariantViolation::DuplicateMemberName {
aggregate,
name: m.name.clone(),
});
} else if let Some(first) = names.iter().find(|held| held.eq_ignore_ascii_case(&m.name)) {
out.push(InvariantViolation::NameCollidesOnlyInCase {
scope: aggregate,
first: String::from(*first),
second: m.name.clone(),
});
} else {
names.push(&m.name);
}
}
}
fn check_member_ids(
ast: &Ast,
aggregate: NodeId,
members: &[zerodds_ast_core::Id<crate::MemberNode>],
out: &mut Vec<InvariantViolation>,
) {
let mut assigned: Vec<(u32, NodeId)> = Vec::new();
for member in members {
let Ok(m) = ast.member(*member) else {
continue;
};
let Some(id) = m.facets.effective_member_id() else {
continue;
};
if let Some((_, first)) = assigned.iter().find(|(claimed, _)| *claimed == id) {
out.push(InvariantViolation::DuplicateMemberId {
aggregate,
id,
first: *first,
second: m.node_id,
});
} else {
assigned.push((id, m.node_id));
}
}
}
fn resolve_aliases<'a>(ast: &'a Ast, start: &'a crate::TypeNode) -> Option<&'a crate::TypeNode> {
const MAX_ALIAS_DEPTH: usize = 64;
let mut node = start;
for _ in 0..MAX_ALIAS_DEPTH {
let TypeKind::Alias { aliased } = &node.kind else {
return Some(node);
};
node = ast.type_(aliased.resolved()?).ok()?;
}
None
}
fn check_case_labels(
ast: &Ast,
union: NodeId,
discriminator: zerodds_ast_core::Id<crate::TypeNode>,
cases: &[zerodds_ast_core::Id<crate::CaseNode>],
out: &mut Vec<InvariantViolation>,
) {
let Ok(disc) = ast.type_(discriminator) else {
return;
};
let Some(resolved) = resolve_aliases(ast, disc) else {
return;
};
let integral = matches!(
&resolved.kind,
TypeKind::Primitive(p) if p.is_integral()
|| matches!(
p,
crate::PrimitiveKind::Boolean
| crate::PrimitiveKind::Char
| crate::PrimitiveKind::WChar
)
) || matches!(
&resolved.kind,
TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. }
);
if !integral {
for case in cases {
let Ok(c) = ast.case(*case) else {
continue;
};
if c.labels.iter().any(|l| !l.is_default()) {
out.push(InvariantViolation::IncompatibleCaseLabel {
case: c.node_id,
discriminator: disc.node_id,
detail: "discriminator is neither integral, boolean, character nor an enum",
});
}
}
return;
}
for case in cases {
let Ok(c) = ast.case(*case) else {
continue;
};
for label in &c.labels {
let Some(expr) = label.expr() else {
continue;
};
if let Err(error) = ast.eval_integral(expr) {
if !matches!(error, crate::EvalError::Unresolved { .. }) {
out.push(InvariantViolation::IncompatibleCaseLabel {
case: c.node_id,
discriminator: disc.node_id,
detail: "label does not evaluate to an integral value",
});
}
}
}
}
let _ = union;
}