From 3fd8d368a5ae25eb562513d7f366cf0cb2d22712 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 19 Oct 2026 02:30:33 -0700 Subject: [PATCH 1/2] [CtorEval] Skip uninitialized non-nullable locals in partial evaluation When evalCtor partially evaluates a function, non-parameter locals start in FunctionScope with Literal::makeZeros(type), which produces a null reference placeholder for non-nullable reference locals. If a non-nullable local was not yet written before partial evaluation stopped, emitting a local.set with that serialized null expression produced invalid Wasm. Furthermore, wrapping the initial local.sets in a separate inner block before the copied function body prevented initialized non-nullable locals from structurally dominating uses in the remaining body. Only emit initial local.sets when the serialized expression is a valid subtype of the local type, and insert them directly at the start of the copied body block. --- src/tools/wasm-ctor-eval.cpp | 12 ++- .../lit/ctor-eval/materialize-null-local.wast | 80 ++++++++++++++++++- 2 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index a948f258c65..6eb80037021 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -1334,12 +1334,16 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, copyBody->list[i] = builder.makeNop(); } - // Put the local sets at the front of the function body. - auto* setsBlock = builder.makeBlock(); + // Put the local sets at the front of the function body. Skip + // uninitialized non-nullable locals, whose default null value in the + // interpreter is not a valid subtype of the local's type. + Index insertIndex = 0; for (Index i = 0; i < localExprs.size(); ++i) { - setsBlock->list.push_back(builder.makeLocalSet(i, localExprs[i])); + if (Type::isSubType(localExprs[i]->type, func->getLocalType(i))) { + copyBody->list.insertAt(insertIndex++, + builder.makeLocalSet(i, localExprs[i])); + } } - copyBody = builder.makeSequence(setsBlock, copyBody, copyBody->type); // We may have return-called into a function with different parameter // types, but we ultimately need to export a function with the original diff --git a/test/lit/ctor-eval/materialize-null-local.wast b/test/lit/ctor-eval/materialize-null-local.wast index 1f880f6816b..fd330c398ee 100644 --- a/test/lit/ctor-eval/materialize-null-local.wast +++ b/test/lit/ctor-eval/materialize-null-local.wast @@ -1,12 +1,22 @@ ;; NOTE: Assertions have been generated by update_lit_checks.py --all-items and should not be edited. -;; RUN: wasm-ctor-eval %s -all --ctors=test --kept-exports=test --ignore-external-input -S -o - \ +;; RUN: wasm-ctor-eval %s -all --ctors=test,non_nullable --kept-exports=test,non_nullable --ignore-external-input -S -o - \ ;; RUN: | filecheck %s ;; Check that materializing a non-data null local, which at time of writing uses ;; Builder::makeConstantExpression, does not trigger an assertion failure. (module + ;; CHECK: (type $struct (struct)) + (type $struct (struct)) + + ;; CHECK: (type $1 (func (param externref))) + + ;; CHECK: (type $2 (func (result (ref $struct)))) + + ;; CHECK: (import "env" "g" (global $g i32)) + (import "env" "g" (global $g i32)) + (func $test (export "test") (param $0 externref) (local $3 anyref) (local.set $3 @@ -15,12 +25,74 @@ ) ) ) + + (func $non_nullable (export "non_nullable") (result (ref $struct)) + ;; Partially evaluate the first statement, then stop on the imported global. + ;; Do not emit an invalid (local.set $unset (ref.null none)) for the + ;; uninitialized non-nullable local, and ensure the initialized non-nullable + ;; local $init structurally dominates its use in the remaining body. + (local $init (ref $struct)) + (local $unset (ref $struct)) + (local.set $init + (struct.new $struct) + ) + (if + (global.get $g) + (then + (return + (local.get $init) + ) + ) + ) + (local.set $unset + (struct.new $struct) + ) + (if + (global.get $g) + (then + (local.set $unset + (local.get $init) + ) + ) + ) + (local.get $unset) + ) ) -;; CHECK: (type $0 (func (param externref))) +;; CHECK: (global $ctor-eval$global (ref (exact $struct)) (struct.new_default $struct)) -;; CHECK: (export "test" (func $test_1)) +;; CHECK: (export "test" (func $test_2)) -;; CHECK: (func $test_1 (type $0) (param $0 externref) +;; CHECK: (export "non_nullable" (func $non_nullable_3)) + +;; CHECK: (func $test_2 (type $1) (param $0 externref) ;; CHECK-NEXT: (local $3 anyref) ;; CHECK-NEXT: (nop) ;; CHECK-NEXT: ) + +;; CHECK: (func $non_nullable_3 (type $2) (result (ref $struct)) +;; CHECK-NEXT: (local $0 (ref (exact $struct))) +;; CHECK-NEXT: (local $1 (ref (exact $struct))) +;; CHECK-NEXT: (local.set $0 +;; CHECK-NEXT: (global.get $ctor-eval$global) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (global.get $g) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (return +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (struct.new_default $struct) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (if +;; CHECK-NEXT: (global.get $g) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (local.set $1 +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: ) From 13c15437abecf6f5c938703e504995a2b420e58c Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 19 Oct 2026 19:51:40 -0700 Subject: [PATCH 2/2] no quadratic --- src/tools/wasm-ctor-eval.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/tools/wasm-ctor-eval.cpp b/src/tools/wasm-ctor-eval.cpp index 6eb80037021..fcb02f9c35f 100644 --- a/src/tools/wasm-ctor-eval.cpp +++ b/src/tools/wasm-ctor-eval.cpp @@ -1329,21 +1329,21 @@ EvalCtorOutcome evalCtor(EvallingModuleRunner& instance, auto* copyBody = builder.blockify(ExpressionManipulator::copy(func->body, wasm)); - // Remove the items we've evalled. - for (Index i = 0; i < successes; i++) { - copyBody->list[i] = builder.makeNop(); - } - - // Put the local sets at the front of the function body. Skip - // uninitialized non-nullable locals, whose default null value in the - // interpreter is not a valid subtype of the local's type. - Index insertIndex = 0; + // Replace the items we've evalled with the local sets at the front of the + // function body. Skip uninitialized non-nullable locals, whose default + // null value in the interpreter is not a valid subtype of the local's + // type. + std::vector newList; + newList.reserve(localExprs.size() + copyBody->list.size() - successes); for (Index i = 0; i < localExprs.size(); ++i) { if (Type::isSubType(localExprs[i]->type, func->getLocalType(i))) { - copyBody->list.insertAt(insertIndex++, - builder.makeLocalSet(i, localExprs[i])); + newList.push_back(builder.makeLocalSet(i, localExprs[i])); } } + newList.insert(newList.end(), + copyBody->list.begin() + successes, + copyBody->list.end()); + copyBody->list.set(newList); // We may have return-called into a function with different parameter // types, but we ultimately need to export a function with the original