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}