搜索/修改输出 JavaScript 的编译器及其构建系统,并逐层测试

修改输出 JavaScript 的编译器及其构建系统,并逐层测试

在输出 JavaScript 的编译器 monorepo 中让 AI 做修改时使用。它通过任务运行器构建,不改已冻结的语法树定义,给改动的每一层补测试,把新的构建选项贯通到底,并用实测而不是 linter 的意见来判断速度。

要使用这个AI规则
## 通过任务运行器构建,不跳过任何一级 使用仓库自带的任务运行器,而不要手工调用底层的构建工具。默认目标构建平台工具链;另有独立的目标用于添加标准库、运行测试套件、格式化代码,以及在不改写代码的前提下检查格式。 ### 各目标彼此层层依赖 任何需要 JavaScript 工具链的目标都会自动执行依赖安装。默认构建会产出工具链的二进制文件——既包括编译器可执行文件,也包括独立的构建系统二进制文件——并把它们复制到平台专属的包目录中。标准库目标会使用这些工具链产物来编译运行时源码。测试目标则复用其上方的全部内容。 ### 不要跳过任何一层 由于每个目标都依赖它下面的那个目标,直接调用较低层的工具可能会让陈旧的二进制文件残留下来,产生与干净构建不一致的结果。当构建结果看起来不可能发生时,先运行 clean 目标并从最上层重新构建,然后再继续排查。 ## 不改已冻结的语法树定义,改为更新转换模块 有些文件是被刻意冻结的,因为外部的解析器扩展依赖它们的确切形态。绝不要修改语法树定义的冻结版本。当实时语法树发生变化时,应改为更新在实时表示与冻结表示之间进行转换的那两个映射模块,这样扩展就能继续正常工作,而主语法树仍可自由演进。 ### 与原始 fork 的兼容性不是约束条件 本编译器最初是另一个编译器的 fork,但本项目维护自己的语法树,并且可以做出相对于原项目的破坏性变更。判断依据应当是什么最有利于当前的编译目标,而不是上游项目怎么做。 ### 避免保留新的关键字 优先选择不需要新增保留字的实现方式,因为保留一个新词会破坏已经在使用该名称的现有代码。把新关键字当作最后的手段,先去寻找能复用现有语法结构的写法。 ## 修好根本问题,不屏蔽警告,也不用通配符占位 绝不要使用抑制警告的属性来让编译器闭嘴。应当修复底层的问题——被抑制的警告恰恰隐藏了该警告本来要捕捉的那一类错误。 ### 通配符是一个决定,而不是一条捷径 不要把通配符模式当作新语言特性的占位符,然后就把它们忘了。只有当你确信在那个具体场景中该值确实应当被忽略时才使用它,并确认每一项新语言特性都在所有编译器层中被完整处理。 ### 显式丢弃结果,并把状态贯穿传递 如果调用某个函数只是为了它的副作用,应使用显式的 ignore 函数,而不是把结果绑定到通配符上。绝不要丢弃带状态的结果——要把它沿调用链一路传递下去。 ### 让模式匹配保持诚实的小约定 不要给已经带有必填标签参数的函数再加一个末尾的 unit 参数;标签本身就已经防止了意外的部分应用。当记录模式所针对的那个警告在本代码库中已被禁用时,就省略末尾的通配符。留意不同中间表示中名称相似的构造子,因为同一个变体名在不同的编译阶段可能含义不同。 ## 动手前先弄清要改的是哪一个编译层 整条流水线是:在解析器中把源文本转换成表层语法树,施加前端变换,在类型检查器中产出带类型的树,下降为 lambda 级中间表示,转换为 JavaScript 级中间表示,最后生成代码。在动手改动之前,先确认你正在编辑的是哪一层。 ### 各层对应到各个目录 syntax 目录负责解析与表层语法。frontend 目录负责语法树变换以及外部函数接口的处理。类型检查器目录负责类型检查与带类型树的变换。core 目录同时存放 lambda 级和 JavaScript 级的模块,通过文件前缀加以区分。另有独立的目录分别存放扩展工具与数据结构、面向目标生态系统的类型定义生成,以及语言服务器与配套工具。 ### 改动通常会横跨多层 一项新的语言特性通常依次需要:解析器改动、语法树定义更新、类型检查支持、lambda 级处理、JavaScript 生成,以及测试。漏掉其中任何一层,就会得到一个能顺利解析、随后却编译错误的特性。 ## 打印各阶段的中间表示来找出 bug 编译器二进制文件接受若干标志,用于打印各个阶段的结果:预处理后的源码、解析后的解析树、类型检查后的带类型树,以及未优化的 lambda 表示。在你开始阅读编译器源码之前,先把每个阶段的转储结果与你的预期进行对比。 ### 把症状定位到负责它的那一层 输出格式方面的问题属于 pretty-printer 模块。类型错误属于类型检查器相关模块。只在优化之后才出现的错误结果属于 lambda 级的分析 pass。生成的代码格式不正确则属于 JavaScript 生成模块。 ### 用小规模用例复现 在迭代过程中,请编译单个最小文件,而不是整个项目。当某个表示需要检查时,可以在 lambda 打印模块中临时添加打印语句,并在改动合入之前移除这些临时插桩。 ## 改完一层就立刻给这一层补测试 每修改一个编译器层,就立刻补上测试,而不是等所有改动都完成之后再补。测试补得太晚会掩盖是哪一层引入了回归,把一次小范围的二分查找变成对整份改动的排查。 ### 总是需要测试的情形 新语言特性和缺陷修复始终需要测试。修改分析 pass 或改变 JavaScript 生成始终需要测试。始终要检查生成的 JavaScript 的质量,而不仅仅是它的正确性。 ### 覆盖到正确的套件 解析器测试验证语法。集成测试覆盖端到端行为。单元测试覆盖单个编译器函数。构建测试覆盖错误情形与边界情形,包括单文件类型错误,以及需要分别提供实现文件和接口文件的跨模块错误。往返转换、生成类型、分析和工具链等套件各有自己的目标,当对应领域发生变化时应当运行它们。 ## 在同一次修改中更新错误和警告的目录 仓库中按模块维护着一份目录,列出编译器可能产生的每一个错误和警告,每一条目要么映射到一个测试夹具,要么映射到一条说明它为何不可达的书面理由。该目录是发现覆盖率缺口和识别可删除死代码的主要工具,因此过期的条目会让这两项工作都变得更困难。 ### 在与代码同一次改动中更新目录 当你新增或删除一个错误变体时,要在对应的模块章节中新增或删除它那一行,把它的状态设置为已覆盖、不可达或待办,并且要么链接到覆盖它的夹具,要么记录该变体为何无法被触发。当你新增或删除一个夹具时,要更新受影响那一行的夹具路径与状态,使目录与测试套件永远不会脱节。 ## 保持编译速度,并让输出的 JavaScript 易读 本编译器的设计目标是快速的反馈循环,并且必须能扩展到大型代码库,同时它生成的输出是要给人阅读的。这两项特性都是评审标准,而不是事后才考虑的东西。 ### 让生成的代码保持易读 避免产出无意义的生成符号,保持输出的可读性。一项能产出正确但不可读的输出的改动,不算完成。 ### 前后都要测量 对每一项改动都要考虑其对编译速度的影响,在 lambda 级和 JavaScript 级使用合适的优化 pass,并且对任何以性能为动机的改动,都要在改动前后做性能剖析,而不是靠读代码来推断。 ## 遵守命名规则,并为用户可见的改动写变更日志 要匹配你所使用语言的命名约定:编译器的实现语言使用蛇形命名法,而用被编译语言本身写成的源码使用驼峰命名法。遵循代码库中已有的模式,优先使用现有的工具函数而不是重新造轮子,为复杂算法和不显而易见的逻辑写注释,并在合理的范围内保持向后兼容。 ### 提交附带的义务 按照项目要求的形式对提交进行签署,为每一项改动附上相应的测试,并在提交之前确认构建通过。 ### 记录面向用户的改动 对任何面向用户的改动都要添加一条变更日志条目,无论它是缺陷修复、新特性还是破坏性变更。把它放在当前未发布版本对应的章节之下,并在该行末尾附上指向该贡献的链接。提交的内容中应当包含这样一条。 ## 把新的构建系统选项加到它经过的每一处 配套的构建系统用一门系统级语言编写,提供快速的增量构建和更好的错误报告。它的模块划分很清晰:核心数据结构(如构建状态与模块记录)、编译与编译器参数生成、语法树生成与解析器参数、包发现与依赖解析、模块依赖图与环检测、产物清理、构建日志与错误报告、命令行界面、项目配置解析、文件监视器,以及入口点。 ### 在选项经过的每一处都要添加它 在 build 和 watch 两种参数类型中都声明该标志,并更新它们之间的转换。为任何新的项目配置字段扩展配置解析器。修改相关的构建模块。然后把该参数贯穿整条链路:从入口点传入构建函数,再传入构建初始化,再传入构建状态,并且在 watch 路径上同样如此。为新行为添加单元测试。 ### 提前确定优先级与错误处理 命令行标志覆盖项目配置文件。通过项目标准的错误包装 result 类型来传播错误,而不是直接 panic,并使用 debug 级别的日志来输出开发期诊断信息。 ## 指向本地编译器,直接运行构建系统 像持续集成那样,从仓库根目录通过指向其清单文件的方式运行构建系统自己的工具链命令,而不要 cd 进入该子项目。任务运行器同样提供了用于构建它以及运行其集成测试的快捷方式。 ### 让开发版二进制文件指向本地工具链 如果直接运行,该二进制文件否则会去使用已安装的发行版。在运行它之前,先导出指向编译器可执行文件路径和运行时路径的环境变量,两者都要解析为当前检出目录内的绝对路径。请注意,开发版二进制文件的名字可能取自包名而不是工具名,所以要检查实际产出的文件名,不要想当然。 ### 当行为不明朗时提高日志详细程度 构建系统接受逐级递增的详细程度标志,分别对应 info 级和 debug 级日志。这正是你查看细节的方式,比如某次 watch 会话实际在监视哪些文件夹。 ### 追踪遵循标准规范 分布式追踪完全通过标准的遥测环境变量来配置——收集器端点、追踪专用的端点覆盖、导出器请求头、服务名、资源属性和日志级别——没有任何项目专有的开关。设置端点就是启用遥测的开关;如果一个都不设置,追踪就是空操作。当你希望事件出现在追踪中时,要通过 tracing 宏而不是普通的日志宏来产生事件,因为这两者使用的是彼此独立的系统。 ## 让 CI 测试等待条件成立,而不是固定休眠 持续集成的执行机比开发机器慢,这会让固定时长的 sleep 变得脆弱。请对你真正关心的条件进行轮询,例如等待某个文件出现,而不是按猜测的时长睡眠。 ### 异步关闭并不等于已经结束 一个用来通知 watcher 退出的辅助函数——比如通过删除锁文件来实现——只是发出了信号;它并不会等待进程真正终止。避免在退出之前触发配置变更事件,因为 watcher 可能会启动一次你没有预料到的并发重建。 ### 不要自己手写平台相关的文本编辑 就地流式编辑在不同操作系统上的行为不同,所以在 shell 测试中要使用仓库提供的替换与路径规范化辅助工具,而不是直接调用原始的编辑器命令。单个集成脚本可以单独运行:导出可执行文件路径和运行时路径,source 共享的辅助文件,然后调用那一个脚本即可,这在无需运行整个套件就想快速迭代时很有用。 ## 用实际性能来判断 linter 的建议 linter 的建议是参考输入,不是判决。当某项被建议的重构可能损害性能或清晰度时,要明确地权衡取舍,而不是机械地照搬该建议。 ### 只在值得的地方接受复杂度 参数结构体并不自动就比多个参数更好;对于那些确实需要许多参数、而结构体只会徒增仪式感的函数,可以在局部允许该 lint。当一次拷贝规模很小、只发生一次、是所有权规则所要求的,并且不在热点路径上时,它是可以接受的——不要为了绕开它而过度设计。 ### 依据证据来优化 先做性能剖析,再做优化。在构建系统中,大多数性能上的顾虑与实际编译时间相比都是微不足道的。而在效率确实重要的地方,要沿着调用链保持参数类型一致,而不是在每一个边界上都在拥有所有权的字符串类型和借用的字符串类型之间来回转换,因为这些转换会白白增加内存分配。只重新编译脏模块,把彼此独立的工作并行化,在大型项目上关注构建状态的内存占用,并尽量减少文件系统操作。