private IExpression CompileExpression(Queue <Token> tokens, OperatorPrecedence minimumPrecedence, IList <string> variables, bool isWithinFunction)
        {
            if (tokens == null)
            {
                throw new ArgumentNullException("tokens", "You must call Tokenise before compiling");
            }

            IExpression leftHandSide  = null;
            var         currentToken  = tokens.PeekOrDefault();
            Token       previousToken = null;

            while (currentToken != null)
            {
                Func <IExpression[], IDictionary <string, object>, object> function = null;
                IOperator op = null;

                if (_registeredOperators.TryGetValue(currentToken.CurrentToken, out op)) // Are we an IOperator?
                {
                    var precedence = op.GetPrecedence(previousToken);

                    if (precedence > minimumPrecedence)
                    {
                        tokens.Dequeue();

                        if (!op.CanGetCaptiveTokens(previousToken, currentToken, tokens))
                        {
                            // Do it anyway to update the list of tokens
                            op.GetCaptiveTokens(previousToken, currentToken, tokens);
                            break;
                        }
                        else
                        {
                            IExpression rightHandSide = null;

                            var captiveTokens = op.GetCaptiveTokens(previousToken, currentToken, tokens);

                            if (captiveTokens.Length > 1)
                            {
                                var innerTokens = op.GetInnerCaptiveTokens(captiveTokens);
                                rightHandSide = CompileExpression(new Queue <Token>(innerTokens), OperatorPrecedence.Minimum, variables, isWithinFunction);

                                currentToken = captiveTokens[captiveTokens.Length - 1];
                            }
                            else
                            {
                                rightHandSide = CompileExpression(tokens, precedence, variables, isWithinFunction);
                                // We are at the end of an expression so fake it up.
                                currentToken = new Token(")", -1);
                            }

                            leftHandSide = op.BuildExpression(previousToken, new[] { leftHandSide, rightHandSide }, _options);
                        }
                    }
                    else
                    {
                        break;
                    }
                }
                else if (_registeredFunctions.TryGetValue(currentToken.CurrentToken, out function)) // or an IFunction?
                {
                    this.CheckForExistingParticipant(leftHandSide, currentToken, isWithinFunction);

                    var expressions   = new List <IExpression>();
                    var captiveTokens = new Queue <Token>();
                    var parenCount    = 0;
                    tokens.Dequeue();

                    // Loop through the list of tokens and split by ParameterSeparator character
                    while (tokens.Count > 0)
                    {
                        var nextToken = tokens.Dequeue();

                        if (string.Equals(nextToken.CurrentToken, "(", StringComparison.Ordinal))
                        {
                            parenCount++;
                        }
                        else if (string.Equals(nextToken.CurrentToken, ")", StringComparison.Ordinal))
                        {
                            parenCount--;
                        }

                        if (!(parenCount == 1 && nextToken.CurrentToken == "(") &&
                            !(parenCount == 0 && nextToken.CurrentToken == ")"))
                        {
                            captiveTokens.Enqueue(nextToken);
                        }

                        if (parenCount == 0 &&
                            captiveTokens.Any())
                        {
                            expressions.Add(CompileExpression(captiveTokens, minimumPrecedence: OperatorPrecedence.Minimum, variables: variables, isWithinFunction: true));
                            captiveTokens.Clear();
                        }
                        else if (string.Equals(nextToken.CurrentToken, ParameterSeparator.ToString(), StringComparison.Ordinal) && parenCount == 1)
                        {
                            // TODO: Should we expect expressions to be null???
                            expressions.Add(CompileExpression(captiveTokens, minimumPrecedence: 0, variables: variables, isWithinFunction: true));
                            captiveTokens.Clear();
                        }

                        if (parenCount <= 0)
                        {
                            break;
                        }
                    }

                    leftHandSide = new FunctionExpression(currentToken.CurrentToken, function, expressions.ToArray());
                }
                else if (currentToken.CurrentToken.IsNumeric()) // Or a number
                {
                    this.CheckForExistingParticipant(leftHandSide, currentToken, isWithinFunction);

                    tokens.Dequeue();
                    int     intValue     = 0;
                    decimal decimalValue = 0.0M;
                    double  doubleValue  = 0.0;
                    float   floatValue   = 0.0f;
                    long    longValue    = 0;

                    if (int.TryParse(currentToken.CurrentToken, out intValue))
                    {
                        leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.Integer, intValue);
                    }
                    else if (decimal.TryParse(currentToken.CurrentToken, out decimalValue))
                    {
                        leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.Decimal, decimalValue);
                    }
                    else if (double.TryParse(currentToken.CurrentToken, out doubleValue))
                    {
                        leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.Double, doubleValue);
                    }
                    else if (float.TryParse(currentToken.CurrentToken, out floatValue))
                    {
                        leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.Float, floatValue);
                    }
                    else if (long.TryParse(currentToken.CurrentToken, out longValue))
                    {
                        leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.Long, longValue);
                    }
                }
                else if (currentToken.CurrentToken.StartsWith("[") && currentToken.CurrentToken.EndsWith("]")) // or a variable?
                {
                    this.CheckForExistingParticipant(leftHandSide, currentToken, isWithinFunction);

                    tokens.Dequeue();
                    string variableName = currentToken.CurrentToken.Replace("[", "").Replace("]", "");
                    leftHandSide = new VariableExpression(variableName);

                    if (!variables.Contains(variableName, _stringComparer))
                    {
                        variables.Add(variableName);
                    }
                }
                else if (string.Equals(currentToken.CurrentToken, "true", StringComparison.OrdinalIgnoreCase)) // or a boolean?
                {
                    this.CheckForExistingParticipant(leftHandSide, currentToken, isWithinFunction);

                    tokens.Dequeue();
                    leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.Boolean, true);
                }
                else if (string.Equals(currentToken.CurrentToken, "false", StringComparison.OrdinalIgnoreCase))
                {
                    this.CheckForExistingParticipant(leftHandSide, currentToken, isWithinFunction);

                    tokens.Dequeue();
                    leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.Boolean, false);
                }
                else if (string.Equals(currentToken.CurrentToken, "null", StringComparison.OrdinalIgnoreCase)) // or a null?
                {
                    this.CheckForExistingParticipant(leftHandSide, currentToken, isWithinFunction);

                    tokens.Dequeue();
                    leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.Null, null);
                }
                else if (currentToken.CurrentToken.StartsWith(DateSeparator.ToString()) && currentToken.CurrentToken.EndsWith(DateSeparator.ToString())) // or a date?
                {
                    this.CheckForExistingParticipant(leftHandSide, currentToken, isWithinFunction);

                    tokens.Dequeue();

                    string   dateToken = currentToken.CurrentToken.Replace(DateSeparator.ToString(), "");
                    DateTime date      = DateTime.MinValue;

                    // If we can't parse the date let's check for some known tags.
                    if (!DateTime.TryParse(dateToken, out date))
                    {
                        if (string.Equals("TODAY", dateToken, StringComparison.OrdinalIgnoreCase))
                        {
                            date = DateTime.Today;
                        }
                        else if (string.Equals("NOW", dateToken, StringComparison.OrdinalIgnoreCase))
                        {
                            date = DateTime.Now;
                        }
                        else
                        {
                            throw new UnrecognisedTokenException(dateToken);
                        }
                    }

                    leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.DateTime, date);
                }
                else if ((currentToken.CurrentToken.StartsWith("'") && currentToken.CurrentToken.EndsWith("'")) ||
                         (currentToken.CurrentToken.StartsWith("\"") && currentToken.CurrentToken.EndsWith("\"")))
                {
                    this.CheckForExistingParticipant(leftHandSide, currentToken, isWithinFunction);

                    tokens.Dequeue();
                    leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.String, CleanString(currentToken.CurrentToken.Substring(1, currentToken.Length - 2)));
                }
                else if (string.Equals(currentToken.CurrentToken, ParameterSeparator.ToString(), StringComparison.Ordinal)) // Make sure we ignore the parameter separator
                {
                    // TODO should we throw an exception if we are not within a function?
                    if (!isWithinFunction)
                    {
                        throw new ExpressiveException($"Unexpected token '{currentToken}'");
                    }
                    tokens.Dequeue();

                    //throw new InvalidOperationException("Unrecognised token '" + currentToken + "'");

                    //if (!string.Equals(currentToken, ParameterSeparator.ToString(), StringComparison.Ordinal)) // Make sure we ignore the parameter separator
                    //{
                    //    currentToken = CleanString(currentToken);

                    //    leftHandSide = new ConstantValueExpression(ConstantValueExpressionType.Unknown, currentToken);
                    //}
                }
                else
                {
                    tokens.Dequeue();

                    throw new UnrecognisedTokenException(currentToken.CurrentToken);
                }

                previousToken = currentToken;
                currentToken  = tokens.PeekOrDefault();
            }

            return(leftHandSide);
        }