blob: 6bf95b12a1b176ab70f03fcc3692c5fb0d1bde3a [file] [edit]
/*
* 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.
*/
// Unconditionally make all ref params, locals, and struct fields (todo)
// nullable. This helps reduce code size by
// 1. Eliminating `ref.as_non_null`s - When --traps-never-happen, all
// `ref.as_non_null`s can be eliminated, except on the boundary of things
// that couldn't be relaxed e.g. imported function params. This is done
// by --strip-refining-casts.
// 2. Allowing more locals to share a declaration group, since they can no
// longer differ by nullability.
// 3. Exposing more opportunities for type merging by causing more function
// signatures and struct/arrays to be identical.
//
// This pass does not strip redundant ref.as_non_nulls after definitions have
// been relaxed. Follow with --strip-refining-casts to do so.
//
// This pass strictly removes information so it should come near the end of
// Binaryen's pipeline.
#include <utility>
#include <variant>
#include "pass.h"
#include "wasm-builder.h"
#include "wasm.h"
#include "ir/child-typer.h"
#include "ir/find_all.h"
#include "ir/module-utils.h"
#include "ir/utils.h"
namespace wasm {
namespace {
bool isSignatureRewritable(
Function* func, const std::unordered_set<Function*>& referencedFuncs) {
// The signatures of imported and exported functions aren't changed to
// preserve the API boundary. Exported functions are implicitly 'referenced'
// (i.e. they may be the target of a call_ref even if they never appear in a
// ref.func), so no need to check that a second time.
return !func->imported() && !referencedFuncs.contains(func);
}
[[nodiscard]] bool makeNullable(Type& t) {
if (t.isNonNullableRef()) {
t = t.with(Nullability::Nullable);
return true;
}
return false;
}
void insertRefAsNonNull(Expression** curr, Builder& builder) {
// unreachable also doesn't need a change
if (!(*curr)->type.isNullableRef()) {
return;
}
*curr = builder.makeRefAs(RefAsOp::RefAsNonNull, *curr);
}
void fixupReturn(Expression** returnVal, Module* module, Function* func) {
auto results = func->getResults();
if (results.size() > 1) {
Fatal() << "TODO: Tuples not supported yet";
return;
}
if (results.size() == 1 && results[0].isNonNullableRef()) {
Builder builder(*module);
insertRefAsNonNull(returnVal, builder);
}
}
struct InsertRefAsNonNull
: PostWalker<InsertRefAsNonNull,
UnifiedExpressionVisitor<InsertRefAsNonNull>> {
struct Typer : ChildTyper<Typer> {
Typer(Module& module, Function* func) : ChildTyper(module, func) {}
void note(Expression** currp, Constraints constraints) {
if (constraints.size() > 1) {
Fatal() << "TODO: Tuples not supported yet";
return;
}
std::visit(
overloaded{
[&](const Type& type) {
if (type.isNonNullableRef() && (*currp)->type.isNullableRef()) {
*currp = Builder(wasm).makeRefAs(RefAsOp::RefAsNonNull, *currp);
}
},
[](Index) { /* no nullability constraint */ },
[&](const VarRef& var) {
std::visit(overloaded{
[](Index) { /* no nullability constraint */ },
[&](Nullability nullability) {
// A polymorphic expression demanded a non-null. This
// can't actually happen today.
if (nullability == Nullability::NonNullable) {
*currp = Builder(wasm).makeRefAs(
RefAsOp::RefAsNonNull, *currp);
}
},
},
var.null);
}},
constraints[0]);
}
void noteUnknown() {
// This is an unreachable with a type immediate e.g. struct.set.
// In this case we don't need to insert any ref.as_non_null.
}
Type getLabelType(Name) {
// Labeled blocks are skipped. See `InsertRefAsNonNull::visitExpression`.
WASM_UNREACHABLE("unexpected branch");
}
} typer;
InsertRefAsNonNull(Module& module, Function* func) : typer(module, func) {}
void visitExpression(Expression* curr) {
// Types for labeled blocks will be handled by ReFinalize. Skip these
// so we don't need to implement `getLabelType`.
if (Properties::isBranch(curr) ||
Properties::isControlFlowStructure(curr)) {
return;
}
typer.visit(curr);
}
};
// Pass implementing the bulk of function-parallel work after global rewrites
// are done.
struct LocalsToNullable : WalkerPass<PostWalker<LocalsToNullable>> {
using Super = WalkerPass<PostWalker<LocalsToNullable>>;
const std::unordered_set<Function*>& funcsWithRelaxedSignature;
// true iff any expression types were changed.
// false if only dead local definitions were changed.
bool changed = false;
// true iff the function's signature was relaxed before this pass ran, i.e. if
// funcsWithRelaxedSignature contains the function.
bool signatureWasRelaxed = false;
LocalsToNullable(
const std::unordered_set<Function*>& funcsWithRelaxedSignature)
: funcsWithRelaxedSignature(funcsWithRelaxedSignature) {}
std::unique_ptr<Pass> create() override {
return std::make_unique<LocalsToNullable>(funcsWithRelaxedSignature);
}
bool isFunctionParallel() override { return true; }
bool runsOnFunction(Function* func) const override {
return func->vars.size() > 0 || funcsWithRelaxedSignature.contains(func);
}
// 1. Make all local definitions nullable and update gets and sets.
// 2. Refinalize to fixup block types
// 3. Insert ref.as_non_null where a parent demands it, e.g. return values,
// calls to unrelaxed imports, unrelaxed struct.set.
//
// Refinalize is not needed after step 3 because a block never demands a
// non-null and thus step 3 makes no difference to block types.
void doWalkFunction(Function* func) {
signatureWasRelaxed = funcsWithRelaxedSignature.contains(func);
bool changedLocals = false;
for (Index i = 0; i < func->vars.size(); ++i) {
changedLocals |= makeNullable(func->vars[i]);
}
if (!changedLocals && !signatureWasRelaxed) {
return;
}
Super::doWalkFunction(func);
if (!changed) {
return;
}
ReFinalize().walkFunctionInModule(func, getModule());
InsertRefAsNonNull(*getModule(), func)
.walkFunctionInModule(func, getModule());
fixupReturn(&func->body, getModule(), func);
}
void visitLocalGet(LocalGet* curr) {
if (!signatureWasRelaxed && getFunction()->isParam(curr->index)) {
return;
}
changed |= makeNullable(curr->type);
}
void visitLocalSet(LocalSet* curr) {
if (!signatureWasRelaxed && getFunction()->isParam(curr->index)) {
return;
}
if (curr->isTee()) {
changed |= makeNullable(curr->type);
}
}
};
struct RelaxNullability : Pass {
bool requiresNonNullableLocalFixups() override { return false; }
void run(Module* module) override {
auto relaxedFuncs = relaxFunctionParams(module);
PassRunner runner(getPassRunner());
runner.add(std::make_unique<LocalsToNullable>(relaxedFuncs));
runner.run();
}
private:
// Returns the set of functions that had their signatures changed.
std::unordered_set<Function*> relaxFunctionParams(Module* module) {
auto referencedFuncs = ModuleUtils::getReferencedFuncs(*module);
std::unordered_set<Function*> relaxedFuncs;
for (auto& func : module->functions) {
if (!isSignatureRewritable(func.get(), referencedFuncs)) {
continue;
}
std::vector<Type> relaxedParams;
bool changed = false;
for (Type t : func->getParams()) {
changed |= makeNullable(t);
relaxedParams.push_back(t);
}
if (changed) {
relaxedFuncs.insert(func.get());
func->setParams(std::move(relaxedParams));
}
}
return relaxedFuncs;
}
};
} // anonymous namespace
Pass* createRelaxNullabilityPass() { return new RelaxNullability(); }
} // namespace wasm