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

Code before:
    expect(Token.RPAREN, "unexpectedToken", ")");
    var body = parseObjectBody();
    return new ObjectMember.ForGenerator(par1, par2, expr, body, start.endWith(body.span()));
  }

  private Expr parseExpr() {
    return parseExpr(null);
  }

  private record OpInfo(int prec, boolean isLeftAssoc) {}

  private OpInfo getOpInfo(Operator operator) {
    return switch (operator) {
      case NULL_COALESCE -> new OpInfo(1, false);
      case PIPE -> new OpInfo(2, true);
      case OR -> new OpInfo(3, true);
      case AND -> new OpInfo(4, true);
      case EQ_EQ, NOT_EQ -> new OpInfo(5, true);
      case IS, AS -> new OpInfo(6, true);
      case LT, LTE, GT, GTE -> new OpInfo(7, true);
      case PLUS, MINUS -> new OpInfo(8, true);
      case MULT, DIV, INT_DIV, MOD -> new OpInfo(9, true);
      case POW -> new OpInfo(10, false);
      case DOT, QDOT -> new OpInfo(11, true);
    };
  }

  @SuppressWarnings("DuplicatedCode")
  private Expr parseExpr(@Nullable String expectation) {
    return parseExpr(expectation, 1);
  }

  private Expr parseExpr(@Nullable String expectation, int minPrecedence) {
    var expr = parseExprAtom(expectation);
    var op = getOperator();
    while (op != null) {
      var opInfo = getOpInfo(op);
      if (opInfo.prec < minPrecedence) break;
      // `-` must be in the same line as the left operand and have no semicolons inbetween
      if (op == Operator.MINUS && (precededBySemicolon || _lookahead.newLinesBetween > 0)) break;

      next(); // operator
      switch (op) {
        case IS -> {
          var type = parseType();
          expr = new Expr.TypeCheckExpr(expr, type, expr.span().endWith(type.span()));
        }
        case AS -> {
          var type = parseType();
          expr = new Expr.TypeCastExpr(expr, type, expr.span().endWith(type.span()));
        }

Review comment (suggestion):
We can avoid allocation here by creating these as static final's on record `OpInfo`, e.g.

```java
  private record OpInfo(int prec, boolean isLeftAssoc) {
    static final OpInfo NULL_COALESCE = new OpInfo(1, false);
    static final OpInfo PIPE = new OpInfo(2, true);
    // etc
```

Or, maybe even better, you can add these as fields to the `Operator` enum directly:

```java
public enum Operator {
  POW(10, false),
  // etc

  private final int prec;
  private final boolean isLeftAssoc;

  Operator(int prec, boolean isLeftAssoc) {
    this.prec = prec;
    this.isLeftAssoc = isLeftAssoc;
  }

  public int prec() {
    return prec;
  }

  public boolean isLeftAssoc() {
    return prec;
  }
}
```

Code after:
    expect(Token.RPAREN, "unexpectedToken", ")");
    var body = parseObjectBody();
    return new ObjectMember.ForGenerator(par1, par2, expr, body, start.endWith(body.span()));
  }

  private Expr parseExpr() {
    return parseExpr(null);
  }

  @SuppressWarnings("DuplicatedCode")
  private Expr parseExpr(@Nullable String expectation) {
    return parseExpr(expectation, 1);
  }

  private Expr parseExpr(@Nullable String expectation, int minPrecedence) {
    var expr = parseExprAtom(expectation);
    var op = getOperator();
    while (op != null) {
      if (op.getPrec() < minPrecedence) break;
      // `-` must be in the same line as the left operand and have no semicolons inbetween
      if (op == Operator.MINUS && (precededBySemicolon || _lookahead.newLinesBetween > 0)) break;

      next(); // operator
      switch (op) {
        case IS -> {
          var type = parseType();
          expr = new Expr.TypeCheckExpr(expr, type, expr.span().endWith(type.span()));
        }
        case AS -> {
          var type = parseType();
          expr = new Expr.TypeCastExpr(expr, type, expr.span().endWith(type.span()));
        }
        case DOT, QDOT -> {
          var rhs = parseIdentifier();
          var isNullable = op == Operator.QDOT;
          ArgumentList argumentList = null;
          if (lookahead == Token.LPAREN
              && !precededBySemicolon
              && _lookahead.newLinesBetween == 0) {
            argumentList = parseArgumentList();
          }
          var lastSpan = argumentList != null ? argumentList.span() : rhs.span();
          expr =
              new QualifiedAccessExpr(
                  expr, rhs, isNullable, argumentList, expr.span().endWith(lastSpan));
        }
        default -> {
          var nextMinPrec = op.isLeftAssoc() ? op.getPrec() + 1 : op.getPrec();
          var rhs = parseExpr(expectation, nextMinPrec);
          expr = new BinaryOperatorExpr(expr, rhs, op, expr.span().endWith(rhs.span()));
        }