Trim:一种面向EVM的基于操作码的编程语言

  • 0xMacro
  • 发布于 2025-03-05 19:24
  • 阅读 7

Trim 是一种面向 EVM 的、基于操作码的编程语言,它提供了一种更可读的方式来编写高度优化的代码,而不会引入额外的复杂性。它具有 S 表达式、字符串、标签、宏等特性,可以更方便地编写智能合约。文章介绍了 Trim 的基本使用、语法、特性和宏,并展示了如何使用 Trim 编写智能合约。

Trim

<p align="center"> <picture> <!-- <source media="(prefers-color-scheme: dark)" srcset="./public/trim-dark.png"/> --> <img src="./public/trim.png" alt="Trim 编程语言的 Logo" loading="lazy" decoding="async" width="440px" /> </picture> </p>

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 等效于 0x4064
  • 字节(例如 4bytes 等效于 0x044

所有符号在编译期间都会转换为十六进制。

Trim 有一些内置宏。它也有用户定义的宏。

math

在编译时进行数学计算时,你有两种选择:

  1. 直接使用每个运算符宏,或者
  2. 使用 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/fn-selector

一个方便的宏,用于输出函数选择器(也称为 ABI 编码函数调用 的 "function id")段。 有助于运行特定于函数的代码。

(EQ (abi/fn-selector "foo()") DUP1)
(JUMPI #foo _)

; ...

##foo
; 更多代码在这里

push

通常,当你想推送文字值时,只需简单地编写它,例如 (ADD 0x01 0x02)(EQ "abc" _)

但是,如果你想将字符串推送到堆栈上怎么办? 只需使用 push 宏:

("Hi")      ; 错误,无效的 token
(push "Hi") ; 有效!

init-runtime-code

这是一个简单的宏,用于将标准 "将运行时代码复制到内存并返回" 部分部署智能合约 – 几乎每个合约都需要。

使用此宏,你可以使用以下模板开始编写你想要的任何合约!

(init-runtime-code)
##runtime
;; TODO: 在这里编写代码!

以上等效于:

(SUB CODESIZE #runtime)
DUP1
(CODECOPY 0x00 #runtime _)
(RETURN 0x00 _)

##runtime
;; TODO: 在这里编写代码!

def

你可以使用 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 宏允许你定义编译时常量,这些常量可以在代码的其他地方进行插值。

基本用法:

; 定义一个常量
(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

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

revertreturn 宏是一种稍微更方便的方式来退出合约执行,主要是由于其简洁的语法,可用于调试。

; 隐式返回堆栈的顶部
(return)

; 返回一个特定值(仍然从堆栈中取出,但很明确)
(return (MLOAD 0x00))

; 简单的 revert 带消息
(revert "Something went wrong")

; 条件 revert - 仅当调用者不是所有者时才 revert
(revert "Unauthorized" (ISZERO (EQ caller owner)))

路线图

这些是我们正在考虑添加到 Trim 中的一些功能。创建一个 issue 来讨论或建议更多!

  • [x] 用户定义的宏
  • [ ] 使用宏定义标签
  • [ ] Hardhat 集成
  • [ ] 更多标准 ABI 宏
  • [ ] 导入

开发

  • 运行 tsc --watch 然后运行 npm test
  • 如果/当需要更新标准 opcode 列表时,运行 node update-opcodes.js
  • 原文链接: github.com/0xMacro/trim/...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
0xMacro
0xMacro
江湖只有他的大名,没有他的介绍。