@onda-lang/wasm-compiler 0.8.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1900 @@
1
+ import binaryen from "binaryen";
2
+ import { MirCompilerLowering } from "./lowering.js";
3
+ import {
4
+ OndaBinaryenError,
5
+ supportsMirOperation,
6
+ PROCESSOR_ABI_VERSION,
7
+ PROCESSOR_ARTIFACT_FORMAT,
8
+ PROCESSOR_ARTIFACT_FORMAT_VERSION,
9
+ PROCESSOR_SNAPSHOT_FORMAT_VERSION,
10
+ WASM32_ADDRESS_SPACE_BYTES,
11
+ DELEGATE_RECORD_HEADER_SIZE,
12
+ PRINT_RECORD_HEADER_SIZE,
13
+ POINTER_GLOBALS,
14
+ typeName,
15
+ decodeI64Literal,
16
+ decodeFloatLiteral,
17
+ } from "./shared.js";
18
+
19
+ export class MirCompiler extends MirCompilerLowering {
20
+ compileSliceSource(source, context) {
21
+ if (source.kind === "place") {
22
+ const typeId = this.placeTypeId(source.data, context);
23
+ const type = this.type(typeId);
24
+ if (type.kind === "slice") {
25
+ return this.loadSlicePlace(source.data, context);
26
+ }
27
+ if (type.kind !== "array") {
28
+ this.fail(`slice place source has unsupported type '${type.kind}'`);
29
+ }
30
+ const element = this.type(type.data.element);
31
+ if (element.kind !== "scalar") {
32
+ this.fail("slice array source must have primitive elements");
33
+ }
34
+ return [
35
+ this.placeAddress(source.data, context),
36
+ this.placeAddress(source.data, context),
37
+ this.module.i32.const(type.data.len),
38
+ this.module.i32.const(this.scalarSize(element.data)),
39
+ ];
40
+ }
41
+ if (source.kind === "const_data") {
42
+ const item = this.constLayout[source.data];
43
+ if (!item) this.fail(`const data id ${source.data} is out of range`);
44
+ return [
45
+ this.module.i32.const(item.address),
46
+ this.module.i32.const(item.address),
47
+ this.module.i32.const(item.len),
48
+ this.module.i32.const(this.scalarSize(item.scalar)),
49
+ ];
50
+ }
51
+ if (source.kind === "buffer") {
52
+ const buffer = this.requireBufferRef(source.data.buffer);
53
+ const elementSize = this.scalarSize(buffer.element);
54
+ const staticIndex = this.staticBufferRefIndex(source.data.buffer);
55
+ const prelude = [];
56
+ let descriptorIndex;
57
+ if (staticIndex === null) {
58
+ const indexLocal = this.allocateGeneratedLocal(
59
+ context,
60
+ "i32",
61
+ "buffer.descriptor_index",
62
+ );
63
+ prelude.push(
64
+ this.module.local.set(
65
+ indexLocal,
66
+ this.compileBufferRefIndex(source.data.buffer, context),
67
+ ),
68
+ );
69
+ descriptorIndex = () =>
70
+ this.module.local.get(indexLocal, binaryen.i32);
71
+ } else {
72
+ descriptorIndex = () => this.module.i32.const(staticIndex);
73
+ }
74
+ const component = (globalName, scalar) => this.bufferTableValueFactory(
75
+ globalName,
76
+ descriptorIndex,
77
+ staticIndex,
78
+ scalar,
79
+ context,
80
+ );
81
+ let channels = this.bufferChannelsFactory(
82
+ source.data.buffer,
83
+ descriptorIndex,
84
+ staticIndex,
85
+ context,
86
+ );
87
+ if (
88
+ staticIndex === null
89
+ && this.bufferRefChannelMetadata(source.data.buffer).kind === "dynamic"
90
+ ) {
91
+ channels = this.snapshotOperationValue(
92
+ channels,
93
+ "i32",
94
+ "buffer.channels",
95
+ prelude,
96
+ context,
97
+ );
98
+ }
99
+ const readAddress = component(POINTER_GLOBALS.buffers, "i32");
100
+ const writeAddress = component(POINTER_GLOBALS.bufferWrites, "i32");
101
+ let read = readAddress();
102
+ let write = writeAddress();
103
+ if (source.data.channel !== null) {
104
+ const channelLocal = this.allocateGeneratedLocal(
105
+ context,
106
+ "i32",
107
+ "buffer.slice_channel",
108
+ );
109
+ prelude.push(
110
+ this.module.local.set(
111
+ channelLocal,
112
+ this.compileDynamicBoundedIndex(
113
+ () => this.compileValue(source.data.channel, context),
114
+ channels,
115
+ "clamp",
116
+ context,
117
+ true,
118
+ ),
119
+ ),
120
+ );
121
+ const channelOffset = () => this.module.i32.mul(
122
+ this.module.local.get(channelLocal, binaryen.i32),
123
+ this.module.i32.const(elementSize),
124
+ );
125
+ read = this.bufferPointerWithOffset(
126
+ readAddress,
127
+ channelOffset(),
128
+ false,
129
+ context,
130
+ );
131
+ write = this.bufferPointerWithOffset(
132
+ writeAddress,
133
+ channelOffset(),
134
+ true,
135
+ context,
136
+ );
137
+ }
138
+ const result = [
139
+ read,
140
+ write,
141
+ component(POINTER_GLOBALS.bufferFrames, "i32")(),
142
+ source.data.channel === null
143
+ ? this.module.i32.const(elementSize)
144
+ : this.module.i32.mul(channels(), this.module.i32.const(elementSize)),
145
+ ];
146
+ if (prelude.length > 0) {
147
+ result[0] = this.module.block(
148
+ null,
149
+ [...prelude, result[0]],
150
+ binaryen.i32,
151
+ );
152
+ }
153
+ return result;
154
+ }
155
+ if (source.kind === "buffer_param") {
156
+ const type = this.bufferParamType(source.data.parameter, context);
157
+ const elementSize = this.scalarSize(type.data.element);
158
+ const staticSelector = this.staticBufferParamSelector(
159
+ source.data.parameter,
160
+ context,
161
+ );
162
+ const prelude = [];
163
+ let selector = null;
164
+ if (
165
+ source.data.parameter?.kind === "array_element"
166
+ && staticSelector === null
167
+ ) {
168
+ const selectorLocal = this.allocateGeneratedLocal(
169
+ context,
170
+ "i32",
171
+ "buffer_param.selector",
172
+ );
173
+ prelude.push(
174
+ this.module.local.set(
175
+ selectorLocal,
176
+ this.compileBufferParamSelector(source.data.parameter, context),
177
+ ),
178
+ );
179
+ selector = () => this.module.local.get(selectorLocal, binaryen.i32);
180
+ } else if (staticSelector !== null) {
181
+ selector = () => this.module.i32.const(staticSelector);
182
+ }
183
+ const component = (offset, scalar) => this.bufferParamComponentFactory(
184
+ source.data.parameter,
185
+ offset,
186
+ scalar,
187
+ selector,
188
+ staticSelector,
189
+ context,
190
+ );
191
+ let channels = this.bufferParamChannelsFactory(
192
+ source.data.parameter,
193
+ selector,
194
+ staticSelector,
195
+ context,
196
+ );
197
+ if (
198
+ source.data.parameter?.kind === "array_element"
199
+ && staticSelector === null
200
+ && this.bufferChannelMetadata(
201
+ type.data.channels,
202
+ type.data.element,
203
+ ).kind === "dynamic"
204
+ ) {
205
+ channels = this.snapshotOperationValue(
206
+ channels,
207
+ "i32",
208
+ "buffer_param.channels",
209
+ prelude,
210
+ context,
211
+ );
212
+ }
213
+ const readAddress = component(0, "i32");
214
+ const writeAddress = component(1, "i32");
215
+ let read = readAddress();
216
+ let write = writeAddress();
217
+ if (source.data.channel !== null) {
218
+ const channelLocal = this.allocateGeneratedLocal(
219
+ context,
220
+ "i32",
221
+ "buffer_param.slice_channel",
222
+ );
223
+ prelude.push(
224
+ this.module.local.set(
225
+ channelLocal,
226
+ this.compileDynamicBoundedIndex(
227
+ () => this.compileValue(source.data.channel, context),
228
+ channels,
229
+ "clamp",
230
+ context,
231
+ true,
232
+ ),
233
+ ),
234
+ );
235
+ const channelOffset = () => this.module.i32.mul(
236
+ this.module.local.get(channelLocal, binaryen.i32),
237
+ this.module.i32.const(elementSize),
238
+ );
239
+ read = this.bufferPointerWithOffset(
240
+ readAddress,
241
+ channelOffset(),
242
+ false,
243
+ context,
244
+ );
245
+ write = this.bufferPointerWithOffset(
246
+ writeAddress,
247
+ channelOffset(),
248
+ true,
249
+ context,
250
+ );
251
+ }
252
+ const result = [
253
+ read,
254
+ write,
255
+ component(2, "i32")(),
256
+ source.data.channel === null
257
+ ? this.module.i32.const(elementSize)
258
+ : this.module.i32.mul(channels(), this.module.i32.const(elementSize)),
259
+ ];
260
+ if (prelude.length > 0) {
261
+ result[0] = this.module.block(
262
+ null,
263
+ [...prelude, result[0]],
264
+ binaryen.i32,
265
+ );
266
+ }
267
+ return result;
268
+ }
269
+ this.fail(`unsupported slice source '${String(source.kind)}'`);
270
+ }
271
+
272
+ sliceElementScalar(value, context) {
273
+ if (value.kind !== "local") this.fail("slice value is not a local");
274
+ const local = context.function.locals[value.data];
275
+ const type = local && this.type(local.ty);
276
+ if (!type || type.kind !== "slice") {
277
+ this.fail(`local id ${value.data} is not slice-typed`);
278
+ }
279
+ return type.data.element;
280
+ }
281
+
282
+ sliceAccess(value, context) {
283
+ if (value.kind !== "local") this.fail("slice value is not a local");
284
+ const local = context.function.locals[value.data];
285
+ const type = local && this.type(local.ty);
286
+ if (!type || type.kind !== "slice") {
287
+ this.fail(`local id ${value.data} is not slice-typed`);
288
+ }
289
+ return type.data.access;
290
+ }
291
+
292
+ compileSliceAddress(slice, index, bounds, context, write) {
293
+ return this.compileSliceAddressWithFactories(
294
+ () => this.compileSliceValue(slice, context),
295
+ () => this.compileValue(index, context),
296
+ bounds,
297
+ context,
298
+ write,
299
+ );
300
+ }
301
+
302
+ compileSliceAddressWithFactories(slice, index, bounds, context, write) {
303
+ const bounded = this.compileDynamicBoundedIndex(
304
+ index,
305
+ () => slice()[2],
306
+ bounds,
307
+ context,
308
+ );
309
+ return this.module.i32.add(
310
+ slice()[write ? 1 : 0],
311
+ this.module.i32.mul(bounded, slice()[3]),
312
+ );
313
+ }
314
+
315
+ compileArrayWindowAddress(data, parameterType, context) {
316
+ const sourceTypeId = this.placeTypeId(data.array, context);
317
+ const sourceType = this.type(sourceTypeId);
318
+ if (sourceType.kind !== "array") {
319
+ this.fail("array-window source is not a fixed array");
320
+ }
321
+ if (
322
+ !this.typesEquivalent(sourceType.data.element, parameterType.data.element) ||
323
+ sourceType.data.len < parameterType.data.len
324
+ ) {
325
+ this.fail("array-window source does not contain the required parameter shape");
326
+ }
327
+ const elementSize = this.typeLayout(parameterType.data.element).size;
328
+ const start = this.compileWindowStart(
329
+ () => this.compileValue(data.start, context),
330
+ () =>
331
+ this.module.i32.const(
332
+ sourceType.data.len - parameterType.data.len,
333
+ ),
334
+ data.bounds,
335
+ context,
336
+ );
337
+ return this.module.i32.add(
338
+ this.placeAddress(data.array, context),
339
+ this.module.i32.mul(start(), this.module.i32.const(elementSize)),
340
+ );
341
+ }
342
+
343
+ compileSliceWindowAddress(data, parameterType, context, write) {
344
+ const elementType = this.type(parameterType.data.element);
345
+ if (elementType.kind !== "scalar") {
346
+ this.fail("slice-window fixed-array parameter element is not scalar");
347
+ }
348
+ const slice = () => this.compileSliceValue(data.slice, context);
349
+ const elementSize = this.scalarSize(elementType.data);
350
+ const requiredLen = parameterType.data.len;
351
+ const start = this.compileWindowStart(
352
+ () => this.compileValue(data.start, context),
353
+ () =>
354
+ this.module.i32.sub(
355
+ slice()[2],
356
+ this.module.i32.const(requiredLen),
357
+ ),
358
+ data.bounds,
359
+ context,
360
+ );
361
+ const address = () =>
362
+ this.module.i32.add(
363
+ slice()[write ? 1 : 0],
364
+ this.module.i32.mul(start(), slice()[3]),
365
+ );
366
+ if (data.bounds === "unchecked") {
367
+ return address();
368
+ }
369
+ const invalidShape = () =>
370
+ this.module.i32.or(
371
+ this.module.i32.ne(
372
+ slice()[3],
373
+ this.module.i32.const(elementSize),
374
+ ),
375
+ this.module.i32.lt_s(
376
+ slice()[2],
377
+ this.module.i32.const(requiredLen),
378
+ ),
379
+ );
380
+ return this.module.if(
381
+ invalidShape(),
382
+ this.raiseRuntimeFailure(context),
383
+ address(),
384
+ );
385
+ }
386
+
387
+ compileWindowStart(start, maximum, bounds, context) {
388
+ const zero = () => this.module.i32.const(0);
389
+ if (bounds === "unchecked") {
390
+ return start;
391
+ }
392
+ if (bounds === "clamp") {
393
+ return () => {
394
+ const low = () =>
395
+ this.module.select(
396
+ this.module.i32.lt_s(start(), zero()),
397
+ zero(),
398
+ start(),
399
+ );
400
+ return this.module.select(
401
+ this.module.i32.gt_s(low(), maximum()),
402
+ maximum(),
403
+ low(),
404
+ );
405
+ };
406
+ }
407
+ if (bounds === "checked") {
408
+ return () =>
409
+ this.module.if(
410
+ this.module.i32.or(
411
+ this.module.i32.lt_s(start(), zero()),
412
+ this.module.i32.gt_s(start(), maximum()),
413
+ ),
414
+ this.raiseRuntimeFailure(context),
415
+ start(),
416
+ );
417
+ }
418
+ this.fail(`unknown bounds mode '${String(bounds)}'`);
419
+ }
420
+
421
+ compileSliceLoad(data, context) {
422
+ const scalar = this.sliceElementScalar(data.slice, context);
423
+ return this.loadScalar(
424
+ scalar,
425
+ this.compileSliceAddress(data.slice, data.index, data.bounds, context, false),
426
+ );
427
+ }
428
+
429
+ compileSliceStore(data, context) {
430
+ if (this.sliceAccess(data.slice, context) !== "read_write") {
431
+ this.fail("slice store destination is read-only");
432
+ }
433
+ const scalar = this.sliceElementScalar(data.slice, context);
434
+ return this.storeScalar(
435
+ scalar,
436
+ this.compileSliceAddress(data.slice, data.index, data.bounds, context, true),
437
+ this.compileValue(data.value, context),
438
+ );
439
+ }
440
+
441
+ compileSliceFill(statement, data, context) {
442
+ if (this.sliceAccess(data.destination, context) !== "read_write") {
443
+ this.fail("slice fill destination is read-only");
444
+ }
445
+ const scratch = context.sliceScratch.get(statement);
446
+ if (!scratch || scratch.count !== 1) {
447
+ this.fail("internal slice-fill scratch local is missing");
448
+ }
449
+ const counter = scratch.index;
450
+ const id = this.nextLabel++;
451
+ const vectorLoopLabel = `$onda.slice.fill.vector.${id}`;
452
+ const scalarLoopLabel = `$onda.slice.fill.scalar.${id}`;
453
+ const destination = () => this.compileSliceValue(data.destination, context);
454
+ const counterValue = () => this.module.local.get(counter, binaryen.i32);
455
+ const scalar = this.sliceElementScalar(data.destination, context);
456
+ const scalarSize = this.scalarSize(scalar);
457
+ const address = () =>
458
+ this.module.i32.add(
459
+ destination()[1],
460
+ this.module.i32.mul(counterValue(), destination()[3]),
461
+ );
462
+ const scalarLoop = () =>
463
+ this.module.loop(
464
+ scalarLoopLabel,
465
+ this.module.if(
466
+ this.module.i32.lt_s(counterValue(), destination()[2]),
467
+ this.module.block(null, [
468
+ this.storeScalar(
469
+ scalar,
470
+ address(),
471
+ this.compileValue(data.value, context),
472
+ ),
473
+ this.module.local.set(
474
+ counter,
475
+ this.module.i32.add(counterValue(), this.module.i32.const(1)),
476
+ ),
477
+ this.module.br(scalarLoopLabel),
478
+ ]),
479
+ ),
480
+ );
481
+ const body = [];
482
+ if (this.options.simd) {
483
+ const lanes = 16 / scalarSize;
484
+ const vectorCondition = this.module.i32.and(
485
+ this.module.i32.eq(
486
+ destination()[3],
487
+ this.module.i32.const(scalarSize),
488
+ ),
489
+ this.module.i32.and(
490
+ this.module.i32.ge_u(
491
+ destination()[2],
492
+ this.module.i32.const(lanes),
493
+ ),
494
+ this.module.i32.le_u(
495
+ counterValue(),
496
+ this.module.i32.sub(
497
+ destination()[2],
498
+ this.module.i32.const(lanes),
499
+ ),
500
+ ),
501
+ ),
502
+ );
503
+ body.push(
504
+ this.module.loop(
505
+ vectorLoopLabel,
506
+ this.module.if(
507
+ vectorCondition,
508
+ this.module.block(null, [
509
+ this.module.v128.store(
510
+ 0,
511
+ scalarSize,
512
+ address(),
513
+ this.compileVectorSplat(
514
+ scalar,
515
+ this.compileValue(data.value, context),
516
+ ),
517
+ ),
518
+ this.module.local.set(
519
+ counter,
520
+ this.module.i32.add(
521
+ counterValue(),
522
+ this.module.i32.const(lanes),
523
+ ),
524
+ ),
525
+ this.module.br(vectorLoopLabel),
526
+ ]),
527
+ ),
528
+ ),
529
+ );
530
+ }
531
+ body.push(scalarLoop());
532
+ return this.module.block(null, [
533
+ this.module.local.set(counter, this.module.i32.const(0)),
534
+ ...body,
535
+ ]);
536
+ }
537
+
538
+ compileSliceCopy(statement, data, context) {
539
+ if (this.sliceAccess(data.destination, context) !== "read_write") {
540
+ this.fail("slice copy destination is read-only");
541
+ }
542
+ const scratch = context.sliceScratch.get(statement);
543
+ if (!scratch || scratch.count !== 2) {
544
+ this.fail("internal slice-copy scratch locals are missing");
545
+ }
546
+ const count = scratch.index;
547
+ const counter = scratch.index + 1;
548
+ const id = this.nextLabel++;
549
+ const loopLabel = `$onda.slice.copy.${id}`;
550
+ const destination = () => this.compileSliceValue(data.destination, context);
551
+ const source = () => this.compileSliceValue(data.source, context);
552
+ const countValue = () => this.module.local.get(count, binaryen.i32);
553
+ const counterValue = () => this.module.local.get(counter, binaryen.i32);
554
+ const copyIndex = () =>
555
+ this.module.select(
556
+ this.module.i32.and(
557
+ this.module.i32.eq(destination()[3], source()[3]),
558
+ this.module.i32.gt_u(destination()[1], source()[0]),
559
+ ),
560
+ this.module.i32.sub(
561
+ this.module.i32.sub(countValue(), this.module.i32.const(1)),
562
+ counterValue(),
563
+ ),
564
+ counterValue(),
565
+ );
566
+ const sourceAddress = () =>
567
+ this.module.i32.add(
568
+ source()[0],
569
+ this.module.i32.mul(copyIndex(), source()[3]),
570
+ );
571
+ const destinationAddress = () =>
572
+ this.module.i32.add(
573
+ destination()[1],
574
+ this.module.i32.mul(copyIndex(), destination()[3]),
575
+ );
576
+ const sourceScalar = this.sliceElementScalar(data.source, context);
577
+ const destinationScalar = this.sliceElementScalar(data.destination, context);
578
+ const copiedValue = () => {
579
+ const loaded = this.loadScalar(sourceScalar, sourceAddress());
580
+ return sourceScalar === destinationScalar
581
+ ? loaded
582
+ : this.compileCast(sourceScalar, destinationScalar, loaded);
583
+ };
584
+ const nonEmpty = () =>
585
+ this.module.i32.gt_s(countValue(), this.module.i32.const(0));
586
+ const sourceEnd = () =>
587
+ this.module.i32.add(
588
+ this.module.i32.add(
589
+ source()[0],
590
+ this.module.i32.mul(
591
+ this.module.i32.sub(countValue(), this.module.i32.const(1)),
592
+ source()[3],
593
+ ),
594
+ ),
595
+ this.module.i32.const(this.scalarSize(sourceScalar)),
596
+ );
597
+ const destinationEnd = () =>
598
+ this.module.i32.add(
599
+ this.module.i32.add(
600
+ destination()[1],
601
+ this.module.i32.mul(
602
+ this.module.i32.sub(countValue(), this.module.i32.const(1)),
603
+ destination()[3],
604
+ ),
605
+ ),
606
+ this.module.i32.const(this.scalarSize(destinationScalar)),
607
+ );
608
+ const overlaps = () =>
609
+ this.module.i32.and(
610
+ nonEmpty(),
611
+ this.module.i32.and(
612
+ this.module.i32.lt_u(destination()[1], sourceEnd()),
613
+ this.module.i32.lt_u(source()[0], destinationEnd()),
614
+ ),
615
+ );
616
+ const invalidOverlap = () =>
617
+ this.module.i32.and(
618
+ this.module.i32.ne(destination()[3], source()[3]),
619
+ overlaps(),
620
+ );
621
+ const scalarCopy = () =>
622
+ this.module.loop(
623
+ loopLabel,
624
+ this.module.if(
625
+ this.module.i32.lt_s(counterValue(), countValue()),
626
+ this.module.block(null, [
627
+ this.storeScalar(destinationScalar, destinationAddress(), copiedValue()),
628
+ this.module.local.set(
629
+ counter,
630
+ this.module.i32.add(counterValue(), this.module.i32.const(1)),
631
+ ),
632
+ this.module.br(loopLabel),
633
+ ]),
634
+ ),
635
+ );
636
+ const sameRepresentation = sourceScalar === destinationScalar;
637
+ const copy = sameRepresentation
638
+ ? this.module.if(
639
+ this.module.i32.and(
640
+ this.module.i32.eq(
641
+ destination()[3],
642
+ this.module.i32.const(this.scalarSize(destinationScalar)),
643
+ ),
644
+ this.module.i32.eq(
645
+ source()[3],
646
+ this.module.i32.const(this.scalarSize(sourceScalar)),
647
+ ),
648
+ ),
649
+ // memory.copy has memmove overlap semantics and lets engines use
650
+ // their tuned bulk-memory implementation for contiguous slices.
651
+ this.module.memory.copy(
652
+ destination()[1],
653
+ source()[0],
654
+ this.module.i32.mul(
655
+ countValue(),
656
+ this.module.i32.const(this.scalarSize(sourceScalar)),
657
+ ),
658
+ ),
659
+ scalarCopy(),
660
+ )
661
+ : scalarCopy();
662
+ return this.module.block(null, [
663
+ this.module.local.set(
664
+ count,
665
+ this.module.select(
666
+ this.module.i32.lt_s(destination()[2], source()[2]),
667
+ destination()[2],
668
+ source()[2],
669
+ ),
670
+ ),
671
+ this.module.if(invalidOverlap(), this.raiseRuntimeFailure(context)),
672
+ this.module.local.set(counter, this.module.i32.const(0)),
673
+ copy,
674
+ ]);
675
+ }
676
+
677
+ compileVectorSplat(scalar, value) {
678
+ switch (scalar) {
679
+ case "bool": return this.module.i8x16.splat(value);
680
+ case "i32": return this.module.i32x4.splat(value);
681
+ case "i64": return this.module.i64x2.splat(value);
682
+ case "f32": return this.module.f32x4.splat(value);
683
+ case "f64": return this.module.f64x2.splat(value);
684
+ default: this.fail(`unknown SIMD scalar type '${String(scalar)}'`);
685
+ }
686
+ }
687
+
688
+ compileValue(value, context) {
689
+ switch (value.kind) {
690
+ case "local": {
691
+ const scalar = context.localScalars[value.data];
692
+ if (!scalar) {
693
+ this.fail(`local id ${value.data} is not a scalar or is out of range`);
694
+ }
695
+ return this.module.local.get(
696
+ this.localIndex(value.data, context),
697
+ this.wasmType(scalar),
698
+ );
699
+ }
700
+ case "constant":
701
+ return this.compileConstant(value.data);
702
+ default:
703
+ this.fail(`unknown MIR value '${String(value.kind)}'`);
704
+ }
705
+ }
706
+
707
+ compileConstant(value) {
708
+ switch (value.type) {
709
+ case "f32":
710
+ return this.module.f32.const(decodeFloatLiteral(value.value, "f32", this));
711
+ case "f64":
712
+ return this.module.f64.const(decodeFloatLiteral(value.value, "f64", this));
713
+ case "i32":
714
+ return this.module.i32.const(value.value);
715
+ case "i64":
716
+ return this.module.i64.const(decodeI64Literal(value.value, this));
717
+ case "bool":
718
+ return this.module.i32.const(value.value ? 1 : 0);
719
+ default:
720
+ this.fail(`unknown scalar constant type '${String(value.type)}'`);
721
+ }
722
+ }
723
+
724
+ valueScalarType(value, context) {
725
+ if (value.kind === "constant") {
726
+ return value.data.type;
727
+ }
728
+ if (value.kind === "local") {
729
+ const scalar = context.localScalars[value.data];
730
+ if (!scalar) {
731
+ this.fail(`local id ${value.data} is not a scalar or is out of range`);
732
+ }
733
+ return scalar;
734
+ }
735
+ this.fail(`unknown MIR value '${String(value.kind)}'`);
736
+ }
737
+
738
+ placeScalarType(place, context) {
739
+ const typeId = this.placeTypeId(place, context);
740
+ return this.requireScalarType(typeId, "place");
741
+ }
742
+
743
+ placeTypeId(place, context) {
744
+ let typeId;
745
+ switch (place.base.kind) {
746
+ case "local":
747
+ typeId = context.function.locals[place.base.data]?.ty;
748
+ break;
749
+ case "parameter":
750
+ typeId = context.function.params[place.base.data]?.ty;
751
+ break;
752
+ case "state":
753
+ typeId = this.mir.state[place.base.data]?.ty;
754
+ break;
755
+ case "param":
756
+ typeId = this.mir.interface.params[place.base.data]?.ty;
757
+ break;
758
+ case "event_param": {
759
+ const event = this.mir.interface.events[context.eventId];
760
+ typeId = event?.params[place.base.data]?.ty;
761
+ break;
762
+ }
763
+ default:
764
+ this.fail(`place base '${place.base.kind}' is not supported yet`);
765
+ }
766
+ if (!Number.isInteger(typeId)) {
767
+ this.fail(`place base '${place.base.kind}' id ${place.base.data} is out of range`);
768
+ }
769
+ for (const projection of place.projections) {
770
+ const type = this.type(typeId);
771
+ if (projection.kind === "index" && type.kind === "array") {
772
+ typeId = type.data.element;
773
+ } else {
774
+ this.fail(`projection '${projection.kind}' on '${type.kind}' is not supported yet`);
775
+ }
776
+ }
777
+ return typeId;
778
+ }
779
+
780
+ loadPlace(place, context) {
781
+ if (place.base.kind === "local" && place.projections.length === 0) {
782
+ const scalar = this.placeScalarType(place, context);
783
+ return this.module.local.get(
784
+ this.localIndex(place.base.data, context),
785
+ this.wasmType(scalar),
786
+ );
787
+ }
788
+ if (place.base.kind === "parameter" && place.projections.length === 0) {
789
+ const scalar = this.placeScalarType(place, context);
790
+ const layout = context.paramLayouts[place.base.data];
791
+ if (!layout || !["scalar", "scalar_ref"].includes(layout.kind)) {
792
+ this.fail(`parameter id ${place.base.data} is not a scalar`);
793
+ }
794
+ if (layout.kind === "scalar_ref") {
795
+ return this.loadScalar(
796
+ scalar,
797
+ this.module.local.get(layout.index, binaryen.i32),
798
+ );
799
+ }
800
+ return this.module.local.get(layout.index, this.wasmType(scalar));
801
+ }
802
+ const scalar = this.placeScalarType(place, context);
803
+ return this.loadScalar(scalar, this.placeAddress(place, context));
804
+ }
805
+
806
+ storePlace(place, value, scalar, context) {
807
+ if (place.base.kind === "local" && place.projections.length === 0) {
808
+ return this.module.local.set(this.localIndex(place.base.data, context), value);
809
+ }
810
+ if (place.base.kind === "parameter" && place.projections.length === 0) {
811
+ const layout = context.paramLayouts[place.base.data];
812
+ if (layout?.kind !== "scalar_ref") {
813
+ this.fail("assignment to a by-value function parameter is not supported");
814
+ }
815
+ return this.storeScalar(
816
+ scalar,
817
+ this.module.local.get(layout.index, binaryen.i32),
818
+ value,
819
+ );
820
+ }
821
+ return this.storeScalar(scalar, this.placeAddress(place, context), value);
822
+ }
823
+
824
+ placeAddress(place, context) {
825
+ let typeId;
826
+ let address;
827
+ switch (place.base.kind) {
828
+ case "parameter": {
829
+ const layout = context.paramLayouts[place.base.data];
830
+ if (!layout || !["scalar_ref", "array_ref"].includes(layout.kind)) {
831
+ this.fail(`parameter id ${place.base.data} is not an addressable reference`);
832
+ }
833
+ typeId = context.function.params[place.base.data].ty;
834
+ address = this.module.local.get(layout.index, binaryen.i32);
835
+ break;
836
+ }
837
+ case "local": {
838
+ const layout =
839
+ this.localArrayLayout[context.functionId]?.[place.base.data]
840
+ ?? this.localScalarRefLayout[context.functionId]?.[place.base.data];
841
+ if (!layout) {
842
+ this.fail(`local id ${place.base.data} is not addressable`);
843
+ }
844
+ typeId = context.function.locals[place.base.data].ty;
845
+ address = this.module.i32.const(layout.address);
846
+ break;
847
+ }
848
+ case "state": {
849
+ const layout = this.stateLayout[place.base.data];
850
+ if (!layout) this.fail(`state id ${place.base.data} is out of range`);
851
+ typeId = this.mir.state[place.base.data].ty;
852
+ address = this.module.i32.add(
853
+ this.module.global.get(POINTER_GLOBALS.state, binaryen.i32),
854
+ this.module.i32.const(layout.offset),
855
+ );
856
+ break;
857
+ }
858
+ case "param": {
859
+ const layout = this.paramLayout[place.base.data];
860
+ if (!layout) this.fail(`param id ${place.base.data} is out of range`);
861
+ typeId = this.mir.interface.params[place.base.data].ty;
862
+ address = this.module.i32.add(
863
+ this.module.global.get(POINTER_GLOBALS.params, binaryen.i32),
864
+ this.module.i32.const(layout.offset),
865
+ );
866
+ break;
867
+ }
868
+ case "event_param": {
869
+ const event = this.mir.interface.events[context.eventId];
870
+ const layout = this.eventLayout[context.eventId]?.[place.base.data];
871
+ if (!event || !layout) {
872
+ this.fail(
873
+ `event parameter id ${place.base.data} is invalid for function '${context.function.name}'`,
874
+ );
875
+ }
876
+ typeId = event.params[place.base.data].ty;
877
+ if (this.type(typeId).kind === "slice") {
878
+ this.fail("slice event parameters require slice-value lowering");
879
+ }
880
+ address = this.compileEventParamAddress(context.eventId, place.base.data);
881
+ break;
882
+ }
883
+ default:
884
+ this.fail(
885
+ `addressable place base '${place.base.kind}' is not in the first Binaryen slice`,
886
+ );
887
+ }
888
+
889
+ for (const projection of place.projections) {
890
+ const type = this.type(typeId);
891
+ if (projection.kind !== "index" || type.kind !== "array") {
892
+ this.fail(`projection '${projection.kind}' on '${type.kind}' is not supported yet`);
893
+ }
894
+ const elementLayout = this.typeLayout(type.data.element);
895
+ const index = this.compileBoundedIndex(
896
+ projection.data.index,
897
+ type.data.len,
898
+ projection.data.bounds,
899
+ context,
900
+ );
901
+ address = this.module.i32.add(
902
+ address,
903
+ this.module.i32.mul(index, this.module.i32.const(elementLayout.size)),
904
+ );
905
+ typeId = type.data.element;
906
+ }
907
+ return address;
908
+ }
909
+
910
+ compileBoundedIndex(value, length, bounds, context) {
911
+ if (!Number.isInteger(length) || length <= 0) {
912
+ this.fail("array and port lengths must be positive integers");
913
+ }
914
+ if (bounds === "unchecked") {
915
+ return this.compileValue(value, context);
916
+ }
917
+ if (bounds === "clamp") {
918
+ return this.module.select(
919
+ this.module.i32.lt_s(
920
+ this.compileValue(value, context),
921
+ this.module.i32.const(0),
922
+ ),
923
+ this.module.i32.const(0),
924
+ this.module.select(
925
+ this.module.i32.ge_s(
926
+ this.compileValue(value, context),
927
+ this.module.i32.const(length),
928
+ ),
929
+ this.module.i32.const(length - 1),
930
+ this.compileValue(value, context),
931
+ ),
932
+ );
933
+ }
934
+ if (bounds === "checked") {
935
+ const outOfBounds = this.module.i32.or(
936
+ this.module.i32.lt_s(
937
+ this.compileValue(value, context),
938
+ this.module.i32.const(0),
939
+ ),
940
+ this.module.i32.ge_s(
941
+ this.compileValue(value, context),
942
+ this.module.i32.const(length),
943
+ ),
944
+ );
945
+ return this.module.if(
946
+ outOfBounds,
947
+ this.raiseRuntimeFailure(context),
948
+ this.compileValue(value, context),
949
+ );
950
+ }
951
+ this.fail(`unknown bounds mode '${String(bounds)}'`);
952
+ }
953
+
954
+ compileDynamicBoundedIndex(
955
+ index,
956
+ length,
957
+ bounds,
958
+ context,
959
+ clampLengthKnownPositive = false,
960
+ ) {
961
+ if (bounds === "unchecked") {
962
+ return index();
963
+ }
964
+ if (bounds === "clamp") {
965
+ const maximum = () =>
966
+ this.module.i32.sub(length(), this.module.i32.const(1));
967
+ const clamped = () =>
968
+ this.module.select(
969
+ this.module.i32.lt_s(index(), this.module.i32.const(0)),
970
+ this.module.i32.const(0),
971
+ this.module.select(
972
+ this.module.i32.gt_s(index(), maximum()),
973
+ maximum(),
974
+ index(),
975
+ ),
976
+ );
977
+ if (clampLengthKnownPositive) return clamped();
978
+ return this.module.if(
979
+ this.module.i32.le_s(length(), this.module.i32.const(0)),
980
+ this.raiseRuntimeFailure(context),
981
+ clamped(),
982
+ );
983
+ }
984
+ if (bounds === "checked") {
985
+ return this.module.if(
986
+ this.module.i32.or(
987
+ this.module.i32.lt_s(index(), this.module.i32.const(0)),
988
+ this.module.i32.ge_s(index(), length()),
989
+ ),
990
+ this.raiseRuntimeFailure(context),
991
+ index(),
992
+ );
993
+ }
994
+ this.fail(`unknown bounds mode '${String(bounds)}'`);
995
+ }
996
+
997
+ compileUnary(op, scalar, value) {
998
+ if (!supportsMirOperation("unary", op, scalar)) {
999
+ this.fail(`unary operation '${String(op)}' does not support scalar '${scalar}'`);
1000
+ }
1001
+ switch (op) {
1002
+ case "negate":
1003
+ if (scalar === "f32" || scalar === "f64") return this.module[scalar].neg(value);
1004
+ return this.module[scalar].sub(this.zero(scalar), value);
1005
+ case "logical_not":
1006
+ return this.module.i32.eqz(value);
1007
+ case "bit_not":
1008
+ return this.module[scalar].xor(value, this.minusOne(scalar));
1009
+ default:
1010
+ this.fail(`unknown unary operation '${String(op)}'`);
1011
+ }
1012
+ }
1013
+
1014
+ compileBinary(op, scalar, lhs, rhs, context) {
1015
+ if (!supportsMirOperation("binary", op, scalar)) {
1016
+ this.fail(`binary operation '${String(op)}' does not support scalar '${scalar}'`);
1017
+ }
1018
+ const wasm = this.module[scalar === "bool" ? "i32" : scalar];
1019
+ const integer = scalar === "i32" || scalar === "i64" || scalar === "bool";
1020
+ switch (op) {
1021
+ case "add": return wasm.add(lhs(), rhs());
1022
+ case "subtract": return wasm.sub(lhs(), rhs());
1023
+ case "multiply": return wasm.mul(lhs(), rhs());
1024
+ case "divide": {
1025
+ if (!integer) return wasm.div(lhs(), rhs());
1026
+ const minimum = () =>
1027
+ scalar === "i64"
1028
+ ? this.module.i64.const(-(1n << 63n))
1029
+ : this.module.i32.const(-0x8000_0000);
1030
+ const negativeOne = () =>
1031
+ scalar === "i64"
1032
+ ? this.module.i64.const(-1n)
1033
+ : this.module.i32.const(-1);
1034
+ const overflow = this.module.i32.and(
1035
+ wasm.eq(lhs(), minimum()),
1036
+ wasm.eq(rhs(), negativeOne()),
1037
+ );
1038
+ const division = this.module.if(
1039
+ overflow,
1040
+ minimum(),
1041
+ wasm.div_s(lhs(), rhs()),
1042
+ );
1043
+ return this.module.if(
1044
+ wasm.eq(rhs(), this.zero(scalar)),
1045
+ this.raiseRuntimeFailure(context),
1046
+ division,
1047
+ );
1048
+ }
1049
+ case "remainder":
1050
+ if (!integer) {
1051
+ return this.compileMathKernelCall("remainder", scalar, [lhs(), rhs()]);
1052
+ }
1053
+ return this.module.if(
1054
+ wasm.eq(rhs(), this.zero(scalar)),
1055
+ this.raiseRuntimeFailure(context),
1056
+ wasm.rem_s(lhs(), rhs()),
1057
+ );
1058
+ case "bit_and": return wasm.and(lhs(), rhs());
1059
+ case "bit_or": return wasm.or(lhs(), rhs());
1060
+ case "bit_xor": return wasm.xor(lhs(), rhs());
1061
+ case "shift_left": return wasm.shl(lhs(), rhs());
1062
+ case "shift_right": return wasm.shr_s(lhs(), rhs());
1063
+ default:
1064
+ this.fail(`unknown binary operation '${String(op)}'`);
1065
+ }
1066
+ }
1067
+
1068
+ compileCompare(op, scalar, lhs, rhs) {
1069
+ if (!supportsMirOperation("compare", op, scalar)) {
1070
+ this.fail(`comparison '${String(op)}' does not support scalar '${scalar}'`);
1071
+ }
1072
+ const type = scalar === "bool" ? "i32" : scalar;
1073
+ const wasm = this.module[type];
1074
+ const integer = type === "i32" || type === "i64";
1075
+ switch (op) {
1076
+ case "equal": return wasm.eq(lhs, rhs);
1077
+ case "not_equal": return wasm.ne(lhs, rhs);
1078
+ case "less": return integer ? wasm.lt_s(lhs, rhs) : wasm.lt(lhs, rhs);
1079
+ case "less_equal": return integer ? wasm.le_s(lhs, rhs) : wasm.le(lhs, rhs);
1080
+ case "greater": return integer ? wasm.gt_s(lhs, rhs) : wasm.gt(lhs, rhs);
1081
+ case "greater_equal": return integer ? wasm.ge_s(lhs, rhs) : wasm.ge(lhs, rhs);
1082
+ default:
1083
+ this.fail(`unknown comparison '${String(op)}'`);
1084
+ }
1085
+ }
1086
+
1087
+ compileCast(from, to, value) {
1088
+ const source = from === "bool" ? "i32" : from;
1089
+ const target = to === "bool" ? "i32" : to;
1090
+ if (source === target) return value;
1091
+ if (target === "bool") return this.module[source].ne(value, this.zero(source));
1092
+ if (source === "f32" && target === "f64") return this.module.f64.promote(value);
1093
+ if (source === "f64" && target === "f32") return this.module.f32.demote(value);
1094
+ if (source === "i32" && target === "i64") return this.module.i64.extend_s(value);
1095
+ if (source === "i64" && target === "i32") return this.module.i32.wrap(value);
1096
+ if ((source === "i32" || source === "i64") && target === "f32") {
1097
+ return this.module.f32.convert_s[source](value);
1098
+ }
1099
+ if ((source === "i32" || source === "i64") && target === "f64") {
1100
+ return this.module.f64.convert_s[source](value);
1101
+ }
1102
+ if ((source === "f32" || source === "f64") && target === "i32") {
1103
+ this.module.setFeatures(
1104
+ this.module.getFeatures() | binaryen.Features.NontrappingFPToInt,
1105
+ );
1106
+ return this.module.i32.trunc_s_sat[source](value);
1107
+ }
1108
+ if ((source === "f32" || source === "f64") && target === "i64") {
1109
+ this.module.setFeatures(
1110
+ this.module.getFeatures() | binaryen.Features.NontrappingFPToInt,
1111
+ );
1112
+ return this.module.i64.trunc_s_sat[source](value);
1113
+ }
1114
+ this.fail(`unsupported scalar cast from '${from}' to '${to}'`);
1115
+ }
1116
+
1117
+ compileIntrinsic(data, expectedScalar, context) {
1118
+ const scalar = data.args.length
1119
+ ? this.valueScalarType(data.args[0], context)
1120
+ : expectedScalar;
1121
+ const args = data.args.map((value) => this.compileValue(value, context));
1122
+ const isFloat = scalar === "f32" || scalar === "f64";
1123
+ const isInteger = scalar === "i32" || scalar === "i64";
1124
+ if (!isFloat && !isInteger) {
1125
+ this.fail(`intrinsic '${data.intrinsic}' requires numeric operands`);
1126
+ }
1127
+ const wasm = this.module[scalar];
1128
+
1129
+ if (isInteger) {
1130
+ // Binaryen expressions are tree nodes, so each use needs its own local
1131
+ // get/constant node even when the MIR value is the same.
1132
+ const arg = (index) => this.compileValue(data.args[index], context);
1133
+ switch (data.intrinsic) {
1134
+ case "abs":
1135
+ return this.module.select(
1136
+ wasm.lt_s(arg(0), this.zero(scalar)),
1137
+ wasm.sub(this.zero(scalar), arg(0)),
1138
+ arg(0),
1139
+ );
1140
+ case "min":
1141
+ return this.module.select(wasm.lt_s(arg(0), arg(1)), arg(0), arg(1));
1142
+ case "max":
1143
+ return this.module.select(wasm.gt_s(arg(0), arg(1)), arg(0), arg(1));
1144
+ case "range_clamp":
1145
+ return this.module.select(
1146
+ wasm.lt_s(arg(0), arg(1)),
1147
+ arg(1),
1148
+ this.module.select(
1149
+ wasm.gt_s(arg(0), arg(2)),
1150
+ arg(2),
1151
+ arg(0),
1152
+ ),
1153
+ );
1154
+ case "range_wrap": { // Bounds are validator-required integer constants.
1155
+ const lowerLiteral = data.args[1]?.data;
1156
+ const upperLiteral = data.args[2]?.data;
1157
+ if (
1158
+ data.args[1]?.kind !== "constant"
1159
+ || data.args[2]?.kind !== "constant"
1160
+ || lowerLiteral?.type !== scalar
1161
+ || upperLiteral?.type !== scalar
1162
+ ) {
1163
+ this.fail("range_wrap requires constant bounds matching its integer operand");
1164
+ }
1165
+ const lower = scalar === "i64"
1166
+ ? decodeI64Literal(lowerLiteral.value, this)
1167
+ : BigInt(lowerLiteral.value);
1168
+ const upper = scalar === "i64"
1169
+ ? decodeI64Literal(upperLiteral.value, this)
1170
+ : BigInt(upperLiteral.value);
1171
+ const bits = scalar === "i64" ? 64n : 32n;
1172
+ const width = upper - lower + 1n;
1173
+ if (width === (1n << bits)) return arg(0);
1174
+ const encodedWidth = BigInt.asIntN(Number(bits), width);
1175
+ const widthValue = scalar === "i64"
1176
+ ? this.module.i64.const(encodedWidth)
1177
+ : this.module.i32.const(Number(encodedWidth));
1178
+ const encodedSpan = BigInt.asIntN(Number(bits), width - 1n);
1179
+ const spanValue = scalar === "i64"
1180
+ ? this.module.i64.const(encodedSpan)
1181
+ : this.module.i32.const(Number(encodedSpan));
1182
+ const one = scalar === "i64"
1183
+ ? this.module.i64.const(1n)
1184
+ : this.module.i32.const(1);
1185
+ const distanceFromLower = lower === 0n
1186
+ ? arg(0)
1187
+ : wasm.sub(arg(0), arg(1));
1188
+ return this.module.if(
1189
+ wasm.le_u(distanceFromLower, spanValue),
1190
+ arg(0),
1191
+ this.module.if(
1192
+ wasm.lt_s(arg(0), arg(1)),
1193
+ wasm.sub(
1194
+ arg(2),
1195
+ wasm.rem_u(
1196
+ wasm.sub(wasm.sub(arg(1), one), arg(0)),
1197
+ widthValue,
1198
+ ),
1199
+ ),
1200
+ wasm.add(
1201
+ arg(1),
1202
+ wasm.rem_u(
1203
+ wasm.sub(arg(0), wasm.add(arg(2), one)),
1204
+ widthValue,
1205
+ ),
1206
+ ),
1207
+ ),
1208
+ );
1209
+ }
1210
+ default:
1211
+ this.fail(`intrinsic '${data.intrinsic}' requires f32 or f64 operands`);
1212
+ }
1213
+ }
1214
+
1215
+ switch (data.intrinsic) {
1216
+ case "sqrt": return wasm.sqrt(args[0]);
1217
+ case "abs": return wasm.abs(args[0]);
1218
+ case "floor": return wasm.floor(args[0]);
1219
+ case "ceil": return wasm.ceil(args[0]);
1220
+ case "trunc": return wasm.trunc(args[0]);
1221
+ case "min": return wasm.min(args[0], args[1]);
1222
+ case "max": return wasm.max(args[0], args[1]);
1223
+ case "range_clamp": {
1224
+ const arg = (index) => this.compileValue(data.args[index], context);
1225
+ return this.module.select(
1226
+ wasm.ne(arg(0), arg(0)),
1227
+ arg(1),
1228
+ this.module.select(
1229
+ wasm.lt(arg(0), arg(1)),
1230
+ arg(1),
1231
+ this.module.select(
1232
+ wasm.gt(arg(0), arg(2)),
1233
+ arg(2),
1234
+ arg(0),
1235
+ ),
1236
+ ),
1237
+ );
1238
+ }
1239
+ case "fma": return this.compileMathKernelCall(data.intrinsic, scalar, args);
1240
+ case "sin":
1241
+ case "cos":
1242
+ case "tan":
1243
+ case "tanh":
1244
+ case "atan":
1245
+ case "atan2":
1246
+ case "exp":
1247
+ case "log":
1248
+ case "pow":
1249
+ return this.compileMathKernelCall(data.intrinsic, scalar, args);
1250
+ case "round":
1251
+ return this.compileRoundHelper(scalar, args);
1252
+ default:
1253
+ this.fail(`unknown intrinsic '${String(data.intrinsic)}'`);
1254
+ }
1255
+ }
1256
+
1257
+ compileMathKernelCall(intrinsic, scalar, args) {
1258
+ const name = `onda_math_${intrinsic}_${scalar}`;
1259
+ if (!this.requiredMathHelpers.has(name)) {
1260
+ this.fail(`math kernel was not reserved for helper '${name}'`);
1261
+ }
1262
+ return this.module.call(
1263
+ name,
1264
+ args,
1265
+ this.wasmType(scalar),
1266
+ );
1267
+ }
1268
+
1269
+ compileRoundHelper(scalar, args) {
1270
+ const name = `$onda.math.round.${scalar}`;
1271
+ if (!this.internalHelpers.has(name)) {
1272
+ const wasm = this.module[scalar];
1273
+ const get = () => this.module.local.get(0, this.wasmType(scalar));
1274
+ const trunc = () => wasm.trunc(get());
1275
+ const magnitude = wasm.abs(wasm.sub(get(), trunc()));
1276
+ const rounded = wasm.add(
1277
+ trunc(),
1278
+ wasm.copysign(wasm.const(1), get()),
1279
+ );
1280
+ this.module.addFunction(
1281
+ name,
1282
+ this.wasmType(scalar),
1283
+ this.wasmType(scalar),
1284
+ [],
1285
+ this.module.select(
1286
+ wasm.ge(magnitude, wasm.const(0.5)),
1287
+ rounded,
1288
+ trunc(),
1289
+ ),
1290
+ );
1291
+ this.internalHelpers.add(name);
1292
+ }
1293
+ return this.module.call(name, args, this.wasmType(scalar));
1294
+ }
1295
+
1296
+ loadScalar(scalar, address) {
1297
+ switch (scalar) {
1298
+ case "bool": return this.module.i32.load8_u(0, 1, address);
1299
+ case "i32": return this.module.i32.load(0, 4, address);
1300
+ case "i64": return this.module.i64.load(0, 8, address);
1301
+ case "f32": return this.module.f32.load(0, 4, address);
1302
+ case "f64": return this.module.f64.load(0, 8, address);
1303
+ default: this.fail(`unknown scalar type '${String(scalar)}'`);
1304
+ }
1305
+ }
1306
+
1307
+ storeScalar(scalar, address, value) {
1308
+ switch (scalar) {
1309
+ case "bool": return this.module.i32.store8(0, 1, address, value);
1310
+ case "i32": return this.module.i32.store(0, 4, address, value);
1311
+ case "i64": return this.module.i64.store(0, 8, address, value);
1312
+ case "f32": return this.module.f32.store(0, 4, address, value);
1313
+ case "f64": return this.module.f64.store(0, 8, address, value);
1314
+ default: this.fail(`unknown scalar type '${String(scalar)}'`);
1315
+ }
1316
+ }
1317
+
1318
+ storePackedScalar(scalar, address, value) {
1319
+ switch (scalar) {
1320
+ case "bool": return this.module.i32.store8(0, 1, address, value);
1321
+ case "i32": return this.module.i32.store(0, 1, address, value);
1322
+ case "i64": return this.module.i64.store(0, 1, address, value);
1323
+ case "f32": return this.module.f32.store(0, 1, address, value);
1324
+ case "f64": return this.module.f64.store(0, 1, address, value);
1325
+ default: this.fail(`unknown scalar type '${String(scalar)}'`);
1326
+ }
1327
+ }
1328
+
1329
+ type(typeId) {
1330
+ const type = this.mir.types[typeId];
1331
+ if (!type) this.fail(`type id ${typeId} is out of range`);
1332
+ return type;
1333
+ }
1334
+
1335
+ typesEquivalent(lhsId, rhsId, visiting = new Set()) {
1336
+ if (lhsId === rhsId) return true;
1337
+ const key = `${lhsId}:${rhsId}`;
1338
+ if (visiting.has(key)) return true;
1339
+ const lhs = this.mir.types[lhsId];
1340
+ const rhs = this.mir.types[rhsId];
1341
+ if (!lhs || !rhs || lhs.kind !== rhs.kind) return false;
1342
+ visiting.add(key);
1343
+ let equivalent = false;
1344
+ if (lhs.kind === "scalar") {
1345
+ equivalent = lhs.data === rhs.data;
1346
+ } else if (lhs.kind === "array") {
1347
+ equivalent =
1348
+ lhs.data.len === rhs.data.len &&
1349
+ this.typesEquivalent(lhs.data.element, rhs.data.element, visiting);
1350
+ } else if (lhs.kind === "slice") {
1351
+ equivalent =
1352
+ lhs.data.element === rhs.data.element &&
1353
+ lhs.data.access === rhs.data.access;
1354
+ } else if (lhs.kind === "buffer") {
1355
+ equivalent =
1356
+ lhs.data.element === rhs.data.element &&
1357
+ JSON.stringify(lhs.data.channels) === JSON.stringify(rhs.data.channels) &&
1358
+ lhs.data.access === rhs.data.access;
1359
+ } else if (lhs.kind === "buffer_span") {
1360
+ equivalent =
1361
+ lhs.data.element === rhs.data.element &&
1362
+ JSON.stringify(lhs.data.channels) === JSON.stringify(rhs.data.channels) &&
1363
+ lhs.data.access === rhs.data.access &&
1364
+ lhs.data.len === rhs.data.len;
1365
+ } else if (lhs.kind === "tuple") {
1366
+ equivalent =
1367
+ lhs.data.length === rhs.data.length &&
1368
+ lhs.data.every((element, index) =>
1369
+ this.typesEquivalent(element, rhs.data[index], visiting),
1370
+ );
1371
+ } else if (lhs.kind === "struct") {
1372
+ equivalent = lhs.data === rhs.data;
1373
+ }
1374
+ visiting.delete(key);
1375
+ return equivalent;
1376
+ }
1377
+
1378
+ typeLayout(typeId) {
1379
+ const type = this.type(typeId);
1380
+ if (type.kind === "scalar") {
1381
+ const size = this.scalarSize(type.data);
1382
+ return { size, align: size, scalar: type.data };
1383
+ }
1384
+ if (type.kind === "array") {
1385
+ const element = this.typeLayout(type.data.element);
1386
+ if (!Number.isInteger(type.data.len) || type.data.len <= 0) {
1387
+ this.fail("fixed array length must be a positive integer");
1388
+ }
1389
+ return {
1390
+ size: element.size * type.data.len,
1391
+ align: element.align,
1392
+ scalar: element.scalar,
1393
+ };
1394
+ }
1395
+ this.fail(`storage layout for MIR type '${type.kind}' is not supported yet`);
1396
+ }
1397
+
1398
+ requireScalarType(typeId, description) {
1399
+ const type = this.type(typeId);
1400
+ if (type.kind !== "scalar") {
1401
+ this.fail(`${description} has unsupported non-scalar type '${type.kind}'`);
1402
+ }
1403
+ return type.data;
1404
+ }
1405
+
1406
+ scalarSize(scalar) {
1407
+ switch (scalar) {
1408
+ case "bool": return 1;
1409
+ case "i32":
1410
+ case "f32": return 4;
1411
+ case "i64":
1412
+ case "f64": return 8;
1413
+ default: this.fail(`unknown scalar type '${String(scalar)}'`);
1414
+ }
1415
+ }
1416
+
1417
+ requireWasm32Extent(byteLength, description) {
1418
+ if (
1419
+ !Number.isSafeInteger(byteLength)
1420
+ || byteLength < 0
1421
+ || byteLength >= WASM32_ADDRESS_SPACE_BYTES
1422
+ ) {
1423
+ this.fail(`${description} must fit within the wasm32 4 GiB address space`);
1424
+ }
1425
+ }
1426
+
1427
+ wasmType(scalar) {
1428
+ return binaryen[scalar === "bool" ? "i32" : scalar];
1429
+ }
1430
+
1431
+ wasmResultType(scalars) {
1432
+ if (scalars.length === 0) return binaryen.none;
1433
+ if (scalars.length === 1) return this.wasmType(scalars[0]);
1434
+ return binaryen.createType(scalars.map((scalar) => this.wasmType(scalar)));
1435
+ }
1436
+
1437
+ zero(scalar) {
1438
+ const type = scalar === "bool" ? "i32" : scalar;
1439
+ return type === "i64"
1440
+ ? this.module.i64.const(0n)
1441
+ : this.module[type].const(0);
1442
+ }
1443
+
1444
+ minusOne(scalar) {
1445
+ const type = scalar === "bool" ? "i32" : scalar;
1446
+ return type === "i64"
1447
+ ? this.module.i64.const(-1n)
1448
+ : this.module.i32.const(-1);
1449
+ }
1450
+
1451
+ localIndex(localId, context) {
1452
+ if (!Number.isInteger(localId) || localId < 0 || localId >= context.localScalars.length) {
1453
+ this.fail(`local id ${localId} is out of range`);
1454
+ }
1455
+ return context.localLayouts[localId].index;
1456
+ }
1457
+
1458
+ requireFunctionId(id, description) {
1459
+ if (!Number.isInteger(id) || id < 0 || id >= this.mir.functions.length) {
1460
+ this.fail(`${description} function id ${id} is out of range`);
1461
+ }
1462
+ }
1463
+
1464
+ requireBuffer(id) {
1465
+ if (!Number.isInteger(id) || id < 0 || id >= this.mir.interface.buffers.length) {
1466
+ this.fail(`buffer id ${id} is out of range`);
1467
+ }
1468
+ return this.mir.interface.buffers[id];
1469
+ }
1470
+
1471
+ bufferRefFirst(bufferRef) {
1472
+ if (Number.isInteger(bufferRef)) return bufferRef;
1473
+ if (bufferRef?.kind === "direct" && Number.isInteger(bufferRef.data)) {
1474
+ return bufferRef.data;
1475
+ }
1476
+ if (
1477
+ bufferRef?.kind === "array_element"
1478
+ && Number.isInteger(bufferRef.data?.first)
1479
+ && Number.isInteger(bufferRef.data?.len)
1480
+ && bufferRef.data.len > 0
1481
+ ) {
1482
+ return bufferRef.data.first;
1483
+ }
1484
+ this.fail("invalid MIR buffer reference");
1485
+ }
1486
+
1487
+ requireBufferRef(bufferRef) {
1488
+ const first = this.bufferRefFirst(bufferRef);
1489
+ const buffer = this.requireBuffer(first);
1490
+ if (bufferRef?.kind === "array_element") {
1491
+ const last = first + bufferRef.data.len - 1;
1492
+ this.requireBuffer(last);
1493
+ for (let id = first + 1; id <= last; id += 1) {
1494
+ const candidate = this.requireBuffer(id);
1495
+ if (
1496
+ candidate.element !== buffer.element
1497
+ || JSON.stringify(candidate.channels) !== JSON.stringify(buffer.channels)
1498
+ || candidate.access !== buffer.access
1499
+ ) {
1500
+ this.fail("buffer-array elements must have one descriptor type");
1501
+ }
1502
+ }
1503
+ }
1504
+ return buffer;
1505
+ }
1506
+
1507
+ bufferRefChannelMetadata(bufferRef) {
1508
+ const buffer = this.requireBufferRef(bufferRef);
1509
+ return this.bufferChannelMetadata(buffer.channels, buffer.element);
1510
+ }
1511
+
1512
+ compileBufferRefIndex(bufferRef, context) {
1513
+ const staticIndex = this.staticBufferRefIndex(bufferRef);
1514
+ if (staticIndex !== null) return this.module.i32.const(staticIndex);
1515
+ const first = this.bufferRefFirst(bufferRef);
1516
+ const data = bufferRef.data;
1517
+ const selector = this.compileDynamicBoundedIndex(
1518
+ () => this.compileValue(data.selector, context),
1519
+ () => this.module.i32.const(data.len),
1520
+ data.bounds,
1521
+ context,
1522
+ true,
1523
+ );
1524
+ return this.module.i32.add(this.module.i32.const(first), selector);
1525
+ }
1526
+
1527
+ staticBufferRefIndex(bufferRef) {
1528
+ const first = this.bufferRefFirst(bufferRef);
1529
+ if (Number.isInteger(bufferRef) || bufferRef.kind === "direct") return first;
1530
+ const data = bufferRef.data;
1531
+ const selector = data.selector;
1532
+ if (
1533
+ selector?.kind !== "constant"
1534
+ || selector.data?.type !== "i32"
1535
+ || !Number.isInteger(selector.data.value)
1536
+ ) {
1537
+ return null;
1538
+ }
1539
+ let index = selector.data.value;
1540
+ if (data.bounds === "clamp") {
1541
+ index = Math.min(data.len - 1, Math.max(0, index));
1542
+ } else if (index < 0 || index >= data.len) {
1543
+ // Preserve checked failure behavior and leave invalid unchecked MIR to
1544
+ // the normal validated lowering path.
1545
+ return null;
1546
+ }
1547
+ return first + index;
1548
+ }
1549
+
1550
+ currentLabel(labels, statement) {
1551
+ const label = labels.at(-1);
1552
+ if (!label) this.fail(`'${statement}' appears outside a MIR loop`);
1553
+ return label;
1554
+ }
1555
+
1556
+ buildMetadata() {
1557
+ const stateSize = this.stateLayout.byteLength ?? 0;
1558
+ const paramSize = this.paramLayout.byteLength ?? 0;
1559
+ const snapshot = this.stateSnapshotMetadata();
1560
+ const eventExports = this.mir.interface.events.map(
1561
+ (_, id) => `onda_event_${id}`,
1562
+ );
1563
+ const requiredExports = [
1564
+ "memory",
1565
+ "__heap_base",
1566
+ "onda_processor_init",
1567
+ "onda_process",
1568
+ ...eventExports,
1569
+ ];
1570
+ const targetFeatures = ["bulk-memory"];
1571
+ if (this.options.simd) targetFeatures.push("simd128");
1572
+ return {
1573
+ format: PROCESSOR_ARTIFACT_FORMAT,
1574
+ format_version: PROCESSOR_ARTIFACT_FORMAT_VERSION,
1575
+ artifact_kind: "webassembly_module",
1576
+ abi_version: PROCESSOR_ABI_VERSION,
1577
+ backend: "binaryen-js",
1578
+ mir_schema_version: this.mir.schema_version,
1579
+ target: {
1580
+ triple: "wasm32-unknown-unknown",
1581
+ cpu: "generic",
1582
+ features: targetFeatures.map((feature) => `+${feature}`).join(","),
1583
+ reloc_model: "static",
1584
+ code_model: "default",
1585
+ opt_level: String(this.options.optimizeLevel),
1586
+ abi_name: null,
1587
+ data_layout: "e-m:e-p:32:32-i64:64-n32:64-S128",
1588
+ pointer_width_bits: 32,
1589
+ byte_order: "little_endian",
1590
+ pointer_model: "linear_memory_offset",
1591
+ calling_convention: "core_webassembly",
1592
+ },
1593
+ integration: {
1594
+ required_symbols: requiredExports,
1595
+ one_processor_per_artifact: true,
1596
+ profile: {
1597
+ kind: "core_webassembly_module",
1598
+ imports: [],
1599
+ memory_export: "memory",
1600
+ heap_base_export: "__heap_base",
1601
+ },
1602
+ },
1603
+ required_features: targetFeatures,
1604
+ optimization: {
1605
+ enabled: this.options.optimize,
1606
+ level: this.options.optimizeLevel,
1607
+ shrink_level: this.options.shrinkLevel,
1608
+ fast_math: this.options.fastMath,
1609
+ simd: this.options.simd,
1610
+ inline_functions_with_loops:
1611
+ this.options.allowInliningFunctionsWithLoops,
1612
+ },
1613
+ compile: {
1614
+ sample_rate: this.mir.config.sample_rate,
1615
+ block_size: this.mir.config.block_size,
1616
+ fast_math: this.options.fastMath,
1617
+ },
1618
+ exports: {
1619
+ memory: "memory",
1620
+ heap_base: "__heap_base",
1621
+ init: "onda_processor_init",
1622
+ process: "onda_process",
1623
+ events: eventExports,
1624
+ },
1625
+ runtime: {
1626
+ state_size_bytes: stateSize,
1627
+ state_align_bytes: 16,
1628
+ state_initialization: "zeroed",
1629
+ snapshot_size_bytes: snapshot.byteLength,
1630
+ snapshot_format_version: PROCESSOR_SNAPSHOT_FORMAT_VERSION,
1631
+ snapshot_byte_order: "little_endian",
1632
+ snapshot_restore_base: "post_init_physical_state_image",
1633
+ param_size_bytes: paramSize,
1634
+ param_align_bytes: 16,
1635
+ requires_full_blocks: false,
1636
+ delegate_record_header_size_bytes: DELEGATE_RECORD_HEADER_SIZE,
1637
+ print_record_header_size_bytes: PRINT_RECORD_HEADER_SIZE,
1638
+ },
1639
+ metadata: {
1640
+ source_files: (this.mir.source_files ?? []).map((file) => ({
1641
+ path: file.path,
1642
+ })),
1643
+ log_sites: (this.mir.log_sites ?? []).map((site, index) => ({
1644
+ index,
1645
+ label: site.label ?? null,
1646
+ source: {
1647
+ file: site.source.file ?? null,
1648
+ line: site.source.line,
1649
+ column: site.source.column,
1650
+ end_line: site.source.end_line,
1651
+ end_column: site.source.end_column,
1652
+ },
1653
+ lexical_owner: site.lexical_owner,
1654
+ declaration: site.declaration ?? null,
1655
+ argument_types: [...site.argument_types],
1656
+ payload_size_bytes: site.payload_size,
1657
+ })),
1658
+ states: snapshot.entries,
1659
+ inputs: this.portMetadata(this.mir.interface.inputs, this.inputLayout),
1660
+ outputs: this.portMetadata(this.mir.interface.outputs, this.outputLayout),
1661
+ control_outputs: this.mir.interface.control_outputs.map((output, id) => ({
1662
+ name: output.name,
1663
+ type_repr: typeName(this.type(output.ty), this),
1664
+ scalar: this.storageShape(output.ty).scalar,
1665
+ array_len: this.storageShape(output.ty).length,
1666
+ element_size_bytes: this.scalarSize(this.storageShape(output.ty).scalar),
1667
+ slot_offset: this.interfaceSlotOffset(
1668
+ this.mir.interface.control_outputs,
1669
+ id,
1670
+ ),
1671
+ byte_offset: null,
1672
+ state_byte_offset: this.controlOutputLayout[id].offset,
1673
+ byte_size: this.controlOutputLayout[id].size,
1674
+ default_reprs: null,
1675
+ range_min_repr: null,
1676
+ range_max_repr: null,
1677
+ param_control: null,
1678
+ })),
1679
+ params: this.mir.interface.params.map((param, id) => ({
1680
+ name: param.name,
1681
+ type_repr: typeName(this.type(param.ty), this),
1682
+ scalar: this.storageShape(param.ty).scalar,
1683
+ array_len: this.storageShape(param.ty).length,
1684
+ element_size_bytes: this.scalarSize(this.storageShape(param.ty).scalar),
1685
+ slot_offset: this.interfaceSlotOffset(this.mir.interface.params, id),
1686
+ byte_offset: this.paramLayout[id].offset,
1687
+ state_byte_offset: null,
1688
+ byte_size: this.paramLayout[id].size,
1689
+ default_reprs: this.constantReprs(param.default),
1690
+ range_min_repr: this.scalarRepr(param.range?.min),
1691
+ range_max_repr: this.scalarRepr(param.range?.max),
1692
+ param_control: this.storageShape(param.ty).length === 1 && param.range
1693
+ ? {
1694
+ scale: param.control.scale,
1695
+ curve: param.control.curve,
1696
+ unit: param.control.unit,
1697
+ step_repr: this.scalarRepr(param.control.step),
1698
+ step_count: param.control.step_count,
1699
+ }
1700
+ : null,
1701
+ })),
1702
+ buffers: this.mir.interface.buffers.map((buffer, bufferId) => {
1703
+ const channels = this.bufferChannelMetadata(buffer.channels, buffer.element);
1704
+ return {
1705
+ name: buffer.name,
1706
+ type_repr: this.bufferTypeRepr(buffer, channels),
1707
+ scalar: buffer.element,
1708
+ element_size_bytes: this.scalarSize(buffer.element),
1709
+ channels: channels.kind,
1710
+ static_channels: channels.count,
1711
+ access: buffer.access,
1712
+ may_write: this.bufferMayWrite[bufferId],
1713
+ };
1714
+ }),
1715
+ buffer_arrays: (this.mir.interface.buffer_arrays ?? []).map((array) => ({
1716
+ name: array.name,
1717
+ first_buffer: array.first,
1718
+ len: array.len,
1719
+ })),
1720
+ events: this.mir.interface.events.map((event, eventId) => ({
1721
+ name: event.name,
1722
+ export: `onda_event_${eventId}`,
1723
+ payload_size_bytes: this.eventLayout[eventId].byteLength,
1724
+ payload_min_size_bytes: this.eventLayout[eventId].minimumByteLength,
1725
+ has_dynamic_payload: this.eventLayout[eventId].dynamic,
1726
+ params: event.params.map((param, paramId) => {
1727
+ const shape = this.storageShape(param.ty);
1728
+ return {
1729
+ name: param.name,
1730
+ type_repr: typeName(this.type(param.ty), this),
1731
+ scalar: shape.scalar,
1732
+ array_len: shape.length ?? 0,
1733
+ is_array: shape.isArray === true && shape.isSlice !== true,
1734
+ is_slice: shape.isSlice === true,
1735
+ byte_offset: this.eventLayout[eventId][paramId].offset,
1736
+ byte_size: this.eventLayout[eventId][paramId].size,
1737
+ element_size_bytes: this.scalarSize(shape.scalar),
1738
+ has_default: param.default !== null && param.default !== undefined,
1739
+ default_reprs: this.constantReprs(param.default),
1740
+ };
1741
+ }),
1742
+ })),
1743
+ delegates: this.mir.interface.delegates.map((delegate, delegateId) => ({
1744
+ index: delegateId,
1745
+ name: delegate.name,
1746
+ payload_size_bytes: this.delegateLayout[delegateId].byteLength,
1747
+ payload_min_size_bytes: this.delegateLayout[delegateId].minimumByteLength,
1748
+ has_dynamic_payload: this.delegateLayout[delegateId].dynamic,
1749
+ params: delegate.params.map((param, paramId) => {
1750
+ const shape = this.storageShape(param.ty);
1751
+ return {
1752
+ name: param.name,
1753
+ type_repr: typeName(this.type(param.ty), this),
1754
+ scalar: shape.scalar,
1755
+ array_len: shape.length ?? 0,
1756
+ is_array: shape.isArray === true && shape.isSlice !== true,
1757
+ is_slice: shape.isSlice === true,
1758
+ byte_offset: this.delegateLayout[delegateId][paramId].offset,
1759
+ byte_size: this.delegateLayout[delegateId][paramId].size,
1760
+ element_size_bytes: this.scalarSize(shape.scalar),
1761
+ };
1762
+ }),
1763
+ })),
1764
+ },
1765
+ };
1766
+ }
1767
+
1768
+ stateSnapshotMetadata() {
1769
+ let byteOffset = 0;
1770
+ const entries = [];
1771
+ for (const [id, slot] of this.mir.state.entries()) {
1772
+ if (slot.persistence !== "snapshot") {
1773
+ continue;
1774
+ }
1775
+ const shape = this.storageShape(slot.ty);
1776
+ const layout = this.stateLayout[id];
1777
+ entries.push({
1778
+ name: slot.name,
1779
+ authored: slot.authored,
1780
+ type_repr: typeName(this.type(slot.ty), this),
1781
+ scalar: shape.scalar,
1782
+ array_len: shape.length,
1783
+ element_size_bytes: this.scalarSize(shape.scalar),
1784
+ packed_snapshot_byte_offset: byteOffset,
1785
+ physical_state_byte_offset: layout.offset,
1786
+ byte_size: layout.size,
1787
+ integer_range: slot.integer_range === undefined || slot.integer_range === null
1788
+ ? null
1789
+ : {
1790
+ min: {
1791
+ type: slot.integer_range.min.type,
1792
+ value: String(slot.integer_range.min.value),
1793
+ },
1794
+ max: {
1795
+ type: slot.integer_range.max.type,
1796
+ value: String(slot.integer_range.max.value),
1797
+ },
1798
+ mode: slot.integer_range.mode,
1799
+ },
1800
+ });
1801
+ byteOffset += layout.size;
1802
+ }
1803
+ return { entries, byteLength: byteOffset };
1804
+ }
1805
+
1806
+ portMetadata(ports, layouts) {
1807
+ return ports.map((port, id) => ({
1808
+ name: port.name,
1809
+ type_repr: typeName(this.type(port.ty), this),
1810
+ scalar: layouts[id].scalar,
1811
+ array_len: layouts[id].channels,
1812
+ element_size_bytes: layouts[id].size,
1813
+ slot_offset: layouts[id].channel,
1814
+ byte_offset: null,
1815
+ state_byte_offset: null,
1816
+ byte_size: layouts[id].size * layouts[id].channels,
1817
+ default_reprs: null,
1818
+ range_min_repr: null,
1819
+ range_max_repr: null,
1820
+ param_control: null,
1821
+ }));
1822
+ }
1823
+
1824
+ interfaceSlotOffset(values, end) {
1825
+ let offset = 0;
1826
+ for (let id = 0; id < end; id += 1) {
1827
+ offset += this.storageShape(values[id].ty).length;
1828
+ }
1829
+ return offset;
1830
+ }
1831
+
1832
+ constantReprs(value) {
1833
+ if (value === null || value === undefined) return null;
1834
+ if (value.kind === "scalar") return [this.scalarRepr(value.data)];
1835
+ if (value.kind === "aggregate") {
1836
+ return value.data.flatMap((entry) => this.constantReprs(entry) ?? []);
1837
+ }
1838
+ this.fail(`unknown MIR constant kind '${String(value.kind)}'`);
1839
+ }
1840
+
1841
+ scalarRepr(value) {
1842
+ if (value === null || value === undefined) return null;
1843
+ if (Object.is(value.value, -0)) return "-0";
1844
+ return String(value.value);
1845
+ }
1846
+
1847
+ bufferTypeRepr(buffer, channels) {
1848
+ if (channels.kind === "mono") return `buffer<${buffer.element}>`;
1849
+ if (channels.kind === "static") {
1850
+ return `buffer<${buffer.element}[${channels.count}]>`;
1851
+ }
1852
+ return `buffer<${buffer.element}[]>`;
1853
+ }
1854
+
1855
+ storageShape(typeId) {
1856
+ const type = this.type(typeId);
1857
+ if (type.kind === "scalar") {
1858
+ return { scalar: type.data, length: 1, isArray: false };
1859
+ }
1860
+ if (type.kind === "array") {
1861
+ const element = this.type(type.data.element);
1862
+ if (element.kind !== "scalar") {
1863
+ this.fail("nested aggregate storage metadata is not supported yet");
1864
+ }
1865
+ return { scalar: element.data, length: type.data.len, isArray: true };
1866
+ }
1867
+ if (type.kind === "slice") {
1868
+ return {
1869
+ scalar: type.data.element,
1870
+ length: null,
1871
+ isArray: true,
1872
+ isSlice: true,
1873
+ };
1874
+ }
1875
+ this.fail(`storage metadata for MIR type '${type.kind}' is not supported yet`);
1876
+ }
1877
+
1878
+ bufferChannelMetadata(channels, element) {
1879
+ if (channels === "mono") {
1880
+ return { kind: "mono", count: 1 };
1881
+ }
1882
+ if (channels === "dynamic") {
1883
+ return { kind: "dynamic", count: null };
1884
+ }
1885
+ if (
1886
+ channels &&
1887
+ typeof channels === "object" &&
1888
+ Number.isInteger(channels.static) &&
1889
+ channels.static > 0 &&
1890
+ channels.static <= Math.floor(0x7fffffff / this.scalarSize(element))
1891
+ ) {
1892
+ return { kind: "static", count: channels.static };
1893
+ }
1894
+ this.fail(`invalid MIR buffer channel descriptor '${JSON.stringify(channels)}'`);
1895
+ }
1896
+
1897
+ fail(message) {
1898
+ throw new OndaBinaryenError(message);
1899
+ }
1900
+ }