Skip to main content

Try it Live

Run Bytecode examples in the interactive playground

bytecode_get_next_pc(bytecode_t bytecode, size_t current_pc, size_t *next_pc) bool

Calculate the next program counter position after the instruction at current_pc.Parameters:
  • bytecode: Bytecode to analyze
  • current_pc: Current program counter position
  • next_pc: Output parameter for next PC
Returns: bool - true if next_pc is valid, false if at endExample:
#include "primitives.h"

bytecode_t code = bytecode_from_hex("0x60016002015b00");
size_t next_pc;

if (bytecode_get_next_pc(code, 0, &next_pc)) {
    printf("Next PC: %zu\n", next_pc); // 2
} else {
    printf("At end of bytecode\n");
}

bytecode_free(code);
Defined in: primitives.h

How It Works

getNextPc() handles the variable-width nature of EVM instructions:

Regular Opcodes (1 byte)

ADD, MUL, STOP, JUMPDEST, etc.
└─ Next PC = Current PC + 1

PUSH Instructions (1 + N bytes)

PUSH1:  opcode (1 byte) + data (1 byte)  = 2 bytes total
PUSH2:  opcode (1 byte) + data (2 bytes) = 3 bytes total
...
PUSH32: opcode (1 byte) + data (32 bytes) = 33 bytes total

Next PC = Current PC + 1 + N (where N = push width)
Example:
const code = Bytecode("0x7F" + "FF".repeat(32) + "01");
// PUSH32 (0x7F) + 32 bytes data + ADD (0x01)

console.log(code.getNextPc(0));  // 33 (skip PUSH32 + 32 bytes)
console.log(code.getNextPc(33)); // 34 (ADD is 1 byte)

Usage Patterns

Manual Iteration

const code = Bytecode(bytecode);

let pc = 0;
while (pc !== undefined && pc < code.size()) {
  const instruction = code.at(pc);
  console.log(`PC ${pc}: ${instruction}`);

  pc = code.getNextPc(pc);
}
For iteration, prefer using bytecode.scan() which handles this automatically. Use getNextPc() for cases requiring manual PC control.

Jump Target Calculation

// Calculate all possible next PCs from a JUMPI
function getPossibleNextPcs(
  code: BrandedBytecode,
  jumpiPc: number,
  jumpTarget: number
): number[] {
  const nextPcs: number[] = [];

  // Fallthrough path (condition false)
  const fallthrough = code.getNextPc(jumpiPc);
  if (fallthrough !== undefined) {
    nextPcs.push(fallthrough);
  }

  // Jump path (condition true)
  nextPcs.push(jumpTarget);

  return nextPcs;
}

const jumpiAt = 10;
const target = 50;
const paths = getPossibleNextPcs(code, jumpiAt, target);

console.log(`JUMPI at PC ${jumpiAt} can go to: ${paths.join(', ')}`);

Instruction Boundary Validation

// Check if PC is at an instruction boundary (not in PUSH data)
function isInstructionStart(code: BrandedBytecode, pc: number): boolean {
  if (pc === 0) return true; // Entry point is always instruction start

  // Walk backward to find the instruction that contains or precedes this PC
  for (let checkPc = pc - 1; checkPc >= 0; checkPc--) {
    const nextPc = code.getNextPc(checkPc);
    if (nextPc === undefined) continue;

    if (nextPc === pc) {
      // Found an instruction that ends exactly at pc
      return true;
    } else if (nextPc > pc) {
      // Found an instruction that spans past pc (we're in PUSH data)
      return false;
    }
  }

  return false;
}

console.log(isInstructionStart(code, 0));  // true (entry)
console.log(isInstructionStart(code, 1));  // false (inside PUSH1 data)
console.log(isInstructionStart(code, 2));  // true (next instruction)

Disassembly with PC Tracking

const code = Bytecode(bytecode);
const disassembly: string[] = [];

let pc = 0;
while (pc !== undefined && pc < code.size()) {
  const opcode = code.raw()[pc];
  const instruction = Opcode.getName(opcode);

  // Get PUSH data if applicable
  if (Opcode.isPush(opcode)) {
    const pushSize = opcode - 0x5F; // PUSH1 = 0x60, size = 1
    const data = code.raw().slice(pc + 1, pc + 1 + pushSize);
    const value = bytesToHex(data);
    disassembly.push(`${pc}: ${instruction} 0x${value}`);
  } else {
    disassembly.push(`${pc}: ${instruction}`);
  }

  pc = code.getNextPc(pc);
}

console.log(disassembly.join('\n'));

Coverage Tracking

// Track which PCs were executed
const executedPcs = new Set<number>();

// Simulate execution with PC tracking
function simulateExecution(code: BrandedBytecode, startPc: number = 0) {
  let pc: number | undefined = startPc;

  while (pc !== undefined) {
    executedPcs.add(pc);

    const opcode = code.raw()[pc];

    // Handle control flow
    if (opcode === Opcode.JUMP) {
      // Would need stack to determine target
      break;
    } else if (opcode === Opcode.STOP || opcode === Opcode.RETURN) {
      break;
    }

    pc = code.getNextPc(pc);
  }
}

simulateExecution(code);

// Calculate coverage
const allPcs = new Set<number>();
let pc = 0;
while (pc !== undefined && pc < code.size()) {
  allPcs.add(pc);
  pc = code.getNextPc(pc);
}

const coverage = (executedPcs.size / allPcs.size) * 100;
console.log(`PC coverage: ${coverage.toFixed(1)}%`);

Edge Cases

At End of Bytecode

const code = Bytecode("0x00"); // STOP
const nextPc = code.getNextPc(0);

console.log(nextPc); // undefined (no next instruction)

Invalid PC

const nextPc = code.getNextPc(9999);
console.log(nextPc); // undefined (PC out of bounds)

Truncated PUSH

// Malformed bytecode: PUSH2 with only 1 byte of data
const malformed = Bytecode("0x6100"); // PUSH2 0x00 (missing 1 byte)

const nextPc = code.getNextPc(0);
// Implementation dependent: may return PC past end or undefined
For malformed bytecode (truncated PUSH instructions), behavior is implementation-defined. Always validate bytecode before processing.

PC in Middle of PUSH Data

// PUSH2 0x1234 at PC 0 means:
//   PC 0: PUSH2 opcode
//   PC 1: 0x12 (data byte 1)
//   PC 2: 0x34 (data byte 2)
//   PC 3: next instruction

const nextFromOpcode = code.getNextPc(0); // 3 (correct)
const nextFromData1  = code.getNextPc(1); // undefined or error (invalid PC)
const nextFromData2  = code.getNextPc(2); // undefined or error (invalid PC)
Only call getNextPc() from instruction start positions. Calling from within PUSH data is undefined behavior.

Performance

getNextPc() is O(1) - constant time:
  • Reads single byte at current PC
  • Calculates width based on opcode
  • Returns PC + 1 + width
No caching or pre-analysis required.

Comparison with scan()

FeaturegetNextPc()scan()
Use caseManual PC controlAutomatic iteration
ReturnsNext PC numberInstruction object
PUSH dataReturns PC after dataIncludes parsed value
PerformanceO(1) per callO(1) per instruction
Ease of useLow-levelHigh-level
Use getNextPc() when:
  • Building custom traversal logic
  • Implementing jump analysis
  • Need fine-grained PC control
  • Working with PC-based data structures
Use scan() when:
  • Standard iteration over instructions
  • Need instruction metadata (opcode, type, value)
  • Want fusion detection
  • Don’t need manual PC manipulation

See Also

  • scan - High-level iterator (recommended for most use cases)
  • getBlock - Get block containing a PC
  • parseInstructions - Parse all instructions to array
  • Opcode - Opcode utilities for determining instruction width