Skip to main content

oximo_expr/
arena.rs

1use std::cell::RefCell;
2use std::marker::PhantomData;
3use std::ops::{Deref, DerefMut};
4use std::sync::Arc;
5use std::sync::atomic::{AtomicU8, AtomicU32, Ordering};
6
7use parking_lot::{Mutex, MutexGuard};
8use smallvec::SmallVec;
9use thiserror::Error;
10
11static NEXT_MODEL_ID: AtomicU32 = AtomicU32::new(1);
12
13/// Stable identity shared by a model, its expression handles, and its results.
14///
15/// IDs are unique within a process. A transformed or cloned model receives a new
16/// identity even when it preserves the source model's numeric variable IDs.
17#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
18pub struct ModelId(u32);
19
20impl ModelId {
21    /// Sentinel used by detached/default result values that have not been
22    /// associated with a model.
23    pub const UNASSIGNED: Self = Self(0);
24
25    fn fresh() -> Self {
26        let mut id = NEXT_MODEL_ID.load(Ordering::Relaxed);
27        loop {
28            let next = id.checked_add(1).expect("model ID space exhausted");
29            match NEXT_MODEL_ID.compare_exchange_weak(
30                id,
31                next,
32                Ordering::Relaxed,
33                Ordering::Relaxed,
34            ) {
35                Ok(_) => return Self(id),
36                Err(actual) => id = actual,
37            }
38        }
39    }
40
41    #[must_use]
42    pub const fn get(self) -> u32 {
43        self.0
44    }
45}
46
47impl std::fmt::Display for ModelId {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        self.0.fmt(f)
50    }
51}
52
53/// An expression, indexed family, model, or result came from another model.
54#[derive(Copy, Clone, Debug, Error, PartialEq, Eq)]
55#[error("model mismatch: expected model {expected}, got model {actual}")]
56pub struct ModelMismatchError {
57    pub expected: ModelId,
58    pub actual: ModelId,
59}
60
61impl ModelMismatchError {
62    #[must_use]
63    pub const fn new(expected: ModelId, actual: ModelId) -> Self {
64        Self { expected, actual }
65    }
66}
67
68#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
69pub struct ExprId(pub u32);
70
71impl ExprId {
72    #[inline]
73    pub fn index(self) -> usize {
74        self.0 as usize
75    }
76}
77
78#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
79pub struct VarId(pub u32);
80
81impl VarId {
82    #[inline]
83    pub fn index(self) -> usize {
84        self.0 as usize
85    }
86}
87
88#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
89pub struct ParamId(pub u32);
90
91impl ParamId {
92    #[inline]
93    pub fn index(self) -> usize {
94        self.0 as usize
95    }
96}
97
98pub type Children = SmallVec<[ExprId; 4]>;
99
100/// Unary scalar functions represented by [`ExprNode::Unary`].
101///
102/// This enum is public so expression consumers can exhaustively describe the
103/// nonlinear vocabulary they support. Adding a variant is therefore a
104/// deliberate source-breaking extension for exhaustive matches.
105#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
106pub enum UnaryOp {
107    Neg,
108    Abs,
109    Sqrt,
110    Cbrt,
111    Exp,
112    Exp2,
113    Expm1,
114    Log,
115    Log2,
116    Log10,
117    Log1p,
118    Sin,
119    Cos,
120    Tan,
121    Asin,
122    Acos,
123    Atan,
124    Sinh,
125    Cosh,
126    Tanh,
127    Asinh,
128    Acosh,
129    Atanh,
130}
131
132impl UnaryOp {
133    /// Canonical modeling-language spelling used in rendering and diagnostics.
134    #[must_use]
135    pub const fn name(self) -> &'static str {
136        match self {
137            Self::Neg => "neg",
138            Self::Abs => "abs",
139            Self::Sqrt => "sqrt",
140            Self::Cbrt => "cbrt",
141            Self::Exp => "exp",
142            Self::Exp2 => "exp2",
143            Self::Expm1 => "expm1",
144            Self::Log => "log",
145            Self::Log2 => "log2",
146            Self::Log10 => "log10",
147            Self::Log1p => "log1p",
148            Self::Sin => "sin",
149            Self::Cos => "cos",
150            Self::Tan => "tan",
151            Self::Asin => "asin",
152            Self::Acos => "acos",
153            Self::Atan => "atan",
154            Self::Sinh => "sinh",
155            Self::Cosh => "cosh",
156            Self::Tanh => "tanh",
157            Self::Asinh => "asinh",
158            Self::Acosh => "acosh",
159            Self::Atanh => "atanh",
160        }
161    }
162
163    /// Apply this operation with Rust's IEEE-754 `f64` semantics.
164    #[must_use]
165    #[inline]
166    pub fn apply(self, value: f64) -> f64 {
167        match self {
168            Self::Neg => -value,
169            Self::Abs => value.abs(),
170            Self::Sqrt => value.sqrt(),
171            Self::Cbrt => value.cbrt(),
172            Self::Exp => value.exp(),
173            Self::Exp2 => value.exp2(),
174            Self::Expm1 => value.exp_m1(),
175            Self::Log => value.ln(),
176            Self::Log2 => value.log2(),
177            Self::Log10 => value.log10(),
178            Self::Log1p => value.ln_1p(),
179            Self::Sin => value.sin(),
180            Self::Cos => value.cos(),
181            Self::Tan => value.tan(),
182            Self::Asin => value.asin(),
183            Self::Acos => value.acos(),
184            Self::Atan => value.atan(),
185            Self::Sinh => value.sinh(),
186            Self::Cosh => value.cosh(),
187            Self::Tanh => value.tanh(),
188            Self::Asinh => value.asinh(),
189            Self::Acosh => value.acosh(),
190            Self::Atanh => value.atanh(),
191        }
192    }
193
194    /// Whether this operation is nonsmooth on its ordinary real domain.
195    #[must_use]
196    pub const fn is_nonsmooth(self) -> bool {
197        matches!(self, Self::Abs)
198    }
199}
200
201impl std::fmt::Display for UnaryOp {
202    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
203        f.write_str(self.name())
204    }
205}
206
207/// Here we use a linear fast-path: `sum(coeff * var) + constant`.
208/// Built by the operator overloads when all children are linear,
209/// so LP/MILP construction never walks an `Add(Mul(Const, Var), ...)` tree.
210
211#[derive(Clone, Debug)]
212pub enum ExprNode {
213    Const(f64),
214    Var(VarId),
215    Param(ParamId),
216    Add(Children),
217    Mul(Children),
218    /// Unary scalar operation.
219    Unary(UnaryOp, ExprId),
220    Pow(ExprId, ExprId),
221    Div(ExprId, ExprId),
222    /// Two-argument arctangent in `y.atan2(x)` order.
223    Atan2(ExprId, ExprId),
224    /// N-ary minimum.
225    Min(Children),
226    /// N-ary maximum.
227    Max(Children),
228    Linear {
229        coeffs: Vec<(VarId, f64)>,
230        constant: f64,
231    },
232}
233
234#[derive(Clone, Debug, Default)]
235pub struct ExprArena {
236    nodes: Arc<Vec<ExprNode>>,
237    param_values: Arc<Vec<f64>>,
238}
239
240impl ExprArena {
241    pub fn new() -> Self {
242        Self::default()
243    }
244
245    pub fn with_capacity(cap: usize) -> Self {
246        Self { nodes: Arc::new(Vec::with_capacity(cap)), param_values: Arc::new(Vec::new()) }
247    }
248
249    /// Clone this arena while reserving room for nodes that will immediately
250    /// be appended to the copy.
251    #[doc(hidden)]
252    #[must_use]
253    pub fn __clone_with_additional_capacity(&self, additional_nodes: usize) -> Self {
254        let mut nodes = Vec::with_capacity(self.nodes.len().saturating_add(additional_nodes));
255        nodes.extend_from_slice(&self.nodes);
256        Self { nodes: Arc::new(nodes), param_values: Arc::new(self.param_values.as_ref().clone()) }
257    }
258
259    /// Reserve room for additional expression nodes without changing IDs.
260    #[doc(hidden)]
261    pub fn __reserve_nodes(&mut self, additional: usize) {
262        Arc::make_mut(&mut self.nodes).reserve(additional);
263    }
264
265    #[doc(hidden)]
266    pub(crate) fn __append_nodes(&mut self, nodes: Vec<ExprNode>) {
267        Arc::make_mut(&mut self.nodes).extend(nodes);
268    }
269
270    #[inline]
271    pub fn len(&self) -> usize {
272        self.nodes.len()
273    }
274
275    #[inline]
276    pub fn is_empty(&self) -> bool {
277        self.nodes.is_empty()
278    }
279
280    /// # Panics
281    ///
282    /// Panics if the number of expressions exceeds `u32::MAX` (expression arena overflow).
283    pub fn push(&mut self, node: ExprNode) -> ExprId {
284        let id = ExprId(u32::try_from(self.nodes.len()).expect("expression arena overflow"));
285        Arc::make_mut(&mut self.nodes).push(node);
286        id
287    }
288
289    #[inline]
290    pub fn get(&self, id: ExprId) -> &ExprNode {
291        &self.nodes[id.index()]
292    }
293
294    #[inline]
295    pub fn get_mut(&mut self, id: ExprId) -> &mut ExprNode {
296        &mut Arc::make_mut(&mut self.nodes)[id.index()]
297    }
298
299    pub fn nodes(&self) -> &[ExprNode] {
300        &self.nodes
301    }
302
303    pub fn constant(&mut self, v: f64) -> ExprId {
304        self.push(ExprNode::Const(v))
305    }
306
307    pub fn var(&mut self, v: VarId) -> ExprId {
308        self.push(ExprNode::Var(v))
309    }
310
311    pub fn param(&mut self, p: ParamId) -> ExprId {
312        self.push(ExprNode::Param(p))
313    }
314
315    /// Allocate a fresh parameter initialized to `value`, returning its
316    /// [`ParamId`]. Push a [`ExprNode::Param`] with [`Self::param`] to reference
317    /// it inside an expression.
318    ///
319    /// # Panics
320    ///
321    /// Panics if the number of parameters exceeds `u32::MAX`.
322    pub fn new_param(&mut self, value: f64) -> ParamId {
323        let id = ParamId(u32::try_from(self.param_values.len()).expect("parameter arena overflow"));
324        Arc::make_mut(&mut self.param_values).push(value);
325        id
326    }
327
328    #[inline]
329    pub fn num_params(&self) -> usize {
330        self.param_values.len()
331    }
332
333    /// Current value bound to parameter `p`.
334    ///
335    /// # Panics
336    ///
337    /// Panics if `p` was not allocated by [`Self::new_param`] on this arena.
338    #[inline]
339    pub fn param_value(&self, p: ParamId) -> f64 {
340        self.param_values[p.index()]
341    }
342
343    /// Look up the value of `p`, returning `None` if `p` is out of range.
344    #[inline]
345    pub fn try_param_value(&self, p: ParamId) -> Option<f64> {
346        self.param_values.get(p.index()).copied()
347    }
348
349    /// Re-bind parameter `p` to `value`. Takes effect on the next extraction or
350    /// evaluation.
351    ///
352    /// # Panics
353    ///
354    /// Panics if `p` was not allocated by [`Self::new_param`] on this arena.
355    #[inline]
356    pub fn set_param_value(&mut self, p: ParamId, value: f64) {
357        Arc::make_mut(&mut self.param_values)[p.index()] = value;
358    }
359
360    pub fn linear(&mut self, coeffs: Vec<(VarId, f64)>, constant: f64) -> ExprId {
361        self.push(ExprNode::Linear { coeffs, constant })
362    }
363}
364
365/// Read/push surface shared by the canonical and worker-local arenas.
366pub(crate) trait ArenaAccess {
367    fn get(&self, id: ExprId) -> &ExprNode;
368    fn param_value(&self, id: ParamId) -> f64;
369    fn push(&mut self, node: ExprNode) -> ExprId;
370
371    fn constant(&mut self, value: f64) -> ExprId {
372        self.push(ExprNode::Const(value))
373    }
374
375    fn var(&mut self, id: VarId) -> ExprId {
376        self.push(ExprNode::Var(id))
377    }
378}
379
380impl ArenaAccess for ExprArena {
381    fn get(&self, id: ExprId) -> &ExprNode {
382        self.get(id)
383    }
384
385    fn param_value(&self, id: ParamId) -> f64 {
386        self.param_value(id)
387    }
388
389    fn push(&mut self, node: ExprNode) -> ExprId {
390        self.push(node)
391    }
392}
393
394#[derive(Clone, Debug, Default)]
395#[doc(hidden)]
396pub struct FrozenExprArena {
397    nodes: Arc<Vec<ExprNode>>,
398    param_values: Arc<Vec<f64>>,
399}
400
401impl FrozenExprArena {
402    fn len(&self) -> usize {
403        self.nodes.len()
404    }
405}
406
407#[derive(Debug)]
408struct ForkedExprArena {
409    base: FrozenExprArena,
410    nodes: Vec<ExprNode>,
411}
412
413impl ForkedExprArena {
414    fn new(base: FrozenExprArena) -> Self {
415        Self { base, nodes: Vec::new() }
416    }
417}
418
419impl ArenaAccess for ForkedExprArena {
420    fn get(&self, id: ExprId) -> &ExprNode {
421        let index = id.index();
422        if index < self.base.nodes.len() {
423            &self.base.nodes[index]
424        } else {
425            &self.nodes[index - self.base.nodes.len()]
426        }
427    }
428
429    fn param_value(&self, id: ParamId) -> f64 {
430        self.base.param_values[id.index()]
431    }
432
433    fn push(&mut self, node: ExprNode) -> ExprId {
434        let index =
435            self.base.nodes.len().checked_add(self.nodes.len()).expect("expression arena overflow");
436        let id = ExprId(u32::try_from(index).expect("expression arena overflow"));
437        self.nodes.push(node);
438        id
439    }
440}
441
442struct ActiveFork {
443    arena_key: usize,
444    arena: ForkedExprArena,
445}
446
447thread_local! {
448    static ACTIVE_FORKS: RefCell<Vec<ActiveFork>> = const { RefCell::new(Vec::new()) };
449    static HELD_WRITE_GUARDS: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
450}
451
452const WRITE_REENTRY_MESSAGE: &str = "expression arena write guard re-entered on the same thread";
453const BATCH_ACTIVE: u8 = 1 << 0;
454const WRITE_GUARD_ACTIVE: u8 = 1 << 1;
455
456struct InstalledWriteGuard {
457    arena_key: usize,
458}
459
460impl InstalledWriteGuard {
461    fn new(arena_key: usize) -> Self {
462        HELD_WRITE_GUARDS.with(|guards| {
463            let mut guards = guards.borrow_mut();
464            assert!(!guards.contains(&arena_key), "{WRITE_REENTRY_MESSAGE}");
465            guards.push(arena_key);
466        });
467        Self { arena_key }
468    }
469}
470
471impl Drop for InstalledWriteGuard {
472    fn drop(&mut self) {
473        HELD_WRITE_GUARDS.with(|guards| {
474            let mut guards = guards.borrow_mut();
475            let position = guards
476                .iter()
477                .rposition(|&arena_key| arena_key == self.arena_key)
478                .expect("expression arena write guard routing state missing");
479            guards.remove(position);
480        });
481    }
482}
483
484/// Parallel-safe owner used by [`crate::Expr`] handles.
485///
486/// Ordinary construction writes the canonical arena. Indexed batch builders
487/// temporarily route expression operations to worker-local forks and merge the
488/// resulting nodes in deterministic index order.
489#[derive(Debug)]
490pub struct ExprArenaCell {
491    model_id: ModelId,
492    inner: Mutex<ExprArena>,
493    state: AtomicU8,
494}
495
496impl ExprArenaCell {
497    pub fn new(arena: ExprArena) -> Self {
498        Self { model_id: ModelId::fresh(), inner: Mutex::new(arena), state: AtomicU8::new(0) }
499    }
500
501    #[inline]
502    #[must_use]
503    pub const fn model_id(&self) -> ModelId {
504        self.model_id
505    }
506
507    /// Take a cheap immutable snapshot of the canonical arena.
508    ///
509    /// The returned value is not a live read lock.
510    /// Later writes are isolated by copy-on-write and are not
511    /// visible through this snapshot.
512    ///
513    /// # Panics
514    ///
515    /// Panics if an indexed batch is active. Snapshot reads cannot observe a
516    /// worker-local fork and would otherwise wait on the canonical arena lock
517    /// held by the batch.
518    pub fn borrow(&self) -> ExprArenaSnapshot<'_> {
519        let state = self.state.load(Ordering::Acquire);
520        assert!(
521            state & BATCH_ACTIVE == 0,
522            "expression arena snapshot requested during an active indexed batch"
523        );
524        self.assert_no_write_reentry(state);
525        ExprArenaSnapshot { arena: self.inner.lock().clone(), _cell: PhantomData }
526    }
527
528    /// Mutably borrow the canonical expression arena.
529    ///
530    /// # Panics
531    ///
532    /// Panics if an indexed batch is active, because its worker-local forks
533    /// must see a stable canonical snapshot until they are merged, or if this
534    /// thread already holds a write guard for this arena.
535    pub fn borrow_mut(&self) -> ExprArenaWriteGuard<'_> {
536        let state = self.state.load(Ordering::Acquire);
537        assert!(
538            state & BATCH_ACTIVE == 0,
539            "expression arena accessed outside its indexed worker during an active batch"
540        );
541        self.assert_no_write_reentry(state);
542        let guard = self.inner.lock();
543        self.state.fetch_or(WRITE_GUARD_ACTIVE, Ordering::Release);
544        ExprArenaWriteGuard { cell: self, _installed: InstalledWriteGuard::new(self.key()), guard }
545    }
546
547    fn key(&self) -> usize {
548        std::ptr::from_ref(self) as usize
549    }
550
551    #[inline]
552    fn assert_no_write_reentry(&self, state: u8) {
553        // Avoid touching thread-local state unless a public guard is alive.
554        // If another thread owns that guard, the lookup is harmless and we
555        // preserve the mutex's normal blocking behavior.
556        if state & WRITE_GUARD_ACTIVE != 0 {
557            let key = self.key();
558            HELD_WRITE_GUARDS.with(|guards| {
559                assert!(!guards.borrow().contains(&key), "{WRITE_REENTRY_MESSAGE}");
560            });
561        }
562    }
563
564    pub(crate) fn with_ref<R>(&self, f: impl FnOnce(&dyn ArenaAccess) -> R) -> R {
565        let state = self.state.load(Ordering::Acquire);
566        if state & BATCH_ACTIVE == 0 {
567            self.assert_no_write_reentry(state);
568            let arena = self.inner.lock();
569            return f(&*arena);
570        }
571
572        let mut f = Some(f);
573        let local = ACTIVE_FORKS.with(|forks| {
574            let forks = forks.borrow();
575            forks
576                .iter()
577                .rfind(|fork| fork.arena_key == self.key())
578                .map(|fork| f.take().expect("arena callback already consumed")(&fork.arena))
579        });
580        if let Some(result) = local {
581            return result;
582        }
583        panic!("expression arena accessed outside its indexed worker during an active batch");
584    }
585
586    pub(crate) fn with_mut<R>(&self, f: impl FnOnce(&mut dyn ArenaAccess) -> R) -> R {
587        let state = self.state.load(Ordering::Acquire);
588        if state & BATCH_ACTIVE == 0 {
589            self.assert_no_write_reentry(state);
590            let mut arena = self.inner.lock();
591            return f(&mut *arena);
592        }
593
594        let mut f = Some(f);
595        let local = ACTIVE_FORKS.with(|forks| {
596            let mut forks = forks.borrow_mut();
597            forks
598                .iter_mut()
599                .rfind(|fork| fork.arena_key == self.key())
600                .map(|fork| f.take().expect("arena callback already consumed")(&mut fork.arena))
601        });
602        if let Some(result) = local {
603            return result;
604        }
605        panic!("expression arena accessed outside its indexed worker during an active batch");
606    }
607
608    #[doc(hidden)]
609    pub fn __begin_batch(&self) -> ExprArenaBatchGuard<'_> {
610        let state = self.state.load(Ordering::Acquire);
611        self.assert_no_write_reentry(state);
612        assert!(
613            self.state.fetch_or(BATCH_ACTIVE, Ordering::AcqRel) & BATCH_ACTIVE == 0,
614            "nested expression arena batches are not supported"
615        );
616        ExprArenaBatchGuard { cell: self, arena: self.inner.lock() }
617    }
618
619    /// Run a worker callback against a private fork of `base`.
620    ///
621    /// This is an internal batching primitive. The callback must not start a
622    /// nested batch for this arena or access the arena from another thread.
623    /// Such access is rejected while the batch is active.
624    #[doc(hidden)]
625    pub fn __with_fork<R>(&self, base: FrozenExprArena, f: impl FnOnce() -> R) -> ForkOutput<R> {
626        let key = self.key();
627        ACTIVE_FORKS.with(|forks| {
628            let mut forks = forks.borrow_mut();
629            assert!(
630                !forks.iter().any(|fork| fork.arena_key == key),
631                "nested expression forks for one arena are not supported"
632            );
633            forks.push(ActiveFork { arena_key: key, arena: ForkedExprArena::new(base) });
634        });
635        let mut installed = InstalledFork { arena_key: key, armed: true };
636        let value = f();
637        let arena = installed.take();
638        ForkOutput { value, base: arena.base, nodes: arena.nodes }
639    }
640}
641
642impl Default for ExprArenaCell {
643    fn default() -> Self {
644        Self::new(ExprArena::default())
645    }
646}
647
648struct InstalledFork {
649    arena_key: usize,
650    armed: bool,
651}
652
653impl InstalledFork {
654    fn take(&mut self) -> ForkedExprArena {
655        self.armed = false;
656        ACTIVE_FORKS.with(|forks| {
657            let mut forks = forks.borrow_mut();
658            let position = forks
659                .iter()
660                .rposition(|fork| fork.arena_key == self.arena_key)
661                .expect("active expression fork missing");
662            forks.remove(position).arena
663        })
664    }
665}
666
667impl Drop for InstalledFork {
668    fn drop(&mut self) {
669        if self.armed {
670            ACTIVE_FORKS.with(|forks| {
671                let mut forks = forks.borrow_mut();
672                if let Some(position) =
673                    forks.iter().rposition(|fork| fork.arena_key == self.arena_key)
674                {
675                    forks.remove(position);
676                }
677            });
678        }
679    }
680}
681
682#[derive(Debug)]
683#[doc(hidden)]
684pub struct ForkOutput<T> {
685    pub value: T,
686    base: FrozenExprArena,
687    nodes: Vec<ExprNode>,
688}
689
690#[derive(Copy, Clone, Debug)]
691#[doc(hidden)]
692pub struct ExprIdRemap {
693    local_base: usize,
694    local_len: usize,
695    global_base: usize,
696}
697
698impl ExprIdRemap {
699    fn validate(self, id: ExprId) {
700        let index = id.index();
701        if index >= self.local_base {
702            assert!(
703                index - self.local_base < self.local_len,
704                "expression fork returned an unknown node"
705            );
706        }
707    }
708
709    /// Translate an expression ID produced by one worker fork to its merged ID.
710    ///
711    /// # Panics
712    ///
713    /// Panics if `id` refers neither to the fork's canonical base nor to a node
714    /// created by that fork, or if the remapped ID exceeds `u32::MAX`.
715    pub fn apply(self, id: ExprId) -> ExprId {
716        let index = id.index();
717        if index < self.local_base {
718            return id;
719        }
720        self.validate(id);
721        let local = index - self.local_base;
722        ExprId(u32::try_from(self.global_base + local).expect("expression arena overflow"))
723    }
724}
725
726/// Immutable copy-on-write snapshot of an [`ExprArenaCell`].
727pub struct ExprArenaSnapshot<'a> {
728    arena: ExprArena,
729    _cell: PhantomData<&'a ExprArenaCell>,
730}
731
732impl std::fmt::Debug for ExprArenaSnapshot<'_> {
733    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
734        self.arena.fmt(f)
735    }
736}
737
738impl Deref for ExprArenaSnapshot<'_> {
739    type Target = ExprArena;
740
741    fn deref(&self) -> &Self::Target {
742        &self.arena
743    }
744}
745
746pub struct ExprArenaWriteGuard<'a> {
747    // Remove the thread-local marker before unlocking the mutex. There is no
748    // user code between field drops, so another operation cannot observe the
749    // short transition.
750    cell: &'a ExprArenaCell,
751    _installed: InstalledWriteGuard,
752    guard: MutexGuard<'a, ExprArena>,
753}
754
755impl Drop for ExprArenaWriteGuard<'_> {
756    fn drop(&mut self) {
757        self.cell.state.fetch_and(!WRITE_GUARD_ACTIVE, Ordering::Release);
758    }
759}
760
761impl std::fmt::Debug for ExprArenaWriteGuard<'_> {
762    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
763        self.guard.fmt(f)
764    }
765}
766
767impl Deref for ExprArenaWriteGuard<'_> {
768    type Target = ExprArena;
769
770    fn deref(&self) -> &Self::Target {
771        &self.guard
772    }
773}
774
775impl DerefMut for ExprArenaWriteGuard<'_> {
776    fn deref_mut(&mut self) -> &mut Self::Target {
777        &mut self.guard
778    }
779}
780
781#[doc(hidden)]
782pub struct ExprArenaBatchGuard<'a> {
783    cell: &'a ExprArenaCell,
784    arena: MutexGuard<'a, ExprArena>,
785}
786
787impl std::fmt::Debug for ExprArenaBatchGuard<'_> {
788    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
789        f.debug_struct("ExprArenaBatchGuard").field("arena", &self.arena).finish()
790    }
791}
792
793impl ExprArenaBatchGuard<'_> {
794    pub fn snapshot(&self) -> FrozenExprArena {
795        FrozenExprArena {
796            nodes: Arc::clone(&self.arena.nodes),
797            param_values: Arc::clone(&self.arena.param_values),
798        }
799    }
800
801    /// Append worker-fork nodes in slice order and return each fork's ID map.
802    ///
803    /// # Panics
804    ///
805    /// Panics if a fork was created from a different snapshot or the merged
806    /// expression arena would exceed `u32::MAX` nodes.
807    pub fn merge<T>(&mut self, forks: &mut [ForkOutput<T>]) -> Vec<ExprIdRemap> {
808        let initial_len = self.arena.len();
809        let additional = forks.iter().map(|fork| fork.nodes.len()).sum::<usize>();
810        let final_len =
811            self.arena.len().checked_add(additional).expect("expression arena overflow");
812        if final_len > 0 {
813            u32::try_from(final_len - 1).expect("expression arena overflow");
814        }
815        let mut remaps = Vec::with_capacity(forks.len());
816        let mut global_base = initial_len;
817        for fork in forks.iter() {
818            assert!(
819                Arc::ptr_eq(&fork.base.nodes, &self.arena.nodes)
820                    && Arc::ptr_eq(&fork.base.param_values, &self.arena.param_values),
821                "expression fork used a stale arena snapshot"
822            );
823            remaps.push(ExprIdRemap {
824                local_base: fork.base.len(),
825                local_len: fork.nodes.len(),
826                global_base,
827            });
828            global_base += fork.nodes.len();
829        }
830
831        // Validate every child reference before changing the canonical arena.
832        // This keeps merge failure-atomic even if a worker returned an ID that
833        // was not part of its fork.
834        for (fork, remap) in forks.iter().zip(&remaps) {
835            for node in &fork.nodes {
836                validate_node(node, *remap);
837            }
838        }
839
840        // The remaps retain the fork base lengths needed by remap_node and
841        // ExprIdRemap::apply. Release the base snapshots before reserving so
842        // Arc::make_mut can extend the canonical node vector in place.
843        for fork in forks.iter_mut() {
844            drop(std::mem::take(&mut fork.base));
845        }
846
847        self.arena.__reserve_nodes(additional);
848        for (fork, remap) in forks.iter_mut().zip(&remaps) {
849            for node in &mut fork.nodes {
850                remap_node(node, *remap);
851            }
852            self.arena.__append_nodes(std::mem::take(&mut fork.nodes));
853        }
854        remaps
855    }
856}
857
858fn validate_node(node: &ExprNode, remap: ExprIdRemap) {
859    match node {
860        ExprNode::Add(children)
861        | ExprNode::Mul(children)
862        | ExprNode::Min(children)
863        | ExprNode::Max(children) => {
864            for child in children {
865                remap.validate(*child);
866            }
867        }
868        ExprNode::Unary(_, child) => remap.validate(*child),
869        ExprNode::Pow(left, right) | ExprNode::Div(left, right) | ExprNode::Atan2(left, right) => {
870            remap.validate(*left);
871            remap.validate(*right);
872        }
873        ExprNode::Const(_) | ExprNode::Var(_) | ExprNode::Param(_) | ExprNode::Linear { .. } => {}
874    }
875}
876
877impl Drop for ExprArenaBatchGuard<'_> {
878    fn drop(&mut self) {
879        self.cell.state.fetch_and(!BATCH_ACTIVE, Ordering::Release);
880    }
881}
882
883fn remap_node(node: &mut ExprNode, remap: ExprIdRemap) {
884    match node {
885        ExprNode::Add(children)
886        | ExprNode::Mul(children)
887        | ExprNode::Min(children)
888        | ExprNode::Max(children) => {
889            for child in children {
890                *child = remap.apply(*child);
891            }
892        }
893        ExprNode::Unary(_, child) => *child = remap.apply(*child),
894        ExprNode::Pow(left, right) | ExprNode::Div(left, right) | ExprNode::Atan2(left, right) => {
895            *left = remap.apply(*left);
896            *right = remap.apply(*right);
897        }
898        ExprNode::Const(_) | ExprNode::Var(_) | ExprNode::Param(_) | ExprNode::Linear { .. } => {}
899    }
900}
901
902#[cfg(test)]
903mod tests {
904    use std::panic::{AssertUnwindSafe, catch_unwind};
905
906    use super::*;
907    use crate::{Expr, evaluate, extract_linear};
908
909    fn assert_send_sync<T: Send + Sync>() {}
910
911    #[test]
912    fn expression_handles_are_send_and_sync() {
913        assert_send_sync::<Expr<'static>>();
914        assert_send_sync::<ExprArenaCell>();
915    }
916
917    #[test]
918    fn write_guard_reentry_panics_and_cleans_up_instead_of_blocking() {
919        let cell = ExprArenaCell::new(ExprArena::new());
920        let x = Expr::from_var(&cell, VarId(0));
921
922        let result = catch_unwind(AssertUnwindSafe(|| {
923            let _guard = cell.borrow_mut();
924            std::hint::black_box(x + 1.0);
925        }));
926
927        assert!(result.is_err());
928        let constant = Expr::constant(&cell, 4.0);
929        assert!(matches!(cell.borrow().get(constant.id), ExprNode::Const(4.0)));
930    }
931
932    #[test]
933    fn forked_nodes_merge_and_remap_in_order() {
934        let cell = ExprArenaCell::new(ExprArena::new());
935        let x = Expr::from_var(&cell, VarId(0));
936        let y = Expr::from_var(&cell, VarId(1));
937        let mut batch = cell.__begin_batch();
938        let snapshot = batch.snapshot();
939        let mut forks = vec![cell.__with_fork(snapshot.clone(), || {
940            let nonlinear = (x.sin() * y.cos()).exp();
941            let quotient = nonlinear / (x.abs() + 2.0);
942            quotient.powi(2)
943        })];
944        drop(snapshot);
945        let remaps = batch.merge(&mut forks);
946        let root = remaps[0].apply(forks[0].value.id);
947        drop(batch);
948
949        let arena = cell.borrow();
950        let values: &[f64] = &[0.5, 0.25];
951        let value = evaluate(&arena, root, &values).unwrap();
952        let expected = ((0.5_f64.sin() * 0.25_f64.cos()).exp() / 2.5).powi(2);
953        assert!((value - expected).abs() < 1e-12);
954    }
955
956    #[test]
957    fn merge_releases_fork_base_before_reserving() {
958        let cell = ExprArenaCell::new(ExprArena::new());
959        let mut batch = cell.__begin_batch();
960        let snapshot = batch.snapshot();
961        let mut forks = vec![cell.__with_fork(snapshot.clone(), || Expr::constant(&cell, 1.0))];
962        drop(snapshot);
963        let nodes = Arc::as_ptr(&batch.arena.nodes);
964
965        let remaps = batch.merge(&mut forks);
966
967        assert_eq!(Arc::as_ptr(&batch.arena.nodes), nodes);
968        assert_eq!(remaps[0].local_base, 0);
969        assert_eq!(remaps[0].local_len, 1);
970    }
971
972    #[test]
973    fn invalid_later_fork_does_not_append_earlier_nodes() {
974        let cell = ExprArenaCell::new(ExprArena::new());
975        let mut batch = cell.__begin_batch();
976        let snapshot = batch.snapshot();
977        let mut forks = vec![
978            cell.__with_fork(snapshot.clone(), || Expr::constant(&cell, 1.0)),
979            cell.__with_fork(snapshot, || Expr::constant(&cell, 2.0)),
980        ];
981        forks[1].nodes.push(ExprNode::Unary(UnaryOp::Neg, ExprId(u32::MAX)));
982        assert!(catch_unwind(AssertUnwindSafe(|| batch.merge(&mut forks))).is_err());
983        assert_eq!(batch.arena.len(), 0);
984        assert_eq!(forks[0].nodes.len(), 1);
985        assert_eq!(forks[1].nodes.len(), 2);
986    }
987
988    #[test]
989    fn merged_linear_node_still_supports_borrowed_extraction() {
990        let cell = ExprArenaCell::new(ExprArena::new());
991        let x = Expr::from_var(&cell, VarId(0));
992        let mut batch = cell.__begin_batch();
993        let snapshot = batch.snapshot();
994        let mut forks = vec![cell.__with_fork(snapshot.clone(), || 3.0 * x + 2.0)];
995        drop(snapshot);
996        let remaps = batch.merge(&mut forks);
997        let root = remaps[0].apply(forks[0].value.id);
998        drop(batch);
999
1000        let arena = cell.borrow();
1001        let terms = extract_linear(&arena, root).unwrap();
1002        assert!(matches!(terms.coeffs, std::borrow::Cow::Borrowed(_)));
1003        assert_eq!(terms.coeffs.as_ref(), &[(VarId(0), 3.0)]);
1004        assert!((terms.constant - 2.0).abs() < f64::EPSILON);
1005    }
1006
1007    #[test]
1008    fn panicking_fork_cleans_up_routing_state() {
1009        let cell = ExprArenaCell::new(ExprArena::new());
1010        let result = catch_unwind(AssertUnwindSafe(|| {
1011            let batch = cell.__begin_batch();
1012            let snapshot = batch.snapshot();
1013            let _: ForkOutput<()> = cell.__with_fork(snapshot, || panic!("worker failed"));
1014            drop(batch);
1015        }));
1016        assert!(result.is_err());
1017        let constant = Expr::constant(&cell, 4.0);
1018        assert!(matches!(cell.borrow().get(constant.id), ExprNode::Const(4.0)));
1019    }
1020
1021    #[test]
1022    #[should_panic(expected = "snapshot requested during an active indexed batch")]
1023    fn snapshot_read_during_batch_is_rejected_instead_of_blocking() {
1024        let cell = ExprArenaCell::new(ExprArena::new());
1025        let _batch = cell.__begin_batch();
1026        let _snapshot = cell.borrow();
1027    }
1028
1029    #[test]
1030    #[should_panic(expected = "stale arena snapshot")]
1031    fn merge_rejects_same_length_but_different_snapshot() {
1032        let cell = ExprArenaCell::new(ExprArena::new());
1033        let other = ExprArenaCell::new(ExprArena::new());
1034        let batch = cell.__begin_batch();
1035        let other_batch = other.__begin_batch();
1036        let stale = other_batch.snapshot();
1037        drop(other_batch);
1038        let mut forks = vec![cell.__with_fork(stale, || Expr::constant(&cell, 1.0))];
1039        let mut batch = batch;
1040        let _ = batch.merge(&mut forks);
1041    }
1042}