diff --git a/crates/oximo-core/src/display.rs b/crates/oximo-core/src/display.rs index 1e37b844..54fe8716 100644 --- a/crates/oximo-core/src/display.rs +++ b/crates/oximo-core/src/display.rs @@ -289,7 +289,7 @@ impl fmt::Display for Model { } } - if self.is_feasibility() || self.semantic_objective.borrow().is_some() { + if self.is_feasibility() || self.objective.borrow().is_some() { writeln!(f, "{}", self.display_objective())?; } #[cfg(feature = "gdp")] diff --git a/crates/oximo-core/src/model.rs b/crates/oximo-core/src/model.rs index 0cba25b6..acf20ae4 100644 --- a/crates/oximo-core/src/model.rs +++ b/crates/oximo-core/src/model.rs @@ -26,7 +26,7 @@ use crate::indicator::{IndicatorConstraint, IndicatorConstraintId}; use crate::objective::{Objective, ObjectiveSense}; use crate::param::Parameter; use crate::reformulation::{IndicatorReformulationArtifacts, SosReformulationArtifacts}; -use crate::shared::SharedCell; +use crate::shared::{LazySharedCell, SharedCell}; use crate::soc::{SocConstraint, SocConstraintId}; use crate::sos::{SosConstraint, SosConstraintId}; use crate::var::Variable; @@ -114,24 +114,26 @@ pub struct Model { pub(crate) var_names: SharedCell>, pub(crate) parameters: SharedCell>, pub(crate) param_names: SharedCell>, - pub(crate) semantics: SharedCell, + pub(crate) semantics: LazySharedCell, implicit_scalars: Cell, scalar_declarations: Cell, - pub(crate) alternative_scalar_views: - SharedCell>, + pub(crate) alternative_scalar_views: LazySharedCell< + FxHashMap, + >, pub(crate) inactive_scalar_views: - SharedCell>, + LazySharedCell>, pub(crate) constraints: SharedCell>, pub(crate) semantic_names: SharedCell, pub(crate) soc_constraints: SharedCell>, pub(crate) psd_constraints: SharedCell>, pub(crate) sos_constraints: SharedCell>, pub(crate) indicator_constraints: SharedCell>, - pub(crate) sos_reformulations: SharedCell>, - pub(crate) indicator_reformulations: SharedCell>, - pub(crate) semantic_objective: SharedCell>, + pub(crate) sos_reformulations: LazySharedCell>, + pub(crate) indicator_reformulations: LazySharedCell>, + pub(crate) semantic_objective: LazySharedCell>, pub(crate) objective: SharedCell>, objective_declared: Cell, + objective_signature: Cell>, cached_kind: Cell>, cached_kind_parameter_revision: Cell, /// Monotonic counter for auto-naming anonymous constraints registered via @@ -280,19 +282,25 @@ impl Model { indicator_constraints: self.indicator_constraints.clone(), sos_reformulations: self.sos_reformulations.clone(), indicator_reformulations: self.indicator_reformulations.clone(), - semantic_objective: SharedCell::new(self.semantic_objective.borrow().clone()), + semantic_objective: LazySharedCell::default(), objective: SharedCell::new(self.objective.borrow().clone()), objective_declared: Cell::new(self.objective_declared.get()), + objective_signature: Cell::new(self.objective_signature.get()), cached_kind: Cell::new(self.cached_kind.get()), cached_kind_parameter_revision: Cell::new(self.cached_kind_parameter_revision.get()), auto_seq: Cell::new(self.auto_seq.get()), }; - if let Some(objective) = cloned.semantic_objective.borrow_mut().as_mut() { + if self.objective_signature.get().is_none() + && let Some(source) = + self.semantic_objective.borrow().as_deref().and_then(Option::as_ref) + { + let mut objective = source.clone(); objective.function.rebind_model(cloned.id()); + *cloned.semantic_objective.borrow_mut() = Some(objective); } - if !cloned.semantics.borrow().extensions.is_empty() { + if cloned.semantics.borrow().as_deref().is_some_and(|s| !s.extensions.is_empty()) { cloned.semantics.borrow_mut().rebind_extensions(cloned.id()); } @@ -321,22 +329,23 @@ impl Model { var_names: SharedCell::new(FxHashMap::default()), parameters: SharedCell::new(Vec::new()), param_names: SharedCell::new(FxHashMap::default()), - semantics: SharedCell::new(semantic::SemanticRegistry::default()), + semantics: LazySharedCell::default(), implicit_scalars: Cell::new(true), scalar_declarations: Cell::new(0), - alternative_scalar_views: SharedCell::new(FxHashMap::default()), - inactive_scalar_views: SharedCell::new(FxHashMap::default()), + alternative_scalar_views: LazySharedCell::default(), + inactive_scalar_views: LazySharedCell::default(), constraints: SharedCell::new(Vec::new()), semantic_names: SharedCell::new(semantic::ConstraintNames::default()), soc_constraints: SharedCell::new(Vec::new()), psd_constraints: SharedCell::new(Vec::new()), sos_constraints: SharedCell::new(Vec::new()), indicator_constraints: SharedCell::new(Vec::new()), - sos_reformulations: SharedCell::new(Vec::new()), - indicator_reformulations: SharedCell::new(Vec::new()), - semantic_objective: SharedCell::new(None), + sos_reformulations: LazySharedCell::default(), + indicator_reformulations: LazySharedCell::default(), + semantic_objective: LazySharedCell::default(), objective: SharedCell::new(None), objective_declared: Cell::new(false), + objective_signature: Cell::new(None), cached_kind: Cell::new(None), cached_kind_parameter_revision: Cell::new(0), auto_seq: Cell::new(0), @@ -426,7 +435,9 @@ impl Model { /// previously set objective. Not part of the stable public API. #[doc(hidden)] pub fn __feasibility(&self) { - *self.semantic_objective.borrow_mut() = None; + self.objective.assert_mutable(); + self.semantic_objective.clear(); + self.objective_signature.set(None); *self.objective.borrow_mut() = None; self.objective_declared.set(true); self.cached_kind.set(None); @@ -443,8 +454,9 @@ impl Model { let function = crate::FunctionValue::new(crate::Function::semantic_data(&semantic_function)); self.assert_expr_belongs(expr); - *self.semantic_objective.borrow_mut() = - Some(crate::objective::SemanticObjective { function, sense }); + self.objective.assert_mutable(); + self.semantic_objective.clear(); + self.objective_signature.set(semantic::storage::ScalarKind::of(&function)); *self.objective.borrow_mut() = Some(Objective { expr: expr.id, sense }); self.objective_declared.set(true); self.cached_kind.set(None); @@ -453,7 +465,7 @@ impl Model { /// Whether feasibility was declared explicitly via `objective!(m, Feasibility)`, /// as opposed to a model that simply has no objective set. pub fn is_feasibility(&self) -> bool { - self.objective_declared.get() && self.semantic_objective.borrow().is_none() + self.objective_declared.get() && self.objective.borrow().is_none() } /// Ensure the model has a solve direction declared: either an objective @@ -482,8 +494,15 @@ impl Model { } impl Model { + /// Inspect the original objective function and direction. pub fn semantic_objective(&self) -> Ref<'_, Option> { - self.semantic_objective.borrow() + self.semantic_objective.borrow_or_init(|| { + let kind = self.objective_signature.get()?; + self.objective.borrow().as_ref().map(|objective| crate::objective::SemanticObjective { + function: kind.function(self.id(), objective.expr), + sense: objective.sense, + }) + }) } pub fn minimize(&self, function: F) { @@ -514,9 +533,16 @@ impl Model { "objective requires a scalar expression representation; opaque registration is deferred" ), }; + self.objective.assert_mutable(); + let kind = semantic::storage::ScalarKind::of(&function); + if kind.is_some() { + self.semantic_objective.clear(); + } else { + *self.semantic_objective.borrow_mut() = + Some(crate::objective::SemanticObjective { function, sense }); + } + self.objective_signature.set(kind); *self.objective.borrow_mut() = Some(view); - *self.semantic_objective.borrow_mut() = - Some(crate::objective::SemanticObjective { function, sense }); self.objective_declared.set(true); self.invalidate_kind(); } diff --git a/crates/oximo-core/src/model/constraints.rs b/crates/oximo-core/src/model/constraints.rs index 63029989..eba00204 100644 --- a/crates/oximo-core/src/model/constraints.rs +++ b/crates/oximo-core/src/model/constraints.rs @@ -511,10 +511,8 @@ impl Model { } constraints.reserve(count); names.reserve(count); - self.reserve_semantics(ConstraintId(0).into(), count); let mut ids = Vec::with_capacity(count); - let mut semantics = self.semantic_writer(); - let mut alternatives = self.alternative_scalar_views.borrow_mut(); + let mut semantics = None; for item in items { let id = @@ -529,7 +527,7 @@ impl Model { }); if item.alternative { - alternatives.insert( + self.alternative_scalar_views.borrow_mut().insert( id.into(), crate::semantic::ScalarConstraintView { lhs: item.lhs, @@ -538,7 +536,21 @@ impl Model { }, ); } - item.semantic.record(&mut semantics, id, &item.name, item.origin, item.alternative); + if semantics.is_none() + && self.implicit_scalars.get() + && matches!(item.semantic, PendingSemantic::Direct(_)) + && matches!(item.origin, PendingOrigin::Declared) + { + self.record_declared_scalar(id); + } else { + item.semantic.record( + semantics.get_or_insert_with(|| self.semantic_writer()), + id, + &item.name, + item.origin, + item.alternative, + ); + } names.insert(item.name, id); ids.push(id); } @@ -771,7 +783,7 @@ impl Model { if self.implicit_scalars.get() { self.scalar_declarations.get() } else { - self.semantics.borrow().len() + self.semantics.borrow().as_deref().map_or(0, super::semantic::SemanticRegistry::len) } } @@ -968,7 +980,6 @@ impl Model { } constraints.reserve(count); names.reserve(count); - self.reserve_semantics(SocConstraintId(0).into(), count); let mut semantics = self.semantic_writer(); for item in items { @@ -1117,7 +1128,6 @@ impl Model { } constraints.reserve(count); names.reserve(count); - self.reserve_semantics(SosConstraintId(0).into(), count); let mut semantics = self.semantic_writer(); for item in items { @@ -1401,10 +1411,8 @@ impl Model { constraints.reserve(items.len()); names.reserve(items.len()); - self.reserve_semantics(IndicatorConstraintId(0).into(), items.len()); let mut ids = Vec::with_capacity(items.len()); - let mut semantics = self.semantic_writer(); - let mut alternatives = self.alternative_scalar_views.borrow_mut(); + let mut semantics = None; for item in items { let id = IndicatorConstraintId( @@ -1412,7 +1420,7 @@ impl Model { ); if item.alternative { - alternatives.insert( + self.alternative_scalar_views.borrow_mut().insert( id.into(), crate::semantic::ScalarConstraintView { lhs: item.lhs, @@ -1421,7 +1429,7 @@ impl Model { }, ); } - semantics.record_indicator( + semantics.get_or_insert_with(|| self.semantic_writer()).record_indicator( id, item.name.clone(), self.id(), diff --git a/crates/oximo-core/src/model/dependencies.rs b/crates/oximo-core/src/model/dependencies.rs index 2f7bd001..1313eab5 100644 --- a/crates/oximo-core/src/model/dependencies.rs +++ b/crates/oximo-core/src/model/dependencies.rs @@ -291,7 +291,7 @@ impl Model { } let semantic = self.semantic_objective.borrow(); - if let Some(objective) = semantic.as_ref() { + if let Some(objective) = semantic.as_deref().and_then(Option::as_ref) { objective.function.visit_expressions(&mut |root| objective_roots.push(root)); } } @@ -319,7 +319,10 @@ impl Model { for (index, constraint) in constraints.algebraic().iter().enumerate() { let id = ConstraintId(u32::try_from(index).expect("constraint count fits u32")); - let root = inactive.get(&id).map_or(constraint.lhs, |view| view.lhs); + let root = inactive + .as_deref() + .and_then(|views| views.get(&id)) + .map_or(constraint.lhs, |view| view.lhs); let parameters = builder.parameters_for_roots([root]); builder.add_target(ParameterTarget::Algebraic(id), parameters); } diff --git a/crates/oximo-core/src/model/indexed.rs b/crates/oximo-core/src/model/indexed.rs index 77583670..7751a0ef 100644 --- a/crates/oximo-core/src/model/indexed.rs +++ b/crates/oximo-core/src/model/indexed.rs @@ -613,7 +613,6 @@ impl Model { if !indexed_parallel(keys.len(), forced_parallel, PAR_INDEXED_SOC_THRESHOLD) { self.soc_constraints.borrow_mut().reserve(keys.len()); self.soc_names_mut().reserve(keys.len()); - self.reserve_semantics(crate::SocConstraintId(0).into(), keys.len()); for key in &keys { let name: SmolStr = format_index_name_small(name_prefix, key); @@ -690,7 +689,6 @@ impl Model { if !indexed_parallel(keys.len(), forced_parallel, PAR_INDEXED_SOS_THRESHOLD) { self.sos_constraints.borrow_mut().reserve(keys.len()); self.sos_names_mut().reserve(keys.len()); - self.reserve_semantics(crate::SosConstraintId(0).into(), keys.len()); for key in &keys { self.register_sos_one(sos_type, prepare(key)); @@ -1159,6 +1157,5 @@ impl Model { fn reserve_scalar_family(&self, count: usize) { self.constraints.borrow_mut().reserve(count); self.constraint_names_mut().reserve(count); - self.reserve_semantics(crate::ConstraintId(0).into(), count); } } diff --git a/crates/oximo-core/src/model/semantic.rs b/crates/oximo-core/src/model/semantic.rs index 75397bfe..aeb8e25f 100644 --- a/crates/oximo-core/src/model/semantic.rs +++ b/crates/oximo-core/src/model/semantic.rs @@ -170,7 +170,9 @@ impl Model { /// Their row multipliers require an explicit mapping to semantic coordinates. #[inline] pub fn visit_alternative_lowerings(&self, visitor: impl FnMut(SemanticConstraintId)) { - self.alternative_scalar_views.borrow().keys().copied().for_each(visitor); + if let Some(views) = self.alternative_scalar_views.borrow().as_deref() { + views.keys().copied().for_each(visitor); + } } /// Return the stored scalar row or indicator consequent selected by an explicit lowering. @@ -179,13 +181,7 @@ impl Model { &self, id: SemanticConstraintId, ) -> Option { - self.alternative_scalar_views.borrow().get(&id).copied() - } - - /// Reserve metadata space for additions to one native registry. - pub(super) fn reserve_semantics(&self, registry: SemanticConstraintId, additional: usize) { - let mut records = self.semantics.borrow_mut(); - records.reserve(registry, additional); + self.alternative_scalar_views.borrow().as_deref().and_then(|views| views.get(&id).copied()) } /// Record a function/set pair as an original model declaration. @@ -418,7 +414,7 @@ impl Model { _ => None, } } else { - self.semantics.borrow().position(id) + self.semantics.borrow().as_deref().and_then(|records| records.position(id)) } } @@ -454,13 +450,17 @@ impl Model { let Some(row) = rows.get(id.index()) else { return Ok(None) }; if row.signature.is_some() { - return Ok(Some(self.inactive_scalar_views.borrow().get(&id).copied().unwrap_or( - crate::ScalarConstraintView { - lhs: row.lhs, - lower: row.lower, - upper: row.upper, - }, - ))); + return Ok(Some( + self.inactive_scalar_views + .borrow() + .as_deref() + .and_then(|views| views.get(&id).copied()) + .unwrap_or(crate::ScalarConstraintView { + lhs: row.lhs, + lower: row.lower, + upper: row.upper, + }), + )); } } @@ -575,7 +575,11 @@ impl Model { let record = self.semantic_constraint(handle)?.expect("unknown semantic handle"); let (lhs, lower, upper) = if !active { (self.__constant(0.0).id(), f64::NEG_INFINITY, f64::INFINITY) - } else if let Some(view) = self.alternative_scalar_views.borrow().get(&handle.id()) + } else if let Some(view) = self + .alternative_scalar_views + .borrow() + .as_deref() + .and_then(|views| views.get(&handle.id())) { (view.lhs, view.lower, view.upper) } else { @@ -626,6 +630,8 @@ impl Model { } else { self.semantics .borrow() + .as_deref() + .expect("registered metadata") .entry(self.semantic_position(handle.id())) .expect("registered position") .representation @@ -755,7 +761,10 @@ impl Model { let position = self.semantic_position(id); if !self.implicit_scalars.get() || !matches!(origin, ConstraintOrigin::Declared) { - self.semantic_writer().records.set_origin(position, origin); + let name = SmolStr::new( + self.semantic_constraints_ref().get(position).expect("registered position").name, + ); + self.semantic_writer().records.set_origin(position, origin, &name); } } } @@ -950,7 +959,12 @@ impl Model { ) -> Result<(), crate::ConstraintUpdateError> { self.ensure_constraint_not_reformulated(id)?; - if self.alternative_scalar_views.borrow().contains_key(&id) { + if self + .alternative_scalar_views + .borrow() + .as_deref() + .is_some_and(|views| views.contains_key(&id)) + { return Err(crate::ConstraintUpdateError::AlternativeLowering(id)); } @@ -970,12 +984,20 @@ impl Model { } let reformulated = match id { - SemanticConstraintId::SpecialOrderedSet(id) => { - self.sos_reformulations.borrow().iter().any(|a| a.source == id) - } - SemanticConstraintId::Indicator(id) => { - self.indicator_reformulations.borrow().iter().any(|a| a.source == id) - } + SemanticConstraintId::SpecialOrderedSet(id) => self + .sos_reformulations + .borrow() + .as_deref() + .into_iter() + .flatten() + .any(|a| a.source == id), + SemanticConstraintId::Indicator(id) => self + .indicator_reformulations + .borrow() + .as_deref() + .into_iter() + .flatten() + .any(|a| a.source == id), _ => false, }; diff --git a/crates/oximo-core/src/model/semantic/positions.rs b/crates/oximo-core/src/model/semantic/positions.rs index d415cec8..c14d7d62 100644 --- a/crates/oximo-core/src/model/semantic/positions.rs +++ b/crates/oximo-core/src/model/semantic/positions.rs @@ -1,54 +1,54 @@ use crate::SemanticConstraintId; -const MISSING: usize = usize::MAX; +#[derive(Clone, Copy, Debug)] +struct PositionRun { + first: usize, + position: usize, + len: usize, +} #[derive(Clone, Debug, Default)] struct NativePositions { - first: usize, - values: Vec, + runs: smallvec::SmallVec<[PositionRun; 1]>, } impl NativePositions { - /// Map a native index to its declaration position, storing only the used index range. - fn insert(&mut self, index: usize, position: usize) { - if self.values.is_empty() { - self.first = index; - } else if index < self.first { - self.values.splice(0..0, std::iter::repeat_n(MISSING, self.first - index)); - self.first = index; + fn insert_run(&mut self, first: usize, position: usize, len: usize) { + if let Some(last) = self.runs.last_mut() + && last.first + last.len == first + && last.position + last.len == position + { + last.len += len; + return; } - - let offset = index - self.first; - - if offset >= self.values.len() { - self.values.resize(offset.checked_add(1).expect("constraint ID overflow"), MISSING); + let index = self.runs.partition_point(|run| run.first < first); + self.runs.insert(index, PositionRun { first, position, len }); + if index + 1 < self.runs.len() { + let next = self.runs[index + 1]; + let current = &mut self.runs[index]; + if current.first + current.len == next.first + && current.position + current.len == next.position + { + current.len += next.len; + self.runs.remove(index + 1); + } } - self.values[offset] = position; } - /// Look up a native index, returning `None` for gaps or indexes outside the stored range. fn get(&self, index: usize) -> Option { - index - .checked_sub(self.first) - .and_then(|offset| self.values.get(offset)) - .copied() - .filter(|&position| position != MISSING) - } - - /// Return whether no native positions have been stored. - fn is_empty(&self) -> bool { - self.values.is_empty() + let run = + self.runs.get(self.runs.partition_point(|run| run.first <= index).checked_sub(1)?)?; + let offset = index - run.first; + (offset < run.len).then(|| run.position + offset) } } #[derive(Clone, Debug, Default)] pub(crate) struct SemanticPositions { native: [NativePositions; 5], - identity: Option<(usize, usize)>, } impl SemanticPositions { - /// Select the position table belonging to a semantic ID's native registry. fn registry(id: SemanticConstraintId) -> usize { match id { SemanticConstraintId::Algebraic(_) => 0, @@ -59,60 +59,16 @@ impl SemanticPositions { } } - /// Expand an implicit identity mapping into the selected registry's position table. - fn materialize_identity(&mut self) { - if let Some((registry, len)) = self.identity.take() { - let positions = &mut self.native[registry]; - positions.first = 0; - positions.values.extend(0..len); - } + pub(super) fn insert_run(&mut self, id: SemanticConstraintId, position: usize, len: usize) { + self.native[Self::registry(id)].insert_run(id.index(), position, len); } - /// Reserve only the registry selected by `id`. - pub(super) fn reserve(&mut self, id: SemanticConstraintId, additional: usize) { - let registry = Self::registry(id); - - if let Some((existing, _)) = self.identity { - if existing == registry { - return; - } - self.materialize_identity(); - } else if self.native.iter().all(NativePositions::is_empty) { - self.identity = Some((registry, 0)); - return; - } - self.native[registry].values.reserve(additional); - } - - /// Store an ID's declaration position. - #[inline] - pub(super) fn insert(&mut self, id: SemanticConstraintId, position: usize) { - let registry = Self::registry(id); - - if let Some((existing, len)) = self.identity.as_mut() { - if registry == *existing && id.index() == *len && position == *len { - *len += 1; - return; - } - self.materialize_identity(); - } else if position == 0 - && id.index() == 0 - && self.native.iter().all(NativePositions::is_empty) - { - self.identity = Some((registry, 1)); - return; - } - - self.native[registry].insert(id.index(), position); + #[cfg(test)] + fn insert(&mut self, id: SemanticConstraintId, position: usize) { + self.insert_run(id, position, 1); } - /// Find an ID's declaration position through its implicit or stored mapping. - #[inline] pub(super) fn get(&self, id: SemanticConstraintId) -> Option { - if let Some((registry, len)) = self.identity { - return (Self::registry(id) == registry && id.index() < len).then(|| id.index()); - } - self.native[Self::registry(id)].get(id.index()) } } @@ -129,7 +85,7 @@ mod tests { let mut positions = SemanticPositions::default(); positions.insert(ConstraintId(100_000).into(), 100_001); positions.insert(ConstraintId(100_002).into(), 100_003); - assert_eq!(positions.native[0].values.len(), 3); + assert_eq!(positions.native[0].runs.len(), 2); assert_eq!(positions.get(ConstraintId(99_999).into()), None); assert_eq!(positions.get(ConstraintId(100_001).into()), None); @@ -143,14 +99,12 @@ mod tests { #[test] fn homogeneous_prefix_survives_reservations_mixing_and_independent_forks() { let mut positions = SemanticPositions::default(); - positions.reserve(ConstraintId(0).into(), 64); for id in 0..64 { positions.insert(ConstraintId(id).into(), id as usize); } let mut fork = positions.clone(); - positions.reserve(SocConstraintId(0).into(), 2); positions.insert(SocConstraintId(0).into(), 64); positions.insert(ConstraintId(64).into(), 65); fork.insert(ConstraintId(64).into(), 64); @@ -192,6 +146,6 @@ mod tests { assert_eq!(positions.get(ConstraintId(1).into()), None); assert_eq!(fork.get(ConstraintId(1).into()), Some(6)); assert_eq!(positions.get(ConstraintId(u32::MAX).into()), None); - assert_eq!(positions.native[0].values.len(), 3); + assert_eq!(positions.native[0].runs.len(), 2); } } diff --git a/crates/oximo-core/src/model/semantic/storage.rs b/crates/oximo-core/src/model/semantic/storage.rs index 9bbd64a2..729f5199 100644 --- a/crates/oximo-core/src/model/semantic/storage.rs +++ b/crates/oximo-core/src/model/semantic/storage.rs @@ -105,171 +105,183 @@ impl SemanticEntry { } } +#[derive(Clone, Copy, Debug)] +struct SemanticRun { + first: SemanticEntry, + start: usize, + len: usize, +} + #[derive(Clone, Debug, Default)] -enum SemanticEntries { - #[default] - Empty, - Scalars { - len: usize, - origin: u32, - tail: Vec, - }, - Uniform { - first: SemanticEntry, - len: usize, - }, - Dense(Vec), +struct SemanticEntries { + runs: smallvec::SmallVec<[SemanticRun; 1]>, + overrides: rustc_hash::FxHashMap, + len: usize, } impl SemanticEntries { - /// Count entries across compact prefixes and explicitly stored declarations. - fn len(&self) -> usize { - match self { - Self::Empty => 0, - Self::Scalars { len, tail, .. } => *len + tail.len(), - Self::Uniform { len, .. } => *len, - Self::Dense(entries) => entries.len(), + fn base(&self, position: usize) -> Option { + if position >= self.len { + return None; } + let run = &self.runs[self.runs.partition_point(|run| run.start <= position) - 1]; + run.first.offset(position - run.start) } - /// Read one declaration entry without expanding compact storage. fn get(&self, position: usize) -> Option { - match self { - Self::Empty => None, - Self::Scalars { len, origin, tail } => { - if position < *len { - Some(SemanticEntry { - id: crate::ConstraintId(u32::try_from(position).expect("registered ID")) - .into(), - representation: Representation::Scalar, - origin: *origin, - }) - } else { - tail.get(position - len).copied() - } - } - Self::Uniform { first, len } => { - (position < *len).then(|| first.offset(position)).flatten() - } - Self::Dense(entries) => entries.get(position).copied(), + self.overrides.get(&position).copied().or_else(|| self.base(position)) + } + + #[inline] + fn push(&mut self, entry: SemanticEntry) { + if let Some(last) = self.runs.last_mut() + && entry.follows(last.first, last.len) + { + last.len += 1; + } else { + self.runs.push(SemanticRun { first: entry, start: self.len, len: 1 }); } + self.len += 1; } - /// Expand compact entries into a mutable vector. - fn materialize(&mut self) -> &mut Vec { - if matches!(self, Self::Scalars { .. }) { - *self = Self::Dense( - (0..self.len()) - .map(|position| self.get(position).expect("registered position")) - .collect(), - ); - } else if let Self::Uniform { first, len } = *self { - *self = Self::Dense( - (0..len).map(|offset| first.offset(offset).expect("registered ID")).collect(), - ); - } else if matches!(self, Self::Empty) { - *self = Self::Dense(Vec::new()); + fn replace(&mut self, position: usize, entry: SemanticEntry) { + let base = self.base(position).expect("registered position"); + debug_assert_eq!(base.id, entry.id); + if base == entry { + self.overrides.remove(&position); + } else { + self.overrides.insert(position, entry); } + } +} - let Self::Dense(entries) = self else { unreachable!("materialized entries") }; - entries +#[derive(Clone)] +pub(super) struct SemanticEntryIter<'a> { + entries: Option<&'a SemanticEntries>, + front: usize, + back: usize, + front_run: usize, + back_run: usize, +} + +impl SemanticEntryIter<'_> { + pub(super) fn implicit(len: usize) -> Self { + Self { entries: None, front: 0, back: len, front_run: 0, back_run: 0 } } - /// Append an entry while retaining compact scalar or uniform runs when possible. #[inline] - fn push(&mut self, entry: SemanticEntry) { - if let (SemanticConstraintId::Algebraic(id), Representation::Scalar) = - (entry.id, entry.representation) - { - match self { - Self::Empty if id.index() == 0 => { - *self = Self::Scalars { len: 1, origin: entry.origin, tail: Vec::new() }; - return; - } - Self::Scalars { len, origin, tail } - if tail.is_empty() && *origin == entry.origin && id.index() == *len => - { - *len += 1; - return; - } - _ => {} + fn at(&self, position: usize, run: usize) -> SemanticEntry { + self.entries.map_or_else( + || SemanticRegistry::declared_scalar(position), + |entries| { + entries.overrides.get(&position).copied().unwrap_or_else(|| { + let run = &entries.runs[run]; + run.first.offset(position - run.start).expect("registered ID") + }) + }, + ) + } +} + +impl Iterator for SemanticEntryIter<'_> { + type Item = SemanticEntry; + + #[inline] + fn next(&mut self) -> Option { + if self.front == self.back { + return None; + } + if let Some(entries) = self.entries { + let run = &entries.runs[self.front_run]; + if self.front == run.start + run.len { + self.front_run += 1; } } + let result = self.at(self.front, self.front_run); + self.front += 1; + Some(result) + } - match self { - Self::Empty => *self = Self::Uniform { first: entry, len: 1 }, - Self::Scalars { tail, .. } => tail.push(entry), - Self::Uniform { first, len } if entry.follows(*first, *len) => *len += 1, - _ => self.materialize().push(entry), + fn nth(&mut self, n: usize) -> Option { + if n >= self.len() { + self.front = self.back; + return None; + } + if n != 0 { + self.front += n; + if let Some(entries) = self.entries { + self.front_run = entries.runs.partition_point(|run| run.start <= self.front) - 1; + } } + self.next() } - /// Replace one entry, expanding compact storage only when needed. - fn replace(&mut self, position: usize, entry: SemanticEntry) { - if let Self::Scalars { len, tail, .. } = self - && position >= *len - { - tail[position - *len] = entry; - return; - } + fn last(mut self) -> Option { + self.next_back() + } - if let Self::Scalars { origin, .. } = self - && entry.origin == *origin - && matches!(entry.id, SemanticConstraintId::Algebraic(id) if id.index() == position) - && let Representation::Scalar = entry.representation - { - return; - } + fn count(self) -> usize { + self.len() + } - if self.get(position) != Some(entry) { - if let Self::Scalars { len, tail, .. } = self - && position.checked_add(1) == Some(*len) - { - *len -= 1; - tail.insert(0, entry); - return; - } - self.materialize()[position] = entry; - } + fn size_hint(&self) -> (usize, Option) { + let len = self.back - self.front; + (len, Some(len)) } +} - /// Reserve existing entry buffers without expanding compact runs. - fn reserve(&mut self, additional: usize) { - match self { - Self::Scalars { tail, .. } if !tail.is_empty() => tail.reserve(additional), - Self::Dense(entries) => entries.reserve(additional), - _ => {} +impl DoubleEndedIterator for SemanticEntryIter<'_> { + fn next_back(&mut self) -> Option { + if self.front == self.back { + return None; + } + self.back -= 1; + if let Some(entries) = self.entries + && self.back < entries.runs[self.back_run].start + { + self.back_run -= 1; } + Some(self.at(self.back, self.back_run)) } - /// Count scalar entries stored as an implicit prefix. - fn scalar_prefix(&self) -> usize { - match self { - Self::Scalars { len, .. } => *len, - _ => 0, + fn nth_back(&mut self, n: usize) -> Option { + if n >= self.len() { + self.back = self.front; + return None; } + if n != 0 { + self.back -= n; + if let Some(entries) = self.entries { + self.back_run = entries.runs.partition_point(|run| run.start < self.back) - 1; + } + } + self.next_back() } } +impl ExactSizeIterator for SemanticEntryIter<'_> {} + #[derive(Clone, Debug, Default)] pub(crate) struct SemanticRegistry { - positions: Option>, + positions: super::positions::SemanticPositions, entries: SemanticEntries, pub(crate) extensions: Vec, origins: Vec, + origin_overrides: rustc_hash::FxHashMap, } impl SemanticRegistry { - /// Initialize metadata for scalar rows already registered in native storage. + /// Initialize the existing scalar prefix in constant space and time. pub(super) fn initialize_scalar_prefix(&mut self, len: usize) { debug_assert_eq!(self.len(), 0); - if len != 0 { - self.entries = SemanticEntries::Scalars { len, origin: DECLARED, tail: Vec::new() }; + let first = Self::declared_scalar(0); + self.entries.runs.push(SemanticRun { first, start: 0, len }); + self.entries.len = len; + self.positions.insert_run(first.id, 0, len); } } - /// Build metadata for a declared scalar row at its native index. pub(super) fn declared_scalar(position: usize) -> SemanticEntry { SemanticEntry { id: crate::ConstraintId(u32::try_from(position).expect("registered ID")).into(), @@ -278,78 +290,32 @@ impl SemanticRegistry { } } - /// Return the number of semantic entries. pub(crate) fn len(&self) -> usize { - self.entries.len() + self.entries.len } - /// Read declaration metadata by its position. pub(crate) fn entry(&self, position: usize) -> Option { self.entries.get(position) } - /// Find an ID's declaration position using compact runs or the position index. - pub(crate) fn position(&self, id: SemanticConstraintId) -> Option { - match &self.entries { - SemanticEntries::Scalars { len, .. } => match id { - SemanticConstraintId::Algebraic(id) if id.index() < *len => Some(id.index()), - _ => self.positions.as_ref()?.get(id), - }, - SemanticEntries::Uniform { first, len } - if std::mem::discriminant(&id) == std::mem::discriminant(&first.id) => - { - id.index().checked_sub(first.id.index()).filter(|&offset| offset < *len) - } - SemanticEntries::Empty | SemanticEntries::Uniform { .. } => None, - SemanticEntries::Dense(_) => self.positions.as_ref()?.get(id), + pub(super) fn iter(&self) -> SemanticEntryIter<'_> { + SemanticEntryIter { + entries: Some(&self.entries), + front: 0, + back: self.len(), + front_run: 0, + back_run: self.entries.runs.len().saturating_sub(1), } } - /// Build the ID-to-position index for entries outside the implicit scalar prefix. - fn index_dense(&mut self) { - let start = self.entries.scalar_prefix(); - - if self.positions.is_none() - && start < self.len() - && matches!(self.entries, SemanticEntries::Dense(_) | SemanticEntries::Scalars { .. }) - { - let mut positions = Box::::default(); - - for position in start..self.len() { - let entry = self.entries.get(position).expect("registered position"); - positions.insert(entry.id, position); - } - self.positions = Some(positions); - } + pub(crate) fn position(&self, id: SemanticConstraintId) -> Option { + self.positions.get(id) } - /// Append metadata and update any required ID-to-position index. #[inline] fn push_entry(&mut self, entry: SemanticEntry) { - let position = self.len(); + self.positions.insert_run(entry.id, self.len(), 1); self.entries.push(entry); - - if position < self.entries.scalar_prefix() { - return; - } - - if let Some(positions) = &mut self.positions { - positions.insert(entry.id, position); - } else if matches!( - self.entries, - SemanticEntries::Dense(_) | SemanticEntries::Scalars { .. } - ) { - self.index_dense(); - } - } - - /// Reserve entry and position-index space for additions to one registry. - pub(crate) fn reserve(&mut self, registry: SemanticConstraintId, additional: usize) { - self.entries.reserve(additional); - - if let Some(positions) = &mut self.positions { - positions.reserve(registry, additional); - } } /// Store native metadata when possible, otherwise retain the full semantic record. @@ -430,16 +396,9 @@ impl SemanticRegistry { } } - /// Replace metadata and rebuild position indexes if the compact scalar prefix changes. + /// Retain isolated edits without expanding declaration runs or inverse indexes. fn replace_entry(&mut self, position: usize, entry: SemanticEntry) { - let prefix = self.entries.scalar_prefix(); self.entries.replace(position, entry); - - if prefix != self.entries.scalar_prefix() { - self.positions = None; - } - - self.index_dense(); } /// Recover provenance from an extension, a native row name, or the origin table. @@ -447,6 +406,17 @@ impl SemanticRegistry { &'a self, entry: &SemanticEntry, name: &smol_str::SmolStr, + ) -> std::borrow::Cow<'a, ConstraintOrigin> { + if let Some(origin) = self.origin_overrides.get(&entry.id) { + return std::borrow::Cow::Borrowed(origin); + } + self.base_origin(entry, name) + } + + fn base_origin<'a>( + &'a self, + entry: &SemanticEntry, + name: &smol_str::SmolStr, ) -> std::borrow::Cow<'a, ConstraintOrigin> { if let Representation::Extension(index) = entry.representation { std::borrow::Cow::Borrowed(&self.extensions[index as usize].origin) @@ -460,20 +430,38 @@ impl SemanticRegistry { } /// Change an entry's provenance while preserving its declaration position. - pub(crate) fn set_origin(&mut self, position: usize, origin: ConstraintOrigin) { - let mut entry = self.entries.get(position).expect("registered position"); + pub(crate) fn set_origin( + &mut self, + position: usize, + origin: ConstraintOrigin, + name: &smol_str::SmolStr, + ) { + let entry = self.entry(position).expect("registered position"); if let Representation::Extension(index) = entry.representation { self.extensions[index as usize].origin = origin; } else { - entry.origin = self.store_origin(origin); - self.replace_entry(position, entry); + let base = self.base_origin(&entry, name); + let equal = match (base.as_ref(), &origin) { + (ConstraintOrigin::Declared, ConstraintOrigin::Declared) => true, + (ConstraintOrigin::Range(a), ConstraintOrigin::Range(b)) => a == b, + ( + ConstraintOrigin::Generated { sources: a, transformation: at }, + ConstraintOrigin::Generated { sources: b, transformation: bt }, + ) => a == b && at == bt, + _ => false, + }; + if equal { + self.origin_overrides.remove(&entry.id); + } else { + self.origin_overrides.insert(entry.id, origin); + } } } /// Update a record, retaining native metadata or moving its payload to extension storage. pub(crate) fn replace_record(&mut self, position: usize, record: SemanticConstraint) { - let mut entry = self.entries.get(position).expect("registered position"); + let mut entry = self.entry(position).expect("registered position"); if let Representation::Extension(index) = entry.representation { self.extensions[index as usize] = record; @@ -482,6 +470,7 @@ impl SemanticRegistry { } else { let index = u32::try_from(self.extensions.len()).expect("extension count overflow"); entry.representation = Representation::Extension(index); + self.origin_overrides.remove(&entry.id); self.extensions.push(record); } self.replace_entry(position, entry); @@ -494,3 +483,130 @@ impl SemanticRegistry { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn seeking_matches_dense_iteration_across_runs_overrides_and_exhaustion() { + fn check(iter: SemanticEntryIter<'_>, expected: &[SemanticEntry]) { + for front in 0..=expected.len() { + for back in 0..=expected.len() - front { + let mut remaining = iter.clone(); + for _ in 0..front { + remaining.next(); + } + for _ in 0..back { + remaining.next_back(); + } + let expected = &expected[front..expected.len() - back]; + assert_eq!(remaining.clone().count(), expected.len()); + assert_eq!(remaining.clone().last(), expected.last().copied()); + for n in (0..=expected.len()).chain([usize::MAX]) { + let mut forward = remaining.clone(); + let mut backward = remaining.clone(); + let mut dense_forward = expected.iter().copied(); + let mut dense_backward = expected.iter().copied(); + assert_eq!(forward.nth(n), dense_forward.nth(n)); + assert_eq!(backward.nth_back(n), dense_backward.nth_back(n)); + assert_eq!(forward.len(), dense_forward.len()); + assert_eq!(backward.len(), dense_backward.len()); + assert_eq!(forward.clone().last(), dense_forward.clone().last()); + assert_eq!(backward.clone().last(), dense_backward.clone().last()); + assert_eq!(forward.next_back(), dense_forward.next_back()); + assert_eq!(backward.next(), dense_backward.next()); + assert_eq!(forward.collect::>(), dense_forward.collect::>()); + assert_eq!( + backward.collect::>(), + dense_backward.collect::>() + ); + } + } + } + } + + let mut records = SemanticRegistry::default(); + records.initialize_scalar_prefix(3); + for index in 0..2 { + records.push_declared(crate::SocConstraintId(index).into(), Representation::Soc); + } + for index in 3..7 { + records.push_row_range(crate::ConstraintId(index).into(), Representation::Scalar); + } + for position in [2, 3, 7] { + let mut entry = records.entry(position).unwrap(); + entry.representation = Representation::Extension(0); + records.replace_entry(position, entry); + } + let expected: Vec<_> = (0..records.len()).map(|i| records.entry(i).unwrap()).collect(); + check(records.iter(), &expected); + + let implicit: Vec<_> = (0..9).map(SemanticRegistry::declared_scalar).collect(); + check(SemanticEntryIter::implicit(implicit.len()), &implicit); + check(SemanticRegistry::default().iter(), &[]); + } + + #[test] + fn sparse_origins_do_not_expand_a_million_scalar_declarations() { + let mut records = SemanticRegistry::default(); + records.initialize_scalar_prefix(1_000_000); + for position in [0, 500_000, 999_999] { + records.set_origin(position, ConstraintOrigin::Range("edited".into()), &"row".into()); + } + assert_eq!(records.entries.runs.len(), 1); + assert_eq!(records.entries.overrides.len(), 0); + assert_eq!(records.origin_overrides.len(), 3); + assert!(records.origins.is_empty()); + for position in [0, 500_000, 999_999] { + assert_eq!( + records.position(crate::ConstraintId(position).into()), + Some(position as usize) + ); + records.set_origin(position as usize, ConstraintOrigin::Declared, &"row".into()); + } + assert!(records.origin_overrides.is_empty()); + assert_eq!(records.entries.runs.len(), 1); + } + + #[test] + fn mixed_runs_sparse_representations_and_bidirectional_iteration_keep_positions() { + let mut records = SemanticRegistry::default(); + records.initialize_scalar_prefix(100_000); + records.push_declared(crate::SocConstraintId(0).into(), Representation::Soc); + for index in 100_000..200_000 { + records.push_row_range(crate::ConstraintId(index).into(), Representation::Scalar); + } + assert_eq!(records.entries.runs.len(), 3); + assert_eq!(records.positions.get(crate::ConstraintId(199_999).into()), Some(200_000)); + + let mut changed = records.entry(50_000).unwrap(); + changed.representation = Representation::Extension(0); + records.replace_entry(50_000, changed); + assert_eq!(records.entries.overrides.len(), 1); + assert_eq!(records.entry(50_000), Some(changed)); + records.replace_entry(50_000, SemanticRegistry::declared_scalar(50_000)); + assert!(records.entries.overrides.is_empty()); + + let expected: Vec<_> = (0..records.len()).map(|i| records.entry(i).unwrap().id).collect(); + assert_eq!(records.iter().map(|entry| entry.id).collect::>(), expected); + assert_eq!( + records.iter().rev().map(|entry| entry.id).collect::>(), + expected.iter().rev().copied().collect::>() + ); + let mut iter = records.iter(); + let mut front = 0; + let mut back = expected.len(); + while front < back { + assert_eq!(iter.next().unwrap().id, expected[front]); + front += 1; + if front < back { + back -= 1; + assert_eq!(iter.next_back().unwrap().id, expected[back]); + } + assert_eq!(iter.len(), back - front); + } + assert!(iter.next().is_none()); + assert!(iter.next_back().is_none()); + } +} diff --git a/crates/oximo-core/src/model/semantic/views.rs b/crates/oximo-core/src/model/semantic/views.rs index 24feb379..8f58e128 100644 --- a/crates/oximo-core/src/model/semantic/views.rs +++ b/crates/oximo-core/src/model/semantic/views.rs @@ -2,19 +2,61 @@ use std::cell::Ref; use rustc_hash::FxHashMap; -use super::storage::{Representation, SemanticRegistry}; +use super::storage::{Representation, SemanticEntry, SemanticEntryIter, SemanticRegistry}; use crate::semantic::{ FunctionRef, ScalarConstraintView, SemanticConstraintRef, SemanticPairRef, SetRef, }; use crate::{ConstraintId, Model, ModelConstraints, SemanticConstraintId}; +#[derive(Clone)] +struct SemanticIter<'a, F> { + entries: SemanticEntryIter<'a>, + view: F, +} + +impl T> Iterator for SemanticIter<'_, F> { + type Item = T; + + fn next(&mut self) -> Option { + self.entries.next().map(&mut self.view) + } + + fn nth(&mut self, n: usize) -> Option { + self.entries.nth(n).map(&mut self.view) + } + + fn last(mut self) -> Option { + self.entries.last().map(&mut self.view) + } + + fn count(self) -> usize { + self.entries.count() + } + + fn size_hint(&self) -> (usize, Option) { + self.entries.size_hint() + } +} + +impl T> DoubleEndedIterator for SemanticIter<'_, F> { + fn next_back(&mut self) -> Option { + self.entries.next_back().map(&mut self.view) + } + + fn nth_back(&mut self, n: usize) -> Option { + self.entries.nth_back(n).map(&mut self.view) + } +} + +impl T> ExactSizeIterator for SemanticIter<'_, F> {} + /// A read guard over semantic declarations and their canonical native payloads. #[derive(Debug)] pub struct SemanticConstraints<'a> { model_id: crate::ModelId, - records: Ref<'a, SemanticRegistry>, + records: Ref<'a, Option>>, native: ModelConstraints<'a>, - inactive: Ref<'a, FxHashMap>, + inactive: Ref<'a, Option>>>, implicit_len: Option, } @@ -31,7 +73,8 @@ impl<'a> SemanticConstraints<'a> { /// Count all declarations, including inactive constraints. pub fn len(&self) -> usize { - self.implicit_len.unwrap_or_else(|| self.records.len()) + self.implicit_len + .unwrap_or_else(|| self.records.as_deref().map_or(0, SemanticRegistry::len)) } /// Return whether the model has no constraint declarations. @@ -47,7 +90,7 @@ impl<'a> SemanticConstraints<'a> { &self, ) -> impl ExactSizeIterator + DoubleEndedIterator + Clone + '_ { - (0..self.len()).map(|position| self.entry(position).expect("registered position").id) + SemanticIter { entries: self.entries(), view: |entry: SemanticEntry| entry.id } } /// Read metadata at a declaration position without expanding an implicit scalar prefix. @@ -55,7 +98,7 @@ impl<'a> SemanticConstraints<'a> { if let Some(len) = self.implicit_len { (position < len).then(|| SemanticRegistry::declared_scalar(position)) } else { - self.records.entry(position) + self.records.as_deref()?.entry(position) } } @@ -66,7 +109,7 @@ impl<'a> SemanticConstraints<'a> { pub fn iter( &self, ) -> impl ExactSizeIterator> + DoubleEndedIterator { - (0..self.len()).map(|position| self.get(position).expect("registered position")) + SemanticIter { entries: self.entries(), view: |entry| self.view(entry) } } /// Build a semantic view at a declaration position, borrowing its registered payloads. @@ -74,12 +117,24 @@ impl<'a> SemanticConstraints<'a> { /// # Panics /// Panics if declaration metadata or native constraint storage is inconsistent. pub fn get(&self, position: usize) -> Option> { + Some(self.view(self.entry(position)?)) + } + + fn entries(&self) -> super::storage::SemanticEntryIter<'_> { + self.records.as_deref().filter(|_| self.implicit_len.is_none()).map_or_else( + || super::storage::SemanticEntryIter::implicit(self.implicit_len.unwrap_or(0)), + SemanticRegistry::iter, + ) + } + + #[inline] + fn view(&self, entry: super::storage::SemanticEntry) -> SemanticConstraintRef<'_> { use SemanticConstraintId as Id; - let entry = self.entry(position)?; let model_id = self.model_id; let (name, active, function, set) = match (entry.id, entry.representation) { (_, Representation::Extension(index)) => { - let source = &self.records.extensions[index as usize]; + let source = &self.records.as_deref().expect("registered metadata").extensions + [index as usize]; let mut view = source.as_ref(); view.active = match entry.id { Id::Algebraic(id) => self.native.algebraic()[id.index()].active, @@ -92,14 +147,15 @@ impl<'a> SemanticConstraints<'a> { } Id::Indicator(id) => self.native.indicators()[id.index()].active, }; - return Some(view); + return view; } (Id::Algebraic(id), Representation::Scalar) => { let row = &self.native.algebraic()[id.index()]; let signature = row.signature.expect("canonical scalar signature"); let (root, lower, upper) = self .inactive - .get(&id) + .as_deref() + .and_then(|views| views.get(&id)) .map_or((row.lhs, row.lower, row.upper), |v| (v.lhs, v.lower, v.upper)); ( &row.name, @@ -158,12 +214,50 @@ impl<'a> SemanticConstraints<'a> { _ => unreachable!("native representation must match its registry"), }; - Some(SemanticConstraintRef { + SemanticConstraintRef { id: entry.id, name, active, pair: SemanticPairRef { function, set }, - origin: self.records.origin(&entry, name), - }) + origin: self.records.as_deref().map_or( + std::borrow::Cow::Borrowed(&crate::ConstraintOrigin::Declared), + |records| records.origin(&entry, name), + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::cell::Cell; + + #[test] + fn seeking_projects_only_returned_entries() { + let visits = Cell::new(0); + let make_iter = || SemanticIter { + entries: SemanticEntryIter::implicit(10), + view: |entry: SemanticEntry| { + visits.set(visits.get() + 1); + entry.id.index() + }, + }; + let mut iter = make_iter(); + assert_eq!(iter.nth(3), Some(3)); + assert_eq!(visits.get(), 1); + assert_eq!(iter.nth_back(2), Some(7)); + assert_eq!(visits.get(), 2); + assert_eq!(iter.next(), Some(4)); + assert_eq!(iter.next_back(), Some(6)); + assert_eq!(iter.len(), 1); + assert_eq!(iter.last(), Some(5)); + assert_eq!(visits.get(), 5); + + assert_eq!(make_iter().count(), 10); + let mut empty = make_iter(); + assert_eq!(empty.nth(usize::MAX), None); + assert_eq!(empty.last(), None); + assert_eq!(make_iter().nth_back(usize::MAX), None); + assert_eq!(visits.get(), 5); } } diff --git a/crates/oximo-core/src/model/tests.rs b/crates/oximo-core/src/model/tests.rs index ecec0ae4..9e88378e 100644 --- a/crates/oximo-core/src/model/tests.rs +++ b/crates/oximo-core/src/model/tests.rs @@ -9,6 +9,108 @@ use oximo_expr::{ExprClass, ModelMismatchError, classify, extract_linear}; use std::panic::{AssertUnwindSafe, catch_unwind}; +#[test] +fn plain_scalar_families_keep_cold_metadata_absent_through_reads_and_forks() { + use crate::prelude::*; + rayon::ThreadPoolBuilder::new().num_threads(6).build().unwrap().install(|| { + for parallel in [false, true] { + let model = Model::new("lazy scalar metadata"); + variable!(model, x); + let keys = Set::range(0..2048); + model.add_constraints_over_with( + "rows", + &keys, + &|i: usize| x.le(i as f64), + Some(parallel), + ); + objective!(model, Min, x); + assert!(model.implicit_scalars.get()); + assert_eq!(model.scalar_declarations.get(), keys.len()); + let key = model.__update_key(); + assert_eq!(model.semantic_constraints_ref().iter().count(), keys.len()); + model.visit_alternative_lowerings(|_| panic!("unexpected lowering")); + assert!(model.alternative_lowering_view(ConstraintId(0).into()).is_none()); + assert!(model.sos_reformulations().is_empty()); + assert!(model.indicator_reformulations().is_empty()); + let _dependencies = model.parameter_dependencies(); + assert_eq!(model.__update_key(), key); + assert!(model.semantics.borrow().is_none()); + assert!(model.alternative_scalar_views.borrow().is_none()); + assert!(model.inactive_scalar_views.borrow().is_none()); + assert!(model.sos_reformulations.borrow().is_none()); + assert!(model.indicator_reformulations.borrow().is_none()); + assert!(model.semantic_objective.borrow().is_none()); + + let views = model.semantic_constraints_ref(); + let fork = model.fork(); + assert!(fork.semantics.borrow().is_none()); + assert!(fork.semantic_objective.borrow().is_none()); + assert_eq!(views.len(), fork.num_constraints()); + assert!( + catch_unwind(AssertUnwindSafe(|| { + model.set_constraint_origin( + ConstraintId(0).into(), + crate::ConstraintOrigin::Range("test".into()), + ); + })) + .is_err() + ); + drop(views); + assert_eq!(model.__update_key(), key); + } + }); +} + +#[test] +fn objective_cache_reads_preserve_update_keys_and_forks_rebind_lazily() { + use crate::prelude::*; + let model = Model::new("objective cache"); + variable!(model, x); + param!(model, p = 2.0); + objective!(model, Min, p * x); + let key = model.__update_key(); + let snapshot = model.semantic_objective(); + assert_eq!(snapshot.as_ref().unwrap().function.model_id(), model.id()); + assert_eq!(model.__update_key(), key); + assert!(catch_unwind(AssertUnwindSafe(|| model.__maximize(x))).is_err()); + assert_eq!(model.__update_key(), key); + let fork = model.fork(); + assert!(fork.semantic_objective.borrow().is_none()); + assert_eq!(fork.semantic_objective().as_ref().unwrap().function.model_id(), fork.id()); + drop(snapshot); + + let native = model.objective(); + assert!(catch_unwind(AssertUnwindSafe(|| model.__feasibility())).is_err()); + assert_eq!(model.__update_key(), key); + assert!(!model.is_feasibility()); + drop(native); + + objective!(model, Max, x.square()); + assert!(model.semantic_objective.borrow().is_none()); + assert_ne!(model.__update_key(), key); + let key = model.__update_key(); + assert!( + model + .semantic_objective() + .as_ref() + .unwrap() + .function + .signature() + .is::>() + ); + p.set_param_value(4.0).unwrap(); + assert_eq!(model.__update_key(), key); + objective!(model, Feasibility); + let key = model.__update_key(); + assert!(model.is_feasibility()); + assert!(model.semantic_objective().is_none()); + assert_eq!(model.__update_key(), key); + assert!(model.fork().semantic_objective.borrow().is_none()); + objective!(model, Min, x); + assert!(!model.is_feasibility()); + assert!(model.semantic_objective().is_some()); +} + #[test] fn update_key_ignores_parameter_registration_and_names_but_tracks_new_uses() { use crate::prelude::*; diff --git a/crates/oximo-core/src/model/variables.rs b/crates/oximo-core/src/model/variables.rs index e3845052..8c401516 100644 --- a/crates/oximo-core/src/model/variables.rs +++ b/crates/oximo-core/src/model/variables.rs @@ -363,14 +363,16 @@ impl Model { if self .sos_reformulations .borrow() - .iter() + .as_deref() + .into_iter() + .flatten() .any(|a| sos[a.source.index()].members.iter().any(|m| m.variable == id)) { return Err(ModelError::FrozenVariable(id)); } let indicators = self.indicator_constraints.borrow(); let arena = self.arena(); - if self.indicator_reformulations.borrow().iter().any(|a| { + if self.indicator_reformulations.borrow().as_deref().into_iter().flatten().any(|a| { let c = &indicators[a.source.index()]; id != c.trigger && extract_linear(&arena, c.lhs) diff --git a/crates/oximo-core/src/reformulation/indicator.rs b/crates/oximo-core/src/reformulation/indicator.rs index 3e7ea953..310fc440 100644 --- a/crates/oximo-core/src/reformulation/indicator.rs +++ b/crates/oximo-core/src/reformulation/indicator.rs @@ -363,6 +363,8 @@ impl Model { /// Complete indicator reformulation history retained by this model. #[must_use] pub fn indicator_reformulations(&self) -> Ref<'_, [IndicatorReformulationArtifacts]> { - Ref::map(self.indicator_reformulations.borrow(), Vec::as_slice) + Ref::map(self.indicator_reformulations.borrow(), |value| { + value.as_deref().map_or(&[][..], Vec::as_slice) + }) } } diff --git a/crates/oximo-core/src/reformulation/sos.rs b/crates/oximo-core/src/reformulation/sos.rs index c64d567d..80504a0c 100644 --- a/crates/oximo-core/src/reformulation/sos.rs +++ b/crates/oximo-core/src/reformulation/sos.rs @@ -204,7 +204,9 @@ impl Model { /// [`Self::reformulate_sos`] is no longer available. #[must_use] pub fn sos_reformulations(&self) -> Ref<'_, [SosReformulationArtifacts]> { - Ref::map(self.sos_reformulations.borrow(), Vec::as_slice) + Ref::map(self.sos_reformulations.borrow(), |value| { + value.as_deref().map_or(&[][..], Vec::as_slice) + }) } } diff --git a/crates/oximo-core/src/semantic/view.rs b/crates/oximo-core/src/semantic/view.rs index cf3d6293..8d657904 100644 --- a/crates/oximo-core/src/semantic/view.rs +++ b/crates/oximo-core/src/semantic/view.rs @@ -72,6 +72,7 @@ impl FunctionRef<'_> { } } + #[inline] pub fn visit_expressions(&self, visitor: &mut dyn FnMut(ExprId)) { match self { Self::Borrowed(value) => value.visit_expressions(visitor), diff --git a/crates/oximo-core/src/shared.rs b/crates/oximo-core/src/shared.rs index 1eecbe0c..e5ed67f7 100644 --- a/crates/oximo-core/src/shared.rs +++ b/crates/oximo-core/src/shared.rs @@ -26,6 +26,11 @@ impl SharedCell { self.1.get() } + /// Preflight coupled writes without detaching storage or advancing its revision. + pub(crate) fn assert_mutable(&self) { + drop(self.0.borrow_mut()); + } + #[inline] pub(crate) fn borrow(&self) -> Ref<'_, T> { Ref::map(self.0.borrow(), |storage| match storage { @@ -110,6 +115,62 @@ impl Clone for SharedCell { } } +pub(crate) struct LazySharedCell { + value: RefCell>>, + revision: Cell, +} + +impl Default for LazySharedCell { + fn default() -> Self { + Self { value: RefCell::new(None), revision: Cell::new(0) } + } +} + +impl LazySharedCell { + pub(crate) fn borrow(&self) -> Ref<'_, Option>> { + self.value.borrow() + } + + pub(crate) fn revision(&self) -> u64 { + self.revision.get() + } + + /// Initialize a read cache without recording a logical model mutation. + pub(crate) fn borrow_or_init(&self, init: impl FnOnce() -> T) -> Ref<'_, T> { + if self.value.borrow().is_none() { + *self.value.borrow_mut() = Some(Arc::new(init())); + } + Ref::map(self.value.borrow(), |value| value.as_deref().expect("initialized registry")) + } + + fn mark_changed(&self) { + self.revision.set(self.revision.get().checked_add(1).expect("registry revision overflow")); + } + + pub(crate) fn clear(&self) { + let mut value = self.value.borrow_mut(); + self.mark_changed(); + *value = None; + } +} + +impl LazySharedCell { + pub(crate) fn borrow_mut(&self) -> RefMut<'_, T> { + let value = self.value.borrow_mut(); + self.mark_changed(); + RefMut::map(value, |value| Arc::make_mut(value.get_or_insert_with(Arc::default))) + } +} + +impl Clone for LazySharedCell { + fn clone(&self) -> Self { + Self { + value: RefCell::new(self.value.borrow().clone()), + revision: Cell::new(self.revision.get()), + } + } +} + /// A slice that shares its backing buffer and detaches on mutation. pub struct SharedSlice { storage: SliceStorage, diff --git a/crates/oximo-core/tests/function_representation.rs b/crates/oximo-core/tests/function_representation.rs index 100226e4..056052ef 100644 --- a/crates/oximo-core/tests/function_representation.rs +++ b/crates/oximo-core/tests/function_representation.rs @@ -201,6 +201,57 @@ impl SemanticFunction for Wrapped { impl FunctionInSet for Wrapped {} impl FunctionInSet for Wrapped {} +impl ObjectiveFunction for Wrapped {} + +#[test] +fn custom_objective_payloads_survive_guarded_forks_and_builtin_replacement() { + let model = Model::new("custom objective"); + + variable!(model, x); + param!(model, p = 2.0); + let function = Wrapped(ScalarFunctionData::from_expression(p * x)); + model.minimize(function); + let key = model.__update_key(); + let semantic = model.semantic_objective(); + assert!( + semantic.as_ref().unwrap().function.signature().is::>>() + ); + + let fork = model.fork(); + let copied = fork.semantic_objective(); + assert!( + copied.as_ref().unwrap().function.signature().is::>>() + ); + assert_eq!(copied.as_ref().unwrap().function.model_id(), fork.id()); + assert_eq!(semantic.as_ref().unwrap().function.model_id(), model.id()); + assert_eq!(model.__update_key(), key); + assert_eq!( + model.parameter_dependencies().parameters_for(oximo_core::ParameterTarget::Objective), + &[p.param_id().unwrap()] + ); + + drop(copied); + drop(semantic); + objective!(model, Max, x); + assert!( + model + .semantic_objective() + .as_ref() + .unwrap() + .function + .signature() + .is::>() + ); + assert!( + fork.semantic_objective() + .as_ref() + .unwrap() + .function + .signature() + .is::>>() + ); +} + #[test] fn cone_lookup_handles_match_the_exact_semantic_signature() { let model = Model::new("cone handle signatures");