Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested interfaces in wit #1624

Open
wants to merge 15 commits into
base: main
Choose a base branch
from

Conversation

macovedj
Copy link
Contributor

This PR enables the ability to describe nested interfaces in wit via the nest keyword.

package foo:bar;

interface baz {
  nest other:pkg/qux
}

This would indicate that that the instance associated with baz would export the instance associated with interface qux from package other:pkg.

(component 
  (type instance
    (type instance 
    ... definition of "other:pkg/qux" 
    )
   (export "other:pkg/qux" type 0)
  )
  (export "foo:bar/baz" type 0)
)

As I understand it, most use cases most immediately benefit from nesting interfaces from foreign packages, so I started with that, though we could certainly extend this PR or have follow ups to support any combo of locally defined/inlined/anonymous interfaces being nested.

This issue points out that wit currently is not able to express nested instances that can be expressed in wat/binary, so wit-bindgen can't be used by language toolchains to generate that binary. This syntax can be leveraged so that the unlocked dependency syntax referenced in the linked issue can specify the exports it expects in the imports it's describing.

Copy link
Member

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done a bit of a cursory review to start off with but I think that this still has a few missing pieces. For example src/resolve.rs in the wit-parser care will need to be updated to handle this new features of interfaces in a number of locations.

At a high level though I think it would be best to propose this feature on the component-model repository first. There's a number of high-level questions about how to design this feature that I think should be thought through such as:

  • Is the syntax nest a:b/c; what everyone agrees on?
  • Should nest a; be allowed?
  • Should nest a { /* ... */ } be allowed?
  • How will code generators for guest langauges map this?

Those are at least the questions off the top of my head which I think would be best to settle first before finishing up the implementation here. A PR to the component model repository would also help flesh out the wasm-encoding of this WIT construct.

crates/wit-parser/src/ast.rs Outdated Show resolved Hide resolved
crates/wit-parser/src/ast/resolve.rs Outdated Show resolved Hide resolved
Comment on lines 402 to 415
for nest_item in i.items.iter() {
if let InterfaceItem::Nest(n) = nest_item {
if package_items.insert(n.name.name, n.name.span).is_some() {
bail!(Error::new(
n.name.span,
format!("duplicate item named `{}`", n.name.name),
))
}
let prev = decl_list_ns.insert(n.name.name, ());
assert!(prev.is_none());
let prev = order.insert(n.name.name, Vec::new());
assert!(prev.is_none());
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this is quite right because this seems like it would be an interface-level validation rather than a package-level validation which I think this loop is doing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these changes got removed as a result of implementing some of the other comments made. While part of the logic was for validation, I think that some of the inserts here were needed to properly resolve interfaces, whereas now that's not the case. Given that these lines have been removed, I'm not sure that any validation is needed now? Unless there's a place that you'd expect to see a validation that isn't present.

crates/wit-parser/src/ast/resolve.rs Outdated Show resolved Hide resolved
crates/wit-parser/src/lib.rs Outdated Show resolved Hide resolved
crates/wit-component/src/printing.rs Outdated Show resolved Hide resolved
@macovedj
Copy link
Contributor Author

Sounds good! I can work on opening something up over there.

@macovedj
Copy link
Contributor Author

I think the issues re: encoding seem like they've mostly been hashed out here. While there seems to be a plan for the non-foreign interface nesting, I think it's ok to start with what this PR originally scoped (only nest a:b/c)? There might still be a bit more conversation re: code generation, but I'll probably go ahead and try and address most of the comments here soon.

crates/wit-parser/src/lib.rs Outdated Show resolved Hide resolved
crates/wit-parser/src/ast.rs Outdated Show resolved Hide resolved
crates/wit-parser/src/ast/resolve.rs Outdated Show resolved Hide resolved
crates/wit-parser/src/ast/resolve.rs Outdated Show resolved Hide resolved
crates/wit-parser/src/ast/resolve.rs Outdated Show resolved Hide resolved
crates/wit-parser/src/metadata.rs Show resolved Hide resolved
crates/wit-component/src/encoding/wit/v2.rs Outdated Show resolved Hide resolved
crates/wit-component/src/encoding/wit/v2.rs Outdated Show resolved Hide resolved
crates/wit-component/src/encoding/wit/v1.rs Outdated Show resolved Hide resolved
crates/wit-parser/src/decoding.rs Show resolved Hide resolved
Comment on lines +340 to +355
fn interface_path(&self, iface: InterfaceId) -> String {
let iface = &self.resolve.interfaces[iface];
let pkg_id = iface.package.unwrap();
let pkg = &self.resolve.packages[pkg_id];
if let Some(v) = &pkg.name.version {
format!(
"{}:{}/{}@{}",
pkg.name.namespace,
pkg.name.name,
iface.name.as_ref().unwrap(),
v.to_string()
)
} else {
format!("{}/{}", pkg.name.to_string(), iface.name.as_ref().unwrap())
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this I think Resolve::id_of might be what you want here? (in that I think it's equivalent to this function)

Comment on lines +280 to +283
let ty = &self.resolve.types[*id];
if let TypeOwner::Interface(iface_id) = ty.owner {
self.interface = Some(iface_id);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be best done a little differently than updating self.interface here. What I'm worried about is that this updates self.interface but doesn't restore it what it previously was.

Looking throughout this code though push_instance and pop_instance might be a good place to hook into? I think it should be ok to move most modifications of self.interface into those functions where push takes an InterfaceId and sets self.interface and then pop restores the previous value saved during push.

docs: Docs<'a>,
attributes: Vec<Attribute<'a>>,
) -> Result<Self> {
tokens.eat(Token::Nest)?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you'll want to expect here instead of eat (as eat has a boolean return)

_ => bail!("expected interface name"),
}
}
_ => bail!("instance type export `{name}` is not a type, function or instance"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this name needs to be updated to exp_name

stability: Default::default(),
});
}
_ => bail!("expected interface name"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind adding in the exp_name to the error message here?

@@ -726,6 +727,40 @@ impl WitPackageDecoder<'_> {
Ok(())
}

fn register_export(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function may want to get folded into register_import perhaps? (maybe coupled with a function rename). The internal structure looks quite similar and I think it's needed for importing an interface with nest as well?

@alexcrichton
Copy link
Member

One thing I'd also recommend for testing this is fuzzing. For example here's a small patch to enable some fuzzing of this commit:

diff --git a/crates/wit-smith/src/generate.rs b/crates/wit-smith/src/generate.rs
index 41c5c5c1..82db5fdd 100644
--- a/crates/wit-smith/src/generate.rs
+++ b/crates/wit-smith/src/generate.rs
@@ -344,6 +344,7 @@ impl<'a> InterfaceGenerator<'a> {
             Use,
             Type,
             Function,
+            Nest,
         }

         let mut parts = Vec::new();
@@ -374,6 +375,11 @@ impl<'a> InterfaceGenerator<'a> {
                 Generate::Function => {
                     parts.push(self.gen_func(u)?);
                 }
+                Generate::Nest => {
+                    if let Some(part) = self.gen_nest(u)? {
+                        parts.push(part);
+                    }
+                }
             }
         }

@@ -882,6 +888,15 @@ impl<'a> InterfaceGenerator<'a> {
     fn gen_unique_name(&mut self, u: &mut Unstructured<'_>) -> Result<String> {
         gen_unique_name(u, &mut self.unique_names)
     }
+
+    fn gen_nest(&mut self, u: &mut Unstructured<'_>) -> Result<Option<String>> {
+        let mut path = String::new();
+        if self.gen.gen_path(u, self.file, &mut path)?.is_none() {
+            return Ok(None);
+        }
+
+        Ok(Some(format!("nest {path};")))
+    }
 }

 fn gen_unique_name(u: &mut Unstructured<'_>, set: &mut HashSet<String>) -> Result<String> {

which is executed via

FUZZER=roundtrip_wit cargo +nightly fuzz run -s none run

(you can pass --dev between run and run to run in debug mode too)

That'll help weed out issues dealing with round-tripping interfaces and such. For example that quickly finds this locally for me:

thread '<unnamed>' panicked at fuzz/src/roundtrip_wit.rs:13:61:
called `Result::unwrap()` on an `Err` value: export name `name:name1/[email protected]+1.2.0` conflicts with previous name `name:name1/[email protected]+1.2.0` (at offset 0x6a)
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

which I think means two things:

  1. Resolve validation needs to guarantee that you can't nest the same interface twice (maybe this means it should switch to IndexSet?)
  2. The fuzz generator needs to record what was nested and not generate documents that nest the same interface twice. Either that or the error is handled in the fuzzer and the test case is discarded (but I think here the former is the better route to take)

@alexcrichton alexcrichton self-assigned this Sep 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants