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 description:
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
+    ;; []
+  )
+)