From 47d7975f0b83095a7ea6494f11a1970f6edfc324 Mon Sep 17 00:00:00 2001 From: Ryan P Date: Tue, 30 Jun 2026 16:03:46 -0400 Subject: [PATCH 1/2] Support wasip3 wit changes WAC frontend --- LANGUAGE.md | 8 +- crates/wac-parser/src/ast/printer.rs | 20 +++ crates/wac-parser/src/ast/type.rs | 162 +++++++++++++++++- crates/wac-parser/src/lexer.rs | 24 +++ crates/wac-parser/src/resolution.rs | 47 ++++- crates/wac-parser/tests/encoding/types.wac | 32 ++++ .../tests/encoding/types.wac.result | 68 ++++++++ .../tests/parser/fail/async-not-func.wac | 3 + .../parser/fail/async-not-func.wac.result | 9 + .../parser/fail/expected-multiple.wac.result | 2 +- .../parser/fail/future-missing-close.wac | 3 + .../fail/future-missing-close.wac.result | 9 + .../parser/fail/stream-missing-close.wac | 3 + .../fail/stream-missing-close.wac.result | 9 + .../wac-parser/tests/parser/import.wac.result | 4 + .../tests/parser/resource.wac.result | 4 + .../wac-parser/tests/parser/types.wac.result | 5 + crates/wac-parser/tests/parser/use.wac.result | 1 + .../resolution/fail/mismatched-async.wac | 7 + .../fail/mismatched-async.wac.result | 11 ++ .../fail/mismatched-async/foo/bar.wat | 6 + .../tests/resolution/wasip3-types.wac | 34 ++++ .../tests/resolution/wasip3-types.wac.result | 14 ++ 23 files changed, 472 insertions(+), 13 deletions(-) create mode 100644 crates/wac-parser/tests/parser/fail/async-not-func.wac create mode 100644 crates/wac-parser/tests/parser/fail/async-not-func.wac.result create mode 100644 crates/wac-parser/tests/parser/fail/future-missing-close.wac create mode 100644 crates/wac-parser/tests/parser/fail/future-missing-close.wac.result create mode 100644 crates/wac-parser/tests/parser/fail/stream-missing-close.wac create mode 100644 crates/wac-parser/tests/parser/fail/stream-missing-close.wac.result create mode 100644 crates/wac-parser/tests/resolution/fail/mismatched-async.wac create mode 100644 crates/wac-parser/tests/resolution/fail/mismatched-async.wac.result create mode 100644 crates/wac-parser/tests/resolution/fail/mismatched-async/foo/bar.wat create mode 100644 crates/wac-parser/tests/resolution/wasip3-types.wac create mode 100644 crates/wac-parser/tests/resolution/wasip3-types.wac.result diff --git a/LANGUAGE.md b/LANGUAGE.md index 712f3573..b243f4f5 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -485,7 +485,7 @@ ids ::= id (',' id)* ','? enum-decl ::= 'enum' id '{' ids '}' type-alias ::= 'type' id '=' (func-type | type) ';' func-type-ref ::= func-type | id -func-type ::= 'func' '(' params? ')' ('->' results)? +func-type ::= 'async'? 'func' '(' params? ')' ('->' results)? params ::= named-type (',' named-type)* ','? results ::= type | '(' named-type (',' named-type)* ','? ')' @@ -508,6 +508,9 @@ type ::= u8 | option | result | borrow + | stream + | future + | error-context | id tuple ::= 'tuple' '<' type (',' type)* ','? '>' list ::= 'list' '<' type '>' @@ -517,6 +520,9 @@ result ::= 'result' | 'result' '<' '_' ',' type '>' | 'result' '<' type ',' type '>' borrow ::= 'borrow' '<' type '>' +stream ::= 'stream' | 'stream' '<' type '>' +future ::= 'future' | 'future' '<' type '>' +error-context ::= 'error-context' let-statement ::= 'let' id '=' expr ';' expr ::= primary-expr postfix-expr* diff --git a/crates/wac-parser/src/ast/printer.rs b/crates/wac-parser/src/ast/printer.rs index e4f69d03..6030bc1d 100644 --- a/crates/wac-parser/src/ast/printer.rs +++ b/crates/wac-parser/src/ast/printer.rs @@ -125,6 +125,9 @@ impl<'a, W: Write> DocumentPrinter<'a, W> { /// Prints the given function type. pub fn func_type(&mut self, ty: &FuncType) -> std::fmt::Result { + if ty.is_async { + write!(self.writer, "async ")?; + } write!(self.writer, "func(")?; self.named_types(&ty.params)?; write!(self.writer, ")")?; @@ -212,6 +215,23 @@ impl<'a, W: Write> DocumentPrinter<'a, W> { Type::Borrow(id, _) => { write!(self.writer, "borrow<{id}>", id = self.source(id.span)) } + Type::Stream(ty, _) => match ty { + Some(ty) => { + write!(self.writer, "stream<")?; + self.ty(ty)?; + write!(self.writer, ">") + } + None => write!(self.writer, "stream"), + }, + Type::Future(ty, _) => match ty { + Some(ty) => { + write!(self.writer, "future<")?; + self.ty(ty)?; + write!(self.writer, ">") + } + None => write!(self.writer, "future"), + }, + Type::ErrorContext(_) => write!(self.writer, "error-context"), Type::Ident(id) => write!(self.writer, "{id}", id = self.source(id.span)), } } diff --git a/crates/wac-parser/src/ast/type.rs b/crates/wac-parser/src/ast/type.rs index 02b71749..580cba43 100644 --- a/crates/wac-parser/src/ast/type.rs +++ b/crates/wac-parser/src/ast/type.rs @@ -482,7 +482,7 @@ pub enum FuncTypeRef<'a> { impl<'a> Parse<'a> for FuncTypeRef<'a> { fn parse(lexer: &mut Lexer<'a>) -> ParseResult { let mut lookahead = Lookahead::new(lexer); - if lookahead.peek(Token::FuncKeyword) { + if lookahead.peek(Token::AsyncKeyword) || lookahead.peek(Token::FuncKeyword) { Ok(Self::Func(Parse::parse(lexer)?)) } else if Ident::peek(&mut lookahead) { Ok(Self::Ident(Parse::parse(lexer)?)) @@ -496,6 +496,8 @@ impl<'a> Parse<'a> for FuncTypeRef<'a> { #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct FuncType<'a> { + /// Whether the function is async. + pub is_async: bool, /// The parameters of the function. pub params: Vec>, /// The results of the function. @@ -504,6 +506,7 @@ pub struct FuncType<'a> { impl<'a> Parse<'a> for FuncType<'a> { fn parse(lexer: &mut Lexer<'a>) -> ParseResult { + let is_async = parse_optional(lexer, Token::AsyncKeyword, |_| Ok(()))?.is_some(); parse_token(lexer, Token::FuncKeyword)?; parse_token(lexer, Token::OpenParen)?; let params = parse_delimited(lexer, Token::CloseParen, true)?; @@ -511,13 +514,17 @@ impl<'a> Parse<'a> for FuncType<'a> { let results = parse_optional(lexer, Token::Arrow, Parse::parse)?.unwrap_or(ResultList::Empty); - Ok(Self { params, results }) + Ok(Self { + is_async, + params, + results, + }) } } impl Peek for FuncType<'_> { fn peek(lookahead: &mut Lookahead) -> bool { - lookahead.peek(Token::FuncKeyword) + lookahead.peek(Token::AsyncKeyword) || lookahead.peek(Token::FuncKeyword) } } @@ -604,7 +611,7 @@ pub enum TypeAliasKind<'a> { impl<'a> Parse<'a> for TypeAliasKind<'a> { fn parse(lexer: &mut Lexer<'a>) -> ParseResult { let mut lookahead = Lookahead::new(lexer); - if lookahead.peek(Token::FuncKeyword) { + if lookahead.peek(Token::AsyncKeyword) || lookahead.peek(Token::FuncKeyword) { Ok(Self::Func(Parse::parse(lexer)?)) } else if Type::peek(&mut lookahead) { Ok(Self::Type(Parse::parse(lexer)?)) @@ -661,6 +668,12 @@ pub enum Type<'a> { }, /// A borrow type. Borrow(Ident<'a>, SourceSpan), + /// A stream type. + Stream(Option>>, SourceSpan), + /// A future type. + Future(Option>>, SourceSpan), + /// An error-context type. + ErrorContext(SourceSpan), /// An identifier to a value type. Ident(Ident<'a>), } @@ -686,7 +699,10 @@ impl Type<'_> { | Self::List(_, span) | Self::Option(_, span) | Self::Result { span, .. } - | Self::Borrow(_, span) => *span, + | Self::Borrow(_, span) + | Self::Stream(_, span) + | Self::Future(_, span) + | Self::ErrorContext(span) => *span, Self::Ident(ident) => ident.span, } } @@ -817,6 +833,42 @@ impl<'a> Parse<'a> for Type<'a> { (close.offset() + close.len()) - span.offset(), ), )) + } else if lookahead.peek(Token::StreamKeyword) { + let span = lexer.next().unwrap().1; + let ty = parse_optional(lexer, Token::OpenAngle, |lexer| { + let ty = Box::new(Parse::parse(lexer)?); + let close = parse_token(lexer, Token::CloseAngle)?; + Ok((ty, close)) + })?; + match ty { + Some((ty, close)) => Ok(Self::Stream( + Some(ty), + SourceSpan::new( + span.offset().into(), + (close.offset() + close.len()) - span.offset(), + ), + )), + None => Ok(Self::Stream(None, span)), + } + } else if lookahead.peek(Token::FutureKeyword) { + let span = lexer.next().unwrap().1; + let ty = parse_optional(lexer, Token::OpenAngle, |lexer| { + let ty = Box::new(Parse::parse(lexer)?); + let close = parse_token(lexer, Token::CloseAngle)?; + Ok((ty, close)) + })?; + match ty { + Some((ty, close)) => Ok(Self::Future( + Some(ty), + SourceSpan::new( + span.offset().into(), + (close.offset() + close.len()) - span.offset(), + ), + )), + None => Ok(Self::Future(None, span)), + } + } else if lookahead.peek(Token::ErrorContextKeyword) { + Ok(Self::ErrorContext(lexer.next().unwrap().1)) } else if Ident::peek(&mut lookahead) { Ok(Self::Ident(Parse::parse(lexer)?)) } else { @@ -845,6 +897,9 @@ impl Peek for Type<'_> { || lookahead.peek(Token::OptionKeyword) || lookahead.peek(Token::ResultKeyword) || lookahead.peek(Token::BorrowKeyword) + || lookahead.peek(Token::StreamKeyword) + || lookahead.peek(Token::FutureKeyword) + || lookahead.peek(Token::ErrorContextKeyword) || Ident::peek(lookahead) } } @@ -1561,6 +1616,103 @@ enum foo { .unwrap(); } + #[test] + fn stream_future_error_context_roundtrip() { + // stream (no payload) + roundtrip( + "package foo:bar; type a = stream;", + "package foo:bar;\n\ntype a = stream;\n", + ) + .unwrap(); + + // stream + roundtrip( + "package foo:bar; type b = stream;", + "package foo:bar;\n\ntype b = stream;\n", + ) + .unwrap(); + + // future (no payload) + roundtrip( + "package foo:bar; type c = future;", + "package foo:bar;\n\ntype c = future;\n", + ) + .unwrap(); + + // future + roundtrip( + "package foo:bar; type d = future;", + "package foo:bar;\n\ntype d = future;\n", + ) + .unwrap(); + + // error-context + roundtrip( + "package foo:bar; type e = error-context;", + "package foo:bar;\n\ntype e = error-context;\n", + ) + .unwrap(); + + // nested: stream> + roundtrip( + "package foo:bar; type f = stream>;", + "package foo:bar;\n\ntype f = stream>;\n", + ) + .unwrap(); + + // future> + roundtrip( + "package foo:bar; type g = future>;", + "package foo:bar;\n\ntype g = future>;\n", + ) + .unwrap(); + } + + #[test] + fn async_func_roundtrip() { + // async func type alias + roundtrip( + "package foo:bar; type x = async func() -> string;", + "package foo:bar;\n\ntype x = async func() -> string;\n", + ) + .unwrap(); + + // async func with params + roundtrip( + "package foo:bar; type y = async func(a: u32, b: string) -> stream;", + "package foo:bar;\n\ntype y = async func(a: u32, b: string) -> stream;\n", + ) + .unwrap(); + + // async func with no results + roundtrip( + "package foo:bar; type z = async func();", + "package foo:bar;\n\ntype z = async func();\n", + ) + .unwrap(); + + // sync func still works (no async prefix) + roundtrip( + "package foo:bar; type w = func() -> u32;", + "package foo:bar;\n\ntype w = func() -> u32;\n", + ) + .unwrap(); + + // async func in interface export + roundtrip( + "package foo:bar; interface i { do-work: async func(input: string) -> future; }", + "package foo:bar;\n\ninterface i {\n do-work: async func(input: string) -> future;\n}\n", + ) + .unwrap(); + + // async func in world import/export + roundtrip( + "package foo:bar; world w { import run: async func(); export process: async func(data: list) -> stream; }", + "package foo:bar;\n\nworld w {\n import run: async func();\n\n export process: async func(data: list) -> stream;\n}\n", + ) + .unwrap(); + } + #[test] fn interface_roundtrip() { roundtrip( diff --git a/crates/wac-parser/src/lexer.rs b/crates/wac-parser/src/lexer.rs index 59502f45..056bf692 100644 --- a/crates/wac-parser/src/lexer.rs +++ b/crates/wac-parser/src/lexer.rs @@ -238,6 +238,18 @@ pub enum Token { /// The `targets` keyword. #[token("targets")] TargetsKeyword, + /// The `async` keyword. + #[token("async")] + AsyncKeyword, + /// The `stream` keyword. + #[token("stream")] + StreamKeyword, + /// The `future` keyword. + #[token("future")] + FutureKeyword, + /// The `error-context` keyword. + #[token("error-context")] + ErrorContextKeyword, /// The `;` symbol. #[token(";")] @@ -342,6 +354,10 @@ impl fmt::Display for Token { Token::AsKeyword => write!(f, "`as` keyword"), Token::PackageKeyword => write!(f, "`package` keyword"), Token::TargetsKeyword => write!(f, "`targets` keyword"), + Token::AsyncKeyword => write!(f, "`async` keyword"), + Token::StreamKeyword => write!(f, "`stream` keyword"), + Token::FutureKeyword => write!(f, "`future` keyword"), + Token::ErrorContextKeyword => write!(f, "`error-context` keyword"), Token::Semicolon => write!(f, "`;`"), Token::OpenBrace => write!(f, "`{{`"), Token::CloseBrace => write!(f, "`}}`"), @@ -833,6 +849,10 @@ include as package targets +async +stream +future +error-context "#, &[ (Ok(Token::ImportKeyword), "import", 1..7), @@ -874,6 +894,10 @@ targets (Ok(Token::AsKeyword), "as", 207..209), (Ok(Token::PackageKeyword), "package", 210..217), (Ok(Token::TargetsKeyword), "targets", 218..225), + (Ok(Token::AsyncKeyword), "async", 226..231), + (Ok(Token::StreamKeyword), "stream", 232..238), + (Ok(Token::FutureKeyword), "future", 239..245), + (Ok(Token::ErrorContextKeyword), "error-context", 246..259), ], ); } diff --git a/crates/wac-parser/src/resolution.rs b/crates/wac-parser/src/resolution.rs index 80694e2a..c0506a31 100644 --- a/crates/wac-parser/src/resolution.rs +++ b/crates/wac-parser/src/resolution.rs @@ -960,6 +960,7 @@ impl<'a> AstResolver<'a> { &ty.results, FuncKind::Free, None, + ty.is_async, )?), ast::ImportType::Interface(i) => { ItemKind::Instance(self.inline_interface(state, i, packages)?) @@ -1330,9 +1331,14 @@ impl<'a> AstResolver<'a> { log::debug!("resolving type alias for id `{id}`", id = alias.id.string); let ty = match &alias.kind { - ast::TypeAliasKind::Func(f) => { - Type::Func(self.func_type(state, &f.params, &f.results, FuncKind::Free, None)?) - } + ast::TypeAliasKind::Func(f) => Type::Func(self.func_type( + state, + &f.params, + &f.results, + FuncKind::Free, + None, + f.is_async, + )?), ast::TypeAliasKind::Type(ty) => match ty { ast::Type::Ident(id) => { let (item, _) = state.local_item(id)?; @@ -1390,7 +1396,7 @@ impl<'a> AstResolver<'a> { ) -> ResolutionResult { match r { ast::FuncTypeRef::Func(ty) => { - self.func_type(state, &ty.params, &ty.results, kind, None) + self.func_type(state, &ty.params, &ty.results, kind, None, ty.is_async) } ast::FuncTypeRef::Ident(id) => { let (item, _) = state.local_item(id)?; @@ -1473,6 +1479,25 @@ impl<'a> AstResolver<'a> { span: id.span, }) } + ast::Type::Stream(ty, _) => { + let ty = ty.as_ref().map(|t| Self::ty(state, t)).transpose()?; + Ok(ValueType::Defined( + state + .graph + .types_mut() + .add_defined_type(DefinedType::Stream(ty)), + )) + } + ast::Type::Future(ty, _) => { + let ty = ty.as_ref().map(|t| Self::ty(state, t)).transpose()?; + Ok(ValueType::Defined( + state + .graph + .types_mut() + .add_defined_type(DefinedType::Future(ty)), + )) + } + ast::Type::ErrorContext(_) => Ok(ValueType::Primitive(PrimitiveType::ErrorContext)), ast::Type::Ident(id) => { let (item, _) = state.local_item(id)?; let kind = item.kind(&state.graph); @@ -1627,6 +1652,7 @@ impl<'a> AstResolver<'a> { &f.results, FuncKind::Free, None, + f.is_async, )?), ast::ExternType::Interface(i) => { ItemKind::Instance(self.inline_interface(state, i, packages)?) @@ -2049,6 +2075,7 @@ impl<'a> AstResolver<'a> { &ast::ResultList::Empty, FuncKind::Constructor, Some(id), + false, )?, ) } @@ -2074,7 +2101,14 @@ impl<'a> AstResolver<'a> { ( method_extern_name(decl.id.string, method_id.string, kind), - self.func_type(state, &ty.params, &ty.results, kind, Some(id))?, + self.func_type( + state, + &ty.params, + &ty.results, + kind, + Some(id), + ty.is_async, + )?, ) } }; @@ -2093,6 +2127,7 @@ impl<'a> AstResolver<'a> { func_results: &ast::ResultList<'a>, kind: FuncKind, resource: Option, + is_async: bool, ) -> ResolutionResult { let mut params = IndexMap::new(); @@ -2133,7 +2168,7 @@ impl<'a> AstResolver<'a> { Ok(state.graph.types_mut().add_func_type(FuncType { params, result, - is_async: false, + is_async, })) } diff --git a/crates/wac-parser/tests/encoding/types.wac b/crates/wac-parser/tests/encoding/types.wac index 45c79f3e..48ec4f6c 100644 --- a/crates/wac-parser/tests/encoding/types.wac +++ b/crates/wac-parser/tests/encoding/types.wac @@ -125,3 +125,35 @@ type option-alias = option; type result-alias = result; type tuple-alias = tuple; type nested-list-alias = list>; + +// wasip3 types: stream, future, error-context +type stream1 = stream; +type stream2 = stream; +type stream3 = stream>; +type future1 = future; +type future2 = future; +type future3 = future>; +type ec = error-context; + +// async function types +type async-fn1 = async func() -> string; +type async-fn2 = async func(data: list) -> stream; +type sync-fn = func() -> string; + +// interface with async functions and wasip3 types +interface async-iface { + process: async func(input: list) -> stream; + get-result: async func() -> future; + sync-op: func(x: u32) -> u32; + type my-stream = stream; + type my-future = future; + type my-ec = error-context; +} + +// world with async imports/exports +world async-world { + import run: async func() -> string; + import fetch: async func(url: string) -> stream; + export process: async func(data: list) -> future; +} + diff --git a/crates/wac-parser/tests/encoding/types.wac.result b/crates/wac-parser/tests/encoding/types.wac.result index 6d91ecc1..2571200b 100644 --- a/crates/wac-parser/tests/encoding/types.wac.result +++ b/crates/wac-parser/tests/encoding/types.wac.result @@ -181,6 +181,74 @@ (type (;69;) (list u8)) (type (;70;) (list 69)) (export $nested-list-alias (;71;) "nested-list-alias" (type 70)) + (type (;72;) (stream)) + (export $stream1 (;73;) "stream1" (type 72)) + (type (;74;) (stream u8)) + (export $stream2 (;75;) "stream2" (type 74)) + (type (;76;) (list string)) + (type (;77;) (stream 76)) + (export $stream3 (;78;) "stream3" (type 77)) + (type (;79;) (future)) + (export $future1 (;80;) "future1" (type 79)) + (type (;81;) (future u32)) + (export $future2 (;82;) "future2" (type 81)) + (type (;83;) (option string)) + (type (;84;) (future 83)) + (export $future3 (;85;) "future3" (type 84)) + (type (;86;) error-context) + (export $ec (;87;) "ec" (type 86)) + (type (;88;) (func async (result string))) + (export $async-fn1 (;89;) "async-fn1" (type 88)) + (type (;90;) (list u8)) + (type (;91;) (stream u8)) + (type (;92;) (func async (param "data" 90) (result 91))) + (export $async-fn2 (;93;) "async-fn2" (type 92)) + (type (;94;) (func (result string))) + (export $sync-fn (;95;) "sync-fn" (type 94)) + (type (;96;) + (component + (type (;0;) + (instance + (type (;0;) (list u8)) + (type (;1;) (stream u8)) + (type (;2;) (func async (param "input" 0) (result 1))) + (export (;0;) "process" (func (type 2))) + (type (;3;) (future string)) + (type (;4;) (func async (result 3))) + (export (;1;) "get-result" (func (type 4))) + (type (;5;) (func (param "x" u32) (result u32))) + (export (;2;) "sync-op" (func (type 5))) + (type (;6;) (stream u32)) + (export (;7;) "my-stream" (type (eq 6))) + (type (;8;) (future string)) + (export (;9;) "my-future" (type (eq 8))) + (type (;10;) error-context) + (export (;11;) "my-ec" (type (eq 10))) + ) + ) + (export (;0;) "test:pkg/async-iface" (instance (type 0))) + ) + ) + (export $async-iface (;97;) "async-iface" (type 96)) + (type (;98;) + (component + (type (;0;) + (component + (type (;0;) (func async (result string))) + (import "run" (func (;0;) (type 0))) + (type (;1;) (stream u8)) + (type (;2;) (func async (param "url" string) (result 1))) + (import "fetch" (func (;1;) (type 2))) + (type (;3;) (list u8)) + (type (;4;) (future u32)) + (type (;5;) (func async (param "data" 3) (result 4))) + (export (;2;) "process" (func (type 5))) + ) + ) + (export (;0;) "test:pkg/async-world" (component (type 0))) + ) + ) + (export $async-world (;99;) "async-world" (type 98)) (@producers (processed-by "wac-parser" "0.10.1") ) diff --git a/crates/wac-parser/tests/parser/fail/async-not-func.wac b/crates/wac-parser/tests/parser/fail/async-not-func.wac new file mode 100644 index 00000000..026b1faf --- /dev/null +++ b/crates/wac-parser/tests/parser/fail/async-not-func.wac @@ -0,0 +1,3 @@ +package test:comp; + +type bad = async u32; diff --git a/crates/wac-parser/tests/parser/fail/async-not-func.wac.result b/crates/wac-parser/tests/parser/fail/async-not-func.wac.result new file mode 100644 index 00000000..1bd5f86c --- /dev/null +++ b/crates/wac-parser/tests/parser/fail/async-not-func.wac.result @@ -0,0 +1,9 @@ +failed to parse document + + × expected `func` keyword, found `u32` keyword + ╭─[tests/parser/fail/async-not-func.wac:3:18] + 2 │ + 3 │ type bad = async u32; + · ─┬─ + · ╰── unexpected `u32` keyword + ╰──── diff --git a/crates/wac-parser/tests/parser/fail/expected-multiple.wac.result b/crates/wac-parser/tests/parser/fail/expected-multiple.wac.result index 1db1800b..74c57ede 100644 --- a/crates/wac-parser/tests/parser/fail/expected-multiple.wac.result +++ b/crates/wac-parser/tests/parser/fail/expected-multiple.wac.result @@ -1,6 +1,6 @@ failed to parse document - × expected either `func` keyword, `u8` keyword, `s8` keyword, `u16` keyword, `s16` keyword, `u32` keyword, `s32` keyword, `u64` keyword, `s64` keyword, `f32` keyword, or more..., found end of + × expected either `async` keyword, `func` keyword, `u8` keyword, `s8` keyword, `u16` keyword, `s16` keyword, `u32` keyword, `s32` keyword, `u64` keyword, `s64` keyword, or more..., found end of │ input ╭─[tests/parser/fail/expected-multiple.wac:3:8] 2 │ diff --git a/crates/wac-parser/tests/parser/fail/future-missing-close.wac b/crates/wac-parser/tests/parser/fail/future-missing-close.wac new file mode 100644 index 00000000..8d020fae --- /dev/null +++ b/crates/wac-parser/tests/parser/fail/future-missing-close.wac @@ -0,0 +1,3 @@ +package test:comp; + +type bad = future`, found `;` + ╭─[tests/parser/fail/future-missing-close.wac:3:25] + 2 │ + 3 │ type bad = future`, found `;` + ╭─[tests/parser/fail/stream-missing-close.wac:3:21] + 2 │ + 3 │ type bad = stream string; +}; + +let i = new foo:bar { x }; diff --git a/crates/wac-parser/tests/resolution/fail/mismatched-async.wac.result b/crates/wac-parser/tests/resolution/fail/mismatched-async.wac.result new file mode 100644 index 00000000..3f3f39f7 --- /dev/null +++ b/crates/wac-parser/tests/resolution/fail/mismatched-async.wac.result @@ -0,0 +1,11 @@ +failed to resolve document + + × mismatched instantiation argument `x` + ├─▶ mismatched type for export `f` + ╰─▶ expected async function, found sync function + ╭─[tests/resolution/fail/mismatched-async.wac:7:23] + 6 │ + 7 │ let i = new foo:bar { x }; + · ┬ + · ╰── mismatched argument `x` + ╰──── diff --git a/crates/wac-parser/tests/resolution/fail/mismatched-async/foo/bar.wat b/crates/wac-parser/tests/resolution/fail/mismatched-async/foo/bar.wat new file mode 100644 index 00000000..d9725477 --- /dev/null +++ b/crates/wac-parser/tests/resolution/fail/mismatched-async/foo/bar.wat @@ -0,0 +1,6 @@ +(component + (instance (import "x") + (type (func async (result string))) + (export "f" (func (type 0))) + ) +) diff --git a/crates/wac-parser/tests/resolution/wasip3-types.wac b/crates/wac-parser/tests/resolution/wasip3-types.wac new file mode 100644 index 00000000..b22015a1 --- /dev/null +++ b/crates/wac-parser/tests/resolution/wasip3-types.wac @@ -0,0 +1,34 @@ +package test:comp; + +// wasip3 value types: stream, future, error-context +type stream1 = stream; +type stream2 = stream; +type stream3 = stream>; + +type future1 = future; +type future2 = future; +type future3 = future>; + +type ec = error-context; + +// async function types +type async-fn = async func() -> string; +type async-fn-with-stream = async func(data: list) -> stream; +type sync-fn = func() -> string; + +/// Interface mixing async and sync functions and wasip3 types +interface async-iface { + process: async func(input: list) -> stream; + get-result: async func() -> future; + sync-op: func(x: u32) -> u32; + type my-stream = stream; + type my-future = future; + type my-ec = error-context; +} + +/// World with async imports and exports +world async-world { + import run: async func() -> string; + import fetch: async func(url: string) -> stream; + export process: async func(data: list) -> future; +} diff --git a/crates/wac-parser/tests/resolution/wasip3-types.wac.result b/crates/wac-parser/tests/resolution/wasip3-types.wac.result new file mode 100644 index 00000000..7484dbdd --- /dev/null +++ b/crates/wac-parser/tests/resolution/wasip3-types.wac.result @@ -0,0 +1,14 @@ +digraph { + 0 [ label = "type definition \"stream1\""; kind = "stream"; export = "stream1"] + 1 [ label = "type definition \"stream2\""; kind = "stream"; export = "stream2"] + 2 [ label = "type definition \"stream3\""; kind = "stream"; export = "stream3"] + 3 [ label = "type definition \"future1\""; kind = "future"; export = "future1"] + 4 [ label = "type definition \"future2\""; kind = "future"; export = "future2"] + 5 [ label = "type definition \"future3\""; kind = "future"; export = "future3"] + 6 [ label = "type definition \"ec\""; kind = "error-context"; export = "ec"] + 7 [ label = "type definition \"async-fn\""; kind = "function type"; export = "async-fn"] + 8 [ label = "type definition \"async-fn-with-stream\""; kind = "function type"; export = "async-fn-with-stream"] + 9 [ label = "type definition \"sync-fn\""; kind = "function type"; export = "sync-fn"] + 10 [ label = "type definition \"async-iface\""; kind = "interface"; export = "async-iface"] + 11 [ label = "type definition \"async-world\""; kind = "world"; export = "async-world"] +} From 21d002b3c3bd90ae0af4ffb039416ce9eead4c05 Mon Sep 17 00:00:00 2001 From: Ryan P Date: Tue, 14 Jul 2026 10:20:59 -0400 Subject: [PATCH 2/2] parse_optional_payload helper, negative tests --- crates/wac-parser/src/ast.rs | 24 ++++++++++++ crates/wac-parser/src/ast/type.rs | 38 ++----------------- .../parser/fail/error-context-payload.wac | 3 ++ .../fail/error-context-payload.wac.result | 9 +++++ .../parser/fail/future-empty-payload.wac | 3 ++ .../fail/future-empty-payload.wac.result | 9 +++++ .../parser/fail/stream-empty-payload.wac | 3 ++ .../fail/stream-empty-payload.wac.result | 9 +++++ 8 files changed, 64 insertions(+), 34 deletions(-) create mode 100644 crates/wac-parser/tests/parser/fail/error-context-payload.wac create mode 100644 crates/wac-parser/tests/parser/fail/error-context-payload.wac.result create mode 100644 crates/wac-parser/tests/parser/fail/future-empty-payload.wac create mode 100644 crates/wac-parser/tests/parser/fail/future-empty-payload.wac.result create mode 100644 crates/wac-parser/tests/parser/fail/stream-empty-payload.wac create mode 100644 crates/wac-parser/tests/parser/fail/stream-empty-payload.wac.result diff --git a/crates/wac-parser/src/ast.rs b/crates/wac-parser/src/ast.rs index 1698c3b3..a8b3b9df 100644 --- a/crates/wac-parser/src/ast.rs +++ b/crates/wac-parser/src/ast.rs @@ -188,6 +188,30 @@ where } } +/// Consumes the current keyword token, parses an optional `` payload, then +/// builds the type using it's constructor. +pub fn parse_optional_payload<'a>( + lexer: &mut Lexer<'a>, + ctor: impl FnOnce(Option>>, SourceSpan) -> Type<'a>, +) -> ParseResult> { + let span = lexer.next().unwrap().1; + let ty = parse_optional(lexer, Token::OpenAngle, |lexer| { + let ty = Box::new(Parse::parse(lexer)?); + let close = parse_token(lexer, Token::CloseAngle)?; + Ok((ty, close)) + })?; + Ok(match ty { + Some((ty, close)) => ctor( + Some(ty), + SourceSpan::new( + span.offset().into(), + (close.offset() + close.len()) - span.offset(), + ), + ), + None => ctor(None, span), + }) +} + /// Used to look ahead one token in the lexer. /// /// The lookahead stores up to 10 attempted tokens. diff --git a/crates/wac-parser/src/ast/type.rs b/crates/wac-parser/src/ast/type.rs index 580cba43..622fd2d8 100644 --- a/crates/wac-parser/src/ast/type.rs +++ b/crates/wac-parser/src/ast/type.rs @@ -1,6 +1,6 @@ use super::{ - parse_delimited, parse_optional, parse_token, DocComment, Error, Ident, Lookahead, PackagePath, - Parse, ParseResult, Peek, + parse_delimited, parse_optional, parse_optional_payload, parse_token, DocComment, Error, Ident, + Lookahead, PackagePath, Parse, ParseResult, Peek, }; use crate::lexer::{Lexer, Token}; use miette::SourceSpan; @@ -834,39 +834,9 @@ impl<'a> Parse<'a> for Type<'a> { ), )) } else if lookahead.peek(Token::StreamKeyword) { - let span = lexer.next().unwrap().1; - let ty = parse_optional(lexer, Token::OpenAngle, |lexer| { - let ty = Box::new(Parse::parse(lexer)?); - let close = parse_token(lexer, Token::CloseAngle)?; - Ok((ty, close)) - })?; - match ty { - Some((ty, close)) => Ok(Self::Stream( - Some(ty), - SourceSpan::new( - span.offset().into(), - (close.offset() + close.len()) - span.offset(), - ), - )), - None => Ok(Self::Stream(None, span)), - } + parse_optional_payload(lexer, Self::Stream) } else if lookahead.peek(Token::FutureKeyword) { - let span = lexer.next().unwrap().1; - let ty = parse_optional(lexer, Token::OpenAngle, |lexer| { - let ty = Box::new(Parse::parse(lexer)?); - let close = parse_token(lexer, Token::CloseAngle)?; - Ok((ty, close)) - })?; - match ty { - Some((ty, close)) => Ok(Self::Future( - Some(ty), - SourceSpan::new( - span.offset().into(), - (close.offset() + close.len()) - span.offset(), - ), - )), - None => Ok(Self::Future(None, span)), - } + parse_optional_payload(lexer, Self::Future) } else if lookahead.peek(Token::ErrorContextKeyword) { Ok(Self::ErrorContext(lexer.next().unwrap().1)) } else if Ident::peek(&mut lookahead) { diff --git a/crates/wac-parser/tests/parser/fail/error-context-payload.wac b/crates/wac-parser/tests/parser/fail/error-context-payload.wac new file mode 100644 index 00000000..8b2eabf6 --- /dev/null +++ b/crates/wac-parser/tests/parser/fail/error-context-payload.wac @@ -0,0 +1,3 @@ +package test:comp; + +type bad = error-context; diff --git a/crates/wac-parser/tests/parser/fail/error-context-payload.wac.result b/crates/wac-parser/tests/parser/fail/error-context-payload.wac.result new file mode 100644 index 00000000..f883afd3 --- /dev/null +++ b/crates/wac-parser/tests/parser/fail/error-context-payload.wac.result @@ -0,0 +1,9 @@ +failed to parse document + + × expected `;`, found `<` + ╭─[tests/parser/fail/error-context-payload.wac:3:25] + 2 │ + 3 │ type bad = error-context; + · ┬ + · ╰── unexpected `<` + ╰──── diff --git a/crates/wac-parser/tests/parser/fail/future-empty-payload.wac b/crates/wac-parser/tests/parser/fail/future-empty-payload.wac new file mode 100644 index 00000000..6b370e91 --- /dev/null +++ b/crates/wac-parser/tests/parser/fail/future-empty-payload.wac @@ -0,0 +1,3 @@ +package test:comp; + +type bad = future<>; diff --git a/crates/wac-parser/tests/parser/fail/future-empty-payload.wac.result b/crates/wac-parser/tests/parser/fail/future-empty-payload.wac.result new file mode 100644 index 00000000..97912951 --- /dev/null +++ b/crates/wac-parser/tests/parser/fail/future-empty-payload.wac.result @@ -0,0 +1,9 @@ +failed to parse document + + × expected either `u8` keyword, `s8` keyword, `u16` keyword, `s16` keyword, `u32` keyword, `s32` keyword, `u64` keyword, `s64` keyword, `f32` keyword, `f64` keyword, or more..., found `>` + ╭─[tests/parser/fail/future-empty-payload.wac:3:19] + 2 │ + 3 │ type bad = future<>; + · ┬ + · ╰── unexpected `>` + ╰──── diff --git a/crates/wac-parser/tests/parser/fail/stream-empty-payload.wac b/crates/wac-parser/tests/parser/fail/stream-empty-payload.wac new file mode 100644 index 00000000..ae84dc5c --- /dev/null +++ b/crates/wac-parser/tests/parser/fail/stream-empty-payload.wac @@ -0,0 +1,3 @@ +package test:comp; + +type bad = stream<>; diff --git a/crates/wac-parser/tests/parser/fail/stream-empty-payload.wac.result b/crates/wac-parser/tests/parser/fail/stream-empty-payload.wac.result new file mode 100644 index 00000000..823c6f16 --- /dev/null +++ b/crates/wac-parser/tests/parser/fail/stream-empty-payload.wac.result @@ -0,0 +1,9 @@ +failed to parse document + + × expected either `u8` keyword, `s8` keyword, `u16` keyword, `s16` keyword, `u32` keyword, `s32` keyword, `u64` keyword, `s64` keyword, `f32` keyword, `f64` keyword, or more..., found `>` + ╭─[tests/parser/fail/stream-empty-payload.wac:3:19] + 2 │ + 3 │ type bad = stream<>; + · ┬ + · ╰── unexpected `>` + ╰────