Skip to content

Commit dbdc78d

Browse files
committed
Improve syntax macro performance
We were taking a massive performance hit by using shift. I've added a custom FastShiftArray to to bypass those limitations. Syntax macros processing of 10k lines went from 2216 ms to 156ms Now compiling 200k lines in 52 seconds (M1 Pro 16gb). Likely would have taken hours before these changes
1 parent fce0c5b commit dbdc78d

File tree

2 files changed

+114
-27
lines changed

2 files changed

+114
-27
lines changed

src/lib/fast-shift-array.mts

+88
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// TODO: Add map, filter, reduce, amd findIndex
2+
export class FastShiftArray<T> {
3+
private items: T[];
4+
private headIndex: number;
5+
6+
constructor(...args: T[]) {
7+
this.items = args;
8+
this.headIndex = 0;
9+
}
10+
11+
shift(): T | undefined {
12+
if (this.headIndex >= this.items.length) {
13+
return undefined;
14+
}
15+
const value = this.items[this.headIndex];
16+
this.items[this.headIndex] = undefined as any; // Optional: clear the reference for garbage collection
17+
this.headIndex++;
18+
return value;
19+
}
20+
21+
unshift(...items: T[]): number {
22+
if (items.length === 0) return this.length;
23+
this.headIndex = Math.max(this.headIndex - items.length, 0);
24+
for (let i = 0; i < items.length; i++) {
25+
this.items[this.headIndex + i] = items[i];
26+
}
27+
return this.length;
28+
}
29+
30+
push(...items: T[]): number {
31+
this.items.push(...items);
32+
return this.length;
33+
}
34+
35+
pop(): T | undefined {
36+
if (this.length === 0) return undefined;
37+
return this.items.pop();
38+
}
39+
40+
at(index: number): T | undefined {
41+
if (index < 0) {
42+
index = this.length + index;
43+
}
44+
return this.items[this.headIndex + index];
45+
}
46+
47+
set(index: number, value: T): boolean {
48+
const targetIndex = index < 0 ? this.length + index : index;
49+
if (targetIndex < 0 || targetIndex >= this.length) {
50+
return false; // Index out of bounds
51+
}
52+
this.items[this.headIndex + targetIndex] = value;
53+
return true;
54+
}
55+
56+
get length(): number {
57+
return this.items.length - this.headIndex;
58+
}
59+
60+
slice(start?: number, end?: number): T[] {
61+
const actualStart =
62+
start !== undefined
63+
? this.headIndex + (start < 0 ? this.length + start : start)
64+
: this.headIndex;
65+
const actualEnd =
66+
end !== undefined
67+
? this.headIndex + (end < 0 ? this.length + end : end)
68+
: this.items.length;
69+
return this.items.slice(actualStart, actualEnd);
70+
}
71+
72+
splice(start: number, deleteCount: number = 0, ...items: T[]): T[] {
73+
const actualStart =
74+
this.headIndex + (start < 0 ? this.length + start : start);
75+
return this.items.splice(actualStart, deleteCount, ...items);
76+
}
77+
78+
// Converts the FastShiftArray back to a normal array
79+
toArray(): T[] {
80+
return this.items.slice(this.headIndex);
81+
}
82+
83+
// Optional: Method to reset the headIndex
84+
resetShift(): void {
85+
this.items.splice(0, this.headIndex);
86+
this.headIndex = 0;
87+
}
88+
}

src/syntax-objects/list.mts

+26-27
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { FastShiftArray } from "../lib/fast-shift-array.mjs";
12
import { Expr } from "./expr.mjs";
23
import { Float } from "./float.mjs";
34
import { getIdStr } from "./get-id-str.mjs";
@@ -10,7 +11,7 @@ export class List extends Syntax {
1011
readonly syntaxType = "list";
1112
/** True when the list was defined by the user using parenthesis i.e. (hey, there) */
1213
mayBeTuple?: boolean;
13-
value: Expr[] = [];
14+
store: FastShiftArray<Expr> = new FastShiftArray();
1415

1516
constructor(
1617
opts: SyntaxMetadata & {
@@ -29,20 +30,24 @@ export class List extends Syntax {
2930
}
3031
}
3132

33+
get value() {
34+
return this.store.toArray();
35+
}
36+
3237
get hasChildren() {
33-
return !!this.value.length;
38+
return !!this.store.length;
3439
}
3540

3641
get length() {
37-
return this.value.length;
42+
return this.store.length;
3843
}
3944

4045
at(index: number): Expr | undefined {
41-
return this.value.at(index);
46+
return this.store.at(index);
4247
}
4348

4449
exprAt(index: number): Expr {
45-
const expr = this.value.at(index);
50+
const expr = this.store.at(index);
4651
if (!expr) {
4752
throw new Error(`No expr at ${index}`);
4853
}
@@ -68,7 +73,7 @@ export class List extends Syntax {
6873
set(index: number, expr: Expr | string) {
6974
const result = typeof expr === "string" ? Identifier.from(expr) : expr;
7075
result.parent = this;
71-
this.value[index] = result;
76+
this.store.set(index, result);
7277
return this;
7378
}
7479

@@ -82,48 +87,42 @@ export class List extends Syntax {
8287
}
8388

8489
consume(): Expr {
85-
const next = this.value.shift();
90+
const next = this.store.shift();
8691
if (!next) throw new Error("No remaining expressions");
8792
return next;
8893
}
8994

90-
consumeRest(): List {
91-
const newVal = this.slice(0);
92-
this.value = [];
93-
return newVal;
94-
}
95-
9695
first(): Expr | undefined {
97-
return this.value[0];
96+
return this.store.at(0);
9897
}
9998

10099
last(): Expr | undefined {
101-
return this.value.at(-1);
100+
return this.store.at(-1);
102101
}
103102

104103
/** Returns all but the first element in an array */
105104
rest(): Expr[] {
106-
return this.value.slice(1);
105+
return this.store.toArray().slice(1);
107106
}
108107

109108
pop(): Expr | undefined {
110-
return this.value.pop();
109+
return this.store.pop();
111110
}
112111

113112
push(...expr: ListValue[]) {
114113
expr.forEach((ex) => {
115114
if (typeof ex === "string") {
116-
this.value.push(new Identifier({ value: ex, parent: this }));
115+
this.store.push(new Identifier({ value: ex, parent: this }));
117116
return;
118117
}
119118

120119
if (typeof ex === "number" && Number.isInteger(ex)) {
121-
this.value.push(new Int({ value: ex, parent: this }));
120+
this.store.push(new Int({ value: ex, parent: this }));
122121
return;
123122
}
124123

125124
if (typeof ex === "number") {
126-
this.value.push(new Float({ value: ex, parent: this }));
125+
this.store.push(new Float({ value: ex, parent: this }));
127126
return;
128127
}
129128

@@ -139,11 +138,11 @@ export class List extends Syntax {
139138
}
140139

141140
if (ex.isList() && ex.calls("splice_quote")) {
142-
this.value.push(...ex.rest());
141+
this.store.push(...ex.rest());
143142
return;
144143
}
145144

146-
this.value.push(ex);
145+
this.store.push(ex);
147146
});
148147

149148
return this;
@@ -156,12 +155,12 @@ export class List extends Syntax {
156155
insert(expr: Expr | string, at = 0) {
157156
const result = typeof expr === "string" ? Identifier.from(expr) : expr;
158157
result.parent = this;
159-
this.value.splice(at, 0, result);
158+
this.store.splice(at, 0, result);
160159
return this;
161160
}
162161

163162
remove(index: number, count = 1) {
164-
this.value.splice(index, count);
163+
this.store.splice(index, count);
165164
return this;
166165
}
167166

@@ -200,16 +199,16 @@ export class List extends Syntax {
200199
slice(start?: number, end?: number): List {
201200
return new List({
202201
...super.getCloneOpts(),
203-
value: this.value.slice(start, end),
202+
value: this.store.slice(start, end),
204203
});
205204
}
206205

207206
sliceAsArray(start?: number, end?: number) {
208-
return this.value.slice(start, end);
207+
return this.store.slice(start, end);
209208
}
210209

211210
toArray(): Expr[] {
212-
return [...this.value];
211+
return this.value;
213212
}
214213

215214
toJSON() {

0 commit comments

Comments
 (0)