Skip to main content

max / quasi

13.4 KB · 344 lines History Blame Raw
1 //! Writing a request's values into a residual.
2 //!
3 //! The third thing a `#[staged]` shape gets. The first is the ordinary
4 //! function, the second is the twin that renders with sentinels, and this is
5 //! what a request actually calls: a walk over the residual that pushes the
6 //! renderer's own literals and writes the request's values into the gaps.
7 //!
8 //! No `Node` is built. That is the whole point of the task this belongs to, and
9 //! it is why this is generated rather than interpreted: the values come from
10 //! the same expressions the declaration wrote, evaluated in place.
11 //!
12 //! # Why holes are filled by number and not by position
13 //!
14 //! Render order and declaration order are not the same. A cell's `activate`
15 //! address is written after the cell's value and renders before it, inside the
16 //! anchor that wraps it. So a filler that walked the residual writing values in
17 //! the order it had them would put the address in the text and the text in the
18 //! address.
19 //!
20 //! The residual carries each hole's number, and [`crate::symbolic`] recorded
21 //! the expression for each number in the same traversal that assigned it. So a
22 //! straight run of markup is filled by asking for holes by number, and only the
23 //! things that change control flow -- a guard, a loop, another shape -- have to
24 //! appear in the same order in both. Those do, because a container renders its
25 //! members in the order it was told them.
26
27 use proc_macro2::{Span, TokenStream};
28 use quote::{format_ident, quote};
29 use syn::{Ident, Result};
30
31 use crate::ast::{Declaration, Hole, Step};
32 use crate::symbolic::Fill;
33
34 /// The filler's name, which is what an `include` calls.
35 pub fn fill_name(name: &Ident) -> Ident {
36 format_ident!("{}_fill", name, span = name.span())
37 }
38
39 /// The one-call entry point, which makes a cursor and closes it.
40 fn serve_name(name: &Ident) -> Ident {
41 format_ident!("{}_serve", name, span = name.span())
42 }
43
44 /// The filler for one shape, and the entry point beside it.
45 pub fn filler(declaration: &Declaration, fill: &[Fill]) -> Result<TokenStream> {
46 let name = fill_name(&declaration.name);
47 let serve = serve_name(&declaration.name);
48 let vis = &declaration.vis;
49
50 let params: Vec<TokenStream> = declaration
51 .params
52 .iter()
53 .map(|param| {
54 let name = &param.name;
55 let ty = &param.ty;
56 quote!(#name: #ty)
57 })
58 .collect();
59 let forwarded: Vec<&Ident> = declaration.params.iter().map(|param| &param.name).collect();
60
61 let body = self::body(fill)?;
62
63 let fill_doc = format!(
64 " Write a request's values into [`{}`]'s residual.",
65 declaration.name
66 );
67 let serve_doc = format!(
68 " Serve [`{}`] from a residual, building no `Node`.",
69 declaration.name
70 );
71
72 Ok(quote! {
73 #[doc = #fill_doc]
74 ///
75 /// Continues on the cursor it is given and does not close it, so a
76 /// shape that includes this one carries on where it left off.
77 // `out` is appended to, which `&mut str` cannot do. clippy reads the
78 // signature of a shape that happens to push nothing itself -- one whose
79 // every value comes from an included shape, or one with no values at
80 // all -- and suggests the slice. Taking the suggestion would break every
81 // other shape emitted by this same code.
82 #[allow(clippy::ptr_arg)]
83 #vis fn #name(
84 cursor: &mut ::quasi_router::stage::Cursor<'_>,
85 out: &mut ::std::string::String,
86 #(#params),*
87 ) {
88 #body
89 }
90
91 #[doc = #serve_doc]
92 #vis fn #serve(
93 residual: &::quasi_router::stage::Residual,
94 #(#params),*
95 ) -> ::std::string::String {
96 let mut out = ::std::string::String::with_capacity(residual.literal_len() + 64);
97 let mut cursor = residual.cursor();
98 #name(&mut cursor, &mut out, #(#forwarded),*);
99 ::quasi_router::stage::Cursor::finish(&mut cursor, &mut out);
100 out
101 }
102 })
103 }
104
105 /// One body: a shape's, a branch's, or a loop's.
106 ///
107 /// Every straight stretch is offered **every hole at this level**, not just the
108 /// ones written since the last branch or loop. That is what makes the filler
109 /// agree with the residual about which stretch a hole is in, and the two do not
110 /// otherwise agree: the residual is in render order and the fill program is in
111 /// declaration order, and control flow moves between them.
112 ///
113 /// MNW's `/git/{owner}` is the site. A row declares two cells, then a guarded
114 /// third, then `activate` -- and `activate`'s address renders in the row's
115 /// opening tag, before any cell. So the residual's first stretch holds the
116 /// address's holes and the fill program's first run held the two cells', and
117 /// the cursor asked for a number that run had no arm for.
118 ///
119 /// Offering all of them costs a wider `match` in the generated code and nothing
120 /// at run time: an arm's expression is evaluated only when the cursor asks for
121 /// that number, and the cursor asks only for the holes the stretch it is
122 /// walking actually has. A stretch with no holes at all still gets the call,
123 /// because which stretch that is is exactly what declaration order cannot say.
124 fn body(fill: &[Fill]) -> Result<TokenStream> {
125 let holes: Vec<&Fill> = fill
126 .iter()
127 .filter(|one| matches!(one, Fill::Hole { .. }))
128 .collect();
129
130 let mut out = TokenStream::new();
131 for one in fill {
132 if matches!(one, Fill::Hole { .. }) {
133 continue;
134 }
135 out.extend(run(&holes)?);
136 out.extend(control(one, &holes)?);
137 }
138 out.extend(run(&holes)?);
139 Ok(out)
140 }
141
142 /// One straight stretch of markup, with its holes answered by number.
143 fn run(holes: &[&Fill]) -> Result<TokenStream> {
144 if holes.is_empty() {
145 return Ok(TokenStream::new());
146 }
147 let arms = answers(holes)?;
148
149 Ok(quote! {
150 ::quasi_router::stage::Cursor::fill(cursor, out, &mut |which, out| match which {
151 #(#arms)*
152 // A hole at another level, or one this shape does not have. The
153 // residual and this code came from one declaration, so a number
154 // outside the level's own set is a pairing bug rather than a case.
155 _ => ::core::unreachable!("the residual has a hole the filler does not"),
156 });
157 })
158 }
159
160 /// One `match` arm per hole, answering it by number.
161 fn answers(holes: &[&Fill]) -> Result<Vec<TokenStream>> {
162 holes
163 .iter()
164 .map(|one| {
165 let Fill::Hole { id, hole } = one else {
166 unreachable!("only holes are collected here");
167 };
168 let value = crate::emit::hole(&owned(hole))?;
169 Ok(quote!(#id => ::quasi_webview::stage::Fill::fill(&(#value), out),))
170 })
171 .collect()
172 }
173
174 /// A guard, a loop, or another shape.
175 ///
176 /// `level` is every hole the stretch around this one has. A guard needs them
177 /// because the residual may hold [`Op::Arms`](quasi_router::stage::Op::Arms)
178 /// where the declaration said a guard, and an arm's holes are numbered in the
179 /// level around it rather than in the arm -- which is what lets one arm carry a
180 /// hole the other does not.
181 fn control(one: &Fill, level: &[&Fill]) -> Result<TokenStream> {
182 Ok(match one {
183 Fill::Hole { .. } => unreachable!("handled by the caller"),
184 Fill::Branch { guard, body } => {
185 let predicate = crate::emit::predicate(guard)?;
186 let inner = self::body(body)?;
187 // Both sets, because either shape may turn up here and the two
188 // number their holes in the same shape-wide sequence. An arm is
189 // evaluated only when the cursor asks for it, so offering more than
190 // one shape can use costs a wider `match` and nothing at run time.
191 let mut reachable: Vec<&Fill> = level.to_vec();
192 reachable.extend(body.iter().filter(|one| matches!(one, Fill::Hole { .. })));
193 let answers = answers(&reachable)?;
194 quote! {
195 {
196 let taken = #predicate;
197 ::quasi_router::stage::Cursor::choose(
198 cursor,
199 out,
200 taken,
201 &mut |which, out| match which {
202 #(#answers)*
203 _ => ::core::unreachable!(
204 "the residual has a hole the filler does not"
205 ),
206 },
207 &mut |cursor, out| {
208 #inner
209 ::quasi_router::stage::Cursor::finish(cursor, out);
210 },
211 );
212 }
213 }
214 }
215 Fill::Repeat {
216 dereferenced,
217 binder,
218 iterable,
219 body,
220 } => {
221 let over = crate::emit::hole(iterable)?;
222 let inner = self::body(body)?;
223 let bound = if *dereferenced {
224 quote!(&#binder)
225 } else {
226 quote!(#binder)
227 };
228 quote! {
229 {
230 let repeated = ::quasi_router::stage::Cursor::repeat(cursor, out);
231 let scope = ::quasi_router::stage::Cursor::scope(cursor);
232 for #bound in #over {
233 let mut cursor =
234 &mut ::quasi_router::stage::Cursor::over(repeated, scope);
235 #inner
236 ::quasi_router::stage::Cursor::finish(cursor, out);
237 }
238 }
239 }
240 }
241 // Two markups at one position, picked by a predicate rather than by a
242 // value. `Arms`'s machinery exactly, with the scrutinee replaced: arm 0
243 // is the setting made, which is `Plan::swap`'s own reading.
244 Fill::Swap { guard } => {
245 let predicate = crate::emit::predicate(guard)?;
246 let answers = answers(level)?;
247 quote! {
248 {
249 let taken = usize::from(!(#predicate));
250 ::quasi_router::stage::Cursor::pick(
251 cursor,
252 out,
253 taken,
254 &mut |which, out| match which {
255 #(#answers)*
256 _ => ::core::unreachable!(
257 "the residual has a hole the filler does not"
258 ),
259 },
260 );
261 }
262 }
263 }
264 // A dispatch: the residual holds one arm per position and the request
265 // picks the index. No body to walk, because every arm's holes were
266 // numbered at this level -- see `symbolic::Fill::Arms`.
267 Fill::Arms {
268 scrutinee,
269 patterns,
270 } => {
271 let value = crate::emit::hole(scrutinee)?;
272 let which = patterns
273 .iter()
274 .enumerate()
275 .map(|(at, pattern)| {
276 let pattern = crate::emit::pattern(pattern);
277 Ok(quote!(#pattern => #at,))
278 })
279 .collect::<Result<Vec<_>>>()?;
280 let last = patterns.len();
281 let answers = answers(level)?;
282 quote! {
283 {
284 let taken = match #value {
285 #(#which)*
286 _ => #last,
287 };
288 ::quasi_router::stage::Cursor::pick(
289 cursor,
290 out,
291 taken,
292 &mut |which, out| match which {
293 #(#answers)*
294 _ => ::core::unreachable!(
295 "the residual has a hole the filler does not"
296 ),
297 },
298 );
299 }
300 }
301 }
302 // The callee's markup was spliced into this residual where its shape
303 // was included, so its filler carries on with the same cursor.
304 Fill::Include { callee, args, site } => {
305 let mut path = callee.clone();
306 let last = path
307 .segments
308 .last_mut()
309 .ok_or_else(|| syn::Error::new(Span::call_site(), "an empty path"))?;
310 last.ident = fill_name(&last.ident);
311 let args = args
312 .iter()
313 .map(crate::emit::arg)
314 .collect::<Result<Vec<_>>>()?;
315 quote! {
316 {
317 // The callee's holes are numbered in its own namespace, so
318 // the walk is told whose they are before it reaches them.
319 let held = ::quasi_router::stage::Cursor::enter(cursor, #site);
320 #path(cursor, out, #(#args),*);
321 ::quasi_router::stage::Cursor::leave(cursor, held);
322 }
323 }
324 }
325 })
326 }
327
328 /// The hole with a trailing `.clone()` taken off.
329 ///
330 /// A declaration clones because the vocabulary takes the value by value. A
331 /// filler only writes it, so the clone would be one allocation per hole per row
332 /// bought for nothing, which at two hundred rows is the whole allocation
333 /// budget.
334 fn owned(hole: &Hole) -> Hole {
335 let mut trimmed = hole.clone();
336 if let Some(Step::Method { name, args }) = trimmed.steps.last()
337 && name == "clone"
338 && args.is_empty()
339 {
340 trimmed.steps.pop();
341 }
342 trimmed
343 }
344