| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Tobias Koppers @sokra
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | const TOMBSTONE = Symbol("tombstone");
|
|---|
| 9 | const UNDEFINED_MARKER = Symbol("undefined");
|
|---|
| 10 |
|
|---|
| 11 | /**
|
|---|
| 12 | * Public cell value exposed by `StackedMap`, where `undefined` is preserved as
|
|---|
| 13 | * a valid stored result.
|
|---|
| 14 | * @template T
|
|---|
| 15 | * @typedef {T | undefined} Cell<T>
|
|---|
| 16 | */
|
|---|
| 17 |
|
|---|
| 18 | /**
|
|---|
| 19 | * Internal cell value used to distinguish deleted entries and explicit
|
|---|
| 20 | * `undefined` assignments while traversing stacked scopes.
|
|---|
| 21 | * @template T
|
|---|
| 22 | * @typedef {T | typeof TOMBSTONE | typeof UNDEFINED_MARKER} InternalCell<T>
|
|---|
| 23 | */
|
|---|
| 24 |
|
|---|
| 25 | /**
|
|---|
| 26 | * Converts an internal key/value pair into the external representation returned
|
|---|
| 27 | * by iteration helpers.
|
|---|
| 28 | * @template K
|
|---|
| 29 | * @template V
|
|---|
| 30 | * @param {[K, InternalCell<V>]} pair the internal cell
|
|---|
| 31 | * @returns {[K, Cell<V>]} its “safe” representation
|
|---|
| 32 | */
|
|---|
| 33 | const extractPair = (pair) => {
|
|---|
| 34 | const key = pair[0];
|
|---|
| 35 | const val = pair[1];
|
|---|
| 36 | if (val === UNDEFINED_MARKER || val === TOMBSTONE) {
|
|---|
| 37 | return [key, undefined];
|
|---|
| 38 | }
|
|---|
| 39 | return /** @type {[K, Cell<V>]} */ (pair);
|
|---|
| 40 | };
|
|---|
| 41 |
|
|---|
| 42 | /**
|
|---|
| 43 | * Layered map that supports child scopes while memoizing lookups from parent
|
|---|
| 44 | * scopes into the current layer.
|
|---|
| 45 | * @template K
|
|---|
| 46 | * @template V
|
|---|
| 47 | */
|
|---|
| 48 | class StackedMap {
|
|---|
| 49 | /**
|
|---|
| 50 | * Creates a new map layer on top of an optional parent stack.
|
|---|
| 51 | * @param {Map<K, InternalCell<V>>[]=} parentStack an optional parent
|
|---|
| 52 | */
|
|---|
| 53 | constructor(parentStack) {
|
|---|
| 54 | /** @type {Map<K, InternalCell<V>>} */
|
|---|
| 55 | this.map = new Map();
|
|---|
| 56 | /** @type {Map<K, InternalCell<V>>[]} */
|
|---|
| 57 | this.stack = parentStack === undefined ? [] : [...parentStack];
|
|---|
| 58 | this.stack.push(this.map);
|
|---|
| 59 | }
|
|---|
| 60 |
|
|---|
| 61 | /**
|
|---|
| 62 | * Stores a value in the current layer, preserving explicit `undefined`
|
|---|
| 63 | * values with an internal marker.
|
|---|
| 64 | * @param {K} item the key of the element to add
|
|---|
| 65 | * @param {V} value the value of the element to add
|
|---|
| 66 | * @returns {void}
|
|---|
| 67 | */
|
|---|
| 68 | set(item, value) {
|
|---|
| 69 | this.map.set(item, value === undefined ? UNDEFINED_MARKER : value);
|
|---|
| 70 | }
|
|---|
| 71 |
|
|---|
| 72 | /**
|
|---|
| 73 | * Deletes a key from the current view, either by removing it outright in the
|
|---|
| 74 | * root layer or by recording a tombstone in child layers.
|
|---|
| 75 | * @param {K} item the item to delete
|
|---|
| 76 | * @returns {void}
|
|---|
| 77 | */
|
|---|
| 78 | delete(item) {
|
|---|
| 79 | if (this.stack.length > 1) {
|
|---|
| 80 | this.map.set(item, TOMBSTONE);
|
|---|
| 81 | } else {
|
|---|
| 82 | this.map.delete(item);
|
|---|
| 83 | }
|
|---|
| 84 | }
|
|---|
| 85 |
|
|---|
| 86 | /**
|
|---|
| 87 | * Checks whether a key exists in the current scope chain, caching any parent
|
|---|
| 88 | * lookup result in the current layer.
|
|---|
| 89 | * @param {K} item the item to test
|
|---|
| 90 | * @returns {boolean} true if the item exists in this set
|
|---|
| 91 | */
|
|---|
| 92 | has(item) {
|
|---|
| 93 | const topValue = this.map.get(item);
|
|---|
| 94 | if (topValue !== undefined) {
|
|---|
| 95 | return topValue !== TOMBSTONE;
|
|---|
| 96 | }
|
|---|
| 97 | if (this.stack.length > 1) {
|
|---|
| 98 | for (let i = this.stack.length - 2; i >= 0; i--) {
|
|---|
| 99 | const value = this.stack[i].get(item);
|
|---|
| 100 | if (value !== undefined) {
|
|---|
| 101 | this.map.set(item, value);
|
|---|
| 102 | return value !== TOMBSTONE;
|
|---|
| 103 | }
|
|---|
| 104 | }
|
|---|
| 105 | this.map.set(item, TOMBSTONE);
|
|---|
| 106 | }
|
|---|
| 107 | return false;
|
|---|
| 108 | }
|
|---|
| 109 |
|
|---|
| 110 | /**
|
|---|
| 111 | * Returns the visible value for a key, caching parent hits and misses in the
|
|---|
| 112 | * current layer.
|
|---|
| 113 | * @param {K} item the key of the element to return
|
|---|
| 114 | * @returns {Cell<V>} the value of the element
|
|---|
| 115 | */
|
|---|
| 116 | get(item) {
|
|---|
| 117 | const topValue = this.map.get(item);
|
|---|
| 118 | if (topValue !== undefined) {
|
|---|
| 119 | return topValue === TOMBSTONE || topValue === UNDEFINED_MARKER
|
|---|
| 120 | ? undefined
|
|---|
| 121 | : topValue;
|
|---|
| 122 | }
|
|---|
| 123 | if (this.stack.length > 1) {
|
|---|
| 124 | for (let i = this.stack.length - 2; i >= 0; i--) {
|
|---|
| 125 | const value = this.stack[i].get(item);
|
|---|
| 126 | if (value !== undefined) {
|
|---|
| 127 | this.map.set(item, value);
|
|---|
| 128 | return value === TOMBSTONE || value === UNDEFINED_MARKER
|
|---|
| 129 | ? undefined
|
|---|
| 130 | : value;
|
|---|
| 131 | }
|
|---|
| 132 | }
|
|---|
| 133 | this.map.set(item, TOMBSTONE);
|
|---|
| 134 | }
|
|---|
| 135 | }
|
|---|
| 136 |
|
|---|
| 137 | /**
|
|---|
| 138 | * Collapses the stacked layers into a single concrete map.
|
|---|
| 139 | */
|
|---|
| 140 | _compress() {
|
|---|
| 141 | if (this.stack.length === 1) return;
|
|---|
| 142 | this.map = new Map();
|
|---|
| 143 | for (const data of this.stack) {
|
|---|
| 144 | for (const pair of data) {
|
|---|
| 145 | if (pair[1] === TOMBSTONE) {
|
|---|
| 146 | this.map.delete(pair[0]);
|
|---|
| 147 | } else {
|
|---|
| 148 | this.map.set(pair[0], pair[1]);
|
|---|
| 149 | }
|
|---|
| 150 | }
|
|---|
| 151 | }
|
|---|
| 152 | this.stack = [this.map];
|
|---|
| 153 | }
|
|---|
| 154 |
|
|---|
| 155 | /**
|
|---|
| 156 | * Returns the visible keys as an array after collapsing the stack.
|
|---|
| 157 | * @returns {K[]} array of keys
|
|---|
| 158 | */
|
|---|
| 159 | asArray() {
|
|---|
| 160 | this._compress();
|
|---|
| 161 | return [...this.map.keys()];
|
|---|
| 162 | }
|
|---|
| 163 |
|
|---|
| 164 | /**
|
|---|
| 165 | * Returns the visible keys as a `Set` after collapsing the stack.
|
|---|
| 166 | * @returns {Set<K>} set of keys
|
|---|
| 167 | */
|
|---|
| 168 | asSet() {
|
|---|
| 169 | this._compress();
|
|---|
| 170 | return new Set(this.map.keys());
|
|---|
| 171 | }
|
|---|
| 172 |
|
|---|
| 173 | /**
|
|---|
| 174 | * Returns visible key/value pairs using the external representation.
|
|---|
| 175 | * @returns {[K, Cell<V>][]} array of key/value pairs
|
|---|
| 176 | */
|
|---|
| 177 | asPairArray() {
|
|---|
| 178 | this._compress();
|
|---|
| 179 | return Array.from(this.map.entries(), extractPair);
|
|---|
| 180 | }
|
|---|
| 181 |
|
|---|
| 182 | /**
|
|---|
| 183 | * Returns the visible contents as a plain `Map`.
|
|---|
| 184 | * @returns {Map<K, Cell<V>>} materialized map
|
|---|
| 185 | */
|
|---|
| 186 | asMap() {
|
|---|
| 187 | return new Map(this.asPairArray());
|
|---|
| 188 | }
|
|---|
| 189 |
|
|---|
| 190 | /**
|
|---|
| 191 | * Returns the number of visible keys after collapsing the stack.
|
|---|
| 192 | * @returns {number} number of keys
|
|---|
| 193 | */
|
|---|
| 194 | get size() {
|
|---|
| 195 | this._compress();
|
|---|
| 196 | return this.map.size;
|
|---|
| 197 | }
|
|---|
| 198 |
|
|---|
| 199 | /**
|
|---|
| 200 | * Creates a child `StackedMap` that sees the current layers as its parent
|
|---|
| 201 | * scope.
|
|---|
| 202 | * @returns {StackedMap<K, V>} child map
|
|---|
| 203 | */
|
|---|
| 204 | createChild() {
|
|---|
| 205 | return new StackedMap(this.stack);
|
|---|
| 206 | }
|
|---|
| 207 | }
|
|---|
| 208 |
|
|---|
| 209 | module.exports = StackedMap;
|
|---|