关于本书
这本书讲的是如何用 Riddle 写程序。它假设你已经会至少一门编程语言——知道什么叫类型、函数、循环和作用域——但不假设你会 Rust 或 Kotlin。
书分三层:
- 第一部分到第七部分是教程,按依赖顺序推进:先跑通一个项目,再学语言基础,然后是所有权、数据建模、错误处理、抽象机制和集合;
- 第八部分讲工程与工具:构建、宏、编辑器、FFI;
- 附录是查阅资料:当前能力边界、形式化语法和错误码。
Riddle 还是技术预览,语法和 ABI 都可能不兼容地变化。这本书只写仓库里已经实现、并能由源码、测试或命令行行为验证的东西;路线图上的功能不会写成现状。如果书里的描述和编译器的实际行为不一致,以实现为准,也欢迎直接提 issue 或 PR 修文档。
Riddle 的设计取向解释这门语言想解决什么问题;从 Rust 或 Kotlin 过来给出相似写法之间的语义差异速查。
本书的教学顺序参考了 The Rust Programming Language、Rust 语言圣经 和 Kotlin 官方文档中文版 的组织方式,但没有照搬它们的语言特性。
Riddle 的设计取向
Riddle 想验证一个问题:既然 GC 已经负责回收堆内存,移动语义、借用检查和确定性析构还有保留的价值吗?
Riddle 的答案是保留,但把标注降到最低。
Rust 用生命周期参数把引用的有效期写进类型。这套机制能表达的东西远超日常需要,代价却落在每一次编译和每一次编辑器请求上:组合子风格的代码要做大量类型体操,检查器要反复求解约束,泛型实例化还会带来子类型相关的问题。真正写业务代码时,需要三个以上生命周期字面量的场合很少。既然收益有限,Riddle 干脆不提供生命周期语法,改由编译器分析引用来源:引用可能在当前栈帧之外继续存活时,值被提升到 GC 堆,所有权和借用规则本身不变。
Go 的并发模型和工具链体验值得借鉴,但它对底层的处理是另一个极端:语言刻意保持简单,需要接触内存布局或调用 C 时只能依赖不安全的后门,标准库也因此留下了不少补丁式设计。Riddle 选择把底层能力显式隔离在 unsafe、原始指针和 FFI 里,其余部分保持静态检查。
组合起来,Riddle 的取舍是:
- 值默认移动,
Copy类型按复制处理; - 引用分共享与可变,借用冲突在编译期报错;
Drop在所有者结束时确定性执行,不依赖 GC 时机;- 存储位置由逃逸分析决定,按需提升到保守式非移动 GC 堆;
- 共享行为用 trait + impl 表达,没有类继承。
不提供的东西同样是设计的一部分:没有生命周期参数、没有显式 Box/Rc/Arc、没有异常、没有类。这些选择会在对应章节里反复出现。
Riddle 目前是单线程语言,线程、异步和网络都还没有实现。除此之外,语法和 ABI 仍会在版本之间变化。
从 Rust 或 Kotlin 过来
Riddle 借用了 Rust 和 Kotlin 里好认的写法,但关键字相同不代表语义相同。下面只比较当前已经实现的行为。
速查
| 主题 | Riddle | Rust | Kotlin |
|---|---|---|---|
| 函数 | fun add(x: i32) -> i32 | fn add(x: i32) -> i32 | fun add(x: Int): Int |
| 不可变绑定 | let value = 1; | let value = 1; | val value = 1 |
| 可变绑定 | let mut value = 1; | let mut value = 1; | var value = 1 |
| 数据建模 | struct、enum | struct、enum | class、data class、enum class |
| 共享行为 | trait + impl | trait + impl | 接口、继承、扩展函数 |
| 分支匹配 | match | match | when |
| 可恢复失败 | Option、Result、? | Option、Result、? | 可空类型与异常 |
| 可增长序列 | Vector<T> | Vec<T> | MutableList<T> |
| 项目工具 | clue + Clue.toml | Cargo | Gradle / Maven |
相对 Rust
没有生命周期参数。 编译器自己追踪引用来源,判断值能不能留在栈上;引用需要活过当前栈帧时,值被提升到 GC 堆。移动后使用、借用冲突和 Drop 时序仍然按静态规则检查,GC 只影响存储位置。
没有 Box、Rc、Arc。 堆存储是自动的,共享一个值仍然靠引用,不靠引用计数。裸 dyn Trait 本身就是拥有所有权的值:开 GC 时分配在 GC 堆,关 GC 时走 riddle_alloc / riddle_free。
没有生命周期,也就没有生命周期标注带来的类型体操,泛型和方法解析更简单,代价是表达力下降:需要精确控制值何时被回收时,只能靠作用域和 Drop。
声明式宏(macro_rules!)不存在。 需要生成代码时写过程宏,见编写过程宏。
相对 Kotlin
fun 相同,块的返回值规则不同。 函数体的最后一个表达式就是返回值,不需要 return:
fun double(value: i32) -> i32 {
value * 2
}
Kotlin 里等价写法要么用 return,要么写成单表达式函数。别因为都叫 fun 就照搬函数体规则。
没有类、可空类型和异常。 数据用结构体,共享行为用 trait 和 impl。可能缺失的值用 Option<T>,可恢复失败用 Result<T, E>,不可恢复路径用 panic。没有 null、throw、try、catch。
值会移动。 把非 Copy 值传出去之后原绑定就不能再用。提升到 GC 堆不会把值变成可以随便共享的对象,也不会取消借用检查。实现 Drop 的值在所有者结束时析构。
平台支持只有 C11 后端。 能否链接由目标组件和本机 C 工具链决定,没有 JVM、JS 或 Wasm 目标。
读外部教程时
Rust 和 Kotlin 的资料可以借来理解概念,但每段代码都要按形式化语法和当前工具链状态重新确认。名字相同的地方,边界条件往往不同。
安装 Riddle 工具链
工具链是四个可执行文件:clue(项目与依赖)、riddle(fmt / run / repl)、riddlec(编译器)、riddle-lsp(语言服务器)。当前版本 0.3.0,预编译包和源码构建两种装法都可以。
下载预编译包
GitHub Releases 为 7 个宿主平台各提供一个 zip,文件名形如 riddle-v<version>-<platform>-<arch>.zip:
| 文件名 | 宿主 |
|---|---|
riddle-v0.3.0-linux-x86_64.zip | x86_64 Linux |
riddle-v0.3.0-linux-aarch64.zip | aarch64 Linux |
riddle-v0.3.0-linux-i686.zip | i686 Linux |
riddle-v0.3.0-windows-x86_64.zip | x86_64 Windows |
riddle-v0.3.0-windows-i686.zip | i686 Windows |
riddle-v0.3.0-windows-aarch64.zip | aarch64 Windows |
riddle-v0.3.0-macos-aarch64.zip | Apple Silicon macOS |
包内是 clue、riddle-lsp、riddlec、riddle(Windows 上带 .exe),以及 LICENSE、README.md 和 crates/gc/src/runtime.c。解压后把目录加入 PATH,再确认版本:
> clue --version
clue 0.3.0
> riddle --version
riddle 0.3.0 (3a783d1)
> riddlec --version
riddlec 0.3.0 (3a783d1)
> riddle-lsp --version
riddle-lsp 0.3.0 (3a783d1)
clue 只打印版本号;另外三个会附加构建时的 git 短哈希,所以实际输出里的哈希与这里不同。
Release 里还有第二类 zip,文件名形如 riddle-v<version>-target-<triple>.zip,只面向交叉编译。它不含可执行文件,内容是 LICENSE、runtime.c 和一份 target.toml:
schema = 1
triple = "aarch64-unknown-linux-gnu"
runtime = "runtime.c"
llvm_version = "22.1.3"
这正是 clue 在 --target 构建时读取的目标组件格式,字段含义见下面的交叉编译一节。
从源码构建
仓库用 rust-toolchain.toml 固定 channel = "1.97.1",rustup 进入仓库目录时会自动使用这个工具链。
git clone --depth 1 https://github.com/riddle-lang/riddle.git
cd riddle
cargo install --path . --features install-bins --force --target-dir "${TMPDIR:-/tmp}/riddle-install"
git clone --depth 1 https://github.com/riddle-lang/riddle.git
Set-Location riddle
cargo install --path . --features install-bins --force --target-dir "$env:TEMP\riddle-install"
一条命令装齐四个二进制。install-bins 是根发行包 riddle 的 feature,负责暴露 clue、riddlec、riddle-lsp 三个安装入口;riddle 本身不需要它。--target-dir 把中间产物放到仓库的 target/ 之外,避免污染工作副本。
cargo install riddle 从 crates.io 安装走不通。根包和 clue、riddlec、riddle-lsp 三个成员 crate 都是 publish = false,彼此之间全部是 path 依赖,所以安装源只能指向仓库根。
只想构建、不想安装到 ~/.cargo/bin 时:
cargo build -p riddle --release --features install-bins --bins
不要把 --all-features 加到 workspace 范围的 Cargo 命令上。根发行包与成员 crate 都会产出名为 clue、riddlec 的二进制,Cargo 会报输出文件碰撞;上面这条指定了 -p riddle,不会触发。
改动工具链源码后,仓库的常规校验是:
cargo test --workspace --all-targets
cargo check -p riddle --features install-bins --bins
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
最后一条把 Clippy 默认规则集的告警当成错误;pedantic、nursery、cargo 属于可选的额外类别,不是合并门槛。
C 工具链
clue build、clue run、clue test、clue bench、clue install 会把生成的 C 编译再链接成本机可执行文件,需要系统里有一个能编 C11 的编译器兼链接器。下面这些命令不需要 C 工具链:
riddle run、riddle repl:直接用内置 MIR 解释器执行;riddle fmt:只调整空白,不编译;riddlec --emit mir:打印中间表示;riddlec --backend c:只写 C 源码,不调用系统编译器;clue check、riddle-lsp:只做前端分析与编辑功能,接入编辑器见编辑器与 LSP。
探测顺序按目标平台分三档:
| 情形 | 依次尝试 |
|---|---|
| 宿主是 Windows,目标也是 Windows | clang-cl、clang、cc、gcc、cl |
| 非 Windows 宿主,目标是 Windows | clang、cc、gcc、clang-cl |
| 其它 | clang、cc、gcc |
三档都落空后还会扫描 PATH 里形如 clang-cl-<n>、clang-<n>、gcc-<n> 的文件,按版本降序尝试。每个候选都要通过一次真实的编译链接探测——编译 int main(void) { return 0; } 并检查产物是否生成,通过的记录成构建目录里的 .cc-<identity> 标记文件。非宿主目标只接受 clang。
设置 CC 后就不再探测其它候选,失败时依次可能是:
C compiler from CC `...` could not report its version
C compiler from CC `...` cannot compile and link C11
no usable C11 compiler and linker found; tried <候选列表>; set CC to a compiler executable
构建库目标还要归档工具:Unix flavor 用 ar(ar crs),MSVC flavor 用 llvm-lib(clang)或 lib。
交叉编译
clue check --target、clue build --target 和 riddlec --target 接受的目标恰好七个,其它 triple 会被拒绝:
x86_64-unknown-linux-gnuaarch64-unknown-linux-gnui686-unknown-linux-gnux86_64-pc-windows-msvci686-pc-windows-msvcaarch64-pc-windows-msvcaarch64-apple-darwin
目标选择优先级是 --target、环境变量 RIDDLE_TARGET、Clue.toml 里的 [build].target,都没有才用宿主。
非宿主目标需要目标组件。组件根目录取自环境变量:RIDDLE_TARGET_ROOT 指向组件目录本身,RIDUP_TOOLCHAIN_ROOT 则指向包含 targets/ 的工具链根;两者都没设置时 clue 直接报缺组件。缺组件和缺系统组件的报错都把补救命令写在消息里:
target component `aarch64-unknown-linux-gnu` is not installed; run `ridup target add aarch64-unknown-linux-gnu`
C toolchain for `aarch64-unknown-linux-gnu` is missing a Linux sysroot; run `ridup target configure aarch64-unknown-linux-gnu --sysroot <path>`
macOS 目标缺 Apple SDK、Windows 跨目标缺 Windows SDK 或 MSVC 路径时报错结构相同,参数分别是 --sysroot 和 --windows-sdk / --msvc。
ridup
ridup 是仓库外的独立工具链管理器:下载发行包、切换工具链、安装目标组件都由它负责,命令细节以它自己的文档为准。clue 与它之间只有上文两个接口——组件的存放位置,以及错误消息里出现的那几条命令。
安装 ridup 本身:
cargo install --git https://github.com/riddle-lang/ridup --locked
常见问题
cargo build 报 Rust 版本不够。 仓库固定 Rust 1.97.1,确认 rustup 已安装该工具链:rustup toolchain install 1.97.1。在仓库目录里 rustup 会按 rust-toolchain.toml 自动选用。
clue build 报找不到 C11 编译器。 装一个 clang 或 gcc,或者用 CC 指定绝对路径,例如 CC=C:\LLVM\bin\clang.exe。
仓库里没有 examples/ 目录。 示例以本书的可运行代码和 clue new 生成的项目为准。
你好,Riddle
创建项目
clue new hello
cd hello
目标目录不存在时 clue new 会建好一个可构建的包,终端里只打印一行:
clue: created hello
生成的文件如下。.gitignore 里只有一行 /.clue,把构建目录挡在版本控制之外。
hello/
.gitignore
Clue.toml
src/
main.rid
Clue.toml 声明包名、版本、二进制目标,并留出一个空的依赖表:
[package]
name = "hello"
version = "0.1.0"
[[bin]]
name = "hello"
path = "src/main.rid"
[dependencies]
初始的 src/main.rid 只有一个空函数:
fun main() {
}
写第一个程序
把 src/main.rid 整个替换成:
struct Point {
x: i32,
y: i32,
}
fun distance_squared(point: Point) -> i32 {
point.x * point.x + point.y * point.y
}
fun main() {
let point = Point { x: 3, y: 4 };
let value = distance_squared(point);
println!("distance squared = {}", value);
}
检查并运行
在项目目录里执行 clue check,通过后打印一行 clue: checked 和入口文件的绝对路径。接着 clue run 完成构建、启动程序:
> clue check
clue: checked <项目>/src/main.rid
> clue run
clue: built <项目>/.clue/build/hello.exe
distance squared = 25
第二次运行不会重新编译,同样的位置换成 clue: fresh <项目>/.clue/build/hello.exe。程序自己的退出码原样传给 shell。
clue run 需要机器上能编 C11 的编译器;没有的话换用解释器,输出相同,也不需要构建目录:
> riddle run src/main.rid
distance squared = 25
解释器不生成可执行文件也不缓存结果,适合反复试代码;装 C 工具链的过程见安装工具链。清单字段、依赖和各平台产物路径集中在项目、依赖与构建。
这段代码做了什么
struct Point 声明两个 i32 字段;Point { x: 3, y: 4 } 按字段名构造值,字段顺序可以换。fun 声明函数,参数写成 名称: 类型,返回类型跟在 -> 后面。distance_squared 的函数体只有一个表达式,末尾没有分号,它就充当返回值。
let 建立绑定,默认不可变;println! 是内置宏,{} 按 Display 格式化后面的实参。
Point 不是 Copy 类型,把 point 传给 distance_squared 是一次移动,调用之后原绑定不能再读。写成下面这样,clue check 会在运行前拦下来:
struct Point {
x: i32,
y: i32,
}
fun distance_squared(point: Point) -> i32 {
point.x * point.x + point.y * point.y
}
fun main() {
let point = Point { x: 3, y: 4 };
let value = distance_squared(point);
println!("distance squared = {}", point.x); // E0100
}
诊断带的 note 给出了替代做法:需要保留原值就借出去。完整规则见移动语义。
诊断长什么样
clue check 和 clue run 都把诊断写到 stderr,并以退出码 1 结束。上面那段代码的完整输出是:
error[E0100]: use of moved field: `x`
--> src/main.rid:13:5
|
12 | let value = distance_squared(point);
| ----- value moved here
13 | println!("distance squared = {}", point.x);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: borrow with `&` if the original value must remain usable
error: aborting due to 1 previous error
Error: check failed
clue check 只跑前端分析,不调用 C 编译器,因此遇到错误时最后一行是 Error: check failed;clue run 会在打印同样的诊断后以 Error: build failed 结束。
创建与构建项目
创建包
clue new hello # 目标目录必须不存在
clue init hello # 目标目录已存在也可以
clue new --lib math # 库包
clue new demo --workspace # 虚拟工作区
new 在目标已存在时直接失败,退出码 1:
Error: destination `hello` already exists
init 允许写进已有目录,但不会覆盖已有文件;目录里已经有 Clue.toml,或者已经有该类型对应的入口源码(二进制包是 src/main.rid,库包是 src/lib.rid)时报:
Error: refusing to overwrite files in `<目录绝对路径>`
--lib 与 --bin 互斥,两个都不写时默认二进制包。二进制包生成:
[package]
name = "hello"
version = "0.1.0"
[[bin]]
name = "hello"
path = "src/main.rid"
[dependencies]
库包把 [[bin]] 换成 [lib],入口是 src/lib.rid,初始内容带一个示例函数:
[lib]
name = "math"
path = "src/lib.rid"
pub fun add(x: i32, y: i32) -> i32 {
x + y
}
--workspace 只写一份清单,不生成源码:
[workspace]
crates = []
二进制包和库包的 new、init 都会往目标目录的 .gitignore 追加 /.clue;文件里已经有 .clue(/.clue、.clue/ 等写法都算)时跳过。--workspace 只写清单,不生成 .gitignore;它自己的 init 在已有 Clue.toml 时以 refusing to overwrite ... 失败,消息里带清单的绝对路径。
入口与构建目标
[[bin]] 和 [lib] 省略字段时取默认值:
| 目标 | 字段 | 默认值 |
|---|---|---|
[[bin]] | name | 文件名的 stem;文件叫 main.rid 时用包名 |
path | src/main.rid | |
required-features | 空 | |
[lib] | name | 包名把 - 换成 _ |
path | src/lib.rid | |
proc-macro | false | |
crate-type | riddlelib;还可选 staticlib、cdylib |
除了显式声明,Clue 还会自动发现两类 bin:src/main.rid 存在且尚未被声明时追加一个以包名命名的 bin;src/bin/*.rid 每个文件追加一个 bin,名字取文件 stem。多个 bin 时 clue check 和 clue build 都处理全部,clue run 必须指明哪一个:
> clue run
Error: package `binproj` has multiple binary targets; choose one with `--bin <name>`
一个目标都推导不出来时,入口文件按包类型依次查找:
- 二进制包:
src/main.rid、src/lib.rid、<包名>.rid、main.rid; - 库与过程宏包:
src/lib.rid、<包名>.rid、lib.rid、src/main.rid。
全部落空时报:
Error: missing entry file; expected src/main.rid, src/lib.rid, <package>.rid, main.rid, or lib.rid
[package].entry 可以直接写死入口的相对路径,设置后不再走上面的推导。[[test]]、[[example]]、[[bench]] 的 path 必须显式给出,对应的自动发现目录是 tests/、examples/、benches/;表名都是单数。
模块文件
不带花括号的 mod name; 会去读文件,位置相对当前模块所在目录:先找 name.rid,再找 name/mod.rid。两个文件都在、或者两个都不在,构建都会停下来:
module `foo` is ambiguous; both `<目录>/foo.rid` and `<目录>/foo/mod.rid` exist
module `foo` not found; expected `<目录>/foo.rid` or `<目录>/foo/mod.rid`
进入 name 模块后,它的子模块从 name/ 目录下继续找。只有被 mod 声明的文件才进入编译:src/util/mod.rid 存在但源码里没写 mod util; 时,这个文件完全不参与构建。
src/
main.rid mod util; → src/util.rid 或 src/util/mod.rid
util/
mod.rid mod text; → src/util/text.rid
text.rid
带花括号的 mod name { ... } 是内联写法,不读文件:
mod util {
pub fun double(value: i32) -> i32 {
value * 2
}
}
fun main() {
println!("{}", util::double(21));
}
依赖
[dependencies] 里的每个键既是包名,也是注入当前包的模块名。三种来源:
[dependencies]
math = { path = "../math" }
codec = { git = "https://example.com/codec.git", tag = "v1.0.0" }
json = "^1.2"
internal = { version = "^0.4", registry = "company" }
字符串简写等价于只写 version 的 registry 依赖。表格式支持的字段:
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
package | string | 同键名 | 键名与真实包名不一致时用它 |
version | semver 需求 | 无 | 本地依赖版本不满足时报错 |
path | string | — | 相对当前包根 |
git | URL | — | 需要系统里有 git |
branch / tag / rev | string | 默认分支 | 三者最多写一个,且必须与 git 同时出现 |
registry | string | default | |
optional | bool | false | 只有同名 feature 被启用才加载 |
features | string 数组 | 空 | 转发给依赖的 feature |
default-features | bool | true | 为 false 时不自动排队依赖的 default |
path 和 git 同时写、branch/tag/rev 写多个,都会在解析清单阶段失败。键名必须是合法模块名([_A-Za-z][_0-9A-Za-z]*)。解析和加载阶段的几种典型报错:
dependency `a` cannot specify both `path` and `git`
dependency `a` may specify only one of `branch`, `tag`, or `rev`
dependency name `1bad` must be a valid module name
dependency `m` requires version `^1.0`, found `0.9.0`
dependency `m` expected package `math-core`, found `math`
依赖源码以 mod <键名> { ... } 的形式内联进当前包,所以调用直接写模块路径:
fun main() {
println!("{}", math::one());
}
依赖包里要被外部使用的项必须声明 pub:
pub fun one() -> i32 {
1
}
[dev-dependencies] 的表结构和字段完全相同,只在构建 test、example、bench 目标时加载。可选依赖会自动获得同名 feature 槽位,[features] 里可以写三种转发:
[features]
default = ["json"]
json = ["dep:json"]
extras = ["codec/tracing"]
dep:json 启用可选依赖,codec/tracing 启用依赖 codec 并转发它的 tracing feature,codec?/tracing 只在 codec 已被别的路径启用时才生效。
工作区
工作区根只有 [workspace],成员各自保留完整的 [package]、目标和依赖:
[workspace]
crates = ["hello", "math", "crates/*"]
crates 元素是相对根目录的路径,可以用 * 通配一个路径分量。几条校验规则:
- 绝对路径、匹配不到任何目录、把工作区根自己注册进来、同一个成员注册两次,都直接报错;
- 每个成员目录必须有
Clue.toml,成员之间不能重名; - 工作区内部的 path 依赖必须同时出现在
crates里,否则报错说明该依赖没有注册进工作区(path dependency ... is not registered in workspace ...);工作区之外的 path 依赖不受这条限制; - 依赖成环时报
cyclic package dependency: a -> b -> a。
在根目录执行命令会按依赖顺序处理全部成员;在成员目录里直接执行则只处理当前成员。三种选择方式:
clue check # 根目录:全部成员;成员目录:当前成员
clue check --workspace # 忽略当前目录,处理全部成员
clue check -p math # 只处理 math
--package 和 --workspace 只在工作区里有效,单包项目里给任一个都报 --package and --workspace require a workspace root;在工作区的成员目录里也能用,两种情况都会回到工作区根求解。clue run 没有 --workspace,工作区选中多个成员时报 workspace run requires --package when multiple crates are selected,所以要写成 clue run -p hello。
工作区只在根目录维护一份 Clue.lock,成员目录里不生成锁文件。
Clue.lock
锁文件版本是 3,顶层是 version 和重复的 [[package]]。一个两成员工作区的内容形如:
version = 3
[[package]]
name = "hello"
version = "0.1.0"
source = 'path+<hello 的绝对路径>'
path = "hello"
dependencies = ["math"]
source_hash = "fb4bf286efe87e99"
[[package]]
name = "math"
version = "0.1.0"
source = 'path+<math 的绝对路径>'
path = "math"
source_hash = "24ce9170a1394d28"
path是包相对工作区根的路径;单包项目里根包写"."。source记录来源:本地包是path+<绝对路径>,registry 包是registry+<index 地址>,git 包是git+<url>#<revision>。source_hash是 16 位十六进制指纹,只覆盖包内的*.rid和Clue.toml,跳过.clue、.git、target与锁文件本身,因此改 README 之类的文件不会让锁失效。dependencies列出直接依赖的包名,checksum记录 registry 包的校验和,features记录启用的 feature;这些字段为空时不会写出来。
写锁是幂等的:求值结果与现有内容一致时不碰文件,否则临时文件加原子替换。跨进程互斥用的是 <包根>/.clue/lock-guard,不是项目根目录下的 Clue.lock.guard。
--locked 要求锁文件存在且与本次求解结果完全一致,否则退出码 1:
Error: missing `<项目>/Clue.lock`; run without --locked once
Error: `<项目>/Clue.lock` is out of date; run without --locked to update it
clue fetch 解析依赖并写锁文件(clue: fetched dependencies),普通构建复用锁定版本,clue update 重新求解(clue: updated dependencies)。clue tree 和 clue metadata 要求锁文件已经存在。--offline 是全局 flag,只允许使用本地已有内容;等价的开关是环境变量 CLUE_OFFLINE=1 和配置文件里的 [net] offline = true。
构建产物
宿主上不加 --release 的构建落在包的 .clue/build;--release 或目标不等于宿主时,落在 .clue/build/<triple>/<profile>,profile 是 debug 或 release:
.clue/build/hello.c # 生成的 C
.clue/build/hello.exe # 宿主 debug 可执行文件
.clue/build/<triple>/release/hello.exe # --release 或目标不是宿主
判据是「profile 是 debug 且目标等于宿主」——所以 clue build --target <宿主 triple> 在 debug 下仍然写进 .clue/build,并不会多出一层目录。每个构建目录里有一个 .build-lock 文件锁,同一包的并发构建会被串行化。
目标名 <target> 指 bin 或 lib 的名字,同名产物放在一起:
| 产物 | 文件名 |
|---|---|
| 生成的 C | <target>.c |
| 内置 runtime | <target>.runtime.c([runtime].source 存在时不生成) |
| 参数 runtime | <target>.args.c |
| 指纹 | <target>.hash |
| 可执行文件 | <target>,Windows 是 <target>.exe |
| 目标文件 | <target>.o,MSVC flavor 是 <target>.obj |
| 元数据 | <target>.rmeta |
| riddlelib | <target>.rlib |
| staticlib | lib<name>.a,Windows 是 <name>.lib |
| cdylib | lib<name>.so / lib<name>.dylib / <name>.dll |
构建消息区分新建和复用:
clue: built <可执行文件绝对路径>
clue: fresh <可执行文件绝对路径>
clue: built library `math`
clue: cached library `math`
指纹包含清单、源码、runtime 文本、目标 triple 和 C 编译器的身份。源码文本参与指纹,改注释同样会触发重新构建;换编译器或编译器版本会让整个目录的缓存失效。
库目标还会进全局缓存:$CLUE_HOME/cache/build/<triple>/<profile>/<fingerprint>/,命中时打印上面那条 clue: cached library 消息,并从缓存恢复产物。关掉它有两种方式:清单里写 [build] cache = false,或者设 CLUE_BUILD_CACHE=0。二进制目标不走这个缓存。
clue clean 只删包的 .clue/build,工作区下每个成员加根各删一次,不碰 $CLUE_HOME 里的全局内容。
环境变量
| 变量 | 作用 |
|---|---|
CLUE_HOME | 全局目录;未设置时依次取 $USERPROFILE/.clue、$HOME/.clue,都没有时报 cannot determine Clue home; set CLUE_HOME |
CLUE_OFFLINE | 强制离线,取值 1/true/yes 或 0/false/no |
CLUE_JOBS | 并行构建数,必须是正整数;也可用 -j 覆盖 |
CLUE_BUILD_CACHE | 只有字面值 0 会关闭全局库缓存 |
CLUE_REGISTRY_INDEX / CLUE_REGISTRY_TOKEN | 覆盖默认 registry 的 index 与 token |
RIDDLE_TARGET | 默认交叉目标,优先级低于 --target、高于 [build].target |
CC | 指定 C 编译器,设置后不再探测其它候选 |
registry 的默认名字是 default,默认 index 是 https://registry.riddle-lang.org/index。全局配置在 $CLUE_HOME/config.toml,项目配置在 <项目>/.clue/config.toml 并覆盖全局;[registries.<name>] 里的 index、api、token 是唯一记录 registry 的地方,Clue.toml 里没有 [registries]。
C 工具链
clue build、clue run、clue test、clue bench、clue install 都要把 C 编译并链接起来,探测顺序按平台组合分三档:
| 情形 | 依次尝试 |
|---|---|
| Windows 宿主,Windows 目标 | clang-cl、clang、cc、gcc、cl |
| 其它宿主,Windows 目标 | clang、cc、gcc、clang-cl |
| 其余组合 | clang、cc、gcc |
这之后还会扫描 PATH 里形如 clang-cl-<n>、clang-<n>、gcc-<n> 的程序,按版本降序尝试;每个候选都要真的编译链接出一个可执行文件才会被采用。设置 CC 就只用它,失败时直接报错而不是退回探测。库目标另需归档工具 ar,或者 MSVC flavor 下的 llvm-lib / lib。完整报错文本和交叉编译所需的目标组件见安装工具链。
注释与文档注释
// 之后到行尾是注释。/* ... */ 是块注释,可以嵌套:
/* 外层
/* 内层 */
外层继续 */
块注释没有闭合时会报 unterminated block comment,并指出起始位置;它不会把文件剩下的内容静默当成注释。
块注释里还有一条额外规则:如果 // 是一行中第一个非空白内容,这一行剩下的部分全部按文本处理,其中的 */ 不再闭合注释:
/* 暂时保留旧实现
// let cached = slow_lookup(key); */
这一行仍然在注释里
*/
这条规则是为了注释掉本身带 */ 的代码。// 出现在一行中间时没有这个效果,它后面的 */ 照常收尾——下面这段注释在第一个 */ 处就结束了:
/* 行内的 // 不保护后面的 */ fun main() { }
文档注释
写在声明前面的 /// 或 /** ... */ 是该声明的文档:
/// 返回两点间距离的平方。
fun distance_squared(x: i32, y: i32) -> i32 {
x * x + y * y
}
文件或模块级的说明用 //! 或 /*! ... */,通常写在文件开头:
//! 几何工具。
/*! 块形式同样有效。 */
结构体字段、枚举变体这类不能单独写前置注释的位置,可以在同一行末尾用 //<:
struct Point {
x: i32, //< 横坐标
y: i32, //< 纵坐标
}
//< 附着到它前面的节点。///< 不是行尾形式,它按 /// 处理并附着到后面的节点。普通 // 永远只是注释,不会成为文档。
文档注释不生成代码,也不参与类型检查。读它的是编辑器的悬停和签名帮助,以及 clue doc 生成的 HTML 文档;两处都按 Markdown 渲染标题、列表和代码围栏。
变量与可变性
let 创建绑定,默认不可变:重新赋值报 E0031。需要更新时写 let mut。
fun main() {
let answer = 42;
let mut count = 0;
count += 1;
println!("{} {}", answer, count);
}
可变性属于绑定,不属于值:同一个值可以先被不可变绑定持有,再被可变绑定持有。
类型标注与推断
可以写类型,也可以让编译器从初始化式推断。字面量没有期望类型时默认整数是 i32、浮点是 f64:
fun main() {
let age: i32 = 18;
let name: &str = "Riddle";
let ratio = 0.5;
println!("{} {} {}", age, name, ratio);
}
没有隐式数值提升:i32 不会自动变成 i64 或 f64,要转换就写 as,见数据类型。
遮蔽
再写一次 let 会创建新绑定,可以换类型:
fun main() {
let label = "hello";
let label = label.len();
println!("{}", label); // 5
}
被遮蔽的值不会提前析构,它一直活到作用域结束。给一个实现了 Drop 的类型就能看出来:
struct R { n: i32 }
impl std::ops::Drop for R {
fun drop(&mut self) {
println!("drop {}", self.n);
}
}
fun main() {
let r = R { n: 1 };
let r = R { n: 2 };
println!("end");
}
输出是 end、drop 2、drop 1。Rust 会在遮蔽那一行析构旧值,Riddle 不会。
解构绑定
let 后面是模式,可以直接拆开元组和结构体:
struct Point { x: i32, y: i32 }
fun main() {
let (a, b) = (1, 2);
let Point { x, y } = Point { x: 3, y: 4 };
let (_, second) = (10, 20);
println!("{}", a + b + x + y + second);
}
mut 写在单个绑定前面,不能修饰整条语句:let (mut count, step) = (0, 5); 合法,let mut (count, step) = ... 是语法错误。
普通 let 没有分支,模式必须覆盖该类型的全部取值,枚举变体和字面量这类可反驳模式报 E0057。需要处理「匹配失败」时用 let-else:
fun unwrap_or_zero(value: Option<i32>) -> i32 {
let Some(number) = value else {
return 0;
};
number
}
else 块必须发散(return、break、continue 或无限 loop),否则报 E0066。匹配成功后绑定在后面的代码里可用,变体载荷同样会绑到名字上;clue build 产出的可执行文件与 riddle run / riddle repl 在这件事上行为一致。
延迟初始化
绑定可以先声明、后赋值,编译器检查每条路径:
fun choose(flag: bool) -> i32 {
let value: i32;
if flag {
value = 10;
} else {
value = 20;
}
value
}
不可变绑定的首次赋值不需要 mut;第二次赋值报 E0031。某条路径上没有赋值就读取,报 E0059。
const
const 声明编译期常量,必须写类型并给初始化式:
const WIDTH: usize = 8;
const HEIGHT: usize = WIDTH / 2;
fun main() {
let grid: [i32; 32] = [0; WIDTH * HEIGHT];
println!("{}", grid[0]);
}
const 可以写在模块顶层,也可以写在 impl 块里,后者用 类型::名字 访问:
struct Scale { factor: i32 }
impl Scale {
const DEFAULT: i32 = 1;
}
fun main() {
println!("{}", Scale::DEFAULT);
}
初始化式能用的东西比想象中少:字面量、其它常量、二元运算、无语句的块、元组/数组/结构体字面量、一元运算、字段访问、as 和下标。函数调用、if、match、循环、赋值都不算常量表达式,报 E0060。
求值器按有符号 i128 建模,再按申明类型截断并查范围,所以下面这些都能折叠:
const N: i32 = -1; // ok
const F: f64 = 1.5; // ok,浮点常量不参与折叠,也不被拒绝
const S: &str = "hi"; // ok,同上
const C: char = 'x'; // ok
const B: bool = true; // ok
const A: usize = 2 - 3; // E0011:结果超出 usize 范围
常量值超出申明类型报 E0011。求值成功的非负整数常量可以用作数组长度和 const 泛型实参。
数据类型
Riddle 是静态类型语言:每个绑定、参数、返回值和表达式都有编译期类型,大部分局部绑定可以推断。
标量
| 类型 | 含义 |
|---|---|
i8 i16 i32 i64 isize | 有符号整数 |
u8 u16 u32 u64 usize | 无符号整数 |
f32 f64 | 浮点数 |
bool | true / false |
char | Unicode 标量值 |
() | unit,也是空元组 |
! | never,表示不会正常产生值 |
isize 和 usize 跟随目标指针宽度。字面量和常量值的范围检查用的是所选目标(riddlec --target、RIDDLE_TARGET,或 Clue 的 [build].target)的宽度,不是运行编译器的机器:32 位 triple 下 4294967296usize 报 E0011,64 位 triple 下合法。riddle run 和 riddle repl 走的 MIR 解释器在任何宿主上都把 usize 当成一个 8 字节字,所以这条路径固定按 64 位检查。没有 i128 / u128 / f16 / f128:词法器接受这些后缀,类型检查报 E0011。
数值字面量可以带进制、下划线和类型后缀:
fun main() {
let integer = 42i32;
let byte = 255u8;
let mask = 0xff_00u16;
let permissions = 0o755;
let flags = 0b1010_0101u8;
let single = 3.14f32;
let double = 1.0f64;
println!("{} {} {} {} {} {} {}", integer, byte, mask, permissions, flags, single, double);
}
整数运算按位宽回绕;除零和有符号最小值除 -1 会终止进程;移位计数按位宽取模;有符号右移是算术右移。
char as 整数 得到 Unicode 码点,u8 as char 也允许。其他整数不能直接转成 char。
元组
元组是定长异构值,用逗号区分于分组括号,字段按位置访问:
fun main() {
let pair: (i32, bool) = (42, true);
let single: (i32,) = (42,);
let unit: () = ();
let (number, flag) = pair;
let _: () = unit;
println!("{} {} {}", number, flag, single.0);
}
(2) 只是整数 2,(2,) 才是单元素元组。
数组、切片与不定长类型
[T; N] 是长度写在类型里的数组,[T] 是长度未知的切片:
fun main() {
let values: [i32; 3] = [1, 2, 3];
let zeros: [i32; 3] = [0; 3];
let slice: &[i32] = &values;
println!("{} {}", values[0], slice[1]);
}
数组可以直接索引和 for 遍历,切片方法(len、is_empty、get、iter 等)也能在数组值上直接调用:接收者按值传到 impl [T] 的方法时需要 {指针, 长度} 这一对,编译器会在调用点把数组提升成切片,values.len() 和 (&values).len() 等价。数组长度是类型的一部分,所以 [i32; 3] 到 &[i32] 的转换只影响传参方式,不改变数组本身。
str、[T]、dyn Trait 是不定长类型,只能出现在引用、原始指针或 impl 目标位置;对它们的引用是两字胖指针(数据地址 + 长度)。
数组长度可以来自 const 或 const 泛型参数:
struct Buffer<T, const N: usize> {
data: [T; N],
}
fun main() {
let buffer: Buffer<i32, 3> = Buffer { data: [1, 2, 3] };
println!("{}", buffer.data[2]);
}
引用与原始指针
&T 是共享引用,&mut T 是可变引用,两者都遵守借用规则,见引用、借用与逃逸。
*const T / *mut T 是给 FFI 和底层实现用的原始指针,不参与借用检查,解引用要在 unsafe 里,见 FFI 与 C 后端。
字符串
str 是 UTF-8 内容,&str 是指向它的胖指针,字符串字面量就是 &str:
fun main() {
let greeting: &str = "hello";
let raw: &str = r#"say "hi""#;
println!("{} {} {} {} {}", greeting, raw, greeting.len(), greeting.is_empty(), greeting.as_bytes()[0]);
}
len() 返回 UTF-8 字节数而不是字符数。&str 可以直接 for 遍历,按 UTF-8 解码产出 char。可增长的 String 见集合。
结构体、枚举与泛型
struct 和 enum 定义名义类型,参数化写法不需要在 > 之间加空格:
struct Pair<A, B> {
first: A,
second: B,
}
enum Slot<T> {
Empty,
Value(T),
}
fun main() {
let slot: Slot<Pair<i32, bool>> = Slot::Value(Pair { first: 1, second: true });
match slot {
Slot::Value(pair) => println!("{} {}", pair.first, pair.second),
Slot::Empty => println!("empty"),
}
}
类型转换
隐式转换只有几种:字面量到期望的数值类型、&[T; N] 到 &[T](可变性必须一致)、具体类型到 &dyn Trait / dyn Trait、&dyn Child 到 &dyn Parent。没有 i32 到 i64、没有 i32 到 f64、没有 char 到 u32。
as 支持这些组合:
| 源 | 目标 |
|---|---|
| 整数 | 整数、浮点、bool、原始指针 |
| 浮点 | 整数、浮点 |
bool、char | 整数 |
u8 | char |
| 原始指针 | 原始指针 |
&T | *const T / *mut T(T 必须 sized;要 *mut 时源必须是 &mut) |
其余组合报 E0012。原始指针不能转回整数。裸指针与切片、&[u8] 与 &str 之间的布局转换要写在 unsafe 里。
fun main() {
let wide = 1i32 as i64;
let back = wide as i32;
let code = '中' as u32;
let letter = 65u8 as char;
println!("{} {} {} {}", wide, back, code, letter);
}
函数
用 fun 声明函数。参数写在括号里并带类型,返回类型跟在 -> 后面,函数体是一个块:
fun add(a: i32, b: i32) -> i32 {
a + b
}
没有返回值时省略 ->,函数体类型是 ()。
参数就是绑定:传非 Copy 值会把所有权移动进函数,Copy 类型按复制传入。见移动语义。
返回值
块的最后一个是表达式就是返回值,不需要 return:
fun square(x: i32) -> i32 {
x * x
}
return 留给提前退出:条件分支里结束函数时用它,其余情况用尾表达式更省事。
fun abs(x: i32) -> i32 {
if x < 0 {
return -x;
}
x
}
函数是值
函数名本身可以当值使用:
fun add(a: i32, b: i32) -> i32 {
a + b
}
fun main() {
let f = add;
println!("{}", f(1, 2)); // 3
}
fun(i32, i32) -> i32 这种函数类型语法已经移除。需要把函数当参数传时,用 impl Fn(i32, i32) -> i32 或显式的 F: Fn(i32, i32) -> i32 约束:
fun apply<F: Fn(i32, i32) -> i32>(f: F, x: i32) -> i32 {
f(x, x)
}
捕获环境、Fn / FnMut / FnOnce 的区别和 dyn Fn 见匿名函数与迭代器。
impl Trait
参数位置的 impl Trait 等价于一个隐藏的泛型参数,每次调用按具体类型单态化:
fun show(x: impl std::fmt::Display) {
println!("{}", x);
}
fun main() {
show(1);
show("text");
}
返回位置的 impl Trait 隐藏一个具体的返回类型,所有返回路径必须是同一个类型:
fun make() -> impl std::fmt::Display {
1
}
两个位置不能简单互推:返回位置不允许直接把调用方传进来的泛型参数还回去,fun f(x: impl Display) -> impl Display { x } 会报 E0035,因为编译器无法证明这个不透明类型实现了 Display。需要在调用方和被调方之间传递抽象类型时,用泛型参数或 dyn Trait。
泛型函数
类型参数、推断、显式实参和 const 泛型见泛型。
只写签名
语法上允许省略函数体:
fun external_log(value: i32);
这种声明只通过前端检查,编译器不会为它生成任何实现;调用它时解释器会报 call to unknown function,C 后端生成的调用也没有对应的定义。需要调用外部实现时用 unsafe extern "C",见FFI 与 C 后端。
表达式与块
Riddle 里几乎一切都是表达式:字面量、路径、运算、调用、字段访问、if、match、loop、块都是,并且都能产生值。
1
x + y
foo(1, 2)
point.x
items[0]
n as i64
语句是丢弃了结果的表达式。加上分号就表示「求值然后丢掉」:
fun value() -> i32 {
let x = 1 + 2;
x + 1
}
x + 1 没有分号,它是块的尾表达式,也就是返回值;写成 x + 1; 就变成语句,函数返回 ()。
块
{ ... } 是表达式,同时创建一个作用域:
fun main() {
let outer = 1;
let inner_value = {
let inner = 2;
inner + outer
};
println!("{}", inner_value); // 3
// inner 在这里不存在
}
内层绑定遮蔽外层同名绑定,离开块后外层恢复可见。
运算符与优先级
从高到低:
| 优先级 | 运算符 | 结合性 |
|---|---|---|
| 1 | 后缀:调用 f()、方法 x.m()、字段 x.f、元组下标 x.0、索引 x[i]、? | 左 |
| 2 | 前缀:- ! * & &mut | 右 |
| 3 | as | 左 |
| 4 | * / % | 左 |
| 5 | + - | 左 |
| 6 | << >> | 左 |
| 7 | & | 左 |
| 8 | ^ | 左 |
| 9 | | | 左 |
| 10 | < <= > >= | 左 |
| 11 | == != | 左 |
| 12 | && | 左 |
| 13 | || | 左 |
| 14 | .. ..= | 左 |
| 15 | = 与复合赋值 | 右 |
几处容易和别的语言记混的地方:
- 移位低于加减:
1 << 2 + 1是1 << 3,等于 8; - 位运算高于比较:
1 & 2 == 0是(1 & 2) == 0,等于true; as高于算术和比较,但低于前缀运算符:-1i32 as u32先取负再转换,结果是4294967295;- 比较可以链式书写但类型检查会拒绝:
1 < 2 < 3报E0003,因为它等价于true < 3; - 区间两端都不能省,
0..、..5、..都是语法错误。
赋值也是表达式,值是 (),所以不能把它塞进需要具体类型的位置:
fun main() {
let mut a = 0;
let b: () = a = 1;
println!("{} {}", a, b); // 1 ()
}
复合赋值 +=、-=、*=、/=、%=、<<=、>>=、&=、^=、|= 先读左侧、运算、再写回。
区间
a..b 是左闭右开区间,a..=b 是闭区间,两者都是普通表达式值:
fun main() {
for i in 0..3 {
println!("{}", i); // 0 1 2
}
for i in 0..=2 {
println!("{}", i); // 0 1 2
}
}
a..b 脱糖为 std::ops::range(a, b),a..=b 脱糖为 std::ops::range_inclusive(a, b),不需要导入。开区间 a..、..b、.. 都没有实现。
类型转换
as 支持整数之间、整数与浮点数之间、浮点数之间、bool 与整数之间、u8 与 char 之间、整数到原始指针,以及原始指针之间。原始指针不能转回整数,其他组合报 E0012。
unsafe 块
unsafe { ... } 是表达式,为原始指针解引用与索引、不安全函数调用提供上下文,并保留自己的值:
fun main() {
let value = unsafe { 5 };
println!("{}", value);
}
类型检查、移动检查和借用检查在 unsafe 里照常执行,它只放开那些显式标注为不安全的操作。
控制流
if
if 是表达式,分支都是块,块的值就是分支的值:
fun sign(x: i32) -> i32 {
if x < 0 {
-1
} else if x == 0 {
0
} else {
1
}
}
没有 else 时整个 if 的类型是 (),两个分支都参与类型检查:
fun main() {
if 1 > 0 {
println!("positive");
}
}
if 的条件必须是 bool,没有隐式转换。
if let
if let 用一个模式代替条件,匹配成功时执行第一个分支,绑定只在那个分支内可见:
fun describe(value: Option<i32>) -> i32 {
if let Some(n) = value {
n
} else {
0
}
}
模式可以是任何模式,但不能带 if guard;需要 guard 时用 match。等价写法是只写两个 arm 的 match。
while 与 while let
while 在条件为 true 时反复执行循环体,类型是 ():
fun count_to_three() {
let mut i = 0;
while i < 3 {
println!("{}", i);
i += 1;
}
}
while let 每次迭代重新求值条件,模式匹配失败时结束循环,适合消费会耗尽的序列:
fun drain(mut current: Option<i32>) -> i32 {
let mut total = 0;
while let Some(value) = current {
total += value;
current = Some(value - 1);
}
total
}
与 if let 一样,绑定只在循环体内可见,也不支持 guard。
loop
loop 只由 break 结束,并且会产生值:break 值; 的值就是整个 loop 表达式的值。
fun first_multiple_of_seven(limit: i32) -> i32 {
let mut i = 0;
loop {
if i >= limit {
break -1;
}
if i > 0 && i % 7 == 0 {
break i;
}
i += 1;
}
}
所有 break 的值必须是同一个类型。没有任何可达 break 的 loop 类型是 !,可以用在需要其他类型的位置。带值的 break 只能出现在 loop 里,在 while 或 for 中写 break 值; 报 E0065。
for
for 遍历实现了 IntoIterator 的值:
fun sum_to_three() -> i32 {
let mut sum = 0;
for item in 0..3 {
sum += item;
}
sum
}
循环头可以是任何不可反驳模式,元素直接解构:
fun total(pairs: [(i32, i32); 2]) -> i32 {
let mut sum = 0;
for (key, value) in pairs {
sum += key + value;
}
sum
}
可反驳模式(枚举变体、字面量)不能写在循环头,会报 E0057,需要区分情况时在循环体里 match。
数组、切片、&str、Vector、HashMap、集合都能直接遍历;遍历共享容器时元素是引用(&i32 之类),打印前要先解引用。为自定义类型实现 IntoIterator 见匿名函数与迭代器。
break 与 continue
break 结束最近一层循环,continue 跳到下一次迭代:
fun sum_odds_below_six() -> i32 {
let mut sum = 0;
for value in 0..10 {
if value == 6 {
break;
}
if value % 2 == 0 {
continue;
}
sum += value;
}
sum
}
两者都只能出现在循环体内,循环不支持标签。break; 在三种循环里都可用;带值的 break 只对 loop 有效。循环控制语句写错位置报 E0042(带值但不在 loop 里是 E0065)。
match 基础
match 按值的形状选择分支,每个 arm 由模式、可选 guard 和 => 后的表达式组成:
fun classify(n: i32) -> i32 {
match n {
x if x < 0 => -1,
0 => 0,
_ => 1,
}
}
match 是表达式,所有 arm 必须产生兼容的类型,并且必须覆盖全部取值,否则报 E0039。Option 和 Result 由 prelude 重导出,可以直接写 Some、None、Ok、Err。
完整的模式语法、或模式与穷尽性规则见枚举、模式与 match。
移动语义
赋值、传参和模式绑定默认移动非 Copy 值。移动之后,原来的绑定不再有效:
struct Point { x: i32, y: i32 }
fun main() {
let point = Point { x: 1, y: 2 };
let moved = point;
println!("{}", moved.x);
println!("{}", point.x); // E0100:point 已经被移动
}
consume(point) 这样的调用同样移动实参。移动语义让「谁负责这个值」始终唯一,析构也因此有确定的位置。
Copy
这些类型按复制传递,不移动:
- 整数、浮点、
bool、char、()、!; &T、*const T、*mut T;- 具名函数项;
- 元素都是
Copy的元组和数组。
&mut T 不是 Copy,第一次使用之后再使用会报 E0100。
结构体和枚举不会因为字段都是标量就自动 Copy,必须显式声明:
#[derive(Copy, Clone)]
struct Point { x: i32, y: i32 }
fun main() {
let point = Point { x: 1, y: 2 };
let first = point;
let second = point;
println!("{}", first.x + second.x);
}
也可以手写 impl std::marker::Copy for Point {}。两种写法都要求所有字段都是 Copy,否则报 E0041;Copy 与 Drop 不能同时实现,报 E0055。标准库只为标量提供了 Copy 实现,元组、数组和引用靠编译器内建规则。
Clone 是另一个 trait,要显式实现或派生,Copy 不会自动带来 Clone。
借用代替移动
只想读值就借用,所有权留在原处:
struct Point { x: i32, y: i32 }
fun distance_squared(point: &Point) -> i32 {
point.x * point.x + point.y * point.y
}
fun main() {
let point = Point { x: 3, y: 4 };
println!("{}", distance_squared(&point));
println!("{}", point.x);
}
解引用按值读取
*reference 读取引用指向的值:T: Copy 时得到副本,否则是一次移动,而引用并不拥有 T,所以报 E0308:
fun main() {
let pair = (1i32, 2i32);
let reference = &pair;
let copy = *reference;
println!("{} {}", copy.0, copy.1);
}
模式绑定与部分移动
match 和 for 里的绑定会取得匹配值的所有权。绑定 Copy 字段只是复制,绑定非 Copy 字段才是移动:
struct Holder { items: Vector<i32>, name: &str }
fun main() {
let holder = Holder { items: Vector::new(), name: "h" };
match holder {
Holder { items } => println!("{}", items.len()),
}
println!("{}", holder.name); // 未绑定的字段仍然可用
}
let 解构的粒度和 match 一样:模式移动了哪些字段,原值就只在那些字段上失效,没被绑定的字段照常可用:
struct Holder { items: Vector<i32>, name: &str }
fun main() {
let holder = Holder { items: Vector::new(), name: "h" };
let Holder { items } = holder;
println!("{}", items.len());
println!("{}", holder.name); // ok:name 没被移动
println!("{}", holder.items.len()); // E0100:items 已经移出去了
}
字段全是 Copy 类型时,解构只产生副本,原值照常可用。
块表达式的尾表达式也是移动:块的值要比块自己的局部活得久,所以 { holder } 无论结果是被走值还是只被借用都移动了 holder,之后再读 holder 报 E0100。写块是为了临时构造一个值时(println!("{}", { ...; buffer })、format!(…).as_str())不用操心这一点,值的所有权已经交给块的结果,析构只在结果上做一遍。
实现了 Drop 的类型不允许通过模式移出非 Copy 字段,报 E0305——用户析构函数必须总能看到完整的 self:
struct Resource { name: std::string::String }
impl std::ops::Drop for Resource {
fun drop(&mut self) {
println!("dropping");
}
}
fun main() {
let resource = Resource { name: std::string::String::from_str("file") };
let Resource { name } = resource; // E0305
println!("{:?}", name);
}
析构顺序
- 同一个作用域里的局部变量按声明逆序析构;
- 聚合值内部按字段声明顺序析构;
- 类型自己实现了
Drop时,先调用用户的drop(&mut self),再按声明顺序析构字段。
struct First { n: i32 }
struct Second { n: i32 }
impl std::ops::Drop for First {
fun drop(&mut self) { println!("first {}", self.n); }
}
impl std::ops::Drop for Second {
fun drop(&mut self) { println!("second {}", self.n); }
}
struct Pair { first: First, second: Second }
fun main() {
let pair = Pair { first: First { n: 1 }, second: Second { n: 2 } };
let last = First { n: 3 };
println!("end");
}
输出是 end、first 3(last 后声明,先析构)、first 1、second 2(pair 的字段按声明顺序)。
部分移动只清掉对应字段的析构标记,其余字段照常析构。
控制流与借用
借用在持有它的绑定最后一次使用之后结束,循环体里的冲突只报一次。具体规则、冲突错误码和逃逸行为见引用、借用与逃逸。
引用、借用与逃逸
Riddle 没有生命周期参数。引用的合法性由借用检查负责,引用的存储位置由逃逸分析决定。
共享引用与可变引用
&T 是共享引用,可以同时存在多个;&mut T 是可变引用,存活期间不能有别的引用指向同一位置:
fun main() {
let mut value = 42;
let shared = &value;
println!("{}", *shared);
let exclusive = &mut value;
*exclusive = 43;
println!("{}", value);
}
同时存在的引用类型不匹配时按四种情况报错:
| 现在要 | 已经有了 | 错误码 |
|---|---|---|
&mut | & | E0300 |
& | &mut | E0301 |
&mut | &mut | E0302 |
& | & | 允许 |
借用在持有它的绑定的最后一次使用之后结束,不是等到作用域结束:
fun main() {
let mut value = 1;
let shared = &value;
println!("{}", *shared);
value = 2;
println!("{}", value);
}
这是 NLL(非词法生命周期)的近似实现:判定依据是源码位置,不构建控制流活跃性;循环体里的冲突用不动点求解,收敛后只报告一次。
借用期间不能赋值或移动
E0303 是有活借用时赋值,E0304 是有活借用时移动:
fun main() {
let mut value = 1;
let shared = &value;
value = 2; // E0303
println!("{}", *shared);
}
不相交的结构体字段可以分别可变借用。已知常量下标可以分别借用,动态下标按重叠处理。
引用穿过函数
返回值里的引用保留它的来源:返回值派生自哪个参数,调用结束后那个参数就还在借用状态。
struct Holder { value: i32 }
fun pick(holder: &Holder) -> &i32 {
&holder.value
}
fun main() {
let holder = Holder { value: 7 };
let borrowed = pick(&holder);
println!("{}", *borrowed);
}
编译器为每个函数计算「参数怎么流到返回值」的摘要,泛型 bound 派发时取所有实现的并集;拿不到具体被调者(extern 声明、函数指针、dyn 调用)时按最保守的情况处理。
通过共享引用写入
&T 不允许写入指向的值:
struct Sample { n: i32 }
fun bump(sample: &Sample) {
sample.n = 1; // E0309
}
字段可以声明为 mut,表示该字段在共享引用下也可写:
struct Counter { mut hits: i32 }
impl Counter {
fun bump(&self) {
self.hits += 1;
}
}
fun main() {
let counter = Counter { hits: 0 };
counter.bump();
counter.bump();
println!("{}", counter.hits); // 2
}
可写性是字段自己的属性,与到达它的路径无关:经 &self、经普通字段、经另一个 mut 字段到达都一样可写。枚举变体字段不能声明 mut,写了报 E0014,因为变体字段只能通过模式绑定访问。
经共享引用取 &mut 到 mut 字段只在那一次调用内部有效:可以当接收者或实参,不能绑定到名字、不能返回。
struct Log { mut items: Vector<&str> }
impl Log {
fun add(&self, item: &str) {
self.items.push(item);
}
}
fun main() {
let log = Log { items: Vector::new() };
log.add("first");
println!("{}", log.items.len());
}
把 &mut 绑定到名字(let target = &mut counter.hits;)报 E0309,因为这个借用会活过那次调用。
调用别的函数时,被调方可能写哪些 mut 字段由每个函数的摘要决定。从某个 mut 字段派生的借用不能跨越可能写它的调用,否则报 E0300。dyn 调用和函数指针没有摘要,按「可能写该参数的所有 mut 字段」保守处理。
Drop 与引用
借用了实现 Drop 的局部值时,引用不能活得比拥有者久,否则报 E0306:
struct Resource { name: &str }
impl std::ops::Drop for Resource {
fun drop(&mut self) { println!("drop {}", self.name); }
}
fun name_of(resource: &Resource) -> &str {
resource.name
}
fun main() {
let resource = Resource { name: "file" };
println!("{}", name_of(&resource));
}
这个程序是合法的:resource 活到函数结束。把 name_of(&resource) 的结果存进比 resource 活得更久的绑定时才会报错。
逃逸与存储位置
逃逸分析判断值会不会在当前栈帧之外继续被使用:会逃逸的提到 GC 堆,不逃逸的留在栈上。所有权和借用规则在两种情况下完全相同。
默认开启 GC(Clue.toml 的 [runtime] gc = true)。关掉 GC 就没有堆可以提升,引用逃出栈存储报 E0310:
fun make() -> &i32 {
let value = 1;
&value // GC 关闭时报 E0310
}
原始指针不受借用检查约束,这是 unsafe 之外的绕行方式,代价是编译器不再保证它指向有效存储。
结构体
结构体把一组命名字段组合成一个类型。字段列表不能省——没有元组结构体,也没有 unit 结构体:
struct Point {
x: i32,
y: i32,
}
struct Empty {} 合法;struct S(i32); 和 struct S; 是语法错误。
构造与字段
字段简写和普通写法可以混用,没有 Rust 的函数式更新语法(..other):
fun main() {
let x = 3;
let point = Point { x, y: 4 };
println!("{} {}", point.x, point.y);
}
写字段需要可变绑定或 &mut;共享引用下写普通字段报 E0309:
fun main() {
let mut point = Point { x: 1, y: 2 };
point.x = 5;
println!("{}", point.x);
}
字段默认私有,加 pub 才能被其它模块访问,规则见模块、use 与包。
泛型结构体
类型参数可以带 bound、where 和默认值。默认类型实参只允许出现在 struct、enum、trait 的声明里:
struct Holder<T = i32> {
value: T,
}
fun main() {
let plain = Holder { value: 1 };
let named: Holder<bool> = Holder { value: true };
println!("{} {}", plain.value, named.value);
}
关联函数与方法
impl 块里带接收者的函数是方法,用 .方法(...) 调用;不带接收者的用 类型::函数(...) 调用。
固有 impl 里 Self 指回正在实现的那个类型,返回类型、参数、类型实参位置都能用:fun zero() -> Self、fun combine(self, other: Self)、Vector<Self>。只有构造写法不行,Self { a: 0 } 报 E0050,要写具体结构体名。trait 定义和 trait 实现里的 -> Self 一直正常,标准库的 Clone 就是 fun clone(&self) -> Self;。
struct Point { x: i32, y: i32 }
impl Point {
fun origin() -> Point {
Point { x: 0, y: 0 }
}
fun length_squared(&self) -> i32 {
self.x * self.x + self.y * self.y
}
}
fun main() {
let point = Point::origin();
println!("{}", point.length_squared());
}
接收者可以写 &self、&mut self 或不带参数的 self。Self 之外的类型名解析规则、常量与类型别名见 impl 块。
递归类型
字段不能内联包含自己,否则报 E0072。用引用、原始指针、dyn Trait 或长度为 0 的数组打断环:
struct Node {
value: i32,
next: Option<&Node>,
}
fun main() {
let tail = Node { value: 2, next: None };
let head = Node { value: 1, next: Some(&tail) };
match head.next {
Some(node) => println!("{}", node.value),
None => println!("none"),
}
}
[T; N](N 不为 0)和 [T] 不打断环;Vector<T> 可以,因为它的内部存的是裸指针:
struct Tree {
value: i32,
children: Vector<Tree>,
}
fun main() {
let mut root = Tree { value: 1, children: Vector::new() };
root.children.push(Tree { value: 2, children: Vector::new() });
println!("{}", root.children.len());
}
mut 字段
字段可以声明为 mut,表示它在共享引用下仍然可写,这是 Riddle 提供的内部可变性机制:
struct Counter {
mut hits: i32,
name: &str,
}
impl Counter {
fun bump(&self) {
self.hits += 1;
}
}
fun main() {
let counter = Counter { hits: 0, name: "clicks" };
counter.bump();
counter.bump();
println!("{} {}", counter.name, counter.hits);
}
修饰符顺序是 pub 在前、mut 在后(pub mut hits: i32)。mut 只作用于字段本身:字段类型写成 &mut i32 不会让字段变可变。枚举变体的字段不能声明 mut,报了 E0014。
完整规则(哪些场景能拿到 &mut、调用摘要怎么算)见引用、借用与逃逸。
移动与 Copy
结构体默认按移动传递,除非实现了 Copy:
#[derive(Copy, Clone)]
struct Point { x: i32, y: i32 }
fun main() {
let point = Point { x: 1, y: 2 };
let first = point;
let second = point;
println!("{}", first.x + second.x);
}
实现 Copy 要求所有字段都是 Copy(否则 E0041),并且不能同时实现 Drop(E0055)。见移动语义。
枚举、模式与 match
枚举的每个变体可以不带数据、带元组数据,或带命名字段:
enum Message {
Quit,
Write(i32),
Move { x: i32, y: i32 },
}
变体不能写 pub,也没有显式判别值(A = 1 是语法错误)。结构体风格的变体字段可以写 pub;元组变体的元素不能写 mut,结构体变体的字段写 mut 报 E0014。
构造与匹配
fun describe(message: Message) {
match message {
Message::Quit => println!("quit"),
Message::Write(code) => println!("write {}", code),
Message::Move { x, y } => println!("move {} {}", x, y),
}
}
fun main() {
describe(Message::Quit);
describe(Message::Write(3));
describe(Message::Move { x: 1, y: 2 });
}
每个 arm 是「模式 + 可选 guard + => + 表达式」。arm 体是块时逗号可以省略。match 是表达式,所有 arm 的类型必须兼容。
模式
| 形态 | 例子 |
|---|---|
| 通配 | _ |
| 绑定 | x、mut x |
| 字面量 | 0、1.5、'a'、"s"、true |
| 元组 | (a, b)、() |
| 引用 | &p、&mut p、&&p |
| 结构体 | Point { x, y: 0 } |
| 枚举变体 | Some(x)、E::A |
| 或模式 | A | B,只在 match 臂的顶层 |
没有 ref / ref mut、没有 @ 绑定、没有切片模式 [a, b]、没有区间模式 1..=5、不能在备选内部再嵌或模式(Some(1 | 2)),字面量模式也不能带负号。
或模式
enum Color { Red, Green, Blue }
fun code(color: Color) -> i32 {
match color {
Color::Red | Color::Green => 1,
Color::Blue => 2,
}
}
fun main() {
println!("{}", code(Color::Green));
}
臂首可以写一个多余的 |,没有语义。备选不能绑定变量:E::A(x) | E::B(x) 报 E0050,需要在臂体里绑定,或者拆成两个 arm。或模式只属于 match 臂,let A | B = v 和 if let A | B = v 都不接受。
穷尽性
match 必须覆盖全部取值,否则报 E0039 并指出缺少的模式:
enum State { Idle, Running }
fun label(state: State) -> &str {
match state {
State::Idle => "idle",
}
}
带 guard 的 arm 不参与穷尽性判断:x if x < 0 => ... 不能顶替 _。! 类型的值不需要任何 arm。
guard
guard 是 if 加一个 bool 表达式,可以使用模式绑定的名字:
fun classify(n: i32) -> &str {
match n {
x if x < 0 => "negative",
0 => "zero",
_ => "positive",
}
}
fun main() {
println!("{}", classify(-3));
}
在 guard 里移动绑定值报 E0307,因为 guard 可能对同一个值求值多次。
引用与匹配
拿 &T 去匹配时,模式里的绑定自动变成引用,不需要写 &:
fun main() {
let value = Some(5);
let reference = &value;
match reference {
Some(n) => println!("{}", *n),
None => println!("none"),
}
}
也可以显式写引用模式 &pattern / &mut pattern,它们的含义与显式解引用一致。默认引用模式下再写 mut 或 &mut 会报 E0010。
解构赋值
(a, b) = pair; 和 Point { x, y } = point; 都会写回左值。右侧先求值并存进一个临时位置,再逐个分量存到左边的目标上,所以左边互相重叠也没问题——(a, b) = (b, a); 就是标准的交换写法。结构体形态按字段名配对,与书写顺序无关;元组和结构体可以互相嵌套,逐层展开。
struct Point {
x: i32,
y: i32,
}
fun main() {
let mut a = 0;
let mut b = 0;
(a, b) = (1, 2);
println!("{} {}", a, b); // 1 2
(a, b) = (b, a);
println!("{} {}", a, b); // 2 1
let mut point = Point { x: 0, y: 0 };
Point { x: point.x, y: point.y } = Point { x: 5, y: 6 };
println!("{} {}", point.x, point.y); // 5 6
}
左边的每个分量都得是可写的地方(变量、字段、下标),不是模式绑定。同一个名字出现两次不会报错,按书写顺序依次写入,最后一次生效。
错误处理
Riddle 没有异常。可恢复的失败用 Option 和 Result 表达,不可恢复的路径用 panic 终止进程。三个类型都在 prelude 里,不需要导入。
Option
Option<T> 表示「可能有值」:
fun find_first_even(values: &[i32]) -> Option<i32> {
for value in values {
if value % 2 == 0 {
return Some(*value);
}
}
None
}
fun main() {
match find_first_even(&[1, 3, 4, 5]) {
Some(value) => println!("{}", value),
None => println!("none"),
}
}
方法有 is_some、is_none、unwrap、expect、unwrap_or、unwrap_or_else、map、map_or、and、and_then、or、or_else。没有 ok_or、filter、take、as_ref。
unwrap 和 expect 在 None 上调用会 panic:
fun main() {
let value: Option<i32> = None;
println!("{}", value.unwrap_or(0)); // 0
println!("{}", value.unwrap()); // panic
}
Result
Result<T, E> 区分成功与失败,E 由你决定:
fun parse_positive(text: &str) -> Result<i32, &str> {
match std::parse::parse_i32(text) {
Ok(value) => {
if value > 0 {
Ok(value)
} else {
Err("not positive")
}
}
Err(_) => Err("not a number"),
}
}
fun main() {
match parse_positive("42") {
Ok(value) => println!("{}", value),
Err(reason) => println!("{}", reason),
}
}
方法有 is_ok、is_err、unwrap、expect、unwrap_or、unwrap_or_else、map、map_err、map_or、and、and_then、ok、err。
用 ? 传播
? 在失败时提前返回:操作数是 Option 就返回 None,是 Result 就返回 Err。
fun half(n: i32) -> Result<i32, &str> {
if n % 2 == 0 {
Ok(n / 2)
} else {
Err("odd")
}
}
fun twice(n: i32) -> Result<i32, &str> {
let h = half(n)?;
Ok(h * 2)
}
fun main() {
match twice(8) {
Ok(value) => println!("{}", value),
Err(reason) => println!("{}", reason),
}
}
约束有三条:
- 操作数必须是
Option或Result,其他类型报E0061; - 在返回
Option的函数里才能对Option用?,否则报E0062;对Result用?时,外层返回类型必须是同一个Result枚举; Result的错误类型要能转成外层错误类型,先找Into::into再找From::from,都找不到报E0063。
? 不是 trait 驱动的,没有 Try,也不能用在类型别名或包装类型上。
panic 与断言
panic! 打印消息和源码位置,然后终止进程;它不做栈展开,也没有捕获机制:
fun main() {
let limit = 3;
if limit > 2 {
panic!("limit {} is too large", limit);
}
}
assert!、assert_eq!、assert_ne! 以及对应的 debug_assert* 在条件不成立时 panic;todo!、unimplemented!、unreachable! 用固定消息 panic。它们都返回 !,可以放在需要其他类型的位置:
fun require_positive(value: i32) -> i32 {
if value > 0 {
value
} else {
unreachable!("checked by the caller")
}
}
fun main() {
assert_eq!(require_positive(2), 2);
println!("ok");
}
panic 之后的退出码在 Windows 上是 3,其它平台是 134;riddle run 与编译产物一致。
Option 和 Result 在元素是 Copy 时也是 Copy,可以像普通值一样传递。它们没有 Clone 和 PartialEq 实现,比较内容要显式 match。
泛型
泛型参数写在 <> 里:<T> 是类型参数,<const N: usize> 是常量参数。函数、结构体、枚举、trait 和 impl 都可以声明它们。
泛型函数
类型参数由实参推断,一个函数可以有多个:
fun id<T>(value: T) -> T {
value
}
fun pair<A, B>(first: A, second: B) -> (A, B) {
(first, second)
}
fun main() -> i32 {
let n = id(1);
let p = pair(n, true);
p.0
}
调用处可以显式给出类型实参,写法和 Rust 一致,需要前导 :::
fun id<T>(value: T) -> T {
value
}
struct Wrapper<T> {
value: T,
}
impl<T: Copy> Wrapper<T> {
fun with<U>(self, extra: U) -> (T, U) {
(self.value, extra)
}
}
fun main() -> bool {
let n = id::<i32>(1);
let wrapper = Wrapper { value: n };
let pair = wrapper.with::<bool>(true);
pair.1
}
方法自己的类型实参写在方法名后(wrapper.with::<bool>(true)),类型本身的实参写在类型路径上:Vector::<i32>::new()。表达式的类型实参后面必须紧跟 ( 或 {,所以结构体字面量也能显式指定:
struct Box2<T> {
value: T,
}
fun main() -> i32 {
let b = Box2::<i32> { value: 1 };
b.value
}
类型位置不需要 ::,直接写 Box2<i32>。
泛型类型
结构体和枚举的类型参数用法相同。嵌套泛型里 >> 直接连写,不用插空格:
struct Pair2<A, B> {
first: A,
second: B,
}
enum Slot<T> {
Empty,
Value(T),
}
fun main() -> i32 {
let slot: Slot<Pair2<i32, bool>> = Slot::Value(Pair2 { first: 1, second: true });
match slot {
Slot::Empty => 0,
Slot::Value(pair) => pair.first,
}
}
结构体和枚举的声明里可以直接写 bound(struct Box2<T: Copy>),也可以写 where 子句。
const 泛型
常量参数的用途是数组长度。它由数组实参推断,也可以在类型标注里显式写出:
struct Buffer<T, const N: usize> {
data: [T; N],
}
impl<T, const N: usize> Buffer<T, N> {
fun len(&self) -> usize {
N
}
}
fun len_of<const N: usize>(values: [i32; N]) -> usize {
N
}
fun main() -> usize {
let buffer: Buffer<i32, 3> = Buffer { data: [1, 2, 3] };
buffer.len() + len_of([1, 2, 3])
}
常量参数的推断只来自类型:函数调用里不能写 len_of::<3>(..)。
fun len_of<const N: usize>(values: [i32; N]) -> usize {
N
}
fun main() -> usize {
len_of::<3>([1, 2, 3]) // E0005
}
常量参数没有出现在任何数组长度里时,函数无法被调用:fun f<const N: usize>() -> usize { N } 没有可以推断 N 的来源,调用它报 E0005。类型标注不受这个限制,let s: S<3> = ... 可以给一个只声明 <const N: usize> 的结构体定值。
泛型 impl
固有 impl 和 trait impl 都可以带参数,bound 写在 impl 上,对该 impl 里的所有方法生效:
struct Wrap<T> {
value: T,
}
trait Peek {
fun peek(&self) -> i32;
}
struct Thing {
n: i32,
}
impl Peek for Thing {
fun peek(&self) -> i32 {
self.n
}
}
impl<T: Peek> Peek for Wrap<T> {
fun peek(&self) -> i32 {
self.value.peek()
}
}
fun main() -> i32 {
let wrapper = Wrap { value: Thing { n: 4 } };
wrapper.peek()
}
bound 不满足时方法根本找不到,报 E0013 unknown method(Wrap<i32>::peek());这一点与自由函数的 E0035 不同,见 impl 块。
bound 与 where
<T: A + B> 要求同时满足多个 trait,where 子句等价,只是位置更灵活:
trait Named {
fun name(&self) -> i32;
}
trait Tagged {
fun tag(&self) -> i32;
}
fun combine<T>(value: T) -> i32
where T: Named + Tagged
{
value.name() + value.tag()
}
bound 里可以约束关联类型:
fun add_box<T: std::ops::Add<Output = T>>(left: T, right: T) -> T {
left + right
}
fun main() -> i32 {
add_box(1, 2)
}
bound 在调用点检查。实参类型不满足时报 E0035:
trait Named {
fun name(&self) -> i32;
}
fun read<T: Named>(value: T) -> i32 {
value.name()
}
fun main() -> i32 {
read(1) // E0035
}
trait 名解析不到报 E0023,类型实参个数不符报 E0032,泛型实参列表不允许尾逗号。trait 声明可以给类型参数默认值(trait Add<Rhs = Self>),函数和 impl 不行。
单态化
泛型在编译期按用到的类型组合展开成具体函数,运行时没有类型信息。MIR 里能看到展开后的符号:
fn id__i32(%0: I32) -> I32
fn id__bool(%0: bool) -> bool
需要运行时多态时用 dyn Trait,见 Trait。
递归泛型
递归调用如果让类型实参变大,实例会无限增长,类型检查直接拒绝:
struct Wrap<T> {
inner: T,
}
fun grow<T>(value: T) -> i32 {
grow(Wrap { inner: value }) // E0033
}
fun main() -> i32 {
grow(1)
}
类型实参不变的递归是允许的:
fun depth<T>(value: T, n: i32) -> i32 {
if n == 0 {
0
} else {
depth(value, n - 1)
}
}
fun main() -> i32 {
depth(1, 3)
}
生命周期参数不存在;dyn A + B 和 impl A + B 也不支持,多个 bound 只能写在 bound 位置。
Trait
trait 声明一组方法(有的可以带默认体)和关联类型,由具体类型在 impl 块里实现。声明和实现分开写,调用时按接收者的类型解析。
声明与实现
trait Summary {
fun summarize(&self) -> i32;
}
struct Article {
words: i32,
}
impl Summary for Article {
fun summarize(&self) -> i32 {
self.words
}
}
fun main() -> i32 {
let article = Article { words: 3 };
article.summarize()
}
没有函数体的方法就是必需方法,impl 里缺一个报 E0026。带体的方法可以直接调用同一 trait 的其它方法,impl 里覆写它时以 impl 的版本为准:
trait Value {
fun base(&self) -> i32;
fun doubled(&self) -> i32 {
self.base() * 2
}
}
struct Item {
value: i32,
}
impl Value for Item {
fun base(&self) -> i32 {
self.value
}
}
fun main() -> i32 {
let item = Item { value: 4 };
item.doubled()
}
父 trait
trait Tagged: Named 表示实现 Tagged 的类型也必须实现 Named,于是 T: Tagged 的泛型代码可以直接调用 Named 的方法:
trait Named {
fun name(&self) -> i32;
}
trait Tagged: Named {
fun tag(&self) -> i32;
}
struct Thing {
value: i32,
}
impl Named for Thing {
fun name(&self) -> i32 {
self.value
}
}
impl Tagged for Thing {
fun tag(&self) -> i32 {
self.value + 1
}
}
fun combine<T: Tagged>(value: T) -> i32 {
value.name() + value.tag()
}
fun main() -> i32 {
combine(Thing { value: 1 })
}
父 trait 可以写多个,用 + 分隔。名字解析不到或继承成环报 E0044;只实现了子 trait 而没实现父 trait 报 E0036:
trait Named {
fun name(&self) -> i32;
}
trait Tagged: Named {
fun tag(&self) -> i32;
}
struct Thing {
value: i32,
}
impl Tagged for Thing { // E0036
fun tag(&self) -> i32 {
self.value
}
}
fun main() {}
泛型约束
bound 写在类型参数上(<T: Named>)或 where 子句里,多个 bound 用 + 连接,可以约束关联类型(T: std::ops::Add<Output = T>)。语法与调用点检查见泛型:实参不满足 bound 报 E0035,trait 名解析不到报 E0023,实参个数不符报 E0032。
trait 的类型参数可以带默认值,省略时用默认值补齐:
trait Same<Rhs = Self> {
fun same(&self, other: Rhs) -> bool;
}
struct Number {
value: i32,
}
impl Same for Number {
fun same(&self, other: Number) -> bool {
other.value == self.value
}
}
fun main() -> bool {
let a = Number { value: 1 };
let b = Number { value: 1 };
a.same(b)
}
关联类型
关联类型是 trait 里的占位类型,由每个 impl 填上具体类型。trait 的方法可以用 Self::名字 引用它:
trait Source {
type Item;
fun get(&self) -> Self::Item;
}
struct Number {
value: i32,
}
impl Source for Number {
type Item = i32;
fun get(&self) -> i32 {
self.value
}
}
fun read<S: Source<Item = i32>>(source: &S) -> i32 {
source.get()
}
fun main() -> i32 {
let number = Number { value: 7 };
read(&number)
}
impl 里没给必需关联类型报 E0027。dyn Trait 用关联类型时必须写成 dyn Source<Item = i32>,漏掉绑定报 E0034:
trait Source {
type Item;
fun get(&self) -> Self::Item;
}
fun read(value: &dyn Source) -> i32 { // E0034
value.get()
}
fun main() {}
关联常量不存在:trait 的条目只允许 fun 和 type,写 const 在解析阶段就被拒绝(expected trait item, found Const),没有错误码。固有常量只能放在固有 impl 里,见 impl 块。
dyn Trait
&dyn Trait 和 &mut dyn Trait 是借用视图,具体类型在运行时才确定。&T 可以自动转成 &dyn Trait,&dyn 子 trait 也可以转成 &dyn 父 trait:
trait Speak {
fun speak(&self) -> i32;
}
trait Loud: Speak {
fun volume(&self) -> i32;
}
struct Speaker {
value: i32,
}
impl Speak for Speaker {
fun speak(&self) -> i32 {
self.value
}
}
impl Loud for Speaker {
fun volume(&self) -> i32 {
self.value * 2
}
}
fun quiet(value: &dyn Speak) -> i32 {
value.speak()
}
fun main() -> i32 {
let speaker = Speaker { value: 5 };
let loud: &dyn Loud = &speaker;
quiet(loud)
}
方法表是可派发方法的并集:impl 提供的方法、trait 默认方法和父 trait 的方法都在里面。父 trait 与子 trait 有同名方法时,dyn 上的那次调用报 E0013 ambiguous;普通方法解析不报歧义,见 impl 块。
裸 dyn Trait 是拥有所有权的值,可以直接当返回类型或参数类型,不需要额外的包装类型:
trait Speak {
fun speak(&self) -> i32;
}
struct Speaker {
value: i32,
}
impl Speak for Speaker {
fun speak(&self) -> i32 {
self.value
}
}
fun make() -> dyn Speak {
Speaker { value: 7 }
}
fun call_owned(value: dyn Speak) -> i32 {
value.speak()
}
fun main() -> i32 {
call_owned(make())
}
拥有型对象在堆上保存数据指针、方法槽和一个 drop 槽;离开作用域时通过 drop 槽析构具体类型的值。dyn Fn(i32) -> i32 这类 callable 对象用同一套表示,接收者规则见匿名函数与迭代器。
对象安全
对象安全不是 trait 声明处的检查:trait 定义和 impl 本身都能通过,只有在 dyn 上调用那个方法时才报 E0013。判定条件有五个:
- 第一个参数必须是
self接收者,否则报 the first parameter is not aselfreceiver; - 接收者必须借用(
&self/&mut self),按值self报 by-valueselfcannot be called through a borrowed dyn object; - 方法不能有自己的泛型参数(含
const参数),否则报 generic methods require static monomorphization; - 非接收者参数里不能出现裸
Self; - 返回类型里不能出现裸
Self。
按值 self 的 trait 可以正常声明和实现,只有拿它做动态调用时才失败:
trait Duplicate {
fun duplicate(self) -> Self;
}
struct Speaker {
value: i32,
}
impl Duplicate for Speaker {
fun duplicate(self) -> Self {
self
}
}
fun call(value: &dyn Duplicate) -> i32 {
value.duplicate().value // E0013
}
fun main() {}
孤儿规则与一致性
trait 和类型都在同一个包里时随意实现。跨包实现要求 Self 或 trait 的类型实参里有本包定义的结构体或枚举,否则报 E0048:
impl std::ops::Add for bool { // E0048
type Output = bool;
fun add(self, rhs: bool) -> bool {
self
}
}
fun main() {}
&T 和 &mut T 在判定中是透明的,本地性跟着被指向的类型走。#[fundamental] 能把同样的透明性给自定义类型,但它是标准库保留属性,用户代码写它报 E0049。
同一个 trait 对重叠的类型实现两次报 E0047。impl 的 where 子句还要满足 Paterson 条件:bound 里的类型必须严格小于被实现的类型,impl<T> Foo for T where Wrap<T>: Foo 报 E0037。
内置 trait
Copy、Clone、Drop、比较与运算符 trait、Display 和 Debug 都是标准库里的 trait,用 #[lang = "..."] 标记,编译器按标记识别它们。Copy 与 Drop 的语义见移动语义,运算符、派生和格式化见常用标准库。
Fn、FnMut、FnOnce 是保留名,用户声明同名 trait 报 E0048。#[lang] 和 #[fundamental] 同样只有标准库能用,用户包写它们报 E0049。
impl 块
impl 用来给类型添加固有函数,或者为类型实现 trait。当前 Riddle 的写法接近 Rust:impl Type 写固有实现,impl Trait for Type 写 trait 实现。
固有 impl
固有 impl 直接写目标类型:
struct Point {
x: i32,
y: i32,
}
impl Point {
fun new(x: i32, y: i32) -> Point {
Point { x, y }
}
fun x(&self) -> i32 {
self.x
}
}
关联函数通过路径调用:
let p = Point::new(1, 2);
带接收者的方法通过点号调用:
let x = p.x();
self 接收者
方法可以使用 self、&self 或 &mut self 作为第一个参数:
impl Point {
fun take(self) -> i32 {
self.x
}
fun inspect(&self) -> i32 {
self.x
}
fun shift(&mut self, dx: i32) {
self.x += dx;
}
}
&self 适合只读访问,&mut self 适合修改接收者,self 会移动接收者。
泛型 impl
泛型 impl 与 const 参数的规则见泛型一章。
Trait impl
为类型实现 trait 使用 impl Trait for Type:
trait Show {
fun show(value: i32) -> &str;
type Output;
}
struct Widget {}
impl Show for Widget {
fun show(value: i32) -> &str {
"ok"
}
type Output = i32;
}
编译器会检查 trait 要求的方法和关联类型是否完整、签名是否匹配。
用户类型还可以用 impl Fn(参数类型...) -> 返回类型 for Type、FnMut 或 FnOnce 实现可调用能力;对应的 call receiver 必须依次为 &self、&mut self 或 self。完整示例见闭包与迭代器。
trait impl 可以使用 where 子句约束泛型参数:
trait Marker {}
trait Wrap {}
struct Box<T> {
value: T,
}
impl<T> Wrap for Box<T>
where T: Marker
{}
为了避免 trait 求解无限递归,impl 的 where 约束必须满足 Paterson condition:约束里的类型要严格小于被实现的类型。例如 impl<T> Foo for T where Vec<T>: Foo {} 会被拒绝。
常量和类型别名
impl 块中还可以定义关联常量和关联类型别名:
impl Point {
const ORIGIN: Point = Point { x: 0, y: 0 };
type Pair = (i32, i32);
}
模块、use 与包
模块负责组织名字和可见性,use 负责把路径引入当前作用域,Clue 的包依赖则构成项目之间的边界。枚举已经拆到枚举、模式与 match。
内联模块
使用 mod name { ... } 定义内联模块:
mod math {
pub fun one() -> i32 {
1
}
}
fun main() -> i32 {
math::one()
}
模块中的项默认私有。只有 pub 项能从模块外通过路径访问。pub 也可以用于结构体字段、结构体、枚举、trait、const、type alias 和 use。
文件模块
通过 riddlec 或 Clue 从文件加载源码时,可以声明外部模块:
mod math;
pub mod util;
编译器会在当前模块目录下寻找 name.rid 或 name/mod.rid,两者同时存在会报错。模块不会仅因为文件存在就自动加入编译,父模块必须写出对应的 mod 声明。
目录按层级推进。src/main.rid 中的 mod foo; 会读取 src/foo.rid 或 src/foo/mod.rid;进入 foo 后,其中的 mod bar; 会继续读取 src/foo/bar.rid 或 src/foo/bar/mod.rid。
路径可以使用 self、super、crate 和以 :: 开头的绝对形式。
use 与重新导出
use 可以导入普通路径、别名、glob 或列表:
use crate::math::one;
use crate::math::one as one_value;
use crate::math::*;
use crate::{math::one, util::helper as help};
pub use 会重新导出名字,可用于隐藏内部模块结构:
mod math {
mod inner {
pub fun one() -> i32 { 1 }
}
pub use self::inner::one;
}
过程宏也使用独立宏命名空间中的 use,支持分组、别名、glob、混合导入和通过模块 pub use 重新导出。具体清单与示例见Clue 构建器。
包依赖也是模块
Clue 的本地 path 依赖会以依赖键作为当前包中的模块名:
[dependencies]
math = { path = "../math" }
当前包可以访问依赖公开导出的项:
fun main() -> i32 {
math::one()
}
依赖键与真实包名不同时,使用 Cargo 风格的 package 字段:
[dependencies]
math = { package = "math-core", path = "../math-core" }
Clue 支持 path、git 和 sparse registry 依赖,并使用 Clue.lock 锁定版本、git revision、registry checksum 和源码指纹。完整项目布局、feature 和入口规则见创建与构建项目。
集合
Vector、String、HashMap、HashSet、TreeMap、TreeSet 的缓冲区在运行时分配,可以增长。定长数组 [T; N] 和切片 [T] 不管理分配,见数据类型。
String
String 内部是 Vector<u8>,内容始终是合法 UTF-8。String::new() 创建空串;String::from_str 从 &str 复制一份;String::from_utf8 校验字节序列,非法时返回 None。
fun main() {
let mut text = String::new();
text.push_str("hello");
text.push_char(' ');
text.push_str("world");
println!("{} {}", text.as_str(), text.len());
let bytes = [228u8, 184u8, 173u8];
println!("{}", String::from_utf8(&bytes).is_some());
}
| 方法 | 说明 |
|---|---|
String::new() -> String | 空串 |
String::from_str(value: &str) -> String | 复制一份 |
String::from_utf8(bytes: &[u8]) -> Option<String> | 校验 UTF-8 |
as_str(&self) -> &str | 借用整个缓冲区 |
as_bytes(&self) -> &[u8] | 原始字节 |
len(&self) -> usize、capacity(&self) -> usize、is_empty(&self) -> bool | 字节数、容量、是否为空 |
push_str(&mut self, value: &str)、push_char(&mut self, value: char) | 追加 |
clear(&mut self) | 清空内容,容量保留 |
contains(&self, needle: &str) -> bool | 是否包含子串 |
find(&self, needle: &str) -> Option<usize> | 子串的字节下标 |
starts_with(&self, prefix: &str) -> bool、ends_with(&self, suffix: &str) -> bool | 前后缀判断 |
trim(&self) -> &str | 去掉两端的空格、TAB、LF、CR |
slice(&self, start: usize, end: usize) -> Option<&str> | 字节区间,端点不在字符边界上返回 None |
split(&self, separator: &str) -> Vector<String> | 切分 |
replace(&self, from: &str, to: &str) -> String | 替换全部匹配 |
to_ascii_uppercase(&self) -> String、to_ascii_lowercase(&self) -> String | 只转换 ASCII 字母 |
String 还有两个 unsafe 构造入口 from_raw_bytes 和 from_utf8_raw,接收裸指针和长度,不做校验。&str 上有同名的只读方法,所以字面量可以直接调用 "abc".len()。
len、find、slice 用的都是字节下标,不是字符下标:
fun main() {
let text = String::from_str("中x");
println!("{} {}", text.len(), text.find("x").unwrap_or(0usize)); // 4 3
println!("{:?}", text.slice(0usize, 1usize).is_none()); // true
let csv = String::from_str("a,b,");
println!("{:?}", csv.split(",")); // ["a", "b", ""]
let word = String::from_str("abc");
println!("{:?}", word.split("")); // ["a", "b", "c"]
println!("[{}]", word.replace("", "-")); // [abc]
}
split("") 按字符切开且不产生首尾空串,replace("", ...) 直接返回原串,这两点与 Rust 不同。非空分隔符保留尾随空串,所以 "a,b,".split(",") 有三段。
String 不能相加,也不能直接遍历:
fun main() {
let a = String::from_str("a");
let b = String::from_str("b");
let joined = a + b; // E0003
for c in joined { // E0035
println!("{}", c);
}
}
拼接用 push_str、format!,或先 String::from_str 再追加;按字符遍历写 for c in text.as_str(),它产出 char。String 没有 chars()、lines()、truncate、insert、remove、to_string、parse。
as_str 借用缓冲区,借用到它的最后一次使用为止,所以修改可以紧跟在最后一次读取之后:
fun main() {
let mut text = String::from_str("hello");
let view = text.as_str();
println!("{}", view);
text.push_str(" world");
println!("{}", text.as_str());
}
把修改插到读取之前则报 E0300:
fun main() {
let mut text = String::from_str("hello");
let view = text.as_str();
text.push_str(" world"); // E0300
println!("{}", view);
}
String 实现了 Clone、PartialEq、Eq、Hash、Display、Debug 和 From<&str>,所以它能当 HashMap 的键。
Vector
Vector<T> 是可增长的顺序容器,元素按值存放在 GC 堆上的缓冲区里。
fun main() {
let mut values = vec![3, 1, 2];
values.push(4);
values.sort();
let removed = values.remove(1usize);
println!("{} {:?}", removed, values); // 2 [1, 3, 4]
let first = match values.get(0usize) {
Some(value) => *value,
None => 0,
};
println!("{}", first); // 1
}
vec! 有三种形态:vec![a, b] 逐个把元素移动进去,vec![value; count] 要求 T: Clone 并复制 count 份,vec![] 的元素类型由标注或后续用法推断。等价的构造器是 Vector::from_elem(value, count) 和 Vector::from_iterator(&mut iterator)。
| 方法 | 说明 |
|---|---|
new() -> Vector<T> | 空向量 |
from_elem(value: T, count: usize) -> Vector<T> | count 份 value 的克隆,要求 T: Clone |
from_iterator<I>(iterator: &mut I) -> Vector<T> | 抽干迭代器,要求 I: Iterator<Item = T> |
len(&self) -> usize、capacity(&self) -> usize、is_empty(&self) -> bool | 长度、容量、是否为空 |
push(&mut self, value: T)、pop(&mut self) -> Option<T> | 末尾追加、取出末尾 |
get(&self, index: usize) -> Option<&T>、get_mut(&mut self, index: usize) -> Option<&mut T> | 越界返回 None |
swap(&mut self, left: usize, right: usize) | 交换两个位置,越界或相等时不做任何事 |
insert(&mut self, index: usize, value: T) | 插入并后移,越界 panic |
remove(&mut self, index: usize) -> T | 移位删除:后面的元素整体前移,越界 panic |
contains(&self, value: &T) -> bool | 要求 T: PartialEq |
sort(&mut self) | 稳定排序,要求 T: PartialOrd |
retain(&mut self, predicate: impl FnMut(&T) -> bool) | 原地保留谓词为真的元素 |
clear(&mut self) | 长度清零,容量保留 |
as_ptr(&self) -> *const T、as_slice(&self) -> &[T] | 裸指针视图、切片视图 |
iter(&self) -> SliceIter<T>、iter_mut(&mut self) -> SliceIterMut<T> | 借用迭代器 |
remove 是移位删除,顺序不变,代价是 O(n)。Vector 没有 swap_remove,也没有 sort_by、binary_search、truncate、resize、drain、extend、first、last:
fun main() {
let v = vec![1, 2, 3];
let copy = v.clone(); // E0013
let value = v.swap_remove(0usize); // E0013
println!("{} {}", copy.len(), value);
}
Vector 也不实现 Clone;复制只能新建一个向量再逐个 push。
下标 values[index] 越界时直接 panic(消息 Vector index out of bounds,进程退出码 3),需要可恢复的访问就用 get。Index 和 IndexMut 在标准库里只有 Vector 的实现。
三种遍历方式产出的元素不同:按值 for value in values 消耗向量并产出 T,for value in &values 产出 &T,for value in &mut values 产出 &mut T。按值遍历之后原向量不能再使用(E0100)。
fun main() {
let mut values = vec![1, 2, 3];
for value in &mut values {
*value *= 2;
}
let total = values.iter().fold(0, [acc, value: &i32 -> acc + *value]);
println!("{:?} {}", values, total);
}
HashMap 与 HashSet
两个哈希容器的键要求 Hash + Eq。实现 Hash 的类型有 bool、char、全部整数、f32/f64(按位模式)、String、&str、2 到 6 元组,加上派生出来的结构体和枚举。&str 与 String 用同一套 FNV-1a 折叠,所以 "alpha" 和 String::from("alpha") 的哈希相同。str 本身不是值类型,&str 才是可以放进 HashMap 的那个键;浮点实现了 Hash 却没有实现 Eq,所以 f64 键仍然报 E0035:
use std::collections::HashMap;
fun main() {
let mut by_name: HashMap<&str, i32> = HashMap::new();
by_name.insert("a", 1); // ok
let mut by_score: HashMap<f64, i32> = HashMap::new();
by_score.insert(1.5, 2); // E0035
println!("{} {}", by_name.len(), by_score.len());
}
键也可以用 String,或给结构体派生 Hash、PartialEq、Eq。
HashMap 方法 | 说明 |
|---|---|
new() -> HashMap<K, V> | 空表 |
len(&self) -> usize、is_empty(&self) -> bool | 元素个数 |
insert(&mut self, key: K, value: V) | 键已存在时覆盖值,返回 () |
get(&self, key: &K) -> Option<&V>、get_mut(&mut self, key: &K) -> Option<&mut V> | 按键查找 |
remove(&mut self, key: &K) -> Option<V> | 删除并返回旧值 |
contains_key(&self, key: &K) -> bool | 是否存在 |
get_or_insert(&mut self, key: K, default: V) -> &mut V | 不存在时插入 default,返回值的可变引用 |
entry(&mut self, key: K) -> Entry<K, V> | 进入条目 API |
iter(&self) -> HashMapIter<K, V> | 产出 (&K, &V) |
keys(&self) -> HashMapKeys<K, V>、values(&self) -> HashMapValues<K, V> | 产出 &K / &V |
entry 返回枚举 Entry<K, V>,两个变体是 Occupied { value: &mut V } 和 Vacant { key: K, map: &mut HashMap<K, V> },可以直接 match。更常用的是三个收尾方法:or_insert(default) -> &mut V、or_insert_with(impl FnOnce() -> V) -> &mut V、or_default() -> &mut V(要求 V: Default):
use std::collections::HashMap;
fun main() {
let mut counts: HashMap<String, i32> = HashMap::new();
let a = counts.entry(String::from_str("a")).or_insert(0);
*a += 1;
let b = counts.entry(String::from_str("b")).or_insert_with([ -> 7]);
*b += 1;
println!("{} {}", *counts.get(&String::from_str("a")).unwrap(), *counts.get(&String::from_str("b")).unwrap());
}
entry(...).or_insert(...) 的返回值要绑定到变量,或把整条语句放进内层块。直接把它当语句丢弃时,HashMap 的可变借用会保持到当前块结束,紧接着的 insert 或下一次 entry 报 E0302:
use std::collections::HashMap;
fun main() {
let mut counts: HashMap<i32, i32> = HashMap::new();
counts.entry(1).or_insert(0);
counts.insert(2, 0); // E0302
println!("{}", counts.len());
}
遍历 &HashMap 得到的是插入顺序,直到发生第一次删除。删除在内部的平行 keys/values 上做 swap-remove——最后一个条目被换进空槽——所以被移动的键会出现在被删键的位置:插入 1、2、3 之后再 remove(&1),遍历顺序是 3、2。顺序是实现细节,不要写进程序逻辑。
use std::collections::HashMap;
fun main() {
let mut m: HashMap<i32, i32> = HashMap::new();
m.insert(1, 10);
m.insert(2, 20);
m.insert(3, 30);
m.remove(&1);
for (key, value) in &m {
println!("{} {}", *key, *value); // 3 30,然后 2 20
}
}
三个视图类型只实现 Iterator,没有 IntoIterator,所以 for 循环里要写 &m,而视图本身仍能用 count() 这类迭代器方法:
use std::collections::HashMap;
fun main() {
let mut m: HashMap<i32, i32> = HashMap::new();
m.insert(1, 10);
for (key, value) in m.iter() { // E0035
println!("{} {}", *key, *value);
}
}
遍历产出的是引用,println!("{}", key) 里 key: &i32 既不实现 Display 也不实现 Debug,先写 *key 解引用。
HashSet 方法 | 说明 |
|---|---|
new() -> HashSet<T> | 空集合 |
len(&self) -> usize、is_empty(&self) -> bool | 元素个数 |
insert(&mut self, value: T) | 已存在时不做任何事,返回 () |
contains(&self, value: &T) -> bool | 是否存在 |
remove(&mut self, value: &T) -> bool | 删除,返回是否删掉了元素 |
iter(&self) -> HashSetIter<T> | 产出 &T |
HashSetIter 和 &HashSet 都实现了 IntoIterator,for value in seen.iter() 与 for value in &seen 都能用;按值 for value in seen 报 E0035。顺序规则与 HashMap 相同。
use std::collections::HashSet;
fun main() {
let mut seen: HashSet<i32> = HashSet::new();
seen.insert(3);
seen.insert(1);
seen.insert(3);
for value in seen.iter() {
println!("{}", *value);
}
println!("{} {}", seen.len(), seen.remove(&3));
}
两个类型都没有 clear、with_capacity、retain;HashSet 也没有 union、is_subset,HashMap 没有 iter_mut、values_mut,也没有按值的 IntoIterator。
TreeMap 与 TreeSet
有序容器的键要求 Ord,内部是红黑树,遍历按升序。
TreeMap 方法 | 说明 |
|---|---|
new() -> TreeMap<K, V> | 空表 |
len(&self) -> usize、is_empty(&self) -> bool | 元素个数 |
insert(&mut self, key: K, value: V) | 键已存在时覆盖值,返回 () |
get(&self, key: &K) -> Option<&V>、contains_key(&self, key: &K) -> bool | 按键查找 |
remove(&mut self, key: &K) -> Option<V> | 删除并返回旧值 |
iter(&self) -> TreeMapIter<K, V> | 中序遍历,产出 (&K, &V) |
keys(&self) -> TreeMapKeys<K, V>、values(&self) -> TreeMapValues<K, V> | 产出 &K / &V |
range(&self, start: &K, end: &K) -> TreeMapRangeIter<K, V> | 左闭右开区间,产出 (&K, &V) |
range 的参数是引用,区间包含 start、不包含 end:
use std::collections::TreeMap;
fun main() {
let mut scores: TreeMap<i32, i32> = TreeMap::new();
scores.insert(30, 3);
scores.insert(10, 1);
scores.insert(20, 2);
scores.insert(40, 4);
for (key, value) in &scores {
println!("{} {}", *key, *value); // 10 1、20 2、30 3、40 4
}
for (key, _) in scores.range(&20, &40) {
println!("range {}", *key); // 20、30
}
println!("{}", scores.range(&35, &40).count()); // 0
}
keys、values、range 三个视图都有 IntoIterator,而 iter() 返回的 TreeMapIter 没有,所以 for (k, v) in scores.iter() 报 E0035,写 for (k, v) in &scores。TreeMap 没有 get_mut(E0013)、没有 entry、没有 clear,也没有按值的 IntoIterator。
TreeSet<T> 是 TreeMap<T, ()> 的包装,提供 new、len、is_empty、insert(返回 ())、contains、remove(返回 bool)和 iter(产出 &T)。TreeSetIter 与 &TreeSet 实现 IntoIterator:
use std::collections::TreeSet;
fun main() {
let mut ids: TreeSet<i32> = TreeSet::new();
ids.insert(30);
ids.insert(10);
ids.insert(20);
ids.insert(10);
for id in &ids {
println!("{}", *id); // 10、20、30
}
println!("{} {}", ids.len(), ids.remove(&10));
}
TreeSet 没有 range,即使内部的 TreeMap 有。
数组与切片
[T; N]、&[T; N]、&mut [T; N]、&[T]、&mut [T] 都实现了 IntoIterator,按值或按引用产出元素。SliceIter 和 SliceIterMut 自己不实现 IntoIterator,所以 for value in values.iter() 报 E0035:
fun main() {
let values = vec![1, 2, 3];
let mut total = 0;
for value in values.iter() { // E0035
total += *value;
}
println!("{}", total);
}
正确写法是 for value in &values 或 for value in values.as_slice()。切片类型的方法只有 len、is_empty、get、get_mut、as_ptr、as_mut_ptr、iter、iter_mut;first、last、split_at、chunks、windows、sort 都不存在。数组本身只有 Debug 和迭代器实现。两者的类型规则见数据类型。
匿名函数与迭代器
方括号匿名函数
匿名函数写成 [参数 -> 体],可以存进变量、当实参传递、从函数返回:
fun apply(f: impl Fn(i32) -> i32, value: i32) -> i32 {
f(value)
}
fun main() {
let inc = [it -> it + 1];
let doubled = [it: i32 -> it * 2](21);
println!("{} {}", apply(inc, 41), doubled);
}
参数类型和返回类型都从期望的可调用签名推断,推断不出来时给参数写标注([x: i32 -> x]);返回类型始终推断,不能标注。it 只是单参时的惯用名,不是关键字或隐式参数。
零参写成 [ -> 体]——空的 [] 仍然是空数组字面量。参数支持解构和尾逗号,体可以是块表达式:
fun main() {
let sum = [a: i32, b: i32 -> a + b];
let maker = [ -> 7];
let base = 10;
let offset = move [it: i32 -> base + it];
println!("{} {} {}", sum(1, 2), maker(), offset(1));
}
move [...] 把用到的外部位置按值捕获,Copy 值仍然复制:
fun main() {
let base = 5;
let add = move [it: i32 -> it + base];
println!("{} {}", add(1), base);
}
后缀位置写 expr [参数 -> 体] 表示用这个匿名函数作为唯一实参调用 expr,方法链依赖这条规则:values.iter().map [value -> *value * 2]。
每个匿名函数表达式有自己的具体类型,两个写法完全相同的表达式也是不同类型。需要让两个参数是同一个具体类型时,用泛型参数加 Fn bound(见下)。匿名函数不能声明泛型参数、where 子句或返回类型,也不能递归调用自己:
fun main() {
let fact = [n: i32 -> if n <= 1 { 1 } else { n * fact(n - 1) }]; // E0050
println!("{}", fact(3));
}
三种写法已经移除:竖线闭包 |x| x 是解析错误,fun(x) { x } 有专门诊断,类型位置的 fun(i32) -> i32 同样有专门诊断。旧代码里的 fun 匿名函数写成方括号形式即可。
fun main() {
let f = |x| x; // 解析错误:竖线闭包语法已移除
println!("{}", f(1));
}
捕获
匿名函数按函数体里的用法决定每个外部位置的捕获方式:
- 只读取:按共享引用捕获;
- 赋值或取
&mut:按可变引用捕获; - 值被移走(交给按值参数、作为返回值、存进其他值):按值捕获,
Copy值在这个位置仍然按复制处理。
捕获精确到静态字段和元组元素。读 pair.left 不会借用 pair.right:
struct Pair { left: String, right: String }
fun main() {
let mut pair = Pair { left: String::from_str("a"), right: String::from_str("b") };
let left_len = [ -> pair.left.len()];
pair.right.push_str("!");
println!("{} {}", left_len(), pair.right.as_str());
}
常量下标 values[0] 同样精确到元素,变量下标 values[i] 只能捕获整个 values。解引用捕获的是那个引用值本身,不是它指向的存储。
捕获方式决定调用能力:只读环境的是 Fn,修改环境的是 FnMut,调用时移出环境中非 Copy 值的是 FnOnce。Fn 满足 FnMut 和 FnOnce,FnMut 满足 FnOnce。FnMut 需要可变绑定才能调用:
fun count() -> i32 {
let mut total = 0;
let mut add = [value: i32 -> {
total += value;
total
}];
add(1);
add(2);
total
}
fun main() {
println!("{}", count());
}
可变捕获的借用活到闭包的最后一次使用,在那之前读被捕获的绑定会报 E0301:
fun main() {
let mut total = 0;
let mut add = [value: i32 -> {
total += value;
total
}];
add(1);
println!("{} {}", total, add(2)); // E0301
}
按值捕获时目标仍有活借用报 E0304;从实现了 Drop 的类型移出字段报 E0305。闭包环境需要逃逸时被提升到 GC 堆,所以可以从函数返回拥有捕获的匿名函数:
fun make_len() -> impl Fn() -> usize {
let text = String::from_str("hello");
move [ -> text.len()]
}
fun main() {
println!("{}", make_len()());
}
反过来,闭包返回指向被捕获容器的引用过不了借用检查:
fun main() {
let values = vec![1, 2, 3];
let pick = [index: usize -> &values[index]]; // E0306
println!("{}", *pick(1usize));
}
可调用参数与返回值
参数位置的 impl Fn(i32) -> i32 引入一个隐藏类型参数,接收匿名函数、命名函数项和实现了 callable trait 的用户类型。每个 impl Fn* 参数各自引入一个隐藏类型;要让两个参数是同一个具体类型,写显式泛型参数:
fun apply2(first: impl Fn(i32) -> i32, second: impl Fn(i32) -> i32, value: i32) -> i32 {
first(value) + second(value)
}
fun combine<F>(first: F, second: F, value: i32) -> i32
where F: Fn(i32) -> i32
{
first(value) + second(value)
}
fun main() {
println!("{}", apply2([it -> it + 1], [it -> it * 2], 10));
let inc = [it -> it + 1];
println!("{}", combine(inc, inc, 10));
}
mut 写在参数绑定上,FnMut 参数需要它才能调用两次:
fun call_twice(mut f: impl FnMut(i32) -> i32, value: i32) -> i32 {
f(value);
f(value)
}
fun main() {
println!("{}", call_twice([it -> it + 1], 1));
}
返回位置的 impl Fn* 隐藏一个具体返回类型,所有返回路径必须产生同一个类型:
fun make_adder(base: i32) -> impl Fn(i32) -> i32 {
move [value: i32 -> base + value]
}
fun main() {
let add = make_adder(3);
println!("{}", add(4));
}
两个不同的匿名函数不能从同一个 impl Fn 返回路径出去:
fun pick(flag: bool) -> impl Fn(i32) -> i32 {
if flag {
[it -> it + 1]
} else {
[it -> it * 2] // E0002
}
}
fun main() {
println!("{}", pick(true)(1));
}
用户类型可以实现 callable trait,call 的 receiver 依次是 &self、&mut self、self,与 Fn、FnMut、FnOnce 对应:
struct Adder { amount: i32 }
impl Fn(i32) -> i32 for Adder {
fun call(&self, value: i32) -> i32 {
value + self.amount
}
}
fun main() {
let adder = Adder { amount: 2 };
println!("{}", adder(40));
}
receiver 写错报 E0020,缺少 call 报 E0021。Fn、FnMut、FnOnce 是保留 trait 名,自己声明同名 trait 报 E0048:
struct Adder { amount: i32 }
impl Fn(i32) -> i32 for Adder {
fun call(&mut self, value: i32) -> i32 { // E0020
value + self.amount
}
}
dyn Fn* 支持拥有值和借用值,三种能力都可用。dyn Fn 必须带签名,缺签名报 E0047:
fun apply(f: &dyn Fn(i32) -> i32, value: i32) -> i32 {
f(value)
}
fun main() {
let inc = [it: i32 -> it + 1];
let borrowed: &dyn Fn(i32) -> i32 = &inc;
println!("{} {}", apply(borrowed, 1), borrowed(2));
}
拥有的 FnMut 值通过不可变绑定调用报 E0031;&mut dyn FnMut 只要求引用本身可变。unsafe 函数项不能传给安全的 Fn* 参数,报 E0001。类型位置曾经存在的 fun(T) -> U 已经移除,写上会得到指向 impl Fn 的诊断:
fun apply(f: fun(i32) -> i32, value: i32) -> i32 { // 解析错误:函数类型语法已移除
f(value)
}
迭代协议
for value in x 只需要两个协议:IntoIterator 把值变成迭代器,Iterator 逐个取出元素。
trait Iterator {
type Item;
fun next(&mut self) -> Option<Self::Item>;
}
trait IntoIterator {
type Item;
type IntoIter;
fun into_iter(self) -> Self::IntoIter;
}
手写实现必须把 next 写成 &mut self。for 只看 IntoIterator:只实现 Iterator 的类型报 E0035,标准库里的 SliceIter、HashMapIter 都是这种情况。
struct Counter {
limit: i32,
}
struct CounterIter {
limit: i32,
current: i32,
}
impl Iterator for CounterIter {
type Item = i32;
fun next(&mut self) -> Option<i32> {
if self.current >= self.limit {
return None;
}
let value = self.current;
self.current += 1;
Some(value)
}
}
impl IntoIterator for Counter {
type Item = i32;
type IntoIter = CounterIter;
fun into_iter(self) -> CounterIter {
CounterIter { limit: self.limit, current: 0 }
}
}
fun main() {
let counter = Counter { limit: 3 };
let mut total = 0;
for value in counter {
total += value;
}
println!("{}", total);
}
泛型参数用 IntoIterator<Item = ..., IntoIter = ...> bound 后同样能写 for。
区间语法 start..end 和 start..=end 降级为 std::ops 里的 range 和 range_inclusive 调用;Range 不在 prelude,函数形式要先 use std::ops::range;。
内置可迭代值
标准库为下列值实现了 IntoIterator:
| 值 | 产出 |
|---|---|
Vector<T> | T,顺序遍历并消耗向量 |
&Vector<T>、&mut Vector<T> | &T、&mut T |
[T; N] | T;&[T; N] 和 &mut [T; N] 产出引用 |
&[T]、&mut [T] | &T、&mut T |
&str、字符串字面量 | char,按 UTF-8 解码 |
&HashMap<K, V> | (&K, &V),插入顺序,第一次删除后顺序改变 |
HashSetIter<T>、&HashSet<T> | &T,顺序同 HashMap |
&TreeMap<K, V>、TreeMapKeys、TreeMapValues、TreeMapRangeIter | 按键升序 |
TreeSetIter<T>、&TreeSet<T> | &T,升序 |
Range<T>、RangeInclusive<T> | T,升序 |
| 十个迭代器适配器 | 见下一节 |
没有实现 IntoIterator 的包括 HashMapIter、HashMapKeys、HashMapValues、TreeMapIter、SliceIter、SliceIterMut、StrIter、VectorIntoIter、array::IntoIter,以及按值的 HashSet<T> 和 TreeSet<T>。
fun main() {
let values = vec![1, 2, 3];
let mut total = 0;
for value in &values {
total += *value;
}
let mut letters = 0;
for _ in "abc" {
letters += 1;
}
println!("{} {}", total, letters);
}
迭代器适配器
Iterator 的必需项只有 next,其余 15 个方法都带默认实现。按 receiver 分成两组:
收 &mut self | 收 self |
|---|---|
count() -> usize | fold<B>(init: B, f: impl FnMut(B, Item) -> B) -> B |
nth(n: usize) -> Option<Item> | map<U, F>(f) -> Map<...>,闭包收 Item、产出 U |
for_each(f: impl FnMut(Item) -> ()) | filter<F>(predicate) -> Filter<...>,谓词收 &Item、产出 Item |
all(predicate: impl FnMut(Item) -> bool) | chain<J>(other: J) where J: Iterator<Item = Item> |
any(predicate: impl FnMut(Item) -> bool) | take_while<F>(predicate)、skip_while<F>(predicate),谓词收 &Item、产出 Item |
find(predicate: impl FnMut(&Item) -> bool) -> Option<Item> | inspect(f: impl FnMut(&Item) -> ()),产出 Item |
position(predicate: impl FnMut(&Item) -> bool) -> Option<usize> | collect(self) -> Vector<Item> |
两组方法的 receiver 不同带来两个结果:count、nth、find 这类方法用完还能继续用同一个迭代器(Rust 里不行);map、filter 这类方法会消费迭代器,产出的适配器本身也是迭代器。谓词收 &Item,所以对 values.iter()(元素是 &i32)写 filter 时闭包参数是 &&i32。
fun main() {
let values = vec![1, 2, 3, 4, 5];
let total = values.iter().fold(0, [acc, value: &i32 -> acc + *value]);
let evens: Vector<i32> = values.iter().map([value: &i32 -> *value]).filter([value: &i32 -> *value % 2 == 0]).collect();
println!("{} {:?}", total, evens);
}
collect 只能收集成 Vector<Item>。要急切地映射或过滤到新向量,用自由函数 map_into(&mut iterator, f) 和 filter_into(&mut iterator, predicate)。
自由函数位于 std::iter,需要导入:enumerate、take、skip、zip、chain、take_while、skip_while、inspect、map_into、filter_into、min、max。take、skip、chain、take_while、skip_while、inspect 产出原来的 Item,enumerate 产出 (usize, Item),zip 产出 (A::Item, B::Item) 二元组。其中 take、skip、zip、enumerate 只有函数形式,写成方法报 E0013:
use std::iter::{enumerate, take};
fun main() {
let values = vec![10, 20, 30];
for (index, value) in enumerate(take(values.iter(), 2usize)) {
println!("{} {}", index, *value);
}
}
min 和 max 按元素自身的顺序比较。元素是引用时先把值取出来:min(values.iter()) 比较的是 &i32,当前实现返回的结果不正确;min(values.iter().map([value: &i32 -> *value])) 才有正确的语义。
use std::iter::{max, min};
fun main() {
let values = vec![4, 1, 3];
let smallest = min(values.iter().map([value: &i32 -> *value])).unwrap_or(0);
let largest = max(values.iter().map([value: &i32 -> *value])).unwrap_or(0);
println!("{} {}", smallest, largest);
}
十个适配器都实现了 IntoIterator,所以 for value in values.iter().map(f) 能过,而 for value in values.iter() 不能——这不是同一件事。DoubleEndedIterator 只有 SliceIter 一个实现者:
fun main() {
let values = vec![1, 2, 3];
let mut iter = values.as_slice().iter();
println!("{} {}", *iter.next_back().unwrap(), *iter.next().unwrap());
}
Iterator 没有 sum、product、last、rev、reduce、flat_map、filter_map、peekable、step_by、scan、by_ref、size_hint;也没有 FromIterator 和 Extend,Option、Result、&mut I 都不是迭代器。循环里的 break、continue 与迭代器的析构规则见控制流。
常用标准库
标准库随 riddlec 一起编译,不需要在 Clue.toml 里声明依赖,名字都以 std:: 开头。一部分通过 prelude 直接可用,其余要显式 use。
prelude 里有什么
prelude 精确包含这些名字:Option、Some、None、Result、Ok、Err、String、Vector、Clone、Copy、Default、Into、Drop、drop、Eq、Ord、PartialEq、PartialOrd、Iterator、IntoIterator。
不在 prelude:From、Hash、Display、Debug、Formatter、Ordering、Range 与 range,四个集合类型,以及 std::parse、std::fs、std::io、std::env、std::time、std::random 里的全部内容。派生宏也不经过 prelude,#[derive(Debug)] 展开成 crate::std::fmt::Debug 全路径。
use std::collections::HashMap;
use std::parse::parse_i32;
fun main() {
let mut counts: HashMap<i32, i32> = HashMap::new();
counts.insert(1, parse_i32("2").unwrap_or(0));
println!("{:?}", counts.get(&1).is_some());
println!("{}", counts.len());
}
格式宏与说明符
函数宏在编译期展开,不需要导入:assert、assert_eq、assert_ne、debug_assert、debug_assert_eq、debug_assert_ne、format、panic、print、println、todo、unimplemented、unreachable、vec,以及过程宏用的 quote。没有 write!、writeln!、stringify!、concat!、matches!。
占位符只有四种形态,格式串必须是字符串字面量:
| 占位符 | 要求 |
|---|---|
{}、{:?} | 对应实参实现 Display / Debug |
{0}、{0:?} | 按位置引用实参,可以重复引用同一个实参 |
{name}、{name:?} | 读取调用处同名局部变量,不是命名实参 |
宽度、精度、进制和对齐都不支持,{:>5} 报 E0400;实参比占位符少也报 E0400。println!("{a}", a = 1) 这种命名实参写法不可用,报 E0050:
fun main() {
println!("[{:>5}]", 1i32); // E0400
}
fun main() {
println!("{} {}", 1i32); // E0400
}
Display 为 &str、String、bool、char、全部整数、f32/f64 和 2 到 6 元组实现。Vector、HashMap、Option、Result 只有 Debug,println!("{}", values) 报 E0035;引用同样不参与自动解引用,key: &i32 用 {} 或 {:?} 都报 E0035,要先写 *key:
fun main() {
let values = vec![1, 2, 3];
println!("{:?}", values);
println!("{}", values); // E0035
}
浮点的 {} 和 {:?} 输出相同:固定 6 位小数、直接截断,特殊值输出 NaN、inf、-inf、-0.000000。字符串和字符的 Debug 加引号并转义 \n、\r、\t、\0、反斜杠和引号本身。
fun main() {
println!("{} {:?}", 1.5f64, 1.5f64); // 1.500000 1.500000
println!("{}", 0.9999999f64); // 0.999999
println!("{:?}", "a\nb"); // "a\nb"
println!("{:?}", 'x'); // 'x'
println!("{}", (1i32, 2i32)); // (1, 2)
}
assert_eq! 和 assert_ne! 只求值两侧一次,失败时打印两侧的 Debug 值,所有断言宏都接受自定义消息。todo!、unimplemented!、unreachable! 以固定消息 panic。debug_assert 系列与对应的 assert 系列行为相同,当前没有按构建配置关闭它们的开关。println!() 只输出换行,print!() 什么都不输出;写 stderr 用 std::io 的 eprint、eprintln。
标准派生
编译器内置 9 个派生宏:Debug、Clone、Copy、Default、Hash、PartialEq、Eq、PartialOrd、Ord,可用于结构体和 unit、tuple、named 三类枚举变体。
Clone、Default、Hash与比较按字段声明顺序工作;PartialEq在变体不同时返回false;PartialOrd和Ord先按变体声明顺序、再按 payload 字典序比较,PartialOrd原样传播字段返回的None;Copy和Eq生成标记 impl,但仍然校验字段:#[derive(Copy)]遇到String字段报E0041,只派生Eq而没有PartialEq报E0036;- 泛型参数自动获得相应 bound,
Wrapper<T>的Cloneimpl 要求T: Clone。
#[derive(Clone, Debug, PartialEq, Eq)]
struct Config { retries: i32, label: String }
#[derive(Default, Debug)]
struct Settings { retries: i32 }
#[derive(Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
enum State {
#[default]
Idle,
Running(i32),
}
fun main() {
let a = Config { retries: 3, label: String::from_str("x") };
let b = a.clone();
println!("{:?} {}", b, a == b);
let settings: Settings = Default::default();
println!("{}", settings.retries);
let state: State = Default::default();
println!("{}", state == State::Idle);
}
结构体的 Default 逐字段调用 Default::default();枚举必须用 #[default] 标记恰好一个 unit 变体,否则展开阶段报 E0400。调用时写 Default::default() 并靠绑定的类型标注选择实现,Settings::default() 这种关联函数写法不可用。
#[derive(Default)]
enum State {
Idle,
Running(i32),
}
fun main() {
let state: State = Default::default(); // E0400
println!("{}", state == State::Idle);
}
解析
std::parse 只解析整数,不解析浮点:
| 函数 | 返回 |
|---|---|
parse_i32(&str)、parse_i64(&str)、parse_u64(&str)、parse_usize(&str) | Result<T, ParseIntError> |
parse_with_radix(&str, radix: u32) | Result<i64, ParseIntError>,radix 取 2 到 36 |
ParseIntError::kind(&self) -> &ParseIntErrorKind 给出四种原因:Empty、InvalidDigit、PosOverflow、NegOverflow。没有 FromStr trait,也没有 "12".parse::<i32>() 这种写法。
use std::parse::{parse_i32, parse_with_radix, ParseIntErrorKind};
fun main() {
let decimal = parse_i32("42").unwrap_or(0);
let hex = parse_with_radix("ff", 16u32).unwrap_or(0);
match parse_i32("12x") {
Ok(number) => println!("{}", number),
Err(reason) => match reason.kind() {
ParseIntErrorKind::Empty => println!("empty"),
ParseIntErrorKind::InvalidDigit => println!("invalid digit"),
ParseIntErrorKind::PosOverflow => println!("overflow"),
ParseIntErrorKind::NegOverflow => println!("underflow"),
},
}
println!("{} {}", decimal, hex);
}
时间与随机
std::time 里 time_now() -> i64 返回秒级时间戳,Duration 由 from_secs(u64)、from_millis(u64) 构造,as_secs、as_millis 取回数值,sleep(Duration) 阻塞当前线程。Duration 没有 Debug、Clone、PartialEq,也没有算术运算符;没有 Instant 和 SystemTime。
std::random 提供 random_u32()、random_u64()、random_bool() 和 random_below(bound: u32);bound 为 0 时返回 0,否则取模,不保证无偏。
use std::time::{sleep, time_now, Duration};
fun main() {
let start = time_now();
sleep(Duration::from_millis(20));
println!("{} {}", start <= time_now(), Duration::from_secs(2).as_millis());
}
进程参数
std::env::args_os() -> Vector<OsString> 无损保留宿主传入的参数,std::env::args() -> Vector<String> 是它的 Unicode 版本,任一参数不是有效 Unicode 时 panic(消息 process argument is not valid Unicode)。第一个参数是程序自身,用 riddle run 跑脚本时是脚本路径。
参数是 OsString,方法有 new()、from_str(&str)、as_encoded_bytes(&self) -> &[u8]、unsafe from_encoded_bytes_unchecked(&[u8])、len、is_empty 和 into_string(self) -> Result<String, OsString>。没有 OsStr、CString,也没有 Hash、Ord。
use std::env::args_os;
fun main() {
let args = args_os();
for arg in &args {
println!("{}", arg.len());
}
}
遍历要写 for arg in &args:args_os().iter() 返回 SliceIter,它没有 IntoIterator,放进 for 报 E0035。
模块地图
| 模块 | 内容 |
|---|---|
std::option / std::result | Option、Result 及其方法 |
std::vector / std::string | Vector、String,见集合 |
std::collections | HashMap、HashSet、TreeMap、TreeSet、Entry,见集合 |
std::iter | Iterator、IntoIterator、适配器与自由函数,见匿名函数与迭代器 |
std::ops | 运算符 trait、Drop、Range、range、range_inclusive |
std::cmp / std::hash / std::clone / std::default / std::convert / std::marker | 比较、哈希、克隆、默认值、Into/From、Copy |
std::fmt | Display、Debug、Formatter、Error |
std::io | eprint、eprintln、read_line、BufReader、ReadError |
std::fs | FsFile、read_to_string、write、exists、remove、rename、copy、metadata、read_dir、FsError |
std::env / std::ffi | args、args_os、OsString |
std::parse / std::time / std::random / std::process | 整数解析、时间与休眠、随机数、exit |
std::mem | drop、replace、size_of、swap、take |
std::char / std::slice / std::array | ASCII 判断与大小写转换、切片与数组方法 |
没有 std::vec(模块名是 std::vector)、std::num、std::debug、std::thread、std::sync、std::path、std::error。
完整 API 文档
clue doc 生成当前包的 HTML 参考:先跑一次完整检查,通过后写到 <PATH>/.clue/doc/index.html,每个模块一页,页名把模块路径的 :: 换成 -(例如 std-collections-hash_map.html)。默认只收录 pub 条目,--document-private-items 放开过滤;检查不干净时报 documentation requires a clean check 并且不写文件。标准库的现成产物在 std/.clue/doc/,命令行细节见 Clue 构建器。
Clue 构建器
clue 读 Clue.toml,展开入口文件里的 mod name; 和依赖,把包交给 riddlec 生成 C,再调用系统 C 工具链链接出可执行文件或库。clue --help 只列出子命令名,除 clue doc 外没有帮助文本,所以每个子命令实际接受的参数都写在下面。
子命令与 flag
--offline 和 -j/--jobs <N> 是全局 flag。前者不做网络访问,只用本地缓存;后者取正整数,缺省时先看配置文件里的 build.jobs,再缺省时取机器的可用并行度。
| 子命令 | 位置参数 | flag |
|---|---|---|
clue init | <PATH> | --lib、--bin、--workspace,三者互斥 |
clue new | <PATH> | 同 clue init |
clue check | [PATH] | -p/--package <NAME>、--workspace、--target <TRIPLE>、--bin <NAME>、--locked、--all-features、--all-targets、--no-default-features、--features <a,b> |
clue build | [PATH] | check 的全部,另有 --example <NAME>、--release;--bin 与 --example 互斥 |
clue run | [PATH] | -p/--package、--target、--bin、--example、--release、--locked、--all-features、--no-default-features、--features,-- 之后全部传给程序 |
clue test | [PATH] | -p/--package、--workspace、--test <NAME>、--no-run、--release、--locked、--all-features、--no-default-features、--features |
clue bench | [PATH] | -p/--package、--workspace、--bench <NAME>、--locked、--all-features、--no-default-features、--features,-- 之后全部传给程序 |
clue add | <NAME> | --version <REQ>、--path、--git、--branch、--tag、--rev、--registry、--package、--features、--no-default-features、--optional、--dev、--project <DIR>(默认 .) |
clue remove | <NAME> | --dev、--project <DIR>(默认 .) |
clue fetch | [PATH] | --locked、--dev、--features |
clue update | [PATH] | 无 |
clue tree | [PATH] | -e/--edges normal|features,默认 normal |
clue metadata | [PATH] | 无 |
clue package | [PATH] | --list |
clue publish | [PATH] | --registry <NAME>、--dry-run |
clue install | [<PACKAGE>[@<REQ>]] | --path、--git、--branch、--tag、--rev、--version <REQ>、--registry(需要给出包名)、--release |
clue uninstall | <NAME> | --path <DIR>,默认 . |
clue clean | [PATH] | 无 |
clue doc | [PATH] | -p/--package、--open、--document-private-items、--no-std |
参数上的差异:
clue run没有--workspace,也没有--all-targets;check、build、test、bench都有--workspace。--all-targets只出现在check和build。clue test没有--target、--all-targets和-- <args>;clue bench用不到--release(profile 固定为 Release),也没有--target和--no-run。clue doc的-p/--package与check、build用同一套包选择:在工作区里给名字就只文档那一个包(文档写到<包>/.clue/doc),名字不属于工作区报package ... is not a workspace crate,单包目录下给-p照旧报--package与--workspace需要工作区根。--features用逗号分隔,可以重复给。互斥组合在参数解析阶段被拒绝,退出码 2。
退出码
| 情形 | 退出码 |
|---|---|
| 成功 | 0 |
| 任意错误 | 1,stderr 以 Error: <消息> 输出 |
| clap 参数错误(互斥、缺值、非法 triple) | 2 |
clue run | 子进程的退出码;取不到时为 1 |
| Ctrl+C | 1,消息是 build cancelled 或 operation cancelled |
初始化项目
clue new <PATH> 要求目标目录不存在,clue init <PATH> 允许目录已经存在。两者都拒绝覆盖已有的 Clue.toml 或 src/*.rid。不传 --lib 时按二进制项目生成:
hello/
.gitignore
Clue.toml
src/main.rid
入口内容是 fun main() {\n}\n。--lib 改为写 src/lib.rid,里面是 add(x: i32, y: i32) -> i32。.gitignore 追加一行 /.clue,已有这一行时跳过。--workspace 写出只含 crates 的虚拟清单,并要求目标文件原本不存在:
[workspace]
crates = []
成功输出 clue: initialized <path> 或 clue: created <path>。
Clue.toml
清单由手写解析器读取,字段名和类型都按下面的规则校验。除虚拟工作区根外,[package] 是必需的,缺了会报 missing [package] in <path>。
[package]
| 字段 | 类型 | 必填 | 默认 |
|---|---|---|---|
name | 字符串 | 是 | — |
version | semver 字符串 | 否 | 0.1.0 |
entry | 相对路径 | 否 | 按目标或入口规则推导 |
license | 字符串 | 否 | 无 |
description | 字符串 | 否 | 无 |
repository | 字符串 | 否 | 无 |
authors | 字符串数组 | 否 | 空数组 |
publish | bool 或字符串数组 | 否 | 无(允许发布到任何 registry) |
name不能为空,不能是.或..,不能含路径分隔符,否则报invalid package name ...。publish = false表示禁止发布;publish = true与缺省表示不限;数组是 registry 白名单,发布到名单外的 registry 会报package ... cannot be published to registry ...。version不是合法 semver 时报invalid package version ...;publish的类型不对时报package.publish must be a boolean or an array of registry names。- 虚拟工作区根只有
[workspace],没有[package]。把它当工作区根加载时,清单里如果还有[package],会报workspace root ... must not contain [package]。 - 没有
edition、keywords、categories、readme、homepage、documentation、rust-version、include/exclude、default-run、links这些 Cargo 字段。
入口文件
[package].entry 一旦出现就直接作为入口,优先于 [[bin]].path 和 [lib].path,与目标数量无关。没有 entry 时:
- 有
[[bin]]就用第一个 bin 的path; - 否则有
[lib]就用它的path(默认src/lib.rid); - 两者都没有时按包类型寻找。binary 包依次尝试
src/main.rid、src/lib.rid、<name>.rid、main.rid;库和过程宏包依次尝试src/lib.rid、<name>.rid、lib.rid、src/main.rid。
解析出的入口文件必须存在,否则报 entry file ... does not exist;唯一例外是 binary 包声明了多个 [[bin]] 时。连候选都找不到时报 missing entry file; expected src/main.rid, src/lib.rid, <package>.rid, main.rid, or lib.rid。
目标
| 表 | 自动发现目录 | 必填字段 |
|---|---|---|
[[bin]] | src/main.rid、src/bin/*.rid | 无 |
[lib] | 无 | 无 |
[[test]] | tests/*.rid | path |
[[example]] | examples/*.rid | path |
[[bench]] | benches/*.rid | path |
四个数组表的名字都是单数。写成单表(而不是数组)报 <table> must be an array of targets;[[test]]、[[example]]、[[bench]] 的 path 必填,缺了报 <table>[<index>].path is required。
[[bin]] 的字段:
| 字段 | 类型 | 默认 |
|---|---|---|
path | 相对路径 | src/main.rid |
name | 字符串 | 文件 stem;stem 是 main 时用包名 |
required-features | 字符串数组 | 空 |
自动发现分两步:src/main.rid 存在且没有被显式声明时,追加一个以包名命名的 bin;再扫描 src/bin/*.rid,文件 stem 就是 bin 名。名字重复报 duplicate binary target name ...。
[lib] 的字段:
| 字段 | 类型 | 默认 |
|---|---|---|
name | 字符串 | 包名把 - 换成 _ |
path | 相对路径 | src/lib.rid |
proc-macro | bool | false |
crate-type | 字符串数组 | ["riddlelib"] |
crate-type 只接受 riddlelib、staticlib、cdylib,其它值报 unsupported library crate type ...,重复值会去重。[[test]]、[[example]]、[[bench]] 的其余字段是 name(默认文件 stem)和 required-features;同名目标报 duplicate <table> target name ...。
被依赖的包必须声明 [lib]。一个只有 [[bin]] 的包被别人依赖时,加载会报 library dependencies must declare a [lib] target。
[dependencies] 与 [dev-dependencies]
一个依赖可以写成版本字符串简写,也可以写成表:
[dependencies]
math = { path = "../math", version = "^1.0" }
json = "^1.2"
codec = { git = "https://example.com/codec.git", tag = "v1.0.0" }
log = { version = "^1", optional = true, default-features = false }
[dev-dependencies]
assertions = { path = "../assertions" }
其它类型报 dependency <alias> must be a version string or table。表字段:
| 字段 | 类型 | 默认 |
|---|---|---|
package | 字符串 | 与依赖键相同,用于重命名依赖 |
version | semver 约束 | 无 |
path | 相对路径 | — |
git | URL | — |
branch / tag / rev | 字符串 | 默认分支 |
registry | 字符串 | 默认 registry |
optional | bool | false |
features | 字符串数组 | 空 |
default-features | bool | true |
path与git同时给出报dependency <alias> cannot specify both path and git;branch、tag、rev给多于一个报dependency <alias> may specify only one of branch, tag, or rev。- 版本约束非法报
invalid version requirement ... for dependency ...。 - 依赖键会成为当前包里的模块名,必须是合法模块名(
[_A-Za-z][_0-9A-Za-z]*),否则报dependency name ... must be a valid module name。 - 依赖源码以
mod <alias> { ... }内联进当前包。依赖清单里的包名与声明的package不符时报dependency ... expected package ..., found ...;版本不满足时报dependency ... requires version ..., found ...。 [dev-dependencies]只在 test、example、bench 目标里加载;optional依赖只在对应 feature 启用时加载。- git 依赖和
clue install --git需要 PATH 里有git,git 失败时它的 stderr 会原样出现在错误里。 - 工作区内的 path 依赖必须在
workspace.crates中注册,否则报path dependency ... at ... is not registered in workspace ...;工作区之外的 path 依赖不受这条限制。
[features]
[features]
default = ["std"]
std = []
extras = ["dep:codec", "codec/fast", "log?/verbose"]
- 每个 feature 写成
name = ["项"]。值不是数组报features.<name> must be an array,元素不是字符串报features.<name> entries must be strings。 - feature 名只能用
_、-和 ASCII 字母数字,首字符是字母数字或_,否则报invalid feature name ...。 - 可选依赖自动获得一个同名 feature 槽位。
dep:<name>启用可选依赖;<name>/<feature>启用该依赖并转发 feature;<name>?/<feature>只在依赖已经被别的路径启用时才转发。- 未声明的名字报
unknown feature ...,未知依赖的 feature 报unknown dependency feature ...。 --features a,b按上面的规则逐步展开;--all-features把[features]里的所有 key 当作请求;--no-default-features只是不再自动加入default,清单里没有default时本来也不会加。
[workspace]
[workspace]
crates = ["hello", "libs/*"]
唯一字段是 crates:相对路径数组,* 可以通配一个路径分量。缺失报 missing workspace.crates,类型不对报 workspace.crates must be an array of paths,元素不是字符串报 workspace.crates[<index>] must be a string path。
注册时的校验:
| 情形 | 消息 |
|---|---|
| 绝对路径 | workspace crate path ... must be relative |
| 通配不匹配任何目录 | workspace crate path ... matched no directories |
| 工作区根注册自己 | workspace cannot register itself as a crate |
| 成员目录没有清单 | workspace crate ... has no Clue.toml |
| 同一目录注册两次 | workspace crate ... is registered more than once |
| 两个成员同名 | package name ... is used by both ... and ... |
| 成员之间成环 | cyclic package dependency: a -> b -> a |
工作区只有根目录的一份 Clue.lock,成员不单独生成。构建时成员按依赖拓扑分批,批内并行,批间串行。
[runtime]
只能在二进制包里声明,库或依赖包写了会报 [runtime] is only supported for binary packages。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
gc | bool | true | 是否链接带收集器的运行时 |
source | 相对包根的 C 文件路径 | 无 | 替换内置运行时,文件必须存在 |
gc = false与source同时给出报runtime.gc = false cannot be combined with runtime.source。source指向不存在的文件报runtime source ... does not exist。gc = false时链接的是无收集器运行时,所有权堆值改用riddle_alloc/riddle_free管理,需要延长存活期的引用会被拒绝(E0310)。source则整份替换内置运行时实现。- 运行时的 ABI、分配器和布局描述符见引用、借用与逃逸与FFI 与 C 后端。
[build]
| 字段 | 类型 | 默认 |
|---|---|---|
target | 目标 triple | 无 |
cache | bool | true |
target 是目标选择链上的一环,见下文。cache = false 关闭全局库缓存。
不存在的表
[profile]、[patch]、[replace]、[workspace.dependencies]、[workspace.package]、[lints]、[badges]、[target.*] 和清单内的 [registries] 都没有实现,解析器里没有对应分支。Release 构建只由 --release 决定;registry 配置写在 $CLUE_HOME/config.toml 或项目的 .clue/config.toml。
清单错误与编辑器诊断
clue 侧的清单错误没有错误码,全部是文本消息,经 Error: <消息> 输出到 stderr,退出码 1。上面各节引用的就是这些消息的原文。
riddle-lsp 侧另有一套 CLUE 码:
| 码 | 严重性 | 含义 |
|---|---|---|
CLUE0001 | error | 项目分析或加载失败,比如缺入口文件、依赖解析失败 |
CLUE0002 | error | TOML 语法错误 invalid TOML: ... |
CLUE0003 | warning | 未知 key |
CLUE0004 | error | 类型错误、依赖规则冲突、非法 crate-type |
这套 schema 是语言服务器手写的白名单,和 clue 的解析器是两份独立代码,已知两处偏差:
[build].cache在clue里合法,但不在服务器的buildkey 白名单里,编辑器会报unknown key build.cache(CLUE0003警告)。这是误报,构建行为以clue为准。- 服务器的
[package]白名单只有name、version、license、entry、publish,所以合法的description、authors、repository也会被标成未知 key。
构建与产物
clue check 只做检查,不生成 C;每个被检查的 bin 打印一行 clue: checked <入口路径>,有诊断时以 check failed 结束。没有 --bin 时逐个检查 required-features 已满足的 bin,全部被过滤掉时报 no binary target has its required features enabled: ...。--all-targets 额外检查 test、example、bench 三类目标,未满足 required-features 的目标静默跳过。
clue build 生成 C 并链接。--release 只切换 BuildProfile,不读清单里的 profile 表;--all-targets 构建 test、example、bench,但只编译不运行。构建目录里的产物:
| 文件 | 说明 |
|---|---|
<target>.c | 生成的 C 源码 |
<target>.runtime.c | 内置运行时的副本;[runtime].source 时不生成 |
<target>.args.c | 进程参数运行时 |
<target>.hash | 16 位十六进制指纹 |
<target>[.exe] | 可执行文件 |
<target>.o / .obj | 库的目标文件 |
<target>.rmeta | JSON 元数据(schema 1、name、version、target、source_hash) |
<target>.rlib | riddlelib 产物 |
lib<name>.a / <name>.lib | staticlib 产物 |
lib<name>.so / .dylib / <name>.dll | cdylib 产物 |
.cc-<identity> | C 编译器探测结果缓存 |
目录规则不对称:宿主 debug 构建直接用 <包根>/.clue/build;release 构建和任何显式目标用 <包根>/.clue/build/<triple>/<profile>,profile 取 debug 或 release。每个构建目录里有一个 .build-lock,同一包的并发构建会串行化。所有写盘都是先写临时文件再原子替换。Clue 没有 target/ 目录。
构建成功时的输出是 clue: built <exe>、clue: fresh <exe>、clue: built library <name>、clue: fresh library <name> 或 clue: cached library <name>。
缓存
指纹由清单内容、展开后的源码、运行时源码、进程参数运行时代码、字面量 c11、riddlec 的 git hash、目标 triple 和 C 编译器的程序名、flavor、版本一起算出。.c、.args.c、运行时文件都存在且 .hash 内容匹配时才算新鲜,否则重新生成。换 riddlec 或换 C 编译器都会让整个缓存失效。二进制还要额外确认可执行文件和依赖归档仍在。
库构建会把产物发布到全局缓存,路径是 $CLUE_HOME/cache/build/<triple>/<profile>/<fingerprint>/。指纹相同、依赖产物齐全时直接从缓存恢复,打印 clue: cached library <name>;二进制不走这个缓存。发布失败不影响构建结果。关闭方式有 [build] cache = false 和 CLUE_BUILD_CACHE=0(只有字面 0 生效)。
clue clean 删除 <包根>/.clue/build;工作区里会对根和每个成员各删一次。它不动 $CLUE_HOME 下的全局缓存。
Clue.lock
单包项目在项目根目录维护 Clue.lock,工作区只在根目录维护一份。文件顶层是 version(当前 3)和 [[package]] 数组:
| 字段 | 说明 |
|---|---|
name、version | 包名与版本,version 是字符串 |
source | ""(根或 path)、path+<绝对路径>、registry+<索引>、git+<url>#<rev> |
path | 相对工作区根(单包时相对项目根)的路径 |
dependencies | 依赖名数组,为空时不写出 |
source_hash | 源码指纹 |
checksum | registry 包的校验和,为空时不写出 |
features | 启用的 feature,为空时不写出 |
source_hash 递归收集包里的 *.rid 和 Clue.toml,跳过 .clue、.git、target 和 Clue.lock*,按相对路径排序后哈希,输出 16 位十六进制。改非源码文件不会动锁文件。
--locked要求锁文件已存在且与本次求解结果完全相等,否则报missing <path>; run without --locked once或<path> is out of date; run without --locked to update it。- 不加
--locked时,内容没变就不碰文件,变了才用临时文件加原子替换写回。 - 锁文件用
<包根>/.clue/lock-guard做进程间互斥,不是项目根目录下的Clue.lock.guard。 clue tree和clue metadata会先解析依赖,需要锁文件存在,缺了报missing <path>;clue package和clue package --list不解析依赖,也不触发 fetch。
依赖获取、打包与发布
clue fetch解析依赖并写锁文件,打印clue: fetched dependencies。clue update忽略已有锁定结果重新求解。clue tree从锁文件打印依赖树,节点形如name version [features: a,b];-e features显示 feature,-e normal(默认)不显示。clue metadata输出 JSON,包含manifest(name、version、description、authors、repository、license、publish)和lock。clue package生成.clue/package/<name>-<version>.cluepkg(tar.gz),打包时排除.git和.clue,遇到符号链接报package contains symbolic link ...;--list只打印将打包的相对路径。clue publish --dry-run只打包并按publish白名单校验,不联网;真实发布向{api}/v1/crates/new发 multipart 请求,用 Bearer token 鉴权。clue install不带来源参数时安装当前目录的包;name@<需求>不能与--version同时给;库包不能安装(cannot install a library package)。装好的可执行文件放在$CLUE_HOME/bin,clue uninstall <NAME>从这里删除。- registry 包下载后会校验 SHA-256,并按安全路径规则解包。
依赖来源有 path、git 和 sparse registry 三种。clue add 与 clue remove 用 toml_edit 原地修改清单,保留无关的排版,成功后打印 clue: added <name> / clue: removed <name>。
全局目录与配置
CLUE_HOME 的解析顺序是 $CLUE_HOME、$USERPROFILE/.clue、$HOME/.clue;都没有时报 cannot determine Clue home; set CLUE_HOME。
| 路径 | 用途 |
|---|---|
$CLUE_HOME/config.toml | 全局配置 |
<项目>/.clue/config.toml | 项目配置,覆盖全局 |
$CLUE_HOME/registry/src/<hash16(index)>/<name>-<version> | 解包后的 registry 包 |
$CLUE_HOME/git/db/<hash16(url)> | git 裸库 |
$CLUE_HOME/git/checkouts/<hash16(url)>/<revision> | git 检出 |
$CLUE_HOME/bin | clue install 的目标目录 |
$CLUE_HOME/cache/build/... | 全局库缓存 |
配置文件的表:
[net]
offline = false
[build]
jobs = 8
[registry]
default = "default"
[registries.default]
index = "https://registry.riddle-lang.org/index"
api = "https://registry.riddle-lang.org"
token = "..."
build.jobs 必须是正数,否则报 build.jobs in ... must be positive。默认 registry 名是 default,默认索引是 https://registry.riddle-lang.org/index。
环境变量:
| 变量 | 作用 |
|---|---|
CLUE_HOME | 全局目录 |
CLUE_OFFLINE | 强制离线,接受 1/true/yes 或 0/false/no,其它值报 CLUE_OFFLINE must be true or false |
CLUE_JOBS | 并行度,必须是正整数 |
CLUE_REGISTRY_INDEX / CLUE_REGISTRY_TOKEN | 覆盖默认 registry 的索引和令牌 |
CLUE_BUILD_CACHE | 取 0 时关闭全局库缓存 |
RIDDLE_TARGET | 目标 triple |
RIDDLE_TARGET_ROOT / RIDUP_TOOLCHAIN_ROOT | 目标组件的根目录,前者优先 |
CC | 指定 C 编译器,见下文 |
目标平台与 C 工具链
目标按 --target、RIDDLE_TARGET、[build].target、宿主的顺序选择,先命中者生效。选中的 triple 同时给出检查 usize/isize 字面量的宽度,i686-* 下 4294967296usize 报 E0011。[build].target 要等包加载完才看得见(加载就发生在分析内部),所以它和宿主宽度不一致时 clue check/clue build 会按构建目标的宽度再分析一次;宽度一致时只有一次分析。支持的 triple 恰好七个:
x86_64-unknown-linux-gnuaarch64-unknown-linux-gnui686-unknown-linux-gnux86_64-pc-windows-msvci686-pc-windows-msvcaarch64-pc-windows-msvcaarch64-apple-darwin
非宿主目标需要一个已安装的目标组件(ridup target add <triple>)。组件目录里的 target.toml 声明 schema = 1、triple 和运行时文件路径;缺组件报 target component <triple> is not installed; run ridup target add <triple>。组件还提供可选的 c-toolchain.toml,并且:
- Linux 目标缺 sysroot 报
... is missing a Linux sysroot; run ridup target configure <triple> --sysroot <path>; - 非 macOS 宿主构建 macOS 目标缺 SDK 时同理;
- Windows 目标缺 Windows SDK 或 MSVC 路径时报错并给出
ridup target configure的参数。
C 编译器的选择顺序:
- 设置了
CC就只用它。报不出版本报C compiler from CC ... could not report its version;编译或链接失败报C compiler from CC ... cannot compile and link C11。不会回退到别的编译器。 - 否则用目标组件
c-toolchain.toml里配置的编译器,失败同样直接报错。 - 再按平台探测:宿主 Windows 是
clang-cl、clang、cc、gcc、cl;跨到 Windows 是clang、cc、gcc、clang-cl;其它是clang、cc、gcc。之后追加 PATH 里带版本号的clang-cl-N、clang-N、gcc-N,按版本降序。候选必须能编译并链接一个 C11 程序,否则跳过。
全部失败时报 no usable C11 compiler and linker found; tried <列表>; set CC to a compiler executable。归档静态库需要 ar(Unix flavor)或 llvm-lib / lib(MSVC flavor)。交叉编译在 clang 下会加 --target=<triple> 和 -fuse-ld=lld。工具侧路径含非 ASCII 字符时,Clue 会从公共父目录用相对路径调用编译器。
clue run 和 clue test / clue bench 在运行产物时只能在宿主目标上(clue test --no-run 只构建,不受这条限制),交叉产物要复制到目标系统执行。工作区里多个成员被选中时,clue run 要求 --package。clue run 的工作目录是包根,程序退出码原样成为 clue 的退出码。
过程宏
过程宏包声明 [lib] proc-macro = true,源码里用 #[proc_macro]、#[proc_macro_attribute] 或 #[proc_macro_derive(Name)] 导出宏函数。
[package]
name = "answer-macros"
[lib]
path = "src/lib.rid"
proc-macro = true
clue new 没有 --proc-macro,这种清单要手写。宏包编译成宿主平台的动态库加一个 runner,不链进目标程序;每个宏包一个常驻子进程,单次展开最多 10 秒,输入输出各受 16 MiB 限制,宏里的 print 走 stderr(stdout 是私有协议通道),宏内 panic 被隔离,不会带崩 clue。syn 与 quote! 在宏宿主里自动注入,不需要在清单里声明依赖。宏函数的签名要求、导入方式和可用工具见编写过程宏,语法节点接口见内置 syn 与 quote!。
生成 API 文档
clue doc
clue doc --open
clue doc --document-private-items
cd std && clue doc --no-std
clue doc 先做一次完整检查,只有检查干净时才写文档,否则报 documentation requires a clean check。输出固定在 <PATH>/.clue/doc,入口是 index.html,每个模块一页,页名把模块路径的 :: 换成 -(std-collections-hash_map.html),根模块就是 index.html。每页有导航栏、条目签名和文档注释正文;正文支持一级标题、- 列表和三反引号代码块,其余按段落处理,内容全部做 HTML 转义。默认只收录 pub 条目,--document-private-items 放开过滤,--open 生成后用系统默认程序打开 index.html。没有搜索索引、跨包链接和源码视图。
作为 Rust 库使用
clue 同时是库,公开了项目创建、检查、构建、运行、清理和项目分析的函数:
use clue::{ProjectKind, build, check, new};
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let root = Path::new("hello");
new(root, ProjectKind::Binary)?;
check(root)?;
build(root)?;
Ok(())
}
init 对应 clue init(允许目录已存在),run 接受 &[OsString] 作为程序参数。这个包没有发布到 registry,只能作为仓库内的 path 依赖使用。安装工具链见安装工具链,项目从零开始的流程见项目、依赖与构建。
内置 syn 与 quote!
syn 是随标准库源码发布的模块(std/std/syn.rid),quote! 是编译器内置的函数宏。两者都不属于随程序链接的 std:编译过程宏包时,clue 把 std/std/proc_macro.rid 与 std/std/syn.rid 的文本前置进宿主源码,宏包因此直接写use syn::{...}就能用,不需要在Clue.toml里声明依赖,quote!也无需导入。普通程序里既没有syn模块也没有TokenStream类型,本页片段只在过程宏包内成立,所以全部用 text 围栏。
解析入口
三个函数都返回 Result<T, Error>:
use syn::{Expr, Type, parse, parse_str};
let expr = parse::<Expr>(tokens); // 从 TokenStream 解析
let ty = parse_str::<Type>("&mut [i32; 2]"); // 先词法分析,再解析
parse::<T>(tokens: TokenStream) -> Result<T, Error>:从流的开头解析一个T。parse_str::<T>(source: &str) -> Result<T, Error>:字符串先过TokenStream::from_str,词法错误(LexError)转成带位置的Error。ParseStream::parse::<T>() -> Result<T, Error>:从当前游标继续解析,用在自定义Parse里。
实现了 Parse 的类型只有七个:
| 类型 | 接受的输入 |
|---|---|
DeriveInput | struct 或 enum 的完整定义 |
File | 一串条目与语句 |
Item | 一个顶层条目 |
Stmt | 一条语句 |
Expr | 一个表达式 |
Type | 一个类型 |
Pat | 一个模式 |
Error 的两个字段都是公开的:
pub struct Error {
pub span: Span,
pub message: String,
}
Error::new(span, message)构造,error.emit()把它作为 error 级诊断交给编译器。解析失败时先emit()再返回空TokenStream,调用点就会看到一条带位置的错误,而不是“宏没有输出”。
ParseStream
ParseStream保留整个 token 流和一个游标:
pub struct ParseStream {
tokens: TokenStream,
index: usize,
}
| 方法 | 返回 | 行为 |
|---|---|---|
new(tokens) | ParseStream | 游标置于开头 |
is_empty() | bool | 游标是否已经越过最后一个 token |
peek_ident(expected) | bool | 下一个 token 是否是同名标识符,不移动游标 |
peek_punct(expected) | bool | 下一个 token 是否是给定标点,不移动游标 |
span() | Span | 当前 token 的跨度;已到末尾时是Span::call_site() |
next() | Option<TokenTree> | 取出下一个 token 并前移游标 |
remaining() | TokenStream | 游标之后的全部 token 副本,不移动游标 |
parse::<T>() | Result<T, Error> | 把游标交给 T 的 Parse 实现 |
接口直接操作结构化 token,不做字符串切分。
自定义 Parse
自定义类型只要实现 Parse 就能被 parse、parse_str 和 input.parse::<T>() 解析:
use syn::{Error, Parse, ParseStream};
struct NameInput {
name: Ident,
}
impl Parse for NameInput {
fun parse(input: &mut ParseStream) -> Result<NameInput, Error> {
match input.next() {
Option::Some(TokenTree::Ident(name)) => {
if input.is_empty() {
Result::Ok(NameInput { name })
} else {
Result::Err(Error::new(input.span(), "expected end of input"))
}
},
Option::Some(tree) => Result::Err(Error::new(tree.span(), "expected an identifier")),
Option::None => Result::Err(Error::new(input.span(), "expected an identifier")),
}
}
}
Parse 的签名是 fun parse(input: &mut ParseStream) -> Result<Self, Error>,实现里写出具体类型即可。
DeriveInput
derive 宏的输入通常是整个条目,DeriveInput把它拆成可读的字段:
pub struct DeriveInput {
pub attrs: Vector<Attribute>,
pub vis: Visibility,
pub ident: Ident,
pub generics: Generics,
pub data: Data,
tokens: TokenStream, // 私有:完整输入的 token
}
pub换成Visibility::Public,没有可见性修饰符则是Visibility::Inherited。Data分两支:
pub enum Data {
Struct(DataStruct),
Enum(DataEnum),
}
pub struct DataStruct {
pub fields: TokenStream, // 花括号里的原始 token
pub named: Vector<Field>,
}
pub struct DataEnum {
pub variants: TokenStream, // 花括号里的原始 token
pub items: Vector<Variant>,
}
字段与变体:
pub struct Field {
pub attrs: Vector<Attribute>,
pub vis: Visibility,
pub ident: Ident,
pub ty: Type,
pub tokens: TokenStream,
}
pub enum Fields {
Unit,
Named(Vector<Field>),
Unnamed(Vector<Type>),
}
pub struct Variant {
pub attrs: Vector<Attribute>,
pub ident: Ident,
pub fields: Fields,
pub tokens: TokenStream,
}
Attribute.tokens是两个 token:#加方括号组。要按内容判断就attr.tokens.to_string().contains("skip")。
泛型与 where 子句分开保存:
pub struct Generics {
pub tokens: TokenStream, // <...>,空则无泛型参数
pub where_clause: TokenStream, // where ...,空则没有
pub params: Vector<GenericParam>,
pub predicates: Vector<WherePredicate>,
}
pub enum GenericParam {
Type(TypeParam),
Const(ConstParam),
}
tokens与where_clause是原样的 token,直接插进quote!就能还原出impl<T> Foo<T> where T: Copy这样的头部。
DeriveInput的私有字段tokens保存完整输入,to_token_stream()返回它的副本,ToTokens也写同一份内容。用结构体模式拆DeriveInput会漏掉这个私有字段:当前实现下拆完再移动data会让宏进程卡住,直到单次展开的 10 秒超时,因此按字段取值——parsed.ident、parsed.generics.tokens、&parsed.data。
其他语法节点
Item、Stmt、Expr、Type、Pat 会校验语法并记录类别,每个变体存的是该节点的完整 token:
| 节点 | 变体 |
|---|---|
Item | Module、Use、Function、Struct、Enum、Trait、Impl、Const、TypeAlias、Extern |
Stmt | Item、Local、Expr、Break、Continue、Return |
Expr | Literal、Path、Block、Tuple、Array、Struct、Call、Field、Index、Unary、Binary、Cast、Try、Closure、If、While、For、Match、Unsafe、Macro |
Type | Path、Reference、Pointer、Tuple、Array、Const、Never、ImplTrait、Macro |
Pat | Wildcard、Literal、Tuple、Struct、Enum、Binding、Reference、Macro |
这些节点没有字段级 AST:Expr::Binary只知道自己是二元表达式,左右操作数要从 token 里读,或者交给 Visit、Fold 递归处理。Stmt是唯一嵌套别的节点的变体(Stmt::Item持有Item)。
ToTokens
ToTokens是往TokenStream写 token 的接口,syn重导出了它:
pub trait ToTokens {
fun to_tokens(&self, output: &mut TokenStream);
}
use syn::{Expr, ToTokens, parse_str};
let expr = parse_str::<Expr>("value + 1").unwrap();
let mut output = TokenStream::new();
expr.to_tokens(&mut output);
syn为这些类型实现了它:File、Item、Stmt、Expr、Type、Pat、Attribute、Field、Variant、Fields、DataStruct、DataEnum、Data、Visibility、Generics、GenericParam、TypeParam、ConstParam、WherePredicate、DeriveInput。proc_macro一侧的TokenStream、TokenTree、Group、Ident、Punct、Literal、String、str同样实现了它,所以这些值都能直接用在quote!的#后面。
遍历:Visit
Visit按借用遍历。七个方法都有默认实现,只覆盖需要观察的节点,并在自定义实现里调用对应的walk_*继续递归:
pub trait Visit {
fun visit_file(&mut self, node: &File) { walk_file(self, node); }
fun visit_item(&mut self, node: &Item) { walk_item(self, node); }
fun visit_stmt(&mut self, node: &Stmt) { walk_stmt(self, node); }
fun visit_expr(&mut self, node: &Expr) { walk_expr(self, node); }
fun visit_type(&mut self, node: &Type) { walk_type(self, node); }
fun visit_pat(&mut self, node: &Pat) { walk_pat(self, node); }
fun visit_derive_input(&mut self, node: &DeriveInput) { walk_derive_input(self, node); }
}
use syn::{Expr, Type, Visit, parse_str};
struct Counter {
exprs: usize,
types: usize,
}
impl Visit for Counter {
fun visit_expr(&mut self, node: &Expr) {
self.exprs += 1usize;
syn::walk_expr(self, node);
}
fun visit_type(&mut self, node: &Type) {
self.types += 1usize;
syn::walk_type(self, node);
}
}
let mut counter = Counter { exprs: 0usize, types: 0usize };
counter.visit_expr(&parse_str::<Expr>("value.call(1)? + 2").unwrap());
递归函数是syn::walk_file、syn::walk_item、syn::walk_stmt、syn::walk_expr、syn::walk_type、syn::walk_pat、syn::walk_derive_input。
改写:Fold
Fold按值取得节点并返回改写后的节点。默认实现做递归,覆盖某个方法后调用同名的syn::fold_*即可回到默认递归:
use syn::{Expr, Fold, parse_str};
struct ReplaceTwo {}
impl Fold for ReplaceTwo {
fun fold_expr(&mut self, node: Expr) -> Expr {
let replace = match &node {
Expr::Literal(tokens) => tokens.to_string().as_str() == "2",
_ => false,
};
if replace {
match parse_str::<Expr>("3") {
Result::Ok(value) => { return value; },
Result::Err(_) => {},
}
}
syn::fold_expr(self, node)
}
}
syn::fold_expr(&mut folder, node)只做默认递归,不会回调folder.fold_expr,所以不会自递归。可覆盖的方法是fold_file、fold_item、fold_stmt、fold_expr、fold_type、fold_pat、fold_derive_input,对应syn::fold_file等七个自由函数。
quote!
quote!把写在一对花括号里的 token 拼成新的TokenStream。#name插入实现了ToTokens的值:
let name = Ident::new("answer", Span::call_site());
let body = parse_str::<Expr>("40 + 2").unwrap();
let output = quote! {
fun #name() -> i32 { #body }
};
#(...)*是重复块,*前可以放一个分隔 token:
let mut names: Vector<Ident> = Vector::new();
names.push(Ident::new("first", Span::call_site()));
names.push(Ident::new("second", Span::call_site()));
let tuple = quote! { (#(#names),*) };
// tuple.to_string() == "(first , second)"
块内有多个#name时按下标配对:
let zipped = quote! { { #(#names: #values),* } };
// zipped.to_string() == "{first : one , second : two}"
规则:
- 重复变量要有
len()和as_slice(),两次迭代之间按#name.as_slice()[i]取值,Vector<T>满足这个要求。 - 参与同一个重复块的变量长度必须相等,否则宏进程运行时 panic(
quote repetition variables have different lengths),本次展开以 E0400 失败。 - 重复块至少要有一个
#name,否则展开报错,消息以quote repetition must contain开头。 - 分隔符只能是一个 token,写成
#(...),*或#(...);*,不能是 token 序列。 to_string()按 token 渲染并补空格,所以比较结果时要写(first , second)这种形式。
quote!展开出的代码构造crate::TokenStream,因此它只在过程宏宿主里可用,普通程序里没有这个类型。
边界
- 通用语法节点只保留分类后的 token,不提供与字段一一对应的 AST。
DeriveInput只接受struct与enum的定义形式,且必须带花括号体——Riddle 的结构体只有命名字段一种写法(struct Wrapper(i32);是解析错误),也没有 union 条目,所以Fields::Unnamed只出现在枚举变体上。ParseStream只有peek_*、next、remaining、parse这几个接口,没有 Rustsyn的解析宏与组合子。Span::mixed_site()当前等价于Span::call_site(),Span::join返回Option<Span>。- 重复语法只有
*,没有?或+。
编写过程宏
过程宏是宏包导出的、在编译期运行的 Riddle 函数。它收到的是 token 而不是文本,返回的 token 会被插回调用点再继续解析。宏包由clue编译成宿主平台的动态库,由独立的 runner 进程按需执行。
宏包代码只在过程宏宿主里编译:clue 编译宿主时前置注入std/std/proc_macro.rid与std/std/syn.rid,并内置quote!。riddlec单独编译不了这些片段,所以本页的宏包代码与调用方代码都用 text 围栏。
宏包清单
clue new没有--proc-macro选项,先建普通库再改清单:
clue new --lib answer-macros
[package]
name = "answer-macros"
version = "0.1.0"
[lib]
name = "answer-macros"
path = "src/lib.rid"
proc-macro = true
[dependencies]
proc-macro = true把包的类别定为过程宏包:产物是answer-macros.proc-macro(动态库)加answer-macros.proc-macro-runner(可执行文件),不会链进使用方的程序。宏包可以依赖另一个宏包(本地 path 依赖即可),clue check也能直接检查宏包自身。
宿主里可直接使用TokenStream、TokenTree、Group、Ident、Punct、Literal、Span、Diagnostic、Delimiter、Spacing、ToTokens与syn模块,不需要声明依赖;quote!也是内置的,不需要导入。细节见内置 syn 与 quote!。
三种导出属性
| 种类 | 导出属性 | 签名 | 调用位置 |
|---|---|---|---|
| 函数式宏 | #[proc_macro] | fun (input: TokenStream) -> TokenStream | name!(...),出现在表达式、条目、类型、模式位置 |
| derive 宏 | #[proc_macro_derive(Name)] | fun (input: TokenStream) -> TokenStream | #[derive(Name)],用于结构体与枚举 |
| 属性宏 | #[proc_macro_attribute] | fun (args: TokenStream, item: TokenStream) -> TokenStream | #[name(...)],用于条目 |
构建宏包时逐条校验签名,任一条不满足都会让构建失败:
- 函数必须是
pub; - 不能有泛型参数;
- 参数个数固定:属性宏两个
TokenStream,函数式宏与 derive 宏一个; - 返回类型必须是
TokenStream; - 一个函数最多挂一个导出属性,宏名不能重复,整个包一个宏都不导出也会失败。
这些检查发生在clue侧,报的是普通错误文本(退出码 1),没有错误码前缀。
展开过程
编译器先在 token 文档里定位调用点,把调用点的参数整体作为平衡的 token tree 交给宏,再把结果写回文档重新解析,如此循环到没有可展开的调用。宏看到的是 token tree,空白和注释不在其中。
- derive 宏与属性宏的输出必须是顶层条目,否则报 E0400(
derive macro output must contain only top-level items、attribute macro output must contain only top-level items)。 - 输出里的宏会继续展开,嵌套深度上限 32,超过时报
process macro expansion exceeded the maximum depth of 32。 - 函数式宏的展开结果直接替换调用点,所以输出必须符合所在位置的语法:表达式位置输出表达式,条目位置输出条目。
- 属性宏只能标记条目,不能标记语句或表达式。
第一个函数式宏
#[proc_macro]
pub fun answer(_input: TokenStream) -> TokenStream {
TokenStream::from_str("42").unwrap_or(TokenStream::new())
}
TokenStream::from_str做词法分析,失败返回LexError;这里用unwrap_or退回空流。参数名写_input表示不读输入,Riddle 也允许未使用的参数。
使用方照常声明依赖并导入宏:
[dependencies]
answer_macros = { package = "answer-macros", path = "../answer-macros" }
use answer_macros::answer;
fun main() -> i32 {
answer!()
}
derive 宏
derive 宏拿到整个条目的 token。用syn::parse解析成DeriveInput比手写 token 遍历可靠:
use crate::std::vector::Vector;
use syn::{Attribute, Data, DeriveInput, parse};
fun skipped(attrs: &Vector<Attribute>) -> bool {
for attr in attrs.as_slice() {
if attr.tokens.to_string().contains("skip") {
return true;
}
}
false
}
#[proc_macro_derive(Getters, attributes(getter))]
pub fun derive_getters(input: TokenStream) -> TokenStream {
let parsed = match parse::<DeriveInput>(input) {
Result::Ok(value) => value,
Result::Err(error) => {
error.emit();
return TokenStream::new();
},
};
let mut getter_fns = Vector::new();
match &parsed.data {
Data::Struct(data) => {
for field in data.named.as_slice() {
if skipped(&field.attrs) {
continue;
}
let field_name = &field.ident;
let field_ty = &field.ty;
getter_fns.push(quote! {
pub fun #field_name(&self) -> &#field_ty {
&self.#field_name
}
});
}
},
Data::Enum(_) => {
Diagnostic::error(
parsed.ident.span(),
"Getters can only be derived for structs",
).emit();
return TokenStream::new();
},
}
let struct_name = &parsed.ident;
let generic_params = &parsed.generics.tokens;
let where_clause = &parsed.generics.where_clause;
quote! {
impl #generic_params #struct_name #generic_params #where_clause {
#(#getter_fns)*
}
}
}
use answer_macros::Getters;
#[derive(Getters)]
struct Point<T> where T: Copy {
x: T,
y: T,
}
fun main() -> i32 {
let point = Point { x: 1, y: 2 };
*point.x() + *point.y()
}
要点:
- 解析失败时先
error.emit()再返回空TokenStream,让调用点看到定位到输入的错误;宏里panic只会让 worker 失败,用户得到的是展开失败而不是有位置的诊断。 parsed.data、parsed.generics按字段取,data.named.as_slice()按借用遍历;不要用结构体模式拆DeriveInput(原因见「常见问题」)。#(#getter_fns)*把Vector里的每个TokenStream按顺序展开;同一个重复块里的多个变量长度必须相等,否则宏进程 panic,本次展开以 E0400 失败。- 宏返回的 token 会被重新解析,展开失败的诊断定位到调用点,也就是
#[derive(...)]那一行。
属性宏
属性宏收到属性括号里的 token 和被标记的整个条目,返回的必须是条目:
#[proc_macro_attribute]
pub fun trace_level(args: TokenStream, item: TokenStream) -> TokenStream {
if args.is_empty() {
Diagnostic::error(
Span::call_site(),
"trace_level requires a numeric argument",
).emit();
return TokenStream::new();
}
let mut output = TokenStream::new();
output.extend(TokenStream::from_str("const TRACE_LEVEL: i32 = ").unwrap_or(TokenStream::new()));
output.extend(args);
output.extend(TokenStream::from_str(";").unwrap_or(TokenStream::new()));
output.extend(item);
output
}
use answer_macros::trace_level;
#[trace_level(3)]
fun main() -> i32 {
TRACE_LEVEL
}
#[trace_level(3)]展开成const TRACE_LEVEL: i32 = 3;再加原来的条目。参数 token 原样插入输出,属性宏只管条目这一层。
helper 属性
#[proc_macro_derive(Name, attributes(a, b))]为这个 derive 注册 helper 属性。helper 属性只能出现在该 derive 标记的条目、枚举变体和字段上;没有注册就用会报cannot find attribute ...; derive helper attributes must be declared。上面的Getters注册了getter,字段写成:
#[derive(Getters)]
struct User {
name: String,
#[getter(skip)]
password_hash: String,
}
helper 属性连同#和方括号一起出现在Field.attrs里,宏用attr.tokens.to_string().contains("skip")判断即可。同一个 helper 名字重复声明会在构建宏包时报错。
诊断与 span
Diagnostic有四个级别,emit()把它们交给编译器:
Diagnostic::error(span, "message").emit();
Diagnostic::warning(span, "message").emit();
Diagnostic::note(span, "message").emit();
Diagnostic::help(span, "message").emit();
span 决定错误指向哪里:
- 输入 token 自带的 span(
field.ident.span())指向源码里的具体位置; Span::call_site()指向整个宏调用,Span::mixed_site()当前与它等价;span.join(other)返回覆盖两者的Option<Span>。
syn的Error也带span与message,error.emit()等价于发一条 error 级诊断。
导入与命名
宏名与类型、trait、值不在同一个命名空间,可以同名。derive 宏、属性宏和函数式宏都需要use导入才能用短名,不导入时写限定路径#[derive(answer_macros::Getters)]。分组、别名、glob 和pub use重导出都支持:
use answer_macros::{Getters, answer as value, trace_level};
#[trace_level(3)]
#[derive(Getters)]
struct User {
name: i32,
}
fun main() -> i32 { value!() }
worker
每个宏包对应一个常驻子进程,第一次展开时才启动,之后复用:
- 单次展开超时 10 秒,超时后 worker 被杀掉,错误是
proc-macro加宏名加exceeded the 10 second timeout,下次展开重新启动; - 请求与响应的消息上限都是 16 MiB,展开结果另有 128 条的缓存上限;
- 宏里的
panic由 worker 承担,clue把它报成本次展开失败,不会跟着崩; - stdout 是宏与
clue之间的私有协议通道,所以宏里的print、println!输出走 stderr; - 协议版本与线格式是内部实现,不构成对外接口。
常见问题
cannot find derive macro ... in this scope; import it with use package::...;:没有导入,或者宏包没有导出这个名字。cannot find macro ... in this scope:函数式宏的导入缺失。cannot find attribute ...; derive helper attributes must be declared with attributes(...):字段上用了没有注册的 helper 属性。derive macro output must contain only top-level items:derive 输出里混进了语句或表达式。- 展开超时(
exceeded the 10 second timeout)也可能是宏自己卡住:用结构体模式拆DeriveInput(漏掉私有字段tokens)后再移动data,当前实现会停在这里,改用parsed.data这样的字段访问。 - 宏返回空
TokenStream通常意味着前面的诊断已经发出,先看诊断。
编辑器与 LSP
riddle-lsp 是工具链里唯一的语言服务器,仓库为 VS Code、Helix、IntelliJ IDEA 和 Zed 各带一份适配。四份适配的完成度不一样:只有 VS Code 和 Helix 会把 Clue.toml 也发给服务器。
riddle-lsp 的参数与传输
服务器只支持 stdio:启动后从 stdin 读 JSON-RPC,往 stdout 写。没有 TCP、端口或 socket 模式。全部参数就三个:
| flag | 默认 | 作用 |
|---|---|---|
--no-std | 关 | 不加载内置标准库 |
--completion-delay-ms <MS> | 40 | 把窗口内到达的补全请求合并成一次 |
--trace-latency | 关 | 把各阶段耗时打印到 stderr;也可用环境变量 RIDDLE_LSP_TRACE_LATENCY |
另有 clap 自带的 -V/--version 和 -h/--help。没有 --tcp、--port、--stdio、--log、--log-file、--clientProcessId 之类的选项。serverInfo 的名字是 riddle-lsp,版本串是 crate 版本加编译时的 git hash。
位置编码在 initialize 时协商,支持 utf-16、utf-8、utf-32(优先 utf-16)。客户端一个都不提供时,initialize 返回 invalid_params,消息是 riddle-lsp needs one of the position encodings utf-16, utf-8 or utf-32; the client offered only: ...,服务器不会退回错误坐标。
--no-std 直接关掉标准库加载,语义高亮、悬停和补全都会失去 std 里的信息,只保留当前缓冲能解析出的部分。
工作区发现会跳过 .git、.clue、target、node_modules、dist;如果打开的是虚拟工作区根,就只加载 [workspace].crates 里的成员。索引在保存和文件事件上重建,textDocumentSync.save 是开启的,但没有 willSave。
服务器实现了什么
| 能力 | 细节 |
|---|---|
| 文档同步 | open/close、增量 change、save |
| 诊断 | push 与 pull 都有;diagnosticProvider 打开 interFileDependencies 与 workspaceDiagnostics;workspace/diagnostic 覆盖未打开模块与本地依赖 |
| 补全 | . 和 : 触发;跨文件、自动导入、重名路径区分;resolveProvider 为 false |
| 悬停 | 函数签名、推断类型、struct/enum 声明;方法调用显示实例化后的签名 |
| 签名帮助 | ( 和 , 触发,, 重新触发,跟踪当前参数 |
| 跳转 | 定义、声明、类型定义、trait 方法实现、调用层级;类型层级的条件见下文 |
| 引用与重命名 | 项目级查找引用;prepareProvider 为 true;字段、trait 方法、导入别名都能重命名 |
| 符号 | 文档符号(含 impl 块)与工作区符号 |
| 语义高亮 | 全文与区间两种请求,全文带 delta;legend 有 16 种 token type 和 4 种 modifier(declaration、mutable、static、defaultLibrary) |
| Inlay Hint | let 绑定推断类型、匿名函数参数类型(已写类型的不提示)、多行链式调用每一级的结果类型、调用实参名 |
| 折叠与选择范围 | 花括号块、连续注释行、连续 use 组;结构化选择范围 |
| 文档链接 | mod foo;(内联 mod 跳过)与 use 的首段,链到 foo.rid 或 foo/mod.rid;两种文件都在时不产生链接,未落盘的缓冲不产生链接 |
| 格式化 | 全文与区间格式化,tabSize / insertSpaces 由客户端传入 |
| 代码动作 | quickfix、source.organizeImports、source.addMissingImports、source.fixAll,resolveProvider 为 false |
| Clue.toml | 诊断、节与键补全、键悬停、节符号 |
诊断的 source 是 riddle(编译器)或 clue(清单与项目加载)。带错误码的诊断会附上 codeDescription,指向 https://riddle-lang.github.io/docs/errorcode.html#<小写错误码>。服务器自己产生的码只有 LSP0001:缓冲与编辑器失同步时用它,而不是静默清掉诊断。
quickfix 的修复由诊断驱动:
| 触发 | 动作 |
|---|---|
E0031 且消息含 cannot call a mutable closure through an immutable binding | 给匿名函数绑定加 mut |
E0031 且消息以 cannot assign to 开头 | 给绑定加 mut |
E0046 且消息以 requires an unsafe block 结尾 | 包一层 unsafe 块 |
E0007 且消息以 missing field 开头 | 插入缺失字段(todo!() 占位) |
E0039 且消息含 missing pattern | 补 match 分支或通配分支 |
E0051 且消息是 empty use declaration | 删掉空的 use |
E0056 且消息建议改用 drop(value) | 改写成 drop(...) |
需要分析的修复有 E0050 的名字纠错与自动导入、E0013 的方法名纠错、E0026 的 trait 方法 stub。source.organizeImports 排序、去重并删除空 use;source.addMissingImports 只合并自动导入类编辑;source.fixAll 收集附加编辑并避开重叠。
调用层级只包含编译器能静态解析的目标,不推测函数指针、匿名函数或 trait 的运行时分派。类型层级不在静态 capabilities 里(当前依赖的 lsp-types 没有这个字段),只有客户端声明支持动态注册时,服务器才在 initialized 阶段注册 textDocument/prepareTypeHierarchy;客户端不支持动态注册时这项功能不可见。文件重命名监听只广告 **/*.rid 且 kind = File。
几个容易被忽略的限制:codeActionProvider、completionProvider、documentLinkProvider 的 resolveProvider 都是 false;单文档场景(未纳入 Clue 项目的文件)走独立的检查会话,语义高亮、Inlay Hint 和代码动作不保证跨包;不可重命名的目标返回 invalid_params(例如 there is nothing to rename at this position、this item is not defined in the project and cannot be renamed),而不是空结果。
编辑器支持矩阵
| 项目 | VS Code | Helix | IntelliJ IDEA | Zed |
|---|---|---|---|---|
| 适配形式 | JavaScript 扩展 | languages.toml 加查询文件 | Kotlin 插件 | WASM 扩展 |
.rid 识别 | 语言 id riddle,扩展名 .rid | file-types 里的 { glob = "*.rid" } | 文件类型 Riddle,扩展名 rid | path_suffixes = ["rid"] |
| 基础高亮 | 自带 TextMate 语法 | Rust Tree-sitter grammar 加 highlights.scm | 无 | Rust Tree-sitter grammar 加 highlights.scm |
Clue.toml 路由 | 有,独立 clue 语言 id | 有,按文件名 glob | 无 | 无 |
| 适配层的 Inlay Hint 开关 | riddle.inlayHints.enabled | 无 | 无 | 无 |
| 服务器路径 | riddle.server.path 与 riddle.server.arguments | languages.toml 的 command 与 args | 无,硬编码 riddle-lsp | lsp.riddle-lsp.binary 的 path 与 arguments |
| 打包产物 | riddle-vscode.vsix | riddle-helix.zip | riddle-intellij.zip | riddle-zed.zip |
| 安装方式 | code --install-extension | 合并到 Helix 配置目录 | Install Plugin from Disk | Dev Extension |
默认情况下四份适配都从 PATH 找 riddle-lsp。VS Code、Helix、Zed 可以改成显式路径,IntelliJ 只能依赖 PATH;Zed 的扩展在设置和工作树的 PATH 里都找不到时报 riddle-lsp was not found on PATH。
VS Code
扩展的 documentSelector 同时匹配 riddle 和 clue 两种语言,所以 .rid 与 Clue.toml 共用同一个服务器实例。激活事件是 onLanguage:riddle、onLanguage:clue 和 workspaceContains:**/Clue.toml。
TextMate 语法(source.riddle)只覆盖 .rid;Clue.toml 靠 VS Code 内建的 TOML 高亮显示,语义信息来自服务器。扩展还注册了一个名为 riddle 的图标主题。
| 配置项 | 类型 | 默认 | 作用 |
|---|---|---|---|
riddle.server.path | string | riddle-lsp | 服务器可执行文件路径 |
riddle.server.arguments | string[] | 空 | 传给服务器的参数 |
riddle.inlayHints.enabled | boolean | true | Inlay Hint 总开关 |
riddle.trace.server | off / messages / verbose | off | VS Code 与服务器之间的 JSON-RPC 跟踪 |
riddle.inlayHints.enabled 是扩展自己实现的:关掉后中间件直接返回空数组,其余能力不受影响。
构建扩展需要 npm:npm run build 先生成图标,再用 esbuild 把 extension.js 打成 dist/extension.js;产出 vsix 的是 vsce package(打包脚本按 npm ci、npm run check、vsce package 的顺序执行)。安装用 code --install-extension riddle-vscode.vsix,也可以在扩展面板的 ... 菜单里选 Install from VSIX…。扩展的 README 示例里写的是旧版本号,实际文件名以打包输出为准。仓库里没有发布到 Marketplace 的配置,release 流程只把 vsix 作为发布资产上传。
Helix
languages.toml 注册一个服务器和一个语言:
[language-server.riddle-lsp]
command = "riddle-lsp"
[[language]]
name = "riddle"
scope = "source.riddle"
language-id = "riddle"
file-types = [
{ glob = "*.rid" },
{ glob = "Clue.toml" },
]
roots = ["Clue.toml", ".git"]
comment-tokens = "//"
indent = { tab-width = 4, unit = " " }
grammar = "rust"
language-servers = ["riddle-lsp"]
Clue.toml 由同一个 riddle 条目按文件名匹配,所以清单的诊断、补全和悬停也来自这个服务器。查询文件有三个:runtime/queries/riddle/highlights.scm、indents.scm、textobjects.scm。
仓库里没有 Helix 的安装说明。按 Helix 的通用做法:把上面的两个配置块合并进 Helix 配置目录的 languages.toml(已有文件时不要整个覆盖),把 runtime/queries/riddle 复制到配置目录的 runtime/queries/riddle,然后重启 Helix。Linux / macOS 的配置目录是 ~/.config/helix,Windows 是 %AppData%\helix。安装后可以用 hx --health riddle 检查语言服务器和三类查询是否都被找到。
需要指定服务器路径或参数时改 [language-server.riddle-lsp]:
[language-server.riddle-lsp]
command = "/path/to/riddle-lsp"
args = ["--completion-delay-ms", "25"]
Helix 的 grammar = "rust" 意味着 Tree-sitter 按 Rust 语法近似处理结构;Riddle 专有的标识符分类来自服务器的语义高亮。
IntelliJ IDEA
插件 id 是 org.riddlelang.intellij,依赖 com.intellij.modules.platform 和 com.intellij.modules.lsp。它注册文件类型 Riddle(扩展名 rid),并通过平台自带的 LSP API 启动服务器。启动命令硬编码为 riddle-lsp,从 IDE 进程的 PATH 里查找,插件没有提供任何配置项,也不传额外参数;改完系统 PATH 要完全退出并重启 IDE。
插件没有贡献 TextMate 或 Tree-sitter 语法,语义高亮完全来自服务器;服务器没起来时 .rid 文件没有任何 Riddle 高亮。它也不支持 Clue.toml,清单文件仍按普通文件处理。
目标平台是 IntelliJ Platform 2026.1 及以上,不支持 Android Studio;构建需要 JDK 25 或更高版本作为 Gradle JVM。只构建插件时:
Set-Location editors\intellij
.\gradlew.bat buildPlugin
产物在 build/distributions/riddle-intellij-<version>.zip。安装走 Settings | Plugins | Install Plugin from Disk…,选择 ZIP 后重启 IDE。仓库里没有发布到 Marketplace 的配置。
Zed
扩展声明一个语言服务器 riddle-lsp,语言名 Riddle,schema_version = 1。语言配置里 grammar = "rust"、path_suffixes = ["rid"]、line_comments = ["// "]、tab_size = 4、hard_tabs = false,并带括号自动闭合;高亮查询在 languages/riddle/highlights.scm。
扩展本体是 WASM(zed_extension_api 0.1.0),启动服务器时先读工作树的 lsp.riddle-lsp.binary 设置,没有再退回 which("riddle-lsp"):
{
"lsp": {
"riddle-lsp": {
"binary": {
"path": "/path/to/riddle-lsp",
"arguments": ["--completion-delay-ms", "25"]
}
}
}
}
基础颜色来自 Rust Tree-sitter grammar;服务器的语义高亮需要显式开启:
{
"languages": {
"Riddle": {
"semantic_tokens": "full"
}
}
}
Zed 不支持 Clue.toml:zed_extension_api 0.1.0 无法为一个服务器声明第二种语言。
仓库里没有 Zed 的安装说明。扩展目录顶层有 extension.toml,可以按 Zed 的 Dev Extension 流程从解压目录(或仓库里的 editors/zed)加载。打包产物 riddle-zed.zip 里是 Cargo.lock、Cargo.toml、extension.toml、languages 和 src。
Clue.toml 的 schema 偏差
服务器侧的清单 schema 是一份手写白名单,覆盖 package、dependencies、dev-dependencies、features、bin、lib、test、example、bench、workspace、runtime、build 十二个节,和 clue 的解析器不共享代码。已知两处不一致:
[build].cache在clue里是合法字段,但服务器的build白名单只有target,编辑器会报unknown key build.cache(CLUE0003警告)。这是误报。[package]的白名单只有name、version、license、entry、publish,所以合法的description、authors、repository也会被标成未知键(unknown key package.description之类)。
清单里的字段、默认值和消息以 Clue 构建器 为准。
打包
editors/package.sh(Bash,需要 zip)和 editors/package.ps1(PowerShell)都在 editors/dist 下产出四个文件:riddle-vscode.vsix、riddle-intellij.zip、riddle-helix.zip、riddle-zed.zip。
- VS Code:
npm ci、npm run check(语法检查加图标生成和 esbuild 打包),再用vsce package。 - IntelliJ:
gradlew buildPlugin,脚本取build/distributions里最新的riddle-intellij-*.zip复制成riddle-intellij.zip。 - Helix:把
languages.toml和runtime压成 zip。 - Zed:把
Cargo.lock、Cargo.toml、extension.toml、languages、src压成 zip。
打 VS Code 包需要 Node.js 与 npm(npm ci、esbuild、vsce),打 IntelliJ 包需要 JDK 25 或更高版本,首次构建时 Gradle 会下载 IntelliJ Platform SDK;Helix 和 Zed 的产物只是把现成文件压缩。
常见问题
编辑器说找不到 riddle-lsp。 先在编辑器内置终端里运行 riddle-lsp --version。外部终端可用而编辑器里不可用时,完全退出编辑器再启动,让它重新读取 PATH;或者直接配置绝对路径(VS Code 用 riddle.server.path,Helix 改 command,Zed 用 lsp.riddle-lsp.binary.path,IntelliJ 只能修 PATH)。
VS Code 有颜色但没有诊断。 基础高亮由扩展内的 TextMate 语法提供,和服务器是否启动无关。检查 riddle.server.path 与 riddle.server.arguments,再打开 Output 面板看 Riddle Language Server 的输出。
Helix 没有高亮或缩进。 运行 hx --health riddle,确认语言服务器和三个 .scm 都显示可用;不可用时检查它们是否在配置目录的 runtime/queries/riddle 下。
Zed 只有基础语法颜色。 在 settings.json 里把 languages.Riddle.semantic_tokens 设为 "full",然后重启 language server。
IntelliJ 里没有诊断或高亮。 确认 IDE 是 2026.1 或更高版本,在 IDE 内置终端运行 riddle-lsp --version;命令不可用时修好 PATH 并完全重启 IDE,仍不行就用 Help | Show Log 看 LSP 启动错误。插件本身没有语法回退。
看得到跳转定义但看不到类型层级。 类型层级依赖客户端的动态注册能力,客户端不支持时服务器不会注册 textDocument/prepareTypeHierarchy,这不是配置问题。
清单里出现 unknown key build.cache 或 unknown key package.description。 这是服务器 schema 的已知偏差,clue 接受这些字段,警告可以忽略。
FFI 与底层工具链
本页覆盖直接操作编译器与运行时的部分:riddlec、riddle fmt/run/repl、C 后端产物、extern "C"、GC 运行时与解释器。项目级构建、依赖与目标平台配置见Clue 构建器。
riddlec
riddlec是单文件级编译器:读入 Riddle 源码,跑完整管线,按需要打印 MIR 或写出 C。
riddlec [--verbose] [--no-std] [--backend c] [--target <triple>] [--output <file>] <file>...
| flag | 短 | 取值 | 说明 |
|---|---|---|---|
--verbose | -v | — | 逐阶段状态打到 stdout |
--no-std | — | — | 不加载随编译器附带的 std |
--backend | -b | c | 生成 C 并写文件 |
--emit | — | c / mir | 见下 |
--target | — | triple | 目标平台,同时决定 usize/isize 的宽度;缺省是宿主或RIDDLE_TARGET |
--output | -o | 路径 | 输出文件 |
--version | -V | — | 版本号加构建时的 git hash |
--help | -h | — | 帮助 |
--emit与--backend互斥,两者的行为也不对称:
--emit mir把整个程序的 MIR 打印到 stdout,不写文件,退出码 0,输出以module main {开头。--emit c与--backend c是同一条代码生成路径:写到-o,没给-o就写到第一个输入文件同名的.c。两个 flag 互斥,所以--emit c不会带上--backend,但生成结果与之一致,main缺失同样在这一阶段报E0401。
其余行为:
- 多个
FILES会拼成一个包(用换行分隔),各自保留 source map,因此文件之间可以互相引用;入口是第一个文件。目录不能作为输入。 -o以.c结尾就原样使用;其它扩展名追加.c(-o app.h写出app.h.c);不给-o时用第一个输入文件的 stem 加.c,写在当前工作目录。- 没有
main时--backend先报error[E0401],消息为 “nomainfunction found in the entry package”,不写文件。 -v按顺序打印target: <triple>以及parse、macro expansion、hir lower、type check、move + escape analysis、MIR lowering的状态(ok/failed/skipped);parse 失败时后面几项直接打印为skipped。- 诊断只有 rustc 风格的人类可读文本,写 stderr,末尾是
error: aborting due to N previous error(s)。没有 JSON 输出选项。
退出码:
| 情形 | 退出码 |
|---|---|
| 成功 | 0 |
| 编译或检查有错误、没有输入文件、输入文件读不到、工作线程 panic | 1 |
参数错误(flag 互斥、非法--target等) | 2 |
--target恰好支持七个 triple:x86_64-unknown-linux-gnu、aarch64-unknown-linux-gnu、i686-unknown-linux-gnu、x86_64-pc-windows-msvc、i686-pc-windows-msvc、aarch64-pc-windows-msvc、aarch64-apple-darwin。传给--target的其它值由 clap 报参数错误(退出码 2),RIDDLE_TARGET里的非法值走普通错误路径。
选中的 triple 同时给出 usize/isize 的宽度:i686-* 下 4294967296usize 报 E0011,因为生成的 C 要把它装进 32 位的 size_t;在 64 位宿主上交叉编译到 i686-* 也同样拒绝,宿主宽度不再参与判断。riddle run和riddle repl 不选 triple——MIR 解释器在任何宿主都把 usize 当成一个 8 字节字,所以那条路径固定按 64 位检查。
环境变量:RIDDLE_TARGET在没给--target时决定目标;设置了RIDDLEC_PHASE_TIMING(值任意)时每个阶段的耗时打印到 stderr。
riddle fmt
riddle fmt复用riddlec的格式化器,编辑器里的textDocument/formatting也是同一份实现。
| flag | 取值 | 默认 | 说明 |
|---|---|---|---|
--emit | files / stdout / check | files | 写回文件、打到 stdout、只检查 |
--check | — | 关 | 等价于--emit check,与--emit互斥 |
--tab-size | 整数 | 4 | 缩进宽度,最小 1 |
--hard-tabs | — | 关(用空格) | 用制表符缩进 |
[FILES]... | 路径 | 空 | 不给文件或给-时读 stdin |
- stdin 模式下再给别的文件名会报错并退出码 2。
- 每个文件单独处理:读不到就报错并继续下一个;有词法或语法错误就打
riddle fmt: <name>:<line>:<col>: <message>并跳过该文件。 files只写内容真的变化的文件;check对每个会变的文件打印would reformat <path>。- 退出码:任何文件失败,或
check模式下有文件会变,都是 1;否则 0。stdin 加check且内容不一致时退出码 1。 - 格式化只改空白、保留 token 文本,换行风格跟随原文(原文含 CRLF 就用 CRLF)。
riddle fmt src/main.rid
riddle fmt --check src/main.rid
cat src/main.rid | riddle fmt --emit stdout
riddle run
riddle run [--seed <N>] <FILE> [-- <程序参数>...]
riddle run编译单个文件后交给内置解释器,不调用 C 工具链。文件之后的参数原样传给程序,--可写可不写;std::env::args()的第 0 项是源文件路径,不是可执行文件名。--seed <u64>设置std::random的种子,0表示按时钟播种。
| 情形 | 退出码 |
|---|---|
| 源文件读不到 | 2 |
编译失败(含缺少main,此时报error[E0401]) | 1 |
| 程序正常返回 | main的返回值 |
panic、abort、unreachable、调用未支持的extern | Windows 3,其它平台 134 |
| 栈溢出与内部错误 | 101 |
panic 的位置经过 source map 映射,指向真实源文件(穿过宏展开,也能映进随编译器附带的 std)。
riddle repl
riddle repl没有 flag,提示符是>>>,括号未闭合时用|续行(判定只看圆括号、方括号、花括号的净余额,不解析语法)。
- 顶层定义(
fun、struct、use等)累积进会话;let与表达式行写进一个自动生成的main。 - 每次求值都把整个会话重新编译并重跑,所以之前的副作用会重放,只有新增的输出会上报。
- 表达式打印自己的
{:?}值,并绑定到__供后续行使用;值是()时不打印。 - 命令:
:help或:?、:reset(清空会话)、:mir(打印最近一次成功编译的 MIR)、:quit/:exit/:q。其它:xxx只提示未知命令,不退出。 panic("...")这种不带路径的调用在 REPL 里不可用(会报未解析的名字),用panic!("...")宏。- 只有
:quit与 EOF 结束会话,都是退出码 0;行编辑器初始化失败是 1。 - 历史只存在内存里,没有
:load、:type等命令。
C 后端
每个包只生成一个.c文件,没有.h:prelude、类型定义、GC 描述符表、函数前置声明、函数体和 C 入口都在这一个文件里。文件头两行是// Generated by Riddle和一条建议的编译命令。
符号命名:
| 对象 | 名字 |
|---|---|
| 函数 | riddle_f_<名字每个字节的两位小写 hex> |
| 类型 | riddle_t_<symbol 的 hex>,symbol 形如struct::<id>::<Name> |
| 字段 | riddle_m_<hex> |
| 形参与局部值 | riddle_v_<hex> |
| 元组 | riddle_tuple_<hash16>,字段是f0、f1… |
| 函数指针 | riddle_fn_<hash16> |
| 导出函数 | 源码原名(extern "C"定义或#[c_export]) |
main有两种形态,取决于程序是否需要运行时(用到堆分配、调用rgc_*或读进程参数):
/* 不需要 runtime,例如 --no-std 下空的 fun main() {} */
int main(void) { return 0; }
/* 需要 runtime */
int main(int argc, char **argv) {
riddle_args_init((int32_t)argc, argv);
int rgc_stack_anchor = 0;
rgc_init(&rgc_stack_anchor);
return (int)riddle_f_main();
}
需要 runtime 时源码里的main改名成 mangled 名,由这层包装调用。默认加载 std 时基本都会走这一形态。返回()的main在 C 里是int加return 0;,返回i32的就是int32_t main(void);带参数的main在需要 runtime 时是编译错误(runtime-backed main cannot take parameters)。
类型映射:
| Riddle | C |
|---|---|
i8 / i16 / i32 / i64 | int8_t / int16_t / int32_t / int64_t |
u8 / u16 / u32 / u64 | uint8_t / uint16_t / uint32_t / uint64_t |
isize / usize | ptrdiff_t / size_t |
bool | bool |
char | uint32_t |
str / &str / &mut str | riddle_str({ const char* ptr; size_t len; }) |
[T] / &[T] / &mut [T] | riddle_slice({ void* ptr; size_t len; }) |
&T / &mut T | T*;共享引用也不加const,因为mut字段要能通过共享引用写入 |
*const T / *mut T | 内部值是T*;extern "C"声明里统一成void* |
[T; N] | C 数组;N == 0写成[1] |
struct S | riddle_t_<hex> |
(A, B) | 生成的 struct,字段f0、f1 |
enum E | 带tag和 payload 字段的 struct,变体字段名是{Variant}_{index}或{Variant}_{field} |
() | 返回位置是void,值位置是riddle_unit(unsigned char) |
运算与陷阱的生成方式与解释器一一对应:整数加减乘先提升到无符号 carrier 再还原;除零与MIN / -1由内联守卫打印riddle: division by zero之类的消息后abort();移位量按宽度取模;浮点转整数饱和(NaN 变 0);浮点余数调用fmod/fmodf;下标越界内联检查并abort();&str相等按内容(memcmp)比较,裸指针比较按地址。panic 渲染成thread 'main' panicked at <file>:<line>:<column>:加消息,然后abort()。
后端会直接拒绝的形态:
- 参数或返回值是裸
[T]、&[str]、切片返回值; - 裸
str参数或返回值(类型检查器通常更早报 E0043); - 导出名或 extern 名不是合法的 C 标识符;
gc = false时出现rgc_*调用。
手动编译生成的 C
riddlec --backend c -o hello.c hello.rid
cc hello.c crates/gc/src/runtime.c crates/gc/src/args_runtime.c -o hello
生成的头部注释就是这条命令的形态:需要 runtime 时是cc out.c runtime.c args_runtime.c -o out,否则是cc out.c -o out。手动编译要自己保证 C11,并显式给出一个内存运行时;进程参数由args_runtime.c提供,和内存运行时分开,因此自定义分配器不必实现std::env那部分。
两个容易踩的点:
- 浮点余数会生成
fmod/fmodf调用。在 glibc 目标上链接时要自己加-lm;clue的链接参数里没有-lm。 clue也不加-pthread:随附运行时是单线程的,不需要线程库。
extern “C”
声明块必须是unsafe extern "C" { ... },否则解析期报extern blocks must use unsafe extern。块里的函数默认是 unsafe 的,只有显式写成safe fun的声明才能在安全代码里直接调用:
unsafe extern "C" {
safe fun abs(x: i32) -> i32;
fun malloc(size: usize) -> *mut u8;
fun free(pointer: *mut u8);
}
fun main() -> i32 {
let buffer = unsafe { malloc(16) };
unsafe { free(buffer); }
abs(-42)
}
- 没有函数体的单函数声明(
extern "C" fun name(...) -> T;)是解析错误:single-function extern declarations are not supported; use an unsafe extern block。 - 带函数体的顶层
extern "C" fun name(...) { ... }是导出定义,不是导入。 - 声明里不允许泛型参数,解析期就会报错。
- 后端不按符号名内置任何 C 函数,
abs、malloc、strlen都是普通外部符号,由系统库或你自己的 C 代码提供。 safe fun是声明者对调用契约的承诺:标错的声明会让安全代码直接踩到未定义行为。
&str跨导入边界时会复制成 NUL 结尾的 C 字符串:
unsafe extern "C" {
safe fun puts(text: &str) -> i32;
}
fun main() -> i32 {
puts("hello from riddle")
}
调用点把参数字节复制到临时缓冲区并补一个 NUL,以const char*传入,调用结束即释放,所以 C 侧不能保存这个指针;参数里含嵌入的 NUL 会在运行时abort()(riddle: C string contains an embedded NUL)。导入返回&str时,C 侧必须返回 NUL 结尾的字符串,长度由strlen恢复后拷进托管内存。
切片只支持&[T](T有大小且不是str),并且会拆成两个 C 参数:&[i32]变成const int32_t*加size_t,&mut [T]用T*加size_t。结构体可以按值传递,生成的是extern int32_t takes_pair(riddle_t_<hex>);这样的声明。
unsafe、原始指针与as转换:
unsafe fun dereference(pointer: *const i32) -> i32 {
unsafe { *pointer }
}
fun main() -> i32 {
let pointer = 0usize as *const i32;
unsafe { dereference(pointer) }
}
解引用原始指针、索引原始指针、调用 unsafe 函数都必须写在unsafe { }里(否则报 E0046)。unsafe fun的函数体仍从安全上下文开始,内部的不安全操作要自己再包一层块。unsafe不关闭类型、可变性、移动和借用检查;不安全函数项也不会满足安全的Fn、FnMut、FnOnce约束,不能借安全 callable 参数绕开检查。
导出给 C
两种写法都导出源码原名:
#[c_export]
pub fun add(x: i32, y: i32) -> i32 {
x + y
}
extern "C" fun multiply(x: i32, y: i32) -> i32 {
x * y
}
两者的&str ABI 不同:
- 带函数体的
extern "C" fun保持riddle_str { const char* ptr; size_t len; },长度不丢; #[c_export]把&str参数写成const char*,函数体入口用strlen还原长度,所以 C 侧必须传 NUL 结尾的字符串。
导出名直接占用 C 的全局命名空间,只有“是不是合法 C 标识符”这一层检查(含关键字黑名单),没有前缀和命名空间。普通 Riddle 函数(包括库包的pub函数)名字一律被 mangle,C 侧不能按名字调用。
C 侧看到的 ABI 仍在预览阶段,会不兼容地变化:例如rgc_alloc已经从rgc_alloc(size_t)变成rgc_alloc(size_t, const uint32_t*)。跨语言边界建议显式传指针和长度,而不是依赖内部结构体布局。
GC 运行时
默认运行时是crates/gc/src/runtime.c(mark-sweep),crates/gc/src/args_runtime.c提供进程参数,两者分开链接;gc = false时改用crates/gc/src/no_gc_runtime.c。
| 符号 | 签名 | 语义 |
|---|---|---|
rgc_init | void rgc_init(void *stack_bottom) | 记录栈底;没初始化时回收直接返回 |
rgc_alloc | void *rgc_alloc(size_t size, const uint32_t *descriptor) | size == 0提升为 1;按阈值决定是否先回收;校验描述符;≤1 KiB 走 size class,否则malloc;清零描述符列出的指针槽后登记;存活期间地址不移动 |
rgc_realloc | void *rgc_realloc(void *ptr, size_t size) | 保留原前缀,可能换地址,继承原描述符 |
rgc_free | void rgc_free(void *ptr) | 接受NULL;只释放精确命中的基址,内部指针与外来指针静默忽略 |
rgc_collect | void rgc_collect(void) | 一轮完整标记清扫 |
rgc_is_allocated | int rgc_is_allocated(const void *ptr) | 精确基址当前是否已登记 |
riddle_alloc_bytes | void *riddle_alloc_bytes(size_t size) | 无类型字节存储,默认运行时等于rgc_alloc(size, NULL) |
rgc_stat_* | 见下 | 统计读数 |
统计访问器:rgc_stat_live_bytes、rgc_stat_live_objects、rgc_stat_total_objects、rgc_stat_collections、rgc_stat_typed_objects、rgc_stat_conservative_objects、rgc_stat_marked_objects、rgc_stat_typed_payload_scans、rgc_stat_conservative_payload_scans、rgc_stat_last_mark_ns、rgc_stat_last_sweep_ns、rgc_stat_heap_bytes、rgc_stat_chunks。都返回size_t。
描述符:
descriptor[0]是指针槽数量,descriptor[1 + i]是第 i 个槽在载荷内的字节偏移。NULL表示布局未知,退回逐字保守扫描;{ 0u }表示精确的“没有指针”,最省。- 登记时校验:槽数超过
size / sizeof(uintptr_t),或某个偏移加一个机器字超出对象,都会静默退化为NULL。 - 编译器侧一张描述符最多列 512 个槽(
RGC_MAX_DESCRIPTOR_SLOTS):恰好 512 个槽仍然会生成描述符,第 513 个槽才退化为NULL。 - 只是“无法忠实描述”的情况也会退化:还没降级成 tagged struct 的
enum、查表失败的类型。 - typed 分配只清零描述符列出的槽,其余字节不清零;传
NULL的分配完全不清零。堆分配不保证返回零初始化内存。
分配形状与扫描:
- ≤1 KiB 的分配从 64 KiB 的 chunk 里按 16 字节 size class 切块(共 64 类),更大的直接
malloc;清扫只把块还给对应的空闲链或malloc,chunk 本身一直留着。 - 根集是捕获的寄存器加
[当前帧, stack_bottom)之间的机器字;保守扫描按机器字对齐,内部指针解析到所属对象。 - 对象头存在带外登记表里,交给用户的地址就是载荷起始地址;精确查找走地址哈希表。
- 每轮回收后阈值变为
max(RGC_MIN_HEAP, live * 2),RGC_MIN_HEAP默认 1 MiB,可以用-D覆盖(编译期宏)。 - 设置了
RGC_DEBUG_STATS(值任意,存在即开)时,每轮回收往 stderr 打一行,含存活字节与对象数、typed/conservative 计数、本轮回收量、mark/sweep 微秒、下次阈值与两类扫描次数。
这块 ABI 是技术预览:不支持移动式回收、finalizer 和多线程栈注册,实现按单线程写。
gc = false时:
- 只提供分配器:
riddle_alloc(malloc,失败abort())、riddle_alloc_bytes(等于riddle_alloc,永不扫描)、riddle_realloc、riddle_free,产物里没有任何rgc_*符号。 - 编译器把堆分配降成
riddle_alloc(sizeof(T))(不带描述符)、释放降成riddle_free,不生成描述符表,也不发rgc_init。 - 引用逃逸不再是可接受的提升,而是编译期错误 E0310:需要自己保证不返回、也不保存指向栈值的引用。
自定义 runtime 写在Clue.toml的[runtime] source里(相对包根的 C 文件路径,必须存在),并且只对二进制包有效;gc = false不能与source同时给。provider 至少要实现rgc_init、rgc_alloc、rgc_realloc、rgc_free、rgc_collect;描述符参数可以忽略(一直保守扫描也是安全的),进程参数那部分不用管。自定义源码原样参与构建指纹,改一个字节就会重新构建。
解释器
riddle run和riddle repl用的是树遍历解释器,输入是完全 lowering 之后的 MIR——与 C 后端同一份,所以两侧语义按同一套规则实现:整数环绕、除零与MIN / -1的 trap、移位按宽度取模、浮点转整数饱和、下标越界abort、&str按内容比较、panic消息与位置映射、riddle: <message>形式的 abort 文本都一致。
标准库声明的extern由内置 shim 提供,覆盖:
- 字符与格式化输出:
putchar、riddle_fmt_fputc_stderr、riddle_io_stdin; - 文件系统:
riddle_fs_fopen、riddle_fs_fclose、riddle_fs_fread、riddle_fs_fwrite、riddle_fs_fflush、riddle_fs_fgetc、riddle_fs_ferror、riddle_fs_exists、riddle_fs_size、riddle_fs_is_file、riddle_fs_is_dir、riddle_fs_read_dir、riddle_fs_remove、riddle_fs_rename; - 进程与参数:
abort、riddle_process_exit、riddle_argc、riddle_argv_at、riddle_argv_len; - 时间与随机:
riddle_time、riddle_sleep_ms、riddle_random_u32、riddle_random_u64; - 内存门面:
rgc_realloc、rgc_free、riddle_str_slice_ptr、riddle_mem_swap。
把随附 std 的 extern 声明与这张 shim 表对差,有五条没有实现:riddle_alloc_bytes、riddle_proc_call_site_start、riddle_proc_call_site_end、riddle_proc_emit_diagnostic、riddle_proc_set_output。它们都来自std::proc_macro,所以过程宏相关的那条路径在解释器下不可用。其余extern(包括用户自己声明、期望链接到真实 C 符号的那些)会报riddle: interpreter does not support extern加符号名,并以退出码 3(Windows)或 134 结束。
内存行为与 C 侧不同:
- 指针是“分配 id 加偏移”,id 0 是空指针;字符串与函数指针靠 interning 保持身份。
- 每次分配都零初始化,
rgc_realloc的 shim 分配新块并拷贝min(旧长, 新长)。 - 内存会回收:栈帧返回时释放它的
alloca块,HeapFree与rgc_free释放程序持有的堆块,rgc_realloc释放旧块,空出来的 id 由后续分配复用,所以分配表不随运行时长单调增长。字符串字面量和提升后的存储不属于栈帧槽位,一直有效。 - 读到已释放的指针报
dangling pointer to released allocation N,读到越界偏移报memory access of N bytes at offset M escapes allocation K,不会把过期数据当正常结果返回。 - 运行配置是 256 MiB 栈与
max_depth = 200_000,超过报stack overflow。
当前工具链状态
本页列出当前实现里确实存在的能力,以及明确的缺口,作为读其他章节时的对照基准。
编译流程
riddlec 是编译器入口。多个输入文件按换行拼成一个包,各自保留 source map,彼此可以直接引用顶层条目;目录输入不支持。默认把随编译器附带的 std 拼在用户源码之后,--no-std 关掉这个行为。
管线按固定顺序执行:
- parse:词法与语法分析,
IncrementalParser支持按片段局部重解析; - lower HIR:语法树降到 HIR,条目树、属性与文档注释附着在这一步成形;
- scope graph:增量作用域图与名字解析,找不到候选时给出 E0050;
- type check:
IncrementalTypeChecker,复用未改动函数体与全局检查的结果; - escape analysis:过程间不动点,标出需要稳定存储的局部值、参数、临时量和匿名函数环境;
- move/borrow:移动后使用(E0100)、借用冲突(E0300–E0302)、借用期间的赋值与移动(E0303、E0304);
- MIR lowering:降成 SSA 基本块加 Phi 节点,只在需要后端产物时执行;
- C 后端:只在
--backend c时执行,每个包写出一个.c文件。
riddlec file.rid 不带 --backend 或 --emit 时在第 6 步之后停止。-v 把每一步的 ok / failed / skipped 打到 stdout,设置 RIDDLEC_PHASE_TIMING 后各阶段耗时打到 stderr。--emit mir 把整个程序的 MIR 打印到 stdout,不需要 C 工具链;--emit c 与 --backend c 走同一条生成路径,写到 -o 指定的位置,没给 -o 时用第一个输入文件的文件名加 .c 后缀。--target 接受 7 个 triple:x86_64-unknown-linux-gnu、aarch64-unknown-linux-gnu、i686-unknown-linux-gnu、x86_64-pc-windows-msvc、i686-pc-windows-msvc、aarch64-pc-windows-msvc、aarch64-apple-darwin。选中的 triple 同时给出 usize/isize 的宽度,整数范围检查用的就是它,运行编译器的那台机器的宽度不参与。
诊断只有一种形式:{severity}[{code}]: {message},随后一行 --> file:line:col 和源码摘录,最后可有 = help: / = note:,末尾汇总 aborting due to N previous error(s)。没有 JSON 输出或 --message-format 选项。退出码:成功 0,编译失败或没有输入文件 1,clap 参数错误 2。
MIR 与解释器
MIR 位于类型检查与代码生成之间,形式是 SSA 基本块:
- 终结符
Branch、CondBranch、Return、Unreachable,Phi合并多个前驱块的值; - 分配指令
Alloca(栈)与HeapAlloc(GC 堆),由逃逸分析结果选择; - 内存指令
Load、Store、FieldPtr、IndexPtr、CheckedIndexPtr、ExtractValue、HeapFree; - 运算指令
Const、BinOp、UnOp、Cmp、Cast、SizeOf; - 值构造
StructValue、SparseStructValue、ArrayValue、TupleValue,枚举用稀疏初始化让各变体的 payload 槽位稳定; - 转换操作码有
IntToInt、IntToChar、IntToFloat、FloatToInt、FloatToFloat、BoolToInt、IntToBool、IntToPtr、PtrToPtr,Cmp支持Eq、Neq、Lt、Gt、LtEq、GtEq; - 可调用值统一为
{ call, env, drop },FunctionRef取函数地址,CallIndirect传入环境后调用; - 类型有
FnPtr、Ptr、Struct、Enum、Tuple、Array、Slice、Str、Never、Void,裸Str与Slice没有独立大小。
crates/interpreter 是树遍历解释器,输入是完全降级后的 mir::Module(已单态化,匿名函数与 trait object 已经变成函数指针结构),服务 riddle run 和 riddle repl,不需要 C 工具链。两种执行方式的结果一致:
| 行为 | 结果 |
|---|---|
| 整数算术 | 按位宽回绕 |
除零、MIN / -1 | 终止,输出 riddle: division by zero 一类消息,Windows 退出码 3、其它平台 134 |
| 移位 | 计数按位宽取模,有符号右移是算术右移 |
| 浮点转整数 | NaN 转 0,越界钳到 min / max,其余截断 |
| 数组与切片下标 | 先比较下标与长度,越界输出 riddle: index out of bounds |
&str 相等 | 按内容比较,不是按地址 |
| panic | thread 'main' panicked at file:line:col: 加消息,位置经 source map 映射回 .rid 源码 |
解释器覆盖 std 声明的大部分 extern:std::fs 的文件与目录操作、time、sleep、随机数、std::process::exit、标准输入、rgc_realloc / rgc_free 等。缺口有两处:过程宏用到的 riddle_proc_* 没有 shim,用户自己声明的 extern "C" 也没有实现,命中时输出 riddle: interpreter does not support extern。
解释器的指针是「分配 id + 偏移」,所有分配零初始化(C 侧只清零描述符列出的指针槽)。内存会回收:alloca 取用的块随栈帧返回一起释放,HeapFree、rgc_free 和 rgc_realloc 释放程序持有的堆块,释放出去的 id 会被后续分配重新使用,所以分配表不再随运行时长单调增长。字符串字面量和提升后的存储不是栈帧槽位,它们和 C 后端里的 static 数组一样长期有效。读到已释放的指针直接 trapping,报 dangling pointer to released allocation N,而不是把过期字节当作有效数据返回。递归深度上限 200000,栈 256 MiB,超限输出 riddle: stack overflow ...。riddle run 的退出码:读不到源文件 2,编译失败 1,程序正常返回用 main 的返回值,栈溢出与内部错误 101。
语言特性
词法与语法
- 关键字 34 个:
let、fun、struct、if、else、while、loop、break、continue、return、as、self、mod、use、mut、move、pub、super、crate、enum、trait、impl、dyn、match、const、type、extern、unsafe、safe、for、in、where、true、false。Self不是关键字,而是impl作用域里的类型别名:trait 定义、trait 实现和固有 impl 都能用它写返回类型、参数和类型实参(fun zero() -> Self、Vector<Self>),只有Self { … }这种构造写法不认,报E0050;safe只在unsafe extern块内合法;static、ref、async、await、macro、box、union等不是关键字,会被当作普通标识符。 - 注释四种:
//、///与//!、/* */、/** */与/*! */。块注释可嵌套。/**/会先被当成块文档注释开头,随后找不到闭合的*/,因此报未闭合块注释;//<只在同一行紧跟节点之后才作为尾随文档注释附着。 - 整数支持十进制和
0x、0o、0b前缀、_分隔符与 12 种宽度后缀。浮点是1.5、1e5、1.5f32三种形态,1.不是浮点字面量。字符串转义只有\n、\r、\t、\0、\\、\"、\',没有\xHH和\u{...};字符串可以跨行;裸串写成r"..."或r#"..."#。 - 属性
#[...]内部是平衡 token 序列,可以放在语句、参数、字段、枚举变体、trait 与 impl 条目、match 臂、表达式、类型和模式之前。只有#[lang = "..."]、#[fundamental]和#[derive(...)]有语义,#[inline]、#[cfg]一类既不报错也不生效;内部属性#![...]不支持。
类型系统
- 标量:
i8、i16、i32、i64、isize、u8、u16、u32、u64、usize、f32、f64、bool、char、()、!。i128、u128、f16、f128只在词法层存在,类型检查报 E0011。 - 不定长类型只有
str、[T]和dyn Trait,只能出现在引用或裸指针之后;把它们放进绑定、参数、返回值或字段报 E0043。 - 引用
&T、&mut T,裸指针*const T、*mut T,元组、[T; N]、const 泛型参数,dyn Trait、impl Trait、impl Fn(..) -> T。函数类型语法fun(T) -> U已移除,诊断会指向impl Fn。 Copy由std::marker::Copy标记:标量、&T、裸指针与函数项天然满足;&mut T不是Copy;元组和数组按元素结构派生;struct 与 enum 必须显式impl Copy或#[derive(Copy)],编译器校验全部字段与变体 payload。- 标为
mut的结构体字段提供内部可变性,&self方法可以直接写它。枚举变体字段不能标mut(报 E0014),普通字段经共享引用写入报 E0309。mut字段上的&mut只限当次调用内使用:可以当接收者或实参(add_to(&mut counter.hits)),不能绑定到名字、不能返回、不能存进别的聚合体。
struct Counter {
pub mut hits: i32,
}
impl Counter {
fun bump(&self) {
self.hits += 1;
}
}
fun main() {
let counter = Counter { hits: 0 };
counter.bump();
println!("{}", counter.hits);
}
- 布局检查查找内联包含自己的字段并报 E0072。打断递归环的方式是
&T、裸指针、dyn Trait、[T; 0]或内部用裸指针的Vector<T>;[T; N](N 非 0)和[T]不打断。查找按裸名在整个 item tree 里进行,限定路径写法的递归字段可能漏检。 - 关联类型已实现:trait 内声明(可以带默认值)、impl 提供、
dyn Trait<Assoc = T>绑定,缺必需关联类型报 E0027;trait 里没有关联 const。
条目、模块与可见性
- 条目有
fun、unsafe fun、struct、enum、trait、impl、mod、use、const、type、extern,路径式宏调用也可以写在条目位置。它们可以出现在顶层,也可以出现在函数体里;条目后不能带分号。pub允许出现在fun、unsafe fun、unsafe extern、struct、mod、use、enum、trait、const、type之前,不能写在impl前;struct 字段、trait 条目和 impl 条目可以写pub。 mod name { ... }内联模块与mod name;外部模块都在。use支持as别名、::*、::{a, b as c}(可嵌套,允许尾逗号)、前导::和self、super、crate段;pub use重导出。- 可见性检查:私有字段与私有方法在定义模块之外访问报 E0054。固有方法和字段要求同 package 且定义模块包含使用模块,trait impl 的方法不按
pub过滤。同一作用域的顶层重名报 E0064,空use报 E0051,glob 目标找不到报 E0052。 const NAME: Ty = value;可以写在模块和 impl 里。初始化式必须是常量表达式:字面量、对其它const的路径引用、没有语句的块、聚合字面量、一元与二元运算、字段访问、as、?和下标可以;函数调用、匿名函数和控制流报 E0060,初始化成环同样报 E0060。常量可以当数组长度和 const 泛型实参。求值器按有符号i128建模,const N: i32 = -1;、'x'、true都能折叠,结果按申明类型截断后再查范围,超出范围报 E0011。数组长度和 const 泛型实参仍然要求非负整数。type别名在模块和 impl 里必须写成type Name = Ty;,trait 里可以只声明;别名不支持泛型参数,别名环报 E0391。
表达式与控制流
- 字面量、运算、调用、
if、match、块、loop、unsafe { }都是表达式并且有值,块的尾表达式就是块的值。块状表达式在语句位置可以不带分号,其它表达式必须写。 - 运算符全集:算术
+ - * / %,比较== != < > <= >=,逻辑&& || !,位运算& | ^ << >>,前缀&(借用)、&mut、*(解引用)与as、?,赋值=与+= -= *= /= %= &= |= ^= <<= >>=。除赋值右结合外,所有二元运算符左结合,区间也是左结合:a..b..c解析成(a..b)..c。 - 控制流有
if/else、if let、while、while let、loop、for ... in ...、break(带值只允许在loop内,否则 E0065)、continue、return。while、for与无else的if的值是()。 let支持延迟初始化和let ... else;let与for头部的模式必须不可反驳,否则 E0057。if let之后的绑定在成功分支内可见,let ... else成功后的绑定进入外层作用域。match支持 guard、or-pattern 和递归穷尽性检查,非穷尽报 E0039,整数 scrutinee 还会给出未覆盖的区间。?接受Result<T, E>和Option<T>:错误分支经Into转换后返回(没有 impl 时回退From),Option操作数把None提前返回。- 匿名函数用方括号:
[x -> x * 2]、[ -> 1]、move [x -> x],参数可以带类型和模式。后缀位置values.map [v -> v * 2]把方括号形式当实参调用values.map。方括号语法里没有写泛型参数和返回类型的位置,自递归要用具名函数。fun(x) { ... }与|x| x两种写法都不存在。 - 宏只有路径式调用
path!(...)、path![...]、path!{...},参数内容是平衡 token 序列。内置函数式宏 15 个:assert、assert_eq、assert_ne、debug_assert、debug_assert_eq、debug_assert_ne、format、panic、print、println、quote、todo、unimplemented、unreachable、vec。内置 derive 9 个:Debug、Clone、Copy、Default、Hash、PartialEq、Eq、PartialOrd、Ord。格式串只接受字符串字面量,占位符是{}、{0}、{name},各自可选:?,没有宽度、精度和对齐。 - 模式种类:
_、绑定(含mut)、字面量、元组、&与&mut、结构体、枚举的 unit / tuple / struct 变体、路径、or-pattern(只在 match 臂顶层)。没有-1这类负数字面量、ref/ref mut、@绑定、切片模式[a, b]、区间模式1..=5、嵌套 or-patternSome(1 | 2)。引用 match ergonomics 会自动解引用&T/&mut T,默认绑定模式变成引用后再写mut或&mut报 E0010。
Trait 与泛型
- trait 条目只能是
fun、unsafe fun和type;方法可以有默认体,impl 未覆写时使用默认体。 - 支持 supertrait、传递 bound、父方法与环检查(E0044),以及「impl 某 trait 必须 impl 其 supertrait」(E0036)。
- impl 契约检查:缺必需方法 E0026,缺必需关联类型 E0027,
unsafe一致性、参数个数、泛型个数不符报 E0028,参数类型不符 E0029,返回类型不符 E0030。 - 一致性检查:重叠 impl 报 E0047,孤儿规则报 E0048,违反 Paterson 条件报 E0037。
#[lang = "..."]把 trait 标为编译器内置项,驱动运算符与下标分派:copy、drop、clone、partial_eq、eq、partial_ord、ord、debug,算术与位运算的add、sub、mul、div、rem、neg、not、bitand、bitor、bitxor、shl、shr,对应的复合赋值add_assign到shr_assign,以及index、index_mut。option和result是枚举 lang 标记。加载 std 时用户包写内部属性报 E0049;--no-std下可以为自定义 core 定义 lang item。加载 std 时用户包写内部属性报 E0049;--no-std下可以为自定义 core 定义 lang item。- 泛型支持类型参数与 const 参数(
<T, const N: usize>)、<T: A + B>、where子句。默认类型实参只允许出现在 struct、enum、trait 声明里,函数与 impl 不允许。泛型经单态化实现,泛型递归调用让类型实参不断增长时报 E0033。 dyn Trait支持关联类型绑定与父 trait 向上转型;dyn A + B和impl A + B都不支持,多 bound 只能出现在 bound 位置。
所有权、借用与逃逸
- 值默认移动,
Copy类型按复制传入,移动后使用报 E0100。 - 借用冲突有三档:要可变借用而存在共享借用 E0300,要共享借用而存在可变借用 E0301,同一位置同时两个可变借用 E0302。借用存活期间的赋值与移动分别报 E0303、E0304。
- 借用到绑定的最后一次使用为止(NLL 式近似,不是按 CFG 的活跃变量分析)。
- 通过共享引用写入报 E0309,例外是
mut字段。检查按被调函数的内部写摘要工作;遇到dyn分派、函数指针调用、泛型 bound 分派和没有函数体的extern声明时,退化为「该实参的全部mut字段」这一保守结果。 Drop类型:不能移出字段(E0305),借用不能比拥有者活得久(E0306),matchguard 里不能移动被守护的位置(E0307),不能从非Copy值的解引用移出(E0308),Drop + Copy报 E0055,直接调用Drop::drop报 E0056。- 逃逸分析决定局部值放栈还是 GC 堆。默认开启 GC,会逃出栈帧的引用被提升到堆,移动与借用检查不受影响。
Clue.toml里写[runtime] gc = false后语义不同:同样的引用逃逸变成编译错误 E0310,程序必须自己保证不返回、也不保存指向栈值的引用。
fun make_value() -> &i32 {
let value = 7;
&value
}
fun main() {
println!("{}", *make_value());
}
标准库
- 随编译器附带的 std 自动拼在用户源码之后,prelude 直接提供
Option、Result、String、Vector、Some、None、Ok、Err、Copy、Clone、Drop、drop、Default、Into和迭代协议,其余从各自模块导入。 - 模块覆盖
option、result、string、vector、iter、slice、array、ops、cmp、marker、clone、default、convert、hash、collections(Vector、HashMap、HashSet、TreeMap、TreeSet)、fs、io、env、process、mem、time、random、parse、char、fmt、ffi。 - 没有
Box、Rc、Arc、Weak、Cell、RefCell,也没有sync、rc、thread模块。 - 各模块的公开 API 见常用标准库。
C 后端与运行时
- 后端接口只返回一个字符串,产物是一个包的单个
.c文件,没有.h;文件头注释给出完整的编译命令。 - 内部符号一律转义成
riddle_<kind>_<十六进制>,只有extern "C"定义和#[c_export]的函数使用源码原名,且名称必须是合法 C 标识符。 - 类型映射的要点:
&T和&mut T都是T*且不加const(mut字段要能经共享引用写入);str与&[T]是{ ptr, len }胖表示;枚举降成 tagged struct;匿名函数值是riddle_closure_<hash>,字段为call、env、drop;dyn Trait是数据指针加方法槽,拥有所有权的形式多一个drop槽。 - 整数加减乘与位运算先提升到无符号载体再还原,避免有符号溢出 UB;除法与取模内联除零和
MIN / -1检查;越界检查是内联的if加abort();panic 输出thread 'main' panicked at ...后终止。 - GC 描述符表按写入函数的
heap_alloc生成,每个类型一张偏移数组,NULL表示退回逐字保守扫描,槽数上限RGC_MAX_DESCRIPTOR_SLOTS是 512(第 513 个槽才退化,不是「达到 512 就退化」)。 - 运行时有三份 C 源码:默认的 mark-sweep
runtime.c、no_gc_runtime.c和总是单独链接的args_runtime.c。只有需要 runtime 的程序才会生成int main(int argc, char **argv)包装并调用riddle_args_init与rgc_init;不需要 runtime 的fun main() { }直接是int main(void) { return 0; }。 Clue.toml的[runtime] source可以用自定义 provider 替换内存运行时,provider 至少要提供rgc_init、rgc_alloc(size, descriptor)、rgc_realloc、rgc_free、rgc_collect;gc = false与source互斥。gc = false时编译器把heap_alloc降为riddle_alloc并停止生成描述符表。- FFI 支持的形态:标量、
*const T/*mut T(声明处统一成void*)、&T、按值的 struct 与元组、&[T](拆成指针加长度)、导入参数与返回位置上的&str(调用点复制并补 NUL,指针只在这次调用内有效)。不支持的形态:裸[T]、裸str参数与返回、带泛型参数的extern声明,导出名非法时报is not a valid C identifier。 - 生成代码的 ABI 属于技术预览,跨版本不保证兼容。
编辑器与 LSP
riddle-lsp 只有 stdio 传输,参数只有 --no-std、--completion-delay-ms(默认 40)和 --trace-latency。它用与 riddlec 相同的检查管线,停在 move/borrow 之后,不降级 MIR。
已实现的能力:增量文本同步、pull 与 workspace 诊断、补全(含自动导入)、悬停、签名帮助、声明 / 定义 / 类型定义 / 实现跳转、查找引用、重命名(带 prepareRename)、调用层级、类型层级、文档与工作区符号、文档高亮、语义 Token(full、range、delta)、Inlay Hint、折叠、选区范围、文档链接、整文档与范围格式化、代码动作。代码动作里有 7 种 quickfix(补 mut、包 unsafe 块、补缺失字段、补 match 分支、删空 use、替换成 drop(...) 等)、解析类修复(就近名字建议、导入、生成 trait 方法 stub)和 source.organizeImports、source.addMissingImports、source.fixAll 三个聚合动作。
诊断的 source 是 riddle 或 clue,有码的诊断附带指向错误码手册的链接;服务器自身的码只有 LSP0001(缓冲区与编辑器失同步)。Clue.toml 的诊断码是 CLUE0001–CLUE0004。
已知限制:
- 位置编码要协商出
utf-16、utf-8、utf-32之一,否则initialize返回invalid_params; - 类型层级只在客户端支持动态注册时可用,静态 capabilities 里没有它;
- 索引在保存与文件事件上重建;
- Clue.toml 的 schema 是手写白名单,
[build] cache会被报成未知键build.cache; --no-std会让语义高亮、悬停和补全失去 std 信息;- 单文档路径用独立检查会话,语义高亮、Inlay Hint 和代码动作不保证跨包。
仓库里提供 VS Code、IntelliJ IDEA 2026.1+、Zed 和 Helix 四个适配。把 Clue.toml 路由到服务器的是 VS Code(独立 clue 语言 id)和 Helix(按文件名 glob),Zed 与 IntelliJ 只处理 .rid。
工具与项目
| 工具 | 当前能力 |
|---|---|
riddlec | 前端检查、--emit mir、--backend c、--no-std、--target |
riddle fmt | 就地改写、--emit stdout、--check、--tab-size、--hard-tabs,无文件时读 stdin |
riddle run | 单文件解释执行,--seed 与 -- 之后的程序参数 |
riddle repl | 同一解释器,指令 :help、:reset、:mir、:quit |
riddle-lsp | stdio 语言服务器,见上一节 |
clue | 包管理与构建 |
clue 的子命令是 init、new、check、build、run、add、remove、fetch、update、tree、metadata、package、publish、install、uninstall、clean、doc、test、bench,没有 fmt、lsp、repl、target。Clue.toml 支持的 section 是 [package]、[dependencies]、[dev-dependencies]、[features]、[[bin]]、[lib]、[[test]]、[[example]]、[[bench]]、[workspace]、[runtime]、[build];没有 [profile]、[patch]、[replace]、[lints]、[target.*]、[workspace.dependencies]、[workspace.package]。依赖有三种来源:版本字符串(registry)、path、git(branch、tag、rev 只能选一个)。锁文件是 Clue.lock v3。registry 配置不在 Clue.toml,而在 $CLUE_HOME/config.toml 或项目下的 .clue/config.toml。
宿主 debug 构建写 .clue/build,release 或跨目标写 .clue/build/<triple>/<profile>。库产物是 .o、.rlib 和 .rmeta(另有可选的静态库与动态库),二进制产物是本机可执行文件;同名库可以命中 $CLUE_HOME/cache/build 下的全局缓存,[build] cache = false 或 CLUE_BUILD_CACHE=0 关闭它。
需要 C 工具链的命令是 clue build、clue run、clue test、clue bench、clue install:它们生成 C 之后调用 C11 编译器并链接,找不到时报 no usable C11 compiler and linker found...,可以用 CC 指定;构建库还需要 ar 或 llvm-lib / lib。不需要 C 工具链的是 clue check、clue doc、riddle run、riddle repl、riddle fmt、riddle-lsp 和 riddlec --emit mir。clue doc 生成 .clue/doc/*.html,它的 -p/--package 被接受但不生效;clue new 没有 --proc-macro,过程宏包要手写清单。
过程宏包用 [lib] proc-macro = true 标记,导出属性是 #[proc_macro]、#[proc_macro_attribute] 和 #[proc_macro_derive(Name)]。构建产物是动态库加一个 runner 可执行文件,协议版本 1,单次展开超时 10 秒,消息上限 16 MiB。宏里的 print 走 stderr(stdout 是协议通道),宏里的 panic 被隔离在子进程里。过程宏宿主里 clue 会注入 std/std/proc_macro.rid 与 std/std/syn.rid,所以不需要声明 syn 依赖;quote! 是编译器内置函数宏,std 里没有 quote.rid。用法见内置 syn 与 quote!和编写过程宏。
当前限制
- 单线程语言:没有线程、互斥锁、原子变量、
async/await和网络,标准库里也没有对应模块。 - 没有生命周期语法,
'a是词法错误。返回值的借用来源靠过程间摘要推断,泛型 bound 分派的部分依赖 trait 方法上的#[flow = "behind_reference"]契约。 - 没有
Box、Rc、Arc、Weak、Cell、RefCell。共享可变状态目前只能用mut字段表达。 - 范围表达式只有
a..b和a..=b,两端都必须有操作数;a..、..b、..是语法错误。match里也不能写范围模式1..=5。 - 没有循环标签,
'outer: loop { ... }无法解析;跳出多层循环要用函数或标志位。 - 没有
macro_rules!。宏只有路径式调用,加上内置宏和[lib] proc-macro = true包导出的过程宏。 - trait 里不能声明关联 const,
const只能出现在模块和 impl 中。 i128、u128、f16、f128只在词法层可写,语义上不存在这些宽度。- 没有
union、元组结构体、unit 结构体、显式枚举判别值、结构体的函数式更新语法..base和内部属性#![...]。 - 过程宏运行时接口(
riddle_proc_*)和用户自定义extern "C"在解释器下不可用。 riddle run只接受一个源文件;多文件程序要用riddlec或clue。- 数字解析没有十六进制浮点。
for头部的模式必须不可反驳,否则报 E0057。mut字段的内部写检查在dyn分派、函数指针调用、泛型 bound 分派和无函数体的extern声明处退化为保守结果。- 属性没有条件编译:
#[cfg]、#[inline]之类不生效也不报错。 - 递归类型检查按裸名查找,跨包同名类型会互相干扰,限定路径写法可能漏检。
- 生成代码的 ABI 不保证稳定,跨版本可能不兼容地变化。
形式化语法
下面按解析器的分派顺序列出当前接受的形状,顺序与 crates/frontend/src/parser.rs 一致。记法:"..." 是字面 token,? 可选,* 零或多次,+ 一次或多次,| 选择,(...) 分组,裸名字是规则名。本页只描述形状,不描述名字解析和类型规则。
词法
关键字 34 个:
let fun struct if else while loop break continue return as self mod use mut move pub
super crate enum trait impl dyn match const type extern unsafe safe for in where true false
大写 Self 不是关键字。它在每个 impl 作用域里指向正在实现的那个类型,返回类型、参数、类型实参位置都有效(fun zero() -> Self、fun combine(self, other: Self)、Vector<Self>),trait 定义与 trait 实现里也是同一个写法。构造仍然要写具体结构体名:Self { x: 1 } 报 E0050。
标识符是 [a-zA-Z][a-zA-Z0-9_]* 或 _[a-zA-Z0-9_]+。单独的 _ 是另一个 token,不能当表达式:_ = a; 报 expected statement or expression, found Underscore。关键字不能当标识符、字段名或参数名:fun f(type: i32) 报 expected Ident, found TypeKw。
注释四种,全是 trivia,可以出现在任何两个 token 之间:
| 写法 | 形式 |
|---|---|
//... | 行注释 |
///...、//!... | 行文档注释 |
/*...*/ | 块注释 |
/**...*/、/*!...*/ | 块文档注释 |
块注释可以嵌套。/**/ 不是空块注释:/** 先被当成块文档注释的开头,剩余输入里再找不到 */,于是整段变成未闭合注释并报 unterminated block comment;空块注释要写 /* */。
字面量:
literal = int_lit | float_lit | string_lit | raw_string | char_lit | "true" | "false"
int_lit = dec_lit | hex_lit | oct_lit | bin_lit
dec_lit = [0-9] [0-9_]*
hex_lit = "0x" "_"* [0-9a-fA-F] [0-9a-fA-F_]*
oct_lit = "0o" "_"* [0-7] [0-7_]*
bin_lit = "0b" "_"* [01] [01_]*
int_suffix = "i8" | "i16" | "i32" | "i64" | "i128" | "isize"
| "u8" | "u16" | "u32" | "u64" | "u128" | "usize"
float_lit = [0-9]+ "." [0-9]+ ([eE] [+-]? [0-9]+)? float_suffix?
| [0-9]+ [eE] [+-]? [0-9]+ float_suffix?
| [0-9]+ float_suffix
float_suffix = "f16" | "f32" | "f64" | "f128"
string_lit = '"' ( [^"\\] | "\\" any )* '"'
raw_string = "r" "#"* '"' ... '"' "#"*
char_lit = "'" ( [^'\\] | "\\" any ) "'"
_可以出现在数字之间或末尾:1_000u64、0x_FF。i128、u128、f16、f128词法接受,但类型系统里没有这些类型,用到就报E0011。1.不是浮点字面量:let x = 1.;被读成字段访问,报expected Ident, found Semi。- 普通字符串可以跨行。转义只处理
\n\r\t\0\\\"\',其它\X原样留下X,所以"\x41"的值是x41;没有\xHH,也没有\u{...}。 b"..."字节串和r#ident裸标识符都不存在:b"x"被读成标识符b加一个字符串。
标点和运算符 token 全集:
-> == != <= >= && || => += -= *= /= %= &= |= ^= <<= >>= << >> + - * / % & | ^ < > ! ? #
( ) { } [ ] . .. ..= : :: ; , =
没有 @、~、$、...,也没有生命周期:'a 报 unrecognized character。
顶层与条目
文件内容是 statement*。每个 statement 先吃掉属性,再按第一个 token 分派:
"pub" -> pub_item
"let" -> var_decl
"fun" "(" -> 表达式语句(已移除的匿名函数)
"fun" -> func_decl
"unsafe" "fun" -> func_decl
"unsafe" "extern" -> extern_decl
"struct" -> struct_decl
"mod" -> mod_decl
"use" -> use_decl
"enum" -> enum_decl
"trait" -> trait_decl
"impl" -> impl_decl
"const" -> const_decl
"type" -> type_alias_decl
"break" -> break_stmt
"continue" -> continue_stmt
"return" -> return_stmt
"extern" -> extern_decl
其它 -> 表达式语句
pub_item 是下列条目加 pub 前缀:
pub_item = "pub" ( func_decl
| "unsafe" ( func_decl | extern_decl )
| struct_decl | mod_decl | use_decl | enum_decl | trait_decl
| const_decl | type_alias_decl | extern_decl ) ;
pub impl S {} 报 expected item after 'pub', found Impl。
块体与文件顶层用同一条 statement 规则:
program = statement* ;
block = "{" statement* expression? "}" ;
条目后面不能再写分号:fun f() {}; 报 expected expression, found Semi。
属性
attribute = "#" "[" balanced_token* "]" ;
# 必须紧跟 [,内部只做括号配平,不按语法解析。能写属性的位置:语句前、参数前、struct 字段、enum 变体、trait 条目、impl 条目、extern、match 臂、表达式、类型、模式、字段模式。
struct S {
#[field]
x: i32,
}
#[derive(Debug, Clone)]
struct T {
y: i32,
}
#[item]
fun make(#[param] v: i32) -> #[ret] S {
S { x: v }
}
fun main() {
let #[pat] s = make(1);
println!("{} {:?}", s.x, T { y: 2 });
}
只有 #[lang = "..."]、#[fundamental] 和 #[derive(...)] 会被处理,其余属性不报错也不生效,只作为元数据保留。同一个类型上写了 #[derive(...)] 时,字段上的未知属性会被当成 derive 辅助属性,报 E0400。内部属性 #![...] 不支持:写在文件开头报 expected expression, found Hash。
模块、use 与路径
mod_decl = "pub"? "mod" ident ( ";" | "{" statement* "}" ) ;
use_decl = "pub"? "use" use_tree ";" ;
use_tree = "{" use_tree ( "," use_tree )* ","? "}"
| path ( "as" ident
| "::" "*"
| "::" "{" use_tree ( "," use_tree )* ","? "}" )? ;
path = "::"? path_segment ( "::" path_segment )* ;
path_segment = ( ident | "self" | "super" | "crate" ) ( "::" type_arg_list )? ;
use 树支持 as 别名、::*、可嵌套的 ::{...}、尾逗号、前导 ::,以及 self/super/crate 段:use std::io::{self, print};、use super::x;、use ::std::io::print; 都合法。
路径段里的类型实参要写 ::<...>。表达式位置更严:Vec::<i32>::new()、g::<i32>(1) 合法,写成 g<i32>(1) 报 generic arguments in expression paths must use ::<...>。
let 绑定
var_decl = "let" pattern ( ":" ty )? ( "=" expression ( "else" block )? )? ";" ;
初始化式可以省略。let x: i32; 是延迟初始化,赋值前使用报 E0059;类型和初始化式都省略时(let x;)编译器要从首次赋值推断类型,推断不出来报 E0045。
else 是 let-else,必须写在初始化式后面(let x else { }; 报 let-else requires an initializer),后面必须是块,块必须发散,否则报 E0066。
let 只接受单个模式:写成 let 1 | 2 = x; 会得到「or-patterns are only allowed on match arms」的诊断。
函数与参数
func_decl = "pub"? "unsafe"? "fun" ident generic_params? param_list ( "->" ty )? where_clause? ( block | ";" ) ;
param_list = "(" ( param ( "," param )* ","? )? ")" ;
param = attribute* ( "&" "mut"? "self" | "self" | "mut"? ident ":" ty ) ;
- 参数列表允许尾逗号(
fun f(a: i32,) {});泛型参数表不允许(fun f<T,>() {}报expected Ident, found Greater)。 - receiver 有三种:
&self、&mut self、按值的self。普通参数必须写类型,缺了报expected parameter type。 - 参数上的
mut是绑定可变性,不是类型的一部分:fun f(mut x: i32) { x = 1; }合法。 - 函数体可以写成
;,但只有extern块里的声明允许这样:模块层的fun f();报E0015(降级不会为它生成函数,调用只会在运行期失败)。 - 块内最后一个不带
;的表达式就是块的值。
结构体
struct_decl = "pub"? "struct" ident generic_params? where_clause? struct_field_list ;
struct_field_list = "{" ( struct_field ( "," struct_field )* ","? )? "}" ;
struct_field = attribute* "pub"? "mut"? ident ":" ty ;
字段列表不能省:struct S(i32); 报 expected LBrace, found LParen,struct S; 报 expected LBrace, found Semi。没有元组结构体和 unit 结构体。
字段修饰符顺序固定为 pub 在前、mut 在后:pub mut x: i32 合法,mut pub x: i32 报 expected Ident, found Pub。mut 字段表示内部可变性。
跳转与块
return_stmt = "return" expression? ";" ;
break_stmt = "break" expression? ";" ;
continue_stmt = "continue" ";" ;
expr_stmt = expression ";" | block_expression ";"? ;
break 带值只对 loop 有意义:while 或 for 里写 break 1; 报 E0065,循环外写报 E0042。
块状表达式({}、if、while、loop、for、match、unsafe)在语句位置可以省略分号,其它表达式必须写分号。块状表达式在语句位置就结束该语句,紧跟的 ( 不会粘成调用。
表达式
前缀、原子与后缀
prefix_op = "+" | "-" | "&" "mut"? | "&&" | "*" | "!" ;
postfix_op = "(" arg_list ")" | "." ( ident | number ) | "[" expression "]" | bracket_lambda | "?" ;
arg_list = "(" ( expression ( "," expression )* ","? )? ")" ;
atom = literal
| path
| "(" ( expression ( "," expression )* ","? )? ")"
| "[" "]" | "[" expression ( "," expression )* ","? "]"
| "[" expression ";" expression "]"
| "move"? "[" ( lambda_param ( "," lambda_param )* ","? )? "->" expression "]"
| path ( "::" type_arg_list )? "{" ( field_init ( "," field_init )* ","? )? "}"
| macro_call
| block | if_expr | while_expr | loop_expr | for_expr | match_expr | unsafe_expr ;
lambda_param = pattern ( ":" ty )? ;
field_init = ident ( ":" expression )? ;
- 结构体字面量只在左侧是路径时成立;没有函数式更新,
S { ..base }报expected Ident, found DotDot。 [组内在嵌套深度 0 处出现->就按方括号 lambda 解析,否则按数组字面量。后缀位置的expr [ ... -> ... ]表示把该 lambda 当唯一实参调用expr:values.map [v -> v * 2]。it只是惯例名字,不是隐式参数。- 匿名函数
fun(x) { ... }已移除,写它会得到指向方括号 lambda 的诊断。
fun apply(f: impl Fn(i32) -> i32, v: i32) -> i32 {
f(v)
}
fun main() {
let twice = [x -> x * 2];
println!("{} {}", apply(twice, 3), apply([x -> x + 1], 3));
}
控制流表达式
if_expr = "if" condition block ( "else" ( if_expr | block ) )? ;
while_expr = "while" condition block ;
loop_expr = "loop" block ;
for_expr = "for" pattern "in" expression block ;
condition = expression_no_struct | "let" pattern "=" expression_no_struct ;
match_expr = "match" expression_no_struct "{" ( match_arm ( "," match_arm )* ","? )? "}" ;
match_arm = attribute* arm_pattern ( "if" expression )? "=>" expression ;
unsafe_expr = "unsafe" block ;
if、while、for、match的头部用expression_no_struct,即禁掉结构体字面量的表达式,所以if Foo { }里的{属于块。else后面只能是块或另一个if;if let ... if guard不是语法。- match 臂体是块时尾逗号可省;
match x { }能解析,穷尽性由类型检查负责(不穷尽报E0039)。 while、for和不带else的if的值类型是()。
优先级与结合性
从松到紧:
| 运算 | 结合性 |
|---|---|
= += -= *= /= %= &= |= ^= <<= >>= | 右 |
.. ..= | 左 |
|| | 左 |
&& | 左 |
== != | 左 |
< > <= >= | 左 |
| | 左 |
^ | 左 |
& | 左 |
<< >> | 左 |
+ - | 左 |
* / % | 左 |
as | 左 |
前缀 + - & &mut && * ! | — |
后缀调用、.、索引、?、结构体字面量 | 左 |
解析器里的绑定力数值:赋值 1、区间 1、|| 2、&& 4、相等 6、比较 8、| 10、^ 12、& 14、移位 16、加减 18、乘除模 20、as 21、前缀 22、后缀与结构体字面量 23。
位运算分成四级,不是一级:<< >> 紧于 &,& 紧于 ^,^ 紧于 |,四者都紧于比较。
赋值右结合,所以 a = b = 1; 读成 a = (b = 1),内层表达式的值是 ():
fun main() {
let mut a = 1;
let mut b = 2;
a = b = 1; // E0001
}
区间也是左结合:a..b..c 读成 (a..b)..c。区间两端都必须有操作数,..5、a..、.. 都是语法错误。
中缀运算符右侧是完整表达式,不限于一元表达式,所以 1 + if c { 2 } else { 3 } 合法。块状表达式同样能当值用:
fun main() {
let a = 1 + 2 * 3;
let b = (1 + 2) * 3;
let c = 1 & 2 ^ 3 | 4;
let d = { 1 };
let e = if a == 7 { 2 } else { 3 };
println!("{} {} {} {} {}", a, b, c, d, e);
}
loop { break 1; }、match、unsafe { 1 } 也都能直接产生值;不带 else 的 if 出现在值位置时报 E0002。
类型
ty = attribute* ty_kind ;
ty_kind = "!"
| "&" "mut"? ty
| "&&" ty
| "*" ( "const" | "mut" ) ty
| "(" ( ty ( "," ty )* ","? )? ")"
| "[" ty ( ";" expression )? "]"
| number
| "impl" bound
| "dyn" bound
| path ( "<" type_list ">" )?
| macro_call ;
()既是 unit 值也是 unit 类型;(T)就是T,(T,)是元组。unit不是类型名。&后面可以跟一层mut;&&mut T不接受(&&只吃一层,后面直接要类型),而&&mut模式合法。- 裸指针必须写
*const T或*mut T:*i32报expected 'const' or 'mut' after '*' in pointer type。 - 数组类型的长度按表达式解析,不限于字面量:
[i32; 4]、[u8; 2 + 2]。裸整数字面量单独作类型(例如Array<i32, 4>里的4)是 const 泛型实参。 impl和dyn后面只能跟一个 bound:impl A + B、dyn A + B都报expected RParen, found Plus。多 bound 只能出现在<T: A + B>、超 trait 列表和where谓词里。- 函数类型
fun(i32) -> i32已移除,写它会得到改用impl Fn(i32) -> i32或显式 bound 的诊断。
宏调用
macro_call = path "!" ( "(" balanced_token* ")" | "[" balanced_token* "]" | "{" balanced_token* "}" ) ;
内容按配平 token 吃掉,不按普通语法解析。内置宏由编译器在解析前按源码文本展开,所以宏参数不受上面这些规则约束;未定义的宏报 E0400。macro_rules! 不存在,macro_rules! m { ... } 报「expected a delimited token tree after !」。宏系统本身见编写过程宏。
枚举
enum_decl = "pub"? "enum" ident generic_params? where_clause? "{" ( enum_variant ( "," enum_variant )* ","? )? "}" ;
enum_variant = attribute* ident ( "(" ( ty ( "," ty )* ","? )? ")" | struct_field_list )? ;
三种变体形态都支持:A、A(i32, i32)、A { x: i32 }。判别值不存在:enum E { A = 1 } 报 expected RBrace, found Eq。变体名上不能写 pub(报 expected Ident, found Pub)。tuple 变体的元素是类型,写 mut 报 expected type, found Mut;struct 变体的字段可以写 pub,写 mut 语法通过,类型检查报 E0014。
trait
trait_decl = "pub"? "trait" ident generic_params? ( ":" bound ( "+" bound )* )? "{" trait_item* "}" ;
trait_item = attribute* "pub"? ( func_decl | "type" ident ( "=" ty )? ";" ) ;
trait 里只有方法和关联类型。关联常量不存在:trait T { const C: i32; } 报 expected trait item, found Const。方法可以带默认体;关联类型两种形态都行(type U; 和 type U = i32;)。超 trait 用 + 分隔,不写尾逗号。
impl
impl_decl = attribute* "impl" generic_params? ty ( "for" ty )? where_clause? "{" impl_item* "}" ;
impl_item = attribute* "pub"? ( func_decl | "type" ident "=" ty ";" | const_decl ) ;
impl 条目是方法、类型别名和关联常量。类型别名必须写 = Ty(type T; 报 expected Eq, found Semi),常量必须写 = 值。pub impl S {} 不合法。可调用特质有专门语法:Fn、FnMut、FnOnce 后面直接跟括号参数列表,例如 impl Fn(i32) -> i32 for S { fun call(&self, x: i32) -> i32 { x } }。
struct S {
pub x: i32,
mut y: i32,
}
impl S {
const SCALE: i32 = 2;
type Unit = i32;
fun scaled(&self, dx: i32) -> i32 {
(self.x + dx) * S::SCALE
}
}
fun main() {
let s = S { x: 1, y: 2 };
println!("{} {}", s.scaled(1), s.y);
}
常量与类型别名
const_decl = "pub"? "const" ident ":" ty "=" expression ";" ;
type_alias_decl = "pub"? "type" ident ( "=" ty )? ";" ;
模块和 impl 里的类型别名必须写 = Ty;trait 里 type X; 与 type X = Ty; 都能写。类型别名不能带泛型参数:type Foo<T> = i32; 报 expected Eq, found Less。
const 的初始化式必须是常量表达式,用结构体字面量初始化报 E0060。
泛型参数、bound 与 where
generic_params = "<" generic_param ( "," generic_param )* ">" ;
generic_param = ident ( ":" bound ( "+" bound )* )? ( "=" ty )?
| "const" ident ":" ty ;
bound = path ( "(" type_list ")" "->" ty
| "<" ( bound_arg ( "," bound_arg )* ","? )? ">" )? ;
bound_arg = ident "=" ty | ty ;
where_clause = "where" where_predicate ( "," where_predicate )* ","? ;
where_predicate = ty ":" bound ( "+" bound )* ;
type_list = ty ( "," ty )* ","? ;
type_arg_list = "<" ( type_list )? ">" ;
- 默认类型实参只允许出现在 struct、enum、trait 的声明里;函数和 impl 不允许,
fun f<T = i32>() {}报expected Greater, found Eq。 - 泛型参数表不写尾逗号;
where谓词可以写尾逗号。 - bound 可以是路径、带实参的路径,或者
Fn(...) -> T形式的可调用特质;关联类型绑定写在实参里,例如dyn Iterator<Item = i32>。 - 类型实参里的
>>会拆成两个>,所以Vec<Vec<i32>>不用加空格。
extern 与 ABI 声明
extern_decl = attribute* "pub"? "unsafe"? "extern" string ( "{" extern_func_sig* "}" | func_decl ) ;
extern_func_sig = attribute* "pub"? ( "safe" | "unsafe" )? "fun" ident param_list ( "->" ty )? where_clause? ";" ;
- ABI 字符串必填:
unsafe extern { fun f(); }报expected String, found LBrace。 - 块形式必须写
unsafe extern:extern "C" { fun f(); }报extern blocks must use unsafe extern。 - 单函数形式必须带函数体:
unsafe extern "C" fun f();报 single-function extern declarations are not supported。 - 块里每个函数可以写
safe或unsafe;safe只在这里合法,写成safe fun f() {}报expected expression, found Safe。 - 签名不能带泛型参数。
模式
arm_pattern = "|"? pattern ( "|" pattern )* ;
pattern = attribute* pattern_kind ;
pattern_kind = "mut" ident
| "&" "mut"? pattern
| "&&" "mut"? pattern
| "_"
| literal
| "(" ( pattern ( "," pattern )* ","? )? ")"
| path ( "!" balanced_token*
| "(" ( pattern ( "," pattern )* ","? )? ")"
| "{" ( field_pattern ( "," field_pattern )* ","? )? "}" )? ;
field_pattern = attribute* ident ( ":" pattern )? ;
mut只能直接跟标识符:mut x是绑定模式,mut (a, b)不是模式。- 裸路径
x落成绑定;E::A、Some(x)、P { x, y }都是路径模式。 A | B只在 match 臂顶层:let 1 | 2 = x;有专门诊断,if let A | B = v是语法错误。- 字面量模式不能带负号:
-1报expected pattern, found Minus。 - 不存在
ref/ref mut、@绑定、切片模式[a, b]、区间模式1..=5、嵌套 or-patternSome(1 | 2)。
struct P {
x: i32,
y: i32,
}
fun classify(n: i32) -> i32 {
match n {
1 | 2 => 10,
n if n > 10 => 20,
_ => 30,
}
}
fun main() {
let p = P { x: 1, y: 2 };
let P { x, y } = p;
println!("{} {} {}", classify(x + y), x, y);
}
当前不存在的形式
| 写法 | 结果 |
|---|---|
macro_rules! | 报「expected a delimited token tree after !」;只有 path!(...) 形式的调用 |
| 生命周期标注 | 'a 报 unrecognized character,源码里没有生命周期语法 |
函数类型 fun(i32) -> i32 | 已移除,诊断指向 impl Fn(i32) -> i32 |
开区间 ..x、x..、.. | 报 expected expression, found DotDot 或 found Semi |
| trait 里的关联 const | 报 expected trait item, found Const;impl 里可以写 |
循环标签 'a: loop {} | ' 是非法字符 |
| 元组结构体、unit 结构体 | struct S(i32); 报 expected LBrace, found LParen,struct S; 报 found Semi |
enum 判别值 A = 1 | 报 expected RBrace, found Eq |
dyn A + B、impl A + B | 报 expected RParen, found Plus |
内部属性 #![...] | 报 expected expression, found Hash |
闭包 |x| x + 1 | 报 expected expression, found Pipe;用 [x -> x + 1] |
匿名函数 fun(x) { ... } | 已移除,诊断指向方括号 lambda |
结构体更新 S { ..base } | 报 expected Ident, found DotDot |
字节串、裸标识符、\xHH、\u{...} | b"..." 与 r#ident 不成立,\u{...} 是非法字符 |
空块注释 /**/ | 报 unterminated block comment,用 /* */ |
Self { … } 构造 | 报 E0050: unresolved name: Self,-> Self 在固有 impl 里已经可用,构造要用具体结构体名 |
错误码参考
编译器的每条错误都带一个四位码,格式是 error[E0001]: ...。这个码是稳定接口:LSP 诊断的 code 字段就是它,code_description 的链接指向本文对应的锚点。语法错误不带码,只打印 error: expected ...。
本文收录当前实现定义的 80 个 E 码:78 个由 crates/ 下的检查器发出,E0400 由 riddlec 的过程宏驱动发出,E0200 目前没有任何发射点。每个条目给出含义、触发条件和一段可单独编译的最小程序,程序里的 // EXXXX 注释标出它会触发的码。除 E0053(要 riddlec --no-std 或写在标准库里)和 E0310(要在 Clue.toml 里关掉 GC)以外,示例都能直接用 riddlec <文件> 复现。
| 主题 | 码 |
|---|---|
| 类型、调用与字面量 | E0001–E0012、E0043 |
| 名字、字段、方法与可见性 | E0013–E0015、E0050–E0052、E0054、E0064 |
| trait、impl 与泛型 | E0020–E0030、E0032–E0037、E0044–E0045、E0047 |
| 内部属性、lang item 与保留名 | E0048–E0049、E0053 |
| 模式、控制流与 unsafe | E0010、E0038–E0039、E0042、E0046、E0057–E0058、E0061–E0063、E0065–E0066 |
| 常量、类型布局与降级 | E0040、E0060、E0067、E0072、E0391 |
| 移动、初始化与析构 | E0031、E0041、E0055–E0056、E0059、E0100、E0305–E0308 |
| 借用、逃逸与 GC | E0200、E0300–E0304、E0309–E0310 |
| 过程宏与内部码 | E0400、E0999 |
类型、调用与字面量
E0001 类型不匹配
let 初始化式、赋值、实参、返回值、字段初始化的类型与期望类型不一致时发出。二元运算两侧类型不同(例如 1 + "a")也走这个码,数组长度与期望长度不符同样是它。
fun main() {
let n: i32 = "one"; // E0001
}
类型之间的隐式转换白名单很窄,只有数值字面量的待定类型、切片降级、trait object 提升等少数几种,见数据类型。 相关:E0002、E0012
E0002 分支类型无法合并
if 与 else、match 各臂、值位置上的无 else if,类型对不上时发出。数组重复表达式 [value; n] 的长度不是能折进 usize 的整数字面量时也是这个码。
fun main() {
let c = true;
let x = if c { 1 } else { "a" }; // E0002
}
[0; n] 里的 n 是变量时报 array repeat length must be an integer literal that fits usize。分支规则见控制流。
相关:E0001、E0003
E0003 运算符的操作数类型不对
算术要求数值,%、<<、>> 要求整数,位运算要求整数或 bool,排序比较要求两侧兼容的数值、char 或实现了 PartialOrd 的类型,一元 ! 要求 bool 或整数。
fun main() {
let x = true + 1; // E0003
}
相等比较走 PartialEq,缺实现时报 E0036。运算符清单见表达式与块。
E0004 调用了不是函数的值
fun main() {
let x = 1;
x(); // E0004
}
E0005 实参个数或形状不符
函数、方法、匿名函数、可调用字段的调用点实参数量不对时发出。泛型调用的类型实参无法从实参和期望返回类型推断出来时,也用这个码,原文是 cannot infer type argument(s) for ...。
fun f(a: i32) {}
fun main() {
f(); // E0005
}
E0006 未知字段
结构体字面量或字段访问用了定义里没有的字段名,枚举结构变体上的字段访问同样处理。
struct P { x: i32 }
fun main() {
let p = P { x: 1 };
let y = p.y; // E0006
}
E0007 结构体字面量缺字段
struct P { x: i32, y: i32 }
fun main() {
let p = P { x: 1 }; // E0007
}
字段名写错是 E0006,不是缺字段。
E0008 解引用不是引用或裸指针的值
fun main() {
let x = 1;
let y = *x; // E0008
}
帮助文本里还留着一句「只有数组能下标」,下标失败实际报 E0036。
E0009 结构体字面量或变体的形状不对
路径解析到的不是结构体、给元组变体写结构体字段、结构体字面量的类型实参个数不对,都报这个码。
enum E { A(i32) }
fun main() {
let x = E::A { v: 1 }; // E0009
}
路径本身找不到是 E0050,变体不属于被匹配的枚举是 E0038。
E0011 字面量后缀或数值范围非法
词法层接受 i128、u128、f16、f128 后缀,类型系统里没有这些类型,写出来会得到「不支持」的提示;无法识别的后缀报未知后缀。字面量和常量值超出目标类型范围时也是这个码。usize/isize 的边界由选中的目标给出(--target、RIDDLE_TARGET 或 [build].target),i686-* 下 4294967296usize 越界,64 位目标下合法;运行编译器机器的宽度不参与。
fun main() {
let x = 1i128; // E0011
}
let x: u8 = 300; 报整数越界。42_i99 不属于这个码:_ 会另起一个 token,得到的是解析错误,而解析错误不带码。
E0012 as 的类型对不支持
支持的组合:整数之间、整数与浮点之间、整数转 bool、bool 与 char 转整数、u8 转 char、裸指针之间,以及 &T 转 *const T/*mut T(要求 T sized,目标可变时源也必须可变)。其余组合报这个码。
fun main() {
let x = true as f64; // E0012
}
(*const T, usize) 与 &[T]、&[u8] 与 &str 之间的布局转换需要 unsafe;*const T 转 *mut T 单独需要 unsafe,缺了报 E0046。指针与 FFI 见 FFI 与 C 后端。
E0043 不定长类型出现在需要定长的位置
str、[T]、dyn Trait 没有独立于借用或指针的布局,不能作为局部绑定、参数、返回值、字段,也不能作为元组、数组、结构体、枚举的类型实参。&T 和 *const T 的内层允许不定长。
struct S { s: str } // E0043
字符串值用 &str,切片与 trait object 用 &[T]、&dyn Trait 或拥有形式。见数据类型。
相关:E0072
名字、字段、方法与可见性
E0013 方法不存在或不能动态派发
接收者上没有这个固有方法或 trait 方法时报 unknown method。dyn Trait 上同名多来源的歧义方法、以及不满足对象安全的方法(按值 self、泛型方法、参数或返回值里出现裸 Self)在被调用时也报这个码。
struct P {}
fun main() {
let p = P {};
p.missing(); // E0013
}
对象安全不是 trait 声明处的硬性检查:声明本身能通过,调用点才报错。
trait T { fun consume(self) -> i32; }
struct S {}
impl T for S { fun consume(self) -> i32 { 1 } }
fun f(x: &dyn T) -> i32 {
x.consume() // E0013
}
E0014 枚举变体字段不能写 mut
结构体字段上的 mut 表示经共享引用也能写;变体字段只能通过模式到达,写入无法穿过对枚举的共享引用,因此直接拒绝。
enum E { A { mut x: i32 } } // E0014
fun main() {}
把字段放进结构体,或改用 &mut 接收者。见结构体。
E0015 函数声明缺少函数体
fun 声明以分号结束时没有函数体:MIR 降级不会为它生成任何函数,对它的调用只会在运行期以 “call to unknown function” 失败,因此在编译期拒绝。extern 块里的声明不受此限制。
fun helper() -> i32; // E0015
fun main() -> i32 { helper() }
补上函数体,或把声明移入 unsafe extern "C" 块。
E0050 名字解析不到
作用域图在所有可见作用域里都找不到这个路径时发出。
fun main() {
let x = missing; // E0050
}
见模块、use 与包。 相关:E0051、E0052、E0064
E0051 空的 use 声明
use 树里没有任何可以暴露的名字,例如只写了 crate、super 这样的锚点。
use crate; // E0051
E0052 glob 导入的目标找不到
use missing::*; // E0052
普通路径导入失败报 E0050。
E0054 字段或方法不可见
结构体字段与固有方法默认私有,可见性要求同一个包,并且定义模块包含使用模块。trait impl 里的方法不做可见性过滤。
mod m {
pub struct P { value: i32 }
}
fun read(p: m::P) -> i32 {
p.value // E0054
}
加 pub,或通过类型自己提供的公开方法访问。见模块、use 与包。
E0064 同一作用域里重名
函数、结构体、枚举、trait、常量、类型别名、模块共享当前作用域的声明名,重复定义时两个位置都会被标注。
fun f() {}
fun f() {} // E0064
不同模块里的同名声明不冲突。相关:E0050
trait、impl 与泛型
E0020 trait 内重复方法,或 callable 签名不符
同一个 trait 里定义了两个同名方法时发出。impl Fn* for T 的 call 方法签名与 impl 头写的调用签名不一致时也用这个码。
trait T {
fun f();
fun f(); // E0020
}
两个 impl 方法签名不符是 E0028、E0029、E0030。
E0021 callable impl 缺 call
impl Fn(..) -> T for X 要求实现体提供 call。Fn 用 &self,FnMut 用 &mut self,FnOnce 用 self。
struct S {}
impl Fn(i32) -> i32 for S {} // E0021
E0022 trait 内重复关联类型
trait T {
type A;
type A; // E0022
}
dyn Trait<X = ..> 里对同一个关联类型写两次绑定也是这个码。相关:E0025、E0034
E0023 引用了未知 trait
impl 的 trait、impl Trait、泛型 bound 里写了找不到的 trait 名时发出。
struct S {}
impl Missing for S {} // E0023
相关:E0044
E0024 impl 内重复方法
struct S {}
impl S {
fun f() {}
fun f() {} // E0024
}
E0025 impl 内重复关联类型
struct S {}
impl S {
type A = i32;
type A = i64; // E0025
}
E0026 impl 缺少必需方法
trait 里没有默认体的方法必须在 impl 里出现。
trait T { fun f(); }
struct S {}
impl T for S {} // E0026
trait 方法有默认体时不报,关联类型看 E0027。
E0027 impl 缺少必需关联类型
trait 里没有默认值的类型别名必须在 impl 里给出。
trait T { type A; }
struct S {}
impl T for S {} // E0027
E0028 impl 方法签名与 trait 声明不符
参数个数、泛型个数、unsafe 不一致都报这个码。
trait T { fun f(x: i32); }
struct S {}
impl T for S {
fun f() {} // E0028
}
E0029 impl 方法参数类型不符
trait T { fun f(x: i32); }
struct S {}
impl T for S {
fun f(x: bool) {} // E0029
}
E0030 impl 方法返回类型不符
trait T { fun f() -> i32; }
struct S {}
impl T for S {
fun f() -> bool { true } // E0030
}
E0032 泛型实参个数不符
结构体、枚举、trait 的类型实参个数与声明不匹配时发出。trait 参数带默认值时,尾部的实参可以省略。
struct B<T> { v: T }
fun f(b: B<i32, i32>) {} // E0032
E0033 泛型递归让类型实参变大
泛型函数递归调用自己时,实际类型实参如果被包进了更大的类型,单态化会没有终点,编译器在调用图上找到这种环就拒绝。
struct Box<T> { v: T }
fun wrap<T>(v: T) {
wrap(Box { v }); // E0033
}
类型实参相同的递归是允许的,见泛型。
E0034 类型标注非法
类型名找不到、dyn Trait 缺关联类型绑定、数组类型写反([3; i32])等都在这里报。未知类型名走这个码,不是 E0050。
fun main() {
let x: Missing = 1; // E0034
}
数组类型写反时另给一条 note:array types use [T; N]; write [i32; 3] instead。
E0035 trait bound 不满足
调用点、方法查找、for 迭代协议等位置要求某个类型实现 trait,而实际类型没有实现时发出。
trait M {}
struct B<T> where T: M { v: T }
struct P {}
fun main() {
let b = B { v: P {} }; // E0035
}
E0036 缺少运算符或索引所需的实现
相等比较缺 PartialEq、索引缺 Index/IndexMut、类型不能被下标类型索引,以及 impl 了某个 trait 却没有 impl 它的 supertrait,都报这个码。
struct P {}
fun main() {
let x = P {} == P {}; // E0036
}
排序比较(<、>)两侧不兼容走 E0003。同点原始指针的 == 是内建按地址比较,不需要实现。
E0037 impl 的 where 子句违反 Paterson 条件
impl 的 where bound 必须严格小于被 impl 的类型,否则 trait 求解可能不终止。
trait F {}
struct V<T> { v: T }
impl<T> F for T where V<T>: F {} // E0037
E0044 supertrait 未知或成环
trait C: Missing {} // E0044
trait First: Second 与 trait Second: First 这样的环同样报这个码。impl 了子 trait 却没有 impl 父 trait 报 E0036。
E0045 类型推断不出来
匿名函数参数没有标注类型,且没有任何地方能约束它时发出。延迟绑定的 let 在首次赋值前也推不出类型,同样报这个码。
fun main() {
let f = [x -> x]; // E0045
}
写成 [x: i32 -> x],或让调用点约束参数类型。见匿名函数与迭代器。
相关:E0067
E0047 impl 冲突或重叠
同一个 trait 的两个 impl header 覆盖到同一组类型时发出,第二条诊断会指向先出现的 impl。callable impl 缺调用签名、dyn Fn 缺签名也用这个码。
trait F {}
struct P {}
impl F for P {}
impl F for P {} // E0047
impl Fn for S {} 报 impl Fn requires a callable signature。见 impl 块。
相关:E0048
内部属性、lang item 与保留名
E0048 违反孤儿规则
当前包只能实现自己定义的 trait,或为自己定义的类型实现外部 trait。判定顺序是先查 impl 重叠(E0047),再查本地性:&T 与 #[fundamental] 类型会传递本地性,本地类型之前不能出现未被覆盖的类型参数。Fn、FnMut、FnOnce 名字由编译器保留,重名声明也报这个码。
impl Clone for () { // E0048
fun clone(&self) -> () { () }
}
Clone 与 () 都来自标准库,所以这是外部 trait 加外部类型。见 Trait。
E0049 用户包使用了保留的内部属性
默认加载标准库时,#[lang = "..."] 与 #[fundamental] 只允许出现在随编译器附加的标准库中。来源检查优先于属性形状检查,所以用户包里的任何写法统一报这个码。
#[lang = "copy"] // E0049
trait MyCopy {}
用 riddlec --no-std 时不附加标准库,参与编译的包可以自行定义 lang item,那时形状错误才由 E0053 接管。
E0053 lang item 或 #[fundamental] 的形状非法
lang 名未知、同一个 lang item 定义两次、一个 trait 标多个 [lang]、目标不是 trait 或枚举、缺字符串值、签名不符、Option 与 Result 的泛型个数不对,以及 #[fundamental] 用在非类型上或带了值,都报这个码。
// 需要 riddlec --no-std,或写在标准库自身里
#[lang = "unknown"]
trait F {} // E0053: unknown lang item 'unknown'
#[lang = "copy"]
trait A {}
#[lang = "copy"]
trait B {} // E0053: lang item 'copy' is defined more than once
在默认标准库模式下,同样的写法先被 E0049 拦下。
模式、控制流与 unsafe
E0010 模式元数或绑定修饰符位置不对
元组模式的元素个数与值不符、&pat 没有初值、在自动引用解构后的模式里再写 mut 或 &mut,都报这个码。
fun main() {
let (x, y) = (1, 2, 3); // E0010
}
模式形状与类型不符是 E0038,同一模式里重复绑定是 E0058。见枚举、模式与 match。
E0038 模式的形状或类型不符
解构元数不对、变体不属于被匹配的枚举、把非枚举值写成路径模式等,都报这个码。
enum L { S }
enum R { S }
fun f(x: L) -> i32 {
match x {
R::S => 1, // E0038
L::S => 0,
}
}
E0039 match 不穷尽
至少有一个取值没有被任何无 guard 的臂覆盖。诊断会给出一个缺失模式,整数模式还会在 note 里列出未覆盖的区间;带 guard 的臂不计入穷尽性。
enum S { A, B }
fun f(s: S) -> i32 {
match s {
S::A => 1, // E0039
}
}
补上缺失的臂,或用 _ 覆盖剩余取值。
E0042 break 或 continue 不在循环里
fun f() {
break; // E0042
}
while、loop、for 的函数体、闭包体都不构成循环边界。见控制流。
相关:E0065
E0046 该操作需要 unsafe
解引用或索引裸指针、调用 unsafe fun 与不安全的外部函数、裸部件与切片的布局转换、*const T 转 *mut T,都必须在 unsafe 块里。
fun f(p: *const i32) -> i32 {
*p // E0046
}
unsafe 只支持块形式,fun f() { unsafe foo(); } 会得到语法错误。裸指针的有效性由程序员保证。见 FFI 与 C 后端。
相关:E0012
E0057 let 或 for 头里的模式可反驳
普通 let 与 for 头没有备选分支,模式必须覆盖该类型的每个取值。let-else 的 else 能发散时不受此限制。
fun main() {
let o: Option<i32> = Option::Some(1);
let Option::Some(v) = o; // E0057
}
换成 let Option::Some(v) = o else { return; };,或改用 match。见枚举、模式与 match。
相关:E0038、E0066
E0058 同一模式里重复绑定
一个模式内的绑定名必须唯一。不同 let 语句或不同臂之间可以正常遮蔽同名变量。
fun main() {
let (a, a) = (1, 2); // E0058
}
E0061 ? 的操作数不是 Option 或 Result
? 只能展开标准库的 Option 与 Result,用在普通值或自定义枚举上报这个码。
fun f() -> i32 {
let x = 1?; // E0061
x
}
E0062 ? 与函数返回类型不匹配
含 ? 的函数必须返回同一种枚举:操作数是 Result 就返回 Result,是 Option 就返回 Option。
fun f() -> i32 {
let x = Option::Some(1)?; // E0062
x
}
E0063 ? 的错误类型转不过去
源错误类型到返回类型的错误类型,编译器先找 Into::into,再找 From::from;两条路都拿不到目标类型时报这个码。
fun g() -> Result<i32, i32> { Result::Err(1) }
fun f() -> Result<i32, bool> {
let x = g()?; // E0063
Result::Ok(x)
}
E0065 带值的 break 出现在 loop 之外
只有 loop 能用带值的 break 交出结果,while 与 for 里只能写 break;。
fun f() {
while true {
break 1; // E0065
}
}
相关:E0042
E0066 let-else 的 else 块不发散
let-else 的 else 块必须离开当前控制流,正常落到绑定之后会报这个码。return、break、continue、panic! 或不会结束的 loop 都可以。
fun main() {
let o: Option<i32> = Option::Some(1);
let Option::Some(v) = o else { 0 }; // E0066
}
写成 else { return; } 就能通过。相关:E0057
常量、类型布局与降级
E0040 源码无法降级到 HIR
HIR 降级阶段遇到它解析不了的东西时发出,最常见的是超出 u64 的整数字面量。
fun main() {
let x = 99999999999999999999; // E0040
}
类型已经确定、只是超出目标类型范围的字面量报 E0011。
E0060 常量初始化式不是常量表达式,或常量成环
常量初始化式允许字面量、对其它常量的引用、非赋值的二元运算、无语句的块、元组/数组/结构体字面量、一元 -/+/!、字段访问、as、? 和下标。函数调用、闭包、if/while/loop/for/match/unsafe、带语句的块、赋值都不允许。
fun g() -> i32 { 0 }
const X: i32 = g(); // E0060
const A: i32 = B; 与 const B: i32 = A; 这样相互引用同样报这个码。常量值超出申明类型范围报 E0011。
E0067 推断出无限类型
统一两个类型时检出自我引用的替换环,继续下去会构造出无限大的类型。典型写法是把一个参数类型尚未确定的匿名函数传给它自己。
fun main() {
let id = [value -> value];
id(id); // E0067
}
这段代码同时也会报 E0045,因为 value 的类型同样推不出来。给绑定加显式标注或改掉自应用即可。
E0072 递归类型大小无限
结构体或枚举的字段内联包含自己时发出,诊断会给该字段挂 recursive field 次标签。&T、*const T、*mut T、impl Trait、dyn Trait 和长度为 0 的 [T; 0] 都能打断环;[T; N](N 不为 0)和 [T] 不能。
struct N { next: N } // E0072
标准库里没有 Arc/Rc,共享递归结构要靠 &T、裸指针、dyn Trait 或 Vector<T>(内部是 *mut T)。类型别名成环是 E0391。
相关:E0043
E0391 类型别名展开成环
type A = A; // E0391
这是别名环,与 E0072 的布局环是两回事。
移动、初始化与析构
E0031 给不可变绑定赋值
左侧绑定没有 mut 却被重新赋值时发出。复合赋值的左值不可变、调用需要 &mut self 的方法但接收者绑定不可变、把需要修改捕获环境的匿名函数存进不可变绑定,也报这个码。
fun main() {
let x = 1;
x = 2; // E0031
}
经共享引用写入是 E0309,两者不是一回事。见变量与可变性。
E0041 Copy impl 不合法
为类型实现 Copy 时,它的所有字段也必须 Copy,否则副本会与原件共享资源。无法枚举字段的类型(例如 &mut T)同样报这个码。
struct T { v: i32 }
struct W { v: T }
impl Copy for W {} // E0041
字段全 Copy 的结构体也不会自动 Copy,必须显式 impl 或 [derive(Copy)]。同时实现 Drop 是 E0055。
E0055 为实现了 Drop 的类型实现 Copy
拥有析构逻辑的类型不能按位复制,否则多个副本会重复释放同一份资源。去掉其中一个 impl。
struct S {}
impl Drop for S {
fun drop(&mut self) {}
}
impl Copy for S {} // E0055
E0056 直接调用 Drop::drop
析构方法只能由编译器调用。要提前结束一个值,用标准库的 drop(value)。
struct S {}
impl Drop for S {
fun drop(&mut self) {}
}
fun f(mut s: S) {
s.drop(); // E0056
}
析构顺序与 drop glue 见移动语义。
E0059 读取未初始化的绑定
let x; 可以延迟初始化,但每条到达使用点的路径都必须先赋值;编译器合并 if、match 与循环的控制流,可能仍未初始化的读取就报这个码。
fun f() -> i32 {
let x: i32;
x // E0059
}
E0100 使用已移动的值
值被移动后再次使用时报这个码。&T、*const T 是 Copy,&mut T 不是;结构体与枚举不会因为字段全 Copy 就自动 Copy。
struct P { x: i32 }
fun main() {
let a = P { x: 1 };
let b = a;
let c = a; // E0100
}
移动语义与部分移动见移动语义。 相关:E0304、E0305、E0308
E0305 从实现了 Drop 的类型里移出字段
实现了 Drop 的值必须整体保持有效,单独移出非 Copy 字段会跳过析构,因此被拒。
struct Guard {}
struct Owner { guard: Guard }
impl Drop for Owner {
fun drop(&mut self) {}
}
fun main() {
let owner = Owner { guard: Guard {} };
let Owner { guard } = owner; // E0305
}
字段是 Copy 时那是复制,不算移出。没有显式 Drop 的聚合类型可以部分移动,编译器用字段级 drop flag 避免重复析构。
相关:E0100、E0307
E0306 借用的存活期超过 Drop 拥有者
实现 Drop 的局部值在作用域结束时被确定性析构,指向它的引用不能作为返回值或捕获进逃逸的匿名函数活得更久。
struct Guard { value: i32 }
impl Drop for Guard {
fun drop(&mut self) {}
}
fun leak() -> &i32 {
let guard = Guard { value: 1 };
&guard.value // E0306
}
把所有权移出函数,或让引用只在拥有者作用域内使用。见引用、借用与逃逸。
E0307 在 match guard 里移动
guard 失败时还要继续尝试后面的臂,所以 guard 只能查看或借用非 Copy 的模式绑定,不能取得所有权;把 place 移进匿名函数同样被拒。
struct Token {}
enum MaybeToken { Some(Token), None }
fun consume(value: Token) -> bool { true }
fun main(value: MaybeToken) {
match value {
MaybeToken::Some(token) if consume(token) => {}, // E0307
MaybeToken::Some(token) => {},
MaybeToken::None => {},
}
}
把移动放到选中臂的 body 里,或先借用再判断。 相关:E0304、E0305
E0308 从解引用里移出非 Copy 值
*reference 或显式 &pattern/&mut pattern 里的按值绑定会读取引用指向的 T;引用不拥有这个值,T 不是 Copy 时不能搬出来。
struct Token {}
fun main() {
let token = Token {};
let r = &token;
let m = *r; // E0308
}
保留引用并通过它访问,或只在确实允许按位复制时为类型实现 Copy。*reference = value 是写回原位置,不属于这个码。
相关:E0100
借用、逃逸与 GC
E0200 保留码
E0200 当前没有任何发射点。逃逸提升是静默的:逃逸分析的结果只用来决定 MIR 里用 alloca 还是堆分配,不会转成用户可见的诊断。
// 没有可复现的写法:源码中不存在这个码的发射点
E0300 要可变借用,已有共享借用
同一个 place 上已有共享借用且仍然活跃时,不能再创建可变借用。
fun main() {
let mut x = 1;
let r = &x;
let m = &mut x; // E0300
println!("{} {}", *r, *m);
}
借用到持有它的绑定的最后一次使用为止,按源码位置计算,所以先用完再借是允许的:
fun main() {
let mut x = 1;
let r = &x;
println!("{}", *r);
let m = &mut x;
*m = 2;
}
这个近似不是 CFG 活跃性分析,循环体会做不动点迭代,冲突只报一次。见引用、借用与逃逸。 相关:E0301、E0302、E0309
E0301 要共享借用,已有可变借用
fun main() {
let mut x = 1;
let m = &mut x;
let r = &x; // E0301
println!("{} {}", *m, *r);
}
E0302 同一 place 同时两个可变借用
fun main() {
let mut x = 1;
let a = &mut x;
let b = &mut x; // E0302
println!("{} {}", *a, *b);
}
不相交的结构体字段可以分别可变借用;已知常量下标可以分别可变借用,动态下标按重叠处理。方法返回的引用会保持接收者被借用。 相关:E0300
E0303 借用期间赋值
place 仍有活跃借用时不能给它赋值。
fun main() {
let mut x = 1;
let r = &x;
x = 2; // E0303
println!("{}", *r);
}
E0304 借用期间移动
place 仍有活跃借用时不能移动它,模式里的字段移动和移进匿名函数同样受限。
struct P { x: i32 }
fun main() {
let p = P { x: 1 };
let r = &p;
let q = p; // E0304
println!("{} {}", r.x, q.x);
}
E0309 通过共享引用写入
&T 只能读。字段访问和下标会隐式解引用,所以经共享引用写入与显式 *r = value 一样被拒;&self 接收者同理。
struct Counter { mut hits: i32, name: i32 }
impl Counter {
fun bump(&self) {
self.name = 1; // E0309
}
}
字段声明成 mut 就不受这条限制:经 &self 写 hits、或对 hits 调用需要 &mut self 的方法都可以。但 mut 字段上取 &mut 只允许出现在调用里(实参或接收者),绑定到名字、返回或存进其它聚合体会报同一个码。
struct Counter { mut hits: i32, name: i32 }
fun add_to(target: &mut i32) {
*target += 1;
}
impl Counter {
fun bump(&self) {
self.hits += 1;
add_to(&mut self.hits);
}
}
fun main() {
let counter = Counter { hits: 0, name: 0 };
counter.bump();
}
struct Counter { mut hits: i32 }
impl Counter {
fun bad(&self) {
let target = &mut self.hits; // E0309
*target = 1;
}
}
经裸指针写入不受此检查约束。见引用、借用与逃逸。 相关:E0300、E0303、E0031
E0310 无 GC 模式下引用逃出栈存储
Clue.toml 里设置 [runtime] gc = false 后,不会再靠 GC 堆延长局部值的存活时间;返回局部值或临时值的引用、按引用捕获并逃出当前栈帧都会被拒。默认配置下(GC 开启)同样的代码只做堆提升,不报错。
// Clue.toml
[runtime]
gc = false
// src/main.rid,用 clue check 复现
struct Data { value: i32 }
fun escaped() -> &Data {
let value = Data { value: 42 };
&value // E0310
}
fun main() { escaped(); }
诊断文本是 reference to stack-owned value cannot escape when GC is disabled,help 提示返回或捕获拥有所有权的数据,note 说明从调用者传入的引用可以直接转发,因为那不会延长它指向值的存活期。gc = false 不能和 runtime.source 同时使用。见 Clue 构建器与引用、借用与逃逸。
过程宏与内部码
E0400 过程宏展开错误
riddlec 的过程宏驱动发出的错误,不在用户源码的 span 上。最常见的触发是调用了找不到的宏。
fun main() {
unknown_macro!(); // E0400
}
E0999 MIR 降级的兜底码
类型检查通过、但 MIR 的 as 降级找不到对应 CastOp 时发出,原文是 unsupported cast from X to Y。正常用户代码见不到它:出现这个码说明类型检查器的转换矩阵与 MIR 不一致,是编译器的 bug。
// 没有可复现的用户写法:这是内部兜底码
相关:E0012
编辑器里的码
LSP 诊断直接沿用上面的 E 码,source 固定为 riddle,code 是字符串形式的码。语言服务器无法从编辑器的增量变更重建缓冲区时,会为该文档发一条码为 LSP0001 的错误,消息是 this file is out of sync with the editor, so it was not analysed; reopen it or undo recent edits to restore diagnostics。见编辑器与 LSP。