Repository: apple/pkl
File: pkl-parser/src/main/java/org/pkl/parser/GenericParser.java (Java)

Code before:
    var fullOpToken = fullLookahead();
    var operator = getOperator(fullOpToken.tk);
    while (operator != null) {
      if (operator.getPrec() < minPrecedence) break;
      // `-` and `[]` must be in the same line as the left operand and have no semicolons inbetween
      if ((operator == Operator.MINUS || operator == Operator.SUBSCRIPT)
          && (fullOpToken.hasSemicolon || !expr.span.sameLine(fullOpToken.tk.span))) break;
      var children = new ArrayList<Node>();
      children.add(expr);
      ff(children);
      var op = next();
      children.add(make(NodeType.OPERATOR, op.span));
      ff(children);
      var nodeType = NodeType.BINARY_OP_EXPR;
      var nextMinPrec = operator.isLeftAssoc() ? operator.getPrec() + 1 : operator.getPrec();
      switch (op.token) {
        case IS, AS -> children.add(parseType());
        case LBRACK -> {
          nodeType = NodeType.SUBSCRIPT_EXPR;
          children.add(parseExpr("]"));
          ff(children);
          expect(Token.RBRACK, children, "unexpectedToken", "]");
        }
        case NON_NULL -> nodeType = NodeType.NON_NULL_EXPR;
        default -> children.add(parseExpr(expectation, nextMinPrec));
      }

      expr = new Node(nodeType, children);
      fullOpToken = fullLookahead();
      operator = getOperator(fullOpToken.tk);
    }
    return expr;
  }

  private @Nullable Operator getOperator(FullToken tk) {
    return switch (tk.token) {
      case POW -> Operator.POW;
      case STAR -> Operator.MULT;
      case DIV -> Operator.DIV;
      case INT_DIV -> Operator.INT_DIV;
      case MOD -> Operator.MOD;
      case PLUS -> Operator.PLUS;
      case MINUS -> Operator.MINUS;
      case GT -> Operator.GT;
      case GTE -> Operator.GTE;
      case LT -> Operator.LT;
      case LTE -> Operator.LTE;
      case IS -> Operator.IS;
      case AS -> Operator.AS;
      case EQUAL -> Operator.EQ_EQ;
      case NOT_EQUAL -> Operator.NOT_EQ;

Review comment (bug):
Bugfix and parser change: parse qualified access as its own node type, and don't permit any arbitrary expression to follow the dot.

Code after:
    var fullOpToken = fullLookahead();
    var operator = getOperator(fullOpToken.tk);
    while (operator != null) {
      if (operator.getPrec() < minPrecedence) break;
      // `-` and `[]` must be in the same line as the left operand and have no semicolons inbetween
      if ((operator == Operator.MINUS || operator == Operator.SUBSCRIPT)
          && (fullOpToken.hasSemicolon || !expr.span.sameLine(fullOpToken.tk.span))) break;
      var children = new ArrayList<Node>();
      children.add(expr);
      ff(children);
      var op = next();
      children.add(make(NodeType.OPERATOR, op.span));
      ff(children);
      var nodeType = NodeType.BINARY_OP_EXPR;
      var nextMinPrec = operator.isLeftAssoc() ? operator.getPrec() + 1 : operator.getPrec();
      switch (op.token) {
        case IS, AS -> children.add(parseType());
        case LBRACK -> {
          nodeType = NodeType.SUBSCRIPT_EXPR;
          children.add(parseExpr("]"));
          ff(children);
          expect(Token.RBRACK, children, "unexpectedToken", "]");
        }
        case DOT, QDOT -> {
          nodeType = NodeType.QUALIFIED_ACCESS_EXPR;
          children.add(parseUnqualifiedAccessExpr());
        }
        case NON_NULL -> nodeType = NodeType.NON_NULL_EXPR;
        default -> children.add(parseExpr(expectation, nextMinPrec));
      }

      expr = new Node(nodeType, children);
      fullOpToken = fullLookahead();
      operator = getOperator(fullOpToken.tk);
    }
    return expr;
  }

  private @Nullable Operator getOperator(FullToken tk) {
    return switch (tk.token) {
      case POW -> Operator.POW;
      case STAR -> Operator.MULT;
      case DIV -> Operator.DIV;
      case INT_DIV -> Operator.INT_DIV;
      case MOD -> Operator.MOD;
      case PLUS -> Operator.PLUS;
      case MINUS -> Operator.MINUS;
      case GT -> Operator.GT;
      case GTE -> Operator.GTE;
      case LT -> Operator.LT;
      case LTE -> Operator.LTE;