Repository: bytecodealliance/wasm-tools
Pull request #1593 (2024-06-05 20:57:16)

More internal refactorings of `wasmprinter` to print to an I/O source
This PR is a series of commits where the intention is that each commit individually is a bite-sized chunk which moves `wasmprinter` towards being able to print directly to an I/O source instead of unconditionally to a `String`. The eventual end goal is to avoid the need to buffer the entire output of the printing and additionally plumb through colors from `termcolor` to be able to print a "pretty" module as opposed to the bland monochrome modules that are printed today. The CLI has all the necessary hooks and such to print colors, but so far they aren't plumbed to many locations.

This PR alone does not enable colors, it's just intended to make the next and final PR to add colors much smaller. As a whole this PR may be a bit messy but each commit should individually pass tests and showcase which refactorings were necessary to get towards this goal.


Patch:
diff --git a/crates/wasmprinter/src/lib.rs b/crates/wasmprinter/src/lib.rs
index 9dcc9d9b..54f851d8 100644
--- a/crates/wasmprinter/src/lib.rs
+++ b/crates/wasmprinter/src/lib.rs
@@ -28,25 +28,33 @@ mod operator;
 pub fn print_file(file: impl AsRef<Path>) -> Result<String> {
     let file = file.as_ref();
     let contents = std::fs::read(file).context(format!("failed to read `{}`", file.display()))?;
-    Printer::new().print(&contents)
+    print_bytes(contents)
 }
 
 /// Prints an in-memory `wasm` binary blob into an in-memory `String` which is
 /// its textual representation.
 pub fn print_bytes(wasm: impl AsRef<[u8]>) -> Result<String> {
-    Printer::new().print(wasm.as_ref())
+    let mut dst = String::new();
+    Config::new().print(wasm.as_ref(), &mut dst)?;
+    Ok(dst)
 }
 
-/// Context used for printing a WebAssembly binary.
+/// Configuration used to print a WebAssembly binary.
 ///
-/// This is largely only required if you'd like to register custom printers for
-/// custom sections in a wasm binary.
+/// This structure is used to control the overal structure of how wasm binaries
+/// are printed and tweaks various ways that configures the output.
 #[derive(Default)]
-pub struct Printer {
+pub struct Config {
     print_offsets: bool,
     print_skeleton: bool,
-    printers: HashMap<String, Box<dyn FnMut(&mut Printer, usize, &[u8]) -> Result<()>>>,
-    result: String,
+    printers: HashMap<String, Box<dyn Fn(&mut Printer, usize, &[u8]) -> Result<()>>>,
+    name_unnamed: bool,
+}
+
+/// This structure is the actual structure that prints WebAssembly binaries.
+pub struct Printer<'cfg> {
+    config: &'cfg Config,
+    result: &'cfg mut String,
     /// The `i`th line in `result` is at offset `lines[i]`.
     lines: Vec<usize>,
     /// The binary offset for the `i`th line is `line_offsets[i]`.
@@ -55,7 +63,6 @@ pub struct Printer {
     line: usize,
     group_lines: Vec<usize>,
     code_section_hints: Vec<(u32, Vec<(usize, BranchHint)>)>,
-    name_unnamed: bool,
 }
 
 #[derive(Default)]
@@ -145,8 +152,8 @@ struct Naming {
     name: String,
 }
 
-impl Printer {
-    /// Creates a new `Printer` object that's ready to start printing wasm
+impl Config {
+    /// Creates a new [`Config`] object that's ready to start printing wasm
     /// binaries to strings.
     pub fn new() -> Self {
         Self::default()
@@ -196,38 +203,58 @@ impl Printer {
     pub fn add_custom_section_printer(
         &mut self,
         section: &str,
-        printer: impl FnMut(&mut Printer, usize, &[u8]) -> Result<()> + 'static,
+        printer: impl Fn(&mut Printer, usize, &[u8]) -> Result<()> + 'static,
     ) {
         self.printers.insert(section.to_string(), Box::new(printer));
     }
 
-    /// Gets the output result of this `Printer`, or where all output is going.
-    pub fn result_mut(&mut self) -> &mut String {
-        &mut self.result
-    }
-
     /// Prints a WebAssembly binary into a `String`
     ///
     /// This function takes an entire `wasm` binary blob and will print it to
     /// the WebAssembly Text Format and return the result as a `String`.
-    pub fn print(&mut self, wasm: &[u8]) -> Result<String> {
-        self.print_contents(wasm)?;
-        Ok(mem::take(&mut self.result))
+    pub fn print(&mut self, wasm: &[u8], dst: &mut String) -> Result<()> {
+        Printer {
+            config: self,
+            result: dst,
+            code_section_hints: Vec::new(),
+            group_lines: Vec::new(),
+            line: 0,
+            nesting: 0,
+            lines: Vec::new(),
+            line_offsets: Vec::new(),
+        }
+        .print_contents(wasm)
     }
 
     /// Get the line-by-line WAT disassembly for the given Wasm, along with the
     /// binary offsets for each line.
     pub fn offsets_and_lines<'a>(
-        &'a mut self,
+        &self,
         wasm: &[u8],
+        storage: &'a mut String,
     ) -> Result<impl Iterator<Item = (Option<usize>, &'a str)> + 'a> {
-        self.print_contents(wasm)?;
+        let mut printer = Printer {
+            config: self,
+            result: storage,
+            code_section_hints: Vec::new(),
+            group_lines: Vec::new(),
+            line: 0,
+            nesting: 0,
+            lines: Vec::new(),
+            line_offsets: Vec::new(),
+        };
+        printer.print_contents(wasm)?;
 
-        let end = self.result.len();
-        let result = &self.result;
+        let Printer {
+            lines,
+            line_offsets,
+            ..
+        } = printer;
+        let end = storage.len();
+        let result = &storage[..];
 
-        let mut offsets = self.line_offsets.iter().copied();
-        let mut lines = self.lines.iter().copied().peekable();
+        let mut offsets = line_offsets.into_iter();
+        let mut lines = lines.into_iter().peekable();
 
         Ok(std::iter::from_fn(move || {
             let offset = offsets.next()?;
@@ -237,7 +264,9 @@ impl Printer {
             Some((offset, line))
         }))
     }
+}
 
+impl Printer<'_> {
     fn read_names_and_code<'a>(
         &mut self,
         mut bytes: &'a [u8],
@@ -360,24 +389,24 @@ impl Printer {
                             states.push(State::new(Encoding::Module));
                             states.last_mut().unwrap().custom_section_place = Some("before first");
                             if states.len() > 1 {
-                                self.start_group("core module");
+                                self.start_group("core module")?;
                             } else {
-                                self.start_group("module");
+                                self.start_group("module")?;
                             }
 
                             if states.len() > 1 {
                                 let parent = &states[states.len() - 2];
-                                self.result.push(' ');
+                                self.result.write_str(" ")?;
                                 self.print_name(&parent.core.module_names, parent.core.modules)?;
                             }
                         }
                         Encoding::Component => {
                             states.push(State::new(Encoding::Component));
-                            self.start_group("component");
+                            self.start_group("component")?;
 
                             if states.len() > 1 {
                                 let parent = &states[states.len() - 2];
-                                self.result.push(' ');
+                                self.result.write_str(" ")?;
                                 self.print_name(
                                     &parent.component.component_names,
                                     parent.component.components,
@@ -398,47 +427,51 @@ impl Printer {
 
                     if len == 1 {
                         if let Some(name) = state.name.as_ref() {
-                            self.result.push(' ');
+                            self.result.write_str(" ")?;
                             name.write(&mut self.result);
                         }
                     }
                 }
                 Payload::CustomSection(c) => {
                     let mut printed = false;
-                    let mut printers = mem::take(&mut self.printers);
-                    if let Some(printer) = printers.get_mut(c.name()) {
+                    // let mut printers = mem::take(&mut self.printers);
+                    if let Some(printer) = self.config.printers.get(c.name()) {
                         printer(self, c.data_offset(), c.data())?;
                         printed = true;
                     }
-                    self.printers = printers;
-
-                    // Attempt to print this custom section if it wasn't already
-                    // handled specifically above. Note that custom sections are
-                    // allowed to be invalid, so any parse error here is ignored
-                    // and resets the output to what it was when this started.
-                    // Otherwise I/O errors and other things are plumbed through
-                    // in case they happen.
+                    // self.printers = printers;
+
                     if printed {
                         continue;
                     }
-                    let cur = self.result.len();
+
+                    // If this wasn't handled specifically above then try to
+                    // print the known custom builtin sections. If this fails
+                    // because the custom section is malformed then don't fail
+                    // the whole print.
                     let state = states.last().unwrap();
+                    let start = self.nesting;
                     let err = match self.print_custom_section(state, c.clone()) {
                         Ok(()) => continue,
+                        Err(e) if !e.is::<BinaryReaderError>() => return Err(e),
                         Err(e) => e,
                     };
-                    if !err.is::<BinaryReaderError>() {
-                        return Err(err);
+
+                    // Restore the nesting level to what it was prior to
+                    // starting to print the custom section. Then emit a comment
+                    // indicating why the custom section failed to parse.
+                    //
+                    // This may leave broken syntax in the output module but
+                    // it's probably better than swallowing the error.
+                    while self.nesting > start {
+                        self.end_group()?;
                     }
-                    self.result.truncate(cur);
                     let msg = format!("failed to parse custom section `{}`: {err}", c.name());
                     for line in msg.lines() {
-                        self.newline(c.data_offset());
-                        self.result.push_str(";; ");
-                        self.result.push_str(line);
+                        self.newline(c.data_offset())?;
+                        write!(self.result, ";; {line}")?;
                     }
-                    self.newline(c.range().end);
-                    self.print_raw_custom_section(state, c)?;
+                    self.newline(c.range().end)?;
                 }
                 Payload::TypeSection(s) => {
                     self.update_custom_section_place(&mut states, "after type");
@@ -488,10 +521,10 @@ impl Printer {
                 Payload::StartSection { func, range } => {
                     self.update_custom_section_place(&mut states, "after start");
                     Self::ensure_module(&states)?;
-                    self.newline(range.start);
-                    self.start_group("start ");
+                    self.newline(range.start)?;
+                    self.start_group("start ")?;
                     self.print_idx(&states.last().unwrap().core.func_names, func)?;
-                    self.end_group();
+                    self.end_group()?;
                 }
                 Payload::ElementSection(s) => {
                     self.update_custom_section_place(&mut states, "after element");
@@ -525,7 +558,7 @@ impl Printer {
                     expected = Some(Encoding::Module);
                     parsers.push(parser);
                     parser = inner;
-                    self.newline(range.start);
+                    self.newline(range.start)?;
                 }
                 Payload::InstanceSection(s) => {
                     Self::ensure_component(&states)?;
@@ -540,7 +573,7 @@ impl Printer {
                     expected = Some(Encoding::Component);
                     parsers.push(parser);
                     parser = inner;
-                    self.newline(range.start);
+                    self.newline(range.start)?;
                 }
                 Payload::ComponentInstanceSection(s) => {
                     Self::ensure_component(&states)?;
@@ -572,7 +605,7 @@ impl Printer {
                 }
 
                 Payload::End(offset) => {
-                    self.end_group(); // close the `module` or `component` group
+                    self.end_group()?; // close the `module` or `component` group
 
                     let state = states.pop().unwrap();
                     if let Some(parent) = states.last_mut() {
@@ -586,9 +619,9 @@ impl Printer {
                         }
                         parser = parsers.pop().unwrap();
                     } else {
-                        self.newline(offset);
-                        if self.print_offsets {
-                            self.result.push('\n');
+                        self.newline(offset)?;
+                        if self.config.print_offsets {
+                            self.result.write_str("\n")?;
                         }
                         break;
                     }
@@ -609,21 +642,22 @@ impl Printer {
         }
     }
 
-    fn start_group(&mut self, name: &str) {
-        self.result.push('(');
-        self.result.push_str(name);
+    fn start_group(&mut self, name: &str) -> Result<()> {
+        write!(self.result, "({name}")?;
         self.nesting += 1;
         self.group_lines.push(self.line);
+        Ok(())
     }
 
-    fn end_group(&mut self) {
+    fn end_group(&mut self) -> Result<()> {
         self.nesting -= 1;
         if let Some(line) = self.group_lines.pop() {
             if line != self.line {
-                self.newline_unknown_pos();
+                self.newline_unknown_pos()?;
             }
         }
-        self.result.push(')');
+        self.result.write_str(")")?;
+        Ok(())
     }
 
     fn register_names(&mut self, state: &mut State, names: NameSectionReader<'_>) -> Result<()> {
@@ -722,7 +756,7 @@ impl Printer {
     }
 
     fn print_core_type(&mut self, states: &mut Vec<State>, ty: CoreType) -> Result<()> {
-        self.start_group("core type ");
+        self.start_group("core type ")?;
         self.print_name(
             &states.last().unwrap().core.type_names,
             states.last().unwrap().core.types.len() as u32,
@@ -735,10 +769,10 @@ impl Printer {
                         unreachable!("Wasm GC types cannot appear in components yet")
                     }
                 };
-                self.result.push(' ');
-                self.start_group("func");
+                self.result.write_str(" ")?;
+                self.start_group("func")?;
                 self.print_func_type(states.last().unwrap(), &ty, None)?;
-                self.end_group();
+                self.end_group()?;
                 Some(SubType {
                     is_final: true,
                     supertype_idx: None,
@@ -750,7 +784,7 @@ impl Printer {
                 None
             }
         };
-        self.end_group(); // `core type` itself
+        self.end_group()?; // `core type` itself
 
         states.last_mut().unwrap().core.types.push(ty);
         Ok(())
@@ -762,32 +796,32 @@ impl Printer {
         offset: usize,
         types: impl Iterator<Item = SubType>,
     ) -> Result<()> {
-        self.start_group("rec");
+        self.start_group("rec")?;
         for ty in types {
-            self.newline(offset + 2);
+            self.newline(offset + 2)?;
             self.print_type(state, ty)?;
         }
-        self.end_group(); // `rec`
+        self.end_group()?; // `rec`
         Ok(())
     }
 
     fn print_type(&mut self, state: &mut State, ty: SubType) -> Result<()> {
-        self.start_group("type ");
+        self.start_group("type ")?;
         let ty_idx = state.core.types.len() as u32;
         self.print_name(&state.core.type_names, ty_idx)?;
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         self.print_sub(state, &ty, ty_idx)?;
-        self.end_group(); // `type`
+        self.end_group()?; // `type`
         state.core.types.push(Some(ty));
         Ok(())
     }
 
     fn print_sub(&mut self, state: &State, ty: &SubType, ty_idx: u32) -> Result<u32> {
         let r = if !ty.is_final || !ty.supertype_idx.is_none() {
-            self.start_group("sub");
+            self.start_group("sub")?;
             self.print_sub_type(state, ty)?;
             let r = self.print_composite(state, &ty.composite_type, ty_idx)?;
-            self.end_group(); // `sub`
+            self.end_group()?; // `sub`
             r
         } else {
             self.print_composite(state, &ty.composite_type, ty_idx)?
@@ -798,21 +832,21 @@ impl Printer {
     fn print_composite(&mut self, state: &State, ty: &CompositeType, ty_idx: u32) -> Result<u32> {
         let r = match &ty {
             CompositeType::Func(ty) => {
-                self.start_group("func");
+                self.start_group("func")?;
                 let r = self.print_func_type(state, ty, None)?;
-                self.end_group(); // `func`
+                self.end_group()?; // `func`
                 r
             }
             CompositeType::Array(ty) => {
-                self.start_group("array");
+                self.start_group("array")?;
                 let r = self.print_array_type(state, ty)?;
-                self.end_group(); // `array`
+                self.end_group()?; // `array`
                 r
             }
             CompositeType::Struct(ty) => {
-                self.start_group("struct");
+                self.start_group("struct")?;
                 let r = self.print_struct_type(state, ty, ty_idx)?;
-                self.end_group(); // `struct`
+                self.end_group()?; // `struct`
                 r
             }
         };
@@ -826,7 +860,7 @@ impl Printer {
     ) -> Result<()> {
         for ty in parser.into_iter_with_offsets() {
             let (offset, ty) = ty?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_core_type(states, ty)?;
         }
 
@@ -836,7 +870,7 @@ impl Printer {
     fn print_types(&mut self, state: &mut State, parser: TypeSectionReader<'_>) -> Result<()> {
         for ty in parser.into_iter_with_offsets() {
             let (offset, rec_group) = ty?;
-            self.newline(offset);
+            self.newline(offset)?;
             if rec_group.is_explicit_rec_group() {
                 self.print_rec(state, offset, rec_group.into_types())?
             } else {
@@ -875,7 +909,7 @@ impl Printer {
         names_for: Option<u32>,
     ) -> Result<u32> {
         if !ty.params().is_empty() {
-            self.result.push(' ');
+            self.result.write_str(" ")?;
         }
 
         let mut params = NamedLocalPrinter::new("param");
@@ -883,18 +917,19 @@ impl Printer {
         // we need to be careful to terminate previous param blocks and open
         // a new one if that's the case with a named parameter.
         for (i, param) in ty.params().iter().enumerate() {
-            params.start_local(names_for.unwrap_or(u32::MAX), i as u32, self, state);
+            params.start_local(names_for.unwrap_or(u32::MAX), i as u32, self, state)?;
             self.print_valtype(state, *param)?;
-            params.end_local(&mut self.result);
+            params.end_local(self)?;
         }
-        params.finish(&mut self.result);
+        params.finish(self)?;
         if !ty.results().is_empty() {
-            self.result.push_str(" (result");
+            self.result.write_str(" ")?;
+            self.start_group("result")?;
             for result in ty.results().iter() {
-                self.result.push(' ');
+                self.result.write_str(" ")?;
                 self.print_valtype(state, *result)?;
             }
-            self.result.push(')');
+            self.end_group()?;
         }
         Ok(ty.params().len() as u32)
     }
@@ -905,20 +940,20 @@ impl Printer {
         ty: &FieldType,
         ty_field_idx: Option<(u32, u32)>,
     ) -> Result<u32> {
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         if let Some(idxs @ (_, field_idx)) = ty_field_idx {
             match state.core.field_names.index_to_name.get(&idxs) {
                 Some(name) => write!(self.result, "${} ", name.identifier())?,
-                None if self.name_unnamed => write!(self.result, "$#field{field_idx} ")?,
+                None if self.config.name_unnamed => write!(self.result, "$#field{field_idx} ")?,
                 None => {}
             }
         }
         if ty.mutable {
-            self.result.push_str("(mut ");
+            self.result.write_str("(mut ")?;
         }
         self.print_storage_type(state, ty.element_type)?;
         if ty.mutable {
-            self.result.push_str(")");
+            self.result.write_str(")")?;
         }
         Ok(0)
     }
@@ -929,29 +964,29 @@ impl Printer {
 
     fn print_struct_type(&mut self, state: &State, ty: &StructType, ty_idx: u32) -> Result<u32> {
         for (field_index, field) in ty.fields.iter().enumerate() {
-            self.result.push_str(" (field");
+            self.result.write_str(" (field")?;
             self.print_field_type(state, field, Some((ty_idx, field_index as u32)))?;
-            self.result.push(')');
+            self.result.write_str(")")?;
         }
         Ok(0)
     }
 
     fn print_sub_type(&mut self, state: &State, ty: &SubType) -> Result<u32> {
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         if ty.is_final {
-            self.result.push_str("final ");
+            self.result.write_str("final ")?;
         }
         if let Some(idx) = ty.supertype_idx {
             self.print_idx(&state.core.type_names, idx.as_module_index().unwrap())?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
         }
         Ok(0)
     }
 
     fn print_storage_type(&mut self, state: &State, ty: StorageType) -> Result<()> {
         match ty {
-            StorageType::I8 => self.result.push_str("i8"),
-            StorageType::I16 => self.result.push_str("i16"),
+            StorageType::I8 => self.result.write_str("i8")?,
+            StorageType::I16 => self.result.write_str("i16")?,
             StorageType::Val(val_type) => self.print_valtype(state, val_type)?,
         }
         Ok(())
@@ -959,11 +994,11 @@ impl Printer {
 
     fn print_valtype(&mut self, state: &State, ty: ValType) -> Result<()> {
         match ty {
-            ValType::I32 => self.result.push_str("i32"),
-            ValType::I64 => self.result.push_str("i64"),
-            ValType::F32 => self.result.push_str("f32"),
-            ValType::F64 => self.result.push_str("f64"),
-            ValType::V128 => self.result.push_str("v128"),
+            ValType::I32 => self.result.write_str("i32")?,
+            ValType::I64 => self.result.write_str("i64")?,
+            ValType::F32 => self.result.write_str("f32")?,
+            ValType::F64 => self.result.write_str("f64")?,
+            ValType::V128 => self.result.write_str("v128")?,
             ValType::Ref(rt) => self.print_reftype(state, rt)?,
         }
         Ok(())
@@ -972,46 +1007,47 @@ impl Printer {
     fn print_reftype(&mut self, state: &State, ty: RefType) -> Result<()> {
         if ty.is_nullable() {
             match ty.as_non_null() {
-                RefType::FUNC => self.result.push_str("funcref"),
-                RefType::EXTERN => self.result.push_str("externref"),
-                RefType::I31 => self.result.push_str("i31ref"),
-                RefType::ANY => self.result.push_str("anyref"),
-                RefType::NONE => self.result.push_str("nullref"),
-                RefType::NOEXTERN => self.result.push_str("nullexternref"),
-                RefType::NOFUNC => self.result.push_str("nullfuncref"),
-                RefType::EQ => self.result.push_str("eqref"),
-                RefType::STRUCT => self.result.push_str("structref"),
-                RefType::ARRAY => self.result.push_str("arrayref"),
-                RefType::EXN => self.result.push_str("exnref"),
-                RefType::NOEXN => self.result.push_str("nullexnref"),
+                RefType::FUNC => self.result.write_str("funcref")?,
+                RefType::EXTERN => self.result.write_str("externref")?,
+                RefType::I31 => self.result.write_str("i31ref")?,
+                RefType::ANY => self.result.write_str("anyref")?,
+                RefType::NONE => self.result.write_str("nullref")?,
+                RefType::NOEXTERN => self.result.write_str("nullexternref")?,
+                RefType::NOFUNC => self.result.write_str("nullfuncref")?,
+                RefType::EQ => self.result.write_str("eqref")?,
+                RefType::STRUCT => self.result.write_str("structref")?,
+                RefType::ARRAY => self.result.write_str("arrayref")?,
+                RefType::EXN => self.result.write_str("exnref")?,
+                RefType::NOEXN => self.result.write_str("nullexnref")?,
                 _ => {
-                    self.result.push_str("(ref null ");
+                    self.start_group("ref")?;
+                    self.result.write_str(" null ")?;
                     self.print_heaptype(state, ty.heap_type())?;
-                    self.result.push_str(")");
+                    self.end_group()?;
                 }
             }
         } else {
-            self.result.push_str("(ref ");
+            self.start_group("ref ")?;
             self.print_heaptype(state, ty.heap_type())?;
-            self.result.push_str(")");
+            self.end_group()?;
         }
         Ok(())
     }
 
     fn print_heaptype(&mut self, state: &State, ty: HeapType) -> Result<()> {
         match ty {
-            HeapType::Func => self.result.push_str("func"),
-            HeapType::Extern => self.result.push_str("extern"),
-            HeapType::Any => self.result.push_str("any"),
-            HeapType::None => self.result.push_str("none"),
-            HeapType::NoExtern => self.result.push_str("noextern"),
-            HeapType::NoFunc => self.result.push_str("nofunc"),
-            HeapType::Eq => self.result.push_str("eq"),
-            HeapType::Struct => self.result.push_str("struct"),
-            HeapType::Array => self.result.push_str("array"),
-            HeapType::I31 => self.result.push_str("i31"),
-            HeapType::Exn => self.result.push_str("exn"),
-            HeapType::NoExn => self.result.push_str("noexn"),
+            HeapType::Func => self.result.write_str("func")?,
+            HeapType::Extern => self.result.write_str("extern")?,
+            HeapType::Any => self.result.write_str("any")?,
+            HeapType::None => self.result.write_str("none")?,
+            HeapType::NoExtern => self.result.write_str("noextern")?,
+            HeapType::NoFunc => self.result.write_str("nofunc")?,
+            HeapType::Eq => self.result.write_str("eq")?,
+            HeapType::Struct => self.result.write_str("struct")?,
+            HeapType::Array => self.result.write_str("array")?,
+            HeapType::I31 => self.result.write_str("i31")?,
+            HeapType::Exn => self.result.write_str("exn")?,
+            HeapType::NoExn => self.result.write_str("noexn")?,
             HeapType::Concrete(i) => {
                 self.print_idx(&state.core.type_names, i.as_module_index().unwrap())?;
             }
@@ -1022,7 +1058,7 @@ impl Printer {
     fn print_imports(&mut self, state: &mut State, parser: ImportSectionReader<'_>) -> Result<()> {
         for import in parser.into_iter_with_offsets() {
             let (offset, import) = import?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_import(state, &import, true)?;
             match import.ty {
                 TypeRef::Func(_) => state.core.funcs += 1,
@@ -1036,23 +1072,23 @@ impl Printer {
     }
 
     fn print_import(&mut self, state: &State, import: &Import<'_>, index: bool) -> Result<()> {
-        self.start_group("import ");
+        self.start_group("import ")?;
         self.print_str(import.module)?;
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         self.print_str(import.name)?;
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         self.print_import_ty(state, &import.ty, index)?;
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
     fn print_import_ty(&mut self, state: &State, ty: &TypeRef, index: bool) -> Result<()> {
         match ty {
             TypeRef::Func(f) => {
-                self.start_group("func ");
+                self.start_group("func ")?;
                 if index {
                     self.print_name(&state.core.func_names, state.core.funcs)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                 }
                 self.print_core_type_ref(state, *f)?;
             }
@@ -1061,37 +1097,37 @@ impl Printer {
             TypeRef::Tag(f) => self.print_tag_type(state, f, index)?,
             TypeRef::Global(f) => self.print_global_type(state, f, index)?,
         }
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
     fn print_table_type(&mut self, state: &State, ty: &TableType, index: bool) -> Result<()> {
-        self.start_group("table ");
+        self.start_group("table ")?;
         if index {
             self.print_name(&state.core.table_names, state.core.tables)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
         }
         if ty.table64 {
-            self.result.push_str("i64 ");
+            self.result.write_str("i64 ")?;
         }
         self.print_limits(ty.initial, ty.maximum)?;
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         self.print_reftype(state, ty.element_type)?;
         Ok(())
     }
 
     fn print_memory_type(&mut self, state: &State, ty: &MemoryType, index: bool) -> Result<()> {
-        self.start_group("memory ");
+        self.start_group("memory ")?;
         if index {
             self.print_name(&state.core.memory_names, state.core.memories)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
         }
         if ty.memory64 {
-            self.result.push_str("i64 ");
+            self.result.write_str("i64 ")?;
         }
         self.print_limits(ty.initial, ty.maximum)?;
         if ty.shared {
-            self.result.push_str(" shared");
+            self.result.write_str(" shared")?;
         }
         if let Some(p) = ty.page_size_log2 {
             let p = 1_u64
@@ -1103,10 +1139,10 @@ impl Printer {
     }
 
     fn print_tag_type(&mut self, state: &State, ty: &TagType, index: bool) -> Result<()> {
-        self.start_group("tag ");
+        self.start_group("tag ")?;
         if index {
             self.print_name(&state.core.tag_names, state.core.tags)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
         }
         self.print_core_functype_idx(state, ty.func_type_idx, None)?;
         Ok(())
@@ -1124,21 +1160,21 @@ impl Printer {
     }
 
     fn print_global_type(&mut self, state: &State, ty: &GlobalType, index: bool) -> Result<()> {
-        self.start_group("global ");
+        self.start_group("global ")?;
         if index {
             self.print_name(&state.core.global_names, state.core.globals)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
         }
         if ty.shared || ty.mutable {
-            self.result.push('(');
+            self.result.write_str("(")?;
             if ty.shared {
-                self.result.push_str("shared ");
+                self.result.write_str("shared ")?;
             }
             if ty.mutable {
-                self.result.push_str("mut ");
+                self.result.write_str("mut ")?;
             }
             self.print_valtype(state, ty.content_type)?;
-            self.result.push(')');
+            self.result.write_str(")")?;
         } else {
             self.print_valtype(state, ty.content_type)?;
         }
@@ -1148,16 +1184,16 @@ impl Printer {
     fn print_tables(&mut self, state: &mut State, parser: TableSectionReader<'_>) -> Result<()> {
         for table in parser.into_iter_with_offsets() {
             let (offset, table) = table?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_table_type(state, &table.ty, true)?;
             match &table.init {
                 TableInit::RefNull => {}
                 TableInit::Expr(expr) => {
-                    self.result.push_str(" ");
+                    self.result.write_str(" ")?;
                     self.print_const_expr(state, expr)?;
                 }
             }
-            self.end_group();
+            self.end_group()?;
             state.core.tables += 1;
         }
         Ok(())
@@ -1166,9 +1202,9 @@ impl Printer {
     fn print_memories(&mut self, state: &mut State, parser: MemorySectionReader<'_>) -> Result<()> {
         for memory in parser.into_iter_with_offsets() {
             let (offset, memory) = memory?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_memory_type(state, &memory, true)?;
-            self.end_group();
+            self.end_group()?;
             state.core.memories += 1;
         }
         Ok(())
@@ -1177,9 +1213,9 @@ impl Printer {
     fn print_tags(&mut self, state: &mut State, parser: TagSectionReader<'_>) -> Result<()> {
         for tag in parser.into_iter_with_offsets() {
             let (offset, tag) = tag?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_tag_type(state, &tag, true)?;
-            self.end_group();
+            self.end_group()?;
             state.core.tags += 1;
         }
         Ok(())
@@ -1188,11 +1224,11 @@ impl Printer {
     fn print_globals(&mut self, state: &mut State, parser: GlobalSectionReader<'_>) -> Result<()> {
         for global in parser.into_iter_with_offsets() {
             let (offset, global) = global?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_global_type(state, &global.ty, true)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             self.print_const_expr(state, &global.init_expr)?;
-            self.end_group();
+            self.end_group()?;
             state.core.globals += 1;
         }
         Ok(())
@@ -1211,11 +1247,11 @@ impl Printer {
             let mut body = body.get_binary_reader();
             let offset = body.original_position();
             let ty = ty?;
-            self.newline(offset);
-            self.start_group("func ");
+            self.newline(offset)?;
+            self.start_group("func ")?;
             let func_idx = state.core.funcs;
             self.print_name(&state.core.func_names, func_idx)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             let params = self
                 .print_core_functype_idx(state, ty, Some(func_idx))?
                 .unwrap_or(0);
@@ -1230,13 +1266,13 @@ impl Printer {
                 _ => Vec::new(),
             };
 
-            if self.print_skeleton {
-                self.result.push_str(" ...");
+            if self.config.print_skeleton {
+                self.result.write_str(" ...")?;
             } else {
                 self.print_func_body(state, func_idx, params, &mut body, &hints)?;
             }
 
-            self.end_group();
+            self.end_group()?;
 
             state.core.funcs += 1;
         }
@@ -1268,16 +1304,16 @@ impl Printer {
             }
             for _ in 0..cnt {
                 if first {
-                    self.newline(offset);
+                    self.newline(offset)?;
                     first = false;
                 }
-                locals.start_local(func_idx, params + local_idx, self, state);
+                locals.start_local(func_idx, params + local_idx, self, state)?;
                 self.print_valtype(state, ty)?;
-                locals.end_local(&mut self.result);
+                locals.end_local(self)?;
                 local_idx += 1;
             }
         }
-        locals.finish(&mut self.result);
+        locals.finish(self)?;
 
         let nesting_start = self.nesting;
 
@@ -1289,12 +1325,11 @@ impl Printer {
             if let Some(((hint_offset, hint), rest)) = branch_hints.split_first() {
                 if hint.func_offset == (body.original_position() - func_start) as u32 {
                     branch_hints = rest;
-                    op_printer.printer.newline(*hint_offset);
+                    op_printer.printer.newline(*hint_offset)?;
                     let desc = if hint.taken { "\"\\01\"" } else { "\"\\00\"" };
                     write!(
                         op_printer.printer.result,
-                        "(@metadata.code.branch_hint {})",
-                        desc
+                        "(@metadata.code.branch_hint {desc})",
                     )?;
                 }
             }
@@ -1310,30 +1345,30 @@ impl Printer {
         // with the closing paren printed for the func.
         if self.nesting != nesting_start {
             self.nesting = nesting_start;
-            self.newline(body.original_position());
+            self.newline(body.original_position())?;
         }
 
         Ok(())
     }
 
-    fn newline(&mut self, offset: usize) {
+    fn newline(&mut self, offset: usize) -> Result<()> {
         self.print_newline(Some(offset))
     }
 
-    fn newline_unknown_pos(&mut self) {
+    fn newline_unknown_pos(&mut self) -> Result<()> {
         self.print_newline(None)
     }
 
-    fn print_newline(&mut self, offset: Option<usize>) {
-        self.result.push('\n');
+    fn print_newline(&mut self, offset: Option<usize>) -> Result<()> {
+        self.result.write_str("\n")?;
 
         self.lines.push(self.result.len());
         self.line_offsets.push(offset);
 
-        if self.print_offsets {
+        if self.config.print_offsets {
             match offset {
-                Some(offset) => write!(self.result, "(;@{offset:<6x};)").unwrap(),
-                None => self.result.push_str("           "),
+                Some(offset) => write!(self.result, "(;@{offset:<6x};)")?,
+                None => self.result.write_str("           ")?,
             }
         }
         self.line += 1;
@@ -1342,64 +1377,67 @@ impl Printer {
         // reasonable to avoid generating hundreds of megabytes of whitespace
         // for small-ish modules that have deep-ish nesting.
         for _ in 0..self.nesting.min(MAX_NESTING_TO_PRINT) {
-            self.result.push_str("  ");
+            self.result.write_str("  ")?;
         }
+        Ok(())
     }
 
     fn print_exports(&mut self, state: &State, data: ExportSectionReader) -> Result<()> {
         for export in data.into_iter_with_offsets() {
             let (offset, export) = export?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_export(state, &export)?;
         }
         Ok(())
     }
 
     fn print_export(&mut self, state: &State, export: &Export) -> Result<()> {
-        self.start_group("export ");
+        self.start_group("export ")?;
         self.print_str(export.name)?;
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         self.print_external_kind(state, export.kind, export.index)?;
-        self.end_group(); // export
+        self.end_group()?; // export
         Ok(())
     }
 
     fn print_external_kind(&mut self, state: &State, kind: ExternalKind, index: u32) -> Result<()> {
-        self.result.push('(');
         match kind {
             ExternalKind::Func => {
-                self.result.push_str("func ");
+                self.start_group("func ")?;
                 self.print_idx(&state.core.func_names, index)?;
             }
             ExternalKind::Table => {
-                self.result.push_str("table ");
+                self.start_group("table ")?;
                 self.print_idx(&state.core.table_names, index)?;
             }
             ExternalKind::Global => {
-                self.result.push_str("global ");
+                self.start_group("global ")?;
                 self.print_idx(&state.core.global_names, index)?;
             }
             ExternalKind::Memory => {
-                self.result.push_str("memory ");
+                self.start_group("memory ")?;
                 self.print_idx(&state.core.memory_names, index)?;
             }
-            ExternalKind::Tag => write!(self.result, "tag {}", index)?,
+            ExternalKind::Tag => {
+                self.start_group("tag ")?;
+                write!(self.result, "{index}")?;
+            }
         }
-        self.result.push(')');
+        self.end_group()?;
         Ok(())
     }
 
     fn print_core_type_ref(&mut self, state: &State, idx: u32) -> Result<()> {
-        self.result.push_str("(type ");
+        self.start_group("type ")?;
         self.print_idx(&state.core.type_names, idx)?;
-        self.result.push(')');
+        self.end_group()?;
         Ok(())
     }
 
     fn print_component_type_ref(&mut self, state: &State, idx: u32) -> Result<()> {
-        self.result.push_str("(type ");
+        self.start_group("type ")?;
         self.print_idx(&state.component.type_names, idx)?;
-        self.result.push(')');
+        self.end_group()?;
         Ok(())
     }
 
@@ -1413,7 +1451,7 @@ impl Printer {
     fn _print_idx(&mut self, names: &HashMap<u32, Naming>, idx: u32, desc: &str) -> Result<()> {
         match names.get(&idx) {
             Some(name) => write!(self.result, "${}", name.identifier())?,
-            None if self.name_unnamed => write!(self.result, "$#{desc}{idx}")?,
+            None if self.config.name_unnamed => write!(self.result, "$#{desc}{idx}")?,
             None => write!(self.result, "{idx}")?,
         }
         Ok(())
@@ -1422,7 +1460,7 @@ impl Printer {
     fn print_local_idx(&mut self, state: &State, func: u32, idx: u32) -> Result<()> {
         match state.core.local_names.index_to_name.get(&(func, idx)) {
             Some(name) => write!(self.result, "${}", name.identifier())?,
-            None if self.name_unnamed => write!(self.result, "$#local{idx}")?,
+            None if self.config.name_unnamed => write!(self.result, "$#local{idx}")?,
             None => write!(self.result, "{}", idx)?,
         }
         Ok(())
@@ -1431,7 +1469,7 @@ impl Printer {
     fn print_field_idx(&mut self, state: &State, ty: u32, idx: u32) -> Result<()> {
         match state.core.field_names.index_to_name.get(&(ty, idx)) {
             Some(name) => write!(self.result, "${}", name.identifier())?,
-            None if self.name_unnamed => write!(self.result, "$#field{idx}")?,
+            None if self.config.name_unnamed => write!(self.result, "$#field{idx}")?,
             None => write!(self.result, "{}", idx)?,
         }
         Ok(())
@@ -1453,9 +1491,9 @@ impl Printer {
         match names.get(&cur_idx) {
             Some(name) => {
                 name.write(&mut self.result);
-                self.result.push(' ');
+                self.result.write_str(" ")?;
             }
-            None if self.name_unnamed => {
+            None if self.config.name_unnamed => {
                 write!(self.result, "$#{desc}{cur_idx} ")?;
             }
             None => {}
@@ -1467,49 +1505,49 @@ impl Printer {
     fn print_elems(&mut self, state: &mut State, data: ElementSectionReader) -> Result<()> {
         for (i, elem) in data.into_iter_with_offsets().enumerate() {
             let (offset, mut elem) = elem?;
-            self.newline(offset);
-            self.start_group("elem ");
+            self.newline(offset)?;
+            self.start_group("elem ")?;
             self.print_name(&state.core.element_names, i as u32)?;
             match &mut elem.kind {
                 ElementKind::Passive => {}
-                ElementKind::Declared => write!(self.result, " declare")?,
+                ElementKind::Declared => self.result.write_str(" declare")?,
                 ElementKind::Active {
                     table_index,
                     offset_expr,
                 } => {
                     let table_index = table_index.unwrap_or(0);
                     if table_index != 0 {
-                        self.result.push_str(" (table ");
+                        self.result.write_str(" (table ")?;
                         self.print_idx(&state.core.table_names, table_index)?;
-                        self.result.push(')');
+                        self.result.write_str(")")?;
                     }
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     self.print_const_expr_sugar(state, offset_expr, "offset")?;
                 }
             }
-            self.result.push(' ');
+            self.result.write_str(" ")?;
 
-            if self.print_skeleton {
-                self.result.push_str("...");
+            if self.config.print_skeleton {
+                self.result.write_str("...")?;
             } else {
                 match elem.items {
                     ElementItems::Functions(reader) => {
-                        self.result.push_str("func");
+                        self.result.write_str("func")?;
                         for idx in reader {
-                            self.result.push(' ');
+                            self.result.write_str(" ")?;
                             self.print_idx(&state.core.func_names, idx?)?
                         }
                     }
                     ElementItems::Expressions(ty, reader) => {
                         self.print_reftype(state, ty)?;
                         for expr in reader {
-                            self.result.push(' ');
+                            self.result.write_str(" ")?;
                             self.print_const_expr_sugar(state, &expr?, "item")?
                         }
                     }
                 }
             }
-            self.end_group();
+            self.end_group()?;
         }
         Ok(())
     }
@@ -1517,10 +1555,10 @@ impl Printer {
     fn print_data(&mut self, state: &mut State, data: DataSectionReader) -> Result<()> {
         for (i, data) in data.into_iter_with_offsets().enumerate() {
             let (offset, data) = data?;
-            self.newline(offset);
-            self.start_group("data ");
+            self.newline(offset)?;
+            self.start_group("data ")?;
             self.print_name(&state.core.data_names, i as u32)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             match &data.kind {
                 DataKind::Passive => {}
                 DataKind::Active {
@@ -1528,20 +1566,20 @@ impl Printer {
                     offset_expr,
                 } => {
                     if *memory_index != 0 {
-                        self.result.push_str("(memory ");
+                        self.result.write_str("(memory ")?;
                         self.print_idx(&state.core.memory_names, *memory_index)?;
-                        self.result.push_str(") ");
+                        self.result.write_str(") ")?;
                     }
                     self.print_const_expr_sugar(state, offset_expr, "offset")?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                 }
             }
-            if self.print_skeleton {
-                self.result.push_str("...");
+            if self.config.print_skeleton {
+                self.result.write_str("...")?;
             } else {
                 self.print_bytes(data.data)?;
             }
-            self.end_group();
+            self.end_group()?;
         }
         Ok(())
     }
@@ -1555,7 +1593,7 @@ impl Printer {
         expr: &ConstExpr,
         explicit: &str,
     ) -> Result<()> {
-        self.start_group("");
+        self.start_group("")?;
         let mut reader = expr.get_operators_reader();
 
         if reader.read().is_ok() && !reader.is_end_then_eof() {
@@ -1564,7 +1602,7 @@ impl Printer {
 
         self.print_const_expr(state, expr)?;
 
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
@@ -1586,22 +1624,23 @@ impl Printer {
         Ok(())
     }
 
-    fn print_primitive_val_type(&mut self, ty: &PrimitiveValType) {
+    fn print_primitive_val_type(&mut self, ty: &PrimitiveValType) -> Result<()> {
         match ty {
-            PrimitiveValType::Bool => self.result.push_str("bool"),
-            PrimitiveValType::S8 => self.result.push_str("s8"),
-            PrimitiveValType::U8 => self.result.push_str("u8"),
-            PrimitiveValType::S16 => self.result.push_str("s16"),
-            PrimitiveValType::U16 => self.result.push_str("u16"),
-            PrimitiveValType::S32 => self.result.push_str("s32"),
-            PrimitiveValType::U32 => self.result.push_str("u32"),
-            PrimitiveValType::S64 => self.result.push_str("s64"),
-            PrimitiveValType::U64 => self.result.push_str("u64"),
-            PrimitiveValType::F32 => self.result.push_str("f32"),
-            PrimitiveValType::F64 => self.result.push_str("f64"),
-            PrimitiveValType::Char => self.result.push_str("char"),
-            PrimitiveValType::String => self.result.push_str("string"),
+            PrimitiveValType::Bool => self.result.write_str("bool")?,
+            PrimitiveValType::S8 => self.result.write_str("s8")?,
+            PrimitiveValType::U8 => self.result.write_str("u8")?,
+            PrimitiveValType::S16 => self.result.write_str("s16")?,
+            PrimitiveValType::U16 => self.result.write_str("u16")?,
+            PrimitiveValType::S32 => self.result.write_str("s32")?,
+            PrimitiveValType::U32 => self.result.write_str("u32")?,
+            PrimitiveValType::S64 => self.result.write_str("s64")?,
+            PrimitiveValType::U64 => self.result.write_str("u64")?,
+            PrimitiveValType::F32 => self.result.write_str("f32")?,
+            PrimitiveValType::F64 => self.result.write_str("f64")?,
+            PrimitiveValType::Char => self.result.write_str("char")?,
+            PrimitiveValType::String => self.result.write_str("string")?,
         }
+        Ok(())
     }
 
     fn print_record_type(
@@ -1609,75 +1648,75 @@ impl Printer {
         state: &State,
         fields: &[(&str, ComponentValType)],
     ) -> Result<()> {
-        self.start_group("record");
+        self.start_group("record")?;
         for (name, ty) in fields.iter() {
-            self.result.push(' ');
-            self.start_group("field ");
+            self.result.write_str(" ")?;
+            self.start_group("field ")?;
             self.print_str(name)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             self.print_component_val_type(state, ty)?;
-            self.end_group()
+            self.end_group()?;
         }
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
     fn print_variant_type(&mut self, state: &State, cases: &[VariantCase]) -> Result<()> {
-        self.start_group("variant");
+        self.start_group("variant")?;
         for case in cases {
-            self.result.push(' ');
-            self.start_group("case ");
+            self.result.write_str(" ")?;
+            self.start_group("case ")?;
             self.print_str(case.name)?;
 
             if let Some(ty) = case.ty {
-                self.result.push(' ');
+                self.result.write_str(" ")?;
                 self.print_component_val_type(state, &ty)?;
             }
 
             if let Some(refines) = case.refines {
-                self.result.push(' ');
-                self.start_group("refines ");
+                self.result.write_str(" ")?;
+                self.start_group("refines ")?;
                 write!(&mut self.result, "{}", refines)?;
-                self.end_group();
+                self.end_group()?;
             }
-            self.end_group()
+            self.end_group()?;
         }
-        self.end_group();
+        self.end_group()?;
 
         Ok(())
     }
 
     fn print_list_type(&mut self, state: &State, element_ty: &ComponentValType) -> Result<()> {
-        self.start_group("list ");
+        self.start_group("list ")?;
         self.print_component_val_type(state, element_ty)?;
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
     fn print_tuple_type(&mut self, state: &State, tys: &[ComponentValType]) -> Result<()> {
-        self.start_group("tuple");
+        self.start_group("tuple")?;
         for ty in tys {
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             self.print_component_val_type(state, ty)?;
         }
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
     fn print_flag_or_enum_type(&mut self, ty: &str, names: &[&str]) -> Result<()> {
-        self.start_group(ty);
+        self.start_group(ty)?;
         for name in names {
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             self.print_str(name)?;
         }
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
     fn print_option_type(&mut self, state: &State, inner: &ComponentValType) -> Result<()> {
-        self.start_group("option ");
+        self.start_group("option ")?;
         self.print_component_val_type(state, inner)?;
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
@@ -1687,28 +1726,28 @@ impl Printer {
         ok: Option<ComponentValType>,
         err: Option<ComponentValType>,
     ) -> Result<()> {
-        self.start_group("result");
+        self.start_group("result")?;
 
         if let Some(ok) = ok {
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             self.print_component_val_type(state, &ok)?;
         }
 
         if let Some(err) = err {
-            self.result.push(' ');
-            self.start_group("error ");
+            self.result.write_str(" ")?;
+            self.start_group("error ")?;
             self.print_component_val_type(state, &err)?;
-            self.end_group();
+            self.end_group()?;
         }
 
-        self.end_group();
+        self.end_group()?;
 
         Ok(())
     }
 
     fn print_defined_type(&mut self, state: &State, ty: &ComponentDefinedType) -> Result<()> {
         match ty {
-            ComponentDefinedType::Primitive(ty) => self.print_primitive_val_type(ty),
+            ComponentDefinedType::Primitive(ty) => self.print_primitive_val_type(ty)?,
             ComponentDefinedType::Record(fields) => self.print_record_type(state, fields)?,
             ComponentDefinedType::Variant(cases) => self.print_variant_type(state, cases)?,
             ComponentDefinedType::List(ty) => self.print_list_type(state, ty)?,
@@ -1718,14 +1757,14 @@ impl Printer {
             ComponentDefinedType::Option(ty) => self.print_option_type(state, ty)?,
             ComponentDefinedType::Result { ok, err } => self.print_result_type(state, *ok, *err)?,
             ComponentDefinedType::Own(idx) => {
-                self.start_group("own ");
+                self.start_group("own ")?;
                 self.print_idx(&state.component.type_names, *idx)?;
-                self.end_group();
+                self.end_group()?;
             }
             ComponentDefinedType::Borrow(idx) => {
-                self.start_group("borrow ");
+                self.start_group("borrow ")?;
                 self.print_idx(&state.component.type_names, *idx)?;
-                self.end_group();
+                self.end_group()?;
             }
         }
 
@@ -1735,12 +1774,8 @@ impl Printer {
     fn print_component_val_type(&mut self, state: &State, ty: &ComponentValType) -> Result<()> {
         match ty {
             ComponentValType::Primitive(ty) => self.print_primitive_val_type(ty),
-            ComponentValType::Type(idx) => {
-                self.print_idx(&state.component.type_names, *idx)?;
-            }
+            ComponentValType::Type(idx) => self.print_idx(&state.component.type_names, *idx),
         }
-
-        Ok(())
     }
 
     fn print_module_type(
@@ -1749,10 +1784,10 @@ impl Printer {
         decls: Vec<ModuleTypeDeclaration>,
     ) -> Result<()> {
         states.push(State::new(Encoding::Module));
-        self.newline_unknown_pos();
-        self.start_group("module");
+        self.newline_unknown_pos()?;
+        self.start_group("module")?;
         for decl in decls {
-            self.newline_unknown_pos();
+            self.newline_unknown_pos()?;
             match decl {
                 ModuleTypeDeclaration::Type(ty) => {
                     self.print_type(states.last_mut().unwrap(), ty)?
@@ -1761,18 +1796,18 @@ impl Printer {
                     self.print_outer_alias(states, kind, count, index)?;
                 }
                 ModuleTypeDeclaration::Export { name, ty } => {
-                    self.start_group("export ");
+                    self.start_group("export ")?;
                     self.print_str(name)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     self.print_import_ty(states.last_mut().unwrap(), &ty, false)?;
-                    self.end_group();
+                    self.end_group()?;
                 }
                 ModuleTypeDeclaration::Import(import) => {
                     self.print_import(states.last_mut().unwrap(), &import, false)?;
                 }
             }
         }
-        self.end_group();
+        self.end_group()?;
         states.pop();
         Ok(())
     }
@@ -1783,10 +1818,10 @@ impl Printer {
         decls: Vec<ComponentTypeDeclaration<'a>>,
     ) -> Result<()> {
         states.push(State::new(Encoding::Component));
-        self.newline_unknown_pos();
-        self.start_group("component");
+        self.newline_unknown_pos()?;
+        self.start_group("component")?;
         for decl in decls {
-            self.newline_unknown_pos();
+            self.newline_unknown_pos()?;
             match decl {
                 ComponentTypeDeclaration::CoreType(ty) => self.print_core_type(states, ty)?,
                 ComponentTypeDeclaration::Type(ty) => self.print_component_type_def(states, ty)?,
@@ -1794,20 +1829,20 @@ impl Printer {
                     self.print_component_alias(states, alias)?;
                 }
                 ComponentTypeDeclaration::Export { name, ty } => {
-                    self.start_group("export ");
+                    self.start_group("export ")?;
                     self.print_component_kind_name(states.last_mut().unwrap(), ty.kind())?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     self.print_str(name.0)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     self.print_component_import_ty(states.last_mut().unwrap(), &ty, false)?;
-                    self.end_group();
+                    self.end_group()?;
                 }
                 ComponentTypeDeclaration::Import(import) => {
                     self.print_component_import(states.last_mut().unwrap(), &import, true)?
                 }
             }
         }
-        self.end_group();
+        self.end_group()?;
         states.pop().unwrap();
         Ok(())
     }
@@ -1818,10 +1853,10 @@ impl Printer {
         decls: Vec<InstanceTypeDeclaration<'a>>,
     ) -> Result<()> {
         states.push(State::new(Encoding::Component));
-        self.newline_unknown_pos();
-        self.start_group("instance");
+        self.newline_unknown_pos()?;
+        self.start_group("instance")?;
         for decl in decls {
-            self.newline_unknown_pos();
+            self.newline_unknown_pos()?;
             match decl {
                 InstanceTypeDeclaration::CoreType(ty) => self.print_core_type(states, ty)?,
                 InstanceTypeDeclaration::Type(ty) => self.print_component_type_def(states, ty)?,
@@ -1829,17 +1864,17 @@ impl Printer {
                     self.print_component_alias(states, alias)?;
                 }
                 InstanceTypeDeclaration::Export { name, ty } => {
-                    self.start_group("export ");
+                    self.start_group("export ")?;
                     self.print_component_kind_name(states.last_mut().unwrap(), ty.kind())?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     self.print_str(name.0)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     self.print_component_import_ty(states.last_mut().unwrap(), &ty, false)?;
-                    self.end_group();
+                    self.end_group()?;
                 }
             }
         }
-        self.end_group();
+        self.end_group()?;
         states.pop().unwrap();
         Ok(())
     }
@@ -1862,23 +1897,23 @@ impl Printer {
     ) -> Result<()> {
         let state = states.last().unwrap();
         let outer = Self::outer_state(states, count)?;
-        self.start_group("alias outer ");
+        self.start_group("alias outer ")?;
         if let Some(name) = outer.name.as_ref() {
             name.write(&mut self.result);
         } else {
-            self.result.push_str(count.to_string().as_str());
+            write!(self.result, "{count}")?;
         }
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         match kind {
             OuterAliasKind::Type => {
                 self.print_idx(&outer.core.type_names, index)?;
-                self.result.push(' ');
-                self.start_group("type ");
+                self.result.write_str(" ")?;
+                self.start_group("type ")?;
                 self.print_name(&state.core.type_names, state.core.types.len() as u32)?;
             }
         }
-        self.end_group(); // kind
-        self.end_group(); // alias
+        self.end_group()?; // kind
+        self.end_group()?; // alias
 
         let state = states.last_mut().unwrap();
         match kind {
@@ -1889,28 +1924,28 @@ impl Printer {
     }
 
     fn print_component_func_type(&mut self, state: &State, ty: &ComponentFuncType) -> Result<()> {
-        self.start_group("func");
+        self.start_group("func")?;
         for (name, ty) in ty.params.iter() {
-            self.result.push(' ');
-            self.start_group("param ");
+            self.result.write_str(" ")?;
+            self.start_group("param ")?;
             self.print_str(name)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             self.print_component_val_type(state, ty)?;
-            self.end_group()
+            self.end_group()?;
         }
 
         for (name, ty) in ty.results.iter() {
-            self.result.push(' ');
-            self.start_group("result ");
+            self.result.write_str(" ")?;
+            self.start_group("result ")?;
             if let Some(name) = name {
                 self.print_str(name)?;
-                self.result.push(' ');
+                self.result.write_str(" ")?;
             }
             self.print_component_val_type(state, ty)?;
-            self.end_group()
+            self.end_group()?;
         }
 
-        self.end_group();
+        self.end_group()?;
 
         Ok(())
     }
@@ -1920,18 +1955,18 @@ impl Printer {
         states: &mut Vec<State>,
         ty: ComponentType<'a>,
     ) -> Result<()> {
-        self.start_group("type ");
+        self.start_group("type ")?;
         {
             let state = states.last_mut().unwrap();
             self.print_name(&state.component.type_names, state.component.types)?;
         }
         match ty {
             ComponentType::Defined(ty) => {
-                self.result.push(' ');
+                self.result.write_str(" ")?;
                 self.print_defined_type(states.last_mut().unwrap(), &ty)?;
             }
             ComponentType::Func(ty) => {
-                self.result.push(' ');
+                self.result.write_str(" ")?;
                 self.print_component_func_type(states.last_mut().unwrap(), &ty)?;
             }
             ComponentType::Component(decls) => {
@@ -1941,20 +1976,20 @@ impl Printer {
                 self.print_instance_type(states, decls.into_vec())?;
             }
             ComponentType::Resource { rep, dtor } => {
-                self.result.push_str(" ");
-                self.start_group("resource");
-                self.result.push_str(" (rep ");
+                self.result.write_str(" ")?;
+                self.start_group("resource")?;
+                self.result.write_str(" (rep ")?;
                 self.print_valtype(states.last().unwrap(), rep)?;
-                self.result.push_str(")");
+                self.result.write_str(")")?;
                 if let Some(dtor) = dtor {
-                    self.result.push_str(" (dtor (func ");
+                    self.result.write_str(" (dtor (func ")?;
                     self.print_idx(&states.last().unwrap().core.func_names, dtor)?;
-                    self.result.push_str("))");
+                    self.result.write_str("))")?;
                 }
-                self.end_group();
+                self.end_group()?;
             }
         }
-        self.end_group();
+        self.end_group()?;
 
         states.last_mut().unwrap().component.types += 1;
 
@@ -1968,7 +2003,7 @@ impl Printer {
     ) -> Result<()> {
         for ty in parser.into_iter_with_offsets() {
             let (offset, ty) = ty?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_component_type_def(states, ty)?;
         }
 
@@ -1982,7 +2017,7 @@ impl Printer {
     ) -> Result<()> {
         for import in parser.into_iter_with_offsets() {
             let (offset, import) = import?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_component_import(state, &import, true)?;
         }
 
@@ -1995,11 +2030,11 @@ impl Printer {
         import: &ComponentImport,
         index: bool,
     ) -> Result<()> {
-        self.start_group("import ");
+        self.start_group("import ")?;
         self.print_str(import.name.0)?;
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         self.print_component_import_ty(state, &import.ty, index)?;
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
@@ -2011,78 +2046,76 @@ impl Printer {
     ) -> Result<()> {
         match ty {
             ComponentTypeRef::Module(idx) => {
-                self.start_group("core module ");
+                self.start_group("core module ")?;
                 if index {
                     self.print_name(&state.core.module_names, state.core.modules as u32)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     state.core.modules += 1;
                 }
                 self.print_component_type_ref(state, *idx)?;
-                self.end_group();
+                self.end_group()?;
             }
             ComponentTypeRef::Func(idx) => {
-                self.start_group("func ");
+                self.start_group("func ")?;
                 if index {
                     self.print_name(&state.component.func_names, state.component.funcs)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     state.component.funcs += 1;
                 }
                 self.print_component_type_ref(state, *idx)?;
-                self.end_group();
+                self.end_group()?;
             }
             ComponentTypeRef::Value(ty) => {
-                self.start_group("value ");
+                self.start_group("value ")?;
                 if index {
                     self.print_name(&state.component.value_names, state.component.values)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     state.component.values += 1;
                 }
                 match ty {
-                    ComponentValType::Primitive(ty) => self.print_primitive_val_type(ty),
-                    ComponentValType::Type(idx) => {
-                        self.print_component_type_ref(state, *idx)?;
-                    }
+                    ComponentValType::Primitive(ty) => self.print_primitive_val_type(ty)?,
+                    ComponentValType::Type(idx) => self.print_component_type_ref(state, *idx)?,
                 }
-                self.end_group();
+                self.end_group()?;
             }
             ComponentTypeRef::Type(bounds) => {
-                self.result.push_str("(type ");
+                self.result.write_str("(type ")?;
                 if index {
                     self.print_name(&state.component.type_names, state.component.types)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     state.component.types += 1;
                 }
                 match bounds {
                     TypeBounds::Eq(idx) => {
-                        self.result.push_str("(eq ");
+                        self.result.write_str("(eq ")?;
                         self.print_idx(&state.component.type_names, *idx)?;
-                        self.result.push(')');
+                        self.result.write_str(")")?;
                     }
                     TypeBounds::SubResource => {
-                        self.result.push_str("(sub resource)");
+                        self.result.write_str("(sub resource)")?;
                     }
                 };
-                self.result.push(')');
+                self.result.write_str(")")?;
             }
             ComponentTypeRef::Instance(idx) => {
-                self.start_group("instance ");
+                self.start_group("instance ")?;
                 if index {
                     self.print_name(&state.component.instance_names, state.component.instances)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     state.component.instances += 1;
                 }
                 self.print_component_type_ref(state, *idx)?;
-                self.end_group();
+                self.end_group()?;
             }
             ComponentTypeRef::Component(idx) => {
-                self.start_group("component ");
+                self.start_group("component ")?;
                 if index {
                     self.print_name(&state.component.component_names, state.component.components)?;
-                    self.result.push(' ');
+                    self.result.write_str(" ")?;
                     state.component.components += 1;
                 }
                 self.print_component_type_ref(state, *idx)?;
-                self.end_group();
+                self.end_group()?;
             }
         }
         Ok(())
@@ -2095,7 +2128,7 @@ impl Printer {
     ) -> Result<()> {
         for export in parser.into_iter_with_offsets() {
             let (offset, export) = export?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_component_export(state, &export, true)?;
         }
         Ok(())
@@ -2107,19 +2140,19 @@ impl Printer {
         export: &ComponentExport,
         named: bool,
     ) -> Result<()> {
-        self.start_group("export ");
+        self.start_group("export ")?;
         if named {
             self.print_component_kind_name(state, export.kind)?;
-            self.result.push(' ');
+            self.result.write_str(" ")?;
         }
         self.print_str(export.name.0)?;
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         self.print_component_external_kind(state, export.kind, export.index)?;
         if let Some(ty) = &export.ty {
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             self.print_component_import_ty(state, &ty, false)?;
         }
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
@@ -2160,22 +2193,22 @@ impl Printer {
     fn start_component_external_kind_group(&mut self, kind: ComponentExternalKind) -> Result<()> {
         match kind {
             ComponentExternalKind::Module => {
-                self.start_group("core module ");
+                self.start_group("core module ")?;
             }
             ComponentExternalKind::Component => {
-                self.start_group("component ");
+                self.start_group("component ")?;
             }
             ComponentExternalKind::Instance => {
-                self.start_group("instance ");
+                self.start_group("instance ")?;
             }
             ComponentExternalKind::Func => {
-                self.start_group("func ");
+                self.start_group("func ")?;
             }
             ComponentExternalKind::Value => {
-                self.start_group("value ");
+                self.start_group("value ")?;
             }
             ComponentExternalKind::Type => {
-                self.start_group("type ");
+                self.start_group("type ")?;
             }
         }
         Ok(())
@@ -2208,7 +2241,7 @@ impl Printer {
                 self.print_idx(&state.component.type_names, index)?;
             }
         }
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
@@ -2218,27 +2251,27 @@ impl Printer {
         options: &[CanonicalOption],
     ) -> Result<()> {
         for option in options {
-            self.result.push(' ');
+            self.result.write_str(" ")?;
             match option {
-                CanonicalOption::UTF8 => self.result.push_str("string-encoding=utf8"),
-                CanonicalOption::UTF16 => self.result.push_str("string-encoding=utf16"),
+                CanonicalOption::UTF8 => self.result.write_str("string-encoding=utf8")?,
+                CanonicalOption::UTF16 => self.result.write_str("string-encoding=utf16")?,
                 CanonicalOption::CompactUTF16 => {
-                    self.result.push_str("string-encoding=latin1+utf16")
+                    self.result.write_str("string-encoding=latin1+utf16")?
                 }
                 CanonicalOption::Memory(idx) => {
-                    self.start_group("memory ");
+                    self.start_group("memory ")?;
                     self.print_idx(&state.core.memory_names, *idx)?;
-                    self.end_group();
+                    self.end_group()?;
                 }
                 CanonicalOption::Realloc(idx) => {
-                    self.start_group("realloc ");
+                    self.start_group("realloc ")?;
                     self.print_idx(&state.core.func_names, *idx)?;
-                    self.end_group();
+                    self.end_group()?;
                 }
                 CanonicalOption::PostReturn(idx) => {
-                    self.start_group("post-return ");
+                    self.start_group("post-return ")?;
                     self.print_idx(&state.core.func_names, *idx)?;
-                    self.end_group();
+                    self.end_group()?;
                 }
             }
         }
@@ -2252,73 +2285,73 @@ impl Printer {
     ) -> Result<()> {
         for func in parser.into_iter_with_offsets() {
             let (offset, func) = func?;
-            self.newline(offset);
+            self.newline(offset)?;
             match func {
                 CanonicalFunction::Lift {
                     core_func_index,
                     type_index,
                     options,
                 } => {
-                    self.start_group("func ");
+                    self.start_group("func ")?;
                     self.print_name(&state.component.func_names, state.component.funcs)?;
-                    self.result.push(' ');
-                    self.start_group("type ");
+                    self.result.write_str(" ")?;
+                    self.start_group("type ")?;
                     self.print_idx(&state.component.type_names, type_index)?;
-                    self.end_group();
-                    self.result.push(' ');
-                    self.start_group("canon lift ");
-                    self.start_group("core func ");
+                    self.end_group()?;
+                    self.result.write_str(" ")?;
+                    self.start_group("canon lift ")?;
+                    self.start_group("core func ")?;
                     self.print_idx(&state.core.func_names, core_func_index)?;
-                    self.end_group();
+                    self.end_group()?;
                     self.print_canonical_options(state, &options)?;
-                    self.end_group();
-                    self.end_group();
+                    self.end_group()?;
+                    self.end_group()?;
                     state.component.funcs += 1;
                 }
                 CanonicalFunction::Lower {
                     func_index,
                     options,
                 } => {
-                    self.start_group("core func ");
+                    self.start_group("core func ")?;
                     self.print_name(&state.core.func_names, state.core.funcs)?;
-                    self.result.push(' ');
-                    self.start_group("canon lower ");
-                    self.start_group("func ");
+                    self.result.write_str(" ")?;
+                    self.start_group("canon lower ")?;
+                    self.start_group("func ")?;
                     self.print_idx(&state.component.func_names, func_index)?;
-                    self.end_group();
+                    self.end_group()?;
                     self.print_canonical_options(state, &options)?;
-                    self.end_group();
-                    self.end_group();
+                    self.end_group()?;
+                    self.end_group()?;
                     state.core.funcs += 1;
                 }
                 CanonicalFunction::ResourceNew { resource } => {
-                    self.start_group("core func ");
+                    self.start_group("core func ")?;
                     self.print_name(&state.core.func_names, state.core.funcs)?;
-                    self.result.push(' ');
-                    self.start_group("canon resource.new ");
+                    self.result.write_str(" ")?;
+                    self.start_group("canon resource.new ")?;
                     self.print_idx(&state.component.type_names, resource)?;
-                    self.end_group();
-                    self.end_group();
+                    self.end_group()?;
+                    self.end_group()?;
                     state.core.funcs += 1;
                 }
                 CanonicalFunction::ResourceDrop { resource } => {
-                    self.start_group("core func ");
+                    self.start_group("core func ")?;
                     self.print_name(&state.core.func_names, state.core.funcs)?;
-                    self.result.push(' ');
-                    self.start_group("canon resource.drop ");
+                    self.result.write_str(" ")?;
+                    self.start_group("canon resource.drop ")?;
                     self.print_idx(&state.component.type_names, resource)?;
-                    self.end_group();
-                    self.end_group();
+                    self.end_group()?;
+                    self.end_group()?;
                     state.core.funcs += 1;
                 }
                 CanonicalFunction::ResourceRep { resource } => {
-                    self.start_group("core func ");
+                    self.start_group("core func ")?;
                     self.print_name(&state.core.func_names, state.core.funcs)?;
-                    self.result.push(' ');
-                    self.start_group("canon resource.rep ");
+                    self.result.write_str(" ")?;
+                    self.start_group("canon resource.rep ")?;
                     self.print_idx(&state.component.type_names, resource)?;
-                    self.end_group();
-                    self.end_group();
+                    self.end_group()?;
+                    self.end_group()?;
                     state.core.funcs += 1;
                 }
             }
@@ -2330,30 +2363,30 @@ impl Printer {
     fn print_instances(&mut self, state: &mut State, parser: InstanceSectionReader) -> Result<()> {
         for instance in parser.into_iter_with_offsets() {
             let (offset, instance) = instance?;
-            self.newline(offset);
-            self.start_group("core instance ");
+            self.newline(offset)?;
+            self.start_group("core instance ")?;
             self.print_name(&state.core.instance_names, state.core.instances)?;
             match instance {
                 Instance::Instantiate { module_index, args } => {
-                    self.result.push(' ');
-                    self.start_group("instantiate ");
+                    self.result.write_str(" ")?;
+                    self.start_group("instantiate ")?;
                     self.print_idx(&state.core.module_names, module_index)?;
                     for arg in args.iter() {
-                        self.newline(offset);
+                        self.newline(offset)?;
                         self.print_instantiation_arg(state, arg)?;
                     }
-                    self.end_group();
+                    self.end_group()?;
                     state.core.instances += 1;
                 }
                 Instance::FromExports(exports) => {
                     for export in exports.iter() {
-                        self.newline(offset);
+                        self.newline(offset)?;
                         self.print_export(state, export)?;
                     }
                     state.core.instances += 1;
                 }
             }
-            self.end_group();
+            self.end_group()?;
         }
         Ok(())
     }
@@ -2365,8 +2398,8 @@ impl Printer {
     ) -> Result<()> {
         for instance in parser.into_iter_with_offsets() {
             let (offset, instance) = instance?;
-            self.newline(offset);
-            self.start_group("instance ");
+            self.newline(offset)?;
+            self.start_group("instance ")?;
             self.print_name(&state.component.instance_names, state.component.instances)?;
             state.component.instances += 1;
             match instance {
@@ -2374,39 +2407,39 @@ impl Printer {
                     component_index,
                     args,
                 } => {
-                    self.result.push(' ');
-                    self.start_group("instantiate ");
+                    self.result.write_str(" ")?;
+                    self.start_group("instantiate ")?;
                     self.print_idx(&state.component.component_names, component_index)?;
                     for arg in args.iter() {
-                        self.newline(offset);
+                        self.newline(offset)?;
                         self.print_component_instantiation_arg(state, arg)?;
                     }
-                    self.end_group();
+                    self.end_group()?;
                 }
                 ComponentInstance::FromExports(exports) => {
                     for export in exports.iter() {
-                        self.newline(offset);
+                        self.newline(offset)?;
                         self.print_component_export(state, export, false)?;
                     }
                 }
             }
-            self.end_group();
+            self.end_group()?;
         }
         Ok(())
     }
 
     fn print_instantiation_arg(&mut self, state: &State, arg: &InstantiationArg) -> Result<()> {
-        self.start_group("with ");
+        self.start_group("with ")?;
         self.print_str(arg.name)?;
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         match arg.kind {
             InstantiationArgKind::Instance => {
-                self.start_group("instance ");
+                self.start_group("instance ")?;
                 self.print_idx(&state.core.instance_names, arg.index)?;
-                self.end_group();
+                self.end_group()?;
             }
         }
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
@@ -2415,11 +2448,11 @@ impl Printer {
         state: &State,
         arg: &ComponentInstantiationArg,
     ) -> Result<()> {
-        self.start_group("with ");
+        self.start_group("with ")?;
         self.print_str(arg.name)?;
-        self.result.push(' ');
+        self.result.write_str(" ")?;
         self.print_component_external_kind(state, arg.kind, arg.index)?;
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
@@ -2429,28 +2462,28 @@ impl Printer {
         pos: usize,
         start: ComponentStartFunction,
     ) -> Result<()> {
-        self.newline(pos);
-        self.start_group("start ");
+        self.newline(pos)?;
+        self.start_group("start ")?;
         self.print_idx(&state.component.func_names, start.func_index)?;
 
         for arg in start.arguments.iter() {
-            self.result.push(' ');
-            self.start_group("value ");
+            self.result.write_str(" ")?;
+            self.start_group("value ")?;
             self.print_idx(&state.component.value_names, *arg)?;
-            self.end_group();
+            self.end_group()?;
         }
 
         for _ in 0..start.results {
-            self.result.push(' ');
-            self.start_group("result ");
-            self.start_group("value ");
+            self.result.write_str(" ")?;
+            self.start_group("result ")?;
+            self.start_group("value ")?;
             self.print_name(&state.component.value_names, state.component.values)?;
-            self.end_group();
-            self.end_group();
+            self.end_group()?;
+            self.end_group()?;
             state.component.values += 1;
         }
 
-        self.end_group(); // start
+        self.end_group()?; // start
 
         Ok(())
     }
@@ -2462,7 +2495,7 @@ impl Printer {
     ) -> Result<()> {
         for alias in parser.into_iter_with_offsets() {
             let (offset, alias) = alias?;
-            self.newline(offset);
+            self.newline(offset)?;
             self.print_component_alias(states, alias)?;
         }
         Ok(())
@@ -2480,16 +2513,16 @@ impl Printer {
                 name,
             } => {
                 let state = states.last_mut().unwrap();
-                self.start_group("alias export ");
+                self.start_group("alias export ")?;
                 self.print_idx(&state.component.instance_names, instance_index)?;
-                self.result.push(' ');
+                self.result.write_str(" ")?;
                 self.print_str(name)?;
-                self.result.push(' ');
+                self.result.write_str(" ")?;
                 self.start_component_external_kind_group(kind)?;
                 self.print_component_kind_name(state, kind)?;
-                self.end_group();
+                self.end_group()?;
 
-                self.end_group(); // alias export
+                self.end_group()?; // alias export
             }
             ComponentAlias::CoreInstanceExport {
                 instance_index,
@@ -2497,87 +2530,87 @@ impl Printer {
                 name,
             } => {
                 let state = states.last_mut().unwrap();
-                self.start_group("alias core export ");
+                self.start_group("alias core export ")?;
                 self.print_idx(&state.core.instance_names, instance_index)?;
-                self.result.push(' ');
+                self.result.write_str(" ")?;
                 self.print_str(name)?;
-                self.result.push(' ');
+                self.result.write_str(" ")?;
                 match kind {
                     ExternalKind::Func => {
-                        self.start_group("core func ");
+                        self.start_group("core func ")?;
                         self.print_name(&state.core.func_names, state.core.funcs)?;
-                        self.end_group();
+                        self.end_group()?;
                         state.core.funcs += 1;
                     }
                     ExternalKind::Table => {
-                        self.start_group("core table ");
+                        self.start_group("core table ")?;
                         self.print_name(&state.core.table_names, state.core.tables)?;
-                        self.end_group();
+                        self.end_group()?;
                         state.core.tables += 1;
                     }
                     ExternalKind::Memory => {
-                        self.start_group("core memory ");
+                        self.start_group("core memory ")?;
                         self.print_name(&state.core.memory_names, state.core.memories)?;
-                        self.end_group();
+                        self.end_group()?;
                         state.core.memories += 1;
                     }
                     ExternalKind::Global => {
-                        self.start_group("core global ");
+                        self.start_group("core global ")?;
                         self.print_name(&state.core.global_names, state.core.globals)?;
-                        self.end_group();
+                        self.end_group()?;
                         state.core.globals += 1;
                     }
                     ExternalKind::Tag => {
-                        self.start_group("core tag ");
+                        self.start_group("core tag ")?;
                         self.print_name(&state.core.tag_names, state.core.tags)?;
-                        self.end_group();
+                        self.end_group()?;
                         state.core.tags += 1;
                     }
                 }
-                self.end_group(); // alias export
+                self.end_group()?; // alias export
             }
 
             ComponentAlias::Outer { kind, count, index } => {
                 let state = states.last().unwrap();
                 let outer = Self::outer_state(states, count)?;
-                self.start_group("alias outer ");
+                self.start_group("alias outer ")?;
                 if let Some(name) = outer.name.as_ref() {
                     name.write(&mut self.result);
                 } else {
-                    self.result.push_str(count.to_string().as_str());
+                    write!(self.result, "{count}")?;
                 }
-                self.result.push(' ');
+                self.result.write_str(" ")?;
                 match kind {
                     ComponentOuterAliasKind::CoreModule => {
                         self.print_idx(&outer.core.module_names, index)?;
-                        self.result.push(' ');
-                        self.start_group("core module ");
+                        self.result.write_str(" ")?;
+                        self.start_group("core module ")?;
                         self.print_name(&state.core.module_names, state.core.modules)?;
                     }
                     ComponentOuterAliasKind::CoreType => {
                         self.print_idx(&outer.core.type_names, index)?;
-                        self.result.push(' ');
-                        self.start_group("core type ");
+                        self.result.write_str(" ")?;
+                        self.start_group("core type ")?;
                         self.print_name(&state.core.type_names, state.core.types.len() as u32)?;
                     }
                     ComponentOuterAliasKind::Type => {
                         self.print_idx(&outer.component.type_names, index)?;
-                        self.result.push(' ');
-                        self.start_group("type ");
+                        self.result.write_str(" ")?;
+                        self.start_group("type ")?;
                         self.print_name(&state.component.type_names, state.component.types)?;
                     }
                     ComponentOuterAliasKind::Component => {
                         self.print_idx(&outer.component.component_names, index)?;
-                        self.result.push(' ');
-                        self.start_group("component ");
+                        self.result.write_str(" ")?;
+                        self.start_group("component ")?;
                         self.print_name(
                             &state.component.component_names,
                             state.component.components,
                         )?;
                     }
                 }
-                self.end_group(); // kind
-                self.end_group(); // alias
+                self.end_group()?; // kind
+                self.end_group()?; // alias
 
                 let state = states.last_mut().unwrap();
                 match kind {
@@ -2593,45 +2626,37 @@ impl Printer {
 
     fn print_str(&mut self, name: &str) -> Result<()> {
         let mut bytes = [0; 4];
-        self.result.push('"');
+        self.result.write_str("\"")?;
         for c in name.chars() {
             let v = c as u32;
             if (0x20..0x7f).contains(&v) && c != '"' && c != '\\' && v < 0xff {
-                self.result.push(c);
+                write!(self.result, "{c}")?;
             } else {
                 for byte in c.encode_utf8(&mut bytes).as_bytes() {
-                    self.hex_byte(*byte);
+                    self.hex_byte(*byte)?;
                 }
             }
         }
-        self.result.push('"');
+        self.result.write_str("\"")?;
         Ok(())
     }
 
     fn print_bytes(&mut self, bytes: &[u8]) -> Result<()> {
-        self.result.push('"');
+        self.result.write_str("\"")?;
         for byte in bytes {
             if *byte >= 0x20 && *byte < 0x7f && *byte != b'"' && *byte != b'\\' {
-                self.result.push(*byte as char);
+                write!(self.result, "{}", *byte as char)?;
             } else {
-                self.hex_byte(*byte);
+                self.hex_byte(*byte)?;
             }
         }
-        self.result.push('"');
+        self.result.write_str("\"")?;
         Ok(())
     }
 
-    fn hex_byte(&mut self, byte: u8) {
-        fn to_hex(b: u8) -> char {
-            if b < 10 {
-                (b'0' + b) as char
-            } else {
-                (b'a' + b - 10) as char
-            }
-        }
-        self.result.push('\\');
-        self.result.push(to_hex((byte >> 4) & 0xf));
-        self.result.push(to_hex(byte & 0xf));
+    fn hex_byte(&mut self, byte: u8) -> Result<()> {
+        write!(self.result, "\\{byte:02x}")?;
+        Ok(())
     }
 
     fn print_custom_section(
@@ -2644,11 +2669,11 @@ impl Printer {
             // `dylink.0` below to use in tests. Note that this syntax is not
             // official at this time.
             KnownCustom::Producers(s) => {
-                self.newline(section.range().start);
+                self.newline(section.range().start)?;
                 self.print_producers_section(s)
             }
             KnownCustom::Dylink0(s) => {
-                self.newline(section.range().start);
+                self.newline(section.range().start)?;
                 self.print_dylink0_section(s)
             }
 
@@ -2675,41 +2700,39 @@ impl Printer {
         state: &State,
         section: CustomSectionReader<'_>,
     ) -> Result<()> {
-        self.newline(section.range().start);
-        self.start_group("@custom ");
+        self.newline(section.range().start)?;
+        self.start_group("@custom ")?;
         self.print_str(section.name())?;
         if let Some(place) = state.custom_section_place {
-            self.result.push_str(" (");
-            self.result.push_str(place);
-            self.result.push_str(")");
+            write!(self.result, " ({place})")?;
         }
-        self.result.push_str(" ");
+        self.result.write_str(" ")?;
         self.print_bytes(section.data())?;
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
     fn print_producers_section(&mut self, section: ProducersSectionReader<'_>) -> Result<()> {
-        self.start_group("@producers");
+        self.start_group("@producers")?;
         for field in section {
             let field = field?;
             for value in field.values.into_iter_with_offsets() {
                 let (offset, value) = value?;
-                self.newline(offset);
-                self.start_group(field.name);
-                self.result.push_str(" ");
+                self.newline(offset)?;
+                self.start_group(field.name)?;
+                self.result.write_str(" ")?;
                 self.print_str(value.name)?;
-                self.result.push_str(" ");
+                self.result.write_str(" ")?;
                 self.print_str(value.version)?;
-                self.end_group();
+                self.end_group()?;
             }
         }
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
     fn print_dylink0_section(&mut self, mut section: Dylink0SectionReader<'_>) -> Result<()> {
-        self.start_group("@dylink.0");
+        self.start_group("@dylink.0")?;
         loop {
             let start = section.original_position();
             let next = match section.next() {
@@ -2719,8 +2742,8 @@ impl Printer {
             };
             match next {
                 Dylink0Subsection::MemInfo(info) => {
-                    self.newline(start);
-                    self.start_group("mem-info");
+                    self.newline(start)?;
+                    self.start_group("mem-info")?;
                     if info.memory_size > 0 || info.memory_alignment > 0 {
                         write!(
                             self.result,
@@ -2735,35 +2758,35 @@ impl Printer {
                             info.table_size, info.table_alignment
                         )?;
                     }
-                    self.end_group();
+                    self.end_group()?;
                 }
                 Dylink0Subsection::Needed(needed) => {
-                    self.newline(start);
-                    self.start_group("needed");
+                    self.newline(start)?;
+                    self.start_group("needed")?;
                     for s in needed {
-                        self.result.push_str(" ");
+                        self.result.write_str(" ")?;
                         self.print_str(s)?;
                     }
-                    self.end_group();
+                    self.end_group()?;
                 }
                 Dylink0Subsection::ExportInfo(info) => {
                     for info in info {
-                        self.newline(start);
-                        self.start_group("export-info ");
+                        self.newline(start)?;
+                        self.start_group("export-info ")?;
                         self.print_str(info.name)?;
                         self.print_dylink0_flags(info.flags)?;
-                        self.end_group();
+                        self.end_group()?;
                     }
                 }
                 Dylink0Subsection::ImportInfo(info) => {
                     for info in info {
-                        self.newline(start);
-                        self.start_group("import-info ");
+                        self.newline(start)?;
+                        self.start_group("import-info ")?;
                         self.print_str(info.module)?;
-                        self.result.push_str(" ");
+                        self.result.write_str(" ")?;
                         self.print_str(info.field)?;
                         self.print_dylink0_flags(info.flags)?;
-                        self.end_group();
+                        self.end_group()?;
                     }
                 }
                 Dylink0Subsection::Unknown { ty, .. } => {
@@ -2771,7 +2794,7 @@ impl Printer {
                 }
             }
         }
-        self.end_group();
+        self.end_group()?;
         Ok(())
     }
 
@@ -2780,7 +2803,7 @@ impl Printer {
             ($($name:ident = $text:tt)*) => ({$(
                 if flags.contains(SymbolFlags::$name) {
                     flags.remove(SymbolFlags::$name);
-                    self.result.push_str(concat!(" ", $text));
+                    write!(self.result, concat!(" ", $text))?;
                 }
             )*})
         }
@@ -2840,28 +2863,33 @@ impl NamedLocalPrinter {
         }
     }
 
-    fn start_local(&mut self, func: u32, local: u32, dst: &mut Printer, state: &State) {
+    fn start_local(
+        &mut self,
+        func: u32,
+        local: u32,
+        dst: &mut Printer,
+        state: &State,
+    ) -> Result<()> {
         let name = state.core.local_names.index_to_name.get(&(func, local));
 
         // Named locals must be in their own group, so if we have a name we need
         // to terminate the previous group.
         if name.is_some() && self.in_group {
-            dst.result.push(')');
+            dst.end_group()?;
             self.in_group = false;
         }
 
         if self.first {
             self.first = false;
         } else {
-            dst.result.push(' ');
+            dst.result.write_str(" ")?;
         }
 
         // Next we either need a separator if we're already in a group or we
         // need to open a group for our new local.
         if !self.in_group {
-            dst.result.push('(');
-            dst.result.push_str(self.group_name);
-            dst.result.push(' ');
+            dst.start_group(self.group_name)?;
+            dst.result.write_str(" ")?;
             self.in_group = true;
         }
 
@@ -2869,30 +2897,33 @@ impl NamedLocalPrinter {
         match name {
             Some(name) => {
                 name.write(&mut dst.result);
-                dst.result.push(' ');
+                dst.result.write_str(" ")?;
                 self.end_group_after_local = true;
             }
-            None if dst.name_unnamed => {
-                dst.result.push_str(&format!("$#local{local} "));
+            None if dst.config.name_unnamed => {
+                write!(dst.result, "$#local{local} ")?;
                 self.end_group_after_local = true;
             }
             None => {
                 self.end_group_after_local = false;
             }
         }
+        Ok(())
     }
 
-    fn end_local(&mut self, dst: &mut String) {
+    fn end_local(&mut self, dst: &mut Printer) -> Result<()> {
         if self.end_group_after_local {
-            dst.push(')');
+            dst.end_group()?;
             self.end_group_after_local = false;
             self.in_group = false;
         }
+        Ok(())
     }
-    fn finish(self, dst: &mut String) {
+    fn finish(self, dst: &mut Printer) -> Result<()> {
         if self.in_group {
-            dst.push(')');
+            dst.end_group()?;
         }
+        Ok(())
     }
 }
 
@@ -2911,7 +2942,7 @@ macro_rules! print_float {
             let f = $float::from_bits(bits);
             if bits >> (int_width - 1) != 0 {
                 bits ^= 1 << (int_width - 1);
-                self.result.push('-');
+                self.result.write_str("-")?;
             }
             if f.is_infinite() {
                 write!(self.result, "inf (;={};)", f)?;
@@ -2945,11 +2976,11 @@ macro_rules! print_float {
             let mut exponent = (((bits << 1) as $sint) >> (mantissa_width + 1)).wrapping_sub(bias);
             exponent = (exponent << (int_width - exp_width)) >> (int_width - exp_width);
             let mut fraction = bits & ((1 << mantissa_width) - 1);
-            self.result.push_str("0x");
+            self.result.write_str("0x")?;
             if bits == 0 {
-                self.result.push_str("0p+0");
+                self.result.write_str("0p+0")?;
             } else {
-                self.result.push('1');
+                self.result.write_str("1")?;
                 if fraction > 0 {
                     fraction <<= (int_width - mantissa_width);
 
@@ -2966,7 +2997,7 @@ macro_rules! print_float {
                         exponent -= leading as $sint;
                     }
 
-                    self.result.push('.');
+                    self.result.write_str(".")?;
                     while fraction > 0 {
                         write!(self.result, "{:x}", fraction >> (int_width - 4))?;
                         fraction <<= 4;
@@ -2980,7 +3011,7 @@ macro_rules! print_float {
     };
 }
 
-impl Printer {
+impl Printer<'_> {
     print_float!(print_f32 f32 u32 i32 8);
     print_float!(print_f64 f64 u64 i64 11);
 }
@@ -3017,11 +3048,7 @@ impl Naming {
             || name.starts_with('#')
             || used.map(|set| !set.insert(name)).unwrap_or(false)
         {
-            let mut id = String::new();
-            id.push('#');
-            id.push_str(group);
-            write!(id, "{}", index).unwrap();
-            id.push('<');
+            let mut id = format!("#{group}{index}<");
             id.extend(name.chars().map(|c| if is_idchar(c) { c } else { '_' }));
             id.push('>');
             identifier = Some(id);
diff --git a/crates/wasmprinter/src/operator.rs b/crates/wasmprinter/src/operator.rs
index 1eb0e486..6150f74c 100644
--- a/crates/wasmprinter/src/operator.rs
+++ b/crates/wasmprinter/src/operator.rs
@@ -3,8 +3,8 @@ use anyhow::{anyhow, bail, Result};
 use std::fmt::Write;
 use wasmparser::{BlockType, BrTable, Catch, MemArg, Ordering, RefType, TryTable, VisitOperator};
 
-pub struct PrintOperator<'a, 'b> {
-    pub(super) printer: &'a mut Printer,
+pub struct PrintOperator<'cfg, 'a, 'b> {
+    pub(super) printer: &'a mut Printer<'cfg>,
     pub(super) op_offset: usize,
     nesting_start: u32,
     state: &'b mut State,
@@ -18,12 +18,12 @@ pub enum OperatorSeparator {
     None,
 }
 
-impl<'a, 'b> PrintOperator<'a, 'b> {
+impl<'cfg, 'a, 'b> PrintOperator<'cfg, 'a, 'b> {
     pub(super) fn new(
-        printer: &'a mut Printer,
+        printer: &'a mut Printer<'cfg>,
         state: &'b mut State,
         sep: OperatorSeparator,
-    ) -> PrintOperator<'a, 'b> {
+    ) -> Self {
         PrintOperator {
             nesting_start: printer.nesting,
             op_offset: 0,
@@ -35,44 +35,46 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
         }
     }
 
-    fn push_str(&mut self, s: &str) {
+    fn push_str(&mut self, s: &str) -> Result<()> {
         self.printer.result.push_str(s);
+        Ok(())
     }
 
     fn result(&mut self) -> &mut String {
         &mut self.printer.result
     }
 
-    fn separator(&mut self) {
+    fn separator(&mut self) -> Result<()> {
         match self.sep {
-            OperatorSeparator::Newline => {
-                self.printer.newline(self.op_offset);
-            }
-            OperatorSeparator::None => {}
+            OperatorSeparator::Newline => self.printer.newline(self.op_offset),
+            OperatorSeparator::None => Ok(()),
         }
     }
 
     /// Called just before an instruction that introduces a block such as
     /// `block`, `if`, `loop`, etc.
-    fn block_start(&mut self) {
-        self.separator();
+    fn block_start(&mut self) -> Result<()> {
+        self.separator()?;
         self.printer.nesting += 1;
         self.label_indices.push(self.label);
+        Ok(())
     }
 
     /// Used for `else` and `delegate`
-    fn block_mid(&mut self) {
+    fn block_mid(&mut self) -> Result<()> {
         self.printer.nesting -= 1;
-        self.separator();
+        self.separator()?;
         self.printer.nesting += 1;
+        Ok(())
     }
 
     /// Used for `end` to terminate the prior block.
-    fn block_end(&mut self) {
+    fn block_end(&mut self) -> Result<()> {
         if self.printer.nesting > self.nesting_start {
             self.printer.nesting -= 1;
-            self.separator();
+            self.separator()?;
         }
+        Ok(())
     }
 
     fn blockty(&mut self, ty: BlockType) -> Result<()> {
@@ -88,7 +90,7 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
                 name.write(&mut self.printer.result);
                 true
             }
-            None if self.printer.name_unnamed => {
+            None if self.printer.config.name_unnamed => {
                 // Subtract one from the depth here because the label was
                 // already pushed onto our stack when the instruction was
                 // entered so its own label is one less.
@@ -101,12 +103,13 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
         match ty {
             BlockType::Empty => {}
             BlockType::Type(t) => {
-                self.push_str(" (result ");
+                self.push_str(" ")?;
+                self.printer.start_group("result ")?;
                 self.printer.print_valtype(self.state, t)?;
-                self.push_str(")");
+                self.printer.end_group()?;
             }
             BlockType::FuncType(idx) => {
-                self.push_str(" ");
+                self.push_str(" ")?;
                 self.printer
                     .print_core_functype_idx(self.state, idx, None)?;
             }
@@ -117,7 +120,7 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     fn maybe_blockty_label_comment(&mut self, has_name: bool) -> Result<()> {
         if !has_name {
             let depth = self.cur_depth();
-            self.push_str(" ");
+            self.push_str(" ")?;
             write!(self.result(), ";; label = @{}", depth)?;
         }
 
@@ -130,13 +133,13 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn tag_index(&mut self, index: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.tag_names, index)?;
         Ok(())
     }
 
     fn relative_depth(&mut self, depth: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         match self.cur_depth().checked_sub(depth) {
             // If this relative depth is in-range relative to the current depth,
             // then try to print a name for this label. Label names are tracked
@@ -181,7 +184,7 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
                     // Note that synthesized label names don't handle the
                     // function itself, so i==0, branching to a function label,
                     // is not supported and otherwise labels are offset by 1.
-                    None if !name_conflict && self.printer.name_unnamed && i > 0 => {
+                    None if !name_conflict && self.printer.config.name_unnamed && i > 0 => {
                         write!(self.result(), "$#label{}", i - 1)?
                     }
 
@@ -217,23 +220,23 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn function_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.func_names, idx)
     }
 
     fn local_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer
             .print_local_idx(self.state, self.state.core.funcs, idx)
     }
 
     fn global_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.global_names, idx)
     }
 
     fn table_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.table_names, idx)
     }
 
@@ -242,27 +245,27 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn memory_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.memory_names, idx)
     }
 
     fn type_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_core_type_ref(self.state, idx)
     }
 
     fn array_type_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.type_names, idx)
     }
 
     fn array_type_index_dst(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.type_names, idx)
     }
 
     fn array_type_index_src(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.type_names, idx)
     }
 
@@ -272,37 +275,37 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn struct_type_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.type_names, idx)
     }
 
     fn from_ref_type(&mut self, ref_ty: RefType) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_reftype(self.state, ref_ty)
     }
 
     fn to_ref_type(&mut self, ref_ty: RefType) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_reftype(self.state, ref_ty)
     }
 
     fn data_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.data_names, idx)
     }
 
     fn array_data_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.data_names, idx)
     }
 
     fn elem_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.element_names, idx)
     }
 
     fn array_elem_index(&mut self, idx: u32) -> Result<()> {
-        self.push_str(" ");
+        self.push_str(" ")?;
         self.printer.print_idx(&self.state.core.element_names, idx)
     }
 
@@ -357,29 +360,29 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
         let try_table_label = self.label_indices.pop().unwrap();
 
         for catch in table.catches {
-            self.result().push(' ');
+            self.result().write_str(" ")?;
             match catch {
                 Catch::One { tag, label } => {
-                    self.printer.start_group("catch");
+                    self.printer.start_group("catch")?;
                     self.tag_index(tag)?;
                     self.relative_depth(label)?;
-                    self.printer.end_group();
+                    self.printer.end_group()?;
                 }
                 Catch::OneRef { tag, label } => {
-                    self.printer.start_group("catch_ref");
+                    self.printer.start_group("catch_ref")?;
                     self.tag_index(tag)?;
                     self.relative_depth(label)?;
-                    self.printer.end_group();
+                    self.printer.end_group()?;
                 }
                 Catch::All { label } => {
-                    self.printer.start_group("catch_all");
+                    self.printer.start_group("catch_all")?;
                     self.relative_depth(label)?;
-                    self.printer.end_group();
+                    self.printer.end_group()?;
                 }
                 Catch::AllRef { label } => {
-                    self.printer.start_group("catch_all_ref");
+                    self.printer.start_group("catch_all_ref")?;
                     self.relative_depth(label)?;
-                    self.printer.end_group();
+                    self.printer.end_group()?;
                 }
             }
         }
@@ -399,7 +402,7 @@ macro_rules! define_visit {
     ($(@$proposal:ident $op:ident $({ $($arg:ident: $argty:ty),* })? => $visit:ident )*) => ($(
         fn $visit(&mut self $( , $($arg: $argty),* )?) -> Self::Output {
             define_visit!(before_op self $op);
-            self.push_str(define_visit!(name $op));
+            self.push_str(define_visit!(name $op))?;
             $(
                 define_visit!(payload self $op $($arg)*);
             )?
@@ -413,17 +416,17 @@ macro_rules! define_visit {
     // depth as well as the stack of labels.
     //
     // The catch-all for "before an op" is "print an newline"
-    (before_op $self:ident Loop) => ($self.block_start(););
-    (before_op $self:ident Block) => ($self.block_start(););
-    (before_op $self:ident If) => ($self.block_start(););
-    (before_op $self:ident Try) => ($self.block_start(););
-    (before_op $self:ident TryTable) => ($self.block_start(););
-    (before_op $self:ident Catch) => ($self.block_mid(););
-    (before_op $self:ident CatchAll) => ($self.block_mid(););
-    (before_op $self:ident Delegate) => ($self.block_end(););
-    (before_op $self:ident Else) => ($self.block_mid(););
-    (before_op $self:ident End) => ($self.block_end(););
-    (before_op $self:ident $op:ident) => ($self.separator(););
+    (before_op $self:ident Loop) => ($self.block_start()?;);
+    (before_op $self:ident Block) => ($self.block_start()?;);
+    (before_op $self:ident If) => ($self.block_start()?;);
+    (before_op $self:ident Try) => ($self.block_start()?;);
+    (before_op $self:ident TryTable) => ($self.block_start()?;);
+    (before_op $self:ident Catch) => ($self.block_mid()?;);
+    (before_op $self:ident CatchAll) => ($self.block_mid()?;);
+    (before_op $self:ident Delegate) => ($self.block_end()?;);
+    (before_op $self:ident Else) => ($self.block_mid()?;);
+    (before_op $self:ident End) => ($self.block_end()?;);
+    (before_op $self:ident $op:ident) => ($self.separator()?;);
 
     // After some opcodes the label stack is popped.
     // (after_op $self:ident Delegate) => ($self.label_indices.pop(););
@@ -448,20 +451,21 @@ macro_rules! define_visit {
         $self.type_index($ty)?;
     );
     (payload $self:ident CallRef $ty:ident) => (
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         $self.printer.print_idx(&$self.state.core.type_names, $ty)?;
     );
     (payload $self:ident ReturnCallRef $ty:ident) => (
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         $self.printer.print_idx(&$self.state.core.type_names, $ty)?;
     );
     (payload $self:ident TypedSelect $ty:ident) => (
-        $self.push_str(" (result ");
+        $self.push_str(" ")?;
+        $self.printer.start_group("result ")?;
         $self.printer.print_valtype($self.state, $ty)?;
-        $self.push_str(")")
+        $self.printer.end_group()?;
     );
     (payload $self:ident RefNull $hty:ident) => (
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         $self.printer.print_heaptype($self.state, $hty)?;
     );
     (payload $self:ident TableInit $segment:ident $table:ident) => (
@@ -511,15 +515,15 @@ macro_rules! define_visit {
     (payload $self:ident I32Const $val:ident) => (write!($self.result(), " {}", $val)?);
     (payload $self:ident I64Const $val:ident) => (write!($self.result(), " {}", $val)?);
     (payload $self:ident F32Const $val:ident) => (
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         $self.printer.print_f32($val.bits())?;
     );
     (payload $self:ident F64Const $val:ident) => (
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         $self.printer.print_f64($val.bits())?;
     );
     (payload $self:ident V128Const $val:ident) => (
-        $self.push_str(" i32x4");
+        $self.push_str(" i32x4")?;
         for chunk in $val.bytes().chunks(4) {
             write!(
                 $self.result(),
@@ -532,47 +536,47 @@ macro_rules! define_visit {
         }
     );
     (payload $self:ident RefTestNonNull $hty:ident) => (
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         let rty = RefType::new(false, $hty)
             .ok_or_else(|| anyhow!("implementation limit: type index too large"))?;
         $self.printer.print_reftype($self.state, rty)?;
     );
     (payload $self:ident RefTestNullable $hty:ident) => (
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         let rty = RefType::new(true, $hty)
             .ok_or_else(|| anyhow!("implementation limit: type index too large"))?;
         $self.printer.print_reftype($self.state, rty)?;
     );
     (payload $self:ident RefCastNonNull $hty:ident) => (
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         let rty = RefType::new(false, $hty)
             .ok_or_else(|| anyhow!("implementation limit: type index too large"))?;
         $self.printer.print_reftype($self.state, rty)?;
     );
     (payload $self:ident RefCastNullable $hty:ident) => (
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         let rty = RefType::new(true, $hty)
             .ok_or_else(|| anyhow!("implementation limit: type index too large"))?;
         $self.printer.print_reftype($self.state, rty)?;
     );
     (payload $self:ident StructGet $ty:ident $field:ident) => (
         $self.struct_type_index($ty)?;
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         $self.printer.print_field_idx($self.state, $ty, $field)?;
     );
     (payload $self:ident StructGetS $ty:ident $field:ident) => (
         $self.struct_type_index($ty)?;
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         $self.printer.print_field_idx($self.state, $ty, $field)?;
     );
     (payload $self:ident StructGetU $ty:ident $field:ident) => (
         $self.struct_type_index($ty)?;
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         $self.printer.print_field_idx($self.state, $ty, $field)?;
     );
     (payload $self:ident StructSet $ty:ident $field:ident) => (
         $self.struct_type_index($ty)?;
-        $self.push_str(" ");
+        $self.push_str(" ")?;
         $self.printer.print_field_idx($self.state, $ty, $field)?;
     );
     (payload $self:ident $op:ident $($arg:ident)*) => (
@@ -1162,7 +1166,7 @@ macro_rules! define_visit {
     (name GlobalAtomicRmwCmpxchg) => ("global.atomic.rmw.cmpxchg");
 }
 
-impl<'a> VisitOperator<'a> for PrintOperator<'_, '_> {
+impl<'a> VisitOperator<'a> for PrintOperator<'_, '_, '_> {
     type Output = Result<()>;
 
     wasmparser::for_each_operator!(define_visit);
diff --git a/crates/wit-component/src/encoding.rs b/crates/wit-component/src/encoding.rs
index 336266de..057e755c 100644
--- a/crates/wit-component/src/encoding.rs
+++ b/crates/wit-component/src/encoding.rs
@@ -2133,33 +2133,33 @@ mod test {
 
     use super::*;
     use std::path::Path;
-    use wit_parser::UnresolvedPackage;
+    use wit_parser::UnresolvedPackageGroup;
 
     #[test]
     fn it_renames_imports() {
         let mut resolve = Resolve::new();
-        let pkg = resolve
-            .push(
-                UnresolvedPackage::parse(
-                    Path::new("test.wit"),
-                    r#"
+        let UnresolvedPackageGroup {
+            mut packages,
+            source_map,
+        } = UnresolvedPackageGroup::parse(
+            Path::new("test.wit"),
+            r#"
 package test:wit;
 
 interface i {
-    f: func();
+f: func();
 }
 
 world test {
-    import i;
-    import foo: interface {
-        f: func();
-    }
+import i;
+import foo: interface {
+f: func();
+}
 }
 "#,
-                )
-                .unwrap(),
-            )
-            .unwrap();
+        )
+        .unwrap();
+        let pkg = resolve.push(packages.remove(0), &source_map).unwrap();
 
         let world = resolve.select_world(pkg, None).unwrap();
 
diff --git a/crates/wit-component/src/lib.rs b/crates/wit-component/src/lib.rs
index 9b3be71c..7d81335e 100644
--- a/crates/wit-component/src/lib.rs
+++ b/crates/wit-component/src/lib.rs
@@ -5,9 +5,9 @@
 use std::str::FromStr;
 use std::{borrow::Cow, fmt::Display};
 
-use anyhow::{bail, Result};
+use anyhow::{bail, Context, Result};
 use wasm_encoder::{CanonicalOption, Encode, Section};
-use wit_parser::{Resolve, WorldId};
+use wit_parser::{parse_use_path, PackageId, ParsedUsePath, Resolve, WorldId};
 
 mod encoding;
 mod gc;
@@ -79,6 +79,43 @@ impl From<StringEncoding> for wasm_encoder::CanonicalOption {
     }
 }
 
+/// Handles world name resolution for cases when multiple packages may have been resolved. If this
+/// is the case, and we're dealing with input that contains a user-supplied world name (like via a
+/// CLI command, for instance), we want to ensure that the world name follows the following rules:
+///
+///   * If there is a single resolved package with a single world, the world name name MAY be
+///     omitted.
+///   * If there is a single resolved package with multiple worlds, the world name MUST be supplied,
+///     but MAY or MAY NOT be fully-qualified.
+///   * If there are multiple resolved packages, the world name MUST be fully-qualified.
+pub fn resolve_world_from_name(
+    resolve: &Resolve,
+    resolved_packages: Vec<PackageId>,
+    world_name: Option<&str>,
+) -> Result<WorldId> {
+    match resolved_packages.len() {
+        0 => bail!("all of the supplied WIT source files were empty"),
+        1 => resolve.select_world(resolved_packages[0], world_name.as_deref()),
+        _ => match world_name.as_deref() {
+            Some(name) => {
+                let world_path = parse_use_path(name).with_context(|| {
+                    format!("failed to parse world specifier `{name}`")
+                })?;
+                match world_path {
+                        ParsedUsePath::Name(name) => bail!("the world specifier must be of the fully-qualified, id-based form (ex: \"wasi:http/proxy\" rather than \"proxy\"); you used {name}"),
+                        ParsedUsePath::Package(pkg_name, _) => {
+                            match resolve.package_names.get(&pkg_name) {
+                                Some(pkg_id) => resolve.select_world(pkg_id.clone(), world_name.as_deref()),
+                                None => bail!("the world specifier you provided named {pkg_name}, but no package with that name was found"),
+                            }
+                        }
+                    }
+            }
+            None => bail!("the supplied WIT source files describe multiple packages; please provide a fully-qualified world-specifier to the `embed` command"),
+        },
+    }
+}
+
 /// A producer section to be added to all modules and components synthesized by
 /// this crate
 pub(crate) fn base_producers() -> wasm_metadata::Producers {
@@ -112,7 +149,7 @@ mod tests {
 
     use anyhow::Result;
     use wasmparser::Payload;
-    use wit_parser::{Resolve, UnresolvedPackage};
+    use wit_parser::{Resolve, UnresolvedPackageGroup};
 
     use super::{embed_component_metadata, StringEncoding};
 
@@ -147,8 +184,11 @@ world test-world {}
 
         // Parse pre-canned WIT to build resolver
         let mut resolver = Resolve::default();
-        let pkg = UnresolvedPackage::parse(&Path::new("in-code.wit"), COMPONENT_WIT)?;
-        let pkg_id = resolver.push(pkg)?;
+        let UnresolvedPackageGroup {
+            mut packages,
+            source_map,
+        } = UnresolvedPackageGroup::parse(&Path::new("in-code.wit"), COMPONENT_WIT)?;
+        let pkg_id = resolver.push(packages.remove(0), &source_map)?;
         let world = resolver.select_world(pkg_id, Some("test-world").into())?;
 
         // Embed component metadata
diff --git a/crates/wit-component/src/metadata.rs b/crates/wit-component/src/metadata.rs
index 0fdc7e5d..59cb7353 100644
--- a/crates/wit-component/src/metadata.rs
+++ b/crates/wit-component/src/metadata.rs
@@ -42,7 +42,7 @@
 //! the three arguments originally passed to `encode`.
 
 use crate::validation::BARE_FUNC_MODULE_NAME;
-use crate::{DecodedWasm, StringEncoding};
+use crate::{resolve_world_from_name, DecodedWasm, StringEncoding};
 use anyhow::{bail, Context, Result};
 use indexmap::IndexMap;
 use std::borrow::Cow;
@@ -259,12 +259,12 @@ impl Bindgen {
                 let world_name = reader.read_string()?;
                 wasm = &data[reader.original_position()..];
 
-                let (r, pkg) = match crate::decode(wasm)? {
-                    DecodedWasm::WitPackage(resolve, pkg) => (resolve, pkg),
-                    DecodedWasm::Component(..) => bail!("expected an encoded wit package"),
+                let (r, pkgs) = match crate::decode(wasm)? {
+                    DecodedWasm::WitPackages(resolve, pkgs) => (resolve, pkgs),
+                    DecodedWasm::Component(..) => bail!("expected encoded wit package(s)"),
                 };
                 resolve = r;
-                world = resolve.packages[pkg].worlds[world_name];
+                world = resolve_world_from_name(&resolve, pkgs, Some(world_name.into()))?;
             }
 
             // Current format where `data` is a wasm component itself.
diff --git a/crates/wit-component/src/printing.rs b/crates/wit-component/src/printing.rs
index 33be432d..28406a50 100644
--- a/crates/wit-component/src/printing.rs
+++ b/crates/wit-component/src/printing.rs
@@ -50,37 +50,56 @@ impl WitPrinter {
         self
     }
 
-    /// Print the given WIT package to a string.
-    pub fn print(&mut self, resolve: &Resolve, pkgid: PackageId) -> Result<String> {
-        let pkg = &resolve.packages[pkgid];
-        self.print_docs(&pkg.docs);
-        self.output.push_str("package ");
-        self.print_name(&pkg.name.namespace);
-        self.output.push_str(":");
-        self.print_name(&pkg.name.name);
-        if let Some(version) = &pkg.name.version {
-            self.output.push_str(&format!("@{version}"));
-        }
-        self.print_semicolon();
-        self.output.push_str("\n\n");
-        for (name, id) in pkg.interfaces.iter() {
-            self.print_docs(&resolve.interfaces[*id].docs);
-            self.print_stability(&resolve.interfaces[*id].stability);
-            self.output.push_str("interface ");
-            self.print_name(name);
-            self.output.push_str(" {\n");
-            self.print_interface(resolve, *id)?;
-            writeln!(&mut self.output, "}}\n")?;
-        }
+    /// Print a set of one or more WIT packages into a string.
+    pub fn print(&mut self, resolve: &Resolve, pkg_ids: &[PackageId]) -> Result<String> {
+        let has_multiple_packages = pkg_ids.len() > 1;
+        for (i, pkg_id) in pkg_ids.into_iter().enumerate() {
+            if i > 0 {
+                self.output.push_str("\n\n");
+            }
 
-        for (name, id) in pkg.worlds.iter() {
-            self.print_docs(&resolve.worlds[*id].docs);
-            self.print_stability(&resolve.worlds[*id].stability);
-            self.output.push_str("world ");
-            self.print_name(name);
-            self.output.push_str(" {\n");
-            self.print_world(resolve, *id)?;
-            writeln!(&mut self.output, "}}")?;
+            let pkg = &resolve.packages[pkg_id.clone()];
+            self.print_docs(&pkg.docs);
+            self.output.push_str("package ");
+            self.print_name(&pkg.name.namespace);
+            self.output.push_str(":");
+            self.print_name(&pkg.name.name);
+            if let Some(version) = &pkg.name.version {
+                self.output.push_str(&format!("@{version}"));
+            }
+
+            if has_multiple_packages {
+                self.output.push_str("{");
+                self.output.indent += 1
+            } else {
+                self.print_semicolon();
+                self.output.push_str("\n\n");
+            }
+
+            for (name, id) in pkg.interfaces.iter() {
+                self.print_docs(&resolve.interfaces[*id].docs);
+                self.print_stability(&resolve.interfaces[*id].stability);
+                self.output.push_str("interface ");
+                self.print_name(name);
+                self.output.push_str(" {\n");
+                self.print_interface(resolve, *id)?;
+                writeln!(&mut self.output, "}}\n")?;
+            }
+
+            for (name, id) in pkg.worlds.iter() {
+                self.print_docs(&resolve.worlds[*id].docs);
+                self.print_stability(&resolve.worlds[*id].stability);
+                self.output.push_str("world ");
+                self.print_name(name);
+                self.output.push_str(" {\n");
+                self.print_world(resolve, *id)?;
+                writeln!(&mut self.output, "}}")?;
+            }
+
+            if has_multiple_packages {
+                self.output.push_str("}");
+                self.output.indent -= 1
+            }
         }
 
         Ok(std::mem::take(&mut self.output).into())
diff --git a/crates/wit-component/src/semver_check.rs b/crates/wit-component/src/semver_check.rs
index 66d01566..011b108d 100644
--- a/crates/wit-component/src/semver_check.rs
+++ b/crates/wit-component/src/semver_check.rs
@@ -2,7 +2,7 @@ use crate::{
     dummy_module, embed_component_metadata, encoding::encode_world, ComponentEncoder,
     StringEncoding,
 };
-use anyhow::{Context, Result};
+use anyhow::{bail, Context, Result};
 use wasm_encoder::{ComponentBuilder, ComponentExportKind, ComponentTypeRef};
 use wasmparser::{Validator, WasmFeatures};
 use wit_parser::{Resolve, WorldId};
@@ -47,6 +47,18 @@ pub fn semver_check(mut resolve: Resolve, prev: WorldId, new: WorldId) -> Result
         pkg.name.version = None;
     }
 
+    let old_pkg_id = resolve.worlds[prev]
+        .package
+        .context("old world not in named package")?;
+    let old_pkg_name = &resolve.packages[old_pkg_id].name;
+    let new_pkg_id = resolve.worlds[new]
+        .package
+        .context("new world not in named package")?;
+    let new_pkg_name = &resolve.packages[new_pkg_id].name;
+    if old_pkg_id != new_pkg_id {
+        bail!("the old world is in package {old_pkg_name}, which is not the same as the new world, which is in package {new_pkg_name}", )
+    }
+
     // Component that will be validated at the end.
     let mut root_component = ComponentBuilder::default();
 
diff --git a/crates/wit-parser/fuzz/fuzz_targets/parse.rs b/crates/wit-parser/fuzz/fuzz_targets/parse.rs
index 4bc47fb8..b9d21dc4 100644
--- a/crates/wit-parser/fuzz/fuzz_targets/parse.rs
+++ b/crates/wit-parser/fuzz/fuzz_targets/parse.rs
@@ -10,5 +10,8 @@ fuzz_target!(|data: &[u8]| {
         Err(_) => return,
     };
 
-    drop(wit_parser::UnresolvedPackage::parse("foo".as_ref(), &data));
+    drop(wit_parser::UnresolvedPackageGroup::parse(
+        "foo".as_ref(),
+        &data,
+    ));
 });
diff --git a/crates/wit-parser/src/ast.rs b/crates/wit-parser/src/ast.rs
index 22a7d7d1..d65d5fed 100644
--- a/crates/wit-parser/src/ast.rs
+++ b/crates/wit-parser/src/ast.rs
@@ -1,8 +1,9 @@
-use crate::{Error, UnresolvedPackage};
+use crate::{Error, UnresolvedPackage, UnresolvedPackageGroup};
 use anyhow::{bail, Context, Result};
 use lex::{Span, Token, Tokenizer};
 use semver::Version;
 use std::borrow::Cow;
+use std::collections::HashSet;
 use std::fmt;
 use std::path::{Path, PathBuf};
 
@@ -14,29 +15,131 @@ pub mod toposort;
 
 pub use lex::validate_id;
 
-pub struct Ast<'a> {
+enum Ast<'a> {
+    ExplicitPackages(Vec<ExplicitPackage<'a>>),
+    PartialImplicitPackage(PartialImplicitPackage<'a>),
+}
+
+pub struct ExplicitPackage<'a> {
+    package_id: PackageName<'a>,
+    decl_list: DeclList<'a>,
+}
+
+pub struct PartialImplicitPackage<'a> {
     package_id: Option<PackageName<'a>>,
+    decl_list: DeclList<'a>,
+}
+
+/// Stores all of the declarations in a package's scope. In AST terms, this
+/// means everything except the `package` declaration that demarcates a package
+/// scope. In the traditional implicit format, these are all of the declarations
+/// non-`package` declarations in the file:
+///
+/// ```wit
+/// package foo:name;
+///
+/// /* START DECL LIST */
+/// // Some comment...
+/// interface i {}
+/// world w {}
+/// /* END DECL LIST */
+/// ```
+///
+/// In the explicit package style, a [`DeclList`] is everything inside of each
+/// `package` element's brackets:
+///
+/// ```wit
+/// package foo:name {
+///   /* START FIRST DECL LIST */
+///   // Some comment...
+///   interface i {}
+///   world w {}
+///   /* END FIRST DECL LIST */
+/// }
+///
+/// package bar:name {
+///   /* START SECOND DECL LIST */
+///   // Some comment...
+///   interface i {}
+///   world w {}
+///   /* END SECOND DECL LIST */
+/// }
+/// ```
+pub struct DeclList<'a> {
     items: Vec<AstItem<'a>>,
 }
 
-impl<'a> Ast<'a> {
-    pub fn parse(lexer: &mut Tokenizer<'a>) -> Result<Self> {
+impl<'a> DeclList<'a> {
+    fn parse_explicit_package_items(tokens: &mut Tokenizer<'a>) -> Result<DeclList<'a>> {
         let mut items = Vec::new();
-        let mut package_id = None;
-        let mut docs = parse_docs(lexer)?;
-        if lexer.eat(Token::Package)? {
-            let package_docs = std::mem::take(&mut docs);
-            package_id = Some(PackageName::parse(lexer, package_docs)?);
-            lexer.expect_semicolon()?;
-            docs = parse_docs(lexer)?;
+        let mut docs = parse_docs(tokens)?;
+        loop {
+            if tokens.eat(Token::RightBrace)? {
+                return Ok(DeclList { items });
+            }
+            items.push(AstItem::parse(tokens, docs)?);
+            docs = parse_docs(tokens)?;
         }
-        while lexer.clone().next()?.is_some() {
-            items.push(AstItem::parse(lexer, docs)?);
-            docs = parse_docs(lexer)?;
+    }
+
+    fn parse_implicit_package_items(
+        tokens: &mut Tokenizer<'a>,
+        mut docs: Docs<'a>,
+    ) -> Result<DeclList<'a>> {
+        let mut items = Vec::new();
+        while tokens.clone().next()?.is_some() {
+            items.push(AstItem::parse(tokens, docs)?);
+            docs = parse_docs(tokens)?;
         }
-        Ok(Self { package_id, items })
+        Ok(DeclList { items })
     }
+}
 
+impl<'a> Ast<'a> {
+    fn parse(tokens: &mut Tokenizer<'a>) -> Result<Self> {
+        let mut maybe_package_id = None;
+        let mut packages = Vec::new();
+        let mut docs = parse_docs(tokens)?;
+        loop {
+            if tokens.clone().next()?.is_none() {
+                break;
+            }
+            if !tokens.eat(Token::Package)? {
+                if !packages.is_empty() {
+                    bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds");
+                }
+                break;
+            }
+
+            let package_id = PackageName::parse(tokens, std::mem::take(&mut docs))?;
+            if tokens.eat(Token::LeftBrace)? {
+                packages.push(ExplicitPackage {
+                    package_id: package_id,
+                    decl_list: DeclList::parse_explicit_package_items(tokens)?,
+                });
+                docs = parse_docs(tokens)?;
+            } else {
+                maybe_package_id = Some(package_id);
+                tokens.expect_semicolon()?;
+                if !packages.is_empty() {
+                    bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds");
+                }
+                docs = parse_docs(tokens)?;
+                break;
+            }
+        }
+
+        if packages.is_empty() {
+            return Ok(Ast::PartialImplicitPackage(PartialImplicitPackage {
+                package_id: maybe_package_id,
+                decl_list: DeclList::parse_implicit_package_items(tokens, docs)?,
+            }));
+        }
+        Ok(Ast::ExplicitPackages(packages))
+    }
+}
+
+impl<'a> DeclList<'a> {
     fn for_each_path<'b>(
         &'b self,
         mut f: impl FnMut(
@@ -1483,6 +1586,40 @@ struct Source {
     contents: String,
 }
 
+enum ResolverKind<'a> {
+    Unknown,
+    Explicit(Vec<UnresolvedPackage>),
+    PartialImplicit(Resolver<'a>),
+}
+
+fn parse_package(
+    unparsed_pkgs: Vec<ExplicitPackage>,
+    src: &Source,
+    explicit_pkg_names: &mut HashSet<crate::PackageName>,
+    parsed_pkgs: &mut Vec<UnresolvedPackage>,
+) -> Result<()> {
+    for pkg in unparsed_pkgs {
+        let mut resolver = Resolver::default();
+        let pkg_name = pkg.package_id.package_name();
+        let ingested = resolver
+            .push_then_resolve(pkg)
+            .with_context(|| format!("failed to start resolving path: {}", src.path.display()))?;
+
+        match explicit_pkg_names.get(&pkg_name) {
+            Some(_) => bail!(
+                "colliding explicit package names, multiple packages named `{}`",
+                pkg_name
+            ),
+            None => explicit_pkg_names.insert(pkg_name),
+        };
+
+        if let Some(unresolved) = ingested {
+            parsed_pkgs.push(unresolved);
+        }
+    }
+    Ok(())
+}
+
 impl SourceMap {
     /// Creates a new empty source map.
     pub fn new() -> SourceMap {
@@ -1532,10 +1669,11 @@ impl SourceMap {
         self.offset = new_offset;
     }
 
-    /// Parses the files added to this source map into an [`UnresolvedPackage`].
-    pub fn parse(self) -> Result<UnresolvedPackage> {
-        let mut doc = self.rewrite_error(|| {
-            let mut resolver = Resolver::default();
+    /// Parses the files added to this source map into one or more [`UnresolvedPackage`]s.
+    pub fn parse(self) -> Result<UnresolvedPackageGroup> {
+        let mut resolver_kind = ResolverKind::Unknown;
+        let parsed_pkgs = self.rewrite_error(|| {
+            let mut explicit_pkg_names: HashSet<crate::PackageName> = HashSet::new();
             let mut srcs = self.sources.iter().collect::<Vec<_>>();
             srcs.sort_by_key(|src| &src.path);
             for src in srcs {
@@ -1548,15 +1686,55 @@ impl SourceMap {
                     self.require_f32_f64,
                 )
                 .with_context(|| format!("failed to tokenize path: {}", src.path.display()))?;
-                let ast = Ast::parse(&mut tokens)?;
-                resolver.push(ast).with_context(|| {
-                    format!("failed to start resolving path: {}", src.path.display())
-                })?;
+
+                match Ast::parse(&mut tokens)? {
+                    Ast::ExplicitPackages(pkgs) => {
+                        match &mut resolver_kind {
+                            ResolverKind::Unknown => {
+                                let mut parsed_pkgs = Vec::new();
+                                parse_package(pkgs, src, &mut explicit_pkg_names, &mut parsed_pkgs)?;
+                                resolver_kind = ResolverKind::Explicit(parsed_pkgs);
+                            },
+                            ResolverKind::Explicit(parsed_pkgs) => {
+                                parse_package(pkgs, src, &mut explicit_pkg_names, parsed_pkgs)?;
+                            },
+                            ResolverKind::PartialImplicit(_) => bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds"),
+                        }
+                    }
+                    Ast::PartialImplicitPackage(partial) => {
+                        match &mut resolver_kind {
+                            ResolverKind::Unknown => {
+                                let mut resolver = Resolver::default();
+                                resolver.push_partial(partial).with_context(|| {
+                                    format!("failed to start resolving path: {}", src.path.display())
+                                })?;
+                                resolver_kind = ResolverKind::PartialImplicit(resolver);
+                            },
+                            ResolverKind::Explicit(_) => bail!("WIT files cannot mix top-level explicit `package` declarations with other declaration kinds"),
+                            ResolverKind::PartialImplicit(resolver) => {
+                                resolver.push_partial(partial).with_context(|| {
+                                    format!("failed to start resolving path: {}", src.path.display())
+                                })?;
+                            }
+                        }
+                    }
+                }
+            }
+
+            match resolver_kind {
+                ResolverKind::Unknown => bail!("No WIT packages found in the supplied source"),
+                ResolverKind::Explicit(pkgs) => Ok(pkgs),
+                ResolverKind::PartialImplicit(mut resolver) => match resolver.resolve()? {
+                    Some(pkg) => Ok(vec![pkg]),
+                    None => bail!("No WIT packages found in the supplied source"),
+                },
             }
-            resolver.resolve()
         })?;
-        doc.source_map = self;
-        Ok(doc)
+
+        Ok(UnresolvedPackageGroup {
+            packages: parsed_pkgs,
+            source_map: self,
+        })
     }
 
     pub(crate) fn rewrite_error<F, T>(&self, f: F) -> Result<T>
@@ -1653,21 +1831,21 @@ impl SourceMap {
     }
 }
 
-pub(crate) enum AstUsePath {
+pub enum ParsedUsePath {
     Name(String),
     Package(crate::PackageName, String),
 }
 
-pub(crate) fn parse_use_path(s: &str) -> Result<AstUsePath> {
+pub fn parse_use_path(s: &str) -> Result<ParsedUsePath> {
     let mut tokens = Tokenizer::new(s, 0, Some(true), None)?;
     let path = UsePath::parse(&mut tokens)?;
     if tokens.next()?.is_some() {
         bail!("trailing tokens in path specifier");
     }
     Ok(match path {
-        UsePath::Id(id) => AstUsePath::Name(id.name.to_string()),
+        UsePath::Id(id) => ParsedUsePath::Name(id.name.to_string()),
         UsePath::Package { id, name } => {
-            AstUsePath::Package(id.package_name(), name.name.to_string())
+            ParsedUsePath::Package(id.package_name(), name.name.to_string())
         }
     })
 }
diff --git a/crates/wit-parser/src/ast/lex.rs b/crates/wit-parser/src/ast/lex.rs
index 51a4623c..93ad6008 100644
--- a/crates/wit-parser/src/ast/lex.rs
+++ b/crates/wit-parser/src/ast/lex.rs
@@ -190,6 +190,9 @@ impl<'a> Tokenizer<'a> {
         }
     }
 
+    /// Three possibilities when calling this method: an `Err(...)` indicates that lexing failed, an
+    /// `Ok(Some(...))` produces the next token, and `Ok(None)` indicates that there are no more
+    /// tokens available.
     pub fn next_raw(&mut self) -> Result<Option<(Span, Token)>, Error> {
         let (str_start, ch) = match self.chars.next() {
             Some(pair) => pair,
diff --git a/crates/wit-parser/src/ast/resolve.rs b/crates/wit-parser/src/ast/resolve.rs
index 4004a846..0d3465a6 100644
--- a/crates/wit-parser/src/ast/resolve.rs
+++ b/crates/wit-parser/src/ast/resolve.rs
@@ -13,8 +13,8 @@ pub struct Resolver<'a> {
     /// Package docs.
     package_docs: Docs,
 
-    /// All WIT files which are going to be resolved together.
-    asts: Vec<ast::Ast<'a>>,
+    /// All non-`package` WIT decls are going to be resolved together.
+    decl_lists: Vec<ast::DeclList<'a>>,
 
     // Arenas that get plumbed to the final `UnresolvedPackage`
     types: Arena<TypeDef>,
@@ -29,9 +29,9 @@ pub struct Resolver<'a> {
     /// is updated as the ASTs are walked.
     cur_ast_index: usize,
 
-    /// A map per `ast::Ast` which keeps track of the file's top level names in
-    /// scope. This maps each name onto either a world or an interface, handling
-    /// things like `use` at the top level.
+    /// A map per `ast::DeclList` which keeps track of the file's top level
+    /// names in scope. This maps each name onto either a world or an interface,
+    /// handling things like `use` at the top level.
     ast_items: Vec<IndexMap<&'a str, AstItem>>,
 
     /// A map for the entire package being created of all names defined within,
@@ -109,11 +109,11 @@ enum TypeOrItem {
 }
 
 impl<'a> Resolver<'a> {
-    pub(crate) fn push(&mut self, ast: ast::Ast<'a>) -> Result<()> {
+    pub(crate) fn push_partial(&mut self, partial: ast::PartialImplicitPackage<'a>) -> Result<()> {
         // As each WIT file is pushed into this resolver keep track of the
         // current package name assigned. Only one file needs to mention it, but
         // if multiple mention it then they must all match.
-        if let Some(cur) = &ast.package_id {
+        if let Some(cur) = &partial.package_id {
             let cur_name = cur.package_name();
             if let Some(prev) = &self.package_name {
                 if cur_name != *prev {
@@ -140,33 +140,38 @@ impl<'a> Resolver<'a> {
                 self.package_docs = docs;
             }
         }
-        self.asts.push(ast);
+        self.decl_lists.push(partial.decl_list);
         Ok(())
     }
 
-    pub(crate) fn resolve(&mut self) -> Result<UnresolvedPackage> {
+    pub(crate) fn resolve(&mut self) -> Result<Option<UnresolvedPackage>> {
         // At least one of the WIT files must have a `package` annotation.
         let name = match &self.package_name {
             Some(name) => name.clone(),
-            None => bail!("no `package` header was found in any WIT file for this package"),
+            None => {
+                if self.decl_lists.is_empty() {
+                    return Ok(None);
+                }
+                bail!("no `package` header was found in any WIT file for this package")
+            }
         };
 
         // First populate information about foreign dependencies and the general
         // structure of the package. This should resolve the "base" of many
         // `use` statements and additionally generate a topological ordering of
         // all interfaces in the package to visit.
-        let asts = mem::take(&mut self.asts);
-        self.populate_foreign_deps(&asts);
-        let (iface_order, world_order) = self.populate_ast_items(&asts)?;
-        self.populate_foreign_types(&asts)?;
+        let decl_lists = mem::take(&mut self.decl_lists);
+        self.populate_foreign_deps(&decl_lists);
+        let (iface_order, world_order) = self.populate_ast_items(&decl_lists)?;
+        self.populate_foreign_types(&decl_lists)?;
 
         // Use the topological ordering of all interfaces to resolve all
         // interfaces in-order. Note that a reverse-mapping from ID to AST is
         // generated here to assist with this.
         let mut iface_id_to_ast = IndexMap::new();
         let mut world_id_to_ast = IndexMap::new();
-        for (i, ast) in asts.iter().enumerate() {
-            for item in ast.items.iter() {
+        for (i, decl_list) in decl_lists.iter().enumerate() {
+            for item in decl_list.items.iter() {
                 match item {
                     ast::AstItem::Interface(iface) => {
                         let id = match self.ast_items[i][iface.name.name] {
@@ -199,7 +204,7 @@ impl<'a> Resolver<'a> {
             self.resolve_world(id, world)?;
         }
 
-        Ok(UnresolvedPackage {
+        Ok(Some(UnresolvedPackage {
             name,
             docs: mem::take(&mut self.package_docs),
             worlds: mem::take(&mut self.worlds),
@@ -222,61 +227,71 @@ impl<'a> Resolver<'a> {
             world_spans: mem::take(&mut self.world_spans),
             type_spans: mem::take(&mut self.type_spans),
             foreign_dep_spans: mem::take(&mut self.foreign_dep_spans),
-            source_map: SourceMap::default(),
             required_resource_types: mem::take(&mut self.required_resource_types),
-        })
+        }))
+    }
+
+    pub(crate) fn push_then_resolve(
+        &mut self,
+        package: ast::ExplicitPackage<'a>,
+    ) -> Result<Option<UnresolvedPackage>> {
+        self.package_name = Some(package.package_id.package_name());
+        self.docs(&package.package_id.docs);
+        self.decl_lists = vec![package.decl_list];
+        self.resolve()
     }
 
     /// Registers all foreign dependencies made within the ASTs provided.
     ///
     /// This will populate the `self.foreign_{deps,interfaces,worlds}` maps with all
     /// `UsePath::Package` entries.
-    fn populate_foreign_deps(&mut self, asts: &[ast::Ast<'a>]) {
+    fn populate_foreign_deps(&mut self, decl_lists: &[ast::DeclList<'a>]) {
         let mut foreign_deps = mem::take(&mut self.foreign_deps);
         let mut foreign_interfaces = mem::take(&mut self.foreign_interfaces);
         let mut foreign_worlds = mem::take(&mut self.foreign_worlds);
-        for ast in asts {
-            ast.for_each_path(|_, path, _names, world_or_iface| {
-                let (id, name) = match path {
-                    ast::UsePath::Package { id, name } => (id, name),
-                    _ => return Ok(()),
-                };
-
-                let deps = foreign_deps.entry(id.package_name()).or_insert_with(|| {
-                    self.foreign_dep_spans.push(id.span);
-                    IndexMap::new()
-                });
-                let id = *deps.entry(name.name).or_insert_with(|| {
-                    match world_or_iface {
-                        WorldOrInterface::World => {
-                            log::trace!(
-                                "creating a world for foreign dep: {}/{}",
-                                id.package_name(),
-                                name.name
-                            );
-                            AstItem::World(self.alloc_world(name.span))
-                        }
-                        WorldOrInterface::Interface | WorldOrInterface::Unknown => {
-                            // Currently top-level `use` always assumes an interface, so the
-                            // `Unknown` case is the same as `Interface`.
-                            log::trace!(
-                                "creating an interface for foreign dep: {}/{}",
-                                id.package_name(),
-                                name.name
-                            );
-                            AstItem::Interface(self.alloc_interface(name.span))
+        for decl_list in decl_lists {
+            decl_list
+                .for_each_path(|_, path, _names, world_or_iface| {
+                    let (id, name) = match path {
+                        ast::UsePath::Package { id, name } => (id, name),
+                        _ => return Ok(()),
+                    };
+
+                    let deps = foreign_deps.entry(id.package_name()).or_insert_with(|| {
+                        self.foreign_dep_spans.push(id.span);
+                        IndexMap::new()
+                    });
+                    let id = *deps.entry(name.name).or_insert_with(|| {
+                        match world_or_iface {
+                            WorldOrInterface::World => {
+                                log::trace!(
+                                    "creating a world for foreign dep: {}/{}",
+                                    id.package_name(),
+                                    name.name
+                                );
+                                AstItem::World(self.alloc_world(name.span))
+                            }
+                            WorldOrInterface::Interface | WorldOrInterface::Unknown => {
+                                // Currently top-level `use` always assumes an interface, so the
+                                // `Unknown` case is the same as `Interface`.
+                                log::trace!(
+                                    "creating an interface for foreign dep: {}/{}",
+                                    id.package_name(),
+                                    name.name
+                                );
+                                AstItem::Interface(self.alloc_interface(name.span))
+                            }
                         }
-                    }
-                });
+                    });
 
-                let _ = match id {
-                    AstItem::Interface(id) => foreign_interfaces.insert(id),
-                    AstItem::World(id) => foreign_worlds.insert(id),
-                };
+                    let _ = match id {
+                        AstItem::Interface(id) => foreign_interfaces.insert(id),
+                        AstItem::World(id) => foreign_worlds.insert(id),
+                    };
 
-                Ok(())
-            })
-            .unwrap();
+                    Ok(())
+                })
+                .unwrap();
         }
         self.foreign_deps = foreign_deps;
         self.foreign_interfaces = foreign_interfaces;
@@ -323,18 +338,18 @@ impl<'a> Resolver<'a> {
     /// generated for resolving use-paths later on.
     fn populate_ast_items(
         &mut self,
-        asts: &[ast::Ast<'a>],
+        decl_lists: &[ast::DeclList<'a>],
     ) -> Result<(Vec<InterfaceId>, Vec<WorldId>)> {
         let mut package_items = IndexMap::new();
 
         // Validate that all worlds and interfaces have unique names within this
         // package across all ASTs which make up the package.
         let mut names = HashMap::new();
-        let mut ast_namespaces = Vec::new();
+        let mut decl_list_namespaces = Vec::new();
         let mut order = IndexMap::new();
-        for ast in asts {
-            let mut ast_ns = IndexMap::new();
-            for item in ast.items.iter() {
+        for decl_list in decl_lists {
+            let mut decl_list_ns = IndexMap::new();
+            for item in decl_list.items.iter() {
                 match item {
                     ast::AstItem::Interface(i) => {
                         if package_items.insert(i.name.name, i.name.span).is_some() {
@@ -343,7 +358,7 @@ impl<'a> Resolver<'a> {
                                 format!("duplicate item named `{}`", i.name.name),
                             ))
                         }
-                        let prev = ast_ns.insert(i.name.name, ());
+                        let prev = decl_list_ns.insert(i.name.name, ());
                         assert!(prev.is_none());
                         let prev = order.insert(i.name.name, Vec::new());
                         assert!(prev.is_none());
@@ -357,7 +372,7 @@ impl<'a> Resolver<'a> {
                                 format!("duplicate item named `{}`", w.name.name),
                             ))
                         }
-                        let prev = ast_ns.insert(w.name.name, ());
+                        let prev = decl_list_ns.insert(w.name.name, ());
                         assert!(prev.is_none());
                         let prev = order.insert(w.name.name, Vec::new());
                         assert!(prev.is_none());
@@ -368,7 +383,7 @@ impl<'a> Resolver<'a> {
                     ast::AstItem::Use(_) => {}
                 }
             }
-            ast_namespaces.push(ast_ns);
+            decl_list_namespaces.push(decl_list_ns);
         }
 
         // Next record dependencies between interfaces as induced via `use`
@@ -380,13 +395,13 @@ impl<'a> Resolver<'a> {
             Local(ast::Id<'a>),
         }
 
-        for ast in asts {
+        for decl_list in decl_lists {
             // Record, in the context of this file, what all names are defined
             // at the top level and whether they point to other items in this
             // package or foreign items. Foreign deps are ignored for
             // topological ordering.
-            let mut ast_ns = IndexMap::new();
-            for item in ast.items.iter() {
+            let mut decl_list_ns = IndexMap::new();
+            for item in decl_list.items.iter() {
                 let (name, src) = match item {
                     ast::AstItem::Use(u) => {
                         let name = u.as_.as_ref().unwrap_or(u.item.name());
@@ -399,7 +414,7 @@ impl<'a> Resolver<'a> {
                     ast::AstItem::Interface(i) => (&i.name, ItemSource::Local(i.name.clone())),
                     ast::AstItem::World(w) => (&w.name, ItemSource::Local(w.name.clone())),
                 };
-                if ast_ns.insert(name.name, (name.span, src)).is_some() {
+                if decl_list_ns.insert(name.name, (name.span, src)).is_some() {
                     bail!(Error::new(
                         name.span,
                         format!("duplicate name `{}` in this file", name.name),
@@ -409,7 +424,7 @@ impl<'a> Resolver<'a> {
 
             // With this file's namespace information look at all `use` paths
             // and record dependencies between interfaces.
-            ast.for_each_path(|iface, path, _names, _| {
+            decl_list.for_each_path(|iface, path, _names, _| {
                 // If this import isn't contained within an interface then it's
                 // in a world and it doesn't need to participate in our
                 // topo-sort.
@@ -421,7 +436,7 @@ impl<'a> Resolver<'a> {
                     ast::UsePath::Id(id) => id,
                     ast::UsePath::Package { .. } => return Ok(()),
                 };
-                match ast_ns.get(used_name.name) {
+                match decl_list_ns.get(used_name.name) {
                     Some((_, ItemSource::Foreign)) => return Ok(()),
                     Some((_, ItemSource::Local(id))) => {
                         order[iface.name].push(id.clone());
@@ -473,9 +488,9 @@ impl<'a> Resolver<'a> {
                 ast::AstItem::Use(_) => unreachable!(),
             };
         }
-        for ast in asts {
+        for decl_list in decl_lists {
             let mut items = IndexMap::new();
-            for item in ast.items.iter() {
+            for item in decl_list.items.iter() {
                 let (name, ast_item) = match item {
                     ast::AstItem::Use(u) => {
                         if !u.attributes.is_empty() {
@@ -533,10 +548,10 @@ impl<'a> Resolver<'a> {
     /// This is done after all interfaces are generated so `self.resolve_path`
     /// can be used to determine if what's being imported from is a foreign
     /// interface or not.
-    fn populate_foreign_types(&mut self, asts: &[ast::Ast<'a>]) -> Result<()> {
-        for (i, ast) in asts.iter().enumerate() {
+    fn populate_foreign_types(&mut self, decl_lists: &[ast::DeclList<'a>]) -> Result<()> {
+        for (i, decl_list) in decl_lists.iter().enumerate() {
             self.cur_ast_index = i;
-            ast.for_each_path(|_, path, names, _| {
+            decl_list.for_each_path(|_, path, names, _| {
                 let names = match names {
                     Some(names) => names,
                     None => return Ok(()),
diff --git a/crates/wit-parser/src/decoding.rs b/crates/wit-parser/src/decoding.rs
index 848aa6ea..1c61ecfd 100644
--- a/crates/wit-parser/src/decoding.rs
+++ b/crates/wit-parser/src/decoding.rs
@@ -2,6 +2,7 @@ use crate::*;
 use anyhow::{anyhow, bail};
 use indexmap::IndexSet;
 use std::mem;
+use std::slice;
 use std::{collections::HashMap, io::Read};
 use wasmparser::Chunk;
 use wasmparser::{
@@ -344,12 +345,12 @@ impl ComponentInfo {
 
 /// Result of the [`decode`] function.
 pub enum DecodedWasm {
-    /// The input to [`decode`] was a binary-encoded WIT package.
+    /// The input to [`decode`] was one or more binary-encoded WIT package(s).
     ///
-    /// The full resolve graph is here plus the identifier of the package that
-    /// was encoded. Note that other packages may be within the resolve if this
-    /// package refers to foreign packages.
-    WitPackage(Resolve, PackageId),
+    /// The full resolve graph is here plus the identifier of the packages that
+    /// were encoded. Note that other packages may be within the resolve if any
+    /// of the main packages refer to other, foreign packages.
+    WitPackages(Resolve, Vec<PackageId>),
 
     /// The input to [`decode`] was a component and its interface is specified
     /// by the world here.
@@ -360,16 +361,18 @@ impl DecodedWasm {
     /// Returns the [`Resolve`] for WIT types contained.
     pub fn resolve(&self) -> &Resolve {
         match self {
-            DecodedWasm::WitPackage(resolve, _) => resolve,
+            DecodedWasm::WitPackages(resolve, _) => resolve,
             DecodedWasm::Component(resolve, _) => resolve,
         }
     }
 
-    /// Returns the main package of what was decoded.
-    pub fn package(&self) -> PackageId {
+    /// Returns the main packages of what was decoded.
+    pub fn packages(&self) -> &[PackageId] {
         match self {
-            DecodedWasm::WitPackage(_, id) => *id,
-            DecodedWasm::Component(resolve, world) => resolve.worlds[*world].package.unwrap(),
+            DecodedWasm::WitPackages(_, ids) => ids,
+            DecodedWasm::Component(resolve, world) => {
+                slice::from_ref(&resolve.worlds[*world].package.as_ref().unwrap())
+            }
         }
     }
 }
@@ -383,12 +386,12 @@ pub fn decode_reader(reader: impl Read) -> Result<DecodedWasm> {
             WitEncodingVersion::V1 => {
                 log::debug!("decoding a v1 WIT package encoded as wasm");
                 let (resolve, pkg) = info.decode_wit_v1_package()?;
-                Ok(DecodedWasm::WitPackage(resolve, pkg))
+                Ok(DecodedWasm::WitPackages(resolve, vec![pkg]))
             }
             WitEncodingVersion::V2 => {
                 log::debug!("decoding a v2 WIT package encoded as wasm");
                 let (resolve, pkg) = info.decode_wit_v2_package()?;
-                Ok(DecodedWasm::WitPackage(resolve, pkg))
+                Ok(DecodedWasm::WitPackages(resolve, vec![pkg]))
             }
         }
     } else {
diff --git a/crates/wit-parser/src/lib.rs b/crates/wit-parser/src/lib.rs
index a9f54bd1..e227f7fc 100644
--- a/crates/wit-parser/src/lib.rs
+++ b/crates/wit-parser/src/lib.rs
@@ -17,6 +17,7 @@ pub mod abi;
 mod ast;
 use ast::lex::Span;
 pub use ast::SourceMap;
+pub use ast::{parse_use_path, ParsedUsePath};
 mod sizealign;
 pub use sizealign::*;
 mod resolve;
@@ -110,10 +111,19 @@ pub struct UnresolvedPackage {
     world_spans: Vec<WorldSpan>,
     type_spans: Vec<Span>,
     foreign_dep_spans: Vec<Span>,
-    source_map: SourceMap,
     required_resource_types: Vec<(TypeId, Span)>,
 }
 
+/// Tracks a set of packages, all pulled from the same group of WIT source files.
+#[derive(Default)]
+pub struct UnresolvedPackageGroup {
+    /// A set of packages that share source file(s).
+    pub packages: Vec<UnresolvedPackage>,
+
+    /// A set of processed source files from which these packages have been parsed.
+    pub source_map: SourceMap,
+}
+
 #[derive(Clone)]
 struct WorldSpan {
     span: Span,
@@ -209,12 +219,17 @@ impl fmt::Display for Error {
 
 impl std::error::Error for Error {}
 
-impl UnresolvedPackage {
+impl UnresolvedPackageGroup {
+    /// Creates an empty set of packages.
+    pub fn new() -> UnresolvedPackageGroup {
+        UnresolvedPackageGroup::default()
+    }
+
     /// Parses the given string as a wit document.
     ///
     /// The `path` argument is used for error reporting. The `contents` provided
     /// will not be able to use `pkg` use paths to other documents.
-    pub fn parse(path: &Path, contents: &str) -> Result<Self> {
+    pub fn parse(path: &Path, contents: &str) -> Result<UnresolvedPackageGroup> {
         let mut map = SourceMap::default();
         map.push(path, contents);
         map.parse()
@@ -223,13 +238,13 @@ impl UnresolvedPackage {
     /// Parse a WIT package at the provided path.
     ///
     /// The path provided is inferred whether it's a file or a directory. A file
-    /// is parsed with [`UnresolvedPackage::parse_file`] and a directory is
-    /// parsed with [`UnresolvedPackage::parse_dir`].
-    pub fn parse_path(path: &Path) -> Result<Self> {
+    /// is parsed with [`UnresolvedPackageGroup::parse_file`] and a directory is
+    /// parsed with [`UnresolvedPackageGroup::parse_dir`].
+    pub fn parse_path(path: &Path) -> Result<UnresolvedPackageGroup> {
         if path.is_dir() {
-            UnresolvedPackage::parse_dir(path)
+            UnresolvedPackageGroup::parse_dir(path)
         } else {
-            UnresolvedPackage::parse_file(path)
+            UnresolvedPackageGroup::parse_file(path)
         }
     }
 
@@ -237,7 +252,7 @@ impl UnresolvedPackage {
     ///
     /// The WIT package returned will be a single-document package and will not
     /// be able to use `pkg` paths to other documents.
-    pub fn parse_file(path: &Path) -> Result<Self> {
+    pub fn parse_file(path: &Path) -> Result<UnresolvedPackageGroup> {
         let contents = std::fs::read_to_string(path)
             .with_context(|| format!("failed to read file {path:?}"))?;
         Self::parse(path, &contents)
@@ -247,7 +262,7 @@ impl UnresolvedPackage {
     ///
     /// All files with the extension `*.wit` or `*.wit.md` will be loaded from
     /// `path` into the returned package.
-    pub fn parse_dir(path: &Path) -> Result<Self> {
+    pub fn parse_dir(path: &Path) -> Result<UnresolvedPackageGroup> {
         let mut map = SourceMap::default();
         let cx = || format!("failed to read directory {path:?}");
         for entry in path.read_dir().with_context(&cx)? {
@@ -273,12 +288,6 @@ impl UnresolvedPackage {
         }
         map.parse()
     }
-
-    /// Returns an iterator over the list of source files that were read when
-    /// parsing this package.
-    pub fn source_files(&self) -> impl Iterator<Item = &Path> {
-        self.source_map.source_files()
-    }
 }
 
 #[derive(Debug, Clone)]
diff --git a/crates/wit-parser/src/resolve.rs b/crates/wit-parser/src/resolve.rs
index fa294f98..8a34a8e6 100644
--- a/crates/wit-parser/src/resolve.rs
+++ b/crates/wit-parser/src/resolve.rs
@@ -1,11 +1,12 @@
 use crate::ast::lex::Span;
-use crate::ast::{parse_use_path, AstUsePath};
+use crate::ast::{parse_use_path, ParsedUsePath};
 #[cfg(feature = "serde")]
 use crate::serde_::{serialize_arena, serialize_id_map};
 use crate::{
     AstItem, Docs, Error, Function, FunctionKind, Handle, IncludeName, Interface, InterfaceId,
-    InterfaceSpan, PackageName, Results, Stability, Type, TypeDef, TypeDefKind, TypeId, TypeOwner,
-    UnresolvedPackage, World, WorldId, WorldItem, WorldKey, WorldSpan,
+    InterfaceSpan, PackageName, Results, SourceMap, Stability, Type, TypeDef, TypeDefKind, TypeId,
+    TypeOwner, UnresolvedPackage, UnresolvedPackageGroup, World, WorldId, WorldItem, WorldKey,
+    WorldSpan,
 };
 use anyhow::{anyhow, bail, Context, Result};
 use id_arena::{Arena, Id};
@@ -105,7 +106,42 @@ pub type PackageId = Id<Package>;
 enum ParsedFile {
     #[cfg(feature = "decoding")]
     Package(PackageId),
-    Unresolved(UnresolvedPackage),
+    Unresolved(UnresolvedPackageGroup),
+}
+
+/// Visitor helper for performing topological sort on a group of packages.
+fn visit<'a>(
+    pkg: &'a UnresolvedPackage,
+    pkg_details_map: &'a BTreeMap<PackageName, (UnresolvedPackage, usize)>,
+    order: &mut IndexSet<PackageName>,
+    visiting: &mut HashSet<&'a PackageName>,
+    source_maps: &[SourceMap],
+) -> Result<()> {
+    if order.contains(&pkg.name) {
+        return Ok(());
+    }
+
+    match pkg_details_map.get(&pkg.name) {
+        Some(pkg_details) => {
+            let (_, source_maps_index) = pkg_details;
+            source_maps[*source_maps_index].rewrite_error(|| {
+                for (i, (dep, _)) in pkg.foreign_deps.iter().enumerate() {
+                    let span = pkg.foreign_dep_spans[i];
+                    if !visiting.insert(dep) {
+                        bail!(Error::new(span, "package depends on itself"));
+                    }
+                    if let Some(dep) = pkg_details_map.get(dep) {
+                        let (dep_pkg, _) = dep;
+                        visit(dep_pkg, pkg_details_map, order, visiting, source_maps)?;
+                    }
+                    assert!(visiting.remove(dep));
+                }
+                assert!(order.insert(pkg.name.clone()));
+                Ok(())
+            })
+        }
+        None => panic!("No pkg_details found for package when doing topological sort"),
+    }
 }
 
 impl Resolve {
@@ -131,11 +167,11 @@ impl Resolve {
     ///
     /// Returns the top-level [`PackageId`] as well as a list of all files read
     /// during this parse.
-    pub fn push_path(&mut self, path: impl AsRef<Path>) -> Result<(PackageId, Vec<PathBuf>)> {
+    pub fn push_path(&mut self, path: impl AsRef<Path>) -> Result<(Vec<PackageId>, Vec<PathBuf>)> {
         self._push_path(path.as_ref())
     }
 
-    fn _push_path(&mut self, path: &Path) -> Result<(PackageId, Vec<PathBuf>)> {
+    fn _push_path(&mut self, path: &Path) -> Result<(Vec<PackageId>, Vec<PathBuf>)> {
         if path.is_dir() {
             self.push_dir(path).with_context(|| {
                 format!(
@@ -144,15 +180,72 @@ impl Resolve {
                 )
             })
         } else {
-            let id = self.push_file(path)?;
-            Ok((id, vec![path.to_path_buf()]))
+            let ids = self.push_file(path)?;
+            Ok((ids, vec![path.to_path_buf()]))
+        }
+    }
+
+    fn sort_unresolved_packages(
+        &mut self,
+        unresolved_groups: Vec<UnresolvedPackageGroup>,
+    ) -> Result<(Vec<PackageId>, Vec<PathBuf>)> {
+        let mut pkg_ids = Vec::new();
+        let mut path_bufs = Vec::new();
+        let mut pkg_details_map = BTreeMap::new();
+        let mut source_maps = Vec::new();
+        for (i, unresolved_group) in unresolved_groups.into_iter().enumerate() {
+            let UnresolvedPackageGroup {
+                packages,
+                source_map,
+            } = unresolved_group;
+
+            source_maps.push(source_map);
+            for pkg in packages {
+                pkg_details_map.insert(pkg.name.clone(), (pkg, i));
+            }
+        }
+
+        // Perform a simple topological sort which will bail out on cycles
+        // and otherwise determine the order that packages must be added to
+        // this `Resolve`.
+        let mut order = IndexSet::new();
+        let mut visiting = HashSet::new();
+        for pkg_details in pkg_details_map.values() {
+            let (pkg, _) = pkg_details;
+            visit(
+                pkg,
+                &pkg_details_map,
+                &mut order,
+                &mut visiting,
+                &source_maps,
+            )?;
+        }
+
+        // Ensure that the final output is topologically sorted. Use a set to ensure that we render
+        // the buffers for each `SourceMap` only once, even though multiple packages may references
+        // the same `SourceMap`.
+        let mut seen_source_maps = HashSet::new();
+        for name in order {
+            match pkg_details_map.remove(&name) {
+                Some((pkg, source_map_index)) => {
+                    let source_map = &source_maps[source_map_index];
+                    if !seen_source_maps.contains(&source_map_index) {
+                        seen_source_maps.insert(source_map_index);
+                        path_bufs.extend(source_map.source_files().map(|p| p.to_path_buf()));
+                    }
+                    pkg_ids.push(self.push(pkg, source_map)?);
+                }
+                None => panic!("package in topologically ordered set, but not in package map"),
+            }
         }
+
+        Ok((pkg_ids, path_bufs))
     }
 
     /// Parses the filesystem directory at `path` as a WIT package and returns
     /// the fully resolved [`PackageId`] as a result.
     ///
-    /// The directory itself is parsed with [`UnresolvedPackage::parse_dir`]
+    /// The directory itself is parsed with [`UnresolvedPackageGroup::parse_dir`]
     /// which has more information on the layout of the directory. This method,
     /// however, additionally supports an optional `deps` dir where dependencies
     /// can be located.
@@ -160,79 +253,39 @@ impl Resolve {
     /// All entries in the `deps` directory are inspected and parsed as follows:
     ///
     /// * Any directories inside of `deps` are assumed to be another WIT package
-    ///   and are parsed with [`UnresolvedPackage::parse_dir`].
-    /// * WIT files (`*.wit`) are parsed with [`UnresolvedPackage::parse_file`].
+    ///   and are parsed with [`UnresolvedPackageGroup::parse_dir`].
+    /// * WIT files (`*.wit`) are parsed with [`UnresolvedPackageGroup::parse_file`].
     /// * WebAssembly files (`*.wasm` or `*.wat`) are assumed to be WIT packages
     ///   encoded to wasm and are parsed and inserted into `self`.
     ///
-    /// This function returns the [`PackageId`] of the root parsed package at
+    /// This function returns the [`PackageId`]s of the root parsed packages at
     /// `path`, along with a list of all paths that were consumed during parsing
-    /// for the root package and all dependency packages.
-    pub fn push_dir(&mut self, path: &Path) -> Result<(PackageId, Vec<PathBuf>)> {
-        let pkg = UnresolvedPackage::parse_dir(path)
+    /// for the root package and all dependency packages, for each package encountered.
+    pub fn push_dir(&mut self, path: &Path) -> Result<(Vec<PackageId>, Vec<PathBuf>)> {
+        let deps_path = path.join("deps");
+        let unresolved_deps = self.parse_deps_dir(&deps_path).with_context(|| {
+            format!(
+                "failed to parse dependency directory: {}",
+                deps_path.display()
+            )
+        })?;
+        let (_, mut path_bufs) = self.sort_unresolved_packages(unresolved_deps)?;
+
+        let unresolved_top_level = UnresolvedPackageGroup::parse_dir(path)
             .with_context(|| format!("failed to parse package: {}", path.display()))?;
+        let (pkgs_ids, mut top_level_path_bufs) =
+            self.sort_unresolved_packages(vec![unresolved_top_level])?;
 
-        let deps = path.join("deps");
-        let mut deps = self
-            .parse_deps_dir(&deps)
-            .with_context(|| format!("failed to parse dependency directory: {}", deps.display()))?;
-
-        // Perform a simple topological sort which will bail out on cycles
-        // and otherwise determine the order that packages must be added to
-        // this `Resolve`.
-        let mut order = IndexSet::new();
-        let mut visiting = HashSet::new();
-        for pkg in deps.values().chain([&pkg]) {
-            visit(&pkg, &deps, &mut order, &mut visiting)?;
-        }
-
-        // Using the topological ordering insert each package incrementally.
-        // Additionally note that the last item visited here is the root
-        // package, which is the one returned here.
-        let mut last = None;
-        let mut files = Vec::new();
-        let mut pkg = Some(pkg);
-        for name in order {
-            let pkg = deps.remove(&name).unwrap_or_else(|| pkg.take().unwrap());
-            files.extend(pkg.source_files().map(|p| p.to_path_buf()));
-            let pkgid = self.push(pkg)?;
-            last = Some(pkgid);
-        }
-
-        return Ok((last.unwrap(), files));
-
-        fn visit<'a>(
-            pkg: &'a UnresolvedPackage,
-            deps: &'a BTreeMap<PackageName, UnresolvedPackage>,
-            order: &mut IndexSet<PackageName>,
-            visiting: &mut HashSet<&'a PackageName>,
-        ) -> Result<()> {
-            if order.contains(&pkg.name) {
-                return Ok(());
-            }
-            pkg.source_map.rewrite_error(|| {
-                for (i, (dep, _)) in pkg.foreign_deps.iter().enumerate() {
-                    let span = pkg.foreign_dep_spans[i];
-                    if !visiting.insert(dep) {
-                        bail!(Error::new(span, "package depends on itself"));
-                    }
-                    if let Some(dep) = deps.get(dep) {
-                        visit(dep, deps, order, visiting)?;
-                    }
-                    assert!(visiting.remove(dep));
-                }
-                assert!(order.insert(pkg.name.clone()));
-                Ok(())
-            })
-        }
+        path_bufs.append(&mut top_level_path_bufs);
+        Ok((pkgs_ids, path_bufs))
     }
 
-    fn parse_deps_dir(&mut self, path: &Path) -> Result<BTreeMap<PackageName, UnresolvedPackage>> {
-        let mut ret = BTreeMap::new();
+    fn parse_deps_dir(&mut self, path: &Path) -> Result<Vec<UnresolvedPackageGroup>> {
+        let mut unresolved_deps = Vec::new();
         // If there's no `deps` dir, then there's no deps, so return the
         // empty set.
         if !path.exists() {
-            return Ok(ret);
+            return Ok(unresolved_deps);
         }
         let mut entries = path
             .read_dir()
@@ -241,38 +294,37 @@ impl Resolve {
         entries.sort_by_key(|e| e.file_name());
         for dep in entries {
             let path = dep.path();
-
-            let pkg = if dep.file_type()?.is_dir() || path.metadata()?.is_dir() {
+            if dep.file_type()?.is_dir() || path.metadata()?.is_dir() {
                 // If this entry is a directory or a symlink point to a
                 // directory then always parse it as an `UnresolvedPackage`
                 // since it's intentional to not support recursive `deps`
                 // directories.
-                UnresolvedPackage::parse_dir(&path)
-                    .with_context(|| format!("failed to parse package: {}", path.display()))?
+                unresolved_deps.push(
+                    UnresolvedPackageGroup::parse_dir(&path)
+                        .with_context(|| format!("failed to parse package: {}", path.display()))?,
+                )
             } else {
                 // If this entry is a file then we may want to ignore it but
                 // this may also be a standalone WIT file or a `*.wasm` or
                 // `*.wat` encoded package.
                 let filename = dep.file_name();
-                match Path::new(&filename).extension().and_then(|s| s.to_str()) {
-                    Some("wit") | Some("wat") | Some("wasm") => match self._push_file(&path)? {
-                        #[cfg(feature = "decoding")]
-                        ParsedFile::Package(_) => continue,
-                        ParsedFile::Unresolved(pkg) => pkg,
+                unresolved_deps.push(
+                    match Path::new(&filename).extension().and_then(|s| s.to_str()) {
+                        Some("wit") | Some("wat") | Some("wasm") => match self._push_file(&path)? {
+                            #[cfg(feature = "decoding")]
+                            ParsedFile::Package(_) => continue,
+                            ParsedFile::Unresolved(pkgs) => pkgs,
+                        },
+
+                        // Other files in deps dir are ignored for now to avoid
+                        // accidentally including things like `.DS_Store` files in
+                        // the call below to `parse_dir`.
+                        _ => continue,
                     },
-
-                    // Other files in deps dir are ignored for now to avoid
-                    // accidentally including things like `.DS_Store` files in
-                    // the call below to `parse_dir`.
-                    _ => continue,
-                }
+                )
             };
-            let prev = ret.insert(pkg.name.clone(), pkg);
-            if let Some(prev) = prev {
-                bail!("duplicate definitions of package `{}` found", prev.name);
-            }
         }
-        Ok(ret)
+        Ok(unresolved_deps)
     }
 
     /// Parses the contents of `path` from the filesystem and pushes the result
@@ -288,11 +340,11 @@ impl Resolve {
     ///
     /// In both situations the `PackageId` of the resulting resolved package is
     /// returned from this method.
-    pub fn push_file(&mut self, path: impl AsRef<Path>) -> Result<PackageId> {
+    pub fn push_file(&mut self, path: impl AsRef<Path>) -> Result<Vec<PackageId>> {
         match self._push_file(path.as_ref())? {
             #[cfg(feature = "decoding")]
-            ParsedFile::Package(id) => Ok(id),
-            ParsedFile::Unresolved(pkg) => self.push(pkg),
+            ParsedFile::Package(id) => Ok(vec![id]),
+            ParsedFile::Unresolved(pkgs) => self.append(pkgs),
         }
     }
 
@@ -322,9 +374,9 @@ impl Resolve {
                     DecodedWasm::Component(..) => {
                         bail!("found an actual component instead of an encoded WIT package in wasm")
                     }
-                    DecodedWasm::WitPackage(resolve, pkg) => {
+                    DecodedWasm::WitPackages(resolve, pkgs) => {
                         let remap = self.merge(resolve)?;
-                        return Ok(ParsedFile::Package(remap.packages[pkg.index()]));
+                        return Ok(ParsedFile::Package(remap.packages[pkgs[0].index()]));
                     }
                 }
             }
@@ -335,8 +387,8 @@ impl Resolve {
             Ok(s) => s,
             Err(_) => bail!("input file is not valid utf-8 [{}]", path.display()),
         };
-        let pkg = UnresolvedPackage::parse(path, text)?;
-        Ok(ParsedFile::Unresolved(pkg))
+        let pkgs = UnresolvedPackageGroup::parse(path, text)?;
+        Ok(ParsedFile::Unresolved(pkgs))
     }
 
     /// Appends a new [`UnresolvedPackage`] to this [`Resolve`], creating a
@@ -347,13 +399,32 @@ impl Resolve {
     /// as [`Resolve::push_path`].
     ///
     /// Any dependency resolution error or otherwise world-elaboration error
-    /// will be returned here. If successful a package identifier is returned
+    /// will be returned here, if successful a package identifier is returned
     /// which corresponds to the package that was just inserted.
-    pub fn push(&mut self, mut unresolved: UnresolvedPackage) -> Result<PackageId> {
-        let source_map = mem::take(&mut unresolved.source_map);
+    pub fn push(
+        &mut self,
+        unresolved: UnresolvedPackage,
+        source_map: &SourceMap,
+    ) -> Result<PackageId> {
         source_map.rewrite_error(|| Remap::default().append(self, unresolved))
     }
 
+    /// Appends new [`UnresolvedPackageSet`] to this [`Resolve`], creating a
+    /// fully resolved package with no dangling references.
+    ///
+    /// The `deps` argument indicates that the named dependencies in
+    /// `unresolved` to packages are resolved by the mapping specified.
+    ///
+    /// Any dependency resolution error or otherwise world-elaboration error
+    /// will be returned here, if successful a package identifier is returned
+    /// which corresponds to the package that was just inserted.
+    ///
+    /// The returned [PackageId]s are listed in topologically sorted order.
+    pub fn append(&mut self, unresolved_groups: UnresolvedPackageGroup) -> Result<Vec<PackageId>> {
+        let (pkg_ids, _) = self.sort_unresolved_packages(vec![unresolved_groups])?;
+        Ok(pkg_ids)
+    }
+
     pub fn all_bits_valid(&self, ty: &Type) -> bool {
         match ty {
             Type::U8
@@ -454,6 +525,7 @@ impl Resolve {
             packages,
             package_names,
             features: _,
+            ..
         } = resolve;
 
         let mut moved_types = Vec::new();
@@ -760,8 +832,8 @@ impl Resolve {
         let path = parse_use_path(world)
             .with_context(|| format!("failed to parse world specifier `{world}`"))?;
         let (pkg, world) = match path {
-            AstUsePath::Name(name) => (pkg, name),
-            AstUsePath::Package(pkg, interface) => {
+            ParsedUsePath::Name(name) => (pkg, name),
+            ParsedUsePath::Package(pkg, interface) => {
                 let pkg = match self.package_names.get(&pkg) {
                     Some(pkg) => *pkg,
                     None => {
@@ -2492,7 +2564,7 @@ mod tests {
     }
 
     fn parse_into(resolve: &mut Resolve, wit: &str) -> PackageId {
-        let pkg = crate::UnresolvedPackage::parse("input.wit".as_ref(), wit).unwrap();
-        resolve.push(pkg).unwrap()
+        let pkgs = crate::UnresolvedPackageGroup::parse("input.wit".as_ref(), wit).unwrap();
+        resolve.append(pkgs).unwrap()[0]
     }
 }
diff --git a/crates/wit-smith/src/lib.rs b/crates/wit-smith/src/lib.rs
index 36410ada..3a70a00f 100644
--- a/crates/wit-smith/src/lib.rs
+++ b/crates/wit-smith/src/lib.rs
@@ -6,7 +6,7 @@
 //! type structures.
 
 use arbitrary::{Result, Unstructured};
-use wit_parser::Resolve;
+use wit_parser::{Resolve, UnresolvedPackageGroup};
 
 mod config;
 pub use self::config::Config;
@@ -21,8 +21,11 @@ pub fn smith(config: &Config, u: &mut Unstructured<'_>) -> Result<Vec<u8>> {
     let mut resolve = Resolve::default();
     let mut last = None;
     for pkg in pkgs {
-        let unresolved = pkg.sources.parse().unwrap();
-        let id = match resolve.push(unresolved) {
+        let UnresolvedPackageGroup {
+            mut packages,
+            source_map,
+        } = pkg.sources.parse().unwrap();
+        let id = match resolve.push(packages.remove(0), &source_map) {
             Ok(id) => id,
             Err(e) => {
                 if e.to_string().contains(
diff --git a/fuzz/src/roundtrip_wit.rs b/fuzz/src/roundtrip_wit.rs
index d5e91c58..19967992 100644
--- a/fuzz/src/roundtrip_wit.rs
+++ b/fuzz/src/roundtrip_wit.rs
@@ -11,17 +11,23 @@ pub fn run(u: &mut Unstructured<'_>) -> Result<()> {
     })?;
     write_file("doc1.wasm", &wasm);
     let (resolve, _pkg) = match wit_component::decode(&wasm).unwrap() {
-        DecodedWasm::WitPackage(resolve, pkg) => (resolve, pkg),
+        DecodedWasm::WitPackages(resolve, pkg) => (resolve, pkg),
         DecodedWasm::Component(..) => unreachable!(),
     };
 
     roundtrip_through_printing("doc1", &resolve, &wasm);
 
-    let (resolve2, pkg2) = match wit_component::decode(&wasm).unwrap() {
-        DecodedWasm::WitPackage(resolve, pkg) => (resolve, pkg),
+    let (resolve2, pkgs2) = match wit_component::decode(&wasm).unwrap() {
+        DecodedWasm::WitPackages(resolve, pkgs) => (resolve, pkgs),
         DecodedWasm::Component(..) => unreachable!(),
     };
 
+    // wit_smith returns WIT source with only a single package.
+    if pkgs2.len() != 1 {
+        panic!("rountrip WIT test smithed file with multiple packages")
+    }
+
+    let pkg2 = pkgs2[0];
     let wasm2 =
         wit_component::encode(Some(true), &resolve2, pkg2).expect("failed to encode WIT document");
     write_file("doc2.wasm", &wasm2);
@@ -80,12 +86,12 @@ fn roundtrip_through_printing(file: &str, resolve: &Resolve, wasm: &[u8]) {
     for (id, pkg) in resolve.packages.iter() {
         let mut map = SourceMap::new();
         let pkg_name = &pkg.name;
-        let doc = WitPrinter::default().print(resolve, id).unwrap();
+        let doc = WitPrinter::default().print(resolve, &[id]).unwrap();
         write_file(&format!("{file}-{pkg_name}.wit"), &doc);
         map.push(format!("{pkg_name}.wit").as_ref(), doc);
         let unresolved = map.parse().unwrap();
-        let id = new_resolve.push(unresolved).unwrap();
-        last = Some(id);
+        let id = new_resolve.append(unresolved).unwrap();
+        last = Some(id.last().unwrap().to_owned());
     }
 
     // Finally encode the `new_resolve` which should be the exact same as
diff --git a/src/bin/wasm-tools/component.rs b/src/bin/wasm-tools/component.rs
index 366d325f..4f3e8d48 100644
--- a/src/bin/wasm-tools/component.rs
+++ b/src/bin/wasm-tools/component.rs
@@ -11,9 +11,10 @@ use wasm_tools::Output;
 use wasmparser::WasmFeatures;
 use wat::Detect;
 use wit_component::{
-    embed_component_metadata, ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter,
+    embed_component_metadata, resolve_world_from_name, ComponentEncoder, DecodedWasm, Linker,
+    StringEncoding, WitPrinter,
 };
-use wit_parser::{PackageId, Resolve, UnresolvedPackage};
+use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup};
 
 /// WebAssembly wit-based component tooling.
 #[derive(Parser)]
@@ -199,10 +200,10 @@ impl WitResolve {
         return resolve;
     }
 
-    fn load(&self) -> Result<(Resolve, PackageId)> {
+    fn load(&self) -> Result<(Resolve, Vec<PackageId>)> {
         let mut resolve = Self::resolve_with_features(&self.features);
-        let id = resolve.push_path(&self.wit)?.0;
-        Ok((resolve, id))
+        let (pkg_ids, _) = resolve.push_path(&self.wit)?;
+        Ok((resolve, pkg_ids))
     }
 }
 
@@ -232,18 +233,21 @@ pub struct EmbedOpts {
     /// The expected string encoding format for the component.
     ///
     /// Supported values are: `utf8` (default), `utf16`, and `compact-utf16`.
-    /// This is only applicable to the `--wit` argument to describe the string
+    /// This is only applicable to the `wit` argument to describe the string
     /// encoding of the functions in that world.
     #[clap(long, value_name = "ENCODING")]
     encoding: Option<StringEncoding>,
 
     /// The world that the component uses.
     ///
-    /// This is the path, within the `WIT` package provided as a positional
-    /// argument, to the `world` that the core wasm module works with. This can
-    /// either be a bare string which a document name that has a `default
-    /// world`, or it can be a `foo/bar` name where `foo` names a document and
-    /// `bar` names a world within that document.
+    /// This is the path, within the `WIT` source provided as a positional
+    /// argument, to the `world` that the core wasm module works with. If this
+    /// option is omitted then the "main package" pointed to by `WIT` must have
+    /// a single world and that's what is used to embed. Otherwise this could be
+    /// a bare string `foo` to point to the `world foo` within the main
+    /// package of WIT. Finally this can be a fully qualified name too such as
+    /// `wasi:http/proxy` which can select a world from a WIT dependency as
+    /// well.
     #[clap(short, long)]
     world: Option<String>,
 
@@ -275,9 +279,8 @@ impl EmbedOpts {
         } else {
             Some(self.io.parse_input_wasm()?)
         };
-        let (resolve, id) = self.resolve.load()?;
-        let world = resolve.select_world(id, self.world.as_deref())?;
-
+        let (resolve, pkg_ids) = self.resolve.load()?;
+        let world = resolve_world_from_name(&resolve, pkg_ids, self.world.as_deref())?;
         let mut wasm = wasm.unwrap_or_else(|| wit_component::dummy_module(&resolve, world));
 
         embed_component_metadata(
@@ -531,8 +534,8 @@ impl WitOpts {
         if let Some(input) = &self.input {
             if input.is_dir() {
                 let mut resolve = WitResolve::resolve_with_features(&self.features);
-                let id = resolve.push_dir(&input)?.0;
-                return Ok(DecodedWasm::WitPackage(resolve, id));
+                let (pkg_ids, _) = resolve.push_dir(&input)?;
+                return Ok(DecodedWasm::WitPackages(resolve, pkg_ids));
             }
         }
 
@@ -579,9 +582,9 @@ impl WitOpts {
                     Err(_) => bail!("input was not valid utf-8"),
                 };
                 let mut resolve = WitResolve::resolve_with_features(&self.features);
-                let pkg = UnresolvedPackage::parse(path, input)?;
-                let id = resolve.push(pkg)?;
-                Ok(DecodedWasm::WitPackage(resolve, id))
+                let pkgs = UnresolvedPackageGroup::parse(path, input)?;
+                let ids = resolve.append(pkgs)?;
+                Ok(DecodedWasm::WitPackages(resolve, ids))
             }
         }
     }
@@ -589,8 +592,12 @@ impl WitOpts {
     fn emit_wasm(&self, decoded: &DecodedWasm) -> Result<()> {
         assert!(self.wasm || self.wat);
         assert!(self.out_dir.is_none());
+        if decoded.packages().len() != 1 {
+            bail!("emitting WASM for multi-package WIT files is not yet supported")
+        }
 
-        let bytes = wit_component::encode(None, decoded.resolve(), decoded.package())?;
+        let decoded_package = decoded.packages()[0];
+        let bytes = wit_component::encode(None, decoded.resolve(), decoded_package)?;
         if !self.skip_validation {
             wasmparser::Validator::new_with_features(
                 WasmFeatures::default() | WasmFeatures::COMPONENT_MODEL,
@@ -608,7 +615,7 @@ impl WitOpts {
         assert!(!self.wasm && !self.wat);
 
         let resolve = decoded.resolve();
-        let main = decoded.package();
+        let main = decoded.packages();
 
         let mut printer = WitPrinter::default();
         printer.emit_docs(!self.no_docs);
@@ -632,8 +639,8 @@ impl WitOpts {
                 }
 
                 for (id, pkg) in resolve.packages.iter() {
-                    let output = printer.print(resolve, id)?;
-                    let out_dir = if id == main {
+                    let output = printer.print(resolve, &[id])?;
+                    let out_dir = if main.contains(&id) {
                         dir.clone()
                     } else {
                         let dir = dir.join("deps");
@@ -659,7 +666,7 @@ impl WitOpts {
                 }
             }
             None => {
-                let output = printer.print(resolve, main)?;
+                let output = printer.print(resolve, &main)?;
                 self.output.output(Output::Wat(&output))?;
             }
         }
@@ -687,7 +694,9 @@ pub struct TargetsOpts {
     #[clap(flatten)]
     resolve: WitResolve,
 
-    /// The world used to test whether a component conforms to its signature.
+    /// The world used to test whether a component conforms to its signature. If the `WIT` source
+    /// provided contains multiple packages, this option must be set, and must be of the
+    /// fully-qualified form (ex: "wasi:http/proxy")
     #[clap(short, long)]
     world: Option<String>,
 
@@ -702,8 +711,8 @@ impl TargetsOpts {
 
     /// Executes the application.
     fn run(self) -> Result<()> {
-        let (resolve, package_id) = self.resolve.load()?;
-        let world = resolve.select_world(package_id, self.world.as_deref())?;
+        let (resolve, pkg_ids) = self.resolve.load()?;
+        let world = resolve_world_from_name(&resolve, pkg_ids, self.world.as_deref())?;
         let component_to_test = self.input.parse_wasm()?;
 
         wit_component::targets(&resolve, world, &component_to_test)?;
@@ -742,9 +751,9 @@ impl SemverCheckOpts {
     }
 
     fn run(self) -> Result<()> {
-        let (resolve, package_id) = self.resolve.load()?;
-        let prev = resolve.select_world(package_id, Some(&self.prev))?;
-        let new = resolve.select_world(package_id, Some(&self.new))?;
+        let (resolve, pkg_ids) = self.resolve.load()?;
+        let prev = resolve_world_from_name(&resolve, pkg_ids.clone(), Some(self.prev).as_deref())?;
+        let new = resolve_world_from_name(&resolve, pkg_ids, Some(self.new).as_deref())?;
         wit_component::semver_check(resolve, prev, new)?;
         Ok(())
     }
diff --git a/src/bin/wasm-tools/print.rs b/src/bin/wasm-tools/print.rs
index 947a18db..041ceaa5 100644
--- a/src/bin/wasm-tools/print.rs
+++ b/src/bin/wasm-tools/print.rs
@@ -34,11 +34,12 @@ impl Opts {
 
     pub fn run(&self) -> Result<()> {
         let wasm = self.io.parse_input_wasm()?;
-        let mut printer = wasmprinter::Printer::new();
+        let mut printer = wasmprinter::Config::new();
         printer.print_offsets(self.print_offsets);
         printer.print_skeleton(self.skeleton);
         printer.name_unnamed(self.name_unnamed);
-        let wat = printer.print(&wasm)?;
+        let mut wat = String::new();
+        printer.print(&wasm, &mut wat)?;
         self.io.output(wasm_tools::Output::Wat(&wat))?;
         Ok(())
     }


Test patch:
diff --git a/crates/wasmprinter/tests/all.rs b/crates/wasmprinter/tests/all.rs
index 4985aa82..3d96f9c3 100644
--- a/crates/wasmprinter/tests/all.rs
+++ b/crates/wasmprinter/tests/all.rs
@@ -248,8 +248,12 @@ fn offsets_and_lines_smoke_test() {
     "#;
     let bytes = wat::parse_str(MODULE).unwrap();
 
-    let mut printer = wasmprinter::Printer::new();
-    let actual: Vec<_> = printer.offsets_and_lines(&bytes).unwrap().collect();
+    let mut storage = String::new();
+    let printer = wasmprinter::Config::new();
+    let actual: Vec<_> = printer
+        .offsets_and_lines(&bytes, &mut storage)
+        .unwrap()
+        .collect();
 
     #[rustfmt::skip]
     let expected = vec![
diff --git a/crates/wit-component/tests/components.rs b/crates/wit-component/tests/components.rs
index 91f3051b..87917cce 100644
--- a/crates/wit-component/tests/components.rs
+++ b/crates/wit-component/tests/components.rs
@@ -4,7 +4,7 @@ use pretty_assertions::assert_eq;
 use std::{borrow::Cow, fs, path::Path};
 use wasm_encoder::{Encode, Section};
 use wit_component::{ComponentEncoder, DecodedWasm, Linker, StringEncoding, WitPrinter};
-use wit_parser::{PackageId, Resolve, UnresolvedPackage};
+use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup};
 
 /// Tests the encoding of components.
 ///
@@ -75,126 +75,141 @@ fn main() -> Result<()> {
 
 fn run_test(path: &Path) -> Result<()> {
     let test_case = path.file_stem().unwrap().to_str().unwrap();
-
     let mut resolve = Resolve::default();
-    let (pkg, _) = resolve
-        .push_dir(&path)
-        .context("failed to push directory into resolve")?;
+    let (pkg_ids, _) = resolve.push_dir(&path)?;
+    let pkg_count = pkg_ids.len();
 
-    let module_path = path.join("module.wat");
-    let mut adapters = glob::glob(path.join("adapt-*.wat").to_str().unwrap())?;
-    let result = if module_path.is_file() {
-        let module = read_core_module(&module_path, &resolve, pkg)
-            .with_context(|| format!("failed to read core module at {module_path:?}"))?;
-        adapters
-            .try_fold(
-                ComponentEncoder::default().module(&module)?.validate(true),
-                |encoder, path| {
-                    let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg)?;
-                    Ok::<_, Error>(encoder.adapter(&name, &wasm)?)
-                },
-            )?
-            .encode()
-    } else {
-        let mut libs = glob::glob(path.join("lib-*.wat").to_str().unwrap())?
-            .map(|path| Ok(("lib-", path?, false)))
-            .chain(
-                glob::glob(path.join("dlopen-lib-*.wat").to_str().unwrap())?
-                    .map(|path| Ok(("dlopen-lib-", path?, true))),
-            )
-            .collect::<Result<Vec<_>>>()?;
+    for pkg_id in pkg_ids {
+        // If this test case contained multiple packages, create separate sub-directories for
+        // each.
+        let mut path = path.to_path_buf();
+        if pkg_count > 1 {
+            let pkg_name = &resolve.packages[pkg_id].name;
+            path.push(pkg_name.namespace.clone());
+            path.push(pkg_name.name.clone());
+            fs::create_dir_all(path.clone())?;
+        }
 
-        // Sort list to ensure deterministic order, which determines priority in cases of duplicate symbols:
-        libs.sort_by(|(_, a, _), (_, b, _)| a.cmp(b));
+        let module_path = path.join("module.wat");
+        let mut adapters = glob::glob(path.join("adapt-*.wat").to_str().unwrap())?;
+        let result = if module_path.is_file() {
+            let module = read_core_module(&module_path, &resolve, pkg_id)
+                .with_context(|| format!("failed to read core module at {module_path:?}"))?;
+            adapters
+                .try_fold(
+                    ComponentEncoder::default().module(&module)?.validate(true),
+                    |encoder, path| {
+                        let (name, wasm) =
+                            read_name_and_module("adapt-", &path?, &resolve, pkg_id)?;
+                        Ok::<_, Error>(encoder.adapter(&name, &wasm)?)
+                    },
+                )?
+                .encode()
+        } else {
+            let mut libs = glob::glob(path.join("lib-*.wat").to_str().unwrap())?
+                .map(|path| Ok(("lib-", path?, false)))
+                .chain(
+                    glob::glob(path.join("dlopen-lib-*.wat").to_str().unwrap())?
+                        .map(|path| Ok(("dlopen-lib-", path?, true))),
+                )
+                .collect::<Result<Vec<_>>>()?;
 
-        let mut linker = Linker::default().validate(true);
+            // Sort list to ensure deterministic order, which determines priority in cases of duplicate symbols:
+            libs.sort_by(|(_, a, _), (_, b, _)| a.cmp(b));
 
-        if path.join("stub-missing-functions").is_file() {
-            linker = linker.stub_missing_functions(true);
-        }
+            let mut linker = Linker::default().validate(true);
 
-        if path.join("use-built-in-libdl").is_file() {
-            linker = linker.use_built_in_libdl(true);
-        }
+            if path.join("stub-missing-functions").is_file() {
+                linker = linker.stub_missing_functions(true);
+            }
 
-        let linker = libs
-            .into_iter()
-            .try_fold(linker, |linker, (prefix, path, dl_openable)| {
-                let (name, wasm) = read_name_and_module(prefix, &path, &resolve, pkg)?;
-                Ok::<_, Error>(linker.library(&name, &wasm, dl_openable)?)
-            })?;
+            if path.join("use-built-in-libdl").is_file() {
+                linker = linker.use_built_in_libdl(true);
+            }
 
-        adapters
-            .try_fold(linker, |linker, path| {
-                let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg)?;
-                Ok::<_, Error>(linker.adapter(&name, &wasm)?)
-            })?
-            .encode()
-    };
-    let component_path = path.join("component.wat");
-    let component_wit_path = path.join("component.wit.print");
-    let error_path = path.join("error.txt");
+            let linker =
+                libs.into_iter()
+                    .try_fold(linker, |linker, (prefix, path, dl_openable)| {
+                        let (name, wasm) = read_name_and_module(prefix, &path, &resolve, pkg_id)?;
+                        Ok::<_, Error>(linker.library(&name, &wasm, dl_openable)?)
+                    })?;
 
-    let bytes = match result {
-        Ok(bytes) => {
-            if test_case.starts_with("error-") {
-                bail!("expected an error but got success");
+            adapters
+                .try_fold(linker, |linker, path| {
+                    let (name, wasm) = read_name_and_module("adapt-", &path?, &resolve, pkg_id)?;
+                    Ok::<_, Error>(linker.adapter(&name, &wasm)?)
+                })?
+                .encode()
+        };
+        let component_path = path.join("component.wat");
+        let component_wit_path = path.join("component.wit.print");
+        let error_path = path.join("error.txt");
+
+        let bytes = match result {
+            Ok(bytes) => {
+                if test_case.starts_with("error-") {
+                    bail!("expected an error but got success");
+                }
+                bytes
             }
-            bytes
-        }
-        Err(err) => {
-            if !test_case.starts_with("error-") {
-                return Err(err);
+            Err(err) => {
+                if !test_case.starts_with("error-") {
+                    return Err(err);
+                }
+                assert_output(&format!("{err:?}"), &error_path)?;
+                return Ok(());
             }
-            assert_output(&format!("{err:?}"), &error_path)?;
-            return Ok(());
-        }
-    };
+        };
 
-    let wat = wasmprinter::print_bytes(&bytes).context("failed to print bytes")?;
-    assert_output(&wat, &component_path)?;
-    let (pkg, resolve) = match wit_component::decode(&bytes).context("failed to decode resolve")? {
-        DecodedWasm::WitPackage(..) => unreachable!(),
-        DecodedWasm::Component(resolve, world) => (resolve.worlds[world].package.unwrap(), resolve),
-    };
-    let wit = WitPrinter::default()
-        .print(&resolve, pkg)
-        .context("failed to print WIT")?;
-    assert_output(&wit, &component_wit_path)?;
+        let wat = wasmprinter::print_bytes(&bytes).context("failed to print bytes")?;
+        assert_output(&wat, &component_path)?;
+        let (pkg, resolve) =
+            match wit_component::decode(&bytes).context("failed to decode resolve")? {
+                DecodedWasm::WitPackages(..) => unreachable!(),
+                DecodedWasm::Component(resolve, world) => {
+                    (resolve.worlds[world].package.unwrap(), resolve)
+                }
+            };
+        let wit = WitPrinter::default()
+            .print(&resolve, &[pkg])
+            .context("failed to print WIT")?;
+        assert_output(&wit, &component_wit_path)?;
 
-    UnresolvedPackage::parse(&component_wit_path, &wit).context("failed to parse printed WIT")?;
+        UnresolvedPackageGroup::parse(&component_wit_path, &wit)
+            .context("failed to parse printed WIT")?;
 
-    // Check that the producer data got piped through properly
-    let metadata = wasm_metadata::Metadata::from_binary(&bytes)?;
-    match metadata {
-        // Depends on the ComponentEncoder always putting the first module as the 0th child:
-        wasm_metadata::Metadata::Component { children, .. } => match children[0].as_ref() {
-            wasm_metadata::Metadata::Module { producers, .. } => {
-                let producers = producers.as_ref().expect("child module has producers");
-                let processed_by = producers
-                    .get("processed-by")
-                    .expect("child has processed-by section");
-                assert_eq!(
-                    processed_by
-                        .get("wit-component")
-                        .expect("wit-component producer present"),
-                    env!("CARGO_PKG_VERSION")
-                );
-                if module_path.is_file() {
+        // Check that the producer data got piped through properly
+        let metadata = wasm_metadata::Metadata::from_binary(&bytes)?;
+        match metadata {
+            // Depends on the ComponentEncoder always putting the first module as the 0th child:
+            wasm_metadata::Metadata::Component { children, .. } => match children[0].as_ref() {
+                wasm_metadata::Metadata::Module { producers, .. } => {
+                    let producers = producers.as_ref().expect("child module has producers");
+                    let processed_by = producers
+                        .get("processed-by")
+                        .expect("child has processed-by section");
                     assert_eq!(
                         processed_by
-                            .get("my-fake-bindgen")
-                            .expect("added bindgen field present"),
-                        "123.45"
+                            .get("wit-component")
+                            .expect("wit-component producer present"),
+                        env!("CARGO_PKG_VERSION")
                     );
-                } else {
-                    // Otherwise, we used `Linker`, which synthesizes the
-                    // "main" module and thus won't have `my-fake-bindgen`
+                    if module_path.is_file() {
+                        assert_eq!(
+                            processed_by
+                                .get("my-fake-bindgen")
+                                .expect("added bindgen field present"),
+                            "123.45"
+                        );
+                    } else {
+                        // Otherwise, we used `Linker`, which synthesizes the
+                        // "main" module and thus won't have `my-fake-bindgen`
+                    }
                 }
-            }
-            _ => panic!("expected child to be a module"),
-        },
-        _ => panic!("expected top level metadata of component"),
+                _ => panic!("expected child to be a module"),
+            },
+            _ => panic!("expected top level metadata of component"),
+        }
     }
 
     Ok(())
diff --git a/crates/wit-component/tests/components/multi-package/baz/qux/component.wat b/crates/wit-component/tests/components/multi-package/baz/qux/component.wat
new file mode 100644
index 00000000..b028617c
--- /dev/null
+++ b/crates/wit-component/tests/components/multi-package/baz/qux/component.wat
@@ -0,0 +1,90 @@
+(component
+  (type (;0;)
+    (instance
+      (type (;0;) s8)
+      (export (;1;) "x" (type (eq 0)))
+      (type (;2;) (list string))
+      (type (;3;) (func (param "x" 2)))
+      (export (;0;) "qux1" (func (type 3)))
+      (type (;4;) (func))
+      (export (;1;) "qux2" (func (type 4)))
+      (type (;5;) (func (param "x" 1)))
+      (export (;2;) "qux3" (func (type 5)))
+    )
+  )
+  (import "qux" (instance (;0;) (type 0)))
+  (core module (;0;)
+    (type (;0;) (func (param i32 i32)))
+    (type (;1;) (func))
+    (type (;2;) (func (param i32)))
+    (type (;3;) (func (param i32 i32 i32 i32) (result i32)))
+    (import "qux" "qux1" (func (;0;) (type 0)))
+    (import "qux" "qux2" (func (;1;) (type 1)))
+    (import "qux" "qux3" (func (;2;) (type 2)))
+    (func (;3;) (type 3) (param i32 i32 i32 i32) (result i32)
+      unreachable
+    )
+    (memory (;0;) 1)
+    (export "memory" (memory 0))
+    (export "cabi_realloc" (func 3))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+      (processed-by "my-fake-bindgen" "123.45")
+    )
+  )
+  (core module (;1;)
+    (type (;0;) (func (param i32 i32)))
+    (func $indirect-qux-qux1 (;0;) (type 0) (param i32 i32)
+      local.get 0
+      local.get 1
+      i32.const 0
+      call_indirect (type 0)
+    )
+    (table (;0;) 1 1 funcref)
+    (export "0" (func $indirect-qux-qux1))
+    (export "$imports" (table 0))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;2;)
+    (type (;0;) (func (param i32 i32)))
+    (import "" "0" (func (;0;) (type 0)))
+    (import "" "$imports" (table (;0;) 1 1 funcref))
+    (elem (;0;) (i32.const 0) func 0)
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core instance (;0;) (instantiate 1))
+  (alias core export 0 "0" (core func (;0;)))
+  (alias export 0 "qux2" (func (;0;)))
+  (core func (;1;) (canon lower (func 0)))
+  (alias export 0 "qux3" (func (;1;)))
+  (core func (;2;) (canon lower (func 1)))
+  (core instance (;1;)
+    (export "qux1" (func 0))
+    (export "qux2" (func 1))
+    (export "qux3" (func 2))
+  )
+  (core instance (;2;) (instantiate 0
+      (with "qux" (instance 1))
+    )
+  )
+  (alias core export 2 "memory" (core memory (;0;)))
+  (alias core export 2 "cabi_realloc" (core func (;3;)))
+  (alias core export 0 "$imports" (core table (;0;)))
+  (alias export 0 "qux1" (func (;2;)))
+  (core func (;4;) (canon lower (func 2) (memory 0) string-encoding=utf8))
+  (core instance (;3;)
+    (export "$imports" (table 0))
+    (export "0" (func 4))
+  )
+  (core instance (;4;) (instantiate 2
+      (with "" (instance 3))
+    )
+  )
+  (@producers
+    (processed-by "wit-component" "$CARGO_PKG_VERSION")
+  )
+)
diff --git a/crates/wit-component/tests/components/multi-package/baz/qux/component.wit.print b/crates/wit-component/tests/components/multi-package/baz/qux/component.wit.print
new file mode 100644
index 00000000..71d20021
--- /dev/null
+++ b/crates/wit-component/tests/components/multi-package/baz/qux/component.wit.print
@@ -0,0 +1,13 @@
+package root:component;
+
+world root {
+  import qux: interface {
+    type x = s8;
+
+    qux1: func(x: list<string>);
+
+    qux2: func();
+
+    qux3: func(x: x);
+  }
+}
diff --git a/crates/wit-component/tests/components/multi-package/baz/qux/module.wat b/crates/wit-component/tests/components/multi-package/baz/qux/module.wat
new file mode 100644
index 00000000..8eb6e37d
--- /dev/null
+++ b/crates/wit-component/tests/components/multi-package/baz/qux/module.wat
@@ -0,0 +1,7 @@
+(module
+  (import "qux" "qux1" (func (param i32 i32)))
+  (import "qux" "qux2" (func))
+  (import "qux" "qux3" (func (param i32)))
+  (memory (export "memory") 1)
+  (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) unreachable)
+)
diff --git a/crates/wit-component/tests/components/multi-package/foo/bar/component.wat b/crates/wit-component/tests/components/multi-package/foo/bar/component.wat
new file mode 100644
index 00000000..82caf152
--- /dev/null
+++ b/crates/wit-component/tests/components/multi-package/foo/bar/component.wat
@@ -0,0 +1,82 @@
+(component
+  (type (;0;)
+    (instance
+      (type (;0;) (record (field "a" u8)))
+      (export (;1;) "x" (type (eq 0)))
+      (type (;2;) (func (param "x" string)))
+      (export (;0;) "bar1" (func (type 2)))
+      (type (;3;) (func (param "x" 1)))
+      (export (;1;) "bar2" (func (type 3)))
+    )
+  )
+  (import "bar" (instance (;0;) (type 0)))
+  (core module (;0;)
+    (type (;0;) (func (param i32 i32)))
+    (type (;1;) (func (param i32)))
+    (type (;2;) (func (param i32 i32 i32 i32) (result i32)))
+    (import "bar" "bar1" (func (;0;) (type 0)))
+    (import "bar" "bar2" (func (;1;) (type 1)))
+    (func (;2;) (type 2) (param i32 i32 i32 i32) (result i32)
+      unreachable
+    )
+    (memory (;0;) 1)
+    (export "memory" (memory 0))
+    (export "cabi_realloc" (func 2))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+      (processed-by "my-fake-bindgen" "123.45")
+    )
+  )
+  (core module (;1;)
+    (type (;0;) (func (param i32 i32)))
+    (func $indirect-bar-bar1 (;0;) (type 0) (param i32 i32)
+      local.get 0
+      local.get 1
+      i32.const 0
+      call_indirect (type 0)
+    )
+    (table (;0;) 1 1 funcref)
+    (export "0" (func $indirect-bar-bar1))
+    (export "$imports" (table 0))
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core module (;2;)
+    (type (;0;) (func (param i32 i32)))
+    (import "" "0" (func (;0;) (type 0)))
+    (import "" "$imports" (table (;0;) 1 1 funcref))
+    (elem (;0;) (i32.const 0) func 0)
+    (@producers
+      (processed-by "wit-component" "$CARGO_PKG_VERSION")
+    )
+  )
+  (core instance (;0;) (instantiate 1))
+  (alias core export 0 "0" (core func (;0;)))
+  (alias export 0 "bar2" (func (;0;)))
+  (core func (;1;) (canon lower (func 0)))
+  (core instance (;1;)
+    (export "bar1" (func 0))
+    (export "bar2" (func 1))
+  )
+  (core instance (;2;) (instantiate 0
+      (with "bar" (instance 1))
+    )
+  )
+  (alias core export 2 "memory" (core memory (;0;)))
+  (alias core export 2 "cabi_realloc" (core func (;2;)))
+  (alias core export 0 "$imports" (core table (;0;)))
+  (alias export 0 "bar1" (func (;1;)))
+  (core func (;3;) (canon lower (func 1) (memory 0) string-encoding=utf8))
+  (core instance (;3;)
+    (export "$imports" (table 0))
+    (export "0" (func 3))
+  )
+  (core instance (;4;) (instantiate 2
+      (with "" (instance 3))
+    )
+  )
+  (@producers
+    (processed-by "wit-component" "$CARGO_PKG_VERSION")
+  )
+)
diff --git a/crates/wit-component/tests/components/multi-package/foo/bar/component.wit.print b/crates/wit-component/tests/components/multi-package/foo/bar/component.wit.print
new file mode 100644
index 00000000..f3cc24ed
--- /dev/null
+++ b/crates/wit-component/tests/components/multi-package/foo/bar/component.wit.print
@@ -0,0 +1,13 @@
+package root:component;
+
+world root {
+  import bar: interface {
+    record x {
+      a: u8,
+    }
+
+    bar1: func(x: string);
+
+    bar2: func(x: x);
+  }
+}
diff --git a/crates/wit-component/tests/components/multi-package/foo/bar/module.wat b/crates/wit-component/tests/components/multi-package/foo/bar/module.wat
new file mode 100644
index 00000000..7a56c616
--- /dev/null
+++ b/crates/wit-component/tests/components/multi-package/foo/bar/module.wat
@@ -0,0 +1,6 @@
+(module
+  (import "bar" "bar1" (func (param i32 i32)))
+  (import "bar" "bar2" (func (param i32)))
+  (memory (export "memory") 1)
+  (func (export "cabi_realloc") (param i32 i32 i32 i32) (result i32) unreachable)
+)
diff --git a/crates/wit-component/tests/components/multi-package/module.wit b/crates/wit-component/tests/components/multi-package/module.wit
new file mode 100644
index 00000000..838ad730
--- /dev/null
+++ b/crates/wit-component/tests/components/multi-package/module.wit
@@ -0,0 +1,24 @@
+package foo:bar {
+  world module {
+    import bar: interface {
+      record x {
+          a: u8
+      }
+
+      bar1: func(x: string);
+      bar2: func(x: x);
+    }
+  }
+}
+
+package baz:qux {
+  world module {
+    import qux: interface {
+      type x = s8;
+
+      qux1: func(x: list<string>);
+      qux2: func();
+      qux3: func(x: x);
+    }
+  }
+}
diff --git a/crates/wit-component/tests/interfaces.rs b/crates/wit-component/tests/interfaces.rs
index 6cb76439..926499db 100644
--- a/crates/wit-component/tests/interfaces.rs
+++ b/crates/wit-component/tests/interfaces.rs
@@ -5,7 +5,7 @@ use std::fs;
 use std::path::Path;
 use wasmparser::WasmFeatures;
 use wit_component::WitPrinter;
-use wit_parser::{PackageId, Resolve, UnresolvedPackage};
+use wit_parser::{PackageId, Resolve, UnresolvedPackageGroup};
 
 /// Tests the encoding of a WIT package as a WebAssembly binary.
 ///
@@ -48,55 +48,66 @@ fn main() -> Result<()> {
 
 fn run_test(path: &Path, is_dir: bool) -> Result<()> {
     let mut resolve = Resolve::new();
-    let package = if is_dir {
+    let packages = if is_dir {
         resolve.push_dir(path)?.0
     } else {
-        resolve.push(UnresolvedPackage::parse_file(path)?)?
+        resolve.append(UnresolvedPackageGroup::parse_file(path)?)?
     };
 
-    assert_print(&resolve, package, path, is_dir)?;
+    for package in packages {
+        assert_print(&resolve, &[package], path, is_dir)?;
 
-    let features = WasmFeatures::default() | WasmFeatures::COMPONENT_MODEL;
+        let features = WasmFeatures::default() | WasmFeatures::COMPONENT_MODEL;
 
-    // First convert the WIT package to a binary WebAssembly output, then
-    // convert that binary wasm to textual wasm, then assert it matches the
-    // expectation.
-    let wasm = wit_component::encode(Some(true), &resolve, package)?;
-    let wat = wasmprinter::print_bytes(&wasm)?;
-    assert_output(&path.with_extension("wat"), &wat)?;
-    wasmparser::Validator::new_with_features(features)
-        .validate_all(&wasm)
-        .context("failed to validate wasm output")?;
+        // First convert the WIT package to a binary WebAssembly output, then
+        // convert that binary wasm to textual wasm, then assert it matches the
+        // expectation.
+        let wasm = wit_component::encode(Some(true), &resolve, package)?;
+        let wat = wasmprinter::print_bytes(&wasm)?;
+        assert_output(&path.with_extension("wat"), &wat)?;
+        wasmparser::Validator::new_with_features(features)
+            .validate_all(&wasm)
+            .context("failed to validate wasm output")?;
 
-    // Next decode a fresh WIT package from the WebAssembly generated. Print
-    // this package's documents and assert they all match the expectations.
-    let decoded = wit_component::decode(&wasm)?;
-    let resolve = decoded.resolve();
+        // Next decode a fresh WIT package from the WebAssembly generated. Print
+        // this package's documents and assert they all match the expectations.
+        let decoded = wit_component::decode(&wasm)?;
+        assert_eq!(
+            1,
+            decoded.packages().len(),
+            "Each input WIT package should produce WASM that contains only one package"
+        );
 
-    assert_print(resolve, decoded.package(), path, is_dir)?;
+        let decoded_package = decoded.packages()[0];
+        let resolve = decoded.resolve();
 
-    // Finally convert the decoded package to wasm again and make sure it
-    // matches the prior wasm.
-    let wasm2 = wit_component::encode(Some(true), resolve, decoded.package())?;
-    if wasm != wasm2 {
-        let wat2 = wasmprinter::print_bytes(&wasm)?;
-        assert_eq!(wat, wat2, "document did not roundtrip correctly");
+        assert_print(resolve, decoded.packages(), path, is_dir)?;
+
+        // Finally convert the decoded package to wasm again and make sure it
+        // matches the prior wasm.
+        let wasm2 = wit_component::encode(Some(true), resolve, decoded_package)?;
+        if wasm != wasm2 {
+            let wat2 = wasmprinter::print_bytes(&wasm)?;
+            assert_eq!(wat, wat2, "document did not roundtrip correctly");
+        }
     }
 
     Ok(())
 }
 
-fn assert_print(resolve: &Resolve, package: PackageId, path: &Path, is_dir: bool) -> Result<()> {
-    let pkg = &resolve.packages[package];
-    let expected = if is_dir {
-        path.join(format!("{}.wit.print", &pkg.name.name))
-    } else {
-        path.with_extension("wit.print")
-    };
-    let output = WitPrinter::default().print(resolve, package)?;
-    assert_output(&expected, &output)?;
+fn assert_print(resolve: &Resolve, pkg_ids: &[PackageId], path: &Path, is_dir: bool) -> Result<()> {
+    let output = WitPrinter::default().print(resolve, &pkg_ids)?;
+    for pkg_id in pkg_ids {
+        let pkg = &resolve.packages[*pkg_id];
+        let expected = if is_dir {
+            path.join(format!("{}.wit.print", &pkg.name.name))
+        } else {
+            path.with_extension("wit.print")
+        };
+        assert_output(&expected, &output)?;
+    }
 
-    UnresolvedPackage::parse("foo.wit".as_ref(), &output)
+    UnresolvedPackageGroup::parse("foo.wit".as_ref(), &output)
         .context("failed to parse printed output")?;
     Ok(())
 }
diff --git a/crates/wit-component/tests/linking.rs b/crates/wit-component/tests/linking.rs
index b774827d..56757f97 100644
--- a/crates/wit-component/tests/linking.rs
+++ b/crates/wit-component/tests/linking.rs
@@ -2,7 +2,7 @@ use {
     anyhow::{Context, Result},
     std::path::Path,
     wit_component::StringEncoding,
-    wit_parser::{Resolve, UnresolvedPackage},
+    wit_parser::{Resolve, UnresolvedPackageGroup},
 };
 
 const FOO: &str = r#"
@@ -141,7 +141,11 @@ fn encode(wat: &str, wit: Option<&str>) -> Result<Vec<u8>> {
 
     if let Some(wit) = wit {
         let mut resolve = Resolve::default();
-        let pkg = resolve.push(UnresolvedPackage::parse(Path::new("wit"), wit)?)?;
+        let UnresolvedPackageGroup {
+            mut packages,
+            source_map,
+        } = UnresolvedPackageGroup::parse(Path::new("wit"), wit)?;
+        let pkg = resolve.push(packages.remove(0), &source_map)?;
         let world = resolve.select_world(pkg, None)?;
 
         wit_component::embed_component_metadata(
diff --git a/crates/wit-component/tests/merge.rs b/crates/wit-component/tests/merge.rs
index 264c0c0d..f9a70859 100644
--- a/crates/wit-component/tests/merge.rs
+++ b/crates/wit-component/tests/merge.rs
@@ -46,7 +46,7 @@ fn merging() -> Result<()> {
                         .join("merge")
                         .join(&pkg.name.name)
                         .with_extension("wit");
-                    let output = WitPrinter::default().print(&into, id)?;
+                    let output = WitPrinter::default().print(&into, &[id])?;
                     assert_output(&expected, &output)?;
                 }
             }
diff --git a/crates/wit-component/tests/targets.rs b/crates/wit-component/tests/targets.rs
index 23579b7f..8dac12d3 100644
--- a/crates/wit-component/tests/targets.rs
+++ b/crates/wit-component/tests/targets.rs
@@ -1,6 +1,6 @@
-use anyhow::{Context, Result};
+use anyhow::{bail, Context, Result};
 use std::{fs, path::Path};
-use wit_parser::{Resolve, UnresolvedPackage, WorldId};
+use wit_parser::{Resolve, UnresolvedPackageGroup, WorldId};
 
 /// Tests whether a component targets a world.
 ///
@@ -79,11 +79,20 @@ fn load_test_wit(path: &Path) -> Result<(Resolve, WorldId)> {
     const TEST_TARGET_WORLD_ID: &str = "foobar";
 
     let test_wit_path = path.join("test.wit");
-    let package =
-        UnresolvedPackage::parse_file(&test_wit_path).context("failed to parse WIT package")?;
+    let UnresolvedPackageGroup {
+        mut packages,
+        source_map,
+    } = UnresolvedPackageGroup::parse_file(&test_wit_path)
+        .context("failed to parse WIT package")?;
+    if packages.is_empty() {
+        bail!("Files were completely empty - are you sure these are the files you're looking for?")
+    }
+    if packages.len() > 1 {
+        bail!("Multi-package targeting tests are not yet supported.")
+    }
 
     let mut resolve = Resolve::default();
-    let package_id = resolve.push(package)?;
+    let package_id = resolve.push(packages.remove(0), &source_map)?;
 
     let world_id = resolve
         .select_world(package_id, Some(TEST_TARGET_WORLD_ID))
diff --git a/crates/wit-component/tests/wit.rs b/crates/wit-component/tests/wit.rs
index 92d77132..860fd108 100644
--- a/crates/wit-component/tests/wit.rs
+++ b/crates/wit-component/tests/wit.rs
@@ -9,7 +9,7 @@ fn parse_wit_dir() -> Result<()> {
     drop(env_logger::try_init());
 
     let mut resolver = Resolve::default();
-    let package_id = resolver.push_path("tests/wit/parse-dir/wit")?.0;
+    let package_id = resolver.push_path("tests/wit/parse-dir/wit")?.0[0];
     assert!(resolver
         .select_world(package_id, "foo-world".into())
         .is_ok());
@@ -25,7 +25,7 @@ fn parse_wit_file() -> Result<()> {
     let mut resolver = Resolve::default();
     let package_id = resolver
         .push_path("tests/wit/parse-dir/wit/deps/bar/bar.wit")?
-        .0;
+        .0[0];
     resolver.select_world(package_id, "bar-world".into())?;
     assert!(resolver
         .interfaces
diff --git a/crates/wit-parser/tests/ui/multi-file-multi-package.wit.json b/crates/wit-parser/tests/ui/multi-file-multi-package.wit.json
new file mode 100644
index 00000000..a2dea317
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-file-multi-package.wit.json
@@ -0,0 +1,250 @@
+{
+  "worlds": [
+    {
+      "name": "w2",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "imp2": {
+          "interface": {
+            "id": 1
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "w3",
+      "imports": {
+        "interface-2": {
+          "interface": {
+            "id": 2
+          }
+        },
+        "imp3": {
+          "interface": {
+            "id": 3
+          }
+        }
+      },
+      "exports": {},
+      "package": 1
+    },
+    {
+      "name": "w1",
+      "imports": {
+        "interface-4": {
+          "interface": {
+            "id": 4
+          }
+        },
+        "imp1": {
+          "interface": {
+            "id": 5
+          }
+        }
+      },
+      "exports": {},
+      "package": 2
+    },
+    {
+      "name": "w4",
+      "imports": {
+        "interface-6": {
+          "interface": {
+            "id": 6
+          }
+        },
+        "imp4": {
+          "interface": {
+            "id": 7
+          }
+        }
+      },
+      "exports": {},
+      "package": 3
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "i2",
+      "types": {
+        "b": 0
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": null,
+      "types": {
+        "b": 1
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": "i3",
+      "types": {
+        "a": 2
+      },
+      "functions": {},
+      "package": 1
+    },
+    {
+      "name": null,
+      "types": {
+        "a": 3
+      },
+      "functions": {},
+      "package": 1
+    },
+    {
+      "name": "i1",
+      "types": {
+        "a": 4
+      },
+      "functions": {},
+      "package": 2
+    },
+    {
+      "name": null,
+      "types": {
+        "a": 5
+      },
+      "functions": {},
+      "package": 2
+    },
+    {
+      "name": "i4",
+      "types": {
+        "b": 6
+      },
+      "functions": {},
+      "package": 3
+    },
+    {
+      "name": null,
+      "types": {
+        "b": 7
+      },
+      "functions": {},
+      "package": 3
+    }
+  ],
+  "types": [
+    {
+      "name": "b",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 0
+      }
+    },
+    {
+      "name": "b",
+      "kind": {
+        "type": 0
+      },
+      "owner": {
+        "interface": 1
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 2
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 2
+      },
+      "owner": {
+        "interface": 3
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 4
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 4
+      },
+      "owner": {
+        "interface": 5
+      }
+    },
+    {
+      "name": "b",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 6
+      }
+    },
+    {
+      "name": "b",
+      "kind": {
+        "type": 6
+      },
+      "owner": {
+        "interface": 7
+      }
+    }
+  ],
+  "packages": [
+    {
+      "name": "bar:name",
+      "interfaces": {
+        "i2": 0
+      },
+      "worlds": {
+        "w2": 0
+      }
+    },
+    {
+      "name": "baz:name",
+      "interfaces": {
+        "i3": 2
+      },
+      "worlds": {
+        "w3": 1
+      }
+    },
+    {
+      "name": "foo:name",
+      "interfaces": {
+        "i1": 4
+      },
+      "worlds": {
+        "w1": 2
+      }
+    },
+    {
+      "name": "qux:name",
+      "interfaces": {
+        "i4": 6
+      },
+      "worlds": {
+        "w4": 3
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/multi-file-multi-package/a.wit b/crates/wit-parser/tests/ui/multi-file-multi-package/a.wit
new file mode 100644
index 00000000..79e98376
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-file-multi-package/a.wit
@@ -0,0 +1,23 @@
+package foo:name {
+  interface i1 {
+    type a = u32;
+  }
+
+  world w1 {
+    import imp1: interface {
+        use i1.{a};
+    }
+  }
+}
+
+package bar:name {
+  interface i2 {
+    type b = u32;
+  }
+
+  world w2 {
+    import imp2: interface {
+        use i2.{b};
+    }
+  }
+}
diff --git a/crates/wit-parser/tests/ui/multi-file-multi-package/b.wit b/crates/wit-parser/tests/ui/multi-file-multi-package/b.wit
new file mode 100644
index 00000000..0dcd1d5d
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-file-multi-package/b.wit
@@ -0,0 +1,23 @@
+package baz:name {
+  interface i3 {
+    type a = u32;
+  }
+
+  world w3 {
+    import imp3: interface {
+        use i3.{a};
+    }
+  }
+}
+
+package qux:name {
+  interface i4 {
+    type b = u32;
+  }
+
+  world w4 {
+    import imp4: interface {
+        use i4.{b};
+    }
+  }
+}
diff --git a/crates/wit-parser/tests/ui/multi-package-shared-deps.wit.json b/crates/wit-parser/tests/ui/multi-package-shared-deps.wit.json
new file mode 100644
index 00000000..d3434da0
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-package-shared-deps.wit.json
@@ -0,0 +1,83 @@
+{
+  "worlds": [
+    {
+      "name": "w-bar",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "interface-1": {
+          "interface": {
+            "id": 1
+          }
+        }
+      },
+      "exports": {},
+      "package": 2
+    },
+    {
+      "name": "w-qux",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "interface-1": {
+          "interface": {
+            "id": 1
+          }
+        }
+      },
+      "exports": {},
+      "package": 3
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "types",
+      "types": {},
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": "types",
+      "types": {},
+      "functions": {},
+      "package": 1
+    }
+  ],
+  "types": [],
+  "packages": [
+    {
+      "name": "foo:dep1",
+      "interfaces": {
+        "types": 0
+      },
+      "worlds": {}
+    },
+    {
+      "name": "foo:dep2",
+      "interfaces": {
+        "types": 1
+      },
+      "worlds": {}
+    },
+    {
+      "name": "foo:bar",
+      "interfaces": {},
+      "worlds": {
+        "w-bar": 0
+      }
+    },
+    {
+      "name": "foo:qux",
+      "interfaces": {},
+      "worlds": {
+        "w-qux": 1
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep1/types.wit b/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep1/types.wit
new file mode 100644
index 00000000..4c84420d
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep1/types.wit
@@ -0,0 +1,2 @@
+package foo:dep1;
+interface types {}
diff --git a/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep2/types.wit b/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep2/types.wit
new file mode 100644
index 00000000..b7cf9466
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-package-shared-deps/deps/dep2/types.wit
@@ -0,0 +1,2 @@
+package foo:dep2;
+interface types {}
diff --git a/crates/wit-parser/tests/ui/multi-package-shared-deps/packages.wit b/crates/wit-parser/tests/ui/multi-package-shared-deps/packages.wit
new file mode 100644
index 00000000..22d82aa9
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-package-shared-deps/packages.wit
@@ -0,0 +1,13 @@
+package foo:bar {
+  world w-bar {
+    import foo:dep1/types;
+    import foo:dep2/types;
+  }
+}
+
+package foo:qux {
+  world w-qux {
+    import foo:dep1/types;
+    import foo:dep2/types;
+  }
+}
diff --git a/crates/wit-parser/tests/ui/multi-package-transitive-deps.wit.json b/crates/wit-parser/tests/ui/multi-package-transitive-deps.wit.json
new file mode 100644
index 00000000..ece4cdad
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-package-transitive-deps.wit.json
@@ -0,0 +1,116 @@
+{
+  "worlds": [
+    {
+      "name": "w-bar",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "interface-1": {
+          "interface": {
+            "id": 1
+          }
+        }
+      },
+      "exports": {},
+      "package": 2
+    },
+    {
+      "name": "w-qux",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        }
+      },
+      "exports": {},
+      "package": 3
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "types",
+      "types": {
+        "a": 0
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": "types",
+      "types": {
+        "a": 1,
+        "r": 2
+      },
+      "functions": {},
+      "package": 1
+    }
+  ],
+  "types": [
+    {
+      "name": "a",
+      "kind": "resource",
+      "owner": {
+        "interface": 0
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 0
+      },
+      "owner": {
+        "interface": 1
+      }
+    },
+    {
+      "name": "r",
+      "kind": {
+        "record": {
+          "fields": [
+            {
+              "name": "f",
+              "type": "u8"
+            }
+          ]
+        }
+      },
+      "owner": {
+        "interface": 1
+      }
+    }
+  ],
+  "packages": [
+    {
+      "name": "foo:dep2",
+      "interfaces": {
+        "types": 0
+      },
+      "worlds": {}
+    },
+    {
+      "name": "foo:dep1",
+      "interfaces": {
+        "types": 1
+      },
+      "worlds": {}
+    },
+    {
+      "name": "foo:bar",
+      "interfaces": {},
+      "worlds": {
+        "w-bar": 0
+      }
+    },
+    {
+      "name": "foo:qux",
+      "interfaces": {},
+      "worlds": {
+        "w-qux": 1
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep1/types.wit b/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep1/types.wit
new file mode 100644
index 00000000..825a3964
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep1/types.wit
@@ -0,0 +1,9 @@
+package foo:dep1;
+
+interface types {
+  use foo:dep2/types.{a};
+
+  record r {
+    f: u8,
+  }
+}
diff --git a/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep2/types.wit b/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep2/types.wit
new file mode 100644
index 00000000..603efb70
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-package-transitive-deps/deps/dep2/types.wit
@@ -0,0 +1,5 @@
+package foo:dep2;
+
+interface types {
+  resource a;
+}
diff --git a/crates/wit-parser/tests/ui/multi-package-transitive-deps/packages.wit b/crates/wit-parser/tests/ui/multi-package-transitive-deps/packages.wit
new file mode 100644
index 00000000..04e04f0a
--- /dev/null
+++ b/crates/wit-parser/tests/ui/multi-package-transitive-deps/packages.wit
@@ -0,0 +1,11 @@
+package foo:bar {
+  world w-bar {
+    import foo:dep1/types;
+  }
+}
+
+package foo:qux {
+  world w-qux {
+    import foo:dep2/types;
+  }
+}
diff --git a/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit b/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit
new file mode 100644
index 00000000..876012cf
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit
@@ -0,0 +1,23 @@
+package foo:name {
+  interface i {
+    type a = u32;
+  }
+
+  world w {
+    import imp: interface {
+        use i.{a};
+    }
+  }
+}
+
+package bar:name {
+  interface i {
+    type a = u32;
+  }
+
+  world w {
+    import imp: interface {
+        use i.{a};
+    }
+  }
+}
diff --git a/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit.json b/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit.json
new file mode 100644
index 00000000..f4c9e705
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-explicit-colliding-decl-names.wit.json
@@ -0,0 +1,130 @@
+{
+  "worlds": [
+    {
+      "name": "w",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "imp": {
+          "interface": {
+            "id": 1
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "w",
+      "imports": {
+        "interface-2": {
+          "interface": {
+            "id": 2
+          }
+        },
+        "imp": {
+          "interface": {
+            "id": 3
+          }
+        }
+      },
+      "exports": {},
+      "package": 1
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "i",
+      "types": {
+        "a": 0
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": null,
+      "types": {
+        "a": 1
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": "i",
+      "types": {
+        "a": 2
+      },
+      "functions": {},
+      "package": 1
+    },
+    {
+      "name": null,
+      "types": {
+        "a": 3
+      },
+      "functions": {},
+      "package": 1
+    }
+  ],
+  "types": [
+    {
+      "name": "a",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 0
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 0
+      },
+      "owner": {
+        "interface": 1
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 2
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 2
+      },
+      "owner": {
+        "interface": 3
+      }
+    }
+  ],
+  "packages": [
+    {
+      "name": "bar:name",
+      "interfaces": {
+        "i": 0
+      },
+      "worlds": {
+        "w": 0
+      }
+    },
+    {
+      "name": "foo:name",
+      "interfaces": {
+        "i": 2
+      },
+      "worlds": {
+        "w": 1
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit b/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit
new file mode 100644
index 00000000..52d1c682
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit
@@ -0,0 +1,15 @@
+package foo:name {
+  interface i1 {
+    type a = u32;
+  }
+}
+
+package bar:name {
+  world w1 {
+    import imp1: interface {
+      use foo:name/i1.{a};
+
+      fn: func(a: a);
+    }
+  }
+}
diff --git a/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit.json b/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit.json
new file mode 100644
index 00000000..1d59a07e
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-explicit-internal-references.wit.json
@@ -0,0 +1,87 @@
+{
+  "worlds": [
+    {
+      "name": "w1",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "imp1": {
+          "interface": {
+            "id": 1
+          }
+        }
+      },
+      "exports": {},
+      "package": 1
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "i1",
+      "types": {
+        "a": 0
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": null,
+      "types": {
+        "a": 1
+      },
+      "functions": {
+        "fn": {
+          "name": "fn",
+          "kind": "freestanding",
+          "params": [
+            {
+              "name": "a",
+              "type": 1
+            }
+          ],
+          "results": []
+        }
+      },
+      "package": 1
+    }
+  ],
+  "types": [
+    {
+      "name": "a",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 0
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 0
+      },
+      "owner": {
+        "interface": 1
+      }
+    }
+  ],
+  "packages": [
+    {
+      "name": "foo:name",
+      "interfaces": {
+        "i1": 0
+      },
+      "worlds": {}
+    },
+    {
+      "name": "bar:name",
+      "interfaces": {},
+      "worlds": {
+        "w1": 0
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit b/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit
new file mode 100644
index 00000000..583b5559
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit
@@ -0,0 +1,23 @@
+package foo:name@1.0.0 {
+  interface i1 {
+    type a = u32;
+  }
+
+  world w1 {
+    import imp1: interface {
+        use i1.{a};
+    }
+  }
+}
+
+package foo:name@1.0.1 {
+  interface i1 {
+    type a = u32;
+  }
+
+  world w1 {
+    import imp1: interface {
+        use i1.{a};
+    }
+  }
+}
diff --git a/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit.json b/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit.json
new file mode 100644
index 00000000..23a10462
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-explicit-with-semver.wit.json
@@ -0,0 +1,130 @@
+{
+  "worlds": [
+    {
+      "name": "w1",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "imp1": {
+          "interface": {
+            "id": 1
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "w1",
+      "imports": {
+        "interface-2": {
+          "interface": {
+            "id": 2
+          }
+        },
+        "imp1": {
+          "interface": {
+            "id": 3
+          }
+        }
+      },
+      "exports": {},
+      "package": 1
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "i1",
+      "types": {
+        "a": 0
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": null,
+      "types": {
+        "a": 1
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": "i1",
+      "types": {
+        "a": 2
+      },
+      "functions": {},
+      "package": 1
+    },
+    {
+      "name": null,
+      "types": {
+        "a": 3
+      },
+      "functions": {},
+      "package": 1
+    }
+  ],
+  "types": [
+    {
+      "name": "a",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 0
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 0
+      },
+      "owner": {
+        "interface": 1
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 2
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 2
+      },
+      "owner": {
+        "interface": 3
+      }
+    }
+  ],
+  "packages": [
+    {
+      "name": "foo:name@1.0.0",
+      "interfaces": {
+        "i1": 0
+      },
+      "worlds": {
+        "w1": 0
+      }
+    },
+    {
+      "name": "foo:name@1.0.1",
+      "interfaces": {
+        "i1": 2
+      },
+      "worlds": {
+        "w1": 1
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/packages-multiple-explicit.wit b/crates/wit-parser/tests/ui/packages-multiple-explicit.wit
new file mode 100644
index 00000000..79e98376
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-multiple-explicit.wit
@@ -0,0 +1,23 @@
+package foo:name {
+  interface i1 {
+    type a = u32;
+  }
+
+  world w1 {
+    import imp1: interface {
+        use i1.{a};
+    }
+  }
+}
+
+package bar:name {
+  interface i2 {
+    type b = u32;
+  }
+
+  world w2 {
+    import imp2: interface {
+        use i2.{b};
+    }
+  }
+}
diff --git a/crates/wit-parser/tests/ui/packages-multiple-explicit.wit.json b/crates/wit-parser/tests/ui/packages-multiple-explicit.wit.json
new file mode 100644
index 00000000..de73c51d
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-multiple-explicit.wit.json
@@ -0,0 +1,130 @@
+{
+  "worlds": [
+    {
+      "name": "w2",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "imp2": {
+          "interface": {
+            "id": 1
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    },
+    {
+      "name": "w1",
+      "imports": {
+        "interface-2": {
+          "interface": {
+            "id": 2
+          }
+        },
+        "imp1": {
+          "interface": {
+            "id": 3
+          }
+        }
+      },
+      "exports": {},
+      "package": 1
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "i2",
+      "types": {
+        "b": 0
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": null,
+      "types": {
+        "b": 1
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": "i1",
+      "types": {
+        "a": 2
+      },
+      "functions": {},
+      "package": 1
+    },
+    {
+      "name": null,
+      "types": {
+        "a": 3
+      },
+      "functions": {},
+      "package": 1
+    }
+  ],
+  "types": [
+    {
+      "name": "b",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 0
+      }
+    },
+    {
+      "name": "b",
+      "kind": {
+        "type": 0
+      },
+      "owner": {
+        "interface": 1
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 2
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 2
+      },
+      "owner": {
+        "interface": 3
+      }
+    }
+  ],
+  "packages": [
+    {
+      "name": "bar:name",
+      "interfaces": {
+        "i2": 0
+      },
+      "worlds": {
+        "w2": 0
+      }
+    },
+    {
+      "name": "foo:name",
+      "interfaces": {
+        "i1": 2
+      },
+      "worlds": {
+        "w1": 1
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/packages-single-explicit.wit b/crates/wit-parser/tests/ui/packages-single-explicit.wit
new file mode 100644
index 00000000..695155a9
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-single-explicit.wit
@@ -0,0 +1,11 @@
+package foo:name {
+  interface i1 {
+    type a = u32;
+  }
+
+  world w1 {
+    import imp1: interface {
+        use i1.{a};
+    }
+  }
+}
diff --git a/crates/wit-parser/tests/ui/packages-single-explicit.wit.json b/crates/wit-parser/tests/ui/packages-single-explicit.wit.json
new file mode 100644
index 00000000..3eefde28
--- /dev/null
+++ b/crates/wit-parser/tests/ui/packages-single-explicit.wit.json
@@ -0,0 +1,70 @@
+{
+  "worlds": [
+    {
+      "name": "w1",
+      "imports": {
+        "interface-0": {
+          "interface": {
+            "id": 0
+          }
+        },
+        "imp1": {
+          "interface": {
+            "id": 1
+          }
+        }
+      },
+      "exports": {},
+      "package": 0
+    }
+  ],
+  "interfaces": [
+    {
+      "name": "i1",
+      "types": {
+        "a": 0
+      },
+      "functions": {},
+      "package": 0
+    },
+    {
+      "name": null,
+      "types": {
+        "a": 1
+      },
+      "functions": {},
+      "package": 0
+    }
+  ],
+  "types": [
+    {
+      "name": "a",
+      "kind": {
+        "type": "u32"
+      },
+      "owner": {
+        "interface": 0
+      }
+    },
+    {
+      "name": "a",
+      "kind": {
+        "type": 0
+      },
+      "owner": {
+        "interface": 1
+      }
+    }
+  ],
+  "packages": [
+    {
+      "name": "foo:name",
+      "interfaces": {
+        "i1": 0
+      },
+      "worlds": {
+        "w1": 0
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit
new file mode 100644
index 00000000..1e9e0a5c
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit
@@ -0,0 +1,3 @@
+package foo:name {}
+
+package foo:name {}
diff --git a/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit.result b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit.result
new file mode 100644
index 00000000..fcc55c6e
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-colliding-names.wit.result
@@ -0,0 +1,1 @@
+colliding explicit package names, multiple packages named `foo:name`
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit
new file mode 100644
index 00000000..4fcb27cb
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit
@@ -0,0 +1,13 @@
+package foo:name {}
+
+package bar:name {
+  interface unused {
+    type a = u32;
+  }
+
+  world w {
+    import imp: interface {
+        use missing.{a};
+    }
+  }
+}
diff --git a/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit.result b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit.result
new file mode 100644
index 00000000..2c0161f9
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/explicit-packages-with-error.wit.result
@@ -0,0 +1,8 @@
+failed to start resolving path: tests/ui/parse-fail/explicit-packages-with-error.wit
+
+Caused by:
+    interface or world `missing` does not exist
+         --> tests/ui/parse-fail/explicit-packages-with-error.wit:10:13
+          |
+       10 |         use missing.{a};
+          |             ^------
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit b/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit
new file mode 100644
index 00000000..9c208438
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit
@@ -0,0 +1,23 @@
+package foo:name {
+  interface i1 {
+    type a = u32;
+  }
+
+  world w1 {
+    import imp1: interface {
+        use i1.{a};
+    }
+  }
+}
+
+
+interface i2 {
+  type b = u32;
+}
+
+world w2 {
+  import imp2: interface {
+    use i2.{b};
+  }
+}
+
diff --git a/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit.result b/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit.result
new file mode 100644
index 00000000..8a6fa246
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/mix-explicit-then-implicit-package.wit.result
@@ -0,0 +1,1 @@
+WIT files cannot mix top-level explicit `package` declarations with other declaration kinds
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit b/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit
new file mode 100644
index 00000000..58b934af
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit
@@ -0,0 +1,23 @@
+interface i1 {
+  type a = u32;
+}
+
+world w1 {
+  import imp1: interface {
+    use i1.{a};
+  }
+}
+
+
+package foo:name {
+  interface i2 {
+    type b = u32;
+  }
+
+  world w2 {
+    import imp2: interface {
+        use i2.{b};
+    }
+  }
+}
+
diff --git a/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit.result b/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit.result
new file mode 100644
index 00000000..6f71c3a3
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/mix-implicit-then-explicit-package.wit.result
@@ -0,0 +1,5 @@
+expected `world`, `interface` or `use`, found keyword `package`
+     --> tests/ui/parse-fail/mix-implicit-then-explicit-package.wit:12:1
+      |
+   12 | package foo:name {
+      | ^------
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit b/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit
new file mode 100644
index 00000000..fb0e456f
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit
@@ -0,0 +1,11 @@
+package foo:bar {
+  interface i {
+    use foo:qux/i.{};
+  }
+}
+
+package foo:qux {
+  interface i {
+    use foo:bar/i.{};
+  }
+}
diff --git a/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit.result b/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit.result
new file mode 100644
index 00000000..b596cf2d
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/multiple-package-inline-cycle.wit.result
@@ -0,0 +1,5 @@
+package depends on itself
+     --> tests/ui/parse-fail/multiple-package-inline-cycle.wit:3:9
+      |
+    3 |     use foo:qux/i.{};
+      |         ^------
\ No newline at end of file
diff --git a/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit b/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit
new file mode 100644
index 00000000..4ebb1692
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit
@@ -0,0 +1,15 @@
+package foo:name;
+
+interface i1 {
+  type a = u32;
+}
+
+package bar:name;
+
+world w1 {
+  import imp1: interface {
+    use foo:name/i1.{a};
+
+    fn: func(a: a);
+  }
+}
diff --git a/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit.result b/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit.result
new file mode 100644
index 00000000..2135ac63
--- /dev/null
+++ b/crates/wit-parser/tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit.result
@@ -0,0 +1,5 @@
+expected `world`, `interface` or `use`, found keyword `package`
+     --> tests/ui/parse-fail/multiple-packages-no-scope-blocks.wit:7:1
+      |
+    7 | package bar:name;
+      | ^------
\ No newline at end of file
diff --git a/tests/cli/print-core-wasm-wit-multiple-packages.wit b/tests/cli/print-core-wasm-wit-multiple-packages.wit
new file mode 100644
index 00000000..66a2ac2d
--- /dev/null
+++ b/tests/cli/print-core-wasm-wit-multiple-packages.wit
@@ -0,0 +1,21 @@
+// RUN: component embed --dummy --world=bar:bar/my-world % | component wit
+
+package foo:foo {
+  interface my-interface {
+    foo: func();
+  }
+
+  world my-world {
+    import my-interface;
+  }
+}
+
+package bar:bar {
+  interface my-interface {
+    foo: func();
+  }
+
+  world my-world {
+    import my-interface;
+  }
+}
diff --git a/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout b/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout
new file mode 100644
index 00000000..75feb032
--- /dev/null
+++ b/tests/cli/print-core-wasm-wit-multiple-packages.wit.stdout
@@ -0,0 +1,5 @@
+package root:root;
+
+world root {
+  import bar:bar/my-interface;
+}
diff --git a/tests/cli/semver-check-different-packages.wit b/tests/cli/semver-check-different-packages.wit
new file mode 100644
index 00000000..2a666e5e
--- /dev/null
+++ b/tests/cli/semver-check-different-packages.wit
@@ -0,0 +1,12 @@
+// FAIL: component semver-check % --prev a:b/prev --new c:d/new
+
+package a:b {
+  world prev {}
+}
+
+package c:d {
+  world new {
+    import a: func();
+    import b: interface {}
+  }
+}
diff --git a/tests/cli/semver-check-different-packages.wit.stderr b/tests/cli/semver-check-different-packages.wit.stderr
new file mode 100644
index 00000000..c9dfe93a
--- /dev/null
+++ b/tests/cli/semver-check-different-packages.wit.stderr
@@ -0,0 +1,1 @@
+error: the old world is in package a:b, which is not the same as the new world, which is in package c:d
diff --git a/tests/cli/semver-check-multiple-packages.wit b/tests/cli/semver-check-multiple-packages.wit
new file mode 100644
index 00000000..49f23c39
--- /dev/null
+++ b/tests/cli/semver-check-multiple-packages.wit
@@ -0,0 +1,23 @@
+// RUN: component semver-check % --prev a:b/prev --new a:b/next
+
+package a:b {
+  world prev {}
+
+  interface next-interface {
+
+  }
+
+  world next {
+    import a: func();
+    import b: interface {}
+    import next-interface;
+  }
+}
+
+package c:d {
+  world old {}
+
+  world new {
+    import a: func();
+  }
+}
diff --git a/tests/snapshots/local/producers.wast/5.print b/tests/snapshots/local/producers.wast/5.print
index 89b70f24..f7a0254e 100644
--- a/tests/snapshots/local/producers.wast/5.print
+++ b/tests/snapshots/local/producers.wast/5.print
@@ -1,6 +1,5 @@
 (module
-    ;; failed to parse custom section `producers`: unexpected end-of-file (at offset 0x15)
-    
-    (@custom "producers" (before first) "\01")
-  )
-  
\ No newline at end of file
+  (@producers)
+  ;; failed to parse custom section `producers`: unexpected end-of-file (at offset 0x15)
+  
+)
diff --git a/tests/snapshots/local/producers.wast/6.print b/tests/snapshots/local/producers.wast/6.print
index 67eba579..f03a5a59 100644
--- a/tests/snapshots/local/producers.wast/6.print
+++ b/tests/snapshots/local/producers.wast/6.print
@@ -1,7 +1,6 @@
 (module
-    ;; failed to parse custom section `producers`: invalid producers field name: `a
-    ;; a` (at offset 0x15)
-    
-    (@custom "producers" (before first) "\01\03a\0aa\01\01a\01a")
-  )
-  
\ No newline at end of file
+  (@producers)
+  ;; failed to parse custom section `producers`: invalid producers field name: `a
+  ;; a` (at offset 0x15)
+  
+)