Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SignaturePruning: Properly handle public types #6630

Merged
merged 11 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions src/ir/type-updating.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ namespace wasm {

GlobalTypeRewriter::GlobalTypeRewriter(Module& wasm) : wasm(wasm) {}

void GlobalTypeRewriter::update() { mapTypes(rebuildTypes()); }
void GlobalTypeRewriter::update(
const std::vector<HeapType>& additionalPrivateTypes) {
mapTypes(rebuildTypes(additionalPrivateTypes));
}

GlobalTypeRewriter::TypeMap GlobalTypeRewriter::rebuildTypes(
const std::vector<HeapType>& additionalPrivateTypes) {
Expand All @@ -40,8 +43,17 @@ GlobalTypeRewriter::TypeMap GlobalTypeRewriter::rebuildTypes(
Index i = 0;
auto privateTypes = ModuleUtils::getPrivateHeapTypes(wasm);

for (auto t : additionalPrivateTypes) {
privateTypes.push_back(t);
if (!additionalPrivateTypes.empty()) {
// Only add additional private types that are not already in the list.
std::unordered_set<HeapType> privateTypesSet(privateTypes.begin(),
privateTypes.end());

for (auto t : additionalPrivateTypes) {
if (!privateTypesSet.count(t)) {
privateTypes.push_back(t);
privateTypesSet.insert(t);
}
}
}

// Topological sort to have supertypes first, but we have to account for the
Expand Down
24 changes: 16 additions & 8 deletions src/ir/type-updating.h
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,12 @@ class GlobalTypeRewriter {
// Main entry point. This performs the entire process of creating new heap
// types and calling the hooks below, then applies the new types throughout
// the module.
void update();
//
// This only operates on private types (so as not to modify the module's
// external ABI). It takes as a parameter a list of public types to consider
// private, which allows more flexibility (e.g. in closed world if a pass
// knows a type is safe to modify despite being public, it can add it).
void update(const std::vector<HeapType>& additionalPrivateTypes = {});

using TypeMap = std::unordered_map<HeapType, HeapType>;

Expand Down Expand Up @@ -398,7 +403,10 @@ class GlobalTypeRewriter {

// Helper for the repeating pattern of just updating Signature types using a
// map of old heap type => new Signature.
static void updateSignatures(const SignatureUpdates& updates, Module& wasm) {
static void
updateSignatures(const SignatureUpdates& updates,
Module& wasm,
const std::vector<HeapType>& additionalPrivateTypes = {}) {
if (updates.empty()) {
return;
}
Expand All @@ -407,9 +415,11 @@ class GlobalTypeRewriter {
const SignatureUpdates& updates;

public:
SignatureRewriter(Module& wasm, const SignatureUpdates& updates)
SignatureRewriter(Module& wasm,
const SignatureUpdates& updates,
const std::vector<HeapType>& additionalPrivateTypes)
: GlobalTypeRewriter(wasm), updates(updates) {
update();
update(additionalPrivateTypes);
}

void modifySignature(HeapType oldSignatureType, Signature& sig) override {
Expand All @@ -419,17 +429,15 @@ class GlobalTypeRewriter {
sig.results = getTempType(iter->second.results);
}
}
} rewriter(wasm, updates);
} rewriter(wasm, updates, additionalPrivateTypes);
}

protected:
// Builds new types after updating their contents using the hooks below and
// returns a map from the old types to the modified types. Used internally in
// update().
//
// This only operates on private types (so as not to modify the module's
// external ABI). It takes as a parameter a list of public types to consider
// private, which allows more flexibility.
// See above regarding private types.
TypeMap
rebuildTypes(const std::vector<HeapType>& additionalPrivateTypes = {});

Expand Down
22 changes: 15 additions & 7 deletions src/passes/SignaturePruning.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,6 @@ namespace wasm {
namespace {

struct SignaturePruning : public Pass {
// Maps each heap type to the possible pruned heap type. We will fill this
// during analysis and then use it while doing an update of the types. If a
// type has no improvement that we can find, it will not appear in this map.
std::unordered_map<HeapType, Signature> newSignatures;

void run(Module* module) override {
if (!module->features.hasGC()) {
return;
Expand Down Expand Up @@ -182,6 +177,11 @@ struct SignaturePruning : public Pass {
// types with subtyping relations at once.
SubTypes subTypes(*module);

// Maps each heap type to the possible pruned heap type. We will fill this
kripken marked this conversation as resolved.
Show resolved Hide resolved
// during analysis and then use it while doing an update of the types. If a
// type has no improvement that we can find, it will not appear in this map.
std::unordered_map<HeapType, Signature> newSignatures;

// Find parameters to prune.
//
// TODO: The order matters here, and more than one cycle can find more work
Expand Down Expand Up @@ -291,8 +291,16 @@ struct SignaturePruning : public Pass {
}
}

// Rewrite the types.
GlobalTypeRewriter::updateSignatures(newSignatures, *module);
// Rewrite the types. We pass in all the types we intend to modify as being
// "additional private types" because we have proven above that they are
// safe to modify, even if they are technically public (e.g. they may be in
// a singleton big rec group that is public because one member is public).
std::vector<HeapType> additionalPrivateTypes;
for (auto& [type, sig] : newSignatures) {
additionalPrivateTypes.push_back(type);
}
GlobalTypeRewriter::updateSignatures(
newSignatures, *module, additionalPrivateTypes);

if (callTargetsToLocalize.empty()) {
return false;
Expand Down
49 changes: 49 additions & 0 deletions test/lit/passes/signature-pruning.wast
Original file line number Diff line number Diff line change
Expand Up @@ -1151,3 +1151,52 @@
)
)
)

;; $exported is exported. The entire rec group becomes exported as well, which
;; causes $unused-param's type to be public, which means we cannot normally
;; modify it. However, in closed world we allow such changes, and we can remove
;; the unused param there. What happens is that we keep the original public rec
;; group as-is, and add a new rec group for private types, put the pruned type
;; there, and use that pruned type on $unused-param.
(module
(rec
;; CHECK: (rec
;; CHECK-NEXT: (type $much (func))

;; CHECK: (type $1 (func))

;; CHECK: (rec
;; CHECK-NEXT: (type $none (func))
(type $none (func))
;; CHECK: (type $much (func (param i32)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like the type name is repeated in the output, which we shouldn't allow.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a quirk we have atm. It's actually useful in cases where the old and new types remain in use (like here), but we should probably ensure a new unique name. I can look into that separately.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect us to have problems with text round trip fuzzing before this is fixed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see issues in fuzzing so far, but this is a pre-existing issue, so that isn't surprising. It must take specific fuzzer luck to get it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Experimenting with a patch that deduplicates the names, the problem is that it will generate a large diff on existing tests (before this PR; unrelated) regardless of whether we deduplicate the old or the new name. We seem to just have enough cases that both remain in use. The diff one way is 66 K and the other is 276 K, so at least one is clearly less annoying, but it will still be a quite large change unfortunately.

(type $much (func (param i32)))
)

;; CHECK: (export "exported" (func $exported))

;; CHECK: (func $exported (type $none)
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: )
(func $exported (export "exported") (type $none)
)

;; CHECK: (func $unused-param (type $much)
;; CHECK-NEXT: (local $0 i32)
;; CHECK-NEXT: (local.set $0
;; CHECK-NEXT: (i32.const 0)
;; CHECK-NEXT: )
;; CHECK-NEXT: (nop)
;; CHECK-NEXT: )
(func $unused-param (type $much) (param $param i32)
)

;; CHECK: (func $caller (type $1)
;; CHECK-NEXT: (call $unused-param)
;; CHECK-NEXT: )
(func $caller
(call $unused-param
(i32.const 0)
)
)
)

Loading