specjam_codegen/
registry.rs

1//! test vector registry generator
2
3use anyhow::Result;
4use heck::ToUpperCamelCase;
5use proc_macro2::Span;
6use serde_json::Value;
7use std::{
8    fs::{self, ReadDir},
9    path::Path,
10};
11use syn::{parse_quote, Expr, Ident, Item, ItemMod, LitStr};
12
13/// The scale sections which contains both tiny and large test vectors
14const SCALE_SECTIONS: [&str; 6] = [
15    "assurances",
16    "authorizations",
17    "disputes",
18    "reports",
19    "safrole",
20    "statistics",
21];
22
23/// The general sections which have only one set of test vectors
24const GENERAL_SECTIONS: &[&str] = &["history", "preimages"];
25
26/// The test vector registry
27pub struct Registry<'s> {
28    /// The root directory of the test vectors
29    root: &'s Path,
30
31    /// The output directory
32    output: &'s Path,
33
34    /// The registry
35    registry: Vec<Item>,
36
37    /// The test vectors
38    tests: ItemMod,
39}
40
41impl<'s> Registry<'s> {
42    /// Generate the test vector registry
43    pub fn new(root: &'s Path, output: &'s Path) -> Self {
44        Self {
45            root,
46            output,
47            registry: Vec::new(),
48            tests: parse_quote!(
49                /// The test vectors
50                pub mod tests {}
51            ),
52        }
53    }
54
55    /// generate all tests
56    pub fn run(mut self) -> Result<()> {
57        self.scale()?;
58        self.general()?;
59        self.codec()?;
60        self.pvm()?;
61        self.shuffle()?;
62        self.trie()?;
63
64        // impl the registry
65        let all = self.extract_all_tests();
66        let tests = self.tests;
67        let items = self.registry;
68        let registry = quote::quote! {
69            #all
70
71            #(#items)*
72
73            #tests
74        };
75
76        // write the output to the output directory
77        let out = self.output.join("registry.rs");
78        fs::write(out, registry.to_string())?;
79        Ok(())
80    }
81
82    /// Extract all tests from the registry
83    fn extract_all_tests(&self) -> syn::Item {
84        let mut tests = Vec::new();
85        for item in &self.registry {
86            if let syn::Item::Const(syn::ItemConst { expr, .. }) = item {
87                if let syn::Expr::Array(syn::ExprArray { elems, .. }) = *expr.clone() {
88                    tests.extend(elems.into_iter())
89                }
90            }
91        }
92
93        let len = tests.len();
94        let len: Expr = parse_quote!(#len);
95        parse_quote! {
96            #[doc = "The all test vectors"]
97            pub const ALL_TESTS: [crate::Test; #len] = [#(#tests),*];
98        }
99    }
100
101    /// Generate the scale test vectors
102    fn scale(&mut self) -> Result<()> {
103        for section in SCALE_SECTIONS {
104            let path = self.root.join(section);
105            let mut tests = Vec::new();
106            for scale in crate::SCALE {
107                let path = path.join(scale);
108                let dir = fs::read_dir(&path)?;
109                tests.extend(self.process_base(section, dir, Some(scale.to_string()))?);
110            }
111            self.embed_namespace(section, tests);
112        }
113
114        Ok(())
115    }
116
117    /// Generate the general test vectors
118    fn general(&mut self) -> Result<()> {
119        for section in GENERAL_SECTIONS {
120            let path = self.root.join(section).join("data");
121            let dir = fs::read_dir(path)?;
122            let tests = self.process_base(section, dir, None)?;
123            self.embed_namespace(section, tests);
124        }
125
126        Ok(())
127    }
128
129    /// Generate the codec test vectors
130    fn codec(&mut self) -> Result<()> {
131        let Some((_, tests)) = self.tests.content.as_mut() else {
132            return Err(anyhow::anyhow!("tests already initialized"));
133        };
134
135        let mut const_tests = Vec::new();
136        for entry in fs::read_dir(self.root.join("codec").join("data"))? {
137            let path = entry?.path();
138            if path.extension().unwrap_or_default() != "json" {
139                continue;
140            }
141
142            let bin = hex::encode(fs::read(path.with_extension("bin"))?);
143            let parse = move |json: Value| Ok((json.to_string(), bin));
144            let test = wrap_test(tests, &None, "codec", &path, parse)?;
145            const_tests.push(test);
146        }
147
148        self.embed_namespace("codec", const_tests);
149        Ok(())
150    }
151
152    /// Generate the pvm test vectors
153    fn pvm(&mut self) -> Result<()> {
154        let Some((_, tests)) = self.tests.content.as_mut() else {
155            return Err(anyhow::anyhow!("tests already initialized"));
156        };
157
158        let section = "pvm";
159        let dir = fs::read_dir(self.root.join(section).join("programs"))?;
160        let mut const_tests = Vec::new();
161        for entry in dir {
162            let path = entry?.path();
163            let test = self::wrap_test(tests, &None, section, &path, |json| {
164                let input = serde_json::json!({
165                    "name": json["name"],
166                    "initial-regs": json["pre-state"],
167                    "initial-pc": json["initial-pc"],
168                    "initial-regs": json["initial-regs"],
169                    "initial-page-map": json["initial-page-map"],
170                    "initial-memory": json["initial-memory"],
171                    "initial-gas": json["initial-gas"],
172                    "program": json["program"],
173
174                })
175                .to_string();
176
177                let output = serde_json::json!({
178                    "expected-status": json["expected-status"],
179                    "expected-regs": json["expected-regs"],
180                    "expected-pc": json["expected-pc"],
181                    "expected-memory": json["expected-memory"],
182                    "expected-gas": json["expected-gas"],
183                })
184                .to_string();
185
186                Ok((input, output))
187            })?;
188
189            const_tests.push(test);
190        }
191
192        self.embed_namespace(section, const_tests);
193        Ok(())
194    }
195
196    /// Generate the trie test vectors
197    fn trie(&mut self) -> Result<()> {
198        let file = self.root.join("trie").join("trie.json");
199        let Some((_, tests)) = self.tests.content.as_mut() else {
200            return Err(anyhow::anyhow!("tests already initialized"));
201        };
202
203        let test = wrap_test(
204            tests,
205            &None,
206            "trie",
207            &file,
208            |json| -> Result<(String, String)> {
209                let vectors = json
210                    .as_array()
211                    .ok_or_else(|| anyhow::anyhow!("invalid trie test"))?;
212
213                let mut input = Vec::new();
214                let mut output = Vec::new();
215                for vector in vectors {
216                    input.push(serde_json::json!({
217                        "input": vector["input"],
218                    }));
219                    output.push(serde_json::json!({
220                        "output": vector["output"],
221                    }));
222                }
223
224                Ok((
225                    serde_json::to_string(&input)?,
226                    serde_json::to_string(&output)?,
227                ))
228            },
229        )?;
230
231        self.embed_namespace("trie", vec![test]);
232        Ok(())
233    }
234
235    /// Generate the shuffle test vectors
236    fn shuffle(&mut self) -> Result<()> {
237        let file = self.root.join("shuffle").join("shuffle_tests.json");
238        let Some((_, tests)) = self.tests.content.as_mut() else {
239            return Err(anyhow::anyhow!("tests already initialized"));
240        };
241
242        let test = wrap_test(
243            tests,
244            &None,
245            "shuffle",
246            &file,
247            |json| -> Result<(String, String)> {
248                let vectors = json
249                    .as_array()
250                    .ok_or_else(|| anyhow::anyhow!("invalid shuffle test"))?;
251
252                let mut input = Vec::new();
253                let mut output = Vec::new();
254                for vector in vectors {
255                    input.push(serde_json::json!({
256                        "input": vector["input"],
257                        "entropy": vector["entropy"],
258                    }));
259                    output.push(serde_json::json!({
260                        "output": vector["output"],
261                    }));
262                }
263
264                Ok((
265                    serde_json::to_string(&input)?,
266                    serde_json::to_string(&output)?,
267                ))
268            },
269        )?;
270
271        self.embed_namespace("shuffle", vec![test]);
272        Ok(())
273    }
274
275    fn process_base(
276        &mut self,
277        section: &str,
278        dir: ReadDir,
279        scale: Option<String>,
280    ) -> Result<Vec<syn::Path>> {
281        let Some((_, tests)) = self.tests.content.as_mut() else {
282            return Err(anyhow::anyhow!("tests already initialized"));
283        };
284
285        let mut const_tests = Vec::new();
286        for entry in dir {
287            let path = entry?.path();
288            if path.extension().unwrap_or_default() != "json" {
289                continue;
290            }
291
292            let test = self::wrap_test(tests, &scale, section, &path, |json| {
293                let input = serde_json::json!({
294                    "input": json["input"],
295                    "pre_state": json["pre_state"],
296                })
297                .to_string();
298
299                let output = serde_json::json!({
300                    "output": json["output"],
301                    "post_state": json["post_state"],
302                })
303                .to_string();
304
305                Ok((input, output))
306            })?;
307
308            const_tests.push(test);
309        }
310
311        Ok(const_tests)
312    }
313
314    /// Embed the namespace into the registry
315    fn embed_namespace(&mut self, section: &str, tests: Vec<syn::Path>) {
316        let namespace = Ident::new(&section.to_uppercase(), Span::call_site());
317        let tests_len = tests.len();
318        let tests_len: Expr = parse_quote!(#tests_len);
319        let doc = LitStr::new(
320            &format!("The test vectors for the {section} section"),
321            Span::call_site(),
322        );
323
324        self.registry.push(parse_quote! {
325                #[doc = #doc]
326                pub const #namespace: [crate::Test; #tests_len] = [#(#tests),*];
327        });
328    }
329}
330
331fn wrap_test<P>(
332    tests: &mut Vec<Item>,
333    scale: &Option<String>,
334    section: &str,
335    file: &Path,
336    parse: P,
337) -> Result<syn::Path>
338where
339    P: FnOnce(Value) -> Result<(String, String)>,
340{
341    // build the constant Test
342    let mut test = file
343        .with_extension("")
344        .file_name()
345        .ok_or_else(|| anyhow::anyhow!("invalid file name"))?
346        .to_string_lossy()
347        .replace('-', "_");
348
349    let doc = LitStr::new(
350        &format!("test vector {test} for {section}"),
351        Span::call_site(),
352    );
353
354    // read the json file
355    let json: Value = serde_json::from_slice(&fs::read(file)?)
356        .map_err(|e| anyhow::anyhow!("invalid json {file:?} : {e}"))?;
357    let (input, output) = parse(json)?;
358
359    // construct the constant Test
360    let test_lower = LitStr::new(&test, Span::call_site());
361    test = test.to_uppercase();
362    let const_test = {
363        let mut test = format!("TEST_{}_{test}", section.to_uppercase());
364        if let Some(scale) = &scale {
365            test.push_str(&format!("_{}", scale.to_uppercase()));
366        }
367        Ident::new(&test, Span::call_site())
368    };
369    let const_input = LitStr::new(&input, Span::call_site());
370    let const_output = LitStr::new(&output, Span::call_site());
371
372    // handle enum
373    let section_caml = Ident::new(&section.to_upper_camel_case(), Span::call_site());
374    let scale: Expr = if let Some(scale) = &scale {
375        let ident = Ident::new(&scale.to_upper_camel_case(), Span::call_site());
376        parse_quote!(Some(crate::Scale::#ident))
377    } else {
378        parse_quote!(None)
379    };
380
381    tests.push(parse_quote!(
382        #[doc = #doc]
383        pub const #const_test: crate::Test = crate::Test {
384            scale: #scale,
385            section: crate::Section::#section_caml,
386            name: #test_lower,
387            input: #const_input,
388            output: #const_output,
389        };
390    ));
391
392    Ok(parse_quote!(tests::#const_test))
393}