From 0f73d76cef963a31b1cff5b0bee3b94438db30d7 Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 4 Sep 2025 09:42:23 -0700 Subject: [PATCH 1/2] clarify --- src/passes/RemoveUnusedModuleElements.cpp | 23 +++++- ...used-module-elements-refs-descriptors.wast | 70 +++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 6be12de05f4..6b538d54353 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -561,8 +561,27 @@ struct Analyzer { auto* new_ = curr->cast(); - // Use the descriptor right now, normally. (We only have special - // optimization for struct.new operands, below.) + // Use the descriptor right now, normally. We only have special + // optimization for struct.new operands, below, because this is not needed + // for descriptors: descriptors always have a struct "in the middle", so + // optimizing normal struct fields is enough. That is, imagine we have a + // struct with a descriptor: + // + // (struct.new $A + // (ref.func $c) + // (struct.new $A.desc + // (ref.func $d) + // ) + // ) + // + // The struct has a ref.func on it, and the descriptor does as well. Say we + // never field 0 from $A, then we can avoid marking $c as reached; this is + // the usual struct optimization we do, below. Now, say we never read the + // descriptor, then we also never read field 0 from $A.desc, that is, the + // usual struct optimization on the descriptor class is enough for us to + // avoid marking $d as reached. Put another way, a descriptor must be a + // struct; if it could be a function, then we'd need to optimize descriptors + // as we do normal fields. if (new_->desc) { use(new_->desc); } diff --git a/test/lit/passes/remove-unused-module-elements-refs-descriptors.wast b/test/lit/passes/remove-unused-module-elements-refs-descriptors.wast index c56d7e3f28c..90869e99cdf 100644 --- a/test/lit/passes/remove-unused-module-elements-refs-descriptors.wast +++ b/test/lit/passes/remove-unused-module-elements-refs-descriptors.wast @@ -127,3 +127,73 @@ (elem $no-trap-get anyref (item (struct.new $struct (global.get $desc)))) ) +(module + ;; CHECK: (type $void (func)) + (type $void (func)) + + (rec + ;; CHECK: (rec + ;; CHECK-NEXT: (type $vtable (sub (descriptor $vtable.desc (struct (field (ref $void)))))) + (type $vtable (sub (descriptor $vtable.desc (struct (field (ref $void)))))) + ;; CHECK: (type $vtable.desc (sub (describes $vtable (struct (field (ref $void)))))) + (type $vtable.desc (sub (describes $vtable (struct (field (ref $void)))))) + ) + + ;; CHECK: (global $vtable (ref $vtable) (struct.new $vtable + ;; CHECK-NEXT: (ref.func $a) + ;; CHECK-NEXT: (struct.new $vtable.desc + ;; CHECK-NEXT: (ref.func $b) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: )) + (global $vtable (ref $vtable) (struct.new $vtable + (ref.func $a) + (struct.new $vtable.desc + (ref.func $b) + ) + )) + + ;; CHECK: (export "export" (func $export)) + + ;; CHECK: (func $export (type $void) + ;; CHECK-NEXT: (call_ref $void + ;; CHECK-NEXT: (struct.get $vtable 0 + ;; CHECK-NEXT: (global.get $vtable) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $export (export "export") + ;; Read $a and call it. $b, in the descriptor, should not be callable. + (call_ref $void + (struct.get $vtable 0 + (global.get $vtable) + ) + ) + ) + + ;; CHECK: (func $a (type $void) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (i32.const 42) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $a (type $void) + ;; This is reached from above. + (drop (i32.const 42)) + ) + + ;; CHECK: (func $b (type $void) + ;; CHECK-NEXT: (unreachable) + ;; CHECK-NEXT: ) + (func $b (type $void) + ;; This is not reached: We never read the descriptor, so we never read field 0 + ;; in it, leaving this as dead (in closed world). That it itself seems to read + ;; the descriptor should not confuse us. + (call_ref $void + (struct.get $vtable.desc 0 + (ref.get_desc $vtable + (global.get $vtable) + ) + ) + ) + ) +) + From 3e7bb52c836e429d7b011c7730cf2f6fa465f0fc Mon Sep 17 00:00:00 2001 From: Alon Zakai Date: Thu, 4 Sep 2025 10:56:20 -0700 Subject: [PATCH 2/2] rephrase --- src/passes/RemoveUnusedModuleElements.cpp | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 6b538d54353..44c1848355b 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -563,9 +563,10 @@ struct Analyzer { // Use the descriptor right now, normally. We only have special // optimization for struct.new operands, below, because this is not needed - // for descriptors: descriptors always have a struct "in the middle", so - // optimizing normal struct fields is enough. That is, imagine we have a - // struct with a descriptor: + // for descriptors: a descriptor must be a struct, and our "lazy reading" + // optimization operates on it (if it could be a function, then we'd need to + // do more here). In other words, descriptor reads always have a struct "in + // the middle", that we can optimize, like here: // // (struct.new $A // (ref.func $c) @@ -574,14 +575,12 @@ struct Analyzer { // ) // ) // - // The struct has a ref.func on it, and the descriptor does as well. Say we - // never field 0 from $A, then we can avoid marking $c as reached; this is + // The struct has a ref.func on it, and the descriptor as well. Say we never + // read field 0 from $A, then we can avoid marking $c as reached; this is // the usual struct optimization we do, below. Now, say we never read the // descriptor, then we also never read field 0 from $A.desc, that is, the // usual struct optimization on the descriptor class is enough for us to - // avoid marking $d as reached. Put another way, a descriptor must be a - // struct; if it could be a function, then we'd need to optimize descriptors - // as we do normal fields. + // avoid marking $d as reached. if (new_->desc) { use(new_->desc); }