Skip to content

Commit

Permalink
feat: preliminary support for WIT templates
Browse files Browse the repository at this point in the history
This is a minimum viable implementation of WIT templates per
WebAssembly/component-model#172.  It
supports interfaces with at most one wildcard function, which may be
expanded (i.e. monomorphized) with a set of substitutions provided by
the application developer when generating guest bindings.  I will be
posting separate PRs to the `wit-bindgen` and `wasmtime` repos to
implement and test guest and host binding generation, respectively.

Signed-off-by: Joel Dice <[email protected]>
  • Loading branch information
fibonacci1729 authored and dicej committed Mar 22, 2023
1 parent 7108f55 commit 8095934
Show file tree
Hide file tree
Showing 6 changed files with 164 additions and 16 deletions.
2 changes: 2 additions & 0 deletions crates/wit-component/src/decoding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,7 @@ impl WitPackageDecoder<'_> {
types: IndexMap::default(),
functions: IndexMap::new(),
document: doc,
wildcard: None,
})
});
Ok(interface)
Expand All @@ -624,6 +625,7 @@ impl WitPackageDecoder<'_> {
types: IndexMap::default(),
functions: IndexMap::new(),
document: doc,
wildcard: None,
};

for (name, export_url, ty) in ty.exports(self.info.types.as_ref()) {
Expand Down
41 changes: 38 additions & 3 deletions crates/wit-parser/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ use std::path::{Path, PathBuf};

pub mod lex;

pub use expand::expand;
mod expand;

pub use resolve::Resolver;
mod resolve;
pub mod toposort;
Expand Down Expand Up @@ -464,10 +467,31 @@ struct Stream<'a> {

pub struct Value<'a> {
docs: Docs<'a>,
name: Id<'a>,
name: VariableId<'a>,
kind: ValueKind<'a>,
}

impl<'a> Value<'a> {
pub(crate) fn name(&self) -> &'a str {
match &self.name {
VariableId::Id(id) => id.name,
VariableId::Wildcard(_) => "*",
}
}

pub(crate) fn span(&self) -> Span {
match self.name {
VariableId::Id(Id { span, .. }) => span,
VariableId::Wildcard(span) => span,
}
}
}

pub enum VariableId<'a> {
Wildcard(Span), // `*`
Id(Id<'a>),
}

struct Union<'a> {
span: Span,
cases: Vec<UnionCase<'a>>,
Expand Down Expand Up @@ -552,7 +576,7 @@ impl<'a> InterfaceItem<'a> {
Some((_span, Token::Union)) => {
TypeDef::parse_union(tokens, docs).map(InterfaceItem::TypeDef)
}
Some((_span, Token::Id)) | Some((_span, Token::ExplicitId)) => {
Some((_span, Token::Star | Token::Id | Token::ExplicitId)) => {
Value::parse(tokens, docs).map(InterfaceItem::Value)
}
Some((_span, Token::Use)) => Use::parse(tokens).map(InterfaceItem::Use),
Expand Down Expand Up @@ -670,13 +694,24 @@ impl<'a> TypeDef<'a> {

impl<'a> Value<'a> {
fn parse(tokens: &mut Tokenizer<'a>, docs: Docs<'a>) -> Result<Self> {
let name = parse_id(tokens)?;
let name = parse_variable_id(tokens)?;
tokens.expect(Token::Colon)?;
let kind = ValueKind::parse(tokens)?;
Ok(Value { docs, name, kind })
}
}

fn parse_variable_id<'a>(tokens: &mut Tokenizer<'a>) -> Result<VariableId<'a>> {
match tokens.clone().next()? {
Some((_span, Token::Id | Token::ExplicitId)) => parse_id(tokens).map(VariableId::Id),
Some((span, Token::Star)) => {
tokens.expect(Token::Star)?;
Ok(VariableId::Wildcard(span))
}
other => Err(err_expected(tokens, "an identifier or `*`", other).into()),
}
}

fn parse_id<'a>(tokens: &mut Tokenizer<'a>) -> Result<Id<'a>> {
match tokens.next()? {
Some((span, Token::Id)) => Ok(Id {
Expand Down
90 changes: 90 additions & 0 deletions crates/wit-parser/src/ast/expand.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
use crate::{Interface, InterfaceId, Resolve, WorldItem};
use anyhow::{anyhow, Result};
use id_arena::Arena;
use indexmap::IndexMap;
use std::collections::{HashMap, HashSet};

pub type Substitutions = HashMap<String, HashMap<String, HashSet<String>>>;

pub fn expand(resolve: &mut Resolve, mut substitutions: Substitutions) -> Result<()> {
let mut new_interfaces = resolve.interfaces.clone();
for (_, world) in &mut resolve.worlds {
let mut subs = substitutions.remove(&world.name).unwrap_or_default();
expand_interfaces(
&world.name,
"imports",
&mut world.imports,
&mut new_interfaces,
&mut subs,
)?;
expand_interfaces(
&world.name,
"exports",
&mut world.exports,
&mut new_interfaces,
&mut subs,
)?;
}

if !substitutions.is_empty() {
log::warn!("unused substitutions were provided: {substitutions:?}",);
}

resolve.interfaces = new_interfaces;

Ok(())
}

fn expand_interfaces(
world_name: &str,
desc: &str,
items: &mut IndexMap<String, WorldItem>,
new_interfaces: &mut Arena<Interface>,
substitutions: &mut HashMap<String, HashSet<String>>,
) -> Result<()> {
for (name, item) in items {
if let WorldItem::Interface(interface) = item {
if new_interfaces[*interface].wildcard.is_some() {
let new_interface = expand_interface(
*interface,
new_interfaces,
substitutions.remove(name).ok_or_else(|| {
anyhow!(
"world {world_name} {desc} item {name} contains wildcards \
but no substitutions were provided",
)
})?,
);
*interface = new_interfaces.alloc(new_interface);
}
}
}

if !substitutions.is_empty() {
log::warn!("unused substitutions were provided for world {world_name}: {substitutions:?}",);
}

Ok(())
}

fn expand_interface(
interface: InterfaceId,
new_interfaces: &Arena<Interface>,
substitutions: HashSet<String>,
) -> Interface {
let mut new_interface = new_interfaces[interface].clone();
// Make the expanded interface anonymous; otherwise the generated component type will fail to validate due to
// the existence of multiple interfaces with the same name.
//
// TODO: implement something like
// https://github.com/WebAssembly/component-model/issues/172#issuecomment-1466939890, which may entail changes
// to how interfaces are modeled in WIT, `Resolve`, and the component model.
new_interface.name = None;
let function = new_interface.wildcard.take().unwrap();
for var_name in substitutions {
let mut new_func = function.clone();
new_func.name = var_name.clone();
assert!(new_interface.functions.insert(var_name, new_func).is_none());
}
new_interface
}
39 changes: 27 additions & 12 deletions crates/wit-parser/src/ast/resolve.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::{Error, ParamList, ResultList, ValueKind};
use super::{Error, ParamList, ResultList, ValueKind, VariableId};
use crate::ast::toposort::toposort;
use crate::*;
use anyhow::{anyhow, bail, Result};
Expand Down Expand Up @@ -186,6 +186,7 @@ impl<'a> Resolver<'a> {
docs: Docs::default(),
document: doc,
functions: IndexMap::new(),
wildcard: None,
});
DocumentItem::Interface(id)
});
Expand All @@ -205,6 +206,7 @@ impl<'a> Resolver<'a> {
docs: Docs::default(),
document: doc,
functions: IndexMap::new(),
wildcard: None,
})
}),
};
Expand Down Expand Up @@ -531,6 +533,7 @@ impl<'a> Resolver<'a> {
name: name.map(|s| s.to_string()),
functions: IndexMap::new(),
types: IndexMap::new(),
wildcard: None,
});
if let Some(name) = name {
self.document_interfaces[document.index()]
Expand Down Expand Up @@ -563,11 +566,21 @@ impl<'a> Resolver<'a> {
match field {
ast::InterfaceItem::Value(value) => match &value.kind {
ValueKind::Func(func) => {
self.define_interface_name(&value.name, TypeOrItem::Item("function"))?;
let func = self.resolve_function(&value.docs, value.name.name, func)?;
let prev = self.interfaces[interface_id]
.functions
.insert(value.name.name.to_string(), func);
if !matches!(value.name, VariableId::Wildcard(_)) {
self.define_interface_name(
value.name(),
value.span(),
TypeOrItem::Item("function"),
)?;
}
let func = self.resolve_function(&value.docs, value.name(), func)?;
let prev = if matches!(value.name, VariableId::Wildcard(_)) {
self.interfaces[interface_id].wildcard.replace(func)
} else {
self.interfaces[interface_id]
.functions
.insert(value.name().to_string(), func)
};
assert!(prev.is_none());
}
},
Expand Down Expand Up @@ -646,7 +659,9 @@ impl<'a> Resolver<'a> {
name: Some(def.name.name.to_string()),
owner,
});
self.define_interface_name(&def.name, TypeOrItem::Type(id))?;
let name = def.name.name;
let span = def.name.span;
self.define_interface_name(name, span, TypeOrItem::Type(id))?;
}
Ok(())
}
Expand Down Expand Up @@ -675,7 +690,7 @@ impl<'a> Resolver<'a> {
name: Some(name.name.to_string()),
owner,
});
self.define_interface_name(name, TypeOrItem::Type(id))?;
self.define_interface_name(name.name, name.span, TypeOrItem::Type(id))?;
}
Ok(())
}
Expand Down Expand Up @@ -758,12 +773,12 @@ impl<'a> Resolver<'a> {
}
}

fn define_interface_name(&mut self, name: &ast::Id<'a>, item: TypeOrItem) -> Result<()> {
let prev = self.type_lookup.insert(name.name, (item, name.span));
fn define_interface_name(&mut self, name: &'a str, span: Span, item: TypeOrItem) -> Result<()> {
let prev = self.type_lookup.insert(name, (item, span));
if prev.is_some() {
Err(Error {
span: name.span,
msg: format!("name `{}` is defined more than once", name.name),
span,
msg: format!("name `{}` is defined more than once", name),
}
.into())
} else {
Expand Down
5 changes: 4 additions & 1 deletion crates/wit-parser/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::path::Path;
pub mod abi;
mod ast;
use ast::lex::Span;
pub use ast::SourceMap;
pub use ast::{expand, SourceMap};
mod sizealign;
pub use sizealign::*;
mod resolve;
Expand Down Expand Up @@ -289,6 +289,9 @@ pub struct Interface {

/// The document that this interface belongs to.
pub document: DocumentId,

/// TODO: A templated function.
pub wildcard: Option<Function>,
}

#[derive(Debug, Clone, PartialEq)]
Expand Down
3 changes: 3 additions & 0 deletions crates/wit-parser/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,9 @@ impl Remap {
for (_, func) in iface.functions.iter_mut() {
self.update_function(func);
}
if let Some(func) = &mut iface.wildcard {
self.update_function(func);
}
}

fn update_function(&self, func: &mut Function) {
Expand Down

0 comments on commit 8095934

Please sign in to comment.