use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OperationTrace {
touched_blocks: BlockSet,
claimed_write_blocks: BlockSet,
recorded_topology_edit: TopologyEditKind,
remapped_blocks: BlockSet,
preserved_cache: DerivedState,
read_cache: DerivedState,
cleared_cache: DerivedState,
updated_cache: DerivedState,
outcome: Option<OpOutcome>,
}
impl OperationTrace {
#[must_use]
pub const fn touched_blocks(&self) -> BlockSet {
self.touched_blocks
}
#[must_use]
pub const fn read_cache(&self) -> DerivedState {
self.read_cache
}
#[must_use]
pub const fn remapped_blocks(&self) -> BlockSet {
self.remapped_blocks
}
#[must_use]
pub const fn preserved_cache(&self) -> DerivedState {
self.preserved_cache
}
#[must_use]
pub const fn cleared_cache(&self) -> DerivedState {
self.cleared_cache
}
#[must_use]
pub const fn updated_cache(&self) -> DerivedState {
self.updated_cache
}
#[must_use]
pub fn outcome(&self) -> Option<&OpOutcome> {
self.outcome.as_ref()
}
}
#[cfg(feature = "op-contracts")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BlockLifecycle {
Available,
Begun,
Committed,
}
#[cfg(feature = "op-contracts")]
#[derive(Debug, Clone, PartialEq, Eq)]
struct ContractSourceSnapshot {
atom_count: usize,
bond_endpoints: Vec<(AtomId, AtomId)>,
}
#[cfg(feature = "op-contracts")]
impl ContractSourceSnapshot {
fn from_molecule(molecule: &Molecule) -> Self {
Self {
atom_count: molecule.num_atoms(),
bond_endpoints: molecule
.bonds()
.iter()
.map(|bond| (bond.begin(), bond.end()))
.collect(),
}
}
const fn num_atoms(&self) -> usize {
self.atom_count
}
fn num_bonds(&self) -> usize {
self.bond_endpoints.len()
}
fn bond_endpoint(&self, index: usize) -> Option<(AtomId, AtomId)> {
self.bond_endpoints.get(index).copied()
}
}
#[cfg(feature = "op-contracts")]
enum ContractSource<'a> {
Borrowed(&'a Molecule),
Snapshot(ContractSourceSnapshot),
}
#[cfg(feature = "op-contracts")]
impl ContractSource<'_> {
fn num_atoms(&self) -> usize {
match self {
Self::Borrowed(molecule) => molecule.num_atoms(),
Self::Snapshot(snapshot) => snapshot.num_atoms(),
}
}
fn num_bonds(&self) -> usize {
match self {
Self::Borrowed(molecule) => molecule.num_bonds(),
Self::Snapshot(snapshot) => snapshot.num_bonds(),
}
}
fn bond_endpoint(&self, index: usize) -> Option<(AtomId, AtomId)> {
match self {
Self::Borrowed(molecule) => molecule
.bonds()
.get(index)
.map(|bond| (bond.begin(), bond.end())),
Self::Snapshot(snapshot) => snapshot.bond_endpoint(index),
}
}
}
pub struct OpParts<'a> {
spec: &'static MoleculeOpSpec,
#[cfg(feature = "op-contracts")]
contract_source: ContractSource<'a>,
working: Molecule,
in_place_target: Option<&'a mut Molecule>,
topology_mapping: Option<TopologyMapping>,
#[cfg(feature = "op-contracts")]
topology_lifecycle: BlockLifecycle,
#[cfg(feature = "op-contracts")]
coordinates_lifecycle: BlockLifecycle,
#[cfg(feature = "op-contracts")]
properties_lifecycle: BlockLifecycle,
#[cfg(feature = "op-contracts")]
trace: OperationTrace,
}
impl<'a> OpParts<'a> {
pub(crate) fn new(
source: &'a Molecule,
spec: &'static MoleculeOpSpec,
) -> Result<Self, OperationError> {
validate_semantic_preconditions(source, spec)?;
Ok(Self {
spec,
#[cfg(feature = "op-contracts")]
contract_source: ContractSource::Borrowed(source),
working: source.clone(),
in_place_target: None,
topology_mapping: None,
#[cfg(feature = "op-contracts")]
topology_lifecycle: BlockLifecycle::Available,
#[cfg(feature = "op-contracts")]
coordinates_lifecycle: BlockLifecycle::Available,
#[cfg(feature = "op-contracts")]
properties_lifecycle: BlockLifecycle::Available,
#[cfg(feature = "op-contracts")]
trace: OperationTrace {
touched_blocks: BlockSet::NONE,
claimed_write_blocks: BlockSet::NONE,
recorded_topology_edit: TopologyEditKind::None,
remapped_blocks: BlockSet::NONE,
preserved_cache: DerivedState::NONE,
read_cache: DerivedState::NONE,
cleared_cache: DerivedState::NONE,
updated_cache: DerivedState::NONE,
outcome: None,
},
})
}
pub(crate) fn new_in_place(
target: &'a mut Molecule,
spec: &'static MoleculeOpSpec,
) -> Result<Self, OperationError> {
validate_semantic_preconditions(target, spec)?;
#[cfg(feature = "op-contracts")]
let contract_source =
ContractSource::Snapshot(ContractSourceSnapshot::from_molecule(target));
let working = std::mem::take(target);
Ok(Self {
spec,
#[cfg(feature = "op-contracts")]
contract_source,
working,
in_place_target: Some(target),
topology_mapping: None,
#[cfg(feature = "op-contracts")]
topology_lifecycle: BlockLifecycle::Available,
#[cfg(feature = "op-contracts")]
coordinates_lifecycle: BlockLifecycle::Available,
#[cfg(feature = "op-contracts")]
properties_lifecycle: BlockLifecycle::Available,
#[cfg(feature = "op-contracts")]
trace: OperationTrace {
touched_blocks: BlockSet::NONE,
claimed_write_blocks: BlockSet::NONE,
recorded_topology_edit: TopologyEditKind::None,
remapped_blocks: BlockSet::NONE,
preserved_cache: DerivedState::NONE,
read_cache: DerivedState::NONE,
cleared_cache: DerivedState::NONE,
updated_cache: DerivedState::NONE,
outcome: None,
},
})
}
pub(crate) fn begin_topology_read(&self) -> Result<MoleculeReadParts<'_>, OperationError> {
self.validate_access_spec()?;
#[cfg(feature = "op-contracts")]
{
if !self.spec.access.read().contains(BlockSet::TOPOLOGY)
|| self.spec.access.write().contains(BlockSet::TOPOLOGY)
{
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation attempted to read topology outside its registry read access",
});
}
}
Ok(MoleculeReadParts::from_molecule(&self.working))
}
pub(crate) fn with_topology_read_parts<R>(
&self,
topology: TopologyBlock,
f: impl FnOnce(MoleculeReadParts<'_>) -> Result<R, OperationError>,
) -> Result<R, OperationError> {
let view = self.read_parts_for_topology(topology)?;
f(MoleculeReadParts::from_molecule(&view))
}
pub(crate) fn with_block_read_parts<R>(
&self,
topology: TopologyBlock,
coordinates: CoordinateBlock,
properties: MoleculeProperties,
f: impl FnOnce(MoleculeReadParts<'_>) -> Result<R, OperationError>,
) -> Result<R, OperationError> {
let view = self.read_parts_for_blocks(topology, coordinates, properties)?;
f(MoleculeReadParts::from_molecule(&view))
}
pub(crate) fn with_optional_block_read_parts<R>(
&self,
topology: TopologyBlock,
coordinates: Option<&CoordinateBlock>,
properties: Option<&MoleculeProperties>,
f: impl FnOnce(MoleculeReadParts<'_>) -> Result<R, OperationError>,
) -> Result<R, OperationError> {
let view = self.read_parts_for_optional_blocks(topology, coordinates, properties)?;
f(MoleculeReadParts::from_molecule(&view))
}
fn read_parts_for_topology(&self, topology: TopologyBlock) -> Result<Molecule, OperationError> {
self.read_parts_for_blocks(
topology,
self.working.coordinate_block().clone(),
self.working.properties().clone(),
)
}
fn read_parts_for_blocks(
&self,
topology: TopologyBlock,
coordinates: CoordinateBlock,
properties: MoleculeProperties,
) -> Result<Molecule, OperationError> {
Molecule::from_operation_blocks(
topology,
coordinates,
properties,
self.working.derived_cache().clone(),
self.working.capabilities_block(),
)
.map_err(|failure| OperationError::InvariantViolation {
operation: self.spec,
failure,
})
}
fn read_parts_for_optional_blocks(
&self,
topology: TopologyBlock,
coordinates: Option<&CoordinateBlock>,
properties: Option<&MoleculeProperties>,
) -> Result<Molecule, OperationError> {
self.read_parts_for_blocks(
topology,
coordinates
.cloned()
.unwrap_or_else(|| self.working.coordinate_block().clone()),
properties
.cloned()
.unwrap_or_else(|| self.working.properties().clone()),
)
}
pub(crate) fn begin_topology_mut(&mut self) -> Result<TopologyBlock, OperationError> {
self.begin_block_mut(BlockSet::TOPOLOGY)?;
#[cfg(feature = "op-contracts")]
{
self.topology_lifecycle = BlockLifecycle::Begun;
}
Ok(if self.in_place_target.is_some() {
self.working.take_topology_block_or_clone()
} else {
self.working.topology_block().clone()
})
}
pub(crate) fn commit_topology(
&mut self,
mut topology: TopologyBlock,
) -> Result<(), OperationError> {
#[cfg(feature = "op-contracts")]
{
if self.topology_lifecycle != BlockLifecycle::Begun {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "topology block was not begun before commit",
});
}
}
topology.adjacency =
crate::AdjacencyList::from_topology(topology.atoms.len(), &topology.bonds);
self.record_mutation(BlockSet::TOPOLOGY);
self.working.replace_topology_block(topology);
#[cfg(feature = "op-contracts")]
{
self.topology_lifecycle = BlockLifecycle::Committed;
}
Ok(())
}
pub(crate) fn begin_coordinates_mut(&mut self) -> Result<CoordinateBlock, OperationError> {
self.begin_block_mut(BlockSet::COORDINATES)?;
#[cfg(feature = "op-contracts")]
{
self.coordinates_lifecycle = BlockLifecycle::Begun;
}
Ok(if self.in_place_target.is_some() {
self.working.take_coordinate_block_or_clone()
} else {
self.working.coordinate_block().clone()
})
}
pub(crate) fn commit_coordinates(
&mut self,
coordinates: CoordinateBlock,
) -> Result<(), OperationError> {
#[cfg(feature = "op-contracts")]
{
if self.coordinates_lifecycle != BlockLifecycle::Begun {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "coordinate block was not begun before commit",
});
}
}
self.record_mutation(BlockSet::COORDINATES);
self.working.replace_coordinate_block(coordinates);
#[cfg(feature = "op-contracts")]
{
self.coordinates_lifecycle = BlockLifecycle::Committed;
}
Ok(())
}
pub(crate) fn begin_properties_mut(&mut self) -> Result<MoleculeProperties, OperationError> {
self.begin_block_mut(BlockSet::PROPERTIES)?;
#[cfg(feature = "op-contracts")]
{
self.properties_lifecycle = BlockLifecycle::Begun;
}
Ok(if self.in_place_target.is_some() {
self.working.take_properties_or_clone()
} else {
self.working.properties().clone()
})
}
pub(crate) fn commit_properties(
&mut self,
properties: MoleculeProperties,
) -> Result<(), OperationError> {
#[cfg(feature = "op-contracts")]
{
if self.properties_lifecycle != BlockLifecycle::Begun {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "properties block was not begun before commit",
});
}
}
self.record_mutation(BlockSet::PROPERTIES);
self.working.replace_properties(properties);
#[cfg(feature = "op-contracts")]
{
self.properties_lifecycle = BlockLifecycle::Committed;
}
Ok(())
}
pub(crate) fn record_topology_edit(
&mut self,
kind: TopologyEditKind,
) -> Result<(), OperationError> {
#[cfg(feature = "op-contracts")]
{
if kind == TopologyEditKind::Local
&& matches!(
self.spec.topology_edit,
TopologyEditKind::Appending
| TopologyEditKind::Compacting
| TopologyEditKind::Renumbering
| TopologyEditKind::Merge
)
{
return Ok(());
}
if self.spec.topology_edit != kind {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "recorded topology edit does not match registry declaration",
});
}
self.trace.recorded_topology_edit = kind;
}
#[cfg(not(feature = "op-contracts"))]
{
let _ = kind;
}
Ok(())
}
pub(crate) fn record_topology_mapping(&mut self, mapping: TopologyMapping) {
self.topology_mapping = Some(mapping);
self.record_remapped(self.spec.auto_remap);
}
#[cfg(feature = "op-contracts")]
fn check_cache_write_permission(&self, state: DerivedState) {
let effects = self.spec.derived_effects;
let allowed = effects.recompute();
if !allowed.contains(state) {
panic!(
"cache write permission violation: operation `{}` attempted to write \
derived state `{:?}` but only has `recompute({:?})` \
permissions",
self.spec.method,
state,
effects.recompute(),
);
}
}
#[cfg(feature = "op-contracts")]
fn check_cache_clear_permission(&self, states: DerivedState) {
let effects = self.spec.derived_effects;
let allowed = effects.invalidate().union(effects.recompute());
let forbidden = states.bits() & !allowed.bits();
if forbidden != 0 {
panic!(
"cache clear permission violation: operation `{}` attempted to clear \
derived state bits `{:#010b}` but only has `invalidate({:?})` and `recompute({:?})` \
permissions",
self.spec.method,
forbidden,
effects.invalidate(),
effects.recompute(),
);
}
}
#[cfg(feature = "op-contracts")]
fn check_cache_read_permission(&self, state: DerivedState) {
let effects = self.spec.derived_effects;
let allowed = effects.preserve();
if !allowed.contains(state) {
panic!(
"cache read permission violation: operation `{}` attempted to read \
derived state `{:?}` but only has `preserve({:?})` \
permissions",
self.spec.method,
state,
effects.preserve(),
);
}
}
pub(crate) fn set_rings_cache(&mut self, rings: crate::RingInfo) {
#[cfg(feature = "op-contracts")]
self.check_cache_write_permission(DerivedState::RINGS);
self.record_mutation(BlockSet::DERIVED_CACHE);
self.working.derived_cache_mut().rings = Some(rings);
self.record_updated_cache(DerivedState::RINGS);
}
pub(crate) fn set_ring_families_cache(&mut self, ring_families: crate::RingInfo) {
#[cfg(feature = "op-contracts")]
self.check_cache_write_permission(DerivedState::RING_FAMILIES);
self.record_mutation(BlockSet::DERIVED_CACHE);
self.working.derived_cache_mut().ring_families = Some(ring_families);
self.record_updated_cache(DerivedState::RING_FAMILIES);
}
pub(crate) fn set_valence_cache(&mut self, valence: crate::ValenceAssignment) {
#[cfg(feature = "op-contracts")]
self.check_cache_write_permission(DerivedState::VALENCE);
self.record_mutation(BlockSet::DERIVED_CACHE);
self.working.derived_cache_mut().valence = Some(valence);
self.record_updated_cache(DerivedState::VALENCE);
}
pub(crate) fn mark_aromaticity_valid(&mut self) {
#[cfg(feature = "op-contracts")]
self.check_cache_write_permission(DerivedState::AROMATICITY);
self.record_mutation(BlockSet::DERIVED_CACHE);
self.working.derived_cache_mut().aromaticity_valid = true;
self.record_updated_cache(DerivedState::AROMATICITY);
}
#[allow(dead_code)]
pub(crate) fn mark_stereo_handled(&mut self) {
#[cfg(feature = "op-contracts")]
self.check_cache_write_permission(DerivedState::STEREO);
self.record_mutation(BlockSet::DERIVED_CACHE);
self.working.derived_cache_mut().stereo_valid = true;
self.record_updated_cache(DerivedState::STEREO);
}
pub(crate) fn clear_cache(&mut self, states: DerivedState) {
#[cfg(feature = "op-contracts")]
{
self.check_cache_clear_permission(states);
self.trace.cleared_cache |= states;
}
if states.touches_cache() {
self.record_mutation(BlockSet::DERIVED_CACHE);
self.working.derived_cache_mut().invalidate(states);
}
#[cfg(not(feature = "op-contracts"))]
{
let _ = states;
}
}
pub(crate) fn prove_preserved(
&mut self,
states: DerivedState,
proof: PreservationProof,
) -> Result<(), OperationError> {
#[cfg(feature = "op-contracts")]
{
if !self.spec.derived_effects.preserve().contains(states) {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation attempted to prove preservation for undeclared derived states",
});
}
match proof {
PreservationProof::LeafAtomAppend => {
self.validate_leaf_atom_append_preservation()?
}
}
self.trace.preserved_cache |= states;
}
#[cfg(not(feature = "op-contracts"))]
{
let _ = states;
let _ = proof;
}
Ok(())
}
pub(crate) fn finish(self, outcome: OpOutcome) -> Result<Molecule, OperationError> {
debug_assert!(self.in_place_target.is_none());
#[cfg(feature = "op-contracts")]
{
let mut this = self;
this.trace.outcome = Some(outcome);
this.validate_contract()?;
enforce_molecule_invariants(&this.working).map_err(|failure| {
OperationError::InvariantViolation {
operation: this.spec,
failure,
}
})?;
Ok(this.working)
}
#[cfg(not(feature = "op-contracts"))]
{
let _ = outcome;
enforce_molecule_invariants(&self.working).map_err(|failure| {
OperationError::InvariantViolation {
operation: self.spec,
failure,
}
})?;
Ok(self.working)
}
}
pub(crate) fn abort_in_place(mut self) {
let Some(target) = self.in_place_target.take() else {
return;
};
*target = self.working;
}
pub(crate) fn finish_in_place(self, outcome: OpOutcome) -> Result<(), OperationError> {
#[cfg(feature = "op-contracts")]
{
let mut this = self;
this.trace.outcome = Some(outcome);
let validation = this.validate_contract().and_then(|()| {
enforce_molecule_invariants(&this.working).map_err(|failure| {
OperationError::InvariantViolation {
operation: this.spec,
failure,
}
})
});
let target = this
.in_place_target
.take()
.ok_or(OperationError::InvalidInput {
operation: this.spec,
message: "in-place operation was finished without an in-place target",
})?;
*target = this.working;
validation
}
#[cfg(not(feature = "op-contracts"))]
{
let mut this = self;
let _ = outcome;
let validation = enforce_molecule_invariants(&this.working).map_err(|failure| {
OperationError::InvariantViolation {
operation: this.spec,
failure,
}
});
let target = this
.in_place_target
.take()
.ok_or(OperationError::InvalidInput {
operation: this.spec,
message: "in-place operation was finished without an in-place target",
})?;
*target = this.working;
validation
}
}
fn record_mutation(&mut self, block: BlockSet) {
#[cfg(feature = "op-contracts")]
{
assert!(
self.spec.access.can_write(block) && self.spec.may_mutate.contains(block),
"operation `{}` attempted to mutate a block outside its registry permissions",
self.spec.method
);
self.trace.touched_blocks = self.trace.touched_blocks.union(block);
}
#[cfg(not(feature = "op-contracts"))]
{
let _ = block;
}
}
#[cfg(feature = "op-contracts")]
fn validate_access_spec(&self) -> Result<(), OperationError> {
if self.spec.access.has_overlapping_read_write() {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation access declares the same block as both read and write",
});
}
if self.spec.access.write() != self.spec.may_mutate {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation access write set must match may_mutate",
});
}
Ok(())
}
#[cfg(not(feature = "op-contracts"))]
fn validate_access_spec(&self) -> Result<(), OperationError> {
Ok(())
}
fn begin_block_mut(&mut self, block: BlockSet) -> Result<(), OperationError> {
self.validate_access_spec()?;
#[cfg(feature = "op-contracts")]
{
if !self.spec.access.can_write(block) {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation attempted to write a block outside its registry access",
});
}
let lifecycle = if block == BlockSet::TOPOLOGY {
self.topology_lifecycle
} else if block == BlockSet::COORDINATES {
self.coordinates_lifecycle
} else if block == BlockSet::PROPERTIES {
self.properties_lifecycle
} else {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation attempted to begin an unknown block",
});
};
if lifecycle != BlockLifecycle::Available {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation attempted to begin the same writable block twice",
});
}
self.trace.claimed_write_blocks = self.trace.claimed_write_blocks.union(block);
}
#[cfg(not(feature = "op-contracts"))]
{
let _ = block;
}
Ok(())
}
fn record_updated_cache(&mut self, state: DerivedState) {
#[cfg(feature = "op-contracts")]
{
self.trace.updated_cache = self.trace.updated_cache.union(state);
}
#[cfg(not(feature = "op-contracts"))]
{
let _ = state;
}
}
#[cfg(feature = "op-contracts")]
fn validate_leaf_atom_append_preservation(&self) -> Result<(), OperationError> {
if self.spec.topology_edit != TopologyEditKind::Appending {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof requires an appending topology operation",
});
}
let Some(mapping) = &self.topology_mapping else {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof requires a topology mapping",
});
};
let old_atom_count = self.contract_source.num_atoms();
let old_bond_count = self.contract_source.num_bonds();
if mapping.atoms().old_to_new().len() != old_atom_count
|| mapping.bonds().old_to_new().len() != old_bond_count
|| mapping.atoms().new_to_old().len() != self.working.num_atoms()
|| mapping.bonds().new_to_old().len() != self.working.num_bonds()
{
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof has inconsistent mapping dimensions",
});
}
for (old_idx, mapped) in mapping.atoms().old_to_new().iter().enumerate() {
if *mapped != Some(AtomId::new(old_idx)) {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof requires identity mapping for old atoms",
});
}
}
for (old_idx, mapped) in mapping.bonds().old_to_new().iter().enumerate() {
if *mapped != Some(crate::BondId::new(old_idx)) {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof requires identity mapping for old bonds",
});
}
}
for old_idx in 0..old_bond_count {
let Some((before_begin, before_end)) = self.contract_source.bond_endpoint(old_idx)
else {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof found missing source bond endpoint",
});
};
let after = &self.working.bonds()[old_idx];
if before_begin != after.begin() || before_end != after.end() {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof detected changed old bond endpoints",
});
}
}
let mut appended_degrees =
vec![0usize; self.working.num_atoms().saturating_sub(old_atom_count)];
for bond in &self.working.bonds()[old_bond_count..] {
let begin_old = bond.begin().index() < old_atom_count;
let end_old = bond.end().index() < old_atom_count;
if begin_old == end_old {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof requires every appended bond to connect one old atom and one appended atom",
});
}
let appended_idx = if begin_old {
bond.end().index() - old_atom_count
} else {
bond.begin().index() - old_atom_count
};
if appended_idx >= appended_degrees.len() {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof found appended bond referencing an out-of-range atom",
});
}
appended_degrees[appended_idx] += 1;
}
if appended_degrees.iter().any(|degree| *degree != 1) {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "leaf-append preservation proof requires every appended atom to be a degree-one leaf",
});
}
Ok(())
}
#[cfg(feature = "op-contracts")]
fn validate_contract(&self) -> Result<(), OperationError> {
self.validate_access_spec()?;
let effects = self.spec.derived_effects;
let recompute_ds = effects.recompute();
if recompute_ds.intersects(effects.preserve())
|| recompute_ds.intersects(effects.invalidate())
|| effects.preserve().intersects(effects.invalidate())
{
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation derived_effects contains overlapping effect categories",
});
}
let updated_or_cleared = self.trace.cleared_cache | self.trace.updated_cache;
if !updated_or_cleared.contains(self.spec.needs_update()) {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation body did not clear or update every required cache state",
});
}
if !self
.trace
.preserved_cache
.contains(self.spec.derived_effects.preserve())
{
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation body did not prove every declared preserved derived state",
});
}
if self.spec.requires_mapping == MappingRequirement::Required
&& self.topology_mapping.is_none()
{
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "strong topology operation did not record a topology mapping",
});
}
if !self.trace.remapped_blocks.contains(self.spec.auto_remap) {
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation did not remap every registry-required block",
});
}
if self.trace.touched_blocks.contains(BlockSet::TOPOLOGY)
&& self.spec.topology_edit != TopologyEditKind::None
&& self.trace.recorded_topology_edit == TopologyEditKind::None
{
return Err(OperationError::InvalidInput {
operation: self.spec,
message: "operation touched topology without recording the registry topology edit",
});
}
Ok(())
}
fn record_remapped(&mut self, block: BlockSet) {
#[cfg(feature = "op-contracts")]
{
self.trace.remapped_blocks = self.trace.remapped_blocks.union(block);
}
#[cfg(not(feature = "op-contracts"))]
{
let _ = block;
}
}
}
fn validate_semantic_preconditions(
molecule: &Molecule,
spec: &'static MoleculeOpSpec,
) -> Result<(), OperationError> {
let preconditions = spec.semantic_preconditions;
if preconditions.contains(SemanticPreconditionSet::TRUSTED_BOND_TOPOLOGY)
&& molecule.num_atoms() != 0
&& molecule.topology_trust() != TopologyTrust::TrustedGraph
{
return Err(OperationError::Precondition {
operation: spec,
requirement: SemanticPrecondition::TrustedBondTopology,
message: "operation requires a molecule with trusted bond topology",
});
}
if preconditions.contains(SemanticPreconditionSet::HYDROGEN_OWNERSHIP_REPRESENTED)
&& !hydrogen_ownership_is_represented(molecule)
{
return Err(OperationError::Precondition {
operation: spec,
requirement: SemanticPrecondition::HydrogenOwnershipRepresented,
message: "explicit hydrogen atoms must be connected to their owning heavy atoms",
});
}
Ok(())
}
fn hydrogen_ownership_is_represented(molecule: &Molecule) -> bool {
if !molecule
.atoms()
.iter()
.any(|atom| atom.atomic_number() != 1)
{
return true;
}
molecule
.atoms()
.iter()
.filter(|atom| atom.atomic_number() == 1)
.all(|atom| {
molecule
.topology_block()
.adjacency
.neighbors_of(atom.id().index())
.iter()
.any(|neighbor| {
molecule
.atom(AtomId::new(neighbor.atom_index))
.is_some_and(|neighbor_atom| neighbor_atom.atomic_number() != 1)
})
})
}