Skip to main content

max / quasi

Give a bare list its marks, so a spliced shape carries them across A mark lives on the container that holds the members, and three of emit's builders accrete into a `Vec` with nowhere to keep one: a menu's entries, a panel's members, a list's rows. Each refused a marked run, which meant a shape whose members accrete into a list could not be staged -- 52 of the population's 484 answer `Vec<Node>`, third after `Slot` and `Node`. All three accumulate into `Staged<Vec<_>>` now, which is a value and its marks beside it, so the marking block reads the same there as everywhere. The ordinary path takes the value back out at the end and pays a null pointer for it. Where the list is consumed in place the consumer absorbs: a table takes its rows and their marks together. Where it is a shape's answer, the twin's return type says so -- free, because nothing but the derivation calls a twin -- and the region splicing the members in absorbs them at the offset they landed. That last part is `Spliced`, a trait on the container rather than a branch in the macro, because the container is the only place that offset is known and a caller should write the same `include each` either way. New fixture, `spliced`, because none of the others took this path: a shape answering `Vec<Node>` with two guards and a loop, spliced into a region with members of its own either side. Both branches, the loop, and six fill cases across named/unnamed and three run lengths.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-08 20:13 UTC
Signed with PGP, not checked
Commit: 5a962aa770a717381c5e78adc1d3b651999020c1
Parent: 8370d3b
7 files changed, +399 insertions, -62 deletions
M Cargo.lock +4 -4
@@ -6313,8 +6313,8 @@
6313 6313 ]
6314 6314
6315 6315 [[patch.unused]]
6316 - name = "synckit-client"
6317 - version = "0.10.0"
6316 + name = "quasi-type"
6317 + version = "0.1.3"
6318 6318
6319 6319 [[patch.unused]]
6320 6320 name = "kberg"
@@ -6333,5 +6333,5 @@
6333 6333 version = "0.4.1"
6334 6334
6335 6335 [[patch.unused]]
6336 - name = "quasi-type"
6337 - version = "0.1.3"
6336 + name = "synckit-client"
6337 + version = "0.10.0"
@@ -105,6 +105,7 @@
105 105 mod immediate;
106 106 mod marked;
107 107 mod paged;
108 + mod spliced;
108 109 mod staging;
109 110
110 111 #[cfg(feature = "askama")]
@@ -12,7 +12,6 @@
12 12
13 13 use proc_macro2::{Span, TokenStream};
14 14 use quote::{format_ident, quote};
15 - use syn::spanned::Spanned as _;
16 15 use syn::{LitStr, Result};
17 16
18 17 use crate::ast::{
@@ -111,12 +110,20 @@
111 110 /// signature as the shape it stands for, and a second spelling of this mapping
112 111 /// would drift from the first the moment a shaped type was added.
113 112 pub fn returns_type(returns: &syn::Type) -> Result<TokenStream> {
114 - let shaped = shaped_name(returns)?;
115 - Ok(match &shaped {
113 + shaped_type(&shaped_name(returns)?)
114 + }
115 +
116 + /// The Rust type one [`Shaped`] is written as.
117 + fn shaped_type(shaped: &Shaped) -> Result<TokenStream> {
118 + Ok(match shaped {
116 119 Shaped::Nodes => quote!(::std::vec::Vec<::quasi_router::Node>),
117 120 Shaped::Acts => quote!(::std::vec::Vec<::quasi_router::Act>),
121 + Shaped::Marked(inner) => {
122 + let inner = shaped_type(inner)?;
123 + quote!(::quasi_router::stage::Staged<#inner>)
124 + }
118 125 Shaped::Single { name, optional } => {
119 - let shaped_type = format_ident!("{}", name, span = returns.span());
126 + let shaped_type = format_ident!("{}", name, span = Span::call_site());
120 127 if *optional {
121 128 quote!(::std::option::Option<::quasi_router::#shaped_type>)
122 129 } else {
@@ -149,6 +156,14 @@
149 156 /// what a row offers and what a chosen set does. Every member is an `act`,
150 157 /// because a menu is nothing but controls.
151 158 Acts,
159 + /// A staged twin's answer for either list: the value, and the marks beside
160 + /// it.
161 + ///
162 + /// A bare `Vec` has nowhere to keep a mark, so a twin answering one returns
163 + /// `Staged<Vec<_>>` instead and whatever splices its members in absorbs
164 + /// them at the offset they landed. A twin's return type is free -- nothing
165 + /// but the derivation calls one -- which is what makes this available.
166 + Marked(Box<Shaped>),
152 167 }
153 168
154 169 /// The type a shape returns, read off the declaration.
@@ -162,6 +177,12 @@
162 177 let Some(last) = path.path.segments.last() else {
163 178 return Err(syn::Error::new_spanned(returns, "an empty return type"));
164 179 };
180 + if last.ident == "Staged" {
181 + let Some(inner) = sole_argument(returns, &last.arguments)? else {
182 + return Err(syn::Error::new_spanned(returns, "`Staged` of what?"));
183 + };
184 + return Ok(Shaped::Marked(Box::new(shaped_name(inner)?)));
185 + }
165 186 if last.ident == "Vec" {
166 187 let Some(inner) = sole_argument(returns, &last.arguments)? else {
167 188 return Err(syn::Error::new_spanned(returns, "`Vec` of what?"));
@@ -206,6 +227,11 @@
206 227 returns,
207 228 "a menu with no entries is an empty `Vec<Act>`, not a `None`",
208 229 )),
230 + Shaped::Marked(_) => Err(syn::Error::new_spanned(
231 + returns,
232 + "a twin's marks ride beside its value, so there is nothing for an \
233 + `Option` to wrap here",
234 + )),
209 235 };
210 236 }
211 237 Ok(Shaped::Single {
@@ -240,8 +266,19 @@
240 266 /// independent within one block.
241 267 fn value_body(items: &[Item], shaped: &Shaped, span: Span) -> Result<TokenStream> {
242 268 let shaped = match shaped {
243 - Shaped::Nodes => return nodes(items),
244 - Shaped::Acts => return acts(items),
269 + Shaped::Nodes => return nodes(items, false),
270 + Shaped::Acts => return acts(items, false),
271 + Shaped::Marked(inner) => {
272 + return match inner.as_ref() {
273 + Shaped::Nodes => nodes(items, true),
274 + Shaped::Acts => acts(items, true),
275 + _ => Err(syn::Error::new(
276 + span,
277 + "only a list is answered with its marks beside it; every \
278 + other container keeps its own",
279 + )),
280 + };
281 + }
245 282 single @ Shaped::Single { .. } => single,
246 283 };
247 284 let Shaped::Single {
@@ -1885,13 +1922,27 @@
1885 1922 /// itself, and a `Vec` is told `push` and answers with nothing, so one of the
1886 1923 /// two spellings would have had to be special-cased inside the general form
1887 1924 /// anyway.
1888 - fn nodes(items: &[Item]) -> Result<TokenStream> {
1925 + /// A panel's members, as a list.
1926 + ///
1927 + /// `marked` is a staged twin, which answers `Staged<Vec<Node>>` instead: a bare
1928 + /// `Vec` has nowhere to keep a mark, so the marks ride beside the value until
1929 + /// whatever splices the members in absorbs them. Both forms accumulate into the
1930 + /// same wrapper, so the marking block reads the same here as everywhere else,
1931 + /// and the ordinary form takes the value back out at the end -- which costs a
1932 + /// null pointer and no allocation.
1933 + fn nodes(items: &[Item], marked: bool) -> Result<TokenStream> {
1889 1934 let held = format_ident!("nodes", span = Span::call_site());
1890 1935 let statements = node_statements(items, &held)?;
1936 + let answer = if marked {
1937 + quote!(#held)
1938 + } else {
1939 + quote!(#held.value)
1940 + };
1891 1941 Ok(quote!({
1892 - let mut #held = ::std::vec::Vec::new();
1942 + let mut #held =
1943 + ::quasi_router::stage::Staged::plain(::std::vec::Vec::new());
1893 1944 #(#statements)*
1894 - #held
1945 + #answer
1895 1946 }))
1896 1947 }
1897 1948
@@ -1901,13 +1952,20 @@
1901 1952 /// nothing but controls. A guard and a loop work the way they do everywhere
1902 1953 /// else, which is the whole reason this exists -- audiofiles' row menu is
1903 1954 /// fourteen entries of which nine are conditional.
1904 - fn acts(items: &[Item]) -> Result<TokenStream> {
1955 + /// A menu's entries, as a list. See [`nodes`] for what `marked` changes.
1956 + fn acts(items: &[Item], marked: bool) -> Result<TokenStream> {
1905 1957 let held = format_ident!("entries", span = Span::call_site());
1906 1958 let statements = act_statements(items, &held)?;
1959 + let answer = if marked {
1960 + quote!(#held)
1961 + } else {
1962 + quote!(#held.value)
1963 + };
1907 1964 Ok(quote!({
1908 - let mut #held = ::std::vec::Vec::new();
1965 + let mut #held =
1966 + ::quasi_router::stage::Staged::plain(::std::vec::Vec::new());
1909 1967 #(#statements)*
1910 - #held
1968 + #answer
1911 1969 }))
1912 1970 }
1913 1971
@@ -1915,15 +1973,31 @@
1915 1973 items
1916 1974 .iter()
1917 1975 .map(|item| match item {
1918 - // A bare `Vec` has nowhere to keep a mark. `Staged<Vec<_>>` is the
1919 - // wrapper that fixes it, and until this builder uses one, a shape
1920 - // whose members accrete into a list cannot be staged. Refused
1921 - // rather than dropped: a residual missing a branch the fill program
1922 - // has is a fault a request discovers.
1923 - Item::Marked { .. } => Err(syn::Error::new(
1924 - Span::call_site(),
1925 - "a menu's entries cannot be marked yet, so this shape cannot be staged",
1926 - )),
1976 + // The wrapper is what a mark lives on: `Staged<Vec<_>>` is a
1977 + // value and its marks beside it, which is the answer to a bare
1978 + // `Vec` having nowhere to keep one.
1979 + Item::Marked { site, varies, body } => {
1980 + let inner = act_statements(body, entries)?;
1981 + let varies = self::varies(varies)?;
1982 + let plan = format_ident!("{}", crate::symbolic::PLAN, span = Span::call_site());
1983 + let at = format_ident!("listed_from", span = Span::call_site());
1984 + let to = format_ident!("listed_to", span = Span::call_site());
1985 + Ok(quote!({
1986 + let #at = ::quasi_router::stage::Marking::placed(&#entries);
1987 + #(#inner)*
1988 + let #to = ::quasi_router::stage::Marking::placed(&#entries);
1989 + ::quasi_router::stage::Marking::mark(
1990 + &mut #entries,
1991 + ::quasi_router::stage::Mark {
1992 + scope: #plan.scope(),
1993 + id: #site,
1994 + varies: #varies,
1995 + from: #at,
1996 + to: #to,
1997 + },
1998 + );
1999 + }))
2000 + }
1927 2001 Item::Bind { name, source } => {
1928 2002 let value = source_value(source)?;
1929 2003 Ok(quote!(let #name = #value;))
@@ -1948,13 +2022,13 @@
1948 2022 Ok(quote!({
1949 2023 let #held = #value;
1950 2024 if #test {
1951 - #entries.push(#held);
2025 + #entries.value.push(#held);
1952 2026 }
1953 2027 }))
1954 2028 }
1955 2029 Item::Emit(emission) => {
1956 2030 let value = act(emission)?;
1957 - Ok(quote!(#entries.push(#value);))
2031 + Ok(quote!(#entries.value.push(#value);))
1958 2032 }
1959 2033 Item::Attribute { name, .. } => Err(syn::Error::new(
1960 2034 name.span(),
@@ -1968,15 +2042,31 @@
1968 2042 items
1969 2043 .iter()
1970 2044 .map(|item| match item {
1971 - // A bare `Vec` has nowhere to keep a mark. `Staged<Vec<_>>` is the
1972 - // wrapper that fixes it, and until this builder uses one, a shape
1973 - // whose members accrete into a list cannot be staged. Refused
1974 - // rather than dropped: a residual missing a branch the fill program
1975 - // has is a fault a request discovers.
1976 - Item::Marked { .. } => Err(syn::Error::new(
1977 - Span::call_site(),
1978 - "a panel's members cannot be marked yet, so this shape cannot be staged",
1979 - )),
2045 + // The wrapper is what a mark lives on: `Staged<Vec<_>>` is a
2046 + // value and its marks beside it, which is the answer to a bare
2047 + // `Vec` having nowhere to keep one.
2048 + Item::Marked { site, varies, body } => {
2049 + let inner = node_statements(body, nodes)?;
2050 + let varies = self::varies(varies)?;
2051 + let plan = format_ident!("{}", crate::symbolic::PLAN, span = Span::call_site());
2052 + let at = format_ident!("listed_from", span = Span::call_site());
2053 + let to = format_ident!("listed_to", span = Span::call_site());
2054 + Ok(quote!({
2055 + let #at = ::quasi_router::stage::Marking::placed(&#nodes);
2056 + #(#inner)*
2057 + let #to = ::quasi_router::stage::Marking::placed(&#nodes);
2058 + ::quasi_router::stage::Marking::mark(
2059 + &mut #nodes,
2060 + ::quasi_router::stage::Mark {
2061 + scope: #plan.scope(),
2062 + id: #site,
2063 + varies: #varies,
2064 + from: #at,
2065 + to: #to,
2066 + },
2067 + );
2068 + }))
2069 + }
1980 2070 Item::Bind { name, source } => {
1981 2071 let value = source_value(source)?;
1982 2072 Ok(quote!(let #name = #value;))
@@ -2001,13 +2091,13 @@
2001 2091 Ok(quote!({
2002 2092 let #member = #value;
2003 2093 if #test {
2004 - #nodes.push(#member);
2094 + #nodes.value.push(#member);
2005 2095 }
2006 2096 }))
2007 2097 }
2008 2098 Item::Emit(emission) => {
2009 2099 let value = panel_member(emission)?;
2010 - Ok(quote!(#nodes.push(#value);))
2100 + Ok(quote!(#nodes.value.push(#value);))
2011 2101 }
2012 2102 // The one attribute a panel takes, and it means in a panel what it
2013 2103 // means on a region: splice another shape's nodes in here. A panel
@@ -2021,7 +2111,7 @@
2021 2111 } if name == "extend" => {
2022 2112 refuse_body(name, body)?;
2023 2113 let spliced = args.iter().map(arg).collect::<Result<Vec<_>>>()?;
2024 - let extending = quote!(#(#nodes.extend(#spliced);)*);
2114 + let extending = quote!(#(#nodes.value.extend(#spliced);)*);
2025 2115 guard.as_ref().map_or_else(
2026 2116 || Ok(extending.clone()),
2027 2117 |guard| {
@@ -2093,23 +2183,35 @@
2093 2183 }
2094 2184 }
2095 2185 let statements = list_statements(&body, &rows)?;
2186 + // The rows accrete into a wrapper so a marked run has somewhere to be
2187 + // recorded, and the table absorbs those marks as it takes the rows. A list
2188 + // declares no columns, so its members are numbered from the first row and
2189 + // the offset is zero -- `Table::placed` counts the columns first and there
2190 + // are none.
2096 2191 if more.is_empty() {
2097 2192 return Ok(quote!({
2098 - let mut #rows = ::std::vec::Vec::new();
2193 + let mut #rows =
2194 + ::quasi_router::stage::Staged::plain(::std::vec::Vec::new());
2099 2195 #(#statements)*
2100 - ::quasi_router::Node::list(#rows)
2196 + let mut listed = ::quasi_router::Node::list(#rows.value);
2197 + ::quasi_router::stage::Marking::absorb(&mut listed, &#rows.marks, 0);
2198 + listed
2101 2199 }));
2102 2200 }
2103 2201 Ok(quote!({
2104 - let mut #rows = ::std::vec::Vec::new();
2202 + let mut #rows =
2203 + ::quasi_router::stage::Staged::plain(::std::vec::Vec::new());
2105 2204 #(#statements)*
2106 2205 let mut #rest = ::std::option::Option::None;
2107 2206 #(#more)*
2108 - ::quasi_router::Node::Table {
2207 + let mut listed = ::quasi_router::Node::Table {
2109 2208 columns: ::std::vec::Vec::new(),
2110 - rows: #rows,
2209 + rows: #rows.value,
2111 2210 more: #rest,
2112 - }
2211 + marks: ::quasi_router::stage::Marks::none(),
2212 + };
2213 + ::quasi_router::stage::Marking::absorb(&mut listed, &#rows.marks, 0);
2214 + listed
2113 2215 }))
2114 2216 }
2115 2217
@@ -2117,15 +2219,31 @@
2117 2219 items
2118 2220 .iter()
2119 2221 .map(|item| match item {
2120 - // A bare `Vec` has nowhere to keep a mark. `Staged<Vec<_>>` is the
2121 - // wrapper that fixes it, and until this builder uses one, a shape
2122 - // whose members accrete into a list cannot be staged. Refused
2123 - // rather than dropped: a residual missing a branch the fill program
2124 - // has is a fault a request discovers.
2125 - Item::Marked { .. } => Err(syn::Error::new(
2126 - Span::call_site(),
2127 - "a list's rows cannot be marked yet, so this shape cannot be staged",
2128 - )),
2222 + // The wrapper is what a mark lives on: `Staged<Vec<_>>` is a
2223 + // value and its marks beside it, which is the answer to a bare
2224 + // `Vec` having nowhere to keep one.
2225 + Item::Marked { site, varies, body } => {
2226 + let inner = list_statements(&body.iter().collect::<Vec<_>>(), rows)?;
2227 + let varies = self::varies(varies)?;
2228 + let plan = format_ident!("{}", crate::symbolic::PLAN, span = Span::call_site());
2229 + let at = format_ident!("listed_from", span = Span::call_site());
2230 + let to = format_ident!("listed_to", span = Span::call_site());
2231 + Ok(quote!({
2232 + let #at = ::quasi_router::stage::Marking::placed(&#rows);
2233 + #(#inner)*
2234 + let #to = ::quasi_router::stage::Marking::placed(&#rows);
2235 + ::quasi_router::stage::Marking::mark(
2236 + &mut #rows,
2237 + ::quasi_router::stage::Mark {
2238 + scope: #plan.scope(),
2239 + id: #site,
2240 + varies: #varies,
2241 + from: #at,
2242 + to: #to,
2243 + },
2244 + );
2245 + }))
2246 + }
2129 2247 Item::Bind { name, source } => {
2130 2248 let value = source_value(source)?;
2131 2249 Ok(quote!(let #name = #value;))
@@ -2149,13 +2267,13 @@
2149 2267 Ok(quote!({
2150 2268 let #held = #value;
2151 2269 if #test {
2152 - #rows.push(#held);
2270 + #rows.value.push(#held);
2153 2271 }
2154 2272 }))
2155 2273 }
2156 2274 Item::Emit(emission) => {
2157 2275 let row = list_row(emission)?;
2158 - Ok(quote!(#rows.push(#row);))
2276 + Ok(quote!(#rows.value.push(#row);))
2159 2277 }
2160 2278 Item::Attribute { name, .. } => Err(syn::Error::new(
2161 2279 name.span(),
@@ -349,12 +349,33 @@
349 349 name: Ident::new(PLAN, Span::call_site()),
350 350 ty: syn::parse_quote!(&::quasi_router::stage::Plan),
351 351 }],
352 - returns: declaration.returns.clone(),
352 + // A shape answering a bare list answers `Staged<Vec<_>>` here: a
353 + // `Vec` has nowhere to keep a mark, so the marks ride beside the
354 + // value until whatever splices its members in absorbs them. Free to
355 + // do, because nothing but the derivation calls a twin.
356 + returns: listed(&declaration.returns),
353 357 items,
354 358 },
355 359 })
356 360 }
357 361
362 + /// A twin's return type: a bare list gains its marks, everything else is itself.
363 + ///
364 + /// Read syntactically rather than through `emit`'s reader, because this runs
365 + /// before the twin is emitted and the answer is one shape of type. A container
366 + /// keeps its own marks inside it and needs nothing here.
367 + fn listed(returns: &syn::Type) -> syn::Type {
368 + let syn::Type::Path(path) = returns else {
369 + return returns.clone();
370 + };
371 + match path.path.segments.last() {
372 + Some(last) if last.ident == "Vec" => {
373 + syn::parse_quote!(::quasi_router::stage::Staged<#returns>)
374 + }
375 + _ => returns.clone(),
376 + }
377 + }
378 +
358 379 fn items(items: &[Item], counters: &mut Counters) -> Result<Vec<Item>> {
359 380 items
360 381 .iter()
@@ -5597,6 +5597,31 @@
5597 5597 }
5598 5598 }
5599 5599
5600 + impl<I: IntoIterator<Item = Node>> crate::stage::Spliced<Slot> for I {
5601 + fn splice_into(self, container: &mut Slot) {
5602 + for node in self {
5603 + container.push(node);
5604 + }
5605 + }
5606 + }
5607 +
5608 + impl crate::stage::Spliced<Slot> for crate::stage::Staged<Vec<Node>> {
5609 + /// The members, and their marks moved to where they landed.
5610 + ///
5611 + /// The child numbered its members from its own zero and they are now at an
5612 + /// offset in this region, so every bound moves by the same amount. Done
5613 + /// here because the container is the only place that offset is known.
5614 + fn splice_into(self, container: &mut Slot) {
5615 + use crate::stage::Marking as _;
5616 +
5617 + let at = container.placed();
5618 + for node in self.value {
5619 + container.push(node);
5620 + }
5621 + container.absorb(&self.marks, at);
5622 + }
5623 + }
5624 +
5600 5625 impl crate::stage::Marking for Node {
5601 5626 /// However many members this kind of node holds.
5602 5627 ///
@@ -5790,6 +5815,16 @@
5790 5815 /// is why the whole tree kept building.
5791 5816 #[must_use]
5792 5817 pub fn with(mut self, node: Node) -> Self {
5818 + self.push(node);
5819 + self
5820 + }
5821 +
5822 + /// [`with`](Self::with) for a caller holding this by reference.
5823 + ///
5824 + /// The splicing half needs it: a run of members is put in one at a time and
5825 + /// the marks that came with them are moved afterwards, which a chaining
5826 + /// call cannot express.
5827 + fn push(&mut self, node: Node) {
5793 5828 match &mut self.body {
5794 5829 Body::All(members) => members.push(Ranked::new(node)),
5795 5830 // A member added to a selective region is a frame with no name,
@@ -5797,7 +5832,6 @@
5797 5832 // nowhere else.
5798 5833 Body::Selective { frames, .. } => frames.push(Frame::unnamed(node)),
5799 5834 }
5800 - self
5801 5835 }
5802 5836
5803 5837 /// Add every node a shape answered with, chaining.
@@ -5813,10 +5847,8 @@
5813 5847 /// filler to call. Said as one `include each`, the caller splices a
5814 5848 /// reference the way every other `include` does.
5815 5849 #[must_use]
5816 - pub fn with_all(mut self, nodes: impl IntoIterator<Item = Node>) -> Self {
5817 - for node in nodes {
5818 - self = self.with(node);
5819 - }
5850 + pub fn with_all(mut self, nodes: impl crate::stage::Spliced<Self>) -> Self {
5851 + nodes.splice_into(&mut self);
5820 5852 self
5821 5853 }
5822 5854
@@ -310,6 +310,19 @@
310 310 }
311 311 }
312 312
313 + /// A run of members a container can take, with or without marks beside it.
314 + ///
315 + /// What makes `include each` compose. A shape's members are spliced into
316 + /// whatever holds them, and a staged twin's come with marks numbered from that
317 + /// shape's own zero; the container is where the offset they landed at is known,
318 + /// so the container is what absorbs them. A caller writes the same call either
319 + /// way and neither has to know which it is holding.
320 + pub trait Spliced<C> {
321 + /// Put the members into `container`, absorbing any marks that came with
322 + /// them.
323 + fn splice_into(self, container: &mut C);
324 + }
325 +
313 326 /// One mark, resolved to the bytes the renderer wrote for it.
314 327 ///
315 328 /// What a [`Mark`] becomes once something has drawn it: the same site, and the
@@ -1,0 +1,152 @@
1 + //! A shape that answers a run of members, spliced into the region that holds it.
2 + //!
3 + //! The shape MNW's feed panel is: `panel_body` answers `Vec<Node>` and the
4 + //! region around it takes them whole, so the members of two shapes end up in
5 + //! one container and the marks of the inner one have to move to where they
6 + //! landed.
7 + //!
8 + //! # Why it is a fixture rather than a case in `declared`
9 + //!
10 + //! A bare `Vec` is the one container with nowhere to keep a mark. Everything
11 + //! else in the tree carries its own -- a region, a table, a row, a field -- so
12 + //! a guard inside one of those is recorded where it happened. A list is
13 + //! answered by `Staged<Vec<_>>` instead: the value keeps its own type and the
14 + //! marks ride beside it until the container splicing them in absorbs them at
15 + //! the offset its own members reached.
16 + //!
17 + //! 52 of the population's 484 shapes answer `Vec<Node>`, third after `Slot` and
18 + //! `Node`, so this is not a corner.
19 +
20 + // The fixture data below is built by this module's tests and by nothing else:
21 + // the bench itself measures the shapes, not the rows behind them.
22 + #![allow(dead_code)]
23 +
24 + use quasi_declare::declare;
25 +
26 + /// What the panel read.
27 + pub(crate) struct Feed {
28 + pub heading: String,
29 + pub items: Vec<String>,
30 + }
31 +
32 + declare! {
33 + /// The panel's members, in order, with nothing wrapping them.
34 + ///
35 + /// A heading that is only there when the feed is named, an empty state that
36 + /// is only there when it has nothing, and a row per item. All three land in
37 + /// the caller's region, so all three marks are numbered here and moved
38 + /// there.
39 + #[staged]
40 + pub(crate) shape body(feed: &Feed) -> Vec<Node>;
41 +
42 + text "{feed.heading}" unless feed.heading.is_empty();
43 +
44 + empty "Nothing here yet." when feed.items.is_empty();
45 +
46 + for item in feed.items.iter() {
47 + text "{item}";
48 + }
49 + }
50 +
51 + declare! {
52 + /// The region the panel's members are spliced into.
53 + ///
54 + /// Its own members either side of the splice, so a mark that failed to move
55 + /// would land on one of these rather than quietly on nothing.
56 + #[staged]
57 + pub(crate) shape panel(feed: &Feed) -> Node;
58 +
59 + region "FEED" as Pane {
60 + text "Above.";
61 + include each body(feed);
62 + text "Below.";
63 + }
64 + }
65 +
66 + #[cfg(test)]
67 + mod tests {
68 + use super::*;
69 + use quasi_http::Serves as _;
70 + use quasi_router::stage::{Op, Residual};
71 + use quasi_webview::Webview;
72 +
73 + fn feed(heading: &str, items: &[&str]) -> Feed {
74 + Feed {
75 + heading: heading.to_owned(),
76 + items: items.iter().map(|item| (*item).to_owned()).collect(),
77 + }
78 + }
79 +
80 + fn residual() -> Residual {
81 + quasi_webview::stage::derive(&Webview::new(), panel_staged)
82 + }
83 +
84 + /// The spliced shape's own structure survives the splice.
85 + ///
86 + /// Two branches and a loop, all three declared in `body` and all three
87 + /// recorded against the region that took its members. A mark that did not
88 + /// move would cover the caller's own text instead, which is what the
89 + /// members either side of the splice are here to catch.
90 + #[test]
91 + fn a_spliced_shape_carries_its_marks_into_the_region() {
92 + fn count(ops: &[Op], branches: &mut usize, loops: &mut usize) {
93 + for op in ops {
94 + match op {
95 + Op::Branch(body) => {
96 + *branches += 1;
97 + count(body, branches, loops);
98 + }
99 + Op::Loop(body) => {
100 + *loops += 1;
101 + count(body, branches, loops);
102 + }
103 + Op::Arms(arms) => {
104 + for arm in arms.iter() {
105 + count(arm, branches, loops);
106 + }
107 + }
108 + Op::Lit(_) | Op::Hole { .. } => {}
109 + }
110 + }
111 + }
112 +
113 + let residual = residual();
114 + let (mut branches, mut loops) = (0, 0);
115 + count(residual.ops(), &mut branches, &mut loops);
116 + assert_eq!(branches, 2, "{:#?}", residual.ops());
117 + assert_eq!(loops, 1, "{:#?}", residual.ops());
118 +
119 + // And the caller's own members are outside all of it.
120 + let settled: String = residual
121 + .ops()
122 + .iter()
123 + .filter_map(|op| match op {
124 + Op::Lit(text) => Some(text.as_ref()),
125 + _ => None,
126 + })
127 + .collect();
128 + assert!(settled.contains("Above."), "{settled}");
129 + assert!(settled.contains("Below."), "{settled}");
130 + }
131 +
132 + /// Filling the residual gives back what the renderer would have written.
133 + ///
134 + /// Across the cases the marks separate: named or not, empty or not, and
135 + /// three lengths of the run.
136 + #[test]
137 + fn a_filled_panel_is_what_the_renderer_would_have_produced() {
138 + let webview = Webview::new();
139 + let residual = residual();
140 + for heading in ["", "Today"] {
141 + for items in [&[][..], &["one"][..], &["one", "two", "three"][..]] {
142 + let feed = feed(heading, items);
143 + assert_eq!(
144 + webview.fragment(&panel(&feed)),
145 + panel_serve(&residual, &feed),
146 + "heading {heading:?}, {} items",
147 + items.len()
148 + );
149 + }
150 + }
151 + }
152 + }