using System.Text.RegularExpressions;
using System.Collections.Generic;
public static void Main(string[] args)
var tokeniser = new Tokeniser("b NOT a c");
var parser = new Parser();
var tokens = parser.Shunt(tokeniser);
var ast = parser.BuildAst(tokens);
var renderer = new Renderer();
Console.WriteLine(renderer.Render(ast));
public INode BuildAst(IEnumerable<IToken> tokens)
var stack = new Stack<INode>();
foreach (var token in tokens)
case VariableToken variable:
stack.Push(new UnaryOperatorNode(EqToken.Instance, new VariableNode(variable)));
throw new Exception($"Expected 2 parameters for operator {token}, got fewer");
stack.Push(new BinaryOperatorNode((OperatorToken)token, stack.Pop(), stack.Pop()));
throw new Exception($"Expected 1 parameter for operator {token}, got fewer");
stack.Push(stack.Pop().Invert());
throw new Exception("Unexpected leftover tokens");
public IEnumerable<IToken> Shunt(Tokeniser tokeniser)
var operators = new Stack<IToken>();
bool lastTokenWasVariable = false;
while (tokeniser.Next() is { } token)
if (lastTokenWasVariable && token is VariableToken or NotToken)
foreach (var t in ProcessOperator(AndToken.Instance))
case VariableToken variable:
foreach (var t in ProcessOperator(op))
while (operators.TryPeek(out var peek) && peek is not LeftParenToken)
if (operators.Count == 0)
throw new Exception("Count not find matching '(' for ')'");
if (!operators.TryPop(out var pop) || pop is not LeftParenToken)
throw new Exception("Expected a '(' at the top of the operators stack");
lastTokenWasVariable = token is VariableToken;
while (operators.TryPop(out var op))
if (op is LeftParenToken)
throw new Exception("Unexpected '('");
IEnumerable<IToken> ProcessOperator(OperatorToken op1)
while (operators.TryPeek(out var peek) && peek is OperatorToken op2 && (op1.IsLeftAssociative ? op2.Precedence >= op1.Precedence : op2.Precedence > op1.Precedence))
private static readonly Regex tokenRegex = new Regex(@"(\(|\)|\w+)\s*");
private readonly string input;
public Tokeniser(string input)
if (position == input.Length)
var match = tokenRegex.Match(input, position);
if (!match.Success || match.Index != position)
throw new Exception($"Unexpected token at start of '{input.Substring(position)}'");
string token = match.Groups[1].Value;
position += match.Length;
"(" => LeftParenToken.Instance,
")" => RightParenToken.Instance,
"AND" => AndToken.Instance,
"OR" => OrToken.Instance,
"NOT" => NotToken.Instance,
"EQ" => EqToken.Instance,
_ => new VariableToken(token),
public string Render(INode rootNode)
var sb = new StringBuilder();
case BinaryOperatorNode op:
VisitWithParens(op.Left);
sb.Append($" {op.Operator} ");
VisitWithParens(op.Right);
case UnaryOperatorNode op:
sb.Append($"({op.Operator} ");
case VariableNode variable:
sb.Append(variable.Variable);
void VisitWithParens(INode node)
if (node is BinaryOperatorNode)
if (node is BinaryOperatorNode)
public class LeftParenToken : IToken
public static LeftParenToken Instance { get; } = new LeftParenToken();
private LeftParenToken() { }
public override string ToString() => "(";
public class RightParenToken : IToken
public static RightParenToken Instance { get; } = new RightParenToken();
private RightParenToken() { }
public override string ToString() => ")";
public abstract class OperatorToken : IToken
public int Precedence { get; }
public bool IsLeftAssociative { get; }
protected OperatorToken(int precedence, bool isLeftAssociative) => (Precedence, IsLeftAssociative) = (precedence, isLeftAssociative);
public class AndToken : OperatorToken
public static AndToken Instance { get; } = new AndToken();
private AndToken() : base(1, true) { }
public override string ToString() => "AND";
public class OrToken : OperatorToken
public static OrToken Instance { get; } = new OrToken();
private OrToken() : base(1, true) { }
public override string ToString() => "OR";
public class EqToken : OperatorToken
public static EqToken Instance { get; } = new EqToken();
private EqToken() : base(2, false) { }
public override string ToString() => "EQ";
public class NotToken : OperatorToken
public static NotToken Instance { get; } = new NotToken();
private NotToken() : base(2, false) { }
public override string ToString() => "NOT";
public class VariableToken : IToken
public string Name { get; }
public VariableToken(string name) => Name = name;
public override string ToString() => Name;
public class BinaryOperatorNode : INode
public OperatorToken Operator { get; }
public INode Left { get; }
public INode Right { get; }
public BinaryOperatorNode(OperatorToken op, INode right, INode left) => (Operator, Right, Left) = (op, right, left);
AndToken => new BinaryOperatorNode(OrToken.Instance, Right.Invert(), Left.Invert()),
OrToken => new BinaryOperatorNode(AndToken.Instance, Right.Invert(), Left.Invert()),
_ => throw new Exception($"Unexpected binary operator type {Operator}"),
public class UnaryOperatorNode : INode
public OperatorToken Operator { get; }
public INode Child { get; }
public UnaryOperatorNode(OperatorToken op, INode child) => (Operator, Child) = (op, child);
NotToken => new UnaryOperatorNode(EqToken.Instance, Child.Invert()),
EqToken => new UnaryOperatorNode(NotToken.Instance, Child.Invert()),
_ => throw new Exception($"Unexpected unary operator type {Operator}"),
public class VariableNode : INode
public VariableToken Variable { get; }
public VariableNode(VariableToken variable) => Variable = variable;