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