Roc 语言 I128 闭区间(Inclusive Range)语法深度解析:基于 REPL 快照测试的完整实战指南
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
本指南以 Roc 编译器仓库中的 REPL 快照文档 test/snapshots/repl/i128_range_to.md 为核心骨架,系统讲解 Roc 语言 I128 闭区间语法(..=)的写法、迭代行为与边界语义。你将掌握(start.I128..=end.I128).iter()与Iter.fold的组合用法,理解单元素区间、反向空区间的底层判定规则,并透过快照测试中 token 化、解析、规范化(canonicalize)与类型推导各阶段输出,看清区间表达式在编译器内部如何被翻译为range_inclusive_to方法调用。
一、关联文档是什么:REPL 快照测试的定位
test/snapshots/repl/i128_range_to.md属于 Roc 编译器仓库的快照测试(Snapshot Test)体系。快照测试通过捕获指定 Roc 代码在编译管线各阶段(token 化、解析、规范化、类型检查、REPL 求值)的输出,来验证编译器行为是否与预期一致。type=repl表示该快照直接驱动 REPL 求值器执行SOURCE中的表达式,并把每一条命令的输出记录在OUTPUT段中。
该文档的核心声明是 META 段中的description:
I128 inclusive range syntax iterates from start to end即:I128 闭区间语法会从起始值迭代到结束值(包含两端)。整篇快照围绕这一语义设计了三组代表性输入,覆盖正向区间、单元素区间与反向区间三种情况。
关于快照测试的更多运行机制,可参考 test/snapshots/README.md,其中说明了快照对编译管线各阶段的捕获方式,以及zig build run-snapshot-tool -- <file_path>等命令的用法(见下文第七节)。
二、I128 闭区间基础写法:..=与.iter()
在 Roc 语言中,闭区间(inclusive range)使用..=运算符构造,表示包含首尾两端的数值范围。关联文档中给出的三个 I128 示例均采用同一模式:
(-2.I128..=2.I128).iter()拆解来看:
-2.I128与2.I128:带I128类型注解的整数字面量,明确将范围端点限定为 128 位有符号整数;..=:闭区间运算符(token 名称为OpDoubleDotEquals,见 test/snapshots/range_inclusive.md);.iter():将区间(Range)转换为迭代器(Iter),以便交给Iter.fold消费。
Iter.fold是 Roc 标准库中遍历迭代器的常用折叠函数,签名形如Iter.fold(iter, initial, closure):把初始值[](空 List)与迭代产生的每个元素一起交给闭包|acc, item| acc.append(item),将元素逐个追加进列表,最终返回完整列表。因此整个表达式Iter.fold(iter, [], |acc, item| acc.append(item))的求值结果,就是把该区间迭代出的全部元素收集为一个 List——这正是观察区间迭代行为的直接手段。
三、三个核心用例逐行解读:正向、单元素与反向空区间
关联文档 i128_range_to.md 的 SOURCE 与 OUTPUT 完整如下:
» Iter.fold((-2.I128..=2.I128).iter(), [], |acc, item| acc.append(item)) » Iter.fold((0.I128..=0.I128).iter(), [], |acc, item| acc.append(item)) » Iter.fold((3.I128..=1.I128).iter(), [], |acc, item| acc.append(item))对应输出:
[-2, -1, 0, 1, 2] --- [0] --- []三条用例分别验证了闭区间的三种典型情况:
3.1 正向区间:-2..=2
起始值-2小于结束值2,迭代器按+1 步进从-2走到2,端点2被包含,得到[-2, -1, 0, 1, 2],共 5 个元素。这正是"inclusive range iterates from start to end"的直接体现。
3.2 单元素区间:0..=0
起始值与结束值相等时,区间恰好包含唯一元素0,输出[0]。闭区间语义下"相等端点"不会被误判为空,这与排他区间(..<,见第五节)形成鲜明对比。
3.3 反向区间:3..=1
起始值3大于结束值1,属于降序(descending)区间。快照输出为[],说明 Roc 的 I128 区间迭代不支持反向迭代——当start > end时,闭区间直接产生空迭代序列,而不会倒序输出[3, 2, 1]。
值得注意的是,这一行为并非 I128 独有。仓库中所有数值类型的*_range_to.md快照(如 i32_range_to.md、u128_range_to.md)都遵循同样的规则:反向区间一律得到空列表。
四、降级边界(Empty/Trivial Range)的统一判定
闭区间何时为空?综合关联文档与同目录快照,可以归纳出两条判定规则:
- 反向区间为空:
start > end→[](如3.I128..=1.I128); - 单元素区间非空:
start == end→ 恰含一个元素(如0.I128..=0.I128输出[0])。
U128 快照 u128_range_to.md 给出了同样的三条用例:
» Iter.fold((1.U128..=5.U128).iter(), [], |acc, item| acc.append(item)) » Iter.fold((0.U128..=0.U128).iter(), [], |acc, item| acc.append(item)) » Iter.fold((5.U128..=3.U128).iter(), [], |acc, item| acc.append(item))输出分别为[1, 2, 3, 4, 5]、[0]、[]——正向完整、单元素保留、反向为空,与 I128 行为完全一致。这提示闭区间的空区间判定逻辑是跨数值类型统一的,I128 快照验证的其实是这套通用语义在有符号 128 位整数上的具体表现。
五、闭区间(..=)与排他区间(..<)的行为对比
Roc 提供两种区间运算符,语义差异仅在于是否包含结束端点:
| 运算符 | 名称 | token | 端点语义 | 示例(I128) | 结果 |
|---|---|---|---|---|---|
..= | 闭区间(inclusive) | OpDoubleDotEquals | 含 start 与 end | (-2.I128..=2.I128) | [-2, -1, 0, 1, 2] |
..< | 排他区间(exclusive) | OpDoubleDotLessThan | 含 start、不含 end | (-2.I128..<2.I128) | [-2, -1, 0, 1] |
对比同目录的 i128_range_until.md:
» Iter.fold((-2.I128..<2.I128).iter(), [], |acc, item| acc.append(item)) » Iter.fold((1.I128..<1.I128).iter(), [], |acc, item| acc.append(item)) » Iter.fold((3.I128..<1.I128).iter(), [], |acc, item| acc.append(item))输出:
[-2, -1, 0, 1] --- [] --- []两个关键差异点:
- 同一区间
-2..<2比-2..=2少一个元素(不含2); 1.I128..<1.I128输出[]——排他区间在 start == end 时为空,而闭区间0..=0输出[0]。因此"单元素区间"是闭区间独有的行为。
浮点类型同样适用这两套语义。例如 dec_range_to.md 中0.5.Dec..=2.5输出[0.5, 1.5, 2.5](步进 1.0、含 2.5),而 dec_range_until.md 中0.5..<3.5输出[0.5, 1.5, 2.5](不含 3.5);1.25.Dec..=1.25输出[1.25]。F32/F64 快照(f32_range_to.md、f64_range_to.md)也验证了相同模式。
六、I128 的数值边界:快照为什么选 I128 作为测试目标
I128 是 Roc 支持的最大整数类型之一,选择它作为区间测试目标并非偶然。同目录快照 repl_i128_large_values.md 验证了 I128/U128 能表示超出 64 位的极值:
» 170141183460469231731687303715884105727.I128 → 170141183460469231731687303715884105727 » -170141183460469231731687303715884105728.I128 → -170141183460469231731687303715884105728 » 340282366920938463463374607431768211455.U128 → 340282366920938463463374607431768211455- I128 的取值范围为
-170141183460469231731687303715884105728~170141183460469231731687303715884105727(-2^127~2^127 - 1); - 上界
170141183460469231731687303715884105727是 I128 的最大值,下界-170141183460469231731687303715884105728是最小值。
因此,i128_range_to.md验证的是区间迭代逻辑在完整 128 位宽度下依然正确:端点比较、+1 步进、空区间判定等操作不会因数值宽度或符号位而出现偏差。U128 的最大值340282366920938463463374607431768211455则覆盖了无符号场景的上界。
七、从源码看实现:..=在编译管线中如何被处理
快照测试的价值在于它把编译各阶段的中间表示都"钉"在文档里。参考 range_inclusive.md(以r = 1..=5为例),可以完整还原..=的编译路径:
1. Token 化——..=被识别为OpDoubleDotEquals(区别于排他区间的OpDoubleDotLessThan):
LowerIdent,OpAssign,Int,OpDoubleDotEquals,Int,EndOfFile,2. 解析(Parse)——..=作为二元运算符(e-binop)处理,左右操作数分别为区间两端:
(s-decl (p-ident (raw "r")) (e-binop (op "..=") (e-int (raw "1")) (e-int (raw "5"))))3. 规范化(Canonicalize)——区间表达式被翻译为对range_inclusive_to方法的分发调用(dispatch call):
(e-dispatch-call (method "range_inclusive_to") (constraint-fn-var 223) (receiver (e-num (value "1"))) (args (e-num (value "5"))))对比排他区间 range_exclusive.md,1..<5规范化后调用的是range_exclusive_to。也就是说,..=与..<在编译后期被分别路由到两个不同的底层方法,这解释了第五节中端点包含差异的来源。
4. 类型推导——1..=5的类型被推断为Range(Dec):
(patt (type "Range(Dec)"))) (expr (type "Range(Dec)")))这里可见 Roc 的类型默认策略:不带类型注解的整数字面量在区间表达式中默认落为Dec。而关联文档中显式书写.I128注解,正是为了把区间端点钉死在 I128 上,从而精确测试 I128 的迭代语义。
7.1 快照测试的实际运行方式
依据 test/snapshots/README.md,可以手动重放本快照:
# 生成全部快照 zig build run-snapshot-tool # 仅更新/重跑指定快照 zig build run-snapshot-tool -- test/snapshots/repl/i128_range_to.md # 调试 REPL 求值过程(打印解释器 trace) zig build run-snapshot-tool -- test/snapshots/repl/i128_range_to.md --trace-eval其中--trace-eval仅适用于type=repl快照且一次只能处理单个文件;debug 构建默认开启 trace,release 构建需通过-Dtrace-eval=true启用。快照的PROBLEMS段为NIL,表示这三条表达式在编译与求值过程中没有产生任何诊断报告——即它们是合法的、被完整支持的语法。
八、闭区间在for循环中的典型用法
区间不仅是Iter的数据源,也直接驱动for循环。快照 range_for_loop.md 展示了闭区间的循环写法(该快照同时演示了var可变绑定须以$开头的新约定):
total : U64 total = { var $sum_ = 0 for i in 1..=5 { $sum_ = $sum_ + i } $sum_ }其规范化结果(range_for_loop.md)显示,for i in 1..=5中的区间同样被翻译为range_inclusive_to方法调用。由于闭区间包含结束端点,1..=5会迭代1, 2, 3, 4, 5,累加和为15;若改用1..<5则只迭代到4。在设计循环边界时,这一差异直接影响循环次数,是实际编码中最容易踩中的细节。
九、常见错误:裸..不是合法的区间运算符
Roc 要求区间运算符必须二选一:..<(排他)或..=(闭)。若写成裸1..5,编译器会报错。快照 range_bare_double_dot_error.md 记录了完整的诊断信息:
I was parsing an expression, and `..` is not a range operator. Use `..<` for an exclusive range or `..=` for an inclusive range. For example: 1..<10 1..=10token 阶段..被单独识别为DoubleDot(而非OpDoubleDotEquals/OpDoubleDotLessThan),解析阶段产生e-malformed,类型退化为Error。因此,在 Roc 中书写区间时永远不要省略=或<。
十、总结:I128 闭区间的完整行为画像
综合关联文档与仓库证据,Roc 语言(start.I128..=end.I128)的迭代行为可归纳为一张速查表:
| 条件 | 行为 | 示例 | 结果 |
|---|---|---|---|
start < end | 按 +1 步进,包含两端 | -2..=2 | [-2, -1, 0, 1, 2] |
start == end | 恰含一个元素 | 0..=0 | [0] |
start > end | 空迭代(不支持反向) | 3..=1 | [] |
三条规则与同目录下 I8~I128、U8~U128、Dec、F32、F64 的所有range_to快照行为一致,属于 Roc 区间语义的通用约定;而i128_range_to.md的价值,在于把这一约定在128 位有符号整数全量程(含-2^127与2^127-1极值)上做了验证。实现证据可继续查看:
- test/snapshots/repl/i128_range_to.md(本文核心文档)
- test/snapshots/repl/i128_range_until.md(I128 排他区间对照)
- test/snapshots/repl/u128_range_to.md(U128 闭区间)
- test/snapshots/range_inclusive.md(
..=编译管线全阶段) - test/snapshots/range_exclusive.md(
..<编译管线全阶段) - test/snapshots/range_for_loop.md(
for i in 1..=5循环) - test/snapshots/repl/repl_i128_large_values.md(I128/U128 极值)
- test/snapshots/README.md(快照测试机制与命令)
【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考