### ----------------------------------------------------
### -- CBOR::Simple
### -- Licenses: Artistic-2.0
### -- Authors: Geoffrey Broadwell
### -- File: lib/CBOR/Simple.rakumod
### ----------------------------------------------------
### -- Chunk 2 of 3
pos = nqp::add_I(nqp::decont($pos), 8, Int);
            $v
        }
        elsif $argument == CBOR_1Byte {
            my $val  = nqp::readuint($cbor, $pos++, $ne8);
            my $fail = $val < 24 ?? "Badly formed" !!
                       $val < 32 ?? "Reserved"     !!
                                    "Unassigned"   ;
            fail-malformed "$fail simple value $val";
        }
        elsif $argument == CBOR_Indefinite_Break {
            $breakable ?? Break
                       !! fail-malformed "Unexpected break signal";
        }
        else {
            fail-malformed "Badly formed simple value $argument";
        }
    }

    my &decode-tag = {
        my $tag-number = read-uint;
        if 64 <= $tag-number <= 87 {  # RFC 8746 Typed Arrays; details bit-coded in tag
            # Decode tag
            my $is-float         = $tag-number +& 24 == 16;
            my $is-signed        = $tag-number +& 8;
            my $is-little-endian = $tag-number +& 4;
            my $size             = 1 +< ($tag-number +& 3 + $is-float);

            # Determine Endian type to read with
            my $on-little-endian = Kernel.endian == LittleEndian;
            my $is-same-endian   = !($is-little-endian ?^ $on-little-endian);
            my $endian           = $is-same-endian   ?? NativeEndian !!
                                   $on-little-endian ?? BigEndian    !!
                                                        LittleEndian;

            # Look at tagged content and check that it is a byte string
            $argument = nqp::bitand_i(
                my int $initial-byte = nqp::readuint($cbor, $pos++, $ne8),
                CBOR_Argument_Mask);
            fail-malformed "Typed Array tag ($tag-number) does not contain a byte string"
                unless nqp::bitand_i($initial-byte, CBOR_MajorType_Mask) == CBOR_BStr;

            # Check that the byte string has an even number of elements
            my $bytes = read-uint;
            fail-malformed "Typed Array with element size $size does not evenly divide byte length $bytes"
                if $bytes % $size;

            # Determine actual element count
            my int $elems = $bytes div $size;

            # Parse out the actual array
            if $is-float {
                if $size == 2 {
                    my $array := array[num32].new;

                    # Presize array to reduce copying
                    nqp::setelems($array, $elems);

                    # We can't just memcopy, so apply NQP afterburners instead
                    my int $p = $pos;
                    my int $t = nqp::bitor_i(nqp::const::BINARY_SIZE_16_BIT, $endian);
                    my int $i = -1;
                    nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_n($array, $i,
                                       num-from-bin16(
                                           nqp::readuint($cbor,
                                                         nqp::add_i($p, nqp::bitshiftl_i($i,1)),
                                                         $t)))
                    );

                    $pos += $bytes;
                    $array
                }
                elsif $size == 4 {
                    my $array := array[num32].new;

                    # Presize array to reduce copying
                    nqp::setelems($array, $elems);

                    # We can't just memcopy, so apply NQP afterburners instead
                    my int $p = $pos;
                    my int $t = nqp::bitor_i(nqp::const::BINARY_SIZE_32_BIT, $endian);
                    my int $i = -1;
                    nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_n($array, $i,
                                       nqp::readnum($cbor,
                                                    nqp::add_i($p,
                                                               nqp::bitshiftl_i($i, 2)),
                                                    $t))
                    );

                    $pos += $bytes;
                    $array
                }
                elsif $size == 8 {
                    my $array := array[num64].new;

                    # Presize array to reduce copying
                    nqp::setelems($array, $elems);

                    # We can't just memcopy, so apply NQP afterburners instead
                    my int $p = $pos;
                    my int $t = nqp::bitor_i(nqp::const::BINARY_SIZE_64_BIT, $endian);
                    my int $i = -1;
                    nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_n($array, $i,
                                       nqp::readnum($cbor,
                                                    nqp::add_i($p,
                                                               nqp::bitshiftl_i($i, 3)),
                                                    $t))
                    );

                    $pos += $bytes;
                    $array
                }
                else {
                    die "Unable to parse native float array with element size $size";
                }
            }
            else {
                if $size == 1 {
                    fail-malformed "CBOR extension tag 76 indicating signed 8-bit little-endian packed array is reserved by RFC 8746" if $tag-number == 76;

                    my $array := array[$is-signed ?? int8 !! uint8].new;

                    # Presize array to reduce copying
                    nqp::setelems($array, $elems);

                    # We can't just memcopy, so apply NQP afterburners instead
                    my int $p = $pos;
                    my int $i = -1;
                    $is-signed
                    ?? nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_i($array, $i,
                                       nqp::readint($cbor,
                                                    nqp::add_i($p, $i),
                                                    $ne8))
                       )
                    !! nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_i($array, $i,
                                       nqp::readuint($cbor,
                                                     nqp::add_i($p, $i),
                                                     $ne8))
                       );

                    $pos += $bytes;
                    $array
                }
                elsif $size == 2 {
                    my $array := array[$is-signed ?? int16 !! uint16].new;

                    # Presize array to reduce copying
                    nqp::setelems($array, $elems);

                    # We can't just memcopy, so apply NQP afterburners instead
                    my int $p = $pos;
                    my int $t = nqp::bitor_i(nqp::const::BINARY_SIZE_16_BIT, $endian);
                    my int $i = -1;
                    $is-signed
                    ?? nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_i($array, $i,
                                       nqp::readint($cbor,
                                                    nqp::add_i($p,
                                                               nqp::bitshiftl_i($i, 1)),
                                                    $t))
                       )
                    !! nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_i($array, $i,
                                       nqp::readuint($cbor,
                                                     nqp::add_i($p,
                                                                nqp::bitshiftl_i($i, 1)),
                                                     $t))
                       );

                    $pos += $bytes;
                    $array
                }
                elsif $size == 4 {
                    my $array := array[$is-signed ?? int32 !! uint32].new;

                    # Presize array to reduce copying
                    nqp::setelems($array, $elems);

                    # We can't just memcopy, so apply NQP afterburners instead
                    my int $p = $pos;
                    my int $t = nqp::bitor_i(nqp::const::BINARY_SIZE_32_BIT, $endian);
                    my int $i = -1;
                    $is-signed
                    ?? nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_i($array, $i,
                                       nqp::readint($cbor,
                                                    nqp::add_i($p,
                                                               nqp::bitshiftl_i($i, 2)),
                                                    $t))
                       )
                    !! nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_i($array, $i,
                                       nqp::readuint($cbor,
                                                     nqp::add_i($p,
                                                                nqp::bitshiftl_i($i, 2)),
                                                     $t))
                       );

                    $pos += $bytes;
                    $array
                }
                elsif $size == 8 {
                    my $array := array[$is-signed ?? int64 !! uint64].new;

                    # Presize array to reduce copying
                    nqp::setelems($array, $elems);

                    # We can't just memcopy, so apply NQP afterburners instead
                    my int $p = $pos;
                    my int $t = nqp::bitor_i(nqp::const::BINARY_SIZE_64_BIT, $endian);
                    my int $i = -1;
                    $is-signed
                    ?? nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_i($array, $i,
                                       nqp::readint($cbor,
                                                    nqp::add_i($p,
                                                               nqp::bitshiftl_i($i, 3)),
                                                    $t))
                       )
                    !! nqp::while(
                        nqp::islt_i(($i = nqp::add_i($i,1)),$elems),
                        nqp::bindpos_i($array, $i,
                                       nqp::readuint($cbor,
                                                     nqp::add_i($p,
                                                                nqp::bitshiftl_i($i, 3)),
                                                     $t))
                       );

                    $pos += $bytes;
                    $array
                }
            }
        }
        elsif $tag-number == CBOR_Tag_Rational {
            fail-malformed "Rational tag (30) does not contain an array with exactly two elements"
                unless nqp::readuint($cbor, $pos++, $ne8) == CBOR_Array + 2;

            my $nu = decode;
            my $de = decode;
            fail-malformed "Rational tag (30) numerator is not an integer"
                unless nqp::istype($nu, Int);
            fail-malformed "Rational tag (30) denominator is not a positive integer"
                unless nqp::istype($de, Int) && $de > 0;

            $de <= CBOR_Max_UInt_8Byte ?? Rat.new(   $nu, $de)
                                       !! FatRat.new($nu, $de)
        }
        elsif $tag-number == CBOR_Tag_DateTime_Number {
            my $seconds := decode;
            fail-malformed "Epoch DateTime tag(1) does not contain a real number"
                unless nqp::istype($seconds, Real);
            Instant.from-posix($seconds) // fail-malformed "Epoch DateTime could not be decoded"
        }
        elsif $tag-number == CBOR_Tag_DateTime_String {
            my $dt := decode;
            fail-malformed "DateTime tag (0) does not contain a string"
                unless nqp::istype($dt, Str);
            DateTime.new($dt) // fail-malformed "DateTime string could not be parsed"
        }
        elsif $tag-number == CBOR_Tag_Date_Integer {
            my $days := decode;
            fail-malformed "Gregorian days tag(100) does not contain an integer"
                unless nqp::istype($days, Int);
            Date.new-from-daycount($days + 40587) // fail-malformed "Gregorian days could not be decoded"
        }
        elsif $tag-number == CBOR_Tag_Date_String {
            my $date := decode;
            fail-malformed "Date string tag (1004) does not contain a string"
                unless nqp::istype($date, Str);
            Date.new($date) // fail-malformed "Date string could not be parsed"
        }
        elsif $tag-number == CBOR_Tag_Unsigned_BigInt {
            my $bytes := decode;
            fail-malformed "Unsigned BigInt does not contain a byte string"
                unless nqp::istype($bytes, Buf);
            my $value = 0;
            $value = $value * 256 + $_ for @$bytes;
            $value
        }
        elsif $tag-number == CBOR_Tag_Negative_BigInt {
            my $bytes := decode;
            fail-malformed "Negative BigInt does not contain a byte string"
                unless nqp::istype($bytes, Buf);
            my $value = 0;
            $value = $value * 256 + $_ for @$bytes;
            +^$value
        }
        elsif $tag-number == CBOR_Tag_Capture {
            my $capture_top = nqp::readuint($cbor, $pos++, $ne8);

            if    $capture_top == CBOR_Array {
                Capture.new()
            }
            elsif $capture_top == CBOR_Array + 1 {
                my $child := decode;
                $child := $child.value
                       if nqp::istype($child, Tagged)
                       && (   $child.tag-number == CBOR_Tag_Object_Key_Map
                           || $child.tag-number == CBOR_Tag_String_Key_Map);
                if nqp::istype($child, Array) {
                    Capture.new(:list($child))
                }
                elsif nqp::istype($child, Map) {
                    Capture.new(:hash($child))
                }
                else {
                    fail-malformed "Capture tag ($tag-number) array's child is neither an array nor a map";
                }
            }
            elsif $capture_top == CBOR_Array + 2 {
                my $list := decode;
                my $hash := decode;
                   $hash := $hash.value
                         if nqp::istype($hash, Tagged)
                         && (   $hash.tag-number == CBOR_Tag_Object_Key_Map
                             || $hash.tag-number == CBOR_Tag_String_Key_Map);

                fail-malformed "Capture tag ($tag-number) array's first element is not an array"
                    unless nqp::istype($list, Array);
                fail-malformed "Capture tag ($tag-number) array's second element is not a map"
                    unless nqp::istype($hash, Map);

                Capture.new(:$list, :$hash)
            }
            else {
                fail-malformed "Capture tag ($tag-number) does not contain an array with at most two elements";
            }
        }
        elsif $tag-number == CBOR_Tag_Set {
            fail-malformed "Set tag (258) does not contain an array"
                unless nqp::readuint($cbor, $pos, $ne8) +& CBOR_MajorType_Mask == CBOR_Array;
            (decode).Set
        }
        elsif $tag-number == CBOR_Tag_Object_Key_Map {
            fail-malformed "Map with object keys tag (259) does not contain a map"
                unless nqp::readuint($cbor, $pos, $ne8) +& CBOR_MajorType_Mask == CBOR_Map;
            my %map := decode;
            if nqp::istype(%map.keyof, Str) {
                my %mu-map{Mu};
                %mu-map{$_} = %map{$_} for %map.keys;
                %mu-map
            }
            else {
                %map
            }
        }
        elsif $tag-number == CBOR_Tag_String_Key_Map {
            fail-malformed "Map with string keys tag (275) does not contain a map"
                unless nqp::readuint($cbor, $pos, $ne8) +& CBOR_MajorType_Mask == CBOR_Map;
            my %map := decode;
            fail-malformed "Map with string keys tag (275) contains non-string keys"
                unless nqp::istype(%map.keyof, Str);
            %map
        }
        elsif $tag-number == CBOR_Tag_Decimal_Fraction
           || $tag-number == CBOR_Tag_Decimal_Extended {
            fail-malformed "Decimal Fraction tag ($tag-number) does not contain an array with exactly two elements"
                unless nqp::readuint($cbor, $pos++, $ne8) == CBOR_Array + 2;

            my $exp = decode;
            my $man = decode;
            fail-malformed "Decimal Fraction tag (4) exponent is not an integer"
                unless nqp::istype($exp, Int);
            fail-malformed "Decimal Fraction tag (4) mantissa is not an integer"
                unless nqp::istype($man, Int);

            $exp >= 0 ?? $man * 10 ** $exp !! do {
                my $de = 10 ** -$exp;
                $de <= CBOR_Max_UInt_8Byte ?? Rat.new(   $man, $de)
                                           !! FatRat.new($man, $de)
            }
        }
        elsif $tag-number == CBOR_Tag_Bigfloat
           || $tag-number == CBOR_Tag_Bigfloat_Extended {
            fail-malformed "Bigfloat tag ($tag-number) does not contain an array with exactly two elements"
                unless nqp::readuint($cbor, $pos++, $ne8) == CBOR_Array + 2;

            my $exp = decode;
            my $man = decode;
            fail-malformed "Bigfloat tag (5) exponent is not an integer"
                unless nqp::istype($exp, Int);
            fail-malformed "Bigfloat tag (5) mantissa is not an integer"
                unless nqp::istype($man, Int);

            $exp >= 0 ?? $man * 2 ** $exp !! do {
                my $de = 2 ** -$exp;
                $de <= CBOR_Max_UInt_8Byte ?? Rat.new(   $man, $de)
                                           !! FatRat.new($man, $de)
            }
        }
        elsif $tag-number == CBOR_Tag_Absent {
            fail-malformed "Absent tag (31) does not contain the undefined value"
                unless nqp::readuint($cbor, $pos++, $ne8) == CBOR_SVal + CBOR_Undef;
            Nil
        }
        # Lazy decoding: byte string containing CBOR-encoded data that is NOT unwrapped
        elsif $tag-number == CBOR_Tag_Encoded_CBOR
           || $tag-number == CBOR_Tag_Encoded_Sequence {
            fail-malformed "Encoded CBOR tag ($tag-number) does not contain a byte string"
                unless nqp::readuint($cbor, $pos, $ne8) +& CBOR_MajorType_Mask == CBOR_BStr;
            Tagged.new(:$tag-number, :value(decode))
        }
        # Self-tagged CBOR item or CBOR Sequence, just unwrap the decoded tag content
        elsif $tag-number == CBOR_Tag_Self_Described
           || $tag-number == CBOR_Tag_Self_Sequence {
            decode
        }
        # Intentionally (as per spec) invalid tag values
        elsif $tag-number == CBOR_Tag_Invalid_2Byte
           || $tag-number == CBOR_Tag_Invalid_4Byte
           || $tag-number == CBOR_Tag_Invalid_8Byte {
            fail-malformed "Multi-byte tag number has all bits on";
        }
        # Final fallback: Just wrapped the value in a CBOR::Simple::Tagged object
        else {
            Tagged.new(:$tag-number, :value(decode))
        }
    }

    my @decoders =
        &read-uint,
        { +^read-uint },
        &decode-bstr,
        &decode-tstr,
        &decode-array,
        &decode-map,
        &decode-tag,
        &decode-sval;

    my &decode = {
        $argument = nqp::bitand_i(
            my int $initial-byte = nqp::readuint($cbor, $pos++, $ne8),
            CBOR_Argument_Mask);

        @decoders.AT-POS(nqp::bitshiftr_i(nqp::bitand_i($initial-byte,
                                                        CBOR_MajorType_Mask), 5)).()
    }

    decode;
}


# Convert a CBOR-encoded value to human diagnostic form
multi cbor-diagnostic(Blob:D $cbor) is export {
    cbor-diagnostic($cbor, my $pos = 0)
}

# Convert a CBOR-encoded value to human diagnostic form, starting at $pos
multi cbor-diagnostic(Blob:D $cbor, Int:D $pos is rw, Bool:D :$breakable = False) is export {
    my $initial-byte = $cbor.read-uint8($pos++);
    my $major-type   = $initial-byte +& CBOR_MajorType_Mask;
    my $argument     = $initial-byte +& CBOR_Argument_Mask;

    my &read-uint = {
        if $argument < CBOR_1Byte {
            $argument
        }
        elsif $argument == CBOR_1Byte {
            $cbor.read-uint8($pos++)
        }
        elsif $argument == CBOR_2Byte {
            my $v = $cbor.read-uint16($pos, BigEndian);
            $pos += 2;
            $v
        }
        elsif $argument == CBOR_4Byte {
            my $v = $cbor.read-uint32($pos, BigEndian);
            $pos += 4;
            $v
        }
        elsif $argument == CBOR_8Byte {
            my $v = $cbor.read-uint64($pos, BigEndian);
            $pos += 8;
            $v
        }
        elsif $argument == CBOR_Indefinite_Break {
            Whatever
        }
        else {
            fail "argument($argument)"
        }
    }

    given $major-type {
        when CBOR_UInt {
            ~read-uint
        }
        when CBOR_NInt {
            ~(+^read-uint)
        }
        when CBOR_BStr {
            my $bytes = read-uint;

            # Indefinite length
            if $bytes === Whatever {
                # Peek and see if there are any chunks
                if $cbor.read-uint8($pos) == CBOR_SVal +| CBOR_Indefinite_Break {
                    "''_"
                }
                else {
                    my @chunks;
                    until (my $chunk := cbor-diagnostic($cbor, $pos, :breakable))
                          =:= Break {
                        @chunks.push($chunk);
                    }
                    '(_ ' ~ @chunks.join(', ') ~ ')';
                }
            }
            # Definite length
            elsif $bytes > CBOR_Max_UInt_63Bit {
                "'Unreasonably long byte string, length $bytes'"
            }
            else {
                my $buf = $cbor.subbuf($pos, $bytes);
                if $buf.bytes == $bytes {
                    $pos += $bytes;
                    "h'" ~ $buf.list.map(*.fmt('%02x')).join ~ "'"
                }
                else {
                    "'Byte string too short, {$buf.bytes} < $bytes bytes'"
                }
            }
        }
        when CBOR_TStr {
            my $bytes = read-uint;

            # Indefinite length
            if $bytes === Whatever {
                # Peek and see if there are any chunks
                if $cbor.read-uint8($pos) == CBOR_SVal +| CBOR_Indefinite_Break {
                    '""_'
                }
                else {
                    my @chunks;
                    until (my $chunk := cbor-diagnostic($cbor, $pos, :breakable))
                          =:= Break {
                        @chunks.push($chunk);
                    }
                    '(_ ' ~ @chunks.join(', ') ~ ')';
                }
            }
            # Definite length
            elsif $bytes > CBOR_Max_UInt_63Bit {
                "\"Unreasonably long text string, length $bytes\""
            }
            else {
                my $utf8 = $cbor.subbuf($pos, $bytes);
                if $utf8.bytes == $bytes {
                    $pos += $bytes;
                    # XXXX: JSON escaping?
                    '"' ~ $utf8.decode ~ '"'
                }
                else {
                    "\"Text string too short, {$utf8.bytes} < $bytes bytes\""
                }
            }
        }
        when CBOR_Array {
            # Indefinite length
            $argument == CBOR_Indefinite_Break
            ?? do {
                my @array;
                until (my $item := cbor-diagnostic($cbor, $pos, :breakable)) =:= Break {
                    @array.push($item);
                }
                '[_ ' ~ @array.join(', ') ~ ']'
            }
            !! '[' ~ (^read-uint).map({ cbor-diagnostic($cbor, $pos) }).join(', ') ~ ']'
        }
        when CBOR_Map {
            my @pairs;
            # Indefinite length
            if $argument == CBOR_Indefinite_Break {
                loop {
                    my $k := cbor-diagnostic($cbor, $pos, :breakable);
                    last if $k =:= Break;
                    @pairs.push($k => cbor-diagnostic($cbor, $pos));
                }
                '{_ ' ~ @pairs.fmt('%s: %s', ', ') ~ '}'
            }
            # Definite length
            else {
                for ^(read-uint) {
                    my $k = cbor-diagnostic($cbor, $pos);
                    my $v = cbor-diagnostic($cbor, $pos);
                    @pairs.push($k => $v);
                }
                '{' ~ @pairs.fmt('%s: %s', ', ') ~ '}'
            }
        }
        when CBOR_Tag {
            read-uint() ~ '(' ~ cbor-diagnostic($cbor, $pos) ~ ')'
        }
        when CBOR_SVal {
            my constant %svals = 20 => 'false', 21 => 'true',
                                 22 => 'null',  23 => 'undefined';
            sub JS-Num($v) {
                $v.isNaN   ??  'NaN'      !!
                $v ==  Inf ??  'Infinity' !!
                $v == -Inf ?? '-Infinity' !! do {
                    my $basic = (~$v).subst(/'e0'$/, '').subst(/'e'('+'|'-')'0'/, {"e$0"});
                    $basic.contains('.') ?? $basic !! $basic.subst(/('e' | $)/, {".0$0"})
                }
            }

            if $argument < CBOR_False {
                "simple($argument)"
            }
            elsif $argument <= CBOR_Undef {
                %svals{$argument}
            }
            elsif $argument == CBOR_1Byte {
                my $sval = $cbor.read-uint8($pos++);
                "simple($sval)"
            }
            elsif $argument == CBOR_2Byte {
                my $v = num-from-bin16($cbor.read-uint16($pos, BigEndian));
                $pos += 2;
                JS-Num($v) ~ '_1'
            }
            elsif $argument == CBOR_4Byte {
                my $v = $cbor.read-num32($pos, BigEndian);
                $pos += 4;
                JS-Num($v) ~ '_2'
            }
            elsif $argument == CBOR_8Byte {
                my $v = $cbor.read-num64($pos, BigEndian);
                $pos += 8;
                JS-Num($v) ~ '_3'
            }
            elsif $breakable && $argument == CBOR_Indefinite_Break {
                Break
            }
            else {
                "simple($argument)"
            }

        }
    }
}


=begin pod

=head1 NAME

CBOR::Simple - Simple codec for the CBOR serialization format


=head1 SYNOPSIS

=begin code :lang<raku>

use CBOR::Simple;

# Encode a Raku value to CBOR, or vice-versa
my $cbor = cbor-encode($value);
my $val1 = cbor-decode($cbor);               # Fails if more data past first decoded value
my $val2 = cbor-decode($cbor, my $pos = 0);  # Updates $pos after decoding first value

# By default, cbor-decode() marks partially corrupt parsed structures with
# Failure nodes at the point of corruption
my $bad  = cbor-decode(buf8.new(0x81 xx 3));  # [[[Failure]]]

# Callers can instead force throwing exceptions on any error
my $*CBOR_SIMPLE_FATAL_ERRORS = True;
my $bad  = cbor-decode(buf8.new(0x81 xx 3));  # BOOM!

# Decode CBOR into diagnostic text, used for checking encodings and complex structures
my $diag = cbor-diagnostic($cbor);

# Force the encoder to tag a value with a particular tag number
my $tagged = CBOR::Simple::Tagged.new(:$tag-number, :$value);
my $cbor   = cbor-encode($tagged);

=end code


=head1 DESCRIPTION

C<CBOR::Simple> is an easy-to-use implementation of the core functionality of
the L<CBOR serialization format|https://cbor.io/>, implementing the standard as
of L<RFC 8949|https://tools.ietf.org/html/rfc8949>, plus a collection of common
tag extensions as described below in
L<TAG IMPLEMENTATION STATUS|#tag-implementation-status>.


=head2 PERFORMANCE

C<CBOR::Simple> is one of the fastest data structure serialization codecs
available for Raku.  It is comparable in round-trip speed to C<JSON::Fast>
for data structures that are the most JSON-friendly.  For all other cases
tested, C<CBOR::Simple> produces smaller, higher fidelity encodings, faster.
For more detail, and comparison with other Raku serialization codecs, see
L<serializer-perf|https://github.com/japhb/serializer-perf>.


=head2 NYI

Currently known NOT to work:

=item Any tag marked '✘' (valid but not yet supported) or 'D' (deprecated spec)
      in the ENCODE or DECODE column of the Tag Status Details table below, or
      any tag not explicitly listed therein, will be treated as an opaque tagged
      value rather than treated as a native type.

=item Packed arrays of 128-bit floats (num128); these are not supported in
      Rakudo yet.

=item Encoding I<finite> 16-bit floats (num16); encoding 16-bit NaN and ±Inf,
      as well as decoding any num16 all work.  This is a performance tradeoff
      rather than a technical limitation; detecting whether a finite num32 can
      be shrunk to 16 bits without losing information is costly and rarely
      results in space savings except in trivial cases (e.g. Nums containing
      only small integers).


=head2 TAG CONTENT STRICTNESS

When encoding, C<CBOR::Simple> makes every attempt to encode tagged content
strictly within the tag standards as written, always producing spec-compliant
encoded values.

When decoding, C<CBOR::Simple> will often slightly relax the allowed content
types in tagged content, especially when later tag proposals made no change
other than to extend the allowed content types and allocate a new tag number
for that.  In the extension case C<CBOR::Simple> is likely to allow both the
old and new tag to accept the same content domain when decoding.

For example, when encoding C<CBOR::Simple> will always encode C<Instant> or
C<DateTime> as a CBOR epoch-based date/time (tag 1), using standard integer
or floating point content data.  But when I<decoding>, C<CBOR::Simple> will
accept any content that decodes properly as a Raku C<Real> value -- and in
particular will handle a CBOR Rational (tag 30) as another valid content type.


=head2 DATE, DATETIME, INSTANT

Raku's builtin time handling is richer than the default CBOR data model (though
certain tag extensions improve this), so the following mappings apply:

=item1 Encoding

=item2 C<Instant> and C<DateTime> are both written as tag 1 (epoch-based
       date/time) with integer (if lossless) or floating point content.

=item2 Other C<Dateish> are written as tag 100 (RFC 8943 days since 1970-01-01).

=item1 Decoding

=item2 Tag 0 (date/time string) is parsed as a C<DateTime>.

=item2 Tag 1 (epoch-based date/time) is parsed via C<Instant.from-posix()>, and
       handles any Real type in the tag content.

=item2 Tag 100 (days since 1970-01-01) is parsed via C<Date.new-from-daycount()>.

=item2 Tag 1004 (date string) is parsed as a C<Date>.


=head2 UNDEFINED VALUES

=item CBOR's C<null> is translated as C<Any> in Raku.

=item CBOR's C<undefined> is translated as C<Mu> in Raku.

=item A real C<Nil> in an array (which must be I<bound>, not assigned) is
      encoded as a CBOR Absent tag (31).  Absent values will be recognized on
      decode as well, but since array contents are I<assigned> into their
      parent array during decoding, a C<Nil> in an array will be translated to
      C<Any> by Raku's array assignment semantics.


=head2 OTHER SPECIAL CASES

=item To mark a substructure for lazy decoding (treating it as an opaque
      C<Blob> until explicitly decoded), use the tagged value idiom in the
      SYNOPSIS with `:tag-number(24)` (encoded CBOR value) or
      `:tag-number(63)` (encoded CBOR Sequence).

=item CBOR strings claiming to be longer than C<2⁶³-1> are treated as malformed.

=item Bigfloats and decimal fractions (tags 4, 5, 264, 265) with very large
      exponents may result in numeric overflow when decoded.

=item Keys for Associative types are sorted using Raku's internal `sort` method
      rather than the RFC 8949 default sort, because the latter is much slower.

=item C<cbor-diagnostic()> always adds encoding indicators for float values.


=head2 TAG IMPLEMENTATION STATUS

Note that unrecognized tags will decode to their contents wrapped with a
C<CBOR::Simple::Tagged> object that records its C<tag-number>; check marks in
the details table indicate conversion to/from an appropriate native Raku type
rather than this default behavior.

=begin table :caption<Tag Status Overview: Native Raku Types>
    GROUP          | SUPPORT | NOTES
    ============== |=========|======
    Core           | Good    | Core RFC 8949 CBOR data model and syntax
    Collections    | Good    | Sets, maps with only object or only string keys
    Graph          | NONE    | Cyclic, indirected, and self-referential structures
    Numbers        | Good    | Rational/BigInt/BigFloat support except non-finite triplets
    Packed Arrays  | Partial | Packed num32/64, [u]int* arrays full; num16 is read-only
    Special Arrays | NONE    | Explicit multi-dim/homogenous arrays
    Tag Fallbacks  | Good    | Round tripping of unknown tagged content
    Date/Time      | Partial | All but tagged time (tags 1001-1003) supported
=end table

=begin table :caption<Tag Status Overview: Specialty Types>
    GROUP          | SUPPORT | NOTES
    ============== |=========|======
    Enc