Skip to main content

citum_engine/processor/
run_state.rs

1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Per-render-run mutable state for [`Processor`](super::Processor).
7//!
8//! See `docs/specs/EXPLICIT_RENDER_RUN_STATE.md` for the full design.
9//!
10//! `RunState` owns the citation-order-dependent state that used to live as
11//! `RefCell` fields directly on `Processor`: citation numbers, the cited-ID
12//! set, dynamic (cite-time) compound-group membership, and first-note
13//! tracking. It is created fresh via [`Processor::begin_run`](super::Processor::begin_run),
14//! populated in citation-processing order by registration methods that take
15//! `&mut RunState`, and then finalized into a [`FinalizedRun`] so that
16//! bibliography rendering — which takes `&FinalizedRun` — cannot be called
17//! before registration is complete. That ordering contract is enforced by
18//! the type system: there is no way to construct a `FinalizedRun` other than
19//! through [`RunState::finalize`]. Citation rendering itself stays
20//! `&mut RunState`-threaded rather than moving to `&FinalizedRun`:
21//! registration and rendering are interleaved per citation (see
22//! `citation.rs`'s module docs), so a citation can only be rendered as part
23//! of the same in-progress run that is registering it.
24//!
25//! Two fields, `citation_numbers` and `first_note_by_id`, stay behind a
26//! `RwLock` even inside `RunState`/`FinalizedRun`: the render layer
27//! (`Renderer::get_or_assign_citation_number`) lazily assigns a citation
28//! number the first time a reference is rendered, which is a monotonic,
29//! assign-once operation, not a read. This does not weaken the ordering
30//! contract this type adds — it only means "render before registration is
31//! complete" is a compile error, not that rendering can never touch interior
32//! state. `RwLock` (rather than `RefCell`) is required so that `FinalizedRun`
33//! is `Sync` and bibliography entries can render across threads (see
34//! `docs/specs/PARALLEL_BIBLIOGRAPHY_RENDERING.md`); lock poisoning is
35//! recovered from rather than propagated, since a panicking reader/writer
36//! does not invalidate the numbering data already in the map.
37
38use indexmap::IndexMap;
39use std::collections::{HashMap, HashSet};
40use std::sync::RwLock;
41
42/// Mutable per-render-run state: citation numbering, cite-order tracking,
43/// and dynamic (cite-time) compound-group membership.
44///
45/// Create with [`Processor::begin_run`](super::Processor::begin_run);
46/// populate via registration methods (`&self, &mut RunState`); consume via
47/// [`finalize`](RunState::finalize) before rendering.
48///
49/// `Clone` is provided for long-lived callers (e.g. the FFI session handle)
50/// that need to render a bibliography from a snapshot of the current state
51/// without pausing ongoing citation registration on the original `RunState`.
52/// Implemented by hand (rather than derived) because `RwLock<T>` is not
53/// `Clone` even when `T` is; the impl below clones the locked contents into
54/// fresh locks instead.
55#[derive(Debug)]
56pub struct RunState {
57    /// Citation numbers assigned to references (for numeric styles).
58    ///
59    /// Stays `RwLock`: the render layer lazily assigns numbers the first
60    /// time a reference is rendered (see module docs).
61    pub(super) citation_numbers: RwLock<HashMap<String, usize>>,
62    /// First note number in which each reference was cited (note styles only).
63    ///
64    /// Stays `RwLock` for the same reason as `citation_numbers`.
65    pub(super) first_note_by_id: RwLock<HashMap<String, u32>>,
66    /// IDs of items that were cited in a visible way.
67    pub(super) cited_ids: HashSet<String>,
68    /// Compound numeric groups: citation number → ordered ref IDs in the group.
69    pub(super) compound_groups: IndexMap<usize, Vec<String>>,
70    /// Dynamic equivalent of `Processor::compound_set_by_ref` for cite-time groups.
71    ///
72    /// Maps each dynamic group member (head and tails) to the head's ref ID,
73    /// which acts as the set identifier. Merged with static data at render time.
74    pub(super) dynamic_compound_set_by_ref: HashMap<String, String>,
75    /// Dynamic equivalent of `Processor::compound_member_index` for cite-time groups.
76    ///
77    /// Maps each dynamic group member to its 0-based position within the group.
78    /// Merged with static data at render time.
79    pub(super) dynamic_compound_member_index: HashMap<String, usize>,
80    /// Dynamic equivalent of `Processor::compound_sets` for cite-time groups.
81    ///
82    /// Maps each dynamic group's head ref ID to the ordered list of all members.
83    /// Merged with static `compound_sets` at render time so sub-label lookup works.
84    pub(super) dynamic_compound_sets: IndexMap<String, Vec<String>>,
85}
86
87impl Clone for RunState {
88    /// Snapshot the current state, including the interior-mutable citation
89    /// numbers and first-note map, into a fresh, independently-lockable copy.
90    fn clone(&self) -> Self {
91        Self {
92            citation_numbers: RwLock::new(
93                self.citation_numbers
94                    .read()
95                    .unwrap_or_else(std::sync::PoisonError::into_inner)
96                    .clone(),
97            ),
98            first_note_by_id: RwLock::new(
99                self.first_note_by_id
100                    .read()
101                    .unwrap_or_else(std::sync::PoisonError::into_inner)
102                    .clone(),
103            ),
104            cited_ids: self.cited_ids.clone(),
105            compound_groups: self.compound_groups.clone(),
106            dynamic_compound_set_by_ref: self.dynamic_compound_set_by_ref.clone(),
107            dynamic_compound_member_index: self.dynamic_compound_member_index.clone(),
108            dynamic_compound_sets: self.dynamic_compound_sets.clone(),
109        }
110    }
111}
112
113impl Default for RunState {
114    fn default() -> Self {
115        Self {
116            citation_numbers: RwLock::new(HashMap::new()),
117            first_note_by_id: RwLock::new(HashMap::new()),
118            cited_ids: HashSet::new(),
119            compound_groups: IndexMap::new(),
120            dynamic_compound_set_by_ref: HashMap::new(),
121            dynamic_compound_member_index: HashMap::new(),
122            dynamic_compound_sets: IndexMap::new(),
123        }
124    }
125}
126
127impl RunState {
128    /// Complete the registration phase, producing a [`FinalizedRun`].
129    ///
130    /// This is a plain newtype wrap with no additional computation; it
131    /// exists purely as a compile-time marker that registration for this
132    /// run is considered complete, so rendering methods that require
133    /// citation order/numbering can require `&FinalizedRun` instead of
134    /// `&RunState`.
135    #[must_use]
136    pub fn finalize(self) -> FinalizedRun {
137        FinalizedRun(self)
138    }
139}
140
141/// A [`RunState`] that has completed the registration phase.
142///
143/// Rendering methods that depend on cite order or citation numbers (e.g.
144/// bibliography rendering, citation-collapse across a document) take
145/// `&FinalizedRun` rather than `&RunState`, so calling them before
146/// registration is complete is a compile error.
147#[derive(Debug)]
148pub struct FinalizedRun(pub(super) RunState);
149
150impl FinalizedRun {
151    /// Borrow the underlying run state.
152    ///
153    /// Available to processor submodules that need read access to run
154    /// fields during rendering (e.g. `cited_ids`, `compound_groups`).
155    pub(super) fn state(&self) -> &RunState {
156        &self.0
157    }
158}