Skip to content

Commit c268ebf

Browse files
authored
Fix LeftAssociative and Unary position reset (#301)
1 parent 4686495 commit c268ebf

File tree

5 files changed

+909
-42
lines changed

5 files changed

+909
-42
lines changed
Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
using Parlot.Compilation;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using System.Linq.Expressions;
6+
7+
namespace Parlot.Fluent;
8+
9+
/// <summary>
10+
/// A parser that creates a left-associative structure from a base parser and a list of operators.
11+
/// c.f. https://en.wikipedia.org/wiki/Operator_associativity
12+
/// </summary>
13+
/// <typeparam name="T">The type of the value being parsed.</typeparam>
14+
/// <typeparam name="TInput">The type of the operator parsers.</typeparam>
15+
public sealed class LeftAssociative<T, TInput> : Parser<T>, ICompilable
16+
{
17+
private readonly Parser<T> _parser;
18+
private readonly (Parser<TInput> Op, Func<T, T, T> Factory)[] _operators;
19+
20+
public LeftAssociative(Parser<T> parser, (Parser<TInput> op, Func<T, T, T> factory)[] operators)
21+
{
22+
_parser = parser ?? throw new ArgumentNullException(nameof(parser));
23+
_operators = operators ?? throw new ArgumentNullException(nameof(operators));
24+
25+
if (_operators.Length == 0)
26+
{
27+
throw new ArgumentException("At least one operator must be provided.", nameof(operators));
28+
}
29+
}
30+
31+
public override bool Parse(ParseContext context, ref ParseResult<T> result)
32+
{
33+
context.EnterParser(this);
34+
35+
// Parse the first operand (e.g., multiplicative)
36+
if (!_parser.Parse(context, ref result))
37+
{
38+
context.ExitParser(this);
39+
return false;
40+
}
41+
42+
var value = result.Value;
43+
var end = result.End;
44+
45+
// Parse zero or more (operator operand) pairs
46+
while (true)
47+
{
48+
var operatorPosition = context.Scanner.Cursor.Position;
49+
var operatorResult = new ParseResult<TInput>();
50+
Func<T, T, T>? matchedFactory = null;
51+
52+
// Try each operator
53+
foreach (var (op, factory) in _operators)
54+
{
55+
if (op.Parse(context, ref operatorResult))
56+
{
57+
matchedFactory = factory;
58+
break;
59+
}
60+
}
61+
62+
if (matchedFactory == null)
63+
{
64+
// No operator matched, we're done
65+
break;
66+
}
67+
68+
// Parse the right operand
69+
var rightResult = new ParseResult<T>();
70+
if (!_parser.Parse(context, ref rightResult))
71+
{
72+
// Operator matched but no right operand - rollback operator consumption.
73+
context.Scanner.Cursor.ResetPosition(operatorPosition);
74+
break;
75+
}
76+
77+
// Apply the operator
78+
value = matchedFactory(value, rightResult.Value);
79+
end = rightResult.End;
80+
}
81+
82+
result = new ParseResult<T>(result.Start, end, value);
83+
84+
context.ExitParser(this);
85+
return true;
86+
}
87+
88+
public CompilationResult Compile(CompilationContext context)
89+
{
90+
var result = context.CreateCompilationResult<T>();
91+
92+
var nextNum = context.NextNumber;
93+
var currentValue = result.DeclareVariable<T>($"leftAssocValue{nextNum}");
94+
var matchedFactory = result.DeclareVariable<Func<T, T, T>>($"matchedFactory{nextNum}");
95+
var operatorPosition = result.DeclareVariable<TextPosition>($"leftAssocPos{nextNum}");
96+
97+
var breakLabel = Expression.Label($"leftAssocBreak{nextNum}");
98+
99+
var firstParserResult = _parser.Build(context);
100+
var rightParserResult = _parser.Build(context);
101+
102+
// Build operator matching expressions - each operator sets matchedFactory if it matches
103+
var operatorChecks = new List<Expression>();
104+
var allOperatorVariables = new List<ParameterExpression>();
105+
106+
for (int i = 0; i < _operators.Length; i++)
107+
{
108+
var (op, factory) = _operators[i];
109+
var opCompileResult = op.Build(context);
110+
111+
foreach (var variable in opCompileResult.Variables)
112+
{
113+
allOperatorVariables.Add(variable);
114+
}
115+
116+
var factoryConst = Expression.Constant(factory);
117+
118+
if (i > 0)
119+
{
120+
operatorChecks.Add(
121+
Expression.IfThen(
122+
Expression.Equal(matchedFactory, Expression.Constant(null, typeof(Func<T, T, T>))),
123+
Expression.Block(
124+
opCompileResult.Body.Concat([
125+
Expression.IfThen(
126+
opCompileResult.Success,
127+
Expression.Assign(matchedFactory, factoryConst)
128+
)
129+
])
130+
)
131+
)
132+
);
133+
}
134+
else
135+
{
136+
operatorChecks.AddRange(opCompileResult.Body);
137+
operatorChecks.Add(
138+
Expression.IfThen(
139+
opCompileResult.Success,
140+
Expression.Assign(matchedFactory, factoryConst)
141+
)
142+
);
143+
}
144+
}
145+
146+
var scanner = Expression.Field(context.ParseContext, nameof(ParseContext.Scanner));
147+
var cursor = Expression.Field(scanner, nameof(Scanner.Cursor));
148+
var cursorPosition = Expression.Property(cursor, nameof(Cursor.Position));
149+
var resetPosition = typeof(Cursor).GetMethod(nameof(Cursor.ResetPosition), [typeof(TextPosition).MakeByRefType()])!;
150+
151+
// Build the loop body with its own variable scope
152+
var loopBody = Expression.Block(
153+
allOperatorVariables.Concat(rightParserResult.Variables),
154+
new Expression[]
155+
{
156+
Expression.Assign(matchedFactory, Expression.Constant(null, typeof(Func<T, T, T>))),
157+
Expression.Assign(operatorPosition, cursorPosition)
158+
}
159+
.Concat(operatorChecks)
160+
.Concat([
161+
Expression.IfThen(
162+
Expression.Equal(matchedFactory, Expression.Constant(null, typeof(Func<T, T, T>))),
163+
Expression.Break(breakLabel)
164+
)
165+
])
166+
.Concat(rightParserResult.Body)
167+
.Concat([
168+
Expression.IfThen(
169+
Expression.Not(rightParserResult.Success),
170+
Expression.Block(
171+
Expression.Call(cursor, resetPosition, operatorPosition),
172+
Expression.Break(breakLabel)
173+
)
174+
),
175+
Expression.Assign(currentValue,
176+
Expression.Invoke(matchedFactory, currentValue, rightParserResult.Value))
177+
])
178+
);
179+
180+
var loopExpr = Expression.Loop(loopBody, breakLabel);
181+
182+
result.Body.Add(
183+
Expression.Block(
184+
firstParserResult.Variables,
185+
new Expression[] { Expression.Block(firstParserResult.Body) }.Concat([
186+
Expression.IfThenElse(
187+
firstParserResult.Success,
188+
Expression.Block(
189+
Expression.Assign(currentValue, firstParserResult.Value),
190+
loopExpr,
191+
Expression.Assign(result.Success, Expression.Constant(true)),
192+
context.DiscardResult ? Expression.Empty() : Expression.Assign(result.Value, currentValue)
193+
),
194+
Expression.Assign(result.Success, Expression.Constant(false))
195+
)
196+
])
197+
)
198+
);
199+
200+
return result;
201+
}
202+
203+
public override string ToString() => Name ?? $"LeftAssociative({_parser})";
204+
}
205+
206+
public sealed class LeftAssociativeWithContext<T, TInput> : Parser<T>, ICompilable
207+
{
208+
private readonly Parser<T> _parser;
209+
private readonly (Parser<TInput> Op, Func<ParseContext, T, T, T> Factory)[] _operators;
210+
211+
public LeftAssociativeWithContext(Parser<T> parser, (Parser<TInput> op, Func<ParseContext, T, T, T> factory)[] operators)
212+
{
213+
_parser = parser ?? throw new ArgumentNullException(nameof(parser));
214+
_operators = operators ?? throw new ArgumentNullException(nameof(operators));
215+
216+
if (_operators.Length == 0)
217+
{
218+
throw new ArgumentException("At least one operator must be provided.", nameof(operators));
219+
}
220+
}
221+
222+
public override bool Parse(ParseContext context, ref ParseResult<T> result)
223+
{
224+
context.EnterParser(this);
225+
226+
if (!_parser.Parse(context, ref result))
227+
{
228+
context.ExitParser(this);
229+
return false;
230+
}
231+
232+
var value = result.Value;
233+
var end = result.End;
234+
235+
while (true)
236+
{
237+
var operatorPosition = context.Scanner.Cursor.Position;
238+
var operatorResult = new ParseResult<TInput>();
239+
Func<ParseContext, T, T, T>? matchedFactory = null;
240+
241+
foreach (var (op, factory) in _operators)
242+
{
243+
if (op.Parse(context, ref operatorResult))
244+
{
245+
matchedFactory = factory;
246+
break;
247+
}
248+
}
249+
250+
if (matchedFactory == null)
251+
{
252+
break;
253+
}
254+
255+
var rightResult = new ParseResult<T>();
256+
if (!_parser.Parse(context, ref rightResult))
257+
{
258+
context.Scanner.Cursor.ResetPosition(operatorPosition);
259+
break;
260+
}
261+
262+
value = matchedFactory(context, value, rightResult.Value);
263+
end = rightResult.End;
264+
}
265+
266+
result = new ParseResult<T>(result.Start, end, value);
267+
268+
context.ExitParser(this);
269+
return true;
270+
}
271+
272+
public CompilationResult Compile(CompilationContext context)
273+
{
274+
var result = context.CreateCompilationResult<T>();
275+
276+
var nextNum = context.NextNumber;
277+
var currentValue = result.DeclareVariable<T>($"leftAssocCtxValue{nextNum}");
278+
var matchedFactory = result.DeclareVariable<Func<ParseContext, T, T, T>>($"matchedFactoryCtx{nextNum}");
279+
var operatorPosition = result.DeclareVariable<TextPosition>($"leftAssocCtxPos{nextNum}");
280+
281+
var breakLabel = Expression.Label($"leftAssocCtxBreak{nextNum}");
282+
283+
var firstParserResult = _parser.Build(context);
284+
var rightParserResult = _parser.Build(context);
285+
286+
var operatorChecks = new List<Expression>();
287+
var allOperatorVariables = new List<ParameterExpression>();
288+
289+
for (int i = 0; i < _operators.Length; i++)
290+
{
291+
var (op, factory) = _operators[i];
292+
var opCompileResult = op.Build(context);
293+
294+
foreach (var variable in opCompileResult.Variables)
295+
{
296+
allOperatorVariables.Add(variable);
297+
}
298+
299+
var factoryConst = Expression.Constant(factory);
300+
301+
if (i > 0)
302+
{
303+
operatorChecks.Add(
304+
Expression.IfThen(
305+
Expression.Equal(matchedFactory, Expression.Constant(null, typeof(Func<ParseContext, T, T, T>))),
306+
Expression.Block(
307+
opCompileResult.Body.Concat([
308+
Expression.IfThen(
309+
opCompileResult.Success,
310+
Expression.Assign(matchedFactory, factoryConst)
311+
)
312+
])
313+
)
314+
)
315+
);
316+
}
317+
else
318+
{
319+
operatorChecks.AddRange(opCompileResult.Body);
320+
operatorChecks.Add(
321+
Expression.IfThen(
322+
opCompileResult.Success,
323+
Expression.Assign(matchedFactory, factoryConst)
324+
)
325+
);
326+
}
327+
}
328+
329+
var scanner = Expression.Field(context.ParseContext, nameof(ParseContext.Scanner));
330+
var cursor = Expression.Field(scanner, nameof(Scanner.Cursor));
331+
var cursorPosition = Expression.Property(cursor, nameof(Cursor.Position));
332+
var resetPosition = typeof(Cursor).GetMethod(nameof(Cursor.ResetPosition), [typeof(TextPosition).MakeByRefType()])!;
333+
334+
var loopBody = Expression.Block(
335+
allOperatorVariables.Concat(rightParserResult.Variables),
336+
new Expression[]
337+
{
338+
Expression.Assign(matchedFactory, Expression.Constant(null, typeof(Func<ParseContext, T, T, T>))),
339+
Expression.Assign(operatorPosition, cursorPosition)
340+
}
341+
.Concat(operatorChecks)
342+
.Concat([
343+
Expression.IfThen(
344+
Expression.Equal(matchedFactory, Expression.Constant(null, typeof(Func<ParseContext, T, T, T>))),
345+
Expression.Break(breakLabel)
346+
)
347+
])
348+
.Concat(rightParserResult.Body)
349+
.Concat([
350+
Expression.IfThen(
351+
Expression.Not(rightParserResult.Success),
352+
Expression.Block(
353+
Expression.Call(cursor, resetPosition, operatorPosition),
354+
Expression.Break(breakLabel)
355+
)
356+
),
357+
Expression.Assign(currentValue,
358+
Expression.Invoke(matchedFactory, context.ParseContext, currentValue, rightParserResult.Value))
359+
])
360+
);
361+
362+
var loopExpr = Expression.Loop(loopBody, breakLabel);
363+
364+
result.Body.Add(
365+
Expression.Block(
366+
firstParserResult.Variables,
367+
new Expression[] { Expression.Block(firstParserResult.Body) }.Concat([
368+
Expression.IfThenElse(
369+
firstParserResult.Success,
370+
Expression.Block(
371+
Expression.Assign(currentValue, firstParserResult.Value),
372+
loopExpr,
373+
Expression.Assign(result.Success, Expression.Constant(true)),
374+
context.DiscardResult ? Expression.Empty() : Expression.Assign(result.Value, currentValue)
375+
),
376+
Expression.Assign(result.Success, Expression.Constant(false))
377+
)
378+
])
379+
)
380+
);
381+
382+
return result;
383+
}
384+
385+
public override string ToString() => Name ?? $"LeftAssociative({_parser})";
386+
}

0 commit comments

Comments
 (0)