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}