blob: eb10880313930cc446fb7c7a381e9fe43ea9d286 [file] [edit]
/*
* Copyright 2025 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.
*/
//
// Relaxes nullability for code size, assuming traps never happen.
//
// Non-nullable reference types carry a cost in the binary: every value that
// flows into a non-nullable parameter or local that is not already known to
// be non-null needs a ref.as_non_null, and locals that only differ in
// nullability cannot share a declaration group, while function types that only
// differ in parameter nullability cannot be merged. When traps never happen a
// ref.as_non_null can never fail, so it only serves the type system, and we
// are free to remove it wherever the consumer of its value accepts a nullable
// reference.
//
// This pass therefore
//
// 1. makes every non-nullable reference parameter of every private function
// type nullable (results are unchanged, as callers often rely on them);
// 2. makes every reference local nullable; and
// 3. walks each function and, using the type constraints the parent imposes
// on each child (ChildTyper), removes ref.as_non_null where the parent
// accepts a nullable value and inserts one where the parent still demands
// a non-nullable value (struct.new operands, calls to public functions,
// returns, global.set, etc.).
//
// Public (exported/imported) function types, tag types and types with
// signature subtyping are left alone. The pass requires --traps-never-happen
// and does nothing without it. Follow it with --optimize-instructions and
// --coalesce-locals to fold the copies `tee $a (ref.as_non_null (get $b))`
// that become plain copies, and with --type-merging to merge the function
// types that now differ only in results.
//
#include <unordered_set>
#include "ir/child-typer.h"
#include "ir/find_all.h"
#include "ir/intrinsics.h"
#include "ir/module-utils.h"
#include "ir/properties.h"
#include "ir/subtypes.h"
#include "ir/type-updating.h"
#include "ir/utils.h"
#include "pass.h"
#include "wasm-builder.h"
#include "wasm.h"
namespace wasm {
namespace {
Type makeNullable(Type type) {
if (type.isRef() && type.isNonNullable()) {
return type.with(Nullable);
}
return type;
}
// Per-function fix-up: relax the vars, then remove or add ref.as_non_null
// according to what each parent demands of its children.
struct FunctionRelaxer
: public WalkerPass<
PostWalker<FunctionRelaxer, UnifiedExpressionVisitor<FunctionRelaxer>>> {
bool isFunctionParallel() override { return true; }
// We only make locals more nullable, which cannot break validation.
bool requiresNonNullableLocalFixups() override { return false; }
std::unique_ptr<Pass> create() override {
return std::make_unique<FunctionRelaxer>();
}
void doWalkFunction(Function* func) {
for (auto& var : func->vars) {
var = makeNullable(var);
}
// local.get/local.tee types are tied to the local types (params may have
// been relaxed by the signature update as well), and ReFinalize does not
// update them.
for (auto* get : FindAll<LocalGet>(func->body).list) {
get->type = func->getLocalType(get->index);
}
for (auto* set : FindAll<LocalSet>(func->body).list) {
if (set->isTee() && set->type != Type::unreachable) {
set->type = func->getLocalType(set->index);
}
}
// Propagate the new nullability through blocks, ifs, etc. so that the walk
// below sees accurate types.
ReFinalize().walkFunctionInModule(func, getModule());
Super::doWalkFunction(func);
// The implicit return.
auto results = func->getResults();
if (results.isRef() && results.isNonNullable() &&
func->body->type.isRef() && func->body->type.isNullable()) {
func->body = Builder(*getModule()).makeRefAs(RefAsNonNull, func->body);
}
}
struct Typer : public ChildTyper<Typer> {
FunctionRelaxer& parent;
bool changed = false;
Typer(FunctionRelaxer& parent)
: ChildTyper(*parent.getModule(), parent.getFunction()), parent(parent) {}
void note(Expression** childp, Constraints constraints) {
if (constraints.size() != 1) {
// TODO: tuples
return;
}
auto* child = *childp;
if (!child->type.isRef()) {
return;
}
bool demandsNonNull = false;
bool acceptsNull = false;
auto& constraint = constraints[0];
if (auto* type = std::get_if<Type>(&constraint)) {
if (type->isRef()) {
demandsNonNull = type->isNonNullable();
acceptsNull = type->isNullable();
}
} else if (auto* ref = std::get_if<VarRef>(&constraint)) {
if (auto* null = std::get_if<Nullability>(&ref->null)) {
demandsNonNull = *null == NonNullable;
acceptsNull = *null == Nullable;
}
}
if (demandsNonNull) {
if (child->type.isNullable()) {
*childp = Builder(*parent.getModule()).makeRefAs(RefAsNonNull, child);
changed = true;
}
return;
}
if (acceptsNull) {
// Traps never happen, so a ref.as_non_null here only narrows the type,
// which the parent does not need.
while (auto* as = (*childp)->dynCast<RefAs>()) {
if (as->op != RefAsNonNull) {
break;
}
*childp = as->value;
changed = true;
}
}
}
Type getLabelType(Name label) { WASM_UNREACHABLE("unexpected branch"); }
void noteUnknown() {}
};
void visitExpression(Expression* curr) {
// Control flow structures and branches just forward values; their types
// follow from their children and are handled by refinalization.
if (Properties::isControlFlowStructure(curr) ||
Properties::isBranch(curr)) {
return;
}
Typer typer(*this);
typer.visit(curr);
if (typer.changed) {
ReFinalizeNode().visit(curr);
}
}
};
struct RelaxNullability : public Pass {
// Signatures and locals change, and all casts are revisited.
bool invalidatesDWARF() override { return true; }
void run(Module* module) override {
if (!getPassOptions().trapsNeverHappen) {
// Without this assumption removing casts changes behavior.
return;
}
// 1. Relax the parameters of private function types.
std::unordered_set<HeapType> fixed;
for (auto type :
ModuleUtils::getPublicHeapTypes(*module, getPassOptions().worldMode)) {
fixed.insert(type);
}
for (auto& tag : module->tags) {
fixed.insert(tag->type);
}
for (auto func : Intrinsics(*module).getJSCalledFunctions()) {
fixed.insert(module->getFunction(func)->type.getHeapType());
}
auto types = ModuleUtils::collectHeapTypes(*module);
SubTypes subTypes(types);
GlobalTypeRewriter::SignatureUpdates updates;
for (auto type : types) {
if (!type.isSignature() || fixed.count(type)) {
continue;
}
// Keep it simple: do not touch types that take part in subtyping.
if (type.getDeclaredSuperType() ||
!subTypes.getImmediateSubTypes(type).empty()) {
continue;
}
auto sig = type.getSignature();
std::vector<Type> params;
bool changed = false;
for (auto param : sig.params) {
auto relaxed = makeNullable(param);
changed = changed || relaxed != param;
params.push_back(relaxed);
}
if (changed) {
updates[type] = Signature(Type(params), sig.results);
}
}
GlobalTypeRewriter::updateSignatures(
updates, *module, getPassOptions().worldMode);
// 2 + 3. Relax locals and fix up casts in every function.
FunctionRelaxer().run(getPassRunner(), module);
}
};
} // anonymous namespace
Pass* createRelaxNullabilityPass() { return new RelaxNullability(); }
} // namespace wasm