Meplang - An EVM low-level language
Meplang is a low-level programming language that produces EVM bytecode. It is designed for developers who need full control over the control flow in their smart contracts.
Meplang is a low-level language and is not meant for complex smart contract development. It is recommended to use more high-level languages like Solidity or Vyper for that.
Please note that the work on Meplang is still in progress, and users should always verify that the output bytecode is as expected before deployment.
Installation
-
Install Rust on your machine.
-
Run the following to build the Meplang compiler from source:
cargo install --git https://github.com/meppent/meplang.git
To update from source, run the same command again.
Hello World
Here is an example of a simple Meplang contract, that returns "Hello World!" as bytes:
contract HelloWorld {
block main {
// copy the bytes into memory
push(hello_world.size) push(hello_world.pc) push(0x) codecopy
// return them
push(hello_world.size) push(0x) return
}
block hello_world {
// "Hello World!" as bytes
0x48656c6c6f20576f726c6421
}
}
To compile this contract saved as hello_world.mep
, run the following command:
meplang compile -contract HelloWorld -input hello_world.mep
Or the shortened version:
meplang compile -c HelloWorld -i hello_world.mep
This will print the runtime bytecode in the terminal. To save the output as in a file, use the argument -o
or -output
:
meplang compile -c HelloWorld -i hello_world.mep -o runtime.bytecode
Deployment bytecode
The compilation gives the runtime bytecode of the smart contract. To get the deployment contract, use an auxilliary contract, and compile it:
contract Constructor {
block main {
// copy the bytes into memory
push(deployed.size) push(deployed.pc) push(0x) codecopy
// return them
push(deployed.size) push(0x) return
}
block deployed {
&Deployed.code
}
}
// the contract that will be deployed
contract Deployed {
block main {}
}
Compile the contract Constructor
to get the deployment bytecode of the contract Deployed
.
Basic syntax
- A contract is declared with the keyword
contract
. Many contracts can be defined in a single file. A contract can copy the runtime bytecode of another contract using&Contract.code
inside a block. - A block is declared inside a contract using the keyword
block
. A block can be defined abstract (see later) using the keywordabstract
beforeblock
. The first opcodes of the contract are from the necessary block namedmain
(or a block surrounded by the attribute#[main]
). - A constant is declared inside a contract using the keyword
const
. Constants can only be used inside a functionpush
inside a block.
contract Contract {
const balance_of_selector = 0x70a08231;
const weth_address = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
#[assume(msize = 0x00)]
block main {
push(balance_of_selector) push(0x) mstore // mem[0x1c..0x20] = 0x70a08231
#[assume(msize = 0x20)]
address push(0x20) mstore // mem[0x20..0x40] = address(this)
#[assume(msize = 0x40)]
// mem[0x00..0x20] = weth.call{value: 0, gas: gas()}(mem[0x1c..0x20])
push(0x20) push(0x1c) push(0x24) push(0x) push(0x) push(weth_address) gas call
// the contract's balance in weth is stored at mem[0x00..0x20]
}
}
- Inside a block, any opcode can be used except PUSH1 to PUSH32 opcodes. Raw bytecode can also be used as is. A value can be pushed using the function
push
, which can take an hexadecimal literal, a constant, a non-abstract block PC or size as an argument. Only values inside apush
function will be optimized by the compiler.
contract Contract {
const magic_number = 0xff;
#[assume(msize = 0x00)]
block main {
push(magic_number) push(0x) mstore
#[assume(msize = 0x20)]
push(0x20) // can be replaced by the opcode `msize` during the compilation
0x6020 // won't be changed at the compilation
push(end_block.size) // will be replaced by the actual size of the block `end_block`
push(end_block.pc) // will be replaced by the actual pc of the beginning of the block `end_block`
jump
}
block end_block {
jumpdest // do not forget to begin with jumpdest if we can jump on this block
push(0x) push(0x) return
}
}
- A non-abstract block can be copied at most once inside another block using the opreator
*
. An abstract block can be copied as many times as desired inside other blocks using the operator&
. Therefore, we cannot refer to thepc
or to thesize
of an abstract block, because it may appear multiple times in the bytecode, and not be compiled the same every time.
contract Contract {
#[assume(msize = 0x00)]
block main {
callvalue &shift_right_20_bytes // will most certainly be compiled `callvalue push1 0x20 shr`
push(0x) push(0x) mstore
#[assume(msize = 0x20)]
callvalue &shift_right_20_bytes // will most certainly be compiled `callvalue msize shr` because we assumed msize = 0x20.
*end_block
}
abstract block shift_right_20_bytes {
push(0x20) shr
}
block end_block {
// no jumpdest here because we do not jump on this block, we copy it
push(0x) push(0x) return
}
}
- Many attributes exist to guide the compiler. They are declared over a contract, a block, or a line inside a block using the syntax
#[ATTRIBUTE]
. The current list of existing attributes is:assume
to tell the compiler that from this point, an opcode will push on the stack a defined value. The compiler can then replace somepush
opcodes with these assumptions.clear_assume
to clear an assumption made previously.main
the main block can be marked with this attribute if it is not namedmain
.last
to tell the compiler that the block must be placed at the end of the bytecode.keep
to tell the compiler that this block must be kept somewhere in the bytecode even if it is unused.
More examples of contracts can be found in the folder examples.
Future features
assert
attribute to impose conditions on a block pc or a contract size.- Heuristics to improve compilation optimizations.
- Inheritance of contracts.