Skip to main content

A generic ring buffer

A complete program built around one generator: a fixed-capacity ring buffer, instantiated twice for two unrelated element types, and used by code that knows nothing about how it was produced.

A ring buffer is a good demonstration because it is genuinely worth generating. It is about sixty lines of index arithmetic that does not vary with the element type at all, and writing it twice by hand is how the second copy drifts from the first.

// `Slots` is the backing array, `Name` the buffer type, `prefix` the constructor prefix.
// Both name arguments are needed because a type must be PascalCase and a function
// snake_case, so one cannot be derived from the other.
comptime func ring_of(T: type, Name: ident, prefix: ident) {
  array [<$Name Slots>] { mut $T }

  pub struct $Name {
    slots: mut &[<$Name Slots>],
    head: mut i32,
    used: mut i32,
  }

  /// A ring holding at most `capacity` elements. Oldest is dropped when full.
  pub func [<new_ $prefix>](capacity: i32) -> &$Name {
    let n: i32 = capacity < 1 ? 1 : capacity;
    return new $Name { slots: new [<$Name Slots>][n]{}, head: 0, used: 0 };
  }

  pub func $Name.len(self: &$Name) -> i32 { return self->used; }

  pub func $Name.capacity(self: &$Name) -> i32 { return #self->slots; }

  pub func $Name.is_full(self: &$Name) -> i32 {
    return self->used == #self->slots;
  }

  /// Appends `value`. When the ring is full this overwrites the oldest element and
  /// advances `head`, which is the whole point of a ring.
  pub func $Name.push(self: &$Name, value: $T) {
    let at: i32 = (self->head + self->used) % #self->slots;
    self->slots[at] = value;
    if (self.is_full()) {
      self->head = (self->head + 1) % #self->slots;
    } else {
      self->used = self->used + 1;
    }
  }

  /// The element `index` positions after the oldest. Out of range traps, matching the
  /// standard library's convention for a direct index.
  pub func $Name.at(self: &$Name, index: i32) -> $T {
    if (index < 0 || index >= self->used) { unreachable; }
    return self->slots[(self->head + index) % #self->slots];
  }

  /// Removes and returns the oldest element. The second result distinguishes "took a
  /// zero" from "there was nothing to take", which no sentinel can do for a generic `T`.
  pub func $Name.take(self: &$Name) -> ($T, i32) {
    if (self->used == 0) { return (self->slots[0], 0); }
    let value: $T = self->slots[self->head];
    self->head = (self->head + 1) % #self->slots;
    self->used = self->used - 1;
    return (value, 1);
  }
}

// Two element types, two genuinely distinct WebAssembly GC types.
comptime ring_of(i32, Readings, readings);
comptime ring_of(f64, Samples, samples);

// Ordinary code over a generated type. Nothing here can tell it was generated -- which is
// the property that makes the feature worth having.
func window_sum(r: &Readings) -> i32 {
  var total: i32 = 0;
  for i in 0..r.len() { total = total + r.at(i); }
  return total;
}

export func check as "check";
func check() -> i32 {
  // Capacity 4, but eight pushes: the first four are overwritten.
  let r: &Readings = new_readings(4);
  for i in 0..8 { r.push(i * 10); }

  if (r.len() != 4) { return 1; }
  if (r.capacity() != 4) { return 2; }
  // The four survivors are 40, 50, 60, 70.
  if (r.at(0) != 40) { return 3; }
  if (r.at(3) != 70) { return 4; }
  if (window_sum(r) != 220) { return 5; }

  let (oldest, ok): (i32, i32) = r.take();
  if (ok != 1 || oldest != 40) { return 6; }
  if (r.len() != 3) { return 7; }

  // The same generator, a different element type, no extra code.
  let s: &Samples = new_samples(2);
  s.push(1.5);
  s.push(2.5);
  s.push(4.0);
  if (s.len() != 2) { return 8; }
  if (s.at(0) != 2.5) { return 9; }

  return 0;
}

check() returns 0.

What the compiled module contains

Everything above the instantiations is a template and appears nowhere in the output. What the module holds is what you would have written by hand: two array types, two struct types, and one copy of each method per element type, with unboxed fields and direct calls.

Methods the program never reaches are not emitted at all. is_full survives because push calls it; if nothing had, it would be absent — and it would still not be reported as unused, since it is not code the caller wrote.

The one thing to notice about take

take returns a pair rather than a sentinel, and that is forced rather than fastidious. A monomorphic IntRing could return -1 for "empty", but a generator does not know what T is, so no value it could return is guaranteed not to be a real element. The ok flag is the only honest answer, and the standard library's own containers use the same shape for the same reason.

Where this can't go

The element type has to be defaultable, because the backing array is created default-initialized. Numeric types are, and so is any nullable ?Foo; a non-null &Foo is not. A ring of structs is therefore comptime ring_of(?Point, PointRing, point_ring);, and at hands back a ?Point you narrow with is. That is WebAssembly's rule rather than the generator's, and it is reported at the instantiation.

See Generics for the full feature, and std.vec for a growable container built the same way.