Repository: bytecodealliance/wasm-tools
Pull request #1294 (2023-11-16 18:41:19)

stop kebab-casing and validate component names
Currently wasm-compose is kebab-casing package names, as this used to make sense before recent changes to import syntax.

With the updated changes, if a package is imported with the name:
`locked-dep=<foo:add@1.0.0>,integrity=<sha256-d780a28ec35f001cf1e61401ec1ee22dad5821ccf8280affcf135074c7c53cf2>`,

 wasm-compose will change it to
`locked-dep-foo-add-1-0-0-integrity-sha256-d780a28ec35f001cf1e61401ec1ee22dad5821ccf8280affcf135074c7c53cf2`.  

This PR stops package names from being kebab-cased, and validates their names using `ComponentName` from wasmparser.

macovedj: > Thanks! Could you add a test for this as well?

Sure thing!  Looks like there are some existing test failures I'll have to investigate as well.
macovedj: Ok I just went ahead and enabled validation for `$input` specifically for the use case that is `wasm-compose` because it felt like the best option to me... happy to to go with another option though if that makes sense.
macovedj: After conferring with Peter, went ahead and reverted the new validation and changed `$input` to `composition-root`

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 70fc06dc..3538d9a0 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1699,7 +1699,7 @@ checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
 
 [[package]]
 name = "wasm-compose"
-version = "0.4.15"
+version = "0.4.16"
 dependencies = [
  "anyhow",
  "glob",
@@ -1713,9 +1713,9 @@ dependencies = [
  "serde_derive",
  "serde_yaml",
  "smallvec",
- "wasm-encoder 0.38.0",
- "wasmparser 0.118.0",
- "wasmprinter 0.2.74",
+ "wasm-encoder 0.38.1",
+ "wasmparser 0.118.1",
+ "wasmprinter 0.2.75",
  "wat",
  "wit-component",
 ]
@@ -1731,17 +1731,17 @@ dependencies = [
 
 [[package]]
 name = "wasm-encoder"
-version = "0.38.0"
+version = "0.38.1"
 dependencies = [
  "anyhow",
  "leb128",
  "tempfile",
- "wasmparser 0.118.0",
+ "wasmparser 0.118.1",
 ]
 
 [[package]]
 name = "wasm-metadata"
-version = "0.10.13"
+version = "0.10.14"
 dependencies = [
  "anyhow",
  "clap 4.4.8",
@@ -1750,14 +1750,14 @@ dependencies = [
  "serde_derive",
  "serde_json",
  "spdx",
- "wasm-encoder 0.38.0",
- "wasmparser 0.118.0",
+ "wasm-encoder 0.38.1",
+ "wasmparser 0.118.1",
  "wat",
 ]
 
 [[package]]
 name = "wasm-mutate"
-version = "0.2.42"
+version = "0.2.43"
 dependencies = [
  "anyhow",
  "clap 4.4.8",
@@ -1766,9 +1766,9 @@ dependencies = [
  "log",
  "rand",
  "thiserror",
- "wasm-encoder 0.38.0",
- "wasmparser 0.118.0",
- "wasmprinter 0.2.74",
+ "wasm-encoder 0.38.1",
+ "wasmparser 0.118.1",
+ "wasmprinter 0.2.75",
  "wat",
 ]
 
@@ -1785,14 +1785,14 @@ dependencies = [
  "num_cpus",
  "rand",
  "wasm-mutate",
- "wasmparser 0.118.0",
- "wasmprinter 0.2.74",
+ "wasmparser 0.118.1",
+ "wasmprinter 0.2.75",
  "wasmtime",
 ]
 
 [[package]]
 name = "wasm-shrink"
-version = "0.1.43"
+version = "0.1.44"
 dependencies = [
  "anyhow",
  "blake3",
@@ -1801,14 +1801,14 @@ dependencies = [
  "log",
  "rand",
  "wasm-mutate",
- "wasmparser 0.118.0",
- "wasmprinter 0.2.74",
+ "wasmparser 0.118.1",
+ "wasmprinter 0.2.75",
  "wat",
 ]
 
 [[package]]
 name = "wasm-smith"
-version = "0.13.0"
+version = "0.13.1"
 dependencies = [
  "arbitrary",
  "criterion",
@@ -1819,15 +1819,15 @@ dependencies = [
  "rand",
  "serde",
  "serde_derive",
- "wasm-encoder 0.38.0",
- "wasmparser 0.118.0",
- "wasmprinter 0.2.74",
+ "wasm-encoder 0.38.1",
+ "wasmparser 0.118.1",
+ "wasmprinter 0.2.75",
  "wat",
 ]
 
 [[package]]
 name = "wasm-tools"
-version = "1.0.53"
+version = "1.0.54"
 dependencies = [
  "addr2line 0.21.0",
  "anyhow",
@@ -1849,13 +1849,13 @@ dependencies = [
  "tempfile",
  "termcolor",
  "wasm-compose",
- "wasm-encoder 0.38.0",
+ "wasm-encoder 0.38.1",
  "wasm-metadata",
  "wasm-mutate",
  "wasm-shrink",
  "wasm-smith",
- "wasmparser 0.118.0",
- "wasmprinter 0.2.74",
+ "wasmparser 0.118.1",
+ "wasmprinter 0.2.75",
  "wast",
  "wat",
  "wit-component",
@@ -1871,8 +1871,8 @@ dependencies = [
  "wasm-mutate",
  "wasm-shrink",
  "wasm-smith",
- "wasmparser 0.118.0",
- "wasmprinter 0.2.74",
+ "wasmparser 0.118.1",
+ "wasmprinter 0.2.75",
  "wast",
  "wat",
 ]
@@ -1887,11 +1887,11 @@ dependencies = [
  "libfuzzer-sys",
  "log",
  "tempfile",
- "wasm-encoder 0.38.0",
+ "wasm-encoder 0.38.1",
  "wasm-mutate",
  "wasm-smith",
- "wasmparser 0.118.0",
- "wasmprinter 0.2.74",
+ "wasmparser 0.118.1",
+ "wasmprinter 0.2.75",
  "wasmtime",
  "wast",
  "wat",
@@ -1922,7 +1922,7 @@ dependencies = [
 
 [[package]]
 name = "wasmparser"
-version = "0.118.0"
+version = "0.118.1"
 dependencies = [
  "anyhow",
  "criterion",
@@ -1932,7 +1932,7 @@ dependencies = [
  "once_cell",
  "rayon",
  "semver",
- "wasm-encoder 0.38.0",
+ "wasm-encoder 0.38.1",
  "wast",
  "wat",
 ]
@@ -1949,13 +1949,13 @@ dependencies = [
 
 [[package]]
 name = "wasmprinter"
-version = "0.2.74"
+version = "0.2.75"
 dependencies = [
  "anyhow",
  "diff",
  "rayon",
  "tempfile",
- "wasmparser 0.118.0",
+ "wasmparser 0.118.1",
  "wast",
  "wat",
 ]
@@ -2213,21 +2213,21 @@ dependencies = [
 
 [[package]]
 name = "wast"
-version = "69.0.0"
+version = "69.0.1"
 dependencies = [
  "anyhow",
  "leb128",
  "memchr",
  "rayon",
  "unicode-width",
- "wasm-encoder 0.38.0",
- "wasmparser 0.118.0",
+ "wasm-encoder 0.38.1",
+ "wasmparser 0.118.1",
  "wat",
 ]
 
 [[package]]
 name = "wat"
-version = "1.0.81"
+version = "1.0.82"
 dependencies = [
  "wast",
 ]
@@ -2357,7 +2357,7 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
 
 [[package]]
 name = "wit-component"
-version = "0.18.2"
+version = "0.19.0"
 dependencies = [
  "anyhow",
  "bitflags 2.4.1",
@@ -2369,10 +2369,10 @@ dependencies = [
  "serde",
  "serde_derive",
  "serde_json",
- "wasm-encoder 0.38.0",
+ "wasm-encoder 0.38.1",
  "wasm-metadata",
- "wasmparser 0.118.0",
- "wasmprinter 0.2.74",
+ "wasmparser 0.118.1",
+ "wasmprinter 0.2.75",
  "wasmtime",
  "wast",
  "wat",
@@ -2421,13 +2421,13 @@ dependencies = [
  "env_logger",
  "libfuzzer-sys",
  "log",
- "wasmprinter 0.2.74",
+ "wasmprinter 0.2.75",
  "wit-parser 0.13.0",
 ]
 
 [[package]]
 name = "wit-smith"
-version = "0.1.23"
+version = "0.1.24"
 dependencies = [
  "arbitrary",
  "clap 4.4.8",
diff --git a/Cargo.toml b/Cargo.toml
index fd766e65..dad0bd26 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wasm-tools"
-version = "1.0.53"
+version = "1.0.54"
 authors = ["The Wasmtime Project Developers"]
 edition.workspace = true
 description = "CLI tools for interoperating with WebAssembly files"
@@ -46,19 +46,19 @@ pretty_assertions = "1.3.0"
 semver = "1.0.0"
 smallvec = "1.11.1"
 
-wasm-compose = { version = "0.4.15", path = "crates/wasm-compose" }
-wasm-encoder = { version = "0.38.0", path = "crates/wasm-encoder" }
-wasm-metadata = { version = "0.10.13", path = "crates/wasm-metadata" }
-wasm-mutate = { version = "0.2.42", path = "crates/wasm-mutate" }
-wasm-shrink = { version = "0.1.43", path = "crates/wasm-shrink" }
-wasm-smith = { version = "0.13.0", path = "crates/wasm-smith" }
-wasmparser = { version = "0.118.0", path = "crates/wasmparser" }
-wasmprinter = { version = "0.2.74", path = "crates/wasmprinter" }
-wast = { version = "69.0.0", path = "crates/wast" }
-wat = { version = "1.0.81", path = "crates/wat" }
-wit-component = { version = "0.18.2", path = "crates/wit-component" }
+wasm-compose = { version = "0.4.16", path = "crates/wasm-compose" }
+wasm-encoder = { version = "0.38.1", path = "crates/wasm-encoder" }
+wasm-metadata = { version = "0.10.14", path = "crates/wasm-metadata" }
+wasm-mutate = { version = "0.2.43", path = "crates/wasm-mutate" }
+wasm-shrink = { version = "0.1.44", path = "crates/wasm-shrink" }
+wasm-smith = { version = "0.13.1", path = "crates/wasm-smith" }
+wasmparser = { version = "0.118.1", path = "crates/wasmparser" }
+wasmprinter = { version = "0.2.75", path = "crates/wasmprinter" }
+wast = { version = "69.0.1", path = "crates/wast" }
+wat = { version = "1.0.82", path = "crates/wat" }
+wit-component = { version = "0.19.0", path = "crates/wit-component" }
 wit-parser = { version = "0.13.0", path = "crates/wit-parser" }
-wit-smith = { version = "0.1.23", path = "crates/wit-smith" }
+wit-smith = { version = "0.1.24", path = "crates/wit-smith" }
 
 [dependencies]
 anyhow = { workspace = true }
diff --git a/crates/wasm-compose/CONFIG.md b/crates/wasm-compose/CONFIG.md
index f27d782c..05e9c70b 100644
--- a/crates/wasm-compose/CONFIG.md
+++ b/crates/wasm-compose/CONFIG.md
@@ -7,16 +7,16 @@ To specify the configuration file to use, pass the `--config` option to
 
 The configuration file has the following top-level fields:
 
-* `search-paths` : `list<string>` (optional) - a list of paths to search for
-dependencies.
-* `skip-validation` : `bool` (optional) - a boolean indicating whether to skip
-validation of the resulting composed component.
-* `dependencies` : `map<string, dependency>` (optional) - a map specifying the
-explicit locations of transitive dependencies.
-* `instantiations` : `map<string, instantiation>` (optional) - a map specifying
-the explicit instantiations of transitive dependencies.
-* `definitions` : `list<string>` (optional) - a list of paths to _definition_
-components.
+- `search-paths` : `list<string>` (optional) - a list of paths to search for
+  dependencies.
+- `skip-validation` : `bool` (optional) - a boolean indicating whether to skip
+  validation of the resulting composed component.
+- `dependencies` : `map<string, dependency>` (optional) - a map specifying the
+  explicit locations of transitive dependencies.
+- `instantiations` : `map<string, instantiation>` (optional) - a map specifying
+  the explicit instantiations of transitive dependencies.
+- `definitions` : `list<string>` (optional) - a list of paths to _definition_
+  components.
 
 ## Dependencies
 
@@ -25,10 +25,10 @@ dependency, rather than searching the configured search paths.
 
 A dependency has the following fields:
 
-* `path` : `string` - the path to the WebAssembly component file; the path is
+- `path` : `string` - the path to the WebAssembly component file; the path is
   relative to the configuration file.
 
-* `import` : `string` (optional) - the name to use for importing the component.
+- `import` : `string` (optional) - the name to use for importing the component.
   If not present, the component at the given path will be defined directly in
   the composed component.
 
@@ -49,11 +49,11 @@ dependencies:
 
 In the above example, two dependencies are defined in the configuration:
 
-* `a` - the contents of `a.wasm` will be defined directly in the composed
-component.
-* `b` - a component of the same type as `b.wasm` will be imported in the
-composed component with name `b`; the original file will not be embedded in the
-composed component.
+- `a` - the contents of `a.wasm` will be defined directly in the composed
+  component.
+- `b` - a component of the same type as `b.wasm` will be imported in the
+  composed component with name `b`; the original file will not be embedded in the
+  composed component.
 
 _Note: importing components from a composed component is not currently
 supported in [Wasmtime](https://github.com/bytecodealliance/wasmtime)._
@@ -68,19 +68,19 @@ instantiated, even allowing a dependency to be instantiated multiple times.
 
 An instantiation has the following fields:
 
-* `dependency` : `string` (optional) - the name of the dependency to
+- `dependency` : `string` (optional) - the name of the dependency to
   instantiate.
 
   If unspecified, the instantiation will be for a dependency of the same name
   as the instantiation itself.
 
-* `arguments` : `map<string, argument>` (optional) - a mapping of argument
+- `arguments` : `map<string, argument>` (optional) - a mapping of argument
   names to arguments.
 
   Argument names match the names of the imports of the dependency being
   instantiated.
 
-Note that the instantiation name `$input` is special and signifies how the input
+Note that the instantiation name `root` is special and signifies how the input
 component is to be instantiated.
 
 ### Instantiation arguments
@@ -90,9 +90,9 @@ to the instantiation of a dependency.
 
 An argument has the following fields:
 
-* `instance` : `string` - the name of the instance to pass as the argument.
+- `instance` : `string` - the name of the instance to pass as the argument.
 
-* `export` : `string` (optional) - the name of the exported instance on
+- `export` : `string` (optional) - the name of the exported instance on
   `instance` to use as the argument.
 
   If not present, the instance specified by `instance` will be passed directly.
@@ -106,7 +106,7 @@ A slightly complex example of configuring instantiations:
 
 ```yaml
 instantiations:
-  $input:
+  root:
     arguments:
       a: b
   b:
@@ -118,7 +118,7 @@ instantiations:
     dependency: f
 ```
 
-In the above example, the `$input` instantiation (i.e. the root instantiation)
+In the above example, the `root` instantiation (i.e. the root instantiation)
 has explicitly specified that the argument named `a` is to be provided instance
 `b`.
 
diff --git a/crates/wasm-compose/Cargo.toml b/crates/wasm-compose/Cargo.toml
index a6e719d7..0ae2ccd0 100644
--- a/crates/wasm-compose/Cargo.toml
+++ b/crates/wasm-compose/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wasm-compose"
-version = "0.4.15"
+version = "0.4.16"
 edition.workspace = true
 authors = ["Peter Huene <peter@huene.dev>"]
 license = "Apache-2.0 WITH LLVM-exception"
diff --git a/crates/wasm-compose/example/server/config.yml b/crates/wasm-compose/example/server/config.yml
index c9261947..b5ba2d25 100644
--- a/crates/wasm-compose/example/server/config.yml
+++ b/crates/wasm-compose/example/server/config.yml
@@ -2,7 +2,7 @@ search-paths:
   - ../service/target/wasm32-wasi/release
 
 instantiations:
-  $input:
+  root:
     arguments:
       example:service/handler@0.1.0:
         instance: svc
diff --git a/crates/wasm-compose/src/composer.rs b/crates/wasm-compose/src/composer.rs
index 66948d09..e846ce12 100644
--- a/crates/wasm-compose/src/composer.rs
+++ b/crates/wasm-compose/src/composer.rs
@@ -17,7 +17,7 @@ use wasmparser::{
 };
 
 /// The root component name used in configuration.
-pub const ROOT_COMPONENT_NAME: &str = "$input";
+pub const ROOT_COMPONENT_NAME: &str = "root";
 
 /// A reference to an instance import on a component.
 #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
diff --git a/crates/wasm-compose/src/encoding.rs b/crates/wasm-compose/src/encoding.rs
index 749548c2..7a64bb8e 100644
--- a/crates/wasm-compose/src/encoding.rs
+++ b/crates/wasm-compose/src/encoding.rs
@@ -2,7 +2,6 @@ use crate::graph::{
     type_desc, CompositionGraph, EncodeOptions, ExportIndex, ImportIndex, InstanceId,
 };
 use anyhow::{anyhow, bail, Result};
-use heck::ToKebabCase;
 use indexmap::{IndexMap, IndexSet};
 use petgraph::EdgeDirection;
 use smallvec::SmallVec;
@@ -1038,7 +1037,7 @@ enum ImportMapEntry<'a> {
 /// Represents the import map built during the encoding
 /// of a composition graph.
 #[derive(Default)]
-struct ImportMap<'a>(IndexMap<Cow<'a, str>, ImportMapEntry<'a>>);
+struct ImportMap<'a>(IndexMap<&'a str, ImportMapEntry<'a>>);
 
 impl<'a> ImportMap<'a> {
     fn new(import_components: bool, graph: &'a CompositionGraph) -> Result<Self> {
@@ -1062,7 +1061,7 @@ impl<'a> ImportMap<'a> {
             assert!(self
                 .0
                 .insert(
-                    entry.component.name.to_kebab_case().into(),
+                    &entry.component.name,
                     ImportMapEntry::Component(&entry.component),
                 )
                 .is_none());
diff --git a/crates/wasm-compose/src/graph.rs b/crates/wasm-compose/src/graph.rs
index f184d3eb..bf86106b 100644
--- a/crates/wasm-compose/src/graph.rs
+++ b/crates/wasm-compose/src/graph.rs
@@ -11,6 +11,7 @@ use std::{
     sync::atomic::{AtomicUsize, Ordering},
 };
 use wasmparser::{
+    names::ComponentName,
     types::{
         ComponentAnyTypeId, ComponentEntityType, ComponentInstanceTypeId, Remap, Remapping,
         ResourceId, SubtypeCx, Types, TypesRef,
@@ -48,12 +49,11 @@ pub struct Component<'a> {
 
 impl<'a> Component<'a> {
     /// Constructs a new component from reading the given file.
-    pub fn from_file(name: impl Into<String>, path: impl AsRef<Path>) -> Result<Self> {
+    pub fn from_file(name: &str, path: impl AsRef<Path>) -> Result<Self> {
         let path = path.as_ref();
         log::info!("parsing WebAssembly component file `{}`", path.display());
-
         let component = Self::parse(
-            name.into(),
+            ComponentName::new(name, 0)?.to_string(),
             Some(path.to_owned()),
             wat::parse_file(path)
                 .with_context(|| {
@@ -83,8 +83,12 @@ impl<'a> Component<'a> {
         }
 
         log::info!("parsing WebAssembly component from bytes");
-        let component =
-            Self::parse(name.into(), None, bytes).context("failed to parse component")?;
+        let component = Self::parse(
+            ComponentName::new(&name.into(), 0)?.to_string(),
+            None,
+            bytes,
+        )
+        .context("failed to parse component")?;
 
         log::debug!("WebAssembly component parsed:\n{component:#?}",);
 
diff --git a/crates/wasm-encoder/Cargo.toml b/crates/wasm-encoder/Cargo.toml
index 8b3429e1..9f81d75f 100644
--- a/crates/wasm-encoder/Cargo.toml
+++ b/crates/wasm-encoder/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wasm-encoder"
-version = "0.38.0"
+version = "0.38.1"
 authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
 edition.workspace = true
 license = "Apache-2.0 WITH LLVM-exception"
diff --git a/crates/wasm-metadata/Cargo.toml b/crates/wasm-metadata/Cargo.toml
index 032329c6..43bbb573 100644
--- a/crates/wasm-metadata/Cargo.toml
+++ b/crates/wasm-metadata/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wasm-metadata"
-version = "0.10.13"
+version = "0.10.14"
 edition.workspace = true
 license = "Apache-2.0 WITH LLVM-exception"
 repository = "https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wasm-metadata"
diff --git a/crates/wasm-mutate/Cargo.toml b/crates/wasm-mutate/Cargo.toml
index 8f9637cc..816483ac 100644
--- a/crates/wasm-mutate/Cargo.toml
+++ b/crates/wasm-mutate/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wasm-mutate"
-version = "0.2.42"
+version = "0.2.43"
 edition.workspace = true
 license = "Apache-2.0 WITH LLVM-exception"
 repository = "https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wasm-mutate"
diff --git a/crates/wasm-shrink/Cargo.toml b/crates/wasm-shrink/Cargo.toml
index 6261c262..17fd9754 100644
--- a/crates/wasm-shrink/Cargo.toml
+++ b/crates/wasm-shrink/Cargo.toml
@@ -8,7 +8,7 @@ license = "Apache-2.0 WITH LLVM-exception"
 readme = "./README.md"
 repository = "https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wasm-shrink"
 name = "wasm-shrink"
-version = "0.1.43"
+version = "0.1.44"
 
 [dependencies]
 anyhow = { workspace = true }
diff --git a/crates/wasm-smith/Cargo.toml b/crates/wasm-smith/Cargo.toml
index 56cbf9ac..76973b62 100644
--- a/crates/wasm-smith/Cargo.toml
+++ b/crates/wasm-smith/Cargo.toml
@@ -8,7 +8,7 @@ license = "Apache-2.0 WITH LLVM-exception"
 name = "wasm-smith"
 readme = "./README.md"
 repository = "https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wasm-smith"
-version = "0.13.0"
+version = "0.13.1"
 exclude = ["/benches/corpus"]
 
 [[bench]]
diff --git a/crates/wasmparser/Cargo.toml b/crates/wasmparser/Cargo.toml
index 1af24854..7410a2af 100644
--- a/crates/wasmparser/Cargo.toml
+++ b/crates/wasmparser/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wasmparser"
-version = "0.118.0"
+version = "0.118.1"
 authors = ["Yury Delendik <ydelendik@mozilla.com>"]
 license = "Apache-2.0 WITH LLVM-exception"
 repository = "https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wasmparser"
diff --git a/crates/wasmparser/src/validator/operators.rs b/crates/wasmparser/src/validator/operators.rs
index 314d2624..9d5c7ce1 100644
--- a/crates/wasmparser/src/validator/operators.rs
+++ b/crates/wasmparser/src/validator/operators.rs
@@ -2243,6 +2243,11 @@ where
         Ok(())
     }
     fn visit_ref_null(&mut self, mut heap_type: HeapType) -> Self::Output {
+        if let Some(ty) = RefType::new(true, heap_type) {
+            self.features
+                .check_ref_type(ty)
+                .map_err(|e| BinaryReaderError::new(e, self.offset))?;
+        }
         self.resources
             .check_heap_type(&mut heap_type, self.offset)?;
         let ty = ValType::Ref(
diff --git a/crates/wasmprinter/Cargo.toml b/crates/wasmprinter/Cargo.toml
index 381e51bd..e59fb811 100644
--- a/crates/wasmprinter/Cargo.toml
+++ b/crates/wasmprinter/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wasmprinter"
-version = "0.2.74"
+version = "0.2.75"
 authors = ["Alex Crichton <alex@alexcrichton.com>"]
 edition.workspace = true
 license = "Apache-2.0 WITH LLVM-exception"
diff --git a/crates/wast/Cargo.toml b/crates/wast/Cargo.toml
index e43d49f1..642c3854 100644
--- a/crates/wast/Cargo.toml
+++ b/crates/wast/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wast"
-version = "69.0.0"
+version = "69.0.1"
 authors = ["Alex Crichton <alex@alexcrichton.com>"]
 edition.workspace = true
 license = "Apache-2.0 WITH LLVM-exception"
diff --git a/crates/wat/Cargo.toml b/crates/wat/Cargo.toml
index a0871c29..851f5f13 100644
--- a/crates/wat/Cargo.toml
+++ b/crates/wat/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "wat"
-version = "1.0.81"
+version = "1.0.82"
 authors = ["Alex Crichton <alex@alexcrichton.com>"]
 edition.workspace = true
 license = "Apache-2.0 WITH LLVM-exception"
diff --git a/crates/wit-component/Cargo.toml b/crates/wit-component/Cargo.toml
index a0c5da95..d4df5ca7 100644
--- a/crates/wit-component/Cargo.toml
+++ b/crates/wit-component/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name = "wit-component"
 authors = ["Peter Huene <peter@huene.dev>"]
-version = "0.18.2"
+version = "0.19.0"
 edition.workspace = true
 license = "Apache-2.0 WITH LLVM-exception"
 readme = "README.md"
diff --git a/crates/wit-smith/Cargo.toml b/crates/wit-smith/Cargo.toml
index d5b77397..2fa51fd3 100644
--- a/crates/wit-smith/Cargo.toml
+++ b/crates/wit-smith/Cargo.toml
@@ -5,7 +5,7 @@ edition.workspace = true
 license = "Apache-2.0 WITH LLVM-exception"
 name = "wit-smith"
 repository = "https://github.com/bytecodealliance/wasm-tools/tree/main/crates/wit-smith"
-version = "0.1.23"
+version = "0.1.24"
 
 [dependencies]
 arbitrary = { workspace = true, features = ["derive"] }
diff --git a/src/bin/wasm-tools/addr2line.rs b/src/bin/wasm-tools/addr2line.rs
index 5478a7cb..ea4c6091 100644
--- a/src/bin/wasm-tools/addr2line.rs
+++ b/src/bin/wasm-tools/addr2line.rs
@@ -1,10 +1,11 @@
 use addr2line::{Context, LookupResult};
-use anyhow::{bail, Context as _, Result};
+use anyhow::{anyhow, bail, Context as _, Result};
 use gimli::EndianSlice;
 use std::collections::HashMap;
 use std::io::Write;
+use std::ops::Range;
 use std::u64;
-use wasmparser::{Parser, Payload};
+use wasmparser::{Encoding, Parser, Payload};
 
 /// Translate a WebAssembly address to a filename and line number using DWARF
 /// debugging information.
@@ -40,6 +41,12 @@ pub struct Opts {
     code_section_relative: bool,
 }
 
+struct Module<'a> {
+    range: Range<u64>,
+    code_start: Option<u64>,
+    custom_sections: HashMap<&'a str, &'a [u8]>,
+}
+
 impl Opts {
     pub fn general_opts(&self) -> &wasm_tools::GeneralOpts {
         self.io.general_opts()
@@ -48,54 +55,60 @@ impl Opts {
     pub fn run(&self) -> Result<()> {
         let wasm = self.io.parse_input_wasm()?;
 
-        let (code_start, custom_sections) = self
+        let modules = self
             .parse_custom_sections(&wasm)
             .context("failed to parse input and read custom sections")?;
-
-        let dwarf = gimli::Dwarf::load(|id| -> Result<_> {
-            let data = custom_sections.get(id.name()).copied().unwrap_or(&[]);
-            Ok(EndianSlice::new(data, gimli::LittleEndian))
-        })?;
-        let cx = Context::from_dwarf(dwarf)
-            .context("failed to create addr2line dwarf mapping context")?;
-
         let mut output = self.io.output_writer()?;
 
         for addr in self.addresses.iter() {
-            self.addr2line(&addr, code_start, &cx, &mut output)
+            self.addr2line(&addr, &modules, &mut output)
                 .with_context(|| format!("failed to find frames for `{addr}`"))?;
         }
 
         Ok(())
     }
 
-    fn parse_custom_sections<'a>(
-        &self,
-        wasm: &'a [u8],
-    ) -> Result<(Option<u64>, HashMap<&'a str, &'a [u8]>)> {
-        let mut ret = HashMap::new();
-        let mut code_start = None;
+    fn parse_custom_sections<'a>(&self, wasm: &'a [u8]) -> Result<Vec<Module<'a>>> {
+        let mut ret = Vec::new();
+        let mut cur_module = None;
         for payload in Parser::new(0).parse_all(wasm) {
             match payload? {
+                Payload::Version {
+                    encoding: Encoding::Module,
+                    range,
+                    ..
+                } => {
+                    assert!(cur_module.is_none());
+                    cur_module = Some(Module {
+                        range: range.start as u64..0,
+                        code_start: None,
+                        custom_sections: HashMap::new(),
+                    });
+                }
+
                 Payload::CustomSection(s) => {
-                    ret.insert(s.name(), s.data());
+                    if let Some(cur) = &mut cur_module {
+                        cur.custom_sections.insert(s.name(), s.data());
+                    }
                 }
                 Payload::CodeSectionStart { range, .. } => {
-                    code_start = Some(range.start as u64);
+                    assert!(cur_module.is_some());
+                    cur_module.as_mut().unwrap().code_start = Some(range.start as u64);
+                }
+
+                Payload::End(offset) => {
+                    if let Some(mut module) = cur_module.take() {
+                        module.range.end = offset as u64;
+                        ret.push(module);
+                    }
                 }
                 _ => {}
             }
         }
-        Ok((code_start, ret))
+        Ok(ret)
     }
 
-    fn addr2line(
-        &self,
-        addr: &str,
-        code_start: Option<u64>,
-        cx: &Context<EndianSlice<gimli::LittleEndian>>,
-        out: &mut dyn Write,
-    ) -> Result<()> {
+    fn addr2line(&self, addr: &str, modules: &[Module<'_>], out: &mut dyn Write) -> Result<()> {
         // Support either `0x` or `@` prefixes for hex addresses since 0x is
         // standard and @ is used by wasmprinter (and web browsers I think?)
         let addr = if let Some(hex) = addr.strip_prefix("0x").or_else(|| addr.strip_prefix("@")) {
@@ -104,12 +117,28 @@ impl Opts {
             addr.parse()?
         };
 
+        let module = modules
+            .iter()
+            .find(|module| module.range.start <= addr && addr <= module.range.end)
+            .ok_or_else(|| anyhow!("no module found which contains this address"))?;
+
+        let dwarf = gimli::Dwarf::load(|id| -> Result<_> {
+            let data = module
+                .custom_sections
+                .get(id.name())
+                .copied()
+                .unwrap_or(&[]);
+            Ok(EndianSlice::new(data, gimli::LittleEndian))
+        })?;
+        let cx = Context::from_dwarf(dwarf)
+            .context("failed to create addr2line dwarf mapping context")?;
+
         // Addresses in DWARF are relative to the start of the text section, so
         // factor that in here.
         let text_relative_addr = if self.code_section_relative {
             addr
         } else {
-            match code_start {
+            match module.code_start {
                 Some(start) => addr
                     .checked_sub(start)
                     .context("address is before the beginning of the text section")?,


Test patch:
diff --git a/crates/wasm-compose/tests/compositions/complex-import/composed.wat b/crates/wasm-compose/tests/compositions/complex-import/composed.wat
index 3466f2c2..3fc8cd5b 100644
--- a/crates/wasm-compose/tests/compositions/complex-import/composed.wat
+++ b/crates/wasm-compose/tests/compositions/complex-import/composed.wat
@@ -14,7 +14,7 @@
       (export (;1;) "m2" (func (type 1)))
     )
   )
-  (import "input" (component (;0;) (type 0)))
+  (import "root" (component (;0;) (type 0)))
   (type (;1;)
     (component
       (type (;0;)
diff --git a/crates/wasm-compose/tests/compositions/conflict-on-import/error.txt b/crates/wasm-compose/tests/compositions/conflict-on-import/error.txt
index 2b5cf6da..c9d4dc0d 100644
--- a/crates/wasm-compose/tests/compositions/conflict-on-import/error.txt
+++ b/crates/wasm-compose/tests/compositions/conflict-on-import/error.txt
@@ -1,1 +1,1 @@
-cannot import instance with name `a` for an instantiation argument of component `b` because it conflicts with an imported instantiation argument of component `$input`
+cannot import instance with name `a` for an instantiation argument of component `b` because it conflicts with an imported instantiation argument of component `root`
diff --git a/crates/wasm-compose/tests/compositions/connect-resources/config.yml b/crates/wasm-compose/tests/compositions/connect-resources/config.yml
index 96ee0575..cdf470b6 100644
--- a/crates/wasm-compose/tests/compositions/connect-resources/config.yml
+++ b/crates/wasm-compose/tests/compositions/connect-resources/config.yml
@@ -1,5 +1,5 @@
 instantiations:
-  $input:
+  root:
     arguments:
       example:service/logging@0.1.0:
         instance: logger
diff --git a/crates/wasm-compose/tests/compositions/def-mismatch/error.txt b/crates/wasm-compose/tests/compositions/def-mismatch/error.txt
index 984c5854..751af4a4 100644
--- a/crates/wasm-compose/tests/compositions/def-mismatch/error.txt
+++ b/crates/wasm-compose/tests/compositions/def-mismatch/error.txt
@@ -1,4 +1,4 @@
-failed to connect instance `$input` to definition component `tests/compositions/def-mismatch/definitions.wat`
+failed to connect instance `root` to definition component `tests/compositions/def-mismatch/definitions.wat`
 
 Caused by:
     source instance export `foo` is not compatible with target instance import `foo`
diff --git a/crates/wasm-compose/tests/compositions/export-on-import/config.yml b/crates/wasm-compose/tests/compositions/export-on-import/config.yml
index 0a0d48cc..0000489a 100644
--- a/crates/wasm-compose/tests/compositions/export-on-import/config.yml
+++ b/crates/wasm-compose/tests/compositions/export-on-import/config.yml
@@ -1,5 +1,5 @@
 instantiations:
-  $input:
+  root:
     arguments:
       a:
         instance: a
diff --git a/crates/wasm-compose/tests/compositions/incompatible-explicit-export/config.yml b/crates/wasm-compose/tests/compositions/incompatible-explicit-export/config.yml
index 0a0d48cc..0000489a 100644
--- a/crates/wasm-compose/tests/compositions/incompatible-explicit-export/config.yml
+++ b/crates/wasm-compose/tests/compositions/incompatible-explicit-export/config.yml
@@ -1,5 +1,5 @@
 instantiations:
-  $input:
+  root:
     arguments:
       a:
         instance: a
diff --git a/crates/wasm-compose/tests/compositions/instantiation-cycle/config.yml b/crates/wasm-compose/tests/compositions/instantiation-cycle/config.yml
index b95f8ad6..30627850 100644
--- a/crates/wasm-compose/tests/compositions/instantiation-cycle/config.yml
+++ b/crates/wasm-compose/tests/compositions/instantiation-cycle/config.yml
@@ -1,5 +1,5 @@
 instantiations:
-  $input:
+  root:
     arguments:
       b: a1
   a1:
diff --git a/crates/wasm-compose/tests/compositions/invalid-instantiation-arg/config.yml b/crates/wasm-compose/tests/compositions/invalid-instantiation-arg/config.yml
index 9117c70a..76c12e83 100644
--- a/crates/wasm-compose/tests/compositions/invalid-instantiation-arg/config.yml
+++ b/crates/wasm-compose/tests/compositions/invalid-instantiation-arg/config.yml
@@ -1,4 +1,4 @@
 instantiations:
-  $input:
+  root:
     arguments:
       invalid: a
diff --git a/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/a.wat b/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/a.wat
new file mode 100644
index 00000000..c026d7b8
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/a.wat
@@ -0,0 +1,5 @@
+(component
+  (import "b" (instance (export "a" (func))))
+  (alias export 0 "a" (func))
+  (export "a" (func 0))
+)
diff --git a/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/config.yml b/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/config.yml
new file mode 100644
index 00000000..54d12620
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/config.yml
@@ -0,0 +1,6 @@
+instantiations:
+  root:
+    arguments:
+      locked-dep=<foo:add@1.0.>: a1
+  a1:
+    dependency: a
diff --git a/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/error.txt b/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/error.txt
new file mode 100644
index 00000000..3cfba13a
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/error.txt
@@ -0,0 +1,5 @@
+failed to parse component `tests/compositions/locked-dep-invalid-semver/root.wat`
+
+Caused by:
+    import name `locked-dep=<foo:add@1.0.>` is not a valid extern name
+`1.0.` is not a valid semver: unexpected end of input while parsing patch version number (at offset 0x1b)
\ No newline at end of file
diff --git a/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/root.wat b/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/root.wat
new file mode 100644
index 00000000..8b4b1fb8
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/locked-dep-invalid-semver/root.wat
@@ -0,0 +1,3 @@
+(component
+  (import "locked-dep=<foo:add@1.0.>" (instance (export "a" (func))))
+)
diff --git a/crates/wasm-compose/tests/compositions/locked-dep-simple/a.wat b/crates/wasm-compose/tests/compositions/locked-dep-simple/a.wat
new file mode 100644
index 00000000..c026d7b8
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/locked-dep-simple/a.wat
@@ -0,0 +1,5 @@
+(component
+  (import "b" (instance (export "a" (func))))
+  (alias export 0 "a" (func))
+  (export "a" (func 0))
+)
diff --git a/crates/wasm-compose/tests/compositions/locked-dep-simple/composed.wat b/crates/wasm-compose/tests/compositions/locked-dep-simple/composed.wat
new file mode 100644
index 00000000..6f738756
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/locked-dep-simple/composed.wat
@@ -0,0 +1,37 @@
+(component
+  (type (;0;)
+    (instance
+      (type (;0;) (func))
+      (export (;0;) "a" (func (type 0)))
+    )
+  )
+  (import "b" (instance (;0;) (type 0)))
+  (component (;0;)
+    (type (;0;)
+      (instance
+        (type (;0;) (func))
+        (export (;0;) "a" (func (type 0)))
+      )
+    )
+    (import "locked-dep=<foo:add@1.0.0>" (instance (;0;) (type 0)))
+  )
+  (component (;1;)
+    (type (;0;)
+      (instance
+        (type (;0;) (func))
+        (export (;0;) "a" (func (type 0)))
+      )
+    )
+    (import "b" (instance (;0;) (type 0)))
+    (alias export 0 "a" (func (;0;)))
+    (export (;1;) "a" (func 0))
+  )
+  (instance (;1;) (instantiate 1
+      (with "b" (instance 0))
+    )
+  )
+  (instance (;2;) (instantiate 0
+      (with "locked-dep=<foo:add@1.0.0>" (instance 1))
+    )
+  )
+)
\ No newline at end of file
diff --git a/crates/wasm-compose/tests/compositions/locked-dep-simple/config.yml b/crates/wasm-compose/tests/compositions/locked-dep-simple/config.yml
new file mode 100644
index 00000000..dc700e55
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/locked-dep-simple/config.yml
@@ -0,0 +1,6 @@
+instantiations:
+  root:
+    arguments:
+      locked-dep=<foo:add@1.0.0>: a1
+  a1:
+    dependency: a
diff --git a/crates/wasm-compose/tests/compositions/locked-dep-simple/root.wat b/crates/wasm-compose/tests/compositions/locked-dep-simple/root.wat
new file mode 100644
index 00000000..db0ad938
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/locked-dep-simple/root.wat
@@ -0,0 +1,3 @@
+(component
+  (import "locked-dep=<foo:add@1.0.0>" (instance (export "a" (func))))
+)
diff --git a/crates/wasm-compose/tests/compositions/missing-explicit-export/config.yml b/crates/wasm-compose/tests/compositions/missing-explicit-export/config.yml
index 0a0d48cc..0000489a 100644
--- a/crates/wasm-compose/tests/compositions/missing-explicit-export/config.yml
+++ b/crates/wasm-compose/tests/compositions/missing-explicit-export/config.yml
@@ -1,5 +1,5 @@
 instantiations:
-  $input:
+  root:
     arguments:
       a:
         instance: a
diff --git a/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/a.wat b/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/a.wat
new file mode 100644
index 00000000..c026d7b8
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/a.wat
@@ -0,0 +1,5 @@
+(component
+  (import "b" (instance (export "a" (func))))
+  (alias export 0 "a" (func))
+  (export "a" (func 0))
+)
diff --git a/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/config.yml b/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/config.yml
new file mode 100644
index 00000000..ebfa9050
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/config.yml
@@ -0,0 +1,6 @@
+instantiations:
+  root:
+    arguments:
+      unlocked-dep=<foo:add@{>=1.0.}>: a1
+  a1:
+    dependency: a
diff --git a/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/error.txt b/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/error.txt
new file mode 100644
index 00000000..e43e6938
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/error.txt
@@ -0,0 +1,5 @@
+failed to parse component `tests/compositions/unlocked-dep-invalid-semver/root.wat`
+
+Caused by:
+    import name `unlocked-dep=<foo:add@{>=1.0.}>` is not a valid extern name
+`1.0.` is not a valid semver: unexpected end of input while parsing patch version number (at offset 0x1b)
\ No newline at end of file
diff --git a/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/root.wat b/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/root.wat
new file mode 100644
index 00000000..28c7c4a9
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/unlocked-dep-invalid-semver/root.wat
@@ -0,0 +1,3 @@
+(component
+  (import "unlocked-dep=<foo:add@{>=1.0.}>" (instance (export "a" (func))))
+)
diff --git a/crates/wasm-compose/tests/compositions/unlocked-dep-simple/a.wat b/crates/wasm-compose/tests/compositions/unlocked-dep-simple/a.wat
new file mode 100644
index 00000000..c026d7b8
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/unlocked-dep-simple/a.wat
@@ -0,0 +1,5 @@
+(component
+  (import "b" (instance (export "a" (func))))
+  (alias export 0 "a" (func))
+  (export "a" (func 0))
+)
diff --git a/crates/wasm-compose/tests/compositions/unlocked-dep-simple/composed.wat b/crates/wasm-compose/tests/compositions/unlocked-dep-simple/composed.wat
new file mode 100644
index 00000000..286c359b
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/unlocked-dep-simple/composed.wat
@@ -0,0 +1,37 @@
+(component
+  (type (;0;)
+    (instance
+      (type (;0;) (func))
+      (export (;0;) "a" (func (type 0)))
+    )
+  )
+  (import "b" (instance (;0;) (type 0)))
+  (component (;0;)
+    (type (;0;)
+      (instance
+        (type (;0;) (func))
+        (export (;0;) "a" (func (type 0)))
+      )
+    )
+    (import "unlocked-dep=<foo:add@{>=1.0.0}>" (instance (;0;) (type 0)))
+  )
+  (component (;1;)
+    (type (;0;)
+      (instance
+        (type (;0;) (func))
+        (export (;0;) "a" (func (type 0)))
+      )
+    )
+    (import "b" (instance (;0;) (type 0)))
+    (alias export 0 "a" (func (;0;)))
+    (export (;1;) "a" (func 0))
+  )
+  (instance (;1;) (instantiate 1
+      (with "b" (instance 0))
+    )
+  )
+  (instance (;2;) (instantiate 0
+      (with "unlocked-dep=<foo:add@{>=1.0.0}>" (instance 1))
+    )
+  )
+)
\ No newline at end of file
diff --git a/crates/wasm-compose/tests/compositions/unlocked-dep-simple/config.yml b/crates/wasm-compose/tests/compositions/unlocked-dep-simple/config.yml
new file mode 100644
index 00000000..127ef67c
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/unlocked-dep-simple/config.yml
@@ -0,0 +1,6 @@
+instantiations:
+  root:
+    arguments:
+      unlocked-dep=<foo:add@{>=1.0.0}>: a1
+  a1:
+    dependency: a
diff --git a/crates/wasm-compose/tests/compositions/unlocked-dep-simple/root.wat b/crates/wasm-compose/tests/compositions/unlocked-dep-simple/root.wat
new file mode 100644
index 00000000..b2d3267d
--- /dev/null
+++ b/crates/wasm-compose/tests/compositions/unlocked-dep-simple/root.wat
@@ -0,0 +1,3 @@
+(component
+  (import "unlocked-dep=<foo:add@{>=1.0.0}>" (instance (export "a" (func))))
+)
diff --git a/tests/local/missing-features/reference-types/gc.wast b/tests/local/missing-features/reference-types/gc.wast
index dd1e45ba..1a63b7d9 100644
--- a/tests/local/missing-features/reference-types/gc.wast
+++ b/tests/local/missing-features/reference-types/gc.wast
@@ -28,3 +28,6 @@
 (assert_invalid
   (module (func (block (result i31ref) unreachable)))
   "heap types not supported without the gc feature")
+(assert_invalid
+  (module (func ref.null array drop))
+  "heap types not supported without the gc feature")