blob: e5ff03fae9fd40ce5ba592fef82ad49837955ca2 [file] [log] [blame]
/*
* Copyright 2016 WebAssembly Community Group participants
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
//
// Print out text in s-expression format
//
#include <wasm.h>
#include <wasm-printing.h>
#include <pass.h>
namespace wasm {
struct PrintSExpression : public Visitor<PrintSExpression> {
std::ostream& o;
unsigned indent = 0;
bool minify;
const char *maybeSpace;
const char *maybeNewLine;
bool full = false; // whether to not elide nodes in output when possible
// (like implicit blocks) and to emit types
Module* currModule = nullptr;
Function* currFunction = nullptr;
PrintSExpression(std::ostream& o) : o(o) {
setMinify(false);
if (getenv("BINARYEN_PRINT_FULL")) {
full = std::stoi(getenv("BINARYEN_PRINT_FULL"));
}
}
void visit(Expression* curr) {
if (currFunction) {
// show an annotation, if there is one
auto& annotations = currFunction->annotations;
auto iter = annotations.find(curr);
if (iter != annotations.end()) {
o << ";; " << iter->second << '\n';
doIndent(o, indent);
}
}
Visitor<PrintSExpression>::visit(curr);
}
void setMinify(bool minify_) {
minify = minify_;
maybeSpace = minify ? "" : " ";
maybeNewLine = minify ? "" : "\n";
}
void setFull(bool full_) { full = full_; }
void incIndent() {
if (minify) return;
o << '\n';
indent++;
}
void decIndent() {
if (!minify) {
indent--;
doIndent(o, indent);
}
o << ')';
}
void printFullLine(Expression *expression) {
!minify && doIndent(o, indent);
if (full) {
o << "[" << printWasmType(expression->type) << "] ";
}
visit(expression);
o << maybeNewLine;
}
Name printableLocal(Index index) {
Name name;
if (currFunction) {
name = currFunction->tryLocalName(index);
}
if (!name.is()) {
name = Name::fromInt(index);
}
return name;
}
std::ostream& printName(Name name) {
// we need to quote names if they have tricky chars
if (strpbrk(name.str, "()")) {
o << '"' << name << '"';
} else {
o << name;
}
return o;
}
void visitBlock(Block *curr) {
// special-case Block, because Block nesting (in their first element) can be incredibly deep
std::vector<Block*> stack;
while (1) {
if (stack.size() > 0) doIndent(o, indent);
stack.push_back(curr);
if (full) {
o << "[" << printWasmType(curr->type) << "] ";
}
printOpening(o, "block");
if (curr->name.is()) {
o << ' ';
printName(curr->name);
}
if (isConcreteWasmType(curr->type)) {
o << ' ' << printWasmType(curr->type);
}
incIndent();
if (curr->list.size() > 0 && curr->list[0]->is<Block>()) {
// recurse into the first element
curr = curr->list[0]->cast<Block>();
continue;
} else {
break; // that's all we can recurse, start to unwind
}
}
auto* top = stack.back();
while (stack.size() > 0) {
curr = stack.back();
stack.pop_back();
auto& list = curr->list;
for (size_t i = 0; i < list.size(); i++) {
if (curr != top && i == 0) {
// one of the block recursions we already handled
decIndent();
o << '\n';
continue;
}
printFullLine(list[i]);
}
}
decIndent();
}
void visitIf(If *curr) {
printOpening(o, "if");
if (isConcreteWasmType(curr->type)) {
o << ' ' << printWasmType(curr->type);
}
incIndent();
printFullLine(curr->condition);
// ifTrue and False have implict blocks, avoid printing them if possible
if (!full && curr->ifTrue->is<Block>() && curr->ifTrue->dynCast<Block>()->name.isNull() && curr->ifTrue->dynCast<Block>()->list.size() == 1) {
printFullLine(curr->ifTrue->dynCast<Block>()->list.back());
} else {
printFullLine(curr->ifTrue);
}
if (curr->ifFalse) {
if (!full && curr->ifFalse->is<Block>() && curr->ifFalse->dynCast<Block>()->name.isNull() && curr->ifFalse->dynCast<Block>()->list.size() == 1) {
printFullLine(curr->ifFalse->dynCast<Block>()->list.back());
} else {
printFullLine(curr->ifFalse);
}
}
decIndent();
}
void visitLoop(Loop *curr) {
printOpening(o, "loop");
if (curr->name.is()) {
o << ' ' << curr->name;
}
if (isConcreteWasmType(curr->type)) {
o << ' ' << printWasmType(curr->type);
}
incIndent();
auto block = curr->body->dynCast<Block>();
if (!full && block && block->name.isNull()) {
// wasm spec has loops containing children directly, while our ast
// has a single child for simplicity. print out the optimal form.
for (auto expression : block->list) {
printFullLine(expression);
}
} else {
printFullLine(curr->body);
}
decIndent();
}
void visitBreak(Break *curr) {
if (curr->condition) {
printOpening(o, "br_if ");
printName(curr->name);
incIndent();
} else {
printOpening(o, "br ");
printName(curr->name);
if (!curr->value || curr->value->is<Nop>()) {
// avoid a new line just for the parens
o << ')';
return;
}
incIndent();
}
if (curr->value && !curr->value->is<Nop>()) printFullLine(curr->value);
if (curr->condition) {
printFullLine(curr->condition);
}
decIndent();
}
void visitSwitch(Switch *curr) {
printOpening(o, "br_table");
for (auto& t : curr->targets) {
o << ' ' << t;
}
o << ' ' << curr->default_;
incIndent();
if (curr->value && !curr->value->is<Nop>()) printFullLine(curr->value);
printFullLine(curr->condition);
decIndent();
}
template<typename CallBase>
void printCallBody(CallBase* curr) {
printName(curr->target);
if (curr->operands.size() > 0) {
incIndent();
for (auto operand : curr->operands) {
printFullLine(operand);
}
decIndent();
} else {
o << ')';
}
}
void visitCall(Call *curr) {
printOpening(o, "call ");
printCallBody(curr);
}
void visitCallImport(CallImport *curr) {
printOpening(o, "call ");
printCallBody(curr);
}
void visitCallIndirect(CallIndirect *curr) {
printOpening(o, "call_indirect ") << curr->fullType;
incIndent();
for (auto operand : curr->operands) {
printFullLine(operand);
}
printFullLine(curr->target);
decIndent();
}
void visitGetLocal(GetLocal *curr) {
printOpening(o, "get_local ") << printableLocal(curr->index) << ')';
}
void visitSetLocal(SetLocal *curr) {
if (curr->isTee()) {
printOpening(o, "tee_local ");
} else {
printOpening(o, "set_local ");
}
o << printableLocal(curr->index);
incIndent();
printFullLine(curr->value);
decIndent();
}
void visitGetGlobal(GetGlobal *curr) {
printOpening(o, "get_global ");
printName(curr->name) << ')';
}
void visitSetGlobal(SetGlobal *curr) {
printOpening(o, "set_global ");
printName(curr->name);
incIndent();
printFullLine(curr->value);
decIndent();
}
void visitLoad(Load *curr) {
o << '(';
prepareColor(o) << printWasmType(curr->type) << ".load";
if (curr->bytes < 4 || (curr->type == i64 && curr->bytes < 8)) {
if (curr->bytes == 1) {
o << '8';
} else if (curr->bytes == 2) {
o << "16";
} else if (curr->bytes == 4) {
o << "32";
} else {
abort();
}
o << (curr->signed_ ? "_s" : "_u");
}
restoreNormalColor(o);
if (curr->offset) {
o << " offset=" << curr->offset;
}
if (curr->align != curr->bytes) {
o << " align=" << curr->align;
}
incIndent();
printFullLine(curr->ptr);
decIndent();
}
void visitStore(Store *curr) {
o << '(';
prepareColor(o) << printWasmType(curr->valueType) << ".store";
if (curr->bytes < 4 || (curr->valueType == i64 && curr->bytes < 8)) {
if (curr->bytes == 1) {
o << '8';
} else if (curr->bytes == 2) {
o << "16";
} else if (curr->bytes == 4) {
o << "32";
} else {
abort();
}
}
restoreNormalColor(o);
if (curr->offset) {
o << " offset=" << curr->offset;
}
if (curr->align != curr->bytes) {
o << " align=" << curr->align;
}
incIndent();
printFullLine(curr->ptr);
printFullLine(curr->value);
decIndent();
}
void visitConst(Const *curr) {
o << curr->value;
}
void visitUnary(Unary *curr) {
o << '(';
prepareColor(o);
switch (curr->op) {
case ClzInt32: o << "i32.clz"; break;
case CtzInt32: o << "i32.ctz"; break;
case PopcntInt32: o << "i32.popcnt"; break;
case EqZInt32: o << "i32.eqz"; break;
case ClzInt64: o << "i64.clz"; break;
case CtzInt64: o << "i64.ctz"; break;
case PopcntInt64: o << "i64.popcnt"; break;
case EqZInt64: o << "i64.eqz"; break;
case NegFloat32: o << "f32.neg"; break;
case AbsFloat32: o << "f32.abs"; break;
case CeilFloat32: o << "f32.ceil"; break;
case FloorFloat32: o << "f32.floor"; break;
case TruncFloat32: o << "f32.trunc"; break;
case NearestFloat32: o << "f32.nearest"; break;
case SqrtFloat32: o << "f32.sqrt"; break;
case NegFloat64: o << "f64.neg"; break;
case AbsFloat64: o << "f64.abs"; break;
case CeilFloat64: o << "f64.ceil"; break;
case FloorFloat64: o << "f64.floor"; break;
case TruncFloat64: o << "f64.trunc"; break;
case NearestFloat64: o << "f64.nearest"; break;
case SqrtFloat64: o << "f64.sqrt"; break;
case ExtendSInt32: o << "i64.extend_s/i32"; break;
case ExtendUInt32: o << "i64.extend_u/i32"; break;
case WrapInt64: o << "i32.wrap/i64"; break;
case TruncSFloat32ToInt32: o << "i32.trunc_s/f32"; break;
case TruncSFloat32ToInt64: o << "i64.trunc_s/f32"; break;
case TruncUFloat32ToInt32: o << "i32.trunc_u/f32"; break;
case TruncUFloat32ToInt64: o << "i64.trunc_u/f32"; break;
case TruncSFloat64ToInt32: o << "i32.trunc_s/f64"; break;
case TruncSFloat64ToInt64: o << "i64.trunc_s/f64"; break;
case TruncUFloat64ToInt32: o << "i32.trunc_u/f64"; break;
case TruncUFloat64ToInt64: o << "i64.trunc_u/f64"; break;
case ReinterpretFloat32: o << "i32.reinterpret/f32"; break;
case ReinterpretFloat64: o << "i64.reinterpret/f64"; break;
case ConvertUInt32ToFloat32: o << "f32.convert_u/i32"; break;
case ConvertUInt32ToFloat64: o << "f64.convert_u/i32"; break;
case ConvertSInt32ToFloat32: o << "f32.convert_s/i32"; break;
case ConvertSInt32ToFloat64: o << "f64.convert_s/i32"; break;
case ConvertUInt64ToFloat32: o << "f32.convert_u/i64"; break;
case ConvertUInt64ToFloat64: o << "f64.convert_u/i64"; break;
case ConvertSInt64ToFloat32: o << "f32.convert_s/i64"; break;
case ConvertSInt64ToFloat64: o << "f64.convert_s/i64"; break;
case PromoteFloat32: o << "f64.promote/f32"; break;
case DemoteFloat64: o << "f32.demote/f64"; break;
case ReinterpretInt32: o << "f32.reinterpret/i32"; break;
case ReinterpretInt64: o << "f64.reinterpret/i64"; break;
default: abort();
}
incIndent();
printFullLine(curr->value);
decIndent();
}
void visitBinary(Binary *curr) {
o << '(';
prepareColor(o);
switch (curr->op) {
case AddInt32: o << "i32.add"; break;
case SubInt32: o << "i32.sub"; break;
case MulInt32: o << "i32.mul"; break;
case DivSInt32: o << "i32.div_s"; break;
case DivUInt32: o << "i32.div_u"; break;
case RemSInt32: o << "i32.rem_s"; break;
case RemUInt32: o << "i32.rem_u"; break;
case AndInt32: o << "i32.and"; break;
case OrInt32: o << "i32.or"; break;
case XorInt32: o << "i32.xor"; break;
case ShlInt32: o << "i32.shl"; break;
case ShrUInt32: o << "i32.shr_u"; break;
case ShrSInt32: o << "i32.shr_s"; break;
case RotLInt32: o << "i32.rotl"; break;
case RotRInt32: o << "i32.rotr"; break;
case EqInt32: o << "i32.eq"; break;
case NeInt32: o << "i32.ne"; break;
case LtSInt32: o << "i32.lt_s"; break;
case LtUInt32: o << "i32.lt_u"; break;
case LeSInt32: o << "i32.le_s"; break;
case LeUInt32: o << "i32.le_u"; break;
case GtSInt32: o << "i32.gt_s"; break;
case GtUInt32: o << "i32.gt_u"; break;
case GeSInt32: o << "i32.ge_s"; break;
case GeUInt32: o << "i32.ge_u"; break;
case AddInt64: o << "i64.add"; break;
case SubInt64: o << "i64.sub"; break;
case MulInt64: o << "i64.mul"; break;
case DivSInt64: o << "i64.div_s"; break;
case DivUInt64: o << "i64.div_u"; break;
case RemSInt64: o << "i64.rem_s"; break;
case RemUInt64: o << "i64.rem_u"; break;
case AndInt64: o << "i64.and"; break;
case OrInt64: o << "i64.or"; break;
case XorInt64: o << "i64.xor"; break;
case ShlInt64: o << "i64.shl"; break;
case ShrUInt64: o << "i64.shr_u"; break;
case ShrSInt64: o << "i64.shr_s"; break;
case RotLInt64: o << "i64.rotl"; break;
case RotRInt64: o << "i64.rotr"; break;
case EqInt64: o << "i64.eq"; break;
case NeInt64: o << "i64.ne"; break;
case LtSInt64: o << "i64.lt_s"; break;
case LtUInt64: o << "i64.lt_u"; break;
case LeSInt64: o << "i64.le_s"; break;
case LeUInt64: o << "i64.le_u"; break;
case GtSInt64: o << "i64.gt_s"; break;
case GtUInt64: o << "i64.gt_u"; break;
case GeSInt64: o << "i64.ge_s"; break;
case GeUInt64: o << "i64.ge_u"; break;
case AddFloat32: o << "f32.add"; break;
case SubFloat32: o << "f32.sub"; break;
case MulFloat32: o << "f32.mul"; break;
case DivFloat32: o << "f32.div"; break;
case CopySignFloat32: o << "f32.copysign"; break;
case MinFloat32: o << "f32.min"; break;
case MaxFloat32: o << "f32.max"; break;
case EqFloat32: o << "f32.eq"; break;
case NeFloat32: o << "f32.ne"; break;
case LtFloat32: o << "f32.lt"; break;
case LeFloat32: o << "f32.le"; break;
case GtFloat32: o << "f32.gt"; break;
case GeFloat32: o << "f32.ge"; break;
case AddFloat64: o << "f64.add"; break;
case SubFloat64: o << "f64.sub"; break;
case MulFloat64: o << "f64.mul"; break;
case DivFloat64: o << "f64.div"; break;
case CopySignFloat64: o << "f64.copysign"; break;
case MinFloat64: o << "f64.min"; break;
case MaxFloat64: o << "f64.max"; break;
case EqFloat64: o << "f64.eq"; break;
case NeFloat64: o << "f64.ne"; break;
case LtFloat64: o << "f64.lt"; break;
case LeFloat64: o << "f64.le"; break;
case GtFloat64: o << "f64.gt"; break;
case GeFloat64: o << "f64.ge"; break;
default: abort();
}
restoreNormalColor(o);
incIndent();
printFullLine(curr->left);
printFullLine(curr->right);
decIndent();
}
void visitSelect(Select *curr) {
o << '(';
prepareColor(o) << "select";
incIndent();
printFullLine(curr->ifTrue);
printFullLine(curr->ifFalse);
printFullLine(curr->condition);
decIndent();
}
void visitDrop(Drop *curr) {
o << '(';
prepareColor(o) << "drop";
incIndent();
printFullLine(curr->value);
decIndent();
}
void visitReturn(Return *curr) {
printOpening(o, "return");
if (!curr->value) {
// avoid a new line just for the parens
o << ')';
return;
}
incIndent();
printFullLine(curr->value);
decIndent();
}
void visitHost(Host *curr) {
switch (curr->op) {
case PageSize: printOpening(o, "pagesize") << ')'; break;
case CurrentMemory: printOpening(o, "current_memory") << ')'; break;
case GrowMemory: {
printOpening(o, "grow_memory");
incIndent();
printFullLine(curr->operands[0]);
decIndent();
break;
}
case HasFeature: printOpening(o, "hasfeature ") << curr->nameOperand << ')'; break;
default: abort();
}
}
void visitNop(Nop *curr) {
printMinorOpening(o, "nop") << ')';
}
void visitUnreachable(Unreachable *curr) {
printMinorOpening(o, "unreachable") << ')';
}
// Module-level visitors
void visitFunctionType(FunctionType *curr, Name* internalName = nullptr) {
o << "(func";
if (internalName) o << ' ' << *internalName;
if (curr->params.size() > 0) {
o << maybeSpace;
printMinorOpening(o, "param");
for (auto& param : curr->params) {
o << ' ' << printWasmType(param);
}
o << ')';
}
if (curr->result != none) {
o << maybeSpace;
printMinorOpening(o, "result ") << printWasmType(curr->result) << ')';
}
o << ")";
}
void visitImport(Import *curr) {
printOpening(o, "import ");
printText(o, curr->module.str) << ' ';
printText(o, curr->base.str) << ' ';
switch (curr->kind) {
case ExternalKind::Function: if (curr->functionType) visitFunctionType(curr->functionType, &curr->name); break;
case ExternalKind::Table: printTableHeader(&currModule->table); break;
case ExternalKind::Memory: printMemoryHeader(&currModule->memory); break;
case ExternalKind::Global: o << "(global " << curr->name << ' ' << printWasmType(curr->globalType) << ")"; break;
default: WASM_UNREACHABLE();
}
o << ')';
}
void visitExport(Export *curr) {
printOpening(o, "export ");
printText(o, curr->name.str) << " (";
switch (curr->kind) {
case ExternalKind::Function: o << "func"; break;
case ExternalKind::Table: o << "table"; break;
case ExternalKind::Memory: o << "memory"; break;
case ExternalKind::Global: o << "global"; break;
default: WASM_UNREACHABLE();
}
o << ' ';
printName(curr->value) << "))";
}
void visitGlobal(Global *curr) {
printOpening(o, "global ");
printName(curr->name) << ' ';
if (curr->mutable_) {
o << "(mut " << printWasmType(curr->type) << ") ";
} else {
o << printWasmType(curr->type) << ' ';
}
visit(curr->init);
o << ')';
}
void visitFunction(Function *curr) {
currFunction = curr;
printOpening(o, "func ", true);
printName(curr->name);
if (curr->type.is()) {
o << maybeSpace << "(type " << curr->type << ')';
}
if (curr->params.size() > 0) {
for (size_t i = 0; i < curr->params.size(); i++) {
o << maybeSpace;
printMinorOpening(o, "param ") << printableLocal(i) << ' ' << printWasmType(curr->getLocalType(i)) << ')';
}
}
if (curr->result != none) {
o << maybeSpace;
printMinorOpening(o, "result ") << printWasmType(curr->result) << ')';
}
incIndent();
for (size_t i = curr->getVarIndexBase(); i < curr->getNumLocals(); i++) {
doIndent(o, indent);
printMinorOpening(o, "local ") << printableLocal(i) << ' ' << printWasmType(curr->getLocalType(i)) << ')';
o << maybeNewLine;
}
// It is ok to emit a block here, as a function can directly contain a list, even if our
// ast avoids that for simplicity. We can just do that optimization here..
if (!full && curr->body->is<Block>() && curr->body->cast<Block>()->name.isNull()) {
Block* block = curr->body->cast<Block>();
for (auto item : block->list) {
printFullLine(item);
}
} else {
printFullLine(curr->body);
}
decIndent();
}
void printTableHeader(Table* curr) {
printOpening(o, "table") << ' ';
o << curr->initial;
if (curr->max != Table::kMaxSize) o << ' ' << curr->max;
o << " anyfunc)";
}
void visitTable(Table *curr) {
// if table wasn't imported, declare it
if (!curr->imported) {
doIndent(o, indent);
printTableHeader(curr);
o << maybeNewLine;
}
if (curr->segments.empty()) return;
doIndent(o, indent);
for (auto& segment : curr->segments) {
// Don't print empty segments
if (segment.data.empty()) continue;
printOpening(o, "elem ", true);
visit(segment.offset);
for (auto name : segment.data) {
o << ' ';
printName(name);
}
o << ')';
}
o << maybeNewLine;
}
void printMemoryHeader(Memory* curr) {
printOpening(o, "memory") << ' ';
printName(curr->name) << ' ';
o << curr->initial;
if (curr->max && curr->max != Memory::kMaxSize) o << ' ' << curr->max;
o << ")";
}
void visitMemory(Memory* curr) {
// if memory wasn't imported, declare it
if (!curr->imported) {
doIndent(o, indent);
printMemoryHeader(curr);
o << '\n';
}
for (auto segment : curr->segments) {
doIndent(o, indent);
printOpening(o, "data ", true);
visit(segment.offset);
o << " \"";
for (size_t i = 0; i < segment.data.size(); i++) {
unsigned char c = segment.data[i];
switch (c) {
case '\n': o << "\\n"; break;
case '\r': o << "\\0d"; break;
case '\t': o << "\\t"; break;
case '\f': o << "\\0c"; break;
case '\b': o << "\\08"; break;
case '\\': o << "\\\\"; break;
case '"' : o << "\\\""; break;
case '\'' : o << "\\'"; break;
default: {
if (c >= 32 && c < 127) {
o << c;
} else {
o << std::hex << '\\' << (c/16) << (c%16) << std::dec;
}
}
}
}
o << "\")\n";
}
}
void visitModule(Module *curr) {
currModule = curr;
printOpening(o, "module", true);
incIndent();
for (auto& child : curr->functionTypes) {
doIndent(o, indent);
printOpening(o, "type") << ' ';
printName(child->name) << ' ';
visitFunctionType(child.get());
o << ")" << maybeNewLine;
}
for (auto& child : curr->imports) {
doIndent(o, indent);
visitImport(child.get());
o << maybeNewLine;
}
for (auto& child : curr->globals) {
doIndent(o, indent);
visitGlobal(child.get());
o << maybeNewLine;
}
if (curr->table.exists) {
visitTable(&curr->table); // Prints its own newlines
}
visitMemory(&curr->memory);
for (auto& child : curr->exports) {
doIndent(o, indent);
visitExport(child.get());
o << maybeNewLine;
}
if (curr->start.is()) {
doIndent(o, indent);
printOpening(o, "start") << ' ' << curr->start << ')';
o << maybeNewLine;
}
for (auto& child : curr->functions) {
doIndent(o, indent);
visitFunction(child.get());
o << maybeNewLine;
}
decIndent();
o << maybeNewLine;
currModule = nullptr;
}
};
void Printer::run(PassRunner* runner, Module* module) {
PrintSExpression print(o);
print.visitModule(module);
}
Pass *createPrinterPass() {
return new Printer();
}
// Prints out a minified module
class MinifiedPrinter : public Printer {
public:
MinifiedPrinter() : Printer() {}
MinifiedPrinter(std::ostream* o) : Printer(o) {}
void run(PassRunner* runner, Module* module) override {
PrintSExpression print(o);
print.setMinify(true);
print.visitModule(module);
}
};
Pass *createMinifiedPrinterPass() {
return new MinifiedPrinter();
}
// Prints out a module withough elision, i.e., the full ast
class FullPrinter : public Printer {
public:
FullPrinter() : Printer() {}
FullPrinter(std::ostream* o) : Printer(o) {}
void run(PassRunner* runner, Module* module) override {
PrintSExpression print(o);
print.setFull(true);
print.visitModule(module);
}
};
Pass *createFullPrinterPass() {
return new FullPrinter();
}
// Print individual expressions
std::ostream& WasmPrinter::printExpression(Expression* expression, std::ostream& o, bool minify, bool full) {
if (!expression) {
o << "(null expression)";
return o;
}
PrintSExpression print(o);
print.setMinify(minify);
if (full) {
print.setFull(true);
o << "[" << printWasmType(expression->type) << "] ";
}
print.visit(expression);
return o;
}
} // namespace wasm