Skip to main content

zdc_bench/
sizes.rs

1//! Bundle size, which §14A.4 also makes a deliverable.
2//!
3//! Bytes as shipped, uncompressed and unminified — there is no minifier in
4//! the pipeline, so a minified figure would be a claim about a tool that
5//! does not exist. Every arm's runtime cost is listed alongside, because a
6//! bundle that is small only by leaving the runtime out is not small.
7
8use zdc_codegen::{Bundle, Options};
9
10/// One compiled example, in bytes.
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct BundleSize {
13    pub name: String,
14    pub client_js: usize,
15    pub boot_js: usize,
16    pub styles_css: usize,
17    pub index_html: usize,
18    pub manifest_json: usize,
19}
20
21impl BundleSize {
22    /// Everything a build writes, excluding the runtime, which is shared.
23    pub fn total(&self) -> usize {
24        self.client_js + self.boot_js + self.styles_css + self.index_html + self.manifest_json
25    }
26}
27
28pub fn repository_path(relative: &str) -> std::path::PathBuf {
29    std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
30        .join("../..")
31        .join(relative)
32}
33
34/// Run the whole pipeline over a source, keeping the diagnostics.
35///
36/// The five passes are §17.1.2's, in §17.1.2's order, because the emitted
37/// sizes are only the compiler's sizes if the pipeline is the compiler's.
38///
39/// A refusal is a result here rather than a failure: which constructs the
40/// compiler still refuses is exactly what the benchmark's documented gap is
41/// made of, and a test pins it. Placement, type and flow errors join
42/// emission refusals in that result for the same reason — a benchmark arm
43/// the language cannot yet express should report why, not crash. Parse and
44/// resolve errors join them too, so that a survey over every example in the
45/// repository can report which ones do not build instead of aborting on the
46/// first (§14A.4).
47pub fn try_compile(source: &str, name: &str) -> Result<Bundle, Vec<String>> {
48    let program = zdc_parser::parse(source).map_err(|e| vec![e.message])?;
49    let hir = zdc_resolve::Resolver::new(&program)
50        .resolve()
51        .map_err(|errors| errors.into_iter().map(|e| e.message).collect::<Vec<_>>())?;
52
53    let split = zdc_graph::split(&hir);
54    if split.has_errors() {
55        return Err(split.errors().map(|error| error.message.clone()).collect());
56    }
57    let verdict = zdc_graph::ifc(&hir, &split);
58    let table = zdc_types::check(&hir, &split)
59        .map_err(|errors| errors.into_iter().map(|e| e.message).collect::<Vec<_>>())?;
60
61    // The flow pass's own permission to emit. There is no other way to
62    // build an `Inputs`, so forgetting to ask is a compile error.
63    let Some(cleared) = verdict.clearance() else {
64        return Err(verdict
65            .errors()
66            .map(|error| error.message.clone())
67            .collect());
68    };
69
70    let options = Options::new(name, "bench");
71    let inputs = zdc_codegen::Inputs {
72        hir: &hir,
73        split: &split,
74        verdict: &verdict,
75        table: &table,
76        cleared,
77    };
78    zdc_codegen::compile(&inputs, &options)
79        .map_err(|errors| errors.into_iter().map(|e| e.message).collect())
80}
81
82/// Compile a file in the repository, or fail with every diagnostic.
83pub fn compile(relative: &str) -> Bundle {
84    let source = std::fs::read_to_string(repository_path(relative))
85        .unwrap_or_else(|e| panic!("reading {relative}: {e}"));
86    try_compile(&source, relative).unwrap_or_else(|errors| {
87        panic!(
88            "{relative} failed to compile:\n{}",
89            errors
90                .iter()
91                .map(|message| format!("  {message}"))
92                .collect::<Vec<_>>()
93                .join("\n")
94        )
95    })
96}
97
98/// Every example this compiler can build today, sized.
99pub fn bundle_sizes() -> Vec<BundleSize> {
100    [
101        "examples/hello.zd",
102        "examples/counter.zd",
103        "crates/zdc-bench/bench/row.zd",
104    ]
105    .into_iter()
106    .map(|relative| {
107        let bundle = compile(relative);
108        BundleSize {
109            name: relative.to_string(),
110            client_js: bundle.client_js.len(),
111            // The two lines that used to be an inline `<script>` (#146).
112            // Counted, because "everything a build writes" is what the
113            // total above claims to be.
114            boot_js: bundle.boot_js.as_ref().map_or(0, String::len),
115            styles_css: bundle.styles_css.len(),
116            // Every program sized here has a `view`; a module with none
117            // ships no page, and zero is the honest number for it.
118            index_html: bundle.index_html.as_ref().map_or(0, String::len),
119            manifest_json: bundle.manifest_json.len(),
120        }
121    })
122    .collect()
123}
124
125/// The runtime files a bundle links against, in bytes.
126///
127/// `elements.js` is listed separately because generated code never imports
128/// it (§16.3.1) — it is what the direct-emission arm would have shipped.
129///
130/// `foreign.js` is annotated for the same kind of reason in the opposite
131/// direction: it *is* shipped, but only to a program that writes a
132/// `foreign … gives view`, so adding its bytes to the two above would
133/// overstate what an ordinary page downloads. Which files a given bundle
134/// links is `Bundle::runtime`, and the per-program table above is what
135/// reports it.
136pub fn runtime_sizes() -> Vec<(&'static str, usize)> {
137    // As a release build ships them. The module doc above says "bytes as
138    // shipped", and since #140 a module's source and what a reader
139    // downloads are two different lengths: the `// $dev` assertions are in
140    // the file and not in the bundle. Measuring the file would report a
141    // cost nobody pays.
142    let shipped = |source| zdc_runtime::for_mode(source, zdc_runtime::Mode::Release).len();
143    vec![
144        ("runtime/signal.js", shipped(zdc_runtime::SIGNAL_JS)),
145        ("runtime/dom.js", shipped(zdc_runtime::DOM_JS)),
146        // No backticks inside the label: the table wraps every name in a
147        // code span, and a nested pair closes it early.
148        (
149            "runtime/foreign.js (a gives-view foreign only)",
150            shipped(zdc_runtime::FOREIGN_JS),
151        ),
152        (
153            "runtime/markup.js (a program with Prose only)",
154            shipped(zdc_runtime::MARKUP_JS),
155        ),
156        (
157            "runtime/list.js (a program with an each only)",
158            shipped(zdc_runtime::LIST_JS),
159        ),
160        ("runtime/base.css", zdc_runtime::BASE_CSS.len()),
161        (
162            "runtime/elements.js (direct emission only)",
163            shipped(zdc_runtime::ELEMENTS_JS),
164        ),
165    ]
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn every_listed_example_compiles_and_is_not_empty() {
174        let sizes = bundle_sizes();
175        assert_eq!(sizes.len(), 3, "three examples are listed: {sizes:?}");
176        for size in sizes {
177            assert!(size.client_js > 0, "{} emitted nothing", size.name);
178            assert!(size.total() > size.client_js);
179        }
180    }
181}