← All modules

Distribution · encoding

LEB128

Works

LEB128 variable-length integers, signed and unsigned — the format DWARF, WebAssembly and Protocol Buffers pack integers with — at arbitrary precision.

Version
1.0 zef:jnthn
Depends
none beyond the core
License
Artistic-2.0
Its own test suite
3 files, green
Checked
2026-09-15 against Raku++ 3.28.0 and Rakudo 2026.08
Where it lives
raku.land · source

Install it #

$ rakupp install LEB128

zef install LEB128 writes the same store; either installer leaves the module usable by both engines.

What it is for #

A four-byte integer field wastes three bytes when the value is under 128, and most values in a binary format are small. LEB128 fixes that: seven bits of payload per byte, the top bit meaning "another byte follows". WebAssembly uses it for every index and offset, DWARF for every line-table delta, Protocol Buffers for every varint field.

Reading or writing any of those formats from Raku means having this.

Encoding and decoding #

File
use LEB128;

sub hex(Buf $b) { $b.list.map({ .fmt('%02X') }).join(' ') }

say 'unsigned:';
for 0, 1, 127, 128, 300, 624485, 2**64 -> $v {
    my Buf $e = encode-leb128-unsigned($v);
    say sprintf('  %-22s %2d bytes  %-30s -> %s',
        $v.Str, $e.elems, hex($e), decode-leb128-unsigned($e).Str);
}
say '';
say 'signed:';
for 0, 63, 64, -1, -64, -65, 624485, -624485 -> $v {
    my Buf $e = encode-leb128-signed($v);
    say sprintf('  %-10s %2d bytes  %-12s -> %s',
        $v.Str, $e.elems, hex($e), decode-leb128-signed($e).Str);
}
Output
unsigned:
  0                       1 bytes  00                             -> 0
  1                       1 bytes  01                             -> 1
  127                     1 bytes  7F                             -> 127
  128                     2 bytes  80 01                          -> 128
  300                     2 bytes  AC 02                          -> 300
  624485                  3 bytes  E5 8E 26                       -> 624485
  18446744073709551616   10 bytes  80 80 80 80 80 80 80 80 80 02  -> 18446744073709551616

signed:
  0           1 bytes  00           -> 0
  63          1 bytes  3F           -> 63
  64          2 bytes  C0 00        -> 64
  -1          1 bytes  7F           -> -1
  -64         1 bytes  40           -> -64
  -65         2 bytes  BF 7F        -> -65
  624485      3 bytes  E5 8E 26     -> 624485
  -624485     3 bytes  9B F1 59     -> -624485

624485 encoding to E5 8E 26 is the DWARF specification's own worked example. Note the byte-boundary asymmetry in the signed form: 63 fits in one byte and 64 needs two, while -64 fits and -65 does not — the sign extension comes from bit 6 of the final byte.

Splicing into a buffer #

File
use LEB128;

my Buf $buf .= new;
my int $offset = 0;

for 300, 7, 624485 -> $v {
    my $written = encode-leb128-unsigned($v, $buf, $offset);
    say sprintf('encoded %-8s at offset %-2d -> %d byte(s) written, buffer now %s',
        $v.Str, $offset, $written, $buf.list.map({ .fmt('%02X') }).join(' '));
    $offset += $written;
}
Output
encoded 300      at offset 0  -> 2 byte(s) written, buffer now AC 02
encoded 7        at offset 2  -> 1 byte(s) written, buffer now AC 02 07
encoded 624485   at offset 3  -> 3 byte(s) written, buffer now AC 02 07 E5 8E 26

The three-argument form returns bytes written, not the new offset, so you must accumulate. The natural misreading works for the first two values and silently corrupts the third. decode's $read is rw parameter is likewise a count, not a position.

Malformed input #

File
use LEB128;

for 'empty', Buf.new,
    'a continuation with nothing after it', Buf.new(0x80),
    'all continuations', Buf.new(0xFF, 0xFF, 0xFF) -> $label, $b {
    my $r = try decode-leb128-unsigned($b);
    say sprintf('%-38s -> %s', $label, $! ?? $!.^name !! $r.Str);
}
say '';
say 'trailing bytes after a complete value are IGNORED:';
say '  decode(01 DE AD) = ', decode-leb128-unsigned(Buf.new(0x01, 0xDE, 0xAD));
say 'and a non-canonical padded zero is accepted:';
say '  decode(80 80 80 00) = ', decode-leb128-unsigned(Buf.new(0x80, 0x80, 0x80, 0x00));
Output
empty                                  -> X::LEB128::Incomplete
a continuation with nothing after it   -> X::LEB128::Incomplete
all continuations                      -> X::LEB128::Incomplete

trailing bytes after a complete value are IGNORED:
  decode(01 DE AD) = 1
and a non-canonical padded zero is accepted:
  decode(80 80 80 00) = 0

Truncation raises a typed X::LEB128::Incomplete rather than returning garbage, which is the right behaviour. Trailing data is not reported, so if you need "this buffer held exactly one value", check the byte count yourself.

The one thing to know #

encode-leb128-unsigned on a negative number never returns. Not an exception you can catch — the process hangs.

The parameter is typed Int, not UInt, so a negative sails past the type check; the encoder then shifts right forever, because -1 +> 7 is -1 at arbitrary precision, appending bytes until memory runs out.

The signed encoder handles every negative correctly, so the trap is specifically routing a possibly-negative value into the unsigned sub — which is exactly what happens when a delta, an offset difference, or a subtraction result reaches it. Guard with $v >= 0 at your own call site, or use the signed form.

Where the two engines differ #

On the native int in the splicing form's signature. Raku++ coerces a boxed Int; Rakudo refuses to dispatch at all:

File
use LEB128;

my Buf $buf .= new;
my int $native = 0;
say 'a native int offset works on both engines : ',
    encode-leb128-unsigned(300, $buf, $native), ' byte(s)';
say 'and the buffer holds : ', $buf.list.map({ .fmt('%02X') }).join(' ');
Output
a native int offset works on both engines : 2 byte(s)
and the buffer holds : AC 02

Declare my int $offset, as above, and the two agree. A plain my Int $off works on Raku++ and dies with X::Multi::NoMatch on Rakudo.

Everything else in this page — the round trips, the DWARF vector, the 64-bit-and-beyond values, the typed truncation error, the tolerance for trailing and padded bytes — was byte-identical on both engines.