Repository: bytecodealliance/wasm-tools
Pull request #1592 (2024-06-04 22:08:50)

Refactor `wasmprinter` to rely less on a `String` output
This commit aims at starting to refactor `wasmprinter` to be able to output to a stream rather than unconditionally to a `String`. The fact that the current output is always an in-memory `String` is leveraged in a number of places in the internals to undo prints or capture temporary prints. These were all the easiest thing to do at the time but do not play well with the idea of streaming output. This commit instead refactors `wasmprinter` to remove all these bits and be more naturally oriented around "just stream out bytes at that's it". This is achieved with some internal refactorings, reorganization, and juggling some responsibilities.

A future PR will switch the internals to actually be `std::io::Write` or, my final goal, `termcolor::WriteColor`


Patch:
diff --git a/crates/wasmparser/src/binary_reader.rs b/crates/wasmparser/src/binary_reader.rs
index e542db1c..99dfca38 100644
--- a/crates/wasmparser/src/binary_reader.rs
+++ b/crates/wasmparser/src/binary_reader.rs
@@ -1712,6 +1712,12 @@ impl<'a> BinaryReader<'a> {
         self.visit_operator(&mut OperatorFactory::new())
     }
 
+    /// Returns whether there is an `end` opcode followed by eof remaining in
+    /// this reader.
+    pub fn is_end_then_eof(&self) -> bool {
+        self.remaining_buffer() == &[0x0b]
+    }
+
     fn read_lane_index(&mut self, max: u8) -> Result<u8> {
         let index = self.read_u8()?;
         if index >= max {
diff --git a/crates/wasmparser/src/readers/core/operators.rs b/crates/wasmparser/src/readers/core/operators.rs
index 252df15d..69f8baec 100644
--- a/crates/wasmparser/src/readers/core/operators.rs
+++ b/crates/wasmparser/src/readers/core/operators.rs
@@ -209,6 +209,12 @@ impl<'a> OperatorsReader<'a> {
     pub fn get_binary_reader(&self) -> BinaryReader<'a> {
         self.reader.clone()
     }
+
+    /// Returns whether there is an `end` opcode followed by eof remaining in
+    /// this reader.
+    pub fn is_end_then_eof(&self) -> bool {
+        self.reader.is_end_then_eof()
+    }
 }
 
 impl<'a> IntoIterator for OperatorsReader<'a> {
diff --git a/crates/wasmprinter/src/lib.rs b/crates/wasmprinter/src/lib.rs
index 415751de..9dcc9d9b 100644
--- a/crates/wasmprinter/src/lib.rs
+++ b/crates/wasmprinter/src/lib.rs
@@ -1281,9 +1281,9 @@ impl Printer {
 
         let nesting_start = self.nesting;
 
-        let mut buf = String::new();
-        let mut op_printer = operator::PrintOperator::new(self, state);
-        while !body.eof() {
+        let mut op_printer =
+            operator::PrintOperator::new(self, state, operator::OperatorSeparator::Newline);
+        while !body.is_end_then_eof() {
             // Branch hints are stored in increasing order of their body offset
             // so print them whenever their instruction comes up.
             if let Some(((hint_offset, hint), rest)) = branch_hints.split_first() {
@@ -1299,48 +1299,8 @@ impl Printer {
                 }
             }
 
-            let offset = body.original_position();
-            mem::swap(&mut buf, &mut op_printer.printer.result);
-            let op_kind = body.visit_operator(&mut op_printer)??;
-            mem::swap(&mut buf, &mut op_printer.printer.result);
-
-            match op_kind {
-                // The final `end` in a reader is not printed, it's implied
-                // in the text format.
-                operator::OpKind::End if body.eof() => break,
-
-                // When we start a block we newline to the current
-                // indentation, then we increase the indentation so further
-                // instructions are tabbed over.
-                operator::OpKind::BlockStart => {
-                    op_printer.printer.newline(offset);
-                    op_printer.printer.nesting += 1;
-                }
-
-                // `else`/`catch` are special in that it's printed at
-                // the previous indentation, but it doesn't actually change
-                // our nesting level.
-                operator::OpKind::BlockMid => {
-                    op_printer.printer.nesting -= 1;
-                    op_printer.printer.newline(offset);
-                    op_printer.printer.nesting += 1;
-                }
-
-                // Exiting a block prints `end` at the previous indentation
-                // level.
-                operator::OpKind::End | operator::OpKind::Delegate
-                    if op_printer.printer.nesting > nesting_start =>
-                {
-                    op_printer.printer.nesting -= 1;
-                    op_printer.printer.newline(offset);
-                }
-
-                // .. otherwise everything else just has a normal newline
-                // out in front.
-                _ => op_printer.printer.newline(offset),
-            }
-            op_printer.printer.result.push_str(&buf);
-            buf.truncate(0);
+            op_printer.op_offset = body.original_position();
+            body.visit_operator(&mut op_printer)??;
         }
 
         // If this was an invalid function body then the nesting may not
@@ -1596,40 +1556,14 @@ impl Printer {
         explicit: &str,
     ) -> Result<()> {
         self.start_group("");
-        let mut prev = mem::take(&mut self.result);
         let mut reader = expr.get_operators_reader();
-        let mut op_printer = operator::PrintOperator::new(self, state);
-        let mut first_op = None;
-        for i in 0.. {
-            if reader.eof() {
-                break;
-            }
-            match reader.visit_operator(&mut op_printer)?? {
-                operator::OpKind::End if reader.eof() => {}
 
-                _ if i == 0 => first_op = Some(mem::take(&mut op_printer.printer.result)),
-
-                _ => {
-                    if i == 1 {
-                        prev.push_str(explicit);
-                        prev.push(' ');
-                        prev.push_str(&first_op.take().unwrap());
-                    }
-                    prev.push(' ');
-                    prev.push_str(&op_printer.printer.result);
-                }
-            }
-
-            op_printer.printer.result.truncate(0);
+        if reader.read().is_ok() && !reader.is_end_then_eof() {
+            write!(self.result, "{explicit} ")?;
         }
 
-        // If `first_op` is still set here then it means we don't need to print
-        // an expression with `explicit` as the leading token, instead we can
-        // print the single operator.
-        if let Some(op) = first_op {
-            prev.push_str(&op);
-        }
-        self.result = prev;
+        self.print_const_expr(state, expr)?;
+
         self.end_group();
         Ok(())
     }
@@ -1639,23 +1573,16 @@ impl Printer {
         let mut reader = expr.get_operators_reader();
         let mut first = true;
 
-        let mut result = mem::take(&mut self.result);
-        let mut op_printer = operator::PrintOperator::new(self, state);
-        while !reader.eof() {
+        let mut op_printer =
+            operator::PrintOperator::new(self, state, operator::OperatorSeparator::None);
+        while !reader.is_end_then_eof() {
             if first {
                 first = false;
             } else {
                 op_printer.printer.result.push(' ');
             }
-            match reader.visit_operator(&mut op_printer)?? {
-                operator::OpKind::End if reader.eof() => {}
-                _ => {
-                    result.push_str(&op_printer.printer.result);
-                    op_printer.printer.result.truncate(0);
-                }
-            }
+            reader.visit_operator(&mut op_printer)??;
         }
-        self.result = result;
         Ok(())
     }
 
diff --git a/crates/wasmprinter/src/operator.rs b/crates/wasmprinter/src/operator.rs
index 6912582a..1eb0e486 100644
--- a/crates/wasmprinter/src/operator.rs
+++ b/crates/wasmprinter/src/operator.rs
@@ -5,20 +5,33 @@ use wasmparser::{BlockType, BrTable, Catch, MemArg, Ordering, RefType, TryTable,
 
 pub struct PrintOperator<'a, 'b> {
     pub(super) printer: &'a mut Printer,
+    pub(super) op_offset: usize,
     nesting_start: u32,
     state: &'b mut State,
     label: u32,
     label_indices: Vec<u32>,
+    sep: OperatorSeparator,
+}
+
+pub enum OperatorSeparator {
+    Newline,
+    None,
 }
 
 impl<'a, 'b> PrintOperator<'a, 'b> {
-    pub(super) fn new(printer: &'a mut Printer, state: &'b mut State) -> PrintOperator<'a, 'b> {
+    pub(super) fn new(
+        printer: &'a mut Printer,
+        state: &'b mut State,
+        sep: OperatorSeparator,
+    ) -> PrintOperator<'a, 'b> {
         PrintOperator {
             nesting_start: printer.nesting,
+            op_offset: 0,
             printer,
             state,
             label: 0,
             label_indices: Vec::new(),
+            sep,
         }
     }
 
@@ -30,29 +43,35 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
         &mut self.printer.result
     }
 
-    /// This is called after every instruction and is used to manage the
-    /// `label_indices` stack.
-    fn update_label_stack(&mut self, kind: OpKind) {
-        match kind {
-            OpKind::Normal => {}
-
-            // The previous label was just defined, so add it to the stack.
-            OpKind::BlockStart => {
-                self.label_indices.push(self.label - 1);
+    fn separator(&mut self) {
+        match self.sep {
+            OperatorSeparator::Newline => {
+                self.printer.newline(self.op_offset);
             }
+            OperatorSeparator::None => {}
+        }
+    }
 
-            // The previous label is being defined at the same depth as the
-            // latest label, meaning it's overwriting its entry.
-            OpKind::BlockMid => {
-                if let Some(last) = self.label_indices.last_mut() {
-                    *last = self.label - 1;
-                }
-            }
+    /// Called just before an instruction that introduces a block such as
+    /// `block`, `if`, `loop`, etc.
+    fn block_start(&mut self) {
+        self.separator();
+        self.printer.nesting += 1;
+        self.label_indices.push(self.label);
+    }
 
-            // Label is out of scope so remove it from the stack.
-            OpKind::End | OpKind::Delegate => {
-                self.label_indices.pop();
-            }
+    /// Used for `else` and `delegate`
+    fn block_mid(&mut self) {
+        self.printer.nesting -= 1;
+        self.separator();
+        self.printer.nesting += 1;
+    }
+
+    /// Used for `end` to terminate the prior block.
+    fn block_end(&mut self) {
+        if self.printer.nesting > self.nesting_start {
+            self.printer.nesting -= 1;
+            self.separator();
         }
     }
 
@@ -62,11 +81,6 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn blockty_without_label_comment(&mut self, ty: BlockType) -> Result<bool> {
-        // Trim the trailing space, if any.
-        if self.result().ends_with(" ") {
-            self.result().pop();
-        }
-
         let key = (self.state.core.funcs, self.label);
         let has_name = match self.state.core.label_names.index_to_name.get(&key) {
             Some(name) => {
@@ -75,7 +89,10 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
                 true
             }
             None if self.printer.name_unnamed => {
-                let depth = self.cur_depth();
+                // 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.
+                let depth = self.cur_depth() - 1;
                 write!(self.result(), " $#label{depth}")?;
                 true
             }
@@ -101,10 +118,7 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
         if !has_name {
             let depth = self.cur_depth();
             self.push_str(" ");
-            // Note that 1 is added to the current depth here since if a block
-            // type is being printed then a block is being created which will
-            // increase the label depth of the block itself.
-            write!(self.result(), ";; label = @{}", depth + 1)?;
+            write!(self.result(), ";; label = @{}", depth)?;
         }
 
         self.label += 1;
@@ -116,11 +130,13 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn tag_index(&mut self, index: u32) -> Result<()> {
+        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(" ");
         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
@@ -194,29 +210,30 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn targets(&mut self, targets: BrTable<'_>) -> Result<()> {
-        for (i, item) in targets.targets().chain([Ok(targets.default())]).enumerate() {
-            if i > 0 {
-                self.push_str(" ");
-            }
+        for item in targets.targets().chain([Ok(targets.default())]) {
             self.relative_depth(item?)?;
         }
         Ok(())
     }
 
     fn function_index(&mut self, idx: u32) -> Result<()> {
+        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.printer
             .print_local_idx(self.state, self.state.core.funcs, idx)
     }
 
     fn global_index(&mut self, idx: u32) -> Result<()> {
+        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.printer.print_idx(&self.state.core.table_names, idx)
     }
 
@@ -225,6 +242,7 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn memory_index(&mut self, idx: u32) -> Result<()> {
+        self.push_str(" ");
         self.printer.print_idx(&self.state.core.memory_names, idx)
     }
 
@@ -234,74 +252,74 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     }
 
     fn array_type_index(&mut self, idx: u32) -> Result<()> {
+        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.printer.print_idx(&self.state.core.type_names, idx)
     }
 
     fn array_type_index_src(&mut self, idx: u32) -> Result<()> {
+        self.push_str(" ");
         self.printer.print_idx(&self.state.core.type_names, idx)
     }
 
     fn array_size(&mut self, array_size: u32) -> Result<()> {
-        write!(&mut self.printer.result, "{array_size}")?;
+        write!(&mut self.printer.result, " {array_size}")?;
         Ok(())
     }
 
     fn struct_type_index(&mut self, idx: u32) -> Result<()> {
+        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.printer.print_reftype(self.state, ref_ty)
     }
 
     fn to_ref_type(&mut self, ref_ty: RefType) -> Result<()> {
+        self.push_str(" ");
         self.printer.print_reftype(self.state, ref_ty)
     }
 
     fn data_index(&mut self, idx: u32) -> Result<()> {
+        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.printer.print_idx(&self.state.core.data_names, idx)
     }
 
     fn elem_index(&mut self, idx: u32) -> Result<()> {
+        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.printer.print_idx(&self.state.core.element_names, idx)
     }
 
     fn lane(&mut self, lane: u8) -> Result<()> {
-        write!(self.result(), "{lane}")?;
+        write!(self.result(), " {lane}")?;
         Ok(())
     }
 
     fn lanes(&mut self, lanes: [u8; 16]) -> Result<()> {
-        for (i, lane) in lanes.iter().enumerate() {
-            if i > 0 {
-                self.push_str(" ");
-            }
-            write!(self.result(), "{lane}")?;
+        for lane in lanes.iter() {
+            write!(self.result(), " {lane}")?;
         }
         Ok(())
     }
 
     fn memarg(&mut self, memarg: MemArg) -> Result<()> {
-        // Remove the leading ' ' inserted by the macro below since memarg may
-        // not actually print anything if all of its parameters are defaulted.
-        // Ideally we wouldn't rely on the ability to pop here but this ends up
-        // being the easiest.
-        assert_eq!(self.printer.result.pop(), Some(' '));
-
         if memarg.memory != 0 {
-            self.result().push(' ');
             self.memory_index(memarg.memory)?;
         }
         if memarg.offset != 0 {
@@ -320,7 +338,7 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
     fn ordering(&mut self, ordering: Ordering) -> Result<()> {
         write!(
             self.result(),
-            "{}",
+            " {}",
             match ordering {
                 Ordering::SeqCst => "seq_cst",
                 Ordering::AcqRel => "acq_rel",
@@ -335,52 +353,43 @@ impl<'a, 'b> PrintOperator<'a, 'b> {
         // Nesting has already been incremented but labels for catch start above
         // this `try_table` not at the `try_table`. Temporarily decrement this
         // nesting count and increase it below after printing catch clauses.
-        self.printer.nesting -= 1;
+        self.printer.nesting -= 2;
+        let try_table_label = self.label_indices.pop().unwrap();
 
         for catch in table.catches {
             self.result().push(' ');
             match catch {
                 Catch::One { tag, label } => {
-                    self.printer.start_group("catch ");
+                    self.printer.start_group("catch");
                     self.tag_index(tag)?;
-                    self.result().push(' ');
                     self.relative_depth(label)?;
                     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.result().push(' ');
                     self.relative_depth(label)?;
                     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();
                 }
                 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.nesting += 1;
+        self.label_indices.push(try_table_label);
+        self.printer.nesting += 2;
         self.maybe_blockty_label_comment(has_name)?;
         Ok(())
     }
 }
 
-#[derive(PartialEq, Copy, Clone)]
-pub enum OpKind {
-    BlockStart,
-    BlockMid,
-    End,
-    Delegate,
-    Normal,
-}
-
 macro_rules! define_visit {
     // General structure of all the operator printer methods:
     //
@@ -389,32 +398,37 @@ macro_rules! define_visit {
     // * Return the `OpKind`, as defined by this macro
     ($(@$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));
             $(
                 define_visit!(payload self $op $($arg)*);
             )?
-            let kind = define_visit!(kind $op);
-            if kind != OpKind::Normal {
-                self.update_label_stack(kind);
-            }
-            Ok(kind)
+
+            define_visit!(after_op self $op);
+            Ok(())
         }
     )*);
 
-    // Macro case to classify instructions based on their `$op` naming into an
-    // `OpKind`. There are a few special cases here but the vast majority of
-    // operators fall into the `Normal` category.
-    (kind Block) => (OpKind::BlockStart);
-    (kind Loop) => (OpKind::BlockStart);
-    (kind If) => (OpKind::BlockStart);
-    (kind Try) => (OpKind::BlockStart);
-    (kind TryTable) => (OpKind::BlockStart);
-    (kind Catch) => (OpKind::BlockMid);
-    (kind CatchAll) => (OpKind::BlockMid);
-    (kind Delegate) => (OpKind::Delegate);
-    (kind Else) => (OpKind::BlockMid);
-    (kind End) => (OpKind::End);
-    (kind $other:tt) => (OpKind::Normal);
+    // Control-flow related opcodes have special handling to manage nested
+    // 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(););
+
+    // After some opcodes the label stack is popped.
+    // (after_op $self:ident Delegate) => ($self.label_indices.pop(););
+    (after_op $self:ident End) => ($self.label_indices.pop(););
+    (after_op $self:ident $op:ident) => ();
 
     // How to print the payload of an instruction. There are a number of
     // instructions that have special cases such as avoiding printing anything
@@ -423,14 +437,12 @@ macro_rules! define_visit {
     // payload field.
     (payload $self:ident CallIndirect $ty:ident $table:ident) => (
         if $table != 0 {
-            $self.push_str(" ");
             $self.table_index($table)?;
         }
         $self.type_index($ty)?;
     );
     (payload $self:ident ReturnCallIndirect $ty:ident $table:ident) => (
         if $table != 0 {
-            $self.push_str(" ");
             $self.table_index($table)?;
         }
         $self.type_index($ty)?;
@@ -453,58 +465,46 @@ macro_rules! define_visit {
         $self.printer.print_heaptype($self.state, $hty)?;
     );
     (payload $self:ident TableInit $segment:ident $table:ident) => (
-        $self.push_str(" ");
         if $table != 0 {
             $self.table_index($table)?;
-            $self.push_str(" ");
         }
         $self.elem_index($segment)?;
     );
     (payload $self:ident TableCopy $dst:ident $src:ident) => (
         if $src != 0 || $dst != 0 {
-            $self.push_str(" ");
             $self.table_index($dst)?;
-            $self.push_str(" ");
             $self.table_index($src)?;
         }
     );
     (payload $self:ident MemoryGrow $mem:ident) => (
         if $mem != 0 {
-            $self.push_str(" ");
             $self.memory_index($mem)?;
         }
     );
     (payload $self:ident MemorySize $mem:ident) => (
         if $mem != 0 {
-            $self.push_str(" ");
             $self.memory_index($mem)?;
         }
     );
     (payload $self:ident MemoryInit $segment:ident $mem:ident) => (
         if $mem != 0 {
-            $self.push_str(" ");
             $self.memory_index($mem)?;
         }
-        $self.push_str(" ");
         $self.data_index($segment)?;
     );
     (payload $self:ident MemoryCopy $dst:ident $src:ident) => (
         if $src != 0 || $dst != 0 {
-            $self.push_str(" ");
             $self.memory_index($dst)?;
-            $self.push_str(" ");
             $self.memory_index($src)?;
         }
     );
     (payload $self:ident MemoryFill $mem:ident) => (
         if $mem != 0 {
-            $self.push_str(" ");
             $self.memory_index($mem)?;
         }
     );
     (payload $self:ident MemoryDiscard $mem:ident) => (
         if $mem != 0 {
-            $self.push_str(" ");
             $self.memory_index($mem)?;
         }
     );
@@ -556,34 +556,27 @@ macro_rules! define_visit {
         $self.printer.print_reftype($self.state, rty)?;
     );
     (payload $self:ident StructGet $ty:ident $field:ident) => (
-        $self.push_str(" ");
         $self.struct_type_index($ty)?;
         $self.push_str(" ");
         $self.printer.print_field_idx($self.state, $ty, $field)?;
     );
     (payload $self:ident StructGetS $ty:ident $field:ident) => (
-        $self.push_str(" ");
         $self.struct_type_index($ty)?;
         $self.push_str(" ");
         $self.printer.print_field_idx($self.state, $ty, $field)?;
     );
     (payload $self:ident StructGetU $ty:ident $field:ident) => (
-        $self.push_str(" ");
         $self.struct_type_index($ty)?;
         $self.push_str(" ");
         $self.printer.print_field_idx($self.state, $ty, $field)?;
     );
     (payload $self:ident StructSet $ty:ident $field:ident) => (
-        $self.push_str(" ");
         $self.struct_type_index($ty)?;
         $self.push_str(" ");
         $self.printer.print_field_idx($self.state, $ty, $field)?;
     );
     (payload $self:ident $op:ident $($arg:ident)*) => (
-        $(
-            $self.push_str(" ");
-            $self.$arg($arg)?;
-        )*
+        $($self.$arg($arg)?;)*
     );
 
     (name Block) => ("block");
@@ -1170,7 +1163,7 @@ macro_rules! define_visit {
 }
 
 impl<'a> VisitOperator<'a> for PrintOperator<'_, '_> {
-    type Output = Result<OpKind>;
+    type Output = Result<()>;
 
     wasmparser::for_each_operator!(define_visit);
 }


Test patch:
diff --git a/tests/cli/print-deprecated-exceptions2.wat.stdout b/tests/cli/print-deprecated-exceptions2.wat.stdout
index e7b40f2b..ec616143 100644
--- a/tests/cli/print-deprecated-exceptions2.wat.stdout
+++ b/tests/cli/print-deprecated-exceptions2.wat.stdout
@@ -32,7 +32,7 @@
   (func (;5;) (type 0)
     try ;; label = @1
       try ;; label = @2
-      delegate 0 (;@2;)
+      delegate 0 (;@1;)
     catch 0
     end
   )
diff --git a/tests/local/labels.wast b/tests/local/labels.wast
new file mode 100644
index 00000000..fd05940d
--- /dev/null
+++ b/tests/local/labels.wast
@@ -0,0 +1,33 @@
+(module
+  (func
+    br 0
+    try_table $a
+      br $a
+      br 1
+      loop $b
+        br $a
+        br $b
+        br 2
+        block $c
+          br $a
+          br $b
+          br $c
+          br 3
+          if $d
+            br $a
+            br $b
+            br $c
+            br $d
+            br 4
+          else
+            br $a
+            br $b
+            br $c
+            br $d
+            br 4
+          end
+        end
+      end
+    end
+  )
+)
diff --git a/tests/snapshots/local/labels.wast.json b/tests/snapshots/local/labels.wast.json
new file mode 100644
index 00000000..a1225ebd
--- /dev/null
+++ b/tests/snapshots/local/labels.wast.json
@@ -0,0 +1,10 @@
+{
+  "source_filename": "tests/local/labels.wast",
+  "commands": [
+    {
+      "type": "module",
+      "line": 1,
+      "filename": "labels.0.wasm"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/tests/snapshots/local/labels.wast/0.print b/tests/snapshots/local/labels.wast/0.print
new file mode 100644
index 00000000..347694e0
--- /dev/null
+++ b/tests/snapshots/local/labels.wast/0.print
@@ -0,0 +1,34 @@
+(module
+  (type (;0;) (func))
+  (func (;0;) (type 0)
+    br 0
+    try_table $a
+      br $a
+      br 1
+      loop $b
+        br $a
+        br $b
+        br 2
+        block $c
+          br $a
+          br $b
+          br $c
+          br 3
+          if $d
+            br $a
+            br $b
+            br $c
+            br $d
+            br 4
+          else
+            br $a
+            br $b
+            br $c
+            br $d
+            br 4
+          end
+        end
+      end
+    end
+  )
+)