mirror of
https://github.com/status-im/nimbus-eth1.git
synced 2025-01-29 05:25:34 +00:00
e040e2671a
* Added basic async capabilities for vm2. This is a whole new Git branch, not the same one as last time (https://github.com/status-im/nimbus-eth1/pull/1250) - there wasn't much worth salvaging. Main differences: I didn't do the "each opcode has to specify an async handler" junk that I put in last time. Instead, in oph_memory.nim you can see sloadOp calling asyncChainTo and passing in an async operation. That async operation is then run by the execCallOrCreate (or asyncExecCallOrCreate) code in interpreter_dispatch.nim. In the test code, the (previously existing) macro called "assembler" now allows you to add a section called "initialStorage", specifying fake data to be used by the EVM computation run by that test. (In the long run we'll obviously want to write tests that for-real use the JSON-RPC API to asynchronously fetch data; for now, this was just an expedient way to write a basic unit test that exercises the async-EVM code pathway.) There's also a new macro called "concurrentAssemblers" that allows you to write a test that runs multiple assemblers concurrently (and then waits for them all to finish). There's one example test using this, in test_op_memory_lazy.nim, though you can't actually see it doing so unless you uncomment some echo statements in async_operations.nim (in which case you can see the two concurrently running EVM computations each printing out what they're doing, and you'll see that they interleave). A question: is it possible to make EVMC work asynchronously? (For now, this code compiles and "make test" passes even if ENABLE_EVMC is turned on, but it doesn't actually work asynchronously, it just falls back on doing the usual synchronous EVMC thing. See FIXME-asyncAndEvmc.) * Moved the AsyncOperationFactory to the BaseVMState object. * Made the AsyncOperationFactory into a table of fn pointers. Also ditched the plain-data Vm2AsyncOperation type; it wasn't really serving much purpose. Instead, the pendingAsyncOperation field directly contains the Future. * Removed the hasStorage idea. It's not the right solution to the "how do we know whether we still need to fetch the storage value or not?" problem. I haven't implemented the right solution yet, but at least we're better off not putting in a wrong one. * Added/modified/removed some comments. (Based on feedback on the PR.) * Removed the waitFor from execCallOrCreate. There was some back-and-forth in the PR regarding whether nested waitFor calls are acceptable: https://github.com/status-im/nimbus-eth1/pull/1260#discussion_r998587449 The eventual decision was to just change the waitFor to a doAssert (since we probably won't want this extra functionality when running synchronously anyway) to make sure that the Future is already finished.
517 lines
16 KiB
Nim
517 lines
16 KiB
Nim
# Nimbus
|
|
# Copyright (c) 2018 Status Research & Development GmbH
|
|
# Licensed under either of
|
|
# * Apache License, version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
|
# http://www.apache.org/licenses/LICENSE-2.0)
|
|
# * MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
|
# http://opensource.org/licenses/MIT)
|
|
# at your option. This file may not be copied, modified, or distributed except
|
|
# according to those terms.
|
|
|
|
## EVM Opcode Handlers: Stack, Memory, Storage And Flow Operations
|
|
## ===============================================================
|
|
##
|
|
|
|
import
|
|
../../../errors,
|
|
../../async_operations,
|
|
../../code_stream,
|
|
../../computation,
|
|
../../memory,
|
|
../../stack,
|
|
../../types,
|
|
../gas_costs,
|
|
../gas_meter,
|
|
../op_codes,
|
|
../utils/utils_numeric,
|
|
./oph_defs,
|
|
./oph_helpers,
|
|
eth/common,
|
|
stint,
|
|
strformat
|
|
|
|
when not defined(evmc_enabled):
|
|
import
|
|
../../state,
|
|
../../../db/accounts_cache
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Private helpers
|
|
# ------------------------------------------------------------------------------
|
|
|
|
when evmc_enabled:
|
|
proc sstoreEvmc(c: Computation, slot, newValue: UInt256) =
|
|
let
|
|
currentValue = c.getStorage(slot)
|
|
status = c.host.setStorage(c.msg.contractAddress, slot, newValue)
|
|
gasParam = GasParams(kind: Op.Sstore, s_status: status)
|
|
gasCost = c.gasCosts[Sstore].c_handler(newValue, gasParam)[0]
|
|
|
|
c.gasMeter.consumeGas(
|
|
gasCost, &"SSTORE: {c.msg.contractAddress}[{slot}] " &
|
|
&"-> {newValue} ({currentValue})")
|
|
|
|
else:
|
|
proc sstoreImpl(c: Computation, slot, newValue: UInt256) =
|
|
let
|
|
currentValue = c.getStorage(slot)
|
|
gasParam = GasParams(
|
|
kind: Op.Sstore,
|
|
s_currentValue: currentValue)
|
|
|
|
(gasCost, gasRefund) =
|
|
c.gasCosts[Sstore].c_handler(newValue, gasParam)
|
|
|
|
c.gasMeter.consumeGas(
|
|
gasCost, &"SSTORE: {c.msg.contractAddress}[{slot}] " &
|
|
&"-> {newValue} ({currentValue})")
|
|
if gasRefund > 0:
|
|
c.gasMeter.refundGas(gasRefund)
|
|
|
|
c.vmState.mutateStateDB:
|
|
db.setStorage(c.msg.contractAddress, slot, newValue)
|
|
|
|
|
|
proc sstoreNetGasMeteringImpl(c: Computation; slot, newValue: UInt256) =
|
|
let
|
|
stateDB = c.vmState.readOnlyStateDB
|
|
currentValue = c.getStorage(slot)
|
|
|
|
gasParam = GasParams(
|
|
kind: Op.Sstore,
|
|
s_currentValue: currentValue,
|
|
s_originalValue: stateDB.getCommittedStorage(c.msg.contractAddress, slot))
|
|
|
|
(gasCost, gasRefund) = c.gasCosts[Sstore].c_handler(newValue, gasParam)
|
|
|
|
c.gasMeter.consumeGas(
|
|
gasCost, &"SSTORE EIP2200: {c.msg.contractAddress}[{slot}]" &
|
|
&" -> {newValue} ({currentValue})")
|
|
|
|
if gasRefund != 0:
|
|
c.gasMeter.refundGas(gasRefund)
|
|
|
|
c.vmState.mutateStateDB:
|
|
db.setStorage(c.msg.contractAddress, slot, newValue)
|
|
|
|
|
|
proc jumpImpl(c: Computation; jumpTarget: UInt256) =
|
|
if jumpTarget >= c.code.len.u256:
|
|
raise newException(
|
|
InvalidJumpDestination, "Invalid Jump Destination")
|
|
|
|
let jt = jumpTarget.truncate(int)
|
|
c.code.pc = jt
|
|
|
|
let nextOpcode = c.code.peek
|
|
if nextOpcode != JumpDest:
|
|
raise newException(InvalidJumpDestination, "Invalid Jump Destination")
|
|
|
|
# TODO: next check seems redundant
|
|
if not c.code.isValidOpcode(jt):
|
|
raise newException(
|
|
InvalidInstruction, "Jump resulted in invalid instruction")
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Private, op handlers implementation
|
|
# ------------------------------------------------------------------------------
|
|
|
|
const
|
|
popOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x50, Remove item from stack.
|
|
discard k.cpt.stack.popInt
|
|
|
|
mloadOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x51, Load word from memory
|
|
let (memStartPos) = k.cpt.stack.popInt(1)
|
|
|
|
let memPos = memStartPos.cleanMemRef
|
|
k.cpt.gasMeter.consumeGas(
|
|
k.cpt.gasCosts[Mload].m_handler(k.cpt.memory.len, memPos, 32),
|
|
reason = "MLOAD: GasVeryLow + memory expansion")
|
|
|
|
k.cpt.memory.extend(memPos, 32)
|
|
k.cpt.stack.push:
|
|
k.cpt.memory.read(memPos, 32)
|
|
|
|
|
|
mstoreOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x52, Save word to memory
|
|
let (memStartPos, value) = k.cpt.stack.popInt(2)
|
|
|
|
let memPos = memStartPos.cleanMemRef
|
|
k.cpt.gasMeter.consumeGas(
|
|
k.cpt.gasCosts[Mstore].m_handler(k.cpt.memory.len, memPos, 32),
|
|
reason = "MSTORE: GasVeryLow + memory expansion")
|
|
|
|
k.cpt.memory.extend(memPos, 32)
|
|
k.cpt.memory.write(memPos, value.toByteArrayBE)
|
|
|
|
|
|
mstore8Op: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x53, Save byte to memory
|
|
let (memStartPos, value) = k.cpt.stack.popInt(2)
|
|
|
|
let memPos = memStartPos.cleanMemRef
|
|
k.cpt.gasMeter.consumeGas(
|
|
k.cpt.gasCosts[Mstore].m_handler(k.cpt.memory.len, memPos, 1),
|
|
reason = "MSTORE8: GasVeryLow + memory expansion")
|
|
|
|
k.cpt.memory.extend(memPos, 1)
|
|
k.cpt.memory.write(memPos, [value.toByteArrayBE[31]])
|
|
|
|
# -------
|
|
|
|
sloadOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x54, Load word from storage.
|
|
let cpt = k.cpt # so it can safely be captured by the asyncChainTo closure below
|
|
let (slot) = cpt.stack.popInt(1)
|
|
cpt.asyncChainTo(asyncFactory(cpt).lazyDataSource.ifNecessaryGetStorage(cpt, slot)):
|
|
cpt.stack.push:
|
|
cpt.getStorage(slot)
|
|
|
|
sloadEIP2929Op: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x54, EIP2929: Load word from storage for Berlin and later
|
|
let (slot) = k.cpt.stack.popInt(1)
|
|
|
|
when evmc_enabled:
|
|
let gasCost = if k.cpt.host.accessStorage(k.cpt.msg.contractAddress, slot) == EVMC_ACCESS_COLD:
|
|
ColdSloadCost
|
|
else:
|
|
WarmStorageReadCost
|
|
k.cpt.gasMeter.consumeGas(gasCost, reason = "sloadEIP2929")
|
|
else:
|
|
k.cpt.vmState.mutateStateDB:
|
|
let gasCost = if not db.inAccessList(k.cpt.msg.contractAddress, slot):
|
|
db.accessList(k.cpt.msg.contractAddress, slot)
|
|
ColdSloadCost
|
|
else:
|
|
WarmStorageReadCost
|
|
k.cpt.gasMeter.consumeGas(gasCost, reason = "sloadEIP2929")
|
|
k.cpt.stack.push:
|
|
k.cpt.getStorage(slot)
|
|
|
|
# -------
|
|
|
|
sstoreOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x55, Save word to storage.
|
|
let (slot, newValue) = k.cpt.stack.popInt(2)
|
|
|
|
checkInStaticContext(k.cpt)
|
|
when evmc_enabled:
|
|
sstoreEvmc(k.cpt, slot, newValue)
|
|
else:
|
|
sstoreImpl(k.cpt, slot, newValue)
|
|
|
|
|
|
sstoreEIP1283Op: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x55, EIP1283: sstore for Constantinople and later
|
|
let (slot, newValue) = k.cpt.stack.popInt(2)
|
|
|
|
checkInStaticContext(k.cpt)
|
|
when evmc_enabled:
|
|
sstoreEvmc(k.cpt, slot, newValue)
|
|
else:
|
|
sstoreNetGasMeteringImpl(k.cpt, slot, newValue)
|
|
|
|
|
|
sstoreEIP2200Op: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x55, EIP2200: sstore for Istanbul and later
|
|
let (slot, newValue) = k.cpt.stack.popInt(2)
|
|
|
|
checkInStaticContext(k.cpt)
|
|
const SentryGasEIP2200 = 2300
|
|
|
|
if k.cpt.gasMeter.gasRemaining <= SentryGasEIP2200:
|
|
raise newException(
|
|
OutOfGas,
|
|
"Gas not enough to perform EIP2200 SSTORE")
|
|
|
|
when evmc_enabled:
|
|
sstoreEvmc(k.cpt, slot, newValue)
|
|
else:
|
|
sstoreNetGasMeteringImpl(k.cpt, slot, newValue)
|
|
|
|
|
|
sstoreEIP2929Op: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x55, EIP2929: sstore for Berlin and later
|
|
let (slot, newValue) = k.cpt.stack.popInt(2)
|
|
checkInStaticContext(k.cpt)
|
|
|
|
# Minimum gas required to be present for an SSTORE call, not consumed
|
|
const SentryGasEIP2200 = 2300
|
|
|
|
if k.cpt.gasMeter.gasRemaining <= SentryGasEIP2200:
|
|
raise newException(OutOfGas, "Gas not enough to perform EIP2200 SSTORE")
|
|
|
|
when evmc_enabled:
|
|
if k.cpt.host.accessStorage(k.cpt.msg.contractAddress, slot) == EVMC_ACCESS_COLD:
|
|
k.cpt.gasMeter.consumeGas(ColdSloadCost, reason = "sstoreEIP2929")
|
|
else:
|
|
k.cpt.vmState.mutateStateDB:
|
|
if not db.inAccessList(k.cpt.msg.contractAddress, slot):
|
|
db.accessList(k.cpt.msg.contractAddress, slot)
|
|
k.cpt.gasMeter.consumeGas(ColdSloadCost, reason = "sstoreEIP2929")
|
|
|
|
when evmc_enabled:
|
|
sstoreEvmc(k.cpt, slot, newValue)
|
|
else:
|
|
sstoreNetGasMeteringImpl(k.cpt, slot, newValue)
|
|
|
|
# -------
|
|
|
|
jumpOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x56, Alter the program counter
|
|
let (jumpTarget) = k.cpt.stack.popInt(1)
|
|
jumpImpl(k.cpt, jumpTarget)
|
|
|
|
jumpIOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x57, Conditionally alter the program counter.
|
|
let (jumpTarget, testedValue) = k.cpt.stack.popInt(2)
|
|
if testedValue != 0:
|
|
jumpImpl(k.cpt, jumpTarget)
|
|
|
|
pcOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x58, Get the value of the program counter prior to the increment
|
|
## corresponding to this instruction.
|
|
k.cpt.stack.push:
|
|
max(k.cpt.code.pc - 1, 0)
|
|
|
|
msizeOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x59, Get the size of active memory in bytes.
|
|
k.cpt.stack.push:
|
|
k.cpt.memory.len
|
|
|
|
gasOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x5a, Get the amount of available gas, including the corresponding
|
|
## reduction for the cost of this instruction.
|
|
k.cpt.stack.push:
|
|
k.cpt.gasMeter.gasRemaining
|
|
|
|
jumpDestOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x5b, Mark a valid destination for jumps. This operation has no effect
|
|
## on machine state during execution.
|
|
discard
|
|
|
|
#[
|
|
EIP-2315: temporary disabled
|
|
Reason : not included in berlin hard fork
|
|
beginSubOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x5c, Marks the entry point to a subroutine
|
|
raise newException(
|
|
OutOfGas,
|
|
"Abort: Attempt to execute BeginSub opcode")
|
|
|
|
|
|
returnSubOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x5d, Returns control to the caller of a subroutine.
|
|
if k.cpt.returnStack.len == 0:
|
|
raise newException(
|
|
OutOfGas,
|
|
"Abort: invalid returnStack during ReturnSub")
|
|
k.cpt.code.pc = k.cpt.returnStack.pop()
|
|
|
|
|
|
jumpSubOp: Vm2OpFn = proc (k: var Vm2Ctx) =
|
|
## 0x5e, Transfers control to a subroutine.
|
|
let (jumpTarget) = k.cpt.stack.popInt(1)
|
|
|
|
if jumpTarget >= k.cpt.code.len.u256:
|
|
raise newException(
|
|
InvalidJumpDestination, "JumpSub destination exceeds code len")
|
|
|
|
let returnPC = k.cpt.code.pc
|
|
let jt = jumpTarget.truncate(int)
|
|
k.cpt.code.pc = jt
|
|
|
|
let nextOpcode = k.cpt.code.peek
|
|
if nextOpcode != BeginSub:
|
|
raise newException(
|
|
InvalidJumpDestination, "Invalid JumpSub destination")
|
|
|
|
if k.cpt.returnStack.len == 1023:
|
|
raise newException(
|
|
FullStack, "Out of returnStack")
|
|
|
|
k.cpt.returnStack.add returnPC
|
|
inc k.cpt.code.pc
|
|
]#
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# Public, op exec table entries
|
|
# ------------------------------------------------------------------------------
|
|
|
|
const
|
|
vm2OpExecMemory*: seq[Vm2OpExec] = @[
|
|
|
|
(opCode: Pop, ## x50, Remove item from stack
|
|
forks: Vm2OpAllForks,
|
|
name: "pop",
|
|
info: "Remove item from stack",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: popOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Mload, ## 0x51, Load word from memory
|
|
forks: Vm2OpAllForks,
|
|
name: "mload",
|
|
info: "Load word from memory",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: mloadOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Mstore, ## 0x52, Save word to memory
|
|
forks: Vm2OpAllForks,
|
|
name: "mstore",
|
|
info: "Save word to memory",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: mstoreOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Mstore8, ## 0x53, Save byte to memory
|
|
forks: Vm2OpAllForks,
|
|
name: "mstore8",
|
|
info: "Save byte to memory",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: mstore8Op,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Sload, ## 0x54, Load word from storage
|
|
forks: Vm2OpAllForks - Vm2OpBerlinAndLater,
|
|
name: "sload",
|
|
info: "Load word from storage",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: sloadOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Sload, ## 0x54, sload for Berlin and later
|
|
forks: Vm2OpBerlinAndLater,
|
|
name: "sloadEIP2929",
|
|
info: "EIP2929: sload for Berlin and later",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: sloadEIP2929Op,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Sstore, ## 0x55, Save word
|
|
forks: Vm2OpAllForks - Vm2OpConstantinopleAndLater,
|
|
name: "sstore",
|
|
info: "Save word to storage",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: sstoreOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Sstore, ## 0x55, sstore for Constantinople and later
|
|
forks: Vm2OpConstantinopleAndLater - Vm2OpPetersburgAndLater,
|
|
name: "sstoreEIP1283",
|
|
info: "EIP1283: sstore for Constantinople and later",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: sstoreEIP1283Op,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Sstore, ## 0x55, sstore for Petersburg and later
|
|
forks: Vm2OpPetersburgAndLater - Vm2OpIstanbulAndLater,
|
|
name: "sstore",
|
|
info: "sstore for Constantinople and later",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: sstoreOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Sstore, ## 0x55, sstore for Istanbul and later
|
|
forks: Vm2OpIstanbulAndLater - Vm2OpBerlinAndLater,
|
|
name: "sstoreEIP2200",
|
|
info: "EIP2200: sstore for Istanbul and later",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: sstoreEIP2200Op,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Sstore, ## 0x55, sstore for Berlin and later
|
|
forks: Vm2OpBerlinAndLater,
|
|
name: "sstoreEIP2929",
|
|
info: "EIP2929: sstore for Istanbul and later",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: sstoreEIP2929Op,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Jump, ## 0x56, Jump
|
|
forks: Vm2OpAllForks,
|
|
name: "jump",
|
|
info: "Alter the program counter",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: jumpOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: JumpI, ## 0x57, Conditional jump
|
|
forks: Vm2OpAllForks,
|
|
name: "jumpI",
|
|
info: "Conditionally alter the program counter",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: jumpIOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Pc, ## 0x58, Program counter prior to instruction
|
|
forks: Vm2OpAllForks,
|
|
name: "pc",
|
|
info: "Get the value of the program counter prior to the increment "&
|
|
"corresponding to this instruction",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: pcOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Msize, ## 0x59, Memory size
|
|
forks: Vm2OpAllForks,
|
|
name: "msize",
|
|
info: "Get the size of active memory in bytes",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: msizeOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: Gas, ## 0x5a, Get available gas
|
|
forks: Vm2OpAllForks,
|
|
name: "gas",
|
|
info: "Get the amount of available gas, including the corresponding "&
|
|
"reduction for the cost of this instruction",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: gasOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: JumpDest, ## 0x5b, Mark jump target. This operation has no effect
|
|
## on machine state during execution
|
|
forks: Vm2OpAllForks,
|
|
name: "jumpDest",
|
|
info: "Mark a valid destination for jumps",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: jumpDestOp,
|
|
post: vm2OpIgnore))]
|
|
|
|
#[
|
|
EIP-2315: temporary disabled
|
|
Reason : not included in berlin hard fork
|
|
(opCode: BeginSub, ## 0x5c, Begin subroutine
|
|
forks: Vm2OpBerlinAndLater,
|
|
name: "beginSub",
|
|
info: " Marks the entry point to a subroutine",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: beginSubOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: ReturnSub, ## 0x5d, Return
|
|
forks: Vm2OpBerlinAndLater,
|
|
name: "returnSub",
|
|
info: "Returns control to the caller of a subroutine",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: returnSubOp,
|
|
post: vm2OpIgnore)),
|
|
|
|
(opCode: JumpSub, ## 0x5e, Call subroutine
|
|
forks: Vm2OpBerlinAndLater,
|
|
name: "jumpSub",
|
|
info: "Transfers control to a subroutine",
|
|
exec: (prep: vm2OpIgnore,
|
|
run: jumpSubOp,
|
|
post: vm2OpIgnore))]
|
|
]#
|
|
|
|
# ------------------------------------------------------------------------------
|
|
# End
|
|
# ------------------------------------------------------------------------------
|