【免费下载链接】roc
A fast, friendly, functional language.
_ = 42是 roc(A fast, friendly, functional language)中一条极简却语义关键的下划线赋值语句:它不把任何值绑定到名字,只表达"丢弃这个表达式的结果"。本文以仓库快照测试 test/snapshots/statement/underscore_assignment.md 为骨架,从词法、语法、canonicalize、类型推断到格式化与 AST 快照,完整剖析该语句在 roc 编译器(Zig 实现)中的处理链路,并给出可直接运行的实战示例与参数说明。
快照测试的结构:一个语句如何被编译流水线逐级"过检"
roc 仓库中的快照测试(snapshot test)把一段源码送入编译器的各个阶段,并逐段记录每个阶段产出的中间表示,作为回归基准。本篇文章所依据的 underscore_assignment.md 正是这种测试的一个最小样本,其结构分为六段:
| 段 | 内容 | 说明 |
|---|---|---|
META | description=Bare underscore assignment to discard a value、type=snippet | 测试描述与类型 |
SOURCE | _ = 42 | 被测源码 |
EXPECTED | NIL | 期望输出(无) |
PROBLEMS | NIL | 期望零编译问题 |
TOKENS | 词法记号序列 | 词法分析结果 |
PARSE | 语法树(S-expression) | 解析结果 |
FORMATTED | NO CHANGE | 格式化器结果 |
CANONICALIZE | 规范化 IR(S-expression) | canonicalize 结果 |
TYPES | 推断出的类型 | 类型检查结果 |
其中SOURCE只有一行:
_ = 42含义是:把整数42赋值给通配符_,从而丢弃这个值。EXPECTED为NIL表示该测试没有任何期望的运行时输出,PROBLEMS为NIL表示编译器不应报告任何错误或警告——即这是一条合法且被编译器接受的语句。
词法层面:_如何被识别为独立的记号
快照中的TOKENS段给出了词法分析(tokenize)的产物:
Underscore,OpAssign,Int, EndOfFile,也就是说,_ = 42被切分成四个记号:Underscore、OpAssign(赋值运算符=)、Int(整数字面量42),以及文件结束符EndOfFile。
在词法实现 src/parse/tokenize.zig 中,_与逗号、点号等被归类为"结构性标点"(structural punctuation),同时记号缓冲还记录了标识符是否以_开头、以_结尾的标志(starts_with_underscore/ends_with_underscore)。这一点很关键:单独一个_是一个独立的、专门用于丢弃的记号,它不同于_foo这种"带下划线前缀、但仍命名了一个东西"的标识符。按语言参考 docs/langref/naming.md 的规则:
- 小写名字可以由
_、$或小写 ASCII 字母开头; _前缀仅用于命名"实际上不会被用到"的东西,且后面必须跟一个小写 ASCII 字母(如_unused);- 编译器会在同一作用域内引用以
_开头的名字时给出警告; - 而
_模式本身不是名字,它不命名任何东西。
因此_ = 42中的_走的是"模式"(pattern)路径,而不是"标识符"路径,这正是它在后续解析阶段被构造成p-underscore节点的原因。
语法层面:_ = 42被解析成什么样的 AST
快照的PARSE段展示了 Parser 产出的语法树(以类 Clojure 的 S-expression 表示):
(file (type-mod) (statements (s-decl (p-underscore) (e-int (raw "42")))))逐层解读:
file是顶层节点,type-mod表示类型修饰(此处为空);statements是语句列表,其中包含一条语句;s-decl(statement-declaration)表示这是一条声明语句,即"赋值声明";p-underscore表示左侧模式是下划线模式(underscore pattern);e-int (raw "42")表示右侧表达式是整数字面量,原始文本为42。
在解析器源码中,p-underscore有对应的 AST 节点与存储结构:src/parse/AST.zig 定义了underscore节点并在打印时输出p-underscore静态原子;src/parse/Node.zig 与 src/parse/NodeStore.zig 中则有underscore_patt节点标签及其构造逻辑(将underscoreAST 节点转换为underscore_patt节点)。解析器错误提示中也把下划线列为合法模式起点之一:"Patterns can be lowercase names, tags, literals, lists, records, tuples, underscores, or nested patterns."
值得注意:由于右侧是字面量42,它被解析为e-int;而如果右侧是更复杂的表达式(如函数调用、算术运算),这里会相应地出现其他表达式节点,但左侧的p-underscore结构保持不变。
格式化:为什么答案是NO CHANGE
快照的FORMATTED段只有一行:
NO CHANGE这表示 roc 的格式化器(formatter)对_ = 42不做任何改写:该写法本身就是规范的。这验证了两点:
- 下划线赋值语句的写法(
_、空格、=、空格、表达式)符合格式化规范; _作为一个保留通配符,不会被重命名、加括号或改写。
从源码结构看,格式化相关逻辑位于 src/fmt 目录,快照断言NO CHANGE意味着格式化前后 AST 完全一致。这也是为什么你可以放心地把它当作一种惯用写法直接写进代码。
Canonicalize:规范化成d-let+p-underscore的 IR
快照的CANONICALIZE段展示了规范化(canonicalization)阶段产出的中间表示:
(can-ir (d-let (p-underscore) (e-num (value "42"))))与 PARSE 阶段相比,有两处明显变化:
s-decl变成了d-let——声明语句被规范化为"let 绑定"形式的声明 IR;e-int (raw "42")变成了e-num (value "42")——整数字面量从"原始文本"变成"数值"语义节点,即42已被解析为数值42。
而p-underscore保持不变,说明在规范化阶段,下划线模式仍然作为一个独立的模式节点存在。在 canonicalize 的实现 src/canonicalize/CIR.zig 中,pattern_underscore标签被用于判断模式是否为下划线模式;src/canonicalize/Can.zig 也专门维护了"用于下划线校验的临时类型变量标识符"(scratch type variable identifiers for underscore validation),说明编译器对下划线模式有专门的类型处理路径。
从语义上说,d-let+p-underscore表达了"建立一个 let 绑定,但绑定目标是_",其效果等价于"计算右侧表达式、丢弃其结果",不会向作用域引入任何新名字。
类型层面:表达式被推断为Dec,且不产生任何定义
快照的TYPES段给出了类型推断的结果:
(inferred-types (defs) (expressions (expr (type "Dec"))))关键信息有两点:
defs为空列表——本次语句没有产生任何定义(definitions),因为_不绑定名字,自然也不会向类型环境注册任何定义。这从类型层面印证了"丢弃"语义。expressions中唯一表达式的类型是Dec——即十进制数值类型。整数42在 roc 中默认被推断为Dec(decimal),这是 roc 数值字面量的默认推断类型,相关说明可参见语言参考 docs/langref/numbers.md。
也就是说:编译器先完整地对右侧表达式做类型推断(得到Dec),然后因为左侧是_,不产生任何名字绑定,整个语句的类型效果是"无副作用地求值并丢弃"。
实战用法:什么时候该用_ = expr
结合 docs/langref/naming.md 对_的规则(_前缀仅用于命名不真正使用的东西;_模式不是名字、不命名任何东西),_ = expr的典型使用场景包括:
- 丢弃函数调用的返回结果:当一个函数有返回值但你只关心其副作用时,用
_ = call()显式丢弃; - 在模式匹配中占位:虽然
_ = 42是最简形式,但下划线模式更多出现在match的分支模式、记录解构、元组解构中,用来忽略不关心的字段或分支,例如:
_ = { x: 1, y: 2 } # 丢弃整个记录- 与命名下划线区分:如果确实想给"不使用的值"起一个名字(便于自我文档化),应使用
_name(下划线前缀 + 小写字母),例如:
_unused = 42 # 命名但未使用的绑定此时编译器对_name的规则是:在同一作用域内被引用会给出警告(见 docs/langref/naming.md),但它仍然是一个真实的名字,与_有本质区别——_连名字都不是。
需要注意的是:_ = expr在 roc 中作为语句(statement)使用,出现在函数体或模块顶层;它不是一个表达式,不能像函数参数默认值那样嵌入到表达式中。这一点与许多语言(如 Python、Rust 中的_)的行为类似,但 roc 将_严格定义为"模式"而非"变量"。
从源码到快照:这条语句如何验证编译器正确性
作为 test/snapshots/statement 目录下的一个回归测试样本,underscore_assignment.md的价值在于:
- 防止解析器回归:断言
_ = 42始终被解析为s-decl+p-underscore结构,而非被误识别为标识符绑定; - 防止类型检查回归:断言该语句不产生任何定义、且右侧表达式正常完成类型推断;
- 防止格式化回归:断言
NO CHANGE,保证格式化器不会破坏这种惯用写法。
从源码结构看,快照测试由 src/snapshot_tool 目录下的工具负责生成与校验;每份快照的每一段(TOKENS/PARSE/FORMATTED/CANONICALIZE/TYPES)都对应编译流水线的一个真实阶段输出。因此,当你修改 roc 编译器中与模式解析、let 规范化或数值类型推断相关的代码时,这份快照就是一道自动防线:任何阶段输出的变化都会触发快照不匹配,从而暴露回归。
小结
_ = 42虽只有一行,却完整贯穿了 roc 编译器的六条流水线:词法上_是独立的Underscore记号;语法上它是s-decl声明中的p-underscore模式;格式化上它保持NO CHANGE;canonicalize 后成为d-let+p-underscore绑定;类型上不产生任何定义且右侧表达式被推断为Dec。理解这条语句,就等于理解了 roc 中"下划线 = 丢弃、命名下划线 = 未使用名字"这一核心命名哲学,以及快照测试如何为编译器行为提供逐阶段的精确保障。在编写 roc 代码时,善用_ = expr显式表达"此处有意丢弃结果",既能提升可读性,也能让类型检查器准确理解你的意图。
【免费下载链接】roc
A fast, friendly, functional language.
相关推荐
Roc 语言名义类型值解构(`Type.(pat)`)语法详解:以编译器快照测试为线索
Roc 语言名义类型值解构( Type. pat )语法详解:以编译器快照测试为线索 导读 本篇围绕 Roc 编译仓库中的快照测试 test/snapshots
Roc 语言中的下划线通配符模式:基于快照测试详解 `_` 在解构赋值中的应用
Roc 语言中的下划线通配符模式:基于快照测试详解 _ 在解构赋值中的应用 _ (下划线)是 Roc 语言中"匹配一切、但不绑定任何变量"的通配符模式,常用于解
Roc 编译器快照测试实战:赋值语句中 Record 解构的完整编译管线
Roc 编译器快照测试实战:赋值语句中 Record 解构的完整编译管线 本篇以快照文件 module_record_destructure.md https:
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考