diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp index 6be12de05f4..44c1848355b 100644 --- a/src/passes/RemoveUnusedModuleElements.cpp +++ b/src/passes/RemoveUnusedModuleElements.cpp @@ -561,8 +561,26 @@ 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: 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) + // (struct.new $A.desc + // (ref.func $d) + // ) + // ) + // + // 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. 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) + ) + ) + ) + ) +) +