Bit-packed values
A colour and a generational handle, each living in one integer. This is where
packed structs, enums,
and const
earn their keep together: the layout is stated once, the channel names are real, and the
compiled module contains none of the three.
The module
// One i32 carrying four fields. Bits run least-significant first in declaration order:
// `red` is 0..8, `green` 8..16, `blue` 16..24, and `opaque` is bit 24.
packed struct Rgb : i32 {
red: u8,
green: u8,
blue: u8,
opaque: bool,
}
// An i31-represented value is a real WebAssembly `(ref i31)`, so it fits in an `?any`
// slot with nothing allocated to carry it. That is what giving up the 32nd bit buys.
packed struct Handle : i31 {
generation: u10,
index: u20,
}
enum Channel { Red, Green, Blue }
// A `param` is a knob a build may turn; the `const` derives from it and may not, so the
// two cannot drift apart.
param Gain: i32 = 2;
const Ceiling: i32 = 255 / Gain;
struct Slot { payload: mut ?any }
func Rgb.channel(self: Rgb, which: Channel) -> i32 {
switch (which) {
case Channel.Red: { return self->red; }
case Channel.Green: { return self->green; }
case Channel.Blue: { return self->blue; }
default: { return 0; }
}
}
func check() -> i32 {
var c: Rgb = new Rgb { red: 30, green: 60, blue: 90, opaque: true };
if (c.channel(Channel.Red) != 30) { return 1; }
if (c.channel(Channel.Blue) != 90) { return 2; }
// Writing one field replaces its bits and leaves every other field alone.
c->green = 200;
if (c.channel(Channel.Green) != 200) { return 3; }
if (c.channel(Channel.Red) != 30) { return 4; }
if (c->opaque != 1) { return 5; }
// A field keeps the low N bits of what it is given: 300 is 0b1_0010_1100, so a u8
// field stores 44.
c->blue = 300;
if (c->blue != 44) { return 6; }
if (Ceiling != 127) { return 7; }
// A handle in an `?any` slot, unboxed.
let h: Handle = new Handle { generation: 3, index: 70000 };
let slot: &Slot = new Slot { payload: h };
let stored: ?any = slot->payload;
if (stored is &i31) {
let bits: i32 = stored as i32.u;
let back: Handle = (bits as &i31) as Handle;
if (back->generation != 3) { return 8; }
if (back->index != 70000) { return 9; }
return 0;
}
return 10;
}
export func check as "check";
Why it works that way
Nothing in the emitted module knows about any of the three declarations. There is no
type-section entry for Rgb or Handle -- a packed struct is its representation, so
Rgb is an i32 and Handle is a (ref i31). Channel lowers to a transparent alias
for i32, and Ceiling is substituted as a literal before name resolution runs. What
reaches WebAssembly is the shifts and masks you would otherwise have written by hand.
c->green = 200 is a read-modify-write of c. A packed value is a value, so there
is nothing to mutate in place: the compiler clears the field's bit range, ORs the new
value in, and writes the whole integer back. That is why the binding is a var -- a
let has nowhere to write back to, and the compiler says so rather than computing a new
value and discarding it.
The switch still needs its default. Channel is a C-style enum, so an
enum-typed value is an ordinary integer and channel(42) is representable. There is no
exhaustiveness checking to lean on, by design.
The ?any round trip is explicit in both directions. Handle really is an i31
reference, so it goes into the slot with no conversion at all. Coming back needs the
is &i31 test (the slot could hold anything), then as i32.u for the bits, then
as Handle to reinterpret them. Nothing here is implicit, because two i31-represented
packed structs are indistinguishable at runtime -- a cast between them could not be
checked.
Expected result
check() returns 0. Try turning the knob: --define Gain=5 makes Ceiling 51, and
the Ceiling != 127 check then returns 7.
Related reading: packed structs,
enumerations,
compile-time parameters and const, and
GC structs for the allocating kind.