Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion scripts/fuzz_opt.py
Original file line number Diff line number Diff line change
Expand Up @@ -2232,9 +2232,16 @@ def do_handle_pair(self, input, before_wasm, after_wasm, opts):
pre_vm = random.choice(vms)
pre = self.do_run(pre_vm, js_file, pre_wasm)

# We are about to optimize, and do not trust the given wasm file to
# have marked all js-called methods properly. In particular, it could
# have a configureAll that is not in the start function.
full_opts = opts + [
'--mark-js-called',
]

# Optimize.
post_wasm = abspath('post.wasm')
cmd = [in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + opts + FEATURE_OPTS
cmd = [in_bin('wasm-opt'), pre_wasm, '-o', post_wasm] + full_opts + FEATURE_OPTS
print(' '.join(cmd))
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode:
Expand Down
9 changes: 9 additions & 0 deletions src/ir/intrinsics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,15 @@ std::vector<Name> Intrinsics::getJSCalledFunctions() {
}

// ConfigureAlls in a start function make their functions callable.
//
// TODO: Rather than scan the start, which does not handle all cases
// (configureAll can be called from an export), we could remove this and
// expect users to mark all functions as jsCalled. The MarkJSCalled pass scans
// for configureAlls and emits that annotation, so users could basically run
// it, if they don't want to manually annotate. Then the code here could
// get unified into that pass. The errors above (like the elem segment not
// having the right size etc.) could then be improved and/or turned into
// warnings.
if (module.start) {
auto* start = module.getFunction(module.start);
if (!start->imported()) {
Expand Down
1 change: 1 addition & 0 deletions src/passes/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ set(passes_SOURCES
LocalSubtyping.cpp
LogExecution.cpp
LoopInvariantCodeMotion.cpp
MarkJSCalled.cpp
Memory64Lowering.cpp
MemoryPacking.cpp
MergeBlocks.cpp
Expand Down
79 changes: 79 additions & 0 deletions src/passes/MarkJSCalled.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* 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.
*/

//
// Users should mark JS-called functions using @binaryen.js.called. This pass
// helps by auto-marking them where possible. The main thing this does is to
// find any configureAll calls and mark the functions referred to there.
//
// We do automatically handle configureAll in the start function (in
// intrinsics.cpp), so this pass is only needed for other uses of configureAll,
// like from an export.
//

#include "ir/find_all.h"
#include "ir/intrinsics.h"
#include "ir/module-utils.h"
#include "pass.h"
#include "wasm.h"

namespace wasm {

struct MarkJSCalled : public Pass {
void run(Module* module) override {
Intrinsics intrinsics(*module);

// See if there even is a configureAll.
auto hasConfigureAll = false;
for (auto& func : module->functions) {
if (intrinsics.isConfigureAll(func.get())) {
hasConfigureAll = true;
break;
}
}
if (!hasConfigureAll) {
return;
}

using JSCalledSet = std::unordered_set<Name>;

ModuleUtils::ParallelFunctionAnalysis<JSCalledSet> analysis(
*module, [&](Function* func, JSCalledSet& jsCalled) {
if (func->imported()) {
return;
}

FindAll<Call> calls(func->body);
for (auto* call : calls.list) {
if (intrinsics.isConfigureAll(call)) {
for (auto name : intrinsics.getConfigureAllFunctions(call)) {
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 would be good to produce a warning or fatal error when there are function operands passed to configureAll that we are not able to analyze. As a follow-up to this change we should remove getConfigureAllFunctions and all its uses in favor of explicitly annotated js-called functions anyway, so I think we can just inline the logic from getConfigureAllFunctions here.

In the future, if folks have different usage patterns and are running into the warning or fatal error, we can add support for more usage patterns here as well. (array.new_fixed usage comes to mind as low-hanging fruit.)

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.

so I think we can just inline the logic from getConfigureAllFunctions here.

I'd rather not do that in this PR, so that it is NFC aside from adding a new pass? I don't want to change behavior for users.

But I agree on the path after this PR, yes, it would be nice to unify and simplify this.

(Given we shouldn't unify it yet, I don't think it's worth adding detailed warnings here - that would lead to a bunch of duplicated code.)

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.

Sure, maybe a TODO about adding a warning/error, 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.

Sure, done.

jsCalled.insert(name);
}
}
}
});

for (auto& [_, jsCalled] : analysis.map) {
for (auto name : jsCalled) {
module->getFunction(name)->funcAnnotations.jsCalled = true;
}
}
}
};

Pass* createMarkJSCalledPass() { return new MarkJSCalled(); }

} // namespace wasm
3 changes: 3 additions & 0 deletions src/passes/pass.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@ void PassRegistry::registerPasses() {
registerPass("limit-segments",
"attempt to merge segments to fit within web limits",
createLimitSegmentsPass);
registerPass("mark-js-called",
"mark js called functions (using configureAll) as doing so",
createMarkJSCalledPass);
registerPass("memory64-lowering",
"lower loads and stores to a 64-bit memory to instead use a "
"32-bit one",
Expand Down
1 change: 1 addition & 0 deletions src/passes/passes.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Pass* createInstrumentLocalsPass();
Pass* createInstrumentMemoryPass();
Pass* createLLVMMemoryCopyFillLoweringPass();
Pass* createLoopInvariantCodeMotionPass();
Pass* createMarkJSCalledPass();
Pass* createMemory64LoweringPass();
Pass* createMemoryPackingPass();
Pass* createMergeBlocksPass();
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-metadce.test
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@
;; CHECK-NEXT: --log-execution instrument the build with
;; CHECK-NEXT: logging of where execution goes
;; CHECK-NEXT:
;; CHECK-NEXT: --mark-js-called mark js called functions (using
;; CHECK-NEXT: configureAll) as doing so
;; CHECK-NEXT:
;; CHECK-NEXT: --memory-packing packs memory into separate
;; CHECK-NEXT: segments, skipping zeros
;; CHECK-NEXT:
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm-opt.test
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,9 @@
;; CHECK-NEXT: --log-execution instrument the build with
;; CHECK-NEXT: logging of where execution goes
;; CHECK-NEXT:
;; CHECK-NEXT: --mark-js-called mark js called functions (using
;; CHECK-NEXT: configureAll) as doing so
;; CHECK-NEXT:
;; CHECK-NEXT: --memory-packing packs memory into separate
;; CHECK-NEXT: segments, skipping zeros
;; CHECK-NEXT:
Expand Down
3 changes: 3 additions & 0 deletions test/lit/help/wasm2js.test
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@
;; CHECK-NEXT: --log-execution instrument the build with
;; CHECK-NEXT: logging of where execution goes
;; CHECK-NEXT:
;; CHECK-NEXT: --mark-js-called mark js called functions (using
;; CHECK-NEXT: configureAll) as doing so
;; CHECK-NEXT:
;; CHECK-NEXT: --memory-packing packs memory into separate
;; CHECK-NEXT: segments, skipping zeros
;; CHECK-NEXT:
Expand Down
102 changes: 102 additions & 0 deletions test/lit/passes/mark-js-called.wast
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited.

;; RUN: foreach %s %t wasm-opt --mark-js-called -all -S -o - | filecheck %s

;; $configured will be marked as @binaryen.js.called. $already is already marked,
;; and nothing changess. $unconfigured* are not in configureAll so they are left
;; alone.

(module
;; CHECK: (type $0 (func))

;; CHECK: (type $externs (array (mut externref)))
(type $externs (array (mut externref)))

;; CHECK: (type $funcs (array (mut funcref)))
(type $funcs (array (mut funcref)))

;; CHECK: (type $bytes (array (mut i8)))
(type $bytes (array (mut i8)))

;; CHECK: (type $configureAll (func (param (ref null $externs) (ref null $funcs) (ref null $bytes) externref)))
(type $configureAll (func (param (ref null $externs)) (param (ref null $funcs)) (param (ref null $bytes)) (param externref)))

;; CHECK: (import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll) (param (ref null $externs) (ref null $funcs) (ref null $bytes) externref)))
(import "wasm:js-prototypes" "configureAll" (func $configureAll (type $configureAll)))

;; CHECK: (data $bytes "12345678")
(data $bytes "12345678")

;; CHECK: (elem $externs externref (item (ref.null noextern)))
(elem $externs externref
(ref.null extern)
)

;; CHECK: (elem $funcs func $configured $already)
(elem $funcs funcref
(ref.func $configured)
(ref.func $already)
)

;; CHECK: (elem $other func $unconfigured)
(elem $other funcref
(ref.func $unconfigured)
)

;; CHECK: (start $start)
(start $start)

;; CHECK: (func $start (type $0)
;; CHECK-NEXT: (call $configureAll
;; CHECK-NEXT: (array.new_elem $externs $externs
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: (i32.const 1)
;; CHECK-NEXT: )
;; CHECK-NEXT: (array.new_elem $funcs $funcs
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: (i32.const 2)
;; CHECK-NEXT: )
;; CHECK-NEXT: (array.new_data $bytes $bytes
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: (i32.const 8)
;; CHECK-NEXT: )
;; CHECK-NEXT: (ref.null noextern)
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $start
(call $configureAll
(array.new_elem $externs $externs
(i32.const 0) (i32.const 1))
(array.new_elem $funcs $funcs
(i32.const 0) (i32.const 2))
(array.new_data $bytes $bytes
(i32.const 0) (i32.const 8))
(ref.null extern)
)
)

;; CHECK: (@binaryen.js.called)
;; CHECK-NEXT: (func $configured (type $0)
;; CHECK-NEXT: )
(func $configured
)

;; CHECK: (@binaryen.js.called)
;; CHECK-NEXT: (func $already (type $0)
;; CHECK-NEXT: )
(@binaryen.js.called)
(func $already
)

;; CHECK: (func $unconfigured (type $0)
;; CHECK-NEXT: )
(func $unconfigured
)

;; CHECK: (@binaryen.js.called)
;; CHECK-NEXT: (func $unconfigured-already (type $0)
;; CHECK-NEXT: )
(@binaryen.js.called)
(func $unconfigured-already
)
)
Loading