max / quasi
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
6 files changed,
+134 insertions,
-12 deletions
| @@ -310,6 +310,12 @@ | |||
| 310 | 310 | Activate(Action), | |
| 311 | 311 | /// `include <hole>;` -- whatever another shape built. | |
| 312 | 312 | Include(Hole), | |
| 313 | + | /// `include each <shape>`: a shape answering many members, spliced whole. | |
| 314 | + | /// | |
| 315 | + | /// The plural of [`Include`](Self::Include), and a separate emission rather | |
| 316 | + | /// than a flag on it because what it places is a run and not a node: only | |
| 317 | + | /// a container that holds many members can take one. | |
| 318 | + | IncludeEach(Hole), | |
| 313 | 319 | /// `link <arg> to <action>;` | |
| 314 | 320 | Link { text: Arg, action: Action }, | |
| 315 | 321 | /// `region <arg> as <kind> { .. }` |
| @@ -677,6 +677,12 @@ | |||
| 677 | 677 | None => Ok((node, quote!(.with(#held)))), | |
| 678 | 678 | } | |
| 679 | 679 | } | |
| 680 | + | // A shape that answers many members, spliced whole. The plural of | |
| 681 | + | // `include`, and only a container that holds many can take one. | |
| 682 | + | (Container::Slot, Emission::IncludeEach(supplier)) => { | |
| 683 | + | let supplier = hole(supplier)?; | |
| 684 | + | Ok((quote!(#supplier), quote!(.with_all(#held)))) | |
| 685 | + | } | |
| 680 | 686 | (Container::Slot, other) => Ok((panel_member(other)?, quote!(.with(#held)))), | |
| 681 | 687 | (Container::Screen, Emission::Region { name, kind, body }) => { | |
| 682 | 688 | Ok((slot(name, kind, body)?, quote!(.with(#held)))) | |
| @@ -1019,6 +1025,14 @@ | |||
| 1019 | 1025 | /// One emission as a `Node` value. | |
| 1020 | 1026 | fn node(emission: &Emission) -> Result<TokenStream> { | |
| 1021 | 1027 | match emission { | |
| 1028 | + | // A run of members is not a node, so it can only be written where a | |
| 1029 | + | // container holds many. Refused where it is written rather than | |
| 1030 | + | // reported against generated code. | |
| 1031 | + | Emission::IncludeEach(_) => Err(syn::Error::new( | |
| 1032 | + | Span::call_site(), | |
| 1033 | + | "`include each` splices a run of members, so it belongs in a body \ | |
| 1034 | + | that holds many: write `include` for one node", | |
| 1035 | + | )), | |
| 1022 | 1036 | // `framed` says how a member is placed, so it has no meaning where a | |
| 1023 | 1037 | // bare node is wanted. Refused where it is written rather than silently | |
| 1024 | 1038 | // losing the name. | |
| @@ -1993,7 +2007,9 @@ | |||
| 1993 | 2007 | fn emission_span(emission: &Emission) -> Span { | |
| 1994 | 2008 | match emission { | |
| 1995 | 2009 | Emission::Framed { inner, .. } => emission_span(inner), | |
| 1996 | - | Emission::Include(_) | Emission::Link { .. } => Span::call_site(), | |
| 2010 | + | Emission::Include(_) | Emission::IncludeEach(_) | Emission::Link { .. } => { | |
| 2011 | + | Span::call_site() | |
| 2012 | + | } | |
| 1997 | 2013 | Emission::Simple { member, .. } => member.span(), | |
| 1998 | 2014 | Emission::Screen { arrangement, .. } => arrangement.span(), | |
| 1999 | 2015 | Emission::Row { .. } | Emission::List(_) => Span::call_site(), |
| @@ -100,6 +100,7 @@ | |||
| 100 | 100 | | Emission::At { inner, .. } => bodies(inner), | |
| 101 | 101 | Emission::Guarded { inner, .. } => bodies(inner), | |
| 102 | 102 | Emission::Include(_) | |
| 103 | + | | Emission::IncludeEach(_) | |
| 103 | 104 | | Emission::Link { .. } | |
| 104 | 105 | | Emission::Activate(_) | |
| 105 | 106 | | Emission::Given { .. } => &[], | |
| @@ -194,7 +195,7 @@ | |||
| 194 | 195 | | Emission::Offers { label, .. } | |
| 195 | 196 | | Emission::Offering { label, .. } | |
| 196 | 197 | | Emission::Removes { label, .. } => walk_arg(label, found), | |
| 197 | - | Emission::Include(hole) => note(hole, found), | |
| 198 | + | Emission::Include(hole) | Emission::IncludeEach(hole) => note(hole, found), | |
| 198 | 199 | Emission::Beside { inner, .. } | |
| 199 | 200 | | Emission::Framed { inner, .. } | |
| 200 | 201 | | Emission::At { inner, .. } => { |
| @@ -689,10 +689,31 @@ | |||
| 689 | 689 | )) | |
| 690 | 690 | } | |
| 691 | 691 | "include" => { | |
| 692 | + | // `include each <shape>` splices a shape that answers many | |
| 693 | + | // members. `each` cannot be a shape name here: a hole starts | |
| 694 | + | // with a path or a binding and the next token would be `(`. | |
| 695 | + | let every = input.peek(Ident) | |
| 696 | + | && input.fork().parse::<Ident>()? == "each" | |
| 697 | + | && !input.fork().parse::<Hole>().is_ok_and(|_| false); | |
| 698 | + | let every = every && { | |
| 699 | + | let fork = input.fork(); | |
| 700 | + | fork.parse::<Ident>()?; | |
| 701 | + | fork.peek(Ident) || fork.peek(Token![:]) | |
| 702 | + | }; | |
| 703 | + | if every { | |
| 704 | + | input.parse::<Ident>()?; | |
| 705 | + | } | |
| 692 | 706 | let supplier: Hole = input.parse()?; | |
| 693 | 707 | let guard = guard(input)?; | |
| 694 | 708 | input.parse::<Token![;]>()?; | |
| 695 | - | Ok(guarded(guard, Self::Include(supplier))) | |
| 709 | + | Ok(guarded( | |
| 710 | + | guard, | |
| 711 | + | if every { | |
| 712 | + | Self::IncludeEach(supplier) | |
| 713 | + | } else { | |
| 714 | + | Self::Include(supplier) | |
| 715 | + | }, | |
| 716 | + | )) | |
| 696 | 717 | } | |
| 697 | 718 | "given" => { | |
| 698 | 719 | let scrutinee: Hole = input.parse()?; |
| @@ -321,7 +321,9 @@ | |||
| 321 | 321 | // branches in render order and the filler walks them in declaration | |
| 322 | 322 | // order, so the two would disagree about which branch is which. | |
| 323 | 323 | Item::Attribute { name, args, guard } => { | |
| 324 | - | if let Some(guard) = guard { | |
| 324 | + | if let Some(guard) = guard | |
| 325 | + | && !placed(name) | |
| 326 | + | { | |
| 325 | 327 | return Err(syn::Error::new( | |
| 326 | 328 | guard.span, | |
| 327 | 329 | "a staged shape cannot guard a setting: a setting renders in \ | |
| @@ -329,6 +331,25 @@ | |||
| 329 | 331 | where the declaration wrote it", | |
| 330 | 332 | )); | |
| 331 | 333 | } | |
| 334 | + | if let Some(guard) = guard { | |
| 335 | + | let staged = self::guard(guard, counters); | |
| 336 | + | counters.enter(); | |
| 337 | + | let args = if structured(name) { | |
| 338 | + | through(args, counters)? | |
| 339 | + | } else { | |
| 340 | + | self::args(args, counters)? | |
| 341 | + | }; | |
| 342 | + | let body = counters.exit(); | |
| 343 | + | counters.wrote(Fill::Branch { | |
| 344 | + | guard: (*guard).clone(), | |
| 345 | + | body, | |
| 346 | + | }); | |
| 347 | + | return Ok(Item::Attribute { | |
| 348 | + | name: name.clone(), | |
| 349 | + | args, | |
| 350 | + | guard: Some(staged), | |
| 351 | + | }); | |
| 352 | + | } | |
| 332 | 353 | Item::Attribute { | |
| 333 | 354 | name: name.clone(), | |
| 334 | 355 | args: if structured(name) { | |
| @@ -466,7 +487,26 @@ | |||
| 466 | 487 | /// where the whole call is the value and staging through it would hand | |
| 467 | 488 | /// `tier_line` a sentinel where it wants a `&TierPrices`. Only the slot knows | |
| 468 | 489 | /// which of the two it is looking at. | |
| 469 | - | const STRUCTURED: &[&str] = &["token", "meter", "stats", "option"]; | |
| 490 | + | const STRUCTURED: &[&str] = &["token", "meter", "stats", "option", "figure"]; | |
| 491 | + | ||
| 492 | + | /// The settings that render where the declaration wrote them. | |
| 493 | + | /// | |
| 494 | + | /// A setting is refused a guard because most of them render in their | |
| 495 | + | /// container's opening tag: `measured`, `width`, `priority` are decided before | |
| 496 | + | /// any member is placed, so the branch a guard makes is not where it was | |
| 497 | + | /// written and the residual's branches and the filler's would be in different | |
| 498 | + | /// orders. | |
| 499 | + | /// | |
| 500 | + | /// `figure` is not one of those. A stats strip accretes its figures in | |
| 501 | + | /// declaration order and the renderer draws them in that order, so a guarded | |
| 502 | + | /// figure's branch is exactly where it was written. user_analytics has three, | |
| 503 | + | /// one per tone a delta can carry. | |
| 504 | + | const PLACED: &[&str] = &["figure"]; | |
| 505 | + | ||
| 506 | + | /// Whether a guard on this setting lands where it was written. | |
| 507 | + | fn placed(name: &Ident) -> bool { | |
| 508 | + | PLACED.contains(&name.to_string().as_str()) | |
| 509 | + | } | |
| 470 | 510 | ||
| 471 | 511 | /// Whether this slot takes a structure rather than a value. | |
| 472 | 512 | fn structured(name: &Ident) -> bool { | |
| @@ -677,18 +717,36 @@ | |||
| 677 | 717 | action: self::action(action, counters)?, | |
| 678 | 718 | }, | |
| 679 | 719 | Emission::Include(hole) => Emission::Include(include(hole, counters)?), | |
| 720 | + | // The plural, staged the same way: the callee has a twin of its own and | |
| 721 | + | // a filler of its own, and the caller splices a reference to both. | |
| 722 | + | Emission::IncludeEach(hole) => Emission::IncludeEach(include(hole, counters)?), | |
| 680 | 723 | Emission::Region { name, kind, body } => Emission::Region { | |
| 681 | 724 | name: arg(name, counters)?, | |
| 682 | 725 | kind: match kind { | |
| 683 | 726 | RegionKind::Variant(variant) => RegionKind::Variant(variant.clone()), | |
| 684 | - | // A `RegionKind` has no sentinel, and guessing a variant would | |
| 685 | - | // put a kind in the residual that no request asked for. | |
| 727 | + | // A `RegionKind` has no sentinel, so a supplied one is refused | |
| 728 | + | // -- unless nothing about it is supplied. `RegionKind::ceded("revenue-chart")` | |
| 729 | + | // names a path and passes a literal, which is the same value on | |
| 730 | + | // every request, so it is evaluated once while the residual is | |
| 731 | + | // derived. That is `#[constant]`'s rule one level down, and it | |
| 732 | + | // is the same test: every argument fixed where it is written. | |
| 686 | 733 | RegionKind::Supplied(hole) => { | |
| 687 | - | return Err(syn::Error::new( | |
| 688 | - | hole.span(), | |
| 689 | - | "a staged shape cannot take its region kind from a supplier: \ | |
| 690 | - | a `RegionKind` has no sentinel to stand in for it", | |
| 691 | - | )); | |
| 734 | + | let fixed_kind = match &hole.root { | |
| 735 | + | HoleRoot::Path(_) => hole.steps.is_empty(), | |
| 736 | + | HoleRoot::Call { args, .. } => { | |
| 737 | + | hole.steps.is_empty() && args.iter().all(fixed) | |
| 738 | + | } | |
| 739 | + | HoleRoot::Binding(_) => false, | |
| 740 | + | }; | |
| 741 | + | if !fixed_kind { | |
| 742 | + | return Err(syn::Error::new( | |
| 743 | + | hole.span(), | |
| 744 | + | "a staged shape cannot take its region kind from a supplier \ | |
| 745 | + | that reads the request: a `RegionKind` has no sentinel to \ | |
| 746 | + | stand in for it", | |
| 747 | + | )); | |
| 748 | + | } | |
| 749 | + | RegionKind::Supplied(hole.clone()) | |
| 692 | 750 | } | |
| 693 | 751 | }, | |
| 694 | 752 | body: items(body, counters)?, |
| @@ -5492,6 +5492,26 @@ | |||
| 5492 | 5492 | self | |
| 5493 | 5493 | } | |
| 5494 | 5494 | ||
| 5495 | + | /// Add every node a shape answered with, chaining. | |
| 5496 | + | /// | |
| 5497 | + | /// [`with`](Self::with)'s plural, and the whole of what `include each` | |
| 5498 | + | /// emits. A shape answering `Vec<Node>` has no region of its own -- what it | |
| 5499 | + | /// returns is a run of members and not one node -- so before this a caller | |
| 5500 | + | /// spread it by hand, with a loop whose body was `include node;`. | |
| 5501 | + | /// | |
| 5502 | + | /// That loop is why this exists. It reads as a loop over data and is a | |
| 5503 | + | /// splice, and a staged shape cannot make sense of it: the `include` names | |
| 5504 | + | /// a binding rather than a shape, so there is no twin to retarget to and no | |
| 5505 | + | /// filler to call. Said as one `include each`, the caller splices a | |
| 5506 | + | /// reference the way every other `include` does. | |
| 5507 | + | #[must_use] | |
| 5508 | + | pub fn with_all(mut self, nodes: impl IntoIterator<Item = Node>) -> Self { | |
| 5509 | + | for node in nodes { | |
| 5510 | + | self = self.with(node); | |
| 5511 | + | } | |
| 5512 | + | self | |
| 5513 | + | } | |
| 5514 | + | ||
| 5495 | 5515 | /// A named frame: a tab, or a disclosure's one panel. | |
| 5496 | 5516 | /// | |
| 5497 | 5517 | /// The only way to write a label, and it places the member at the same |