-
Notifications
You must be signed in to change notification settings - Fork 858
PrintBoundary pass, emitting a JSON summary of the API boundary of the module #8703
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ef51118
8cf542b
91ae16e
377a466
573cdf1
88d29d1
97843d5
4354d3e
a3e30c6
56ea60e
fb5ff35
b8492ff
e5ca754
5b67220
c23c910
a2812a0
6b1ea4a
71ce04b
4b500a2
fcd36ce
a89a700
7d01d88
43f58d2
8a5a5f5
68d3481
5d8f9e6
2ca997c
1fa9086
4046717
0bb32cb
9de1bf5
24308d4
03ea8b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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); | ||
| 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 | ||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ;; CHECK-NEXT: } | ||
| ;; CHECK-NEXT: }, | ||
| ;; CHECK-NEXT: { | ||
| ;; CHECK-NEXT: "name": "glob", | ||
| ;; CHECK-NEXT: "kind": "global", | ||
| ;; CHECK-NEXT: "type": "i32" | ||
| ;; CHECK-NEXT: } | ||
| ;; CHECK-NEXT: ] | ||
| ;; CHECK-NEXT: } | ||
There was a problem hiding this comment.
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
Exporteven supports holding a HeapType given that we have noExternalKindfor heap types. Do you remember the context around #7335?There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?