StackedMap.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const TOMBSTONE = Symbol("tombstone");
  7. const UNDEFINED_MARKER = Symbol("undefined");
  8. /**
  9. * Public cell value exposed by `StackedMap`, where `undefined` is preserved as
  10. * a valid stored result.
  11. * @template T
  12. * @typedef {T | undefined} Cell<T>
  13. */
  14. /**
  15. * Internal cell value used to distinguish deleted entries and explicit
  16. * `undefined` assignments while traversing stacked scopes.
  17. * @template T
  18. * @typedef {T | typeof TOMBSTONE | typeof UNDEFINED_MARKER} InternalCell<T>
  19. */
  20. /**
  21. * Converts an internal key/value pair into the external representation returned
  22. * by iteration helpers.
  23. * @template K
  24. * @template V
  25. * @param {[K, InternalCell<V>]} pair the internal cell
  26. * @returns {[K, Cell<V>]} its “safe” representation
  27. */
  28. const extractPair = (pair) => {
  29. const key = pair[0];
  30. const val = pair[1];
  31. if (val === UNDEFINED_MARKER || val === TOMBSTONE) {
  32. return [key, undefined];
  33. }
  34. return /** @type {[K, Cell<V>]} */ (pair);
  35. };
  36. /**
  37. * Layered map that supports child scopes while memoizing lookups from parent
  38. * scopes. Layers form a linked parent chain, so `createChild` is O(1) instead
  39. * of copying a layer array per scope. A layer's `Map` is only allocated on its
  40. * first own write, and lookup results are memoized into the nearest
  41. * `Map`-bearing layer of the walk — read-only layers (most block scopes) stay
  42. * allocation-free and share their parent's memoized entries.
  43. * @template K
  44. * @template V
  45. */
  46. class StackedMap {
  47. /**
  48. * Creates a new map layer on top of an optional parent.
  49. * @param {StackedMap<K, V>=} parent an optional parent map
  50. */
  51. constructor(parent) {
  52. /** @type {Map<K, InternalCell<V>> | undefined} */
  53. this.map = undefined;
  54. /** @type {StackedMap<K, V> | undefined} */
  55. this.parent = parent;
  56. }
  57. /**
  58. * Stores a value in the current layer, preserving explicit `undefined`
  59. * values with an internal marker.
  60. * @param {K} item the key of the element to add
  61. * @param {V} value the value of the element to add
  62. * @returns {void}
  63. */
  64. set(item, value) {
  65. (this.map || (this.map = new Map())).set(
  66. item,
  67. value === undefined ? UNDEFINED_MARKER : value
  68. );
  69. }
  70. /**
  71. * Deletes a key from the current view, either by removing it outright in the
  72. * root layer or by recording a tombstone in child layers.
  73. * @param {K} item the item to delete
  74. * @returns {void}
  75. */
  76. delete(item) {
  77. if (this.parent !== undefined) {
  78. (this.map || (this.map = new Map())).set(item, TOMBSTONE);
  79. } else if (this.map !== undefined) {
  80. this.map.delete(item);
  81. }
  82. }
  83. /**
  84. * Checks whether a key exists in the current scope chain, caching any parent
  85. * lookup result in the nearest `Map`-bearing layer.
  86. * @param {K} item the item to test
  87. * @returns {boolean} true if the item exists in this set
  88. */
  89. has(item) {
  90. const own = this.map;
  91. if (own !== undefined) {
  92. const topValue = own.get(item);
  93. if (topValue !== undefined) {
  94. return topValue !== TOMBSTONE;
  95. }
  96. }
  97. /** @type {StackedMap<K, V> | undefined} */
  98. let memoTarget = own !== undefined ? this : undefined;
  99. /** @type {StackedMap<K, V> | undefined} */
  100. let current = this.parent;
  101. while (current !== undefined) {
  102. const map = current.map;
  103. if (map !== undefined) {
  104. const value = map.get(item);
  105. if (value !== undefined) {
  106. if (memoTarget !== undefined) {
  107. /** @type {Map<K, InternalCell<V>>} */ (memoTarget.map).set(
  108. item,
  109. value
  110. );
  111. }
  112. return value !== TOMBSTONE;
  113. }
  114. if (memoTarget === undefined) memoTarget = current;
  115. }
  116. current = current.parent;
  117. }
  118. if (memoTarget !== undefined) {
  119. /** @type {Map<K, InternalCell<V>>} */ (memoTarget.map).set(
  120. item,
  121. TOMBSTONE
  122. );
  123. }
  124. return false;
  125. }
  126. /**
  127. * Returns the visible value for a key, caching parent hits and misses in the
  128. * nearest `Map`-bearing layer so repeated lookups (from this layer or any
  129. * sibling below that layer) answer with a single probe.
  130. * @param {K} item the key of the element to return
  131. * @returns {Cell<V>} the value of the element
  132. */
  133. get(item) {
  134. const own = this.map;
  135. if (own !== undefined) {
  136. const topValue = own.get(item);
  137. if (topValue !== undefined) {
  138. return topValue === TOMBSTONE || topValue === UNDEFINED_MARKER
  139. ? undefined
  140. : topValue;
  141. }
  142. }
  143. /** @type {StackedMap<K, V> | undefined} */
  144. let memoTarget = own !== undefined ? this : undefined;
  145. /** @type {StackedMap<K, V> | undefined} */
  146. let current = this.parent;
  147. while (current !== undefined) {
  148. const map = current.map;
  149. if (map !== undefined) {
  150. const value = map.get(item);
  151. if (value !== undefined) {
  152. if (memoTarget !== undefined) {
  153. /** @type {Map<K, InternalCell<V>>} */ (memoTarget.map).set(
  154. item,
  155. value
  156. );
  157. }
  158. return value === TOMBSTONE || value === UNDEFINED_MARKER
  159. ? undefined
  160. : value;
  161. }
  162. if (memoTarget === undefined) memoTarget = current;
  163. }
  164. current = current.parent;
  165. }
  166. if (memoTarget !== undefined) {
  167. /** @type {Map<K, InternalCell<V>>} */ (memoTarget.map).set(
  168. item,
  169. TOMBSTONE
  170. );
  171. }
  172. }
  173. /**
  174. * Collapses the parent chain into a single concrete map.
  175. */
  176. _compress() {
  177. if (this.parent === undefined) {
  178. const map = this.map;
  179. if (map === undefined) {
  180. this.map = new Map();
  181. return;
  182. }
  183. // a parent-less layer holds tombstones only as memoized misses
  184. // (deletes remove outright) — drop them from the visible view
  185. for (const pair of map) {
  186. if (pair[1] === TOMBSTONE) map.delete(pair[0]);
  187. }
  188. return;
  189. }
  190. /** @type {StackedMap<K, V>[]} */
  191. const layers = [];
  192. /** @type {StackedMap<K, V> | undefined} */
  193. let current = this;
  194. do {
  195. layers.push(current);
  196. current = current.parent;
  197. } while (current !== undefined);
  198. const map = new Map();
  199. for (let i = layers.length - 1; i >= 0; i--) {
  200. const layerMap = layers[i].map;
  201. if (layerMap !== undefined) {
  202. for (const pair of layerMap) {
  203. if (pair[1] === TOMBSTONE) {
  204. map.delete(pair[0]);
  205. } else {
  206. map.set(pair[0], pair[1]);
  207. }
  208. }
  209. }
  210. }
  211. this.map = map;
  212. this.parent = undefined;
  213. }
  214. /**
  215. * Returns the visible keys as an array after collapsing the stack.
  216. * @returns {K[]} array of keys
  217. */
  218. asArray() {
  219. this._compress();
  220. return [.../** @type {Map<K, InternalCell<V>>} */ (this.map).keys()];
  221. }
  222. /**
  223. * Returns the visible keys as a `Set` after collapsing the stack.
  224. * @returns {Set<K>} set of keys
  225. */
  226. asSet() {
  227. this._compress();
  228. return new Set(/** @type {Map<K, InternalCell<V>>} */ (this.map).keys());
  229. }
  230. /**
  231. * Returns visible key/value pairs using the external representation.
  232. * @returns {[K, Cell<V>][]} array of key/value pairs
  233. */
  234. asPairArray() {
  235. this._compress();
  236. return Array.from(
  237. /** @type {Map<K, InternalCell<V>>} */ (this.map).entries(),
  238. extractPair
  239. );
  240. }
  241. /**
  242. * Returns the visible contents as a plain `Map`.
  243. * @returns {Map<K, Cell<V>>} materialized map
  244. */
  245. asMap() {
  246. return new Map(this.asPairArray());
  247. }
  248. /**
  249. * Returns the number of visible keys after collapsing the stack.
  250. * @returns {number} number of keys
  251. */
  252. get size() {
  253. this._compress();
  254. return /** @type {Map<K, InternalCell<V>>} */ (this.map).size;
  255. }
  256. /**
  257. * Creates a child `StackedMap` that sees the current layers as its parent
  258. * scope.
  259. * @returns {StackedMap<K, V>} child map
  260. */
  261. createChild() {
  262. return new StackedMap(this);
  263. }
  264. }
  265. module.exports = StackedMap;