whole_func_string: function() {
      let result, expr;
      if (this.token === "@") return this.node("silent")(this.next().read_expr());
      if (this.token === "+")
        return this.node("unary")("+", this.next().read_expr());
      if (this.token === "-")
        return this.node("unary")("-", this.next().read_expr());
      if (this.token === "!")
        return this.node("unary")("!", this.next().read_expr());
      if (this.token === "~")
        return this.node("unary")("~", this.next().read_expr());
  
      if (this.token === "(") {
        expr = this.next().read_expr();
        expr.parenthesizedExpression = true;
        this.expect(")") && this.next();
        return this.handleDereferencable(expr);
      }
  
      if (this.token === "`") {
        // https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L1048
        return this.read_encapsed_string("`");
      }
  
      if (this.token === this.tok.T_LIST) {
        let assign = null;
        const isInner = this.innerList;
        result = this.node("list");
        if (!isInner) {
          assign = this.node("assign");
        }
        if (this.next().expect("(")) {
          this.next();
        }
  
        if (!this.innerList) this.innerList = true;
  
        // reads inner items
        const assignList = this.read_array_pair_list(false);
        if (this.expect(")")) {
          this.next();
        }
  
        // check if contains at least one assignment statement
        let hasItem = false;
        for (let i = 0; i < assignList.length; i++) {
          if (assignList[i] !== null) {
            hasItem = true;
            break;
          }
        }
        if (!hasItem) {
          this.raiseError(
            "Fatal Error :  Cannot use empty list on line " +
              this.lexer.yylloc.first_line
          );
        }
  
        // handles the node resolution
        if (!isInner) {
          this.innerList = false;
          if (this.expect("=")) {
            return assign(
              result(assignList, false),
              this.next().read_expr(),
              "="
            );
          } else {
            // error fallback : list($a, $b);
            return result(assignList, false);
          }
        } else {
          return result(assignList, false);
        }
      }
  
      if (this.token === this.tok.T_CLONE)
        return this.node("clone")(this.next().read_expr());
  
      switch (this.token) {
        case this.tok.T_INC:
          return this.node("pre")(
            "+",
            this.next().read_variable(false, false, false)
          );
  
        case this.tok.T_DEC:
          return this.node("pre")(
            "-",
            this.next().read_variable(false, false, false)
          );
  
        case this.tok.T_NEW:
          return this.read_new_expr();
  
        case this.tok.T_ISSET: {
          result = this.node("isset");
          if (this.next().expect("(")) {
            this.next();
          }
          const variables = this.read_list(this.read_expr, ",");
          if (this.expect(")")) {
            this.next();
          }
          return result(variables);
        }
        case this.tok.T_EMPTY: {
          result = this.node("empty");
          if (this.next().expect("(")) {
            this.next();
          }
          const expression = this.read_expr();
          if (this.expect(")")) {
            this.next();
          }
          return result(expression);
        }
        case this.tok.T_INCLUDE:
          return this.node("include")(false, false, this.next().read_expr());
  
        case this.tok.T_INCLUDE_ONCE:
          return this.node("include")(true, false, this.next().read_expr());
  
        case this.tok.T_REQUIRE:
          return this.node("include")(false, true, this.next().read_expr());
  
        case this.tok.T_REQUIRE_ONCE:
          return this.node("include")(true, true, this.next().read_expr());
  
        case this.tok.T_EVAL:
          result = this.node("eval");
          if (this.next().expect("(")) {
            this.next();
          }
          expr = this.read_expr();
          if (this.expect(")")) {
            this.next();
          }
          return result(expr);
  
        case this.tok.T_INT_CAST:
          return this.read_expr_cast("int");
  
        case this.tok.T_DOUBLE_CAST:
          return this.read_expr_cast("float");
  
        case this.tok.T_STRING_CAST:
          return this.read_expr_cast(
            this.text().indexOf("binary") !== -1 ? "binary" : "string"
          );
  
        case this.tok.T_ARRAY_CAST:
          return this.read_expr_cast("array");
  
        case this.tok.T_OBJECT_CAST:
          return this.read_expr_cast("object");
  
        case this.tok.T_BOOL_CAST:
          return this.read_expr_cast("bool");
  
        case this.tok.T_UNSET_CAST:
          return this.read_expr_cast("unset");
  
        case this.tok.T_EXIT: {
          const useDie = this.lexer.yytext.toLowerCase() === "die";
          result = this.node("exit");
          let status = null;
          if (this.next().token === "(") {
            if (this.next().token !== ")") {
              status = this.read_expr();
              if (this.expect(")")) {
                this.next();
              }
            } else {
              this.next();
            }
          }
          return result(status, useDie);
        }
  
        case this.tok.T_PRINT:
          return this.node("print")(this.next().read_expr());
  
        // T_YIELD (expr (T_DOUBLE_ARROW expr)?)?
        case this.tok.T_YIELD: {
          let value = null;
          let key = null;
          result = this.node("yield");
          if (this.next().is("EXPR")) {
            // reads the yield return value
            value = this.read_expr();
            if (this.token === this.tok.T_DOUBLE_ARROW) {
              // reads the yield returned key
              key = value;
              value = this.next().read_expr();
            }
          }
          return result(value, key);
        }
  
        // T_YIELD_FROM expr
        case this.tok.T_YIELD_FROM:
          result = this.node("yieldfrom");
          expr = this.next().read_expr();
          return result(expr);
  
        case this.tok.T_FUNCTION:
          return this.read_function(true);
  
        case this.tok.T_STATIC: {
          const backup = [this.token, this.lexer.getState()];
          if (this.next().token === this.tok.T_FUNCTION) {
            // handles static function
            return this.read_function(true, [0, 1, 0]);
          } else {
            // rollback
            this.lexer.tokens.push(backup);
            this.next();
          }
        }
      }
  
      // SCALAR | VARIABLE
      if (this.is("VARIABLE")) {
        result = this.node();
        expr = this.read_variable(false, false, false);
  
        // https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L877
        // should accept only a variable
        const isConst =
          expr.kind === "identifier" ||
          (expr.kind === "staticlookup" && expr.offset.kind === "identifier");
  
        // VARIABLES SPECIFIC OPERATIONS
        switch (this.token) {
          case "=": {
            if (isConst) this.error("VARIABLE");
            let right;
            if (this.next().token == "&") {
              if (this.next().token === this.tok.T_NEW) {
                right = this.read_new_expr();
              } else {
                right = this.read_variable(false, false, true);
              }
            } else {
              right = this.read_expr();
            }
            return result("assign", expr, right, "=");
          }
  
          // operations :
          case this.tok.T_PLUS_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "+=");
  
          case this.tok.T_MINUS_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "-=");
  
          case this.tok.T_MUL_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "*=");
  
          case this.tok.T_POW_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "**=");
  
          case this.tok.T_DIV_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "/=");
  
          case this.tok.T_CONCAT_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), ".=");
  
          case this.tok.T_MOD_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "%=");
  
          case this.tok.T_AND_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "&=");
  
          case this.tok.T_OR_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "|=");
  
          case this.tok.T_XOR_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "^=");
  
          case this.tok.T_SL_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "<<=");
  
          case this.tok.T_SR_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), ">>=");
  
          case this.tok.T_INC:
            if (isConst) this.error("VARIABLE");
            this.next();
            return result("post", "+", expr);
          case this.tok.T_DEC:
            if (isConst) this.error("VARIABLE");
            this.next();
            return result("post", "-", expr);
          default:
            // see #193
            result.destroy(expr);
        }
      } else if (this.is("SCALAR")) {
        result = this.node();
        expr = this.read_scalar();
        if (expr.kind === "array" && expr.shortForm && this.token === "=") {
          // list assign
          const list = this.node("list")(expr.items, true);
          if (expr.loc) list.loc = expr.loc;
          const right = this.next().read_expr();
          return result("assign", list, right, "=");
        } else {
          // see #189 - swap docs on nodes
          result.destroy(expr);
        }
        // classic array
        return this.handleDereferencable(expr);
      } else {
        this.error("EXPR");
        this.next();
      }
  
      // returns variable | scalar
      return expr;
    }
func_code_string: function() {
      let result, expr;
      if (this.token === "@") return this.node("silent")(this.next().read_expr());
      if (this.token === "+")
        return this.node("unary")("+", this.next().read_expr());
      if (this.token === "-")
        return this.node("unary")("-", this.next().read_expr());
      if (this.token === "!")
        return this.node("unary")("!", this.next().read_expr());
      if (this.token === "~")
        return this.node("unary")("~", this.next().read_expr());
  
      if (this.token === "(") {
        expr = this.next().read_expr();
        expr.parenthesizedExpression = true;
        this.expect(")") && this.next();
        return this.handleDereferencable(expr);
      }
  
      if (this.token === "`") {
        // https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L1048
        return this.read_encapsed_string("`");
      }
  
      if (this.token === this.tok.T_LIST) {
        let assign = null;
        const isInner = this.innerList;
        result = this.node("list");
        if (!isInner) {
          assign = this.node("assign");
        }
        if (this.next().expect("(")) {
          this.next();
        }
  
        if (!this.innerList) this.innerList = true;
  
        // reads inner items
        const assignList = this.read_array_pair_list(false);
        if (this.expect(")")) {
          this.next();
        }
  
        // check if contains at least one assignment statement
        let hasItem = false;
        for (let i = 0; i < assignList.length; i++) {
          if (assignList[i] !== null) {
            hasItem = true;
            break;
          }
        }
        if (!hasItem) {
          this.raiseError(
            "Fatal Error :  Cannot use empty list on line " +
              this.lexer.yylloc.first_line
          );
        }
  
        // handles the node resolution
        if (!isInner) {
          this.innerList = false;
          if (this.expect("=")) {
            return assign(
              result(assignList, false),
              this.next().read_expr(),
              "="
            );
          } else {
            // error fallback : list($a, $b);
            return result(assignList, false);
          }
        } else {
          return result(assignList, false);
        }
      }
  
      if (this.token === this.tok.T_CLONE)
        return this.node("clone")(this.next().read_expr());
  
      switch (this.token) {
        case this.tok.T_INC:
          return this.node("pre")(
            "+",
            this.next().read_variable(false, false, false)
          );
  
        case this.tok.T_DEC:
          return this.node("pre")(
            "-",
            this.next().read_variable(false, false, false)
          );
  
        case this.tok.T_NEW:
          return this.read_new_expr();
  
        case this.tok.T_ISSET: {
          result = this.node("isset");
          if (this.next().expect("(")) {
            this.next();
          }
          const variables = this.read_list(this.read_expr, ",");
          if (this.expect(")")) {
            this.next();
          }
          return result(variables);
        }
        case this.tok.T_EMPTY: {
          result = this.node("empty");
          if (this.next().expect("(")) {
            this.next();
          }
          const expression = this.read_expr();
          if (this.expect(")")) {
            this.next();
          }
          return result(expression);
        }
        case this.tok.T_INCLUDE:
          return this.node("include")(false, false, this.next().read_expr());
  
        case this.tok.T_INCLUDE_ONCE:
          return this.node("include")(true, false, this.next().read_expr());
  
        case this.tok.T_REQUIRE:
          return this.node("include")(false, true, this.next().read_expr());
  
        case this.tok.T_REQUIRE_ONCE:
          return this.node("include")(true, true, this.next().read_expr());
  
        case this.tok.T_EVAL:
          result = this.node("eval");
          if (this.next().expect("(")) {
            this.next();
          }
          expr = this.read_expr();
          if (this.expect(")")) {
            this.next();
          }
          return result(expr);
  
        case this.tok.T_INT_CAST:
          return this.read_expr_cast("int");
  
        case this.tok.T_DOUBLE_CAST:
          return this.read_expr_cast("float");
  
        case this.tok.T_STRING_CAST:
          return this.read_expr_cast(
            this.text().indexOf("binary") !== -1 ? "binary" : "string"
          );
  
        case this.tok.T_ARRAY_CAST:
          return this.read_expr_cast("array");
  
        case this.tok.T_OBJECT_CAST:
          return this.read_expr_cast("object");
  
        case this.tok.T_BOOL_CAST:
          return this.read_expr_cast("bool");
  
        case this.tok.T_UNSET_CAST:
          return this.read_expr_cast("unset");
  
        case this.tok.T_EXIT: {
          const useDie = this.lexer.yytext.toLowerCase() === "die";
          result = this.node("exit");
          let status = null;
          if (this.next().token === "(") {
            if (this.next().token !== ")") {
              status = this.read_expr();
              if (this.expect(")")) {
                this.next();
              }
            } else {
              this.next();
            }
          }
          return result(status, useDie);
        }
  
        case this.tok.T_PRINT:
          return this.node("print")(this.next().read_expr());
  
        // T_YIELD (expr (T_DOUBLE_ARROW expr)?)?
        case this.tok.T_YIELD: {
          let value = null;
          let key = null;
          result = this.node("yield");
          if (this.next().is("EXPR")) {
            // reads the yield return value
            value = this.read_expr();
            if (this.token === this.tok.T_DOUBLE_ARROW) {
              // reads the yield returned key
              key = value;
              value = this.next().read_expr();
            }
          }
          return result(value, key);
        }
  
        // T_YIELD_FROM expr
        case this.tok.T_YIELD_FROM:
          result = this.node("yieldfrom");
          expr = this.next().read_expr();
          return result(expr);
  
        case this.tok.T_FUNCTION:
          return this.read_function(true);
  
        case this.tok.T_STATIC: {
          const backup = [this.token, this.lexer.getState()];
          if (this.next().token === this.tok.T_FUNCTION) {
            // handles static function
            return this.read_function(true, [0, 1, 0]);
          } else {
            // rollback
            this.lexer.tokens.push(backup);
            this.next();
          }
        }
      }
  
      // SCALAR | VARIABLE
      if (this.is("VARIABLE")) {
        result = this.node();
        expr = this.read_variable(false, false, false);
  
        // https://github.com/php/php-src/blob/master/Zend/zend_language_parser.y#L877
        // should accept only a variable
        const isConst =
          expr.kind === "identifier" ||
          (expr.kind === "staticlookup" && expr.offset.kind === "identifier");
  
        // VARIABLES SPECIFIC OPERATIONS
        switch (this.token) {
          case "=": {
            if (isConst) this.error("VARIABLE");
            let right;
            if (this.next().token == "&") {
              if (this.next().token === this.tok.T_NEW) {
                right = this.read_new_expr();
              } else {
                right = this.read_variable(false, false, true);
              }
            } else {
              right = this.read_expr();
            }
            return result("assign", expr, right, "=");
          }
  
          // operations :
          case this.tok.T_PLUS_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "+=");
  
          case this.tok.T_MINUS_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "-=");
  
          case this.tok.T_MUL_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "*=");
  
          case this.tok.T_POW_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "**=");
  
          case this.tok.T_DIV_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "/=");
  
          case this.tok.T_CONCAT_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), ".=");
  
          case this.tok.T_MOD_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "%=");
  
          case this.tok.T_AND_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "&=");
  
          case this.tok.T_OR_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "|=");
  
          case this.tok.T_XOR_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "^=");
  
          case this.tok.T_SL_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), "<<=");
  
          case this.tok.T_SR_EQUAL:
            if (isConst) this.error("VARIABLE");
            return result("assign", expr, this.next().read_expr(), ">>=");
  
          case this.tok.T_INC:
            if (isConst) this.error("VARIABLE");
            this.next();
            return result("post", "+", expr);
          case this.tok.T_DEC:
            if (isConst) this.error("VARIABLE");
            this.next();
            return result("post", "-", expr);
          default:
            // see #193
            result.destroy(expr);
        }
      } else if (this.is("SCALAR")) {
        result = this.node();
        expr = this.read_scalar();
        if (expr.kind === "array" && expr.shortForm && this.token === "=") {
          // list assign
          const list = this.node("list")(expr.items, true);
          if (expr.loc) list.loc = expr.loc;
          const right = this.next().read_expr();
          return result("assign", list, right, "=");
        } else {
          // see #189 - swap docs on nodes
          result.destroy(expr);
        }
        // classic array
        return this.handleDereferencable(expr);
      } else {
        this.error("EXPR");
        this.next();
      }
  
      // returns variable | scalar
      return expr;
    }
func_documentation_string: ```ebnf
  Reads an expression
  expr ::= @todo
  ```