@onda-lang/wasm-compiler 0.7.5 → 0.8.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/README.md +53 -0
- package/THIRD_PARTY_NOTICES.md +4 -2
- package/api.md +407 -0
- package/dist/backend/compiler/core.js +2544 -0
- package/dist/backend/compiler/index.js +1900 -0
- package/dist/backend/compiler/lowering.js +2297 -0
- package/dist/backend/compiler/shared.js +247 -0
- package/dist/backend/constants.js +1 -1
- package/dist/backend/index.js +1 -6012
- package/dist/build.json +3 -3
- package/dist/frontend/README.md +8 -5
- package/dist/frontend/onda_compiler_web.d.ts +21 -12
- package/dist/frontend/onda_compiler_web.js +118 -10
- package/dist/frontend/onda_compiler_web_bg.wasm +0 -0
- package/dist/frontend/onda_compiler_web_bg.wasm.d.ts +10 -7
- package/dist/frontend/package.json +2 -2
- package/dist/licenses/ONDA-LICENSE.txt +21 -0
- package/dist/licenses/RUST-DEPENDENCIES.txt +953 -0
- package/dist/version.js +1 -1
- package/package.json +6 -5
- package/src/index.d.ts +61 -1
- package/src/index.js +307 -24
- package/src/worker.js +31 -0
- /package/dist/licenses/{BINARYEN-LICENSE → BINARYEN-LICENSE.txt} +0 -0
- /package/dist/licenses/{LIBM-LICENSE → LIBM-LICENSE.txt} +0 -0
|
@@ -0,0 +1,2544 @@
|
|
|
1
|
+
import binaryen from "binaryen";
|
|
2
|
+
import {
|
|
3
|
+
SUPPORTED_MIR_SCHEMA_VERSION,
|
|
4
|
+
OndaBinaryenError,
|
|
5
|
+
ONDA_MATH_KERNEL_WASM,
|
|
6
|
+
PROCESSOR_EXECUTION_OK,
|
|
7
|
+
PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE,
|
|
8
|
+
validateProcessorMetadata,
|
|
9
|
+
PAGE_BYTES,
|
|
10
|
+
STATIC_BASE,
|
|
11
|
+
MATH_KERNEL_RESERVED_END,
|
|
12
|
+
MATH_KERNEL_DATA_SEGMENT,
|
|
13
|
+
MATH_KERNEL_STACK_GLOBAL,
|
|
14
|
+
MAX_MEMORY_PAGES,
|
|
15
|
+
DEFAULT_OPTIMIZE_LEVEL,
|
|
16
|
+
ONDA_PROCESS_FULL_BLOCK,
|
|
17
|
+
DELEGATE_BATCH_USED_OFFSET,
|
|
18
|
+
DELEGATE_BATCH_RECORD_COUNT_OFFSET,
|
|
19
|
+
DELEGATE_BATCH_OVERFLOW_OFFSET,
|
|
20
|
+
EXECUTION_OUTPUT_DELEGATE_BATCH_OFFSET,
|
|
21
|
+
EXECUTION_OUTPUT_PRINT_BATCH_OFFSET,
|
|
22
|
+
EXECUTION_OUTPUT_SEQUENCE_OFFSET,
|
|
23
|
+
RUNTIME_FAILURE_GLOBAL,
|
|
24
|
+
INIT_ALL_GLOBAL,
|
|
25
|
+
POINTER_GLOBALS,
|
|
26
|
+
BUFFER_DESCRIPTOR_POINTER_GLOBALS,
|
|
27
|
+
TRAPPING_DESCRIPTOR_UNARY_OPS,
|
|
28
|
+
TRAPPING_DESCRIPTOR_BINARY_OPS,
|
|
29
|
+
collectMathKernelHelpers,
|
|
30
|
+
alignUp,
|
|
31
|
+
encodeScalarValues,
|
|
32
|
+
} from "./shared.js";
|
|
33
|
+
|
|
34
|
+
export class MirCompilerCore {
|
|
35
|
+
constructor(mir, options) {
|
|
36
|
+
this.mir = mir;
|
|
37
|
+
this.options = {
|
|
38
|
+
optimize: options.optimize !== false,
|
|
39
|
+
emitText: options.emitText === true,
|
|
40
|
+
optimizeLevel: options.optimizeLevel ?? DEFAULT_OPTIMIZE_LEVEL,
|
|
41
|
+
shrinkLevel: options.shrinkLevel ?? 0,
|
|
42
|
+
fastMath: options.fastMath === true,
|
|
43
|
+
simd: options.simd !== false,
|
|
44
|
+
allowInliningFunctionsWithLoops:
|
|
45
|
+
options.allowInliningFunctionsWithLoops === true,
|
|
46
|
+
};
|
|
47
|
+
this.module = new binaryen.Module();
|
|
48
|
+
this.functionNames = [];
|
|
49
|
+
this.stateLayout = [];
|
|
50
|
+
this.paramLayout = [];
|
|
51
|
+
this.inputLayout = [];
|
|
52
|
+
this.outputLayout = [];
|
|
53
|
+
this.controlOutputLayout = [];
|
|
54
|
+
this.eventLayout = [];
|
|
55
|
+
this.constLayout = [];
|
|
56
|
+
this.localArrayLayout = [];
|
|
57
|
+
this.localScalarRefLayout = [];
|
|
58
|
+
this.memorySegments = [];
|
|
59
|
+
this.requiredMathHelpers = collectMathKernelHelpers(mir);
|
|
60
|
+
this.nextStaticAddress = this.requiredMathHelpers.size > 0
|
|
61
|
+
? MATH_KERNEL_RESERVED_END
|
|
62
|
+
: STATIC_BASE;
|
|
63
|
+
this.internalHelpers = new Set();
|
|
64
|
+
this.functionMayFail = [];
|
|
65
|
+
this.bufferMayWrite = [];
|
|
66
|
+
this.fallbackBufferReadAddress = 0;
|
|
67
|
+
this.fallbackBufferWriteAddress = 0;
|
|
68
|
+
this.scalarParameterByValue = [];
|
|
69
|
+
this.nextLabel = 0;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
compile() {
|
|
73
|
+
try {
|
|
74
|
+
this.validateEnvelope();
|
|
75
|
+
this.buildLayouts();
|
|
76
|
+
this.addMathKernel();
|
|
77
|
+
this.addMemoryAndContextGlobals();
|
|
78
|
+
this.addMirFunctions();
|
|
79
|
+
this.addAbiWrappers();
|
|
80
|
+
|
|
81
|
+
if (!this.module.validate()) {
|
|
82
|
+
throw new OndaBinaryenError("Binaryen rejected the generated WebAssembly module");
|
|
83
|
+
}
|
|
84
|
+
if (this.options.optimize) {
|
|
85
|
+
const previousOptimizeLevel = binaryen.getOptimizeLevel();
|
|
86
|
+
const previousShrinkLevel = binaryen.getShrinkLevel();
|
|
87
|
+
const previousFastMath = binaryen.getFastMath();
|
|
88
|
+
const previousLoopInlining =
|
|
89
|
+
binaryen.getAllowInliningFunctionsWithLoops();
|
|
90
|
+
try {
|
|
91
|
+
binaryen.setOptimizeLevel(this.options.optimizeLevel);
|
|
92
|
+
binaryen.setShrinkLevel(this.options.shrinkLevel);
|
|
93
|
+
binaryen.setFastMath(this.options.fastMath);
|
|
94
|
+
binaryen.setAllowInliningFunctionsWithLoops(
|
|
95
|
+
this.options.allowInliningFunctionsWithLoops,
|
|
96
|
+
);
|
|
97
|
+
this.module.optimize();
|
|
98
|
+
if (this.hoistInvariantBufferDescriptorLoads()) {
|
|
99
|
+
// The first rewrite makes descriptor provenance explicit in
|
|
100
|
+
// locals. A small cleanup is enough to expose aliases that were
|
|
101
|
+
// shared by Binaryen's original loop body; one final rewrite then
|
|
102
|
+
// catches those without paying for a second full O4 pipeline.
|
|
103
|
+
this.module.runPasses([
|
|
104
|
+
"simplify-locals",
|
|
105
|
+
"optimize-instructions",
|
|
106
|
+
"coalesce-locals",
|
|
107
|
+
"vacuum",
|
|
108
|
+
]);
|
|
109
|
+
if (this.hoistInvariantBufferDescriptorLoads()) {
|
|
110
|
+
this.module.runPasses(["vacuum"]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
} finally {
|
|
114
|
+
binaryen.setOptimizeLevel(previousOptimizeLevel);
|
|
115
|
+
binaryen.setShrinkLevel(previousShrinkLevel);
|
|
116
|
+
binaryen.setFastMath(previousFastMath);
|
|
117
|
+
binaryen.setAllowInliningFunctionsWithLoops(previousLoopInlining);
|
|
118
|
+
}
|
|
119
|
+
if (!this.module.validate()) {
|
|
120
|
+
throw new OndaBinaryenError(
|
|
121
|
+
"Binaryen rejected the optimized WebAssembly module",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const wasm = this.module.emitBinary();
|
|
127
|
+
const result = {
|
|
128
|
+
wasm,
|
|
129
|
+
metadata: this.buildMetadata(),
|
|
130
|
+
};
|
|
131
|
+
if (this.options.emitText) {
|
|
132
|
+
result.wat = this.module.emitText();
|
|
133
|
+
}
|
|
134
|
+
// Binaryen already validated the module above. Validate the descriptor here
|
|
135
|
+
// without asking the JavaScript engine to compile the Wasm a second time.
|
|
136
|
+
validateProcessorMetadata(result.metadata, "webassembly_module");
|
|
137
|
+
return result;
|
|
138
|
+
} finally {
|
|
139
|
+
this.module.dispose();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
hoistInvariantBufferDescriptorLoads() {
|
|
144
|
+
// Binaryen must conservatively assume that arbitrary linear-memory stores
|
|
145
|
+
// can rewrite host descriptor tables. After inlining, recover the stronger
|
|
146
|
+
// processor ABI contract explicitly: descriptor bindings are immutable for
|
|
147
|
+
// one entry-point invocation, so address-invariant loads belong in the loop
|
|
148
|
+
// preheader. Sample-varying addresses remain untouched.
|
|
149
|
+
this.descriptorLoadsHoisted = 0;
|
|
150
|
+
for (let index = 0; index < this.module.getNumFunctions(); index += 1) {
|
|
151
|
+
const func = this.module.getFunctionByIndex(index);
|
|
152
|
+
const body = binaryen.Function.getBody(func);
|
|
153
|
+
// The local-write scan below is part of the safety proof. If Binaryen
|
|
154
|
+
// adds an expression kind that this backend does not know how to walk,
|
|
155
|
+
// leave the whole function untouched rather than silently overlooking
|
|
156
|
+
// a nested local.tee.
|
|
157
|
+
if (!this.visitExpression(body, () => {})) continue;
|
|
158
|
+
const rewritten = this.rewriteDescriptorLoops(body, func);
|
|
159
|
+
if (rewritten !== body) binaryen.Function.setBody(func, rewritten);
|
|
160
|
+
}
|
|
161
|
+
return this.descriptorLoadsHoisted > 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
rewriteDescriptorLoops(expression, func) {
|
|
165
|
+
this.rewriteExpressionChildren(expression, (child) =>
|
|
166
|
+
this.rewriteDescriptorLoops(child, func)
|
|
167
|
+
);
|
|
168
|
+
if (binaryen.getExpressionInfo(expression).id !== binaryen.LoopId) {
|
|
169
|
+
return expression;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const body = binaryen.Loop.getBody(expression);
|
|
173
|
+
const controlPaths = this.descriptorControlPaths(body);
|
|
174
|
+
const definitions = new Map();
|
|
175
|
+
const writtenLocals = new Set();
|
|
176
|
+
this.visitExpression(body, (candidate) => {
|
|
177
|
+
const info = binaryen.getExpressionInfo(candidate);
|
|
178
|
+
if (info.id !== binaryen.LocalSetId) return;
|
|
179
|
+
writtenLocals.add(info.index);
|
|
180
|
+
const entries = definitions.get(info.index) ?? [];
|
|
181
|
+
entries.push(info.value);
|
|
182
|
+
definitions.set(info.index, entries);
|
|
183
|
+
});
|
|
184
|
+
const initializers = [];
|
|
185
|
+
// A pointer-local tee can expose the same invariant address to later
|
|
186
|
+
// descriptor loads. Cache its value in a fresh local: assigning the
|
|
187
|
+
// original local in the preheader would change loop-entry semantics.
|
|
188
|
+
const loopLocalCaches = new Map();
|
|
189
|
+
|
|
190
|
+
const rewriteLoad = (candidate) => {
|
|
191
|
+
this.rewriteExpressionChildren(candidate, rewriteLoad);
|
|
192
|
+
const info = binaryen.getExpressionInfo(candidate);
|
|
193
|
+
if (info.id !== binaryen.LoadId || info.isAtomic) return candidate;
|
|
194
|
+
const candidatePath = controlPaths.get(candidate) ?? [];
|
|
195
|
+
const localCache = (local) => {
|
|
196
|
+
const cache = loopLocalCaches.get(local);
|
|
197
|
+
return cache && this.descriptorPathDominates(cache.path, candidatePath)
|
|
198
|
+
? cache
|
|
199
|
+
: null;
|
|
200
|
+
};
|
|
201
|
+
if (!this.descriptorPointerExpression(
|
|
202
|
+
info.ptr,
|
|
203
|
+
definitions,
|
|
204
|
+
(local) => !writtenLocals.has(local) || localCache(local) !== null,
|
|
205
|
+
)) {
|
|
206
|
+
return candidate;
|
|
207
|
+
}
|
|
208
|
+
this.cacheDescriptorPointerSideEffects(
|
|
209
|
+
info.ptr,
|
|
210
|
+
func,
|
|
211
|
+
initializers,
|
|
212
|
+
loopLocalCaches,
|
|
213
|
+
controlPaths,
|
|
214
|
+
candidatePath,
|
|
215
|
+
);
|
|
216
|
+
// Binaryen exposes FunctionAddVar through its generated C-API surface,
|
|
217
|
+
// but not through the small Function convenience wrapper.
|
|
218
|
+
const cache = binaryen._BinaryenFunctionAddVar(func, info.type);
|
|
219
|
+
this.descriptorLoadsHoisted += 1;
|
|
220
|
+
const hoistedLoad = this.module.copyExpression(candidate);
|
|
221
|
+
const hoistedInfo = binaryen.getExpressionInfo(hoistedLoad);
|
|
222
|
+
binaryen.Load.setPtr(
|
|
223
|
+
hoistedLoad,
|
|
224
|
+
this.descriptorPointerForPreheader(
|
|
225
|
+
hoistedInfo.ptr,
|
|
226
|
+
loopLocalCaches,
|
|
227
|
+
candidatePath,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
initializers.push(this.module.local.set(cache, hoistedLoad));
|
|
231
|
+
const sideEffects = this.descriptorPointerSideEffects(info.ptr);
|
|
232
|
+
const value = this.module.local.get(cache, info.type);
|
|
233
|
+
return sideEffects.length === 0
|
|
234
|
+
? value
|
|
235
|
+
: this.module.block(null, [...sideEffects, value], info.type);
|
|
236
|
+
};
|
|
237
|
+
const rewrittenBody = rewriteLoad(body);
|
|
238
|
+
if (rewrittenBody !== body) binaryen.Loop.setBody(expression, rewrittenBody);
|
|
239
|
+
return initializers.length === 0
|
|
240
|
+
? expression
|
|
241
|
+
: this.module.block(null, [...initializers, expression]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
descriptorControlPaths(expression) {
|
|
245
|
+
const paths = new Map();
|
|
246
|
+
const visit = (candidate, path) => {
|
|
247
|
+
paths.set(candidate, path);
|
|
248
|
+
const info = binaryen.getExpressionInfo(candidate);
|
|
249
|
+
if (info.id === binaryen.IfId) {
|
|
250
|
+
visit(info.condition, path);
|
|
251
|
+
visit(info.ifTrue, [...path, `if:${candidate}:true`]);
|
|
252
|
+
if (info.ifFalse) {
|
|
253
|
+
visit(info.ifFalse, [...path, `if:${candidate}:false`]);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (info.id === binaryen.LoopId) {
|
|
258
|
+
visit(info.body, [...path, `loop:${candidate}`]);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
this.rewriteExpressionChildren(candidate, (child) => {
|
|
262
|
+
visit(child, path);
|
|
263
|
+
return child;
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
visit(expression, []);
|
|
267
|
+
return paths;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
descriptorPathDominates(dominator, candidate) {
|
|
271
|
+
// Rewrite traversal is in evaluation order, so an available cache is
|
|
272
|
+
// earlier than the candidate. The path prefix additionally proves that
|
|
273
|
+
// it was not produced only in a sibling branch or nested loop.
|
|
274
|
+
return dominator.length <= candidate.length
|
|
275
|
+
&& dominator.every((entry, index) => entry === candidate[index]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
cacheDescriptorPointerSideEffects(
|
|
279
|
+
expression,
|
|
280
|
+
func,
|
|
281
|
+
initializers,
|
|
282
|
+
loopLocalCaches,
|
|
283
|
+
controlPaths,
|
|
284
|
+
candidatePath,
|
|
285
|
+
) {
|
|
286
|
+
const info = binaryen.getExpressionInfo(expression);
|
|
287
|
+
if (info.id === binaryen.LocalSetId && info.isTee) {
|
|
288
|
+
this.cacheDescriptorPointerSideEffects(
|
|
289
|
+
info.value,
|
|
290
|
+
func,
|
|
291
|
+
initializers,
|
|
292
|
+
loopLocalCaches,
|
|
293
|
+
controlPaths,
|
|
294
|
+
candidatePath,
|
|
295
|
+
);
|
|
296
|
+
const cache = binaryen._BinaryenFunctionAddVar(func, info.type);
|
|
297
|
+
const value = this.descriptorPointerForPreheader(
|
|
298
|
+
this.module.copyExpression(info.value),
|
|
299
|
+
loopLocalCaches,
|
|
300
|
+
candidatePath,
|
|
301
|
+
);
|
|
302
|
+
initializers.push(this.module.local.set(cache, value));
|
|
303
|
+
loopLocalCaches.set(info.index, {
|
|
304
|
+
index: cache,
|
|
305
|
+
type: info.type,
|
|
306
|
+
path: controlPaths.get(expression) ?? candidatePath,
|
|
307
|
+
});
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
if (info.id === binaryen.UnaryId) {
|
|
311
|
+
this.cacheDescriptorPointerSideEffects(
|
|
312
|
+
info.value,
|
|
313
|
+
func,
|
|
314
|
+
initializers,
|
|
315
|
+
loopLocalCaches,
|
|
316
|
+
controlPaths,
|
|
317
|
+
candidatePath,
|
|
318
|
+
);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (info.id === binaryen.BinaryId) {
|
|
322
|
+
for (const child of [info.left, info.right]) {
|
|
323
|
+
this.cacheDescriptorPointerSideEffects(
|
|
324
|
+
child,
|
|
325
|
+
func,
|
|
326
|
+
initializers,
|
|
327
|
+
loopLocalCaches,
|
|
328
|
+
controlPaths,
|
|
329
|
+
candidatePath,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
descriptorPointerForPreheader(expression, loopLocalCaches, candidatePath) {
|
|
336
|
+
this.rewriteExpressionChildren(expression, (child) =>
|
|
337
|
+
this.descriptorPointerForPreheader(
|
|
338
|
+
child,
|
|
339
|
+
loopLocalCaches,
|
|
340
|
+
candidatePath,
|
|
341
|
+
)
|
|
342
|
+
);
|
|
343
|
+
const info = binaryen.getExpressionInfo(expression);
|
|
344
|
+
if (info.id === binaryen.LocalGetId) {
|
|
345
|
+
const cache = loopLocalCaches.get(info.index);
|
|
346
|
+
if (cache && this.descriptorPathDominates(cache.path, candidatePath)) {
|
|
347
|
+
return this.module.local.get(cache.index, cache.type);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (info.id === binaryen.LocalSetId && info.isTee) {
|
|
351
|
+
const cache = loopLocalCaches.get(info.index);
|
|
352
|
+
if (cache && this.descriptorPathDominates(cache.path, candidatePath)) {
|
|
353
|
+
return this.module.local.get(cache.index, cache.type);
|
|
354
|
+
}
|
|
355
|
+
return info.value;
|
|
356
|
+
}
|
|
357
|
+
return expression;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
descriptorPointerExpression(expression, definitions, localIsInvariant) {
|
|
361
|
+
if (!this.expressionUsesDescriptorTable(expression, definitions, new Set())) {
|
|
362
|
+
return false;
|
|
363
|
+
}
|
|
364
|
+
const visit = (candidate) => {
|
|
365
|
+
const info = binaryen.getExpressionInfo(candidate);
|
|
366
|
+
if (info.id === binaryen.ConstId) return true;
|
|
367
|
+
if (info.id === binaryen.LocalGetId) return localIsInvariant(info.index);
|
|
368
|
+
if (info.id === binaryen.GlobalGetId) {
|
|
369
|
+
if (BUFFER_DESCRIPTOR_POINTER_GLOBALS.has(info.name)) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
if (info.id === binaryen.LocalSetId && info.isTee) {
|
|
375
|
+
return visit(info.value);
|
|
376
|
+
}
|
|
377
|
+
if (info.id === binaryen.UnaryId) {
|
|
378
|
+
return !TRAPPING_DESCRIPTOR_UNARY_OPS.has(info.op)
|
|
379
|
+
&& visit(info.value);
|
|
380
|
+
}
|
|
381
|
+
if (info.id === binaryen.BinaryId) {
|
|
382
|
+
return !TRAPPING_DESCRIPTOR_BINARY_OPS.has(info.op)
|
|
383
|
+
&& visit(info.left)
|
|
384
|
+
&& visit(info.right);
|
|
385
|
+
}
|
|
386
|
+
if (info.id === binaryen.SelectId) {
|
|
387
|
+
if (
|
|
388
|
+
this.expressionContainsTee(info.condition)
|
|
389
|
+
|| this.expressionContainsTee(info.ifTrue)
|
|
390
|
+
|| this.expressionContainsTee(info.ifFalse)
|
|
391
|
+
) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
return visit(info.condition) && visit(info.ifTrue) && visit(info.ifFalse);
|
|
395
|
+
}
|
|
396
|
+
return false;
|
|
397
|
+
};
|
|
398
|
+
return visit(expression);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
expressionUsesDescriptorTable(expression, definitions, visitingLocals) {
|
|
402
|
+
const info = binaryen.getExpressionInfo(expression);
|
|
403
|
+
if (info.id === binaryen.GlobalGetId) {
|
|
404
|
+
return BUFFER_DESCRIPTOR_POINTER_GLOBALS.has(info.name);
|
|
405
|
+
}
|
|
406
|
+
if (info.id === binaryen.LocalGetId) {
|
|
407
|
+
const values = definitions.get(info.index);
|
|
408
|
+
if (
|
|
409
|
+
!values
|
|
410
|
+
|| values.length !== 1
|
|
411
|
+
|| visitingLocals.has(info.index)
|
|
412
|
+
) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
visitingLocals.add(info.index);
|
|
416
|
+
const result = this.expressionUsesDescriptorTable(
|
|
417
|
+
values[0],
|
|
418
|
+
definitions,
|
|
419
|
+
visitingLocals,
|
|
420
|
+
);
|
|
421
|
+
visitingLocals.delete(info.index);
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
if (info.id === binaryen.LocalSetId && info.isTee) {
|
|
425
|
+
return this.expressionUsesDescriptorTable(
|
|
426
|
+
info.value,
|
|
427
|
+
definitions,
|
|
428
|
+
visitingLocals,
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
if (info.id === binaryen.UnaryId) {
|
|
432
|
+
return this.expressionUsesDescriptorTable(
|
|
433
|
+
info.value,
|
|
434
|
+
definitions,
|
|
435
|
+
visitingLocals,
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (info.id === binaryen.BinaryId) {
|
|
439
|
+
return this.expressionUsesDescriptorTable(
|
|
440
|
+
info.left,
|
|
441
|
+
definitions,
|
|
442
|
+
visitingLocals,
|
|
443
|
+
) || this.expressionUsesDescriptorTable(
|
|
444
|
+
info.right,
|
|
445
|
+
definitions,
|
|
446
|
+
visitingLocals,
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (info.id === binaryen.SelectId) {
|
|
450
|
+
return this.expressionUsesDescriptorTable(
|
|
451
|
+
info.condition,
|
|
452
|
+
definitions,
|
|
453
|
+
visitingLocals,
|
|
454
|
+
) || this.expressionUsesDescriptorTable(
|
|
455
|
+
info.ifTrue,
|
|
456
|
+
definitions,
|
|
457
|
+
visitingLocals,
|
|
458
|
+
) || this.expressionUsesDescriptorTable(
|
|
459
|
+
info.ifFalse,
|
|
460
|
+
definitions,
|
|
461
|
+
visitingLocals,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
return false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
descriptorPointerSideEffects(expression) {
|
|
468
|
+
const result = [];
|
|
469
|
+
const visit = (candidate) => {
|
|
470
|
+
const info = binaryen.getExpressionInfo(candidate);
|
|
471
|
+
if (info.id === binaryen.LocalSetId && info.isTee) {
|
|
472
|
+
result.push(
|
|
473
|
+
this.module.local.set(info.index, this.module.copyExpression(info.value)),
|
|
474
|
+
);
|
|
475
|
+
} else if (info.id === binaryen.UnaryId) {
|
|
476
|
+
visit(info.value);
|
|
477
|
+
} else if (info.id === binaryen.BinaryId) {
|
|
478
|
+
visit(info.left);
|
|
479
|
+
visit(info.right);
|
|
480
|
+
} else if (info.id === binaryen.SelectId) {
|
|
481
|
+
// Pointer selectors generated by this backend are side-effect free.
|
|
482
|
+
// A nested tee would need conditional reconstruction, so leave it to
|
|
483
|
+
// the conservative invariance check instead of moving it here.
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
visit(expression);
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
expressionContainsTee(expression) {
|
|
491
|
+
const info = binaryen.getExpressionInfo(expression);
|
|
492
|
+
if (info.id === binaryen.LocalSetId) return info.isTee;
|
|
493
|
+
if (info.id === binaryen.UnaryId) {
|
|
494
|
+
return this.expressionContainsTee(info.value);
|
|
495
|
+
}
|
|
496
|
+
if (info.id === binaryen.BinaryId) {
|
|
497
|
+
return this.expressionContainsTee(info.left)
|
|
498
|
+
|| this.expressionContainsTee(info.right);
|
|
499
|
+
}
|
|
500
|
+
if (info.id === binaryen.SelectId) {
|
|
501
|
+
return this.expressionContainsTee(info.condition)
|
|
502
|
+
|| this.expressionContainsTee(info.ifTrue)
|
|
503
|
+
|| this.expressionContainsTee(info.ifFalse);
|
|
504
|
+
}
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
visitExpression(expression, visitor) {
|
|
509
|
+
visitor(expression);
|
|
510
|
+
let complete = true;
|
|
511
|
+
const supported = this.rewriteExpressionChildren(expression, (child) => {
|
|
512
|
+
if (!this.visitExpression(child, visitor)) complete = false;
|
|
513
|
+
return child;
|
|
514
|
+
});
|
|
515
|
+
return complete && supported;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
rewriteExpressionChildren(expression, rewrite) {
|
|
519
|
+
const info = binaryen.getExpressionInfo(expression);
|
|
520
|
+
const replace = (child, setter) => {
|
|
521
|
+
if (child) setter(rewrite(child));
|
|
522
|
+
};
|
|
523
|
+
switch (info.id) {
|
|
524
|
+
case binaryen.BlockId:
|
|
525
|
+
info.children.forEach((child, index) =>
|
|
526
|
+
replace(child, (value) => binaryen.Block.setChildAt(expression, index, value))
|
|
527
|
+
);
|
|
528
|
+
break;
|
|
529
|
+
case binaryen.IfId:
|
|
530
|
+
replace(info.condition, (value) => binaryen.If.setCondition(expression, value));
|
|
531
|
+
replace(info.ifTrue, (value) => binaryen.If.setIfTrue(expression, value));
|
|
532
|
+
replace(info.ifFalse, (value) => binaryen.If.setIfFalse(expression, value));
|
|
533
|
+
break;
|
|
534
|
+
case binaryen.LoopId:
|
|
535
|
+
replace(info.body, (value) => binaryen.Loop.setBody(expression, value));
|
|
536
|
+
break;
|
|
537
|
+
case binaryen.BreakId:
|
|
538
|
+
replace(info.condition, (value) => binaryen.Break.setCondition(expression, value));
|
|
539
|
+
replace(info.value, (value) => binaryen.Break.setValue(expression, value));
|
|
540
|
+
break;
|
|
541
|
+
case binaryen.SwitchId:
|
|
542
|
+
replace(info.condition, (value) => binaryen.Switch.setCondition(expression, value));
|
|
543
|
+
replace(info.value, (value) => binaryen.Switch.setValue(expression, value));
|
|
544
|
+
break;
|
|
545
|
+
case binaryen.CallId:
|
|
546
|
+
info.operands.forEach((child, index) =>
|
|
547
|
+
replace(child, (value) => binaryen.Call.setOperandAt(expression, index, value))
|
|
548
|
+
);
|
|
549
|
+
break;
|
|
550
|
+
case binaryen.CallIndirectId:
|
|
551
|
+
replace(info.target, (value) => binaryen.CallIndirect.setTarget(expression, value));
|
|
552
|
+
info.operands.forEach((child, index) =>
|
|
553
|
+
replace(child, (value) => binaryen.CallIndirect.setOperandAt(expression, index, value))
|
|
554
|
+
);
|
|
555
|
+
break;
|
|
556
|
+
case binaryen.LocalSetId:
|
|
557
|
+
replace(info.value, (value) => binaryen.LocalSet.setValue(expression, value));
|
|
558
|
+
break;
|
|
559
|
+
case binaryen.GlobalSetId:
|
|
560
|
+
replace(info.value, (value) => binaryen.GlobalSet.setValue(expression, value));
|
|
561
|
+
break;
|
|
562
|
+
case binaryen.LoadId:
|
|
563
|
+
replace(info.ptr, (value) => binaryen.Load.setPtr(expression, value));
|
|
564
|
+
break;
|
|
565
|
+
case binaryen.StoreId:
|
|
566
|
+
replace(info.ptr, (value) => binaryen.Store.setPtr(expression, value));
|
|
567
|
+
replace(info.value, (value) => binaryen.Store.setValue(expression, value));
|
|
568
|
+
break;
|
|
569
|
+
case binaryen.UnaryId:
|
|
570
|
+
replace(info.value, (value) => binaryen.Unary.setValue(expression, value));
|
|
571
|
+
break;
|
|
572
|
+
case binaryen.BinaryId:
|
|
573
|
+
replace(info.left, (value) => binaryen.Binary.setLeft(expression, value));
|
|
574
|
+
replace(info.right, (value) => binaryen.Binary.setRight(expression, value));
|
|
575
|
+
break;
|
|
576
|
+
case binaryen.SelectId:
|
|
577
|
+
replace(info.ifTrue, (value) => binaryen.Select.setIfTrue(expression, value));
|
|
578
|
+
replace(info.ifFalse, (value) => binaryen.Select.setIfFalse(expression, value));
|
|
579
|
+
replace(info.condition, (value) => binaryen.Select.setCondition(expression, value));
|
|
580
|
+
break;
|
|
581
|
+
case binaryen.DropId:
|
|
582
|
+
replace(info.value, (value) => binaryen.Drop.setValue(expression, value));
|
|
583
|
+
break;
|
|
584
|
+
case binaryen.ReturnId:
|
|
585
|
+
replace(info.value, (value) => binaryen.Return.setValue(expression, value));
|
|
586
|
+
break;
|
|
587
|
+
case binaryen.MemoryCopyId:
|
|
588
|
+
replace(info.dest, (value) => binaryen.MemoryCopy.setDest(expression, value));
|
|
589
|
+
replace(info.source, (value) => binaryen.MemoryCopy.setSource(expression, value));
|
|
590
|
+
replace(info.size, (value) => binaryen.MemoryCopy.setSize(expression, value));
|
|
591
|
+
break;
|
|
592
|
+
case binaryen.MemoryFillId:
|
|
593
|
+
replace(info.dest, (value) => binaryen.MemoryFill.setDest(expression, value));
|
|
594
|
+
replace(info.value, (value) => binaryen.MemoryFill.setValue(expression, value));
|
|
595
|
+
replace(info.size, (value) => binaryen.MemoryFill.setSize(expression, value));
|
|
596
|
+
break;
|
|
597
|
+
case binaryen.SIMDExtractId:
|
|
598
|
+
replace(info.vec, (value) => binaryen.SIMDExtract.setVec(expression, value));
|
|
599
|
+
break;
|
|
600
|
+
case binaryen.SIMDReplaceId:
|
|
601
|
+
replace(info.vec, (value) => binaryen.SIMDReplace.setVec(expression, value));
|
|
602
|
+
replace(info.value, (value) => binaryen.SIMDReplace.setValue(expression, value));
|
|
603
|
+
break;
|
|
604
|
+
case binaryen.SIMDShuffleId:
|
|
605
|
+
replace(info.left, (value) => binaryen.SIMDShuffle.setLeft(expression, value));
|
|
606
|
+
replace(info.right, (value) => binaryen.SIMDShuffle.setRight(expression, value));
|
|
607
|
+
break;
|
|
608
|
+
case binaryen.SIMDTernaryId:
|
|
609
|
+
replace(info.a, (value) => binaryen.SIMDTernary.setA(expression, value));
|
|
610
|
+
replace(info.b, (value) => binaryen.SIMDTernary.setB(expression, value));
|
|
611
|
+
replace(info.c, (value) => binaryen.SIMDTernary.setC(expression, value));
|
|
612
|
+
break;
|
|
613
|
+
case binaryen.SIMDShiftId:
|
|
614
|
+
replace(info.vec, (value) => binaryen.SIMDShift.setVec(expression, value));
|
|
615
|
+
replace(info.shift, (value) => binaryen.SIMDShift.setShift(expression, value));
|
|
616
|
+
break;
|
|
617
|
+
case binaryen.SIMDLoadId:
|
|
618
|
+
replace(info.ptr, (value) => binaryen.SIMDLoad.setPtr(expression, value));
|
|
619
|
+
break;
|
|
620
|
+
case binaryen.SIMDLoadStoreLaneId:
|
|
621
|
+
replace(info.ptr, (value) => binaryen.SIMDLoadStoreLane.setPtr(expression, value));
|
|
622
|
+
replace(info.vec, (value) => binaryen.SIMDLoadStoreLane.setVec(expression, value));
|
|
623
|
+
break;
|
|
624
|
+
case binaryen.TupleMakeId:
|
|
625
|
+
info.operands.forEach((child, index) =>
|
|
626
|
+
replace(child, (value) => binaryen.TupleMake.setOperandAt(expression, index, value))
|
|
627
|
+
);
|
|
628
|
+
break;
|
|
629
|
+
case binaryen.TupleExtractId:
|
|
630
|
+
replace(info.tuple, (value) => binaryen.TupleExtract.setTuple(expression, value));
|
|
631
|
+
break;
|
|
632
|
+
case binaryen.ConstId:
|
|
633
|
+
case binaryen.LocalGetId:
|
|
634
|
+
case binaryen.GlobalGetId:
|
|
635
|
+
case binaryen.NopId:
|
|
636
|
+
case binaryen.UnreachableId:
|
|
637
|
+
case binaryen.MemorySizeId:
|
|
638
|
+
case binaryen.DataDropId:
|
|
639
|
+
break;
|
|
640
|
+
default:
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
validateEnvelope() {
|
|
647
|
+
const mir = this.mir;
|
|
648
|
+
if (!mir || typeof mir !== "object" || Array.isArray(mir)) {
|
|
649
|
+
this.fail("MIR must be a JSON object");
|
|
650
|
+
}
|
|
651
|
+
if (mir.schema_version !== SUPPORTED_MIR_SCHEMA_VERSION) {
|
|
652
|
+
this.fail(
|
|
653
|
+
`unsupported MIR schema version ${String(mir.schema_version)}; expected ${SUPPORTED_MIR_SCHEMA_VERSION}`,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
for (const field of ["types", "state", "const_data", "functions"]) {
|
|
657
|
+
if (!Array.isArray(mir[field])) {
|
|
658
|
+
this.fail(`MIR field '${field}' must be an array`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (!mir.interface || typeof mir.interface !== "object") {
|
|
662
|
+
this.fail("MIR field 'interface' must be an object");
|
|
663
|
+
}
|
|
664
|
+
for (const field of [
|
|
665
|
+
"inputs",
|
|
666
|
+
"outputs",
|
|
667
|
+
"control_outputs",
|
|
668
|
+
"params",
|
|
669
|
+
"buffers",
|
|
670
|
+
"events",
|
|
671
|
+
"delegates",
|
|
672
|
+
]) {
|
|
673
|
+
if (!Array.isArray(mir.interface[field])) {
|
|
674
|
+
this.fail(`MIR interface field '${field}' must be an array`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
if (!mir.entry_points || !Number.isInteger(mir.entry_points.init)) {
|
|
678
|
+
this.fail("MIR entry_points are missing or invalid");
|
|
679
|
+
}
|
|
680
|
+
if (!Number.isInteger(mir.entry_points.process)) {
|
|
681
|
+
this.fail("MIR process entry point is missing or invalid");
|
|
682
|
+
}
|
|
683
|
+
if (!Number.isInteger(mir.config?.block_size) || mir.config.block_size <= 0) {
|
|
684
|
+
this.fail("MIR block size must be a positive integer");
|
|
685
|
+
}
|
|
686
|
+
if (mir.config.block_size > 0x7fff_ffff) {
|
|
687
|
+
this.fail("MIR block size must fit the signed i32 process ABI");
|
|
688
|
+
}
|
|
689
|
+
if (
|
|
690
|
+
!Number.isInteger(this.options.optimizeLevel) ||
|
|
691
|
+
this.options.optimizeLevel < 0 ||
|
|
692
|
+
this.options.optimizeLevel > 4
|
|
693
|
+
) {
|
|
694
|
+
this.fail("Binaryen optimizeLevel must be an integer from 0 through 4");
|
|
695
|
+
}
|
|
696
|
+
if (
|
|
697
|
+
!Number.isInteger(this.options.shrinkLevel) ||
|
|
698
|
+
this.options.shrinkLevel < 0 ||
|
|
699
|
+
this.options.shrinkLevel > 2
|
|
700
|
+
) {
|
|
701
|
+
this.fail("Binaryen shrinkLevel must be an integer from 0 through 2");
|
|
702
|
+
}
|
|
703
|
+
this.validateCurrentSchemaEnvelope();
|
|
704
|
+
this.validateProcessEntrySignature();
|
|
705
|
+
this.validateAcyclicCallGraph();
|
|
706
|
+
this.analyzeBufferWrites();
|
|
707
|
+
this.analyzeRecoverableFailures();
|
|
708
|
+
this.analyzeScalarReferenceParameters();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
analyzeScalarReferenceParameters() {
|
|
712
|
+
// MIR reference modes are conservative. Internal scalar references that
|
|
713
|
+
// are never written, never escape to a writable reference, and never alias
|
|
714
|
+
// a writable argument can safely use a value ABI. This removes scratch
|
|
715
|
+
// memory traffic and exposes their values to post-inlining loop analysis.
|
|
716
|
+
const candidates = this.mir.functions.map((func) =>
|
|
717
|
+
func.params.map((parameter) => {
|
|
718
|
+
const type = this.type(parameter.ty);
|
|
719
|
+
return type.kind === "scalar" && parameter.mode !== "value";
|
|
720
|
+
})
|
|
721
|
+
);
|
|
722
|
+
const callSites = this.mir.functions.map(() => []);
|
|
723
|
+
|
|
724
|
+
const visitBlock = (functionId, block) => {
|
|
725
|
+
for (const statement of block.statements) {
|
|
726
|
+
const kind = statement.kind?.kind;
|
|
727
|
+
const data = statement.kind?.data;
|
|
728
|
+
if (
|
|
729
|
+
kind === "assign"
|
|
730
|
+
&& data.destination?.base?.kind === "parameter"
|
|
731
|
+
&& data.destination.projections.length === 0
|
|
732
|
+
) {
|
|
733
|
+
candidates[functionId][data.destination.base.data] = false;
|
|
734
|
+
} else if (kind === "call") {
|
|
735
|
+
callSites[data.function].push({ caller: functionId, call: data });
|
|
736
|
+
} else if (kind === "if") {
|
|
737
|
+
visitBlock(functionId, data.then_block);
|
|
738
|
+
visitBlock(functionId, data.else_block);
|
|
739
|
+
} else if (kind === "loop") {
|
|
740
|
+
visitBlock(functionId, data.body);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
for (const [functionId, func] of this.mir.functions.entries()) {
|
|
745
|
+
visitBlock(functionId, func.body);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const unprojectedScalarPlace = (argument, functionId) => {
|
|
749
|
+
if (
|
|
750
|
+
argument?.kind !== "place"
|
|
751
|
+
|| argument.data.projections.length !== 0
|
|
752
|
+
|| !["local", "parameter"].includes(argument.data.base.kind)
|
|
753
|
+
) {
|
|
754
|
+
return null;
|
|
755
|
+
}
|
|
756
|
+
const typeId = argument.data.base.kind === "local"
|
|
757
|
+
? this.mir.functions[functionId].locals[argument.data.base.data]?.ty
|
|
758
|
+
: this.mir.functions[functionId].params[argument.data.base.data]?.ty;
|
|
759
|
+
return Number.isInteger(typeId) && this.type(typeId).kind === "scalar"
|
|
760
|
+
? argument.data.base
|
|
761
|
+
: null;
|
|
762
|
+
};
|
|
763
|
+
const sameBase = (lhs, rhs) =>
|
|
764
|
+
lhs?.kind === rhs?.kind && lhs?.data === rhs?.data;
|
|
765
|
+
|
|
766
|
+
let changed = true;
|
|
767
|
+
while (changed) {
|
|
768
|
+
changed = false;
|
|
769
|
+
for (const [calleeId, sites] of callSites.entries()) {
|
|
770
|
+
for (const { caller: callerId, call } of sites) {
|
|
771
|
+
for (let parameterId = 0; parameterId < candidates[calleeId].length; parameterId += 1) {
|
|
772
|
+
if (!candidates[calleeId][parameterId]) continue;
|
|
773
|
+
const base = unprojectedScalarPlace(call.args[parameterId], callerId);
|
|
774
|
+
const forwardedCandidate =
|
|
775
|
+
base?.kind !== "parameter"
|
|
776
|
+
|| candidates[callerId][base.data];
|
|
777
|
+
const aliasesWritableArgument = call.args.some((argument, index) => {
|
|
778
|
+
if (index === parameterId || candidates[calleeId][index]) return false;
|
|
779
|
+
return sameBase(
|
|
780
|
+
base,
|
|
781
|
+
unprojectedScalarPlace(argument, callerId),
|
|
782
|
+
);
|
|
783
|
+
});
|
|
784
|
+
if (!base || !forwardedCandidate || aliasesWritableArgument) {
|
|
785
|
+
candidates[calleeId][parameterId] = false;
|
|
786
|
+
changed = true;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
for (const [callerId, func] of this.mir.functions.entries()) {
|
|
793
|
+
const visitCalls = (block) => {
|
|
794
|
+
for (const statement of block.statements) {
|
|
795
|
+
const kind = statement.kind?.kind;
|
|
796
|
+
const data = statement.kind?.data;
|
|
797
|
+
if (kind === "call") {
|
|
798
|
+
data.args.forEach((argument, parameterId) => {
|
|
799
|
+
const base = unprojectedScalarPlace(argument, callerId);
|
|
800
|
+
if (
|
|
801
|
+
base?.kind === "parameter"
|
|
802
|
+
&& candidates[callerId][base.data]
|
|
803
|
+
&& !candidates[data.function][parameterId]
|
|
804
|
+
) {
|
|
805
|
+
candidates[callerId][base.data] = false;
|
|
806
|
+
changed = true;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
} else if (kind === "if") {
|
|
810
|
+
visitCalls(data.then_block);
|
|
811
|
+
visitCalls(data.else_block);
|
|
812
|
+
} else if (kind === "loop") {
|
|
813
|
+
visitCalls(data.body);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
visitCalls(func.body);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
this.scalarParameterByValue = candidates;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
parameterPassingMode(functionId, parameterId) {
|
|
824
|
+
return this.scalarParameterByValue[functionId]?.[parameterId]
|
|
825
|
+
? "value"
|
|
826
|
+
: this.mir.functions[functionId].params[parameterId].mode;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
validateCurrentSchemaEnvelope() {
|
|
830
|
+
const persistenceKinds = new Set([
|
|
831
|
+
"snapshot",
|
|
832
|
+
"instance_scratch",
|
|
833
|
+
"control_mirror",
|
|
834
|
+
]);
|
|
835
|
+
for (const [stateId, slot] of this.mir.state.entries()) {
|
|
836
|
+
if (typeof slot?.authored !== "boolean") {
|
|
837
|
+
this.fail(`state slot ${stateId} has an invalid authored flag`);
|
|
838
|
+
}
|
|
839
|
+
if (slot?.pinned !== undefined && typeof slot.pinned !== "boolean") {
|
|
840
|
+
this.fail(`state slot ${stateId} has an invalid pinned flag`);
|
|
841
|
+
}
|
|
842
|
+
if (!persistenceKinds.has(slot?.persistence)) {
|
|
843
|
+
this.fail(
|
|
844
|
+
`state slot ${stateId} has invalid persistence '${String(slot?.persistence)}'`,
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const mirrors = new Set();
|
|
850
|
+
for (const [outputId, output] of this.mir.interface.control_outputs.entries()) {
|
|
851
|
+
if (
|
|
852
|
+
!Number.isInteger(output?.mirror) ||
|
|
853
|
+
output.mirror < 0 ||
|
|
854
|
+
output.mirror >= this.mir.state.length
|
|
855
|
+
) {
|
|
856
|
+
this.fail(`control output ${outputId} has an invalid mirror state id`);
|
|
857
|
+
}
|
|
858
|
+
if (mirrors.has(output.mirror)) {
|
|
859
|
+
this.fail(`control output ${outputId} reuses mirror state ${output.mirror}`);
|
|
860
|
+
}
|
|
861
|
+
mirrors.add(output.mirror);
|
|
862
|
+
const slot = this.mir.state[output.mirror];
|
|
863
|
+
if (slot.persistence !== "control_mirror") {
|
|
864
|
+
this.fail(
|
|
865
|
+
`control output ${outputId} mirror state ${output.mirror} is not control_mirror storage`,
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
if (!this.typesEquivalent(output.ty, slot.ty)) {
|
|
869
|
+
this.fail(
|
|
870
|
+
`control output ${outputId} type does not match mirror state ${output.mirror}`,
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const origins = new Set(["source", "compiler_generated"]);
|
|
876
|
+
const inlineHints = new Set(["auto", "always", "never"]);
|
|
877
|
+
for (const [functionId, func] of this.mir.functions.entries()) {
|
|
878
|
+
if (
|
|
879
|
+
!func?.attributes ||
|
|
880
|
+
!origins.has(func.attributes.origin) ||
|
|
881
|
+
!inlineHints.has(func.attributes.inline) ||
|
|
882
|
+
typeof func.attributes.runtime_context !== "boolean"
|
|
883
|
+
) {
|
|
884
|
+
this.fail(
|
|
885
|
+
`function ${functionId} has invalid schema-${SUPPORTED_MIR_SCHEMA_VERSION} attributes`,
|
|
886
|
+
);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
validateProcessEntrySignature() {
|
|
892
|
+
const processId = this.mir.entry_points.process;
|
|
893
|
+
this.requireFunctionId(processId, "process entry point");
|
|
894
|
+
const process = this.mir.functions[processId];
|
|
895
|
+
if (process?.kind?.kind !== "process") {
|
|
896
|
+
this.fail("MIR process entry point must have process function kind");
|
|
897
|
+
}
|
|
898
|
+
if (!Array.isArray(process.params) || process.params.length !== 3) {
|
|
899
|
+
this.fail(
|
|
900
|
+
"MIR process entry point must have exactly three parameters (start_frame, frames, flags)",
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
if (!Array.isArray(process.results) || process.results.length !== 0) {
|
|
904
|
+
this.fail("MIR process entry point must not return values");
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const names = ["start_frame", "frames", "flags"];
|
|
908
|
+
for (const [index, name] of names.entries()) {
|
|
909
|
+
const parameter = process.params[index];
|
|
910
|
+
if (parameter?.name !== name) {
|
|
911
|
+
this.fail(`MIR process parameter ${index} must be named '${name}'`);
|
|
912
|
+
}
|
|
913
|
+
if (parameter.mode !== "value") {
|
|
914
|
+
this.fail(`MIR process parameter '${name}' must use value passing mode`);
|
|
915
|
+
}
|
|
916
|
+
const type = this.mir.types[parameter.ty];
|
|
917
|
+
if (type?.kind !== "scalar" || type.data !== "i32") {
|
|
918
|
+
this.fail(`MIR process parameter '${name}' must have type i32`);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
validateAcyclicCallGraph() {
|
|
924
|
+
const functionCount = this.mir.functions.length;
|
|
925
|
+
const collectCalls = (block, callees) => {
|
|
926
|
+
for (const statement of block?.statements ?? []) {
|
|
927
|
+
const kind = statement.kind?.kind;
|
|
928
|
+
const data = statement.kind?.data;
|
|
929
|
+
if (kind === "call") {
|
|
930
|
+
if (
|
|
931
|
+
Number.isInteger(data?.function)
|
|
932
|
+
&& data.function >= 0
|
|
933
|
+
&& data.function < functionCount
|
|
934
|
+
) {
|
|
935
|
+
callees.add(data.function);
|
|
936
|
+
}
|
|
937
|
+
} else if (kind === "if") {
|
|
938
|
+
collectCalls(data?.then_block, callees);
|
|
939
|
+
collectCalls(data?.else_block, callees);
|
|
940
|
+
} else if (kind === "loop") {
|
|
941
|
+
collectCalls(data?.body, callees);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
const edges = this.mir.functions.map((func) => {
|
|
947
|
+
const callees = new Set();
|
|
948
|
+
collectCalls(func.body, callees);
|
|
949
|
+
return [...callees].sort((lhs, rhs) => lhs - rhs);
|
|
950
|
+
});
|
|
951
|
+
const visits = new Uint8Array(functionCount);
|
|
952
|
+
const path = [];
|
|
953
|
+
const visit = (functionId) => {
|
|
954
|
+
if (visits[functionId] === 2) return null;
|
|
955
|
+
if (visits[functionId] === 1) {
|
|
956
|
+
const start = Math.max(0, path.indexOf(functionId));
|
|
957
|
+
return [...path.slice(start), functionId];
|
|
958
|
+
}
|
|
959
|
+
visits[functionId] = 1;
|
|
960
|
+
path.push(functionId);
|
|
961
|
+
for (const callee of edges[functionId]) {
|
|
962
|
+
const cycle = visit(callee);
|
|
963
|
+
if (cycle) return cycle;
|
|
964
|
+
}
|
|
965
|
+
path.pop();
|
|
966
|
+
visits[functionId] = 2;
|
|
967
|
+
return null;
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
for (let functionId = 0; functionId < functionCount; functionId += 1) {
|
|
971
|
+
const cycle = visit(functionId);
|
|
972
|
+
if (cycle) {
|
|
973
|
+
const display = cycle
|
|
974
|
+
.map((id) => this.mir.functions[id]?.name ?? `@fn${id}`)
|
|
975
|
+
.join(" -> ");
|
|
976
|
+
this.fail(`recursive call cycle is not realtime-safe: ${display}`);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
analyzeBufferWrites() {
|
|
982
|
+
const bufferOrigin = (id) => `buffer:${id}`;
|
|
983
|
+
const parameterOrigin = (id, slot = 0) => `parameter:${id}:${slot}`;
|
|
984
|
+
const selectedSlots = (selector, len, bounds) => {
|
|
985
|
+
if (!Number.isInteger(len) || len <= 0) return [];
|
|
986
|
+
if (
|
|
987
|
+
selector?.kind === "constant"
|
|
988
|
+
&& selector.data?.type === "i32"
|
|
989
|
+
&& Number.isInteger(selector.data.value)
|
|
990
|
+
) {
|
|
991
|
+
let slot = selector.data.value;
|
|
992
|
+
if (bounds === "clamp") {
|
|
993
|
+
slot = Math.min(len - 1, Math.max(0, slot));
|
|
994
|
+
return [slot];
|
|
995
|
+
}
|
|
996
|
+
if (slot >= 0 && slot < len) return [slot];
|
|
997
|
+
}
|
|
998
|
+
return Array.from({ length: len }, (_, slot) => slot);
|
|
999
|
+
};
|
|
1000
|
+
const bufferIds = (bufferRef) => {
|
|
1001
|
+
if (Number.isInteger(bufferRef)) return [bufferRef];
|
|
1002
|
+
if (bufferRef?.kind === "direct" && Number.isInteger(bufferRef.data)) {
|
|
1003
|
+
return [bufferRef.data];
|
|
1004
|
+
}
|
|
1005
|
+
if (
|
|
1006
|
+
bufferRef?.kind === "array_element"
|
|
1007
|
+
&& Number.isInteger(bufferRef.data?.first)
|
|
1008
|
+
&& Number.isInteger(bufferRef.data?.len)
|
|
1009
|
+
&& bufferRef.data.len > 0
|
|
1010
|
+
) {
|
|
1011
|
+
return selectedSlots(
|
|
1012
|
+
bufferRef.data.selector,
|
|
1013
|
+
bufferRef.data.len,
|
|
1014
|
+
bufferRef.data.bounds,
|
|
1015
|
+
).map(
|
|
1016
|
+
(slot) => bufferRef.data.first + slot,
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
return [];
|
|
1020
|
+
};
|
|
1021
|
+
const bufferOrigins = (bufferRef) =>
|
|
1022
|
+
new Set(bufferIds(bufferRef).map(bufferOrigin));
|
|
1023
|
+
const bufferParamSlots = (parameterRef, func) => {
|
|
1024
|
+
if (Number.isInteger(parameterRef)) {
|
|
1025
|
+
return [{ parameter: parameterRef, slot: 0 }];
|
|
1026
|
+
}
|
|
1027
|
+
if (
|
|
1028
|
+
parameterRef?.kind === "direct"
|
|
1029
|
+
&& Number.isInteger(parameterRef.data)
|
|
1030
|
+
) {
|
|
1031
|
+
return [{ parameter: parameterRef.data, slot: 0 }];
|
|
1032
|
+
}
|
|
1033
|
+
if (
|
|
1034
|
+
parameterRef?.kind === "array_element"
|
|
1035
|
+
&& Number.isInteger(parameterRef.data?.span)
|
|
1036
|
+
) {
|
|
1037
|
+
const parameter = func.params?.[parameterRef.data.span];
|
|
1038
|
+
const type = parameter && this.type(parameter.ty);
|
|
1039
|
+
const len = type?.kind === "buffer_span" ? type.data.len : 1;
|
|
1040
|
+
return selectedSlots(
|
|
1041
|
+
parameterRef.data.selector,
|
|
1042
|
+
len,
|
|
1043
|
+
parameterRef.data.bounds,
|
|
1044
|
+
).map((slot) => ({ parameter: parameterRef.data.span, slot }));
|
|
1045
|
+
}
|
|
1046
|
+
return [];
|
|
1047
|
+
};
|
|
1048
|
+
const bufferParamOrigins = (parameterRef, func) => new Set(
|
|
1049
|
+
bufferParamSlots(parameterRef, func).map(({ parameter, slot }) =>
|
|
1050
|
+
parameterOrigin(parameter, slot)
|
|
1051
|
+
),
|
|
1052
|
+
);
|
|
1053
|
+
const localId = (value) =>
|
|
1054
|
+
value?.kind === "local" && Number.isInteger(value.data)
|
|
1055
|
+
? value.data
|
|
1056
|
+
: null;
|
|
1057
|
+
const setEquals = (lhs, rhs) =>
|
|
1058
|
+
lhs.size === rhs.size && [...lhs].every((entry) => rhs.has(entry));
|
|
1059
|
+
const summaryEquals = (lhs, rhs) =>
|
|
1060
|
+
setEquals(lhs.buffers, rhs.buffers)
|
|
1061
|
+
&& setEquals(lhs.parameters, rhs.parameters);
|
|
1062
|
+
|
|
1063
|
+
const valueOrigins = (value, aliases) => {
|
|
1064
|
+
const id = localId(value);
|
|
1065
|
+
return id === null ? new Set() : new Set(aliases[id] ?? []);
|
|
1066
|
+
};
|
|
1067
|
+
const placeOrigins = (place, aliases) => {
|
|
1068
|
+
const base = place?.base;
|
|
1069
|
+
if (base?.kind === "parameter" && Number.isInteger(base.data)) {
|
|
1070
|
+
return new Set([parameterOrigin(base.data, 0)]);
|
|
1071
|
+
}
|
|
1072
|
+
if (base?.kind === "local" && Number.isInteger(base.data)) {
|
|
1073
|
+
return new Set(aliases[base.data] ?? []);
|
|
1074
|
+
}
|
|
1075
|
+
return new Set();
|
|
1076
|
+
};
|
|
1077
|
+
const rvalueOrigins = (value, aliases, func) => {
|
|
1078
|
+
if (value?.kind === "use") {
|
|
1079
|
+
return valueOrigins(value.data, aliases);
|
|
1080
|
+
}
|
|
1081
|
+
if (value?.kind === "load") {
|
|
1082
|
+
return placeOrigins(value.data, aliases);
|
|
1083
|
+
}
|
|
1084
|
+
if (value?.kind !== "make_slice") {
|
|
1085
|
+
return new Set();
|
|
1086
|
+
}
|
|
1087
|
+
const source = value.data?.source;
|
|
1088
|
+
if (source?.kind === "buffer") {
|
|
1089
|
+
return bufferOrigins(source.data?.buffer);
|
|
1090
|
+
}
|
|
1091
|
+
if (source?.kind === "buffer_param") {
|
|
1092
|
+
return bufferParamOrigins(source.data?.parameter, func);
|
|
1093
|
+
}
|
|
1094
|
+
if (source?.kind === "place") {
|
|
1095
|
+
return placeOrigins(source.data, aliases);
|
|
1096
|
+
}
|
|
1097
|
+
return new Set();
|
|
1098
|
+
};
|
|
1099
|
+
const collectAliases = (func) => {
|
|
1100
|
+
const aliases = (func.locals ?? []).map(() => new Set());
|
|
1101
|
+
let changed = true;
|
|
1102
|
+
const visitBlock = (block) => {
|
|
1103
|
+
for (const statement of block?.statements ?? []) {
|
|
1104
|
+
const kind = statement.kind?.kind;
|
|
1105
|
+
const data = statement.kind?.data;
|
|
1106
|
+
if (
|
|
1107
|
+
kind === "assign"
|
|
1108
|
+
&& data?.destination?.projections?.length === 0
|
|
1109
|
+
&& data.destination.base?.kind === "local"
|
|
1110
|
+
) {
|
|
1111
|
+
const destination = data.destination.base.data;
|
|
1112
|
+
const origins = rvalueOrigins(data.value, aliases, func);
|
|
1113
|
+
for (const origin of origins) {
|
|
1114
|
+
if (!aliases[destination].has(origin)) {
|
|
1115
|
+
aliases[destination].add(origin);
|
|
1116
|
+
changed = true;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
} else if (kind === "if") {
|
|
1120
|
+
visitBlock(data?.then_block);
|
|
1121
|
+
visitBlock(data?.else_block);
|
|
1122
|
+
} else if (kind === "loop") {
|
|
1123
|
+
visitBlock(data?.body);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
while (changed) {
|
|
1128
|
+
changed = false;
|
|
1129
|
+
visitBlock(func.body);
|
|
1130
|
+
}
|
|
1131
|
+
return aliases;
|
|
1132
|
+
};
|
|
1133
|
+
const collectUnsupportedResults = (func) => {
|
|
1134
|
+
const results = new Set();
|
|
1135
|
+
const visitBlock = (block) => {
|
|
1136
|
+
for (const statement of block?.statements ?? []) {
|
|
1137
|
+
const kind = statement.kind?.kind;
|
|
1138
|
+
const data = statement.kind?.data;
|
|
1139
|
+
if (kind === "call") {
|
|
1140
|
+
for (const result of data?.results ?? []) {
|
|
1141
|
+
const type = this.mir.types[func.locals?.[result]?.ty];
|
|
1142
|
+
if (type?.kind === "slice" || type?.kind === "buffer") {
|
|
1143
|
+
results.add(result);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
} else if (kind === "if") {
|
|
1147
|
+
visitBlock(data?.then_block);
|
|
1148
|
+
visitBlock(data?.else_block);
|
|
1149
|
+
} else if (kind === "loop") {
|
|
1150
|
+
visitBlock(data?.body);
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
};
|
|
1154
|
+
visitBlock(func.body);
|
|
1155
|
+
return results;
|
|
1156
|
+
};
|
|
1157
|
+
const argumentValue = (argument) => {
|
|
1158
|
+
switch (argument?.kind) {
|
|
1159
|
+
case "value": return argument.data;
|
|
1160
|
+
case "slice_element":
|
|
1161
|
+
case "slice_window": return argument.data?.slice;
|
|
1162
|
+
default: return null;
|
|
1163
|
+
}
|
|
1164
|
+
};
|
|
1165
|
+
const argumentUsesUnsupportedResult = (argument, unsupported) => {
|
|
1166
|
+
const value = argumentValue(argument);
|
|
1167
|
+
const valueLocal = localId(value);
|
|
1168
|
+
if (valueLocal !== null) {
|
|
1169
|
+
return unsupported.has(valueLocal);
|
|
1170
|
+
}
|
|
1171
|
+
if (argument?.kind === "place") {
|
|
1172
|
+
const base = argument.data?.base;
|
|
1173
|
+
return base?.kind === "local" && unsupported.has(base.data);
|
|
1174
|
+
}
|
|
1175
|
+
if (argument?.kind === "array_window") {
|
|
1176
|
+
const base = argument.data?.array?.base;
|
|
1177
|
+
return base?.kind === "local" && unsupported.has(base.data);
|
|
1178
|
+
}
|
|
1179
|
+
return false;
|
|
1180
|
+
};
|
|
1181
|
+
const argumentOrigins = (argument, aliases, func, slot) => {
|
|
1182
|
+
switch (argument?.kind) {
|
|
1183
|
+
case "buffer":
|
|
1184
|
+
return bufferOrigins(argument.data);
|
|
1185
|
+
case "buffer_param":
|
|
1186
|
+
return bufferParamOrigins(argument.data, func);
|
|
1187
|
+
case "buffer_span": {
|
|
1188
|
+
const span = argument.data;
|
|
1189
|
+
if (span?.kind === "interface") {
|
|
1190
|
+
const len = span.data?.len ?? 0;
|
|
1191
|
+
if (Number.isInteger(slot) && slot >= 0 && slot < len) {
|
|
1192
|
+
return new Set([bufferOrigin(span.data.first + slot)]);
|
|
1193
|
+
}
|
|
1194
|
+
return new Set(Array.from({ length: len }, (_, index) =>
|
|
1195
|
+
bufferOrigin(span.data.first + index)
|
|
1196
|
+
));
|
|
1197
|
+
}
|
|
1198
|
+
if (span?.kind === "parameter") {
|
|
1199
|
+
const start = span.data?.start ?? 0;
|
|
1200
|
+
const len = span.data?.len ?? 0;
|
|
1201
|
+
if (Number.isInteger(slot) && slot >= 0 && slot < len) {
|
|
1202
|
+
return new Set([parameterOrigin(span.data.span, start + slot)]);
|
|
1203
|
+
}
|
|
1204
|
+
return new Set(Array.from({ length: len }, (_, index) =>
|
|
1205
|
+
parameterOrigin(span.data.span, start + index)
|
|
1206
|
+
));
|
|
1207
|
+
}
|
|
1208
|
+
return new Set();
|
|
1209
|
+
}
|
|
1210
|
+
case "place":
|
|
1211
|
+
return placeOrigins(argument.data, aliases);
|
|
1212
|
+
case "array_window":
|
|
1213
|
+
return placeOrigins(argument.data?.array, aliases);
|
|
1214
|
+
case "value":
|
|
1215
|
+
return valueOrigins(argument.data, aliases);
|
|
1216
|
+
case "slice_element":
|
|
1217
|
+
case "slice_window":
|
|
1218
|
+
return valueOrigins(argument.data?.slice, aliases);
|
|
1219
|
+
default:
|
|
1220
|
+
return new Set();
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
const markOrigins = (origins, summary) => {
|
|
1224
|
+
for (const origin of origins) {
|
|
1225
|
+
const [kind, idText] = origin.split(":");
|
|
1226
|
+
const id = Number(idText);
|
|
1227
|
+
if (kind === "buffer") {
|
|
1228
|
+
summary.buffers.add(id);
|
|
1229
|
+
} else if (kind === "parameter") {
|
|
1230
|
+
summary.parameters.add(origin);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
|
|
1235
|
+
const aliases = this.mir.functions.map(collectAliases);
|
|
1236
|
+
const unsupported = this.mir.functions.map(collectUnsupportedResults);
|
|
1237
|
+
let summaries = this.mir.functions.map(() => ({
|
|
1238
|
+
buffers: new Set(),
|
|
1239
|
+
parameters: new Set(),
|
|
1240
|
+
}));
|
|
1241
|
+
while (true) {
|
|
1242
|
+
const next = this.mir.functions.map((func, functionId) => {
|
|
1243
|
+
const summary = { buffers: new Set(), parameters: new Set() };
|
|
1244
|
+
const markValueWrite = (value, description) => {
|
|
1245
|
+
const id = localId(value);
|
|
1246
|
+
if (id !== null && unsupported[functionId].has(id)) {
|
|
1247
|
+
this.fail(
|
|
1248
|
+
`cannot infer interface-buffer writes for ${description} through a slice returned by a MIR call`,
|
|
1249
|
+
);
|
|
1250
|
+
}
|
|
1251
|
+
markOrigins(valueOrigins(value, aliases[functionId]), summary);
|
|
1252
|
+
};
|
|
1253
|
+
const visitBlock = (block) => {
|
|
1254
|
+
for (const statement of block?.statements ?? []) {
|
|
1255
|
+
const kind = statement.kind?.kind;
|
|
1256
|
+
const data = statement.kind?.data;
|
|
1257
|
+
if (
|
|
1258
|
+
kind === "assign"
|
|
1259
|
+
&& data?.destination?.base?.kind === "parameter"
|
|
1260
|
+
) {
|
|
1261
|
+
summary.parameters.add(parameterOrigin(data.destination.base.data, 0));
|
|
1262
|
+
} else if (kind === "buffer_store") {
|
|
1263
|
+
for (const buffer of bufferIds(data?.buffer)) {
|
|
1264
|
+
summary.buffers.add(buffer);
|
|
1265
|
+
}
|
|
1266
|
+
} else if (kind === "buffer_param_store") {
|
|
1267
|
+
markOrigins(
|
|
1268
|
+
bufferParamOrigins(data?.parameter, func),
|
|
1269
|
+
summary,
|
|
1270
|
+
);
|
|
1271
|
+
} else if (kind === "slice_store") {
|
|
1272
|
+
markValueWrite(data?.slice, "slice store");
|
|
1273
|
+
} else if (kind === "slice_fill" || kind === "slice_copy") {
|
|
1274
|
+
markValueWrite(data?.destination, "slice write");
|
|
1275
|
+
} else if (kind === "call") {
|
|
1276
|
+
const callee = summaries[data?.function];
|
|
1277
|
+
if (!callee) {
|
|
1278
|
+
this.fail(
|
|
1279
|
+
`MIR call references missing function ${String(data?.function)}`,
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
for (const buffer of callee.buffers) {
|
|
1283
|
+
summary.buffers.add(buffer);
|
|
1284
|
+
}
|
|
1285
|
+
for (const parameterOriginValue of callee.parameters) {
|
|
1286
|
+
const [, parameterText, slotText] = parameterOriginValue.split(":");
|
|
1287
|
+
const parameter = Number(parameterText);
|
|
1288
|
+
const slot = Number(slotText);
|
|
1289
|
+
const argument = data?.args?.[parameter];
|
|
1290
|
+
if (!argument) {
|
|
1291
|
+
this.fail(
|
|
1292
|
+
`MIR call to function ${data.function} has no argument for writable parameter ${parameter}`,
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
if (
|
|
1296
|
+
argumentUsesUnsupportedResult(
|
|
1297
|
+
argument,
|
|
1298
|
+
unsupported[functionId],
|
|
1299
|
+
)
|
|
1300
|
+
) {
|
|
1301
|
+
this.fail(
|
|
1302
|
+
"cannot infer interface-buffer writes through a slice or buffer returned by a MIR call",
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
markOrigins(
|
|
1306
|
+
argumentOrigins(argument, aliases[functionId], func, slot),
|
|
1307
|
+
summary,
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
} else if (kind === "if") {
|
|
1311
|
+
visitBlock(data?.then_block);
|
|
1312
|
+
visitBlock(data?.else_block);
|
|
1313
|
+
} else if (kind === "loop") {
|
|
1314
|
+
visitBlock(data?.body);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
visitBlock(func.body);
|
|
1319
|
+
return summary;
|
|
1320
|
+
});
|
|
1321
|
+
if (next.every((summary, index) => summaryEquals(summary, summaries[index]))) {
|
|
1322
|
+
summaries = next;
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
summaries = next;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const roots = [
|
|
1329
|
+
this.mir.entry_points.init,
|
|
1330
|
+
this.mir.entry_points.process,
|
|
1331
|
+
...this.mir.interface.events.map((event) => event.handler),
|
|
1332
|
+
];
|
|
1333
|
+
this.bufferMayWrite = this.mir.interface.buffers.map(() => false);
|
|
1334
|
+
for (const root of roots) {
|
|
1335
|
+
const summary = summaries[root];
|
|
1336
|
+
if (!summary) {
|
|
1337
|
+
this.fail(`MIR buffer-write root function ${String(root)} is missing`);
|
|
1338
|
+
}
|
|
1339
|
+
for (const bufferId of summary.buffers) {
|
|
1340
|
+
if (
|
|
1341
|
+
!Number.isInteger(bufferId)
|
|
1342
|
+
|| bufferId < 0
|
|
1343
|
+
|| bufferId >= this.bufferMayWrite.length
|
|
1344
|
+
) {
|
|
1345
|
+
this.fail(
|
|
1346
|
+
`MIR buffer-write analysis references missing buffer ${String(bufferId)}`,
|
|
1347
|
+
);
|
|
1348
|
+
}
|
|
1349
|
+
this.bufferMayWrite[bufferId] = true;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
for (const [bufferId, mayWrite] of this.bufferMayWrite.entries()) {
|
|
1353
|
+
if (mayWrite && this.mir.interface.buffers[bufferId].access !== "read_write") {
|
|
1354
|
+
this.fail(
|
|
1355
|
+
`MIR writes read-only interface buffer '${this.mir.interface.buffers[bufferId].name}'`,
|
|
1356
|
+
);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
analyzeRecoverableFailures() {
|
|
1362
|
+
const callees = this.mir.functions.map(() => new Set());
|
|
1363
|
+
const direct = this.mir.functions.map(() => false);
|
|
1364
|
+
const binaryMayFail = (functionId, value) => {
|
|
1365
|
+
if (
|
|
1366
|
+
value.kind !== "binary"
|
|
1367
|
+
|| !["divide", "remainder"].includes(value.data?.op)
|
|
1368
|
+
) {
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1371
|
+
const lhs = value.data.lhs;
|
|
1372
|
+
const scalar = lhs.kind === "constant"
|
|
1373
|
+
? lhs.data.type
|
|
1374
|
+
: this.requireScalarType(
|
|
1375
|
+
this.mir.functions[functionId].locals[lhs.data].ty,
|
|
1376
|
+
`binary operand in '${this.mir.functions[functionId].name}'`,
|
|
1377
|
+
);
|
|
1378
|
+
return scalar === "i32" || scalar === "i64";
|
|
1379
|
+
};
|
|
1380
|
+
const checkedBoundsKinds = new Set([
|
|
1381
|
+
"array_window",
|
|
1382
|
+
"buffer_load",
|
|
1383
|
+
"buffer_param_load",
|
|
1384
|
+
"buffer_param_store",
|
|
1385
|
+
"buffer_store",
|
|
1386
|
+
"const_data_load",
|
|
1387
|
+
"index",
|
|
1388
|
+
"input_load",
|
|
1389
|
+
"make_slice",
|
|
1390
|
+
"output_load",
|
|
1391
|
+
"output_store",
|
|
1392
|
+
"control_output_store",
|
|
1393
|
+
]);
|
|
1394
|
+
const dynamicBoundsKinds = new Set([
|
|
1395
|
+
"slice_element",
|
|
1396
|
+
"slice_load",
|
|
1397
|
+
"slice_store",
|
|
1398
|
+
"slice_window",
|
|
1399
|
+
]);
|
|
1400
|
+
const scan = (functionId, value) => {
|
|
1401
|
+
if (value === null || value === undefined) return;
|
|
1402
|
+
if (Array.isArray(value)) {
|
|
1403
|
+
for (const entry of value) scan(functionId, entry);
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
if (typeof value !== "object") return;
|
|
1407
|
+
if (value.kind === "call" && Number.isInteger(value.data?.function)) {
|
|
1408
|
+
callees[functionId].add(value.data.function);
|
|
1409
|
+
}
|
|
1410
|
+
const fixedDelegatePayloadMayFail = value.kind === "publish_delegate"
|
|
1411
|
+
&& this.mir.interface.delegates[value.data?.delegate]?.params.some(
|
|
1412
|
+
(param) => this.type(param.ty).kind === "array",
|
|
1413
|
+
);
|
|
1414
|
+
const bounds = value.data?.bounds;
|
|
1415
|
+
if (
|
|
1416
|
+
value.kind === "process_frame"
|
|
1417
|
+
|| value.kind === "slice_copy"
|
|
1418
|
+
|| fixedDelegatePayloadMayFail
|
|
1419
|
+
|| binaryMayFail(functionId, value)
|
|
1420
|
+
|| (checkedBoundsKinds.has(value.kind) && bounds === "checked")
|
|
1421
|
+
|| (dynamicBoundsKinds.has(value.kind) && bounds !== "unchecked")
|
|
1422
|
+
) {
|
|
1423
|
+
direct[functionId] = true;
|
|
1424
|
+
}
|
|
1425
|
+
for (const child of Object.values(value)) scan(functionId, child);
|
|
1426
|
+
};
|
|
1427
|
+
for (const [functionId, func] of this.mir.functions.entries()) {
|
|
1428
|
+
scan(functionId, func.body);
|
|
1429
|
+
}
|
|
1430
|
+
this.functionMayFail = [...direct];
|
|
1431
|
+
let changed = true;
|
|
1432
|
+
while (changed) {
|
|
1433
|
+
changed = false;
|
|
1434
|
+
for (const [functionId, targets] of callees.entries()) {
|
|
1435
|
+
if (
|
|
1436
|
+
!this.functionMayFail[functionId]
|
|
1437
|
+
&& [...targets].some((target) => this.functionMayFail[target])
|
|
1438
|
+
) {
|
|
1439
|
+
this.functionMayFail[functionId] = true;
|
|
1440
|
+
changed = true;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
buildLayouts() {
|
|
1447
|
+
this.stateLayout = this.layoutNamedValues(this.mir.state);
|
|
1448
|
+
this.paramLayout = this.layoutNamedValues(this.mir.interface.params);
|
|
1449
|
+
this.inputLayout = this.layoutPorts(this.mir.interface.inputs);
|
|
1450
|
+
this.outputLayout = this.layoutPorts(this.mir.interface.outputs);
|
|
1451
|
+
this.controlOutputLayout = this.layoutControlOutputs();
|
|
1452
|
+
this.eventLayout = this.mir.interface.events.map((event) =>
|
|
1453
|
+
this.layoutEventValues(event.params),
|
|
1454
|
+
);
|
|
1455
|
+
this.delegateLayout = this.mir.interface.delegates.map((delegate) =>
|
|
1456
|
+
this.layoutEventValues(delegate.params),
|
|
1457
|
+
);
|
|
1458
|
+
this.requireWasm32Extent(
|
|
1459
|
+
this.stateLayout.byteLength,
|
|
1460
|
+
"MIR physical state storage",
|
|
1461
|
+
);
|
|
1462
|
+
this.requireWasm32Extent(
|
|
1463
|
+
this.paramLayout.byteLength,
|
|
1464
|
+
"MIR parameter storage",
|
|
1465
|
+
);
|
|
1466
|
+
for (const [eventId, layout] of this.eventLayout.entries()) {
|
|
1467
|
+
this.requireWasm32Extent(
|
|
1468
|
+
layout.minimumByteLength,
|
|
1469
|
+
`MIR event ${eventId} fixed payload storage`,
|
|
1470
|
+
);
|
|
1471
|
+
}
|
|
1472
|
+
for (const [delegateId, layout] of this.delegateLayout.entries()) {
|
|
1473
|
+
this.requireWasm32Extent(
|
|
1474
|
+
layout.minimumByteLength,
|
|
1475
|
+
`MIR delegate ${delegateId} fixed payload storage`,
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
for (let id = 0; id < this.mir.const_data.length; id += 1) {
|
|
1480
|
+
const data = this.mir.const_data[id];
|
|
1481
|
+
const scalar = data.element;
|
|
1482
|
+
const size = this.scalarSize(scalar);
|
|
1483
|
+
this.nextStaticAddress = alignUp(this.nextStaticAddress, size);
|
|
1484
|
+
const address = this.nextStaticAddress;
|
|
1485
|
+
const bytes = encodeScalarValues(data.values, scalar, this);
|
|
1486
|
+
this.memorySegments.push({
|
|
1487
|
+
offset: this.module.i32.const(address),
|
|
1488
|
+
data: bytes,
|
|
1489
|
+
});
|
|
1490
|
+
this.constLayout.push({ address, scalar, len: data.values.length });
|
|
1491
|
+
this.nextStaticAddress += bytes.byteLength;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
this.localArrayLayout = this.mir.functions.map((func) =>
|
|
1495
|
+
func.locals.map((local) => {
|
|
1496
|
+
const type = this.type(local.ty);
|
|
1497
|
+
if (type.kind !== "array") return null;
|
|
1498
|
+
const layout = this.typeLayout(local.ty);
|
|
1499
|
+
this.nextStaticAddress = alignUp(this.nextStaticAddress, layout.align);
|
|
1500
|
+
const address = this.nextStaticAddress;
|
|
1501
|
+
this.nextStaticAddress += layout.size;
|
|
1502
|
+
return { ...layout, address };
|
|
1503
|
+
}),
|
|
1504
|
+
);
|
|
1505
|
+
this.localScalarRefLayout = this.mir.functions.map((func, functionId) => {
|
|
1506
|
+
const addressTaken = this.collectAddressTakenScalarLocals(functionId);
|
|
1507
|
+
return func.locals.map((local, localId) => {
|
|
1508
|
+
if (!addressTaken.has(localId)) return null;
|
|
1509
|
+
const type = this.type(local.ty);
|
|
1510
|
+
if (type.kind !== "scalar") return null;
|
|
1511
|
+
const size = this.scalarSize(type.data);
|
|
1512
|
+
this.nextStaticAddress = alignUp(this.nextStaticAddress, size);
|
|
1513
|
+
const address = this.nextStaticAddress;
|
|
1514
|
+
this.nextStaticAddress += size;
|
|
1515
|
+
return { address, scalar: type.data, size };
|
|
1516
|
+
});
|
|
1517
|
+
});
|
|
1518
|
+
if (this.mir.interface.buffers.length > 0) {
|
|
1519
|
+
const fallbackBytes = Math.max(
|
|
1520
|
+
...this.mir.interface.buffers.map((buffer) =>
|
|
1521
|
+
this.scalarSize(buffer.element)),
|
|
1522
|
+
);
|
|
1523
|
+
this.nextStaticAddress = alignUp(this.nextStaticAddress, 8);
|
|
1524
|
+
this.fallbackBufferReadAddress = this.nextStaticAddress;
|
|
1525
|
+
this.nextStaticAddress += fallbackBytes;
|
|
1526
|
+
this.nextStaticAddress = alignUp(this.nextStaticAddress, 8);
|
|
1527
|
+
this.fallbackBufferWriteAddress = this.nextStaticAddress;
|
|
1528
|
+
this.nextStaticAddress += fallbackBytes;
|
|
1529
|
+
}
|
|
1530
|
+
this.nextStaticAddress = alignUp(this.nextStaticAddress, 16);
|
|
1531
|
+
this.requireWasm32Extent(this.nextStaticAddress, "MIR static storage");
|
|
1532
|
+
this.requireWasm32Extent(
|
|
1533
|
+
this.nextStaticAddress
|
|
1534
|
+
+ this.paramLayout.byteLength
|
|
1535
|
+
+ this.stateLayout.byteLength,
|
|
1536
|
+
"MIR static, parameter, and physical state storage",
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
collectAddressTakenScalarLocals(functionId) {
|
|
1541
|
+
const result = new Set();
|
|
1542
|
+
const visitBlock = (block) => {
|
|
1543
|
+
for (const statement of block.statements) {
|
|
1544
|
+
const kind = statement.kind?.kind;
|
|
1545
|
+
const data = statement.kind?.data;
|
|
1546
|
+
if (kind === "call") {
|
|
1547
|
+
const target = this.mir.functions[data.function];
|
|
1548
|
+
data.args.forEach((argument, index) => {
|
|
1549
|
+
const parameter = target?.params[index];
|
|
1550
|
+
const type = parameter && this.type(parameter.ty);
|
|
1551
|
+
if (
|
|
1552
|
+
this.parameterPassingMode(data.function, index) !== "value"
|
|
1553
|
+
&& type?.kind === "scalar"
|
|
1554
|
+
&& argument.kind === "place"
|
|
1555
|
+
&& argument.data.base.kind === "local"
|
|
1556
|
+
&& argument.data.projections.length === 0
|
|
1557
|
+
) {
|
|
1558
|
+
result.add(argument.data.base.data);
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
} else if (kind === "if") {
|
|
1562
|
+
visitBlock(data.then_block);
|
|
1563
|
+
visitBlock(data.else_block);
|
|
1564
|
+
} else if (kind === "loop") {
|
|
1565
|
+
visitBlock(data.body);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
};
|
|
1569
|
+
visitBlock(this.mir.functions[functionId].body);
|
|
1570
|
+
return result;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
layoutNamedValues(values) {
|
|
1574
|
+
let offset = 0;
|
|
1575
|
+
const result = [];
|
|
1576
|
+
for (const value of values) {
|
|
1577
|
+
const layout = this.typeLayout(value.ty);
|
|
1578
|
+
offset = alignUp(offset, layout.align);
|
|
1579
|
+
result.push({ ...layout, offset });
|
|
1580
|
+
offset += layout.size;
|
|
1581
|
+
}
|
|
1582
|
+
result.byteLength = alignUp(offset, 16);
|
|
1583
|
+
return result;
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
layoutControlOutputs() {
|
|
1587
|
+
return this.mir.interface.control_outputs.map((output) => {
|
|
1588
|
+
const layout = this.typeLayout(output.ty);
|
|
1589
|
+
return {
|
|
1590
|
+
...layout,
|
|
1591
|
+
offset: this.stateLayout[output.mirror].offset,
|
|
1592
|
+
};
|
|
1593
|
+
});
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
layoutEventValues(values) {
|
|
1597
|
+
let offset = 0;
|
|
1598
|
+
let dynamic = false;
|
|
1599
|
+
const result = values.map((value) => {
|
|
1600
|
+
const type = this.type(value.ty);
|
|
1601
|
+
if (type.kind === "slice") {
|
|
1602
|
+
const entry = {
|
|
1603
|
+
offset: dynamic ? null : offset,
|
|
1604
|
+
size: null,
|
|
1605
|
+
dynamic: true,
|
|
1606
|
+
headerSize: 4,
|
|
1607
|
+
scalar: type.data.element,
|
|
1608
|
+
};
|
|
1609
|
+
offset += 4;
|
|
1610
|
+
dynamic = true;
|
|
1611
|
+
return entry;
|
|
1612
|
+
}
|
|
1613
|
+
const layout = this.typeLayout(value.ty);
|
|
1614
|
+
const entry = { ...layout, offset: dynamic ? null : offset, dynamic: false };
|
|
1615
|
+
offset += layout.size;
|
|
1616
|
+
return entry;
|
|
1617
|
+
});
|
|
1618
|
+
result.byteLength = dynamic ? null : offset;
|
|
1619
|
+
result.minimumByteLength = offset;
|
|
1620
|
+
result.dynamic = dynamic;
|
|
1621
|
+
return result;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
layoutPorts(ports) {
|
|
1625
|
+
let channel = 0;
|
|
1626
|
+
return ports.map((port, portId) => {
|
|
1627
|
+
const type = this.type(port.ty);
|
|
1628
|
+
const flattened = this.flattenPortType(type);
|
|
1629
|
+
this.requireWasm32Extent(
|
|
1630
|
+
this.scalarSize(flattened.scalar) * this.mir.config.block_size,
|
|
1631
|
+
`MIR audio port ${portId} channel storage`,
|
|
1632
|
+
);
|
|
1633
|
+
const result = {
|
|
1634
|
+
channel,
|
|
1635
|
+
channels: flattened.channels,
|
|
1636
|
+
scalar: flattened.scalar,
|
|
1637
|
+
size: this.scalarSize(flattened.scalar),
|
|
1638
|
+
isArray: type.kind === "array",
|
|
1639
|
+
};
|
|
1640
|
+
channel += flattened.channels;
|
|
1641
|
+
return result;
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
flattenPortType(type) {
|
|
1646
|
+
if (type.kind === "scalar") {
|
|
1647
|
+
return { scalar: type.data, channels: 1 };
|
|
1648
|
+
}
|
|
1649
|
+
if (type.kind === "array") {
|
|
1650
|
+
const element = this.type(type.data.element);
|
|
1651
|
+
if (element.kind !== "scalar") {
|
|
1652
|
+
this.fail("nested aggregate audio ports are not supported yet");
|
|
1653
|
+
}
|
|
1654
|
+
return { scalar: element.data, channels: type.data.len };
|
|
1655
|
+
}
|
|
1656
|
+
this.fail(`audio port type '${type.kind}' is not supported yet`);
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
addMemoryAndContextGlobals() {
|
|
1660
|
+
const initialPages = Math.max(
|
|
1661
|
+
1,
|
|
1662
|
+
Math.ceil(this.nextStaticAddress / PAGE_BYTES),
|
|
1663
|
+
);
|
|
1664
|
+
if (initialPages > MAX_MEMORY_PAGES) {
|
|
1665
|
+
this.fail(
|
|
1666
|
+
`MIR static storage requires ${initialPages} Wasm pages, exceeding the Wasm32 limit`,
|
|
1667
|
+
);
|
|
1668
|
+
}
|
|
1669
|
+
this.module.setMemory(
|
|
1670
|
+
initialPages,
|
|
1671
|
+
MAX_MEMORY_PAGES,
|
|
1672
|
+
"memory",
|
|
1673
|
+
this.memorySegments,
|
|
1674
|
+
);
|
|
1675
|
+
this.module.addGlobal(
|
|
1676
|
+
"__heap_base",
|
|
1677
|
+
binaryen.i32,
|
|
1678
|
+
false,
|
|
1679
|
+
this.module.i32.const(this.nextStaticAddress),
|
|
1680
|
+
);
|
|
1681
|
+
this.module.addGlobalExport("__heap_base", "__heap_base");
|
|
1682
|
+
|
|
1683
|
+
for (const name of Object.values(POINTER_GLOBALS)) {
|
|
1684
|
+
this.module.addGlobal(name, binaryen.i32, true, this.module.i32.const(0));
|
|
1685
|
+
}
|
|
1686
|
+
this.module.addGlobal(
|
|
1687
|
+
RUNTIME_FAILURE_GLOBAL,
|
|
1688
|
+
binaryen.i32,
|
|
1689
|
+
true,
|
|
1690
|
+
this.module.i32.const(0),
|
|
1691
|
+
);
|
|
1692
|
+
this.module.addGlobal(
|
|
1693
|
+
INIT_ALL_GLOBAL,
|
|
1694
|
+
binaryen.i32,
|
|
1695
|
+
true,
|
|
1696
|
+
this.module.i32.const(0),
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
addMathKernel() {
|
|
1701
|
+
if (this.requiredMathHelpers.size === 0) return;
|
|
1702
|
+
|
|
1703
|
+
const source = binaryen.readBinary(ONDA_MATH_KERNEL_WASM);
|
|
1704
|
+
try {
|
|
1705
|
+
if (
|
|
1706
|
+
source.getNumGlobals() !== 1
|
|
1707
|
+
|| source.getNumTables() !== 0
|
|
1708
|
+
|| source.getNumDataSegments() !== 1
|
|
1709
|
+
) {
|
|
1710
|
+
this.fail("embedded Wasm math kernel has an unsupported module shape");
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
for (let index = source.getNumExports() - 1; index >= 0; index -= 1) {
|
|
1714
|
+
const exported = binaryen.getExportInfo(source.getExportByIndex(index));
|
|
1715
|
+
if (!this.requiredMathHelpers.has(exported.name)) {
|
|
1716
|
+
source.removeExport(exported.name);
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
source.runPasses(["remove-unused-module-elements"]);
|
|
1720
|
+
|
|
1721
|
+
if (source.getNumGlobals() > 1 || source.getNumDataSegments() > 1) {
|
|
1722
|
+
this.fail("optimized Wasm math kernel has an unsupported module shape");
|
|
1723
|
+
}
|
|
1724
|
+
if (source.getNumGlobals() === 1) {
|
|
1725
|
+
const global = binaryen.getGlobalInfo(source.getGlobalByIndex(0));
|
|
1726
|
+
if (
|
|
1727
|
+
global.module
|
|
1728
|
+
|| global.name !== MATH_KERNEL_STACK_GLOBAL
|
|
1729
|
+
|| global.type !== binaryen.i32
|
|
1730
|
+
|| !global.mutable
|
|
1731
|
+
) {
|
|
1732
|
+
this.fail("embedded Wasm math kernel has an invalid stack global");
|
|
1733
|
+
}
|
|
1734
|
+
this.module.addGlobal(
|
|
1735
|
+
global.name,
|
|
1736
|
+
global.type,
|
|
1737
|
+
global.mutable,
|
|
1738
|
+
this.module.copyExpression(global.init),
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
if (source.getNumDataSegments() === 1) {
|
|
1743
|
+
const segment = source.getDataSegmentInfo(source.getDataSegmentByIndex(0));
|
|
1744
|
+
if (
|
|
1745
|
+
segment.name !== MATH_KERNEL_DATA_SEGMENT
|
|
1746
|
+
|| segment.passive
|
|
1747
|
+
|| !Number.isInteger(segment.offset)
|
|
1748
|
+
|| segment.offset < STATIC_BASE
|
|
1749
|
+
|| segment.offset + segment.data.byteLength > MATH_KERNEL_RESERVED_END
|
|
1750
|
+
) {
|
|
1751
|
+
this.fail("embedded Wasm math kernel exceeds its reserved memory region");
|
|
1752
|
+
}
|
|
1753
|
+
this.memorySegments.push({
|
|
1754
|
+
offset: this.module.i32.const(segment.offset),
|
|
1755
|
+
data: new Uint8Array(segment.data),
|
|
1756
|
+
});
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
this.module.setFeatures(this.module.getFeatures() | source.getFeatures());
|
|
1760
|
+
for (let index = 0; index < source.getNumFunctions(); index += 1) {
|
|
1761
|
+
const func = binaryen.getFunctionInfo(source.getFunctionByIndex(index));
|
|
1762
|
+
if (func.module || !func.body) {
|
|
1763
|
+
this.fail("embedded Wasm math kernel must not import functions");
|
|
1764
|
+
}
|
|
1765
|
+
this.module.addFunction(
|
|
1766
|
+
func.name,
|
|
1767
|
+
func.params,
|
|
1768
|
+
func.results,
|
|
1769
|
+
func.vars,
|
|
1770
|
+
this.module.copyExpression(func.body),
|
|
1771
|
+
);
|
|
1772
|
+
}
|
|
1773
|
+
} finally {
|
|
1774
|
+
source.dispose();
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
addMirFunctions() {
|
|
1779
|
+
this.functionNames = this.mir.functions.map((_, id) => `$onda.fn.${id}`);
|
|
1780
|
+
for (let id = 0; id < this.mir.functions.length; id += 1) {
|
|
1781
|
+
this.addMirFunction(id, this.mir.functions[id]);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
addMirFunction(id, func) {
|
|
1786
|
+
let nextIndex = 0;
|
|
1787
|
+
const paramLayouts = func.params.map((param, parameterId) => {
|
|
1788
|
+
const layout = this.functionValueLayout(
|
|
1789
|
+
param.ty,
|
|
1790
|
+
nextIndex,
|
|
1791
|
+
`parameter '${param.name}'`,
|
|
1792
|
+
false,
|
|
1793
|
+
this.parameterPassingMode(id, parameterId),
|
|
1794
|
+
);
|
|
1795
|
+
nextIndex += layout.components.length;
|
|
1796
|
+
return layout;
|
|
1797
|
+
});
|
|
1798
|
+
const paramScalars = paramLayouts.flatMap((layout) => layout.components);
|
|
1799
|
+
const localLayouts = func.locals.map((local, localId) => {
|
|
1800
|
+
const layout = this.functionValueLayout(
|
|
1801
|
+
local.ty,
|
|
1802
|
+
nextIndex,
|
|
1803
|
+
`local ${localId} of '${func.name}'`,
|
|
1804
|
+
true,
|
|
1805
|
+
);
|
|
1806
|
+
nextIndex += layout.components.length;
|
|
1807
|
+
return layout;
|
|
1808
|
+
});
|
|
1809
|
+
const flatLocalScalars = localLayouts.flatMap((layout) => layout.components);
|
|
1810
|
+
const localScalars = func.locals.map((local) => {
|
|
1811
|
+
const type = this.type(local.ty);
|
|
1812
|
+
return type.kind === "scalar" ? type.data : null;
|
|
1813
|
+
});
|
|
1814
|
+
const resultScalars = func.results.map((result, resultId) =>
|
|
1815
|
+
this.requireScalarType(result, `result ${resultId} of '${func.name}'`),
|
|
1816
|
+
);
|
|
1817
|
+
const callResultLocals = this.collectCallResultLocals(func);
|
|
1818
|
+
const sliceScratch = this.collectSliceScratchLocals(func);
|
|
1819
|
+
const processFrameLocals = this.collectProcessFrameLocals(func);
|
|
1820
|
+
const generatedLocalBase =
|
|
1821
|
+
paramScalars.length +
|
|
1822
|
+
flatLocalScalars.length +
|
|
1823
|
+
callResultLocals.length +
|
|
1824
|
+
sliceScratch.count;
|
|
1825
|
+
if (
|
|
1826
|
+
resultScalars.length > 1
|
|
1827
|
+
|| callResultLocals.some((entry) => entry.resultCount > 1)
|
|
1828
|
+
) {
|
|
1829
|
+
this.module.setFeatures(
|
|
1830
|
+
this.module.getFeatures() | binaryen.Features.Multivalue,
|
|
1831
|
+
);
|
|
1832
|
+
}
|
|
1833
|
+
const context = {
|
|
1834
|
+
function: func,
|
|
1835
|
+
functionId: id,
|
|
1836
|
+
paramScalars,
|
|
1837
|
+
paramLayouts,
|
|
1838
|
+
localScalars,
|
|
1839
|
+
localLayouts,
|
|
1840
|
+
flatLocalCount: flatLocalScalars.length,
|
|
1841
|
+
callResultLocals: new Map(
|
|
1842
|
+
callResultLocals.map((entry, index) => [
|
|
1843
|
+
entry.call,
|
|
1844
|
+
{
|
|
1845
|
+
index: paramScalars.length + flatLocalScalars.length + index,
|
|
1846
|
+
type: entry.type,
|
|
1847
|
+
},
|
|
1848
|
+
]),
|
|
1849
|
+
),
|
|
1850
|
+
sliceScratch: new Map(
|
|
1851
|
+
sliceScratch.entries.map((entry, index) => [
|
|
1852
|
+
entry.statement,
|
|
1853
|
+
{
|
|
1854
|
+
index:
|
|
1855
|
+
paramScalars.length +
|
|
1856
|
+
flatLocalScalars.length +
|
|
1857
|
+
callResultLocals.length +
|
|
1858
|
+
sliceScratch.offsets[index],
|
|
1859
|
+
count: entry.count,
|
|
1860
|
+
},
|
|
1861
|
+
]),
|
|
1862
|
+
),
|
|
1863
|
+
eventId: func.kind?.kind === "event" ? func.kind.data : null,
|
|
1864
|
+
processFrameLocals,
|
|
1865
|
+
generatedLocalBase,
|
|
1866
|
+
generatedLocals: [],
|
|
1867
|
+
entryInitializers: [],
|
|
1868
|
+
bufferDescriptorCache: new Map(),
|
|
1869
|
+
audioChannelPointerCache: new Map(),
|
|
1870
|
+
breakLabels: [],
|
|
1871
|
+
continueLabels: [],
|
|
1872
|
+
};
|
|
1873
|
+
const compiledBody = this.compileBlock(func.body, context);
|
|
1874
|
+
const body = context.entryInitializers.length === 0
|
|
1875
|
+
? compiledBody
|
|
1876
|
+
: this.module.block(null, [
|
|
1877
|
+
...context.entryInitializers,
|
|
1878
|
+
compiledBody,
|
|
1879
|
+
]);
|
|
1880
|
+
const functionRef = this.module.addFunction(
|
|
1881
|
+
this.functionNames[id],
|
|
1882
|
+
binaryen.createType(paramScalars.map((type) => this.wasmType(type))),
|
|
1883
|
+
this.wasmResultType(resultScalars),
|
|
1884
|
+
[
|
|
1885
|
+
...flatLocalScalars.map((type) => this.wasmType(type)),
|
|
1886
|
+
...callResultLocals.map((entry) => entry.type),
|
|
1887
|
+
...Array.from({ length: sliceScratch.count }, () => binaryen.i32),
|
|
1888
|
+
...context.generatedLocals.map((entry) => this.wasmType(entry.scalar)),
|
|
1889
|
+
],
|
|
1890
|
+
body,
|
|
1891
|
+
);
|
|
1892
|
+
for (let paramId = 0; paramId < func.params.length; paramId += 1) {
|
|
1893
|
+
this.setFunctionValueNames(
|
|
1894
|
+
functionRef,
|
|
1895
|
+
paramLayouts[paramId],
|
|
1896
|
+
`${func.params[paramId].name}.arg`,
|
|
1897
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
for (let localId = 0; localId < func.locals.length; localId += 1) {
|
|
1900
|
+
const name = func.locals[localId].name;
|
|
1901
|
+
if (name) {
|
|
1902
|
+
// Source names can repeat across disjoint lexical scopes while
|
|
1903
|
+
// Binaryen requires every debug local name in a function to be unique.
|
|
1904
|
+
// Keep the source spelling readable and make identity explicit with
|
|
1905
|
+
// the deterministic MIR local ID.
|
|
1906
|
+
this.setFunctionValueNames(
|
|
1907
|
+
functionRef,
|
|
1908
|
+
localLayouts[localId],
|
|
1909
|
+
`${name}.local${localId}`,
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
for (const local of context.generatedLocals) {
|
|
1914
|
+
binaryen.Function.setLocalName(functionRef, local.index, local.name);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
functionValueLayout(
|
|
1919
|
+
typeId,
|
|
1920
|
+
index,
|
|
1921
|
+
description,
|
|
1922
|
+
allowStorageOnly = false,
|
|
1923
|
+
passingMode = "value",
|
|
1924
|
+
) {
|
|
1925
|
+
const type = this.type(typeId);
|
|
1926
|
+
if (type.kind === "scalar") {
|
|
1927
|
+
if (passingMode !== "value") {
|
|
1928
|
+
return {
|
|
1929
|
+
index,
|
|
1930
|
+
typeId,
|
|
1931
|
+
kind: "scalar_ref",
|
|
1932
|
+
scalar: type.data,
|
|
1933
|
+
components: ["i32"],
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
return { index, typeId, kind: "scalar", components: [type.data] };
|
|
1937
|
+
}
|
|
1938
|
+
if (type.kind === "slice") {
|
|
1939
|
+
return {
|
|
1940
|
+
index,
|
|
1941
|
+
typeId,
|
|
1942
|
+
kind: "slice",
|
|
1943
|
+
components: ["i32", "i32", "i32", "i32"],
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
if (type.kind === "buffer") {
|
|
1947
|
+
this.bufferChannelMetadata(type.data.channels, type.data.element);
|
|
1948
|
+
return {
|
|
1949
|
+
index,
|
|
1950
|
+
typeId,
|
|
1951
|
+
kind: "buffer",
|
|
1952
|
+
components: ["i32", "i32", "i32", "i32", "f32", "i32"],
|
|
1953
|
+
};
|
|
1954
|
+
}
|
|
1955
|
+
if (type.kind === "buffer_span") {
|
|
1956
|
+
this.bufferChannelMetadata(type.data.channels, type.data.element);
|
|
1957
|
+
if (passingMode !== "value") {
|
|
1958
|
+
this.fail(`${description} buffer span must use value passing mode`);
|
|
1959
|
+
}
|
|
1960
|
+
return {
|
|
1961
|
+
index,
|
|
1962
|
+
typeId,
|
|
1963
|
+
kind: "buffer_span",
|
|
1964
|
+
components: ["i32", "i32", "i32", "i32", "i32", "i32"],
|
|
1965
|
+
};
|
|
1966
|
+
}
|
|
1967
|
+
if (type.kind === "array") {
|
|
1968
|
+
if (passingMode !== "value") {
|
|
1969
|
+
return { index, typeId, kind: "array_ref", components: ["i32"] };
|
|
1970
|
+
}
|
|
1971
|
+
if (allowStorageOnly) {
|
|
1972
|
+
return { index, typeId, kind: "array", components: [] };
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
this.fail(`${description} has unsupported function value type '${type.kind}'`);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
setFunctionValueNames(functionRef, layout, name) {
|
|
1979
|
+
if (layout.kind === "scalar") {
|
|
1980
|
+
binaryen.Function.setLocalName(functionRef, layout.index, name);
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
if (layout.kind === "scalar_ref") {
|
|
1984
|
+
binaryen.Function.setLocalName(functionRef, layout.index, `${name}.address`);
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (layout.kind === "array_ref") {
|
|
1988
|
+
binaryen.Function.setLocalName(functionRef, layout.index, `${name}.address`);
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
if (layout.kind === "array") return;
|
|
1992
|
+
const suffixes = layout.kind === "buffer"
|
|
1993
|
+
? ["read_address", "write_address", "frames", "channels", "sample_rate", "bound"]
|
|
1994
|
+
: layout.kind === "buffer_span"
|
|
1995
|
+
? ["read_table", "write_table", "frames_table", "channels_table", "sample_rates_table", "bound_table"]
|
|
1996
|
+
: ["read_address", "write_address", "length", "stride"];
|
|
1997
|
+
for (const [offset, suffix] of suffixes.entries()) {
|
|
1998
|
+
binaryen.Function.setLocalName(
|
|
1999
|
+
functionRef,
|
|
2000
|
+
layout.index + offset,
|
|
2001
|
+
`${name}.${suffix}`,
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
collectCallResultLocals(func) {
|
|
2007
|
+
const result = [];
|
|
2008
|
+
const visitBlock = (block) => {
|
|
2009
|
+
for (const statement of block.statements) {
|
|
2010
|
+
const kind = statement.kind?.kind;
|
|
2011
|
+
const data = statement.kind?.data;
|
|
2012
|
+
if (kind === "call" && data.results.length > 0) {
|
|
2013
|
+
this.requireFunctionId(data.function, "call target");
|
|
2014
|
+
const target = this.mir.functions[data.function];
|
|
2015
|
+
const aliasesResult = data.args.some((argument, index) =>
|
|
2016
|
+
this.parameterPassingMode(data.function, index) !== "value"
|
|
2017
|
+
&& argument.kind === "place"
|
|
2018
|
+
&& argument.data.base.kind === "local"
|
|
2019
|
+
&& argument.data.projections.length === 0
|
|
2020
|
+
&& this.type(target.params[index].ty).kind === "scalar"
|
|
2021
|
+
);
|
|
2022
|
+
if (data.results.length === 1 && !aliasesResult) continue;
|
|
2023
|
+
const scalars = target.results.map((typeId, resultId) =>
|
|
2024
|
+
this.requireScalarType(
|
|
2025
|
+
typeId,
|
|
2026
|
+
`result ${resultId} of '${target.name}'`,
|
|
2027
|
+
),
|
|
2028
|
+
);
|
|
2029
|
+
result.push({
|
|
2030
|
+
call: data,
|
|
2031
|
+
resultCount: scalars.length,
|
|
2032
|
+
type: binaryen.createType(
|
|
2033
|
+
scalars.map((scalar) => this.wasmType(scalar)),
|
|
2034
|
+
),
|
|
2035
|
+
});
|
|
2036
|
+
} else if (kind === "if") {
|
|
2037
|
+
visitBlock(data.then_block);
|
|
2038
|
+
visitBlock(data.else_block);
|
|
2039
|
+
} else if (kind === "loop") {
|
|
2040
|
+
visitBlock(data.body);
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
visitBlock(func.body);
|
|
2045
|
+
return result;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
2048
|
+
collectSliceScratchLocals(func) {
|
|
2049
|
+
const entries = [];
|
|
2050
|
+
const offsets = [];
|
|
2051
|
+
let count = 0;
|
|
2052
|
+
const visitBlock = (block) => {
|
|
2053
|
+
for (const statement of block.statements) {
|
|
2054
|
+
const kind = statement.kind?.kind;
|
|
2055
|
+
const data = statement.kind?.data;
|
|
2056
|
+
if (kind === "slice_fill" || kind === "slice_copy") {
|
|
2057
|
+
offsets.push(count);
|
|
2058
|
+
const scratchCount = kind === "slice_copy" ? 2 : 1;
|
|
2059
|
+
entries.push({ statement, count: scratchCount });
|
|
2060
|
+
count += scratchCount;
|
|
2061
|
+
} else if (kind === "if") {
|
|
2062
|
+
visitBlock(data.then_block);
|
|
2063
|
+
visitBlock(data.else_block);
|
|
2064
|
+
} else if (kind === "loop") {
|
|
2065
|
+
visitBlock(data.body);
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
};
|
|
2069
|
+
visitBlock(func.body);
|
|
2070
|
+
return { entries, offsets, count };
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
collectProcessFrameLocals(func) {
|
|
2074
|
+
const definitions = Array.from({ length: func.locals.length }, () => 0);
|
|
2075
|
+
const candidates = new Set();
|
|
2076
|
+
const visitBlock = (block) => {
|
|
2077
|
+
for (const statement of block.statements) {
|
|
2078
|
+
const kind = statement.kind?.kind;
|
|
2079
|
+
const data = statement.kind?.data;
|
|
2080
|
+
if (kind === "assign" && data.destination?.base?.kind === "local") {
|
|
2081
|
+
const localId = data.destination.base.data;
|
|
2082
|
+
if (
|
|
2083
|
+
Number.isInteger(localId) &&
|
|
2084
|
+
localId >= 0 &&
|
|
2085
|
+
localId < definitions.length
|
|
2086
|
+
) {
|
|
2087
|
+
definitions[localId] += 1;
|
|
2088
|
+
if (
|
|
2089
|
+
data.destination.projections.length === 0 &&
|
|
2090
|
+
data.value?.kind === "process_frame"
|
|
2091
|
+
) {
|
|
2092
|
+
candidates.add(localId);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
} else if (kind === "call") {
|
|
2096
|
+
for (const localId of data.results) {
|
|
2097
|
+
if (
|
|
2098
|
+
Number.isInteger(localId) &&
|
|
2099
|
+
localId >= 0 &&
|
|
2100
|
+
localId < definitions.length
|
|
2101
|
+
) {
|
|
2102
|
+
definitions[localId] += 1;
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
} else if (kind === "if") {
|
|
2106
|
+
visitBlock(data.then_block);
|
|
2107
|
+
visitBlock(data.else_block);
|
|
2108
|
+
} else if (kind === "loop") {
|
|
2109
|
+
visitBlock(data.body);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
visitBlock(func.body);
|
|
2114
|
+
return new Set(
|
|
2115
|
+
[...candidates].filter((localId) => definitions[localId] === 1),
|
|
2116
|
+
);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
defaultFunctionResult(context) {
|
|
2120
|
+
const scalars = context.function.results.map((typeId, resultId) =>
|
|
2121
|
+
this.requireScalarType(
|
|
2122
|
+
typeId,
|
|
2123
|
+
`result ${resultId} of '${context.function.name}'`,
|
|
2124
|
+
),
|
|
2125
|
+
);
|
|
2126
|
+
const values = scalars.map((scalar) => this.zero(scalar));
|
|
2127
|
+
if (values.length === 0) return undefined;
|
|
2128
|
+
if (values.length === 1) return values[0];
|
|
2129
|
+
return this.module.tuple.make(values);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
returnFromCurrentFunction(context) {
|
|
2133
|
+
return this.module.return(this.defaultFunctionResult(context));
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
raiseRuntimeFailure(context) {
|
|
2137
|
+
return this.module.block(null, [
|
|
2138
|
+
this.module.global.set(
|
|
2139
|
+
RUNTIME_FAILURE_GLOBAL,
|
|
2140
|
+
this.module.i32.const(PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE),
|
|
2141
|
+
),
|
|
2142
|
+
...this.resetDelegateBatch(),
|
|
2143
|
+
this.returnFromCurrentFunction(context),
|
|
2144
|
+
]);
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
propagateRuntimeFailure(calleeId, context) {
|
|
2148
|
+
if (!this.functionMayFail[calleeId]) return [];
|
|
2149
|
+
return [
|
|
2150
|
+
this.module.if(
|
|
2151
|
+
this.module.i32.ne(
|
|
2152
|
+
this.module.global.get(RUNTIME_FAILURE_GLOBAL, binaryen.i32),
|
|
2153
|
+
this.module.i32.const(PROCESSOR_EXECUTION_OK),
|
|
2154
|
+
),
|
|
2155
|
+
this.returnFromCurrentFunction(context),
|
|
2156
|
+
),
|
|
2157
|
+
];
|
|
2158
|
+
}
|
|
2159
|
+
|
|
2160
|
+
resetRuntimeFailure(functionId) {
|
|
2161
|
+
if (!this.functionMayFail[functionId]) return [];
|
|
2162
|
+
return [
|
|
2163
|
+
this.module.global.set(
|
|
2164
|
+
RUNTIME_FAILURE_GLOBAL,
|
|
2165
|
+
this.module.i32.const(PROCESSOR_EXECUTION_OK),
|
|
2166
|
+
),
|
|
2167
|
+
];
|
|
2168
|
+
}
|
|
2169
|
+
|
|
2170
|
+
executionStatus(functionId) {
|
|
2171
|
+
return this.functionMayFail[functionId]
|
|
2172
|
+
? this.module.global.get(RUNTIME_FAILURE_GLOBAL, binaryen.i32)
|
|
2173
|
+
: this.module.i32.const(PROCESSOR_EXECUTION_OK);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2176
|
+
resetDelegateBatch() {
|
|
2177
|
+
const batch = () =>
|
|
2178
|
+
this.module.global.get(POINTER_GLOBALS.delegateBatch, binaryen.i32);
|
|
2179
|
+
const stores = [
|
|
2180
|
+
DELEGATE_BATCH_USED_OFFSET,
|
|
2181
|
+
DELEGATE_BATCH_RECORD_COUNT_OFFSET,
|
|
2182
|
+
DELEGATE_BATCH_OVERFLOW_OFFSET,
|
|
2183
|
+
].map((offset) =>
|
|
2184
|
+
this.module.i32.store(
|
|
2185
|
+
offset,
|
|
2186
|
+
4,
|
|
2187
|
+
batch(),
|
|
2188
|
+
this.module.i32.const(0),
|
|
2189
|
+
)
|
|
2190
|
+
);
|
|
2191
|
+
return [
|
|
2192
|
+
this.module.if(
|
|
2193
|
+
this.module.i32.ne(batch(), this.module.i32.const(0)),
|
|
2194
|
+
this.module.block(null, stores),
|
|
2195
|
+
),
|
|
2196
|
+
];
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
executionOutputBatch(outputLocal, fieldOffset) {
|
|
2200
|
+
const output = () => this.module.local.get(outputLocal, binaryen.i32);
|
|
2201
|
+
return this.module.if(
|
|
2202
|
+
this.module.i32.ne(output(), this.module.i32.const(0)),
|
|
2203
|
+
this.module.i32.load(fieldOffset, 4, output()),
|
|
2204
|
+
this.module.i32.const(0),
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
executionOutputSequence(outputLocal) {
|
|
2209
|
+
const output = () => this.module.local.get(outputLocal, binaryen.i32);
|
|
2210
|
+
return this.module.if(
|
|
2211
|
+
this.module.i32.ne(output(), this.module.i32.const(0)),
|
|
2212
|
+
this.module.i32.add(
|
|
2213
|
+
output(),
|
|
2214
|
+
this.module.i32.const(EXECUTION_OUTPUT_SEQUENCE_OFFSET),
|
|
2215
|
+
),
|
|
2216
|
+
this.module.i32.const(0),
|
|
2217
|
+
);
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
advanceOutputSequence(sequenceLocal) {
|
|
2221
|
+
const pointer = () =>
|
|
2222
|
+
this.module.global.get(POINTER_GLOBALS.outputSequence, binaryen.i32);
|
|
2223
|
+
const sequence = () => this.module.local.get(sequenceLocal, binaryen.i32);
|
|
2224
|
+
return this.module.block(null, [
|
|
2225
|
+
this.module.local.set(
|
|
2226
|
+
sequenceLocal,
|
|
2227
|
+
this.module.i32.load(0, 4, pointer()),
|
|
2228
|
+
),
|
|
2229
|
+
this.module.i32.store(
|
|
2230
|
+
0,
|
|
2231
|
+
4,
|
|
2232
|
+
pointer(),
|
|
2233
|
+
this.module.select(
|
|
2234
|
+
this.module.i32.eq(sequence(), this.module.i32.const(-1)),
|
|
2235
|
+
sequence(),
|
|
2236
|
+
this.module.i32.add(sequence(), this.module.i32.const(1)),
|
|
2237
|
+
),
|
|
2238
|
+
),
|
|
2239
|
+
]);
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
fullInitClearRanges() {
|
|
2243
|
+
const ranges = [];
|
|
2244
|
+
let cursor = 0;
|
|
2245
|
+
for (const [stateId, slot] of this.mir.state.entries()) {
|
|
2246
|
+
if (slot.pinned !== true) continue;
|
|
2247
|
+
const layout = this.stateLayout[stateId];
|
|
2248
|
+
if (cursor < layout.offset) {
|
|
2249
|
+
ranges.push({ offset: cursor, size: layout.offset - cursor });
|
|
2250
|
+
}
|
|
2251
|
+
cursor = layout.offset + layout.size;
|
|
2252
|
+
}
|
|
2253
|
+
if (cursor < this.stateLayout.byteLength) {
|
|
2254
|
+
ranges.push({
|
|
2255
|
+
offset: cursor,
|
|
2256
|
+
size: this.stateLayout.byteLength - cursor,
|
|
2257
|
+
});
|
|
2258
|
+
}
|
|
2259
|
+
return ranges;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
addAbiWrappers() {
|
|
2263
|
+
const initId = this.mir.entry_points.init;
|
|
2264
|
+
const processId = this.mir.entry_points.process;
|
|
2265
|
+
this.requireFunctionId(initId, "init entry point");
|
|
2266
|
+
this.requireFunctionId(processId, "process entry point");
|
|
2267
|
+
|
|
2268
|
+
this.module.setFeatures(
|
|
2269
|
+
this.module.getFeatures() |
|
|
2270
|
+
binaryen.Features.BulkMemory |
|
|
2271
|
+
binaryen.Features.BulkMemoryOpt |
|
|
2272
|
+
(this.options.simd ? binaryen.Features.SIMD128 : 0),
|
|
2273
|
+
);
|
|
2274
|
+
// Pinned declarations fully initialize their own slots on this path.
|
|
2275
|
+
// Clear only the complementary ranges, including layout padding, so large
|
|
2276
|
+
// pinned arrays are never written once here and again by their initializer.
|
|
2277
|
+
const fullInitClears = this.fullInitClearRanges().map(({ offset, size }) =>
|
|
2278
|
+
this.module.memory.fill(
|
|
2279
|
+
offset === 0
|
|
2280
|
+
? this.module.local.get(1, binaryen.i32)
|
|
2281
|
+
: this.module.i32.add(
|
|
2282
|
+
this.module.local.get(1, binaryen.i32),
|
|
2283
|
+
this.module.i32.const(offset),
|
|
2284
|
+
),
|
|
2285
|
+
this.module.i32.const(0),
|
|
2286
|
+
this.module.i32.const(size),
|
|
2287
|
+
)
|
|
2288
|
+
);
|
|
2289
|
+
const initBody = this.module.block(null, [
|
|
2290
|
+
...this.resetRuntimeFailure(initId),
|
|
2291
|
+
this.module.global.set(
|
|
2292
|
+
POINTER_GLOBALS.params,
|
|
2293
|
+
this.module.local.get(0, binaryen.i32),
|
|
2294
|
+
),
|
|
2295
|
+
this.module.global.set(
|
|
2296
|
+
POINTER_GLOBALS.state,
|
|
2297
|
+
this.module.local.get(1, binaryen.i32),
|
|
2298
|
+
),
|
|
2299
|
+
this.module.global.set(
|
|
2300
|
+
POINTER_GLOBALS.buffers,
|
|
2301
|
+
this.module.local.get(3, binaryen.i32),
|
|
2302
|
+
),
|
|
2303
|
+
this.module.global.set(
|
|
2304
|
+
POINTER_GLOBALS.bufferWrites,
|
|
2305
|
+
this.module.local.get(3, binaryen.i32),
|
|
2306
|
+
),
|
|
2307
|
+
this.module.global.set(
|
|
2308
|
+
POINTER_GLOBALS.bufferFrames,
|
|
2309
|
+
this.module.local.get(4, binaryen.i32),
|
|
2310
|
+
),
|
|
2311
|
+
this.module.global.set(
|
|
2312
|
+
POINTER_GLOBALS.bufferChannels,
|
|
2313
|
+
this.module.local.get(5, binaryen.i32),
|
|
2314
|
+
),
|
|
2315
|
+
this.module.global.set(
|
|
2316
|
+
POINTER_GLOBALS.bufferSampleRates,
|
|
2317
|
+
this.module.local.get(6, binaryen.i32),
|
|
2318
|
+
),
|
|
2319
|
+
this.module.global.set(
|
|
2320
|
+
POINTER_GLOBALS.delegateBatch,
|
|
2321
|
+
this.executionOutputBatch(7, EXECUTION_OUTPUT_DELEGATE_BATCH_OFFSET),
|
|
2322
|
+
),
|
|
2323
|
+
this.module.global.set(
|
|
2324
|
+
POINTER_GLOBALS.printBatch,
|
|
2325
|
+
this.executionOutputBatch(7, EXECUTION_OUTPUT_PRINT_BATCH_OFFSET),
|
|
2326
|
+
),
|
|
2327
|
+
this.module.global.set(
|
|
2328
|
+
POINTER_GLOBALS.outputSequence,
|
|
2329
|
+
this.executionOutputSequence(7),
|
|
2330
|
+
),
|
|
2331
|
+
this.module.global.set(
|
|
2332
|
+
INIT_ALL_GLOBAL,
|
|
2333
|
+
this.module.i32.ne(
|
|
2334
|
+
this.module.local.get(2, binaryen.i32),
|
|
2335
|
+
this.module.i32.const(0),
|
|
2336
|
+
),
|
|
2337
|
+
),
|
|
2338
|
+
...(fullInitClears.length === 0
|
|
2339
|
+
? []
|
|
2340
|
+
: [
|
|
2341
|
+
this.module.if(
|
|
2342
|
+
this.module.global.get(INIT_ALL_GLOBAL, binaryen.i32),
|
|
2343
|
+
this.module.block(null, fullInitClears),
|
|
2344
|
+
),
|
|
2345
|
+
]),
|
|
2346
|
+
this.module.call(this.functionNames[initId], [], binaryen.none),
|
|
2347
|
+
this.executionStatus(initId),
|
|
2348
|
+
], binaryen.i32);
|
|
2349
|
+
this.module.addFunction(
|
|
2350
|
+
"$onda.abi.init",
|
|
2351
|
+
binaryen.createType([
|
|
2352
|
+
binaryen.i32,
|
|
2353
|
+
binaryen.i32,
|
|
2354
|
+
binaryen.i32,
|
|
2355
|
+
binaryen.i32,
|
|
2356
|
+
binaryen.i32,
|
|
2357
|
+
binaryen.i32,
|
|
2358
|
+
binaryen.i32,
|
|
2359
|
+
binaryen.i32,
|
|
2360
|
+
]),
|
|
2361
|
+
binaryen.i32,
|
|
2362
|
+
[],
|
|
2363
|
+
initBody,
|
|
2364
|
+
);
|
|
2365
|
+
this.module.addFunctionExport("$onda.abi.init", "onda_processor_init");
|
|
2366
|
+
|
|
2367
|
+
const processParams = binaryen.createType(
|
|
2368
|
+
Array.from({ length: 12 }, () => binaryen.i32),
|
|
2369
|
+
);
|
|
2370
|
+
const startFrame = () => this.module.local.get(4, binaryen.i32);
|
|
2371
|
+
const frames = () => this.module.local.get(5, binaryen.i32);
|
|
2372
|
+
const flags = () => this.module.local.get(6, binaryen.i32);
|
|
2373
|
+
const invalidRange = this.module.i32.or(
|
|
2374
|
+
this.module.i32.or(
|
|
2375
|
+
this.module.i32.or(
|
|
2376
|
+
this.module.i32.lt_s(startFrame(), this.module.i32.const(0)),
|
|
2377
|
+
this.module.i32.lt_s(frames(), this.module.i32.const(0)),
|
|
2378
|
+
),
|
|
2379
|
+
this.module.i32.or(
|
|
2380
|
+
this.module.i32.gt_s(
|
|
2381
|
+
startFrame(),
|
|
2382
|
+
this.module.i32.const(this.mir.config.block_size),
|
|
2383
|
+
),
|
|
2384
|
+
this.module.i32.gt_s(
|
|
2385
|
+
frames(),
|
|
2386
|
+
this.module.i32.sub(
|
|
2387
|
+
this.module.i32.const(this.mir.config.block_size),
|
|
2388
|
+
startFrame(),
|
|
2389
|
+
),
|
|
2390
|
+
),
|
|
2391
|
+
),
|
|
2392
|
+
),
|
|
2393
|
+
this.module.i32.ne(
|
|
2394
|
+
this.module.i32.and(
|
|
2395
|
+
flags(),
|
|
2396
|
+
this.module.i32.const(~ONDA_PROCESS_FULL_BLOCK),
|
|
2397
|
+
),
|
|
2398
|
+
this.module.i32.const(0),
|
|
2399
|
+
),
|
|
2400
|
+
);
|
|
2401
|
+
const processBody = this.module.block(null, [
|
|
2402
|
+
this.module.global.set(
|
|
2403
|
+
POINTER_GLOBALS.delegateBatch,
|
|
2404
|
+
this.executionOutputBatch(11, EXECUTION_OUTPUT_DELEGATE_BATCH_OFFSET),
|
|
2405
|
+
),
|
|
2406
|
+
this.module.global.set(
|
|
2407
|
+
POINTER_GLOBALS.printBatch,
|
|
2408
|
+
this.executionOutputBatch(11, EXECUTION_OUTPUT_PRINT_BATCH_OFFSET),
|
|
2409
|
+
),
|
|
2410
|
+
this.module.global.set(
|
|
2411
|
+
POINTER_GLOBALS.outputSequence,
|
|
2412
|
+
this.executionOutputSequence(11),
|
|
2413
|
+
),
|
|
2414
|
+
this.module.if(
|
|
2415
|
+
invalidRange,
|
|
2416
|
+
this.module.return(
|
|
2417
|
+
this.module.i32.const(PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE),
|
|
2418
|
+
),
|
|
2419
|
+
),
|
|
2420
|
+
...this.resetRuntimeFailure(processId),
|
|
2421
|
+
this.module.global.set(
|
|
2422
|
+
POINTER_GLOBALS.state,
|
|
2423
|
+
this.module.local.get(0, binaryen.i32),
|
|
2424
|
+
),
|
|
2425
|
+
this.module.global.set(
|
|
2426
|
+
POINTER_GLOBALS.params,
|
|
2427
|
+
this.module.local.get(1, binaryen.i32),
|
|
2428
|
+
),
|
|
2429
|
+
this.module.global.set(
|
|
2430
|
+
POINTER_GLOBALS.inputs,
|
|
2431
|
+
this.module.local.get(2, binaryen.i32),
|
|
2432
|
+
),
|
|
2433
|
+
this.module.global.set(
|
|
2434
|
+
POINTER_GLOBALS.outputs,
|
|
2435
|
+
this.module.local.get(3, binaryen.i32),
|
|
2436
|
+
),
|
|
2437
|
+
this.module.global.set(
|
|
2438
|
+
POINTER_GLOBALS.buffers,
|
|
2439
|
+
this.module.local.get(7, binaryen.i32),
|
|
2440
|
+
),
|
|
2441
|
+
this.module.global.set(
|
|
2442
|
+
POINTER_GLOBALS.bufferWrites,
|
|
2443
|
+
this.module.local.get(7, binaryen.i32),
|
|
2444
|
+
),
|
|
2445
|
+
this.module.global.set(
|
|
2446
|
+
POINTER_GLOBALS.bufferFrames,
|
|
2447
|
+
this.module.local.get(8, binaryen.i32),
|
|
2448
|
+
),
|
|
2449
|
+
this.module.global.set(
|
|
2450
|
+
POINTER_GLOBALS.bufferChannels,
|
|
2451
|
+
this.module.local.get(9, binaryen.i32),
|
|
2452
|
+
),
|
|
2453
|
+
this.module.global.set(
|
|
2454
|
+
POINTER_GLOBALS.bufferSampleRates,
|
|
2455
|
+
this.module.local.get(10, binaryen.i32),
|
|
2456
|
+
),
|
|
2457
|
+
this.module.call(
|
|
2458
|
+
this.functionNames[processId],
|
|
2459
|
+
[startFrame(), frames(), flags()],
|
|
2460
|
+
binaryen.none,
|
|
2461
|
+
),
|
|
2462
|
+
this.executionStatus(processId),
|
|
2463
|
+
], binaryen.i32);
|
|
2464
|
+
this.module.addFunction(
|
|
2465
|
+
"$onda.abi.process",
|
|
2466
|
+
processParams,
|
|
2467
|
+
binaryen.i32,
|
|
2468
|
+
[],
|
|
2469
|
+
processBody,
|
|
2470
|
+
);
|
|
2471
|
+
this.module.addFunctionExport("$onda.abi.process", "onda_process");
|
|
2472
|
+
|
|
2473
|
+
this.mir.interface.events.forEach((event, eventId) => {
|
|
2474
|
+
this.requireFunctionId(event.handler, `event '${event.name}' handler`);
|
|
2475
|
+
const handler = this.mir.functions[event.handler];
|
|
2476
|
+
if (
|
|
2477
|
+
handler.kind?.kind !== "event" ||
|
|
2478
|
+
handler.kind.data !== eventId ||
|
|
2479
|
+
handler.params.length !== 0 ||
|
|
2480
|
+
handler.results.length !== 0
|
|
2481
|
+
) {
|
|
2482
|
+
this.fail(`event '${event.name}' has an invalid MIR handler signature`);
|
|
2483
|
+
}
|
|
2484
|
+
const wrapperName = `$onda.abi.event.${eventId}`;
|
|
2485
|
+
const body = this.module.block(null, [
|
|
2486
|
+
this.module.global.set(
|
|
2487
|
+
POINTER_GLOBALS.delegateBatch,
|
|
2488
|
+
this.executionOutputBatch(7, EXECUTION_OUTPUT_DELEGATE_BATCH_OFFSET),
|
|
2489
|
+
),
|
|
2490
|
+
this.module.global.set(
|
|
2491
|
+
POINTER_GLOBALS.printBatch,
|
|
2492
|
+
this.executionOutputBatch(7, EXECUTION_OUTPUT_PRINT_BATCH_OFFSET),
|
|
2493
|
+
),
|
|
2494
|
+
this.module.global.set(
|
|
2495
|
+
POINTER_GLOBALS.outputSequence,
|
|
2496
|
+
this.executionOutputSequence(7),
|
|
2497
|
+
),
|
|
2498
|
+
...this.resetRuntimeFailure(event.handler),
|
|
2499
|
+
this.module.global.set(
|
|
2500
|
+
POINTER_GLOBALS.eventPayload,
|
|
2501
|
+
this.module.local.get(0, binaryen.i32),
|
|
2502
|
+
),
|
|
2503
|
+
this.module.global.set(
|
|
2504
|
+
POINTER_GLOBALS.params,
|
|
2505
|
+
this.module.local.get(1, binaryen.i32),
|
|
2506
|
+
),
|
|
2507
|
+
this.module.global.set(
|
|
2508
|
+
POINTER_GLOBALS.state,
|
|
2509
|
+
this.module.local.get(2, binaryen.i32),
|
|
2510
|
+
),
|
|
2511
|
+
this.module.global.set(
|
|
2512
|
+
POINTER_GLOBALS.buffers,
|
|
2513
|
+
this.module.local.get(3, binaryen.i32),
|
|
2514
|
+
),
|
|
2515
|
+
this.module.global.set(
|
|
2516
|
+
POINTER_GLOBALS.bufferWrites,
|
|
2517
|
+
this.module.local.get(3, binaryen.i32),
|
|
2518
|
+
),
|
|
2519
|
+
this.module.global.set(
|
|
2520
|
+
POINTER_GLOBALS.bufferFrames,
|
|
2521
|
+
this.module.local.get(4, binaryen.i32),
|
|
2522
|
+
),
|
|
2523
|
+
this.module.global.set(
|
|
2524
|
+
POINTER_GLOBALS.bufferChannels,
|
|
2525
|
+
this.module.local.get(5, binaryen.i32),
|
|
2526
|
+
),
|
|
2527
|
+
this.module.global.set(
|
|
2528
|
+
POINTER_GLOBALS.bufferSampleRates,
|
|
2529
|
+
this.module.local.get(6, binaryen.i32),
|
|
2530
|
+
),
|
|
2531
|
+
this.module.call(this.functionNames[event.handler], [], binaryen.none),
|
|
2532
|
+
this.executionStatus(event.handler),
|
|
2533
|
+
], binaryen.i32);
|
|
2534
|
+
this.module.addFunction(
|
|
2535
|
+
wrapperName,
|
|
2536
|
+
binaryen.createType(Array.from({ length: 8 }, () => binaryen.i32)),
|
|
2537
|
+
binaryen.i32,
|
|
2538
|
+
[],
|
|
2539
|
+
body,
|
|
2540
|
+
);
|
|
2541
|
+
this.module.addFunctionExport(wrapperName, `onda_event_${eventId}`);
|
|
2542
|
+
});
|
|
2543
|
+
}
|
|
2544
|
+
}
|