Skip to content

Commit 0badf73

Browse files
authored
rust: Reimplement how stream/future payloads work (#1528)
* rust: Reimplement how stream/future payloads work Previously stream/future payload were generated by collecting the set of types used in `future` and `stream` types in a WIT, rendering them to a string, deduplicating based on this string representation, and then generating various impls-with-vtables. This stringification strategy unfortunately falls down in a few situations such as: * Type aliases in WIT render as two names in Rust, but they're using the same Rust type. * Types with the same definition, but in multiple modules, will have two different paths in Rust but alias the same type. * Primitives may be used directly in streams/futures but then additionally used as a WIT type alias. In all of these situations it's effectively exposing how Rust requires at most one-impl-per-type-definition but the stringification/deduping was just a proxy for implementing this restriction and not a precise calculation. Using the work from bytecodealliance/wasm-tools#2447 as well as #1468 it's possible to do all of this without stringifying. Specifically #1468, transitively enabled by bytecodealliance/wasm-tools#2447, enables building a set of equal types that the Rust generator knows will all alias the same type definition. Using this it's possible to translate a payload to its "canonical payload" representation ID-wise and perform hashing/deduplication based on that. This in turn solves all of the issues above as well as previous issues such as #1432 and #1433 without requiring the workaround in #1482. The end result is that all of these various bugs should be fixed and the Rust generator should be much more reliable about when exactly a trait impl is emitted vs not. Closes #1523 Closes #1524 * Try fixing CI * Fix typo * Fix rust CI
1 parent cb8b0ac commit 0badf73

File tree

11 files changed

+201
-75
lines changed

11 files changed

+201
-75
lines changed

crates/core/src/types.rs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,27 @@ impl Types {
9494
}
9595
}
9696
}
97-
pub fn collect_equal_types(&mut self, resolve: &Resolve) {
97+
98+
/// Populates the return value of [`Types::get_representative_type`] with
99+
/// the `resolve` passed in.
100+
///
101+
/// The `may_alias_another_type` closure is used to determine whether the
102+
/// language's definition of the provided `TypeId` might possibly alias
103+
/// some other type in a language. This is a language-specific deduction
104+
/// which can also be affected by options to a binding generator. If a type
105+
/// can't be aliased by anything else then it can't be considered equal to
106+
/// anything else. Note that in this situations other types may still
107+
/// be equal to it, such as aliased types at the WIT level (e.g. `type foo
108+
/// = some-record`).
109+
pub fn collect_equal_types(
110+
&mut self,
111+
resolve: &Resolve,
112+
may_alias_another_type: &dyn Fn(TypeId) -> bool,
113+
) {
98114
for (i, (ty, _)) in resolve.types.iter().enumerate() {
115+
if !may_alias_another_type(ty) {
116+
continue;
117+
}
99118
// TODO: we could define a hash function for TypeDefKind to prevent the inner loop.
100119
for (earlier, _) in resolve.types.iter().take(i) {
101120
if self.equal_types.find(ty) == self.equal_types.find(earlier) {

crates/rust/src/bindgen.rs

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use crate::{
2-
ConstructorReturnType, Identifier, InterfaceGenerator, RustFlagsRepr,
3-
classify_constructor_return_type, int_repr, to_rust_ident,
2+
ConstructorReturnType, InterfaceGenerator, RustFlagsRepr, classify_constructor_return_type,
3+
int_repr, to_rust_ident,
44
};
55
use heck::*;
66
use std::fmt::Write as _;
@@ -428,28 +428,13 @@ impl Bindgen for FunctionBindgen<'_, '_> {
428428
let async_support = self.r#gen.r#gen.async_support_path();
429429
let op = &operands[0];
430430
let name = match payload {
431-
Some(Type::Id(type_id)) => {
432-
let dealiased_id = dealias(resolve, *type_id);
433-
self.r#gen.type_name_owned_with_id(
434-
&Type::Id(dealiased_id),
435-
Identifier::StreamOrFuturePayload,
436-
)
437-
}
438-
Some(ty) => self
439-
.r#gen
440-
.type_name_owned_with_id(ty, Identifier::StreamOrFuturePayload),
441-
None => "()".into(),
431+
Some(ty) => self.r#gen.type_name_owned(ty),
432+
None => "()".to_string(),
442433
};
443-
let ordinal = self
444-
.r#gen
445-
.r#gen
446-
.future_payloads
447-
.get_index_of(&name)
448-
.unwrap();
449434
let path = self.r#gen.path_to_root();
450435
results.push(format!(
451436
"{async_support}::FutureReader::new\
452-
({op} as u32, &{path}wit_future::vtable{ordinal}::VTABLE)"
437+
({op} as u32, &<{name} as {path}wit_future::FuturePayload>::VTABLE)"
453438
))
454439
}
455440

@@ -462,28 +447,13 @@ impl Bindgen for FunctionBindgen<'_, '_> {
462447
let async_support = self.r#gen.r#gen.async_support_path();
463448
let op = &operands[0];
464449
let name = match payload {
465-
Some(Type::Id(type_id)) => {
466-
let dealiased_id = dealias(resolve, *type_id);
467-
self.r#gen.type_name_owned_with_id(
468-
&Type::Id(dealiased_id),
469-
Identifier::StreamOrFuturePayload,
470-
)
471-
}
472-
Some(ty) => self
473-
.r#gen
474-
.type_name_owned_with_id(ty, Identifier::StreamOrFuturePayload),
475-
None => "()".into(),
450+
Some(ty) => self.r#gen.type_name_owned(ty),
451+
None => "()".to_string(),
476452
};
477-
let ordinal = self
478-
.r#gen
479-
.r#gen
480-
.stream_payloads
481-
.get_index_of(&name)
482-
.unwrap();
483453
let path = self.r#gen.path_to_root();
484454
results.push(format!(
485455
"{async_support}::StreamReader::new\
486-
({op} as u32, &{path}wit_stream::vtable{ordinal}::VTABLE)"
456+
({op} as u32, &<{name} as {path}wit_stream::StreamPayload>::VTABLE)"
487457
))
488458
}
489459

@@ -1281,7 +1251,7 @@ impl Bindgen for FunctionBindgen<'_, '_> {
12811251
self.push_str(&format!(
12821252
" let array{tmp}: [_; {size}] = core::array::from_fn(|{index_var}| {{
12831253
let base = {base}.add({index_var} * {elemsize});
1284-
{body}
1254+
{body}
12851255
}});"
12861256
));
12871257
let result = format!("array{tmp}");

crates/rust/src/interface.rs

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -523,11 +523,30 @@ macro_rules! {macro_name} {{
523523
func_name: &str,
524524
payload_type: Option<&Type>,
525525
) {
526-
let name = match payload_type {
527-
Some(Type::Id(type_id)) => {
528-
let dealiased_id = dealias(self.resolve, *type_id);
529-
self.type_name_owned(&Type::Id(dealiased_id))
526+
let payload_type = match payload_type {
527+
// Rust requires one-impl-per-type, so any `id` here is transformed
528+
// into its canonical representation using
529+
// `get_representative_type`. This ensures that type aliases, uses,
530+
// etc, all get canonicalized to the exact same ID regardless of
531+
// type structure.
532+
//
533+
// Note that `get_representative_type` maps ids-to-ids which is 95%
534+
// of what we want, but this additionally goes one layer further to
535+
// see if the final id is actually itself a typedef, which would
536+
// always be to a primitive, and then uses the primitive type
537+
// instead of the typedef to canonicalize with other streams/futures
538+
// using the primitive type.
539+
Some(Type::Id(id)) => {
540+
let id = self.r#gen.types.get_representative_type(*id);
541+
match self.resolve.types[id].kind {
542+
TypeDefKind::Type(t) => Some(t),
543+
_ => Some(Type::Id(id)),
544+
}
530545
}
546+
other => other.copied(),
547+
};
548+
let payload_type = payload_type.as_ref();
549+
let name = match payload_type {
531550
Some(payload_type) => self.type_name_owned(payload_type),
532551
None => "()".into(),
533552
};
@@ -536,7 +555,7 @@ macro_rules! {macro_name} {{
536555
PayloadFor::Stream => &mut self.r#gen.stream_payloads,
537556
};
538557

539-
if map.contains_key(&name) {
558+
if map.contains_key(&payload_type.copied()) {
540559
return;
541560
}
542561
let ordinal = map.len();
@@ -685,7 +704,7 @@ pub mod vtable{ordinal} {{
685704
PayloadFor::Future => &mut self.r#gen.future_payloads,
686705
PayloadFor::Stream => &mut self.r#gen.stream_payloads,
687706
};
688-
map.insert(name, code);
707+
map.insert(payload_type.copied(), code);
689708
}
690709

691710
fn generate_guest_import(&mut self, func: &Function, interface: Option<&WorldKey>) {
@@ -1713,14 +1732,7 @@ unsafe fn call_import(&mut self, _params: Self::ParamsLower, _results: *mut u8)
17131732
}
17141733
}
17151734

1716-
pub(crate) fn type_name_owned_with_id(&mut self, ty: &Type, id: Identifier<'i>) -> String {
1717-
let old_identifier = mem::replace(&mut self.identifier, id);
1718-
let name = self.type_name_owned(ty);
1719-
self.identifier = old_identifier;
1720-
name
1721-
}
1722-
1723-
fn type_name_owned(&mut self, ty: &Type) -> String {
1735+
pub fn type_name_owned(&mut self, ty: &Type) -> String {
17241736
self.type_name(
17251737
ty,
17261738
TypeMode {
@@ -2579,7 +2591,7 @@ impl<'a> wit_bindgen_core::InterfaceGenerator<'a> for InterfaceGenerator<'a> {
25792591

25802592
fn define_type(&mut self, name: &str, id: TypeId) {
25812593
let equal = self.r#gen.types.get_representative_type(id);
2582-
if equal == id {
2594+
if !self.r#gen.opts.merge_structurally_equal_types() || equal == id {
25832595
wit_bindgen_core::define_type(self, name, id)
25842596
} else {
25852597
let docs = &self.resolve.types[id].docs;

crates/rust/src/lib.rs

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ pub struct RustWasm {
5050
/// Maps wit interface and type names to their Rust identifiers
5151
with: GenerationConfiguration,
5252

53-
future_payloads: IndexMap<String, String>,
54-
stream_payloads: IndexMap<String, String>,
53+
future_payloads: IndexMap<Option<Type>, String>,
54+
stream_payloads: IndexMap<Option<Type>, String>,
5555
}
5656

5757
#[derive(Default)]
@@ -1129,9 +1129,39 @@ impl WorldGenerator for RustWasm {
11291129
uwriteln!(self.src_preamble, "// * async: {opt}");
11301130
}
11311131
self.types.analyze(resolve);
1132-
if self.opts.merge_structurally_equal_types() {
1133-
self.types.collect_equal_types(resolve);
1134-
}
1132+
self.types.collect_equal_types(resolve, &|a| {
1133+
// If `--merge-structurally-equal-types` is enabled then any type
1134+
// anywhere can be generated as a type alias to anything else.
1135+
if self.opts.merge_structurally_equal_types() {
1136+
return true;
1137+
}
1138+
1139+
match resolve.types[a].kind {
1140+
// These types are all defined with `type Foo = ...` in Rust
1141+
// since Rust either has native representations or they live in
1142+
// libraries or similar.
1143+
TypeDefKind::Type(_)
1144+
| TypeDefKind::Handle(_)
1145+
| TypeDefKind::List(_)
1146+
| TypeDefKind::Tuple(_)
1147+
| TypeDefKind::Option(_)
1148+
| TypeDefKind::Result(_)
1149+
| TypeDefKind::Future(_)
1150+
| TypeDefKind::Stream(_)
1151+
| TypeDefKind::Map(..)
1152+
| TypeDefKind::FixedLengthList(..) => true,
1153+
1154+
// These types are all defined with fresh new types defined
1155+
// in generated bindings and thus can't alias some other
1156+
// existing type.
1157+
TypeDefKind::Record(_)
1158+
| TypeDefKind::Variant(_)
1159+
| TypeDefKind::Enum(_)
1160+
| TypeDefKind::Flags(_)
1161+
| TypeDefKind::Resource
1162+
| TypeDefKind::Unknown => false,
1163+
}
1164+
});
11351165
self.world = Some(world);
11361166

11371167
let world = &resolve.worlds[world];

crates/test/src/cpp.rs

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,11 @@ impl LanguageMethods for Cpp {
4141

4242
fn should_fail_verify(
4343
&self,
44-
name: &str,
45-
_config: &crate::config::WitConfig,
44+
_name: &str,
45+
config: &crate::config::WitConfig,
4646
_args: &[String],
4747
) -> bool {
48-
match name {
49-
"async-trait-function.wit"
50-
| "error-context.wit"
51-
| "futures.wit"
52-
| "import-export-future.wit"
53-
| "import-export-stream.wit"
54-
| "resources-with-futures.wit"
55-
| "resources-with-streams.wit"
56-
| "streams.wit"
57-
| "async-resource-func.wit" => true,
58-
_ => false,
59-
}
48+
config.async_
6049
}
6150

6251
fn prepare(&self, runner: &mut Runner) -> anyhow::Result<()> {

crates/test/src/csharp.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ impl LanguageMethods for Csharp {
4949
| "resource-fallible-constructor.wit"
5050
| "async-resource-func.wit"
5151
| "import-export-stream.wit"
52+
| "issue-1432.wit"
53+
| "issue-1433.wit"
54+
| "future-same-type-different-names.wit"
5255
)
5356
}
5457

crates/test/src/rust.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ impl LanguageMethods for Rust {
6363
// no_std doesn't currently work with async
6464
if config.async_
6565
&& args.iter().any(|s| s == "--std-feature")
66-
// Except this one actually _does_ work:
66+
// Except these actually do work:
6767
&& name != "async-trait-function.wit-no-std"
68+
&& name != "async-resource-func.wit-no-std"
6869
{
6970
return true;
7071
}

tests/codegen/async-resource-func.wit

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//@ async = true
2+
13
package foo:bar;
24

35
world bindings {
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//@ async = true
2+
3+
package a:b;
4+
5+
interface i {
6+
resource a;
7+
f1: func(x: future<a>);
8+
9+
type b = a;
10+
f2: func(x: future<b>);
11+
}
12+
13+
interface i2 {
14+
use i.{a};
15+
f3: func(x: future<a>);
16+
17+
use i.{b};
18+
f4: func(x: future<b>);
19+
}
20+
21+
interface i3 {
22+
f5: func(x: future<u8>);
23+
24+
type hello = u8;
25+
f6: func(x: future<hello>);
26+
}
27+
28+
interface i4 {
29+
type a = result<u8, u32>;
30+
f7: func(x: future<a>);
31+
32+
type b = result<u8, u32>;
33+
f8: func(x: future<b>);
34+
}
35+
36+
interface i5 {
37+
type a = result<u8, u32>;
38+
f9: func(x: future<a>);
39+
}
40+
41+
interface i6 {
42+
enum error-code {
43+
io,
44+
closed,
45+
}
46+
}
47+
48+
interface i7 {
49+
use i6.{error-code};
50+
f10: func() -> future<result<_, error-code>>;
51+
}
52+
53+
interface i8 {
54+
use i6.{error-code};
55+
f11: func() -> future<result<_, error-code>>;
56+
}
57+
58+
world foo {
59+
import i;
60+
import i2;
61+
import i3;
62+
import i4;
63+
import i5;
64+
import i6;
65+
import i7;
66+
import i8;
67+
}

tests/codegen/issue-1432.wit

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//@ async = true
2+
3+
package a:b;
4+
5+
world w {
6+
import i;
7+
export i;
8+
}
9+
10+
interface i {
11+
use t.{r};
12+
f: func(p: stream<r>);
13+
}
14+
15+
interface t {
16+
record r {
17+
x: u32,
18+
}
19+
}

0 commit comments

Comments
 (0)