Skip to content

Commit 3179cfc

Browse files
committed
feat!: add opt-in interior mutability via a Ref type parameter
Shared `Variable` and `Array` declarations were reachable through many `Literal::Variable` references but could not be edited, since `Arc<T>` hands out no `&mut`. Adding/removing variables and constraints already worked (those are plain `Vec`s the caller owns); mutating the interior of a shared declaration did not, which is what a presolve or rewriting pass needs. Every public generic type now carries a trailing `Ref` parameter, defaulted to `Immutable`: helpers::Immutable -> Of<T> = Arc<T> (today's behaviour) helpers::Mutable -> Of<T> = Arc<RwLock<T>> let fzn: FlatZinc<String, Mutable> = FlatZinc::from_fzn(rdr)?; var.write().unwrap().ty = Type::Int(Some(RangeList::from(0..=1))); A defaulted type parameter is the only opt-in that stays source compatible; a cargo feature would be unification-unsafe for a published library, silently changing the public API when two dependents disagree. `Mutable::Of<T>` is just `Arc<RwLock<T>>`, so edits go through the normal lock API and no `with_mut` helper is needed. `ArcKey` and `cloned_key` are untouched: `ArcKey<T>` is generic over `T` and already serves both markers as `ArcKey<RwLock<Variable<..>>>`. BREAKING CHANGE: `NamedRef::name` returns `Cow<'_, str>` rather than `&str`; the name lives behind a lock under `Mutable`, so no borrow can escape. `Cow` derefs to `str`, so most call sites are unaffected. BREAKING CHANGE: `Argument`'s parameters are now `<Identifier, Ref, L>`. `L` defaults to `Literal<Identifier, Ref>`, and a default cannot forward -reference a later parameter, so `Ref` has to precede `L` here even though it trails on the other eleven types. BREAKING CHANGE: the `From<Arc<..>> for NamedRef` conversions are now implemented per marker rather than generically. An associated type is opaque, so the compiler cannot rule out `Ref::Of<Array<..>>` and `Ref::Of<Variable<..>>` naming the same type, and the generic impls overlap. Generic code should construct the variants directly. Corpus fixtures are unchanged: `Immutable` is the default and its `Debug` and `Display` output forwards exactly as `Arc` did. The `Mutable` deadlock hazard — `NamedRef`'s `Hash`, `Ord`, and `PartialEq` take a read lock to reach the name — is documented on both types.
1 parent 5212855 commit 3179cfc

6 files changed

Lines changed: 706 additions & 202 deletions

File tree

src/fzn.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ use winnow::{
1616
use crate::{
1717
FlatZinc, Type,
1818
error::FznParseError,
19+
helpers::FznRef,
1920
intermediate::{
2021
self, Argument, Array, Constraint, Declaration, Literal, Method, NameId, ParserState,
2122
SolveObjective, Variable,
@@ -231,7 +232,9 @@ fn map_parse_error<Identifier, F>(
231232
///
232233
/// This is used by [`crate::FlatZinc::from_fzn`], which is the public entry
233234
/// point for `.fzn` parsing.
234-
pub(crate) fn parse<I, E>(source: impl BufRead) -> Result<FlatZinc<I>, FznParseError>
235+
pub(crate) fn parse<I, E, Ref: FznRef>(
236+
source: impl BufRead,
237+
) -> Result<FlatZinc<I, Ref>, FznParseError>
235238
where
236239
I: Clone + for<'a> TryFrom<&'a str, Error = E>,
237240
E: Display,
@@ -241,10 +244,10 @@ where
241244

242245
/// Parse the `.fzn` source to a [`FlatZinc`] instance using a custom
243246
/// identifier interner.
244-
pub(crate) fn parse_with_interner<I, F, E>(
247+
pub(crate) fn parse_with_interner<I, F, E, Ref: FznRef>(
245248
mut source: impl BufRead,
246249
mut interner: F,
247-
) -> Result<FlatZinc<I>, FznParseError>
250+
) -> Result<FlatZinc<I, Ref>, FznParseError>
248251
where
249252
I: Clone,
250253
F: FnMut(&str) -> Result<I, E>,
@@ -608,6 +611,7 @@ mod tests {
608611
}
609612

610613
use std::{
614+
borrow::Cow,
611615
convert::Infallible,
612616
fmt::Debug,
613617
fs::File,
@@ -811,7 +815,7 @@ mod tests {
811815
.unwrap_or_else(|| panic!("expected interned name `{expected}`"))
812816
}
813817

814-
fn output_names<Identifier>(fzn: &FlatZinc<Identifier>) -> Vec<&str> {
818+
fn output_names<Identifier>(fzn: &FlatZinc<Identifier>) -> Vec<Cow<'_, str>> {
815819
fzn.output.iter().map(NamedRef::name).collect()
816820
}
817821

src/helpers.rs

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,139 @@
22
//! deserialization of FlatZinc data.
33
44
use std::{
5+
borrow::Cow,
6+
fmt::Debug,
57
hash::{Hash, Hasher},
68
ops::Deref,
7-
sync::Arc,
9+
sync::{Arc, PoisonError, RwLock},
810
};
911

12+
/// A family of smart pointers used to share the [`Variable`](crate::Variable)
13+
/// and [`Array`](crate::Array) declarations of a
14+
/// [`FlatZinc`](crate::FlatZinc) instance.
15+
///
16+
/// This is the trait behind the `Ref` type parameter of the public types. It
17+
/// selects whether the shared declarations are plain [`Immutable`] `Arc<T>`
18+
/// values, as produced by parsing, or [`Mutable`] `Arc<RwLock<T>>` values that
19+
/// can be edited in place after the instance has been constructed.
20+
///
21+
/// The supertraits are free — implementors are zero-sized markers — and let the
22+
/// derived `Clone`, `Debug`, and `PartialEq` implementations on the public
23+
/// types, which bound every type parameter, be satisfied by `Ref: FznRef`
24+
/// alone.
25+
pub trait FznRef: Copy + Debug + Eq {
26+
/// The reference type used to share a `T`.
27+
type Of<T>: Clone;
28+
29+
/// Allocate a new shared declaration.
30+
fn new<T>(value: T) -> Self::Of<T>;
31+
32+
/// Borrow the shared declaration for reading, and apply `f` to it.
33+
fn with<T, R>(node: &Self::Of<T>, f: impl FnOnce(&T) -> R) -> R;
34+
35+
/// The address of the shared allocation, used to test pointer identity.
36+
///
37+
/// Comparing two references with [`FznRef::with`] would take two read locks
38+
/// under [`Mutable`], which deadlocks if both name the same declaration and
39+
/// a writer is waiting. Testing this first avoids that.
40+
///
41+
/// This returns a bare address rather than a pointer, matching the
42+
/// `addr` method on raw pointers, because the result is only ever compared:
43+
/// keeping it a pointer would carry provenance that no caller is entitled
44+
/// to use.
45+
fn addr<T>(node: &Self::Of<T>) -> usize;
46+
47+
/// Project a string field out of the shared declaration.
48+
///
49+
/// The result borrows from `node` for [`Immutable`], but must be cloned for
50+
/// [`Mutable`], where no borrow can escape the lock guard.
51+
///
52+
/// This exists solely for [`NamedRef::name`](crate::NamedRef::name), which
53+
/// must hand back a value that outlives the call. Anything that consumes
54+
/// the name in place should use [`FznRef::with`] instead, which allocates
55+
/// under neither marker.
56+
fn map_str<'a, T>(node: &'a Self::Of<T>, f: impl FnOnce(&T) -> &str) -> Cow<'a, str>;
57+
}
58+
59+
/// Marker selecting immutable shared declarations, represented as `Arc<T>`.
60+
///
61+
/// This is the default, and the representation produced by parsing. Reading a
62+
/// declaration is a plain pointer dereference.
63+
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
64+
pub struct Immutable;
65+
66+
/// Marker selecting interior-mutable shared declarations, represented as
67+
/// `Arc<RwLock<T>>`.
68+
///
69+
/// This allows a [`Variable`](crate::Variable) or [`Array`](crate::Array) to be
70+
/// edited after the instance has been constructed, with every reference to that
71+
/// declaration observing the change. Since `Mutable::Of<T>` is simply
72+
/// `Arc<RwLock<T>>`, edits are made through the normal lock API:
73+
///
74+
/// ```
75+
/// # use std::sync::{Arc, RwLock};
76+
/// # use flatzinc_serde::{Type, Variable, helpers::Mutable};
77+
/// # let var: Arc<RwLock<Variable<String, Mutable>>> = Arc::new(RwLock::new(Variable {
78+
/// # name: "x".to_owned(),
79+
/// # ty: Type::Int(None),
80+
/// # ann: Vec::new(),
81+
/// # defined: false,
82+
/// # introduced: false,
83+
/// # }));
84+
/// var.write().unwrap().ty = Type::Bool;
85+
/// ```
86+
///
87+
/// ### Warning
88+
///
89+
/// Reading a declaration takes a read lock, and several trait implementations
90+
/// do so implicitly: [`Display`](std::fmt::Display) and `Serialize` on any type
91+
/// that reaches a declaration, and [`Hash`], [`Ord`], and [`PartialEq`] on
92+
/// [`NamedRef`](crate::NamedRef), which read the declaration name. Holding a
93+
/// write guard on a declaration while invoking any of these on a value that
94+
/// reaches the same declaration will deadlock.
95+
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
96+
pub struct Mutable;
97+
98+
impl FznRef for Immutable {
99+
type Of<T> = Arc<T>;
100+
101+
fn new<T>(value: T) -> Self::Of<T> {
102+
Arc::new(value)
103+
}
104+
105+
fn with<T, R>(node: &Self::Of<T>, f: impl FnOnce(&T) -> R) -> R {
106+
f(node)
107+
}
108+
109+
fn addr<T>(node: &Self::Of<T>) -> usize {
110+
Arc::as_ptr(node).addr()
111+
}
112+
113+
fn map_str<'a, T>(node: &'a Self::Of<T>, f: impl FnOnce(&T) -> &str) -> Cow<'a, str> {
114+
Cow::Borrowed(f(node))
115+
}
116+
}
117+
118+
impl FznRef for Mutable {
119+
type Of<T> = Arc<RwLock<T>>;
120+
121+
fn new<T>(value: T) -> Self::Of<T> {
122+
Arc::new(RwLock::new(value))
123+
}
124+
125+
fn with<T, R>(node: &Self::Of<T>, f: impl FnOnce(&T) -> R) -> R {
126+
f(&node.read().unwrap_or_else(PoisonError::into_inner))
127+
}
128+
129+
fn addr<T>(node: &Self::Of<T>) -> usize {
130+
Arc::as_ptr(node).addr()
131+
}
132+
133+
fn map_str<'a, T>(node: &'a Self::Of<T>, f: impl FnOnce(&T) -> &str) -> Cow<'a, str> {
134+
Cow::Owned(f(&node.read().unwrap_or_else(PoisonError::into_inner)).to_owned())
135+
}
136+
}
137+
10138
/// A wrapper around an [`Arc`] that can be used as a key for collections, such
11139
/// as [`BTreeMap`](std::collections::BTreeMap),
12140
/// [`HashMap`](std::collections::HashMap), and
@@ -47,13 +175,15 @@ impl<T> Eq for ArcKey<T> {}
47175

48176
impl<T> Hash for ArcKey<T> {
49177
fn hash<H: Hasher>(&self, state: &mut H) {
50-
Arc::as_ptr(&self.key).hash(state);
178+
Arc::as_ptr(&self.key).addr().hash(state);
51179
}
52180
}
53181

54182
impl<T> Ord for ArcKey<T> {
55183
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
56-
Arc::as_ptr(&self.key).cmp(&Arc::as_ptr(&other.key))
184+
Arc::as_ptr(&self.key)
185+
.addr()
186+
.cmp(&Arc::as_ptr(&other.key).addr())
57187
}
58188
}
59189

0 commit comments

Comments
 (0)