This is part of a series of posts documenting Sprache:

The ChainOperator and ChainRightOperator methods are helpers for parsing left and right-associative operators respectivey, e.g. 1 + 2 or 5 ^ 2.

ChainOperator

Chain a left-associative operator of operations matched by op and operands matched by operand.

  • Parser<T> ChainOperator<T, TOp>(Parser<TOp> op, Parser<T> operand, Func<TOp, T, T, T> apply)

op is a parser for the operator (e.g +, ^), and operand is the parser for the operands such as numbers or other expressions. apply is a function that combines the terms and operand, into a return value. This parser can match zero or more operations, however at least one operand must be matched. The parser in the following example parses a chain or zero or more additions between numbers.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Parser<char> add = Parse.Char('+').Token();
Parser<int> number = Parse.Number.Token().Select(int.Parse);

Parser<int> expr = Parse.ChainOperator(add, number, (op, left, right) => left + right);

Assert.Equal(3, expr.Parse("1 + 2"));
Assert.Equal(9, expr.Parse("1 + 2 + 3 + 3"));
Assert.Equal(1, expr.Parse("1"));
// Unexpected end of input reached; expected numeric character
Assert.Throws<ParseException>(() => expr.Parse(""));

Note that the type returned from apply must match the operand type, so that the operators can be chained together. In this example the operand type and the returned value is a string so we can visualize how the chaining works:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Parser<char> add = Parse.Char('+').Token();
Parser<char> subtract = Parse.Char('-').Token();
Parser<string> number = Parse.Number.Token();

Parser<string> expr = Parse.ChainOperator(add.Or(subtract), number, 
    (op, left, right) => $"({left} {op} {right})");

Assert.Equal("(1 + 2)", expr.Parse("1 + 2"));
Assert.Equal("(((1 + 2) - 3) + 3)", expr.Parse("1 + 2 - 3 + 3"));
Assert.Equal("1", expr.Parse("1"));

XChainOperator

XChainOperator is similar to ChainOperator, however fails in cases where certain inputs are only partially parsed.

  • Parser<T> XChainOperator<T, TOp>(Parser<TOp> op, Parser<T> operand, Func<TOp, T, T, T> apply)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Parser<char> addOp = Parse.Char('+').Token();
Parser<int> number = Parse.Number.Token().Select(int.Parse);

Parser<int> add = Parse.ChainOperator(addOp, number, (op, left, right) => left + right);
Parser<int> addX = Parse.XChainOperator(addOp, number, (op, left, right) => left + right);

Assert.Equal(4, add.Parse("1 + 3 + aaa"));
// unexpected 'a'; expected numeric character
Assert.Throws<ParseException>(() => addX.Parse("1 + 3 + aaa"));

Assert.Equal(8, add.Parse("1 + 3 + 4a + 5"));
Assert.Equal(8, addX.Parse("1 + 3 + 4a + 5"));

Like all the other X methods, this is intended to improve handling of errors by failing fast. In some cases (such as the first example), it helps by returning a more specific error message, however in the second example it successfully matched a chained operator, " + 4”, and so the result was the same as using ChainOperator.

ChainRightOperator

Like ChainOperator, however for right-associative operators, like ^.

  • Parser<T> ChainRightOperator<T, TOp>(Parser<TOp> op, Parser<T> operand, Func<TOp, T, T, T> apply)
1
2
3
4
5
6
7
Parser<char> exp = Parse.Char('^').Token();
Parser<string> number = Parse.Number.Token();

Parser<string> expr = Parse.ChainRightOperator(exp, number,  (op, left, right) => $"({left} {op} {right})");

Assert.Equal("(1 ^ 2)", expr.Parse("1 ^ 2"));
Assert.Equal("(1 ^ (2 ^ (3 ^ 3)))", expr.Parse("1 ^ 2 ^ 3 ^ 3"));

XChainRightOperator

Like ChainRightOperator, however fails on some partially parsed inputs. See XChainOperator for examples.

  • Parser<T> XChainRightOperator<T, TOp>(Parser<TOp> op, Parser<T> operand, Func<TOp, T, T, T> apply)