/**
 * MVEL 2.0
 * Copyright (C) 2007 The Codehaus
 * Mike Brock, Dhanji Prasanna, John Graham, Mark Proctor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.mvel2.compiler;

import org.mvel2.CompileException;
import org.mvel2.ErrorDetail;
import org.mvel2.Operator;
import org.mvel2.ParserContext;
import org.mvel2.ast.*;
import org.mvel2.integration.VariableResolverFactory;
import org.mvel2.util.ErrorUtil;
import org.mvel2.util.ExecutionStack;
import org.mvel2.util.FunctionParser;
import org.mvel2.util.ProtoParser;

import java.io.Serializable;
import java.util.HashMap;
import java.util.WeakHashMap;

import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static java.lang.Double.parseDouble;
import static java.lang.Runtime.getRuntime;
import static java.lang.System.getProperty;
import static java.lang.Thread.currentThread;
import static org.mvel2.Operator.*;
import static org.mvel2.ast.TypeDescriptor.getClassReference;
import static org.mvel2.util.ArrayTools.findFirst;
import static org.mvel2.util.ParseTools.*;
import static org.mvel2.util.PropertyTools.isEmpty;
import static org.mvel2.util.Soundex.soundex;

/**
 * This is the core parser that the subparsers extend.
 *
 * @author Christopher Brock
 */
public class AbstractParser implements Parser, Serializable {
  protected char[] expr;
  protected int cursor;

  protected int start;
  protected int length;
  protected int end;
  protected int st;

  protected int fields;

  protected static final int OP_OVERFLOW = -2;
  protected static final int OP_TERMINATE = -1;
  protected static final int OP_RESET_FRAME = 0;
  protected static final int OP_CONTINUE = 1;

  protected boolean greedy = true;
  protected boolean lastWasIdentifier = false;
  protected boolean lastWasLineLabel = false;
  protected boolean lastWasComment = false;
  protected boolean compileMode = false;

  protected int literalOnly = -1;

  protected int lastLineStart = 0;
  protected int line = 0;

  protected ASTNode lastNode;

  private static final WeakHashMap<String, char[]> EX_PRECACHE = new WeakHashMap<String, char[]>(15);

  public static HashMap<String, Object> LITERALS;
  public static HashMap<String, Integer> OPERATORS;

  protected ExecutionStack stk;
  protected ExecutionStack splitAccumulator = new ExecutionStack();

  protected static ThreadLocal<ParserContext> parserContext;
  protected ParserContext pCtx;
  protected ExecutionStack dStack;
  protected Object ctx;
  protected VariableResolverFactory variableFactory;

  protected boolean debugSymbols = false;

  static {
    setupParser();
  }

  /**
   * This method is internally called by the static initializer for AbstractParser in order to setup the parser.
   * The static initialization populates the operator and literal tables for the parser.  In some situations, like
   * OSGi, it may be necessary to utilize this manually.
   */
  public static void setupParser() {
    if (LITERALS == null || LITERALS.isEmpty()) {
      LITERALS = new HashMap<String, Object>();
      OPERATORS = new HashMap<String, Integer>();

      LITERALS.put("true", TRUE);
      LITERALS.put("false", FALSE);

      LITERALS.put("null", null);
      LITERALS.put("nil", null);

      LITERALS.put("empty", BlankLiteral.INSTANCE);

      /**
       * Add System and all the class wrappers from the JCL.
       */
      LITERALS.put("System", System.class);
      LITERALS.put("String", String.class);
      LITERALS.put("CharSequence", CharSequence.class);

      LITERALS.put("Integer", Integer.class);
      LITERALS.put("int", Integer.class);

      LITERALS.put("Long", Long.class);
      LITERALS.put("long", Long.class);

      LITERALS.put("Boolean", Boolean.class);
      LITERALS.put("boolean", Boolean.class);

      LITERALS.put("Short", Short.class);
      LITERALS.put("short", Short.class);

      LITERALS.put("Character", Character.class);
      LITERALS.put("char", Character.class);

      LITERALS.put("Double", Double.class);
      LITERALS.put("double", Double.class);

      LITERALS.put("Float", Float.class);
      LITERALS.put("float", Float.class);

      LITERALS.put("Byte", Byte.class);
      LITERALS.put("byte", Byte.class);

      LITERALS.put("Math", Math.class);
      LITERALS.put("Void", Void.class);
      LITERALS.put("Object", Object.class);
      LITERALS.put("Number", Number.class);

      LITERALS.put("Class", Class.class);
      LITERALS.put("ClassLoader", ClassLoader.class);
      LITERALS.put("Runtime", Runtime.class);
      LITERALS.put("Thread", Thread.class);
      LITERALS.put("Compiler", Compiler.class);
      LITERALS.put("StringBuffer", StringBuffer.class);
      LITERALS.put("ThreadLocal", ThreadLocal.class);
      LITERALS.put("SecurityManager", SecurityManager.class);
      LITERALS.put("StrictMath", StrictMath.class);

      LITERALS.put("Exception", Exception.class);

      LITERALS.put("Array", java.lang.reflect.Array.class);

      if (parseDouble(getProperty("java.version").substring(0, 3)) >= 1.5) {
        try {
          LITERALS.put("StringBuilder", currentThread().getContextClassLoader().loadClass("java.lang.StringBuilder"));
        }
        catch (Exception e) {
          throw new RuntimeException("cannot resolve a built-in literal", e);
        }
      }

      setLanguageLevel(Boolean.getBoolean("mvel.future.lang.support") ? 6 : 5);
    }
  }

  protected ASTNode nextTokenSkipSymbols() {
    ASTNode n = nextToken();
    if (n != null && n.getFields() == -1) n = nextToken();
    return n;
  }

  /**
   * Retrieve the next token in the expression.
   *
   * @return -
   */
  protected ASTNode nextToken() {
    try {
      /**
       * If the cursor is at the end of the expression, we have nothing more to do:
       * return null.
       */
      if (!splitAccumulator.isEmpty()) {
        lastNode = (ASTNode) splitAccumulator.pop();
        if (cursor >= end && lastNode instanceof EndOfStatement) {
          return nextToken();
        } else {
          return lastNode;
        }
      } else if (cursor >= end) {
        return null;
      }

      int brace, idx;
      int tmpStart;

      String name;
      /**
       * Because of parser recursion for sub-expression parsing, we sometimes need to remain
       * certain field states.  We do not reset for assignments, boolean mode, list creation or
       * a capture only mode.
       */

      boolean capture = false, union = false;

      if ((fields & ASTNode.COMPILE_IMMEDIATE) != 0 && pCtx == null) {
        debugSymbols = (pCtx = getParserContext()).isDebugSymbols();
      }

      if (debugSymbols) {
        if (!lastWasLineLabel) {
          if (pCtx.getSourceFile() == null) {
            throw new CompileException("unable to produce debugging symbols: source name must be provided.", expr, st);
          }

          if (!pCtx.isLineMapped(pCtx.getSourceFile())) {
            pCtx.initLineMapping(pCtx.getSourceFile(), expr);
          }

          skipWhitespace();

          if (cursor >= end) {
            return null;
          }

          int line = pCtx.getLineFor(pCtx.getSourceFile(), cursor);

          if (!pCtx.isVisitedLine(pCtx.getSourceFile(), pCtx.setLineCount(line)) && !pCtx.isBlockSymbols()) {
            lastWasLineLabel = true;
            pCtx.visitLine(pCtx.getSourceFile(), line);

            return lastNode = pCtx.setLastLineLabel(new LineLabel(pCtx.getSourceFile(), line));
          }
        } else {
          lastWasComment = lastWasLineLabel = false;
        }
      }

      /**
       * Skip any whitespace currently under the starting point.
       */
      skipWhitespace();

      /**
       * From here to the end of the method is the core MVEL parsing code.  Fiddling around here is asking for
       * trouble unless you really know what you're doing.
       */

      st = cursor;

      Mainloop:
      while (cursor != end) {
        if (isIdentifierPart(expr[cursor])) {
          capture = true;
          cursor++;

          while (cursor != end && isIdentifierPart(expr[cursor])) cursor++;
        }

        /**
         * If the current character under the cursor is a valid
         * part of an identifier, we keep capturing.
         */

        if (capture) {
          String t;
          if (OPERATORS.containsKey(t = new String(expr, st, cursor - st))) {
            switch (OPERATORS.get(t)) {
              case NEW:
                if (!isIdentifierPart(expr[st = cursor = trimRight(cursor)])) {
                  throw new CompileException("unexpected character (expected identifier): "
                          + expr[cursor], expr, st);
                }

                /**
                 * Capture the beginning part of the token.
                 */
                do {
                  captureToNextTokenJunction();
                  skipWhitespace();
                }
                while (cursor < end && expr[cursor] == '[');

                /**
                 * If it's not a dimentioned array, continue capturing if necessary.
                 */
                if (cursor < end && !lastNonWhite(']')) captureToEOT();

                TypeDescriptor descr = new TypeDescriptor(expr, st, trimLeft(cursor) - st, fields);

                if (pCtx == null) pCtx = getParserContext();

                if (pCtx.hasProtoImport(descr.getClassName())) {
                  return lastNode = new NewPrototypeNode(descr);
                }

                lastNode = new NewObjectNode(descr, fields, pCtx);

                skipWhitespace();
                if (cursor != end && expr[cursor] == '{') {
                  if (!((NewObjectNode) lastNode).getTypeDescr().isUndimensionedArray()) {
                    throw new CompileException(
                            "conflicting syntax: dimensioned array with initializer block",
                            expr, st);
                  }

                  st = cursor;
                  Class egressType = lastNode.getEgressType();

                  if (egressType == null) {
                    try {
                      egressType = getClassReference(pCtx, descr);
                    }
                    catch (ClassNotFoundException e) {
                      throw new CompileException("could not instantiate class", expr, st, e);
                    }
                  }


                  cursor = balancedCaptureWithLineAccounting(expr, st, end, expr[cursor], pCtx) + 1;
                  if (tokenContinues()) {
                    lastNode = new InlineCollectionNode(expr, st, cursor - st, fields,
                            egressType, pCtx);
                    st = cursor;
                    captureToEOT();
                    return lastNode = new Union(expr, st + 1, cursor, fields, lastNode);
                  } else {
                    return lastNode = new InlineCollectionNode(expr, st, cursor - st, fields,
                            egressType, pCtx);
                  }
                } else if (((NewObjectNode) lastNode).getTypeDescr().isUndimensionedArray()) {
                  throw new CompileException("array initializer expected", expr, st);
                }
                st = cursor;

                return lastNode;

              case ASSERT:
                st = cursor = trimRight(cursor);
                captureToEOS();
                return lastNode = new AssertNode(expr, st, cursor-- - st, fields, pCtx);

              case RETURN:
                st = cursor = trimRight(cursor);
                captureToEOS();
                return lastNode = new ReturnNode(expr, st, cursor - st, fields, pCtx);

              case IF:
                return captureCodeBlock(ASTNode.BLOCK_IF);

              case ELSE:
                throw new CompileException("else without if", expr, st);

              case FOREACH:
                return captureCodeBlock(ASTNode.BLOCK_FOREACH);

              case WHILE:
                return captureCodeBlock(ASTNode.BLOCK_WHILE);

              case UNTIL:
                return captureCodeBlock(ASTNode.BLOCK_UNTIL);

              case FOR:
                return captureCodeBlock(ASTNode.BLOCK_FOR);

              case WITH:
                return captureCodeBlock(ASTNode.BLOCK_WITH);

              case DO:
                return captureCodeBlock(ASTNode.BLOCK_DO);

              case PROTO:
                return captureCodeBlock(PROTO);

              case ISDEF:
                st = cursor = trimRight(cursor);
                captureToNextTokenJunction();
                return lastNode = new IsDef(expr, st, cursor - st);

              case IMPORT:
                st = cursor = trimRight(cursor);
                captureToEOS();
                ImportNode importNode = new ImportNode(expr, st, cursor - st);

                if (pCtx == null) pCtx = getParserContext();

                if (importNode.isPackageImport()) {
                  pCtx.addPackageImport(importNode.getPackageImport());
                } else {
                  pCtx.addImport(importNode.getImportClass().getSimpleName(), importNode.getImportClass());
                }
                return lastNode = importNode;

              case IMPORT_STATIC:
                st = cursor = trimRight(cursor);
                captureToEOS();
                return lastNode = new StaticImportNode(expr, st, trimLeft(cursor) - st);

              case FUNCTION:
                lastNode = captureCodeBlock(FUNCTION);
                st = cursor + 1;
                return lastNode;

              case UNTYPED_VAR:
                int end;
                st = cursor + 1;

                while (true) {
                  captureToEOT();
                  end = cursor;
                  skipWhitespace();

                  if (cursor != end && expr[cursor] == '=') {
                    if (end == (cursor = st))
                      throw new CompileException("illegal use of reserved word: var", expr, st);

                    continue Mainloop;
                  } else {
                    name = new String(expr, st, end - st);
                    if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                      splitAccumulator.add(lastNode = new IndexedDeclTypedVarNode(idx, st, end - st, Object.class));
                    } else {
                      splitAccumulator.add(lastNode = new DeclTypedVarNode(name, expr, st, end - st, Object.class,
                              fields, pCtx));
                    }
                  }

                  if (cursor == this.end || expr[cursor] != ',') break;
                  else {
                    cursor++;
                    skipWhitespace();
                    st = cursor;
                  }
                }

                return (ASTNode) splitAccumulator.pop();
            }
          }

          skipWhitespace();

          /**
           * If we *were* capturing a token, and we just hit a non-identifier
           * character, we stop and figure out what to do.
           */
          if (cursor != end && expr[cursor] == '(') {
            cursor = balancedCaptureWithLineAccounting(expr, cursor, end, '(', pCtx) + 1;
          }

          /**
           * If we encounter any of the following cases, we are still dealing with
           * a contiguous token.
           */
          CaptureLoop:
          while (cursor != end) {
            switch (expr[cursor]) {
              case '.':
                union = true;
                cursor++;
                skipWhitespace();

                continue;

              case '?':
                if (lookToLast() == '.') {
                  union = true;
                  cursor++;
                  continue;
                } else {
                  break CaptureLoop;
                }

              case '+':
                switch (lookAhead()) {
                  case '+':
                    name = new String(subArray(st, trimLeft(cursor)));
                    if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                      lastNode = new IndexedPostFixIncNode(idx, pCtx);
                    } else {
                      lastNode = new PostFixIncNode(name, pCtx);
                    }

                    cursor += 2;

                    expectEOS();

                    return lastNode;

                  case '=':
                    name = createStringTrimmed(expr, st, cursor - st);
                    st = cursor += 2;

                    captureToEOS();

                    if (union) {
                      return lastNode = new DeepAssignmentNode(expr, st = trimRight(st), trimLeft(cursor) - st, fields,
                              ADD, name, pCtx);
                    } else if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                      return lastNode = new IndexedAssignmentNode(expr, st, cursor - st, fields,
                              ADD, name, idx, pCtx);
                    } else {
                      return lastNode = new OperativeAssign(name, expr, st = trimRight(st), trimLeft(cursor) - st,
                              ADD, fields, pCtx);
                    }
                }

                if (isDigit(lookAhead()) &&
                        cursor > 1 && (expr[cursor - 1] == 'E' || expr[cursor - 1] == 'e')
                        && isDigit(expr[cursor - 2])) {
                  cursor++;
                  //     capture = true;
                  continue Mainloop;
                }
                break CaptureLoop;

              case '-':
                switch (lookAhead()) {
                  case '-':
                    name = new String(subArray(st, trimLeft(cursor)));
                    if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                      lastNode = new IndexedPostFixDecNode(idx, pCtx);
                    } else {
                      lastNode = new PostFixDecNode(name, pCtx);
                    }
                    cursor += 2;

                    expectEOS();

                    return lastNode;

                  case '=':
                    name = new String(expr, st, trimLeft(cursor) - st);
                    st = cursor += 2;

                    captureToEOS();

                    if (union) {
                      return lastNode = new DeepAssignmentNode(expr, st, cursor - st, fields,
                              SUB, t, pCtx);
                    } else if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                      return lastNode = new IndexedOperativeAssign(expr, st, cursor - st,
                              SUB, idx, fields, pCtx);
                    } else {
                      return lastNode = new OperativeAssign(name, expr, st, cursor - st,
                              SUB, fields, pCtx);
                    }
                }

                if (isDigit(lookAhead()) &&
                        cursor > 1 && (expr[cursor - 1] == 'E' || expr[cursor - 1] == 'e')
                        && isDigit(expr[cursor - 2])) {
                  cursor++;
                  capture = true;
                  continue Mainloop;
                }
                break CaptureLoop;

              /**
               * Exit immediately for any of these cases.
               */
              case '!':
              case ',':
              case '"':
              case '\'':
              case ';':
              case ':':
                break CaptureLoop;

              case '\u00AB': // special compact code for recursive parses
              case '\u00BB':
              case '\u00AC':
              case '&':
              case '^':
              case '|':
              case '*':
              case '/':
              case '%':
                char op = expr[cursor];
                if (lookAhead() == '=') {
                  name = new String(expr, st, trimLeft(cursor) - st);

                  st = cursor += 2;
                  captureToEOS();

                  if (union) {
                    return lastNode = new DeepAssignmentNode(expr, st, cursor - st, fields,
                            opLookup(op), t, pCtx);
                  } else if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                    return lastNode = new IndexedOperativeAssign(expr, st, cursor - st,
                            opLookup(op), idx, fields, pCtx);
                  } else {
                    return lastNode = new OperativeAssign(name, expr, st, cursor - st,
                            opLookup(op), fields, pCtx);
                  }
                }
                break CaptureLoop;

              case '<':
                if ((lookAhead() == '<' && lookAhead(2) == '=')) {
                  name = new String(expr, st, trimLeft(cursor) - st);

                  st = cursor += 3;
                  captureToEOS();

                  if (union) {
                    return lastNode = new DeepAssignmentNode(expr, st, cursor - st, fields,
                            BW_SHIFT_LEFT, t, pCtx);
                  } else if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                    return lastNode = new IndexedOperativeAssign(expr, st, cursor - st,
                            BW_SHIFT_LEFT, idx, fields, pCtx);
                  } else {
                    return lastNode = new OperativeAssign(name, expr, st, cursor - st,
                            BW_SHIFT_LEFT, fields, pCtx);
                  }
                }
                break CaptureLoop;

              case '>':
                if (lookAhead() == '>') {
                  if (lookAhead(2) == '=') {
                    name = new String(expr, st, trimLeft(cursor) - st);

                    st = cursor += 3;
                    captureToEOS();

                    if (union) {
                      return lastNode = new DeepAssignmentNode(expr, st, cursor - st, fields,
                              BW_SHIFT_RIGHT, t, pCtx);
                    } else if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                      return lastNode = new IndexedOperativeAssign(expr, st, cursor - st,
                              BW_SHIFT_RIGHT, idx, fields, pCtx);
                    } else {
                      return lastNode = new OperativeAssign(name, expr, st, cursor - st,
                              BW_SHIFT_RIGHT, fields, pCtx);
                    }
                  } else if ((lookAhead(2) == '>' && lookAhead(3) == '=')) {
                    name = new String(expr, st, trimLeft(cursor) - st);

                    st = cursor += 4;
                    captureToEOS();

                    if (union) {
                      return lastNode = new DeepAssignmentNode(expr, st, cursor - st, fields,
                              BW_USHIFT_RIGHT, t, pCtx);
                    } else if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                      return lastNode = new IndexedOperativeAssign(expr, st, cursor - st,
                              BW_USHIFT_RIGHT, idx, fields, pCtx);
                    } else {
                      return lastNode = new OperativeAssign(name, expr, st, cursor - st,
                              BW_USHIFT_RIGHT, fields, pCtx);
                    }
                  }
                }
                break CaptureLoop;

              case '(':
                cursor = balancedCaptureWithLineAccounting(expr, cursor, end, '(', pCtx) + 1;
                continue;

              case '[':
                cursor = balancedCaptureWithLineAccounting(expr, cursor, end, '[', pCtx) + 1;
                continue;

              case '{':
                if (!union) break CaptureLoop;
                cursor = balancedCaptureWithLineAccounting(expr, cursor, end, '{', pCtx) + 1;
                continue;

              case '~':
                if (lookAhead() == '=') {
                  // tmp = subArray(start, trimLeft(cursor));
                  tmpStart = st;
                  int tmpOffset = cursor - st;
                  st = cursor += 2;

                  captureToEOT();

                  return lastNode = new RegExMatch(expr, tmpStart, tmpOffset, fields, st, cursor - st, pCtx);
                }
                break CaptureLoop;

              case '=':
                if (lookAhead() == '+') {
                  name = new String(expr, st, trimLeft(cursor) - st);

                  st = cursor += 2;

                  if (!isNextIdentifierOrLiteral()) {
                    throw new CompileException("unexpected symbol '" + expr[cursor] + "'", expr, st);
                  }

                  captureToEOS();

                  if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                    return lastNode = new IndexedOperativeAssign(expr, st, cursor - st,
                            ADD, idx, fields, pCtx);
                  } else {
                    return lastNode = new OperativeAssign(name, expr, st, cursor - st,
                            ADD, fields, pCtx);
                  }
                } else if (lookAhead() == '-') {
                  name = new String(expr, st, trimLeft(cursor) - st);

                  st = cursor += 2;

                  if (!isNextIdentifierOrLiteral()) {
                    throw new CompileException("unexpected symbol '" + expr[cursor] + "'", expr, st);
                  }

                  captureToEOS();

                  if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                    return lastNode = new IndexedOperativeAssign(expr, st, cursor - st,
                            SUB, idx, fields, pCtx);
                  } else {
                    return lastNode = new OperativeAssign(name, expr, st, cursor - st,
                            SUB, fields, pCtx);
                  }
                }
                if (greedy && lookAhead() != '=') {
                  cursor++;

                  if (union) {
                    captureToEOS();

                    return lastNode = new DeepAssignmentNode(expr, st, cursor - st,
                            fields | ASTNode.ASSIGN, pCtx);
                  } else if (lastWasIdentifier) {
                    return procTypedNode(false);
                  } else if (pCtx != null && ((idx = pCtx.variableIndexOf(t)) != -1
                          && (pCtx.isIndexAllocation()))) {
                    captureToEOS();

                    IndexedAssignmentNode ian = new IndexedAssignmentNode(expr, st = trimRight(st),
                            trimLeft(cursor) - st,
                            ASTNode.ASSIGN, idx, pCtx);

                    if (idx == -1) {
                      pCtx.addIndexedInput(t = ian.getAssignmentVar());
                      ian.setRegister(pCtx.variableIndexOf(t));
                    }
                    return lastNode = ian;
                  } else {
                    captureToEOS();

                    return lastNode = new AssignmentNode(expr, st, cursor - st,
                            fields | ASTNode.ASSIGN, pCtx);
                  }
                }
                break CaptureLoop;

              default:
                if (cursor != end) {
                  if (isIdentifierPart(expr[cursor])) {
                    if (!union) {
                      break CaptureLoop;
                    }
                    cursor++;
                    while (cursor != end && isIdentifierPart(expr[cursor])) cursor++;
                  } else if ((cursor + 1) != end && isIdentifierPart(expr[cursor + 1])) {
                    break CaptureLoop;
                  } else {
                    cursor++;
                  }
                } else {
                  break CaptureLoop;
                }
            }
          }

          /**
           * Produce the token.
           */
          trimWhitespace();

          return createPropertyToken(st, cursor);
        } else {
          switch (expr[cursor]) {
            case '.': {
              cursor++;
              if (isDigit(expr[cursor])) {
                capture = true;
                continue;
              }
              expectNextChar_IW('{');

              return lastNode = new ThisWithNode(expr, st, cursor - st - 1
                      , cursor + 1,
                      (cursor = balancedCaptureWithLineAccounting(expr,
                              cursor, end, '{', pCtx) + 1) - 3, fields, pCtx);
            }

            case '@': {
              st++;
              captureToEOT();

              if (pCtx == null || (pCtx.getInterceptors() == null || !pCtx.getInterceptors().
                      containsKey(name = new String(expr, st, cursor - st)))) {
                throw new CompileException("reference to undefined interceptor: "
                        + new String(expr, st, cursor - st), expr, st);
              }

              return lastNode = new InterceptorWrapper(pCtx.getInterceptors().get(name), nextToken());
            }

            case '=':
              return createOperator(expr, st, (cursor += 2));

            case '-':
              if (lookAhead() == '-') {
                cursor += 2;
                skipWhitespace();
                st = cursor;
                captureIdentifier();

                name = new String(subArray(st, cursor));
                if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                  return lastNode = new IndexedPreFixDecNode(idx, pCtx);
                } else {
                  return lastNode = new PreFixDecNode(name, pCtx);
                }
              } else if ((cursor == start || (lastNode != null &&
                      (lastNode instanceof BooleanNode || lastNode.isOperator())))
                      && !isDigit(lookAhead())) {

                captureToEOT();
                return new Sign(expr, st, cursor - st, fields, pCtx);
              } else if ((cursor != start && !isWhitespace(expr[cursor - 1]) && (
                      !(lastNode != null && (lastNode instanceof BooleanNode || lastNode.isOperator()))))
                      || !isDigit(lookAhead())) {

                return createOperator(expr, st, cursor++ + 1);
              } else if ((cursor - 1) != start || (!isDigit(expr[cursor - 1])) && isDigit(lookAhead())) {
                cursor++;
                break;
              } else {
                throw new CompileException("not a statement", expr, st);
              }

            case '+':
              if (lookAhead() == '+') {
                cursor += 2;
                skipWhitespace();
                st = cursor;
                captureIdentifier();

                name = new String(subArray(st, cursor));
                if (pCtx != null && (idx = pCtx.variableIndexOf(name)) != -1) {
                  return lastNode = new IndexedPreFixIncNode(idx, pCtx);
                } else {
                  return lastNode = new PreFixIncNode(name, pCtx);
                }
              }
              return createOperator(expr, st, cursor++ + 1);

            case '*':
              if (lookAhead() == '*') {
                cursor++;
              }
              return createOperator(expr, st, cursor++ + 1);

            case ';':
              cursor++;
              lastWasIdentifier = false;
              return lastNode = new EndOfStatement();

            case '#':
            case '/':
            case '?':
            case ':':
            case '^':
            case '%': {
              return createOperator(expr, st, cursor++ + 1);
            }

            case '(': {
              cursor++;

              boolean singleToken = true;

              skipWhitespace();
              for (brace = 1; cursor != end && brace != 0; cursor++) {
                switch (expr[cursor]) {
                  case '(':
                    brace++;
                    break;
                  case ')':
                    brace--;
                    break;
                  case '\'':
                    cursor = captureStringLiteral('\'', expr, cursor, end);
                    break;
                  case '"':
                    cursor = captureStringLiteral('"', expr, cursor, end);
                    break;
                  case 'i':
                    if (brace == 1 && isWhitespace(lookBehind()) && lookAhead() == 'n' && isWhitespace(lookAhead(2))) {

                      for (int level = brace; cursor != end; cursor++) {
                        switch (expr[cursor]) {
                          case '(':
                            brace++;
                            break;
                          case ')':
                            if (--brace < level) {
                              cursor++;
                              if (tokenContinues()) {
                                lastNode = new Fold(expr, trimRight(st + 1),
                                        cursor - st - 2, fields, pCtx);
                                if (expr[st = cursor] == '.') st++;
                                captureToEOT();
                                return lastNode = new Union(expr, st = trimRight(st),
                                        cursor - st, fields, lastNode);
                              } else {
                                return lastNode = new Fold(expr, trimRight(st + 1),
                                        cursor - st - 2, fields, pCtx);
                              }
                            }
                            break;
                          case '\'':
                            cursor = captureStringLiteral('\'', expr, cursor, end);
                            break;
                          case '"':
                            cursor = captureStringLiteral('\"', expr, cursor, end);
                            break;
                        }
                      }

                      throw new CompileException("unterminated projection; closing parathesis required",
                              expr, st);
                    }
                    break;

                  default:
                    /**
                     * Check to see if we should disqualify this current token as a potential
                     * type-cast candidate.
                     */

                    if (expr[cursor] != '.') {
                      switch (expr[cursor]) {
                        case '[':
                        case ']':
                          break;

                        default:
                          if (!(isIdentifierPart(expr[cursor]) || expr[cursor] == '.')) {
                            singleToken = false;
                          }
                      }
                    }
                }
              }

              if (brace != 0) {
                throw new CompileException("unbalanced braces in expression: (" + brace + "):",
                        expr, st);
              }

              tmpStart = -1;
              if (singleToken) {
                int _st;
                TypeDescriptor tDescr = new TypeDescriptor(expr, _st = trimRight(st + 1),
                        trimLeft(cursor - 1) - _st, fields);

                Class cls;
                try {
                  if (tDescr.isClass() && (cls = getClassReference(pCtx, tDescr)) != null) {
                    st = cursor;
                    captureToEOS();

                    return lastNode = new TypeCast(expr, st, cursor - st,
                            cls, fields, pCtx);
                  }
                }
                catch (ClassNotFoundException e) {
                  // fallthrough
                }

              }

              if (tmpStart != -1) {
                return handleUnion(handleSubstatement(new Substatement(expr, tmpStart, cursor - tmpStart, fields, pCtx)));
              } else {
                return handleUnion(
                        handleSubstatement(
                                new Substatement(expr, st = trimRight(st + 1),
                                        trimLeft(cursor - 1) - st, fields, pCtx)));
              }
            }

            case '}':
            case ']':
            case ')': {
              throw new CompileException("unbalanced braces", expr, st);
            }

            case '>': {
              switch (expr[cursor + 1]) {
                case '>':
                  if (expr[cursor += 2] == '>') cursor++;
                  return createOperator(expr, st, cursor);
                case '=':
                  return createOperator(expr, st, cursor += 2);
                default:
                  return createOperator(expr, st, ++cursor);
              }
            }

            case '<': {
              if (expr[++cursor] == '<') {
                if (expr[++cursor] == '<') cursor++;
                return createOperator(expr, st, cursor);
              } else if (expr[cursor] == '=') {
                return createOperator(expr, st, ++cursor);
              } else {
                return createOperator(expr, st, cursor);
              }
            }

            case '\'':
            case '"':
              lastNode = new LiteralNode(handleStringEscapes(subset(expr, st + 1,
                      (cursor = captureStringLiteral(expr[cursor], expr, cursor, end)) - st - 1))
                      , String.class);

              cursor++;

              if (tokenContinues()) {
                return lastNode = handleUnion(lastNode);
              }

              return lastNode;

            case '&': {
              if (expr[cursor++ + 1] == '&') {
                return createOperator(expr, st, ++cursor);
              } else {
                return createOperator(expr, st, cursor);
              }
            }

            case '|': {
              if (expr[cursor++ + 1] == '|') {
                return createOperator(expr, st, ++cursor);
              } else {
                return createOperator(expr, st, cursor);
              }
            }

            case '~':
              if ((cursor++ - 1 != 0 || !isIdentifierPart(lookBehind()))
                      && isDigit(expr[cursor])) {
                st = cursor;
                captureToEOT();
                return lastNode = new Invert(expr, st, cursor - st, fields, pCtx);
              } else if (expr[cursor] == '(') {
                st = cursor--;
                captureToEOT();
                return lastNode = new Invert(expr, st, cursor - st, fields, pCtx);
              } else {
                if (expr[cursor] == '=') cursor++;
                return createOperator(expr, st, cursor);
              }

            case '!': {
              ++cursor;
              if (isNextIdentifier()) {
                if (lastNode != null && !lastNode.isOperator()) {
                  throw new CompileException("unexpected operator '!'", expr, st);
                }

                st = cursor;
                captureToEOT();
                if ("new".equals(name = new String(expr, st, cursor - st))
                        || "isdef".equals(name)) {
                  captureToEOT();
                  return lastNode = new Negation(expr, st, cursor - st, fields, pCtx);
                } else {
                  return lastNode = new Negation(expr, st, cursor - st, fields, pCtx);
                }
              } else if (expr[cursor] == '(') {
                st = cursor--;
                captureToEOT();
                return lastNode = new Negation(expr, st, cursor - st, fields, pCtx);
              } else if (expr[cursor] != '=')
                throw new CompileException("unexpected operator '!'", expr, st, null);
              else {
                return createOperator(expr, st, ++cursor);
              }
            }

            case '[':
            case '{':
              cursor = balancedCaptureWithLineAccounting(expr, cursor, end, expr[cursor], pCtx) + 1;
              if (tokenContinues()) {
                lastNode = new InlineCollectionNode(expr, st, cursor - st, fields, pCtx);
                st = cursor;
                captureToEOT();
                if (expr[st] == '.') st++;

                return lastNode = new Union(expr, st, cursor - st, fields, lastNode);
              } else {
                return lastNode = new InlineCollectionNode(expr, st, cursor - st, fields, pCtx);
              }

            default:
              cursor++;
          }
        }
      }

      if (st == cursor)
        return null;
      else
        return createPropertyToken(st, cursor);
    }
    catch (RedundantCodeException e) {
      return nextToken();
    }
    catch (NumberFormatException e) {
      throw new CompileException("badly formatted number: " + e.getMessage(), expr, st, e);
    }
    catch (StringIndexOutOfBoundsException e) {
      throw new CompileException("unexpected end of statement", expr, cursor, e);
    }
    catch (ArrayIndexOutOfBoundsException e) {
      throw new CompileException("unexpected end of statement", expr, cursor, e);
    }
    catch (CompileException e) {
      throw ErrorUtil.rewriteIfNeeded(e, expr, cursor);
    }
  }

  public ASTNode handleSubstatement(Substatement stmt) {
    if (stmt.getStatement() != null && stmt.getStatement().isLiteralOnly()) {
      return new LiteralNode(stmt.getStatement().getValue(null, null, null));
    } else {
      return stmt;
    }
  }

  /**
   * Handle a union between a closed statement and a residual property chain.
   *
   * @param node an ast node
   * @return ASTNode
   */
  protected ASTNode handleUnion(ASTNode node) {
    if (cursor != end) {
      skipWhitespace();
      int union = -1;
      if (cursor < end) {
        switch (expr[cursor]) {
          case '.':
            union = cursor + 1;
            break;
          case '[':
            union = cursor;
        }
      }

      if (union != -1) {
        captureToEOT();
        return lastNode = new Union(expr, union, cursor - union, fields, node);
      }

    }
    return lastNode = node;
  }

  /**
   * Create an operator node.
   *
   * @param expr  an char[] containing the expression
   * @param start the start offet for the token
   * @param end   the end offset for the token
   * @return ASTNode
   */
  private ASTNode createOperator(final char[] expr, final int start, final int end) {
    lastWasIdentifier = false;
    return lastNode = new OperatorNode(OPERATORS.get(new String(expr, start, end - start)), expr, start);
  }

  /**
   * Create a copy of an array based on a sub-range.  Works faster than System.arrayCopy() for arrays shorter than
   * 1000 elements in most cases, so the parser uses this internally.
   *
   * @param start the start offset
   * @param end   the end offset
   * @return an array
   */
  private char[] subArray(final int start, final int end) {
    if (start >= end) return new char[0];

    char[] newA = new char[end - start];
    for (int i = 0; i != newA.length; i++) {
      newA[i] = expr[i + start];
    }

    return newA;
  }

  /**
   * Generate a property token
   *
   * @param st  the start offset
   * @param end the end offset
   * @return an ast node
   */
  private ASTNode createPropertyToken(int st, int end) {
    String tmp;

    if (isPropertyOnly(expr, st, end)) {
      if (pCtx != null && pCtx.hasImports()) {
        int find;

        if ((find = findFirst('.', st, end - st, expr)) != -1) {
          String iStr = new String(expr, st, find - st);
          if (pCtx.hasImport(iStr)) {
            lastWasIdentifier = true;
            return lastNode = new LiteralDeepPropertyNode(expr, find + 1, end - find - 1, fields,
                    pCtx.getImport(iStr));
          }
        } else {
          if (pCtx.hasImport(tmp = new String(expr, st, cursor - st))) {
            lastWasIdentifier = true;
            return lastNode = new LiteralNode(pCtx.getStaticOrClassImport(tmp));
          }
        }
      }

      if (LITERALS.containsKey(tmp = new String(expr, st, end - st))) {
        lastWasIdentifier = true;
        return lastNode = new LiteralNode(LITERALS.get(tmp));
      } else if (OPERATORS.containsKey(tmp)) {
        lastWasIdentifier = false;
        return lastNode = new OperatorNode(OPERATORS.get(tmp), expr, st);
      } else if (lastWasIdentifier) {
        return procTypedNode(true);
      }
    }

    lastWasIdentifier = true;

    return lastNode = new ASTNode(expr, trimRight(st), trimLeft(end) - st, fields);
  }

  /**
   * Process the current typed node
   *
   * @param decl node is a declaration or not
   * @return and ast node
   */
  private ASTNode procTypedNode(boolean decl) {
    while (true) {
      if (lastNode.getLiteralValue() instanceof String) {
        char[] tmp = ((String) lastNode.getLiteralValue()).toCharArray();
        TypeDescriptor tDescr = new TypeDescriptor(tmp, 0, tmp.length, 0);

        try {
          lastNode.setLiteralValue(getClassReference(pCtx, tDescr));
          lastNode.discard();
        }
        catch (Exception e) {
          // fall through;
        }
      }

      if (lastNode.isLiteral() && lastNode.getLiteralValue() instanceof Class) {
        lastNode.discard();

        captureToEOS();

        if (decl) {
          splitAccumulator.add(new DeclTypedVarNode(new String(expr, st, cursor - st), expr, st, cursor - st,
                  (Class) lastNode.getLiteralValue(), fields | ASTNode.ASSIGN, pCtx));
        } else {
          captureToEOS();
          splitAccumulator.add(new TypedVarNode(expr, st, cursor - st - 1, fields | ASTNode.ASSIGN, (Class)
                  lastNode.getLiteralValue(), pCtx));
        }
      } else if (lastNode instanceof Proto) {
        captureToEOS();
        if (decl) {
          splitAccumulator.add(new DeclProtoVarNode(new String(expr, st, cursor - st),
                  (Proto) lastNode, fields | ASTNode.ASSIGN, pCtx));
        } else {
          splitAccumulator.add(new ProtoVarNode(expr, st, cursor - st, fields | ASTNode.ASSIGN, (Proto)
                  lastNode, pCtx));
        }
      }

      // this redundant looking code is needed to work with the interpreter and MVELSH properly.
      else if ((fields & ASTNode.COMPILE_IMMEDIATE) == 0) {
        if (stk.peek() instanceof Class) {
          captureToEOS();
          if (decl) {
            splitAccumulator.add(new DeclTypedVarNode(new String(expr, st, cursor - st), expr, st, cursor - st,
                    (Class) stk.pop(), fields | ASTNode.ASSIGN, pCtx));
          } else {
            splitAccumulator.add(new TypedVarNode(expr, st, cursor - st,
                    fields | ASTNode.ASSIGN, (Class) stk.pop(), pCtx));
          }
        } else if (stk.peek() instanceof Proto) {
          captureToEOS();
          if (decl) {
            splitAccumulator.add(new DeclProtoVarNode(new String(expr, st, cursor - st),
                    (Proto) stk.pop(), fields | ASTNode.ASSIGN, pCtx));
          } else {
            splitAccumulator.add(new ProtoVarNode(expr, st, cursor - st, fields | ASTNode.ASSIGN, (Proto)
                    stk.pop(), pCtx));
          }
        } else {
          throw new CompileException("unknown class or illegal statement: " + lastNode.getLiteralValue(), expr, cursor);
        }
      } else {
        throw new CompileException("unknown class or illegal statement: " + lastNode.getLiteralValue(), expr, cursor);
      }

      skipWhitespace();
      if (cursor < end && expr[cursor] == ',') {
        st = ++cursor;
        splitAccumulator.add(new EndOfStatement());
      } else {
        return (ASTNode) splitAccumulator.pop();
      }
    }
  }

  /**
   * Generate a code block token.
   *
   * @param condStart  the start offset for the condition
   * @param condEnd    the end offset for the condition
   * @param blockStart the start offset for the block
   * @param blockEnd   the end offset for the block
   * @param type       the type of block
   * @return and ast node
   */
  private ASTNode createBlockToken(final int condStart,
                                   final int condEnd, final int blockStart, final int blockEnd, int type) {
    lastWasIdentifier = false;
    cursor++;

    if (isStatementNotManuallyTerminated()) {
      splitAccumulator.add(new EndOfStatement());
    }

    int condOffset = condEnd - condStart;
    int blockOffset = blockEnd - blockStart;

    if (blockOffset < 0) blockOffset = 0;

    switch (type) {
      case ASTNode.BLOCK_IF:
        return new IfNode(expr, condStart, condOffset, blockStart, blockOffset, fields, pCtx);
      case ASTNode.BLOCK_FOR:
        for (int i = condStart; i < condEnd; i++) {
          if (expr[i] == ';')
            return new ForNode(expr, condStart, condOffset, blockStart, blockOffset, fields, pCtx);
          else if (expr[i] == ':')
            break;
        }
      case ASTNode.BLOCK_FOREACH:
        return new ForEachNode(expr, condStart, condOffset, blockStart, blockOffset, fields, pCtx);
      case ASTNode.BLOCK_WHILE:
        return new WhileNode(expr, condStart, condOffset, blockStart, blockOffset, fields, pCtx);
      case ASTNode.BLOCK_UNTIL:
        return new UntilNode(expr, condStart, condOffset, blockStart, blockOffset, fields, pCtx);
      case ASTNode.BLOCK_DO:
        return new DoNode(expr, condStart, condOffset, blockStart, blockOffset, fields, pCtx);
      case ASTNode.BLOCK_DO_UNTIL:
        return new DoUntilNode(expr, condStart, condOffset, blockStart, blockOffset, pCtx);
      default:
        return new WithNode(expr, condStart, condOffset, blockStart, blockOffset, fields, pCtx);
    }
  }

  /**
   * Capture a code block by type.
   *
   * @param type the block type
   * @return an ast node
   */
  private ASTNode captureCodeBlock(int type) {
    boolean cond = true;

    ASTNode first = null;
    ASTNode tk = null;

    switch (type) {
      case ASTNode.BLOCK_IF: {
        do {
          if (tk != null) {
            captureToNextTokenJunction();
            skipWhitespace();
            cond = expr[cursor] != '{' && expr[cursor] == 'i' && expr[++cursor] == 'f'
                    && expr[cursor = incNextNonBlank()] == '(';
          }

          if (((IfNode) (tk = _captureBlock(tk, expr, cond, type))).getElseBlock() != null) {
            cursor++;
            return first;
          }

          if (first == null) first = tk;

          if (cursor != end && expr[cursor] != ';') {
            cursor++;
          }
        }
        while (ifThenElseBlockContinues());

        return first;
      }

      case ASTNode.BLOCK_DO:
        skipWhitespace();
        return _captureBlock(null, expr, false, type);

      default: // either BLOCK_WITH or BLOCK_FOREACH
        captureToNextTokenJunction();
        skipWhitespace();
        return _captureBlock(null, expr, true, type);
    }
  }

  private ASTNode _captureBlock(ASTNode node, final char[] expr, boolean cond, int type) {
    skipWhitespace();
    int startCond = 0;
    int endCond = 0;

    int blockStart;
    int blockEnd;

    String name;

    /**
     * Functions are a special case we handle differently from the rest of block parsing
     */
    switch (type) {
      case FUNCTION: {
        int st = cursor;

        captureToNextTokenJunction();

        if (cursor == end) {
          throw new CompileException("unexpected end of statement", expr, st);
        }

        /**
         * Check to see if the name is legal.
         */
        if (isReservedWord(name = createStringTrimmed(expr, st, cursor - st))
                || isNotValidNameorLabel(name))
          throw new CompileException("illegal function name or use of reserved word", expr, cursor);

        if (pCtx == null) pCtx = getParserContext();

        FunctionParser parser = new FunctionParser(name, cursor, end - cursor, expr, fields, pCtx, splitAccumulator);
        Function function = parser.parse();
        cursor = parser.getCursor();

        return lastNode = function;
      }
      case PROTO:
        if (ProtoParser.isUnresolvedWaiting()) {
          if (pCtx == null) pCtx = getParserContext();
          ProtoParser.checkForPossibleUnresolvedViolations(expr, cursor, pCtx);
        }

        int st = cursor;
        captureToNextTokenJunction();

        if (isReservedWord(name = createStringTrimmed(expr, st, cursor - st))
                || isNotValidNameorLabel(name))
          throw new CompileException("illegal prototype name or use of reserved word", expr, cursor);

        if (expr[cursor = nextNonBlank()] != '{') {
          throw new CompileException("expected '{' but found: " + expr[cursor], expr, cursor);
        }

        cursor = balancedCaptureWithLineAccounting(expr, st = cursor + 1, end, '{', pCtx);

        if (pCtx == null) pCtx = getParserContext();

        ProtoParser parser = new ProtoParser(expr, st, cursor, name, pCtx, fields, splitAccumulator);
        Proto proto = parser.parse();

        if (pCtx == null) pCtx = getParserContext();

        pCtx.addImport(proto);

        proto.setCursorPosition(st, cursor);
        cursor = parser.getCursor();

        ProtoParser.notifyForLateResolution(proto);

        return lastNode = proto;

      default:
        if (cond) {
          if (expr[cursor] != '(') {
            throw new CompileException("expected '(' but encountered: " + expr[cursor], expr, cursor);
          }

          /**
           * This block is an: IF, FOREACH or WHILE node.
           */

          endCond = cursor = balancedCaptureWithLineAccounting(expr, startCond = cursor, end, '(', pCtx);

          startCond++;
          cursor++;
        }
    }

    skipWhitespace();

    if (cursor >= end) {
      throw new CompileException("unexpected end of statement", expr, end);
    } else if (expr[cursor] == '{') {
      blockEnd = cursor = balancedCaptureWithLineAccounting(expr, blockStart = cursor, end, '{', pCtx);
    } else {
      blockStart = cursor - 1;
      captureToEOSorEOL();
      blockEnd = cursor + 1;
    }

    if (type == ASTNode.BLOCK_IF) {
      IfNode ifNode = (IfNode) node;

      if (node != null) {
        if (!cond) {
          return ifNode.setElseBlock(expr, st = trimRight(blockStart + 1), trimLeft(blockEnd) - st, pCtx);
        } else {
          return ifNode.setElseIf((IfNode) createBlockToken(startCond, endCond, trimRight(blockStart + 1),
                  trimLeft(blockEnd), type));
        }
      } else {
        return createBlockToken(startCond, endCond, blockStart + 1, blockEnd, type);
      }
    } else if (type == ASTNode.BLOCK_DO) {
      cursor++;
      skipWhitespace();
      st = cursor;
      captureToNextTokenJunction();

      if ("while".equals(name = new String(expr, st, cursor - st))) {
        skipWhitespace();
        startCond = cursor + 1;
        endCond = cursor = balancedCaptureWithLineAccounting(expr, cursor, end, '(', pCtx);
        return createBlockToken(startCond, endCond, trimRight(blockStart + 1), trimLeft(blockEnd), type);
      } else if ("until".equals(name)) {
        skipWhitespace();
        startCond = cursor + 1;
        endCond = cursor = balancedCaptureWithLineAccounting(expr, cursor, end, '(', pCtx);
        return createBlockToken(startCond, endCond, trimRight(blockStart + 1), trimLeft(blockEnd),
                ASTNode.BLOCK_DO_UNTIL);
      } else {
        throw new CompileException("expected 'while' or 'until' but encountered: " + name, expr, cursor);
      }
    }
    // DON"T REMOVE THIS COMMENT!
    // else if (isFlag(ASTNode.BLOCK_FOREACH) || isFlag(ASTNode.BLOCK_WITH)) {
    else {
      return createBlockToken(startCond, endCond, trimRight(blockStart + 1), trimLeft(blockEnd), type);
    }
  }


  /**
   * Checking from the current cursor position, check to see if the if-then-else block continues.
   *
   * @return boolean value
   */
  protected boolean ifThenElseBlockContinues() {
    if ((cursor + 4) < end) {
      if (expr[cursor] != ';') cursor--;
      skipWhitespace();

      return expr[cursor] == 'e' && expr[cursor + 1] == 'l' && expr[cursor + 2] == 's' && expr[cursor + 3] == 'e'
              && (isWhitespace(expr[cursor + 4]) || expr[cursor + 4] == '{');
    }
    return false;
  }

  /**
   * Checking from the current cursor position, check to see if we're inside a contiguous identifier.
   *
   * @return -
   */
  protected boolean tokenContinues() {
    if (cursor == end) return false;
    else if (expr[cursor] == '.' || expr[cursor] == '[') return true;
    else if (isWhitespace(expr[cursor])) {
      int markCurrent = cursor;
      skipWhitespace();
      if (cursor != end && (expr[cursor] == '.' || expr[cursor] == '[')) return true;
      cursor = markCurrent;
    }
    return false;
  }

  /**
   * The parser should find a statement ending condition when this is called, otherwise everything should blow up.
   */
  protected void expectEOS() {
    skipWhitespace();
    if (cursor != end && expr[cursor] != ';') {
      switch (expr[cursor]) {
        case '&':
          if (lookAhead() == '&') return;
          else break;
        case '|':
          if (lookAhead() == '|') return;
          else break;
        case '!':
          if (lookAhead() == '=') return;
          else break;

        case '<':
        case '>':
          return;

        case '=': {
          switch (lookAhead()) {
            case '=':
            case '+':
            case '-':
            case '*':
              return;
          }
          break;
        }

        case '+':
        case '-':
        case '/':
        case '*':
          if (lookAhead() == '=') return;
          else break;
      }

      throw new CompileException("expected end of statement but encountered: "
              + (cursor == end ? "<end of stream>" : expr[cursor]), expr, cursor);
    }
  }

  /**
   * Checks to see if the next part of the statement is an identifier part.
   *
   * @return boolean true if next part is identifier part.
   */
  protected boolean isNextIdentifier() {
    while (cursor != end && isWhitespace(expr[cursor])) cursor++;
    return cursor != end && isIdentifierPart(expr[cursor]);
  }

  /**
   * Capture from the current cursor position, to the end of the statement.
   */
  protected void captureToEOS() {
    while (cursor != end) {
      switch (expr[cursor]) {
        case '(':
        case '[':
        case '{':
          if ((cursor = balancedCaptureWithLineAccounting(expr, cursor, end, expr[cursor], pCtx)) >= end)
            return;
          break;

        case '"':
        case '\'':
          cursor = captureStringLiteral(expr[cursor], expr, cursor, end);
          break;

        case ',':
        case ';':
        case '}':
          return;
      }
      cursor++;
    }
  }

  /**
   * From the current cursor position, capture to the end of statement, or the end of line, whichever comes first.
   */
  protected void captureToEOSorEOL() {
    while (cursor != end && (expr[cursor] != '\n' && expr[cursor] != '\r' && expr[cursor] != ';')) {
      cursor++;
    }
  }

  /**
   * From the current cursor position, capture to the end of the line.
   */
//  protected void captureToEOL() {
//    while (cursor != end && (expr[cursor] != '\n')) cursor++;
//  }


  /**
   * Capture to the end of the current identifier under the cursor.
   */
  protected void captureIdentifier() {
    boolean captured = false;
    if (cursor == end) throw new CompileException("unexpected end of statement: EOF", expr, cursor);
    while (cursor != end) {
      switch (expr[cursor]) {
        case ';':
          return;

        default: {
          if (!isIdentifierPart(expr[cursor])) {
            if (captured) return;
            throw new CompileException("unexpected symbol (was expecting an identifier): " + expr[cursor],
                    expr, cursor);
          } else {
            captured = true;
          }
        }
      }
      cursor++;
    }
  }

  /**
   * From the current cursor position, capture to the end of the current token.
   */
  protected void captureToEOT() {
    skipWhitespace();
    do {
      switch (expr[cursor]) {
        case '(':
        case '[':
        case '{':
          if ((cursor = balancedCaptureWithLineAccounting(expr, cursor, end, expr[cursor], pCtx)) == -1) {
            throw new CompileException("unbalanced braces", expr, cursor);
          }
          break;

        case ',':
        case '=':
        case '&':
        case '|':
        case ';':
          return;

        case '.':
          skipWhitespace();
          break;

        case '\'':
          cursor = captureStringLiteral('\'', expr, cursor, end);
          break;
        case '"':
          cursor = captureStringLiteral('"', expr, cursor, end);
          break;

        default:
          if (isWhitespace(expr[cursor])) {
            skipWhitespace();

            if (cursor < end && expr[cursor] == '.') {
              if (cursor != end) cursor++;
              skipWhitespace();
              break;
            } else {
              trimWhitespace();
              return;
            }
          }
      }
    }
    while (++cursor < end);
  }


  protected boolean lastNonWhite(char c) {
    int i = cursor - 1;
    while (isWhitespace(expr[i])) i--;
    return c == expr[i];
  }

  /**
   * From the specified cursor position, trim out any whitespace between the current position and the end of the
   * last non-whitespace character.
   *
   * @param pos - current position
   * @return new position.
   */
  protected int trimLeft(int pos) {
    if (pos > end) pos = end;
    while (pos > 0 && pos >= st && (isWhitespace(expr[pos - 1]) || expr[pos - 1] == ';')) pos--;
    return pos;
  }

  /**
   * From the specified cursor position, trim out any whitespace between the current position and beginning of the
   * first non-whitespace character.
   *
   * @param pos -
   * @return -
   */
  protected int trimRight(int pos) {
    while (pos != end && isWhitespace(expr[pos])) pos++;
    return pos;
  }

  /**
   * If the cursor is currently pointing to whitespace, move the cursor forward to the first non-whitespace
   * character, but account for carraige returns in the script (updates parser field: line).
   */
  protected void skipWhitespace() {
    Skip:
    while (cursor != end) {
      switch (expr[cursor]) {
        case '\n':
          line++;
          lastLineStart = cursor;
        case '\r':
          cursor++;
          continue;
        case '/':
          if (cursor + 1 != end) {
            switch (expr[cursor + 1]) {
              case '/':
                // expr[cursor++] = ' ';
                cursor++;
                while (cursor != end && expr[cursor] != '\n') {
                  //expr[cursor++] = ' ';
                  cursor++;
                }
                if (cursor != end) {
                  //expr[cursor++] = ' ';
                  cursor++;
                }

                line++;
                lastLineStart = cursor;

                continue;

              case '*':
                int len = end - 1;
                int st = cursor;
                cursor++;

                int countLines = 0;
                while (cursor != len && !(expr[cursor] == '*' && expr[cursor + 1] == '/')) {
                  if (expr[cursor] == '\n') {
                    countLines++;
                  }

                  cursor++;
                }
                if (cursor != len) {
                  cursor += 2;
                }

                if (countLines == 0) {
                  for (int i = st; i < cursor; i++) {
                    expr[i] = ' ';
                  }
                }

                continue;

              default:
                break Skip;

            }
          }
        default:
          if (!isWhitespace(expr[cursor])) break Skip;

      }
      cursor++;
    }
  }

  /**
   * From the current cursor position, capture to the end of the next token junction.
   */
  protected void captureToNextTokenJunction() {
    while (cursor != end) {
      switch (expr[cursor]) {
        case '{':
        case '(':
          return;
        case '[':
          cursor = balancedCaptureWithLineAccounting(expr, cursor, end, '[', pCtx) + 1;
          continue;
        default:
          if (isWhitespace(expr[cursor])) {
            return;
          }
          cursor++;
      }
    }
  }

  /**
   * From the current cursor position, trim backward over any whitespace to the first non-whitespace character.
   */
  protected void trimWhitespace() {
    while (cursor != 0 && isWhitespace(expr[cursor - 1])) cursor--;
  }

  /**
   * Set and finesse the expression, trimming an leading or proceeding whitespace.
   *
   * @param expression the expression
   */
  protected void setExpression(String expression) {
    if (expression != null && expression.length() != 0) {
      synchronized (EX_PRECACHE) {
        if ((this.expr = EX_PRECACHE.get(expression)) == null) {
          end = length = (this.expr = expression.toCharArray()).length;

          // trim any whitespace.
          while (length != 0 && isWhitespace(this.expr[length - 1])) length--;

          char[] e = new char[length];

          for (int i = 0; i != e.length; i++)
            e[i] = expr[i];

          EX_PRECACHE.put(expression, e);
        } else {
          end = length = this.expr.length;
        }
      }
    }
  }

  /**
   * Set and finesse the expression, trimming an leading or proceeding whitespace.
   *
   * @param expression the expression
   */
  protected void setExpression(char[] expression) {
    end = length = (this.expr = expression).length;
    while (length != 0 && isWhitespace(this.expr[length - 1])) length--;
  }

  /**
   * Return the previous non-whitespace character.
   *
   * @return -
   */
  protected char lookToLast() {
    if (cursor == start) return 0;
    int temp = cursor;
    for (; ; ) {
      if (temp == start || !isWhitespace(expr[--temp])) break;
    }
    return expr[temp];
  }

  /**
   * Return the last character (delta -1 of cursor position).
   *
   * @return -
   */
  protected char lookBehind() {
    if (cursor == start) return 0;
    else return expr[cursor - 1];
  }

  /**
   * Return the next character (delta 1 of cursor position).
   *
   * @return -
   */
  protected char lookAhead() {
    if (cursor + 1 != end) {
      return expr[cursor + 1];
    } else {
      return 0;
    }
  }

  /**
   * Return the character, forward of the currrent cursor position based on the specified range delta.
   *
   * @param range -
   * @return -
   */
  protected char lookAhead(int range) {
    if ((cursor + range) >= end) return 0;
    else {
      return expr[cursor + range];
    }
  }

  /**
   * Returns true if the next is an identifier or literal.
   *
   * @return true of false
   */
  protected boolean isNextIdentifierOrLiteral() {
    int tmp = cursor;
    if (tmp == end) return false;
    else {
      while (tmp != end && isWhitespace(expr[tmp])) tmp++;
      if (tmp == end) return false;
      char n = expr[tmp];
      return isIdentifierPart(n) || isDigit(n) || n == '\'' || n == '"';
    }
  }

  /**
   * Increment one cursor position, and move cursor to next non-blank part.
   *
   * @return cursor position
   */
  public int incNextNonBlank() {
    cursor++;
    return nextNonBlank();
  }

  /**
   * Move to next cursor position from current cursor position.
   *
   * @return cursor position
   */
  public int nextNonBlank() {
    if ((cursor + 1) >= end) {
      throw new CompileException("unexpected end of statement", expr, st);
    }
    int i = cursor;
    while (i != end && isWhitespace(expr[i])) i++;
    return i;
  }

  /**
   * Expect the next specified character or fail
   *
   * @param c character
   */
  public void expectNextChar_IW(char c) {
    nextNonBlank();
    if (cursor == end) throw new CompileException("unexpected end of statement", expr, st);
    if (expr[cursor] != c)
      throw new CompileException("unexpected character ('" + expr[cursor] + "'); was expecting: " + c, expr, st);
  }


  /**
   * NOTE: This method assumes that the current position of the cursor is at the end of a logical statement, to
   * begin with.
   * <p/>
   * Determines whether or not the logical statement is manually terminated with a statement separator (';').
   *
   * @return -
   */
  protected boolean isStatementNotManuallyTerminated() {
    if (cursor >= end) return false;
    int c = cursor;
    while (c != end && isWhitespace(expr[c])) c++;
    return !(c != end && expr[c] == ';');
  }

  protected ParserContext getParserContext() {
    if (parserContext == null || parserContext.get() == null) {
      newContext();
    }
    return parserContext.get();
  }

  public static ParserContext getCurrentThreadParserContext() {
    return contextControl(GET_OR_CREATE, null, null);
  }

  public static void setCurrentThreadParserContext(ParserContext pCtx) {
    contextControl(SET, pCtx, null);
  }

  /**
   * Create a new ParserContext in the current thread.
   */
  public void newContext() {
    contextControl(SET, new ParserContext(), this);
  }

  /**
   * Create a new ParserContext in the current thread, using the one specified.
   *
   * @param pCtx -
   */
  public void newContext(ParserContext pCtx) {
    contextControl(SET, this.pCtx = pCtx, this);
  }

  /**
   * Remove the current ParserContext from the thread.
   */
  public void removeContext() {
    contextControl(REMOVE, null, this);
  }

  public static ParserContext contextControl(int operation, ParserContext pCtx, AbstractParser parser) {
    synchronized (getRuntime()) {
      if (parserContext == null) parserContext = new ThreadLocal<ParserContext>();

      switch (operation) {
        case SET:
          pCtx.setRootParser(parser);
          parserContext.set(pCtx);
          return pCtx;

        case REMOVE:
          parserContext.set(null);
          return null;

        case GET_OR_CREATE:
          if (parserContext.get() == null) {
            parserContext.set(new ParserContext(parser));
          }

        case GET:
          return parserContext.get();
      }
    }

    return null;
  }

  protected static final int SET = 0;
  protected static final int REMOVE = 1;
  protected static final int GET = 2;
  protected static final int GET_OR_CREATE = 3;

  protected void addFatalError(String message) {
    pCtx.addError(new ErrorDetail(expr, st, true, message));
  }

  protected void addFatalError(String message, int start) {
    pCtx.addError(new ErrorDetail(expr, start, true, message));
  }

  public static final int LEVEL_5_CONTROL_FLOW = 5;
  public static final int LEVEL_4_ASSIGNMENT = 4;
  public static final int LEVEL_3_ITERATION = 3;
  public static final int LEVEL_2_MULTI_STATEMENT = 2;
  public static final int LEVEL_1_BASIC_LANG = 1;
  public static final int LEVEL_0_PROPERTY_ONLY = 0;

  public static void setLanguageLevel(int level) {
    OPERATORS.clear();
    OPERATORS.putAll(loadLanguageFeaturesByLevel(level));
  }

  public static HashMap<String, Integer> loadLanguageFeaturesByLevel(int languageLevel) {
    HashMap<String, Integer> operatorsTable = new HashMap<String, Integer>();
    switch (languageLevel) {
      case 6:  // prototype definition
        operatorsTable.put("proto", PROTO);

      case 5:  // control flow operations
        operatorsTable.put("if", IF);
        operatorsTable.put("else", ELSE);
        operatorsTable.put("?", TERNARY);
        operatorsTable.put("switch", SWITCH);
        operatorsTable.put("function", FUNCTION);
        operatorsTable.put("def", FUNCTION);


      case 4: // assignment
        operatorsTable.put("=", ASSIGN);
        operatorsTable.put("var", UNTYPED_VAR);
        operatorsTable.put("+=", ASSIGN_ADD);
        operatorsTable.put("-=", ASSIGN_SUB);
        operatorsTable.put("/=", ASSIGN_DIV);
        operatorsTable.put("%=", ASSIGN_MOD);

      case 3: // iteration
        operatorsTable.put("foreach", FOREACH);
        operatorsTable.put("while", WHILE);
        operatorsTable.put("until", UNTIL);
        operatorsTable.put("for", FOR);
        operatorsTable.put("do", DO);

      case 2: // multi-statement
        operatorsTable.put("return", RETURN);
        operatorsTable.put(";", END_OF_STMT);

      case 1: // boolean, math ops, projection, assertion, objection creation, block setters, imports
        operatorsTable.put("+", ADD);
        operatorsTable.put("-", SUB);
        operatorsTable.put("*", MULT);
        operatorsTable.put("**", POWER);
        operatorsTable.put("/", DIV);
        operatorsTable.put("%", MOD);
        operatorsTable.put("==", EQUAL);
        operatorsTable.put("!=", NEQUAL);
        operatorsTable.put(">", GTHAN);
        operatorsTable.put(">=", GETHAN);
        operatorsTable.put("<", LTHAN);
        operatorsTable.put("<=", LETHAN);
        operatorsTable.put("&&", AND);
        operatorsTable.put("and", AND);
        operatorsTable.put("||", OR);
        operatorsTable.put("or", CHOR);
        operatorsTable.put("~=", REGEX);
        operatorsTable.put("instanceof", INSTANCEOF);
        operatorsTable.put("is", INSTANCEOF);
        operatorsTable.put("contains", CONTAINS);
        operatorsTable.put("soundslike", SOUNDEX);
        operatorsTable.put("strsim", SIMILARITY);
        operatorsTable.put("convertable_to", CONVERTABLE_TO);
        operatorsTable.put("isdef", ISDEF);

        operatorsTable.put("#", STR_APPEND);

        operatorsTable.put("&", BW_AND);
        operatorsTable.put("|", BW_OR);
        operatorsTable.put("^", BW_XOR);
        operatorsTable.put("<<", BW_SHIFT_LEFT);
        operatorsTable.put("<<<", BW_USHIFT_LEFT);
        operatorsTable.put(">>", BW_SHIFT_RIGHT);
        operatorsTable.put(">>>", BW_USHIFT_RIGHT);

        operatorsTable.put("new", Operator.NEW);
        operatorsTable.put("in", PROJECTION);

        operatorsTable.put("with", WITH);

        operatorsTable.put("assert", ASSERT);
        operatorsTable.put("import", IMPORT);
        operatorsTable.put("import_static", IMPORT_STATIC);

        operatorsTable.put("++", INC);
        operatorsTable.put("--", DEC);

      case 0: // Property access and inline collections
        operatorsTable.put(":", TERNARY_ELSE);
    }
    return operatorsTable;
  }

  /**
   * Remove the current parser context from the thread.
   */
  public static void resetParserContext() {
    contextControl(REMOVE, null, null);
  }

  protected static boolean isArithmeticOperator(int operator) {
    return operator != -1 && operator < 6;
  }

  /**
   * Reduce the current operations on the stack.
   *
   * @param operator the operator
   * @return a stack control code
   */
  protected int arithmeticFunctionReduction(int operator) {
    ASTNode tk;
    int operator2;

    /**
     * If the next token is an operator, we check to see if it has a higher
     * precdence.
     */
    if ((tk = nextToken()) != null) {
      if (isArithmeticOperator(operator2 = tk.getOperator()) && PTABLE[operator2] > PTABLE[operator]) {
        stk.xswap();
        /**
         * The current arith. operator is of higher precedence the last.
         */

        tk = nextToken();

        /**
         * Check to see if we're compiling or executing interpretively.  If we're compiling, we really
         * need to stop if this is not a literal.
         */
        if (compileMode && !tk.isLiteral()) {


          splitAccumulator.push(tk, new OperatorNode(operator2, expr, st));
          return OP_OVERFLOW;
        }

        dStack.push(operator = operator2, tk.getReducedValue(ctx, ctx, variableFactory));

        while (true) {
          // look ahead again
          if ((tk = nextToken()) != null && (operator2 = tk.getOperator()) != -1
                  && operator2 != 37 && PTABLE[operator2] > PTABLE[operator]) {
            // if we have back to back operations on the stack, we don't xswap

            if (dStack.isReduceable()) {
              stk.copyx2(dStack);
            }

            /**
             * This operator is of higher precedence, or the same level precedence.  push to the RHS.
             */
            dStack.push(operator = operator2, nextToken().getReducedValue(ctx, ctx, variableFactory));

            continue;
          } else if (tk != null && operator2 != -1 && operator2 != 37) {
            if (PTABLE[operator2] == PTABLE[operator]) {
              if (!dStack.isEmpty()) dreduce();
              else {
                while (stk.isReduceable()) {
                  stk.xswap_op();
                }
              }

              /**
               * This operator is of the same level precedence.  push to the RHS.
               */

              dStack.push(operator = operator2, nextToken().getReducedValue(ctx, ctx, variableFactory));

              continue;
            } else {
              /**
               * The operator doesn't have higher precedence. Therfore reduce the LHS.
               */
              while (dStack.size() > 1) {
                dreduce();
              }

              operator = tk.getOperator();
              // Reduce the lesser or equal precedence operations.
              while (stk.size() != 1 && stk.peek2() instanceof Integer &&
                      ((operator2 = (Integer) stk.peek2()) < PTABLE.length) &&
                      PTABLE[operator2] >= PTABLE[operator]) {
                stk.xswap_op();
              }
            }
          } else {
            /**
             * There are no more tokens.
             */

            if (dStack.size() > 1) {
              dreduce();
            }

            if (stk.isReduceable()) stk.xswap();

            break;
          }

          if ((tk = nextToken()) != null) {
            switch (operator) {
              case AND: {
                if (!(stk.peekBoolean())) return OP_TERMINATE;
                else {
                  splitAccumulator.add(tk);
                  return AND;
                }
              }
              case OR: {
                if ((stk.peekBoolean())) return OP_TERMINATE;
                else {
                  splitAccumulator.add(tk);
                  return OR;
                }
              }

              default:
                stk.push(operator, tk.getReducedValue(ctx, ctx, variableFactory));
            }
          }
        }
      } else if (!tk.isOperator()) {
        throw new CompileException("unexpected token: " + tk.getName(), expr, st);
      } else {
        reduce();
        splitAccumulator.push(tk);
      }
    }

    // while any values remain on the stack
    // keep XSWAPing and reducing, until there is nothing left.
    if (stk.isReduceable()) {
      while (true) {
        reduce();
        if (stk.isReduceable()) {
          stk.xswap();
        } else {
          break;
        }
      }
    }

    return OP_RESET_FRAME;
  }

  private void dreduce() {
    stk.copy2(dStack);
    stk.op();
  }

  /**
   * This method is called when we reach the point where we must subEval a trinary operation in the expression.
   * (ie. val1 op val2).  This is not the same as a binary operation, although binary operations would appear
   * to have 3 structures as well.  A binary structure (or also a junction in the expression) compares the
   * current state against 2 downrange structures (usually an op and a val).
   */
  protected void reduce() {
    Object v1, v2;
    int operator;
    try {
      switch (operator = (Integer) stk.pop()) {
        case ADD:
        case SUB:
        case DIV:
        case MULT:
        case MOD:
        case EQUAL:
        case NEQUAL:
        case GTHAN:
        case LTHAN:
        case GETHAN:
        case LETHAN:
        case POWER:
          stk.op(operator);
          break;

        case AND:
          v1 = stk.pop();
          stk.push(((Boolean) stk.pop()) && ((Boolean) v1));
          break;

        case OR:
          v1 = stk.pop();
          stk.push(((Boolean) stk.pop()) || ((Boolean) v1));
          break;

        case CHOR:
          v1 = stk.pop();
          if (!isEmpty(v2 = stk.pop()) || !isEmpty(v1)) {
            stk.clear();
            stk.push(!isEmpty(v2) ? v2 : v1);
            return;
          } else stk.push(null);
          break;

        case REGEX:
          stk.push(java.util.regex.Pattern.compile(java.lang.String.valueOf(stk.pop()))
                  .matcher(java.lang.String.valueOf(stk.pop())).matches());
          break;

        case INSTANCEOF:
          stk.push(((Class) stk.pop()).isInstance(stk.pop()));
          break;

        case CONVERTABLE_TO:
          stk.push(org.mvel2.DataConversion.canConvert(stk.peek2().getClass(), (Class) stk.pop2()));
          break;

        case CONTAINS:
          stk.push(containsCheck(stk.peek2(), stk.pop2()));
          break;

        case BW_AND:
          stk.push(asInt(stk.peek2()) & asInt(stk.pop2()));
          break;

        case BW_OR:
          stk.push(asInt(stk.peek2()) | asInt(stk.pop2()));
          break;

        case BW_XOR:
          stk.push(asInt(stk.peek2()) ^ asInt(stk.pop2()));
          break;

        case BW_SHIFT_LEFT:
          stk.push(asInt(stk.peek2()) << asInt(stk.pop2()));
          break;

        case BW_USHIFT_LEFT:
          int iv2 = asInt(stk.peek2());
          if (iv2 < 0) iv2 *= -1;
          stk.push(iv2 << asInt(stk.pop2()));
          break;

        case BW_SHIFT_RIGHT:
          stk.push(asInt(stk.peek2()) >> asInt(stk.pop2()));
          break;

        case BW_USHIFT_RIGHT:
          stk.push(asInt(stk.peek2()) >>> asInt(stk.pop2()));
          break;

        case SOUNDEX:
          stk.push(soundex(java.lang.String.valueOf(stk.pop()))
                  .equals(soundex(java.lang.String.valueOf(stk.pop()))));
          break;

        case SIMILARITY:
          stk.push(similarity(java.lang.String.valueOf(stk.pop()), java.lang.String.valueOf(stk.pop())));
          break;
      }
    }
    catch (ClassCastException e) {
      throw new CompileException("syntax error or incompatable types", expr, st, e);
    }
    catch (ArithmeticException e) {
      throw new CompileException("arithmetic error: " + e.getMessage(), expr, st, e);
    }
    catch (Exception e) {
      throw new CompileException("failed to subEval expression", expr, st, e);
    }
  }

  public int getCursor() {
    return cursor;
  }

  public char[] getExpression() {
    return expr;
  }

  private static int asInt(final Object o) {
    return (Integer) o;
  }

  public void setPCtx(ParserContext pCtx) {
    this.debugSymbols = (this.pCtx = pCtx).isDebugSymbols();
  }
}
