From e8a39f0bc9c6178e9ecc47768ab49f21b50ea7bf Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 22 Sep 2026 21:47:55 -0700 Subject: [PATCH 1/2] [MakeSharedObjects] Support externref tags Keep externref parameter types on tags unchanged so exceptions leaving or entering the module carry externrefs rather than lowered i31ref table indices. Convert operands from i31ref to externref in visitThrow and convert popped values from externref to i31ref in visitPop, fixing up any resulting block-nested pops after walking the function. This lets imports of JSTag continue working as expected. --- src/passes/MakeSharedObjects.cpp | 82 ++++++- test/lit/passes/make-shared-objects.wast | 266 +++++++++++++++++++++++ 2 files changed, 345 insertions(+), 3 deletions(-) diff --git a/src/passes/MakeSharedObjects.cpp b/src/passes/MakeSharedObjects.cpp index f4ff53fe53e..81b4b05eb74 100644 --- a/src/passes/MakeSharedObjects.cpp +++ b/src/passes/MakeSharedObjects.cpp @@ -44,6 +44,7 @@ // references. #include "ir/drop.h" +#include "ir/eh-utils.h" #include "ir/module-utils.h" #include "ir/names.h" #include "ir/type-updating.h" @@ -510,8 +511,16 @@ struct MakeSharedObjects } std::unordered_map callRefTypes; + bool hasBlockNestedPop = false; void walkFunction(Function* curr) { + hasBlockNestedPop = false; + // Update local types before walking the body so that any scratch locals + // added during the walk with original boundary types (e.g. externref) are + // not overwritten afterward. + for (auto& type : curr->vars) { + updateType(type); + } // When we replace call_ref target operands with i31s, we will lose // information we need about the intended call target type. Collect those // types up front before we update anything. @@ -528,6 +537,9 @@ struct MakeSharedObjects CallRefCollector collector(callRefTypes); collector.walk(curr->body); WalkerPass::walkFunction(curr); + if (hasBlockNestedPop) { + EHUtils::handleBlockNestedPops(curr, *getModule()); + } } void visitCallRef(CallRef* curr) { @@ -725,10 +737,56 @@ struct MakeSharedObjects #include "wasm-delegations-fields.def" } - void visitFunction(Function* curr) { - for (auto& type : curr->vars) { - updateType(type); + // Compute the boundary parameter types for a tag, keeping externref + // parameters unchanged while lowering other parameter types. + Type getTagBoundaryParams(Type origParams) { + std::vector params; + for (Type t : origParams) { + params.push_back(externTable.isTableType(t) ? t : updatedType(t)); + } + return Type(params); + } + + // Convert lowered i31ref operands back to externrefs when throwing a tag + // that expects externref parameters. + void visitThrow(Throw* curr) { + visitExpression(curr); + Type origParams = getModule()->getTag(curr->tag)->params(); + for (Index i = 0; i < curr->operands.size(); ++i) { + curr->operands[i] = + externTable.convertToRef(curr->operands[i], origParams[i]); + } + } + + // Wrap pops with externref types in conversions from the caught externrefs + // to i31ref table indices. + void visitPop(Pop* curr) { + if (!externTable.hasTableType(curr->type)) { + visitExpression(curr); + return; + } + Type origParams = curr->type; + Type internalParams = updatedType(origParams); + Type boundaryParams = getTagBoundaryParams(origParams); + curr->type = boundaryParams; + if (origParams.isSingle()) { + replaceCurrent( + externTable.convertToIndex(curr, origParams, internalParams)); + return; } + Builder builder(*getModule()); + Index scratch = Builder::addVar(getFunction(), boundaryParams); + auto* set = builder.makeLocalSet(scratch, curr); + std::vector elems; + elems.reserve(origParams.size()); + for (Index i = 0; i < origParams.size(); ++i) { + auto* extract = builder.makeTupleExtract( + builder.makeLocalGet(scratch, boundaryParams), i); + elems.push_back( + externTable.convertToIndex(extract, origParams[i], internalParams[i])); + } + replaceCurrent(builder.makeSequence(set, builder.makeTupleMake(elems))); + hasBlockNestedPop = true; } void visitTable(Table* curr) { updateType(curr->type); } @@ -794,6 +852,11 @@ struct MakeSharedObjects Type origType; }; + struct TagToUpdate { + Tag* tag; + HeapType origType; + }; + void visitModule(Module* wasm) { std::vector importsToWrap; for (auto& func : wasm->functions) { @@ -814,8 +877,21 @@ struct MakeSharedObjects } } + std::vector tagsToUpdate; + for (auto& tag : wasm->tags) { + if (externTable.hasTableType(tag->params())) { + tagsToUpdate.push_back({tag.get(), tag->type}); + } + } + rewriteTypes(); + for (auto& [tag, origType] : tagsToUpdate) { + tag->type = getBoundaryType(Type(origType, NonNullable), + Type(tag->type, NonNullable)) + .getHeapType(); + } + for (auto& info : globalImportsToWrap) { wrapGlobalImport(info.global, info.origType); } diff --git a/test/lit/passes/make-shared-objects.wast b/test/lit/passes/make-shared-objects.wast index a51a20cd5ff..b643ff95acf 100644 --- a/test/lit/passes/make-shared-objects.wast +++ b/test/lit/passes/make-shared-objects.wast @@ -2683,3 +2683,269 @@ ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) ;; CHECK-NEXT: ) +(module + ;; All tags keep externref parameters unchanged while other reference + ;; parameters are lowered to shared types. + (tag $js-tag (import "WebAssembly" "JSTag") (param externref)) + ;; CHECK: (type $0 (func (result i32 (ref (shared i31))))) + + ;; CHECK: (type $1 (func (param externref))) + + ;; CHECK: (type $2 (func (param i32 (ref extern)))) + + ;; CHECK: (type $3 (func (param (ref null (shared any))))) + + ;; CHECK: (type $4 (func (param (ref null (shared i31)) (ref (shared i31)) (ref null (shared any))))) + + ;; CHECK: (type $5 (func (result (ref null (shared i31))))) + + ;; CHECK: (type $6 (func)) + + ;; CHECK: (type $7 (func (param externref) (result (ref null (shared i31))))) + + ;; CHECK: (type $8 (func (param (ref null (shared i31))) (result externref))) + + ;; CHECK: (import "WebAssembly" "JSTag" (tag $js-tag (type $1) (param externref))) + + ;; CHECK: (table $externs 0 externref) + + ;; CHECK: (tag $export-tag (type $2) (param i32 (ref extern))) + (tag $export-tag (export "export_tag") (param i32 (ref extern))) + ;; CHECK: (tag $internal-tag (type $1) (param externref)) + (tag $internal-tag (param externref)) + ;; CHECK: (tag $anyref-tag (type $3) (param (ref null (shared any)))) + (tag $anyref-tag (param anyref)) + + ;; CHECK: (export "export_tag" (tag $export-tag)) + + ;; CHECK: (func $throw-tags (type $4) (param $x (ref null (shared i31))) (param $y (ref (shared i31))) (param $z (ref null (shared any))) + ;; CHECK-NEXT: (throw $js-tag + ;; CHECK-NEXT: (call $index_to_extern + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw $export-tag + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: (ref.cast (ref extern) + ;; CHECK-NEXT: (call $index_to_extern + ;; CHECK-NEXT: (local.get $y) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw $internal-tag + ;; CHECK-NEXT: (call $index_to_extern + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (throw $anyref-tag + ;; CHECK-NEXT: (local.get $z) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $throw-tags (param $x externref) (param $y (ref extern)) (param $z anyref) + ;; Throwing any tag with externref parameters converts externref operands + ;; from i31ref to externref, while other reference parameters are lowered. + (throw $js-tag + (local.get $x) + ) + (throw $export-tag + (i32.const 42) + (local.get $y) + ) + (throw $internal-tag + (local.get $x) + ) + (throw $anyref-tag + (local.get $z) + ) + ) + + ;; CHECK: (func $try-legacy (type $5) (result (ref null (shared i31))) + ;; CHECK-NEXT: (try (result (ref null (shared i31))) + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $js-tag + ;; CHECK-NEXT: (call $extern_to_index + ;; CHECK-NEXT: (pop externref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $internal-tag + ;; CHECK-NEXT: (call $extern_to_index + ;; CHECK-NEXT: (pop externref) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $anyref-tag + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (pop (ref null (shared any))) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.null (shared none)) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try-legacy (result externref) + ;; Legacy try/catch wraps the pop of any tag with externref parameters in a + ;; conversion from externref to i31ref. + (try (result externref) + (do + (ref.null noextern) + ) + (catch $js-tag + (pop externref) + ) + (catch $internal-tag + (pop externref) + ) + (catch $anyref-tag + (drop + (pop anyref) + ) + (ref.null noextern) + ) + ) + ) + + ;; CHECK: (func $try-legacy-tuple (type $0) (result i32 (ref (shared i31))) + ;; CHECK-NEXT: (local $0 (tuple i32 (ref extern))) + ;; CHECK-NEXT: (try (type $0) (result i32 (ref (shared i31))) + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $export-tag + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (pop (tuple i32 (ref extern))) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (call $extern_to_index + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try-legacy-tuple (result i32 (ref extern)) + ;; Legacy try/catch on a multi-value exported tag converts the externref + ;; tuple element from externref to i31ref. + (try (result i32 (ref extern)) + (do + (tuple.make 2 + (i32.const 0) + (unreachable) + ) + ) + (catch $export-tag + (pop (tuple i32 (ref extern))) + ) + ) + ) + + ;; CHECK: (func $try-legacy-tuple-nested (type $6) + ;; CHECK-NEXT: (local $t (tuple i32 (ref (shared i31)))) + ;; CHECK-NEXT: (local $1 (tuple i32 (ref extern))) + ;; CHECK-NEXT: (local $2 (tuple i32 (ref extern))) + ;; CHECK-NEXT: (try + ;; CHECK-NEXT: (do + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (catch $export-tag + ;; CHECK-NEXT: (local.set $2 + ;; CHECK-NEXT: (pop (tuple i32 (ref extern))) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $t + ;; CHECK-NEXT: (block (type $0) (result i32 (ref (shared i31))) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (local.get $2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (tuple.make 2 + ;; CHECK-NEXT: (tuple.extract 2 0 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (ref.as_non_null + ;; CHECK-NEXT: (call $extern_to_index + ;; CHECK-NEXT: (tuple.extract 2 1 + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $try-legacy-tuple-nested + (local $t (tuple i32 (ref extern))) + ;; A multi-value pop inside an expression is wrapped in a block by visitPop + ;; and then hoisted to the start of the catch body by handleBlockNestedPop. + (try + (do + (nop) + ) + (catch $export-tag + (local.set $t + (pop (tuple i32 (ref extern))) + ) + ) + ) + ) +) + +;; CHECK: (func $extern_to_index (type $7) (param $0 externref) (result (ref null (shared i31))) +;; CHECK-NEXT: (local $1 i32) +;; CHECK-NEXT: (if (result (ref null (shared i31))) +;; CHECK-NEXT: (ref.is_null +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (ref.null (shared none)) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (else +;; CHECK-NEXT: (if (result (ref (shared i31))) +;; CHECK-NEXT: (i32.ge_s +;; CHECK-NEXT: (local.tee $1 +;; CHECK-NEXT: (table.grow $externs +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: (i32.const 1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (i32.const 0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (ref.i31_shared +;; CHECK-NEXT: (local.get $1) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (else +;; CHECK-NEXT: (unreachable) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) + +;; CHECK: (func $index_to_extern (type $8) (param $0 (ref null (shared i31))) (result externref) +;; CHECK-NEXT: (if (result externref) +;; CHECK-NEXT: (ref.is_null +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (then +;; CHECK-NEXT: (ref.null noextern) +;; CHECK-NEXT: ) +;; CHECK-NEXT: (else +;; CHECK-NEXT: (table.get $externs +;; CHECK-NEXT: (i31.get_u +;; CHECK-NEXT: (local.get $0) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) +;; CHECK-NEXT: ) From 2dc058769ef8ecdf4fd03614934fd3ad2cd3950a Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Fri, 25 Sep 2026 13:58:13 -0700 Subject: [PATCH 2/2] update names --- src/passes/MakeSharedObjects.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/passes/MakeSharedObjects.cpp b/src/passes/MakeSharedObjects.cpp index 90792b00720..044e44f5031 100644 --- a/src/passes/MakeSharedObjects.cpp +++ b/src/passes/MakeSharedObjects.cpp @@ -742,7 +742,7 @@ struct MakeSharedObjects Type getTagBoundaryParams(Type origParams) { std::vector params; for (Type t : origParams) { - params.push_back(externTable.isTableType(t) ? t : updatedType(t)); + params.push_back(externTable.canHold(t) ? t : updatedType(t)); } return Type(params); } @@ -761,7 +761,7 @@ struct MakeSharedObjects // Wrap pops with externref types in conversions from the caught externrefs // to i31ref table indices. void visitPop(Pop* curr) { - if (!externTable.hasTableType(curr->type)) { + if (!externTable.hasHoldable(curr->type)) { visitExpression(curr); return; } @@ -886,7 +886,7 @@ struct MakeSharedObjects std::vector tagsToUpdate; for (auto& tag : wasm->tags) { - if (externTable.hasTableType(tag->params())) { + if (externTable.hasHoldable(tag->params())) { tagsToUpdate.push_back({tag.get(), tag->type}); } }