Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changes/added/925.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add jump-and-link instruction `JAL $retaddr $targetptr offset` that can be used to implement efficient subroutine calls.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ If a specific opcode has unexpected behaviors, maybe there is a unit test
already that you can reuse to reproduce a bug. You need to add a new `test_case` like:

```rust
#[test_case(JumpMode::Absolute, 0, 0, 100 => Ok(4); "absolute jump")]
#[test_case(JumpMode::RelativeIS, 0, 0, 100 => Ok(4); "is-relative jump")]
```

Before the test and run this specific test or all tests.
Expand Down
8 changes: 5 additions & 3 deletions fuel-asm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ impl_instructions! {
0x97 POPL popl [bitmask: Imm24]
"Pop a bitmask-selected set of registers in range 40..64 to the stack."
0x98 POPH poph [bitmask: Imm24]
"Store return address and jump to an absolute address."
0x99 JAL jal [ret_addr: RegId target: RegId offset: Imm12]

"Compare 128bit integers"
0xa0 WDCM wdcm [dst: RegId lhs: RegId rhs: RegId flags: Imm06]
Expand Down Expand Up @@ -455,7 +457,7 @@ impl RegId {
pub const SP: Self = Self(0x05);
/// Stack start pointer. Memory address of bottom of current writable stack area.
pub const SSP: Self = Self(0x04);
/// Smallest writable register.
/// Smallest user-writable register.
pub const WRITABLE: Self = Self(0x10);
/// Contains zero (0), for convenience.
pub const ZERO: Self = Self(0x00);
Expand Down Expand Up @@ -705,8 +707,8 @@ impl Opcode {
| K256 | S256 | NOOP | FLAG | ADDI | ANDI | DIVI | EXPI | MODI | MULI
| MLDV | ORI | SLLI | SRLI | SUBI | XORI | JNEI | LB | LW | SB | SW
| MCPI | MCLI | GM | MOVI | JNZI | JI | JMP | JNE | JMPF | JMPB | JNZF
| JNZB | JNEF | JNEB | CFEI | CFSI | CFE | CFS | GTF | LDC | BSIZ | BLDD
| ECOP | EPAR => true,
| JNZB | JNEF | JNEB | JAL | CFEI | CFSI | CFE | CFS | GTF | LDC | BSIZ
| BLDD | ECOP | EPAR => true,
_ => false,
}
}
Expand Down
1 change: 1 addition & 0 deletions fuel-vm/src/interpreter/executors/instruction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ where
Instruction::PSHH(op) => op.execute(interpreter),
Instruction::POPL(op) => op.execute(interpreter),
Instruction::POPH(op) => op.execute(interpreter),
Instruction::JAL(op) => op.execute(interpreter),
Instruction::WDCM(op) => op.execute(interpreter),
Instruction::WQCM(op) => op.execute(interpreter),
Instruction::WDOP(op) => op.execute(interpreter),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ fn writes_to_ra(opcode: Opcode) -> bool {
Opcode::POPL => false,
Opcode::PSHH => false,
Opcode::PSHL => false,
Opcode::JAL => true,
Opcode::SLL => true,
Opcode::SLLI => true,
Opcode::SRL => true,
Expand Down Expand Up @@ -286,6 +287,7 @@ fn writes_to_rb(opcode: Opcode) -> bool {
Opcode::POPL => false,
Opcode::PSHH => false,
Opcode::PSHL => false,
Opcode::JAL => false,
Opcode::SLL => false,
Opcode::SLLI => false,
Opcode::SRL => false,
Expand Down
43 changes: 38 additions & 5 deletions fuel-vm/src/interpreter/executors/opcodes_impl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ use crate::{
};
use core::ops::Div;
use fuel_asm::{
Instruction,
PanicReason,
RegId,
op::{
ADD,
ADDI,
Expand Down Expand Up @@ -1121,7 +1123,7 @@ where
) -> IoResult<ExecuteState, S::DataError> {
interpreter.gas_charge(interpreter.gas_costs().ji())?;
let imm = self.unpack();
interpreter.jump(JumpArgs::new(JumpMode::Absolute).to_address(imm.into()))?;
interpreter.jump(JumpArgs::new(JumpMode::RelativeIS).to_address(imm.into()))?;
Ok(ExecuteState::Proceed)
}
}
Expand All @@ -1141,7 +1143,7 @@ where
interpreter.gas_charge(interpreter.gas_costs().jnei())?;
let (a, b, imm) = self.unpack();
interpreter.jump(
JumpArgs::new(JumpMode::Absolute)
JumpArgs::new(JumpMode::RelativeIS)
.with_condition(interpreter.registers[a] != interpreter.registers[b])
.to_address(imm.into()),
)?;
Expand All @@ -1164,7 +1166,7 @@ where
interpreter.gas_charge(interpreter.gas_costs().jnzi())?;
let (a, imm) = self.unpack();
interpreter.jump(
JumpArgs::new(JumpMode::Absolute)
JumpArgs::new(JumpMode::RelativeIS)
.with_condition(interpreter.registers[a] != 0)
.to_address(imm.into()),
)?;
Expand All @@ -1187,7 +1189,7 @@ where
interpreter.gas_charge(interpreter.gas_costs().jmp())?;
let a = self.unpack();
interpreter.jump(
JumpArgs::new(JumpMode::Absolute).to_address(interpreter.registers[a]),
JumpArgs::new(JumpMode::RelativeIS).to_address(interpreter.registers[a]),
)?;
Ok(ExecuteState::Proceed)
}
Expand All @@ -1208,7 +1210,7 @@ where
interpreter.gas_charge(interpreter.gas_costs().jne())?;
let (a, b, c) = self.unpack();
interpreter.jump(
JumpArgs::new(JumpMode::Absolute)
JumpArgs::new(JumpMode::RelativeIS)
.with_condition(interpreter.registers[a] != interpreter.registers[b])
.to_address(interpreter.registers[c]),
)?;
Expand Down Expand Up @@ -1358,6 +1360,37 @@ where
}
}

impl<M, S, Tx, Ecal, V> Execute<M, S, Tx, Ecal, V> for fuel_asm::op::JAL
where
M: Memory,
S: InterpreterStorage,
Tx: ExecutableTransaction,
Ecal: EcalHandler,
V: Verifier,
{
fn execute(
self,
interpreter: &mut Interpreter<M, S, Tx, Ecal, V>,
) -> IoResult<ExecuteState, S::DataError> {
interpreter.gas_charge(interpreter.gas_costs().jmp())?;
let (reg_ret_addr, reg_target, offset) = self.unpack();

// Storing return address to zero register discards it instead
// While we use saturating_add here, the PC shoudln't ever have values above the
// memory size anyway
let ret_addr =
interpreter.registers[RegId::PC].saturating_add(Instruction::SIZE as u64);
interpreter.set_user_reg_or_discard(reg_ret_addr, ret_addr)?;

interpreter.jump(
JumpArgs::new(JumpMode::Assign)
.to_address(interpreter.registers[reg_target])
.plus_fixed(offset.into()),
)?;
Ok(ExecuteState::Proceed)
}
}

impl<M, S, Tx, Ecal, V> Execute<M, S, Tx, Ecal, V> for fuel_asm::op::RET
where
M: Memory,
Expand Down
52 changes: 32 additions & 20 deletions fuel-vm/src/interpreter/flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,11 +253,13 @@ pub(crate) fn revert(

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum JumpMode {
/// `$pc = $is + address`
Absolute,
/// `$pc = $pc + address`
/// `$pc = reg + (imm * instruction_size)`
Assign,
/// `$pc = $is + (reg + imm) * instruction_size)`
RelativeIS,
/// `$pc = $pc + (reg + imm + 1) * instruction_size`
RelativeForwards,
/// `$pc = $pc - address`
/// `$pc = $pc - (reg + imm + 1) * instruction_size`
RelativeBackwards,
}

Expand Down Expand Up @@ -303,23 +305,33 @@ impl JumpArgs {
return Ok(inc_pc(pc)?)
}

let offset_instructions = match self.mode {
JumpMode::Absolute => self.dynamic.saturating_add(self.fixed),
// Here +1 is added since jumping to the jump instruction itself doesn't make
// sense
JumpMode::RelativeForwards | JumpMode::RelativeBackwards => {
self.dynamic.saturating_add(self.fixed).saturating_add(1)
}
};

let offset_bytes = offset_instructions.saturating_mul(Instruction::SIZE as Word);

let target_addr = match self.mode {
JumpMode::Absolute => is.saturating_add(offset_bytes),
JumpMode::RelativeForwards => pc.saturating_add(offset_bytes),
JumpMode::RelativeBackwards => pc
.checked_sub(offset_bytes)
.ok_or(PanicReason::MemoryOverflow)?,
JumpMode::Assign => self
.dynamic
.saturating_add(self.fixed.saturating_mul(Instruction::SIZE as Word)),
JumpMode::RelativeIS => {
let offset_instructions = self.dynamic.saturating_add(self.fixed);
let offset_bytes =
offset_instructions.saturating_mul(Instruction::SIZE as Word);
is.saturating_add(offset_bytes)
}
// In relative jumps, +1 is added since jumping to the jump instruction itself
// is not useful
JumpMode::RelativeForwards => {
let offset_instructions =
self.dynamic.saturating_add(self.fixed).saturating_add(1);
let offset_bytes =
offset_instructions.saturating_mul(Instruction::SIZE as Word);
pc.saturating_add(offset_bytes)
}
JumpMode::RelativeBackwards => {
let offset_instructions =
self.dynamic.saturating_add(self.fixed).saturating_add(1);
let offset_bytes =
offset_instructions.saturating_mul(Instruction::SIZE as Word);
pc.checked_sub(offset_bytes)
.ok_or(PanicReason::MemoryOverflow)?
}
};

if target_addr >= VM_MAX_RAM {
Expand Down
22 changes: 19 additions & 3 deletions fuel-vm/src/interpreter/flow/jump_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,28 @@ use test_case::test_case;

use crate::error::PanicOrBug;

#[test_case(0, 0, 0, 0 => Ok(0); "noop jump")]
#[test_case(0, 10, 0, 0 => Ok(0); "jump to zero")]
#[test_case(0, 10, 0, 4 => Ok(16); "jump to zero plus offset")]
#[test_case(0, 10, 8, 0 => Ok(8); "jump to nonzero")]
#[test_case(0, 10, 8, 4 => Ok(24); "jump to nonzero plus offset")]
#[test_case(1234, 10, 0, 4 => Ok(16); "is won't affect jump")]
#[test_case(0, 0, VM_MAX_RAM + 1, 0 => Err(PanicOrBug::Panic(PanicReason::MemoryOverflow)); "jump too far forward")]
#[test_case(0, 0, 0, VM_MAX_RAM => Err(PanicOrBug::Panic(PanicReason::MemoryOverflow)); "jump too far forward with offset")]
fn test_assign_jump(is: Word, mut pc: Word, j: Word, f: Word) -> SimpleResult<Word> {
JumpArgs::new(JumpMode::Assign)
.to_address(j)
.plus_fixed(f)
.jump(Reg::new(&is), RegMut::new(&mut pc))
.map(|_| pc)
}

#[test_case(0, 0, 0 => Ok(0); "noop jump")]
#[test_case(0, 0, 20 => Ok(80); "jump forwards")]
#[test_case(0, 80, 10 => Ok(40); "jump backwards")]
#[test_case(0, 40, VM_MAX_RAM => Err(PanicOrBug::Panic(PanicReason::MemoryOverflow)); "jump too far forward")]
fn test_absolute_jump(is: Word, mut pc: Word, j: Word) -> SimpleResult<Word> {
JumpArgs::new(JumpMode::Absolute)
JumpArgs::new(JumpMode::RelativeIS)
.to_address(j)
.jump(Reg::new(&is), RegMut::new(&mut pc))
.map(|_| pc)
Expand Down Expand Up @@ -36,10 +52,10 @@ fn test_relative_backwards_jump(is: Word, mut pc: Word, j: Word) -> SimpleResult
.map(|_| pc)
}

#[test_case(JumpMode::Absolute, 0, 0, 100 => Ok(4); "absolute jump")]
#[test_case(JumpMode::RelativeIS, 0, 0, 100 => Ok(4); "absolute jump")]
#[test_case(JumpMode::RelativeForwards, 0, 1000, 100 => Ok(1004); "relative jump forwards")]
#[test_case(JumpMode::RelativeBackwards, 0, 1000, 100 => Ok(1004); "relative jump backwards")]
#[test_case(JumpMode::Absolute, 0, 40, VM_MAX_RAM => Ok(44); "abslute jump too far forward")]
#[test_case(JumpMode::RelativeIS, 0, 40, VM_MAX_RAM => Ok(44); "abslute jump too far forward")]
#[test_case(JumpMode::RelativeForwards, 0, 40, VM_MAX_RAM => Ok(44); "relative jump too far forward")]
#[test_case(JumpMode::RelativeBackwards, 0, 40, 100 => Ok(44); "relative too far backwards")]
fn test_not_performed_conditional_jump(
Expand Down
16 changes: 16 additions & 0 deletions fuel-vm/src/interpreter/internal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use fuel_asm::{
Flags,
Instruction,
PanicReason,
RegId,
};
use fuel_tx::{
Output,
Expand Down Expand Up @@ -45,6 +46,21 @@ where
let tx_offset = self.tx_offset();
update_memory_output(&self.tx, self.memory.as_mut(), tx_offset, idx)
}

/// Sets a non-system register to a value. If a system register is passed, return a
/// panic. Writes to the zero register are ignored.
pub fn set_user_reg_or_discard(&mut self, reg: RegId, val: Word) -> SimpleResult<()> {
if reg == RegId::ZERO {
return Ok(());
}

if reg < RegId::WRITABLE {
return Err(PanicReason::ReservedRegisterNotWritable.into());
}

self.registers[reg] = val;
Ok(())
}
}

/// Increase the variable output with a given asset ID. Modifies both the referenced tx
Expand Down
Loading
Loading