Repository: tetratelabs/wazero
Pull request #966 (2022-12-27 11:14:00)

cli: adds -hostlogging=filesystem for diagnosing problems
This adds a logging flag with only one initial option: filesystem

This is needed to complete filesystem support in WASI and `GOARCH=wasm GOOS=js`. This PR is rough, but the code will  be improved over the week alongside filesystem improvements.

### Example of TinyGo (WASI) tests

*NOTE: the test itself is expected to fail. What's visible here is what filesystem calls led to the failure*
```bash
$ ./build/tinygo test -target wasi -c -o os.wasm os
$ wazero run -hostlogging=filesystem -mount=.:/ os.wasm -test.v
==> wasi_snapshot_preview1.fd_prestat_get(fd=3)
<== (prestat={pr_name_len=1},errno=ESUCCESS)
==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=3)
<== (path=/,errno=ESUCCESS)
==> wasi_snapshot_preview1.fd_prestat_get(fd=4)
<== (prestat=,errno=EBADF)
--> wasi_snapshot_preview1.path_unlink_file(fd=3,path=tmp/_os_test_TestTempDir)
<-- errno=ENOSYS
--> wasi_snapshot_preview1.path_remove_directory(fd=3,path=tmp/_os_test_TestTempDir)
<-- errno=ENOSYS
==> wasi_snapshot_preview1.fd_fdstat_get(fd=3)
<== (stat={filetype=DIRECTORY,fdflags=APPEND,fs_rights_base=,fs_rights_inheriting=},errno=ESUCCESS)
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=SYMLINK_FOLLOW,path=tmp/_os_test_TestTempDir,oflags=CREAT,fs_rights_base=,fs_rights_inheriting=,fdflags=)
<== (opened_fd=,errno=ENOENT)
--> wasi_snapshot_preview1.path_unlink_file(fd=3,path=_os_test_TestChDir)
<-- errno=ENOSYS
--> wasi_snapshot_preview1.path_remove_directory(fd=3,path=_os_test_TestChDir)
<-- errno=ENOSYS
--> wasi_snapshot_preview1.path_create_directory(fd=3,path=_os_test_TestChDir)
<-- errno=ENOSYS
==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=SYMLINK_FOLLOW,path=_os_test_TestChDir)
<== (filestat=,errno=ENOENT)
--> wasi_snapshot_preview1.path_unlink_file(fd=3,path=_os_test_TestChDir)
<-- errno=ENOSYS
--> wasi_snapshot_preview1.path_remove_directory(fd=3,path=_os_test_TestChDir)
<-- errno=ENOSYS
==> wasi_snapshot_preview1.fd_fdstat_get(fd=3)
<== (stat={filetype=DIRECTORY,fdflags=APPEND,fs_rights_base=,fs_rights_inheriting=},errno=ESUCCESS)
==> wasi_snapshot_preview1.path_open(fd=3,dirflags=SYMLINK_FOLLOW,path=tmp/TestFd.txt614390,oflags=CREAT|EXCL,fs_rights_base=,fs_rights_inheriting=,fdflags=)
<== (opened_fd=,errno=ENOENT)
==> wasi_snapshot_preview1.fd_write(fd=1,iovs=93508,iovs_len=1)
panic: runtime error: nil pointer dereference
<== (nwritten=46,errno=ESUCCESS)
error instantiating wasm binary: module[] function[_start] failed: wasm error: unreachable
wasm stack trace:
	.runtime.runtimePanic(i32,i32)
		0x9541: /Users/adrian/oss/tinygo/src/runtime/runtime_tinygowasm.go:72:6 (inlined)
		        /Users/adrian/oss/tinygo/src/runtime/panic.go:59:7
	.runtime.nilPanic()
		0x5d7f: /Users/adrian/oss/tinygo/src/runtime/panic.go:122:14 (inlined)
		        /Users/adrian/oss/tinygo/src/runtime/gc_stack_portable.go:46:21
	.(*os.File).Name(i32,i32)
		0x3452d: /Users/adrian/oss/tinygo/src/os/file.go:71:2 (inlined)
		         <Go interface method> (inlined)
--snip--
```

### Example of `GOARCH=wasm GOOS=js` tests
*NOTE: the test itself is expected to fail. What's visible here is what filesystem calls led to the failure*

```bash
$ GOARCH=wasm GOOS=js ./bin/go test -c -o os.wasm os
$ ../wazero/wazero run -hostlogging=filesystem -mount=.:/ os.wasm -test.v
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=21,fOffset=<nil>))
=== RUN   TestExpand
<== (err=<nil>,n=21)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=29,fOffset=<nil>))
--- PASS: TestExpand (0.00s)
<== (err=<nil>,n=29)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=32,fOffset=<nil>))
=== RUN   TestConsistentEnviron
<== (err=<nil>,n=32)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=40,fOffset=<nil>))
--- PASS: TestConsistentEnviron (0.00s)
<== (err=<nil>,n=40)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=23,fOffset=<nil>))
=== RUN   TestUnsetenv
<== (err=<nil>,n=23)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=31,fOffset=<nil>))
--- PASS: TestUnsetenv (0.00s)
<== (err=<nil>,n=31)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=23,fOffset=<nil>))
=== RUN   TestClearenv
<== (err=<nil>,n=23)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=31,fOffset=<nil>))
--- PASS: TestClearenv (0.00s)
<== (err=<nil>,n=31)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=24,fOffset=<nil>))
=== RUN   TestLookupEnv
<== (err=<nil>,n=24)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=32,fOffset=<nil>))
--- PASS: TestLookupEnv (0.00s)
<== (err=<nil>,n=32)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=33,fOffset=<nil>))
=== RUN   TestEnvironConsistency
<== (err=<nil>,n=33)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=41,fOffset=<nil>))
--- PASS: TestEnvironConsistency (0.00s)
<== (err=<nil>,n=41)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=25,fOffset=<nil>))
=== RUN   TestErrIsExist
<== (err=<nil>,n=25)
==> go.syscall/js.valueCall(fs.open(name=/tmp/_Go_ErrIsExist3171997792,flags=,perm=-rw-------))
<== (err=open tmp/_Go_ErrIsExist3171997792: file does not exist,fd=0)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=110,fOffset=<nil>))
    error_test.go:19: open ErrIsExist tempfile: open /tmp/_Go_ErrIsExist3171997792: No such file or directory
<== (err=<nil>,n=110)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=33,fOffset=<nil>))
--- FAIL: TestErrIsExist (0.00s)
<== (err=<nil>,n=33)
==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=28,fOffset=<nil>))
=== RUN   TestErrIsNotExist
<== (err=<nil>,n=28)
error instantiating wasm binary: runtime error: invalid memory address or nil pointer dereference (recovered by wazero)
wasm stack trace:
	go.syscall/js.valueCall(i32)
	.syscall_js.valueCall(i32) i32
	.syscall_js.Value.Call(i32) i32
	.syscall.fsCall(i32) i32
	.syscall.Mkdir(i32) i32
	.os.Mkdir.func1(i32) i32
	.os.Mkdir(i32) i32
	.os.MkdirTemp(i32) i32
	.wasm_pc_f_loop()
	.wasm_export_run(i32,i32)
```

codefromthecrypt: funny this test found that mtimeMs was broken due to precision problem. fixed it.
codefromthecrypt: dates are correct now, just the test will fail since we aren't checking out full history (has mtime of now instead of real mtime). I'll change the test with a comment.
codefromthecrypt: windows flags don't match. will sort it in the morning
codefromthecrypt: ok finished all flags and refreshed desc. This will help a lot as we implement create/mkdir support.

waiting until tests pass in case there's a remaining windows issue.
codefromthecrypt: windows issue is that it hasn't a world perms thing, and the string logging of stat helped identify it. should be sorted now.

Patch:
diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go
index 90c50663..3c8518e1 100644
--- a/cmd/wazero/wazero.go
+++ b/cmd/wazero/wazero.go
@@ -14,6 +14,7 @@ import (
 	"github.com/tetratelabs/wazero"
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental"
+	"github.com/tetratelabs/wazero/experimental/logging"
 	gojs "github.com/tetratelabs/wazero/imports/go"
 	"github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1"
 	"github.com/tetratelabs/wazero/internal/version"
@@ -25,7 +26,7 @@ func main() {
 }
 
 // doMain is separated out for the purpose of unit testing.
-func doMain(stdOut io.Writer, stdErr io.Writer, exit func(code int)) {
+func doMain(stdOut io.Writer, stdErr logging.Writer, exit func(code int)) {
 	flag.CommandLine.SetOutput(stdErr)
 
 	var help bool
@@ -91,6 +92,8 @@ func doCompile(args []string, stdErr io.Writer, exit func(code int)) {
 
 	ctx := maybeUseCacheDir(context.Background(), cacheDir, stdErr, exit)
 
+	ctx = maybeUseCacheDir(ctx, cacheDir, stdErr, exit)
+
 	rt := wazero.NewRuntime(ctx)
 	defer rt.Close(ctx)
 
@@ -102,7 +105,7 @@ func doCompile(args []string, stdErr io.Writer, exit func(code int)) {
 	}
 }
 
-func doRun(args []string, stdOut io.Writer, stdErr io.Writer, exit func(code int)) {
+func doRun(args []string, stdOut io.Writer, stdErr logging.Writer, exit func(code int)) {
 	flags := flag.NewFlagSet("run", flag.ExitOnError)
 	flags.SetOutput(stdErr)
 
@@ -118,6 +121,8 @@ func doRun(args []string, stdOut io.Writer, stdErr io.Writer, exit func(code int
 		"filesystem path to expose to the binary in the form of <host path>[:<wasm path>]. If wasm path is not "+
 			"provided, the host path will be used. Can be specified multiple times.")
 
+	hostLogging := hostLoggingFlag(flags)
+
 	cacheDir := cacheDirFlag(flags)
 
 	_ = flags.Parse(args)
@@ -195,7 +200,9 @@ func doRun(args []string, stdOut io.Writer, stdErr io.Writer, exit func(code int
 
 	wasmExe := filepath.Base(wasmPath)
 
-	ctx := maybeUseCacheDir(context.Background(), cacheDir, stdErr, exit)
+	ctx := maybeHostLogging(context.Background(), hostLogging, stdErr, exit)
+
+	ctx = maybeUseCacheDir(ctx, cacheDir, stdErr, exit)
 
 	rt := wazero.NewRuntime(ctx)
 	defer rt.Close(ctx)
@@ -261,6 +268,24 @@ func detectImports(imports []api.FunctionDefinition) (needsWASI, needsGo bool) {
 	return
 }
 
+func hostLoggingFlag(flags *flag.FlagSet) *string {
+	return flags.String("hostlogging", "", "Scope of host functions to log to stderr. "+
+		"Current values: filesystem")
+}
+
+func maybeHostLogging(ctx context.Context, hostLogging *string, stdErr logging.Writer, exit func(code int)) context.Context {
+	h := *hostLogging
+	switch h {
+	case "":
+	case "filesystem":
+		ctx = context.WithValue(ctx, experimental.FunctionListenerFactoryKey{}, logging.NewFilesystemLoggingListenerFactory(stdErr))
+	default:
+		fmt.Fprintf(stdErr, "invalid hostLogging value: %v\n", h)
+		exit(1)
+	}
+	return ctx
+}
+
 func cacheDirFlag(flags *flag.FlagSet) *string {
 	return flags.String("cachedir", "", "Writeable directory for native code compiled from wasm. "+
 		"Contents are re-used for the same version of wazero.")
diff --git a/experimental/logging/log_listener.go b/experimental/logging/log_listener.go
index 08d7a37d..2d458a47 100644
--- a/experimental/logging/log_listener.go
+++ b/experimental/logging/log_listener.go
@@ -7,6 +7,8 @@ import (
 
 	"github.com/tetratelabs/wazero/api"
 	"github.com/tetratelabs/wazero/experimental"
+	wasilogging "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1/logging"
+	gologging "github.com/tetratelabs/wazero/internal/gojs/logging"
 	"github.com/tetratelabs/wazero/internal/logging"
 	"github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
 )
@@ -37,6 +39,14 @@ func NewHostLoggingListenerFactory(w Writer) experimental.FunctionListenerFactor
 	return &loggingListenerFactory{w: toInternalWriter(w), hostOnly: true}
 }
 
+// NewFilesystemLoggingListenerFactory is an experimental.FunctionListenerFactory
+// that logs exported filesystem functions to the writer.
+//
+// This is an alternative to NewHostLoggingListenerFactory.
+func NewFilesystemLoggingListenerFactory(w Writer) experimental.FunctionListenerFactory {
+	return &loggingListenerFactory{w: toInternalWriter(w), fsOnly: true}
+}
+
 func toInternalWriter(w Writer) logging.Writer {
 	if w, ok := w.(logging.Writer); ok {
 		return w
@@ -47,6 +57,7 @@ func toInternalWriter(w Writer) logging.Writer {
 type loggingListenerFactory struct {
 	w        logging.Writer
 	hostOnly bool
+	fsOnly   bool
 }
 
 type flusher interface {
@@ -57,18 +68,32 @@ type flusher interface {
 // experimental.FunctionListener.
 func (f *loggingListenerFactory) NewListener(fnd api.FunctionDefinition) experimental.FunctionListener {
 	exported := len(fnd.ExportNames()) > 0
-	if f.hostOnly && // choose functions defined or callable by the host
+	if (f.hostOnly || f.fsOnly) && // choose functions defined or callable by the host
 		fnd.GoFunction() == nil && // not defined by the host
 		!exported { // not callable by the host
 		return nil
 	}
 
 	var pLoggers []logging.ParamLogger
+	var pSampler logging.ParamSampler
 	var rLoggers []logging.ResultLogger
 	switch fnd.ModuleName() {
 	case wasi_snapshot_preview1.ModuleName:
-		pLoggers, rLoggers = wasi_snapshot_preview1.ValueLoggers(fnd)
+		if f.fsOnly && !wasilogging.IsFilesystemFunction(fnd) {
+			return nil
+		}
+		pLoggers, rLoggers = wasilogging.Config(fnd)
+	case "go":
+		// TODO: Now, gojs logging is filesystem only, but will need to be
+		// updated later.
+		pSampler, pLoggers, rLoggers = gologging.Config(fnd)
+		if len(pLoggers) == 0 && len(rLoggers) == 0 {
+			return nil // not yet supported
+		}
 	default:
+		if f.fsOnly {
+			return nil
+		}
 		pLoggers, rLoggers = logging.ValueLoggers(fnd)
 	}
 
@@ -85,6 +110,7 @@ func (f *loggingListenerFactory) NewListener(fnd api.FunctionDefinition) experim
 		beforePrefix: before,
 		afterPrefix:  after,
 		pLoggers:     pLoggers,
+		pSampler:     pSampler,
 		rLoggers:     rLoggers,
 	}
 }
@@ -103,12 +129,17 @@ type loggingListener struct {
 	w                         logging.Writer
 	beforePrefix, afterPrefix string
 	pLoggers                  []logging.ParamLogger
+	pSampler                  logging.ParamSampler
 	rLoggers                  []logging.ResultLogger
 }
 
 // Before logs to stdout the module and function name, prefixed with '-->' and
 // indented based on the call nesting level.
 func (l *loggingListener) Before(ctx context.Context, mod api.Module, _ api.FunctionDefinition, params []uint64) context.Context {
+	if s := l.pSampler; s != nil && !s(ctx, mod, params) {
+		return ctx
+	}
+
 	var nestLevel int
 	if ls := ctx.Value(logging.LoggerKey{}); ls != nil {
 		nestLevel = ls.(*logState).nestLevel
@@ -142,38 +173,38 @@ func (l *loggingListener) After(ctx context.Context, mod api.Module, _ api.Funct
 // logIndented logs an indented l.w like this: "-->\t\t\t$nestLevel$funcName\n"
 func (l *loggingListener) logIndented(ctx context.Context, mod api.Module, nestLevel int, isBefore bool, params []uint64, err error, results []uint64) {
 	for i := 1; i < nestLevel; i++ {
-		l.w.WriteByte('\t') // nolint
+		l.w.WriteByte('\t') //nolint
 	}
 	if isBefore { // before
-		l.w.WriteString(l.beforePrefix) // nolint
+		l.w.WriteString(l.beforePrefix) //nolint
 		l.logParams(ctx, mod, params)
 	} else { // after
-		l.w.WriteString(l.afterPrefix) // nolint
+		l.w.WriteString(l.afterPrefix) //nolint
 		if err != nil {
-			l.w.WriteString(" error: ")  // nolint
-			l.w.WriteString(err.Error()) // nolint
+			l.w.WriteString(" error: ")  //nolint
+			l.w.WriteString(err.Error()) //nolint
 		} else {
 			l.logResults(ctx, mod, params, results)
 		}
 	}
-	l.w.WriteByte('\n') // nolint
+	l.w.WriteByte('\n') //nolint
 
 	if f, ok := l.w.(flusher); ok {
-		f.Flush() // nolint
+		f.Flush() //nolint
 	}
 }
 
 func (l *loggingListener) logParams(ctx context.Context, mod api.Module, params []uint64) {
 	paramLen := len(l.pLoggers)
-	l.w.WriteByte('(') // nolint
+	l.w.WriteByte('(') //nolint
 	if paramLen > 0 {
 		l.pLoggers[0](ctx, mod, l.w, params)
 		for i := 1; i < paramLen; i++ {
-			l.w.WriteByte(',') // nolint
+			l.w.WriteByte(',') //nolint
 			l.pLoggers[i](ctx, mod, l.w, params)
 		}
 	}
-	l.w.WriteByte(')') // nolint
+	l.w.WriteByte(')') //nolint
 }
 
 func (l *loggingListener) logResults(ctx context.Context, mod api.Module, params, results []uint64) {
@@ -181,17 +212,17 @@ func (l *loggingListener) logResults(ctx context.Context, mod api.Module, params
 	if resultLen == 0 {
 		return
 	}
-	l.w.WriteByte(' ') // nolint
+	l.w.WriteByte(' ') //nolint
 	switch resultLen {
 	case 1:
 		l.rLoggers[0](ctx, mod, l.w, params, results)
 	default:
-		l.w.WriteByte('(') // nolint
+		l.w.WriteByte('(') //nolint
 		l.rLoggers[0](ctx, mod, l.w, params, results)
 		for i := 1; i < resultLen; i++ {
-			l.w.WriteByte(',') // nolint
+			l.w.WriteByte(',') //nolint
 			l.rLoggers[i](ctx, mod, l.w, params, results)
 		}
-		l.w.WriteByte(')') // nolint
+		l.w.WriteByte(')') //nolint
 	}
 }
diff --git a/imports/wasi_snapshot_preview1/fs.go b/imports/wasi_snapshot_preview1/fs.go
index 9a874974..ba832a4c 100644
--- a/imports/wasi_snapshot_preview1/fs.go
+++ b/imports/wasi_snapshot_preview1/fs.go
@@ -11,6 +11,7 @@ import (
 
 	"github.com/tetratelabs/wazero/api"
 	internalsys "github.com/tetratelabs/wazero/internal/sys"
+	"github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -160,12 +161,12 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	}
 
 	filetype := getWasiFiletype(stat.Mode())
-	var fdflags wasiFdflags
+	var fdflags uint16
 
 	// Determine if it is writeable
 	if w := fsc.FdWriter(fd); w != nil {
 		// TODO: maybe cache flags to open instead
-		fdflags = wasiFdflagsAppend
+		fdflags = wasi_snapshot_preview1.FD_APPEND
 	}
 
 	writeFdstat(buf, filetype, fdflags)
@@ -173,28 +174,18 @@ func fdFdstatGetFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	return ErrnoSuccess
 }
 
-type wasiFdflags = byte // actually 16-bit, but there aren't that many.
-const (
-	wasiFdflagsNone wasiFdflags = 1<<iota - 1
-	wasiFdflagsAppend
-	wasiFdflagsDsync
-	wasiFdflagsNonblock
-	wasiFdflagsRsync
-	wasiFdflagsSync
-)
-
 var blockFdstat = []byte{
-	byte(wasiFiletypeBlockDevice), 0, // filetype
+	wasi_snapshot_preview1.FILETYPE_BLOCK_DEVICE, 0, // filetype
 	0, 0, 0, 0, 0, 0, // fdflags
 	0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_base
 	0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
 }
 
-func writeFdstat(buf []byte, filetype wasiFiletype, fdflags wasiFdflags) {
+func writeFdstat(buf []byte, filetype uint8, fdflags uint16) {
 	// memory is re-used, so ensure the result is defaulted.
 	copy(buf, blockFdstat)
-	buf[0] = uint8(filetype)
-	buf[2] = fdflags
+	buf[0] = filetype
+	buf[2] = byte(fdflags)
 }
 
 // fdFdstatSetFlags is the WASI function named fdFdstatSetFlagsName which
@@ -259,20 +250,7 @@ var fdFdstatSetRights = stubFunction(
 // Note: This is similar to `fstat` in POSIX.
 // See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-fd_filestat_getfd-fd---errno-filestat
 // and https://linux.die.net/man/3/fstat
-var fdFilestatGet = newHostFunc(fdFilestatGetName, fdFilestatGetFn, []api.ValueType{i32, i32}, "fd", "result.buf")
-
-type wasiFiletype uint8
-
-const (
-	wasiFiletypeUnknown wasiFiletype = iota
-	wasiFiletypeBlockDevice
-	wasiFiletypeCharacterDevice
-	wasiFiletypeDirectory
-	wasiFiletypeRegularFile
-	wasiFiletypeSocketDgram
-	wasiFiletypeSocketStream
-	wasiFiletypeSymbolicLink
-)
+var fdFilestatGet = newHostFunc(fdFilestatGetName, fdFilestatGetFn, []api.ValueType{i32, i32}, "fd", "result.filestat")
 
 // fdFilestatGetFn cannot currently use proxyResultParams because filestat is
 // larger than api.ValueTypeI64 (i64 == 8 bytes, but filestat is 64).
@@ -299,18 +277,18 @@ func fdFilestatGetFunc(mod api.Module, fd, resultBuf uint32) Errno {
 	return ErrnoSuccess
 }
 
-func getWasiFiletype(fileMode fs.FileMode) wasiFiletype {
-	wasiFileType := wasiFiletypeUnknown
+func getWasiFiletype(fileMode fs.FileMode) uint8 {
+	wasiFileType := wasi_snapshot_preview1.FILETYPE_UNKNOWN
 	if fileMode&fs.ModeDevice != 0 {
-		wasiFileType = wasiFiletypeBlockDevice
+		wasiFileType = wasi_snapshot_preview1.FILETYPE_BLOCK_DEVICE
 	} else if fileMode&fs.ModeCharDevice != 0 {
-		wasiFileType = wasiFiletypeCharacterDevice
+		wasiFileType = wasi_snapshot_preview1.FILETYPE_CHARACTER_DEVICE
 	} else if fileMode&fs.ModeDir != 0 {
-		wasiFileType = wasiFiletypeDirectory
+		wasiFileType = wasi_snapshot_preview1.FILETYPE_DIRECTORY
 	} else if fileMode&fs.ModeType == 0 {
-		wasiFileType = wasiFiletypeRegularFile
+		wasiFileType = wasi_snapshot_preview1.FILETYPE_REGULAR_FILE
 	} else if fileMode&fs.ModeSymlink != 0 {
-		wasiFileType = wasiFiletypeSymbolicLink
+		wasiFileType = wasi_snapshot_preview1.FILETYPE_SYMBOLIC_LINK
 	}
 	return wasiFileType
 }
@@ -318,7 +296,7 @@ func getWasiFiletype(fileMode fs.FileMode) wasiFiletype {
 var blockFilestat = []byte{
 	0, 0, 0, 0, 0, 0, 0, 0, // device
 	0, 0, 0, 0, 0, 0, 0, 0, // inode
-	byte(wasiFiletypeBlockDevice), 0, 0, 0, 0, 0, 0, 0, // filetype
+	wasi_snapshot_preview1.FILETYPE_BLOCK_DEVICE, 0, 0, 0, 0, 0, 0, 0, // filetype
 	1, 0, 0, 0, 0, 0, 0, 0, // nlink
 	0, 0, 0, 0, 0, 0, 0, 0, // filesize
 	0, 0, 0, 0, 0, 0, 0, 0, // atim
@@ -333,7 +311,7 @@ func writeFilestat(buf []byte, stat fs.FileInfo) {
 
 	// memory is re-used, so ensure the result is defaulted.
 	copy(buf, blockFilestat[:32])
-	buf[16] = uint8(filetype)
+	buf[16] = filetype
 	le.PutUint64(buf[32:], filesize)     // filesize
 	le.PutUint64(buf[40:], uint64(mtim)) // atim
 	le.PutUint64(buf[48:], uint64(mtim)) // mtim
@@ -886,9 +864,9 @@ func writeDirent(buf []byte, dNext uint64, dNamlen uint32, dType bool) {
 	le.PutUint64(buf[8:], 0)        // no d_ino
 	le.PutUint32(buf[16:], dNamlen) // d_namlen
 
-	filetype := wasiFiletypeRegularFile
+	filetype := wasi_snapshot_preview1.FILETYPE_REGULAR_FILE
 	if dType {
-		filetype = wasiFiletypeDirectory
+		filetype = wasi_snapshot_preview1.FILETYPE_DIRECTORY
 	}
 	le.PutUint32(buf[20:], uint32(filetype)) //  d_type
 }
@@ -1162,11 +1140,9 @@ var pathCreateDirectory = stubFunction(
 var pathFilestatGet = newHostFunc(
 	pathFilestatGetName, pathFilestatGetFn,
 	[]api.ValueType{i32, i32, i32, i32, i32},
-	"fd", "flags", "path", "path_len", "result.buf",
+	"fd", "flags", "path", "path_len", "result.filestat",
 )
 
-// pathFilestatGetFn cannot currently use proxyResultParams because filestat is
-// larger than api.ValueTypeI64 (i64 == 8 bytes, but filestat is 64).
 func pathFilestatGetFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
@@ -1294,21 +1270,6 @@ var pathOpen = newHostFunc(
 	"fd", "dirflags", "path", "path_len", "oflags", "fs_rights_base", "fs_rights_inheriting", "fdflags", "result.opened_fd",
 )
 
-// wasiOflags are open flags used by pathOpen
-// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-oflags-flagsu16
-type wasiOflags = byte // actually 16-bit, but there aren't that many.
-const (
-	wasiOflagsNone wasiFdflags = 1<<iota - 1 // nolint
-	// wasiOflagsCreat creates a file if it does not exist.
-	wasiOflagsCreat // nolint
-	// wasiOflagsDirectory fails if not a directory.
-	wasiOflagsDirectory
-	// wasiOflagsExcl fails if file already exists.
-	wasiOflagsExcl // nolint
-	// wasiOflagsTrunc truncates the file to size 0.
-	wasiOflagsTrunc // nolint
-)
-
 func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	fsc := mod.(*wasm.CallContext).Sys.FS()
 
@@ -1324,13 +1285,13 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	// oflags are currently not something we can pass to the filesystem to
 	// enforce, because fs.FS has no flags parameter. So, we have to validate
 	// them externally, in worst case after the file was already allocated.
-	oflags := wasiOflags(uint32(params[4]))
+	oflags := uint16(params[4])
 
 	// rights aren't used
 	_, _ = params[5], params[6]
 
-	// TODO: only notable fdflag for opening is wasiFdflagsAppend
-	_ /* fdflags */ = wasiFdflags(uint32(params[7]))
+	// TODO: only notable fdflag for opening is FD_APPEND
+	_ /* fdflags */ = uint16(params[7])
 	resultOpenedFd := uint32(params[8])
 
 	// Note: We don't handle AT_FDCWD, as that's resolved in the compiler.
@@ -1359,7 +1320,7 @@ func pathOpenFn(_ context.Context, mod api.Module, params []uint64) Errno {
 	}
 
 	// Check any flags that require the file to evaluate.
-	if oflags&wasiOflagsDirectory != 0 {
+	if oflags&wasi_snapshot_preview1.O_DIRECTORY != 0 {
 		if errno = failIfNotDirectory(fsc, newFD); errno != ErrnoSuccess {
 			return errno
 		}
diff --git a/imports/wasi_snapshot_preview1/logging/logging.go b/imports/wasi_snapshot_preview1/logging/logging.go
new file mode 100644
index 00000000..88757969
--- /dev/null
+++ b/imports/wasi_snapshot_preview1/logging/logging.go
@@ -0,0 +1,301 @@
+package logging
+
+import (
+	"context"
+	"encoding/binary"
+	"strconv"
+	"strings"
+
+	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/logging"
+	"github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
+)
+
+var le = binary.LittleEndian
+
+func IsFilesystemFunction(fnd api.FunctionDefinition) bool {
+	switch {
+	case strings.HasPrefix(fnd.Name(), "path_"):
+		return true
+	case strings.HasPrefix(fnd.Name(), "fd_"):
+		return true
+	}
+	return false
+}
+
+func Config(fnd api.FunctionDefinition) (pLoggers []logging.ParamLogger, rLoggers []logging.ResultLogger) {
+	switch fnd.Name() {
+	case "fd_prestat_get":
+		pLoggers = []logging.ParamLogger{logging.NewParamLogger(0, "fd", logging.ValueTypeI32)}
+		rLoggers = []logging.ResultLogger{resultParamLogger("prestat", logPrestat(1).Log), logErrno}
+		return
+	case "proc_exit":
+		return logging.ValueLoggers(fnd)
+	}
+
+	for idx := uint32(0); idx < uint32(len(fnd.ParamTypes())); idx++ {
+		name := fnd.ParamNames()[idx]
+		var logger logging.ParamLogger
+
+		if isLookupFlags(fnd, name) {
+			logger = (&logLookupflags{name, idx}).Log
+			pLoggers = append(pLoggers, logger)
+			continue
+		}
+
+		isResult := strings.HasPrefix(name, "result.")
+
+		if strings.Contains(name, "path") {
+			if isResult {
+				name = resultParamName(name)
+				logger = logString(idx).Log
+				rLoggers = append(rLoggers, resultParamLogger(name, logger))
+			} else {
+				logger = logging.NewParamLogger(idx, name, logging.ValueTypeString)
+				pLoggers = append(pLoggers, logger)
+			}
+			idx++
+			continue
+		}
+
+		switch name {
+		case "fdflags":
+			logger = logFdflags(idx).Log
+		case "oflags":
+			logger = logOflags(idx).Log
+		case "fs_rights_base":
+			logger = logFsRightsBase(idx).Log
+		case "fs_rights_inheriting":
+			logger = logFsRightsInheriting(idx).Log
+		case "result.nread", "result.nwritten", "result.opened_fd":
+			name = resultParamName(name)
+			logger = logMemI32(idx).Log
+			rLoggers = append(rLoggers, resultParamLogger(name, logger))
+			continue
+		case "result.filestat":
+			name = resultParamName(name)
+			logger = logFilestat(idx).Log
+			rLoggers = append(rLoggers, resultParamLogger(name, logger))
+			continue
+		case "result.stat":
+			name = resultParamName(name)
+			logger = logFdstat(idx).Log
+			rLoggers = append(rLoggers, resultParamLogger(name, logger))
+			continue
+		default:
+			logger = logging.NewParamLogger(idx, name, fnd.ParamTypes()[idx])
+		}
+		pLoggers = append(pLoggers, logger)
+	}
+	// All WASI functions except proc_after return only an logErrno result.
+	rLoggers = append(rLoggers, logErrno)
+	return
+}
+
+func isLookupFlags(fnd api.FunctionDefinition, name string) bool {
+	switch fnd.Name() {
+	case "path_filestat_get", "path_filestat_set_times":
+		return name == "flags"
+	case "path_link":
+		return name == "old_flags"
+	case "path_open":
+		return name == "dirflags"
+	}
+	return false
+}
+
+func logErrno(_ context.Context, _ api.Module, w logging.Writer, _, results []uint64) {
+	errno := wasi_snapshot_preview1.ErrnoName(uint32(results[0]))
+	w.WriteString("errno=") //nolint
+	w.WriteString(errno)    //nolint
+}
+
+type logMemI32 uint32
+
+func (i logMemI32) Log(_ context.Context, mod api.Module, w logging.Writer, params []uint64) {
+	if v, ok := mod.Memory().ReadUint32Le(uint32(params[i])); ok {
+		writeI32(w, v)
+	}
+}
+
+type logFilestat uint32
+
+func (i logFilestat) Log(_ context.Context, mod api.Module, w logging.Writer, params []uint64) {
+	offset, byteCount := uint32(params[i]), uint32(64)
+	if buf, ok := mod.Memory().Read(offset, byteCount); ok {
+		w.WriteString("{filetype=")                                 //nolint
+		w.WriteString(wasi_snapshot_preview1.FiletypeName(buf[16])) //nolint
+		w.WriteString(",size=")                                     //nolint
+		writeI64(w, le.Uint64(buf[32:]))
+		w.WriteString(",mtim=") //nolint
+		writeI64(w, le.Uint64(buf[40:]))
+		w.WriteString("}") //nolint
+	}
+}
+
+type logFdstat uint32
+
+func (i logFdstat) Log(_ context.Context, mod api.Module, w logging.Writer, params []uint64) {
+	offset, byteCount := uint32(params[i]), uint32(24)
+	if buf, ok := mod.Memory().Read(offset, byteCount); ok {
+		w.WriteString("{filetype=")                                //nolint
+		w.WriteString(wasi_snapshot_preview1.FiletypeName(buf[0])) //nolint
+		w.WriteString(",fdflags=")                                 //nolint
+		writeFlags(w, fdflagNames[:], int(le.Uint16(buf[2:])))
+		w.WriteString(",fs_rights_base=") //nolint
+		writeFlags(w, rightNames[:], int(le.Uint16(buf[8:])))
+		w.WriteString(",fs_rights_inheriting=") //nolint
+		writeFlags(w, rightNames[:], int(le.Uint16(buf[16:])))
+		w.WriteString("}") //nolint
+	}
+}
+
+type logString uint32
+
+func (i logString) Log(_ context.Context, mod api.Module, w logging.Writer, params []uint64) {
+	offset, byteCount := uint32(params[i]), uint32(params[i+1])
+	if s, ok := mod.Memory().Read(offset, byteCount); ok {
+		w.Write(s) //nolint
+	}
+}
+
+type logPrestat uint32
+
+// Log writes the only valid field: pr_name_len
+// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#prestat_dir
+func (i logPrestat) Log(_ context.Context, mod api.Module, w logging.Writer, params []uint64) {
+	offset := uint32(params[i]) + 4 // skip to pre_name_len field
+	if nameLen, ok := mod.Memory().ReadUint32Le(offset); ok {
+		w.WriteString("{pr_name_len=") //nolint
+		writeI32(w, nameLen)
+		w.WriteString("}") //nolint
+	}
+}
+
+// resultParamLogger logs the value of the parameter on ESUCCESS.
+func resultParamLogger(name string, pLogger logging.ParamLogger) logging.ResultLogger {
+	prefix := name + "="
+	return func(ctx context.Context, mod api.Module, w logging.Writer, params, results []uint64) {
+		w.WriteString(prefix) //nolint
+		if results[0] == 0 {  // ESUCCESS
+			pLogger(ctx, mod, w, params)
+		}
+	}
+}
+
+type logFdflags int
+
+func (i logFdflags) Log(_ context.Context, _ api.Module, w logging.Writer, params []uint64) {
+	w.WriteString("fdflags=") //nolint
+	writeFlags(w, fdflagNames[:], int(params[i]))
+}
+
+type logLookupflags struct {
+	name string
+	i    uint32
+}
+
+func (l *logLookupflags) Log(_ context.Context, _ api.Module, w logging.Writer, params []uint64) {
+	w.WriteString(l.name) //nolint
+	w.WriteByte('=')      //nolint
+	writeFlags(w, lookupflagNames[:], int(params[l.i]))
+}
+
+type logFsRightsBase uint32
+
+func (i logFsRightsBase) Log(_ context.Context, _ api.Module, w logging.Writer, params []uint64) {
+	w.WriteString("fs_rights_base=") //nolint
+	writeFlags(w, rightNames[:], int(params[i]))
+}
+
+type logFsRightsInheriting uint32
+
+func (i logFsRightsInheriting) Log(_ context.Context, _ api.Module, w logging.Writer, params []uint64) {
+	w.WriteString("fs_rights_inheriting=") //nolint
+	writeFlags(w, rightNames[:], int(params[i]))
+}
+
+type logOflags int
+
+func (i logOflags) Log(_ context.Context, _ api.Module, w logging.Writer, params []uint64) {
+	w.WriteString("oflags=") //nolint
+	writeFlags(w, oflagNames[:], int(params[i]))
+}
+
+func writeFlags(w logging.Writer, names []string, f int) {
+	first := true
+	for i, sf := range names {
+		target := 1 << i
+		if target&f != 0 {
+			if !first {
+				w.WriteByte('|') //nolint
+			} else {
+				first = false
+			}
+			w.WriteString(sf) //nolint
+		}
+	}
+}
+
+var oflagNames = [...]string{
+	"CREAT",
+	"DIRECTORY",
+	"EXCL",
+	"TRUNC",
+}
+
+var fdflagNames = [...]string{
+	"APPEND",
+	"DSYNC",
+	"NONBLOCK",
+	"RSYNC",
+	"SYNC",
+}
+
+var lookupflagNames = [...]string{
+	"SYMLINK_FOLLOW",
+}
+
+var rightNames = [...]string{
+	"FD_DATASYNC",
+	"FD_READ",
+	"FD_SEEK",
+	"FDSTAT_SET_FLAGS",
+	"FD_SYNC",
+	"FD_TELL",
+	"FD_WRITE",
+	"FD_ADVISE",
+	"FD_ALLOCATE",
+	"PATH_CREATE_DIRECTORY",
+	"PATH_CREATE_FILE",
+	"PATH_LINK_SOURCE",
+	"PATH_LINK_TARGET",
+	"PATH_OPEN",
+	"FD_READDIR",
+	"PATH_READLINK",
+	"PATH_RENAME_SOURCE",
+	"PATH_RENAME_TARGET",
+	"PATH_FILESTAT_GET",
+	"PATH_FILESTAT_SET_SIZE",
+	"PATH_FILESTAT_SET_TIMES",
+	"FD_FILESTAT_GET",
+	"FD_FILESTAT_SET_SIZE",
+	"FD_FILESTAT_SET_TIMES",
+	"PATH_SYMLINK",
+	"PATH_REMOVE_DIRECTORY",
+	"PATH_UNLINK_FILE",
+	"POLL_FD_READWRITE",
+	"SOCK_SHUTDOWN",
+}
+
+func resultParamName(name string) string {
+	return name[7:] // without "result."
+}
+
+func writeI32(w logging.Writer, v uint32) {
+	w.WriteString(strconv.FormatInt(int64(int32(v)), 10)) //nolint
+}
+
+func writeI64(w logging.Writer, v uint64) {
+	w.WriteString(strconv.FormatInt(int64(v), 10)) //nolint
+}
diff --git a/internal/engine/compiler/impl_amd64.go b/internal/engine/compiler/impl_amd64.go
index bae74dcf..60163b6d 100644
--- a/internal/engine/compiler/impl_amd64.go
+++ b/internal/engine/compiler/impl_amd64.go
@@ -50,7 +50,7 @@ var (
 )
 
 var (
-	amd64UnreservedVectorRegisters = []asm.Register{ // nolint
+	amd64UnreservedVectorRegisters = []asm.Register{ //nolint
 		amd64.RegX0, amd64.RegX1, amd64.RegX2, amd64.RegX3,
 		amd64.RegX4, amd64.RegX5, amd64.RegX6, amd64.RegX7,
 		amd64.RegX8, amd64.RegX9, amd64.RegX10, amd64.RegX11,
@@ -61,7 +61,7 @@ var (
 	// TODO: Maybe it is safe just save rbp, rsp somewhere
 	// in Go-allocated variables, and reuse these registers
 	// in compiled functions and write them back before returns.
-	amd64UnreservedGeneralPurposeRegisters = []asm.Register{ // nolint
+	amd64UnreservedGeneralPurposeRegisters = []asm.Register{ //nolint
 		amd64.RegAX, amd64.RegCX, amd64.RegDX, amd64.RegBX,
 		amd64.RegSI, amd64.RegDI, amd64.RegR8, amd64.RegR9,
 		amd64.RegR10, amd64.RegR11, amd64.RegR12,
diff --git a/internal/engine/compiler/impl_arm64.go b/internal/engine/compiler/impl_arm64.go
index 3b6c1705..98793c56 100644
--- a/internal/engine/compiler/impl_arm64.go
+++ b/internal/engine/compiler/impl_arm64.go
@@ -60,7 +60,7 @@ var (
 	// Note (see arm64 section in https://go.dev/doc/asm):
 	// * RegR18 is reserved as a platform register, and we don't use it in Compiler.
 	// * RegR28 is reserved for Goroutine by Go runtime, and we don't use it in Compiler.
-	arm64UnreservedGeneralPurposeRegisters = []asm.Register{ // nolint
+	arm64UnreservedGeneralPurposeRegisters = []asm.Register{ //nolint
 		arm64.RegR3, arm64.RegR4, arm64.RegR5, arm64.RegR6, arm64.RegR7, arm64.RegR8,
 		arm64.RegR9, arm64.RegR10, arm64.RegR11, arm64.RegR12, arm64.RegR13,
 		arm64.RegR14, arm64.RegR15, arm64.RegR16, arm64.RegR17, arm64.RegR19,
diff --git a/internal/gojs/builtin.go b/internal/gojs/builtin.go
index 1c314056..ed2b07f5 100644
--- a/internal/gojs/builtin.go
+++ b/internal/gojs/builtin.go
@@ -6,43 +6,11 @@ import (
 	"github.com/tetratelabs/wazero/internal/gojs/goos"
 )
 
-const (
-	// predefined
-
-	// The below are derived from analyzing `*_js.go` source.
-	idObjectConstructor uint32 = goos.NextID + iota
-	idArrayConstructor
-	idJsProcess
-	idJsfs
-	idJsfsConstants
-	idUint8ArrayConstructor
-	idJsCrypto
-	idJsDateConstructor
-	idJsDate
-	idHttpFetch
-	idHttpHeaders
-	nextID
-)
-
-const (
-	refObjectConstructor      = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idObjectConstructor)
-	refArrayConstructor       = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idArrayConstructor)
-	refJsProcess              = (goos.NanHead|goos.Ref(goos.TypeFlagObject))<<32 | goos.Ref(idJsProcess)
-	refJsfs                   = (goos.NanHead|goos.Ref(goos.TypeFlagObject))<<32 | goos.Ref(idJsfs)
-	refJsfsConstants          = (goos.NanHead|goos.Ref(goos.TypeFlagObject))<<32 | goos.Ref(idJsfsConstants)
-	refUint8ArrayConstructor  = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idUint8ArrayConstructor)
-	refJsCrypto               = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idJsCrypto)
-	refJsDateConstructor      = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idJsDateConstructor)
-	refJsDate                 = (goos.NanHead|goos.Ref(goos.TypeFlagObject))<<32 | goos.Ref(idJsDate)
-	refHttpFetch              = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idHttpFetch)
-	refHttpHeadersConstructor = (goos.NanHead|goos.Ref(goos.TypeFlagFunction))<<32 | goos.Ref(idHttpHeaders)
-)
-
 // newJsGlobal = js.Global() // js.go init
 func newJsGlobal(rt http.RoundTripper) *jsVal {
 	var fetchProperty interface{} = undefined
 	if rt != nil {
-		fetchProperty = refHttpFetch
+		fetchProperty = goos.RefHttpFetch
 	}
 	return newJsVal(goos.RefValueGlobal, "global").
 		addProperties(map[string]interface{}{
@@ -68,14 +36,14 @@ var (
 
 	// objectConstructor is used by js.ValueOf to make `map[string]any`.
 	//	Get("Object") // js.go init
-	objectConstructor = newJsVal(refObjectConstructor, "Object")
+	objectConstructor = newJsVal(goos.RefObjectConstructor, "Object")
 
 	// arrayConstructor is used by js.ValueOf to make `[]any`.
 	//	Get("Array") // js.go init
-	arrayConstructor = newJsVal(refArrayConstructor, "Array")
+	arrayConstructor = newJsVal(goos.RefArrayConstructor, "Array")
 
 	// jsProcess = js.Global().Get("process") // fs_js.go init
-	jsProcess = newJsVal(refJsProcess, "process").
+	jsProcess = newJsVal(goos.RefJsProcess, "process").
 			addProperties(map[string]interface{}{
 			"pid":  float64(1),        // Get("pid").Int() in syscall_js.go for syscall.Getpid
 			"ppid": goos.RefValueZero, // Get("ppid").Int() in syscall_js.go for syscall.Getppid
@@ -92,5 +60,5 @@ var (
 	//	// fs_js.go, rand_js.go, roundtrip_js.go init
 	//
 	// It has only one invocation pattern: `buf := uint8Array.New(len(b))`
-	uint8ArrayConstructor = newJsVal(refUint8ArrayConstructor, "Uint8Array")
+	uint8ArrayConstructor = newJsVal(goos.RefUint8ArrayConstructor, "Uint8Array")
 )
diff --git a/internal/gojs/crypto.go b/internal/gojs/crypto.go
index 787b958f..35172db9 100644
--- a/internal/gojs/crypto.go
+++ b/internal/gojs/crypto.go
@@ -4,6 +4,7 @@ import (
 	"context"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/gojs/goos"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -14,7 +15,7 @@ import (
 //	jsCrypto.Call("getRandomValues", a /* uint8Array */)
 //
 // This is defined as `Get("crypto")` in rand_js.go init
-var jsCrypto = newJsVal(refJsCrypto, "crypto").
+var jsCrypto = newJsVal(goos.RefJsCrypto, "crypto").
 	addFunction("getRandomValues", &getRandomValues{})
 
 type getRandomValues struct{}
diff --git a/internal/gojs/custom/fs.go b/internal/gojs/custom/fs.go
new file mode 100644
index 00000000..823c77bb
--- /dev/null
+++ b/internal/gojs/custom/fs.go
@@ -0,0 +1,61 @@
+package custom
+
+const (
+	NameCallback = "callback"
+
+	NameFs        = "fs"
+	NameFsOpen    = "open"
+	NameFsStat    = "stat"
+	NameFsFstat   = "fstat"
+	NameFsLstat   = "lstat"
+	NameFsClose   = "close"
+	NameFsRead    = "read"
+	NameFsWrite   = "write"
+	NameFsReaddir = "readdir"
+)
+
+// FsNameSection are the functions defined in the object named NameFs. Results
+// here are those set to the current event object, but effectively are results
+// of the host function.
+var FsNameSection = map[string]*Names{
+	NameFsOpen: {
+		Name:        NameFsOpen,
+		ParamNames:  []string{"name", "flags", "perm", NameCallback},
+		ResultNames: []string{"err", "fd"},
+	},
+	NameFsStat: {
+		Name:        NameFsStat,
+		ParamNames:  []string{"name", NameCallback},
+		ResultNames: []string{"err", "stat"},
+	},
+	NameFsFstat: {
+		Name:        NameFsFstat,
+		ParamNames:  []string{"fd", NameCallback},
+		ResultNames: []string{"err", "stat"},
+	},
+	NameFsLstat: {
+		Name:        NameFsLstat,
+		ParamNames:  []string{"name", NameCallback},
+		ResultNames: []string{"err", "stat"},
+	},
+	NameFsClose: {
+		Name:        NameFsClose,
+		ParamNames:  []string{"fd", NameCallback},
+		ResultNames: []string{"err", "ok"},
+	},
+	NameFsRead: {
+		Name:        NameFsRead,
+		ParamNames:  []string{"fd", "buf", "offset", "byteCount", "fOffset", NameCallback},
+		ResultNames: []string{"err", "n"},
+	},
+	NameFsWrite: {
+		Name:        NameFsWrite,
+		ParamNames:  []string{"fd", "buf", "offset", "byteCount", "fOffset", NameCallback},
+		ResultNames: []string{"err", "n"},
+	},
+	NameFsReaddir: {
+		Name:        NameFsReaddir,
+		ParamNames:  []string{"name", NameCallback},
+		ResultNames: []string{"err", "dirents"},
+	},
+}
diff --git a/internal/gojs/fs.go b/internal/gojs/fs.go
index e8dab231..a4487501 100644
--- a/internal/gojs/fs.go
+++ b/internal/gojs/fs.go
@@ -9,6 +9,7 @@ import (
 	"syscall"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/gojs/custom"
 	"github.com/tetratelabs/wazero/internal/gojs/goos"
 	internalsys "github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/wasm"
@@ -20,18 +21,18 @@ var (
 	// js.fsCall conventions:
 	// * funcWrapper callback is the last parameter
 	//   * arg0 is error and up to one result in arg1
-	jsfs = newJsVal(refJsfs, "fs").
+	jsfs = newJsVal(goos.RefJsfs, custom.NameFs).
 		addProperties(map[string]interface{}{
 			"constants": jsfsConstants, // = jsfs.Get("constants") // init
 		}).
-		addFunction("open", &jsfsOpen{}).
-		addFunction("stat", &jsfsStat{}).
-		addFunction("fstat", &jsfsFstat{}).
-		addFunction("lstat", &jsfsStat{}). // because fs.FS doesn't support symlink
-		addFunction("close", &jsfsClose{}).
-		addFunction("read", &jsfsRead{}).
-		addFunction("write", &jsfsWrite{}).
-		addFunction("readdir", &jsfsReaddir{})
+		addFunction(custom.NameFsOpen, &jsfsOpen{}).
+		addFunction(custom.NameFsStat, &jsfsStat{}).
+		addFunction(custom.NameFsFstat, &jsfsFstat{}).
+		addFunction(custom.NameFsLstat, &jsfsStat{}). // because fs.FS doesn't support symlink
+		addFunction(custom.NameFsClose, &jsfsClose{}).
+		addFunction(custom.NameFsRead, &jsfsRead{}).
+		addFunction(custom.NameFsWrite, &jsfsWrite{}).
+		addFunction(custom.NameFsReaddir, &jsfsReaddir{})
 
 	// TODO: stub all these with syscall.ENOSYS
 	//	* _, err := fsCall("mkdir", path, perm) // syscall.Mkdir
@@ -52,7 +53,7 @@ var (
 	//	* _, err := fsCall("fsync", fd) // syscall.Fsync
 
 	// jsfsConstants = jsfs Get("constants") // fs_js.go init
-	jsfsConstants = newJsVal(refJsfsConstants, "constants").
+	jsfsConstants = newJsVal(goos.RefJsfsConstants, "constants").
 			addProperties(map[string]interface{}{
 			"O_WRONLY": oWRONLY,
 			"O_RDWR":   oRDWR,
@@ -94,7 +95,7 @@ func (*jsfsOpen) invoke(ctx context.Context, mod api.Module, args ...interface{}
 	callback := args[3].(funcWrapper)
 
 	fd, err := syscallOpen(mod, name, flags, perm)
-	return callback.invoke(ctx, mod, refJsfs, err, fd) // note: error first
+	return callback.invoke(ctx, mod, goos.RefJsfs, err, fd) // note: error first
 }
 
 // jsfsStat is used for syscall.Stat
@@ -108,7 +109,7 @@ func (*jsfsStat) invoke(ctx context.Context, mod api.Module, args ...interface{}
 	callback := args[1].(funcWrapper)
 
 	stat, err := syscallStat(mod, name)
-	return callback.invoke(ctx, mod, refJsfs, err, stat) // note: error first
+	return callback.invoke(ctx, mod, goos.RefJsfs, err, stat) // note: error first
 }
 
 // syscallStat is like syscall.Stat
@@ -135,7 +136,7 @@ func (*jsfsFstat) invoke(ctx context.Context, mod api.Module, args ...interface{
 	callback := args[1].(funcWrapper)
 
 	fstat, err := syscallFstat(fsc, fd)
-	return callback.invoke(ctx, mod, refJsfs, err, fstat) // note: error first
+	return callback.invoke(ctx, mod, goos.RefJsfs, err, fstat) // note: error first
 }
 
 // syscallFstat is like syscall.Fstat
@@ -149,8 +150,8 @@ func syscallFstat(fsc *internalsys.FSContext, fd uint32) (*jsSt, error) {
 		ret.isDir = stat.IsDir()
 		// TODO ret.dev=stat.Sys
 		ret.mode = uint32(stat.Mode())
-		ret.size = uint32(stat.Size())
-		ret.mtimeMs = uint32(stat.ModTime().UnixMilli())
+		ret.size = uint64(stat.Size())
+		ret.mtimeMs = uint64(stat.ModTime().UnixMilli())
 		return ret, nil
 	}
 }
@@ -168,7 +169,7 @@ func (*jsfsClose) invoke(ctx context.Context, mod api.Module, args ...interface{
 	callback := args[1].(funcWrapper)
 
 	err := syscallClose(fsc, fd)
-	return callback.invoke(ctx, mod, refJsfs, err, true) // note: error first
+	return callback.invoke(ctx, mod, goos.RefJsfs, err, true) // note: error first
 }
 
 // syscallClose is like syscall.Close
@@ -198,7 +199,7 @@ func (*jsfsRead) invoke(ctx context.Context, mod api.Module, args ...interface{}
 	callback := args[5].(funcWrapper)
 
 	n, err := syscallRead(mod, fd, fOffset, buf.slice[offset:offset+byteCount])
-	return callback.invoke(ctx, mod, refJsfs, err, n) // note: error first
+	return callback.invoke(ctx, mod, goos.RefJsfs, err, n) // note: error first
 }
 
 // syscallRead is like syscall.Read
@@ -251,9 +252,9 @@ func (*jsfsWrite) invoke(ctx context.Context, mod api.Module, args ...interface{
 
 	if byteCount > 0 { // empty is possible on EOF
 		n, err := syscallWrite(mod, fd, fOffset, buf.slice[offset:offset+byteCount])
-		return callback.invoke(ctx, mod, refJsfs, err, n) // note: error first
+		return callback.invoke(ctx, mod, goos.RefJsfs, err, n) // note: error first
 	}
-	return callback.invoke(ctx, mod, refJsfs, nil, goos.RefValueZero)
+	return callback.invoke(ctx, mod, goos.RefJsfs, nil, goos.RefValueZero)
 }
 
 // syscallWrite is like syscall.Write
@@ -284,7 +285,7 @@ func (*jsfsReaddir) invoke(ctx context.Context, mod api.Module, args ...interfac
 	callback := args[1].(funcWrapper)
 
 	stat, err := syscallReaddir(ctx, mod, name)
-	return callback.invoke(ctx, mod, refJsfs, err, stat) // note: error first
+	return callback.invoke(ctx, mod, goos.RefJsfs, err, stat) // note: error first
 }
 
 func syscallReaddir(_ context.Context, mod api.Module, name string) (*objectArray, error) {
@@ -377,12 +378,17 @@ type jsSt struct {
 	uid     uint32
 	gid     uint32
 	rdev    uint32
-	size    uint32
+	size    uint64
 	blksize uint32
 	blocks  uint32
-	atimeMs uint32
-	mtimeMs uint32
-	ctimeMs uint32
+	atimeMs uint64
+	mtimeMs uint64
+	ctimeMs uint64
+}
+
+// String implements fmt.Stringer
+func (s *jsSt) String() string {
+	return fmt.Sprintf("{isDir=%v,mode=%s,size=%d,mtimeMs=%d}", s.isDir, fs.FileMode(s.mode), s.size, s.mtimeMs)
 }
 
 // get implements jsGet.get
diff --git a/internal/gojs/goarch/wasm.go b/internal/gojs/goarch/wasm.go
index 9b023237..f31bdad1 100644
--- a/internal/gojs/goarch/wasm.go
+++ b/internal/gojs/goarch/wasm.go
@@ -113,7 +113,7 @@ func (s *stack) ParamUint32(i int) uint32 {
 
 // Refresh implements Stack.Refresh
 func (s *stack) Refresh(mod api.Module) {
-	s.refresh(mod.Memory(), getSP(mod))
+	s.refresh(mod.Memory(), GetSP(mod))
 }
 
 func (s *stack) refresh(mem api.Memory, sp uint32) {
@@ -155,11 +155,11 @@ func (s *stack) SetResultUint32(i int, v uint32) {
 	s.SetResult(i, uint64(v))
 }
 
-// getSP gets the stack pointer, which is needed prior to storing a value when
+// GetSP gets the stack pointer, which is needed prior to storing a value when
 // in an operation that can trigger a Go event handler.
 //
 // See https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L210-L213
-func getSP(mod api.Module) uint32 {
+func GetSP(mod api.Module) uint32 {
 	// Cheat by reading global[0] directly instead of through a function proxy.
 	// https://github.com/golang/go/blob/go1.19/src/runtime/rt0_js_wasm.s#L87-L90
 	return uint32(mod.(*wasm.CallContext).GlobalVal(0))
diff --git a/internal/gojs/goos/js.go b/internal/gojs/goos/goos.go
similarity index 78%
rename from internal/gojs/goos/js.go
rename to internal/gojs/goos/goos.go
index 893c0ec5..849af5f6 100644
--- a/internal/gojs/goos/js.go
+++ b/internal/gojs/goos/goos.go
@@ -26,6 +26,8 @@ import (
 type Ref uint64
 
 const (
+	// predefined
+
 	IdValueNaN uint32 = iota
 	IdValueZero
 	IdValueNull
@@ -33,6 +35,20 @@ const (
 	IdValueFalse
 	IdValueGlobal
 	IdJsGo
+
+	// The below are derived from analyzing `*_js.go` source.
+
+	IdObjectConstructor
+	IdArrayConstructor
+	IdJsProcess
+	IdJsfs
+	IdJsfsConstants
+	IdUint8ArrayConstructor
+	IdJsCrypto
+	IdJsDateConstructor
+	IdJsDate
+	IdHttpFetch
+	IdHttpHeaders
 	NextID
 )
 
@@ -45,6 +61,18 @@ const (
 	RefValueFalse     = (NanHead|Ref(TypeFlagNone))<<32 | Ref(IdValueFalse)
 	RefValueGlobal    = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdValueGlobal)
 	RefJsGo           = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsGo)
+
+	RefObjectConstructor      = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdObjectConstructor)
+	RefArrayConstructor       = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdArrayConstructor)
+	RefJsProcess              = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsProcess)
+	RefJsfs                   = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsfs)
+	RefJsfsConstants          = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsfsConstants)
+	RefUint8ArrayConstructor  = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdUint8ArrayConstructor)
+	RefJsCrypto               = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdJsCrypto)
+	RefJsDateConstructor      = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdJsDateConstructor)
+	RefJsDate                 = (NanHead|Ref(TypeFlagObject))<<32 | Ref(IdJsDate)
+	RefHttpFetch              = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdHttpFetch)
+	RefHttpHeadersConstructor = (NanHead|Ref(TypeFlagFunction))<<32 | Ref(IdHttpHeaders)
 )
 
 type TypeFlag byte
@@ -54,7 +82,7 @@ const (
 	TypeFlagNone TypeFlag = iota
 	TypeFlagObject
 	TypeFlagString
-	TypeFlagSymbol // nolint
+	TypeFlagSymbol //nolint
 	TypeFlagFunction
 )
 
@@ -75,6 +103,10 @@ func (ref Ref) ParseFloat() (v float64, ok bool) {
 	return
 }
 
+// GetLastEventArgs returns the arguments to the last event created by
+// custom.NameSyscallValueCall.
+type GetLastEventArgs func(context.Context) []interface{}
+
 type ValLoader func(context.Context, Ref) interface{}
 
 type Stack interface {
@@ -86,6 +118,7 @@ type Stack interface {
 
 	ParamVal(ctx context.Context, i int, loader ValLoader) interface{}
 
+	// ParamVals is used by functions whose final parameter is an arg array.
 	ParamVals(ctx context.Context, mem api.Memory, i int, loader ValLoader) []interface{}
 
 	SetResultRef(i int, v Ref)
@@ -211,6 +244,10 @@ type stackFunc struct {
 
 // Call implements the same method as defined on api.GoModuleFunction.
 func (f *stackFunc) Call(ctx context.Context, mod api.Module, wasmStack []uint64) {
-	s := &stack{goarch.NewStack(f.name, mod.Memory(), uint32(wasmStack[0]))}
+	s := NewStack(f.name, mod.Memory(), uint32(wasmStack[0]))
 	f.f(ctx, mod, s)
 }
+
+func NewStack(name string, mem api.Memory, sp uint32) *stack {
+	return &stack{goarch.NewStack(name, mem, sp)}
+}
diff --git a/internal/gojs/http.go b/internal/gojs/http.go
index 4aa9395b..895ba7d8 100644
--- a/internal/gojs/http.go
+++ b/internal/gojs/http.go
@@ -13,7 +13,7 @@ import (
 )
 
 // headersConstructor = Get("Headers").New() // http.Roundtrip && "fetch"
-var headersConstructor = newJsVal(refHttpHeadersConstructor, "Headers")
+var headersConstructor = newJsVal(goos.RefHttpHeadersConstructor, "Headers")
 
 type RoundTripperKey struct{}
 
diff --git a/internal/gojs/logging/logging.go b/internal/gojs/logging/logging.go
new file mode 100644
index 00000000..02da4d0e
--- /dev/null
+++ b/internal/gojs/logging/logging.go
@@ -0,0 +1,174 @@
+package logging
+
+import (
+	"context"
+	"fmt"
+	"io/fs"
+	"os"
+	"strconv"
+
+	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/gojs"
+	"github.com/tetratelabs/wazero/internal/gojs/custom"
+	"github.com/tetratelabs/wazero/internal/gojs/goarch"
+	"github.com/tetratelabs/wazero/internal/gojs/goos"
+	"github.com/tetratelabs/wazero/internal/logging"
+)
+
+func Config(fnd api.FunctionDefinition) (pSampler logging.ParamSampler, pLoggers []logging.ParamLogger, rLoggers []logging.ResultLogger) {
+	switch fnd.Name() {
+	case custom.NameRuntimeWasmWrite:
+		pLoggers = []logging.ParamLogger{runtimeWasmWriteParamLogger}
+	case custom.NameSyscallValueCall:
+		pSampler = syscallValueCallParamSampler
+		pLoggers = []logging.ParamLogger{syscallValueCallParamLogger}
+		rLoggers = []logging.ResultLogger{syscallValueCallResultLogger}
+	default: // only filesystem for now
+	}
+	return
+}
+
+func runtimeWasmWriteParamLogger(_ context.Context, mod api.Module, w logging.Writer, params []uint64) {
+	mem := mod.Memory()
+	funcName := custom.NameSyscallValueCall
+	stack := goos.NewStack(funcName, mem, uint32(params[0]))
+	fd := stack.ParamUint32(0)
+	pLen := stack.ParamUint32(2)
+
+	w.WriteString(funcName) //nolint
+	w.WriteString("(fd=")   //nolint
+	writeI32(w, fd)
+	w.WriteString(",p_len=") //nolint
+	writeI32(w, pLen)
+	w.WriteByte(')') //nolint
+}
+
+func syscallValueCallParamLogger(ctx context.Context, mod api.Module, w logging.Writer, params []uint64) {
+	vRef, m, args := syscallValueCallParams(ctx, mod, params)
+
+	// TODO: add more than just filesystem
+	if vRef != goos.RefJsfs {
+		return
+	}
+
+	if m == custom.NameFsOpen {
+		w.WriteString("fs.open(")       //nolint
+		w.WriteString("name=")          //nolint
+		w.WriteString(args[0].(string)) //nolint
+		w.WriteString(",flags=")        //nolint
+		writeOFlags(w, int(args[1].(float64)))
+		w.WriteString(",perm=")                                        //nolint
+		w.WriteString(fs.FileMode(uint32(args[2].(float64))).String()) //nolint
+		w.WriteByte(')')                                               //nolint
+		return
+	}
+
+	argNames := custom.FsNameSection[m].ParamNames
+
+	w.WriteString("fs.") //nolint
+	w.WriteString(m)     //nolint
+	w.WriteByte('(')     //nolint
+	writeVals(w, args, argNames)
+	w.WriteByte(')') //nolint
+}
+
+func syscallValueCallParamSampler(ctx context.Context, mod api.Module, params []uint64) bool {
+	vRef, _, _ := syscallValueCallParams(ctx, mod, params)
+
+	// TODO: add more than just filesystem
+	return vRef == goos.RefJsfs
+}
+
+func syscallValueCallParams(ctx context.Context, mod api.Module, params []uint64) (goos.Ref, string, []interface{}) {
+	mem := mod.Memory()
+	funcName := custom.NameSyscallValueCall
+	stack := goos.NewStack(funcName, mem, uint32(params[0]))
+	vRef := stack.ParamRef(0)               //nolint
+	m := stack.ParamString(mem, 1 /*, 2 */) //nolint
+	args := stack.ParamVals(ctx, mem, 3 /*, 4 */, gojs.LoadValue)
+	return vRef, m, args
+}
+
+func syscallValueCallResultLogger(ctx context.Context, mod api.Module, w logging.Writer, params, results []uint64) {
+	mem := mod.Memory()
+	funcName := custom.NameSyscallValueCall
+	stack := goos.NewStack(funcName, mem, goarch.GetSP(mod))
+	vRef := stack.ParamRef(0)               //nolint
+	m := stack.ParamString(mem, 1 /*, 2 */) //nolint
+
+	// TODO: add more than just filesystem
+	if vRef != goos.RefJsfs {
+		return
+	}
+
+	args := gojs.GetLastEventArgs(ctx)
+	argNames := custom.FsNameSection[m].ResultNames
+
+	w.WriteByte('(') //nolint
+	writeVals(w, args, argNames)
+	w.WriteByte(')') //nolint
+}
+
+func writeVals(w logging.Writer, vals []interface{}, names []string) {
+	valLen := len(vals)
+	if valLen > 0 {
+		w.WriteString(names[0]) //nolint
+		w.WriteByte('=')        //nolint
+		// TODO: learn the types of the vals.
+		w.WriteString(fmt.Sprintf("%v", vals[0])) //nolint
+		for i := 1; i < valLen; i++ {
+			switch names[i] {
+			case custom.NameCallback:
+				return // last val
+			case "buf": // always equal size with byteCount
+				continue
+			}
+
+			w.WriteByte(',')                          //nolint
+			w.WriteString(names[i])                   //nolint
+			w.WriteByte('=')                          //nolint
+			w.WriteString(fmt.Sprintf("%v", vals[i])) //nolint
+		}
+	}
+}
+
+func writeOFlags(w logging.Writer, f int) {
+	// Iterate a subflagset in order to avoid OS differences, notably for windows
+	first := true
+	for i, sf := range oFlags {
+		if f&sf != 0 {
+			if !first {
+				w.WriteByte('|') //nolint
+			} else {
+				first = false
+			}
+			w.WriteString(oflagToString[i]) //nolint
+		}
+	}
+}
+
+var oFlags = [...]int{
+	os.O_RDONLY,
+	os.O_WRONLY,
+	os.O_RDWR,
+	os.O_APPEND,
+	os.O_CREATE,
+	os.O_EXCL,
+	os.O_SYNC,
+	os.O_TRUNC,
+}
+
+var oflagToString = [...]string{
+	"RDONLY",
+	"WRONLY",
+	"RDWR",
+	"APPEND",
+	"CREATE",
+	"EXCL",
+	"SYNC",
+	"TRUNC",
+}
+
+func writeI32(w logging.Writer, v uint32) {
+	w.WriteString(strconv.FormatInt(int64(int32(v)), 10)) //nolint
+}
diff --git a/internal/gojs/state.go b/internal/gojs/state.go
index 6661b886..175c14f6 100644
--- a/internal/gojs/state.go
+++ b/internal/gojs/state.go
@@ -25,6 +25,16 @@ func getState(ctx context.Context) *state {
 	return ctx.Value(stateKey{}).(*state)
 }
 
+// GetLastEventArgs implements goos.GetLastEventArgs
+func GetLastEventArgs(ctx context.Context) []interface{} {
+	if ls := ctx.Value(stateKey{}).(*state)._lastEvent; ls != nil {
+		if args := ls.args; args != nil {
+			return args.slice
+		}
+	}
+	return nil
+}
+
 type event struct {
 	// id is the funcWrapper.id
 	id     uint32
@@ -51,11 +61,11 @@ var (
 	NaN       = math.NaN()
 )
 
-// loadValue reads up to 8 bytes at the memory offset `addr` to return the
+// LoadValue reads up to 8 bytes at the memory offset `addr` to return the
 // value written by storeValue.
 //
 // See https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L122-L133
-func loadValue(ctx context.Context, ref goos.Ref) interface{} { // nolint
+func LoadValue(ctx context.Context, ref goos.Ref) interface{} { //nolint
 	switch ref {
 	case 0:
 		return undefined
@@ -73,25 +83,25 @@ func loadValue(ctx context.Context, ref goos.Ref) interface{} { // nolint
 		return getState(ctx).valueGlobal
 	case goos.RefJsGo:
 		return getState(ctx)
-	case refObjectConstructor:
+	case goos.RefObjectConstructor:
 		return objectConstructor
-	case refArrayConstructor:
+	case goos.RefArrayConstructor:
 		return arrayConstructor
-	case refJsProcess:
+	case goos.RefJsProcess:
 		return jsProcess
-	case refJsfs:
+	case goos.RefJsfs:
 		return jsfs
-	case refJsfsConstants:
+	case goos.RefJsfsConstants:
 		return jsfsConstants
-	case refUint8ArrayConstructor:
+	case goos.RefUint8ArrayConstructor:
 		return uint8ArrayConstructor
-	case refJsCrypto:
+	case goos.RefJsCrypto:
 		return jsCrypto
-	case refJsDateConstructor:
+	case goos.RefJsDateConstructor:
 		return jsDateConstructor
-	case refJsDate:
+	case goos.RefJsDate:
 		return jsDate
-	case refHttpHeadersConstructor:
+	case goos.RefHttpHeadersConstructor:
 		return headersConstructor
 	default:
 		if f, ok := ref.ParseFloat(); ok { // numbers are passed through as a Ref
@@ -106,7 +116,7 @@ func loadValue(ctx context.Context, ref goos.Ref) interface{} { // nolint
 // Any side effects besides memory must be cleaned up on wasmExit.
 //
 // See https://github.com/golang/go/blob/go1.19/misc/wasm/wasm_exec.js#L135-L183
-func storeRef(ctx context.Context, v interface{}) goos.Ref { // nolint
+func storeRef(ctx context.Context, v interface{}) goos.Ref { //nolint
 	// allow-list because we control all implementations
 	if v == undefined {
 		return goos.RefValueUndefined
@@ -134,25 +144,25 @@ func storeRef(ctx context.Context, v interface{}) goos.Ref { // nolint
 	} else if _, ok := v.(string); ok {
 		id := getState(ctx).values.increment(v)
 		return goos.ValueRef(id, goos.TypeFlagString)
-	} else if ui, ok := v.(uint32); ok {
-		if ui == 0 {
-			return goos.RefValueZero
-		}
-		// numbers are encoded as float and passed through as a Ref
-		return goos.Ref(api.EncodeF64(float64(ui)))
-	} else if u, ok := v.(uint64); ok {
-		// float is already encoded as a uint64, doesn't need to be stored.
-		return goos.Ref(u)
+	} else if u32, ok := v.(uint32); ok {
+		return toFloatRef(float64(u32))
+	} else if u64, ok := v.(uint64); ok {
+		return toFloatRef(float64(u64))
 	} else if f64, ok := v.(float64); ok {
-		if f64 == 0 {
-			return goos.RefValueZero
-		}
-		return goos.Ref(api.EncodeF64(f64))
+		return toFloatRef(f64)
 	}
 	id := getState(ctx).values.increment(v)
 	return goos.ValueRef(id, goos.TypeFlagObject)
 }
 
+func toFloatRef(f float64) goos.Ref {
+	if f == 0 {
+		return goos.RefValueZero
+	}
+	// numbers are encoded as float and passed through as a Ref
+	return goos.Ref(api.EncodeF64(f))
+}
+
 type values struct {
 	// Below is needed to avoid exhausting the ID namespace finalizeRef reclaims
 	// See https://go-review.googlesource.com/c/go/+/203600
@@ -164,7 +174,7 @@ type values struct {
 }
 
 func (j *values) get(id uint32) interface{} {
-	index := id - nextID
+	index := id - goos.NextID
 	if index >= uint32(len(j.values)) {
 		panic(fmt.Errorf("id %d is out of range %d", id, len(j.values)))
 	}
@@ -183,15 +193,15 @@ func (j *values) increment(v interface{}) uint32 {
 		j.ids[v] = id
 	}
 	j.goRefCounts[id]++
-	return id + nextID
+	return id + goos.NextID
 }
 
 func (j *values) decrement(id uint32) {
-	// Special IDs are not refcounted.
-	if id < nextID {
+	// Special IDs are not goos.Refcounted.
+	if id < goos.NextID {
 		return
 	}
-	id -= nextID
+	id -= goos.NextID
 	j.goRefCounts[id]--
 	if j.goRefCounts[id] == 0 {
 		j.values[id] = nil
@@ -204,6 +214,8 @@ func (j *values) decrement(id uint32) {
 type state struct {
 	values        *values
 	_pendingEvent *event
+	// _lastEvent was the last _pendingEvent value
+	_lastEvent *event
 
 	valueGlobal *jsVal
 
@@ -237,6 +249,7 @@ func (s *state) clear() {
 	}
 	s.values.idPool = s.values.idPool[:0]
 	s._pendingEvent = nil
+	s._lastEvent = nil
 }
 
 func toInt64(arg interface{}) int64 {
@@ -258,7 +271,7 @@ func toUint32(arg interface{}) uint32 {
 }
 
 // valueString returns the string form of JavaScript string, boolean and number types.
-func valueString(v interface{}) string { // nolint
+func valueString(v interface{}) string { //nolint
 	if s, ok := v.(string); ok {
 		return s
 	} else {
diff --git a/internal/gojs/syscall.go b/internal/gojs/syscall.go
index 4b590183..12384df0 100644
--- a/internal/gojs/syscall.go
+++ b/internal/gojs/syscall.go
@@ -54,7 +54,7 @@ func stringVal(ctx context.Context, mod api.Module, stack goos.Stack) {
 var ValueGet = goos.NewFunc(custom.NameSyscallValueGet, valueGet)
 
 func valueGet(ctx context.Context, mod api.Module, stack goos.Stack) {
-	v := stack.ParamVal(ctx, 0, loadValue)
+	v := stack.ParamVal(ctx, 0, LoadValue)
 	p := stack.ParamString(mod.Memory(), 1 /*, 2 */)
 
 	var result interface{}
@@ -86,15 +86,17 @@ func valueGet(ctx context.Context, mod api.Module, stack goos.Stack) {
 var ValueSet = goos.NewFunc(custom.NameSyscallValueSet, valueSet)
 
 func valueSet(ctx context.Context, mod api.Module, stack goos.Stack) {
-	v := stack.ParamVal(ctx, 0, loadValue)
+	v := stack.ParamVal(ctx, 0, LoadValue)
 	p := stack.ParamString(mod.Memory(), 1 /*, 2 */)
-	x := stack.ParamVal(ctx, 3, loadValue)
+	x := stack.ParamVal(ctx, 3, LoadValue)
 
 	if p := p; v == getState(ctx) {
 		switch p {
 		case "_pendingEvent":
 			if x == nil { // syscall_js.handleEvent
-				v.(*state)._pendingEvent = nil
+				s := v.(*state)
+				s._lastEvent = s._pendingEvent
+				s._pendingEvent = nil
 				return
 			}
 		}
@@ -125,7 +127,7 @@ var ValueDelete = goarch.StubFunction(custom.NameSyscallValueDelete)
 var ValueIndex = goos.NewFunc(custom.NameSyscallValueIndex, valueIndex)
 
 func valueIndex(ctx context.Context, _ api.Module, stack goos.Stack) {
-	v := stack.ParamVal(ctx, 0, loadValue)
+	v := stack.ParamVal(ctx, 0, LoadValue)
 	i := stack.ParamUint32(1)
 
 	result := v.(*objectArray).slice[i]
@@ -151,10 +153,10 @@ func valueCall(ctx context.Context, mod api.Module, stack goos.Stack) {
 	mem := mod.Memory()
 	vRef := stack.ParamRef(0)
 	m := stack.ParamString(mem, 1 /*, 2 */)
-	args := stack.ParamVals(ctx, mem, 3 /*, 4 */, loadValue)
+	args := stack.ParamVals(ctx, mem, 3 /*, 4 */, LoadValue)
 	// 5 = padding
 
-	v := loadValue(ctx, vRef)
+	v := LoadValue(ctx, vRef)
 	c, isCall := v.(jsCall)
 	if !isCall {
 		panic(fmt.Errorf("TODO: valueCall(v=%v, m=%s, args=%v)", v, m, args))
@@ -189,17 +191,17 @@ var ValueNew = goos.NewFunc(custom.NameSyscallValueNew, valueNew)
 func valueNew(ctx context.Context, mod api.Module, stack goos.Stack) {
 	mem := mod.Memory()
 	vRef := stack.ParamRef(0)
-	args := stack.ParamVals(ctx, mem, 1 /*, 2 */, loadValue)
+	args := stack.ParamVals(ctx, mem, 1 /*, 2 */, LoadValue)
 	// 3 = padding
 
 	var res goos.Ref
 	var ok bool
 	switch vRef {
-	case refArrayConstructor:
+	case goos.RefArrayConstructor:
 		result := &objectArray{}
 		res = storeRef(ctx, result)
 		ok = true
-	case refUint8ArrayConstructor:
+	case goos.RefUint8ArrayConstructor:
 		var result interface{}
 		a := args[0]
 		if n, ok := a.(float64); ok {
@@ -213,16 +215,16 @@ func valueNew(ctx context.Context, mod api.Module, stack goos.Stack) {
 		}
 		res = storeRef(ctx, result)
 		ok = true
-	case refObjectConstructor:
+	case goos.RefObjectConstructor:
 		result := &object{properties: map[string]interface{}{}}
 		res = storeRef(ctx, result)
 		ok = true
-	case refHttpHeadersConstructor:
+	case goos.RefHttpHeadersConstructor:
 		result := &headers{headers: http.Header{}}
 		res = storeRef(ctx, result)
 		ok = true
-	case refJsDateConstructor:
-		res = refJsDate
+	case goos.RefJsDateConstructor:
+		res = goos.RefJsDate
 		ok = true
 	default:
 		panic(fmt.Errorf("TODO: valueNew(v=%v, args=%v)", vRef, args))
@@ -241,7 +243,7 @@ func valueNew(ctx context.Context, mod api.Module, stack goos.Stack) {
 var ValueLength = goos.NewFunc(custom.NameSyscallValueLength, valueLength)
 
 func valueLength(ctx context.Context, _ api.Module, stack goos.Stack) {
-	v := stack.ParamVal(ctx, 0, loadValue)
+	v := stack.ParamVal(ctx, 0, LoadValue)
 
 	len := len(v.(*objectArray).slice)
 
@@ -258,7 +260,7 @@ func valueLength(ctx context.Context, _ api.Module, stack goos.Stack) {
 var ValuePrepareString = goos.NewFunc(custom.NameSyscallValuePrepareString, valuePrepareString)
 
 func valuePrepareString(ctx context.Context, _ api.Module, stack goos.Stack) {
-	v := stack.ParamVal(ctx, 0, loadValue)
+	v := stack.ParamVal(ctx, 0, LoadValue)
 
 	s := valueString(v)
 
@@ -278,7 +280,7 @@ func valuePrepareString(ctx context.Context, _ api.Module, stack goos.Stack) {
 var ValueLoadString = goos.NewFunc(custom.NameSyscallValueLoadString, valueLoadString)
 
 func valueLoadString(ctx context.Context, mod api.Module, stack goos.Stack) {
-	v := stack.ParamVal(ctx, 0, loadValue)
+	v := stack.ParamVal(ctx, 0, LoadValue)
 	b := stack.ParamBytes(mod.Memory(), 1 /*, 2 */)
 
 	s := valueString(v)
@@ -305,7 +307,7 @@ var CopyBytesToGo = goos.NewFunc(custom.NameSyscallCopyBytesToGo, copyBytesToGo)
 func copyBytesToGo(ctx context.Context, mod api.Module, stack goos.Stack) {
 	dst := stack.ParamBytes(mod.Memory(), 0 /*, 1 */)
 	// padding = 2
-	src := stack.ParamVal(ctx, 3, loadValue)
+	src := stack.ParamVal(ctx, 3, LoadValue)
 
 	var n uint32
 	var ok bool
@@ -331,7 +333,7 @@ func copyBytesToGo(ctx context.Context, mod api.Module, stack goos.Stack) {
 var CopyBytesToJS = goos.NewFunc(custom.NameSyscallCopyBytesToJS, copyBytesToJS)
 
 func copyBytesToJS(ctx context.Context, mod api.Module, stack goos.Stack) {
-	dst := stack.ParamVal(ctx, 0, loadValue)
+	dst := stack.ParamVal(ctx, 0, LoadValue)
 	src := stack.ParamBytes(mod.Memory(), 1 /*, 2 */)
 	// padding = 3
 
diff --git a/internal/gojs/time.go b/internal/gojs/time.go
index 551c3ea7..a64e166d 100644
--- a/internal/gojs/time.go
+++ b/internal/gojs/time.go
@@ -4,17 +4,18 @@ import (
 	"context"
 
 	"github.com/tetratelabs/wazero/api"
+	"github.com/tetratelabs/wazero/internal/gojs/goos"
 )
 
 var (
 	// jsDateConstructor returns jsDate.
 	//
 	// This is defined as `Get("Date")` in zoneinfo_js.go time.initLocal
-	jsDateConstructor = newJsVal(refJsDateConstructor, "Date")
+	jsDateConstructor = newJsVal(goos.RefJsDateConstructor, "Date")
 
 	// jsDate is used inline in zoneinfo_js.go for time.initLocal.
 	// `.Call("getTimezoneOffset").Int()` returns a timezone offset.
-	jsDate = newJsVal(refJsDate, "jsDate").
+	jsDate = newJsVal(goos.RefJsDate, "jsDate").
 		addFunction("getTimezoneOffset", &getTimezoneOffset{})
 )
 
diff --git a/internal/logging/logging.go b/internal/logging/logging.go
index b717daaf..20341055 100644
--- a/internal/logging/logging.go
+++ b/internal/logging/logging.go
@@ -38,6 +38,8 @@ type LoggerKey struct{}
 
 type ParamLogger func(ctx context.Context, mod api.Module, w Writer, params []uint64)
 
+type ParamSampler func(ctx context.Context, mod api.Module, params []uint64) bool
+
 type ResultLogger func(ctx context.Context, mod api.Module, w Writer, params, results []uint64)
 
 type Writer interface {
@@ -97,8 +99,8 @@ type namedParamLogger struct {
 }
 
 func (n namedParamLogger) Log(ctx context.Context, mod api.Module, w Writer, params []uint64) {
-	w.WriteString(n.name) // nolint
-	w.WriteByte('=')      // nolint
+	w.WriteString(n.name) //nolint
+	w.WriteByte('=')      //nolint
 	n.valWriter(ctx, mod, w, n.idx, params)
 }
 
@@ -122,8 +124,8 @@ type namedResultLogger struct {
 }
 
 func (n namedResultLogger) Log(ctx context.Context, mod api.Module, w Writer, _, results []uint64) {
-	w.WriteString(n.name) // nolint
-	w.WriteByte('=')      // nolint
+	w.WriteString(n.name) //nolint
+	w.WriteByte('=')      //nolint
 	n.valWriter(ctx, mod, w, n.idx, results)
 }
 
diff --git a/internal/wasi_snapshot_preview1/wasi.go b/internal/wasi_snapshot_preview1/wasi.go
index 6b71280c..ee51c4da 100644
--- a/internal/wasi_snapshot_preview1/wasi.go
+++ b/internal/wasi_snapshot_preview1/wasi.go
@@ -3,12 +3,7 @@
 package wasi_snapshot_preview1
 
 import (
-	"context"
 	"fmt"
-	"strings"
-
-	"github.com/tetratelabs/wazero/api"
-	"github.com/tetratelabs/wazero/internal/logging"
 )
 
 const ModuleName = "wasi_snapshot_preview1"
@@ -101,68 +96,173 @@ var errnoToString = [...]string{
 	"ENOTCAPABLE",
 }
 
-func ValueLoggers(fnd api.FunctionDefinition) (pLoggers []logging.ParamLogger, rLoggers []logging.ResultLogger) {
-	switch fnd.Name() {
-	case "fd_prestat_get":
-		pLoggers = []logging.ParamLogger{
-			logging.NewParamLogger(0, "fd", logging.ValueTypeI32),
-		}
-		name := "prestat"
-		rLoggers = []logging.ResultLogger{
-			resultParamLogger(name, logging.NewParamLogger(1, name, logging.ValueTypeMemH64)),
-			wasiErrno,
-		}
-		return
-	case "proc_exit":
-		return logging.ValueLoggers(fnd)
-	}
+// oflags are open flags used by path_open
+// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-oflags-flagsu16
+const (
+	// O_CREAT creates a file if it does not exist.
+	O_CREAT uint16 = 1 << iota //nolint
+	// O_DIRECTORY fails if not a directory.
+	O_DIRECTORY
+	// O_EXCL fails if file already exists.
+	O_EXCL //nolint
+	// O_TRUNC truncates the file to size 0.
+	O_TRUNC //nolint
+)
 
-	for idx := uint32(0); idx < uint32(len(fnd.ParamTypes())); idx++ {
-		name := fnd.ParamNames()[idx]
-		isResult := strings.HasPrefix(name, "result.")
-
-		var logger logging.ParamLogger
-		if strings.Contains(name, "path") {
-			if isResult {
-				name = name[7:]
-			}
-			logger = logging.NewParamLogger(idx, name, logging.ValueTypeString)
-			idx++
-			if isResult {
-				rLoggers = append(rLoggers, resultParamLogger(name, logger))
-				continue
-			}
-		} else if name == "result.nread" {
-			name = name[7:]
-			logger = logging.NewParamLogger(idx, name, logging.ValueTypeMemI32)
-			rLoggers = append(rLoggers, resultParamLogger(name, logger))
-			continue
-		} else {
-			logger = logging.NewParamLogger(idx, name, fnd.ParamTypes()[idx])
-		}
-		pLoggers = append(pLoggers, logger)
-	}
-	// All WASI functions except proc_after return only an errno result.
-	rLoggers = append(rLoggers, wasiErrno)
-	return
-}
+// file descriptor flags
+// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#fdflags
+const (
+	FD_APPEND uint16 = 1 << iota //nolint
+	FD_DSYNC
+	FD_NONBLOCK
+	FD_RSYNC
+	FD_SYNC
+)
 
-func wasiErrno(_ context.Context, _ api.Module, w logging.Writer, _, results []uint64) {
-	errno := ErrnoName(uint32(results[0]))
-	w.WriteString("errno=") // nolint
-	w.WriteString(errno)    // nolint
-}
+// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#lookupflags
+const (
+	// LOOKUP_SYMLINK_FOLLOW expands a path if it resolves into a symbolic
+	// link.
+	LOOKUP_SYMLINK_FOLLOW uint16 = 1 << iota //nolint
+)
+
+// See https://github.com/WebAssembly/WASI/blob/snapshot-01/phases/snapshot/docs.md#-rights-flagsu64
+const (
+	// RIGHT_FD_DATASYNC is the right to invoke fd_datasync. If RIGHT_PATH_OPEN
+	// is set, includes the right to invoke path_open with FD_DSYNC.
+	RIGHT_FD_DATASYNC uint32 = 1 << iota //nolint
+
+	// RIGHT_FD_READ is he right to invoke fd_read and sock_recv. If
+	// RIGHT_FD_SYNC is set, includes the right to invoke fd_pread.
+	RIGHT_FD_READ
+
+	// RIGHT_FD_SEEK is the right to invoke fd_seek. This flag implies
+	// RIGHT_FD_TELL.
+	RIGHT_FD_SEEK
+
+	// RIGHT_FDSTAT_SET_FLAGS is the right to invoke fd_fdstat_set_flags.
+	RIGHT_FDSTAT_SET_FLAGS
+
+	// RIGHT_FD_SYNC The right to invoke fd_sync. If path_open is set, includes
+	// the right to invoke path_open with FD_RSYNC and FD_DSYNC.
+	RIGHT_FD_SYNC
+
+	// RIGHT_FD_TELL is the right to invoke fd_seek in such a way that the file
+	// offset remains unaltered (i.e., whence::cur with offset zero), or to
+	// invoke fd_tell.
+	RIGHT_FD_TELL
+
+	// RIGHT_FD_WRITE is the right to invoke fd_write and sock_send. If
+	// RIGHT_FD_SEEK is set, includes the right to invoke fd_pwrite.
+	RIGHT_FD_WRITE
+
+	// RIGHT_FD_ADVISE is the right to invoke fd_advise.
+	RIGHT_FD_ADVISE
+
+	// RIGHT_FD_ALLOCATE is the right to invoke fd_allocate.
+	RIGHT_FD_ALLOCATE
+
+	// RIGHT_PATH_CREATE_DIRECTORY is the right to invoke
+	// path_create_directory.
+	RIGHT_PATH_CREATE_DIRECTORY
+
+	// RIGHT_PATH_CREATE_FILE when RIGHT_PATH_OPEN is set, the right to invoke
+	// path_open with O_CREATE.
+	RIGHT_PATH_CREATE_FILE
+
+	// RIGHT_PATH_LINK_SOURCE is the right to invoke path_link with the file
+	// descriptor as the source directory.
+	RIGHT_PATH_LINK_SOURCE
+
+	// RIGHT_PATH_LINK_TARGET is the right to invoke path_link with the file
+	// descriptor as the target directory.
+	RIGHT_PATH_LINK_TARGET
 
-// resultParamLogger logs the value of the parameter when the operation is
-// successful or faults due to out of memory.
-func resultParamLogger(name string, pLogger logging.ParamLogger) logging.ResultLogger {
-	empty := name + "="
-	return func(ctx context.Context, mod api.Module, w logging.Writer, params, results []uint64) {
-		switch results[0] {
-		case 0, 21: // ESUCCESS, EFAULT
-			pLogger(ctx, mod, w, params)
-		default:
-			w.WriteString(empty) // nolint
-		}
+	// RIGHT_PATH_OPEN is the right to invoke path_open.
+	RIGHT_PATH_OPEN
+
+	// RIGHT_FD_READDIR is the right to invoke fd_readdir.
+	RIGHT_FD_READDIR
+
+	// RIGHT_PATH_READLINK is the right to invoke path_readlink.
+	RIGHT_PATH_READLINK
+
+	// RIGHT_PATH_RENAME_SOURCE is the right to invoke path_rename with the
+	// file descriptor as the source directory.
+	RIGHT_PATH_RENAME_SOURCE
+
+	// RIGHT_PATH_RENAME_TARGET is the right to invoke path_rename with the
+	// file descriptor as the target directory.
+	RIGHT_PATH_RENAME_TARGET
+
+	// RIGHT_PATH_FILESTAT_GET is the right to invoke path_filestat_get.
+	RIGHT_PATH_FILESTAT_GET
+
+	// RIGHT_PATH_FILESTAT_SET_SIZE is the right to change a file's size (there
+	// is no path_filestat_set_size). If RIGHT_PATH_OPEN is set, includes the
+	// right to invoke path_open with O_TRUNC.
+	RIGHT_PATH_FILESTAT_SET_SIZE
+
+	// RIGHT_PATH_FILESTAT_SET_TIMES is the right to invoke
+	// path_filestat_set_times.
+	RIGHT_PATH_FILESTAT_SET_TIMES
+
+	// RIGHT_FD_FILESTAT_GET is the right to invoke fd_filestat_get.
+	RIGHT_FD_FILESTAT_GET
+
+	// RIGHT_FD_FILESTAT_SET_SIZE is the right to invoke fd_filestat_set_size.
+	RIGHT_FD_FILESTAT_SET_SIZE
+
+	// RIGHT_FD_FILESTAT_SET_TIMES is the right to invoke
+	// fd_filestat_set_times.
+	RIGHT_FD_FILESTAT_SET_TIMES
+
+	// RIGHT_PATH_SYMLINK is the right to invoke path_symlink.
+	RIGHT_PATH_SYMLINK
+
+	// RIGHT_PATH_REMOVE_DIRECTORY is the right to invoke
+	// path_remove_directory.
+	RIGHT_PATH_REMOVE_DIRECTORY
+
+	// RIGHT_PATH_UNLINK_FILE is the right to invoke path_unlink_file.
+	RIGHT_PATH_UNLINK_FILE
+
+	// RIGHT_POLL_FD_READWRITE when RIGHT_FD_READ is set, includes the right to
+	// invoke poll_oneoff to subscribe to eventtype::fd_read. If RIGHT_FD_WRITE
+	// is set, includes the right to invoke poll_oneoff to subscribe to
+	// eventtype::fd_write.
+	RIGHT_POLL_FD_READWRITE
+
+	// RIGHT_SOCK_SHUTDOWN is the right to invoke sock_shutdown.
+	RIGHT_SOCK_SHUTDOWN
+)
+
+const (
+	FILETYPE_UNKNOWN uint8 = iota
+	FILETYPE_BLOCK_DEVICE
+	FILETYPE_CHARACTER_DEVICE
+	FILETYPE_DIRECTORY
+	FILETYPE_REGULAR_FILE
+	FILETYPE_SOCKET_DGRAM
+	FILETYPE_SOCKET_STREAM
+	FILETYPE_SYMBOLIC_LINK
+)
+
+// FiletypeName returns string name of the file type.
+func FiletypeName(filetype uint8) string {
+	if int(filetype) < len(filetypeToString) {
+		return filetypeToString[filetype]
 	}
+	return fmt.Sprintf("filetype(%d)", filetype)
+}
+
+var filetypeToString = [...]string{
+	"UNKNOWN",
+	"BLOCK_DEVICE",
+	"CHARACTER_DEVICE",
+	"DIRECTORY",
+	"REGULAR_FILE",
+	"SOCKET_DGRAM",
+	"SOCKET_STREAM",
+	"SYMBOLIC_LINK",
 }


Test patch:
diff --git a/cmd/wazero/wazero_test.go b/cmd/wazero/wazero_test.go
index 18f067a0..51264f9f 100644
--- a/cmd/wazero/wazero_test.go
+++ b/cmd/wazero/wazero_test.go
@@ -29,9 +29,6 @@ var wasmWasiEnv []byte
 //go:embed testdata/wasi_fd.wasm
 var wasmWasiFd []byte
 
-//go:embed testdata/fs/bear.txt
-var bearTxt []byte
-
 // wasmCat is compiled on demand with `GOARCH=wasm GOOS=js`
 var wasmCat []byte
 
@@ -178,8 +175,16 @@ func TestRun(t *testing.T) {
 	tmpDir, oldwd := requireChdirToTemp(t)
 	defer os.Chdir(oldwd) //nolint
 
-	bearPath := filepath.Join(tmpDir, "bear.txt")
-	require.NoError(t, os.WriteFile(bearPath, bearTxt, 0o600))
+	// We can't rely on the mtime from git because in CI, only the last commit
+	// is checked out. Instead, grab the effective mtime.
+	bearPath := filepath.Join(oldwd, "testdata", "fs", "bear.txt")
+	bearStat, err := os.Stat(bearPath)
+	require.NoError(t, err)
+	bearMtime := bearStat.ModTime().UnixMilli()
+	// The file is world read, but windows cannot see that and reports world
+	// write. Hence, we save off the current interpretation of mode for
+	// comparison.
+	bearMode := bearStat.Mode()
 
 	existingDir1 := filepath.Join(tmpDir, "existing1")
 	require.NoError(t, os.Mkdir(existingDir1, 0o700))
@@ -221,6 +226,19 @@ func TestRun(t *testing.T) {
 			wazeroOpts: []string{fmt.Sprintf("--mount=%s:/", filepath.Dir(bearPath))},
 			stdOut:     "pooh\n",
 		},
+		{
+			name:       "wasi filesystem",
+			wasm:       wasmWasiFd,
+			wazeroOpts: []string{"--hostlogging=filesystem", fmt.Sprintf("--mount=%s:/", filepath.Dir(bearPath))},
+			stdOut:     "pooh\n",
+			stdErr: `==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=bear.txt,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+<== (opened_fd=4,errno=ESUCCESS)
+==> wasi_snapshot_preview1.fd_read(fd=4,iovs=1024,iovs_len=1)
+<== (nread=5,errno=ESUCCESS)
+==> wasi_snapshot_preview1.fd_write(fd=1,iovs=1024,iovs_len=1)
+<== (nwritten=5,errno=ESUCCESS)
+`,
+		},
 		{
 			name:       "GOARCH=wasm GOOS=js",
 			wasm:       wasmCat,
@@ -228,6 +246,28 @@ func TestRun(t *testing.T) {
 			wasmArgs:   []string{"/bear.txt"},
 			stdOut:     "pooh\n",
 		},
+		{
+			name:       "GOARCH=wasm GOOS=js filesystem",
+			wasm:       wasmCat,
+			wazeroOpts: []string{"--hostlogging=filesystem", fmt.Sprintf("--mount=%s:/", filepath.Dir(bearPath))},
+			wasmArgs:   []string{"/bear.txt"},
+			stdOut:     "pooh\n",
+			stdErr: fmt.Sprintf(`==> go.syscall/js.valueCall(fs.open(name=/bear.txt,flags=,perm=----------))
+<== (err=<nil>,fd=4)
+==> go.syscall/js.valueCall(fs.fstat(fd=4))
+<== (err=<nil>,stat={isDir=false,mode=%[1]s,size=5,mtimeMs=%[2]d})
+==> go.syscall/js.valueCall(fs.fstat(fd=4))
+<== (err=<nil>,stat={isDir=false,mode=%[1]s,size=5,mtimeMs=%[2]d})
+==> go.syscall/js.valueCall(fs.read(fd=4,offset=0,byteCount=512,fOffset=<nil>))
+<== (err=<nil>,n=5)
+==> go.syscall/js.valueCall(fs.read(fd=4,offset=0,byteCount=507,fOffset=<nil>))
+<== (err=<nil>,n=0)
+==> go.syscall/js.valueCall(fs.close(fd=4))
+<== (err=<nil>,ok=true)
+==> go.syscall/js.valueCall(fs.write(fd=1,offset=0,byteCount=5,fOffset=<nil>))
+<== (err=<nil>,n=5)
+`, bearMode, bearMtime),
+		},
 		{
 			name:       "cachedir existing absolute",
 			wazeroOpts: []string{"--cachedir=" + existingDir1},
diff --git a/imports/wasi_snapshot_preview1/clock_test.go b/imports/wasi_snapshot_preview1/clock_test.go
index 49c75ee4..10ffd24f 100644
--- a/imports/wasi_snapshot_preview1/clock_test.go
+++ b/imports/wasi_snapshot_preview1/clock_test.go
@@ -241,7 +241,7 @@ func Test_clockTimeGet_Errors(t *testing.T) {
 		expectedLog              string
 	}{
 		{
-			name:            "resultTimestamp out-of-memory",
+			name:            "resultTimestamp OOM",
 			resultTimestamp: memorySize,
 			expectedLog: `
 ==> wasi_snapshot_preview1.clock_time_get(id=0,precision=0,result.timestamp=65536)
diff --git a/imports/wasi_snapshot_preview1/fs_test.go b/imports/wasi_snapshot_preview1/fs_test.go
index 4344e6e0..053338c0 100644
--- a/imports/wasi_snapshot_preview1/fs_test.go
+++ b/imports/wasi_snapshot_preview1/fs_test.go
@@ -17,6 +17,7 @@ import (
 	"github.com/tetratelabs/wazero/internal/leb128"
 	internalsys "github.com/tetratelabs/wazero/internal/sys"
 	"github.com/tetratelabs/wazero/internal/testing/require"
+	"github.com/tetratelabs/wazero/internal/wasi_snapshot_preview1"
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
@@ -123,8 +124,8 @@ func Test_fdFdstatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_fdstat_get(fd=0,result.stat=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=0)
+<== (stat={filetype=BLOCK_DEVICE,fdflags=,fs_rights_base=,fs_rights_inheriting=},errno=ESUCCESS)
 `,
 		},
 		{
@@ -137,8 +138,8 @@ func Test_fdFdstatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_fdstat_get(fd=1,result.stat=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=1)
+<== (stat={filetype=BLOCK_DEVICE,fdflags=APPEND,fs_rights_base=,fs_rights_inheriting=},errno=ESUCCESS)
 `,
 		},
 		{
@@ -151,8 +152,8 @@ func Test_fdFdstatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_fdstat_get(fd=2,result.stat=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=2)
+<== (stat={filetype=BLOCK_DEVICE,fdflags=APPEND,fs_rights_base=,fs_rights_inheriting=},errno=ESUCCESS)
 `,
 		},
 		{
@@ -165,8 +166,8 @@ func Test_fdFdstatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_fdstat_get(fd=3,result.stat=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=3)
+<== (stat={filetype=DIRECTORY,fdflags=,fs_rights_base=,fs_rights_inheriting=},errno=ESUCCESS)
 `,
 		},
 		{
@@ -179,8 +180,8 @@ func Test_fdFdstatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_fdstat_get(fd=4,result.stat=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=4)
+<== (stat={filetype=REGULAR_FILE,fdflags=,fs_rights_base=,fs_rights_inheriting=},errno=ESUCCESS)
 `,
 		},
 		{
@@ -193,8 +194,8 @@ func Test_fdFdstatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // fs_rights_inheriting
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_fdstat_get(fd=5,result.stat=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=5)
+<== (stat={filetype=DIRECTORY,fdflags=,fs_rights_base=,fs_rights_inheriting=},errno=ESUCCESS)
 `,
 		},
 		{
@@ -202,8 +203,8 @@ func Test_fdFdstatGet(t *testing.T) {
 			fd:            math.MaxUint32,
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_fdstat_get(fd=-1,result.stat=0)
-<== errno=EBADF
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=-1)
+<== (stat=,errno=EBADF)
 `,
 		},
 		{
@@ -212,8 +213,8 @@ func Test_fdFdstatGet(t *testing.T) {
 			resultFdstat:  memorySize - 24 + 1,
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_fdstat_get(fd=5,result.stat=65513)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.fd_fdstat_get(fd=5)
+<== (stat=,errno=EFAULT)
 `,
 		},
 	}
@@ -249,7 +250,7 @@ func Test_fdFdstatSetFlags(t *testing.T) {
 func Test_fdFdstatSetRights(t *testing.T) {
 	log := requireErrnoNosys(t, fdFdstatSetRightsName, 0, 0, 0)
 	require.Equal(t, `
---> wasi_snapshot_preview1.fd_fdstat_set_rights(fd=0,fs_rights_base=0,fs_rights_inheriting=0)
+--> wasi_snapshot_preview1.fd_fdstat_set_rights(fd=0,fs_rights_base=,fs_rights_inheriting=)
 <-- errno=ENOSYS
 `, log)
 }
@@ -297,8 +298,8 @@ func Test_fdFilestatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // ctim
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_filestat_get(fd=0,result.buf=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_filestat_get(fd=0)
+<== (filestat={filetype=BLOCK_DEVICE,size=0,mtim=0},errno=ESUCCESS)
 `,
 		},
 		{
@@ -316,8 +317,8 @@ func Test_fdFilestatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // ctim
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_filestat_get(fd=1,result.buf=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_filestat_get(fd=1)
+<== (filestat={filetype=BLOCK_DEVICE,size=0,mtim=0},errno=ESUCCESS)
 `,
 		},
 		{
@@ -335,8 +336,8 @@ func Test_fdFilestatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // ctim
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_filestat_get(fd=2,result.buf=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_filestat_get(fd=2)
+<== (filestat={filetype=BLOCK_DEVICE,size=0,mtim=0},errno=ESUCCESS)
 `,
 		},
 		{
@@ -353,8 +354,8 @@ func Test_fdFilestatGet(t *testing.T) {
 				0, 0, 0, 0, 0, 0, 0, 0, // TODO: ctim
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_filestat_get(fd=3,result.buf=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_filestat_get(fd=3)
+<== (filestat={filetype=DIRECTORY,size=0,mtim=0},errno=ESUCCESS)
 `,
 		},
 		{
@@ -371,8 +372,8 @@ func Test_fdFilestatGet(t *testing.T) {
 				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // ctim
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_filestat_get(fd=4,result.buf=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_filestat_get(fd=4)
+<== (filestat={filetype=REGULAR_FILE,size=10,mtim=1667482413000000000},errno=ESUCCESS)
 `,
 		},
 		{
@@ -389,8 +390,8 @@ func Test_fdFilestatGet(t *testing.T) {
 				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // ctim
 			},
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_filestat_get(fd=5,result.buf=0)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_filestat_get(fd=5)
+<== (filestat={filetype=DIRECTORY,size=0,mtim=1667482413000000000},errno=ESUCCESS)
 `,
 		},
 		{
@@ -398,8 +399,8 @@ func Test_fdFilestatGet(t *testing.T) {
 			fd:            math.MaxUint32,
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_filestat_get(fd=-1,result.buf=0)
-<== errno=EBADF
+==> wasi_snapshot_preview1.fd_filestat_get(fd=-1)
+<== (filestat=,errno=EBADF)
 `,
 		},
 		{
@@ -408,8 +409,8 @@ func Test_fdFilestatGet(t *testing.T) {
 			resultFilestat: memorySize - 64 + 1,
 			expectedErrno:  ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_filestat_get(fd=5,result.buf=65473)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.fd_filestat_get(fd=5)
+<== (filestat=,errno=EFAULT)
 `,
 		},
 	}
@@ -557,7 +558,7 @@ func Test_fdPread_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_pread(fd=4,iovs=65536,iovs_len=65536,offset=7)
-<== (nread=OOM(65536,4),errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 		{
@@ -568,7 +569,7 @@ func Test_fdPread_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_pread(fd=4,iovs=65536,iovs_len=65535,offset=0)
-<== (nread=OOM(65535,4),errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 		{
@@ -582,7 +583,7 @@ func Test_fdPread_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_pread(fd=4,iovs=65532,iovs_len=65532,offset=0)
-<== (nread=2367,errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 		{
@@ -597,7 +598,7 @@ func Test_fdPread_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_pread(fd=4,iovs=65528,iovs_len=65528,offset=0)
-<== (nread=16777279,errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 		{
@@ -613,7 +614,7 @@ func Test_fdPread_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_pread(fd=4,iovs=65527,iovs_len=65527,offset=0)
-<== (nread=2367,errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 		{
@@ -630,7 +631,7 @@ func Test_fdPread_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_pread(fd=4,iovs=65527,iovs_len=65527,offset=0)
-<== (nread=OOM(65536,4),errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 	}
@@ -671,7 +672,7 @@ func Test_fdPrestatGet(t *testing.T) {
 	requireErrno(t, ErrnoSuccess, mod, fdPrestatGetName, uint64(fd), uint64(resultPrestat))
 	require.Equal(t, `
 ==> wasi_snapshot_preview1.fd_prestat_get(fd=3)
-<== (prestat=0000000001000000,errno=ESUCCESS)
+<== (prestat={pr_name_len=1},errno=ESUCCESS)
 `, "\n"+log.String())
 
 	actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
@@ -709,7 +710,7 @@ func Test_fdPrestatGet_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_prestat_get(fd=3)
-<== (prestat=OOM(65536,8),errno=EFAULT)
+<== (prestat=,errno=EFAULT)
 `,
 		},
 		// TODO: non pre-opened file == api.ErrnoBadf
@@ -778,7 +779,7 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=3)
-<== (path=OOM(65536,1),errno=EFAULT)
+<== (path=,errno=EFAULT)
 `,
 		},
 		{
@@ -789,7 +790,7 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_prestat_dir_name(fd=3)
-<== (path=OOM(65536,1),errno=EFAULT)
+<== (path=,errno=EFAULT)
 `,
 		},
 		{
@@ -833,8 +834,8 @@ func Test_fdPrestatDirName_Errors(t *testing.T) {
 func Test_fdPwrite(t *testing.T) {
 	log := requireErrnoNosys(t, fdPwriteName, 0, 0, 0, 0, 0)
 	require.Equal(t, `
---> wasi_snapshot_preview1.fd_pwrite(fd=0,iovs=0,iovs_len=0,offset=0,result.nwritten=0)
-<-- errno=ENOSYS
+--> wasi_snapshot_preview1.fd_pwrite(fd=0,iovs=0,iovs_len=0,offset=0)
+<-- (nwritten=,errno=ENOSYS)
 `, log)
 }
 
@@ -908,7 +909,7 @@ func Test_fdRead_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65536,iovs_len=65535)
-<== (nread=OOM(65535,4),errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 		{
@@ -922,7 +923,7 @@ func Test_fdRead_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65532,iovs_len=65532)
-<== (nread=2367,errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 		{
@@ -937,7 +938,7 @@ func Test_fdRead_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65528,iovs_len=65528)
-<== (nread=16777279,errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 		{
@@ -953,7 +954,7 @@ func Test_fdRead_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65527,iovs_len=65527)
-<== (nread=2367,errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 		{
@@ -970,7 +971,7 @@ func Test_fdRead_Errors(t *testing.T) {
 			expectedErrno: ErrnoFault,
 			expectedLog: `
 ==> wasi_snapshot_preview1.fd_read(fd=4,iovs=65527,iovs_len=65527)
-<== (nread=OOM(65536,4),errno=EFAULT)
+<== (nread=,errno=EFAULT)
 `,
 		},
 	}
@@ -1976,8 +1977,8 @@ func Test_fdWrite(t *testing.T) {
 
 	requireErrno(t, ErrnoSuccess, mod, fdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten))
 	require.Equal(t, `
-==> wasi_snapshot_preview1.fd_write(fd=4,iovs=1,iovs_len=2,result.nwritten=26)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_write(fd=4,iovs=1,iovs_len=2)
+<== (nwritten=6,errno=ESUCCESS)
 `, "\n"+log.String())
 
 	actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
@@ -2027,8 +2028,8 @@ func Test_fdWrite_discard(t *testing.T) {
 	fd := 1 // stdout
 	requireErrno(t, ErrnoSuccess, mod, fdWriteName, uint64(fd), uint64(iovs), uint64(iovsCount), uint64(resultNwritten))
 	require.Equal(t, `
-==> wasi_snapshot_preview1.fd_write(fd=1,iovs=1,iovs_len=2,result.nwritten=26)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.fd_write(fd=1,iovs=1,iovs_len=2)
+<== (nwritten=6,errno=ESUCCESS)
 `, "\n"+log.String())
 
 	actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
@@ -2057,8 +2058,8 @@ func Test_fdWrite_Errors(t *testing.T) {
 			fd:            42, // arbitrary invalid fd
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_write(fd=42,iovs=0,iovs_len=1,result.nwritten=0)
-<== errno=EBADF
+==> wasi_snapshot_preview1.fd_write(fd=42,iovs=0,iovs_len=1)
+<== (nwritten=,errno=EBADF)
 `,
 		},
 		{
@@ -2067,8 +2068,8 @@ func Test_fdWrite_Errors(t *testing.T) {
 			iovs:          memSize - 2,
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_write(fd=4,iovs=65534,iovs_len=1,result.nwritten=0)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.fd_write(fd=4,iovs=65534,iovs_len=1)
+<== (nwritten=,errno=EFAULT)
 `,
 		},
 		{
@@ -2077,8 +2078,8 @@ func Test_fdWrite_Errors(t *testing.T) {
 			iovs:          memSize - 4, // iovs[0].offset was 4 bytes and iovs[0].length next, but not enough mod.Memory()!
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_write(fd=4,iovs=65532,iovs_len=1,result.nwritten=0)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.fd_write(fd=4,iovs=65532,iovs_len=1)
+<== (nwritten=,errno=EFAULT)
 `,
 		},
 		{
@@ -2087,8 +2088,8 @@ func Test_fdWrite_Errors(t *testing.T) {
 			iovs:          memSize - 5, // iovs[0].offset (where to read "hi") is outside memory.
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_write(fd=4,iovs=65531,iovs_len=1,result.nwritten=0)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.fd_write(fd=4,iovs=65531,iovs_len=1)
+<== (nwritten=,errno=EFAULT)
 `,
 		},
 		{
@@ -2097,8 +2098,8 @@ func Test_fdWrite_Errors(t *testing.T) {
 			iovs:          memSize - 9, // iovs[0].offset (where to read "hi") is in memory, but truncated.
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_write(fd=4,iovs=65527,iovs_len=1,result.nwritten=0)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.fd_write(fd=4,iovs=65527,iovs_len=1)
+<== (nwritten=,errno=EFAULT)
 `,
 		},
 		{
@@ -2107,8 +2108,8 @@ func Test_fdWrite_Errors(t *testing.T) {
 			resultNwritten: memSize, // read was ok, but there wasn't enough memory to write the result.
 			expectedErrno:  ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.fd_write(fd=4,iovs=0,iovs_len=1,result.nwritten=65536)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.fd_write(fd=4,iovs=0,iovs_len=1)
+<== (nwritten=,errno=EFAULT)
 `,
 		},
 	}
@@ -2193,8 +2194,8 @@ func Test_pathFilestatGet(t *testing.T) {
 				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // ctim
 			),
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=0,path=a,result.buf=2)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=,path=a)
+<== (filestat={filetype=REGULAR_FILE,size=10,mtim=1667482413000000000},errno=ESUCCESS)
 `,
 		},
 		{
@@ -2215,8 +2216,8 @@ func Test_pathFilestatGet(t *testing.T) {
 				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // ctim
 			),
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=5,flags=0,path=a,result.buf=2)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.path_filestat_get(fd=5,flags=,path=a)
+<== (filestat={filetype=REGULAR_FILE,size=20,mtim=1667482413000000000},errno=ESUCCESS)
 `,
 		},
 		{
@@ -2237,8 +2238,8 @@ func Test_pathFilestatGet(t *testing.T) {
 				0x0, 0x82, 0x13, 0x80, 0x6b, 0x16, 0x24, 0x17, // ctim
 			),
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=0,path=b,result.buf=2)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=,path=b)
+<== (filestat={filetype=DIRECTORY,size=0,mtim=1667482413000000000},errno=ESUCCESS)
 `,
 		},
 		{
@@ -2246,8 +2247,8 @@ func Test_pathFilestatGet(t *testing.T) {
 			fd:            math.MaxUint32,
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=-1,flags=0,path=,result.buf=0)
-<== errno=EBADF
+==> wasi_snapshot_preview1.path_filestat_get(fd=-1,flags=,path=)
+<== (filestat=,errno=EBADF)
 `,
 		},
 		{
@@ -2258,8 +2259,8 @@ func Test_pathFilestatGet(t *testing.T) {
 			resultFilestat: 2,
 			expectedErrno:  ErrnoNotdir,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=4,flags=0,path=a,result.buf=2)
-<== errno=ENOTDIR
+==> wasi_snapshot_preview1.path_filestat_get(fd=4,flags=,path=a)
+<== (filestat=,errno=ENOTDIR)
 `,
 		},
 		{
@@ -2270,8 +2271,8 @@ func Test_pathFilestatGet(t *testing.T) {
 			resultFilestat: 2,
 			expectedErrno:  ErrnoNoent,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=0,path=?,result.buf=2)
-<== errno=ENOENT
+==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=,path=?)
+<== (filestat=,errno=ENOENT)
 `,
 		},
 		{
@@ -2282,8 +2283,8 @@ func Test_pathFilestatGet(t *testing.T) {
 			resultFilestat: 2,
 			expectedErrno:  ErrnoNoent,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=5,flags=0,path=?,result.buf=2)
-<== errno=ENOENT
+==> wasi_snapshot_preview1.path_filestat_get(fd=5,flags=,path=?)
+<== (filestat=,errno=ENOENT)
 `,
 		},
 		{
@@ -2294,8 +2295,8 @@ func Test_pathFilestatGet(t *testing.T) {
 			resultFilestat: 7,
 			expectedErrno:  ErrnoNoent,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=5,flags=0,path=../foo,result.buf=7)
-<== errno=ENOENT
+==> wasi_snapshot_preview1.path_filestat_get(fd=5,flags=,path=../foo)
+<== (filestat=,errno=ENOENT)
 `,
 		},
 		{
@@ -2305,8 +2306,8 @@ func Test_pathFilestatGet(t *testing.T) {
 			pathLen:       memorySize,
 			expectedErrno: ErrnoNametoolong,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=0,path=OOM(1,65536),result.buf=0)
-<== errno=ENAMETOOLONG
+==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=,path=OOM(1,65536))
+<== (filestat=,errno=ENAMETOOLONG)
 `,
 		},
 		{
@@ -2317,8 +2318,8 @@ func Test_pathFilestatGet(t *testing.T) {
 			resultFilestat: memorySize - 64 + 1,
 			expectedErrno:  ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=0,path=a,result.buf=65473)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.path_filestat_get(fd=3,flags=,path=a)
+<== (filestat=,errno=EFAULT)
 `,
 		},
 	}
@@ -2347,7 +2348,7 @@ func Test_pathFilestatGet(t *testing.T) {
 func Test_pathFilestatSetTimes(t *testing.T) {
 	log := requireErrnoNosys(t, pathFilestatSetTimesName, 0, 0, 0, 0, 0, 0, 0)
 	require.Equal(t, `
---> wasi_snapshot_preview1.path_filestat_set_times(fd=0,flags=0,path=,atim=0,mtim=0,fst_flags=0)
+--> wasi_snapshot_preview1.path_filestat_set_times(fd=0,flags=,path=,atim=0,mtim=0,fst_flags=0)
 <-- errno=ENOSYS
 `, log)
 }
@@ -2356,7 +2357,7 @@ func Test_pathFilestatSetTimes(t *testing.T) {
 func Test_pathLink(t *testing.T) {
 	log := requireErrnoNosys(t, pathLinkName, 0, 0, 0, 0, 0, 0, 0)
 	require.Equal(t, `
---> wasi_snapshot_preview1.path_link(old_fd=0,old_flags=0,old_path=,new_fd=0,new_path=)
+--> wasi_snapshot_preview1.path_link(old_fd=0,old_flags=,old_path=,new_fd=0,new_path=)
 <-- errno=ENOSYS
 `, log)
 }
@@ -2395,8 +2396,8 @@ func Test_pathOpen(t *testing.T) {
 	requireErrno(t, ErrnoSuccess, mod, pathOpenName, uint64(rootFD), uint64(dirflags), uint64(path),
 		uint64(pathLen), uint64(oflags), fsRightsBase, fsRightsInheriting, uint64(fdflags), uint64(resultOpenedFd))
 	require.Equal(t, `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=wazero,oflags=0,fs_rights_base=1,fs_rights_inheriting=2,fdflags=0,result.opened_fd=8)
-<== errno=ESUCCESS
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=wazero,oflags=,fs_rights_base=FD_DATASYNC,fs_rights_inheriting=FD_READ,fdflags=)
+<== (opened_fd=4,errno=ESUCCESS)
 `, "\n"+log.String())
 
 	actual, ok := mod.Memory().Read(0, uint32(len(expectedMemory)))
@@ -2435,8 +2436,8 @@ func Test_pathOpen_Errors(t *testing.T) {
 			fd:            42, // arbitrary invalid fd
 			expectedErrno: ErrnoBadf,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=42,dirflags=0,path=,oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0)
-<== errno=EBADF
+==> wasi_snapshot_preview1.path_open(fd=42,dirflags=,path=,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+<== (opened_fd=,errno=EBADF)
 `,
 		},
 		{
@@ -2446,8 +2447,8 @@ func Test_pathOpen_Errors(t *testing.T) {
 			pathLen:       validPathLen,
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=OOM(65536,6),oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=OOM(65536,6),oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+<== (opened_fd=,errno=EFAULT)
 `,
 		},
 		{
@@ -2458,19 +2459,19 @@ func Test_pathOpen_Errors(t *testing.T) {
 			// fstest.MapFS returns file not found instead of invalid on invalid path
 			expectedErrno: ErrnoNoent,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=../foo,oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0)
-<== errno=ENOENT
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=../foo,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+<== (opened_fd=,errno=ENOENT)
 `,
 		},
 		{
 			name:          "out-of-memory reading pathLen",
 			fd:            validFD,
 			path:          validPath,
-			pathLen:       mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is out-of-memory for path
+			pathLen:       mod.Memory().Size() + 1, // path is in the valid memory range, but pathLen is OOM for path
 			expectedErrno: ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=OOM(0,65537),oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=OOM(0,65537),oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+<== (opened_fd=,errno=EFAULT)
 `,
 		},
 		{
@@ -2481,8 +2482,8 @@ func Test_pathOpen_Errors(t *testing.T) {
 			pathLen:       validPathLen - 1, // this make the path "wazer", which doesn't exit
 			expectedErrno: ErrnoNoent,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=wazer,oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0)
-<== errno=ENOENT
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=wazer,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+<== (opened_fd=,errno=ENOENT)
 `,
 		},
 		{
@@ -2494,21 +2495,21 @@ func Test_pathOpen_Errors(t *testing.T) {
 			resultOpenedFd: mod.Memory().Size(), // path and pathLen correctly point to the right path, but where to write the opened FD is outside memory.
 			expectedErrno:  ErrnoFault,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=wazero,oflags=0,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=65536)
-<== errno=EFAULT
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=wazero,oflags=,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+<== (opened_fd=,errno=EFAULT)
 `,
 		},
 		{
 			name:          "oflags=directory, but not a directory",
-			oflags:        uint32(wasiOflagsDirectory),
+			oflags:        uint32(wasi_snapshot_preview1.O_DIRECTORY),
 			fd:            validFD,
 			pathName:      fileName,
 			path:          validPath,
 			pathLen:       validPathLen,
 			expectedErrno: ErrnoNotdir,
 			expectedLog: `
-==> wasi_snapshot_preview1.path_open(fd=3,dirflags=0,path=notdir,oflags=3,fs_rights_base=0,fs_rights_inheriting=0,fdflags=0,result.opened_fd=0)
-<== errno=ENOTDIR
+==> wasi_snapshot_preview1.path_open(fd=3,dirflags=,path=notdir,oflags=DIRECTORY,fs_rights_base=,fs_rights_inheriting=,fdflags=)
+<== (opened_fd=,errno=ENOTDIR)
 `,
 		},
 	}
diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go
index 397f17a8..e32f1341 100644
--- a/internal/gojs/compiler_test.go
+++ b/internal/gojs/compiler_test.go
@@ -23,7 +23,6 @@ import (
 
 func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig) (stdout, stderr string, err error) {
 	var stdoutBuf, stderrBuf bytes.Buffer
-
 	ns := rt.NewNamespace(ctx)
 	builder := rt.NewHostModuleBuilder("go")
 	gojs.NewFunctionExporter().ExportFunctions(builder)
diff --git a/internal/wasm/memory_test.go b/internal/wasm/memory_test.go
index b564eab6..832b2e45 100644
--- a/internal/wasm/memory_test.go
+++ b/internal/wasm/memory_test.go
@@ -756,7 +756,7 @@ func BenchmarkWriteString(b *testing.B) {
 		"hello world",
 		strings.Repeat("hello ", 10),
 	}
-	// nolint intentionally testing interface access
+	//nolint intentionally testing interface access
 	var mem api.Memory
 	mem = &MemoryInstance{Buffer: make([]byte, 1000), Min: 1}
 	for _, tt := range tests {