From d9b13e6dd6a01872837cbc25269ce59f89073cc0 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 23 Mar 2026 13:15:54 -0700 Subject: [PATCH 1/3] fix --- src/passes/TypeRefining.cpp | 40 ++++++++--- test/lit/passes/type-refining-gufa.wast | 51 ++++++++++++++ test/lit/passes/type-refining.wast | 91 ++++++++++++++----------- 3 files changed, 133 insertions(+), 49 deletions(-) diff --git a/src/passes/TypeRefining.cpp b/src/passes/TypeRefining.cpp index 762c0a94e66..ee9846253ba 100644 --- a/src/passes/TypeRefining.cpp +++ b/src/passes/TypeRefining.cpp @@ -474,11 +474,9 @@ struct TypeRefining : public Pass { TypeRewriter(wasm, *this).update(); - ReFinalize().run(getPassRunner(), &wasm); - - // After refinalizing, we may still have situations that do not validate. - // In some cases we can infer something more precise than can be represented - // in wasm, like here: + // Even with refinalizing (which we do below), we may still have situations + // that do not validate, because in some cases we can infer something more + // precise than can be represented in wasm. For example: // // (try (result A) // (struct.get ..) ;; returns B. @@ -514,9 +512,7 @@ struct TypeRefining : public Pass { for (Index i = 0; i < fields.size(); i++) { auto*& operand = curr->operands[i]; auto fieldType = fields[i].type; - if (!Type::isSubType(operand->type, fieldType)) { - operand = Builder(*getModule()).makeRefCast(operand, fieldType); - } + operand = fixType(operand, fieldType); } } @@ -532,17 +528,39 @@ struct TypeRefining : public Pass { } auto fieldType = type.getStruct().fields[curr->index].type; + curr->value = fixType(curr->value, fieldType); + } - if (!Type::isSubType(curr->value->type, fieldType)) { - curr->value = - Builder(*getModule()).makeRefCast(curr->value, fieldType); + // Fix up a given value so it fits into the type the location it is + // written to. + Expression* fixType(Expression* value, Type type) { + if (Type::isSubType(value->type, type)) { + return value; } + // We cast to fix this up. An exception is a bottom type, which we can + // handle by emitting a null (which works with types that cannot be + // cast, like continuations; it also explicitly provides the value being + // written, which other passes would do anyhow). + Builder builder(*getModule()); + auto heapType = type.getHeapType(); + if (heapType.isBottom()) { + auto* drop = builder.makeDrop(value); + if (type.isNonNullable()) { + // This will just trap. + return builder.makeSequence(drop, builder.makeUnreachable()); + } else { + return builder.makeSequence(drop, builder.makeRefNull(heapType)); + } + } + return builder.makeRefCast(value, type); } }; WriteUpdater updater; updater.run(getPassRunner(), &wasm); updater.runOnModuleCode(getPassRunner(), &wasm); + + ReFinalize().run(getPassRunner(), &wasm); } }; diff --git a/test/lit/passes/type-refining-gufa.wast b/test/lit/passes/type-refining-gufa.wast index d6330890d8f..f572385d2a7 100644 --- a/test/lit/passes/type-refining-gufa.wast +++ b/test/lit/passes/type-refining-gufa.wast @@ -611,3 +611,54 @@ ) ) +;; When refining to nullcontref we cannot use a cast. Just emit a null. +(module + (rec + ;; NRML: (rec + ;; NRML-NEXT: (type $func (sub (func))) + ;; GUFA: (rec + ;; GUFA-NEXT: (type $func (sub (func))) + (type $func (sub (func))) + ;; NRML: (type $cont (sub (cont $func))) + ;; GUFA: (type $cont (sub (cont $func))) + (type $cont (sub (cont $func))) + ;; NRML: (type $struct (struct (field (ref null $cont)))) + ;; GUFA: (type $struct (struct (field nullcontref))) + (type $struct (struct (field (ref null $cont)))) + ) + + ;; NRML: (type $3 (func)) + + ;; NRML: (func $test (type $3) + ;; NRML-NEXT: (local $null (ref null $cont)) + ;; NRML-NEXT: (drop + ;; NRML-NEXT: (struct.new $struct + ;; NRML-NEXT: (local.get $null) + ;; NRML-NEXT: ) + ;; NRML-NEXT: ) + ;; NRML-NEXT: ) + ;; GUFA: (type $3 (func)) + + ;; GUFA: (func $test (type $3) + ;; GUFA-NEXT: (local $null (ref null $cont)) + ;; GUFA-NEXT: (drop + ;; GUFA-NEXT: (struct.new $struct + ;; GUFA-NEXT: (block (result nullcontref) + ;; GUFA-NEXT: (drop + ;; GUFA-NEXT: (local.get $null) + ;; GUFA-NEXT: ) + ;; GUFA-NEXT: (ref.null nocont) + ;; GUFA-NEXT: ) + ;; GUFA-NEXT: ) + ;; GUFA-NEXT: ) + ;; GUFA-NEXT: ) + (func $test + (local $null (ref null $cont)) + (drop + (struct.new $struct + (local.get $null) + ) + ) + ) +) + diff --git a/test/lit/passes/type-refining.wast b/test/lit/passes/type-refining.wast index ec64c58bebc..f5e51f65d49 100644 --- a/test/lit/passes/type-refining.wast +++ b/test/lit/passes/type-refining.wast @@ -1018,20 +1018,7 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $A 0 ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: (if (result (ref $A)) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (struct.get $A 0 - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (else - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.new $A + ;; CHECK-NEXT: (ref.cast (ref $A) ;; CHECK-NEXT: (if (result (ref $A)) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (then @@ -1045,6 +1032,23 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.new $A + ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (if (result (ref $A)) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (local.get $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $non-nullability-block (param $nn (ref $A)) (param $A (ref null $A)) (struct.set $A 0 @@ -1150,9 +1154,11 @@ ;; CHECK: (func $0 (type $2) (result (ref $A)) ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (ref.cast (ref (exact $B)) - ;; CHECK-NEXT: (struct.get $A 0 - ;; CHECK-NEXT: (struct.new $A - ;; CHECK-NEXT: (struct.new_default $B) + ;; CHECK-NEXT: (ref.cast (ref (exact $B)) + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (struct.new $A + ;; CHECK-NEXT: (struct.new_default $B) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) @@ -1252,23 +1258,29 @@ (tag $tag) ;; CHECK: (func $struct.new (type $2) (param $extern externref) (result anyref) - ;; CHECK-NEXT: (struct.new $A - ;; CHECK-NEXT: (ref.cast (ref noextern) - ;; CHECK-NEXT: (try (result externref) - ;; CHECK-NEXT: (do - ;; CHECK-NEXT: (struct.get $A 0 - ;; CHECK-NEXT: (struct.new $A - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: (block ;; (replaces unreachable StructNew we can't emit) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (try (result externref) + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (struct.new $A + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $tag + ;; CHECK-NEXT: (local.get $extern) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (catch $tag - ;; CHECK-NEXT: (local.get $extern) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $struct.new (param $extern externref) (result anyref) @@ -1305,21 +1317,24 @@ ;; CHECK: (func $struct.set (type $3) (param $ref (ref $A)) (param $extern externref) ;; CHECK-NEXT: (struct.set $A 0 ;; CHECK-NEXT: (local.get $ref) - ;; CHECK-NEXT: (ref.cast (ref noextern) - ;; CHECK-NEXT: (try (result externref) - ;; CHECK-NEXT: (do - ;; CHECK-NEXT: (struct.get $A 0 - ;; CHECK-NEXT: (struct.new $A - ;; CHECK-NEXT: (ref.as_non_null - ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (try (result externref) + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (struct.new $A + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (ref.null noextern) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (catch $tag - ;; CHECK-NEXT: (local.get $extern) + ;; CHECK-NEXT: (catch $tag + ;; CHECK-NEXT: (local.get $extern) + ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (unreachable) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) From d5cf133da7e2b510739f35aa1e4af2141181067a Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 23 Mar 2026 16:39:09 -0700 Subject: [PATCH 2/3] fix --- src/passes/TypeRefining.cpp | 24 ++++++++++++------ test/lit/passes/type-refining.wast | 40 +++++++++++++----------------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/src/passes/TypeRefining.cpp b/src/passes/TypeRefining.cpp index ee9846253ba..f99ed5093e1 100644 --- a/src/passes/TypeRefining.cpp +++ b/src/passes/TypeRefining.cpp @@ -474,9 +474,13 @@ struct TypeRefining : public Pass { TypeRewriter(wasm, *this).update(); - // Even with refinalizing (which we do below), we may still have situations - // that do not validate, because in some cases we can infer something more - // precise than can be represented in wasm. For example: + // Refinalization fixes up types and makes them fit in the places we write + // them to. + ReFinalize().run(getPassRunner(), &wasm); + + // After refinalizing, we may still have situations that do not validate. + // In some cases we can infer something more precise than can be represented + // in wasm, like here: // // (try (result A) // (struct.get ..) ;; returns B. @@ -531,6 +535,8 @@ struct TypeRefining : public Pass { curr->value = fixType(curr->value, fieldType); } + bool refinalize = false; + // Fix up a given value so it fits into the type the location it is // written to. Expression* fixType(Expression* value, Type type) { @@ -546,7 +552,8 @@ struct TypeRefining : public Pass { if (heapType.isBottom()) { auto* drop = builder.makeDrop(value); if (type.isNonNullable()) { - // This will just trap. + // This will just trap. Make it trap, and update parents' types. + refinalize = true; return builder.makeSequence(drop, builder.makeUnreachable()); } else { return builder.makeSequence(drop, builder.makeRefNull(heapType)); @@ -554,13 +561,16 @@ struct TypeRefining : public Pass { } return builder.makeRefCast(value, type); } + + void visitFunction(Function* func) { + if (refinalize) { + ReFinalize().walkFunctionInModule(func, getModule()); + } + } }; WriteUpdater updater; updater.run(getPassRunner(), &wasm); - updater.runOnModuleCode(getPassRunner(), &wasm); - - ReFinalize().run(getPassRunner(), &wasm); } }; diff --git a/test/lit/passes/type-refining.wast b/test/lit/passes/type-refining.wast index f5e51f65d49..d4d141d52d3 100644 --- a/test/lit/passes/type-refining.wast +++ b/test/lit/passes/type-refining.wast @@ -1018,7 +1018,20 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: (struct.set $A 0 ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: (ref.cast (ref $A) + ;; CHECK-NEXT: (if (result (ref $A)) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (local.get $A) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (else + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (if (result (ref $A)) ;; CHECK-NEXT: (i32.const 1) ;; CHECK-NEXT: (then @@ -1032,23 +1045,6 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (drop - ;; CHECK-NEXT: (struct.new $A - ;; CHECK-NEXT: (ref.cast (ref $A) - ;; CHECK-NEXT: (if (result (ref $A)) - ;; CHECK-NEXT: (i32.const 1) - ;; CHECK-NEXT: (then - ;; CHECK-NEXT: (struct.get $A 0 - ;; CHECK-NEXT: (local.get $A) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: (else - ;; CHECK-NEXT: (unreachable) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) - ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) (func $non-nullability-block (param $nn (ref $A)) (param $A (ref null $A)) (struct.set $A 0 @@ -1154,11 +1150,9 @@ ;; CHECK: (func $0 (type $2) (result (ref $A)) ;; CHECK-NEXT: (struct.new $A ;; CHECK-NEXT: (ref.cast (ref (exact $B)) - ;; CHECK-NEXT: (ref.cast (ref (exact $B)) - ;; CHECK-NEXT: (struct.get $A 0 - ;; CHECK-NEXT: (struct.new $A - ;; CHECK-NEXT: (struct.new_default $B) - ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (struct.get $A 0 + ;; CHECK-NEXT: (struct.new $A + ;; CHECK-NEXT: (struct.new_default $B) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) From 277a899f65bec69e134ca27e4f88b083445295f0 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Mon, 23 Mar 2026 16:41:22 -0700 Subject: [PATCH 3/3] fix --- test/lit/passes/type-refining.wast | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/lit/passes/type-refining.wast b/test/lit/passes/type-refining.wast index d4d141d52d3..4c0261620d0 100644 --- a/test/lit/passes/type-refining.wast +++ b/test/lit/passes/type-refining.wast @@ -1288,8 +1288,8 @@ ;; type than the body. ;; ;; In such situations we rely on other optimizations to improve things, like - ;; getting rid of the catch in this case. In this pass we add a cast to get - ;; things to validate, which should be removable by other passes later on. + ;; getting rid of the catch in this case. In this pass we add an unreachable + ;; for the uninhabitable type, which fixes validation. (struct.new $A (try (result externref) (do