@onda-lang/wasm-compiler 0.5.4 → 0.7.0

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.
@@ -8,6 +8,8 @@ import {
8
8
  PROCESSOR_ABI_VERSION,
9
9
  PROCESSOR_ARTIFACT_FORMAT,
10
10
  PROCESSOR_ARTIFACT_FORMAT_VERSION,
11
+ PROCESSOR_EXECUTION_OK,
12
+ PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE,
11
13
  PROCESSOR_SNAPSHOT_FORMAT_VERSION,
12
14
  validateProcessorMetadata,
13
15
  } from "./artifact.js";
@@ -17,6 +19,8 @@ export {
17
19
  PROCESSOR_ABI_VERSION,
18
20
  PROCESSOR_ARTIFACT_FORMAT,
19
21
  PROCESSOR_ARTIFACT_FORMAT_VERSION,
22
+ PROCESSOR_EXECUTION_OK,
23
+ PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE,
20
24
  PROCESSOR_SNAPSHOT_FORMAT_VERSION,
21
25
  createProcessorArtifactFiles,
22
26
  loadProcessorArtifactFiles,
@@ -38,6 +42,7 @@ const MAX_MEMORY_PAGES = 65_536;
38
42
  const WASM32_ADDRESS_SPACE_BYTES = MAX_MEMORY_PAGES * PAGE_BYTES;
39
43
  const DEFAULT_OPTIMIZE_LEVEL = 4;
40
44
  const ONDA_PROCESS_FULL_BLOCK = (1 << 0) | (1 << 1);
45
+ const RUNTIME_FAILURE_GLOBAL = "$onda.runtime_failure";
41
46
  const MATH_KERNEL_INTRINSICS = new Set([
42
47
  "sin",
43
48
  "cos",
@@ -59,10 +64,38 @@ const POINTER_GLOBALS = Object.freeze({
59
64
  state: "$onda.state",
60
65
  eventPayload: "$onda.event_payload",
61
66
  buffers: "$onda.buffers",
67
+ bufferWrites: "$onda.buffer_writes",
62
68
  bufferFrames: "$onda.buffer_frames",
63
69
  bufferChannels: "$onda.buffer_channels",
64
70
  bufferSampleRates: "$onda.buffer_sample_rates",
65
71
  });
72
+ const BUFFER_DESCRIPTOR_POINTER_GLOBALS = new Set([
73
+ POINTER_GLOBALS.buffers,
74
+ POINTER_GLOBALS.bufferWrites,
75
+ POINTER_GLOBALS.bufferFrames,
76
+ POINTER_GLOBALS.bufferChannels,
77
+ POINTER_GLOBALS.bufferSampleRates,
78
+ ]);
79
+ const TRAPPING_DESCRIPTOR_UNARY_OPS = new Set([
80
+ binaryen.TruncSFloat32ToInt32,
81
+ binaryen.TruncSFloat32ToInt64,
82
+ binaryen.TruncSFloat64ToInt32,
83
+ binaryen.TruncSFloat64ToInt64,
84
+ binaryen.TruncUFloat32ToInt32,
85
+ binaryen.TruncUFloat32ToInt64,
86
+ binaryen.TruncUFloat64ToInt32,
87
+ binaryen.TruncUFloat64ToInt64,
88
+ ]);
89
+ const TRAPPING_DESCRIPTOR_BINARY_OPS = new Set([
90
+ binaryen.DivSInt32,
91
+ binaryen.DivSInt64,
92
+ binaryen.DivUInt32,
93
+ binaryen.DivUInt64,
94
+ binaryen.RemSInt32,
95
+ binaryen.RemSInt64,
96
+ binaryen.RemUInt32,
97
+ binaryen.RemUInt64,
98
+ ]);
66
99
 
67
100
  // Compiles MIR emitted by Onda's semantic producer. The producer owns proofs
68
101
  // for operations marked `bounds: "unchecked"` and all other validated MIR
@@ -179,6 +212,11 @@ class MirCompiler {
179
212
  ? MATH_KERNEL_RESERVED_END
180
213
  : STATIC_BASE;
181
214
  this.internalHelpers = new Set();
215
+ this.functionMayFail = [];
216
+ this.bufferMayWrite = [];
217
+ this.fallbackBufferReadAddress = 0;
218
+ this.fallbackBufferWriteAddress = 0;
219
+ this.scalarParameterByValue = [];
182
220
  this.nextLabel = 0;
183
221
  }
184
222
 
@@ -208,6 +246,21 @@ class MirCompiler {
208
246
  this.options.allowInliningFunctionsWithLoops,
209
247
  );
210
248
  this.module.optimize();
249
+ if (this.hoistInvariantBufferDescriptorLoads()) {
250
+ // The first rewrite makes descriptor provenance explicit in
251
+ // locals. A small cleanup is enough to expose aliases that were
252
+ // shared by Binaryen's original loop body; one final rewrite then
253
+ // catches those without paying for a second full O4 pipeline.
254
+ this.module.runPasses([
255
+ "simplify-locals",
256
+ "optimize-instructions",
257
+ "coalesce-locals",
258
+ "vacuum",
259
+ ]);
260
+ if (this.hoistInvariantBufferDescriptorLoads()) {
261
+ this.module.runPasses(["vacuum"]);
262
+ }
263
+ }
211
264
  } finally {
212
265
  binaryen.setOptimizeLevel(previousOptimizeLevel);
213
266
  binaryen.setShrinkLevel(previousShrinkLevel);
@@ -238,6 +291,509 @@ class MirCompiler {
238
291
  }
239
292
  }
240
293
 
294
+ hoistInvariantBufferDescriptorLoads() {
295
+ // Binaryen must conservatively assume that arbitrary linear-memory stores
296
+ // can rewrite host descriptor tables. After inlining, recover the stronger
297
+ // processor ABI contract explicitly: descriptor bindings are immutable for
298
+ // one entry-point invocation, so address-invariant loads belong in the loop
299
+ // preheader. Sample-varying addresses remain untouched.
300
+ this.descriptorLoadsHoisted = 0;
301
+ for (let index = 0; index < this.module.getNumFunctions(); index += 1) {
302
+ const func = this.module.getFunctionByIndex(index);
303
+ const body = binaryen.Function.getBody(func);
304
+ // The local-write scan below is part of the safety proof. If Binaryen
305
+ // adds an expression kind that this backend does not know how to walk,
306
+ // leave the whole function untouched rather than silently overlooking
307
+ // a nested local.tee.
308
+ if (!this.visitExpression(body, () => {})) continue;
309
+ const rewritten = this.rewriteDescriptorLoops(body, func);
310
+ if (rewritten !== body) binaryen.Function.setBody(func, rewritten);
311
+ }
312
+ return this.descriptorLoadsHoisted > 0;
313
+ }
314
+
315
+ rewriteDescriptorLoops(expression, func) {
316
+ this.rewriteExpressionChildren(expression, (child) =>
317
+ this.rewriteDescriptorLoops(child, func)
318
+ );
319
+ if (binaryen.getExpressionInfo(expression).id !== binaryen.LoopId) {
320
+ return expression;
321
+ }
322
+
323
+ const body = binaryen.Loop.getBody(expression);
324
+ const controlPaths = this.descriptorControlPaths(body);
325
+ const definitions = new Map();
326
+ const writtenLocals = new Set();
327
+ this.visitExpression(body, (candidate) => {
328
+ const info = binaryen.getExpressionInfo(candidate);
329
+ if (info.id !== binaryen.LocalSetId) return;
330
+ writtenLocals.add(info.index);
331
+ const entries = definitions.get(info.index) ?? [];
332
+ entries.push(info.value);
333
+ definitions.set(info.index, entries);
334
+ });
335
+ const initializers = [];
336
+ // A pointer-local tee can expose the same invariant address to later
337
+ // descriptor loads. Cache its value in a fresh local: assigning the
338
+ // original local in the preheader would change loop-entry semantics.
339
+ const loopLocalCaches = new Map();
340
+
341
+ const rewriteLoad = (candidate) => {
342
+ this.rewriteExpressionChildren(candidate, rewriteLoad);
343
+ const info = binaryen.getExpressionInfo(candidate);
344
+ if (info.id !== binaryen.LoadId || info.isAtomic) return candidate;
345
+ const candidatePath = controlPaths.get(candidate) ?? [];
346
+ const localCache = (local) => {
347
+ const cache = loopLocalCaches.get(local);
348
+ return cache && this.descriptorPathDominates(cache.path, candidatePath)
349
+ ? cache
350
+ : null;
351
+ };
352
+ if (!this.descriptorPointerExpression(
353
+ info.ptr,
354
+ definitions,
355
+ (local) => !writtenLocals.has(local) || localCache(local) !== null,
356
+ )) {
357
+ return candidate;
358
+ }
359
+ this.cacheDescriptorPointerSideEffects(
360
+ info.ptr,
361
+ func,
362
+ initializers,
363
+ loopLocalCaches,
364
+ controlPaths,
365
+ candidatePath,
366
+ );
367
+ // Binaryen exposes FunctionAddVar through its generated C-API surface,
368
+ // but not through the small Function convenience wrapper.
369
+ const cache = binaryen._BinaryenFunctionAddVar(func, info.type);
370
+ this.descriptorLoadsHoisted += 1;
371
+ const hoistedLoad = this.module.copyExpression(candidate);
372
+ const hoistedInfo = binaryen.getExpressionInfo(hoistedLoad);
373
+ binaryen.Load.setPtr(
374
+ hoistedLoad,
375
+ this.descriptorPointerForPreheader(
376
+ hoistedInfo.ptr,
377
+ loopLocalCaches,
378
+ candidatePath,
379
+ ),
380
+ );
381
+ initializers.push(this.module.local.set(cache, hoistedLoad));
382
+ const sideEffects = this.descriptorPointerSideEffects(info.ptr);
383
+ const value = this.module.local.get(cache, info.type);
384
+ return sideEffects.length === 0
385
+ ? value
386
+ : this.module.block(null, [...sideEffects, value], info.type);
387
+ };
388
+ const rewrittenBody = rewriteLoad(body);
389
+ if (rewrittenBody !== body) binaryen.Loop.setBody(expression, rewrittenBody);
390
+ return initializers.length === 0
391
+ ? expression
392
+ : this.module.block(null, [...initializers, expression]);
393
+ }
394
+
395
+ descriptorControlPaths(expression) {
396
+ const paths = new Map();
397
+ const visit = (candidate, path) => {
398
+ paths.set(candidate, path);
399
+ const info = binaryen.getExpressionInfo(candidate);
400
+ if (info.id === binaryen.IfId) {
401
+ visit(info.condition, path);
402
+ visit(info.ifTrue, [...path, `if:${candidate}:true`]);
403
+ if (info.ifFalse) {
404
+ visit(info.ifFalse, [...path, `if:${candidate}:false`]);
405
+ }
406
+ return;
407
+ }
408
+ if (info.id === binaryen.LoopId) {
409
+ visit(info.body, [...path, `loop:${candidate}`]);
410
+ return;
411
+ }
412
+ this.rewriteExpressionChildren(candidate, (child) => {
413
+ visit(child, path);
414
+ return child;
415
+ });
416
+ };
417
+ visit(expression, []);
418
+ return paths;
419
+ }
420
+
421
+ descriptorPathDominates(dominator, candidate) {
422
+ // Rewrite traversal is in evaluation order, so an available cache is
423
+ // earlier than the candidate. The path prefix additionally proves that
424
+ // it was not produced only in a sibling branch or nested loop.
425
+ return dominator.length <= candidate.length
426
+ && dominator.every((entry, index) => entry === candidate[index]);
427
+ }
428
+
429
+ cacheDescriptorPointerSideEffects(
430
+ expression,
431
+ func,
432
+ initializers,
433
+ loopLocalCaches,
434
+ controlPaths,
435
+ candidatePath,
436
+ ) {
437
+ const info = binaryen.getExpressionInfo(expression);
438
+ if (info.id === binaryen.LocalSetId && info.isTee) {
439
+ this.cacheDescriptorPointerSideEffects(
440
+ info.value,
441
+ func,
442
+ initializers,
443
+ loopLocalCaches,
444
+ controlPaths,
445
+ candidatePath,
446
+ );
447
+ const cache = binaryen._BinaryenFunctionAddVar(func, info.type);
448
+ const value = this.descriptorPointerForPreheader(
449
+ this.module.copyExpression(info.value),
450
+ loopLocalCaches,
451
+ candidatePath,
452
+ );
453
+ initializers.push(this.module.local.set(cache, value));
454
+ loopLocalCaches.set(info.index, {
455
+ index: cache,
456
+ type: info.type,
457
+ path: controlPaths.get(expression) ?? candidatePath,
458
+ });
459
+ return;
460
+ }
461
+ if (info.id === binaryen.UnaryId) {
462
+ this.cacheDescriptorPointerSideEffects(
463
+ info.value,
464
+ func,
465
+ initializers,
466
+ loopLocalCaches,
467
+ controlPaths,
468
+ candidatePath,
469
+ );
470
+ return;
471
+ }
472
+ if (info.id === binaryen.BinaryId) {
473
+ for (const child of [info.left, info.right]) {
474
+ this.cacheDescriptorPointerSideEffects(
475
+ child,
476
+ func,
477
+ initializers,
478
+ loopLocalCaches,
479
+ controlPaths,
480
+ candidatePath,
481
+ );
482
+ }
483
+ }
484
+ }
485
+
486
+ descriptorPointerForPreheader(expression, loopLocalCaches, candidatePath) {
487
+ this.rewriteExpressionChildren(expression, (child) =>
488
+ this.descriptorPointerForPreheader(
489
+ child,
490
+ loopLocalCaches,
491
+ candidatePath,
492
+ )
493
+ );
494
+ const info = binaryen.getExpressionInfo(expression);
495
+ if (info.id === binaryen.LocalGetId) {
496
+ const cache = loopLocalCaches.get(info.index);
497
+ if (cache && this.descriptorPathDominates(cache.path, candidatePath)) {
498
+ return this.module.local.get(cache.index, cache.type);
499
+ }
500
+ }
501
+ if (info.id === binaryen.LocalSetId && info.isTee) {
502
+ const cache = loopLocalCaches.get(info.index);
503
+ if (cache && this.descriptorPathDominates(cache.path, candidatePath)) {
504
+ return this.module.local.get(cache.index, cache.type);
505
+ }
506
+ return info.value;
507
+ }
508
+ return expression;
509
+ }
510
+
511
+ descriptorPointerExpression(expression, definitions, localIsInvariant) {
512
+ if (!this.expressionUsesDescriptorTable(expression, definitions, new Set())) {
513
+ return false;
514
+ }
515
+ const visit = (candidate) => {
516
+ const info = binaryen.getExpressionInfo(candidate);
517
+ if (info.id === binaryen.ConstId) return true;
518
+ if (info.id === binaryen.LocalGetId) return localIsInvariant(info.index);
519
+ if (info.id === binaryen.GlobalGetId) {
520
+ if (BUFFER_DESCRIPTOR_POINTER_GLOBALS.has(info.name)) {
521
+ return true;
522
+ }
523
+ return false;
524
+ }
525
+ if (info.id === binaryen.LocalSetId && info.isTee) {
526
+ return visit(info.value);
527
+ }
528
+ if (info.id === binaryen.UnaryId) {
529
+ return !TRAPPING_DESCRIPTOR_UNARY_OPS.has(info.op)
530
+ && visit(info.value);
531
+ }
532
+ if (info.id === binaryen.BinaryId) {
533
+ return !TRAPPING_DESCRIPTOR_BINARY_OPS.has(info.op)
534
+ && visit(info.left)
535
+ && visit(info.right);
536
+ }
537
+ if (info.id === binaryen.SelectId) {
538
+ if (
539
+ this.expressionContainsTee(info.condition)
540
+ || this.expressionContainsTee(info.ifTrue)
541
+ || this.expressionContainsTee(info.ifFalse)
542
+ ) {
543
+ return false;
544
+ }
545
+ return visit(info.condition) && visit(info.ifTrue) && visit(info.ifFalse);
546
+ }
547
+ return false;
548
+ };
549
+ return visit(expression);
550
+ }
551
+
552
+ expressionUsesDescriptorTable(expression, definitions, visitingLocals) {
553
+ const info = binaryen.getExpressionInfo(expression);
554
+ if (info.id === binaryen.GlobalGetId) {
555
+ return BUFFER_DESCRIPTOR_POINTER_GLOBALS.has(info.name);
556
+ }
557
+ if (info.id === binaryen.LocalGetId) {
558
+ const values = definitions.get(info.index);
559
+ if (
560
+ !values
561
+ || values.length !== 1
562
+ || visitingLocals.has(info.index)
563
+ ) {
564
+ return false;
565
+ }
566
+ visitingLocals.add(info.index);
567
+ const result = this.expressionUsesDescriptorTable(
568
+ values[0],
569
+ definitions,
570
+ visitingLocals,
571
+ );
572
+ visitingLocals.delete(info.index);
573
+ return result;
574
+ }
575
+ if (info.id === binaryen.LocalSetId && info.isTee) {
576
+ return this.expressionUsesDescriptorTable(
577
+ info.value,
578
+ definitions,
579
+ visitingLocals,
580
+ );
581
+ }
582
+ if (info.id === binaryen.UnaryId) {
583
+ return this.expressionUsesDescriptorTable(
584
+ info.value,
585
+ definitions,
586
+ visitingLocals,
587
+ );
588
+ }
589
+ if (info.id === binaryen.BinaryId) {
590
+ return this.expressionUsesDescriptorTable(
591
+ info.left,
592
+ definitions,
593
+ visitingLocals,
594
+ ) || this.expressionUsesDescriptorTable(
595
+ info.right,
596
+ definitions,
597
+ visitingLocals,
598
+ );
599
+ }
600
+ if (info.id === binaryen.SelectId) {
601
+ return this.expressionUsesDescriptorTable(
602
+ info.condition,
603
+ definitions,
604
+ visitingLocals,
605
+ ) || this.expressionUsesDescriptorTable(
606
+ info.ifTrue,
607
+ definitions,
608
+ visitingLocals,
609
+ ) || this.expressionUsesDescriptorTable(
610
+ info.ifFalse,
611
+ definitions,
612
+ visitingLocals,
613
+ );
614
+ }
615
+ return false;
616
+ }
617
+
618
+ descriptorPointerSideEffects(expression) {
619
+ const result = [];
620
+ const visit = (candidate) => {
621
+ const info = binaryen.getExpressionInfo(candidate);
622
+ if (info.id === binaryen.LocalSetId && info.isTee) {
623
+ result.push(
624
+ this.module.local.set(info.index, this.module.copyExpression(info.value)),
625
+ );
626
+ } else if (info.id === binaryen.UnaryId) {
627
+ visit(info.value);
628
+ } else if (info.id === binaryen.BinaryId) {
629
+ visit(info.left);
630
+ visit(info.right);
631
+ } else if (info.id === binaryen.SelectId) {
632
+ // Pointer selectors generated by this backend are side-effect free.
633
+ // A nested tee would need conditional reconstruction, so leave it to
634
+ // the conservative invariance check instead of moving it here.
635
+ }
636
+ };
637
+ visit(expression);
638
+ return result;
639
+ }
640
+
641
+ expressionContainsTee(expression) {
642
+ const info = binaryen.getExpressionInfo(expression);
643
+ if (info.id === binaryen.LocalSetId) return info.isTee;
644
+ if (info.id === binaryen.UnaryId) {
645
+ return this.expressionContainsTee(info.value);
646
+ }
647
+ if (info.id === binaryen.BinaryId) {
648
+ return this.expressionContainsTee(info.left)
649
+ || this.expressionContainsTee(info.right);
650
+ }
651
+ if (info.id === binaryen.SelectId) {
652
+ return this.expressionContainsTee(info.condition)
653
+ || this.expressionContainsTee(info.ifTrue)
654
+ || this.expressionContainsTee(info.ifFalse);
655
+ }
656
+ return false;
657
+ }
658
+
659
+ visitExpression(expression, visitor) {
660
+ visitor(expression);
661
+ let complete = true;
662
+ const supported = this.rewriteExpressionChildren(expression, (child) => {
663
+ if (!this.visitExpression(child, visitor)) complete = false;
664
+ return child;
665
+ });
666
+ return complete && supported;
667
+ }
668
+
669
+ rewriteExpressionChildren(expression, rewrite) {
670
+ const info = binaryen.getExpressionInfo(expression);
671
+ const replace = (child, setter) => {
672
+ if (child) setter(rewrite(child));
673
+ };
674
+ switch (info.id) {
675
+ case binaryen.BlockId:
676
+ info.children.forEach((child, index) =>
677
+ replace(child, (value) => binaryen.Block.setChildAt(expression, index, value))
678
+ );
679
+ break;
680
+ case binaryen.IfId:
681
+ replace(info.condition, (value) => binaryen.If.setCondition(expression, value));
682
+ replace(info.ifTrue, (value) => binaryen.If.setIfTrue(expression, value));
683
+ replace(info.ifFalse, (value) => binaryen.If.setIfFalse(expression, value));
684
+ break;
685
+ case binaryen.LoopId:
686
+ replace(info.body, (value) => binaryen.Loop.setBody(expression, value));
687
+ break;
688
+ case binaryen.BreakId:
689
+ replace(info.condition, (value) => binaryen.Break.setCondition(expression, value));
690
+ replace(info.value, (value) => binaryen.Break.setValue(expression, value));
691
+ break;
692
+ case binaryen.SwitchId:
693
+ replace(info.condition, (value) => binaryen.Switch.setCondition(expression, value));
694
+ replace(info.value, (value) => binaryen.Switch.setValue(expression, value));
695
+ break;
696
+ case binaryen.CallId:
697
+ info.operands.forEach((child, index) =>
698
+ replace(child, (value) => binaryen.Call.setOperandAt(expression, index, value))
699
+ );
700
+ break;
701
+ case binaryen.CallIndirectId:
702
+ replace(info.target, (value) => binaryen.CallIndirect.setTarget(expression, value));
703
+ info.operands.forEach((child, index) =>
704
+ replace(child, (value) => binaryen.CallIndirect.setOperandAt(expression, index, value))
705
+ );
706
+ break;
707
+ case binaryen.LocalSetId:
708
+ replace(info.value, (value) => binaryen.LocalSet.setValue(expression, value));
709
+ break;
710
+ case binaryen.GlobalSetId:
711
+ replace(info.value, (value) => binaryen.GlobalSet.setValue(expression, value));
712
+ break;
713
+ case binaryen.LoadId:
714
+ replace(info.ptr, (value) => binaryen.Load.setPtr(expression, value));
715
+ break;
716
+ case binaryen.StoreId:
717
+ replace(info.ptr, (value) => binaryen.Store.setPtr(expression, value));
718
+ replace(info.value, (value) => binaryen.Store.setValue(expression, value));
719
+ break;
720
+ case binaryen.UnaryId:
721
+ replace(info.value, (value) => binaryen.Unary.setValue(expression, value));
722
+ break;
723
+ case binaryen.BinaryId:
724
+ replace(info.left, (value) => binaryen.Binary.setLeft(expression, value));
725
+ replace(info.right, (value) => binaryen.Binary.setRight(expression, value));
726
+ break;
727
+ case binaryen.SelectId:
728
+ replace(info.ifTrue, (value) => binaryen.Select.setIfTrue(expression, value));
729
+ replace(info.ifFalse, (value) => binaryen.Select.setIfFalse(expression, value));
730
+ replace(info.condition, (value) => binaryen.Select.setCondition(expression, value));
731
+ break;
732
+ case binaryen.DropId:
733
+ replace(info.value, (value) => binaryen.Drop.setValue(expression, value));
734
+ break;
735
+ case binaryen.ReturnId:
736
+ replace(info.value, (value) => binaryen.Return.setValue(expression, value));
737
+ break;
738
+ case binaryen.MemoryCopyId:
739
+ replace(info.dest, (value) => binaryen.MemoryCopy.setDest(expression, value));
740
+ replace(info.source, (value) => binaryen.MemoryCopy.setSource(expression, value));
741
+ replace(info.size, (value) => binaryen.MemoryCopy.setSize(expression, value));
742
+ break;
743
+ case binaryen.MemoryFillId:
744
+ replace(info.dest, (value) => binaryen.MemoryFill.setDest(expression, value));
745
+ replace(info.value, (value) => binaryen.MemoryFill.setValue(expression, value));
746
+ replace(info.size, (value) => binaryen.MemoryFill.setSize(expression, value));
747
+ break;
748
+ case binaryen.SIMDExtractId:
749
+ replace(info.vec, (value) => binaryen.SIMDExtract.setVec(expression, value));
750
+ break;
751
+ case binaryen.SIMDReplaceId:
752
+ replace(info.vec, (value) => binaryen.SIMDReplace.setVec(expression, value));
753
+ replace(info.value, (value) => binaryen.SIMDReplace.setValue(expression, value));
754
+ break;
755
+ case binaryen.SIMDShuffleId:
756
+ replace(info.left, (value) => binaryen.SIMDShuffle.setLeft(expression, value));
757
+ replace(info.right, (value) => binaryen.SIMDShuffle.setRight(expression, value));
758
+ break;
759
+ case binaryen.SIMDTernaryId:
760
+ replace(info.a, (value) => binaryen.SIMDTernary.setA(expression, value));
761
+ replace(info.b, (value) => binaryen.SIMDTernary.setB(expression, value));
762
+ replace(info.c, (value) => binaryen.SIMDTernary.setC(expression, value));
763
+ break;
764
+ case binaryen.SIMDShiftId:
765
+ replace(info.vec, (value) => binaryen.SIMDShift.setVec(expression, value));
766
+ replace(info.shift, (value) => binaryen.SIMDShift.setShift(expression, value));
767
+ break;
768
+ case binaryen.SIMDLoadId:
769
+ replace(info.ptr, (value) => binaryen.SIMDLoad.setPtr(expression, value));
770
+ break;
771
+ case binaryen.SIMDLoadStoreLaneId:
772
+ replace(info.ptr, (value) => binaryen.SIMDLoadStoreLane.setPtr(expression, value));
773
+ replace(info.vec, (value) => binaryen.SIMDLoadStoreLane.setVec(expression, value));
774
+ break;
775
+ case binaryen.TupleMakeId:
776
+ info.operands.forEach((child, index) =>
777
+ replace(child, (value) => binaryen.TupleMake.setOperandAt(expression, index, value))
778
+ );
779
+ break;
780
+ case binaryen.TupleExtractId:
781
+ replace(info.tuple, (value) => binaryen.TupleExtract.setTuple(expression, value));
782
+ break;
783
+ case binaryen.ConstId:
784
+ case binaryen.LocalGetId:
785
+ case binaryen.GlobalGetId:
786
+ case binaryen.NopId:
787
+ case binaryen.UnreachableId:
788
+ case binaryen.MemorySizeId:
789
+ case binaryen.DataDropId:
790
+ break;
791
+ default:
792
+ return false;
793
+ }
794
+ return true;
795
+ }
796
+
241
797
  validateEnvelope() {
242
798
  const mir = this.mir;
243
799
  if (!mir || typeof mir !== "object" || Array.isArray(mir)) {
@@ -297,6 +853,127 @@ class MirCompiler {
297
853
  this.validateCurrentSchemaEnvelope();
298
854
  this.validateProcessEntrySignature();
299
855
  this.validateAcyclicCallGraph();
856
+ this.analyzeBufferWrites();
857
+ this.analyzeRecoverableFailures();
858
+ this.analyzeScalarReferenceParameters();
859
+ }
860
+
861
+ analyzeScalarReferenceParameters() {
862
+ // MIR reference modes are conservative. Internal scalar references that
863
+ // are never written, never escape to a writable reference, and never alias
864
+ // a writable argument can safely use a value ABI. This removes scratch
865
+ // memory traffic and exposes their values to post-inlining loop analysis.
866
+ const candidates = this.mir.functions.map((func) =>
867
+ func.params.map((parameter) => {
868
+ const type = this.type(parameter.ty);
869
+ return type.kind === "scalar" && parameter.mode !== "value";
870
+ })
871
+ );
872
+ const callSites = this.mir.functions.map(() => []);
873
+
874
+ const visitBlock = (functionId, block) => {
875
+ for (const statement of block.statements) {
876
+ const kind = statement.kind?.kind;
877
+ const data = statement.kind?.data;
878
+ if (
879
+ kind === "assign"
880
+ && data.destination?.base?.kind === "parameter"
881
+ && data.destination.projections.length === 0
882
+ ) {
883
+ candidates[functionId][data.destination.base.data] = false;
884
+ } else if (kind === "call") {
885
+ callSites[data.function].push({ caller: functionId, call: data });
886
+ } else if (kind === "if") {
887
+ visitBlock(functionId, data.then_block);
888
+ visitBlock(functionId, data.else_block);
889
+ } else if (kind === "loop") {
890
+ visitBlock(functionId, data.body);
891
+ }
892
+ }
893
+ };
894
+ for (const [functionId, func] of this.mir.functions.entries()) {
895
+ visitBlock(functionId, func.body);
896
+ }
897
+
898
+ const unprojectedScalarPlace = (argument, functionId) => {
899
+ if (
900
+ argument?.kind !== "place"
901
+ || argument.data.projections.length !== 0
902
+ || !["local", "parameter"].includes(argument.data.base.kind)
903
+ ) {
904
+ return null;
905
+ }
906
+ const typeId = argument.data.base.kind === "local"
907
+ ? this.mir.functions[functionId].locals[argument.data.base.data]?.ty
908
+ : this.mir.functions[functionId].params[argument.data.base.data]?.ty;
909
+ return Number.isInteger(typeId) && this.type(typeId).kind === "scalar"
910
+ ? argument.data.base
911
+ : null;
912
+ };
913
+ const sameBase = (lhs, rhs) =>
914
+ lhs?.kind === rhs?.kind && lhs?.data === rhs?.data;
915
+
916
+ let changed = true;
917
+ while (changed) {
918
+ changed = false;
919
+ for (const [calleeId, sites] of callSites.entries()) {
920
+ for (const { caller: callerId, call } of sites) {
921
+ for (let parameterId = 0; parameterId < candidates[calleeId].length; parameterId += 1) {
922
+ if (!candidates[calleeId][parameterId]) continue;
923
+ const base = unprojectedScalarPlace(call.args[parameterId], callerId);
924
+ const forwardedCandidate =
925
+ base?.kind !== "parameter"
926
+ || candidates[callerId][base.data];
927
+ const aliasesWritableArgument = call.args.some((argument, index) => {
928
+ if (index === parameterId || candidates[calleeId][index]) return false;
929
+ return sameBase(
930
+ base,
931
+ unprojectedScalarPlace(argument, callerId),
932
+ );
933
+ });
934
+ if (!base || !forwardedCandidate || aliasesWritableArgument) {
935
+ candidates[calleeId][parameterId] = false;
936
+ changed = true;
937
+ }
938
+ }
939
+ }
940
+ }
941
+
942
+ for (const [callerId, func] of this.mir.functions.entries()) {
943
+ const visitCalls = (block) => {
944
+ for (const statement of block.statements) {
945
+ const kind = statement.kind?.kind;
946
+ const data = statement.kind?.data;
947
+ if (kind === "call") {
948
+ data.args.forEach((argument, parameterId) => {
949
+ const base = unprojectedScalarPlace(argument, callerId);
950
+ if (
951
+ base?.kind === "parameter"
952
+ && candidates[callerId][base.data]
953
+ && !candidates[data.function][parameterId]
954
+ ) {
955
+ candidates[callerId][base.data] = false;
956
+ changed = true;
957
+ }
958
+ });
959
+ } else if (kind === "if") {
960
+ visitCalls(data.then_block);
961
+ visitCalls(data.else_block);
962
+ } else if (kind === "loop") {
963
+ visitCalls(data.body);
964
+ }
965
+ }
966
+ };
967
+ visitCalls(func.body);
968
+ }
969
+ }
970
+ this.scalarParameterByValue = candidates;
971
+ }
972
+
973
+ parameterPassingMode(functionId, parameterId) {
974
+ return this.scalarParameterByValue[functionId]?.[parameterId]
975
+ ? "value"
976
+ : this.mir.functions[functionId].params[parameterId].mode;
300
977
  }
301
978
 
302
979
  validateCurrentSchemaEnvelope() {
@@ -444,39 +1121,499 @@ class MirCompiler {
444
1121
  }
445
1122
  }
446
1123
 
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",
1124
+ analyzeBufferWrites() {
1125
+ const bufferOrigin = (id) => `buffer:${id}`;
1126
+ const parameterOrigin = (id, slot = 0) => `parameter:${id}:${slot}`;
1127
+ const selectedSlots = (selector, len, bounds) => {
1128
+ if (!Number.isInteger(len) || len <= 0) return [];
1129
+ if (
1130
+ selector?.kind === "constant"
1131
+ && selector.data?.type === "i32"
1132
+ && Number.isInteger(selector.data.value)
1133
+ ) {
1134
+ let slot = selector.data.value;
1135
+ if (bounds === "clamp") {
1136
+ slot = Math.min(len - 1, Math.max(0, slot));
1137
+ return [slot];
1138
+ }
1139
+ if (slot >= 0 && slot < len) return [slot];
1140
+ }
1141
+ return Array.from({ length: len }, (_, slot) => slot);
1142
+ };
1143
+ const bufferIds = (bufferRef) => {
1144
+ if (Number.isInteger(bufferRef)) return [bufferRef];
1145
+ if (bufferRef?.kind === "direct" && Number.isInteger(bufferRef.data)) {
1146
+ return [bufferRef.data];
1147
+ }
1148
+ if (
1149
+ bufferRef?.kind === "array_element"
1150
+ && Number.isInteger(bufferRef.data?.first)
1151
+ && Number.isInteger(bufferRef.data?.len)
1152
+ && bufferRef.data.len > 0
1153
+ ) {
1154
+ return selectedSlots(
1155
+ bufferRef.data.selector,
1156
+ bufferRef.data.len,
1157
+ bufferRef.data.bounds,
1158
+ ).map(
1159
+ (slot) => bufferRef.data.first + slot,
1160
+ );
1161
+ }
1162
+ return [];
1163
+ };
1164
+ const bufferOrigins = (bufferRef) =>
1165
+ new Set(bufferIds(bufferRef).map(bufferOrigin));
1166
+ const bufferParamSlots = (parameterRef, func) => {
1167
+ if (Number.isInteger(parameterRef)) {
1168
+ return [{ parameter: parameterRef, slot: 0 }];
1169
+ }
1170
+ if (
1171
+ parameterRef?.kind === "direct"
1172
+ && Number.isInteger(parameterRef.data)
1173
+ ) {
1174
+ return [{ parameter: parameterRef.data, slot: 0 }];
1175
+ }
1176
+ if (
1177
+ parameterRef?.kind === "array_element"
1178
+ && Number.isInteger(parameterRef.data?.span)
1179
+ ) {
1180
+ const parameter = func.params?.[parameterRef.data.span];
1181
+ const type = parameter && this.type(parameter.ty);
1182
+ const len = type?.kind === "buffer_span" ? type.data.len : 1;
1183
+ return selectedSlots(
1184
+ parameterRef.data.selector,
1185
+ len,
1186
+ parameterRef.data.bounds,
1187
+ ).map((slot) => ({ parameter: parameterRef.data.span, slot }));
1188
+ }
1189
+ return [];
1190
+ };
1191
+ const bufferParamOrigins = (parameterRef, func) => new Set(
1192
+ bufferParamSlots(parameterRef, func).map(({ parameter, slot }) =>
1193
+ parameterOrigin(parameter, slot)
1194
+ ),
463
1195
  );
464
- for (const [eventId, layout] of this.eventLayout.entries()) {
465
- this.requireWasm32Extent(
466
- layout.minimumByteLength,
467
- `MIR event ${eventId} fixed payload storage`,
468
- );
469
- }
1196
+ const localId = (value) =>
1197
+ value?.kind === "local" && Number.isInteger(value.data)
1198
+ ? value.data
1199
+ : null;
1200
+ const setEquals = (lhs, rhs) =>
1201
+ lhs.size === rhs.size && [...lhs].every((entry) => rhs.has(entry));
1202
+ const summaryEquals = (lhs, rhs) =>
1203
+ setEquals(lhs.buffers, rhs.buffers)
1204
+ && setEquals(lhs.parameters, rhs.parameters);
1205
+
1206
+ const valueOrigins = (value, aliases) => {
1207
+ const id = localId(value);
1208
+ return id === null ? new Set() : new Set(aliases[id] ?? []);
1209
+ };
1210
+ const placeOrigins = (place, aliases) => {
1211
+ const base = place?.base;
1212
+ if (base?.kind === "parameter" && Number.isInteger(base.data)) {
1213
+ return new Set([parameterOrigin(base.data, 0)]);
1214
+ }
1215
+ if (base?.kind === "local" && Number.isInteger(base.data)) {
1216
+ return new Set(aliases[base.data] ?? []);
1217
+ }
1218
+ return new Set();
1219
+ };
1220
+ const rvalueOrigins = (value, aliases, func) => {
1221
+ if (value?.kind === "use") {
1222
+ return valueOrigins(value.data, aliases);
1223
+ }
1224
+ if (value?.kind === "load") {
1225
+ return placeOrigins(value.data, aliases);
1226
+ }
1227
+ if (value?.kind !== "make_slice") {
1228
+ return new Set();
1229
+ }
1230
+ const source = value.data?.source;
1231
+ if (source?.kind === "buffer") {
1232
+ return bufferOrigins(source.data?.buffer);
1233
+ }
1234
+ if (source?.kind === "buffer_param") {
1235
+ return bufferParamOrigins(source.data?.parameter, func);
1236
+ }
1237
+ if (source?.kind === "place") {
1238
+ return placeOrigins(source.data, aliases);
1239
+ }
1240
+ return new Set();
1241
+ };
1242
+ const collectAliases = (func) => {
1243
+ const aliases = (func.locals ?? []).map(() => new Set());
1244
+ let changed = true;
1245
+ const visitBlock = (block) => {
1246
+ for (const statement of block?.statements ?? []) {
1247
+ const kind = statement.kind?.kind;
1248
+ const data = statement.kind?.data;
1249
+ if (
1250
+ kind === "assign"
1251
+ && data?.destination?.projections?.length === 0
1252
+ && data.destination.base?.kind === "local"
1253
+ ) {
1254
+ const destination = data.destination.base.data;
1255
+ const origins = rvalueOrigins(data.value, aliases, func);
1256
+ for (const origin of origins) {
1257
+ if (!aliases[destination].has(origin)) {
1258
+ aliases[destination].add(origin);
1259
+ changed = true;
1260
+ }
1261
+ }
1262
+ } else if (kind === "if") {
1263
+ visitBlock(data?.then_block);
1264
+ visitBlock(data?.else_block);
1265
+ } else if (kind === "loop") {
1266
+ visitBlock(data?.body);
1267
+ }
1268
+ }
1269
+ };
1270
+ while (changed) {
1271
+ changed = false;
1272
+ visitBlock(func.body);
1273
+ }
1274
+ return aliases;
1275
+ };
1276
+ const collectUnsupportedResults = (func) => {
1277
+ const results = new Set();
1278
+ const visitBlock = (block) => {
1279
+ for (const statement of block?.statements ?? []) {
1280
+ const kind = statement.kind?.kind;
1281
+ const data = statement.kind?.data;
1282
+ if (kind === "call") {
1283
+ for (const result of data?.results ?? []) {
1284
+ const type = this.mir.types[func.locals?.[result]?.ty];
1285
+ if (type?.kind === "slice" || type?.kind === "buffer") {
1286
+ results.add(result);
1287
+ }
1288
+ }
1289
+ } else if (kind === "if") {
1290
+ visitBlock(data?.then_block);
1291
+ visitBlock(data?.else_block);
1292
+ } else if (kind === "loop") {
1293
+ visitBlock(data?.body);
1294
+ }
1295
+ }
1296
+ };
1297
+ visitBlock(func.body);
1298
+ return results;
1299
+ };
1300
+ const argumentValue = (argument) => {
1301
+ switch (argument?.kind) {
1302
+ case "value": return argument.data;
1303
+ case "slice_element":
1304
+ case "slice_window": return argument.data?.slice;
1305
+ default: return null;
1306
+ }
1307
+ };
1308
+ const argumentUsesUnsupportedResult = (argument, unsupported) => {
1309
+ const value = argumentValue(argument);
1310
+ const valueLocal = localId(value);
1311
+ if (valueLocal !== null) {
1312
+ return unsupported.has(valueLocal);
1313
+ }
1314
+ if (argument?.kind === "place") {
1315
+ const base = argument.data?.base;
1316
+ return base?.kind === "local" && unsupported.has(base.data);
1317
+ }
1318
+ if (argument?.kind === "array_window") {
1319
+ const base = argument.data?.array?.base;
1320
+ return base?.kind === "local" && unsupported.has(base.data);
1321
+ }
1322
+ return false;
1323
+ };
1324
+ const argumentOrigins = (argument, aliases, func, slot) => {
1325
+ switch (argument?.kind) {
1326
+ case "buffer":
1327
+ return bufferOrigins(argument.data);
1328
+ case "buffer_param":
1329
+ return bufferParamOrigins(argument.data, func);
1330
+ case "buffer_span": {
1331
+ const span = argument.data;
1332
+ if (span?.kind === "interface") {
1333
+ const len = span.data?.len ?? 0;
1334
+ if (Number.isInteger(slot) && slot >= 0 && slot < len) {
1335
+ return new Set([bufferOrigin(span.data.first + slot)]);
1336
+ }
1337
+ return new Set(Array.from({ length: len }, (_, index) =>
1338
+ bufferOrigin(span.data.first + index)
1339
+ ));
1340
+ }
1341
+ if (span?.kind === "parameter") {
1342
+ const start = span.data?.start ?? 0;
1343
+ const len = span.data?.len ?? 0;
1344
+ if (Number.isInteger(slot) && slot >= 0 && slot < len) {
1345
+ return new Set([parameterOrigin(span.data.span, start + slot)]);
1346
+ }
1347
+ return new Set(Array.from({ length: len }, (_, index) =>
1348
+ parameterOrigin(span.data.span, start + index)
1349
+ ));
1350
+ }
1351
+ return new Set();
1352
+ }
1353
+ case "place":
1354
+ return placeOrigins(argument.data, aliases);
1355
+ case "array_window":
1356
+ return placeOrigins(argument.data?.array, aliases);
1357
+ case "value":
1358
+ return valueOrigins(argument.data, aliases);
1359
+ case "slice_element":
1360
+ case "slice_window":
1361
+ return valueOrigins(argument.data?.slice, aliases);
1362
+ default:
1363
+ return new Set();
1364
+ }
1365
+ };
1366
+ const markOrigins = (origins, summary) => {
1367
+ for (const origin of origins) {
1368
+ const [kind, idText] = origin.split(":");
1369
+ const id = Number(idText);
1370
+ if (kind === "buffer") {
1371
+ summary.buffers.add(id);
1372
+ } else if (kind === "parameter") {
1373
+ summary.parameters.add(origin);
1374
+ }
1375
+ }
1376
+ };
470
1377
 
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),
1378
+ const aliases = this.mir.functions.map(collectAliases);
1379
+ const unsupported = this.mir.functions.map(collectUnsupportedResults);
1380
+ let summaries = this.mir.functions.map(() => ({
1381
+ buffers: new Set(),
1382
+ parameters: new Set(),
1383
+ }));
1384
+ while (true) {
1385
+ const next = this.mir.functions.map((func, functionId) => {
1386
+ const summary = { buffers: new Set(), parameters: new Set() };
1387
+ const markValueWrite = (value, description) => {
1388
+ const id = localId(value);
1389
+ if (id !== null && unsupported[functionId].has(id)) {
1390
+ this.fail(
1391
+ `cannot infer interface-buffer writes for ${description} through a slice returned by a MIR call`,
1392
+ );
1393
+ }
1394
+ markOrigins(valueOrigins(value, aliases[functionId]), summary);
1395
+ };
1396
+ const visitBlock = (block) => {
1397
+ for (const statement of block?.statements ?? []) {
1398
+ const kind = statement.kind?.kind;
1399
+ const data = statement.kind?.data;
1400
+ if (
1401
+ kind === "assign"
1402
+ && data?.destination?.base?.kind === "parameter"
1403
+ ) {
1404
+ summary.parameters.add(parameterOrigin(data.destination.base.data, 0));
1405
+ } else if (kind === "buffer_store") {
1406
+ for (const buffer of bufferIds(data?.buffer)) {
1407
+ summary.buffers.add(buffer);
1408
+ }
1409
+ } else if (kind === "buffer_param_store") {
1410
+ markOrigins(
1411
+ bufferParamOrigins(data?.parameter, func),
1412
+ summary,
1413
+ );
1414
+ } else if (kind === "slice_store") {
1415
+ markValueWrite(data?.slice, "slice store");
1416
+ } else if (kind === "slice_fill" || kind === "slice_copy") {
1417
+ markValueWrite(data?.destination, "slice write");
1418
+ } else if (kind === "call") {
1419
+ const callee = summaries[data?.function];
1420
+ if (!callee) {
1421
+ this.fail(
1422
+ `MIR call references missing function ${String(data?.function)}`,
1423
+ );
1424
+ }
1425
+ for (const buffer of callee.buffers) {
1426
+ summary.buffers.add(buffer);
1427
+ }
1428
+ for (const parameterOriginValue of callee.parameters) {
1429
+ const [, parameterText, slotText] = parameterOriginValue.split(":");
1430
+ const parameter = Number(parameterText);
1431
+ const slot = Number(slotText);
1432
+ const argument = data?.args?.[parameter];
1433
+ if (!argument) {
1434
+ this.fail(
1435
+ `MIR call to function ${data.function} has no argument for writable parameter ${parameter}`,
1436
+ );
1437
+ }
1438
+ if (
1439
+ argumentUsesUnsupportedResult(
1440
+ argument,
1441
+ unsupported[functionId],
1442
+ )
1443
+ ) {
1444
+ this.fail(
1445
+ "cannot infer interface-buffer writes through a slice or buffer returned by a MIR call",
1446
+ );
1447
+ }
1448
+ markOrigins(
1449
+ argumentOrigins(argument, aliases[functionId], func, slot),
1450
+ summary,
1451
+ );
1452
+ }
1453
+ } else if (kind === "if") {
1454
+ visitBlock(data?.then_block);
1455
+ visitBlock(data?.else_block);
1456
+ } else if (kind === "loop") {
1457
+ visitBlock(data?.body);
1458
+ }
1459
+ }
1460
+ };
1461
+ visitBlock(func.body);
1462
+ return summary;
1463
+ });
1464
+ if (next.every((summary, index) => summaryEquals(summary, summaries[index]))) {
1465
+ summaries = next;
1466
+ break;
1467
+ }
1468
+ summaries = next;
1469
+ }
1470
+
1471
+ const roots = [
1472
+ this.mir.entry_points.init,
1473
+ this.mir.entry_points.process,
1474
+ ...this.mir.interface.events.map((event) => event.handler),
1475
+ ];
1476
+ this.bufferMayWrite = this.mir.interface.buffers.map(() => false);
1477
+ for (const root of roots) {
1478
+ const summary = summaries[root];
1479
+ if (!summary) {
1480
+ this.fail(`MIR buffer-write root function ${String(root)} is missing`);
1481
+ }
1482
+ for (const bufferId of summary.buffers) {
1483
+ if (
1484
+ !Number.isInteger(bufferId)
1485
+ || bufferId < 0
1486
+ || bufferId >= this.bufferMayWrite.length
1487
+ ) {
1488
+ this.fail(
1489
+ `MIR buffer-write analysis references missing buffer ${String(bufferId)}`,
1490
+ );
1491
+ }
1492
+ this.bufferMayWrite[bufferId] = true;
1493
+ }
1494
+ }
1495
+ for (const [bufferId, mayWrite] of this.bufferMayWrite.entries()) {
1496
+ if (mayWrite && this.mir.interface.buffers[bufferId].access !== "read_write") {
1497
+ this.fail(
1498
+ `MIR writes read-only interface buffer '${this.mir.interface.buffers[bufferId].name}'`,
1499
+ );
1500
+ }
1501
+ }
1502
+ }
1503
+
1504
+ analyzeRecoverableFailures() {
1505
+ const callees = this.mir.functions.map(() => new Set());
1506
+ const direct = this.mir.functions.map(() => false);
1507
+ const binaryMayFail = (functionId, value) => {
1508
+ if (
1509
+ value.kind !== "binary"
1510
+ || !["divide", "remainder"].includes(value.data?.op)
1511
+ ) {
1512
+ return false;
1513
+ }
1514
+ const lhs = value.data.lhs;
1515
+ const scalar = lhs.kind === "constant"
1516
+ ? lhs.data.type
1517
+ : this.requireScalarType(
1518
+ this.mir.functions[functionId].locals[lhs.data].ty,
1519
+ `binary operand in '${this.mir.functions[functionId].name}'`,
1520
+ );
1521
+ return scalar === "i32" || scalar === "i64";
1522
+ };
1523
+ const checkedBoundsKinds = new Set([
1524
+ "array_window",
1525
+ "buffer_load",
1526
+ "buffer_param_load",
1527
+ "buffer_param_store",
1528
+ "buffer_store",
1529
+ "const_data_load",
1530
+ "index",
1531
+ "input_load",
1532
+ "make_slice",
1533
+ "output_load",
1534
+ "output_store",
1535
+ "control_output_store",
1536
+ ]);
1537
+ const dynamicBoundsKinds = new Set([
1538
+ "slice_element",
1539
+ "slice_load",
1540
+ "slice_store",
1541
+ "slice_window",
1542
+ ]);
1543
+ const scan = (functionId, value) => {
1544
+ if (value === null || value === undefined) return;
1545
+ if (Array.isArray(value)) {
1546
+ for (const entry of value) scan(functionId, entry);
1547
+ return;
1548
+ }
1549
+ if (typeof value !== "object") return;
1550
+ if (value.kind === "call" && Number.isInteger(value.data?.function)) {
1551
+ callees[functionId].add(value.data.function);
1552
+ }
1553
+ const bounds = value.data?.bounds;
1554
+ if (
1555
+ value.kind === "process_frame"
1556
+ || value.kind === "slice_copy"
1557
+ || binaryMayFail(functionId, value)
1558
+ || (checkedBoundsKinds.has(value.kind) && bounds === "checked")
1559
+ || (dynamicBoundsKinds.has(value.kind) && bounds !== "unchecked")
1560
+ ) {
1561
+ direct[functionId] = true;
1562
+ }
1563
+ for (const child of Object.values(value)) scan(functionId, child);
1564
+ };
1565
+ for (const [functionId, func] of this.mir.functions.entries()) {
1566
+ scan(functionId, func.body);
1567
+ }
1568
+ this.functionMayFail = [...direct];
1569
+ let changed = true;
1570
+ while (changed) {
1571
+ changed = false;
1572
+ for (const [functionId, targets] of callees.entries()) {
1573
+ if (
1574
+ !this.functionMayFail[functionId]
1575
+ && [...targets].some((target) => this.functionMayFail[target])
1576
+ ) {
1577
+ this.functionMayFail[functionId] = true;
1578
+ changed = true;
1579
+ }
1580
+ }
1581
+ }
1582
+ }
1583
+
1584
+ buildLayouts() {
1585
+ this.stateLayout = this.layoutNamedValues(this.mir.state);
1586
+ this.paramLayout = this.layoutNamedValues(this.mir.interface.params);
1587
+ this.inputLayout = this.layoutPorts(this.mir.interface.inputs);
1588
+ this.outputLayout = this.layoutPorts(this.mir.interface.outputs);
1589
+ this.controlOutputLayout = this.layoutControlOutputs();
1590
+ this.eventLayout = this.mir.interface.events.map((event) =>
1591
+ this.layoutEventValues(event.params),
1592
+ );
1593
+ this.requireWasm32Extent(
1594
+ this.stateLayout.byteLength,
1595
+ "MIR physical state storage",
1596
+ );
1597
+ this.requireWasm32Extent(
1598
+ this.paramLayout.byteLength,
1599
+ "MIR parameter storage",
1600
+ );
1601
+ for (const [eventId, layout] of this.eventLayout.entries()) {
1602
+ this.requireWasm32Extent(
1603
+ layout.minimumByteLength,
1604
+ `MIR event ${eventId} fixed payload storage`,
1605
+ );
1606
+ }
1607
+
1608
+ for (let id = 0; id < this.mir.const_data.length; id += 1) {
1609
+ const data = this.mir.const_data[id];
1610
+ const scalar = data.element;
1611
+ const size = this.scalarSize(scalar);
1612
+ this.nextStaticAddress = alignUp(this.nextStaticAddress, size);
1613
+ const address = this.nextStaticAddress;
1614
+ const bytes = encodeScalarValues(data.values, scalar, this);
1615
+ this.memorySegments.push({
1616
+ offset: this.module.i32.const(address),
480
1617
  data: bytes,
481
1618
  });
482
1619
  this.constLayout.push({ address, scalar, len: data.values.length });
@@ -507,6 +1644,18 @@ class MirCompiler {
507
1644
  return { address, scalar: type.data, size };
508
1645
  });
509
1646
  });
1647
+ if (this.mir.interface.buffers.length > 0) {
1648
+ const fallbackBytes = Math.max(
1649
+ ...this.mir.interface.buffers.map((buffer) =>
1650
+ this.scalarSize(buffer.element)),
1651
+ );
1652
+ this.nextStaticAddress = alignUp(this.nextStaticAddress, 8);
1653
+ this.fallbackBufferReadAddress = this.nextStaticAddress;
1654
+ this.nextStaticAddress += fallbackBytes;
1655
+ this.nextStaticAddress = alignUp(this.nextStaticAddress, 8);
1656
+ this.fallbackBufferWriteAddress = this.nextStaticAddress;
1657
+ this.nextStaticAddress += fallbackBytes;
1658
+ }
510
1659
  this.nextStaticAddress = alignUp(this.nextStaticAddress, 16);
511
1660
  this.requireWasm32Extent(this.nextStaticAddress, "MIR static storage");
512
1661
  this.requireWasm32Extent(
@@ -529,7 +1678,7 @@ class MirCompiler {
529
1678
  const parameter = target?.params[index];
530
1679
  const type = parameter && this.type(parameter.ty);
531
1680
  if (
532
- parameter?.mode !== "value"
1681
+ this.parameterPassingMode(data.function, index) !== "value"
533
1682
  && type?.kind === "scalar"
534
1683
  && argument.kind === "place"
535
1684
  && argument.data.base.kind === "local"
@@ -663,6 +1812,12 @@ class MirCompiler {
663
1812
  for (const name of Object.values(POINTER_GLOBALS)) {
664
1813
  this.module.addGlobal(name, binaryen.i32, true, this.module.i32.const(0));
665
1814
  }
1815
+ this.module.addGlobal(
1816
+ RUNTIME_FAILURE_GLOBAL,
1817
+ binaryen.i32,
1818
+ true,
1819
+ this.module.i32.const(0),
1820
+ );
666
1821
  }
667
1822
 
668
1823
  addMathKernel() {
@@ -752,13 +1907,13 @@ class MirCompiler {
752
1907
 
753
1908
  addMirFunction(id, func) {
754
1909
  let nextIndex = 0;
755
- const paramLayouts = func.params.map((param) => {
1910
+ const paramLayouts = func.params.map((param, parameterId) => {
756
1911
  const layout = this.functionValueLayout(
757
1912
  param.ty,
758
1913
  nextIndex,
759
1914
  `parameter '${param.name}'`,
760
1915
  false,
761
- param.mode,
1916
+ this.parameterPassingMode(id, parameterId),
762
1917
  );
763
1918
  nextIndex += layout.components.length;
764
1919
  return layout;
@@ -785,6 +1940,11 @@ class MirCompiler {
785
1940
  const callResultLocals = this.collectCallResultLocals(func);
786
1941
  const sliceScratch = this.collectSliceScratchLocals(func);
787
1942
  const processFrameLocals = this.collectProcessFrameLocals(func);
1943
+ const generatedLocalBase =
1944
+ paramScalars.length +
1945
+ flatLocalScalars.length +
1946
+ callResultLocals.length +
1947
+ sliceScratch.count;
788
1948
  if (
789
1949
  resultScalars.length > 1
790
1950
  || callResultLocals.some((entry) => entry.resultCount > 1)
@@ -825,10 +1985,21 @@ class MirCompiler {
825
1985
  ),
826
1986
  eventId: func.kind?.kind === "event" ? func.kind.data : null,
827
1987
  processFrameLocals,
1988
+ generatedLocalBase,
1989
+ generatedLocals: [],
1990
+ entryInitializers: [],
1991
+ bufferDescriptorCache: new Map(),
1992
+ audioChannelPointerCache: new Map(),
828
1993
  breakLabels: [],
829
1994
  continueLabels: [],
830
1995
  };
831
- const body = this.compileBlock(func.body, context);
1996
+ const compiledBody = this.compileBlock(func.body, context);
1997
+ const body = context.entryInitializers.length === 0
1998
+ ? compiledBody
1999
+ : this.module.block(null, [
2000
+ ...context.entryInitializers,
2001
+ compiledBody,
2002
+ ]);
832
2003
  const functionRef = this.module.addFunction(
833
2004
  this.functionNames[id],
834
2005
  binaryen.createType(paramScalars.map((type) => this.wasmType(type))),
@@ -837,6 +2008,7 @@ class MirCompiler {
837
2008
  ...flatLocalScalars.map((type) => this.wasmType(type)),
838
2009
  ...callResultLocals.map((entry) => entry.type),
839
2010
  ...Array.from({ length: sliceScratch.count }, () => binaryen.i32),
2011
+ ...context.generatedLocals.map((entry) => this.wasmType(entry.scalar)),
840
2012
  ],
841
2013
  body,
842
2014
  );
@@ -861,6 +2033,9 @@ class MirCompiler {
861
2033
  );
862
2034
  }
863
2035
  }
2036
+ for (const local of context.generatedLocals) {
2037
+ binaryen.Function.setLocalName(functionRef, local.index, local.name);
2038
+ }
864
2039
  }
865
2040
 
866
2041
  functionValueLayout(
@@ -888,15 +2063,28 @@ class MirCompiler {
888
2063
  index,
889
2064
  typeId,
890
2065
  kind: "slice",
891
- components: ["i32", "i32", "i32"],
2066
+ components: ["i32", "i32", "i32", "i32"],
892
2067
  };
893
2068
  }
894
2069
  if (type.kind === "buffer") {
2070
+ this.bufferChannelMetadata(type.data.channels, type.data.element);
895
2071
  return {
896
2072
  index,
897
2073
  typeId,
898
2074
  kind: "buffer",
899
- components: ["i32", "i32", "i32", "f32"],
2075
+ components: ["i32", "i32", "i32", "i32", "f32"],
2076
+ };
2077
+ }
2078
+ if (type.kind === "buffer_span") {
2079
+ this.bufferChannelMetadata(type.data.channels, type.data.element);
2080
+ if (passingMode !== "value") {
2081
+ this.fail(`${description} buffer span must use value passing mode`);
2082
+ }
2083
+ return {
2084
+ index,
2085
+ typeId,
2086
+ kind: "buffer_span",
2087
+ components: ["i32", "i32", "i32", "i32", "i32"],
900
2088
  };
901
2089
  }
902
2090
  if (type.kind === "array") {
@@ -925,8 +2113,10 @@ class MirCompiler {
925
2113
  }
926
2114
  if (layout.kind === "array") return;
927
2115
  const suffixes = layout.kind === "buffer"
928
- ? ["address", "frames", "channels", "sample_rate"]
929
- : ["address", "length", "stride"];
2116
+ ? ["read_address", "write_address", "frames", "channels", "sample_rate"]
2117
+ : layout.kind === "buffer_span"
2118
+ ? ["read_table", "write_table", "frames_table", "channels_table", "sample_rates_table"]
2119
+ : ["read_address", "write_address", "length", "stride"];
930
2120
  for (const [offset, suffix] of suffixes.entries()) {
931
2121
  binaryen.Function.setLocalName(
932
2122
  functionRef,
@@ -946,7 +2136,7 @@ class MirCompiler {
946
2136
  this.requireFunctionId(data.function, "call target");
947
2137
  const target = this.mir.functions[data.function];
948
2138
  const aliasesResult = data.args.some((argument, index) =>
949
- target.params[index]?.mode !== "value"
2139
+ this.parameterPassingMode(data.function, index) !== "value"
950
2140
  && argument.kind === "place"
951
2141
  && argument.data.base.kind === "local"
952
2142
  && argument.data.projections.length === 0
@@ -1049,6 +2239,62 @@ class MirCompiler {
1049
2239
  );
1050
2240
  }
1051
2241
 
2242
+ defaultFunctionResult(context) {
2243
+ const scalars = context.function.results.map((typeId, resultId) =>
2244
+ this.requireScalarType(
2245
+ typeId,
2246
+ `result ${resultId} of '${context.function.name}'`,
2247
+ ),
2248
+ );
2249
+ const values = scalars.map((scalar) => this.zero(scalar));
2250
+ if (values.length === 0) return undefined;
2251
+ if (values.length === 1) return values[0];
2252
+ return this.module.tuple.make(values);
2253
+ }
2254
+
2255
+ returnFromCurrentFunction(context) {
2256
+ return this.module.return(this.defaultFunctionResult(context));
2257
+ }
2258
+
2259
+ raiseRuntimeFailure(context) {
2260
+ return this.module.block(null, [
2261
+ this.module.global.set(
2262
+ RUNTIME_FAILURE_GLOBAL,
2263
+ this.module.i32.const(PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE),
2264
+ ),
2265
+ this.returnFromCurrentFunction(context),
2266
+ ]);
2267
+ }
2268
+
2269
+ propagateRuntimeFailure(calleeId, context) {
2270
+ if (!this.functionMayFail[calleeId]) return [];
2271
+ return [
2272
+ this.module.if(
2273
+ this.module.i32.ne(
2274
+ this.module.global.get(RUNTIME_FAILURE_GLOBAL, binaryen.i32),
2275
+ this.module.i32.const(PROCESSOR_EXECUTION_OK),
2276
+ ),
2277
+ this.returnFromCurrentFunction(context),
2278
+ ),
2279
+ ];
2280
+ }
2281
+
2282
+ resetRuntimeFailure(functionId) {
2283
+ if (!this.functionMayFail[functionId]) return [];
2284
+ return [
2285
+ this.module.global.set(
2286
+ RUNTIME_FAILURE_GLOBAL,
2287
+ this.module.i32.const(PROCESSOR_EXECUTION_OK),
2288
+ ),
2289
+ ];
2290
+ }
2291
+
2292
+ executionStatus(functionId) {
2293
+ return this.functionMayFail[functionId]
2294
+ ? this.module.global.get(RUNTIME_FAILURE_GLOBAL, binaryen.i32)
2295
+ : this.module.i32.const(PROCESSOR_EXECUTION_OK);
2296
+ }
2297
+
1052
2298
  addAbiWrappers() {
1053
2299
  const initId = this.mir.entry_points.init;
1054
2300
  const processId = this.mir.entry_points.process;
@@ -1062,6 +2308,7 @@ class MirCompiler {
1062
2308
  (this.options.simd ? binaryen.Features.SIMD128 : 0),
1063
2309
  );
1064
2310
  const initBody = this.module.block(null, [
2311
+ ...this.resetRuntimeFailure(initId),
1065
2312
  this.module.global.set(
1066
2313
  POINTER_GLOBALS.params,
1067
2314
  this.module.local.get(0, binaryen.i32),
@@ -1076,11 +2323,12 @@ class MirCompiler {
1076
2323
  this.module.i32.const(this.stateLayout.byteLength ?? 0),
1077
2324
  ),
1078
2325
  this.module.call(this.functionNames[initId], [], binaryen.none),
1079
- ]);
2326
+ this.executionStatus(initId),
2327
+ ], binaryen.i32);
1080
2328
  this.module.addFunction(
1081
2329
  "$onda.abi.init",
1082
2330
  binaryen.createType([binaryen.i32, binaryen.i32]),
1083
- binaryen.none,
2331
+ binaryen.i32,
1084
2332
  [],
1085
2333
  initBody,
1086
2334
  );
@@ -1121,7 +2369,13 @@ class MirCompiler {
1121
2369
  ),
1122
2370
  );
1123
2371
  const processBody = this.module.block(null, [
1124
- this.module.if(invalidRange, this.module.unreachable()),
2372
+ this.module.if(
2373
+ invalidRange,
2374
+ this.module.return(
2375
+ this.module.i32.const(PROCESSOR_EXECUTION_RUNTIME_SAFETY_FAILURE),
2376
+ ),
2377
+ ),
2378
+ ...this.resetRuntimeFailure(processId),
1125
2379
  this.module.global.set(
1126
2380
  POINTER_GLOBALS.state,
1127
2381
  this.module.local.get(0, binaryen.i32),
@@ -1142,6 +2396,10 @@ class MirCompiler {
1142
2396
  POINTER_GLOBALS.buffers,
1143
2397
  this.module.local.get(7, binaryen.i32),
1144
2398
  ),
2399
+ this.module.global.set(
2400
+ POINTER_GLOBALS.bufferWrites,
2401
+ this.module.local.get(7, binaryen.i32),
2402
+ ),
1145
2403
  this.module.global.set(
1146
2404
  POINTER_GLOBALS.bufferFrames,
1147
2405
  this.module.local.get(8, binaryen.i32),
@@ -1159,11 +2417,12 @@ class MirCompiler {
1159
2417
  [startFrame(), frames(), flags()],
1160
2418
  binaryen.none,
1161
2419
  ),
1162
- ]);
2420
+ this.executionStatus(processId),
2421
+ ], binaryen.i32);
1163
2422
  this.module.addFunction(
1164
2423
  "$onda.abi.process",
1165
2424
  processParams,
1166
- binaryen.none,
2425
+ binaryen.i32,
1167
2426
  [],
1168
2427
  processBody,
1169
2428
  );
@@ -1182,6 +2441,7 @@ class MirCompiler {
1182
2441
  }
1183
2442
  const wrapperName = `$onda.abi.event.${eventId}`;
1184
2443
  const body = this.module.block(null, [
2444
+ ...this.resetRuntimeFailure(event.handler),
1185
2445
  this.module.global.set(
1186
2446
  POINTER_GLOBALS.eventPayload,
1187
2447
  this.module.local.get(0, binaryen.i32),
@@ -1198,6 +2458,10 @@ class MirCompiler {
1198
2458
  POINTER_GLOBALS.buffers,
1199
2459
  this.module.local.get(3, binaryen.i32),
1200
2460
  ),
2461
+ this.module.global.set(
2462
+ POINTER_GLOBALS.bufferWrites,
2463
+ this.module.local.get(3, binaryen.i32),
2464
+ ),
1201
2465
  this.module.global.set(
1202
2466
  POINTER_GLOBALS.bufferFrames,
1203
2467
  this.module.local.get(4, binaryen.i32),
@@ -1211,11 +2475,12 @@ class MirCompiler {
1211
2475
  this.module.local.get(6, binaryen.i32),
1212
2476
  ),
1213
2477
  this.module.call(this.functionNames[event.handler], [], binaryen.none),
1214
- ]);
2478
+ this.executionStatus(event.handler),
2479
+ ], binaryen.i32);
1215
2480
  this.module.addFunction(
1216
2481
  wrapperName,
1217
2482
  binaryen.createType(Array.from({ length: 7 }, () => binaryen.i32)),
1218
- binaryen.none,
2483
+ binaryen.i32,
1219
2484
  [],
1220
2485
  body,
1221
2486
  );
@@ -1326,7 +2591,16 @@ class MirCompiler {
1326
2591
  const args = data.args.flatMap((argument, index) => {
1327
2592
  const parameterType = this.type(target.params[index].ty);
1328
2593
  if (parameterType.kind === "scalar") {
1329
- if (target.params[index].mode === "value") {
2594
+ const passingMode = this.parameterPassingMode(data.function, index);
2595
+ if (passingMode === "value") {
2596
+ if (target.params[index].mode !== "value") {
2597
+ if (argument.kind !== "place") {
2598
+ this.fail(
2599
+ `promoted scalar reference argument ${index} of '${target.name}' is not a place`,
2600
+ );
2601
+ }
2602
+ return [this.loadPlace(argument.data, context)];
2603
+ }
1330
2604
  if (argument.kind !== "value") {
1331
2605
  this.fail(`scalar call argument ${index} of '${target.name}' is not a value`);
1332
2606
  }
@@ -1345,6 +2619,7 @@ class MirCompiler {
1345
2619
  argument.data.index,
1346
2620
  argument.data.bounds,
1347
2621
  context,
2622
+ target.params[index].mode === "read_write_reference",
1348
2623
  ),
1349
2624
  ];
1350
2625
  }
@@ -1374,22 +2649,32 @@ class MirCompiler {
1374
2649
  ];
1375
2650
  }
1376
2651
  return [
1377
- this.compileSliceWindowAddress(
1378
- argument.data,
1379
- parameterType,
1380
- context,
1381
- ),
2652
+ this.compileSliceWindowAddress(
2653
+ argument.data,
2654
+ parameterType,
2655
+ context,
2656
+ target.params[index].mode === "read_write_reference",
2657
+ ),
1382
2658
  ];
1383
2659
  }
1384
2660
  if (parameterType.kind === "buffer") {
1385
2661
  if (argument.kind === "buffer") {
1386
- return this.compileInterfaceBufferValue(argument.data);
2662
+ return this.compileInterfaceBufferValue(argument.data, context);
2663
+ }
2664
+ if (argument.kind === "buffer_param") {
2665
+ return this.loadBufferParamValue(argument.data, context);
1387
2666
  }
1388
2667
  if (argument.kind === "place") {
1389
2668
  return this.loadBufferPlace(argument.data, context);
1390
2669
  }
1391
2670
  this.fail(`buffer call argument ${index} of '${target.name}' is invalid`);
1392
2671
  }
2672
+ if (parameterType.kind === "buffer_span") {
2673
+ if (argument.kind !== "buffer_span") {
2674
+ this.fail(`buffer span call argument ${index} of '${target.name}' is invalid`);
2675
+ }
2676
+ return this.compileBufferSpanValue(argument.data, parameterType, context);
2677
+ }
1393
2678
  this.fail(
1394
2679
  `call argument ${index} of '${target.name}' has unsupported type '${parameterType.kind}'`,
1395
2680
  );
@@ -1405,7 +2690,7 @@ class MirCompiler {
1405
2690
  const localReferenceSync = data.args.flatMap((argument, index) => {
1406
2691
  const parameter = target.params[index];
1407
2692
  if (
1408
- parameter.mode === "value"
2693
+ this.parameterPassingMode(data.function, index) === "value"
1409
2694
  || argument.kind !== "place"
1410
2695
  || argument.data.base.kind !== "local"
1411
2696
  || argument.data.projections.length !== 0
@@ -1465,6 +2750,7 @@ class MirCompiler {
1465
2750
  this.module.local.set(resultSpill.index, call),
1466
2751
  ...afterCall,
1467
2752
  ...assignResults,
2753
+ ...this.propagateRuntimeFailure(data.function, context),
1468
2754
  ]);
1469
2755
  }
1470
2756
  let compiledCall;
@@ -1492,8 +2778,18 @@ class MirCompiler {
1492
2778
  ),
1493
2779
  ]);
1494
2780
  }
1495
- if (localReferenceSync.length === 0) return compiledCall;
1496
- return this.module.block(null, [...beforeCall, compiledCall, ...afterCall]);
2781
+ if (localReferenceSync.length === 0) {
2782
+ const propagation = this.propagateRuntimeFailure(data.function, context);
2783
+ return propagation.length === 0
2784
+ ? compiledCall
2785
+ : this.module.block(null, [compiledCall, ...propagation]);
2786
+ }
2787
+ return this.module.block(null, [
2788
+ ...beforeCall,
2789
+ compiledCall,
2790
+ ...afterCall,
2791
+ ...this.propagateRuntimeFailure(data.function, context),
2792
+ ]);
1497
2793
  }
1498
2794
 
1499
2795
  compileOutputStore(data, context) {
@@ -1502,12 +2798,13 @@ class MirCompiler {
1502
2798
  if (!port) {
1503
2799
  this.fail(`output id ${data.output} is out of range`);
1504
2800
  }
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)),
2801
+ const channelPointer = this.audioChannelPointer(
2802
+ POINTER_GLOBALS.outputs,
2803
+ port,
2804
+ data.element,
2805
+ data.bounds,
2806
+ context,
1509
2807
  );
1510
- const channelPointer = this.module.i32.load(0, 4, tableAddress);
1511
2808
  const sampleAddress = this.module.i32.add(
1512
2809
  channelPointer,
1513
2810
  this.module.i32.mul(
@@ -1564,13 +2861,13 @@ class MirCompiler {
1564
2861
  }
1565
2862
 
1566
2863
  compileBufferStore(data, context) {
1567
- const buffer = this.requireBuffer(data.buffer);
2864
+ const buffer = this.requireBufferRef(data.buffer);
1568
2865
  if (buffer.access !== "read_write") {
1569
2866
  this.fail(`buffer '${buffer.name}' is read-only`);
1570
2867
  }
1571
2868
  return this.storeScalar(
1572
2869
  buffer.element,
1573
- this.compileBufferAddress(data, context),
2870
+ this.compileBufferAddress(data, context, true),
1574
2871
  this.compileValue(data.value, context),
1575
2872
  );
1576
2873
  }
@@ -1582,7 +2879,7 @@ class MirCompiler {
1582
2879
  }
1583
2880
  return this.storeScalar(
1584
2881
  type.data.element,
1585
- this.compileBufferParamAddress(data, context),
2882
+ this.compileBufferParamAddress(data, context, true),
1586
2883
  this.compileValue(data.value, context),
1587
2884
  );
1588
2885
  }
@@ -1608,6 +2905,7 @@ class MirCompiler {
1608
2905
  scalar,
1609
2906
  () => this.compileValue(data.lhs, context),
1610
2907
  () => this.compileValue(data.rhs, context),
2908
+ context,
1611
2909
  );
1612
2910
  }
1613
2911
  case "compare": {
@@ -1640,27 +2938,24 @@ class MirCompiler {
1640
2938
  case "buffer_param_load":
1641
2939
  return this.compileBufferParamLoad(data, context);
1642
2940
  case "buffer_len":
1643
- return this.compileBufferLen(data);
2941
+ return this.compileBufferLen(data, context);
1644
2942
  case "buffer_param_len":
1645
2943
  return this.compileBufferParamLen(data, context);
1646
2944
  case "buffer_channels":
1647
- return this.loadBufferTableValue(
1648
- POINTER_GLOBALS.bufferChannels,
1649
- data,
1650
- "i32",
1651
- );
2945
+ return this.compileBufferChannels(data, context);
1652
2946
  case "buffer_param_channels":
1653
- return this.loadBufferParamComponent(data, 2, "i32", context);
2947
+ return this.compileBufferParamChannels(data, context);
1654
2948
  case "buffer_sample_rate":
1655
2949
  return this.loadBufferTableValue(
1656
2950
  POINTER_GLOBALS.bufferSampleRates,
1657
2951
  data,
1658
2952
  "f32",
2953
+ context,
1659
2954
  );
1660
2955
  case "buffer_param_sample_rate":
1661
- return this.loadBufferParamComponent(data, 3, "f32", context);
2956
+ return this.loadBufferParamComponent(data, 4, "f32", context);
1662
2957
  case "slice_len":
1663
- return this.compileSliceValue(data, context)[1];
2958
+ return this.compileSliceValue(data, context)[2];
1664
2959
  case "slice_load":
1665
2960
  return this.compileSliceLoad(data, context);
1666
2961
  case "make_slice":
@@ -1690,12 +2985,13 @@ class MirCompiler {
1690
2985
  if (!port) {
1691
2986
  this.fail(`input id ${data.input} is out of range`);
1692
2987
  }
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)),
2988
+ const channelPointer = this.audioChannelPointer(
2989
+ POINTER_GLOBALS.inputs,
2990
+ port,
2991
+ data.element,
2992
+ data.bounds,
2993
+ context,
1697
2994
  );
1698
- const channelPointer = this.module.i32.load(0, 4, tableAddress);
1699
2995
  const sampleAddress = this.module.i32.add(
1700
2996
  channelPointer,
1701
2997
  this.module.i32.mul(
@@ -1718,7 +3014,7 @@ class MirCompiler {
1718
3014
  const invalid = this.module.i32.ge_u(offset(), frames());
1719
3015
  return this.module.if(
1720
3016
  invalid,
1721
- this.module.unreachable(),
3017
+ this.raiseRuntimeFailure(context),
1722
3018
  this.module.i32.add(startFrame(), offset()),
1723
3019
  );
1724
3020
  }
@@ -1739,12 +3035,13 @@ class MirCompiler {
1739
3035
  if (!port) {
1740
3036
  this.fail(`output id ${data.output} is out of range`);
1741
3037
  }
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)),
3038
+ const channelPointer = this.audioChannelPointer(
3039
+ POINTER_GLOBALS.outputs,
3040
+ port,
3041
+ data.element,
3042
+ data.bounds,
3043
+ context,
1746
3044
  );
1747
- const channelPointer = this.module.i32.load(0, 4, tableAddress);
1748
3045
  const sampleAddress = this.module.i32.add(
1749
3046
  channelPointer,
1750
3047
  this.module.i32.mul(
@@ -1769,6 +3066,62 @@ class MirCompiler {
1769
3066
  return this.module.i32.add(this.module.i32.const(port.channel), index);
1770
3067
  }
1771
3068
 
3069
+ audioChannelPointer(globalName, port, element, bounds, context) {
3070
+ const staticChannel = this.staticPortChannel(port, element, bounds);
3071
+ if (staticChannel === null) {
3072
+ return this.loadAudioChannelPointer(
3073
+ globalName,
3074
+ this.compilePortChannel(port, element, bounds, context),
3075
+ );
3076
+ }
3077
+ const key = `${globalName}:${staticChannel}`;
3078
+ let local = context.audioChannelPointerCache.get(key);
3079
+ if (local === undefined) {
3080
+ local = this.allocateGeneratedLocal(
3081
+ context,
3082
+ "i32",
3083
+ `audio.${globalName.slice(6)}.${staticChannel}`,
3084
+ );
3085
+ context.audioChannelPointerCache.set(key, local);
3086
+ context.entryInitializers.push(
3087
+ this.module.local.set(
3088
+ local,
3089
+ this.loadAudioChannelPointer(
3090
+ globalName,
3091
+ this.module.i32.const(staticChannel),
3092
+ ),
3093
+ ),
3094
+ );
3095
+ }
3096
+ return this.module.local.get(local, binaryen.i32);
3097
+ }
3098
+
3099
+ loadAudioChannelPointer(globalName, channel) {
3100
+ const tableAddress = this.module.i32.add(
3101
+ this.module.global.get(globalName, binaryen.i32),
3102
+ this.module.i32.mul(channel, this.module.i32.const(4)),
3103
+ );
3104
+ return this.module.i32.load(0, 4, tableAddress);
3105
+ }
3106
+
3107
+ staticPortChannel(port, element, bounds) {
3108
+ if (!port.isArray) return port.channel;
3109
+ if (
3110
+ element?.kind !== "constant"
3111
+ || element.data?.type !== "i32"
3112
+ || !Number.isInteger(element.data.value)
3113
+ ) {
3114
+ return null;
3115
+ }
3116
+ let index = element.data.value;
3117
+ if (bounds === "clamp") {
3118
+ index = Math.min(port.channels - 1, Math.max(0, index));
3119
+ } else if (index < 0 || index >= port.channels) {
3120
+ return null;
3121
+ }
3122
+ return port.channel + index;
3123
+ }
3124
+
1772
3125
  compileConstDataLoad(data, context) {
1773
3126
  const item = this.constLayout[data.data];
1774
3127
  if (!item) {
@@ -1783,10 +3136,10 @@ class MirCompiler {
1783
3136
  }
1784
3137
 
1785
3138
  compileBufferLoad(data, context) {
1786
- const buffer = this.requireBuffer(data.buffer);
3139
+ const buffer = this.requireBufferRef(data.buffer);
1787
3140
  return this.loadScalar(
1788
3141
  buffer.element,
1789
- this.compileBufferAddress(data, context),
3142
+ this.compileBufferAddress(data, context, false),
1790
3143
  );
1791
3144
  }
1792
3145
 
@@ -1794,161 +3147,798 @@ class MirCompiler {
1794
3147
  const type = this.bufferParamType(data.parameter, context);
1795
3148
  return this.loadScalar(
1796
3149
  type.data.element,
1797
- this.compileBufferParamAddress(data, context),
3150
+ this.compileBufferParamAddress(data, context, false),
1798
3151
  );
1799
3152
  }
1800
3153
 
1801
- compileBufferParamAddress(data, context) {
3154
+ compileBufferParamAddress(data, context, write) {
1802
3155
  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
- ),
3156
+ return this.withBufferParamSelector(
3157
+ data.parameter,
3158
+ context,
3159
+ binaryen.i32,
3160
+ (selector, staticSelector, prelude) => {
3161
+ let frames = this.bufferParamComponentFactory(
3162
+ data.parameter,
3163
+ 2,
3164
+ "i32",
3165
+ selector,
3166
+ staticSelector,
3167
+ context,
3168
+ );
3169
+ let channels = this.bufferParamChannelsFactory(
3170
+ data.parameter,
3171
+ selector,
3172
+ staticSelector,
3173
+ context,
3174
+ );
3175
+ if (data.parameter?.kind === "array_element" && staticSelector === null) {
3176
+ frames = this.snapshotOperationValue(
3177
+ frames,
3178
+ "i32",
3179
+ "buffer_param.frames",
3180
+ prelude,
3181
+ context,
3182
+ );
3183
+ if (this.bufferChannelMetadata(
3184
+ type.data.channels,
3185
+ type.data.element,
3186
+ ).kind === "dynamic") {
3187
+ channels = this.snapshotOperationValue(
3188
+ channels,
3189
+ "i32",
3190
+ "buffer_param.channels",
3191
+ prelude,
3192
+ context,
3193
+ );
3194
+ }
3195
+ }
3196
+ const frame = this.compileDynamicBoundedIndex(
3197
+ () => this.compileValue(data.index, context),
3198
+ frames,
3199
+ data.bounds,
3200
+ context,
3201
+ true,
3202
+ );
3203
+ const index = data.channel === null
3204
+ ? frame
3205
+ : this.module.i32.add(
3206
+ this.module.i32.mul(frame, channels()),
3207
+ this.compileDynamicBoundedIndex(
3208
+ () => this.compileValue(data.channel, context),
3209
+ channels,
3210
+ data.bounds,
3211
+ context,
3212
+ true,
3213
+ ),
3214
+ );
3215
+ const pointer = this.bufferParamComponentFactory(
3216
+ data.parameter,
3217
+ write ? 1 : 0,
3218
+ "i32",
3219
+ selector,
3220
+ staticSelector,
3221
+ context,
3222
+ );
3223
+ return this.bufferPointerWithOffset(
3224
+ pointer,
3225
+ this.module.i32.mul(
3226
+ index,
3227
+ this.module.i32.const(this.scalarSize(type.data.element)),
3228
+ ),
3229
+ write,
3230
+ context,
3231
+ );
3232
+ },
1827
3233
  );
1828
3234
  }
1829
3235
 
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,
3236
+ compileBufferAddress(data, context, write) {
3237
+ const buffer = this.requireBufferRef(data.buffer);
3238
+ return this.withBufferRefIndex(
3239
+ data.buffer,
3240
+ context,
3241
+ binaryen.i32,
3242
+ (descriptorIndex, staticIndex, prelude) => {
3243
+ let frames = this.bufferTableValueFactory(
3244
+ POINTER_GLOBALS.bufferFrames,
3245
+ descriptorIndex,
3246
+ staticIndex,
3247
+ "i32",
3248
+ context,
3249
+ );
3250
+ let channels = this.bufferChannelsFactory(
3251
+ data.buffer,
3252
+ descriptorIndex,
3253
+ staticIndex,
3254
+ context,
3255
+ );
3256
+ if (staticIndex === null) {
3257
+ frames = this.snapshotOperationValue(
3258
+ frames,
1842
3259
  "i32",
3260
+ "buffer.frames",
3261
+ prelude,
3262
+ context,
3263
+ );
3264
+ if (this.bufferRefChannelMetadata(data.buffer).kind === "dynamic") {
3265
+ channels = this.snapshotOperationValue(
3266
+ channels,
3267
+ "i32",
3268
+ "buffer.channels",
3269
+ prelude,
3270
+ context,
3271
+ );
3272
+ }
3273
+ }
3274
+ const frame = this.compileDynamicBoundedIndex(
3275
+ () => this.compileValue(data.index, context),
3276
+ frames,
3277
+ data.bounds,
3278
+ context,
3279
+ true,
3280
+ );
3281
+ const index = data.channel === null
3282
+ ? frame
3283
+ : this.module.i32.add(
3284
+ this.module.i32.mul(frame, channels()),
3285
+ this.compileDynamicBoundedIndex(
3286
+ () => this.compileValue(data.channel, context),
3287
+ channels,
3288
+ data.bounds,
3289
+ context,
3290
+ true,
3291
+ ),
3292
+ );
3293
+ const pointer = this.bufferTableValueFactory(
3294
+ write ? POINTER_GLOBALS.bufferWrites : POINTER_GLOBALS.buffers,
3295
+ descriptorIndex,
3296
+ staticIndex,
3297
+ "i32",
3298
+ context,
3299
+ );
3300
+ return this.bufferPointerWithOffset(
3301
+ pointer,
3302
+ this.module.i32.mul(
3303
+ index,
3304
+ this.module.i32.const(this.scalarSize(buffer.element)),
3305
+ ),
3306
+ write,
3307
+ context,
3308
+ );
3309
+ },
3310
+ );
3311
+ }
3312
+
3313
+ bufferPointerWithOffset(pointer, byteOffset, write, context) {
3314
+ const local = this.allocateGeneratedLocal(
3315
+ context,
3316
+ "i32",
3317
+ write ? "buffer.write_pointer" : "buffer.read_pointer",
3318
+ );
3319
+ const stablePointer = () => this.module.local.get(local, binaryen.i32);
3320
+ const fallback = write
3321
+ ? this.fallbackBufferWriteAddress
3322
+ : this.fallbackBufferReadAddress;
3323
+ return this.module.block(
3324
+ null,
3325
+ [
3326
+ this.module.local.set(local, pointer()),
3327
+ this.module.i32.add(
3328
+ stablePointer(),
3329
+ this.module.select(
3330
+ this.module.i32.eq(
3331
+ stablePointer(),
3332
+ this.module.i32.const(fallback),
3333
+ ),
3334
+ this.module.i32.const(0),
3335
+ byteOffset,
1843
3336
  ),
1844
3337
  ),
1845
- this.compileValue(data.channel, context),
1846
- );
1847
- };
1848
- const index = this.compileDynamicBoundedIndex(
1849
- rawIndex,
1850
- () => this.compileBufferTotalScalarLen(data.buffer),
1851
- data.bounds,
3338
+ ],
3339
+ binaryen.i32,
3340
+ );
3341
+ }
3342
+
3343
+ compileBufferLen(bufferRef, context) {
3344
+ this.requireBufferRef(bufferRef);
3345
+ return this.withBufferRefIndex(
3346
+ bufferRef,
3347
+ context,
3348
+ binaryen.i32,
3349
+ (index, staticIndex) => this.bufferTableValueFactory(
3350
+ POINTER_GLOBALS.bufferFrames,
3351
+ index,
3352
+ staticIndex,
3353
+ "i32",
3354
+ context,
3355
+ )(),
3356
+ );
3357
+ }
3358
+
3359
+ compileBufferChannels(bufferRef, context) {
3360
+ const channels = this.bufferRefChannelMetadata(bufferRef);
3361
+ if (channels.kind === "mono") return this.module.i32.const(1);
3362
+ if (channels.kind === "static") return this.module.i32.const(channels.count);
3363
+ return this.withBufferRefIndex(
3364
+ bufferRef,
3365
+ context,
3366
+ binaryen.i32,
3367
+ (index, staticIndex) => this.bufferTableValueFactory(
3368
+ POINTER_GLOBALS.bufferChannels,
3369
+ index,
3370
+ staticIndex,
3371
+ "i32",
3372
+ context,
3373
+ )(),
3374
+ );
3375
+ }
3376
+
3377
+ compileBufferParamLen(parameterId, context) {
3378
+ this.bufferParamType(parameterId, context);
3379
+ return this.withBufferParamSelector(
3380
+ parameterId,
3381
+ context,
3382
+ binaryen.i32,
3383
+ (selector, staticSelector) => this.bufferParamComponentFactory(
3384
+ parameterId,
3385
+ 2,
3386
+ "i32",
3387
+ selector,
3388
+ staticSelector,
3389
+ context,
3390
+ )(),
3391
+ );
3392
+ }
3393
+
3394
+ compileBufferParamChannels(parameterId, context) {
3395
+ const type = this.bufferParamType(parameterId, context);
3396
+ const channels = this.bufferChannelMetadata(
3397
+ type.data.channels,
3398
+ type.data.element,
3399
+ );
3400
+ if (channels.kind === "mono") return this.module.i32.const(1);
3401
+ if (channels.kind === "static") return this.module.i32.const(channels.count);
3402
+ return this.withBufferParamSelector(
3403
+ parameterId,
3404
+ context,
3405
+ binaryen.i32,
3406
+ (selector, staticSelector) => this.bufferParamComponentFactory(
3407
+ parameterId,
3408
+ 3,
3409
+ "i32",
3410
+ selector,
3411
+ staticSelector,
3412
+ context,
3413
+ )(),
3414
+ );
3415
+ }
3416
+
3417
+ bufferParamType(parameterId, context) {
3418
+ const parameter = context.function.params[this.bufferParamIds(parameterId, context)[0]];
3419
+ const type = parameter && this.type(parameter.ty);
3420
+ const expectedKind = parameterId?.kind === "array_element"
3421
+ ? "buffer_span"
3422
+ : "buffer";
3423
+ if (!type || type.kind !== expectedKind) {
3424
+ this.fail(`parameter id ${parameterId} is not a buffer`);
3425
+ }
3426
+ return type;
3427
+ }
3428
+
3429
+ bufferParamIds(parameterRef, context) {
3430
+ if (Number.isInteger(parameterRef)) return [parameterRef];
3431
+ if (
3432
+ parameterRef?.kind === "direct"
3433
+ && Number.isInteger(parameterRef.data)
3434
+ ) {
3435
+ return [parameterRef.data];
3436
+ }
3437
+ if (
3438
+ parameterRef?.kind === "array_element"
3439
+ && Number.isInteger(parameterRef.data?.span)
3440
+ && parameterRef.data.span >= 0
3441
+ && parameterRef.data.span < context.function.params.length
3442
+ ) {
3443
+ return [parameterRef.data.span];
3444
+ }
3445
+ this.fail("invalid buffer parameter reference");
3446
+ }
3447
+
3448
+ bufferParamLayout(parameterId, context) {
3449
+ const layout = context.paramLayouts[parameterId];
3450
+ if (!layout || !["buffer", "buffer_span"].includes(layout.kind)) {
3451
+ this.fail(`parameter id ${parameterId} has no buffer descriptor`);
3452
+ }
3453
+ return layout;
3454
+ }
3455
+
3456
+ staticBufferParamSelector(parameterRef, context) {
3457
+ if (parameterRef?.kind !== "array_element") return null;
3458
+ const reference = parameterRef.data;
3459
+ const type = this.bufferParamType(parameterRef, context);
3460
+ const selector = reference.selector;
3461
+ if (
3462
+ selector?.kind !== "constant"
3463
+ || selector.data?.type !== "i32"
3464
+ || !Number.isInteger(selector.data.value)
3465
+ ) {
3466
+ return null;
3467
+ }
3468
+ let index = selector.data.value;
3469
+ if (reference.bounds === "clamp") {
3470
+ index = Math.min(type.data.len - 1, Math.max(0, index));
3471
+ } else if (index < 0 || index >= type.data.len) {
3472
+ return null;
3473
+ }
3474
+ return index;
3475
+ }
3476
+
3477
+ compileBufferParamSelector(parameterRef, context) {
3478
+ const reference = parameterRef.data;
3479
+ const type = this.bufferParamType(parameterRef, context);
3480
+ return this.compileDynamicBoundedIndex(
3481
+ () => this.compileValue(reference.selector, context),
3482
+ () => this.module.i32.const(type.data.len),
3483
+ reference.bounds,
3484
+ context,
1852
3485
  true,
1853
3486
  );
1854
- const pointer = this.loadBufferTableValue(
1855
- POINTER_GLOBALS.buffers,
1856
- data.buffer,
3487
+ }
3488
+
3489
+ withBufferParamSelector(parameterRef, context, resultType, build) {
3490
+ if (parameterRef?.kind !== "array_element") {
3491
+ return build(null, null, []);
3492
+ }
3493
+ const staticSelector = this.staticBufferParamSelector(parameterRef, context);
3494
+ if (staticSelector !== null) {
3495
+ return build(
3496
+ () => this.module.i32.const(staticSelector),
3497
+ staticSelector,
3498
+ [],
3499
+ );
3500
+ }
3501
+ const selectorLocal = this.allocateGeneratedLocal(
3502
+ context,
1857
3503
  "i32",
3504
+ "buffer_param.selector",
1858
3505
  );
1859
- return this.module.i32.add(
1860
- pointer,
1861
- this.module.i32.mul(
1862
- index,
1863
- this.module.i32.const(this.scalarSize(buffer.element)),
3506
+ const prelude = [
3507
+ this.module.local.set(
3508
+ selectorLocal,
3509
+ this.compileBufferParamSelector(parameterRef, context),
1864
3510
  ),
3511
+ ];
3512
+ const result = build(
3513
+ () => this.module.local.get(selectorLocal, binaryen.i32),
3514
+ null,
3515
+ prelude,
1865
3516
  );
3517
+ return this.module.block(null, [...prelude, result], resultType);
1866
3518
  }
1867
3519
 
1868
- compileBufferLen(bufferId) {
1869
- this.requireBuffer(bufferId);
1870
- return this.loadBufferTableValue(
1871
- POINTER_GLOBALS.bufferFrames,
1872
- bufferId,
3520
+ bufferParamComponentFactory(
3521
+ parameterRef,
3522
+ offset,
3523
+ scalar,
3524
+ selector,
3525
+ staticSelector,
3526
+ context,
3527
+ ) {
3528
+ const parameterId = this.bufferParamIds(parameterRef, context)[0];
3529
+ const layout = this.bufferParamLayout(parameterId, context);
3530
+ if (parameterRef?.kind !== "array_element") {
3531
+ return () =>
3532
+ this.module.local.get(layout.index + offset, this.wasmType(scalar));
3533
+ }
3534
+ const rawLoad = (index) => {
3535
+ const table = this.module.local.get(layout.index + offset, binaryen.i32);
3536
+ const address = this.module.i32.add(
3537
+ table,
3538
+ this.module.i32.mul(
3539
+ index,
3540
+ this.module.i32.const(this.scalarSize(scalar)),
3541
+ ),
3542
+ );
3543
+ return this.loadScalar(scalar, address);
3544
+ };
3545
+ const load = (index) => {
3546
+ if (scalar !== "i32" || (offset !== 0 && offset !== 1)) {
3547
+ return rawLoad(index);
3548
+ }
3549
+ return this.resolveBufferPointer(
3550
+ () => rawLoad(index),
3551
+ offset === 1,
3552
+ context,
3553
+ );
3554
+ };
3555
+ if (staticSelector === null) return () => load(selector());
3556
+
3557
+ const key = `buffer_param:${parameterId}:${staticSelector}:${offset}:${scalar}`;
3558
+ let local = context.bufferDescriptorCache.get(key);
3559
+ if (local === undefined) {
3560
+ local = this.allocateGeneratedLocal(
3561
+ context,
3562
+ scalar,
3563
+ `buffer_param.component${offset}.${parameterId}.${staticSelector}`,
3564
+ );
3565
+ context.bufferDescriptorCache.set(key, local);
3566
+ context.entryInitializers.push(
3567
+ this.module.local.set(
3568
+ local,
3569
+ load(this.module.i32.const(staticSelector)),
3570
+ ),
3571
+ );
3572
+ }
3573
+ return () => this.module.local.get(local, this.wasmType(scalar));
3574
+ }
3575
+
3576
+ bufferParamChannelsFactory(
3577
+ parameterRef,
3578
+ selector,
3579
+ staticSelector,
3580
+ context,
3581
+ ) {
3582
+ const type = this.bufferParamType(parameterRef, context);
3583
+ const channels = this.bufferChannelMetadata(
3584
+ type.data.channels,
3585
+ type.data.element,
3586
+ );
3587
+ if (channels.kind === "mono") return () => this.module.i32.const(1);
3588
+ if (channels.kind === "static") {
3589
+ return () => this.module.i32.const(channels.count);
3590
+ }
3591
+ return this.bufferParamComponentFactory(
3592
+ parameterRef,
3593
+ 3,
1873
3594
  "i32",
3595
+ selector,
3596
+ staticSelector,
3597
+ context,
1874
3598
  );
1875
3599
  }
1876
3600
 
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,
3601
+ loadBufferParamComponent(parameterId, offset, scalar, context) {
3602
+ return this.withBufferParamSelector(
3603
+ parameterId,
3604
+ context,
3605
+ this.wasmType(scalar),
3606
+ (selector, staticSelector) => this.bufferParamComponentFactory(
3607
+ parameterId,
3608
+ offset,
3609
+ scalar,
3610
+ selector,
3611
+ staticSelector,
3612
+ context,
3613
+ )(),
3614
+ );
3615
+ }
3616
+
3617
+ loadBufferParamValue(parameterId, context) {
3618
+ const components = ["i32", "i32", "i32", "i32", "f32"];
3619
+ if (parameterId?.kind !== "array_element") {
3620
+ return components.map((scalar, offset) =>
3621
+ this.loadBufferParamComponent(parameterId, offset, scalar, context),
3622
+ );
3623
+ }
3624
+ const staticSelector = this.staticBufferParamSelector(parameterId, context);
3625
+ let selector;
3626
+ let initializeSelector = null;
3627
+ if (staticSelector === null) {
3628
+ const selectorLocal = this.allocateGeneratedLocal(
3629
+ context,
1884
3630
  "i32",
1885
- ),
3631
+ "buffer_param.selector",
3632
+ );
3633
+ selector = () => this.module.local.get(selectorLocal, binaryen.i32);
3634
+ initializeSelector = this.module.local.set(
3635
+ selectorLocal,
3636
+ this.compileBufferParamSelector(parameterId, context),
3637
+ );
3638
+ } else {
3639
+ selector = () => this.module.i32.const(staticSelector);
3640
+ }
3641
+ const values = components.map((scalar, offset) =>
3642
+ this.bufferParamComponentFactory(
3643
+ parameterId,
3644
+ offset,
3645
+ scalar,
3646
+ selector,
3647
+ staticSelector,
3648
+ context,
3649
+ )(),
3650
+ );
3651
+ if (initializeSelector !== null) {
3652
+ values[0] = this.module.block(
3653
+ null,
3654
+ [initializeSelector, values[0]],
3655
+ binaryen.i32,
3656
+ );
3657
+ }
3658
+ return values;
3659
+ }
3660
+
3661
+ loadBufferPlace(place, context) {
3662
+ if (place.base.kind !== "parameter" || place.projections.length !== 0) {
3663
+ this.fail("buffer call arguments must be unprojected buffer parameters");
3664
+ }
3665
+ const layout = this.bufferParamLayout(place.base.data, context);
3666
+ return layout.components.map((scalar, offset) =>
3667
+ this.module.local.get(layout.index + offset, this.wasmType(scalar)),
1886
3668
  );
1887
3669
  }
1888
3670
 
1889
- compileBufferParamLen(parameterId, context) {
1890
- this.bufferParamType(parameterId, context);
1891
- return this.loadBufferParamComponent(parameterId, 1, "i32", context);
3671
+ compileInterfaceBufferValue(bufferRef, context) {
3672
+ this.requireBufferRef(bufferRef);
3673
+ const staticIndex = this.staticBufferRefIndex(bufferRef);
3674
+ let descriptorIndex;
3675
+ let initializeIndex = null;
3676
+ if (staticIndex === null) {
3677
+ const indexLocal = this.allocateGeneratedLocal(
3678
+ context,
3679
+ "i32",
3680
+ "buffer.descriptor_index",
3681
+ );
3682
+ descriptorIndex = () =>
3683
+ this.module.local.get(indexLocal, binaryen.i32);
3684
+ initializeIndex = this.module.local.set(
3685
+ indexLocal,
3686
+ this.compileBufferRefIndex(bufferRef, context),
3687
+ );
3688
+ } else {
3689
+ descriptorIndex = () => this.module.i32.const(staticIndex);
3690
+ }
3691
+ const component = (globalName, scalar) => this.bufferTableValueFactory(
3692
+ globalName,
3693
+ descriptorIndex,
3694
+ staticIndex,
3695
+ scalar,
3696
+ context,
3697
+ )();
3698
+ const channels = this.bufferRefChannelMetadata(bufferRef);
3699
+ const values = [
3700
+ component(POINTER_GLOBALS.buffers, "i32"),
3701
+ component(POINTER_GLOBALS.bufferWrites, "i32"),
3702
+ component(POINTER_GLOBALS.bufferFrames, "i32"),
3703
+ channels.kind === "mono"
3704
+ ? this.module.i32.const(1)
3705
+ : channels.kind === "static"
3706
+ ? this.module.i32.const(channels.count)
3707
+ : component(POINTER_GLOBALS.bufferChannels, "i32"),
3708
+ component(POINTER_GLOBALS.bufferSampleRates, "f32"),
3709
+ ];
3710
+ if (initializeIndex !== null) {
3711
+ values[0] = this.module.block(
3712
+ null,
3713
+ [initializeIndex, values[0]],
3714
+ binaryen.i32,
3715
+ );
3716
+ }
3717
+ return values;
3718
+ }
3719
+
3720
+ compileBufferSpanValue(spanRef, expectedType, context) {
3721
+ const data = spanRef?.data;
3722
+ if (!data || data.len !== expectedType.data.len) {
3723
+ this.fail("buffer span argument length does not match parameter type");
3724
+ }
3725
+ const tableGlobals = [
3726
+ POINTER_GLOBALS.buffers,
3727
+ POINTER_GLOBALS.bufferWrites,
3728
+ POINTER_GLOBALS.bufferFrames,
3729
+ POINTER_GLOBALS.bufferChannels,
3730
+ POINTER_GLOBALS.bufferSampleRates,
3731
+ ];
3732
+ const tableScalars = ["i32", "i32", "i32", "i32", "f32"];
3733
+ let tables;
3734
+ let start;
3735
+ if (spanRef.kind === "interface") {
3736
+ if (
3737
+ !Number.isInteger(data.first)
3738
+ || data.first < 0
3739
+ || data.first + data.len > this.mir.interface.buffers.length
3740
+ ) {
3741
+ this.fail("interface buffer span is out of range");
3742
+ }
3743
+ tables = tableGlobals.map((name) => this.module.global.get(name, binaryen.i32));
3744
+ start = data.first;
3745
+ } else if (spanRef.kind === "parameter") {
3746
+ const parameter = context.function.params[data.span];
3747
+ const sourceType = parameter && this.type(parameter.ty);
3748
+ const layout = context.paramLayouts[data.span];
3749
+ if (
3750
+ !sourceType
3751
+ || sourceType.kind !== "buffer_span"
3752
+ || !layout
3753
+ || layout.kind !== "buffer_span"
3754
+ || !Number.isInteger(data.start)
3755
+ || data.start < 0
3756
+ || data.start + data.len > sourceType.data.len
3757
+ ) {
3758
+ this.fail("buffer span parameter window is out of range");
3759
+ }
3760
+ tables = layout.components.map((_, offset) =>
3761
+ this.module.local.get(layout.index + offset, binaryen.i32));
3762
+ start = data.start;
3763
+ } else {
3764
+ this.fail("invalid buffer span reference");
3765
+ }
3766
+ return tables.map((table, offset) => this.module.i32.add(
3767
+ table,
3768
+ this.module.i32.const(start * this.scalarSize(tableScalars[offset])),
3769
+ ));
1892
3770
  }
1893
3771
 
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),
3772
+ loadBufferTableValue(globalName, bufferRef, scalar, context) {
3773
+ this.requireBufferRef(bufferRef);
3774
+ return this.withBufferRefIndex(
3775
+ bufferRef,
3776
+ context,
3777
+ this.wasmType(scalar),
3778
+ (index, staticIndex) => this.bufferTableValueFactory(
3779
+ globalName,
3780
+ index,
3781
+ staticIndex,
3782
+ scalar,
3783
+ context,
3784
+ )(),
1899
3785
  );
1900
3786
  }
1901
3787
 
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`);
3788
+ bufferTableValueFactory(
3789
+ globalName,
3790
+ descriptorIndex,
3791
+ staticIndex,
3792
+ scalar,
3793
+ context,
3794
+ ) {
3795
+ const load = () => {
3796
+ const raw = () => this.loadBufferTableValueAt(
3797
+ globalName,
3798
+ descriptorIndex(),
3799
+ scalar,
3800
+ );
3801
+ if (
3802
+ scalar === "i32"
3803
+ && (globalName === POINTER_GLOBALS.buffers
3804
+ || globalName === POINTER_GLOBALS.bufferWrites)
3805
+ ) {
3806
+ return this.resolveBufferPointer(
3807
+ raw,
3808
+ globalName === POINTER_GLOBALS.bufferWrites,
3809
+ context,
3810
+ );
3811
+ }
3812
+ return raw();
3813
+ };
3814
+ if (staticIndex === null) {
3815
+ return load;
1907
3816
  }
1908
- return type;
3817
+ const key = `${globalName}:${staticIndex}:${scalar}`;
3818
+ let local = context.bufferDescriptorCache.get(key);
3819
+ if (local === undefined) {
3820
+ local = this.allocateGeneratedLocal(
3821
+ context,
3822
+ scalar,
3823
+ `buffer.${globalName.slice(6)}.${staticIndex}`,
3824
+ );
3825
+ context.bufferDescriptorCache.set(key, local);
3826
+ context.entryInitializers.push(
3827
+ this.module.local.set(
3828
+ local,
3829
+ load(),
3830
+ ),
3831
+ );
3832
+ }
3833
+ return () => this.module.local.get(local, this.wasmType(scalar));
1909
3834
  }
1910
3835
 
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`);
3836
+ bufferChannelsFactory(
3837
+ bufferRef,
3838
+ descriptorIndex,
3839
+ staticIndex,
3840
+ context,
3841
+ ) {
3842
+ const channels = this.bufferRefChannelMetadata(bufferRef);
3843
+ if (channels.kind === "mono") {
3844
+ return () => this.module.i32.const(1);
1915
3845
  }
1916
- return layout;
3846
+ if (channels.kind === "static") {
3847
+ return () => this.module.i32.const(channels.count);
3848
+ }
3849
+ return this.bufferTableValueFactory(
3850
+ POINTER_GLOBALS.bufferChannels,
3851
+ descriptorIndex,
3852
+ staticIndex,
3853
+ "i32",
3854
+ context,
3855
+ );
1917
3856
  }
1918
3857
 
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));
3858
+ loadBufferTableValueAt(globalName, descriptorIndex, scalar) {
3859
+ const size = this.scalarSize(scalar);
3860
+ const load = () => this.loadScalar(
3861
+ scalar,
3862
+ this.module.i32.add(
3863
+ this.module.global.get(globalName, binaryen.i32),
3864
+ this.module.i32.mul(
3865
+ descriptorIndex,
3866
+ this.module.i32.const(size),
3867
+ ),
3868
+ ),
3869
+ );
3870
+ return load();
1922
3871
  }
1923
3872
 
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)),
3873
+ resolveBufferPointer(load, write, context) {
3874
+ const local = this.allocateGeneratedLocal(
3875
+ context,
3876
+ "i32",
3877
+ write ? "buffer.write_or_discard" : "buffer.read_or_zero",
3878
+ );
3879
+ const pointer = () => this.module.local.get(local, binaryen.i32);
3880
+ return this.module.block(
3881
+ null,
3882
+ [
3883
+ this.module.local.set(local, load()),
3884
+ this.module.select(
3885
+ this.module.i32.ne(pointer(), this.module.i32.const(0)),
3886
+ pointer(),
3887
+ this.module.i32.const(
3888
+ write
3889
+ ? this.fallbackBufferWriteAddress
3890
+ : this.fallbackBufferReadAddress,
3891
+ ),
3892
+ ),
3893
+ ],
3894
+ binaryen.i32,
1931
3895
  );
1932
3896
  }
1933
3897
 
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"),
3898
+ withBufferRefIndex(bufferRef, context, resultType, build) {
3899
+ const staticIndex = this.staticBufferRefIndex(bufferRef);
3900
+ if (staticIndex !== null) {
3901
+ return build(
3902
+ () => this.module.i32.const(staticIndex),
3903
+ staticIndex,
3904
+ [],
3905
+ );
3906
+ }
3907
+ const indexLocal = this.allocateGeneratedLocal(
3908
+ context,
3909
+ "i32",
3910
+ "buffer.descriptor_index",
3911
+ );
3912
+ const prelude = [
3913
+ this.module.local.set(
3914
+ indexLocal,
3915
+ this.compileBufferRefIndex(bufferRef, context),
3916
+ ),
1941
3917
  ];
3918
+ const result = build(
3919
+ () => this.module.local.get(indexLocal, binaryen.i32),
3920
+ null,
3921
+ prelude,
3922
+ );
3923
+ return this.module.block(null, [...prelude, result], resultType);
1942
3924
  }
1943
3925
 
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),
3926
+ snapshotOperationValue(factory, scalar, name, prelude, context) {
3927
+ const local = this.allocateGeneratedLocal(context, scalar, name);
3928
+ prelude.push(
3929
+ this.module.local.set(local, factory()),
1950
3930
  );
1951
- return this.loadScalar(scalar, address);
3931
+ return () => this.module.local.get(local, this.wasmType(scalar));
3932
+ }
3933
+
3934
+ allocateGeneratedLocal(context, scalar, name) {
3935
+ const index = context.generatedLocalBase + context.generatedLocals.length;
3936
+ context.generatedLocals.push({
3937
+ index,
3938
+ scalar,
3939
+ name: `${name}.generated${context.generatedLocals.length}`,
3940
+ });
3941
+ return index;
1952
3942
  }
1953
3943
 
1954
3944
  compileSliceValue(value, context) {
@@ -1982,8 +3972,11 @@ class MirCompiler {
1982
3972
  }
1983
3973
  const header = () =>
1984
3974
  this.compileEventParamAddress(context.eventId, place.base.data);
3975
+ const address = () =>
3976
+ this.module.i32.add(header(), this.module.i32.const(4));
1985
3977
  return [
1986
- this.module.i32.add(header(), this.module.i32.const(4)),
3978
+ address(),
3979
+ address(),
1987
3980
  this.module.i32.load(0, 4, header()),
1988
3981
  this.module.i32.const(this.scalarSize(type.data.element)),
1989
3982
  ];
@@ -2043,7 +4036,7 @@ class MirCompiler {
2043
4036
  this.fail("slice assignment destination must be an unprojected local");
2044
4037
  }
2045
4038
  const layout = context.localLayouts[place.base.data];
2046
- if (!layout || layout.kind !== "slice" || components.length !== 3) {
4039
+ if (!layout || layout.kind !== "slice" || components.length !== 4) {
2047
4040
  this.fail(`local id ${place.base.data} is not a valid slice destination`);
2048
4041
  }
2049
4042
  return this.module.block(
@@ -2055,27 +4048,53 @@ class MirCompiler {
2055
4048
  }
2056
4049
 
2057
4050
  compileMakeSlice(data, context) {
2058
- const source = () => this.compileSliceSource(data.source, context);
4051
+ const sourceValues = this.compileSliceSource(data.source, context);
4052
+ const sourceLocals = sourceValues.map((_, offset) =>
4053
+ this.allocateGeneratedLocal(
4054
+ context,
4055
+ "i32",
4056
+ `slice.source_component${offset}`,
4057
+ ),
4058
+ );
4059
+ const initializeSource = sourceValues.map((value, offset) =>
4060
+ this.module.local.set(sourceLocals[offset], value),
4061
+ );
4062
+ const source = (offset) => () =>
4063
+ this.module.local.get(sourceLocals[offset], binaryen.i32);
2059
4064
  const range = this.compileSliceRange(
2060
4065
  () => this.compileValue(data.start, context),
2061
4066
  () => this.compileValue(data.len, context),
2062
- () => source()[1],
4067
+ source(2),
2063
4068
  data.bounds,
4069
+ context,
2064
4070
  );
2065
- return [
4071
+ const result = [
2066
4072
  this.module.i32.add(
2067
- source()[0],
4073
+ source(0)(),
2068
4074
  this.module.i32.mul(
2069
4075
  range.start(),
2070
- source()[2],
4076
+ source(3)(),
4077
+ ),
4078
+ ),
4079
+ this.module.i32.add(
4080
+ source(1)(),
4081
+ this.module.i32.mul(
4082
+ range.start(),
4083
+ source(3)(),
2071
4084
  ),
2072
4085
  ),
2073
4086
  range.len(),
2074
- source()[2],
4087
+ source(3)(),
2075
4088
  ];
4089
+ result[0] = this.module.block(
4090
+ null,
4091
+ [...initializeSource, result[0]],
4092
+ binaryen.i32,
4093
+ );
4094
+ return result;
2076
4095
  }
2077
4096
 
2078
- compileSliceRange(start, len, sourceLen, bounds) {
4097
+ compileSliceRange(start, len, sourceLen, bounds, context) {
2079
4098
  const zero = () => this.module.i32.const(0);
2080
4099
  if (bounds === "unchecked") {
2081
4100
  return { start, len };
@@ -2111,7 +4130,7 @@ class MirCompiler {
2111
4130
  };
2112
4131
  return { start: normalizedStart, len: normalizedLen };
2113
4132
  }
2114
- if (bounds === "trap") {
4133
+ if (bounds === "checked") {
2115
4134
  const invalid = () => {
2116
4135
  const remaining = () => this.module.i32.sub(sourceLen(), start());
2117
4136
  return this.module.i32.or(
@@ -2127,7 +4146,7 @@ class MirCompiler {
2127
4146
  };
2128
4147
  return {
2129
4148
  start: () =>
2130
- this.module.if(invalid(), this.module.unreachable(), start()),
4149
+ this.module.if(invalid(), this.raiseRuntimeFailure(context), start()),
2131
4150
  len,
2132
4151
  };
2133
4152
  }
@@ -2149,6 +4168,7 @@ class MirCompiler {
2149
4168
  this.fail("slice array source must have primitive elements");
2150
4169
  }
2151
4170
  return [
4171
+ this.placeAddress(source.data, context),
2152
4172
  this.placeAddress(source.data, context),
2153
4173
  this.module.i32.const(type.data.len),
2154
4174
  this.module.i32.const(this.scalarSize(element.data)),
@@ -2158,79 +4178,229 @@ class MirCompiler {
2158
4178
  const item = this.constLayout[source.data];
2159
4179
  if (!item) this.fail(`const data id ${source.data} is out of range`);
2160
4180
  return [
4181
+ this.module.i32.const(item.address),
2161
4182
  this.module.i32.const(item.address),
2162
4183
  this.module.i32.const(item.len),
2163
4184
  this.module.i32.const(this.scalarSize(item.scalar)),
2164
4185
  ];
2165
4186
  }
2166
4187
  if (source.kind === "buffer") {
2167
- const buffer = this.requireBuffer(source.data.buffer);
4188
+ const buffer = this.requireBufferRef(source.data.buffer);
2168
4189
  const elementSize = this.scalarSize(buffer.element);
2169
- const address = this.loadBufferTableValue(
2170
- POINTER_GLOBALS.buffers,
4190
+ const staticIndex = this.staticBufferRefIndex(source.data.buffer);
4191
+ const prelude = [];
4192
+ let descriptorIndex;
4193
+ if (staticIndex === null) {
4194
+ const indexLocal = this.allocateGeneratedLocal(
4195
+ context,
4196
+ "i32",
4197
+ "buffer.descriptor_index",
4198
+ );
4199
+ prelude.push(
4200
+ this.module.local.set(
4201
+ indexLocal,
4202
+ this.compileBufferRefIndex(source.data.buffer, context),
4203
+ ),
4204
+ );
4205
+ descriptorIndex = () =>
4206
+ this.module.local.get(indexLocal, binaryen.i32);
4207
+ } else {
4208
+ descriptorIndex = () => this.module.i32.const(staticIndex);
4209
+ }
4210
+ const component = (globalName, scalar) => this.bufferTableValueFactory(
4211
+ globalName,
4212
+ descriptorIndex,
4213
+ staticIndex,
4214
+ scalar,
4215
+ context,
4216
+ );
4217
+ let channels = this.bufferChannelsFactory(
2171
4218
  source.data.buffer,
2172
- "i32",
4219
+ descriptorIndex,
4220
+ staticIndex,
4221
+ context,
2173
4222
  );
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,
4223
+ if (
4224
+ staticIndex === null
4225
+ && this.bufferRefChannelMetadata(source.data.buffer).kind === "dynamic"
4226
+ ) {
4227
+ channels = this.snapshotOperationValue(
4228
+ channels,
2185
4229
  "i32",
4230
+ "buffer.channels",
4231
+ prelude,
4232
+ context,
2186
4233
  );
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,
4234
+ }
4235
+ const readAddress = component(POINTER_GLOBALS.buffers, "i32");
4236
+ const writeAddress = component(POINTER_GLOBALS.bufferWrites, "i32");
4237
+ let read = readAddress();
4238
+ let write = writeAddress();
4239
+ if (source.data.channel !== null) {
4240
+ const channelLocal = this.allocateGeneratedLocal(
4241
+ context,
2201
4242
  "i32",
2202
- ),
2203
- this.module.i32.mul(channels(), this.module.i32.const(elementSize)),
4243
+ "buffer.slice_channel",
4244
+ );
4245
+ prelude.push(
4246
+ this.module.local.set(
4247
+ channelLocal,
4248
+ this.compileDynamicBoundedIndex(
4249
+ () => this.compileValue(source.data.channel, context),
4250
+ channels,
4251
+ "clamp",
4252
+ context,
4253
+ true,
4254
+ ),
4255
+ ),
4256
+ );
4257
+ const channelOffset = () => this.module.i32.mul(
4258
+ this.module.local.get(channelLocal, binaryen.i32),
4259
+ this.module.i32.const(elementSize),
4260
+ );
4261
+ read = this.bufferPointerWithOffset(
4262
+ readAddress,
4263
+ channelOffset(),
4264
+ false,
4265
+ context,
4266
+ );
4267
+ write = this.bufferPointerWithOffset(
4268
+ writeAddress,
4269
+ channelOffset(),
4270
+ true,
4271
+ context,
4272
+ );
4273
+ }
4274
+ const result = [
4275
+ read,
4276
+ write,
4277
+ component(POINTER_GLOBALS.bufferFrames, "i32")(),
4278
+ source.data.channel === null
4279
+ ? this.module.i32.const(elementSize)
4280
+ : this.module.i32.mul(channels(), this.module.i32.const(elementSize)),
2204
4281
  ];
4282
+ if (prelude.length > 0) {
4283
+ result[0] = this.module.block(
4284
+ null,
4285
+ [...prelude, result[0]],
4286
+ binaryen.i32,
4287
+ );
4288
+ }
4289
+ return result;
2205
4290
  }
2206
4291
  if (source.kind === "buffer_param") {
2207
4292
  const type = this.bufferParamType(source.data.parameter, context);
2208
4293
  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
- ];
4294
+ const staticSelector = this.staticBufferParamSelector(
4295
+ source.data.parameter,
4296
+ context,
4297
+ );
4298
+ const prelude = [];
4299
+ let selector = null;
4300
+ if (
4301
+ source.data.parameter?.kind === "array_element"
4302
+ && staticSelector === null
4303
+ ) {
4304
+ const selectorLocal = this.allocateGeneratedLocal(
4305
+ context,
4306
+ "i32",
4307
+ "buffer_param.selector",
4308
+ );
4309
+ prelude.push(
4310
+ this.module.local.set(
4311
+ selectorLocal,
4312
+ this.compileBufferParamSelector(source.data.parameter, context),
4313
+ ),
4314
+ );
4315
+ selector = () => this.module.local.get(selectorLocal, binaryen.i32);
4316
+ } else if (staticSelector !== null) {
4317
+ selector = () => this.module.i32.const(staticSelector);
2217
4318
  }
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,
4319
+ const component = (offset, scalar) => this.bufferParamComponentFactory(
4320
+ source.data.parameter,
4321
+ offset,
4322
+ scalar,
4323
+ selector,
4324
+ staticSelector,
4325
+ context,
2225
4326
  );
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)),
4327
+ let channels = this.bufferParamChannelsFactory(
4328
+ source.data.parameter,
4329
+ selector,
4330
+ staticSelector,
4331
+ context,
4332
+ );
4333
+ if (
4334
+ source.data.parameter?.kind === "array_element"
4335
+ && staticSelector === null
4336
+ && this.bufferChannelMetadata(
4337
+ type.data.channels,
4338
+ type.data.element,
4339
+ ).kind === "dynamic"
4340
+ ) {
4341
+ channels = this.snapshotOperationValue(
4342
+ channels,
4343
+ "i32",
4344
+ "buffer_param.channels",
4345
+ prelude,
4346
+ context,
4347
+ );
4348
+ }
4349
+ const readAddress = component(0, "i32");
4350
+ const writeAddress = component(1, "i32");
4351
+ let read = readAddress();
4352
+ let write = writeAddress();
4353
+ if (source.data.channel !== null) {
4354
+ const channelLocal = this.allocateGeneratedLocal(
4355
+ context,
4356
+ "i32",
4357
+ "buffer_param.slice_channel",
4358
+ );
4359
+ prelude.push(
4360
+ this.module.local.set(
4361
+ channelLocal,
4362
+ this.compileDynamicBoundedIndex(
4363
+ () => this.compileValue(source.data.channel, context),
4364
+ channels,
4365
+ "clamp",
4366
+ context,
4367
+ true,
4368
+ ),
4369
+ ),
4370
+ );
4371
+ const channelOffset = () => this.module.i32.mul(
4372
+ this.module.local.get(channelLocal, binaryen.i32),
4373
+ this.module.i32.const(elementSize),
4374
+ );
4375
+ read = this.bufferPointerWithOffset(
4376
+ readAddress,
4377
+ channelOffset(),
4378
+ false,
4379
+ context,
4380
+ );
4381
+ write = this.bufferPointerWithOffset(
4382
+ writeAddress,
4383
+ channelOffset(),
4384
+ true,
4385
+ context,
4386
+ );
4387
+ }
4388
+ const result = [
4389
+ read,
4390
+ write,
4391
+ component(2, "i32")(),
4392
+ source.data.channel === null
4393
+ ? this.module.i32.const(elementSize)
4394
+ : this.module.i32.mul(channels(), this.module.i32.const(elementSize)),
2233
4395
  ];
4396
+ if (prelude.length > 0) {
4397
+ result[0] = this.module.block(
4398
+ null,
4399
+ [...prelude, result[0]],
4400
+ binaryen.i32,
4401
+ );
4402
+ }
4403
+ return result;
2234
4404
  }
2235
4405
  this.fail(`unsupported slice source '${String(source.kind)}'`);
2236
4406
  }
@@ -2255,23 +4425,26 @@ class MirCompiler {
2255
4425
  return type.data.access;
2256
4426
  }
2257
4427
 
2258
- compileSliceAddress(slice, index, bounds, context) {
4428
+ compileSliceAddress(slice, index, bounds, context, write) {
2259
4429
  return this.compileSliceAddressWithFactories(
2260
4430
  () => this.compileSliceValue(slice, context),
2261
4431
  () => this.compileValue(index, context),
2262
4432
  bounds,
4433
+ context,
4434
+ write,
2263
4435
  );
2264
4436
  }
2265
4437
 
2266
- compileSliceAddressWithFactories(slice, index, bounds) {
4438
+ compileSliceAddressWithFactories(slice, index, bounds, context, write) {
2267
4439
  const bounded = this.compileDynamicBoundedIndex(
2268
4440
  index,
2269
- () => slice()[1],
4441
+ () => slice()[2],
2270
4442
  bounds,
4443
+ context,
2271
4444
  );
2272
4445
  return this.module.i32.add(
2273
- slice()[0],
2274
- this.module.i32.mul(bounded, slice()[2]),
4446
+ slice()[write ? 1 : 0],
4447
+ this.module.i32.mul(bounded, slice()[3]),
2275
4448
  );
2276
4449
  }
2277
4450
 
@@ -2295,6 +4468,7 @@ class MirCompiler {
2295
4468
  sourceType.data.len - parameterType.data.len,
2296
4469
  ),
2297
4470
  data.bounds,
4471
+ context,
2298
4472
  );
2299
4473
  return this.module.i32.add(
2300
4474
  this.placeAddress(data.array, context),
@@ -2302,7 +4476,7 @@ class MirCompiler {
2302
4476
  );
2303
4477
  }
2304
4478
 
2305
- compileSliceWindowAddress(data, parameterType, context) {
4479
+ compileSliceWindowAddress(data, parameterType, context, write) {
2306
4480
  const elementType = this.type(parameterType.data.element);
2307
4481
  if (elementType.kind !== "scalar") {
2308
4482
  this.fail("slice-window fixed-array parameter element is not scalar");
@@ -2314,15 +4488,16 @@ class MirCompiler {
2314
4488
  () => this.compileValue(data.start, context),
2315
4489
  () =>
2316
4490
  this.module.i32.sub(
2317
- slice()[1],
4491
+ slice()[2],
2318
4492
  this.module.i32.const(requiredLen),
2319
4493
  ),
2320
4494
  data.bounds,
4495
+ context,
2321
4496
  );
2322
4497
  const address = () =>
2323
4498
  this.module.i32.add(
2324
- slice()[0],
2325
- this.module.i32.mul(start(), slice()[2]),
4499
+ slice()[write ? 1 : 0],
4500
+ this.module.i32.mul(start(), slice()[3]),
2326
4501
  );
2327
4502
  if (data.bounds === "unchecked") {
2328
4503
  return address();
@@ -2330,22 +4505,22 @@ class MirCompiler {
2330
4505
  const invalidShape = () =>
2331
4506
  this.module.i32.or(
2332
4507
  this.module.i32.ne(
2333
- slice()[2],
4508
+ slice()[3],
2334
4509
  this.module.i32.const(elementSize),
2335
4510
  ),
2336
4511
  this.module.i32.lt_s(
2337
- slice()[1],
4512
+ slice()[2],
2338
4513
  this.module.i32.const(requiredLen),
2339
4514
  ),
2340
4515
  );
2341
4516
  return this.module.if(
2342
4517
  invalidShape(),
2343
- this.module.unreachable(),
4518
+ this.raiseRuntimeFailure(context),
2344
4519
  address(),
2345
4520
  );
2346
4521
  }
2347
4522
 
2348
- compileWindowStart(start, maximum, bounds) {
4523
+ compileWindowStart(start, maximum, bounds, context) {
2349
4524
  const zero = () => this.module.i32.const(0);
2350
4525
  if (bounds === "unchecked") {
2351
4526
  return start;
@@ -2365,14 +4540,14 @@ class MirCompiler {
2365
4540
  );
2366
4541
  };
2367
4542
  }
2368
- if (bounds === "trap") {
4543
+ if (bounds === "checked") {
2369
4544
  return () =>
2370
4545
  this.module.if(
2371
4546
  this.module.i32.or(
2372
4547
  this.module.i32.lt_s(start(), zero()),
2373
4548
  this.module.i32.gt_s(start(), maximum()),
2374
4549
  ),
2375
- this.module.unreachable(),
4550
+ this.raiseRuntimeFailure(context),
2376
4551
  start(),
2377
4552
  );
2378
4553
  }
@@ -2383,7 +4558,7 @@ class MirCompiler {
2383
4558
  const scalar = this.sliceElementScalar(data.slice, context);
2384
4559
  return this.loadScalar(
2385
4560
  scalar,
2386
- this.compileSliceAddress(data.slice, data.index, data.bounds, context),
4561
+ this.compileSliceAddress(data.slice, data.index, data.bounds, context, false),
2387
4562
  );
2388
4563
  }
2389
4564
 
@@ -2394,7 +4569,7 @@ class MirCompiler {
2394
4569
  const scalar = this.sliceElementScalar(data.slice, context);
2395
4570
  return this.storeScalar(
2396
4571
  scalar,
2397
- this.compileSliceAddress(data.slice, data.index, data.bounds, context),
4572
+ this.compileSliceAddress(data.slice, data.index, data.bounds, context, true),
2398
4573
  this.compileValue(data.value, context),
2399
4574
  );
2400
4575
  }
@@ -2417,14 +4592,14 @@ class MirCompiler {
2417
4592
  const scalarSize = this.scalarSize(scalar);
2418
4593
  const address = () =>
2419
4594
  this.module.i32.add(
2420
- destination()[0],
2421
- this.module.i32.mul(counterValue(), destination()[2]),
4595
+ destination()[1],
4596
+ this.module.i32.mul(counterValue(), destination()[3]),
2422
4597
  );
2423
4598
  const scalarLoop = () =>
2424
4599
  this.module.loop(
2425
4600
  scalarLoopLabel,
2426
4601
  this.module.if(
2427
- this.module.i32.lt_s(counterValue(), destination()[1]),
4602
+ this.module.i32.lt_s(counterValue(), destination()[2]),
2428
4603
  this.module.block(null, [
2429
4604
  this.storeScalar(
2430
4605
  scalar,
@@ -2444,18 +4619,18 @@ class MirCompiler {
2444
4619
  const lanes = 16 / scalarSize;
2445
4620
  const vectorCondition = this.module.i32.and(
2446
4621
  this.module.i32.eq(
2447
- destination()[2],
4622
+ destination()[3],
2448
4623
  this.module.i32.const(scalarSize),
2449
4624
  ),
2450
4625
  this.module.i32.and(
2451
4626
  this.module.i32.ge_u(
2452
- destination()[1],
4627
+ destination()[2],
2453
4628
  this.module.i32.const(lanes),
2454
4629
  ),
2455
4630
  this.module.i32.le_u(
2456
4631
  counterValue(),
2457
4632
  this.module.i32.sub(
2458
- destination()[1],
4633
+ destination()[2],
2459
4634
  this.module.i32.const(lanes),
2460
4635
  ),
2461
4636
  ),
@@ -2515,8 +4690,8 @@ class MirCompiler {
2515
4690
  const copyIndex = () =>
2516
4691
  this.module.select(
2517
4692
  this.module.i32.and(
2518
- this.module.i32.eq(destination()[2], source()[2]),
2519
- this.module.i32.gt_u(destination()[0], source()[0]),
4693
+ this.module.i32.eq(destination()[3], source()[3]),
4694
+ this.module.i32.gt_u(destination()[1], source()[0]),
2520
4695
  ),
2521
4696
  this.module.i32.sub(
2522
4697
  this.module.i32.sub(countValue(), this.module.i32.const(1)),
@@ -2527,12 +4702,12 @@ class MirCompiler {
2527
4702
  const sourceAddress = () =>
2528
4703
  this.module.i32.add(
2529
4704
  source()[0],
2530
- this.module.i32.mul(copyIndex(), source()[2]),
4705
+ this.module.i32.mul(copyIndex(), source()[3]),
2531
4706
  );
2532
4707
  const destinationAddress = () =>
2533
4708
  this.module.i32.add(
2534
- destination()[0],
2535
- this.module.i32.mul(copyIndex(), destination()[2]),
4709
+ destination()[1],
4710
+ this.module.i32.mul(copyIndex(), destination()[3]),
2536
4711
  );
2537
4712
  const sourceScalar = this.sliceElementScalar(data.source, context);
2538
4713
  const destinationScalar = this.sliceElementScalar(data.destination, context);
@@ -2550,7 +4725,7 @@ class MirCompiler {
2550
4725
  source()[0],
2551
4726
  this.module.i32.mul(
2552
4727
  this.module.i32.sub(countValue(), this.module.i32.const(1)),
2553
- source()[2],
4728
+ source()[3],
2554
4729
  ),
2555
4730
  ),
2556
4731
  this.module.i32.const(this.scalarSize(sourceScalar)),
@@ -2558,10 +4733,10 @@ class MirCompiler {
2558
4733
  const destinationEnd = () =>
2559
4734
  this.module.i32.add(
2560
4735
  this.module.i32.add(
2561
- destination()[0],
4736
+ destination()[1],
2562
4737
  this.module.i32.mul(
2563
4738
  this.module.i32.sub(countValue(), this.module.i32.const(1)),
2564
- destination()[2],
4739
+ destination()[3],
2565
4740
  ),
2566
4741
  ),
2567
4742
  this.module.i32.const(this.scalarSize(destinationScalar)),
@@ -2570,13 +4745,13 @@ class MirCompiler {
2570
4745
  this.module.i32.and(
2571
4746
  nonEmpty(),
2572
4747
  this.module.i32.and(
2573
- this.module.i32.lt_u(destination()[0], sourceEnd()),
4748
+ this.module.i32.lt_u(destination()[1], sourceEnd()),
2574
4749
  this.module.i32.lt_u(source()[0], destinationEnd()),
2575
4750
  ),
2576
4751
  );
2577
4752
  const invalidOverlap = () =>
2578
4753
  this.module.i32.and(
2579
- this.module.i32.ne(destination()[2], source()[2]),
4754
+ this.module.i32.ne(destination()[3], source()[3]),
2580
4755
  overlaps(),
2581
4756
  );
2582
4757
  const scalarCopy = () =>
@@ -2599,18 +4774,18 @@ class MirCompiler {
2599
4774
  ? this.module.if(
2600
4775
  this.module.i32.and(
2601
4776
  this.module.i32.eq(
2602
- destination()[2],
4777
+ destination()[3],
2603
4778
  this.module.i32.const(this.scalarSize(destinationScalar)),
2604
4779
  ),
2605
4780
  this.module.i32.eq(
2606
- source()[2],
4781
+ source()[3],
2607
4782
  this.module.i32.const(this.scalarSize(sourceScalar)),
2608
4783
  ),
2609
4784
  ),
2610
4785
  // memory.copy has memmove overlap semantics and lets engines use
2611
4786
  // their tuned bulk-memory implementation for contiguous slices.
2612
4787
  this.module.memory.copy(
2613
- destination()[0],
4788
+ destination()[1],
2614
4789
  source()[0],
2615
4790
  this.module.i32.mul(
2616
4791
  countValue(),
@@ -2624,12 +4799,12 @@ class MirCompiler {
2624
4799
  this.module.local.set(
2625
4800
  count,
2626
4801
  this.module.select(
2627
- this.module.i32.lt_s(destination()[1], source()[1]),
2628
- destination()[1],
2629
- source()[1],
4802
+ this.module.i32.lt_s(destination()[2], source()[2]),
4803
+ destination()[2],
4804
+ source()[2],
2630
4805
  ),
2631
4806
  ),
2632
- this.module.if(invalidOverlap(), this.module.unreachable()),
4807
+ this.module.if(invalidOverlap(), this.raiseRuntimeFailure(context)),
2633
4808
  this.module.local.set(counter, this.module.i32.const(0)),
2634
4809
  copy,
2635
4810
  ]);
@@ -2892,7 +5067,7 @@ class MirCompiler {
2892
5067
  ),
2893
5068
  );
2894
5069
  }
2895
- if (bounds === "trap") {
5070
+ if (bounds === "checked") {
2896
5071
  const outOfBounds = this.module.i32.or(
2897
5072
  this.module.i32.lt_s(
2898
5073
  this.compileValue(value, context),
@@ -2905,14 +5080,20 @@ class MirCompiler {
2905
5080
  );
2906
5081
  return this.module.if(
2907
5082
  outOfBounds,
2908
- this.module.unreachable(),
5083
+ this.raiseRuntimeFailure(context),
2909
5084
  this.compileValue(value, context),
2910
5085
  );
2911
5086
  }
2912
5087
  this.fail(`unknown bounds mode '${String(bounds)}'`);
2913
5088
  }
2914
5089
 
2915
- compileDynamicBoundedIndex(index, length, bounds, clampLengthKnownPositive = false) {
5090
+ compileDynamicBoundedIndex(
5091
+ index,
5092
+ length,
5093
+ bounds,
5094
+ context,
5095
+ clampLengthKnownPositive = false,
5096
+ ) {
2916
5097
  if (bounds === "unchecked") {
2917
5098
  return index();
2918
5099
  }
@@ -2932,17 +5113,17 @@ class MirCompiler {
2932
5113
  if (clampLengthKnownPositive) return clamped();
2933
5114
  return this.module.if(
2934
5115
  this.module.i32.le_s(length(), this.module.i32.const(0)),
2935
- this.module.unreachable(),
5116
+ this.raiseRuntimeFailure(context),
2936
5117
  clamped(),
2937
5118
  );
2938
5119
  }
2939
- if (bounds === "trap") {
5120
+ if (bounds === "checked") {
2940
5121
  return this.module.if(
2941
5122
  this.module.i32.or(
2942
5123
  this.module.i32.lt_s(index(), this.module.i32.const(0)),
2943
5124
  this.module.i32.ge_s(index(), length()),
2944
5125
  ),
2945
- this.module.unreachable(),
5126
+ this.raiseRuntimeFailure(context),
2946
5127
  index(),
2947
5128
  );
2948
5129
  }
@@ -2966,7 +5147,7 @@ class MirCompiler {
2966
5147
  }
2967
5148
  }
2968
5149
 
2969
- compileBinary(op, scalar, lhs, rhs) {
5150
+ compileBinary(op, scalar, lhs, rhs, context) {
2970
5151
  if (!supportsMirOperation("binary", op, scalar)) {
2971
5152
  this.fail(`binary operation '${String(op)}' does not support scalar '${scalar}'`);
2972
5153
  }
@@ -2990,13 +5171,26 @@ class MirCompiler {
2990
5171
  wasm.eq(lhs(), minimum()),
2991
5172
  wasm.eq(rhs(), negativeOne()),
2992
5173
  );
2993
- return this.module.if(overflow, minimum(), wasm.div_s(lhs(), rhs()));
5174
+ const division = this.module.if(
5175
+ overflow,
5176
+ minimum(),
5177
+ wasm.div_s(lhs(), rhs()),
5178
+ );
5179
+ return this.module.if(
5180
+ wasm.eq(rhs(), this.zero(scalar)),
5181
+ this.raiseRuntimeFailure(context),
5182
+ division,
5183
+ );
2994
5184
  }
2995
5185
  case "remainder":
2996
5186
  if (!integer) {
2997
5187
  return this.compileMathKernelCall("remainder", scalar, [lhs(), rhs()]);
2998
5188
  }
2999
- return wasm.rem_s(lhs(), rhs());
5189
+ return this.module.if(
5190
+ wasm.eq(rhs(), this.zero(scalar)),
5191
+ this.raiseRuntimeFailure(context),
5192
+ wasm.rem_s(lhs(), rhs()),
5193
+ );
3000
5194
  case "bit_and": return wasm.and(lhs(), rhs());
3001
5195
  case "bit_or": return wasm.or(lhs(), rhs());
3002
5196
  case "bit_xor": return wasm.xor(lhs(), rhs());
@@ -3083,6 +5277,16 @@ class MirCompiler {
3083
5277
  return this.module.select(wasm.lt_s(arg(0), arg(1)), arg(0), arg(1));
3084
5278
  case "max":
3085
5279
  return this.module.select(wasm.gt_s(arg(0), arg(1)), arg(0), arg(1));
5280
+ case "range_clamp":
5281
+ return this.module.select(
5282
+ wasm.lt_s(arg(0), arg(1)),
5283
+ arg(1),
5284
+ this.module.select(
5285
+ wasm.gt_s(arg(0), arg(2)),
5286
+ arg(2),
5287
+ arg(0),
5288
+ ),
5289
+ );
3086
5290
  default:
3087
5291
  this.fail(`intrinsic '${data.intrinsic}' requires f32 or f64 operands`);
3088
5292
  }
@@ -3096,6 +5300,22 @@ class MirCompiler {
3096
5300
  case "trunc": return wasm.trunc(args[0]);
3097
5301
  case "min": return wasm.min(args[0], args[1]);
3098
5302
  case "max": return wasm.max(args[0], args[1]);
5303
+ case "range_clamp": {
5304
+ const arg = (index) => this.compileValue(data.args[index], context);
5305
+ return this.module.select(
5306
+ wasm.ne(arg(0), arg(0)),
5307
+ arg(1),
5308
+ this.module.select(
5309
+ wasm.lt(arg(0), arg(1)),
5310
+ arg(1),
5311
+ this.module.select(
5312
+ wasm.gt(arg(0), arg(2)),
5313
+ arg(2),
5314
+ arg(0),
5315
+ ),
5316
+ ),
5317
+ );
5318
+ }
3099
5319
  case "fma": return this.compileMathKernelCall(data.intrinsic, scalar, args);
3100
5320
  case "sin":
3101
5321
  case "cos":
@@ -3205,6 +5425,12 @@ class MirCompiler {
3205
5425
  lhs.data.element === rhs.data.element &&
3206
5426
  JSON.stringify(lhs.data.channels) === JSON.stringify(rhs.data.channels) &&
3207
5427
  lhs.data.access === rhs.data.access;
5428
+ } else if (lhs.kind === "buffer_span") {
5429
+ equivalent =
5430
+ lhs.data.element === rhs.data.element &&
5431
+ JSON.stringify(lhs.data.channels) === JSON.stringify(rhs.data.channels) &&
5432
+ lhs.data.access === rhs.data.access &&
5433
+ lhs.data.len === rhs.data.len;
3208
5434
  } else if (lhs.kind === "tuple") {
3209
5435
  equivalent =
3210
5436
  lhs.data.length === rhs.data.length &&
@@ -3311,6 +5537,85 @@ class MirCompiler {
3311
5537
  return this.mir.interface.buffers[id];
3312
5538
  }
3313
5539
 
5540
+ bufferRefFirst(bufferRef) {
5541
+ if (Number.isInteger(bufferRef)) return bufferRef;
5542
+ if (bufferRef?.kind === "direct" && Number.isInteger(bufferRef.data)) {
5543
+ return bufferRef.data;
5544
+ }
5545
+ if (
5546
+ bufferRef?.kind === "array_element"
5547
+ && Number.isInteger(bufferRef.data?.first)
5548
+ && Number.isInteger(bufferRef.data?.len)
5549
+ && bufferRef.data.len > 0
5550
+ ) {
5551
+ return bufferRef.data.first;
5552
+ }
5553
+ this.fail("invalid MIR buffer reference");
5554
+ }
5555
+
5556
+ requireBufferRef(bufferRef) {
5557
+ const first = this.bufferRefFirst(bufferRef);
5558
+ const buffer = this.requireBuffer(first);
5559
+ if (bufferRef?.kind === "array_element") {
5560
+ const last = first + bufferRef.data.len - 1;
5561
+ this.requireBuffer(last);
5562
+ for (let id = first + 1; id <= last; id += 1) {
5563
+ const candidate = this.requireBuffer(id);
5564
+ if (
5565
+ candidate.element !== buffer.element
5566
+ || JSON.stringify(candidate.channels) !== JSON.stringify(buffer.channels)
5567
+ || candidate.access !== buffer.access
5568
+ ) {
5569
+ this.fail("buffer-array elements must have one descriptor type");
5570
+ }
5571
+ }
5572
+ }
5573
+ return buffer;
5574
+ }
5575
+
5576
+ bufferRefChannelMetadata(bufferRef) {
5577
+ const buffer = this.requireBufferRef(bufferRef);
5578
+ return this.bufferChannelMetadata(buffer.channels, buffer.element);
5579
+ }
5580
+
5581
+ compileBufferRefIndex(bufferRef, context) {
5582
+ const staticIndex = this.staticBufferRefIndex(bufferRef);
5583
+ if (staticIndex !== null) return this.module.i32.const(staticIndex);
5584
+ const first = this.bufferRefFirst(bufferRef);
5585
+ const data = bufferRef.data;
5586
+ const selector = this.compileDynamicBoundedIndex(
5587
+ () => this.compileValue(data.selector, context),
5588
+ () => this.module.i32.const(data.len),
5589
+ data.bounds,
5590
+ context,
5591
+ true,
5592
+ );
5593
+ return this.module.i32.add(this.module.i32.const(first), selector);
5594
+ }
5595
+
5596
+ staticBufferRefIndex(bufferRef) {
5597
+ const first = this.bufferRefFirst(bufferRef);
5598
+ if (Number.isInteger(bufferRef) || bufferRef.kind === "direct") return first;
5599
+ const data = bufferRef.data;
5600
+ const selector = data.selector;
5601
+ if (
5602
+ selector?.kind !== "constant"
5603
+ || selector.data?.type !== "i32"
5604
+ || !Number.isInteger(selector.data.value)
5605
+ ) {
5606
+ return null;
5607
+ }
5608
+ let index = selector.data.value;
5609
+ if (data.bounds === "clamp") {
5610
+ index = Math.min(data.len - 1, Math.max(0, index));
5611
+ } else if (index < 0 || index >= data.len) {
5612
+ // Preserve checked failure behavior and leave invalid unchecked MIR to
5613
+ // the normal validated lowering path.
5614
+ return null;
5615
+ }
5616
+ return first + index;
5617
+ }
5618
+
3314
5619
  currentLabel(labels, statement) {
3315
5620
  const label = labels.at(-1);
3316
5621
  if (!label) this.fail(`'${statement}' appears outside a MIR loop`);
@@ -3418,6 +5723,7 @@ class MirCompiler {
3418
5723
  default_reprs: null,
3419
5724
  range_min_repr: null,
3420
5725
  range_max_repr: null,
5726
+ param_control: null,
3421
5727
  })),
3422
5728
  params: this.mir.interface.params.map((param, id) => ({
3423
5729
  name: param.name,
@@ -3432,9 +5738,18 @@ class MirCompiler {
3432
5738
  default_reprs: this.constantReprs(param.default),
3433
5739
  range_min_repr: this.scalarRepr(param.range?.min),
3434
5740
  range_max_repr: this.scalarRepr(param.range?.max),
5741
+ param_control: this.storageShape(param.ty).length === 1 && param.range
5742
+ ? {
5743
+ scale: param.control.scale,
5744
+ curve: param.control.curve,
5745
+ unit: param.control.unit,
5746
+ step_repr: this.scalarRepr(param.control.step),
5747
+ step_count: param.control.step_count,
5748
+ }
5749
+ : null,
3435
5750
  })),
3436
- buffers: this.mir.interface.buffers.map((buffer) => {
3437
- const channels = this.bufferChannelMetadata(buffer.channels);
5751
+ buffers: this.mir.interface.buffers.map((buffer, bufferId) => {
5752
+ const channels = this.bufferChannelMetadata(buffer.channels, buffer.element);
3438
5753
  return {
3439
5754
  name: buffer.name,
3440
5755
  type_repr: this.bufferTypeRepr(buffer, channels),
@@ -3443,9 +5758,14 @@ class MirCompiler {
3443
5758
  channels: channels.kind,
3444
5759
  static_channels: channels.count,
3445
5760
  access: buffer.access,
3446
- may_write: buffer.access === "read_write",
5761
+ may_write: this.bufferMayWrite[bufferId],
3447
5762
  };
3448
5763
  }),
5764
+ buffer_arrays: (this.mir.interface.buffer_arrays ?? []).map((array) => ({
5765
+ name: array.name,
5766
+ first_buffer: array.first,
5767
+ len: array.len,
5768
+ })),
3449
5769
  events: this.mir.interface.events.map((event, eventId) => ({
3450
5770
  name: event.name,
3451
5771
  export: `onda_event_${eventId}`,
@@ -3509,6 +5829,7 @@ class MirCompiler {
3509
5829
  default_reprs: null,
3510
5830
  range_min_repr: null,
3511
5831
  range_max_repr: null,
5832
+ param_control: null,
3512
5833
  }));
3513
5834
  }
3514
5835
 
@@ -3536,11 +5857,11 @@ class MirCompiler {
3536
5857
  }
3537
5858
 
3538
5859
  bufferTypeRepr(buffer, channels) {
3539
- if (channels.kind === "mono") return `buffer[${buffer.element}]`;
5860
+ if (channels.kind === "mono") return `buffer<${buffer.element}>`;
3540
5861
  if (channels.kind === "static") {
3541
- return `buffer[${buffer.element}[${channels.count}]]`;
5862
+ return `buffer<${buffer.element}[${channels.count}]>`;
3542
5863
  }
3543
- return `buffer[${buffer.element}[]]`;
5864
+ return `buffer<${buffer.element}[]>`;
3544
5865
  }
3545
5866
 
3546
5867
  storageShape(typeId) {
@@ -3566,7 +5887,7 @@ class MirCompiler {
3566
5887
  this.fail(`storage metadata for MIR type '${type.kind}' is not supported yet`);
3567
5888
  }
3568
5889
 
3569
- bufferChannelMetadata(channels) {
5890
+ bufferChannelMetadata(channels, element) {
3570
5891
  if (channels === "mono") {
3571
5892
  return { kind: "mono", count: 1 };
3572
5893
  }
@@ -3577,7 +5898,8 @@ class MirCompiler {
3577
5898
  channels &&
3578
5899
  typeof channels === "object" &&
3579
5900
  Number.isInteger(channels.static) &&
3580
- channels.static > 0
5901
+ channels.static > 0 &&
5902
+ channels.static <= Math.floor(0x7fffffff / this.scalarSize(element))
3581
5903
  ) {
3582
5904
  return { kind: "static", count: channels.static };
3583
5905
  }