wasm_contract/
parser.rs

1//! Parsers to get a wasm contract from text
2//!
3//! The grammar of the text format is:
4//! contract = contract-entry+
5//! contract-entry = import-assertion | export-assertion
6//!
7//! import-assertion = "(" "assert_import" import-entry+ ")"
8//! import-entry = import-fn | import-global
9//! import-fn = "(" "func" namespace name param-list? result-list? ")"
10//! import-global = "(" "global" namespace name type-decl ")"
11//!
12//! export-assertion = "(" "assert_export" export-entry+ ")"
13//! export-entry = export-fn | export-global
14//! export-fn = "(" "func" name param-list? result-list? ")"
15//! export-global = "(" "global" name type-decl ")"
16//!
17//! param-list = "(" param type* ")"
18//! result-list = "(" result type* ")"
19//! type-decl = "(" "type" type ")"
20//! namespace = "\"" identifier "\""
21//! name = "\"" identifier "\""
22//! identifier = any character that's not a whitespace character or an open or close parenthesis
23//! type = "i32" | "i64" | "f32" | "f64"
24//!
25//! + means 1 or more
26//! * means 0 or more
27//! ? means 0 or 1
28//! | means "or"
29//! "\"" means one `"` character
30//!
31//! comments start with a `;` character and go until a newline `\n` character is reached
32//! comments and whitespace are valid between any tokens
33
34use nom::{
35    branch::*,
36    bytes::complete::{escaped, is_not, tag},
37    character::complete::{char, multispace0, multispace1, one_of},
38    combinator::*,
39    error::context,
40    multi::{many0, many1},
41    sequence::{delimited, preceded, tuple},
42    IResult,
43};
44
45use crate::contract::*;
46
47/// Some example input:
48/// (assert_import (func "ns" "name" (param f64 i32) (result f64 i32)))
49/// (assert_export (func "name" (param f64 i32) (result f64 i32)))
50/// (assert_import (global "ns" "name" (type f64)))
51
52pub fn parse_contract(mut input: &str) -> Result<Contract, String> {
53    let mut import_found = true;
54    let mut export_found = true;
55    let mut contract = Contract::default();
56    while import_found || export_found {
57        if let Result::Ok((inp, out)) = preceded(space_comments, parse_imports)(input) {
58            for entry in out.into_iter() {
59                if let Some(dup) = contract.imports.insert(entry.get_key(), entry) {
60                    return Err(format!("Duplicate import found {:?}", dup));
61                }
62            }
63            input = inp;
64            import_found = true;
65        } else {
66            import_found = false;
67        }
68
69        if let Result::Ok((inp, out)) = preceded(space_comments, parse_exports)(input) {
70            for entry in out.into_iter() {
71                if let Some(dup) = contract.exports.insert(entry.get_key(), entry) {
72                    return Err(format!("Duplicate export found {:?}", dup));
73                }
74            }
75            input = inp;
76            export_found = true;
77        } else {
78            export_found = false;
79        }
80    }
81    if !input.is_empty() {
82        Err(format!("Could not parse remaining input: {}", input))
83    } else {
84        Ok(contract)
85    }
86}
87
88fn parse_comment(input: &str) -> IResult<&str, ()> {
89    map(
90        preceded(multispace0, preceded(char(';'), many0(is_not("\n")))),
91        |_| (),
92    )(input)
93}
94
95/// Consumes spaces and comments
96/// comments must terminate with a new line character
97fn space_comments<'a>(mut input: &'a str) -> IResult<&'a str, ()> {
98    let mut space_found = true;
99    let mut comment_found = true;
100    while space_found || comment_found {
101        let space: IResult<&'a str, _> = multispace1(input);
102        space_found = if let Result::Ok((inp, _)) = space {
103            input = inp;
104            true
105        } else {
106            false
107        };
108        comment_found = if let Result::Ok((inp, _)) = parse_comment(input) {
109            input = inp;
110            true
111        } else {
112            false
113        };
114    }
115    Ok((input, ()))
116}
117
118fn parse_imports(input: &str) -> IResult<&str, Vec<Import>> {
119    let parse_import_inner = context(
120        "assert_import",
121        preceded(
122            tag("assert_import"),
123            many1(preceded(space_comments, alt((func_import, global_import)))),
124        ),
125    );
126    s_exp(parse_import_inner)(input)
127}
128
129fn parse_exports(input: &str) -> IResult<&str, Vec<Export>> {
130    let parse_export_inner = context(
131        "assert_export",
132        preceded(
133            tag("assert_export"),
134            many1(preceded(space_comments, alt((func_export, global_export)))),
135        ),
136    );
137    s_exp(parse_export_inner)(input)
138}
139
140/// A quoted identifier, must be valid UTF8
141fn identifier(input: &str) -> IResult<&str, &str> {
142    let name_inner = escaped(is_not("\"\\"), '\\', one_of("\"n\\"));
143    context("identifier", delimited(char('"'), name_inner, char('"')))(input)
144}
145
146/// Parses a wasm primitive type
147fn wasm_type(input: &str) -> IResult<&str, WasmType> {
148    let i32_tag = map(tag("i32"), |_| WasmType::I32);
149    let i64_tag = map(tag("i64"), |_| WasmType::I64);
150    let f32_tag = map(tag("f32"), |_| WasmType::F32);
151    let f64_tag = map(tag("f64"), |_| WasmType::F64);
152
153    alt((i32_tag, i64_tag, f32_tag, f64_tag))(input)
154}
155
156/// Parses an S-expression
157fn s_exp<'a, O1, F>(inner: F) -> impl Fn(&'a str) -> IResult<&'a str, O1>
158where
159    F: Fn(&'a str) -> IResult<&'a str, O1>,
160{
161    delimited(
162        char('('),
163        preceded(space_comments, inner),
164        preceded(space_comments, char(')')),
165    )
166}
167
168/// (global "name" (type f64))
169fn global_import(input: &str) -> IResult<&str, Import> {
170    let global_type_inner = preceded(tag("type"), preceded(space_comments, wasm_type));
171    let type_s_exp = s_exp(global_type_inner);
172    let global_import_inner = context(
173        "global import inner",
174        preceded(
175            tag("global"),
176            map(
177                tuple((
178                    preceded(space_comments, identifier),
179                    preceded(space_comments, identifier),
180                    preceded(space_comments, type_s_exp),
181                )),
182                |(ns, name, var_type)| Import::Global {
183                    namespace: ns.to_string(),
184                    name: name.to_string(),
185                    var_type,
186                },
187            ),
188        ),
189    );
190    s_exp(global_import_inner)(input)
191}
192
193/// (global "name" (type f64))
194fn global_export(input: &str) -> IResult<&str, Export> {
195    let global_type_inner = preceded(tag("type"), preceded(space_comments, wasm_type));
196    let type_s_exp = s_exp(global_type_inner);
197    let global_export_inner = context(
198        "global export inner",
199        preceded(
200            tag("global"),
201            map(
202                tuple((
203                    preceded(space_comments, identifier),
204                    preceded(space_comments, type_s_exp),
205                )),
206                |(name, var_type)| Export::Global {
207                    name: name.to_string(),
208                    var_type,
209                },
210            ),
211        ),
212    );
213    s_exp(global_export_inner)(input)
214}
215
216/// (func "ns" "name" (param f64 i32) (result f64 i32))
217fn func_import(input: &str) -> IResult<&str, Import> {
218    let param_list_inner = preceded(tag("param"), many0(preceded(space_comments, wasm_type)));
219    let param_list = opt(s_exp(param_list_inner));
220    let result_list_inner = preceded(tag("result"), many0(preceded(space_comments, wasm_type)));
221    let result_list = opt(s_exp(result_list_inner));
222    let func_import_inner = context(
223        "func import inner",
224        preceded(
225            tag("func"),
226            map(
227                tuple((
228                    preceded(space_comments, identifier),
229                    preceded(space_comments, identifier),
230                    preceded(space_comments, param_list),
231                    preceded(space_comments, result_list),
232                )),
233                |(ns, name, pl, rl)| Import::Func {
234                    namespace: ns.to_string(),
235                    name: name.to_string(),
236                    params: pl.unwrap_or_default(),
237                    result: rl.unwrap_or_default(),
238                },
239            ),
240        ),
241    );
242    s_exp(func_import_inner)(input)
243}
244
245/// (func "name" (param f64 i32) (result f64 i32))
246fn func_export(input: &str) -> IResult<&str, Export> {
247    let param_list_inner = preceded(tag("param"), many0(preceded(space_comments, wasm_type)));
248    let param_list = opt(s_exp(param_list_inner));
249    let result_list_inner = preceded(tag("result"), many0(preceded(space_comments, wasm_type)));
250    let result_list = opt(s_exp(result_list_inner));
251    let func_export_inner = context(
252        "func export inner",
253        preceded(
254            tag("func"),
255            map(
256                tuple((
257                    preceded(space_comments, identifier),
258                    preceded(space_comments, param_list),
259                    preceded(space_comments, result_list),
260                )),
261                |(name, pl, rl)| Export::Func {
262                    name: name.to_string(),
263                    params: pl.unwrap_or_default(),
264                    result: rl.unwrap_or_default(),
265                },
266            ),
267        ),
268    );
269    s_exp(func_export_inner)(input)
270}
271
272#[cfg(test)]
273mod test {
274    use super::*;
275    use std::collections::HashMap;
276
277    #[test]
278    fn parse_wasm_type() {
279        let i32_res = wasm_type("i32").unwrap();
280        assert_eq!(i32_res, ("", WasmType::I32));
281        let i64_res = wasm_type("i64").unwrap();
282        assert_eq!(i64_res, ("", WasmType::I64));
283        let f32_res = wasm_type("f32").unwrap();
284        assert_eq!(f32_res, ("", WasmType::F32));
285        let f64_res = wasm_type("f64").unwrap();
286        assert_eq!(f64_res, ("", WasmType::F64));
287
288        assert!(wasm_type("i128").is_err());
289    }
290
291    #[test]
292    fn parse_identifier() {
293        let inner_str = "柴は可愛すぎるだと思います";
294        let input = format!("\"{}\"", &inner_str);
295        let parse_res = identifier(&input).unwrap();
296        assert_eq!(parse_res, ("", inner_str))
297    }
298
299    #[test]
300    fn parse_global_import() {
301        let parse_res = global_import("(global \"env\" \"length\" (type i32))").unwrap();
302        assert_eq!(
303            parse_res,
304            (
305                "",
306                Import::Global {
307                    namespace: "env".to_string(),
308                    name: "length".to_string(),
309                    var_type: WasmType::I32,
310                }
311            )
312        );
313    }
314
315    #[test]
316    fn parse_global_export() {
317        let parse_res = global_export("(global \"length\" (type i32))").unwrap();
318        assert_eq!(
319            parse_res,
320            (
321                "",
322                Export::Global {
323                    name: "length".to_string(),
324                    var_type: WasmType::I32,
325                }
326            )
327        );
328    }
329
330    #[test]
331    fn parse_func_import() {
332        let parse_res =
333            func_import("(func \"ns\" \"name\" (param f64 i32) (result f64 i32))").unwrap();
334        assert_eq!(
335            parse_res,
336            (
337                "",
338                Import::Func {
339                    namespace: "ns".to_string(),
340                    name: "name".to_string(),
341                    params: vec![WasmType::F64, WasmType::I32],
342                    result: vec![WasmType::F64, WasmType::I32],
343                }
344            )
345        );
346    }
347
348    #[test]
349    fn parse_func_export() {
350        let parse_res = func_export("(func \"name\" (param f64 i32) (result f64 i32))").unwrap();
351        assert_eq!(
352            parse_res,
353            (
354                "",
355                Export::Func {
356                    name: "name".to_string(),
357                    params: vec![WasmType::F64, WasmType::I32],
358                    result: vec![WasmType::F64, WasmType::I32],
359                }
360            )
361        );
362    }
363
364    #[test]
365    fn parse_imports_test() {
366        let parse_res = parse_imports(
367            "(assert_import (func \"ns\" \"name\" (param f64 i32) (result f64 i32)))",
368        )
369        .unwrap();
370        assert_eq!(
371            parse_res,
372            (
373                "",
374                vec![Import::Func {
375                    namespace: "ns".to_string(),
376                    name: "name".to_string(),
377                    params: vec![WasmType::F64, WasmType::I32],
378                    result: vec![WasmType::F64, WasmType::I32],
379                }]
380            )
381        );
382
383        let parse_res = parse_imports(
384            "(assert_import (func \"ns\" \"name\"  
385                                               (param f64 i32) (result f64 i32))
386    ( global \"env\" \"length\" ( type 
387;; i32 is the best type
388i32 )
389)
390                                          (func \"ns\" \"name2\" (param f32
391                                                                      i64)
392                               ;; The return value comes next
393                                                                (
394                                                                 result
395                                                                 f64
396                                                                 i32
397                                                                 )
398                                          ) 
399)",
400        )
401        .unwrap();
402        assert_eq!(
403            parse_res,
404            (
405                "",
406                vec![
407                    Import::Func {
408                        namespace: "ns".to_string(),
409                        name: "name".to_string(),
410                        params: vec![WasmType::F64, WasmType::I32],
411                        result: vec![WasmType::F64, WasmType::I32],
412                    },
413                    Import::Global {
414                        namespace: "env".to_string(),
415                        name: "length".to_string(),
416                        var_type: WasmType::I32,
417                    },
418                    Import::Func {
419                        namespace: "ns".to_string(),
420                        name: "name2".to_string(),
421                        params: vec![WasmType::F32, WasmType::I64],
422                        result: vec![WasmType::F64, WasmType::I32],
423                    },
424                ]
425            )
426        );
427    }
428
429    #[test]
430    fn top_level_test() {
431        let parse_res = parse_contract(
432            " (assert_import (func \"ns\" \"name\" (param f64 i32) (result f64 i32)))
433 (assert_export (func \"name2\" (param) (result i32)))
434 (assert_import (global \"env\" \"length\" (type f64)))",
435        )
436        .unwrap();
437
438        let imports = vec![
439            Import::Func {
440                namespace: "ns".to_string(),
441                name: "name".to_string(),
442                params: vec![WasmType::F64, WasmType::I32],
443                result: vec![WasmType::F64, WasmType::I32],
444            },
445            Import::Global {
446                namespace: "env".to_string(),
447                name: "length".to_string(),
448                var_type: WasmType::F64,
449            },
450        ];
451        let exports = vec![Export::Func {
452            name: "name2".to_string(),
453            params: vec![],
454            result: vec![WasmType::I32],
455        }];
456        let import_map = imports
457            .into_iter()
458            .map(|entry| (entry.get_key(), entry))
459            .collect::<HashMap<(String, String), Import>>();
460        let export_map = exports
461            .into_iter()
462            .map(|entry| (entry.get_key(), entry))
463            .collect::<HashMap<String, Export>>();
464        assert_eq!(
465            parse_res,
466            Contract {
467                imports: import_map,
468                exports: export_map,
469            }
470        );
471    }
472
473    #[test]
474    fn duplicates_not_allowed() {
475        let parse_res = parse_contract(
476            " (assert_import (func \"ns\" \"name\" (param f64 i32) (result f64 i32)))
477; test comment
478  ;; hello
479 (assert_import (func \"ns\" \"name\" (param) (result i32)))
480 (assert_import (global \"length\" (type f64)))
481
482",
483        );
484
485        assert!(parse_res.is_err());
486    }
487
488    #[test]
489    fn test_comment_space_parsing() {
490        let parse_res = space_comments(" ").unwrap();
491        assert_eq!(parse_res, ("", ()));
492        let parse_res = space_comments("").unwrap();
493        assert_eq!(parse_res, ("", ()));
494        let parse_res = space_comments("; hello\n").unwrap();
495        assert_eq!(parse_res, ("", ()));
496        let parse_res = space_comments("abc").unwrap();
497        assert_eq!(parse_res, ("abc", ()));
498    }
499
500    #[test]
501    fn test_param_elision() {
502        let parse_res = parse_contract(
503            " (assert_import (func \"ns\" \"name\" (result f64 i32)))
504(assert_export (func \"name\"))",
505        )
506        .unwrap();
507
508        let imports = vec![Import::Func {
509            namespace: "ns".to_string(),
510            name: "name".to_string(),
511            params: vec![],
512            result: vec![WasmType::F64, WasmType::I32],
513        }];
514        let exports = vec![Export::Func {
515            name: "name".to_string(),
516            params: vec![],
517            result: vec![],
518        }];
519        let import_map = imports
520            .into_iter()
521            .map(|entry| (entry.get_key(), entry))
522            .collect::<HashMap<(String, String), Import>>();
523        let export_map = exports
524            .into_iter()
525            .map(|entry| (entry.get_key(), entry))
526            .collect::<HashMap<String, Export>>();
527        assert_eq!(
528            parse_res,
529            Contract {
530                imports: import_map,
531                exports: export_map,
532            }
533        );
534    }
535
536    #[test]
537    fn typo_gets_caught() {
538        let contract_src = r#"
539(assert_import (func "env" "do_panic" (params i32 i64)))
540(assert_import (global "length" (type i32)))"#;
541        let result = parse_contract(contract_src);
542        assert!(result.is_err());
543    }
544}