RFC-029f: Роли целей компиляции и семантика поверхности импорта
Аннотация
Реализация расширенного слота «Плана дочерних RFC» RFC-029 (слоты 029b–029e заняты, далее следует 029f): определяет для исходных файлов модель ролей целей компиляции (пять категорий: Script / Bin / Lib / Test / Internal) и семантику их поверхностей импорта — при отсутствии манифеста роль выводится по достижимости из точки входа (нулевая конфигурация), при наличии манифеста объявляется явно через поля [lib] / [[bin]] / [exports] (поля, уже определённые в RFC-015). Заполняет четыре семантических пробела: по каким правилам считается поверхность межпакетного импорта, область применения исключения pub для предупреждений о мёртвом коде (решение B по #321), связь между [exports] и достижимостью в Registry, а также устранение терминологической неоднозначности между [binaries] в RFC-014b (предкомпилированные дистрибутивные артефакты) и файлами-источниками роли bin.
Мотивация
Основания и границы от ведущего документа
RFC-029 (принят) с решениями по выбору точки входа и видимости является непосредственным предшественником настоящего RFC:
Выбор файла входа, приоритет: 1.
[run].main2. полеpathпервого[[bin]]3.src/main.yx(соглашение по умолчанию). — RFC-029 §Межпакетные циклы: пояснения
Видимость: отсутствует. Область видимости + граница дистрибуции покрывают все сценарии, новое ключевое слово не требуется. — RFC-029 §Журнал проектных решений (2026-07-30)
Второй пункт — проектная красная линия настоящего RFC: семантика поверхности импорта не должна вводить новое ключевое слово видимости, её может нести только «граница дистрибуции» (роль файла + объявленная поверхность экспорта).
Каждый из четырёх потребителей упёрся в одну и ту же стену:
| Потребитель | Точка столкновения |
|---|---|
| #321 предупреждения о мёртвом коде | Решение B «pub = всегда интерфейс наружу, без предупреждений» принято из-за отсутствия модели ролей, различающей «настоящий наружный» и «похожий на наружный»; вопрос «предупреждать ли о неиспользуемом pub внутри bin» (вариант 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 для мёртвого кода не имеет области применения. Решение B по #321 «pub никогда не предупреждает» слишком консервативно в однофайловом скрипте (у скрипта нет внешних потребителей, pub бессмысленен) и слишком либерально в файле входа многопроектного проекта (никто не импортирует pub корневого файла — это мёртвый код). Без модели ролей линия раздела между вариантами A и B не проводится.
- Три концепции экспорта не согласованы.
[exports](015, отображение путей),[lib](015, путь к одному файлу), достижимость в Registry (029, от входа поuse) — соотносятся как включение, равенство или независимы — нигде не зафиксировано. - Столкновение терминов.
[binaries]в RFC-014b — это предкомпилированные дистрибутивные артефакты (стратегия приоритета загрузки .so/exe), что совершенно иной уровень, чем «цель bin (роль в исходниках)». После приземления 014c оба неизбежно окажутся на одном экране, и без скорейшего устранения неоднозначности недоразумения будут множиться.
Эмпирическое подтверждение: консервативность решения B — уже реальная боль. Запись ретроспективы M2 в #321: структурная причина молчания всей группы W1001–W1005 в конфигурации по умолчанию — именно «весь pub считается наружным интерфейсом» — это компенсирующее поведение из-за отсутствия модели, а не финальное состояние.
Предложение
Основная конструкция: модель ролей файлов
Роль — атрибут уровня файла (согласуется с гранулярностью полей в 015: [lib] — один файл, [[bin]] — список файлов, [exports] — отображение файлов), а не переключатель уровня пакета.
| Роль | Определение | Семантика |
|---|---|---|
| Bin | Файл, на который указывают [run].main / [[bin]].path; или (без манифеста) файл, содержащий main и не используемый (use) ни одним другим файлом | Точка входа программы. Требует main и эта функция должна быть функцией; иначе ошибка компиляции (E3020 — отсутствует main / E3021 — main не функция), причём на верхнем уровне не должно быть исполняемых операторов. Её pub может порождать предупреждения о мёртвом коде (нет внешних потребителей); main — корень достижимости |
| Lib | Файл, попавший в отображение [exports]; или [lib].path; или (без манифеста) файл, на который ссылается use другого файла | Граница дистрибуции. Её pub исключён из предупреждений о мёртвом коде (внешние потребители пакета невидимы, лучше недосообщить); экспортируемые через неё символы видны между пакетами |
| Test | Файл, удовлетворяющий правилам тестовых файлов RFC-036 (каталог tests/, *_test.yx и пр. существующие соглашения); явное объявление [[test]] не вводится | Тестовый код. Не участвует в анализе мёртвого кода; его ссылки считаются корнями достижимости для тестируемого кода |
| Internal | В пакете с манифестом — файл, не входящий в поверхность экспорта и не содержащий main | Внутренняя реализация пакета. pub исключён до второго этапа (ужесточение по достижимости в графе use внутри пакета, при предварительной верификации корпусом роли Bin); use извне пакета недостижим |
| Script | Файл, запускаемый напрямую как одиночный (run foo.yx) | Понятия точки входа нет. Операторы верхнего уровня (включая инициализацию связываний) выполняются в порядке написания и составляют тело программы; main — обычное связывание, не вызывается автоматически — чтобы запустить, напишите main(). В Registry присутствует только std |
Приоритет определения: явное объявление в манифесте > вывод по достижимости из входа > соглашения 036 для тестов. Явное объявление всегда побеждает — вывод лишь умолчание при отсутствии объявления.
Определение точки входа
Модель ролей используется не только для анализа мёртвого кода, но и управляет определением точки входа. Правила входа для ролей:
| Роль | Правило входа |
|---|---|
| Script (без манифеста) | Понятия точки входа нет. Операторы верхнего уровня (включая инициализацию связываний) выполняются в порядке написания и составляют тело программы; main — обычное связывание, не вызывается автоматически — чтобы запустить, напишите main() |
| Bin (с манифестом) | Требует main и эта функция должна быть функцией; иначе ошибка компиляции (E3020 — отсутствует main / E3021 — main не функция). На верхнем уровне не должно быть исполняемых операторов |
| Lib / Internal / Test | Не требуют main (они не являются точкой входа) |
Script и Bin должны быть взаимоисключающими по точке входа: «в Script main не особенный» и «в Bin main — это вход» не могут сосуществовать — если бы Script одновременно выполнял операторы верхнего уровня и неявно вызывал main, скрипт с явным main() запускался бы дважды (#356). Поэтому в Script лишь одна точка исполнения: операторы верхнего уровня.
Различие между определением роли и определением точки входа (замечание о реализации): определение точки входа использует «наличие манифеста» (find_project_root().is_some()), а не roles::classify(). Причина: classify отвечает на вопрос «кто потребляет этот файл» (для анализа мёртвого кода); файл с манифестом, но без 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→ поверхность импорта = этот единственный файл; - Если ничего нет (нет манифеста зависимости, голая зависимость по пути) → сохраняется текущее поведение: импортируемы все файлы верхнего уровня (совместимость с существующим поведением; ужесточение — отдельный вопрос).
Члены рабочего пространства: поверхность экспорта определяется только манифестом самого пакета-члена; корень рабочего пространства не может перекрывать или расширять поверхность экспорта членов — самодостаточность членов — ключевая конструкция 014c, и перекрытие на уровне корня разрушило бы инкапсуляцию.
Связь с достижимостью в Registry (согласовано с 029): [exports]/[lib] определяют множество допустимых начальных точек для межпакетного разрешения; после определения начальных точек Registry по-прежнему расширяется по правилу 029 «достижимое из входа по use» — внутренние файлы, которые use-ит экспортируемый файл, попадают при линковке, но не порождают новых межпакетных начальных точек.
Границы с существующими планами
| Сосед | Граница |
|---|---|
RFC-029d «CLI --entry перекрытие входа» (планируется) | 029f определяет модель ролей; 029d её потребляет — --entry является средством CLI-уровня для перекрытия роли Bin |
RFC-014b [binaries] | Устранение неоднозначности терминов: [binaries] — это предкомпилированные дистрибутивные артефакты (стратегия приоритета загрузки), [[bin]] — это объявление роли в исходниках. Первые присутствуют в манифесте зависимого пакета, вторые — в манифесте текущего пакета; не взаимозаменяемы |
| RFC-014c Рабочее пространство | Поверхность импорта при взаимном цитировании пакетов-членов = порядок из настоящего RFC; механизмы координации рабочего пространства (members, общий lockfile) остаются в 014c |
| RFC-036 Тесты | Определение роли Test напрямую использует существующие файловые правила 036, новых соглашений не вводится |
| #321 Мёртвый код | Решение B (pub всегда исключён) явно понижается до «переходной семантики при отсутствии target»; возможность предупреждения pub в роли Bin — форма реализации варианта A в настоящем RFC |
Детальная конструкция
Алгоритм определения роли (внутри оркестратора)
fn classify(files, manifest, entry_reach) -> Map<File, Role>:
# 1. Уровень явных объявлений: манифест имеет приоритет
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 используется (use) ≥ 1 другим файлом: 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: парсинг манифеста дополняется отображением[exports]→ таблица ролей (разбор полей уже есть в 015).frontend/module/orchestrator.rs: перед построением Registry вставляется фаза классификации ролей; межпакетное разрешение проверяет целиuseпо порядку поверхности импорта, при выходе за границу выдаётся существующее семействоmodule_not_found(новые коды ошибок не вводятся, в сообщение добавляется подсказка «не входит в поверхность экспорта»).typecheck/passes/dead_code.rs: множество корней входа меняется с «main + весь pub» на «main + pub файлов роли Lib» (Bin — немедленно с предупреждением; Internal — исключение до второго этапа, Phase 2 см. в стратегии реализации).- LSP (RFC-017): автодополнение/наведение фильтруют «импортируемые элементы» по поверхности импорта; диагностика отсутствия
mainвыдаётся только для файлов роли Bin.
Поведение во время выполнения
Без изменений. Модель ролей — это чисто семантика времени компиляции и статического анализа, IR и исполнение не затрагиваются.
Обратная совместимость
- Проекты без манифеста: поведение полностью не меняется (уровень вывода воспроизводит текущее состояние — используемый — Lib, содержащий main — Bin). Приращение предупреждений о мёртвом коде только в Bin-файлах: неиспользуемый pub корневого файла, который никто не импортирует, из молчания переходит в W1001 — это и есть ожидаемое поведение варианта A из #321, а не поломка.
- Проекты с манифестом: если
[exports]объявлен, поверхность импорта сужается с «все pub-файлы» до объявленной. Это единственная точка сужения поведения: код, зависящий от внутренних файлов чужих пакетов, начнёт получатьmodule_not_found. Учитывая, что экосистема пакетов ещё не сложилась (029: «в настоящее время экосистемы сторонних пакетов нет»), площадь поломки равна нулю; всё же закладывается переходный период в дополнительной версии (сначала предупреждение, затем ошибка).
Компромиссы
Достоинства
- Нулевая новая конфигурационная поверхность: все поля уже определены в 015, настоящий RFC лишь дополняет семантику.
- Нулевое новое ключевое слово: поверхность импорта несёт граница дистрибуции — изоморфно решению 029 «видимости не существует».
- Четыре потребителя (#321 / 014c / 029a / 037) моделируются разом, никто больше не упирается в свою стену.
- Путь без манифеста — нулевая стоимость: ДНК скриптового языка с нулевой конфигурацией сохранена полностью.
Недостатки
- Вывод роли (используемый — Lib) в распространённой форме «файл входа use-ит утилитарный файл» приписывает утилитарному файлу роль Lib, и его pub исключается — грубее идеальной гранулярности. Это осознанный выбор в сторону «лучше недосообщить», ужесточение требует анализа направления вызовов в графе use и в первой версии не выполняется.
- Сужение поверхности импорта через
[exports]— это семантическое ужесточение с эффектом поломки (несмотря на переходный период).
Альтернативные варианты
| Вариант | Описание | Причина отклонения |
|---|---|---|
| Открыть на верхнем уровне RFC-040 | Новый номер верхнего уровня | Семантика модульного графа (поверхность импорта / достижимость / вход) целиком лежит во владении 029; выделение породит две верхнеуровневые записи об одном предмете; к тому же шаблон слотов 029b–029e уже установлен |
| Вложить в 014c | Как раздел рабочего пространства | Направление зависимости обратное: языковой уровень (029x) определяет семантику, уровень пакетов (014x) её потребляет. 014c ещё на рассмотрении, расширение в процессе замедлит его приземление |
| Вариант A в исходном виде (меню #321) | манифест форсирует target [[bin]]/[[lib]] для получения семантики мёртвого кода | Ради узкого среза (неиспользуемый pub корневого файла с main) вводится обязательная конфигурация; уровень вывода в настоящем RFC даёт тот же выигрыш при нулевой конфигурации |
| Только вывод по входу (без уровня манифеста) | Не потребляются поля 015 | [exports] — уже принятое поле 015, и межпакетная поверхность импорта не обходится без него; отсутствие определения означает пробел для 014c |
Стратегия реализации
Поэтапность
- Phase 1: классификация ролей + разрешение поверхности импорта + возможность предупреждения pub в Bin (Internal/Test сохраняют исключение)
- Phase 2: ужесточение pub в Internal до «предупреждать, если недостижимо в графе use внутри пакета» — предпосылкой служит верификация на корпусе Bin-роли после приземления Phase 1, подтверждающая отсутствие ложных срабатываний; выполняется по факту готовности, без расписания
Зависимости
- Предпосылки: оркестратор RFC-029 (приземлён), поля манифеста RFC-015 (определены).
- Зависимые: #321 (предупреждение pub в Bin), RFC-014c (поверхность импорта), RFC-029a (роль как граница единиц кэша), RFC-037 (выбор артефактов), RFC-029d (перекрытие через
--entry). - Порядок приземления с RFC-014c: до завершения рассмотрения 014c порядок разрешения импорта из 029f должен быть зафиксирован, 014c потребляет его по ссылке, чтобы избежать расширения области рассмотрения в процессе.
Риски
- Расхождение между выводом роли и интуицией пользователя (первый пункт компромиссов) → всё приращение поведения выдаётся в форме предупреждений (без блокировки), в любой момент можно отключить.
- Управление переходным периодом сужения
[exports]→ в дополнительной версии сначала предупреждение.
Открытые вопросы
Все три вопроса этапа проекта решены (2026-09-13, решения внесены в Приложение B и в основной текст), нерешённых нет:
- [x] Ужесточать ли pub в Internal → Решение: на втором этапе ужесточение по достижимости в графе use внутри пакета, при предварительной верификации корпусом Bin-роли (см. Phase 2 стратегии реализации)
- [x] Вводить ли явное объявление
[[test]]→ Решение: не вводится, роль Test определяется только правилами 036; при появлении у 036 потребности в явном объявлении оно будет добавлено в самом 036 - [x] Допустимо ли перекрытие поверхности экспорта членов со стороны корня рабочего пространства → Решение: не допускается, самодостаточность членов — ключевая конструкция 014c, и перекрытие на уровне корня разрушило бы инкапсуляцию
Приложение B: Журнал проектных решений
| Решение | Содержание | Дата | Записал | Основание |
|---|---|---|---|---|
| Номер | 029f (029b зарезервирован под hot reload, 029c удалён, 029d/029e заняты) | 2026-09-12 | Чэньсюй | RFC-029 §План дочерних RFC |
| Отнесение к семейству 029, а не к верхнему RFC | Да | 2026-09-12 | Чэньсюй | Поверхность импорта / достижимость / вход относятся к семантике модульного графа; 029 уже потребляет приоритет входа из [[bin]] |
| Ключевое слово видимости | Не вводится | 2026-09-12 | Чэньсюй | Изоморфно решению RFC-029 «видимости не существует», поверхность импорта несёт граница дистрибуции |
| Статус решения B по #321 | Понижается до «переходной семантики при отсутствии target» | 2026-09-12 | Чэньсюй | Возможность предупреждения pub в роли Bin — форма реализации варианта A из #321 |
| Гранулярность роли | Уровень файла | 2026-09-12 | Чэньсюй | Гранулярность полей 015 ([lib] — один файл, [[bin]] — список, [exports] — отображение) |
Устранение неоднозначности [binaries] | предкомпилированные дистрибутивные артефакты из 014b ≠ роль источников [[bin]] | 2026-09-12 | Чэньсюй | Два уровня понятий (стратегия загрузки vs роль компиляции), фиксируется до приземления 014c |
| Ужесточение pub в Internal | На втором этапе по достижимости в графе use внутри пакета, при предварительной верификации корпусом Bin-роли | 2026-09-13 | Чэньсюй | Лучше недосообщить, постепенное ужесточение, корпусо-управляемый подход избегает преждевременных ложных срабатываний |
Явное объявление [[test]] | Не вводится; Test определяется только правилами 036 | 2026-09-13 | Чэньсюй | До появления потребности у 036 конфигурационная поверхность не добавляется |
| Перекрытие корнем рабочего пространства поверхности экспорта членов | Не допускается | 2026-09-13 | Чэньсюй | Самодостаточность членов — ключевая конструкция 014c, перекрытие на уровне корня разрушило бы инкапсуляцию |
Автовызов main в Script | Не автовызов — операторы верхнего уровня и есть программа, 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 | Обходная форма для одиночного файла без манифеста, запускаемого напрямую; всё поведение совпадает с текущим |
Ссылки
- RFC-029 Семантика модулей (родительский RFC: выбор входа, решения по видимости, план дочерних RFC)
- RFC-015 Система конфигурации (определения полей
[lib]/[[bin]]/[exports]) - RFC-014b Система сборки и дистрибуция бинарников (
[binaries], объект устранения неоднозначности терминов) - RFC-014c Поддержка рабочих пространств (первый потребитель поверхности импорта, на рассмотрении)
- RFC-036 Тестовый фреймворк (источник правил для роли Test)
- #321 M2 Отдельный канал эмиссии кодов предупреждений (источник решения B и варианта A)
