Repository: bytecodealliance/wasm-tools (rust)

Issue:
Add option to show operand stack's types in disassembly
When debugging some fiddly wasm bits I often annotate the WAT disassembly with the types on the stack in between each instruction:

```wat
;; []
i32.const 32
;; [i32]
i32.const 10
;; [i32 i32]
i32.add
;; [i32]
```

Would be nice if we added a flag to `wasm-tools print` to do this automatically.

I guess `wasmprinter` can handle invalid Wasm at the moment, so we would have to maybe make this best effort or require valid input for when this is enabled or something.

Pull request:
wasm-tools print: add option to print operand stack between instructions
Closes #1381 (mentioned in https://github.com/bytecodealliance/wasm-tools/issues/2235#issuecomment-2968469805)

Example output:

![image](https://github.com/user-attachments/assets/c2f5a305-e837-4bdc-9cb8-b093255b844f)

Patch:
diff --git a/Cargo.toml b/Cargo.toml
index a08308c0..f990cc06 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -175,7 +175,7 @@ bitflags = { workspace = true, optional = true }
 rayon = { workspace = true, optional = true }
 
 # Dependencies of `print`
-wasmprinter = { workspace = true, features = ['component-model'] }
+wasmprinter = { workspace = true, features = ['component-model', 'validate'] }
 
 # Dependencies of `smith`
 arbitrary = { workspace = true, optional = true }
diff --git a/crates/wasmprinter/Cargo.toml b/crates/wasmprinter/Cargo.toml
index b868ce15..bfc881de 100644
--- a/crates/wasmprinter/Cargo.toml
+++ b/crates/wasmprinter/Cargo.toml
@@ -25,5 +25,6 @@ wasmparser = { workspace = true, features = ['std', 'simd'] }
 wat = { path = "../wat" }
 
 [features]
-default = ['component-model']
+default = ['component-model', 'validate']
 component-model = ['wasmparser/component-model']
+validate = ['wasmparser/validate', 'wasmparser/features']
diff --git a/crates/wasmprinter/src/lib.rs b/crates/wasmprinter/src/lib.rs
index dd0d21f8..c24683f2 100644
--- a/crates/wasmprinter/src/lib.rs
+++ b/crates/wasmprinter/src/lib.rs
@@ -7,6 +7,7 @@
 //! and debugging and such.
 
 #![deny(missing_docs)]
+#![cfg_attr(docsrs, feature(doc_auto_cfg))]
 
 use anyhow::{Context, Result, anyhow, bail};
 use operator::{OpPrinter, OperatorSeparator, OperatorState, PrintOperator, PrintOperatorFolded};
@@ -25,6 +26,12 @@ const MAX_WASM_FUNCTION_SIZE: u32 = 128 * 1024;
 
 #[cfg(feature = "component-model")]
 mod component;
+#[cfg(feature = "validate")]
+mod operand_stack;
+#[cfg(not(feature = "validate"))]
+mod operand_stack_disabled;
+#[cfg(not(feature = "validate"))]
+use operand_stack_disabled as operand_stack;
 mod operator;
 mod print;
 
@@ -57,6 +64,7 @@ pub struct Config {
     name_unnamed: bool,
     fold_instructions: bool,
     indent_text: String,
+    print_operand_stack: bool,
 }
 
 impl Default for Config {
@@ -67,6 +75,7 @@ impl Default for Config {
             name_unnamed: false,
             fold_instructions: false,
             indent_text: "  ".to_string(),
+            print_operand_stack: false,
         }
     }
 }
@@ -246,6 +255,30 @@ impl Config {
         self
     }
 
+    /// Print the operand stack types within function bodies,
+    /// flagging newly pushed operands when color output is enabled. E.g.:
+    ///
+    /// ```wasm
+    /// (module
+    ///   (type (;0;) (func))
+    ///   (func (;0;) (type 0)
+    ///     i32.const 4
+    ///     ;; [i32]
+    ///     i32.const 5
+    ///     ;; [i32 i32]
+    ///     i32.add
+    ///     ;; [i32]
+    ///     drop
+    ///     ;; []
+    ///   )
+    /// )
+    /// ```
+    #[cfg(feature = "validate")]
+    pub fn print_operand_stack(&mut self, enable: bool) -> &mut Self {
+        self.print_operand_stack = enable;
+        self
+    }
+
     /// Select the string to use when indenting.
     ///
     /// The indent allowed here are arbitrary and unchecked. You should enter
@@ -415,6 +448,12 @@ impl Printer<'_, '_> {
         #[cfg(feature = "component-model")]
         let mut parsers = Vec::new();
 
+        let mut validator = if self.config.print_operand_stack {
+            operand_stack::Validator::new()
+        } else {
+            None
+        };
+
         loop {
             let payload = match parser.parse(bytes, true)? {
                 Chunk::NeedMoreData(_) => unreachable!(),
@@ -423,6 +462,15 @@ impl Printer<'_, '_> {
                     payload
                 }
             };
+            if let Some(validator) = &mut validator {
+                match validator.payload(&payload) {
+                    Ok(()) => {}
+                    Err(e) => {
+                        self.newline_unknown_pos()?;
+                        write!(self.result, ";; module or component is invalid: {e}")?;
+                    }
+                }
+            }
             match payload {
                 Payload::Version { encoding, .. } => {
                     if let Some(e) = expected {
@@ -605,7 +653,11 @@ impl Printer<'_, '_> {
                     Self::ensure_module(&states)?;
                 }
                 Payload::CodeSectionEntry(body) => {
-                    self.print_code_section_entry(states.last_mut().unwrap(), &body)?;
+                    self.print_code_section_entry(
+                        states.last_mut().unwrap(),
+                        &body,
+                        validator.as_mut().and_then(|v| v.next_func()),
+                    )?;
                     self.update_custom_section_place(&mut states, "after code");
                 }
                 Payload::DataCountSection { .. } => {
@@ -699,6 +751,7 @@ impl Printer<'_, '_> {
                                 }
                             }
                             parser = parsers.pop().unwrap();
+
                             continue;
                         }
                     }
@@ -1328,6 +1381,7 @@ impl Printer<'_, '_> {
         &mut self,
         state: &mut State,
         body: &FunctionBody<'_>,
+        validator: Option<operand_stack::FuncValidator>,
     ) -> Result<()> {
         self.newline(body.get_binary_reader().original_position())?;
         self.start_group("func ")?;
@@ -1355,7 +1409,7 @@ impl Printer<'_, '_> {
         if self.config.print_skeleton {
             self.result.write_str(" ...")?;
         } else {
-            self.print_func_body(state, func_idx, params, &body, &hints)?;
+            self.print_func_body(state, func_idx, params, &body, &hints, validator)?;
         }
 
         self.end_group()?;
@@ -1370,6 +1424,7 @@ impl Printer<'_, '_> {
         params: u32,
         body: &FunctionBody<'_>,
         branch_hints: &[(usize, BranchHint)],
+        mut validator: Option<operand_stack::FuncValidator>,
     ) -> Result<()> {
         let mut first = true;
         let mut local_idx = 0;
@@ -1400,6 +1455,14 @@ impl Printer<'_, '_> {
         }
         locals.finish(self)?;
 
+        if let Some(f) = &mut validator {
+            if let Err(e) = f.read_locals(body.get_binary_reader()) {
+                validator = None;
+                self.newline_unknown_pos()?;
+                write!(self.result, ";; locals are invalid: {e}")?;
+            }
+        }
+
         let nesting_start = self.nesting;
         let fold_instructions = self.config.fold_instructions;
         let mut operator_state = OperatorState::new(self, OperatorSeparator::Newline);
@@ -1408,11 +1471,22 @@ impl Printer<'_, '_> {
             let mut folded_printer = PrintOperatorFolded::new(self, state, &mut operator_state);
             folded_printer.set_offset(func_start);
             folded_printer.begin_function(func_idx)?;
-            Self::print_operators(&mut reader, branch_hints, func_start, &mut folded_printer)?;
-            folded_printer.finalize()?;
+            Self::print_operators(
+                &mut reader,
+                branch_hints,
+                func_start,
+                &mut folded_printer,
+                validator,
+            )?;
         } else {
             let mut flat_printer = PrintOperator::new(self, state, &mut operator_state);
-            Self::print_operators(&mut reader, branch_hints, func_start, &mut flat_printer)?;
+            Self::print_operators(
+                &mut reader,
+                branch_hints,
+                func_start,
+                &mut flat_printer,
+                validator,
+            )?;
         }
 
         // If this was an invalid function body then the nesting may not
@@ -1433,12 +1507,24 @@ impl Printer<'_, '_> {
         mut branch_hints: &[(usize, BranchHint)],
         func_start: usize,
         op_printer: &mut O,
+        mut validator: Option<operand_stack::FuncValidator>,
     ) -> Result<()> {
         let mut ops = OperatorsReader::new(body.clone());
         while !ops.eof() {
             if ops.is_end_then_eof() {
+                let mut annotation = None;
+                if let Some(f) = &mut validator {
+                    match f.visit_operator(&ops, true) {
+                        Ok(()) => {}
+                        Err(_) => {
+                            annotation = Some(String::from("type mismatch at end of expression"))
+                        }
+                    }
+                }
+
                 ops.read()?; // final "end" opcode terminates instruction sequence
                 ops.finish()?;
+                op_printer.finalize(annotation.as_deref())?;
                 return Ok(());
             }
 
@@ -1450,9 +1536,22 @@ impl Printer<'_, '_> {
                     op_printer.branch_hint(*hint_offset, hint.taken)?;
                 }
             }
-
+            let mut annotation = None;
+            if let Some(f) = &mut validator {
+                let result = f
+                    .visit_operator(&ops, false)
+                    .map_err(anyhow::Error::from)
+                    .and_then(|()| f.visualize_operand_stack(op_printer.use_color()));
+                match result {
+                    Ok(s) => annotation = Some(s),
+                    Err(_) => {
+                        validator = None;
+                        annotation = Some(String::from("(invalid)"));
+                    }
+                }
+            }
             op_printer.set_offset(ops.original_position());
-            op_printer.visit_operator(&mut ops)?;
+            op_printer.visit_operator(&mut ops, annotation.as_deref())?;
         }
         ops.finish()?; // for the error message
         bail!("unexpected end of operators");
@@ -1726,12 +1825,10 @@ impl Printer<'_, '_> {
         if fold {
             let mut folded_printer = PrintOperatorFolded::new(self, state, &mut operator_state);
             folded_printer.begin_const_expr();
-            Self::print_operators(&mut reader, &[], 0, &mut folded_printer)?;
-            folded_printer.finalize()?;
+            Self::print_operators(&mut reader, &[], 0, &mut folded_printer, None)?;
         } else {
             let mut op_printer = PrintOperator::new(self, state, &mut operator_state);
-            op_printer.suppress_label_comments();
-            Self::print_operators(&mut reader, &[], 0, &mut op_printer)?;
+            Self::print_operators(&mut reader, &[], 0, &mut op_printer, None)?;
         }
 
         Ok(())
diff --git a/crates/wasmprinter/src/operand_stack.rs b/crates/wasmprinter/src/operand_stack.rs
new file mode 100644
index 00000000..924b483a
--- /dev/null
+++ b/crates/wasmprinter/src/operand_stack.rs
@@ -0,0 +1,128 @@
+use crate::{Config, PrintTermcolor, Printer};
+use wasmparser::{
+    BinaryReader, Frame, OperatorsReader, Payload, Result, ValType, ValidPayload,
+    ValidatorResources, WasmFeatures,
+};
+
+pub struct Validator {
+    validator: wasmparser::Validator,
+    last_function: Option<FuncValidator>,
+}
+
+impl Validator {
+    pub fn new() -> Option<Validator> {
+        Some(Validator {
+            validator: wasmparser::Validator::new_with_features(WasmFeatures::all()),
+            last_function: None,
+        })
+    }
+
+    pub fn payload(&mut self, payload: &Payload<'_>) -> Result<()> {
+        match self.validator.payload(payload)? {
+            ValidPayload::Ok | ValidPayload::End(_) | ValidPayload::Parser(_) => Ok(()),
+            ValidPayload::Func(validator, _) => {
+                assert!(self.last_function.is_none());
+                self.last_function = Some(FuncValidator {
+                    push_count: 0,
+                    validator: validator.into_validator(Default::default()),
+                });
+                Ok(())
+            }
+        }
+    }
+
+    pub fn next_func(&mut self) -> Option<FuncValidator> {
+        Some(self.last_function.take().unwrap())
+    }
+}
+
+pub struct FuncValidator {
+    validator: wasmparser::FuncValidator<ValidatorResources>,
+    push_count: u32,
+}
+
+impl FuncValidator {
+    pub fn read_locals(&mut self, mut reader: BinaryReader<'_>) -> Result<()> {
+        self.validator.read_locals(&mut reader)
+    }
+
+    pub fn visit_operator(&mut self, reader: &OperatorsReader<'_>, is_end: bool) -> Result<()> {
+        let pos = reader.original_position();
+        reader
+            .clone()
+            .visit_operator(&mut self.validator.visitor(pos))??;
+
+        if !is_end {
+            let op = reader.clone().read()?;
+            let arity = op.operator_arity(&self.validator.visitor(pos));
+            if let Some((_pop_count, push_count)) = arity {
+                self.push_count = push_count;
+            }
+        }
+        Ok(())
+    }
+
+    pub fn visualize_operand_stack(&self, use_color: bool) -> crate::Result<String> {
+        let mut buf_color = PrintTermcolor(termcolor::Ansi::new(Vec::new()));
+        let mut buf_nocolor = PrintTermcolor(termcolor::NoColor::new(Vec::new()));
+
+        let stack_printer = Printer {
+            result: if use_color {
+                &mut buf_color
+            } else {
+                &mut buf_nocolor
+            },
+            config: &Config::new(),
+            nesting: 0,
+            line: 0,
+            group_lines: Vec::new(),
+            code_section_hints: Vec::new(),
+        };
+
+        if let Some(&Frame { height, .. }) = self.validator.get_control_frame(0) {
+            stack_printer.result.start_comment()?;
+            stack_printer.result.write_str("[")?;
+            let max_height = self.validator.operand_stack_height() as usize;
+            for depth in (height..max_height).rev() {
+                if depth + 1 < max_height {
+                    stack_printer.result.write_str(" ")?;
+                }
+                if depth + 1 == height + self.push_count as usize {
+                    stack_printer.result.start_type()?;
+                }
+                match self.validator.get_operand_type(depth) {
+                    Some(Some(ty)) => {
+                        stack_printer.result.write_str(&ty_to_str(ty))?;
+                    }
+                    Some(None) => {
+                        stack_printer.result.write_str("(unknown)")?;
+                    }
+                    None => {
+                        stack_printer.result.write_str("(invalid)")?;
+                    }
+                }
+            }
+            stack_printer.result.start_comment()?;
+            stack_printer.result.write_str("]")?;
+            stack_printer.result.reset_color()?;
+        }
+
+        let ret = String::from_utf8(if use_color {
+            buf_color.0.into_inner()
+        } else {
+            buf_nocolor.0.into_inner()
+        })?;
+        return Ok(ret);
+
+        fn ty_to_str(ty: ValType) -> String {
+            match ty {
+                ValType::I32 => String::from("i32"),
+                ValType::I64 => String::from("i64"),
+                ValType::F32 => String::from("f32"),
+                ValType::F64 => String::from("f64"),
+                ValType::V128 => String::from("v128"),
+                ValType::Ref(r) => format!("{r}"),
+            }
+        }
+    }
+}
diff --git a/crates/wasmprinter/src/operand_stack_disabled.rs b/crates/wasmprinter/src/operand_stack_disabled.rs
new file mode 100644
index 00000000..85c75d21
--- /dev/null
+++ b/crates/wasmprinter/src/operand_stack_disabled.rs
@@ -0,0 +1,33 @@
+use wasmparser::{BinaryReader, OperatorsReader, Payload, Result};
+
+pub enum Validator {}
+
+impl Validator {
+    pub fn new() -> Option<Validator> {
+        None
+    }
+
+    pub fn payload(&mut self, _payload: &Payload<'_>) -> Result<()> {
+        match *self {}
+    }
+
+    pub fn next_func(&mut self) -> Option<FuncValidator> {
+        match *self {}
+    }
+}
+
+pub enum FuncValidator {}
+
+impl FuncValidator {
+    pub fn read_locals(&mut self, _reader: BinaryReader<'_>) -> Result<()> {
+        match *self {}
+    }
+
+    pub fn visit_operator(&mut self, _reader: &OperatorsReader<'_>, _is_end: bool) -> Result<()> {
+        match *self {}
+    }
+
+    pub fn visualize_operand_stack(&self, _use_color: bool) -> anyhow::Result<String> {
+        match *self {}
+    }
+}
diff --git a/crates/wasmprinter/src/operator.rs b/crates/wasmprinter/src/operator.rs
index f1e2991e..35cdc852 100644
--- a/crates/wasmprinter/src/operator.rs
+++ b/crates/wasmprinter/src/operator.rs
@@ -32,7 +32,6 @@ pub struct PrintOperator<'printer, 'state, 'a, 'b> {
     pub(super) printer: &'printer mut Printer<'a, 'b>,
     state: &'state mut State,
     operator_state: &'printer mut OperatorState,
-    suppress_label_comments: bool,
 }
 
 struct FoldedInstruction {
@@ -56,7 +55,6 @@ pub struct PrintOperatorFolded<'printer, 'state, 'a, 'b> {
     pub(super) printer: &'printer mut Printer<'a, 'b>,
     state: &'state mut State,
     operator_state: &'printer mut OperatorState,
-    suppress_label_comments: bool,
     control: Vec<Block>,
     branch_hint: Option<FoldedInstruction>,
     original_separator: OperatorSeparator,
@@ -80,7 +78,6 @@ impl<'printer, 'state, 'a, 'b> PrintOperator<'printer, 'state, 'a, 'b> {
             printer,
             state,
             operator_state,
-            suppress_label_comments: false,
         }
     }
 
@@ -174,11 +171,16 @@ impl<'printer, 'state, 'a, 'b> PrintOperator<'printer, 'state, 'a, 'b> {
     }
 
     fn maybe_blockty_label_comment(&mut self, has_name: bool) -> Result<()> {
-        if !has_name && !self.suppress_label_comments {
+        if !has_name {
             let depth = self.cur_depth();
             self.push_str(" ")?;
             self.result().start_comment()?;
-            write!(self.result(), ";; label = @{depth}")?;
+            match self.operator_state.sep {
+                OperatorSeparator::Newline | OperatorSeparator::None => {
+                    write!(self.result(), ";; label = @{depth}")
+                }
+                _ => write!(self.result(), " (; label = @{depth} ;)"),
+            }?;
             self.result().reset_color()?;
         }
 
@@ -1405,8 +1407,13 @@ impl<'a> VisitSimdOperator<'a> for PrintOperator<'_, '_, '_, '_> {
 pub trait OpPrinter {
     fn branch_hint(&mut self, offset: usize, taken: bool) -> Result<()>;
     fn set_offset(&mut self, offset: usize);
-    fn visit_operator(&mut self, reader: &mut OperatorsReader<'_>) -> Result<()>;
-    fn suppress_label_comments(&mut self);
+    fn visit_operator(
+        &mut self,
+        reader: &mut OperatorsReader<'_>,
+        annotation: Option<&str>,
+    ) -> Result<()>;
+    fn finalize(&mut self, annotation: Option<&str>) -> Result<()>;
+    fn use_color(&self) -> bool;
 }
 
 impl OpPrinter for PrintOperator<'_, '_, '_, '_> {
@@ -1423,12 +1430,33 @@ impl OpPrinter for PrintOperator<'_, '_, '_, '_> {
         self.operator_state.op_offset = offset;
     }
 
-    fn visit_operator(&mut self, reader: &mut OperatorsReader<'_>) -> Result<()> {
-        reader.visit_operator(self)?
+    fn visit_operator(
+        &mut self,
+        reader: &mut OperatorsReader<'_>,
+        annotation: Option<&str>,
+    ) -> Result<()> {
+        reader.visit_operator(self)??;
+        if let Some(s) = annotation {
+            self.printer.newline_unknown_pos()?;
+            self.result().start_comment()?;
+            write!(self.result(), ";; {s}")?;
+            self.result().reset_color()?;
+        }
+        Ok(())
+    }
+
+    fn finalize(&mut self, annotation: Option<&str>) -> Result<()> {
+        if let Some(s) = annotation {
+            self.printer.newline_unknown_pos()?;
+            self.result().start_comment()?;
+            write!(self.printer.result, ";; {s}")?;
+            self.result().reset_color()?;
+        }
+        Ok(())
     }
 
-    fn suppress_label_comments(&mut self) {
-        self.suppress_label_comments = true;
+    fn use_color(&self) -> bool {
+        self.printer.result.supports_async_color()
     }
 }
 
@@ -1450,14 +1478,13 @@ impl OpPrinter for PrintOperatorFolded<'_, '_, '_, '_> {
         self.operator_state.op_offset = offset;
     }
 
-    fn suppress_label_comments(&mut self) {
-        self.suppress_label_comments = true;
-    }
-
-    fn visit_operator(&mut self, reader: &mut OperatorsReader<'_>) -> Result<()> {
+    fn visit_operator(
+        &mut self,
+        reader: &mut OperatorsReader<'_>,
+        annotation: Option<&str>,
+    ) -> Result<()> {
         let operator = reader.clone().read()?;
         let (params, results) = operator.operator_arity(self).unwrap_or((0, 0));
-        let use_color = self.printer.result.supports_async_color();
         let mut buf_color = PrintTermcolor(Ansi::new(Vec::new()));
         let mut buf_nocolor = PrintTermcolor(NoColor::new(Vec::new()));
         let internal_config = Config {
@@ -1466,7 +1493,7 @@ impl OpPrinter for PrintOperatorFolded<'_, '_, '_, '_> {
         };
         let mut internal_printer = Printer {
             config: &internal_config,
-            result: if use_color {
+            result: if self.use_color() {
                 &mut buf_color
             } else {
                 &mut buf_nocolor
@@ -1480,11 +1507,18 @@ impl OpPrinter for PrintOperatorFolded<'_, '_, '_, '_> {
         let mut op_printer =
             PrintOperator::new(&mut internal_printer, self.state, self.operator_state);
         reader.visit_operator(&mut op_printer)??;
+        if let Some(s) = annotation {
+            internal_printer.result.start_comment()?;
+            write!(internal_printer.result, " (; {s}")?;
+            internal_printer.result.start_comment()?;
+            write!(internal_printer.result, " ;)")?;
+            internal_printer.result.reset_color()?;
+        }
 
         self.printer.nesting = internal_printer.nesting;
         self.printer.line = internal_printer.line;
 
-        let inst = String::from_utf8(if use_color {
+        let inst = String::from_utf8(if self.use_color() {
             buf_color.0.into_inner()
         } else {
             buf_nocolor.0.into_inner()
@@ -1510,6 +1544,27 @@ impl OpPrinter for PrintOperatorFolded<'_, '_, '_, '_> {
             _ => self.handle_plain(inst, params, results),
         }
     }
+
+    // Recurse through the stack and print each folded instruction.
+    fn finalize(&mut self, annotation: Option<&str>) -> Result<()> {
+        if self.control.len() != 1 {
+            bail!("instruction sequence not closed");
+        }
+        for inst in &self.control.last().unwrap().folded {
+            PrintOperatorFolded::print(&mut self.printer, &mut self.original_separator, &inst)?;
+        }
+        if let Some(s) = annotation {
+            self.printer.newline_unknown_pos()?;
+            self.printer.result.start_comment()?;
+            write!(self.printer.result, ";; {s}")?;
+            self.printer.result.reset_color()?;
+        }
+        Ok(())
+    }
+
+    fn use_color(&self) -> bool {
+        self.printer.result.supports_async_color()
+    }
 }
 
 impl ModuleArity for PrintOperatorFolded<'_, '_, '_, '_> {
@@ -1576,7 +1631,6 @@ impl<'printer, 'state, 'a, 'b> PrintOperatorFolded<'printer, 'state, 'a, 'b> {
             printer,
             state,
             operator_state,
-            suppress_label_comments: false,
             control: Vec::new(),
             branch_hint: None,
             original_separator,
@@ -1655,17 +1709,6 @@ impl<'printer, 'state, 'a, 'b> PrintOperatorFolded<'printer, 'state, 'a, 'b> {
         Ok(())
     }
 
-    // Recurse through the stack and print each folded instruction.
-    pub fn finalize(&mut self) -> Result<()> {
-        if self.control.len() != 1 {
-            bail!("instruction sequence not closed");
-        }
-        for inst in &self.control.last().unwrap().folded {
-            PrintOperatorFolded::print(&mut self.printer, &mut self.original_separator, &inst)?;
-        }
-        Ok(())
-    }
-
     // Print a folded instruction to the "real" printer. First print the "plain"
     // instruction, then recursively print each instruction that was folded in to the
     // foldedinst phrase.
diff --git a/crates/wit-encoder/src/render.rs b/crates/wit-encoder/src/render.rs
index 60a710f9..ac9dec5f 100644
--- a/crates/wit-encoder/src/render.rs
+++ b/crates/wit-encoder/src/render.rs
@@ -7,14 +7,14 @@ pub struct RenderOpts {
     /// width of each indent
     pub indent_width: usize,
     /// current indent depth
-    pub ident_count: usize,
+    pub indent_count: usize,
 }
 
 impl Default for RenderOpts {
     fn default() -> Self {
         Self {
             indent_width: 2,
-            ident_count: 0,
+            indent_count: 0,
         }
     }
 }
@@ -22,27 +22,27 @@ impl Default for RenderOpts {
 impl RenderOpts {
     /// Indent
     ///
-    /// This will clone Self, and increment self.ident_count.
+    /// This will clone Self, and increment self.indent_count.
     pub fn indent(&self) -> Self {
         Self {
             indent_width: self.indent_width,
-            ident_count: self.ident_count + 1,
+            indent_count: self.indent_count + 1,
         }
     }
 
     /// Outdent
     ///
-    /// This will clone Self, and decrement self.ident_count.
+    /// This will clone Self, and decrement self.indent_count.
     pub fn outdent(&self) -> Self {
         Self {
             indent_width: self.indent_width,
-            ident_count: self.ident_count - 1,
+            indent_count: self.indent_count - 1,
         }
     }
 
     /// Get the actual characters
     pub fn spaces(&self) -> String {
-        let space_count = self.indent_width * self.ident_count;
+        let space_count = self.indent_width * self.indent_count;
         " ".repeat(space_count)
     }
 }
diff --git a/src/bin/wasm-tools/print.rs b/src/bin/wasm-tools/print.rs
index cdb2dd81..94a60698 100644
--- a/src/bin/wasm-tools/print.rs
+++ b/src/bin/wasm-tools/print.rs
@@ -31,6 +31,10 @@ pub struct Opts {
     #[clap(short, long)]
     fold_instructions: bool,
 
+    /// Print the contents of the operand stack within function bodies
+    #[clap(long)]
+    print_operand_stack: bool,
+
     /// The string to use when indenting.
     #[clap(long)]
     indent_text: Option<String>,
@@ -52,6 +56,7 @@ impl Opts {
         config.print_skeleton(self.skeleton);
         config.name_unnamed(self.name_unnamed);
         config.fold_instructions(self.fold_instructions);
+        config.print_operand_stack(self.print_operand_stack);
         match self.indent_text.as_ref() {
             Some(s) => {
                 config.indent_text(s);


Test patch:
diff --git a/tests/cli/print-operand-stack-component.wat b/tests/cli/print-operand-stack-component.wat
new file mode 100644
index 00000000..dcc29dbe
--- /dev/null
+++ b/tests/cli/print-operand-stack-component.wat
@@ -0,0 +1,6 @@
+;; RUN: print --print-operand-stack %
+
+(component
+  (component (core module (func i32.const 0 drop)))
+  (component (core module (func i32.const 0 drop)))
+)
diff --git a/tests/cli/print-operand-stack-component.wat.stdout b/tests/cli/print-operand-stack-component.wat.stdout
new file mode 100644
index 00000000..7cc7610f
--- /dev/null
+++ b/tests/cli/print-operand-stack-component.wat.stdout
@@ -0,0 +1,24 @@
+(component
+  (component (;0;)
+    (core module (;0;)
+      (type (;0;) (func))
+      (func (;0;) (type 0)
+        i32.const 0
+        ;; [i32]
+        drop
+        ;; []
+      )
+    )
+  )
+  (component (;1;)
+    (core module (;0;)
+      (type (;0;) (func))
+      (func (;0;) (type 0)
+        i32.const 0
+        ;; [i32]
+        drop
+        ;; []
+      )
+    )
+  )
+)
diff --git a/tests/cli/print-operand-stack-folded.wat b/tests/cli/print-operand-stack-folded.wat
new file mode 100644
index 00000000..7a5aa574
--- /dev/null
+++ b/tests/cli/print-operand-stack-folded.wat
@@ -0,0 +1,47 @@
+;; RUN: print --print-operand-stack -f %
+
+(module
+  (type $ft (func))
+  (type $st (struct))
+  (type $at (array i8))
+
+  (table 10 anyref)
+
+  (elem declare func $f)
+  (func $f)
+
+  (func (export "init") (param $x externref)
+    (table.set (i32.const 0) (ref.null any))
+    (table.set (i32.const 1) (ref.i31 (i32.const 7)))
+    (table.set (i32.const 2) (struct.new_default $st))
+    (table.set (i32.const 3) (array.new_default $at (i32.const 0)))
+    (table.set (i32.const 4) (any.convert_extern (local.get $x)))
+    (table.set (i32.const 5) (ref.null i31))
+    (table.set (i32.const 6) (ref.null struct))
+    (table.set (i32.const 7) (ref.null none))
+  )
+
+  (func (export "ref_cast_non_null") (param $i i32)
+    (drop (ref.as_non_null (table.get (local.get $i))))
+    (drop (ref.cast (ref null any) (table.get (local.get $i))))
+  )
+  (func (export "ref_cast_null") (param $i i32)
+    (drop (ref.cast anyref (table.get (local.get $i))))
+    (drop (ref.cast structref (table.get (local.get $i))))
+    (drop (ref.cast arrayref (table.get (local.get $i))))
+    (drop (ref.cast i31ref (table.get (local.get $i))))
+    (drop (ref.cast nullref (table.get (local.get $i))))
+  )
+  (func (export "ref_cast_i31") (param $i i32)
+    (drop (ref.cast (ref i31) (table.get (local.get $i))))
+    (drop (ref.cast i31ref (table.get (local.get $i))))
+  )
+  (func (export "ref_cast_struct") (param $i i32)
+    (drop (ref.cast (ref struct) (table.get (local.get $i))))
+    (drop (ref.cast structref (table.get (local.get $i))))
+  )
+  (func (export "ref_cast_array") (param $i i32)
+    (drop (ref.cast (ref array) (table.get (local.get $i))))
+    (drop (ref.cast arrayref (table.get (local.get $i))))
+  )
+)
diff --git a/tests/cli/print-operand-stack-folded.wat.stdout b/tests/cli/print-operand-stack-folded.wat.stdout
new file mode 100644
index 00000000..13de435f
--- /dev/null
+++ b/tests/cli/print-operand-stack-folded.wat.stdout
@@ -0,0 +1,107 @@
+(module
+  (type $ft (;0;) (func))
+  (type $st (;1;) (struct))
+  (type $at (;2;) (array i8))
+  (type (;3;) (func (param externref)))
+  (type (;4;) (func (param i32)))
+  (table (;0;) 10 anyref)
+  (export "init" (func 1))
+  (export "ref_cast_non_null" (func 2))
+  (export "ref_cast_null" (func 3))
+  (export "ref_cast_i31" (func 4))
+  (export "ref_cast_struct" (func 5))
+  (export "ref_cast_array" (func 6))
+  (elem (;0;) declare func $f)
+  (func $f (;0;) (type $ft))
+  (func (;1;) (type 3) (param $x externref)
+    (table.set 0 (; [] ;)
+      (i32.const 0 (; [i32] ;))
+      (ref.null any (; [i32 anyref] ;)))
+    (table.set 0 (; [] ;)
+      (i32.const 1 (; [i32] ;))
+      (ref.i31 (; [i32 (ref i31)] ;)
+        (i32.const 7 (; [i32 i32] ;))))
+    (table.set 0 (; [] ;)
+      (i32.const 2 (; [i32] ;))
+      (struct.new_default $st (; [i32 (ref (id 1))] ;)))
+    (table.set 0 (; [] ;)
+      (i32.const 3 (; [i32] ;))
+      (array.new_default $at (; [i32 (ref (id 2))] ;)
+        (i32.const 0 (; [i32 i32] ;))))
+    (table.set 0 (; [] ;)
+      (i32.const 4 (; [i32] ;))
+      (any.convert_extern (; [i32 anyref] ;)
+        (local.get $x (; [i32 externref] ;))))
+    (table.set 0 (; [] ;)
+      (i32.const 5 (; [i32] ;))
+      (ref.null i31 (; [i32 i31ref] ;)))
+    (table.set 0 (; [] ;)
+      (i32.const 6 (; [i32] ;))
+      (ref.null struct (; [i32 structref] ;)))
+    (table.set 0 (; [] ;)
+      (i32.const 7 (; [i32] ;))
+      (ref.null none (; [i32 nullref] ;)))
+  )
+  (func (;2;) (type 4) (param $i i32)
+    (drop (; [] ;)
+      (ref.as_non_null (; [(ref any)] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+    (drop (; [] ;)
+      (ref.cast anyref (; [anyref] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+  )
+  (func (;3;) (type 4) (param $i i32)
+    (drop (; [] ;)
+      (ref.cast anyref (; [anyref] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+    (drop (; [] ;)
+      (ref.cast structref (; [structref] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+    (drop (; [] ;)
+      (ref.cast arrayref (; [arrayref] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+    (drop (; [] ;)
+      (ref.cast i31ref (; [i31ref] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+    (drop (; [] ;)
+      (ref.cast nullref (; [nullref] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+  )
+  (func (;4;) (type 4) (param $i i32)
+    (drop (; [] ;)
+      (ref.cast (ref i31) (; [(ref i31)] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+    (drop (; [] ;)
+      (ref.cast i31ref (; [i31ref] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+  )
+  (func (;5;) (type 4) (param $i i32)
+    (drop (; [] ;)
+      (ref.cast (ref struct) (; [(ref struct)] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+    (drop (; [] ;)
+      (ref.cast structref (; [structref] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+  )
+  (func (;6;) (type 4) (param $i i32)
+    (drop (; [] ;)
+      (ref.cast (ref array) (; [(ref array)] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+    (drop (; [] ;)
+      (ref.cast arrayref (; [arrayref] ;)
+        (table.get 0 (; [anyref] ;)
+          (local.get $i (; [i32] ;)))))
+  )
+)
diff --git a/tests/cli/print-operand-stack-invalid.wat b/tests/cli/print-operand-stack-invalid.wat
new file mode 100644
index 00000000..e4586946
--- /dev/null
+++ b/tests/cli/print-operand-stack-invalid.wat
@@ -0,0 +1,10 @@
+;; RUN: print --print-operand-stack %
+
+(module
+  (func (param i32)
+    local.get 0
+    i64.div_u
+    f32.load
+  )
+)
+
diff --git a/tests/cli/print-operand-stack-invalid.wat.stdout b/tests/cli/print-operand-stack-invalid.wat.stdout
new file mode 100644
index 00000000..5692bac7
--- /dev/null
+++ b/tests/cli/print-operand-stack-invalid.wat.stdout
@@ -0,0 +1,10 @@
+(module
+  (type (;0;) (func (param i32)))
+  (func (;0;) (type 0) (param i32)
+    local.get 0
+    ;; [i32]
+    i64.div_u
+    ;; (invalid)
+    f32.load
+  )
+)
diff --git a/tests/cli/print-operand-stack.wat b/tests/cli/print-operand-stack.wat
new file mode 100644
index 00000000..75dfc023
--- /dev/null
+++ b/tests/cli/print-operand-stack.wat
@@ -0,0 +1,47 @@
+;; RUN: print --print-operand-stack %
+
+(module
+  (type $ft (func))
+  (type $st (struct))
+  (type $at (array i8))
+
+  (table 10 anyref)
+
+  (elem declare func $f)
+  (func $f)
+
+  (func (export "init") (param $x externref)
+    (table.set (i32.const 0) (ref.null any))
+    (table.set (i32.const 1) (ref.i31 (i32.const 7)))
+    (table.set (i32.const 2) (struct.new_default $st))
+    (table.set (i32.const 3) (array.new_default $at (i32.const 0)))
+    (table.set (i32.const 4) (any.convert_extern (local.get $x)))
+    (table.set (i32.const 5) (ref.null i31))
+    (table.set (i32.const 6) (ref.null struct))
+    (table.set (i32.const 7) (ref.null none))
+  )
+
+  (func (export "ref_cast_non_null") (param $i i32)
+    (drop (ref.as_non_null (table.get (local.get $i))))
+    (drop (ref.cast (ref null any) (table.get (local.get $i))))
+  )
+  (func (export "ref_cast_null") (param $i i32)
+    (drop (ref.cast anyref (table.get (local.get $i))))
+    (drop (ref.cast structref (table.get (local.get $i))))
+    (drop (ref.cast arrayref (table.get (local.get $i))))
+    (drop (ref.cast i31ref (table.get (local.get $i))))
+    (drop (ref.cast nullref (table.get (local.get $i))))
+  )
+  (func (export "ref_cast_i31") (param $i i32)
+    (drop (ref.cast (ref i31) (table.get (local.get $i))))
+    (drop (ref.cast i31ref (table.get (local.get $i))))
+  )
+  (func (export "ref_cast_struct") (param $i i32)
+    (drop (ref.cast (ref struct) (table.get (local.get $i))))
+    (drop (ref.cast structref (table.get (local.get $i))))
+  )
+  (func (export "ref_cast_array") (param $i i32)
+    (drop (ref.cast (ref array) (table.get (local.get $i))))
+    (drop (ref.cast arrayref (table.get (local.get $i))))
+  )
+)
diff --git a/tests/cli/print-operand-stack.wat.stdout b/tests/cli/print-operand-stack.wat.stdout
new file mode 100644
index 00000000..e1e61b28
--- /dev/null
+++ b/tests/cli/print-operand-stack.wat.stdout
@@ -0,0 +1,186 @@
+(module
+  (type $ft (;0;) (func))
+  (type $st (;1;) (struct))
+  (type $at (;2;) (array i8))
+  (type (;3;) (func (param externref)))
+  (type (;4;) (func (param i32)))
+  (table (;0;) 10 anyref)
+  (export "init" (func 1))
+  (export "ref_cast_non_null" (func 2))
+  (export "ref_cast_null" (func 3))
+  (export "ref_cast_i31" (func 4))
+  (export "ref_cast_struct" (func 5))
+  (export "ref_cast_array" (func 6))
+  (elem (;0;) declare func $f)
+  (func $f (;0;) (type $ft))
+  (func (;1;) (type 3) (param $x externref)
+    i32.const 0
+    ;; [i32]
+    ref.null any
+    ;; [i32 anyref]
+    table.set 0
+    ;; []
+    i32.const 1
+    ;; [i32]
+    i32.const 7
+    ;; [i32 i32]
+    ref.i31
+    ;; [i32 (ref i31)]
+    table.set 0
+    ;; []
+    i32.const 2
+    ;; [i32]
+    struct.new_default $st
+    ;; [i32 (ref (id 1))]
+    table.set 0
+    ;; []
+    i32.const 3
+    ;; [i32]
+    i32.const 0
+    ;; [i32 i32]
+    array.new_default $at
+    ;; [i32 (ref (id 2))]
+    table.set 0
+    ;; []
+    i32.const 4
+    ;; [i32]
+    local.get $x
+    ;; [i32 externref]
+    any.convert_extern
+    ;; [i32 anyref]
+    table.set 0
+    ;; []
+    i32.const 5
+    ;; [i32]
+    ref.null i31
+    ;; [i32 i31ref]
+    table.set 0
+    ;; []
+    i32.const 6
+    ;; [i32]
+    ref.null struct
+    ;; [i32 structref]
+    table.set 0
+    ;; []
+    i32.const 7
+    ;; [i32]
+    ref.null none
+    ;; [i32 nullref]
+    table.set 0
+    ;; []
+  )
+  (func (;2;) (type 4) (param $i i32)
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.as_non_null
+    ;; [(ref any)]
+    drop
+    ;; []
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast anyref
+    ;; [anyref]
+    drop
+    ;; []
+  )
+  (func (;3;) (type 4) (param $i i32)
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast anyref
+    ;; [anyref]
+    drop
+    ;; []
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast structref
+    ;; [structref]
+    drop
+    ;; []
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast arrayref
+    ;; [arrayref]
+    drop
+    ;; []
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast i31ref
+    ;; [i31ref]
+    drop
+    ;; []
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast nullref
+    ;; [nullref]
+    drop
+    ;; []
+  )
+  (func (;4;) (type 4) (param $i i32)
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast (ref i31)
+    ;; [(ref i31)]
+    drop
+    ;; []
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast i31ref
+    ;; [i31ref]
+    drop
+    ;; []
+  )
+  (func (;5;) (type 4) (param $i i32)
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast (ref struct)
+    ;; [(ref struct)]
+    drop
+    ;; []
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast structref
+    ;; [structref]
+    drop
+    ;; []
+  )
+  (func (;6;) (type 4) (param $i i32)
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast (ref array)
+    ;; [(ref array)]
+    drop
+    ;; []
+    local.get $i
+    ;; [i32]
+    table.get 0
+    ;; [anyref]
+    ref.cast arrayref
+    ;; [arrayref]
+    drop
+    ;; []
+  )
+)