summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/passes/RemoveUnusedModuleElements.cpp231
-rw-r--r--src/wasm/wasm-validator.cpp7
-rw-r--r--test/lit/passes/remove-unused-module-elements_all-features.wast195
3 files changed, 298 insertions, 135 deletions
diff --git a/src/passes/RemoveUnusedModuleElements.cpp b/src/passes/RemoveUnusedModuleElements.cpp
index 8b9ed2a78..747b8806c 100644
--- a/src/passes/RemoveUnusedModuleElements.cpp
+++ b/src/passes/RemoveUnusedModuleElements.cpp
@@ -49,10 +49,15 @@
namespace wasm {
-// TODO: Add data segment, multiple memories (#5224)
-// TODO: use Effects below to determine if a memory is used
-// This pass does not have multi-memories support
-enum class ModuleElementKind { Function, Global, Tag, Table, ElementSegment };
+enum class ModuleElementKind {
+ Function,
+ Global,
+ Tag,
+ Memory,
+ Table,
+ DataSegment,
+ ElementSegment,
+};
// An element in the module that we track: a kind (function, global, etc.) + the
// name of the particular element.
@@ -70,7 +75,6 @@ struct ReferenceFinder : public PostWalker<ReferenceFinder> {
std::vector<HeapType> callRefTypes;
std::vector<Name> refFuncs;
std::vector<StructField> structFields;
- bool usesMemory = false;
// Add an item to the output data structures.
void note(ModuleElement element) { elements.push_back(element); }
@@ -132,21 +136,44 @@ struct ReferenceFinder : public PostWalker<ReferenceFinder> {
note({ModuleElementKind::Global, curr->name});
}
- void visitLoad(Load* curr) { usesMemory = true; }
- void visitStore(Store* curr) { usesMemory = true; }
- void visitAtomicCmpxchg(AtomicCmpxchg* curr) { usesMemory = true; }
- void visitAtomicRMW(AtomicRMW* curr) { usesMemory = true; }
- void visitAtomicWait(AtomicWait* curr) { usesMemory = true; }
- void visitAtomicNotify(AtomicNotify* curr) { usesMemory = true; }
- void visitMemoryInit(MemoryInit* curr) { usesMemory = true; }
+ void visitLoad(Load* curr) {
+ note({ModuleElementKind::Memory, curr->memory});
+ }
+ void visitStore(Store* curr) {
+ note({ModuleElementKind::Memory, curr->memory});
+ }
+ void visitAtomicCmpxchg(AtomicCmpxchg* curr) {
+ note({ModuleElementKind::Memory, curr->memory});
+ }
+ void visitAtomicRMW(AtomicRMW* curr) {
+ note({ModuleElementKind::Memory, curr->memory});
+ }
+ void visitAtomicWait(AtomicWait* curr) {
+ note({ModuleElementKind::Memory, curr->memory});
+ }
+ void visitAtomicNotify(AtomicNotify* curr) {
+ note({ModuleElementKind::Memory, curr->memory});
+ }
+ void visitMemoryInit(MemoryInit* curr) {
+ note({ModuleElementKind::DataSegment, curr->segment});
+ note({ModuleElementKind::Memory, curr->memory});
+ }
void visitDataDrop(DataDrop* curr) {
- // TODO: Replace this with a use of a data segment (#5224).
- usesMemory = true;
+ note({ModuleElementKind::DataSegment, curr->segment});
+ }
+ void visitMemoryCopy(MemoryCopy* curr) {
+ note({ModuleElementKind::Memory, curr->destMemory});
+ note({ModuleElementKind::Memory, curr->sourceMemory});
+ }
+ void visitMemoryFill(MemoryFill* curr) {
+ note({ModuleElementKind::Memory, curr->memory});
+ }
+ void visitMemorySize(MemorySize* curr) {
+ note({ModuleElementKind::Memory, curr->memory});
+ }
+ void visitMemoryGrow(MemoryGrow* curr) {
+ note({ModuleElementKind::Memory, curr->memory});
}
- void visitMemoryCopy(MemoryCopy* curr) { usesMemory = true; }
- void visitMemoryFill(MemoryFill* curr) { usesMemory = true; }
- void visitMemorySize(MemorySize* curr) { usesMemory = true; }
- void visitMemoryGrow(MemoryGrow* curr) { usesMemory = true; }
void visitRefFunc(RefFunc* curr) { noteRefFunc(curr->func); }
void visitTableGet(TableGet* curr) {
note({ModuleElementKind::Table, curr->table});
@@ -175,13 +202,13 @@ struct ReferenceFinder : public PostWalker<ReferenceFinder> {
}
void visitArrayNewSeg(ArrayNewSeg* curr) {
switch (curr->op) {
- case NewData:
- // TODO: Replace this with a use of the specific data segment (#5224).
- usesMemory = true;
+ case NewData: {
+ note({ModuleElementKind::DataSegment, curr->segment});
return;
case NewElem:
note({ModuleElementKind::ElementSegment, curr->segment});
return;
+ }
}
WASM_UNREACHABLE("unexpected op");
}
@@ -220,8 +247,6 @@ struct Analyzer {
// perform that analysis in readStructFields unreadStructFieldExprMap, below.
std::vector<Expression*> expressionQueue;
- bool usesMemory = false;
-
// The signatures that we have seen a call_ref for. When we see a RefFunc of a
// signature in here, we know it is used; otherwise it may only be referred
// to.
@@ -265,18 +290,6 @@ struct Analyzer {
use(element);
}
- // Globals used in memory/table init expressions are also roots.
- for (auto& segment : module->dataSegments) {
- if (!segment->isPassive) {
- use(segment->offset);
- }
- }
- for (auto& segment : module->elementSegments) {
- if (segment->table.is()) {
- use(segment->offset);
- }
- }
-
// Main loop on both the module and the expression queues.
while (processExpressions() || processModule()) {
}
@@ -310,9 +323,6 @@ struct Analyzer {
for (auto structField : finder.structFields) {
useStructField(structField);
}
- if (finder.usesMemory) {
- usesMemory = true;
- }
// Scan the children to continue our work.
scanChildren(curr);
@@ -425,24 +435,60 @@ struct Analyzer {
assert(used.count(curr));
auto& [kind, value] = curr;
- if (kind == ModuleElementKind::Function) {
- // if not an import, walk it
- auto* func = module->getFunction(value);
- if (!func->imported()) {
- use(func->body);
+ switch (kind) {
+ case ModuleElementKind::Function: {
+ // if not an import, walk it
+ auto* func = module->getFunction(value);
+ if (!func->imported()) {
+ use(func->body);
+ }
+ break;
}
- } else if (kind == ModuleElementKind::Global) {
- // if not imported, it has an init expression we can walk
- auto* global = module->getGlobal(value);
- if (!global->imported()) {
- use(global->init);
+ case ModuleElementKind::Global: {
+ // if not imported, it has an init expression we can walk
+ auto* global = module->getGlobal(value);
+ if (!global->imported()) {
+ use(global->init);
+ }
+ break;
+ }
+ case ModuleElementKind::Tag:
+ break;
+ case ModuleElementKind::Memory:
+ ModuleUtils::iterMemorySegments(
+ *module, value, [&](DataSegment* segment) {
+ if (!segment->data.empty()) {
+ use({ModuleElementKind::DataSegment, segment->name});
+ }
+ });
+ break;
+ case ModuleElementKind::Table:
+ ModuleUtils::iterTableSegments(
+ *module, value, [&](ElementSegment* segment) {
+ if (!segment->data.empty()) {
+ use({ModuleElementKind::ElementSegment, segment->name});
+ }
+ });
+ break;
+ case ModuleElementKind::DataSegment: {
+ auto* segment = module->getDataSegment(value);
+ if (segment->offset) {
+ use(segment->offset);
+ use({ModuleElementKind::Memory, segment->memory});
+ }
+ break;
}
- } else if (kind == ModuleElementKind::Table) {
- ModuleUtils::iterTableSegments(
- *module, value, [&](ElementSegment* segment) {
+ case ModuleElementKind::ElementSegment: {
+ auto* segment = module->getElementSegment(value);
+ if (segment->offset) {
use(segment->offset);
- use({ModuleElementKind::ElementSegment, segment->name});
- });
+ use({ModuleElementKind::Table, segment->table});
+ }
+ for (auto* expr : segment->data) {
+ use(expr);
+ }
+ break;
+ }
}
}
return worked;
@@ -456,6 +502,9 @@ struct Analyzer {
moduleQueue.emplace_back(element);
}
}
+ void use(ModuleElementKind kind, Name value) {
+ use(ModuleElement(kind, value));
+ }
void use(Expression* curr) {
// For expressions we do not need to check if they have already been seen:
@@ -580,13 +629,6 @@ struct Analyzer {
referenced.insert({ModuleElementKind::Function, func});
}
- if (finder.usesMemory) {
- // TODO: We could do better here, but leave that for the full refactor
- // here that will also add multimemory. Then this will be as simple
- // as supporting tables here (which are just more module elements).
- usesMemory = true;
- }
-
// Note: nothing to do with |callRefTypes| and |structFields|, which only
// involve types. This function only cares about references to module
// elements like functions, globals, and tables. (References to types are
@@ -624,15 +666,7 @@ struct RemoveUnusedModuleElements : public Pass {
roots.emplace_back(ModuleElementKind::Function, func->name);
});
}
- ModuleUtils::iterActiveElementSegments(
- *module, [&](ElementSegment* segment) {
- auto table = module->getTable(segment->table);
- if (table->imported() && !segment->data.empty()) {
- roots.emplace_back(ModuleElementKind::ElementSegment, segment->name);
- }
- });
// Exports are roots.
- bool exportsMemory = false;
for (auto& curr : module->exports) {
if (curr->kind == ExternalKind::Function) {
roots.emplace_back(ModuleElementKind::Function, curr->value);
@@ -642,20 +676,28 @@ struct RemoveUnusedModuleElements : public Pass {
roots.emplace_back(ModuleElementKind::Tag, curr->value);
} else if (curr->kind == ExternalKind::Table) {
roots.emplace_back(ModuleElementKind::Table, curr->value);
- ModuleUtils::iterTableSegments(
- *module, curr->value, [&](ElementSegment* segment) {
- roots.emplace_back(ModuleElementKind::ElementSegment,
- segment->name);
- });
} else if (curr->kind == ExternalKind::Memory) {
- exportsMemory = true;
+ roots.emplace_back(ModuleElementKind::Memory, curr->value);
}
}
- // Check for special imports, which are roots.
- bool importsMemory = false;
- if (!module->memories.empty() && module->memories[0]->imported()) {
- importsMemory = true;
- }
+
+ // Active segments that write to imported tables and memories are roots
+ // because those writes are externally observable even if the module does
+ // not otherwise use the tables or memories.
+ ModuleUtils::iterActiveDataSegments(*module, [&](DataSegment* segment) {
+ if (module->getMemory(segment->memory)->imported() &&
+ !segment->data.empty()) {
+ roots.emplace_back(ModuleElementKind::DataSegment, segment->name);
+ }
+ });
+ ModuleUtils::iterActiveElementSegments(
+ *module, [&](ElementSegment* segment) {
+ if (module->getTable(segment->table)->imported() &&
+ !segment->data.empty()) {
+ roots.emplace_back(ModuleElementKind::ElementSegment, segment->name);
+ }
+ });
+
// For now, all functions that can be called indirectly are marked as roots.
// TODO: Compute this based on which ElementSegments are actually used,
// and which functions have a call_indirect of the proper type.
@@ -701,35 +743,22 @@ struct RemoveUnusedModuleElements : public Pass {
module->removeTags([&](Tag* curr) {
return !needed({ModuleElementKind::Tag, curr->name});
});
- module->removeElementSegments([&](ElementSegment* curr) {
- return !needed({ModuleElementKind::ElementSegment, curr->name});
+ module->removeMemories([&](Memory* curr) {
+ return !needed(ModuleElement(ModuleElementKind::Memory, curr->name));
});
- // Since we've removed all empty element segments, here we mark all tables
- // that have a segment left.
- std::unordered_set<Name> nonemptyTables;
- ModuleUtils::iterActiveElementSegments(
- *module,
- [&](ElementSegment* segment) { nonemptyTables.insert(segment->table); });
module->removeTables([&](Table* curr) {
- return (nonemptyTables.count(curr->name) == 0 || !curr->imported()) &&
- !needed({ModuleElementKind::Table, curr->name});
+ return !needed(ModuleElement(ModuleElementKind::Table, curr->name));
+ });
+ module->removeDataSegments([&](DataSegment* curr) {
+ return !needed(ModuleElement(ModuleElementKind::DataSegment, curr->name));
+ });
+ module->removeElementSegments([&](ElementSegment* curr) {
+ return !needed({ModuleElementKind::ElementSegment, curr->name});
});
// TODO: After removing elements, we may be able to remove more things, and
// should continue to work. (For example, after removing a reference
// to a function from an element segment, we may be able to remove
// that function, etc.)
-
- // Handle the memory
- if (!exportsMemory && !analyzer.usesMemory) {
- if (!importsMemory) {
- // The memory is unobservable to the outside, we can remove the
- // contents.
- module->removeDataSegments([&](DataSegment* curr) { return true; });
- }
- if (module->dataSegments.empty() && !module->memories.empty()) {
- module->removeMemory(module->memories[0]->name);
- }
- }
}
};
diff --git a/src/wasm/wasm-validator.cpp b/src/wasm/wasm-validator.cpp
index d8dba953f..951481dac 100644
--- a/src/wasm/wasm-validator.cpp
+++ b/src/wasm/wasm-validator.cpp
@@ -1437,12 +1437,7 @@ void FunctionValidator::visitDataDrop(DataDrop* curr) {
"Bulk memory operations require bulk memory [--enable-bulk-memory]");
shouldBeEqualOrFirstIsUnreachable(
curr->type, Type(Type::none), curr, "data.drop must have type none");
- if (!shouldBeFalse(getModule()->memories.empty(),
- curr,
- "Memory operations require a memory")) {
- return;
- }
- shouldBeTrue(getModule()->getDataSegment(curr->segment),
+ shouldBeTrue(getModule()->getDataSegmentOrNull(curr->segment),
curr,
"data.drop segment should exist");
}
diff --git a/test/lit/passes/remove-unused-module-elements_all-features.wast b/test/lit/passes/remove-unused-module-elements_all-features.wast
index 00a9d8f14..d5c394e30 100644
--- a/test/lit/passes/remove-unused-module-elements_all-features.wast
+++ b/test/lit/passes/remove-unused-module-elements_all-features.wast
@@ -351,63 +351,89 @@
(memory.atomic.notify (i32.const 0) (i32.const 0))
)
)
-(module ;; atomic.fence does not use a memory, so should not keep the memory alive.
+(module ;; atomic.fence and data.drop do not use a memory, so should not keep the memory alive.
(memory $0 (shared 1 1))
+ (data "")
;; CHECK: (type $none_=>_none (func))
+ ;; CHECK: (data $0 "")
+
;; CHECK: (export "fake-user" (func $user))
(export "fake-user" $user)
;; CHECK: (func $user (type $none_=>_none)
;; CHECK-NEXT: (atomic.fence)
+ ;; CHECK-NEXT: (data.drop $0)
;; CHECK-NEXT: )
(func $user
(atomic.fence)
+ (data.drop 0)
)
)
(module ;; more use checks
- ;; CHECK: (type $none_=>_i32 (func (result i32)))
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (import "env" "mem" (memory $0 256))
+ (import "env" "mem" (memory $0 256))
+ ;; CHECK: (memory $1 23 256)
+ (memory $1 23 256)
+ (memory $unused 1 1)
- ;; CHECK: (memory $0 23 256)
- (memory $0 23 256)
;; CHECK: (export "user" (func $user))
(export "user" $user)
- ;; CHECK: (func $user (type $none_=>_i32) (result i32)
- ;; CHECK-NEXT: (memory.grow
- ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK: (func $user (type $none_=>_none)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (memory.grow $0
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (memory.grow $1
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
;; CHECK-NEXT: )
;; CHECK-NEXT: )
- (func $user (result i32)
- (memory.grow (i32.const 0))
+ (func $user
+ (drop (memory.grow $0 (i32.const 0)))
+ (drop (memory.grow $1 (i32.const 0)))
)
)
(module ;; more use checks
;; CHECK: (type $none_=>_i32 (func (result i32)))
- ;; CHECK: (import "env" "memory" (memory $0 256))
- (import "env" "memory" (memory $0 256))
+ ;; CHECK: (memory $0 23 256)
+ (memory $0 23 256)
;; CHECK: (export "user" (func $user))
(export "user" $user)
;; CHECK: (func $user (type $none_=>_i32) (result i32)
- ;; CHECK-NEXT: (memory.grow
- ;; CHECK-NEXT: (i32.const 0)
- ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: (memory.size)
;; CHECK-NEXT: )
(func $user (result i32)
- (memory.grow (i32.const 0))
+ (memory.size)
)
)
-(module ;; more use checks
- ;; CHECK: (type $none_=>_i32 (func (result i32)))
+(module ;; memory.copy should keep both memories alive
+ ;; CHECK: (type $none_=>_none (func))
- ;; CHECK: (memory $0 23 256)
- (memory $0 23 256)
+ ;; CHECK: (memory $0 1 1)
+ (memory $0 1 1)
+ ;; CHECK: (memory $1 1 1)
+ (memory $1 1 1)
+ (memory $unused 1 1)
;; CHECK: (export "user" (func $user))
(export "user" $user)
- ;; CHECK: (func $user (type $none_=>_i32) (result i32)
- ;; CHECK-NEXT: (memory.size)
+ ;; CHECK: (func $user (type $none_=>_none)
+ ;; CHECK-NEXT: (memory.copy $0 $1
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
;; CHECK-NEXT: )
- (func $user (result i32)
- (memory.size)
+ (func $user
+ (memory.copy $0 $1
+ (i32.const 0)
+ (i32.const 0)
+ (i32.const 0)
+ )
)
)
(module
@@ -554,15 +580,40 @@
)
)
)
-(module ;; the table is imported - we can't remove it
+(module
+ ;; We import two tables and have an active segment that writes to one of them.
+ ;; We must keep that table and the segment, but we can remove the other table.
;; CHECK: (type $0 (func (param f64) (result f64)))
(type $0 (func (param f64) (result f64)))
- ;; CHECK: (import "env" "table" (table $timport$0 6 6 funcref))
- (import "env" "table" (table 6 6 funcref))
- (elem (i32.const 0) $0)
- ;; CHECK: (elem $0 (i32.const 0) $0)
+ ;; CHECK: (import "env" "written" (table $written 6 6 funcref))
+ (import "env" "written" (table $written 6 6 funcref))
+
+ (import "env" "unwritten" (table $unwritten 6 6 funcref))
+
+ (table $defined-unused 6 6 funcref)
+
+ ;; CHECK: (table $defined-used 6 6 funcref)
+ (table $defined-used 6 6 funcref)
+
+ ;; CHECK: (elem $active1 (table $written) (i32.const 0) func $0)
+ (elem $active1 (table $written) (i32.const 0) $0)
+
+ ;; This empty active segment doesn't keep the unwritten table alive.
+ (elem $active2 (table $unwritten) (i32.const 0))
+
+ (elem $active3 (table $defined-unused) (i32.const 0) $0)
+
+ ;; CHECK: (elem $active4 (table $defined-used) (i32.const 0) func $0)
+ (elem $active4 (table $defined-used) (i32.const 0) $0)
+
+ (elem $active5 (table $defined-used) (i32.const 0))
;; CHECK: (func $0 (type $0) (param $var$0 f64) (result f64)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (table.get $defined-used
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
;; CHECK-NEXT: (if (result f64)
;; CHECK-NEXT: (f64.eq
;; CHECK-NEXT: (f64.const 1)
@@ -573,6 +624,11 @@
;; CHECK-NEXT: )
;; CHECK-NEXT: )
(func $0 (; 0 ;) (type $0) (param $var$0 f64) (result f64)
+ (drop
+ (table.get $defined-used
+ (i32.const 0)
+ )
+ )
(if (result f64)
(f64.eq
(f64.const 1)
@@ -583,3 +639,86 @@
)
)
)
+(module
+ ;; The same thing works for memories with active segments.
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (import "env" "written" (memory $written 1 1))
+ (import "env" "written" (memory $written 1 1))
+
+ (import "env" "unwritten" (memory $unwritten 1 1))
+
+ (memory $defined-unused 1 1)
+
+ ;; CHECK: (memory $defined-used 1 1)
+ (memory $defined-used 1 1)
+
+ ;; CHECK: (data $active1 (i32.const 0) "foobar")
+ (data $active1 (memory $written) (i32.const 0) "foobar")
+
+ (data $active2 (memory $unwritten) (i32.const 0) "")
+
+ (data $active3 (memory $defined-unused) (i32.const 0) "hello")
+
+ ;; CHECK: (data $active4 (memory $defined-used) (i32.const 0) "hello")
+ (data $active4 (memory $defined-used) (i32.const 0) "hello")
+
+ (data $active5 (memory $defined-used) (i32.const 0) "")
+
+ ;; CHECK: (export "user" (func $user))
+
+ ;; CHECK: (func $user (type $none_=>_none)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (i32.load $defined-used
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $user (export "user")
+ (drop
+ (i32.load $defined-used
+ (i32.const 0)
+ )
+ )
+ )
+)
+(module
+ ;; Nothing should break if the unused segments precede the used segments.
+ ;; CHECK: (type $none_=>_none (func))
+
+ ;; CHECK: (type $array (array funcref))
+ (type $array (array funcref))
+
+ (memory $mem 1 1)
+ (table $tab 1 1 funcref)
+
+ (data $unused "")
+ (elem $unused func)
+
+ ;; CHECK: (data $used "")
+ (data $used "")
+ ;; CHECK: (elem $used func)
+ (elem $used func)
+
+ ;; CHECK: (export "user" (func $user))
+
+ ;; CHECK: (func $user (type $none_=>_none)
+ ;; CHECK-NEXT: (data.drop $used)
+ ;; CHECK-NEXT: (drop
+ ;; CHECK-NEXT: (array.new_elem $array $used
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: (i32.const 0)
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ ;; CHECK-NEXT: )
+ (func $user (export "user")
+ (data.drop 1)
+ (drop
+ (array.new_elem $array 1
+ (i32.const 0)
+ (i32.const 0)
+ (i32.const 0)
+ )
+ )
+ )
+)