Skip to main content

zdc_bench/
shape.rs

1//! Proving the ZDeceptron arm is the compiler's output and not a flattering
2//! transcription of it.
3//!
4//! `each` in the view is refused today (§16.5, M5b), so the benchmark's list
5//! cannot come out of `zdc build`. What can, and does, is the row: its
6//! template, the walk to the holes, and the sequence of bindings attached at
7//! them. Those three things are what the row costs, so those three things
8//! are extracted from both sides and compared.
9//!
10//! What is deliberately *not* compared is the last argument of each binding
11//! — the getter. In the emission it reads a module signal; in the benchmark
12//! it reads the row's item, which is what `each` would supply. That
13//! substitution is the documented gap, and pinning everything around it is
14//! what keeps the gap from widening unnoticed.
15
16/// The parts of a row emission that determine what the row costs.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct RowShape {
19    /// The static HTML the region parses into, cloned per row.
20    pub template: String,
21    /// `const $nN = …;` — the compile-time offsets to each hole.
22    pub walk: Vec<String>,
23    /// Each binding, with its trailing getter or handler removed.
24    pub bindings: Vec<String>,
25}
26
27/// The shape of the row the compiler emits for `bench/row.zd`.
28pub fn emitted_row(client_js: &str) -> RowShape {
29    let template = template_argument(client_js)
30        .unwrap_or_else(|| panic!("the emitted module has no `template(…)` call:\n{client_js}"));
31    let (walk, bindings) = statements(client_js.lines());
32    RowShape {
33        template,
34        walk,
35        bindings,
36    }
37}
38
39/// The shape of the row the benchmark's ZDeceptron arm renders.
40pub fn benchmark_row(benchmark_js: &str) -> RowShape {
41    let template = row_html_constant(benchmark_js)
42        .unwrap_or_else(|| panic!("`js/benchmark.js` has no `const ROW_HTML = '…';`"));
43    let body = between(benchmark_js, "// ZDC-EMITTED-BEGIN", "// ZDC-EMITTED-END")
44        .unwrap_or_else(|| panic!("`js/benchmark.js` has no ZDC-EMITTED-BEGIN/END region"));
45    let (walk, bindings) = statements(body.lines());
46    RowShape {
47        template,
48        walk,
49        bindings,
50    }
51}
52
53/// The single-quoted argument of the first `template(…)` call.
54fn template_argument(source: &str) -> Option<String> {
55    let start = source.find("template('")? + "template('".len();
56    let end = source[start..].find("')")? + start;
57    Some(source[start..end].to_string())
58}
59
60/// The single-quoted value of `const ROW_HTML = '…';`.
61fn row_html_constant(source: &str) -> Option<String> {
62    let line = source
63        .lines()
64        .find(|line| line.starts_with("const ROW_HTML = '"))?;
65    let start = line.find('\'')? + 1;
66    let end = line.rfind('\'')?;
67    (end > start).then(|| line[start..end].to_string())
68}
69
70fn between<'a>(source: &'a str, open: &str, close: &str) -> Option<&'a str> {
71    let start = source.find(open)? + open.len();
72    let end = source[start..].find(close)? + start;
73    Some(&source[start..end])
74}
75
76/// Split a run of lines into the walk and the bindings.
77///
78/// `mount` is skipped: the emitted `main` mounts the region into a
79/// container, and a row is inserted by `each` instead. That is the one
80/// structural difference the gap forces, and naming it here keeps it from
81/// being mistaken for drift.
82fn statements<'a>(lines: impl Iterator<Item = &'a str>) -> (Vec<String>, Vec<String>) {
83    let mut walk = Vec::new();
84    let mut bindings = Vec::new();
85    for line in lines {
86        let line = line.trim();
87        if let Some(rest) = line.strip_prefix("const $n") {
88            walk.push(format!("const $n{rest}"));
89            continue;
90        }
91        for name in ["bindText", "bindAttr", "bindStyle", "on"] {
92            if line.starts_with(&format!("{name}(")) {
93                bindings.push(without_last_argument(line));
94            }
95        }
96    }
97    (walk, bindings)
98}
99
100/// A call with its final argument dropped: `on($n2, 'click', () => …)`
101/// becomes `on($n2, 'click')`.
102fn without_last_argument(call: &str) -> String {
103    let Some(open) = call.find('(') else {
104        return call.to_string();
105    };
106    let name = &call[..open];
107    let inner = &call[open + 1..];
108    let mut arguments: Vec<String> = Vec::new();
109    let mut current = String::new();
110    let mut depth = 0usize;
111    let mut quote: Option<char> = None;
112
113    for character in inner.chars() {
114        if let Some(open_quote) = quote {
115            current.push(character);
116            if character == open_quote {
117                quote = None;
118            }
119            continue;
120        }
121        match character {
122            '\'' | '"' => {
123                quote = Some(character);
124                current.push(character);
125            }
126            '(' | '[' | '{' => {
127                depth += 1;
128                current.push(character);
129            }
130            ')' if depth == 0 => break,
131            ')' | ']' | '}' => {
132                depth = depth.saturating_sub(1);
133                current.push(character);
134            }
135            ',' if depth == 0 => {
136                arguments.push(current.trim().to_string());
137                current.clear();
138            }
139            _ => current.push(character),
140        }
141    }
142    if !current.trim().is_empty() {
143        arguments.push(current.trim().to_string());
144    }
145    arguments.pop();
146    format!("{name}({})", arguments.join(", "))
147}
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152
153    #[test]
154    fn the_last_argument_is_dropped_whatever_it_contains() {
155        assert_eq!(
156            without_last_argument("on($n2, 'click', () => f(a, b));"),
157            "on($n2, 'click')"
158        );
159        assert_eq!(
160            without_last_argument("bindText($n1.firstChild, rowId);"),
161            "bindText($n1.firstChild)"
162        );
163        assert_eq!(
164            without_last_argument("bindAttr($n0, 'class', () => 'a, b' + (c)());"),
165            "bindAttr($n0, 'class')"
166        );
167    }
168
169    #[test]
170    fn a_walk_and_its_bindings_are_separated() {
171        let (walk, bindings) = statements(
172            [
173                "  const $n0 = $r.firstChild;",
174                "  bindText($n0, x);",
175                "  return mount($r, c);",
176            ]
177            .into_iter(),
178        );
179        assert_eq!(walk, vec!["const $n0 = $r.firstChild;"]);
180        assert_eq!(bindings, vec!["bindText($n0)"]);
181    }
182
183    #[test]
184    fn the_template_argument_is_read_out_of_an_emission() {
185        assert_eq!(
186            template_argument("const $t0 = template('<div></div>');\n").as_deref(),
187            Some("<div></div>")
188        );
189    }
190
191    #[test]
192    fn a_missing_marker_is_not_silently_an_empty_region() {
193        assert!(between("nothing here", "// A", "// B").is_none());
194    }
195}