1use std::cell::Cell;
2
3use serde::Serialize;
4use topcoat_core::context::Cx;
5use topcoat_view::{NodeViewParts, PartsWriter};
6
7use crate::{Js, Surrogate, Surrogated};
8
9#[derive(Debug, Clone)]
14pub struct Expr<T> {
15 pub(crate) evaluated: T,
16 pub(crate) js: Js,
17 pub(crate) is_static: bool,
18}
19
20impl<T> Expr<T> {
21 #[inline]
26 pub fn evaluate(evaluate: impl FnOnce() -> T, js: Js) -> Self {
27 let _scope = ReadScope::enter();
28 let evaluated = evaluate();
29 Self {
30 evaluated,
31 js,
32 is_static: SIGNAL_READ.get() == Some(false),
33 }
34 }
35
36 #[inline]
40 #[must_use]
41 pub fn is_static(&self) -> bool {
42 self.is_static
43 }
44
45 #[inline]
64 #[must_use]
65 pub fn dynamic(mut self) -> Self {
66 self.is_static = false;
67 self
68 }
69
70 #[inline]
72 pub fn into_evaluated_and_js(self) -> (T, Js) {
73 (self.evaluated, self.js)
74 }
75
76 #[doc(hidden)]
78 pub fn into_captured_value(self) -> T::Surrogate
79 where
80 T: Surrogated,
81 {
82 if !self.is_static {
83 mark_signal_read();
84 }
85 self.evaluated.into_surrogate()
86 }
87}
88
89impl<T> From<T> for Expr<T>
90where
91 T: Surrogated,
92 T::Surrogate: Serialize,
93{
94 fn from(value: T) -> Self {
100 let surrogate = value.into_surrogate();
101 let js = Js::builder().surrogate(&surrogate).build();
102 Self::evaluate(|| surrogate.into_real(), js)
103 }
104}
105
106impl<T> NodeViewParts for Expr<T>
107where
108 T: NodeViewParts,
109{
110 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
111 if self.is_static {
112 self.evaluated.into_view_parts(cx, parts);
113 return;
114 }
115
116 parts.push_comment(|comment| {
122 comment.push_promoted_str_unescaped(&"::topcoat::expr::start(\"");
123 self.js.write(comment);
124 comment.push_promoted_str_unescaped(&"\")");
125 });
126 self.evaluated.into_view_parts(cx, parts);
127 parts.push_comment(|comment| {
128 comment.push_promoted_str_unescaped(&"::topcoat::expr::end");
129 });
130 }
131}
132
133thread_local! {
134 static SIGNAL_READ: Cell<Option<bool>> = const { Cell::new(None) };
135}
136
137pub(crate) fn mark_signal_read() -> bool {
139 if SIGNAL_READ.get().is_some() {
140 SIGNAL_READ.set(Some(true));
141 true
142 } else {
143 false
144 }
145}
146
147struct ReadScope {
149 previous: Option<bool>,
150}
151
152impl ReadScope {
153 fn enter() -> Self {
154 Self {
155 previous: SIGNAL_READ.replace(Some(false)),
156 }
157 }
158}
159
160impl Drop for ReadScope {
161 fn drop(&mut self) {
162 let read = SIGNAL_READ.replace(self.previous) == Some(true);
163 if read {
164 mark_signal_read();
165 }
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use std::{panic::catch_unwind, sync::Barrier};
172
173 use super::*;
174
175 #[test]
176 fn nested_evaluations_propagate_reads_without_contaminating_siblings() {
177 let outer = Expr::evaluate(
178 || {
179 let inner = Expr::evaluate(mark_signal_read, Js::source(""));
180 assert!(!inner.is_static());
181 let sibling = Expr::evaluate(|| 1, Js::source(""));
182 assert!(sibling.is_static());
183 },
184 Js::source(""),
185 );
186 assert!(!outer.is_static());
187 assert!(Expr::evaluate(|| 1, Js::source("")).is_static());
188 }
189
190 #[test]
191 fn panic_restores_the_parent_scope_and_preserves_its_reads() {
192 let outer = Expr::evaluate(
193 || {
194 let panic = catch_unwind(|| {
195 Expr::evaluate(
196 || {
197 mark_signal_read();
198 panic!("evaluation failed");
199 },
200 Js::source(""),
201 )
202 });
203 assert!(panic.is_err());
204 },
205 Js::source(""),
206 );
207 assert!(!outer.is_static());
208 assert!(Expr::evaluate(|| 1, Js::source("")).is_static());
209 }
210
211 #[test]
212 fn concurrent_evaluations_do_not_share_reads() {
213 let barrier = Barrier::new(2);
214 std::thread::scope(|scope| {
215 let dynamic = scope.spawn(|| {
216 Expr::evaluate(
217 || {
218 mark_signal_read();
219 barrier.wait();
220 barrier.wait();
221 },
222 Js::source(""),
223 )
224 });
225 let constant = Expr::evaluate(
226 || {
227 barrier.wait();
228 barrier.wait();
229 },
230 Js::source(""),
231 );
232 assert!(constant.is_static());
233 assert!(!dynamic.join().unwrap().is_static());
234 });
235 }
236}