Instructions Guide
Overview
Many of you are using the SGP-Lite framework and will be needing to make new instructions. This guide is aimed at explaining the syntax for doing so.
Instruction syntax
The general syntax of an instruction is the following:
struct NAME { //struct is similar to class but everything is default public
template <typename Spec>
static void run(sgpl::Core<Spec> &core, const sgpl::Instruction<Spec> &inst,
const sgpl::Program<Spec> &,
typename Spec::peripheral_t &state) noexcept {
//Whatever you want to happen when the instruction is executed by the organism
}
static std::string name() { return "NAME"; } //Whatever you want the human readable name to be
static size_t prevalence() { return 1; } //How prevalent you want this instruction to be relative to others, 1 is a good value
}
Parameters
There are quite a few complicated parameters that an instruction’s run
has access to and you might want to use. The provided IOInstruction
demonstrates using the important ones:
uint32_t output = core.registers[inst.args[0]];
To access the values in the organism’s registers, you get the registers through the core
with core.registers
. This is basically an array so you can access it with []
as you would expect.
Typically you want to access the register that the instruction specified.
You can access information about the instruction that the organism just executed through the inst
parameter and specifically find out about which of the registers the instruction was trying to act on by indexing into inst.args
.
For example, if the organism had just done io r3
, inst.args[0]
would return the r3
register information, which is then used to look up what is in that register with core.registers[inst.args[0]]
.
You can also make changes to what is in any of the organism’s registers, for example when IO gets a new random number and puts it into the same register:
uint32_t input = sgpl::tlrand.Get().GetUInt();
core.registers[inst.args[0]] = input;
You can access your World
subclass through the state
parameter with state.world
, for instance when IO checks if the organism solved a task with CheckOutput
:
state.world->CheckOutput(output, state);
Adding to library
Once you’ve made your new instructions, you just need to add them to the type declaration for your Library
at the end of Instructions.h
:
using Library = sgpl::OpLibraryCoupler<sgpl::CompleteOpLibrary, IOInstruction,
ReproduceInstruction, NewInstruction, OtherNewInstruction>;
//Just add a comma and your instruction name with the <>
Printing nicely
Since the printing of the genome is a custom method in the starter code, any instructions you add, including using more of the SGP-Lite provided instructions, will need to be added to the print genome functionality.
In CPU.h
, there is a method PrintGenome
that creates a map of instruction names and how many registers the instruction uses (“arity”). To add a new instruction, just add a new pair to this map:
std::map<std::string, size_t> arities{{"Multiply", 3}, {"Add", 3},
{"Subtract", 3}, {"Divide", 3},
{"IO", 1}, {"Reproduce", 0},
{"NEW", 3}}; //NEW instruction uses 3 registers
Avida Instruction Set
To get close to the original default Avida instruction set, I recommend grabbing the following from SGP-Lite (it’s not a perfect list or a perfect match, but will get your organisms started, feel free to play around with others from the CompleteOpLibrary):
- sgpl::Nop<0>
- sgpl::Nop<1>
- sgpl::Nop<2>
- sgpl::BitwiseShift
- sgpl::Increment
- sgpl::Decrement
- sgpl::Add
- sgpl::Subtract
- sgpl::global::JumpIfNot
- sgpl::local::JumpIfNot
- sgpl::global::Anchor
And making a Nand
instruction, with the following internal logic:
uint32_t reg_b = core.registers[inst.args[1]]; //grab value from second register
uint32_t reg_c = core.registers[inst.args[2]]; //grab value from third register
uint32_t nand_val = ~(reg_b & reg_c); //compute NAND
core.registers[inst.args[0]] = nand_val; //place NAND value in first register