### ----------------------------------------------------
### -- CBOR::Simple
### -- Licenses: Artistic-2.0
### -- Authors: Geoffrey Broadwell
### -- File: lib/CBOR/Simple.rakumod
### ----------------------------------------------------
### -- Chunk 1 of 3
unit module CBOR::Simple:auth<zef:japhb>:api<0>:ver<0.1.4>;

use nqp;
use TinyFloats;


enum CBORMajorType (
    CBOR_UInt  => 0,
    CBOR_NInt  => 1 +< 5,
    CBOR_BStr  => 2 +< 5,
    CBOR_TStr  => 3 +< 5,
    CBOR_Array => 4 +< 5,
    CBOR_Map   => 5 +< 5,
    CBOR_Tag   => 6 +< 5,
    CBOR_SVal  => 7 +< 5,
);


enum CBORMagicNumber (
    CBOR_MajorType_Mask => 0xE0,
    CBOR_Argument_Mask  => 0x1F,

    CBOR_False => 20,
    CBOR_True  => 21,
    CBOR_Null  => 22,
    CBOR_Undef => 23,

    CBOR_1Byte => 24,
    CBOR_2Byte => 25,
    CBOR_4Byte => 26,
    CBOR_8Byte => 27,

    CBOR_Indefinite_Break => 31,
);


enum CBORMinMax (
    CBOR_Max_UInt_1Byte => 255,
    CBOR_Max_UInt_2Byte => 65535,
    CBOR_Max_UInt_4Byte => 4294967295,
    CBOR_Max_UInt_8Byte => 18446744073709551615,
    CBOR_Max_UInt_63Bit => 9223372036854775807,

    CBOR_Min_NInt_1Byte => -256,
    CBOR_Min_NInt_2Byte => -65536,
    CBOR_Min_NInt_4Byte => -4294967296,
    CBOR_Min_NInt_8Byte => -18446744073709551616,
    CBOR_Min_NInt_63Bit => -9223372036854775808,
);


enum CBORTagNumber (
    CBOR_Tag_DateTime_String   => 0,
    CBOR_Tag_DateTime_Number   => 1,
    CBOR_Tag_Unsigned_BigInt   => 2,
    CBOR_Tag_Negative_BigInt   => 3,
    CBOR_Tag_Decimal_Fraction  => 4,
    CBOR_Tag_Bigfloat          => 5,

    #  6..15 unassigned
    # 16..18 NYI
    # 19..20 unassigned
    # 21..23 NYI

    CBOR_Tag_Encoded_CBOR      => 24,

    # 25..29 NYI

    CBOR_Tag_Rational          => 30,
    CBOR_Tag_Absent            => 31,

    # 32..34 NYI
    # 35     deprecated
    # 36..47 NYI
    # 48..60 unassigned or deprecated
    # 61     NYI
    # 62     unassigned

    CBOR_Tag_Encoded_Sequence  => 63,

    # 64..79 NYI
    # 80..87 Supported as a block
    # 88..95 unassigned
    # 96..98 NYI
    # 99     unassigned

    CBOR_Tag_Date_Integer      => 100,

    CBOR_Tag_Set               => 258,
    CBOR_Tag_Object_Key_Map    => 259,
    CBOR_Tag_Decimal_Extended  => 264,
    CBOR_Tag_Bigfloat_Extended => 265,
    CBOR_Tag_String_Key_Map    => 275,
    CBOR_Tag_Date_String       => 1004,
    CBOR_Tag_Capture           => 25441,  # This is the reference implementation!
    CBOR_Tag_Self_Described    => 55799,
    CBOR_Tag_Self_Sequence     => 55800,

    CBOR_Tag_Invalid_2Byte     => 65535,
    CBOR_Tag_Invalid_4Byte     => 4294967295,
    CBOR_Tag_Invalid_8Byte     => 18446744073709551615,
);


# Break sentinel
my class Break { }


# Introspection of tagged values
class Tagged {
    has UInt:D $.tag-number is required;
    has Mu     $.value      is required;
    has Str:D  $.desc       =  '';
}


# Parsing exceptions
class X::Malformed is X::AdHoc {}

PROCESS::<$CBOR_SIMPLE_FATAL_ERRORS> = False;


# Buffer read/write constants
my int $ne8  = nqp::bitor_i(nqp::const::BINARY_SIZE_8_BIT, NativeEndian);
my int $be16 = nqp::bitor_i(nqp::const::BINARY_SIZE_16_BIT, BigEndian);
my int $be32 = nqp::bitor_i(nqp::const::BINARY_SIZE_32_BIT, BigEndian);
my int $be64 = nqp::bitor_i(nqp::const::BINARY_SIZE_64_BIT, BigEndian);


# Precache a utf8 encoder, since we'll be doing it a LOT
my $utf8-encoder = Encoding::Registry.find("utf8").encoder;


# Don't use the RFC 8949 map key sorting, it's really slow
constant RFC8949_Map_Key_Sort = False;


# Encode an arbitrary value to CBOR, possibly with leading self-describing tag
multi cbor-encode(Mu $value, :$cbor-self-tag, :$cbor-sequence-tag) is export {
    my $pos;
    $cbor-sequence-tag
    ?? cbor-encode($value, $pos = 3, buf8.new(0xd9, 0xd9, 0xf8))
    !! $cbor-self-tag
       ?? cbor-encode($value, $pos = 3, buf8.new(0xd9, 0xd9, 0xf7))
       !! cbor-encode($value, $pos = 0)
}

# Encode an arbitrary value to CBOR, specifying a buffer position to begin writing
multi cbor-encode(Mu $value, Int:D $pos is rw, Buf:D $buf = buf8.new) is export {
    # This gets called a LOT, so go for speed
    my &write-uint = -> int $major-type, int $value {
        nqp::if(
            nqp::islt_i($value, CBOR_1Byte),
            nqp::writeuint($buf, $pos++, nqp::add_i($major-type, $value), $ne8),
            nqp::if(
                nqp::isle_i($value, CBOR_Max_UInt_1Byte),
                nqp::stmts(
                    nqp::writeuint($buf, $pos++, nqp::add_i($major-type, CBOR_1Byte), $ne8),
                    nqp::writeuint($buf, $pos++, $value, $ne8),
                ),
                nqp::if(
                    nqp::isle_i($value, CBOR_Max_UInt_2Byte),
                    nqp::stmts(
                        nqp::writeuint($buf, $pos++, nqp::add_i($major-type, CBOR_2Byte), $ne8),
                        nqp::writeuint($buf, $pos, $value, $be16),
                        ($pos = nqp::add_I(nqp::decont($pos), 2, Int)),
                    ),
                    nqp::if(
                        nqp::isle_i($value, CBOR_Max_UInt_4Byte),
                        nqp::stmts(
                            nqp::writeuint($buf, $pos++, nqp::add_i($major-type, CBOR_4Byte), $ne8),
                            nqp::writeuint($buf, $pos, $value, $be32),
                            ($pos = nqp::add_I(nqp::decont($pos), 4, Int)),
                        ),
                        nqp::stmts(
                            nqp::writeuint($buf, $pos++, nqp::add_i($major-type, CBOR_8Byte), $ne8),
                            nqp::writeuint($buf, $pos, $value, $be64),
                            ($pos = nqp::add_I(nqp::decont($pos), 8, Int)),
                        )
                    )
                )
            )
        )
    }

    my sub write-medium-uint(int $major-type, $value) {
        nqp::writeuint($buf, $pos++, $major-type + CBOR_8Byte, $ne8);
        $buf.write-uint64($pos, $value, BigEndian);
        # $pos += 8;
        $pos = nqp::add_I(nqp::decont($pos), 8, Int);
    }

    my &encode = -> $value {
    # Defined values
        with $value {
            # First classify by general role, then by actual type

            # Check for Numeric before Stringy so allomorphs prefer Numeric
            if nqp::istype($_, Numeric) {
                if nqp::istype($_, Bool) {
                    nqp::writeuint($buf, $pos++, ($_ ?? CBOR_SVal + CBOR_True
                                                     !! CBOR_SVal + CBOR_False), $ne8);
                }
                elsif nqp::istype($_, Int) {
                    # Small int
                    if CBOR_Min_NInt_63Bit <= $_ <= CBOR_Max_UInt_63Bit {
                        $_ >= 0 ?? write-uint(CBOR_UInt,   $_)
                                !! write-uint(CBOR_NInt, +^$_);
                    }
                    # Medium int
                    elsif CBOR_Min_NInt_8Byte <= $_ <= CBOR_Max_UInt_8Byte {
                        $_ >= 0 ?? write-medium-uint(CBOR_UInt,   $_)
                                !! write-medium-uint(CBOR_NInt, +^$_);
                    }
                    # Unsigned BigInt
                    elsif $_ >= 0 {
                        nqp::writeuint($buf, $pos++, CBOR_Tag + CBOR_Tag_Unsigned_BigInt, $ne8);
                        my @bytes = .polymod(256 xx *).reverse;
                        my $bytes = @bytes.elems;
                        write-uint(CBOR_BStr, $bytes);
                        $buf.splice($pos, $bytes, @bytes);
                        $pos += $bytes;
                    }
                    # Negative BigInt
                    else {
                        nqp::writeuint($buf, $pos++, CBOR_Tag + CBOR_Tag_Negative_BigInt, $ne8);
                        my @bytes = (+^$_).polymod(256 xx *).reverse;
                        my $bytes = @bytes.elems;
                        write-uint(CBOR_BStr, $bytes);
                        $buf.splice($pos, $bytes, @bytes);
                        $pos += $bytes;
                    }
                }
                elsif nqp::istype($_, Num) {
                    # Handle NaN and ±Inf separately, so others can be fast-pathed
                    if nqp::isnanorinf($_) {
                        # ±Inf case (NaN is never equal to itself)
                        if nqp::iseq_n($_, $_) {
                            # Two-byte encoding, only sign bit differs
                            nqp::writeuint($buf, $pos++, CBOR_SVal + CBOR_2Byte, $ne8);
                            nqp::writeuint($buf, $pos++, $_ > 0 ?? 0x7C !! 0xFC, $ne8);
                            nqp::writeuint($buf, $pos++, 0, $ne8);
                        }
                        # NaN case, complicated by requirement to retain significand info
                        else {
                            my buf8 $nan .= new;
                            $nan.write-num64(0, $_, BigEndian);

                            if $nan[4] == $nan[5] == $nan[6] == $nan[7] == 0 {
                                # 4-byte NaN required
                                if $nan[3] || $nan[2] +& 3 {
                                    my num32 $nan32 = $_;
                                    nqp::writeuint($buf, $pos++, CBOR_SVal + CBOR_4Byte, $ne8);
                                    nqp::writenum($buf, $pos, $nan32, $be32);

                                    # Canonify NaN sign bit to 0, even on platforms with -NaN
                                    nqp::writeuint($buf, $pos, nqp::readuint($buf, $pos, $ne8) +& 0x7F, $ne8);
                                    # $pos += 4;
                                    $pos = nqp::add_I(nqp::decont($pos), 4, Int);
                                }
                                # 2-byte NaN sufficient
                                else {
                                    nqp::writeuint($buf, $pos++, CBOR_SVal + CBOR_2Byte, $ne8);
                                    nqp::writeuint($buf, $pos, bin16-from-num($_), $be16);
                                    # $pos += 4;
                                    $pos = nqp::add_I(nqp::decont($pos), 4, Int);
                                }
                            }
                            # 8-byte NaN required
                            else {
                                nqp::writeuint($buf, $pos++, CBOR_SVal + CBOR_8Byte, $ne8);
                                nqp::writenum($buf, $pos, $_, $be64);

                                # Canonify NaN sign bit to 0, even on platforms with -NaN
                                nqp::writeuint($buf, $pos, nqp::readuint($buf, $pos, $ne8) +& 0x7F, $ne8);
                                # $pos += 8;
                                $pos = nqp::add_I(nqp::decont($pos), 8, Int);
                            }
                        }
                    }
                    # Can safely shrink to 32 bits with no loss of information
                    elsif nqp::iseq_n($_, (my num32 $num32 = $_)) {
                        # XXXX: 16-bit num support path
                        # my $bin16 = bin16-from-num($_);
                        # my $num16 = num-from-bin16($bin16);
                        # if $num16 == $_ {
                        #     nqp::writeuint($buf, $pos++, CBOR_SVal + CBOR_2Byte, $ne8);
                        #     nqp::writeuint($buf, $pos, $bin16, $be16);
                        #     $pos += 2;
                        # }
                        nqp::writeuint($buf, $pos++, CBOR_SVal + CBOR_4Byte, $ne8);
                        nqp::writenum($buf, $pos, $num32, $be32);
                        # $pos += 4;
                        $pos = nqp::add_I(nqp::decont($pos), 4, Int);
                    }
                    # Needs full 64-bit num for round trip
                    else {
                        nqp::writeuint($buf, $pos++, CBOR_SVal + CBOR_8Byte, $ne8);
                        nqp::writenum($buf, $pos, $_, $be64);
                        # $pos += 8;
                        $pos = nqp::add_I(nqp::decont($pos), 8, Int);
                    }
                }
                elsif nqp::istype($_, Rational) {
                    # write-uint(CBOR_Tag, CBOR_Tag_Rational);
                    nqp::writeuint($buf, $pos++, CBOR_Tag + CBOR_1Byte, $ne8);
                    nqp::writeuint($buf, $pos++, CBOR_Tag_Rational, $ne8);
                    nqp::writeuint($buf, $pos++, CBOR_Array + 2, $ne8);

                    my $nu := .numerator;
                    # Slow path for FatRats and "big" Rats
                    if nqp::istype($_, FatRat)
                    || $nu > CBOR_Max_UInt_63Bit
                    || $nu < CBOR_Min_NInt_63Bit {
                        encode($nu);
                        encode(.denominator);
                    }
                    # Fast path for "small" Rats
                    else {
                        $nu >= 0 ?? write-uint(CBOR_UInt,   $nu)
                                 !! write-uint(CBOR_NInt, +^$nu);
                        write-uint(CBOR_UInt, .denominator);
                    }
                }
                elsif nqp::istype($_, Instant) {
                    my $num = .to-posix[0].Num;
                    my $val = $num.Int == $num ?? $num.Int !! $num;

                    nqp::writeuint($buf, $pos++, CBOR_Tag + CBOR_Tag_DateTime_Number, $ne8);
                    encode($val);
                }
                elsif nqp::istype($_, Real) {
                    # XXXX: Pretend any other Real is a Num
                    encode(.Num);
                }
                else {
                    my $ex = "Don't know how to encode a {$value.^name}";
                    $*CBOR_SIMPLE_FATAL_ERRORS ?? die $ex !! fail $ex;
                }
            }
            elsif nqp::istype($_, Stringy) {
                if nqp::istype($_, Str) {
                    write-uint(CBOR_TStr,
                               my $bytes := nqp::elems(
                                   my $utf8 := $utf8-encoder.encode-chars($_)));

                    nqp::splice($buf, $utf8, $pos, $bytes);
                    $pos = nqp::add_I(nqp::decont($pos), $bytes, Int);
                }
                elsif nqp::istype($_, Blob) {
                    write-uint(CBOR_BStr, my $bytes := .bytes);
                    $buf.splice($pos, $bytes, $_);
                    $pos = nqp::add_I(nqp::decont($pos), $bytes, Int);
                }
                else {
                    my $ex = "Don't know how to encode a {$value.^name}";
                    $*CBOR_SIMPLE_FATAL_ERRORS ?? die $ex !! fail $ex;
                }
            }
            elsif nqp::istype($_, Capture) {
                write-uint(CBOR_Tag, CBOR_Tag_Capture);
                my $list := .list;
                my $hash := .hash;

                # Use preferred serialization, skipping empty children
                encode($list ?? ($hash ?? [$list, $hash] !! [$list,])
                             !! ($hash ?? [$hash,]       !! []      ));
            }
            # XXXX: Seq/Iterator?
            elsif nqp::istype($_, Positional) {
                constant $endian    = Kernel.endian == LittleEndian ?? 4 !! 0;
                constant $nan-fix   = (buf8.new.write-num64(0, NaN, BigEndian)
                                       .read-uint8(0)) +& 0x80;
                constant %type-info =
                    #          TAG         SHIFT  BYTES  CONSTANT
                    byte   => (0x40,           0, 1, nqp::const::BINARY_SIZE_8_BIT),
                    uint8  => (0x40,           0, 1, nqp::const::BINARY_SIZE_8_BIT),
                    uint16 => (0x41 + $endian, 1, 2, nqp::const::BINARY_SIZE_16_BIT),
                    uint32 => (0x42 + $endian, 2, 4, nqp::const::BINARY_SIZE_32_BIT),
                    uint64 => (0x43 + $endian, 3, 8, nqp::const::BINARY_SIZE_64_BIT),
                    uint   => (0x43 + $endian, 3, 8, nqp::const::BINARY_SIZE_64_BIT),

                    int8   => (0x48,           0, 1, nqp::const::BINARY_SIZE_8_BIT),
                    int16  => (0x49 + $endian, 1, 2, nqp::const::BINARY_SIZE_16_BIT),
                    int32  => (0x4A + $endian, 2, 4, nqp::const::BINARY_SIZE_32_BIT),
                    int64  => (0x4B + $endian, 3, 8, nqp::const::BINARY_SIZE_64_BIT),
                    int    => (0x4B + $endian, 3, 8, nqp::const::BINARY_SIZE_64_BIT),

                    num32  => (0x51 + $endian, 2, 4, nqp::const::BINARY_SIZE_32_BIT),
                    num64  => (0x52 + $endian, 3, 8, nqp::const::BINARY_SIZE_64_BIT),
                    num    => (0x52 + $endian, 3, 8, nqp::const::BINARY_SIZE_64_BIT);

                # Try to pack native arrays using RFC 8746 Typed Arrays tag
                if nqp::istype($_, array) {
                    my $array     := $_<>;
                    my $type       = $array.of;
                    my $type-info  = %type-info{$type.^name};
                    my int $elems  = $array.elems;

                    # Packed array types supported in RFC 8746
                    if $type-info {
                        my $tag   = $type-info[0];
                        write-uint(CBOR_Tag, $tag);

                        my $bytes = $elems * $type-info[2];
                        write-uint(CBOR_BStr, $bytes);

                        my int $s = $type-info[1];
                        my int $t = nqp::bitor_i($type-info[3], NativeEndian);
                        my int $p = $pos;
                        my int $i = -1;

                        # num* types
                        if $tag >= 0x50 {
                            # Slow(er) path for systems with signed NaN
                            if $nan-fix {
                                my int $sign-offset = $endian ?? $type-info[2] - 1 !! 0;
                                nqp::while(
                                    nqp::islt_i(($i = nqp::add_i($i, 1)), $elems),
                                    nqp::stmts(
                                        (my int $l = nqp::add_i($p, nqp::bitshiftl_i($i, $s))),
                                        (my num $n = nqp::atpos_n($array, $i)),
                                        (nqp::writenum($buf, $l, $n, $t)),
                                        # Only do sign fix for NaN (!= self)
                                        (nqp::if(nqp::isne_n($n, $n),
                                                 nqp::stmts(
                                                     (my int $sl = nqp::add_i($l, $sign-offset)),
                                                     (nqp::writeuint($buf, $sl,
                                                                     nqp::readuint($buf, $sl, $ne8) +& 0x7F, $ne8)),
                                                 ))
                                        ),
                                    )
                                );
                            }
                            # Fast path for systems that don't need NaN fix
                            else {
                                nqp::while(
                                    nqp::islt_i(($i = nqp::add_i($i, 1)), $elems),
                                    nqp::writenum($buf,
                                                  nqp::add_i($p, nqp::bitshiftl_i($i, $s)),
                                                  nqp::atpos_n($array, $i),
                                                  $t)
                                );
                            }
                        }
                        # (signed) int* types
                        elsif $tag >= 0x48 {
                            if $s {
                                nqp::while(
                                    nqp::islt_i(($i = nqp::add_i($i, 1)), $elems),
                                    nqp::writeint($buf,
                                                  nqp::add_i($p, nqp::bitshiftl_i($i, $s)),
                                                  nqp::atpos_i($array, $i),
                                                  $t)
                                );
                            }
                            else {
                                nqp::while(
                                    nqp::islt_i(($i = nqp::add_i($i, 1)), $elems),
                                    nqp::writeint($buf,
                                                  nqp::add_i($p, $i),
                                                  nqp::atpos_i($array, $i),
                                                  $t)
                                );
                            }
                        }
                        # uint* types
                        else {
                            if $s {
                                nqp::while(
                                    nqp::islt_i(($i = nqp::add_i($i, 1)), $elems),
                                    nqp::writeuint($buf,
                                                   nqp::add_i($p, nqp::bitshiftl_i($i, $s)),
                                                   nqp::atpos_i($array, $i),
                                                   $t)
                                );
                            }
                            else {
                                nqp::while(
                                    nqp::islt_i(($i = nqp::add_i($i, 1)), $elems),
                                    nqp::writeuint($buf,
                                                   nqp::add_i($p, $i),
                                                   nqp::atpos_i($array, $i),
                                                   $t)
                                );
                            }
                        }

                        $pos += $bytes;
                    }
                    else {
                        # Fake other packed array types (e.g. strarray)
                        # by writing them as standard CBOR Arrays instead
                        write-uint(CBOR_Array, $elems);
                        encode($_) for @$_;
                    }
                }
                # Treat all other Positional types as standard Arrays
                else {
                    write-uint(CBOR_Array, .elems);
                    encode($_) for @$_;
                }
            }
            elsif nqp::istype($_, Associative) {
                if nqp::istype($_, Setty) {
                    write-uint(CBOR_Tag, CBOR_Tag_Set);
                    encode(.keys.sort.cache);
                }
                else {
                    write-uint(CBOR_Map, .elems);
                    if RFC8949_Map_Key_Sort {
                        my @pairs = .map: {
                            cbor-encode(.key, my $ = 0) => .value
                        };
                        @pairs.sort(*.key).map: {
                            my $bytes = .key.bytes;
                            $buf.splice($pos, $bytes, .key);
                            $pos += $bytes;
                            encode(.value);
                        }
                    }
                    else {
                        for .sort {
                            encode(.key);
                            encode(.value);
                        }
                    }
                }
            }
            elsif nqp::istype($_, Dateish) {
                if nqp::istype($_, DateTime) {
                    my $num = .Instant.to-posix[0].Num;
                    my $val = $num.Int == $num ?? $num.Int !! $num;

                    nqp::writeuint($buf, $pos++, CBOR_Tag + CBOR_Tag_DateTime_Number, $ne8);
                    encode($val);
                }
                else {
                    nqp::writeuint($buf, $pos++, CBOR_Tag + CBOR_1Byte, $ne8);
                    nqp::writeuint($buf, $pos++, CBOR_Tag_Date_Integer, $ne8);
                    encode(.daycount - 40587);  # Raku MJD -> RFC 8943 days
                }
            }
            elsif nqp::istype($_, Tagged) {
                write-uint(CBOR_Tag, .tag-number);
                encode(.value);
            }
            else {
                my $ex = "Don't know how to encode a {$value.^name}";
                $*CBOR_SIMPLE_FATAL_ERRORS ?? die $ex !! fail $ex;
            }
        }
        # Undefined values
        else {
            if nqp::istype($_, Nil) {
                nqp::writeuint($buf, $pos++, CBOR_Tag  + CBOR_1Byte, $ne8);
                nqp::writeuint($buf, $pos++, CBOR_Tag_Absent,        $ne8);
                nqp::writeuint($buf, $pos++, CBOR_SVal + CBOR_Undef, $ne8);
            }
            else {
                # Any:U is CBOR null, other Mu:U is CBOR undefined
                nqp::writeuint($buf, $pos++, CBOR_SVal + (nqp::istype($value, Any)
                                                          ?? CBOR_Null !! CBOR_Undef), $ne8);
            }
        }
    }

    encode($value);
    $buf
}


# Decode the first value from CBOR-encoded data
multi cbor-decode(Blob:D $cbor) is export {
    my $value := cbor-decode($cbor, my $pos = 0);
    if $pos < $cbor.bytes {
        my $ex = X::Malformed.new(:payload("Extra data after decoded value"));
        $*CBOR_SIMPLE_FATAL_ERRORS ?? die $ex !! fail $ex;
    }
    $value
}

# Decode the next value from CBOR-encoded data, starting at $pos
multi cbor-decode(Blob:D $cbor, Int:D $pos is rw, Bool:D :$breakable = False) is export {
    my &fail-malformed = -> Str:D $reason {
        my $ex = X::Malformed.new(:payload($reason));
        $*CBOR_SIMPLE_FATAL_ERRORS ?? die $ex !! fail $ex;
    }

    CATCH {
        when /^ 'MVMArray: read_buf out of bounds' / {
            fail-malformed "Early end of input";
        }
        default { .rethrow }
    }

    my int $cbor-length = $cbor.bytes;
    my int $argument;

    # This gets called by almost all definite value decoders, so go for speed
    my &read-uint = {
        my int $v;
        nqp::if(
            nqp::islt_i($argument, CBOR_1Byte),
            $argument,
            nqp::if(
                nqp::iseq_i($argument, CBOR_1Byte),
                nqp::readuint($cbor, $pos++, $ne8),
                nqp::if(
                    nqp::iseq_i($argument, CBOR_2Byte),
                    nqp::stmts(
                        ($v = nqp::readuint($cbor, $pos, $be16)),
                        ($pos = nqp::add_I(nqp::decont($pos), 2, Int)),
                        $v
                    ),
                    nqp::if(
                        nqp::iseq_i($argument, CBOR_4Byte),
                        nqp::stmts(
                            ($v = nqp::readuint($cbor, $pos, $be32)),
                            ($pos = nqp::add_I(nqp::decont($pos), 4, Int)),
                            $v
                        ),
                        nqp::if(
                            nqp::iseq_i($argument, CBOR_8Byte),
                            nqp::stmts(
                                (my $v64 = nqp::readuint($cbor, $pos, $be64)),
                                ($pos = nqp::add_I(nqp::decont($pos), 8, Int)),
                                $v64
                            ),
                            fail-malformed("Invalid argument $argument")
                        )
                    )
                )
            )
        )
    }

    my &decode-bstr = {
        # Indefinite length
        if $argument == CBOR_Indefinite_Break && !$breakable {
            my buf8 $joined .= new;
            until (my $chunk := cbor-decode($cbor, $pos, :breakable)) =:= Break {
                fail-malformed "Byte string chunk has wrong type"
                    unless nqp::istype($chunk, Buf);
                $joined.append($chunk);
            }
            $joined
        }
        # Definite length
        elsif (my $bytes = read-uint) {
            fail-malformed "Unreasonably long byte string"
                if $bytes > CBOR_Max_UInt_63Bit;

            fail-malformed "Byte string too short"
                unless $cbor-length >= (my $a = $pos + $bytes);

            my $buf := nqp::slice($cbor, $pos, $a - 1);
            $pos = $a;
            $buf
        }
        else { buf8.new }
    }

    my &decode-tstr = {
        # Indefinite length
        if nqp::iseq_i($argument, CBOR_Indefinite_Break) && !$breakable {
            my @chunks;
            until (my $chunk := cbor-decode($cbor, $pos, :breakable)) =:= Break {
                fail-malformed "Text string chunk has wrong type"
                    unless nqp::istype($chunk, Str);
                @chunks.push($chunk);
            }
            @chunks.join
        }
        # Definite length
        elsif (my $bytes := read-uint) {
            fail-malformed "Unreasonably long text string"
                if $bytes > CBOR_Max_UInt_63Bit;

            fail-malformed "Text string too short"
                unless $cbor-length >= (my $a := $pos + $bytes);

            my $str := nqp::p6box_s(nqp::decode(nqp::slice($cbor, $pos, $a - 1), 'utf8'));
            $pos = $a;
            $str
        }
        else { '' }
    }

    my &decode-array = {
        # Indefinite length
        $argument == CBOR_Indefinite_Break
        ?? do {
            my @array;
            until (my $item := cbor-decode($cbor, $pos, :breakable)) =:= Break {
                @array.push($item);
            }
            @array
        }
        !! my @ = (^read-uint).map(&decode)
    }

    my &decode-map = {
        my %str-map;
        my %mu-map{Mu};

        # Indefinite length
        if nqp::iseq_i($argument, CBOR_Indefinite_Break) {
            until (my $k := cbor-decode($cbor, $pos, :breakable)) =:= Break {
                nqp::istype($k, Str) ?? %str-map.ASSIGN-KEY($k, decode)
                                     !! (%mu-map.AT-KEY($k) = decode);
            }
        }
        # Definite length
        else {
            my int $elems = read-uint;
            my int $i     = 0;

            nqp::while(
                nqp::isle_i($i = nqp::add_i($i, 1), $elems),
                nqp::if(
                    nqp::istype((my $k = decode), Str),
                    %str-map.ASSIGN-KEY($k, decode),
                    (%mu-map.AT-KEY($k) = decode)
                )
            );
        }

        %mu-map.elems
        ?? nqp::stmts((%mu-map{$_} = %str-map{$_} for %str-map.keys), %mu-map)
        !! %str-map
    }

    my &decode-sval = {
        my constant %svals = 20 => False, 21 => True, 22 => Any, 23 => Mu;

        if $argument <= CBOR_Undef {
            $argument < CBOR_False
                ?? fail-malformed("Unassigned simple value $argument")
                !! %svals{$argument}
        }
        elsif $argument == CBOR_8Byte {
            my num64 $v = $cbor.read-num64($pos, BigEndian);
            # $pos += 8;
            $pos = nqp::add_I(nqp::decont($pos), 8, Int);
            $v
        }
        elsif $argument == CBOR_4Byte {
            my num32 $v = $cbor.read-num32($pos, BigEndian);
            # $pos += 4;
            $pos = nqp::add_I(nqp::decont($pos), 8, Int);
            $v
        }
        elsif $argument == CBOR_2Byte {
            my num32 $v = num-from-bin16($cbor.read-uint16($pos, BigEndian));
            # $pos += 2;
            $