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#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
18pub struct ModelId(u32);
19
20impl ModelId {
21 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#[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#[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 #[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 #[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 #[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#[derive(Clone, Debug)]
212pub enum ExprNode {
213 Const(f64),
214 Var(VarId),
215 Param(ParamId),
216 Add(Children),
217 Mul(Children),
218 Unary(UnaryOp, ExprId),
220 Pow(ExprId, ExprId),
221 Div(ExprId, ExprId),
222 Atan2(ExprId, ExprId),
224 Min(Children),
226 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 #[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 #[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 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 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 #[inline]
339 pub fn param_value(&self, p: ParamId) -> f64 {
340 self.param_values[p.index()]
341 }
342
343 #[inline]
345 pub fn try_param_value(&self, p: ParamId) -> Option<f64> {
346 self.param_values.get(p.index()).copied()
347 }
348
349 #[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
365pub(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#[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 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 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 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 #[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 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
726pub 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 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 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 for (fork, remap) in forks.iter().zip(&remaps) {
835 for node in &fork.nodes {
836 validate_node(node, *remap);
837 }
838 }
839
840 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}