Skip to content

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].main 2. [[bin]] 第一项的 path 3. 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 全文确认):

  1. 跨包导入面无定义。014c 的成员包互引示例只有目录结构(src/lib.yx), use utils.helper 能拿到什么没有任何条文。当前隐式规则是"该包所有顶层 pub 文件"——内部实现文件也被拖进命名空间,依赖边界形同虚设。
  2. 死代码 pub 豁免没有适用域。#321 定案 B 的"pub 永不报"在单文件脚本里 过保守(脚本没有外部消费者,pub 无意义),在多文件项目的入口文件里又 过宽松(无人 import 根文件的 pub 就是死代码)。没有角色模型,A/B 两案的 切分线画不出来。
  3. 三个导出概念无人对齐。[exports](015,路径映射)、[lib](015, 单文件路径)、Registry 可达性(029,从入口沿 use 可达)——三者是包含 关系、相等关系还是各管各的,无条文。
  4. 名词撞车。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 豁免死代码(包外消费者不可见,宁漏报);经它导出的符号跨包可见
TestRFC-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。

示例 ​

toml
# 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
yaoxiang
# 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 的解析序(第一条命中即生效):

  1. [exports] 映射存在 → 导入面 = 映射列出的文件集合,x 须在这些文件的 顶层绑定中;
  2. 否则 [lib].path 存在 → 导入面 = 该单文件;
  3. 都没有(无 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 的出处)