Add a MakeSharedObjects pass - #8953
Conversation
This pass makes all functions unshared and all structs and arrays shared. It avoids validation errors from storing unshared function references in shared structs and arrays by replacing all function references with i31 references holding indices into a function table. Indirect calls and casts are updated to extract their function reference from the table. This new pass will make it easier to convert unshared Wasm GC programs to run on the shared heap for testing purposes. It will also lower shared functions emitted by toolchains to allow them to run on prototypes that do not support shared functions.
|
Looks like there's a determinism bug with the type renaming when multiple types get merged. Will look into that tomorrow. |
| (exact == Exact && !heapType.isBasic() ? ExactMask : 0)) { | ||
| assert(!(heapType.getID() & | ||
| (TupleMask | NullMask | (heapType.isBasic() ? 0 : ExactMask)))); | ||
| assert(!heapType.isBasic() || exact == Inexact); |
There was a problem hiding this comment.
The when we map old function types to i31, the general type updating code tries to update the heap type but keep exactness. There would be other ways to solve this, but this seemed simple enough.
There was a problem hiding this comment.
I ended up reverting this and checking whether the new type is basic before trying to make it exact.
| if (auto super = type.getDeclaredSuperType()) { | ||
| setSubType(i, map(*super)); | ||
| if (auto mapped = map(*super); !mapped.isBasic()) { | ||
| setSubType(i, map(*super)); |
There was a problem hiding this comment.
| setSubType(i, map(*super)); | |
| setSubType(i, mapped); |
Or is there some reason not to?
There was a problem hiding this comment.
Oh yeah, that's an oversight.
| if (auto desc = type.getDescriptorType()) { | ||
| setDescriptor(i, map(*desc)); | ||
| if (auto mapped = map(*desc); !mapped.isBasic()) { | ||
| setDescriptor(i, map(*desc)); |
There was a problem hiding this comment.
Maybe add a comment? I assume this can happen if both of a descriptor-described pair are mapped to a basic type?
kripken
left a comment
There was a problem hiding this comment.
lgtm % comments and also I am only 25% through the tests
| // Multiple old type indices are being merged into the same new type. | ||
| // Deterministically keep the lesser index. | ||
| newIt->second = std::min(newIt->second, it->second); | ||
| } |
There was a problem hiding this comment.
Maybe split this out from this PR?
There was a problem hiding this comment.
Sure, I'll split it out once I've verified that the fix is correct by having the tests pass here.
|
|
||
| GlobalTypeRewriter rewriter(*getModule(), getPassOptions().worldMode); | ||
| rewriter.mapTypes(oldToNew); | ||
| rewriter.mapTypeNamesAndIndices(oldToNew); |
There was a problem hiding this comment.
We discussed having a generic helper for this type of rewriting - which could be useful in e.g. StringLowering as well. Is that feasible?
There was a problem hiding this comment.
We can definitely create a helper, but I'm not totally clear on where the boundaries of the shared logic should be. What do you have in mind, specifically?
There was a problem hiding this comment.
Basically, you are replacing function types with i31, and StringLowering wants to replace string types with extern. Both can use an API similar to the current TypeMapper, but that preserves rec group shapes (rather than the default TypeMapper logic which creates a new singleton rec group). So this new API just replaces types "in place", without reshaping rec groups.
There was a problem hiding this comment.
Do you mind if we do this refactoring as an immediate follow-up?
| map(t.getHeapType()), t.getNullability(), t.getExactness()); | ||
| auto mapped = map(t.getHeapType()); | ||
| auto null = t.getNullability(); | ||
| auto exact = mapped.isBasic() ? Inexact : t.getExactness(); |
There was a problem hiding this comment.
| auto exact = mapped.isBasic() ? Inexact : t.getExactness(); | |
| // Basic types cannot be exact. | |
| auto exact = mapped.isBasic() ? Inexact : t.getExactness(); |
| ;; TODO: Handle exnref, contref. | ||
| ) | ||
|
|
||
| (module |
There was a problem hiding this comment.
| (module | |
| ;; Test recursive function references. | |
| (module |
I think?
| ;; CHECK-NEXT: ) | ||
| ;; CHECK-NEXT: ) | ||
| (func $multiple-ref-func | ||
| ;; Mapping of functions to i31 values should be consistent. |
There was a problem hiding this comment.
Shouldn't a table be generated that maps these indices to functions?
There was a problem hiding this comment.
We only add the table if it was ever actually used. I could add a call or cast to this module to make sure the table gets generated, if you want.
There was a problem hiding this comment.
If later tests show the table then there is no need, but maybe add a comment.
| ) | ||
| ) | ||
|
|
||
| (module |
There was a problem hiding this comment.
| (module | |
| ;; Test function indexing in global locations | |
| (module |
I think?
| ;; CHECK: (elem $funcs (i32.const 0)) | ||
|
|
||
| ;; CHECK: (func $ref-test-func (type $1) (param $0 (ref null (shared i31))) (result i32) | ||
| ;; CHECK-NEXT: (ref.test (ref (shared i31)) |
There was a problem hiding this comment.
Wait, is this sound? This would also accept an i31 that wasn't a funcref in a past life.
There was a problem hiding this comment.
Yes, thanks to the magic of separate type hierarchies. It's impossible for an i31 to reach a cast on the function hierarchy in the original module, so all the i31s that reach these transformed casts must represent function references.
| (module | ||
| (type $sig (func (param i32) (result i32))) | ||
|
|
||
| ;; Test that types in if, loop, result |
|
|
||
| ;; Test that types in if, loop, result | ||
|
|
||
| ;; Test that function types in globals, tables, element segments, and tags are updated. |
There was a problem hiding this comment.
Kind of. There was a previous test checking that the instructions were updated, but this one is checking that the types are updated. The previous test covered that for the case of funcref, but this one covers it for the case of defined function types.
This pass makes all functions unshared and all structs and arrays shared. It avoids validation errors from storing unshared function references in shared structs and arrays by replacing all function references with i31 references holding indices into a function table. Indirect calls and casts are updated to extract their function reference from the table.
This new pass will make it easier to convert unshared Wasm GC programs to run on the shared heap for testing purposes. It will also lower shared functions emitted by toolchains to allow them to run on prototypes that do not support shared functions.