/ lib / path.js
path.js
  1  import { WeakStackFrame } from '@bablr/weak-stack';
  2  import * as btree from '@bablr/agast-helpers/btree';
  3  import {
  4    ReferenceTag,
  5    ArrayInitializerTag,
  6    EmbeddedNode,
  7    DoctypeTag,
  8    OpenNodeTag,
  9    CloseNodeTag,
 10    GapTag,
 11    NullTag,
 12    ShiftTag,
 13  } from './symbols.js';
 14  import {
 15    buildArrayInitializerTag,
 16    buildEmbeddedNode,
 17    buildGapTag,
 18    buildReferenceTag,
 19    buildShiftTag,
 20  } from './builders.js';
 21  
 22  export const getOpenTag = (node) => {
 23    let tag = btree.getAt(0, node.children);
 24    if (tag.type === NullTag || tag.type === GapTag) return null;
 25    if (tag.type === DoctypeTag) {
 26      tag = btree.getAt(1, node.children);
 27    }
 28    if (tag && tag.type !== OpenNodeTag) throw new Error();
 29    return tag;
 30  };
 31  
 32  export const getCloseTag = (node) => {
 33    const { children } = node;
 34    const tag = btree.getAt(-1, children);
 35    if (tag.type !== CloseNodeTag) return null;
 36    return tag;
 37  };
 38  
 39  export const isNullNode = (node) => {
 40    return node && node.type === null && btree.getAt(0, node.children).type === NullTag;
 41  };
 42  
 43  export const isFragmentNode = (node) => {
 44    return node && node.type === null && getOpenTag(node)?.value.type === null;
 45  };
 46  
 47  export const isGapNode = (node) => {
 48    return node && node.type === null && btree.getAt(0, node.children).type === GapTag;
 49  };
 50  
 51  const { hasOwn } = Object;
 52  const { isArray } = Array;
 53  
 54  export const referencesAreEqual = (a, b) => {
 55    return (
 56      a === b ||
 57      (a.value.name === b.value.name &&
 58        a.value.isArray === b.value.isArray &&
 59        a.value.flags.hasGap === b.value.flags.hasGap &&
 60        a.value.flags.expression === b.value.flags.expression)
 61    );
 62  };
 63  
 64  export const getProperties = (ref, properties) => {
 65    const { name, index, isArray } = ref.value;
 66  
 67    if (name === '.') {
 68      if (!hasOwn(properties, name)) {
 69        return null;
 70      }
 71    }
 72  
 73    if (isArray) {
 74      return btree.getAt(index ?? -1, properties[name]);
 75    } else {
 76      return properties[name];
 77    }
 78  };
 79  
 80  export const getPropertiesSimple = (ref, properties) => {
 81    const { name, index, isArray } = ref.value;
 82  
 83    if (!hasOwn(properties, name)) {
 84      return null;
 85    }
 86  
 87    if (isArray) {
 88      return properties[name][index == null ? properties[name].length - 1 : index];
 89    } else {
 90      return properties[name];
 91    }
 92  };
 93  
 94  export const get = (ref, node) => {
 95    const { flags } = ref.value;
 96    const result = getProperties(ref, node.properties);
 97  
 98    return flags.expression ? btree.getAt(-1, result.node) : result?.node;
 99  };
100  
101  export const getShifted = (shiftIndex, ref, node) => {
102    const { flags } = ref.value;
103  
104    const result = getProperties(ref, node.properties);
105    return flags.expression ? btree.getAt(shiftIndex ?? -1, result.node) : result?.node;
106  };
107  
108  export const add = (node, reference, value, shift = null) => {
109    if (node == null || reference == null || value == null) throw new Error();
110  
111    const { properties } = node;
112    const { name, isArray, flags } = reference.value;
113  
114    if (node.type && name === '.') {
115      throw new Error('Only fragments can have . properties');
116    }
117  
118    if (name == null) throw new Error();
119  
120    const lastChild = btree.getAt(-1, node.children);
121  
122    if (lastChild.type === ReferenceTag) {
123      if (!referencesAreEqual(lastChild, reference)) throw new Error();
124    } else if (lastChild.type !== ShiftTag) {
125      node.children = btree.push(node.children, shift == null ? reference : buildShiftTag(shift));
126    }
127  
128    if (name === '#' || name === '@') {
129      node.children = btree.push(node.children, buildEmbeddedNode(value));
130    } else {
131      if (isArray) {
132        let isInitializer = Array.isArray(value);
133        let exists = !isInitializer && hasOwn(properties, name);
134  
135        let existed = exists;
136        if (!existed) {
137          if (isInitializer && value.length)
138            throw new Error('Array value only allowed for initialization');
139  
140          properties[name] = [];
141          node.children = btree.push(node.children, buildArrayInitializerTag(value));
142          exists = !isInitializer;
143        }
144  
145        if (exists) {
146          if (!existed) {
147            if (btree.getAt(-1, node.children).type === ReferenceTag) throw new Error();
148            node.children = btree.push(node.children, reference);
149          }
150  
151          let newBinding;
152          if (flags.expression) {
153            let shiftedNodes = shift != null ? btree.getAt(-1, properties[name])?.node : [];
154  
155            newBinding = {
156              reference,
157              node: btree.push(shiftedNodes, value),
158            };
159          } else {
160            newBinding = { reference, node: value };
161          }
162  
163          properties[name] =
164            shift != null
165              ? btree.replaceAt(-1, properties[name], newBinding)
166              : btree.push(properties[name], newBinding);
167  
168          node.children = btree.push(node.children, buildGapTag(value));
169        }
170      } else {
171        if (hasOwn(properties, name)) {
172          throw new Error();
173        }
174  
175        if (flags.expression) {
176          let shiftedNodes = shift ? properties[name]?.node : [];
177          properties[name] = { reference, node: btree.push(shiftedNodes, value) };
178        } else {
179          properties[name] = { reference, node: value };
180        }
181        node.children = btree.push(node.children, buildGapTag(value));
182      }
183    }
184  };
185  
186  export function* allTagPathsFor(range) {
187    if (range == null) return;
188  
189    let startPath = range[0];
190    let endPath = range[1];
191    let path = startPath;
192  
193    while (path) {
194      if (path.inner) {
195        path = new TagPath(path.innerPath, 0);
196      }
197  
198      yield path;
199  
200      if (
201        endPath &&
202        path.childrenIndex === endPath.childrenIndex &&
203        path.path.node === endPath.path.node
204      ) {
205        return;
206      }
207  
208      path = path.next;
209    }
210  }
211  
212  export function* allTagsFor(range) {
213    for (const path of allTagPathsFor(range)) {
214      yield path.tag;
215    }
216  }
217  
218  export function* ownTagPathsFor(range) {
219    if (!isArray(range)) throw new Error();
220  
221    const startPath = range[0];
222    const endPath = range[1];
223  
224    let path = startPath;
225  
226    if (startPath.outer !== endPath.outer) throw new Error();
227  
228    const { children } = startPath.outer;
229  
230    for (let i = startPath.childrenIndex; i < endPath.childrenIndex; i++) {
231      yield children[i];
232    }
233  }
234  
235  export class PathResolver {
236    constructor() {
237      this.childrenIndex = -1;
238      this.counters = {};
239      this.reference = null;
240    }
241  
242    advance(tag) {
243      this.childrenIndex++;
244  
245      const { counters } = this;
246      if (tag.type === ReferenceTag) {
247        const { isArray, name, flags } = tag.value;
248  
249        let resolvedReference = tag;
250  
251        this.reference = tag;
252  
253        if (isArray) {
254          if (hasOwn(counters, name)) {
255            const counter = ++counters[name];
256  
257            resolvedReference = buildReferenceTag(name, isArray, flags, counter);
258          }
259        } else if (name !== '@' && name !== '#') {
260          if (hasOwn(counters, name)) throw new Error();
261  
262          counters[name] = true;
263        }
264  
265        return resolvedReference;
266      } else if (tag.type === ArrayInitializerTag) {
267        counters[this.reference.value.name] = -1;
268        return this.reference.value.name;
269      }
270    }
271  }
272  
273  Object.freeze(PathResolver.prototype);
274  
275  const findRight = (arr, predicate) => {
276    for (let i = arr.length - 1; i >= 0; i--) {
277      const value = arr[i];
278      if (predicate(value)) return value;
279    }
280    return null;
281  };
282  
283  const skipLevels = 3;
284  const skipShiftExponentGrowth = 4;
285  const skipAmounts = new Array(skipLevels)
286    .fill(null)
287    .map((_, i) => 2 >> (i * skipShiftExponentGrowth));
288  const skipsByFrame = new WeakMap();
289  
290  const buildSkips = (frame) => {
291    let skipIdx = 0;
292    let skipAmount = skipAmounts[skipIdx];
293    let skips;
294    while ((frame.depth & skipAmount) === skipAmount) {
295      if (!skips) {
296        skips = [];
297        skipsByFrame.set(frame, skips);
298      }
299  
300      skips[skipIdx] = frame.at(frame.depth - skipAmount);
301  
302      skipIdx++;
303      skipAmount = skipAmounts[skipIdx];
304    }
305  };
306  
307  const skipToDepth = (depth, frame) => {
308    let parent = frame;
309  
310    if (depth > frame.depth) throw new Error();
311  
312    let d = frame.depth;
313    for (; d > depth; ) {
314      const skips = skipsByFrame.get(frame);
315      parent = (skips && findRight(skips, (skip) => d - skip > depth)) || parent.parent;
316      d = parent.depth;
317    }
318    return parent;
319  };
320  
321  const buildBindings = (node) => {
322    const { children, properties } = node;
323    const referenceIndexes = new Array(children.length);
324    const childrenIndexes = Object.fromEntries(Object.keys(properties).map((key) => [key, null]));
325  
326    const resolver = new PathResolver();
327  
328    for (const tag of btree.traverse(children)) {
329      resolver.advance(tag);
330      const i = resolver.childrenIndex;
331  
332      if (tag.type === ReferenceTag) {
333        const { name, isArray, index } = tag.value;
334  
335        if (!name) throw new Error();
336        // if (name === '.') throw new Error();
337  
338        const counter = isArray
339          ? hasOwn(resolver.counters, name)
340            ? resolver.counters[name]
341            : null
342          : null;
343  
344        if (index != null && index !== counter) throw new Error();
345  
346        referenceIndexes[i] = counter;
347  
348        if (isArray) {
349          if (childrenIndexes[name] === null || !hasOwn(childrenIndexes, name)) {
350            childrenIndexes[name] = [];
351          } else if (counter >= 0) {
352            childrenIndexes[name][counter] = i;
353          } else {
354            throw new Error();
355          }
356        } else {
357          if (name !== '#' && name !== '@') {
358            childrenIndexes[name] = i;
359          }
360        }
361      } else {
362        referenceIndexes[i] = null;
363      }
364    }
365  
366    return { referenceIndexes, childrenIndexes };
367  };
368  
369  const nodeStates = new WeakMap();
370  
371  // TODO remove this; it is a very bad API to have to support!!
372  export const updatePath = (path, tag) => {
373    const { node, childrenIndexes, referenceIndexes } = path;
374    const i = btree.getSum(node.children) - 1;
375  
376    if (tag.type === ReferenceTag) {
377      const { name, isArray, index: literalArrayIndex } = tag.value;
378  
379      const arrayIndex = isArray
380        ? hasOwn(node.properties, name)
381          ? btree.getSum(node.properties[name])
382          : -1
383        : null;
384  
385      if (literalArrayIndex != null && literalArrayIndex !== arrayIndex) throw new Error();
386  
387      referenceIndexes[i] = arrayIndex;
388  
389      if (isArray) {
390        if (!hasOwn(childrenIndexes, name) || childrenIndexes[name] === null) {
391          childrenIndexes[name] = [];
392        } else {
393          if (arrayIndex >= 0) {
394            childrenIndexes[name][arrayIndex] = i;
395          }
396        }
397      } else {
398        if (name !== '#' && name !== '@') {
399          childrenIndexes[name] = i;
400        }
401      }
402    } else if (tag.type === ArrayInitializerTag) {
403      referenceIndexes[i] = -1;
404    } else {
405      referenceIndexes[i] = null;
406    }
407  };
408  
409  export const Path = class AgastPath extends WeakStackFrame {
410    static from(node) {
411      return this.create(node);
412    }
413  
414    constructor(parent, node, referenceIndex = null) {
415      super(parent);
416  
417      if (!(hasOwn(node, 'type') && hasOwn(node, 'language'))) throw new Error();
418  
419      if (parent && referenceIndex == null) throw new Error();
420      if (!node) throw new Error();
421      if (isArray(node)) throw new Error();
422  
423      this.node = node;
424      this.referenceIndex = referenceIndex; // in the parent
425  
426      if (
427        referenceIndex != null &&
428        ![ReferenceTag, ShiftTag].includes(btree.getAt(referenceIndex, parent.node.children).type)
429      )
430        throw new Error();
431  
432      nodeStates.set(node, buildBindings(node));
433  
434      if (parent && (!this.reference || ![ReferenceTag, ShiftTag].includes(this.reference.type))) {
435        throw new Error();
436      }
437  
438      if (!Number.isFinite(this.depth)) throw new Error();
439  
440      buildSkips(this);
441    }
442  
443    get referenceIndexes() {
444      return nodeStates.get(this.node).referenceIndexes;
445    }
446  
447    get childrenIndexes() {
448      return nodeStates.get(this.node).childrenIndexes;
449    }
450  
451    get reference() {
452      return this.outer && btree.getAt(this.referenceIndex, this.outer.children);
453    }
454  
455    get referencePath() {
456      return this.outer && new TagPath(this.parent, this.referenceIndex);
457    }
458  
459    get gap() {
460      return this.outer && btree.getAt(this.referenceIndex + 1, this.outer.children);
461    }
462  
463    get gapPath() {
464      return this.outer && new TagPath(this.parent, this.referenceIndex + 1);
465    }
466  
467    get outer() {
468      return this.parent?.node;
469    }
470  
471    get(reference, shiftIndex) {
472      let node = getShifted(shiftIndex, reference, this.node);
473  
474      let shiftOffset = (shiftIndex ?? 0) * 2;
475  
476      return (
477        node && this.push(node, getPropertiesSimple(reference, this.childrenIndexes) + shiftOffset)
478      );
479    }
480  
481    at(depth) {
482      return skipToDepth(depth, this);
483    }
484  };
485  
486  export const tagPathsAreEqual = (a, b) => {
487    if (a == null || b == null) return b == a;
488    return a.path.node === b.path.node && a.childrenIndex === b.childrenIndex;
489  };
490  
491  export class TagPath {
492    constructor(path, childrenIndex) {
493      if (path == null || childrenIndex == null) throw new Error();
494  
495      this.path = path;
496      this.childrenIndex = childrenIndex;
497  
498      if (this.tag == null) throw new Error();
499    }
500  
501    static from(path, childrenIndex) {
502      let size = btree.getSum(path.node.children);
503      let index = childrenIndex < 0 ? size + childrenIndex : childrenIndex;
504  
505      return index >= 0 && index < size ? new TagPath(path, index) : null;
506    }
507  
508    get tag() {
509      return this.child;
510    }
511  
512    get node() {
513      return this.path.node;
514    }
515  
516    get child() {
517      return btree.getAt(this.childrenIndex, this.path.node.children);
518    }
519  
520    get nextSibling() {
521      const { path, childrenIndex } = this;
522  
523      const child =
524        childrenIndex + 1 >= btree.getSum(path.node.children)
525          ? null
526          : btree.getAt(childrenIndex + 1, path.node.children);
527  
528      return child && new TagPath(path, childrenIndex + 1);
529    }
530  
531    get next() {
532      let { path, childrenIndex } = this;
533  
534      let leaving = false;
535  
536      for (;;) {
537        let prevTag = btree.getAt(childrenIndex - 1, path.node.children);
538        let tag = btree.getAt(childrenIndex, path.node.children);
539        let isInitialTag = path.node === this.path.node && childrenIndex === this.childrenIndex;
540        let wasLeaving = leaving;
541        leaving = false;
542  
543        if (!tag) return null;
544  
545        // done
546        if (
547          !isInitialTag &&
548          tag.type !== EmbeddedNode &&
549          (tag.type !== GapTag || isGapNode(path.node) || prevTag.type === ShiftTag)
550        ) {
551          return new TagPath(path, childrenIndex);
552        }
553  
554        // in
555        if (tag.type === EmbeddedNode && !wasLeaving) {
556          path = path.push(tag.value, childrenIndex - 1);
557          childrenIndex = 0;
558          continue;
559        }
560  
561        // in
562        if (tag.type === GapTag && !wasLeaving && !isGapNode(path.node)) {
563          let refIndex = childrenIndex - 1;
564          let refTag;
565          let prevTag = btree.getAt(childrenIndex - 1, path.node.children);
566          let nextTag = btree.getAt(childrenIndex + 1, path.node.children);
567  
568          if (
569            path.parent &&
570            btree.getAt(path.referenceIndex, path.outer.children)?.type === ShiftTag &&
571            childrenIndex === 2
572          ) {
573            childrenIndex = path.referenceIndex + 1;
574            path = path.parent;
575            leaving = true;
576            continue;
577          }
578  
579          if (prevTag.type === ReferenceTag) {
580            refTag = prevTag;
581  
582            if (nextTag && nextTag.type === ShiftTag) {
583              const shifts = getProperties(refTag, path.node.properties).node;
584  
585              if (!Array.isArray(shifts)) throw new Error();
586  
587              const { name, isArray, flags } = refTag.value;
588              let resolvedReference = refTag;
589              if (isArray) {
590                let index = path.referenceIndexes[refIndex];
591                resolvedReference =
592                  index === -1 ? null : buildReferenceTag(name, index != null, flags, index);
593              }
594  
595              path = path.get(resolvedReference, 0);
596              childrenIndex = 0;
597  
598              if (!path) {
599                return null;
600              }
601              continue;
602            } else {
603              if (
604                !['#', '@'].includes(refTag.value.name) &&
605                (!refTag.value.isArray || path.referenceIndexes[refIndex] != null)
606              ) {
607                const { name, isArray, flags } = refTag.value;
608                let resolvedReference = refTag;
609                if (isArray) {
610                  let index = path.referenceIndexes[refIndex];
611                  resolvedReference =
612                    index === -1 ? null : buildReferenceTag(name, index != null, flags, index);
613                }
614  
615                if (resolvedReference) {
616                  path = path.get(resolvedReference);
617                  childrenIndex = 0;
618  
619                  if (!path) {
620                    return null;
621                  }
622                  continue;
623                }
624              }
625            }
626          } else if (prevTag.type === ShiftTag) {
627            let refIndex = childrenIndex - prevTag.value.index * 2 - 1;
628            let refTag = btree.getAt(refIndex, path.node.children);
629  
630            const { name, isArray, flags } = refTag.value;
631            let resolvedReference = refTag;
632            if (isArray) {
633              let index = path.referenceIndexes[refIndex];
634              resolvedReference =
635                index === -1 ? null : buildReferenceTag(name, index != null, flags, index);
636            }
637  
638            if (resolvedReference) {
639              path = path.get(resolvedReference);
640              // this was introducing errors
641              // caused us to return to a point before we left
642              path.referenceIndex = childrenIndex;
643              childrenIndex = 3;
644              continue;
645            }
646          } else {
647            throw new Error();
648          }
649        }
650  
651        // shift
652        if (tag.type === ShiftTag) {
653          let refIndex = childrenIndex - tag.value.index * 2;
654          let refTag = btree.getAt(refIndex, path.node.children);
655  
656          const { name, isArray, flags } = refTag.value;
657          let resolvedReference = null;
658          if (isArray) {
659            let index = path.referenceIndexes[refIndex];
660            resolvedReference =
661              index === -1 ? null : buildReferenceTag(name, index != null, flags, index);
662          } else {
663            resolvedReference = refTag;
664          }
665  
666          if (resolvedReference) {
667            path = path.get(resolvedReference, tag.value.index);
668            childrenIndex = 0;
669            continue;
670          }
671  
672          // go backwards through any other shifts until we're done
673          // path = path.parent;
674          // childrenIndex = 0;
675          // continue;
676        }
677  
678        // over
679        if (path.node && childrenIndex + 1 < btree.getSum(path.node.children)) {
680          childrenIndex++;
681          continue;
682        }
683  
684        // out
685        if (path.referenceIndex != null) {
686          do {
687            if (btree.getAt(path.referenceIndex + 2, path.outer.children)?.type === ShiftTag) {
688              childrenIndex =
689                btree.getSum(path.outer.children) > path.referenceIndex + 2
690                  ? path.referenceIndex + 2
691                  : null;
692            } else {
693              childrenIndex = path.referenceIndex + 1;
694            }
695  
696            path = path.parent;
697            leaving = true;
698          } while (childrenIndex == null);
699  
700          leaving = true;
701          continue;
702        }
703  
704        return null;
705      }
706    }
707  
708    get nextUnshifted() {
709      let { path, childrenIndex } = this;
710  
711      let leaving = false;
712  
713      for (;;) {
714        let tag = btree.getAt(childrenIndex, path.node.children);
715        let isInitialTag = path.node === this.path.node && childrenIndex === this.childrenIndex;
716        let wasLeaving = leaving;
717        leaving = false;
718  
719        if (!tag) return null;
720  
721        // done
722        if (
723          !isInitialTag &&
724          tag.type !== EmbeddedNode &&
725          tag.type !== ShiftTag &&
726          (tag.type !== GapTag || isGapNode(path.node))
727        ) {
728          return new TagPath(path, childrenIndex);
729        }
730  
731        // in
732        if (tag.type === EmbeddedNode && !wasLeaving) {
733          path = path.push(tag.value, childrenIndex - 1);
734          childrenIndex = 0;
735          continue;
736        }
737  
738        // in
739        if (tag.type === GapTag && !wasLeaving && !isGapNode(path.node)) {
740          let refIndex = childrenIndex - 1;
741          let refTag;
742          let prevTag = btree.getAt(childrenIndex - 1, path.node.children);
743  
744          if (prevTag.type === ShiftTag) {
745            // continue
746          } else if (prevTag.type === ReferenceTag) {
747            refTag = prevTag;
748  
749            if (
750              !['#', '@'].includes(refTag.value.name) &&
751              (!refTag.value.isArray || path.referenceIndexes[refIndex] != null)
752            ) {
753              const { name, isArray, flags } = refTag.value;
754              let resolvedReference = refTag;
755              if (isArray) {
756                let index = path.referenceIndexes[refIndex];
757                resolvedReference =
758                  index === -1 ? null : buildReferenceTag(name, index != null, flags, index);
759              }
760  
761              if (resolvedReference) {
762                path = path.get(resolvedReference);
763                childrenIndex = 0;
764  
765                if (!path) {
766                  return null;
767                }
768                continue;
769              }
770            }
771          } else {
772            throw new Error();
773          }
774        }
775  
776        // over
777        if (path.node && childrenIndex + 1 < btree.getSum(path.node.children)) {
778          childrenIndex++;
779          continue;
780        }
781  
782        // out
783        if (path.referenceIndex != null) {
784          do {
785            childrenIndex = path.referenceIndex + 1;
786  
787            path = path.parent;
788            leaving = true;
789          } while (childrenIndex == null);
790  
791          leaving = true;
792          continue;
793        }
794  
795        return null;
796      }
797    }
798  
799    get previousSibling() {
800      const { path, childrenIndex } = this;
801  
802      const child = childrenIndex - 1 < 0 ? null : btree.getAt(childrenIndex - 1, path.node.children);
803  
804      return child && new TagPath(path, childrenIndex - 1);
805    }
806  
807    get previous() {
808      throw new Error('not implemented');
809    }
810  
811    get previousUnshifted() {
812      let { path, childrenIndex } = this;
813  
814      let leaving = false;
815  
816      for (;;) {
817        let tag = btree.getAt(childrenIndex, path.node.children);
818        let isInitialTag = path.node === this.path.node && childrenIndex === this.childrenIndex;
819        let wasLeaving = leaving;
820        leaving = false;
821  
822        if (!tag) return null;
823  
824        // done
825        if (
826          !isInitialTag &&
827          tag.type !== EmbeddedNode &&
828          tag.type !== ShiftTag &&
829          (tag.type !== GapTag || isGapNode(path.node))
830        ) {
831          return new TagPath(path, childrenIndex);
832        }
833  
834        // in
835        if (tag.type === EmbeddedNode && !wasLeaving) {
836          path = path.push(tag.value, childrenIndex - 1);
837          childrenIndex = btree.getSum(tag.value.children) - 1;
838          continue;
839        }
840  
841        // in
842        if (tag.type === GapTag && !wasLeaving && !isGapNode(path.node)) {
843          let refIndex = childrenIndex - 1;
844          let refTag;
845          let prevTag = btree.getAt(childrenIndex - 1, path.node.children);
846  
847          if (prevTag.type === ShiftTag) {
848            // continue
849          } else if (prevTag.type === ReferenceTag) {
850            refTag = prevTag;
851  
852            if (
853              !['#', '@'].includes(refTag.value.name) &&
854              (!refTag.value.isArray || path.referenceIndexes[refIndex] != null)
855            ) {
856              const { name, isArray, flags } = refTag.value;
857              let resolvedReference = refTag;
858              if (isArray) {
859                let index = path.referenceIndexes[refIndex];
860                resolvedReference =
861                  index === -1 ? null : buildReferenceTag(name, index != null, flags, index);
862              }
863  
864              if (resolvedReference) {
865                path = path.get(resolvedReference);
866                childrenIndex = 0;
867  
868                if (!path) {
869                  return null;
870                }
871                continue;
872              }
873            }
874          } else {
875            throw new Error();
876          }
877        }
878  
879        // over
880        if (path.node && childrenIndex + 1 < btree.getSum(path.node.children)) {
881          childrenIndex--;
882          continue;
883        }
884  
885        // out
886        if (path.referenceIndex != null) {
887          do {
888            childrenIndex = path.referenceIndex;
889  
890            path = path.parent;
891            leaving = true;
892          } while (childrenIndex == null);
893  
894          leaving = true;
895          continue;
896        }
897  
898        return null;
899      }
900    }
901  
902    get inner() {
903      return this.innerPath?.node;
904    }
905  
906    get innerPath() {
907      let { tag, previousSibling: ref } = this;
908  
909      if (tag.type !== GapTag || isGapNode(this.node) || ref.tag.type === ShiftTag) {
910        return null;
911      }
912  
913      if (ref.tag.type !== ReferenceTag) throw new Error();
914  
915      let resolvedRef = ref.tag;
916  
917      if (ref.tag.value.isArray) {
918        const { name, flags, isArray } = ref.tag.value;
919        resolvedRef = buildReferenceTag(
920          name,
921          isArray,
922          flags,
923          ref.path.referenceIndexes[ref.childrenIndex],
924        );
925      }
926  
927      return this.path.get(resolvedRef);
928    }
929  
930    equalTo(tagPath) {
931      return this.node === tagPath.node && this.childrenIndex === tagPath.childrenIndex;
932    }
933  }