diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..dc005c1
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,49 @@
+# Changelog
+
+## 0.4.0 — 軽量 GLR (LightGlr) モード + 読み取り専用 OnReduce
+
+### ⚠ 破壊的変更 (後方互換性なし)
+
+0.3.0 から以下の API が変更されました。既存コードの修正が必要です。
+
+- **OnReduce の ctx が読み取り専用**: `OnReduce(MyCtx ctx)` → `OnReduce(SemanticContext ctx)`。宣言 (TryDeclare) や診断追加 (Error) は不可。
+- **SemanticContext.Symbols が読み取り専用**: `IReadOnlySymbolTable` を返す (Lookup のみ)。
+- **SemanticContext.Diagnostics 削除**: Diagnostics は `BasicSemanticContext` に移動。
+- **書き込みは [Enter]/[Exit] で**: `ctx.WritableSymbols.TryDeclare(...)` / `ctx.Diagnostics.Error(...)`。
+- **LALR のパニックモード削除**: スタック pop の代わりに Corchuelo et al. ER1/ER2/ER3 を使用。
+- **List が COW に**: 破壊的 `List.Add` → copy-on-write (ErrorRepair の probe 安全性)。
+
+### 新機能
+
+- **軽量 GLR (LightGlr) モード**: `[Grammar(ParseMode = ParseMode.LightGlr)]` で本質的曖昧性 (cast/paren, generic) を並行 fork で解決。
+- **ErrorRepair (Corchuelo et al.)**: ER1 挿入 / ER2 削除 / ER3 Forward move。LALR・LightGlr 両モードで共通使用。トークンを捨てない高品質エラー回復。
+- **OnAccepted(ctx)**: GLR の fork が収束 (ルート確定) した時に呼ばれるコールバック。ctx 書き込み可能。
+- **AmbiguousCandidates**: `ParseResult.AmbiguousCandidates` で複数解釈を観察可能。
+- **NotifyAccepted**: ルート確定を AST に通知する virtual メソッド。
+- **IReadOnlySymbolTable**: 読み取り専用シンボル表インターフェース。
+- **GrammarSpec の [Rule] モデル対応**: Perf.Gen が正しく [Rule] static メソッドを生成。
+
+### パフォーマンス
+
+- LightGlrDriver に fast path (単一スタック時 LALR に近い性能) を追加。
+- 単一スタック時は List/Queue/HashSet をバイパス、アロケーションほぼゼロ。
+- fork が必要なコンフリクト経路のみ Clone。
+
+### 内部変更
+
+- `AstFirst.Glr.ErrorRepair` クラス追加 (Corchuelo ER1/ER2/BR3 共通ロジック)。
+- `AstFirst.Glr.GlrTables` クラス追加 (LALR テーブル参照ラッパ)。
+- `AstFirst.Glr.LightGlrDriver` クラス追加 (Tomita-lite GLR ドライバ)。
+- `GlrParserEmitter` クラス追加 (LightGlr 専用エミッタ)。
+- `ParserEmitter` の panic モードを Corchuelo 修復に置換。
+
+---
+
+## 0.3.0 — 意味解析機能の拡充
+
+- [Enter]/[Exit]/[OnReduce] 属性・汎用 Walker・型システム拡張
+- AST ノードの Span を子から自動計算 (OnReduce で上書き可)
+
+## 0.2.x — 初期リリース
+
+- LALR(1) パーサ生成・Lexer・AST・[Rule] static モデル
diff --git a/Directory.Build.props b/Directory.Build.props
index d6ad8ee..b0c76e8 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 0.3.0
+ 0.4.0
actbit
https://github.com/actbit/AstFirst
https://github.com/actbit/AstFirst
diff --git a/README.ja.md b/README.ja.md
index 0982f1b..3181e45 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -5,7 +5,7 @@
日本語 / [English](README.md)
-C# の**クラスと属性**で文法を書くと、Source Generator がコンパイル時に Lexer と LALR(1) Parser を生成するパーサジェネレータ。生成された Parser は意味解析(スコープ付きシンボル表・2パス目 Walker・型チェック・`Accept`/`Reject` による意味的曖昧性解決)を乗せられる AST を返す。
+C# の**クラスと属性**で文法を書くと、Source Generator がコンパイル時に Lexer と LALR(1) / 軽量 GLR (LightGlr) Parser を生成するパーサジェネレータ。生成された Parser は意味解析(スコープ付きシンボル表・2パス目 Walker・型チェック・`Accept`/`Reject` による意味的曖昧性解決)を乗せられる AST を返す。
## 他のライブラリとの比較
@@ -17,13 +17,13 @@ AstFirst はパーサジェネレータ・パーサコンビネータと同じ
| コード生成 | **コンパイル時** (Source Generator) | ビルド時コード生成ツール | **しない** (実行時に解釈) | 該当なし |
| 実行時パースコスト | **ゼロ** — 静的テーブル・ディスパッチなし | 生成コード | 解釈実行 (パース毎にアロケーション/ディスパッチ) | 該当なし |
| AOT / Native AOT | ✓ 実行時コード生成なし | △ | ✓ | 該当なし |
-| アルゴリズム | LALR(1) テーブル駆動 | LL(\*) / ALL(\*) | 再帰下降コンビネータ | 該当なし |
-| エラー回復 | panic mode (組み込み) | あり | 自作が必要 | 該当なし |
-| 文法の表現力 | LALR(1) — `[Precedence]` で衝突解決 | LL(\*) | **チューリング完全** (任意の C# で分岐) | 該当なし |
+| アルゴリズム | LALR(1) + 軽量 GLR (LightGlr) | LL(\*) / ALL(\*) | 再帰下降コンビネータ | 該当なし |
+| エラー回復 | Corchuelo et al. ER1/ER2/ER3 (組み込み) | あり | 自作が必要 | 該当なし |
+| 文法の表現力 | LALR(1) + GLR — `[Precedence]` / fork で衝突解決 | LL(\*) | **チューリング完全** (任意の C# で分岐) | 該当なし |
-**コンビネータ (Superpower/Pidgin) に対する強み**: パーサを*コンパイル時*に静的テーブルとして生成するため、解釈もディスパッチもパーサ構築も実行時に走らない。起動・パース毎のコストが事実上ゼロで、AOT/Native AOT にも綺麗に通る。panic mode のエラー回復を組み込み。文法は宣言的な C# なので、IDE のナビゲーションやリファクタリングが効く。
+**コンビネータ (Superpower/Pidgin) に対する強み**: パーサを*コンパイル時*に静的テーブルとして生成するため、解釈もディスパッチもパーサ構築も実行時に走らない。起動・パース毎のコストが事実上ゼロで、AOT/Native AOT にも綺麗に通る。Corchuelo et al. の高品質エラー回復 (ER1 挿入 / ER2 削除 / ER3 Forward move) を組み込み。文法は宣言的な C# なので、IDE のナビゲーションやリファクタリングが効く。
-**トレードオフ**: LALR(1) は `[Precedence]`/結合性で shift-reduce 衝突を解決する必要がある(コンビネータはパース中に任意の C# で分岐できて自由)。C# 専用のツールチェイン。
+**トレードオフ**: LALR(1) は `[Precedence]`/結合性で shift-reduce 衝突を解決する必要がある。ただし LightGlr モード (`[Grammar(ParseMode = ParseMode.LightGlr)]`) で本質的曖昧性 (cast/paren・generic 等) を並行 fork で扱える。C# 専用のツールチェイン。
**なぜ Source Generator か**: Lexer/Parser/Walker はコンパイラが見る普通の C# で、IDE で開ける (`.g.cs` がプロジェクト内にあり、Go to Definition が効く)。実行時にパーサを*構築*することは一度もなく、文法のミス(未解決コンフリクト・到達不能規則)は初回 `Parse` ではなく**コンパイル時の警告**として出る。
@@ -32,12 +32,12 @@ AstFirst はパーサジェネレータ・パーサコンビネータと同じ
- **C# コードで文法定義**: クラスの継承ツリーで構文、`[Rule]` static メソッドの引数で右辺・字句ルール。特別な構文や DSL ファイルは不要。
- **Source Generator (`IIncrementalGenerator`)**: コンパイル時に Lexer / Parser / partial プロパティ の C# コードを生成。実行時コード生成なし。
- **正規表現ベースのレクサ**: 文字クラス圧縮、最長一致 + 優先度駆動、`{m,n}` 量指定子、Unicode 補助面に対応。トークンの**行・列**も計算。
-- **LALR(1) 構文解析**: 優先度/結合性 (`[Precedence]`) で shift-reduce 衝突を解決(`*` > `+`、代入の右結合等)。
-- **意味的曖昧性の解決 (Accept/Reject)**: reduce 時の `OnReduce` で `Reject()` すると、優先度順の別候補(別規則/shift)へフォールバック。cast vs 括弧式のような意味依存の曖昧性を構文解析で解決できる。
+- **LALR(1) + 軽量 GLR 構文解析**: デフォルトは LALR(1)。`[Grammar(ParseMode = ParseMode.LightGlr)]` で軽量 GLR に切り替え、本質的曖昧性 (cast/paren・generic) を並行 fork で解決。優先度/結合性 (`[Precedence]`) で shift-reduce 衝突を解決(`*` > `+`、代入の右結合等)。
+- **高品質エラー回復 (Corchuelo et al.)**: ER1 挿入 / ER2 削除 / ER3 Forward move で構文エラー後も解析を継続。トークンを捨てない。LALR・GLR 両モードで共通使用。
- **AST 構築 + 子の自動保持 + Span 自動計算**: reduce 時に Generator 生成の partial コンストラクタが子・終端をプロパティへ自動セットし、子の `Span` をマージしてノードの `Span` を設定してから `OnReduce` を呼ぶ。子の手動代入も Span 設定も不要(`OnReduce` で上書き可)。
- **2パス目の意味解析**: `Parse` 後に各ノードの `OnSecondPassEnter`/`OnSecondPassExit`(トップダウン)を自動呼出。スコープの Push/Pop 等の正確な意味解析が書ける。
- **意味解析ヘルパー**: `[Enter]`/`[Exit]`/`[OnReduce]` 属性ルール、汎用 Walker (`{Root}Walker`)、スコープ付きシンボル表 (`ScopedSymbolTable`)、シンボル解決 (`ResolveOrError`)、型システム (`TypeSymbol` / `FunctionTypeSymbol` / `ArrayTypeSymbol` / `OverloadResolver`)、束縛解析 (`AstNode.SetAnnotation`)、診断 (`ParseResult.Diagnostics`)。
-- **エラー回復**: panic mode で構文エラー後も解析を継続し、`ParseResult` で AST + エラーリストを返す。
+- **エラー回復**: Corchuelo et al. ER1/ER2/ER3 で構文エラー後も解析を継続し、`ParseResult` で AST + エラーリストを返す。
## クイックスタート
@@ -303,14 +303,14 @@ AST 構築成功(宣言/継承/generic クラス/enum/struct/interface/プロ
| 指標 | 値 |
|---|---|
-| Parse_CSharp(50 クラス入力 ≈ 7KB) | 0.68 ms(≈ 10 MB/s)※ |
-| Parse_CSharp アロケーション | 627 KB |
+| Parse_CSharp(50 クラス入力 ≈ 7KB、LightGlr) | 11.4 ms(≈ 0.6 MB/s)※ |
+| Parse_CSharp アロケーション | 1,931 KB |
| Build_CSharp(ModelToTable.Build 純粋時間) | 190 ms |
-| クリーンビルド時間 | 11 s |
+| クリーンビルド時間 | 12 s |
| LALR 状態数 / シンボル数 | 798 / 608 |
-| 生成コードサイズ | 6.0 MB(8011 行) |
+| 生成コードサイズ | 6.2 MB(15363 行) |
-> ※ Parse 時間は [Rule] モデル移行後(Reject/TryFallback + Span スタック)の代表値。Build_CSharp 190 ms は Worklist アルゴリズム最適化後。詳細は [samples/Perf/PerfSummary.md](samples/Perf/PerfSummary.md) 参照。Parse のアロケーションは AST 構築(reduce ごとに `new`)が主。
+> ※ CSharpParser は LightGlr モードで運用 (cast/paren・generic の本質的曖昧性を fork で解決)。state 308 で識別子ごとに fork するため、LALR 単一スタック (0.68ms) より低速。LALR モードの文法 (DeepPrec 等) は単一スタックの fast path で LALR に近い性能を維持。詳細は [samples/Perf/PerfSummary.md](samples/Perf/PerfSummary.md) 参照。
### コンフリクト解決技術
@@ -365,7 +365,7 @@ AstFirst.slnx
## テスト
-293 テスト(AstFirst.Tests 247 + Generator.Tests 46)。レクサ/DFA/LALR の各段階、エンドツーエンド、エラー回復、意味解析(スコープ・2パス目・型チェック・ctx → `ParseResult.Diagnostics` の統合)、`Accept`/`Reject` フォールバック、位置情報(行・列)を検証。
+352 テスト(AstFirst.Tests 299 + Generator.Tests 53)。レクサ/DFA/LALR の各段階、エンドツーエンド、エラー回復 (Corchuelo)、GLR fork/dedup、意味解析(スコープ・2パス目・型チェック・ctx → `ParseResult.Diagnostics` の統合)、`Accept`/`Reject` フォールバック、`OnAccepted` コールバック、位置情報(行・列)を検証。
## ライセンス
diff --git a/README.md b/README.md
index 8c15b54..a0bff73 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
[日本語](README.ja.md) / English
-A parser generator where you write the grammar in **plain C# classes and attributes**, and a Source Generator emits a Lexer and an LALR(1) Parser at compile time. The generated Parser returns an AST you can layer semantic analysis on (scoped symbol table, two-pass Walker, type checking, and `Accept`/`Reject` to resolve semantic ambiguity).
+A parser generator where you write the grammar in **plain C# classes and attributes**, and a Source Generator emits a Lexer and an LALR(1) / Lightweight GLR (LightGlr) Parser at compile time. The generated Parser returns an AST you can layer semantic analysis on (scoped symbol table, two-pass Walker, type checking, and `Accept`/`Reject` to resolve semantic ambiguity).
## How it compares
@@ -17,13 +17,13 @@ AstFirst sits in the same space as parser generators and combinator libraries, b
| Code generated | **compile time** (Source Generator) | build-time codegen tool | **never** — interpreted at runtime | n/a |
| Runtime parse cost | **zero** — static tables, no dispatch | generated code | interpreted (allocation/dispatch per parse) | n/a |
| AOT / Native AOT | ✓ no runtime codegen | △ | ✓ | n/a |
-| Algorithm | LALR(1) table-driven | LL(\*) / ALL(\*) | recursive-descent combinators | n/a |
-| Error recovery | panic mode (built-in) | yes | hand-rolled | n/a |
-| Grammar power | LALR(1) — resolve conflicts with `[Precedence]` | LL(\*) | **Turing-complete** (branch on any C#) | n/a |
+| Algorithm | LALR(1) + Lightweight GLR (LightGlr) | LL(\*) / ALL(\*) | recursive-descent combinators | n/a |
+| Error recovery | Corchuelo et al. ER1/ER2/ER3 (built-in) | yes | hand-rolled | n/a |
+| Grammar power | LALR(1) + GLR — resolve conflicts with `[Precedence]` / fork | LL(\*) | **Turing-complete** (branch on any C#) | n/a |
-**Strengths vs combinators (Superpower/Pidgin)**: the parser is emitted *at compile time* as static tables — no interpretation, no delegate dispatch, no per-parse construction. Startup and per-parse cost are effectively zero, it AOTs / Native-AOTs cleanly, and panic-mode error recovery is built in. The grammar is declarative C#, so the IDE can navigate and refactor it.
+**Strengths vs combinators (Superpower/Pidgin)**: the parser is emitted *at compile time* as static tables — no interpretation, no delegate dispatch, no per-parse construction. Startup and per-parse cost are effectively zero, it AOTs / Native-AOTs cleanly, and Corchuelo et al. error recovery (ER1 insert / ER2 delete / ER3 Forward move) is built in. The grammar is declarative C#, so the IDE can navigate and refactor it.
-**Trade-offs**: LALR(1) needs `[Precedence]`/associativity to resolve shift-reduce conflicts — combinator libraries are freer (you can branch on arbitrary C# mid-parse). A C#-only toolchain.
+**Trade-offs**: LALR(1) needs `[Precedence]`/associativity to resolve shift-reduce conflicts. However, LightGlr mode (`[Grammar(ParseMode = ParseMode.LightGlr)]`) handles inherent ambiguity (cast/paren, generics) via parallel fork. A C#-only toolchain.
**Why a Source Generator?** the Lexer/Parser/Walker are ordinary C# the compiler sees and the IDE can open (the `.g.cs` lives under your project, Go to Definition works). You never *build* the parser at runtime, and grammar mistakes (unresolved conflicts, unreachable rules) surface as **compile-time warnings**, not at the first `Parse`.
@@ -32,12 +32,12 @@ AstFirst sits in the same space as parser generators and combinator libraries, b
- **Grammar in C# code**: the inheritance tree expresses syntax; the parameters of a `[Rule]` static method express the RHS and lexical rules. No special DSL files.
- **Source Generator (`IIncrementalGenerator`)**: emits Lexer / Parser / partial properties C# code at compile time. No runtime code generation.
- **Regex-based lexer**: character-class compaction, longest-match + priority-driven, `{m,n}` quantifiers, Unicode supplementary planes. Computes **line/column** of each token.
-- **LALR(1) parsing**: resolves shift-reduce conflicts with precedence/associativity (`[Precedence]`) (e.g. `*` > `+`, right-associative assignment).
-- **Semantic ambiguity resolution (Accept/Reject)**: call `Reject()` in the reduce-time `OnReduce` to fall back to the next candidate (another rule / shift) in priority order. Resolves meaning-dependent ambiguity like cast vs. parenthesized expression during parsing.
+- **LALR(1) + Lightweight GLR parsing**: default is LALR(1). Switch to Lightweight GLR with `[Grammar(ParseMode = ParseMode.LightGlr)]` to handle inherent ambiguity (cast/paren, generics) via parallel fork. Resolves shift-reduce conflicts with precedence/associativity (`[Precedence]`) (e.g. `*` > `+`, right-associative assignment).
+- **High-quality error recovery (Corchuelo et al.)**: ER1 insert / ER2 delete / ER3 Forward move to continue parsing after syntax errors without discarding tokens. Used in both LALR and GLR modes.
- **AST construction + automatic child retention + automatic Span**: at reduce time a generator-emitted partial constructor sets children/terminals into properties automatically, merges their `Span`s into the node's `Span`, and then calls `OnReduce`. No manual child assignment or Span setup (overridable in `OnReduce`).
- **Two-pass semantic analysis**: after `Parse`, each node's `OnSecondPassEnter`/`OnSecondPassExit` (top-down) is called automatically. Accurate semantic analysis like scope Push/Pop is straightforward.
- **Semantic analysis helpers**: `[Enter]`/`[Exit]`/`[OnReduce]` attribute rules, a generic Walker (`{Root}Walker`), scoped symbol table (`ScopedSymbolTable`), symbol resolution (`ResolveOrError`), type system (`TypeSymbol` / `FunctionTypeSymbol` / `ArrayTypeSymbol` / `OverloadResolver`), binding (`AstNode.SetAnnotation`), diagnostics (`ParseResult.Diagnostics`).
-- **Error recovery**: continues after syntax errors via panic mode; `ParseResult` carries the AST + error list.
+- **Error recovery**: continues after syntax errors via Corchuelo et al. ER1/ER2/ER3; `ParseResult` carries the AST + error list.
## Quick start
@@ -316,7 +316,7 @@ Japanese versions are under `docs/ja/` and [README.md](README.md).
## Tests
-293 tests (AstFirst.Tests 247 + Generator.Tests 46). Covers lexer/DFA/LALR stages, end-to-end, error recovery, semantic analysis (scopes, two-pass, type checking, ctx -> `ParseResult.Diagnostics` integration), `Accept`/`Reject` fallback, and positions (line/column).
+352 tests (AstFirst.Tests 299 + Generator.Tests 53). Covers lexer/DFA/LALR stages, end-to-end, error recovery (Corchuelo), GLR fork/dedup, semantic analysis (scopes, two-pass, type checking, ctx -> `ParseResult.Diagnostics` integration), `Accept`/`Reject` fallback, `OnAccepted` callback, and positions (line/column).
## License
diff --git a/docs/en/grammar-reference.md b/docs/en/grammar-reference.md
index a2b3115..649a3d2 100644
--- a/docs/en/grammar-reference.md
+++ b/docs/en/grammar-reference.md
@@ -28,6 +28,54 @@ public abstract partial class Expr : AstNode { }
The `Mode` named property switches dialects (see below).
+### ParseMode (parser execution mode)
+
+The `ParseMode` named property selects the parser execution mode. Default is `Lalr` (deterministic LALR(1)).
+
+| Value | Behavior |
+|---|---|
+| `ParseMode.Lalr` (default) | Deterministic LALR(1). Conflicts resolved by precedence/associativity; unresolved ones are warnings (ASTF001). |
+| `ParseMode.LightGlr` | Lightweight GLR. Forks in parallel at conflict cells, merges on convergence. Handles **inherent ambiguity** such as cast/paren or generic type/expression. Conflicts are resolved by forking, so no ASTF001. Result is a single AST (`ParseResult.Ast`); if multiple interpretations survive, observe via `ParseResult.AmbiguousCandidates`. |
+
+```csharp
+[Grammar(ParseMode = ParseMode.LightGlr)]
+```
+
+- **One mode per class**: `Lalr` and `LightGlr` cannot be specified simultaneously on the same `[Grammar]` root (choose one). Combinable with `Mode` (dialect).
+- **OnReduce constraint**: In LightGlr, `OnReduce` is invoked at reduce time even for undetermined branches. Therefore `OnReduce` (partial) must only set the node's own properties (`Name`/`Value`/`Span`, etc.) and must **not** mutate external state (`ScopedSymbolTable` / `DiagnosticBag`, etc.). Do semantic analysis in the second pass via `[Enter]`/`[Exit]` (Walker) to avoid leftover side effects from discarded branches.
+- **Error repair (Corchuelo et al. ER1/ER2/ER3) known limitations**:
+ - **Inserted tokens have null value**: Tokens inserted by ER1 have no value (the user did not write them). `OnReduce` that accesses `Token.Text` on such a token will throw `NullReferenceException`. SimulateForward (ER3) validates with real reduce + try/catch to reject candidates that throw, but fork divergence means zero risk is not guaranteed. Write `OnReduce` null-safe.
+ - **N=3 and costs are fixed**: Forward move symbols `N=3`, insert cost=1/delete cost=2 are hardcoded. The Corchuelo paper recommends per-language tuning; not yet supported.
+ - **Single-pass repair (no recursion)**: The original Corchuelo applies ER1/ER2/ER3 recursively; this implementation applies one round only. Consecutive errors are repaired one at a time on subsequent dead states.
+ - **SimulateForward checks the first path only**: Does not fork at conflict cells during simulation, so full agreement with production fork paths is not guaranteed.
+- **Error recovery behavior**: LightGlr's Corchuelo repair differs from the panic-mode recovery used in Lalr mode — it inserts/deletes tokens to continue parsing. The same input may produce different error positions/messages depending on the mode.
+
+### ⚠ Breaking Changes (0.4.0)
+
+The following changes are **not backward compatible**. Existing code must be updated.
+
+- **OnReduce ctx is read-only**: `OnReduce(MyCtx ctx)` → `OnReduce(SemanticContext ctx)`. The ctx type is always `SemanticContext` (base class). `OnAccepted` still receives the user's ctx type (writable).
+- **SemanticContext.Symbols is read-only**: Returns `IReadOnlySymbolTable` (`Lookup` only). `TryDeclare` / `PushScope` / `PopScope` are not available.
+- **SemanticContext.Diagnostics removed**: `Diagnostics` moved to `BasicSemanticContext`. `ctx.Diagnostics.Error(...)` in OnReduce is a compile error.
+- **Writes go in [Enter]/[Exit]**: Use `ctx.WritableSymbols.TryDeclare(...)` / `ctx.Diagnostics.Error(...)` inside `[Enter]`/`[Exit]` attribute methods (2nd-pass Walker).
+
+**Migration example**:
+```csharp
+// ❌ Before (0.3.0): declarations/diagnostics in OnReduce
+partial void OnReduce(MyCtx ctx)
+{
+ if (!ctx.Symbols.TryDeclare(...)) ctx.Diagnostics.Error(...);
+}
+
+// ✅ After (0.4.0): OnReduce for node-local only, declarations in [Enter]
+partial void OnReduce(SemanticContext ctx) { Name = ...; Span = ...; }
+// In [Grammar] root class:
+[Enter] static void Declare(MyNode n, BasicSemanticContext ctx)
+{
+ if (!ctx.WritableSymbols.TryDeclare(...)) ctx.Diagnostics.Error(...);
+}
+```
+
## `[Rule]`
Attach to a static method that defines a production. Multiple per class allowed. The body is empty (semantic actions go in `OnReduce`).
diff --git a/docs/ja/grammar-reference.md b/docs/ja/grammar-reference.md
index 75189d3..e2acadd 100644
--- a/docs/ja/grammar-reference.md
+++ b/docs/ja/grammar-reference.md
@@ -28,6 +28,54 @@ public abstract partial class Expr : AstNode { }
`Mode` 名前付きプロパティで複数方言を切り替えられる(後述)。
+### ParseMode(パーサの実行モード)
+
+`ParseMode` 名前付きプロパティでパーサの実行モードを切り替えられる。既定は `Lalr`(確定 LALR(1))。
+
+| 値 | 動作 |
+|---|---|
+| `ParseMode.Lalr`(既定) | 確定 LALR(1)。コンフリクトは優先度/結合性で解決し、解決不能分は警告 (ASTF001)。 |
+| `ParseMode.LightGlr` | 軽量 GLR。コンフリクトセルで並行 fork し、収束でマージ。cast/paren・generic の型/式など**本質的曖昧性**を扱える。コンフリクトは fork で解決されるため ASTF001 を出さない。結果は単一 AST(`ParseResult.Ast`)。複数解釈が残った場合は `ParseResult.AmbiguousCandidates` で観察可能。 |
+
+```csharp
+[Grammar(ParseMode = ParseMode.LightGlr)]
+```
+
+- **1 クラス 1 モード**: `Lalr` と `LightGlr` を同じ `[Grammar]` ルートに同時指定はできない(1 モード選択)。`Mode`(方言)とは併用可。
+- **OnReduce の制約**: LightGlr では未確定の分岐でも reduce 時に `OnReduce` が呼ばれる。そのため `OnReduce`(partial)は**ノード自身のプロパティ設定(`Name`/`Value`/`Span` 等)のみ**とし、外部の mutable 状態(`ScopedSymbolTable` / `DiagnosticBag` 等)の変更を行ってはならない。意味解析は 2 パス目の `[Enter]`/`[Exit]`(Walker)で行うこと(破棄された分岐の副作用が残るのを防ぐ)。
+- **エラー修復 (Corchuelo et al. ER1/ER2/ER3) の既知の制限**:
+ - **挿入トークンは値 null**: ER1 で補完されたトークンはユーザーが書いていないため値が `null`。これを子に持つノードの `OnReduce` で `Token.Text` 等を呼ぶと `NullReferenceException` になる。修復検証 (ER3 SimulateForward) で実 reduce を try/catch して例外を出す候補は弾くが、fork 差により本番で漏れる可能性がゼロではない。`OnReduce` は null 安全に書くことが望ましい。
+ - **N=3・コスト固定**: ER3 の Forward move 確認シンボル数 `N=3`、挿入コスト=1/削除コスト=2 は固定値。Corchuelo 論文では言語に応じたチューニングを推奨しているが、本実装では未対応。
+ - **1 回修復 (再帰なし)**: Corchuelo 本来は ER1/ER2/ER3 を再帰的に適用するが、本実装は1回の ER1/ER2 + ER3 のみ。連続エラーは次の dead で順次修復される。
+ - **SimulateForward は最初の経路のみ確認**: コンフリクトセルでも fork せず最初の shift/reduce のみ追うため、本番の fork 経路との完全一致は保証しない。
+- **エラー回復の挙動**: LightGlr の Corchuelo 修復は、LALR モードの panic mode 回復とは異なり、トークンを補完/削除してパースを続行する。同じ入力でもモードによりエラー位置・メッセージが変わる場合がある。
+
+### ⚠ バージョン互換性のない変更 (0.4.0)
+
+以下の変更は**後方互換性がありません**。既存コードの修正が必要です。
+
+- **OnReduce の ctx が読み取り専用**: `OnReduce(MyCtx ctx)` → `OnReduce(SemanticContext ctx)`。ctx の型が常に `SemanticContext` (基底) になります。`OnAccepted` は引き続きユーザーの ctx 型 (書き込み可) を受け取ります。
+- **SemanticContext.Symbols が読み取り専用**: `IReadOnlySymbolTable` を返す (`Lookup` のみ)。`TryDeclare` / `PushScope` / `PopScope` は不可。
+- **SemanticContext.Diagnostics が削除**: `Diagnostics` は `BasicSemanticContext` に移動。OnReduce で `ctx.Diagnostics.Error(...)` はコンパイルエラーになります。
+- **書き込みは [Enter]/[Exit] で**: `ctx.WritableSymbols.TryDeclare(...)` / `ctx.Diagnostics.Error(...)` は `[Enter]`/`[Exit]` 属性メソッド (2パス目 Walker) 内で行う。
+
+**移行例**:
+```csharp
+// ❌ 前 (0.3.0): OnReduce で宣言・診断
+partial void OnReduce(MyCtx ctx)
+{
+ if (!ctx.Symbols.TryDeclare(...)) ctx.Diagnostics.Error(...);
+}
+
+// ✅ 後 (0.4.0): OnReduce はノードローカルのみ、宣言は [Enter] で
+partial void OnReduce(SemanticContext ctx) { Name = ...; Span = ...; }
+// [Grammar] ルートクラスで:
+[Enter] static void Declare(MyNode n, BasicSemanticContext ctx)
+{
+ if (!ctx.WritableSymbols.TryDeclare(...)) ctx.Diagnostics.Error(...);
+}
+```
+
## `[Rule]`
生成規則を定義する static メソッドに付ける。1クラスに複数置ける。本体は空(意味アクションは `OnReduce` に書く)。
diff --git a/samples/CSharpParser/GeneratedGrammar.cs b/samples/CSharpParser/GeneratedGrammar.cs
index 0463416..3c53a32 100644
--- a/samples/CSharpParser/GeneratedGrammar.cs
+++ b/samples/CSharpParser/GeneratedGrammar.cs
@@ -3,7 +3,7 @@
namespace CSharpParser;
-[Grammar]
+[Grammar(ParseMode = ParseMode.LightGlr)]
[Skip(@"(\s|//[^\n]*)+")]
public abstract partial class CSharpCompilationUnit : AstNode { }
diff --git a/samples/MiniC/MiniCGrammar.cs b/samples/MiniC/MiniCGrammar.cs
index 176f1e0..e514a21 100644
--- a/samples/MiniC/MiniCGrammar.cs
+++ b/samples/MiniC/MiniCGrammar.cs
@@ -20,8 +20,8 @@ public abstract partial class Program : AstNode
[Exit] public static void ExitAssign(AssignStmt n, MiniCContext ctx) => SemanticAnalyzer.ExitAssign(n, ctx);
[Exit] public static void ExitIf(IfStmt n, MiniCContext ctx) => SemanticAnalyzer.ExitCondition(n.Cond, n.Cond.Span, "if", ctx);
[Exit] public static void ExitWhile(WhileStmt n, MiniCContext ctx) => SemanticAnalyzer.ExitCondition(n.Cond, n.Cond.Span, "while", ctx);
- [Enter] public static void EnterBlock(BlockStmt n, MiniCContext ctx) => ctx.Symbols.PushScope();
- [Exit] public static void ExitBlock(BlockStmt n, MiniCContext ctx) => ctx.Symbols.PopScope();
+ [Enter] public static void EnterBlock(BlockStmt n, MiniCContext ctx) => ctx.WritableSymbols.PushScope();
+ [Exit] public static void ExitBlock(BlockStmt n, MiniCContext ctx) => ctx.WritableSymbols.PopScope();
[Enter] public static void EnterVar(VarExpr n, MiniCContext ctx) => SemanticAnalyzer.EnterVar(n, ctx);
[Exit] public static void ExitNum(NumExpr n, MiniCContext ctx) => SemanticAnalyzer.SetType(n, SemanticAnalyzer.Int, ctx);
[Exit] public static void ExitBool(BoolExpr n, MiniCContext ctx) => SemanticAnalyzer.SetType(n, SemanticAnalyzer.Bool, ctx);
@@ -48,7 +48,7 @@ public sealed partial class DeclStmt : Stmt
public string Name { get; private set; } = "";
[Rule]
public static void Decl([Token(@"int", Priority = 1)] Token kw, [Token(@"[A-Za-z_]\w*")] Token nameTok, [Token(@";")] Token semi, MiniCContext ctx) { }
- partial void OnReduce(MiniCContext ctx) { Name = NameTok.Text; Span = NameTok.Span; }
+ partial void OnReduce(SemanticContext ctx) { Name = NameTok.Text; Span = NameTok.Span; }
}
// int x = expr; (初期化付き)
@@ -57,7 +57,7 @@ public sealed partial class DeclStmtInit : Stmt
public string Name { get; private set; } = "";
[Rule]
public static void DeclInit([Token(@"int", Priority = 1)] Token kw, [Token(@"[A-Za-z_]\w*")] Token nameTok, [Token(@"=")] Token eq, Expr init, [Token(@";")] Token semi, MiniCContext ctx) { }
- partial void OnReduce(MiniCContext ctx) { Name = NameTok.Text; Span = NameTok.Span; }
+ partial void OnReduce(SemanticContext ctx) { Name = NameTok.Text; Span = NameTok.Span; }
}
// x = expr;
@@ -66,7 +66,7 @@ public sealed partial class AssignStmt : Stmt
public string Name { get; private set; } = "";
[Rule]
public static void Assign([Token(@"[A-Za-z_]\w*")] Token nameTok, [Token(@"=")] Token eq, Expr value, [Token(@";")] Token semi, MiniCContext ctx) { }
- partial void OnReduce(MiniCContext ctx) { Name = NameTok.Text; Span = NameTok.Span; }
+ partial void OnReduce(SemanticContext ctx) { Name = NameTok.Text; Span = NameTok.Span; }
}
// print(expr);
@@ -95,7 +95,7 @@ public sealed partial class BlockStmt : Stmt
{
[Rule]
public static void Block([Token(@"\{")] Token lb, [Repeat(Min = 0)] Stmt statements, [Token(@"\}")] Token rb, MiniCContext ctx) { }
- partial void OnReduce(MiniCContext ctx) { Span = SourceSpan.Merge(Lb.Span, Rb.Span); }
+ partial void OnReduce(SemanticContext ctx) { Span = SourceSpan.Merge(Lb.Span, Rb.Span); }
}
// --- 式 ---
@@ -106,7 +106,7 @@ public sealed partial class NumExpr : Expr
public int Value { get; private set; }
[Rule]
public static void NumToken([Token(@"[0-9]+")] Token num, MiniCContext ctx) { }
- partial void OnReduce(MiniCContext ctx) { Value = int.Parse(Num.Text); Span = Num.Span; }
+ partial void OnReduce(SemanticContext ctx) { Value = int.Parse(Num.Text); Span = Num.Span; }
}
public sealed partial class BoolExpr : Expr
@@ -114,7 +114,7 @@ public sealed partial class BoolExpr : Expr
public bool Value { get; private set; }
[Rule]
public static void Bool([Token(@"true|false", Priority = 1)] Token kw, MiniCContext ctx) { }
- partial void OnReduce(MiniCContext ctx) { Value = Kw.Text == "true"; Span = Kw.Span; }
+ partial void OnReduce(SemanticContext ctx) { Value = Kw.Text == "true"; Span = Kw.Span; }
}
public sealed partial class VarExpr : Expr
@@ -122,7 +122,7 @@ public sealed partial class VarExpr : Expr
public string Name { get; private set; } = "";
[Rule]
public static void Var([Token(@"[A-Za-z_]\w*")] Token nameTok, MiniCContext ctx) { }
- partial void OnReduce(MiniCContext ctx) { Name = NameTok.Text; Span = NameTok.Span; }
+ partial void OnReduce(SemanticContext ctx) { Name = NameTok.Text; Span = NameTok.Span; }
}
[Precedence(1)]
diff --git a/samples/MiniC/SemanticAnalyzer.cs b/samples/MiniC/SemanticAnalyzer.cs
index a0b3420..11b0655 100644
--- a/samples/MiniC/SemanticAnalyzer.cs
+++ b/samples/MiniC/SemanticAnalyzer.cs
@@ -15,12 +15,12 @@ public static class SemanticAnalyzer
// --- 宣言: int x; / int x = expr; ---
public static void EnterDecl(DeclStmt n, MiniCContext ctx)
{
- if (!ctx.Symbols.TryDeclare(n.Name, n.Span, null, out _))
+ if (!ctx.WritableSymbols.TryDeclare(n.Name, n.Span, null, out _))
ctx.Diagnostics.Error($"変数 '{n.Name}' は既に宣言されています", n.Span);
}
public static void EnterDeclInit(DeclStmtInit n, MiniCContext ctx)
{
- if (!ctx.Symbols.TryDeclare(n.Name, n.Span, null, out _))
+ if (!ctx.WritableSymbols.TryDeclare(n.Name, n.Span, null, out _))
ctx.Diagnostics.Error($"変数 '{n.Name}' は既に宣言されています", n.Span);
}
public static void ExitDeclInit(DeclStmtInit n, MiniCContext ctx)
@@ -29,7 +29,7 @@ public static void ExitDeclInit(DeclStmtInit n, MiniCContext ctx)
// --- 代入 ---
public static void EnterAssign(AssignStmt n, MiniCContext ctx)
{
- var sym = ctx.Symbols.ResolveOrError(n.Name, n.Span, ctx.Diagnostics);
+ var sym = ctx.WritableSymbols.ResolveOrError(n.Name, n.Span, ctx.Diagnostics);
if (sym is not null) n.SetAnnotation("symbol", sym); // 束縛: ノードにシンボルを紐付け
}
public static void ExitAssign(AssignStmt n, MiniCContext ctx)
@@ -38,7 +38,7 @@ public static void ExitAssign(AssignStmt n, MiniCContext ctx)
// --- 変数参照 ---
public static void EnterVar(VarExpr n, MiniCContext ctx)
{
- var sym = ctx.Symbols.ResolveOrError(n.Name, n.Span, ctx.Diagnostics);
+ var sym = ctx.WritableSymbols.ResolveOrError(n.Name, n.Span, ctx.Diagnostics);
if (sym is not null)
{
n.SetAnnotation("symbol", sym); // 束縛
diff --git a/samples/Perf/Perf.CSharp/GeneratedGrammar.cs b/samples/Perf/Perf.CSharp/GeneratedGrammar.cs
index 0463416..3c53a32 100644
--- a/samples/Perf/Perf.CSharp/GeneratedGrammar.cs
+++ b/samples/Perf/Perf.CSharp/GeneratedGrammar.cs
@@ -3,7 +3,7 @@
namespace CSharpParser;
-[Grammar]
+[Grammar(ParseMode = ParseMode.LightGlr)]
[Skip(@"(\s|//[^\n]*)+")]
public abstract partial class CSharpCompilationUnit : AstNode { }
diff --git a/samples/Perf/Perf.Grammars/CSharpFactory.cs b/samples/Perf/Perf.Grammars/CSharpFactory.cs
index 6a4f505..cf92866 100644
--- a/samples/Perf/Perf.Grammars/CSharpFactory.cs
+++ b/samples/Perf/Perf.Grammars/CSharpFactory.cs
@@ -30,6 +30,7 @@ public static class CSharpFactory
public static GrammarSpec Create()
{
var spec = new GrammarSpec(Namespace, Root, skipRegex: @"(\s|//[^\n]*)+");
+ spec.ParseMode = "LightGlr"; // C# の型/式 (cast/paren, generic) の本質的曖昧性を GLR で解決
spec.AddAbstract(Root, "AstNode");
AddLexical(spec);
AddTypes(spec);
diff --git a/samples/Perf/Perf.Grammars/GrammarSpec.cs b/samples/Perf/Perf.Grammars/GrammarSpec.cs
index 43d7b92..51a9f8b 100644
--- a/samples/Perf/Perf.Grammars/GrammarSpec.cs
+++ b/samples/Perf/Perf.Grammars/GrammarSpec.cs
@@ -17,6 +17,8 @@ public sealed class GrammarSpec
/// csSource の [Skip(@"...")] に埋め込む正規表現 (verbatim 内なのでそのまま)。
public string SkipRegex { get; }
public List Nodes { get; } = new();
+ /// ParseMode 名 ("LightGlr" 等)。null/空なら既定 (Lalr)。ToCsSource で [Grammar(ParseMode = ParseMode.X)] に出力。
+ public string? ParseMode { get; set; }
public GrammarSpec(string ns, string rootClass, string skipRegex)
{
@@ -50,9 +52,9 @@ public string ToCsSource()
sb.AppendLine();
// ルート (開始記号)
- sb.AppendLine("[Grammar]");
+ sb.AppendLine(string.IsNullOrEmpty(ParseMode) ? "[Grammar]" : "[Grammar(ParseMode = ParseMode." + ParseMode + ")]");
sb.AppendLine("[Skip(@\"" + SkipRegex + "\")]");
- sb.AppendLine("public abstract class " + RootClass + " : AstNode { }");
+ sb.AppendLine("public abstract partial class " + RootClass + " : AstNode { }");
sb.AppendLine();
foreach (var n in Nodes)
@@ -60,7 +62,7 @@ public string ToCsSource()
if (n.ClassName == RootClass) continue; // ルートは上で出力済み
if (n.IsAbstract)
{
- sb.AppendLine("public abstract class " + n.ClassName + " : " + n.BaseClass + " { }");
+ sb.AppendLine("public abstract partial class " + n.ClassName + " : " + n.BaseClass + " { }");
sb.AppendLine();
continue;
}
@@ -72,7 +74,7 @@ public string ToCsSource()
p += ")]";
sb.AppendLine(p);
}
- sb.AppendLine("public sealed class " + n.ClassName + " : " + n.BaseClass);
+ sb.AppendLine("public sealed partial class " + n.ClassName + " : " + n.BaseClass);
sb.AppendLine("{");
foreach (var ctor in n.Ctors)
{
@@ -88,7 +90,8 @@ public string ToCsSource()
}
ps.Add(attr + p.CsType + " " + p.Name);
}
- sb.AppendLine(" public " + n.ClassName + "(" + string.Join(", ", ps) + ") { }");
+ sb.AppendLine(" [Rule]");
+ sb.AppendLine(" public static void Reduce(" + string.Join(", ", ps) + ") { }");
}
sb.AppendLine("}");
sb.AppendLine();
diff --git a/samples/Perf/PerfSummary.md b/samples/Perf/PerfSummary.md
index fc89cd1..e277a8a 100644
--- a/samples/Perf/PerfSummary.md
+++ b/samples/Perf/PerfSummary.md
@@ -9,12 +9,12 @@
| パターン | LALR状態数 | シンボル数 | 生成コード(byte) | 生成コード(行) | ビルド時間(ms) |
|---|---:|---:|---:|---:|---:|
-| DeepPrec | 44 | 45 | 58214 | 624 | 3007 |
-| WideRules | 205 | 207 | 601219 | 2321 | 3447 |
-| ManyTokens | 205 | 206 | 599511 | 2322 | 3660 |
-| DeepNest | 7 | 8 | 11454 | 230 | 3101 |
-| MegaLang | 121 | 119 | 252995 | 1406 | 3315 |
-| CSharp | 798 | 608 | 6023011 | 8011 | 10969 |
+| DeepPrec | 44 | 45 | 81280 | 1339 | 5888 |
+| WideRules | 205 | 207 | 613200 | 3545 | 5455 |
+| ManyTokens | 205 | 206 | 615657 | 3546 | 5169 |
+| DeepNest | 7 | 8 | 12352 | 260 | 3346 |
+| MegaLang | 121 | 119 | 276789 | 2486 | 3443 |
+| CSharp | 798 | 608 | 6183596 | 15363 | 11694 |
## 実行パフォーマンス(BenchmarkDotNet、大規模テスト)
@@ -27,18 +27,20 @@ C# 完全文法(365 規則 / 798 状態)を最大規模のストレステス
| 文法 | 規則数 | Build | Parse | Parse Allocated |
|---|---:|---:|---:|---:|
-| DeepPrec | 22 | 2.75 ms | 1.94 ms | 4.7 MB |
-| WideRules | 103 | 2.71 ms | 2.14 ms | 4.7 MB |
-| ManyTokens | 103 | 73.0 ms | 1.14 ms | 2.5 MB |
-| DeepNest | 3 | 0.011 ms | 0.104 ms | 338 KB |
-| MegaLang | 58 | 4.02 ms | 0.193 ms | 401 KB |
-| **CSharp** | **365** | **190 ms** | **0.68 ms** | **627 KB** |
+| DeepPrec | 22 | 2.75 ms | 2.3 ms | 4.5 MB |
+| WideRules | 103 | 2.71 ms | — | — |
+| ManyTokens | 103 | 73.0 ms | — | — |
+| DeepNest | 3 | 0.011 ms | 0.17 ms | 376 KB |
+| MegaLang | 58 | 4.02 ms | 0.40 ms | 454 KB |
+| **CSharp (LightGlr)** | **365** | **190 ms** | **11.4 ms** | **1,931 KB** |
+
+> 上記は `dotnet run -c Release -- direct` (Stopwatch + GC 直接計測、warmup 5 回後 1 回) の値。WideRules / ManyTokens は未再計測 (—)。BenchmarkDotNet による精密計測は Windows Defender 適用除外設定後に再実行すること。
### 所見
-- **Parse_CSharp 0.68 ms**(50 クラス ≈ 7KB 入力、≈ 10 MB/s)。[Rule] モデル移行後(Reject/TryFallback + Span スタック)。スタックを Span でラップし境界チェックを最適化。
-- **Build_CSharp 190 ms** は `LalrLookahead` の不動点反復を Worklist アルゴリズムで最適化(798状態×365規則)。WideRules(103 規則)2.7 ms → CSharp(365 規則)190 ms と非線形に増大。ただし Generator 実行時のみの1回コストで、実行時ではない。
-- **Parse のアロケーション 627 KB**(入力の ≈90 倍)は AST 構築(reduce ごとにノードを `new`)。仕組み上(AST は結果として返るためオブジェクト寿命 = AST 寿命)抑制が困難。詳細は README「C# 完全文法ベンチマーク」章。
+- **Parse_CSharp 11.4 ms**(50 クラス ≈ 7KB 入力、LightGlr モード)。state 308 で識別子ごとに fork するため LALR 単一スタック (旧 0.68 ms) より低速。fast path (単一スタック時 List/Queue/HashSet バイパス) で 84.8 ms → 11.4 ms に最適化済み。
+- **LALR モードの文法** (DeepPrec / MegaLang / DeepNest) は単一スタック fast path で動作。旧値の 2〜3 倍 (COW リスト + NotifyAccepted のオーバーヘッド)。
+- **Build_CSharp 190 ms** はテーブル構築時間 (LALR 共通)。LightGlr でも同じ LALR テーブルを使用するため不変。
### 計測環境
@@ -63,4 +65,4 @@ Windows Defender が BenchmarkDotNet のベンチ子プロセスを遮断する
| DeepPrec | 4385 KB | |
※ 時間は 1 回計測のため JIT/GC ノイズが大きく、BenchmarkDotNet(上記「実行パフォーマンス」表)ほど正確ではない。
- 正確な回帰値は Windows Defender 適用除外設定後、BenchmarkDotNet で再計測すること。
+ 正確な回帰値は Windows Defender 適用除外設定後、BenchmarkDotNet で再計測すること。
\ No newline at end of file
diff --git a/src/AstFirst.Generator/AstFirst.Generator.csproj b/src/AstFirst.Generator/AstFirst.Generator.csproj
index d0becf4..b9eff73 100644
--- a/src/AstFirst.Generator/AstFirst.Generator.csproj
+++ b/src/AstFirst.Generator/AstFirst.Generator.csproj
@@ -46,4 +46,8 @@
+
+
+
+
diff --git a/src/AstFirst.Generator/GlrParserEmitter.cs b/src/AstFirst.Generator/GlrParserEmitter.cs
new file mode 100644
index 0000000..ebd1c26
--- /dev/null
+++ b/src/AstFirst.Generator/GlrParserEmitter.cs
@@ -0,0 +1,267 @@
+using System.Collections.Generic;
+using System.Text;
+using AstFirst.Core.Lexing;
+using AstFirst.Core.Parsing;
+
+namespace AstFirst.Generator;
+
+///
+/// LightGlr モード専用のパーサコード生成エミッタ。
+/// と同じテーブル直列化 (LALR(1) 表を static 配列に) を行いつつ、
+/// Parse 本体は AstFirst.Glr.LightGlrDriver に委譲する (コンフリクトセルで並行 fork)。
+/// reduce は object?[] children を受け取り COW (copy-on-write) でリストを構築 (GSS での共有安全)。
+/// partial ノード生成は をそのまま流用 (OnReduce は reduce 時呼び出し)。
+///
+internal static class GlrParserEmitter
+{
+ public static string EmitParser(GrammarModel model, Grammar grammar, LalrTable table, IReadOnlyList rules, string ns)
+ {
+ int stateCount = table.StateCount;
+ int symbolCount = table.SymbolCount;
+ int prodCount = grammar.Productions.Count;
+ var (typeName, lexerName, parserName) = Names(model);
+
+ // 終端 Symbol.Id と LexerRule.TokenId を突き合わせ (ParserEmitter と同一)。
+ var patternToTerminalId = new Dictionary();
+ foreach (var sym in grammar.Symbols)
+ {
+ if (!sym.IsTerminal) continue;
+ if (sym.Name.StartsWith("token:"))
+ patternToTerminalId[sym.Name.Substring("token:".Length)] = sym.Id;
+ }
+
+ var tokenDerivedTypes = new HashSet();
+ foreach (var td in model.TokenDefs)
+ if (td.Key != "AstFirst.Token") tokenDerivedTypes.Add(td.Key);
+ int maxTokenId = 0;
+ foreach (var r in rules) if (r.TokenId > maxTokenId) maxTokenId = r.TokenId;
+ var tokenIdToSym = new int[maxTokenId + 1];
+ for (int i = 0; i < tokenIdToSym.Length; i++) tokenIdToSym[i] = -1;
+ foreach (var r in rules)
+ if (patternToTerminalId.TryGetValue(r.Pattern, out var sid))
+ tokenIdToSym[r.TokenId] = sid;
+
+ int eofSym = grammar.EndOfFile.Id;
+
+ // コンフリクトセルの全候補を収集 (ParserEmitter と同一)。
+ var altKeys = new List();
+ var altActs = new List>();
+ for (int s = 0; s < stateCount; s++)
+ for (int c = 0; c < symbolCount; c++)
+ {
+ var alts = table.Alternatives(s, c);
+ if (alts.Count < 2) continue;
+ var list = new List();
+ for (int k = 1; k < alts.Count; k++) list.Add(EncodeAction(alts[k]));
+ if (list.Count == 0) continue;
+ altKeys.Add(s * symbolCount + c);
+ altActs.Add(list);
+ }
+
+ var sb = new StringBuilder();
+ sb.AppendLine("// ");
+ sb.AppendLine("#nullable enable");
+ sb.AppendLine("using System;");
+ sb.AppendLine("using System.Collections.Generic;");
+ sb.AppendLine("using AstFirst.Core.Lexing;");
+ if (ns.Length > 0) { sb.AppendLine("namespace " + ns + ";"); sb.AppendLine(); }
+ sb.AppendLine("public static class " + parserName);
+ sb.AppendLine("{");
+ if (model.HasSecondPass)
+ {
+ var walkerSuffix = string.IsNullOrEmpty(model.Mode) ? "" : "_" + model.Mode;
+ var (_, rootType) = CodeEmitter.SplitFullName(model.RootTypeFullName);
+ var walkerName = rootType + walkerSuffix + "Walker";
+ sb.AppendLine(" private static readonly " + walkerName + " __defaultWalker = new " + walkerName + "._Default();");
+ }
+
+ EmitMatrixByte(sb, "ActionKind", stateCount, symbolCount, s => c => ActionKindByte(table.Action(s, c)));
+ EmitMatrixIntRaw(sb, "ActionValue", stateCount, symbolCount, s => c => table.Action(s, c).Value);
+ EmitMatrixIntRaw(sb, "Goto", stateCount, symbolCount, s => c => table.Goto(s, c));
+
+ sb.Append(" public static readonly int[] ProdLhs = new int[] { ");
+ for (int p = 0; p < prodCount; p++) { if (p > 0) sb.Append(", "); sb.Append(grammar.Productions[p].Lhs.Id); }
+ sb.AppendLine(" };");
+ sb.Append(" public static readonly int[] ProdLen = new int[] { ");
+ for (int p = 0; p < prodCount; p++) { if (p > 0) sb.Append(", "); sb.Append(grammar.Productions[p].Rhs.Length); }
+ sb.AppendLine(" };");
+
+ sb.Append(" public static readonly int[] TokenIdToSym = new int[] { ");
+ for (int i = 0; i < tokenIdToSym.Length; i++) { if (i > 0) sb.Append(", "); sb.Append(tokenIdToSym[i]); }
+ sb.AppendLine(" };");
+
+ sb.AppendLine(" public const int EofSym = " + eofSym + ";");
+ sb.AppendLine(" public const int StateCount = " + stateCount + ";");
+ sb.AppendLine(" public const int SymbolCount = " + symbolCount + ";");
+
+ sb.Append(" public static readonly string?[] SymNames = new string?[] { ");
+ for (int si = 0; si < symbolCount; si++)
+ {
+ if (si > 0) sb.Append(", ");
+ var escaped = grammar.Symbols[si].Name.Replace("\\", "\\\\").Replace("\"", "\\\"");
+ sb.Append("\"").Append(escaped).Append("\"");
+ }
+ sb.AppendLine(" };");
+
+ var dr = table.DefaultReduceTable;
+ sb.Append(" public static readonly int[] DefaultReduce = new int[] { ");
+ for (int s = 0; s < stateCount; s++) { if (s > 0) sb.Append(", "); sb.Append(s < dr.Count ? dr[s] : -1); }
+ sb.AppendLine(" };");
+
+ sb.Append(" public static readonly int[] AltKeys = new int[] { ");
+ for (int i = 0; i < altKeys.Count; i++) { if (i > 0) sb.Append(", "); sb.Append(altKeys[i]); }
+ sb.AppendLine(" };");
+ sb.AppendLine(" public static readonly int[][] AltActs = new int[" + altActs.Count + "][]");
+ sb.AppendLine(" {");
+ for (int i = 0; i < altActs.Count; i++)
+ {
+ sb.Append(" new int[] { ");
+ for (int j = 0; j < altActs[i].Count; j++) { if (j > 0) sb.Append(", "); sb.Append(altActs[i][j]); }
+ sb.Append(" }");
+ sb.AppendLine(i < altActs.Count - 1 ? "," : "");
+ }
+ sb.AppendLine(" };");
+
+ EmitGlrParse(sb, lexerName, model);
+ EmitGlrHelpers(sb, grammar, model, tokenDerivedTypes);
+
+ sb.AppendLine("}");
+ return sb.ToString();
+ }
+
+ public static string EmitPartial(GrammarModel model, NodeModel node, string ns)
+ => ParserEmitter.EmitPartial(model, node, ns);
+
+ private static void EmitGlrParse(StringBuilder sb, string lexerName, GrammarModel model)
+ {
+ sb.AppendLine(" public static AstFirst.ParseResult Parse(string input) => Parse(input, null);");
+ sb.AppendLine(" public static AstFirst.ParseResult Parse(string input, AstFirst.SemanticContext? context)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" var ctx = (context as AstFirst.BasicSemanticContext) ?? new AstFirst.BasicSemanticContext();");
+ sb.AppendLine(" var tokens = " + lexerName + ".Tokenize(input);");
+ sb.AppendLine(" var __tables = new AstFirst.Glr.GlrTables(ActionKind, ActionValue, Goto, ProdLhs, ProdLen, DefaultReduce, TokenIdToSym, AltKeys, AltActs, StateCount, SymbolCount, EofSym, 0, SymNames);");
+ sb.AppendLine(" var __r = AstFirst.Glr.LightGlrDriver.Run(__tables, tokens, ctx, __ReduceNode, __ToToken);");
+ sb.AppendLine(" object? result = __r.Candidates.Count > 0 ? __r.Candidates[0] : null;");
+ if (model.HasSecondPass)
+ sb.AppendLine(" if (result is AstFirst.AstNode __root) __defaultWalker.Walk(__root, ctx);");
+ sb.AppendLine(" return new AstFirst.ParseResult(result, __r.Errors, ctx.Diagnostics.Items, __r.Candidates.Count > 1 ? __r.Candidates : null);");
+ sb.AppendLine(" }");
+ }
+
+ private static void EmitGlrHelpers(StringBuilder sb, Grammar grammar, GrammarModel model, HashSet tokenDerivedTypes)
+ {
+ // __ReduceNode: 規則 prodId で reduce。children[i] (右辺 i 番目) を参照 → partial コンストラクタ new。
+ // ListReduceActionModel の再帰ケースは COW (copy-on-write): 共有スタックで破壊しないよう新リストを構築。
+ sb.AppendLine(" private static object? __ReduceNode(int val, object?[] children, AstFirst.SemanticContext ctx)");
+ sb.AppendLine(" {");
+ sb.AppendLine(" switch (val)");
+ sb.AppendLine(" {");
+ foreach (var prod in grammar.Productions)
+ {
+ switch (prod.Tag)
+ {
+ case ReduceActionModel action:
+ {
+ sb.Append(" case ").Append(prod.Id).Append(": { return new ").Append(action.AstTypeName).Append("(\"").Append(action.RuleName).Append("\"");
+ for (int j = 0; j < action.Parameters.Count; j++)
+ {
+ sb.Append(", ");
+ var p = action.Parameters[j];
+ if (p.IsContext) sb.Append("(").Append(p.CastTypeName).Append(")ctx");
+ else if (tokenDerivedTypes.Contains(p.CastTypeName)) sb.Append("new ").Append(p.CastTypeName).Append("(((AstFirst.Token)children[").Append(p.ChildIndex).Append("]!).Text)");
+ else sb.Append("(").Append(p.CastTypeName).Append(")children[").Append(p.ChildIndex).Append("]!");
+ }
+ sb.AppendLine("); }");
+ break;
+ }
+ case ListReduceActionModel listAction:
+ {
+ string elemType = listAction.ElementType;
+ string listType = "System.Collections.Generic.List<" + elemType + ">";
+ sb.Append(" case ").Append(prod.Id).Append(": { ");
+ if (listAction.IsRecursive)
+ {
+ // List_T → List_T item: COW。既存リストをコピーして末尾に item を add (共有スタックで安全)。
+ sb.Append("var __src = (").Append(listType).Append(")children[0]!; ");
+ sb.Append("var __list = new ").Append(listType).Append("(__src.Count + 1); ");
+ sb.Append("foreach (var __x in __src) __list.Add(__x); ");
+ sb.Append("__list.Add((").Append(elemType).Append(")children[1]!); ");
+ }
+ else if (listAction.IsEmpty)
+ {
+ sb.Append("var __list = new ").Append(listType).Append("(0); ");
+ }
+ else
+ {
+ sb.Append("var __list = new ").Append(listType).Append("(4); ");
+ sb.Append("__list.Add((").Append(elemType).Append(")children[0]!); ");
+ }
+ sb.AppendLine("return __list; }");
+ break;
+ }
+ case PassThroughActionModel:
+ {
+ sb.Append(" case ").Append(prod.Id).Append(": { return children[0]!; }").AppendLine();
+ break;
+ }
+ }
+ }
+ sb.AppendLine(" default: return null;");
+ sb.AppendLine(" }");
+ sb.AppendLine(" }");
+
+ sb.AppendLine(" private static AstFirst.Token __ToToken(AstFirst.Core.Lexing.LexToken t)");
+ sb.AppendLine(" => new AstFirst.BasicToken(t.Span, new AstFirst.SourceSpan(new AstFirst.Position(t.Start, t.StartLine, t.StartColumn), new AstFirst.Position(t.End, t.EndLine, t.EndColumn)));");
+ }
+
+ private static int EncodeAction(LrAction a)
+ {
+ int k = a.Kind switch { LrActionKind.Shift => 1, LrActionKind.Reduce => 2, LrActionKind.Accept => 3, _ => 0 };
+ return k * 1000000 + (a.Value < 0 ? 0 : a.Value);
+ }
+
+ private static byte ActionKindByte(LrAction a) => a.Kind switch
+ {
+ LrActionKind.Shift => 1,
+ LrActionKind.Reduce => 2,
+ LrActionKind.Accept => 3,
+ _ => 0
+ };
+
+ private static (string type, string lexer, string parser) Names(GrammarModel model)
+ {
+ var (ns, type) = CodeEmitter.SplitFullName(model.RootTypeFullName);
+ var suffix = string.IsNullOrEmpty(model.Mode) ? "" : "_" + model.Mode;
+ return (type + suffix, type + suffix + "Lexer", type + suffix + "Parser");
+ }
+
+ private static void EmitMatrixByte(StringBuilder sb, string name, int rows, int cols,
+ System.Func> cell)
+ {
+ sb.Append(" public static readonly byte[] ").Append(name).Append(" = new byte[").Append(rows * cols).AppendLine("]");
+ sb.AppendLine(" {");
+ for (int r = 0; r < rows; r++)
+ {
+ var row = cell(r);
+ sb.Append(" ");
+ for (int c = 0; c < cols; c++) { if (c > 0) sb.Append(", "); sb.Append(row(c)); }
+ sb.AppendLine(r < rows - 1 ? "," : "");
+ }
+ sb.AppendLine(" };");
+ }
+
+ private static void EmitMatrixIntRaw(StringBuilder sb, string name, int rows, int cols,
+ System.Func> cell)
+ {
+ sb.Append(" public static readonly int[] ").Append(name).Append(" = new int[").Append(rows * cols).AppendLine("]");
+ sb.AppendLine(" {");
+ for (int r = 0; r < rows; r++)
+ {
+ var row = cell(r);
+ sb.Append(" ");
+ for (int c = 0; c < cols; c++) { if (c > 0) sb.Append(", "); sb.Append(row(c)); }
+ sb.AppendLine(r < rows - 1 ? "," : "");
+ }
+ sb.AppendLine(" };");
+ }
+}
diff --git a/src/AstFirst.Generator/ModelExtraction.cs b/src/AstFirst.Generator/ModelExtraction.cs
index ae30457..3112b0f 100644
--- a/src/AstFirst.Generator/ModelExtraction.cs
+++ b/src/AstFirst.Generator/ModelExtraction.cs
@@ -68,17 +68,22 @@ public static GrammarModel Extract(Compilation compilation, INamedTypeSymbol roo
if (a.AttributeClass?.Name == "SkipAttribute" && a.ConstructorArguments.Length > 0 && a.ConstructorArguments[0].Value is string ss)
skipPatterns.Add(ss);
- // [Grammar(Mode = "...")] の Mode を取得。
+ // [Grammar(Mode = "...")] の Mode と [Grammar(ParseMode = ...)] の ParseMode を取得。
+ // ParseMode は enum だが Generator は Runtime を直接参照しないため整数値として読む。
string? mode = null;
+ var parseMode = ParseMode.Lalr;
foreach (var a in rootType.GetAttributes())
if (a.AttributeClass?.Name == "GrammarAttribute")
foreach (var na in a.NamedArguments)
+ {
if (na.Key == "Mode" && na.Value.Value is string m) mode = m;
+ if (na.Key == "ParseMode" && na.Value.Value is int pm) parseMode = (ParseMode)pm;
+ }
// [OnReduce]/[Enter]/[Exit] 属性付き意味解析ルール ([Grammar] ルートクラスの static メソッド) を収集。
var analyzeRules = ExtractAnalyzeRules(rootType, astNodeBase, contextBase);
- return new GrammarModel(rootType.ToDisplayString(), nodes, Dedup(tokenDefs), skipPatterns, mode, rootLocation, tokenDerivedWarnings, analyzeRules);
+ return new GrammarModel(rootType.ToDisplayString(), nodes, Dedup(tokenDefs), skipPatterns, mode, rootLocation, tokenDerivedWarnings, analyzeRules, parseMode);
}
/// [OnReduce]/[Enter]/[Exit] 属性付き意味解析ルール ([Grammar] ルートクラスの static メソッド) を収集。
diff --git a/src/AstFirst.Generator/Models.cs b/src/AstFirst.Generator/Models.cs
index 5109dfc..03d5ed8 100644
--- a/src/AstFirst.Generator/Models.cs
+++ b/src/AstFirst.Generator/Models.cs
@@ -5,12 +5,23 @@
namespace AstFirst.Generator;
+/// パーザの実行モード (Generator 用。Runtime の AstFirst.ParseMode と値が一致。
+/// Generator は Runtime を直接参照しないためミラーリングする)。
+public enum ParseMode
+{
+ /// LALR(1) 確定パーサ (既定)。
+ Lalr,
+ /// 軽量 GLR: コンフリクトセルで並行 fork し、収束でマージ。
+ LightGlr,
+}
+
/// DSL から抽出した文法モデル。等価比較可能 (IncrementalGenerator のキャッシュ判定用)。
/// シンボル/構文ノードは一切持たず、文字列/整数/bool のみ。
public sealed class GrammarModel : IEquatable
{
public string RootTypeFullName { get; }
public string? Mode { get; } // [Grammar(Mode=...)] の Mode
+ public ParseMode ParseMode { get; } // [Grammar(ParseMode=...)] の ParseMode (既定 Lalr)
public IReadOnlyList Nodes { get; }
public IReadOnlyList TokenDefs { get; }
public IReadOnlyList SkipPatterns { get; }
@@ -24,7 +35,8 @@ public sealed class GrammarModel : IEquatable
public GrammarModel(string rootTypeFullName, IReadOnlyList nodes, IReadOnlyList tokenDefs,
IReadOnlyList? skipPatterns = null, string? mode = null, Location? rootLocation = null,
- IReadOnlyList? tokenDerivedWarnings = null, IReadOnlyList? analyzeRules = null)
+ IReadOnlyList? tokenDerivedWarnings = null, IReadOnlyList? analyzeRules = null,
+ ParseMode parseMode = ParseMode.Lalr)
{
RootTypeFullName = rootTypeFullName;
Nodes = nodes;
@@ -34,12 +46,13 @@ public GrammarModel(string rootTypeFullName, IReadOnlyList nodes, IRe
RootLocation = rootLocation;
TokenDerivedWarnings = tokenDerivedWarnings ?? Array.Empty();
AnalyzeRules = analyzeRules ?? Array.Empty();
+ ParseMode = parseMode;
}
public bool Equals(GrammarModel? other) =>
other is not null && RootTypeFullName == other.RootTypeFullName
&& SeqEqual(Nodes, other.Nodes) && SeqEqual(TokenDefs, other.TokenDefs)
- && SeqEqual(AnalyzeRules, other.AnalyzeRules);
+ && SeqEqual(AnalyzeRules, other.AnalyzeRules) && ParseMode == other.ParseMode;
/// いずれかのノードが IOnSecondPassEnter/Exit を実装するか、[Enter]/[Exit] ルールがあるか。
/// いずれもなければ Walker/Walk を生成しない (空走査回避・ゼロコスト)。
@@ -61,6 +74,7 @@ public override int GetHashCode()
int h = StringComparer.Ordinal.GetHashCode(RootTypeFullName);
for (int i = 0; i < Nodes.Count; i++) h = unchecked(h * 31 + Nodes[i].GetHashCode());
for (int i = 0; i < AnalyzeRules.Count; i++) h = unchecked(h * 31 + AnalyzeRules[i].GetHashCode());
+ h = unchecked(h * 31 + (int)ParseMode);
return h;
}
diff --git a/src/AstFirst.Generator/ParserEmitter.cs b/src/AstFirst.Generator/ParserEmitter.cs
index b71ecda..1a0f1d4 100644
--- a/src/AstFirst.Generator/ParserEmitter.cs
+++ b/src/AstFirst.Generator/ParserEmitter.cs
@@ -139,7 +139,7 @@ private static void EmitParse(StringBuilder sb, string lexerName, GrammarModel m
sb.AppendLine(" public static AstFirst.ParseResult Parse(string input) => Parse(input, null);");
sb.AppendLine(" public static AstFirst.ParseResult Parse(string input, AstFirst.SemanticContext? context)");
sb.AppendLine(" {");
- sb.AppendLine(" var ctx = context ?? new AstFirst.BasicSemanticContext();");
+ sb.AppendLine(" var ctx = (context as AstFirst.BasicSemanticContext) ?? new AstFirst.BasicSemanticContext();");
sb.AppendLine(" var tokens = " + lexerName + ".Tokenize(input);");
sb.AppendLine(" var states = new int[64];");
sb.AppendLine(" var values = new object?[64];");
@@ -173,6 +173,7 @@ private static void EmitParse(StringBuilder sb, string lexerName, GrammarModel m
sb.AppendLine(" else if (dk == 2) // Reduce (仮想 reduce)");
sb.AppendLine(" {");
sb.AppendLine(" var node = ReduceNode(dv, valuesSpan, top, ctx);");
+ sb.AppendLine(" if (node is AstFirst.AstNode __na) __na.NotifyAccepted(ctx);");
sb.AppendLine(" if (node is AstFirst.AstNode __an && !__an.IsAccepted)");
sb.AppendLine(" {");
sb.AppendLine(" // Reject: フォールバック候補を試す");
@@ -210,19 +211,12 @@ private static void EmitParse(StringBuilder sb, string lexerName, GrammarModel m
sb.AppendLine(" errors.Add(new AstFirst.ParseError(\"予期しないトークン\" + (exp.Length > 0 ? \" (期待: \" + exp + \")\" : \"\"), pos));");
sb.AppendLine(" lastErrorPos = i;");
sb.AppendLine(" }");
- sb.AppendLine(" bool recovered = false;");
- sb.AppendLine(" while (top > 1 && i < tokens.Count)");
- sb.AppendLine(" {");
- sb.AppendLine(" top--;");
- sb.AppendLine(" int topState = statesSpan[top - 1];");
- sb.AppendLine(" int curSym = TokenIdToSym[tokens[i].TokenId];");
- sb.AppendLine(" if (curSym >= 0 && ActionKind[topState * SymbolCount + curSym] != 0) { recovered = true; break; }");
- sb.AppendLine(" }");
- sb.AppendLine(" if (!recovered)");
- sb.AppendLine(" {");
- sb.AppendLine(" if (top <= 1) { top = 0; statesSpan[top++] = 0; }");
- sb.AppendLine(" if (i < tokens.Count) i++; else break;");
- sb.AppendLine(" }");
+ sb.AppendLine(" var __et = new AstFirst.Glr.GlrTables(ActionKind, ActionValue, Goto, ProdLhs, ProdLen, DefaultReduce, TokenIdToSym, AltKeys, AltActs, StateCount, SymbolCount, EofSym, 0, SymNames);");
+ sb.AppendLine(" var __es = new AstFirst.Glr.LightGlrDriver.LightGlrStack(states, values, top, i);");
+ sb.AppendLine(" var __er = AstFirst.Glr.ErrorRepair.TryRepair(__et, tokens, __es, (int p, object?[] c, AstFirst.SemanticContext x) => ReduceNode(p, c.AsSpan(), c.Length, x), ToToken, ctx);");
+ sb.AppendLine(" if (__er != null) { states = __er.States; values = __er.Values; top = __er.Top; i = __er.Pos; statesSpan = states; valuesSpan = values; continue; }");
+ sb.AppendLine(" // Corchuelo 修復失敗 → トークンを1つ進めて続行 (パニックモード不使用)");
+ sb.AppendLine(" if (i < tokens.Count) i++; else break;");
sb.AppendLine(" }");
sb.AppendLine(" }");
// 2パス目: IOnSecondPassEnter/Exit を実装するノードが1つでもあれば Walker を生成・呼出。
@@ -309,8 +303,11 @@ private static void EmitHelpers(StringBuilder sb, Grammar grammar, GrammarModel
sb.Append(" case ").Append(prod.Id).Append(": { ");
if (listAction.IsRecursive)
{
- // List_T → List_T item: 既存リスト (右辺0) に item (右辺1) を Add。
- sb.Append("var __list = (").Append(listType).Append(")values[top - ").Append(len).Append(" + 0]!; ");
+ // List_T → List_T item: COW (copy-on-write)。既存リストをコピーして末尾に Add。
+ // ErrorRepair の probe (浅いコピー) でリストが共有されるため、破壊的 Add は不可。
+ sb.Append("var __src = (").Append(listType).Append(")values[top - ").Append(len).Append(" + 0]!; ");
+ sb.Append("var __list = new ").Append(listType).Append("(__src.Count + 1); ");
+ sb.Append("foreach (var __x in __src) __list.Add(__x); ");
sb.Append("__list.Add((").Append(elemType).Append(")values[top - ").Append(len).Append(" + 1]!); ");
}
else if (listAction.IsEmpty)
@@ -384,7 +381,15 @@ public static string EmitPartial(GrammarModel model, NodeModel node, string ns)
// RuleName: 抽象基底、または継承プロパティがない (基底が RuleName を持たない) 場合のみ生成。
if (node.Rules.Count > 0 && (isAbstractBase || !hasInherited))
sb.AppendLine(" public readonly string RuleName;");
- sb.AppendLine(" partial void OnReduce" + ctxParam + ";");
+ // OnReduce は読み取り専用 SemanticContext (ctx の書き換えを防ぐ)。
+ // OnAccepted はルート確定後なのでユーザーの ctx 型 (書き込み可) を渡す。
+ sb.AppendLine(" partial void OnReduce(" + (ctxType is not null ? "AstFirst.SemanticContext ctx" : "") + ");");
+ sb.AppendLine(" partial void OnAccepted" + ctxParam + ";");
+ // NotifyAccepted: 常に override を生成。
+ if (ctxType is not null)
+ sb.AppendLine(" public override void NotifyAccepted(AstFirst.SemanticContext? ctx) => OnAccepted((" + ctxType + ")ctx!);");
+ else
+ sb.AppendLine(" public override void NotifyAccepted(AstFirst.SemanticContext? ctx) => OnAccepted();");
// コンストラクタ: 抽象基底は protected (派生から : base で呼ばれる)、具象は internal。
// 同じ引数型シグネチャの[Rule]が複数ある場合は1つに統合 (ruleName で実行時に区別)。
@@ -463,7 +468,7 @@ public static string EmitPartial(GrammarModel model, NodeModel node, string ns)
if (ar.Phase != AnalyzePhase.OnReduce) continue;
if (ar.TargetNodeFullName != node.FullName) continue;
if (ctxCall.Length == 0) continue; // ctx なしノードには注入不可
- sb.AppendLine(" " + ar.GrammarClassFullName + "." + ar.MethodName + "(this, (" + ar.CtxTypeFullName + ")" + ctxCall + ");");
+ sb.AppendLine(" " + ar.GrammarClassFullName + "." + ar.MethodName + "(this, (AstFirst.SemanticContext)" + ctxCall + ");");
}
sb.AppendLine(" }");
}
diff --git a/src/AstFirst.Generator/ParserGenerator.cs b/src/AstFirst.Generator/ParserGenerator.cs
index c810960..96cfe31 100644
--- a/src/AstFirst.Generator/ParserGenerator.cs
+++ b/src/AstFirst.Generator/ParserGenerator.cs
@@ -36,8 +36,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
var dfa = ModelToDfa.Build(model, out var rules);
// 優先度/結合性で解決できなかったコンフリクトを警告で報告 (構文的曖昧さの可視化)。
- foreach (var conflict in table.Conflicts)
- spc.ReportDiagnostic(Microsoft.CodeAnalysis.Diagnostic.Create(DiagnosticDescriptors.GrammarConflict, model.RootLocation, conflict.Description));
+ // LightGlr モードではコンフリクトは並行 fork で解決されるため警告しない (確定 LALR のみ)。
+ if (model.ParseMode != ParseMode.LightGlr)
+ {
+ foreach (var conflict in table.Conflicts)
+ spc.ReportDiagnostic(Microsoft.CodeAnalysis.Diagnostic.Create(DiagnosticDescriptors.GrammarConflict, model.RootLocation, conflict.Description));
+ }
// 到達不能/未定義非終端を警告で報告 (規則の過不足の可視化)。
foreach (var nt in grammar.UnreachableNonTerminals)
@@ -48,7 +52,9 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
spc.ReportDiagnostic(Microsoft.CodeAnalysis.Diagnostic.Create(DiagnosticDescriptors.TokenDerivedNoStringCtor, model.RootLocation, $"Token 派生型 '{tdw}' に (string) コンストラクタがありません (new DerivedType(token.Text) の生成に必要) / Token-derived type '{tdw}' has no (string) constructor"));
spc.AddSource(typeName + suffix + "Lexer.g.cs", CodeEmitter.EmitLexer(model, dfa, rules, typeName + suffix + "Lexer", ns));
- spc.AddSource(typeName + suffix + "Parser.g.cs", ParserEmitter.EmitParser(model, grammar, table, rules, ns));
+ spc.AddSource(typeName + suffix + "Parser.g.cs", model.ParseMode == ParseMode.LightGlr
+ ? GlrParserEmitter.EmitParser(model, grammar, table, rules, ns)
+ : ParserEmitter.EmitParser(model, grammar, table, rules, ns));
// 汎用 Walker: IOnSecondPassEnter/Exit または [Enter]/[Exit] を使う文法でのみ生成 (ゼロコスト)。
if (model.HasSecondPass)
spc.AddSource(typeName + suffix + "Walker.g.cs", WalkerEmitter.EmitWalker(model, ns));
diff --git a/src/AstFirst.Runtime/AstNode.cs b/src/AstFirst.Runtime/AstNode.cs
index fb44226..1ae7bf8 100644
--- a/src/AstFirst.Runtime/AstNode.cs
+++ b/src/AstFirst.Runtime/AstNode.cs
@@ -10,7 +10,7 @@ namespace AstFirst;
/// AST ノードの基底。非終端記号の具象形 (= 1つの生成規則) はこれを継承します。
/// ノードの構築 (コンストラクタ) が還元時のアクション+意味解析を兼ねます。
///
-public abstract class AstNode
+public abstract partial class AstNode
{
/// このノードが覆うソース範囲。コンストラクタで子から計算して設定する。
public SourceSpan Span { get; protected set; }
@@ -37,6 +37,10 @@ public void SetAnnotation(string key, object? value)
/// この構文を受領しない。優先度順の別候補 (別規則/shift) へフォールバックする。
protected void Reject() => AcceptState = AcceptState.Rejected;
+ /// ルートが1つに確定した時に呼ばれる (GLR の fork 収束、または LALR の reduce 時)。
+ /// 派生クラスの partial 生成コードが override して OnAccepted(ctx) を呼ぶ。
+ public virtual void NotifyAccepted(SemanticContext? ctx) { }
+
/// 受領されたか (Accepted、または既定の Undecided)。パーサ生成コードが参照。
public bool IsAccepted => AcceptState != AcceptState.Rejected;
}
@@ -81,24 +85,26 @@ public sealed class DiagnosticBag
}
///
-/// Semantic analysis context. Declare a constructor parameter of a -derived type
-/// and the generator injects an instance from the parser (providing a and ).
+/// 意味解析コンテキスト (読み取り専用)。OnReduce に渡される。
+/// シンボル表の読み取り (Lookup) のみ可能。宣言 (Declare) や診断追加 (Error) は不可。
+/// 書き込みは ([Enter]/[Exit] Walker 用) を使う。
///
-///
-/// 意味解析コンテキスト。 派生型のコンストラクタ引数として宣言すると、
-/// Generator がパーサからインスタンスを注入します ( と を提供)。
-///
public abstract class SemanticContext
{
- public abstract ScopedSymbolTable Symbols { get; }
- public abstract DiagnosticBag Diagnostics { get; }
+ /// 読み取り専用のシンボル表 (Lookup のみ)。OnReduce で宣言を防ぐ。
+ public abstract IReadOnlySymbolTable Symbols { get; }
}
-/// SemanticContext の標準実装。生成コードが既定で使う。ユーザーが派生して独自 ctx を定義可能。
+/// SemanticContext の標準実装。[Enter]/[Exit] Walker で書き込み可能。
public class BasicSemanticContext : SemanticContext
{
- public override ScopedSymbolTable Symbols { get; } = new ScopedSymbolTable();
- public override DiagnosticBag Diagnostics { get; } = new DiagnosticBag();
- /// ノード→型 の対応 (型推論・型チェックの結果)。意味解析ウォークで使用。
+ private readonly ScopedSymbolTable _symbols = new ScopedSymbolTable();
+ /// 読み取り専用ビュー (基底 API)。
+ public override IReadOnlySymbolTable Symbols => _symbols;
+ /// 書き込み可能なシンボル表 ([Enter]/[Exit] で宣言・スコープ操作に使用)。
+ public ScopedSymbolTable WritableSymbols => _symbols;
+ /// 診断バグ ([Enter]/[Exit] でエラー・警告の追加に使用)。
+ public DiagnosticBag Diagnostics { get; } = new DiagnosticBag();
+ /// ノード→型 の対応 (型推論・型チェックの結果)。
public TypeContext Types { get; } = new TypeContext();
}
diff --git a/src/AstFirst.Runtime/Attributes.cs b/src/AstFirst.Runtime/Attributes.cs
index 2e4a613..fb8fc06 100644
--- a/src/AstFirst.Runtime/Attributes.cs
+++ b/src/AstFirst.Runtime/Attributes.cs
@@ -16,12 +16,24 @@ public sealed class PatternAttribute(string regex) : Attribute
public int Priority { get; set; }
}
+/// パーサの実行モード。
+public enum ParseMode
+{
+ /// LALR(1) 確定パーサ (既定)。コンフリクトは優先度/結合性で解決し、解決不能分は警告 (ASTF001)。
+ Lalr,
+ /// 軽量 GLR: コンフリクトセルで並行 fork し、収束でマージ。本質的曖昧性 (cast/paren, generic の型/式 等) を扱う。
+ LightGlr,
+}
+
/// 文法の開始記号 (ルート非終端) のクラスに付ける。Generator の抽出開始点。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class GrammarAttribute : Attribute
{
- /// 複数フォーマット/方言時のモード名 (フェーズ7)。
+ /// 複数フォーマット/方言時のモード名。
public string? Mode { get; set; }
+ /// パーサの実行モード。既定は (確定 LALR(1))。
+ /// は軽量 GLR (コンフリクトを並行 fork で解決)。
+ public ParseMode ParseMode { get; set; } = ParseMode.Lalr;
}
/// スキップパターン (空白・コメント等)。クラスまたはアセンブリに付ける。
diff --git a/src/AstFirst.Runtime/Glr/ErrorRepair.cs b/src/AstFirst.Runtime/Glr/ErrorRepair.cs
new file mode 100644
index 0000000..204672c
--- /dev/null
+++ b/src/AstFirst.Runtime/Glr/ErrorRepair.cs
@@ -0,0 +1,124 @@
+using System.Collections.Generic;
+using AstFirst.Core.Lexing;
+
+namespace AstFirst.Glr;
+
+///
+/// Corchuelo et al. のエラー修復 (ER1 挿入 / ER2 削除 / ER3 Forward move)。
+/// LALR パーサと LightGlrDriver の両方から呼ばれる共通の修復ロジック。
+/// 参考: "Repairing Syntax Errors in LR Parsers" (Corchuelo, Pérez, Ruiz, Toro, Sevilla)。
+///
+public static class ErrorRepair
+{
+ private const int N = 3; // ER3 の Forward move シンボル数
+ private const int CostInsert = 1; // 挿入は安い (欠落の補完)
+ private const int CostDelete = 2; // 削除は高い (入力の破棄)
+
+ /// エラー状態のスタック s から修復を試みる。成功すれば修復適用済みのスタック、失敗すれば null。
+ public static LightGlrDriver.LightGlrStack? TryRepair(
+ GlrTables t, IReadOnlyList tokens,
+ LightGlrDriver.LightGlrStack s,
+ System.Func reduce,
+ System.Func toToken,
+ SemanticContext ctx)
+ {
+ LightGlrDriver.LightGlrStack? best = null;
+ int bestCost = int.MaxValue;
+ int qm = s.State;
+ var dummyToken = new BasicToken("", default(SourceSpan));
+
+ // ER1: 現状態 qm で shift 可能な終端 t0 (≠$) を挿入候補。
+ for (int t0 = 0; t0 < t.SymbolCount; t0++)
+ {
+ if (t0 == t.EofSym) continue;
+ if (t.ActionKind[qm * t.SymbolCount + t0] != 1) continue;
+ var probe = s.Clone();
+ probe.Push(t.ActionValue[qm * t.SymbolCount + t0], dummyToken);
+ if (SimulateForward(t, tokens, reduce, toToken, ctx, probe) && CostInsert < bestCost)
+ {
+ best = s.Clone();
+ best.Push(t.ActionValue[qm * t.SymbolCount + t0], dummyToken);
+ bestCost = CostInsert;
+ }
+ }
+
+ // ER2: 現トークン t1 を削除候補。
+ if (s.Pos < tokens.Count)
+ {
+ var probe = s.Clone();
+ probe.Pos = s.Pos + 1;
+ if (SimulateForward(t, tokens, reduce, toToken, ctx, probe) && CostDelete < bestCost)
+ {
+ best = s.Clone();
+ best.Pos = s.Pos + 1;
+ bestCost = CostDelete;
+ }
+ }
+
+ return best;
+ }
+
+ /// ER3 Forward move: N シンボル (または accept) までパースを進められるか確認。
+ private static bool SimulateForward(GlrTables t, IReadOnlyList tokens,
+ System.Func reduce,
+ System.Func toToken,
+ SemanticContext ctx, LightGlrDriver.LightGlrStack sim)
+ {
+ int parsed = 0;
+ var visited = new HashSet<(int, int)>();
+ while (parsed < N)
+ {
+ int guard = 0;
+ while (true)
+ {
+ if (guard++ > 4096) return false;
+ int la = LookaheadSym(t, tokens, sim.Pos);
+ if (la < 0) return false;
+ if (!visited.Add((sim.State, sim.Pos))) return false;
+ var acts = t.Actions(sim.State, la);
+ int ra = -1;
+ foreach (var a in acts) if (a.Kind == 2) { ra = a.Value; break; }
+ if (ra < 0) break;
+ try { ApplyReduce(t, reduce, ctx, sim, ra); }
+ catch { return false; }
+ }
+ int la2 = LookaheadSym(t, tokens, sim.Pos);
+ if (la2 < 0) return false;
+ var acts2 = t.Actions(sim.State, la2);
+ bool hasShift = false, hasAccept = false;
+ int shiftState = -1;
+ foreach (var a in acts2)
+ {
+ if (a.Kind == 1) { hasShift = true; shiftState = a.Value; }
+ else if (a.Kind == 3) hasAccept = true;
+ }
+ if (hasAccept) return parsed > 0;
+ if (!hasShift) return false;
+ sim.Push(shiftState, la2 == t.EofSym ? null : (object)toToken(tokens[sim.Pos]));
+ sim.Pos++;
+ parsed++;
+ }
+ return true;
+ }
+
+ internal static void ApplyReduce(GlrTables t, System.Func reduce,
+ SemanticContext ctx, LightGlrDriver.LightGlrStack s, int prodId)
+ {
+ int len = t.ProdLen[prodId];
+ var children = len == 0 ? System.Array.Empty