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]
47 pub fn into_evaluated_and_js(self) -> (T, Js) {
48 (self.evaluated, self.js)
49 }
50
51 #[doc(hidden)]
53 pub fn into_captured_value(self) -> T::Surrogate
54 where
55 T: Surrogated,
56 {
57 if !self.is_static {
58 mark_signal_read();
59 }
60 self.evaluated.into_surrogate()
61 }
62}
63
64impl<T> From<T> for Expr<T>
65where
66 T: Surrogated,
67 T::Surrogate: Serialize,
68{
69 fn from(value: T) -> Self {
75 let surrogate = value.into_surrogate();
76 let js = Js::builder().surrogate(&surrogate).build();
77 Self::evaluate(|| surrogate.into_real(), js)
78 }
79}
80
81impl<T> NodeViewParts for Expr<T>
82where
83 T: NodeViewParts,
84{
85 fn into_view_parts(self, cx: &Cx, parts: &mut PartsWriter<'_>) {
86 if self.is_static {
87 self.evaluated.into_view_parts(cx, parts);
88 return;
89 }
90
91 parts.push_comment(|comment| {
97 comment.push_promoted_str_unescaped(&"::topcoat::expr::start(\"");
98 self.js.write(comment);
99 comment.push_promoted_str_unescaped(&"\")");
100 });
101 self.evaluated.into_view_parts(cx, parts);
102 parts.push_comment(|comment| {
103 comment.push_promoted_str_unescaped(&"::topcoat::expr::end");
104 });
105 }
106}
107
108thread_local! {
109 static SIGNAL_READ: Cell<Option<bool>> = const { Cell::new(None) };
110}
111
112pub(crate) fn mark_signal_read() -> bool {
114 if SIGNAL_READ.get().is_some() {
115 SIGNAL_READ.set(Some(true));
116 true
117 } else {
118 false
119 }
120}
121
122struct ReadScope {
124 previous: Option<bool>,
125}
126
127impl ReadScope {
128 fn enter() -> Self {
129 Self {
130 previous: SIGNAL_READ.replace(Some(false)),
131 }
132 }
133}
134
135impl Drop for ReadScope {
136 fn drop(&mut self) {
137 let read = SIGNAL_READ.replace(self.previous) == Some(true);
138 if read {
139 mark_signal_read();
140 }
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use std::{panic::catch_unwind, sync::Barrier};
147
148 use super::*;
149
150 #[test]
151 fn nested_evaluations_propagate_reads_without_contaminating_siblings() {
152 let outer = Expr::evaluate(
153 || {
154 let inner = Expr::evaluate(mark_signal_read, Js::source(""));
155 assert!(!inner.is_static());
156 let sibling = Expr::evaluate(|| 1, Js::source(""));
157 assert!(sibling.is_static());
158 },
159 Js::source(""),
160 );
161 assert!(!outer.is_static());
162 assert!(Expr::evaluate(|| 1, Js::source("")).is_static());
163 }
164
165 #[test]
166 fn panic_restores_the_parent_scope_and_preserves_its_reads() {
167 let outer = Expr::evaluate(
168 || {
169 let panic = catch_unwind(|| {
170 Expr::evaluate(
171 || {
172 mark_signal_read();
173 panic!("evaluation failed");
174 },
175 Js::source(""),
176 )
177 });
178 assert!(panic.is_err());
179 },
180 Js::source(""),
181 );
182 assert!(!outer.is_static());
183 assert!(Expr::evaluate(|| 1, Js::source("")).is_static());
184 }
185
186 #[test]
187 fn concurrent_evaluations_do_not_share_reads() {
188 let barrier = Barrier::new(2);
189 std::thread::scope(|scope| {
190 let dynamic = scope.spawn(|| {
191 Expr::evaluate(
192 || {
193 mark_signal_read();
194 barrier.wait();
195 barrier.wait();
196 },
197 Js::source(""),
198 )
199 });
200 let constant = Expr::evaluate(
201 || {
202 barrier.wait();
203 barrier.wait();
204 },
205 Js::source(""),
206 );
207 assert!(constant.is_static());
208 assert!(!dynamic.join().unwrap().is_static());
209 });
210 }
211}