-
Notifications
You must be signed in to change notification settings - Fork 573
fix(tree): track chunkIndex on field entry so getField finds parent #27084
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,10 +166,19 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
| if (this.nestedCursor !== undefined) { | ||
| return this.nestedCursor.mode; | ||
| } | ||
| // Compute the number of nodes deep the current depth is. | ||
| // We want the floor of the result, which can computed using a bitwise shift assuming the depth is less than 2^31, which seems safe. | ||
| // eslint-disable-next-line no-bitwise | ||
| const halfHeight = (this.siblingStack.length + 1) >> 1; | ||
| this.assertChunkStacksMatchNodeDepth(); | ||
| return this.siblingStack.length % 2 === 0 | ||
| ? CursorLocationType.Fields | ||
| : CursorLocationType.Nodes; | ||
| } | ||
|
|
||
| /** | ||
| * Asserts that the node-only stacks (`indexOfChunkStack` and `indexWithinChunkStack`) are in sync with `siblingStack`. | ||
| * Since `siblingStack` interleaves field and node levels while the node-only stacks are pushed/popped only on node-level transitions, | ||
| * their length should always equal the number of node levels traversed. | ||
| */ | ||
| private assertChunkStacksMatchNodeDepth(): void { | ||
| const halfHeight = this.getNodeOnlyHeightFromHeight(); | ||
| assert( | ||
| this.indexOfChunkStack.length === halfHeight, | ||
| 0x51c /* unexpected indexOfChunkStack */, | ||
|
|
@@ -178,9 +187,6 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
| this.indexWithinChunkStack.length === halfHeight, | ||
| 0x51d /* unexpected indexWithinChunkStack */, | ||
| ); | ||
| return this.siblingStack.length % 2 === 0 | ||
| ? CursorLocationType.Fields | ||
| : CursorLocationType.Nodes; | ||
| } | ||
|
|
||
| public getFieldKey(): FieldKey { | ||
|
|
@@ -203,11 +209,32 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
| return this.indexStack[height] ?? oob(); | ||
| } | ||
|
|
||
| private getStackedNode(height: number): BasicChunk { | ||
| const index = this.getStackedNodeIndex(height); | ||
| private getStackedChunkIndex(height: number): number { | ||
| assert(height % 2 === 1, "must be node height"); | ||
| assert(height >= 0, "must not be above root"); | ||
| return this.indexOfChunkStack[this.getNodeOnlyHeightFromHeight(height)] ?? oob(); | ||
| } | ||
|
|
||
| private getStackedChunk(height: number): BasicChunk { | ||
| const index = this.getStackedChunkIndex(height); | ||
| return (this.siblingStack[height] as readonly TreeChunk[])[index] as BasicChunk; | ||
| } | ||
|
|
||
| /** | ||
| * Converts a {@link height}, which contains field and node levels, into the corresponding depth/index | ||
| * for the node-only stacks ({@link indexOfChunkStack} and {@link indexWithinChunkStack}), which are | ||
| * only pushed on node-level transitions. | ||
| * | ||
| * @param height - A depth in {@link siblingStack} to convert. Defaults to {@link siblingStack}'s | ||
| * current length, which gives the current depth of the node-only stacks. | ||
| * @returns `floor(height / 2)` — the number of node levels at or below the given stack height. | ||
| */ | ||
| private getNodeOnlyHeightFromHeight(height: number = this.siblingStack.length): number { | ||
| // The bitwise shift computes the floor, which is valid assuming the depth is less than 2^31, which seems safe. | ||
| // eslint-disable-next-line no-bitwise | ||
| return height >> 1; | ||
| } | ||
|
|
||
| public getFieldLength(): number { | ||
| if (this.nestedCursor !== undefined) { | ||
| return this.nestedCursor.getFieldLength(); | ||
|
|
@@ -322,6 +349,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
| assert(this.mode === CursorLocationType.Nodes, 0x528 /* must be in nodes mode */); | ||
| this.siblingStack.push(this.siblings); | ||
| this.indexStack.push(this.index); | ||
| // Save the chunk array position of the current node. When siblings contain | ||
| // multi node chunks, the flat node index diverges from the array position, | ||
| // so getField needs this to locate the parent in the sibling array. | ||
| this.indexOfChunkStack.push(this.indexOfChunk); | ||
| this.indexWithinChunkStack.push(this.indexWithinChunk); | ||
|
|
||
| // For fields, siblings are only used for key lookup and | ||
| // nextField and which has arbitrary iteration order, | ||
|
|
@@ -330,6 +362,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
| // at the cost of an allocation here. | ||
| this.index = 0; | ||
| this.siblings = [key]; | ||
| this.assertChunkStacksMatchNodeDepth(); | ||
| } | ||
|
|
||
| public nextField(): boolean { | ||
|
|
@@ -355,8 +388,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
|
|
||
| this.siblingStack.push(this.siblings); | ||
| this.indexStack.push(this.index); | ||
| this.indexOfChunkStack.push(this.indexOfChunk); | ||
| this.indexWithinChunkStack.push(this.indexWithinChunk); | ||
|
brrichards marked this conversation as resolved.
|
||
| this.index = 0; | ||
| this.siblings = [...fields.keys()]; // TODO: avoid this copy | ||
| this.assertChunkStacksMatchNodeDepth(); | ||
| return true; | ||
| } | ||
|
|
||
|
|
@@ -422,12 +458,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
| } | ||
| this.siblingStack.push(this.siblings); | ||
| this.indexStack.push(this.index); | ||
| this.indexOfChunkStack.push(this.indexOfChunk); | ||
| this.indexWithinChunkStack.push(this.indexWithinChunk); | ||
| this.index = 0; | ||
| this.siblings = siblings; | ||
| this.indexOfChunk = 0; | ||
| this.indexWithinChunk = 0; | ||
| this.assertChunkStacksMatchNodeDepth(); | ||
| this.initNestedCursor(); | ||
| return true; | ||
| } | ||
|
|
@@ -486,6 +521,11 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
| this.siblings = | ||
| this.siblingStack.pop() ?? fail(0xaf0 /* Unexpected siblingStack.length */); | ||
| this.index = this.indexStack.pop() ?? fail(0xaf1 /* Unexpected indexStack.length */); | ||
| this.indexOfChunk = | ||
| this.indexOfChunkStack.pop() ?? fail("Unexpected indexOfChunkStack.length"); | ||
| this.indexWithinChunk = | ||
| this.indexWithinChunkStack.pop() ?? fail("Unexpected indexWithinChunkStack.length"); | ||
|
CraigMacomber marked this conversation as resolved.
|
||
| this.assertChunkStacksMatchNodeDepth(); | ||
| } | ||
|
|
||
| public exitNode(): void { | ||
|
|
@@ -502,18 +542,30 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
| this.siblings = | ||
| this.siblingStack.pop() ?? fail(0xaf2 /* Unexpected siblingStack.length */); | ||
| this.index = this.indexStack.pop() ?? fail(0xaf3 /* Unexpected indexStack.length */); | ||
| this.indexOfChunk = | ||
| this.indexOfChunkStack.pop() ?? fail(0xaf4 /* Unexpected indexOfChunkStack.length */); | ||
| this.indexWithinChunk = | ||
| this.indexWithinChunkStack.pop() ?? | ||
| fail(0xaf5 /* Unexpected indexWithinChunkStack.length */); | ||
| // At the Fields level these aren't semantically used, but reset for consistent state | ||
| // (so a fully-iterated cursor matches a fresh cursor at the same logical position). | ||
| this.indexOfChunk = 0; | ||
| this.indexWithinChunk = 0; | ||
| this.assertChunkStacksMatchNodeDepth(); | ||
| } | ||
|
|
||
| private getNode(): BasicChunk { | ||
| assert(this.mode === CursorLocationType.Nodes, 0x52f /* can only get node when in node */); | ||
| return (this.siblings as TreeChunk[])[this.index] as BasicChunk; | ||
| return (this.siblings as TreeChunk[])[this.indexOfChunk] as BasicChunk; | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the chunks that make up the field the cursor is currently in. At the root, this is | ||
| * {@link root} directly. Otherwise, the cursor must be in {@link CursorLocationType.Fields} mode, | ||
| * and the result is looked up on the parent node using the current field key. | ||
| * | ||
| * @remarks The parent node is the {@link BasicChunk} in the node array at the top of | ||
| * {@link siblingStack} while we are in {@link CursorLocationType.Fields} mode. We need the parent | ||
| * since a field's chunks are stored on the parent node's {@link BasicChunk.fields} map, not on | ||
| * the cursor itself. | ||
| * | ||
| * @returns The chunks that make up the field the cursor is currently in. | ||
| */ | ||
| private getField(): readonly TreeChunk[] { | ||
|
brrichards marked this conversation as resolved.
|
||
| if (this.siblingStack.length === 0) { | ||
| return this.root; | ||
|
|
@@ -522,7 +574,7 @@ export class BasicChunkCursor extends SynchronousCursor implements ChunkedCursor | |
| this.mode === CursorLocationType.Fields, | ||
| 0x530 /* can only get field when in fields */, | ||
| ); | ||
| const parent = this.getStackedNode(this.indexStack.length - 1); | ||
| const parent = this.getStackedChunk(this.siblingStack.length - 1); | ||
|
Comment on lines
562
to
+577
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Details about the parent should be a code comment inside the method, since its not relevant to callers. Also, now that I see this explanation (thanks for that!), I think this code could be simplified. this.siblingStack should, at the correct level, contain the TreeChunk array we want, so we can skip having to get the parent node in the first place. |
||
| const key: FieldKey = this.getFieldKey(); | ||
| const field = parent.fields.get(key) ?? []; | ||
| return field; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should always be true, but for clarity/safety, can we:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that as this are is rather performance sensitive, and very unlikely to fail due to some specific runtime data, I opted for debugAssert which can be compiled out of production builds. We will likely want to do the same for some other places in here.