///|
fn Parser::parse_expr(self : Parser) -> Expr {
  self.parse_null_coalesce_expr()
}

///|
// PKL-pkspec-C2: Apple Pkl lets an unambiguously-infix operator lead a
// continuation line:
//   pending =
//     s.pending
//       || (...)
// After `skip_whitespace` leaves the cursor on a newline, peek past
// trivia + semicolons; if `kind` follows, advance the cursor onto it and
// return true so the binary-loop / null-coalesce detectors treat it as a
// chain continuation. The operators that use this (`||`, `&&`, `==`,
// `!=`, `|>`, `??`) can never start a fresh statement, so consuming the
// intervening newline is unambiguous.
fn Parser::at_infix_across_newline(
  self : Parser,
  kind : @cst.SyntaxKind,
) -> Bool {
  if self.at(kind) {
    return true
  }
  let after_sep = self.skip_trivia_and_semicolons_from(self.pos)
  if after_sep < self.len && self.tokens[after_sep].kind() == kind {
    self.skip_trivia_and_semicolons()
    true
  } else {
    false
  }
}

///|
fn Parser::parse_binary_loop(
  self : Parser,
  parse_operand : () -> Expr,
  operators : () -> BinaryOp?,
) -> Expr {
  let checkpoint = self.builder.checkpoint()
  let mut left = parse_operand()
  let mut maybe_op = operators()
  while maybe_op is Some(op) {
    self.builder.start_node_at(checkpoint, binary_expr())
    ignore(self.bump())
    // PKL-pkspec-C2: the right operand may sit on a fresh line after a
    // leading-operator newline (`p\n  || (\n  ...)`). Swallow the
    // intervening separators so the operand parser starts on it.
    self.skip_trivia_and_semicolons()
    let right = parse_operand()
    self.builder.finish_node()
    left = BinaryExpr(op, left, right)
    maybe_op = operators()
  }
  left
}

///|
fn Parser::parse_null_coalesce_expr(self : Parser) -> Expr {
  let checkpoint = self.builder.checkpoint()
  let left = self.parse_pipe_expr()
  self.skip_whitespace()
  if self.at_infix_across_newline(coalesce()) {
    self.builder.start_node_at(checkpoint, binary_expr())
    ignore(self.bump())
    self.skip_trivia_and_semicolons()
    let right = self.parse_null_coalesce_expr()
    self.builder.finish_node()
    BinaryExpr(NullCoalesce, left, right)
  } else {
    left
  }
}

///|
fn Parser::parse_pipe_expr(self : Parser) -> Expr {
  self.parse_binary_loop(fn() { self.parse_or_expr() }, fn() {
    self.skip_whitespace()
    if self.at_infix_across_newline(pipe_forward()) {
      Some(Pipe)
    } else {
      None
    }
  })
}

///|
fn Parser::parse_or_expr(self : Parser) -> Expr {
  self.parse_binary_loop(fn() { self.parse_and_expr() }, fn() {
    self.skip_whitespace()
    if self.at_infix_across_newline(or_or()) {
      Some(Or)
    } else {
      None
    }
  })
}

///|
fn Parser::parse_and_expr(self : Parser) -> Expr {
  self.parse_binary_loop(fn() { self.parse_equality_expr() }, fn() {
    self.skip_whitespace()
    if self.at_infix_across_newline(and_and()) {
      Some(And)
    } else {
      None
    }
  })
}

///|
fn Parser::parse_equality_expr(self : Parser) -> Expr {
  self.parse_binary_loop(fn() { self.parse_comparison_expr() }, fn() {
    self.skip_whitespace()
    if self.at_infix_across_newline(equal_equal()) {
      Some(Equal)
    } else if self.at_infix_across_newline(not_equal()) {
      Some(NotEqual)
    } else {
      None
    }
  })
}

///|
fn Parser::parse_comparison_expr(self : Parser) -> Expr {
  let checkpoint = self.builder.checkpoint()
  let mut left = self.parse_add_expr()
  let mut keep_going = true
  while keep_going {
    self.skip_whitespace()
    let op : BinaryOp? = if self.at(lt()) {
      Some(LessThan)
    } else if self.at(lte()) {
      Some(LessOrEqual)
    } else if self.at(gt()) {
      Some(GreaterThan)
    } else if self.at(gte()) {
      Some(GreaterOrEqual)
    } else if self.at(is_kw()) {
      Some(Is)
    } else {
      None
    }
    match op {
      Some(actual_op) => {
        self.builder.start_node_at(checkpoint, binary_expr())
        ignore(self.bump())
        let right = match actual_op {
          Is => self.parse_type_operand_expr()
          _ => self.parse_add_expr()
        }
        self.builder.finish_node()
        left = BinaryExpr(actual_op, left, right)
      }
      None => keep_going = false
    }
  }
  left
}

///|
fn Parser::parse_type_operand_expr(self : Parser) -> Expr {
  self.skip_trivia()
  // PKL-148at: `as` / `is` right-operand also accepts string-literal
  // singletons (`"Pigeon"` as a type) and parenthesized / function-type
  // shapes (`(Int) -> Int`). For string literals and `(` openings, the
  // type text walker handles the consumption just like the identifier
  // path — collect every type-position token until a real expression
  // operator boundary.
  if self.at(identifier()) || self.at(lparen()) || self.at(string_token()) {
    self.builder.start_node(name_ref())
    let type_name = self.parse_type_text(
      stop_at_arrow=false,
      stop_at_expression_operator=true,
    )
    self.builder.finish_node()
    Identifier(type_name)
  } else {
    self.parse_add_expr()
  }
}

///|
fn Parser::parse_add_expr(self : Parser) -> Expr {
  let checkpoint = self.builder.checkpoint()
  let mut left = self.parse_mul_expr()
  let mut keep_going = true
  while keep_going {
    // PKL-148au: `+` is unambiguously binary, so it can continue
    // across newlines / semicolons (`1\n + 2`, `1;; +; 2`). `-` is
    // ambiguous (`2\n - 1` is two elements, not one binary), so it
    // only continues across plain whitespace + comments.
    self.skip_whitespace()
    if self.at(plus()) {
      self.builder.start_node_at(checkpoint, binary_expr())
      ignore(self.bump())
      let right = self.parse_mul_expr()
      self.builder.finish_node()
      left = BinaryExpr(BinaryOp::Add, left, right)
      continue
    }
    if self.at(minus()) {
      self.builder.start_node_at(checkpoint, binary_expr())
      ignore(self.bump())
      let right = self.parse_mul_expr()
      self.builder.finish_node()
      left = BinaryExpr(BinaryOp::Subtract, left, right)
      continue
    }
    // PKL-148au: no operator on the current line — peek past
    // newlines / semicolons for a continuation `+`. `-` stays
    // out of this branch because of the unary-minus ambiguity.
    let after_sep = self.skip_trivia_and_semicolons_from(self.pos)
    if after_sep < self.len && self.tokens[after_sep].kind() == plus() {
      self.skip_trivia_and_semicolons()
      self.builder.start_node_at(checkpoint, binary_expr())
      ignore(self.bump())
      // Also swallow any trailing separators after the operator
      // (`1;; +; 2;` puts a `;` between `+` and `2`).
      self.skip_trivia_and_semicolons()
      let right = self.parse_mul_expr()
      self.builder.finish_node()
      left = BinaryExpr(BinaryOp::Add, left, right)
      continue
    }
    keep_going = false
  }
  left
}

///|
fn Parser::parse_mul_expr(self : Parser) -> Expr {
  let checkpoint = self.builder.checkpoint()
  let mut left = self.parse_power_expr()
  while ({
          // PKL-148au: `*` / `/` / `%` / `~/` are unambiguously
          // binary, so they continue across newlines / semicolons
          // (`1\n / 2`, `3 *\n 4`). Peek past separators when the
          // current line has no operator.
          self.skip_whitespace()
          if self.at(star()) ||
            self.at(slash()) ||
            self.at(percent()) ||
            self.at(int_div()) {
            true
          } else {
            let after_sep = self.skip_trivia_and_semicolons_from(self.pos)
            if after_sep < self.len {
              let k = self.tokens[after_sep].kind()
              if k == star() || k == slash() || k == percent() || k == int_div() {
                self.skip_trivia_and_semicolons()
                true
              } else {
                false
              }
            } else {
              false
            }
          }
        }) {
    self.builder.start_node_at(checkpoint, binary_expr())
    let op = if self.at(star()) {
      BinaryOp::Multiply
    } else if self.at(slash()) {
      BinaryOp::Divide
    } else if self.at(percent()) {
      BinaryOp::Modulo
    } else {
      BinaryOp::IntDivide
    }
    ignore(self.bump())
    // PKL-148au: swallow separators after the operator so the
    // right operand can sit on a fresh line / past a `;` (`3 *\n
    // 4`, `1;; / 2;`).
    self.skip_trivia_and_semicolons()
    let right = self.parse_power_expr()
    self.builder.finish_node()
    left = BinaryExpr(op, left, right)
  }
  left
}

///|
fn Parser::parse_power_expr(self : Parser) -> Expr {
  let checkpoint = self.builder.checkpoint()
  let left = self.parse_cast_expr()
  self.skip_whitespace()
  if self.at(pow()) {
    self.builder.start_node_at(checkpoint, binary_expr())
    ignore(self.bump())
    let right = self.parse_power_expr()
    self.builder.finish_node()
    BinaryExpr(Power, left, right)
  } else {
    left
  }
}

///|
fn Parser::parse_cast_expr(self : Parser) -> Expr {
  let checkpoint = self.builder.checkpoint()
  let mut left = self.parse_unary_expr()
  while ({
          self.skip_whitespace()
          self.at_infix_across_newline(as_kw())
        }) {
    self.builder.start_node_at(checkpoint, binary_expr())
    ignore(self.bump())
    let right = self.parse_type_operand_expr()
    self.builder.finish_node()
    left = BinaryExpr(As, left, right)
  }
  left
}

///|
fn Parser::parse_unary_expr(self : Parser) -> Expr {
  self.skip_trivia()
  if self.at(minus()) || self.at(bang()) {
    self.builder.start_node(unary_expr())
    let op = if self.at(minus()) { Negate } else { Not }
    ignore(self.bump())
    let expr = self.parse_unary_expr()
    self.builder.finish_node()
    UnaryExpr(op, expr)
  } else {
    self.parse_postfix_expr()
  }
}

///|
fn Parser::parse_postfix_expr(self : Parser) -> Expr {
  let checkpoint = self.builder.checkpoint()
  let mut expr = self.parse_primary_expr()
  let mut keep_going = true
  while keep_going {
    // Apple Pkl accepts dot-chains broken across lines:
    //   xs
    //     .toList()
    //     .map((x) -> x + 1)
    // Peek past trivia (whitespace + newline + comment) — if the next
    // significant token is `.` / `?.`, advance the cursor past the trivia
    // and continue the chain. Otherwise stay put so the newline keeps its
    // role as a statement separator.
    let trivia_end = self.skip_trivia_from(self.pos)
    let next_kind = if trivia_end >= self.len {
      eof()
    } else {
      self.tokens[trivia_end].kind()
    }
    // PKL-148aq: `next_kind == dot()` is normally a member-access
    // continuation, but a run of three consecutive `.`s is the
    // spread operator (`...listing`). When the dot-chain heuristic
    // would consume `.` from a spread that follows a bare element
    // (`{ 0\n  ...listing }` or `{ 3...IntSeq(...) }`), the inner
    // member parser then bumps `.` looking for an identifier, finds
    // another `.`, and surfaces `unsupported expression`. Bail out
    // of the entire postfix loop when three dots are stacked so the
    // caller's `at_triple_dot` arm picks up the spread.
    let is_triple_dot = trivia_end + 2 < self.len &&
      self.tokens[trivia_end].kind() == dot() &&
      self.tokens[trivia_end + 1].kind() == dot() &&
      self.tokens[trivia_end + 2].kind() == dot()
    if is_triple_dot {
      break
    }
    if self.at(star()) &&
      expr is Identifier("read") &&
      self.peek_non_whitespace_kind(self.pos + 1) == lparen() {
      // Apple Pkl's `read*("glob")` is a glob intrinsic, not binary
      // multiplication. Keep it on the existing CallExpr path by using
      // a synthetic callee name that the evaluator recognises.
      self.builder.start_node_at(checkpoint, call_expr())
      ignore(self.bump())
      self.skip_whitespace()
      ignore(self.expect(lparen(), "("))
      let arguments : Array[Expr] = []
      self.skip_whitespace()
      while !self.at(eof()) && !self.at(rparen()) {
        arguments.push(self.parse_expr())
        self.skip_whitespace()
        if self.at(comma()) {
          ignore(self.bump())
          self.skip_whitespace()
        } else {
          break
        }
      }
      self.skip_whitespace()
      if self.at(rparen()) {
        ignore(self.bump())
      } else {
        self.builder.token(error_kind(), "")
      }
      self.builder.finish_node()
      expr = CallExpr(Identifier("read*"), arguments)
      continue
    }
    let mut separated_call = false
    if next_kind == dot() || next_kind == qdot() {
      // PKL-107: pull byte_offset along with self.pos so subsequent
      // position-tagged diagnostics still point at the right source
      // location. The trivia tokens are NOT pushed to the cst here
      // (existing behaviour for dot-chain continuation).
      for i = self.pos; i < trivia_end; i = i + 1 {
        self.byte_offset += self.tokens[i].text().length()
      }
      self.pos = trivia_end
    } else {
      let before_whitespace = self.pos
      self.skip_whitespace()
      separated_call = self.pos > before_whitespace
    }
    if self.at(dot()) || self.at(qdot()) {
      let unsupported_start = self.current_offset()
      let safe = self.at(qdot())
      self.builder.start_node_at(checkpoint, member_access())
      ignore(self.bump())
      self.skip_whitespace()
      let field_name = if self.at(identifier()) {
        self.bump().text()
      } else {
        if !self.at(eof()) {
          ignore(self.bump())
        }
        ""
      }
      self.builder.finish_node()
      expr = if field_name == "" {
        self.record_unsupported_syntax(
          unsupported_start,
          self.current_offset(),
          kind_name(unsupported_expr()),
        )
        UnsupportedExpr
      } else if safe {
        SafeMemberAccess(expr, field_name)
      } else {
        MemberAccess(expr, field_name)
      }
    } else if self.at(lparen()) {
      if separated_call && !expr_allows_whitespace_call(expr) {
        keep_going = false
        continue
      }
      self.builder.start_node_at(checkpoint, call_expr())
      ignore(self.bump())
      let arguments : Array[Expr] = []
      self.skip_whitespace()
      while !self.at(eof()) && !self.at(rparen()) {
        arguments.push(self.parse_expr())
        self.skip_whitespace()
        if !self.at(comma()) && !self.at(rparen()) {
          self.skip_call_argument_tail()
          self.skip_whitespace()
        }
        if self.at(comma()) {
          ignore(self.bump())
          self.skip_trivia()
          // PKL-148bh: trailing comma before `)` is accepted by Apple
          // Pkl (`parser/methodTrailingCommas` exercises every form
          // including multi-line bodies). Without this exit the next
          // iteration would call `parse_expr` on `)`, push an
          // UnsupportedExpr / placeholder argument, and the arity
          // check would reject the call.
          if self.at(rparen()) {
            break
          }
        } else {
          break
        }
      }
      self.skip_whitespace()
      if self.at(rparen()) {
        ignore(self.bump())
      } else {
        self.builder.token(error_kind(), "")
      }
      self.builder.finish_node()
      expr = CallExpr(expr, arguments)
    } else if self.at(lbracket()) {
      self.builder.start_node_at(checkpoint, subscript_access())
      ignore(self.bump())
      let key = self.parse_expr()
      ignore(self.expect(rbracket(), "]"))
      self.builder.finish_node()
      expr = SubscriptAccess(expr, key)
    } else if self.at(lbrace()) {
      self.builder.start_node_at(checkpoint, amend_expr())
      let members = self.parse_object_body_members()
      self.builder.finish_node()
      expr = AmendExpr(expr, members)
    } else if self.at(non_null()) {
      self.builder.start_node_at(checkpoint, unary_expr())
      ignore(self.bump())
      self.builder.finish_node()
      expr = NonNullExpr(expr)
    } else if self.at(question()) {
      // PKL-103: `f?(...)` is Apple Pkl's null-safe call form. Today
      // only `read?(uri)` is wired through, but the parser recognises
      // the generic shape so future null-safe intrinsics share the
      // dispatch. `?` not followed by `(` keeps the existing
      // unsupported-syntax behaviour.
      let unsupported_start = self.current_offset()
      let after_question = self.peek_non_whitespace_kind(self.pos + 1)
      if after_question == lparen() {
        ignore(self.bump())
        self.skip_whitespace()
        self.builder.start_node_at(checkpoint, call_expr())
        ignore(self.expect(lparen(), "("))
        let arguments : Array[Expr] = []
        self.skip_whitespace()
        while !self.at(eof()) && !self.at(rparen()) {
          arguments.push(self.parse_expr())
          self.skip_whitespace()
          if self.at(comma()) {
            ignore(self.bump())
            self.skip_whitespace()
          } else {
            break
          }
        }
        self.skip_whitespace()
        if self.at(rparen()) {
          ignore(self.bump())
        } else {
          self.builder.token(error_kind(), "")
        }
        self.builder.finish_node()
        expr = NullSafeCallExpr(expr, arguments)
      } else {
        ignore(self.bump())
        self.record_unsupported_syntax(
          unsupported_start,
          self.current_offset(),
          kind_name(unsupported_expr()),
        )
        expr = UnsupportedExpr
      }
    } else {
      keep_going = false
    }
  }
  expr
}

///|
fn expr_allows_whitespace_call(expr : Expr) -> Bool {
  match expr {
    Identifier(_)
    | MemberAccess(_, _)
    | SafeMemberAccess(_, _)
    | CallExpr(_, _)
    | NullSafeCallExpr(_, _)
    | SubscriptAccess(_, _)
    | NonNullExpr(_) => true
    _ => false
  }
}

///|
fn Parser::parse_primary_expr(self : Parser) -> Expr {
  self.skip_trivia()
  match self.peek_kind() {
    kind if kind == int_token() => {
      self.builder.start_node(literal_expr())
      let tok = self.bump()
      self.builder.finish_node()
      IntLiteral(parse_int_text(tok.text()))
    }
    kind if kind == float_token() => {
      // PKL-092: Float literal. The decimal form is `.`;
      // exponent forms (`1e10`, `2.5e-3`) are deferred — adding them only
      // touches the lexer / parser, the evaluator already handles
      // `Double` arithmetic uniformly.
      self.builder.start_node(literal_expr())
      let tok = self.bump()
      self.builder.finish_node()
      FloatLiteral(parse_float_text(tok.text()))
    }
    kind if kind == true_kw() => {
      self.builder.start_node(literal_expr())
      ignore(self.bump())
      self.builder.finish_node()
      BoolLiteral(true)
    }
    kind if kind == false_kw() => {
      self.builder.start_node(literal_expr())
      ignore(self.bump())
      self.builder.finish_node()
      BoolLiteral(false)
    }
    kind if kind == null_kw() => {
      self.builder.start_node(literal_expr())
      ignore(self.bump())
      self.builder.finish_node()
      NullLiteral
    }
    kind if kind == string_token() => {
      self.builder.start_node(literal_expr())
      let tok = self.bump()
      self.builder.finish_node()
      parse_string_literal_text(tok.text())
    }
    kind if kind == identifier() => {
      self.builder.start_node(name_ref())
      let tok = self.bump()
      self.builder.finish_node()
      Identifier(tok.text())
    }
    // Apple Pkl: `module.foo` is a self-reference to the current module's
    // `foo` binding. Parse as `Identifier("module")` so the postfix `.foo`
    // chain attaches; the evaluator special-cases lookups on the
    // `module` identifier to resolve from the module-level binding set.
    kind if kind == module_kw() => {
      self.builder.start_node(name_ref())
      ignore(self.bump())
      self.builder.finish_node()
      Identifier("module")
    }
    kind if kind == import_kw() => self.parse_import_expr()
    kind if kind == new_kw() => self.parse_object_expr()
    kind if kind == if_kw() => self.parse_if_expr()
    kind if kind == let_kw() => self.parse_let_expr()
    kind if kind == lparen() => {
      if self.function_literal_at_current() {
        return self.parse_lambda_expr()
      }
      if self.legacy_dynamic_constructor_at_current() {
        return self.parse_legacy_dynamic_constructor()
      }
      if self.paren_expr_looks_unsupported() {
        return self.parse_unsupported_expr()
      }
      self.builder.start_node(paren_expr())
      ignore(self.bump())
      let expr = self.parse_expr()
      // PKL-pkspec-C: Apple Pkl treats newlines (and surrounding
      // trivia) as whitespace inside a parenthesized group, so a `)` on
      // its own line is valid. Skip any trailing trivia the inner
      // expression left in place before matching the close paren —
      // without this `( \n expr \n )` fails with `expected ), got
      // newline`.
      self.skip_trivia()
      ignore(self.expect(rparen(), ")"))
      self.builder.finish_node()
      expr
    }
    _ => self.parse_unsupported_expr()
  }
}

///|
fn Parser::legacy_dynamic_constructor_at_current(self : Parser) -> Bool {
  if !self.at(lparen()) {
    return false
  }
  let name_idx = self.skip_trivia_from(self.pos + 1)
  if name_idx >= self.len ||
    self.tokens[name_idx].kind() != identifier() ||
    self.tokens[name_idx].text() != "Dynamic" {
    return false
  }
  let close_idx = self.skip_trivia_from(name_idx + 1)
  if close_idx >= self.len || self.tokens[close_idx].kind() != rparen() {
    return false
  }
  let body_idx = self.skip_trivia_from(close_idx + 1)
  body_idx < self.len && self.tokens[body_idx].kind() == lbrace()
}

///|
fn Parser::parse_legacy_dynamic_constructor(self : Parser) -> Expr {
  self.builder.start_node(object_expr())
  ignore(self.expect(lparen(), "("))
  self.skip_whitespace()
  ignore(self.expect(identifier(), "Dynamic"))
  self.skip_whitespace()
  ignore(self.expect(rparen(), ")"))
  let members = self.parse_object_body_members()
  self.builder.finish_node()
  TypedObjectLiteral("Dynamic", members)
}

///|
fn Parser::parse_lambda_expr(self : Parser) -> Expr {
  self.builder.start_node(lambda_expr())
  ignore(self.expect(lparen(), "("))
  let parameters : Array[FunctionParameter] = []
  self.skip_whitespace()
  while !self.at(eof()) && !self.at(rparen()) {
    match self.parse_function_parameter() {
      Some(parameter) => parameters.push(parameter)
      None => ignore(self.bump())
    }
    self.skip_whitespace()
    if self.at(comma()) {
      ignore(self.bump())
      self.skip_whitespace()
    }
  }
  ignore(self.expect(rparen(), ")"))
  // PKL-148bb: lambda return type stops at the body-marker `->`.
  // `parse_type_annotation`'s default (`stop_at_arrow=false`) is the
  // right call for class-method parameter / return slots where a
  // function-typed annotation (`(String) -> Int`) should land whole,
  // but a lambda's own return slot is immediately followed by the
  // body's `->` and must stop before it.
  self.skip_whitespace()
  let return_type_name = if self.at(colon()) {
    ignore(self.bump())
    self.skip_whitespace()
    let t = self.parse_type_text(
      stop_at_arrow=true,
      stop_at_expression_operator=false,
    )
    if t == "" {
      None
    } else {
      Some(t)
    }
  } else {
    None
  }
  ignore(self.expect(arrow(), "->"))
  let body = self.parse_expr()
  self.builder.finish_node()
  LambdaExpr(parameters, body, return_type_name)
}

///|
/// Parse `let (name = value) body`.
fn Parser::parse_let_expr(self : Parser) -> Expr {
  self.builder.start_node(let_decl())
  ignore(self.expect(let_kw(), "let"))
  self.skip_whitespace()
  ignore(self.expect(lparen(), "("))
  // Inside the binding parens, newlines are soft whitespace. This is the
  // shape emitted by the official formatter for long binding values.
  self.skip_trivia()
  let name = match self.expect(identifier(), "identifier") {
    Some(tok) => tok.text()
    None => ""
  }
  let type_name = self.parse_type_annotation()
  self.skip_whitespace()
  ignore(self.expect(eq(), "="))
  let value = self.parse_expr()
  self.skip_trivia()
  ignore(self.expect(rparen(), ")"))
  let body = self.parse_expr()
  self.builder.finish_node()
  if name == "" {
    return UnsupportedExpr
  }
  LetExpr(name, type_name, value, body)
}

///|
fn Parser::parse_if_expr(self : Parser) -> Expr {
  self.builder.start_node(conditional_expr())
  ignore(self.expect(if_kw(), "if"))
  ignore(self.expect(lparen(), "("))
  let condition = self.parse_expr()
  // A formatted multiline condition leaves the cursor on the newline
  // immediately before `)`, which is unambiguously trivia here.
  self.skip_trivia()
  ignore(self.expect(rparen(), ")"))
  let then_expr = self.parse_expr()
  self.skip_trivia()
  ignore(self.expect(else_kw(), "else"))
  let else_expr = self.parse_expr()
  self.builder.finish_node()
  ConditionalExpr(condition, then_expr, else_expr)
}

///|
fn Parser::parse_unsupported_expr(self : Parser) -> Expr {
  self.builder.start_node(unsupported_expr())
  let mut parens = 0
  let mut braces = 0
  let mut brackets = 0
  let mut consumed = false
  self.skip_trivia()
  let unsupported_start = self.current_offset()
  while !self.at(eof()) {
    if consumed && parens == 0 && braces == 0 && brackets == 0 {
      if self.at(rbrace()) ||
        self.at(rparen()) ||
        self.at(rbracket()) ||
        self.at(comma()) {
        break
      }
      if is_separator(self.peek_kind()) &&
        self.at_module_member_boundary_from(self.pos + 1) {
        break
      }
    }
    if self.at(lparen()) {
      parens += 1
    } else if self.at(rparen()) {
      if parens == 0 {
        break
      }
      parens -= 1
    } else if self.at(lbrace()) {
      braces += 1
    } else if self.at(rbrace()) {
      if braces == 0 {
        break
      }
      braces -= 1
    } else if self.at(lbracket()) {
      brackets += 1
    } else if self.at(rbracket()) {
      if brackets == 0 {
        break
      }
      brackets -= 1
    }
    consumed = true
    ignore(self.bump())
  }
  if !consumed && !self.at(eof()) {
    ignore(self.bump())
  }
  self.record_unsupported_syntax(
    unsupported_start,
    self.current_offset(),
    kind_name(unsupported_expr()),
  )
  self.builder.finish_node()
  UnsupportedExpr
}

///|
fn Parser::parse_import_expr(self : Parser) -> Expr {
  self.builder.start_node(import_expr())
  ignore(self.expect(import_kw(), "import"))
  self.skip_whitespace()
  let is_glob = self.at(star())
  if self.at(star()) {
    ignore(self.bump())
  }
  if self.at(lparen()) {
    ignore(self.bump())
  }
  self.skip_whitespace()
  let uri = if self.at(string_token()) {
    unquote(self.bump().text())
  } else {
    while !self.at(eof()) && !self.at(rparen()) {
      ignore(self.bump())
    }
    ""
  }
  if self.at(rparen()) {
    ignore(self.bump())
  }
  self.builder.finish_node()
  if is_glob {
    ImportGlobExpr(uri)
  } else {
    ImportExpr(uri)
  }
}

///|
fn Parser::parse_object_expr(self : Parser) -> Expr {
  self.parse_object_expr_with_expected_type(None)
}

///|
fn Parser::parse_expr_with_expected_type(
  self : Parser,
  type_name : String?,
) -> Expr {
  self.skip_whitespace()
  if type_name == Some("Dynamic") && self.at_bare_new_body() {
    self.parse_object_expr_with_expected_type(type_name)
  } else {
    self.parse_expr()
  }
}

///|
fn Parser::at_bare_new_body(self : Parser) -> Bool {
  if !self.at(new_kw()) {
    return false
  }
  let body_idx = self.skip_trivia_from(self.pos + 1)
  body_idx < self.len && self.tokens[body_idx].kind() == lbrace()
}

///|
fn Parser::parse_object_expr_with_expected_type(
  self : Parser,
  expected_type_name : String?,
) -> Expr {
  self.builder.start_node(object_expr())
  ignore(self.expect(new_kw(), "new"))
  self.skip_whitespace()
  let type_name = self.parse_type_text(
    stop_at_arrow=false,
    stop_at_expression_operator=false,
  )
  // PKL-136 drive-by: `new Listing { ... }` previously fell into the
  // TypedObjectLiteral arm because `parse_type_text` returns the full
  // generic form (`Listing`). Normalise the prefix so the body
  // parses as a listing / mapping body either way.
  let value = if type_name == "Listing" {
    self.parse_listing_body()
  } else if type_name.has_prefix("Listing<") {
    CallExpr(Identifier("@__typed_listing"), [
      StringLiteral(type_name),
      self.parse_listing_body(),
    ])
  } else if type_name == "Mapping" {
    self.parse_mapping_body()
  } else if type_name.has_prefix("Mapping<") {
    CallExpr(Identifier("@__typed_listing"), [
      StringLiteral(type_name),
      self.parse_mapping_body(),
    ])
  } else if type_name == "" {
    match expected_type_name {
      Some("Dynamic") =>
        // PKL-148ax: `local x: Dynamic = new { 1; foo = "foo" }`
        // is a Dynamic body, not a Listing with an illegal property.
        // The declared type is the only place that disambiguates this
        // mixed element/property body before evaluation.
        TypedObjectLiteral("Dynamic", self.parse_object_body_members())
      _ =>
        // PKL-138: peek the first significant token inside `{` to
        // decide whether `new { ... }` is a listing, mapping, or object
        // body. Without an expected Dynamic annotation, infer from the
        // body's first item shape.
        self.parse_inferred_new_body()
    }
  } else {
    TypedObjectLiteral(type_name, self.parse_object_body_members())
  }
  self.builder.finish_node()
  value
}

///|
/// PKL-138: dispatch `new { ... }` (no explicit type) by peeking the
/// first significant token inside the body brace.
///
/// - First token `[` → Mapping body (`new { ["a"] = 1 }`)
/// - First token starts a property decl (`ident =` / `ident :` / `ident {`)
///   or a visibility / control keyword (`local`, `hidden`, `fixed`, `when`,
///   `for`) → Object body
/// - First token `}` (empty body) → Object body (the eval coerces to the
///   empty Listing / Mapping if the binding's annotation requires it)
/// - Otherwise (bare expression: literal, `(`, `new`, unary operator,
///   identifier-without-property-suffix) → Listing body
fn Parser::parse_inferred_new_body(self : Parser) -> Expr {
  self.skip_whitespace()
  if !self.at(lbrace()) {
    return self.parse_object_body()
  }
  let first_idx = self.skip_trivia_from(self.pos + 1)
  let first = if first_idx >= self.len {
    eof()
  } else {
    self.tokens[first_idx].kind()
  }
  let first_text = if first_idx >= self.len {
    ""
  } else {
    self.tokens[first_idx].text()
  }
  // PKL-150: `{ local NAME = EXPR; … }` can sit in either an object
  // body (subsequent members are property declarations) or a listing
  // body (subsequent members are bare element expressions). Disambiguate
  // by skipping past every leading `local …;` block and looking at the
  // first non-`local` token: a property-decl form → object, anything
  // else → listing.
  let after_locals_idx = self.skip_local_decls_from(first_idx)
  let after_locals_kind = if after_locals_idx >= self.len {
    eof()
  } else {
    self.tokens[after_locals_idx].kind()
  }
  let after_locals_text = if after_locals_idx >= self.len {
    ""
  } else {
    self.tokens[after_locals_idx].text()
  }
  let body_first = if first == local_kw() { after_locals_kind } else { first }
  let body_first_idx = if first == local_kw() {
    after_locals_idx
  } else {
    first_idx
  }
  let _ = if first == local_kw() { after_locals_text } else { first_text }
  // PKL-148x: a body whose first significant token is `...` (triple
  // dot, spread) routes through the object-body parser now that the
  // Dynamic-shape `@element$` / `@subscript$` sentinels exist; the
  // object-body `@spread` arm absorbs the payload regardless of
  // whether it evaluates to an ObjectValue (merge members),
  // ListingValue / ListValue / SetValue (push as `@element$`
  // entries), MappingValue (push as `@subscript$` entries), or
  // NullValue (silently skip).
  let body_first_is_triple_dot = body_first_idx + 2 < self.len &&
    self.tokens[body_first_idx].kind() == dot() &&
    self.tokens[body_first_idx + 1].kind() == dot() &&
    self.tokens[body_first_idx + 2].kind() == dot()
  // PKL-148aj: skip past any leading member-header tokens (modifier
  // idents like `const` / `fixed` / `abstract`, the `hidden` / `local`
  // visibility prefixes, and annotations) before deciding whether the
  // body parses as an object or a listing. Previously only `hidden`
  // and `fixed` were treated as property-decl signals here, so `const
  // qux = 99` (and any combination like `const local function`) routed
  // through `parse_listing_body` and the `qux = 99` member was lost.
  let prefix_skipped_idx = self.skip_member_prefix_from(body_first_idx)
  let prefix_skipped_kind = if prefix_skipped_idx >= self.len {
    eof()
  } else {
    self.tokens[prefix_skipped_idx].kind()
  }
  let prefix_skipped_text = if prefix_skipped_idx >= self.len {
    ""
  } else {
    self.tokens[prefix_skipped_idx].text()
  }
  if body_first == lbracket() {
    self.parse_mapping_body()
  } else if body_first == rbrace() ||
    body_first == when_kw() ||
    body_first == for_kw() ||
    body_first_is_triple_dot ||
    self.function_amend_signature_at_from(body_first_idx) ||
    self.at_property_decl_from(body_first_idx) ||
    self.at_property_decl_from(prefix_skipped_idx) ||
    prefix_skipped_text == "function" ||
    prefix_skipped_kind == when_kw() ||
    prefix_skipped_kind == for_kw() {
    self.parse_object_body()
  } else {
    self.parse_listing_body()
  }
}

///|
/// PKL-148aj: walk past trivia + any sequence of annotation / visibility
/// (`local`, `hidden`) / modifier (`const`, `fixed`, `abstract`, ...)
/// tokens starting at `start`, returning the index of the first non-
/// prefix significant token. Mirrors the loop in `consume_member_header`
/// without committing to a parser advance, so callers can peek at what a
/// modifier run is actually decorating.
fn Parser::skip_member_prefix_from(self : Parser, start : Int) -> Int {
  let mut i = self.skip_trivia_from(start)
  while i < self.len {
    let kind = self.tokens[i].kind()
    let text = self.tokens[i].text()
    if kind == at_sign() {
      // Skip the annotation name (dotted identifier chain) and an
      // optional balanced `(...)` / `{...}` payload.
      let mut j = i + 1
      while j < self.len &&
            (
              self.tokens[j].kind() == identifier() ||
              self.tokens[j].kind() == dot()
            ) {
        j += 1
      }
      j = self.skip_trivia_from(j)
      if j < self.len &&
        (self.tokens[j].kind() == lparen() || self.tokens[j].kind() == lbrace()) {
        let open_kind = self.tokens[j].kind()
        let close_kind = if open_kind == lparen() { rparen() } else { rbrace() }
        let mut depth = 0
        while j < self.len {
          let k = self.tokens[j].kind()
          if k == open_kind {
            depth += 1
          } else if k == close_kind {
            depth -= 1
            if depth == 0 {
              j += 1
              break
            }
          }
          j += 1
        }
      }
      i = self.skip_trivia_from(j)
    } else if kind == local_kw() {
      i = self.skip_trivia_from(i + 1)
    } else if kind == identifier() &&
      (text == "hidden" || is_modifier_text(text)) {
      i = self.skip_trivia_from(i + 1)
    } else {
      break
    }
  }
  i
}

///|
/// Walk past every `local NAME [: TYPE] = EXPR` declaration starting
/// at `start`, returning the index of the first non-`local` significant
/// token. Used by `parse_inferred_new_body` to disambiguate object-vs-
/// listing bodies that lead with one or more `local` bindings — the
/// body kind is determined by what follows the leading locals, not by
/// the `local` keyword itself.
fn Parser::skip_local_decls_from(self : Parser, start : Int) -> Int {
  let mut i = start
  while i < self.len {
    let i_skipped = self.skip_trivia_from(i)
    if i_skipped >= self.len {
      return i_skipped
    }
    if self.tokens[i_skipped].kind() != local_kw() {
      return i_skipped
    }
    // Skip `local`, then walk to the end of the declaration (the next
    // separator at zero-balance brace/paren/bracket depth).
    let mut j = i_skipped + 1
    let mut parens = 0
    let mut braces = 0
    let mut brackets = 0
    while j < self.len {
      let kind = self.tokens[j].kind()
      if parens == 0 && braces == 0 && brackets == 0 {
        if kind == rbrace() || kind == rparen() || kind == rbracket() {
          break
        }
        if is_separator(kind) {
          break
        }
      }
      if kind == lparen() {
        parens += 1
      } else if kind == rparen() && parens > 0 {
        parens -= 1
      } else if kind == lbrace() {
        braces += 1
      } else if kind == rbrace() && braces > 0 {
        braces -= 1
      } else if kind == lbracket() {
        brackets += 1
      } else if kind == rbracket() && brackets > 0 {
        brackets -= 1
      }
      j += 1
    }
    i = j
  }
  i
}

///|
/// Helper: skip trivia tokens starting at `start` and return the next
/// significant token index.
fn Parser::skip_trivia_from(self : Parser, start : Int) -> Int {
  let mut i = start
  while i < self.len && is_trivia(self.tokens[i].kind()) {
    i += 1
  }
  i
}