Trim 是一种面向 EVM 的、基于操作码的编程语言,它提供了一种更可读的方式来编写高度优化的代码,而不会引入额外的复杂性。它具有 S 表达式、字符串、标签、宏等特性,可以更方便地编写智能合约。文章介绍了 Trim 的基本使用、语法、特性和宏,并展示了如何使用 Trim 编写智能合约。
Trim 是一种面向以太坊虚拟机 (EVM) 的 opcode 编程语言。它提供了一种以更可读的方式编写高度优化代码的语法,而不会引入精神或复杂性负担。
首先安装 Trim(以及 ethereumjs/vm,因为你可能会使用它的 opcode 定义)
npm install trim-evm
然后使用二进制文件...
% trim myfile.trim
0x6100...
% echo '(ADD 0x00 0x01)' | trim
0x6001600001
...或者导入并编译:
import { trim } from 'trim-evm'
const bytecode = trim`(ADD 0x00 0x01)`
console.log("编译成功!生成的字节码:", bytecode)
如果需要配置 opcodes:
import { trim } from 'trim-evm'
const bytecode = trim.compile(trim.source`(ADD 0x00 0x01)`, {
opcodes: ...,
opcodesMetadata: ...,
})
这是一个模板,可以开始使用 Trim 编写完整的智能合约:
(init-runtime-code)
##runtime
(CALLDATACOPY 0x1c 0x00 0x04)
(MLOAD 0x00) ; 将函数 id 复制到堆栈上
(EQ (abi/fn-selector "hello()") DUP1)
(JUMPI #hello _)
REVERT ; 没有匹配的函数 id
##hello
(MSTORE 0x00 "Hello, world!")
(RETURN 0x00 0x20)
首先,Trim 是裸汇编的超集。你总是可以用简单的方式编写 opcodes。例如,这是有效的 Trim 代码:
PUSH1 0x20
PUSH2 0x1000
ADD
MLOAD
Trim 引入的是 s-expressions。 S-expression 允许你以 opcode-arguments 符号编写:
(MLOAD (ADD 0x1000 0x20))
此代码等效于前面的示例。
你也可以使用顶部运算符 (_
) 来引用堆栈的顶部。以下示例都是等效的:
; 示例 A
PUSH1 0x20
(ADD 0x1000 _)
(MLOAD _)
; 示例 B
(ADD 0x1000 0x20)
(MLOAD _)
; 示例 C
(ADD 0x1000 0x20)
MLOAD
请注意,你不必用 s-expressions 编写 所有 代码。
当你编写 Trim s-expression 时,你将有权访问一些特性。除了定义标签外,这些特性只能在 s-expressions 中访问。
Trim 允许你编写双引号字符串文字。例如:
; 旧方法
PUSH12 0x48656c6c6f2c205472696d21
EQ
; 新方法
(EQ "Hello, Trim!" _)
部署 EVM 智能合约时,你会将 初始化代码 和 运行时代码 作为一段长字节序列一起部署。在此部署交易期间,EVM 将 运行 此代码,获取其 返回值,然后将此返回值 持久化 到区块链。换句话说,返回的值是字节码,该字节码将在以后对新合约地址进行交易时始终运行。
不幸的是,为此任务用普通 opcodes 编写代码非常麻烦,因为它涉及手动计算字节并将这些数字硬编码到你的代码中。更糟糕的是,如果在开发过程中添加或删除代码行,你将必须重新计数并更新这些硬编码的数字,然后才能再次测试它。
Trim 通过引入 标签 来解决这个问题:
(SUB CODESIZE #runtime)
DUP1
(CODECOPY 0x00 #runtime _)
(RETURN 0x00 _)
##runtime
(MSTORE 0x00 "Hello, world!")
(RETURN 0x00 0x20)
在上面的代码中,第 6 行是 标签定义,最终为 15 个字节:
#runtime
引用 3 个字节(每个引用都编译为 PUSH2
语句)0x00
2 个字节(每个引用都编译为 PUSH1
语句)#runtime
定义之前的每个其他 opcode 1 个字节。#runtime
标签Trim 将名为 #runtime
的标签视为特例。如果存在,则在 #runtime
之后 定义的所有标签将自动偏移该数量。这是为了更正运行时标签偏移所必需的,以补偿初始化代码的删除。
你可以在 Trim 中的任何位置编写十六进制(例如 0xfeed
)。
但是,Trim 还支持多种数值符号,以帮助你编写更具可读性的代码:
15
)2words
等效于 0x40
或 64
)4bytes
等效于 0x04
或 4
)所有符号在编译期间都会转换为十六进制。
Trim 有一些内置宏。它也有用户定义的宏。
在编译时进行数学计算时,你有两种选择:
math
宏编写具有自然数学运算符优先级的表达式。例如,以下两行是等效的:
(push (math 1 + 2 * 30 / 4 - 5))
(push (- (+ 1 (/ (* 2 30) 4)) 5))
两种方法都将表达式传递给 JS 运行时。结果必须为正整数。
如果你编写的表达式不会自然地收敛为整数,则可以使用以下助手:
(push (// 10 3)) ;=> 3 (整数除法)
(push (math/ceil 10 / 3)) ;=> 4
(push (math/floor 10 / 3)) ;=> 3
一个方便的宏,用于输出函数选择器(也称为 ABI 编码函数调用 的 "function id")段。 有助于运行特定于函数的代码。
(EQ (abi/fn-selector "foo()") DUP1)
(JUMPI #foo _)
; ...
##foo
; 更多代码在这里
通常,当你想推送文字值时,只需简单地编写它,例如 (ADD 0x01 0x02)
或 (EQ "abc" _)
。
但是,如果你想将字符串推送到堆栈上怎么办? 只需使用 push
宏:
("Hi") ; 错误,无效的 token
(push "Hi") ; 有效!
这是一个简单的宏,用于将标准 "将运行时代码复制到内存并返回" 部分部署智能合约 – 几乎每个合约都需要。
使用此宏,你可以使用以下模板开始编写你想要的任何合约!
(init-runtime-code)
##runtime
;; TODO: 在这里编写代码!
以上等效于:
(SUB CODESIZE #runtime)
DUP1
(CODECOPY 0x00 #runtime _)
(RETURN 0x00 _)
##runtime
;; TODO: 在这里编写代码!
你可以使用 def
宏定义自己的宏。
例如,一个常见的模式是具有函数签名到标签的查找表。 这是你通常编写的内容,没有宏:
;; 假设函数选择器已经在堆栈顶部
(EQ (abi/fn-selector "decimals()") DUP1)
(JUMPI #decimals _)
(EQ (abi/fn-selector "balanceOf(address)") DUP1)
(JUMPI #balanceOf _)
;; ...
##decimals
JUMPDEST
;; decimals() 的代码
##balanceOf
JUMPDEST
;; balanceOf(address) 的代码
如果你有很多这些代码,你可以编写一个零成本宏抽象来使代码更好一些:
(def defun (sig label)
(EQ (abi/fn-selector sig) DUP1)
(JUMPI label _))
然后,重写以前的查找表以使用它:
(defun "decimals()" #decimals)
(defun "balanceOf(address)" #balanceOf)
宏只会重写术语,因此使用宏与不使用宏之间没有运行时成本。
defconst
宏允许你定义编译时常量,这些常量可以在代码的其他地方进行插值。
基本用法:
; 定义一个常量
(defconst DECIMALS 18)
; 在表达式中使用它
(MSTORE 0x00 DECIMALS)
; 常量可以引用其他常量
(defconst ONE_TOKEN (math 10 ** DECIMALS))
; 常量可以使用任何有效的 Trim 表达式
(defconst OWNER_SLOT (keccak256 "owner.slot"))
常量在编译时进行评估,因此没有运行时开销。 它们与其他宏结合使用时特别有用:
; 定义一些常见的存储槽
(defconst OWNER_SLOT 0x00)
(defconst PAUSED_SLOT 0x01)
; 创建一个宏来检查所有权
(def require-owner ()
(revert "Unauthorized"
(ISZERO (EQ (SLOAD OWNER_SLOT) (CALLER)))))
defcounter
宏允许你定义可以在表达式中递增和使用的编译时计数器。 这对于生成数字序列或管理预定义的内存槽非常有用。
基本用法:
; 定义一个从 0 开始的计数器
(defcounter my-counter)
; 定义具有初始值的计数器
(defcounter slot 10)
; 使用计数器值
(push (my-counter)) ; 推送 0
; 递增和使用
(push (slot ++)) ; 推送 10 并在之后递增
(push (slot)) ; 推送 11
; 添加到计数器
(push (math 1word * (my-counter += 3))) ; 立即添加 3 并使用结果
一个常见的用例是以更可维护的方式管理内存槽("寄存器")。 例如:
; 定义一个用于跟踪内存槽的计数器
(defcounter reg-counter)
; 创建一个宏来定义命名的内存寄存器
(def defreg (name)
(def name () (math 1word * (reg-counter ++))))
; 定义一些命名的内存槽
(defreg $balance)
(defreg $owner)
; 使用命名的槽(它们将位于 0x00、0x20 等)
(MSTORE $balance 100)
(MSTORE $owner 0xabc...)
Trim 在编译期间评估所有计数器操作,从而在最终字节码中产生固定值。
revert
和 return
宏是一种稍微更方便的方式来退出合约执行,主要是由于其简洁的语法,可用于调试。
; 隐式返回堆栈的顶部
(return)
; 返回一个特定值(仍然从堆栈中取出,但很明确)
(return (MLOAD 0x00))
; 简单的 revert 带消息
(revert "Something went wrong")
; 条件 revert - 仅当调用者不是所有者时才 revert
(revert "Unauthorized" (ISZERO (EQ caller owner)))
这些是我们正在考虑添加到 Trim 中的一些功能。创建一个 issue 来讨论或建议更多!
tsc --watch
然后运行 npm test
node update-opcodes.js
- 原文链接: github.com/0xMacro/trim/...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!