Repository: tetratelabs/wazero
Pull request #1895 (2024-01-04 22:29:46)

wazevo: lifts up arm64 label related codes to backend pkg


Patch:
diff --git a/internal/engine/wazevo/backend/compiler_lower.go b/internal/engine/wazevo/backend/compiler_lower.go
index 8f513423..7a99ee54 100644
--- a/internal/engine/wazevo/backend/compiler_lower.go
+++ b/internal/engine/wazevo/backend/compiler_lower.go
@@ -9,9 +9,8 @@ import (
 func (c *compiler) Lower() {
 	c.assignVirtualRegisters()
 	c.mach.InitializeABI(c.ssaBuilder.Signature())
-	c.mach.StartLoweringFunction(c.ssaBuilder.BlockIDMax())
+	c.mach.ExecutableContext().StartLoweringFunction(c.ssaBuilder.BlockIDMax())
 	c.lowerBlocks()
-	c.mach.EndLoweringFunction()
 }
 
 // lowerBlocks lowers each block in the ssa.Builder.
@@ -20,11 +19,13 @@ func (c *compiler) lowerBlocks() {
 	for blk := builder.BlockIteratorReversePostOrderBegin(); blk != nil; blk = builder.BlockIteratorReversePostOrderNext() {
 		c.lowerBlock(blk)
 	}
+
+	ectx := c.mach.ExecutableContext()
 	// After lowering all blocks, we need to link adjacent blocks to layout one single instruction list.
 	var prev ssa.BasicBlock
 	for next := builder.BlockIteratorReversePostOrderBegin(); next != nil; next = builder.BlockIteratorReversePostOrderNext() {
 		if prev != nil {
-			c.mach.LinkAdjacentBlocks(prev, next)
+			ectx.LinkAdjacentBlocks(prev, next)
 		}
 		prev = next
 	}
@@ -58,6 +59,7 @@ func (c *compiler) lowerBlock(blk ssa.BasicBlock) {
 	}
 
 	// Now start lowering the non-branching instructions.
+	ectx := mach.ExecutableContext()
 	for ; cur != nil; cur = cur.Prev() {
 		c.setCurrentGroupID(cur.GroupID())
 		if cur.Lowered() {
@@ -71,7 +73,7 @@ func (c *compiler) lowerBlock(blk ssa.BasicBlock) {
 		default:
 			mach.LowerInstr(cur)
 		}
-		mach.FlushPendingInstructions()
+		ectx.FlushPendingInstructions()
 	}
 
 	// Finally, if this is the entry block, we have to insert copies of arguments from the real location to the VReg.
@@ -79,7 +81,7 @@ func (c *compiler) lowerBlock(blk ssa.BasicBlock) {
 		c.lowerFunctionArguments(blk)
 	}
 
-	mach.EndBlock()
+	ectx.EndBlock()
 }
 
 // lowerBranches is called right after StartBlock and before any LowerInstr call if
@@ -88,13 +90,15 @@ func (c *compiler) lowerBlock(blk ssa.BasicBlock) {
 //
 // See ssa.Instruction IsBranching, and the comment on ssa.BasicBlock.
 func (c *compiler) lowerBranches(br0, br1 *ssa.Instruction) {
+	ectx := c.mach.ExecutableContext()
+
 	c.setCurrentGroupID(br0.GroupID())
 	c.mach.LowerSingleBranch(br0)
-	c.mach.FlushPendingInstructions()
+	ectx.FlushPendingInstructions()
 	if br1 != nil {
 		c.setCurrentGroupID(br1.GroupID())
 		c.mach.LowerConditionalBranch(br1)
-		c.mach.FlushPendingInstructions()
+		ectx.FlushPendingInstructions()
 	}
 
 	if br0.Opcode() == ssa.OpcodeJump {
@@ -109,10 +113,12 @@ func (c *compiler) lowerBranches(br0, br1 *ssa.Instruction) {
 			c.lowerBlockArguments(args, target)
 		}
 	}
-	c.mach.FlushPendingInstructions()
+	ectx.FlushPendingInstructions()
 }
 
 func (c *compiler) lowerFunctionArguments(entry ssa.BasicBlock) {
+	ectx := c.mach.ExecutableContext()
+
 	c.tmpVals = c.tmpVals[:0]
 	for i := 0; i < entry.Params(); i++ {
 		p := entry.Param(i)
@@ -124,7 +130,7 @@ func (c *compiler) lowerFunctionArguments(entry ssa.BasicBlock) {
 		}
 	}
 	c.mach.ABI().CalleeGenFunctionArgsToVRegs(c.tmpVals)
-	c.mach.FlushPendingInstructions()
+	ectx.FlushPendingInstructions()
 }
 
 func (c *compiler) lowerFunctionReturns(returns []ssa.Value) {
diff --git a/internal/engine/wazevo/backend/executable_context.go b/internal/engine/wazevo/backend/executable_context.go
new file mode 100644
index 00000000..a89488ea
--- /dev/null
+++ b/internal/engine/wazevo/backend/executable_context.go
@@ -0,0 +1,214 @@
+package backend
+
+import (
+	"fmt"
+	"math"
+
+	"github.com/tetratelabs/wazero/internal/engine/wazevo/ssa"
+	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
+)
+
+type ExecutableContext interface {
+	// StartLoweringFunction is called when the lowering of the given function is started.
+	// maximumBlockID is the maximum value of ssa.BasicBlockID existing in the function.
+	StartLoweringFunction(maximumBlockID ssa.BasicBlockID)
+
+	// LinkAdjacentBlocks is called after finished lowering all blocks in order to create one single instruction list.
+	LinkAdjacentBlocks(prev, next ssa.BasicBlock)
+
+	// EndBlock is called when the compilation of the current block is finished.
+	EndBlock()
+
+	// FlushPendingInstructions flushes the pending instructions to the buffer.
+	// This will be called after the lowering of each SSA Instruction.
+	FlushPendingInstructions()
+}
+
+type ExecutableContextT[Instr any] struct {
+	CurrentSSABlk ssa.BasicBlock
+
+	// InstrPool is the InstructionPool of instructions.
+	InstructionPool wazevoapi.Pool[Instr]
+	asNop           func(*Instr)
+	setNext         func(*Instr, *Instr)
+	setPrev         func(*Instr, *Instr)
+
+	// RootInstr is the root instruction of the executable.
+	RootInstr         *Instr
+	labelPositionPool wazevoapi.Pool[LabelPosition[Instr]]
+	NextLabel         Label
+	// LabelPositions maps a label to the instructions of the region which the label represents.
+	LabelPositions     map[Label]*LabelPosition[Instr]
+	OrderedBlockLabels []*LabelPosition[Instr]
+
+	// PerBlockHead and PerBlockEnd are the head and tail of the instruction list per currently-compiled ssa.BasicBlock.
+	PerBlockHead, PerBlockEnd *Instr
+	// PendingInstructions are the instructions which are not yet emitted into the instruction list.
+	PendingInstructions []*Instr
+
+	// SsaBlockIDToLabels maps an SSA block ID to the label.
+	SsaBlockIDToLabels []Label
+}
+
+func NewExecutableContextT[Instr any](
+	resetInstruction func(*Instr),
+	setNext func(*Instr, *Instr),
+	setPrev func(*Instr, *Instr),
+	asNop func(*Instr),
+) *ExecutableContextT[Instr] {
+	return &ExecutableContextT[Instr]{
+		InstructionPool:   wazevoapi.NewPool[Instr](resetInstruction),
+		asNop:             asNop,
+		setNext:           setNext,
+		setPrev:           setPrev,
+		labelPositionPool: wazevoapi.NewPool[LabelPosition[Instr]](resetLabelPosition[Instr]),
+		LabelPositions:    make(map[Label]*LabelPosition[Instr]),
+		NextLabel:         LabelInvalid,
+	}
+}
+
+func resetLabelPosition[T any](l *LabelPosition[T]) {
+	*l = LabelPosition[T]{}
+}
+
+// StartLoweringFunction implements ExecutableContext.
+func (e *ExecutableContextT[Instr]) StartLoweringFunction(max ssa.BasicBlockID) {
+	imax := int(max)
+	if len(e.SsaBlockIDToLabels) <= imax {
+		// Eagerly allocate labels for the blocks since the underlying slice will be used for the next iteration.
+		e.SsaBlockIDToLabels = append(e.SsaBlockIDToLabels, make([]Label, imax+1)...)
+	}
+}
+
+func (e *ExecutableContextT[Instr]) StartBlock(blk ssa.BasicBlock) (l Label, pos *LabelPosition[Instr]) {
+	e.CurrentSSABlk = blk
+
+	l = e.SsaBlockIDToLabels[e.CurrentSSABlk.ID()]
+	if l == LabelInvalid {
+		l = e.AllocateLabel()
+		e.SsaBlockIDToLabels[blk.ID()] = l
+	}
+
+	end := e.allocateNop0()
+	e.PerBlockHead, e.PerBlockEnd = end, end
+
+	labelPos, ok := e.LabelPositions[l]
+	if !ok {
+		labelPos = e.AllocateLabelPosition(l)
+		e.LabelPositions[l] = labelPos
+	}
+	e.OrderedBlockLabels = append(e.OrderedBlockLabels, labelPos)
+	labelPos.Begin, labelPos.End = end, end
+	return l, labelPos
+}
+
+// EndBlock implements ExecutableContext.
+func (e *ExecutableContextT[T]) EndBlock() {
+	// Insert nop0 as the head of the block for convenience to simplify the logic of inserting instructions.
+	e.insertAtPerBlockHead(e.allocateNop0())
+
+	l := e.SsaBlockIDToLabels[e.CurrentSSABlk.ID()]
+	e.LabelPositions[l].Begin = e.PerBlockHead
+
+	if e.CurrentSSABlk.EntryBlock() {
+		e.RootInstr = e.PerBlockHead
+	}
+}
+
+func (e *ExecutableContextT[T]) insertAtPerBlockHead(i *T) {
+	if e.PerBlockHead == nil {
+		e.PerBlockHead = i
+		e.PerBlockEnd = i
+		return
+	}
+	e.setNext(i, e.PerBlockHead)
+	e.setPrev(e.PerBlockHead, i)
+	e.PerBlockHead = i
+}
+
+// FlushPendingInstructions implements ExecutableContext.
+func (e *ExecutableContextT[T]) FlushPendingInstructions() {
+	l := len(e.PendingInstructions)
+	if l == 0 {
+		return
+	}
+	for i := l - 1; i >= 0; i-- { // reverse because we lower instructions in reverse order.
+		e.insertAtPerBlockHead(e.PendingInstructions[i])
+	}
+	e.PendingInstructions = e.PendingInstructions[:0]
+}
+
+func (e *ExecutableContextT[T]) Reset() {
+	e.labelPositionPool.Reset()
+	e.InstructionPool.Reset()
+	for l := Label(0); l <= e.NextLabel; l++ {
+		delete(e.LabelPositions, l)
+	}
+	e.PendingInstructions = e.PendingInstructions[:0]
+	e.OrderedBlockLabels = e.OrderedBlockLabels[:0]
+	e.RootInstr = nil
+	e.SsaBlockIDToLabels = e.SsaBlockIDToLabels[:0]
+	e.PerBlockHead, e.PerBlockEnd = nil, nil
+	e.NextLabel = LabelInvalid
+}
+
+// AllocateLabel allocates an unused label.
+func (e *ExecutableContextT[T]) AllocateLabel() Label {
+	e.NextLabel++
+	return e.NextLabel
+}
+
+func (e *ExecutableContextT[T]) AllocateLabelPosition(la Label) *LabelPosition[T] {
+	l := e.labelPositionPool.Allocate()
+	l.L = la
+	return l
+}
+
+func (e *ExecutableContextT[T]) GetOrAllocateSSABlockLabel(blk ssa.BasicBlock) Label {
+	if blk.ReturnBlock() {
+		return LabelReturn
+	}
+	l := e.SsaBlockIDToLabels[blk.ID()]
+	if l == LabelInvalid {
+		l = e.AllocateLabel()
+		e.SsaBlockIDToLabels[blk.ID()] = l
+	}
+	return l
+}
+
+func (e *ExecutableContextT[T]) allocateNop0() *T {
+	i := e.InstructionPool.Allocate()
+	e.asNop(i)
+	return i
+}
+
+// LinkAdjacentBlocks implements backend.Machine.
+func (e *ExecutableContextT[T]) LinkAdjacentBlocks(prev, next ssa.BasicBlock) {
+	prevLabelPos := e.LabelPositions[e.GetOrAllocateSSABlockLabel(prev)]
+	nextLabelPos := e.LabelPositions[e.GetOrAllocateSSABlockLabel(next)]
+	e.setNext(prevLabelPos.End, nextLabelPos.Begin)
+}
+
+// LabelPosition represents the regions of the generated code which the label represents.
+type LabelPosition[Instr any] struct {
+	L            Label
+	Begin, End   *Instr
+	BinarySize   int64
+	BinaryOffset int64
+}
+
+// Label represents a position in the generated code which is either
+// a real instruction or the constant InstructionPool (e.g. jump tables).
+//
+// This is exactly the same as the traditional "label" in assembly code.
+type Label uint32
+
+const (
+	LabelInvalid Label = 0
+	LabelReturn  Label = math.MaxUint32
+)
+
+// String implements backend.Machine.
+func (l Label) String() string {
+	return fmt.Sprintf("L%d", l)
+}
diff --git a/internal/engine/wazevo/backend/isa/amd64/machine.go b/internal/engine/wazevo/backend/isa/amd64/machine.go
index ce18cd6b..4326bcf2 100644
--- a/internal/engine/wazevo/backend/isa/amd64/machine.go
+++ b/internal/engine/wazevo/backend/isa/amd64/machine.go
@@ -20,6 +20,12 @@ type machine struct {
 	stackBoundsCheckDisabled bool
 }
 
+// ExecutableContext implements backend.Machine.
+func (m *machine) ExecutableContext() backend.ExecutableContext {
+	// TODO implement me
+	panic("implement me")
+}
+
 // DisableStackCheck implements backend.Machine.
 func (m *machine) DisableStackCheck() {
 	m.stackBoundsCheckDisabled = true
@@ -91,12 +97,6 @@ func (m *machine) LinkAdjacentBlocks(prev, next ssa.BasicBlock) {
 	panic("implement me")
 }
 
-// EndLoweringFunction implements backend.Machine.
-func (m *machine) EndLoweringFunction() {
-	// TODO implement me
-	panic("implement me")
-}
-
 // Reset implements backend.Machine.
 func (m *machine) Reset() {
 	// TODO implement me
diff --git a/internal/engine/wazevo/backend/isa/arm64/abi.go b/internal/engine/wazevo/backend/isa/arm64/abi.go
index f75aa55b..378b754a 100644
--- a/internal/engine/wazevo/backend/isa/arm64/abi.go
+++ b/internal/engine/wazevo/backend/isa/arm64/abi.go
@@ -309,9 +309,10 @@ func (a *abiImpl) callerGenFunctionReturnVReg(retIndex int, reg regalloc.VReg, s
 }
 
 func (m *machine) resolveAddressModeForOffsetAndInsert(cur *instruction, offset int64, dstBits byte, rn regalloc.VReg, allowTmpRegUse bool) (*instruction, addressMode) {
-	m.pendingInstructions = m.pendingInstructions[:0]
+	exct := m.executableContext
+	exct.PendingInstructions = exct.PendingInstructions[:0]
 	mode := m.resolveAddressModeForOffset(offset, dstBits, rn, allowTmpRegUse)
-	for _, instr := range m.pendingInstructions {
+	for _, instr := range exct.PendingInstructions {
 		cur = linkInstr(cur, instr)
 	}
 	return cur, mode
diff --git a/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go b/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go
index e09abea5..8bb7a2b4 100644
--- a/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go
+++ b/internal/engine/wazevo/backend/isa/arm64/abi_go_call.go
@@ -14,6 +14,7 @@ var calleeSavedRegistersSorted = []regalloc.VReg{
 
 // CompileGoFunctionTrampoline implements backend.Machine.
 func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *ssa.Signature, needModuleContextPtr bool) []byte {
+	exct := m.executableContext
 	argBegin := 1 // Skips exec context by default.
 	if needModuleContextPtr {
 		argBegin++
@@ -25,7 +26,7 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
 
 	cur := m.allocateInstr()
 	cur.asNop0()
-	m.rootInstr = cur
+	exct.RootInstr = cur
 
 	// Execution context is always the first argument.
 	execCtrPtr := x0VReg
@@ -242,7 +243,7 @@ func (m *machine) CompileGoFunctionTrampoline(exitCode wazevoapi.ExitCode, sig *
 	ret.asRet(nil)
 	linkInstr(cur, ret)
 
-	m.encode(m.rootInstr)
+	m.encode(m.executableContext.RootInstr)
 	return m.compiler.Buf()
 }
 
@@ -298,18 +299,20 @@ func (m *machine) restoreRegistersInExecutionContext(cur *instruction, regs []re
 }
 
 func (m *machine) lowerConstantI64AndInsert(cur *instruction, dst regalloc.VReg, v int64) *instruction {
-	m.pendingInstructions = m.pendingInstructions[:0]
+	exct := m.executableContext
+	exct.PendingInstructions = exct.PendingInstructions[:0]
 	m.lowerConstantI64(dst, v)
-	for _, instr := range m.pendingInstructions {
+	for _, instr := range exct.PendingInstructions {
 		cur = linkInstr(cur, instr)
 	}
 	return cur
 }
 
 func (m *machine) lowerConstantI32AndInsert(cur *instruction, dst regalloc.VReg, v int32) *instruction {
-	m.pendingInstructions = m.pendingInstructions[:0]
+	exct := m.executableContext
+	exct.PendingInstructions = exct.PendingInstructions[:0]
 	m.lowerConstantI32(dst, v)
-	for _, instr := range m.pendingInstructions {
+	for _, instr := range exct.PendingInstructions {
 		cur = linkInstr(cur, instr)
 	}
 	return cur
diff --git a/internal/engine/wazevo/backend/isa/arm64/instr.go b/internal/engine/wazevo/backend/isa/arm64/instr.go
index 3aa50e4c..7bd9cfb3 100644
--- a/internal/engine/wazevo/backend/isa/arm64/instr.go
+++ b/internal/engine/wazevo/backend/isa/arm64/instr.go
@@ -36,6 +36,18 @@ type (
 	instructionKind int
 )
 
+func asNop0(i *instruction) {
+	i.kind = nop0
+}
+
+func setNext(i, next *instruction) {
+	i.next = next
+}
+
+func setPrev(i, prev *instruction) {
+	i.prev = prev
+}
+
 // IsCall implements regalloc.Instr IsCall.
 func (i *instruction) IsCall() bool {
 	return i.kind == call
@@ -625,7 +637,7 @@ func (i *instruction) asFpuCSel(rd, rn, rm operand, c condFlag, _64bit bool) {
 }
 
 func (i *instruction) asBr(target label) {
-	if target == returnLabel {
+	if target == labelReturn {
 		panic("BUG: call site should special case for returnLabel")
 	}
 	i.kind = br
@@ -1396,7 +1408,7 @@ func (i *instruction) String() (str string) {
 			}
 		case condKindCondFlagSet:
 			if offset := i.condBrOffset(); offset != 0 {
-				if target == invalidLabel {
+				if target == labelInvalid {
 					str = fmt.Sprintf("b.%s #%#x", c.flag(), offset)
 				} else {
 					str = fmt.Sprintf("b.%s #%#x, (%s)", c.flag(), offset, target.String())
diff --git a/internal/engine/wazevo/backend/isa/arm64/instr_encoding.go b/internal/engine/wazevo/backend/isa/arm64/instr_encoding.go
index 80a8f1ef..52ee3ab5 100644
--- a/internal/engine/wazevo/backend/isa/arm64/instr_encoding.go
+++ b/internal/engine/wazevo/backend/isa/arm64/instr_encoding.go
@@ -10,7 +10,7 @@ import (
 
 // Encode implements backend.Machine Encode.
 func (m *machine) Encode() {
-	m.encode(m.rootInstr)
+	m.encode(m.executableContext.RootInstr)
 }
 
 func (m *machine) encode(root *instruction) {
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_instr.go b/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
index f7250910..706884e8 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
@@ -17,6 +17,7 @@ import (
 
 // LowerSingleBranch implements backend.Machine.
 func (m *machine) LowerSingleBranch(br *ssa.Instruction) {
+	ectx := m.executableContext
 	switch br.Opcode() {
 	case ssa.OpcodeJump:
 		_, _, targetBlk := br.BranchData()
@@ -24,8 +25,8 @@ func (m *machine) LowerSingleBranch(br *ssa.Instruction) {
 			return
 		}
 		b := m.allocateInstr()
-		target := m.getOrAllocateSSABlockLabel(targetBlk)
-		if target == returnLabel {
+		target := ectx.GetOrAllocateSSABlockLabel(targetBlk)
+		if target == labelReturn {
 			b.asRet(m.currentABI)
 		} else {
 			b.asBr(target)
@@ -63,7 +64,7 @@ func (m *machine) lowerBrTable(i *ssa.Instruction) {
 	// TODO: reuse the slice!
 	labels := make([]uint32, len(targets))
 	for j, target := range targets {
-		labels[j] = uint32(m.getOrAllocateSSABlockLabel(target))
+		labels[j] = uint32(m.executableContext.GetOrAllocateSSABlockLabel(target))
 	}
 
 	brSequence.asBrTableSequence(adjustedIndex, labels)
@@ -72,16 +73,17 @@ func (m *machine) lowerBrTable(i *ssa.Instruction) {
 
 // LowerConditionalBranch implements backend.Machine.
 func (m *machine) LowerConditionalBranch(b *ssa.Instruction) {
+	exctx := m.executableContext
 	cval, args, targetBlk := b.BranchData()
 	if len(args) > 0 {
 		panic(fmt.Sprintf(
 			"conditional branch shouldn't have args; likely a bug in critical edge splitting: from %s to %s",
-			m.currentSSABlk,
+			exctx.CurrentSSABlk,
 			targetBlk,
 		))
 	}
 
-	target := m.getOrAllocateSSABlockLabel(targetBlk)
+	target := exctx.GetOrAllocateSSABlockLabel(targetBlk)
 	cvalDef := m.compiler.ValueDefinition(cval)
 
 	switch {
@@ -710,7 +712,7 @@ func (m *machine) LowerInstr(instr *ssa.Instruction) {
 	default:
 		panic("TODO: lowering " + op.String())
 	}
-	m.FlushPendingInstructions()
+	m.executableContext.FlushPendingInstructions()
 }
 
 func (m *machine) lowerShuffle(rd, rn, rm operand, lane1, lane2 uint64) {
diff --git a/internal/engine/wazevo/backend/isa/arm64/machine.go b/internal/engine/wazevo/backend/isa/arm64/machine.go
index 821bdfb8..60fdb34d 100644
--- a/internal/engine/wazevo/backend/isa/arm64/machine.go
+++ b/internal/engine/wazevo/backend/isa/arm64/machine.go
@@ -3,7 +3,6 @@ package arm64
 import (
 	"context"
 	"fmt"
-	"math"
 	"strings"
 
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/backend"
@@ -15,27 +14,13 @@ import (
 type (
 	// machine implements backend.Machine.
 	machine struct {
-		compiler      backend.Compiler
-		currentABI    *abiImpl
-		currentSSABlk ssa.BasicBlock
+		compiler          backend.Compiler
+		executableContext *backend.ExecutableContextT[instruction]
+		currentABI        *abiImpl
 		// abis maps ssa.SignatureID to the ABI implementation.
-		abis      []abiImpl
-		instrPool wazevoapi.Pool[instruction]
-		// rootInstr is the root instruction of the currently-compiled function.
-		rootInstr *instruction
-		// perBlockHead and perBlockEnd are the head and tail of the instruction list per currently-compiled ssa.BasicBlock.
-		perBlockHead, perBlockEnd *instruction
-		// pendingInstructions are the instructions which are not yet emitted into the instruction list.
-		pendingInstructions []*instruction
-		regAllocFn          regAllocFunctionImpl
-		nextLabel           label
-
-		// ssaBlockIDToLabels maps an SSA block ID to the label.
-		ssaBlockIDToLabels []label
-		// labelToInstructions maps a label to the instructions of the region which the label represents.
-		labelPositions     map[label]*labelPosition
-		orderedBlockLabels []*labelPosition
-		labelPositionPool  wazevoapi.Pool[labelPosition]
+		abis []abiImpl
+
+		regAllocFn regAllocFunctionImpl
 
 		// addendsWorkQueue is used during address lowering, defined here for reuse.
 		addendsWorkQueue queue[ssa.Value]
@@ -94,20 +79,6 @@ type (
 		ext extendOp
 	}
 
-	// label represents a position in the generated code which is either
-	// a real instruction or the constant pool (e.g. jump tables).
-	//
-	// This is exactly the same as the traditional "label" in assembly code.
-	label uint32
-
-	// labelPosition represents the regions of the generated code which the label represents.
-	labelPosition struct {
-		l            label
-		begin, end   *instruction
-		binarySize   int64
-		binaryOffset int64
-	}
-
 	condBrReloc struct {
 		cbr *instruction
 		// currentLabelPos is the labelPosition within which condBr is defined.
@@ -116,37 +87,39 @@ type (
 		nextLabel label
 		offset    int64
 	}
+
+	labelPosition = backend.LabelPosition[instruction]
+	label         = backend.Label
 )
 
 const (
-	invalidLabel = 0
-	returnLabel  = math.MaxUint32
+	labelReturn  = backend.LabelReturn
+	labelInvalid = backend.LabelInvalid
 )
 
 // NewBackend returns a new backend for arm64.
 func NewBackend() backend.Machine {
 	m := &machine{
-		instrPool:         wazevoapi.NewPool[instruction](resetInstruction),
-		labelPositionPool: wazevoapi.NewPool[labelPosition](resetLabelPosition),
-		labelPositions:    make(map[label]*labelPosition),
 		spillSlots:        make(map[regalloc.VRegID]int64),
-		nextLabel:         invalidLabel,
+		executableContext: newExecutableContext(),
 	}
 	m.regAllocFn.m = m
 	m.regAllocFn.labelToRegAllocBlockIndex = make(map[label]int)
 	return m
 }
 
+func newExecutableContext() *backend.ExecutableContextT[instruction] {
+	return backend.NewExecutableContextT[instruction](resetInstruction, setNext, setPrev, asNop0)
+}
+
+// ExecutableContext implements backend.Machine.
+func (m *machine) ExecutableContext() backend.ExecutableContext {
+	return m.executableContext
+}
+
 // Reset implements backend.Machine.
 func (m *machine) Reset() {
 	m.regAllocStarted = false
-	m.instrPool.Reset()
-	m.labelPositionPool.Reset()
-	m.currentSSABlk = nil
-	for l := label(0); l <= m.nextLabel; l++ {
-		delete(m.labelPositions, l)
-	}
-	m.pendingInstructions = m.pendingInstructions[:0]
 	m.clobberedRegs = m.clobberedRegs[:0]
 	for key := range m.spillSlots {
 		m.clobberedRegs = append(m.clobberedRegs, regalloc.VReg(key))
@@ -155,15 +128,11 @@ func (m *machine) Reset() {
 		delete(m.spillSlots, regalloc.VRegID(key))
 	}
 	m.clobberedRegs = m.clobberedRegs[:0]
-	m.orderedBlockLabels = m.orderedBlockLabels[:0]
 	m.regAllocFn.reset()
 	m.spillSlotSize = 0
 	m.unresolvedAddressModes = m.unresolvedAddressModes[:0]
-	m.rootInstr = nil
-	m.ssaBlockIDToLabels = m.ssaBlockIDToLabels[:0]
-	m.perBlockHead, m.perBlockEnd = nil, nil
 	m.maxRequiredStackSizeForCalls = 0
-	m.nextLabel = invalidLabel
+	m.executableContext.Reset()
 }
 
 // InitializeABI implements backend.Machine InitializeABI.
@@ -181,67 +150,20 @@ func (m *machine) ABI() backend.FunctionABI {
 	return m.currentABI
 }
 
-// allocateLabel allocates an unused label.
-func (m *machine) allocateLabel() label {
-	m.nextLabel++
-	return m.nextLabel
-}
-
 // SetCompiler implements backend.Machine.
 func (m *machine) SetCompiler(ctx backend.Compiler) {
 	m.compiler = ctx
 }
 
-// StartLoweringFunction implements backend.Machine.
-func (m *machine) StartLoweringFunction(max ssa.BasicBlockID) {
-	imax := int(max)
-	if len(m.ssaBlockIDToLabels) <= imax {
-		// Eagerly allocate labels for the blocks since the underlying slice will be used for the next iteration.
-		m.ssaBlockIDToLabels = append(m.ssaBlockIDToLabels, make([]label, imax+1)...)
-	}
-}
-
-// EndLoweringFunction implements backend.Machine.
-func (m *machine) EndLoweringFunction() {}
-
 // StartBlock implements backend.Machine.
 func (m *machine) StartBlock(blk ssa.BasicBlock) {
-	m.currentSSABlk = blk
-
-	l := m.ssaBlockIDToLabels[m.currentSSABlk.ID()]
-	if l == invalidLabel {
-		l = m.allocateLabel()
-		m.ssaBlockIDToLabels[blk.ID()] = l
-	}
-
-	end := m.allocateNop()
-	m.perBlockHead, m.perBlockEnd = end, end
-
-	labelPos, ok := m.labelPositions[l]
-	if !ok {
-		labelPos = m.allocateLabelPosition(l)
-		m.labelPositions[l] = labelPos
-	}
-	m.orderedBlockLabels = append(m.orderedBlockLabels, labelPos)
-	labelPos.begin, labelPos.end = end, end
-	m.regAllocFn.addBlock(blk, l, labelPos)
-}
-
-// EndBlock implements backend.Machine.
-func (m *machine) EndBlock() {
-	// Insert nop0 as the head of the block for convenience to simplify the logic of inserting instructions.
-	m.insertAtPerBlockHead(m.allocateNop())
-
-	l := m.ssaBlockIDToLabels[m.currentSSABlk.ID()]
-	m.labelPositions[l].begin = m.perBlockHead
-
-	if m.currentSSABlk.EntryBlock() {
-		m.rootInstr = m.perBlockHead
-	}
+	l, pos := m.executableContext.StartBlock(blk)
+	m.regAllocFn.addBlock(blk, l, pos)
 }
 
 func (m *machine) insert(i *instruction) {
-	m.pendingInstructions = append(m.pendingInstructions, i)
+	ectx := m.executableContext
+	ectx.PendingInstructions = append(ectx.PendingInstructions, i)
 }
 
 func (m *machine) insertBrTargetLabel() label {
@@ -251,56 +173,19 @@ func (m *machine) insertBrTargetLabel() label {
 }
 
 func (m *machine) allocateBrTarget() (nop *instruction, l label) {
-	l = m.allocateLabel()
+	ectx := m.executableContext
+	l = ectx.AllocateLabel()
 	nop = m.allocateInstr()
 	nop.asNop0WithLabel(l)
-	pos := m.allocateLabelPosition(l)
-	pos.begin, pos.end = nop, nop
-	m.labelPositions[l] = pos
+	pos := ectx.AllocateLabelPosition(l)
+	pos.Begin, pos.End = nop, nop
+	ectx.LabelPositions[l] = pos
 	return
 }
 
-func (m *machine) allocateLabelPosition(la label) *labelPosition {
-	l := m.labelPositionPool.Allocate()
-	l.l = la
-	return l
-}
-
-func resetLabelPosition(l *labelPosition) {
-	*l = labelPosition{}
-}
-
-// FlushPendingInstructions implements backend.Machine.
-func (m *machine) FlushPendingInstructions() {
-	l := len(m.pendingInstructions)
-	if l == 0 {
-		return
-	}
-	for i := l - 1; i >= 0; i-- { // reverse because we lower instructions in reverse order.
-		m.insertAtPerBlockHead(m.pendingInstructions[i])
-	}
-	m.pendingInstructions = m.pendingInstructions[:0]
-}
-
-func (m *machine) insertAtPerBlockHead(i *instruction) {
-	if m.perBlockHead == nil {
-		m.perBlockHead = i
-		m.perBlockEnd = i
-		return
-	}
-	i.next = m.perBlockHead
-	m.perBlockHead.prev = i
-	m.perBlockHead = i
-}
-
-// String implements backend.Machine.
-func (l label) String() string {
-	return fmt.Sprintf("L%d", l)
-}
-
 // allocateInstr allocates an instruction.
 func (m *machine) allocateInstr() *instruction {
-	instr := m.instrPool.Allocate()
+	instr := m.executableContext.InstructionPool.Allocate()
 	if !m.regAllocStarted {
 		instr.addedBeforeRegAlloc = true
 	}
@@ -366,6 +251,7 @@ func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 
 	// Reuse the slice to gather the unresolved conditional branches.
 	cbrs := m.condBrRelocs[:0]
+	ectx := m.executableContext
 
 	var fn string
 	var fnIndex int
@@ -373,7 +259,7 @@ func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 	if wazevoapi.PerfMapEnabled {
 		fn = wazevoapi.GetCurrentFunctionName(ctx)
 		labelToSSABlockID = make(map[label]ssa.BasicBlockID)
-		for i, l := range m.ssaBlockIDToLabels {
+		for i, l := range ectx.SsaBlockIDToLabels {
 			labelToSSABlockID[l] = ssa.BasicBlockID(i)
 		}
 		fnIndex = wazevoapi.GetCurrentFunctionIndex(ctx)
@@ -381,23 +267,23 @@ func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 
 	// Next, in order to determine the offsets of relative jumps, we have to calculate the size of each label.
 	var offset int64
-	for i, pos := range m.orderedBlockLabels {
-		pos.binaryOffset = offset
+	for i, pos := range ectx.OrderedBlockLabels {
+		pos.BinaryOffset = offset
 		var size int64
-		for cur := pos.begin; ; cur = cur.next {
+		for cur := pos.Begin; ; cur = cur.next {
 			switch cur.kind {
 			case nop0:
 				l := cur.nop0Label()
-				if pos, ok := m.labelPositions[l]; ok {
-					pos.binaryOffset = offset + size
+				if pos, ok := ectx.LabelPositions[l]; ok {
+					pos.BinaryOffset = offset + size
 				}
 			case condBr:
 				if !cur.condBrOffsetResolved() {
 					var nextLabel label
-					if i < len(m.orderedBlockLabels)-1 {
+					if i < len(ectx.OrderedBlockLabels)-1 {
 						// Note: this is only used when the block ends with fallthrough,
 						// therefore can be safely assumed that the next block exists when it's needed.
-						nextLabel = m.orderedBlockLabels[i+1].l
+						nextLabel = ectx.OrderedBlockLabels[i+1].L
 					}
 					cbrs = append(cbrs, condBrReloc{
 						cbr: cur, currentLabelPos: pos, offset: offset + size,
@@ -406,14 +292,14 @@ func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 				}
 			}
 			size += cur.size()
-			if cur == pos.end {
+			if cur == pos.End {
 				break
 			}
 		}
 
 		if wazevoapi.PerfMapEnabled {
 			if size > 0 {
-				l := pos.l
+				l := pos.L
 				var labelStr string
 				if blkID, ok := labelToSSABlockID[l]; ok {
 					labelStr = fmt.Sprintf("%s::SSA_Block[%s]", l, blkID)
@@ -424,7 +310,7 @@ func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 			}
 		}
 
-		pos.binarySize = size
+		pos.BinarySize = size
 		offset += size
 	}
 
@@ -436,7 +322,7 @@ func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 		offset := reloc.offset
 
 		target := cbr.condBrLabel()
-		offsetOfTarget := m.labelPositions[target].binaryOffset
+		offsetOfTarget := ectx.LabelPositions[target].BinaryOffset
 		diff := offsetOfTarget - offset
 		if divided := diff >> 2; divided < minSignedInt19 || divided > maxSignedInt19 {
 			// This case the conditional branch is too huge. We place the trampoline instructions at the end of the current block,
@@ -456,11 +342,11 @@ func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 	}
 
 	var currentOffset int64
-	for cur := m.rootInstr; cur != nil; cur = cur.next {
+	for cur := ectx.RootInstr; cur != nil; cur = cur.next {
 		switch cur.kind {
 		case br:
 			target := cur.brLabel()
-			offsetOfTarget := m.labelPositions[target].binaryOffset
+			offsetOfTarget := ectx.LabelPositions[target].BinaryOffset
 			diff := offsetOfTarget - currentOffset
 			divided := diff >> 2
 			if divided < minSignedInt26 || divided > maxSignedInt26 {
@@ -471,7 +357,7 @@ func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 		case condBr:
 			if !cur.condBrOffsetResolved() {
 				target := cur.condBrLabel()
-				offsetOfTarget := m.labelPositions[target].binaryOffset
+				offsetOfTarget := ectx.LabelPositions[target].BinaryOffset
 				diff := offsetOfTarget - currentOffset
 				if divided := diff >> 2; divided < minSignedInt19 || divided > maxSignedInt19 {
 					panic("BUG: branch relocation for large conditional branch larger than 19-bit range must be handled properly")
@@ -481,7 +367,7 @@ func (m *machine) ResolveRelativeAddresses(ctx context.Context) {
 		case brTableSequence:
 			for i := range cur.targets {
 				l := label(cur.targets[i])
-				offsetOfTarget := m.labelPositions[l].binaryOffset
+				offsetOfTarget := ectx.LabelPositions[l].BinaryOffset
 				diff := offsetOfTarget - (currentOffset + brTableSequenceOffsetTableBegin)
 				cur.targets[i] = uint32(diff)
 			}
@@ -502,7 +388,7 @@ const (
 )
 
 func (m *machine) insertConditionalJumpTrampoline(cbr *instruction, currentBlk *labelPosition, nextLabel label) {
-	cur := currentBlk.end
+	cur := currentBlk.End
 	originalTarget := cbr.condBrLabel()
 	endNext := cur.next
 
@@ -525,44 +411,26 @@ func (m *machine) insertConditionalJumpTrampoline(cbr *instruction, currentBlk *
 	cur = linkInstr(cur, br)
 
 	// Update the end of the current block.
-	currentBlk.end = cur
+	currentBlk.End = cur
 
 	linkInstr(cur, endNext)
 }
 
-func (m *machine) getOrAllocateSSABlockLabel(blk ssa.BasicBlock) label {
-	if blk.ReturnBlock() {
-		return returnLabel
-	}
-	l := m.ssaBlockIDToLabels[blk.ID()]
-	if l == invalidLabel {
-		l = m.allocateLabel()
-		m.ssaBlockIDToLabels[blk.ID()] = l
-	}
-	return l
-}
-
-// LinkAdjacentBlocks implements backend.Machine.
-func (m *machine) LinkAdjacentBlocks(prev, next ssa.BasicBlock) {
-	prevLabelPos := m.labelPositions[m.getOrAllocateSSABlockLabel(prev)]
-	nextLabelPos := m.labelPositions[m.getOrAllocateSSABlockLabel(next)]
-	prevLabelPos.end.next = nextLabelPos.begin
-}
-
 // Format implements backend.Machine.
 func (m *machine) Format() string {
+	ectx := m.executableContext
 	begins := map[*instruction]label{}
-	for l, pos := range m.labelPositions {
-		begins[pos.begin] = l
+	for l, pos := range ectx.LabelPositions {
+		begins[pos.Begin] = l
 	}
 
 	irBlocks := map[label]ssa.BasicBlockID{}
-	for i, l := range m.ssaBlockIDToLabels {
+	for i, l := range ectx.SsaBlockIDToLabels {
 		irBlocks[l] = ssa.BasicBlockID(i)
 	}
 
 	var lines []string
-	for cur := m.rootInstr; cur != nil; cur = cur.next {
+	for cur := ectx.RootInstr; cur != nil; cur = cur.next {
 		if l, ok := begins[cur]; ok {
 			var labelStr string
 			if blkID, ok := irBlocks[l]; ok {
diff --git a/internal/engine/wazevo/backend/isa/arm64/machine_pro_epi_logue.go b/internal/engine/wazevo/backend/isa/arm64/machine_pro_epi_logue.go
index 62b4e0ec..fa496640 100644
--- a/internal/engine/wazevo/backend/isa/arm64/machine_pro_epi_logue.go
+++ b/internal/engine/wazevo/backend/isa/arm64/machine_pro_epi_logue.go
@@ -9,7 +9,9 @@ import (
 
 // SetupPrologue implements backend.Machine.
 func (m *machine) SetupPrologue() {
-	cur := m.rootInstr
+	ectx := m.executableContext
+
+	cur := ectx.RootInstr
 	prevInitInst := cur.next
 
 	//
@@ -186,7 +188,8 @@ func (m *machine) createFrameSizeSlot(cur *instruction, s int64) *instruction {
 
 // SetupEpilogue implements backend.Machine.
 func (m *machine) SetupEpilogue() {
-	for cur := m.rootInstr; cur != nil; cur = cur.next {
+	ectx := m.executableContext
+	for cur := ectx.RootInstr; cur != nil; cur = cur.next {
 		if cur.kind == ret {
 			m.setupEpilogueAfter(cur.prev)
 			continue
@@ -360,7 +363,7 @@ func (m *machine) insertStackBoundsCheck(requiredStackSize int64, cur *instructi
 
 	// b.ge #imm
 	cbr := m.allocateInstr()
-	cbr.asCondBr(ge.asCond(), invalidLabel, false /* ignored */)
+	cbr.asCondBr(ge.asCond(), labelInvalid, false /* ignored */)
 	cur = linkInstr(cur, cbr)
 
 	// Set the required stack size and set it to the exec context.
@@ -407,9 +410,11 @@ func (m *machine) insertStackBoundsCheck(requiredStackSize int64, cur *instructi
 
 // CompileStackGrowCallSequence implements backend.Machine.
 func (m *machine) CompileStackGrowCallSequence() []byte {
+	ectx := m.executableContext
+
 	cur := m.allocateInstr()
 	cur.asNop0()
-	m.rootInstr = cur
+	ectx.RootInstr = cur
 
 	// Save the callee saved and argument registers.
 	cur = m.saveRegistersInExecutionContext(cur, saveRequiredRegs)
@@ -431,14 +436,16 @@ func (m *machine) CompileStackGrowCallSequence() []byte {
 	ret.asRet(nil)
 	linkInstr(cur, ret)
 
-	m.encode(m.rootInstr)
+	m.encode(ectx.RootInstr)
 	return m.compiler.Buf()
 }
 
 func (m *machine) addsAddOrSubStackPointer(cur *instruction, rd regalloc.VReg, diff int64, add bool) *instruction {
-	m.pendingInstructions = m.pendingInstructions[:0]
+	ectx := m.executableContext
+
+	ectx.PendingInstructions = ectx.PendingInstructions[:0]
 	m.insertAddOrSubStackPointer(rd, diff, add)
-	for _, inserted := range m.pendingInstructions {
+	for _, inserted := range ectx.PendingInstructions {
 		cur = linkInstr(cur, inserted)
 	}
 	return cur
diff --git a/internal/engine/wazevo/backend/isa/arm64/machine_regalloc.go b/internal/engine/wazevo/backend/isa/arm64/machine_regalloc.go
index eaa5f479..5bff98d1 100644
--- a/internal/engine/wazevo/backend/isa/arm64/machine_regalloc.go
+++ b/internal/engine/wazevo/backend/isa/arm64/machine_regalloc.go
@@ -204,7 +204,7 @@ func (r *regAllocBlockImpl) Preds() int {
 func (r *regAllocBlockImpl) Pred(i int) regalloc.Block {
 	sb := r.sb
 	pred := sb.Pred(i)
-	l := r.f.m.ssaBlockIDToLabels[pred.ID()]
+	l := r.f.m.executableContext.SsaBlockIDToLabels[pred.ID()]
 	index := r.f.labelToRegAllocBlockIndex[l]
 	return &r.f.reversePostOrderBlocks[index]
 }
@@ -221,7 +221,7 @@ func (r *regAllocBlockImpl) Succ(i int) regalloc.Block {
 	if succ.ReturnBlock() {
 		return nil
 	}
-	l := r.f.m.ssaBlockIDToLabels[succ.ID()]
+	l := r.f.m.executableContext.SsaBlockIDToLabels[succ.ID()]
 	index := r.f.labelToRegAllocBlockIndex[l]
 	return &r.f.reversePostOrderBlocks[index]
 }
@@ -240,7 +240,7 @@ func (f *regAllocFunctionImpl) LoopNestingForestRoots() int {
 // LoopNestingForestRoot implements regalloc.Function LoopNestingForestRoot.
 func (f *regAllocFunctionImpl) LoopNestingForestRoot(i int) regalloc.Block {
 	blk := f.loopNestingForestRoots[i]
-	l := f.m.ssaBlockIDToLabels[blk.ID()]
+	l := f.m.executableContext.SsaBlockIDToLabels[blk.ID()]
 	index := f.labelToRegAllocBlockIndex[l]
 	return &f.reversePostOrderBlocks[index]
 }
@@ -254,21 +254,21 @@ func (r *regAllocBlockImpl) LoopNestingForestChildren() int {
 // LoopNestingForestChild implements regalloc.Block LoopNestingForestChild.
 func (r *regAllocBlockImpl) LoopNestingForestChild(i int) regalloc.Block {
 	blk := r.loopNestingForestChildren[i]
-	l := r.f.m.ssaBlockIDToLabels[blk.ID()]
+	l := r.f.m.executableContext.SsaBlockIDToLabels[blk.ID()]
 	index := r.f.labelToRegAllocBlockIndex[l]
 	return &r.f.reversePostOrderBlocks[index]
 }
 
 // InstrIteratorBegin implements regalloc.Block InstrIteratorBegin.
 func (r *regAllocBlockImpl) InstrIteratorBegin() regalloc.Instr {
-	r.cur = r.pos.begin
+	r.cur = r.pos.Begin
 	return r.cur
 }
 
 // InstrIteratorNext implements regalloc.Block InstrIteratorNext.
 func (r *regAllocBlockImpl) InstrIteratorNext() regalloc.Instr {
 	for {
-		if r.cur == r.pos.end {
+		if r.cur == r.pos.End {
 			return nil
 		}
 		instr := r.cur.next
@@ -284,14 +284,14 @@ func (r *regAllocBlockImpl) InstrIteratorNext() regalloc.Instr {
 
 // InstrRevIteratorBegin implements regalloc.Block InstrRevIteratorBegin.
 func (r *regAllocBlockImpl) InstrRevIteratorBegin() regalloc.Instr {
-	r.cur = r.pos.end
+	r.cur = r.pos.End
 	return r.cur
 }
 
 // InstrRevIteratorNext implements regalloc.Block InstrRevIteratorNext.
 func (r *regAllocBlockImpl) InstrRevIteratorNext() regalloc.Instr {
 	for {
-		if r.cur == r.pos.begin {
+		if r.cur == r.pos.Begin {
 			return nil
 		}
 		instr := r.cur.prev
@@ -390,7 +390,7 @@ func (m *machine) insertReloadRegisterAt(v regalloc.VReg, instr *instruction, af
 // LowestCommonAncestor implements regalloc.Function LowestCommonAncestor.
 func (f *regAllocFunctionImpl) LowestCommonAncestor(blk1, blk2 regalloc.Block) regalloc.Block {
 	ret := f.m.compiler.SSABuilder().LowestCommonAncestor(blk1.(*regAllocBlockImpl).sb, blk2.(*regAllocBlockImpl).sb)
-	l := f.m.ssaBlockIDToLabels[ret.ID()]
+	l := f.m.executableContext.SsaBlockIDToLabels[ret.ID()]
 	index := f.labelToRegAllocBlockIndex[l]
 	return &f.reversePostOrderBlocks[index]
 }
@@ -401,24 +401,24 @@ func (f *regAllocFunctionImpl) Idom(blk regalloc.Block) regalloc.Block {
 	if idom == nil {
 		panic("BUG: idom must not be nil")
 	}
-	l := f.m.ssaBlockIDToLabels[idom.ID()]
+	l := f.m.executableContext.SsaBlockIDToLabels[idom.ID()]
 	index := f.labelToRegAllocBlockIndex[l]
 	return &f.reversePostOrderBlocks[index]
 }
 
 // FirstInstr implements regalloc.Block FirstInstr.
 func (r *regAllocBlockImpl) FirstInstr() regalloc.Instr {
-	return r.pos.begin
+	return r.pos.Begin
 }
 
 // LastInstr implements regalloc.Block LastInstr.
 func (r *regAllocBlockImpl) LastInstr() regalloc.Instr {
 	if r.cachedLastInstr == nil {
-		cur := r.pos.end
+		cur := r.pos.End
 		for cur.kind == nop0 {
 			cur = cur.prev
-			if cur == r.pos.begin {
-				r.cachedLastInstr = r.pos.end
+			if cur == r.pos.Begin {
+				r.cachedLastInstr = r.pos.End
 				return r.cachedLastInstr
 			}
 		}
@@ -426,7 +426,7 @@ func (r *regAllocBlockImpl) LastInstr() regalloc.Instr {
 		case br:
 			r.cachedLastInstr = cur
 		default:
-			r.cachedLastInstr = r.pos.end
+			r.cachedLastInstr = r.pos.End
 		}
 	}
 	return r.cachedLastInstr
diff --git a/internal/engine/wazevo/backend/machine.go b/internal/engine/wazevo/backend/machine.go
index c94ac577..76c58a6d 100644
--- a/internal/engine/wazevo/backend/machine.go
+++ b/internal/engine/wazevo/backend/machine.go
@@ -11,6 +11,8 @@ import (
 type (
 	// Machine is a backend for a specific ISA machine.
 	Machine interface {
+		ExecutableContext() ExecutableContext
+
 		// DisableStackCheck disables the stack check for the current compilation for debugging/testing.
 		DisableStackCheck()
 
@@ -28,10 +30,6 @@ type (
 		// This is only called once per Machine, i.e. before the first compilation.
 		SetCompiler(Compiler)
 
-		// StartLoweringFunction is called when the lowering of the given function is started.
-		// maximumBlockID is the maximum value of ssa.BasicBlockID existing in the function.
-		StartLoweringFunction(maximumBlockID ssa.BasicBlockID)
-
 		// StartBlock is called when the compilation of the given block is started.
 		// The order of this being called is the reverse post order of the ssa.BasicBlock(s) as we iterate with
 		// ssa.Builder BlockIteratorReversePostOrderBegin and BlockIteratorReversePostOrderEnd.
@@ -50,22 +48,9 @@ type (
 		// for optimization.
 		LowerInstr(*ssa.Instruction)
 
-		// EndBlock is called when the compilation of the current block is finished.
-		EndBlock()
-
-		// LinkAdjacentBlocks is called after finished lowering all blocks in order to create one single instruction list.
-		LinkAdjacentBlocks(prev, next ssa.BasicBlock)
-
-		// EndLoweringFunction is called when the lowering of the current function is finished.
-		EndLoweringFunction()
-
 		// Reset resets the machine state for the next compilation.
 		Reset()
 
-		// FlushPendingInstructions flushes the pending instructions to the buffer.
-		// This will be called after the lowering of each SSA Instruction.
-		FlushPendingInstructions()
-
 		// InsertMove inserts a move instruction from src to dst whose type is typ.
 		InsertMove(dst, src regalloc.VReg, typ ssa.Type)
 


Test patch:
diff --git a/internal/engine/wazevo/backend/isa/arm64/abi_entry_preamble_test.go b/internal/engine/wazevo/backend/isa/arm64/abi_entry_preamble_test.go
index 3c95d9b1..d0f1ce8e 100644
--- a/internal/engine/wazevo/backend/isa/arm64/abi_entry_preamble_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/abi_entry_preamble_test.go
@@ -397,7 +397,7 @@ func TestAbiImpl_constructEntryPreamble(t *testing.T) {
 		t.Run(tc.name, func(t *testing.T) {
 			_, _, m := newSetupWithMockContext()
 			abi := m.getOrCreateABIImpl(tc.sig)
-			m.rootInstr = abi.constructEntryPreamble()
+			m.executableContext.RootInstr = abi.constructEntryPreamble()
 			require.Equal(t, tc.exp, m.Format())
 		})
 	}
@@ -532,7 +532,7 @@ func TestMachine_goEntryPreamblePassArg(t *testing.T) {
 		t.Run(tc.exp, func(t *testing.T) {
 			_, _, m := newSetupWithMockContext()
 			cur := m.allocateNop()
-			m.rootInstr = cur
+			m.executableContext.RootInstr = cur
 			m.goEntryPreamblePassArg(cur, paramSlicePtr, &tc.arg, tc.argSlotBeginOffsetFromSP)
 			require.Equal(t, tc.exp, m.Format())
 			m.Encode()
@@ -685,7 +685,7 @@ func TestMachine_goEntryPreamblePassResult(t *testing.T) {
 		t.Run(tc.exp, func(t *testing.T) {
 			_, _, m := newSetupWithMockContext()
 			cur := m.allocateNop()
-			m.rootInstr = cur
+			m.executableContext.RootInstr = cur
 			m.goEntryPreamblePassResult(cur, paramSlicePtr, &tc.arg, tc.retStart)
 			require.Equal(t, tc.exp, m.Format())
 			m.Encode()
diff --git a/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go b/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go
index 23fc2bf0..2cb710e4 100644
--- a/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/abi_go_call_test.go
@@ -564,7 +564,7 @@ func Test_goFunctionCallLoadStackArg(t *testing.T) {
 				_, result := m.goFunctionCallLoadStackArg(nop, originalArg0Reg, tc.arg, intVReg, floatVReg)
 				require.Equal(t, tc.expResultReg, result)
 
-				m.rootInstr = nop
+				m.executableContext.RootInstr = nop
 
 				require.Equal(t, tc.exp, m.Format())
 				m.Encode()
@@ -630,7 +630,7 @@ func Test_goFunctionCallStoreStackResult(t *testing.T) {
 				nop := m.allocateNop()
 				m.goFunctionCallStoreStackResult(nop, spVReg, tc.result, tc.resultReg)
 
-				m.rootInstr = nop
+				m.executableContext.RootInstr = nop
 
 				require.Equal(t, tc.exp, m.Format())
 				m.Encode()
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go b/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go
index e3f97023..913162e7 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go
@@ -18,7 +18,7 @@ func TestMachine_LowerConditionalBranch(t *testing.T) {
 		brz bool, intCond ssa.IntegerCmpCond, floatCond ssa.FloatCmpCond,
 		ctx *mockCompiler, builder ssa.Builder, m *machine,
 	) (instr *ssa.Instruction, verify func(t *testing.T)) {
-		m.StartLoweringFunction(10)
+		m.executableContext.StartLoweringFunction(10)
 		entry := builder.CurrentBlock()
 		isInt := intCond != ssa.IntegerCmpCondInvalid
 
@@ -63,7 +63,7 @@ func TestMachine_LowerConditionalBranch(t *testing.T) {
 	}
 
 	icmpInSameGroupFromParamAndImm12 := func(brz bool, ctx *mockCompiler, builder ssa.Builder, m *machine) (instr *ssa.Instruction, verify func(t *testing.T)) {
-		m.StartLoweringFunction(10)
+		m.executableContext.StartLoweringFunction(10)
 		entry := builder.CurrentBlock()
 		v1 := entry.AddParam(builder, ssa.TypeI32)
 
@@ -103,7 +103,7 @@ func TestMachine_LowerConditionalBranch(t *testing.T) {
 		{
 			name: "icmp in different group",
 			setup: func(ctx *mockCompiler, builder ssa.Builder, m *machine) (instr *ssa.Instruction, verify func(t *testing.T)) {
-				m.StartLoweringFunction(10)
+				m.executableContext.StartLoweringFunction(10)
 				entry := builder.CurrentBlock()
 				v1, v2 := entry.AddParam(builder, ssa.TypeI64), entry.AddParam(builder, ssa.TypeI64)
 
@@ -218,7 +218,7 @@ func TestMachine_LowerSingleBranch(t *testing.T) {
 		{
 			name: "b",
 			setup: func(ctx *mockCompiler, builder ssa.Builder, m *machine) (instr *ssa.Instruction) {
-				m.StartLoweringFunction(10)
+				m.executableContext.StartLoweringFunction(10)
 				jump := builder.AllocateInstruction()
 				jump.AsJump(nil, builder.AllocateBasicBlock())
 				builder.InsertInstruction(jump)
@@ -229,7 +229,7 @@ func TestMachine_LowerSingleBranch(t *testing.T) {
 		{
 			name: "ret",
 			setup: func(ctx *mockCompiler, builder ssa.Builder, m *machine) (instr *ssa.Instruction) {
-				m.StartLoweringFunction(10)
+				m.executableContext.StartLoweringFunction(10)
 				jump := builder.AllocateInstruction()
 				jump.AsJump(nil, builder.ReturnBlock())
 				builder.InsertInstruction(jump)
@@ -389,8 +389,8 @@ L2:
 func TestMachine_exitWithCode(t *testing.T) {
 	_, _, m := newSetupWithMockContext()
 	m.lowerExitWithCode(x1VReg, wazevoapi.ExitCodeGrowStack)
-	m.FlushPendingInstructions()
-	m.encode(m.perBlockHead)
+	m.executableContext.FlushPendingInstructions()
+	m.encode(m.executableContext.PerBlockHead)
 	require.Equal(t, `
 movz x1?, #0x1, lsl 0
 str w1?, [x1]
@@ -454,8 +454,8 @@ fcvtzu w1, s2
 			m.lowerFpuToInt(operandNR(x1VReg), operandNR(v2VReg), x15VReg, false, false, false, tc.nontrapping)
 			require.Equal(t, tc.expectedAsm, "\n"+formatEmittedInstructionsInCurrentBlock(m)+"\n")
 
-			m.FlushPendingInstructions()
-			m.encode(m.perBlockHead)
+			m.executableContext.FlushPendingInstructions()
+			m.encode(m.executableContext.PerBlockHead)
 		})
 	}
 }
@@ -536,8 +536,8 @@ mul x1.4s, x2.4s, x15.4s
 			m.lowerVIMul(operandNR(x1VReg), operandNR(x2VReg), operandNR(x15VReg), tc.arrangement)
 			require.Equal(t, tc.expectedAsm, "\n"+formatEmittedInstructionsInCurrentBlock(m)+"\n")
 
-			m.FlushPendingInstructions()
-			m.encode(m.perBlockHead)
+			m.executableContext.FlushPendingInstructions()
+			m.encode(m.executableContext.PerBlockHead)
 			buf := m.compiler.Buf()
 			require.Equal(t, tc.expectedBytes, hex.EncodeToString(buf))
 		})
@@ -641,8 +641,8 @@ cset x15, ne
 			m.lowerVcheckTrue(tc.op, operandNR(x1VReg), operandNR(x15VReg), tc.arrangement)
 			require.Equal(t, tc.expectedAsm, "\n"+formatEmittedInstructionsInCurrentBlock(m)+"\n")
 
-			m.FlushPendingInstructions()
-			m.encode(m.perBlockHead)
+			m.executableContext.FlushPendingInstructions()
+			m.encode(m.executableContext.PerBlockHead)
 			buf := m.compiler.Buf()
 			require.Equal(t, tc.expectedBytes, hex.EncodeToString(buf))
 		})
@@ -726,8 +726,8 @@ add w15, w15, w1?, lsl #1
 			m.lowerVhighBits(operandNR(x1VReg), operandNR(x15VReg), tc.arrangement)
 			require.Equal(t, tc.expectedAsm, "\n"+formatEmittedInstructionsInCurrentBlock(m)+"\n")
 
-			m.FlushPendingInstructions()
-			m.encode(m.perBlockHead)
+			m.executableContext.FlushPendingInstructions()
+			m.encode(m.executableContext.PerBlockHead)
 			buf := m.compiler.Buf()
 			require.Equal(t, tc.expectedBytes, hex.EncodeToString(buf))
 		})
@@ -775,8 +775,8 @@ tbl x1.16b, { v29.16b, v30.16b }, v1?.16b
 			m.lowerShuffle(operandNR(x1VReg), operandNR(x2VReg), operandNR(x15VReg), lane1, lane2)
 			require.Equal(t, tc.expectedAsm, "\n"+formatEmittedInstructionsInCurrentBlock(m)+"\n")
 
-			m.FlushPendingInstructions()
-			m.encode(m.perBlockHead)
+			m.executableContext.FlushPendingInstructions()
+			m.encode(m.executableContext.PerBlockHead)
 			buf := m.compiler.Buf()
 			require.Equal(t, tc.expectedBytes, hex.EncodeToString(buf))
 		})
@@ -832,8 +832,8 @@ ushl x1.16b, x2.16b, v2?.16b
 			m.lowerVShift(tc.op, operandNR(x1VReg), operandNR(x2VReg), operandNR(x15VReg), tc.arrangement)
 			require.Equal(t, tc.expectedAsm, "\n"+formatEmittedInstructionsInCurrentBlock(m)+"\n")
 
-			m.FlushPendingInstructions()
-			m.encode(m.perBlockHead)
+			m.executableContext.FlushPendingInstructions()
+			m.encode(m.executableContext.PerBlockHead)
 			buf := m.compiler.Buf()
 			require.Equal(t, tc.expectedBytes, hex.EncodeToString(buf))
 		})
diff --git a/internal/engine/wazevo/backend/isa/arm64/machine_pro_epi_logue_test.go b/internal/engine/wazevo/backend/isa/arm64/machine_pro_epi_logue_test.go
index bc174a47..70b94471 100644
--- a/internal/engine/wazevo/backend/isa/arm64/machine_pro_epi_logue_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/machine_pro_epi_logue_test.go
@@ -100,14 +100,14 @@ func TestMachine_SetupPrologue(t *testing.T) {
 			m.currentABI = &tc.abi
 
 			root := m.allocateNop()
-			m.rootInstr = root
+			m.executableContext.RootInstr = root
 			udf := m.allocateInstr()
 			udf.asUDF()
 			root.next = udf
 			udf.prev = root
 
 			m.SetupPrologue()
-			require.Equal(t, root, m.rootInstr)
+			require.Equal(t, root, m.executableContext.RootInstr)
 			m.Encode()
 			require.Equal(t, tc.exp, m.Format())
 		})
@@ -213,14 +213,14 @@ func TestMachine_SetupEpilogue(t *testing.T) {
 			m.currentABI = &tc.abi
 
 			root := m.allocateNop()
-			m.rootInstr = root
+			m.executableContext.RootInstr = root
 			ret := m.allocateInstr()
 			ret.asRet(nil)
 			root.next = ret
 			ret.prev = root
 			m.SetupEpilogue()
 
-			require.Equal(t, root, m.rootInstr)
+			require.Equal(t, root, m.executableContext.RootInstr)
 			m.Encode()
 			require.Equal(t, tc.exp, m.Format())
 		})
@@ -263,9 +263,9 @@ func TestMachine_insertStackBoundsCheck(t *testing.T) {
 		tc := tc
 		t.Run(tc.exp, func(t *testing.T) {
 			_, _, m := newSetupWithMockContext()
-			m.rootInstr = m.allocateInstr()
-			m.rootInstr.asNop0()
-			m.insertStackBoundsCheck(tc.requiredStackSize, m.rootInstr)
+			m.executableContext.RootInstr = m.allocateInstr()
+			m.executableContext.RootInstr.asNop0()
+			m.insertStackBoundsCheck(tc.requiredStackSize, m.executableContext.RootInstr)
 			m.Encode()
 			require.Equal(t, tc.exp, m.Format())
 		})
diff --git a/internal/engine/wazevo/backend/isa/arm64/machine_regalloc_test.go b/internal/engine/wazevo/backend/isa/arm64/machine_regalloc_test.go
index 0b7e53ff..396aabd1 100644
--- a/internal/engine/wazevo/backend/isa/arm64/machine_regalloc_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/machine_regalloc_test.go
@@ -81,7 +81,7 @@ func TestRegAllocFunctionImpl_ReloadRegisterAfter(t *testing.T) {
 	require.Equal(t, iload.kind, uLoad64)
 	require.Equal(t, fload.kind, fpuLoad64)
 
-	m.rootInstr = i1
+	m.executableContext.RootInstr = i1
 	require.Equal(t, `
 	ldr d1, [sp, #0x18]
 	ldr x1, [sp, #0x10]
@@ -111,7 +111,7 @@ func TestRegAllocFunctionImpl_StoreRegisterBefore(t *testing.T) {
 	require.Equal(t, iload.kind, store64)
 	require.Equal(t, fload.kind, fpuStore64)
 
-	m.rootInstr = i1
+	m.executableContext.RootInstr = i1
 	require.Equal(t, `
 	str x1, [sp, #0x10]
 	str d1, [sp, #0x18]
@@ -183,7 +183,7 @@ func TestMachine_insertStoreRegisterAt(t *testing.T) {
 						m.insertStoreRegisterAt(x1VReg, i2, after)
 						m.insertStoreRegisterAt(v1VReg, i2, after)
 					}
-					m.rootInstr = i1
+					m.executableContext.RootInstr = i1
 					require.Equal(t, tc.expected, m.Format())
 				})
 			}
@@ -256,7 +256,7 @@ func TestMachine_insertReloadRegisterAt(t *testing.T) {
 						m.insertReloadRegisterAt(x1VReg, i2, after)
 						m.insertReloadRegisterAt(v1VReg, i2, after)
 					}
-					m.rootInstr = i1
+					m.executableContext.RootInstr = i1
 
 					require.Equal(t, tc.expected, m.Format())
 				})
@@ -338,7 +338,7 @@ func TestMachine_swap(t *testing.T) {
 			i2.prev = cur
 
 			m.swap(cur, tc.x1, tc.x2, tc.tmp)
-			m.rootInstr = cur
+			m.executableContext.RootInstr = cur
 
 			require.Equal(t, tc.expected, m.Format())
 		})
diff --git a/internal/engine/wazevo/backend/isa/arm64/machine_test.go b/internal/engine/wazevo/backend/isa/arm64/machine_test.go
index 48e3dd30..959cc625 100644
--- a/internal/engine/wazevo/backend/isa/arm64/machine_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/machine_test.go
@@ -4,32 +4,9 @@ import (
 	"testing"
 
 	"github.com/tetratelabs/wazero/internal/engine/wazevo/backend/regalloc"
-	"github.com/tetratelabs/wazero/internal/engine/wazevo/wazevoapi"
 	"github.com/tetratelabs/wazero/internal/testing/require"
 )
 
-func TestMachine_insertAtHead(t *testing.T) {
-	t.Run("no head", func(t *testing.T) {
-		m := &machine{}
-		i := &instruction{kind: condBr}
-		m.insertAtPerBlockHead(i)
-		require.Equal(t, i, m.perBlockHead)
-		require.Equal(t, i, m.perBlockEnd)
-	})
-	t.Run("has head", func(t *testing.T) {
-		prevHead := &instruction{kind: br}
-		m := &machine{perBlockHead: prevHead, perBlockEnd: prevHead}
-		i := &instruction{kind: condBr}
-		m.insertAtPerBlockHead(i)
-		require.Equal(t, i, m.perBlockHead)
-		require.Equal(t, prevHead, m.perBlockEnd)
-		require.Equal(t, nil, prevHead.next)
-		require.Equal(t, i, prevHead.prev)
-		require.Equal(t, prevHead, i.next)
-		require.Equal(t, nil, i.prev)
-	})
-}
-
 func TestMachine_resolveAddressingMode(t *testing.T) {
 	t.Run("imm12/arg", func(t *testing.T) {
 		m := &machine{}
@@ -57,7 +34,7 @@ func TestMachine_resolveAddressingMode(t *testing.T) {
 	})
 
 	t.Run("tmp reg", func(t *testing.T) {
-		m := &machine{instrPool: wazevoapi.NewPool[instruction](resetInstruction)}
+		m := &machine{executableContext: newExecutableContext()}
 		root := &instruction{kind: udf}
 		i := &instruction{prev: root}
 		i.asULoad(operandNR(x17VReg), addressMode{
@@ -66,7 +43,7 @@ func TestMachine_resolveAddressingMode(t *testing.T) {
 		}, 64)
 		m.resolveAddressingMode(0, 0x40000001, i)
 
-		m.rootInstr = root
+		m.executableContext.RootInstr = root
 		require.Equal(t, `
 	udf
 	movz x27, #0x1, lsl 0
@@ -197,26 +174,28 @@ L200:
 			originalEndNext := m.allocateInstr()
 			originalEndNext.asExitSequence(x0VReg)
 
-			originLabelPos := m.allocateLabelPosition(originLabel)
-			originLabelPos.begin = cbr
-			originLabelPos.end = linkInstr(cbr, end)
-			originNextLabelPos := m.allocateLabelPosition(originLabelNext)
-			originNextLabelPos.begin = originalEndNext
-			linkInstr(originLabelPos.end, originalEndNext)
+			ectx := m.executableContext
+
+			originLabelPos := ectx.AllocateLabelPosition(originLabel)
+			originLabelPos.Begin = cbr
+			originLabelPos.End = linkInstr(cbr, end)
+			originNextLabelPos := ectx.AllocateLabelPosition(originLabelNext)
+			originNextLabelPos.Begin = originalEndNext
+			linkInstr(originLabelPos.End, originalEndNext)
 
-			m.labelPositions[originLabel] = originLabelPos
-			m.labelPositions[originLabelNext] = originNextLabelPos
+			ectx.LabelPositions[originLabel] = originLabelPos
+			ectx.LabelPositions[originLabelNext] = originNextLabelPos
 
-			m.rootInstr = cbr
+			ectx.RootInstr = cbr
 			require.Equal(t, tc.expBefore, m.Format())
 
-			m.nextLabel = 9999999
+			ectx.NextLabel = 9999999
 			m.insertConditionalJumpTrampoline(cbr, originLabelPos, originLabelNext)
 
 			require.Equal(t, tc.expAfter, m.Format())
 
 			// The original label position should be updated to the unconditional jump to the original target destination.
-			require.Equal(t, "b L12345", originLabelPos.end.String())
+			require.Equal(t, "b L12345", originLabelPos.End.String())
 		})
 	}
 }
diff --git a/internal/engine/wazevo/backend/isa/arm64/util_test.go b/internal/engine/wazevo/backend/isa/arm64/util_test.go
index 4f432373..1be5a76d 100644
--- a/internal/engine/wazevo/backend/isa/arm64/util_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/util_test.go
@@ -10,13 +10,13 @@ import (
 )
 
 func getPendingInstr(m *machine) *instruction {
-	return m.pendingInstructions[0]
+	return m.executableContext.PendingInstructions[0]
 }
 
 func formatEmittedInstructionsInCurrentBlock(m *machine) string {
-	m.FlushPendingInstructions()
+	m.executableContext.FlushPendingInstructions()
 	var strs []string
-	for cur := m.perBlockHead; cur != nil; cur = cur.next {
+	for cur := m.executableContext.PerBlockHead; cur != nil; cur = cur.next {
 		strs = append(strs, cur.String())
 	}
 	return strings.Join(strs, "\n")
diff --git a/internal/engine/wazevo/backend/machine_test.go b/internal/engine/wazevo/backend/machine_test.go
index fde03353..1b999f89 100644
--- a/internal/engine/wazevo/backend/machine_test.go
+++ b/internal/engine/wazevo/backend/machine_test.go
@@ -26,6 +26,8 @@ type mockMachine struct {
 	rinfo                  *regalloc.RegisterInfo
 }
 
+func (m mockMachine) ExecutableContext() ExecutableContext { panic("implement me") }
+
 func (m mockMachine) CompileEntryPreamble(signature *ssa.Signature) []byte {
 	panic("TODO")
 }