Coverage Summary for Class: GhostJsonFlatReader (com.ghost.serialization.parser)

Class Method, % Branch, % Line, % Instruction, %
GhostJsonFlatReader 100% (54/54) 72.2% (184/255) 88.7% (376/424) 84.9% (1844/2171)
GhostJsonFlatReader$Companion 100% (1/1) 100% (1/1) 100% (2/2)
Total 100% (55/55) 72.2% (184/255) 88.7% (377/425) 85% (1846/2173)


 @file:OptIn(InternalGhostApi::class)
 @file:Suppress("FunctionName")
 
 package com.ghost.serialization.parser
 
 import com.ghost.serialization.parser.GhostJsonConstants as C
 
 import com.ghost.serialization.InternalGhostApi
 import com.ghost.serialization.exception.GhostJsonException
 import okio.ByteString
 import okio.ByteString.Companion.encodeUtf8
 import com.ghost.serialization.parser.GhostHeuristics.initialCollectionCapacity
 
 /**
  * Ultra-fast, specialized JSON parser for Kotlin Multiplatform that operates directly
  * on a flat [ByteArray] without any interface dispatch or hasFastPath boundaries.
  */
 open class GhostJsonFlatReader(
     public var rawData: ByteArray,
     var maxDepth: Int = C.MAX_DEPTH,
     /**
      * When true, enables strict JSON validation: rejects unknown/unmapped fields
      * and performs strict bitwise syntax validation on missing or duplicate commas.
      * Defaults to false for maximum lenient parsing performance.
      */
     var strictMode: Boolean = false,
     var coerceStringsToNumbers: Boolean = false,
     var coerceBooleans: Boolean = false,
     var maxCollectionSize: Int = GhostHeuristics.maxCollectionSize,
     /**
      * When true, [captureRawJson] copies captured UTF-8 into an owned array (offset 0)
      * instead of slicing [rawData]. Set by the [GhostJsonStringReader] deserialize bridge.
      */
     var materializeRawJsonCaptures: Boolean = false,
 ) {
 
     @PublishedApi
     internal val source = ByteArrayGhostSource(rawData)
 
     public var limit: Int = rawData.size
 
     public var position: Int = 0
 
     public var nextTokenByte: Int = C.RESET_TOKEN_BYTE
 
     @InternalGhostApi
     fun _getPosition(): Int = position
 
     @InternalGhostApi
     fun _setPosition(p: Int) {
         position = p
     }
 
     @InternalGhostApi
     fun _getRawData(): ByteArray = rawData
 
     @InternalGhostApi
     fun _setNextTokenByte(t: Int) {
         nextTokenByte = t
     }
 
     internal val stringPool = arrayOfNulls<String>(C.STR_POOL_SIZE)
     internal val stringPoolHashes = IntArray(C.STR_POOL_SIZE)
 
     internal var lastScanContentWas7BitOnly: Boolean = false
 
     var depth: Int = 0
     @PublishedApi internal var needsCommaMask: Long = 0L
     @PublishedApi internal var commaConsumedMask: Long = 0L
 
     /**
      * Gets the byte at the specified index, masking it to a positive integer.
      */
     @Suppress("NOTHING_TO_INLINE")
     public inline fun getByte(index: Int): Int {
         return rawData[index].toInt() and C.BYTE_MASK
     }
 
     /**
      * Throws a structured [GhostJsonException] with exact position, line, and column numbers.
      */
     fun throwError(message: String): Nothing {
         val errorPosition = position
         val errorEnd = if (errorPosition > limit) {
             limit
         } else {
             errorPosition
         }
 
         throw GhostJsonException(
             baseMessage = "$message at position $errorPosition",
             computeLineCol = {
                 var columnNumber = 0
                 var lineNumber = 0
                 var byteIndex = 0
                 while (byteIndex < errorEnd) {
                     if ((rawData[byteIndex].toInt() and C.BYTE_MASK) == C.NEWLINE_INT) {
                         lineNumber++
                         columnNumber = 0
                     } else {
                         columnNumber++
                     }
                     byteIndex++
                 }
                 intArrayOf(lineNumber, columnNumber)
             }
         )
     }
 
     /**
      * Skips forward in the byte array by [n] bytes and resets [nextTokenByte].
      */
     fun internalSkip(n: Int) {
         position += n
         nextTokenByte = C.RESET_TOKEN_BYTE
     }
 
     /**
      * Advances the position past any whitespace and caches the next non-whitespace token byte.
      */
     fun skipWhitespace() {
         val localData = rawData
         val nextPos = findNextNonWhitespaceImpl(position, limit) {
             localData[it].toInt() and C.BYTE_MASK
         }
 
         if (nextPos != -1) {
             position = nextPos
             nextTokenByte = localData[position].toInt() and C.BYTE_MASK
         } else {
             position = limit
             nextTokenByte = C.MATCH_END
         }
     }
 
     /**
      * Peeks at the next key to see if it matches the discriminator name without consuming it.
      */
     fun peekDiscriminator(key: String = C.DEFAULT_DISCRIMINATOR_KEY): String? {
         if (key == C.DEFAULT_DISCRIMINATOR_KEY) {
             return peekDiscriminator(C.TYPE_BS)
         }
         return peekDiscriminator(key.encodeUtf8())
     }
 
     /**
      * Peeks at the next key to see if it matches the discriminator byte string without consuming it.
      */
     fun peekDiscriminator(key: ByteString): String? {
         return GhostDiscriminatorPeeker.peek(
             source,
             rawData,
             false,
             position,
             limit,
             key
         )
     }
 
     /**
      * Peeks and returns the next token byte in the stream, skipping preceding whitespaces.
      */
     fun peekNextToken(): Int {
         val cached = nextTokenByte
         if (cached != -1) {
             return cached
         }
         skipWhitespace()
         return nextTokenByte
     }
 
     /**
      * Peeks and returns the next token byte as a [Byte].
      */
     fun peekByte(): Byte = peekNextToken().toByte()
 
     fun nextNonWhitespace(): Int {
         val nextToken = peekNextToken()
         if (nextToken == -1) {
             throwError(C.ERR_UNEXPECTED_EOF)
         }
         internalSkip(1)
         return nextToken
     }
 
     /**
      * Skips and validates that the next characters in the stream match the [expected] byte sequence.
      */
     @InternalGhostApi
     fun skipAndValidateLiteral(expected: ByteString) {
         val size = expected.size
         if (position + size > limit || !expected.rangeEquals(0, rawData, position, size)) {
             throwError("Expected literal ${expected.utf8()}")
         }
         position += size
         nextTokenByte = C.RESET_TOKEN_BYTE
     }
 
     open fun nextFloat(): Float = nextFloatExtension()
     open fun nextDouble(): Double = nextDoubleExtension()
     open fun nextInt(): Int = nextIntExtension()
     open fun nextLong(): Long = nextLongExtension()
 
     /**
      * Resets the reader's state to process a new byte payload.
      */
     fun reset(newData: ByteArray, newLimit: Int = newData.size) {
         resetSlice(newData, offset = 0, length = newLimit)
     }
 
     /**
      * Resets the reader to parse a sub-range of [buffer] without copying (zero-copy slice decode).
      */
     fun resetSlice(buffer: ByteArray, offset: Int, length: Int) {
         this.rawData = buffer
         source.data = buffer
         this.position = offset
         this.limit = offset + length
         this.nextTokenByte = C.RESET_TOKEN_BYTE
         this.depth = 0
         this.needsCommaMask = 0L
         this.commaConsumedMask = 0L
         this.strictMode = false
         this.coerceStringsToNumbers = false
         this.coerceBooleans = false
         this.maxDepth = C.MAX_DEPTH
         this.maxCollectionSize = GhostHeuristics.maxCollectionSize
         this.lastScanContentWas7BitOnly = false
     }
 
     /**
      * Begins consumption of a JSON object '{'. Increments validation depth.
      */
     fun beginObject() {
         if (nextNonWhitespace() != C.OPEN_OBJ_INT) {
             throwError(C.ERR_EXPECTED_BEGIN_OBJ)
         }
         depth++
         if (depth > maxDepth) {
             throwError(C.ERR_DEPTH_EXCEEDED)
         }
         if (depth < C.MAX_BITMASK_DEPTH) {
             val bit = C.BITMASK_UNIT shl depth
             needsCommaMask = needsCommaMask and bit.inv()
             commaConsumedMask = commaConsumedMask and bit.inv()
         }
     }
 
     /**
      * Ends consumption of a JSON object '}'. Decrements validation depth.
      */
     fun endObject() {
         if (nextNonWhitespace() != C.CLOSE_OBJ_INT) {
             throwError(C.ERR_EXPECTED_END_OBJ)
         }
         if (depth > 0) {
             depth--
         }
     }
 
     /**
      * Begins consumption of a JSON array '['. Increments validation depth.
      */
     fun beginArray() {
         if (nextNonWhitespace() != C.OPEN_ARR_INT) {
             throwError(C.ERR_EXPECTED_BEGIN_ARR)
         }
         depth++
         if (depth > maxDepth) {
             throwError(C.ERR_DEPTH_EXCEEDED)
         }
         if (depth < C.MAX_BITMASK_DEPTH) {
             val bit = C.BITMASK_UNIT shl depth
             needsCommaMask = needsCommaMask and bit.inv()
             commaConsumedMask = commaConsumedMask and bit.inv()
         }
     }
 
     /**
      * Ends consumption of a JSON array ']'. Decrements validation depth.
      */
     fun endArray() {
         if (nextNonWhitespace() != C.CLOSE_ARR_INT) {
             throwError(C.ERR_EXPECTED_END_ARR)
         }
         if (depth > 0) {
             depth--
         }
     }
 
     /**
      * Checks if there are more elements in the current JSON container.
      */
     fun hasNext(): Boolean {
         val token = peekNextToken()
         if (
             token == C.CLOSE_ARR_INT ||
             token == C.CLOSE_OBJ_INT ||
             token == C.MATCH_END
         ) {
             return false
         }
         if (strictMode && depth < C.MAX_BITMASK_DEPTH) {
             val bit = C.BITMASK_UNIT shl depth
             if ((commaConsumedMask and bit) != C.RESULT_NONE) {
                 if (token == C.COMMA_INT) {
                     commaConsumedMask = commaConsumedMask and bit.inv()
                     needsCommaMask = needsCommaMask or bit
                 }
             }
             if ((commaConsumedMask and bit) != C.RESULT_NONE) {
                 commaConsumedMask = commaConsumedMask and bit.inv()
                 needsCommaMask = needsCommaMask or bit
             } else {
                 val required = (needsCommaMask and bit) != C.RESULT_NONE
                 if (token == C.COMMA_INT) {
                     if (!required) {
                         throwError(C.ERR_UNEXPECTED_COMMA)
                     }
                     internalSkip(1)
                     val next = peekNextToken()
                     if (next == C.CLOSE_ARR_INT || next == C.CLOSE_OBJ_INT) {
                         throwError(C.ERR_TRAILING_COMMA)
                     }
                     commaConsumedMask = commaConsumedMask or bit
                     needsCommaMask = needsCommaMask and bit.inv()
                 } else {
                     if (required) throwError(C.ERR_EXPECTED_COMMA)
                     needsCommaMask = needsCommaMask or bit
                 }
             }
         } else {
             if (token == C.COMMA_INT) {
                 internalSkip(1)
                 val next = peekNextToken()
                 if (next == C.CLOSE_ARR_INT || next == C.CLOSE_OBJ_INT) {
                     throwError(C.ERR_TRAILING_COMMA)
                 }
             }
         }
         return true
     }
 
     /**
      * Consumes any comma separator and returns the next object key string. Returns null if object ends.
      */
     fun nextKey(): String? {
         val token = peekNextToken()
         if (token == C.CLOSE_OBJ_INT) {
             return null
         }
         if (strictMode && depth < C.MAX_BITMASK_DEPTH) {
             val bit = C.BITMASK_UNIT shl depth
             // If comma was already consumed by consumeArraySeparator(), skip re-requiring it.
             if ((commaConsumedMask and bit) != C.RESULT_NONE) {
                 commaConsumedMask = commaConsumedMask and bit.inv()
                 needsCommaMask = needsCommaMask or bit
             } else {
                 val required = (needsCommaMask and bit) != C.RESULT_NONE
                 if (token == C.COMMA_INT) {
                     if (!required) {
                         throwError(C.ERR_UNEXPECTED_COMMA)
                     }
                     internalSkip(1)
                     if (peekNextToken() == C.CLOSE_OBJ_INT) {
                         throwError(C.ERR_TRAILING_COMMA)
                     }
                     needsCommaMask = needsCommaMask or bit
                 } else {
                     if (required) {
                         throwError(C.ERR_EXPECTED_COMMA_OR_CLOSE_OBJ)
                     }
                     needsCommaMask = needsCommaMask or bit
                 }
             }
         } else {
             if (token == C.COMMA_INT) {
                 internalSkip(1)
                 if (peekNextToken() == C.CLOSE_OBJ_INT) {
                     throwError(C.ERR_TRAILING_COMMA)
                 }
             }
         }
         return readQuotedString()
     }
 
     /**
      * Consumes the ':' key-value separator character.
      */
     fun consumeKeySeparator() {
         if (nextNonWhitespace() != C.COLON_INT) {
             throwError(C.ERR_EXPECTED_COLON)
         }
     }
 
     /**
      * Consumes the array element separating comma if present.
      */
     fun consumeArraySeparator() {
         if (strictMode && depth < C.MAX_BITMASK_DEPTH) {
             val bit = C.BITMASK_UNIT shl depth
             // If hasNext() already consumed the comma, honor that.
             if ((commaConsumedMask and bit) != C.RESULT_NONE) {
                 commaConsumedMask = commaConsumedMask and bit.inv()
                 needsCommaMask = needsCommaMask or bit
                 return
             }
             val token = peekNextToken()
             val required = (needsCommaMask and bit) != C.RESULT_NONE
             if (token == C.COMMA_INT) {
                 // Consume the comma and signal to the next nextKey()/selectNameAndConsume() that
                 // it was already consumed, so they don't re-require one.
                 internalSkip(1)
                 val next = peekNextToken()
                 if (next == C.CLOSE_ARR_INT || next == C.CLOSE_OBJ_INT) {
                     throwError(C.ERR_TRAILING_COMMA)
                 }
                 commaConsumedMask = commaConsumedMask or bit
             } else if (required) {
                 if (token != C.CLOSE_ARR_INT && token != C.CLOSE_OBJ_INT) {
                     throwError(C.ERR_EXPECTED_COMMA_OR_CLOSE_ARR)
                 }
             } else {
                 // First call at this depth: no prior comma needed, but if a non-separator token
                 // follows (neither comma nor closing bracket), the JSON is malformed.
                 if (token != C.CLOSE_ARR_INT && token != C.CLOSE_OBJ_INT) {
                     throwError(C.ERR_EXPECTED_COMMA_OR_CLOSE_ARR)
                 }
             }
             needsCommaMask = needsCommaMask or bit
         } else {
             val token = peekNextToken()
             if (token == C.COMMA_INT) {
                 internalSkip(1)
                 if (peekNextToken() == C.CLOSE_ARR_INT) {
                     throwError(C.ERR_TRAILING_COMMA)
                 }
             }
         }
     }
 
     /**
      * Parses and returns the next [Boolean] value.
      */
     fun nextBoolean(): Boolean {
         val token = peekNextToken()
         if (token == C.TRUE_CHAR_INT) {
             skipAndValidateLiteral(C.TRUE_BS)
             return true
         }
         if (token == C.FALSE_CHAR_INT) {
             skipAndValidateLiteral(C.FALSE_BS)
             return false
         }
         if (coerceBooleans) {
             if (token == C.ONE_INT) {
                 internalSkip(1)
                 return true
             }
             if (token == C.ZERO_INT) {
                 internalSkip(1)
                 return false
             }
             if (token == C.QUOTE_INT) {
                 // can the quoted string bytes directly. No String allocation.
                 return matchCoerceBooleanBytes()
             }
         }
         throwError(C.ERR_EXPECTED_BOOLEAN)
     }
 
     /**
      * Parses and returns the next string literal.
      */
     fun nextString(): String = readQuotedString()
 
     /**
      * Peeks whether the next JSON token is the null value token.
      */
     fun isNextNullValue(): Boolean = peekNextToken() == C.NULL_CHAR_INT
 
     /**
      * Consumes the null value literal from the stream.
      */
     fun consumeNull() {
         skipAndValidateLiteral(C.NULL_BS)
     }
 
     /**
      * Zero-copy boolean coercion matcher. Delegates byte comparison to
      * [matchCoerceBooleanBytes] in GhostParserUtils — single source of truth.
      */
     private fun matchCoerceBooleanBytes(): Boolean {
         val localData = rawData
         val lim = limit
         val contentStart = position + 1 // skip opening
         val end = findClosingQuoteImpl(contentStart, lim) {
             localData[it].toInt() and C.BYTE_MASK
         }
         if (end == -1) throwError(C.UNTERMINATED_STRING_ERROR)
         val length = end - contentStart
         position = end + 1
         nextTokenByte = C.RESET_TOKEN_BYTE
         return matchCoerceBooleanBytes(
             start = contentStart,
             length = length,
             onError = { throwError(C.ERR_EXPECTED_BOOLEAN) },
             getByte = { localData[it].toInt() and C.BYTE_MASK },
         )
     }
 
     /**
      * Selects name and consumes the key separator.
      */
     fun selectNameAndConsume(options: JsonReaderOptions): Int =
         internalSelect(options, consumeSeparator = true)
 
     /**
      * Selects matching string options.
      */
     fun selectString(options: JsonReaderOptions): Int =
         internalSelect(options, consumeSeparator = false)
 
     /**
      * Low-level helper to search and return matched index in options lookup table.
      */
     private fun internalSelect(options: JsonReaderOptions, consumeSeparator: Boolean): Int {
         var token = peekNextToken()
         if (token == C.CLOSE_OBJ_INT) {
             return -1
         }
 
         if (strictMode && depth < C.MAX_BITMASK_DEPTH) {
             val bit = C.BITMASK_UNIT shl depth
             if ((commaConsumedMask and bit) != C.RESULT_NONE) {
                 commaConsumedMask = commaConsumedMask and bit.inv()
                 needsCommaMask = needsCommaMask or bit
             } else {
                 val required = (needsCommaMask and bit) != C.RESULT_NONE
                 if (token == C.COMMA_INT) {
                     if (!required) {
                         throwError(C.ERR_UNEXPECTED_COMMA)
                     }
                     internalSkip(1)
                     token = peekNextToken()
                     if (token == C.CLOSE_OBJ_INT) {
                         throwError(C.ERR_TRAILING_COMMA)
                     }
                     commaConsumedMask = commaConsumedMask and bit.inv()
                     needsCommaMask = needsCommaMask or bit
                 } else {
                     if (required && consumeSeparator) {
                         throwError(C.ERR_EXPECTED_COMMA_OR_CLOSE_OBJ)
                     }
                     needsCommaMask = needsCommaMask or bit
                 }
             }
         } else {
             if (token == C.COMMA_INT) {
                 internalSkip(1)
                 token = peekNextToken()
                 if (token == C.CLOSE_OBJ_INT) {
                     throwError(C.ERR_TRAILING_COMMA)
                 }
             }
         }
 
         if (token != C.QUOTE_INT) {
             throwError(
                 if (consumeSeparator) {
                     C.ERR_EXPECTED_KEY
                 } else {
                     C.ERR_EXPECTED_STRING
                 }
             )
         }
         val start = position + 1
         val localData = rawData
         val end = findClosingQuoteImpl(start, limit) {
             localData[it].toInt() and C.BYTE_MASK
         }
 
         if (end == -1) {
             throwError(C.UNTERMINATED_STRING_ERROR)
         }
 
         val length = end - start
         val key = computeKeyHash(start, length, options.hasCollisions)
 
         val hasIndex = ((key * options.multiplier + length) shr options.shift) and (options.dispatch.size - 1)
         val index = options.dispatch[hasIndex]
 
         if (index != C.MATCH_END) {
             if (verifyKeyMatch(start, length, options.rawBytes[index], consumeSeparator)) {
                 return index
             }
         }
 
         // No match found
         val newPos = end + 1
         position = newPos
         nextTokenByte = C.MATCH_END
         if (consumeSeparator) {
             if (newPos < limit && getByte(newPos) == C.COLON_INT) {
                 position = newPos + 1
             } else {
                 consumeKeySeparator()
             }
         } else if (strictMode) {
             val unknownKey = source.decodeToString(start, end)
             throwError("${C.STRICT_MODE_UNKNOWN_FIELD}$unknownKey")
         }
 
         return C.MATCH_NONE
     }
 
     /**
      * Computes the 32-bit hash value of the string range.
      */
     private fun computeKeyHash(start: Int, length: Int, hasCollisions: Boolean): Int {
         var key = 0
         if (length >= 4) {
             val b0 = rawData[start].toInt() and C.BYTE_MASK
             val b1 = rawData[start + 1].toInt() and C.BYTE_MASK
             val b2 = rawData[start + 2].toInt() and C.BYTE_MASK
             val b3 = rawData[start + 3].toInt() and C.BYTE_MASK
             key = b0 or (b1 shl C.SHIFT_8) or (b2 shl C.SHIFT_16) or (b3 shl C.SHIFT_24)
             if (hasCollisions) {
                 var ci = C.UNICODE_HEX_LENGTH
                 while (ci < length) { key = key * C.COLLISION_HASH_MULTIPLIER + (rawData[start + ci].toInt() and C.BYTE_MASK); ci++ }
             }
         } else {
             if (length >= 1) {
                 key = key or (rawData[start].toInt() and C.BYTE_MASK)
             }
             if (length >= 2) {
                 key = key or ((rawData[start + 1].toInt() and C.BYTE_MASK) shl C.SHIFT_8)
             }
             if (length >= 3) {
                 key = key or ((rawData[start + 2].toInt() and C.BYTE_MASK) shl C.SHIFT_16)
             }
         }
         return key
     }
 
     /**
      * Performs a fast comparison of the parsed string against expected bytes to verify matches.
      */
     private fun verifyKeyMatch(
         start: Int,
         length: Int,
         expected: ByteArray,
         consumeSeparator: Boolean
     ): Boolean {
         // Length is already guaranteed equal by the dispatch table (same hash slot).
         if (expected.size == length) {
             val localData = rawData
             var i = 0
             // Unrolled x4 for typical ASCII field name lengths (4–20 chars).
             while (i + 3 < length) {
                 if (localData[start + i] != expected[i]) return false
                 if (localData[start + i + 1] != expected[i + 1]) return false
                 if (localData[start + i + 2] != expected[i + 2]) return false
                 if (localData[start + i + 3] != expected[i + 3]) return false
                 i += 4
             }
             while (i < length) {
                 if (localData[start + i] != expected[i]) return false
                 i++
             }
             val endPos = start + length
             val newPos = endPos + 1
             position = newPos
             nextTokenByte = -1
             if (consumeSeparator) {
                 if (newPos < limit) {
                     val colonToken = getByte(newPos)
                     if (colonToken == C.COLON_INT) {
                         position = newPos + 1
                     } else {
                         consumeKeySeparator()
                     }
                 } else {
                     consumeKeySeparator()
                 }
             }
             return true
         }
         return false
     }
 
 
     /**
      * Peeks at a key name and returns it if it is a string match.
      */
     fun peekStringField(name: String): String? {
         return peekDiscriminator(name)
     }
 
     /**
      * Skips the next complete value token (object, array, string, number, boolean, null) from the stream.
      */
     fun skipValue() {
         val token = peekNextToken()
         when (token) {
             C.OPEN_OBJ_INT -> {
                 beginObject()
                 while (hasNext()) {
                     if (peekNextToken() != C.QUOTE_INT) {
                         throwError(C.ERR_EXPECTED_KEY)
                     }
                     skipQuotedString()
                     consumeKeySeparator()
                     skipValue()
                 }
                 endObject()
             }
 
             C.OPEN_ARR_INT -> {
                 beginArray()
                 while (hasNext()) {
                     skipValue()
                 }
                 endArray()
             }
 
             C.QUOTE_INT -> {
                 skipQuotedString()
             }
             C.TRUE_CHAR_INT -> {
                 skipAndValidateLiteral(C.TRUE_BS)
             }
             C.FALSE_CHAR_INT -> {
                 skipAndValidateLiteral(C.FALSE_BS)
             }
             C.NULL_CHAR_INT -> {
                 skipAndValidateLiteral(C.NULL_BS)
             }
 
             else -> {
                 skipNumber()
             }
         }
     }
 
     /**
      * Reads a list of items using the provided [itemParser].
      */
     inline fun <T> readList(crossinline itemParser: () -> T): List<T> {
         beginArray()
         if (peekNextToken() == C.CLOSE_ARR_INT) {
             endArray()
             return emptyList()
         }
         val list = ArrayList<T>(initialCollectionCapacity)
         val maxSize = maxCollectionSize
 
         while (true) {
             list.add(itemParser())
             val next = nextNonWhitespace()
             if (next == C.CLOSE_ARR_INT) {
                 if (depth > 0) {
                     depth--
                 }
                 break
             }
             if (next != C.COMMA_INT) {
                 throwError("${C.ERR_EXPECTED_COMMA_OR_CLOSE_ARR} but found $next")
             }
             if (list.size > maxSize) {
                 throwError("${C.ERR_MAX_COLLECTION_SIZE} ($maxSize)")
             }
         }
         return list
     }
 
     /**
      * Reads a set of items using the provided [itemParser].
      * Builds a [HashSet] directly — no intermediate [List] allocation.
      */
     inline fun <T> readSet(crossinline itemParser: () -> T): Set<T> {
         beginArray()
         if (peekNextToken() == C.CLOSE_ARR_INT) {
             endArray()
             return emptySet()
         }
         val set = HashSet<T>(initialCollectionCapacity)
         val maxSize = maxCollectionSize
 
         while (true) {
             set.add(itemParser())
             val next = nextNonWhitespace()
             if (next == C.CLOSE_ARR_INT) {
                 if (depth > 0) {
                     depth--
                 }
                 break
             }
             if (next != C.COMMA_INT) {
                 throwError("${C.ERR_EXPECTED_COMMA_OR_CLOSE_ARR} but found $next")
             }
             if (set.size > maxSize) {
                 throwError("${C.ERR_MAX_COLLECTION_SIZE} ($maxSize)")
             }
         }
         return set
     }
 
     /**
      * Reads a map of keys and values using the provided [keyParser] and [valueParser].
      */
     inline fun <K, V> readMap(
         crossinline keyParser: () -> K,
         crossinline valueParser: () -> V
     ): Map<K, V> {
         beginObject()
         if (peekNextToken() == C.CLOSE_OBJ_INT) {
             endObject()
             return emptyMap()
         }
 
         val map = HashMap<K, V>(initialCollectionCapacity)
         val maxSize = maxCollectionSize
 
         while (true) {
             val key = keyParser()
             consumeKeySeparator()
             val value = valueParser()
             map[key] = value
 
             val next = nextNonWhitespace()
             if (next == C.CLOSE_OBJ_INT) {
                 if (depth > 0) {
                     depth--
                 }
                 break
             }
             if (next != C.COMMA_INT) {
                 throwError("${C.ERR_EXPECTED_COMMA_OR_CLOSE_OBJ} but found $next")
             }
             // The comma was consumed directly via nextNonWhitespace(); clear needsCommaMask so
             // the next keyParser() (nextKey()) doesn't re-require another comma.
             if (depth < C.MAX_BITMASK_DEPTH) {
                 val bit = C.BITMASK_UNIT shl depth
                 needsCommaMask = needsCommaMask and bit.inv()
             }
             if (map.size > maxSize) {
                 throwError("${C.ERR_MAX_COLLECTION_SIZE} ($maxSize)")
             }
         }
         return map
     }
 
     /**
      * Resiliently decodes a value. If an error occurs, skips the value and returns null.
      */
     @InternalGhostApi
     inline fun <T> decodeResilient(crossinline block: () -> T): T? {
         val savedPos = position
         val savedToken = nextTokenByte
         val savedDepth = depth
         val savedNeedsCommaMask = needsCommaMask
         val savedCommaConsumedMask = commaConsumedMask
         try {
             return block()
         } catch (_: GhostJsonException) {
             position = savedPos
             nextTokenByte = savedToken
             depth = savedDepth
             needsCommaMask = savedNeedsCommaMask
             commaConsumedMask = savedCommaConsumedMask
             skipValue()
             return null
         }
     }
 
     companion object {
         @InternalGhostApi
         const val RESET_TOKEN_BYTE: Int = -1
     }
 }