Skip to main content

cranpose_core/
source_trace.rs

1//! Optional composition origins for development tools.
2
3/// A composable definition active when a layout node was emitted.
4#[derive(Clone, Debug, PartialEq, Eq)]
5pub struct SourceLocation {
6    /// Composable function name.
7    pub name: &'static str,
8    /// Source file reported by the compiler.
9    pub file: &'static str,
10    /// One-based declaration line.
11    pub line: u32,
12    /// Package directory reported by the compiler.
13    pub manifest_dir: &'static str,
14    #[cfg(all(feature = "inspection", debug_assertions))]
15    recompositions: Option<RecompositionCounter>,
16}
17
18impl SourceLocation {
19    /// Body executions after the initial composition for this composable instance.
20    /// Returns `None` unless a debug preview enabled recomposition tracking.
21    pub fn recompositions(&self) -> Option<u64> {
22        #[cfg(all(feature = "inspection", debug_assertions))]
23        return self
24            .recompositions
25            .as_ref()
26            .map(|counter| counter.0.executions.get().saturating_sub(1));
27        #[cfg(not(all(feature = "inspection", debug_assertions)))]
28        None
29    }
30}
31
32#[cfg(all(feature = "inspection", debug_assertions))]
33#[derive(Clone, Debug)]
34pub(crate) struct RecompositionCounter(std::rc::Rc<Counter>);
35
36#[cfg(all(feature = "inspection", debug_assertions))]
37#[derive(Debug)]
38struct Counter {
39    executions: std::cell::Cell<u64>,
40    instance_id: u64,
41    name: &'static str,
42    file: &'static str,
43    line: u32,
44    manifest_dir: &'static str,
45}
46
47/// A live composable instance, including functions which emit no layout nodes.
48#[derive(Debug, PartialEq, Eq)]
49pub struct RecompositionInfo {
50    /// Identity of this instance for the lifetime of the preview composition thread.
51    pub instance_id: u64,
52    /// Composable function name.
53    pub name: &'static str,
54    /// Compiler-reported source file.
55    pub file: &'static str,
56    /// One-based declaration line.
57    pub line: u32,
58    /// Compiler-reported package directory.
59    pub manifest_dir: &'static str,
60    /// Body executions after the initial composition.
61    pub recompositions: u64,
62}
63
64/// Captures live instances without inspecting or traversing the layout tree.
65/// Returns an empty list unless tracking is enabled in a debug inspection build.
66pub fn recomposition_snapshot() -> Vec<RecompositionInfo> {
67    #[cfg(all(feature = "inspection", debug_assertions))]
68    {
69        if !crate::thread_state::cells(|cells| cells.track_recompositions.get()) {
70            return Vec::new();
71        }
72        crate::thread_state::runtimes(|state| {
73            let mut registry = state.recomposition_counters.borrow_mut();
74            let mut result = Vec::with_capacity(registry.len());
75            registry.retain(|weak| {
76                let Some(scope) = crate::RecomposeScope::upgrade(weak) else {
77                    return false;
78                };
79                if scope.is_effectively_active()
80                    && let Some(counter) = scope.inner.recompositions.get()
81                {
82                    result.push(RecompositionInfo {
83                        instance_id: counter.0.instance_id,
84                        name: counter.0.name,
85                        file: counter.0.file,
86                        line: counter.0.line,
87                        manifest_dir: counter.0.manifest_dir,
88                        recompositions: counter.0.executions.get().saturating_sub(1),
89                    });
90                }
91                true
92            });
93            result
94        })
95    }
96    #[cfg(not(all(feature = "inspection", debug_assertions)))]
97    Vec::new()
98}
99
100#[cfg(all(feature = "inspection", debug_assertions))]
101impl PartialEq for RecompositionCounter {
102    fn eq(&self, other: &Self) -> bool {
103        std::rc::Rc::ptr_eq(&self.0, &other.0)
104    }
105}
106
107#[cfg(all(feature = "inspection", debug_assertions))]
108impl Eq for RecompositionCounter {}
109
110/// Enables counters for composable instances on this preview's composition thread.
111/// Tracking defaults to disabled and is compiled out of release builds and builds
112/// without `inspection`. Skipped bodies and source call-site markers are not counted.
113pub fn set_recomposition_tracking(enabled: bool) {
114    #[cfg(all(feature = "inspection", debug_assertions))]
115    crate::thread_state::cells(|cells| cells.track_recompositions.set(enabled));
116    #[cfg(not(all(feature = "inspection", debug_assertions)))]
117    let _ = enabled;
118}
119
120#[cfg(all(feature = "inspection", debug_assertions))]
121fn record_composition(
122    name: &'static str,
123    file: &'static str,
124    line: u32,
125    manifest_dir: &'static str,
126) -> Option<RecompositionCounter> {
127    if !crate::thread_state::cells(|cells| cells.track_recompositions.get())
128        || name.starts_with("__cranpose_call:")
129    {
130        return None;
131    }
132    crate::with_current_composer_opt(|composer| {
133        let scope = composer.current_recompose_scope()?;
134        let counter = scope.inner.recompositions.get_or_init(|| {
135            crate::thread_state::runtimes(|state| {
136                state
137                    .recomposition_counters
138                    .borrow_mut()
139                    .push(scope.downgrade());
140            });
141            let instance_id = crate::thread_state::cells(|cells| {
142                let next = &cells.next_recomposition_instance;
143                let id = next.get().saturating_add(1);
144                next.set(id);
145                id
146            });
147            RecompositionCounter(std::rc::Rc::new(Counter {
148                executions: std::cell::Cell::new(0),
149                instance_id,
150                name,
151                file,
152                line,
153                manifest_dir,
154            }))
155        });
156        counter
157            .0
158            .executions
159            .set(counter.0.executions.get().saturating_add(1));
160        Some(counter.clone())
161    })
162    .flatten()
163}
164
165/// Restores the composition origin stack when the function returns or unwinds.
166#[doc(hidden)]
167pub struct SourceScope {
168    #[cfg(feature = "inspection")]
169    depth: usize,
170    marker: std::marker::PhantomData<std::rc::Rc<()>>,
171}
172
173impl Drop for SourceScope {
174    fn drop(&mut self) {
175        #[cfg(feature = "inspection")]
176        crate::thread_state::runtimes(|state| state.source_stack.borrow_mut().truncate(self.depth));
177    }
178}
179
180/// Used by the composable macro; compiles to an empty guard without inspection.
181#[doc(hidden)]
182#[inline]
183pub fn __source_scope(
184    name: &'static str,
185    file: &'static str,
186    line: u32,
187    manifest_dir: &'static str,
188) -> SourceScope {
189    #[cfg(feature = "inspection")]
190    let depth = crate::thread_state::runtimes(|state| {
191        let mut stack = state.source_stack.borrow_mut();
192        let depth = stack.len();
193        stack.push(SourceLocation {
194            name,
195            file,
196            line,
197            manifest_dir,
198            #[cfg(debug_assertions)]
199            recompositions: record_composition(name, file, line, manifest_dir),
200        });
201        depth
202    });
203    #[cfg(not(feature = "inspection"))]
204    let _ = (name, file, line, manifest_dir);
205    SourceScope {
206        #[cfg(feature = "inspection")]
207        depth,
208        marker: std::marker::PhantomData,
209    }
210}
211
212/// Captures the active composable definitions, outermost first.
213#[cfg(feature = "inspection")]
214pub fn current_source_trace() -> std::rc::Rc<[SourceLocation]> {
215    crate::thread_state::runtimes(|state| std::rc::Rc::from(state.source_stack.borrow().as_slice()))
216}
217
218#[cfg(feature = "inspection")]
219pub(crate) struct SourceContext(Vec<SourceLocation>);
220
221#[cfg(feature = "inspection")]
222impl Drop for SourceContext {
223    fn drop(&mut self) {
224        crate::thread_state::runtimes(|state| {
225            *state.source_stack.borrow_mut() = std::mem::take(&mut self.0);
226        });
227    }
228}
229
230#[cfg(feature = "inspection")]
231pub(crate) fn restore_source_trace(trace: &[SourceLocation]) -> SourceContext {
232    SourceContext(crate::thread_state::runtimes(|state| {
233        state.source_stack.replace(trace.to_vec())
234    }))
235}
236
237#[cfg(all(test, feature = "inspection"))]
238#[path = "tests/source_trace_tests.rs"]
239mod tests;