Skip to content

Commit 1626e50

Browse files
committed
Move Map and Set to stdlib, fixes #17
1 parent 7ed55f7 commit 1626e50

File tree

11 files changed

+4173
-5187
lines changed

11 files changed

+4173
-5187
lines changed

dist/asc.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/asc.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

std/assembly/arraybuffer.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,4 @@ export class ArrayBuffer {
2727
move_memory(changetype<usize>(buffer) + HEADER_SIZE, changetype<usize>(this) + HEADER_SIZE + begin, newLen);
2828
return buffer;
2929
}
30-
31-
// internals
32-
33-
static readonly HEADER_SIZE: usize = HEADER_SIZE;
34-
35-
@inline load<T>(index: usize): T {
36-
return load<T>(changetype<usize>(this) + index * sizeof<T>(), HEADER_SIZE);
37-
}
38-
39-
@inline store<T>(index: usize, value: T): void {
40-
store<T>(changetype<usize>(this) + index * sizeof<T>(), value, HEADER_SIZE);
41-
}
4230
}

std/assembly/map.ts

Lines changed: 146 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,165 @@
1+
import {
2+
HEADER_SIZE as HEADER_SIZE_AB
3+
} from "./internal/arraybuffer";
4+
5+
import {
6+
hash
7+
} from "./internal/hash";
8+
9+
// A deterministic hash map based on CloseTable from https://github.com/jorendorff/dht
10+
11+
const INITIAL_CAPACITY = 4;
12+
const FILL_FACTOR: f64 = 8 / 3;
13+
const FREE_FACTOR: f64 = 3 / 4;
14+
15+
/** Structure of a map entry. */
16+
@unmanaged class MapEntry<K,V> {
17+
key: K;
18+
value: V;
19+
taggedNext: usize; // LSB=1 indicates EMPTY
20+
}
21+
22+
/** Empty bit. */
23+
const EMPTY: usize = 1 << 0;
24+
25+
/** Size of a bucket. */
26+
const BUCKET_SIZE = sizeof<usize>();
27+
28+
/** Computes the alignment of an entry. */
29+
@inline function ENTRY_ALIGN<K,V>(): usize {
30+
// can align to 4 instead of 8 if 32-bit and K/V is <= 32-bits
31+
const maxkv = sizeof<K>() > sizeof<V>() ? sizeof<K>() : sizeof<V>();
32+
const align = (maxkv > sizeof<usize>() ? maxkv : sizeof<usize>()) - 1;
33+
return align;
34+
}
35+
36+
/** Computes the aligned size of an entry. */
37+
@inline function ENTRY_SIZE<K,V>(): usize {
38+
const align = ENTRY_ALIGN<K,V>();
39+
const size = (offsetof<MapEntry<K,V>>() + align) & ~align;
40+
return size;
41+
}
42+
143
export class Map<K,V> {
244

3-
private __keys: K[] = [];
4-
private __values: V[] = [];
45+
// buckets holding references to the respective first entry within
46+
private buckets: ArrayBuffer; // usize[bucketsMask + 1]
47+
private bucketsMask: u32;
48+
49+
// entries in insertion order
50+
private entries: ArrayBuffer; // MapEntry<K,V>[entriesCapacity]
51+
private entriesCapacity: i32;
52+
private entriesOffset: i32;
53+
private entriesCount: i32;
54+
55+
get size(): i32 { return this.entriesCount; }
556

6-
// FIXME: not a proper map implementation, just a filler
57+
constructor() { this.clear(); }
758

8-
get size(): i32 {
9-
return this.__keys.length;
59+
clear(): void {
60+
const bucketsSize = INITIAL_CAPACITY * <i32>BUCKET_SIZE;
61+
this.buckets = new ArrayBuffer(bucketsSize);
62+
this.bucketsMask = INITIAL_CAPACITY - 1;
63+
const entriesSize = INITIAL_CAPACITY * <i32>ENTRY_SIZE<K,V>();
64+
this.entries = new ArrayBuffer(entriesSize, true);
65+
this.entriesCapacity = INITIAL_CAPACITY;
66+
this.entriesOffset = 0;
67+
this.entriesCount = 0;
1068
}
1169

12-
get(key: K): V | null {
13-
var keys = this.__keys;
14-
for (let i = 0, k = keys.length; i < k; ++i) {
15-
if (keys[i] == key) {
16-
return this.__values[i];
17-
}
70+
private find(key: K, hashCode: u32): MapEntry<K,V> | null {
71+
var entry = load<MapEntry<K,V>>(
72+
changetype<usize>(this.buckets) + <usize>(hashCode & this.bucketsMask) * BUCKET_SIZE,
73+
HEADER_SIZE_AB
74+
);
75+
while (entry) {
76+
if (!(entry.taggedNext & EMPTY) && entry.key == key) return entry;
77+
entry = changetype<MapEntry<K,V>>(entry.taggedNext & ~EMPTY);
1878
}
1979
return null;
2080
}
2181

2282
has(key: K): bool {
23-
var keys = this.__keys;
24-
for (let i = 0, k = keys.length; i < k; ++i) {
25-
if (keys[i] == key) {
26-
return true;
83+
return this.find(key, hash<K>(key)) !== null;
84+
}
85+
86+
get(key: K): V {
87+
var entry = this.find(key, hash<K>(key));
88+
return entry ? entry.value : <V>unreachable();
89+
}
90+
91+
set(key: K, value: V): void {
92+
var hashCode = hash<K>(key);
93+
var entry = this.find(key, hashCode);
94+
if (entry) {
95+
entry.value = value;
96+
} else {
97+
// check if rehashing is necessary
98+
if (this.entriesOffset == this.entriesCapacity) {
99+
this.rehash(
100+
this.entriesCount < <i32>(this.entriesCapacity * FREE_FACTOR)
101+
? this.bucketsMask // just rehash if 1/4+ entries are empty
102+
: (this.bucketsMask << 1) | 1 // grow capacity to next 2^N
103+
);
27104
}
105+
// append new entry
106+
let entries = this.entries;
107+
entry = changetype<MapEntry<K,V>>(
108+
changetype<usize>(entries) + HEADER_SIZE_AB + this.entriesOffset++ * ENTRY_SIZE<K,V>()
109+
);
110+
entry.key = key;
111+
entry.value = value;
112+
++this.entriesCount;
113+
// link with previous entry in bucket
114+
let bucketPtrBase = changetype<usize>(this.buckets) + <usize>(hashCode & this.bucketsMask) * BUCKET_SIZE;
115+
entry.taggedNext = load<usize>(bucketPtrBase, HEADER_SIZE_AB);
116+
store<usize>(bucketPtrBase, changetype<usize>(entry), HEADER_SIZE_AB);
28117
}
29-
return false;
30118
}
31119

32-
set(key: K, value: V): void {
33-
this.__keys.push(key);
34-
this.__values.push(value);
120+
delete(key: K): bool {
121+
var entry = this.find(key, hash<K>(key));
122+
if (!entry) return false;
123+
entry.taggedNext |= EMPTY;
124+
--this.entriesCount;
125+
// check if rehashing is appropriate
126+
var halfBucketsMask = this.bucketsMask >> 1;
127+
if (
128+
halfBucketsMask + 1 >= max<u32>(INITIAL_CAPACITY, this.entriesCount) &&
129+
this.entriesCount < <i32>(this.entriesCapacity * FREE_FACTOR)
130+
) this.rehash(halfBucketsMask);
131+
return true;
35132
}
36133

37-
clear(): void {
38-
this.__keys.length = 0;
39-
this.__values.length = 0;
134+
private rehash(newBucketsMask: u32): void {
135+
var newBucketsCapacity = <i32>(newBucketsMask + 1);
136+
var newBuckets = new ArrayBuffer(newBucketsCapacity * <i32>BUCKET_SIZE);
137+
var newEntriesCapacity = <i32>(newBucketsCapacity * FILL_FACTOR);
138+
var newEntries = new ArrayBuffer(newEntriesCapacity * <i32>ENTRY_SIZE<K,V>(), true);
139+
140+
// copy old entries to new entries
141+
var oldPtr = changetype<usize>(this.entries) + HEADER_SIZE_AB;
142+
var oldEnd = oldPtr + <usize>this.entriesOffset * ENTRY_SIZE<K,V>();
143+
var newPtr = changetype<usize>(newEntries) + HEADER_SIZE_AB;
144+
while (oldPtr != oldEnd) {
145+
let oldEntry = changetype<MapEntry<K,V>>(oldPtr);
146+
if (!(oldEntry.taggedNext & EMPTY)) {
147+
let newEntry = changetype<MapEntry<K,V>>(newPtr);
148+
newEntry.key = oldEntry.key;
149+
newEntry.value = oldEntry.value;
150+
let newBucketIndex = hash<K>(oldEntry.key) & newBucketsMask;
151+
let newBucketPtrBase = changetype<usize>(newBuckets) + <usize>newBucketIndex * BUCKET_SIZE;
152+
newEntry.taggedNext = load<usize>(newBucketPtrBase, HEADER_SIZE_AB);
153+
store<usize>(newBucketPtrBase, newPtr, HEADER_SIZE_AB);
154+
newPtr += ENTRY_SIZE<K,V>();
155+
}
156+
oldPtr += ENTRY_SIZE<K,V>();
157+
}
158+
159+
this.buckets = newBuckets;
160+
this.bucketsMask = newBucketsMask;
161+
this.entries = newEntries;
162+
this.entriesCapacity = newEntriesCapacity;
163+
this.entriesOffset = this.entriesCount;
40164
}
41165
}

0 commit comments

Comments
 (0)