-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathcompiler.ts
301 lines (280 loc) · 9.07 KB
/
compiler.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
import { parse } from "./parser";
import { emit } from "./emitter";
import {
encodeString,
encodeNestedVector,
encodeSection,
op,
unsignedLEB128,
VAL_TYPE,
FUNCTION_TYPE,
GLOBAL_TYPE,
MUTABILITY,
TYPE_IDX,
EXPORT_TYPE,
SECTION,
MAGIC,
WASM_VERSION,
encodeFlatVector,
} from "./encoding";
import shims from "./shims";
import * as Utils from "./utils";
import { localFuncMap } from "./wasmFunctions";
import { CompilerContext, TypedFunction, EelVersion } from "./types";
import { WASM_MEMORY_SIZE } from "./constants";
type CompilerOptions = {
pools: {
[name: string]: Set<string>;
};
functions: {
[name: string]: {
pool: string;
code: string;
};
};
eelVersion?: EelVersion;
preParsed?: boolean;
};
export function compileModule({
pools,
functions: funcs,
eelVersion = 2,
preParsed = false,
}: CompilerOptions) {
if (Object.keys(pools).includes("shims")) {
throw new Error(
'You may not name a pool "shims". "shims" is reserved for injected JavaScript functions.'
);
}
// Collect all the globals that we expect to get as imports.
const importedVars: [string, string][] = [];
Object.entries(pools).forEach(([poolName, pool]) => {
pool.forEach(variableName => {
importedVars.push([poolName, variableName]);
});
});
// Ensure all the imported globals get the first ids.
const varResolver = new Utils.ScopedIdMap();
importedVars.forEach(([poolName, variableName]) => {
varResolver.get(poolName, variableName);
});
const functionImports = Object.entries(shims).map(([name, func]) => {
return {
args: new Array(func.length).fill(null).map(_ => VAL_TYPE.f64),
// Shims implicitly always return a number
returns: [VAL_TYPE.f64],
name,
};
});
const localFuncOrder: string[] = [];
const moduleFuncs: {
binary: number[];
exportName: string;
args: never[];
returns: never[];
localVariables: number[];
}[] = [];
Object.entries(funcs).forEach(([name, { pool, code }]) => {
if (pools[pool] == null) {
const poolsList = Object.keys(pools);
if (poolsList.length === 0) {
throw new Error(
`The function "${name}" was declared as using a variable ` +
`pool named "${pool}" but no pools were defined.`
);
}
throw new Error(
`The function "${name}" was declared as using a variable ` +
`pool named "${pool}" which is not among the variable ` +
`pools defined. The defined variable pools are: ` +
`${Utils.formatList(poolsList)}.`
);
}
const ast = preParsed ? code : parse(code);
if (typeof ast === "string") {
// TODO: Change the API so this can be enforced by types
throw new Error(
"Got passed unparsed code without setting the preParsed flag"
);
}
if (ast.type !== "SCRIPT") {
throw new Error("Invalid AST");
}
if (ast.body.length === 0) {
return;
}
const localVariables: number[] = [];
const context: CompilerContext = {
resolveVar: name => {
// The `reg00`-`reg99` variables are special in that they are shared between all pools.
if (/^reg\d\d$/.test(name)) {
return varResolver.get(null, name);
}
return varResolver.get(pool, name);
},
resolveLocal: type => {
// TODO: We could provide a way for the emitter to release a local
// variable so that we can reuse it, much in the same way a traditional
// compiler does in register allocation.
localVariables.push(type);
return localVariables.length - 1;
},
resolveFunc: name => {
// If this is a shim, return the shim index.
const shimdex = functionImports.findIndex(func => func.name === name);
if (shimdex !== -1) {
const call = op.call(shimdex);
if (name === "rand" && eelVersion === 1) {
return [...call, op.f64_floor];
}
return call;
}
// If it's not a shim and it's not a defined function, return null.
// The emitter will generate a nice error.
if (localFuncMap[name] == null) {
return null;
}
let index = localFuncOrder.indexOf(name);
if (index === -1) {
localFuncOrder.push(name);
index = localFuncOrder.length - 1;
}
return op.call(index + functionImports.length);
},
rawSource: code,
};
const binary = emit(ast, context);
moduleFuncs.push({
binary,
exportName: name,
args: [],
returns: [],
localVariables,
});
});
const localFuncs = localFuncOrder.map(name => {
const func = localFuncMap[name];
// This check is technically redundant since we check inside resolveLocalFunc
// in the compiler context. It's here just to catch potential compiler bugs.
if (func == null) {
throw new Error(`Undefined local function "${name}"`);
}
return func;
});
// Given a function definition, return a hashable string representation of its signature.
const getSignatureKey = (func: TypedFunction) => {
return [...func.args, "|", ...func.returns].join("-");
};
// https://webassembly.github.io/spec/core/binary/modules.html#type-section
const types: number[][] = [];
const typeIndexByKey: Map<string, number> = new Map();
[...functionImports, ...localFuncs, ...moduleFuncs].forEach(func => {
const key = getSignatureKey(func);
if (typeIndexByKey.has(key)) {
return;
}
types.push([
FUNCTION_TYPE,
...encodeFlatVector(func.args),
...encodeFlatVector(func.returns),
]);
typeIndexByKey.set(key, types.length - 1);
});
function getTypeIndex(func: TypedFunction): number {
const key = getSignatureKey(func);
const typeIndex = typeIndexByKey.get(key);
if (typeIndex == null) {
throw new Error(`Failed to get a type index for key ${key}`);
}
return typeIndex;
}
// https://webassembly.github.io/spec/core/binary/modules.html#import-section
const imports = [
...importedVars.map(([namespace, name]) => {
return [
...encodeString(namespace),
...encodeString(name),
...[GLOBAL_TYPE, VAL_TYPE.f64, MUTABILITY.var],
];
}),
...functionImports.map((func, i) => {
const typeIndex = getTypeIndex(func);
return [
...encodeString("shims"),
...encodeString(func.name),
...[TYPE_IDX, ...unsignedLEB128(typeIndex)],
];
}),
];
// https://webassembly.github.io/spec/core/binary/modules.html#function-section
//
// > Functions are referenced through function indices, starting with the smallest
// > index not referencing a function import.
const functions = [...localFuncs, ...moduleFuncs].map(func => {
const typeIndex = getTypeIndex(func);
return unsignedLEB128(typeIndex);
});
const memories = [
// Only one memory
[
0x01, // Indicates that we are specifying two values (initial/max)
...unsignedLEB128(WASM_MEMORY_SIZE), // Initial size
...unsignedLEB128(WASM_MEMORY_SIZE), // Max size
],
];
// https://webassembly.github.io/spec/core/binary/modules.html#global-section
const globalCount = varResolver.size() - importedVars.length;
const globals = Utils.times(globalCount, () => {
return [
VAL_TYPE.f64, // All eel values are float 64s
MUTABILITY.var, // All globals are mutable
...op.f64_const(0), // Initialize the global to zero
op.end, // All done
];
});
// https://webassembly.github.io/spec/core/binary/modules.html#binary-exportsec
const xports = [...moduleFuncs].map((func, i) => {
const funcIndex = i + functionImports.length + localFuncs.length;
return [
...encodeString(func.exportName),
EXPORT_TYPE.FUNC,
...unsignedLEB128(funcIndex),
];
});
/* Uncomment this to expose memory
xports.push([
...encodeString("memory"),
EXPORT_TYPE.MEMORY,
...unsignedLEB128(0),
]);
*/
// https://webassembly.github.io/spec/core/binary/modules.html#code-section
const codes = [...localFuncs, ...moduleFuncs].map(func => {
// TODO: We could collapse consecutive types here, or even move to a two
// pass approach where ids are resolved after the emitter is run.
const localTypes = (func.localVariables ?? []).map(type => {
return [...unsignedLEB128(1), type];
});
// It's a bit odd that every other section is an array of arrays and this
// one is an array of vectors. The spec says this is so that when navigating
// the binary functions can be skipped efficiently.
return encodeFlatVector([
...encodeNestedVector(localTypes),
...func.binary,
op.end,
]);
});
return new Uint8Array([
// Magic module header
...MAGIC,
// Version number
...WASM_VERSION,
...encodeSection(SECTION.TYPE, types),
...encodeSection(SECTION.IMPORT, imports),
...encodeSection(SECTION.FUNC, functions),
...encodeSection(SECTION.MEMORY, memories),
...encodeSection(SECTION.GLOBAL, globals),
...encodeSection(SECTION.EXPORT, xports),
...encodeSection(SECTION.CODE, codes),
]);
}