Skip to content

Commit

Permalink
Implement associative parsers (#116)
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastienros authored May 23, 2024
1 parent 26bbf26 commit 9aa53da
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 106 deletions.
85 changes: 29 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,23 @@ public static readonly Parser<Expression> Expression;
static FluentParser()
{
/*
* Grammar:
* expression => factor ( ( "-" | "+" ) factor )* ;
* factor => unary ( ( "/" | "*" ) unary )* ;
* unary => ( "-" ) unary
* | primary ;
* primary => NUMBER
* | "(" expression ")" ;
* Grammar:
* The top declaration has a lower priority than the lower one.
*
* additive => multiplicative ( ( "-" | "+" ) multiplicative )* ;
* multiplicative => unary ( ( "/" | "*" ) unary )* ;
* unary => ( "-" ) unary
* | primary ;
* primary => NUMBER
* | "(" expression ")" ;
*/

// The Deferred helper creates a parser that can be referenced by others before it is defined.
// The Deferred helper creates a parser that can be referenced by others before it is defined
var expression = Deferred<Expression>();

var number = Terms.Decimal()
.Then<Expression>(static d => new Number(d));
.Then<Expression>(static d => new Number(d))
;

var divided = Terms.Char('/');
var times = Terms.Char('*');
Expand All @@ -49,54 +52,24 @@ static FluentParser()
// primary => NUMBER | "(" expression ")";
var primary = number.Or(groupExpression);

// The Recursive helper allows to create parsers that depend on themselves.
// ( "-" ) unary | primary;
var unary = Recursive<Expression>((u) =>
minus.And(u)
.Then<Expression>(static x => new NegateExpression(x.Item2))
.Or(primary));

// factor => unary ( ( "/" | "*" ) unary )* ;
var factor = unary.And(ZeroOrMany(divided.Or(times).And(unary)))
.Then(static x =>
{
// unary
var result = x.Item1;

// (("/" | "*") unary ) *
foreach (var op in x.Item2)
{
result = op.Item1 switch
{
'/' => new Division(result, op.Item2),
'*' => new Multiplication(result, op.Item2),
_ => null
};
}

return result;
});

// expression => factor ( ( "-" | "+" ) factor )* ;
expression.Parser = factor.And(ZeroOrMany(plus.Or(minus).And(factor)))
.Then(static x =>
{
// factor
var result = x.Item1;

// (("-" | "+") factor ) *
foreach (var op in x.Item2)
{
result = op.Item1 switch
{
'+' => new Addition(result, op.Item2),
'-' => new Subtraction(result, op.Item2),
_ => null
};
}

return result;
});
var unary = primary.Unary(
(minus, x => new NegateExpression(x))
);

// multiplicative => unary ( ( "/" | "*" ) unary )* ;
var multiplicative = unary.LeftAssociative(
(divided, static (a, b) => new Division(a, b)),
(times, static (a, b) => new Multiplication(a, b))
);

// additive => multiplicative(("-" | "+") multiplicative) * ;
var additive = multiplicative.LeftAssociative(
(plus, static (a, b) => new Addition(a, b)),
(minus, static (a, b) => new Subtraction(a, b))
);

expression.Parser = additive;

Expression = expression;
}
Expand Down
2 changes: 1 addition & 1 deletion src/Parlot/Compilation/CompilationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public CompilationContext()
/// Gets the list of shared lambda expressions representing intermediate parsers.
/// </summary>
/// <remarks>
/// This is used for debug only, in order to inpect the source generated for these intermediate parsers.
/// This is used for debug only, in order to inspect the source generated for these intermediate parsers.
/// </remarks>
public List<Expression> Lambdas { get; } = new();

Expand Down
210 changes: 210 additions & 0 deletions src/Parlot/Fluent/Parsers.Structure.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
using System;
using System.Linq;

namespace Parlot.Fluent
{
public static partial class Parsers
{
/// <summary>
/// Builds a parser that creates a left-associative structure.
/// c.f. https://en.wikipedia.org/wiki/Operator_associativity
/// </summary>
/// <typeparam name="T">The type of the returned parser.</typeparam>
/// <typeparam name="TInput">The type of the symbol parsers.</typeparam>
/// <param name="parser">The higher-priority parser the symbols are separating.</param>
/// <param name="list">The list of operators that can be parsed and their associated result factory methods.</param>
/// <returns></returns>
/// <example>
/// // additive => multiplicative(("-" | "+") multiplicative) * ;
/// var additive = multiplicative.LeftAssociative(
/// (plus, static (a, b) => new Addition(a, b)),
/// (minus, static (a, b) => new Subtraction(a, b))
/// );
/// </example>
public static Parser<T> LeftAssociative<T, TInput>(this Parser<T> parser, params (Parser<TInput> op, Func<T, T, T> factory)[] list)
{
var choices = list.Select(l => new Then<TInput, Func<T, T, T>>(l.op, l.factory));

return parser.And(ZeroOrMany(new OneOf<Func<T, T, T>>(choices.ToArray()).And(parser)))
.Then(static x =>
{
// multiplicative
var result = x.Item1;

// (("-" | "+") multiplicative ) *
foreach (var op in x.Item2)
{
result = op.Item1(result, op.Item2);
}

return result;
});
}

/// <summary>
/// Builds a parser that creates a left-associative structure.
/// c.f. https://en.wikipedia.org/wiki/Operator_associativity
/// </summary>
/// <typeparam name="T">The type of the returned parser.</typeparam>
/// <typeparam name="TInput">The type of the symbol parsers.</typeparam>
/// <param name="parser">The higher-priority parser the symbols are separating.</param>
/// <param name="list">The list of operators that can be parsed and their associated result factory methods.</param>
/// <returns></returns>
/// <example>
/// // additive => multiplicative(("-" | "+") multiplicative) * ;
/// var additive = multiplicative.LeftAssociative(
/// (plus, static (a, b) => new Addition(a, b)),
/// (minus, static (a, b) => new Subtraction(a, b))
/// );
/// </example>
public static Parser<T> LeftAssociative<T, TInput>(this Parser<T> parser, params (Parser<TInput> op, Func<ParseContext, T, T, T> factory)[] list)
{
var choices = list.Select(l => new Then<TInput, Func<ParseContext, T, T, T>>(l.op, l.factory));

return parser.And(ZeroOrMany(new OneOf<Func<ParseContext, T, T, T>>(choices.ToArray()).And(parser)))
.Then(static (context, x) =>
{
// multiplicative
var result = x.Item1;

// (("-" | "+") multiplicative ) *
foreach (var op in x.Item2)
{
result = op.Item1(context, result, op.Item2);
}

return result;
});
}

/// <summary>
/// Builds a parser that creates a right-associative structure.
/// c.f. https://en.wikipedia.org/wiki/Operator_associativity
/// </summary>
/// <typeparam name="T">The type of the returned parser.</typeparam>
/// <typeparam name="TInput">The type of the symbol parsers.</typeparam>
/// <param name="parser">The higher-priority parser the symbols are separating.</param>
/// <param name="list">The list of operators that can be parsed and their associated result factory methods.</param>
/// <returns></returns>
/// <example>
/// // exponentiation => primary( ("^") primary) * ;
/// var exponentiation = primary.RightAssociative(
/// (equal, static (a, b) => new Exponent(a, b))
/// );
/// </example>
public static Parser<T> RightAssociative<T, TInput>(this Parser<T> parser, params (Parser<TInput> op, Func<T, T, T> factory)[] list)
{
var choices = list.Select(l => new Then<TInput, Func<T, T, T>>(l.op, l.factory));

return parser.And(ZeroOrMany(new OneOf<Func<T, T, T>>(choices.ToArray()).And(parser)))
.Then(static x =>
{
// a; (=, b); (^, c) -> = (a, ^(b, c))

var operations = x.Item2;

T result = default;

if (operations.Count > 0)
{
result = operations[operations.Count - 1].Item2;

for (var i = operations.Count - 1; i > 0; i--)
{
result = operations[i].Item1(operations[i - 1].Item2, result);
}

result = operations[0].Item1(x.Item1, result);
}
else
{
result = x.Item1;
}

return result;
});
}

/// <summary>
/// Builds a parser that creates a right-associative structure.
/// c.f. https://en.wikipedia.org/wiki/Operator_associativity
/// </summary>
/// <typeparam name="T">The type of the returned parser.</typeparam>
/// <typeparam name="TInput">The type of the symbol parsers.</typeparam>
/// <param name="parser">The higher-priority parser the symbols are separating.</param>
/// <param name="list">The list of operators that can be parsed and their associated result factory methods.</param>
/// <returns></returns>
/// <example>
/// // exponentiation => primary( ("^") primary) * ;
/// var exponentiation = primary.RightAssociative(
/// (equal, static (a, b) => new Exponent(a, b))
/// );
/// </example>
public static Parser<T> RightAssociative<T, TInput>(this Parser<T> parser, params (Parser<TInput> op, Func<ParseContext, T, T, T> factory)[] list)
{
var choices = list.Select(l => new Then<TInput, Func<ParseContext, T, T, T>>(l.op, l.factory));

return parser.And(ZeroOrMany(new OneOf<Func<ParseContext, T, T, T>>(choices.ToArray()).And(parser)))
.Then(static (context, x) =>
{
// a; (=, b); (^, c) -> = (a, ^(b, c))

var operations = x.Item2;

T result = default;

if (operations.Count > 0)
{
result = operations[operations.Count - 1].Item2;

for (var i = operations.Count - 1; i > 0; i--)
{
result = operations[i].Item1(context, operations[i - 1].Item2, result);
}

result = operations[0].Item1(context, x.Item1, result);
}
else
{
result = x.Item1;
}

return result;
});
}

/// <summary>
/// Builds a parser that creates a unary operation.
/// </summary>
/// <typeparam name="T">The type of the returned parser.</typeparam>
/// <typeparam name="TInput">The type of the symbol parsers.</typeparam>
/// <param name="parser">The higher-priority parser the symbols are separating.</param>
/// <param name="list">The list of operators that can be parsed and their associated result factory methods.</param>
/// <returns></returns>
public static Parser<T> Unary<T, TInput>(this Parser<T> parser, params (Parser<TInput> op, Func<T, T> factory)[] list)
{
return Recursive<T>((u) =>
{
var choices = list.Select(l => new Then<T, T>(l.op.SkipAnd(u), l.factory));
return new OneOf<T>(choices.ToArray()).Or(parser);
});
}

/// <summary>
/// Builds a parser that creates a unary operation.
/// </summary>
/// <typeparam name="T">The type of the returned parser.</typeparam>
/// <typeparam name="TInput">The type of the symbol parsers.</typeparam>
/// <param name="parser">The higher-priority parser the symbols are separating.</param>
/// <param name="list">The list of operators that can be parsed and their associated result factory methods.</param>
/// <returns></returns>
public static Parser<T> Unary<T, TInput>(this Parser<T> parser, params (Parser<TInput> op, Func<ParseContext, T, T> factory)[] list)
{
return Recursive<T>((u) =>
{
var choices = list.Select(l => new Then<T, T>(l.op.SkipAnd(u), l.factory));
return new OneOf<T>(choices.ToArray()).Or(parser);
});
}
}
}
14 changes: 14 additions & 0 deletions src/Samples/Calc/Expression.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
namespace Parlot.Tests.Calc
{
using System;

public abstract class Expression
{
public abstract decimal Evaluate();
Expand Down Expand Up @@ -91,6 +93,18 @@ public override decimal Evaluate()
}
}

public class Exponent : BinaryExpression
{
public Exponent(Expression left, Expression right) : base(left, right)
{
}

public override decimal Evaluate()
{
return (decimal)Math.Pow((double)Left.Evaluate(), (double)Right.Evaluate());
}
}

public class Number : Expression
{
public Number(decimal value)
Expand Down
Loading

0 comments on commit 9aa53da

Please sign in to comment.