Repository: tetratelabs/wazero
Pull request #1204 (2023-03-06 06:34:27)

gojs: implements chown
This finishes the last remaining syscalls in `GOOS=js`. After this is merged, further bugs are easier to hunt down as we know ENOSYS is not expected on writeable file systems.


Patch:
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index 55d3d3a0..29117564 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -575,8 +575,8 @@ func (jsfsChown) invoke(ctx context.Context, mod api.Module, args ...interface{}
 	gid := goos.ValueToUint32(args[2])
 	callback := args[3].(funcWrapper)
 
-	_, _, _ = path, uid, gid // TODO
-	var err error = syscall.ENOSYS
+	fsc := mod.(*wasm.CallContext).Sys.FS()
+	err := fsc.RootFS().Chown(path, int(uid), int(gid))
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
@@ -592,8 +592,14 @@ func (jsfsFchown) invoke(ctx context.Context, mod api.Module, args ...interface{
 	gid := goos.ValueToUint32(args[2])
 	callback := args[3].(funcWrapper)
 
-	_, _, _ = fd, uid, gid // TODO
-	var err error = syscall.ENOSYS
+	// Check to see if the file descriptor is available
+	fsc := mod.(*wasm.CallContext).Sys.FS()
+	var err error
+	if f, ok := fsc.LookupFile(fd); !ok {
+		err = syscall.EBADF
+	} else {
+		err = platform.ChownFile(f.File, int(uid), int(gid))
+	}
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
@@ -609,8 +615,8 @@ func (jsfsLchown) invoke(ctx context.Context, mod api.Module, args ...interface{
 	gid := goos.ValueToUint32(args[2])
 	callback := args[3].(funcWrapper)
 
-	_, _, _ = path, uid, gid // TODO
-	var err error = syscall.ENOSYS
+	fsc := mod.(*wasm.CallContext).Sys.FS()
+	err := fsc.RootFS().Lchown(path, int(uid), int(gid))
 
 	return jsfsInvoke(ctx, mod, callback, err)
 }
diff --git a/internal/platform/chown.go b/internal/platform/chown.go
new file mode 100644
index 00000000..fd169025
--- /dev/null
+++ b/internal/platform/chown.go
@@ -0,0 +1,40 @@
+package platform
+
+import (
+	"io/fs"
+	"os"
+	"syscall"
+)
+
+// Chown is like os.Chown, except it returns a syscall.Errno, not a
+// fs.PathError. For example, this returns syscall.ENOENT if the path doesn't
+// exist. See https://linux.die.net/man/3/chown
+//
+// Note: This always returns syscall.ENOSYS on windows.
+func Chown(path string, uid, gid int) error {
+	err := os.Chown(path, uid, gid)
+	return UnwrapOSError(err)
+}
+
+// Lchown is like os.Lchown, except it returns a syscall.Errno, not a
+// fs.PathError. For example, this returns syscall.ENOENT if the path doesn't
+// exist. See https://linux.die.net/man/3/lchown
+//
+// Note: This always returns syscall.ENOSYS on windows.
+func Lchown(path string, uid, gid int) error {
+	err := os.Lchown(path, uid, gid)
+	return UnwrapOSError(err)
+}
+
+// ChownFile is like syscall.Fchown, but for nanosecond precision and
+// fs.File instead of a file descriptor. This returns syscall.EBADF if the file
+// or directory was closed. See https://linux.die.net/man/3/fchown
+//
+// Note: This always returns syscall.ENOSYS on windows.
+func ChownFile(f fs.File, uid, gid int) error {
+	if f, ok := f.(fdFile); ok {
+		err := fchown(f.Fd(), uid, gid)
+		return UnwrapOSError(err)
+	}
+	return syscall.ENOSYS
+}
diff --git a/internal/platform/chown_unix.go b/internal/platform/chown_unix.go
new file mode 100644
index 00000000..69bdf9a4
--- /dev/null
+++ b/internal/platform/chown_unix.go
@@ -0,0 +1,9 @@
+//go:build !windows
+
+package platform
+
+import "syscall"
+
+func fchown(fd uintptr, uid, gid int) error {
+	return syscall.Fchown(int(fd), uid, gid)
+}
diff --git a/internal/platform/chown_unsupported.go b/internal/platform/chown_unsupported.go
new file mode 100644
index 00000000..1fec3803
--- /dev/null
+++ b/internal/platform/chown_unsupported.go
@@ -0,0 +1,11 @@
+//go:build windows
+
+package platform
+
+import "syscall"
+
+// fchown is not supported on windows. For example, syscall.Fchown returns
+// syscall.EWINDOWS, which is the same as syscall.ENOSYS.
+func fchown(fd uintptr, uid, gid int) error {
+	return syscall.ENOSYS
+}
diff --git a/internal/sysfs/dirfs.go b/internal/sysfs/dirfs.go
index 5ab4b9a5..cedbbf81 100644
--- a/internal/sysfs/dirfs.go
+++ b/internal/sysfs/dirfs.go
@@ -56,8 +56,8 @@ func (d *dirFS) Stat(path string, stat *platform.Stat_t) error {
 }
 
 // Mkdir implements FS.Mkdir
-func (d *dirFS) Mkdir(name string, perm fs.FileMode) (err error) {
-	err = os.Mkdir(d.join(name), perm)
+func (d *dirFS) Mkdir(path string, perm fs.FileMode) (err error) {
+	err = os.Mkdir(d.join(path), perm)
 	if err = platform.UnwrapOSError(err); err == syscall.ENOTDIR {
 		err = syscall.ENOENT
 	}
@@ -65,11 +65,21 @@ func (d *dirFS) Mkdir(name string, perm fs.FileMode) (err error) {
 }
 
 // Chmod implements FS.Chmod
-func (d *dirFS) Chmod(name string, perm fs.FileMode) error {
-	err := os.Chmod(d.join(name), perm)
+func (d *dirFS) Chmod(path string, perm fs.FileMode) error {
+	err := os.Chmod(d.join(path), perm)
 	return platform.UnwrapOSError(err)
 }
 
+// Chown implements FS.Chown
+func (d *dirFS) Chown(path string, uid, gid int) error {
+	return platform.Chown(d.join(path), uid, gid)
+}
+
+// Lchown implements FS.Lchown
+func (d *dirFS) Lchown(path string, uid, gid int) error {
+	return platform.Lchown(d.join(path), uid, gid)
+}
+
 // Rename implements FS.Rename
 func (d *dirFS) Rename(from, to string) error {
 	from, to = d.join(from), d.join(to)
@@ -95,14 +105,14 @@ func (d *dirFS) Link(oldName, newName string) error {
 }
 
 // Rmdir implements FS.Rmdir
-func (d *dirFS) Rmdir(name string) error {
-	err := syscall.Rmdir(d.join(name))
+func (d *dirFS) Rmdir(path string) error {
+	err := syscall.Rmdir(d.join(path))
 	return platform.UnwrapOSError(err)
 }
 
 // Unlink implements FS.Unlink
-func (d *dirFS) Unlink(name string) (err error) {
-	return platform.Unlink(d.join(name))
+func (d *dirFS) Unlink(path string) (err error) {
+	return platform.Unlink(d.join(path))
 }
 
 // Symlink implements FS.Symlink
@@ -115,8 +125,8 @@ func (d *dirFS) Symlink(oldName, link string) (err error) {
 }
 
 // Utimes implements FS.Utimes
-func (d *dirFS) Utimes(name string, atimeNsec, mtimeNsec int64) error {
-	err := syscall.UtimesNano(d.join(name), []syscall.Timespec{
+func (d *dirFS) Utimes(path string, atimeNsec, mtimeNsec int64) error {
+	err := syscall.UtimesNano(d.join(path), []syscall.Timespec{
 		syscall.NsecToTimespec(atimeNsec),
 		syscall.NsecToTimespec(mtimeNsec),
 	})
@@ -124,19 +134,19 @@ func (d *dirFS) Utimes(name string, atimeNsec, mtimeNsec int64) error {
 }
 
 // Truncate implements FS.Truncate
-func (d *dirFS) Truncate(name string, size int64) error {
+func (d *dirFS) Truncate(path string, size int64) error {
 	// Use os.Truncate as syscall.Truncate doesn't exist on Windows.
-	err := os.Truncate(d.join(name), size)
+	err := os.Truncate(d.join(path), size)
 	return platform.UnwrapOSError(err)
 }
 
-func (d *dirFS) join(name string) string {
-	switch name {
+func (d *dirFS) join(path string) string {
+	switch path {
 	case "", ".", "/":
 		// cleanedDir includes an unnecessary delimiter for the root path.
 		return d.cleanedDir[:len(d.cleanedDir)-1]
 	}
-	// TODO: Enforce similar to safefilepath.FromFS(name), but be careful as
-	// relative path inputs are allowed. e.g. dir or name == ../
-	return d.cleanedDir + name
+	// TODO: Enforce similar to safefilepath.FromFS(path), but be careful as
+	// relative path inputs are allowed. e.g. dir or path == ../
+	return d.cleanedDir + path
 }
diff --git a/internal/sysfs/rootfs.go b/internal/sysfs/rootfs.go
index f39841cb..4864de2f 100644
--- a/internal/sysfs/rootfs.go
+++ b/internal/sysfs/rootfs.go
@@ -246,6 +246,12 @@ func (d *openRootDir) ReadDir(count int) ([]fs.DirEntry, error) {
 	return list, nil
 }
 
+// Lstat implements FS.Lstat
+func (c *CompositeFS) Lstat(path string, stat *platform.Stat_t) error {
+	matchIndex, relativePath := c.chooseFS(path)
+	return c.fs[matchIndex].Lstat(relativePath, stat)
+}
+
 // Stat implements FS.Stat
 func (c *CompositeFS) Stat(path string, stat *platform.Stat_t) error {
 	matchIndex, relativePath := c.chooseFS(path)
@@ -258,6 +264,24 @@ func (c *CompositeFS) Mkdir(path string, perm fs.FileMode) error {
 	return c.fs[matchIndex].Mkdir(relativePath, perm)
 }
 
+// Chmod implements FS.Chmod
+func (c *CompositeFS) Chmod(path string, perm fs.FileMode) error {
+	matchIndex, relativePath := c.chooseFS(path)
+	return c.fs[matchIndex].Chmod(relativePath, perm)
+}
+
+// Chown implements FS.Chown
+func (c *CompositeFS) Chown(path string, uid, gid int) error {
+	matchIndex, relativePath := c.chooseFS(path)
+	return c.fs[matchIndex].Chown(relativePath, uid, gid)
+}
+
+// Lchown implements FS.Lchown
+func (c *CompositeFS) Lchown(path string, uid, gid int) error {
+	matchIndex, relativePath := c.chooseFS(path)
+	return c.fs[matchIndex].Lchown(relativePath, uid, gid)
+}
+
 // Rename implements FS.Rename
 func (c *CompositeFS) Rename(from, to string) error {
 	fromFS, fromPath := c.chooseFS(from)
@@ -268,6 +292,44 @@ func (c *CompositeFS) Rename(from, to string) error {
 	return c.fs[fromFS].Rename(fromPath, toPath)
 }
 
+// Readlink implements FS.Readlink
+func (c *CompositeFS) Readlink(path string) (string, error) {
+	matchIndex, relativePath := c.chooseFS(path)
+	return c.fs[matchIndex].Readlink(relativePath)
+}
+
+// Link implements FS.Link.
+func (c *CompositeFS) Link(oldName, newName string) error {
+	fromFS, oldNamePath := c.chooseFS(oldName)
+	toFS, newNamePath := c.chooseFS(newName)
+	if fromFS != toFS {
+		return syscall.ENOSYS // not yet anyway
+	}
+	return c.fs[fromFS].Link(oldNamePath, newNamePath)
+}
+
+// Utimes implements FS.Utimes
+func (c *CompositeFS) Utimes(path string, atimeNsec, mtimeNsec int64) error {
+	matchIndex, relativePath := c.chooseFS(path)
+	return c.fs[matchIndex].Utimes(relativePath, atimeNsec, mtimeNsec)
+}
+
+// Symlink implements FS.Symlink
+func (c *CompositeFS) Symlink(oldName, link string) (err error) {
+	fromFS, oldNamePath := c.chooseFS(oldName)
+	toFS, linkPath := c.chooseFS(link)
+	if fromFS != toFS {
+		return syscall.ENOSYS // not yet anyway
+	}
+	return c.fs[fromFS].Link(oldNamePath, linkPath)
+}
+
+// Truncate implements FS.Truncate
+func (c *CompositeFS) Truncate(path string, size int64) error {
+	matchIndex, relativePath := c.chooseFS(path)
+	return c.fs[matchIndex].Truncate(relativePath, size)
+}
+
 // Rmdir implements FS.Rmdir
 func (c *CompositeFS) Rmdir(path string) error {
 	matchIndex, relativePath := c.chooseFS(path)
@@ -280,12 +342,6 @@ func (c *CompositeFS) Unlink(path string) error {
 	return c.fs[matchIndex].Unlink(relativePath)
 }
 
-// Utimes implements FS.Utimes
-func (c *CompositeFS) Utimes(path string, atimeNsec, mtimeNsec int64) error {
-	matchIndex, relativePath := c.chooseFS(path)
-	return c.fs[matchIndex].Utimes(relativePath, atimeNsec, mtimeNsec)
-}
-
 // chooseFS chooses the best fs and the relative path to use for the input.
 func (c *CompositeFS) chooseFS(path string) (matchIndex int, relativePath string) {
 	matchIndex = -1
diff --git a/internal/sysfs/sysfs.go b/internal/sysfs/sysfs.go
index 906bedaa..235d7270 100644
--- a/internal/sysfs/sysfs.go
+++ b/internal/sysfs/sysfs.go
@@ -128,6 +128,37 @@ type FS interface {
 	// ^^ TODO: Consider syscall.Chmod, though this implies defining and
 	// coercing flags and perms similar to what is done in os.Chmod.
 
+	// Chown is like os.Chown except the path is relative to this file
+	// system, and syscall.Errno are returned instead of an os.PathError.
+	//
+	// # Errors
+	//
+	// The following errors are expected:
+	//   - syscall.EINVAL: `path` is invalid.
+	//   - syscall.ENOENT: `path` does not exist.
+	//
+	// # Notes
+	//
+	//   - Windows will always return syscall.ENOSYS
+	//   - This is similar to https://linux.die.net/man/3/chown
+	Chown(path string, uid, gid int) error
+
+	// Lchown is like os.Lchown except the path is relative to this file
+	// system, and syscall.Errno are returned instead of a os.PathError.
+	//	See https://linux.die.net/man/3/lchown
+	//
+	// # Errors
+	//
+	// The following errors are expected:
+	//   - syscall.EINVAL: `path` is invalid.
+	//   - syscall.ENOENT: `path` does not exist.
+	//
+	// # Notes
+	//
+	//   - Windows will always return syscall.ENOSYS
+	//   - This is similar to https://linux.die.net/man/3/lchown
+	Lchown(path string, uid, gid int) error
+
 	// Rename is similar to syscall.Rename, except the path is relative to this
 	// file system.
 	//
diff --git a/internal/sysfs/unsupported.go b/internal/sysfs/unsupported.go
index 21b956be..7cf2899b 100644
--- a/internal/sysfs/unsupported.go
+++ b/internal/sysfs/unsupported.go
@@ -46,6 +46,16 @@ func (UnimplementedFS) Chmod(path string, perm fs.FileMode) error {
 	return syscall.ENOSYS
 }
 
+// Chown implements FS.Chown
+func (UnimplementedFS) Chown(path string, uid, gid int) error {
+	return syscall.ENOSYS
+}
+
+// Lchown implements FS.Lchown
+func (UnimplementedFS) Lchown(path string, uid, gid int) error {
+	return syscall.ENOSYS
+}
+
 // Rename implements FS.Rename
 func (UnimplementedFS) Rename(from, to string) error {
 	return syscall.ENOSYS
diff --git a/internal/wazeroir/compiler.go b/internal/wazeroir/compiler.go
index 0d2cdb47..da913885 100644
--- a/internal/wazeroir/compiler.go
+++ b/internal/wazeroir/compiler.go
@@ -5,7 +5,6 @@ import (
 	"encoding/binary"
 	"fmt"
 	"math"
-	"os"
 	"strings"
 
 	"github.com/tetratelabs/wazero/api"
@@ -3045,10 +3044,6 @@ func (c *compiler) emit(ops ...Operation) {
 				c.result.IROperationSourceOffsetsInWasmBinary = append(c.result.IROperationSourceOffsetsInWasmBinary,
 					c.currentOpPC+c.bodyOffsetInCodeSection)
 			}
-			if false {
-				fmt.Printf("emitting ")
-				formatOperation(os.Stdout, op)
-			}
 		}
 	}
 }
diff --git a/internal/wazeroir/format.go b/internal/wazeroir/format.go
index ea8e3fd1..152cfd4a 100644
--- a/internal/wazeroir/format.go
+++ b/internal/wazeroir/format.go
@@ -2,9 +2,6 @@ package wazeroir
 
 import (
 	"bytes"
-	"fmt"
-	"io"
-	"strings"
 )
 
 const EntrypointLabel = ".entrypoint"
@@ -14,193 +11,13 @@ func Format(ops []Operation) string {
 
 	_, _ = buf.WriteString(EntrypointLabel + "\n")
 	for _, op := range ops {
-		formatOperation(buf, op)
-	}
-
-	return buf.String()
-}
-
-func formatOperation(w io.StringWriter, b Operation) {
-	var str string
-	var isLabel bool
-	switch o := b.(type) {
-	case OperationUnreachable:
-		str = "unreachable"
-	case OperationLabel:
-		isLabel = true
-		str = fmt.Sprintf("%s:", o.Label)
-	case OperationBr:
-		str = fmt.Sprintf("br %s", o.Target.String())
-	case OperationBrIf:
-		str = fmt.Sprintf("br_if %s, %s", o.Then, o.Else)
-	case OperationBrTable:
-		targets := make([]string, len(o.Targets))
-		for i, t := range o.Targets {
-			targets[i] = t.String()
-		}
-		str = fmt.Sprintf("br_table [%s] %s", strings.Join(targets, ","), o.Default)
-	case OperationCall:
-		str = fmt.Sprintf("call %d", o.FunctionIndex)
-	case OperationCallIndirect:
-		str = fmt.Sprintf("call_indirect: type=%d, table=%d", o.TypeIndex, o.TableIndex)
-	case OperationDrop:
-		str = fmt.Sprintf("drop %d..%d", o.Depth.Start, o.Depth.End)
-	case OperationSelect:
-		str = "select"
-	case OperationPick:
-		str = fmt.Sprintf("pick %d (is_vector=%v)", o.Depth, o.IsTargetVector)
-	case OperationSet:
-		str = fmt.Sprintf("swap %d (is_vector=%v)", o.Depth, o.IsTargetVector)
-	case OperationGlobalGet:
-		str = fmt.Sprintf("global.get %d", o.Index)
-	case OperationGlobalSet:
-		str = fmt.Sprintf("global.set %d", o.Index)
-	case OperationLoad:
-		str = fmt.Sprintf("%s.load (align=%d, offset=%d)", o.Type, o.Arg.Alignment, o.Arg.Offset)
-	case OperationLoad8:
-		str = fmt.Sprintf("%s.load8 (align=%d, offset=%d)", o.Type, o.Arg.Alignment, o.Arg.Offset)
-	case OperationLoad16:
-		str = fmt.Sprintf("%s.load16 (align=%d, offset=%d)", o.Type, o.Arg.Alignment, o.Arg.Offset)
-	case OperationLoad32:
-		var t string
-		if o.Signed {
-			t = "i64"
-		} else {
-			t = "u64"
+		str := op.String()
+		isLabel := op.Kind() == OperationKindLabel
+		if !isLabel {
+			const indent = "\t"
+			str = indent + str
 		}
-		str = fmt.Sprintf("%s.load32 (align=%d, offset=%d)", t, o.Arg.Alignment, o.Arg.Offset)
-	case OperationStore:
-		str = fmt.Sprintf("%s.store (align=%d, offset=%d)", o.Type, o.Arg.Alignment, o.Arg.Offset)
-	case OperationStore8:
-		str = fmt.Sprintf("store8 (align=%d, offset=%d)", o.Arg.Alignment, o.Arg.Offset)
-	case OperationStore16:
-		str = fmt.Sprintf("store16 (align=%d, offset=%d)", o.Arg.Alignment, o.Arg.Offset)
-	case OperationStore32:
-		str = fmt.Sprintf("i64.store32 (align=%d, offset=%d)", o.Arg.Alignment, o.Arg.Offset)
-	case OperationMemorySize:
-		str = "memory.size"
-	case OperationMemoryGrow:
-		str = "memory.grow"
-	case OperationConstI32:
-		str = fmt.Sprintf("i32.const %d", o.Value)
-	case OperationConstI64:
-		str = fmt.Sprintf("i64.const %d", o.Value)
-	case OperationConstF32:
-		str = fmt.Sprintf("f32.const %f", o.Value)
-	case OperationConstF64:
-		str = fmt.Sprintf("f64.const %f", o.Value)
-	case OperationEq:
-		str = fmt.Sprintf("%s.eq", o.Type)
-	case OperationNe:
-		str = fmt.Sprintf("%s.ne", o.Type)
-	case OperationEqz:
-		str = fmt.Sprintf("%s.eqz", o.Type)
-	case OperationLt:
-		str = fmt.Sprintf("%s.lt", o.Type)
-	case OperationGt:
-		str = fmt.Sprintf("%s.gt", o.Type)
-	case OperationLe:
-		str = fmt.Sprintf("%s.le", o.Type)
-	case OperationGe:
-		str = fmt.Sprintf("%s.ge", o.Type)
-	case OperationAdd:
-		str = fmt.Sprintf("%s.add", o.Type)
-	case OperationSub:
-		str = fmt.Sprintf("%s.sub", o.Type)
-	case OperationMul:
-		str = fmt.Sprintf("%s.mul", o.Type)
-	case OperationClz:
-		str = fmt.Sprintf("%s.clz", o.Type)
-	case OperationCtz:
-		str = fmt.Sprintf("%s.ctz", o.Type)
-	case OperationPopcnt:
-		str = fmt.Sprintf("%s.popcnt", o.Type)
-	case OperationDiv:
-		str = fmt.Sprintf("%s.div", o.Type)
-	case OperationRem:
-		str = fmt.Sprintf("%s.rem", o.Type)
-	case OperationAnd:
-		str = fmt.Sprintf("%s.and", o.Type)
-	case OperationOr:
-		str = fmt.Sprintf("%s.or", o.Type)
-	case OperationXor:
-		str = fmt.Sprintf("%s.xor", o.Type)
-	case OperationShl:
-		str = fmt.Sprintf("%s.shl", o.Type)
-	case OperationShr:
-		str = fmt.Sprintf("%s.shr", o.Type)
-	case OperationRotl:
-		str = fmt.Sprintf("%s.rotl", o.Type)
-	case OperationRotr:
-		str = fmt.Sprintf("%s.rotr", o.Type)
-	case OperationAbs:
-		str = fmt.Sprintf("%s.abs", o.Type)
-	case OperationNeg:
-		str = fmt.Sprintf("%s.neg", o.Type)
-	case OperationCeil:
-		str = fmt.Sprintf("%s.ceil", o.Type)
-	case OperationFloor:
-		str = fmt.Sprintf("%s.floor", o.Type)
-	case OperationTrunc:
-		str = fmt.Sprintf("%s.trunc", o.Type)
-	case OperationNearest:
-		str = fmt.Sprintf("%s.nearest", o.Type)
-	case OperationSqrt:
-		str = fmt.Sprintf("%s.sqrt", o.Type)
-	case OperationMin:
-		str = fmt.Sprintf("%s.min", o.Type)
-	case OperationMax:
-		str = fmt.Sprintf("%s.max", o.Type)
-	case OperationCopysign:
-		str = fmt.Sprintf("%s.copysign", o.Type)
-	case OperationI32WrapFromI64:
-		str = "i32.wrap_from.i64"
-	case OperationITruncFromF:
-		str = fmt.Sprintf("%s.truncate_from.%s", o.OutputType, o.InputType)
-	case OperationFConvertFromI:
-		str = fmt.Sprintf("%s.convert_from.%s", o.OutputType, o.InputType)
-	case OperationF32DemoteFromF64:
-		str = "f32.demote_from.f64"
-	case OperationF64PromoteFromF32:
-		str = "f64.promote_from.f32"
-	case OperationI32ReinterpretFromF32:
-		str = "i32.reinterpret_from.f32"
-	case OperationI64ReinterpretFromF64:
-		str = "i64.reinterpret_from.f64"
-	case OperationF32ReinterpretFromI32:
-		str = "f32.reinterpret_from.i32"
-	case OperationF64ReinterpretFromI64:
-		str = "f64.reinterpret_from.i64"
-	case OperationExtend:
-		var in, out string
-		if o.Signed {
-			in = "i32"
-			out = "i64"
-		} else {
-			in = "u32"
-			out = "u64"
-		}
-		str = fmt.Sprintf("%s.extend_from.%s", out, in)
-	case OperationV128Const:
-		str = fmt.Sprintf("v128.const [%#x, %#x]", o.Lo, o.Hi)
-	case OperationV128Add:
-		str = fmt.Sprintf("v128.add (shape=%s)", shapeName(o.Shape))
-	case OperationV128ITruncSatFromF:
-		if o.Signed {
-			str = fmt.Sprintf("v128.ITruncSatFrom%sS", shapeName(o.OriginShape))
-		} else {
-			str = fmt.Sprintf("v128.ITruncSatFrom%sU", shapeName(o.OriginShape))
-		}
-	case OperationBuiltinFunctionCheckExitCode:
-		str = "builtin_function.check_closed"
-	default:
-		panic("unreachable: a bug in wazeroir implementation")
-	}
-
-	if !isLabel {
-		const indent = "\t"
-		str = indent + str
+		_, _ = buf.WriteString(str + "\n")
 	}
-
-	_, _ = w.WriteString(str + "\n")
+	return buf.String()
 }
diff --git a/internal/wazeroir/operations.go b/internal/wazeroir/operations.go
index be514393..f4271b2f 100644
--- a/internal/wazeroir/operations.go
+++ b/internal/wazeroir/operations.go
@@ -2,6 +2,7 @@ package wazeroir
 
 import (
 	"fmt"
+	"strings"
 )
 
 // UnsignedInt represents unsigned 32-bit or 64-bit integers.
@@ -133,7 +134,7 @@ func (s SignedType) String() (ret string) {
 type Operation interface {
 	// Kind returns the kind of the implementation.
 	Kind() OperationKind
-	// TODO String()
+	fmt.Stringer
 }
 
 // OperationKind is the kind of each implementation of Operation interface.
@@ -714,12 +715,157 @@ const (
 	operationKindEnd
 )
 
+var (
+	_ Operation = OperationUnreachable{}
+	_ Operation = OperationLabel{}
+	_ Operation = OperationBr{}
+	_ Operation = OperationBrIf{}
+	_ Operation = OperationBrTable{}
+	_ Operation = OperationCall{}
+	_ Operation = OperationCallIndirect{}
+	_ Operation = OperationDrop{}
+	_ Operation = OperationSelect{}
+	_ Operation = OperationPick{}
+	_ Operation = OperationSet{}
+	_ Operation = OperationGlobalGet{}
+	_ Operation = OperationGlobalSet{}
+	_ Operation = OperationLoad{}
+	_ Operation = OperationLoad8{}
+	_ Operation = OperationLoad16{}
+	_ Operation = OperationLoad32{}
+	_ Operation = OperationStore{}
+	_ Operation = OperationStore8{}
+	_ Operation = OperationStore16{}
+	_ Operation = OperationStore32{}
+	_ Operation = OperationMemorySize{}
+	_ Operation = OperationMemoryGrow{}
+	_ Operation = OperationConstI32{}
+	_ Operation = OperationConstI64{}
+	_ Operation = OperationConstF32{}
+	_ Operation = OperationConstF64{}
+	_ Operation = OperationEq{}
+	_ Operation = OperationNe{}
+	_ Operation = OperationEqz{}
+	_ Operation = OperationLt{}
+	_ Operation = OperationGt{}
+	_ Operation = OperationLe{}
+	_ Operation = OperationGe{}
+	_ Operation = OperationAdd{}
+	_ Operation = OperationSub{}
+	_ Operation = OperationMul{}
+	_ Operation = OperationClz{}
+	_ Operation = OperationCtz{}
+	_ Operation = OperationPopcnt{}
+	_ Operation = OperationDiv{}
+	_ Operation = OperationRem{}
+	_ Operation = OperationAnd{}
+	_ Operation = OperationOr{}
+	_ Operation = OperationXor{}
+	_ Operation = OperationShl{}
+	_ Operation = OperationShr{}
+	_ Operation = OperationRotl{}
+	_ Operation = OperationRotr{}
+	_ Operation = OperationAbs{}
+	_ Operation = OperationNeg{}
+	_ Operation = OperationCeil{}
+	_ Operation = OperationFloor{}
+	_ Operation = OperationTrunc{}
+	_ Operation = OperationNearest{}
+	_ Operation = OperationSqrt{}
+	_ Operation = OperationMin{}
+	_ Operation = OperationMax{}
+	_ Operation = OperationCopysign{}
+	_ Operation = OperationI32WrapFromI64{}
+	_ Operation = OperationITruncFromF{}
+	_ Operation = OperationFConvertFromI{}
+	_ Operation = OperationF32DemoteFromF64{}
+	_ Operation = OperationF64PromoteFromF32{}
+	_ Operation = OperationI32ReinterpretFromF32{}
+	_ Operation = OperationI64ReinterpretFromF64{}
+	_ Operation = OperationF32ReinterpretFromI32{}
+	_ Operation = OperationF64ReinterpretFromI64{}
+	_ Operation = OperationExtend{}
+	_ Operation = OperationSignExtend32From8{}
+	_ Operation = OperationSignExtend32From16{}
+	_ Operation = OperationSignExtend64From8{}
+	_ Operation = OperationSignExtend64From16{}
+	_ Operation = OperationSignExtend64From32{}
+	_ Operation = OperationMemoryInit{}
+	_ Operation = OperationDataDrop{}
+	_ Operation = OperationMemoryCopy{}
+	_ Operation = OperationMemoryFill{}
+	_ Operation = OperationTableInit{}
+	_ Operation = OperationElemDrop{}
+	_ Operation = OperationTableCopy{}
+	_ Operation = OperationRefFunc{}
+	_ Operation = OperationTableGet{}
+	_ Operation = OperationTableSet{}
+	_ Operation = OperationTableSize{}
+	_ Operation = OperationTableGrow{}
+	_ Operation = OperationTableFill{}
+	_ Operation = OperationV128Const{}
+	_ Operation = OperationV128Add{}
+	_ Operation = OperationV128Sub{}
+	_ Operation = OperationV128Load{}
+	_ Operation = OperationV128LoadLane{}
+	_ Operation = OperationV128Store{}
+	_ Operation = OperationV128StoreLane{}
+	_ Operation = OperationV128ExtractLane{}
+	_ Operation = OperationV128ReplaceLane{}
+	_ Operation = OperationV128Splat{}
+	_ Operation = OperationV128Shuffle{}
+	_ Operation = OperationV128Swizzle{}
+	_ Operation = OperationV128AnyTrue{}
+	_ Operation = OperationV128AllTrue{}
+	_ Operation = OperationV128BitMask{}
+	_ Operation = OperationV128And{}
+	_ Operation = OperationV128Not{}
+	_ Operation = OperationV128Or{}
+	_ Operation = OperationV128Xor{}
+	_ Operation = OperationV128Bitselect{}
+	_ Operation = OperationV128AndNot{}
+	_ Operation = OperationV128Shl{}
+	_ Operation = OperationV128Shr{}
+	_ Operation = OperationV128Cmp{}
+	_ Operation = OperationV128AddSat{}
+	_ Operation = OperationV128SubSat{}
+	_ Operation = OperationV128Mul{}
+	_ Operation = OperationV128Div{}
+	_ Operation = OperationV128Neg{}
+	_ Operation = OperationV128Sqrt{}
+	_ Operation = OperationV128Abs{}
+	_ Operation = OperationV128Popcnt{}
+	_ Operation = OperationV128Min{}
+	_ Operation = OperationV128Max{}
+	_ Operation = OperationV128AvgrU{}
+	_ Operation = OperationV128Pmin{}
+	_ Operation = OperationV128Pmax{}
+	_ Operation = OperationV128Ceil{}
+	_ Operation = OperationV128Floor{}
+	_ Operation = OperationV128Trunc{}
+	_ Operation = OperationV128Nearest{}
+	_ Operation = OperationV128Extend{}
+	_ Operation = OperationV128ExtMul{}
+	_ Operation = OperationV128Q15mulrSatS{}
+	_ Operation = OperationV128ExtAddPairwise{}
+	_ Operation = OperationV128FloatPromote{}
+	_ Operation = OperationV128FloatDemote{}
+	_ Operation = OperationV128FConvertFromI{}
+	_ Operation = OperationV128Dot{}
+	_ Operation = OperationV128Narrow{}
+	_ Operation = OperationV128ITruncSatFromF{}
+	_ Operation = OperationBuiltinFunctionCheckExitCode{}
+)
+
 // OperationBuiltinFunctionCheckExitCode implements Operation.
 //
 // OperationBuiltinFunctionCheckExitCode corresponds to the instruction to check the api.Module is already closed due to
 // context.DeadlineExceeded, context.Canceled, or the explicit call of CloseWithExitCode on api.Module.
 type OperationBuiltinFunctionCheckExitCode struct{}
 
+// String implements fmt.Stringer.
+func (o OperationBuiltinFunctionCheckExitCode) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationBuiltinFunctionCheckExitCode) Kind() OperationKind {
 	return OperationKindBuiltinFunctionCheckExitCode
@@ -809,6 +955,9 @@ func (b BranchTargetDrop) String() (ret string) {
 // The engines are expected to exit the execution with wasmruntime.ErrRuntimeUnreachable error.
 type OperationUnreachable struct{}
 
+// String implements fmt.Stringer.
+func (o OperationUnreachable) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationUnreachable) Kind() OperationKind {
 	return OperationKindUnreachable
@@ -821,6 +970,9 @@ type OperationLabel struct {
 	Label Label
 }
 
+// String implements fmt.Stringer.
+func (o OperationLabel) String() string { return o.Label.String() }
+
 // Kind implements Operation.Kind
 func (OperationLabel) Kind() OperationKind {
 	return OperationKindLabel
@@ -833,6 +985,9 @@ type OperationBr struct {
 	Target Label
 }
 
+// String implements fmt.Stringer.
+func (o OperationBr) String() string { return fmt.Sprintf("%s %s", o.Kind(), o.Target.String()) }
+
 // Kind implements Operation.Kind
 func (OperationBr) Kind() OperationKind {
 	return OperationKindBr
@@ -846,6 +1001,9 @@ type OperationBrIf struct {
 	Then, Else BranchTargetDrop
 }
 
+// String implements fmt.Stringer.
+func (o OperationBrIf) String() string { return fmt.Sprintf("%s %s, %s", o.Kind(), o.Then, o.Else) }
+
 // Kind implements Operation.Kind
 func (OperationBrIf) Kind() OperationKind {
 	return OperationKindBrIf
@@ -869,6 +1027,15 @@ type OperationBrTable struct {
 	Default *BranchTargetDrop
 }
 
+// String implements fmt.Stringer.
+func (o OperationBrTable) String() string {
+	targets := make([]string, len(o.Targets))
+	for i, t := range o.Targets {
+		targets[i] = t.String()
+	}
+	return fmt.Sprintf("%s [%s] %s", o.Kind(), strings.Join(targets, ","), o.Default)
+}
+
 // Kind implements Operation.Kind
 func (OperationBrTable) Kind() OperationKind {
 	return OperationKindBrTable
@@ -882,6 +1049,11 @@ type OperationCall struct {
 	FunctionIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationCall) String() string {
+	return fmt.Sprintf("%s %d", o.Kind(), o.FunctionIndex)
+}
+
 // Kind implements Operation.Kind
 func (OperationCall) Kind() OperationKind {
 	return OperationKindCall
@@ -903,6 +1075,11 @@ type OperationCallIndirect struct {
 	TypeIndex, TableIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationCallIndirect) String() string {
+	return fmt.Sprintf("%s: type=%d, table=%d", o.Kind(), o.TypeIndex, o.TableIndex)
+}
+
 // Kind implements Operation.Kind
 func (OperationCallIndirect) Kind() OperationKind {
 	return OperationKindCallIndirect
@@ -923,6 +1100,11 @@ type OperationDrop struct {
 	Depth *InclusiveRange
 }
 
+// String implements fmt.Stringer.
+func (o OperationDrop) String() string {
+	return fmt.Sprintf("%s %d..%d", o.Kind(), o.Depth.Start, o.Depth.End)
+}
+
 // Kind implements Operation.Kind
 func (OperationDrop) Kind() OperationKind {
 	return OperationKindDrop
@@ -939,6 +1121,9 @@ type OperationSelect struct {
 	IsTargetVector bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationSelect) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationSelect) Kind() OperationKind {
 	return OperationKindSelect
@@ -955,6 +1140,11 @@ type OperationPick struct {
 	IsTargetVector bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationPick) String() string {
+	return fmt.Sprintf("%s %d (is_vector=%v)", o.Kind(), o.Depth, o.IsTargetVector)
+}
+
 // Kind implements Operation.Kind
 func (OperationPick) Kind() OperationKind {
 	return OperationKindPick
@@ -971,6 +1161,11 @@ type OperationSet struct {
 	IsTargetVector bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationSet) String() string {
+	return fmt.Sprintf("%s %d (is_vector=%v)", o.Kind(), o.Depth, o.IsTargetVector)
+}
+
 // Kind implements Operation.Kind
 func (OperationSet) Kind() OperationKind {
 	return OperationKindSet
@@ -984,6 +1179,11 @@ func (OperationSet) Kind() OperationKind {
 // See wasm.OpcodeGlobalGet.
 type OperationGlobalGet struct{ Index uint32 }
 
+// String implements fmt.Stringer.
+func (o OperationGlobalGet) String() string {
+	return fmt.Sprintf("%s %d", o.Kind(), o.Index)
+}
+
 // Kind implements Operation.Kind
 func (OperationGlobalGet) Kind() OperationKind {
 	return OperationKindGlobalGet
@@ -997,6 +1197,11 @@ func (OperationGlobalGet) Kind() OperationKind {
 // See wasm.OpcodeGlobalSet.
 type OperationGlobalSet struct{ Index uint32 }
 
+// String implements fmt.Stringer.
+func (o OperationGlobalSet) String() string {
+	return fmt.Sprintf("%s %d", o.Kind(), o.Index)
+}
+
 // Kind implements Operation.Kind
 func (OperationGlobalSet) Kind() OperationKind {
 	return OperationKindGlobalSet
@@ -1028,6 +1233,11 @@ type OperationLoad struct {
 	Arg  MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationLoad) String() string {
+	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", o.Type, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+}
+
 // Kind implements Operation.Kind
 func (OperationLoad) Kind() OperationKind {
 	return OperationKindLoad
@@ -1044,6 +1254,11 @@ type OperationLoad8 struct {
 	Arg  MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationLoad8) String() string {
+	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", o.Type, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+}
+
 // Kind implements Operation.Kind
 func (OperationLoad8) Kind() OperationKind {
 	return OperationKindLoad8
@@ -1060,6 +1275,11 @@ type OperationLoad16 struct {
 	Arg  MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationLoad16) String() string {
+	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", o.Type, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+}
+
 // Kind implements Operation.Kind
 func (OperationLoad16) Kind() OperationKind {
 	return OperationKindLoad16
@@ -1076,6 +1296,17 @@ type OperationLoad32 struct {
 	Arg    MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationLoad32) String() string {
+	var t string
+	if o.Signed {
+		t = "i64"
+	} else {
+		t = "u64"
+	}
+	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", t, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+}
+
 // Kind implements Operation.Kind
 func (OperationLoad32) Kind() OperationKind {
 	return OperationKindLoad32
@@ -1092,6 +1323,11 @@ type OperationStore struct {
 	Arg  MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationStore) String() string {
+	return fmt.Sprintf("%s.%s (align=%d, offset=%d)", o.Type, o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+}
+
 // Kind implements Operation.Kind
 func (OperationStore) Kind() OperationKind {
 	return OperationKindStore
@@ -1107,6 +1343,11 @@ type OperationStore8 struct {
 	Arg MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationStore8) String() string {
+	return fmt.Sprintf("%s (align=%d, offset=%d)", o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+}
+
 // Kind implements Operation.Kind
 func (OperationStore8) Kind() OperationKind {
 	return OperationKindStore8
@@ -1122,6 +1363,11 @@ type OperationStore16 struct {
 	Arg MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationStore16) String() string {
+	return fmt.Sprintf("%s (align=%d, offset=%d)", o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+}
+
 // Kind implements Operation.Kind
 func (OperationStore16) Kind() OperationKind {
 	return OperationKindStore16
@@ -1137,6 +1383,11 @@ type OperationStore32 struct {
 	Arg MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationStore32) String() string {
+	return fmt.Sprintf("%s (align=%d, offset=%d)", o.Kind(), o.Arg.Alignment, o.Arg.Offset)
+}
+
 // Kind implements Operation.Kind.
 func (OperationStore32) Kind() OperationKind {
 	return OperationKindStore32
@@ -1149,6 +1400,9 @@ func (OperationStore32) Kind() OperationKind {
 // The engines are expected to push the current page size of the memory onto the stack.
 type OperationMemorySize struct{}
 
+// String implements fmt.Stringer.
+func (o OperationMemorySize) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationMemorySize) Kind() OperationKind {
 	return OperationKindMemorySize
@@ -1157,6 +1411,9 @@ func (OperationMemorySize) Kind() OperationKind {
 // OperationMemoryGrow implements Operation.
 type OperationMemoryGrow struct{ Alignment uint64 }
 
+// String implements fmt.Stringer.
+func (o OperationMemoryGrow) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 //
 // This corresponds to wasm.OpcodeMemoryGrow.
@@ -1173,6 +1430,11 @@ func (OperationMemoryGrow) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32Const.
 type OperationConstI32 struct{ Value uint32 }
 
+// String implements fmt.Stringer.
+func (o OperationConstI32) String() string {
+	return fmt.Sprintf("%s %#x", o.Kind(), o.Value)
+}
+
 // Kind implements Operation.Kind.
 func (OperationConstI32) Kind() OperationKind {
 	return OperationKindConstI32
@@ -1183,6 +1445,11 @@ func (OperationConstI32) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI64Const.
 type OperationConstI64 struct{ Value uint64 }
 
+// String implements fmt.Stringer.
+func (o OperationConstI64) String() string {
+	return fmt.Sprintf("%s %#x", o.Kind(), o.Value)
+}
+
 // Kind implements Operation.Kind.
 func (OperationConstI64) Kind() OperationKind {
 	return OperationKindConstI64
@@ -1193,6 +1460,11 @@ func (OperationConstI64) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF32Const.
 type OperationConstF32 struct{ Value float32 }
 
+// String implements fmt.Stringer.
+func (o OperationConstF32) String() string {
+	return fmt.Sprintf("%s %f", o.Kind(), o.Value)
+}
+
 // Kind implements Operation.Kind.
 func (OperationConstF32) Kind() OperationKind {
 	return OperationKindConstF32
@@ -1203,6 +1475,11 @@ func (OperationConstF32) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF64Const.
 type OperationConstF64 struct{ Value float64 }
 
+// String implements fmt.Stringer.
+func (o OperationConstF64) String() string {
+	return fmt.Sprintf("%s %f", o.Kind(), o.Value)
+}
+
 // Kind implements Operation.Kind.
 func (OperationConstF64) Kind() OperationKind {
 	return OperationKindConstF64
@@ -1213,6 +1490,11 @@ func (OperationConstF64) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32EqName wasm.OpcodeI64EqName wasm.OpcodeF32EqName wasm.OpcodeF64EqName
 type OperationEq struct{ Type UnsignedType }
 
+// String implements fmt.Stringer.
+func (o OperationEq) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationEq) Kind() OperationKind {
 	return OperationKindEq
@@ -1223,6 +1505,11 @@ func (OperationEq) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32NeName wasm.OpcodeI64NeName wasm.OpcodeF32NeName wasm.OpcodeF64NeName
 type OperationNe struct{ Type UnsignedType }
 
+// String implements fmt.Stringer.
+func (o OperationNe) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationNe) Kind() OperationKind {
 	return OperationKindNe
@@ -1233,6 +1520,11 @@ func (OperationNe) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32EqzName wasm.OpcodeI64EqzName
 type OperationEqz struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationEqz) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationEqz) Kind() OperationKind {
 	return OperationKindEqz
@@ -1243,6 +1535,11 @@ func (OperationEqz) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32LtS wasm.OpcodeI32LtU wasm.OpcodeI64LtS wasm.OpcodeI64LtU wasm.OpcodeF32Lt wasm.OpcodeF64Lt
 type OperationLt struct{ Type SignedType }
 
+// String implements fmt.Stringer.
+func (o OperationLt) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationLt) Kind() OperationKind {
 	return OperationKindLt
@@ -1253,6 +1550,11 @@ func (OperationLt) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32GtS wasm.OpcodeI32GtU wasm.OpcodeI64GtS wasm.OpcodeI64GtU wasm.OpcodeF32Gt wasm.OpcodeF64Gt
 type OperationGt struct{ Type SignedType }
 
+// String implements fmt.Stringer.
+func (o OperationGt) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationGt) Kind() OperationKind {
 	return OperationKindGt
@@ -1263,6 +1565,11 @@ func (OperationGt) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32LeS wasm.OpcodeI32LeU wasm.OpcodeI64LeS wasm.OpcodeI64LeU wasm.OpcodeF32Le wasm.OpcodeF64Le
 type OperationLe struct{ Type SignedType }
 
+// String implements fmt.Stringer.
+func (o OperationLe) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationLe) Kind() OperationKind {
 	return OperationKindLe
@@ -1273,6 +1580,11 @@ func (OperationLe) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32GeS wasm.OpcodeI32GeU wasm.OpcodeI64GeS wasm.OpcodeI64GeU wasm.OpcodeF32Ge wasm.OpcodeF64Ge
 type OperationGe struct{ Type SignedType }
 
+// String implements fmt.Stringer.
+func (o OperationGe) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationGe) Kind() OperationKind {
 	return OperationKindGe
@@ -1283,6 +1595,11 @@ func (OperationGe) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32AddName wasm.OpcodeI64AddName wasm.OpcodeF32AddName wasm.OpcodeF64AddName.
 type OperationAdd struct{ Type UnsignedType }
 
+// String implements fmt.Stringer.
+func (o OperationAdd) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationAdd) Kind() OperationKind {
 	return OperationKindAdd
@@ -1293,6 +1610,11 @@ func (OperationAdd) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32SubName wasm.OpcodeI64SubName wasm.OpcodeF32SubName wasm.OpcodeF64SubName.
 type OperationSub struct{ Type UnsignedType }
 
+// String implements fmt.Stringer.
+func (o OperationSub) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationSub) Kind() OperationKind {
 	return OperationKindSub
@@ -1303,6 +1625,11 @@ func (OperationSub) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32MulName wasm.OpcodeI64MulName wasm.OpcodeF32MulName wasm.OpcodeF64MulName.
 type OperationMul struct{ Type UnsignedType }
 
+// String implements fmt.Stringer.
+func (o OperationMul) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationMul) Kind() OperationKind {
 	return OperationKindMul
@@ -1318,6 +1645,11 @@ func (OperationMul) Kind() OperationKind {
 // See wasm.OpcodeI32Clz wasm.OpcodeI64Clz
 type OperationClz struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationClz) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationClz) Kind() OperationKind {
 	return OperationKindClz
@@ -1332,6 +1664,11 @@ func (OperationClz) Kind() OperationKind {
 // For example, stack of [..., 0xff_ff_ff_00] results in [..., 8].
 type OperationCtz struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationCtz) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationCtz) Kind() OperationKind {
 	return OperationKindCtz
@@ -1346,6 +1683,11 @@ func (OperationCtz) Kind() OperationKind {
 // For example, stack of [..., 0b00_00_00_11] results in [..., 2].
 type OperationPopcnt struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationPopcnt) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationPopcnt) Kind() OperationKind {
 	return OperationKindPopcnt
@@ -1358,6 +1700,11 @@ func (OperationPopcnt) Kind() OperationKind {
 //	wasm.OpcodeI64DivU wasm.OpcodeF32Div wasm.OpcodeF64Div.
 type OperationDiv struct{ Type SignedType }
 
+// String implements fmt.Stringer.
+func (o OperationDiv) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationDiv) Kind() OperationKind {
 	return OperationKindDiv
@@ -1373,6 +1720,11 @@ func (OperationDiv) Kind() OperationKind {
 // the quotient is discarded.
 type OperationRem struct{ Type SignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationRem) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationRem) Kind() OperationKind {
 	return OperationKindRem
@@ -1386,6 +1738,11 @@ func (OperationRem) Kind() OperationKind {
 // top two values on the stack, and pushes the result.
 type OperationAnd struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationAnd) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationAnd) Kind() OperationKind {
 	return OperationKindAnd
@@ -1399,6 +1756,11 @@ func (OperationAnd) Kind() OperationKind {
 // top two values on the stack, and pushes the result.
 type OperationOr struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationOr) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationOr) Kind() OperationKind {
 	return OperationKindOr
@@ -1412,6 +1774,11 @@ func (OperationOr) Kind() OperationKind {
 // top two values on the stack, and pushes the result.
 type OperationXor struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationXor) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationXor) Kind() OperationKind {
 	return OperationKindXor
@@ -1425,6 +1792,11 @@ func (OperationXor) Kind() OperationKind {
 // top two values on the stack, and pushes the result.
 type OperationShl struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationShl) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationShl) Kind() OperationKind {
 	return OperationKindShl
@@ -1438,6 +1810,11 @@ func (OperationShl) Kind() OperationKind {
 // top values on the stack, otherwise do the logical right shift.
 type OperationShr struct{ Type SignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationShr) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationShr) Kind() OperationKind {
 	return OperationKindShr
@@ -1451,6 +1828,11 @@ func (OperationShr) Kind() OperationKind {
 // top two values on the stack, and pushes the result.
 type OperationRotl struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationRotl) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationRotl) Kind() OperationKind {
 	return OperationKindRotl
@@ -1464,6 +1846,11 @@ func (OperationRotl) Kind() OperationKind {
 // top two values on the stack, and pushes the result.
 type OperationRotr struct{ Type UnsignedInt }
 
+// String implements fmt.Stringer.
+func (o OperationRotr) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationRotr) Kind() OperationKind {
 	return OperationKindRotr
@@ -1474,6 +1861,11 @@ func (OperationRotr) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF32Abs wasm.OpcodeF64Abs
 type OperationAbs struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationAbs) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationAbs) Kind() OperationKind {
 	return OperationKindAbs
@@ -1484,6 +1876,11 @@ func (OperationAbs) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF32Neg wasm.OpcodeF64Neg
 type OperationNeg struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationNeg) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationNeg) Kind() OperationKind {
 	return OperationKindNeg
@@ -1494,6 +1891,11 @@ func (OperationNeg) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF32CeilName wasm.OpcodeF64CeilName
 type OperationCeil struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationCeil) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationCeil) Kind() OperationKind {
 	return OperationKindCeil
@@ -1504,6 +1906,11 @@ func (OperationCeil) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF32FloorName wasm.OpcodeF64FloorName
 type OperationFloor struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationFloor) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationFloor) Kind() OperationKind {
 	return OperationKindFloor
@@ -1514,6 +1921,11 @@ func (OperationFloor) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF32TruncName wasm.OpcodeF64TruncName
 type OperationTrunc struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationTrunc) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationTrunc) Kind() OperationKind {
 	return OperationKindTrunc
@@ -1528,6 +1940,11 @@ func (OperationTrunc) Kind() OperationKind {
 // For example, math.Round(-4.5) produces -5 while we want to produce -4.
 type OperationNearest struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationNearest) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationNearest) Kind() OperationKind {
 	return OperationKindNearest
@@ -1538,6 +1955,11 @@ func (OperationNearest) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF32SqrtName wasm.OpcodeF64SqrtName
 type OperationSqrt struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationSqrt) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationSqrt) Kind() OperationKind {
 	return OperationKindSqrt
@@ -1554,6 +1976,11 @@ func (OperationSqrt) Kind() OperationKind {
 // which is a different behavior different from math.Min.
 type OperationMin struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationMin) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationMin) Kind() OperationKind {
 	return OperationKindMin
@@ -1570,6 +1997,11 @@ func (OperationMin) Kind() OperationKind {
 // which is a different behavior different from math.Max.
 type OperationMax struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationMax) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationMax) Kind() OperationKind {
 	return OperationKindMax
@@ -1584,6 +2016,11 @@ func (OperationMax) Kind() OperationKind {
 // For example, stack [..., 1.213, -5.0] results in [..., -1.213].
 type OperationCopysign struct{ Type Float }
 
+// String implements fmt.Stringer.
+func (o OperationCopysign) String() string {
+	return fmt.Sprintf("%s.%s", o.Type, o.Kind())
+}
+
 // Kind implements Operation.Kind.
 func (OperationCopysign) Kind() OperationKind {
 	return OperationKindCopysign
@@ -1597,6 +2034,9 @@ func (OperationCopysign) Kind() OperationKind {
 // with the corresponding 32-bit integer.
 type OperationI32WrapFromI64 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationI32WrapFromI64) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationI32WrapFromI64) Kind() OperationKind {
 	return OperationKindI32WrapFromI64
@@ -1627,6 +2067,11 @@ type OperationITruncFromF struct {
 	NonTrapping bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationITruncFromF) String() string {
+	return fmt.Sprintf("%s.%s.%s (non_trapping=%v)", o.OutputType, o.Kind(), o.InputType, o.NonTrapping)
+}
+
 // Kind implements Operation.Kind.
 func (OperationITruncFromF) Kind() OperationKind {
 	return OperationKindITruncFromF
@@ -1645,6 +2090,11 @@ type OperationFConvertFromI struct {
 	OutputType Float
 }
 
+// String implements fmt.Stringer.
+func (o OperationFConvertFromI) String() string {
+	return fmt.Sprintf("%s.%s.%s", o.OutputType, o.Kind(), o.InputType)
+}
+
 // Kind implements Operation.Kind.
 func (OperationFConvertFromI) Kind() OperationKind {
 	return OperationKindFConvertFromI
@@ -1655,6 +2105,9 @@ func (OperationFConvertFromI) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF32DemoteF64 and is equivalent float32(float64(v)).
 type OperationF32DemoteFromF64 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationF32DemoteFromF64) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationF32DemoteFromF64) Kind() OperationKind {
 	return OperationKindF32DemoteFromF64
@@ -1665,6 +2118,9 @@ func (OperationF32DemoteFromF64) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF64PromoteF32 and is equivalent float64(float32(v)).
 type OperationF64PromoteFromF32 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationF64PromoteFromF32) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationF64PromoteFromF32) Kind() OperationKind {
 	return OperationKindF64PromoteFromF32
@@ -1675,6 +2131,9 @@ func (OperationF64PromoteFromF32) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI32ReinterpretF32Name.
 type OperationI32ReinterpretFromF32 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationI32ReinterpretFromF32) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationI32ReinterpretFromF32) Kind() OperationKind {
 	return OperationKindI32ReinterpretFromF32
@@ -1685,6 +2144,9 @@ func (OperationI32ReinterpretFromF32) Kind() OperationKind {
 // This corresponds to wasm.OpcodeI64ReinterpretF64Name.
 type OperationI64ReinterpretFromF64 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationI64ReinterpretFromF64) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationI64ReinterpretFromF64) Kind() OperationKind {
 	return OperationKindI64ReinterpretFromF64
@@ -1695,6 +2157,8 @@ func (OperationI64ReinterpretFromF64) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF32ReinterpretI32Name.
 type OperationF32ReinterpretFromI32 struct{}
 
+func (o OperationF32ReinterpretFromI32) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationF32ReinterpretFromI32) Kind() OperationKind {
 	return OperationKindF32ReinterpretFromI32
@@ -1705,6 +2169,9 @@ func (OperationF32ReinterpretFromI32) Kind() OperationKind {
 // This corresponds to wasm.OpcodeF64ReinterpretI64Name.
 type OperationF64ReinterpretFromI64 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationF64ReinterpretFromI64) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationF64ReinterpretFromI64) Kind() OperationKind {
 	return OperationKindF64ReinterpretFromI64
@@ -1720,6 +2187,19 @@ func (OperationF64ReinterpretFromI64) Kind() OperationKind {
 // original integer's sign.
 type OperationExtend struct{ Signed bool }
 
+// String implements fmt.Stringer.
+func (o OperationExtend) String() string {
+	var in, out string
+	if o.Signed {
+		in = "i32"
+		out = "i64"
+	} else {
+		in = "u32"
+		out = "u64"
+	}
+	return fmt.Sprintf("%s.%s.%s", out, o.Kind(), in)
+}
+
 // Kind implements Operation.Kind.
 func (OperationExtend) Kind() OperationKind {
 	return OperationKindExtend
@@ -1732,6 +2212,9 @@ func (OperationExtend) Kind() OperationKind {
 // The engines are expected to sign-extend the first 8-bits of 32-bit in as signed 32-bit int.
 type OperationSignExtend32From8 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationSignExtend32From8) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationSignExtend32From8) Kind() OperationKind {
 	return OperationKindSignExtend32From8
@@ -1744,6 +2227,9 @@ func (OperationSignExtend32From8) Kind() OperationKind {
 // The engines are expected to sign-extend the first 16-bits of 32-bit in as signed 32-bit int.
 type OperationSignExtend32From16 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationSignExtend32From16) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationSignExtend32From16) Kind() OperationKind {
 	return OperationKindSignExtend32From16
@@ -1756,6 +2242,9 @@ func (OperationSignExtend32From16) Kind() OperationKind {
 // The engines are expected to sign-extend the first 8-bits of 64-bit in as signed 32-bit int.
 type OperationSignExtend64From8 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationSignExtend64From8) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationSignExtend64From8) Kind() OperationKind {
 	return OperationKindSignExtend64From8
@@ -1768,6 +2257,9 @@ func (OperationSignExtend64From8) Kind() OperationKind {
 // The engines are expected to sign-extend the first 16-bits of 64-bit in as signed 32-bit int.
 type OperationSignExtend64From16 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationSignExtend64From16) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationSignExtend64From16) Kind() OperationKind {
 	return OperationKindSignExtend64From16
@@ -1780,6 +2272,9 @@ func (OperationSignExtend64From16) Kind() OperationKind {
 // The engines are expected to sign-extend the first 32-bits of 64-bit in as signed 32-bit int.
 type OperationSignExtend64From32 struct{}
 
+// String implements fmt.Stringer.
+func (o OperationSignExtend64From32) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationSignExtend64From32) Kind() OperationKind {
 	return OperationKindSignExtend64From32
@@ -1794,6 +2289,9 @@ type OperationMemoryInit struct {
 	DataIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationMemoryInit) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationMemoryInit) Kind() OperationKind {
 	return OperationKindMemoryInit
@@ -1808,6 +2306,9 @@ type OperationDataDrop struct {
 	DataIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationDataDrop) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationDataDrop) Kind() OperationKind {
 	return OperationKindDataDrop
@@ -1818,6 +2319,9 @@ func (OperationDataDrop) Kind() OperationKind {
 // This corresponds to wasm.OpcodeMemoryCopyName.
 type OperationMemoryCopy struct{}
 
+// String implements fmt.Stringer.
+func (o OperationMemoryCopy) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationMemoryCopy) Kind() OperationKind {
 	return OperationKindMemoryCopy
@@ -1828,6 +2332,9 @@ func (OperationMemoryCopy) Kind() OperationKind {
 // This corresponds to wasm.OpcodeMemoryFillName.
 type OperationMemoryFill struct{}
 
+// String implements fmt.Stringer.
+func (o OperationMemoryFill) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationMemoryFill) Kind() OperationKind {
 	return OperationKindMemoryFill
@@ -1843,6 +2350,9 @@ type OperationTableInit struct {
 	TableIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationTableInit) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationTableInit) Kind() OperationKind {
 	return OperationKindTableInit
@@ -1856,6 +2366,9 @@ type OperationElemDrop struct {
 	ElemIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationElemDrop) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationElemDrop) Kind() OperationKind {
 	return OperationKindElemDrop
@@ -1868,6 +2381,9 @@ type OperationTableCopy struct {
 	SrcTableIndex, DstTableIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationTableCopy) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationTableCopy) Kind() OperationKind {
 	return OperationKindTableCopy
@@ -1884,6 +2400,9 @@ type OperationRefFunc struct {
 	FunctionIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationRefFunc) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationRefFunc) Kind() OperationKind {
 	return OperationKindRefFunc
@@ -1896,6 +2415,9 @@ type OperationTableGet struct {
 	TableIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationTableGet) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationTableGet) Kind() OperationKind {
 	return OperationKindTableGet
@@ -1908,6 +2430,9 @@ type OperationTableSet struct {
 	TableIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationTableSet) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationTableSet) Kind() OperationKind {
 	return OperationKindTableSet
@@ -1920,6 +2445,9 @@ type OperationTableSize struct {
 	TableIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationTableSize) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationTableSize) Kind() OperationKind {
 	return OperationKindTableSize
@@ -1932,6 +2460,9 @@ type OperationTableGrow struct {
 	TableIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationTableGrow) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationTableGrow) Kind() OperationKind {
 	return OperationKindTableGrow
@@ -1944,6 +2475,9 @@ type OperationTableFill struct {
 	TableIndex uint32
 }
 
+// String implements fmt.Stringer.
+func (o OperationTableFill) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationTableFill) Kind() OperationKind {
 	return OperationKindTableFill
@@ -1954,6 +2488,11 @@ type OperationV128Const struct {
 	Lo, Hi uint64
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Const) String() string {
+	return fmt.Sprintf("%s [%#x, %#x]", o.Kind(), o.Lo, o.Hi)
+}
+
 // Kind implements Operation.Kind.
 //
 // This corresponds to wasm.OpcodeVecV128Const.
@@ -2001,6 +2540,11 @@ type OperationV128Add struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Add) String() string {
+	return fmt.Sprintf("%s (shape=%s)", o.Kind(), shapeName(o.Shape))
+}
+
 // Kind implements Operation.Kind.
 func (OperationV128Add) Kind() OperationKind {
 	return OperationKindV128Add
@@ -2015,6 +2559,9 @@ type OperationV128Sub struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Sub) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Sub) Kind() OperationKind {
 	return OperationKindV128Sub
@@ -2066,6 +2613,9 @@ type OperationV128Load struct {
 	Arg  MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Load) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Load) Kind() OperationKind {
 	return OperationKindV128Load
@@ -2084,6 +2634,9 @@ type OperationV128LoadLane struct {
 	Arg      MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128LoadLane) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128LoadLane) Kind() OperationKind {
 	return OperationKindV128LoadLane
@@ -2098,6 +2651,9 @@ type OperationV128Store struct {
 	Arg MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Store) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Store) Kind() OperationKind {
 	return OperationKindV128Store
@@ -2116,6 +2672,9 @@ type OperationV128StoreLane struct {
 	Arg      MemoryArg
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128StoreLane) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128StoreLane) Kind() OperationKind {
 	return OperationKindV128StoreLane
@@ -2137,6 +2696,9 @@ type OperationV128ExtractLane struct {
 	Shape  Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128ExtractLane) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128ExtractLane) Kind() OperationKind {
 	return OperationKindV128ExtractLane
@@ -2155,6 +2717,9 @@ type OperationV128ReplaceLane struct {
 	Shape     Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128ReplaceLane) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128ReplaceLane) Kind() OperationKind {
 	return OperationKindV128ReplaceLane
@@ -2171,6 +2736,9 @@ type OperationV128Splat struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Splat) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Splat) Kind() OperationKind {
 	return OperationKindV128Splat
@@ -2181,6 +2749,9 @@ type OperationV128Shuffle struct {
 	Lanes [16]byte
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Shuffle) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 //
 // This corresponds to wasm.OpcodeVecV128i8x16ShuffleName.
@@ -2191,6 +2762,9 @@ func (OperationV128Shuffle) Kind() OperationKind {
 // OperationV128Swizzle implements Operation.
 type OperationV128Swizzle struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128Swizzle) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 //
 // This corresponds to wasm.OpcodeVecI8x16SwizzleName.
@@ -2203,6 +2777,9 @@ func (OperationV128Swizzle) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecV128AnyTrueName.
 type OperationV128AnyTrue struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128AnyTrue) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128AnyTrue) Kind() OperationKind {
 	return OperationKindV128AnyTrue
@@ -2218,6 +2795,9 @@ type OperationV128AllTrue struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128AllTrue) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128AllTrue) Kind() OperationKind {
 	return OperationKindV128AllTrue
@@ -2233,6 +2813,9 @@ type OperationV128BitMask struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128BitMask) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128BitMask) Kind() OperationKind {
 	return OperationKindV128BitMask
@@ -2243,6 +2826,9 @@ func (OperationV128BitMask) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecV128And.
 type OperationV128And struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128And) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128And) Kind() OperationKind {
 	return OperationKindV128And
@@ -2253,6 +2839,9 @@ func (OperationV128And) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecV128Not.
 type OperationV128Not struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128Not) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Not) Kind() OperationKind {
 	return OperationKindV128Not
@@ -2263,6 +2852,9 @@ func (OperationV128Not) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecV128Or.
 type OperationV128Or struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128Or) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Or) Kind() OperationKind {
 	return OperationKindV128Or
@@ -2273,6 +2865,9 @@ func (OperationV128Or) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecV128Xor.
 type OperationV128Xor struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128Xor) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Xor) Kind() OperationKind {
 	return OperationKindV128Xor
@@ -2283,6 +2878,9 @@ func (OperationV128Xor) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecV128Bitselect.
 type OperationV128Bitselect struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128Bitselect) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Bitselect) Kind() OperationKind {
 	return OperationKindV128Bitselect
@@ -2293,6 +2891,9 @@ func (OperationV128Bitselect) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecV128AndNot.
 type OperationV128AndNot struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128AndNot) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128AndNot) Kind() OperationKind {
 	return OperationKindV128AndNot
@@ -2308,6 +2909,9 @@ type OperationV128Shl struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Shl) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Shl) Kind() OperationKind {
 	return OperationKindV128Shl
@@ -2325,6 +2929,9 @@ type OperationV128Shr struct {
 	Signed bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Shr) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Shr) Kind() OperationKind {
 	return OperationKindV128Shr
@@ -2348,6 +2955,9 @@ type OperationV128Cmp struct {
 	Type V128CmpType
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Cmp) String() string { return o.Kind().String() }
+
 // V128CmpType represents a type of vector comparison operation.
 type V128CmpType = byte
 
@@ -2466,6 +3076,9 @@ type OperationV128AddSat struct {
 	Signed bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128AddSat) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128AddSat) Kind() OperationKind {
 	return OperationKindV128AddSat
@@ -2482,6 +3095,9 @@ type OperationV128SubSat struct {
 	Signed bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128SubSat) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128SubSat) Kind() OperationKind {
 	return OperationKindV128SubSat
@@ -2497,6 +3113,9 @@ type OperationV128Mul struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Mul) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Mul) Kind() OperationKind {
 	return OperationKindV128Mul
@@ -2510,6 +3129,9 @@ type OperationV128Div struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Div) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Div) Kind() OperationKind {
 	return OperationKindV128Div
@@ -2524,6 +3146,9 @@ type OperationV128Neg struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Neg) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Neg) Kind() OperationKind {
 	return OperationKindV128Neg
@@ -2537,6 +3162,9 @@ type OperationV128Sqrt struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Sqrt) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Sqrt) Kind() OperationKind {
 	return OperationKindV128Sqrt
@@ -2551,6 +3179,9 @@ type OperationV128Abs struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Abs) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Abs) Kind() OperationKind {
 	return OperationKindV128Abs
@@ -2563,6 +3194,9 @@ type OperationV128Popcnt struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Popcnt) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Popcnt) Kind() OperationKind {
 	return OperationKindV128Popcnt
@@ -2580,6 +3214,9 @@ type OperationV128Min struct {
 	Signed bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Min) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Min) Kind() OperationKind {
 	return OperationKindV128Min
@@ -2597,6 +3234,9 @@ type OperationV128Max struct {
 	Signed bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Max) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Max) Kind() OperationKind {
 	return OperationKindV128Max
@@ -2609,6 +3249,9 @@ type OperationV128AvgrU struct {
 	Shape Shape
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128AvgrU) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128AvgrU) Kind() OperationKind {
 	return OperationKindV128AvgrU
@@ -2619,6 +3262,9 @@ func (OperationV128AvgrU) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecF32x4PminName wasm.OpcodeVecF64x2PminName.
 type OperationV128Pmin struct{ Shape Shape }
 
+// String implements fmt.Stringer.
+func (o OperationV128Pmin) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationV128Pmin) Kind() OperationKind {
 	return OperationKindV128Pmin
@@ -2629,6 +3275,9 @@ func (OperationV128Pmin) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecF32x4PmaxName wasm.OpcodeVecF64x2PmaxName.
 type OperationV128Pmax struct{ Shape Shape }
 
+// String implements fmt.Stringer.
+func (o OperationV128Pmax) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationV128Pmax) Kind() OperationKind {
 	return OperationKindV128Pmax
@@ -2639,6 +3288,9 @@ func (OperationV128Pmax) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecF32x4CeilName wasm.OpcodeVecF64x2CeilName
 type OperationV128Ceil struct{ Shape Shape }
 
+// String implements fmt.Stringer.
+func (o OperationV128Ceil) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationV128Ceil) Kind() OperationKind {
 	return OperationKindV128Ceil
@@ -2649,6 +3301,9 @@ func (OperationV128Ceil) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecF32x4FloorName wasm.OpcodeVecF64x2FloorName
 type OperationV128Floor struct{ Shape Shape }
 
+// String implements fmt.Stringer.
+func (o OperationV128Floor) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationV128Floor) Kind() OperationKind {
 	return OperationKindV128Floor
@@ -2659,6 +3314,9 @@ func (OperationV128Floor) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecF32x4TruncName wasm.OpcodeVecF64x2TruncName
 type OperationV128Trunc struct{ Shape Shape }
 
+// String implements fmt.Stringer.
+func (o OperationV128Trunc) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationV128Trunc) Kind() OperationKind {
 	return OperationKindV128Trunc
@@ -2669,6 +3327,9 @@ func (OperationV128Trunc) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecF32x4NearestName wasm.OpcodeVecF64x2NearestName
 type OperationV128Nearest struct{ Shape Shape }
 
+// String implements fmt.Stringer.
+func (o OperationV128Nearest) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationV128Nearest) Kind() OperationKind {
 	return OperationKindV128Nearest
@@ -2693,6 +3354,9 @@ type OperationV128Extend struct {
 	UseLow bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Extend) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationV128Extend) Kind() OperationKind {
 	return OperationKindV128Extend
@@ -2717,6 +3381,9 @@ type OperationV128ExtMul struct {
 	UseLow bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128ExtMul) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationV128ExtMul) Kind() OperationKind {
 	return OperationKindV128ExtMul
@@ -2727,6 +3394,9 @@ func (OperationV128ExtMul) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecI16x8Q15mulrSatSName
 type OperationV128Q15mulrSatS struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128Q15mulrSatS) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind
 func (OperationV128Q15mulrSatS) Kind() OperationKind {
 	return OperationKindV128Q15mulrSatS
@@ -2745,6 +3415,9 @@ type OperationV128ExtAddPairwise struct {
 	Signed      bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128ExtAddPairwise) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128ExtAddPairwise) Kind() OperationKind {
 	return OperationKindV128ExtAddPairwise
@@ -2757,6 +3430,9 @@ func (OperationV128ExtAddPairwise) Kind() OperationKind {
 // 32-bit floats in the lower 64-bit as two 64-bit floats.
 type OperationV128FloatPromote struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128FloatPromote) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128FloatPromote) Kind() OperationKind {
 	return OperationKindV128FloatPromote
@@ -2767,6 +3443,9 @@ func (OperationV128FloatPromote) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecF32x4DemoteF64x2ZeroName.
 type OperationV128FloatDemote struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128FloatDemote) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128FloatDemote) Kind() OperationKind {
 	return OperationKindV128FloatDemote
@@ -2785,6 +3464,9 @@ type OperationV128FConvertFromI struct {
 	Signed           bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128FConvertFromI) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128FConvertFromI) Kind() OperationKind {
 	return OperationKindV128FConvertFromI
@@ -2795,6 +3477,9 @@ func (OperationV128FConvertFromI) Kind() OperationKind {
 // This corresponds to wasm.OpcodeVecI32x4DotI16x8SName
 type OperationV128Dot struct{}
 
+// String implements fmt.Stringer.
+func (o OperationV128Dot) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Dot) Kind() OperationKind {
 	return OperationKindV128Dot
@@ -2813,6 +3498,9 @@ type OperationV128Narrow struct {
 	Signed      bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128Narrow) String() string { return o.Kind().String() }
+
 // Kind implements Operation.Kind.
 func (OperationV128Narrow) Kind() OperationKind {
 	return OperationKindV128Narrow
@@ -2831,6 +3519,15 @@ type OperationV128ITruncSatFromF struct {
 	Signed      bool
 }
 
+// String implements fmt.Stringer.
+func (o OperationV128ITruncSatFromF) String() string {
+	if o.Signed {
+		return fmt.Sprintf("%s.%sS", o.Kind(), shapeName(o.OriginShape))
+	} else {
+		return fmt.Sprintf("%s.%sU", o.Kind(), shapeName(o.OriginShape))
+	}
+}
+
 // Kind implements Operation.Kind.
 func (OperationV128ITruncSatFromF) Kind() OperationKind {
 	return OperationKindV128ITruncSatFromF


Test patch:
diff --git a/internal/gojs/testdata/writefs/main.go b/internal/gojs/testdata/writefs/main.go
index c40e2990..a7462146 100644
--- a/internal/gojs/testdata/writefs/main.go
+++ b/internal/gojs/testdata/writefs/main.go
@@ -83,6 +83,12 @@ func Main() {
 	} else if mode := stat.Mode() & fs.ModePerm; mode != 0o400 {
 		log.Panicln("expected mode = 0o400", mode)
 	}
+
+	// Invoke Chown in a way nothing changes, to ensure it doesn't panic.
+	if err = f.Chown(-1, -1); err != nil {
+		log.Panicln(err)
+	}
+
 	// Finally, close it.
 	if err = f.Close(); err != nil {
 		log.Panicln(err)
@@ -99,6 +105,11 @@ func Main() {
 		log.Panicln(err)
 	}
 
+	// Invoke Chown in a way nothing changes, to ensure it doesn't panic.
+	if err = os.Chown(file1, -1, -1); err != nil {
+		log.Panicln(err)
+	}
+
 	if stat.Mode().Type() != 0 {
 		log.Panicln("expected type = 0", stat.Mode().Type())
 	}
@@ -171,6 +182,11 @@ func Main() {
 		log.Panicln("expected file != link stat", file1Stat, symlinkStat)
 	}
 
+	// Invoke Lchown in a way nothing changes, to ensure it doesn't panic.
+	if err = os.Lchown(symlink, -1, -1); err != nil {
+		log.Panicln(err)
+	}
+
 	// Test removing a non-empty empty directory
 	if err = syscall.Rmdir(dir); err != syscall.ENOTEMPTY {
 		log.Panicln("unexpected error", err)
diff --git a/internal/platform/chown_unix_test.go b/internal/platform/chown_unix_test.go
new file mode 100644
index 00000000..6f499c72
--- /dev/null
+++ b/internal/platform/chown_unix_test.go
@@ -0,0 +1,174 @@
+//go:build !windows
+
+package platform
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"syscall"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func TestChown(t *testing.T) {
+	tmpDir := t.TempDir()
+
+	dir := path.Join(tmpDir, "dir")
+	require.NoError(t, os.Mkdir(dir, 0o0777))
+	dirF, err := OpenFile(dir, syscall.O_RDONLY, 0)
+	require.NoError(t, err)
+	dirStat, err := dirF.Stat()
+	require.NoError(t, err)
+	dirSys := dirStat.Sys().(*syscall.Stat_t)
+
+	// Similar to TestChown in os_unix_test.go, we can't expect to change
+	// owner unless root, and with another user. Instead, test gid.
+	gid := os.Getgid()
+	groups, err := os.Getgroups()
+	require.NoError(t, err)
+
+	t.Run("-1 parameters means leave alone", func(t *testing.T) {
+		require.NoError(t, Chown(dir, -1, -1))
+		checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
+	})
+
+	t.Run("change gid, but not uid", func(t *testing.T) {
+		require.NoError(t, Chown(dir, -1, gid))
+		checkUidGid(t, dir, dirSys.Uid, uint32(gid))
+	})
+
+	// Now, try any other groups of the current user.
+	for _, g := range groups {
+		g := g
+		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
+			// Test using our Chown
+			require.NoError(t, Chown(dir, -1, g))
+			checkUidGid(t, dir, dirSys.Uid, uint32(g))
+
+			// Revert back with os.File.Chown
+			require.NoError(t, dirF.Chown(-1, gid))
+			checkUidGid(t, dir, dirSys.Uid, uint32(gid))
+		})
+	}
+
+	t.Run("not found", func(t *testing.T) {
+		require.EqualErrno(t, syscall.ENOENT, Chown(path.Join(tmpDir, "a"), -1, gid))
+	})
+}
+
+func TestChownFile(t *testing.T) {
+	tmpDir := t.TempDir()
+
+	dir := path.Join(tmpDir, "dir")
+	require.NoError(t, os.Mkdir(dir, 0o0777))
+	dirF, err := OpenFile(dir, syscall.O_RDONLY, 0)
+	require.NoError(t, err)
+	dirStat, err := dirF.Stat()
+	require.NoError(t, err)
+	dirSys := dirStat.Sys().(*syscall.Stat_t)
+
+	// Similar to TestChownFile in os_unix_test.go, we can't expect to change
+	// owner unless root, and with another user. Instead, test gid.
+	gid := os.Getgid()
+	groups, err := os.Getgroups()
+	require.NoError(t, err)
+
+	t.Run("-1 parameters means leave alone", func(t *testing.T) {
+		require.NoError(t, ChownFile(dirF, -1, -1))
+		checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
+	})
+
+	t.Run("change gid, but not uid", func(t *testing.T) {
+		require.NoError(t, ChownFile(dirF, -1, gid))
+		checkUidGid(t, dir, dirSys.Uid, uint32(gid))
+	})
+
+	// Now, try any other groups of the current user.
+	for _, g := range groups {
+		g := g
+		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
+			// Test using our ChownFile
+			require.NoError(t, ChownFile(dirF, -1, g))
+			checkUidGid(t, dir, dirSys.Uid, uint32(g))
+
+			// Revert back with os.File.Chown
+			require.NoError(t, dirF.Chown(-1, gid))
+			checkUidGid(t, dir, dirSys.Uid, uint32(gid))
+		})
+	}
+
+	t.Run("closed", func(t *testing.T) {
+		require.NoError(t, dirF.Close())
+		require.EqualErrno(t, syscall.EBADF, ChownFile(dirF, -1, gid))
+	})
+}
+
+func TestLchown(t *testing.T) {
+	tmpDir := t.TempDir()
+
+	dir := path.Join(tmpDir, "dir")
+	require.NoError(t, os.Mkdir(dir, 0o0777))
+	dirF, err := OpenFile(dir, syscall.O_RDONLY, 0)
+	require.NoError(t, err)
+	dirStat, err := dirF.Stat()
+	require.NoError(t, err)
+	dirSys := dirStat.Sys().(*syscall.Stat_t)
+
+	link := path.Join(tmpDir, "link")
+	require.NoError(t, os.Symlink(dir, link))
+	linkF, err := OpenFile(link, syscall.O_RDONLY, 0)
+	require.NoError(t, err)
+	linkStat, err := linkF.Stat()
+	require.NoError(t, err)
+	linkSys := linkStat.Sys().(*syscall.Stat_t)
+
+	// Similar to TestLchown in os_unix_test.go, we can't expect to change
+	// owner unless root, and with another user. Instead, test gid.
+	gid := os.Getgid()
+	groups, err := os.Getgroups()
+	require.NoError(t, err)
+
+	t.Run("-1 parameters means leave alone", func(t *testing.T) {
+		require.NoError(t, Lchown(link, -1, -1))
+		checkUidGid(t, link, linkSys.Uid, linkSys.Gid)
+	})
+
+	t.Run("change gid, but not uid", func(t *testing.T) {
+		require.NoError(t, Chown(dir, -1, gid))
+		checkUidGid(t, link, linkSys.Uid, uint32(gid))
+		// Make sure the target didn't change.
+		checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
+	})
+
+	// Now, try any other groups of the current user.
+	for _, g := range groups {
+		g := g
+		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
+			// Test using our Lchown
+			require.NoError(t, Lchown(link, -1, g))
+			checkUidGid(t, link, linkSys.Uid, uint32(g))
+			// Make sure the target didn't change.
+			checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
+
+			// Revert back with syscall.Lchown
+			require.NoError(t, syscall.Lchown(link, -1, gid))
+			checkUidGid(t, link, linkSys.Uid, uint32(gid))
+		})
+	}
+
+	t.Run("not found", func(t *testing.T) {
+		require.EqualErrno(t, syscall.ENOENT, Lchown(path.Join(tmpDir, "a"), -1, gid))
+	})
+}
+
+// checkUidGid uses lstat to ensure the comparison is against the file, not the
+// target of a symbolic link.
+func checkUidGid(t *testing.T, path string, uid, gid uint32) {
+	ls, err := os.Lstat(path)
+	require.NoError(t, err)
+	sys := ls.Sys().(*syscall.Stat_t)
+	require.Equal(t, uid, sys.Uid)
+	require.Equal(t, gid, sys.Gid)
+}
diff --git a/internal/sysfs/dirfs_unix_test.go b/internal/sysfs/dirfs_unix_test.go
new file mode 100644
index 00000000..f08df51c
--- /dev/null
+++ b/internal/sysfs/dirfs_unix_test.go
@@ -0,0 +1,127 @@
+//go:build unix
+
+package sysfs
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"syscall"
+	"testing"
+
+	"github.com/tetratelabs/wazero/internal/platform"
+	"github.com/tetratelabs/wazero/internal/testing/require"
+)
+
+func TestDirFS_Chown(t *testing.T) {
+	tmpDir := t.TempDir()
+	testFS := NewDirFS(tmpDir)
+
+	require.NoError(t, testFS.Mkdir("dir", 0o0777))
+	dirF, err := testFS.OpenFile("dir", syscall.O_RDONLY, 0)
+	require.NoError(t, err)
+	dirStat, err := dirF.Stat()
+	require.NoError(t, err)
+	dirSys := dirStat.Sys().(*syscall.Stat_t)
+
+	// Similar to TestChown in os_unix_test.go, we can't expect to change
+	// owner unless root, and with another user. Instead, test gid.
+	gid := os.Getgid()
+	groups, err := os.Getgroups()
+	require.NoError(t, err)
+
+	t.Run("-1 parameters means leave alone", func(t *testing.T) {
+		require.NoError(t, testFS.Chown("dir", -1, -1))
+		checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, dirSys.Gid)
+	})
+
+	t.Run("change gid, but not uid", func(t *testing.T) {
+		require.NoError(t, testFS.Chown("dir", -1, gid))
+		checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, uint32(gid))
+	})
+
+	// Now, try any other groups of the current user.
+	for _, g := range groups {
+		g := g
+		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
+			// Test using our Chown
+			require.NoError(t, testFS.Chown("dir", -1, g))
+			checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, uint32(g))
+
+			// Revert back with platform.ChownFile
+			require.NoError(t, platform.ChownFile(dirF, -1, gid))
+			checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, uint32(gid))
+		})
+	}
+
+	t.Run("not found", func(t *testing.T) {
+		require.EqualErrno(t, syscall.ENOENT, testFS.Chown("a", -1, gid))
+	})
+}
+
+func TestDirFS_Lchown(t *testing.T) {
+	tmpDir := t.TempDir()
+	testFS := NewDirFS(tmpDir)
+
+	require.NoError(t, testFS.Mkdir("dir", 0o0777))
+	dirF, err := testFS.OpenFile("dir", syscall.O_RDONLY, 0)
+	require.NoError(t, err)
+	dirStat, err := dirF.Stat()
+	require.NoError(t, err)
+	dirSys := dirStat.Sys().(*syscall.Stat_t)
+
+	require.NoError(t, testFS.Symlink("dir", "link"))
+	linkF, err := testFS.OpenFile("link", syscall.O_RDONLY, 0)
+	require.NoError(t, err)
+	linkStat, err := linkF.Stat()
+	require.NoError(t, err)
+	linkSys := linkStat.Sys().(*syscall.Stat_t)
+
+	// Similar to TestLchown in os_unix_test.go, we can't expect to change
+	// owner unless root, and with another user. Instead, test gid.
+	gid := os.Getgid()
+	groups, err := os.Getgroups()
+	require.NoError(t, err)
+
+	t.Run("-1 parameters means leave alone", func(t *testing.T) {
+		require.NoError(t, testFS.Lchown("link", -1, -1))
+		checkUidGid(t, path.Join(tmpDir, "link"), linkSys.Uid, linkSys.Gid)
+	})
+
+	t.Run("change gid, but not uid", func(t *testing.T) {
+		require.NoError(t, testFS.Chown("dir", -1, gid))
+		checkUidGid(t, path.Join(tmpDir, "link"), linkSys.Uid, uint32(gid))
+		// Make sure the target didn't change.
+		checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, dirSys.Gid)
+	})
+
+	// Now, try any other groups of the current user.
+	for _, g := range groups {
+		g := g
+		t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
+			// Test using our Lchown
+			require.NoError(t, testFS.Lchown("link", -1, g))
+			checkUidGid(t, path.Join(tmpDir, "link"), linkSys.Uid, uint32(g))
+			// Make sure the target didn't change.
+			checkUidGid(t, path.Join(tmpDir, "dir"), dirSys.Uid, dirSys.Gid)
+
+			// Revert back with syscall.Lchown
+			require.NoError(t, syscall.Lchown(path.Join(tmpDir, "link"), -1, gid))
+			checkUidGid(t, path.Join(tmpDir, "link"), linkSys.Uid, uint32(gid))
+		})
+	}
+
+	t.Run("not found", func(t *testing.T) {
+		require.EqualErrno(t, syscall.ENOENT, testFS.Lchown("a", -1, gid))
+	})
+}
+
+// checkUidGid uses lstat to ensure the comparison is against the file, not the
+// target of a symbolic link.
+func checkUidGid(t *testing.T, path string, uid, gid uint32) {
+	ls, err := os.Lstat(path)
+	require.NoError(t, err)
+	sys := ls.Sys().(*syscall.Stat_t)
+	require.Equal(t, uid, sys.Uid)
+	require.Equal(t, gid, sys.Gid)
+}
diff --git a/internal/wazeroir/compiler_test.go b/internal/wazeroir/compiler_test.go
index f20319e8..4057559d 100644
--- a/internal/wazeroir/compiler_test.go
+++ b/internal/wazeroir/compiler_test.go
@@ -3163,38 +3163,38 @@ func Test_ensureTermination(t *testing.T) {
 		{
 			ensureTermination: true,
 			exp: `.entrypoint
-	builtin_function.check_closed
-	i32.const 0
-	br .L2
-.L2:
-	builtin_function.check_closed
-	i32.const 1
-	br_if .L2, .L3
-.L3:
-	i32.const 1
-	br .L4
-.L4:
-	builtin_function.check_closed
-	i32.add
-	drop 0..0
-	br .return
+	BuiltinFunctionCheckExitCode
+	ConstI32 0x0
+	Br .L2
+.L2
+	BuiltinFunctionCheckExitCode
+	ConstI32 0x1
+	BrIf .L2, .L3
+.L3
+	ConstI32 0x1
+	Br .L4
+.L4
+	BuiltinFunctionCheckExitCode
+	i32.Add
+	Drop 0..0
+	Br .return
 `,
 		},
 		{
 			ensureTermination: false,
 			exp: `.entrypoint
-	i32.const 0
-	br .L2
-.L2:
-	i32.const 1
-	br_if .L2, .L3
-.L3:
-	i32.const 1
-	br .L4
-.L4:
-	i32.add
-	drop 0..0
-	br .return
+	ConstI32 0x0
+	Br .L2
+.L2
+	ConstI32 0x1
+	BrIf .L2, .L3
+.L3
+	ConstI32 0x1
+	Br .L4
+.L4
+	i32.Add
+	Drop 0..0
+	Br .return
 `,
 		},
 	} {