Skip to content

Commit 28aca6c

Browse files
authored
Sisco/fix editor replace method (#30)
* issue reproduction tests * refactor the entire node query system ti fix fundamental shortcomings and discrepancies * docs: update project todo
1 parent 2484cfa commit 28aca6c

17 files changed

Lines changed: 2388 additions & 739 deletions
Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
using BenchmarkDotNet.Attributes;
2+
using BenchmarkDotNet.Configs;
3+
using TinyTokenizer.Ast;
4+
using Q = TinyTokenizer.Ast.Query;
5+
6+
namespace TinyTokenizer.Benchmarks;
7+
8+
/// <summary>
9+
/// Benchmarks for SyntaxEditor operations including replacements, insertions,
10+
/// and region resolution on various tree sizes and depths.
11+
/// </summary>
12+
[MemoryDiagnoser]
13+
[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)]
14+
[CategoriesColumn]
15+
public class SyntaxEditorBenchmarks
16+
{
17+
#region Test Data
18+
19+
/// <summary>
20+
/// Simple expression for single-token replacements.
21+
/// </summary>
22+
private const string SimpleExpression = "foo + bar * baz";
23+
24+
/// <summary>
25+
/// Empty block for inner replacement benchmarks.
26+
/// </summary>
27+
private const string EmptyBlockInput = "function test() { }";
28+
29+
/// <summary>
30+
/// Block with content for inner replacement benchmarks.
31+
/// </summary>
32+
private const string BlockWithContent = """
33+
function test() {
34+
var x = 1;
35+
var y = 2;
36+
return x + y;
37+
}
38+
""";
39+
40+
/// <summary>
41+
/// Multiple functions for multi-position insertions.
42+
/// </summary>
43+
private static readonly string MultiFunctionInput = GenerateMultiFunctionInput();
44+
45+
/// <summary>
46+
/// Deeply nested blocks for deep tree traversal.
47+
/// </summary>
48+
private static readonly string DeeplyNestedInput = GenerateDeeplyNestedInput(20);
49+
50+
/// <summary>
51+
/// Very deeply nested blocks for stress testing.
52+
/// </summary>
53+
private static readonly string VeryDeeplyNestedInput = GenerateDeeplyNestedInput(50);
54+
55+
private static readonly Schema DefaultSchema = Schema.Create()
56+
.AddCommentStyles(CommentStyle.CStyleSingleLine, CommentStyle.CStyleMultiLine)
57+
.WithOperators(CommonOperators.CFamily)
58+
.Build();
59+
60+
// Pre-parsed trees
61+
private static readonly SyntaxTree SimpleTree = SyntaxTree.Parse(SimpleExpression, DefaultSchema);
62+
private static readonly SyntaxTree EmptyBlockTree = SyntaxTree.Parse(EmptyBlockInput, DefaultSchema);
63+
private static readonly SyntaxTree BlockWithContentTree = SyntaxTree.Parse(BlockWithContent, DefaultSchema);
64+
private static readonly SyntaxTree MultiFunctionTree = SyntaxTree.Parse(MultiFunctionInput, DefaultSchema);
65+
private static readonly SyntaxTree DeeplyNestedTree = SyntaxTree.Parse(DeeplyNestedInput, DefaultSchema);
66+
private static readonly SyntaxTree VeryDeeplyNestedTree = SyntaxTree.Parse(VeryDeeplyNestedInput, DefaultSchema);
67+
68+
private static string GenerateMultiFunctionInput()
69+
{
70+
var template = """
71+
function func{0}(a, b) {{
72+
return a + b;
73+
}}
74+
75+
""";
76+
77+
return string.Concat(Enumerable.Range(0, 50).Select(i =>
78+
string.Format(template, i)));
79+
}
80+
81+
private static string GenerateDeeplyNestedInput(int depth)
82+
{
83+
var open = string.Concat(Enumerable.Repeat("{ ", depth));
84+
var close = string.Concat(Enumerable.Repeat(" }", depth));
85+
return $"function deep() {open}x{close}";
86+
}
87+
88+
#endregion
89+
90+
#region Replace - Single Token
91+
92+
/// <summary>
93+
/// Replace a single identifier token with another identifier.
94+
/// Baseline for minimal edit operation.
95+
/// </summary>
96+
[Benchmark(Baseline = true)]
97+
[BenchmarkCategory("Replace")]
98+
public SyntaxTree Replace_SingleToken()
99+
{
100+
var tree = SyntaxTree.Parse(SimpleExpression, DefaultSchema);
101+
tree.CreateEditor()
102+
.Replace(Q.Ident("foo"), "replaced")
103+
.Commit();
104+
return tree;
105+
}
106+
107+
/// <summary>
108+
/// Replace multiple tokens in a single edit batch.
109+
/// </summary>
110+
[Benchmark]
111+
[BenchmarkCategory("Replace")]
112+
public SyntaxTree Replace_MultipleTokens()
113+
{
114+
var tree = SyntaxTree.Parse(SimpleExpression, DefaultSchema);
115+
tree.CreateEditor()
116+
.Replace(Q.Ident("foo"), "a")
117+
.Replace(Q.Ident("bar"), "b")
118+
.Replace(Q.Ident("baz"), "c")
119+
.Commit();
120+
return tree;
121+
}
122+
123+
/// <summary>
124+
/// Replace all identifiers using a single query.
125+
/// </summary>
126+
[Benchmark]
127+
[BenchmarkCategory("Replace")]
128+
public SyntaxTree Replace_AllIdents()
129+
{
130+
var tree = SyntaxTree.Parse(SimpleExpression, DefaultSchema);
131+
tree.CreateEditor()
132+
.Replace(Q.AnyIdent, "x")
133+
.Commit();
134+
return tree;
135+
}
136+
137+
#endregion
138+
139+
#region Replace - Block Inner
140+
141+
/// <summary>
142+
/// Replace the inner content of an empty block.
143+
/// Tests zero-width region handling.
144+
/// </summary>
145+
[Benchmark]
146+
[BenchmarkCategory("BlockInner")]
147+
public SyntaxTree Replace_BlockInner_Empty()
148+
{
149+
var tree = SyntaxTree.Parse(EmptyBlockInput, DefaultSchema);
150+
tree.CreateEditor()
151+
.Replace(Q.BraceBlock.First().Inner(), "return 42;")
152+
.Commit();
153+
return tree;
154+
}
155+
156+
/// <summary>
157+
/// Replace the inner content of a block with existing content.
158+
/// Tests multi-slot region replacement.
159+
/// </summary>
160+
[Benchmark]
161+
[BenchmarkCategory("BlockInner")]
162+
public SyntaxTree Replace_BlockInner_WithContent()
163+
{
164+
var tree = SyntaxTree.Parse(BlockWithContent, DefaultSchema);
165+
tree.CreateEditor()
166+
.Replace(Q.BraceBlock.First().Inner(), "return 0;")
167+
.Commit();
168+
return tree;
169+
}
170+
171+
/// <summary>
172+
/// Replace inner content with larger replacement text.
173+
/// Tests tree rebuilding with size change.
174+
/// </summary>
175+
[Benchmark]
176+
[BenchmarkCategory("BlockInner")]
177+
public SyntaxTree Replace_BlockInner_Larger()
178+
{
179+
var tree = SyntaxTree.Parse(EmptyBlockInput, DefaultSchema);
180+
var largeContent = string.Join("\n", Enumerable.Range(0, 20).Select(i => $"var x{i} = {i};"));
181+
tree.CreateEditor()
182+
.Replace(Q.BraceBlock.First().Inner(), largeContent)
183+
.Commit();
184+
return tree;
185+
}
186+
187+
#endregion
188+
189+
#region Insert - Multiple Positions
190+
191+
/// <summary>
192+
/// Insert after a single position.
193+
/// </summary>
194+
[Benchmark]
195+
[BenchmarkCategory("Insert")]
196+
public SyntaxTree InsertAfter_SinglePosition()
197+
{
198+
var tree = SyntaxTree.Parse(MultiFunctionInput, DefaultSchema);
199+
tree.CreateEditor()
200+
.InsertAfter(Q.BraceBlock.First().Start(), "// inserted\n")
201+
.Commit();
202+
return tree;
203+
}
204+
205+
/// <summary>
206+
/// Insert after all function body starts (50 positions).
207+
/// Tests edit batching with many positions.
208+
/// </summary>
209+
[Benchmark]
210+
[BenchmarkCategory("Insert")]
211+
public SyntaxTree InsertAfter_ManyPositions()
212+
{
213+
var tree = SyntaxTree.Parse(MultiFunctionInput, DefaultSchema);
214+
tree.CreateEditor()
215+
.InsertAfter(Q.BraceBlock.Start(), "// inserted\n")
216+
.Commit();
217+
return tree;
218+
}
219+
220+
/// <summary>
221+
/// Insert before all closing braces.
222+
/// </summary>
223+
[Benchmark]
224+
[BenchmarkCategory("Insert")]
225+
public SyntaxTree InsertBefore_ManyPositions()
226+
{
227+
var tree = SyntaxTree.Parse(MultiFunctionInput, DefaultSchema);
228+
tree.CreateEditor()
229+
.InsertBefore(Q.BraceBlock.End(), "\n// end")
230+
.Commit();
231+
return tree;
232+
}
233+
234+
#endregion
235+
236+
#region SelectRegions - Deep Trees
237+
238+
/// <summary>
239+
/// Select regions in a moderately deep tree (20 levels).
240+
/// </summary>
241+
[Benchmark]
242+
[BenchmarkCategory("SelectRegions")]
243+
public int SelectRegions_DeepTree()
244+
{
245+
var count = 0;
246+
foreach (var _ in ((IRegionQuery)Q.AnyIdent).SelectRegions(DeeplyNestedTree))
247+
{
248+
count++;
249+
}
250+
return count;
251+
}
252+
253+
/// <summary>
254+
/// Select regions in a very deep tree (50 levels).
255+
/// </summary>
256+
[Benchmark]
257+
[BenchmarkCategory("SelectRegions")]
258+
public int SelectRegions_VeryDeepTree()
259+
{
260+
var count = 0;
261+
foreach (var _ in ((IRegionQuery)Q.AnyIdent).SelectRegions(VeryDeeplyNestedTree))
262+
{
263+
count++;
264+
}
265+
return count;
266+
}
267+
268+
/// <summary>
269+
/// Select block regions in a deep tree.
270+
/// Tests BlockNodeQuery optimization.
271+
/// </summary>
272+
[Benchmark]
273+
[BenchmarkCategory("SelectRegions")]
274+
public int SelectRegions_DeepTree_Blocks()
275+
{
276+
var count = 0;
277+
foreach (var _ in ((IRegionQuery)Q.BraceBlock).SelectRegions(DeeplyNestedTree))
278+
{
279+
count++;
280+
}
281+
return count;
282+
}
283+
284+
/// <summary>
285+
/// Select first match only - tests short-circuit.
286+
/// </summary>
287+
[Benchmark]
288+
[BenchmarkCategory("SelectRegions")]
289+
public int SelectRegions_DeepTree_First()
290+
{
291+
var count = 0;
292+
foreach (var _ in ((IRegionQuery)Q.AnyIdent.First()).SelectRegions(VeryDeeplyNestedTree))
293+
{
294+
count++;
295+
}
296+
return count;
297+
}
298+
299+
#endregion
300+
301+
#region Edit with Transformer
302+
303+
/// <summary>
304+
/// Edit using a transformer function (uppercase identifiers).
305+
/// </summary>
306+
[Benchmark]
307+
[BenchmarkCategory("Transform")]
308+
public SyntaxTree Edit_Transform_SingleToken()
309+
{
310+
var tree = SyntaxTree.Parse(SimpleExpression, DefaultSchema);
311+
tree.CreateEditor()
312+
.Edit(Q.AnyIdent, text => text.ToUpperInvariant())
313+
.Commit();
314+
return tree;
315+
}
316+
317+
/// <summary>
318+
/// Edit using Replace with RedNode transformer.
319+
/// </summary>
320+
[Benchmark]
321+
[BenchmarkCategory("Transform")]
322+
public SyntaxTree Replace_WithNodeTransformer()
323+
{
324+
var tree = SyntaxTree.Parse(SimpleExpression, DefaultSchema);
325+
tree.CreateEditor()
326+
.Replace(Q.AnyIdent, node => node.ToText().ToUpperInvariant())
327+
.Commit();
328+
return tree;
329+
}
330+
331+
#endregion
332+
333+
#region Undo/Redo
334+
335+
/// <summary>
336+
/// Perform edit then undo.
337+
/// </summary>
338+
[Benchmark]
339+
[BenchmarkCategory("UndoRedo")]
340+
public SyntaxTree Edit_ThenUndo()
341+
{
342+
var tree = SyntaxTree.Parse(SimpleExpression, DefaultSchema);
343+
tree.CreateEditor()
344+
.Replace(Q.Ident("foo"), "replaced")
345+
.Commit();
346+
tree.Undo();
347+
return tree;
348+
}
349+
350+
/// <summary>
351+
/// Perform multiple edits then undo all.
352+
/// </summary>
353+
[Benchmark]
354+
[BenchmarkCategory("UndoRedo")]
355+
public SyntaxTree MultipleEdits_ThenUndoAll()
356+
{
357+
var tree = SyntaxTree.Parse(SimpleExpression, DefaultSchema);
358+
359+
for (int i = 0; i < 5; i++)
360+
{
361+
tree.CreateEditor()
362+
.Replace(Q.AnyIdent.First(), $"v{i}")
363+
.Commit();
364+
}
365+
366+
while (tree.CanUndo)
367+
{
368+
tree.Undo();
369+
}
370+
371+
return tree;
372+
}
373+
374+
#endregion
375+
}

0 commit comments

Comments
 (0)