@onda-lang/wasm-compiler 0.5.0-rc.0 → 0.5.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.
@@ -0,0 +1,3679 @@
1
+ import binaryen from "binaryen";
2
+ import { SUPPORTED_MIR_SCHEMA_VERSION } from "./constants.js";
3
+ import { OndaBinaryenError } from "./errors.js";
4
+ import { decodeMirMessagePack } from "./messagepack.js";
5
+ import { supportsMirOperation } from "./operations.js";
6
+ import { ONDA_MATH_KERNEL_WASM } from "./math-kernel.generated.js";
7
+ import {
8
+ PROCESSOR_ABI_VERSION,
9
+ PROCESSOR_ARTIFACT_FORMAT,
10
+ PROCESSOR_ARTIFACT_FORMAT_VERSION,
11
+ PROCESSOR_SNAPSHOT_FORMAT_VERSION,
12
+ validateProcessorMetadata,
13
+ } from "./artifact.js";
14
+
15
+ export {
16
+ OndaArtifactError,
17
+ PROCESSOR_ABI_VERSION,
18
+ PROCESSOR_ARTIFACT_FORMAT,
19
+ PROCESSOR_ARTIFACT_FORMAT_VERSION,
20
+ PROCESSOR_SNAPSHOT_FORMAT_VERSION,
21
+ createProcessorArtifactFiles,
22
+ loadProcessorArtifactFiles,
23
+ parseProcessorMetadata,
24
+ serializeProcessorMetadata,
25
+ validateProcessorArtifact,
26
+ validateProcessorMetadata,
27
+ validateProcessorModule,
28
+ } from "./artifact.js";
29
+ export { SUPPORTED_MIR_SCHEMA_VERSION } from "./constants.js";
30
+ export { OndaBinaryenError } from "./errors.js";
31
+
32
+ const PAGE_BYTES = 64 * 1024;
33
+ const STATIC_BASE = 1024;
34
+ const MATH_KERNEL_RESERVED_END = 32 * 1024;
35
+ const MATH_KERNEL_DATA_SEGMENT = ".rodata";
36
+ const MATH_KERNEL_STACK_GLOBAL = "__stack_pointer";
37
+ const MAX_MEMORY_PAGES = 65_536;
38
+ const WASM32_ADDRESS_SPACE_BYTES = MAX_MEMORY_PAGES * PAGE_BYTES;
39
+ const DEFAULT_OPTIMIZE_LEVEL = 4;
40
+ const ONDA_PROCESS_FULL_BLOCK = (1 << 0) | (1 << 1);
41
+ const MATH_KERNEL_INTRINSICS = new Set([
42
+ "sin",
43
+ "cos",
44
+ "tan",
45
+ "tanh",
46
+ "atan",
47
+ "atan2",
48
+ "exp",
49
+ "log",
50
+ "pow",
51
+ "remainder",
52
+ "fma",
53
+ ]);
54
+
55
+ const POINTER_GLOBALS = Object.freeze({
56
+ inputs: "$onda.inputs",
57
+ outputs: "$onda.outputs",
58
+ params: "$onda.params",
59
+ state: "$onda.state",
60
+ eventPayload: "$onda.event_payload",
61
+ buffers: "$onda.buffers",
62
+ bufferFrames: "$onda.buffer_frames",
63
+ bufferChannels: "$onda.buffer_channels",
64
+ bufferSampleRates: "$onda.buffer_sample_rates",
65
+ });
66
+
67
+ // Compiles MIR emitted by Onda's semantic producer. The producer owns proofs
68
+ // for operations marked `bounds: "unchecked"` and all other validated MIR
69
+ // invariants. This backend deliberately does not expose a partial validator
70
+ // for downloaded or hand-authored MIR.
71
+ export function compileTrustedMir(mirJson, options = {}) {
72
+ return compileMirInternal(mirJson, options);
73
+ }
74
+
75
+ function compileMirInternal(mirJson, options) {
76
+ const mir = parseMirInput(mirJson);
77
+ const compiler = new MirCompiler(mir, options);
78
+ return compiler.compile();
79
+ }
80
+
81
+ function parseMirInput(input) {
82
+ if (typeof input === "string") return parseMirJson(input);
83
+ if (input instanceof ArrayBuffer || ArrayBuffer.isView(input)) {
84
+ try {
85
+ return decodeMirMessagePack(input);
86
+ } catch (error) {
87
+ throw new OndaBinaryenError(`invalid MessagePack MIR: ${error.message}`);
88
+ }
89
+ }
90
+ return input;
91
+ }
92
+
93
+ export function createDefaultImports() {
94
+ return {};
95
+ }
96
+
97
+ function collectMathKernelHelpers(mir) {
98
+ const result = new Set();
99
+ for (const func of mir?.functions ?? []) {
100
+ const localScalars = (func.locals ?? []).map((local) => {
101
+ const type = mir.types?.[local.ty];
102
+ return type?.kind === "scalar" ? type.data : null;
103
+ });
104
+ const valueScalar = (value) => {
105
+ if (value?.kind === "constant") return value.data?.type;
106
+ if (value?.kind === "local") return localScalars[value.data];
107
+ return null;
108
+ };
109
+ const visitBlock = (block) => {
110
+ for (const statement of block?.statements ?? []) {
111
+ const kind = statement.kind?.kind;
112
+ const data = statement.kind?.data;
113
+ if (kind === "assign" && data?.value?.kind === "intrinsic") {
114
+ const intrinsic = data.value.data?.intrinsic;
115
+ const scalar = valueScalar(data.value.data?.args?.[0]);
116
+ if (
117
+ MATH_KERNEL_INTRINSICS.has(intrinsic)
118
+ && (scalar === "f32" || scalar === "f64")
119
+ ) {
120
+ result.add(`onda_math_${intrinsic}_${scalar}`);
121
+ }
122
+ } else if (
123
+ kind === "assign"
124
+ && data?.value?.kind === "binary"
125
+ && data.value.data?.op === "remainder"
126
+ ) {
127
+ const scalar = valueScalar(data.value.data?.lhs);
128
+ if (scalar === "f32" || scalar === "f64") {
129
+ result.add(`onda_math_remainder_${scalar}`);
130
+ }
131
+ } else if (kind === "if") {
132
+ visitBlock(data?.then_block);
133
+ visitBlock(data?.else_block);
134
+ } else if (kind === "loop") {
135
+ visitBlock(data?.body);
136
+ }
137
+ }
138
+ };
139
+ visitBlock(func.body);
140
+ }
141
+ return result;
142
+ }
143
+
144
+ function parseMirJson(json) {
145
+ try {
146
+ return JSON.parse(json);
147
+ } catch (error) {
148
+ throw new OndaBinaryenError(`invalid MIR JSON: ${error.message}`);
149
+ }
150
+ }
151
+
152
+ class MirCompiler {
153
+ constructor(mir, options) {
154
+ this.mir = mir;
155
+ this.options = {
156
+ optimize: options.optimize !== false,
157
+ emitText: options.emitText === true,
158
+ optimizeLevel: options.optimizeLevel ?? DEFAULT_OPTIMIZE_LEVEL,
159
+ shrinkLevel: options.shrinkLevel ?? 0,
160
+ fastMath: options.fastMath === true,
161
+ simd: options.simd !== false,
162
+ allowInliningFunctionsWithLoops:
163
+ options.allowInliningFunctionsWithLoops === true,
164
+ };
165
+ this.module = new binaryen.Module();
166
+ this.functionNames = [];
167
+ this.stateLayout = [];
168
+ this.paramLayout = [];
169
+ this.inputLayout = [];
170
+ this.outputLayout = [];
171
+ this.controlOutputLayout = [];
172
+ this.eventLayout = [];
173
+ this.constLayout = [];
174
+ this.localArrayLayout = [];
175
+ this.localScalarRefLayout = [];
176
+ this.memorySegments = [];
177
+ this.requiredMathHelpers = collectMathKernelHelpers(mir);
178
+ this.nextStaticAddress = this.requiredMathHelpers.size > 0
179
+ ? MATH_KERNEL_RESERVED_END
180
+ : STATIC_BASE;
181
+ this.internalHelpers = new Set();
182
+ this.nextLabel = 0;
183
+ }
184
+
185
+ compile() {
186
+ try {
187
+ this.validateEnvelope();
188
+ this.buildLayouts();
189
+ this.addMathKernel();
190
+ this.addMemoryAndContextGlobals();
191
+ this.addMirFunctions();
192
+ this.addAbiWrappers();
193
+
194
+ if (!this.module.validate()) {
195
+ throw new OndaBinaryenError("Binaryen rejected the generated WebAssembly module");
196
+ }
197
+ if (this.options.optimize) {
198
+ const previousOptimizeLevel = binaryen.getOptimizeLevel();
199
+ const previousShrinkLevel = binaryen.getShrinkLevel();
200
+ const previousFastMath = binaryen.getFastMath();
201
+ const previousLoopInlining =
202
+ binaryen.getAllowInliningFunctionsWithLoops();
203
+ try {
204
+ binaryen.setOptimizeLevel(this.options.optimizeLevel);
205
+ binaryen.setShrinkLevel(this.options.shrinkLevel);
206
+ binaryen.setFastMath(this.options.fastMath);
207
+ binaryen.setAllowInliningFunctionsWithLoops(
208
+ this.options.allowInliningFunctionsWithLoops,
209
+ );
210
+ this.module.optimize();
211
+ } finally {
212
+ binaryen.setOptimizeLevel(previousOptimizeLevel);
213
+ binaryen.setShrinkLevel(previousShrinkLevel);
214
+ binaryen.setFastMath(previousFastMath);
215
+ binaryen.setAllowInliningFunctionsWithLoops(previousLoopInlining);
216
+ }
217
+ if (!this.module.validate()) {
218
+ throw new OndaBinaryenError(
219
+ "Binaryen rejected the optimized WebAssembly module",
220
+ );
221
+ }
222
+ }
223
+
224
+ const wasm = this.module.emitBinary();
225
+ const result = {
226
+ wasm,
227
+ metadata: this.buildMetadata(),
228
+ };
229
+ if (this.options.emitText) {
230
+ result.wat = this.module.emitText();
231
+ }
232
+ // Binaryen already validated the module above. Validate the descriptor here
233
+ // without asking the JavaScript engine to compile the Wasm a second time.
234
+ validateProcessorMetadata(result.metadata, "webassembly_module");
235
+ return result;
236
+ } finally {
237
+ this.module.dispose();
238
+ }
239
+ }
240
+
241
+ validateEnvelope() {
242
+ const mir = this.mir;
243
+ if (!mir || typeof mir !== "object" || Array.isArray(mir)) {
244
+ this.fail("MIR must be a JSON object");
245
+ }
246
+ if (mir.schema_version !== SUPPORTED_MIR_SCHEMA_VERSION) {
247
+ this.fail(
248
+ `unsupported MIR schema version ${String(mir.schema_version)}; expected ${SUPPORTED_MIR_SCHEMA_VERSION}`,
249
+ );
250
+ }
251
+ for (const field of ["types", "state", "const_data", "functions"]) {
252
+ if (!Array.isArray(mir[field])) {
253
+ this.fail(`MIR field '${field}' must be an array`);
254
+ }
255
+ }
256
+ if (!mir.interface || typeof mir.interface !== "object") {
257
+ this.fail("MIR field 'interface' must be an object");
258
+ }
259
+ for (const field of [
260
+ "inputs",
261
+ "outputs",
262
+ "control_outputs",
263
+ "params",
264
+ "buffers",
265
+ "events",
266
+ ]) {
267
+ if (!Array.isArray(mir.interface[field])) {
268
+ this.fail(`MIR interface field '${field}' must be an array`);
269
+ }
270
+ }
271
+ if (!mir.entry_points || !Number.isInteger(mir.entry_points.init)) {
272
+ this.fail("MIR entry_points are missing or invalid");
273
+ }
274
+ if (!Number.isInteger(mir.entry_points.process)) {
275
+ this.fail("MIR process entry point is missing or invalid");
276
+ }
277
+ if (!Number.isInteger(mir.config?.block_size) || mir.config.block_size <= 0) {
278
+ this.fail("MIR block size must be a positive integer");
279
+ }
280
+ if (mir.config.block_size > 0x7fff_ffff) {
281
+ this.fail("MIR block size must fit the signed i32 process ABI");
282
+ }
283
+ if (
284
+ !Number.isInteger(this.options.optimizeLevel) ||
285
+ this.options.optimizeLevel < 0 ||
286
+ this.options.optimizeLevel > 4
287
+ ) {
288
+ this.fail("Binaryen optimizeLevel must be an integer from 0 through 4");
289
+ }
290
+ if (
291
+ !Number.isInteger(this.options.shrinkLevel) ||
292
+ this.options.shrinkLevel < 0 ||
293
+ this.options.shrinkLevel > 2
294
+ ) {
295
+ this.fail("Binaryen shrinkLevel must be an integer from 0 through 2");
296
+ }
297
+ this.validateCurrentSchemaEnvelope();
298
+ this.validateProcessEntrySignature();
299
+ this.validateAcyclicCallGraph();
300
+ }
301
+
302
+ validateCurrentSchemaEnvelope() {
303
+ const persistenceKinds = new Set([
304
+ "snapshot",
305
+ "instance_scratch",
306
+ "control_mirror",
307
+ ]);
308
+ for (const [stateId, slot] of this.mir.state.entries()) {
309
+ if (!persistenceKinds.has(slot?.persistence)) {
310
+ this.fail(
311
+ `state slot ${stateId} has invalid persistence '${String(slot?.persistence)}'`,
312
+ );
313
+ }
314
+ }
315
+
316
+ const mirrors = new Set();
317
+ for (const [outputId, output] of this.mir.interface.control_outputs.entries()) {
318
+ if (
319
+ !Number.isInteger(output?.mirror) ||
320
+ output.mirror < 0 ||
321
+ output.mirror >= this.mir.state.length
322
+ ) {
323
+ this.fail(`control output ${outputId} has an invalid mirror state id`);
324
+ }
325
+ if (mirrors.has(output.mirror)) {
326
+ this.fail(`control output ${outputId} reuses mirror state ${output.mirror}`);
327
+ }
328
+ mirrors.add(output.mirror);
329
+ const slot = this.mir.state[output.mirror];
330
+ if (slot.persistence !== "control_mirror") {
331
+ this.fail(
332
+ `control output ${outputId} mirror state ${output.mirror} is not control_mirror storage`,
333
+ );
334
+ }
335
+ if (!this.typesEquivalent(output.ty, slot.ty)) {
336
+ this.fail(
337
+ `control output ${outputId} type does not match mirror state ${output.mirror}`,
338
+ );
339
+ }
340
+ }
341
+
342
+ const origins = new Set(["source", "compiler_generated"]);
343
+ const inlineHints = new Set(["auto", "always", "never"]);
344
+ for (const [functionId, func] of this.mir.functions.entries()) {
345
+ if (
346
+ !func?.attributes ||
347
+ !origins.has(func.attributes.origin) ||
348
+ !inlineHints.has(func.attributes.inline)
349
+ ) {
350
+ this.fail(
351
+ `function ${functionId} has invalid schema-${SUPPORTED_MIR_SCHEMA_VERSION} attributes`,
352
+ );
353
+ }
354
+ }
355
+ }
356
+
357
+ validateProcessEntrySignature() {
358
+ const processId = this.mir.entry_points.process;
359
+ this.requireFunctionId(processId, "process entry point");
360
+ const process = this.mir.functions[processId];
361
+ if (process?.kind?.kind !== "process") {
362
+ this.fail("MIR process entry point must have process function kind");
363
+ }
364
+ if (!Array.isArray(process.params) || process.params.length !== 3) {
365
+ this.fail(
366
+ "MIR process entry point must have exactly three parameters (start_frame, frames, flags)",
367
+ );
368
+ }
369
+ if (!Array.isArray(process.results) || process.results.length !== 0) {
370
+ this.fail("MIR process entry point must not return values");
371
+ }
372
+
373
+ const names = ["start_frame", "frames", "flags"];
374
+ for (const [index, name] of names.entries()) {
375
+ const parameter = process.params[index];
376
+ if (parameter?.name !== name) {
377
+ this.fail(`MIR process parameter ${index} must be named '${name}'`);
378
+ }
379
+ if (parameter.mode !== "value") {
380
+ this.fail(`MIR process parameter '${name}' must use value passing mode`);
381
+ }
382
+ const type = this.mir.types[parameter.ty];
383
+ if (type?.kind !== "scalar" || type.data !== "i32") {
384
+ this.fail(`MIR process parameter '${name}' must have type i32`);
385
+ }
386
+ }
387
+ }
388
+
389
+ validateAcyclicCallGraph() {
390
+ const functionCount = this.mir.functions.length;
391
+ const collectCalls = (block, callees) => {
392
+ for (const statement of block?.statements ?? []) {
393
+ const kind = statement.kind?.kind;
394
+ const data = statement.kind?.data;
395
+ if (kind === "call") {
396
+ if (
397
+ Number.isInteger(data?.function)
398
+ && data.function >= 0
399
+ && data.function < functionCount
400
+ ) {
401
+ callees.add(data.function);
402
+ }
403
+ } else if (kind === "if") {
404
+ collectCalls(data?.then_block, callees);
405
+ collectCalls(data?.else_block, callees);
406
+ } else if (kind === "loop") {
407
+ collectCalls(data?.body, callees);
408
+ }
409
+ }
410
+ };
411
+
412
+ const edges = this.mir.functions.map((func) => {
413
+ const callees = new Set();
414
+ collectCalls(func.body, callees);
415
+ return [...callees].sort((lhs, rhs) => lhs - rhs);
416
+ });
417
+ const visits = new Uint8Array(functionCount);
418
+ const path = [];
419
+ const visit = (functionId) => {
420
+ if (visits[functionId] === 2) return null;
421
+ if (visits[functionId] === 1) {
422
+ const start = Math.max(0, path.indexOf(functionId));
423
+ return [...path.slice(start), functionId];
424
+ }
425
+ visits[functionId] = 1;
426
+ path.push(functionId);
427
+ for (const callee of edges[functionId]) {
428
+ const cycle = visit(callee);
429
+ if (cycle) return cycle;
430
+ }
431
+ path.pop();
432
+ visits[functionId] = 2;
433
+ return null;
434
+ };
435
+
436
+ for (let functionId = 0; functionId < functionCount; functionId += 1) {
437
+ const cycle = visit(functionId);
438
+ if (cycle) {
439
+ const display = cycle
440
+ .map((id) => this.mir.functions[id]?.name ?? `@fn${id}`)
441
+ .join(" -> ");
442
+ this.fail(`recursive call cycle is not realtime-safe: ${display}`);
443
+ }
444
+ }
445
+ }
446
+
447
+ buildLayouts() {
448
+ this.stateLayout = this.layoutNamedValues(this.mir.state);
449
+ this.paramLayout = this.layoutNamedValues(this.mir.interface.params);
450
+ this.inputLayout = this.layoutPorts(this.mir.interface.inputs);
451
+ this.outputLayout = this.layoutPorts(this.mir.interface.outputs);
452
+ this.controlOutputLayout = this.layoutControlOutputs();
453
+ this.eventLayout = this.mir.interface.events.map((event) =>
454
+ this.layoutEventValues(event.params),
455
+ );
456
+ this.requireWasm32Extent(
457
+ this.stateLayout.byteLength,
458
+ "MIR physical state storage",
459
+ );
460
+ this.requireWasm32Extent(
461
+ this.paramLayout.byteLength,
462
+ "MIR parameter storage",
463
+ );
464
+ for (const [eventId, layout] of this.eventLayout.entries()) {
465
+ this.requireWasm32Extent(
466
+ layout.minimumByteLength,
467
+ `MIR event ${eventId} fixed payload storage`,
468
+ );
469
+ }
470
+
471
+ for (let id = 0; id < this.mir.const_data.length; id += 1) {
472
+ const data = this.mir.const_data[id];
473
+ const scalar = data.element;
474
+ const size = this.scalarSize(scalar);
475
+ this.nextStaticAddress = alignUp(this.nextStaticAddress, size);
476
+ const address = this.nextStaticAddress;
477
+ const bytes = encodeScalarValues(data.values, scalar, this);
478
+ this.memorySegments.push({
479
+ offset: this.module.i32.const(address),
480
+ data: bytes,
481
+ });
482
+ this.constLayout.push({ address, scalar, len: data.values.length });
483
+ this.nextStaticAddress += bytes.byteLength;
484
+ }
485
+
486
+ this.localArrayLayout = this.mir.functions.map((func) =>
487
+ func.locals.map((local) => {
488
+ const type = this.type(local.ty);
489
+ if (type.kind !== "array") return null;
490
+ const layout = this.typeLayout(local.ty);
491
+ this.nextStaticAddress = alignUp(this.nextStaticAddress, layout.align);
492
+ const address = this.nextStaticAddress;
493
+ this.nextStaticAddress += layout.size;
494
+ return { ...layout, address };
495
+ }),
496
+ );
497
+ this.localScalarRefLayout = this.mir.functions.map((func, functionId) => {
498
+ const addressTaken = this.collectAddressTakenScalarLocals(functionId);
499
+ return func.locals.map((local, localId) => {
500
+ if (!addressTaken.has(localId)) return null;
501
+ const type = this.type(local.ty);
502
+ if (type.kind !== "scalar") return null;
503
+ const size = this.scalarSize(type.data);
504
+ this.nextStaticAddress = alignUp(this.nextStaticAddress, size);
505
+ const address = this.nextStaticAddress;
506
+ this.nextStaticAddress += size;
507
+ return { address, scalar: type.data, size };
508
+ });
509
+ });
510
+ this.nextStaticAddress = alignUp(this.nextStaticAddress, 16);
511
+ this.requireWasm32Extent(this.nextStaticAddress, "MIR static storage");
512
+ this.requireWasm32Extent(
513
+ this.nextStaticAddress
514
+ + this.paramLayout.byteLength
515
+ + this.stateLayout.byteLength,
516
+ "MIR static, parameter, and physical state storage",
517
+ );
518
+ }
519
+
520
+ collectAddressTakenScalarLocals(functionId) {
521
+ const result = new Set();
522
+ const visitBlock = (block) => {
523
+ for (const statement of block.statements) {
524
+ const kind = statement.kind?.kind;
525
+ const data = statement.kind?.data;
526
+ if (kind === "call") {
527
+ const target = this.mir.functions[data.function];
528
+ data.args.forEach((argument, index) => {
529
+ const parameter = target?.params[index];
530
+ const type = parameter && this.type(parameter.ty);
531
+ if (
532
+ parameter?.mode !== "value"
533
+ && type?.kind === "scalar"
534
+ && argument.kind === "place"
535
+ && argument.data.base.kind === "local"
536
+ && argument.data.projections.length === 0
537
+ ) {
538
+ result.add(argument.data.base.data);
539
+ }
540
+ });
541
+ } else if (kind === "if") {
542
+ visitBlock(data.then_block);
543
+ visitBlock(data.else_block);
544
+ } else if (kind === "loop") {
545
+ visitBlock(data.body);
546
+ }
547
+ }
548
+ };
549
+ visitBlock(this.mir.functions[functionId].body);
550
+ return result;
551
+ }
552
+
553
+ layoutNamedValues(values) {
554
+ let offset = 0;
555
+ const result = [];
556
+ for (const value of values) {
557
+ const layout = this.typeLayout(value.ty);
558
+ offset = alignUp(offset, layout.align);
559
+ result.push({ ...layout, offset });
560
+ offset += layout.size;
561
+ }
562
+ result.byteLength = alignUp(offset, 16);
563
+ return result;
564
+ }
565
+
566
+ layoutControlOutputs() {
567
+ return this.mir.interface.control_outputs.map((output) => {
568
+ const layout = this.typeLayout(output.ty);
569
+ return {
570
+ ...layout,
571
+ offset: this.stateLayout[output.mirror].offset,
572
+ };
573
+ });
574
+ }
575
+
576
+ layoutEventValues(values) {
577
+ let offset = 0;
578
+ let dynamic = false;
579
+ const result = values.map((value) => {
580
+ const type = this.type(value.ty);
581
+ if (type.kind === "slice") {
582
+ const entry = {
583
+ offset: dynamic ? null : offset,
584
+ size: null,
585
+ dynamic: true,
586
+ headerSize: 4,
587
+ scalar: type.data.element,
588
+ };
589
+ offset += 4;
590
+ dynamic = true;
591
+ return entry;
592
+ }
593
+ const layout = this.typeLayout(value.ty);
594
+ const entry = { ...layout, offset: dynamic ? null : offset, dynamic: false };
595
+ offset += layout.size;
596
+ return entry;
597
+ });
598
+ result.byteLength = dynamic ? null : offset;
599
+ result.minimumByteLength = offset;
600
+ result.dynamic = dynamic;
601
+ return result;
602
+ }
603
+
604
+ layoutPorts(ports) {
605
+ let channel = 0;
606
+ return ports.map((port, portId) => {
607
+ const type = this.type(port.ty);
608
+ const flattened = this.flattenPortType(type);
609
+ this.requireWasm32Extent(
610
+ this.scalarSize(flattened.scalar) * this.mir.config.block_size,
611
+ `MIR audio port ${portId} channel storage`,
612
+ );
613
+ const result = {
614
+ channel,
615
+ channels: flattened.channels,
616
+ scalar: flattened.scalar,
617
+ size: this.scalarSize(flattened.scalar),
618
+ isArray: type.kind === "array",
619
+ };
620
+ channel += flattened.channels;
621
+ return result;
622
+ });
623
+ }
624
+
625
+ flattenPortType(type) {
626
+ if (type.kind === "scalar") {
627
+ return { scalar: type.data, channels: 1 };
628
+ }
629
+ if (type.kind === "array") {
630
+ const element = this.type(type.data.element);
631
+ if (element.kind !== "scalar") {
632
+ this.fail("nested aggregate audio ports are not supported yet");
633
+ }
634
+ return { scalar: element.data, channels: type.data.len };
635
+ }
636
+ this.fail(`audio port type '${type.kind}' is not supported yet`);
637
+ }
638
+
639
+ addMemoryAndContextGlobals() {
640
+ const initialPages = Math.max(
641
+ 1,
642
+ Math.ceil(this.nextStaticAddress / PAGE_BYTES),
643
+ );
644
+ if (initialPages > MAX_MEMORY_PAGES) {
645
+ this.fail(
646
+ `MIR static storage requires ${initialPages} Wasm pages, exceeding the Wasm32 limit`,
647
+ );
648
+ }
649
+ this.module.setMemory(
650
+ initialPages,
651
+ MAX_MEMORY_PAGES,
652
+ "memory",
653
+ this.memorySegments,
654
+ );
655
+ this.module.addGlobal(
656
+ "__heap_base",
657
+ binaryen.i32,
658
+ false,
659
+ this.module.i32.const(this.nextStaticAddress),
660
+ );
661
+ this.module.addGlobalExport("__heap_base", "__heap_base");
662
+
663
+ for (const name of Object.values(POINTER_GLOBALS)) {
664
+ this.module.addGlobal(name, binaryen.i32, true, this.module.i32.const(0));
665
+ }
666
+ }
667
+
668
+ addMathKernel() {
669
+ if (this.requiredMathHelpers.size === 0) return;
670
+
671
+ const source = binaryen.readBinary(ONDA_MATH_KERNEL_WASM);
672
+ try {
673
+ if (
674
+ source.getNumGlobals() !== 1
675
+ || source.getNumTables() !== 0
676
+ || source.getNumDataSegments() !== 1
677
+ ) {
678
+ this.fail("embedded Wasm math kernel has an unsupported module shape");
679
+ }
680
+
681
+ for (let index = source.getNumExports() - 1; index >= 0; index -= 1) {
682
+ const exported = binaryen.getExportInfo(source.getExportByIndex(index));
683
+ if (!this.requiredMathHelpers.has(exported.name)) {
684
+ source.removeExport(exported.name);
685
+ }
686
+ }
687
+ source.runPasses(["remove-unused-module-elements"]);
688
+
689
+ if (source.getNumGlobals() > 1 || source.getNumDataSegments() > 1) {
690
+ this.fail("optimized Wasm math kernel has an unsupported module shape");
691
+ }
692
+ if (source.getNumGlobals() === 1) {
693
+ const global = binaryen.getGlobalInfo(source.getGlobalByIndex(0));
694
+ if (
695
+ global.module
696
+ || global.name !== MATH_KERNEL_STACK_GLOBAL
697
+ || global.type !== binaryen.i32
698
+ || !global.mutable
699
+ ) {
700
+ this.fail("embedded Wasm math kernel has an invalid stack global");
701
+ }
702
+ this.module.addGlobal(
703
+ global.name,
704
+ global.type,
705
+ global.mutable,
706
+ this.module.copyExpression(global.init),
707
+ );
708
+ }
709
+
710
+ if (source.getNumDataSegments() === 1) {
711
+ const segment = source.getDataSegmentInfo(source.getDataSegmentByIndex(0));
712
+ if (
713
+ segment.name !== MATH_KERNEL_DATA_SEGMENT
714
+ || segment.passive
715
+ || !Number.isInteger(segment.offset)
716
+ || segment.offset < STATIC_BASE
717
+ || segment.offset + segment.data.byteLength > MATH_KERNEL_RESERVED_END
718
+ ) {
719
+ this.fail("embedded Wasm math kernel exceeds its reserved memory region");
720
+ }
721
+ this.memorySegments.push({
722
+ offset: this.module.i32.const(segment.offset),
723
+ data: new Uint8Array(segment.data),
724
+ });
725
+ }
726
+
727
+ this.module.setFeatures(this.module.getFeatures() | source.getFeatures());
728
+ for (let index = 0; index < source.getNumFunctions(); index += 1) {
729
+ const func = binaryen.getFunctionInfo(source.getFunctionByIndex(index));
730
+ if (func.module || !func.body) {
731
+ this.fail("embedded Wasm math kernel must not import functions");
732
+ }
733
+ this.module.addFunction(
734
+ func.name,
735
+ func.params,
736
+ func.results,
737
+ func.vars,
738
+ this.module.copyExpression(func.body),
739
+ );
740
+ }
741
+ } finally {
742
+ source.dispose();
743
+ }
744
+ }
745
+
746
+ addMirFunctions() {
747
+ this.functionNames = this.mir.functions.map((_, id) => `$onda.fn.${id}`);
748
+ for (let id = 0; id < this.mir.functions.length; id += 1) {
749
+ this.addMirFunction(id, this.mir.functions[id]);
750
+ }
751
+ }
752
+
753
+ addMirFunction(id, func) {
754
+ let nextIndex = 0;
755
+ const paramLayouts = func.params.map((param) => {
756
+ const layout = this.functionValueLayout(
757
+ param.ty,
758
+ nextIndex,
759
+ `parameter '${param.name}'`,
760
+ false,
761
+ param.mode,
762
+ );
763
+ nextIndex += layout.components.length;
764
+ return layout;
765
+ });
766
+ const paramScalars = paramLayouts.flatMap((layout) => layout.components);
767
+ const localLayouts = func.locals.map((local, localId) => {
768
+ const layout = this.functionValueLayout(
769
+ local.ty,
770
+ nextIndex,
771
+ `local ${localId} of '${func.name}'`,
772
+ true,
773
+ );
774
+ nextIndex += layout.components.length;
775
+ return layout;
776
+ });
777
+ const flatLocalScalars = localLayouts.flatMap((layout) => layout.components);
778
+ const localScalars = func.locals.map((local) => {
779
+ const type = this.type(local.ty);
780
+ return type.kind === "scalar" ? type.data : null;
781
+ });
782
+ const resultScalars = func.results.map((result, resultId) =>
783
+ this.requireScalarType(result, `result ${resultId} of '${func.name}'`),
784
+ );
785
+ const callResultLocals = this.collectCallResultLocals(func);
786
+ const sliceScratch = this.collectSliceScratchLocals(func);
787
+ const processFrameLocals = this.collectProcessFrameLocals(func);
788
+ if (
789
+ resultScalars.length > 1
790
+ || callResultLocals.some((entry) => entry.resultCount > 1)
791
+ ) {
792
+ this.module.setFeatures(
793
+ this.module.getFeatures() | binaryen.Features.Multivalue,
794
+ );
795
+ }
796
+ const context = {
797
+ function: func,
798
+ functionId: id,
799
+ paramScalars,
800
+ paramLayouts,
801
+ localScalars,
802
+ localLayouts,
803
+ flatLocalCount: flatLocalScalars.length,
804
+ callResultLocals: new Map(
805
+ callResultLocals.map((entry, index) => [
806
+ entry.call,
807
+ {
808
+ index: paramScalars.length + flatLocalScalars.length + index,
809
+ type: entry.type,
810
+ },
811
+ ]),
812
+ ),
813
+ sliceScratch: new Map(
814
+ sliceScratch.entries.map((entry, index) => [
815
+ entry.statement,
816
+ {
817
+ index:
818
+ paramScalars.length +
819
+ flatLocalScalars.length +
820
+ callResultLocals.length +
821
+ sliceScratch.offsets[index],
822
+ count: entry.count,
823
+ },
824
+ ]),
825
+ ),
826
+ eventId: func.kind?.kind === "event" ? func.kind.data : null,
827
+ processFrameLocals,
828
+ breakLabels: [],
829
+ continueLabels: [],
830
+ };
831
+ const body = this.compileBlock(func.body, context);
832
+ const functionRef = this.module.addFunction(
833
+ this.functionNames[id],
834
+ binaryen.createType(paramScalars.map((type) => this.wasmType(type))),
835
+ this.wasmResultType(resultScalars),
836
+ [
837
+ ...flatLocalScalars.map((type) => this.wasmType(type)),
838
+ ...callResultLocals.map((entry) => entry.type),
839
+ ...Array.from({ length: sliceScratch.count }, () => binaryen.i32),
840
+ ],
841
+ body,
842
+ );
843
+ for (let paramId = 0; paramId < func.params.length; paramId += 1) {
844
+ this.setFunctionValueNames(
845
+ functionRef,
846
+ paramLayouts[paramId],
847
+ `${func.params[paramId].name}.arg`,
848
+ );
849
+ }
850
+ for (let localId = 0; localId < func.locals.length; localId += 1) {
851
+ const name = func.locals[localId].name;
852
+ if (name) {
853
+ // Source names can repeat across disjoint lexical scopes while
854
+ // Binaryen requires every debug local name in a function to be unique.
855
+ // Keep the source spelling readable and make identity explicit with
856
+ // the deterministic MIR local ID.
857
+ this.setFunctionValueNames(
858
+ functionRef,
859
+ localLayouts[localId],
860
+ `${name}.local${localId}`,
861
+ );
862
+ }
863
+ }
864
+ }
865
+
866
+ functionValueLayout(
867
+ typeId,
868
+ index,
869
+ description,
870
+ allowStorageOnly = false,
871
+ passingMode = "value",
872
+ ) {
873
+ const type = this.type(typeId);
874
+ if (type.kind === "scalar") {
875
+ if (passingMode !== "value") {
876
+ return {
877
+ index,
878
+ typeId,
879
+ kind: "scalar_ref",
880
+ scalar: type.data,
881
+ components: ["i32"],
882
+ };
883
+ }
884
+ return { index, typeId, kind: "scalar", components: [type.data] };
885
+ }
886
+ if (type.kind === "slice") {
887
+ return {
888
+ index,
889
+ typeId,
890
+ kind: "slice",
891
+ components: ["i32", "i32", "i32"],
892
+ };
893
+ }
894
+ if (type.kind === "buffer") {
895
+ return {
896
+ index,
897
+ typeId,
898
+ kind: "buffer",
899
+ components: ["i32", "i32", "i32", "f32"],
900
+ };
901
+ }
902
+ if (type.kind === "array") {
903
+ if (passingMode !== "value") {
904
+ return { index, typeId, kind: "array_ref", components: ["i32"] };
905
+ }
906
+ if (allowStorageOnly) {
907
+ return { index, typeId, kind: "array", components: [] };
908
+ }
909
+ }
910
+ this.fail(`${description} has unsupported function value type '${type.kind}'`);
911
+ }
912
+
913
+ setFunctionValueNames(functionRef, layout, name) {
914
+ if (layout.kind === "scalar") {
915
+ binaryen.Function.setLocalName(functionRef, layout.index, name);
916
+ return;
917
+ }
918
+ if (layout.kind === "scalar_ref") {
919
+ binaryen.Function.setLocalName(functionRef, layout.index, `${name}.address`);
920
+ return;
921
+ }
922
+ if (layout.kind === "array_ref") {
923
+ binaryen.Function.setLocalName(functionRef, layout.index, `${name}.address`);
924
+ return;
925
+ }
926
+ if (layout.kind === "array") return;
927
+ const suffixes = layout.kind === "buffer"
928
+ ? ["address", "frames", "channels", "sample_rate"]
929
+ : ["address", "length", "stride"];
930
+ for (const [offset, suffix] of suffixes.entries()) {
931
+ binaryen.Function.setLocalName(
932
+ functionRef,
933
+ layout.index + offset,
934
+ `${name}.${suffix}`,
935
+ );
936
+ }
937
+ }
938
+
939
+ collectCallResultLocals(func) {
940
+ const result = [];
941
+ const visitBlock = (block) => {
942
+ for (const statement of block.statements) {
943
+ const kind = statement.kind?.kind;
944
+ const data = statement.kind?.data;
945
+ if (kind === "call" && data.results.length > 0) {
946
+ this.requireFunctionId(data.function, "call target");
947
+ const target = this.mir.functions[data.function];
948
+ const aliasesResult = data.args.some((argument, index) =>
949
+ target.params[index]?.mode !== "value"
950
+ && argument.kind === "place"
951
+ && argument.data.base.kind === "local"
952
+ && argument.data.projections.length === 0
953
+ && this.type(target.params[index].ty).kind === "scalar"
954
+ );
955
+ if (data.results.length === 1 && !aliasesResult) continue;
956
+ const scalars = target.results.map((typeId, resultId) =>
957
+ this.requireScalarType(
958
+ typeId,
959
+ `result ${resultId} of '${target.name}'`,
960
+ ),
961
+ );
962
+ result.push({
963
+ call: data,
964
+ resultCount: scalars.length,
965
+ type: binaryen.createType(
966
+ scalars.map((scalar) => this.wasmType(scalar)),
967
+ ),
968
+ });
969
+ } else if (kind === "if") {
970
+ visitBlock(data.then_block);
971
+ visitBlock(data.else_block);
972
+ } else if (kind === "loop") {
973
+ visitBlock(data.body);
974
+ }
975
+ }
976
+ };
977
+ visitBlock(func.body);
978
+ return result;
979
+ }
980
+
981
+ collectSliceScratchLocals(func) {
982
+ const entries = [];
983
+ const offsets = [];
984
+ let count = 0;
985
+ const visitBlock = (block) => {
986
+ for (const statement of block.statements) {
987
+ const kind = statement.kind?.kind;
988
+ const data = statement.kind?.data;
989
+ if (kind === "slice_fill" || kind === "slice_copy") {
990
+ offsets.push(count);
991
+ const scratchCount = kind === "slice_copy" ? 2 : 1;
992
+ entries.push({ statement, count: scratchCount });
993
+ count += scratchCount;
994
+ } else if (kind === "if") {
995
+ visitBlock(data.then_block);
996
+ visitBlock(data.else_block);
997
+ } else if (kind === "loop") {
998
+ visitBlock(data.body);
999
+ }
1000
+ }
1001
+ };
1002
+ visitBlock(func.body);
1003
+ return { entries, offsets, count };
1004
+ }
1005
+
1006
+ collectProcessFrameLocals(func) {
1007
+ const definitions = Array.from({ length: func.locals.length }, () => 0);
1008
+ const candidates = new Set();
1009
+ const visitBlock = (block) => {
1010
+ for (const statement of block.statements) {
1011
+ const kind = statement.kind?.kind;
1012
+ const data = statement.kind?.data;
1013
+ if (kind === "assign" && data.destination?.base?.kind === "local") {
1014
+ const localId = data.destination.base.data;
1015
+ if (
1016
+ Number.isInteger(localId) &&
1017
+ localId >= 0 &&
1018
+ localId < definitions.length
1019
+ ) {
1020
+ definitions[localId] += 1;
1021
+ if (
1022
+ data.destination.projections.length === 0 &&
1023
+ data.value?.kind === "process_frame"
1024
+ ) {
1025
+ candidates.add(localId);
1026
+ }
1027
+ }
1028
+ } else if (kind === "call") {
1029
+ for (const localId of data.results) {
1030
+ if (
1031
+ Number.isInteger(localId) &&
1032
+ localId >= 0 &&
1033
+ localId < definitions.length
1034
+ ) {
1035
+ definitions[localId] += 1;
1036
+ }
1037
+ }
1038
+ } else if (kind === "if") {
1039
+ visitBlock(data.then_block);
1040
+ visitBlock(data.else_block);
1041
+ } else if (kind === "loop") {
1042
+ visitBlock(data.body);
1043
+ }
1044
+ }
1045
+ };
1046
+ visitBlock(func.body);
1047
+ return new Set(
1048
+ [...candidates].filter((localId) => definitions[localId] === 1),
1049
+ );
1050
+ }
1051
+
1052
+ addAbiWrappers() {
1053
+ const initId = this.mir.entry_points.init;
1054
+ const processId = this.mir.entry_points.process;
1055
+ this.requireFunctionId(initId, "init entry point");
1056
+ this.requireFunctionId(processId, "process entry point");
1057
+
1058
+ this.module.setFeatures(
1059
+ this.module.getFeatures() |
1060
+ binaryen.Features.BulkMemory |
1061
+ binaryen.Features.BulkMemoryOpt |
1062
+ (this.options.simd ? binaryen.Features.SIMD128 : 0),
1063
+ );
1064
+ const initBody = this.module.block(null, [
1065
+ this.module.global.set(
1066
+ POINTER_GLOBALS.params,
1067
+ this.module.local.get(0, binaryen.i32),
1068
+ ),
1069
+ this.module.global.set(
1070
+ POINTER_GLOBALS.state,
1071
+ this.module.local.get(1, binaryen.i32),
1072
+ ),
1073
+ this.module.memory.fill(
1074
+ this.module.local.get(1, binaryen.i32),
1075
+ this.module.i32.const(0),
1076
+ this.module.i32.const(this.stateLayout.byteLength ?? 0),
1077
+ ),
1078
+ this.module.call(this.functionNames[initId], [], binaryen.none),
1079
+ ]);
1080
+ this.module.addFunction(
1081
+ "$onda.abi.init",
1082
+ binaryen.createType([binaryen.i32, binaryen.i32]),
1083
+ binaryen.none,
1084
+ [],
1085
+ initBody,
1086
+ );
1087
+ this.module.addFunctionExport("$onda.abi.init", "onda_init");
1088
+
1089
+ const processParams = binaryen.createType(
1090
+ Array.from({ length: 11 }, () => binaryen.i32),
1091
+ );
1092
+ const startFrame = () => this.module.local.get(4, binaryen.i32);
1093
+ const frames = () => this.module.local.get(5, binaryen.i32);
1094
+ const flags = () => this.module.local.get(6, binaryen.i32);
1095
+ const invalidRange = this.module.i32.or(
1096
+ this.module.i32.or(
1097
+ this.module.i32.or(
1098
+ this.module.i32.lt_s(startFrame(), this.module.i32.const(0)),
1099
+ this.module.i32.lt_s(frames(), this.module.i32.const(0)),
1100
+ ),
1101
+ this.module.i32.or(
1102
+ this.module.i32.gt_s(
1103
+ startFrame(),
1104
+ this.module.i32.const(this.mir.config.block_size),
1105
+ ),
1106
+ this.module.i32.gt_s(
1107
+ frames(),
1108
+ this.module.i32.sub(
1109
+ this.module.i32.const(this.mir.config.block_size),
1110
+ startFrame(),
1111
+ ),
1112
+ ),
1113
+ ),
1114
+ ),
1115
+ this.module.i32.ne(
1116
+ this.module.i32.and(
1117
+ flags(),
1118
+ this.module.i32.const(~ONDA_PROCESS_FULL_BLOCK),
1119
+ ),
1120
+ this.module.i32.const(0),
1121
+ ),
1122
+ );
1123
+ const processBody = this.module.block(null, [
1124
+ this.module.if(invalidRange, this.module.unreachable()),
1125
+ this.module.global.set(
1126
+ POINTER_GLOBALS.state,
1127
+ this.module.local.get(0, binaryen.i32),
1128
+ ),
1129
+ this.module.global.set(
1130
+ POINTER_GLOBALS.params,
1131
+ this.module.local.get(1, binaryen.i32),
1132
+ ),
1133
+ this.module.global.set(
1134
+ POINTER_GLOBALS.inputs,
1135
+ this.module.local.get(2, binaryen.i32),
1136
+ ),
1137
+ this.module.global.set(
1138
+ POINTER_GLOBALS.outputs,
1139
+ this.module.local.get(3, binaryen.i32),
1140
+ ),
1141
+ this.module.global.set(
1142
+ POINTER_GLOBALS.buffers,
1143
+ this.module.local.get(7, binaryen.i32),
1144
+ ),
1145
+ this.module.global.set(
1146
+ POINTER_GLOBALS.bufferFrames,
1147
+ this.module.local.get(8, binaryen.i32),
1148
+ ),
1149
+ this.module.global.set(
1150
+ POINTER_GLOBALS.bufferChannels,
1151
+ this.module.local.get(9, binaryen.i32),
1152
+ ),
1153
+ this.module.global.set(
1154
+ POINTER_GLOBALS.bufferSampleRates,
1155
+ this.module.local.get(10, binaryen.i32),
1156
+ ),
1157
+ this.module.call(
1158
+ this.functionNames[processId],
1159
+ [startFrame(), frames(), flags()],
1160
+ binaryen.none,
1161
+ ),
1162
+ ]);
1163
+ this.module.addFunction(
1164
+ "$onda.abi.process",
1165
+ processParams,
1166
+ binaryen.none,
1167
+ [],
1168
+ processBody,
1169
+ );
1170
+ this.module.addFunctionExport("$onda.abi.process", "onda_process");
1171
+
1172
+ this.mir.interface.events.forEach((event, eventId) => {
1173
+ this.requireFunctionId(event.handler, `event '${event.name}' handler`);
1174
+ const handler = this.mir.functions[event.handler];
1175
+ if (
1176
+ handler.kind?.kind !== "event" ||
1177
+ handler.kind.data !== eventId ||
1178
+ handler.params.length !== 0 ||
1179
+ handler.results.length !== 0
1180
+ ) {
1181
+ this.fail(`event '${event.name}' has an invalid MIR handler signature`);
1182
+ }
1183
+ const wrapperName = `$onda.abi.event.${eventId}`;
1184
+ const body = this.module.block(null, [
1185
+ this.module.global.set(
1186
+ POINTER_GLOBALS.eventPayload,
1187
+ this.module.local.get(0, binaryen.i32),
1188
+ ),
1189
+ this.module.global.set(
1190
+ POINTER_GLOBALS.params,
1191
+ this.module.local.get(1, binaryen.i32),
1192
+ ),
1193
+ this.module.global.set(
1194
+ POINTER_GLOBALS.state,
1195
+ this.module.local.get(2, binaryen.i32),
1196
+ ),
1197
+ this.module.global.set(
1198
+ POINTER_GLOBALS.buffers,
1199
+ this.module.local.get(3, binaryen.i32),
1200
+ ),
1201
+ this.module.global.set(
1202
+ POINTER_GLOBALS.bufferFrames,
1203
+ this.module.local.get(4, binaryen.i32),
1204
+ ),
1205
+ this.module.global.set(
1206
+ POINTER_GLOBALS.bufferChannels,
1207
+ this.module.local.get(5, binaryen.i32),
1208
+ ),
1209
+ this.module.global.set(
1210
+ POINTER_GLOBALS.bufferSampleRates,
1211
+ this.module.local.get(6, binaryen.i32),
1212
+ ),
1213
+ this.module.call(this.functionNames[event.handler], [], binaryen.none),
1214
+ ]);
1215
+ this.module.addFunction(
1216
+ wrapperName,
1217
+ binaryen.createType(Array.from({ length: 7 }, () => binaryen.i32)),
1218
+ binaryen.none,
1219
+ [],
1220
+ body,
1221
+ );
1222
+ this.module.addFunctionExport(wrapperName, `onda_event_${eventId}`);
1223
+ });
1224
+ }
1225
+
1226
+ compileBlock(block, context) {
1227
+ return this.module.block(
1228
+ null,
1229
+ block.statements.map((statement) =>
1230
+ this.compileStatement(statement, context),
1231
+ ),
1232
+ );
1233
+ }
1234
+
1235
+ compileStatement(statement, context) {
1236
+ const kind = statement.kind?.kind;
1237
+ const data = statement.kind?.data;
1238
+ switch (kind) {
1239
+ case "assign": {
1240
+ if (data.value?.kind === "process_frame") {
1241
+ const localId =
1242
+ data.destination?.base?.kind === "local" &&
1243
+ data.destination.projections.length === 0
1244
+ ? data.destination.base.data
1245
+ : null;
1246
+ if (!context.processFrameLocals.has(localId)) {
1247
+ this.fail(
1248
+ "process_frame must be the unique definition of an unprojected local",
1249
+ );
1250
+ }
1251
+ }
1252
+ if (this.type(this.placeTypeId(data.destination, context)).kind === "slice") {
1253
+ return this.storeSlicePlace(
1254
+ data.destination,
1255
+ this.compileSliceRvalue(data.value, context),
1256
+ context,
1257
+ );
1258
+ }
1259
+ const scalar = this.placeScalarType(data.destination, context);
1260
+ const value = this.compileRvalue(data.value, scalar, context);
1261
+ return this.storePlace(data.destination, value, scalar, context);
1262
+ }
1263
+ case "call":
1264
+ return this.compileCall(data, context);
1265
+ case "output_store":
1266
+ return this.compileOutputStore(data, context);
1267
+ case "control_output_store":
1268
+ return this.compileControlOutputStore(data, context);
1269
+ case "if":
1270
+ return this.module.if(
1271
+ this.compileValue(data.condition, context),
1272
+ this.compileBlock(data.then_block, context),
1273
+ this.compileBlock(data.else_block, context),
1274
+ );
1275
+ case "loop": {
1276
+ const id = this.nextLabel++;
1277
+ const breakLabel = `$onda.break.${id}`;
1278
+ const continueLabel = `$onda.loop.${id}`;
1279
+ context.breakLabels.push(breakLabel);
1280
+ context.continueLabels.push(continueLabel);
1281
+ const body = this.compileBlock(data.body, context);
1282
+ context.breakLabels.pop();
1283
+ context.continueLabels.pop();
1284
+ return this.module.block(breakLabel, [
1285
+ this.module.loop(
1286
+ continueLabel,
1287
+ this.module.block(null, [body, this.module.br(continueLabel)]),
1288
+ ),
1289
+ ]);
1290
+ }
1291
+ case "break":
1292
+ return this.module.br(this.currentLabel(context.breakLabels, "break"));
1293
+ case "continue":
1294
+ return this.module.br(
1295
+ this.currentLabel(context.continueLabels, "continue"),
1296
+ );
1297
+ case "return": {
1298
+ const values = data.values.map((value) =>
1299
+ this.compileValue(value, context),
1300
+ );
1301
+ return this.module.return(
1302
+ values.length > 1 ? this.module.tuple.make(values) : values[0],
1303
+ );
1304
+ }
1305
+ case "buffer_store":
1306
+ return this.compileBufferStore(data, context);
1307
+ case "buffer_param_store":
1308
+ return this.compileBufferParamStore(data, context);
1309
+ case "slice_store":
1310
+ return this.compileSliceStore(data, context);
1311
+ case "slice_fill":
1312
+ return this.compileSliceFill(statement, data, context);
1313
+ case "slice_copy":
1314
+ return this.compileSliceCopy(statement, data, context);
1315
+ default:
1316
+ this.fail(`unknown MIR statement '${String(kind)}'`);
1317
+ }
1318
+ }
1319
+
1320
+ compileCall(data, context) {
1321
+ this.requireFunctionId(data.function, "call target");
1322
+ const target = this.mir.functions[data.function];
1323
+ if (data.args.length !== target.params.length) {
1324
+ this.fail(`call to '${target.name}' has the wrong number of arguments`);
1325
+ }
1326
+ const args = data.args.flatMap((argument, index) => {
1327
+ const parameterType = this.type(target.params[index].ty);
1328
+ if (parameterType.kind === "scalar") {
1329
+ if (target.params[index].mode === "value") {
1330
+ if (argument.kind !== "value") {
1331
+ this.fail(`scalar call argument ${index} of '${target.name}' is not a value`);
1332
+ }
1333
+ return [this.compileValue(argument.data, context)];
1334
+ }
1335
+ if (!["place", "slice_element"].includes(argument.kind)) {
1336
+ this.fail(
1337
+ `reference call argument ${index} of '${target.name}' is not addressable`,
1338
+ );
1339
+ }
1340
+ return [
1341
+ argument.kind === "place"
1342
+ ? this.placeAddress(argument.data, context)
1343
+ : this.compileSliceAddress(
1344
+ argument.data.slice,
1345
+ argument.data.index,
1346
+ argument.data.bounds,
1347
+ context,
1348
+ ),
1349
+ ];
1350
+ }
1351
+ if (parameterType.kind === "slice") {
1352
+ if (argument.kind !== "value") {
1353
+ this.fail(`slice call argument ${index} of '${target.name}' is not a value`);
1354
+ }
1355
+ return this.compileSliceValue(argument.data, context);
1356
+ }
1357
+ if (parameterType.kind === "array") {
1358
+ if (
1359
+ target.params[index].mode === "value" ||
1360
+ !["place", "array_window", "slice_window"].includes(argument.kind)
1361
+ ) {
1362
+ this.fail(`array reference argument ${index} of '${target.name}' is invalid`);
1363
+ }
1364
+ if (argument.kind === "place") {
1365
+ return [this.placeAddress(argument.data, context)];
1366
+ }
1367
+ if (argument.kind === "array_window") {
1368
+ return [
1369
+ this.compileArrayWindowAddress(
1370
+ argument.data,
1371
+ parameterType,
1372
+ context,
1373
+ ),
1374
+ ];
1375
+ }
1376
+ return [
1377
+ this.compileSliceWindowAddress(
1378
+ argument.data,
1379
+ parameterType,
1380
+ context,
1381
+ ),
1382
+ ];
1383
+ }
1384
+ if (parameterType.kind === "buffer") {
1385
+ if (argument.kind === "buffer") {
1386
+ return this.compileInterfaceBufferValue(argument.data);
1387
+ }
1388
+ if (argument.kind === "place") {
1389
+ return this.loadBufferPlace(argument.data, context);
1390
+ }
1391
+ this.fail(`buffer call argument ${index} of '${target.name}' is invalid`);
1392
+ }
1393
+ this.fail(
1394
+ `call argument ${index} of '${target.name}' has unsupported type '${parameterType.kind}'`,
1395
+ );
1396
+ });
1397
+ if (data.results.length !== target.results.length) {
1398
+ this.fail(`call result arity for '${target.name}' does not match its signature`);
1399
+ }
1400
+ const resultScalars = target.results.map((result, resultId) =>
1401
+ this.requireScalarType(result, `result ${resultId} of '${target.name}'`),
1402
+ );
1403
+ const resultType = this.wasmResultType(resultScalars);
1404
+ const call = this.module.call(this.functionNames[data.function], args, resultType);
1405
+ const localReferenceSync = data.args.flatMap((argument, index) => {
1406
+ const parameter = target.params[index];
1407
+ if (
1408
+ parameter.mode === "value"
1409
+ || argument.kind !== "place"
1410
+ || argument.data.base.kind !== "local"
1411
+ || argument.data.projections.length !== 0
1412
+ ) {
1413
+ return [];
1414
+ }
1415
+ const layout =
1416
+ this.localScalarRefLayout[context.functionId]?.[argument.data.base.data];
1417
+ if (!layout) return [];
1418
+ return [{
1419
+ localId: argument.data.base.data,
1420
+ address: layout.address,
1421
+ scalar: layout.scalar,
1422
+ writeBack: parameter.mode === "read_write_reference",
1423
+ }];
1424
+ });
1425
+ const beforeCall = localReferenceSync.map((sync) =>
1426
+ this.storeScalar(
1427
+ sync.scalar,
1428
+ this.module.i32.const(sync.address),
1429
+ this.module.local.get(
1430
+ this.localIndex(sync.localId, context),
1431
+ this.wasmType(sync.scalar),
1432
+ ),
1433
+ ),
1434
+ );
1435
+ const afterCall = localReferenceSync
1436
+ .filter((sync) => sync.writeBack)
1437
+ .map((sync) =>
1438
+ this.module.local.set(
1439
+ this.localIndex(sync.localId, context),
1440
+ this.loadScalar(sync.scalar, this.module.i32.const(sync.address)),
1441
+ ),
1442
+ );
1443
+ const resultSpill = context.callResultLocals.get(data);
1444
+ if (localReferenceSync.length > 0 && data.results.length > 0) {
1445
+ if (!resultSpill) {
1446
+ this.fail(`internal result spill is missing for call to '${target.name}'`);
1447
+ }
1448
+ const spilledValue = () =>
1449
+ this.module.local.get(resultSpill.index, resultSpill.type);
1450
+ const assignResults = data.results.length === 1
1451
+ ? [
1452
+ this.module.local.set(
1453
+ this.localIndex(data.results[0], context),
1454
+ spilledValue(),
1455
+ ),
1456
+ ]
1457
+ : data.results.map((localId, index) =>
1458
+ this.module.local.set(
1459
+ this.localIndex(localId, context),
1460
+ this.module.tuple.extract(spilledValue(), index),
1461
+ ),
1462
+ );
1463
+ return this.module.block(null, [
1464
+ ...beforeCall,
1465
+ this.module.local.set(resultSpill.index, call),
1466
+ ...afterCall,
1467
+ ...assignResults,
1468
+ ]);
1469
+ }
1470
+ let compiledCall;
1471
+ if (data.results.length === 0) {
1472
+ compiledCall = call;
1473
+ } else if (data.results.length === 1) {
1474
+ compiledCall = this.module.local.set(
1475
+ this.localIndex(data.results[0], context),
1476
+ call,
1477
+ );
1478
+ } else {
1479
+ const tupleLocal = context.callResultLocals.get(data);
1480
+ if (!tupleLocal) {
1481
+ this.fail(`internal tuple spill is missing for call to '${target.name}'`);
1482
+ }
1483
+ const tupleValue = () =>
1484
+ this.module.local.get(tupleLocal.index, tupleLocal.type);
1485
+ compiledCall = this.module.block(null, [
1486
+ this.module.local.set(tupleLocal.index, call),
1487
+ ...data.results.map((localId, index) =>
1488
+ this.module.local.set(
1489
+ this.localIndex(localId, context),
1490
+ this.module.tuple.extract(tupleValue(), index),
1491
+ ),
1492
+ ),
1493
+ ]);
1494
+ }
1495
+ if (localReferenceSync.length === 0) return compiledCall;
1496
+ return this.module.block(null, [...beforeCall, compiledCall, ...afterCall]);
1497
+ }
1498
+
1499
+ compileOutputStore(data, context) {
1500
+ this.requireProcessFrame(data.frame, context, "audio output store");
1501
+ const port = this.outputLayout[data.output];
1502
+ if (!port) {
1503
+ this.fail(`output id ${data.output} is out of range`);
1504
+ }
1505
+ const channel = this.compilePortChannel(port, data.element, data.bounds, context);
1506
+ const tableAddress = this.module.i32.add(
1507
+ this.module.global.get(POINTER_GLOBALS.outputs, binaryen.i32),
1508
+ this.module.i32.mul(channel, this.module.i32.const(4)),
1509
+ );
1510
+ const channelPointer = this.module.i32.load(0, 4, tableAddress);
1511
+ const sampleAddress = this.module.i32.add(
1512
+ channelPointer,
1513
+ this.module.i32.mul(
1514
+ this.compileValue(data.frame, context),
1515
+ this.module.i32.const(port.size),
1516
+ ),
1517
+ );
1518
+ return this.storeScalar(
1519
+ port.scalar,
1520
+ sampleAddress,
1521
+ this.compileValue(data.value, context),
1522
+ );
1523
+ }
1524
+
1525
+ compileControlOutputStore(data, context) {
1526
+ const output = this.mir.interface.control_outputs[data.output];
1527
+ const layout = this.controlOutputLayout[data.output];
1528
+ if (!output || !layout) {
1529
+ this.fail(`control output id ${data.output} is out of range`);
1530
+ }
1531
+ const flattened = this.flattenPortType(this.type(output.ty));
1532
+ let elementOffset = this.module.i32.const(0);
1533
+ if (this.type(output.ty).kind !== "array") {
1534
+ if (data.element !== null) {
1535
+ this.fail("scalar control output unexpectedly has an element index");
1536
+ }
1537
+ } else {
1538
+ if (data.element === null) {
1539
+ this.fail("array control output is missing its element index");
1540
+ }
1541
+ const index = this.compileBoundedIndex(
1542
+ data.element,
1543
+ flattened.channels,
1544
+ data.bounds,
1545
+ context,
1546
+ );
1547
+ elementOffset = this.module.i32.mul(
1548
+ index,
1549
+ this.module.i32.const(layout.size / flattened.channels),
1550
+ );
1551
+ }
1552
+ const address = this.module.i32.add(
1553
+ this.module.i32.add(
1554
+ this.module.global.get(POINTER_GLOBALS.state, binaryen.i32),
1555
+ this.module.i32.const(layout.offset),
1556
+ ),
1557
+ elementOffset,
1558
+ );
1559
+ return this.storeScalar(
1560
+ flattened.scalar,
1561
+ address,
1562
+ this.compileValue(data.value, context),
1563
+ );
1564
+ }
1565
+
1566
+ compileBufferStore(data, context) {
1567
+ const buffer = this.requireBuffer(data.buffer);
1568
+ if (buffer.access !== "read_write") {
1569
+ this.fail(`buffer '${buffer.name}' is read-only`);
1570
+ }
1571
+ return this.storeScalar(
1572
+ buffer.element,
1573
+ this.compileBufferAddress(data, context),
1574
+ this.compileValue(data.value, context),
1575
+ );
1576
+ }
1577
+
1578
+ compileBufferParamStore(data, context) {
1579
+ const type = this.bufferParamType(data.parameter, context);
1580
+ if (type.data.access !== "read_write") {
1581
+ this.fail(`buffer parameter ${data.parameter} is read-only`);
1582
+ }
1583
+ return this.storeScalar(
1584
+ type.data.element,
1585
+ this.compileBufferParamAddress(data, context),
1586
+ this.compileValue(data.value, context),
1587
+ );
1588
+ }
1589
+
1590
+ compileRvalue(rvalue, expectedScalar, context) {
1591
+ const kind = rvalue.kind;
1592
+ const data = rvalue.data;
1593
+ switch (kind) {
1594
+ case "use":
1595
+ return this.compileValue(data, context);
1596
+ case "load":
1597
+ return this.loadPlace(data, context);
1598
+ case "unary":
1599
+ return this.compileUnary(
1600
+ data.op,
1601
+ this.valueScalarType(data.operand, context),
1602
+ this.compileValue(data.operand, context),
1603
+ );
1604
+ case "binary": {
1605
+ const scalar = this.valueScalarType(data.lhs, context);
1606
+ return this.compileBinary(
1607
+ data.op,
1608
+ scalar,
1609
+ () => this.compileValue(data.lhs, context),
1610
+ () => this.compileValue(data.rhs, context),
1611
+ );
1612
+ }
1613
+ case "compare": {
1614
+ const scalar = this.valueScalarType(data.lhs, context);
1615
+ return this.compileCompare(
1616
+ data.op,
1617
+ scalar,
1618
+ this.compileValue(data.lhs, context),
1619
+ this.compileValue(data.rhs, context),
1620
+ );
1621
+ }
1622
+ case "cast":
1623
+ return this.compileCast(
1624
+ this.valueScalarType(data.value, context),
1625
+ data.to,
1626
+ this.compileValue(data.value, context),
1627
+ );
1628
+ case "intrinsic":
1629
+ return this.compileIntrinsic(data, expectedScalar, context);
1630
+ case "process_frame":
1631
+ return this.compileProcessFrame(data, context);
1632
+ case "input_load":
1633
+ return this.compileInputLoad(data, context);
1634
+ case "output_load":
1635
+ return this.compileOutputLoad(data, context);
1636
+ case "const_data_load":
1637
+ return this.compileConstDataLoad(data, context);
1638
+ case "buffer_load":
1639
+ return this.compileBufferLoad(data, context);
1640
+ case "buffer_param_load":
1641
+ return this.compileBufferParamLoad(data, context);
1642
+ case "buffer_len":
1643
+ return this.compileBufferLen(data);
1644
+ case "buffer_param_len":
1645
+ return this.compileBufferParamLen(data, context);
1646
+ case "buffer_channels":
1647
+ return this.loadBufferTableValue(
1648
+ POINTER_GLOBALS.bufferChannels,
1649
+ data,
1650
+ "i32",
1651
+ );
1652
+ case "buffer_param_channels":
1653
+ return this.loadBufferParamComponent(data, 2, "i32", context);
1654
+ case "buffer_sample_rate":
1655
+ return this.loadBufferTableValue(
1656
+ POINTER_GLOBALS.bufferSampleRates,
1657
+ data,
1658
+ "f32",
1659
+ );
1660
+ case "buffer_param_sample_rate":
1661
+ return this.loadBufferParamComponent(data, 3, "f32", context);
1662
+ case "slice_len":
1663
+ return this.compileSliceValue(data, context)[1];
1664
+ case "slice_load":
1665
+ return this.compileSliceLoad(data, context);
1666
+ case "make_slice":
1667
+ this.fail("make_slice must be assigned to a slice-typed destination");
1668
+ break;
1669
+ default:
1670
+ this.fail(`unknown MIR rvalue '${String(kind)}'`);
1671
+ }
1672
+ }
1673
+
1674
+ compileSliceRvalue(rvalue, context) {
1675
+ switch (rvalue.kind) {
1676
+ case "use":
1677
+ return this.compileSliceValue(rvalue.data, context);
1678
+ case "load":
1679
+ return this.loadSlicePlace(rvalue.data, context);
1680
+ case "make_slice":
1681
+ return this.compileMakeSlice(rvalue.data, context);
1682
+ default:
1683
+ this.fail(`rvalue '${String(rvalue.kind)}' does not produce a slice`);
1684
+ }
1685
+ }
1686
+
1687
+ compileInputLoad(data, context) {
1688
+ this.requireProcessFrame(data.frame, context, "audio input load");
1689
+ const port = this.inputLayout[data.input];
1690
+ if (!port) {
1691
+ this.fail(`input id ${data.input} is out of range`);
1692
+ }
1693
+ const channel = this.compilePortChannel(port, data.element, data.bounds, context);
1694
+ const tableAddress = this.module.i32.add(
1695
+ this.module.global.get(POINTER_GLOBALS.inputs, binaryen.i32),
1696
+ this.module.i32.mul(channel, this.module.i32.const(4)),
1697
+ );
1698
+ const channelPointer = this.module.i32.load(0, 4, tableAddress);
1699
+ const sampleAddress = this.module.i32.add(
1700
+ channelPointer,
1701
+ this.module.i32.mul(
1702
+ this.compileValue(data.frame, context),
1703
+ this.module.i32.const(port.size),
1704
+ ),
1705
+ );
1706
+ return this.loadScalar(port.scalar, sampleAddress);
1707
+ }
1708
+
1709
+ compileProcessFrame(data, context) {
1710
+ if (context.function.kind?.kind !== "process") {
1711
+ this.fail("process_frame is only valid in the process entry point");
1712
+ }
1713
+ const offset = () => this.compileValue(data.offset, context);
1714
+ const startFrame = () =>
1715
+ this.module.local.get(context.paramLayouts[0].index, binaryen.i32);
1716
+ const frames = () =>
1717
+ this.module.local.get(context.paramLayouts[1].index, binaryen.i32);
1718
+ const invalid = this.module.i32.ge_u(offset(), frames());
1719
+ return this.module.if(
1720
+ invalid,
1721
+ this.module.unreachable(),
1722
+ this.module.i32.add(startFrame(), offset()),
1723
+ );
1724
+ }
1725
+
1726
+ requireProcessFrame(value, context, operation) {
1727
+ if (
1728
+ context.function.kind?.kind !== "process" ||
1729
+ value?.kind !== "local" ||
1730
+ !context.processFrameLocals.has(value.data)
1731
+ ) {
1732
+ this.fail(`${operation} frame must come directly from process_frame`);
1733
+ }
1734
+ }
1735
+
1736
+ compileOutputLoad(data, context) {
1737
+ this.requireProcessFrame(data.frame, context, "audio output load");
1738
+ const port = this.outputLayout[data.output];
1739
+ if (!port) {
1740
+ this.fail(`output id ${data.output} is out of range`);
1741
+ }
1742
+ const channel = this.compilePortChannel(port, data.element, data.bounds, context);
1743
+ const tableAddress = this.module.i32.add(
1744
+ this.module.global.get(POINTER_GLOBALS.outputs, binaryen.i32),
1745
+ this.module.i32.mul(channel, this.module.i32.const(4)),
1746
+ );
1747
+ const channelPointer = this.module.i32.load(0, 4, tableAddress);
1748
+ const sampleAddress = this.module.i32.add(
1749
+ channelPointer,
1750
+ this.module.i32.mul(
1751
+ this.compileValue(data.frame, context),
1752
+ this.module.i32.const(port.size),
1753
+ ),
1754
+ );
1755
+ return this.loadScalar(port.scalar, sampleAddress);
1756
+ }
1757
+
1758
+ compilePortChannel(port, element, bounds, context) {
1759
+ if (!port.isArray) {
1760
+ if (element !== null) {
1761
+ this.fail("scalar audio port unexpectedly has an element index");
1762
+ }
1763
+ return this.module.i32.const(port.channel);
1764
+ }
1765
+ if (element === null) {
1766
+ this.fail("array audio port is missing its element index");
1767
+ }
1768
+ const index = this.compileBoundedIndex(element, port.channels, bounds, context);
1769
+ return this.module.i32.add(this.module.i32.const(port.channel), index);
1770
+ }
1771
+
1772
+ compileConstDataLoad(data, context) {
1773
+ const item = this.constLayout[data.data];
1774
+ if (!item) {
1775
+ this.fail(`const data id ${data.data} is out of range`);
1776
+ }
1777
+ const index = this.compileBoundedIndex(data.index, item.len, data.bounds, context);
1778
+ const address = this.module.i32.add(
1779
+ this.module.i32.const(item.address),
1780
+ this.module.i32.mul(index, this.module.i32.const(this.scalarSize(item.scalar))),
1781
+ );
1782
+ return this.loadScalar(item.scalar, address);
1783
+ }
1784
+
1785
+ compileBufferLoad(data, context) {
1786
+ const buffer = this.requireBuffer(data.buffer);
1787
+ return this.loadScalar(
1788
+ buffer.element,
1789
+ this.compileBufferAddress(data, context),
1790
+ );
1791
+ }
1792
+
1793
+ compileBufferParamLoad(data, context) {
1794
+ const type = this.bufferParamType(data.parameter, context);
1795
+ return this.loadScalar(
1796
+ type.data.element,
1797
+ this.compileBufferParamAddress(data, context),
1798
+ );
1799
+ }
1800
+
1801
+ compileBufferParamAddress(data, context) {
1802
+ const type = this.bufferParamType(data.parameter, context);
1803
+ const component = (offset, scalar) => () =>
1804
+ this.loadBufferParamComponent(data.parameter, offset, scalar, context);
1805
+ const channels = component(2, "i32");
1806
+ const rawIndex = () => {
1807
+ if (data.channel === null) {
1808
+ return this.compileValue(data.index, context);
1809
+ }
1810
+ return this.module.i32.add(
1811
+ this.module.i32.mul(this.compileValue(data.index, context), channels()),
1812
+ this.compileValue(data.channel, context),
1813
+ );
1814
+ };
1815
+ const index = this.compileDynamicBoundedIndex(
1816
+ rawIndex,
1817
+ () => this.compileBufferParamTotalScalarLen(data.parameter, context),
1818
+ data.bounds,
1819
+ true,
1820
+ );
1821
+ return this.module.i32.add(
1822
+ this.loadBufferParamComponent(data.parameter, 0, "i32", context),
1823
+ this.module.i32.mul(
1824
+ index,
1825
+ this.module.i32.const(this.scalarSize(type.data.element)),
1826
+ ),
1827
+ );
1828
+ }
1829
+
1830
+ compileBufferAddress(data, context) {
1831
+ const buffer = this.requireBuffer(data.buffer);
1832
+ const rawIndex = () => {
1833
+ if (data.channel === null) {
1834
+ return this.compileValue(data.index, context);
1835
+ }
1836
+ return this.module.i32.add(
1837
+ this.module.i32.mul(
1838
+ this.compileValue(data.index, context),
1839
+ this.loadBufferTableValue(
1840
+ POINTER_GLOBALS.bufferChannels,
1841
+ data.buffer,
1842
+ "i32",
1843
+ ),
1844
+ ),
1845
+ this.compileValue(data.channel, context),
1846
+ );
1847
+ };
1848
+ const index = this.compileDynamicBoundedIndex(
1849
+ rawIndex,
1850
+ () => this.compileBufferTotalScalarLen(data.buffer),
1851
+ data.bounds,
1852
+ true,
1853
+ );
1854
+ const pointer = this.loadBufferTableValue(
1855
+ POINTER_GLOBALS.buffers,
1856
+ data.buffer,
1857
+ "i32",
1858
+ );
1859
+ return this.module.i32.add(
1860
+ pointer,
1861
+ this.module.i32.mul(
1862
+ index,
1863
+ this.module.i32.const(this.scalarSize(buffer.element)),
1864
+ ),
1865
+ );
1866
+ }
1867
+
1868
+ compileBufferLen(bufferId) {
1869
+ this.requireBuffer(bufferId);
1870
+ return this.loadBufferTableValue(
1871
+ POINTER_GLOBALS.bufferFrames,
1872
+ bufferId,
1873
+ "i32",
1874
+ );
1875
+ }
1876
+
1877
+ compileBufferTotalScalarLen(bufferId) {
1878
+ this.requireBuffer(bufferId);
1879
+ return this.module.i32.mul(
1880
+ this.compileBufferLen(bufferId),
1881
+ this.loadBufferTableValue(
1882
+ POINTER_GLOBALS.bufferChannels,
1883
+ bufferId,
1884
+ "i32",
1885
+ ),
1886
+ );
1887
+ }
1888
+
1889
+ compileBufferParamLen(parameterId, context) {
1890
+ this.bufferParamType(parameterId, context);
1891
+ return this.loadBufferParamComponent(parameterId, 1, "i32", context);
1892
+ }
1893
+
1894
+ compileBufferParamTotalScalarLen(parameterId, context) {
1895
+ this.bufferParamType(parameterId, context);
1896
+ return this.module.i32.mul(
1897
+ this.compileBufferParamLen(parameterId, context),
1898
+ this.loadBufferParamComponent(parameterId, 2, "i32", context),
1899
+ );
1900
+ }
1901
+
1902
+ bufferParamType(parameterId, context) {
1903
+ const parameter = context.function.params[parameterId];
1904
+ const type = parameter && this.type(parameter.ty);
1905
+ if (!type || type.kind !== "buffer") {
1906
+ this.fail(`parameter id ${parameterId} is not a buffer`);
1907
+ }
1908
+ return type;
1909
+ }
1910
+
1911
+ bufferParamLayout(parameterId, context) {
1912
+ const layout = context.paramLayouts[parameterId];
1913
+ if (!layout || layout.kind !== "buffer") {
1914
+ this.fail(`parameter id ${parameterId} has no buffer descriptor`);
1915
+ }
1916
+ return layout;
1917
+ }
1918
+
1919
+ loadBufferParamComponent(parameterId, offset, scalar, context) {
1920
+ const layout = this.bufferParamLayout(parameterId, context);
1921
+ return this.module.local.get(layout.index + offset, this.wasmType(scalar));
1922
+ }
1923
+
1924
+ loadBufferPlace(place, context) {
1925
+ if (place.base.kind !== "parameter" || place.projections.length !== 0) {
1926
+ this.fail("buffer call arguments must be unprojected buffer parameters");
1927
+ }
1928
+ const layout = this.bufferParamLayout(place.base.data, context);
1929
+ return layout.components.map((scalar, offset) =>
1930
+ this.module.local.get(layout.index + offset, this.wasmType(scalar)),
1931
+ );
1932
+ }
1933
+
1934
+ compileInterfaceBufferValue(bufferId) {
1935
+ this.requireBuffer(bufferId);
1936
+ return [
1937
+ this.loadBufferTableValue(POINTER_GLOBALS.buffers, bufferId, "i32"),
1938
+ this.loadBufferTableValue(POINTER_GLOBALS.bufferFrames, bufferId, "i32"),
1939
+ this.loadBufferTableValue(POINTER_GLOBALS.bufferChannels, bufferId, "i32"),
1940
+ this.loadBufferTableValue(POINTER_GLOBALS.bufferSampleRates, bufferId, "f32"),
1941
+ ];
1942
+ }
1943
+
1944
+ loadBufferTableValue(globalName, bufferId, scalar) {
1945
+ this.requireBuffer(bufferId);
1946
+ const size = this.scalarSize(scalar);
1947
+ const address = this.module.i32.add(
1948
+ this.module.global.get(globalName, binaryen.i32),
1949
+ this.module.i32.const(bufferId * size),
1950
+ );
1951
+ return this.loadScalar(scalar, address);
1952
+ }
1953
+
1954
+ compileSliceValue(value, context) {
1955
+ if (value.kind !== "local") {
1956
+ this.fail("slice values must reside in MIR locals");
1957
+ }
1958
+ const layout = context.localLayouts[value.data];
1959
+ if (!layout || layout.kind !== "slice") {
1960
+ this.fail(`local id ${value.data} is not a slice`);
1961
+ }
1962
+ return layout.components.map((scalar, offset) =>
1963
+ this.module.local.get(layout.index + offset, this.wasmType(scalar)),
1964
+ );
1965
+ }
1966
+
1967
+ loadSlicePlace(place, context) {
1968
+ if (place.projections.length !== 0) {
1969
+ this.fail("slice places cannot have projections");
1970
+ }
1971
+ let layout;
1972
+ if (place.base.kind === "local") {
1973
+ layout = context.localLayouts[place.base.data];
1974
+ } else if (place.base.kind === "parameter") {
1975
+ layout = context.paramLayouts[place.base.data];
1976
+ } else if (place.base.kind === "event_param") {
1977
+ const event = this.mir.interface.events[context.eventId];
1978
+ const parameter = event?.params[place.base.data];
1979
+ const type = parameter && this.type(parameter.ty);
1980
+ if (!type || type.kind !== "slice") {
1981
+ this.fail(`event parameter id ${place.base.data} is not a slice`);
1982
+ }
1983
+ const header = () =>
1984
+ this.compileEventParamAddress(context.eventId, place.base.data);
1985
+ return [
1986
+ this.module.i32.add(header(), this.module.i32.const(4)),
1987
+ this.module.i32.load(0, 4, header()),
1988
+ this.module.i32.const(this.scalarSize(type.data.element)),
1989
+ ];
1990
+ } else {
1991
+ this.fail(`slice place base '${place.base.kind}' is not supported yet`);
1992
+ }
1993
+ if (!layout || layout.kind !== "slice") {
1994
+ this.fail(`place base '${place.base.kind}' id ${place.base.data} is not a slice`);
1995
+ }
1996
+ return layout.components.map((scalar, offset) =>
1997
+ this.module.local.get(layout.index + offset, this.wasmType(scalar)),
1998
+ );
1999
+ }
2000
+
2001
+ compileEventParamAddress(eventId, paramId) {
2002
+ const event = this.mir.interface.events[eventId];
2003
+ if (!event || !Number.isInteger(paramId) || paramId < 0 || paramId >= event.params.length) {
2004
+ this.fail(`event parameter id ${paramId} is invalid for event ${eventId}`);
2005
+ }
2006
+ let offset = () => this.module.i32.const(0);
2007
+ for (let index = 0; index < paramId; index += 1) {
2008
+ const previous = offset;
2009
+ const type = this.type(event.params[index].ty);
2010
+ if (type.kind === "slice") {
2011
+ const elementSize = this.scalarSize(type.data.element);
2012
+ offset = () =>
2013
+ this.module.i32.add(
2014
+ previous(),
2015
+ this.module.i32.add(
2016
+ this.module.i32.const(4),
2017
+ this.module.i32.mul(
2018
+ this.module.i32.load(
2019
+ 0,
2020
+ 4,
2021
+ this.module.i32.add(
2022
+ this.module.global.get(POINTER_GLOBALS.eventPayload, binaryen.i32),
2023
+ previous(),
2024
+ ),
2025
+ ),
2026
+ this.module.i32.const(elementSize),
2027
+ ),
2028
+ ),
2029
+ );
2030
+ } else {
2031
+ const size = this.typeLayout(event.params[index].ty).size;
2032
+ offset = () => this.module.i32.add(previous(), this.module.i32.const(size));
2033
+ }
2034
+ }
2035
+ return this.module.i32.add(
2036
+ this.module.global.get(POINTER_GLOBALS.eventPayload, binaryen.i32),
2037
+ offset(),
2038
+ );
2039
+ }
2040
+
2041
+ storeSlicePlace(place, components, context) {
2042
+ if (place.base.kind !== "local" || place.projections.length !== 0) {
2043
+ this.fail("slice assignment destination must be an unprojected local");
2044
+ }
2045
+ const layout = context.localLayouts[place.base.data];
2046
+ if (!layout || layout.kind !== "slice" || components.length !== 3) {
2047
+ this.fail(`local id ${place.base.data} is not a valid slice destination`);
2048
+ }
2049
+ return this.module.block(
2050
+ null,
2051
+ components.map((component, offset) =>
2052
+ this.module.local.set(layout.index + offset, component),
2053
+ ),
2054
+ );
2055
+ }
2056
+
2057
+ compileMakeSlice(data, context) {
2058
+ const source = () => this.compileSliceSource(data.source, context);
2059
+ const range = this.compileSliceRange(
2060
+ () => this.compileValue(data.start, context),
2061
+ () => this.compileValue(data.len, context),
2062
+ () => source()[1],
2063
+ data.bounds,
2064
+ );
2065
+ return [
2066
+ this.module.i32.add(
2067
+ source()[0],
2068
+ this.module.i32.mul(
2069
+ range.start(),
2070
+ source()[2],
2071
+ ),
2072
+ ),
2073
+ range.len(),
2074
+ source()[2],
2075
+ ];
2076
+ }
2077
+
2078
+ compileSliceRange(start, len, sourceLen, bounds) {
2079
+ const zero = () => this.module.i32.const(0);
2080
+ if (bounds === "unchecked") {
2081
+ return { start, len };
2082
+ }
2083
+ if (bounds === "clamp") {
2084
+ const normalizedStart = () => {
2085
+ const low = () =>
2086
+ this.module.select(
2087
+ this.module.i32.lt_s(start(), zero()),
2088
+ zero(),
2089
+ start(),
2090
+ );
2091
+ return this.module.select(
2092
+ this.module.i32.gt_s(low(), sourceLen()),
2093
+ sourceLen(),
2094
+ low(),
2095
+ );
2096
+ };
2097
+ const normalizedLen = () => {
2098
+ const low = () =>
2099
+ this.module.select(
2100
+ this.module.i32.lt_s(len(), zero()),
2101
+ zero(),
2102
+ len(),
2103
+ );
2104
+ const remaining = () =>
2105
+ this.module.i32.sub(sourceLen(), normalizedStart());
2106
+ return this.module.select(
2107
+ this.module.i32.gt_s(low(), remaining()),
2108
+ remaining(),
2109
+ low(),
2110
+ );
2111
+ };
2112
+ return { start: normalizedStart, len: normalizedLen };
2113
+ }
2114
+ if (bounds === "trap") {
2115
+ const invalid = () => {
2116
+ const remaining = () => this.module.i32.sub(sourceLen(), start());
2117
+ return this.module.i32.or(
2118
+ this.module.i32.or(
2119
+ this.module.i32.lt_s(start(), zero()),
2120
+ this.module.i32.gt_s(start(), sourceLen()),
2121
+ ),
2122
+ this.module.i32.or(
2123
+ this.module.i32.lt_s(len(), zero()),
2124
+ this.module.i32.gt_s(len(), remaining()),
2125
+ ),
2126
+ );
2127
+ };
2128
+ return {
2129
+ start: () =>
2130
+ this.module.if(invalid(), this.module.unreachable(), start()),
2131
+ len,
2132
+ };
2133
+ }
2134
+ this.fail(`unknown bounds mode '${String(bounds)}'`);
2135
+ }
2136
+
2137
+ compileSliceSource(source, context) {
2138
+ if (source.kind === "place") {
2139
+ const typeId = this.placeTypeId(source.data, context);
2140
+ const type = this.type(typeId);
2141
+ if (type.kind === "slice") {
2142
+ return this.loadSlicePlace(source.data, context);
2143
+ }
2144
+ if (type.kind !== "array") {
2145
+ this.fail(`slice place source has unsupported type '${type.kind}'`);
2146
+ }
2147
+ const element = this.type(type.data.element);
2148
+ if (element.kind !== "scalar") {
2149
+ this.fail("slice array source must have primitive elements");
2150
+ }
2151
+ return [
2152
+ this.placeAddress(source.data, context),
2153
+ this.module.i32.const(type.data.len),
2154
+ this.module.i32.const(this.scalarSize(element.data)),
2155
+ ];
2156
+ }
2157
+ if (source.kind === "const_data") {
2158
+ const item = this.constLayout[source.data];
2159
+ if (!item) this.fail(`const data id ${source.data} is out of range`);
2160
+ return [
2161
+ this.module.i32.const(item.address),
2162
+ this.module.i32.const(item.len),
2163
+ this.module.i32.const(this.scalarSize(item.scalar)),
2164
+ ];
2165
+ }
2166
+ if (source.kind === "buffer") {
2167
+ const buffer = this.requireBuffer(source.data.buffer);
2168
+ const elementSize = this.scalarSize(buffer.element);
2169
+ const address = this.loadBufferTableValue(
2170
+ POINTER_GLOBALS.buffers,
2171
+ source.data.buffer,
2172
+ "i32",
2173
+ );
2174
+ if (source.data.channel === null) {
2175
+ return [
2176
+ address,
2177
+ this.compileBufferLen(source.data.buffer),
2178
+ this.module.i32.const(elementSize),
2179
+ ];
2180
+ }
2181
+ const channels = () =>
2182
+ this.loadBufferTableValue(
2183
+ POINTER_GLOBALS.bufferChannels,
2184
+ source.data.buffer,
2185
+ "i32",
2186
+ );
2187
+ const channel = this.compileDynamicBoundedIndex(
2188
+ () => this.compileValue(source.data.channel, context),
2189
+ channels,
2190
+ "clamp",
2191
+ true,
2192
+ );
2193
+ return [
2194
+ this.module.i32.add(
2195
+ address,
2196
+ this.module.i32.mul(channel, this.module.i32.const(elementSize)),
2197
+ ),
2198
+ this.loadBufferTableValue(
2199
+ POINTER_GLOBALS.bufferFrames,
2200
+ source.data.buffer,
2201
+ "i32",
2202
+ ),
2203
+ this.module.i32.mul(channels(), this.module.i32.const(elementSize)),
2204
+ ];
2205
+ }
2206
+ if (source.kind === "buffer_param") {
2207
+ const type = this.bufferParamType(source.data.parameter, context);
2208
+ const elementSize = this.scalarSize(type.data.element);
2209
+ const address = () =>
2210
+ this.loadBufferParamComponent(source.data.parameter, 0, "i32", context);
2211
+ if (source.data.channel === null) {
2212
+ return [
2213
+ address(),
2214
+ this.compileBufferParamLen(source.data.parameter, context),
2215
+ this.module.i32.const(elementSize),
2216
+ ];
2217
+ }
2218
+ const channels = () =>
2219
+ this.loadBufferParamComponent(source.data.parameter, 2, "i32", context);
2220
+ const channel = this.compileDynamicBoundedIndex(
2221
+ () => this.compileValue(source.data.channel, context),
2222
+ channels,
2223
+ "clamp",
2224
+ true,
2225
+ );
2226
+ return [
2227
+ this.module.i32.add(
2228
+ address(),
2229
+ this.module.i32.mul(channel, this.module.i32.const(elementSize)),
2230
+ ),
2231
+ this.loadBufferParamComponent(source.data.parameter, 1, "i32", context),
2232
+ this.module.i32.mul(channels(), this.module.i32.const(elementSize)),
2233
+ ];
2234
+ }
2235
+ this.fail(`unsupported slice source '${String(source.kind)}'`);
2236
+ }
2237
+
2238
+ sliceElementScalar(value, context) {
2239
+ if (value.kind !== "local") this.fail("slice value is not a local");
2240
+ const local = context.function.locals[value.data];
2241
+ const type = local && this.type(local.ty);
2242
+ if (!type || type.kind !== "slice") {
2243
+ this.fail(`local id ${value.data} is not slice-typed`);
2244
+ }
2245
+ return type.data.element;
2246
+ }
2247
+
2248
+ sliceAccess(value, context) {
2249
+ if (value.kind !== "local") this.fail("slice value is not a local");
2250
+ const local = context.function.locals[value.data];
2251
+ const type = local && this.type(local.ty);
2252
+ if (!type || type.kind !== "slice") {
2253
+ this.fail(`local id ${value.data} is not slice-typed`);
2254
+ }
2255
+ return type.data.access;
2256
+ }
2257
+
2258
+ compileSliceAddress(slice, index, bounds, context) {
2259
+ return this.compileSliceAddressWithFactories(
2260
+ () => this.compileSliceValue(slice, context),
2261
+ () => this.compileValue(index, context),
2262
+ bounds,
2263
+ );
2264
+ }
2265
+
2266
+ compileSliceAddressWithFactories(slice, index, bounds) {
2267
+ const bounded = this.compileDynamicBoundedIndex(
2268
+ index,
2269
+ () => slice()[1],
2270
+ bounds,
2271
+ );
2272
+ return this.module.i32.add(
2273
+ slice()[0],
2274
+ this.module.i32.mul(bounded, slice()[2]),
2275
+ );
2276
+ }
2277
+
2278
+ compileArrayWindowAddress(data, parameterType, context) {
2279
+ const sourceTypeId = this.placeTypeId(data.array, context);
2280
+ const sourceType = this.type(sourceTypeId);
2281
+ if (sourceType.kind !== "array") {
2282
+ this.fail("array-window source is not a fixed array");
2283
+ }
2284
+ if (
2285
+ !this.typesEquivalent(sourceType.data.element, parameterType.data.element) ||
2286
+ sourceType.data.len < parameterType.data.len
2287
+ ) {
2288
+ this.fail("array-window source does not contain the required parameter shape");
2289
+ }
2290
+ const elementSize = this.typeLayout(parameterType.data.element).size;
2291
+ const start = this.compileWindowStart(
2292
+ () => this.compileValue(data.start, context),
2293
+ () =>
2294
+ this.module.i32.const(
2295
+ sourceType.data.len - parameterType.data.len,
2296
+ ),
2297
+ data.bounds,
2298
+ );
2299
+ return this.module.i32.add(
2300
+ this.placeAddress(data.array, context),
2301
+ this.module.i32.mul(start(), this.module.i32.const(elementSize)),
2302
+ );
2303
+ }
2304
+
2305
+ compileSliceWindowAddress(data, parameterType, context) {
2306
+ const elementType = this.type(parameterType.data.element);
2307
+ if (elementType.kind !== "scalar") {
2308
+ this.fail("slice-window fixed-array parameter element is not scalar");
2309
+ }
2310
+ const slice = () => this.compileSliceValue(data.slice, context);
2311
+ const elementSize = this.scalarSize(elementType.data);
2312
+ const requiredLen = parameterType.data.len;
2313
+ const start = this.compileWindowStart(
2314
+ () => this.compileValue(data.start, context),
2315
+ () =>
2316
+ this.module.i32.sub(
2317
+ slice()[1],
2318
+ this.module.i32.const(requiredLen),
2319
+ ),
2320
+ data.bounds,
2321
+ );
2322
+ const address = () =>
2323
+ this.module.i32.add(
2324
+ slice()[0],
2325
+ this.module.i32.mul(start(), slice()[2]),
2326
+ );
2327
+ if (data.bounds === "unchecked") {
2328
+ return address();
2329
+ }
2330
+ const invalidShape = () =>
2331
+ this.module.i32.or(
2332
+ this.module.i32.ne(
2333
+ slice()[2],
2334
+ this.module.i32.const(elementSize),
2335
+ ),
2336
+ this.module.i32.lt_s(
2337
+ slice()[1],
2338
+ this.module.i32.const(requiredLen),
2339
+ ),
2340
+ );
2341
+ return this.module.if(
2342
+ invalidShape(),
2343
+ this.module.unreachable(),
2344
+ address(),
2345
+ );
2346
+ }
2347
+
2348
+ compileWindowStart(start, maximum, bounds) {
2349
+ const zero = () => this.module.i32.const(0);
2350
+ if (bounds === "unchecked") {
2351
+ return start;
2352
+ }
2353
+ if (bounds === "clamp") {
2354
+ return () => {
2355
+ const low = () =>
2356
+ this.module.select(
2357
+ this.module.i32.lt_s(start(), zero()),
2358
+ zero(),
2359
+ start(),
2360
+ );
2361
+ return this.module.select(
2362
+ this.module.i32.gt_s(low(), maximum()),
2363
+ maximum(),
2364
+ low(),
2365
+ );
2366
+ };
2367
+ }
2368
+ if (bounds === "trap") {
2369
+ return () =>
2370
+ this.module.if(
2371
+ this.module.i32.or(
2372
+ this.module.i32.lt_s(start(), zero()),
2373
+ this.module.i32.gt_s(start(), maximum()),
2374
+ ),
2375
+ this.module.unreachable(),
2376
+ start(),
2377
+ );
2378
+ }
2379
+ this.fail(`unknown bounds mode '${String(bounds)}'`);
2380
+ }
2381
+
2382
+ compileSliceLoad(data, context) {
2383
+ const scalar = this.sliceElementScalar(data.slice, context);
2384
+ return this.loadScalar(
2385
+ scalar,
2386
+ this.compileSliceAddress(data.slice, data.index, data.bounds, context),
2387
+ );
2388
+ }
2389
+
2390
+ compileSliceStore(data, context) {
2391
+ if (this.sliceAccess(data.slice, context) !== "read_write") {
2392
+ this.fail("slice store destination is read-only");
2393
+ }
2394
+ const scalar = this.sliceElementScalar(data.slice, context);
2395
+ return this.storeScalar(
2396
+ scalar,
2397
+ this.compileSliceAddress(data.slice, data.index, data.bounds, context),
2398
+ this.compileValue(data.value, context),
2399
+ );
2400
+ }
2401
+
2402
+ compileSliceFill(statement, data, context) {
2403
+ if (this.sliceAccess(data.destination, context) !== "read_write") {
2404
+ this.fail("slice fill destination is read-only");
2405
+ }
2406
+ const scratch = context.sliceScratch.get(statement);
2407
+ if (!scratch || scratch.count !== 1) {
2408
+ this.fail("internal slice-fill scratch local is missing");
2409
+ }
2410
+ const counter = scratch.index;
2411
+ const id = this.nextLabel++;
2412
+ const vectorLoopLabel = `$onda.slice.fill.vector.${id}`;
2413
+ const scalarLoopLabel = `$onda.slice.fill.scalar.${id}`;
2414
+ const destination = () => this.compileSliceValue(data.destination, context);
2415
+ const counterValue = () => this.module.local.get(counter, binaryen.i32);
2416
+ const scalar = this.sliceElementScalar(data.destination, context);
2417
+ const scalarSize = this.scalarSize(scalar);
2418
+ const address = () =>
2419
+ this.module.i32.add(
2420
+ destination()[0],
2421
+ this.module.i32.mul(counterValue(), destination()[2]),
2422
+ );
2423
+ const scalarLoop = () =>
2424
+ this.module.loop(
2425
+ scalarLoopLabel,
2426
+ this.module.if(
2427
+ this.module.i32.lt_s(counterValue(), destination()[1]),
2428
+ this.module.block(null, [
2429
+ this.storeScalar(
2430
+ scalar,
2431
+ address(),
2432
+ this.compileValue(data.value, context),
2433
+ ),
2434
+ this.module.local.set(
2435
+ counter,
2436
+ this.module.i32.add(counterValue(), this.module.i32.const(1)),
2437
+ ),
2438
+ this.module.br(scalarLoopLabel),
2439
+ ]),
2440
+ ),
2441
+ );
2442
+ const body = [];
2443
+ if (this.options.simd) {
2444
+ const lanes = 16 / scalarSize;
2445
+ const vectorCondition = this.module.i32.and(
2446
+ this.module.i32.eq(
2447
+ destination()[2],
2448
+ this.module.i32.const(scalarSize),
2449
+ ),
2450
+ this.module.i32.and(
2451
+ this.module.i32.ge_u(
2452
+ destination()[1],
2453
+ this.module.i32.const(lanes),
2454
+ ),
2455
+ this.module.i32.le_u(
2456
+ counterValue(),
2457
+ this.module.i32.sub(
2458
+ destination()[1],
2459
+ this.module.i32.const(lanes),
2460
+ ),
2461
+ ),
2462
+ ),
2463
+ );
2464
+ body.push(
2465
+ this.module.loop(
2466
+ vectorLoopLabel,
2467
+ this.module.if(
2468
+ vectorCondition,
2469
+ this.module.block(null, [
2470
+ this.module.v128.store(
2471
+ 0,
2472
+ scalarSize,
2473
+ address(),
2474
+ this.compileVectorSplat(
2475
+ scalar,
2476
+ this.compileValue(data.value, context),
2477
+ ),
2478
+ ),
2479
+ this.module.local.set(
2480
+ counter,
2481
+ this.module.i32.add(
2482
+ counterValue(),
2483
+ this.module.i32.const(lanes),
2484
+ ),
2485
+ ),
2486
+ this.module.br(vectorLoopLabel),
2487
+ ]),
2488
+ ),
2489
+ ),
2490
+ );
2491
+ }
2492
+ body.push(scalarLoop());
2493
+ return this.module.block(null, [
2494
+ this.module.local.set(counter, this.module.i32.const(0)),
2495
+ ...body,
2496
+ ]);
2497
+ }
2498
+
2499
+ compileSliceCopy(statement, data, context) {
2500
+ if (this.sliceAccess(data.destination, context) !== "read_write") {
2501
+ this.fail("slice copy destination is read-only");
2502
+ }
2503
+ const scratch = context.sliceScratch.get(statement);
2504
+ if (!scratch || scratch.count !== 2) {
2505
+ this.fail("internal slice-copy scratch locals are missing");
2506
+ }
2507
+ const count = scratch.index;
2508
+ const counter = scratch.index + 1;
2509
+ const id = this.nextLabel++;
2510
+ const loopLabel = `$onda.slice.copy.${id}`;
2511
+ const destination = () => this.compileSliceValue(data.destination, context);
2512
+ const source = () => this.compileSliceValue(data.source, context);
2513
+ const countValue = () => this.module.local.get(count, binaryen.i32);
2514
+ const counterValue = () => this.module.local.get(counter, binaryen.i32);
2515
+ const copyIndex = () =>
2516
+ this.module.select(
2517
+ this.module.i32.and(
2518
+ this.module.i32.eq(destination()[2], source()[2]),
2519
+ this.module.i32.gt_u(destination()[0], source()[0]),
2520
+ ),
2521
+ this.module.i32.sub(
2522
+ this.module.i32.sub(countValue(), this.module.i32.const(1)),
2523
+ counterValue(),
2524
+ ),
2525
+ counterValue(),
2526
+ );
2527
+ const sourceAddress = () =>
2528
+ this.module.i32.add(
2529
+ source()[0],
2530
+ this.module.i32.mul(copyIndex(), source()[2]),
2531
+ );
2532
+ const destinationAddress = () =>
2533
+ this.module.i32.add(
2534
+ destination()[0],
2535
+ this.module.i32.mul(copyIndex(), destination()[2]),
2536
+ );
2537
+ const sourceScalar = this.sliceElementScalar(data.source, context);
2538
+ const destinationScalar = this.sliceElementScalar(data.destination, context);
2539
+ const copiedValue = () => {
2540
+ const loaded = this.loadScalar(sourceScalar, sourceAddress());
2541
+ return sourceScalar === destinationScalar
2542
+ ? loaded
2543
+ : this.compileCast(sourceScalar, destinationScalar, loaded);
2544
+ };
2545
+ const nonEmpty = () =>
2546
+ this.module.i32.gt_s(countValue(), this.module.i32.const(0));
2547
+ const sourceEnd = () =>
2548
+ this.module.i32.add(
2549
+ this.module.i32.add(
2550
+ source()[0],
2551
+ this.module.i32.mul(
2552
+ this.module.i32.sub(countValue(), this.module.i32.const(1)),
2553
+ source()[2],
2554
+ ),
2555
+ ),
2556
+ this.module.i32.const(this.scalarSize(sourceScalar)),
2557
+ );
2558
+ const destinationEnd = () =>
2559
+ this.module.i32.add(
2560
+ this.module.i32.add(
2561
+ destination()[0],
2562
+ this.module.i32.mul(
2563
+ this.module.i32.sub(countValue(), this.module.i32.const(1)),
2564
+ destination()[2],
2565
+ ),
2566
+ ),
2567
+ this.module.i32.const(this.scalarSize(destinationScalar)),
2568
+ );
2569
+ const overlaps = () =>
2570
+ this.module.i32.and(
2571
+ nonEmpty(),
2572
+ this.module.i32.and(
2573
+ this.module.i32.lt_u(destination()[0], sourceEnd()),
2574
+ this.module.i32.lt_u(source()[0], destinationEnd()),
2575
+ ),
2576
+ );
2577
+ const invalidOverlap = () =>
2578
+ this.module.i32.and(
2579
+ this.module.i32.ne(destination()[2], source()[2]),
2580
+ overlaps(),
2581
+ );
2582
+ const scalarCopy = () =>
2583
+ this.module.loop(
2584
+ loopLabel,
2585
+ this.module.if(
2586
+ this.module.i32.lt_s(counterValue(), countValue()),
2587
+ this.module.block(null, [
2588
+ this.storeScalar(destinationScalar, destinationAddress(), copiedValue()),
2589
+ this.module.local.set(
2590
+ counter,
2591
+ this.module.i32.add(counterValue(), this.module.i32.const(1)),
2592
+ ),
2593
+ this.module.br(loopLabel),
2594
+ ]),
2595
+ ),
2596
+ );
2597
+ const sameRepresentation = sourceScalar === destinationScalar;
2598
+ const copy = sameRepresentation
2599
+ ? this.module.if(
2600
+ this.module.i32.and(
2601
+ this.module.i32.eq(
2602
+ destination()[2],
2603
+ this.module.i32.const(this.scalarSize(destinationScalar)),
2604
+ ),
2605
+ this.module.i32.eq(
2606
+ source()[2],
2607
+ this.module.i32.const(this.scalarSize(sourceScalar)),
2608
+ ),
2609
+ ),
2610
+ // memory.copy has memmove overlap semantics and lets engines use
2611
+ // their tuned bulk-memory implementation for contiguous slices.
2612
+ this.module.memory.copy(
2613
+ destination()[0],
2614
+ source()[0],
2615
+ this.module.i32.mul(
2616
+ countValue(),
2617
+ this.module.i32.const(this.scalarSize(sourceScalar)),
2618
+ ),
2619
+ ),
2620
+ scalarCopy(),
2621
+ )
2622
+ : scalarCopy();
2623
+ return this.module.block(null, [
2624
+ this.module.local.set(
2625
+ count,
2626
+ this.module.select(
2627
+ this.module.i32.lt_s(destination()[1], source()[1]),
2628
+ destination()[1],
2629
+ source()[1],
2630
+ ),
2631
+ ),
2632
+ this.module.if(invalidOverlap(), this.module.unreachable()),
2633
+ this.module.local.set(counter, this.module.i32.const(0)),
2634
+ copy,
2635
+ ]);
2636
+ }
2637
+
2638
+ compileVectorSplat(scalar, value) {
2639
+ switch (scalar) {
2640
+ case "bool": return this.module.i8x16.splat(value);
2641
+ case "i32": return this.module.i32x4.splat(value);
2642
+ case "i64": return this.module.i64x2.splat(value);
2643
+ case "f32": return this.module.f32x4.splat(value);
2644
+ case "f64": return this.module.f64x2.splat(value);
2645
+ default: this.fail(`unknown SIMD scalar type '${String(scalar)}'`);
2646
+ }
2647
+ }
2648
+
2649
+ compileValue(value, context) {
2650
+ switch (value.kind) {
2651
+ case "local": {
2652
+ const scalar = context.localScalars[value.data];
2653
+ if (!scalar) {
2654
+ this.fail(`local id ${value.data} is not a scalar or is out of range`);
2655
+ }
2656
+ return this.module.local.get(
2657
+ this.localIndex(value.data, context),
2658
+ this.wasmType(scalar),
2659
+ );
2660
+ }
2661
+ case "constant":
2662
+ return this.compileConstant(value.data);
2663
+ default:
2664
+ this.fail(`unknown MIR value '${String(value.kind)}'`);
2665
+ }
2666
+ }
2667
+
2668
+ compileConstant(value) {
2669
+ switch (value.type) {
2670
+ case "f32":
2671
+ return this.module.f32.const(decodeFloatLiteral(value.value, "f32", this));
2672
+ case "f64":
2673
+ return this.module.f64.const(decodeFloatLiteral(value.value, "f64", this));
2674
+ case "i32":
2675
+ return this.module.i32.const(value.value);
2676
+ case "i64":
2677
+ return this.module.i64.const(decodeI64Literal(value.value, this));
2678
+ case "bool":
2679
+ return this.module.i32.const(value.value ? 1 : 0);
2680
+ default:
2681
+ this.fail(`unknown scalar constant type '${String(value.type)}'`);
2682
+ }
2683
+ }
2684
+
2685
+ valueScalarType(value, context) {
2686
+ if (value.kind === "constant") {
2687
+ return value.data.type;
2688
+ }
2689
+ if (value.kind === "local") {
2690
+ const scalar = context.localScalars[value.data];
2691
+ if (!scalar) {
2692
+ this.fail(`local id ${value.data} is not a scalar or is out of range`);
2693
+ }
2694
+ return scalar;
2695
+ }
2696
+ this.fail(`unknown MIR value '${String(value.kind)}'`);
2697
+ }
2698
+
2699
+ placeScalarType(place, context) {
2700
+ const typeId = this.placeTypeId(place, context);
2701
+ return this.requireScalarType(typeId, "place");
2702
+ }
2703
+
2704
+ placeTypeId(place, context) {
2705
+ let typeId;
2706
+ switch (place.base.kind) {
2707
+ case "local":
2708
+ typeId = context.function.locals[place.base.data]?.ty;
2709
+ break;
2710
+ case "parameter":
2711
+ typeId = context.function.params[place.base.data]?.ty;
2712
+ break;
2713
+ case "state":
2714
+ typeId = this.mir.state[place.base.data]?.ty;
2715
+ break;
2716
+ case "param":
2717
+ typeId = this.mir.interface.params[place.base.data]?.ty;
2718
+ break;
2719
+ case "event_param": {
2720
+ const event = this.mir.interface.events[context.eventId];
2721
+ typeId = event?.params[place.base.data]?.ty;
2722
+ break;
2723
+ }
2724
+ default:
2725
+ this.fail(`place base '${place.base.kind}' is not supported yet`);
2726
+ }
2727
+ if (!Number.isInteger(typeId)) {
2728
+ this.fail(`place base '${place.base.kind}' id ${place.base.data} is out of range`);
2729
+ }
2730
+ for (const projection of place.projections) {
2731
+ const type = this.type(typeId);
2732
+ if (projection.kind === "index" && type.kind === "array") {
2733
+ typeId = type.data.element;
2734
+ } else {
2735
+ this.fail(`projection '${projection.kind}' on '${type.kind}' is not supported yet`);
2736
+ }
2737
+ }
2738
+ return typeId;
2739
+ }
2740
+
2741
+ loadPlace(place, context) {
2742
+ if (place.base.kind === "local" && place.projections.length === 0) {
2743
+ const scalar = this.placeScalarType(place, context);
2744
+ return this.module.local.get(
2745
+ this.localIndex(place.base.data, context),
2746
+ this.wasmType(scalar),
2747
+ );
2748
+ }
2749
+ if (place.base.kind === "parameter" && place.projections.length === 0) {
2750
+ const scalar = this.placeScalarType(place, context);
2751
+ const layout = context.paramLayouts[place.base.data];
2752
+ if (!layout || !["scalar", "scalar_ref"].includes(layout.kind)) {
2753
+ this.fail(`parameter id ${place.base.data} is not a scalar`);
2754
+ }
2755
+ if (layout.kind === "scalar_ref") {
2756
+ return this.loadScalar(
2757
+ scalar,
2758
+ this.module.local.get(layout.index, binaryen.i32),
2759
+ );
2760
+ }
2761
+ return this.module.local.get(layout.index, this.wasmType(scalar));
2762
+ }
2763
+ const scalar = this.placeScalarType(place, context);
2764
+ return this.loadScalar(scalar, this.placeAddress(place, context));
2765
+ }
2766
+
2767
+ storePlace(place, value, scalar, context) {
2768
+ if (place.base.kind === "local" && place.projections.length === 0) {
2769
+ return this.module.local.set(this.localIndex(place.base.data, context), value);
2770
+ }
2771
+ if (place.base.kind === "parameter" && place.projections.length === 0) {
2772
+ const layout = context.paramLayouts[place.base.data];
2773
+ if (layout?.kind !== "scalar_ref") {
2774
+ this.fail("assignment to a by-value function parameter is not supported");
2775
+ }
2776
+ return this.storeScalar(
2777
+ scalar,
2778
+ this.module.local.get(layout.index, binaryen.i32),
2779
+ value,
2780
+ );
2781
+ }
2782
+ return this.storeScalar(scalar, this.placeAddress(place, context), value);
2783
+ }
2784
+
2785
+ placeAddress(place, context) {
2786
+ let typeId;
2787
+ let address;
2788
+ switch (place.base.kind) {
2789
+ case "parameter": {
2790
+ const layout = context.paramLayouts[place.base.data];
2791
+ if (!layout || !["scalar_ref", "array_ref"].includes(layout.kind)) {
2792
+ this.fail(`parameter id ${place.base.data} is not an addressable reference`);
2793
+ }
2794
+ typeId = context.function.params[place.base.data].ty;
2795
+ address = this.module.local.get(layout.index, binaryen.i32);
2796
+ break;
2797
+ }
2798
+ case "local": {
2799
+ const layout =
2800
+ this.localArrayLayout[context.functionId]?.[place.base.data]
2801
+ ?? this.localScalarRefLayout[context.functionId]?.[place.base.data];
2802
+ if (!layout) {
2803
+ this.fail(`local id ${place.base.data} is not addressable`);
2804
+ }
2805
+ typeId = context.function.locals[place.base.data].ty;
2806
+ address = this.module.i32.const(layout.address);
2807
+ break;
2808
+ }
2809
+ case "state": {
2810
+ const layout = this.stateLayout[place.base.data];
2811
+ if (!layout) this.fail(`state id ${place.base.data} is out of range`);
2812
+ typeId = this.mir.state[place.base.data].ty;
2813
+ address = this.module.i32.add(
2814
+ this.module.global.get(POINTER_GLOBALS.state, binaryen.i32),
2815
+ this.module.i32.const(layout.offset),
2816
+ );
2817
+ break;
2818
+ }
2819
+ case "param": {
2820
+ const layout = this.paramLayout[place.base.data];
2821
+ if (!layout) this.fail(`param id ${place.base.data} is out of range`);
2822
+ typeId = this.mir.interface.params[place.base.data].ty;
2823
+ address = this.module.i32.add(
2824
+ this.module.global.get(POINTER_GLOBALS.params, binaryen.i32),
2825
+ this.module.i32.const(layout.offset),
2826
+ );
2827
+ break;
2828
+ }
2829
+ case "event_param": {
2830
+ const event = this.mir.interface.events[context.eventId];
2831
+ const layout = this.eventLayout[context.eventId]?.[place.base.data];
2832
+ if (!event || !layout) {
2833
+ this.fail(
2834
+ `event parameter id ${place.base.data} is invalid for function '${context.function.name}'`,
2835
+ );
2836
+ }
2837
+ typeId = event.params[place.base.data].ty;
2838
+ if (this.type(typeId).kind === "slice") {
2839
+ this.fail("slice event parameters require slice-value lowering");
2840
+ }
2841
+ address = this.compileEventParamAddress(context.eventId, place.base.data);
2842
+ break;
2843
+ }
2844
+ default:
2845
+ this.fail(
2846
+ `addressable place base '${place.base.kind}' is not in the first Binaryen slice`,
2847
+ );
2848
+ }
2849
+
2850
+ for (const projection of place.projections) {
2851
+ const type = this.type(typeId);
2852
+ if (projection.kind !== "index" || type.kind !== "array") {
2853
+ this.fail(`projection '${projection.kind}' on '${type.kind}' is not supported yet`);
2854
+ }
2855
+ const elementLayout = this.typeLayout(type.data.element);
2856
+ const index = this.compileBoundedIndex(
2857
+ projection.data.index,
2858
+ type.data.len,
2859
+ projection.data.bounds,
2860
+ context,
2861
+ );
2862
+ address = this.module.i32.add(
2863
+ address,
2864
+ this.module.i32.mul(index, this.module.i32.const(elementLayout.size)),
2865
+ );
2866
+ typeId = type.data.element;
2867
+ }
2868
+ return address;
2869
+ }
2870
+
2871
+ compileBoundedIndex(value, length, bounds, context) {
2872
+ if (!Number.isInteger(length) || length <= 0) {
2873
+ this.fail("array and port lengths must be positive integers");
2874
+ }
2875
+ if (bounds === "unchecked") {
2876
+ return this.compileValue(value, context);
2877
+ }
2878
+ if (bounds === "clamp") {
2879
+ return this.module.select(
2880
+ this.module.i32.lt_s(
2881
+ this.compileValue(value, context),
2882
+ this.module.i32.const(0),
2883
+ ),
2884
+ this.module.i32.const(0),
2885
+ this.module.select(
2886
+ this.module.i32.ge_s(
2887
+ this.compileValue(value, context),
2888
+ this.module.i32.const(length),
2889
+ ),
2890
+ this.module.i32.const(length - 1),
2891
+ this.compileValue(value, context),
2892
+ ),
2893
+ );
2894
+ }
2895
+ if (bounds === "trap") {
2896
+ const outOfBounds = this.module.i32.or(
2897
+ this.module.i32.lt_s(
2898
+ this.compileValue(value, context),
2899
+ this.module.i32.const(0),
2900
+ ),
2901
+ this.module.i32.ge_s(
2902
+ this.compileValue(value, context),
2903
+ this.module.i32.const(length),
2904
+ ),
2905
+ );
2906
+ return this.module.if(
2907
+ outOfBounds,
2908
+ this.module.unreachable(),
2909
+ this.compileValue(value, context),
2910
+ );
2911
+ }
2912
+ this.fail(`unknown bounds mode '${String(bounds)}'`);
2913
+ }
2914
+
2915
+ compileDynamicBoundedIndex(index, length, bounds, clampLengthKnownPositive = false) {
2916
+ if (bounds === "unchecked") {
2917
+ return index();
2918
+ }
2919
+ if (bounds === "clamp") {
2920
+ const maximum = () =>
2921
+ this.module.i32.sub(length(), this.module.i32.const(1));
2922
+ const clamped = () =>
2923
+ this.module.select(
2924
+ this.module.i32.lt_s(index(), this.module.i32.const(0)),
2925
+ this.module.i32.const(0),
2926
+ this.module.select(
2927
+ this.module.i32.gt_s(index(), maximum()),
2928
+ maximum(),
2929
+ index(),
2930
+ ),
2931
+ );
2932
+ if (clampLengthKnownPositive) return clamped();
2933
+ return this.module.if(
2934
+ this.module.i32.le_s(length(), this.module.i32.const(0)),
2935
+ this.module.unreachable(),
2936
+ clamped(),
2937
+ );
2938
+ }
2939
+ if (bounds === "trap") {
2940
+ return this.module.if(
2941
+ this.module.i32.or(
2942
+ this.module.i32.lt_s(index(), this.module.i32.const(0)),
2943
+ this.module.i32.ge_s(index(), length()),
2944
+ ),
2945
+ this.module.unreachable(),
2946
+ index(),
2947
+ );
2948
+ }
2949
+ this.fail(`unknown bounds mode '${String(bounds)}'`);
2950
+ }
2951
+
2952
+ compileUnary(op, scalar, value) {
2953
+ if (!supportsMirOperation("unary", op, scalar)) {
2954
+ this.fail(`unary operation '${String(op)}' does not support scalar '${scalar}'`);
2955
+ }
2956
+ switch (op) {
2957
+ case "negate":
2958
+ if (scalar === "f32" || scalar === "f64") return this.module[scalar].neg(value);
2959
+ return this.module[scalar].sub(this.zero(scalar), value);
2960
+ case "logical_not":
2961
+ return this.module.i32.eqz(value);
2962
+ case "bit_not":
2963
+ return this.module[scalar].xor(value, this.minusOne(scalar));
2964
+ default:
2965
+ this.fail(`unknown unary operation '${String(op)}'`);
2966
+ }
2967
+ }
2968
+
2969
+ compileBinary(op, scalar, lhs, rhs) {
2970
+ if (!supportsMirOperation("binary", op, scalar)) {
2971
+ this.fail(`binary operation '${String(op)}' does not support scalar '${scalar}'`);
2972
+ }
2973
+ const wasm = this.module[scalar === "bool" ? "i32" : scalar];
2974
+ const integer = scalar === "i32" || scalar === "i64" || scalar === "bool";
2975
+ switch (op) {
2976
+ case "add": return wasm.add(lhs(), rhs());
2977
+ case "subtract": return wasm.sub(lhs(), rhs());
2978
+ case "multiply": return wasm.mul(lhs(), rhs());
2979
+ case "divide": {
2980
+ if (!integer) return wasm.div(lhs(), rhs());
2981
+ const minimum = () =>
2982
+ scalar === "i64"
2983
+ ? this.module.i64.const(-(1n << 63n))
2984
+ : this.module.i32.const(-0x8000_0000);
2985
+ const negativeOne = () =>
2986
+ scalar === "i64"
2987
+ ? this.module.i64.const(-1n)
2988
+ : this.module.i32.const(-1);
2989
+ const overflow = this.module.i32.and(
2990
+ wasm.eq(lhs(), minimum()),
2991
+ wasm.eq(rhs(), negativeOne()),
2992
+ );
2993
+ return this.module.if(overflow, minimum(), wasm.div_s(lhs(), rhs()));
2994
+ }
2995
+ case "remainder":
2996
+ if (!integer) {
2997
+ return this.compileMathKernelCall("remainder", scalar, [lhs(), rhs()]);
2998
+ }
2999
+ return wasm.rem_s(lhs(), rhs());
3000
+ case "bit_and": return wasm.and(lhs(), rhs());
3001
+ case "bit_or": return wasm.or(lhs(), rhs());
3002
+ case "bit_xor": return wasm.xor(lhs(), rhs());
3003
+ case "shift_left": return wasm.shl(lhs(), rhs());
3004
+ case "shift_right": return wasm.shr_s(lhs(), rhs());
3005
+ default:
3006
+ this.fail(`unknown binary operation '${String(op)}'`);
3007
+ }
3008
+ }
3009
+
3010
+ compileCompare(op, scalar, lhs, rhs) {
3011
+ if (!supportsMirOperation("compare", op, scalar)) {
3012
+ this.fail(`comparison '${String(op)}' does not support scalar '${scalar}'`);
3013
+ }
3014
+ const type = scalar === "bool" ? "i32" : scalar;
3015
+ const wasm = this.module[type];
3016
+ const integer = type === "i32" || type === "i64";
3017
+ switch (op) {
3018
+ case "equal": return wasm.eq(lhs, rhs);
3019
+ case "not_equal": return wasm.ne(lhs, rhs);
3020
+ case "less": return integer ? wasm.lt_s(lhs, rhs) : wasm.lt(lhs, rhs);
3021
+ case "less_equal": return integer ? wasm.le_s(lhs, rhs) : wasm.le(lhs, rhs);
3022
+ case "greater": return integer ? wasm.gt_s(lhs, rhs) : wasm.gt(lhs, rhs);
3023
+ case "greater_equal": return integer ? wasm.ge_s(lhs, rhs) : wasm.ge(lhs, rhs);
3024
+ default:
3025
+ this.fail(`unknown comparison '${String(op)}'`);
3026
+ }
3027
+ }
3028
+
3029
+ compileCast(from, to, value) {
3030
+ const source = from === "bool" ? "i32" : from;
3031
+ const target = to === "bool" ? "i32" : to;
3032
+ if (source === target) return value;
3033
+ if (target === "bool") return this.module[source].ne(value, this.zero(source));
3034
+ if (source === "f32" && target === "f64") return this.module.f64.promote(value);
3035
+ if (source === "f64" && target === "f32") return this.module.f32.demote(value);
3036
+ if (source === "i32" && target === "i64") return this.module.i64.extend_s(value);
3037
+ if (source === "i64" && target === "i32") return this.module.i32.wrap(value);
3038
+ if ((source === "i32" || source === "i64") && target === "f32") {
3039
+ return this.module.f32.convert_s[source](value);
3040
+ }
3041
+ if ((source === "i32" || source === "i64") && target === "f64") {
3042
+ return this.module.f64.convert_s[source](value);
3043
+ }
3044
+ if ((source === "f32" || source === "f64") && target === "i32") {
3045
+ this.module.setFeatures(
3046
+ this.module.getFeatures() | binaryen.Features.NontrappingFPToInt,
3047
+ );
3048
+ return this.module.i32.trunc_s_sat[source](value);
3049
+ }
3050
+ if ((source === "f32" || source === "f64") && target === "i64") {
3051
+ this.module.setFeatures(
3052
+ this.module.getFeatures() | binaryen.Features.NontrappingFPToInt,
3053
+ );
3054
+ return this.module.i64.trunc_s_sat[source](value);
3055
+ }
3056
+ this.fail(`unsupported scalar cast from '${from}' to '${to}'`);
3057
+ }
3058
+
3059
+ compileIntrinsic(data, expectedScalar, context) {
3060
+ const scalar = data.args.length
3061
+ ? this.valueScalarType(data.args[0], context)
3062
+ : expectedScalar;
3063
+ const args = data.args.map((value) => this.compileValue(value, context));
3064
+ const isFloat = scalar === "f32" || scalar === "f64";
3065
+ const isInteger = scalar === "i32" || scalar === "i64";
3066
+ if (!isFloat && !isInteger) {
3067
+ this.fail(`intrinsic '${data.intrinsic}' requires numeric operands`);
3068
+ }
3069
+ const wasm = this.module[scalar];
3070
+
3071
+ if (isInteger) {
3072
+ // Binaryen expressions are tree nodes, so each use needs its own local
3073
+ // get/constant node even when the MIR value is the same.
3074
+ const arg = (index) => this.compileValue(data.args[index], context);
3075
+ switch (data.intrinsic) {
3076
+ case "abs":
3077
+ return this.module.select(
3078
+ wasm.lt_s(arg(0), this.zero(scalar)),
3079
+ wasm.sub(this.zero(scalar), arg(0)),
3080
+ arg(0),
3081
+ );
3082
+ case "min":
3083
+ return this.module.select(wasm.lt_s(arg(0), arg(1)), arg(0), arg(1));
3084
+ case "max":
3085
+ return this.module.select(wasm.gt_s(arg(0), arg(1)), arg(0), arg(1));
3086
+ default:
3087
+ this.fail(`intrinsic '${data.intrinsic}' requires f32 or f64 operands`);
3088
+ }
3089
+ }
3090
+
3091
+ switch (data.intrinsic) {
3092
+ case "sqrt": return wasm.sqrt(args[0]);
3093
+ case "abs": return wasm.abs(args[0]);
3094
+ case "floor": return wasm.floor(args[0]);
3095
+ case "ceil": return wasm.ceil(args[0]);
3096
+ case "trunc": return wasm.trunc(args[0]);
3097
+ case "min": return wasm.min(args[0], args[1]);
3098
+ case "max": return wasm.max(args[0], args[1]);
3099
+ case "fma": return this.compileMathKernelCall(data.intrinsic, scalar, args);
3100
+ case "sin":
3101
+ case "cos":
3102
+ case "tan":
3103
+ case "tanh":
3104
+ case "atan":
3105
+ case "atan2":
3106
+ case "exp":
3107
+ case "log":
3108
+ case "pow":
3109
+ return this.compileMathKernelCall(data.intrinsic, scalar, args);
3110
+ case "round":
3111
+ return this.compileRoundHelper(scalar, args);
3112
+ default:
3113
+ this.fail(`unknown intrinsic '${String(data.intrinsic)}'`);
3114
+ }
3115
+ }
3116
+
3117
+ compileMathKernelCall(intrinsic, scalar, args) {
3118
+ const name = `onda_math_${intrinsic}_${scalar}`;
3119
+ if (!this.requiredMathHelpers.has(name)) {
3120
+ this.fail(`math kernel was not reserved for helper '${name}'`);
3121
+ }
3122
+ return this.module.call(
3123
+ name,
3124
+ args,
3125
+ this.wasmType(scalar),
3126
+ );
3127
+ }
3128
+
3129
+ compileRoundHelper(scalar, args) {
3130
+ const name = `$onda.math.round.${scalar}`;
3131
+ if (!this.internalHelpers.has(name)) {
3132
+ const wasm = this.module[scalar];
3133
+ const get = () => this.module.local.get(0, this.wasmType(scalar));
3134
+ const trunc = () => wasm.trunc(get());
3135
+ const magnitude = wasm.abs(wasm.sub(get(), trunc()));
3136
+ const rounded = wasm.add(
3137
+ trunc(),
3138
+ wasm.copysign(wasm.const(1), get()),
3139
+ );
3140
+ this.module.addFunction(
3141
+ name,
3142
+ this.wasmType(scalar),
3143
+ this.wasmType(scalar),
3144
+ [],
3145
+ this.module.select(
3146
+ wasm.ge(magnitude, wasm.const(0.5)),
3147
+ rounded,
3148
+ trunc(),
3149
+ ),
3150
+ );
3151
+ this.internalHelpers.add(name);
3152
+ }
3153
+ return this.module.call(name, args, this.wasmType(scalar));
3154
+ }
3155
+
3156
+ loadScalar(scalar, address) {
3157
+ switch (scalar) {
3158
+ case "bool": return this.module.i32.load8_u(0, 1, address);
3159
+ case "i32": return this.module.i32.load(0, 4, address);
3160
+ case "i64": return this.module.i64.load(0, 8, address);
3161
+ case "f32": return this.module.f32.load(0, 4, address);
3162
+ case "f64": return this.module.f64.load(0, 8, address);
3163
+ default: this.fail(`unknown scalar type '${String(scalar)}'`);
3164
+ }
3165
+ }
3166
+
3167
+ storeScalar(scalar, address, value) {
3168
+ switch (scalar) {
3169
+ case "bool": return this.module.i32.store8(0, 1, address, value);
3170
+ case "i32": return this.module.i32.store(0, 4, address, value);
3171
+ case "i64": return this.module.i64.store(0, 8, address, value);
3172
+ case "f32": return this.module.f32.store(0, 4, address, value);
3173
+ case "f64": return this.module.f64.store(0, 8, address, value);
3174
+ default: this.fail(`unknown scalar type '${String(scalar)}'`);
3175
+ }
3176
+ }
3177
+
3178
+ type(typeId) {
3179
+ const type = this.mir.types[typeId];
3180
+ if (!type) this.fail(`type id ${typeId} is out of range`);
3181
+ return type;
3182
+ }
3183
+
3184
+ typesEquivalent(lhsId, rhsId, visiting = new Set()) {
3185
+ if (lhsId === rhsId) return true;
3186
+ const key = `${lhsId}:${rhsId}`;
3187
+ if (visiting.has(key)) return true;
3188
+ const lhs = this.mir.types[lhsId];
3189
+ const rhs = this.mir.types[rhsId];
3190
+ if (!lhs || !rhs || lhs.kind !== rhs.kind) return false;
3191
+ visiting.add(key);
3192
+ let equivalent = false;
3193
+ if (lhs.kind === "scalar") {
3194
+ equivalent = lhs.data === rhs.data;
3195
+ } else if (lhs.kind === "array") {
3196
+ equivalent =
3197
+ lhs.data.len === rhs.data.len &&
3198
+ this.typesEquivalent(lhs.data.element, rhs.data.element, visiting);
3199
+ } else if (lhs.kind === "slice") {
3200
+ equivalent =
3201
+ lhs.data.element === rhs.data.element &&
3202
+ lhs.data.access === rhs.data.access;
3203
+ } else if (lhs.kind === "buffer") {
3204
+ equivalent =
3205
+ lhs.data.element === rhs.data.element &&
3206
+ JSON.stringify(lhs.data.channels) === JSON.stringify(rhs.data.channels) &&
3207
+ lhs.data.access === rhs.data.access;
3208
+ } else if (lhs.kind === "tuple") {
3209
+ equivalent =
3210
+ lhs.data.length === rhs.data.length &&
3211
+ lhs.data.every((element, index) =>
3212
+ this.typesEquivalent(element, rhs.data[index], visiting),
3213
+ );
3214
+ } else if (lhs.kind === "struct") {
3215
+ equivalent = lhs.data === rhs.data;
3216
+ }
3217
+ visiting.delete(key);
3218
+ return equivalent;
3219
+ }
3220
+
3221
+ typeLayout(typeId) {
3222
+ const type = this.type(typeId);
3223
+ if (type.kind === "scalar") {
3224
+ const size = this.scalarSize(type.data);
3225
+ return { size, align: size, scalar: type.data };
3226
+ }
3227
+ if (type.kind === "array") {
3228
+ const element = this.typeLayout(type.data.element);
3229
+ if (!Number.isInteger(type.data.len) || type.data.len <= 0) {
3230
+ this.fail("fixed array length must be a positive integer");
3231
+ }
3232
+ return {
3233
+ size: element.size * type.data.len,
3234
+ align: element.align,
3235
+ scalar: element.scalar,
3236
+ };
3237
+ }
3238
+ this.fail(`storage layout for MIR type '${type.kind}' is not supported yet`);
3239
+ }
3240
+
3241
+ requireScalarType(typeId, description) {
3242
+ const type = this.type(typeId);
3243
+ if (type.kind !== "scalar") {
3244
+ this.fail(`${description} has unsupported non-scalar type '${type.kind}'`);
3245
+ }
3246
+ return type.data;
3247
+ }
3248
+
3249
+ scalarSize(scalar) {
3250
+ switch (scalar) {
3251
+ case "bool": return 1;
3252
+ case "i32":
3253
+ case "f32": return 4;
3254
+ case "i64":
3255
+ case "f64": return 8;
3256
+ default: this.fail(`unknown scalar type '${String(scalar)}'`);
3257
+ }
3258
+ }
3259
+
3260
+ requireWasm32Extent(byteLength, description) {
3261
+ if (
3262
+ !Number.isSafeInteger(byteLength)
3263
+ || byteLength < 0
3264
+ || byteLength >= WASM32_ADDRESS_SPACE_BYTES
3265
+ ) {
3266
+ this.fail(`${description} must fit within the wasm32 4 GiB address space`);
3267
+ }
3268
+ }
3269
+
3270
+ wasmType(scalar) {
3271
+ return binaryen[scalar === "bool" ? "i32" : scalar];
3272
+ }
3273
+
3274
+ wasmResultType(scalars) {
3275
+ if (scalars.length === 0) return binaryen.none;
3276
+ if (scalars.length === 1) return this.wasmType(scalars[0]);
3277
+ return binaryen.createType(scalars.map((scalar) => this.wasmType(scalar)));
3278
+ }
3279
+
3280
+ zero(scalar) {
3281
+ const type = scalar === "bool" ? "i32" : scalar;
3282
+ return type === "i64"
3283
+ ? this.module.i64.const(0n)
3284
+ : this.module[type].const(0);
3285
+ }
3286
+
3287
+ minusOne(scalar) {
3288
+ const type = scalar === "bool" ? "i32" : scalar;
3289
+ return type === "i64"
3290
+ ? this.module.i64.const(-1n)
3291
+ : this.module.i32.const(-1);
3292
+ }
3293
+
3294
+ localIndex(localId, context) {
3295
+ if (!Number.isInteger(localId) || localId < 0 || localId >= context.localScalars.length) {
3296
+ this.fail(`local id ${localId} is out of range`);
3297
+ }
3298
+ return context.localLayouts[localId].index;
3299
+ }
3300
+
3301
+ requireFunctionId(id, description) {
3302
+ if (!Number.isInteger(id) || id < 0 || id >= this.mir.functions.length) {
3303
+ this.fail(`${description} function id ${id} is out of range`);
3304
+ }
3305
+ }
3306
+
3307
+ requireBuffer(id) {
3308
+ if (!Number.isInteger(id) || id < 0 || id >= this.mir.interface.buffers.length) {
3309
+ this.fail(`buffer id ${id} is out of range`);
3310
+ }
3311
+ return this.mir.interface.buffers[id];
3312
+ }
3313
+
3314
+ currentLabel(labels, statement) {
3315
+ const label = labels.at(-1);
3316
+ if (!label) this.fail(`'${statement}' appears outside a MIR loop`);
3317
+ return label;
3318
+ }
3319
+
3320
+ buildMetadata() {
3321
+ const stateSize = this.stateLayout.byteLength ?? 0;
3322
+ const paramSize = this.paramLayout.byteLength ?? 0;
3323
+ const snapshot = this.stateSnapshotMetadata();
3324
+ const eventExports = this.mir.interface.events.map(
3325
+ (_, id) => `onda_event_${id}`,
3326
+ );
3327
+ const requiredExports = [
3328
+ "memory",
3329
+ "__heap_base",
3330
+ "onda_init",
3331
+ "onda_process",
3332
+ ...eventExports,
3333
+ ];
3334
+ const targetFeatures = ["bulk-memory"];
3335
+ if (this.options.simd) targetFeatures.push("simd128");
3336
+ return {
3337
+ format: PROCESSOR_ARTIFACT_FORMAT,
3338
+ format_version: PROCESSOR_ARTIFACT_FORMAT_VERSION,
3339
+ artifact_kind: "webassembly_module",
3340
+ abi_version: PROCESSOR_ABI_VERSION,
3341
+ backend: "binaryen-js",
3342
+ mir_schema_version: this.mir.schema_version,
3343
+ target: {
3344
+ triple: "wasm32-unknown-unknown",
3345
+ cpu: "generic",
3346
+ features: targetFeatures.map((feature) => `+${feature}`).join(","),
3347
+ reloc_model: "static",
3348
+ code_model: "default",
3349
+ opt_level: String(this.options.optimizeLevel),
3350
+ abi_name: null,
3351
+ data_layout: "e-m:e-p:32:32-i64:64-n32:64-S128",
3352
+ pointer_width_bits: 32,
3353
+ byte_order: "little_endian",
3354
+ pointer_model: "linear_memory_offset",
3355
+ calling_convention: "core_webassembly",
3356
+ },
3357
+ integration: {
3358
+ required_symbols: requiredExports,
3359
+ one_processor_per_artifact: true,
3360
+ profile: {
3361
+ kind: "core_webassembly_module",
3362
+ imports: [],
3363
+ memory_export: "memory",
3364
+ heap_base_export: "__heap_base",
3365
+ },
3366
+ },
3367
+ required_features: targetFeatures,
3368
+ optimization: {
3369
+ enabled: this.options.optimize,
3370
+ level: this.options.optimizeLevel,
3371
+ shrink_level: this.options.shrinkLevel,
3372
+ fast_math: this.options.fastMath,
3373
+ simd: this.options.simd,
3374
+ inline_functions_with_loops:
3375
+ this.options.allowInliningFunctionsWithLoops,
3376
+ },
3377
+ compile: {
3378
+ sample_rate: this.mir.config.sample_rate,
3379
+ block_size: this.mir.config.block_size,
3380
+ fast_math: this.options.fastMath,
3381
+ },
3382
+ exports: {
3383
+ memory: "memory",
3384
+ heap_base: "__heap_base",
3385
+ init: "onda_init",
3386
+ process: "onda_process",
3387
+ events: eventExports,
3388
+ },
3389
+ runtime: {
3390
+ state_size_bytes: stateSize,
3391
+ state_align_bytes: 16,
3392
+ state_initialization: "zeroed",
3393
+ snapshot_size_bytes: snapshot.byteLength,
3394
+ snapshot_format_version: PROCESSOR_SNAPSHOT_FORMAT_VERSION,
3395
+ snapshot_byte_order: "little_endian",
3396
+ snapshot_restore_base: "post_init_physical_state_image",
3397
+ param_size_bytes: paramSize,
3398
+ param_align_bytes: 16,
3399
+ requires_full_blocks: false,
3400
+ },
3401
+ metadata: {
3402
+ states: snapshot.entries,
3403
+ inputs: this.portMetadata(this.mir.interface.inputs, this.inputLayout),
3404
+ outputs: this.portMetadata(this.mir.interface.outputs, this.outputLayout),
3405
+ control_outputs: this.mir.interface.control_outputs.map((output, id) => ({
3406
+ name: output.name,
3407
+ type_repr: typeName(this.type(output.ty), this),
3408
+ scalar: this.storageShape(output.ty).scalar,
3409
+ array_len: this.storageShape(output.ty).length,
3410
+ element_size_bytes: this.scalarSize(this.storageShape(output.ty).scalar),
3411
+ slot_offset: this.interfaceSlotOffset(
3412
+ this.mir.interface.control_outputs,
3413
+ id,
3414
+ ),
3415
+ byte_offset: null,
3416
+ state_byte_offset: this.controlOutputLayout[id].offset,
3417
+ byte_size: this.controlOutputLayout[id].size,
3418
+ default_reprs: null,
3419
+ range_min_repr: null,
3420
+ range_max_repr: null,
3421
+ })),
3422
+ params: this.mir.interface.params.map((param, id) => ({
3423
+ name: param.name,
3424
+ type_repr: typeName(this.type(param.ty), this),
3425
+ scalar: this.storageShape(param.ty).scalar,
3426
+ array_len: this.storageShape(param.ty).length,
3427
+ element_size_bytes: this.scalarSize(this.storageShape(param.ty).scalar),
3428
+ slot_offset: this.interfaceSlotOffset(this.mir.interface.params, id),
3429
+ byte_offset: this.paramLayout[id].offset,
3430
+ state_byte_offset: null,
3431
+ byte_size: this.paramLayout[id].size,
3432
+ default_reprs: this.constantReprs(param.default),
3433
+ range_min_repr: this.scalarRepr(param.range?.min),
3434
+ range_max_repr: this.scalarRepr(param.range?.max),
3435
+ })),
3436
+ buffers: this.mir.interface.buffers.map((buffer) => {
3437
+ const channels = this.bufferChannelMetadata(buffer.channels);
3438
+ return {
3439
+ name: buffer.name,
3440
+ type_repr: this.bufferTypeRepr(buffer, channels),
3441
+ scalar: buffer.element,
3442
+ element_size_bytes: this.scalarSize(buffer.element),
3443
+ channels: channels.kind,
3444
+ static_channels: channels.count,
3445
+ access: buffer.access,
3446
+ may_write: buffer.access === "read_write",
3447
+ };
3448
+ }),
3449
+ events: this.mir.interface.events.map((event, eventId) => ({
3450
+ name: event.name,
3451
+ export: `onda_event_${eventId}`,
3452
+ payload_size_bytes: this.eventLayout[eventId].byteLength,
3453
+ payload_min_size_bytes: this.eventLayout[eventId].minimumByteLength,
3454
+ has_dynamic_payload: this.eventLayout[eventId].dynamic,
3455
+ params: event.params.map((param, paramId) => ({
3456
+ name: param.name,
3457
+ type_repr: typeName(this.type(param.ty), this),
3458
+ scalar: this.storageShape(param.ty).scalar,
3459
+ array_len: this.storageShape(param.ty).length ?? 0,
3460
+ is_slice: this.storageShape(param.ty).isSlice === true,
3461
+ byte_offset: this.eventLayout[eventId][paramId].offset,
3462
+ byte_size: this.eventLayout[eventId][paramId].size,
3463
+ element_size_bytes: this.scalarSize(
3464
+ this.storageShape(param.ty).scalar,
3465
+ ),
3466
+ has_default: param.default !== null && param.default !== undefined,
3467
+ default_reprs: this.constantReprs(param.default),
3468
+ })),
3469
+ })),
3470
+ },
3471
+ };
3472
+ }
3473
+
3474
+ stateSnapshotMetadata() {
3475
+ let byteOffset = 0;
3476
+ const entries = [];
3477
+ for (const [id, slot] of this.mir.state.entries()) {
3478
+ if (slot.persistence !== "snapshot") {
3479
+ continue;
3480
+ }
3481
+ const shape = this.storageShape(slot.ty);
3482
+ const layout = this.stateLayout[id];
3483
+ entries.push({
3484
+ name: slot.name,
3485
+ type_repr: typeName(this.type(slot.ty), this),
3486
+ scalar: shape.scalar,
3487
+ array_len: shape.length,
3488
+ element_size_bytes: this.scalarSize(shape.scalar),
3489
+ packed_snapshot_byte_offset: byteOffset,
3490
+ physical_state_byte_offset: layout.offset,
3491
+ byte_size: layout.size,
3492
+ });
3493
+ byteOffset += layout.size;
3494
+ }
3495
+ return { entries, byteLength: byteOffset };
3496
+ }
3497
+
3498
+ portMetadata(ports, layouts) {
3499
+ return ports.map((port, id) => ({
3500
+ name: port.name,
3501
+ type_repr: typeName(this.type(port.ty), this),
3502
+ scalar: layouts[id].scalar,
3503
+ array_len: layouts[id].channels,
3504
+ element_size_bytes: layouts[id].size,
3505
+ slot_offset: layouts[id].channel,
3506
+ byte_offset: null,
3507
+ state_byte_offset: null,
3508
+ byte_size: layouts[id].size * layouts[id].channels,
3509
+ default_reprs: null,
3510
+ range_min_repr: null,
3511
+ range_max_repr: null,
3512
+ }));
3513
+ }
3514
+
3515
+ interfaceSlotOffset(values, end) {
3516
+ let offset = 0;
3517
+ for (let id = 0; id < end; id += 1) {
3518
+ offset += this.storageShape(values[id].ty).length;
3519
+ }
3520
+ return offset;
3521
+ }
3522
+
3523
+ constantReprs(value) {
3524
+ if (value === null || value === undefined) return null;
3525
+ if (value.kind === "scalar") return [this.scalarRepr(value.data)];
3526
+ if (value.kind === "aggregate") {
3527
+ return value.data.flatMap((entry) => this.constantReprs(entry) ?? []);
3528
+ }
3529
+ this.fail(`unknown MIR constant kind '${String(value.kind)}'`);
3530
+ }
3531
+
3532
+ scalarRepr(value) {
3533
+ if (value === null || value === undefined) return null;
3534
+ if (Object.is(value.value, -0)) return "-0";
3535
+ return String(value.value);
3536
+ }
3537
+
3538
+ bufferTypeRepr(buffer, channels) {
3539
+ if (channels.kind === "mono") return `buffer[${buffer.element}]`;
3540
+ if (channels.kind === "static") {
3541
+ return `buffer[${buffer.element}[${channels.count}]]`;
3542
+ }
3543
+ return `buffer[${buffer.element}[]]`;
3544
+ }
3545
+
3546
+ storageShape(typeId) {
3547
+ const type = this.type(typeId);
3548
+ if (type.kind === "scalar") {
3549
+ return { scalar: type.data, length: 1, isArray: false };
3550
+ }
3551
+ if (type.kind === "array") {
3552
+ const element = this.type(type.data.element);
3553
+ if (element.kind !== "scalar") {
3554
+ this.fail("nested aggregate storage metadata is not supported yet");
3555
+ }
3556
+ return { scalar: element.data, length: type.data.len, isArray: true };
3557
+ }
3558
+ if (type.kind === "slice") {
3559
+ return {
3560
+ scalar: type.data.element,
3561
+ length: null,
3562
+ isArray: true,
3563
+ isSlice: true,
3564
+ };
3565
+ }
3566
+ this.fail(`storage metadata for MIR type '${type.kind}' is not supported yet`);
3567
+ }
3568
+
3569
+ bufferChannelMetadata(channels) {
3570
+ if (channels === "mono") {
3571
+ return { kind: "mono", count: 1 };
3572
+ }
3573
+ if (channels === "dynamic") {
3574
+ return { kind: "dynamic", count: null };
3575
+ }
3576
+ if (
3577
+ channels &&
3578
+ typeof channels === "object" &&
3579
+ Number.isInteger(channels.static) &&
3580
+ channels.static > 0
3581
+ ) {
3582
+ return { kind: "static", count: channels.static };
3583
+ }
3584
+ this.fail(`invalid MIR buffer channel descriptor '${JSON.stringify(channels)}'`);
3585
+ }
3586
+
3587
+ fail(message) {
3588
+ throw new OndaBinaryenError(message);
3589
+ }
3590
+ }
3591
+
3592
+ function alignUp(value, alignment) {
3593
+ return Math.ceil(value / alignment) * alignment;
3594
+ }
3595
+
3596
+ function typeName(type, compiler) {
3597
+ if (type.kind === "scalar") return type.data;
3598
+ if (type.kind === "array") {
3599
+ return `${typeName(compiler.type(type.data.element), compiler)}[${type.data.len}]`;
3600
+ }
3601
+ if (type.kind === "slice") return `${type.data.element}[]`;
3602
+ return type.kind;
3603
+ }
3604
+
3605
+ function encodeScalarValues(values, scalar, compiler) {
3606
+ const size = compiler.scalarSize(scalar);
3607
+ const bytes = new Uint8Array(values.length * size);
3608
+ const view = new DataView(bytes.buffer);
3609
+ values.forEach((value, index) => {
3610
+ if (value.type !== scalar) {
3611
+ compiler.fail(`const data scalar '${value.type}' does not match '${scalar}'`);
3612
+ }
3613
+ const offset = index * size;
3614
+ switch (scalar) {
3615
+ case "bool": view.setUint8(offset, value.value ? 1 : 0); break;
3616
+ case "i32": view.setInt32(offset, value.value, true); break;
3617
+ case "i64":
3618
+ view.setBigInt64(offset, decodeI64Literal(value.value, compiler), true);
3619
+ break;
3620
+ case "f32":
3621
+ view.setFloat32(offset, decodeFloatLiteral(value.value, "f32", compiler), true);
3622
+ break;
3623
+ case "f64":
3624
+ view.setFloat64(offset, decodeFloatLiteral(value.value, "f64", compiler), true);
3625
+ break;
3626
+ default: compiler.fail(`unknown const data scalar '${String(scalar)}'`);
3627
+ }
3628
+ });
3629
+ return bytes;
3630
+ }
3631
+
3632
+ const I64_MIN = -(1n << 63n);
3633
+ const I64_MAX = (1n << 63n) - 1n;
3634
+
3635
+ function decodeI64Literal(value, compiler) {
3636
+ if (typeof value !== "string" || !/^-?(0|[1-9][0-9]*)$/.test(value)) {
3637
+ compiler.fail(
3638
+ `MIR schema ${SUPPORTED_MIR_SCHEMA_VERSION} i64 values must be canonical decimal strings`,
3639
+ );
3640
+ }
3641
+ let decoded;
3642
+ try {
3643
+ decoded = BigInt(value);
3644
+ } catch {
3645
+ compiler.fail(`invalid MIR i64 value '${String(value)}'`);
3646
+ }
3647
+ if (decoded < I64_MIN || decoded > I64_MAX) {
3648
+ compiler.fail(`MIR i64 value '${value}' is outside the signed 64-bit range`);
3649
+ }
3650
+ return decoded;
3651
+ }
3652
+
3653
+ function decodeFloatLiteral(value, scalar, compiler) {
3654
+ if (typeof value === "number") {
3655
+ if (!Number.isFinite(value)) {
3656
+ compiler.fail(`${scalar} JSON number must be finite`);
3657
+ }
3658
+ return value;
3659
+ }
3660
+
3661
+ const digits = scalar === "f32" ? 8 : 16;
3662
+ if (
3663
+ typeof value !== "string"
3664
+ || !new RegExp(`^0x[0-9a-f]{${digits}}$`).test(value)
3665
+ ) {
3666
+ compiler.fail(
3667
+ `${scalar} value must be a finite JSON number or an exact ${digits}-digit IEEE bit pattern`,
3668
+ );
3669
+ }
3670
+
3671
+ const bytes = new ArrayBuffer(8);
3672
+ const view = new DataView(bytes);
3673
+ if (scalar === "f32") {
3674
+ view.setUint32(0, Number.parseInt(value.slice(2), 16), true);
3675
+ return view.getFloat32(0, true);
3676
+ }
3677
+ view.setBigUint64(0, BigInt(value), true);
3678
+ return view.getFloat64(0, true);
3679
+ }