-
Notifications
You must be signed in to change notification settings - Fork 115
WIT: Import/Export inconsistencies #629
Description
This is from the point of view of using component composition for system design where dynamic linking / composing components is key.
Issue: Asymmetrical import/export resolution
Transitive import/export dependencies are resolved inconsistently for imports and exports:
- Imports dependencies are resolved as additional imports.
- Exports dependencies are resolved as additional imports.
This causes worlds with symmetrical definitions to be resolved asymmetrical.
While both worlds look like they would compose perfectly together, once resolved they don't and instead leave a gap behind.
Example:
/// Shared types
interface types {
type start = string;
type end = string;
record path {
start: start,
end: end
}
resource path-graph {
constructor();
push: func(path: path);
get-ends: func(start: start) -> list<end>;
get-starts: func(end: end) -> list<start>;
}
}
/// Validate provided graphs for correctness
interface validation {
use types.{start, end, path-graph};
validate: func(graph: path-graph) -> bool;
validate-path: func(start: start, end: end) -> bool;
}
/// Component that want to use validation
world service {
import validation;
}
/// Component that provides validation
world validator {
export validation;
}This would resolve into these worlds on the components:
/// Component that want to use validation
world service {
import types;
import validation;
}
/// Component that provides validation
world validator {
import types;
export validation;
}While the validation interface can be composed, the types interface cannot be composed because it is imported by both components.
This would require an explicit export or a third component or the host to provide these types.
The obvious solution would be to update the resolution to be symmetrical:
- Imports dependencies are resolved as additional imports.
- Exports dependencies are resolved as additional exports.
But this isn't a valuable solution either as it forces the exporter to implement the types even tho it has no use to do so.
Solution: Fix inconsistent differentiation between values and signatures (interfaces)
For the sake of less confusion I will call the classic interfaces/traits "signatures"
Wit types can be split into 2 categories.
These categories have different uses cases for wit itself.
Please correct me if I'm wrong
| Category | type | Compilation | Runtime |
|---|---|---|---|
| Values | string, u32, record, variant, ... |
✅ | ❌ |
| Signatures | resource, function |
✅ | ✅ |
For the runtime, Values don't really matter as the actual implementation is added automatically and doesn't need to be imported or exported.
For the runtime, Signatures matter as the actual implementation is not added automatically and needs to be either imported or implemented to be exported.
So, looking back at the example:
/// Shared types
interface types {
type start = string; // <--- import/export doesn't matter during runtime
type end = string; // <--- import/export doesn't matter during runtime
record path { // <--- import/export doesn't matter during runtime
start: start,
end: end
}
resource path-graph { // <--- ❗ import/export does matter during runtime
constructor();
push: func(path: path);
get-ends: func(start: start) -> list<end>;
get-starts: func(end: end) -> list<start>;
}
}This insight alone doesn't change anything as we also need to rethink the import/export behaviour.
- Values don't need import / export schemantic at all.
The same definitions can be used for both imports and exports.
It's same for generated code via bindgen (kind of relates to Split 'use' inside worlds into 'use import' and 'use export' #308). - Signatures require import / export schemantic.
Whenever a signature is taken into scope it needs to be defined if it's an export or import.
This requires changes to the way entries are referenced via the use directive.
useto reference a valueuse importto import a signature referenceuse exportto export a signature reference
That's the relation to Split 'use' inside worlds into 'use import' and 'use export' #308
Looking at the example again:
/// Validate provided graphs for correctness
interface validation {
use types.{start, end};
// use types.{path-graph};
use import types.{path-graph}; // <--- The validator only wants the signature to validate the graph and doesn't provide the implementation.
validate: func(graph: path-graph) -> bool;
validate-path: func(start: start, end: end) -> bool;
}
/// Component that want to use validation
world service { // <--- Has to export the path-graph
import validation;
}
/// Component that provides validation
world validator { // <--- Will import the path-graph
export validation;
}This would resolve into these worlds on the components:
/// Component that want to use validation
world service {
use types.{start, end, path};
use export types.{path-graph};
import validation;
}
/// Component that provides validation
world validator {
use types.{start, end, path};
use import types.{path-graph};
export validation;
}Summary
With these changes interfaces are more expressive in what they require / provide and are resolved symmetrical.
I just want to share my point of view from using wit and it's interfaces as contracts in general where a symmetrical definition is important.
I'm open for any discussion :)
Benefits
- Symmetrical worlds
- Symmetrical import/export resolution
- More expressive interfaces
- Easier component composition
- No export/import type mismatches in bindgen for value types
- Split 'use' inside worlds into 'use import' and 'use export' #308: Same behaviour for
usein worlds and interfaces
Changes to WIT
- Add
exportkeyword tousedirective (world and interface) - Add
importkeyword tousedirective (world and interface) - Require
export/importonusefor resources and functions
Changes to bindgen (optional)
- Remove different export / import handling for wit types of the Values category (
string,u32,record,variant...)
Handling breaking changes
Make use syntactical suguar for use import on resources and functions.
use types;translates to
use types.{start, end, path};
use import types.{path-graph}; // <--- Same behaviourWorld resolution
Export Side
// Before
import types;
// After
use types.{start, end, path};
use import types.{path-graph}; // <--- Same behaviourImport Side
// Before
import types;
// After
use types.{start, end, path};
use export types.{path-graph}; // <--- Breaking changeCould be feature gated via adding it by default as import aswell and defing the export to reference the import
use import types.{path-graph};
use export types.{path-graph} with { types = import types };Resulting in
// Before
import types;
// After
use types.{start, end, path};
use import types.{path-graph}; // <--- Same behaviour
use export types.{path-graph} with { types = import types };Example uses in WASI
wasi:http@0.3
interface types {
resource request {}
resource response {}
variant error-code {}
}
interface handler {
use types.{error-code};
use import types.{request, response}; // <--- As a request handler i only need the signature of request and response
handle: async func(request: request) -> result<response, error-code>;
}wasi:keyvalue@0.2
interface types {
resource bucket {}
variant error {}
}
interface store {
use types.{error};
use export types.{bucket}; // <--- As a store i need to provide the implementation of the bucket signature
open: func(identifier: string) -> result<bucket, error>;
}