Description
In certain cases, contracts (or collections of contracts) reuse pieces of code. For readability and reusability of complex operations it would be good to allow defining libraries/macros that live outside of a contract and can be used by different contracts.
Under the hood, the compiler would then replace the library function call statement with its bytecode.
One option would to use macros (aka string replacements). E.g.:
macro MULDIV(x, y, z) { x * y / z }
macro DO_STUFF() {
int a = 5;
}
The benefit of this is that it results in the most control over performance / bytesize for the developer. But it is also very easy for this to result in worse readability, e.g. using DO_STUFF()
would cause confusion:
contract Test() {
function spend() {
DO_STUFF()
require(a == 5); // Whoa, where did 'a' come from?
}
}
So it likely makes more sense to go with a properly typed library system, even at the cost of slightly larger / less efficient contracts (note: size can probably be brought down by optimisations in the future if needed). We also think it's likely that the opcode limit will be increased at some point in the future.
So what does a library look like? For example:
library Math {
function muldiv(int x, int y, int z) returns (int) {
return x * y / z;
}
function divmul(int x, int y, int z) returns (int) {
return x / y * z;
}
}
A library is a collection of functions, that get compiled individually. The function has parameters and can potentially return one value. This can be extended to multiple values in the future (by "upgrading" the tuple type to allow for larger tuples).
From the consuming contract's perspective:
When the library is called in a contract, the compiler treats it as a built in function call (e.g. abs()
). In other words, it puts the args on top of the stack and replaces abs()
with OP_ABS
, except OP_ABS
will be a (much) larger piece of bytecode, with more than one opcode, e.g. OP_SWAP OP_MUL OP_DIV
. The function-to-bytecode mapping is retrieved from the compiled "library artifact" (see below).
From the library's perspective:
Every function in a library is compiled independently. Compiling a library function is similar to compiling a contract with a single function, but there are a few notable differences:
- We do not remove the final
OP_VERIFY
, because we only need to do that at the end of a contract execution, not some function call - We need to add a
return
statement that preserves the top stack value and cleans the rest of the stack (known to the function) - We need to create a library artifact interface / generation process to store function inputs/outputs/bytecode per function
We also need to add import functionality, allowing importing libraries into contracts or into other libraries. For simplicity we can stick to 1 library/contract per file.
import "./Math.cash";