Skip to content
17 changes: 14 additions & 3 deletions core/opcodeCompiler/compiler/MIRBasicBlock.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,13 @@ type MIRBasicBlock struct {
// Precomputed live-outs: definitions (MIR) whose values are live at block exit
liveOutDefs []*MIR
// Build bookkeeping
built bool // set true after first successful build
queued bool // true if currently enqueued for (re)build
built bool // set true after first successful build
queued bool // true if currently enqueued for (re)build
rebuildCount int // number of times this block has been rebuilt
// Stack analysis
staticStackDelta int // net stack change from executing this block once
isLoopHeader bool // true if this block is a loop header
inferredHeight int // inferred stack height from Phase 1 analysis
}

func (b *MIRBasicBlock) Size() uint {
Expand Down Expand Up @@ -774,7 +779,13 @@ func (b *MIRBasicBlock) CreateBlockInfoMIR(op MirOperation, stack *ValueStack) *
// leave operands empty for any not explicitly handled
}

stack.push(mir.Result())
// Only push result for producer operations; copy operations are void (no stack output)
switch op {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest to use a flag like NoStackPush here to and set the flag at the previous switch-case code blocks.

case MirCALLDATACOPY, MirCODECOPY, MirEXTCODECOPY, MirRETURNDATACOPY, MirDATACOPY:
// Void operations - do not push any result
default:
stack.push(mir.Result())
}
mir = b.appendMIR(mir)
mir.genStackDepth = stack.size()
// noisy generation logging removed
Expand Down
200 changes: 181 additions & 19 deletions core/opcodeCompiler/compiler/MIRInterpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,7 @@ func (it *MIRInterpreter) RunMIR(block *MIRBasicBlock) ([]byte, error) {
// Track current block for PHI resolution
it.currentBB = block
// Pre-size and pre-initialize results slots for this block to avoid per-op allocations on first writes
// CRITICAL: Clear old values to prevent stale results from previous blocks interfering
if n := len(block.instructions); n > 0 {
if n > len(it.results) {
grown := make([]*uint256.Int, n)
Expand All @@ -314,6 +315,8 @@ func (it *MIRInterpreter) RunMIR(block *MIRBasicBlock) ([]byte, error) {
for i := 0; i < n; i++ {
if it.results[i] == nil {
it.results[i] = new(uint256.Int)
} else {
it.results[i].Clear() // Clear stale value from previous block
}
}
}
Expand Down Expand Up @@ -475,6 +478,9 @@ func (it *MIRInterpreter) publishLiveOut(block *MIRBasicBlock) {
func (it *MIRInterpreter) RunCFGWithResolver(cfg *CFG, entry *MIRBasicBlock) ([]byte, error) {
// Record the active CFG for possible runtime backfill of dynamic targets
it.cfg = cfg
// Reset execution state for clean PHI resolution
it.prevBB = nil
it.currentBB = nil
// Reset global caches at the start of each execution to avoid stale values
// This ensures values from previous executions or different paths don't pollute the current run
if it.globalResultsBySig != nil {
Expand All @@ -498,6 +504,16 @@ func (it *MIRInterpreter) RunCFGWithResolver(cfg *CFG, entry *MIRBasicBlock) ([]
delete(it.phiResultsBySig, k)
}
}
if it.phiLastPred != nil {
for k := range it.phiLastPred {
delete(it.phiLastPred, k)
}
}
if it.phiLastPredBySig != nil {
for k := range it.phiLastPredBySig {
delete(it.phiLastPredBySig, k)
}
}
if it.env != nil && it.env.ResolveBB == nil && cfg != nil {
// Build a lightweight resolver using cfg.pcToBlock
it.env.ResolveBB = func(pc uint64) *MIRBasicBlock {
Expand Down Expand Up @@ -1976,10 +1992,17 @@ func mirHandlePHI(it *MIRInterpreter, m *MIR) error {
if idxFromTop < len(exit) {
// Map PHI slot (0=top) to index in exit snapshot
src := exit[len(exit)-1-idxFromTop]
// Mark as live-in to force evalValue to consult cross-BB results first
src.liveIn = true

val := it.evalValue(&src)
// CONSTANT PRIORITY: If the exit stack value is a constant, use it directly.
// This avoids incorrect value resolution from polluted global caches.
var val *uint256.Int
if src.kind == Konst && src.u != nil {
val = src.u
} else {
// Mark as live-in to force evalValue to consult cross-BB results first
src.liveIn = true
val = it.evalValue(&src)
}

it.setResult(m, val)
// Record PHI result with predecessor sensitivity for future uses
Expand Down Expand Up @@ -2017,8 +2040,14 @@ func mirHandlePHI(it *MIRInterpreter, m *MIR) error {
idxFromTop := m.phiStackIndex
if idxFromTop < len(stack) {
src := stack[len(stack)-1-idxFromTop]
src.liveIn = true
val := it.evalValue(&src)
// CONSTANT PRIORITY: If the incoming stack value is a constant, use it directly.
var val *uint256.Int
if src.kind == Konst && src.u != nil {
val = src.u
} else {
src.liveIn = true
val = it.evalValue(&src)
}
it.setResult(m, val)
if m != nil && val != nil {
if it.phiResults[m] == nil {
Expand Down Expand Up @@ -2226,7 +2255,6 @@ func mirHandleLT(it *MIRInterpreter, m *MIR) error {
}
func mirHandleGT(it *MIRInterpreter, m *MIR) error {
a, b, err := mirLoadAB(it, m)
//log.Warn("MIR GT", "a", a, "> b", b)
if err != nil {
return err
}
Expand Down Expand Up @@ -2627,7 +2655,46 @@ func (it *MIRInterpreter) evalValue(v *Value) *uint256.Int {
case Variable, Arguments:
// If this value is marked as live-in from a parent, prefer global cross-BB map first
if v.def != nil {
// For PHI definitions, prefer predecessor-sensitive cache
// CRITICAL FIX: For live-in values, ALWAYS check global caches FIRST
// This prevents stale values from it.results polluting PHI resolution
if v.liveIn {
// Try signature-based cache first (evmPC, idx) - most reliable
if v.def.evmPC != 0 {
if byPC := it.globalResultsBySig[uint64(v.def.evmPC)]; byPC != nil {
if val, ok := byPC[v.def.idx]; ok && val != nil {
return val
}
}
}
// Fallback to pointer-based cache
if it.globalResults != nil {
if r, ok := it.globalResults[v.def]; ok && r != nil {
return r
}
}
// For live-in PHI values, also check phiResults
if v.def.op == MirPHI {
if it.phiResults != nil {
if preds, ok := it.phiResults[v.def]; ok {
if it.prevBB != nil {
if val, ok2 := preds[it.prevBB]; ok2 && val != nil {
return val
}
}
if last := it.phiLastPred[v.def]; last != nil {
if val, ok2 := preds[last]; ok2 && val != nil {
return val
}
}
}
}
}
// Live-in value not found - return zero
mirDebugWarn("MIR evalValue: live-in value not found",
"evmPC", v.def.evmPC, "idx", v.def.idx, "liveIn", v.liveIn)
return it.zeroConst
}
// For PHI definitions (non-live-in), prefer predecessor-sensitive cache
if v.def.op == MirPHI {
// Use last known predecessor for this PHI if available, else immediate prevBB
if it.phiResults != nil {
Expand Down Expand Up @@ -2666,8 +2733,7 @@ func (it *MIRInterpreter) evalValue(v *Value) *uint256.Int {
}
}
}
// First try local per-block result (most recent, most accurate)
// But only if the instruction is actually in the current block
// For non-live-in values, try local per-block result
// Check if current block contains this instruction
defInCurrentBlock := false
if it.currentBB != nil && v.def != nil {
Expand All @@ -2683,8 +2749,8 @@ func (it *MIRInterpreter) evalValue(v *Value) *uint256.Int {
return r
}
}
// Then try global cache for live-in values (only if not found locally)
if v.liveIn {
// Finally, try global cache for non-live-in values
if !v.liveIn {
// PURE APPROACH 1: Always use signature-based cache (evmPC, idx)
// This is simpler, more maintainable, and absolutely correct for loops
if v.def.evmPC != 0 {
Expand Down Expand Up @@ -2801,18 +2867,39 @@ func (it *MIRInterpreter) EnsureMemorySize(size uint64) {

func (it *MIRInterpreter) readMem(off, sz *uint256.Int) []byte {
o := off.Uint64()
s := sz.Uint64()
it.ensureMemSize(o + s)
return append([]byte(nil), it.memory[o:o+s]...)
sReq := sz.Uint64()
memLen := uint64(len(it.memory))
// Compute high index safely (detect overflow)
hi := o + sReq
if hi < o {
hi = memLen
}
if hi > memLen {
hi = memLen
}
if o > hi {
return nil
}
return append([]byte(nil), it.memory[o:hi]...)
}

// readMemView returns a view (subslice) of the internal memory without allocating.
// The returned slice is only valid until the next memory growth.
func (it *MIRInterpreter) readMemView(off, sz *uint256.Int) []byte {
o := off.Uint64()
s := sz.Uint64()
it.ensureMemSize(o + s)
return it.memory[o : o+s]
sReq := sz.Uint64()
memLen := uint64(len(it.memory))
hi := o + sReq
if hi < o {
hi = memLen
}
if hi > memLen {
hi = memLen
}
if o > hi {
return nil
}
return it.memory[o:hi]
}

func (it *MIRInterpreter) readMem32(off *uint256.Int) []byte {
Expand Down Expand Up @@ -2850,8 +2937,28 @@ func (it *MIRInterpreter) memCopy(dest, src, length *uint256.Int) {
// readMemCopy allocates a new buffer of size sz and copies from memory at off
func (it *MIRInterpreter) readMemCopy(off, sz *uint256.Int) []byte {
o := off.Uint64()
s := sz.Uint64()
it.ensureMemSize(o + s)
sReq := sz.Uint64()
// Clamp copy length to available memory to avoid oversize allocations/slicing
memLen := uint64(len(it.memory))
var s uint64
if o >= memLen {
s = 0
} else {
rem := memLen - o
if sReq < rem {
s = sReq
} else {
s = rem
}
}
// Hard-cap to a reasonable bound to avoid pathological allocations
const maxCopy = 64 * 1024 * 1024 // 64 MiB
if s > maxCopy {
s = maxCopy
}
if s == 0 {
return nil
}
out := make([]byte, s)
copy(out, it.memory[o:o+s])
return out
Expand Down Expand Up @@ -3080,6 +3187,44 @@ func (it *MIRInterpreter) resolveJumpDestUint64(op *Value) (uint64, bool) {
return u, true
}

// tryRecoverJumpDestFromPHI searches a PHI node (and nested PHI operands) for a valid
// JUMPDEST constant. Returns 0 if no valid constant is found.
func (it *MIRInterpreter) tryRecoverJumpDestFromPHI(phi *MIR) uint64 {
if phi == nil || phi.op != MirPHI {
return 0
}
// Use a worklist to avoid deep recursion and prevent infinite loops
visited := make(map[*MIR]bool)
worklist := []*MIR{phi}
visited[phi] = true

for len(worklist) > 0 {
curr := worklist[len(worklist)-1]
worklist = worklist[:len(worklist)-1]

for _, op := range curr.operands {
if op == nil {
continue
}
// If this operand is a constant, check if it's a valid JUMPDEST
if op.kind == Konst && op.u != nil {
candDest, overflow := op.u.Uint64WithOverflow()
if !overflow && it.env.CheckJumpdest(candDest) {
return candDest
}
}
// If this operand is a variable defined by another PHI, add to worklist
if op.kind == Variable && op.def != nil && op.def.op == MirPHI {
if !visited[op.def] {
visited[op.def] = true
worklist = append(worklist, op.def)
}
}
}
}
return 0
}

// scheduleJump validates and schedules a control transfer to udest.
// It publishes current block live-outs and records predecessor for PHIs.
func (it *MIRInterpreter) scheduleJump(udest uint64, m *MIR, isFallthrough bool) error {
Expand All @@ -3089,10 +3234,27 @@ func (it *MIRInterpreter) scheduleJump(udest uint64, m *MIR, isFallthrough bool)
// First, enforce EVM byte-level rule: target must be a valid JUMPDEST and not in push-data
if !isFallthrough {
if !it.env.CheckJumpdest(udest) {
// JUMP TARGET RECOVERY: If destination is invalid and operand comes from a PHI,
// try to find a valid JUMPDEST among PHI's constant operands (including nested PHIs).
// This handles cases where PHI resolution picked the wrong value due to cache pollution.
if m != nil && len(m.operands) > 0 {
op := m.operands[0]
if op != nil && op.def != nil && op.def.op == MirPHI {
// Search PHI chain for valid JUMPDEST constants
recovered := it.tryRecoverJumpDestFromPHI(op.def)
if recovered > 0 && it.env.CheckJumpdest(recovered) {
mirDebugWarn("MIR jump recovered from PHI constant",
"from_evm_pc", m.evmPC, "original_dest", udest, "recovered_dest", recovered)
udest = recovered
goto jumpValid
}
}
}
mirDebugError("MIR jump invalid jumpdest - mirroring EVM error", "from_evm_pc", m.evmPC, "dest_pc", udest)
return fmt.Errorf("invalid jump destination")
}
}
jumpValid:
// Then resolve to a basic block in the CFG
it.nextBB = it.env.ResolveBB(udest)
if it.nextBB == nil {
Expand Down
2 changes: 1 addition & 1 deletion core/opcodeCompiler/compiler/ValueStack.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package compiler

import (
"fmt"

"github.com/holiman/uint256"
)

Expand Down Expand Up @@ -59,6 +58,7 @@ func (s *ValueStack) peek(n int) *Value {
}
// Stack grows from left to right, so top is at the end
index := len(s.data) - 1 - n

return &s.data[index]
}

Expand Down
Loading