From d7f1522085222f55c4444fba5e5ca47b47858ee9 Mon Sep 17 00:00:00 2001 From: Yi LIU Date: Mon, 16 Feb 2026 23:47:05 +0800 Subject: [PATCH] Handle call_ref in asyncify pass The asyncify pass handles Call and CallIndirect but not CallRef. This means call_ref instructions are not recognized as calls in ModuleAnalyzer, canChangeState(), or doesCall(), so they are never properly instrumented with asyncify's unwind/rewind support. Add visitCallRef handlers alongside the existing visitCallIndirect handlers in both walkers, and include CallRef in the doesCall() check. CallRef is treated the same as CallIndirect since both are indirect calls where the target may change state. --- src/passes/Asyncify.cpp | 17 +++- test/lit/passes/asyncify-call-ref.wast | 113 +++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 test/lit/passes/asyncify-call-ref.wast diff --git a/src/passes/Asyncify.cpp b/src/passes/Asyncify.cpp index 48f9cc59419..036044176af 100644 --- a/src/passes/Asyncify.cpp +++ b/src/passes/Asyncify.cpp @@ -665,6 +665,14 @@ class ModuleAnalyzer { } // TODO optimize the other case, at least by type } + void visitCallRef(CallRef* curr) { + if (curr->isReturn) { + Fatal() << "tail calls not yet supported in asyncify"; + } + if (canIndirectChangeState) { + info.canChangeState = true; + } + } }; Walker walker(info, module, canIndirectChangeState); walker.walk(func->body); @@ -834,6 +842,7 @@ class ModuleAnalyzer { } } void visitCallIndirect(CallIndirect* curr) { hasIndirectCall = true; } + void visitCallRef(CallRef* curr) { hasIndirectCall = true; } Module* module; ModuleAnalyzer* analyzer; Map* map; @@ -863,16 +872,16 @@ class ModuleAnalyzer { bool verbose; }; -// Checks if something performs a call: either a direct or indirect call, -// and perhaps it is dropped or assigned to a local. This captures all the -// cases of a call in flat IR. +// Checks if something performs a call: a direct call, indirect call, or +// call_ref, and perhaps it is dropped or assigned to a local. This captures +// all the cases of a call in flat IR. static bool doesCall(Expression* curr) { if (auto* set = curr->dynCast()) { curr = set->value; } else if (auto* drop = curr->dynCast()) { curr = drop->value; } - return curr->is() || curr->is(); + return curr->is() || curr->is() || curr->is(); } class AsyncifyBuilder : public Builder { diff --git a/test/lit/passes/asyncify-call-ref.wast b/test/lit/passes/asyncify-call-ref.wast new file mode 100644 index 00000000000..71b8d209525 --- /dev/null +++ b/test/lit/passes/asyncify-call-ref.wast @@ -0,0 +1,113 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. + +;; Test that asyncify properly instruments call_ref, just like call_indirect. + +;; RUN: wasm-opt %s --asyncify --pass-arg=asyncify-addlist@caller -all -S -o - | filecheck %s + +(module + ;; CHECK: (type $func-type (func)) + (type $func-type (func)) + + ;; CHECK: (import "env" "import" (func $import (type $func-type))) + (import "env" "import" (func $import)) + + (memory 1 2) + + ;; CHECK: (func $caller (type $func-type) + ;; CHECK-NEXT: (local $0 i32) + ;; CHECK-NEXT: (local $1 i32) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $0 + ;; CHECK-NEXT: (block $__asyncify_unwind (result i32) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 2) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const -4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.set $1 + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.or + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (local.get $1) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (call_ref $func-type + ;; CHECK-NEXT: (ref.func $import) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (if + ;; CHECK-NEXT: (i32.eq + ;; CHECK-NEXT: (global.get $__asyncify_state) + ;; CHECK-NEXT: (i32.const 1) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (then + ;; CHECK-NEXT: (br $__asyncify_unwind + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (return) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (block + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (local.get $0) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.store + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: (i32.add + ;; CHECK-NEXT: (i32.load + ;; CHECK-NEXT: (global.get $__asyncify_data) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 4) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (nop) + ;; CHECK-NEXT: ) + (func $caller + (call_ref $func-type + (ref.func $import) + ) + ) +)