Carbon 语言Main包简化声明提案解析:无package头的可执行程序与统一导入语法
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
导读
proposals/p002550-simplified-package-declaration-for-the-main-package.md是 Carbon 语言关于"主包声明简化"的正式设计提案(对应 GitHub PR #2550),它重新定义了可执行程序中package与import声明的书写规则:主包(Main包)内的文件不再需要(部分场景不允许)写package Main ...;,同一包内的导入也改为import library "..."的无包名形式。阅读本文,你将掌握 Carbon 当前顶层声明的完整语法矩阵、Main.Run入口点的判定规则,以及该提案在解析器(parser)与语义检查(check)阶段的落地证据,可直接对照仓库示例代码编写可运行的最小 Carbon 程序。
提案背景:为什么必须简化
旧语法的负担
提案指出,在旧语法下(源自提案 #107 Code and name organization),每个 Carbon 源文件都必须以package声明开头:
package Main impl;对于刚接触 Carbon 的初学者来说,"Hello world" 程序也要先写一行样板声明;在幻灯片、教材示例等教学场景中,作者要么被迫加入样板代码,要么给出会被编译器拒绝的不完整示例。
同包导入同样必须重复包名:
package Geometry library "Shapes" api; import Geometry library "Points";这不仅增加了常见操作的书写负担,还掩盖了一个语义差异:同包导入与跨包导入的行为并不相同——同包导入把目标库的公共名字直接引入文件作用域,而跨包导入只引入包名本身(详见 issue #1136 的讨论)。此外,由于每个包都可以由开发者任意取名,语言本身无法确定哪个包包含程序入口点。
决策链条
提案的 Background 部分记录了三条关键决策来源:
- 提案 #107 引入了当时的
package语法; - issue #1869 决定程序入口点命名为
Main.Run; - issue #1136 决定包名不应注入其文件作用域,且同包
import不得提及包名。
本提案正是把 #1136 与 #1869 的结论正式落实到语言设计中(#1136 中关于private名称访问、非限定名查找的其他决策不属于本提案范围)。
核心设计:Main包与入口点Run
基本概念
Main包:包含程序入口点的包。- 入口点函数
Run:Carbon 程序的入口是名为Run的函数,位于Main包中。 - 跨语言链接:Carbon 库也可以链接进其他语言(特别是 C++)编写的程序,此时没有 Carbon 的
Run函数,由 C++ 提供main函数作为入口。这是对 C++ 互操作目标的明确承认。
入口点的实际判定逻辑
仓库中的语义检查代码实现了这一设计。在 toolchain/sem_ir/entry_point.cpp 中,IsEntryPoint判定一个函数是否为入口点需要同时满足:
static constexpr llvm::StringLiteral EntryPointFunction = "Run"; auto IsEntryPoint(const File& file, FunctionId function_id) -> bool { const auto& function = file.functions().Get(function_id); return function.parent_scope_id == NameScopeId::Package && file.package_id() == PackageNameId::None && function.name_id.has_value() && file.names().GetAsStringIfIdentifier(function.name_id) == EntryPointFunction; }即:函数位于包作用域(Package作用域)、当前文件所属包为未命名包(PackageNameId::None,即Main包)、且函数名为Run。这与提案"入口点为Main.Run"的描述一一对应。
在语义检查阶段 toolchain/check/handle_function.cpp 中,ValidateForEntryPoint还会对入口点做签名校验:
invalid parameters for `Main.Run` function; expected `()` or `(argc: i32, argv: Core.Optional(char*)*)` invalid return type for `Main.Run` function; expected `fn (...)` or `fn (...) -> i32`也就是说,Main.Run目前允许无参数/标准 C 风格参数(argc、argv)形式,返回类型为i32或 void(文件注释表明更详细的签名规则仍在 issue #6735 中跟进)。
新的package声明语法
提案将package声明从"必填"变为按场景区分:
| 场景 | 语法形式 |
|---|---|
非Main包中的文件 | packageFoo[library "Bar"] (api|impl);(沿用 #107 语法不变) |
Main包中属于某个命名库的文件 | library "Bar"(api|impl); |
Main包中不属于任何命名库的impl文件 | 完全省略package声明 |
要点与限制:
Main包没有package Main api;这种写法:即不存在"不属于命名库的api文件"这一概念。- 显式书写
Main这个名字是错误:在package声明或import声明中明确写出Main都会被拒绝。 - 因此,任何其他包都无法导入
Main包的库——Main包天然是程序的终点。
仓库中的语法落地证据
解析器在 toolchain/parse/handle_import_and_package.cpp 中对package/import/library三类声明统一处理:
// Parse the package name. if (DeclKind == NodeKind::LibraryDecl || (DeclKind == NodeKind::ImportDecl && context.PositionIs(Lex::TokenKind::Library))) { // This is either `library ...` or `import library ...`, so no package name // is expected. } else { // We require a package name. This is either an identifier or package name // keyword. ... }即library "..."声明与import library "..."导入不允许出现包名,而普通package/import必须有包名,与提案的语法矩阵严格一致。
同时解析器要求package/library声明必须是文件的第一个非注释行(PackageTooLate诊断:"the{0}declaration must be the first non-comment line"),参见 toolchain/parse/handle_import_and_package.cpp;import声明必须在package之后、其他实体之前(ImportTooLate诊断),参见 toolchain/parse/handle_import_and_package.cpp。
新的import声明语法
三种导入形式
| 语法 | 含义 | 限制 |
|---|---|---|
importFoo; | 导入包Foo的默认库 | 引入名字Foo表示该包;Foo不能是当前包名 |
importFoolibrary "Bar"; | 导入包Foo的库Bar | 引入名字Foo;Foo不能是当前包名 |
import library "Bar"; | 导入当前包的库Bar | 把该库的公共名字直接引入文件作用域;Bar不能是当前库名 |
import library default; | 导入当前包的默认库 | 不允许在当前默认库内部使用 |
这种设计带来一个重要的可读性收益:读者一眼就能区分"我们的库"(import library "...",无包名)与"他们的库"(import Foo ...,有包名)。
解析器对default的支持
在 toolchain/parse/context.cpp 中,ParseLibraryName与ParseLibrarySpecifier处理库名与库说明符,支持default关键字或字符串字面量两种形式(expected \default` or a string literal to specify the library name),并生成LibrarySpecifier/LibraryName/DefaultLibrary节点。accept_default参数只在未给出包名(即同包导入)时允许default,与提案中"default` 仅用于当前包默认库"的约束吻合。
仓库测试用例佐证
- toolchain/check/testdata/packages/explicit_imports.carbon 使用了
import library default;; - toolchain/check/testdata/packages/fail_import_default.carbon 验证了对非法使用
import library default;场景的诊断输出; - 解析器测试 toolchain/parse/testdata/packages/export.carbon 展示了
library "..."(无包名)与package Pkg;两种声明的共存。
无package声明的源文件
提案明确允许:一个 Carbon 程序可以包含没有package声明、也没有Run函数的源文件,甚至可以有多个这样的文件。这类文件因为没有对应的api文件,不能实现任何可导入的功能,但可以为尚未设计完成的 Carbon 特性预留位置:
- 全局注册(global registration)机制;
- 定义具有已知符号(well-known symbols)的函数,类似 C++ 的
extern "C"声明。
仓库中的真实应用示例
提案落地后的语法已经在仓库示例中大量使用,是最直接的实战参考:
最小完整程序examples/hello_world.carbon:
import Core library "io"; fn Run() { Core.PrintStr("Hello world!\n"); }没有任何package声明——文件属于Main包且不属于命名库,Run即程序入口。
多文件程序中的命名库声明:examples/advent2024/day10_common.carbon 展示了Main包内命名库与同包导入的组合:
library "day10_common"; import Core library "io"; import library "io_utils";跨包导入示例examples/advent2024/day1_part1.carbon:
import Core library "io"; import library "day1_common"; import library "sort"; fn Run() { // ... }这里import Core library "io";是跨包导入(引入名字Core,因此调用需写作Core.Print、Core.Range),而import library "day1_common";是同包导入,直接把ReadInputs等名字引入文件作用域。两者语法的差异正是提案所强调的"我们的库 vs 他们的库"的视觉区分。
标准库自身的写法:Main包之外的包仍保留完整声明,例如 core/prelude/types.carbon 开头为package Core library "prelude/types";——注意它只有package ... library ...;而没有api/impl后缀,这与提案中"非Main包文件沿用 #107 语法"的表述一致(当前仓库中api/impl区分由其他机制处理)。
设计依据:与项目目标的对应
提案在 Rationale 一节将每项决定映射到 docs/project/goals.md 中的目标:
- 社区与文化(Community and culture):小段完整程序不再需要
package声明,便于在论坛中直接讨论代码,减轻冗长顶层语法对技术讨论的阻碍; - 语言工具与生态(Language tools and ecosystem):工具测试用例不再被强制要求写
package声明和Main函数,使其变为可选; - 易读、易理解、易编写(Code that is easy to read, understand, and write):初学者写 "hello, world" 的样板代码减少;消除不增加理解价值的冗余;
- 与 C++ 互操作及迁移(Interoperability with and migration from existing C++ code):明确允许包含 Carbon 代码的程序的入口点用其他语言编写;
Main.Run的命名刻意与 C++ 的main足够相似以降低认知成本; - 单一实现路径原则(Prefer providing only one way to do a given thing):每种
package/import声明只有一种合法拼写——不允许冗余指定当前包名、不允许在可省略library段时写library default、不允许在Main包内显式写package声明。该原则的完整论述见 docs/project/principles/one_way.md。
备选方案评估:设计决策的完整脉络
提案详细记录了七类被否决的备选方案,理解这些对比有助于把握语法的边界条件:
- 强制显式书写
Main:如package Main impl;,保持语言一致性、消除特例,但在最简单的程序中增加最昂贵的样板代码,被否决; - 允许书写
Main并视作省略的同义形式:会引入不对应任何意图差异的语法选择,制造无意义风格分歧,违背单一实现路径原则,被否决; - 区分文件作用域与包作用域:只把本地声明引入文件作用域、其他名字需加
PackageName.前缀。经 #1136 长时间讨论后认为问题大于收益;此前可以通过私有别名绕过(例如fn Foo(); private alias OuterFoo = Foo;后以OuterFoo()访问包作用域中Foo,见 docs/project/goals.md 之外提案正文的示例),#1136 还决定未来引入package.Name语法但不在本提案范围内; - 在导入中保留包名:如
package Foo api; import Foo library "Bar";。虽然语法更统一、便于工具搜索,但同语法掩盖了同包/跨包导入的语义差异,且需要为Main包命名从而提供从其他包导入Main包库的非法途径,被否决; - 主包不命名:最终选择命名
Main,因为文档与对话中谈论"主包"更清晰,且名字混排(name mangling)、属性(attributes)、错误消息、源文件命名约定等技术场景可能需要标识符引用该包;保留Main标识符的成本(使其他包无法使用该名字)被认为很小; - 入口点换名:考虑过
Start(只描述开始而非完整执行)、Entry/Entrypoint(不易发现、对初学者不熟悉、非动词)、Exec/Execute(与 C 的exec执行其他程序的语义冲突),最终选择Main(与 C/C++ 的main相似),详见 #1869; - 主包换名:主要候选是
Program,但无决定性技术论据;设计者(painter)选择Main的理由包括:Main未被用作函数名、作为"主包"含入口点不太可能与别的语言的main混淆、比Program更短、有利于约定源文件名main.carbon、便于寻找main函数的人定位本语言版本。相关入口点命名的另一探索见提案 #2265(Acknowledgements 中致谢了其作者 Allison Poppe)。
结语:从提案到可运行程序
该提案的核心成果可以概括为一句话:Main包内,能省的声明全部省掉,跨包导入保持包名,同包导入直接引入库名。今天的 Carbon 仓库中,examples/hello_world.carbon 这样仅有import与fn Run的文件已经可以编译运行,而解析器(toolchain/parse/handle_import_and_package.cpp)与语义检查(toolchain/check/handle_function.cpp、toolchain/sem_ir/entry_point.cpp)则以源码形式固化并校验了这套规则。若要进一步了解提案所依赖的顶层组织模型,可继续阅读 proposals/p000107-code-and-name-organization.md 与 docs/design/code_and_name_organization 目录下的设计文档。
【免费下载链接】carbon-langCarbon Language's main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考