【免费下载链接】roc
A fast, friendly, functional language.
本篇技术指南以 Roc 编译器仓库中的 REPL 快照测试 test/snapshots/repl/u128_range_until.md 为核心骨架,系统讲解U128无符号 128 位整数的开区间(exclusive range)语法start..<end的写法、迭代行为与边界规则,并深入到 src/build/roc/Builtin.roc 中Range类型、range_with_step、range_len_u128、range_iter_standard等底层实现,还原快照测试的生成与校验机制。读完本文,你将掌握 Roc 中U128开区间的完整语义(含空区间判定规则)、其在Iter.fold与for循环中的实战用法,以及如何读懂、运行和更新这类 REPL 快照测试。
一、快照文件定位:它验证了什么
test/snapshots/repl/u128_range_until.md属于 Roc 编译器仓库中的REPL 快照测试。根据 test/snapshots/README.md 的说明,快照测试通过捕获源码在编译管线各阶段(分词、解析、规范化、类型检查等)的输出,来验证编译器行为并防止回归。
该文件的META区块明确给出了它的验证目标:
description=U128 exclusive range syntax iterates from start to end type=repl- description:U128 开区间语法从起点迭代到终点(不含终点);
- type=repl:这是一个 REPL 类型的快照,每个
»前缀行会被当作一条独立的 REPL 表达式依次求值,输出逐条比对。
也就是说,这份快照不是普通文档,而是一份可执行、可校验的规格说明:它把U128..<开区间的正常迭代、单点区间、倒置区间三种情形固化为基准输出,任何编译器改动只要让这三条表达式的求值结果发生变化,快照比对就会失败并暴露回归。
二、快照文件结构逐块拆解
REPL 快照文件由META、SOURCE、OUTPUT、PROBLEMS四个区块组成,本文件的结构如下:
META:元信息
description=U128 exclusive range syntax iterates from start to end type=replSOURCE:三条 REPL 输入
SOURCE区块中,每一行以»(U+00BB)作为 REPL 提示符前缀,表示一次交互输入:
» Iter.fold((0.U128..<3.U128).iter(), [], |acc, item| acc.append(item)) » Iter.fold((1.U128..<1.U128).iter(), [], |acc, item| acc.append(item)) » Iter.fold((5.U128..<3.U128).iter(), [], |acc, item| acc.append(item))三条表达式全部使用Iter.fold把区间迭代器的元素收集进一个列表,便于直接观察迭代产生的元素序列。
OUTPUT:期望输出
[0, 1, 2] --- [] --- []三条输出之间以---分隔,与三条输入一一对应。
PROBLEMS:诊断结果
NILNIL表示这三条表达式在编译(类型检查)阶段没有产生任何报告(report)。在普通快照中,PROBLEMS区域存放的是reporting.Report的规范 S 表达式序列化结果;NIL即"无错误、无警告",印证了这三条 U128 开区间写法是完全合法的代码。
三、U128 类型与范围语法基础
3.1 U128:16 字节的无符号整数
根据 docs/langref/numbers.md 中内建整数类型表,U128是 16 字节(128 位)无符号整数,取值范围为0到340_282_366_920_938_463_463_374_607_431_768_211_455(超过 340 undecillion,即约 3.4×10³⁸)。作为对比,其有符号孪生类型I128的取值范围为-170_141_183_460_469_231_731_687_303_715_884_105_728到170_141_183_460_469_231_731_687_303_715_884_105_727。
Roc 中所有内建数值类型都有固定大小,且大小不随构建目标变化。要显式指定数字字面量的类型,可以在数字后加类型后缀,例如3.U128。仓库中的 test/snapshots/numeric_i128_u128_dec_edge_cases.md 展示了340282366920938463463374607431768211455.U128这类边界字面量在 REPL 中的往返输出。
3.2 范围运算符:..<与..=
Roc 提供两个范围运算符(详见 docs/langref/operators.md 的 "Range Operators" 一节):
| 运算符 | 语义 | 静态分发方法 |
|---|---|---|
start..<end | 开区间,不含end | range_exclusive_to |
start..=end | 闭区间,含end | range_inclusive_to |
两个操作数必须具有相同类型:U128范围即Range(U128),Dec范围即Range(Dec),以此类推。范围运算符的绑定优先级低于其他二元运算符,因此1..<n + 1会被解析为1..<(n + 1);同时范围运算符不可链式书写,1..<5..<10是错误用法。
3.3 范围是可复用的Range值
需要特别强调的是:0.U128..<3.U128这样的表达式立即构建一个可复用的Range(U128)值,而不是一次性循环。这个Range值可以反复调用.iter()产生新的迭代器,也可以直接用于for循环(for会自动调用范围的iter方法,见 docs/langref/loops.md)。这正是快照中先写(0.U128..<3.U128).iter()、再把迭代器交给Iter.fold的原因:Range负责描述,Iter负责消费。
四、三条用例逐一解读
用例 1:正常迭代——从起点到终点,不含终点
Iter.fold((0.U128..<3.U128).iter(), [], |acc, item| acc.append(item)) # => [0, 1, 2]0.U128..<3.U128是开区间,成员为0、1、2,3(上界)被排除。Iter.fold以空列表[]为初始累加器,逐项执行acc.append(item),最终得到[0, 1, 2]。注意append返回新列表(Roc 的数据结构是不可变的),因此这里的acc是不断更新的新列表值。
对照同目录下的闭区间快照 test/snapshots/repl/u128_range_to.md:
» Iter.fold((1.U128..=5.U128).iter(), [], |acc, item| acc.append(item)) # => [1, 2, 3, 4, 5]..=把5也包含进来,一条表达式即可清晰对比开闭区间的差异。
用例 2:下界等于上界——空区间
Iter.fold((1.U128..<1.U128).iter(), [], |acc, item| acc.append(item)) # => []当lower == upper时,开区间不满足lower < upper的成员条件,直接得到空区间,迭代器立即结束,折叠结果为空列表[]。
用例 3:下界大于上界——同样为空
Iter.fold((5.U128..<3.U128).iter(), [], |acc, item| acc.append(item)) # => []当lower > upper(倒置区间)时,开区间同样为空。U128是无符号类型,成员不可能小于下界,因此不会出现"从 5 递减到 3"之类的反向迭代行为——Roc 的范围不隐式反向。
小结:开区间的空区间判定
综合三条用例,..<开区间为空,当且仅当:
lower >= upper(下界不低于上界);- 或步长
step不大于 0。
这一规则在 docs/langref/numbers.md 的 "Ranges" 一节有明确表述,并且与闭区间形成对称:..=闭区间在lower > upper或步长非正时为空。注意Dec、F32、F64等其他数值类型遵循同样的判定逻辑,可对照 test/snapshots/repl/dec_range_until.md(0.5..<3.5产出[0.5, 1.5, 2.5],2.0..<2.0与3.5..<2.5产出[])与 test/snapshots/repl/i128_range_until.md(支持负数下界)验证。
五、源码级原理:Range(U128)从构建到迭代
快照验证的只是行为,而其背后是 src/build/roc/Builtin.roc 中一套完整的类型驱动实现。以下按调用链从外到内拆解。
5.1Range(num)类型的内部结构
Range是一个带标签的记录类型(src/build/roc/Builtin.roc):
Range(num) :: { lower : num, upper : num, step : num, upper_bound : [Exclusive, Inclusive], direction : [To, From], len_if_known : [Known(U64), Unknown], }六个字段的含义:
lower/upper:下界与上界,类型与范围元素类型一致;step:步长,初始为 1;upper_bound:Exclusive(开)或Inclusive(闭);direction:To(正向,从 lower 向 upper)或From(反向,从 upper 侧向 lower);len_if_known:当精确长度可放入U64时记录为Known(count),否则为Unknown,供size_hint使用。
其中upper_bound始终作用于upper,lower是范围成员的锚点(即使direction为From)。
5.2 U128 的五个范围方法
在U128的类型实现中(src/build/roc/Builtin.roc),每个数值类型都暴露同一组范围方法。U128的实现为:
range_len_if_known : U128, U128, U128, [Exclusive, Inclusive] -> [Known(U64), Unknown] range_len_if_known = |lower, upper, step, upper_bound| if step == 0 or lower > upper or (lower == upper and upper_bound == Exclusive) { Known(0) } else { range_len_u128(lower.abs_diff(upper), step, upper_bound) } range_iter : U128, U128, U128, [Exclusive, Inclusive], [To, From], [Known(U64), Unknown] -> Iter(U128) range_iter = range_iter_standard range_exclusive_to : U128, U128 -> Range(U128) range_exclusive_to = |lower, upper| range_with_step(lower, upper, 1, Exclusive, To) range_inclusive_to : U128, U128 -> Range(U128) range_inclusive_to = |lower, upper| range_with_step(lower, upper, 1, Inclusive, To) range_exclusive_from : U128, U128 -> Range(U128) range_exclusive_from = |upper, lower| range_with_step(lower, upper, 1, Exclusive, From) range_inclusive_from : U128, U128 -> Range(U128) range_inclusive_from = |upper, lower| range_with_step(lower, upper, 1, Inclusive, From)这里可以看到三条关键事实:
..<运算符的静态分发目标:0.U128..<3.U128在编译期被分发到U128.range_exclusive_to(0, 3),构造出upper_bound: Exclusive、direction: To、步长为 1 的Range(对应 docs/langref/operators.md 中..<调用range_exclusive_to的说明);- 空区间在构造期就被识别:
range_len_if_known中step == 0 or lower > upper or (lower == upper and upper_bound == Exclusive)直接返回Known(0),这正是用例 2、3 得到空列表的第一层原因——长度提示在Range构建时就已经计算好; range_iter共享标准实现:U128与所有整数类型、Dec共用range_iter_standard,而F32/F64使用独立的range_iter_float。
5.3range_with_step:统一构造器
range_with_step(src/build/roc/Builtin.roc)是各_to/_from方法的共同底层:
range_with_step = |lower, upper, step, upper_bound, direction| Num.Range.custom({ lower, upper, step, upper_bound, direction, len_if_known: lower.range_len_if_known(upper, step, upper_bound), })它通过Num.Range.custom组装记录,并立刻调用lower.range_len_if_known计算长度提示。Range.custom(src/build/roc/Builtin.roc)也开放给第三方数值类型使用,第三方类型可以定义自己的range_iter而不必依赖U64转换。
5.4range_len_u128:U128 专属长度计算
对U128而言,长度计算由range_len_u128完成(src/build/roc/Builtin.roc):
range_len_u128 = |distance, step, upper_bound| if step == 0 { Known(0) } else { base = match upper_bound { Exclusive => (distance - 1) / step Inclusive => distance / step } match base.plus_try(1) { Err(Overflow) => Unknown Ok(len_u128) => match len_u128.to_u64_try() { Ok(len) => Known(len) Err(OutOfRange) => Unknown } } }注意它的两个细节:
- 开闭区间的长度差异:开区间为
(distance - 1) / step + 1,闭区间为distance / step + 1。用例 1 中distance = |3 - 0| = 3、step = 1、Exclusive,得到(3-1)/1 + 1 = 3,与输出[0, 1, 2]的 3 个元素吻合; - 溢出保护:由于
U128的跨度可能超过U64,长度经plus_try与to_u64_try双重检查,放不下U64时如实返回Unknown,绝不伪造长度。
5.5range_iter_standard:共享的状态机迭代器
range_iter_standard(src/build/roc/Builtin.roc)是整数与Dec共用的迭代器工厂,内部以RangeAt(current) | RangeDone两种状态驱动:
range_iter_standard = |lower, upper, step, upper_bound, direction, len_if_known| { initial = match direction { To => if step > 0 { RangeAt(lower) } else { RangeDone } From => match range_standard_last(lower, upper, step, upper_bound) { Ok(last) => RangeAt(last) Err(NoMore) => RangeDone } } Iter.custom( initial, len_if_known, |state| match state { RangeDone => Err(NoMore) RangeAt(current) => match direction { To => { within_upper = match upper_bound { Exclusive => current < upper Inclusive => current <= upper } if step > 0 and within_upper { next_state = match current.plus_try(step) { Ok(next) => if next > current RangeAt(next) else RangeDone Err(Overflow) => RangeDone } Ok((current, next_state)) } else { Err(NoMore) } } From => ... } }, ) }正向(To)迭代的核心逻辑是:
- 起点为
lower,只有当step > 0时才进入RangeAt状态,否则直接RangeDone(空); - 每次产出当前值
current前,先检查current < upper(开区间)或current <= upper(闭区间),不满足立即终止; - 用
plus_try(step)推进,若next <= current(加法停滞)或发生溢出则终止——这正是开区间"从起点递增到终点"语义的实现,也保证了用例 2、3 中RangeDone会立即结束。
反向(From)分支通过range_standard_last先求出"下界锚定的、满足上界约束的最后一个成员",再逐次minus_try(step)递减,从而保证反向迭代与正向迭代产出完全相同的一组元素(只是顺序相反),例如(5.I64..=12).step_by(2).iter_rev()产出11, 9, 7, 5。
5.6Iter.fold的消费语义
快照中的Iter.fold定义于 src/build/roc/Builtin.roc:
fold : Iter(a), acc, (acc, a -> acc) -> acc fold = |iterator, acc, step| match Iter.next(iterator) { Done => acc Skip({ rest }) => Iter.fold(rest, acc, step) One({ item, rest }) => Iter.fold(rest, step(acc, item), step) }fold通过递归消费迭代器:遇到Done返回累加器,遇到One用step(acc, item)更新累加器后继续。快照中的|acc, item| acc.append(item)就是逐步把元素追加到列表尾部;空迭代器直接返回初始值[],因此三个用例的输出可以完全由上述状态机行为推导出来。
六、REPL 快照的生成、运行与校验机制
6.1 快照工具如何执行 REPL 表达式
REPL 快照由 src/snapshot_tool/main.zig 处理。从源码(generateReplOutputSection,src/snapshot_tool/main.zig 附近)可以看到执行流程:
- 读取
SOURCE区块,按»字符切分,去掉空白后得到一组独立表达式(例如0.U128..<3.U128相关的Iter.fold(...)调用); - 创建
SnapshotReplSession,把每条表达式按顺序投入同一个会话求值——这意味着前一条表达式的定义可以被后续表达式引用,模拟真实 REPL 的连续性; - 逐条收集求值输出,与
OUTPUT区块按序比对;比对失败即报告REPL output mismatch at index N,从而在编译器行为变化时第一时间暴露回归。
6.2 常用命令
根据 test/snapshots/README.md 的 "Usage" 一节:
# 生成全部快照 zig build run-snapshot-tool # 只更新/运行指定快照 zig build run-snapshot-tool -- test/snapshots/repl/u128_range_until.md # 用当前实际输出覆盖期望值(谨慎使用) zig build run-snapshot-tool -- test/snapshots/repl/u128_range_until.md --update-expected # 调试 REPL 求值过程(打印解释器跟踪) zig build run-snapshot-tool -- test/snapshots/repl/u128_range_until.md --trace-eval--trace-eval只适用于type=repl的快照,且一次只能处理单个文件;跟踪输出在调试构建中默认开启,发布构建需通过-Dtrace-eval=true启用(见 src/snapshot_tool/main.zig 的参数校验逻辑)。
6.3PROBLEMS: NIL的含义
PROBLEMS区块记录类型检查阶段产生的诊断报告。NIL表示零报告,即快照中的三条 U128 开区间表达式通过了类型检查。若某条表达式存在类型错误(例如混用不同类型的区间边界),该区块会以 S 表达式形式列出完整诊断;对本快照而言,NIL恰好证明Range(U128)的构建与迭代完全符合类型系统约束。
七、测试矩阵中的佐证:U128 开区间不是孤例
7.1 类型检查层
src/check/test/range_test.zig 从类型系统层面固化了范围语义:
- 未标注类型的
0..<3默认推断为Range(Dec)(因为 Roc 数值字面量默认回退到Dec); - 泛型参数上的
start..<finish要求类型具备range_exclusive_to方法("exclusive syntax over generic operands requires range_exclusive_to"); - 两个边界类型必须统一,
1..<"five"会报Type Mismatch; - 整数范围同时支持
.iter()与.iter_rev(),而浮点范围(如F64)拒绝反向迭代(报Missing Method),因为 IEEE 浮点重复加法不可精确逆推。
7.2 求值层
src/eval/test/eval_tests.zig 中有专门覆盖所有数值类型范围的用例 "inspect: exclusive numeric ranges all iterate"(src/eval/test/eval_tests.zig),其中U128一行与快照直接呼应:
u128 = Iter.fold((1.U128..<3.U128).iter(), 0.U64, |acc, _| acc + 1) # u128 == 2,即开区间 (1, 3) 恰好产出 2 个元素同一用例里I128、Dec等 11 种数值类型全部产出2个元素,说明开区间语义在全部数值类型上保持一致。闭区间对应用例(src/eval/test/eval_tests.zig)中(1.U128..=3.U128)求和为 6,与快照[1, 2, 3]的成员一一印证。
7.3 快照族谱
test/snapshots/repl/目录下存在完整的范围快照族:u128_range_to.md、u128_range_until.md、i128_range_until.md、dec_range_until.md、f32_range_until.md、f64_range_until.md以及各整型(i8/i16/i32/i64/u8/u16/u32/u64)的_range_to/_range_until文件。这意味着"U128 开区间迭代从起点到终点"不是特殊处理,而是所有内建数值类型统一契约中的一个抽样验证点。
八、实战建议与延伸阅读
8.1 在真实代码中使用 U128 开区间
快照中的写法可直接迁移到实际 Roc 程序:
# 求和:0 + 1 + 2 total = Iter.fold((0.U128..<3.U128).iter(), 0.U128, |acc, item| acc + item) # 或直接使用 for 循环(自动调用 iter) var $sum = 0.U128 for n in 0.U128..<3.U128 { $sum = $sum + n } # 步长与长度提示 r = (0.U128..<10.U128).step_by(3) # Range(U128),步长 3 r.size_hint() # Known(4):0、3、6、9需要留意的前提:
U128无符号,无法表达负数,需要负范围请使用I128(见 test/snapshots/repl/i128_range_until.md 中的-2.I128..<2.I128);- 开区间写
..<、闭区间写..=,二者元素个数通常相差 1,for n in 0..<3只访问0、1、2; - 空范围是合法的,迭代不会崩溃,这在处理动态边界时非常安全(无需像其他语言那样先判断
start <= end)。
8.2 阅读路线
- 范围语法与数值类型总览:docs/langref/numbers.md("Ranges" 一节)、docs/langref/operators.md("Range Operators")
Range/Iter的完整实现与文档注释:src/build/roc/Builtin.roc- 范围运算符的静态分发说明:docs/langref/static-dispatch.md
- 快照测试体系与命令:test/snapshots/README.md、src/snapshot_tool/main.zig
- 类型检查与求值验证:src/check/test/range_test.zig、src/eval/test/eval_tests.zig
九、总结
test/snapshots/repl/u128_range_until.md 以三条 REPL 表达式精确固化了一个核心语言行为:U128 开区间start..<end从start开始、逐次加 1 递增、在上界之前结束,且当下界不小于上界时为空区间。从..<运算符到range_exclusive_to的静态分发,再到range_with_step构造、range_len_u128计算长度、range_iter_standard状态机迭代,整条链路在 src/build/roc/Builtin.roc 中清晰可查;而NIL的诊断结果与求值层测试的交叉验证,则保证了这份规格说明不仅"读得懂",而且"可运行、可回归、可依赖"。掌握这份快照,就掌握了 Roc 范围系统的设计基石与验证方法论。
【免费下载链接】roc
A fast, friendly, functional language.
相关推荐
Roc 语言 for 循环与空列表迭代语义:基于 REPL 快照测试的源码级解析
Roc 语言 for 循环与空列表迭代语义:基于 REPL 快照测试的源码级解析 本篇技术指南以 Roc 编译器仓库中的 REPL 快照测试 test/snap
VoiceFixer终极指南:三步让任何模糊语音重获新生的AI修复神器
VoiceFixer终极指南:三步让任何模糊语音重获新生的AI修复神器 你是否曾为模糊不清的录音而烦恼?那些珍贵的会议记录、历史采访录音,或者重要的语音备忘录,
Roc 语言 Dec 类型排他区间语义解析:从 REPL 快照测试看 `..<` 区间迭代
Roc 语言 Dec 类型排他区间语义解析:从 REPL 快照测试看 ..< 区间迭代 Roc 语言为 Dec (十进制浮点)等数值类型提供了 start..<
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考