watr
Version:
Light & fast WAT compiler – WebAssembly Text to binary, parse, print, transform
119 lines (104 loc) • 5.15 kB
JavaScript
import parse from './parse.js';
/**
* Formats a tree or a WAT (WebAssembly Text) string into a readable format.
*
* @param {string | Array} tree - The code to print. If a string is provided, it will be parsed into a tree structure first.
* @param {Object} [options={}] - Optional settings for printing.
* @param {string} [options.indent=' '] - The string used for one level of indentation. Defaults to two spaces.
* @param {string} [options.newline='\n'] - The string used for line breaks. Defaults to a newline character.
* @param {boolean} [options.comments=false] - Whether to include comments in the output. Defaults to false.
* @returns {string} The formatted WAT string.
*/
export default function print(tree, options = {}) {
if (typeof tree === 'string') tree = parse(tree);
let { indent=' ', newline='\n', comments=true } = options;
indent ||= '', newline ||= ''; // false -> str
// If tree[0] is a string but NOT starting with `;` (comment), it's a keyword like `module` - print as single node
// Otherwise it's multiple nodes (comments + module) - print each separately
if (typeof tree[0] === 'string' && tree[0][0] !== ';') return printNode(tree)
// Multiple top-level nodes - filter out comments if comments option is false
return tree
.filter(node => comments || !isComment(node))
.map(node => printNode(node))
.join(newline)
function isComment(node) {
return typeof node === 'string' && node[1] === ';'
}
function printNode(node, level = 0) {
if (!Array.isArray(node)) return node
let content = node[0]
if (!content) return ''
let afterLineComment = false // track if we just printed a line comment
// Special handling for try_table: keep catch clauses inline
if (content === 'try_table') {
let i = 1
// Add label if present
if (typeof node[i] === 'string' && node[i][0] === '$') content += ' ' + node[i++]
// Add blocktype if present
if (Array.isArray(node[i]) && (node[i][0] === 'result' || node[i][0] === 'type')) content += ' ' + printNode(node[i++], level)
// Add catch clauses inline
while (Array.isArray(node[i]) && /^catch/.test(node[i][0])) content += ' ' + printNode(node[i++], level).trim()
// Rest is body - print normally
for (; i < node.length; i++) content += Array.isArray(node[i]) ? newline + indent.repeat(level + 1) + printNode(node[i], level + 1) : ' ' + node[i]
return `(${content + newline + indent.repeat(level)})`
}
// flat node (no deep subnodes), eg. (i32.const 1), (module (export "") 1)
// not flat if contains line comments (they need their own line)
let flat = !!newline && node.length < 4 && !node.some(n => typeof n === 'string' && n[0] === ';' && n[1] === ';')
let curIndent = indent.repeat(level + 1)
for (let i = 1; i < node.length; i++) {
const sub = node[i]?.valueOf?.() ?? node[i] // "\00abc\ff" strings are stored as arrays but have ._ with original value
// comments - skip if not enabled
if (typeof sub === 'string' && sub[1] === ';') {
if (!comments) continue
// line comments (;;) - MUST end with newline to avoid consuming following elements
if (sub[0] === ';') {
if (newline) {
// prettified: own line with indent, next element adds its own newline
content += newline + curIndent + sub.trimEnd()
afterLineComment = true
} else {
// minified: keep inline but must have newline after
const last = content[content.length - 1]
if (last && last !== ' ' && last !== '(') content += ' '
content += sub.trimEnd() + '\n'
}
}
// block comments ((;...;)) can stay inline
else {
const last = content[content.length - 1]
if (last && last !== ' ' && last !== '(') content += ' '
content += sub.trimEnd()
}
}
// (<keyword> ...)
else if (Array.isArray(sub)) {
if (flat) flat = sub.every(sub => !Array.isArray(sub))
content += newline + curIndent + printNode(sub, level + 1)
afterLineComment = false
}
// data chunks "\00..."
else if (node[0] === 'data') {
flat = false;
if (newline || content[content.length-1] !== ')') content += newline || ' '
content += curIndent + sub
afterLineComment = false
}
// inline nodes
else {
const last = content[content.length - 1]
// after line comment in prettified mode, need newline + indent
if (afterLineComment && newline) content += newline + curIndent
// after newline from line comment (minified), add indent
else if (last === '\n') content += ''
else if (last && last !== ')' && last !== ' ') content += ' '
else if (newline || last === ')') content += ' '
content += sub
afterLineComment = false
}
}
// shrink unnecessary spaces
if (flat) return `(${content.replaceAll(newline + curIndent + '(', ' (')})`
return `(${content + newline + indent.repeat(level)})`
}
}