fix(List): preserve undefined values when grown past 32 elements by jdeniau · Pull Request #2235 · immutable-js/immutable-js · GitHub
Skip to content

fix(List): preserve undefined values when grown past 32 elements - #2235

Merged
jdeniau merged 1 commit into
mainfrom
fix/avoid-null-when-setsize33
Jun 29, 2026
Merged

jdeniau merged 1 commit into
mainfrom
fix/avoid-null-when-setsize33

Conversation

@jdeniau

@jdeniau jdeniau commented Jun 29, 2026

Copy link
Copy Markdown
Member

Bug

A List grown past 32 elements while every value is undefined reads those values back as null, corrupting get, iteration, toArray, equals and hashCode.

List().setSize(32).setSize(33).get(0); // => null   (expected: undefined)

This affects any path that produces an all-undefined region spanning the 32-element (tail → root) boundary — setSize, push(undefined), mutations, and the unshift/delete/shift sequence from #2230.

Root cause

A List represents an absent trie node as undefined almost everywhere (clear(), emptyList(), and the trie's own array holes via removeBefore). But _root was stored as null in two spots:

  • the small-List constructor: makeList(0, size, SHIFT, null, …)
  • the "origin within tail" branch of setListBounds: newRoot = null

For small lists this is invisible (indices route to the tail, never the root). But once such a List grows past the next 32-element boundary, in-range indices route through _root, and the read idiom node && node.array[i] returns the falsy node itself — null && x === null leaks null, whereas undefined && x === undefined reads correctly.

Fix

Normalize both sites to undefined so an absent root has a single sentinel, consistent with the rest of the file. No reader change is needed: node && node.array[i] is correct once _root is VNode | undefined. JSDoc is added on VNode / makeList / listNodeFor to capture that shape ahead of the TypeScript migration.

Notes

This supersedes #2230: that change guards the "Merge Tail into tree" step, but its condition starts with oldTail && …, so it does not cover the setSize(≤32).setSize(≥33) family (where oldTail is itself null) — setSize(32).setSize(33) still returns null with it applied.

Tests

8 regression cases in __tests__/List.ts (minimal repro + variants, the #2230 sequence, and a null-round-trips guard against over-correction). Full suite green (826 tests, incl. fast-check property tests).

The trie root was stored as `null` in two places (small-List constructor
and the "origin within tail" branch of setListBounds), so a List grown
past 32 elements while all values are `undefined` read them back as
`null`. Normalize both to `undefined`, matching the rest of the file.
@jdeniau
jdeniau merged commit 009164f into main Jun 29, 2026
9 checks passed
@jdeniau
jdeniau deleted the fix/avoid-null-when-setsize33 branch June 29, 2026 21:46
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