news 2026/9/10 0:39:06

Carbon 语言 `Main` 包简化声明提案解析:无 `package` 头的可执行程序与统一导入语法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Carbon 语言 `Main` 包简化声明提案解析:无 `package` 头的可执行程序与统一导入语法

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),它重新定义了可执行程序中packageimport声明的书写规则:主包(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 风格参数(argcargv)形式,返回类型为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引入名字FooFoo不能是当前包名
import library "Bar";导入当前包的库Bar把该库的公共名字直接引入文件作用域;Bar不能是当前库名
import library default;导入当前包的默认库不允许在当前默认库内部使用

这种设计带来一个重要的可读性收益:读者一眼就能区分"我们的库"(import library "...",无包名)与"他们的库"(import Foo ...,有包名)。

解析器对default的支持

在 toolchain/parse/context.cpp 中,ParseLibraryNameParseLibrarySpecifier处理库名与库说明符,支持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.PrintCore.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。

备选方案评估:设计决策的完整脉络

提案详细记录了七类被否决的备选方案,理解这些对比有助于把握语法的边界条件:

  1. 强制显式书写Main:如package Main impl;,保持语言一致性、消除特例,但在最简单的程序中增加最昂贵的样板代码,被否决;
  2. 允许书写Main并视作省略的同义形式:会引入不对应任何意图差异的语法选择,制造无意义风格分歧,违背单一实现路径原则,被否决;
  3. 区分文件作用域与包作用域:只把本地声明引入文件作用域、其他名字需加PackageName.前缀。经 #1136 长时间讨论后认为问题大于收益;此前可以通过私有别名绕过(例如fn Foo(); private alias OuterFoo = Foo;后以OuterFoo()访问包作用域中Foo,见 docs/project/goals.md 之外提案正文的示例),#1136 还决定未来引入package.Name语法但不在本提案范围内;
  4. 在导入中保留包名:如package Foo api; import Foo library "Bar";。虽然语法更统一、便于工具搜索,但同语法掩盖了同包/跨包导入的语义差异,且需要为Main包命名从而提供从其他包导入Main包库的非法途径,被否决;
  5. 主包不命名:最终选择命名Main,因为文档与对话中谈论"主包"更清晰,且名字混排(name mangling)、属性(attributes)、错误消息、源文件命名约定等技术场景可能需要标识符引用该包;保留Main标识符的成本(使其他包无法使用该名字)被认为很小;
  6. 入口点换名:考虑过Start(只描述开始而非完整执行)、Entry/Entrypoint(不易发现、对初学者不熟悉、非动词)、Exec/Execute(与 C 的exec执行其他程序的语义冲突),最终选择Main(与 C/C++ 的main相似),详见 #1869;
  7. 主包换名:主要候选是Program,但无决定性技术论据;设计者(painter)选择Main的理由包括:Main未被用作函数名、作为"主包"含入口点不太可能与别的语言的main混淆、比Program更短、有利于约定源文件名main.carbon、便于寻找main函数的人定位本语言版本。相关入口点命名的另一探索见提案 #2265(Acknowledgements 中致谢了其作者 Allison Poppe)。

结语:从提案到可运行程序

该提案的核心成果可以概括为一句话:Main包内,能省的声明全部省掉,跨包导入保持包名,同包导入直接引入库名。今天的 Carbon 仓库中,examples/hello_world.carbon 这样仅有importfn 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 0:39:05

STM32F103 CAN总线Bootloader设计与实现详解

简介:面向STM32F103的CAN总线Bootloader完整源码包,专为需要在CAN网络中远程升级固件的嵌入式工程师与学生设计。内容贯穿Bootloader全流程:CAN控制器初始化、标准帧与扩展帧分离收发、通信错误处理、固件分帧接收与CRC校验、Flash编程与烧写…

作者头像 李华
网站建设 2026/9/10 0:38:00

STM32 PWM呼吸灯实战:从TIM3配置到引脚重映射的完整解析

简介:这是一个面向STM32F1微控制器的双极性SPWM波形生成代码工程,适用于逆变器、电机驱动、开关电源等需要正弦波输出的应用场景,也适合正在学习电力电子与嵌入式实时控制的开发者参考,可作为高校相关课程设计与毕业设计的参考模板…

作者头像 李华
网站建设 2026/9/10 0:36:54

基于ESP8266的WiFi网络授时时钟设计与实现

简介:一份完整的STM32网络授时时钟工程代码,以STM32F103C8T6为主控、ESP-12F为WiFi模块,配合PCF8563时钟芯片、按键和OLED显示屏,实现联网获取天气与时间信息并定时刷新。工程按功能拆分为多个可读性较强的模块:bsp_es…

作者头像 李华
网站建设 2026/9/10 0:36:03

PS2手柄+STM32四电机麦克纳姆轮小车:从接线到PID调试全解析

简介:这是一套基于STM32RCT6微控制器、利用PS2手柄控制四轮全向轮小车的完整工程资源。项目从手柄信号解析、电机PWM调速到全向轮运动逻辑均有详细代码实现,适合学习STM32库函数开发、嵌入式电机控制及无线遥控小车的开发者参考。压缩包共233个文件&…

作者头像 李华