Skip to main content

pounce_common/
tagged.rs

1//! Tagged-object change tracking.
2//!
3//! Mirrors `Common/IpTaggedObject.{hpp,cpp}`. Each `TaggedObject`
4//! exposes a `Tag` which is bumped from a thread-local counter every
5//! time `object_changed()` is called. Cached results compare stored
6//! tags against current tags to decide whether to recompute.
7//!
8//! Ipopt's implementation is `unsigned int` per-thread starting at 1.
9//! We keep the same semantics with a `u64` counter — the underlying
10//! type is wider (u32 wraparound is reachable in long restoration runs
11//! per Ipopt's own DBG_ASSERT) but the equality semantics are
12//! identical: two `Tag` values compare equal iff they came from the
13//! same `object_changed()` call.
14
15use std::cell::Cell;
16use std::sync::atomic::{AtomicU64, Ordering};
17
18/// Per-`TaggedObject` change tag. Equivalent to `TaggedObject::Tag`
19/// (`unsigned int` upstream).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
21pub struct Tag(pub u64);
22
23impl Tag {
24    pub const NONE: Tag = Tag(0);
25}
26
27thread_local! {
28    /// Mirrors the file-static `IPOPT_THREAD_LOCAL TaggedObject::Tag unique_tag = 1`
29    /// in `IpTaggedObject.cpp`.
30    static UNIQUE_TAG: Cell<u64> = const { Cell::new(1) };
31}
32
33/// Allocate a fresh, never-before-used tag from this thread's counter.
34pub fn next_tag() -> Tag {
35    UNIQUE_TAG.with(|c| {
36        let t = c.get();
37        c.set(t.wrapping_add(1));
38        Tag(t)
39    })
40}
41
42/// Cross-thread fallback used by `TaggedCell` when a TaggedObject is
43/// shared via `Arc` and may be mutated from any thread.
44static GLOBAL_UNIQUE_TAG: AtomicU64 = AtomicU64::new(1);
45
46/// Allocate a fresh tag from the cross-thread global counter.
47pub fn next_tag_global() -> Tag {
48    Tag(GLOBAL_UNIQUE_TAG.fetch_add(1, Ordering::Relaxed))
49}
50
51/// Embeddable tag holder. A struct that wants Ipopt's tagged-object
52/// behavior holds a `TaggedCell` and calls `.bump()` from inside any
53/// state-mutating method, the same way Ipopt classes call
54/// `ObjectChanged()` from inside their setters.
55#[derive(Debug)]
56pub struct TaggedCell {
57    tag: Cell<Tag>,
58}
59
60impl TaggedCell {
61    /// Construct with an initial tag (matches Ipopt's constructor which
62    /// calls `ObjectChanged()` once).
63    pub fn new() -> Self {
64        Self {
65            tag: Cell::new(next_tag()),
66        }
67    }
68
69    /// Current tag — equivalent to `TaggedObject::GetTag`.
70    pub fn tag(&self) -> Tag {
71        self.tag.get()
72    }
73
74    /// Equivalent to `TaggedObject::HasChanged(comparison_tag)`.
75    pub fn has_changed(&self, comparison_tag: Tag) -> bool {
76        self.tag.get() != comparison_tag
77    }
78
79    /// Equivalent to `TaggedObject::ObjectChanged()`. Bumps the
80    /// thread-local counter and stores the new tag.
81    pub fn bump(&self) {
82        self.tag.set(next_tag());
83    }
84}
85
86impl Default for TaggedCell {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92/// Object-safe trait so [`crate::cached::CachedResults`] can take
93/// dependencies as `&dyn TaggedObject`.
94pub trait TaggedObject {
95    fn get_tag(&self) -> Tag;
96}
97
98impl TaggedObject for TaggedCell {
99    fn get_tag(&self) -> Tag {
100        self.tag()
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn fresh_tags_are_distinct() {
110        let a = next_tag();
111        let b = next_tag();
112        assert_ne!(a, b);
113    }
114
115    #[test]
116    fn bump_changes_tag() {
117        let c = TaggedCell::new();
118        let t0 = c.tag();
119        assert!(!c.has_changed(t0));
120        c.bump();
121        assert!(c.has_changed(t0));
122        let t1 = c.tag();
123        assert_ne!(t0, t1);
124    }
125
126    #[test]
127    fn none_never_matches_a_real_tag() {
128        let c = TaggedCell::new();
129        assert!(c.has_changed(Tag::NONE));
130    }
131}