Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 52 additions & 101 deletions crates/core/src/async_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,66 +3,52 @@ use std::collections::HashSet;
use std::fmt;
use wit_parser::{Function, FunctionKind, Resolve, WorldKey};

/// Structure used to parse the command line argument `--async` consistently
/// Structure used to parse the command line argument `--sync` consistently
/// across guest generators.
#[cfg_attr(feature = "clap", derive(clap::Parser))]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
#[derive(Clone, Default, Debug)]
pub struct AsyncFilterSet {
/// Determines which functions to lift or lower `async`, if any.
/// Determines which `async` functions to lift or lower synchronously, if
/// any.
///
/// This option can be passed multiple times and additionally accepts
/// comma-separated values for each option passed. Each individual argument
/// passed here can be one of:
///
/// - `all` - all imports and exports will be async
/// - `-all` - force all imports and exports to be sync
/// - `foo:bar/baz#method` - force this method to be async
/// - `import:foo:bar/baz#method` - force this method to be async, but only
/// - `all` - all imports and exports will be sync
/// - `foo:bar/baz#method` - force this method to be sync
/// - `import:foo:bar/baz#method` - force this method to be sync, but only
/// as an import
/// - `-export:foo:bar/baz#method` - force this export to be sync
/// - `export:foo:bar/baz#method` - force this method to be sync, but only
/// as an export
///
/// If a method is not listed in this option then the WIT's default bindings
/// mode will be used. If the WIT function is defined as `async` then async
/// bindings will be generated, otherwise sync bindings will be generated.
///
/// Options are processed in the order they are passed here, so if a method
/// matches two directives passed the least-specific one should be last.
/// Functions defined as `async` in WIT get async bindings unless they are
/// listed here. Functions not defined as `async` always get sync bindings,
/// as the component model does not allow lifting or lowering them async.
#[cfg_attr(
feature = "clap",
arg(
long = "async",
value_parser = parse_async,
long = "sync",
value_parser = parse_sync,
value_delimiter =',',
value_name = "FILTER",
),
)]
#[cfg_attr(feature = "serde", serde(rename = "async"))]
async_: Vec<Async>,
#[cfg_attr(feature = "serde", serde(rename = "sync"))]
sync: Vec<SyncFilter>,

#[cfg_attr(feature = "clap", arg(skip))]
#[cfg_attr(feature = "serde", serde(skip))]
used_options: HashSet<usize>,
}

#[cfg(feature = "clap")]
fn parse_async(s: &str) -> Result<Async, String> {
Ok(Async::parse(s))
fn parse_sync(s: &str) -> Result<SyncFilter, String> {
Ok(SyncFilter::parse(s))
}

impl AsyncFilterSet {
/// Returns a set where all functions should be async or not depending on
/// `async_` provided.
pub fn all(async_: bool) -> AsyncFilterSet {
AsyncFilterSet {
async_: vec![Async {
enabled: async_,
filter: AsyncFilter::All,
}],
used_options: HashSet::new(),
}
}

/// Returns whether the `func` provided is to be bound `async` or not.
pub fn is_async(
&mut self,
Expand All @@ -75,31 +61,22 @@ impl AsyncFilterSet {
Some(key) => format!("{}#{}", resolve.name_world_key(key), func.name),
None => func.name.clone(),
};
for (i, opt) in self.async_.iter().enumerate() {
let name = match &opt.filter {
AsyncFilter::All => {
self.used_options.insert(i);
return opt.enabled;
}
AsyncFilter::Function(s) => s,
AsyncFilter::Import(s) => {
if !is_import {
continue;
}
s
}
AsyncFilter::Export(s) => {
if is_import {
continue;
}
s
}
let mut sync = false;
for (i, filter) in self.sync.iter().enumerate() {
let matches = match filter {
SyncFilter::All => true,
SyncFilter::Function(s) => *s == name_to_test,
SyncFilter::Import(s) => is_import && *s == name_to_test,
SyncFilter::Export(s) => !is_import && *s == name_to_test,
};
if *name == name_to_test {
if matches {
self.used_options.insert(i);
return opt.enabled;
sync = true;
}
}
if sync {
return false;
}

match &func.kind {
FunctionKind::Freestanding
Expand All @@ -115,86 +92,60 @@ impl AsyncFilterSet {
/// Intended to be used in the header comment of generated code to help
/// indicate what options were specified.
pub fn debug_opts(&self) -> impl Iterator<Item = String> + '_ {
self.async_.iter().map(|opt| opt.to_string())
self.sync.iter().map(|filter| filter.to_string())
}

/// Tests whether all `--async` options were used throughout bindings
/// Tests whether all `--sync` options were used throughout bindings
/// generation, returning an error if any were unused.
pub fn ensure_all_used(&self) -> Result<()> {
for (i, opt) in self.async_.iter().enumerate() {
for (i, filter) in self.sync.iter().enumerate() {
if self.used_options.contains(&i) {
continue;
}
if !matches!(opt.filter, AsyncFilter::All) {
bail!("unused async option: {opt}");
if !matches!(filter, SyncFilter::All) {
bail!("unused sync option: {filter}");
}
}
Ok(())
}

/// Returns whether any option explicitly requests that async is enabled.
pub fn any_enabled(&self) -> bool {
self.async_.iter().any(|o| o.enabled)
}

/// Pushes a new option into this set.
pub fn push(&mut self, directive: &str) {
self.async_.push(Async::parse(directive));
self.sync.push(SyncFilter::parse(directive));
}
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
struct Async {
enabled: bool,
filter: AsyncFilter,
enum SyncFilter {
All,
Function(String),
Import(String),
Export(String),
}

impl Async {
fn parse(s: &str) -> Async {
let (s, enabled) = match s.strip_prefix('-') {
Some(s) => (s, false),
None => (s, true),
};
let filter = match s {
"all" => AsyncFilter::All,
impl SyncFilter {
fn parse(s: &str) -> SyncFilter {
match s {
"all" => SyncFilter::All,
other => match other.strip_prefix("import:") {
Some(s) => AsyncFilter::Import(s.to_string()),
Some(s) => SyncFilter::Import(s.to_string()),
None => match other.strip_prefix("export:") {
Some(s) => AsyncFilter::Export(s.to_string()),
None => AsyncFilter::Function(s.to_string()),
Some(s) => SyncFilter::Export(s.to_string()),
None => SyncFilter::Function(s.to_string()),
},
},
};
Async { enabled, filter }
}
}

impl fmt::Display for Async {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.enabled {
write!(f, "-")?;
}
self.filter.fmt(f)
}
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize))]
enum AsyncFilter {
All,
Function(String),
Import(String),
Export(String),
}

impl fmt::Display for AsyncFilter {
impl fmt::Display for SyncFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AsyncFilter::All => write!(f, "all"),
AsyncFilter::Function(s) => write!(f, "{s}"),
AsyncFilter::Import(s) => write!(f, "import:{s}"),
AsyncFilter::Export(s) => write!(f, "export:{s}"),
SyncFilter::All => write!(f, "all"),
SyncFilter::Function(s) => write!(f, "{s}"),
SyncFilter::Import(s) => write!(f, "import:{s}"),
SyncFilter::Export(s) => write!(f, "export:{s}"),
}
}
}
33 changes: 21 additions & 12 deletions crates/guest-rust/macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ impl Parse for Config {
let mut world = None;
let mut source = None;
let mut features = Vec::new();
let mut async_configured = false;
let mut sync_configured = false;
let mut method_chaining_configured = false;
let mut debug = false;

Expand Down Expand Up @@ -184,11 +184,11 @@ impl Parse for Config {
Opt::Debug(enable) => {
debug = enable.value();
}
Opt::Async(val, span) => {
if async_configured {
return Err(Error::new(span, "cannot specify second async config"));
Opt::Sync(val, span) => {
if sync_configured {
return Err(Error::new(span, "cannot specify second sync config"));
}
async_configured = true;
sync_configured = true;
opts.async_ = val;
}
Opt::ChainableMethods(val, span) => {
Expand Down Expand Up @@ -366,6 +366,7 @@ mod kw {
syn::custom_keyword!(imports);
syn::custom_keyword!(debug);
syn::custom_keyword!(chainable_methods);
syn::custom_keyword!(sync);
syn::custom_keyword!(merge_structurally_equal_types);
}

Expand Down Expand Up @@ -447,7 +448,7 @@ enum Opt {
GenerateUnusedTypes(syn::LitBool),
Features(Vec<syn::LitStr>),
DisableCustomSectionLinkHelpers(syn::LitBool),
Async(AsyncFilterSet, Span),
Sync(AsyncFilterSet, Span),
Debug(syn::LitBool),
ChainableMethods(ChainableMethodFilterSet, Span),
MergeStructurallyEqualTypes(syn::LitBool),
Expand Down Expand Up @@ -646,21 +647,29 @@ impl Parse for Opt {
set.push(&val.value());
}
Ok(Opt::ChainableMethods(set, span))
} else if l.peek(Token![async]) {
let span = input.parse::<Token![async]>()?.span;
} else if l.peek(kw::sync) {
let span = input.parse::<kw::sync>()?.span;
input.parse::<Token![:]>()?;
let mut set = AsyncFilterSet::default();
if input.peek(syn::LitBool) {
let enabled = input.parse::<syn::LitBool>()?.value;
Ok(Opt::Async(AsyncFilterSet::all(enabled), span))
if input.parse::<syn::LitBool>()?.value {
set.push("all");
}
} else {
let mut set = AsyncFilterSet::default();
let contents;
syn::bracketed!(contents in input);
for val in contents.parse_terminated(|p| p.parse::<syn::LitStr>(), Token![,])? {
set.push(&val.value());
}
Ok(Opt::Async(set, span))
}
Ok(Opt::Sync(set, span))
} else if input.peek(Token![async]) {
let span = input.parse::<Token![async]>()?.span;
Err(Error::new(
span,
"the `async` option has been removed: functions that are `async` \
in WIT get async bindings by default, and `sync` opts them out",
))
} else if l.peek(kw::merge_structurally_equal_types) {
input.parse::<kw::merge_structurally_equal_types>()?;
input.parse::<Token![:]>()?;
Expand Down
22 changes: 9 additions & 13 deletions crates/guest-rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -860,22 +860,18 @@ extern crate std;
/// // the specific `generate!` call where it is used.
/// debug: true,
///
/// // Generate async import and/or export bindings.
/// // Generate sync bindings for functions that are `async` in WIT.
/// //
/// // The resulting bindings will use the component model
/// // [async ABI](https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md).
/// //
/// // If this option is not provided then the WIT's source annotation will
/// // be used instead.
/// async: true, // all bindings are async
/// async: false, // all bindings are sync
/// // With an array per-function configuration can be specified. A leading
/// // '-' will disable async for that particular function.
/// async: [
/// // Functions that are `async` in WIT get bindings using the component
/// // model [async ABI](https://github.com/WebAssembly/component-model/blob/main/design/mvp/Async.md)
/// // by default, and all other functions get sync bindings. This option
/// // switches `async` functions back to sync bindings.
/// sync: true, // all bindings are sync
/// // With an array per-function configuration can be specified.
/// sync: [
/// "wasi:http/types@0.3.0-draft#[static]body.finish",
/// "import:wasi:http/handler@0.3.0-draft#handle",
/// "-export:wasi:http/handler@0.3.0-draft#handle",
/// "all",
/// "export:wasi:http/handler@0.3.0-draft#handle",
/// ],
///
/// // All resource methods with empty returns are instead generated as
Expand Down
16 changes: 8 additions & 8 deletions crates/moonbit/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3257,16 +3257,16 @@ mod tests {
}

#[test]
fn async_filters_respect_import_export_direction() {
fn sync_filters_respect_import_export_direction() {
// The world both imports and exports `run` so that each directional
// filter below matches something; an unmatched filter is now an error
// (see `AsyncFilterSet::ensure_all_used`), which would mask what this
// test is actually checking.
let wit = r#"
package a:b;
world runner {
import run: func();
export run: func();
import run: async func();
export run: async func();
}
"#;

Expand All @@ -3278,8 +3278,9 @@ mod tests {
let import_files = try_generate_with_opts(wit, "runner", import_opts).unwrap();
let import = file(&import_files, "world/runner/import.mbt");
let import_ffi = file(&import_files, "world/runner/ffi_import.mbt");
assert!(import.contains("pub async fn run("), "{import}");
assert!(import_ffi.contains("[async-lower]run"), "{import_ffi}");
assert!(import.contains("pub fn run("), "{import}");
assert!(!import.contains("pub async fn run("), "{import}");
assert!(!import_ffi.contains("[async-lower]run"), "{import_ffi}");

let mut export_opts = Opts {
gen_dir: "gen".into(),
Expand All @@ -3289,9 +3290,8 @@ mod tests {
let export_files = try_generate_with_opts(wit, "runner", export_opts).unwrap();
let import = file(&export_files, "world/runner/import.mbt");
let import_ffi = file(&export_files, "world/runner/ffi_import.mbt");
assert!(import.contains("pub fn run("), "{import}");
assert!(!import.contains("pub async fn run("), "{import}");
assert!(!import_ffi.contains("[async-lower]run"), "{import_ffi}");
assert!(import.contains("pub async fn run("), "{import}");
assert!(import_ffi.contains("[async-lower]run"), "{import_ffi}");
}

#[test]
Expand Down
Loading
Loading