RFC-029f: 编译目标角色与导入面语义
摘要
兑现 RFC-029「子 RFC 规划」的延伸槽位(029b–029e 已占,顺延 029f):为源文件定义 编译目标角色模型(Script / Bin / Lib / Test / Internal 五类)及其导入面语义—— 无 manifest 时按入口可达性推断(零配置),有 manifest 时按 [lib] / [[bin]] / [exports](RFC-015 已定义的字段)显式声明。填补四项悬空语义:跨包导入面按什么算、 死代码警告的 pub 豁免适用域(#321 定案 B)、[exports] 与 Registry 可达性的关系、 以及 RFC-014b [binaries](预编译分发产物)与 bin 角色源文件的名词消歧。
动机
宿主与边界依据
RFC-029(已接受)的入口选择与可见性决策是本 RFC 的直接上游:
入口文件选择优先级:1.
[run].main2.[[bin]]第一项的path3.src/main.yx(约定默认)。—— RFC-029 §包间循环:再说
可见性:不存在。作用域 + 分发边界覆盖所有场景,不需要新关键字。 —— RFC-029 §设计决策记录(2026-07-30)
第二条是本 RFC 的设计红线:导入面语义不得引入新的可见性关键字, 只能由"分发边界"(文件角色 + 声明导出面)承担。
四个消费方各自撞到了同一堵墙:
| 消费方 | 撞墙点 |
|---|---|
| #321 死代码警告 | 定案 B「pub = 对外接口永不报」是因为没有角色模型区分"真对外"与"看着像对外";bin 内未用 pub 报不报(方案 A)悬置至今 |
| RFC-014c 工作空间(审核中,#113) | 成员包互相 use 时导入面按什么算——[exports]、[lib] 单文件、还是全部 pub 文件——全文无一条语义条文 |
| RFC-015 配置系统(已接受) | [lib] / [[bin]] / [exports] 字段已定义,除 RFC-029 消费了入口优先级外,其余字段的语义(尤其 [exports] 与 Registry 可达性的关系)无人对齐 |
| RFC-037 打包 / RFC-029a 缓存(草案,#293) | 打包产物选择、增量重编译的缓存单元边界,都需要角色模型作前置 |
当前的问题
四项语义空白(2026-09-12 盘点,翻 014/014a/014b/014c/015/029 全文确认):
- 跨包导入面无定义。014c 的成员包互引示例只有目录结构(
src/lib.yx),use utils.helper能拿到什么没有任何条文。当前隐式规则是"该包所有顶层 pub 文件"——内部实现文件也被拖进命名空间,依赖边界形同虚设。 - 死代码 pub 豁免没有适用域。#321 定案 B 的"pub 永不报"在单文件脚本里 过保守(脚本没有外部消费者,pub 无意义),在多文件项目的入口文件里又 过宽松(无人 import 根文件的 pub 就是死代码)。没有角色模型,A/B 两案的 切分线画不出来。
- 三个导出概念无人对齐。
[exports](015,路径映射)、[lib](015, 单文件路径)、Registry 可达性(029,从入口沿 use 可达)——三者是包含 关系、相等关系还是各管各的,无条文。 - 名词撞车。RFC-014b 的
[binaries]是预编译分发产物(.so/exe 下载 优先策略),与"bin 目标(源码角色)"完全两个层面。014c 落地后两者必然 同屏出现,不趁早消歧会持续产生误解。
实证:B 定案的保守性已经是实际痛点。#321 M2 复盘记录:默认配置下 W1001–W1005 全族静默的结构性原因正是"所有 pub 都被当对外接口"——这是 模型缺失的补偿行为,不是终态。
提案
核心设计:文件角色模型
角色是文件级属性(与 015 的字段粒度一致:[lib] 是单文件、[[bin]] 是文件列表、[exports] 是文件映射),不是包级开关。
| 角色 | 判定 | 语义 |
|---|---|---|
| Bin | [run].main / [[bin]].path 指向的文件;或(无 manifest 时)含 main 且未被任何文件 use 的文件 | 程序入口。要求 main 且必须是函数;否则编译错误(E3020 缺 main / E3021 main 非函数),且顶层不得有可执行语句。其 pub 可报死代码(无包外消费者);main 为可达性根 |
| Lib | [exports] 映射命中的文件;或 [lib].path;或(无 manifest 时)被其他文件的 use 引用到的文件 | 分发边界。其 pub 豁免死代码(包外消费者不可见,宁漏报);经它导出的符号跨包可见 |
| Test | RFC-036 测试文件规则命中的文件(tests/ 目录、*_test.yx 等既有约定);不设 [[test]] 显式声明 | 测试代码。不参与死代码判定;其引用计入被测代码的可达性根 |
| Internal | 包内有 manifest 时,既不在导出面也不含 main 的文件 | 包内实现。pub 豁免至二阶段(按包内 use 图可达性收紧,以 Bin 角色语料验证为前置);包外 use 不可达 |
| Script | 单文件直跑(run foo.yx)的文件 | 无入口概念。顶层语句(含绑定初始化)按书写顺序即程序主体;main 是普通绑定,不自动调用——想跑写 main()。Registry 只有 std |
判定优先级:manifest 显式声明 > 入口可达性推断 > 036 测试约定。 显式声明永远赢——推断只是无声明时的默认。
入口判定
角色模型不仅用于死代码分析,也驱动入口判定。各角色的入口规则:
| 角色 | 入口规则 |
|---|---|
| Script(无 manifest) | 无入口概念。顶层语句(含绑定初始化)按书写顺序即程序主体;main 是普通绑定,不自动调用——想跑写 main() |
| Bin(有 manifest) | 要求 main 且必须是函数;否则编译错误(E3020 缺 main / E3021 main 非函数)。顶层不得有可执行语句 |
| Lib / Internal / Test | 不要求 main(它们不是入口) |
Script 与 Bin 的入口必须互斥:「Script 下 main 不特殊」与「Bin 下 main 是入口」 不能同时成立——若 Script 既执行顶层语句又隐式调 main,显式写了 main() 的脚本会双跑 (#356)。故 Script 只有一个执行入口:顶层语句。
角色判定与入口判定的区别(实现注记):入口判定用「有无 manifest」 (find_project_root().is_some())而非 roles::classify()。原因:classify 回答「这个文件被谁消费」(服务于死代码分析);有 manifest 却没 main 的文件 在 classify 里是 Internal(它不被别的文件 use,合理),但用户把它当入口跑了, 此时必须有入口。两个问题不同,判据自也不同。
详见 docs/src/reference/language-spec/syntax.md §3.11。
示例
# yaoxiang.toml(字段全部是 RFC-015 已定义的,本 RFC 不新增配置面)
[lib]
path = "src/lib.yx"
[[bin]]
name = "my-cli"
path = "src/cli.yx"
[exports]
"." = "src/lib.yx"
"./internal-helper" = "src/helper.yx" # ← 决定跨包可见性的是它,不是 pub# src/cli.yx(Bin 角色)
pub unused_fn = (x: Int) => x # ← W1001 可报:bin 无包外消费者
main: () -> Void = { ... }
# src/lib.yx(Lib 角色,在导出面上)
pub api_fn = ... # ← 永不报:包外消费者不可见
# src/other.yx(Internal 角色,不在导出面)
pub semi_api = ... # ← 豁免至二阶段(宁漏报),届时按 use 图收紧导入面语义
跨包 use pkg.x 的解析序(第一条命中即生效):
[exports]映射存在 → 导入面 = 映射列出的文件集合,x须在这些文件的 顶层绑定中;- 否则
[lib].path存在 → 导入面 = 该单文件; - 都没有(无 manifest 依赖、路径依赖裸目录)→ 维持现状:全部顶层文件 可导入(兼容既有行为,收紧另议)。
工作空间成员:导出面只由成员包自己的 manifest 定义,workspace 根不得 覆盖或扩展成员导出面——成员自包含是 014c 的核心设计,根覆盖会破坏封装。
与 Registry 可达性的关系(对齐 029):[exports]/[lib] 定义的是跨包 解析的合法起点集合;起点确定后,Registry 仍按 029 的"从入口沿 use 可达"扩展——导出文件自身 use 进来的内部文件随链接进入,但不产生新的 跨包起点。
与既有规划的划界
| 邻居 | 边界 |
|---|---|
RFC-029d「CLI --entry 覆盖入口」(规划中) | 029f 定义角色模型;029d 消费它——--entry 是 Bin 角色的 CLI 级覆盖手段 |
RFC-014b [binaries] | 名词消歧:[binaries] 是预编译分发产物(下载优先策略),[[bin]] 是源码角色声明。前者存在于依赖包的 manifest,后者存在于本包 manifest,不互换 |
| RFC-014c 工作空间 | 成员包互引的导入面 = 本 RFC 解析序;workspace 协调机制(members、共享 lockfile)仍归 014c |
| RFC-036 测试 | Test 角色判定直接采用 036 的既有文件规则,不新增约定 |
| #321 死代码 | 定案 B(pub 一律豁免)显式降级为"无 target 时的过渡语义";Bin 角色的 pub 可报是本 RFC 兑现方案 A 的形态 |
详细设计
角色判定算法(编排器内)
fn classify(files, manifest, entry_reach) -> Map<File, Role>:
# 1. 显式层:manifest 声明优先
for f in manifest.exports.values(): role[f] = Lib
if manifest.lib_path: role[lib_path] = Lib
for b in manifest.binaries: role[b.path] = Bin
if manifest.run_main: role[run_main] = Bin
# 2. 推断层:无声明处按入口可达性
for f in files where role[f] 未定:
if f 被 ≥1 个非自身文件 use: role[f] = Lib
elif f 含 main 且无其他文件 use 它: role[f] = Bin
else: role[f] = Internal
# 3. 测试层:036 规则命中 → Test(覆盖 Lib/Internal,不覆盖显式 Bin)
apply_rfc036_test_rules(files, &role)
# 单文件直跑:整个模型旁路,行为不变
if no manifest and single_file: all Script编译器改动
frontend/config.rs:manifest 解析补[exports]→ 角色表的映射 (字段解析 015 已有)。frontend/module/orchestrator.rs:Registry 构建前插入角色分类阶段; 跨包解析按导入面序校验use目标,越界报既有module_not_found族 (不新增错误码,消息补"不在导出面"提示)。typecheck/passes/dead_code.rs:入口点集合从"main + 全部 pub"改为 "main + Lib 角色文件的 pub"(Bin 立即可报;Internal 豁免至二阶段, Phase 2 见实现策略)。- LSP(RFC-017):补全/悬停的"可导入项"按导入面过滤;
main缺失诊断 仅对 Bin 角色文件报。
运行时行为
无变化。角色模型是纯编译期/解析期语义,不影响 IR 与执行。
向后兼容性
- 无 manifest 项目:行为完全不变(推断层还原现状——被 use 的是 Lib、 含 main 的是 Bin)。死代码警告的增量仅在 Bin 文件:无人 import 根文件的 unused pub 从静默变为 W1001——这是 #321 方案 A 的预期行为,非破坏。
- 有 manifest 项目:
[exports]已声明的,导入面从"全部 pub 文件"收窄为 声明面。这是唯一的行为收窄点:依赖他人包内部文件的代码会开始报module_not_found。鉴于包生态尚未建立(029:「当前无第三方包生态」), 破坏面为零;仍设一个次版本过渡期(越界先警告后报错)。
权衡
优点
- 零新配置面:字段全是 015 已定义的,本 RFC 只补语义。
- 零新关键字:导入面由分发边界承担,与 029「可见性不存在」决策同构。
- 四个消费方(#321 / 014c / 029a / 037)一次建模,不再各撞各的墙。
- 无 manifest 路径零成本:脚本语言的零配置基因完整保留。
缺点
- 角色推断(被 use 即 Lib)在"入口文件 use 工具文件"的常见形态下会把 工具文件判成 Lib,其 pub 豁免——比理想粒度宽。这是宁漏报方向的有意 选择,收紧需要 use 图上的调用方向分析,第一版不做。
[exports]收窄导入面是有破坏性的语义收紧(虽有过渡期)。
替代方案
| 方案 | 描述 | 未采纳原因 |
|---|---|---|
| 顶层 RFC-040 | 新开顶层编号 | 模块图语义(导入面/可达性/入口)全在 029 领地,拆顶层会造成同一对象两份顶层文档;且 029b–029e 槽位模式已确立 |
| 挂入 014c | 作为 workspace 的一节 | 依赖方向反了:语言层(029x)定义语义、包层(014x)消费语义。014c 还在审核中,中途扩范围拖累其落地 |
| 方案 A 原样(#321 菜单) | manifest 强制 [[bin]]/[[lib]] target 才有死代码语义 | 为窄切片(带 main 根文件的 unused pub)引入强制配置;本 RFC 的推断层以零配置获得同等收益 |
| 纯入口推断(无 manifest 层) | 不消费 015 字段 | [exports] 已是 015 已接受字段,跨包导入面绕不开它;不定义等于留空白给 014c |
实现策略
分阶段
- Phase 1:角色分类 + 导入面解析 + Bin pub 可报(Internal/Test 维持豁免)
- Phase 2:Internal pub 收紧为"包内 use 图不可达即可报"——前置条件为 Phase 1 落地后 Bin 角色警告语料验证无误报回调;触发即做,不设时间表
依赖关系
- 前置:RFC-029 编排器(已落地)、RFC-015 manifest 字段(已定义)。
- 被依赖:#321(Bin pub 可报)、RFC-014c(导入面)、RFC-029a(角色作为 缓存单元边界)、RFC-037(产物选择)、RFC-029d(
--entry覆盖)。 - 与 RFC-014c 的落地顺序:014c 审核通过前,029f 的导入面序应先定稿, 014c 以引用方式消费,避免审核中途扩范围。
风险
- 角色推断与用户直觉的偏差(上述权衡第一条)→ 以警告(不阻断)形式 呈现全部增量行为,可随时 disable。
[exports]收窄的过渡期管理 → 次版本内先警告。
开放问题
草案阶段三项均已定案(2026-09-13,决议入附录B 与正文),无未决项:
- [x] Internal pub 是否收紧 → 定案:二阶段按包内 use 图可达性收紧, 以 Bin 角色语料验证为前置(见实现策略 Phase 2)
- [x]
[[test]]显式声明 → 定案:不引入,Test 角色仅由 036 规则 判定;036 将来有显式声明需求时由其自身扩展 - [x] workspace 根覆盖成员导出面 → 定案:不允许,成员自包含是 014c 核心设计,根覆盖破坏封装
附录B:设计决策记录
| 决策 | 决定 | 日期 | 记录人 | 依据 |
|---|---|---|---|---|
| 编号 | 029f(029b 已预留热重载、029c 已删除、029d/029e 已占位) | 2026-09-12 | 晨煦 | RFC-029 §子 RFC 规划 |
| 归属 029 家族而非顶层 RFC | 是 | 2026-09-12 | 晨煦 | 导入面/可达性/入口同属模块图语义;029 已消费 [[bin]] 入口优先级 |
| 可见性关键字 | 不引入 | 2026-09-12 | 晨煦 | 与 RFC-029「可见性不存在」决策同构,导入面由分发边界承担 |
| #321 定案 B 的地位 | 降级为"无 target 时的过渡语义" | 2026-09-12 | 晨煦 | Bin 角色 pub 可报即 #321 方案 A 的兑现形态 |
| 角色粒度 | 文件级 | 2026-09-12 | 晨煦 | 015 字段粒度([lib] 单文件、[[bin]] 列表、[exports] 映射) |
[binaries] 消歧 | 014b 预编译分发产物 ≠ [[bin]] 源码角色 | 2026-09-12 | 晨煦 | 两层概念(下载策略 vs 编译角色),014c 落地前钉死 |
| Internal pub 收紧 | 二阶段按包内 use 图可达性收紧,前置 = Bin 角色语料验证 | 2026-09-13 | 晨煦 | 宁漏报渐进收紧,语料驱动避免过早误报 |
[[test]] 显式声明 | 不引入;Test 仅由 036 规则判定 | 2026-09-13 | 晨煦 | 036 未有需求前不加配置面 |
| workspace 根覆盖成员导出面 | 不允许 | 2026-09-13 | 晨煦 | 成员自包含是 014c 核心设计,根覆盖破坏封装 |
Script 下 main 是否自动调用 | 不自动——顶层语句即程序,main() 需显式 | 2026-09-17 | 晨煦 | 两规则共存会让显式 main() 双跑(#356);Script 只能有一个执行入口 |
| 角色模型是否用于入口判定 | 是(此前仅用于死代码警告) | 2026-09-17 | 晨煦 | 本 RFC 定义 Bin 的「main 为可达性根」,不接入口则该语义空转 |
附录C:术语表
| 术语 | 定义 |
|---|---|
| 角色(Role) | 源文件的编译目标身份:Script / Bin / Lib / Test / Internal |
| 导入面(Import Surface) | 跨包 use 时合法解析起点的文件集合,由 [exports]/[lib] 声明或推断 |
| 分发边界 | 029 术语:包的对外暴露范围;本 RFC 将其从隐式(全部 pub 文件)精确化为导入面 |
| Script 态 | 无 manifest 单文件直跑的旁路形态,一切行为与现状一致 |
参考文献
- RFC-029 模块语义(父 RFC:入口选择、可见性决策、子 RFC 规划)
- RFC-015 配置系统(
[lib]/[[bin]]/[exports]字段定义) - RFC-014b 构建系统与二进制分发(
[binaries],名词消歧对象) - RFC-014c 工作空间支持(导入面的首个消费方,审核中)
- RFC-036 测试框架(Test 角色规则来源)
- #321 M2 警告码独立发射通道(定案 B 与方案 A 的出处)
