RelaxNullability: nullable private params/locals under TNH, ref.as_non_null only where demanded (calcworker 2407031 -> 2334280 B) New pass --relax-nullability (traps-never-happen only): private function signature params and all ref locals become nullable; a ChildTyper-driven walk strips ref.as_non_null where the parent accepts null and inserts it where a non-null value is still demanded (struct.new, public calls, returns, global.set, throw). Lit test test/lit/passes/relax-nullability.wast.
diff --git a/src/passes/CMakeLists.txt b/src/passes/CMakeLists.txt index c158d5c..d52576a 100644 --- a/src/passes/CMakeLists.txt +++ b/src/passes/CMakeLists.txt
@@ -118,6 +118,7 @@ RemoveImports.cpp RemoveMemoryInit.cpp RemoveNonJSOps.cpp + RelaxNullability.cpp RemoveRelaxedSIMD.cpp RemoveUnusedBrs.cpp RemoveUnusedNames.cpp
diff --git a/src/passes/RelaxNullability.cpp b/src/passes/RelaxNullability.cpp new file mode 100644 index 0000000..eb10880 --- /dev/null +++ b/src/passes/RelaxNullability.cpp
@@ -0,0 +1,245 @@ +/* + * 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
diff --git a/src/passes/pass.cpp b/src/passes/pass.cpp index 55b20e0..e1d3cec 100644 --- a/src/passes/pass.cpp +++ b/src/passes/pass.cpp
@@ -426,6 +426,10 @@ registerPass("remove-non-js-ops", "removes operations incompatible with js", createRemoveNonJSOpsPass); + registerPass("relax-nullability", + "make private function params and locals nullable, keeping " + "ref.as_non_null only where demanded (traps-never-happen)", + createRelaxNullabilityPass); registerPass("remove-relaxed-simd", "replaces relaxed SIMD instructions with unreachable", createRemoveRelaxedSIMDPass); @@ -751,7 +755,7 @@ addIfNoDWARFIssues( "remove-unused-brs"); // coalesce-locals opens opportunities addIfNoDWARFIssues( - "remove-unused-names"); // remove-unused-brs opens opportunities + "remove-unused-names"); // remove-unused-brs opens opportunities if (options.optimizeLevel >= 3 || options.shrinkLevel >= 1) { addIfNoDWARFIssues("constraint-analysis"); }
diff --git a/src/passes/passes.h b/src/passes/passes.h index 1324edf..4acdeca 100644 --- a/src/passes/passes.h +++ b/src/passes/passes.h
@@ -140,6 +140,7 @@ Pass* createPropagateGlobalsGloballyPass(); Pass* createRandomizeBranchHintsPass(); Pass* createRemoveNonJSOpsPass(); +Pass* createRelaxNullabilityPass(); Pass* createRemoveRelaxedSIMDPass(); Pass* createRemoveEmptyFunctionExportsPass(); Pass* createRemoveExportsPass();
diff --git a/test/lit/help/wasm-metadce.test b/test/lit/help/wasm-metadce.test index ea45a3c..90f5323 100644 --- a/test/lit/help/wasm-metadce.test +++ b/test/lit/help/wasm-metadce.test
@@ -400,6 +400,11 @@ ;; CHECK-NEXT: --propagate-globals-globally propagate global values to other ;; CHECK-NEXT: globals (useful for tests) ;; CHECK-EMPTY: +;; CHECK-NEXT: --relax-nullability make private function params and +;; CHECK-NEXT: locals nullable, keeping +;; CHECK-NEXT: ref.as_non_null only where +;; CHECK-NEXT: demanded (traps-never-happen) +;; CHECK-EMPTY: ;; CHECK-NEXT: --remove-empty-function-exports removes exports of empty ;; CHECK-NEXT: functions ;; CHECK-EMPTY:
diff --git a/test/lit/help/wasm-opt.test b/test/lit/help/wasm-opt.test index 90c3b29..a68be5a 100644 --- a/test/lit/help/wasm-opt.test +++ b/test/lit/help/wasm-opt.test
@@ -445,6 +445,11 @@ ;; CHECK-NEXT: --propagate-globals-globally propagate global values to other ;; CHECK-NEXT: globals (useful for tests) ;; CHECK-EMPTY: +;; CHECK-NEXT: --relax-nullability make private function params and +;; CHECK-NEXT: locals nullable, keeping +;; CHECK-NEXT: ref.as_non_null only where +;; CHECK-NEXT: demanded (traps-never-happen) +;; CHECK-EMPTY: ;; CHECK-NEXT: --remove-empty-function-exports removes exports of empty ;; CHECK-NEXT: functions ;; CHECK-EMPTY:
diff --git a/test/lit/help/wasm2js.test b/test/lit/help/wasm2js.test index fdce36a..9258cb0 100644 --- a/test/lit/help/wasm2js.test +++ b/test/lit/help/wasm2js.test
@@ -364,6 +364,11 @@ ;; CHECK-NEXT: --propagate-globals-globally propagate global values to other ;; CHECK-NEXT: globals (useful for tests) ;; CHECK-EMPTY: +;; CHECK-NEXT: --relax-nullability make private function params and +;; CHECK-NEXT: locals nullable, keeping +;; CHECK-NEXT: ref.as_non_null only where +;; CHECK-NEXT: demanded (traps-never-happen) +;; CHECK-EMPTY: ;; CHECK-NEXT: --remove-empty-function-exports removes exports of empty ;; CHECK-NEXT: functions ;; CHECK-EMPTY:
diff --git a/test/lit/passes/relax-nullability.wast b/test/lit/passes/relax-nullability.wast new file mode 100644 index 0000000..a42b781 --- /dev/null +++ b/test/lit/passes/relax-nullability.wast
@@ -0,0 +1,463 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. + +;; RUN: foreach %s %t wasm-opt -all --closed-world --traps-never-happen --relax-nullability -S -o - | filecheck %s +;; RUN: foreach %s %t wasm-opt -all --closed-world --relax-nullability -S -o - | filecheck %s --check-prefix=NO_TNH + +(module + ;; CHECK: (type $A (struct (field (mut i32)))) + ;; NO_TNH: (type $A (struct (field (mut i32)))) + (type $A (struct (field (mut i32)))) + ;; CHECK: (type $pub (func (param (ref $A)) (result i32))) + + ;; CHECK: (rec + ;; CHECK-NEXT: (type $2 (func (param (ref null $A) i32))) + + ;; CHECK: (type $3 (func (param (ref null $A)))) + + ;; CHECK: (type $4 (func (param (ref null $A) (ref null $sig)) (result (ref $A)))) + + ;; CHECK: (type $sig (func (param (ref null $A)) (result (ref $A)))) + + ;; CHECK: (type $B (struct (field (ref $A)))) + ;; NO_TNH: (type $sig (func (param (ref $A)) (result (ref $A)))) + + ;; NO_TNH: (type $pub (func (param (ref $A)) (result i32))) + + ;; NO_TNH: (type $3 (func (param (ref null $A)) (result i32))) + + ;; NO_TNH: (type $B (struct (field (ref $A)))) + (type $B (struct (field (ref $A)))) + (type $sig (func (param (ref $A)) (result (ref $A)))) + (type $pub (func (param (ref $A)) (result i32))) + + ;; CHECK: (type $7 (func (param (ref null $A)) (result (ref $A)))) + + ;; CHECK: (type $8 (func (param (ref null $A)) (result i32))) + + ;; CHECK: (type $9 (func (param (ref null $A) i32) (result i32))) + + ;; CHECK: (type $10 (func (param (ref $A)))) + + ;; CHECK: (import "env" "import" (func $import (type $pub) (param (ref $A)) (result i32))) + ;; NO_TNH: (type $5 (func (param (ref $A)))) + + ;; NO_TNH: (type $6 (func (param (ref $A) i32) (result i32))) + + ;; NO_TNH: (type $7 (func (param (ref null $A)) (result (ref $A)))) + + ;; NO_TNH: (type $8 (func (param (ref null $A) (ref $sig)) (result (ref $A)))) + + ;; NO_TNH: (type $9 (func (param (ref null $A)))) + + ;; NO_TNH: (type $10 (func (param (ref null $A) i32))) + + ;; NO_TNH: (import "env" "import" (func $import (type $pub) (param (ref $A)) (result i32))) + (import "env" "import" (func $import (param (ref $A)) (result i32))) + + ;; CHECK: (global $g (mut (ref null $A)) (ref.null none)) + ;; NO_TNH: (global $g (mut (ref null $A)) (ref.null none)) + (global $g (mut (ref null $A)) (ref.null $A)) + ;; CHECK: (global $gnn (mut (ref $A)) (struct.new $A + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: )) + ;; NO_TNH: (global $gnn (mut (ref $A)) (struct.new $A + ;; NO_TNH-NEXT: (i32.const 0) + ;; NO_TNH-NEXT: )) + (global $gnn (mut (ref $A)) (struct.new $A (i32.const 0))) + + ;; CHECK: (tag $tag (type $10) (param (ref $A))) + ;; NO_TNH: (tag $tag (type $5) (param (ref $A))) + (tag $tag (param (ref $A))) + + ;; Private callee: the param becomes nullable. The struct.get on it accepts a + ;; nullable ref, so no cast is needed inside. + ;; CHECK: (export "exported" (func $exported)) + + ;; CHECK: (func $callee (type $9) (param $a (ref null $A)) (param $i i32) (result i32) + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NO_TNH: (export "exported" (func $exported)) + + ;; NO_TNH: (func $callee (type $6) (param $a (ref $A)) (param $i i32) (result i32) + ;; NO_TNH-NEXT: (struct.get $A 0 + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + (func $callee (param $a (ref $A)) (param $i i32) (result i32) + (struct.get $A 0 + (local.get $a) + ) + ) + + ;; The arg cast to the private callee is removed; the arg cast to the import + ;; is kept (its type is public). + ;; CHECK: (func $caller (type $8) (param $a (ref null $A)) (result i32) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (call $callee + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $import + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NO_TNH: (func $caller (type $3) (param $a (ref null $A)) (result i32) + ;; NO_TNH-NEXT: (i32.add + ;; NO_TNH-NEXT: (call $callee + ;; NO_TNH-NEXT: (ref.as_non_null + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (i32.const 0) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (call $import + ;; NO_TNH-NEXT: (ref.as_non_null + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + (func $caller (param $a (ref null $A)) (result i32) + (i32.add + (call $callee + (ref.as_non_null + (local.get $a) + ) + (i32.const 0) + ) + (call $import + (ref.as_non_null + (local.get $a) + ) + ) + ) + ) + + ;; A non-nullable local becomes nullable; the copy through a cast becomes a + ;; plain copy (that coalesce-locals can remove later). The struct.new operand + ;; and the global.set of a non-nullable global demand non-null values, so + ;; casts are inserted there. The explicit and implicit returns of a + ;; non-nullable result get casts too. + ;; CHECK: (func $locals (type $7) (param $a (ref null $A)) (result (ref $A)) + ;; CHECK-NEXT: (local $b (ref null $A)) + ;; CHECK-NEXT: (local $c (ref null $A)) + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (block (result (ref null $A)) + ;; CHECK-NEXT: (local.set $b + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $c + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.new $B + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $c) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $gnn + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (global.set $g + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (local.get $c) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (return + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $c) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NO_TNH: (func $locals (type $7) (param $a (ref null $A)) (result (ref $A)) + ;; NO_TNH-NEXT: (local $b (ref $A)) + ;; NO_TNH-NEXT: (local $c (ref $A)) + ;; NO_TNH-NEXT: (local.set $b + ;; NO_TNH-NEXT: (ref.as_non_null + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (local.set $c + ;; NO_TNH-NEXT: (local.get $b) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (drop + ;; NO_TNH-NEXT: (struct.new $B + ;; NO_TNH-NEXT: (local.get $c) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (global.set $gnn + ;; NO_TNH-NEXT: (local.get $b) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (global.set $g + ;; NO_TNH-NEXT: (local.get $b) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (if + ;; NO_TNH-NEXT: (struct.get $A 0 + ;; NO_TNH-NEXT: (local.get $c) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (then + ;; NO_TNH-NEXT: (return + ;; NO_TNH-NEXT: (local.get $c) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (local.get $b) + ;; NO_TNH-NEXT: ) + (func $locals (param $a (ref null $A)) (result (ref $A)) + (local $b (ref $A)) + (local $c (ref $A)) + (local.set $b + (ref.as_non_null + (local.get $a) + ) + ) + (local.set $c + (local.get $b) + ) + (drop + (struct.new $B + (local.get $c) + ) + ) + (global.set $gnn + (local.get $b) + ) + (global.set $g + (local.get $b) + ) + (if + (struct.get $A 0 + (local.get $c) + ) + (then + (return + (local.get $c) + ) + ) + ) + (local.get $b) + ) + + ;; A function whose type is used by a call_ref: the signature is rewritten + ;; for both the function and the call_ref, whose arg cast goes away. The + ;; result stays non-nullable, so the body gets a cast. + ;; CHECK: (func $indirect (type $sig) (param $a (ref null $A)) (result (ref $A)) + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NO_TNH: (func $indirect (type $sig) (param $a (ref $A)) (result (ref $A)) + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + (func $indirect (type $sig) (param $a (ref $A)) (result (ref $A)) + (local.get $a) + ) + + ;; CHECK: (func $call-indirect (type $4) (param $a (ref null $A)) (param $f (ref null $sig)) (result (ref $A)) + ;; CHECK-NEXT: (call_ref $sig + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: (local.get $f) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NO_TNH: (func $call-indirect (type $8) (param $a (ref null $A)) (param $f (ref $sig)) (result (ref $A)) + ;; NO_TNH-NEXT: (call_ref $sig + ;; NO_TNH-NEXT: (ref.as_non_null + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (local.get $f) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + (func $call-indirect (param $a (ref null $A)) (param $f (ref $sig)) (result (ref $A)) + (call_ref $sig + (ref.as_non_null + (local.get $a) + ) + (local.get $f) + ) + ) + + ;; Exported function: the signature is public and must not change, but its + ;; locals still relax. + ;; CHECK: (func $exported (type $pub) (param $a (ref $A)) (result i32) + ;; CHECK-NEXT: (local $t (ref null $A)) + ;; CHECK-NEXT: (local.set $t + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (local.get $t) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NO_TNH: (func $exported (type $pub) (param $a (ref $A)) (result i32) + ;; NO_TNH-NEXT: (local $t (ref $A)) + ;; NO_TNH-NEXT: (local.set $t + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (struct.get $A 0 + ;; NO_TNH-NEXT: (local.get $t) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + (func $exported (export "exported") (type $pub) (param $a (ref $A)) (result i32) + (local $t (ref $A)) + (local.set $t + (local.get $a) + ) + (struct.get $A 0 + (local.get $t) + ) + ) + + ;; Tag types are not changed; the throw operand demands a non-null value. + ;; CHECK: (func $thrower (type $3) (param $a (ref null $A)) + ;; CHECK-NEXT: (throw $tag + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NO_TNH: (func $thrower (type $9) (param $a (ref null $A)) + ;; NO_TNH-NEXT: (throw $tag + ;; NO_TNH-NEXT: (ref.as_non_null + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + (func $thrower (param $a (ref null $A)) + (throw $tag + (ref.as_non_null + (local.get $a) + ) + ) + ) + + ;; Casts whose value is already non-nullable are dropped where allowed, and + ;; a nested chain of casts is removed entirely. + ;; CHECK: (func $chain (type $8) (param $a (ref null $A)) (result i32) + ;; CHECK-NEXT: (local $b (ref null $A)) + ;; CHECK-NEXT: (local.set $b + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (call $callee + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NO_TNH: (func $chain (type $3) (param $a (ref null $A)) (result i32) + ;; NO_TNH-NEXT: (local $b (ref $A)) + ;; NO_TNH-NEXT: (local.set $b + ;; NO_TNH-NEXT: (ref.as_non_null + ;; NO_TNH-NEXT: (ref.as_non_null + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (call $callee + ;; NO_TNH-NEXT: (ref.as_non_null + ;; NO_TNH-NEXT: (local.get $b) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (i32.const 1) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + (func $chain (param $a (ref null $A)) (result i32) + (local $b (ref $A)) + (local.set $b + (ref.as_non_null + (ref.as_non_null + (local.get $a) + ) + ) + ) + (call $callee + (ref.as_non_null + (local.get $b) + ) + (i32.const 1) + ) + ) + + ;; Values flowing through control flow: the if's type becomes nullable and + ;; the demanding consumer gets a single cast on the outside. + ;; CHECK: (func $flow (type $2) (param $a (ref null $A)) (param $x i32) + ;; CHECK-NEXT: (local $b (ref null $A)) + ;; CHECK-NEXT: (local.set $b + ;; CHECK-NEXT: (local.get $a) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.new $B + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (if (result (ref null $A)) + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (local.get $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (struct.new $A + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; NO_TNH: (func $flow (type $10) (param $a (ref null $A)) (param $x i32) + ;; NO_TNH-NEXT: (local $b (ref $A)) + ;; NO_TNH-NEXT: (local.set $b + ;; NO_TNH-NEXT: (ref.as_non_null + ;; NO_TNH-NEXT: (local.get $a) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (drop + ;; NO_TNH-NEXT: (struct.new $B + ;; NO_TNH-NEXT: (if (result (ref $A)) + ;; NO_TNH-NEXT: (local.get $x) + ;; NO_TNH-NEXT: (then + ;; NO_TNH-NEXT: (local.get $b) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: (else + ;; NO_TNH-NEXT: (struct.new $A + ;; NO_TNH-NEXT: (i32.const 1) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + ;; NO_TNH-NEXT: ) + (func $flow (param $a (ref null $A)) (param $x i32) + (local $b (ref $A)) + (local.set $b + (ref.as_non_null + (local.get $a) + ) + ) + (drop + (struct.new $B + (if (result (ref $A)) + (local.get $x) + (then + (local.get $b) + ) + (else + (struct.new $A + (i32.const 1) + ) + ) + ) + ) + ) + ) +)