From 903f80365fab4cbcc5a8d16182a2729a01ec7307 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 6 Aug 2026 09:40:49 -0700 Subject: [PATCH 1/9] fix --- src/wasm/wasm.cpp | 4 ++ test/lit/passes/local-subtyping-desc.wast | 74 +++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 test/lit/passes/local-subtyping-desc.wast diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index a8c5afa8d5d..6694e28b568 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -1212,6 +1212,10 @@ void BrOn::finalize() { if (castType.isNullable()) { // Nulls take the branch, so the result is non-nullable. type = ref->type.with(NonNullable); + } else if (desc->type.isNull()) { + // Cast will never be executed and the instruction will not be emitted. + // Model this with an uninhabitable cast type. + type = desc->type.with(NonNullable); } else { // Nulls do not take the branch, so the result is non-nullable only if // the input is. diff --git a/test/lit/passes/local-subtyping-desc.wast b/test/lit/passes/local-subtyping-desc.wast new file mode 100644 index 00000000000..420ffb0fe56 --- /dev/null +++ b/test/lit/passes/local-subtyping-desc.wast @@ -0,0 +1,74 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. + +;; RUN: wasm-opt %s -all --local-subtyping -S -o - | filecheck %s + +(module + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $0 (sub (descriptor $2) (struct))) + (type $0 (sub (descriptor $2) (struct))) + ;; CHECK: (type $1 (sub $0 (descriptor $3) (struct))) + (type $1 (sub $0 (descriptor $3) (struct))) + ;; CHECK: (type $2 (sub (describes $0) (struct))) + (type $2 (sub (describes $0) (struct))) + ;; CHECK: (type $3 (sub $2 (describes $1) (struct (field i32)))) + (type $3 (sub $2 (describes $1) (struct (field i32)))) + ) + + ;; CHECK: (func $test (type $4) (result (ref null $1)) + ;; CHECK-NEXT: (local $1 (ref none)) + ;; CHECK-NEXT: (local $2 (ref none)) + ;; CHECK-NEXT: (block $block (result (ref none)) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block ;; (replaces unreachable StructGet we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.tee $1 + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (block ;; (replaces unreachable BrOn we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $test (result (ref null $1)) + ;; We can refine both these locals to (ref none). While doing so we must + ;; refinalize the br_on properly, below, to that same type (it is never + ;; executed). + (local $1 (ref $3)) + (local $2 (ref $1)) + (block $block (result (ref null $1)) + (drop + (struct.get $3 0 + (local.tee $1 + (ref.as_non_null + (ref.null none) + ) + ) + ) + ) + (local.set $2 + (br_on_cast_desc_eq $block (ref null $1) (ref null $1) + (ref.null $1) + (local.get $1) + ) + ) + (unreachable) + ) + ) +) + From 59a4cbd787d22cb143b2ea7b5759a815b2e5854f Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 6 Aug 2026 10:56:59 -0700 Subject: [PATCH 2/9] fix --- src/wasm/wasm.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wasm/wasm.cpp b/src/wasm/wasm.cpp index 6694e28b568..2f905482bf6 100644 --- a/src/wasm/wasm.cpp +++ b/src/wasm/wasm.cpp @@ -1212,7 +1212,7 @@ void BrOn::finalize() { if (castType.isNullable()) { // Nulls take the branch, so the result is non-nullable. type = ref->type.with(NonNullable); - } else if (desc->type.isNull()) { + } else if (desc && desc->type.isNull()) { // Cast will never be executed and the instruction will not be emitted. // Model this with an uninhabitable cast type. type = desc->type.with(NonNullable); From df9f6cb1dee5ab85c0afd5d2d58c04a2f31e85e7 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 7 Aug 2026 09:48:11 -0700 Subject: [PATCH 3/9] add detailed commwnt --- test/lit/passes/local-subtyping-desc.wast | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/lit/passes/local-subtyping-desc.wast b/test/lit/passes/local-subtyping-desc.wast index 420ffb0fe56..9ed6e546aeb 100644 --- a/test/lit/passes/local-subtyping-desc.wast +++ b/test/lit/passes/local-subtyping-desc.wast @@ -48,7 +48,18 @@ (func $test (result (ref null $1)) ;; We can refine both these locals to (ref none). While doing so we must ;; refinalize the br_on properly, below, to that same type (it is never - ;; executed). + ;; executed). In more detail, we end up doing two iterations of refining in + ;; the pass, refinalizing each time. Here is what happens to the BrOn: + ;; + ;; * The first time, the types of its inputs are ref=nullref, desc=(ref $3). + ;; That refinalizes into (ref none) for the BrOn. + ;; * The first time, the types of its inputs are ref=nullref, desc=(ref none). + ;; This test verifies that we fixed a bug where the BrOn was given the + ;; type of the ref, i.e., nullref - which led to an assert, as we refined + ;; (ref none) into nullref. That is, the code path handling a null + ;; descriptor type did not consider that it might need to emit a non- + ;; nullable type even if the ref is nullable. + ;; (local $1 (ref $3)) (local $2 (ref $1)) (block $block (result (ref null $1)) From f13e568a6b9e67553fbc4fbd0594feb6aa7515ba Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 7 Aug 2026 09:55:06 -0700 Subject: [PATCH 4/9] simpl --- test/lit/passes/local-subtyping-desc.wast | 46 +++++++++++------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/test/lit/passes/local-subtyping-desc.wast b/test/lit/passes/local-subtyping-desc.wast index 9ed6e546aeb..d03ebfc9df6 100644 --- a/test/lit/passes/local-subtyping-desc.wast +++ b/test/lit/passes/local-subtyping-desc.wast @@ -5,24 +5,20 @@ (module (rec ;; CHECK: (rec - ;; CHECK-NEXT: (type $0 (sub (descriptor $2) (struct))) - (type $0 (sub (descriptor $2) (struct))) - ;; CHECK: (type $1 (sub $0 (descriptor $3) (struct))) - (type $1 (sub $0 (descriptor $3) (struct))) - ;; CHECK: (type $2 (sub (describes $0) (struct))) - (type $2 (sub (describes $0) (struct))) - ;; CHECK: (type $3 (sub $2 (describes $1) (struct (field i32)))) - (type $3 (sub $2 (describes $1) (struct (field i32)))) + ;; CHECK-NEXT: (type $struct (sub (descriptor $desc) (struct))) + (type $struct (sub (descriptor $desc) (struct))) + ;; CHECK: (type $desc (sub (describes $struct) (struct (field i32)))) + (type $desc (sub (describes $struct) (struct (field i32)))) ) - ;; CHECK: (func $test (type $4) (result (ref null $1)) - ;; CHECK-NEXT: (local $1 (ref none)) - ;; CHECK-NEXT: (local $2 (ref none)) + ;; CHECK: (func $test (type $2) (result (ref null $struct)) + ;; CHECK-NEXT: (local $struct (ref none)) + ;; CHECK-NEXT: (local $desc (ref none)) ;; CHECK-NEXT: (block $block (result (ref none)) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (block ;; (replaces unreachable StructGet we can't emit) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.tee $1 + ;; CHECK-NEXT: (local.tee $struct ;; CHECK-NEXT: (ref.as_non_null ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) @@ -31,13 +27,13 @@ ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (local.set $desc ;; CHECK-NEXT: (block ;; (replaces unreachable BrOn we can't emit) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (local.get $struct) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) @@ -45,13 +41,13 @@ ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - (func $test (result (ref null $1)) + (func $test (result (ref null $struct)) ;; We can refine both these locals to (ref none). While doing so we must ;; refinalize the br_on properly, below, to that same type (it is never ;; executed). In more detail, we end up doing two iterations of refining in ;; the pass, refinalizing each time. Here is what happens to the BrOn: ;; - ;; * The first time, the types of its inputs are ref=nullref, desc=(ref $3). + ;; * The first time, the types of its inputs are ref=nullref, desc=(ref $desc). ;; That refinalizes into (ref none) for the BrOn. ;; * The first time, the types of its inputs are ref=nullref, desc=(ref none). ;; This test verifies that we fixed a bug where the BrOn was given the @@ -60,22 +56,22 @@ ;; descriptor type did not consider that it might need to emit a non- ;; nullable type even if the ref is nullable. ;; - (local $1 (ref $3)) - (local $2 (ref $1)) - (block $block (result (ref null $1)) + (local $struct (ref $desc)) + (local $desc (ref $struct)) + (block $block (result (ref null $struct)) (drop - (struct.get $3 0 - (local.tee $1 + (struct.get $desc 0 + (local.tee $struct (ref.as_non_null (ref.null none) ) ) ) ) - (local.set $2 - (br_on_cast_desc_eq $block (ref null $1) (ref null $1) - (ref.null $1) - (local.get $1) + (local.set $desc + (br_on_cast_desc_eq $block (ref null $struct) (ref null $struct) + (ref.null $struct) + (local.get $struct) ) ) (unreachable) From 1ee17f51ff5c52d5787ac9910adab67eb1b5a82e Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 7 Aug 2026 12:50:50 -0700 Subject: [PATCH 5/9] simpl --- test/lit/passes/local-subtyping-desc.wast | 48 ++++++++--------------- 1 file changed, 17 insertions(+), 31 deletions(-) diff --git a/test/lit/passes/local-subtyping-desc.wast b/test/lit/passes/local-subtyping-desc.wast index d03ebfc9df6..63dc47ffcda 100644 --- a/test/lit/passes/local-subtyping-desc.wast +++ b/test/lit/passes/local-subtyping-desc.wast @@ -13,27 +13,18 @@ ;; CHECK: (func $test (type $2) (result (ref null $struct)) ;; CHECK-NEXT: (local $struct (ref none)) - ;; CHECK-NEXT: (local $desc (ref none)) ;; CHECK-NEXT: (block $block (result (ref none)) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (block ;; (replaces unreachable StructGet we can't emit) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.tee $struct - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (ref.null none) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (local.set $desc + ;; CHECK-NEXT: (local.set $struct ;; CHECK-NEXT: (block ;; (replaces unreachable BrOn we can't emit) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (ref.null none) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (local.get $struct) + ;; CHECK-NEXT: (block (result (ref none)) + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (ref.null none) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) @@ -42,38 +33,33 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $test (result (ref null $struct)) - ;; We can refine both these locals to (ref none). While doing so we must + ;; We can refine both the local to (ref none). While doing so we must ;; refinalize the br_on properly, below, to that same type (it is never - ;; executed). In more detail, we end up doing two iterations of refining in - ;; the pass, refinalizing each time. Here is what happens to the BrOn: + ;; executed). In more detail: ;; - ;; * The first time, the types of its inputs are ref=nullref, desc=(ref $desc). + ;; * Initially, after parsing, the types of its inputs are + ;; ref=nullref, desc=(ref $desc). ;; That refinalizes into (ref none) for the BrOn. - ;; * The first time, the types of its inputs are ref=nullref, desc=(ref none). + ;; * When we refinalize, the types of its inputs are + ;; ref=nullref, desc=(ref none). ;; This test verifies that we fixed a bug where the BrOn was given the ;; type of the ref, i.e., nullref - which led to an assert, as we refined ;; (ref none) into nullref. That is, the code path handling a null ;; descriptor type did not consider that it might need to emit a non- ;; nullable type even if the ref is nullable. ;; - (local $struct (ref $desc)) - (local $desc (ref $struct)) + (local $struct (ref $struct)) (block $block (result (ref null $struct)) - (drop - (struct.get $desc 0 - (local.tee $struct + (local.set $struct + (br_on_cast_desc_eq $block (ref null $struct) (ref null $struct) + (ref.null $struct) + (block (result (ref $desc)) (ref.as_non_null (ref.null none) ) ) ) ) - (local.set $desc - (br_on_cast_desc_eq $block (ref null $struct) (ref null $struct) - (ref.null $struct) - (local.get $struct) - ) - ) (unreachable) ) ) From 1edcb4f3964f2501ff3cc4894fe88caacc326144 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 7 Aug 2026 12:52:52 -0700 Subject: [PATCH 6/9] simpl --- test/lit/passes/local-subtyping-desc.wast | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/lit/passes/local-subtyping-desc.wast b/test/lit/passes/local-subtyping-desc.wast index 63dc47ffcda..a1a5aeecfe8 100644 --- a/test/lit/passes/local-subtyping-desc.wast +++ b/test/lit/passes/local-subtyping-desc.wast @@ -33,13 +33,14 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $test (result (ref null $struct)) - ;; We can refine both the local to (ref none). While doing so we must + ;; We can refine the local to (ref none). While doing so we must ;; refinalize the br_on properly, below, to that same type (it is never ;; executed). In more detail: ;; ;; * Initially, after parsing, the types of its inputs are ;; ref=nullref, desc=(ref $desc). ;; That refinalizes into (ref none) for the BrOn. + ;; ;; * When we refinalize, the types of its inputs are ;; ref=nullref, desc=(ref none). ;; This test verifies that we fixed a bug where the BrOn was given the From 5c9e6663267bed89ca80d8150a185831f5fe470c Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 7 Aug 2026 12:53:34 -0700 Subject: [PATCH 7/9] simpl --- test/lit/passes/local-subtyping-desc.wast | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/lit/passes/local-subtyping-desc.wast b/test/lit/passes/local-subtyping-desc.wast index a1a5aeecfe8..d87bfb7ad9e 100644 --- a/test/lit/passes/local-subtyping-desc.wast +++ b/test/lit/passes/local-subtyping-desc.wast @@ -45,7 +45,7 @@ ;; ref=nullref, desc=(ref none). ;; This test verifies that we fixed a bug where the BrOn was given the ;; type of the ref, i.e., nullref - which led to an assert, as we refined - ;; (ref none) into nullref. That is, the code path handling a null + ;; (ref $struct) into nullref. That is, the code path handling a null ;; descriptor type did not consider that it might need to emit a non- ;; nullable type even if the ref is nullable. ;; From abce915795b2a48977398e6856dd104d9cd88b06 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 7 Aug 2026 16:02:03 -0700 Subject: [PATCH 8/9] update existing test --- test/lit/passes/vacuum-desc.wast | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/lit/passes/vacuum-desc.wast b/test/lit/passes/vacuum-desc.wast index 37a7efdde26..ceb37fff0fd 100644 --- a/test/lit/passes/vacuum-desc.wast +++ b/test/lit/passes/vacuum-desc.wast @@ -169,7 +169,7 @@ ;; CHECK: (func $br-on-cast-null-desc (type $2) (param $ref anyref) (param $desc nullref) ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (block $l (result anyref) + ;; CHECK-NEXT: (block $l (result (ref none)) ;; CHECK-NEXT: (block ;; (replaces unreachable BrOn we can't emit) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $ref) @@ -184,7 +184,7 @@ ;; CHECK-NEXT: ) ;; CKIIT: (func $br-on-cast-null-desc (type $2) (param $ref anyref) (param $desc nullref) ;; CKIIT-NEXT: (drop - ;; CKIIT-NEXT: (block $l (result anyref) + ;; CKIIT-NEXT: (block $l (result (ref none)) ;; CKIIT-NEXT: (block ;; (replaces unreachable BrOn we can't emit) ;; CKIIT-NEXT: (drop ;; CKIIT-NEXT: (local.get $ref) From 367e4427dd0d8827e8bd8b1347e460740a4deb53 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Fri, 7 Aug 2026 16:14:38 -0700 Subject: [PATCH 9/9] update existing test --- test/lit/passes/monomorphize-desc.wast | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/lit/passes/monomorphize-desc.wast b/test/lit/passes/monomorphize-desc.wast index 9523bc664a8..277443a41fa 100644 --- a/test/lit/passes/monomorphize-desc.wast +++ b/test/lit/passes/monomorphize-desc.wast @@ -15,7 +15,7 @@ ;; CHECK: (type $3 (func (result anyref))) - ;; CHECK: (type $4 (func (param nullref))) + ;; CHECK: (type $4 (func (param (ref none)))) ;; CHECK: (func $target (type $2) (param $0 i32) ;; CHECK-NEXT: (nop) @@ -60,6 +60,6 @@ ) ) -;; CHECK: (func $target_2 (type $4) (param $0 nullref) +;; CHECK: (func $target_2 (type $4) (param $0 (ref none)) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: )