expression = (ctx, check = predicate) => {
    const operators = [];
    const operands = [];

    // Depth is the nesting level of brackets in this expression. If we find a
    // closing bracket which causes our depth to fall below 1, then we know we
    // should exit the expression.
    let depth = 1;
    let previousToken = null;

    const consume = () => operands.push(operator(ctx, operators, operands));

    const eatUntil = condition => {
      let previous = last(operators);
      while (previous && previous.value !== condition) {
        consume();
        previous = last(operators);
      }
    };

    // The rules for consuming punctuators(+ - , etc.)
    const flushOperators = precedence => {
      let previous = last(operators);
      while (previous &&
      // A sequence is a special case. Note that this is a check for a Sequence NODE.
      // This is so that math operators don't "eat" already parsed sequences of nodes.
      // To put it plainly a comma separated list should never be added to a number.
      // Examples include code like: 1, 2, 3, 2 + 2.
      previous.Type !== Syntax.Sequence &&
      // The rest of this is Shunting Yard rules
      getPrecedence(previous) >= precedence && getAssociativty(previous) === 'left') {
        consume();
        previous = last(operators);
      }
    };

    // Process individual punctuators, below are the rules for handling things like
    // brackets and code blocks. Other punctuators follow a precedence rule parsing
    // approach.
    const processPunctuator = () => {
      switch (ctx.token.value) {
        case '=>':
          flushOperators(getPrecedence(ctx.token));
          operators.push(ctx.token);
          ctx.next();
          if (ctx.token.value === '{') {
            operands.push(blockParser(ctx));
          }
          return false;
        case '(':
          depth++;
          operators.push(ctx.token);
          break;
        case '[':
          depth++;
          operators.push(ctx.token);
          break;
        case ']':
          depth--;
          eatUntil('[');
          consume();
          break;
        case ')':
          {
            depth--;
            if (depth < 1) {
              return false;
            }
            // If we are not in a group already find the last LBracket,
            // consume everything until that point
            eatUntil('(');
            // Pop left bracket
            operators.pop();

            break;
          }
        case '{':
          depth++;
          operators.push(ctx.token);
          break;
        case '}':
          depth--;
          if (depth < 1) {
            return false;
          }
          eatUntil('{');
          consume();
          break;
        default:
          {
            const token = (t => {
              if (t.value === '-' && previousToken == null || t.value === '-' && isPunctuatorAndNotBracket(previousToken)) {
                return _extends({}, t, {
                  value: '--'
                });
              }

              return t;
            })(ctx.token);

            flushOperators(getPrecedence(token));
            operators.push(token);
          }
      }
    };

    // Process individual tokens, this will either push to an operand stack or
    // process an operator.
    const process = () => {
      switch (ctx.token.type) {
        case Syntax.Constant:
          operands.push(parseConstant(ctx));
          break;
        case Syntax.Identifier:
          previousToken = ctx.token;
          // Maybe an Identifier or a function call
          operands.push(maybeIdentifier(ctx));
          return false;
        case Syntax.StringLiteral:
          operands.push(stringLiteral(ctx));
          break;
        case Syntax.Type:
          operands.push(builtInType(ctx));
          break;
        case Syntax.Keyword:
        case Syntax.Punctuator:
          // Some special keywords may show up in expressions, but only a small
          // subset. These keywords are treated as punctuators and processed by
          // the overall punctuator rules
          // EXAMPLE: the 'as' keyword - import statements consist of a sequence of
          // expressions but the as keyword can be used to rename an import within.
          if (ctx.token.type === Syntax.Keyword && !validKeywordsInExpressions.includes(ctx.token.value)) {
            break;
          }
          const punctuatorResult = processPunctuator();
          if (punctuatorResult != null) {
            return punctuatorResult;
          }
          break;
      }

      return true;
    };

    while (ctx.token && check(ctx.token, depth)) {
      if (process()) {
        previousToken = ctx.token;
        ctx.next();
      }
    }

    // If we get to the end of our available tokens then proceed to eat any left over
    // operators and finalize the expression.
    while (operators.length) {
      consume();
    }

    // Last operand should be a node that is at the "root" of this expression
    return operands.pop();
  }
The expression parser for generating all parsed nodes, uses a modified Shunting
Yard algo.

@author Arthur Buldauksas <arthurbuldauskas@gmail.com>