Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

关于本书

《The Riddle Book》面向已经接触过至少一门编程语言、希望实际写出 Riddle 程序的读者。它既是循序渐进的教程,也是当前实现的入口索引;完整语法、错误码和编译器能力则放在附录中,避免参考资料打断学习主线。

如何阅读

本书按依赖关系组织内容:

  1. 用 Clue 创建并运行第一个项目;
  2. 学习注释、变量、类型、函数、表达式、块和控制流等通用概念;
  3. 理解移动、借用、逃逸与析构;
  4. 使用结构体、枚举与模式组织数据;
  5. OptionResultpanic 处理失败;
  6. 学习泛型、trait、impl 和模块等抽象机制;
  7. 学习集合、闭包与迭代器等标准库能力;
  8. 最后进入项目构建、编辑器、FFI 与工具链参考。

教程章节解释“为什么这样写”,工程章节解释“如何完成一个工作流”,附录回答“当前实现到底支持什么”。后面的章节默认读者已经掌握前面的概念。

内容边界

Riddle v0.2.3 仍是技术预览,语法和 ABI 可能发生不兼容变化。本书只描述当前仓库中已经实现并能由源码、测试或工具行为验证的功能,不把路线图写成现状。

如果教程、状态页和编译器行为不一致,以当前编译器和标准库实现为准,并应同步修正文档。当前工具链状态集中列出能力与限制。

编排参考

本书的教学组织参考了以下资料,但没有照搬它们的语言特性:

  • The Rust Programming Language:先运行程序,再逐步解释机制;“通用编程概念”“所有权与内存”“结构体与枚举”“错误处理”“泛型、Trait 与模块”“集合与函数式”的顺序都参考了它的编排;
  • Kotlin 官方文档中文版:把快速上手、基础语法、类与对象、泛型、集合和工具分成不同学习阶段;
  • Rust 语言圣经:把基础、进阶、工具链和实践分成不同学习阶段。

Riddle 与这些语言的具体差异见从 Rust 或 Kotlin 转到 Riddle

进入 Riddle 编程世界

Riddle 是一门仍在成长的实验性编程语言。它受 Rust 和 Go 启发,但目标不是把两门语言各抄一半,而是追问一个更具体的问题:在 GC 已经负责回收堆内存之后,移动语义、借用检查和确定性析构还有没有价值?

为什么要学 Riddle

我理解各位对于新生事物的恐慌和不信任,但如果各位真正深度使用过 Go 和 Rust 之后,一定能得到类似的答案。

Rust 对生命周期的过强控制导致了我们在写类似组合子之类的类型体操时会极大拖慢编译速度和 LSP 的类型检查的速度,同时,生命周期可能还会带来子类型的相关问题。更重要的是,我们平常几乎用不到超过两个以上的生命周期字面量。既然 lifetime 语法的弊大于利,那为什么不把他去掉?

Go 在异步相关方面做的非常不错,但也仅此而已了。Go 的过于简洁和设计上的错误导致了他变成了一个十分庞大的半成品,并且 Unsafe 相关的支持是破坏性的。

我们希望汲取他们两种语言的优缺点,来组合成为一个新的语言。

下一步

工具链装好了,直接进入你好,Riddle。如果你熟悉 Rust 或 Kotlin,可以先看一眼差异速查,避免被相似的关键字带偏。

从 Rust 或 Kotlin 转到 Riddle

Riddle 同时借用了 Rust 和 Kotlin 容易辨认的写法,但相同关键字不代表相同语义。本页只比较当前已经实现的行为。

语法速查

主题RiddleRustKotlin
函数fun add(x: i32) -> i32fn add(x: i32) -> i32fun add(x: Int): Int
不可变绑定let value = 1;let value = 1;val value = 1
可变绑定let mut value = 1;let mut value = 1;var value = 1
数据类型structenumstructenumclassdata classenum class
共享行为trait + impltrait + implinterface、继承与扩展函数
分支匹配matchmatchwhen
可恢复失败OptionResult?OptionResult?可空类型、异常,以及库类型 Result
可增长顺序容器Vector<T>Vec<T>MutableList<T>
项目工具clue + Clue.tomlCargo + Cargo.tomlGradle/Maven

与 Rust 的关键出入

所有权相似,存储策略不同

Riddle 和 Rust 都默认移动非 Copy 值,也都区分 &T&mut T。区别在于,Riddle 不提供显式生命周期参数:编译器追踪引用来源,并在引用可能越过当前栈帧时把值提升到保守式非移动 GC 堆。

GC 只决定存储位置,不代替所有权。移动后使用、冲突借用和 Drop 仍按静态规则检查。Riddle 没有显式的 BoxRcArc 类型;裸 dyn Trait 已经是拥有值,GC 开启时使用 GC 堆,关闭时使用 riddle_alloc / riddle_free

语法只是子集与重新组合

Riddle 使用 Rust 风格的尾表达式、structenumtraitimplmatchif letlet elsemoduse,但当前没有区间模式或声明式宏。Riddle 支持对象安全方法的借用 trait object(&dyn Trait / &mut dyn Trait)和拥有 trait object(dyn Trait),也支持拥有或借用的 dyn Fn / dyn FnMut / dyn FnOnce;泛型和其他 callable 主要通过静态单态化实现。

Cargo 与 Clue 不是同一个工具

Clue 借用了部分 Cargo 清单形状,依赖支持 path、git 和 sparse registry,并通过 Clue.lock 锁定解析结果;它仍不是 Cargo,不能把未实现的 Cargo 选项直接写入 Clue.toml

与 Kotlin 的关键出入

fun 相同,返回规则不同

Riddle 的块可以产生值,函数体最后一个没有分号的表达式就是返回值:

fun double(value: i32) -> i32 {
    value * 2
}

Kotlin 的块体函数通常使用显式 return,单表达式函数则写成 fun double(value: Int) = value * 2。不要因为两者都使用 fun 就照搬函数体规则。

没有类、可空类型或异常语法

Riddle 当前用结构体表示数据,用 trait 和 impl 表示共享行为,没有类继承、T?nullthrowtrycatch。可能缺失的值使用 Option<T>,可恢复失败使用 Result<T, E>,不可恢复路径使用 panic

不是 Kotlin 式托管对象模型

Riddle 值会移动,引用会借用,实现 Drop 的值在所有者结束时确定性析构。逃逸到 GC 堆不会把值变成可随意共享的对象,也不会取消借用检查。

平台与生态范围不同

Kotlin 文档按 JVM、Native、JavaScript、Wasm 和多平台组织内容。Riddle 当前只维护 C11 后端,并由目标组件与系统 C 工具链共同决定能否链接;因此本书不会复制 Kotlin 的平台章节。

阅读外部教程时的原则

可以借用 Rust Book 的概念顺序、Rust 圣经的学习路径和 Kotlin 文档的分类方式,但每段代码都应按 Riddle 的形式化语法当前工具链状态重新确认。遇到相似名称时,先查本书对应章节,不要默认 API 或边界条件也相同。

开始使用 Riddle

这一部分只做一件事:让一个最小 Riddle 项目在本机通过检查并运行。语法细节会在后续章节逐步解释。

Riddle v0.2.3 提供预编译发布包,也可以从源码安装。两种方式都会得到四个命令:

  • clue:创建、检查、构建和运行项目;
  • riddlec:检查一个或多个源码文件(合并为一个包)并生成 C;
  • riddle fmt:格式化 Riddle 源码或检查格式;
  • riddle-lsp:向编辑器提供语言服务。

先按安装 Riddle 工具链完成安装并确认四个命令可用,然后跟随你好,Riddle创建第一个项目,最后在创建与构建项目中了解清单、入口文件、本地依赖和输出路径。

完成后你应该能够:

  • 识别 Clue.tomlsrc/main.rid.clue/build 的职责;
  • 使用 clue check 在不生成可执行文件时检查项目;
  • 使用 clue run 构建并运行程序;
  • 看懂函数、变量、结构体和格式化输出在一个完整程序中的位置。

安装 Riddle 工具链

最省事的方式是下载预编译发布包,它不要求本机先安装 Rust。只有从源码安装 Riddle 时才需要仓库固定的 Rust 1.97.1;可以参考Rust 圣经的安装章节或 Rust 官方的 rustup 页面。

下载预编译版本

GitHub Releases 提供 Windows、Linux 和 macOS 的预编译 zip。下载对应平台和架构的文件,解压后把二进制所在目录加入 PATH。发布包包含 clueriddle-lspriddlecriddle、README 和 Apache-2.0 许可证。

使用 ridup 管理工具链

ridup 负责安装、选择并代理 Riddle 工具链。尚未安装 ridup 时,可以从源码安装:

cargo install --git https://github.com/riddle-lang/ridup --locked

ridup 提供三个发布通道:stable 下载最新正式 Release,nightly 下载每日构建,canary 下载 main 最新源码并在本机执行 release 构建。安装并选择通道:

ridup toolchain install stable
ridup toolchain install nightly
ridup toolchain install canary
ridup default stable
ridup show
ridup toolchain list

重复执行 toolchain install 会更新相应通道。stable 和 nightly 会选择当前宿主的发布归档并校验 SHA-256;canary 需要本机已有 Rust 和 Cargo,不需要 Git。宿主工具链实际按完整 triple 安装,例如 stable-x86_64-pc-windows-msvcstablenightlycanary 是指向当前宿主版本的便捷名称。

本地构建或已解压工具链可以直接链接:

ridup toolchain link dev D:\Code\riddle\target\debug
ridup default dev
ridup run dev clue --version

项目可用 riddle-toolchain.toml 固定工具链:

[toolchain]
channel = "canary"

选择优先级依次是代理参数(例如 clue +dev build)、RIDUP_TOOLCHAIN、最近目录的 ridup override set <toolchain>、最近的 riddle-toolchain.toml、默认工具链。以 clueriddlecriddleriddle-lsp 名称安装的 ridup 可执行文件会作为代理,运行所选工具链中的同名组件。

交叉目标独立于宿主工具链安装:

ridup target add aarch64-unknown-linux-gnu
ridup target list

首版只支持 x86_64-unknown-linux-gnuaarch64-unknown-linux-gnui686-unknown-linux-gnux86_64-pc-windows-msvci686-pc-windows-msvcaarch64-pc-windows-msvcaarch64-apple-darwin,其他 triple 会被拒绝。

如何选择 Release 资产

Release 中有两种 zip,名称里的 target- 表示交叉编译目标,不表示当前宿主平台:

文件名模式用途内容
riddle-v<version>-<platform>-<arch>.zip安装当前机器上的 Riddle 工具链clueriddle-lspriddlecriddle 等可执行文件
riddle-v<version>-target-<triple>.zip为指定目标进行交叉构建runtime.ctarget.toml 和许可证;不包含 clueriddleriddlecriddle-lsp

例如,在 Windows x86_64 上使用 riddle-v0.2.3-windows-x86_64.zip 安装工具;要构建 aarch64-unknown-linux-gnu 程序,则运行 ridup target add aarch64-unknown-linux-gnu 安装对应目标组件。目标组件不会替代 Linux sysroot、Windows SDK/MSVC 库或 Apple SDK,也不需要单独加入 PATH

target add 会先安装 Riddle runtime,再询问是否安装匹配的 LLVM/Clang。目标组件已安装和 C 工具链已就绪是两个独立状态;Linux 还需要目标 sysroot,Windows MSVC 目标需要 Windows SDK 与 MSVC 库,macOS 需要 Apple SDK。缺少这些系统组件时仍可运行 clue check,也可用 riddlec 生成可移植 C,但 Clue 不会把该目标报告为可链接状态。

所有目标命令都可用 --toolchain <name> 指定工具链。C 工具链可以自动探测或单独配置:

ridup c-toolchain install aarch64-unknown-linux-gnu
ridup target configure aarch64-unknown-linux-gnu --compiler C:\LLVM\bin\clang.exe --sysroot D:\sysroots\aarch64-linux-gnu
ridup target remove aarch64-unknown-linux-gnu

从源码构建

找一个适合存放代码的目录,执行下述命令:

git clone https://github.com/riddle-lang/riddle --depth=1
cd riddle
cargo install --path . --features install-bins --force --target-dir "${TMPDIR:-/tmp}/riddle-install"

这会一次安装 clueriddle-lspriddlecriddle。在 PowerShell 中也可以写成:

cargo install --path . --features install-bins --force --target-dir "$env:TEMP\riddle-install"

如果只验证这四个安装二进制的构建,请限定根发行包,避免 workspace 中的同名开发包重复输出:

cargo build -p riddle --release --features install-bins --bins

如果只想在源码目录中临时运行,也可以使用:

cargo run -p riddle -- fmt --help
cargo run -p riddlec -- --help

安装后可用以下命令确认版本:

riddlec --version
clue --version
riddle --version
riddle-lsp --version

验证源码树

提交源码变更前,运行完整 workspace 测试、安装入口检查、格式检查和严格 Clippy:

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

根包的 install-bins feature 负责发布 clueriddle-lspriddlecriddle。不要把 --all-features 加到 workspace 测试命令中,否则根发行包与成员 crate 的同名二进制会触发 Cargo 输出文件碰撞警告;独立的 cargo check 已覆盖这些安装入口。

该命令检查 Clippy 默认启用的全部规则,并把告警视为错误。pedanticnurserycargo 是独立的实验性或额外风格类别,不属于仓库的合并门槛。

需要实时诊断和语义高亮时,继续阅读编辑器与 LSP

当前命令行入口是 riddlec

riddlec [--verbose] [--no-std] [--backend c] [--target <triple>] [--output <file>] <file>...

例如,创建一个项目,再把入口文件通过 C backend 生成 C 源码:

clue new hello
cargo run -p riddlec -- --backend c --output hello.c hello/src/main.rid

C backend 只写出调用 rgc ABI 的 C 源码,不会调用系统编译器。二进制发行包附带默认 runtime.cargs_runtime.c;如需本机可执行文件,可以运行 cc hello.c runtime.c args_runtime.c -o helloclue build 会自动完成这一步。

构建文档

本文档使用 mdBook 编写。如果你想在本地预览文档,可以安装 mdBook:

cargo install mdbook

然后在中文文档目录下构建或预览:

cd docs/zh-CN
mdbook build
mdbook serve --open

构建结果会输出到仓库中的 dist/zh-CN 目录。

常见问题

cargo build 失败怎么办?

请先确认 Rust 版本足够新。当前项目使用 Rust 2024 edition,并通过根目录的 rust-toolchain.toml 固定 Rust 1.97.1

rustup toolchain install 1.97.1

如何运行 C backend 的输出?

riddlec --backend c 只生成 .c 文件。请使用系统中的 ccgccclang,把生成文件与发行包附带的 runtime.cargs_runtime.c 一起编译;默认运行时不依赖 Boehm GC,因此不需要 -lgcclue build 会自动完成这一步。

示例文件在哪里?

仓库不单独维护 examples/ 目录。可以使用 clue new 创建最小项目;其他语言能力以本书中的可运行代码片段和测试为准。

不过示例可能会跟随语言设计快速变化。学习 Riddle 的主要概念时,请按目录继续阅读“通用编程概念”“所有权与内存”和“结构体与枚举”。

你好,Riddle

这一章创建一个真实项目,写入一段完整代码,然后让 Clue 检查并运行它。

创建项目

在终端执行:

clue new hello
cd hello

Clue 会创建:

hello/
  Clue.toml
  src/
    main.rid

Clue.toml 描述包和构建目标,src/main.rid 是默认二进制入口。

写入第一个程序

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 run

程序输出的最后一行应为:

distance squared = 25

这段代码包含什么

struct Point 定义一种数据形状,两个字段都是 i32Point { x: 3, y: 4 } 构造一个值。

函数使用 fun 声明。参数类型写在名称后,返回类型写在 -> 后。distance_squared 的最后一个表达式没有分号,因此它成为函数返回值。

let 创建默认不可变的绑定。println! 使用 {}Display 格式输出值。

point 传给 distance_squared 会移动这个结构体,调用后原绑定不再可用。这里恰好不再需要它;完整规则会在移动语义解释。

检查与运行的区别

clue check 执行解析、名字解析、类型检查以及移动、借用和逃逸分析,但不调用系统 C 编译器。clue run 会先完成构建,再运行生成的本机可执行文件。

项目清单、本地依赖、缓存和输出路径稍后统一放在创建与构建项目说明。现在先进入注释

创建与构建项目

本页从一个空目录开始创建并构建 Clue 项目。

创建项目

clue new hello

new 要求目标目录不存在。要在已有目录中初始化项目,可以运行 clue init .--bin 是默认值,也可以用 clue new --lib math 创建库项目。要创建只负责注册子 crate 的虚拟工作区,可以运行 clue new workspace --workspace

这会创建 hello/Clue.tomlhello/.gitignorehello/src/main.ridClue.toml 的初始内容如下:

[package]
name = "hello"
version = "0.1.0"

[[bin]]
name = "hello"
path = "src/main.rid"

[dependencies]

.gitignore 会忽略 /.clue,也就是 Clue 的构建缓存和输出目录。

库项目改为生成 [lib] 目标和 src/lib.rid

[lib]
name = "math"
path = "src/lib.rid"

initnew 都不会覆盖已有的 Clue.toml 或目标入口源码。默认生成的二进制入口为:

fun main() {
}

构建项目

clue build hello

也可以在项目目录内运行:

clue build

清单中的 [[bin]].path[lib].path 指定入口。单个二进制目标时,旧式 [package].entry 的优先级更高;多个 [[bin]] 各自使用 pathclue build 默认构建全部,也可以用 clue build --bin <name> 只构建一个。没有显式目标时,clue build 按包类型寻找入口文件。二进制包依次检查 src/main.ridsrc/lib.rid<package-name>.ridmain.rid;库和过程宏包依次检查 src/lib.rid<package-name>.ridlib.ridsrc/main.rid

旧清单也可以在 [package] 中指定入口文件:

[package]
name = "hello"
entry = "src/bin/hello.rid"
version = "0.1.0"

构建时会按 Rust 风格展开 mod name;,只有被 mod 声明的文件会加入编译:

  • 在当前模块目录下读取 name.ridname/mod.rid
  • name.ridname/mod.rid 同时存在会报错;
  • 进入 name 模块后,子模块会继续从 name/ 目录下寻找。

例如 src/main.rid 写了 mod foo;,会读取 src/foo.ridsrc/foo/mod.rid。如果 src/foo/mod.rid 里再写 mod bar;,会读取 src/foo/bar.ridsrc/foo/bar/mod.rid。仅仅存在 src/foo/mod.rid 但没有 mod foo; 时,这个目录不会被编译。

如果找不到入口文件,Clue 会报错:

missing entry file; expected src/main.rid, src/lib.rid, <package>.rid, main.rid, or lib.rid

使用依赖

Clue 支持 path、git 和 sparse registry 依赖。假设有两个相邻的本地包:

workspace/
  hello/
    Clue.toml
    src/main.rid
  math/
    Clue.toml
    src/lib.rid

hello/Clue.toml 可以写成:

[package]
name = "hello"
version = "0.1.0"

[dependencies]
math = { path = "../math" }

如果依赖包名不能直接当模块名,可以像 Cargo 一样用 package 指向真实包名:

[dependencies]
math = { package = "math-core", path = "../math-core" }

依赖键会作为模块名出现在 hello 包中:

fun main() -> i32 {
    math::one()
}

依赖包会使用自己的入口规则,也可以在依赖包的 [package] 中设置 entry。依赖声明还可以使用 semver、git 的 branch/tag/rev、registry、feature 和 optional 配置:

[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 }

单包项目会在项目根目录维护 Clue.lock v3;工作区在根目录维护一个统一的锁文件。clue fetch 获取并校验依赖,普通构建复用锁定版本,clue update 才重新选择版本。--locked 拒绝缺失或过期的锁文件,--offline 只使用缓存。

作为依赖加载时,Clue 会优先读取依赖包的 [lib].path;没有 [lib] 目标时,默认先寻找 src/lib.rid。依赖包中要被外部包使用的项需要写成 pub

pub fun one() -> i32 {
    1
}

也就是说,math/src/lib.rid 中的私有函数仍只能被 math 包内部使用,hello 只能通过 math::one() 访问 pub 导出的项。模块、use 和可见性规则见模块、use 与包

使用工作区

工作区根目录的 Clue.toml 只注册子 crate:

[workspace]
crates = ["hello", "math"]

hello/Clue.tomlmath/Clue.toml 仍分别声明自己的 [package]、目标和依赖。根目录执行 clue checkclue build 会按依赖顺序处理所有注册 crate;在子 crate 目录执行时默认只处理当前 crate,--workspace 选择全部,--package <name> 选择一个。工作区中的包和包内二进制分别用 --package <name>--bin <name> 选择。

Clue 会在根目录生成 Clue.lock v3,记录 path 包的相对路径与源码指纹、git revision、registry checksum、依赖关系和启用的 feature。--locked 会拒绝缺失或过期的锁文件。子 crate 不生成锁文件;工作区内部的 path 依赖必须同时出现在 workspace.crates 中。

输出文件

二进制项目会保留生成的 C 源码,并输出本机可执行文件。默认宿主 debug 构建使用兼容旧版本的目录;--release 和交叉目标使用独立目录:

.clue/build/<bin-name>.c
.clue/build/<bin-name>       # Linux/macOS
.clue/build/<bin-name>.exe   # Windows
.clue/build/<target>/release/<bin-name>[.exe]

构建成功时会输出:

clue: built .clue/build/hello.exe

实际路径会随平台变化。源码、Clue.toml、编译器版本和 C 编译器选择都没变化时,再次运行会复用缓存:

clue: fresh .clue/build/hello.exe

库项目默认生成 C、目标文件、.rmeta.rlibcrate-type = ["riddlelib", "staticlib", "cdylib"] 还会生成可复用的静态库和动态库。二进制项目可以直接运行:

clue run hello
clue run hello -- arg1 arg2

设置 CC 时 Clue 会严格使用指定的 C 编译器;否则先尝试目标组件 c-toolchain.toml 中配置的编译器(由 ridup target configure 设置),再探测 clangccgcc(Windows 目标额外尝试 clang-clcl),最后尝试带版本后缀的 GCC/Clang。只有能完成 C11 编译和链接的候选才会被采用。

注释

注释给代码补充人类可读的说明,帮助读者理解意图。编译器会忽略注释的内容。

行注释

Riddle 使用 // 行注释,从 // 到行尾的内容都会被忽略:

// 计算两点间距离的平方
fun distance_squared(x: i32, y: i32) -> i32 {
    x * x + y * y // 尾随注释
}

块注释

块注释从 /* 开始,到匹配的 */ 结束,并支持嵌套:

/* 外层说明
   /* 内层说明 */
   外层继续 */
fun square(x: i32) -> i32 { x * x }

块注释可以出现在表达式、类型和声明之间。未闭合的块注释会把文件剩余内容视为注释;需要保留后续代码时应补上闭合的 */

文档注释

文档注释使用 ////** ... */ 写在声明前;也可以用 //< 在同一行记录前一个声明或字段。//!/*! ... */ 用于模块或文件级说明:

//! 几何工具模块

/** 计算两点间距离的平方。 */
fun distance_squared(x: i32, y: i32) -> i32 {
    x * x + y * y
}

struct Point {
    x: i32, //< 横坐标
    y: i32, //< 纵坐标
}

只有 //< 会附着到前一个节点;普通 // 仍是普通注释,///< 仍按 /// 处理并附着到后一个节点。文档注释不参与类型检查或生成运行时代码。语法树层提供 ast::doc_comments_for_node,HIR 会保留文档注释与声明的源范围;LSP 的悬停和签名帮助会把紧邻声明的文档注释作为 Markdown 说明返回。

写普通的多行说明时,也可以继续使用多条 //

// 该函数只在输入为非负整数时有意义。
// 需要处理负输入时,先用 `abs` 取绝对值。
fun square(x: i32) -> i32 {
    x * x
}

普通注释中的内容不参与任何编译阶段;错误码参考、LSP 悬停等能力不会自动把普通注释转换成文档。文档工具读取文档注释时仍需自行解释其文本格式。

变量与可变性

变量是给值起名字的方式。在 Riddle 中,变量默认不可变。 这个默认选择让代码更容易推理:当你看到一个普通绑定时,就可以假设它不会在后续被改写。

使用 let 创建绑定

最简单的变量绑定使用 let

fun main() {
    let answer = 42;
    print!("{}", answer)
}

这里的 answer 绑定到整数 42。默认情况下,你不能重新给 answer 赋值。

默认不可变

下面的代码表达了 Riddle 不希望你在普通绑定上做的事情:

fun main() {
    let answer = 42;
    answer = 43; // error: answer 不可变
}

不可变默认值有两个好处。 第一,它减少了意外修改。第二,它让移动和引用规则更容易理解,因为一个值不会在你没有注意到的地方被改掉。

使用 mut 表示可变

如果一个变量确实需要变化,需要显式写出 mut

fun main() {
    let mut count = 0;
    count = count + 1;
    print!("{}", count)
}

mut 是一种提醒:这个绑定后面会发生变化。读代码的人看到 mut,就知道需要关注这个值的更新路径。

变量遮蔽

可以用新的 let 声明一个同名变量,新绑定会遮蔽旧的,遮蔽前的名字在遮蔽之后不再可用:

fun main() {
    let value = 5;
    let value = value + 1;  // 新绑定,遮蔽旧的 value
    print!("{}", value)
}

遮蔽和修改的区别在于:修改要求原绑定是 mut 且类型不变;遮蔽总是创建新绑定,可以改变类型:

let label = "hello";     // &str
let label = label.len(); // usize,类型可以不同

遮蔽后的旧绑定不能再通过名字使用,但旧的存储仍然存活到作用域结束:如果旧值实现了 Drop,它会在离开当前作用域时析构,而不是在遮蔽发生时。这与 Rust 的行为不同(Rust 在遮蔽时立即析构旧值),也与 mut 覆盖写不同(覆盖写发生在赋值点)。内部作用域里的同名绑定会遮蔽外层绑定,离开作用域后外层绑定恢复可用(见表达式与块)。

解构绑定

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); // 用 `_` 丢弃不需要的部分
    print!("{}", a + b + x + y + second)
}

mut 属于单个绑定,而不是整条 let。想让其中一个元素可变,就写在它自己前面:

fun main() {
    let (mut count, step) = (0, 5);
    count = count + step;
    print!("{}", count)
}

let mut (count, step) = ... 不是合法写法。

普通 let 没有备选分支,所以它的模式必须匹配该类型的每一个值。枚举变体、字面量这类只覆盖部分取值的模式会报告 E0057,需要改用 match,或者为这个绑定提供 else 分支:

fun unwrap(value: Option<i32>) -> i32 {
    let Some(number) = value else {
        return 0;
    };
    number
}

let-elseelse 块必须发散(例如 returnbreakcontinue 或无限 loop),匹配成功后绑定会在当前作用域的后续代码中可用;失败分支看不到这些绑定。

类型标注

变量可以写类型标注:

fun main() {
    let age: i32 = 18;
    let name: &str = "Riddle";
}

很多时候类型可以从初始化表达式推导出来。需要让意图更清楚,或者编译器无法推导时,可以写出类型。

延迟初始化

let 可以先声明、后赋值。带类型标注的绑定直接使用标注类型;没有标注时,编译器会从首次赋值推断类型:

fun main() {
    let value: i32;
    value = 10;

    let inferred;
    inferred = 20;
}

不可变绑定的首次赋值不需要 mut;如果要在首次赋值后再次赋值,声明时必须写 mut

let once: i32;
once = 1;             // OK
once = 2;             // E0031

let mut many: i32;
many = 1;
many = 2;             // OK

使用前没有在所有路径上完成赋值会报告 E0059。分支需要分别初始化:

fun choose(flag: bool) -> i32 {
    let value: i32;
    if flag { value = 10; } else { value = 20; }
    value
}

如果某条路径没有赋值,后续使用就是错误:

fun incomplete(flag: bool) -> i32 {
    let value: i32;
    if flag { value = 10; }
    value // E0059
}

const 常量

const 用于定义编译期常量,必须写明类型并初始化:

const MAX: i32 = 100;
const GREETING: &str = "hello";

const 可以出现在顶层模块和 impl 块中。与 let 不同,const 的值在编译期确定,不能省略类型标注。

整数常量初始化式支持算术、比较、位运算、一元负号/取反、as 转换以及引用其他常量(初始化循环会被拒绝),并通过编译期求值得出具体值。求值后的常量可以用作数组类型长度、数组重复长度和 const 泛型实参:

const WIDTH: usize = 8;
const HEIGHT: usize = WIDTH / 2;

let row: [i32; WIDTH] = [0; WIDTH];
let grid: [i32; 32] = [0; WIDTH * HEIGHT];

数据类型

Riddle 是静态类型语言。每个绑定、参数、返回值和表达式都会在编译期得到一个具体类型;多数局部绑定可以从初始化表达式推断,函数边界通常显式写出类型。

本章只介绍日常代码首先需要的类型。结构体、枚举、trait、callable、底层 ABI 和属性分别放在后续专题中。

标量类型

类型含义
i8i16i32i64isize有符号整数
u8u16u32u64usize无符号整数
f32f64浮点数
booltruefalse
charUnicode 标量值
()unit 类型及其唯一值
!永不产生值的 never 类型

isizeusize 跟随目标指针宽度。unit 不是 Riddle 类型名,空结果写作 ()

字面量与后缀

数值字面量可以显式指定类型:

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;

整数字面量支持十进制、0x 十六进制、0o 八进制和 0b 二进制;各形式都可以使用 _ 分隔符和整数类型后缀。

词法器会额外接受 i128u128f16f128 后缀,但类型系统只支持 8 到 64 位的整数和 f32 / f64,这些后缀会在类型检查时报 E0011

整数算术按目标位宽回绕。除零以及有符号最小值除以 -1 会终止进程;移位计数按位宽取模。有符号右移使用算术右移。

char 与数值转换

char as Integer 得到 Unicode 码点。u8 as char 也受支持,因为任意 u8 都是合法 Unicode 标量值;其他整数不能直接转换成 char

let code_point = '中' as u32; // 20013
let letter = 65u8 as char;    // 'A'

更完整的 as 转换范围见表达式与块

Never 类型

! 表示表达式永远不会正常产生值。标准宏 panic!(...)todo!(...)unimplemented!(...)unreachable!(...) 返回 !,所以可以出现在需要其他结果类型的分支:

fun require(valid: bool) -> i32 {
    if valid { 42 } else { panic!("invalid state: {}", valid) }
}

当前 panic 运行时会输出源位置和消息,然后调用 C abort();它不会进行栈展开或恢复。可恢复失败应使用 OptionResult,详见错误处理

元组

元组是定长异构值。逗号用于区分元组与普通分组括号:

let pair: (i32, bool) = (42, true);
let single: (i32,) = (42,);
let unit: () = ();

元组元素也可以用数字字段访问,索引从 0 开始:

let number = pair.0;
let flag = pair.1;

元组可以通过模式拆开:

let (number, flag) = pair;

固定长度数组

数组类型写作 [T; N],元素类型与长度都在编译期确定:

let values: [i32; 3] = [1, 2, 3];
let zeros: [i32; 3] = [0; 3];

let mut pair = [10, 20];
pair[0] = 99;

数组和切片的普通索引会检查边界,越界时向 stderr 输出 riddle: index out of bounds 并终止进程。需要可恢复的访问时,先借用为切片,再使用 getget_mut

数组长度可以来自 const 泛型参数:

struct Buffer<T, const N: usize> {
    data: [T; N],
}

let buffer: Buffer<i32, 3> = Buffer { data: [1, 2, 3] };

引用

引用暂时访问一个值而不取得所有权:

  • &T 是共享引用,可以同时存在多个;
  • &mut T 是独占可变引用,存活期间不能再创建冲突引用。
let mut value = 42;
let shared: &i32 = &value;
let copied = *shared;

let exclusive: &mut i32 = &mut value;
*exclusive = 43;

借用检查、自动重借用和引用来源将在引用与逃逸详细解释。

切片与不定长类型

[T]str 是不定长类型,不能直接作为局部变量、参数、返回值或普通字段,必须位于引用或原始指针后。对它们的引用同时携带数据地址和长度:

let values = [1, 2, 3];
let slice: &[i32] = &values;
let text: &str = "hello";

切片提供 lenis_emptygetget_mutiteriter_mut;共享和可变切片引用都可用于 for&[T] / &mut [T] 可由对应可变性的数组引用自动转换。

字符串

str 表示不定长的字符串内容,&str 表示可传递的字符串引用值。裸 str 只能作为引用、原始指针或 impl 的目标,不能作为独立值使用。

&str 是对 str 的引用,由两个机器字组成:指向 UTF-8 数据的指针和字节长度(胖指针)。字符串字面量 "..." 的类型就是 &str

let greeting: &str = "hello";

如果字符串内容里包含很多引号或反斜杠,可以使用 raw string。raw string 不解释转义,结束符由 # 的数量决定:

let a: &str = r"hello";
let b: &str = r#"say "hello""#;
let c: &str = r###"content with "# inside"###;

#[lang = r#"copy"#] 这类属性字符串也支持 raw string。

&str 可以自由地在函数之间传递——它只是一个胖指针值:

fun greet(name: &str) {
    // name 是调用方传入的字符串切片的借用
}

fun main() {
    greet("Riddle");
}

&str 通过标准库提供字节长度、空值判断和字节切片视图:

let text: &str = "hello";
let length = text.len();                    // 5usize(UTF-8 字节数,不是字符数)
let empty = text.is_empty();                // false
let bytes = text.as_bytes();                // &[u8]

for ch in "A中🙂" {
    // ch: char,按 UTF-8 解码
}

as_bytes 不复制数据,返回共享字节切片。&str 可直接用于 for,迭代器按 UTF-8 解码并依次产出 Unicode char

可增长的 String 属于集合,见集合&str 在 C 后端的 ABI 表示见FFI 与底层工具链

结构体、枚举与泛型类型

用户可以定义带字段的结构体和带变体的枚举:

struct Pair<A, B> {
    first: A,
    second: B,
}

enum Slot<T> {
    Empty,
    Value(T),
}

嵌套泛型不需要在 > 之间插入空格:

let value: Slot<Pair<i32, bool>> = Slot::Value(Pair {
    first: 1,
    second: true,
});

函数、trait、impl、结构体与枚举的类型参数都可以直接带 trait bound,也可以使用 where 子句。const 参数声明自己的整数类型,当前主要用于数组长度。构造、模式匹配与行为实现分别见结构体枚举、模式与 matchTraitimpl 块

原始指针

*const T*mut T 是供 FFI 与底层实现使用的原始指针,不参与普通引用的借用跟踪。解引用和索引必须位于 unsafe 中:

let pointer = 0usize as *const i32;
// unsafe { *pointer } 会解引用空指针,不要执行

原始指针不是绕过类型、移动或可变性检查的通用工具。完整安全边界和 C 类型映射见FFI 与底层工具链

继续阅读

匿名函数与 Fn / FnMut / FnOnce 的完整规则见闭包与迭代器。可增长的 String 和其他容器见集合。当前完整类型能力与限制见当前工具链状态

函数

函数是 Riddle 程序的基本组织单位。它把一段逻辑命名,让代码可以被复用、测试和组合。

当前 Riddle 使用 fun 定义函数。

定义函数

一个最小的函数可以没有参数,也没有显式返回类型:

fun greet() {
    print!("{}", "hello")
}

函数名后面是一对括号,函数体放在 {} 中。

参数

参数写在括号里,每个参数都带类型:

fun greet(name: &str) {
    print!("{}", name)
}

多个参数使用逗号分隔:

fun add(a: i32, b: i32) -> i32 {
    a + b
}

参数也是绑定。传入非 Copy 值会移动所有权;整数、布尔值等实现了 Copy 的类型则按复制语义传入。完整规则见移动语义

返回值

返回类型写在 -> 后面:

fun square(x: i32) -> i32 {
    x * x
}

函数体最后一个没有分号的表达式就是返回值。

这和下面显式写 return 的形式表达同样的意图:

fun square(x: i32) -> i32 {
    return x * x;
}

Riddle 鼓励在简单函数中使用尾表达式,因为它能减少样板代码。

提前返回

当你需要提前结束函数时,可以使用 return

fun abs(x: i32) -> i32 {
    if x < 0 {
        return -x;
    }

    x
}

return 更适合错误分支、提前退出或复杂控制流。普通计算则可以交给尾表达式。

泛型函数

泛型函数的类型参数、推断、显式实参和 const 泛型见泛型一章。

可调用参数与返回值

参数位置可以使用一般的 impl Trait,它等价于由编译器引入一个满足该 bound 的隐藏泛型参数;返回位置的 impl Trait 隐藏一个具体返回类型,所有返回路径必须选择同一具体类型。impl Fnimpl FnMutimpl FnOnce 额外携带调用签名;需要运行时分派时可使用拥有或借用的 dyn Trait / dyn Fn*。完整的捕获规则、调用能力与限制见闭包与迭代器

函数声明

有些函数可能只声明签名,具体实现由外部提供:

fun external_log(value: i32);

这种形式以分号结束,没有函数体。

表达式与块

Riddle 中很多东西都是表达式。表达式会产生值,而语句主要用于执行动作。 理解表达式和块,是理解 Riddle 函数、控制流和所有权规则的基础。

表达式会产生值

下面这些都是表达式:

1
x + y
foo(1, 2)
(2, 3)
point.x
items[0]
n as i64

表达式可以出现在变量初始化、函数参数、返回值和更大的表达式中。

元组表达式用逗号区分于普通分组括号:(2, 3) 是二元组,(2,) 是单元素元组,(2) 仍只是整数 2() 表示 unit。

匿名函数

匿名函数使用方括号形式 [参数 -> 体](例如 [x -> x + 1]),可以保存到变量或作为参数传递。它按用法推断捕获方式(共享引用、可变引用或按值),并通过 Fn / FnMut / FnOnce 静态能力参与类型检查;move [参数 -> 体] 按值捕获所有使用到的外部位置。完整的捕获规则、impl Fn* 参数与返回位置用法见闭包与迭代器

分号会丢弃值

在 Riddle 中,分号表示“把这个表达式当成语句执行,并丢弃它的结果”。

fun main() {
    let x = 1 + 2;
    x + 1;
}

x + 1; 有分号,所以它的结果不会作为函数体返回值。

如果去掉分号,它就会成为块的尾表达式:

fun value() -> i32 {
    let x = 1 + 2;
    x + 1
}

这个函数返回 x + 1 的结果。

块也是表达式

块由 {} 包围,可以包含多条语句,也可以有一个尾表达式:

fun main() {
    let value = {
        let a = 1;
        let b = 2;
        a + b
    };

    print!("{}", value)
}

这里内部块的值是 a + b,因此 value 会绑定到这个结果。

块创建作用域

块不仅能产生值,也会创建新的作用域:

fun main() {
    let outer = 1;

    {
        let inner = 2;
        print!("{}", inner);
    }

    print!("{}", outer)
    // inner 在这里不可用
}

作用域会影响变量何时失效,也会影响引用是否逃逸。后面的“引用与逃逸”会详细解释这一点。

常见表达式

数组字面量和索引用于固定长度数组:

fun first() -> i32 {
    let values: [i32; 3] = [1, 2, 3];
    let zeros: [i32; 3] = [0; 3];
    values[0]
}

类型转换使用 as

let wide = 1i32 as i64;

当前支持整数之间、整数与浮点数之间、浮点数之间、布尔值到整数、整数到布尔值、u8char、整数到原始指针,以及原始指针之间的转换。不支持的组合会报告 E0012。

区间表达式

a..ba..=b 分别构造半开区间 [a, b) 和闭区间 [a, b],最常用于 for 循环:

fun total() -> i32 {
    let mut sum = 0;
    for value in 1..=4 {
        sum += value;
    }
    sum
}

a..b 脱糖为 std::ops::range(a, b)a..=b 脱糖为 std::ops::range_inclusive(a, b),两个函数都在 std::ops 中,但区间表达式本身不需要导入。区间是右结合的普通表达式,优先级低于 ||;开区间 a....b.. 尚未实现。

复合赋值会读取左侧、执行对应运算,再写回左侧:

fun count() {
    let mut n: i32 = 1;
    n += 2;
    n <<= 1;
}

unsafe 块也是表达式。它为原始指针解引用、原始指针索引、DST 布局转换和不安全函数调用提供显式上下文;类型、move 和借用检查仍然照常执行:

let value = unsafe {
    1
};

控制流

控制流决定程序在不同条件下执行哪些代码。Riddle 提供 ifwhileformatch

match 在这里先介绍基本用法;引用模式、穷尽性检查和匹配人体工学等完整规则需要结构体和枚举知识,放在枚举、模式与 match一章。

if 表达式

if 根据条件选择一个分支:

fun sign(x: i32) -> i32 {
    if x < 0 {
        -1
    } else if x == 0 {
        0
    } else {
        1
    }
}

这里整个 if 是函数体的尾表达式,因此它的结果就是函数返回值。

分支也是块

每个分支都是一个块。块的尾表达式就是这个分支的值:

fun choose(flag: bool) -> i32 {
    let value = if flag {
        10
    } else {
        20
    };

    value
}

因为 if 会产生值,所以两个分支应该产生兼容的类型。

没有 else 的 if

如果你只需要在条件成立时执行一些动作,可以不写 else

fun print_if_positive(x: i32) {
    if x > 0 {
        print!("{}", x);
    }
}

这种写法更像普通语句。它适合日志、提前检查和局部动作。

if let 表达式

if let 用一个模式代替布尔条件:匹配成功时执行第一个分支,并把模式里的绑定引入分支作用域:

fun describe(value: Option<i32>) -> i32 {
    if let Some(n) = value {
        n
    } else {
        0
    }
}

它等价于一个只关心一种情况的 match:匹配成功的分支对应模式臂,else 分支对应 _ 通配臂(不写 else 时相当于空的 _ 臂)。绑定只在匹配成功的分支内可见,else 分支和语句之后都看不到它。

模式可以使用任何匹配模式(枚举、结构体、元组、字面量等),但不支持 if guard:

fun first_of(pair: Option<(i32, i32)>) -> i32 {
    if let Some((first, _)) = pair {
        first
    } else {
        0
    }
}

while 循环

while 会在条件成立时反复执行块:

fun count_to_three() {
    let mut i = 0;

    while i < 3 {
        print!("{}", i);
        i = i + 1;
    }
}

循环中经常会用到 mut,因为循环变量需要更新。

while let 循环

while let 在每次迭代时重新求值条件表达式,并在模式匹配成功时执行循环体,匹配失败时结束循环:

fun drain(mut current: Option<i32>) -> i32 {
    let mut total = 0;

    while let Some(value) = current {
        total += value;
        current = if value > 0 { Some(value - 1) } else { None };
    }

    total
}

if let 一样,模式绑定只在循环体内可见,且不支持 if guard。因为条件表达式每次迭代都会重新求值,它适合配合返回 Option 的迭代协议消费序列。

loop 循环

loop 是无条件无限循环,只会在 break 时结束:

fun next_power_of_two(mut n: u32) -> u32 {
    loop {
        if n >= 1024 {
            break;
        }
        n = n * 2;
    }
    n
}

whilefor 不同,loop 是会产生值的表达式:在 loop 内可以写 break 值; 把值交给整个循环表达式。所有 break 值的类型会被合并为循环的结果类型:

fun first_match(limit: i32) -> i32 {
    let mut i = 0;

    let found = loop {
        if i >= limit {
            break -1;
        }
        if i % 7 == 0 && i > 0 {
            break i;
        }
        i += 1;
    };

    found
}

带值的 break 只对 loop 有效;在 whilefor 中写 break 值; 会报错。如果一个 loop 没有任何可达的 break,它永远不会结束,类型是 !(never),可以出现在任何期待值的位置。

for 循环

for 使用 IntoIterator / Iterator 协议遍历值:

use std::ops::range;

fun sum_to_three() -> i32 {
    let mut sum = 0;

    for item in range(0, 3) {
        sum += item;
    }

    sum
}

std::ops::range(start, end) 产生半开区间 [start, end),使用前需要显式导入。更常见的是直接写区间表达式:for i in 0..3 等价于 for i in range(0, 3)a..b 脱糖为 std::ops::range(a, b)a..=b 脱糖为 range_inclusive(a, b),都不需要导入。固定长度数组、Vector、切片和 &str 也都可以直接用于 for;如何为自定义类型实现 IntoIterator,见闭包与迭代器

循环头不只是单个变量名,可以是任何不可反驳模式,遍历时直接解构元素:

fun total(pairs: [(i32, i32); 2]) -> i32 {
    let mut sum = 0;

    for (key, value) in pairs {
        sum += key + value;
    }

    sum
}

let 一样,for 没有备选分支,所以枚举变体、字面量这类只覆盖部分取值的可反驳模式不能写在循环头,会报告 E0057;需要区分情况时在循环体内使用 match

break 与 continue

break 立即结束最近一层循环,continue 跳到最近一层循环的下一次迭代:

use std::ops::range;

fun first_three_odd_sum() -> i32 {
    let mut sum = 0;

    for value in range(0, 10) {
        if value == 6 {
            break;
        }
        if value % 2 == 0 {
            continue;
        }
        sum += value;
    }

    sum
}

breakcontinue 只能用在循环体中,且不支持标签。break; 在三种循环中都可用;break 值; 只对 loop 有效,见上文 loop 循环一节。

match 基础

match 根据值的形状选择分支。最简单的用法是匹配字面量:

fun classify(n: i32) -> i32 {
    match n {
        0 => 0,
        _ => 1,
    }
}

arm 由模式、可选的 if guard 和 => 后的表达式组成:

fun classify(n: i32) -> i32 {
    match n {
        x if x < 0 => -1,
        0 => 0,
        _ => 1,
    }
}

标准库的 Option<T> 也是通过 match 处理的常见对象:

fun unwrap_or_zero(value: Option<i32>) -> i32 {
    match value {
        Some(n) => n,
        None => 0,
    }
}

Some / None 由 prelude 重导出,不需要显式导入。match 是表达式,每个 arm 必须产生兼容类型。

模式系统、let 解构与穷尽性检查的完整规则见枚举、模式与 match

所有权与内存

Riddle 把“谁拥有值”“谁暂时借用值”和“值存放在哪里”分成三件事:

  • 所有权决定值何时被移动,以及 Drop 何时运行;
  • 借用检查约束 &T&mut T 能否同时存在;
  • 逃逸分析决定值保留在栈上,还是提升到保守式非移动 GC 堆。

堆分配不会放宽所有权规则。一个值即使被提升到 GC 堆,移动后仍不能继续使用,借用冲突仍会报错,实现了 Drop 时仍按所有者结束的位置析构。

先阅读移动语义,理解赋值、传参、模式绑定与 Copy;再阅读引用与逃逸,理解借用、引用来源和自动堆提升。

matchfor 的非 Copy 模式绑定会取得对应值的所有权。未继续移动的绑定在 arm 或当前循环轮次结束时析构;breakcontinuereturn 也会先清理正在离开的绑定。实现了 Drop 的类型不能在 match / for 模式中解构移出非 Copy 字段,这种代码会报告 E0305let 解构当前不在此列)。

移动语义

Riddle 中,值默认通过移动传递。 移动意味着一个值从一个绑定转移到另一个绑定后,原来的绑定不再拥有这个值。

赋值会移动

看一个结构体例子:

struct Foo {
    x: i32,
    y: i32,
}

fun main() {
    let a = Foo { x: 1, y: 1 };
    let b = a;
    print!("{}", a); // error: a 已经被移动
    print!("{}", b);
}

let b = a; 之后,Foo 的值移动到了 ba 不再可用。

传参会移动

把值传给函数也会移动:

fun consume(foo: Foo) {
    print!("{}", foo.x)
}

fun main() {
    let foo = Foo { x: 1, y: 1 };
    consume(foo);
    print!("{}", foo); // error: foo 已经被移动
}

这种规则能避免“一个值到底由谁负责”的问题。

为什么只有移动

很多语言同时存在复制、共享引用、隐式别名和可变状态。 这些能力都很方便,但组合在一起时,程序行为会变得难以推理。

Riddle 选择让值默认移动,是为了让资源流向更明显:

  • 看到赋值,就知道所有权发生转移;
  • 看到函数调用,就知道参数被交给函数;
  • 需要共享时,显式使用引用;
  • 引用逃逸时,由语言自动提升到 GC。

需要继续使用值时怎么办

如果只是临时查看一个值,可以借用它:

fun inspect(foo: &Foo) {
    print!("{}", foo.x)
}

fun main() {
    let foo = Foo { x: 1, y: 1 };
    inspect(&foo);
    print!("{}", foo); // 可以继续使用
}

引用没有逃逸时,foo 仍然保留在当前作用域中。

Copy 类型

有些类型不会在赋值和传参时移动,而是复制。标量、共享引用、原始指针和命名函数项属于内置 Copy 候选。闭包值拥有环境和析构函数,因此按值传递时会移动。

用户类型可以通过实现 std 中的 lang Copy 进入复制语义:

struct Point {
    x: i32,
    y: i32,
}

impl std::marker::Copy for Point {}

fun main() {
    let p = Point { x: 1, y: 2 };
    let q = p;
    let r = p; // OK:Point 实现了 Copy
}

std/lib.rid 已经提供 std::marker::Copy,普通程序不需要自己声明这个 trait。只有带 #[lang = "copy"] 的 Copy trait 会被 move checker 识别。

解引用不会改变按值使用的规则。let value = *reference 会读取 reference 指向的 TT: Copy 时得到副本;否则因为引用不拥有 T,从解引用位置搬出值会报 E0308。如果要修改原值,应保留 &mut T,例如 let mut point = f(&mut p); point.x = 1;let value = *point 则不是引用别名。

引用模式与自动借用

显式 &pattern / &mut pattern 与显式解引用相同:它们读取引用指向的值,内部按值绑定只允许取得 Copy 内容。模式不会移动引用本身,临时借用会在没有绑定继续持有它时结束:

fun example() -> i32 {
    let mut original = 3;
    let (&mut copied, plain) = (&mut original, 4);
    original = 5; // OK:copied 是副本,临时借用已经结束
    copied + plain + original
}

结构化模式自动解引用 &T / &mut T 时则会创建字段重借用。绑定会保持对原位置的共享或可变借用,直到所有相关绑定的最后一次使用;可变重借用存活期间,父 &mut 会被冻结。不同字段的重借用仍按位置分别追踪。

模式绑定也会移动

matchfor 中的非 Copy 绑定会接管匹配值的所有权:

match value {
    Some(resource) => {
        consume(resource);
    }
}

resource 没有被继续移动时会在当前 arm 结束时析构;传给 consume 后,arm 不会再次析构它。结构体模式只移动实际绑定的非 Copy 字段,未绑定字段和按 Copy 取得的字段仍可在 match 后使用;整个原值因为处于部分移动状态而不能再作为整体使用。for 的当前元素遵循同一规则,未取出的元素继续由迭代器持有。为了保证用户析构函数始终能看到完整的 self,在 matchfor 的模式中,实现了 Drop 的类型不能通过解构模式移出非 Copy 字段,包括嵌套在普通聚合类型中的 Drop 值;这条限制当前不检查 let 解构。

引用与逃逸

引用让你在不移动值的情况下访问它。Riddle 会用逃逸分析决定局部值留在栈上,还是因为引用可能活得更久而提升到 GC 堆。

临时引用

临时引用最常见的用途是把值借给函数使用:

struct Foo {
    x: i32,
    y: i32,
}

fun read(foo: &Foo) -> i32 {
    foo.x
}

fun main() -> i32 {
    let foo = Foo { x: 1, y: 2 };
    let x = read(&foo);
    foo.y + x
}

&foo 只是临时借用。只要引用没有逃出当前需要的范围,foo 可以继续按普通局部值处理。

共享引用和可变引用

&T 是共享引用,&mut T 是可变引用:

let value = 1;
let r: &i32 = &value;

let mut other = 2;
let m: &mut i32 = &mut other;

当前 move checker 会检查这些基本冲突:

  • 已有共享借用时不能再创建可变借用;
  • 已有可变借用时不能再创建共享借用;
  • 不能同时创建两个可变借用;
  • 借用期间不能移动或赋值同一位置。

这些规则按重叠内存位置计算,而不是统计函数里声明了多少个引用。不同结构体字段可以分别拥有可变借用;整个值、同一字段以及无法证明不同的动态索引则视为重叠。

&T 是可复制的共享访问权,复制后两个引用都可以继续读取。&mut T 不是 Copy:普通赋值会移动独占访问权,原引用不能再使用。把 &mut T 传给期望引用的函数时,编译器会创建只覆盖调用的短期重借用,因此同一个可变引用可以重复传递。

let mut value = 1;
let reference = &mut value;
update(reference);
update(reference);

共享重借用会暂时冻结父可变引用的写权限;子借用最后一次使用后,父引用恢复可用。

解引用 *expr

前缀 * 是解引用运算符。它把引用或原始指针指向的存储位置重新作为一个位置表达式使用:

let value = 1;
let shared: &i32 = &value;
let read = *shared;

let mut other = 2;
let mutable: &mut i32 = &mut other;
*mutable = 3;

*shared 的类型是 i32,但它不是一个新的引用,也不会自动生成别名。它在不同上下文中的含义不同:

  • let read = *shared 是按值读取;如果 T: Copy,读取一个副本;如果安全引用指向的 T 不是 Copy,不能从借用内容中搬出值,会报 E0308
  • *mutable = value 是通过可变引用写入原位置,不是把引用改成一个值;
  • &*mutable&mut *mutable 分别创建共享重借用和可变重借用;
  • 结构体字段和索引访问会自动进行同样的解引用,例如 mutable.field 访问的是 (*mutable).field 所在的存储位置。

原始指针的 *ptr 还必须位于 unsafe 上下文中,并且不参与普通引用的借用来源跟踪。要访问原值,应直接绑定引用,例如 let mut point = f(&mut p); point.x = 1;。绑定另一个变量会移动 &mut T 的独占访问权,原引用不能继续使用;它不会创建第二个可变引用。要从安全引用按值取得一个 Point,当前需要让 Point 实现 Copy

当前 E0308 覆盖显式 *reference 在按值绑定、传参、返回和聚合构造中的消费,以它为根的字段、索引位置,以及自动字段或索引解引用(例如 reference.fieldreference[0])的非 Copy 搬出。移动式模式绑定从引用搬出的检查仍待补齐;完整收紧前需要先为拥有型数组迭代提供类似 ManuallyDrop/ptr::read 的内部原语。

方法返回值的引用来源

方法返回的引用会保留 receiver 的来源关系,即使引用被包装进泛型容器,再通过另一个方法取出:

let mut values = Vector::new();
values.push(1);
let mut fallback = 0;
let reference = values.get_mut(0).unwrap_or(&mut fallback);
values.push(2); // E0302: reference 仍借用了 values
*reference = 3;

编译器会让引用来源穿过结构体、枚举、数组、分支、模式绑定和可解析的函数调用。元组和数组构造会按元素保留来源,解构后每个绑定只继承对应元素;如果调用或控制流使聚合形状无法恢复,则退回整体合并的保守规则。无法解析的外部函数或函数值采用保守规则:返回值可能来自所有携带引用的输入。借用通常在引用最后一次使用后结束,而不是机械地持续到整个代码块末尾。

逃逸到堆上只会改变存储位置,不会放宽移动或借用规则;共享借用、可变借用和移动冲突仍按相同方式检查。

触发逃逸

当局部变量的引用作为函数结果返回时,变量会逃逸:

fun make_ref() -> &Foo {
    let foo = Foo { x: 1, y: 2 };
    &foo
}

foo 不能只存在于 make_ref 的栈帧里,因此 MIR 降级会为它生成 GC 堆分配。

包含引用的结构体或数组如果逃逸,里面引用到的局部值也会一起被标记:

struct Pair {
    value: &Foo,
}

fun build() -> Pair {
    let foo = Foo { x: 1, y: 2 };
    Pair { value: &foo }
}

字段访问和数组索引也会传播引用来源:

fun pick() -> &Foo {
    let items = [
        Foo { x: 1, y: 2 },
        Foo { x: 3, y: 4 },
    ];
    &items[0]
}

结构体、元组和数组会保留字段级引用来源。模式绑定只引用某个字段时,MIR 可以只为该绑定建立稳定存储;整个聚合值、动态索引或解引用无法静态区分位置时,才会提升更大的存储槽。

匿名函数按共享或可变引用捕获局部变量时,该局部需要稳定地址。逃逸分析会继续跟踪闭包值:只在当前函数内调用的闭包,其环境和捕获存储都留在栈上;闭包被返回、存入逃逸聚合值或传给可能保存它的函数时,环境及必要的捕获来源才提升到 GC 堆。按值捕获会把值直接放入闭包环境。

函数调用中的逃逸

把引用传给未知函数或外部函数时,编译器会保守地认为该引用可能逃逸:

unsafe extern "C" {
    fun store(value: &Foo);
}

fun caller() {
    let foo = Foo { x: 1, y: 2 };
    unsafe { store(&foo); } // 保守处理:foo 可能逃逸
}

对当前编译单元内能解析到的函数,逃逸分析会做参数摘要的不动点计算,并区分两种结果:参数被保存到未知位置,以及参数的引用来源只流入返回值。前者会立即触发调用方的堆提升;后者会附着到调用表达式,由调用者是否继续返回或保存该结果决定。本地闭包返回捕获引用时也会保留这条来源关系。仅在调用者内部读取返回引用时,来源仍可留在栈上。

fun read(foo: &Foo) -> i32 {
    foo.x
}

fun caller() -> i32 {
    let foo = Foo { x: 1, y: 2 };
    read(&foo) // read 不保存也不返回这个引用
}

分支、循环和 match

逃逸会从 ifwhilematch 的子表达式向外传播。只要某条路径产生了逃逸引用,对应局部就会被标记:

fun maybe(flag: bool) -> &Foo {
    let a = Foo { x: 1, y: 2 };
    let b = Foo { x: 3, y: 4 };

    if flag {
        &a
    } else {
        &b
    }
}

分配结果

逃逸分析结果进入 MIR 降级:

结果MIR 分配
未逃逸且不需要稳定地址的局部SSA 值,不生成分配指令
未逃逸但可变或被闭包按引用捕获的局部Alloca,栈上存储
逃逸局部HeapAlloc,GC 堆存储
未逃逸 / 逃逸的闭包环境Alloca / HeapAlloc

C backend 会把 HeapAlloc 降为运行时 ABI 的 rgc_allocclue build 默认链接内置 GC,也可以按 Clue.toml[runtime].source 链接自定义 GC 或分配器;直接编译 riddlec 生成的 C 时需要同时提供一个运行时实现。

二进制包设置 [runtime] gc = false 后,C backend 不再生成 rgc_* 调用,运行时也不包含收集器或根扫描。拥有所有权的堆值改由 riddle_alloc / riddle_free 管理;原本只能依靠 GC 延长栈对象寿命的引用逃逸会被 E0310 拒绝,输入引用的直接转发不受影响。

结构体

结构体用于把相关数据组合成一个命名类型。 当多个值总是一起出现时,把它们放进结构体通常会让代码更清晰。

定义结构体

使用 struct 定义结构体:

struct Foo {
    x: i32,
    y: i32,
}

Foo 有两个字段:xy。字段名后面写类型。

字段默认私有。需要让声明模块之外的代码构造、读取或解构字段时,在字段前添加 pub

pub struct Point {
    pub x: i32,
    pub y: i32,
}

私有字段仍可在声明模块及其子模块中使用,通常通过公开的构造函数和方法对外提供受控访问。

创建结构体值

可以使用结构体字面量创建值:

fun main() {
    let foo = Foo { x: 1, y: 1 };
    print!("{}", foo.x)
}

字段访问使用点号:

foo.x
foo.y

当局部变量名和字段名相同时,可以使用字段简写:

fun make_foo(x: i32, y: i32) -> Foo {
    Foo { x, y }
}

结构体字面量会检查字段是否可见、是否存在、是否缺失以及字段类型是否匹配。

泛型结构体

结构体可以带类型参数和 const 参数,规则见泛型一章。

关联函数

可以在 impl 块中给结构体定义关联函数,并通过 Type::function(...) 调用:

impl Foo {
    fun new(x: i32, y: i32) -> Foo {
        Foo { x, y }
    }
}

fun main() {
    let foo = Foo::new(1, 2);
}

结构体值会被移动

结构体也是普通值,因此遵循移动语义:

fun take(foo: Foo) {
    print!("{}", foo.x)
}

fun main() {
    let foo = Foo { x: 1, y: 1 };
    take(foo);
    print!("{}", foo); // error: foo 已经被移动
}

如果你只是想临时使用它,可以传引用:

fun inspect(foo: &Foo) {
    print!("{}", foo.x)
}

fun main() {
    let foo = Foo { x: 1, y: 1 };
    inspect(&foo);
    print!("{}", foo)
}

只要引用没有逃逸当前作用域,foo 仍然可以保持栈分配。

枚举、模式与 match

结构体表示“这些字段同时存在”,枚举表示“这些形状只会出现一种”。模式负责拆开其中的数据,match 根据形状选择分支。match 的基础用法在控制流已经介绍,本章完整讲解枚举定义、模式系统和穷尽性检查。

定义枚举

变体可以没有数据,也可以携带元组或具名字段:

enum Message {
    Quit,
    Move(i32, i32),
    Write { text: &str },
}

枚举可以带类型参数和 where 约束:

enum Slot<T> {
    Empty,
    Value(T),
}

标准库的 Option<T>Result<T, E> 也是普通泛型枚举,并由 prelude 重导出其常用变体。

构造与匹配变体

使用枚举路径构造值,再用相同形状的模式取出 payload:

fun describe(message: Message) -> i32 {
    match message {
        Message::Quit => 0,
        Message::Move(x, y) => x + y,
        Message::Write { text } => text.len() as i32,
    }
}

match 是表达式,每个 arm 必须产生兼容类型。编译器会检查枚举、布尔、unit、整数、元组和结构体模式是否穷尽;缺少分支会报告 E0039

常用模式

当前模式包括:

  • _ 通配符;
  • 标识符绑定与 mut 绑定;
  • 字面量和路径;
  • 元组、结构体与枚举变体;
  • &pattern&mut pattern 引用模式。

结构体模式可以只列出需要的字段:

struct Point { x: i32, y: i32 }

fun x_of(point: Point) -> i32 {
    let Point { x, y: _ } = point;
    x
}

普通 let 没有失败分支,所以模式必须覆盖该类型的每个值。枚举变体和字面量通常是可反驳模式,应放在 match 中;直接写在普通 let 中会报告 E0057。需要在失败时离开当前控制流时,可以使用 let-else

let Some(value) = option else {
    return;
};

let-else 的失败分支必须发散,成功后的绑定会留在外层作用域。元组和结构体模式是不可反驳的,可以直接解构:

let (a, b) = pair;             // OK
let Point { x, y } = point;    // OK

穷尽性检查

编译器使用模式矩阵检查 match 是否穷尽。检查会递归展开枚举 payload、元组和结构体字段,并识别 bool() 与整数值域。缺少分支时会报告 E0039 和一个可覆盖的示例模式:

enum State { Ready, Done(i32) }

fun value(state: State) -> i32 {
    match state {
        State::Ready => 0,
        State::Done(1) => 1,
        // E0039: missing pattern `State::Done(_)`
    }
}

整数匹配还会在诊断注记中列出未覆盖的连续区间。例如,只匹配 u802 时,注记会指出 13..=255 尚未覆盖。区间目前只用于诊断展示,不是可写在模式中的区间语法。

带 guard 的 arm 不计入穷尽性,因为 guard 可能在运行时为 false。浮点数、字符和字符串也需要用 _ 或标识符绑定覆盖其余值。

未限定的标识符模式通常绑定并匹配任意值;唯一的例外是与期望枚举类型的 unit 变体同名时,它按该变体的构造器模式处理(因此 match option { None => 0 } 会因缺少 Some 而报穷尽性错误,而不是绑定名为 None 的任意值)。除此之外,下面的 other 不是常量,而是覆盖除前面 arm 之外所有剩余 u8 值的绑定:

fun unsigned(value: u8) -> i32 {
    match value {
        0 => 0,
        other => 1,
    }
}

guard 与绑定

arm 可以在模式后加条件:

fun classify(value: Option<i32>) -> i32 {
    match value {
        Some(number) if number < 0 => -1,
        Some(0) => 0,
        Some(_) => 1,
        None => 2,
    }
}

guard 失败后继续检查后续 arm。guard 只能查看或借用非 Copy 模式绑定,不能取得其所有权(E0307);把移动操作放到选中的 arm body 中。模式绑定的移动与析构规则见移动语义

引用模式与匹配人体工学

显式引用模式会解构恰好一层、且可变性必须相同的引用:

fun read(reference: &mut i32) -> i32 {
    let &mut copied = reference;
    copied
}

fun mixed(mut value: i32) -> i32 {
    let (&mut copied, plain) = (&mut value, 4);
    copied + plain
}

显式模式内的绑定按值取得内容。上例的 copiedi32 副本,不是 &mut i32;若内容不是 Copy,会报告 E0308&pattern 不能匹配 &mut T&mut pattern 也不能匹配 &T

元组、结构体、枚举和字面量等非引用模式遇到引用输入时会自动逐层解引用,并继承默认绑定模式:

struct Pair { left: i32, right: i32 }

fun update(pair: &mut Pair) {
    let Pair { left, right } = pair;
    *left = 10;   // left: &mut i32
    *right = 20;  // right: &mut i32
}

经过共享引用时,内部绑定最终都是共享引用;只经过可变引用时则得到可变引用。裸标识符模式不会自动解引用,因此 let whole = pair; 仍让 whole 取得整个引用值。Riddle 没有 ref / ref mut 语法。

结构化模式自动解引用后,如果默认绑定模式已经变为引用,内部不能再写 mut binding 或显式 &pattern / &mut pattern。需要显式引用模式时,应让它出现在默认 move 模式的位置。

错误处理

Riddle 区分“值可能不存在”“操作可能失败”和“程序无法继续”三种情况,分别使用 Option<T>Result<T, E>panic!

Option 表示可能没有值

Option<T>Some(T)None 两个变体。标准库的 parse_i32 用它表示十进制文本是否能解析成整数:

use std::parse::parse_i32;

fun read_or_zero(text: &str) -> i32 {
    match parse_i32(text) {
        Some(value) => value,
        None => 0,
    }
}

只需要一个后备值时,可以使用 unwrap_or

let value = parse_i32("42").unwrap_or(0);

Option 当前提供 is_someis_noneunwrapexpectunwrap_orunwrap_or_elsemapmap_orand_thenandoror_else

Riddle 当前没有 Kotlin 式 T?null。普通缺失值应建模为 Option

Result 表示成功或失败

Result<T, E>Ok(T) 携带成功值,Err(E) 携带错误:

use std::parse::parse_i32;

fun parse_positive(text: &str) -> Result<i32, &str> {
    match parse_i32(text) {
        Some(value) => if value < 0 {
            Err("expected a non-negative integer")
        } else {
            Ok(value)
        },
        None => Err("not an integer"),
    }
}

Result 提供 is_okis_errunwrapexpectunwrap_orunwrap_or_elsemapmap_errmap_orand_thenandokerr。需要保留错误内容或执行不同恢复逻辑时,优先使用 match,不要立即丢弃 Err

Riddle 当前的 return 是语句,不能像 Rust 那样直接写成 None => return Err(...);让整个 match 产生 Result 即可。

使用问号传播错误

后缀 ? 会在 Ok 时取出成功值,在 Err 时提前返回:

fun double_positive(text: &str) -> Result<i32, &str> {
    let value = parse_positive(text)?;
    Ok(value * 2)
}

? 可以用于 Result<T, E>Option<T>,所在函数也必须返回同一种类型。错误类型相同时会直接传播;不同时,编译器要求操作数的错误类型通过 Into 转换为外层错误类型,没有匹配的 Into impl 时会回退尝试 From。相关诊断是 E0061E0062E0063

panic 用于不可恢复路径

panic!(...) 返回 never 类型 !,因此可以出现在需要任意结果类型的分支,并支持与 format! 相同的编译期格式串检查:

fun require(valid: bool) -> i32 {
    if valid { 42 } else { panic!("invalid state: {}", valid) }
}

运行时会向标准错误输出线程名、源文件、行列和格式化消息,然后调用 C abort();当前没有栈展开、panic hook 或恢复机制。输入错误、文件错误或其他预期失败应使用 OptionResult,不要用 panic! 代替普通错误处理。

断言和不可达路径

assert! 检查布尔条件,assert_eq! / assert_ne! 会把左右表达式各求值一次,并在失败消息中通过 Debug 输出两侧值。三个宏都支持自定义格式化消息:

assert!(length > 0, "length must be positive: {}", length);
assert_eq!(actual, expected);
assert_ne!(state, State::Stopped, "worker must still be active");

debug_assert!debug_assert_eq!debug_assert_ne! 使用相同语义。Riddle 当前没有 debug_assertions 构建配置,因此它们在所有构建中都会执行。

尚未实现的分支可以使用 todo!()unimplemented!();静态上应当不可达的分支可以使用 unreachable!()。它们都返回 !,支持可选的格式化消息,并复用 panic! 的源位置和 abort 诊断。

泛型

泛型让同一份逻辑适用于多种类型。Riddle 的泛型通过静态单态化编译:每个用到的具体类型组合都会在编译期生成独立的实现,泛型本身在运行时没有类型信息;动态分派只存在于显式使用 dyn Trait 对象的场合(见Trait)。

本章统一介绍函数、类型和 impl 中的泛型写法;trait bound 的完整规则见Trait

泛型函数

函数可以带类型参数:

fun id<T>(value: T) -> T {
    value
}

fun main() {
    let n = id(1);       // T 推断为 i32
    let b = id(true);    // T 推断为 bool
}

多个类型参数用逗号分隔:

fun pair<A, B>(first: A, second: B) -> (A, B) {
    (first, second)
}

调用泛型函数或方法时,编译器会联合全部实参与调用位置的期望返回类型推断类型参数,实参的书写顺序不影响推导;也可以在函数名后用 Rust 风格的 ::<...> 显式指定:

fun main() {
    let n = id::<i32>(1);   // 显式指定 T = i32
    let b = id::<bool>(true);
}

方法的显式类型实参使用相同语法,例如 value.convert::<Target>()。调用泛型类型上的关联函数时,类型实参写在类型路径段上:

let values = Vector::<i32>::new();
let converted = Wrapper::<i32>::convert::<bool>();

这里第一组参数选择 impl<T>T,末尾一组参数选择关联函数自己的泛型参数。类型标注中仍然写作 Vector<T>,不需要 ::

const 泛型

函数、结构体和 impl 都可以带 const 参数。当前最常见的用法是把数组长度作为编译期参数:

fun len<const N: usize>(values: [i32; N]) -> i32 {
    0
}

fun main() {
    let n = len([1, 2, 3]); // N 推断为 3
}

const 参数声明自己的整数类型,并可以像类型参数一样实例化:

struct Buffer<T, const N: usize> {
    data: [T; N],
}

let buffer: Buffer<i32, 3> = Buffer { data: [1, 2, 3] };

泛型类型

结构体和枚举都可以带类型参数:

struct Box<T> {
    value: T,
}

enum Slot<T> {
    Empty,
    Value(T),
}

嵌套泛型不需要在 > 之间插入空格:

let value: Slot<Pair<i32, bool>> = Slot::Value(Pair {
    first: 1,
    second: true,
});

结构体字面量可以用 ::<> 显式指定类型参数,在无法从上下文推断时很有用:

let b = Box::<i32> { value: 1 };

泛型 impl

固有 impl 可以带类型参数和 const 参数:

impl<T: Copy> Box<T> {
    fun get(self) -> T {
        self.value
    }
}

impl<T, const N: usize> ArrayIter<T, N> {
    fun len(&self) -> usize {
        N
    }
}

约束与单态化

类型参数可以带 trait bound:函数、trait、impl、结构体和枚举的泛型位置都可以直接写 <T: Trait>,多个 bound 用 + 连接,也可以写成 where 子句;bound 还可以约束关联类型,例如 <T: std::ops::Add<Output = T>>。完整规则见Traitimpl 块

编译器会检查推断或显式给出的实参类型是否满足 bound;函数体内可以通过 bound 调用 trait 方法。C backend 会为用到的泛型函数和方法按类型组合生成单态化函数,并静态分派到具体 impl,不会生成动态分派。

Trait

Trait 是 Riddle 中定义共享行为的机制。它类似于其他语言中的接口(interface),用于声明一组方法和关联类型,供具体类型来实现。

定义 Trait

使用 trait 关键字定义一个 trait:

trait Summary {
    fun summarize() -> &str;
}

Summary trait 声明了一个必需方法 summarize,它不接受参数并返回一个 &str。没有函数体的方法必须由具体类型在 impl 块中提供。

Trait 方法也可以提供默认实现。impl 未覆写时使用默认体,显式覆写优先;默认体可以调用同一 trait 的其他方法:

trait Summary {
    fun title(&self) -> &str;

    fun summarize(&self) -> &str {
        self.title()
    }
}

Trait 可以声明一个或多个父 trait:

trait Named {
    fun name(&self) -> i32;
}

trait Tagged: Named {
    fun tag(&self) -> i32;
}

T: Tagged 会同时满足 T: Named,因此泛型代码可以调用 name。为类型实现 Tagged 前,必须显式实现 Named;多级父 trait 会传递生效。未知父 trait 和继承环会在类型检查时报错,多个父 trait 使用 + 分隔。

为类型实现 Trait

impl 块中为某个具体类型实现 trait:

struct Article {
    title: &str,
    body: &str,
}

impl Summary for Article {
    fun summarize() -> &str {
        "article"
    }
}

借用 Trait Object

对象安全的 trait 可以通过借用 trait object 传递运行时类型:

trait Speak {
    fun speak(&self) -> i32;
}

struct Speaker { value: i32 }

impl Speak for Speaker {
    fun speak(&self) -> i32 { self.value }
}

fun call(value: &dyn Speak) -> i32 {
    value.speak()
}

&dyn Trait&mut dyn Trait 是借用视图,在 MIR 中包含数据指针和方法表,调用通过方法表间接分派。父 trait 的对象安全方法也会递归加入方法表:

trait Loud: Speak {
    fun volume(&self) -> i32;
}

fun inspect(value: &dyn Loud) -> i32 {
    value.speak() + value.volume()
}

当前动态对象要求方法非泛型并使用引用接收者;带泛型方法的 trait 不能用于动态调用。

拥有 Trait Object

裸的 dyn Trait 表示拥有所有权的、大小固定的动态值。把具体实现转换为它时,MIR 会把实现值放入堆存储,并保存数据指针、对象安全方法表和类型专属的 drop 槽位:

fun make() -> dyn Speak {
    Speaker { value: 7 }
}

fun call_owned(value: dyn Speak) -> i32 {
    value.speak()
}

启用 GC 时,堆存储使用 rgc_alloc / rgc_free[runtime] gc = false 时改用 riddle_alloc / riddle_free,因此不需要额外的 Box<T> 语法。拥有值离开作用域时通过 drop 槽位释放具体实现;拥有对象用普通 & 即可重借用为 &dyn Trait,并可向上转型到父 trait。数组 expected type 会逐元素构造拥有对象,带 trait bound 的泛型参数也可转换为拥有对象。跨父 trait 的同名方法会报告歧义;dyn Fndyn FnMutdyn FnOnce 也可作为拥有或借用的 callable 值,并复用 { call, env, drop } ABI;带泛型方法的动态对象仍未支持。

关联类型

Trait 可以包含关联类型,让实现者指定 trait 方法中用到的具体类型:

trait Iterator {
    type Item;
    fun next(&mut self) -> Option<Self::Item>;
}

trait IntoIterator {
    type Item;
    type IntoIter;
    fun into_iter(self) -> Self::IntoIter;
}

在实现时,需要为关联类型指定具体类型:

impl Iterator for Counter {
    type Item = i32;

    fun next(&mut self) -> Option<Self::Item> {
        // ...
    }
}

IteratorIntoIteratorfor item in value 使用的协议。标准库把 Option<T> 放在 std::option、把 Result<T, E> 放在 std::result、把 Rangerange(start, end) 放在 std::ops。与 Rust 一样,prelude 会重导出 SomeNoneOkErrCopyClone 和比较 trait,但不会自动导入 Rangerange。固定长度数组 [T; N] 也已经有 IntoIterator 实现,数组迭代器定义为 std::array::IntoIter<T>,因此 [1, 2, 3] 会匹配 impl<T, const N: usize> IntoIterator for [T; N],按值产出元素且不要求元素类型是 Copy。在 for 中使用这些类型的完整示例见闭包与迭代器

泛型约束

函数、trait、impl、结构体和枚举都可以通过 bound 要求类型实现某个 trait:

fun read<T: Named>(value: T) -> i32 {
    value.name()
}

fun combine<T: Named + Tagged>(value: T) -> i32 {
    value.name() + value.tag()
}

bound 可以约束关联类型:

fun add_box<T: std::ops::Add<Output = T>>(left: T, right: T) -> T {
    left + right
}

也可以使用 where 子句:

impl<T> Wrap for Box<T>
where T: Marker
{}

impl 上的 where 约束会检查 Paterson condition:约束必须严格小于被实现的类型,避免递归 trait 求解无限增长。

Trait impl 还遵循孤儿规则:当前包可以自由实现自己定义的 trait;实现依赖包或标准库的 trait 时,Self 或 trait 类型参数中必须有当前包定义的结构体或枚举,并且第一个本地类型之前不能出现未被类型构造器覆盖的泛型参数。引用会传递本地性但不会覆盖其中的泛型参数,类型别名则按其底层类型判断。违反规则会报告 E0048

#[fundamental] 是编译器内部属性。默认加载标准库时,只有随编译器附加的标准库可以使用它,用户包中使用会报告 E0049。使用 --no-std 时不附加内置标准库,所有参与本次编译的包都可以定义 #[fundamental] 类型,以支持自定义 core 和基础类型体系。

该属性会让被标注的结构体或枚举在孤儿规则判定中变得透明——只要它的某个类型参数是本地类型,整体就视为本地,等价于内置的 &T。标准库或自定义 core 的智能指针类型可借此让 impl ForeignTrait for FundBox<LocalType> 这样的写法合法:

// 默认 std 模式下该定义属于标准库;--no-std 模式下也可由自定义 core 定义
#[fundamental]
struct FundBox<T> { value: T }

// FundBox 透明,FundBox<Local> 视为本地类型
impl ForeignTrait for FundBox<Local> {}

标准库比较 trait 也使用同一套父 trait 关系:Eq: PartialEqPartialOrd: PartialEqOrd: Eq + PartialOrd

内置 Trait

Riddle 的 std/lib.rid 会自动拼到用户源码后面。标准库中用 Rust 风格属性 #[lang = "..."] 标记编译器需要识别的特殊 trait。

Copy

Copy 是一个标记 trait——它不包含任何方法。当一个类型实现 Copy 时,编译器在赋值和传参时会自动进行按位复制,而非移动所有权:

#[lang = "copy"]
trait Copy {
}

基础类型(i32boolf64 等)在 std 中实现了 std::marker::Copy。用户类型通常直接使用标准派生:

实现了 Copy 的类型在赋值后原变量仍然可用:

#[derive(Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fun main() {
    let p = Point { x: 1, y: 2 };
    let q = p;    // 复制而非移动
    print!("{}", p.x);   // OK:p 仍然可用
}

泛型 impl 也可以作为 Copy 匹配模式:

struct Box<T> {
    value: T,
}

impl<T: Copy> Copy for Box<T> {}

fun main() {
    let a: Box<i32> = Box { value: 1 };
    let b = a;
    let c = a; // OK:i32: Copy,因此 Box<i32>: Copy
}

只有被 #[lang = "copy"] 标记的 trait 会触发 move checker 的复制语义;普通同名或未标记 trait 不会自动生效。

与 Rust 一样,标准库的 Option<T>Result<T, E> 使用带 bound 的条件 Copy 实现。编译器会检查用户 Copy impl 的每个结构体字段和枚举 payload;泛型字段必须能由 impl bound 证明为 Copy,否则报告 E0041&mut T 也不会被视为内建 Copy 类型。

其他 std lang trait

当前 std 定义并实现了这些 lang trait:

  • Clone,提供可调用的 clone
  • PartialEqEqPartialOrdOrd
  • AddSubMulDivRemNegNot、位运算、移位和复合赋值 trait,均提供对应必需方法。

运算 trait 的 lang 标记同时是 std 和编译器之间的内建契约。例如,std/std/ops.rid 中包含:

#[lang = "add"]
trait Add<Rhs = Self> {
    type Output;
    fun add(self, rhs: Rhs) -> Self::Output;
}

普通代码直接调用 std 提供的标量 impl:

fun main() -> i32 {
    let left: i32 = 1;
    left.add(2)
}

对于 i32 等标量,left.add(2) 会直接降为 MIR Add,C backend 输出等价的 left + 2,不会生成或调用 add__i32 包装函数。一元运算、位运算、移位和 add_assign 等复合赋值方法遵循相同规则。只有带受支持 #[lang = "..."] 标记的 trait 的标量 impl 会开洞;未标记的同名 trait 和结构体等用户类型 impl 仍保留普通方法调用。

二元、复合赋值、PartialEqPartialOrd trait 都接受默认值为 SelfRhs 参数,因此可以为不同的右操作数类型分别实现 trait。泛型函数中的 T: Add<Rhs, Output = O> 运算会保留为 trait 调用,并在单态化后选择具体 impl。普通赋值和内建复合赋值先计算右侧,再计算左侧位置;重载复合赋值按方法调用顺序先计算左侧接收者,再计算右侧。

PartialEq::eqPartialOrd::partial_cmpOrd::cmp 可以作为普通方法调用;整数、字符和布尔值返回 Ordering,浮点比较遇到 NaN 时返回 None。当前编译器会为用户类型把算术、取余、位运算、移位、一元负号、逻辑非、复合赋值和比较运算分派到对应的 #[lang = "..."] trait 方法,并用 Output 关联类型决定非赋值算术运算的结果类型。== 调用 PartialEq::eq!= 调用默认的 PartialEq::ne<<=>>= 分别调用 PartialOrd::ltlegtge,这些默认方法通过 partial_cmp 判断,遇到 None 时均返回 false

标准库还提供普通 trait DefaultHashDisplayDebugDefault::default() 会根据期望类型静态选择 impl;Hash 用于哈希集合;Display / Debug 通过 Formatter 支持 print!println!#[derive(Debug)]。编译器内置 DebugCloneCopyDefaultHashPartialEqEqPartialOrdOrd 派生;完整规则见常用标准库

RemRemAssign 已为整数及 f32 / f64 实现。C backend 对浮点余数生成 fmod 调用。

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 TypeFnMutFnOnce 实现可调用能力;对应的 call receiver 必须依次为 &self&mut selfself。完整示例见闭包与迭代器

trait impl 可以使用 where 子句约束泛型参数:

trait Marker {}
trait Wrap {}

struct Box<T> {
    value: T,
}

impl<T> Wrap for Box<T>
where T: Marker
{}

为了避免 trait 求解无限递归,implwhere 约束必须满足 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.ridname/mod.rid,两者同时存在会报错。模块不会仅因为文件存在就自动加入编译,父模块必须写出对应的 mod 声明。

目录按层级推进。src/main.rid 中的 mod foo; 会读取 src/foo.ridsrc/foo/mod.rid;进入 foo 后,其中的 mod bar; 会继续读取 src/foo/bar.ridsrc/foo/bar/mod.rid

路径可以使用 selfsupercrate 和以 :: 开头的绝对形式。

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 和入口规则见创建与构建项目

集合

集合类型持有多个值并支持增删改查。Riddle 目前提供 VectorHashMapHashSetTreeMapTreeSet 五种可增长容器和一个可变长字符串,它们的缓冲区都在运行时分配,并随容器一起释放。

字符串切片的只读知识(str&str、字面量、raw string)在数据类型一章;本章集中介绍会增长、会修改的容器。

String

String 是可增长的 UTF-8 字符串,内部使用 Vector<u8> 持有字节:

let mut text = String::from_str("hello");
text.push_str(" world");

let length = text.len();          // 11usize
let capacity = text.capacity();

text.clear();
let empty = text.is_empty();      // true

text.push_str("hello again");
let view = text.as_str();         // "hello again"

String::new() 创建空字符串。as_str() 返回借用当前缓冲区的 &str;借用持续到视图绑定离开作用域,期间借用检查器会拒绝 push_strclear 等可变操作——即使视图之后再没有被读取。因此先完成所有修改、再创建视图,或把视图放进内层块,是最省事的顺序。

常用方法:

方法作用
new()创建空字符串
from_str(value)&str 复制内容
as_str()借用缓冲区为 &str
len() / capacity()字节长度 / 缓冲区容量
is_empty()是否为空
push_str(value)追加 &str
clear()清空内容

String&str 的取舍和 Rust 类似:&str 是只读视图,String 是拥有型可增长数据。len 返回 UTF-8 字节数而不是字符数;按字符遍历请使用 &strfor 循环(见数据类型)。

Vector

Vector<T> 是可增长顺序容器,元素类型为 T

fun sum() -> i32 {
    let mut values = Vector::new();
    values.push(10);
    values.push(20);

    let first = match values.get(0usize) {
        Some(value) => *value,
        None => 0,
    };

    first + values.pop().unwrap_or(0)
}

vec![] 宏提供字面量式的构造。元素列表形式逐个 push,元素按值移动进向量;vec![elem; count] 重复形式要求元素实现 Clone,为每个槽位克隆一份;空 vec![] 的元素类型从绑定注解或后续用法推断:

fun demos() {
    let list = vec![1, 2, 3];
    let zeros = vec![0; 8usize];
    let names = vec![String::from_str("a"), String::from_str("b")];
    let empty: Vector<i32> = vec![];
}

常用方法:

方法作用
new()创建空向量
from_elem(value, count)创建含 countvalue 克隆的向量(要求 T: Clone),vec![value; count] 的底层实现
len() / capacity() / is_empty()长度、容量、是否为空
push(value) / pop()末尾追加 / 取出末尾元素
get(index) / get_mut(index)返回 Option<&T> / Option<&mut T>,越界返回 None
swap(a, b)交换两个位置
clear()清空所有元素
as_slice()借用全部元素为 &[T]

values[index] 下标访问越界时会调用 panic 并终止进程;需要可恢复的访问时使用 get。按值 for 会消耗向量并逐个产出元素(见闭包与迭代器)。

Map 与 Set

集合需要显式导入,不在 prelude 中:

use std::collections::{HashMap, HashSet, TreeMap, TreeSet};

fun main() {
    let mut counts: HashMap<i32, i32> = HashMap::new();
    counts.insert(1, 10);

    let mut ordered: TreeSet<i32> = TreeSet::new();
    ordered.insert(3);
    ordered.insert(1);

    println!("count={} ordered={}", counts.len(), ordered.len());
}
类型键的要求特点
HashMap<K, V>Hash + Eq开放寻址哈希表,平均 O(1) 访问
HashSet<T>Hash + Eq只有键的哈希集合
TreeMap<K, V>Ord红黑树,键有序
TreeSet<T>Ord只有键的有序集合

四个类型都提供 newinsertlenis_empty;映射类型另有 get / contains_key,集合类型另有 contains,当前没有 clear。键要求决定选择:需要排序时用 Tree*,否则优先 Hash*。当前类型名就是这四个完整名称,不提供 Map / Set 别名。

数组与切片

定长数组 [T; N] 和切片 [T] 不是集合——它们不管理运行时分配。数组在编译期确定长度,切片是对已有存储的借用视图,相关规则见数据类型切片与不定长类型

闭包与迭代器

函数式能力在 Riddle 中表现为两类:匿名函数(闭包)让行为可以按值传递,Iterator / IntoIterator 协议让遍历可以统一。这两者都依赖泛型与 trait,因此本章放在泛型、Trait 与模块之后阅读。

匿名函数(方括号 lambda)

匿名函数使用方括号形式 [参数 -> 体],可以保存到变量或作为参数传递。参数类型和返回类型按期望的可调用签名推断,无法推断时再显式标注参数类型。单参惯用名为 it(普通标识符,不是保留字,也可以换成任意名字):

fun apply(f: impl Fn(i32) -> i32, value: i32) -> i32 {
    f(value)
}

fun main() -> i32 {
    let inc = [it -> it + 1];
    let doubled = [it -> it * 2](21);
    apply(inc, 41) + doubled
}

无法从期望签名推断参数类型时需要显式标注,例如 [x: i32 -> x];返回类型始终推断,不能标注。每个匿名函数表达式都有独立的具体类型,即使两个表达式的参数和返回类型完全相同,它们也不会自动变成同一种类型。

多参数、类型标注、解构参数与按值捕获的写法:

let sum = [acc, v -> acc + v];
let flagged = [it: &i32 -> *it > 3];
let first = [(left, _) -> left];
let offset = move [it -> base + it];

零参 lambda 写作 [ -> 体](空的 [] 仍是空数组字面量)。多语句体用块表达式:[it -> { let sq = it * it; sq }]。匿名函数不能声明泛型参数或 where 子句,也不能标注返回类型;需要这些能力时定义具名函数。

判别规则:[ 组内嵌套深度 0 处出现 -> 即为匿名函数,否则是数组字面量,因此 [1, 2, 3][v] 仍是数组。后缀位置同样适用:expr [参数 -> 体] 表示以该 lambda 为实参调用 expr,最常见的用法是方法链:

let chained = Counter { index: 0usize, limit: 5usize }
    .map [v -> v + 1i32]
    .filter [it -> *it > 3i32];

匿名函数支持参数解构,并按用法推断捕获。需要泛型参数、bound、where 子句或递归调用自身时,定义具名函数(具名函数在每个调用点单态化,并且可以递归):

fun choose<T: Copy>(base: i32, value: T) -> i32 {
    if value == base { base } else { 0 }
}
let value = choose::<i32>(3, 9);

move [参数 -> 体] 会按值捕获所有使用到的外部位置,Copy 值仍然复制。move 只改变捕获所有权,不会单独把匿名函数变成 FnOnce;按值捕获后只读取的值仍可产生 Fn

捕获

匿名函数可以捕获外层局部变量和参数,捕获方式由函数体中的用法自动推断:

  • 只读取时按共享引用捕获;
  • 赋值或取得 &mut 时按可变引用捕获;
  • 把非 Copy 值交给按值参数、返回或存入其他值时按值捕获。

捕获按位置精确到静态字段或元组元素。读取 pair.left 不会同时捕获 pair.right;动态索引无法在编译期确定元素,因此会捕获索引基值;通过引用解引用时捕获的是完成访问所需的引用值。

fun count() -> i32 {
    let mut total = 0;
    let mut add = [value: i32 -> {
        total += value;
        total
    }];
    add(1);
    add(2)
}

捕获方式也决定调用能力:只共享读取环境的匿名函数是 Fn,需要修改环境的是 FnMut,调用时移出环境中非 Copy 值的是 FnOnceFn 同时满足 FnMutFnOnce 要求,FnMut 同时满足 FnOnce 要求。调用 FnMut 需要可变位置;作为参数时写成 mut f: impl FnMut(...) -> ...FnOnce 调用后不能再次使用。

Copy 值捕获会在创建闭包时移动该值;move [...] 按值捕获所有使用到的外部位置。按引用捕获的局部需要稳定地址,逃逸分析会决定闭包环境留在栈上还是提升到 GC 堆(见引用与逃逸)。

可调用参数与返回值

参数位置的一般 impl Trait 会引入隐藏泛型参数;返回位置的一般 impl Trait 会隐藏一个具体返回类型。可调用值使用 impl Fnimpl FnMutimpl FnOnce 携带调用签名并接收匿名函数、安全命名函数项或实现对应 callable trait 的用户类型:

fun call_twice(mut f: impl FnMut(i32) -> i32, value: i32) -> i32 {
    f(value);
    f(value)
}

mut 修饰参数绑定,因此也可以用于普通参数。每个 impl Fn* 参数引入独立的隐藏类型;需要让多个参数保持同一具体类型时,显式声明泛型参数:

fun combine<F>(first: F, second: F, value: i32) -> i32
where F: Fn(i32) -> i32
{
    first(value) + second(value)
}

返回位置的 impl Fn* 隐藏一个具体返回类型:

fun make_adder(base: i32) -> impl Fn(i32) -> i32 {
    move [value: i32 -> base + value]
}

所有返回路径必须产生同一个具体匿名函数或命名函数项类型。

用户类型可以实现 callable trait。Fncall 使用 &selfFnMut 使用 &mut selfFnOnce 使用 self,其余参数和返回类型必须与 impl 头中的签名一致:

struct Adder { amount: i32 }

impl Fn(i32) -> i32 for Adder {
    fun call(&self, value: i32) -> i32 {
        value + self.amount
    }
}

dyn Fn* 支持拥有值和借用值,二者共用 { call, env, drop } 的内部 callable ABI;拥有的 FnMut 值或不可变借用需要可变绑定,可变借用 &mut dyn FnMut 只需要引用本身可变,FnOnce 调用后会移动该值。不安全函数项不能传给安全的 Fn* 参数。

迭代协议

for item in value 依赖两个 trait: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;
}

标准库的 Range 位于 std::ops,并提供了 range(start, end) 构造半开区间 [start, end)。与 Rust 一样,prelude 不会自动导入 Rangerange,使用前需要显式导入:

use std::ops::range;

fun sum_to_three() -> i32 {
    let mut sum = 0;

    for item in range(0, 3) {
        sum += item;
    }

    sum
}

用户类型只要实现 IntoIterator,并让它的 IntoIter 实现 Iterator,也可以用于 for。MIR 降级会把这类循环降成 into_iternext 方法调用;泛型参数也可以通过 IntoIterator<Item = ..., IntoIter = ...> bound 使用 for,具体 impl 在单态化时解析。

内置可迭代值

标准库和语言为以下值直接提供了 IntoIterator 实现:

  • 固定长度数组 [T; N] 通过 std::array::IntoIter<T> 逐个按值产出元素,元素类型不需要实现 Copy
  • Vector<T> 按值产出当前保存的元素并消耗向量;
  • 共享切片 &[T]、可变切片 &mut [T] 产出元素引用;
  • &str 按 UTF-8 解码产出 Unicode char
fun use_array() -> i32 {
    let mut sum = 0;

    for item in [1, 2, 3] {
        sum += item;
    }

    sum
}

[T; N] 的迭代器定义为 std::array::IntoIter<T>,因此 [1, 2, 3] 匹配 impl<T, const N: usize> IntoIterator for [T; N]

break 与 continue

break 立即结束最近一层循环,continue 跳到最近一层循环的下一次迭代:

use std::ops::range;

fun first_three_odd_sum() -> i32 {
    let mut sum = 0;

    for value in range(0, 10) {
        if value == 6 {
            break;
        }
        if value % 2 == 0 {
            continue;
        }
        sum += value;
    }

    sum
}

当前只支持无值、无标签的 break;continue;,并且只能在 whilefor 循环体中使用。for 的当前元素、迭代器和提前退出路径具有独立的析构作用域:非 Copy 的模式绑定在元素离开当前迭代时析构,breakcontinuereturn 会先清理正在离开的绑定。

常用标准库

Riddle 会自动加载随编译器附带的标准库。prelude 提供日常使用频率最高的类型、变体和 trait;集合、解析、时间、格式化器与底层输出函数需要从对应模块显式导入。

集合(StringVectorHashMap 等)的用法在集合一章;迭代协议在闭包与迭代器。本页是 API 与行为的速查。

Prelude 中有什么

普通程序可以直接使用:

  • OptionResultSomeNoneOkErr
  • StringVector
  • CopyCloneDropDefaultIntodrop 和比较 trait;
  • 标准 DebugCloneCopyDefaultHashPartialEqEqPartialOrdOrd 派生;
  • IteratorIntoIterator 协议。

函数式标准宏不属于 prelude,也不需要导入。当前包括 format!panic!print!println!,断言宏 assert!assert_eq!assert_ne!debug_assert!debug_assert_eq!debug_assert_ne!,以及 todo!unimplemented!unreachable! 和向量字面量 vec!

同名并不表示与 Rust 标准库具有完整相同的 API。应以本页和当前工具链状态列出的实现为准。

格式化输出

{} 要求参数实现 Display{:?} 要求实现 Debug

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fun main() {
    let point = Point { x: 3, y: 4 };
    println!("point={:?}", point);
    println!("x={} y={}", point.x, point.y);
}

格式宏当前支持多个 {} / {:?}{0} 位置参数(可重复引用任意参数)、{name} 命名捕获(隐式读取调用处的同名局部变量)、尾随逗号以及 {{ / }};格式串的语法、说明符合法性与命名捕获的存在性会在编译期校验,但位置索引是否越界、实参数量是否足够,以及未实现的宽度、对齐和填充说明符不会在编译期拒绝——例如 {1}、参数不足或 {:>5} 会静默通过并在运行时输出空内容。

print! / println! 通过隐藏的标准库输出入口和 std::fmt::{Debug, Display, Formatter, Result} 支持字符串、布尔、字符、整数和浮点标量;Display 输出 UTF-8 字符,浮点数固定输出 6 位小数。字符串和字符的 Debug 输出会添加引号并转义 \\\n\r\t\0;字符串转义双引号 \",字符转义单引号 \'。格式化 trait 不在 prelude 中,底层输出入口不属于用户 API。

panic!() 使用消息 explicit panicpanic!("value={}", value) 与其他格式宏共享编译期格式串检查,并保留宏调用位置用于 panic 诊断。底层 std::panic 模块及其 panic(message) 入口仅供标准库和编译器使用,不会进入普通补全。

assert_eq! / assert_ne! 只求值左右表达式一次,失败时显示两侧的 Debug 值;所有断言宏都支持自定义格式化消息。todo!unimplemented!unreachable! 返回 ! 并产生对应的 panic 消息。Riddle 当前没有按构建配置关闭 debug assertion 的能力,因此 debug_assert!debug_assert_eq!debug_assert_ne! 始终执行。

标准派生

编译器内置 DebugCloneCopyDefaultHashPartialEqEqPartialOrdOrd 派生,可用于结构体和 unit、tuple、named 三类枚举变体。CloneDefaultHash 和比较派生按字段声明顺序工作;PartialEq 在枚举变体不同时返回 falsePartialOrd / Ord 先比较枚举变体声明顺序,再按 payload 做字典序比较,PartialOrd 会原样传播字段返回的 NoneCopyEq 生成标记 impl。泛型类型参数会自动获得相应 trait bound,例如 Wrapper<T>Clone impl 要求 T: Clone

结构体的 Default 会逐字段调用 Default::default()。枚举必须用 #[default] 标记恰好一个 unit 变体:

#[derive(Default, PartialEq, Eq, PartialOrd, Ord)]
enum State {
    #[default]
    Idle,
    Running(i32),
}

Copy 派生仍会经过字段和枚举 payload 校验。比较 trait 保持标准库的父 trait 关系,因此通常按 PartialEq, Eq, PartialOrd, Ord 一起派生;只派生 EqPartialOrdOrd 而没有所需的父 trait impl 会产生类型错误。

解析与时间

std::parse 提供一组溢出安全的解析入口,std::time 提供时间戳与休眠:

use std::parse::parse_i32;
use std::time::{sleep, Duration, time_now};

fun main() {
    let value = parse_i32("42").unwrap_or(0);
    println!("value={} now={}", value, time_now());
    sleep(Duration::from_millis(50));
}

parse_i32 / parse_i64 / parse_u64 / parse_usize 只解析十进制;空串、单独的负号、非法字符和超出目标范围的输入返回 Noneparse_with_radix 支持 2–36 进制。time_now 转发到 C time 并返回 i64Duration 提供 from_secs / from_millis / as_secs / as_millissleep 转发到运行时垫片。

进程参数

std::env::args_os() 返回 Vector<std::ffi::OsString>,无损保留宿主参数。Unix 保存原始字节;Windows 直接解析 GetCommandLineW,使用 WTF-8 保存 UTF-16,因此孤立代理项也不会丢失。OsString::as_encoded_bytes() 只适合在同一平台和版本内传递,into_string() 在参数不是有效 Unicode 时返回原值。

std::env::args() 返回 Vector<String>;只要任一参数不能转换为 Unicode,该函数就会 panic。需要处理任意系统参数时应使用 args_os()

完整 API 清单

模块内容
std::option::Option<T>is_someis_noneunwrapexpectunwrap_orunwrap_or_elsemapmap_orand_thenandoror_else
std::result::Result<T, E>is_okis_errunwrapexpectunwrap_orunwrap_or_elsemapmap_ormap_errand_thenandokerr
std::ffi::OsStringnewfrom_stras_encoded_bytesfrom_encoded_bytes_unchecked(unsafe)、into_stringlenis_empty
std::envargs_osargs
std::string::Stringnewfrom_strfrom_utf8as_stras_byteslencapacityis_emptypush_strpush_charclearslicetrimcontainsfindstarts_withends_withsplitreplaceto_ascii_uppercaseto_ascii_lowercase
std::str(impl)lenis_emptyas_bytescontainsfindstarts_withends_withslicetrimsplitreplaceto_ascii_uppercaseto_ascii_lowercase,以及按 Unicode char 遍历的 StrIter
std::vector::Vector<T>newlencapacityis_emptypushpopinsertremovegetget_mutswapsortcontainsretainclearas_sliceas_ptriteriter_mutfrom_iteratorfrom_elem、读写下标和按值迭代
std::collectionsHashMapHashSet(键需 Hash + Eq)、TreeMapTreeSet(键需 Ord),四类集合均提供 removeHashMap 另有 get_or_insert
std::iterIteratorIntoIterator 协议;Iterator 的默认方法含 mapfilterchaininspectcountnthfoldfor_eachallanyfindpositioncollectstd::iter 另提供急切的 map_into / filter_into,适配器 enumerate / take / skip / take_while / skip_while / zipmin / max,以及 DoubleEndedIterator
std::sliceSliceIterSliceIterMut,以及 [T] 的长度、边界检查访问、原始指针访问和借用迭代
std::array按值、共享借用和可变借用数组迭代器
std::fsFsFileopencreateappendreadwriteflushread_to_string)、existsmetadataread_dir,以及整文件 read_to_string / write
std::randomrandom_u32random_u64random_boolrandom_below
std::opsRangeRangeInclusiverange(start, end)range_inclusive(start, end)Drop,以及算术、位运算、移位、复合赋值和 Index / IndexMut trait
std::markerCopy
std::cloneClone
std::cmpOrderingPartialEqEqPartialOrdOrd
std::defaultDefault,为标量、Option<T>StringVector<T> 提供默认值
std::convertInto<T>From<T>? 错误传播使用的错误转换协议
std::hashHash,通过共享借用为标量提供确定性的 usize 哈希值
std::fmt / std::ioDebugDisplayFormatter 和底层输出函数
std::parseparse_i32parse_i64parse_u64parse_usizeparse_with_radix
std::timetime_nowDurationfrom_secsfrom_millisas_secsas_millis)和 sleep

Vector<T> 会拒绝零大小元素并检查容量乘法溢出;下标越界调用 panic。错误传播的完整规则见错误处理

Clue 构建器

clue 是 Riddle 当前的包管理器和项目构建器。主要命令如下:

clue init|new <path> [--bin|--lib|--workspace]
clue check|build [path] [-p <package>|--workspace] [--bin <name>] [--features a,b|--all-features] [--all-targets] [--locked]
clue run [path] [-p <package>] [--bin <name>|--example <name>] [--features a,b|--all-features] [-- <args>...]
clue test|bench [path] [-p <package>|--workspace] [--test|--bench <name>] [--features a,b|--all-features]
clue add <name> [--version <req>|--path <path>|--git <url>] [--dev]
clue remove <name> [--dev]
clue fetch|update|tree|metadata [path]
clue package [--list] [path]
clue publish [--dry-run] [--registry <name>] [path]
clue install [<package>@<version-req>] [--path <path>|--git <url>]
clue uninstall <name>
clue clean [path]

全局 --offline 只使用缓存,-j/--jobs 控制并行任务数。clue init 在指定目录中初始化项目,clue new 创建新目录和项目;二者都会生成清单、入口源码和忽略文件。它们不会覆盖已有的 Clue.toml 或目标入口源码。clue check 检查项目但不生成 C,clue build 构建项目,clue run 先构建二进制或 example 再运行。

二进制项目会按 bin 名称保留 .clue/build/<bin-name>.c 和默认的 <bin-name>.runtime.c,并在同一目录生成 <bin-name>;Windows 下扩展名为 .exe--release 使用 .clue/build/<target>/release,与 debug 缓存隔离。设置 CC 时 Clue 会严格使用它,失败时不会静默回退;未设置时,会先尝试目标组件 c-toolchain.toml 中配置的编译器(由 ridup target configure 设置),再按候选顺序探测:Linux/macOS 目标依次尝试 clangccgcc;Windows 目标依次尝试 clang-clclangccgcccl(非 Windows 宿主上的交叉目标会把 clang-cl 放到最后),最后追加带版本后缀的 GCC/Clang。候选必须能够完成一次 C11 编译和链接。库项目生成 C、目标文件、.rmeta 和默认 .rlibcrate-type 还可请求静态库和动态库。

目标平台

Clue 按 --targetRIDDLE_TARGETClue.toml[build].target、宿主平台的顺序选择目标:

[build]
target = "aarch64-unknown-linux-gnu"

当前严格限制为以下 7 个目标:

  • 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

交叉构建二进制项目之前需要运行 ridup target add <triple>。目标组件提供 Riddle runtime,但不等于 C 工具链已经就绪:Linux 需要 sysroot,Windows MSVC 目标需要 Windows SDK 与 MSVC 库,macOS 需要 Apple SDK。clue run 只允许运行宿主目标;交叉产物应复制到目标系统运行。

项目布局

推荐布局:

hello/
  .gitignore
  Clue.toml
  src/
    main.rid

二进制项目的清单包含一个 [[bin]] 目标:

[package]
name = "hello"
version = "0.1.0"

[[bin]]
name = "hello"
path = "src/main.rid"

[dependencies]

库项目使用 [lib]src/lib.rid。如果清单没有显式目标,clue build 会按包类型寻找入口。二进制包依次检查 src/main.ridsrc/lib.rid<package-name>.ridmain.rid;库和过程宏包依次检查 src/lib.rid<package-name>.ridlib.ridsrc/main.rid

旧项目仍可以在 [package] 中使用 entry 指定入口:

[package]
name = "hello"
entry = "src/bin/hello.rid"
version = "0.1.0"

单个二进制目标时,旧式 entry 的优先级高于 [[bin]].path / [lib].path。多个 [[bin]] 各自使用 pathcheckbuild 默认处理全部目标,也可以通过 --bin <name> 只处理一个,run 在多目标时要求指定 --bin。未写 name 时从入口文件名推导,重名会被拒绝。

运行时与分配器

二进制项目默认链接 Riddle 自带的保守式非移动 GC。要替换为自定义 GC、arena 或其他分配器,在项目根目录的 Clue.toml 中指定一个 C 源文件:

[runtime]
source = "runtime/custom_gc.c"

路径相对项目根目录解析。运行时源码必须实现以下 ABI:

void rgc_init(void *stack_bottom);
void *rgc_alloc(size_t size);
void *rgc_realloc(void *ptr, size_t size);
void rgc_free(void *ptr);
void rgc_collect(void);

Clue 会单独链接平台进程参数运行时;自定义内存运行时不需要实现 std::env 的参数函数。

rgc_alloc 返回的地址必须满足普通 C 对象的对齐要求,并且在引用仍可能存在时不能移动。rgc_reallocrgc_free 供标准库容器(如 Vector)显式管理缓冲区:rgc_realloc 迁移并保留原内容,rgc_free 立即释放且必须接受空指针;基于 malloc 的分配器可直接委托给 realloc/free。无回收分配器可以忽略 stack_bottom,并把 rgc_collect 实现为空函数。当前 ABI 不支持移动式 GC、finalizer 或多线程栈注册。

要像 Rust 一样完全关闭 GC,在二进制包中设置:

[runtime]
gc = false

这不是把 rgc_collect 留空,而是从生成结果中移除收集器、根扫描和全部 rgc_* 符号,改用 riddle_allocriddle_reallocriddle_free 管理有所有者的堆值。闭包环境和容器缓冲区在所有者结束时确定性释放;需要让栈上值活过其作用域的引用会报告 E0310。输入引用仍可在不延长生命周期的情况下转发。gc = false 不能与 source 同时声明。

运行时属于最终进程,因此 [runtime] 只允许出现在二进制包;库和依赖包只生成 ABI 调用,不能选择运行时。

依赖

[dependencies] 支持 path、git 和 sparse registry。版本使用 semver 约束:

[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" }

表格式依赖还支持 package 重命名、branchtagrevregistryfeaturesdefault-featuresoptional。依赖键会成为当前包里的模块名,package 指向依赖包自己的 [package].name

[dependencies]
math = { package = "math-core", path = "../math-core" }

源码中按模块路径使用依赖键:

fun main() -> i32 {
    math::one()
}

clue addclue remove 会修改清单并保留无关布局。[dev-dependencies] 只在 test、example 和 bench 目标中加载。依赖键必须是合法模块名,也就是字母或 _ 开头,后面跟字母、数字或 _

作为依赖加载时,Clue 会优先使用 [lib].path;没有 [lib] 目标时,再依次寻找 src/lib.rid<package-name>.ridlib.ridsrc/main.rid。依赖包需要用 pub 导出给调用方使用的函数、类型、模块或 use 重新导出。

工作区

根目录可以用虚拟工作区清单注册子 crate:

[workspace]
crates = ["hello", "math"]

每个注册目录都维护自己的 Clue.toml。根目录执行 clue checkclue build 会按依赖顺序处理所有 crate;在子目录执行时默认只处理当前 crate,--workspace 处理全部,--package <name> 选择单个 crate。工作区和包内的二进制分别用 --package <name>--bin <name> 选择。

单包项目在项目根目录生成 Clue.lock v3。工作区只维护根目录的一个锁文件,子 crate 不单独生成;工作区内的 path 依赖必须在 workspace.crates 中注册。锁文件记录包名、版本、source、依赖、启用的 feature、registry checksum、git revision 和源码指纹。普通 checkbuildfetch 优先复用已锁定版本,clue update 才重新选择满足约束的版本;--locked 会拒绝缺失或过期的锁文件,--offline 只读取缓存。registry 包下载后会校验 SHA-256,并按安全路径规则解包。

过程宏

过程宏包使用 [lib] proc-macro = true

[package]
name = "answer-macros"

[lib]
path = "src/lib.rid"
proc-macro = true

宏函数由 Riddle 编写并且必须公开。derive 宏和函数式宏使用 TokenStream -> TokenStream 签名,属性宏接收属性参数与被标记条目两个 TokenStream

#[proc_macro_derive(Answer, attributes(answer))]
pub fun derive_answer(input: TokenStream) -> TokenStream {
    TokenStream::from_str("fun generated_answer() -> i32 { 42 }")
        .unwrap_or(TokenStream::new())
}

#[proc_macro]
pub fun answer(input: TokenStream) -> TokenStream {
    TokenStream::from_str("42").unwrap_or(TokenStream::new())
}

#[proc_macro_attribute]
pub fun replace(args: TokenStream, item: TokenStream) -> TokenStream {
    item
}

TokenStreamTokenTree 序列组成,而不是保存原始源码字符串。TokenTree 分为 GroupIdentPunctLiteral;分组递归包含另一个 TokenStream,每个 token 都带有输入中的字节范围。宏可以借用或取得 token 的所有权进行迭代:

for tree in &input {
    match tree {
        TokenTree::Ident(ident) => println!("{}", ident.as_str()),
        TokenTree::Group(group) => {
            for nested in group.stream() {
                let span = nested.span();
            }
        },
        _ => {},
    }
}

TokenStream::from_str 会执行词法分析,失败时返回 LexError,并把新 token 的位置设为当前宏调用点;to_string() 则提供源码文本视图。空白和注释不属于 token,因此不会逐字保留。TokenStream::clone() 共享底层 token,首次修改时才复制。Clue 与宏宿主之间传递结构化 token tree,编译器也直接把输出 token 送回解析器,不会把整个展开结果重新做一次词法分析。

过程宏包还会自动获得内置 syn 模块和 quote!,不需要在 Clue.toml 中添加依赖。 syn 可以把输入解析为 DeriveInputItemStmtExprTypePatquote! 则通过 #name 插入实现了 ToTokens 的值:

use syn::{DeriveInput, parse};

#[proc_macro_derive(Answer)]
pub fun derive_answer(input: TokenStream) -> TokenStream {
    let parsed = match parse::<DeriveInput>(input) {
        Result::Ok(value) => value,
        Result::Err(error) => {
            error.emit();
            return TokenStream::new();
        },
    };
    let generated = Ident::new("generated_answer", parsed.ident.span());
    quote! { fun #generated() -> i32 { 42 } }
}

结构化 derive 输入、通用语法节点、自定义 ParseVisitFoldquote! 重复语法见内置 synquote!

使用方把宏包声明为依赖,再把 derive 宏导入独立的宏命名空间;下例使用本地 path:

[dependencies]
answer_macros = { package = "answer-macros", path = "../answer-macros" }
use answer_macros::{Answer, answer, replace};

#[answer]
#[derive(Answer)]
struct Value {}

#[replace]
fun old_value() -> i32 { 0 }

fun main() -> i32 { answer!() }

宏导入支持 use answer_macros::{Answer as GenerateAnswer};use answer_macros::*;。宏名可以与类型、trait 或值同名而不冲突;不导入时仍可使用 #[derive(answer_macros::Answer)] 限定写法。derive 只能用于结构体和枚举;Riddle 当前没有 union 条目。pub use 可以在模块中重导出过程宏,混合导入会保留同一条 use 中的普通名称。 函数式宏可用于表达式、条目、类型和模式位置。attributes(answer) 注册的 helper 属性只在 对应 derive 的输入条目、枚举变体和字段上有效,未注册的 helper 属性会在调用宏之前报错。

Clue 会把过程宏包编译为宿主平台进程,而不会把它拼入目标程序。每个过程宏包第一次调用时懒启动一个独立宿主进程,并在后续调用中复用它;单次调用最多运行 10 秒,输入和输出各受 16 MiB 上限保护。宿主崩溃或超时只会终止当前 worker,下一次调用会重新启动,Clue 不会被直接带崩。derive 和属性宏输出必须是顶层条目,函数式宏输出必须适合调用位置。宏诊断会使用传入的 Span,复制到输出的 token 也会把后续编译错误映回原位置;使用 Span::call_site() 创建的 token 和诊断则指向宏调用。生成代码中的宏会继续展开,最大深度为 32。过程宏包可以通过本地 path 依赖使用另一个过程宏包,clue check 也能直接检查过程宏包自身。LSP 会识别宏导入和调用,并提供分类高亮、悬停、定义跳转、引用、别名重命名和按宏种类过滤的补全。

模块、usepub use 和可见性规则见模块、use 与包

项目诊断

clue check

Clue 会展开入口文件声明的外部模块和锁定依赖,再运行完整的编译检查。错误位置会映射回实际的 .rid 文件,而不是统一显示为项目入口文件。

riddle-lsp 使用相同的项目加载规则。打开 Clue 项目中的文件时,诊断会包含模块和本地依赖,并优先使用编辑器中尚未保存的内容;任一文件变化后,所有已打开文档的诊断都会刷新。

构建缓存

Clue 会缓存构建指纹。Clue.toml、展开后的源码、运行时源码、当前 Riddle 编译器版本、目标平台,或者 C 编译器的实际路径与版本发生变化时会重新构建;没有变化且输出文件仍存在时会输出 fresh。成功的 C11 兼容性探测也按编译器身份缓存。

运行项目

clue run
clue run path/to/project -- arg1 arg2

run 只接受二进制项目,并把 -- 后的参数原样传给程序。程序退出码会成为 clue run 的退出码。

特性和目标模型

可选依赖可以在 [features] 中通过 dep:<name> 启用;name/feature 转发依赖 feature,name?/feature 只在依赖已启用时转发。--features a,b 启用命名 feature,--all-features 启用清单中的全部 feature,--no-default-features 禁用 default。未显式声明目标时,Clue 会发现 src/main.ridsrc/bin/*.ridtests/*.ridexamples/*.ridbenches/*.rid--all-targets 还会检查或构建 test、example 和 bench。

获取、发布与安装

clue fetch 获取依赖并更新缓存,clue tree -e features 显示锁定的 feature,clue metadata 输出清单和锁图 JSON。缓存位于 $CLUE_HOME/registry$CLUE_HOME/git;默认 CLUE_HOME 是用户目录下的 .clue

clue package.clue/package 生成 .cluepkg--list 只列出将被打包的文件。clue publish --dry-run 执行打包与发布权限校验但不联网;实际发布使用所选 registry API。clue install --path .clue install calculator@^1clue install --git <url> 会构建二进制并安装到 $CLUE_HOME/binclue uninstall <name> 删除它。

registry 和构建设置可写入 $CLUE_HOME/config.toml 或项目的 .clue/config.toml,项目配置优先。CLUE_OFFLINECLUE_JOBSCLUE_REGISTRY_INDEXCLUE_REGISTRY_TOKEN 可以覆盖配置文件。

作为 Rust 库使用

clue crate 公开项目创建、检查、构建和项目分析 API。

use clue::{ProjectKind, build, check, new, run};
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)?;
    run(root, &[])?;
    Ok(())
}

ProjectKind::Library 用于库项目。需要初始化已有目录时使用 init,其覆盖保护与命令行版本相同。

当前限制

  • 二进制、静态库和动态库的最终链接需要受支持的系统 C 工具链;
  • clue run 只能运行宿主目标,交叉构建产物需要复制到目标系统;
  • 交叉链接除 ridup 目标组件外,仍需要目标平台的 sysroot、SDK 和系统库。

内置 synquote!

Riddle 的过程宏包内置 syn 模块和 quote!。它们随 Clue 注入过程宏包, 不需要在 Clue.toml 中声明额外依赖:

use syn::{Data, DeriveInput, parse};

#[proc_macro_derive(Answer)]
pub fun derive_answer(input: TokenStream) -> TokenStream {
    let parsed = match parse::<DeriveInput>(input) {
        Result::Ok(value) => value,
        Result::Err(error) => {
            error.emit();
            return TokenStream::new();
        },
    };

    match &parsed.data {
        Data::Struct(_) => {},
        Data::Enum(_) => {
            Diagnostic::error(
                parsed.ident.span(),
                "Answer can only be derived for structs",
            ).emit();
            return TokenStream::new();
        },
    }

    let generated = Ident::new("generated_answer", parsed.ident.span());
    quote! {
        fun #generated() -> i32 { 42 }
    }
}

使用方只需依赖并导入这个宏包:

use answer_macros::Answer;

#[derive(Answer)]
struct Marker {}

fun main() -> i32 {
    generated_answer()
}

解析入口

syn 提供两个通用解析函数:

use syn::{Expr, Type, parse, parse_str};

let expr = parse::<Expr>(input);
let ty = parse_str::<Type>("&mut Vector<i32>");
  • parse::<T>(tokens)TokenStream 解析实现了 Parse 的类型。
  • parse_str::<T>(source) 先对字符串进行词法分析,再执行同样的解析。
  • 失败时返回 syn::ErrorError 包含 spanmessage,可用 error.emit() 发出编译诊断。

当前内置实现为以下类型提供 Parse

类型用途
DeriveInput解析 derive 宏接收的结构体或枚举
File解析由多个条目或语句组成的 token 流
Item解析一个顶层条目
Stmt解析一条语句
Expr解析一个表达式
Type解析一个类型
Pat解析一个模式

自定义 Parse

过程宏可以为自己的输入类型实现 Parse

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(), "unexpected token"))
                }
            },
            Option::Some(tree) => {
                Result::Err(Error::new(tree.span(), "expected identifier"))
            },
            Option::None => {
                Result::Err(Error::new(input.span(), "expected identifier"))
            },
        }
    }
}

ParseStream 提供 is_empty()peek_ident()peek_punct()span()next()remaining()parse::<T>()。这些接口直接操作结构化 token, 不会依赖字符串切分。

DeriveInput

DeriveInput 为 derive 宏提供结构化输入:

pub struct DeriveInput {
    pub attrs: Vector<Attribute>,
    pub vis: Visibility,
    pub ident: Ident,
    pub generics: Generics,
    pub data: Data,
}

Visibility 目前分为 InheritedPublicData 分为:

pub enum Data {
    Struct(DataStruct),
    Enum(DataEnum),
}

结构体字段通过 DataStruct.named 和原始字段 token 提供;枚举通过 DataEnum.items 提供结构化变体。字段形状使用 Fields 表示:

pub enum Fields {
    Unit,
    Named(Vector<Field>),
    Unnamed(Vector<Type>),
}

泛型信息位于 Generics

  • tokens 保存 <...>
  • where_clause 保存 where ...
  • params 包含 GenericParam::TypeGenericParam::Const
  • predicates 包含解析后的 WherePredicate

AttributeFieldVariant、泛型参数和 where 谓词同时保留自己的 TokenStream,因此宏既可以读取结构化字段,也可以无损地把原节点写回输出。 DeriveInput::to_token_stream() 返回完整输入的 token 副本。

语法节点

DeriveInput 外,ItemStmtExprTypePat 会校验当前 Riddle 语法并按类别保存 token。这些节点不是每个语法细节都有独立字段的完整 AST; 需要检查具体细节时,可以匹配类别后读取该变体中的 TokenStream,或使用 VisitFold 递归处理嵌套语法。

Item

支持模块、use、函数、结构体、枚举、trait、impl、常量、类型别名和 extern 条目:

match item {
    Item::Function(tokens) => println!("{}", tokens.to_string()),
    Item::Struct(tokens) => println!("{}", tokens.to_string()),
    _ => {},
}

Stmt

Stmt 分为 ItemLocalExprBreakContinueReturnFile.stmts 保存从一个完整 token 流解析出的语句。

Expr

Expr 覆盖字面量、路径、块、元组、数组、结构体字面量、调用、字段访问、 索引、一元和二元表达式、转换、?、闭包、ifwhileformatchunsafe 和宏调用。

Type

Type 覆盖路径、引用、指针、元组、数组、常量类型、never 类型、 impl Trait 和宏类型。

Pat

Pat 覆盖通配符、字面量、元组、结构体、枚举、绑定、引用和宏模式。

所有上述节点以及 DeriveInput 的结构化子节点都实现 ToTokens

use syn::{Expr, ToTokens, parse_str};

let expr = parse_str::<Expr>("value + 1").unwrap();
let mut output = TokenStream::new();
expr.to_tokens(&mut output);

quote!

quote! 把 Riddle token 写入新的 TokenStream#name 会插入实现了 ToTokens 的值:

let name = Ident::new("answer", Span::call_site());
let value = parse_str::<Expr>("40 + 2").unwrap();

let output = quote! {
    fun #name() -> i32 { #value }
};

quote! 支持使用 * 重复一个向量,并可在 * 前放置一个分隔 token:

let tuple = quote! { (#(#names),*) };

同一个重复块中的多个向量会按下标配对:

let fields = quote! { { #(#names: #values),* } };

参与同一重复块的向量长度必须相等;长度不一致会使宏展开失败。重复块必须至少 包含一个 #name。当前重复语法支持 #(...)* 和带单个分隔 token 的 #(...),* 形式。

遍历与改写

Visit 以借用方式遍历节点。覆盖方法后调用对应的 walk_*,即可继续递归:

use syn::{Expr, Visit};

struct ExprCounter {
    count: usize,
}

impl Visit for ExprCounter {
    fun visit_expr(&mut self, node: &Expr) {
        self.count += 1usize;
        syn::walk_expr(self, node);
    }
}

可覆盖的方法为 visit_filevisit_itemvisit_stmtvisit_exprvisit_typevisit_patvisit_derive_input。对应的递归函数分别为 walk_filewalk_itemwalk_stmtwalk_exprwalk_typewalk_patwalk_derive_input

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 {
            return parse_str::<Expr>("3").unwrap();
        }
        syn::fold_expr(self, node)
    }
}

可覆盖的方法为 fold_filefold_itemfold_stmtfold_exprfold_typefold_patfold_derive_input。在自定义方法末尾调用同名的 syn::fold_* 函数可执行默认的递归改写。

当前边界

  • DeriveInput 只接受结构体和枚举;Riddle 当前没有 union 条目。
  • 通用语法节点保留分类后的 token,不提供与 Rust syn 完全同构的字段级 AST。
  • ParseStream 提供最小的 token 游标接口,不包含 Rust syn 的全部解析宏和 parser combinator。
  • quote! 重复目前使用 *,分隔符为一个 token。

底层 TokenStreamTokenTreeSpan 和诊断接口见 Clue 构建器的过程宏章节

编写过程宏

过程宏是运行在编译期、接收 Riddle 源码 token 并生成新代码的函数。它比 声明式宏更强大,也更容易写错。本章从一个空目录开始,逐步完成一个真实的 宏包:包含函数式宏、derive 宏和属性宏各一个,并解释每一行代码为什么这样写。

三种过程宏

种类导出属性签名调用位置
函数式宏#[proc_macro]fun (input: TokenStream) -> TokenStream表达式、条目、类型、模式:name!(...)
derive 宏#[proc_macro_derive(Name, attributes(...))]fun (input: TokenStream) -> TokenStream#[derive(Name)] 结构体或枚举
属性宏#[proc_macro_attribute]fun (args: TokenStream, item: TokenStream) -> TokenStream#[name(...)] 条目

宏展开发生在普通解析之前:属性宏和 derive 宏先于解析被展开,因此它们的参数 以平衡 token tree 表示,编译器不会预先解析其内部语法。生成代码中的宏会继续 展开,最大深度为 32。

创建过程宏包

过程宏包是一个独立的库包,由 [lib] proc-macro = true 标记。CLI 目前只能 创建二进制或普通库,先建库再改清单:

clue new --lib answer-macros

然后把 Clue.toml[lib] 目标改为:

[package]
name = "answer-macros"
version = "0.1.0"

[lib]
name = "answer-macros"
path = "src/lib.rid"
proc-macro = true

[dependencies]

过程宏包自动获得 proc_macro 模块(提供 TokenStreamTokenTreeSpanDiagnostic 等)以及内置的 synquote!,不需要声明任何依赖。宏函数用 Riddle 编写、必须公开,并且签名必须严格匹配上表。包不导出任何宏时 clue check 会报错。

宏包本身不会被链接进使用方的程序。Clue 把它编译成宿主平台进程;每个宏包 第一次调用时懒启动一个独立 worker,后续调用复用该进程。单次调用最多 10 秒, 输入和输出各受 16 MiB 上限保护;worker 崩溃或超时会被丢弃,并在下一次调用时 重新启动,不会带崩 Clue。

心智模型:token,不是文本

宏看到的不是源码字符串,而是一棵递归的 token tree。四种 TokenTree

  • Ident — 标识符,如 getters
  • Punct — 单个标点字符,如 ::-> 由多个 Punct 组成;
  • Literal — 数字、字符串、字符字面量;
  • Group — 配对的 (){}[],内部递归包含另一个 TokenStream

每个 token 都带有一个 Span(源码中的字节范围)。空白和注释不属于 token, 不会逐字保留,所以宏无法感知缩进和注释。

TokenStream 可以借用迭代、按值迭代、push/extendlen()/is_empty()TokenStream::from_str 对字符串做词法分析,失败时返回 LexErrorto_string() 把 token 渲染回源码文本,常用于调试。clone() 共享底层 token, 首次修改时才复制。

第一个函数式宏

src/lib.rid 中写一个完全忽略输入、固定输出 42 的宏:

#[proc_macro]
pub fun answer(_input: TokenStream) -> TokenStream {
    TokenStream::from_str("42").unwrap_or(TokenStream::new())
}

参数名前加 _ 表明宏不读取输入。把它放回参数名 input 也可以,Riddle 允许 参数未使用,只是习惯上用 _ 让意图更明确。

在另一个包中依赖并调用它:

[dependencies]
answer_macros = { package = "answer-macros", path = "../answer-macros" }
use answer_macros::answer;

fun main() -> i32 {
    answer!()
}

函数式宏可以用在表达式、条目、类型和模式位置,输出必须适合调用位置: answer!() 出现在表达式里,输出就必须是表达式。

实战一:derive 宏

derive 宏接收整个结构体或枚举的 token。裸手解析 token 容易出错,应该先用 syn 解析成结构化输入。下面实现一个 Getters 宏:为结构体的每个命名字段 生成同名的 &self 取值方法。

use syn::{Data, DeriveInput, Generics, parse};

#[proc_macro_derive(Getters)]
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();
        },
    };

    // 把结构体按字段拆开,取得字段所有权以便插值进 quote!。
    let DeriveInput {
        attrs: _attrs,
        vis: _vis,
        ident: struct_name,
        generics,
        data,
    } = parsed;

    let Generics {
        tokens: generic_params,
        where_clause,
        params: _params,
        predicates: _predicates,
    } = generics;

    match data {
        Data::Struct(data_struct) => {
            if data_struct.named.is_empty() {
                Diagnostic::error(
                    struct_name.span(),
                    "Getters requires at least one named field",
                ).emit();
                return TokenStream::new();
            }

            let mut getter_fns = Vector::new();
            for field in data_struct.named {
                let field_name = field.ident;
                let field_ty = field.ty;
                getter_fns.push(quote! {
                    pub fun #field_name(&self) -> &#field_ty {
                        &self.#field_name
                    }
                });
            }

            // #generic_params 展开为 <...>,要出现两次:
            // impl<T> 和类型名后的 Foo<T>。
            quote! {
                impl #generic_params #struct_name #generic_params #where_clause {
                    #(#getter_fns)*
                }
            }
        },
        Data::Enum(_) => {
            Diagnostic::error(
                struct_name.span(),
                "Getters can only be derived for structs",
            ).emit();
            TokenStream::new()
        },
    }
}

使用方:

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()
}

要点:

  • parse::<DeriveInput> 失败时用 error.emit() 发出带位置的诊断,然后返回 空 TokenStream,而不是 panic。宏内部 panic 只会让宿主进程失败,产生 一条不友好的错误。
  • Data::Struct / Data::Enum 分派后,DataStruct.named 是命名字段向量; 空字段结构体(struct Marker {})也要考虑。
  • quote!#name 插值实现 ToTokens 的值;#(#getter_fns)* 把向量展开 为重复块。同一个重复块中的多个向量按下标配对,长度必须相等。
  • 诊断和复制到输出的 token 会保留源位置,因此生成的代码出错时,编译错误会 映回字段本身,而不是宏调用点。

实战二:属性宏

属性宏接收两个参数:属性里的参数 token 和被标记的整个条目。它的输出必须是 顶层条目。下面实现一个 #[trace_level(3)]:把参数数值生成一个常量,放在 原条目之前——演示“读参数 + 透传条目“的典型组合:

#[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
}

参数 token 会原样插入输出,因此调用 #[trace_level(3)] 生成 const TRACE_LEVEL: i32 = 3;。属性宏最常见的形态就是“检查参数后原样返回 条目“,例如为 API 路由做校验;完全不改写时直接返回 item 即可。注意属性 宏不能改写语句或表达式,只能处理条目。

helper 属性

#[proc_macro_derive(Name, attributes(helper))] 会注册一个 helper 属性, 只允许出现在该 derive 的条目、枚举变体和字段上。未注册的属性在宏调用之前 就会报错。在 Getters 上注册 getter,允许字段用 #[getter(skip)] 跳过:

#[proc_macro_derive(Getters, attributes(getter))]
pub fun derive_getters(input: TokenStream) -> TokenStream {
    // ... 解析 DeriveInput 同上 ...
    match data {
        Data::Struct(data_struct) => {
            let mut getter_fns = Vector::new();
            for field in data_struct.named {
                let field_name = field.ident;
                let field_ty = field.ty;

                let mut skip = false;
                for attr in field.attrs {
                    if attr.tokens.to_string().contains("skip") {
                        skip = true;
                    }
                }
                if skip {
                    continue;
                }

                getter_fns.push(quote! {
                    pub fun #field_name(&self) -> &#field_ty {
                        &self.#field_name
                    }
                });
            }
            // ...
        },
        Data::Enum(_) => { /* 同上 */ },
    }
}

使用方:

use answer_macros::Getters;

#[derive(Getters)]
struct User {
    name: String,
    #[getter(skip)]
    password_hash: String,
}

Attribute.tokens 保存完整属性 token(含 #[),用 to_string() 检查内容 是最直接的读法。helper 属性由编译器从输入中保留,普通属性(如 #[deprecated]) 会原样出现在 attrs 里。

诊断与 span

Diagnostic 支持四级:

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() 创建的新 token 和诊断指向整个宏调用;
  • Span::mixed_site() 目前等价于 call_site()
  • 多个 span 可用 span.join(other) 合并成覆盖两者的区间。

诊断消息返回为 syn::Error 时同样可用 error.emit(),见 syn 章节的 Parse 实现。

在项目中使用宏

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: String,
}

fun main() -> i32 { value!() }

宏名可以与类型、trait 或值同名而不冲突;混合 use 会保留同一条 use 中的 普通名称。derive 只能放在结构体和枚举上(Riddle 当前没有 union)。

调试与常见问题

  • input.to_string()println!("{}", tree.to_string()) 观察宏收到的 token,是最直接的调试手段。
  • syn 解析失败:error.emit() 会显示期望的语法,对照 DeriveInput 的 结构检查输入是否合法。
  • “must contain only top-level items”:derive 或属性宏输出了非顶层条目。
  • “derive helper attributes must be declared”:字段上使用了未注册的 helper 属性,检查 attributes(...) 列表。
  • “cannot find derive macro X”:没有导入宏或包没有导出该宏,检查 use
  • 空输出:宏返回了 TokenStream::new(),通常是因为诊断已经发出——先看 诊断再看代码。

边界

  • 没有 union 条目;DeriveInput 只接受结构体和枚举。
  • quote! 重复使用 *,分隔符为单个 token。
  • ParseStream 是最小 token 游标,不包含 Rust syn 的全部解析宏。
  • 过程宏包可以依赖另一个过程宏包(本地 path 依赖),clue check 也能直接 检查宏包自身。

完整的 synquote! API 参考见内置 synquote!, 宏包清单、导入与展开机制见 Clue 构建器的过程宏章节

编辑器与 LSP

Riddle 通过 riddle-lsp 为编辑器提供实时诊断、自动导入补全、语义高亮、悬停信息、签名帮助、代码跳转、调用与类型层级、查找引用、重命名、符号导航、格式化和代码折叠。当前仓库提供 Helix、VS Code、Zed 和 IntelliJ IDEA 2026.1+ 的适配文件。

准备 riddle-lsp

先按照安装 Riddle 工具链完成安装,并确认编辑器启动时能找到服务器:

riddle-lsp --version

如果命令不可用,请把 Riddle 二进制目录加入 PATH,或在支持路径设置的编辑器中填写 riddle-lsp 的绝对路径。JetBrains 插件当前固定从 IDE 进程的 PATH 启动服务器。四个适配都会识别 .rid 文件;服务器会发现每个工作区文件夹内的 Clue 项目,加载项目模块和本地依赖,并为未打开文件建立内存索引。

打包编辑器扩展

在仓库根目录运行 PowerShell 或 Bash 脚本:

pwsh -File editors\package.ps1
bash editors/package.sh

脚本需要 Node.js、npm、JDK 25 或更高版本和网络连接,Bash 版本还需要 zip 命令。首次构建 JetBrains 插件时,Gradle wrapper 会下载 Gradle 9 和 IntelliJ Platform 2026.1 SDK。脚本会在 editors/dist 中生成:

文件用途
riddle-vscode.vsix直接导入 VS Code
riddle-intellij.zip从磁盘安装到受支持的 JetBrains IDE
riddle-helix.zip解压后合并到 Helix 配置目录
riddle-zed.zip解压后作为 Zed Dev Extension 导入

JetBrains ZIP 和 VSIX 可以直接安装。Helix 和 Zed 的 ZIP 只负责分发所需文件;导入方式见下文。

Helix

解压 editors/dist/riddle-helix.zip。压缩包内容与仓库中的 editors/helix 相同:

editors/helix/
├── languages.toml
└── runtime/queries/riddle/
  1. 把解压目录中 languages.toml 的两个配置块合并到 Helix 配置目录的 languages.toml。已有文件时不要直接覆盖。
  2. 把解压目录中的 runtime/queries/riddle 复制到 Helix 配置目录的 runtime/queries/riddle
  3. 重新启动 Helix。

Helix 配置目录通常是:

平台路径
Linux / macOS~/.config/helix
Windows%AppData%\helix

直接从仓库导入时,查询文件可以这样复制:

mkdir -p ~/.config/helix/runtime/queries/riddle
cp editors/helix/runtime/queries/riddle/*.scm ~/.config/helix/runtime/queries/riddle/

PowerShell:

$queries = Join-Path $env:APPDATA "helix\runtime\queries\riddle"
New-Item -ItemType Directory -Force $queries | Out-Null
Copy-Item editors\helix\runtime\queries\riddle\*.scm $queries

默认配置从 PATH 启动服务器。需要指定路径或参数时,修改合并后的服务器配置:

[language-server.riddle-lsp]
command = "/path/to/riddle-lsp"
args = ["--no-std"]

补全默认立即响应。需要在持续输入时合并请求,可追加 --completion-delay-ms,单位为毫秒:

args = ["--completion-delay-ms", "25"]

检查安装结果:

hx --health riddle

riddle-lsp、Tree-sitter parser、Highlight queries、Textobject queries 和 Indent queries 都应显示可用。

VS Code

VS Code 适配包含 .rid 文件注册、基础 TextMate 高亮和 LSP 客户端。当前尚未发布到 Marketplace,需要安装本地 VSIX。

命令行安装打包脚本生成的扩展:

code --install-extension editors\dist\riddle-vscode.vsix

也可以打开扩展面板,在右上角 ... 菜单中选择 Install from VSIX…,然后选择 riddle-vscode.vsix

安装完成后重新打开 .rid 文件。右下角的语言模式应显示 Riddle

扩展默认从 PATH 启动 riddle-lsp。可以在 settings.json 中覆盖路径和参数:

{
    "riddle.server.path": "/path/to/riddle-lsp",
    "riddle.server.arguments": ["--completion-delay-ms", "25"]
}

Windows 路径中的反斜杠需要转义:

{
    "riddle.server.path": "C:\\tools\\riddle\\riddle-lsp.exe"
}

修改服务器路径或参数后,执行 Developer: Reload Window 重新启动扩展。

IntelliJ IDEA

插件使用 IntelliJ Platform 2026.1 的 LSP integration API,源码全部为 Kotlin。IntelliJ IDEA 2026.1 及更高版本可用;Android Studio 不在当前支持范围内。

  1. 打开 Settings | Plugins
  2. 点击齿轮菜单,选择 Install Plugin from Disk…
  3. 选择 editors/dist/riddle-intellij.zip,然后重新启动 IDE。
  4. 打开 .rid 文件,确认文件类型显示为 Riddle,并检查诊断、补全和语义高亮。

插件不向 riddle-lsp 传递额外参数,并固定从 IDE 进程的 PATH 查找命令。修改系统 PATH 后需要完全退出并重新启动 IDE。JetBrains 适配没有 TextMate 或 Tree-sitter 回退;没有启动 LSP 时不会出现 Riddle 语义高亮。

只构建这个插件时,可以运行:

Set-Location editors\intellij
.\gradlew.bat buildPlugin

生成的版本化 ZIP 位于 editors/intellij/build/distributions

Zed

Zed 适配当前以 Dev Extension 方式安装。先把 riddle-zed.zip 解压到固定目录,并确认该目录顶层包含 extension.toml,然后:

  1. 在命令面板运行 zed: extensions
  2. 选择 Install Dev Extension
  3. 选择刚才解压的目录;直接从仓库导入时选择 editors/zed
  4. 重新打开 .rid 文件,并确认语言模式为 Riddle

扩展默认从工作树的 PATH 查找 riddle-lsp。也可以在 Zed 的 settings.json 中指定路径、参数,并启用完整语义 Token:

{
    "languages": {
        "Riddle": {
            "semantic_tokens": "full"
        }
    },
    "lsp": {
        "riddle-lsp": {
            "binary": {
                "path": "/path/to/riddle-lsp",
                "arguments": ["--completion-delay-ms", "25"]
            }
        }
    }
}

修改配置后,在命令面板运行 language server: restart。Zed 和 Helix 当前复用 Rust Tree-sitter grammar 作为结构化回退;Riddle 专用的标识符分类由 riddle-lsp 语义 Token 提供。

当前能力

能力状态
.rid 文件识别Helix、VS Code、Zed、JetBrains 均支持
Clue 项目、未保存文件和未打开模块诊断支持
多工作区 Clue 项目发现、内存索引和按文件失效支持
解析、类型、move/borrow 诊断支持
Clue 项目级函数、方法、struct、enum、trait、参数和可变绑定语义高亮支持
跨模块返回类型和调用参数名 Inlay Hint支持
Clue 项目中的关键字、类型、全局项、局部变量、模式绑定和导入别名补全支持
字段、实例方法、模块项、枚举变体和关联函数补全支持
诊断驱动的快速修复(加 mut、补缺失字段、补 match 分支、删除空 usedrop 重写、名字纠错与自动导入)及 source.organizeImports支持
跨文件补全(包含已打开文件的未保存内容)支持
公开符号自动导入、重名路径区分和确定性排序支持
函数签名、推断类型和 struct/enum 声明 Hover(最多 5 个顶层字段或变体,枚举 payload 完整);方法调用显示实例化后的签名(替换过的接收者与返回类型)支持
内联提示:let 绑定推断类型、lambda 参数推断类型、多行链式调用每级结果类型、调用参数名支持
impl 块内补全缺失的 trait 方法(携带签名的 snippet)与关联类型支持
结构化选择范围(selectionRange)、mod 声明的模块文件链接、pull 诊断支持
Clue.toml 清单:未知键/类型/semver/依赖规则诊断(CLUE0002–CLUE0004)、节与键补全、键悬停、节与键符号支持
跳转定义(包含未打开的 Clue 模块)支持
跳转声明与跳转类型定义支持
trait 与 trait 方法的跳转实现支持
静态调用层级与 trait/实现类型层级支持
项目级查找引用与重命名(包含未保存文件、未打开模块、字段、trait 方法和导入别名)支持
签名帮助与当前参数跟踪支持
文档符号与工作区符号搜索支持
文档引用高亮与代码折叠支持
增量文档同步、过期分析取消与 Semantic Token delta支持
编辑器外部 .ridClue.toml 文件变更支持动态监听
格式化支持

工作区中的 Clue 项目会建立内存索引。补全可通过独立的 use path; 编辑自动导入可达的公开符号;调用层级只包含编译器能够静态解析的目标,不推测函数指针、闭包或 Trait 的运行时分派。

常见问题

编辑器提示找不到 riddle-lsp

先在编辑器内置终端运行 riddle-lsp --version。如果外部终端可用而编辑器中不可用,请完全退出并重新启动编辑器,让它重新读取 PATH;也可以直接配置绝对路径。

Helix 没有高亮或缩进查询

运行 hx --health riddle。如果 queries 显示不可用,检查三个 .scm 文件是否位于 Helix 配置目录的 runtime/queries/riddle 下。

Zed 只有基础语法颜色

确认 languages.Riddle.semantic_tokens 设置为 "full",然后重启 language server。Zed 默认不会请求完整语义 Token。

VS Code 有基础高亮但没有诊断

基础高亮由扩展内的 TextMate grammar 提供,不代表 LSP 已启动。检查 riddle.server.path,再打开 Output 面板查看 Riddle Language Server 输出。

JetBrains 中没有诊断或高亮

确认 IDE 是 2026.1 或更高版本,并把 Gradle JVM 设为 JDK 25 或更高版本;在 IDE 内置终端运行 riddle-lsp --version。如果命令不可用,修复 PATH 后完全退出并重新启动 IDE;仍有问题时通过 Help | Show Log 查看 LSP 启动错误。

如果上述的一切都不起作用?

请加入我们的 QQ 群: 677741637

或在 Github 上提交 Issue 来寻求帮助

FFI 与底层工具链

本页汇总当前仓库里和语言使用直接相关的工具、后端和 FFI 能力。

riddlec

riddlec 是当前命令行编译器入口:

riddlec [--verbose] [--no-std] [--backend c] [--target <triple>] [--output <file>] <file>...

常用参数:

参数作用
--verbose, -v打印 parse、HIR lower、type check、move/escape analysis、MIR lowering 的状态
--no-std不加载随编译器附带的标准库
--backend c, -b c使用 C backend 生成代码
--target <triple>选择受支持的目标平台 triple
--output <file>, -o <file>指定输出文件
--version, -V打印版本号和构建时 git commit hash
--help, -h打印帮助

riddlec 会自动把 std/lib.rid 拼到用户源码后面,因此基础 lang trait 不需要手动引入。

riddle fmt

riddle fmt 使用与 LSP 相同的源码格式化器:默认格式化文件,也可以从标准输入读取,或用 --check 检查格式而不修改文件。

riddle fmt src/main.rid
riddle fmt --check src/main.rid
cat src/main.rid | riddle fmt --emit stdout

--tab-size <n> 设置缩进宽度,--hard-tabs 使用制表符。LSP 的 textDocument/formatting 复用同一实现。

CLI 在解析失败时报告行列、返回非零状态并保持文件不变;编辑器中的 LSP 请求仍可对未完成源码提供格式化结果。

C backend

使用 C backend:

clue new hello
cargo run -p riddlec -- --backend c --output hello.c hello/src/main.rid

riddlec 只生成包含 rgc ABI 调用的 C 源码,不再内嵌具体 GC。手动构建时需要同时编译一个运行时实现;仓库中的默认实现位于 crates/gc/src/runtime.c,进程参数运行时位于 crates/gc/src/args_runtime.c(生成的 C 入口会无条件初始化进程参数):

cc hello.c crates/gc/src/runtime.c crates/gc/src/args_runtime.c -o hello

clue build 会自动选择并编译默认运行时,也可以通过 Clue.toml[runtime].source 使用自定义 GC 或分配器。

当前 C backend 会把 Riddle 的结构体生成为 C struct,固定长度数组生成为 C 数组字段,初始化含数组字段的结构体时使用 memcpy 复制数组存储。枚举值会生成为带 tag 和 payload 字段的结构体表示。raw string 会按 C 字符串规则转义后输出。

--output 的行为:

  • --output app:写出 app.c
  • --output app.c:写出 app.c
  • 其他输出名会追加 .c,例如 --output app.h 写出 app.h.c
  • 不写 --output:按第一个输入文件名派生 .c 输出名。

C 类型映射

当前后端使用以下主要表示:

RiddleC
i8 / i16 / i32 / i64int8_t / int16_t / int32_t / int64_t
u8 / u16 / u32 / u64uint8_t / uint16_t / uint32_t / uint64_t
isize / usizeptrdiff_t / size_t
boolbool
charuint32_t
()返回位置为 void;值位置(参数、字段等)使用 riddle_unitunsigned char
&T(定长类型)T*
*const T / *mut T内部值为 T*extern "C" 声明中统一映射为 void*
[T; N]C 数组;零长度数组使用严格 C11 兼容的占位存储
enum带 tag 和 payload 字段的 C struct
callable(内部){ call, env, drop },调用与析构接收隐藏环境参数
&[T](内部)携带指针与长度的切片结构
&str(Riddle 内部)riddle_str { ptr, len }

extern "C" 声明中的指针参数和返回值按 void* 映射,调用点会自动插入兼容的指针转换;内部值才保留具体的 T*&[T](元素必须有大小且不能是 str)作为 extern 参数会自动拆成指针和长度两个 C 参数:&[i32] 映射为 const int32_t*size_t&mut [T]T*size_t;切片返回值与无大小的切片仍被拒绝。&str 在导入与导出边界上的特殊规则见下一节。

extern “C”

外部 C 函数声明块必须使用 unsafe extern。块内函数默认不安全,只有显式标记为 safe fun 的声明才能在安全代码中调用;safe 不能在普通 extern 中使用:

unsafe extern "C" {
    safe fun abs(x: i32) -> i32;
    fun malloc(size: usize) -> *mut u8;
}

fun main() {
    let value = abs(-42);
    let pointer = unsafe { malloc(16) };
}

extern 声明描述一个确定的 C ABI 符号,因此不允许泛型参数。需要泛型封装时,应在普通 Riddle 泛型函数中调用具体的非泛型 FFI 声明。

C backend 不按符号名提供内置 C 函数;每个声明都会生成普通外部符号引用,由系统库、用户 C 代码或所选运行时负责链接。

safe fun 是声明者对整个调用契约的承诺;错误标记可能让安全代码触发未定义行为。

也支持导出 C ABI 函数:

extern "C" fun add(x: i32, y: i32) -> i32 {
    x + y
}

字符串 FFI 不接受裸 str 参数或返回值。&str 在 Riddle 内部是胖指针;调用只有声明、没有函数体的 C 导入时,C backend 会复制参数的字节到临时缓冲区并补一个尾部 NUL,再以 const char* 传出;临时指针只在本次调用期间有效,参数中不能含嵌入的 NUL。若导入返回 &str,返回指针必须以 NUL 结尾,长度由 strlen 恢复。

unsafe extern "C" {
    fun puts(s: &str) -> i32;
}

fun main() {
    unsafe { puts("hello from riddle"); }
}

带函数体的 extern "C" 是导出定义,不会再作为导入重复声明。它的 &str 参数和返回值保留 riddle_str { ptr, len } C 结构体 ABI,以免丢失长度:

struct riddle_str {
    const char *ptr;
    size_t len;
};

在 64 位目标上该结构体占 16 字节,在 32 位目标上占 8 字节。裸 str 没有独立的运行时值或布局。

unsafe、原始指针和 as

低层代码可以使用 unsafe fununsafe 块、原始指针类型和 as 转换:

unsafe extern "C" {
    fun my_alloc(size: usize) -> *const i32;
}

unsafe fun read(ptr: *const i32) -> i32 {
    unsafe { *ptr }
}

fun main() {
    unsafe {
        let p: *const i32 = my_alloc(16);
        let value = read(p);
        let n = 42 as f64;
    }
}

原始指针解引用、原始指针索引以及调用 unsafe fun 都必须位于 unsafe {} 中。unsafe fun 的函数体本身仍从安全上下文开始,内部不安全操作需要显式块。unsafe 不会关闭类型、可变性、move 或借用检查;原始指针不参与普通引用的借用跟踪。

不安全函数项只能在 unsafe {} 中直接调用,也不会满足安全的 FnFnMutFnOnce bound,因此不能借助安全 callable 参数绕过调用检查。

riddle-lsp

riddle-lsp 是 Riddle 的 Language Server Protocol 实现,基于 tower-lsp。它为编辑器提供实时诊断、补全、语义高亮、悬停、签名帮助、代码跳转、引用与重命名、符号搜索、Inlay Hint、格式化和代码折叠:

cargo run -p riddle-lsp

它通过 stdin/stdout 与编辑器通信。文档变化会经过短暂防抖,后台分析复用项目级增量语法树、函数体和类型检查缓存,只发布发生变化的诊断;语义请求只分析所属的 Clue 项目,并协作式取消过期分析。

  • 解析错误:来自词法/语法分析阶段;
  • HIR 诊断:包括 E0040(降级错误)、E0050/E0051/E0052(名字解析错误);
  • 类型检查诊断:E0001–E0034、E0072 等类型和 trait 检查错误;
  • 分析诊断:E0100 移动语义、E0300–E0308 与 E0310 引用、借用与逃逸约束。

诊断附带:

  • 次要标签(related information)指向关联位置;
  • 注释(notes)提供上下文和修复建议;
  • 严重性分层:Error、Warning、Information、Hint。

仓库中的 editors 目录提供 Helix、VS Code、Zed 和 IntelliJ IDEA 2026.1+ 客户端。完整的安装、路径配置、验证步骤和故障排查见编辑器与 LSP

MIR 后端架构

MIR 后端通过统一的 Backend trait 实现:

#![allow(unused)]
fn main() {
trait Backend {
    fn compile(&mut self, module: &Module) -> Result<String, Self::Error>;
    fn name(&self) -> &'static str;
}
}

目前实现并维护的后端:

后端文件状态
Ccrates/mir/src/backend/c.rsCLI 可用(--backend c

附录

附录只收录查阅型资料,不承担循序渐进的教学:

  • 当前工具链状态:当前仓库已经实现并测试覆盖的语言、标准库、后端和工具能力;
  • 形式化语法:声明、语句、表达式、类型和模式的文法;
  • 错误码参考:编译器各阶段错误码、原因与示例。

项目构建、编辑器和 FFI 已移到“工程与工具”部分,因为它们属于日常工作流,而不是语言附录。

当前工具链状态

Riddle 仍处于开发阶段。本页记录当前仓库已经实现并被测试覆盖的能力,避免把未来设计误当成可用功能。

编译流程

riddlec 可执行完整前端和基础后端流程:

  1. 词法分析和语法分析(IncrementalParser 提供局部重解析 API);
  2. AST 包装;
  3. HIR 降级(含 E0040/E0050/E0051/E0052 诊断);
  4. 作用域图构建和名字解析(基于片段的增量作用域图,支持部分失效);
  5. 类型检查(含可复用的 IncrementalTypeChecker);
  6. 逃逸分析(过程间不动点,决定局部值用栈分配还是 GC 堆分配);
  7. move checker(移动后使用、借用冲突、借用期间赋值/移动检查);
  8. HIR 到 MIR 降级(SSA 形式,Phi 节点,基本块,Alloca/HeapAlloc 分配指令);
  9. C 后端代码生成。

命令行入口支持:

riddlec [--verbose] [--no-std] [--backend c] [--target <triple>] [--output <file>] <file>...

--backend c 会生成调用 rgc ABI 的 C 代码;如需可执行文件,使用本机 ccgccclang 同时编译生成结果与发行包附带的 runtime.cargs_runtime.c(C 入口 main 会无条件初始化进程参数,因此 std::envargs() / args_os() 在任何包中调用都可用);生成代码的头部注释会给出完整链接命令。clue build 会自动完成这一步,不依赖 Boehm GC。多个输入文件会作为一个包合并编译,文件之间可以直接引用彼此的顶层条目。

riddle fmt 提供源码格式化和 --check 检查,LSP 格式化请求与该命令共享实现。

不指定后端时,riddlec 在完成 move/borrow 检查后停止;只有生成后端代码时才继续降级 MIR。

riddlec 会自动把 std/lib.rid 拼到用户源码后面,因此 std::marker::Copystd::clone::Clone 和比较、运算 trait 不需要手动定义。

MIR 中间表示

MIR(Mid-level IR)是 SSA 形式的中间表示,位于类型检查和代码生成之间:

  • SSA 基本块:每个函数体由基本块组成,块以 TerminatorBranchCondBranchReturnUnreachable)结束;
  • Phi 节点InstKind::Phi 合并来自多个前驱块的值;
  • 分配指令Alloca(栈分配)和 HeapAlloc(GC 堆分配),由逃逸分析结果驱动;
  • 内存操作LoadStoreFieldPtr(字段指针)、IndexPtr / CheckedIndexPtr(原始指针索引 / 安全数组与切片索引)、ExtractValue(提取聚合字段);
  • 值构造StructValueSparseStructValueArrayValueTupleValue;枚举使用稀疏初始化保证不同变体的 payload 槽位稳定;
  • 类型转换IntToIntIntToCharIntToFloatFloatToIntFloatToFloatBoolToIntIntToBoolIntToPtrPtrToPtr
  • 比较操作Cmp 支持 EqNeqLtGtLtEqGtEq
  • 函数值:可调用值统一为 { call, env, drop }FunctionRef 取得隐藏函数或命名函数适配器地址,CallIndirect 传入环境后调用;未逃逸的捕获环境使用栈存储,只有越过当前栈帧的环境才提升到 GC 堆。

MIR 类型系统包含 FnPtrPtrStructEnumTupleArraySliceStrNeverVoid,并为定长类型提供 size_bytes() 布局估算;裸 StrSlice 没有独立大小。

riddle-lsp

仓库包含 app/riddle-lsp,一个基于 tower-lsp 的 Language Server Protocol 实现:

  • 完整的诊断流水线:解析错误、HIR 诊断、类型检查错误、move/escape 分析诊断全部通过 LSP 推送;
  • 增量文本同步(TextDocumentSyncKind::INCREMENTAL);
  • UTF-16 位置编码(正确处理多字节字符如 emoji);
  • 多工作区管理与索引:发现每个工作区文件夹中的 Clue 项目,在内存中递归索引未打开文件的模块、类型成员、trait 方法、容器和可见性,并维护静态调用边和直接类型关系;文件或 manifest 变化只失效受影响的项目快照;
  • 补全(textDocument/completion):在 Clue 项目中加载模块和本地依赖,优先使用所有已打开文件的未保存内容;候选遵循词法作用域,包含参数、局部变量和模式绑定,并支持字段、实例方法、模块项、枚举变体、关联函数及导入别名;不可见的公开符号可生成独立 use path; 编辑完成自动导入,重名声明保留独立路径;
  • 悬停(textDocument/hover):显示函数签名、字段与参数类型、局部表达式的推断类型,以及声明前的文档注释或同行 //< 尾随文档;
  • 签名帮助(textDocument/signatureHelp):显示函数或方法签名、声明文档,并跟踪嵌套调用中的当前参数;
  • 声明、定义、类型定义与实现跳转(textDocument/declarationtextDocument/definitiontextDocument/typeDefinitiontextDocument/implementation):支持局部绑定、模块项、字段、方法及跨文件符号,并把 trait 调用分别映射到 trait 声明和具体 impl;
  • 静态调用层级与类型层级:调用边覆盖编译器能够静态确定的自由函数、命名函数值、固有方法和 trait 方法声明;类型层级连接直接 supertrait、子 trait 及 impl Trait for Type 的实现类型;
  • 项目级引用与重命名、文档高亮覆盖未打开模块和非文件 URI;文档符号按当前文档返回,工作区符号会合并已打开文档分析与项目 ProjectIndex 中未打开文件的符号;
  • 文档格式化与基于语法块的代码折叠;
  • Inlay Hint 同时提供推断的局部类型和可省略的调用参数名;
  • Code Action 可为可变闭包绑定补 mut,也可把不安全操作包入 unsafe 块;
  • 语义 Token(textDocument/semanticTokens/full),内置类型使用 keyword,区分自由函数、方法、struct、enum 和 trait,关联函数使用 method / static,标准库符号使用 defaultLibrary,并包含函数、参数和方法 declaration 及可变局部变量 declaration / mutable 标记;
  • 诊断区分主标签和次要标签(related information),错误码可跳转到错误码手册,注释和修复建议分别以 note: / help: 附加;
  • Clue 项目按原始文件 URI 发布诊断,包括未打开模块,并在重新分析后清理过期诊断;
  • 诊断严重性层级:Error、Warning、Information、Hint;
  • 文档变更会先合并短时间内的连续输入,再在后台运行诊断并协作式取消过期分析;未变化的文件和无关 Clue 项目直接复用诊断,变化的分析单元复用增量语法树、函数体和全局类型检查缓存,在声明、overlay、磁盘源码或 manifest 变化时保守失效;诊断在 move/borrow 检查后停止,不生成 MIR;UTF-16 位置通过行索引换算,语义 Token 使用包含未保存 overlay 的项目级 HIR,并按文档文本和分析修订缓存;
  • 支持动态注册 .ridClue.toml 文件监听,编辑器外部的源码、模块和 manifest 变更会触发项目缓存失效与重新诊断;
  • 仓库内提供 Helix、VS Code、Zed 和 IntelliJ IDEA 2026.1+ 的 .rid 文件与 riddle-lsp 适配;

工作区中的 Clue 项目会建立内存索引。补全可通过独立的 use path; 编辑自动导入可达的公开符号;调用层级只包含编译器能够静态解析的目标,不推测函数指针、闭包或 Trait 的运行时分派。

安装和验证步骤见编辑器与 LSP

当前语言特性

模块和名字解析

  • mod name { ... } 内联模块;
  • mod name; 外部模块声明的语法;
  • use path;use path as alias;
  • use path::*;
  • use path::{a, b as c};
  • pub 可见性,模块路径只导出 public 项;
  • pub use 重新导出;
  • selfsupercrate::root 风格路径;
  • 局部变量、参数、模块项、结构体、枚举变体、函数和 impl 方法的解析。

变量、函数和表达式

  • let 绑定,默认不可变;
  • let mut 可变绑定;
  • [x -> x + 1]move [x -> x + 1] 方括号匿名函数、参数推断和闭包捕获;支持参数类型标注、参数解构、零参形式与块体;泛型参数、where 子句、返回类型标注与自递归绑定不再属于匿名函数,需要时用具名函数表达(旧的 fun(x) { ... } 匿名函数语法已移除,编译器会给出指向方括号形式的诊断);
  • 参数和返回位置的一般 impl Trait,以及带调用签名的 impl Fnimpl FnMutimpl FnOnce
  • 按用法推断共享、可变和值捕获,精确追踪静态字段和元组元素,并据此检查 FnFnMutFnOnce 调用能力;
  • 每个匿名函数表达式、命名函数项和泛型函数实例具有独立的静态类型;
  • 显式类型标注;
  • 顶层和 impl 内的 const 声明(const NAME: Type = value;),初始化式会做类型、纯表达式和循环检查;
  • 模块和 impl 内的有值 type 别名,以及 trait 中可省略默认值的关联类型;
  • let 支持延迟初始化,首次赋值不要求 mut,并检查跨 ifmatch、循环的 definite-initialization;未初始化读取报 E0059,不可变绑定二次赋值报 E0031
  • 函数定义和函数声明;
  • 泛型函数(类型参数和 const 参数从实参与期望返回类型推断,支持 Rust 风格函数、方法及 Type::<T>::function::<U>() 显式参数、<T: Trait> bound、where 子句,C backend 单态化);
  • 函数参数、返回类型、尾表达式和 return
  • 块表达式;
  • 结构体字段、元组数字字段(.0.1 等)、函数调用和方法调用;
  • 数组字面量、数组重复表达式 [value; N]、数组与切片安全索引(越界终止并报告运行时错误);原始指针索引仍需 unsafe 且不做边界检查;
  • 结构体字面量和字段简写;
  • 类型转换表达式 expr as Type;支持安全的 u8 as char&str&[u8](*const T, usize) / (*mut T, usize)&[T]&[u8]&str 的 DST 等布局转换仅允许在 unsafe 中使用;
  • unsafe { ... } 块表达式,以及原始指针解引用和索引的安全上下文检查;
  • unsafe fun 函数和直接调用检查;不安全函数项不会满足安全的 Fn* bound;
  • unsafe extern "C" 导入块,块内默认不安全并支持 safe fun 显式安全声明;
  • 解引用 *expr

运算符

  • 算术:+-*/%
  • 比较:==!=<><=>=
  • 逻辑:&&||!
  • 位运算:&|^<<>>
  • 赋值:=
  • 复合赋值:+=-=*=/=%=&=|=^=<<=>>=
  • 一元:+-&&mut*!

C backend 对整数回绕、除零、最小值除以 -1、移位计数和浮点转整数使用确定性规则:整数算术按位宽回绕,错误除法终止,移位计数按位宽取模并对有符号右移使用算术语义,NaN 转整数为零且溢出值钳制到边界。

控制流和模式

  • if / else if / else 表达式;
  • if let 模式 = 表达式 { } else { },在 HIR 降级时脱糖为带 _ 通配臂的 match,绑定只在匹配成功的分支内可见;
  • let 模式 = 表达式 else { ... };,允许可反驳模式,失败分支必须发散(E0066),成功后的绑定进入外层作用域;
  • while 循环;
  • while let 模式 = 表达式 { },脱糖为 loop 内每次迭代重新求值的 match,匹配失败时 break
  • loop { } 无限循环表达式,break 值; 交出循环结果,所有 break 值类型合并为结果类型,无可达 break 时类型为 !
  • for item in iterable 循环,按 IntoIterator / Iterator 做类型检查,并在 MIR 中降成 into_iter / next 调用;循环头接受任意不可反驳模式(元组、结构体、通配符等),可反驳模式报告 E0057;当前元素、迭代器和提前退出路径具有独立的析构作用域;
  • 泛型参数可以通过 IntoIterator<Item = ..., IntoIter = ...> bound 使用 for,具体 impl 在单态化时解析;
  • 标准库 Range、固定长度数组 [T; N]、共享切片 &[T]、可变切片 &mut [T]&str 可直接用于 for,数组按值遍历且不要求元素类型为 Copy,字符串迭代产出 Unicode char
  • match 表达式,以及枚举、布尔值、()、整数、元组和结构体的递归穷尽性检查;
  • 非穷尽整数匹配会报告未覆盖的连续值区间;
  • match guard,guard 失败后继续检查后续 arm,且带 guard 的 arm 不计入静态穷尽性;
  • _ 通配模式;
  • 标识符绑定模式;
  • 字面量模式;
  • 路径模式;
  • 显式 &pattern / &mut pattern,支持嵌套引用模式且要求可变性精确匹配;
  • 元组模式;
  • 结构体模式;
  • 枚举 unit/tuple/struct 变体模式,payload 绑定会进入 guard 和 arm 表达式;
  • 引用 match ergonomics:结构化模式自动解引用 &T / &mut T,内部绑定继承共享或可变引用模式;裸绑定保留整个引用,且不提供 ref / ref mut 语法。默认绑定模式变为引用后,内部不能再写 mut binding 或显式引用模式。

类型系统

  • 整数:i8i16i32i64isizeu8u16u32u64usize
  • 浮点:f32f64
  • boolchar()!
  • str:不定长字符串类型,仅能作为引用、原始指针或 impl 的目标;
  • &str:引用 str 的定长胖指针值;
  • [T]:不定长切片类型,仅能位于引用或原始指针后;
  • &[T] / &mut [T]:携带元素地址和长度的胖指针,可由对应可变性的数组引用自动转换;
  • 引用:&T&mut T
  • 原始指针类型:*const T*mut T
  • 元组类型和元组表达式,例如 (2, 3)(2,)
  • 固定长度数组 [T; N]
  • const generics,例如 struct Buffer<T, const N: usize> { data: [T; N] }
  • 结构体;
  • 枚举;
  • 标准库 Option<T>Result<T, E>
  • 独立的匿名函数与命名函数项类型,以及静态 Fn / FnMut / FnOnce bound;
  • 泛型函数、泛型结构体、泛型枚举、泛型 impl;
  • 函数、trait、impl、结构体和枚举的泛型 bound:<T: Trait><T: A + B>where T: Trait
  • 类型参数实例化;
  • const 参数实例化,例如 Buffer<i32, 3>
  • 无空格嵌套泛型类型参数,例如 Box<Box<i32>>Box<Box<Box<i32>>>

Trait 和 impl

  • trait 定义;
  • 父 trait 声明、传递 bound、父方法查找、impl 前置依赖和继承环检查;
  • trait 方法签名;
  • trait 默认方法;impl 未覆写时使用默认体,显式覆写优先;
  • 关联类型声明和默认关联类型;
  • impl Trait for Type
  • 用户类型实现 Fn(参数...) -> 返回类型FnMutFnOnce,并静态调用其 call 方法;
  • impl Type 固有方法;
  • self&self&mut self 接收者;
  • 方法调用 value.method();方法查找失败时会回退为调用存储在字段里的可调用值(self.f(x)f 为闭包、函数项或带 Fn/FnMut bound 的泛型字段),impl 的 callable bound 由闭包签名结构化满足(Fn 值可用于 FnMut/FnOnce 需求);
  • 关联函数路径调用 Type::function(...)
  • Type::Assoc 关联类型路径;
  • trait impl 合约检查:缺少方法、参数类型、返回类型和缺少关联类型会报错;
  • 泛型 trait impl 模式匹配,例如 impl<T> std::marker::Copy for Box<T>
  • impl 上的 where 子句,并检查 Paterson condition:约束必须严格小于被实现的类型;
  • 算术、取余、位运算、移位、一元负号、逻辑非和复合赋值可通过对应的 #[lang = "..."] trait 为用户类型分派;
  • == / != 检查 PartialEq,有序比较检查 PartialOrd
  • 标准库 Iterator / IntoIterator 协议,含 std::ops::{Range, range}、数组 IntoIteratorfor 遍历。

属性和标准库内置项

Riddle 支持 Rust 风格外部属性,可放置在多项位置:

#[item]
struct Item {
    #[field]
    value: i32,
}

fun id(#[param] value: #[ty] i32) -> i32 {
    #[expr] value
}

match value {
    #[arm] Pattern => result,
}

属性当前会进入 AST/HIR。编译器识别 #[lang = "..."],用于把 trait 标记为编译器内置项。默认加载标准库时,该属性仅允许随编译器附加的标准库使用,用户包中出现会触发 E0049;使用 --no-std 时,参与编译的包可以为自定义 core 定义 lang item,编译器仍会检查名称、目标、固定签名和重复注册。

Clue 支持 #[proc_macro_derive(Name, attributes(...))]#[proc_macro_attribute]#[proc_macro] 导出的 Riddle 过程宏。过程宏包由 [lib] proc-macro = true 标记并为宿主平台构建,也可以依赖并使用另一个过程宏包。宏可通过分组、别名、通配符或 pub use 导入独立的宏命名空间,混合 use 会保留普通名称;derive 只允许放在结构体或枚举上,Riddle 当前没有 union 条目。函数式宏使用 name!() 语法,可出现在表达式、条目、类型和模式位置。宏函数接收由 GroupIdentPunctLiteral 组成的递归 TokenStream;输入、输出、诊断和 span 通过带版本的长度前缀结构化协议传递,输出 token 直接进入解析器。复制到输出的 token 会保留源位置,生成代码中的宏会继续展开,最大深度为 32。过程宏包内置 synquote!syn 提供结构化 DeriveInput、Riddle 语法分类、ParseToTokensVisitFoldquote! 支持插值、重复和等长向量配对。LSP 同步支持宏高亮、悬停、定义、引用、别名重命名和补全。

当前标准库会自动拼到用户源码后面,根部通过 prelude 重导出常用项,同时按 Rust 风格分模块定义:

prelude 只直接提供 OptionResultStringVectorSomeNoneOkErrCopyCloneDropdropDefaultInto、比较 trait 和迭代协议。集合、格式化 trait、具体迭代器、区间、解析、时间及底层输出函数需要从各自模块显式导入;标准宏命名空间隐式提供 DebugCloneCopyDefaultHashPartialEqEqPartialOrdOrd 派生,格式化与输出宏,以及 assert! / assert_eq! / assert_ne!、对应的 debug_assert* 宏、todo!unimplemented!unreachable!

  • std::option::Option<T>,提供 is_someis_noneunwrapunwrap_ormapand_thenor
  • std::result::Result<T, E>,提供 is_okis_errunwrapunwrap_ormapand_thenokerr
  • std::ffi::OsString 无损保存平台字符串;std::env::args_os() 在 Unix 保存原始参数字节,在 Windows 解析 GetCommandLineW 并以 WTF-8 保存 UTF-16,std::env::args() 则严格转换为 String,遇到非 Unicode 参数时 panic;
  • print! / println! 通过隐藏的标准库输出入口和 std::fmt::{Debug, Display, Formatter, Result} 支持字符串、布尔、字符、整数和浮点标量;格式化 trait 不在 prelude 中,底层输出入口不属于用户 API。DebugDisplay 都使用 fmt(&self, formatter: &mut Formatter) -> Result,字符串和字符的 Debug 输出会添加引号并转义;标准派生支持结构体、泛型结构体以及 unit、tuple、named 三类枚举变体,当前包括 DebugCloneCopyDefaultHashPartialEqEqPartialOrdOrd,并为泛型参数生成相应 bound;枚举 Default 要求恰好一个带 #[default] 的 unit 变体,排序派生按变体声明顺序和 payload 字典序工作;Copy impl 会验证所有字段和 payload,比较派生仍需满足父 trait;OptionResultStringVectorHashMapHashSetTreeMapTreeSet 均通过 Debug 派生实现格式化;print! / println! 支持空调用,format! 要求字符串字面量并返回 Stringpanic!() 使用 explicit panicpanic!(...) 在终止前格式化消息;四个宏都支持字符串字面量、{} / {:?} / {0} 位置参数 / {name} 命名捕获、尾随逗号以及 {{ / }},并在编译期校验格式串;{} 按从左到右的顺序消费参数,{0} 可重复引用任意参数,{name} 隐式捕获调用处的同名局部变量;宽度、对齐等其他格式说明符尚未实现;
  • assert!assert_eq!assert_ne! 及对应的 debug_assert* 宏复用 panic!;比较断言只求值两侧一次并显示 Debug 值,自定义消息仅在失败路径求值。todo!unimplemented!unreachable! 返回 ! 并保留调用位置;当前所有构建都会执行 debug assertion;
  • vec! 宏支持三种形式:vec![a, b, c] 构造 Vector 并逐个 push(元素按值移动,支持尾随逗号与嵌套 vec!),vec![elem; count] 展开为 Vector::from_elem(elem, count)(要求元素实现 Clone,为每个槽位克隆),空 vec![] 展开为 Vector::new() 块并由上下文推断元素类型(无法推断时报告类型错误);Vector::from_elem 是公开的标准库 API;
  • std::string::String 提供 newfrom_stras_strlencapacityis_emptypush_strpush_charclearsplitreplaceto_ascii_uppercaseto_ascii_lowercasesplit 返回 Vector<String>,空分隔符行为与 find 一致);同一模块按 Rust 风格为 str 提供 lenis_emptyas_bytescontainsfindstarts_withends_withslicetrimsplitreplaceto_ascii_uppercaseto_ascii_lowercase 和按 Unicode char 遍历的 StrIter
  • std::vector::Vector<T> 提供 newlencapacityis_emptypushpopinsertremovegetget_mutswapsort(要求 T: PartialOrd,插入排序)、contains(要求 T: PartialEq)、retainclearas_slice、读写下标和按值迭代; Vector<T> 另提供 from_iterator(把任意迭代器收集为向量)和 from_elem(value, count)(要求 T: Clonevec![value; count] 的底层实现);下标越界调用 panic,缓冲区通过运行时 rgc_reallocrgc_free 管理;
  • Vector<T> 会拒绝零大小元素并检查容量乘法溢出;同点原始指针支持 == / != 按地址比较,p == 0usize as *const T 可用于空指针检查;
  • std::iter::{Iterator, IntoIterator}Iterator 提供默认方法 countnthfoldfor_eachallanyfindposition,以及惰性的 map / filter(通过闭包字段适配器实现,可链式组合并支持 for 遍历);std::iter 另提供急切求值的 map_into / filter_into(返回 Vector)与适配器构造函数 enumerate / take / zip / skip,以及 min / max(返回 Option<Item>,要求 Item: PartialOrd);Iterator::collect 可把任意迭代器收集为 Vector<Self::Item>Vector::from_iterator 与之等价;DoubleEndedIterator 提供 next_back,切片迭代器 SliceIter 支持从尾部遍历;
  • std::slice::{SliceIter, SliceIterMut},并为 [T] 提供长度、边界检查访问、原始指针访问和借用迭代;
  • std::array 中的按值、共享借用和可变借用数组迭代器;
  • std::ops::{Range, range(start, end)};范围表达式 a..b 脱糖为 range(a, b)a..=b 脱糖为 range_inclusive(a, b)std::ops::RangeInclusive,含单元素与空区间语义);
  • std::marker::Copy
  • std::clone::Clone
  • std::cmp::{Ordering, PartialEq, Eq, PartialOrd, Ord}
  • std::ops 下的算术、位运算、移位、复合赋值以及 Index / IndexMut trait,均有可调用的必需方法;这些 trait 由对应 #[lang = "..."] 标记,用户类型的下标操作静态分派到 index / index_mut
  • std::default::Default 为标量、Option<T>StringVector<T> 提供默认值;Default::default() 支持按期望类型静态选择 impl;
  • std::convert::Into<T>? 错误传播使用的错误转换协议;std::convert::From<T> 已提供,? 在没有 Into impl 时回退查找 From impl(Rust 风格错误链路),且 ? 同样支持 Option<T> 操作数(在返回 Option 的函数中把 None 提前返回);
  • std::hash::Hash 通过共享借用为标量提供确定性的 usize 哈希值;
  • std::collections::{TreeMap, TreeSet} 使用红黑树,键要求实现 Ordstd::collections::{HashMap, HashSet} 使用开放寻址哈希表、线性探测和负载扩容,键要求实现 Hash + Eq;四类集合都提供 remove:HashMap 采用线性探测的后移删除(backward-shift deletion),TreeMap 采用带删除修复(delete fixup)的 CLRS 红黑树删除并压缩 arena 槽位;对应实现模块位于 std::collections::{tree_map, tree_set, hash_map, hash_set}HashMap::get_or_insert(key, default) 返回已有值或插入默认值后的可变引用;
  • std::parse 提供 parse_i32 / parse_i64 / parse_u64 / parse_usize(十进制、溢出安全)与 parse_with_radix(2–36 进制);std::time::time_now 转发到 C timeDuration::from_secs / from_millissleep 转发到 riddle_sleep_ms
  • std::fs::FsFile 通过运行时提供的 riddle_fs_* 薄包装(避免与 <stdio.h> 原型冲突)访问 C stdioopen / create / append / read / write / flush / read_to_stringDrop 保证关闭句柄;std::fs::{read_to_string, write} 提供整文件便捷读写;std::fs::{exists, metadata, read_dir} 提供存在性检查、FileMetadata { size, is_file, is_dir } 元数据查询和目录条目枚举(read_dir 返回 Vector<String>,跨平台由 Win32 FindFirstFile / POSIX dirent 支撑);? 可直接在这些 Result<FsError> API 间传播;
  • std::random 提供 random_u32 / random_u64 / random_bool / random_below,由 riddle_random_u32 / riddle_random_u64 运行时垫片支撑(Windows 使用 GetTickCount 种子的 xorshift,POSIX 读取 /dev/urandom);std::ptr 场景下同点原始指针可用 == / != 按地址比较,p == 0usize as *const T 即空指针检查;

DefaultHash、标量格式化和基础集合/解析/时间 API 已经具备可执行行为;整数解析会拒绝空串、非法字符和超出目标范围的输入。

当前影响编译器语义的 lang trait 包括:

  • #[lang = "copy"]:被它标记的 Copy trait 会被 move checker 用来决定用户类型是否按复制语义处理;
  • #[lang = "drop"]:被它标记的 Drop trait 提供确定性析构;Drop + Copy、直接调用析构方法和从显式 Drop 类型移出字段会被拒绝;
  • #[lang = "add"]#[lang = "shr"]:用户类型的算术、位运算和移位会分派到对应 trait 方法;标量 impl 的方法调用直接降为 MIR 运算;
  • #[lang = "neg"]#[lang = "not"]:用户类型的一元负号和逻辑非会分派到对应 trait 方法;标量 impl 的方法调用直接降为 MIR 运算;
  • #[lang = "add_assign"]#[lang = "shr_assign"]:用户类型的复合赋值会分派到对应 trait 方法;标量 impl 的方法调用直接降为 MIR 的读取、运算和写回;
  • #[lang = "index"]#[lang = "index_mut"]:非内建下标读取和可变位置分别静态分派到 Index::indexIndexMut::index_mut;数组、切片和裸指针保留原有直接索引路径;
  • #[lang = "partial_eq"]:用户类型的 == / != 分派到 PartialEq::eq / ne
  • #[lang = "partial_ord"]:用户类型的 <><=>= 分派到 PartialOrd::ltgtlege

Clone::clonePartialEq::eqPartialOrd::partial_cmpOrd::cmp 和各运算 trait 方法可以直接调用。带受支持 lang 标记的标量运算方法不会生成 add__i64 一类 C 包装函数,而是生成原生 C 运算表达式。未标记的同名 trait 仍按普通方法编译;用户类型的运算符会调用对应 trait impl 或默认方法。

二元、复合赋值和比较 trait 支持 Rhs = Self 默认类型参数以及异构右操作数 impl;泛型约束中的运算符调用在单态化后静态选择具体 impl。赋值求值顺序与 Rust 一致:普通赋值和内建复合赋值先右后左,重载复合赋值先左后右。

所有权、移动和逃逸

  • 值默认移动;
  • ? 接受 Result<T, E>Option<T>:成功分支继续当前函数;错误分支通过 Into(无 Into impl 时回退 From)转换后返回外层 ResultOption 操作数则在返回 Option 的函数中把 None 提前返回;
  • 标量、共享引用、原始指针和命名函数项等内置 Copy 候选默认可复制;&mut T 与闭包值不可复制;
  • Option<T>Result<T, E> 仅在所有 payload 类型实现 Copy 时实现 Copy
  • 用户类型可以通过实现 std::marker::Copy 进入复制语义;编译器会验证结构体字段和所有枚举 payload,并在泛型场景中使用 impl bound;
  • move checker 检查移动后使用;
  • 借用期间移动会报错;
  • 方法和函数返回值会传播引用来源,包含 Option<&T> 等泛型包装;元组和数组的来源按元素保留,模式解构不会让无关元素互相延长借用;
  • 引用参数支持自动重借用,局部借用可在最后一次使用后结束;
  • 模式生成的字段重借用按投影分别追踪;子借用存活时冻结父可变引用,显式引用模式复制 Copy 内容而不移动引用;
  • 字段访问本身不会移动整个结构体;
  • 数组元素和结构体字段按值移动;
  • match 解构按字段记录部分移动,未移动的兄弟字段仍可继续使用;
  • 引用逃逸分析通过过程间的“外泄参数 / 返回来源参数”摘要,决定局部值使用栈分配还是 GC 堆分配。
  • 共享/可变闭包捕获会让对应局部获得稳定地址;静态字段和元组元素按投影独立捕获,动态索引与解引用在无法继续静态细分的位置停止;闭包未逃逸时使用栈存储,闭包越过当前栈帧时才提升到 GC 堆,且分配位置不会放宽移动和借用检查;
  • move [...] 按值捕获所有使用到的外部位置;Copy 值仍复制,按值捕获本身不会强制闭包成为 FnOnce
  • Copy 值捕获会在创建闭包时移动该值,FnOnce 闭包调用后不可再次使用。
  • 需要析构的局部、参数、模式绑定、迭代元素、聚合字段和闭包值使用 drop flag 防止移动后的重复析构;逃逸到 GC 堆只改变地址,仍在所有者结束时确定性运行 Drop
  • GC 运行时(runtime.c)对栈执行保守式扫描,从 rgc_init 记录的栈底开始向上标记;堆对象记录在动态注册表中,精确指针查找走地址哈希表,内部指针标记按每次回收重建的地址有序索引二分查找,清扫只访问已注册槽位,回收阈值随存活集合增长(next = max(1 MiB, live * 2)),RGC_DEBUG_STATS=1 会向 stderr 打印回收统计。运行时能处理引用位于寄存器或栈缝中的常见情况,但依赖编译器在标记期间把活引用保持在可扫描的内存中,且未逃逸值(栈上)不参与堆回收;公开的 rgc_* ABI 保持不变。

字符串和 FFI

  • str 是不定长类型,不能作为局部变量、参数、返回值或普通字段;
  • &str{ ptr, len } 胖指针,字符串字面量的类型也是 &str
  • 字符串字面量支持 "..."r"..."r#"..."#r###"..."###
  • extern "C" 支持声明块和带函数体的导出定义;
  • C 导入中的 &str 映射为 const char*,调用点会复制并补齐 NUL,临时指针只在调用期间有效且输入不能含嵌入 NUL;显式 #[c_export] 包装函数也使用该参数 ABI,边界另一侧必须提供 NUL 终止的数据;需要保留长度时应显式传递指针和 usize;带函数体的既有 extern "C" 定义和普通 Riddle 函数仍使用 { ptr, len }
  • C backend 只会在实际调用 C 字符串导入时生成内部的 NUL 终止桥接 helper;除此之外不按函数名提供内置 C helper,所有 extern "C" 声明都按普通外部符号生成;
  • 标准库通过 as_bytes().len() 实现 str::len,并用受限的同布局转换实现 &str / &[u8] 转换;String::as_str 先借用 Vector<u8>&[u8],再通过普通标准库 unsafe 函数转换为 &str,不使用函数 builtin,也不生成或链接 C helper;
  • StringVector<u8> 持有 UTF-8 字节,支持追加、清空和借用为 &str;存活的 as_str() 视图会阻止可能使其失效的可变操作。

后端状态

后端状态
C backendCLI 可用:--backend c。输出使用 rgc 运行时 ABI;默认 provider 由 clue 选择,也支持自定义 provider

C backend 实现统一的 Backend trait:compile(&mut self, module: &Module) -> Result<String, Self::Error>

C backend 会把标量 std 运算 trait 的显式方法调用直接输出为带确定性溢出、除法和移位保护的 +-*&<< 等 C 表达式,不声明或定义对应的 primitive wrapper;用户类型的 trait 方法仍输出普通 C 函数。

工具状态

工具状态
riddle fmt源码格式化 CLI,支持文件、标准输入、--check、缩进宽度和硬制表符;与 LSP 复用 formatter
riddlec编译器 CLI,支持前端检查、MIR 降级和 C backend
riddle-lspLSP 服务器,基于 tower-lsp,提供诊断、补全、悬停、签名帮助、符号导航、引用、重命名、格式化、Inlay Hint 和语义 Token,并识别过程宏命名空间
clue包管理器和项目构建器,支持项目、workspace、path/git/registry 依赖、锁文件、features、test/bench、打包发布与安装;二进制项目会保留 C 并生成本机可执行文件,库项目可生成 .rmeta.rlib、静态库和动态库,过程宏依赖构建为宿主进程

当前限制

  • 标量类型限于 C11 可移植表示:i128u128f16f128 在词法上可写,但类型检查会拒绝并给出诊断,语义上不存在这些宽类型;
  • 进程参数 std::env::args() / args_os() 需要链接 args_runtime.c(见上文编译流程);C 入口 main 无条件调用 riddle_args_init,因此参数在任意包中使用都可用;
  • 当前定位为单线程语言:线程 / 互斥锁 / 原子变量 / async / await / 网络尚未实现;开区间范围(a.. / ..b)、范围模式(match 中的 a..=b)、循环标签、Rc/Arc/Cell/RefCell 等智能指针与内部可变性也尚未实现;match guard 目前只在 match 中提供,let 解构与解构赋值已直接支持;
  • 数字解析不支持十六进制浮点;整数已支持 0x / 0o / 0b 前缀与 _ 分隔符;
  • 泛型目前偏向单态化,尚未覆盖完整 Rust 泛型能力;
  • riddlec 的 C backend 只输出 C;clue build 会严格使用 CC,或自动选择能完成 C11 编译和链接的系统 C 编译器来生成本机可执行文件;
  • 逃逸分析会沿结构体、元组和数组字段传播引用来源;字段模式绑定可以单独提升到 GC 堆,只有根绑定或无法静态细分的访问才提升整个存储槽;
  • TODO:数组 IntoIterator 当前按索引顺序产出元素;若未来允许自定义数组迭代器乱序移出元素,需要先加入 MaybeUninit / ManuallyDrop 等价存储和逐槽存活状态,确保剩余元素只析构一次;
  • trait 方法支持对象安全的 &dyn Trait / &mut dyn Trait 借用对象和拥有所有权的 dyn Trait 值;拥有值使用数据指针、方法表和类型专属 drop 槽位,并在 GC / no-GC runtime 下分别使用 rgc_alloc / rgc_freeriddle_alloc / riddle_free;拥有对象可以重借用为 &dyn Trait,父 trait 支持对象向上转型,泛型参数可在满足 trait bound 时转换为拥有对象,数组字面量会逐元素应用转换;跨父 trait 的同名方法拒绝为歧义,非对象安全方法会明确报告原因;dyn Fndyn FnMutdyn FnOnce 支持拥有值与借用值,并复用 callable ABI;仍不支持带泛型方法的动态对象或异构可调用值容器;
  • 匿名函数不支持泛型参数、返回类型标注或自递归绑定,需要时使用具名泛型函数;带泛型方法的动态对象或异构可调用值容器仍未支持;
  • 这是开发中工具链,不保证语法和 ABI 稳定。

形式化语法

以下文法描述当前普通解析器接受的主要语法,使用扩展 BNF 记法。属性宏和派生宏会在普通解析前展开;宏参数因此以平衡 token tree 表示,而不在这里展开其内部语法。

program = statement*;

attribute = "#" "[" attribute_body "]";
attribute_body = balanced tokens until matching "]";

statement =
    attribute* (
    use_decl
  | mod_decl
  | extern_block
  | extern_fn_decl
  | enum_decl
  | trait_decl
  | impl_decl
  | const_decl
  | type_alias_decl
  | var_decl
  | func_decl
  | struct_decl
  | break_stmt
  | continue_stmt
  | return_stmt
  | expr_stmt
  );

// == FFI ==

extern_block = "pub"? "unsafe" "extern" string_lit "{" (attribute* extern_func_sig)* "}";

extern_func_sig = ("safe" | "unsafe")? "fun" ident "(" (param ("," param)*)? ")" ("->" ty)? ";";

extern_fn_decl = "pub"? "unsafe"? "extern" string_lit func_def;

// == module / use ==

mod_decl = "pub"? "mod" ident (";" | "{" statement* "}");

use_decl = "pub"? "use" use_tree ";";

use_tree =
    path (("as" ident) | ("::" "*") | ("::" "{" use_tree ("," use_tree)* ","? "}"))?
  | "{" use_tree ("," use_tree)* ","? "}";

// == items ==

enum_decl = "pub"? "enum" ident item_generic_params? where_clause? "{" (enum_variant ("," enum_variant)* ","?)? "}";
enum_variant = attribute* ident (("(" type_list? ")") | ("{" struct_field_list? "}"))?;

trait_decl = "pub"? "trait" ident trait_generic_params? (":" generic_bound ("+" generic_bound)*)? "{" trait_item* "}";
trait_item = attribute* (func_decl | assoc_type_decl);

impl_decl = "impl" generic_params? ty ("for" ty)? where_clause? "{" impl_item* "}";
impl_item = attribute* (func_decl | type_alias_decl | const_decl);

func_sig = "unsafe"? "fun" ident generic_params? "(" (param ("," param)*)? ")" ("->" ty)? where_clause?;
type_alias_decl = "pub"? "type" ident "=" ty ";";
assoc_type_decl = "pub"? "type" ident ("=" ty)? ";";
const_decl = "pub"? "const" ident ":" ty "=" expression ";";

item_generic_params = "<" item_generic_param ("," item_generic_param)* ">";
item_generic_param = ident | "const" ident ":" ty;

generic_params = "<" generic_param ("," generic_param)* ">";
generic_param = ident (":" generic_bound ("+" generic_bound)*)? | "const" ident ":" ty;
trait_generic_params = "<" trait_generic_param ("," trait_generic_param)* ">";
trait_generic_param = ident (":" generic_bound ("+" generic_bound)*)? ("=" ty)? | "const" ident ":" ty;
generic_bound = callable_bound | path ("<" generic_bound_arg ("," generic_bound_arg)* ","? ">")?;
generic_bound_arg = ident "=" ty | ty;
callable_bound = ("Fn" | "FnMut" | "FnOnce") "(" type_list? ")" "->" ty;
where_clause = "where" where_predicate ("," where_predicate)* ","?;
where_predicate = ty ":" generic_bound ("+" generic_bound)*;

type_args = "<" type_list? ">";
type_list = ty ("," ty)* ","?;

// == normal statements ==

var_decl = "let" pattern (":" ty)? ("=" expression ("else" block)?)? ";";

param = attribute* ((("&" "mut"?)? "self") | ("mut"? ident ":" ty));

func_def = "fun" ident generic_params? "(" (param ("," param)*)? ")" ("->" ty)? where_clause? block;
func_decl = "pub"? "unsafe"? "fun" ident generic_params? "(" (param ("," param)*)? ")" ("->" ty)? where_clause? (block | ";");

block = "{" statement* expression? "}";

struct_param = attribute* "pub"? ident ":" ty;

struct_decl = "pub"? "struct" ident item_generic_params? where_clause? "{" (struct_param ("," struct_param)* ","?)? "}";

break_stmt = "break" expression? ";";
continue_stmt = "continue" ";";
return_stmt = "return" expression? ";";

expr_stmt = expr_without_block ";" | expr_with_block ";"?;

// == expression ==

expression = expr_with_block | expr_without_block | range_expr;

expr_with_block = block | if_expr | while_expr | loop_expr | for_expr | match_expr | unsafe_expr;

if_expr = "if" (expression | let_condition) block ("else" (if_expr | block))?;

while_expr = "while" (expression | let_condition) block;

let_condition = "let" pattern "=" expression;

loop_expr = "loop" block;

for_expr = "for" pattern "in" expression block;

match_expr = "match" expression "{" match_arm ("," match_arm)* ","? "}";
// 块体 arm(`=> block`)的 `}` 自身终止该 arm,其后的逗号可省略。
match_arm = attribute* pattern ("if" expression)? "=>" expression;

unsafe_expr = "unsafe" block;

expr_without_block = unary (("as" ty) | (binop unary))*;

// 区间表达式:右结合,优先级低于 `||`;`for` 头部与语句表达式位置可用
range_expr = expr_without_block (".." | "..=") expr_without_block;

lambda_param = pattern (":" ty)?;

// 方括号 lambda:`[it -> it * 2]`。判别规则:`[` 组内嵌套深度 0 处出现 `->`
// 即为 lambda,否则为数组字面量。`expr [params -> body]`(后缀位置)表示
// 以该 lambda 为实参调用 `expr`(通常是方法,如 `values.map [v -> v * 2]`)。
bracket_lambda_expr = "move"? "[" bracket_lambda_body "]";
bracket_lambda_body = (lambda_param ("," lambda_param)*)? "->" expression;

unary = prefix_op unary | postfix;

postfix = primary ( "::" "<" type_arg_list ">" "(" arg_list ")" | "(" arg_list ")" | "." (ident | number) | "[" (expression | bracket_lambda_body) "]" | struct_expr_fields | "::" "<" type_arg_list ">" struct_expr_fields | "." ident "(" arg_list ")" | "?" )*;

arg_list = (expression ("," expression)*)?;

type_arg_list = type_list;

primary = literal | macro_call | path | array_expr | tuple_expr | bracket_lambda_expr | "(" expression? ")";

macro_call = path "!" token_tree;
token_tree = "(" balanced_tokens ")" | "[" balanced_tokens "]" | "{" balanced_tokens "}";
balanced_tokens = balanced token sequence;

tuple_expr = "(" expression "," ")"
           | "(" expression ("," expression)+ ","? ")";

array_expr = "[" "]"
           | "[" expression ("," expression)* ","? "]"
           | "[" expression ";" expression "]";

struct_expr = path struct_expr_fields;
struct_expr_fields = "{" (struct_expr_field ("," struct_expr_field)* ","?)? "}";

struct_expr_field = ident (":" expression)?;

pattern = attribute* ("_" | "mut"? ident | literal | macro_call | path | reference_pattern | tuple_pattern | struct_pattern | enum_pattern);

reference_pattern = ("&" "mut"? | "&&" "mut"?) pattern;
tuple_pattern = "(" (pattern ("," pattern)* ","?)? ")";
struct_pattern = path "{" (field_pattern ("," field_pattern)* ","?)? "}";
field_pattern = attribute* ident (":" pattern)?;
enum_pattern = path | path "(" (pattern ("," pattern)* ","?)? ")" | path "{" (field_pattern ("," field_pattern)* ","?)? "}";

// Precedence & Associativity (Pratt binding powers)
//
// Assignment:
//   = += -= *= /= %= &= |= ^= <<= >>=   right-assoc       (lbp=1,  rbp=1)
//
// Prefix (right):  + - & && * !       rbp = 14
//
// Postfix:
//   () . [] ?   left-assoc    (lbp = 15)
//
// Infix:
//   as                            (lbp=13, rbp=13)
//   *  /  %        left-assoc        (lbp=12, rbp=13)
//   +  -           left-assoc        (lbp=10, rbp=11)
//   & | ^ << >>    left-assoc        (lbp=9,  rbp=10)
//   <  >  <=  >=   left-assoc        (lbp=8,  rbp=9)
//   ==  !=         left-assoc        (lbp=6,  rbp=7)
//   &&             left-assoc        (lbp=4,  rbp=5)
//   ||             left-assoc        (lbp=2,  rbp=3)
//
// Range:
//   .. ..=         right-assoc       (lbp=1,  rbp=2)
//
// In `if`, `while`, `for`, and `match` heads, struct expressions are disabled so
// `if Foo { ... }` keeps parsing `{ ... }` as the control-flow block.

// == path / type ==

path_segment = (ident | "self" | "super" | "crate") ("::" type_args)?;
path = ("::")? path_segment ("::" path_segment)*;

ty = attribute* (
     "!"
   | macro_call
   | path type_args?
   | "&" "mut"? ty
   | "&&" ty
   | "*" ("const" | "mut") ty
   | "[" ty (";" expression)? "]"
   | int_lit
   | impl_callable_type
   | dyn_trait_type
   | "(" (ty ("," ty)* ","?)? ")"
   );

impl_callable_type = "impl" callable_bound;

dyn_trait_type = "dyn" generic_bound;

// == operators ==

prefix_op = "+" | "-" | "&" ("mut")? | "&&" | "*" | "!";

binop = "=" | "+=" | "-=" | "*=" | "/=" | "%=" | "&=" | "|=" | "^=" | "<<=" | ">>="
      | "||" | "&&" | "==" | "!=" | "<" | ">" | "<=" | ">="
      | "|" | "^" | "&" | "<<" | ">>"
      | "+" | "-" | "*" | "/" | "%";

// == literals ==

literal = int_lit | float_lit | string_lit | char_lit | bool_lit;

int_lit = (dec_lit | hex_lit | oct_lit | bin_lit) int_suffix?;
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]+)? ("f16" | "f32" | "f64" | "f128")?;
string_lit = "\"" ... "\"" | "r" "#"* "\"" ... "\"" "#"*;
char_lit = "'" ... "'";
bool_lit = "true" | "false";

ident = [a-zA-Z_][a-zA-Z0-9_]*;

&pattern&mut pattern 分别解构一层同可变性的共享引用和可变引用;它们可以嵌套,例如 &&mut value。Riddle 不提供 ref nameref mut name 绑定语法。结构化模式匹配引用时会自动解引用并让内部绑定继承引用模式;默认绑定模式变为引用后,内部不能再写 mut binding 或显式 &pattern / &mut pattern。需要显式引用模式时,应让它出现在默认 move 模式的位置。详见枚举、模式与 match

词法层接受 i128u128f16f128 字面量后缀,但类型系统只支持 8 到 64 位整数与 f32 / f64;使用不支持的尾缀会报告 E0011。整数字面量支持十进制、0x 十六进制、0o 八进制、0b 二进制和 _ 分隔符。

let 可以在声明处初始化,也可以省略初始化式并稍后赋值:let pattern = expression;let pattern: Type; 和可由首次赋值推断类型的 let pattern; 都是合法语法。带初始化式的 let 还可以写 let-else:let pattern = expression else block;,模式不可反驳时必须发散的 else 块负责提前退出;else 块不发散会报告 E0066。延迟初始化的绑定必须在每条到达使用点的路径上先完成赋值;否则会报告 E0059。引用解构依赖初始化式的值类别,因此不能用于延迟初始化声明。type Name; 只用于 trait 中声明关联类型;模块和 impl 中的类型别名需要写出 = Type

::<...> 类型实参在类型位置可以直接书写;作为表达式时,只有后面紧跟 ((调用)或 {(结构体字面量)才会被解析,Foo::<i32> 这样的裸 turbofish 表达式会被拒绝。

Riddle 错误码参考

类型检查 (E0001–E0013, E0031–E0047, E0054–E0058, E0060–E0067, E0072, E0391)

E0001 — 类型不匹配

赋值、函数参数、返回值的类型与预期不符。

let x: i32 = "hello";  // E0001: expected i32, got &str

E0002 — 分支类型不兼容

if 各分支或 match 各 arm 的返回类型不一致。

let x = if cond { 1 } else { "hello" };  // E0002: incompatible types: i32 and &str

数组重复表达式 [value; length] 的长度必须是能适配 usize 的整数字面量,不满足时也报告本错误。

E0003 — 运算符操作数类型不匹配

运算符对操作数类型有各自的约束:算术运算符要求数值类型,%<<>> 和其余位运算要求整数(位运算也接受 bool),排序比较要求可比较的数值、char 或实现了 PartialOrd 的类型;相等比较走 PartialEq,不满足时报 E0036

let x = true + 1;  // E0003: left operand must be numeric, got bool

E0004 — 不能调用非函数值

对不可调用的值进行了函数调用。

let x = 42;
x();  // E0004: cannot call value of type i32

E0005 — 函数参数数量不匹配

调用函数时传入的参数数量与声明不符。

add(1);  // E0005: function expects 2 arguments, got 1

泛型调用无法从实参与期望返回类型推断类型参数时,也会报告本错误(cannot infer type argument(s))。

E0006 — 未知字段

访问或初始化结构体中不存在的字段。

let p = Point { x: 1, z: 2 };  // E0006: unknown field `z` on struct `Point`

E0007 — 缺少字段

结构体字面量缺少必填字段。

let p = Point { x: 1 };  // E0007: missing field `y` in struct literal `Point`

E0008 — 不能解引用

对非指针/非引用类型使用了 * 解引用运算符。

let x = *42;  // E0008: cannot dereference value of type i32

E0009 — 结构体字面量未解析

结构体字面量的路径无法解析到结构体定义。

let x = UnknownType { a: 1 };  // E0009: struct literal does not resolve to a struct

结构体字面量的类型参数数量错误、未知枚举变体,以及给非 struct 风格的枚举变体使用结构体字段时,也报告本错误。

E0010 — 模式不匹配

matchlet 的模式与值的类型不兼容。

let (x, y) = 42;  // E0010: tuple pattern cannot match value of type i32

引用模式还要求引用层数和可变性匹配。结构化模式自动解引用后已经进入引用绑定模式时,内部不能再写 mut binding 或显式引用模式;引用解构也不能用于没有初始化式的延迟声明。

E0011 — 无效字面量后缀

整数或浮点数字面量的类型后缀无效。词法器会接受 i128u128f16f128 后缀,但类型系统不存在这些宽类型,使用它们会得到明确的“不支持”提示;其余无法识别的后缀报告“未知后缀”。

let x = 42i128;  // E0011: integer literal suffix `i128` is not supported
let y = 3.5f16;  // E0011: float literal suffix `f16` is not supported
let z = 42i99;   // E0011: unknown integer literal suffix `i99`

注意 _ 不是合法的后缀分隔符:42_i99 会被词法拆成 42_i99 两个 token,报的是解析错误而不是 E0011。

E0012 — 不支持的类型转换

as 的常用安全转换包括整数之间、整数与浮点数之间、浮点数之间、布尔值与整数、char 到整数、u8char、整数到原始指针、原始指针之间,以及 &str&[u8]。原始 parts 到切片、&[u8]&str 等布局转换只允许在 unsafe 中使用。其他源类型与目标类型组合会报告此错误。

E0013 — 未知方法

对某个接收者调用了不存在的固有方法。

let p = Point { x: 1, y: 2 };
p.missing();  // E0013: unknown method `missing` on type Point

E0031 — 给不可变绑定赋值

左侧绑定没有用 mut 声明,却被重新赋值。

let x = 1;
x = 2;  // E0031: cannot assign to immutable binding

不可变绑定或参数调用需要修改环境的闭包、数组重复表达式的值不是 Copy、以及通过共享引用或 const 指针修改值,也会报告本错误。

E0032 — 类型参数数量不匹配

使用泛型结构体、枚举或 trait 时,传入的类型参数数量和定义不一致;有默认值的 trait 类型参数可以省略。

struct Box<T> { value: T }
let b: Box<i32, bool>;  // E0032: expected 1 type argument, got 2

trait Convert<T> {}
struct Value {}
impl Convert for Value {}  // E0032: expected 1 type argument, got 0

E0033 — 递归泛型调用

泛型函数递归调用时,类型参数必须一致;嵌套包装会导致无限实例化。

fun wrap<T>(value: T) -> T {
    wrap(Box { value })  // E0033: recursive generic call with different type args
}

E0034 — 无效类型标注

变量或参数的类型标注无法解析或格式不正确。

let x: InvalidType = 1;  // E0034: invalid type annotation

数组类型使用 Rust 风格 [T; N],元素类型在前、长度在后。写反时主错误只描述语法无效,修复方案会放在 note: 中:

struct Foo {
    x: [3; i32]
}
error[E0034]: invalid array type syntax
note: array types use `[T; N]`; write `[i32; 3]` instead

E0035 — 泛型 bound 不满足

实例化泛型函数、结构体、枚举或 impl 时,实际类型没有实现要求的 trait。

trait Marker {}
struct Box<T> where T: Marker { value: T }
struct Plain {}
let b = Box { value: Plain {} };  // E0035

E0036 — 缺少必需 trait 实现

用户类型参与比较时需要实现对应的比较 trait;实现子 trait 时,也必须先实现它声明的所有父 trait。

struct Point { x: i32 }
let same = Point { x: 1 } == Point { x: 2 };  // E0036

缺少 Index / IndexMut trait(例如 --no-std 场景下没有定义它们)时也报告本错误。

注意:同点(相同指向类型与可变性)原始指针的 == / != 是内建按地址比较,不需要 PartialEq 实现;p == 0usize as *const T 即空指针检查。

E0037 — impl where 子句违反 Paterson condition

trait impl 的 where 约束不能和被实现类型一样大或更大,否则 trait 求解可能无限递归。

trait Foo {}
struct Vec<T> { value: T }
impl<T> Foo for T where Vec<T>: Foo {}  // E0037

E0041 — Copy 实现包含不可复制字段

为结构体或枚举实现 Copy 时,它的所有字段和 payload 都必须可复制。

struct Token { value: i32 }
struct Wrapper { value: Token }
impl Copy for Wrapper {}  // E0041: `Token` is not Copy

E0042 — 循环控制语句位于循环外

break;continue; 只能出现在 whileforloop 循环体中。

fun invalid() {
    break;  // E0042: `break` outside of a loop
}

E0043 — 不定长类型用在值位置

str、切片 [T] 和 trait 对象 dyn Trait 没有独立于借用/指针的布局,不能作为局部变量、参数、返回值、字段或其他值类型的组成部分。字符串值应使用 &str,切片和动态对象分别通过 &[T]&dyn Trait 或拥有形式使用。

let invalid: str = "hello";  // E0043
let valid: &str = "hello";   // OK

E0044 — 无效的父 trait 声明

父 trait 必须能解析到已声明的 trait,并且 trait 继承关系不能形成环。

trait Child: Missing {}  // E0044: unknown supertrait
trait First: Second {}   // E0044: cycle
trait Second: First {}

E0045 — 无法推断匿名函数参数类型

参数类型无法从函数体、期望的可调用签名或调用点确定。

let id = [x -> x];  // E0045

添加显式类型,例如 [x: i32 -> x]。延迟初始化的 let 绑定在首次赋值前无法确定类型时,也报告本错误。

E0046 — 不安全操作需要 unsafe 上下文

解引用或索引原始指针、调用 unsafe fun 或不安全外部函数,都需要在 unsafe {} 块中进行。原始指针的有效性和生命周期仍由程序员保证,错误的指针操作可能触发未定义行为;固定数组和切片的安全索引则会执行边界检查。

fun read(ptr: *const i32) -> i32 {
    let x = *ptr;  // E0046
    let y = ptr[0]; // E0046
    x + y
}

fun read_safe(ptr: *const i32) -> i32 {
    unsafe {
        let x = *ptr;   // OK: 在 unsafe 块中
        let y = ptr[0]; // OK
        x + y
    }
}

unsafe fun external_contract() {}

fun call_contract() {
    unsafe { external_contract(); }
}

类型检查器在统一两个互相引用的类型时可能构造出无限大的类型(例如把匿名函数传给它自己)。这种情况报告 E0067,与 unsafe 上下文无关。

E0067 — 无法构造无限类型

类型检查器在统一类型时检测到自我引用的替换循环,继续统一会构造出无限大的类型。

fun main() {
    let id = [value -> value];
    id(id);  // E0067: 把 `id` 传给它自己会让参数类型等于自身
}

为绑定添加显式类型标注,或调整调用以打破自我引用的替换循环。

E0054 — 访问私有结构体字段或方法

结构体字段和固有方法默认私有,只能在声明它们的模块及其子模块中使用。模块外访问时,需要在字段或方法声明前添加 pub,或通过类型提供的其他公开接口操作。

mod model {
    pub struct Point {
        value: i32,
    }
}

fun read(point: model::Point) -> i32 {
    point.value  // E0054: field `value` of struct `Point` is private
}

E0055 — 同一类型同时实现 CopyDrop

拥有析构逻辑的类型不能按位复制,否则多个副本会重复释放同一资源。移除其中一个 impl。

E0056 — 直接调用 Drop::drop

析构方法只能由编译器调用。需要提前结束一个值时使用 prelude 中的 drop(value),它会消费所有权并在被调用函数结束前完成析构。

E0057 — letfor 中的可反驳模式

普通 letfor 循环头没有备选分支,模式必须匹配该类型的每一个值。枚举变体、字面量等只覆盖部分取值的模式需要改用 match;带发散 else 块的 let-else 不触发此错误。

enum Opt { None, Some(i32) }

let Opt::Some(v) = o;              // E0057: `Opt::None` is not covered
for Opt::Some(v) in opts {}        // E0057: `Opt::None` is not covered
let Opt::Some(v) = o else { return; }; // OK

元组和结构体模式是不可反驳的,可以直接解构:

let (a, b) = pair;             // OK
let Point { x, y } = point;    // OK
for (k, v) in pairs {}         // OK

E0058 — 同一模式重复绑定名称

一个模式内的每个绑定名称必须唯一。不同 let 语句或不同 match arm 仍可正常遮蔽同名变量。

let (value, value) = (1, 2);  // E0058

E0060 — 常量初始化式无效

常量必须使用可在编译期检查的纯表达式,并且不能形成常量初始化循环。字面量、已检查常量引用、纯运算、转换和聚合值可以使用;函数调用、闭包、控制流或不安全操作会报告此错误。

const ANSWER: i32 = make_answer();  // E0060

E0061 — ? 的操作数不是 ResultOption

? 只能展开 Result<T, E>Option<T>,不能用于普通值或其他枚举。

E0062 — ? 只能出现在返回 ResultOption 的函数中

包含 ? 的函数必须返回与操作数同一种类型:操作数是 Result 时返回同一个 Result 枚举,操作数是 Option 时返回 Option

E0063 — ? 的错误类型无法转换

Result 的错误类型必须实现 Into<目标错误类型>;没有匹配的 Into impl 时,编译器会回退尝试 From<源错误类型> for 目标错误类型,两者都不满足时报告本错误码。

E0065 — 带值的 break 位于 loop 之外

只有 loop { } 无限循环可以通过 break 值; 交出结果;whilefor 中只允许无值的 break;

fun f() {
    while true {
        break 1;  // E0065: `break` with a value is only allowed inside `loop`
    }
}

E0066 — let-else 的失败分支没有发散

let-elseelse 块必须离开当前控制流,不能正常落到绑定之后。使用 returnbreakcontinue 或不会结束的 loop

let Opt::Some(v) = value else {
    0                 // E0066
};

E0072 — 递归类型无限大小

结构体或枚举的字段中包含自身,导致类型大小无法在编译期确定。

struct Node {
    next: Node,  // E0072: recursive type has infinite size
}

对应 note: 会提示使用 &*const*mut 间接引用打破循环。

struct Node {
    next: &Node,  // OK:引用是定长的
}

E0391 — 类型别名展开循环

类型别名直接或间接引用自身,导致编译器无法得到最终类型。别名必须展开到非递归的具体类型。

type Result = Result;  // E0391: cycle detected when expanding type alias `Result`

Trait / Impl 检查 (E0020–E0030, E0047–E0048)

E0020 — trait 重复方法 / callable 签名不匹配

同一个 trait 内定义了同名方法;callable impl(impl Fn* for T)的 call 方法签名与 impl 头声明的调用签名不一致时,也报告本错误码。

trait Foo {
    fun bar();
    fun bar();  // E0020: duplicate method `bar`
}

E0021 — callable impl 缺少 call 方法

impl Fn(...) -> T for X 要求实现体提供名为 call 的方法;缺少时报告本错误码。Fn 使用 &self 接收者,FnMut 使用 &mut selfFnOnce 使用 self

struct Adder {}

impl Fn(i32) -> i32 for Adder {}  // E0021: callable impl is missing required method `call`

E0022 — trait 重复关联类型

同一个 trait 内定义了同名关联类型。

trait Foo {
    type T;
    type T;  // E0022: duplicate associated type `T`
}

E0023 — impl 引用未知 trait

impl Trait for Type 中引用的 trait 不存在。

impl UnknownTrait for Point { }  // E0023: references unknown trait

E0024 — impl 重复方法

同一个 impl 块内定义了同名方法。

impl Point {
    fun bar() { }
    fun bar() { }  // E0024: duplicate method `bar`
}

E0025 — impl 重复关联类型

同一个 impl 块内定义了同名关联类型。

impl Point {
    type T = i32;
    type T = i64;  // E0025: duplicate associated type `T`
}

E0026 — impl 缺少方法

impl 块未实现 trait 要求的所有方法。

trait Foo { fun bar(); }
impl Foo for Point { }  // E0026: missing method `bar`

E0027 — impl 缺少关联类型

impl 块未提供 trait 要求的所有关联类型。

trait Foo { type T; }
impl Foo for Point { }  // E0027: missing associated type `T`

E0028 — impl 方法参数数量不匹配

impl 中方法参数数量与 trait 声明不一致。

trait Foo { fun bar(x: i32); }
impl Foo for Point {
    fun bar() { }  // E0028: parameter count mismatch
}

trait 方法与 impl 方法的 unsafe 安全性不一致(例如期望 safe 的 trait 方法用 unsafe fun 实现)时,也报告本错误。

E0029 — impl 方法参数类型不匹配

impl 中方法参数类型与 trait 声明不一致。

trait Foo { fun bar(x: i32); }
impl Foo for Point {
    fun bar(x: &str) { }  // E0029: parameter type mismatch
}

E0030 — impl 方法返回类型不匹配

impl 中方法返回类型与 trait 声明不一致。

trait Foo { fun bar() -> i32; }
impl Foo for Point {
    fun bar() -> bool { true }  // E0030: return type mismatch
}

E0047 — trait 实现重叠

同一个 trait 不能有两个对同一组类型参数都适用的实现。泛型实现与其覆盖的具体实现也会冲突。

trait Foo {}
struct Point {}

impl Foo for Point {}
impl Foo for Point {}  // E0047: conflicting implementations

callable impl 或 bound 的格式错误也使用本错误码:FnFnMutFnOnce 必须携带 (参数类型...) -> 返回类型 调用签名,dyn 后面必须跟 trait 类型。一般的 impl Trait 不要求调用签名。

E0048 — impl 违反孤儿规则

当前包只能实现自己定义的 trait,或为自己定义的名义类型实现外部 trait。实现外部 trait 时,Self 和 trait 类型参数中必须出现本地类型;在第一个本地类型之前不能出现未被类型构造器覆盖的泛型参数。引用会传递本地性,但不会覆盖其中的泛型参数。标注了 #[fundamental] 的类型是透明的:当其某个类型参数为本地类型时,整体也视为本地(与 &T 行为一致),因此可以为 #[fundamental] 外部类型包裹本地类型的形式实现外部 trait。

use external::{Show, Point};
impl Show for Point {}  // E0048

FnFnMutFnOnce 名称由编译器保留,用户不能用这些名称重新声明 trait;用户类型可以为本地类型实现编译器提供的 callable trait,但仍受上述孤儿规则约束。

E0049 — 默认 std 模式下使用内部属性

默认加载标准库时,#[lang = "..."]#[fundamental] 只允许出现在随编译器附加的标准库中,用户包使用这些属性会被拒绝。来源检查优先于属性格式和目标检查,因此用户包中的任何用法都统一报告 E0049。使用 --no-std 时不附加内置标准库,参与编译的包可以自行定义 lang item 和 #[fundamental] 类型。

#[lang = "copy"]  // E0049: `#[lang = "copy"]` is reserved for the standard library
trait MyCopy {}
#[fundamental]  // E0049: `#[fundamental]` is reserved for the standard library
struct MyBox<T> { value: T }

E0053 — lang item 错误

自定义 core 中的 lang item 必须使用已知名称、标注在 trait 上并满足对应的固定签名。缺少字符串值、错误目标、错误签名、同一 lang item 被定义两次,或同一个 trait 标注多个 lang item,都会报告 E0053#[fundamental] 只能以不带值的形式标注结构体或枚举,形式或目标错误时也报告 E0053

#[lang = "unknown"]  // E0053: unknown lang item
trait Foo {}
#[lang = "copy"] trait A {}
#[lang = "copy"] trait B {}  // E0053: lang item `copy` defined more than once

HIR 降级与名字解析 (E0040, E0050–E0052, E0064)

E0040 — 语法降级错误

AST 到 HIR 降级过程中的语法/语义错误,如无效字面量、缺少表达式等。

let x = 99999999999999999999;  // E0040: invalid integer literal
let y = ;                       // E0040: missing expression statement

E0050 — 未解析名字

路径或名字无法解析到当前作用域中可见的定义。

let x = missing_name;  // E0050: unresolved name

E0051 — 空 use 声明

use 树没有暴露出任何可导入的名字。

E0052 — glob 导入目标不存在

use path::*; 的目标模块无法解析。

use missing::*;  // E0052: glob import target not found

E0064 — 同一作用域重复定义名称

函数、结构体、枚举、trait、常量、类型别名和模块共享当前作用域的声明名称;同一作用域不能重复定义,嵌套在不同模块中的同名声明不冲突。

fun hello() -> i32 { 0 }
fun hello() -> i32 { 1 }  // E0064: `hello` is defined multiple times

移动、逃逸和借用检查 (E0059, E0100, E0200, E0300–E0310)

E0059 — 使用未初始化的 let 绑定

let 可以省略初始化式并稍后赋值,但在每条到达使用点的路径上都必须先完成赋值。编译器会合并 ifmatch 和循环的控制流;可能仍未初始化的读取会报告此错误。

fun main() -> i32 {
    let value: i32;
    value // E0059
}

E0100 — 使用了已移动的值

在所有权转移后再次使用该值。

let x = Point { x: 1, y: 2 };
let y = x;    // x 的所有权转移到 y
let z = x;    // E0100: use of moved value: `x`

E0200 — 逃逸分析提示(保留)

E0200 是保留码:当前逃逸分析只把结果交给 MIR 降级决定 AllocaHeapAlloc,不会以任何形式向用户发出这个诊断码。

E0300 — 可变借用与已有共享借用冲突

已有共享借用尚未结束时,不能再创建可变借用。

let r = &p;
let m = &mut p;  // E0300

从容器取得的共享元素引用也会保持对容器的共享借用;引用仍活跃时调用需要 &mut self 的方法同样触发 E0300。

E0301 — 共享借用与已有可变借用冲突

已有可变借用尚未结束时,不能再创建共享借用。

let m = &mut p;
let r = &p;  // E0301

E0302 — 重复可变借用

同一位置不能同时存在两个可变借用。

let a = &mut p;
let b = &mut p;  // E0302

方法返回的可变引用会关联回 receiver。即使引用经过 Option<&mut T> 等容器传递,只要它后面仍会使用,再次调用 receiver 的可变方法仍会触发 E0302。

E0303 — 借用期间赋值

某个位置仍被借用时,不能给它赋值。

let r = &p;
p = other;  // E0303

E0304 — 借用期间移动

某个位置仍被借用时,不能移动它。

let r = &p;
let q = p;  // E0304

E0305 — 从实现 Drop 的类型中移出字段

显式实现 Drop 的类型必须作为整体保持有效,不能单独移出字段。普通聚合类型仍可部分移动,编译器会用字段级 drop flag 避免重复析构。

E0306 — Drop 所有者的引用逃出作用域

实现 Drop 的值会在所有者作用域结束时确定性析构,因此指向它的引用不能作为返回值活得更久。把所有权移出函数,或让引用只在所有者作用域内使用。

E0307 — 在 match guard 中移动模式绑定

guard 失败时还要继续尝试后续 arm,因此 guard 只能查看或借用非 Copy 模式绑定,不能取得其所有权。把移动操作放到选中的 arm body 中。

E0308 — 从显式安全引用解引用位置移出非 Copy

*reference 或显式 &pattern / &mut pattern 中的按值绑定会读取安全引用指向的 T。如果 T 没有实现 Copy,引用并不拥有这个值,不能直接把它搬出:

struct Token {}

fun main() {
    let mut token = Token {};
    let reference = &mut token;
    let &mut moved = reference;  // E0308
}

保留引用并通过它访问,或只在确实允许按位复制时为类型实现 Copy*reference = value 是写回原位置,不属于此错误。

E0310 — 无 GC 模式下引用逃出栈存储

Clue.toml 设置 [runtime] gc = false 后,不会再用 GC 堆延长局部值的存活时间。返回局部值或临时值的引用、按引用捕获并逃出当前栈帧等行为会被拒绝:

struct Data { value: i32 }

fun escaped() -> &Data {
    let value = Data { value: 42 };
    &value // E0310
}

改为返回有所有权的值、使用 move [...] 按值捕获,或把引用限制在所有者作用域内。来自调用者的输入引用仍可直接转发,因为这不会延长它指向值的生命周期。


泛型、类型与模式 (E0033, E0035, E0037–E0039, E0072)

E0033 — 递归泛型调用

泛型函数递归调用自身时,如果实际类型参数与定义不同(例如被包装进另一个泛型),会导致编译器无限单态化。

E0035 — 泛型 bound 不满足

泛型函数、结构体、枚举或 impl 的 bound 会在实例化时检查;不满足时会报错。

E0037 — impl where 子句违反 Paterson condition

implwhere 约束必须严格小于被实现类型,例如 impl<T> Trait for T where Vec<T>: Trait {} 会被拒绝。

E0038 — 无效的枚举变体模式

枚举变体的所属枚举、形状或字段与被匹配的类型不一致。

enum Left { Same }
enum Right { Same }

fun value(input: Left) -> i32 {
    match input {
        Right::Same => 1,  // E0038
        Left::Same => 0,
    }
}

E0039 — match 不穷尽

至少有一个可能的值没有被任何无 guard 的 arm 覆盖。诊断会给出一个缺失模式;整数模式还会在注记中列出未覆盖的连续区间。

enum State { Ready, Done(i32) }

fun value(state: State) -> i32 {
    match state {
        State::Ready => 0,
        State::Done(1) => 1,
        // E0039: missing pattern `State::Done(_)`
    }
}

添加缺失分支,或使用 _ / 标识符绑定覆盖剩余值。带 guard 的 arm 可能在运行时失败,因此不计入穷尽性。

E0072 — 递归类型具有无限大小

结构体或枚举直接或间接包含自身,没有任何间接层(引用、指针等),导致编译期无法计算类型大小。插入 &*const*mut 打破循环即可修复。


过程宏展开 (E0400)

E0400 — 过程宏展开错误

过程宏的导入和使用违反约定时由编译驱动报告,例如同一个宏被导入多次、use 树无法解析到宏等。过程宏包内部产生的其他诊断(如 syn 解析失败、quote! 重复长度不一致)会使用宏自己发出的错误信息,并映射回宏调用位置。