Skip to main content

cranpose_core/
source_trace.rs

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