Roc 语言 I128 闭区间(Inclusive Range)语法深度解析:基于 REPL 快照测试的完整实战指南
2026/9/19 13:18:08 网站建设 项目流程

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.I1282.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)的统一判定

闭区间何时为空?综合关联文档与同目录快照,可以归纳出两条判定规则:

  1. 反向区间为空start > end[](如3.I128..=1.I128);
  2. 单元素区间非空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 的取值范围为-170141183460469231731687303715884105728170141183460469231731687303715884105727-2^1272^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..=10

token 阶段..被单独识别为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^1272^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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询