Skip to content

Commit f7e2e32

Browse files
Optimize List runtime representation with a stack
1 parent 5cad85d commit f7e2e32

File tree

2 files changed

+69
-34
lines changed

2 files changed

+69
-34
lines changed

src/Fable.Transforms/Fable2Babel.fs

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,6 @@ module Util =
176176
let entRef = Replacements.jsConstructor com ent
177177
com.TransformAsExpr(ctx, entRef)
178178

179-
let makeList com ctx headAndTail =
180-
match headAndTail with
181-
| None -> [||]
182-
| Some(TransformExpr com ctx head, TransformExpr com ctx tail) -> [|head; tail|]
183-
|> coreLibConstructorCall com ctx "Types" "List"
184-
185179
// TODO: range
186180
let makeArray (com: IBabelCompiler) ctx exprs =
187181
List.mapToArray (fun e -> com.TransformAsExpr(ctx, e)) exprs
@@ -814,12 +808,12 @@ module Util =
814808
| Fable.NewTuple vals -> makeTypedArray com ctx Fable.Any (Fable.ArrayValues vals)
815809
// Optimization for bundle size: compile list literals as List.ofArray
816810
| Replacements.ListLiteral(exprs, t) ->
817-
match exprs with
818-
| [] -> makeList com ctx None
819-
| [expr] -> Some(expr, Fable.Value(Fable.NewList (None,t), None)) |> makeList com ctx
820-
| exprs -> [|makeArray com ctx exprs|] |> coreLibCall com ctx r "List" "ofArray"
811+
[|List.rev exprs |> makeArray com ctx|] |> coreLibConstructorCall com ctx "Types" "List"
821812
| Fable.NewList (headAndTail, _) ->
822-
makeList com ctx headAndTail
813+
match headAndTail with
814+
| None -> coreLibConstructorCall com ctx "Types" "List" [||]
815+
| Some(TransformExpr com ctx head, TransformExpr com ctx tail) ->
816+
coreLibCall com ctx None "Types" "newList" [|head; tail|]
823817
| Fable.NewOption (value, t) ->
824818
match value with
825819
| Some (TransformExpr com ctx e) ->
@@ -1217,9 +1211,9 @@ module Util =
12171211
let op = if nonEmpty then BinaryUnequal else BinaryEqual
12181212
upcast BinaryExpression(op, com.TransformAsExpr(ctx, expr), NullLiteral(), ?loc=range)
12191213
| Fable.ListTest nonEmpty ->
1220-
let expr = com.TransformAsExpr(ctx, expr)
1221-
let op = if nonEmpty then BinaryUnequal else BinaryEqual
1222-
upcast BinaryExpression(op, get None expr "tail", NullLiteral(), ?loc=range)
1214+
let expr = get range (com.TransformAsExpr(ctx, expr)) "empty"
1215+
if nonEmpty then upcast UnaryExpression(UnaryNot, expr, ?loc=range)
1216+
else expr
12231217
| Fable.UnionCaseTest(uci, ent) ->
12241218
let expected = FSharp2Fable.Helpers.unionCaseTag ent uci |> ofInt
12251219
let actual = com.TransformAsExpr(ctx, expr) |> getUnionExprTag None

src/fable-library/Types.ts

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,24 +54,60 @@ function compareList<T>(self: List<T>, other: List<T>) {
5454
if (other == null) {
5555
return -1;
5656
}
57-
while (self.tail != null) {
58-
if (other.tail == null) { return 1; }
59-
const res = compare(self.head, other.head);
57+
const idxDiff = self.idx - other.idx;
58+
for (let i = self.idx; i >= 0; i--) {
59+
let otherIdx = i - idxDiff;
60+
if (otherIdx < 0) { return 1; }
61+
const selfItem = self.vals[i];
62+
const otherItem = other.vals[otherIdx];
63+
const res = compare(selfItem, otherItem);
6064
if (res !== 0) { return res; }
61-
self = self.tail;
62-
other = other.tail;
6365
}
64-
return other.tail == null ? 0 : -1;
66+
return self.length === other.length ? 0 : -1;
6567
}
6668
}
6769

70+
export function newList<T>(head: T, tail: List<T>): List<T> {
71+
// If the tail points to the last index of the stack, push the new value into it.
72+
// Otherwise, create a new stack
73+
const vals = tail.vals.length === tail.idx + 1 ? tail.vals : tail.vals.slice(0, tail.idx + 1);
74+
vals.push(head);
75+
const li = new List(vals);
76+
li._tail = tail;
77+
return li;
78+
}
79+
80+
/**
81+
* F# list is represented in runtime by an optimized type that uses a stack (a reverted JS array)
82+
* to store the values, so we can a have a big list represented by a single object (plus the stack).
83+
* It also allows for optimizations in the List module.
84+
*/
6885
export class List<T> implements IEquatable<List<T>>, IComparable<List<T>>, Iterable<T> {
69-
public head: T;
70-
public tail?: List<T>;
86+
public vals: T[];
87+
public idx: number;
88+
public _tail: List<T> | undefined;
89+
90+
constructor(vals?: T[], idx?: number) {
91+
this.vals = vals ?? [];
92+
this.idx = idx ?? this.vals.length - 1;
93+
}
94+
95+
public get empty() {
96+
return this.idx < 0;
97+
}
98+
99+
public get head(): T | undefined {
100+
return this.vals[this.idx];
101+
}
71102

72-
constructor(head?: T, tail?: List<T>) {
73-
this.head = head as T;
74-
this.tail = tail;
103+
public get tail(): List<T> | undefined {
104+
return !this.empty
105+
? this._tail ?? (this._tail = new List(this.vals, this.idx - 1))
106+
: undefined;
107+
}
108+
109+
public get length() {
110+
return this.idx + 1;
75111
}
76112

77113
public toString() {
@@ -83,20 +119,25 @@ export class List<T> implements IEquatable<List<T>>, IComparable<List<T>>, Itera
83119
}
84120

85121
public [Symbol.iterator](): Iterator<T> {
86-
let cur: List<T> | undefined = this;
122+
let curIdx = this.idx;
87123
return {
88-
next: (): IteratorResult<T> => {
89-
const value = cur?.head as T;
90-
const done = cur?.tail == null;
91-
cur = cur?.tail;
92-
return { done, value };
93-
},
124+
next: () => ({
125+
done: curIdx < 0,
126+
value: this.vals[curIdx--],
127+
}),
94128
};
95129
}
96130

97131
public GetHashCode() {
98-
const hashes = Array.from(this).map(structuralHash);
99-
return combineHashCodes(hashes);
132+
if (this.idx < 0) {
133+
return 0;
134+
} else {
135+
const hashes: number[] = new Array(this.idx + 1);
136+
for (let i = this.idx; i >= 0; i--) {
137+
hashes[i] = structuralHash(this.vals[i]);
138+
}
139+
return combineHashCodes(hashes);
140+
}
100141
}
101142

102143
public Equals(other: List<T>): boolean {

0 commit comments

Comments
 (0)