RFC-038: 文の終端と改行ルール
概要
YaoXiang の文の終端ルールを定義する。改行を主境界とし、括弧の未閉合、行末の二項演算子、行頭の .(チェーン継続)を明示的な継続の例外とする。行頭の ( [ は決して前の文に統合されない。; は明示的な区切り文字(単一行の複数文)として保持される。
本 RFC は同時に、syntax.md が文の終端記号を一度も定義していないという規範上の空白を埋めるとともに、parser が行頭の ( [ . を前の文の接尾辞として吸収してしまう既知の欠陥も修正する。
動機
なぜこの機能が必要なのか?
YaoXiang では ; は完全に任意(parser に 14 箇所の skip(Semicolon))であるが、改行は文境界として一度も定義されていない:lexer は改行を破棄し、parser の文境界の唯一の判定は「式の Pratt ループが自然に停止するか」、すなわち「次のトークンが式を継続できるか」である。これにより以下の問題が生じる:
- 動作の自己矛盾:改行の後に 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 | ✅ 合法(ただし規範上の根拠なし) |
提案
中核となる設計
主ルール:改行は文を終了させる。
継続の例外(3 グループ、いずれも主流言語の先例あり):
| # | 例外 | ルール | 先例 |
|---|---|---|---|
| 1 | 括弧の未閉合 | ( [ { の深さ > 0 のとき、改行は終端しない(暗黙の継続) | Python / Scala |
| 2 | 行末の二項演算子 | 行末が二項演算子で終わる → 継続 | Swift / Scala |
| 3 | 行頭の . チェーン継続 | 行頭が . であり、前の行が identifier / ) / ] で終わる → 継続 | Swift |
決して統合しない(JS の最大の教訓を吸収):
- 行頭の
(、[→ 常に新しい文を開始する。JS の有名な落とし穴(a\n(b)→a(b)の呼び出し)は YaoXiang では特に危険:タプル分解(a, b) = ...、spawn、タプルリテラルは全て括弧で始まる文である。 - 行頭の二項 / 単項演算子(
+-*など)→ 新しい文を開始する。行末演算子による継続は一般的な折返しスタイルをすでにカバーしている;行頭演算子による継続(Scala 3 の前置演算子)は単項 / 中置の曖昧さを生むため採用しない。折り返しが必要な場合は括弧で囲む。
; の保持:明示的な区切り文字として、単一行の複数文で使用する(Kotlin / Swift と同方式)。
例
// 改行は文を終端する(ほとんどのコード)
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 / リテラルで始まる行、行末演算子での折り返し、括弧内の改行はすべて現状と一致
- 動作が変化するもの(すべてバグ修正の方向):
f()(2)/1[99]/1.println()の行をまたぐ吸収が「静かに / エラー」から「2 行の独立した文」になる——正しいセマンティクス- 行頭の
.が「吸収(エラー)」から「明示的なチェーン継続」へ——新機能
- リスク:行をまたぐ吸収に依存している合法的なコード(例:
f()\n(2)が返された関数の呼び出しを意図)があれば、f()+(2)の 2 文になる。そのようなコードはそもそもセマンティクスが誤っているか極めて稀であり、RFC レビューでの確認が必要
トレードオフ
利点
- ルールが少なく直感に合う:主ルール 2 本 + 例外 3 グループ、すべて主流言語で検証済みの実践
- 静かなるエラーの解消:
f()(2)、1[99]などが静かに吸収されなくなり、文境界が予測可能になる - チェーン呼び出しに親切:行頭の
.継続は Swift / 業界標準のスタイルに揃う - lexer の変更ゼロ:Span 行番号方式で最小侵襲
欠点
- 行頭二項演算子の継続は未対応(Scala 3 式)——括弧で囲む必要があり、一部のスタイルが制限される
- 行頭の
.継続の判定は「前の行が identifier /)/]で終わる」に依存し、ルールの文書化が必要
代替案
| 案 | 説明 | 採用しない理由 |
|---|---|---|
| A. Span 行番号認識(本提案) | parser の接尾辞演算子が行番号をチェック | ✅ 採用:変更が最小、ルールが明快 |
| B. Go 式の自動セミコロン挿入 | lexer の nlsemi ステートマシンで、行末の特定トークン後に ; を挿入 | { の行末を強制し、行頭の . / 演算子を禁止してチェーンスタイルを犠牲にする;YaoXiang の自由なスタイルに合わない |
| C. セミコロン完全強制 | 各文に ; を必須化 | セミコロン任意という既存のエコシステムとテストファイルの実態に反する |
| D. 現状維持 | ルールなし、トークン駆動 | 既知の欠陥が継続し、文が静かに蒸発する |
実装戦略
依存関係
- 外部依存なし
- RFC-010(統一型文法)とは直交:文の終端は parser 層のルールであり、型の文法には関与しない
リスク
- 行頭の
.チェーンと「行頭の.独立文」の境界:.foo()は独立した文になり得ない(.は合法な文の開始トークンではない)ため、継続判定に曖昧さはない - 既存のテストで吸収動作に依存しているものがあれば、個別に確認が必要(想定ではなし——吸収はすべてバグシナリオ)
未解決の問題
- [ ] 行頭二項演算子の継続(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 は最も完全だがルールが重すぎる——人間に親切なのはルールが最多であることではなく、ルールが少なく、各ルールが直感に合うことである。
