Skip to content

fix: ByteRenderer bitmask overflow at nesting depth >= 64 - #1024

Merged
stephenamar-db merged 2 commits into
databricks:masterfrom
He-Pin:fix/byterenderer-deep-nesting
Jun 24, 2026
Merged

stephenamar-db merged 2 commits into
databricks:masterfrom
He-Pin:fix/byterenderer-deep-nesting

Conversation

@He-Pin

@He-Pin He-Pin commented Jun 24, 2026 •

Copy link
Copy Markdown
Contributor

fix: ByteRenderer handles nesting depth beyond 64 levels

Motivation

ByteRenderer.scala used a 64-bit Long bitmask (emptyBits) to track which nesting levels had empty vs non-empty containers. At depth >= 65, 1L << depth wraps around due to Java's shift masking (shifts modulo 64 for Long), causing bit collisions. This made deeply nested JSON output render incorrectly — closing braces collapsed onto a single line (} } } }) instead of properly indented.

Modification

  • ByteRenderer.scala: Replaced Long bitmask with a growable Array[Long] bitmask. Each Long holds 64 bits; the array grows on demand via ensureCapacity(). Uses only primitive operations (bitwise AND/OR, array access, System.arraycopy) that work on all platforms (JVM, Scala.js, Scala Native, WASM).
  • Added RendererTests regression tests (depth 100 and 200)
  • Added golden test: byterenderer_deep_nesting.jsonnet

Result

Depth cpp-jsonnet sjsonnet (before) sjsonnet (after)
<= 64 ✅ correct ✅ correct ✅ correct
70 each brace on own line trailing } } } } ❌ each brace on own line ✅
150 each brace on own line many braces merged ❌ each brace on own line ✅
200 each brace on own line severe corruption ❌ each brace on own line ✅

He-Pin added 2 commits June 24, 2026 18:25
ByteRenderer used a 64-bit Long bitmask (`emptyBits`) to track which
nesting levels contain empty vs non-empty containers. At depth >= 65,
`1L << depth` wraps around (Java/Scala shifts Long modulo 64), causing
bit collisions with shallower levels. This made `resetEmpty()` at depth
65 clear the same bit used by depth 1, making the depth-1 container
appear empty when closing — collapsing its brace onto the same line as
deeper braces.

Symptom: deeply nested JSON (depth >= 65) rendered with multiple closing
braces on one line (e.g., `} } } } } } }`) instead of properly indented.

Fix: replace the Long bitmask with `java.util.BitSet`, which auto-grows
to support arbitrary depth. The API is a drop-in replacement:
  - `emptyBits |= (1L << depth)` → `emptyBits.set(depth)`
  - `(emptyBits & (1L << depth)) == 0L` → `!emptyBits.get(depth)`
  - `emptyBits &= ~(1L << depth)` → `emptyBits.clear(depth)`

Performance impact: negligible for typical (shallow) JSON. BitSet uses
a single internal `long` for depths < 64, so the hot path is unchanged.

Cross-implementation verification (70-level nested object):

| Depth | C++ jsonnet | sjsonnet (before) | sjsonnet (after) |
|-------|-------------|-------------------|------------------|
| 1-64  | correct     | correct           | correct          |
| 65-70 | correct     | `} } } } } } }`   | correct          |

Includes regression tests:
- RendererTests: direct ByteRenderer tests at depth 100 and 200
- Golden file: byterenderer_deep_nesting.jsonnet (depth 100 + 200)

Note: `std.manifestJson`/`std.manifestJsonEx` were NOT affected — they
use FastMaterializeJsonRenderer which doesn't use bitmask tracking.
Motivation:
java.util.BitSet() no-arg constructor is not available on Scala.js
and Scala Native, causing compilation failures on those platforms.

Modification:
Replace java.util.BitSet with a growable Array[Long] bitmask.
Each Long holds 64 bits; the array grows on demand via
ensureCapacity(). Uses only primitive operations (bitwise AND/OR,
array access, System.arraycopy) that work on all platforms.
Add golden test file for deep nesting regression.

Result:
ByteRenderer deep nesting fix now compiles and works on JVM,
Scala.js, Scala Native, and WASM.
@He-Pin
He-Pin force-pushed the fix/byterenderer-deep-nesting branch from 33fd6bc to d3443ae Compare June 24, 2026 11:00
@stephenamar-db
stephenamar-db merged commit 27f0187 into databricks:master Jun 24, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants