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