3 个版本
0.1.8 | 2024 年 7 月 4 日 |
---|---|
0.1.7 | 2024 年 1 月 6 日 |
0.1.6 | 2023 年 11 月 2 日 |
#45 in #control-flow
54 个月下载量
120KB
2.5K SLoC
Meplang - 一种 EVM 低级语言
Meplang 是一种生成 EVM 字节码的低级编程语言。它为需要对其智能合约中的控制流有完全控制的开发者而设计。
Meplang 是一种低级语言,并不适合复杂智能合约的开发。对于这类开发,建议使用更高级的语言,如 Solidity 或 Yul。
请注意,Meplang 的工作仍在进行中,用户在部署之前应始终验证输出字节码是否符合预期。
安装
-
在您的机器上安装 Rust。
-
运行以下命令从源代码构建 Meplang 编译器
cargo install --git https://github.com/meppent/meplang.git
要从源代码更新,再次运行相同的命令。
你好,世界!
这是一个简单的 Meplang 合约示例,它以字节形式返回 "Hello World!"
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
}
}
要编译保存为 hello_world.mep
的合约,运行以下命令
meplang compile -contract HelloWorld -input hello_world.mep
或简化的版本
meplang compile -c HelloWorld -i hello_world.mep
这将打印终端中的运行时字节码。要导出编译工件(包括运行时字节码),请使用参数 -o
或 -output
meplang compile -c HelloWorld -i hello_world.mep -o hello_world.json
部署字节码
编译生成智能合约的运行时字节码。要获取部署合约,请使用辅助合约,并将其编译
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 {
// ...
}
}
编译合约 Constructor
以获取合约 Deployed
的部署字节码。
基本语法
- 使用关键字
contract
声明 合约。可以在单个文件中定义多个合约。一个合约可以在块中使用&Contract.code
复制另一个合约的运行时字节码。 - 在合约中使用关键字
block
声明一个块。块可以定义为 抽象(见后文),在block
前使用关键字abstract
。合约的第一个操作码来自必要的块main
(或带有属性#[main]
的块)。 - 使用关键字
const
在合约中声明一个常量。常量只能用在块内的函数push
中。
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
}
}
- 在块内,可以使用任何操作码 除了 PUSH1 到 PUSH32 操作码(PUSH0 是允许的)。也可以直接使用原始字节码。可以使用函数
push
来推送一个值,该函数可以接受十六进制字面量、常量、非抽象 块程序计数器或大小作为参数。只有push
函数内的值才会被编译器优化。
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
}
}
- 非抽象 块可以在另一个块中使用操作符
*
至多复制一次。一个 抽象 块可以在其他块中使用操作符&
复制任意次数。因此,我们无法引用 抽象 块的pc
或size
,因为它可能在字节码中出现多次,并且每次编译可能不同。
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]
在合约、块或块内的行上声明。现有属性列表如下assume
告诉编译器从这一点开始,一个操作码将推送一个定义的值到栈上。然后编译器可以用这些假设替换一些push
操作码。clear_assume
用于清除之前的假设。main
如果主块没有命名为main
,则可以标记此属性。last
告诉编译器该块必须放在字节码的末尾。keep
告诉编译器即使该块未使用,也必须在字节码的某处保留该块。
更多合约示例可以在文件夹 examples 中找到。
未来功能
assert
属性可以对块程序计数器或合约大小施加条件。- 启发式算法以提高编译优化。
- 合约继承。
依赖
~12MB
~197K SLoC