Roc 编译器 RcEffect 所有权表一致性校验:从 10023 泄漏事故到双层级验证体系 Roc 编译器 RcEffect 所有权表一致性校验从 #10023 泄漏事故到双层级验证体系【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc导读本文围绕 Roc 编译器A fast, friendly, functional language中一张极其关键但长期“无人监管”的表——RcEffect所有权行为表展开。这张表由LowLevel.rcEffect()为每个底层内置操作builtin逐项返回是借用推断与 ARC引用计数自动插入的唯一决策依据但表中每一行都是手写的“声称”此前没有任何机制验证它与对应 Zig builtin 的真实行为是否一致。PR roc-lang/roc#10023issue #9953证明了一行错误的表项会以静默引用计数失衡的形式泄漏内存。读完本文你将掌握RcEffect的字段语义与构造函数族、两类错误的判定方法、以及当前仓库中已落地的双层校验方案comptime 结构校验 解释器驱动的逐操作一致性测试的源码实现细节。问题背景一张无人校验的中央所有权表所有权决策集中化的架构Roc 编译器把每个底层内置操作的“所有权行为”集中放在一张表中——RcEffect。该表由LowLevel.rcEffect()按操作逐个返回定义见 src/base/LowLevel.zig 起的 switch 分支并同时被两个 ARC 相关消费点使用借用推断src/lir/arc_solve.zig中的result_shares_args逻辑把结果链接到出借方lender参数src/lir/arc_solve.zig 附近的assign_low_level分支ARC 插入result_unique声明结果诞生于一次全新的独立分配见 src/lir/arc_solve.zig 附近及 src/lir/arc_solve.zig 对result_unique的消费。这种架构本身是正确的所有权只在编译器中被决策一次并携带传递而不是在每个后端各自重新决策一遍从而保证所有后端行为一致。问题本质手写声称无人检验但表中每一行都是手写的、关于对应 Zig builtin 实际行为的“声称”此前没有任何东西去检验它。RcEffect的标记位表达能力足够强足以描述微妙的所有权状态一个外层全新、但内部共享某参数分配的返回值运行时唯一性探测操作在“原地修改”与“复制”两条路径上结果都唯一。表达能力强的另一面是它也足以把内置操作描述错。而错误的一行不是编译错误、不是 panic、也不是测试失败——它是生成代码里一次静默的引用计数失衡要么泄漏refcount 偏高要么 use-after-freerefcount 偏低。#10023一行错误表项的真实事故PR roc-lang/roc#10023issue #9953就是证明retainsSharingArgs构造函数族在设置result_shares_args的同时设置了result_unique true于是 ARC 既把结果链接到出借方参数又统计了一次“全新诞生”——每一次成功的Json.parse都会向它的输入字符串泄漏一个引用。这个 bug 的传播路径值得注意bug 藏在表中属于结构性合法、但行为不符的行见下文“两类错误”由于所有权表是集中的bug 通过所有后端一致地传播集中化发挥了作用它逃过了类型检查、全部功能测试最终只是被一个手写的 host-effects 测试偶然捕获——那个测试恰好在该路径上断言了存活分配计数。也就是说今天仓库中对 refcount 平衡的覆盖只存在于“某个测试碰巧想到要断言它”的地方。RcEffect 核心结构字段、构造函数族与证明义务结构定义与字段语义RcEffect定义于 src/base/LowLevel.zig是一个携带逐操作掩码与标记位的结构体字段如下字段类型语义may_allocatebool该操作可能调用allocateWithRefcount诞生分配may_retain_or_releasebool该操作可能改变某个分配的计数may_runtime_uniqueness_check_argsu64 掩码运行时检查这些参数是否唯一以选择原地修改还是复制consume_argsu64 掩码该操作消费这些参数的一个所有权单元result_aliases_consumed_argsu64 掩码从被消费参数取得的所有权单元存活于结果中retain_argsu64 掩码该操作为这些参数增加一个计数典型场景结果中存了指向它们的句柄retain_resultbool结果别名指向保持存活的结构元素、box payload、捕获、借用视图ARC 需在操作后为其 retainresult_borrows_argsu64 掩码结果指向这些参数的 payload 但不拥有它们ARC 跨结果的所有使用保持出借方存活result_shares_argsu64 掩码结果是全新的自有外层值但其内部共享这些参数的分配无缝切片、字节重解释result_uniquebool返回时结果最外层分配计数为 1与参数共享外层分配的结果永不unique内部共享与最外层计数无关证明义务doc comment 明确写下的契约src/base/LowLevel.zig 的文档注释现在精确陈述了每一行的“证明义务”行内省略may_allocate即承诺该操作从不诞生分配行内省略may_retain_or_release即承诺该操作能触达的每个计数都不被改动may_runtime_uniqueness_check_args命名的位置必须是该操作消费的位置且原地路径在参数真的唯一时必须健全ARC 可能证明检查冗余并直接传入unique_argsconsume_args命名的每个参数操作必须在所有路径上 release 或移入结果retain_args命名的参数操作必须自己 retain——ARC 不会代为发出 retain声明却不 retain 会让存储的句柄计数不足result_borrows_args与result_aliases_consumed_args不能重叠结果不能从一个已被消费的参数借用result_unique意味着返回时最外层分配计数为 1运行时唯一性检查操作在两条路径上都满足原地保留原本计数为 1 的分配复制路径返回全新分配。写新行应当是“对这些义务逐条兑现”而不是模式匹配相邻行。构造函数族行通常不直接写裸字段而是通过构造函数族生成src/base/LowLevel.zig常用族包括none()无任何所有权效应allocates()/allocatesRetainingArgs(mask)/allocatesConsumingArgs(mask)纯分配或分配并保留/消费参数retainsOrReleases()/retainsResult()/retainsResultBorrowingArgs(mask)调整计数、保留结果、保留结果并借用参数runtimeUniqueness(mask)运行时唯一性检查族含consume_args、result_aliases_consumed_args、result_unique全套runtimeUniquenessMaybeSharedResult(mask)与上者唯一区别是不声明result_uniquetrim 族返回参数分配的切片runtimeUniquenessRetainingArgs(runtime_mask, retain_mask)检查的同时还保留某些参数retainsSharingArgs(mask)#10023涉及的族声明保留参数且结果内部共享参数分配如str_drop_prefix返回参数分配的切片retainsOrReleasesSharingArgs(mask)/allocatesAndRetainsOrReleasesSharingArgs(mask)共享 计数调整/分配consumesArgsRetainingArgs/consumesArgsReturningConsumedArgsRetainingArgs/consumesArgsReturningConsumedArgs消费参数的组合。表中代表性行速览rcEffect()的 switchsrc/base/LowLevel.zig是数百行手写行的集合例如str_concat→runtimeUniqueness(argMask(.{0}))运行时唯一性检查第 0 参数str_trim/str_trim_start/str_trim_end→runtimeUniquenessMaybeSharedResult共享字符串的 trim 返回参数分配的无缝切片外层计数继承参数str_drop_prefix/str_drop_suffix/str_substring_unsafe→retainsSharingArgs保留参数、结果内部共享其分配list_sublist_borrowed→retainsResultBorrowingArgsbox_box→allocatesRetainingArgsbox_unbox_borrowed→retainsResultBorrowingArgsbox_prepare_update→runtimeUniquenessptr_store→consumesArgsRetainingArgsptr_alloca等 TRMC 引入的指针操作 →none()。两类错误结构性无效与非一致性文档明确指出错误分两类对应两层不同的验证手段结构性无效行structurally invalid rows任何内置操作都不可能真实拥有的组合或掩码引用了该操作根本不存在的参数位置。这类错误仅凭表本身即可在 comptime 检查。非一致性行non-conforming rows结构上合法、但声称与实际src/builtins/*.zig的实现不符#10023正是此类。这类错误只能通过运行内置操作并观察引用计数来发现。当前仓库已按此设计落地了两层机制详见下文其模块职责划分在 src/base/rc_effect_rules.zig 的头部注释中有明确说明。第一层comptime 结构校验rc_effect_rules.zig规则集每条规则对应一个字段src/base/rc_effect_rules.zig用“字段之间的蕴含式”表达规则规则只拒绝行、绝不修补或重解释行13 条规则覆盖RcEffect的每个字段规则含义retain_args_without_rc_flagretain_args命名了参数却没设may_retain_or_releaseconsume_args_without_rc_flagconsume_args命名了参数却没设may_retain_or_releaseretain_result_without_rc_flagretain_result为 true 却没设may_retain_or_releaseshares_args_without_effectresult_shares_args非零却既无may_allocate也无may_retain_or_releaseretain_and_consume_same_arg同一参数既被保留又被消费两者抵消实为借用却两者都未命名alias_of_unconsumed_argresult_aliases_consumed_args命名了未被消费的参数runtime_check_of_unconsumed_arg运行时唯一性检查命中了未消费的参数原地路径会修改不拥有的值runtime_check_without_allocate有运行时唯一性检查却无may_allocate复制路径必然分配borrow_of_consumed_argresult_borrows_args命中了被消费参数出借方单元已消失borrow_and_share_same_arg同一参数既被命名为借用出借方又被命名为内部共享方borrow_without_result_retainresult_borrows_args命名了出借方却无retain_result借用被静默丢弃unique_result_without_sourceresult_unique为 true 但既不分配may_allocate也不从被消费参数取单元unique_result_that_is_retainedretain_result与result_unique同时为 trueviolation(effect)src/base/rc_effect_rules.zig返回一行打破的第一条规则assertRowConformssrc/base/rc_effect_rules.zig在 comptime 触发compileError报错消息命名操作与规则例如RcEffect row for low-level op xxx is invalid: 描述 [rule: 规则名]整表校验与掩码越界检查assertTableConforms()src/base/rc_effect_rules.zig在 comptime 遍历std.enums.values(LowLevel)的全部操作逐行调用assertRowConforms并额外拒绝“representation-only 操作数却带分配效应”的行它由 src/base/mod.zig 调用因此任何编译出的编译器都不可能携带自相矛盾的所有权表。掩码与参数数量arity的核对被刻意不放在这张表旁操作参数个数并未记录在表旁边再手写一张 arity 表只会增加一条未经检验的声称。取而代之的是maskExceedsArgCountsrc/base/rc_effect_rules.zig一个掩码位引用超过真实参数个数的位置时ARC 会把它当作“无此位置”行想描述的所有权被静默丢弃。它在两个接缝处被调用ARC 借用认证器对每个 debug 构建的 low-level 语句调用以及一致性观察器对解释器执行的每个操作调用。否定编译探针让“错误行是编译错误”本身被测试src/base/rc_effect_rejected_row_probe.zig 是一个故意不编译的文件它把#10023中删除的那行retainsSharingArgs(1)再置result_unique true重新引入并调用assertRowConforms(str_drop_prefix, ...)构建步骤run-test-rc-effect-row-rejected要求编译失败且报错携带规则标签。这样“一行错误行 编译错误”这一保证本身就被测试而不是被假设。结构层的单元测试rc_effect_rules.zig底部附带若干测试src/base/rc_effect_rules.zig“表中每一行结构有效”的全表扫描“#10023的行被拒绝”retainsSharingArgs(1)加result_unique true命中unique_result_without_source而干净的retainsSharingArgs(1)无违规“每条规则都能被某行触发且每条规则都可达”13 个精心构造的反例分别命中 13 条规则并用EnumSet断言所有规则都被覆盖掩码越界maskExceedsArgCount(consumesArgsRetainingArgs(0, 0b100), 2)报告位置 2arity 3 时无违规。第二层逐操作一致性测试rc_conformance.zig为什么结构校验不够结构校验无法发现#10023这类错误一行可以结构合法却与实际行为不符。只有运行内置操作才能分辨。因此第二层机制让解释器对执行的每条 low-level 语句报告它实际对可触达引用计数做了什么——哪些分配诞生、哪些计数被移动、结果中哪些分配来自参数——再拿这份报告与行比对src/eval/rc_conformance.zig。检查方向只报“行未解释的行为”一致性规则的方向是单向的全部读作“观察到的行为行未解释”绝不读作“声明了的行为这次没发生”一行是对操作可能做什么的声称单次执行只走一条路径写时复制操作拿到唯一输入时从不走复制路径因此“未行使的声称”不是发现而“未声称的效果”永远是发现——那是 ARC 没有为之规划的计数往来。因此两个效应必须在两种状态下都被驱动——唯一输入与共享输入——即result_unique与写时复制。逐操作扫描src/eval/test/rc_conformance_tests.zig正是负责同时提供这两种状态的。谁为计数负责规则区分“操作自己加的计数”与“ARC 加的计数”因为行决定了哪个是哪个retain_args意味着ARC 发出 retainsrc/lir/arc.zig的retainMaskedArgs所以内置操作不得再为参数计数——操作和 ARC 都计数会泄漏无retain_args的result_shares_args相反ARC 什么都不发内置操作必须自己为结果中存储的句柄计数——两边都不计数会留下悬垂句柄。观察机制与规则集观察仅在 debug 构建开启enabled builtin.mode .Debug and builtin.target.os.tag ! .freestandingsrc/eval/rc_conformance.zigrelease 构建不携带观察器、不分支、解释器语句循环中无钩子。观察器通过DebugRefcountTracker的引用计数事件日志工作复用单条 scratch 观察记录并用open_statements防止重入导致判断半填充的观察。一致性规则共 9 条src/eval/rc_conformance.zig规则触发条件allocated_without_may_allocate操作分配了行未设may_allocateadjusted_counts_without_may_retain_or_release操作改了计数行未设may_retain_or_releaseargument_counted_by_op_and_arc操作给retain_args已让 ARC 计数的参数又加了一次计数句柄高一格argument_counted_without_claim操作给既不在retain_args也不在result_shares_args中的参数计数argument_released_without_consume操作释放了行未在consume_args中命名的参数ARC 会再释放一次shared_argument_not_counted_by_op结果在result_shares_args下持有参数分配ARC 不计数操作自己也没计数result_outlives_uniqueness_claimresult_unique声称最外层计数为 1实际不是result_holds_unnamed_argument_allocation结果持有一个行未链接的参数分配mask_names_missing_argument掩码引用了操作未被给予的参数位置计数读取的安全性也做了处理wasFreedInWindowsrc/eval/rc_conformance.zig在操作后读取任何计数前先检查事件窗口内该 rc 地址是否被 free/realloc避免 use-after-free。静态数据REFCOUNT_STATIC_DATA常量计数不可观察被isObservable排除。覆盖率门禁新内置不能带着未验证的行发布coverageGapssrc/eval/rc_conformance.zig找出行非none()但扫描从未执行过、且不在豁免表的操作——这是未验证的行。豁免表Exemption要求为每个“解释型程序不可达”的操作注明理由staleExemptions还会检出“其实已被覆盖但仍列在豁免表中”的过期豁免。相关单元测试覆盖了这三种情形src/eval/rc_conformance.zig。关键测试重新引入 #10023 行会被抓住rc_conformance.zig中的一个测试src/eval/rc_conformance.zig直接构造#10023形状的观察操作str_drop_prefix参数外层分配计数 3字符串被别处持有结果外层是同一分配用当前行判断 → 0 发现共享内部、无诞生行正确解释用overrideRow把行改回#10023形态retainsSharingArgs(1)result_unique true→ 1 个发现规则为result_outlives_uniqueness_claim操作名str_drop_prefix。另有测试验证“操作在 ARC 的 retain 之上自己再加计数”命中argument_counted_by_op_and_arc、“释放行未消费的参数”命中argument_released_without_consume以及静态数据不参与观察。逐操作扫描两种唯一性状态都要驱动src/eval/test/rc_conformance_tests.zig 中的扫描用解释器运行 Roc 程序观察器全程在场每个被执行的操作都会按其行被评判程序结束后任何“行非 trivial 却没有任何 case 驱动过”的操作都会使测试失败。关键设计在文件头注释中写得很清楚唯一输入只使用一次的值是唯一的操作走原地路径错误的result_unique声称看上去反而成立共享输入同一个值同时被一个列表持有计数大于 1声称必须自己站住脚——这个状态才是抓#10023形状的地方。因此 case 会显式构造共享值shared Str.concat(...)后放入holder [shared, shared]再对同一字符串分别做 trim、lower/upper、reserve 等操作。字符串被刻意构造得足够长避免落入小字符串表示——静态数据计数恒定任何操作都无法移动它src/eval/test/rc_conformance_tests.zig。另有专门 case 覆盖“把空值拼接到共享值上”——这是写时复制操作可以不经复制、不经检查直接交还参数的路径src/eval/test/rc_conformance_tests.zig。Case结构还支持inline_wrappers与expected_live_allocations等控制项。项目成功标准与评估方式按文档以下标准全部满足才算完成comptime 校验器存在覆盖RcEffect每个字段至少一条蕴含规则且测试中添加的故意错误行编译失败并报出操作名与违反的规则重新引入精确的#10023行retainsSharingArgs上result_unique true外层为参数分配被抓住可由 comptime 层表达则在该层拒绝无论如何可由一致性测试证明失败一致性测试随zig build testdebug运行覆盖所有rcEffect()非none()的操作任何无覆盖的新操作都响亮失败——通过测试构建中添加 dummy 操作并观察失败来验证#9953回归Json.parse 输入引用计数平衡作为固定的端到端测试保留release 构建零成本校验器是 comptime测试仅存在于测试构建rcEffect()本身不被改动。评估的两个维度正确性理想错误的所有权行无法静默发布——结构性错误是编译错误非一致性错误是命名操作与不匹配增量的失败 debug 测试。表保持唯一事实来源项目增加的是执行机制而非第二个事实来源。性能理想运行时零开销纯测试/comptime 机制扫描通过解释器用小的值驱动操作保持总测试时间合理。测试计划与相关工程文档规划的测试清单包括结构性无效行的否定编译检查Zigerror测试或构建步骤探针与仓库其他 comptime 契约的测试方式一致逐操作一致性扫描本身固定回归#9953str 共享加上每个构造函数族至少一个 list 操作与一个 str 操作使每个辅助函数的语义都被行使。相关项目silent-drift-guards已落地文档移除是同一“对镜像事实施加执行机制”哲学的另一个实例本文档是其在所有权表上的落地。结语把“手写声称”变成“可验证契约”RcEffect表的集中化架构让所有权决策在所有后端保持一致但也把所有错误的后果放大为“所有后端一致地错”。仓库给出的答案是两层执行机制comptime 结构规则把自相矛盾的行变成编译错误src/base/rc_effect_rules.zigdebug-only 的逐操作一致性扫描把行为不符的行变成命名操作与增量的测试失败src/eval/rc_conformance.zig 与 src/eval/test/rc_conformance_tests.zig否定编译探针src/base/rc_effect_rejected_row_probe.zig则保证“错误行 编译错误”这一承诺本身被测试。#10023式的静默泄漏不再有容身之处而 release 构建依然零成本。【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考