| 22 |
22 |
|
//! has stopped being copy and is structure; move it back into the declaration
|
| 23 |
23 |
|
//! rather than growing the format.
|
| 24 |
24 |
|
//!
|
| 25 |
|
- |
//! Values are strings and nothing else. A number or a bool in a content file
|
| 26 |
|
- |
//! is refused naming the field, because a screen that reads copy is reading
|
| 27 |
|
- |
//! words, and the moment the format grows types it is a description language
|
| 28 |
|
- |
//! with two spellings.
|
|
25 |
+ |
//! Values are words: a string, or a list of them. A number or a bool in a
|
|
26 |
+ |
//! content file is refused naming the field, because a screen that reads copy is
|
|
27 |
+ |
//! reading words, and the moment the format grows types it is a description
|
|
28 |
+ |
//! language with two spellings.
|
|
29 |
+ |
//!
|
|
30 |
+ |
//! A list is admitted because it is still words and not a type. `/use-cases`
|
|
31 |
+ |
//! is the site: nine cards, each ending in a bulleted list of features, and the
|
|
32 |
+ |
//! alternative to a list field was nine numbered fields with the count written
|
|
33 |
+ |
//! into the declaration. What a list may hold is strings, so there is no
|
|
34 |
+ |
//! nesting to grow into a tree, and it is read by a nested loop:
|
|
35 |
+ |
//!
|
|
36 |
+ |
//! ```text
|
|
37 |
+ |
//! for profile in copy "content/use-cases.toml" as profiles {
|
|
38 |
+ |
//! label profile.title;
|
|
39 |
+ |
//! list {
|
|
40 |
+ |
//! for feature in profile.features {
|
|
41 |
+ |
//! row feature;
|
|
42 |
+ |
//! }
|
|
43 |
+ |
//! }
|
|
44 |
+ |
//! }
|
|
45 |
+ |
//! ```
|
|
46 |
+ |
//!
|
|
47 |
+ |
//! The inner loop is unrolled by the same pass as the outer one, so neither
|
|
48 |
+ |
//! reaches [`crate::ast`]. Its binder is the element itself and has no fields,
|
|
49 |
+ |
//! which is what tells the two loops apart at a glance: `profile.title` reads a
|
|
50 |
+ |
//! field and `feature` is one.
|
| 29 |
51 |
|
|
| 30 |
52 |
|
use std::cell::RefCell;
|
| 31 |
53 |
|
use std::collections::BTreeMap;
|
| 118 |
140 |
|
}
|
| 119 |
141 |
|
}
|
| 120 |
142 |
|
|
|
143 |
+ |
/// One field's value: words, or a list of them.
|
|
144 |
+ |
///
|
|
145 |
+ |
/// Two and no third. A list holds strings, so a content file cannot grow a
|
|
146 |
+ |
/// tree, and the reason is the module header's: copy is words.
|
|
147 |
+ |
enum Value {
|
|
148 |
+ |
Text(String),
|
|
149 |
+ |
List(Vec<String>),
|
|
150 |
+ |
}
|
|
151 |
+ |
|
| 121 |
152 |
|
/// One entry's fields, by name.
|
| 122 |
|
- |
type Entry = BTreeMap<String, String>;
|
|
153 |
+ |
type Entry = BTreeMap<String, Value>;
|
| 123 |
154 |
|
|
| 124 |
155 |
|
/// The entries under `key`, read from the file the declaration named.
|
| 125 |
156 |
|
///
|
| 169 |
200 |
|
};
|
| 170 |
201 |
|
let mut entry = Entry::new();
|
| 171 |
202 |
|
for (name, field) in fields {
|
| 172 |
|
- |
let Some(text) = field.as_str() else {
|
| 173 |
|
- |
return Err(syn::Error::new(
|
|
203 |
+ |
let refused = || {
|
|
204 |
+ |
syn::Error::new(
|
| 174 |
205 |
|
key.span(),
|
| 175 |
|
- |
format!("{named} entry {index} field `{name}` is not a string: copy is words"),
|
| 176 |
|
- |
));
|
|
206 |
+ |
format!(
|
|
207 |
+ |
"{named} entry {index} field `{name}` is not a string \
|
|
208 |
+ |
or a list of them: copy is words"
|
|
209 |
+ |
),
|
|
210 |
+ |
)
|
| 177 |
211 |
|
};
|
| 178 |
|
- |
entry.insert(name.clone(), text.to_owned());
|
|
212 |
+ |
let value = if let Some(text) = field.as_str() {
|
|
213 |
+ |
Value::Text(text.to_owned())
|
|
214 |
+ |
} else if let Some(array) = field.as_array() {
|
|
215 |
+ |
let mut items = Vec::with_capacity(array.len());
|
|
216 |
+ |
for element in array {
|
|
217 |
+ |
items.push(element.as_str().ok_or_else(refused)?.to_owned());
|
|
218 |
+ |
}
|
|
219 |
+ |
Value::List(items)
|
|
220 |
+ |
} else {
|
|
221 |
+ |
return Err(refused());
|
|
222 |
+ |
};
|
|
223 |
+ |
entry.insert(name.clone(), value);
|
| 179 |
224 |
|
}
|
| 180 |
225 |
|
entries.push(entry);
|
| 181 |
226 |
|
}
|
| 189 |
234 |
|
/// production in the grammar, drifting from the first the way a second emitter
|
| 190 |
235 |
|
/// would. `binder.field` is three tokens in every position the grammar admits,
|
| 191 |
236 |
|
/// which is what makes the walk exact rather than approximate.
|
|
237 |
+ |
///
|
|
238 |
+ |
/// A nested `for <x> in <binder>.<field> { .. }` over a list field is unrolled
|
|
239 |
+ |
/// here too, once per element, with the inner binder standing for the element
|
|
240 |
+ |
/// itself. It is the same unrolling as the outer loop and for the same reason:
|
|
241 |
+ |
/// a loop the emitter can fold is one that never reaches the AST.
|
| 192 |
242 |
|
fn substitute(
|
| 193 |
243 |
|
template: TokenStream,
|
| 194 |
244 |
|
binder: &Ident,
|
| 196 |
246 |
|
file: &LitStr,
|
| 197 |
247 |
|
) -> Result<TokenStream> {
|
| 198 |
248 |
|
let mut filled = Vec::new();
|
| 199 |
|
- |
let mut tokens = template.into_iter().peekable();
|
|
249 |
+ |
let tokens: Vec<TokenTree> = template.into_iter().collect();
|
|
250 |
+ |
let mut index = 0;
|
| 200 |
251 |
|
|
| 201 |
|
- |
while let Some(token) = tokens.next() {
|
| 202 |
|
- |
match token {
|
|
252 |
+ |
while index < tokens.len() {
|
|
253 |
+ |
if let Some((items, next)) = nested(&tokens, index, binder, entry, file)? {
|
|
254 |
+ |
filled.extend(items);
|
|
255 |
+ |
index = next;
|
|
256 |
+ |
continue;
|
|
257 |
+ |
}
|
|
258 |
+ |
match &tokens[index] {
|
| 203 |
259 |
|
TokenTree::Group(group) => {
|
| 204 |
260 |
|
let inner = substitute(group.stream(), binder, entry, file)?;
|
| 205 |
261 |
|
let mut replacement = Group::new(group.delimiter(), inner);
|
| 206 |
262 |
|
replacement.set_span(group.span());
|
| 207 |
263 |
|
filled.push(TokenTree::Group(replacement));
|
|
264 |
+ |
index += 1;
|
| 208 |
265 |
|
}
|
| 209 |
|
- |
TokenTree::Ident(ident) if ident == *binder => {
|
| 210 |
|
- |
let field = field_after(&mut tokens, &ident)?;
|
| 211 |
|
- |
let Some(text) = entry.get(&field.to_string()) else {
|
| 212 |
|
- |
return Err(syn::Error::new(
|
| 213 |
|
- |
field.span(),
|
| 214 |
|
- |
format!("{} has no `{field}` in {}", binder, file.value()),
|
| 215 |
|
- |
));
|
| 216 |
|
- |
};
|
| 217 |
|
- |
let mut literal = Literal::string(&escaped(text));
|
| 218 |
|
- |
literal.set_span(ident.span());
|
| 219 |
|
- |
filled.push(TokenTree::Literal(literal));
|
|
266 |
+ |
TokenTree::Ident(ident) if ident == binder => {
|
|
267 |
+ |
let field = field_after(&tokens, index, ident)?;
|
|
268 |
+ |
filled.push(literal(text(entry, &field, binder, file)?, ident.span()));
|
|
269 |
+ |
index += 3;
|
|
270 |
+ |
}
|
|
271 |
+ |
other => {
|
|
272 |
+ |
filled.push(other.clone());
|
|
273 |
+ |
index += 1;
|
| 220 |
274 |
|
}
|
| 221 |
|
- |
other => filled.push(other),
|
| 222 |
275 |
|
}
|
| 223 |
276 |
|
}
|
| 224 |
277 |
|
Ok(filled.into_iter().collect())
|
| 225 |
278 |
|
}
|
| 226 |
279 |
|
|
| 227 |
|
- |
/// The field named after the binder, or an error saying the binder is text.
|
| 228 |
|
- |
fn field_after(
|
| 229 |
|
- |
tokens: &mut std::iter::Peekable<proc_macro2::token_stream::IntoIter>,
|
|
280 |
+ |
/// A `for <x> in <binder>.<field> { .. }` at `index`, unrolled.
|
|
281 |
+ |
///
|
|
282 |
+ |
/// `Ok(None)` when the tokens there are not one, which is the ordinary case and
|
|
283 |
+ |
/// not a failure. The body is written out once per element with `x` replaced by
|
|
284 |
+ |
/// the element, and `x` is replaced as a bare ident because an element of a list
|
|
285 |
+ |
/// is text and has no fields to read.
|
|
286 |
+ |
fn nested(
|
|
287 |
+ |
tokens: &[TokenTree],
|
|
288 |
+ |
index: usize,
|
| 230 |
289 |
|
binder: &Ident,
|
| 231 |
|
- |
) -> Result<Ident> {
|
|
290 |
+ |
entry: &Entry,
|
|
291 |
+ |
file: &LitStr,
|
|
292 |
+ |
) -> Result<Option<(Vec<TokenTree>, usize)>> {
|
|
293 |
+ |
let [
|
|
294 |
+ |
TokenTree::Ident(keyword),
|
|
295 |
+ |
TokenTree::Ident(inner),
|
|
296 |
+ |
TokenTree::Ident(in_word),
|
|
297 |
+ |
TokenTree::Ident(outer),
|
|
298 |
+ |
TokenTree::Punct(dot),
|
|
299 |
+ |
TokenTree::Ident(field),
|
|
300 |
+ |
TokenTree::Group(body),
|
|
301 |
+ |
] = &tokens[index..tokens.len().min(index + 7)]
|
|
302 |
+ |
else {
|
|
303 |
+ |
return Ok(None);
|
|
304 |
+ |
};
|
|
305 |
+ |
if keyword != "for" || in_word != "in" || outer != binder || dot.as_char() != '.' {
|
|
306 |
+ |
return Ok(None);
|
|
307 |
+ |
}
|
|
308 |
+ |
if body.delimiter() != proc_macro2::Delimiter::Brace {
|
|
309 |
+ |
return Ok(None);
|
|
310 |
+ |
}
|
|
311 |
+ |
|
|
312 |
+ |
let Some(value) = entry.get(&field.to_string()) else {
|
|
313 |
+ |
return Err(syn::Error::new(
|
|
314 |
+ |
field.span(),
|
|
315 |
+ |
format!("{binder} has no `{field}` in {}", file.value()),
|
|
316 |
+ |
));
|
|
317 |
+ |
};
|
|
318 |
+ |
let Value::List(items) = value else {
|
|
319 |
+ |
return Err(syn::Error::new(
|
|
320 |
+ |
field.span(),
|
|
321 |
+ |
format!(
|
|
322 |
+ |
"`{binder}.{field}` in {} is one string, so it is written and not \
|
|
323 |
+ |
iterated",
|
|
324 |
+ |
file.value()
|
|
325 |
+ |
),
|
|
326 |
+ |
));
|
|
327 |
+ |
};
|
|
328 |
+ |
|
|
329 |
+ |
let mut filled = Vec::new();
|
|
330 |
+ |
for item in items {
|
|
331 |
+ |
filled.extend(element(body.stream(), inner, item, file)?);
|
|
332 |
+ |
}
|
|
333 |
+ |
Ok(Some((filled, index + 7)))
|
|
334 |
+ |
}
|
|
335 |
+ |
|
|
336 |
+ |
/// One pass of a nested loop's body, with its binder standing for `item`.
|
|
337 |
+ |
///
|
|
338 |
+ |
/// The binder is the element and not a record, so a `binder.field` here is a
|
|
339 |
+ |
/// field read against text. That is refused naming both, because the likely
|
|
340 |
+ |
/// mistake is reaching for the outer loop's record from inside the inner one.
|
|
341 |
+ |
fn element(
|
|
342 |
+ |
template: TokenStream,
|
|
343 |
+ |
binder: &Ident,
|
|
344 |
+ |
item: &str,
|
|
345 |
+ |
file: &LitStr,
|
|
346 |
+ |
) -> Result<Vec<TokenTree>> {
|
|
347 |
+ |
let mut filled = Vec::new();
|
|
348 |
+ |
let tokens: Vec<TokenTree> = template.into_iter().collect();
|
|
349 |
+ |
let mut index = 0;
|
|
350 |
+ |
|
|
351 |
+ |
while index < tokens.len() {
|
|
352 |
+ |
match &tokens[index] {
|
|
353 |
+ |
TokenTree::Group(group) => {
|
|
354 |
+ |
let inner = element(group.stream(), binder, item, file)?;
|
|
355 |
+ |
let mut replacement = Group::new(group.delimiter(), inner.into_iter().collect());
|
|
356 |
+ |
replacement.set_span(group.span());
|
|
357 |
+ |
filled.push(TokenTree::Group(replacement));
|
|
358 |
+ |
}
|
|
359 |
+ |
TokenTree::Ident(ident) if ident == binder => {
|
|
360 |
+ |
if let Some(TokenTree::Punct(punct)) = tokens.get(index + 1)
|
|
361 |
+ |
&& punct.as_char() == '.'
|
|
362 |
+ |
{
|
|
363 |
+ |
return Err(syn::Error::new(
|
|
364 |
+ |
ident.span(),
|
|
365 |
+ |
format!(
|
|
366 |
+ |
"`{binder}` is one entry of a list in {}, which is text: \
|
|
367 |
+ |
it has no fields",
|
|
368 |
+ |
file.value()
|
|
369 |
+ |
),
|
|
370 |
+ |
));
|
|
371 |
+ |
}
|
|
372 |
+ |
filled.push(literal(item, ident.span()));
|
|
373 |
+ |
}
|
|
374 |
+ |
other => filled.push(other.clone()),
|
|
375 |
+ |
}
|
|
376 |
+ |
index += 1;
|
|
377 |
+ |
}
|
|
378 |
+ |
Ok(filled)
|
|
379 |
+ |
}
|
|
380 |
+ |
|
|
381 |
+ |
/// One field's words, or an error saying a list is iterated rather than written.
|
|
382 |
+ |
fn text<'a>(entry: &'a Entry, field: &Ident, binder: &Ident, file: &LitStr) -> Result<&'a str> {
|
|
383 |
+ |
let Some(value) = entry.get(&field.to_string()) else {
|
|
384 |
+ |
return Err(syn::Error::new(
|
|
385 |
+ |
field.span(),
|
|
386 |
+ |
format!("{binder} has no `{field}` in {}", file.value()),
|
|
387 |
+ |
));
|
|
388 |
+ |
};
|
|
389 |
+ |
match value {
|
|
390 |
+ |
Value::Text(text) => Ok(text),
|
|
391 |
+ |
Value::List(_) => Err(syn::Error::new(
|
|
392 |
+ |
field.span(),
|
|
393 |
+ |
format!(
|
|
394 |
+ |
"`{binder}.{field}` in {} is a list, so it is iterated and not \
|
|
395 |
+ |
written: `for <x> in {binder}.{field} {{ .. }}`",
|
|
396 |
+ |
file.value()
|
|
397 |
+ |
),
|
|
398 |
+ |
)),
|
|
399 |
+ |
}
|
|
400 |
+ |
}
|
|
401 |
+ |
|
|
402 |
+ |
/// Copy as a string literal, spanned where it was written.
|
|
403 |
+ |
fn literal(text: &str, span: proc_macro2::Span) -> TokenTree {
|
|
404 |
+ |
let mut literal = Literal::string(&escaped(text));
|
|
405 |
+ |
literal.set_span(span);
|
|
406 |
+ |
TokenTree::Literal(literal)
|
|
407 |
+ |
}
|
|
408 |
+ |
|
|
409 |
+ |
/// The field named after the binder, or an error saying the binder is text.
|
|
410 |
+ |
fn field_after(tokens: &[TokenTree], index: usize, binder: &Ident) -> Result<Ident> {
|
| 232 |
411 |
|
let lone = || {
|
| 233 |
412 |
|
syn::Error::new(
|
| 234 |
413 |
|
binder.span(),
|
| 235 |
414 |
|
format!("`{binder}` is one entry's copy: name a field, as `{binder}.heading`"),
|
| 236 |
415 |
|
)
|
| 237 |
416 |
|
};
|
| 238 |
|
- |
match tokens.next() {
|
|
417 |
+ |
match tokens.get(index + 1) {
|
| 239 |
418 |
|
Some(TokenTree::Punct(punct)) if punct.as_char() == '.' => {}
|
| 240 |
419 |
|
_ => return Err(lone()),
|
| 241 |
420 |
|
}
|
| 242 |
|
- |
match tokens.next() {
|
| 243 |
|
- |
Some(TokenTree::Ident(field)) => Ok(field),
|
|
421 |
+ |
match tokens.get(index + 2) {
|
|
422 |
+ |
Some(TokenTree::Ident(field)) => Ok(field.clone()),
|
| 244 |
423 |
|
_ => Err(lone()),
|
| 245 |
424 |
|
}
|
| 246 |
425 |
|
}
|