Implement type imports and exports#7330
Conversation
Implementation of the Type Imports and Exports proposal, which allows
Wasm module to export and import types:
(type $File (export "File") (struct ...))
(import "file" "File" (type $File (sub any)))
wasm-merge can be used to combine several modules using this proposal,
connecting type imports to corresponding type exports. To produce an
output compatible with existing tools, an option --strip-type-exports
can be use to omit any type export from the output.
From an implementation point of view, for imports, a new kind of heap
type is used:
(import "module" "base" (sub absheaptype))
Including the module and base names in the type definition works well
with type canonicalisation.
For exports, we separate type exports from other exports, since they
don't have an internal name but a heap type:
class TypeExport {
Name name;
HeapType heaptype; // exported type
};
This is somewhat error-prone. Typically, to check for repeated exports,
we need to check two tables. But the alternatives do not seem much
better. If we put all export together, then we have to make sure that we
are never trying to access the internal name of a type import.
f57a4d1 to
9e527ef
Compare
56a545e to
7e56d35
Compare
tlively
left a comment
There was a problem hiding this comment.
Thanks for the PR, this looks fantastic. In addition to the comments below, it would be good to add unit tests for type canonicalization and subtyping relationships in test/gtest/type-builder.cpp. It would also be good to add tests for MinifyImportsAndExports, StripTypeExports, and TypeMerging passes with type imports to check that they do the right thing, as well as some tests for errors such as trying to declare a subtype of an imported type.
| if (!ctx.in.takeSExprStart("sub"sv)) { | ||
| return ctx.makeAnyType(Unshared); | ||
| } |
There was a problem hiding this comment.
It doesn't look like the (sub absheaptype) clause is optional in the proposal overview. Is this shorthand documented elsewhere?
There was a problem hiding this comment.
This is mentioned in the Proposal Summary
The text format allows to omit the constraint, in which case it defaults to (sub any).
src/parser/parsers.h
Outdated
| if (inRecGroup) { | ||
| return ctx.in.err("type import not allowed in recursive group"); | ||
| } |
There was a problem hiding this comment.
Unless this is part of the proposed text format extension, what do you think about deferring this check to TypeBuilder::build? Since all type definitions that are not in rec groups desugar to singleton rec groups, I think it would be more natural to allow type imports only in singleton rec groups, but also allow the rec group to be explicit in the text.
There was a problem hiding this comment.
The proposal currently does not say anything about the abbreviation (type $t (import "foo" "bar").
I'll make the change.
src/parser/contexts.h
Outdated
| std::vector<DefPos> dataDefs; | ||
| std::vector<DefPos> tagDefs; | ||
|
|
||
| // Type imports: name, positions of type and import names. |
There was a problem hiding this comment.
| // Type imports: name, positions of type and import names. | |
| // Type imports: name, export names, import names, and type index. |
src/passes/TypeMerging.cpp
Outdated
| case HeapTypeKind::Cont: | ||
| WASM_UNREACHABLE("TODO: cont"); | ||
| case HeapTypeKind::Import: | ||
| return false; |
There was a problem hiding this comment.
Would it make sense to merge duplicate imports here?
There was a problem hiding this comment.
Duplicate imports are merged by the type canonicalization.
src/wasm/wasm-type-shape.cpp
Outdated
| hash_combine(digest, hash(type.getContinuation())); | ||
| return digest; | ||
| case HeapTypeKind::Import: | ||
| assert(type.isContinuation()); |
src/wasm/wasm-type-shape.cpp
Outdated
|
|
||
| size_t hash(Continuation cont) { return hash(cont.type); } | ||
|
|
||
| size_t hash(Import import) { return hash(import.bound); } |
There was a problem hiding this comment.
Same, this should include the names.
| return typer.isSubType(sub.struct_, super.struct_); | ||
| case HeapTypeKind::Array: | ||
| return typer.isSubType(sub.array, super.array); | ||
| case HeapTypeKind::Import: |
There was a problem hiding this comment.
Perhaps we should return false here. It seems possible for someone to accidentally set a supertype on an import entry in a TypeBuilder. We should also check whether the supertype is an import here.
src/wasm/wasm-type.cpp
Outdated
| if (!info.import.bound.isShared()) { | ||
| return TypeBuilder::ErrorReason::InvalidBoundType; | ||
| } | ||
| break; |
There was a problem hiding this comment.
It might make sense to just inherit sharedness from the bound rather than requiring it to be set separately and match. What do you think?
There was a problem hiding this comment.
Indeed, we should do that.
test/lit/merge/type-imports.wat
Outdated
| (import "second" "g" (func $g (param (ref $t2)))) | ||
|
|
||
| ;; Check that the function parameters are updated | ||
| (func (export "f") (param (ref $t1) (ref $t2) (ref $t3) (ref $t4)) |
There was a problem hiding this comment.
If you give these functions $names, then the test output update script should be able to match them up with functions in the output and put them next to each other.
|
Please re-request review from me when you're ready :) |
Implementation of the Type Imports and Exports proposal, which allows Wasm module to export and import types:
wasm-mergecan combine several modules using this proposal, connecting type imports to corresponding type exports. To produce an output compatible with existing tools, an option--strip-type-exportscan be used to omit any type export from the output.From an implementation point of view, for imports, a new kind of heap type is used:
Including the module and base names in the type definition works well with type canonicalization.
For exports, we separate type exports from other exports, since they don't have an internal name but a heap type:
This is somewhat error-prone: to check for repeated exports, we need to check two tables. But the alternatives do not seem much better. If we put all exports together, then we have to make sure that we are never trying to access the internal name of a type import.