1 /* <lambda>null2 * Copyright 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 @file:Suppress("NOTHING_TO_INLINE", "KotlinRedundantDiagnosticSuppress") 18 19 package androidx.compose.runtime.changelist 20 21 import androidx.compose.runtime.Applier 22 import androidx.compose.runtime.EnableDebugRuntimeChecks 23 import androidx.compose.runtime.InternalComposeApi 24 import androidx.compose.runtime.RememberManager 25 import androidx.compose.runtime.SlotWriter 26 import androidx.compose.runtime.changelist.Operation.ObjectParameter 27 import androidx.compose.runtime.collection.fastCopyInto 28 import androidx.compose.runtime.debugRuntimeCheck 29 import androidx.compose.runtime.requirePrecondition 30 import kotlin.contracts.ExperimentalContracts 31 import kotlin.contracts.InvocationKind.EXACTLY_ONCE 32 import kotlin.contracts.contract 33 import kotlin.jvm.JvmField 34 import kotlin.jvm.JvmInline 35 36 private const val OperationsMaxResizeAmount = 1024 37 internal const val OperationsInitialCapacity = 16 38 39 /** 40 * `Operations` is a data structure used to store a sequence of [Operations][Operation] and their 41 * respective arguments. Although the Stack is written to as a last-in-first-out structure, it is 42 * iterated in a first-in-first-out structure. This makes the structure behave somewhat like a 43 * specialized dequeue. 44 * 45 * `Operations` is backed by three backing arrays: one for the operation sequence, the `int` 46 * arguments, and the object arguments. This helps reduce allocations as much as possible. 47 * 48 * `Operations` is not a thread safe data structure. 49 */ 50 internal class Operations : OperationsDebugStringFormattable() { 51 // To create an array of non-nullable references, Kotlin would normally force us to pass an 52 // initializer lambda to the array constructor, which could be expensive for larger arrays. 53 // Using an array of Operation? allows us to bypass the initialization of every entry, but it 54 // means that accessing an entry known to be non-null requires a null-check (via !! for 55 // instance), which produces unwanted code bloat in hot paths. The cast used here allows us to 56 // allocate the array as an array of Operation? but to use it as an array of Operation. 57 // When we want to remove an item from the array, we can cast it back to an array of Operation? 58 // to set the corresponding entry to null (see pop() for instance). 59 @Suppress("UNCHECKED_CAST") 60 @JvmField 61 internal var opCodes = arrayOfNulls<Operation>(OperationsInitialCapacity) as Array<Operation> 62 @JvmField internal var opCodesSize = 0 63 64 @JvmField internal var intArgs = IntArray(OperationsInitialCapacity) 65 @JvmField internal var intArgsSize = 0 66 67 @JvmField internal var objectArgs = arrayOfNulls<Any>(OperationsInitialCapacity) 68 @JvmField internal var objectArgsSize = 0 69 70 /* 71 The two masks below are used to track which arguments have been assigned for the most 72 recently pushed operation. When an argument is set, its corresponding bit is set to 1. 73 The bit indices correspond to the parameter's offset value. Offset 0 corresponds to the 74 least significant bit, so a parameter with offset 2 will correspond to the mask 0b100. 75 */ 76 private var pushedIntMask = 0b0 77 private var pushedObjectMask = 0b0 78 79 /** Returns the number of pending operations contained in this operation stack. */ 80 val size: Int 81 get() = opCodesSize 82 83 fun isEmpty() = size == 0 84 85 fun isNotEmpty() = size != 0 86 87 /** Resets the collection to its initial state, clearing all stored operations and arguments. */ 88 fun clear() { 89 // We don't technically need to clear the opCodes or intArgs arrays, because we ensure 90 // that every operation that gets pushed to this data structure has all of its arguments 91 // set exactly once. This guarantees that they'll overwrite any stale, dirty values from 92 // previous entries on the stack, so we shouldn't ever run into problems of having 93 // uninitialized values causing undefined behavior for other operations. 94 opCodesSize = 0 95 intArgsSize = 0 96 // Clear the object arguments array to prevent leaking memory 97 objectArgs.fill(null, fromIndex = 0, toIndex = objectArgsSize) 98 objectArgsSize = 0 99 } 100 101 /** 102 * Pushes [operation] to the stack, ensures that there is space in the backing argument arrays 103 * to store the parameters, and increments the internal pointers to track the operation's 104 * arguments. 105 * 106 * It is expected that the arguments of this operation will be added after [pushOp] returns. The 107 * index to write a parameter is `intArgsSize - operation.ints + arg.offset` for int arguments, 108 * and `objectArgsSize - operation.objects + arg.offset` for object arguments. 109 * 110 * Do not use this API outside of the [Operations] class directly. Use [push] instead. This 111 * function is kept visible so that it may be inlined. 112 */ 113 @InternalComposeApi 114 fun pushOp(operation: Operation) { 115 if (EnableDebugRuntimeChecks) { 116 pushedIntMask = 0b0 117 pushedObjectMask = 0b0 118 } 119 120 if (opCodesSize == opCodes.size) { 121 resizeOpCodes() 122 } 123 ensureIntArgsSizeAtLeast(intArgsSize + operation.ints) 124 ensureObjectArgsSizeAtLeast(objectArgsSize + operation.objects) 125 126 // Record operation, advance argument pointers 127 opCodes[opCodesSize++] = operation 128 intArgsSize += operation.ints 129 objectArgsSize += operation.objects 130 } 131 132 private fun determineNewSize(currentSize: Int, requiredSize: Int): Int { 133 val resizeAmount = currentSize.coerceAtMost(OperationsMaxResizeAmount) 134 return (currentSize + resizeAmount).coerceAtLeast(requiredSize) 135 } 136 137 private fun resizeOpCodes() { 138 val resizeAmount = opCodesSize.coerceAtMost(OperationsMaxResizeAmount) 139 @Suppress("UNCHECKED_CAST") 140 val newOpCodes = arrayOfNulls<Operation>(opCodesSize + resizeAmount) as Array<Operation> 141 opCodes = opCodes.fastCopyInto(newOpCodes, 0, 0, opCodesSize) 142 } 143 144 private inline fun ensureIntArgsSizeAtLeast(requiredSize: Int) { 145 val currentSize = intArgs.size 146 if (requiredSize > currentSize) { 147 resizeIntArgs(currentSize, requiredSize) 148 } 149 } 150 151 private fun resizeIntArgs(currentSize: Int, requiredSize: Int) { 152 val newIntArgs = IntArray(determineNewSize(currentSize, requiredSize)) 153 intArgs.copyInto(newIntArgs, 0, 0, currentSize) 154 intArgs = newIntArgs 155 } 156 157 private inline fun ensureObjectArgsSizeAtLeast(requiredSize: Int) { 158 val currentSize = objectArgs.size 159 if (requiredSize > currentSize) { 160 resizeObjectArgs(currentSize, requiredSize) 161 } 162 } 163 164 private fun resizeObjectArgs(currentSize: Int, requiredSize: Int) { 165 val newObjectArgs = arrayOfNulls<Any>(determineNewSize(currentSize, requiredSize)) 166 objectArgs.fastCopyInto(newObjectArgs, 0, 0, currentSize) 167 objectArgs = newObjectArgs 168 } 169 170 /** 171 * Adds an [operation] to the stack with no arguments. 172 * 173 * If [operation] defines any arguments, you must use the overload that accepts an `args` lambda 174 * to provide those arguments. This function will throw an exception if the operation defines 175 * any arguments. 176 */ 177 fun push(operation: Operation) { 178 if (EnableDebugRuntimeChecks) { 179 requirePrecondition((operation.ints and operation.objects) == 0) { 180 exceptionMessageForOperationPushNoScope(operation) 181 } 182 } 183 @OptIn(InternalComposeApi::class) pushOp(operation) 184 } 185 186 private fun exceptionMessageForOperationPushNoScope(operation: Operation) = 187 "Cannot push $operation without arguments because it expects " + 188 "${operation.ints} ints and ${operation.objects} objects." 189 190 /** 191 * Adds an [operation] to the stack with arguments. To set arguments on the operation, call 192 * [WriteScope.setObject] and [WriteScope.setInt] inside of the [args] lambda. 193 * 194 * The [args] lambda is called exactly once inline. You must set all arguments defined on the 195 * [operation] exactly once. An exception is thrown if you attempt to call [WriteScope.setInt] 196 * or [WriteScope.setObject] on an argument you have already set, and when [args] returns if not 197 * all arguments were set. 198 */ 199 @Suppress("BanInlineOptIn") 200 @OptIn(ExperimentalContracts::class) 201 inline fun push(operation: Operation, args: WriteScope.() -> Unit) { 202 contract { callsInPlace(args, EXACTLY_ONCE) } 203 204 @OptIn(InternalComposeApi::class) pushOp(operation) 205 WriteScope(this).args() 206 207 ensureAllArgumentsPushedFor(operation) 208 } 209 210 fun ensureAllArgumentsPushedFor(operation: Operation) { 211 debugRuntimeCheck( 212 pushedIntMask == createExpectedArgMask(operation.ints) && 213 pushedObjectMask == createExpectedArgMask(operation.objects) 214 ) { 215 exceptionMessageForOperationPushWithScope(operation) 216 } 217 } 218 219 private fun exceptionMessageForOperationPushWithScope(operation: Operation): String { 220 var missingIntCount = 0 221 val missingInts = buildString { 222 repeat(operation.ints) { arg -> 223 if ((0b1 shl arg) and pushedIntMask == 0b0) { 224 if (missingIntCount > 0) append(", ") 225 append(operation.intParamName(arg)) 226 missingIntCount++ 227 } 228 } 229 } 230 231 var missingObjectCount = 0 232 val missingObjects = buildString { 233 repeat(operation.objects) { arg -> 234 if ((0b1 shl arg) and pushedObjectMask == 0b0) { 235 if (missingIntCount > 0) append(", ") 236 append(operation.objectParamName(ObjectParameter<Nothing>(arg))) 237 missingObjectCount++ 238 } 239 } 240 } 241 242 return "Error while pushing $operation. Not all arguments were provided. " + 243 "Missing $missingIntCount int arguments ($missingInts) " + 244 "and $missingObjectCount object arguments ($missingObjects)." 245 } 246 247 /** 248 * Returns a bitmask int where the bottommost [paramCount] bits are 1's, and the rest of the 249 * bits are 0's. This corresponds to what [pushedIntMask] and [pushedObjectMask] will equal if 250 * all [paramCount] arguments are set for the most recently pushed operation. 251 */ 252 private inline fun createExpectedArgMask(paramCount: Int): Int { 253 // Calling ushr(32) no-ops instead of returning 0, so add a special case if paramCount is 0 254 // Keep the if/else in the parenthesis so we generate a single csetm on aarch64 255 return (if (paramCount == 0) 0 else 0b0.inv()) ushr (Int.SIZE_BITS - paramCount) 256 } 257 258 /** 259 * Removes the most recently added operation and all of its arguments from the stack, clearing 260 * references. 261 */ 262 fun pop() { 263 // We could check for isEmpty(), instead we'll just let the array access throw an index out 264 // of bounds exception 265 val opCodes = opCodes 266 val op = opCodes[--opCodesSize] 267 // See comment where opCodes is defined 268 @Suppress("UNCHECKED_CAST") 269 (opCodes as Array<Operation?>)[opCodesSize] = null 270 271 repeat(op.objects) { objectArgs[--objectArgsSize] = null } 272 273 // We can just skip this work and leave the content of the array as is 274 // repeat(op.ints) { intArgs[--intArgsSize] = 0 } 275 intArgsSize -= op.ints 276 } 277 278 /** 279 * Removes the most recently added operation and all of its arguments from this stack, pushing 280 * them into the [other] stack, and then clearing their references in this stack. 281 */ 282 @OptIn(InternalComposeApi::class) 283 fun popInto(other: Operations) { 284 // We could check for isEmpty(), instead we'll just let the array access throw an index out 285 // of bounds exception 286 val opCodes = opCodes 287 val op = opCodes[--opCodesSize] 288 // See comment where opCodes is defined 289 @Suppress("UNCHECKED_CAST") 290 (opCodes as Array<Operation?>)[opCodesSize] = null 291 292 other.pushOp(op) 293 294 // Move the objects then null out our contents 295 objectArgs.fastCopyInto( 296 destination = other.objectArgs, 297 destinationOffset = other.objectArgsSize - op.objects, 298 startIndex = objectArgsSize - op.objects, 299 endIndex = objectArgsSize 300 ) 301 objectArgs.fill(null, objectArgsSize - op.objects, objectArgsSize) 302 303 // Move the ints too, but no need to clear our current values 304 intArgs.copyInto( 305 destination = other.intArgs, 306 destinationOffset = other.intArgsSize - op.ints, 307 startIndex = intArgsSize - op.ints, 308 endIndex = intArgsSize 309 ) 310 311 objectArgsSize -= op.objects 312 intArgsSize -= op.ints 313 } 314 315 /** 316 * Iterates through the stack in the order that items were added, calling [sink] for each 317 * operation in the stack. 318 * 319 * Iteration moves from oldest elements to newest (more like a queue than a stack). [drain] is a 320 * destructive operation that also clears the items in the stack, and is used to apply all of 321 * the operations in the stack, since they must be applied in the order they were added instead 322 * of being popped. 323 */ 324 inline fun drain(sink: OpIterator.() -> Unit) { 325 forEach(sink) 326 clear() 327 } 328 329 /** 330 * Iterates through the stack, calling [action] for each operation in the stack. Iteration moves 331 * from oldest elements to newest (more like a queue than a stack). 332 */ 333 inline fun forEach(action: OpIterator.() -> Unit) { 334 if (isNotEmpty()) { 335 val iterator = OpIterator() 336 do { 337 iterator.action() 338 } while (iterator.next()) 339 } 340 } 341 342 fun executeAndFlushAllPendingOperations( 343 applier: Applier<*>, 344 slots: SlotWriter, 345 rememberManager: RememberManager, 346 errorContext: OperationErrorContext? 347 ) { 348 drain { 349 with(operation) { 350 executeWithComposeStackTrace(applier, slots, rememberManager, errorContext) 351 } 352 } 353 } 354 355 private fun String.indent() = "$this " 356 357 private inline fun peekOperation() = opCodes[opCodesSize - 1] 358 359 private inline fun topIntIndexOf(parameter: IntParameter) = 360 intArgsSize - peekOperation().ints + parameter 361 362 private inline fun topObjectIndexOf(parameter: ObjectParameter<*>) = 363 objectArgsSize - peekOperation().objects + parameter.offset 364 365 @JvmInline 366 value class WriteScope(private val stack: Operations) { 367 val operation: Operation 368 get() = stack.peekOperation() 369 370 inline fun setInt(parameter: IntParameter, value: Int) = 371 with(stack) { 372 if (EnableDebugRuntimeChecks) { 373 val mask = 0b1 shl parameter 374 debugRuntimeCheck(pushedIntMask and mask == 0) { 375 "Already pushed argument ${operation.intParamName(parameter)}" 376 } 377 pushedIntMask = pushedIntMask or mask 378 } 379 intArgs[topIntIndexOf(parameter)] = value 380 } 381 382 inline fun setInts( 383 parameter1: IntParameter, 384 value1: Int, 385 parameter2: IntParameter, 386 value2: Int 387 ) = 388 with(stack) { 389 if (EnableDebugRuntimeChecks) { 390 val mask = (0b1 shl parameter1) or (0b1 shl parameter2) 391 debugRuntimeCheck(pushedIntMask and mask == 0) { 392 "Already pushed argument(s) ${operation.intParamName(parameter1)}" + 393 ", ${operation.intParamName(parameter2)}" 394 } 395 pushedIntMask = pushedIntMask or mask 396 } 397 val base = intArgsSize - peekOperation().ints 398 val intArgs = intArgs 399 intArgs[base + parameter1] = value1 400 intArgs[base + parameter2] = value2 401 } 402 403 inline fun setInts( 404 parameter1: IntParameter, 405 value1: Int, 406 parameter2: IntParameter, 407 value2: Int, 408 parameter3: IntParameter, 409 value3: Int 410 ) = 411 with(stack) { 412 if (EnableDebugRuntimeChecks) { 413 val mask = (0b1 shl parameter1) or (0b1 shl parameter2) or (0b1 shl parameter3) 414 debugRuntimeCheck(pushedIntMask and mask == 0) { 415 "Already pushed argument(s) ${operation.intParamName(parameter1)}" + 416 ", ${operation.intParamName(parameter2)}" + 417 ", ${operation.intParamName(parameter3)}" 418 } 419 pushedIntMask = pushedIntMask or mask 420 } 421 val base = intArgsSize - peekOperation().ints 422 val intArgs = intArgs 423 intArgs[base + parameter1] = value1 424 intArgs[base + parameter2] = value2 425 intArgs[base + parameter3] = value3 426 } 427 428 fun <T> setObject(parameter: ObjectParameter<T>, value: T) = 429 with(stack) { 430 if (EnableDebugRuntimeChecks) { 431 val mask = 0b1 shl parameter.offset 432 debugRuntimeCheck(pushedObjectMask and mask == 0) { 433 "Already pushed argument ${operation.objectParamName(parameter)}" 434 } 435 pushedObjectMask = pushedObjectMask or mask 436 } 437 objectArgs[topObjectIndexOf(parameter)] = value 438 } 439 440 fun <T, U> setObjects( 441 parameter1: ObjectParameter<T>, 442 value1: T, 443 parameter2: ObjectParameter<U>, 444 value2: U 445 ) = 446 with(stack) { 447 if (EnableDebugRuntimeChecks) { 448 val mask = (0b1 shl parameter1.offset) or (0b1 shl parameter2.offset) 449 debugRuntimeCheck(pushedIntMask and mask == 0) { 450 "Already pushed argument(s) ${operation.objectParamName(parameter1)}" + 451 ", ${operation.objectParamName(parameter2)}" 452 } 453 pushedIntMask = pushedIntMask or mask 454 } 455 val base = objectArgsSize - peekOperation().objects 456 val objectArgs = objectArgs 457 objectArgs[base + parameter1.offset] = value1 458 objectArgs[base + parameter2.offset] = value2 459 } 460 461 fun <T, U, V> setObjects( 462 parameter1: ObjectParameter<T>, 463 value1: T, 464 parameter2: ObjectParameter<U>, 465 value2: U, 466 parameter3: ObjectParameter<V>, 467 value3: V 468 ) = 469 with(stack) { 470 if (EnableDebugRuntimeChecks) { 471 val mask = 472 (0b1 shl parameter1.offset) or 473 (0b1 shl parameter2.offset) or 474 (0b1 shl parameter3.offset) 475 debugRuntimeCheck(pushedIntMask and mask == 0) { 476 "Already pushed argument(s) ${operation.objectParamName(parameter1)}" + 477 ", ${operation.objectParamName(parameter2)}" + 478 ", ${operation.objectParamName(parameter3)}" 479 } 480 pushedIntMask = pushedIntMask or mask 481 } 482 val base = objectArgsSize - peekOperation().objects 483 val objectArgs = objectArgs 484 objectArgs[base + parameter1.offset] = value1 485 objectArgs[base + parameter2.offset] = value2 486 objectArgs[base + parameter3.offset] = value3 487 } 488 489 fun <T, U, V, W> setObjects( 490 parameter1: ObjectParameter<T>, 491 value1: T, 492 parameter2: ObjectParameter<U>, 493 value2: U, 494 parameter3: ObjectParameter<V>, 495 value3: V, 496 parameter4: ObjectParameter<W>, 497 value4: W 498 ) = 499 with(stack) { 500 if (EnableDebugRuntimeChecks) { 501 val mask = 502 (0b1 shl parameter1.offset) or 503 (0b1 shl parameter2.offset) or 504 (0b1 shl parameter3.offset) or 505 (0b1 shl parameter4.offset) 506 debugRuntimeCheck(pushedIntMask and mask == 0) { 507 "Already pushed argument(s) ${operation.objectParamName(parameter1)}" + 508 ", ${operation.objectParamName(parameter2)}" + 509 ", ${operation.objectParamName(parameter3)}" + 510 ", ${operation.objectParamName(parameter4)}" 511 } 512 pushedIntMask = pushedIntMask or mask 513 } 514 val base = objectArgsSize - peekOperation().objects 515 val objectArgs = objectArgs 516 objectArgs[base + parameter1.offset] = value1 517 objectArgs[base + parameter2.offset] = value2 518 objectArgs[base + parameter3.offset] = value3 519 objectArgs[base + parameter4.offset] = value4 520 } 521 } 522 523 inner class OpIterator : OperationArgContainer { 524 private var opIdx = 0 525 private var intIdx = 0 526 private var objIdx = 0 527 528 fun next(): Boolean { 529 if (opIdx >= opCodesSize) return false 530 531 val op = operation 532 intIdx += op.ints 533 objIdx += op.objects 534 opIdx++ 535 return opIdx < opCodesSize 536 } 537 538 /** Returns the [Operation] at the current position of the iterator in the [Operations]. */ 539 val operation: Operation 540 get() = opCodes[opIdx] 541 542 /** 543 * Returns the value of [parameter] for the operation at the current position of the 544 * iterator. 545 */ 546 override fun getInt(parameter: IntParameter): Int = intArgs[intIdx + parameter] 547 548 /** 549 * Returns the value of [parameter] for the operation at the current position of the 550 * iterator. 551 */ 552 @Suppress("UNCHECKED_CAST") 553 override fun <T> getObject(parameter: ObjectParameter<T>): T = 554 objectArgs[objIdx + parameter.offset] as T 555 556 @Suppress("UNUSED") 557 fun currentOperationDebugString() = buildString { 558 append("operation[") 559 append(opIdx) 560 append("] = ") 561 append(currentOpToDebugString("")) 562 } 563 } 564 565 @Suppress("POTENTIALLY_NON_REPORTED_ANNOTATION") 566 @Deprecated( 567 "toString() will return the default implementation from Any. " + 568 "Did you mean to use toDebugString()?", 569 ReplaceWith("toDebugString()") 570 ) 571 override fun toString(): String { 572 return super.toString() 573 } 574 575 override fun toDebugString(linePrefix: String): String { 576 return buildString { 577 var opNumber = 0 578 this@Operations.forEach { 579 append(linePrefix) 580 append(opNumber++) 581 append(". ") 582 appendLine(currentOpToDebugString(linePrefix)) 583 } 584 } 585 } 586 587 private fun Operations.OpIterator.currentOpToDebugString(linePrefix: String): String { 588 val operation = operation 589 return if (operation.ints == 0 && operation.objects == 0) { 590 operation.name 591 } else 592 buildString { 593 append(operation.name) 594 append('(') 595 var isFirstParam = true 596 val argLinePrefix = linePrefix.indent() 597 repeat(operation.ints) { offset -> 598 val name = operation.intParamName(offset) 599 if (!isFirstParam) append(", ") else isFirstParam = false 600 appendLine() 601 append(argLinePrefix) 602 append(name) 603 append(" = ") 604 append(getInt(offset)) 605 } 606 repeat(operation.objects) { offset -> 607 val param = ObjectParameter<Any?>(offset) 608 val name = operation.objectParamName(param) 609 if (!isFirstParam) append(", ") else isFirstParam = false 610 appendLine() 611 append(argLinePrefix) 612 append(name) 613 append(" = ") 614 append(getObject(param).formatOpArgumentToString(argLinePrefix)) 615 } 616 appendLine() 617 append(linePrefix) 618 append(")") 619 } 620 } 621 622 private fun Any?.formatOpArgumentToString(linePrefix: String) = 623 when (this) { 624 null -> "null" 625 is Array<*> -> asIterable().toCollectionString(linePrefix) 626 is IntArray -> asIterable().toCollectionString(linePrefix) 627 is LongArray -> asIterable().toCollectionString(linePrefix) 628 is FloatArray -> asIterable().toCollectionString(linePrefix) 629 is DoubleArray -> asIterable().toCollectionString(linePrefix) 630 is Iterable<*> -> toCollectionString(linePrefix) 631 is OperationsDebugStringFormattable -> toDebugString(linePrefix) 632 else -> toString() 633 } 634 635 private fun <T> Iterable<T>.toCollectionString(linePrefix: String): String = 636 joinToString(prefix = "[", postfix = "]", separator = ", ") { 637 it.formatOpArgumentToString(linePrefix) 638 } 639 } 640 641 internal abstract class OperationsDebugStringFormattable { toDebugStringnull642 abstract fun toDebugString(linePrefix: String = " "): String 643 } 644