Repository: tetratelabs/wazero
Pull request #815 (2022-09-28 09:19:05)

Replaces MemorySizer and CompileConfig with RuntimeConfig
We formerly introduced `MemorySizer` as a way to control capacity independently of size. This was the first and only feature in `CompileConfig`. While possibly used privately, `MemorySizer` has never been used in public GitHub code.

These APIs interfere with how we do caching of compiled modules. Notably, they can change the min or max defined in wasm, which invalidates some constants. This has also had a bad experience, forcing everyone to boilerplate`wazero.NewCompileConfig()` despite that API never being used in open source.

This addresses the use cases in a different way, by moving configuration to `RuntimeConfig` instead. This allows us to remove `MemorySizer` and `CompileConfig`, and the problems with them, yet still retaining functionality in case someone uses it.

* `RuntimeConfig.WithMemoryLimitPages(uint32)`: Prevents memory from growing to 4GB (spec limit) per instance.
  * This works regardless of whether the wasm encodes max or not. If there is no max, it becomes effectively this value.
* `RuntimeConfig.WithMemoryCapacityFromMax(bool)`: Prevents reallocations (when growing).
  * Wasm that never sets max will grow from min to the limit above.

Note: Those who want to change their wasm (ex insert a max where there was none), have to do that externally, ex via compiler settings or post-build transformations such as [wabin](https://github.com/tetratelabs/wabin)



codefromthecrypt: rewrote the description based on feedback from @inkeliz which clarified the original intents.

Patch:
diff --git a/RATIONALE.md b/RATIONALE.md
index 0c9a1b70..8c90c83f 100644
--- a/RATIONALE.md
+++ b/RATIONALE.md
@@ -280,8 +280,6 @@ space. It is accepted that the options pattern is common in Go, which is the mai
 wazero's configuration types cover the three main scopes of WebAssembly use:
 * `RuntimeConfig`: This is the broadest scope, so applies also to compilation
   and instantiation. Ex. This controls the WebAssembly Specification Version.
-* `CompileConfig`: This affects any compilation related concerns not defined in
-  the binary format. Ex. This controls the allocation of linear memory.
 * `ModuleConfig`: This affects modules instantiated after compilation and what
   resources are allowed. Ex. This defines how or if STDOUT is captured.
 
diff --git a/api/wasm.go b/api/wasm.go
index 92181015..d4aad01f 100644
--- a/api/wasm.go
+++ b/api/wasm.go
@@ -486,18 +486,3 @@ func EncodeF64(input float64) uint64 {
 func DecodeF64(input uint64) float64 {
 	return math.Float64frombits(input)
 }
-
-// MemorySizer applies during compilation after a module has been decoded from wasm, but before it is instantiated.
-// This determines the amount of memory pages (65536 bytes per page) to use when a memory is instantiated as a []byte.
-//
-// Ex. Here's how to set the capacity to max instead of min, when set:
-//
-//	capIsMax := func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
-//		if maxPages != nil {
-//			return minPages, *maxPages, *maxPages
-//		}
-//		return minPages, minPages, 65536
-//	}
-//
-// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
-type MemorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32)
diff --git a/cmd/wazero/wazero.go b/cmd/wazero/wazero.go
index c7769477..cf2de795 100644
--- a/cmd/wazero/wazero.go
+++ b/cmd/wazero/wazero.go
@@ -94,7 +94,7 @@ func doRun(args []string, stdOut io.Writer, stdErr io.Writer, exit func(code int
 		WithSysNanotime().
 		WithSysWalltime().
 		WithArgs(append([]string{wasmExe}, wasmArgs...)...)
-	code, err := rt.CompileModule(ctx, wasm, wazero.NewCompileConfig())
+	code, err := rt.CompileModule(ctx, wasm)
 	if err != nil {
 		fmt.Fprintf(stdErr, "error compiling wasm binary: %v\n", err)
 		exit(1)
diff --git a/config.go b/config.go
index 9977236d..5642f7d5 100644
--- a/config.go
+++ b/config.go
@@ -3,6 +3,7 @@ package wazero
 import (
 	"context"
 	"errors"
+	"fmt"
 	"io"
 	"io/fs"
 	"math"
@@ -41,6 +42,28 @@ type RuntimeConfig interface {
 	// defaults to api.CoreFeaturesV2, even though it is not yet a Web
 	// Standard (REC).
 	WithCoreFeatures(api.CoreFeatures) RuntimeConfig
+
+	// WithMemoryLimitPages overrides the maximum pages allowed per memory. The
+	// default is 65536, allowing 4GB total memory per instance. Setting a
+	// value larger than default will panic.
+	//
+	// Ex. To reduce the largest possible memory size from 4GB to 128KB:
+	//	rConfig = wazero.NewRuntimeConfig().WithMemoryLimitPages(2)
+	//
+	// Note: Wasm has 32-bit memory and each page is 65536 (2^16) bytes. This
+	// implies a max of 65536 (2^16) addressable pages.
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
+	WithMemoryLimitPages(memoryLimitPages uint32) RuntimeConfig
+
+	// WithMemoryCapacityFromMax eagerly allocates max memory, unless max is
+	// not defined. The default is false, which means minimum memory is
+	// allocated and any call to grow memory results in re-allocations.
+	//
+	// Ex. To ensure any memory.grow instruction will never re-allocate:
+	//	rConfig = wazero.NewRuntimeConfig().WithMemoryCapacityFromMax(true)
+	//
+	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#grow-mem
+	WithMemoryCapacityFromMax(memoryCapacityFromMax bool) RuntimeConfig
 }
 
 // NewRuntimeConfig returns a RuntimeConfig using the compiler if it is supported in this environment,
@@ -50,14 +73,18 @@ func NewRuntimeConfig() RuntimeConfig {
 }
 
 type runtimeConfig struct {
-	enabledFeatures api.CoreFeatures
-	isInterpreter   bool
-	newEngine       func(context.Context, api.CoreFeatures) wasm.Engine
+	enabledFeatures       api.CoreFeatures
+	memoryLimitPages      uint32
+	memoryCapacityFromMax bool
+	isInterpreter         bool
+	newEngine             func(context.Context, api.CoreFeatures) wasm.Engine
 }
 
 // engineLessConfig helps avoid copy/pasting the wrong defaults.
 var engineLessConfig = &runtimeConfig{
-	enabledFeatures: api.CoreFeaturesV2,
+	enabledFeatures:       api.CoreFeaturesV2,
+	memoryLimitPages:      wasm.MemoryLimitPages,
+	memoryCapacityFromMax: false,
 }
 
 // NewRuntimeConfigCompiler compiles WebAssembly modules into
@@ -101,6 +128,24 @@ func (c *runtimeConfig) WithCoreFeatures(features api.CoreFeatures) RuntimeConfi
 	return ret
 }
 
+// WithMemoryLimitPages implements RuntimeConfig.WithMemoryLimitPages
+func (c *runtimeConfig) WithMemoryLimitPages(memoryLimitPages uint32) RuntimeConfig {
+	ret := c.clone()
+	// This panics instead of returning an error as it is unlikely.
+	if memoryLimitPages > wasm.MemoryLimitPages {
+		panic(fmt.Errorf("memoryLimitPages invalid: %d > %d", memoryLimitPages, wasm.MemoryLimitPages))
+	}
+	ret.memoryLimitPages = memoryLimitPages
+	return ret
+}
+
+// WithMemoryCapacityFromMax implements RuntimeConfig.WithMemoryCapacityFromMax
+func (c *runtimeConfig) WithMemoryCapacityFromMax(memoryCapacityFromMax bool) RuntimeConfig {
+	ret := c.clone()
+	ret.memoryCapacityFromMax = memoryCapacityFromMax
+	return ret
+}
+
 // CompiledModule is a WebAssembly module ready to be instantiated (Runtime.InstantiateModule) as an api.Module.
 //
 // In WebAssembly terminology, this is a decoded, validated, and possibly also compiled module. wazero avoids using
@@ -165,45 +210,6 @@ func (c *compiledModule) ExportedFunctions() map[string]api.FunctionDefinition {
 	return c.module.ExportedFunctions()
 }
 
-// CompileConfig allows you to override what was decoded from wasm, prior to compilation (HostModuleBuilder.Compile or
-// Runtime.CompileModule).
-//
-// For example, WithMemorySizer allows you to override memory size that doesn't match your requirements.
-//
-// Note: CompileConfig is immutable. Each WithXXX function returns a new instance including the corresponding change.
-type CompileConfig interface {
-	// WithMemorySizer are the allocation parameters used for a Wasm memory.
-	// The default is to set cap=min and max=65536 if unset. A nil function is invalid and ignored.
-	WithMemorySizer(api.MemorySizer) CompileConfig
-}
-
-type compileConfig struct {
-	memorySizer api.MemorySizer
-}
-
-// NewCompileConfig returns a CompileConfig that can be used for configuring module compilation.
-func NewCompileConfig() CompileConfig {
-	return &compileConfig{
-		memorySizer: wasm.MemorySizer,
-	}
-}
-
-// clone makes a deep copy of this compile config.
-func (c *compileConfig) clone() *compileConfig {
-	ret := *c // copy except maps which share a ref
-	return &ret
-}
-
-// WithMemorySizer implements CompileConfig.WithMemorySizer
-func (c *compileConfig) WithMemorySizer(memorySizer api.MemorySizer) CompileConfig {
-	if memorySizer == nil {
-		return c
-	}
-	ret := c.clone()
-	ret.memorySizer = memorySizer
-	return ret
-}
-
 // ModuleConfig configures resources needed by functions that have low-level interactions with the host operating
 // system. Using this, resources such as STDIN can be isolated, so that the same module can be safely instantiated
 // multiple times.
@@ -284,7 +290,7 @@ type ModuleConfig interface {
 	// otherwise, is compiler-specific. See /RATIONALE.md for notes.
 	WithFS(fs.FS) ModuleConfig
 
-	// WithName configures the module name. Defaults to what was decoded or overridden via CompileConfig.WithModuleName.
+	// WithName configures the module name. Defaults to what was decoded from the name section.
 	WithName(string) ModuleConfig
 
 	// WithStartFunctions configures the functions to call after the module is
diff --git a/examples/allocation/zig/greet.go b/examples/allocation/zig/greet.go
index 01fea07a..4015984e 100644
--- a/examples/allocation/zig/greet.go
+++ b/examples/allocation/zig/greet.go
@@ -45,7 +45,7 @@ func run() error {
 
 	// Instantiate a WebAssembly module that imports the "log" function defined
 	// in "env" and exports "memory" and functions we'll use in this example.
-	compiled, err := r.CompileModule(ctx, greetWasm, wazero.NewCompileConfig())
+	compiled, err := r.CompileModule(ctx, greetWasm)
 	if err != nil {
 		return err
 	}
diff --git a/examples/namespace/counter.go b/examples/namespace/counter.go
index 9b5a02dc..ad42d8f2 100644
--- a/examples/namespace/counter.go
+++ b/examples/namespace/counter.go
@@ -29,7 +29,7 @@ func main() {
 	defer r.Close(ctx) // This closes everything this Runtime created.
 
 	// Compile WebAssembly that requires its own "env" module.
-	compiled, err := r.CompileModule(ctx, counterWasm, wazero.NewCompileConfig())
+	compiled, err := r.CompileModule(ctx, counterWasm)
 	if err != nil {
 		log.Panicln(err)
 	}
diff --git a/imports/assemblyscript/example/assemblyscript.go b/imports/assemblyscript/example/assemblyscript.go
index 524b8d47..4f59a2cd 100644
--- a/imports/assemblyscript/example/assemblyscript.go
+++ b/imports/assemblyscript/example/assemblyscript.go
@@ -38,7 +38,7 @@ func main() {
 	}
 
 	// Compile the WebAssembly module using the default configuration.
-	code, err := r.CompileModule(ctx, asWasm, wazero.NewCompileConfig())
+	code, err := r.CompileModule(ctx, asWasm)
 	if err != nil {
 		log.Panicln(err)
 	}
diff --git a/imports/go/example/stars.go b/imports/go/example/stars.go
index 4249ac8e..f3588daf 100644
--- a/imports/go/example/stars.go
+++ b/imports/go/example/stars.go
@@ -46,7 +46,7 @@ func main() {
 
 	// Compile the WebAssembly module using the default configuration.
 	start := time.Now()
-	compiled, err := r.CompileModule(ctx, bin, wazero.NewCompileConfig())
+	compiled, err := r.CompileModule(ctx, bin)
 	if err != nil {
 		log.Panicln(err)
 	}
diff --git a/imports/wasi_snapshot_preview1/example/cat.go b/imports/wasi_snapshot_preview1/example/cat.go
index 79db3426..bfcc0a13 100644
--- a/imports/wasi_snapshot_preview1/example/cat.go
+++ b/imports/wasi_snapshot_preview1/example/cat.go
@@ -77,7 +77,7 @@ func main() {
 	}
 
 	// Compile the WebAssembly module using the default configuration.
-	code, err := r.CompileModule(ctx, catWasm, wazero.NewCompileConfig())
+	code, err := r.CompileModule(ctx, catWasm)
 	if err != nil {
 		log.Panicln(err)
 	}
diff --git a/internal/wasm/binary/decoder.go b/internal/wasm/binary/decoder.go
index 354ce514..6747dce9 100644
--- a/internal/wasm/binary/decoder.go
+++ b/internal/wasm/binary/decoder.go
@@ -16,7 +16,8 @@ import (
 func DecodeModule(
 	binary []byte,
 	enabledFeatures api.CoreFeatures,
-	memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32),
+	memoryLimitPages uint32,
+	memoryCapacityFromMax bool,
 ) (*wasm.Module, error) {
 	r := bytes.NewReader(binary)
 
@@ -31,6 +32,8 @@ func DecodeModule(
 		return nil, ErrInvalidVersion
 	}
 
+	memorySizer := newMemorySizer(memoryLimitPages, memoryCapacityFromMax)
+
 	m := &wasm.Module{}
 	for {
 		// TODO: except custom sections, all others are required to be in order, but we aren't checking yet.
@@ -77,7 +80,7 @@ func DecodeModule(
 		case wasm.SectionIDType:
 			m.TypeSection, err = decodeTypeSection(enabledFeatures, r)
 		case wasm.SectionIDImport:
-			if m.ImportSection, err = decodeImportSection(r, memorySizer, enabledFeatures); err != nil {
+			if m.ImportSection, err = decodeImportSection(r, memorySizer, memoryLimitPages, enabledFeatures); err != nil {
 				return nil, err // avoid re-wrapping the error.
 			}
 		case wasm.SectionIDFunction:
@@ -85,7 +88,7 @@ func DecodeModule(
 		case wasm.SectionIDTable:
 			m.TableSection, err = decodeTableSection(r, enabledFeatures)
 		case wasm.SectionIDMemory:
-			m.MemorySection, err = decodeMemorySection(r, memorySizer)
+			m.MemorySection, err = decodeMemorySection(r, memorySizer, memoryLimitPages)
 		case wasm.SectionIDGlobal:
 			if m.GlobalSection, err = decodeGlobalSection(r, enabledFeatures); err != nil {
 				return nil, err // avoid re-wrapping the error.
@@ -128,3 +131,20 @@ func DecodeModule(
 	}
 	return m, nil
 }
+
+// memorySizer derives min, capacity and max pages from decoded wasm.
+type memorySizer func(minPages uint32, maxPages *uint32) (min uint32, capacity uint32, max uint32)
+
+// newMemorySizer sets capacity to minPages unless max is defined and
+// memoryCapacityFromMax is true.
+func newMemorySizer(memoryLimitPages uint32, memoryCapacityFromMax bool) memorySizer {
+	return func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
+		if maxPages != nil {
+			if memoryCapacityFromMax {
+				return minPages, *maxPages, *maxPages
+			}
+			return minPages, minPages, *maxPages
+		}
+		return minPages, minPages, memoryLimitPages
+	}
+}
diff --git a/internal/wasm/binary/import.go b/internal/wasm/binary/import.go
index d34c290d..094b488b 100644
--- a/internal/wasm/binary/import.go
+++ b/internal/wasm/binary/import.go
@@ -13,6 +13,7 @@ func decodeImport(
 	r *bytes.Reader,
 	idx uint32,
 	memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32),
+	memoryLimitPages uint32,
 	enabledFeatures api.CoreFeatures,
 ) (i *wasm.Import, err error) {
 	i = &wasm.Import{}
@@ -35,7 +36,7 @@ func decodeImport(
 	case wasm.ExternTypeTable:
 		i.DescTable, err = decodeTable(r, enabledFeatures)
 	case wasm.ExternTypeMemory:
-		i.DescMem, err = decodeMemory(r, memorySizer)
+		i.DescMem, err = decodeMemory(r, memorySizer, memoryLimitPages)
 	case wasm.ExternTypeGlobal:
 		i.DescGlobal, err = decodeGlobalType(r)
 	default:
diff --git a/internal/wasm/binary/memory.go b/internal/wasm/binary/memory.go
index 488c416e..7ce40e43 100644
--- a/internal/wasm/binary/memory.go
+++ b/internal/wasm/binary/memory.go
@@ -12,6 +12,7 @@ import (
 func decodeMemory(
 	r *bytes.Reader,
 	memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32),
+	memoryLimitPages uint32,
 ) (*wasm.Memory, error) {
 	min, maxP, err := decodeLimitsType(r)
 	if err != nil {
@@ -21,7 +22,7 @@ func decodeMemory(
 	min, capacity, max := memorySizer(min, maxP)
 	mem := &wasm.Memory{Min: min, Cap: capacity, Max: max, IsMaxEncoded: maxP != nil}
 
-	return mem, mem.Validate()
+	return mem, mem.Validate(memoryLimitPages)
 }
 
 // encodeMemory returns the wasm.Memory encoded in WebAssembly 1.0 (20191205) Binary Format.
diff --git a/internal/wasm/binary/section.go b/internal/wasm/binary/section.go
index 3711778b..bd343ddb 100644
--- a/internal/wasm/binary/section.go
+++ b/internal/wasm/binary/section.go
@@ -28,6 +28,7 @@ func decodeTypeSection(enabledFeatures api.CoreFeatures, r *bytes.Reader) ([]*wa
 func decodeImportSection(
 	r *bytes.Reader,
 	memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32),
+	memoryLimitPages uint32,
 	enabledFeatures api.CoreFeatures,
 ) ([]*wasm.Import, error) {
 	vs, _, err := leb128.DecodeUint32(r)
@@ -37,7 +38,7 @@ func decodeImportSection(
 
 	result := make([]*wasm.Import, vs)
 	for i := uint32(0); i < vs; i++ {
-		if result[i], err = decodeImport(r, i, memorySizer, enabledFeatures); err != nil {
+		if result[i], err = decodeImport(r, i, memorySizer, memoryLimitPages, enabledFeatures); err != nil {
 			return nil, err
 		}
 	}
@@ -84,6 +85,7 @@ func decodeTableSection(r *bytes.Reader, enabledFeatures api.CoreFeatures) ([]*w
 func decodeMemorySection(
 	r *bytes.Reader,
 	memorySizer func(minPages uint32, maxPages *uint32) (min, capacity, max uint32),
+	memoryLimitPages uint32,
 ) (*wasm.Memory, error) {
 	vs, _, err := leb128.DecodeUint32(r)
 	if err != nil {
@@ -93,7 +95,7 @@ func decodeMemorySection(
 		return nil, fmt.Errorf("at most one memory allowed in module, but read %d", vs)
 	}
 
-	return decodeMemory(r, memorySizer)
+	return decodeMemory(r, memorySizer, memoryLimitPages)
 }
 
 func decodeGlobalSection(r *bytes.Reader, enabledFeatures api.CoreFeatures) ([]*wasm.Global, error) {
diff --git a/internal/wasm/memory.go b/internal/wasm/memory.go
index 08922032..9ca5c535 100644
--- a/internal/wasm/memory.go
+++ b/internal/wasm/memory.go
@@ -24,15 +24,6 @@ const (
 	MemoryPageSizeInBits = 16
 )
 
-// MemorySizer is the default function that derives min, capacity and max pages from decoded wasm. The capacity
-// returned is set to minPages and max defaults to MemoryLimitPages when maxPages is nil.
-var MemorySizer api.MemorySizer = func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
-	if maxPages != nil {
-		return minPages, minPages, *maxPages
-	}
-	return minPages, minPages, MemoryLimitPages
-}
-
 // compile-time check to ensure MemoryInstance implements api.Memory
 var _ api.Memory = &MemoryInstance{}
 
diff --git a/internal/wasm/module.go b/internal/wasm/module.go
index 36005c7e..dcd637e7 100644
--- a/internal/wasm/module.go
+++ b/internal/wasm/module.go
@@ -751,24 +751,24 @@ type Memory struct {
 }
 
 // Validate ensures values assigned to Min, Cap and Max are within valid thresholds.
-func (m *Memory) Validate() error {
+func (m *Memory) Validate(memoryLimitPages uint32) error {
 	min, capacity, max := m.Min, m.Cap, m.Max
 
-	if max > MemoryLimitPages {
+	if max > memoryLimitPages {
 		return fmt.Errorf("max %d pages (%s) over limit of %d pages (%s)",
-			max, PagesToUnitOfBytes(max), MemoryLimitPages, PagesToUnitOfBytes(MemoryLimitPages))
-	} else if min > MemoryLimitPages {
+			max, PagesToUnitOfBytes(max), memoryLimitPages, PagesToUnitOfBytes(memoryLimitPages))
+	} else if min > memoryLimitPages {
 		return fmt.Errorf("min %d pages (%s) over limit of %d pages (%s)",
-			min, PagesToUnitOfBytes(min), MemoryLimitPages, PagesToUnitOfBytes(MemoryLimitPages))
+			min, PagesToUnitOfBytes(min), memoryLimitPages, PagesToUnitOfBytes(memoryLimitPages))
 	} else if min > max {
 		return fmt.Errorf("min %d pages (%s) > max %d pages (%s)",
 			min, PagesToUnitOfBytes(min), max, PagesToUnitOfBytes(max))
 	} else if capacity < min {
 		return fmt.Errorf("capacity %d pages (%s) less than minimum %d pages (%s)",
 			capacity, PagesToUnitOfBytes(capacity), min, PagesToUnitOfBytes(min))
-	} else if capacity > MemoryLimitPages {
+	} else if capacity > memoryLimitPages {
 		return fmt.Errorf("capacity %d pages (%s) over limit of %d pages (%s)",
-			capacity, PagesToUnitOfBytes(capacity), MemoryLimitPages, PagesToUnitOfBytes(MemoryLimitPages))
+			capacity, PagesToUnitOfBytes(capacity), memoryLimitPages, PagesToUnitOfBytes(memoryLimitPages))
 	}
 	return nil
 }
diff --git a/runtime.go b/runtime.go
index 2b7527aa..17d2c190 100644
--- a/runtime.go
+++ b/runtime.go
@@ -34,7 +34,7 @@ type Runtime interface {
 	NewHostModuleBuilder(moduleName string) HostModuleBuilder
 
 	// CompileModule decodes the WebAssembly binary (%.wasm) or errs if invalid.
-	// Any pre-compilation done after decoding wasm is dependent on RuntimeConfig or CompileConfig.
+	// Any pre-compilation done after decoding wasm is dependent on RuntimeConfig.
 	//
 	// There are two main reasons to use CompileModule instead of InstantiateModuleFromBinary:
 	//   - Improve performance when the same module is instantiated multiple times under different names
@@ -43,10 +43,10 @@ type Runtime interface {
 	// # Notes
 	//
 	//   - The resulting module name defaults to what was binary from the custom name section.
-	//   - Any pre-compilation done after decoding the source is dependent on RuntimeConfig or CompileConfig.
+	//   - Any pre-compilation done after decoding the source is dependent on RuntimeConfig.
 	//
 	// See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#name-section%E2%91%A0
-	CompileModule(ctx context.Context, binary []byte, config CompileConfig) (CompiledModule, error)
+	CompileModule(ctx context.Context, binary []byte) (CompiledModule, error)
 
 	// InstantiateModuleFromBinary instantiates a module from the WebAssembly binary (%.wasm) or errs if invalid.
 	//
@@ -130,20 +130,24 @@ func NewRuntimeWithConfig(ctx context.Context, rConfig RuntimeConfig) Runtime {
 	config := rConfig.(*runtimeConfig)
 	store, ns := wasm.NewStore(config.enabledFeatures, config.newEngine(ctx, config.enabledFeatures))
 	return &runtime{
-		store:           store,
-		ns:              &namespace{store: store, ns: ns},
-		enabledFeatures: config.enabledFeatures,
-		isInterpreter:   config.isInterpreter,
+		store:                 store,
+		ns:                    &namespace{store: store, ns: ns},
+		enabledFeatures:       config.enabledFeatures,
+		memoryLimitPages:      config.memoryLimitPages,
+		memoryCapacityFromMax: config.memoryCapacityFromMax,
+		isInterpreter:         config.isInterpreter,
 	}
 }
 
 // runtime allows decoupling of public interfaces from internal representation.
 type runtime struct {
-	store           *wasm.Store
-	ns              *namespace
-	enabledFeatures api.CoreFeatures
-	isInterpreter   bool
-	compiledModules []*compiledModule
+	store                 *wasm.Store
+	ns                    *namespace
+	enabledFeatures       api.CoreFeatures
+	memoryLimitPages      uint32
+	memoryCapacityFromMax bool
+	isInterpreter         bool
+	compiledModules       []*compiledModule
 }
 
 // NewNamespace implements Runtime.NewNamespace.
@@ -157,17 +161,16 @@ func (r *runtime) Module(moduleName string) api.Module {
 }
 
 // CompileModule implements Runtime.CompileModule
-func (r *runtime) CompileModule(ctx context.Context, binary []byte, cConfig CompileConfig) (CompiledModule, error) {
+func (r *runtime) CompileModule(ctx context.Context, binary []byte) (CompiledModule, error) {
 	if binary == nil {
 		return nil, errors.New("binary == nil")
 	}
 
-	config := cConfig.(*compileConfig)
 	if len(binary) < 4 || !bytes.Equal(binary[0:4], binaryformat.Magic) {
 		return nil, errors.New("invalid binary")
 	}
 
-	internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures, config.memorySizer)
+	internal, err := binaryformat.DecodeModule(binary, r.enabledFeatures, r.memoryLimitPages, r.memoryCapacityFromMax)
 	if err != nil {
 		return nil, err
 	} else if err = internal.Validate(r.enabledFeatures); err != nil {
@@ -215,7 +218,7 @@ func buildListeners(ctx context.Context, r *runtime, internal *wasm.Module) ([]e
 
 // InstantiateModuleFromBinary implements Runtime.InstantiateModuleFromBinary
 func (r *runtime) InstantiateModuleFromBinary(ctx context.Context, binary []byte) (api.Module, error) {
-	if compiled, err := r.CompileModule(ctx, binary, NewCompileConfig()); err != nil {
+	if compiled, err := r.CompileModule(ctx, binary); err != nil {
 		return nil, err
 	} else {
 		compiled.(*compiledModule).closeWithModule = true


Test patch:
diff --git a/config_test.go b/config_test.go
index 78db1cbf..3cc59622 100644
--- a/config_test.go
+++ b/config_test.go
@@ -6,7 +6,6 @@ import (
 	"io"
 	"io/fs"
 	"math"
-	"reflect"
 	"testing"
 	"testing/fstest"
 
@@ -33,58 +32,45 @@ func TestRuntimeConfig(t *testing.T) {
 				enabledFeatures: api.CoreFeaturesV1,
 			},
 		},
-	}
-	for _, tt := range tests {
-		tc := tt
-
-		t.Run(tc.name, func(t *testing.T) {
-			input := &runtimeConfig{}
-			rc := tc.with(input)
-			require.Equal(t, tc.expected, rc)
-			// The source wasn't modified
-			require.Equal(t, &runtimeConfig{}, input)
-		})
-	}
-}
-
-func TestCompileConfig(t *testing.T) {
-	mp := func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
-		return 0, 1, 1
-	}
-	tests := []struct {
-		name     string
-		with     func(CompileConfig) CompileConfig
-		expected *compileConfig
-	}{
 		{
-			name: "WithMemorySizer",
-			with: func(c CompileConfig) CompileConfig {
-				return c.WithMemorySizer(mp)
+			name: "memoryLimitPages",
+			with: func(c RuntimeConfig) RuntimeConfig {
+				return c.WithMemoryLimitPages(10)
+			},
+			expected: &runtimeConfig{
+				memoryLimitPages: 10,
 			},
-			expected: &compileConfig{memorySizer: mp},
 		},
 		{
-			name: "WithMemorySizer twice",
-			with: func(c CompileConfig) CompileConfig {
-				return c.WithMemorySizer(wasm.MemorySizer).WithMemorySizer(mp)
+			name: "memoryCapacityFromMax",
+			with: func(c RuntimeConfig) RuntimeConfig {
+				return c.WithMemoryCapacityFromMax(true)
+			},
+			expected: &runtimeConfig{
+				memoryCapacityFromMax: true,
 			},
-			expected: &compileConfig{memorySizer: mp},
 		},
 	}
+
 	for _, tt := range tests {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			input := &compileConfig{}
-			rc := tc.with(input).(*compileConfig)
-
-			// We cannot compare func, but we can compare reflect.Value
-			// See https://go.dev/ref/spec#Comparison_operators
-			require.Equal(t, reflect.ValueOf(tc.expected.memorySizer), reflect.ValueOf(rc.memorySizer))
+			input := &runtimeConfig{}
+			rc := tc.with(input)
+			require.Equal(t, tc.expected, rc)
 			// The source wasn't modified
-			require.Equal(t, &compileConfig{}, input)
+			require.Equal(t, &runtimeConfig{}, input)
 		})
 	}
+
+	t.Run("memoryLimitPages invalid panics", func(t *testing.T) {
+		err := require.CapturePanic(func() {
+			input := &runtimeConfig{}
+			input.WithMemoryLimitPages(wasm.MemoryLimitPages + 1)
+		})
+		require.EqualError(t, err, "memoryLimitPages invalid: 65537 > 65536")
+	})
 }
 
 func TestModuleConfig(t *testing.T) {
diff --git a/experimental/compilation_cache_example_test.go b/experimental/compilation_cache_example_test.go
index 0737c3ab..35a8ef89 100644
--- a/experimental/compilation_cache_example_test.go
+++ b/experimental/compilation_cache_example_test.go
@@ -41,7 +41,7 @@ func newRuntimeCompileClose(ctx context.Context) {
 	r := wazero.NewRuntime(ctx)
 	defer r.Close(ctx) // This closes everything this Runtime created except the file system cache.
 
-	_, err := r.CompileModule(ctx, fsWasm, wazero.NewCompileConfig())
+	_, err := r.CompileModule(ctx, fsWasm)
 	if err != nil {
 		log.Panicln(err)
 	}
diff --git a/experimental/listener_example_test.go b/experimental/listener_example_test.go
index 90475859..90c5f654 100644
--- a/experimental/listener_example_test.go
+++ b/experimental/listener_example_test.go
@@ -64,7 +64,7 @@ func Example_customListenerFactory() {
 	wasi_snapshot_preview1.MustInstantiate(ctx, r)
 
 	// Compile the WebAssembly module using the default configuration.
-	code, err := r.CompileModule(ctx, listenerWasm, wazero.NewCompileConfig())
+	code, err := r.CompileModule(ctx, listenerWasm)
 	if err != nil {
 		log.Panicln(err)
 	}
diff --git a/experimental/listener_test.go b/experimental/listener_test.go
index dedc8319..e034522c 100644
--- a/experimental/listener_test.go
+++ b/experimental/listener_test.go
@@ -53,7 +53,7 @@ func TestFunctionListenerFactory(t *testing.T) {
 		t.Run("fails on compile if compiler", func(t *testing.T) {
 			r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigCompiler())
 			defer r.Close(testCtx) // This closes everything this Runtime created.
-			_, err := r.CompileModule(ctx, bin, wazero.NewCompileConfig())
+			_, err := r.CompileModule(ctx, bin)
 			require.EqualError(t, err,
 				"context includes a FunctionListenerFactoryKey, which is only supported in the interpreter")
 		})
@@ -62,7 +62,7 @@ func TestFunctionListenerFactory(t *testing.T) {
 	r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfigInterpreter())
 	defer r.Close(ctx) // This closes everything this Runtime created.
 
-	_, err := r.CompileModule(ctx, bin, wazero.NewCompileConfig())
+	_, err := r.CompileModule(ctx, bin)
 	require.NoError(t, err)
 
 	// Ensure each function was converted to a listener eagerly
diff --git a/experimental/logging/log_listener_example_test.go b/experimental/logging/log_listener_example_test.go
index 6b381b04..2aa318da 100644
--- a/experimental/logging/log_listener_example_test.go
+++ b/experimental/logging/log_listener_example_test.go
@@ -30,7 +30,7 @@ func Example_newLoggingListenerFactory() {
 	wasi_snapshot_preview1.MustInstantiate(ctx, r)
 
 	// Compile the WebAssembly module using the default configuration.
-	code, err := r.CompileModule(ctx, listenerWasm, wazero.NewCompileConfig())
+	code, err := r.CompileModule(ctx, listenerWasm)
 	if err != nil {
 		log.Panicln(err)
 	}
diff --git a/imports/assemblyscript/assemblyscript_test.go b/imports/assemblyscript/assemblyscript_test.go
index 7a578c0b..bf07c13d 100644
--- a/imports/assemblyscript/assemblyscript_test.go
+++ b/imports/assemblyscript/assemblyscript_test.go
@@ -435,7 +435,7 @@ func requireProxyModule(t *testing.T, fns FunctionExporter, config wazero.Module
 
 	proxyBin := proxy.GetProxyModuleBinary("env", envCompiled)
 
-	proxyCompiled, err := r.CompileModule(ctx, proxyBin, wazero.NewCompileConfig())
+	proxyCompiled, err := r.CompileModule(ctx, proxyBin)
 	require.NoError(t, err)
 
 	mod, err := r.InstantiateModule(ctx, proxyCompiled, config)
diff --git a/imports/go/example/stars_test.go b/imports/go/example/stars_test.go
index 66b3588f..8499e62e 100644
--- a/imports/go/example/stars_test.go
+++ b/imports/go/example/stars_test.go
@@ -71,7 +71,7 @@ func Benchmark_main(b *testing.B) {
 	if err != nil {
 		b.Fatal(err)
 	}
-	compiled, err := r.CompileModule(ctx, bin, wazero.NewCompileConfig())
+	compiled, err := r.CompileModule(ctx, bin)
 	if err != nil {
 		b.Fatal(err)
 	}
diff --git a/imports/wasi_snapshot_preview1/example_test.go b/imports/wasi_snapshot_preview1/example_test.go
index 6350a7c9..aaed1cee 100644
--- a/imports/wasi_snapshot_preview1/example_test.go
+++ b/imports/wasi_snapshot_preview1/example_test.go
@@ -36,7 +36,7 @@ func Example() {
 	defer wm.Close(testCtx)
 
 	// Compile the WebAssembly module using the default configuration.
-	code, err := r.CompileModule(ctx, exitOnStartWasm, wazero.NewCompileConfig())
+	code, err := r.CompileModule(ctx, exitOnStartWasm)
 	if err != nil {
 		log.Panicln(err)
 	}
diff --git a/imports/wasi_snapshot_preview1/usage_test.go b/imports/wasi_snapshot_preview1/usage_test.go
index f05dd286..8cec8cc2 100644
--- a/imports/wasi_snapshot_preview1/usage_test.go
+++ b/imports/wasi_snapshot_preview1/usage_test.go
@@ -25,7 +25,7 @@ func TestInstantiateModule(t *testing.T) {
 	require.NoError(t, err)
 	defer wm.Close(testCtx)
 
-	compiled, err := r.CompileModule(testCtx, wasiArg, wazero.NewCompileConfig())
+	compiled, err := r.CompileModule(testCtx, wasiArg)
 	require.NoError(t, err)
 	defer compiled.Close(testCtx)
 
diff --git a/imports/wasi_snapshot_preview1/wasi_bench_test.go b/imports/wasi_snapshot_preview1/wasi_bench_test.go
index 9ec011d2..66e4d6a4 100644
--- a/imports/wasi_snapshot_preview1/wasi_bench_test.go
+++ b/imports/wasi_snapshot_preview1/wasi_bench_test.go
@@ -46,7 +46,7 @@ func Benchmark_EnvironGet(b *testing.B) {
 	compiled, err := r.CompileModule(testCtx, binaryformat.EncodeModule(&wasm.Module{
 		MemorySection: &wasm.Memory{Min: 1, Max: 1},
 		ExportSection: []*wasm.Export{{Name: "memory", Type: api.ExternTypeMemory}},
-	}), wazero.NewCompileConfig())
+	}))
 	if err != nil {
 		b.Fatal(err)
 	}
diff --git a/imports/wasi_snapshot_preview1/wasi_test.go b/imports/wasi_snapshot_preview1/wasi_test.go
index 57b58be9..bc2d8679 100644
--- a/imports/wasi_snapshot_preview1/wasi_test.go
+++ b/imports/wasi_snapshot_preview1/wasi_test.go
@@ -42,7 +42,7 @@ func requireProxyModule(t *testing.T, config wazero.ModuleConfig) (api.Module, a
 
 	proxyBin := proxy.GetProxyModuleBinary(ModuleName, wasiModuleCompiled)
 
-	proxyCompiled, err := r.CompileModule(ctx, proxyBin, wazero.NewCompileConfig())
+	proxyCompiled, err := r.CompileModule(ctx, proxyBin)
 	require.NoError(t, err)
 
 	mod, err := r.InstantiateModule(ctx, proxyCompiled, config)
@@ -72,7 +72,7 @@ func requireErrnoNosys(t *testing.T, funcName string, params ...uint64) string {
 
 	proxyBin := proxy.GetProxyModuleBinary(ModuleName, wasiModuleCompiled)
 
-	proxyCompiled, err := r.CompileModule(ctx, proxyBin, wazero.NewCompileConfig())
+	proxyCompiled, err := r.CompileModule(ctx, proxyBin)
 	require.NoError(t, err)
 
 	mod, err := r.InstantiateModule(ctx, proxyCompiled, wazero.NewModuleConfig())
diff --git a/internal/gojs/compiler_test.go b/internal/gojs/compiler_test.go
index fc561e6d..de22303a 100644
--- a/internal/gojs/compiler_test.go
+++ b/internal/gojs/compiler_test.go
@@ -27,7 +27,7 @@ func compileAndRun(ctx context.Context, arg string, config wazero.ModuleConfig)
 	r := wazero.NewRuntimeWithConfig(testCtx, wazero.NewRuntimeConfig())
 	defer r.Close(ctx)
 
-	compiled, compileErr := r.CompileModule(ctx, testBin, wazero.NewCompileConfig())
+	compiled, compileErr := r.CompileModule(ctx, testBin)
 	if compileErr != nil {
 		err = compileErr
 		return
@@ -85,7 +85,7 @@ func TestMain(m *testing.M) {
 	// one test from a cache-miss performance penalty.
 	rt := wazero.NewRuntimeWithConfig(testCtx, wazero.NewRuntimeConfig())
 	defer rt.Close(testCtx)
-	_, err = rt.CompileModule(testCtx, testBin, wazero.NewCompileConfig())
+	_, err = rt.CompileModule(testCtx, testBin)
 	if err != nil {
 		log.Panicln(err)
 	}
diff --git a/internal/integration_test/bench/bench_test.go b/internal/integration_test/bench/bench_test.go
index 234203b5..7fa8f48c 100644
--- a/internal/integration_test/bench/bench_test.go
+++ b/internal/integration_test/bench/bench_test.go
@@ -79,7 +79,7 @@ func BenchmarkCompilation(b *testing.B) {
 }
 
 func runCompilation(b *testing.B, r wazero.Runtime) wazero.CompiledModule {
-	compiled, err := r.CompileModule(testCtx, caseWasm, wazero.NewCompileConfig())
+	compiled, err := r.CompileModule(testCtx, caseWasm)
 	if err != nil {
 		b.Fatal(err)
 	}
diff --git a/internal/integration_test/bench/codec_test.go b/internal/integration_test/bench/codec_test.go
index 52a66ab1..82649520 100644
--- a/internal/integration_test/bench/codec_test.go
+++ b/internal/integration_test/bench/codec_test.go
@@ -89,7 +89,7 @@ func newExample() *wasm.Module {
 
 func TestExampleUpToDate(t *testing.T) {
 	t.Run("binary.DecodeModule", func(t *testing.T) {
-		m, err := binary.DecodeModule(exampleWasm, api.CoreFeaturesV2, wasm.MemorySizer)
+		m, err := binary.DecodeModule(exampleWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false)
 		require.NoError(t, err)
 		require.Equal(t, example, m)
 	})
@@ -116,7 +116,7 @@ func BenchmarkCodec(b *testing.B) {
 	b.Run("binary.DecodeModule", func(b *testing.B) {
 		b.ReportAllocs()
 		for i := 0; i < b.N; i++ {
-			if _, err := binary.DecodeModule(exampleWasm, api.CoreFeaturesV2, wasm.MemorySizer); err != nil {
+			if _, err := binary.DecodeModule(exampleWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false); err != nil {
 				b.Fatal(err)
 			}
 		}
diff --git a/internal/integration_test/engine/adhoc_test.go b/internal/integration_test/engine/adhoc_test.go
index a9b6f403..b73e8df4 100644
--- a/internal/integration_test/engine/adhoc_test.go
+++ b/internal/integration_test/engine/adhoc_test.go
@@ -26,14 +26,6 @@ const (
 
 var memoryCapacityPages = uint32(2)
 
-var compileConfig = wazero.NewCompileConfig().
-	WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
-		if maxPages != nil {
-			return minPages, memoryCapacityPages, *maxPages
-		}
-		return minPages, memoryCapacityPages, memoryCapacityPages
-	})
-
 var moduleConfig = wazero.NewModuleConfig()
 
 var tests = map[string]func(t *testing.T, r wazero.Runtime){
@@ -529,7 +521,7 @@ func testCloseInFlight(t *testing.T, r wazero.Runtime) {
 
 			// Import that module.
 			binary := callReturnImportWasm(t, imported.Name(), t.Name()+"-importing", i32)
-			importingCode, err = r.CompileModule(testCtx, binary, compileConfig)
+			importingCode, err = r.CompileModule(testCtx, binary)
 			require.NoError(t, err)
 
 			importing, err = r.InstantiateModule(testCtx, importingCode, moduleConfig)
@@ -622,7 +614,7 @@ func testMultipleInstantiation(t *testing.T, r wazero.Runtime) {
 		}},
 		ExportSection: []*wasm.Export{{Name: "store"}},
 	})
-	compiled, err := r.CompileModule(testCtx, bin, compileConfig)
+	compiled, err := r.CompileModule(testCtx, bin)
 	require.NoError(t, err)
 	defer compiled.Close(testCtx)
 
diff --git a/internal/integration_test/fs/fs_test.go b/internal/integration_test/fs/fs_test.go
index 98d233ef..7f193d79 100644
--- a/internal/integration_test/fs/fs_test.go
+++ b/internal/integration_test/fs/fs_test.go
@@ -158,7 +158,7 @@ func TestReader(t *testing.T) {
 	sys := wazero.NewModuleConfig().WithFS(realFs)
 
 	// Create a module that just delegates to wasi functions.
-	compiled, err := r.CompileModule(testCtx, fsWasm, wazero.NewCompileConfig())
+	compiled, err := r.CompileModule(testCtx, fsWasm)
 	require.NoError(t, err)
 
 	mod, err := r.InstantiateModule(testCtx, compiled, sys)
diff --git a/internal/integration_test/spectest/spectest.go b/internal/integration_test/spectest/spectest.go
index 26877e84..6febf928 100644
--- a/internal/integration_test/spectest/spectest.go
+++ b/internal/integration_test/spectest/spectest.go
@@ -327,7 +327,7 @@ var spectestWasm []byte
 // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast
 // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25
 func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, enabledFeatures api.CoreFeatures) {
-	mod, err := binaryformat.DecodeModule(spectestWasm, api.CoreFeaturesV2, wasm.MemorySizer)
+	mod, err := binaryformat.DecodeModule(spectestWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false)
 	require.NoError(t, err)
 
 	// (global (export "global_i32") i32 (i32.const 666))
@@ -422,7 +422,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 					case "module":
 						buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
 						require.NoError(t, err, msg)
-						mod, err := binaryformat.DecodeModule(buf, enabledFeatures, wasm.MemorySizer)
+						mod, err := binaryformat.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages, false)
 						require.NoError(t, err, msg)
 						require.NoError(t, mod.Validate(enabledFeatures))
 						mod.AssignModuleID(buf)
@@ -563,7 +563,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 							//
 							// In practice, such a module instance can be used for invoking functions without any issue. In addition, we have to
 							// retain functions after the expected "instantiation" failure, so in wazero we choose to not raise error in that case.
-							mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemorySizer)
+							mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false)
 							require.NoError(t, err, msg)
 
 							err = mod.Validate(s.EnabledFeatures)
@@ -592,7 +592,7 @@ func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(
 }
 
 func requireInstantiationError(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, buf []byte, msg string) {
-	mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemorySizer)
+	mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false)
 	if err != nil {
 		return
 	}
@@ -860,7 +860,7 @@ func TestBinaryEncoder(t *testing.T, testDataFS embed.FS, enabledFeatures api.Co
 
 						buf = requireStripCustomSections(t, buf)
 
-						mod, err := binaryformat.DecodeModule(buf, enabledFeatures, wasm.MemorySizer)
+						mod, err := binaryformat.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages, false)
 						require.NoError(t, err)
 
 						encodedBuf := binaryformat.EncodeModule(mod)
diff --git a/internal/integration_test/vs/runtime.go b/internal/integration_test/vs/runtime.go
index 31ac2c37..124b446b 100644
--- a/internal/integration_test/vs/runtime.go
+++ b/internal/integration_test/vs/runtime.go
@@ -108,7 +108,7 @@ func (r *wazeroRuntime) Compile(ctx context.Context, cfg *RuntimeConfig) (err er
 			return err
 		}
 	}
-	r.compiled, err = r.runtime.CompileModule(ctx, cfg.ModuleWasm, wazero.NewCompileConfig())
+	r.compiled, err = r.runtime.CompileModule(ctx, cfg.ModuleWasm)
 	return
 }
 
diff --git a/internal/wasm/binary/decoder_test.go b/internal/wasm/binary/decoder_test.go
index 1b990e62..d0c328b4 100644
--- a/internal/wasm/binary/decoder_test.go
+++ b/internal/wasm/binary/decoder_test.go
@@ -81,7 +81,7 @@ func TestDecodeModule(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			m, e := DecodeModule(EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemorySizer)
+			m, e := DecodeModule(EncodeModule(tc.input), api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
 			require.NoError(t, e)
 			require.Equal(t, tc.input, m)
 		})
@@ -92,7 +92,7 @@ func TestDecodeModule(t *testing.T) {
 			wasm.SectionIDCustom, 0xf, // 15 bytes in this section
 			0x04, 'm', 'e', 'm', 'e',
 			1, 2, 3, 4, 5, 6, 7, 8, 9, 0)
-		m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemorySizer)
+		m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
 		require.NoError(t, e)
 		require.Equal(t, &wasm.Module{}, m)
 	})
@@ -107,14 +107,14 @@ func TestDecodeModule(t *testing.T) {
 			subsectionIDModuleName, 0x07, // 7 bytes in this subsection
 			0x06, // the Module name simple is 6 bytes long
 			's', 'i', 'm', 'p', 'l', 'e')
-		m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemorySizer)
+		m, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
 		require.NoError(t, e)
 		require.Equal(t, &wasm.Module{NameSection: &wasm.NameSection{ModuleName: "simple"}}, m)
 	})
 	t.Run("data count section disabled", func(t *testing.T) {
 		input := append(append(Magic, version...),
 			wasm.SectionIDDataCount, 1, 0)
-		_, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemorySizer)
+		_, e := DecodeModule(input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
 		require.EqualError(t, e, `data count section not supported as feature "bulk-memory-operations" is disabled`)
 	})
 }
@@ -164,7 +164,7 @@ func TestDecodeModule_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			_, e := DecodeModule(tc.input, api.CoreFeaturesV1, wasm.MemorySizer)
+			_, e := DecodeModule(tc.input, api.CoreFeaturesV1, wasm.MemoryLimitPages, false)
 			require.EqualError(t, e, tc.expectedErr)
 		})
 	}
diff --git a/internal/wasm/binary/memory_test.go b/internal/wasm/binary/memory_test.go
index 23e44fdc..c5fb3fab 100644
--- a/internal/wasm/binary/memory_test.go
+++ b/internal/wasm/binary/memory_test.go
@@ -9,6 +9,80 @@ import (
 	"github.com/tetratelabs/wazero/internal/wasm"
 )
 
+func Test_newMemorySizer(t *testing.T) {
+	zero := uint32(0)
+	one := uint32(1)
+	limit := wasm.MemoryLimitPages
+
+	tests := []struct {
+		name                                       string
+		memoryCapacityFromMax                      bool
+		min                                        uint32
+		max                                        *uint32
+		expectedMin, expectedCapacity, expectedMax uint32
+	}{
+		{
+			name:             "min 0",
+			min:              zero,
+			max:              &limit,
+			expectedMin:      zero,
+			expectedCapacity: zero,
+			expectedMax:      limit,
+		},
+		{
+			name:             "min 0 defaults max to limit",
+			min:              zero,
+			expectedMin:      zero,
+			expectedCapacity: zero,
+			expectedMax:      limit,
+		},
+		{
+			name:             "min 0, max 0",
+			min:              zero,
+			max:              &zero,
+			expectedMin:      zero,
+			expectedCapacity: zero,
+			expectedMax:      zero,
+		},
+		{
+			name:             "min 0, max 1",
+			min:              zero,
+			max:              &one,
+			expectedMin:      zero,
+			expectedCapacity: zero,
+			expectedMax:      one,
+		},
+		{
+			name:                  "min 0, max 1 memoryCapacityFromMax",
+			memoryCapacityFromMax: true,
+			min:                   zero,
+			max:                   &one,
+			expectedMin:           zero,
+			expectedCapacity:      one,
+			expectedMax:           one,
+		},
+		{
+			name:             "min=max",
+			min:              one,
+			max:              &one,
+			expectedMin:      one,
+			expectedCapacity: one,
+			expectedMax:      one,
+		},
+	}
+
+	for _, tt := range tests {
+		tc := tt
+		t.Run(tc.name, func(t *testing.T) {
+			sizer := newMemorySizer(limit, tc.memoryCapacityFromMax)
+			min, capacity, max := sizer(tc.min, tc.max)
+			require.Equal(t, tc.expectedMin, min)
+			require.Equal(t, tc.expectedCapacity, capacity)
+			require.Equal(t, tc.expectedMax, max)
+		})
+	}
+}
+
 func TestMemoryType(t *testing.T) {
 	zero := uint32(0)
 	max := wasm.MemoryLimitPages
@@ -20,12 +94,12 @@ func TestMemoryType(t *testing.T) {
 	}{
 		{
 			name:     "min 0",
-			input:    &wasm.Memory{Max: wasm.MemoryLimitPages, IsMaxEncoded: true},
+			input:    &wasm.Memory{Max: max, IsMaxEncoded: true},
 			expected: []byte{0x1, 0, 0x80, 0x80, 0x4},
 		},
 		{
 			name:     "min 0 default max",
-			input:    &wasm.Memory{Max: wasm.MemoryLimitPages},
+			input:    &wasm.Memory{Max: max},
 			expected: []byte{0x0, 0},
 		},
 		{
@@ -59,7 +133,7 @@ func TestMemoryType(t *testing.T) {
 		})
 
 		t.Run(fmt.Sprintf("decode %s", tc.name), func(t *testing.T) {
-			binary, err := decodeMemory(bytes.NewReader(b), wasm.MemorySizer)
+			binary, err := decodeMemory(bytes.NewReader(b), newMemorySizer(max, false), max)
 			require.NoError(t, err)
 			require.Equal(t, binary, tc.input)
 		})
@@ -67,6 +141,8 @@ func TestMemoryType(t *testing.T) {
 }
 
 func TestDecodeMemoryType_Errors(t *testing.T) {
+	max := wasm.MemoryLimitPages
+
 	tests := []struct {
 		name        string
 		input       []byte
@@ -93,7 +169,7 @@ func TestDecodeMemoryType_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			_, err := decodeMemory(bytes.NewReader(tc.input), wasm.MemorySizer)
+			_, err := decodeMemory(bytes.NewReader(tc.input), newMemorySizer(max, false), max)
 			require.EqualError(t, err, tc.expectedErr)
 		})
 	}
diff --git a/internal/wasm/binary/section_test.go b/internal/wasm/binary/section_test.go
index 2da3e5a4..c2b1bc75 100644
--- a/internal/wasm/binary/section_test.go
+++ b/internal/wasm/binary/section_test.go
@@ -81,6 +81,8 @@ func TestTableSection_Errors(t *testing.T) {
 }
 
 func TestMemorySection(t *testing.T) {
+	max := wasm.MemoryLimitPages
+
 	three := uint32(3)
 	tests := []struct {
 		name     string
@@ -101,7 +103,7 @@ func TestMemorySection(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			memories, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemorySizer)
+			memories, err := decodeMemorySection(bytes.NewReader(tc.input), newMemorySizer(max, false), max)
 			require.NoError(t, err)
 			require.Equal(t, tc.expected, memories)
 		})
@@ -109,6 +111,8 @@ func TestMemorySection(t *testing.T) {
 }
 
 func TestMemorySection_Errors(t *testing.T) {
+	max := wasm.MemoryLimitPages
+
 	tests := []struct {
 		name        string
 		input       []byte
@@ -129,7 +133,7 @@ func TestMemorySection_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			_, err := decodeMemorySection(bytes.NewReader(tc.input), wasm.MemorySizer)
+			_, err := decodeMemorySection(bytes.NewReader(tc.input), newMemorySizer(max, false), max)
 			require.EqualError(t, err, tc.expectedErr)
 		})
 	}
diff --git a/internal/wasm/memory_test.go b/internal/wasm/memory_test.go
index 816bae5e..c40bff78 100644
--- a/internal/wasm/memory_test.go
+++ b/internal/wasm/memory_test.go
@@ -14,21 +14,6 @@ func TestMemoryPageConsts(t *testing.T) {
 	require.Equal(t, MemoryLimitPages, uint32(1<<16))
 }
 
-func TestMemoryPages(t *testing.T) {
-	t.Run("cap=min, nil max", func(t *testing.T) {
-		min, capacity, max := MemorySizer(1, nil)
-		require.Equal(t, uint32(1), min)
-		require.Equal(t, uint32(1), capacity)
-		require.Equal(t, MemoryLimitPages, max)
-	})
-	t.Run("cap=min, max", func(t *testing.T) {
-		min, capacity, max := MemorySizer(1, uint32Ptr(2))
-		require.Equal(t, uint32(1), min)
-		require.Equal(t, uint32(1), capacity)
-		require.Equal(t, uint32(2), max)
-	})
-}
-
 func TestMemoryPagesToBytesNum(t *testing.T) {
 	for _, numPage := range []uint32{0, 1, 5, 10} {
 		require.Equal(t, uint64(numPage*MemoryPageSize), MemoryPagesToBytesNum(numPage))
diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go
index c75e5756..3f6224d2 100644
--- a/internal/wasm/module_test.go
+++ b/internal/wasm/module_test.go
@@ -109,7 +109,7 @@ func TestMemory_Validate(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			err := tc.mem.Validate()
+			err := tc.mem.Validate(MemoryLimitPages)
 			if tc.expectedErr == "" {
 				require.NoError(t, err)
 			} else {
diff --git a/runtime_test.go b/runtime_test.go
index 426df376..a795281d 100644
--- a/runtime_test.go
+++ b/runtime_test.go
@@ -63,7 +63,7 @@ func TestRuntime_CompileModule(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			m, err := r.CompileModule(testCtx, tc.wasm, NewCompileConfig())
+			m, err := r.CompileModule(testCtx, tc.wasm)
 			require.NoError(t, err)
 			code := m.(*compiledModule)
 			if tc.expectedName != "" {
@@ -72,29 +72,11 @@ func TestRuntime_CompileModule(t *testing.T) {
 			require.Equal(t, r.(*runtime).store.Engine, code.compiledEngine)
 		})
 	}
-
-	t.Run("WithMemorySizer", func(t *testing.T) {
-		testWasm := binaryformat.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 1}})
-
-		m, err := r.CompileModule(testCtx, testWasm, NewCompileConfig().
-			WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
-				return 1, 2, 3
-			}))
-		require.NoError(t, err)
-		code := m.(*compiledModule)
-
-		require.Equal(t, &wasm.Memory{
-			Min: 1,
-			Cap: 2,
-			Max: 3,
-		}, code.module.MemorySection)
-	})
 }
 
 func TestRuntime_CompileModule_Errors(t *testing.T) {
 	tests := []struct {
 		name        string
-		config      CompileConfig
 		wasm        []byte
 		expectedErr string
 	}{
@@ -107,29 +89,6 @@ func TestRuntime_CompileModule_Errors(t *testing.T) {
 			wasm:        append(binaryformat.Magic, []byte("yolo")...),
 			expectedErr: "invalid version header",
 		},
-		{
-			name: "memory cap < min", // only one test to avoid duplicating tests in module_test.go
-			config: NewCompileConfig().WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
-				return 3, 1, 3
-			}),
-			wasm: binaryformat.EncodeModule(&wasm.Module{
-				MemorySection: &wasm.Memory{Min: 3},
-			}),
-			expectedErr: "section memory: capacity 1 pages (64 Ki) less than minimum 3 pages (192 Ki)",
-		},
-		{
-			name: "memory cap < min exported", // only one test to avoid duplicating tests in module_test.go
-			config: NewCompileConfig().WithMemorySizer(func(minPages uint32, maxPages *uint32) (min, capacity, max uint32) {
-				return 3, 2, 3
-			}),
-			wasm: binaryformat.EncodeModule(&wasm.Module{
-				MemorySection: &wasm.Memory{},
-				ExportSection: []*wasm.Export{
-					{Name: "memory", Type: api.ExternTypeMemory},
-				},
-			}),
-			expectedErr: "section memory: capacity 2 pages (128 Ki) less than minimum 3 pages (192 Ki)",
-		},
 		{
 			name:        "memory has too many pages",
 			wasm:        binaryformat.EncodeModule(&wasm.Module{MemorySection: &wasm.Memory{Min: 2, Cap: 2, Max: 70000, IsMaxEncoded: true}}),
@@ -144,11 +103,7 @@ func TestRuntime_CompileModule_Errors(t *testing.T) {
 		tc := tt
 
 		t.Run(tc.name, func(t *testing.T) {
-			config := tc.config
-			if config == nil {
-				config = NewCompileConfig()
-			}
-			_, err := r.CompileModule(testCtx, tc.wasm, config)
+			_, err := r.CompileModule(testCtx, tc.wasm)
 			require.EqualError(t, err, tc.expectedErr)
 		})
 	}
@@ -315,7 +270,7 @@ func TestRuntime_InstantiateModule_UsesContext(t *testing.T) {
 		StartSection: &one,
 	})
 
-	code, err := r.CompileModule(testCtx, binary, NewCompileConfig())
+	code, err := r.CompileModule(testCtx, binary)
 	require.NoError(t, err)
 
 	// Instantiate the module, which calls the start function. This will fail if the context wasn't as intended.
@@ -400,7 +355,7 @@ func TestRuntime_InstantiateModule_WithName(t *testing.T) {
 	r := NewRuntime(testCtx)
 	defer r.Close(testCtx)
 
-	base, err := r.CompileModule(testCtx, binaryNamedZero, NewCompileConfig())
+	base, err := r.CompileModule(testCtx, binaryNamedZero)
 	require.NoError(t, err)
 
 	require.Equal(t, "0", base.(*compiledModule).module.NameSection.ModuleName)
@@ -442,7 +397,7 @@ func TestRuntime_InstantiateModule_ExitError(t *testing.T) {
 		StartSection: &one,
 	})
 
-	code, err := r.CompileModule(testCtx, binary, NewCompileConfig())
+	code, err := r.CompileModule(testCtx, binary)
 	require.NoError(t, err)
 
 	// Instantiate the module, which calls the start function.
@@ -479,7 +434,7 @@ func TestRuntime_CloseWithExitCode(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			r := NewRuntime(testCtx)
 
-			code, err := r.CompileModule(testCtx, bin, NewCompileConfig())
+			code, err := r.CompileModule(testCtx, bin)
 			require.NoError(t, err)
 
 			// Instantiate two modules.
@@ -526,7 +481,7 @@ func TestRuntime_Close_ClosesCompiledModules(t *testing.T) {
 	defer r.Close(testCtx)
 
 	// Normally compiled modules are closed when instantiated but this is never instantiated.
-	_, err := r.CompileModule(testCtx, binaryNamedZero, NewCompileConfig())
+	_, err := r.CompileModule(testCtx, binaryNamedZero)
 	require.NoError(t, err)
 	require.Equal(t, uint32(1), engine.CompiledModuleCount())