@onda-lang/wasm-compiler 0.6.0 → 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.
@@ -64,10 +64,38 @@ const POINTER_GLOBALS = Object.freeze({
64
64
  state: "$onda.state",
65
65
  eventPayload: "$onda.event_payload",
66
66
  buffers: "$onda.buffers",
67
+ bufferWrites: "$onda.buffer_writes",
67
68
  bufferFrames: "$onda.buffer_frames",
68
69
  bufferChannels: "$onda.buffer_channels",
69
70
  bufferSampleRates: "$onda.buffer_sample_rates",
70
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
+ ]);
71
99
 
72
100
  // Compiles MIR emitted by Onda's semantic producer. The producer owns proofs
73
101
  // for operations marked `bounds: "unchecked"` and all other validated MIR
@@ -185,6 +213,10 @@ class MirCompiler {
185
213
  : STATIC_BASE;
186
214
  this.internalHelpers = new Set();
187
215
  this.functionMayFail = [];
216
+ this.bufferMayWrite = [];
217
+ this.fallbackBufferReadAddress = 0;
218
+ this.fallbackBufferWriteAddress = 0;
219
+ this.scalarParameterByValue = [];
188
220
  this.nextLabel = 0;
189
221
  }
190
222
 
@@ -214,6 +246,21 @@ class MirCompiler {
214
246
  this.options.allowInliningFunctionsWithLoops,
215
247
  );
216
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
+ }
217
264
  } finally {
218
265
  binaryen.setOptimizeLevel(previousOptimizeLevel);
219
266
  binaryen.setShrinkLevel(previousShrinkLevel);
@@ -244,6 +291,509 @@ class MirCompiler {
244
291
  }
245
292
  }
246
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
+
247
797
  validateEnvelope() {
248
798
  const mir = this.mir;
249
799
  if (!mir || typeof mir !== "object" || Array.isArray(mir)) {
@@ -303,7 +853,127 @@ class MirCompiler {
303
853
  this.validateCurrentSchemaEnvelope();
304
854
  this.validateProcessEntrySignature();
305
855
  this.validateAcyclicCallGraph();
856
+ this.analyzeBufferWrites();
306
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;
307
977
  }
308
978
 
309
979
  validateCurrentSchemaEnvelope() {
@@ -451,90 +1121,470 @@ class MirCompiler {
451
1121
  }
452
1122
  }
453
1123
 
454
- analyzeRecoverableFailures() {
455
- const callees = this.mir.functions.map(() => new Set());
456
- const direct = this.mir.functions.map(() => false);
457
- const binaryMayFail = (functionId, value) => {
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 [];
458
1129
  if (
459
- value.kind !== "binary"
460
- || !["divide", "remainder"].includes(value.data?.op)
1130
+ selector?.kind === "constant"
1131
+ && selector.data?.type === "i32"
1132
+ && Number.isInteger(selector.data.value)
461
1133
  ) {
462
- return false;
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];
463
1140
  }
464
- const lhs = value.data.lhs;
465
- const scalar = lhs.kind === "constant"
466
- ? lhs.data.type
467
- : this.requireScalarType(
468
- this.mir.functions[functionId].locals[lhs.data].ty,
469
- `binary operand in '${this.mir.functions[functionId].name}'`,
470
- );
471
- return scalar === "i32" || scalar === "i64";
1141
+ return Array.from({ length: len }, (_, slot) => slot);
472
1142
  };
473
- const checkedBoundsKinds = new Set([
474
- "array_window",
475
- "buffer_load",
476
- "buffer_param_load",
477
- "buffer_param_store",
478
- "buffer_store",
479
- "const_data_load",
480
- "index",
481
- "input_load",
482
- "make_slice",
483
- "output_load",
484
- "output_store",
485
- "control_output_store",
486
- ]);
487
- const dynamicBoundsKinds = new Set([
488
- "slice_element",
489
- "slice_load",
490
- "slice_store",
491
- "slice_window",
492
- ]);
493
- const scan = (functionId, value) => {
494
- if (value === null || value === undefined) return;
495
- if (Array.isArray(value)) {
496
- for (const entry of value) scan(functionId, entry);
497
- return;
1143
+ const bufferIds = (bufferRef) => {
1144
+ if (Number.isInteger(bufferRef)) return [bufferRef];
1145
+ if (bufferRef?.kind === "direct" && Number.isInteger(bufferRef.data)) {
1146
+ return [bufferRef.data];
498
1147
  }
499
- if (typeof value !== "object") return;
500
- if (value.kind === "call" && Number.isInteger(value.data?.function)) {
501
- callees[functionId].add(value.data.function);
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 }];
502
1169
  }
503
- const bounds = value.data?.bounds;
504
1170
  if (
505
- value.kind === "process_frame"
506
- || value.kind === "slice_copy"
507
- || binaryMayFail(functionId, value)
508
- || (checkedBoundsKinds.has(value.kind) && bounds === "checked")
509
- || (dynamicBoundsKinds.has(value.kind) && bounds !== "unchecked")
1171
+ parameterRef?.kind === "direct"
1172
+ && Number.isInteger(parameterRef.data)
510
1173
  ) {
511
- direct[functionId] = true;
1174
+ return [{ parameter: parameterRef.data, slot: 0 }];
512
1175
  }
513
- for (const child of Object.values(value)) scan(functionId, child);
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 [];
514
1190
  };
515
- for (const [functionId, func] of this.mir.functions.entries()) {
516
- scan(functionId, func.body);
517
- }
518
- this.functionMayFail = [...direct];
519
- let changed = true;
520
- while (changed) {
521
- changed = false;
522
- for (const [functionId, targets] of callees.entries()) {
523
- if (
524
- !this.functionMayFail[functionId]
525
- && [...targets].some((target) => this.functionMayFail[target])
526
- ) {
527
- this.functionMayFail[functionId] = true;
528
- changed = true;
529
- }
1191
+ const bufferParamOrigins = (parameterRef, func) => new Set(
1192
+ bufferParamSlots(parameterRef, func).map(({ parameter, slot }) =>
1193
+ parameterOrigin(parameter, slot)
1194
+ ),
1195
+ );
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)]);
530
1214
  }
531
- }
532
- }
533
-
534
- buildLayouts() {
535
- this.stateLayout = this.layoutNamedValues(this.mir.state);
536
- this.paramLayout = this.layoutNamedValues(this.mir.interface.params);
537
- this.inputLayout = this.layoutPorts(this.mir.interface.inputs);
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
+ };
1377
+
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);
538
1588
  this.outputLayout = this.layoutPorts(this.mir.interface.outputs);
539
1589
  this.controlOutputLayout = this.layoutControlOutputs();
540
1590
  this.eventLayout = this.mir.interface.events.map((event) =>
@@ -594,6 +1644,18 @@ class MirCompiler {
594
1644
  return { address, scalar: type.data, size };
595
1645
  });
596
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
+ }
597
1659
  this.nextStaticAddress = alignUp(this.nextStaticAddress, 16);
598
1660
  this.requireWasm32Extent(this.nextStaticAddress, "MIR static storage");
599
1661
  this.requireWasm32Extent(
@@ -616,7 +1678,7 @@ class MirCompiler {
616
1678
  const parameter = target?.params[index];
617
1679
  const type = parameter && this.type(parameter.ty);
618
1680
  if (
619
- parameter?.mode !== "value"
1681
+ this.parameterPassingMode(data.function, index) !== "value"
620
1682
  && type?.kind === "scalar"
621
1683
  && argument.kind === "place"
622
1684
  && argument.data.base.kind === "local"
@@ -845,13 +1907,13 @@ class MirCompiler {
845
1907
 
846
1908
  addMirFunction(id, func) {
847
1909
  let nextIndex = 0;
848
- const paramLayouts = func.params.map((param) => {
1910
+ const paramLayouts = func.params.map((param, parameterId) => {
849
1911
  const layout = this.functionValueLayout(
850
1912
  param.ty,
851
1913
  nextIndex,
852
1914
  `parameter '${param.name}'`,
853
1915
  false,
854
- param.mode,
1916
+ this.parameterPassingMode(id, parameterId),
855
1917
  );
856
1918
  nextIndex += layout.components.length;
857
1919
  return layout;
@@ -878,6 +1940,11 @@ class MirCompiler {
878
1940
  const callResultLocals = this.collectCallResultLocals(func);
879
1941
  const sliceScratch = this.collectSliceScratchLocals(func);
880
1942
  const processFrameLocals = this.collectProcessFrameLocals(func);
1943
+ const generatedLocalBase =
1944
+ paramScalars.length +
1945
+ flatLocalScalars.length +
1946
+ callResultLocals.length +
1947
+ sliceScratch.count;
881
1948
  if (
882
1949
  resultScalars.length > 1
883
1950
  || callResultLocals.some((entry) => entry.resultCount > 1)
@@ -918,10 +1985,21 @@ class MirCompiler {
918
1985
  ),
919
1986
  eventId: func.kind?.kind === "event" ? func.kind.data : null,
920
1987
  processFrameLocals,
1988
+ generatedLocalBase,
1989
+ generatedLocals: [],
1990
+ entryInitializers: [],
1991
+ bufferDescriptorCache: new Map(),
1992
+ audioChannelPointerCache: new Map(),
921
1993
  breakLabels: [],
922
1994
  continueLabels: [],
923
1995
  };
924
- 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
+ ]);
925
2003
  const functionRef = this.module.addFunction(
926
2004
  this.functionNames[id],
927
2005
  binaryen.createType(paramScalars.map((type) => this.wasmType(type))),
@@ -930,6 +2008,7 @@ class MirCompiler {
930
2008
  ...flatLocalScalars.map((type) => this.wasmType(type)),
931
2009
  ...callResultLocals.map((entry) => entry.type),
932
2010
  ...Array.from({ length: sliceScratch.count }, () => binaryen.i32),
2011
+ ...context.generatedLocals.map((entry) => this.wasmType(entry.scalar)),
933
2012
  ],
934
2013
  body,
935
2014
  );
@@ -954,6 +2033,9 @@ class MirCompiler {
954
2033
  );
955
2034
  }
956
2035
  }
2036
+ for (const local of context.generatedLocals) {
2037
+ binaryen.Function.setLocalName(functionRef, local.index, local.name);
2038
+ }
957
2039
  }
958
2040
 
959
2041
  functionValueLayout(
@@ -981,15 +2063,28 @@ class MirCompiler {
981
2063
  index,
982
2064
  typeId,
983
2065
  kind: "slice",
984
- components: ["i32", "i32", "i32"],
2066
+ components: ["i32", "i32", "i32", "i32"],
985
2067
  };
986
2068
  }
987
2069
  if (type.kind === "buffer") {
2070
+ this.bufferChannelMetadata(type.data.channels, type.data.element);
988
2071
  return {
989
2072
  index,
990
2073
  typeId,
991
2074
  kind: "buffer",
992
- 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"],
993
2088
  };
994
2089
  }
995
2090
  if (type.kind === "array") {
@@ -1018,8 +2113,10 @@ class MirCompiler {
1018
2113
  }
1019
2114
  if (layout.kind === "array") return;
1020
2115
  const suffixes = layout.kind === "buffer"
1021
- ? ["address", "frames", "channels", "sample_rate"]
1022
- : ["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"];
1023
2120
  for (const [offset, suffix] of suffixes.entries()) {
1024
2121
  binaryen.Function.setLocalName(
1025
2122
  functionRef,
@@ -1039,7 +2136,7 @@ class MirCompiler {
1039
2136
  this.requireFunctionId(data.function, "call target");
1040
2137
  const target = this.mir.functions[data.function];
1041
2138
  const aliasesResult = data.args.some((argument, index) =>
1042
- target.params[index]?.mode !== "value"
2139
+ this.parameterPassingMode(data.function, index) !== "value"
1043
2140
  && argument.kind === "place"
1044
2141
  && argument.data.base.kind === "local"
1045
2142
  && argument.data.projections.length === 0
@@ -1299,6 +2396,10 @@ class MirCompiler {
1299
2396
  POINTER_GLOBALS.buffers,
1300
2397
  this.module.local.get(7, binaryen.i32),
1301
2398
  ),
2399
+ this.module.global.set(
2400
+ POINTER_GLOBALS.bufferWrites,
2401
+ this.module.local.get(7, binaryen.i32),
2402
+ ),
1302
2403
  this.module.global.set(
1303
2404
  POINTER_GLOBALS.bufferFrames,
1304
2405
  this.module.local.get(8, binaryen.i32),
@@ -1357,6 +2458,10 @@ class MirCompiler {
1357
2458
  POINTER_GLOBALS.buffers,
1358
2459
  this.module.local.get(3, binaryen.i32),
1359
2460
  ),
2461
+ this.module.global.set(
2462
+ POINTER_GLOBALS.bufferWrites,
2463
+ this.module.local.get(3, binaryen.i32),
2464
+ ),
1360
2465
  this.module.global.set(
1361
2466
  POINTER_GLOBALS.bufferFrames,
1362
2467
  this.module.local.get(4, binaryen.i32),
@@ -1486,7 +2591,16 @@ class MirCompiler {
1486
2591
  const args = data.args.flatMap((argument, index) => {
1487
2592
  const parameterType = this.type(target.params[index].ty);
1488
2593
  if (parameterType.kind === "scalar") {
1489
- 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
+ }
1490
2604
  if (argument.kind !== "value") {
1491
2605
  this.fail(`scalar call argument ${index} of '${target.name}' is not a value`);
1492
2606
  }
@@ -1505,6 +2619,7 @@ class MirCompiler {
1505
2619
  argument.data.index,
1506
2620
  argument.data.bounds,
1507
2621
  context,
2622
+ target.params[index].mode === "read_write_reference",
1508
2623
  ),
1509
2624
  ];
1510
2625
  }
@@ -1534,22 +2649,32 @@ class MirCompiler {
1534
2649
  ];
1535
2650
  }
1536
2651
  return [
1537
- this.compileSliceWindowAddress(
1538
- argument.data,
1539
- parameterType,
1540
- context,
1541
- ),
2652
+ this.compileSliceWindowAddress(
2653
+ argument.data,
2654
+ parameterType,
2655
+ context,
2656
+ target.params[index].mode === "read_write_reference",
2657
+ ),
1542
2658
  ];
1543
2659
  }
1544
2660
  if (parameterType.kind === "buffer") {
1545
2661
  if (argument.kind === "buffer") {
1546
- 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);
1547
2666
  }
1548
2667
  if (argument.kind === "place") {
1549
2668
  return this.loadBufferPlace(argument.data, context);
1550
2669
  }
1551
2670
  this.fail(`buffer call argument ${index} of '${target.name}' is invalid`);
1552
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
+ }
1553
2678
  this.fail(
1554
2679
  `call argument ${index} of '${target.name}' has unsupported type '${parameterType.kind}'`,
1555
2680
  );
@@ -1565,7 +2690,7 @@ class MirCompiler {
1565
2690
  const localReferenceSync = data.args.flatMap((argument, index) => {
1566
2691
  const parameter = target.params[index];
1567
2692
  if (
1568
- parameter.mode === "value"
2693
+ this.parameterPassingMode(data.function, index) === "value"
1569
2694
  || argument.kind !== "place"
1570
2695
  || argument.data.base.kind !== "local"
1571
2696
  || argument.data.projections.length !== 0
@@ -1673,12 +2798,13 @@ class MirCompiler {
1673
2798
  if (!port) {
1674
2799
  this.fail(`output id ${data.output} is out of range`);
1675
2800
  }
1676
- const channel = this.compilePortChannel(port, data.element, data.bounds, context);
1677
- const tableAddress = this.module.i32.add(
1678
- this.module.global.get(POINTER_GLOBALS.outputs, binaryen.i32),
1679
- 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,
1680
2807
  );
1681
- const channelPointer = this.module.i32.load(0, 4, tableAddress);
1682
2808
  const sampleAddress = this.module.i32.add(
1683
2809
  channelPointer,
1684
2810
  this.module.i32.mul(
@@ -1735,13 +2861,13 @@ class MirCompiler {
1735
2861
  }
1736
2862
 
1737
2863
  compileBufferStore(data, context) {
1738
- const buffer = this.requireBuffer(data.buffer);
2864
+ const buffer = this.requireBufferRef(data.buffer);
1739
2865
  if (buffer.access !== "read_write") {
1740
2866
  this.fail(`buffer '${buffer.name}' is read-only`);
1741
2867
  }
1742
2868
  return this.storeScalar(
1743
2869
  buffer.element,
1744
- this.compileBufferAddress(data, context),
2870
+ this.compileBufferAddress(data, context, true),
1745
2871
  this.compileValue(data.value, context),
1746
2872
  );
1747
2873
  }
@@ -1753,7 +2879,7 @@ class MirCompiler {
1753
2879
  }
1754
2880
  return this.storeScalar(
1755
2881
  type.data.element,
1756
- this.compileBufferParamAddress(data, context),
2882
+ this.compileBufferParamAddress(data, context, true),
1757
2883
  this.compileValue(data.value, context),
1758
2884
  );
1759
2885
  }
@@ -1812,27 +2938,24 @@ class MirCompiler {
1812
2938
  case "buffer_param_load":
1813
2939
  return this.compileBufferParamLoad(data, context);
1814
2940
  case "buffer_len":
1815
- return this.compileBufferLen(data);
2941
+ return this.compileBufferLen(data, context);
1816
2942
  case "buffer_param_len":
1817
2943
  return this.compileBufferParamLen(data, context);
1818
2944
  case "buffer_channels":
1819
- return this.loadBufferTableValue(
1820
- POINTER_GLOBALS.bufferChannels,
1821
- data,
1822
- "i32",
1823
- );
2945
+ return this.compileBufferChannels(data, context);
1824
2946
  case "buffer_param_channels":
1825
- return this.loadBufferParamComponent(data, 2, "i32", context);
2947
+ return this.compileBufferParamChannels(data, context);
1826
2948
  case "buffer_sample_rate":
1827
2949
  return this.loadBufferTableValue(
1828
2950
  POINTER_GLOBALS.bufferSampleRates,
1829
2951
  data,
1830
2952
  "f32",
2953
+ context,
1831
2954
  );
1832
2955
  case "buffer_param_sample_rate":
1833
- return this.loadBufferParamComponent(data, 3, "f32", context);
2956
+ return this.loadBufferParamComponent(data, 4, "f32", context);
1834
2957
  case "slice_len":
1835
- return this.compileSliceValue(data, context)[1];
2958
+ return this.compileSliceValue(data, context)[2];
1836
2959
  case "slice_load":
1837
2960
  return this.compileSliceLoad(data, context);
1838
2961
  case "make_slice":
@@ -1862,12 +2985,13 @@ class MirCompiler {
1862
2985
  if (!port) {
1863
2986
  this.fail(`input id ${data.input} is out of range`);
1864
2987
  }
1865
- const channel = this.compilePortChannel(port, data.element, data.bounds, context);
1866
- const tableAddress = this.module.i32.add(
1867
- this.module.global.get(POINTER_GLOBALS.inputs, binaryen.i32),
1868
- 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,
1869
2994
  );
1870
- const channelPointer = this.module.i32.load(0, 4, tableAddress);
1871
2995
  const sampleAddress = this.module.i32.add(
1872
2996
  channelPointer,
1873
2997
  this.module.i32.mul(
@@ -1911,12 +3035,13 @@ class MirCompiler {
1911
3035
  if (!port) {
1912
3036
  this.fail(`output id ${data.output} is out of range`);
1913
3037
  }
1914
- const channel = this.compilePortChannel(port, data.element, data.bounds, context);
1915
- const tableAddress = this.module.i32.add(
1916
- this.module.global.get(POINTER_GLOBALS.outputs, binaryen.i32),
1917
- 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,
1918
3044
  );
1919
- const channelPointer = this.module.i32.load(0, 4, tableAddress);
1920
3045
  const sampleAddress = this.module.i32.add(
1921
3046
  channelPointer,
1922
3047
  this.module.i32.mul(
@@ -1941,6 +3066,62 @@ class MirCompiler {
1941
3066
  return this.module.i32.add(this.module.i32.const(port.channel), index);
1942
3067
  }
1943
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
+
1944
3125
  compileConstDataLoad(data, context) {
1945
3126
  const item = this.constLayout[data.data];
1946
3127
  if (!item) {
@@ -1955,10 +3136,10 @@ class MirCompiler {
1955
3136
  }
1956
3137
 
1957
3138
  compileBufferLoad(data, context) {
1958
- const buffer = this.requireBuffer(data.buffer);
3139
+ const buffer = this.requireBufferRef(data.buffer);
1959
3140
  return this.loadScalar(
1960
3141
  buffer.element,
1961
- this.compileBufferAddress(data, context),
3142
+ this.compileBufferAddress(data, context, false),
1962
3143
  );
1963
3144
  }
1964
3145
 
@@ -1966,163 +3147,798 @@ class MirCompiler {
1966
3147
  const type = this.bufferParamType(data.parameter, context);
1967
3148
  return this.loadScalar(
1968
3149
  type.data.element,
1969
- this.compileBufferParamAddress(data, context),
3150
+ this.compileBufferParamAddress(data, context, false),
1970
3151
  );
1971
3152
  }
1972
3153
 
1973
- compileBufferParamAddress(data, context) {
3154
+ compileBufferParamAddress(data, context, write) {
1974
3155
  const type = this.bufferParamType(data.parameter, context);
1975
- const component = (offset, scalar) => () =>
1976
- this.loadBufferParamComponent(data.parameter, offset, scalar, context);
1977
- const channels = component(2, "i32");
1978
- const rawIndex = () => {
1979
- if (data.channel === null) {
1980
- return this.compileValue(data.index, context);
1981
- }
1982
- return this.module.i32.add(
1983
- this.module.i32.mul(this.compileValue(data.index, context), channels()),
1984
- this.compileValue(data.channel, context),
1985
- );
1986
- };
1987
- const index = this.compileDynamicBoundedIndex(
1988
- rawIndex,
1989
- () => this.compileBufferParamTotalScalarLen(data.parameter, context),
1990
- data.bounds,
3156
+ return this.withBufferParamSelector(
3157
+ data.parameter,
1991
3158
  context,
1992
- true,
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
+ },
1993
3233
  );
1994
- return this.module.i32.add(
1995
- this.loadBufferParamComponent(data.parameter, 0, "i32", context),
1996
- this.module.i32.mul(
3234
+ }
3235
+
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,
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,
3336
+ ),
3337
+ ),
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,
1997
3351
  index,
1998
- this.module.i32.const(this.scalarSize(type.data.element)),
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,
3485
+ true,
3486
+ );
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,
3503
+ "i32",
3504
+ "buffer_param.selector",
3505
+ );
3506
+ const prelude = [
3507
+ this.module.local.set(
3508
+ selectorLocal,
3509
+ this.compileBufferParamSelector(parameterRef, context),
1999
3510
  ),
3511
+ ];
3512
+ const result = build(
3513
+ () => this.module.local.get(selectorLocal, binaryen.i32),
3514
+ null,
3515
+ prelude,
2000
3516
  );
3517
+ return this.module.block(null, [...prelude, result], resultType);
2001
3518
  }
2002
3519
 
2003
- compileBufferAddress(data, context) {
2004
- const buffer = this.requireBuffer(data.buffer);
2005
- const rawIndex = () => {
2006
- if (data.channel === null) {
2007
- return this.compileValue(data.index, context);
2008
- }
2009
- return this.module.i32.add(
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,
2010
3538
  this.module.i32.mul(
2011
- this.compileValue(data.index, context),
2012
- this.loadBufferTableValue(
2013
- POINTER_GLOBALS.bufferChannels,
2014
- data.buffer,
2015
- "i32",
2016
- ),
3539
+ index,
3540
+ this.module.i32.const(this.scalarSize(scalar)),
2017
3541
  ),
2018
- this.compileValue(data.channel, context),
2019
3542
  );
3543
+ return this.loadScalar(scalar, address);
2020
3544
  };
2021
- const index = this.compileDynamicBoundedIndex(
2022
- rawIndex,
2023
- () => this.compileBufferTotalScalarLen(data.buffer),
2024
- data.bounds,
2025
- context,
2026
- true,
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,
2027
3586
  );
2028
- const pointer = this.loadBufferTableValue(
2029
- POINTER_GLOBALS.buffers,
2030
- data.buffer,
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,
2031
3594
  "i32",
3595
+ selector,
3596
+ staticSelector,
3597
+ context,
2032
3598
  );
2033
- return this.module.i32.add(
2034
- pointer,
2035
- this.module.i32.mul(
3599
+ }
3600
+
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,
3630
+ "i32",
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)),
3668
+ );
3669
+ }
3670
+
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
+ ));
3770
+ }
3771
+
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,
2036
3780
  index,
2037
- this.module.i32.const(this.scalarSize(buffer.element)),
2038
- ),
3781
+ staticIndex,
3782
+ scalar,
3783
+ context,
3784
+ )(),
2039
3785
  );
2040
3786
  }
2041
3787
 
2042
- compileBufferLen(bufferId) {
2043
- this.requireBuffer(bufferId);
2044
- return this.loadBufferTableValue(
2045
- POINTER_GLOBALS.bufferFrames,
2046
- bufferId,
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;
3816
+ }
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));
3834
+ }
3835
+
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);
3845
+ }
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,
2047
3853
  "i32",
3854
+ context,
2048
3855
  );
2049
3856
  }
2050
3857
 
2051
- compileBufferTotalScalarLen(bufferId) {
2052
- this.requireBuffer(bufferId);
2053
- return this.module.i32.mul(
2054
- this.compileBufferLen(bufferId),
2055
- this.loadBufferTableValue(
2056
- POINTER_GLOBALS.bufferChannels,
2057
- bufferId,
2058
- "i32",
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
+ ),
2059
3868
  ),
2060
3869
  );
3870
+ return load();
2061
3871
  }
2062
3872
 
2063
- compileBufferParamLen(parameterId, context) {
2064
- this.bufferParamType(parameterId, context);
2065
- return this.loadBufferParamComponent(parameterId, 1, "i32", context);
2066
- }
2067
-
2068
- compileBufferParamTotalScalarLen(parameterId, context) {
2069
- this.bufferParamType(parameterId, context);
2070
- return this.module.i32.mul(
2071
- this.compileBufferParamLen(parameterId, context),
2072
- this.loadBufferParamComponent(parameterId, 2, "i32", context),
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,
2073
3895
  );
2074
3896
  }
2075
3897
 
2076
- bufferParamType(parameterId, context) {
2077
- const parameter = context.function.params[parameterId];
2078
- const type = parameter && this.type(parameter.ty);
2079
- if (!type || type.kind !== "buffer") {
2080
- this.fail(`parameter id ${parameterId} is not a buffer`);
2081
- }
2082
- return type;
2083
- }
2084
-
2085
- bufferParamLayout(parameterId, context) {
2086
- const layout = context.paramLayouts[parameterId];
2087
- if (!layout || layout.kind !== "buffer") {
2088
- this.fail(`parameter id ${parameterId} has no buffer descriptor`);
2089
- }
2090
- return layout;
2091
- }
2092
-
2093
- loadBufferParamComponent(parameterId, offset, scalar, context) {
2094
- const layout = this.bufferParamLayout(parameterId, context);
2095
- return this.module.local.get(layout.index + offset, this.wasmType(scalar));
2096
- }
2097
-
2098
- loadBufferPlace(place, context) {
2099
- if (place.base.kind !== "parameter" || place.projections.length !== 0) {
2100
- this.fail("buffer call arguments must be unprojected buffer parameters");
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
+ );
2101
3906
  }
2102
- const layout = this.bufferParamLayout(place.base.data, context);
2103
- return layout.components.map((scalar, offset) =>
2104
- this.module.local.get(layout.index + offset, this.wasmType(scalar)),
3907
+ const indexLocal = this.allocateGeneratedLocal(
3908
+ context,
3909
+ "i32",
3910
+ "buffer.descriptor_index",
2105
3911
  );
2106
- }
2107
-
2108
- compileInterfaceBufferValue(bufferId) {
2109
- this.requireBuffer(bufferId);
2110
- return [
2111
- this.loadBufferTableValue(POINTER_GLOBALS.buffers, bufferId, "i32"),
2112
- this.loadBufferTableValue(POINTER_GLOBALS.bufferFrames, bufferId, "i32"),
2113
- this.loadBufferTableValue(POINTER_GLOBALS.bufferChannels, bufferId, "i32"),
2114
- this.loadBufferTableValue(POINTER_GLOBALS.bufferSampleRates, bufferId, "f32"),
3912
+ const prelude = [
3913
+ this.module.local.set(
3914
+ indexLocal,
3915
+ this.compileBufferRefIndex(bufferRef, context),
3916
+ ),
2115
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);
2116
3924
  }
2117
3925
 
2118
- loadBufferTableValue(globalName, bufferId, scalar) {
2119
- this.requireBuffer(bufferId);
2120
- const size = this.scalarSize(scalar);
2121
- const address = this.module.i32.add(
2122
- this.module.global.get(globalName, binaryen.i32),
2123
- 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()),
2124
3930
  );
2125
- 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;
2126
3942
  }
2127
3943
 
2128
3944
  compileSliceValue(value, context) {
@@ -2156,8 +3972,11 @@ class MirCompiler {
2156
3972
  }
2157
3973
  const header = () =>
2158
3974
  this.compileEventParamAddress(context.eventId, place.base.data);
3975
+ const address = () =>
3976
+ this.module.i32.add(header(), this.module.i32.const(4));
2159
3977
  return [
2160
- this.module.i32.add(header(), this.module.i32.const(4)),
3978
+ address(),
3979
+ address(),
2161
3980
  this.module.i32.load(0, 4, header()),
2162
3981
  this.module.i32.const(this.scalarSize(type.data.element)),
2163
3982
  ];
@@ -2217,7 +4036,7 @@ class MirCompiler {
2217
4036
  this.fail("slice assignment destination must be an unprojected local");
2218
4037
  }
2219
4038
  const layout = context.localLayouts[place.base.data];
2220
- if (!layout || layout.kind !== "slice" || components.length !== 3) {
4039
+ if (!layout || layout.kind !== "slice" || components.length !== 4) {
2221
4040
  this.fail(`local id ${place.base.data} is not a valid slice destination`);
2222
4041
  }
2223
4042
  return this.module.block(
@@ -2229,25 +4048,50 @@ class MirCompiler {
2229
4048
  }
2230
4049
 
2231
4050
  compileMakeSlice(data, context) {
2232
- 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);
2233
4064
  const range = this.compileSliceRange(
2234
4065
  () => this.compileValue(data.start, context),
2235
4066
  () => this.compileValue(data.len, context),
2236
- () => source()[1],
4067
+ source(2),
2237
4068
  data.bounds,
2238
4069
  context,
2239
4070
  );
2240
- return [
4071
+ const result = [
2241
4072
  this.module.i32.add(
2242
- source()[0],
4073
+ source(0)(),
2243
4074
  this.module.i32.mul(
2244
4075
  range.start(),
2245
- 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)(),
2246
4084
  ),
2247
4085
  ),
2248
4086
  range.len(),
2249
- source()[2],
4087
+ source(3)(),
2250
4088
  ];
4089
+ result[0] = this.module.block(
4090
+ null,
4091
+ [...initializeSource, result[0]],
4092
+ binaryen.i32,
4093
+ );
4094
+ return result;
2251
4095
  }
2252
4096
 
2253
4097
  compileSliceRange(start, len, sourceLen, bounds, context) {
@@ -2324,6 +4168,7 @@ class MirCompiler {
2324
4168
  this.fail("slice array source must have primitive elements");
2325
4169
  }
2326
4170
  return [
4171
+ this.placeAddress(source.data, context),
2327
4172
  this.placeAddress(source.data, context),
2328
4173
  this.module.i32.const(type.data.len),
2329
4174
  this.module.i32.const(this.scalarSize(element.data)),
@@ -2333,81 +4178,229 @@ class MirCompiler {
2333
4178
  const item = this.constLayout[source.data];
2334
4179
  if (!item) this.fail(`const data id ${source.data} is out of range`);
2335
4180
  return [
4181
+ this.module.i32.const(item.address),
2336
4182
  this.module.i32.const(item.address),
2337
4183
  this.module.i32.const(item.len),
2338
4184
  this.module.i32.const(this.scalarSize(item.scalar)),
2339
4185
  ];
2340
4186
  }
2341
4187
  if (source.kind === "buffer") {
2342
- const buffer = this.requireBuffer(source.data.buffer);
4188
+ const buffer = this.requireBufferRef(source.data.buffer);
2343
4189
  const elementSize = this.scalarSize(buffer.element);
2344
- const address = this.loadBufferTableValue(
2345
- POINTER_GLOBALS.buffers,
2346
- source.data.buffer,
2347
- "i32",
2348
- );
2349
- if (source.data.channel === null) {
2350
- return [
2351
- address,
2352
- this.compileBufferLen(source.data.buffer),
2353
- this.module.i32.const(elementSize),
2354
- ];
2355
- }
2356
- const channels = () =>
2357
- this.loadBufferTableValue(
2358
- POINTER_GLOBALS.bufferChannels,
2359
- source.data.buffer,
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,
2360
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
+ ),
2361
4204
  );
2362
- const channel = this.compileDynamicBoundedIndex(
2363
- () => this.compileValue(source.data.channel, context),
2364
- channels,
2365
- "clamp",
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,
2366
4215
  context,
2367
- true,
2368
4216
  );
2369
- return [
2370
- this.module.i32.add(
2371
- address,
2372
- this.module.i32.mul(channel, this.module.i32.const(elementSize)),
2373
- ),
2374
- this.loadBufferTableValue(
2375
- POINTER_GLOBALS.bufferFrames,
2376
- source.data.buffer,
4217
+ let channels = this.bufferChannelsFactory(
4218
+ source.data.buffer,
4219
+ descriptorIndex,
4220
+ staticIndex,
4221
+ context,
4222
+ );
4223
+ if (
4224
+ staticIndex === null
4225
+ && this.bufferRefChannelMetadata(source.data.buffer).kind === "dynamic"
4226
+ ) {
4227
+ channels = this.snapshotOperationValue(
4228
+ channels,
2377
4229
  "i32",
2378
- ),
2379
- this.module.i32.mul(channels(), this.module.i32.const(elementSize)),
4230
+ "buffer.channels",
4231
+ prelude,
4232
+ context,
4233
+ );
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,
4242
+ "i32",
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)),
2380
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;
2381
4290
  }
2382
4291
  if (source.kind === "buffer_param") {
2383
4292
  const type = this.bufferParamType(source.data.parameter, context);
2384
4293
  const elementSize = this.scalarSize(type.data.element);
2385
- const address = () =>
2386
- this.loadBufferParamComponent(source.data.parameter, 0, "i32", context);
2387
- if (source.data.channel === null) {
2388
- return [
2389
- address(),
2390
- this.compileBufferParamLen(source.data.parameter, context),
2391
- this.module.i32.const(elementSize),
2392
- ];
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);
2393
4318
  }
2394
- const channels = () =>
2395
- this.loadBufferParamComponent(source.data.parameter, 2, "i32", context);
2396
- const channel = this.compileDynamicBoundedIndex(
2397
- () => this.compileValue(source.data.channel, context),
2398
- channels,
2399
- "clamp",
4319
+ const component = (offset, scalar) => this.bufferParamComponentFactory(
4320
+ source.data.parameter,
4321
+ offset,
4322
+ scalar,
4323
+ selector,
4324
+ staticSelector,
2400
4325
  context,
2401
- true,
2402
4326
  );
2403
- return [
2404
- this.module.i32.add(
2405
- address(),
2406
- this.module.i32.mul(channel, this.module.i32.const(elementSize)),
2407
- ),
2408
- this.loadBufferParamComponent(source.data.parameter, 1, "i32", context),
2409
- 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)),
2410
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;
2411
4404
  }
2412
4405
  this.fail(`unsupported slice source '${String(source.kind)}'`);
2413
4406
  }
@@ -2432,25 +4425,26 @@ class MirCompiler {
2432
4425
  return type.data.access;
2433
4426
  }
2434
4427
 
2435
- compileSliceAddress(slice, index, bounds, context) {
4428
+ compileSliceAddress(slice, index, bounds, context, write) {
2436
4429
  return this.compileSliceAddressWithFactories(
2437
4430
  () => this.compileSliceValue(slice, context),
2438
4431
  () => this.compileValue(index, context),
2439
4432
  bounds,
2440
4433
  context,
4434
+ write,
2441
4435
  );
2442
4436
  }
2443
4437
 
2444
- compileSliceAddressWithFactories(slice, index, bounds, context) {
4438
+ compileSliceAddressWithFactories(slice, index, bounds, context, write) {
2445
4439
  const bounded = this.compileDynamicBoundedIndex(
2446
4440
  index,
2447
- () => slice()[1],
4441
+ () => slice()[2],
2448
4442
  bounds,
2449
4443
  context,
2450
4444
  );
2451
4445
  return this.module.i32.add(
2452
- slice()[0],
2453
- this.module.i32.mul(bounded, slice()[2]),
4446
+ slice()[write ? 1 : 0],
4447
+ this.module.i32.mul(bounded, slice()[3]),
2454
4448
  );
2455
4449
  }
2456
4450
 
@@ -2482,7 +4476,7 @@ class MirCompiler {
2482
4476
  );
2483
4477
  }
2484
4478
 
2485
- compileSliceWindowAddress(data, parameterType, context) {
4479
+ compileSliceWindowAddress(data, parameterType, context, write) {
2486
4480
  const elementType = this.type(parameterType.data.element);
2487
4481
  if (elementType.kind !== "scalar") {
2488
4482
  this.fail("slice-window fixed-array parameter element is not scalar");
@@ -2494,7 +4488,7 @@ class MirCompiler {
2494
4488
  () => this.compileValue(data.start, context),
2495
4489
  () =>
2496
4490
  this.module.i32.sub(
2497
- slice()[1],
4491
+ slice()[2],
2498
4492
  this.module.i32.const(requiredLen),
2499
4493
  ),
2500
4494
  data.bounds,
@@ -2502,8 +4496,8 @@ class MirCompiler {
2502
4496
  );
2503
4497
  const address = () =>
2504
4498
  this.module.i32.add(
2505
- slice()[0],
2506
- this.module.i32.mul(start(), slice()[2]),
4499
+ slice()[write ? 1 : 0],
4500
+ this.module.i32.mul(start(), slice()[3]),
2507
4501
  );
2508
4502
  if (data.bounds === "unchecked") {
2509
4503
  return address();
@@ -2511,11 +4505,11 @@ class MirCompiler {
2511
4505
  const invalidShape = () =>
2512
4506
  this.module.i32.or(
2513
4507
  this.module.i32.ne(
2514
- slice()[2],
4508
+ slice()[3],
2515
4509
  this.module.i32.const(elementSize),
2516
4510
  ),
2517
4511
  this.module.i32.lt_s(
2518
- slice()[1],
4512
+ slice()[2],
2519
4513
  this.module.i32.const(requiredLen),
2520
4514
  ),
2521
4515
  );
@@ -2564,7 +4558,7 @@ class MirCompiler {
2564
4558
  const scalar = this.sliceElementScalar(data.slice, context);
2565
4559
  return this.loadScalar(
2566
4560
  scalar,
2567
- this.compileSliceAddress(data.slice, data.index, data.bounds, context),
4561
+ this.compileSliceAddress(data.slice, data.index, data.bounds, context, false),
2568
4562
  );
2569
4563
  }
2570
4564
 
@@ -2575,7 +4569,7 @@ class MirCompiler {
2575
4569
  const scalar = this.sliceElementScalar(data.slice, context);
2576
4570
  return this.storeScalar(
2577
4571
  scalar,
2578
- this.compileSliceAddress(data.slice, data.index, data.bounds, context),
4572
+ this.compileSliceAddress(data.slice, data.index, data.bounds, context, true),
2579
4573
  this.compileValue(data.value, context),
2580
4574
  );
2581
4575
  }
@@ -2598,14 +4592,14 @@ class MirCompiler {
2598
4592
  const scalarSize = this.scalarSize(scalar);
2599
4593
  const address = () =>
2600
4594
  this.module.i32.add(
2601
- destination()[0],
2602
- this.module.i32.mul(counterValue(), destination()[2]),
4595
+ destination()[1],
4596
+ this.module.i32.mul(counterValue(), destination()[3]),
2603
4597
  );
2604
4598
  const scalarLoop = () =>
2605
4599
  this.module.loop(
2606
4600
  scalarLoopLabel,
2607
4601
  this.module.if(
2608
- this.module.i32.lt_s(counterValue(), destination()[1]),
4602
+ this.module.i32.lt_s(counterValue(), destination()[2]),
2609
4603
  this.module.block(null, [
2610
4604
  this.storeScalar(
2611
4605
  scalar,
@@ -2625,18 +4619,18 @@ class MirCompiler {
2625
4619
  const lanes = 16 / scalarSize;
2626
4620
  const vectorCondition = this.module.i32.and(
2627
4621
  this.module.i32.eq(
2628
- destination()[2],
4622
+ destination()[3],
2629
4623
  this.module.i32.const(scalarSize),
2630
4624
  ),
2631
4625
  this.module.i32.and(
2632
4626
  this.module.i32.ge_u(
2633
- destination()[1],
4627
+ destination()[2],
2634
4628
  this.module.i32.const(lanes),
2635
4629
  ),
2636
4630
  this.module.i32.le_u(
2637
4631
  counterValue(),
2638
4632
  this.module.i32.sub(
2639
- destination()[1],
4633
+ destination()[2],
2640
4634
  this.module.i32.const(lanes),
2641
4635
  ),
2642
4636
  ),
@@ -2696,8 +4690,8 @@ class MirCompiler {
2696
4690
  const copyIndex = () =>
2697
4691
  this.module.select(
2698
4692
  this.module.i32.and(
2699
- this.module.i32.eq(destination()[2], source()[2]),
2700
- 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]),
2701
4695
  ),
2702
4696
  this.module.i32.sub(
2703
4697
  this.module.i32.sub(countValue(), this.module.i32.const(1)),
@@ -2708,12 +4702,12 @@ class MirCompiler {
2708
4702
  const sourceAddress = () =>
2709
4703
  this.module.i32.add(
2710
4704
  source()[0],
2711
- this.module.i32.mul(copyIndex(), source()[2]),
4705
+ this.module.i32.mul(copyIndex(), source()[3]),
2712
4706
  );
2713
4707
  const destinationAddress = () =>
2714
4708
  this.module.i32.add(
2715
- destination()[0],
2716
- this.module.i32.mul(copyIndex(), destination()[2]),
4709
+ destination()[1],
4710
+ this.module.i32.mul(copyIndex(), destination()[3]),
2717
4711
  );
2718
4712
  const sourceScalar = this.sliceElementScalar(data.source, context);
2719
4713
  const destinationScalar = this.sliceElementScalar(data.destination, context);
@@ -2731,7 +4725,7 @@ class MirCompiler {
2731
4725
  source()[0],
2732
4726
  this.module.i32.mul(
2733
4727
  this.module.i32.sub(countValue(), this.module.i32.const(1)),
2734
- source()[2],
4728
+ source()[3],
2735
4729
  ),
2736
4730
  ),
2737
4731
  this.module.i32.const(this.scalarSize(sourceScalar)),
@@ -2739,10 +4733,10 @@ class MirCompiler {
2739
4733
  const destinationEnd = () =>
2740
4734
  this.module.i32.add(
2741
4735
  this.module.i32.add(
2742
- destination()[0],
4736
+ destination()[1],
2743
4737
  this.module.i32.mul(
2744
4738
  this.module.i32.sub(countValue(), this.module.i32.const(1)),
2745
- destination()[2],
4739
+ destination()[3],
2746
4740
  ),
2747
4741
  ),
2748
4742
  this.module.i32.const(this.scalarSize(destinationScalar)),
@@ -2751,13 +4745,13 @@ class MirCompiler {
2751
4745
  this.module.i32.and(
2752
4746
  nonEmpty(),
2753
4747
  this.module.i32.and(
2754
- this.module.i32.lt_u(destination()[0], sourceEnd()),
4748
+ this.module.i32.lt_u(destination()[1], sourceEnd()),
2755
4749
  this.module.i32.lt_u(source()[0], destinationEnd()),
2756
4750
  ),
2757
4751
  );
2758
4752
  const invalidOverlap = () =>
2759
4753
  this.module.i32.and(
2760
- this.module.i32.ne(destination()[2], source()[2]),
4754
+ this.module.i32.ne(destination()[3], source()[3]),
2761
4755
  overlaps(),
2762
4756
  );
2763
4757
  const scalarCopy = () =>
@@ -2780,18 +4774,18 @@ class MirCompiler {
2780
4774
  ? this.module.if(
2781
4775
  this.module.i32.and(
2782
4776
  this.module.i32.eq(
2783
- destination()[2],
4777
+ destination()[3],
2784
4778
  this.module.i32.const(this.scalarSize(destinationScalar)),
2785
4779
  ),
2786
4780
  this.module.i32.eq(
2787
- source()[2],
4781
+ source()[3],
2788
4782
  this.module.i32.const(this.scalarSize(sourceScalar)),
2789
4783
  ),
2790
4784
  ),
2791
4785
  // memory.copy has memmove overlap semantics and lets engines use
2792
4786
  // their tuned bulk-memory implementation for contiguous slices.
2793
4787
  this.module.memory.copy(
2794
- destination()[0],
4788
+ destination()[1],
2795
4789
  source()[0],
2796
4790
  this.module.i32.mul(
2797
4791
  countValue(),
@@ -2805,9 +4799,9 @@ class MirCompiler {
2805
4799
  this.module.local.set(
2806
4800
  count,
2807
4801
  this.module.select(
2808
- this.module.i32.lt_s(destination()[1], source()[1]),
2809
- destination()[1],
2810
- source()[1],
4802
+ this.module.i32.lt_s(destination()[2], source()[2]),
4803
+ destination()[2],
4804
+ source()[2],
2811
4805
  ),
2812
4806
  ),
2813
4807
  this.module.if(invalidOverlap(), this.raiseRuntimeFailure(context)),
@@ -3431,6 +5425,12 @@ class MirCompiler {
3431
5425
  lhs.data.element === rhs.data.element &&
3432
5426
  JSON.stringify(lhs.data.channels) === JSON.stringify(rhs.data.channels) &&
3433
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;
3434
5434
  } else if (lhs.kind === "tuple") {
3435
5435
  equivalent =
3436
5436
  lhs.data.length === rhs.data.length &&
@@ -3537,6 +5537,85 @@ class MirCompiler {
3537
5537
  return this.mir.interface.buffers[id];
3538
5538
  }
3539
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
+
3540
5619
  currentLabel(labels, statement) {
3541
5620
  const label = labels.at(-1);
3542
5621
  if (!label) this.fail(`'${statement}' appears outside a MIR loop`);
@@ -3669,8 +5748,8 @@ class MirCompiler {
3669
5748
  }
3670
5749
  : null,
3671
5750
  })),
3672
- buffers: this.mir.interface.buffers.map((buffer) => {
3673
- const channels = this.bufferChannelMetadata(buffer.channels);
5751
+ buffers: this.mir.interface.buffers.map((buffer, bufferId) => {
5752
+ const channels = this.bufferChannelMetadata(buffer.channels, buffer.element);
3674
5753
  return {
3675
5754
  name: buffer.name,
3676
5755
  type_repr: this.bufferTypeRepr(buffer, channels),
@@ -3679,9 +5758,14 @@ class MirCompiler {
3679
5758
  channels: channels.kind,
3680
5759
  static_channels: channels.count,
3681
5760
  access: buffer.access,
3682
- may_write: buffer.access === "read_write",
5761
+ may_write: this.bufferMayWrite[bufferId],
3683
5762
  };
3684
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
+ })),
3685
5769
  events: this.mir.interface.events.map((event, eventId) => ({
3686
5770
  name: event.name,
3687
5771
  export: `onda_event_${eventId}`,
@@ -3773,11 +5857,11 @@ class MirCompiler {
3773
5857
  }
3774
5858
 
3775
5859
  bufferTypeRepr(buffer, channels) {
3776
- if (channels.kind === "mono") return `buffer[${buffer.element}]`;
5860
+ if (channels.kind === "mono") return `buffer<${buffer.element}>`;
3777
5861
  if (channels.kind === "static") {
3778
- return `buffer[${buffer.element}[${channels.count}]]`;
5862
+ return `buffer<${buffer.element}[${channels.count}]>`;
3779
5863
  }
3780
- return `buffer[${buffer.element}[]]`;
5864
+ return `buffer<${buffer.element}[]>`;
3781
5865
  }
3782
5866
 
3783
5867
  storageShape(typeId) {
@@ -3803,7 +5887,7 @@ class MirCompiler {
3803
5887
  this.fail(`storage metadata for MIR type '${type.kind}' is not supported yet`);
3804
5888
  }
3805
5889
 
3806
- bufferChannelMetadata(channels) {
5890
+ bufferChannelMetadata(channels, element) {
3807
5891
  if (channels === "mono") {
3808
5892
  return { kind: "mono", count: 1 };
3809
5893
  }
@@ -3814,7 +5898,8 @@ class MirCompiler {
3814
5898
  channels &&
3815
5899
  typeof channels === "object" &&
3816
5900
  Number.isInteger(channels.static) &&
3817
- channels.static > 0
5901
+ channels.static > 0 &&
5902
+ channels.static <= Math.floor(0x7fffffff / this.scalarSize(element))
3818
5903
  ) {
3819
5904
  return { kind: "static", count: channels.static };
3820
5905
  }