wasm_interface/
parser.rs

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