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)
+            )
+          )
+        )
+      )
+    )
+  )
+)