Skip to content

Commit b51f608

Browse files
konardclaude
andcommitted
Add standalone test projects for all C# PEG simplification solutions
This commit adds runnable test projects for each solution approach documented in the case study. Each project demonstrates the specific behavior (success, partial success, or failure) of that approach: - Solution 01: #parse{} expression - demonstrates PEG0011 build error - Solution 02: capture-validate - shows disambiguation failure - Solution 03: semantic predicates - shows input access limitation - Solution 04: hybrid approach - full working solution - Solution 05: minimized hybrid - optimized production implementation Each project can be run independently with: cd solutions/<name>/project && dotnet build && dotnet run 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 5607787 commit b51f608

File tree

17 files changed

+1087
-10
lines changed

17 files changed

+1087
-10
lines changed

docs/case-studies/csharp-peg-simplification/README.md

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,38 @@ highQuoteCapture <string> = raw:highQuoteDoubleRaw &{ ParseMultiQuoteString(raw,
192192
docs/case-studies/csharp-peg-simplification/
193193
├── README.md # This file
194194
├── timeline.md # Detailed timeline with timestamps
195-
├── root-causes.md # Deep dive into each root cause
196-
├── solutions/ # Experimental solutions
197-
│ ├── 01-parse-expression/ # #parse{} approach
198-
│ ├── 02-capture-validate/ # Capture-then-validate approach
199-
│ ├── 03-semantic-predicates/ # Semantic predicates approach
200-
│ └── 04-other-approaches/ # Other attempted solutions
201-
└── experiments/ # Standalone experiment files
195+
├── root-causes.md # Deep dive into each root cause
196+
└── solutions/ # All attempted solutions with runnable test projects
197+
├── 01-parse-expression/ # #parse{} approach (FAILED - PEG0011 error)
198+
│ ├── README.md
199+
│ └── project/ # Runnable test project demonstrating the error
200+
├── 02-capture-validate/ # Capture-then-validate (PARTIAL - disambiguation fails)
201+
│ ├── README.md
202+
│ └── project/ # Runnable test project
203+
├── 03-semantic-predicates/ # Semantic predicates (FAILED - no input access)
204+
│ ├── README.md
205+
│ └── project/ # Runnable test project
206+
├── 04-hybrid-approach/ # Hybrid N=1-5 explicit + N>=6 procedural (SUCCESS)
207+
│ ├── README.md
208+
│ └── project/ # Runnable test project
209+
└── 05-minimized-hybrid/ # CURRENT: N=1,2 explicit + N>=3 procedural (SUCCESS)
210+
├── README.md
211+
└── project/ # Runnable test project
202212
```
203213

214+
### Running the Test Projects
215+
216+
Each solution has a standalone test project. To run:
217+
218+
```bash
219+
cd solutions/<solution-name>/project
220+
dotnet build
221+
dotnet run
222+
```
223+
224+
Solution 01 will fail to build (demonstrating the PEG0011 error).
225+
Solutions 02-05 will build and run, showing their respective behaviors.
226+
204227
## References
205228

206229
- [Peggy.js Documentation](https://peggyjs.org/documentation.html)
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// This program demonstrates the #parse{} approach failure
2+
// When you run 'dotnet build', you will see error PEG0011
3+
4+
using System;
5+
6+
namespace TestParseExpression
7+
{
8+
class Program
9+
{
10+
static void Main(string[] args)
11+
{
12+
Console.WriteLine("=== Test: #parse{} Expression Approach ===");
13+
Console.WriteLine();
14+
Console.WriteLine("This test demonstrates that #parse{} expressions");
15+
Console.WriteLine("do NOT work with the <PegGrammar> MSBuild tag.");
16+
Console.WriteLine();
17+
Console.WriteLine("Expected build error:");
18+
Console.WriteLine(" error PEG0011: Unterminated code section.");
19+
Console.WriteLine();
20+
Console.WriteLine("If you see this message, the grammar compiled");
21+
Console.WriteLine("successfully, which means the bug may have been fixed!");
22+
Console.WriteLine();
23+
24+
// This code won't execute because the project won't compile
25+
// var parser = new QuoteParser();
26+
// var result = parser.Parse("\"hello\"");
27+
// Console.WriteLine($"Parsed: {result}");
28+
}
29+
}
30+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
@namespace TestParseExpression
2+
@classname QuoteParser
3+
@using System.Linq
4+
5+
@members
6+
{
7+
private string _parsedValue = "";
8+
private int _parsedLength;
9+
10+
/// <summary>
11+
/// Universal parser for N-quote strings.
12+
/// Handles any quote character and any number N of quotes.
13+
/// </summary>
14+
private bool ParseQuotedStringAt(string input, int startPos, char quoteChar)
15+
{
16+
if (startPos >= input.Length || input[startPos] != quoteChar)
17+
return false;
18+
19+
// Count opening quotes
20+
int quoteCount = 0;
21+
int pos = startPos;
22+
while (pos < input.Length && input[pos] == quoteChar)
23+
{
24+
quoteCount++;
25+
pos++;
26+
}
27+
28+
string closeSeq = new string(quoteChar, quoteCount);
29+
string escapeSeq = new string(quoteChar, quoteCount * 2);
30+
var content = new System.Text.StringBuilder();
31+
32+
while (pos < input.Length)
33+
{
34+
// Check for escape sequence (2*N quotes)
35+
if (pos + escapeSeq.Length <= input.Length &&
36+
input.Substring(pos, escapeSeq.Length) == escapeSeq)
37+
{
38+
content.Append(closeSeq);
39+
pos += escapeSeq.Length;
40+
continue;
41+
}
42+
43+
// Check for closing sequence (exactly N quotes)
44+
if (pos + quoteCount <= input.Length &&
45+
input.Substring(pos, quoteCount) == closeSeq)
46+
{
47+
int afterClose = pos + quoteCount;
48+
if (afterClose >= input.Length || input[afterClose] != quoteChar)
49+
{
50+
_parsedValue = content.ToString();
51+
_parsedLength = afterClose - startPos;
52+
return true;
53+
}
54+
}
55+
56+
content.Append(input[pos]);
57+
pos++;
58+
}
59+
return false;
60+
}
61+
}
62+
63+
document <string> = q:quoted { q }
64+
65+
// Universal quoted string - handles any N quotes
66+
// THIS DOES NOT WORK with <PegGrammar> tag due to PEG0011 error
67+
quoted <string> = doubleQuoted / singleQuoted / backtickQuoted
68+
69+
// THESE RULES USE #parse{} WHICH CAUSES PEG0011 ERROR
70+
// The #parse{} syntax allows custom procedural parsing but is not
71+
// properly supported when using the <PegGrammar> MSBuild tag.
72+
73+
doubleQuoted <string> = #parse{
74+
if (ParseQuotedStringAt(subject, startCursor.Location, '"'))
75+
{
76+
return new Pegasus.Common.ParseResult<string>(
77+
ref startCursor,
78+
startCursor.Advance(_parsedLength),
79+
_parsedValue
80+
);
81+
}
82+
return null;
83+
}
84+
85+
singleQuoted <string> = #parse{
86+
if (ParseQuotedStringAt(subject, startCursor.Location, '\''))
87+
{
88+
return new Pegasus.Common.ParseResult<string>(
89+
ref startCursor,
90+
startCursor.Advance(_parsedLength),
91+
_parsedValue
92+
);
93+
}
94+
return null;
95+
}
96+
97+
backtickQuoted <string> = #parse{
98+
if (ParseQuotedStringAt(subject, startCursor.Location, '`'))
99+
{
100+
return new Pegasus.Common.ParseResult<string>(
101+
ref startCursor,
102+
startCursor.Advance(_parsedLength),
103+
_parsedValue
104+
);
105+
}
106+
return null;
107+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<OutputType>Exe</OutputType>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<PackageReference Include="Pegasus" Version="4.1.0" />
10+
<!-- Using PegGrammar tag triggers PEG0011 error for #parse{} -->
11+
<PegGrammar Include="QuoteParser.peg" />
12+
</ItemGroup>
13+
</Project>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// This program demonstrates the capture-then-validate approach
2+
// It shows both SUCCESS (isolated strings) and FAILURE (disambiguation)
3+
4+
using System;
5+
6+
namespace TestCaptureValidate
7+
{
8+
class Program
9+
{
10+
static void Main(string[] args)
11+
{
12+
Console.WriteLine("=== Test: Capture-then-Validate Approach ===");
13+
Console.WriteLine();
14+
15+
var parser = new QuoteParser();
16+
17+
// Test cases that WORK (isolated strings)
18+
var successCases = new (string input, string expected)[]
19+
{
20+
("\"hello\"", "hello"),
21+
("\"\"world\"\"", "world"),
22+
("\"\"\"foo\"\"\"", "foo"),
23+
("'text'", "text"),
24+
("''escaped''", "escaped"),
25+
("`backtick`", "backtick"),
26+
("\"\"with \"\"\"\" escape\"\"", "with \"\" escape"),
27+
};
28+
29+
Console.WriteLine("=== Isolated String Tests (Expected: SUCCESS) ===");
30+
int passed = 0, failed = 0;
31+
foreach (var (input, expected) in successCases)
32+
{
33+
try
34+
{
35+
var result = parser.Parse(input);
36+
if (result == expected)
37+
{
38+
Console.WriteLine($"✓ {input}\"{result}\"");
39+
passed++;
40+
}
41+
else
42+
{
43+
Console.WriteLine($"✗ {input}\"{result}\" (expected: \"{expected}\")");
44+
failed++;
45+
}
46+
}
47+
catch (Exception ex)
48+
{
49+
Console.WriteLine($"✗ {input} → Error: {ex.Message}");
50+
failed++;
51+
}
52+
}
53+
54+
Console.WriteLine();
55+
Console.WriteLine("=== Multiple String Tests (Expected: FAILURE) ===");
56+
Console.WriteLine("These tests demonstrate the disambiguation problem:");
57+
Console.WriteLine();
58+
59+
// Test case that FAILS due to greedy disambiguation
60+
var multiInput = "\"first\" \"second\"";
61+
try
62+
{
63+
var result = parser.Parse(multiInput);
64+
Console.WriteLine($"Input: {multiInput}");
65+
Console.WriteLine($"Result: \"{result}\"");
66+
Console.WriteLine("PROBLEM: Greedy pattern captured from first \" to last \"");
67+
Console.WriteLine("Expected: Two separate strings \"first\" and \"second\"");
68+
}
69+
catch (Exception ex)
70+
{
71+
Console.WriteLine($"Input: {multiInput}");
72+
Console.WriteLine($"Error: {ex.Message}");
73+
Console.WriteLine("This failure is expected - greedy patterns can't disambiguate");
74+
}
75+
76+
Console.WriteLine();
77+
Console.WriteLine($"=== Summary ===");
78+
Console.WriteLine($"Isolated strings: {passed} passed, {failed} failed");
79+
Console.WriteLine();
80+
Console.WriteLine("CONCLUSION: Capture-then-validate works for isolated strings");
81+
Console.WriteLine("but FAILS for disambiguation of multiple quoted strings.");
82+
}
83+
}
84+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
@namespace TestCaptureValidate
2+
@classname QuoteParser
3+
@using System.Linq
4+
5+
@members
6+
{
7+
private string _parsedValue = "";
8+
9+
/// <summary>
10+
/// Parse captured text as an N-quote string.
11+
/// The captured text should include opening and closing quotes.
12+
/// </summary>
13+
private bool TryParseQuotedString(string capturedText, char quoteChar)
14+
{
15+
_parsedValue = "";
16+
if (string.IsNullOrEmpty(capturedText) || capturedText[0] != quoteChar)
17+
return false;
18+
19+
// Count opening quotes
20+
int quoteCount = 0;
21+
int pos = 0;
22+
while (pos < capturedText.Length && capturedText[pos] == quoteChar)
23+
{
24+
quoteCount++;
25+
pos++;
26+
}
27+
28+
string closeSeq = new string(quoteChar, quoteCount);
29+
string escapeSeq = new string(quoteChar, quoteCount * 2);
30+
var content = new System.Text.StringBuilder();
31+
32+
while (pos < capturedText.Length)
33+
{
34+
// Check for escape sequence (2*N quotes)
35+
if (pos + escapeSeq.Length <= capturedText.Length &&
36+
capturedText.Substring(pos, escapeSeq.Length) == escapeSeq)
37+
{
38+
content.Append(closeSeq);
39+
pos += escapeSeq.Length;
40+
continue;
41+
}
42+
43+
// Check for closing sequence
44+
if (pos + quoteCount <= capturedText.Length &&
45+
capturedText.Substring(pos, quoteCount) == closeSeq)
46+
{
47+
int afterClose = pos + quoteCount;
48+
if (afterClose >= capturedText.Length || capturedText[afterClose] != quoteChar)
49+
{
50+
// Valid closing - check if we consumed entire captured text
51+
if (afterClose == capturedText.Length)
52+
{
53+
_parsedValue = content.ToString();
54+
return true;
55+
}
56+
// Captured more than one quoted string (disambiguation problem)
57+
return false;
58+
}
59+
}
60+
61+
content.Append(capturedText[pos]);
62+
pos++;
63+
}
64+
return false;
65+
}
66+
}
67+
68+
// Entry point: parse a single quoted string
69+
document <string> = q:quoted { q }
70+
71+
// Try to parse quoted strings using capture-then-validate
72+
// NOTE: This has disambiguation problems with multiple quoted strings
73+
quoted <string> = doubleQuoted / singleQuoted / backtickQuoted
74+
75+
// Double quotes: capture greedy pattern, then validate
76+
doubleQuoted <string> = raw:doubleQuoteCaptureRaw &{ TryParseQuotedString(raw, '"') } { _parsedValue }
77+
78+
// Capture pattern for double quotes
79+
// Matches: one or more ", then content, then one or more "
80+
// WARNING: Greedy - will match from first " to LAST " in input
81+
doubleQuoteCaptureRaw <string> = "" ('"'+ doubleQuoteContent* '"'+)
82+
doubleQuoteContent = [^"] / '"'+ &[^"]
83+
84+
// Single quotes: same pattern
85+
singleQuoted <string> = raw:singleQuoteCaptureRaw &{ TryParseQuotedString(raw, '\'') } { _parsedValue }
86+
singleQuoteCaptureRaw <string> = "" ("'"+ singleQuoteContent* "'"+)
87+
singleQuoteContent = [^'] / "'"+ &[^']
88+
89+
// Backticks: same pattern
90+
backtickQuoted <string> = raw:backtickCaptureRaw &{ TryParseQuotedString(raw, '`') } { _parsedValue }
91+
backtickCaptureRaw <string> = "" ('`'+ backtickContent* '`'+)
92+
backtickContent = [^`] / '`'+ &[^`]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<TargetFramework>net8.0</TargetFramework>
4+
<OutputType>Exe</OutputType>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
<ItemGroup>
9+
<PackageReference Include="Pegasus" Version="4.1.0" />
10+
<PegGrammar Include="QuoteParser.peg" />
11+
</ItemGroup>
12+
</Project>

0 commit comments

Comments
 (0)