Script Machine Registers

Seeking some thoughts/feedback on this idea. This may or may not be already implemented in other coins, it is not exactly a novel idea.
I was looking at some cashscript stuff and it occurred to me that the variable abstraction would be a lot easier to work with if the script machine had some direct access data storage instead of relying on purely sequential access.

I named this script machine registers because the naming convention for the opcodes matches the LOAD and STORE assembley instructions that load/store data to a register.

This is not meant to be a formal chip.

Title: Script Machine Registers
Maintainer: Griffith
Status: Draft
Initial Publication Date: 2024-07-13
Latest Revision Date: 2024-07-13
Version: 0.1

Script Machine Registers

How would they work

By using two opcodes LOAD and STORE we can save up to X variables into script machine registers for easier use later in the script as a more performant alternative to ROLL and PICK. Initially allow for up to 8 (arbitrary value) registers to be used

script machine registers implemented as std::array<StackItem, 8> vRegisters; in the script machine.

OP_STORE implementation

const StackItem &register_value = stackItemAt(-2);
int64_t register_num CScriptNum(stacktop(-1), fRequireMinimal, maxIntegerSize).getint64();
// in the script machine there are only 8 registers (0-7)
// a register number higher than this a failure
if register_num > 7:
    script failure

// store the value in the register
vRegisters[register_num] = register_value
PopStack(); // pop the register number off the stack
PopStack(); // pop the stored value off the stack

Example:

push 10
push 5
OP_STORE # stores the value 10 into register #5

OP_LOAD implementation

int64_t register_num CScriptNum(stacktop(-1), fRequireMinimal, maxIntegerSize).getint64();
// in the script machine there are only 8 registers (0-7)
// a register number higher than this a failure
if register_num > 7:
    script failure

// load the value in the register
const StackItem register_value& = vRegisters[register_num];
PopStack(); // pop the register number off the stack
PushStack(value) // push the loaded value on to the stack

Example:

push 5
OP_LOAD # pops the top stack item (5) and replaces it with the contents of that register

TODO - consider clearing the register after the value is pushed on to the stack?

Final script state

It is not required that all registers be empty for the script to complete successfully. If values remain when the script is finished, they are cleared/ignored

Changelog

3 Likes

Nice! It would make it easier for anyone coding contracts in raw Script, as stack juggling is a big mental overhead :slight_smile:. It would also reduce script sizes as you’d likely need less opcodes for equivalent operation using current set of opcodes.

I saw a proposal for OP_UNROLL too, or just tweak OP_ROLL so negative index would move the current stack item down the stack.

I wonder, what would the efficiency gains be between status quo and unroll/negroll, and then between unroll/negroll and load/unload.

1 Like

It’s worth noting that this feature is being added to the Nexa blockchain (and being activated March 15th 2025) :grinning_face_with_smiling_eyes:

In the Nexa 1.4.1.0 changelog on Gitlab you find the following:

Implement script machine registers: OP_STORE , OP_LOAD (/doc/script_registers.md)
Implement negative indexes for OP_ROLL/OP_PICK (/doc/negativeRollAndPick.md)

So they are also adding @bitcoincashautist’s mentioned alternative of negative roll/pick indexes.

1 Like

Yes. by me. i specced and wrote both of those.

I was also unaware BCHA mentioned negative indexes somewhere. Where was that?

Two posts above yours.

oh. woops. XD
face palm

When writing CashScript users don’t have to know how items on the stack are accessed, so the developer experience for developing in high-level languages wouldn’t change.

What would change with either machine registers or more powerful deep stack manipulation opcodes is that BCH compilers could create more optimized bytecode. Given that it is a bytesize optimization, we’d need to consider how many bytes can be saved depending on the depth of the stackitem.

The need for more granular stack manipulation opcodes is not something I’ve head as often from developers working with script directly.

Would be interested to hear what @bitjson thinks

It’s been a few years since I thought much about stack juggling minimization. IIRC, BCH compilers just need to implement long-known algorithms for optimizing bytecode order – registers don’t ultimately save any bytes, they just avoid the need for some compiler optimizations up to some level of complexity (based on register count). In our case, registers are probably even be a bit worse since we’d presumably use two bytes for each fetch/store (to take advantage of our existing OP_1NEGATE through OP_16) rather than e.g. dedicating N new codepoints for each OP_STORE1/OP_FETCH0, … OP_STORE15/OP_FETCH15.

I haven’t spent much time trying to conclusively prove one way or the other, but my impression is still that we’d ultimately get better contract optimization by implementing those well studied compiler optimizations vs. adding more opcodes. E.g. adding more opcodes may simply increase the number of possible equal-length solutions rather than enabling new, shorter solutions. (But I’d certainly be interested in someone deeply reviewing the subject and quantitatively demonstrating improvements! OP_UNROLL seems particularly plausible to occasionally save bytes.)

By the way – I already shared this in the Bitauth IDE telegram – I’ve been planning on integrating this simple stack optimizer directly into Bitauth IDE for a while but still haven’t gotten around to it. Just publishing it on GitHub pages so it’s easier for others to find and use in the meantime:

https://bitjson.github.io/bch-wizard/

1 Like

For anyone interested, I tried ChatGPT’s o1 pro “deep research” on the topic here:

Archive: https://archive.is/OItoD

true. it probably helps a lot more with the mental model rather than actual script execution and script size.

But you should not have to load a value from a register back on to the stack every time you want to use it. should also be possible to perform operations directly on registers if the op_codes for that are available.