Skip to main content

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}