Repository: bytecodealliance/wasm-tools
Pull request #1978 (2025-01-17 12:47:20)

feat: add support for `stream` with no `<T>`
Add support for `stream`s with no elements (streams of unit)

Refs https://github.com/WebAssembly/component-model/pull/440


Patch:
diff --git a/crates/wasm-compose/src/encoding.rs b/crates/wasm-compose/src/encoding.rs
index 7eb3fb52..a9bd70fa 100644
--- a/crates/wasm-compose/src/encoding.rs
+++ b/crates/wasm-compose/src/encoding.rs
@@ -813,8 +813,8 @@ impl<'a> TypeEncoder<'a> {
         index
     }
 
-    fn stream(&self, state: &mut TypeState<'a>, ty: ct::ComponentValType) -> u32 {
-        let ty = self.component_val_type(state, ty);
+    fn stream(&self, state: &mut TypeState<'a>, ty: Option<ct::ComponentValType>) -> u32 {
+        let ty = ty.map(|ty| self.component_val_type(state, ty));
 
         let index = state.cur.encodable.type_count();
         state.cur.encodable.ty().defined_type().stream(ty);
@@ -1255,9 +1255,7 @@ impl DependencyRegistrar<'_, '_> {
             | ComponentDefinedType::Enum(_)
             | ComponentDefinedType::Flags(_)
             | ComponentDefinedType::ErrorContext => {}
-            ComponentDefinedType::List(t)
-            | ComponentDefinedType::Option(t)
-            | ComponentDefinedType::Stream(t) => self.val_type(*t),
+            ComponentDefinedType::List(t) | ComponentDefinedType::Option(t) => self.val_type(*t),
             ComponentDefinedType::Own(r) | ComponentDefinedType::Borrow(r) => {
                 self.ty(ComponentAnyTypeId::Resource(*r))
             }
@@ -1291,6 +1289,11 @@ impl DependencyRegistrar<'_, '_> {
                     self.val_type(*ty);
                 }
             }
+            ComponentDefinedType::Stream(ty) => {
+                if let Some(ty) = ty {
+                    self.val_type(*ty);
+                }
+            }
         }
     }
 }
diff --git a/crates/wasm-encoder/src/component/types.rs b/crates/wasm-encoder/src/component/types.rs
index 44a877db..543bddbb 100644
--- a/crates/wasm-encoder/src/component/types.rs
+++ b/crates/wasm-encoder/src/component/types.rs
@@ -680,7 +680,7 @@ impl ComponentDefinedTypeEncoder<'_> {
     }
 
     /// Define a `stream` type with the specified payload.
-    pub fn stream(self, payload: ComponentValType) {
+    pub fn stream(self, payload: Option<ComponentValType>) {
         self.0.push(0x66);
         payload.encode(self.0);
     }
diff --git a/crates/wasm-encoder/src/reencode/component.rs b/crates/wasm-encoder/src/reencode/component.rs
index 1ac94d68..c4b1d789 100644
--- a/crates/wasm-encoder/src/reencode/component.rs
+++ b/crates/wasm-encoder/src/reencode/component.rs
@@ -804,7 +804,7 @@ pub mod component_utils {
                 defined.future(t.map(|t| reencoder.component_val_type(t)));
             }
             wasmparser::ComponentDefinedType::Stream(t) => {
-                defined.stream(reencoder.component_val_type(t));
+                defined.stream(t.map(|t| reencoder.component_val_type(t)));
             }
             wasmparser::ComponentDefinedType::ErrorContext => defined.error_context(),
         }
diff --git a/crates/wasmparser/src/readers/component/types.rs b/crates/wasmparser/src/readers/component/types.rs
index 51214495..a56835fd 100644
--- a/crates/wasmparser/src/readers/component/types.rs
+++ b/crates/wasmparser/src/readers/component/types.rs
@@ -508,7 +508,7 @@ pub enum ComponentDefinedType<'a> {
     /// A future type with the specified payload type.
     Future(Option<ComponentValType>),
     /// A stream type with the specified payload type.
-    Stream(ComponentValType),
+    Stream(Option<ComponentValType>),
     /// The error-context type.
     ErrorContext,
 }
diff --git a/crates/wasmparser/src/validator/component.rs b/crates/wasmparser/src/validator/component.rs
index 27e20ead..4133bf4e 100644
--- a/crates/wasmparser/src/validator/component.rs
+++ b/crates/wasmparser/src/validator/component.rs
@@ -759,7 +759,10 @@ impl ComponentState {
                 .as_ref()
                 .map(|ty| types.type_named_valtype(ty, set))
                 .unwrap_or(true),
-            ComponentDefinedType::Stream(ty) => types.type_named_valtype(ty, set),
+            ComponentDefinedType::Stream(ty) => ty
+                .as_ref()
+                .map(|ty| types.type_named_valtype(ty, set))
+                .unwrap_or(true),
         }
     }
 
@@ -1255,7 +1258,9 @@ impl ComponentState {
 
         let mut info = LoweringInfo::default();
         info.requires_memory = true;
-        info.requires_realloc = payload_type.contains_ptr(types);
+        info.requires_realloc = payload_type
+            .map(|ty| ty.contains_ptr(types))
+            .unwrap_or_default();
         self.check_options(None, &info, &options, types, offset, features, true)?;
 
         self.core_funcs
@@ -1439,7 +1444,7 @@ impl ComponentState {
         info.requires_memory = true;
         info.requires_realloc = payload_type
             .map(|ty| ty.contains_ptr(types))
-            .unwrap_or(false);
+            .unwrap_or_default();
         self.check_options(None, &info, &options, types, offset, features, true)?;
 
         self.core_funcs
@@ -3291,7 +3296,8 @@ impl ComponentState {
                     .transpose()?,
             )),
             crate::ComponentDefinedType::Stream(ty) => Ok(ComponentDefinedType::Stream(
-                self.create_component_val_type(ty, offset)?,
+                ty.map(|ty| self.create_component_val_type(ty, offset))
+                    .transpose()?,
             )),
             crate::ComponentDefinedType::ErrorContext => Ok(ComponentDefinedType::ErrorContext),
         }
diff --git a/crates/wasmparser/src/validator/component_types.rs b/crates/wasmparser/src/validator/component_types.rs
index 25f7dd84..76ab1ff5 100644
--- a/crates/wasmparser/src/validator/component_types.rs
+++ b/crates/wasmparser/src/validator/component_types.rs
@@ -1062,7 +1062,7 @@ pub enum ComponentDefinedType {
     /// A future type with the specified payload type.
     Future(Option<ComponentValType>),
     /// A stream type with the specified payload type.
-    Stream(ComponentValType),
+    Stream(Option<ComponentValType>),
     /// The error-context type.
     ErrorContext,
 }
@@ -1950,9 +1950,7 @@ impl TypeAlloc {
                     }
                 }
             }
-            ComponentDefinedType::List(ty)
-            | ComponentDefinedType::Option(ty)
-            | ComponentDefinedType::Stream(ty) => {
+            ComponentDefinedType::List(ty) | ComponentDefinedType::Option(ty) => {
                 self.free_variables_valtype(ty, set);
             }
             ComponentDefinedType::Result { ok, err } => {
@@ -1971,6 +1969,11 @@ impl TypeAlloc {
                     self.free_variables_valtype(ty, set);
                 }
             }
+            ComponentDefinedType::Stream(ty) => {
+                if let Some(ty) = ty {
+                    self.free_variables_valtype(ty, set);
+                }
+            }
         }
     }
 
@@ -2094,9 +2097,9 @@ impl TypeAlloc {
                         .map(|t| self.type_named_valtype(t, set))
                         .unwrap_or(true)
             }
-            ComponentDefinedType::List(ty)
-            | ComponentDefinedType::Option(ty)
-            | ComponentDefinedType::Stream(ty) => self.type_named_valtype(ty, set),
+            ComponentDefinedType::List(ty) | ComponentDefinedType::Option(ty) => {
+                self.type_named_valtype(ty, set)
+            }
 
             // own/borrow themselves don't have to be named, but the resource
             // they refer to must be named.
@@ -2108,6 +2111,11 @@ impl TypeAlloc {
                 .as_ref()
                 .map(|ty| self.type_named_valtype(ty, set))
                 .unwrap_or(true),
+
+            ComponentDefinedType::Stream(ty) => ty
+                .as_ref()
+                .map(|ty| self.type_named_valtype(ty, set))
+                .unwrap_or(true),
         }
     }
 
@@ -2277,9 +2285,7 @@ where
                     }
                 }
             }
-            ComponentDefinedType::List(ty)
-            | ComponentDefinedType::Option(ty)
-            | ComponentDefinedType::Stream(ty) => {
+            ComponentDefinedType::List(ty) | ComponentDefinedType::Option(ty) => {
                 any_changed |= self.remap_valtype(ty, map);
             }
             ComponentDefinedType::Result { ok, err } => {
@@ -2293,7 +2299,7 @@ where
             ComponentDefinedType::Own(id) | ComponentDefinedType::Borrow(id) => {
                 any_changed |= self.remap_resource_id(id, map);
             }
-            ComponentDefinedType::Future(ty) => {
+            ComponentDefinedType::Future(ty) | ComponentDefinedType::Stream(ty) => {
                 if let Some(ty) = ty {
                     any_changed |= self.remap_valtype(ty, map);
                 }
@@ -3234,9 +3240,14 @@ impl<'a> SubtypeCx<'a> {
                 (Some(_), None) => bail!(offset, "expected future type to not be present"),
             },
             (Future(_), b) => bail!(offset, "expected {}, found future", b.desc()),
-            (Stream(a), Stream(b)) => self
-                .component_val_type(a, b, offset)
-                .with_context(|| "type mismatch in stream"),
+            (Stream(a), Stream(b)) => match (a, b) {
+                (None, None) => Ok(()),
+                (Some(a), Some(b)) => self
+                    .component_val_type(a, b, offset)
+                    .with_context(|| "type mismatch in stream"),
+                (None, Some(_)) => bail!(offset, "expected stream type, but found none"),
+                (Some(_), None) => bail!(offset, "expected stream type to not be present"),
+            },
             (Stream(_), b) => bail!(offset, "expected {}, found stream", b.desc()),
             (ErrorContext, ErrorContext) => Ok(()),
             (ErrorContext, b) => bail!(offset, "expected {}, found error-context", b.desc()),
diff --git a/crates/wasmprinter/src/component.rs b/crates/wasmprinter/src/component.rs
index 0a40fe16..24a2cbfd 100644
--- a/crates/wasmprinter/src/component.rs
+++ b/crates/wasmprinter/src/component.rs
@@ -240,10 +240,14 @@ impl Printer<'_, '_> {
         Ok(())
     }
 
-    fn print_stream_type(&mut self, state: &State, ty: ComponentValType) -> Result<()> {
+    fn print_stream_type(&mut self, state: &State, ty: Option<ComponentValType>) -> Result<()> {
         self.start_group("stream")?;
-        self.result.write_str(" ")?;
-        self.print_component_val_type(state, &ty)?;
+
+        if let Some(ty) = ty {
+            self.result.write_str(" ")?;
+            self.print_component_val_type(state, &ty)?;
+        }
+
         self.end_group()?;
 
         Ok(())
diff --git a/crates/wast/src/component/binary.rs b/crates/wast/src/component/binary.rs
index 856b50d7..2bf5e773 100644
--- a/crates/wast/src/component/binary.rs
+++ b/crates/wast/src/component/binary.rs
@@ -133,7 +133,7 @@ fn encode_defined_type(encoder: ComponentDefinedTypeEncoder, ty: &ComponentDefin
         }
         ComponentDefinedType::Own(i) => encoder.own((*i).into()),
         ComponentDefinedType::Borrow(i) => encoder.borrow((*i).into()),
-        ComponentDefinedType::Stream(s) => encoder.stream(s.element.as_ref().into()),
+        ComponentDefinedType::Stream(s) => encoder.stream(s.element.as_deref().map(Into::into)),
         ComponentDefinedType::Future(f) => encoder.future(f.element.as_deref().map(Into::into)),
     }
 }
diff --git a/crates/wast/src/component/expand.rs b/crates/wast/src/component/expand.rs
index 2df745f0..5f171fee 100644
--- a/crates/wast/src/component/expand.rs
+++ b/crates/wast/src/component/expand.rs
@@ -762,7 +762,9 @@ impl<'a> Expander<'a> {
             }
             ComponentDefinedType::Own(_) | ComponentDefinedType::Borrow(_) => {}
             ComponentDefinedType::Stream(t) => {
-                self.expand_component_val_ty(&mut t.element);
+                if let Some(ty) = &mut t.element {
+                    self.expand_component_val_ty(ty);
+                }
             }
             ComponentDefinedType::Future(t) => {
                 if let Some(ty) = &mut t.element {
diff --git a/crates/wast/src/component/resolve.rs b/crates/wast/src/component/resolve.rs
index c488b581..c782a2eb 100644
--- a/crates/wast/src/component/resolve.rs
+++ b/crates/wast/src/component/resolve.rs
@@ -553,7 +553,9 @@ impl<'a> Resolver<'a> {
                 self.resolve_ns(t, Ns::Type)?;
             }
             ComponentDefinedType::Stream(s) => {
-                self.component_val_type(&mut s.element)?;
+                if let Some(ty) = &mut s.element {
+                    self.component_val_type(ty)?;
+                }
             }
             ComponentDefinedType::Future(f) => {
                 if let Some(ty) = &mut f.element {
diff --git a/crates/wast/src/component/types.rs b/crates/wast/src/component/types.rs
index abefa1d1..23951f21 100644
--- a/crates/wast/src/component/types.rs
+++ b/crates/wast/src/component/types.rs
@@ -694,14 +694,14 @@ impl<'a> Parse<'a> for ResultType<'a> {
 #[derive(Debug)]
 pub struct Stream<'a> {
     /// The element type of the stream.
-    pub element: Box<ComponentValType<'a>>,
+    pub element: Option<Box<ComponentValType<'a>>>,
 }
 
 impl<'a> Parse<'a> for Stream<'a> {
     fn parse(parser: Parser<'a>) -> Result<Self> {
         parser.parse::<kw::stream>()?;
         Ok(Self {
-            element: Box::new(parser.parse()?),
+            element: parser.parse::<Option<ComponentValType>>()?.map(Box::new),
         })
     }
 }
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index 6c1b3a25..75852bc1 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -1368,7 +1368,15 @@ impl<'a> EncodingState<'a> {
                             else {
                                 unreachable!()
                             };
-                            let options = options(self, vec![*payload_type], vec![])?;
+                            let options = options(
+                                self,
+                                if let Some(payload_type) = payload_type {
+                                    vec![*payload_type]
+                                } else {
+                                    vec![]
+                                },
+                                vec![],
+                            )?;
                             self.component.stream_write(type_index, options)
                         }
                         PayloadFuncKind::StreamRead => {
@@ -1376,7 +1384,15 @@ impl<'a> EncodingState<'a> {
                             else {
                                 unreachable!()
                             };
-                            let options = options(self, vec![], vec![*payload_type])?;
+                            let options = options(
+                                self,
+                                vec![],
+                                if let Some(payload_type) = payload_type {
+                                    vec![*payload_type]
+                                } else {
+                                    vec![]
+                                },
+                            )?;
                             self.component.stream_read(type_index, options)
                         }
                     }
@@ -1443,8 +1459,8 @@ impl<'a> EncodingState<'a> {
         fn owner(resolve: &Resolve, ty: TypeId) -> Option<InterfaceId> {
             let def = &resolve.types[ty];
             match &def.kind {
-                TypeDefKind::Future(Some(Type::Id(ty))) => owner(resolve, *ty),
-                TypeDefKind::Stream(Type::Id(ty)) => owner(resolve, *ty),
+                TypeDefKind::Future(Some(Type::Id(ty)))
+                | TypeDefKind::Stream(Some(Type::Id(ty))) => owner(resolve, *ty),
                 _ => match &def.owner {
                     TypeOwner::World(_) | TypeOwner::None => None,
                     TypeOwner::Interface(id) => Some(*id),
diff --git a/crates/wit-component/src/encoding/types.rs b/crates/wit-component/src/encoding/types.rs
index 1ed43664..cdf2e893 100644
--- a/crates/wit-component/src/encoding/types.rs
+++ b/crates/wit-component/src/encoding/types.rs
@@ -322,8 +322,12 @@ pub trait ValtypeEncoder<'a> {
         Ok(ComponentValType::Type(index))
     }
 
-    fn encode_stream(&mut self, resolve: &'a Resolve, payload: &Type) -> Result<ComponentValType> {
-        let ty = self.encode_valtype(resolve, payload)?;
+    fn encode_stream(
+        &mut self,
+        resolve: &'a Resolve,
+        payload: &Option<Type>,
+    ) -> Result<ComponentValType> {
+        let ty = self.encode_optional_valtype(resolve, payload.as_ref())?;
         let (index, encoder) = self.defined_type();
         encoder.stream(ty);
         Ok(ComponentValType::Type(index))
diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs
index 5ea93977..9c6e7165 100644
--- a/crates/wit-component/src/printing.rs
+++ b/crates/wit-component/src/printing.rs
@@ -584,9 +584,13 @@ impl<O: Output> WitPrinter<O> {
                         }
                     }
                     TypeDefKind::Stream(ty) => {
-                        self.output.push_str("stream<");
-                        self.print_type_name(resolve, ty)?;
-                        self.output.push_str(">");
+                        if let Some(ty) = ty {
+                            self.output.push_str("stream<");
+                            self.print_type_name(resolve, ty)?;
+                            self.output.push_str(">");
+                        } else {
+                            self.output.push_str("stream");
+                        }
                     }
                     TypeDefKind::ErrorContext => self.output.push_str("error-context"),
                     TypeDefKind::Unknown => unreachable!(),
@@ -758,7 +762,7 @@ impl<O: Output> WitPrinter<O> {
                         self.declare_future(resolve, ty.name.as_deref(), inner.as_ref())?
                     }
                     TypeDefKind::Stream(inner) => {
-                        self.declare_stream(resolve, ty.name.as_deref(), inner)?
+                        self.declare_stream(resolve, ty.name.as_deref(), inner.as_ref())?
                     }
                     TypeDefKind::ErrorContext => self.declare_error_context(ty.name.as_deref())?,
                     TypeDefKind::Unknown => unreachable!(),
@@ -957,16 +961,23 @@ impl<O: Output> WitPrinter<O> {
         Ok(())
     }
 
-    fn declare_stream(&mut self, resolve: &Resolve, name: Option<&str>, ty: &Type) -> Result<()> {
+    fn declare_stream(
+        &mut self,
+        resolve: &Resolve,
+        name: Option<&str>,
+        ty: Option<&Type>,
+    ) -> Result<()> {
         if let Some(name) = name {
             self.output.keyword("type");
             self.output.str(" ");
             self.print_name_type(name, TypeKind::Stream);
             self.output.str(" = ");
             self.output.ty("stream", TypeKind::BuiltIn);
-            self.output.str("<");
-            self.print_type_name(resolve, ty)?;
-            self.output.str(">");
+            if let Some(ty) = ty {
+                self.output.str("<");
+                self.print_type_name(resolve, ty)?;
+                self.output.str(">");
+            }
             self.output.semicolon();
         }
 
diff --git a/crates/wit-encoder/src/from_parser.rs b/crates/wit-encoder/src/from_parser.rs
index a31c34a1..32c97cc9 100644
--- a/crates/wit-encoder/src/from_parser.rs
+++ b/crates/wit-encoder/src/from_parser.rs
@@ -232,7 +232,7 @@ impl<'a> Converter<'a> {
                         TypeDefKind::Type(Type::future(self.convert_option_type(ty)))
                     }
                     wit_parser::TypeDefKind::Stream(ty) => {
-                        TypeDefKind::Type(Type::stream(self.convert_type(ty)))
+                        TypeDefKind::Type(Type::stream(self.convert_option_type(ty)))
                     }
                     wit_parser::TypeDefKind::ErrorContext => TypeDefKind::Type(Type::ErrorContext),
                     // all the following are just `type` declarations
@@ -309,7 +309,7 @@ impl<'a> Converter<'a> {
                             Type::future(self.convert_option_type(type_))
                         }
                         wit_parser::TypeDefKind::Stream(type_) => {
-                            Type::stream(self.convert_type(type_))
+                            Type::stream(self.convert_option_type(type_))
                         }
                         wit_parser::TypeDefKind::ErrorContext => Type::ErrorContext,
                         wit_parser::TypeDefKind::Record(_)
diff --git a/crates/wit-encoder/src/ty.rs b/crates/wit-encoder/src/ty.rs
index 583e39ab..afe4891a 100644
--- a/crates/wit-encoder/src/ty.rs
+++ b/crates/wit-encoder/src/ty.rs
@@ -28,7 +28,7 @@ pub enum Type {
     List(Box<Type>),
     Tuple(Tuple),
     Future(Option<Box<Type>>),
-    Stream(Box<Type>),
+    Stream(Option<Box<Type>>),
     ErrorContext,
     Named(Ident),
 }
@@ -66,8 +66,8 @@ impl Type {
     pub fn future(type_: Option<Type>) -> Self {
         Type::Future(type_.map(Box::new))
     }
-    pub fn stream(type_: Type) -> Self {
-        Type::Stream(Box::new(type_))
+    pub fn stream(type_: Option<Type>) -> Self {
+        Type::Stream(type_.map(Box::new))
     }
     pub fn named(name: impl Into<Ident>) -> Self {
         Type::Named(name.into())
@@ -123,7 +123,10 @@ impl Display for Type {
             Type::Future(Some(type_)) => {
                 write!(f, "future<{type_}>")
             }
-            Type::Stream(type_) => {
+            Type::Stream(None) => {
+                write!(f, "stream")
+            }
+            Type::Stream(Some(type_)) => {
                 write!(f, "stream<{type_}>")
             }
             Type::ErrorContext => write!(f, "error-context"),
diff --git a/crates/wit-parser/src/ast.rs b/crates/wit-parser/src/ast.rs
index b3e3033e..727d248a 100644
--- a/crates/wit-parser/src/ast.rs
+++ b/crates/wit-parser/src/ast.rs
@@ -898,7 +898,7 @@ struct Result_<'a> {
 
 struct Stream<'a> {
     span: Span,
-    ty: Box<Type<'a>>,
+    ty: Option<Box<Type<'a>>>,
 }
 
 struct NamedFunc<'a> {
@@ -1404,14 +1404,15 @@ impl<'a> Type<'a> {
             }
 
             // stream<T>
+            // stream
             Some((span, Token::Stream)) => {
-                tokens.expect(Token::LessThan)?;
-                let ty = Type::parse(tokens)?;
-                tokens.expect(Token::GreaterThan)?;
-                Ok(Type::Stream(Stream {
-                    span,
-                    ty: Box::new(ty),
-                }))
+                let mut ty = None;
+
+                if tokens.eat(Token::LessThan)? {
+                    ty = Some(Box::new(Type::parse(tokens)?));
+                    tokens.expect(Token::GreaterThan)?;
+                };
+                Ok(Type::Stream(Stream { span, ty }))
             }
 
             // error-context
diff --git a/crates/wit-parser/src/ast/resolve.rs b/crates/wit-parser/src/ast/resolve.rs
index b38d6588..4ddca1b9 100644
--- a/crates/wit-parser/src/ast/resolve.rs
+++ b/crates/wit-parser/src/ast/resolve.rs
@@ -95,7 +95,7 @@ enum Key {
     Option(Type),
     Result(Option<Type>, Option<Type>),
     Future(Option<Type>),
-    Stream(Type),
+    Stream(Option<Type>),
     ErrorContext,
 }
 
@@ -1255,7 +1255,9 @@ impl<'a> Resolver<'a> {
             ast::Type::Future(t) => {
                 TypeDefKind::Future(self.resolve_optional_type(t.ty.as_deref(), stability)?)
             }
-            ast::Type::Stream(s) => TypeDefKind::Stream(self.resolve_type(&s.ty, stability)?),
+            ast::Type::Stream(s) => {
+                TypeDefKind::Stream(self.resolve_optional_type(s.ty.as_deref(), stability)?)
+            }
             ast::Type::ErrorContext(_) => TypeDefKind::ErrorContext,
         })
     }
@@ -1325,10 +1327,10 @@ impl<'a> Resolver<'a> {
                     find_in_type(types, Type::Id(*id))
                 }
                 TypeDefKind::Tuple(t) => t.types.iter().find_map(|ty| find_in_type(types, *ty)),
-                TypeDefKind::List(ty) | TypeDefKind::Stream(ty) | TypeDefKind::Option(ty) => {
-                    find_in_type(types, *ty)
+                TypeDefKind::List(ty) | TypeDefKind::Option(ty) => find_in_type(types, *ty),
+                TypeDefKind::Future(ty) | TypeDefKind::Stream(ty) => {
+                    ty.as_ref().and_then(|ty| find_in_type(types, *ty))
                 }
-                TypeDefKind::Future(ty) => ty.as_ref().and_then(|ty| find_in_type(types, *ty)),
                 TypeDefKind::Result(r) => {
                     r.ok.as_ref()
                         .and_then(|ty| find_in_type(types, *ty))
@@ -1657,9 +1659,9 @@ fn collect_deps<'a>(ty: &ast::Type<'a>, deps: &mut Vec<ast::Id<'a>>) {
                 }
             }
         }
-        ast::Type::Option(ast::Option_ { ty, .. })
-        | ast::Type::List(ast::List { ty, .. })
-        | ast::Type::Stream(ast::Stream { ty, .. }) => collect_deps(ty, deps),
+        ast::Type::Option(ast::Option_ { ty, .. }) | ast::Type::List(ast::List { ty, .. }) => {
+            collect_deps(ty, deps)
+        }
         ast::Type::Result(r) => {
             if let Some(ty) = &r.ok {
                 collect_deps(ty, deps);
@@ -1673,5 +1675,10 @@ fn collect_deps<'a>(ty: &ast::Type<'a>, deps: &mut Vec<ast::Id<'a>>) {
                 collect_deps(t, deps)
             }
         }
+        ast::Type::Stream(s) => {
+            if let Some(t) = &s.ty {
+                collect_deps(t, deps)
+            }
+        }
     }
 }
diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs
index 0dbb0414..2cc0b406 100644
--- a/crates/wit-parser/src/decoding.rs
+++ b/crates/wit-parser/src/decoding.rs
@@ -1401,7 +1401,9 @@ impl WitPackageDecoder<'_> {
                 ty.as_ref().map(|ty| self.convert_valtype(ty)).transpose()?,
             )),
 
-            ComponentDefinedType::Stream(ty) => Ok(TypeDefKind::Stream(self.convert_valtype(ty)?)),
+            ComponentDefinedType::Stream(ty) => Ok(TypeDefKind::Stream(
+                ty.as_ref().map(|ty| self.convert_valtype(ty)).transpose()?,
+            )),
 
             ComponentDefinedType::ErrorContext => Ok(TypeDefKind::ErrorContext),
         }
@@ -1693,7 +1695,11 @@ impl Registrar<'_> {
                     TypeDefKind::Type(Type::Id(_)) => return Ok(()),
                     _ => bail!("expected a stream"),
                 };
-                self.valtype(payload, ty)
+                match (payload, ty) {
+                    (Some(a), Some(b)) => self.valtype(a, b),
+                    (None, None) => Ok(()),
+                    _ => bail!("disagreement on stream payload"),
+                }
             }
 
             // These have no recursive structure so they can bail out.
diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs
index a3370cf5..3e11bba8 100644
--- a/crates/wit-parser/src/lib.rs
+++ b/crates/wit-parser/src/lib.rs
@@ -570,7 +570,7 @@ pub enum TypeDefKind {
     Result(Result_),
     List(Type),
     Future(Option<Type>),
-    Stream(Type),
+    Stream(Option<Type>),
     ErrorContext,
     Type(Type),
 
@@ -1140,7 +1140,9 @@ fn find_futures_and_streams(resolve: &Resolve, ty: Type, results: &mut Vec<TypeI
             results.push(id);
         }
         TypeDefKind::Stream(ty) => {
-            find_futures_and_streams(resolve, *ty, results);
+            if let Some(ty) = ty {
+                find_futures_and_streams(resolve, *ty, results);
+            }
             results.push(id);
         }
         TypeDefKind::Unknown => unreachable!(),
@@ -1246,7 +1248,7 @@ mod test {
         });
         let t2 = resolve.types.alloc(TypeDef {
             name: None,
-            kind: TypeDefKind::Stream(Type::U32),
+            kind: TypeDefKind::Stream(Some(Type::U32)),
             owner: TypeOwner::None,
             docs: Docs::default(),
             stability: Stability::Unknown,
diff --git a/crates/wit-parser/src/live.rs b/crates/wit-parser/src/live.rs
index 84e2ebf8..2027bd1b 100644
--- a/crates/wit-parser/src/live.rs
+++ b/crates/wit-parser/src/live.rs
@@ -130,7 +130,7 @@ pub trait TypeIdVisitor {
             | TypeDefKind::List(t)
             | TypeDefKind::Option(t)
             | TypeDefKind::Future(Some(t))
-            | TypeDefKind::Stream(t) => self.visit_type(resolve, t),
+            | TypeDefKind::Stream(Some(t)) => self.visit_type(resolve, t),
             TypeDefKind::Handle(handle) => match handle {
                 crate::Handle::Own(ty) => self.visit_type_id(resolve, *ty),
                 crate::Handle::Borrow(ty) => self.visit_type_id(resolve, *ty),
@@ -164,7 +164,8 @@ pub trait TypeIdVisitor {
             TypeDefKind::ErrorContext
             | TypeDefKind::Flags(_)
             | TypeDefKind::Enum(_)
-            | TypeDefKind::Future(None) => {}
+            | TypeDefKind::Future(None)
+            | TypeDefKind::Stream(None) => {}
             TypeDefKind::Unknown => unreachable!(),
         }
     }
diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs
index d1f40381..59009781 100644
--- a/crates/wit-parser/src/resolve.rs
+++ b/crates/wit-parser/src/resolve.rs
@@ -3019,7 +3019,9 @@ impl Remap {
                     }
                 }
             }
-            Option(t) | List(t) | Stream(t) => self.update_ty(resolve, t, span)?,
+            Option(t) | List(t) | Future(Some(t)) | Stream(Some(t)) => {
+                self.update_ty(resolve, t, span)?
+            }
             Result(r) => {
                 if let Some(ty) = &mut r.ok {
                     self.update_ty(resolve, ty, span)?;
@@ -3028,7 +3030,6 @@ impl Remap {
                     self.update_ty(resolve, ty, span)?;
                 }
             }
-            Future(Some(t)) => self.update_ty(resolve, t, span)?,
             ErrorContext => {}
 
             // Note that `update_ty` is specifically not used here as typedefs
@@ -3039,7 +3040,7 @@ impl Remap {
             Type(_) => {}
 
             // nothing to do for these as they're just names or empty
-            Flags(_) | Enum(_) | Future(None) => {}
+            Flags(_) | Enum(_) | Future(None) | Stream(None) => {}
 
             Unknown => unreachable!(),
         }
@@ -3422,13 +3423,15 @@ impl Remap {
             TypeDefKind::Enum(_) => false,
             TypeDefKind::List(ty)
             | TypeDefKind::Future(Some(ty))
-            | TypeDefKind::Stream(ty)
+            | TypeDefKind::Stream(Some(ty))
             | TypeDefKind::Option(ty) => self.type_has_borrow(resolve, ty),
             TypeDefKind::Result(r) => [&r.ok, &r.err]
                 .iter()
                 .filter_map(|t| t.as_ref())
                 .any(|t| self.type_has_borrow(resolve, t)),
-            TypeDefKind::Future(None) | TypeDefKind::ErrorContext => false,
+            TypeDefKind::Future(None) | TypeDefKind::Stream(None) | TypeDefKind::ErrorContext => {
+                false
+            }
             TypeDefKind::Unknown => unreachable!(),
         }
     }
diff --git a/crates/wit-parser/src/resolve/clone.rs b/crates/wit-parser/src/resolve/clone.rs
index 4c8c18ef..1f7dc543 100644
--- a/crates/wit-parser/src/resolve/clone.rs
+++ b/crates/wit-parser/src/resolve/clone.rs
@@ -110,7 +110,7 @@ impl<'a> Cloner<'a> {
             TypeDefKind::Handle(Handle::Own(ty) | Handle::Borrow(ty)) => {
                 self.type_id(ty);
             }
-            TypeDefKind::Option(ty) | TypeDefKind::List(ty) | TypeDefKind::Stream(ty) => {
+            TypeDefKind::Option(ty) | TypeDefKind::List(ty) => {
                 self.ty(ty);
             }
             TypeDefKind::Tuple(list) => {
@@ -138,8 +138,8 @@ impl<'a> Cloner<'a> {
                     self.ty(err);
                 }
             }
-            TypeDefKind::Future(f) => {
-                if let Some(ty) = f {
+            TypeDefKind::Future(ty) | TypeDefKind::Stream(ty) => {
+                if let Some(ty) = ty {
                     self.ty(ty);
                 }
             }


Test patch:
diff --git a/crates/wit-parser/tests/ui/streams-and-futures.wit b/crates/wit-parser/tests/ui/streams-and-futures.wit
index de023fdc..72d5f3dc 100644
--- a/crates/wit-parser/tests/ui/streams-and-futures.wit
+++ b/crates/wit-parser/tests/ui/streams-and-futures.wit
@@ -9,6 +9,7 @@ interface streams-and-futures {
   resource r1;
   type t6 = stream<r1>;
   type t7 = future<result<r1>>;
+  type t8 = stream;
 
   foo: func(x: stream<u32>, y: t6) -> future<result<list<string>, string>>;
 }
diff --git a/crates/wit-parser/tests/ui/streams-and-futures.wit.json b/crates/wit-parser/tests/ui/streams-and-futures.wit.json
index a422a21a..a0bc2d95 100644
--- a/crates/wit-parser/tests/ui/streams-and-futures.wit.json
+++ b/crates/wit-parser/tests/ui/streams-and-futures.wit.json
@@ -11,7 +11,8 @@
         "t5": 8,
         "r1": 9,
         "t6": 11,
-        "t7": 13
+        "t7": 13,
+        "t8": 14
       },
       "functions": {
         "foo": {
@@ -20,7 +21,7 @@
           "params": [
             {
               "name": "x",
-              "type": 14
+              "type": 15
             },
             {
               "name": "y",
@@ -29,7 +30,7 @@
           ],
           "results": [
             {
-              "type": 17
+              "type": 18
             }
           ]
         }
@@ -155,6 +156,15 @@
         "interface": 0
       }
     },
+    {
+      "name": "t8",
+      "kind": {
+        "stream": null
+      },
+      "owner": {
+        "interface": 0
+      }
+    },
     {
       "name": null,
       "kind": {
@@ -173,7 +183,7 @@
       "name": null,
       "kind": {
         "result": {
-          "ok": 15,
+          "ok": 16,
           "err": "string"
         }
       },
@@ -182,7 +192,7 @@
     {
       "name": null,
       "kind": {
-        "future": 16
+        "future": 17
       },
       "owner": null
     }
@@ -196,4 +206,4 @@
       "worlds": {}
     }
   ]
-}
\ No newline at end of file
+}