Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/passes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ set(passes_SOURCES
PostEmscripten.cpp
Precompute.cpp
Print.cpp
PrintBoundary.cpp
PrintCallGraph.cpp
PrintFeatures.cpp
PrintFunctionMap.cpp
Expand Down
180 changes: 180 additions & 0 deletions src/passes/PrintBoundary.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
* Copyright 2026 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.
*/

//
// Prints the boundary - the imports and exports - in a convenient JSON format.
// Only enough information for JavaScript is provided (for the full info, parse
// the wat or wasm).
//
// Usage:
//
// wasm-opt --print-boundary=OUTFILE
//
// If OUTFILE is not provided, prints to stdout.
//
// Example:
//
// {
// 'imports': [
// {
// 'module': 'foo', // foo.bar
// 'base': 'bar',
// 'kind': 'func',
// 'type': {
// 'params': ['i32', '(ref func)'],
// 'results': ['f64']
// },
// },
// [..]
// ],
// 'exports': [
// {
// 'name': 'foo',
// 'kind': 'global',
// 'type': 'i32',
// },
// [..]
// ]
// }
//

#include "ir/module-utils.h"
#include "pass.h"
#include "support/file.h"
#include "support/json.h"
#include "wasm.h"

namespace wasm {

struct PrintBoundary : public Pass {
bool modifiesBinaryenIR() override { return false; }

void run(Module* module) override {
std::string target = getArgumentOrDefault("print-boundary", "");

// Imports.
auto imports = json::Value::makeArray();

ModuleUtils::iterImportable(
*module, [&](ExternalKind kind, Importable* import) {
auto item = json::Value::makeObject();
item["module"] = json::Value::make(import->module.view());
item["base"] = json::Value::make(import->base.view());
item["kind"] = getKindName(kind);
item["type"] = getExternalType(kind, import->name, *module);
imports->push_back(item);
});

// Exports.
auto exports = json::Value::makeArray();

for (auto& exp : module->exports) {
auto item = json::Value::makeObject();
item["name"] = json::Value::make(exp->name.view());
item["kind"] = getKindName(exp->kind);
item["type"] =
getExternalType(exp->kind, *exp->getInternalName(), *module);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this won't work for exported heap types... but I don't remember why Export even supports holding a HeapType given that we have no ExternalKind for heap types. Do you remember the context around #7335?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wasn't the context, as mentioned there, preparation for type exports..? I feel I am not understanding your question, sorry.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, but why were we preparing for type exports? It doesn't look like anything else has changed to support them since then.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like it's at stage 1 still, I guess nothing changed because it isn't urgent for anything?

exports->push_back(item);
}

// Emit the final structure
json::Value root;
root.setObject();
root["imports"] = imports;
root["exports"] = exports;

Output output(target, Flags::BinaryOption::Text);
root.stringify(output.getStream(), true /* pretty */);
}

// Emits an array of multivalue types. For a signature, emits params and
// results.
//
// We emit an array only when needed, unless forceArray is set.
json::Value::Ref getTypes(Type type, bool forceArray = false) {
if (type.isRef()) {
auto heapType = type.getHeapType();
if (heapType.isSignature()) {
auto sig = heapType.getSignature();
auto ret = json::Value::makeObject();
// Always emit arrays for params and results.
ret["params"] = getTypes(sig.params, true);
ret["results"] = getTypes(sig.results, true);
return ret;
}
}

// Simplify the output, avoiding an array for a single value.
if (!forceArray && type.size() == 1) {
return json::Value::make(type.toString());
}

auto ret = json::Value::makeArray();
for (auto t : type) {
ret->push_back(json::Value::make(t.toString()));
}
return ret;
}

// For an imported or exported thing (something external), and its name,
// return the type info we report for it.
json::Value::Ref getExternalType(ExternalKind kind, Name name, Module& wasm) {
switch (kind) {
case ExternalKind::Function:
return getTypes(wasm.getFunction(name)->type);
break;
case ExternalKind::Table:
break;
case ExternalKind::Memory:
break;
case ExternalKind::Global:
return getTypes(wasm.getGlobal(name)->type);
case ExternalKind::Tag:
break;
case ExternalKind::Invalid:
WASM_UNREACHABLE("invalid ExternalKind");
}
return {};
}

json::Value::Ref getKindName(ExternalKind kind) {
const char* name = nullptr;
switch (kind) {
case ExternalKind::Function:
name = "func";
break;
case ExternalKind::Table:
name = "table";
break;
case ExternalKind::Memory:
name = "memory";
break;
case ExternalKind::Global:
name = "global";
break;
case ExternalKind::Tag:
name = "tag";
break;
case ExternalKind::Invalid:
WASM_UNREACHABLE("invalid ExternalKind");
}
return json::Value::make(name);
}
};

Pass* createPrintBoundaryPass() { return new PrintBoundary(); }

} // namespace wasm
2 changes: 2 additions & 0 deletions src/passes/pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,8 @@ void PassRegistry::registerPasses() {
createPrintFeaturesPass);
registerPass(
"print-full", "print in full s-expression format", createFullPrinterPass);
registerPass(
"print-boundary", "print boundary in JSON format", createPrintBoundaryPass);
registerPass(
"print-call-graph", "print call graph", createPrintCallGraphPass);

Expand Down
1 change: 1 addition & 0 deletions src/passes/passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Pass* createPostEmscriptenPass();
Pass* createPrecomputePass();
Pass* createPrecomputePropagatePass();
Pass* createPrinterPass();
Pass* createPrintBoundaryPass();
Pass* createPrintCallGraphPass();
Pass* createPrintFeaturesPass();
Pass* createPrintFunctionMapPass();
Expand Down
2 changes: 2 additions & 0 deletions test/lit/help/wasm-metadce.test
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@
;; CHECK-NEXT:
;; CHECK-NEXT: --print print in s-expression format
;; CHECK-NEXT:
;; CHECK-NEXT: --print-boundary print boundary in JSON format
;; CHECK-NEXT:
;; CHECK-NEXT: --print-call-graph print call graph
;; CHECK-NEXT:
;; CHECK-NEXT: --print-features print options for enabled
Expand Down
2 changes: 2 additions & 0 deletions test/lit/help/wasm-opt.test
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,8 @@
;; CHECK-NEXT:
;; CHECK-NEXT: --print print in s-expression format
;; CHECK-NEXT:
;; CHECK-NEXT: --print-boundary print boundary in JSON format
;; CHECK-NEXT:
;; CHECK-NEXT: --print-call-graph print call graph
;; CHECK-NEXT:
;; CHECK-NEXT: --print-features print options for enabled
Expand Down
2 changes: 2 additions & 0 deletions test/lit/help/wasm2js.test
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,8 @@
;; CHECK-NEXT:
;; CHECK-NEXT: --print print in s-expression format
;; CHECK-NEXT:
;; CHECK-NEXT: --print-boundary print boundary in JSON format
;; CHECK-NEXT:
;; CHECK-NEXT: --print-call-graph print call graph
;; CHECK-NEXT:
;; CHECK-NEXT: --print-features print options for enabled
Expand Down
72 changes: 72 additions & 0 deletions test/lit/passes/print-boundary.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
(module
(type $struct (struct))

(import "module" "base" (func $foo (param i32) (param f64) (result anyref)))

(import "module2" "other" (func $bar (result i32 f32)))

(global $g (mut i32) (i32.const 42))

(export "one" (func $one))

(export "glob" (global $g))

(func $one (param $x (ref $struct)) (result i32 i32 i32)
(unreachable)
)
)

;; RUN: wasm-opt %s -all --print-boundary -S -o - | filecheck %s

;; CHECK: {
;; CHECK-NEXT: "imports": [
;; CHECK-NEXT: {
;; CHECK-NEXT: "module": "module",
;; CHECK-NEXT: "base": "base",
;; CHECK-NEXT: "kind": "func",
;; CHECK-NEXT: "type": {
;; CHECK-NEXT: "params": [
;; CHECK-NEXT: "i32",
;; CHECK-NEXT: "f64"
;; CHECK-NEXT: ],
;; CHECK-NEXT: "results": [
;; CHECK-NEXT: "anyref"
;; CHECK-NEXT: ]
;; CHECK-NEXT: }
;; CHECK-NEXT: },
;; CHECK-NEXT: {
;; CHECK-NEXT: "module": "module2",
;; CHECK-NEXT: "base": "other",
;; CHECK-NEXT: "kind": "func",
;; CHECK-NEXT: "type": {
;; CHECK-NEXT: "params": [
;; CHECK-NEXT: ],
;; CHECK-NEXT: "results": [
;; CHECK-NEXT: "i32",
;; CHECK-NEXT: "f32"
;; CHECK-NEXT: ]
;; CHECK-NEXT: }
;; CHECK-NEXT: }
;; CHECK-NEXT: ],
;; CHECK-NEXT: "exports": [
;; CHECK-NEXT: {
;; CHECK-NEXT: "name": "one",
;; CHECK-NEXT: "kind": "func",
;; CHECK-NEXT: "type": {
;; CHECK-NEXT: "params": [
;; CHECK-NEXT: "(ref $struct.0)"
;; CHECK-NEXT: ],
;; CHECK-NEXT: "results": [
;; CHECK-NEXT: "i32",
;; CHECK-NEXT: "i32",
;; CHECK-NEXT: "i32"
;; CHECK-NEXT: ]
Comment on lines +56 to +63
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should just print the number of params and results instead of their types, since non-abstract reference types will not be meaningful to consumers of this json. It sounds like Fuzzilli will not use more than the arity anyway.

An alternative that doesn't go so far would be to print the top reference type so the consumer can at least determine what reference type hierarchy the parameter or result is in.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like Fuzzilli to do more than the arity, though (eventually). It would make sense to send references to reference params, for example, reducing the odds of immediate traps that make testcases useless. More specifically, even, when Fuzzilli sees a ref $A is sent out one place, then it could send it back in to another that receives ref $A.

;; CHECK-NEXT: }
;; CHECK-NEXT: },
;; CHECK-NEXT: {
;; CHECK-NEXT: "name": "glob",
;; CHECK-NEXT: "kind": "global",
;; CHECK-NEXT: "type": "i32"
;; CHECK-NEXT: }
;; CHECK-NEXT: ]
;; CHECK-NEXT: }
Loading