
Roc 编译器requires的for子句与平台类型变量基于快照测试的语法与实现解析【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/rocRoc 是一门快速、友好、函数式的语言。本文以 test/snapshots/platform/platform_type_vars.md 快照文件为核心深入讲解平台模块platform头部requires区段中for子句的语法、类型变量type variable的绑定方式以及编译器从词法分析、语法解析到类型推断的完整处理链路。读完本文你将掌握如何用[Model : model] for main : ...为平台入口声明多态类型变量并理解快照测试如何印证编译器的每一层实现。快照文件编译器行为的第一手记录在 Roc 编译器中test/snapshots/目录下的.md文件不是普通文档而是快照测试每个文件把一段 Roc 源码输入编译器然后把编译过程中各阶段的输出分词、解析树、格式化结果、规范 IR、类型推断固化下来作为回归测试的基准。test/snapshots/platform/platform_type_vars.md正是其中一个专门验证platform头中for子句类型变量的快照。该文件结构清晰各区块对应编译器的一个阶段区块含义对应编译器阶段# META快照元信息description、type测试配置# SOURCE被测的 Roc 源码输入# EXPECTED期望的问题报告诊断# TOKENS词法分析输出tokenize# PARSE语法分析树Parser# FORMATTED格式化后的源码格式化器# CANONICALIZE规范化 IR语义分析# TYPES类型推断结果类型推断被测源码一个带for子句的最小平台快照中的# SOURCE是一个最小的 platform 模块platform requires { [Model : model] for main : { init : model, update : model, I64 - Model, render : model - I64 } } exposes [] packages {} provides { roc_main: main } main : { init : Model, update : Model, I64 - Model, render : Model - I64 } main { crash todo }这段代码的核心是requires里的[Model : model] for main : ...。它声明平台要求宿主host提供一个名为main的入口该入口的类型是一个记录类型包含init、update、render三个字段而[Model : model]是一个for子句类型别名把大写别名Model绑定到小写刚性类型变量model从而让main的类型成为参数化多态的——这正是“平台类型变量”platform type vars名称的由来。# EXPECTED为NIL表示该源码不产生任何编译问题是合法的正例测试。for子句语法[Alias : var] for entrypoint : type语法形态for子句出现在requires区段的某个入口条目之前语法为[UpperAlias : lower_var, ...] for entrypoint_name : type方括号内一个或多个以逗号分隔的类型别名声明每个别名由大写别名 : 小写类型变量组成。快照中[Model : model]即把Model绑定到model。for关键字连接别名列表与被参数化的入口名。入口类型:之后写出使用这些别名的类型。快照中main的类型使用了modelinit : model、update : model, I64 - Model、render : model - I64同时返回值使用别名Model。词法层面for是一个关键字编译器把for视为保留关键字。在 src/parse/tokenize.zig 的关键字表中约 588 行可以看到.{ for, .KwFor },并且KwFor出现在三类 token 关键字分组中见 tokenize.zig 中 258-262、419-423 等行的关键字列表意味着在语法解析的多个上下文里它都会被识别为关键字 token。解析层面专门的错误恢复与别名结构Parser.zig在解析requires条目时专门处理[开头的for子句约 1676-1723 行if (self.peek() .OpenSquare) { self.advance(); while (self.peek() ! .CloseSquare and self.peek() ! .EndOfFile) { // 解析 大写别名 : 小写变量 // alias_name 必须是 UpperIdent // 之后必须有 OpColon // rigid_name 必须是 LowerIdent } self.expect(.CloseSquare) catch { ... return .malformed .expected_for_clause_close_square }; self.expect(.KwFor) catch { ... return .malformed .expected_for_keyword }; }对应的错误码定义在 src/parse/AST.zig约 448-454 行它们给出了非常明确的修复提示可直接作为写作时的语法参考expected_for_clause_alias_name别名必须以大写字母开头示例[Arg : a] for main : a - I32expected_for_clause_colon别名与类型变量之间用:分隔expected_for_clause_rigid_name:右侧必须是小写类型变量expected_for_clause_close_square别名列表以]结束expected_for_keyword别名列表后必须写forexpected_for_clause_entrypoint_namefor后必须是小写入口名expected_for_clause_type_colon入口名后写:再接类型。AST 数据结构ForClauseTypeAlias结构体定义在 src/parse/AST.zig约 2389-2395 行/// A type alias mapping in a for-clause: Model : model /// Maps an uppercase alias (Model) to a lowercase rigid variable (model) pub const ForClauseTypeAlias struct { /// The alias name token (e.g., Model) - UpperIdent alias_name: Token.Idx, /// The rigid variable name token (e.g., model) - LowerIdent rigid_name: Token.Idx, ... };从源码结构可以推断alias_name大写别名与rigid_name小写刚性变量分别以 token 索引存储rigid_name的注释明确称其为“刚性变量”rigid variable说明for子句里的类型变量在类型系统中被当作刚性变量处理——即一旦绑定就不能被任意替换。# TOKENS词法分析如何切分这段源码快照的# TOKENS区块记录 tokenize 阶段的输出把[Model : model] for main : ...切分为OpenSquare, UpperIdent, OpColon, LowerIdent, CloseSquare, KwFor, LowerIdent, OpColon, OpenCurly, LowerIdent, OpColon, LowerIdent, Comma, LowerIdent, OpColon, LowerIdent, Comma, UpperIdent, OpArrow, UpperIdent, Comma, LowerIdent, OpColon, LowerIdent, OpArrow, UpperIdent, CloseCurly,逐项对照源码源码片段Token 序列[Model : model]OpenSquare, UpperIdent(Model), OpColon, LowerIdent(model), CloseSquarefor main :KwFor, LowerIdent(main), OpColoninit : modelLowerIdent, OpColon, LowerIdentupdate : model, I64 - ModelLowerIdent, OpColon, LowerIdent, Comma, UpperIdent, OpArrow, UpperIdentrender : model - I64LowerIdent, OpColon, LowerIdent, OpArrow, UpperIdent这验证了词法层的关键事实Model是大写标识符UpperIdentmodel/main/init/update/render是小写标识符LowerIdentfor是独立关键字KwFor-是OpArrow。大小写区分正是后续解析器区分“别名”与“类型变量”的依据。# PARSE语法树中的 for 子句结构# PARSE区块展示了解析树其中requires条目被解析为requires-entry(requires-entry (type-aliases (alias (name Model) (rigid model))) (entrypoint main) (ty-record (anno-record-field (name init) (ty-var (raw model))) (anno-record-field (name update) (ty-fn (ty-var (raw model)) (ty (name I64)) (ty (name Model)))) (anno-record-field (name render) (ty-fn (ty-var (raw model)) (ty (name I64))))))对应到 src/parse/AST.zig 2027-2035 行附近的解析树生成逻辑alias节点把alias_name序列化为name把rigid_name序列化为rigid所以快照中出现(alias (name Model) (rigid model))。同时init字段的类型是(ty-var (raw model))——直接用类型变量而update、render字段是ty-fn函数类型返回值里出现的是(ty (name Model))——即通过别名引用。这里可以清楚看到两种引用方式的区别直接使用小写变量model以ty-var出现通过大写别名引用Model以具名类型ty (name Model)出现由解析树中的alias节点建立映射。platform节点还包含(name )平台名称为空字符串、(exposes)空暴露列表、(packages)空包列表、(provides (symbol-map-entry (symbol roc_main) (func main)))对外提供roc_main符号指向函数main。文件主体的statements部分则解析了main的类型注解与定义类型注解是同样的记录/函数类型结构main的定义是s-decle-block内部是s-crash抛出字符串todo即main { crash todo }。# FORMATTED格式化器的规范输出# FORMATTED区块给出格式化后的规范写法可作为书写for子句的格式基准platform requires { [Model : model] for main : { init : model, update : model, I64 - Model, render : model - I64 } } exposes [] packages {} provides { roc_main: main } main : { init : Model, update : Model, I64 - Model, render : Model - I64 } main { crash todo }注意格式化器的两个细节requires的整个入口被压缩为一行[Model : model] for main : {...}而main的定义体被展开为多行块。这说明快照测试同时承担了格式化回归测试的职责——手写风格不一致没关系编译器会统一到这种规范形式。# CANONICALIZE规范化 IR 中的类型解析# CANONICALIZE区块展示语义分析后的规范 IR。类型注解中的引用被解析为两类查找(ty-record (field (field init) (ty-lookup (name Model) (local))) (field (field update) (ty-fn (effectful false) (ty-lookup (name Model) (local)) (ty-lookup (name I64) (builtin)) (ty-lookup (name Model) (local)))) (field (field render) (ty-fn (effectful false) (ty-lookup (name Model) (local)) (ty-lookup (name I64) (builtin)))))关键观察Model被解析为(ty-lookup (name Model) (local))——本地当前模块内查找即别名Model在本地作用域中解析通过for子句绑定到刚性变量I64被解析为(ty-lookup (name I64) (builtin))——内置类型查找说明I64是编译器内置类型(effectful false)标记函数无副作用effectful 标志说明update、render是纯函数类型。main的值部分被规范化为(d-let ... (e-run-low-level (op crash) (args (e-string (e-literal (string todo))))))即crash被建模为底层运行调用run-low-level参数是字符串字面量todo。# TYPES类型推断的最终结果# TYPES区块给出类型推断结论(inferred-types (defs (patt (type { init: Model, render: Model - I64, update: Model, I64 - Model }))) (expressions (expr (type { init: Model, render: Model - I64, update: Model, I64 - Model }))))main被推断为记录类型{ init: Model, render: Model - I64, update: Model, I64 - Model }定义defs与表达式expressions类型一致。这里的Model是经由for子句绑定的类型变量别名——平台作者在编写应用时可以把Model替换为任意具体类型从而让同一个平台模板适用于不同的模型类型。对照实验没有for子句的固定类型平台同目录的 test/snapshots/platform/platform_int.md 提供了极佳的反面对照。它的requires不使用for子句而是直接声明固定类型platform requires { multiplyInts : I64, I64 - I64 } exposes [] packages {} provides { roc_multiplyInts: multiplyInts } multiplyInts : I64, I64 - I64注意# EXPECTED不再是NIL而是两个诊断EXPOSED BUT NOT DEFINEDprovides声明暴露了multiplyInts但模块内未定义DECLARATION HAS NO VALUEmultiplyInts只有类型注解、没有实现体。对应# PROBLEMS区块中的两条报告runtime_error与warning。这告诉我们provides中暴露的名字必须在模块内实际定义除非像平台类型模块那样由宿主边界提供并且只有注解没有值的声明会被警告。对照两个快照可以看出platform_type_vars的快照把main定义为crash todo正是为了满足“被提供的符号必须被定义”这一约束从而让测试聚焦于for子句本身。实战要点总结for子句语法[Model : model] for main : type——方括号内大写别名 : 小写类型变量多个别名用逗号分隔for后是入口名与类型。参考格式化输出test/snapshots/platform/platform_type_vars.md 的# FORMATTED区块。大小写即语义大写Model是别名UpperIdent小写model是刚性类型变量LowerIdent解析器据此区分for是保留关键字KwFor见 src/parse/tokenize.zig。解析与错误提示requires条目的for子句解析逻辑在 src/parse/Parser.zig相关错误码如expected_for_clause_rigid_name、expected_for_keyword及修复示例在 src/parse/AST.zig。类型解析路径别名在本地作用域解析ty-lookup ... (local)I64等为内置类型(builtin)可在# CANONICALIZE区块验证。被提供的符号必须定义provides中列出的入口若在模块内无定义会触发EXPOSED BUT NOT DEFINED诊断见 test/snapshots/platform/platform_int.md 的# PROBLEMS区块。一句话总结platform_type_vars.md快照把“平台requires的for子句类型变量”从词法、语法、格式化、规范 IR 到类型推断的每一层编译器行为都固化了下来是理解 Roc 平台多态入口类型的最佳入口如果你想看更多平台头快照空头部、字符串平台、目标输出类型等同目录下还有platform_header_empty_1.md、platform_header_str_simple.md、platform_header_targets_output_kinds.md等文件可供对照学习。【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考