@swf2es/codegen 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/LICENSE.third-party +142 -0
- package/README.md +26 -2
- package/dist/codegen.d.ts +289 -0
- package/dist/codegen.js +308 -0
- package/dist/codegen.wasm +0 -0
- package/dist/index.d.ts +315 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +406 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -3
- package/src/index.ts +738 -0
- package/src/webassembly.d.ts +30 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compiles ABC and AVM1 bytecode to ES modules.
|
|
3
|
+
*
|
|
4
|
+
* The compiler itself is AssemblyScript (assembly/), built to codegen.wasm.
|
|
5
|
+
* This wrapper instantiates it and gives its exports their types. The same wasm runs as the browser JIT
|
|
6
|
+
* (a module per ABC as its SWF loads) and as the ahead-of-time compiler,
|
|
7
|
+
* so both produce identical output. See docs/architecture/overview.md.
|
|
8
|
+
*/
|
|
9
|
+
import { AIR_API_VERSION, API_VERSION, checkApiVersion } from "@swf2es/format";
|
|
10
|
+
import { instantiate } from "./codegen.js";
|
|
11
|
+
|
|
12
|
+
/** Bumped whenever generated code or the runtime ABI it calls changes. */
|
|
13
|
+
export const COMPILER_VERSION = "0.0.0";
|
|
14
|
+
|
|
15
|
+
// The API versions the player and the swf2es command reset the compiler
|
|
16
|
+
// with, which module cache keys name: Flash Player's, and AIR's, whose
|
|
17
|
+
// series has the names Flash Player's leaves out. format's, which the
|
|
18
|
+
// runtime takes too.
|
|
19
|
+
export { AIR_API_VERSION, API_VERSION };
|
|
20
|
+
|
|
21
|
+
/** The ABCs added to a compiler, by their indices: each one's hash, and whether it was added as a library. */
|
|
22
|
+
export interface AddedAbcs {
|
|
23
|
+
readonly hashes: readonly string[];
|
|
24
|
+
readonly builtins: readonly boolean[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The log's entries made while only the first `count` ABCs, the libraries,
|
|
29
|
+
* were added, as a key names them: by the SHA-256 of libraryLog's text,
|
|
30
|
+
* taken once, since they are the same in every later module's context.
|
|
31
|
+
*/
|
|
32
|
+
export interface LibraryDigest {
|
|
33
|
+
count: number;
|
|
34
|
+
digest: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The text whose SHA-256 is a LibraryDigest's, for ABC `index`, any added after the first `count`. */
|
|
38
|
+
export function libraryLog(codegen: Codegen, index: number, count: number): string {
|
|
39
|
+
return codegen.context(index, 0, count)?.log ?? "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The text of ABC `index`'s module cache key, whose UTF-8's SHA-256 is the
|
|
44
|
+
* key: what the module depends on, the compiler's identity, the API
|
|
45
|
+
* version, every ABC its domain sees, by hash and whether it is a library,
|
|
46
|
+
* those added after it included, its own place among them, and the
|
|
47
|
+
* compiler's log about them (see Codegen.context), the libraries' part by
|
|
48
|
+
* its digest once taken. The player's module cache and the swf2es command
|
|
49
|
+
* both key by it, so that the command's modules are the cache's. Null
|
|
50
|
+
* where there is no context.
|
|
51
|
+
*/
|
|
52
|
+
export function moduleKey(
|
|
53
|
+
codegen: Codegen,
|
|
54
|
+
index: number,
|
|
55
|
+
abcs: AddedAbcs,
|
|
56
|
+
libraries: LibraryDigest | null,
|
|
57
|
+
): string | null {
|
|
58
|
+
const context = codegen.context(index, libraries?.count ?? 0);
|
|
59
|
+
if (!context) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return JSON.stringify({
|
|
64
|
+
compiler: codegen.identity,
|
|
65
|
+
api: codegen.apiVersion,
|
|
66
|
+
abcs: context.abcs.map((i) => `${abcs.builtins[i] ? "builtin " : ""}${abcs.hashes[i]}`),
|
|
67
|
+
own: context.own,
|
|
68
|
+
libraries,
|
|
69
|
+
log: context.log,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A SWF's AVM1 action blocks, as format's readActions lists them: the SWF's
|
|
75
|
+
* version and each block's range in its decompressed bytes.
|
|
76
|
+
*/
|
|
77
|
+
export interface ActionBlocks {
|
|
78
|
+
readonly version: number;
|
|
79
|
+
readonly blocks: readonly { readonly offset: number; readonly length: number }[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What a SWF's action blocks compile to: one module, and each block's entry in it. */
|
|
83
|
+
export interface CompiledActions {
|
|
84
|
+
module: string;
|
|
85
|
+
/** By block index: the block's entry in the module, as it compiles alone. */
|
|
86
|
+
entries: string[];
|
|
87
|
+
/** By key, each unit in the module's units, as it compiles alone. */
|
|
88
|
+
units: Map<string, ModuleUnit>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Where control goes on in a unit, as V.far is called: an offset in the
|
|
93
|
+
* SWF's decompressed bytes, where the region it goes on in ends, where its
|
|
94
|
+
* actions' bytes end, and whether it runs as a SWF 5's function. The
|
|
95
|
+
* module's units are keyed by these, joined with commas.
|
|
96
|
+
*/
|
|
97
|
+
export interface ActionUnit {
|
|
98
|
+
readonly at: number;
|
|
99
|
+
readonly end: number;
|
|
100
|
+
readonly bound: number;
|
|
101
|
+
readonly five: boolean;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* A unit's code, and what it depends on: it runs as well for any region
|
|
106
|
+
* that ends at or past `high` whose actions' bytes end at or past `reach`,
|
|
107
|
+
* -1 for only the one it was compiled for.
|
|
108
|
+
*/
|
|
109
|
+
export interface CompiledUnit {
|
|
110
|
+
code: string;
|
|
111
|
+
high: number;
|
|
112
|
+
reach: number;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A unit of a module's units, and what it was compiled for. */
|
|
116
|
+
export interface ModuleUnit extends CompiledUnit {
|
|
117
|
+
unit: ActionUnit;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The text of the module cache key of a SWF's AVM1 module, whose UTF-8's
|
|
122
|
+
* SHA-256 is the key: the compiler's identity, the SWF's version, which
|
|
123
|
+
* picks the operations its code calls, and `hash`, the SHA-256 of the
|
|
124
|
+
* SWF's decompressed bytes, which hold its blocks. An AVM1 module links to
|
|
125
|
+
* nothing, so nothing else goes in.
|
|
126
|
+
*/
|
|
127
|
+
export function actionsKey(codegen: Codegen, hash: string, version: number): string {
|
|
128
|
+
return JSON.stringify({ compiler: codegen.identity, avm1: version, swf: hash });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A definition an application domain has found by name, or as a type,
|
|
133
|
+
* which is not the first one from the root down, as the runtime reports it:
|
|
134
|
+
* what the domain's later ABCs compile against (see Codegen.found).
|
|
135
|
+
*/
|
|
136
|
+
export interface FoundDefinition {
|
|
137
|
+
/** The application domain that found it. */
|
|
138
|
+
domain: number;
|
|
139
|
+
/** The name's namespace: its kind (0 public, 1 package-internal, ...) and URI. */
|
|
140
|
+
nsKind: number;
|
|
141
|
+
uri: string;
|
|
142
|
+
name: string;
|
|
143
|
+
/** The ABC that defines it, by its position among those added. */
|
|
144
|
+
abc: number;
|
|
145
|
+
/** Found as a type (avmplus' cached traits), else by name (cached scripts). */
|
|
146
|
+
asType: boolean;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface AbcVersion {
|
|
150
|
+
major: number;
|
|
151
|
+
minor: number;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** What an ABC compiles to: its module, the module's source map, and each compiled method body's entry in it. */
|
|
155
|
+
export interface Compiled {
|
|
156
|
+
module: string;
|
|
157
|
+
/** JSON: the module's lines mapped to the AS3 lines debugline names. */
|
|
158
|
+
sourceMap: string;
|
|
159
|
+
/** By method body index: the body's entry in the module's F, as the JIT compiles it alone. */
|
|
160
|
+
entries: Map<number, string>;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* What ABC `index`'s module depends on in the domain beyond the ABCs'
|
|
165
|
+
* bytes (see Codegen.context).
|
|
166
|
+
*/
|
|
167
|
+
export interface ModuleContext {
|
|
168
|
+
/** The ABCs its application domain sees, by their positions among those added, in load order. */
|
|
169
|
+
abcs: number[];
|
|
170
|
+
/** Its own place in `abcs`. */
|
|
171
|
+
own: number;
|
|
172
|
+
/**
|
|
173
|
+
* The domain's log about those ABCs, in order: the lazy answers fixed so
|
|
174
|
+
* far and the findings recorded, each with how many of `abcs` there were.
|
|
175
|
+
*/
|
|
176
|
+
log: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A module compiled, and what compiling it fixed in the domain, to replay in its place (Codegen.replay); null where it cannot be. */
|
|
180
|
+
export interface LoggedModule {
|
|
181
|
+
module: string;
|
|
182
|
+
log: string | null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The compiler. ABCs are added to a domain in the order the SWF loads them
|
|
187
|
+
* (the builtins first), and the last one added compiles, whole or a method
|
|
188
|
+
* at a time, to code that is byte for byte the same either way.
|
|
189
|
+
*/
|
|
190
|
+
export interface Codegen {
|
|
191
|
+
/**
|
|
192
|
+
* The identity of this codegen.wasm, a hash of its bytes that its build
|
|
193
|
+
* stamps it with; null for one built without it.
|
|
194
|
+
*/
|
|
195
|
+
readonly identity: string | null;
|
|
196
|
+
/** The version at the start of an ABC block, or null if it is too short. */
|
|
197
|
+
abcVersion(abc: Uint8Array): AbcVersion | null;
|
|
198
|
+
/**
|
|
199
|
+
* Start a domain whose user ABCs have API version `apiVersion`: Flash
|
|
200
|
+
* Player's, API_VERSION, by default, or AIR's, AIR_API_VERSION; one
|
|
201
|
+
* that is neither series' (checkApiVersion) is a RangeError.
|
|
202
|
+
*/
|
|
203
|
+
reset(apiVersion?: number): void;
|
|
204
|
+
/** The API version of the last reset. */
|
|
205
|
+
readonly apiVersion: number;
|
|
206
|
+
/**
|
|
207
|
+
* Add an ABC, loaded into application domain `appDomain` (0, the root, by
|
|
208
|
+
* default), linking it against what that domain sees of those before it;
|
|
209
|
+
* 0, or the VerifyError it was rejected with (then it is not added).
|
|
210
|
+
* Throws for an application domain never made, or dropped.
|
|
211
|
+
*/
|
|
212
|
+
add(abc: Uint8Array, builtin?: boolean, appDomain?: number): number;
|
|
213
|
+
/** A new application domain, a child of `parent`: its number. Throws for a `parent` never made, or dropped. */
|
|
214
|
+
childDomain(parent: number): number;
|
|
215
|
+
/**
|
|
216
|
+
* Drop application domain `appDomain` and its descendants once nothing
|
|
217
|
+
* can run their code: no other sees their ABCs, which compile no more,
|
|
218
|
+
* and compact reuses the memory they took. The indices of the ABCs added
|
|
219
|
+
* after them do not change, and neither does what those compile to.
|
|
220
|
+
* Nothing, if `epoch` is given and the domain was reset since.
|
|
221
|
+
*/
|
|
222
|
+
dropDomain(appDomain: number, epoch?: number): void;
|
|
223
|
+
/**
|
|
224
|
+
* How many times `reset` has started a domain: a host that keeps the
|
|
225
|
+
* number a domain had passes it to dropDomain, which then drops nothing
|
|
226
|
+
* if the domain was reset since, as when another player took the Codegen
|
|
227
|
+
* over and its numbers are another domain's.
|
|
228
|
+
*/
|
|
229
|
+
readonly epoch: number;
|
|
230
|
+
/**
|
|
231
|
+
* Rebuild the domain without the ABCs of dropped and evicted domains, if
|
|
232
|
+
* memory would otherwise grow and they hold enough to keep it from
|
|
233
|
+
* growing: whether it did. A rebuild links every live ABC again, tens of
|
|
234
|
+
* milliseconds for a large application, so a host calls this when it has
|
|
235
|
+
* time, as when idle; nothing else does.
|
|
236
|
+
*/
|
|
237
|
+
compact(): boolean;
|
|
238
|
+
/**
|
|
239
|
+
* What the ABCs hold of codegen's memory, roughly, in bytes: the live
|
|
240
|
+
* ones', and the dropped and evicted domains' that compact would free;
|
|
241
|
+
* and the size of codegen's memory, which never shrinks.
|
|
242
|
+
*/
|
|
243
|
+
usage(): { live: number; dead: number; memory: number };
|
|
244
|
+
/**
|
|
245
|
+
* Evict application domain `appDomain` and its descendants while the host
|
|
246
|
+
* has no use for them, as once their ABCs are compiled: as dropped, but
|
|
247
|
+
* reviveDomain makes one live again. compact reuses what they took
|
|
248
|
+
* meanwhile, all but their findings, what was first resolved of them,
|
|
249
|
+
* and their ABCs' places.
|
|
250
|
+
*/
|
|
251
|
+
evictDomain(appDomain: number): void;
|
|
252
|
+
/** Whether application domain `appDomain` is live: made, and neither evicted nor dropped. */
|
|
253
|
+
isLive(appDomain: number): boolean;
|
|
254
|
+
/**
|
|
255
|
+
* Make evicted application domain `appDomain` live again, with its evicted
|
|
256
|
+
* ancestors, the first of them a child of a live domain: given all their
|
|
257
|
+
* ABCs again by their indices, as they were added, their ABCs link as
|
|
258
|
+
* they did, by one rebuild at most, and what they found holds. Throws if
|
|
259
|
+
* they cannot be.
|
|
260
|
+
*/
|
|
261
|
+
reviveDomain(appDomain: number, abcs: Map<number, Uint8Array>): void;
|
|
262
|
+
/** Record a definition an application domain has found; it holds for the ABCs added after. */
|
|
263
|
+
found(definition: FoundDefinition): void;
|
|
264
|
+
/**
|
|
265
|
+
* ABC `index` compiled whole, the last added by default, against every ABC
|
|
266
|
+
* added so far, later ones included, so a SWF's DoABCs may all be added
|
|
267
|
+
* before any compiles; `hashes` are the ABCs' hashes in load order, as
|
|
268
|
+
* the cache key names them.
|
|
269
|
+
*/
|
|
270
|
+
compile(hashes?: string[], index?: number): Compiled;
|
|
271
|
+
/**
|
|
272
|
+
* ABC `index`'s module alone, as `compile` writes it, for a host that
|
|
273
|
+
* loads it and needs neither its source map nor its entries: writing
|
|
274
|
+
* those too would add a large ABC's worth to codegen's memory, which
|
|
275
|
+
* never shrinks.
|
|
276
|
+
*/
|
|
277
|
+
compileModule(hashes?: string[], index?: number): string;
|
|
278
|
+
/**
|
|
279
|
+
* An opt-in JIT module's tables and lazy factories. Its default export
|
|
280
|
+
* takes (rt, jit); jit(body, bindings) returns that body's factory.
|
|
281
|
+
* Verification still runs now to fix scopes against the current domain.
|
|
282
|
+
* This module is not an AOT module and must not enter the module cache.
|
|
283
|
+
*/
|
|
284
|
+
compileLazyModule(hashes?: string[], index?: number): string;
|
|
285
|
+
/**
|
|
286
|
+
* Method bodies of ABC `index` (the last added by default) compiled
|
|
287
|
+
* alone, as the optional lazy JIT compiles each on first factory use;
|
|
288
|
+
* a native, unverified or unknown one is left out. Each is byte for byte
|
|
289
|
+
* its entry in the whole module.
|
|
290
|
+
*/
|
|
291
|
+
compileMethods(bodies: number[], index?: number): Map<number, string>;
|
|
292
|
+
/**
|
|
293
|
+
* What ABC `index`'s module depends on beyond the ABCs' bytes, for a
|
|
294
|
+
* cache key: besides the ABCs its domain sees, what the domain has
|
|
295
|
+
* fixed of what resolves lazily, which a later ABC in an ancestor would
|
|
296
|
+
* change were it first asked now. Its log only of the entries made with
|
|
297
|
+
* more than `after` ABCs added, and `upTo` at most unless negative. Null
|
|
298
|
+
* for an ABC never added.
|
|
299
|
+
*/
|
|
300
|
+
context(index?: number, after?: number, upTo?: number): ModuleContext | null;
|
|
301
|
+
/**
|
|
302
|
+
* A string that changes whenever a context might: a host that took one
|
|
303
|
+
* and finds this the same since need not take it again.
|
|
304
|
+
*/
|
|
305
|
+
revision(): string;
|
|
306
|
+
/** compileModule, with what the compile fixed in the domain (see replay). */
|
|
307
|
+
compileModuleLogged(hashes?: string[], index?: number): LoggedModule;
|
|
308
|
+
/**
|
|
309
|
+
* Fix in the domain what compiling ABC `index` did, from the log
|
|
310
|
+
* compileModuleLogged gave with its module, in place of compiling it:
|
|
311
|
+
* the domain is then as the compile would have left it, if its context
|
|
312
|
+
* is the one that compile had. False if the log is malformed or an
|
|
313
|
+
* entry fails, after doing those before it.
|
|
314
|
+
*/
|
|
315
|
+
replay(log: string, index?: number): boolean;
|
|
316
|
+
/**
|
|
317
|
+
* A SWF's AVM1 action blocks compiled to one module, `bytes` the SWF's
|
|
318
|
+
* decompressed bytes, which the blocks' ranges are of (format's
|
|
319
|
+
* `Swf.bytes`), with each block's entry in it. Needs no domain: AVM1 code
|
|
320
|
+
* links to nothing.
|
|
321
|
+
*/
|
|
322
|
+
compileActions(bytes: Uint8Array, actions: ActionBlocks): CompiledActions;
|
|
323
|
+
/** compileActions' module alone. */
|
|
324
|
+
compileActionsModule(bytes: Uint8Array, actions: ActionBlocks): string;
|
|
325
|
+
/**
|
|
326
|
+
* Blocks of a SWF compiled alone, by index, as a JIT may compile each
|
|
327
|
+
* when it first runs: each byte for byte its entry in the SWF's module.
|
|
328
|
+
* An index with no block is left out.
|
|
329
|
+
*/
|
|
330
|
+
compileActionBlocks(
|
|
331
|
+
bytes: Uint8Array,
|
|
332
|
+
actions: ActionBlocks,
|
|
333
|
+
blocks: number[],
|
|
334
|
+
): Map<number, string>;
|
|
335
|
+
/**
|
|
336
|
+
* A unit of a SWF compiled alone, as V.far asks for it when control first
|
|
337
|
+
* goes on there: byte for byte its entry in the module's units.
|
|
338
|
+
*/
|
|
339
|
+
compileActionUnit(bytes: Uint8Array, actions: ActionBlocks, unit: ActionUnit): CompiledUnit;
|
|
340
|
+
/**
|
|
341
|
+
* Let go of the copy of a SWF compileActionBlocks and compileActionUnit
|
|
342
|
+
* keep, as the host lets the SWF go; it is let go too once `bytes` are
|
|
343
|
+
* collected, and when blocks or units of other SWFs are asked for since.
|
|
344
|
+
*/
|
|
345
|
+
releaseActions(bytes: Uint8Array): void;
|
|
346
|
+
/** How many SWFs compileActionBlocks and compileActionUnit keep a copy of, and how many times they have copied one in. */
|
|
347
|
+
heldActions(): { held: number; copies: number };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Instantiate the compiler from a compiled codegen.wasm module. Loading the
|
|
352
|
+
* bytes is left to the caller (fetch in browsers, fs in node), which keeps
|
|
353
|
+
* this package free of I/O. The file ships as `@swf2es/codegen/codegen.wasm`.
|
|
354
|
+
*/
|
|
355
|
+
export async function createCodegen(module: WebAssembly.Module): Promise<Codegen> {
|
|
356
|
+
const wasm = await instantiate(module, { env: {} });
|
|
357
|
+
const version = wasm.compilerVersion();
|
|
358
|
+
if (version !== COMPILER_VERSION) {
|
|
359
|
+
throw new Error(
|
|
360
|
+
`codegen.wasm is version ${version} but the wrapper expects ${COMPILER_VERSION}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Of the same version, but built before its modules were handed out in
|
|
365
|
+
// place: its *Bytes exports return copies, which this wrapper cannot read.
|
|
366
|
+
// Asked of the module, as the bindings define every export they know of.
|
|
367
|
+
if (!WebAssembly.Module.exports(module).some((e) => e.name === "outputLength")) {
|
|
368
|
+
throw new Error(
|
|
369
|
+
"codegen.wasm has no outputLength export, which the wrapper expects: it is older than the wrapper",
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// codegen.wasm uses AssemblyScript's minimal runtime, which collects
|
|
374
|
+
// garbage only when asked. Nothing runs in wasm between calls, so that is
|
|
375
|
+
// when: every COLLECT_EVERY calls, and after a call that grew memory.
|
|
376
|
+
// Memory never shrinks, so collecting only on growth would let each
|
|
377
|
+
// cycle's garbage grow it a little further.
|
|
378
|
+
let size = wasm.memory.buffer.byteLength;
|
|
379
|
+
let calls = 0;
|
|
380
|
+
let methodBytes = 0;
|
|
381
|
+
// Whether the domain has an ABC to compile: asking before is a mistake worth a message, not a trap.
|
|
382
|
+
let added = 0;
|
|
383
|
+
const last = (what: string) => {
|
|
384
|
+
if (added === 0) {
|
|
385
|
+
throw new Error(`${what}: no ABC has been added to the domain`);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
// An ABC by its position among those added, -1 for the last.
|
|
389
|
+
const indexOf = (index: number) => {
|
|
390
|
+
if (index >= 0 && (!Number.isInteger(index) || index >= added)) {
|
|
391
|
+
throw new Error(`ABC ${index}: only ${added} have been added to the domain`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return index < 0 ? -1 : index;
|
|
395
|
+
};
|
|
396
|
+
// ABC `index`'s module, its source map and entries kept for `keep`. A
|
|
397
|
+
// module is never empty: the ABC was of a dropped application domain.
|
|
398
|
+
const moduleOf = (hashes: string[], index: number, keep: boolean, lazy = false) => {
|
|
399
|
+
// Only the hashes the module names: joining every ABC's for each
|
|
400
|
+
// compile took megabytes of codegen's memory a few thousand loads on.
|
|
401
|
+
const at = indexOf(index);
|
|
402
|
+
const own = at < 0 ? added - 1 : at;
|
|
403
|
+
const linked = collected(wasm.domainLinked(at));
|
|
404
|
+
const named = [hashes[own] ?? ""];
|
|
405
|
+
for (const i of linked ? linked.split(",") : []) {
|
|
406
|
+
named.push(hashes[Number(i)] ?? "");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Its bytes decoded here: lifted as a string, a module of megabytes took
|
|
410
|
+
// longer than compiling it. The emitter writes only well-formed UTF-8,
|
|
411
|
+
// which decodes alike either way.
|
|
412
|
+
const address = lazy
|
|
413
|
+
? wasm.domainLazyModuleBytes(named.join("\n"), at)
|
|
414
|
+
: wasm.domainModuleBytes(named.join("\n"), at, keep, true);
|
|
415
|
+
const module = collected(handed(address, wasm.outputLength()), lazy);
|
|
416
|
+
if (module === "") {
|
|
417
|
+
throw new Error(`ABC ${index}: its application domain was dropped`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return module;
|
|
421
|
+
};
|
|
422
|
+
const ranges = (actions: ActionBlocks) =>
|
|
423
|
+
[
|
|
424
|
+
Int32Array.from(actions.blocks, (b) => b.offset),
|
|
425
|
+
Int32Array.from(actions.blocks, (b) => b.length),
|
|
426
|
+
] as const;
|
|
427
|
+
// The SWFs whose blocks or units compileActionBlocks and compileActionUnit
|
|
428
|
+
// compile, which wasm holds by a handle each: the few used last, so that
|
|
429
|
+
// blocks and units of SWFs that alternate do not copy them in again, each
|
|
430
|
+
// let go as its bytes are collected.
|
|
431
|
+
const holding = new WeakMap<Uint8Array, { handle: number; actions: ActionBlocks }>();
|
|
432
|
+
const holds: number[] = [];
|
|
433
|
+
let handles = 0;
|
|
434
|
+
let copies = 0;
|
|
435
|
+
const release = (handle: number) => {
|
|
436
|
+
const at = holds.indexOf(handle);
|
|
437
|
+
if (at >= 0) {
|
|
438
|
+
holds.splice(at, 1);
|
|
439
|
+
wasm.actionsRelease(handle);
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
const collectedSwfs = new FinalizationRegistry<number>(release);
|
|
443
|
+
const hold = (bytes: Uint8Array, actions: ActionBlocks): number => {
|
|
444
|
+
const known = holding.get(bytes);
|
|
445
|
+
if (known !== undefined && known.actions === actions && holds.includes(known.handle)) {
|
|
446
|
+
holds.splice(holds.indexOf(known.handle), 1);
|
|
447
|
+
holds.push(known.handle);
|
|
448
|
+
return known.handle;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (known !== undefined) {
|
|
452
|
+
release(known.handle);
|
|
453
|
+
collectedSwfs.unregister(known);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const handle = ++handles;
|
|
457
|
+
const [offsets, lengths] = ranges(actions);
|
|
458
|
+
wasm.actionsHold(handle, bytes, offsets, lengths);
|
|
459
|
+
copies++;
|
|
460
|
+
const record = { handle, actions };
|
|
461
|
+
holding.set(bytes, record);
|
|
462
|
+
collectedSwfs.register(bytes, handle, record);
|
|
463
|
+
holds.push(handle);
|
|
464
|
+
while (holds.length > HELD_SWFS) {
|
|
465
|
+
release(holds[0]);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return handle;
|
|
469
|
+
};
|
|
470
|
+
const actionsModule = (bytes: Uint8Array, actions: ActionBlocks, keep: boolean) => {
|
|
471
|
+
const [offsets, lengths] = ranges(actions);
|
|
472
|
+
const address = wasm.actionsModuleBytes(bytes, offsets, lengths, actions.version, keep);
|
|
473
|
+
return collected(handed(address, wasm.outputLength()));
|
|
474
|
+
};
|
|
475
|
+
// UTF-8 a call handed out in codegen's memory, not copied (see handOut in
|
|
476
|
+
// assembly/output.ts), decoded before anything else calls in: the next
|
|
477
|
+
// call may grow memory or move them, and a collection may free them.
|
|
478
|
+
const handed = (address: number, length: number) =>
|
|
479
|
+
length === 0 ? "" : utf8.decode(new Uint8Array(wasm.memory.buffer, address, length));
|
|
480
|
+
const stamps = WebAssembly.Module.customSections(module, "swf2es.build");
|
|
481
|
+
const identity = stamps.length ? String.fromCharCode(...new Uint8Array(stamps[0])) : null;
|
|
482
|
+
let epoch = 0;
|
|
483
|
+
let apiVersion = API_VERSION;
|
|
484
|
+
const collected = <T>(result: T, reclaim = false): T => {
|
|
485
|
+
const grown = wasm.memory.buffer.byteLength;
|
|
486
|
+
if (reclaim || grown > size || ++calls >= COLLECT_EVERY) {
|
|
487
|
+
wasm.__collect();
|
|
488
|
+
size = grown;
|
|
489
|
+
calls = 0;
|
|
490
|
+
methodBytes = 0;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return result;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
identity,
|
|
498
|
+
abcVersion(abc) {
|
|
499
|
+
const packed = collected(wasm.abcVersion(abc));
|
|
500
|
+
return packed < 0 ? null : { major: packed >>> 16, minor: packed & 0xffff };
|
|
501
|
+
},
|
|
502
|
+
get epoch() {
|
|
503
|
+
return epoch;
|
|
504
|
+
},
|
|
505
|
+
reset(version = API_VERSION) {
|
|
506
|
+
checkApiVersion(version);
|
|
507
|
+
added = 0;
|
|
508
|
+
epoch++;
|
|
509
|
+
apiVersion = version;
|
|
510
|
+
collected(wasm.domainReset(version));
|
|
511
|
+
},
|
|
512
|
+
get apiVersion() {
|
|
513
|
+
return apiVersion;
|
|
514
|
+
},
|
|
515
|
+
add(abc, builtin = false, appDomain = 0) {
|
|
516
|
+
const error = collected(wasm.domainAdd(abc, builtin, appDomain));
|
|
517
|
+
if (error < 0) {
|
|
518
|
+
throw new Error(`application domain ${appDomain}: never made, or dropped`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (error === 0) {
|
|
522
|
+
added++;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return error;
|
|
526
|
+
},
|
|
527
|
+
childDomain(parent) {
|
|
528
|
+
const domain = collected(wasm.domainChild(parent));
|
|
529
|
+
if (domain < 0) {
|
|
530
|
+
throw new Error(`application domain ${parent}: never made, or dropped`);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return domain;
|
|
534
|
+
},
|
|
535
|
+
dropDomain(appDomain, of = epoch) {
|
|
536
|
+
if (of === epoch) {
|
|
537
|
+
collected(wasm.domainDrop(appDomain));
|
|
538
|
+
}
|
|
539
|
+
},
|
|
540
|
+
evictDomain(appDomain) {
|
|
541
|
+
collected(wasm.domainEvict(appDomain));
|
|
542
|
+
},
|
|
543
|
+
usage() {
|
|
544
|
+
const [live, dead, memory] = collected(wasm.domainUsage()).split(" ").map(Number);
|
|
545
|
+
return { live, dead, memory };
|
|
546
|
+
},
|
|
547
|
+
compact() {
|
|
548
|
+
// A rebuild makes new tables while the old are live: what earlier
|
|
549
|
+
// calls left is collected first, and the old tables once it is done,
|
|
550
|
+
// before the next call's allocations would grow memory past them.
|
|
551
|
+
if (!collected(wasm.domainWantsCompact())) {
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
wasm.__collect();
|
|
556
|
+
calls = 0;
|
|
557
|
+
if (!wasm.domainCompact()) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
wasm.__collect();
|
|
562
|
+
size = wasm.memory.buffer.byteLength;
|
|
563
|
+
calls = 0;
|
|
564
|
+
return true;
|
|
565
|
+
},
|
|
566
|
+
isLive(appDomain) {
|
|
567
|
+
return collected(wasm.domainState(appDomain)) === 0;
|
|
568
|
+
},
|
|
569
|
+
reviveDomain(appDomain, abcs) {
|
|
570
|
+
// Its ABCs' bytes are copied into the compiler only if a rebuild let go of them.
|
|
571
|
+
let revived = collected(wasm.domainRevive(appDomain));
|
|
572
|
+
if (revived < 0) {
|
|
573
|
+
for (const [index, bytes] of abcs) {
|
|
574
|
+
const error = collected(wasm.domainRestore(index, bytes));
|
|
575
|
+
if (error !== 0) {
|
|
576
|
+
throw new Error(`ABC ${index}: not of an evicted domain, or rejected (${error})`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
revived = collected(wasm.domainRevive(appDomain));
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (revived < 0) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`application domain ${appDomain}: not evicted, its parent not live, or an ABC of it not given`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (revived > 0) {
|
|
590
|
+
wasm.__collect();
|
|
591
|
+
size = wasm.memory.buffer.byteLength;
|
|
592
|
+
calls = 0;
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
found(d) {
|
|
596
|
+
collected(wasm.domainFound(d.domain, d.nsKind, d.uri, d.name, d.abc, d.asType));
|
|
597
|
+
},
|
|
598
|
+
compile(hashes = [], index = -1) {
|
|
599
|
+
last("compile");
|
|
600
|
+
// Each part a call of its own, collected after it: the module's garbage
|
|
601
|
+
// is gone before its entries are written.
|
|
602
|
+
const module = moduleOf(hashes, index, true);
|
|
603
|
+
const sourceMap = collected(wasm.domainSourceMap());
|
|
604
|
+
const entries = parseEntries(collected(wasm.domainModuleEntries()));
|
|
605
|
+
return { module, sourceMap, entries };
|
|
606
|
+
},
|
|
607
|
+
compileModule(hashes = [], index = -1) {
|
|
608
|
+
last("compileModule");
|
|
609
|
+
return moduleOf(hashes, index, false);
|
|
610
|
+
},
|
|
611
|
+
compileLazyModule(hashes = [], index = -1) {
|
|
612
|
+
last("compileLazyModule");
|
|
613
|
+
return moduleOf(hashes, index, false, true);
|
|
614
|
+
},
|
|
615
|
+
context(index = -1, after = 0, upTo = -1) {
|
|
616
|
+
if (added === 0) {
|
|
617
|
+
return null;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const text = collected(wasm.domainContext(indexOf(index), after, upTo));
|
|
621
|
+
if (text === "") {
|
|
622
|
+
return null;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const first = text.indexOf("\n");
|
|
626
|
+
const second = text.indexOf("\n", first + 1);
|
|
627
|
+
const abcs = text.slice(0, first);
|
|
628
|
+
return {
|
|
629
|
+
abcs: abcs ? abcs.split(",").map(Number) : [],
|
|
630
|
+
own: Number(text.slice(first + 1, second < 0 ? text.length : second)),
|
|
631
|
+
log: second < 0 ? "" : text.slice(second + 1),
|
|
632
|
+
};
|
|
633
|
+
},
|
|
634
|
+
revision() {
|
|
635
|
+
return `${epoch} ${wasm.domainRevision()}`;
|
|
636
|
+
},
|
|
637
|
+
compileModuleLogged(hashes = [], index = -1) {
|
|
638
|
+
last("compileModuleLogged");
|
|
639
|
+
const mark = wasm.domainLogMark();
|
|
640
|
+
const module = moduleOf(hashes, index, false);
|
|
641
|
+
const log = collected(wasm.domainLogSince(mark, indexOf(index)));
|
|
642
|
+
return { module, log: log === "!" ? null : log };
|
|
643
|
+
},
|
|
644
|
+
replay(log, index = -1) {
|
|
645
|
+
last("replay");
|
|
646
|
+
return collected(wasm.domainReplay(log, indexOf(index)));
|
|
647
|
+
},
|
|
648
|
+
compileMethods(bodies, index = -1) {
|
|
649
|
+
last("compileMethods");
|
|
650
|
+
const indices = bodies.filter((b) => Number.isInteger(b) && b >= 0);
|
|
651
|
+
if (indices.length === 0) {
|
|
652
|
+
return new Map();
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const address = wasm.domainEmitEachBytes(indices.join(","), true, indexOf(index));
|
|
656
|
+
const length = wasm.outputLength();
|
|
657
|
+
methodBytes += length;
|
|
658
|
+
const text = handed(address, length);
|
|
659
|
+
return parseEntries(collected(text, methodBytes >= COLLECT_METHOD_BYTES));
|
|
660
|
+
},
|
|
661
|
+
compileActions(bytes, actions) {
|
|
662
|
+
const module = actionsModule(bytes, actions, true);
|
|
663
|
+
const entries = parseEntries(collected(wasm.actionsModuleEntries()));
|
|
664
|
+
const units = new Map<string, ModuleUnit>();
|
|
665
|
+
for (const item of collected(wasm.actionsModuleUnits()).split("\u0002")) {
|
|
666
|
+
const [key, numbers, limits, code] = item.split("\u0001");
|
|
667
|
+
if (code !== undefined) {
|
|
668
|
+
const [at, end, bound, five] = numbers.split(",").map(Number);
|
|
669
|
+
const [high, reach] = limits.split(",").map(Number);
|
|
670
|
+
units.set(key, { code, high, reach, unit: { at, end, bound, five: five !== 0 } });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return { module, entries: actions.blocks.map((_, k) => entries.get(k) ?? ""), units };
|
|
675
|
+
},
|
|
676
|
+
compileActionsModule(bytes, actions) {
|
|
677
|
+
return actionsModule(bytes, actions, false);
|
|
678
|
+
},
|
|
679
|
+
compileActionBlocks(bytes, actions, blocks) {
|
|
680
|
+
const indices = blocks.filter((b) => Number.isInteger(b) && b >= 0);
|
|
681
|
+
if (indices.length === 0) {
|
|
682
|
+
return new Map();
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// The SWF held, as for units: a block needs it whole, and copying it
|
|
686
|
+
// in for each block cost a copy of the SWF in codegen's memory
|
|
687
|
+
// beside the one held for units, and the time to copy it.
|
|
688
|
+
const handle = hold(bytes, actions);
|
|
689
|
+
const address = wasm.actionsHeldEmitEachBytes(handle, actions.version, indices.join(","));
|
|
690
|
+
const text = handed(address, wasm.outputLength());
|
|
691
|
+
// A block's analysis can go before the next block runs.
|
|
692
|
+
return parseEntries(collected(text, true));
|
|
693
|
+
},
|
|
694
|
+
compileActionUnit(bytes, actions, { at, end, bound, five }) {
|
|
695
|
+
const handle = hold(bytes, actions);
|
|
696
|
+
const address = wasm.actionsHeldUnitBytes(handle, actions.version, at, end, bound, five);
|
|
697
|
+
const code = handed(address, wasm.outputLength());
|
|
698
|
+
const [high, reach] = wasm.actionsUnitLimits();
|
|
699
|
+
return collected({ code, high, reach });
|
|
700
|
+
},
|
|
701
|
+
releaseActions(bytes) {
|
|
702
|
+
const known = holding.get(bytes);
|
|
703
|
+
if (known !== undefined) {
|
|
704
|
+
release(known.handle);
|
|
705
|
+
holding.delete(bytes);
|
|
706
|
+
collectedSwfs.unregister(known);
|
|
707
|
+
}
|
|
708
|
+
},
|
|
709
|
+
heldActions() {
|
|
710
|
+
return { held: collected(wasm.actionsHeldCount()), copies };
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/** Entries as the wasm writes them: for each, the body's index, U+0001, its entry, U+0002. */
|
|
716
|
+
function parseEntries(text: string): Map<number, string> {
|
|
717
|
+
const entries = new Map<number, string>();
|
|
718
|
+
for (const item of text.split("\u0002")) {
|
|
719
|
+
const at = item.indexOf("\u0001");
|
|
720
|
+
if (at > 0) {
|
|
721
|
+
entries.set(Number(item.slice(0, at)), item.slice(at + 1));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return entries;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const COLLECT_EVERY = 64;
|
|
729
|
+
/** How many SWFs whose blocks or units are compiled codegen holds a copy of at most. */
|
|
730
|
+
const HELD_SWFS = 4;
|
|
731
|
+
// Large method entries should not accumulate sixty-four calls of garbage.
|
|
732
|
+
const COLLECT_METHOD_BYTES = 256 * 1024;
|
|
733
|
+
|
|
734
|
+
// TextDecoder is the web's and node's alike, though not ECMAScript's,
|
|
735
|
+
// whose library alone the compiler's wrapper is typed against.
|
|
736
|
+
declare const TextDecoder: new () => { decode(bytes: Uint8Array): string };
|
|
737
|
+
|
|
738
|
+
const utf8 = new TextDecoder();
|