Crates.io | meplang |
lib.rs | meplang |
version | 0.1.8 |
source | src |
created_at | 2023-11-02 17:51:49.237238 |
updated_at | 2024-07-04 13:13:41.110857 |
description | An EVM low-level language that gives full control over the control flow of the smart contract. |
homepage | https://github.com/makcandrov/meplang |
repository | https://github.com/makcandrov/meplang |
max_upload_size | |
id | 1023030 |
size | 144,542 |
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 Yul 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.
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.
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 export the compilation artifacts (including the runtime bytecode), use the argument -o
or -output
:
meplang compile -c HelloWorld -i hello_world.mep -o hello_world.json
The compilation gives the runtime bytecode of the smart contract. To get the deployment contract, use an auxiliary 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
.
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.block
. A block can be defined abstract (see later) using the keyword abstract
before block
. The first opcodes of the contract are from the necessary block named main
(or a block surrounded by the attribute #[main]
).const
. Constants can only be used inside a function push
inside a block.contract BalanceGetter {
const BALANCE_OF_SELECTOR = 0x70a08231;
const WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
#[assume(msize = 0x00)]
#[assume(returndatasize = 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])
// = WETH.balanceOf(address(this))
push(0x20) push(0x) push(0x24) push(0x1c) push(0x) push(WETH) gas call
#[assume(returndatasize = 0x20)]
// the contract's balance in WETH is stored at mem[0x00..0x20]
push(0x20) push(0x) return
}
}
push
, which can take an hexadecimal literal, a constant, a non-abstract block PC or size as an argument. Only values inside a push
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
}
}
*
. An abstract block can be copied as many times as desired inside other blocks using the operator &
. Therefore, we cannot refer to the pc
or to the size
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
}
}
#[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 some push
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 named main
.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.
assert
attribute to impose conditions on a block pc or a contract size.