RFC-038: 语句终止与换行规则
摘要
定义 YaoXiang 的语句终止规则:以换行为主边界,以**括号闭合、行尾二元运算符、行首 .(链式续行)**为显式续行例外;行首 ( [ 永不并入上一语句。; 保留为显式分隔符(单行多语句)。
本 RFC 同时修补 syntax.md 从未定义语句终止符的规范空白,并修复 parser 将行首 ( [ . 吞并为上一语句后缀的已知缺陷。
动机
为什么需要这个特性?
YaoXiang 的 ; 完全可选(parser 14 处 skip(Semicolon)),但换行从未被定义为语句边界:lexer 丢弃换行,parser 的语句边界唯一判定是「表达式 Pratt 循环自然停止」——即「下一个 token 能否继续表达式」。这导致:
- 行为自相矛盾:换行后跟 identifier/字面量 → 正常终止;换行后跟
([.→ 被吞并为上一表达式后缀(调用/索引/字段访问) - 合法语句被静默破坏:
(c, d) = (3, 4)解构变成1(c, d) = (3, 4),报误导性 E1001;f()(2)、1[99]连错都不报,语句直接蒸发 - 规范空白:
syntax.md§2.9Block ::= '{' Stmt* Expr? '}'未定义 Stmt 之间的分隔符;§1.2 分隔符表只有( ) { } ,
当前的问题
| 块内代码 | 当前 AST | 结果 |
|---|---|---|
x = 1 ⏎ (c, d) = (3, 4) | Assign x = BinOp(Assign, Call(1,[c,d]), Tuple) | E1001 指向无辜 c |
x = f() ⏎ (2) | Call(Call(f),[2]) | 静默(语句蒸发) |
x = 1 ⏎ [99] | Index(1,99) | 静默 |
x = 1 ⏎ .println("hi") | Call(Field(1),["hi"]) | E1053 |
x = 1 + ⏎ 2 | BinOp(Add) | ✅ 合法(但无规范依据) |
x = (1, ⏎ 2) | Tuple | ✅ 合法(但无规范依据) |
提案
核心设计
主规则:换行终止语句。
续行例外(三组,均有主流语言先例):
| # | 例外 | 规则 | 先例 |
|---|---|---|---|
| 1 | 括号未闭合 | ( [ { 深度 > 0 时,换行不终止(隐式续行) | Python/Scala |
| 2 | 行尾二元运算符 | 一行以二元运算符结尾 → 续行 | Swift/Scala |
| 3 | 行首 . 链式续行 | 行首 . 且上一行以标识符/)/] 结尾 → 续行 | Swift |
永不合并(吸取 JS 最大教训):
- 行首
(、[→ 永远开启新语句。JS 的著名坑(a\n(b)→a(b)调用)在 YaoXiang 尤其危险:元组解构(a, b) = ...、spawn、元组字面量全是括号开头语句。 - 行首二元/一元运算符(
+-*等)→ 开启新语句。行尾运算符续行已覆盖常见折行风格;行首运算符续行(Scala 3 前导运算符)会引入一元/中缀歧义,不采用。若需折行,用括号包裹。
; 保留:显式分隔符,单行多条语句时使用(Kotlin/Swift 同款)。
示例
yaoxiang
// 换行终止语句(绝大多数代码)
a = 1
b = 2
// 续行:行尾二元运算符
total = a +
b + c
// 续行:括号未闭合
t = (1,
2)
io.println(
"hi")
// 续行:行首 .(链式调用)
result = list.map(x => x * 2)
.filter(x => x > 10)
.sum()
// 永不合并:行首 ( 是独立解构语句
x = f()
(c, d) = (3, 4) // ✅ 解构,不是 f()(c, d)
// 永不合并:行首 [ 是独立列表
x = 1
[1, 2, 3] // ✅ 独立表达式语句
// 单行多语句:分号
a = 1; b = 2语法变化
syntax.md §2.9 之后新增:
StatementTerminator ::= ';' | Newline (除非下列续行例外)
续行例外(换行不终止):
- '(' '[' '{' 深度 > 0
- 行尾为二元运算符
- 行首为 '.' 且上一行以 Identifier | ')' | ']' 结尾
永不合并:行首 '(' '[' 恒为新语句| 之前 | 之后 |
|---|---|
Block ::= '{' Stmt* Expr? '}'(无分隔定义) | Block ::= '{' (Stmt StatementTerminator)* Expr? '}' |
换行无地位,( [ . 行首被吞并 | 换行终止;( [ 行首永不合并;. 行首显式续行 |
; 可选但语义含糊 | ; = 显式分隔(单行多语句),; 后可有换行 |
详细设计
语句终止判定(parser 规则)
在表达式 Pratt 循环中,后缀运算符(( 调用、[ 索引、. 字段访问)应用前检查:
continuation(prev_expr, op_token) =
prev_expr 结束行 == op_token 起始行 // 同行:正常后缀
|| (op_token == '.' 且 prev_expr 以 Identifier/')'/']' 结束) // 行首 . 链式
|| 括号深度 > 0 // 在未闭合括号内不满足 → 表达式结束,开启新语句。中缀二元运算符不参与行检查(行尾运算符 = 续行,天然正确)。
语法影响面
parse_expression后缀分支加入行号检查(Span 自带行号,无需改 lexer token 流)- 语句解析处消费
;与新行边界(skip(Semicolon)语义保留) - 块结束
}、文件结束 EOF 天然终止语句,无需额外处理 - 不引入 NEWLINE token(方案 B,见替代方案)——Span 行号已足够,改动最小
编译器改动
| 组件 | 改动 |
|---|---|
src/frontend/core/parser/parser_state.rs | parse_expression 后缀运算符行检查 |
src/frontend/core/parser/statements/*.rs | 语句边界消费逻辑微调 |
docs/src/reference/language-spec/syntax.md | §2.9 后新增「语句终止规则」小节 |
| 测试 | tests/yaoxiang/01-syntax/ 新增换行/续行用例;06-compile-errors/ 新增行首吞并回归 |
向后兼容性
- 绝大多数现有代码零改动:identifier/字面量开头的行、行尾运算符折行、括号内换行均与现状一致
- 行为变化点(均为 bug 修复方向):
f()(2)/1[99]/1.println()跨行吞并从「静默/报错」变为「两行独立语句」——正确语义- 行首
.从「吞并(报错)」变为「显式链式续行」——新特性
- 风险:若存在依赖「跨行吞并」的合法代码(如
f()\n(2)意图调用返回函数),将变为f()+(2)两条语句。此类代码本就语义错误或极罕见,需在 RFC 评审中确认
权衡
优点
- 规则少且符合直觉:两条主规则 + 三组例外,全部来自主流语言验证过的实践
- 消除静默错误:
f()(2)、1[99]等不再静默吞并,语句边界可预测 - 链式调用友好:行首
.续行对齐 Swift/业界标准风格 - 零 lexer 改动:Span 行号方案最小侵入
缺点
- 行首二元运算符续行不支持(Scala 3 式)——需括号包裹折行,少数风格受限
- 行首
.续行的判定依赖「上一行以标识符/)/]结尾」,规则需文档化
替代方案
| 方案 | 描述 | 为何不选 |
|---|---|---|
| A. Span 行号感知(本提案) | parser 后缀运算符检查行号 | ✅ 采用:改动最小,规则清晰 |
| B. Go 式自动插分号 | lexer nlsemi 状态机,行尾特定 token 后插 ; | 强制 { 行尾、禁行首 ./运算符,牺牲链式风格;与 YaoXiang 自由风格不符 |
| C. 全分号强制 | 每语句必须 ; | 违背分号可选的既有生态与测试文件现状 |
| D. 维持现状 | 无规则,token 驱动 | 已知缺陷持续,静默语句蒸发 |
实现策略
依赖关系
- 无外部依赖
- 与 RFC-010(统一类型语法)正交:语句终止是 parser 层规则,不涉及类型文法
风险
- 行首
.链式与「行首.独立语句」的边界:.foo()无法独立成句(.不是合法语句起始),故续行判定无歧义 - 现有测试若依赖吞并行为需逐一核查(预期无——吞并均为 bug 场景)
开放问题
- [ ] 行首二元运算符续行(Scala 3 式)是否值得支持?(@ChenXu233:倾向不支持,括号包裹即可)
- [ ]
;后换行是否等价于空语句?现有 parserskip(Semicolon)后接换行自然继续,无需特殊处理 - [ ] 是否引入「未使用表达式结果警告」(Swift 式)作为吞并的进一步兜底?独立 RFC 讨论
附录 A:多语言调研对照
| 语言 | 方案 | 行首 ( | 链式 . | 行首运算符 | 评价 |
|---|---|---|---|---|---|
| Go | lexer 自动插分号(2 规则) | ✅ 安全 | ❌ 禁 | ❌ 禁 | 最确定,牺牲链式风格 |
| JavaScript | ASI 3 规则 + restricted productions | ❌ 著名坑 | 可 | ⚠️ 坑 | 反面教材(a\n(b) → 调用) |
| Python | NEWLINE 硬边界 + 括号隐式续行 | ✅ 安全 | ❌ 需括号 | ❌ 需括号 | 最可预测,续行最弱 |
| Kotlin | SEMI = 分号或换行 | ✅ 安全 | ✅ | ❌(尾随 lambda 坑) | 好,规则藏得深 |
| Swift | 换行终止 + 空格规则 | ✅ 安全 | ✅ | ✅ 空格防护 | 最接近本提案 |
| Scala 3 | nl token + 区域规则 + 前导运算符 | ✅ | ✅ | ✅ | 最全但最复杂,过度工程化 |
本提案 = Go 的确定性 × Python 的括号续行 × Swift 的行首 . 链式。Swift 最接近理想;Scala 3 最全但规则过重——对人类友好不是规则最全,而是规则少且每条符合直觉。
