lumen_ir/fragment.rs
1//! Fragments - the reusable markup subtree.
2//!
3//! A fragment is a named piece of markup with parameters, declared once and
4//! instantiated anywhere. Lumen has one such entity, and both authoring forms
5//! compile to it: a `<template>` in a `.lmn` file, and an `lmn!` block in a
6//! script. The two forms differ in where the text came from, which
7//! [`FragmentKind`] records; everything downstream of the compiler sees the
8//! same [`Fragment`].
9//!
10//! The declaration side lives here. The use site lives on the element that
11//! instantiates a fragment, as [`crate::layout_ir::FragmentUse`], so a tree
12//! carries its own instantiation points and the table stays a flat lookup.
13//!
14//! [`FragmentTable`] is the whole set an app declares, and it travels in the
15//! compiled artifact next to the layout tree. It is ordered by key so the
16//! artifact bytes depend only on what the app declares, not on the order the
17//! compiler happened to visit files in: the same sources build the same
18//! bytes.
19
20use crate::layout_ir::Element;
21use std::collections::btree_map::Entry;
22use std::collections::{BTreeMap, BTreeSet};
23
24/// Tag of the element that marks where a use site's children land inside a
25/// fragment body.
26pub const SLOT_TAG: &str = "slot";
27
28/// Slot name a `<slot>` with no `name` attribute occupies, and the name a
29/// use site's unnamed children are passed under.
30pub const DEFAULT_SLOT: &str = "default";
31
32/// Tag standing for a use site the build could not finish, which the runtime
33/// fills by calling the function it names.
34///
35/// The element itself keeps the name a use site wrote, because that is what
36/// the call needs. This is what the element is *as a box*: an empty one, for
37/// as long as it takes the first tick to replace it.
38pub const FRAGMENT_TAG: &str = "fragment";
39
40/// Which authoring form declared a fragment.
41///
42/// The distinction is kept because diagnostics point back at source: a
43/// `<template>` collision reports a markup file and a line, an `lmn!`
44/// collision reports the script that expanded.
45#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
46pub enum FragmentKind {
47 /// Declared by a `<template>` element in markup.
48 #[default]
49 Template,
50 /// Declared by an `lmn!` block in a script.
51 Markup,
52}
53
54/// A candela function name that reaches a fragment.
55///
56/// A candela function returns an `lmn!` block, and a use site names that
57/// function as a tag. Nothing parses markup while an app runs, so the tree a
58/// use site stands for is baked before the app starts; `inlinable` is whether
59/// baking it is the whole story.
60///
61/// It is set when instantiating the fragment is the same as calling the
62/// function: the block is the function's whole body, and every value the block
63/// reads is one the caller passed. The build then puts the body at the use
64/// site and the subtree is there from the first frame.
65///
66/// It is clear when the function has to run, because it works a value out or
67/// picks between blocks. Every block it may return is compiled just the same;
68/// what stands at the use site is a marker the runtime fills on the first tick,
69/// by calling the function with [`params`](Self::params) and putting the node
70/// it returns in the marker's place.
71#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
72pub struct FragmentComponent {
73 /// The candela function's name, as a use site writes it.
74 pub name: String,
75 /// The function's parameters, in declaration order. A use site binds props
76 /// by name; a call passes them in this order.
77 pub params: Vec<String>,
78 /// Whether the baked body is what the call would have returned.
79 pub inlinable: bool,
80}
81
82/// One declared parameter of a fragment.
83///
84/// A parameter name is what an argument at the use site binds to, and what
85/// [`crate::layout_ir::InterpolationSlot::Arg`] names inside the body.
86#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
87pub struct FragmentParam {
88 /// Parameter name, without sigil.
89 pub name: String,
90 /// Value used when the use site passes no argument. A parameter with
91 /// `None` that the use site leaves out keeps its marker in the tree,
92 /// where the global signal scope resolves it at runtime.
93 pub default: Option<String>,
94}
95
96/// Where a fragment was declared, for diagnostics.
97///
98/// A fragment can carry more than one origin: the same declaration reaching
99/// the table twice (an include pulled in from two places, a script expanded
100/// per call site) merges instead of colliding, and every place it came from
101/// is kept so a later error can name all of them.
102#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
103pub struct FragmentOrigin {
104 /// Source file the declaration was read from.
105 pub file: String,
106 /// 1-based line of the declaration.
107 pub line: u32,
108 /// 1-based column of the declaration.
109 pub col: u32,
110}
111
112/// A named, parameterized markup subtree.
113#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
114pub struct Fragment {
115 /// Lookup key. A use site names this, and it is the key the fragment
116 /// occupies in a [`FragmentTable`].
117 pub key: String,
118 /// Declared parameters, in source order.
119 pub params: Vec<FragmentParam>,
120 /// The subtree instantiated at each use site. More than one root is
121 /// allowed: a fragment can expand to a sequence of siblings.
122 pub body: Vec<Element>,
123 /// Every place this fragment was declared. Never empty for a fragment
124 /// that came from source; a synthesized fragment may have none.
125 pub origins: Vec<FragmentOrigin>,
126 /// Which authoring form declared it.
127 pub kind: FragmentKind,
128 /// Candela function names that reach this fragment. Empty for a
129 /// `<template>`, which is reached by its own key.
130 pub components: Vec<FragmentComponent>,
131}
132
133/// Failures from building a [`FragmentTable`].
134#[derive(Debug, thiserror::Error)]
135pub enum FragmentError {
136 /// Two different fragments claim the same key. The build stops here
137 /// rather than picking one, because either choice silently changes what
138 /// half the use sites render.
139 #[error("fragment key `{key}` is declared twice with different content ({first} and {second})")]
140 Collision {
141 /// The contested key.
142 key: String,
143 /// Where the fragment already in the table came from.
144 first: String,
145 /// Where the rejected declaration came from.
146 second: String,
147 },
148}
149
150/// Every fragment an app declares, keyed by [`Fragment::key`].
151///
152/// Ordered rather than hashed: the artifact encodes the table field by field,
153/// so a stable order is what makes two builds of the same sources produce the
154/// same bytes.
155#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
156#[serde(transparent)]
157pub struct FragmentTable {
158 fragments: BTreeMap<String, Fragment>,
159}
160
161impl FragmentTable {
162 /// Empty table.
163 pub fn new() -> Self {
164 Self::default()
165 }
166
167 /// Add `fragment` under its own key.
168 ///
169 /// A key already holding the same declaration absorbs the new origins and
170 /// keeps one entry, which is what an include reached from two paths
171 /// produces. A key holding a different declaration is an error: keys are
172 /// content-addressed, so two different bodies under one key means the
173 /// content that produced the key was not the content being stored.
174 pub fn insert(&mut self, fragment: Fragment) -> Result<(), FragmentError> {
175 match self.fragments.entry(fragment.key.clone()) {
176 Entry::Vacant(slot) => {
177 slot.insert(fragment);
178 Ok(())
179 }
180 Entry::Occupied(mut slot) => {
181 let existing = slot.get_mut();
182 let same = match (fingerprint(existing), fingerprint(&fragment)) {
183 (Some(a), Some(b)) => a == b,
184 _ => false,
185 };
186 if !same {
187 return Err(FragmentError::Collision {
188 key: fragment.key,
189 first: describe_origins(&existing.origins),
190 second: describe_origins(&fragment.origins),
191 });
192 }
193 for origin in fragment.origins {
194 if !existing.origins.contains(&origin) {
195 existing.origins.push(origin);
196 }
197 }
198 for component in fragment.components {
199 if !existing.components.contains(&component) {
200 existing.components.push(component);
201 }
202 }
203 Ok(())
204 }
205 }
206 }
207
208 /// Look a fragment up by key.
209 pub fn get(&self, key: &str) -> Option<&Fragment> {
210 self.fragments.get(key)
211 }
212
213 /// Every fragment, in key order.
214 pub fn iter(&self) -> impl Iterator<Item = (&String, &Fragment)> {
215 self.fragments.iter()
216 }
217
218 /// Every fragment, in key order, for a pass that rewrites bodies.
219 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Fragment> {
220 self.fragments.values_mut()
221 }
222
223 /// The fragment a candela component name reaches, with whether the baked
224 /// body is what calling the function would have returned.
225 ///
226 /// A name no component carries yields `None`, which the caller reports as
227 /// an unknown tag.
228 pub fn by_component(&self, name: &str) -> Option<(&Fragment, bool)> {
229 self.fragments.values().find_map(|fragment| {
230 fragment
231 .components
232 .iter()
233 .find(|component| component.name == name)
234 .map(|component| (fragment, component.inlinable))
235 })
236 }
237
238 /// The candela component a use site naming `name` reaches.
239 ///
240 /// A function that has to run is reached by name alone, so this answers
241 /// without reference to the block it happens to be attached to.
242 pub fn component(&self, name: &str) -> Option<&FragmentComponent> {
243 self.fragments.values().find_map(|fragment| {
244 fragment
245 .components
246 .iter()
247 .find(|component| component.name == name)
248 })
249 }
250
251 /// Every name a use site may write: each fragment key, plus each candela
252 /// component name.
253 pub fn names(&self) -> BTreeSet<String> {
254 let mut names: BTreeSet<String> = self.fragments.keys().cloned().collect();
255 for fragment in self.fragments.values() {
256 for component in &fragment.components {
257 names.insert(component.name.clone());
258 }
259 }
260 names
261 }
262
263 /// How many fragments are declared.
264 pub fn len(&self) -> usize {
265 self.fragments.len()
266 }
267
268 /// Whether the table declares nothing.
269 pub fn is_empty(&self) -> bool {
270 self.fragments.is_empty()
271 }
272
273 /// Fold `other` into this table, applying the same collision rule per
274 /// key. Used to combine what markup declared with what the scripts
275 /// declared.
276 pub fn merge(&mut self, other: FragmentTable) -> Result<(), FragmentError> {
277 for (_, fragment) in other.fragments {
278 self.insert(fragment)?;
279 }
280 Ok(())
281 }
282}
283
284/// Canonical bytes for the parts of a fragment that decide what it renders.
285///
286/// [`Element`] carries no `PartialEq` (its attribute bag is a wide bag of
287/// float and enum specs), so the artifact's own codec stands in for
288/// structural equality. It writes fields in declaration order with no names,
289/// which makes equal bytes mean equal content. An encoding failure yields
290/// `None`, which the caller reads as "cannot prove these are the same" and
291/// reports as a collision rather than merging on an unchecked assumption.
292fn fingerprint(fragment: &Fragment) -> Option<Vec<u8>> {
293 bincode::serialize(&(&fragment.kind, &fragment.params, &fragment.body)).ok()
294}
295
296/// Render a fragment's origins for an error message. A fragment with no
297/// recorded origin reads as `<generated>`.
298fn describe_origins(origins: &[FragmentOrigin]) -> String {
299 if origins.is_empty() {
300 return "<generated>".to_string();
301 }
302 origins
303 .iter()
304 .map(|o| format!("{}:{}:{}", o.file, o.line, o.col))
305 .collect::<Vec<_>>()
306 .join(", ")
307}
308
309#[cfg(test)]
310mod tests {
311 use super::*;
312 use crate::layout_ir::Attributes;
313
314 fn origin(file: &str, line: u32) -> FragmentOrigin {
315 FragmentOrigin {
316 file: file.to_string(),
317 line,
318 col: 1,
319 }
320 }
321
322 fn body(text: &str) -> Vec<Element> {
323 vec![Element {
324 tag: "label".to_string(),
325 attrs: Attributes {
326 text: Some(text.to_string()),
327 ..Attributes::default()
328 },
329 ..Element::default()
330 }]
331 }
332
333 fn fragment(key: &str, text: &str, origins: Vec<FragmentOrigin>) -> Fragment {
334 Fragment {
335 key: key.to_string(),
336 params: vec![FragmentParam {
337 name: "label".to_string(),
338 default: None,
339 }],
340 body: body(text),
341 origins,
342 kind: FragmentKind::Template,
343 components: Vec::new(),
344 }
345 }
346
347 #[test]
348 fn identical_declarations_merge_origins() {
349 let mut table = FragmentTable::new();
350 table
351 .insert(fragment("card", "hi", vec![origin("a.lmn", 3)]))
352 .expect("first declaration");
353 table
354 .insert(fragment("card", "hi", vec![origin("b.lmn", 9)]))
355 .expect("the same body under the same key merges");
356
357 assert_eq!(table.len(), 1);
358 let card = table.get("card").expect("card is declared");
359 assert_eq!(card.origins.len(), 2);
360 assert_eq!(card.origins[1].file, "b.lmn");
361 }
362
363 #[test]
364 fn repeating_one_origin_does_not_duplicate_it() {
365 let mut table = FragmentTable::new();
366 table
367 .insert(fragment("card", "hi", vec![origin("a.lmn", 3)]))
368 .expect("first declaration");
369 table
370 .insert(fragment("card", "hi", vec![origin("a.lmn", 3)]))
371 .expect("same declaration, same place");
372
373 assert_eq!(table.get("card").expect("card").origins.len(), 1);
374 }
375
376 #[test]
377 fn different_bodies_under_one_key_error() {
378 let mut table = FragmentTable::new();
379 table
380 .insert(fragment("card", "hi", vec![origin("a.lmn", 3)]))
381 .expect("first declaration");
382 let err = table
383 .insert(fragment("card", "bye", vec![origin("b.lmn", 9)]))
384 .expect_err("a different body under the same key is a collision");
385
386 let FragmentError::Collision { key, first, second } = err;
387 assert_eq!(key, "card");
388 assert_eq!(first, "a.lmn:3:1");
389 assert_eq!(second, "b.lmn:9:1");
390 assert_eq!(table.len(), 1);
391 }
392
393 #[test]
394 fn different_params_under_one_key_error() {
395 let mut table = FragmentTable::new();
396 table
397 .insert(fragment("card", "hi", vec![origin("a.lmn", 3)]))
398 .expect("first declaration");
399 let mut other = fragment("card", "hi", vec![origin("b.lmn", 9)]);
400 other.params[0].default = Some("none".to_string());
401 table
402 .insert(other)
403 .expect_err("the same body with different parameters is a collision");
404 }
405
406 #[test]
407 fn merge_applies_the_same_rule() {
408 let mut left = FragmentTable::new();
409 left.insert(fragment("card", "hi", vec![origin("a.lmn", 3)]))
410 .expect("first declaration");
411 let mut right = FragmentTable::new();
412 right
413 .insert(fragment("card", "hi", vec![origin("b.lmn", 9)]))
414 .expect("first declaration");
415 right
416 .insert(fragment("row", "x", vec![origin("b.lmn", 20)]))
417 .expect("second key");
418
419 left.merge(right).expect("compatible tables merge");
420 assert_eq!(left.len(), 2);
421 assert_eq!(left.get("card").expect("card").origins.len(), 2);
422 }
423
424 #[test]
425 fn a_component_name_reaches_the_fragment_it_is_on() {
426 let mut table = FragmentTable::new();
427 let mut fragment = fragment("79114ba6b591efb1", "hi", vec![origin("main.cdl", 3)]);
428 fragment.kind = FragmentKind::Markup;
429 fragment.components.push(FragmentComponent {
430 name: "Home".to_string(),
431 params: Vec::new(),
432 inlinable: true,
433 });
434 table.insert(fragment).expect("first declaration");
435
436 let (found, inlinable) = table.by_component("Home").expect("Home reaches it");
437 assert_eq!(found.key, "79114ba6b591efb1");
438 assert!(inlinable);
439 assert!(table.by_component("Away").is_none());
440 assert_eq!(
441 table.names(),
442 ["79114ba6b591efb1".to_string(), "Home".to_string()].into()
443 );
444 }
445
446 #[test]
447 fn one_body_written_by_two_functions_carries_both_names() {
448 let mut table = FragmentTable::new();
449 for name in ["Home", "Away"] {
450 let mut fragment = fragment("shared", "hi", vec![origin("main.cdl", 3)]);
451 fragment.components.push(FragmentComponent {
452 name: name.to_string(),
453 params: Vec::new(),
454 inlinable: true,
455 });
456 table.insert(fragment).expect("the same body merges");
457 }
458
459 assert_eq!(table.len(), 1);
460 assert!(table.by_component("Home").is_some());
461 assert!(table.by_component("Away").is_some());
462 }
463
464 #[test]
465 fn iteration_is_key_ordered() {
466 let mut table = FragmentTable::new();
467 for key in ["zeta", "alpha", "mid"] {
468 table
469 .insert(fragment(key, key, vec![origin("a.lmn", 1)]))
470 .expect("distinct keys");
471 }
472 let keys: Vec<&str> = table.iter().map(|(k, _)| k.as_str()).collect();
473 assert_eq!(keys, ["alpha", "mid", "zeta"]);
474 }
475}