Skip to main content

wat_fmt/
lib.rs

1#![no_std]
2#![warn(clippy::pedantic)]
3extern crate alloc;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7#[cfg(feature = "wasm")]
8use wasm_bindgen::prelude::*;
9
10#[cfg(feature = "wasm")]
11#[global_allocator]
12static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
13
14enum Token {
15    LParen,
16    RParen,
17    Atom(String),
18}
19
20fn tokenize(input: &str) -> Vec<Token> {
21    let mut tokens = Vec::new();
22    let mut chars = input.chars().peekable();
23
24    while let Some(c) = chars.next() {
25        if c.is_whitespace() {
26            continue;
27        }
28        if c == '(' {
29            tokens.push(Token::LParen);
30        } else if c == ')' {
31            tokens.push(Token::RParen);
32        } else if c == '"' {
33            let mut s = String::new();
34            s.push('"');
35            while let Some(&next) = chars.peek() {
36                s.push(next);
37                chars.next();
38                if next == '"' {
39                    break;
40                }
41            }
42            tokens.push(Token::Atom(s));
43        } else {
44            let mut s = String::new();
45            s.push(c);
46            while let Some(&next) = chars.peek() {
47                if next.is_whitespace() || next == '(' || next == ')' {
48                    break;
49                }
50                s.push(next);
51                chars.next();
52            }
53            tokens.push(Token::Atom(s));
54        }
55    }
56
57    tokens
58}
59
60enum Node {
61    Atom(String),
62    List(Vec<Node>),
63}
64
65fn parse_node(tokens: &[Token], mut i: usize) -> (Node, usize) {
66    if i >= tokens.len() {
67        return (Node::Atom(String::new()), i);
68    }
69    match &tokens[i] {
70        Token::LParen => {
71            i += 1;
72            let mut children = Vec::new();
73            while i < tokens.len() {
74                if let Token::RParen = tokens[i] {
75                    i += 1; // consume the RParen
76                    break;
77                }
78                let (child, new_i) = parse_node(tokens, i);
79                children.push(child);
80                i = new_i;
81            }
82            (Node::List(children), i)
83        }
84        Token::RParen => (Node::Atom(String::from(")")), i + 1),
85        Token::Atom(ref s) => (Node::Atom(s.clone()), i + 1),
86    }
87}
88
89fn parse_all(tokens: &[Token]) -> Vec<Node> {
90    let mut nodes = Vec::new();
91    let mut i = 0;
92    while i < tokens.len() {
93        let (node, new_i) = parse_node(tokens, i);
94        nodes.push(node);
95        i = new_i;
96    }
97    nodes
98}
99
100fn indent_str(indent: usize) -> String {
101    let mut s = String::new();
102    for _ in 0..indent {
103        s.push_str("  ");
104    }
105    s
106}
107
108/// Returns true if the node and all its children can be printed inline.
109fn is_flat_node(node: &Node) -> bool {
110    match node {
111        Node::Atom(_) => true,
112        Node::List(children) => children.iter().all(is_flat_node),
113    }
114}
115
116fn is_flat_list(nodes: &[Node]) -> bool {
117    nodes.iter().all(is_flat_node)
118}
119
120/// Print node inline without extra formatting.
121fn format_node_inline(node: &Node) -> String {
122    match node {
123        Node::Atom(s) => s.clone(),
124        Node::List(children) => {
125            let mut s = String::new();
126            s.push('(');
127            let mut first = true;
128            for child in children {
129                if !first {
130                    s.push(' ');
131                }
132                s.push_str(&format_node_inline(child));
133                first = false;
134            }
135            s.push(')');
136            s
137        }
138    }
139}
140
141/// Returns Some(inline) if the node is “flat” (only contains inline data).
142#[allow(dead_code)]
143fn format_inline(node: &Node) -> Option<String> {
144    if is_flat_node(node) {
145        Some(format_node_inline(node))
146    } else {
147        None
148    }
149}
150
151/// Check for inline signature markers.
152fn is_inline_signature(node: &Node) -> bool {
153    if let Node::List(children) = node {
154        if let Some(Node::Atom(ref keyword)) = children.first() {
155            return keyword == "export" || keyword == "param" || keyword == "result";
156        }
157    }
158    false
159}
160
161/// Check whether a token looks like an opcode rather than a parameter or literal.
162fn is_opcode(token: &str) -> bool {
163    if token.starts_with('$') || token.starts_with('"') {
164        return false;
165    }
166    let mut chars = token.chars();
167    if let Some(first) = chars.next() {
168        if (first == '-' || first == '+') && chars.clone().all(|c| c.is_ascii_digit()) {
169            return false;
170        }
171        if first.is_ascii_digit() && token.chars().all(|c| c.is_ascii_digit()) {
172            return false;
173        }
174    }
175    true
176}
177
178/// Format the instructions in a more readable way.
179fn format_instructions(nodes: &[Node], base_indent: usize) -> String {
180    let mut result = String::new();
181    let mut current_indent = base_indent;
182    let mut i = 0;
183    while i < nodes.len() {
184        match &nodes[i] {
185            Node::Atom(token) => {
186                if token == "if" {
187                    result.push('\n');
188                    result.push_str(&indent_str(current_indent));
189                    result.push_str("if");
190                    current_indent += 1;
191                    i += 1;
192                } else if token == "else" {
193                    // Outdent to match the "if"
194                    current_indent -= 1;
195                    result.push('\n');
196                    result.push_str(&indent_str(current_indent));
197                    result.push_str("else");
198                    // indent the else body
199                    current_indent += 1;
200                    i += 1;
201                } else if token == "end" {
202                    current_indent = current_indent.saturating_sub(1);
203                    result.push('\n');
204                    result.push_str(&indent_str(current_indent));
205                    result.push_str("end");
206                    i += 1;
207                } else if is_opcode(token) {
208                    // Start a new instruction line: group arguments (non-opcodes) with this opcode.
209                    let mut line = token.clone();
210                    i += 1;
211                    while i < nodes.len() {
212                        if let Node::Atom(next_token) = &nodes[i] {
213                            if is_opcode(next_token)
214                                || next_token == "if"
215                                || next_token == "else"
216                                || next_token == "end"
217                            {
218                                break;
219                            }
220                            line.push(' ');
221                            line.push_str(next_token);
222                            i += 1;
223                        } else {
224                            break;
225                        }
226                    }
227                    result.push('\n');
228                    result.push_str(&indent_str(current_indent));
229                    result.push_str(&line);
230                } else {
231                    // For non-opcode atoms, print them on their own line.
232                    result.push('\n');
233                    result.push_str(&indent_str(current_indent));
234                    result.push_str(token);
235                    i += 1;
236                }
237            }
238            Node::List(_) => {
239                result.push('\n');
240                result.push_str(&indent_str(current_indent));
241                result.push_str(&format_node(&nodes[i], current_indent));
242                i += 1;
243            }
244        }
245    }
246    result
247}
248
249/// Format a node with indentation.
250fn format_node(node: &Node, indent: usize) -> String {
251    match node {
252        Node::Atom(s) => s.clone(),
253        Node::List(children) => {
254            if children.is_empty() {
255                return String::from("()");
256            }
257            // Special handling for “module”:
258            if let Some(Node::Atom(ref ident)) = children.first() {
259                if ident == "module" {
260                    let mut s = String::new();
261                    s.push('(');
262                    s.push_str(ident);
263                    for child in children.iter().skip(1) {
264                        s.push('\n');
265                        s.push_str(&indent_str(indent + 1));
266                        s.push_str(&format_node(child, indent + 1));
267                    }
268                    s.push('\n');
269                    s.push_str(&indent_str(indent));
270                    s.push(')');
271                    return s;
272                } else if ident == "func" {
273                    let mut s = String::new();
274                    s.push('(');
275                    // Always print the “func” keyword inline.
276                    s.push_str(&format_node_inline(&children[0]));
277                    let mut i = 1;
278                    // Inline printing for function name and inline signatures.
279                    while i < children.len() {
280                        // If this is an atom and it looks like an opcode (i.e. an instruction),
281                        // then stop printing inline.
282                        if let Node::Atom(ref tok) = children[i] {
283                            if is_opcode(tok) {
284                                break;
285                            }
286                        }
287                        if let Node::List(_) = children[i] {
288                            if !is_inline_signature(&children[i]) {
289                                break;
290                            }
291                        }
292                        s.push(' ');
293                        s.push_str(&format_node_inline(&children[i]));
294                        i += 1;
295                    }
296                    // Format the remaining nodes as instructions.
297                    s.push_str(&format_instructions(&children[i..], indent + 1));
298                    s.push('\n');
299                    s.push_str(&indent_str(indent));
300                    s.push(')');
301                    return s;
302                } else if ["forall", "exists", "assume", "unique"].contains(&ident.as_str()) {
303                    let mut s = String::new();
304                    s.push('(');
305                    s.push_str(ident);
306                    s.push_str(&format_instructions(&children[1..], indent + 1));
307                    s.push('\n');
308                    s.push_str(&indent_str(indent));
309                    s.push(')');
310                    return s;
311                }
312            }
313            // For lists that are flat, use the inline formatter.
314            if is_flat_list(children) {
315                format_node_inline(node)
316            } else {
317                let mut s = String::new();
318                s.push('(');
319                let mut first = true;
320                for child in children {
321                    if first {
322                        s.push_str(&format_node(child, indent + 1));
323                        first = false;
324                    } else {
325                        s.push('\n');
326                        s.push_str(&indent_str(indent + 1));
327                        s.push_str(&format_node(child, indent + 1));
328                    }
329                }
330                s.push('\n');
331                s.push_str(&indent_str(indent));
332                s.push(')');
333                s
334            }
335        }
336    }
337}
338
339/// Format the input `WAT` string into a readable format.
340#[cfg_attr(feature = "wasm", wasm_bindgen)]
341#[must_use]
342pub fn format(input: &str) -> String {
343    let tokens = tokenize(input);
344    let nodes = parse_all(&tokens);
345    if nodes.len() == 1 {
346        format_node(&nodes[0], 0)
347    } else {
348        let mut s = String::new();
349        for node in nodes {
350            s.push_str(&format_node(&node, 0));
351            s.push('\n');
352        }
353        s
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn test_format() {
363        let input = r#"(module (func $add (param $a i32) (param $b i32) (result i32) (local $c i32) i32.uzumaki local.set $c local.get $a local.get $c i32.add) (export "add" (func $add) ) )"#;
364        let expected = r#"(module
365  (func $add (param $a i32) (param $b i32) (result i32)
366    (local $c i32)
367    i32.uzumaki
368    local.set $c
369    local.get $a
370    local.get $c
371    i32.add
372  )
373  (export "add" (func $add))
374)"#;
375        let output = format(input);
376        assert_eq!(output, expected);
377    }
378}