Skip to content

RFC-038: 语句终止与换行规则 ​

摘要 ​

定义 YaoXiang 的语句终止规则:以换行为主边界,以**括号闭合、行尾二元运算符、行首 .(链式续行)**为显式续行例外;行首 ( [ 永不并入上一语句。; 保留为显式分隔符(单行多语句)。

本 RFC 同时修补 syntax.md 从未定义语句终止符的规范空白,并修复 parser 将行首 ( [ . 吞并为上一语句后缀的已知缺陷。

动机 ​

为什么需要这个特性? ​

YaoXiang 的 ; 完全可选(parser 14 处 skip(Semicolon)),但换行从未被定义为语句边界:lexer 丢弃换行,parser 的语句边界唯一判定是「表达式 Pratt 循环自然停止」——即「下一个 token 能否继续表达式」。这导致:

  1. 行为自相矛盾:换行后跟 identifier/字面量 → 正常终止;换行后跟 ( [ . → 被吞并为上一表达式后缀(调用/索引/字段访问)
  2. 合法语句被静默破坏:(c, d) = (3, 4) 解构变成 1(c, d) = (3, 4),报误导性 E1001;f()(2)、1[99] 连错都不报,语句直接蒸发
  3. 规范空白:syntax.md §2.9 Block ::= '{' 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 + ⏎ 2BinOp(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.rsparse_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:倾向不支持,括号包裹即可)
  • [ ] ; 后换行是否等价于空语句?现有 parser skip(Semicolon) 后接换行自然继续,无需特殊处理
  • [ ] 是否引入「未使用表达式结果警告」(Swift 式)作为吞并的进一步兜底?独立 RFC 讨论

附录 A:多语言调研对照 ​

语言方案行首 (链式 .行首运算符评价
Golexer 自动插分号(2 规则)✅ 安全❌ 禁❌ 禁最确定,牺牲链式风格
JavaScriptASI 3 规则 + restricted productions❌ 著名坑可⚠️ 坑反面教材(a\n(b) → 调用)
PythonNEWLINE 硬边界 + 括号隐式续行✅ 安全❌ 需括号❌ 需括号最可预测,续行最弱
KotlinSEMI = 分号或换行✅ 安全✅❌(尾随 lambda 坑)好,规则藏得深
Swift换行终止 + 空格规则✅ 安全✅✅ 空格防护最接近本提案
Scala 3nl token + 区域规则 + 前导运算符✅✅✅最全但最复杂,过度工程化

本提案 = Go 的确定性 × Python 的括号续行 × Swift 的行首 . 链式。Swift 最接近理想;Scala 3 最全但规则过重——对人类友好不是规则最全,而是规则少且每条符合直觉。