Roc 二元运算符编译全链路解析:从 binops.md 快照看词法、规范化与类型检查
发布时间:2026/9/20 23:36:42来源:尧图网络
【免费下载链接】rocA fast, friendly, functional language.项目地址https://gitcode.com/GitHub_Trending/ro/roc点击查看免费下载本文以 Roc 语言编译器Zig 实现的快照测试用例 test/snapshots/binops.md 为核心骨架逐阶段拆解 - * / % //、 !、and or、??共 15 个二元运算表达式从源码到 Token、AST、规范化 IR、类型推断直至错误诊断的完整编译链路。读完本文你将理解 Roc 编译器快照测试的文件结构与运行方式掌握运算符在词法层如何被识别、在规范化层如何脱糖为方法派发与短路求值、在类型检查层如何被约束并能准确解释None ?? 0为何产生 Type Mismatch 诊断。快照测试体系test/snapshots 目录与快照文件结构Roc 编译器使用快照测试Snapshot tests来验证编译器的各阶段行为。按 test/snapshots/README.md 的说明快照测试通过针对特定 Roc 代码示例捕获编译管线每个阶段的输出来校验编译行为覆盖词法分析、解析、规范化、类型检查等环节每个快照文件记录了期望输出用于在编译器行为意外变化时检测回归。每个快照文件由若干带标题的分节组成binops.md 就是一个典型的typeexpr快照包含以下分节分节内容# META元信息descriptionBinops collection、typeexpr# SOURCE被编译的 Roc 源码片段# EXPECTED期望的编译结果摘要TYPE MISMATCH - binops.md:16:5:16:5# PROBLEMS语义诊断的 S-expression 序列化每个reporting.Report# TOKENS词法分析输出的 Token 序列# PARSE语法分析生成的 ASTS-expression 形式# FORMATTED格式化器的输出NO CHANGE表示源码已符合规范格式# CANONICALIZE规范化脱糖后的中间表示# TYPES类型推断得到的表达式类型普通快照typefile、typesnippet、typeexpr等的PROBLEMS分节记录的是诊断的语义——即reporting.Report的规范 S-expression 序列化见 src/reporting/report_sexpr.zig包含严重级别、标题、源码区域和完整文档结构但不含渲染细节无框线字符、ANSI 转义、换行折行等。这保证了诊断语义变化与渲染呈现变化分别落在不同的快照文件中互不干扰。binops.md 源码剖析一份覆盖四类运算符的全家桶快照的SOURCE分节是一个包含 15 个二元运算表达式的元组完整内容如下( 4 2, 4 - 2, 4 * 2, 4 / 2, 4 % 2, 4 2, 4 2, 4 2, 4 2, 4 2, 4 ! 2, 4 // 2, Bool.True and Bool.False, Bool.False or Bool.True, None ?? 0, )这 15 个表达式可以归为四类算术运算加、-减、*乘、/除、%取余、//向零截断整除比较运算小于、大于、小于等于、大于等于、相等、!不相等逻辑运算and逻辑与、or逻辑或作用于Bool.True/Bool.False标签默认值运算??Result 错误兜底作用于None ?? 0。快照TYPES分节给出了整个元组的推断类型(expr (type (Dec, Dec, Dec, Dec, Dec, Bool, Bool, Bool, Bool, Bool, Bool, Dec, Bool, Bool, Dec)))逐项对应前 6 项算术/整除表达式推断为DecRoc 的十进制数类型整数无标注字面量默认推断为Dec6 个比较表达式推断为Bool两个逻辑表达式推断为Bool最后的None ?? 0在存在类型错误的前提下仍参与推断并给出Dec的占位类型。这个类型签名是理解后续各阶段输出的目标答案。词法分析运算符如何被识别为 TokenTOKENS分节展示了词法分析的输出完整内容如下OpenRound, Int,OpPlus,Int,Comma, Int,OpBinaryMinus,Int,Comma, Int,OpStar,Int,Comma, Int,OpSlash,Int,Comma, Int,OpPercent,Int,Comma, Int,OpLessThan,Int,Comma, Int,OpGreaterThan,Int,Comma, Int,OpLessThanOrEq,Int,Comma, Int,OpGreaterThanOrEq,Int,Comma, Int,OpEquals,Int,Comma, Int,OpNotEquals,Int,Comma, Int,OpDoubleSlash,Int,Comma, UpperIdent,NoSpaceDotUpperIdent,OpAnd,UpperIdent,NoSpaceDotUpperIdent,Comma, UpperIdent,NoSpaceDotUpperIdent,OpOr,UpperIdent,NoSpaceDotUpperIdent,Comma, UpperIdent,OpDoubleQuestion,Int,Comma, CloseRound, EndOfFile,词法层揭示了几个重要事实每个运算符都有独立的 Token 标签OpPlus、OpBinaryMinus、OpStar、OpSlash、OpPercent、OpLessThan、OpGreaterThan、OpLessThanOrEq、OpGreaterThanOrEq、OpEquals、OpNotEquals、OpDoubleSlash、OpDoubleQuestion、OpAnd、OpOr这些标签统一定义于 src/parse/tokenize.zig 的Token.Tag枚举中第 104-129 行附近。and/or是关键字 Token 而非标识符Bool.True and Bool.False被切分为UpperIdent, NoSpaceDotUpperIdent, OpAnd, UpperIdent, NoSpaceDotUpperIdent说明逻辑运算符由词法器直接映射为OpAnd/OpOr。双字符运算符通过窥视下一个字符实现最长匹配。tokenize.zig 的逐字符扫描逻辑清晰地展示了这一模式!后跟生成OpNotEquals否则生成OpBang一元取反?后跟?生成OpDoubleQuestion否则生成OpQuestion/NoSpaceOpQuestion/后跟/生成OpDoubleSlash整除否则生成OpSlash浮点除后跟生成OpGreaterThanOrEq否则生成OpGreaterThan后跟生成OpLessThanOrEq后跟-生成OpBackArrow否则生成OpLessThan后跟生成OpEquals后跟生成OpFatArrow、*、%均为单字符直接映射为OpPlus、OpStar、OpPercent。负号存在一元/二元歧义处理扫描-时会通过canFollowUnaryMinus判断前一个 Token 能否接一元负号从而决定生成OpUnaryMinus还是OpBinaryMinus见 tokenize.zig。快照中4 - 2前的操作数是Int因此被正确识别为二元减号OpBinaryMinus。语法分析从 Token 流到 AST 元组PARSE分节将 Token 流组装成语法树。完整输出如下(e-tuple (e-binop (op ) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op -) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op *) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op /) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op %) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op ) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op ) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op ) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op ) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op ) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op !) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op //) (e-int (raw 4)) (e-int (raw 2))) (e-binop (op and) (e-tag (raw Bool.True)) (e-tag (raw Bool.False))) (e-binop (op or) (e-tag (raw Bool.False)) (e-tag (raw Bool.True))) (e-binop (op ??) (e-tag (raw None)) (e-int (raw 0))))语法层的关键观察整个源码是一个元组表达式e-tuple每个二元运算都是一个e-binop节点携带操作符文本、-、、??等与左右两个操作数子节点操作数统一表示为e-int整数字面量保留原始文本raw 4或e-tag标签字面量如Bool.True、None运算符优先级与结合性在此阶段已由 Parser 依据文法解析完毕——快照中所有表达式均为括号内的顶层元组元素不涉及嵌套优先级因此 AST 结构扁平清晰FORMATTED分节显示NO CHANGE意味着该源码片段已符合roc format的规范排版格式化器不会做任何改写。规范化运算符如何脱糖为底层 IRCANONICALIZE分节是理解 Roc 运算符语义的关键——它展示了每个二元运算符在规范化阶段被脱糖为何种更底层的中间表示(e-tuple (elems (e-dispatch-call (method plus) (constraint-fn-var 281) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-dispatch-call (method minus) (constraint-fn-var 297) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-dispatch-call (method times) (constraint-fn-var 313) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-dispatch-call (method div_by) (constraint-fn-var 329) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-dispatch-call (method rem_by) (constraint-fn-var 345) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-dispatch-call (method is_lt) (constraint-fn-var 362) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-dispatch-call (method is_gt) (constraint-fn-var 379) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-dispatch-call (method is_lte) (constraint-fn-var 396) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-dispatch-call (method is_gte) (constraint-fn-var 413) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-method-eq (negated false) (lhs (e-num (value 4))) (rhs (e-num (value 2)))) (e-method-eq (negated true) (lhs (e-num (value 4))) (rhs (e-num (value 2)))) (e-dispatch-call (method div_trunc_by) (constraint-fn-var 469) (receiver (e-num (value 4))) (args (e-num (value 2)))) (e-if (if-branches (if-branch (e-nominal-external (builtin) (e-tag (name True))) (e-nominal-external (builtin) (e-tag (name False))))) (if-else (e-nominal-external (builtin) (e-tag (name False))))) (e-if (if-branches (if-branch (e-nominal-external (builtin) (e-tag (name False))) (e-nominal-external (builtin) (e-tag (name True))))) (if-else (e-nominal-external (builtin) (e-tag (name True))))) (e-match (match (cond (e-tag (name None))) (branches (branch (patterns (pattern (degenerate false) (p-nominal-external (builtin) (p-applied-tag)))) (value (e-lookup-local (p-assign (ident #ok))))) (branch (patterns (pattern (degenerate false) (p-nominal-external (builtin) (p-applied-tag)))) (value (e-num (value 0))))))))))规范化层揭示了 Roc 二元运算的三条核心脱糖路径1. 算术与比较运算符 → 静态分派方法调用。 - * / %分别映射为plus、minus、times、div_by、rem_by//映射为div_trunc_by 映射为is_lt、is_gt、is_lte、is_gte统一表示为e-dispatch-call接收者 参数。运算符与 Token 标签的对应关系在 src/canonicalize/Can.zig 的finish_binop处理中完成OpPlus → add、OpBinaryMinus → sub、OpStar → mul、OpSlash → div、OpPercent → rem、OpLessThan → lt、OpGreaterThan → gt、OpLessThanOrEq → le、OpGreaterThanOrEq → ge、OpEquals → eq、OpNotEquals → ne、OpDoubleSlash → div_trunc、OpAnd → and、OpOr → or。这些方法在 src/canonicalize/BuiltinLowLevel.zig 中按数值类型族注册为底层内置函数如Builtin.Num.{s}.plus、Builtin.Num.{s}.div_trunc_by最终由各后端LLVM/WASM/解释器选择内联或调用内置函数实现。2./!→ 带否定标志的相等性比较。4 2规范化为e-method-eq (negated false)4 ! 2规范化为e-method-eq (negated true)。也就是说!在 IR 层并不是独立的运算符而是is_eq的否定形式。3.and/or→ 短路求值的if表达式。Bool.True and Bool.False被脱糖为e-if条件为左侧操作数为真时取右侧操作数、否则取FalseBool.False or Bool.True同样被脱糖为e-if条件为左侧操作数为真时取True、否则取右侧操作数。这正是 Can.zig 中的实现and生成if lhs { rhs } else { false }or生成if lhs { true } else { rhs }从而在 IR 层面天然获得短路求值语义——右侧表达式仅在必要时才被求值。4.??→ Result 匹配。None ?? 0被规范化为e-match分支一匹配Try(ok, err)模式并透传ok载荷e-lookup-local #ok分支二匹配错误载荷并用默认值0兜底。其实现位于 Can.zig 的canonicalizeDoubleQuestionOp先resolveTryNominalTarget解析 Try 目标再依次追加Ok透传分支、错误载荷通配分支和默认值分支最终组装为addTryMatch。这说明??是专门为 ResultTry(ok, err)类型设计的错误兜底运算符expr ?? default等价于expr为Ok(x)时取x、为Err(_)时取default。可对照 test/snapshots/expr/double_question_binop.md 中类型正确的用例get_name!({}) ?? Bob推断类型为Str印证这一语义。类型检查checkBinopExpr 如何约束二元运算TYPES分节是类型检查阶段的结论。结合 src/check/Check.zig 的checkBinopExpr可以还原每个运算符的类型约束规则算术运算add/sub/mul/div/rem/div_trunc映射到plus/minus/times/div_by/rem_by/div_trunc_by方法通过mkBinopConstraint创建静态分派约束lhs.method(rhs) - lhs——即返回值类型等于接收者类型而左右操作数并不强制统一允许用户自定义异构方法如times : Duration, I64 - Duration。对于内建数值类型方法签名是同质的如Dec.plus : Dec, Dec - Dec因此当一侧是具体数值类型而另一侧是开放字面量时会触发字面量默认化传播。快照中所有4 op 2的算术表达式均推断为Dec——无类型标注的整数字面量默认取Dec。比较运算lt/gt/le/ge映射到is_lt/is_gt/is_lte/is_gte强制lhs 与 rhs 统一为同一类型返回值是全新生成的Bool变量。快照中 6 个比较表达式全部推断为Bool。eq映射到is_eq同样统一左右操作数类型并返回Bool。!ne按 Check.zig 的注释a ! b脱糖为a.is_eq(b).not()——先建立is_eq约束带否定标志再对结果应用not一元运算返回值仍为Bool。这与规范化层e-method-eq (negated true)的形态完全对应。and/or左右操作数都必须统一为Bool通过unifyInContext分别约束两侧以产出更友好的双边错误信息表达式类型即Bool。数值操作数预检reportDefinitelyInvalidNumericBinopOperandCheck.zig会在字面量一侧与明确非数值类型如记录、元组运算时提前报告错误避免无意义的约束求解。诊断层None ?? 0为何报 Type Mismatch快照的EXPECTED分节明确要求编译器报出错误TYPE MISMATCH - binops.md:16:5:16:5第 16 行正是None ?? 0。PROBLEMS分节给出了完整诊断S-expression 形式(reports (report (severity runtime_error) (title Type Mismatch) (region (start 16 5) (end 16 14)) (headline (reflow The first pattern in this) (reflow ) (annotated code match) (reflow ) (reflow is incompatible.)) (document (source-underlines (display (file binops.md) (start 16 5) (end 16 14) (annotation dim) (line-text None ?? 0,)) (underline (start 16 5) (end 16 14) (annotation error))) (line-break) (reflow The first pattern is trying to match:) (line-break) (line-break) (annotation-start code-block) (indent 1) (text Try(ok, err)) (annotation-end) (line-break) (line-break) (reflow But the expression between the) (reflow ) (annotated code match) (reflow ) (reflow parenthesis has the type:) (line-break) (line-break) (annotation-start code-block) (indent 1) (text [None, ..]) (annotation-end) (line-break) (line-break) (reflow These can never match! Either the pattern or expression has a problem.))))这条诊断从语义上解释了错误的根因如规范化层所见??在 IR 中展开为一个match其第一个分支的模式是Try(ok, err)——即 Result 类型的Ok成功载荷而None是一个仅含None标签的开放标签联合类型记为[None, ..]用Try(ok, err)模式去匹配[None, ..]类型的值二者永远无法匹配These can never match!因此报出 Type Mismatch。也就是说??的左侧必须是 ResultTry(ok, err)类型的值None不是 Result把它当作??的左操作数在类型层面就是非法用法。这条诊断同时展示了 Roc 报告系统的结构severityruntime_error、titleType Mismatch、region源码区域 16:5-16:14、headline概括性标题与document带下划线标注的源码摘录 结构化文本块其中document部分与 src/reporting/renderer.zig 中的渲染逻辑对应而语义序列化则来自 src/reporting/report_sexpr.zig。实战如何运行与更新快照快照测试由snapshot_tool驱动src/snapshot_tool常用命令如下详见 test/snapshots/README.md# 生成全部快照 zig build run-snapshot-tool # 更新指定快照例如 binops.md zig build run-snapshot-tool -- test/snapshots/binops.md # 从 PROBLEMS 输出更新期望值 zig build run-snapshot-tool -- test/snapshots/binops.md --update-expected运行后会比对当前编译器各阶段输出与快照记录是否一致从而在词法、语法、规范化、类型检查或诊断语义发生意外变化时第一时间暴露回归。需要调试 REPL 快照的解释器求值过程时可加--trace-eval参数仅适用于typerepl快照。小结test/snapshots/binops.md 虽然只是一份快照文件却完整记录了 Roc 二元运算符从源码到类型结论的全链路行为词法层通过窥视字符实现双字符运算符的最长匹配TOKENS语法层生成扁平的e-binop元组 ASTPARSE规范化层将算术/比较运算符脱糖为静态分派方法调用、将and/or脱糖为短路if、将/!脱糖为带否定标志的相等比较、将??脱糖为 Result 匹配CANONICALIZE类型检查层依据checkBinopExpr的规则约束操作数并完成字面量默认化TYPES诊断层则以结构化 S-expression 呈现None ?? 0的 Type MismatchPROBLEMS。读懂一份快照就等于读懂了编译器这一条完整管线。赞分享【免费下载链接】rocA fast, friendly, functional language.项目地址https://gitcode.com/GitHub_Trending/ro/roc点击查看免费下载相关推荐Roc 编译器一元取反Unary Negation全链路解析从快照测试看 -expr 的分词、解析、规范化与类型检查Roc 编译器一元取反Unary Negation全链路解析从快照测试看 expr 的分词、解析、规范化与类型检查 导读 本文以 Roc 编译器仓库中的快Roc 字符串插值编译链路深度解析从快照测试看词法、解析与规范化实现Roc 字符串插值编译链路深度解析从快照测试看词法、解析与规范化实现 本篇文章以 Roc 编译器仓库中的快照测试 string_interpolation_s从 binop_omnibus 快照看 Roc 二元运算符优先级与编译器快照测试机制从 binop_omnibus 快照看 Roc 二元运算符优先级与编译器快照测试机制 本文以 Roc 语言编译器仓库中的快照测试文档 test/snapshot上一篇为什么选择PainterEngine10个让你爱不释手的强大功能下一篇重塑前端图像处理探索Webp2jpg-online的革新之路创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
网站建设高端定制企业官网