diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml
index d89cf93..2aa8a86 100644
--- a/.github/workflows/compatibility.yml
+++ b/.github/workflows/compatibility.yml
@@ -9,6 +9,22 @@ on:
pull_request:
jobs:
+ tests:
+ name: test suite
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
+ - name: Prepare local package feed
+ run: |
+ mkdir -p artifacts/packages
+ dotnet pack src/AstFirst.Core/AstFirst.Core.csproj -c Release -o artifacts/packages
+ dotnet pack src/AstFirst.Runtime/AstFirst.Runtime.csproj -c Release -o artifacts/packages
+ - name: Run tests
+ run: dotnet test AstFirst.slnx -c Release --nologo
+
compat:
name: ${{ matrix.sdk }} / ${{ matrix.framework }}
strategy:
diff --git a/.github/workflows/publish-nuget.yml b/.github/workflows/publish-nuget.yml
index 2996291..2f4cc01 100644
--- a/.github/workflows/publish-nuget.yml
+++ b/.github/workflows/publish-nuget.yml
@@ -24,6 +24,8 @@ jobs:
fetch-depth: 0 # タグ存在確認のため全履歴+タグを取得
- uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: '10.0.x'
- name: Version を読み取り
id: ver
@@ -71,6 +73,10 @@ jobs:
if: steps.check.outputs.published == 'false'
run: dotnet pack src/AstFirst.Runtime/AstFirst.Runtime.csproj -c Release -p:Version=${{ steps.ver.outputs.version }} -p:PackageReadmePath="$PWD/README.nuget.md" -o artifacts/packages
+ - name: Test
+ if: steps.check.outputs.published == 'false'
+ run: dotnet test AstFirst.slnx -c Release --nologo
+
- name: Pack Generator
if: steps.check.outputs.published == 'false'
run: dotnet pack src/AstFirst.Generator/AstFirst.Generator.csproj -c Release -p:Version=${{ steps.ver.outputs.version }} -p:PackageReadmePath="$PWD/README.nuget.md" -o artifacts/packages
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc005c1..4f26601 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## 0.4.1 — 文法ノード探索の拡張と品質改善
+
+- **別名前空間の文法ノード**: 既定探索で `[Grammar]` ルートの派生型をアセンブリ全体から収集。
+- **探索モード**: `GrammarDiscovery.TypeHierarchy` で名前空間走査を無効化。`Namespace` で従来境界も選択可能。
+- **明示参加**: `[GrammarPart(typeof(Root))]` で名前空間・型階層外の `AstNode` を文法へ追加。
+- **複数文法・方言**: 同じノードを共有する複数文法と、同一ルートの複数 `Mode` を重複生成なしでサポート。
+- **アセンブリ共通Skip**: 宣言済みだが未収集だった `[assembly: Skip(...)]` をGeneratorへ反映。
+- **安定した Core モデル**: `Grammar` が入力コレクションのスナップショットを保持し、`Production` が右辺入力を防御コピー。`GrammarBuilder.Build` を反復可能に。
+- **品質ゲート**: CI と公開フローで全テストを実行。Generator の Core 型競合警告を解消。
+
+---
+
## 0.4.0 — 軽量 GLR (LightGlr) モード + 読み取り専用 OnReduce
### ⚠ 破壊的変更 (後方互換性なし)
diff --git a/Directory.Build.props b/Directory.Build.props
index b0c76e8..33a2c3b 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,6 +1,6 @@
- 0.4.0
+ 0.4.1
actbit
https://github.com/actbit/AstFirst
https://github.com/actbit/AstFirst
diff --git a/README.ja.md b/README.ja.md
index 3181e45..8ff3e65 100644
--- a/README.ja.md
+++ b/README.ja.md
@@ -15,13 +15,13 @@ AstFirst はパーサジェネレータ・パーサコンビネータと同じ
|---|---|---|---|---|
| 文法の定義 | C# のクラス + 属性 | 外部 `.g4` DSL | C# パーサコンビネータ | 該当なし (C#/VB 構文のみ) |
| コード生成 | **コンパイル時** (Source Generator) | ビルド時コード生成ツール | **しない** (実行時に解釈) | 該当なし |
-| 実行時パースコスト | **ゼロ** — 静的テーブル・ディスパッチなし | 生成コード | 解釈実行 (パース毎にアロケーション/ディスパッチ) | 該当なし |
+| 実行時方式 | 静的テーブル、パーサ構築不要 | 生成コード | 解釈実行 (パース毎にアロケーション/ディスパッチ) | 該当なし |
| AOT / Native AOT | ✓ 実行時コード生成なし | △ | ✓ | 該当なし |
| アルゴリズム | LALR(1) + 軽量 GLR (LightGlr) | LL(\*) / ALL(\*) | 再帰下降コンビネータ | 該当なし |
| エラー回復 | Corchuelo et al. ER1/ER2/ER3 (組み込み) | あり | 自作が必要 | 該当なし |
| 文法の表現力 | LALR(1) + GLR — `[Precedence]` / fork で衝突解決 | LL(\*) | **チューリング完全** (任意の C# で分岐) | 該当なし |
-**コンビネータ (Superpower/Pidgin) に対する強み**: パーサを*コンパイル時*に静的テーブルとして生成するため、解釈もディスパッチもパーサ構築も実行時に走らない。起動・パース毎のコストが事実上ゼロで、AOT/Native AOT にも綺麗に通る。Corchuelo et al. の高品質エラー回復 (ER1 挿入 / ER2 削除 / ER3 Forward move) を組み込み。文法は宣言的な C# なので、IDE のナビゲーションやリファクタリングが効く。
+**コンビネータ (Superpower/Pidgin) に対する強み**: パーサを*コンパイル時*に静的テーブルとして生成するため、実行時の解釈・delegate dispatch・パーサ構築が不要で、AOT/Native AOT にも綺麗に通る。Corchuelo et al. の高品質エラー回復 (ER1 挿入 / ER2 削除 / ER3 Forward move) を組み込み。文法は宣言的な C# なので、IDE のナビゲーションやリファクタリングが効く。
**トレードオフ**: LALR(1) は `[Precedence]`/結合性で shift-reduce 衝突を解決する必要がある。ただし LightGlr モード (`[Grammar(ParseMode = ParseMode.LightGlr)]`) で本質的曖昧性 (cast/paren・generic 等) を並行 fork で扱える。C# 専用のツールチェイン。
@@ -97,14 +97,14 @@ var result = ExprParser.Parse("1+2*3");
// result.HasErrors → false
var result2 = ExprParser.Parse("1+");
-// result2.HasErrors → true (panic mode で回復)
+// result2.HasErrors → true (Corchuelo ER1/ER2/ER3 で回復)
```
## 意味解析
AstFirst は構文解析(AST 構築)に加え、意味解析のための標準ヘルパーと 2パスの枠組みを提供する。詳細は [docs/ja/semantic-analysis.md](docs/ja/semantic-analysis.md)。
-- **属性ベースのルール `[OnReduce]` / `[Enter]` / `[Exit]` (推奨)**: 意味ルールを `[Grammar]` ルートクラスの `static` メソッドで書く。Generator がコンストラクタ / Walker から dispatch し、ctx のキャストも自動で挿入(ノード毎のボイラープレート不要)。
+- **属性ベースのルール `[OnReduce]` / `[Enter]` / `[Exit]` (推奨)**: 意味ルールを `[Grammar]` ルートクラスの `static` メソッドで書く。Generator が `[OnReduce]` を Parser の reduce 処理から、`[Enter]`/`[Exit]` を Walker から dispatch し、ctx のキャストも自動で挿入(ノード毎のボイラープレート不要)。
- **1パス目 `OnReduce` (ボトムアップ)**: reduce 時に呼ばれる partial メソッド。`Accept()`/`Reject()` でこの構文を受け入れるか判定(既定 Accept)。`Reject` すると別候補へフォールバック。
- **2パス目 `[Enter]`/`[Exit]` / `OnSecondPassEnter`/`Exit` (トップダウン)**: 生成された汎用 Walker (`{Root}Walker`) が `Parse` 後に `Enter → 子 → Exit` を駆動。スコープ Push/Pop 等の正確な意味解析が書ける。意味フックのない文法では走査を省略(オーバーヘッドなし・ゼロコスト)。
- **型システム**: `TypeSymbol` は継承可能で `FunctionTypeSymbol`/`ArrayTypeSymbol`(共変・反変・構造等価)を組み込み、暗黙の型変換の分類と `OverloadResolver` も提供。`BasicSemanticContext` は `TypeContext` を標準で保持。
@@ -227,11 +227,12 @@ var result = ProgramParser.Parse(code, new MiniCContext());
| 属性 | 対象 | 役割 |
|---|---|---|
| `[Grammar]` | クラス | 文法の開始記号(ルート非終端)。Generator の抽出開始点。`Mode` で複数方言を切り替え。 |
+| `[GrammarPart(typeof(Root))]` | クラス | 名前空間・ルート型階層外のノードを文法へ明示的に追加。`[Grammar].Discovery` で探索方法を選択可能。 |
| `[Rule]` | static メソッド | 生成規則(1クラス1つ)。メソッドの**引数**が右辺。 |
| `[Token(@"regex")]` / `[Pattern(@"regex")]` | `[Rule]` メソッドの `Token` 引数 | 字句ルール(正規表現)。`Priority` でレクサ優先度(大きいほど高優先)。 |
| `[Precedence(n)]` | クラス(演算ノード) | 演算子優先度/結合性。`n` が大きいほど高優先。`IsRightAssociative`/`IsNonAssociative` で結合性。 |
| `[Repeat]` / `[Repeat(Min=0)]` | `[Rule]` メソッドの `AstNode` 派生引数 | リスト(繰り返し)。`Min=1`(既定)は1回以上、`Min=0` は0回以上(空リスト可)。`IReadOnlyList` に展開。 |
-| `[Skip(@"regex")]` | クラス(`[Grammar]` と同じ) | スキップパターン(空白・コメント等)。 |
+| `[Skip(@"regex")]` | `[Grammar]` クラス/アセンブリ | スキップパターン(空白・コメント等)。アセンブリ指定は全Grammar共通。 |
### `[Rule]` メソッドの引数(型ベース分類)
@@ -365,7 +366,7 @@ AstFirst.slnx
## テスト
-352 テスト(AstFirst.Tests 299 + Generator.Tests 53)。レクサ/DFA/LALR の各段階、エンドツーエンド、エラー回復 (Corchuelo)、GLR fork/dedup、意味解析(スコープ・2パス目・型チェック・ctx → `ParseResult.Diagnostics` の統合)、`Accept`/`Reject` フォールバック、`OnAccepted` コールバック、位置情報(行・列)を検証。
+374 テスト(AstFirst.Tests 308 + Generator.Tests 66)。レクサ/DFA/LALR の各段階、エンドツーエンド、エラー回復 (Corchuelo)、GLR fork/dedup、意味解析、文法ノード探索、Coreビルドのスナップショット安定性、`Accept`/`Reject` フォールバック、`OnAccepted`、位置情報を検証。
## ライセンス
diff --git a/README.md b/README.md
index a0bff73..41bd998 100644
--- a/README.md
+++ b/README.md
@@ -15,13 +15,13 @@ AstFirst sits in the same space as parser generators and combinator libraries, b
|---|---|---|---|---|
| Grammar in | plain C# classes + attributes | external `.g4` DSL | C# parser combinators | n/a — C#/VB syntax only |
| 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 |
+| Runtime strategy | static tables, no parser construction | generated code | interpreted (allocation/dispatch per parse) | n/a |
| AOT / Native AOT | ✓ no runtime codegen | △ | ✓ | 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 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.
+**Strengths vs combinators (Superpower/Pidgin)**: the parser is emitted *at compile time* as static tables, so runtime interpretation, delegate dispatch, and parser construction are unnecessary. 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. However, LightGlr mode (`[Grammar(ParseMode = ParseMode.LightGlr)]`) handles inherent ambiguity (cast/paren, generics) via parallel fork. A C#-only toolchain.
@@ -97,14 +97,14 @@ var result = ExprParser.Parse("1+2*3");
// result.HasErrors -> false
var result2 = ExprParser.Parse("1+");
-// result2.HasErrors -> true (recovered via panic mode)
+// result2.HasErrors -> true (recovered via Corchuelo ER1/ER2/ER3)
```
## Semantic analysis
AstFirst provides standard helpers and a two-pass framework for semantic analysis on top of parsing. See [docs/en/semantic-analysis.md](docs/en/semantic-analysis.md) for details.
-- **Attribute-based rules `[OnReduce]` / `[Enter]` / `[Exit]` (recommended)**: write semantic rules as `static` methods on the `[Grammar]` root class. The generator dispatches them from the constructor / Walker and injects the `ctx` cast for you — no per-node boilerplate.
+- **Attribute-based rules `[OnReduce]` / `[Enter]` / `[Exit]` (recommended)**: write semantic rules as `static` methods on the `[Grammar]` root class. The generator dispatches `[OnReduce]` during parser reduction and `[Enter]`/`[Exit]` from the Walker, injecting the `ctx` cast for you — no per-node boilerplate.
- **First pass `OnReduce` (bottom-up)**: a partial method called at reduce time. `Accept()`/`Reject()` decides whether to accept this interpretation (default Accept). `Reject` falls back to the next candidate.
- **Second pass `[Enter]`/`[Exit]` / `OnSecondPassEnter`/`Exit` (top-down)**: a generated generic Walker (`{Root}Walker`) drives `Enter -> children -> Exit` after `Parse`. Accurate semantic analysis like scope Push/Pop fits here. Grammars with no semantic hook skip the traversal entirely (no overhead, zero-cost).
- **Type system**: `TypeSymbol` is inheritable with built-in `FunctionTypeSymbol`/`ArrayTypeSymbol` (variance + structural equality), plus implicit-conversion classification and `OverloadResolver`. `BasicSemanticContext` carries a `TypeContext` by default.
@@ -227,11 +227,12 @@ See [docs/en/grammar-reference.md](docs/en/grammar-reference.md) for details.
| Attribute | Target | Role |
|---|---|---|
| `[Grammar]` | class | Start symbol (root nonterminal). Generator's extraction entry point. `Mode` switches dialects. |
+| `[GrammarPart(typeof(Root))]` | class | Explicitly adds a node outside the grammar namespace/root hierarchy. `[Grammar].Discovery` selects discovery behavior. |
| `[Rule]` | static method | A production (one per class). The method's **parameters** are the RHS. |
| `[Token(@"regex")]` / `[Pattern(@"regex")]` | `Token` parameter of a `[Rule]` method | Lexical rule (regex). `Priority` sets lexer priority (higher wins). |
| `[Precedence(n)]` | class (operator node) | Operator precedence/associativity. Higher `n` binds tighter. `IsRightAssociative`/`IsNonAssociative`. |
| `[Repeat]` / `[Repeat(Min=0)]` | `AstNode`-derived parameter of a `[Rule]` method | List (repetition). `Min=1` (default) = one or more, `Min=0` = zero or more (empty list allowed). Expands to `IReadOnlyList`. |
-| `[Skip(@"regex")]` | class (same as `[Grammar]`) | Skip pattern (whitespace, comments). |
+| `[Skip(@"regex")]` | `[Grammar]` class / assembly | Skip pattern (whitespace, comments). Assembly-level patterns apply to every grammar. |
### `[Rule]` method parameters (type-based classification)
@@ -316,7 +317,7 @@ Japanese versions are under `docs/ja/` and [README.md](README.md).
## Tests
-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).
+374 tests (AstFirst.Tests 308 + Generator.Tests 66). Covers lexer/DFA/LALR stages, end-to-end, error recovery (Corchuelo), GLR fork/dedup, semantic analysis, grammar discovery, Core build snapshot stability, `Accept`/`Reject` fallback, `OnAccepted`, and positions.
## License
diff --git a/docs/en/architecture.md b/docs/en/architecture.md
index a015df9..4e6c32c 100644
--- a/docs/en/architecture.md
+++ b/docs/en/architecture.md
@@ -13,7 +13,7 @@ AstFirst is a parser generator built as three layers plus a generator.
## Generator pipeline
-1. **Extraction** (`ModelExtraction`): traverses AstNode/Token derivatives and `[Pattern]` from the `[Grammar]` root, collects `[OnReduce]`/`[Enter]`/`[Exit]` attribute semantic rules, and converts them into an equality-comparable POCO model (`GrammarModel`/`AnalyzeRuleModel`).
+1. **Extraction** (`ModelExtraction`): follows `[Grammar].Discovery` to collect nodes from the root namespace, root type hierarchy, and `[GrammarPart]`; then converts `[Rule]`/`[Token]` and semantic rules into equality-comparable POCO models.
2. **DFA build** (`ModelToDfa`): regex of each rule -> NFA (Thompson) -> DFA (subset construction) -> minimization (Hopcroft).
3. **LALR table** (`ModelToTable`): LR(0) automaton -> FIRST/NULLABLE -> DeRemer-Pennello lookahead propagation -> ACTION/GOTO tables + conflict detection.
4. **Code emission** (`CodeEmitter` / `ParserEmitter` / `WalkerEmitter`): generates C# for Lexer (DFA arrays), Parser (LALR table + shift/reduce driver), Walker (Enter/Exit/Walk + `[Enter]`/`[Exit]` dispatch), and per-node partials (including `[OnReduce]` dispatch).
@@ -21,7 +21,7 @@ AstFirst is a parser generator built as three layers plus a generator.
## Generated code shape
- **Lexer**: embeds the DFA transition table and accepting rules in `static readonly` arrays; `Tokenize()` runs longest-match + priority-driven. Also computes each token's 1-based line/column.
-- **Parser**: embeds ACTION/GOTO tables and Productions in arrays and drives shift/reduce/accept. At reduce it calls the AST class constructor to build the AST (`[OnReduce]` attribute methods are called right after partial `OnReduce`). Includes panic-mode error recovery.
+- **Parser**: embeds ACTION/GOTO tables and Productions in arrays and drives shift/reduce/accept. At reduce it calls the AST class constructor to build the AST (`[OnReduce]` attribute methods are called right after partial `OnReduce`). Includes Corchuelo ER1/ER2/ER3 error repair.
- **Walker**: `EnterXxx` / `ExitXxx` (virtual, empty) per concrete node + `Walk` (iterative stack: Enter -> children -> Exit). Also invokes `IOnSecondPassEnter`/`Exit` and `[Enter]`/`[Exit]` attribute methods. Children are collected from each node's public properties of AstNode-derived types. If a grammar has no semantic hook at all, the Walker is not emitted (zero-cost).
## Caching strategy
diff --git a/docs/en/grammar-reference.md b/docs/en/grammar-reference.md
index d238260..3847d79 100644
--- a/docs/en/grammar-reference.md
+++ b/docs/en/grammar-reference.md
@@ -9,12 +9,13 @@ AstFirst grammars are written with C# classes and attributes. The generator emit
| Attribute | Target | Role |
|---|---|---|
| `[Grammar]` | class | Start symbol (root nonterminal). Generator's extraction entry point. `Mode` switches dialects. |
+| `[GrammarPart(typeof(Root))]` | class | Explicitly includes an `AstNode` outside the grammar namespace/root hierarchy. |
| `[Rule]` | static method | A production. The method's **parameters** are the RHS. Multiple per class allowed (see below). |
| `[Token(@"regex")]` / `[Pattern(@"regex")]` | `Token` parameter of a `[Rule]` method | Lexical rule (regex). `Priority` sets lexer priority, `Kind` sets token category. |
| `[Precedence(n)]` | class (operator node) | Operator precedence/associativity. Higher `n` binds tighter. |
| `[Repeat]` / `[Repeat(Min=0)]` | `AstNode`-derived parameter of a `[Rule]` method | List (repetition). `Min=1` (default) = one or more, `Min=0` = zero or more. Expands to `IReadOnlyList`. |
-| `[Skip(@"regex")]` | class (same as `[Grammar]`) | Skip pattern (whitespace, comments). |
-| `[OnReduce]` / `[Enter]` / `[Exit]` | static method (on the `[Grammar]` root class) | Semantic rule. The generator dispatches it from the constructor (`[OnReduce]`) / Walker (`[Enter]`/`[Exit]`); the ctx cast is injected. |
+| `[Skip(@"regex")]` | `[Grammar]` class / assembly | Skip pattern (whitespace, comments). Assembly-level patterns apply to every grammar. |
+| `[OnReduce]` / `[Enter]` / `[Exit]` | static method (on the `[Grammar]` root class) | Semantic rule. The generator dispatches `[OnReduce]` during parser reduction and `[Enter]`/`[Exit]` from the Walker; the ctx cast is injected. |
## `[Grammar]`
@@ -28,6 +29,28 @@ public abstract partial class Expr : AstNode { }
The `Mode` named property switches dialects (see below).
+### GrammarDiscovery (node discovery)
+
+Use `Discovery` to select how grammar nodes are found.
+
+| Value | Behavior |
+|---|---|
+| `NamespaceAndTypeHierarchy` (default) | Includes `AstNode` types in the root namespace, root-derived types anywhere in the assembly, and explicit `[GrammarPart]` types. |
+| `TypeHierarchy` | Disables namespace scanning; includes root-derived types anywhere in the assembly and explicit `[GrammarPart]` types. |
+| `Namespace` | Legacy boundary; includes `AstNode` types in the root namespace and explicit `[GrammarPart]` types. |
+
+```csharp
+[Grammar(Discovery = GrammarDiscovery.TypeHierarchy)]
+public abstract partial class Expr : AstNode { }
+
+// Discovered across namespaces because it derives from Expr.
+public sealed partial class NumberExpr : Expr { }
+
+// A shared node outside the hierarchy can opt in explicitly.
+[GrammarPart(typeof(Expr))]
+public sealed partial class SharedValue : AstNode { }
+```
+
### ParseMode (parser execution mode)
The `ParseMode` named property selects the parser execution mode. Default is `Lalr` (deterministic LALR(1)).
@@ -48,7 +71,7 @@ The `ParseMode` named property selects the parser execution mode. Default is `La
- **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.
+- **Error recovery behavior**: Both Lalr and LightGlr use Corchuelo ER1/ER2/ER3 to insert/delete tokens and continue parsing. GLR branching can still produce different error positions/messages between modes for the same input.
### ⚠ Breaking Changes (0.4.0)
@@ -197,10 +220,11 @@ public sealed partial class NonEmpty : Program
## `[Skip]`
-Skip pattern (whitespace, comments). Attach to the same class as `[Grammar]`. Matched spans are removed from the token stream.
+Skip pattern (whitespace, comments). Attach it to a `[Grammar]` class for one grammar or to the assembly for every grammar. Matched spans are removed from the token stream.
```csharp
[Skip(@"(\s|//[^\n]*)+")] // whitespace and line comments
+[assembly: Skip(@"//[^\n]*")] // shared by every grammar
```
## Writing grammar
@@ -242,7 +266,7 @@ Special parameter types of a `[Rule]` method:
- **`OnReduce(ctx)`**: a partial method called when a rule is reduced (bottom-up). Child properties and `Span` (auto-computed from children) are already set. Use `this.RuleName` to branch on the rule, override `Span`, etc.
- **Accept/Reject**: override `IsAccepted` to return `false` to reject a reduce and try fallback candidates. See the [README](../../README.md) "Accept/Reject and fallback" section.
- **`OnSecondPass`**: the second-pass traversal (top-down). For nodes implementing `IOnSecondPassEnter`/`IOnSecondPassExit`, the generator calls `OnSecondPassEnter` (before children) → recurse children → `OnSecondPassExit` (after children).
-- **`[OnReduce]` / `[Enter]` / `[Exit]` attributes**: attach to a `static` method on the `[Grammar]` root class and the generator dispatches it from the Walker / constructor (the ctx cast is injected automatically). `[OnReduce]` runs at reduce; `[Enter]`/`[Exit]` run in the second pass. See the [semantic analysis guide](semantic-analysis.md).
+- **`[OnReduce]` / `[Enter]` / `[Exit]` attributes**: attach to a `static` method on the `[Grammar]` root class. The generator dispatches `[OnReduce]` during parser reduction and `[Enter]`/`[Exit]` from the Walker (the ctx cast is injected automatically). `[OnReduce]` runs at reduce; `[Enter]`/`[Exit]` run in the second pass. See the [semantic analysis guide](semantic-analysis.md).
## Dialects (Mode)
diff --git a/docs/ja/architecture.md b/docs/ja/architecture.md
index 3d308ab..9ae25df 100644
--- a/docs/ja/architecture.md
+++ b/docs/ja/architecture.md
@@ -13,7 +13,7 @@ AstFirst は 3 層 + Generator で構成されるパーサジェネレータ。
## Generator の処理フロー
-1. **抽出** (`ModelExtraction`): `[Grammar]` ルートから AstNode 派生・Token 派生・`[Pattern]` を走査し、`[OnReduce]`/`[Enter]`/`[Exit]` 属性付き意味規則を収集して、等価比較可能な POCO モデル (`GrammarModel`/`AnalyzeRuleModel`) に変換。
+1. **抽出** (`ModelExtraction`): `[Grammar]` の `Discovery` に従い、同一名前空間・ルート型階層・`[GrammarPart]` から文法ノードを収集する。`[Rule]`/`[Token]` と意味規則を等価比較可能な POCO モデルへ変換する。
2. **DFA 構築** (`ModelToDfa`): 字句ルールの正規表現 → NFA (Thompson) → DFA (部分集合構成法) → 最小化 (Hopcroft)。
3. **LALR テーブル** (`ModelToTable`): LR(0) オートマトン → FIRST/NULLABLE → DeRemer-Pennello ルックアヘッド伝播 → ACTION/GOTO テーブル + 衝突検出。
4. **コード生成** (`CodeEmitter` / `ParserEmitter` / `WalkerEmitter`): Lexer(DFA 配列)、Parser(LALR テーブル + shift/reduce 駆動)、Walker(Enter/Exit/Walk + `[Enter]`/`[Exit]` dispatch)、各ノードの partial(`[OnReduce]` dispatch 含む)の C# コードを生成。
@@ -21,7 +21,7 @@ AstFirst は 3 層 + Generator で構成されるパーサジェネレータ。
## 生成コードの構造
- **Lexer**: DFA の遷移表と受理ルールを `static readonly` 配列に埋め込み、`Tokenize()` で最長一致 + 優先度駆動。各トークンの行・列(1 ベース)も計算。
-- **Parser**: ACTION/GOTO テーブル + Productions を配列に埋め込み、shift/reduce/accept を駆動。reduce 時に AST クラスのコンストラクタを呼び AST を構築(`[OnReduce]` 属性メソッドは partial `OnReduce` の直後に呼出)。panic mode のエラー回復付き。
+- **Parser**: ACTION/GOTO テーブル + Productions を配列に埋め込み、shift/reduce/accept を駆動。reduce 時に AST クラスのコンストラクタを呼び AST を構築(`[OnReduce]` 属性メソッドは partial `OnReduce` の直後に呼出)。Corchuelo ER1/ER2/ER3 エラー修復付き。
- **Walker**: 各具象ノードの `EnterXxx` / `ExitXxx`(virtual、空実装)+ `Walk`(反復スタックで Enter → 子 → Exit)。`IOnSecondPassEnter`/`Exit` と `[Enter]`/`[Exit]` 属性メソッドも呼出。子は各ノードの public プロパティから AstNode 派生を収集して辿る。意味解析フックが1つもない文法では生成を省略(ゼロコスト)。
## キャッシュ戦略
diff --git a/docs/ja/grammar-reference.md b/docs/ja/grammar-reference.md
index 6380a2d..bc33160 100644
--- a/docs/ja/grammar-reference.md
+++ b/docs/ja/grammar-reference.md
@@ -9,12 +9,13 @@ AstFirst では C# のクラスと属性で文法を書く。Generator がコン
| 属性 | 対象 | 役割 |
|---|---|---|
| `[Grammar]` | クラス | 文法の開始記号(ルート非終端)。Generator の抽出開始点。`Mode` で複数方言を切り替え。 |
+| `[GrammarPart(typeof(Root))]` | クラス | 名前空間・ルート型階層外の `AstNode` を指定文法へ明示的に参加させる。 |
| `[Rule]` | static メソッド | 生成規則。メソッドの**引数**が右辺。1クラスに複数置ける(後述)。 |
| `[Token(@"regex")]` / `[Pattern(@"regex")]` | `[Rule]` メソッドの `Token` 引数 | 字句ルール(正規表現)。`Priority` でレクサ優先度、`Kind` でトークン種別。 |
| `[Precedence(n)]` | クラス(演算ノード) | 演算子優先度/結合性。`n` が大きいほど高優先。 |
| `[Repeat]` / `[Repeat(Min=0)]` | `[Rule]` メソッドの `AstNode` 派生引数 | リスト(繰り返し)。`Min=1`(既定)= 1回以上、`Min=0` = 0回以上。`IReadOnlyList` に展開。 |
-| `[Skip(@"regex")]` | クラス(`[Grammar]` と同じ) | スキップパターン(空白・コメント等)。 |
-| `[OnReduce]` / `[Enter]` / `[Exit]` | static メソッド(`[Grammar]` ルートクラス) | 意味ルール。Generator がコンストラクタ(`[OnReduce]`)/ Walker(`[Enter]`/`[Exit]`)から dispatch(ctx キャスト自動注入)。 |
+| `[Skip(@"regex")]` | `[Grammar]` クラス/アセンブリ | スキップパターン(空白・コメント等)。アセンブリ指定は全Grammar共通。 |
+| `[OnReduce]` / `[Enter]` / `[Exit]` | static メソッド(`[Grammar]` ルートクラス) | 意味ルール。Generator が `[OnReduce]` を Parser の reduce 処理から、`[Enter]`/`[Exit]` を Walker から dispatch(ctx キャスト自動注入)。 |
## `[Grammar]`
@@ -28,6 +29,28 @@ public abstract partial class Expr : AstNode { }
`Mode` 名前付きプロパティで複数方言を切り替えられる(後述)。
+### GrammarDiscovery(ノード探索)
+
+`Discovery` で文法ノードの探索範囲を選択する。
+
+| 値 | 動作 |
+|---|---|
+| `NamespaceAndTypeHierarchy`(既定) | ルートと同じ名前空間の `AstNode` 派生型、アセンブリ内のルート派生型、明示的な `[GrammarPart]` を収集する。 |
+| `TypeHierarchy` | 名前空間を走査せず、アセンブリ内のルート派生型と明示的な `[GrammarPart]` だけを収集する。 |
+| `Namespace` | 従来互換。同じ名前空間の `AstNode` 派生型と明示的な `[GrammarPart]` を収集する。 |
+
+```csharp
+[Grammar(Discovery = GrammarDiscovery.TypeHierarchy)]
+public abstract partial class Expr : AstNode { }
+
+// 別名前空間でも Expr 派生型なので自動収集される。
+public sealed partial class NumberExpr : Expr { }
+
+// Expr 派生でない共有ノードは明示的に参加させられる。
+[GrammarPart(typeof(Expr))]
+public sealed partial class SharedValue : AstNode { }
+```
+
### ParseMode(パーサの実行モード)
`ParseMode` 名前付きプロパティでパーサの実行モードを切り替えられる。既定は `Lalr`(確定 LALR(1))。
@@ -48,7 +71,7 @@ public abstract partial class Expr : AstNode { }
- **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 回復とは異なり、トークンを補完/削除してパースを続行する。同じ入力でもモードによりエラー位置・メッセージが変わる場合がある。
+- **エラー回復の挙動**: LALR・LightGlr とも Corchuelo ER1/ER2/ER3 でトークンを補完/削除してパースを続行する。GLRの分岐により、同じ入力でもモードごとにエラー位置・メッセージが変わる場合がある。
### ⚠ バージョン互換性のない変更 (0.4.0)
@@ -197,10 +220,11 @@ public sealed partial class NonEmpty : Program
## `[Skip]`
-スキップパターン(空白・コメント等)。`[Grammar]` を付けたクラスに併せて付ける。マッチした部分はトークン列から除外される。
+スキップパターン(空白・コメント等)。`[Grammar]` クラスに付けるとその文法へ、アセンブリに付けると全Grammarへ適用される。マッチした部分はトークン列から除外される。
```csharp
[Skip(@"(\s|//[^\n]*)+")] // 空白と行コメント
+[assembly: Skip(@"//[^\n]*")] // 全Grammar共通
```
## 規則の書き方
@@ -242,7 +266,7 @@ public sealed partial class AAdd : ABinary
- **`OnReduce(ctx)`**: 規則が reduce されたとき(ボトムアップ)に呼ばれる partial メソッド。子プロパティと `Span`(子から自動計算)が既に設定済み。`this.RuleName` で規則を判定、`Span` を上書きする等を行う。
- **Accept/Reject**: `IsAccepted` をオーバーライドして `false` を返すと、その reduce を拒否(Reject)しフォールバック候補を試す。詳細は [README](../../README.md) の「Accept/Reject とフォールバック」。
- **`OnSecondPass`**: 2パス目のトラバーサル(トップダウン)。`IOnSecondPassEnter`/`IOnSecondPassExit` を実装したノードに対し、`OnSecondPassEnter`(子の前)→ 子再帰 → `OnSecondPassExit`(子の後)を自動呼出。
-- **`[OnReduce]` / `[Enter]` / `[Exit]` 属性**: `[Grammar]` ルートクラスの `static` メソッドに付けると、Generator が Walker / コンストラクタに dispatch する(ctx キャストは自動注入)。`[OnReduce]` は reduce 時、`[Enter]`/`[Exit]` は 2パス目。詳細は [意味解析ガイド](semantic-analysis.md)。
+- **`[OnReduce]` / `[Enter]` / `[Exit]` 属性**: `[Grammar]` ルートクラスの `static` メソッドに付けると、Generator が `[OnReduce]` を Parser の reduce 処理から、`[Enter]`/`[Exit]` を Walker から dispatch する(ctx キャストは自動注入)。`[OnReduce]` は reduce 時、`[Enter]`/`[Exit]` は 2パス目。詳細は [意味解析ガイド](semantic-analysis.md)。
## 複数方言(Mode)
diff --git a/src/AstFirst.Core/Parsing/Grammar.cs b/src/AstFirst.Core/Parsing/Grammar.cs
index d8786cf..ce121d3 100644
--- a/src/AstFirst.Core/Parsing/Grammar.cs
+++ b/src/AstFirst.Core/Parsing/Grammar.cs
@@ -32,15 +32,17 @@ public Grammar(
IReadOnlyList? unreachableNonTerminals = null,
IReadOnlyList? undefinedNonTerminals = null)
{
- Productions = productions;
- Symbols = symbols;
+ Productions = productions.ToArray();
+ Symbols = symbols.ToArray();
StartSymbol = startSymbol;
AugmentedStart = augmentedStart;
EndOfFile = endOfFile;
AugmentedProduction = augmentedProduction;
- TerminalPrecedence = terminalPrecedence ?? new Dictionary();
- UnreachableNonTerminals = unreachableNonTerminals ?? Array.Empty();
- UndefinedNonTerminals = undefinedNonTerminals ?? Array.Empty();
+ TerminalPrecedence = terminalPrecedence is null
+ ? new Dictionary()
+ : terminalPrecedence.ToDictionary(pair => pair.Key, pair => pair.Value);
+ UnreachableNonTerminals = unreachableNonTerminals?.ToArray() ?? Array.Empty();
+ UndefinedNonTerminals = undefinedNonTerminals?.ToArray() ?? Array.Empty();
}
}
@@ -98,7 +100,6 @@ public Grammar Build(Symbol startSymbol)
var augStart = GetOrAdd(startSymbol.Name + "'", isTerminal: false);
var eof = GetOrAdd("$", isTerminal: true);
var augProd = new Core.Parsing.Production(_productions.Count, augStart, new[] { startSymbol, eof });
- _productions.Add(augProd);
// 到達不能/未定義非終端を検出。
var reachable = ComputeReachable(startSymbol);
@@ -112,7 +113,8 @@ public Grammar Build(Symbol startSymbol)
if (!s.IsTerminal) rightSideNonTerminals.Add(s);
var undefined = rightSideNonTerminals.Where(nt => !lhsNonTerminals.Contains(nt)).ToList();
- return new Grammar(_productions, _symbolList, startSymbol, augStart, eof, augProd, _terminalPrecedence, unreachable, undefined);
+ var productions = new List(_productions) { augProd };
+ return new Grammar(productions, _symbolList, startSymbol, augStart, eof, augProd, _terminalPrecedence, unreachable, undefined);
}
/// 開始記号から到達可能な非終端を BFS で集める。
diff --git a/src/AstFirst.Core/Parsing/Production.cs b/src/AstFirst.Core/Parsing/Production.cs
index c5d41a5..d127d64 100644
--- a/src/AstFirst.Core/Parsing/Production.cs
+++ b/src/AstFirst.Core/Parsing/Production.cs
@@ -1,3 +1,4 @@
+using System;
namespace AstFirst.Core.Parsing;
/// 生成規則 LHS -> Rhs[0] Rhs[1] ...。Tag には AST クラス等のメタ情報を載せる。
@@ -16,7 +17,8 @@ public Production(int id, Symbol lhs, Symbol[] rhs, object? tag = null, Preceden
{
Id = id;
Lhs = lhs;
- Rhs = rhs;
+ if (rhs is null) throw new ArgumentNullException(nameof(rhs));
+ Rhs = (Symbol[])rhs.Clone();
Tag = tag;
RulePrecedence = rulePrecedence;
}
diff --git a/src/AstFirst.Generator/AstFirst.Generator.csproj b/src/AstFirst.Generator/AstFirst.Generator.csproj
index b9eff73..c57007f 100644
--- a/src/AstFirst.Generator/AstFirst.Generator.csproj
+++ b/src/AstFirst.Generator/AstFirst.Generator.csproj
@@ -50,4 +50,11 @@
+
+
+
+
+
+
+
diff --git a/src/AstFirst.Generator/CodeEmitter.cs b/src/AstFirst.Generator/CodeEmitter.cs
index 30edbb2..de103a7 100644
--- a/src/AstFirst.Generator/CodeEmitter.cs
+++ b/src/AstFirst.Generator/CodeEmitter.cs
@@ -89,7 +89,7 @@ public static (string ns, string type) SplitFullName(string fullName)
/// [Rule] 引数名 (lowerCamel) をプロパティ名 (PascalCase) に。空なら "Item"。
public static string Pascalize(string? s)
- => string.IsNullOrEmpty(s) ? "Item" : char.ToUpperInvariant(s[0]) + s.Substring(1);
+ => string.IsNullOrEmpty(s) ? "Item" : char.ToUpperInvariant(s![0]) + s.Substring(1);
private static string Escape(string s) => s.Replace("\\", "\\\\").Replace("\"", "\\\"");
}
diff --git a/src/AstFirst.Generator/GlrParserEmitter.cs b/src/AstFirst.Generator/GlrParserEmitter.cs
index e98adab..378aa61 100644
--- a/src/AstFirst.Generator/GlrParserEmitter.cs
+++ b/src/AstFirst.Generator/GlrParserEmitter.cs
@@ -185,7 +185,7 @@ private static void EmitGlrHelpers(StringBuilder sb, Grammar grammar, GrammarMod
{
case ReduceActionModel action:
{
- sb.Append(" case ").Append(prod.Id).Append(": { return new ").Append(action.AstTypeName).Append("(\"").Append(action.RuleName).Append("\"");
+ sb.Append(" case ").Append(prod.Id).Append(": { var __node = new ").Append(action.AstTypeName).Append("(\"").Append(action.RuleName).Append("\"");
for (int j = 0; j < action.Parameters.Count; j++)
{
sb.Append(", ");
@@ -197,7 +197,9 @@ private static void EmitGlrHelpers(StringBuilder sb, Grammar grammar, GrammarMod
}
else sb.Append("(").Append(p.CastTypeName).Append(")children[").Append(p.ChildIndex).Append("]!");
}
- sb.AppendLine("); }");
+ sb.Append("); ");
+ ParserEmitter.EmitOnReduceAnalyzeRules(sb, model, action.AstTypeName, "__node");
+ sb.AppendLine("return __node; }");
break;
}
case ListReduceActionModel listAction:
diff --git a/src/AstFirst.Generator/ModelExtraction.cs b/src/AstFirst.Generator/ModelExtraction.cs
index 6558a3e..00ce145 100644
--- a/src/AstFirst.Generator/ModelExtraction.cs
+++ b/src/AstFirst.Generator/ModelExtraction.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
@@ -15,14 +16,24 @@ public static class ModelExtraction
private const string TokenFullName = "AstFirst.Token";
private const string SemanticContextFullName = "AstFirst.SemanticContext";
- public static GrammarModel? Extract(GeneratorAttributeSyntaxContext context)
+ public static ImmutableArray ExtractAll(GeneratorAttributeSyntaxContext context)
{
- if (context.TargetSymbol is not INamedTypeSymbol rootType) return null;
+ if (context.TargetSymbol is not INamedTypeSymbol rootType) return ImmutableArray.Empty;
var location = context.TargetNode?.GetLocation();
- return Extract(context.SemanticModel.Compilation, rootType, location);
+ var models = ImmutableArray.CreateBuilder(context.Attributes.Length);
+ foreach (var grammarAttribute in context.Attributes)
+ models.Add(Extract(context.SemanticModel.Compilation, rootType, location, grammarAttribute));
+ return models.MoveToImmutable();
}
public static GrammarModel Extract(Compilation compilation, INamedTypeSymbol rootType, Location? rootLocation = null)
+ {
+ var grammarAttribute = rootType.GetAttributes()
+ .LastOrDefault(attribute => IsAstFirstAttribute(attribute, "GrammarAttribute"));
+ return Extract(compilation, rootType, rootLocation, grammarAttribute);
+ }
+
+ private static GrammarModel Extract(Compilation compilation, INamedTypeSymbol rootType, Location? rootLocation, AttributeData? grammarAttribute)
{
var astNodeBase = compilation.GetTypeByMetadataName(AstNodeFullName);
var tokenBase = compilation.GetTypeByMetadataName(TokenFullName);
@@ -30,26 +41,39 @@ public static GrammarModel Extract(Compilation compilation, INamedTypeSymbol roo
var secondPassEnter = compilation.GetTypeByMetadataName("AstFirst.IOnSecondPassEnter");
var secondPassExit = compilation.GetTypeByMetadataName("AstFirst.IOnSecondPassExit");
+ string? mode = null;
+ var parseMode = ParseMode.Lalr;
+ var discovery = GrammarDiscovery.NamespaceAndTypeHierarchy;
+ if (grammarAttribute is not null)
+ foreach (var na in grammarAttribute.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;
+ if (na.Key == "Discovery" && na.Value.Value is int d) discovery = (GrammarDiscovery)d;
+ }
+
var nodes = new List();
var tokenDefs = new List();
var tokenDerivedWarnings = new List();
- foreach (var type in GetAllTypes(compilation.Assembly.GlobalNamespace))
+ var allTypes = GetAllTypes(compilation.Assembly.GlobalNamespace).ToList();
+ foreach (var type in allTypes)
{
if (type.TypeKind != Microsoft.CodeAnalysis.TypeKind.Class) continue;
if (type.DeclaredAccessibility != Accessibility.Public) continue;
- // rootType と同じ名前空間の型のみ収集 (別文法の混入を防ぎ、到達不能/未定義検知の誤検知を避ける)。
- if (!SymbolEqualityComparer.Default.Equals(type.ContainingNamespace, rootType.ContainingNamespace)) continue;
-
- if (astNodeBase is not null && InheritsFrom(type, astNodeBase))
- nodes.Add(ExtractNode(type, contextBase, astNodeBase, tokenBase, secondPassEnter, secondPassExit));
- if (tokenBase is not null && InheritsFrom(type, tokenBase))
+ bool sameNamespace = SymbolEqualityComparer.Default.Equals(type.ContainingNamespace, rootType.ContainingNamespace);
+ bool inRootHierarchy = InheritsFromOrEquals(type, rootType);
+ bool explicitPart = IsGrammarPart(type, rootType);
+ bool includeNode = discovery switch
{
- // G7: Token派生型に (string) コンストラクタが必要 (new DerivedType(token.Text) の生成)。
- if (!HasStringConstructor(type))
- tokenDerivedWarnings.Add(type.ToDisplayString());
- }
+ GrammarDiscovery.TypeHierarchy => inRootHierarchy || explicitPart,
+ GrammarDiscovery.Namespace => sameNamespace || explicitPart,
+ _ => sameNamespace || inRootHierarchy || explicitPart,
+ };
+
+ if (includeNode && astNodeBase is not null && InheritsFrom(type, astNodeBase))
+ nodes.Add(ExtractNode(type, contextBase, astNodeBase, tokenBase, secondPassEnter, secondPassExit));
}
nodes.Sort((a, b) => string.CompareOrdinal(a.FullName, b.FullName));
@@ -62,28 +86,30 @@ public static GrammarModel Extract(Compilation compilation, INamedTypeSymbol roo
foreach (var td in ExtractTokenDefsFromRules(n))
tokenDefs.Add(td);
+ // 文法で実際に参照される Token 派生型だけを検証する。Token の名前空間には依存しない。
+ var usedTokenTypes = new HashSet();
+ foreach (var n in nodes)
+ foreach (var r in n.Rules)
+ foreach (var p in r.Parameters)
+ if (p.IsToken && p.TypeFullName != TokenFullName)
+ usedTokenTypes.Add(p.TypeFullName);
+ foreach (var type in allTypes)
+ if (usedTokenTypes.Contains(type.ToDisplayString()) && !HasStringConstructor(type))
+ tokenDerivedWarnings.Add(type.ToDisplayString());
+
// [Skip] パターン ([Grammar] クラスまたはアセンブリ) を収集。
var skipPatterns = new List();
foreach (var a in rootType.GetAttributes())
- if (a.AttributeClass?.Name == "SkipAttribute" && a.ConstructorArguments.Length > 0 && a.ConstructorArguments[0].Value is string ss)
+ if (IsAstFirstAttribute(a, "SkipAttribute") && a.ConstructorArguments.Length > 0 && a.ConstructorArguments[0].Value is string ss)
+ skipPatterns.Add(ss);
+ foreach (var a in compilation.Assembly.GetAttributes())
+ if (IsAstFirstAttribute(a, "SkipAttribute") && a.ConstructorArguments.Length > 0 && a.ConstructorArguments[0].Value is string ss)
skipPatterns.Add(ss);
-
- // [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, parseMode);
+ return new GrammarModel(rootType.ToDisplayString(), nodes, Dedup(tokenDefs), skipPatterns, mode, rootLocation, tokenDerivedWarnings, analyzeRules, parseMode, discovery);
}
/// [OnReduce]/[Enter]/[Exit] 属性付き意味解析ルール ([Grammar] ルートクラスの static メソッド) を収集。
@@ -132,7 +158,7 @@ private static NodeModel ExtractNode(INamedTypeSymbol type, INamedTypeSymbol? co
var precAssoc = AstFirst.Core.Parsing.Associativity.Left;
foreach (var a in type.GetAttributes())
{
- if (a.AttributeClass?.Name != "PrecedenceAttribute") continue;
+ if (!IsAstFirstAttribute(a, "PrecedenceAttribute")) continue;
if (a.ConstructorArguments.Length > 0 && a.ConstructorArguments[0].Value is int pr) precPriority = pr;
foreach (var na in a.NamedArguments)
{
@@ -198,7 +224,7 @@ private static (string? regex, int priority, string? kind) GetPattern(Microsoft.
{
foreach (var a in symbol.GetAttributes())
{
- if (a.AttributeClass?.Name is not ("PatternAttribute" or "TokenAttribute")) continue;
+ if (!IsAstFirstAttribute(a, "PatternAttribute") && !IsAstFirstAttribute(a, "TokenAttribute")) continue;
string? regex = a.ConstructorArguments.Length > 0 && a.ConstructorArguments[0].Value is string s ? s : null;
int priority = 0;
string? kind = null;
@@ -217,7 +243,7 @@ private static int GetRepeatMin(Microsoft.CodeAnalysis.ISymbol symbol)
{
foreach (var a in symbol.GetAttributes())
{
- if (a.AttributeClass?.Name != "RepeatAttribute") continue;
+ if (!IsAstFirstAttribute(a, "RepeatAttribute")) continue;
int min = 1;
foreach (var na in a.NamedArguments)
if (na.Key == "Min" && na.Value.Value is int m) min = m;
@@ -263,7 +289,24 @@ private static IEnumerable ExtractTokenDefsFromRules(NodeModel no
private static bool HasAttribute(Microsoft.CodeAnalysis.ISymbol symbol, string attrName)
{
foreach (var a in symbol.GetAttributes())
- if (a.AttributeClass?.Name == attrName) return true;
+ if (IsAstFirstAttribute(a, attrName)) return true;
+ return false;
+ }
+
+ private static bool IsAstFirstAttribute(AttributeData attribute, string attributeName)
+ => attribute.AttributeClass?.ToDisplayString() == "AstFirst." + attributeName;
+
+ private static bool IsGrammarPart(INamedTypeSymbol type, INamedTypeSymbol rootType)
+ {
+ foreach (var attribute in type.GetAttributes())
+ {
+ if (!IsAstFirstAttribute(attribute, "GrammarPartAttribute")
+ || attribute.ConstructorArguments.Length == 0)
+ continue;
+ if (attribute.ConstructorArguments[0].Value is INamedTypeSymbol configuredRoot
+ && SymbolEqualityComparer.Default.Equals(configuredRoot, rootType))
+ return true;
+ }
return false;
}
@@ -280,12 +323,14 @@ private static bool InheritsFrom(ITypeSymbol type, INamedTypeSymbol baseType)
return false;
}
- /// (string) を1つ取る public コンストラクタがあるか (G7: new DerivedType(token.Text) の生成に必要)。
+ /// 生成された Parser から呼び出せる (string) コンストラクタがあるか。
private static bool HasStringConstructor(INamedTypeSymbol type)
{
foreach (var ctor in type.Constructors)
{
- if (ctor.IsStatic || ctor.DeclaredAccessibility == Accessibility.Private) continue;
+ if (ctor.IsStatic
+ || ctor.DeclaredAccessibility is not (Accessibility.Public or Accessibility.Internal or Accessibility.ProtectedOrInternal))
+ continue;
var parms = ctor.Parameters;
if (parms.Length == 1 && parms[0].Type.SpecialType == SpecialType.System_String)
return true;
diff --git a/src/AstFirst.Generator/Models.cs b/src/AstFirst.Generator/Models.cs
index 4d704b8..937a8f8 100644
--- a/src/AstFirst.Generator/Models.cs
+++ b/src/AstFirst.Generator/Models.cs
@@ -15,6 +15,14 @@ public enum ParseMode
LightGlr,
}
+/// Runtime の AstFirst.GrammarDiscovery と値を一致させる。
+public enum GrammarDiscovery
+{
+ NamespaceAndTypeHierarchy,
+ TypeHierarchy,
+ Namespace,
+}
+
/// DSL から抽出した文法モデル。等価比較可能 (IncrementalGenerator のキャッシュ判定用)。
/// シンボル/構文ノードは一切持たず、文字列/整数/bool のみ。
public sealed class GrammarModel : IEquatable
@@ -22,6 +30,7 @@ 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 GrammarDiscovery Discovery { get; }
public IReadOnlyList Nodes { get; }
public IReadOnlyList TokenDefs { get; }
public IReadOnlyList SkipPatterns { get; }
@@ -36,7 +45,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,
- ParseMode parseMode = ParseMode.Lalr)
+ ParseMode parseMode = ParseMode.Lalr,
+ GrammarDiscovery discovery = GrammarDiscovery.NamespaceAndTypeHierarchy)
{
RootTypeFullName = rootTypeFullName;
Nodes = nodes;
@@ -47,12 +57,17 @@ public GrammarModel(string rootTypeFullName, IReadOnlyList nodes, IRe
TokenDerivedWarnings = tokenDerivedWarnings ?? Array.Empty();
AnalyzeRules = analyzeRules ?? Array.Empty();
ParseMode = parseMode;
+ Discovery = discovery;
}
public bool Equals(GrammarModel? other) =>
other is not null && RootTypeFullName == other.RootTypeFullName
+ && Mode == other.Mode
&& SeqEqual(Nodes, other.Nodes) && SeqEqual(TokenDefs, other.TokenDefs)
- && SeqEqual(AnalyzeRules, other.AnalyzeRules) && ParseMode == other.ParseMode;
+ && SeqEqual(SkipPatterns, other.SkipPatterns)
+ && SeqEqual(TokenDerivedWarnings, other.TokenDerivedWarnings)
+ && SeqEqual(AnalyzeRules, other.AnalyzeRules) && ParseMode == other.ParseMode
+ && Discovery == other.Discovery;
/// いずれかのノードが IOnSecondPassEnter/Exit を実装するか、[Enter]/[Exit] ルールがあるか。
/// いずれもなければ Walker/Walk を生成しない (空走査回避・ゼロコスト)。
@@ -73,8 +88,13 @@ 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 < TokenDefs.Count; i++) h = unchecked(h * 31 + TokenDefs[i].GetHashCode());
+ for (int i = 0; i < SkipPatterns.Count; i++) h = unchecked(h * 31 + StringComparer.Ordinal.GetHashCode(SkipPatterns[i]));
+ if (Mode is not null) h = unchecked(h * 31 + StringComparer.Ordinal.GetHashCode(Mode));
+ for (int i = 0; i < TokenDerivedWarnings.Count; i++) h = unchecked(h * 31 + StringComparer.Ordinal.GetHashCode(TokenDerivedWarnings[i]));
for (int i = 0; i < AnalyzeRules.Count; i++) h = unchecked(h * 31 + AnalyzeRules[i].GetHashCode());
h = unchecked(h * 31 + (int)ParseMode);
+ h = unchecked(h * 31 + (int)Discovery);
return h;
}
@@ -233,7 +253,7 @@ public bool Equals(ChildModel? other) =>
/// 意味解析ルールのフェーズ。
public enum AnalyzePhase
{
- /// 1パス目: reduce 時 (ボトムアップ)。[OnReduce] 属性。Walker 不要 (コンストラクタ経路)。
+ /// 1パス目: reduce 時 (ボトムアップ)。[OnReduce] 属性。Parser の reduce 経路から呼び出す。
OnReduce,
/// 2パス目: ノードに入る時 (トップダウン)。[Enter] 属性。Walker の Enter フェーズ。
Enter,
diff --git a/src/AstFirst.Generator/ParserEmitter.cs b/src/AstFirst.Generator/ParserEmitter.cs
index b511d7f..b6305a7 100644
--- a/src/AstFirst.Generator/ParserEmitter.cs
+++ b/src/AstFirst.Generator/ParserEmitter.cs
@@ -305,7 +305,7 @@ private static void EmitHelpers(StringBuilder sb, Grammar grammar, GrammarModel
case ReduceActionModel action:
{
int len = prod.Rhs.Length;
- sb.Append(" case ").Append(prod.Id).Append(": { return new ").Append(action.AstTypeName).Append("(\"").Append(action.RuleName).Append("\"");
+ sb.Append(" case ").Append(prod.Id).Append(": { var __node = new ").Append(action.AstTypeName).Append("(\"").Append(action.RuleName).Append("\"");
for (int j = 0; j < action.Parameters.Count; j++)
{
sb.Append(", ");
@@ -319,7 +319,9 @@ private static void EmitHelpers(StringBuilder sb, Grammar grammar, GrammarModel
}
else sb.Append("(").Append(p.CastTypeName).Append(")values[top - ").Append(len).Append(" + ").Append(p.ChildIndex).Append("]!");
}
- sb.AppendLine("); }");
+ sb.Append("); ");
+ EmitOnReduceAnalyzeRules(sb, model, action.AstTypeName, "__node");
+ sb.AppendLine("return __node; }");
break;
}
case ListReduceActionModel listAction:
@@ -424,7 +426,7 @@ public static string EmitPartial(GrammarModel model, NodeModel node, string ns)
var typeStr = p.IsRepeat
? "System.Collections.Generic.IReadOnlyList<" + p.TypeFullName + ">"
: p.TypeFullName;
- sb.AppendLine(" public readonly " + typeStr + " " + prop + ";");
+ sb.AppendLine(" public readonly " + typeStr + " " + prop + " = default!;");
}
// RuleName: 抽象基底、または継承プロパティがない (基底が RuleName を持たない) 場合のみ生成。
if (node.Rules.Count > 0 && (isAbstractBase || !hasInherited))
@@ -510,20 +512,22 @@ public static string EmitPartial(GrammarModel model, NodeModel node, string ns)
sb.AppendLine(" if (__autoSpanHas) Span = __autoSpan;");
}
sb.AppendLine(" OnReduce(" + ctxCall + ");");
- // [OnReduce] 属性付き意味解析ルール ([Grammar] ルートクラスの static メソッド)。partial OnReduce の直後に呼ぶ (共存)。
- foreach (var ar in model.AnalyzeRules)
- {
- 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, (AstFirst.SemanticContext)" + ctxCall + ");");
- }
sb.AppendLine(" }");
}
sb.AppendLine("}");
return sb.ToString();
}
+ internal static void EmitOnReduceAnalyzeRules(StringBuilder sb, GrammarModel model, string nodeTypeName, string nodeVariable)
+ {
+ foreach (var rule in model.AnalyzeRules)
+ {
+ if (rule.Phase != AnalyzePhase.OnReduce || rule.TargetNodeFullName != nodeTypeName) continue;
+ sb.Append(rule.GrammarClassFullName).Append('.').Append(rule.MethodName)
+ .Append('(').Append(nodeVariable).Append(", (").Append(rule.CtxTypeFullName).Append(")ctx); ");
+ }
+ }
+
/// ノードの [Rule] の最後の SemanticContext 派生引数の型。なければ null (ctx なし)。
private static string? CtxTypeOf(NodeModel n)
{
diff --git a/src/AstFirst.Generator/ParserGenerator.cs b/src/AstFirst.Generator/ParserGenerator.cs
index 96cfe31..7a73d00 100644
--- a/src/AstFirst.Generator/ParserGenerator.cs
+++ b/src/AstFirst.Generator/ParserGenerator.cs
@@ -19,17 +19,21 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
.ForAttributeWithMetadataName(
GrammarAttributeFullName,
predicate: (node, _) => node is ClassDeclarationSyntax,
- transform: (ctx, _) => ModelExtraction.Extract(ctx))
- .Where(m => m is not null)
- .Select((m, _) => m!);
+ transform: (ctx, _) => ModelExtraction.ExtractAll(ctx))
+ .SelectMany((items, _) => items);
// Lexer と Parser を生成。
context.RegisterSourceOutput(models.Collect(), (spc, modelArray) =>
{
+ var emittedModels = new HashSet();
+ var emittedPartials = new HashSet();
foreach (var model in modelArray)
{
var (ns, typeName) = CodeEmitter.SplitFullName(model.RootTypeFullName);
var suffix = string.IsNullOrEmpty(model.Mode) ? "" : "_" + model.Mode;
+ var modelKey = model.RootTypeFullName + "\0" + model.Mode;
+ if (!emittedModels.Add(modelKey)) continue;
+ var modelHintName = HintNamePart(modelKey);
// テーブルと DFA を1回だけ構築し、Lexer/Parser の生成で共有 (重複ビルドを避ける)。
var (grammar, table) = ModelToTable.BuildWithGrammar(model);
@@ -51,23 +55,41 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
foreach (var tdw in model.TokenDerivedWarnings)
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", model.ParseMode == ParseMode.LightGlr
+ spc.AddSource(modelHintName + "_Lexer.g.cs", CodeEmitter.EmitLexer(model, dfa, rules, typeName + suffix + "Lexer", ns));
+ spc.AddSource(modelHintName + "_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));
+ spc.AddSource(modelHintName + "_Walker.g.cs", WalkerEmitter.EmitWalker(model, ns));
// 各ノードの partial (子プロパティ + OnReduce/OnSecondPass 宣言 + partial コンストラクタ)。
// [Rule] を持つ抽象基底 (中間抽象のプロパティ宣言) は protected コンストラクタ生成のため partial 必要。
// [Rule] のない抽象クラスはフィールド/コンストラクタがないので partial 不要。
foreach (var node in model.Nodes)
{
if (node.IsAbstract && node.Rules.Count == 0) continue;
- var simple = CodeEmitter.SplitFullName(node.FullName).type;
- spc.AddSource(typeName + suffix + "_" + simple + ".partial.g.cs", ParserEmitter.EmitPartial(model, node, ns));
+ var partialKey = node.FullName;
+ if (!emittedPartials.Add(partialKey)) continue;
+ var hintName = HintNamePart(node.FullName);
+ spc.AddSource(modelHintName + "_" + hintName + ".partial.g.cs", ParserEmitter.EmitPartial(model, node, ns));
}
}
});
}
+
+ private static string HintNamePart(string fullName)
+ {
+ var chars = fullName.ToCharArray();
+ uint hash = 2166136261;
+ unchecked
+ {
+ for (int i = 0; i < chars.Length; i++)
+ {
+ hash = (hash ^ chars[i]) * 16777619;
+ if (!char.IsLetterOrDigit(chars[i]) && chars[i] != '_')
+ chars[i] = '_';
+ }
+ }
+ return new string(chars) + "_" + hash.ToString("x8");
+ }
}
diff --git a/src/AstFirst.Runtime/Attributes.cs b/src/AstFirst.Runtime/Attributes.cs
index aefa3dd..d10dc84 100644
--- a/src/AstFirst.Runtime/Attributes.cs
+++ b/src/AstFirst.Runtime/Attributes.cs
@@ -24,6 +24,17 @@ public enum ParseMode
LightGlr,
}
+/// 文法に含めるノードを探索する方法。
+public enum GrammarDiscovery
+{
+ /// 同じ名前空間の AstNode 派生型に加え、別名前空間のルート派生型と明示的な GrammarPart を含める。
+ NamespaceAndTypeHierarchy,
+ /// 名前空間を使わず、ルート派生型と明示的な GrammarPart だけを含める。
+ TypeHierarchy,
+ /// 従来互換: 同じ名前空間の AstNode 派生型と明示的な GrammarPart を含める。
+ Namespace,
+}
+
/// 文法の開始記号 (ルート非終端) のクラスに付ける。Generator の抽出開始点。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
public sealed class GrammarAttribute : Attribute
@@ -33,6 +44,18 @@ public sealed class GrammarAttribute : Attribute
/// パーサの実行モード。既定は (確定 LALR(1))。
/// は軽量 GLR (コンフリクトを並行 fork で解決)。
public ParseMode ParseMode { get; set; } = ParseMode.Lalr;
+ /// 文法ノードの探索方法。
+ public GrammarDiscovery Discovery { get; set; } = GrammarDiscovery.NamespaceAndTypeHierarchy;
+}
+
+///
+/// 名前空間や型階層だけでは発見できない AST ノードを、指定した文法へ明示的に参加させる。
+/// 複数文法への参加も可能。
+///
+[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)]
+public sealed class GrammarPartAttribute(Type grammarRoot) : Attribute
+{
+ public Type GrammarRoot { get; } = grammarRoot;
}
/// スキップパターン (空白・コメント等)。クラスまたはアセンブリに付ける。
diff --git a/src/AstFirst.Runtime/ScopedSymbolTable.cs b/src/AstFirst.Runtime/ScopedSymbolTable.cs
index 7c8d188..844d786 100644
--- a/src/AstFirst.Runtime/ScopedSymbolTable.cs
+++ b/src/AstFirst.Runtime/ScopedSymbolTable.cs
@@ -100,7 +100,7 @@ public ScopedSymbolTable()
}
/// キー+種類付きの子スコープを開き、それを にする。同名種類の複数スコープもキーで区別。
- public Scope PushScope(string key, ScopeKind kind)
+ public Scope PushScope(string? key, ScopeKind kind)
{
Current = new Scope(Current, Current.Depth + 1, key, kind);
return Current;
diff --git a/src/AstFirst/MiniLang/MiniLang.cs b/src/AstFirst/MiniLang/MiniLang.cs
index 85ae6d8..0c5aed1 100644
--- a/src/AstFirst/MiniLang/MiniLang.cs
+++ b/src/AstFirst/MiniLang/MiniLang.cs
@@ -11,7 +11,7 @@ public abstract partial class Stmt : AstNode { }
/// let x = expr;
public sealed partial class LetStmt : Stmt
{
- public string Name { get; private set; }
+ public string Name { get; private set; } = "";
[Rule]
public static void Let([Token(@"let", Priority = 1)] Token kw, [Token(@"[A-Za-z_]\w*")] Token nameTok,
[Token(@"=")] Token eq, Expr value, [Token(@";")] Token semi) { }
@@ -43,7 +43,7 @@ partial void OnReduce()
public sealed partial class VarExpr : Expr
{
- public string Name { get; private set; }
+ public string Name { get; private set; } = "";
[Rule]
public static void VarToken([Token(@"[A-Za-z_]\w*")] Token nameTok) { }
partial void OnReduce()
diff --git a/tests/AstFirst.Generator.Tests/ConflictDiagnosticTests.cs b/tests/AstFirst.Generator.Tests/ConflictDiagnosticTests.cs
index e4ddb1b..1d106ab 100644
--- a/tests/AstFirst.Generator.Tests/ConflictDiagnosticTests.cs
+++ b/tests/AstFirst.Generator.Tests/ConflictDiagnosticTests.cs
@@ -19,18 +19,22 @@ namespace AstFirst {
public abstract class AstNode { }
public abstract class Token { }
public abstract class SemanticContext { }
- [System.AttributeUsage(System.AttributeTargets.Class)]
- public class GrammarAttribute : System.Attribute { }
+ [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)]
+ public class GrammarAttribute : System.Attribute { public string Mode { get; set; } }
[System.AttributeUsage(System.AttributeTargets.Parameter)]
public class PatternAttribute : System.Attribute { public PatternAttribute(string regex) {} }
+ [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)]
+ public class GrammarPartAttribute : System.Attribute { public GrammarPartAttribute(System.Type root) {} }
[System.AttributeUsage(System.AttributeTargets.Class)]
public class PrecedenceAttribute : System.Attribute { public PrecedenceAttribute(int priority) {} }
[System.AttributeUsage(System.AttributeTargets.Method)]
public class RuleAttribute : System.Attribute { }
+ [System.AttributeUsage(System.AttributeTargets.Method)]
+ public class OnReduceAttribute : System.Attribute { }
}
";
- private static IReadOnlyList RunGenerator(string grammar)
+ private static GeneratorDriverRunResult RunGeneratorResult(string grammar)
{
var trusted = (string)System.AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES")!;
var refs = trusted.Split(Path.PathSeparator)
@@ -42,9 +46,12 @@ public class RuleAttribute : System.Attribute { }
CSharpGeneratorDriver driver = CSharpGeneratorDriver.Create(new ParserGenerator());
driver = (CSharpGeneratorDriver)driver.RunGenerators(compilation);
- return driver.GetRunResult().Diagnostics;
+ return driver.GetRunResult();
}
+ private static IReadOnlyList RunGenerator(string grammar)
+ => RunGeneratorResult(grammar).Diagnostics;
+
[Fact]
public void AmbiguousGrammarEmitsAstf001()
{
@@ -110,7 +117,7 @@ public void UndefinedNonTerminalEmitsAstf003()
[AstFirst.Grammar]
public abstract class Root : AstFirst.AstNode { }
public class A : Root {
- public A(B b) { }
+ [AstFirst.Rule] public static void Reduce(B b) { }
}
public abstract class B : AstFirst.AstNode { }
";
@@ -127,7 +134,7 @@ public void TokenDerivedWithoutStringCtorEmitsAstf004()
[AstFirst.Grammar]
public class RootExpr : AstFirst.AstNode { }
public class Num : RootExpr {
- public Num([AstFirst.Pattern(""[0-9]+"")] AstFirst.Token n) { }
+ [AstFirst.Rule] public static void Reduce([AstFirst.Pattern(""[0-9]+"")] IntToken n) { }
}
public class IntToken : AstFirst.Token {
public IntToken([AstFirst.Pattern(""[0-9]+"")] int n) { }
@@ -136,4 +143,133 @@ public IntToken([AstFirst.Pattern(""[0-9]+"")] int n) { }
var diagnostics = RunGenerator(grammar);
Assert.Contains(diagnostics, d => d.Id == "ASTF004");
}
+
+ [Fact]
+ public void TokenDerivedWithProtectedStringCtorEmitsAstf004()
+ {
+ var grammar = @"
+[AstFirst.Grammar]
+public class RootExpr : AstFirst.AstNode { }
+public class Num : RootExpr {
+ [AstFirst.Rule] public static void Reduce([AstFirst.Pattern(""[0-9]+"")] ProtectedToken n) { }
+}
+public class ProtectedToken : AstFirst.Token {
+ protected ProtectedToken(string text) { }
+}
+";
+
+ var diagnostics = RunGenerator(grammar);
+
+ Assert.Contains(diagnostics, d => d.Id == "ASTF004");
+ }
+
+ [Fact]
+ public void SameSimpleNodeNameInDifferentNamespacesUsesUniqueHintNames()
+ {
+ var grammar = @"
+namespace RootNs {
+ [AstFirst.Grammar]
+ public abstract partial class Expr : AstFirst.AstNode { }
+}
+namespace FirstNs {
+ public sealed partial class Number : RootNs.Expr {
+ [AstFirst.Rule] public static void Reduce([AstFirst.Pattern(""1"")] AstFirst.Token n) { }
+ }
+}
+namespace SecondNs {
+ public sealed partial class Number : RootNs.Expr {
+ [AstFirst.Rule] public static void Reduce([AstFirst.Pattern(""2"")] AstFirst.Token n) { }
+ }
+}
+";
+
+ var result = RunGeneratorResult(grammar);
+
+ Assert.DoesNotContain(result.Diagnostics, d => d.Severity == DiagnosticSeverity.Error);
+ var hintNames = result.Results.Single().GeneratedSources.Select(s => s.HintName).ToList();
+ Assert.Equal(hintNames.Count, hintNames.Distinct().Count());
+ Assert.Contains(hintNames, name => name.Contains("FirstNs_Number"));
+ Assert.Contains(hintNames, name => name.Contains("SecondNs_Number"));
+ }
+
+ [Fact]
+ public void SameSimpleRootNameInDifferentNamespacesUsesUniqueHintNames()
+ {
+ var grammar = @"
+namespace FirstNs {
+ [AstFirst.Grammar]
+ public abstract partial class Expr : AstFirst.AstNode { }
+}
+namespace SecondNs {
+ [AstFirst.Grammar]
+ public abstract partial class Expr : AstFirst.AstNode { }
+}
+";
+
+ var result = RunGeneratorResult(grammar);
+ var generated = result.Results.Single().GeneratedSources;
+
+ Assert.DoesNotContain(result.Diagnostics, d => d.Id == "CS8785");
+ Assert.Equal(4, generated.Length);
+ Assert.Equal(generated.Length, generated.Select(source => source.HintName).Distinct().Count());
+ }
+
+ [Fact]
+ public void MultipleGrammarModesGenerateEachParserWithoutDuplicatePartials()
+ {
+ var grammar = @"
+[AstFirst.Grammar(Mode = ""A"")]
+[AstFirst.Grammar(Mode = ""B"")]
+public abstract partial class Expr : AstFirst.AstNode { }
+public sealed partial class Number : Expr {
+ [AstFirst.Rule] public static void Reduce([AstFirst.Pattern(""[0-9]+"")] AstFirst.Token n) { }
+}
+";
+
+ var result = RunGeneratorResult(grammar);
+ var generated = result.Results.Single().GeneratedSources;
+ var sourceText = generated.Select(source => source.SourceText.ToString()).ToList();
+
+ Assert.DoesNotContain(result.Diagnostics, d => d.Id == "CS8785");
+ Assert.Contains(sourceText, source => source.Contains("class Expr_ALexer"));
+ Assert.Contains(sourceText, source => source.Contains("class Expr_AParser"));
+ Assert.Contains(sourceText, source => source.Contains("class Expr_BLexer"));
+ Assert.Contains(sourceText, source => source.Contains("class Expr_BParser"));
+ Assert.Single(generated.Where(source => source.SourceText.ToString().Contains("partial class Number")));
+ }
+
+ [Fact]
+ public void GrammarPartSharedByTwoRootsEmitsOnePartialAndGrammarSpecificHooks()
+ {
+ var grammar = @"
+namespace FirstNs {
+ [AstFirst.Grammar]
+ public abstract partial class Root : AstFirst.AstNode {
+ [AstFirst.OnReduce] public static void Analyze(Shared.Value node, AstFirst.SemanticContext ctx) { }
+ }
+}
+namespace SecondNs {
+ [AstFirst.Grammar]
+ public abstract partial class Root : AstFirst.AstNode {
+ [AstFirst.OnReduce] public static void Analyze(Shared.Value node, AstFirst.SemanticContext ctx) { }
+ }
+}
+namespace Shared {
+ [AstFirst.GrammarPart(typeof(FirstNs.Root))]
+ [AstFirst.GrammarPart(typeof(SecondNs.Root))]
+ public sealed partial class Value : AstFirst.AstNode {
+ [AstFirst.Rule] public static void Reduce([AstFirst.Pattern(""value"")] AstFirst.Token token, AstFirst.SemanticContext ctx) { }
+ }
+}
+";
+
+ var result = RunGeneratorResult(grammar);
+ var generated = result.Results.Single().GeneratedSources;
+ var sourceText = generated.Select(source => source.SourceText.ToString()).ToList();
+
+ Assert.DoesNotContain(result.Diagnostics, d => d.Id == "CS8785");
+ Assert.Single(sourceText, source => source.Contains("partial class Value"));
+ Assert.Contains(sourceText, source => source.Contains("FirstNs.Root.Analyze(__node"));
+ Assert.Contains(sourceText, source => source.Contains("SecondNs.Root.Analyze(__node"));
+ }
}
diff --git a/tests/AstFirst.Generator.Tests/ModelExtractionTests.cs b/tests/AstFirst.Generator.Tests/ModelExtractionTests.cs
index 5896f34..e68034b 100644
--- a/tests/AstFirst.Generator.Tests/ModelExtractionTests.cs
+++ b/tests/AstFirst.Generator.Tests/ModelExtractionTests.cs
@@ -121,6 +121,21 @@ public void ModelIsEquatable()
Assert.Equal(a.GetHashCode(), b.GetHashCode());
}
+ [Fact]
+ public void ModelEqualityIncludesIncrementalGeneratorInputs()
+ {
+ var nodes = new List();
+ var tokens = new List();
+ var baseline = new GrammarModel("Expr", nodes, tokens);
+
+ Assert.NotEqual(baseline, new GrammarModel("Expr", nodes, tokens, skipPatterns: new[] { "\\s+" }));
+ Assert.NotEqual(baseline, new GrammarModel("Expr", nodes, tokens, mode: "V2"));
+ Assert.NotEqual(baseline, new GrammarModel("Expr", nodes, tokens,
+ tokenDerivedWarnings: new[] { "MissingStringCtorToken" }));
+ Assert.NotEqual(baseline, new GrammarModel("Expr", nodes, tokens,
+ discovery: AstFirst.Generator.GrammarDiscovery.TypeHierarchy));
+ }
+
private static GrammarModel ExtractSource(string source, string rootName)
{
var comp = CreateCompilation(source);
@@ -145,6 +160,19 @@ public sealed class A : S { public A([Pattern(""a"")] Token t) { } }
Assert.Contains(model.SkipPatterns, p => p == "\\s+");
}
+ [Fact]
+ public void AssemblySkipPatternCollected()
+ {
+ var source = @"
+using AstFirst;
+[assembly: Skip(""//[^\\n]*"")]
+[Grammar]
+public abstract class S : AstNode { }
+";
+ var model = ExtractSource(source, "S");
+ Assert.Contains("//[^\\n]*", model.SkipPatterns);
+ }
+
[Fact]
public void ModeExtracted()
{
@@ -166,4 +194,139 @@ public void ModeDefaultsToNull()
var model = Extract();
Assert.Null(model.Mode);
}
+
+ [Fact]
+ public void DefaultDiscoveryIncludesRootDescendantsFromOtherNamespaces()
+ {
+ var source = @"
+using AstFirst;
+namespace RootNs { [Grammar] public abstract partial class Expr : AstNode { } }
+namespace NodeNs {
+ public sealed partial class Num : RootNs.Expr {
+ [Rule] public static void Reduce([Token(""[0-9]+"")] Token value) { }
+ }
+}
+";
+ var model = ExtractSource(source, "RootNs.Expr");
+ Assert.Contains(model.Nodes, n => n.FullName == "NodeNs.Num");
+ Assert.Equal(AstFirst.Generator.GrammarDiscovery.NamespaceAndTypeHierarchy, model.Discovery);
+ }
+
+ [Fact]
+ public void TypeHierarchyDiscoveryDoesNotScanTheNamespace()
+ {
+ var source = @"
+using AstFirst;
+namespace Shared {
+ [Grammar(Discovery = AstFirst.GrammarDiscovery.TypeHierarchy)]
+ public abstract partial class Expr : AstNode { }
+ public sealed partial class Unrelated : AstNode {
+ [Rule] public static void Reduce([Token(""x"")] Token value) { }
+ }
+}
+namespace Nodes {
+ public sealed partial class Num : Shared.Expr {
+ [Rule] public static void Reduce([Token(""[0-9]+"")] Token value) { }
+ }
+}
+";
+ var model = ExtractSource(source, "Shared.Expr");
+ Assert.Contains(model.Nodes, n => n.FullName == "Nodes.Num");
+ Assert.DoesNotContain(model.Nodes, n => n.FullName == "Shared.Unrelated");
+ Assert.Equal(AstFirst.Generator.GrammarDiscovery.TypeHierarchy, model.Discovery);
+ }
+
+ [Fact]
+ public void GrammarPartCanIncludeANodeOutsideNamespaceAndHierarchy()
+ {
+ var source = @"
+using AstFirst;
+namespace RootNs {
+ [Grammar(Discovery = AstFirst.GrammarDiscovery.TypeHierarchy)]
+ public abstract partial class Expr : AstNode { }
+}
+namespace SharedNodes {
+ [GrammarPart(typeof(RootNs.Expr))]
+ public sealed partial class SharedValue : AstNode {
+ [Rule] public static void Reduce([Token(""value"")] Token value) { }
+ }
+}
+";
+ var model = ExtractSource(source, "RootNs.Expr");
+ Assert.Contains(model.Nodes, n => n.FullName == "SharedNodes.SharedValue");
+ }
+
+ [Fact]
+ public void UnrelatedGrammarPartAttributeIsIgnored()
+ {
+ var source = @"
+using AstFirst;
+namespace Other {
+ [System.AttributeUsage(System.AttributeTargets.Class)]
+ public sealed class GrammarPartAttribute : System.Attribute {
+ public GrammarPartAttribute(System.Type grammarRoot) { }
+ }
+}
+namespace RootNs {
+ [Grammar(Discovery = AstFirst.GrammarDiscovery.TypeHierarchy)]
+ public abstract partial class Expr : AstNode { }
+}
+namespace SharedNodes {
+ [Other.GrammarPart(typeof(RootNs.Expr))]
+ public sealed partial class SharedValue : AstNode {
+ [Rule] public static void Reduce([Token(""value"")] Token value) { }
+ }
+}
+";
+
+ var model = ExtractSource(source, "RootNs.Expr");
+
+ Assert.DoesNotContain(model.Nodes, n => n.FullName == "SharedNodes.SharedValue");
+ }
+
+ [Fact]
+ public void UnrelatedGrammarAndAssemblySkipAttributesAreIgnored()
+ {
+ var source = @"
+using AstFirst;
+[assembly: Other.Skip(""not-a-lexer-pattern"")]
+namespace Other {
+ [System.AttributeUsage(System.AttributeTargets.Class)]
+ public sealed class GrammarAttribute : System.Attribute {
+ public string Mode { get; set; } = """";
+ }
+ [System.AttributeUsage(System.AttributeTargets.Assembly)]
+ public sealed class SkipAttribute : System.Attribute {
+ public SkipAttribute(string value) { }
+ }
+}
+[AstFirst.Grammar(Mode = ""Real"")]
+[Other.Grammar(Mode = ""Fake"")]
+public abstract partial class Expr : AstFirst.AstNode { }
+";
+
+ var model = ExtractSource(source, "Expr");
+
+ Assert.Equal("Real", model.Mode);
+ Assert.Empty(model.SkipPatterns);
+ }
+
+ [Fact]
+ public void NamespaceDiscoveryRetainsTheLegacyBoundary()
+ {
+ var source = @"
+using AstFirst;
+namespace RootNs {
+ [Grammar(Discovery = AstFirst.GrammarDiscovery.Namespace)]
+ public abstract partial class Expr : AstNode { }
+}
+namespace NodeNs {
+ public sealed partial class Num : RootNs.Expr {
+ [Rule] public static void Reduce([Token(""[0-9]+"")] Token value) { }
+ }
+}
+";
+ var model = ExtractSource(source, "RootNs.Expr");
+ Assert.DoesNotContain(model.Nodes, n => n.FullName == "NodeNs.Num");
+ }
}
diff --git a/tests/AstFirst.Tests/AstFirst.Tests.csproj b/tests/AstFirst.Tests/AstFirst.Tests.csproj
index bf05061..dc56827 100644
--- a/tests/AstFirst.Tests/AstFirst.Tests.csproj
+++ b/tests/AstFirst.Tests/AstFirst.Tests.csproj
@@ -6,6 +6,8 @@
enable
enable
false
+
+ $(NoWarn);ASTF001
diff --git a/tests/AstFirst.Tests/Parsing/GrammarTests.cs b/tests/AstFirst.Tests/Parsing/GrammarTests.cs
index 50655e9..e5008f9 100644
--- a/tests/AstFirst.Tests/Parsing/GrammarTests.cs
+++ b/tests/AstFirst.Tests/Parsing/GrammarTests.cs
@@ -125,4 +125,35 @@ public void CompleteGrammarHasNoUnreachableOrUndefined()
Assert.Empty(g.UnreachableNonTerminals);
Assert.Empty(g.UndefinedNonTerminals);
}
+
+ [Fact]
+ public void BuildReturnsAnImmutableSnapshotAndCanBeRepeated()
+ {
+ var builder = new GrammarBuilder();
+ var start = builder.NonTerminal("S");
+ var token = builder.Terminal("a");
+ builder.Production(start, token);
+
+ var first = builder.Build(start);
+ var second = builder.Build(start);
+
+ Assert.Equal(2, first.Productions.Count);
+ Assert.Equal(2, second.Productions.Count);
+ Assert.Equal(first.Productions.Count, second.Productions.Count);
+ }
+
+ [Fact]
+ public void ProductionCopiesItsRightHandSide()
+ {
+ var builder = new GrammarBuilder();
+ var start = builder.NonTerminal("S");
+ var first = builder.Terminal("a");
+ var replacement = builder.Terminal("b");
+ var rhs = new[] { first };
+
+ var production = new Production(0, start, rhs);
+ rhs[0] = replacement;
+
+ Assert.Equal(first, production.Rhs[0]);
+ }
}
diff --git a/tests/AstFirst.Tests/Runtime/ScopedSymbolTableTests.cs b/tests/AstFirst.Tests/Runtime/ScopedSymbolTableTests.cs
index 258f8c0..9c19204 100644
--- a/tests/AstFirst.Tests/Runtime/ScopedSymbolTableTests.cs
+++ b/tests/AstFirst.Tests/Runtime/ScopedSymbolTableTests.cs
@@ -202,6 +202,6 @@ public void ResolveOrError_Undeclared_AddsDiagnostic_ReturnsNull()
var sym = t.ResolveOrError("missing", Span(0), bag);
Assert.Null(sym);
Assert.True(bag.HasErrors);
- Assert.Equal(1, bag.Items.Count);
+ Assert.Single(bag.Items);
}
}