my class JSONException is Exception {
    has $.text;

    method message {
        'Invalid JSON: ' ~ $!text
    }
}

# A slightly modified version of https://github.com/timo/json_fast/blob/5ce76c039dc143fa9a068f1dfa47b42e58046821/lib/JSON/Fast.pm6
# Key differences:
#  - to-json stringifies Version objects
#  - Removes $*JSON_NAN_INF_SUPPORT and the Falsey code path(s) that use it
#  - Custom code for stringifying some exception related things
my class Rakudo::Internals::JSON {

    my multi sub to-surrogate-pair(Int $ord) {
        my int $base   = $ord - 0x10000;
        my int $top    = $base +& 0b1_1111_1111_1100_0000_0000 +> 10;
        my int $bottom = $base +&               0b11_1111_1111;
        Q/\u/ ~ (0xD800 + $top).base(16) ~ Q/\u/ ~ (0xDC00 + $bottom).base(16);
    }

    my multi sub to-surrogate-pair(Str $input) {
        to-surrogate-pair(nqp::ordat($input, 0));
    }

#?if jvm
    # Older version of str-escape, since the JVM backend does not have .NFD yet
    my sub str-escape(str $text is copy) {
        return $text unless $text ~~ /:m <[\x[5C] \x[22] \x[00]..\x[1F] \x[10000]..\x[10FFFF]]>/;

        $text .= subst(/ :m <[\\ "]> /,
            -> $/ {
                my str $str = $/.Str;
                if $str eq "\\" {
                    "\\\\"
                } elsif nqp::ordat($str, 0) == 92 {
                    "\\\\" ~ tear-off-combiners($str, 0)
                } elsif $str eq "\"" {
                    "\\\""
                } else {
                    "\\\"" ~ tear-off-combiners($str, 0)
                }
            }, :g);
        $text .= subst(/ <[\x[10000]..\x[10FFFF]]> /,
            -> $/ {
                to-surrogate-pair($/.Str);
            }, :g);
        $text .= subst(/ :m <[\x[10000]..\x[10FFFF]]> /,
            -> $/ {
                to-surrogate-pair($/.Str) ~ tear-off-combiners($/.Str, 0);
            }, :g);
        for flat 0..8, 11, 12, 14..0x1f -> $ord {
            my str $chr = chr($ord);
            if $text.contains($chr) {
                $text .= subst($chr, '\\u' ~ $ord.fmt("%04x"), :g);
            }
        }
        $text = $text.subst("\r\n", '\\r\\n',:g)\
                    .subst("\n", '\\n',     :g)\
                    .subst("\r", '\\r',     :g)\
                    .subst("\t", '\\t',     :g);
        $text;
    }
#?endif

#?if !jvm
    my $tab := nqp::list_i(92,116); # \t
    my $lf  := nqp::list_i(92,110); # \n
    my $cr  := nqp::list_i(92,114); # \r
    my $qq  := nqp::list_i(92, 34); # \"
    my $bs  := nqp::list_i(92, 92); # \\

    # Convert string to decomposed codepoints.  Run over that integer array
    # and inject whatever is necessary, don't do anything if simple ascii.
    # Then convert back to string and return that.
    sub str-escape(\text) {
        my $codes := text.NFD;
        my int $i = -1;

        nqp::while(
          nqp::islt_i(++$i,nqp::elems($codes)),
          nqp::if(
            nqp::isle_i((my int $code = nqp::atpos_u($codes,$i)),92)
              || nqp::isge_i($code,128),
            nqp::if(                                       # not ascii
              nqp::isle_i($code,31),
              nqp::if(                                      # control
                nqp::iseq_i($code,10),
                nqp::splice($codes,$lf,$i++,1),              # \n
                nqp::if(
                  nqp::iseq_i($code,13),
                  nqp::splice($codes,$cr,$i++,1),             # \r
                  nqp::if(
                    nqp::iseq_i($code,9),
                    nqp::splice($codes,$tab,$i++,1),           # \t
                    nqp::stmts(                                # other control
                      nqp::splice($codes,$code.fmt(Q/\u%04x/).NFD,$i,1),
                      ($i = nqp::add_i($i,5))
                    )
                  )
                )
              ),
              nqp::if(                                      # not control
                nqp::iseq_i($code,34),
                nqp::splice($codes,$qq,$i++,1),              # "
                nqp::if(
                  nqp::iseq_i($code,92),
                  nqp::splice($codes,$bs,$i++,1),             # \
                  nqp::if(
                    nqp::isge_i($code,0x10000),
                    nqp::stmts(                                # surrogates
                      nqp::splice(
                        $codes,
                        (my $surrogate := to-surrogate-pair($code.chr).NFD),
                        $i,
                        1
                      ),
                      ($i = nqp::sub_i(nqp::add_i($i,nqp::elems($surrogate)),1))
                    )
                  )
                )
              )
            )
          )
        );

        nqp::strfromcodes($codes)
    }
#?endif

    method to-json(
      \obj,
      Bool :$pretty        = False,
      Int  :$level         = 0,
      int  :$spacing       = 2,
      Bool :$sorted-keys   = False,
    ) {

        my $out := nqp::list_s;  # cannot use str @out because of JVM
        my str $spaces = ' ' x $spacing;
        my str $comma  = ",\n" ~ $spaces x $level;

#-- helper subs from here, with visibility to the above lexicals

        sub pretty-positional(\positional --> Nil) {
            $comma = nqp::concat($comma,$spaces);
            nqp::push_s($out,'[');
            nqp::push_s($out,nqp::substr($comma,1));

            for positional.list {
                jsonify($_);
                nqp::push_s($out,$comma);
            }
            nqp::pop_s($out);  # lose last comma

            $comma = nqp::substr($comma,0,nqp::sub_i(nqp::chars($comma),$spacing));
            nqp::push_s($out,nqp::substr($comma,1));
            nqp::push_s($out,']');
        }

        sub pretty-associative(\associative --> Nil) {
            $comma = nqp::concat($comma,$spaces);
            nqp::push_s($out,'{');
            nqp::push_s($out,nqp::substr($comma,1));
            my \pairs := $sorted-keys
              ?? associative.sort(*.key)
              !! associative.list;

            for pairs {
                jsonify(.key);
                nqp::push_s($out,": ");
                jsonify(.value);
                nqp::push_s($out,$comma);
            }
            nqp::pop_s($out);  # lose last comma

            $comma = nqp::substr($comma,0,nqp::sub_i(nqp::chars($comma),$spacing));
            nqp::push_s($out,nqp::substr($comma,1));
            nqp::push_s($out,'}');
        }

        sub unpretty-positional(\positional --> Nil) {
            nqp::push_s($out,'[');
            my int $before = nqp::elems($out);
            for positional.list {
                jsonify($_);
                nqp::push_s($out,",");
            }
            nqp::pop_s($out) if nqp::elems($out) > $before;  # lose last comma
            nqp::push_s($out,']');
        }

        sub unpretty-associative(\associative --> Nil) {
            nqp::push_s($out,'{');
            my \pairs := $sorted-keys
              ?? associative.sort(*.key)
              !! associative.list;

            my int $before = nqp::elems($out);
            for pairs {
                jsonify(.key);
                nqp::push_s($out,":");
                jsonify(.value);
                nqp::push_s($out,",");
            }
            nqp::pop_s($out) if nqp::elems($out) > $before;  # lose last comma
            nqp::push_s($out,'}');
        }

        sub jsonify(\obj --> Nil) {

            with obj {

                # basic ones
                when Bool {
                    nqp::push_s($out,obj ?? "true" !! "false");
                }
                when IntStr {
                    jsonify(.Int);
                }
                when RatStr {
                    jsonify(.Rat);
                }
                when NumStr {
                    jsonify(.Num);
                }
                when Str {
                    nqp::push_s($out,'"');
                    nqp::push_s($out,str-escape(obj));
                    nqp::push_s($out,'"');
                }

                # numeric ones
                when Int {
                    nqp::push_s($out,.Str);
                }
                when Rat {
                    nqp::push_s($out,.contains(".") ?? $_ !! "$_.0")
                      given .Str;
                }
                when FatRat {
                    nqp::push_s($out,.contains(".") ?? $_ !! "$_.0")
                      given .Str;
                }
                when Num {
                    if nqp::isnanorinf($_) {
                        nqp::push_s(
                          $out,
                          $*JSON_NAN_INF_SUPPORT ?? obj.Str !! "null"
                        );
                    }
                    else {
                        nqp::push_s($out,.contains("e") ?? $_ !! $_ ~ "e0")
                          given .Str;
                    }
                }

                # iterating ones
                when Seq {
                    jsonify(.cache);
                }
                when Positional {
                    $pretty
                      ?? pretty-positional($_)
                      !! unpretty-positional($_);
                }
                when Associative {
                    $pretty
                      ?? pretty-associative($_)
                      !! unpretty-associative($_);
                }

                # rarer ones
                when Dateish {
                    nqp::push_s($out,qq/"$_"/);
                }
                when Instant {
                    nqp::push_s($out,qq/"{.DateTime}"/)
                }
                when Version {
                    jsonify(.Str)
                }

                # also handle exceptions here
                when Exception {
                    jsonify(obj.^name => Hash.new(
                      (message => nqp::can(obj,"message")
                        ?? obj.message !! Nil
                      ),
                      obj.^attributes.grep(*.has_accessor).map: {
                          with .name.substr(2) -> $attr {
                              $attr => (
                                (.defined and not $_ ~~ Real|Positional|Associative)
                                  ?? .Str !! $_
                              ) given obj."$attr"()
                          }
                      }
                    ));
                }

                # huh, what?
                default {
                    jsonify( { 0 => 'null' } );
                }
            }
            else {
                nqp::push_s($out,'null');
            }
        }

#-- do the actual work

        jsonify(obj);
        nqp::join("",$out)
    }

    # there's a new version of from-json and friends that's a lot faster,
    # but it relies on the existence of the Uni type.
    # It doesn't exist on jvm, unfortunately.
#?if moar
    my $ws := nqp::list_i;
    nqp::bindpos_i($ws,  9, 1);  # \t
    nqp::bindpos_i($ws, 10, 1);  # \n
    nqp::bindpos_i($ws, 13, 1);  # \r
    nqp::bindpos_i($ws, 32, 1);  # space
    nqp::push_i($ws, 0);  # allow for -1 as value

    my sub nom-ws(str $text, int $pos is rw --> Nil) {
        nqp::while(
          nqp::atpos_i($ws, nqp::ordat($text, $pos)),
          $pos = nqp::add_i($pos, 1)
        )
    }


    my $hexdigits := nqp::list;
    nqp::bindpos($hexdigits,  48,  0);  # 0
    nqp::bindpos($hexdigits,  49,  1);  # 1
    nqp::bindpos($hexdigits,  50,  2);  # 2
    nqp::bindpos($hexdigits,  51,  3);  # 3
    nqp::bindpos($hexdigits,  52,  4);  # 4
    nqp::bindpos($hexdigits,  53,  5);  # 5
    nqp::bindpos($hexdigits,  54,  6);  # 6
    nqp::bindpos($hexdigits,  55,  7);  # 7
    nqp::bindpos($hexdigits,  56,  8);  # 8
    nqp::bindpos($hexdigits,  57,  9);  # 9
    nqp::bindpos($hexdigits,  65, 10);  # A
    nqp::bindpos($hexdigits,  66, 11);  # B
    nqp::bindpos($hexdigits,  67, 12);  # C
    nqp::bindpos($hexdigits,  68, 13);  # D
    nqp::bindpos($hexdigits,  69, 14);  # E
    nqp::bindpos($hexdigits,  70, 15);  # F
    nqp::bindpos($hexdigits,  97, 10);  # a
    nqp::bindpos($hexdigits,  98, 11);  # b
    nqp::bindpos($hexdigits,  99, 12);  # c
    nqp::bindpos($hexdigits, 100, 13);  # d
    nqp::bindpos($hexdigits, 101, 14);  # e
    nqp::bindpos($hexdigits, 102, 15);  # f

    my $escapees := nqp::list_i;
    nqp::bindpos_i($escapees,  34, 34);  # "
    nqp::bindpos_i($escapees,  47, 47);  # /
    nqp::bindpos_i($escapees,  92, 92);  # \
    nqp::bindpos_i($escapees,  98,  8);  # b
    nqp::bindpos_i($escapees, 102, 12);  # f
    nqp::bindpos_i($escapees, 110, 10);  # n
    nqp::bindpos_i($escapees, 114, 13);  # r
    nqp::bindpos_i($escapees, 116,  9);  # t


    my sub parse-string(str $text, int $pos is rw) {
        nqp::if(
          nqp::eqat($text, '"', nqp::sub_i($pos,1))  # starts with clean "
            && nqp::eqat($text, '"',                 # ends with clean "
                 (my int $end = nqp::findnotcclass(nqp::const::CCLASS_WORD,
                   $text, $pos, nqp::sub_i(nqp::chars($text),$pos)))
          ),
          nqp::stmts(
            (my $string := nqp::substr($text, $pos, nqp::sub_i($end, $pos))),
            ($pos = nqp::add_i($end,1)),
            $string
          ),
          parse-string-slow($text, $pos)
        )
    }

    # Slower parsing of string if the string does not exist of 0 or more
    # alphanumeric characters
    my sub parse-string-slow(str $text, int $pos is rw) {

        my int $start = nqp::sub_i($pos,1);  # include starter in string
        nqp::until(
          nqp::iseq_i((my $end := nqp::index($text, '"', $pos)), -1),
          nqp::stmts(
            ($pos = $end + 1),
            (my int $index = 1),
            nqp::while(
              nqp::eqat($text, '\\', nqp::sub_i($end, $index)),
              ($index = nqp::add_i($index, 1))
            ),
            nqp::if(
              nqp::bitand_i($index, 1),
              (return unjsonify-string(      # preceded by an even number of \
                nqp::strtocodes(
                  nqp::substr($text, $start, $end - $start),
                  nqp::const::NORMALIZE_NFD,
                  nqp::create(NFD)
                ),
                $pos
              ))
            )
          )
        );
        die "unexpected end of input in string";
    }

    # convert a sequence of Uni elements into a string, with the initial
    # quoter as the first element.
    my sub unjsonify-string(Uni:D \codes, int $pos) {
        nqp::shift_i(codes);  # lose the " without any decoration

        # fetch a single codepoint from the next 4 Uni elements
        my sub fetch-codepoint() {
            my int $codepoint = 0;
            my int $times = 5;

            nqp::while(
              ($times = nqp::sub_i($times, 1)),
              nqp::if(
                nqp::elems(codes),
                nqp::if(
                  nqp::iseq_i(
                    (my uint32 $ordinal = nqp::shift_i(codes)),
                    48  # 0
                  ),
                  ($codepoint = nqp::mul_i($codepoint, 16)),
                  nqp::if(
                    (my int $adder = nqp::atpos($hexdigits, $ordinal)),
                    ($codepoint = nqp::add_i(
                      nqp::mul_i($codepoint, 16),
                      $adder
                    )),
                    (die "invalid hexadecimal char {
                        nqp::chr($ordinal).perl
                    } in \\u sequence at $pos")
                  )
                ),
                (die "incomplete \\u sequence in string near $pos")
              )
            );

            $codepoint
        }

        my $output := nqp::create(Uni);
        nqp::while(
          nqp::elems(codes),
          nqp::if(
            nqp::iseq_i(
              (my uint32 $ordinal = nqp::shift_i(codes)),
              92  # \
            ),
            nqp::if(                                           # haz an escape
              nqp::iseq_i(($ordinal = nqp::shift_i(codes)), 117),  # u
              nqp::stmts(                                      # has a \u escape
                nqp::if(
                  nqp::isge_i((my int $codepoint = fetch-codepoint), 0xD800)
                    && nqp::islt_i($codepoint, 0xE000),
                  nqp::if(                                     # high surrogate
                    nqp::iseq_i(nqp::atpos_i(codes, 0),  92)        # \
                      && nqp::iseq_i(nqp::atpos_i(codes, 1), 117),  # u
                    nqp::stmts(                                # low surrogate
                      nqp::shift_i(codes),  # get rid of \
                      nqp::shift_i(codes),  # get rid of u
                      nqp::if(
                        nqp::isge_i((my int $low = fetch-codepoint), 0xDC00),
                        ($codepoint = nqp::add_i(              # got low surrogate
                          nqp::add_i(                          # transmogrify
                            nqp::mul_i(nqp::sub_i($codepoint, 0xD800), 0x400),
                            0x10000                            # with
                          ),                                   # low surrogate
                          nqp::sub_i($low, 0xDC00)
                        )),
                        (die "improper low surrogate \\u$low.base(16) for high surrogate \\u$codepoint.base(16) near $pos")
                      )
                    ),
                    (die "missing low surrogate for high surrogate \\u$codepoint.base(16) near $pos")
                  )
                ),
                nqp::push_i($output, $codepoint)
              ),
              nqp::if(                                         # other escapes?
                ($codepoint = nqp::atpos_i($escapees, $ordinal)),
                nqp::push_i($output, $codepoint),              # recognized escape
                (die "unknown escape code found '\\{           # huh?
                    nqp::chr($ordinal)
                }' found near $pos")
              )
            ),
            nqp::if(                                           # not an escape
              nqp::iseq_i($ordinal, 9) || nqp::iseq_i($ordinal, 10),  # \t \n
              (die "this kind of whitespace is not allowed in a string: '{
                  nqp::chr($ordinal).perl
              }' near $pos"),
              nqp::push_i($output, $ordinal)                   # ok codepoint
            )
          )
        );

        nqp::strfromcodes($output)
    }

    my sub parse-numeric(str $text, int $pos is rw) {
        my int $start = nqp::sub_i($pos,1);

        my int $end = nqp::findnotcclass(nqp::const::CCLASS_NUMERIC,
          $text, $pos, nqp::sub_i(nqp::chars($text),$pos));
        nqp::if(
          nqp::iseq_i(nqp::ordat($text, $end), 46),                      # .
          nqp::stmts(
            ($pos = nqp::add_i($end,1)),
            ($end = nqp::findnotcclass(nqp::const::CCLASS_NUMERIC,
              $text, $pos, nqp::sub_i(nqp::chars($text),$pos))
            )
          )
        );

        nqp::if(
          nqp::iseq_i((my int $ordinal = nqp::ordat($text, $end)), 101)  # e
           || nqp::iseq_i($ordinal, 69),                                 # E
          nqp::stmts(
            ($pos = nqp::add_i($end,1)),
            ($pos = nqp::add_i($pos,
              nqp::eqat($text, '-', $pos) || nqp::eqat($text, '+', $pos)
            )),
            ($end = nqp::findnotcclass(nqp::const::CCLASS_NUMERIC,
              $text, $pos, nqp::sub_i(nqp::chars($text),$pos))
            )
          )
        );

        my $result := nqp::substr($text, $start, nqp::sub_i($end,$start)).Numeric;
        nqp::if(
          nqp::istype($result, Failure),
          nqp::stmts(
            $result.Bool,  # handle Failure
            (die "at $pos: invalid number token $text.substr($start,$end - $start)")
          ),
          nqp::stmts(
            ($pos = $end),
            $result
          )
        )
    }

    my sub parse-obj(str $text, int $pos is rw) {
        my %result;
        my $hash := nqp::ifnull(
          nqp::getattr(%result,Map,'$!storage'),
          nqp::bindattr(%result,Map,'$!storage',nqp::hash)
        );

        nom-ws($text, $pos);
        my int $ordinal = nqp::ordat($text, $pos);
        nqp::if(
          nqp::iseq_i($ordinal, 125),  # }             {
          nqp::stmts(
            ($pos = nqp::add_i($pos,1)),
            %result
          ),
          nqp::stmts(
            my $descriptor := nqp::getattr(%result,Hash,'$!descriptor');
            nqp::stmts(  # this level is needed for some reason
              nqp::while(
                1,
                nqp::stmts(
                  nqp::if(
                    nqp::iseq_i($ordinal, 34),  # "
                    (my $key := parse-string($text, $pos = nqp::add_i($pos,1))),
                    (die nqp::if(
                      nqp::iseq_i($pos, nqp::chars($text)),
                      "at end of input: expected a quoted string for an object key",
                      "at $pos: json requires object keys to be strings"
                    ))
                  ),
                  nom-ws($text, $pos),
                  nqp::if(
                    nqp::iseq_i(nqp::ordat($text, $pos), 58),  # :
                    ($pos = nqp::add_i($pos, 1)),
                    (die "expected to see a ':' after an object key")
                  ),
                  nom-ws($text, $pos),
                  nqp::bindkey($hash, $key,
                    nqp::p6scalarwithvalue($descriptor, parse-thing($text, $pos))),
                  nom-ws($text, $pos),
                  ($ordinal = nqp::ordat($text, $pos)),
                  nqp::if(
                    nqp::iseq_i($ordinal, 125),  # }  {
                    nqp::stmts(
                      ($pos = nqp::add_i($pos,1)),
                      (return %result)
                    ),
                    nqp::unless(
                      nqp::iseq_i($ordinal, 44),  # ,
                      (die nqp::if(
                        nqp::iseq_i($pos, nqp::chars($text)),
                        "at end of input: unexpected end of object.",
                        "unexpected '{ nqp::substr($text, $pos, 1) }' in an object at $pos"
                      ))
                    )
                  ),
                  nom-ws($text, $pos = nqp::add_i($pos,1)),
                  ($ordinal = nqp::ordat($text, $pos)),
                )
              )
            )
          )
        )
    }


    my sub parse-array(str $text, int $pos is rw) {
        my @result;
        nqp::bindattr(@result, List, '$!reified',
          my $buffer := nqp::create(IterationBuffer));

        nom-ws($text, $pos);
        nqp::if(
          nqp::eqat($text, ']', $pos),
          nqp::stmts(
            ($pos = nqp::add_i($pos,1)),
            @result
          ),
          nqp::stmts(
            (my $descriptor := nqp::getattr(@result, Array, '$!descriptor')),
            nqp::while(
              1,
              nqp::stmts(
                (my $thing := parse-thing($text, $pos)),
                nom-ws($text, $pos),
                (my int $partitioner = nqp::ordat($text, $pos)),
                nqp::if(
                  nqp::iseq_i($partitioner,93),  # ]
                  nqp::stmts(
                    nqp::push($buffer,nqp::p6scalarwithvalue($descriptor,$thing)),
                    ($pos = nqp::add_i($pos,1)),
                    (return @result)
                  ),
                  nqp::if(
                    nqp::iseq_i($partitioner,44),  # ,
                    nqp::stmts(
                      nqp::push($buffer,nqp::p6scalarwithvalue($descriptor,$thing)),
                      ($pos = nqp::add_i($pos,1))
                    ),
                    (die "at $pos, unexpected partitioner '{
                        nqp::substr($text,$pos,1)
                    }' inside list of things in an array")
                  )
                )
              )
            )
          )
        )
    }

    my sub parse-thing(str $text, int $pos is rw) {
        nom-ws($text, $pos);

        my int $ordinal = nqp::ordat($text, $pos);
        if nqp::iseq_i($ordinal,34) {  # "
            parse-string($text, $pos = $pos + 1)
        }
        elsif nqp::iseq_i($ordinal,91) {  # [
            parse-array($text, $pos = $pos + 1)
        }
        elsif nqp::iseq_i($ordinal,123) {  # {
            parse-obj($text, $pos = $pos + 1)
        }
        elsif nqp::iscclass(nqp::const::CCLASS_NUMERIC, $text, $pos)
          || nqp::iseq_i($ordinal,45) {  # -
            parse-numeric($text, $pos = $pos + 1)
        }
        elsif nqp::iseq_i($ordinal,116) && nqp::eqat($text,'true',$pos) {
            $pos = $pos + 4;
            True
        }
        elsif nqp::iseq_i($ordinal,102) && nqp::eqat($text,'false',$pos) {
            $pos = $pos + 5;
            False
        }
        elsif nqp::iseq_i($ordinal,110) && nqp::eqat($text,'null',$pos) {
            $pos = $pos + 4;
            Any
        }
        else {
            die "at $pos: expected a json object, but got '{
              nqp::substr($text, $pos, 8).perl
            }'";
        }
    }

    method from-json(Str() $text) {
        CATCH { when X::AdHoc { die JSONException.new(:text($_)) } }

        my str $ntext   = $text;
        my int $length  = $text.chars;
        my int $pos     = 0;
        my     $result := parse-thing($text, $pos);

        try nom-ws($text, $pos);

        if $pos != nqp::chars($text) {
            die "additional text after the end of the document: { substr($text, $pos).raku }";
        }

        $result
    }
#?endif

#?if jvm
    my $ws := nqp::list_i;
    nqp::bindpos_i($ws,$_ + 1,1) for 9,10,13,32;  # allow for -1 as value
    my sub nom-ws(str $text, int $pos is rw --> Nil) {
        nqp::while(
          nqp::atpos_i($ws,nqp::ordat($text,$pos) + 1),
          $pos = $pos + 1
        );
        die "reached end of string when looking for something"
          if $pos == nqp::chars($text);
    }

    my sub tear-off-combiners(\text, \pos) {
        text.substr(pos,1).NFD.skip.map( {
             $^ord > 0x10000
               ?? to-surrogate-pair($^ord)
               !! $^ord.chr
        } ).join
    }

    my Mu $hexdigits := nqp::hash(
        '97', 1, '98', 1, '99', 1, '100', 1, '101', 1, '102', 1,
        '48', 1, '49', 1, '50', 1, '51', 1, '52', 1, '53', 1, '54', 1, '55', 1, '56', 1, '57', 1,
        '65', 1, '66', 1, '67', 1, '68', 1, '69', 1, '70', 1);

    my Mu $escapees := nqp::hash(
        "34", '"', "47", "/", "92", "\\", "98", "\b", "102", "\f", "110", "\n", "114", "\r", "116", "\t");

    my sub parse-string(str $text, int $pos is rw) {
        # first we gallop until the end of the string
        my int $startpos = $pos;
        my int $endpos;
        my int $textlength = nqp::chars($text);

        my int $ord;
        my int $has_hexcodes;
        my int $has_treacherous;
        my str $startcombiner = "";
        my Mu  $treacherous;
        my Mu  $escape_counts := nqp::hash();

        unless nqp::eqat($text, '"', $startpos - 1) {
            $startcombiner = tear-off-combiners($text, $startpos - 1);
        }

        loop {
            $ord = nqp::ordat($text, $pos);
            $pos = $pos + 1;

            if $pos > $textlength {
                die "unexpected end of document in string";
            }

            if nqp::eqat($text, '"', $pos - 1) {
                $endpos = $pos - 1;
                last;
            } elsif $ord == 92 {
                if     nqp::eqat($text, '"', $pos) or nqp::eqat($text, '\\', $pos) or nqp::eqat($text, 'b', $pos)
                    or nqp::eqat($text, 'f', $pos) or nqp::eqat($text,  'n', $pos) or nqp::eqat($text, 'r', $pos)
                    or nqp::eqat($text, 't', $pos) or nqp::eqat($text,  '/', $pos) {
                    my str $character = nqp::substr($text, $pos, 1);
                    if nqp::existskey($escape_counts, $character) {
                        nqp::bindkey($escape_counts, $character, nqp::atkey($escape_counts, $character) + 1);
                    } else {
                        nqp::bindkey($escape_counts, $character, 1);
                    }
                    $pos = $pos + 1;
                } elsif nqp::eqat($text, 'u', $pos) {
                    loop {
                        die "unexpected end of document; was looking for four hexdigits." if $textlength - $pos < 5;
                        if      nqp::existskey($hexdigits, nqp::ordat($text, $pos + 1))
                            and nqp::existskey($hexdigits, nqp::ordat($text, $pos + 2))
                            and nqp::existskey($hexdigits, nqp::ordat($text, $pos + 3))
                            and nqp::existskey($hexdigits, nqp::ordat($text, $pos + 4)) {
                            $pos = $pos + 4;
                        } else {
                            die "expected hexadecimals after \\u, but got \"{ nqp::substr($text, $pos - 1, 6) }\" at $pos";
                        }
                        $pos++;
                        if nqp::eqat($text, '\u', $pos) {
                            $pos++;
                        } else {
                            last
                        }
                    }
                    $has_hexcodes++;
                } elsif nqp::existskey($escapees, nqp::ordat($text, $pos)) {
                    # treacherous!
                    $has_treacherous++;
                    $treacherous := nqp::hash() unless $treacherous;
                    my int $treach_ord = nqp::ordat($text, $pos);
                    if nqp::existskey($treacherous, $treach_ord) {
                        nqp::bindkey($treacherous, $treach_ord, nqp::atkey($treacherous, $treach_ord) + 1)
                    } else {
                        nqp::bindkey($treacherous, $treach_ord, 1)
                    }
                    $pos++;
                } else {
                    die "don't understand escape sequence '\\{ nqp::substr($text, $pos, 1) }' at $pos";
                }
            } elsif $ord == 9 or $ord == 10 {
                die "this kind of whitespace is not allowed in a string: { nqp::substr($text, $pos - 1, 1).raku } at {$pos - 1}";
            }
        }

        $pos = $pos + 1;

        my str $raw = nqp::substr($text, $startpos, $endpos - $startpos);
        if $startcombiner {
            $raw = $startcombiner ~ $raw
        }
        if not $has_treacherous and not $has_hexcodes and $escape_counts {
            my @a;
            my @b;
            if nqp::existskey($escape_counts, "n") and nqp::existskey($escape_counts, "r") {
                @a.push("\\r\\n"); @b.push("\r\n");
            }
            if nqp::existskey($escape_counts, "n") {
                @a.push("\\n"); @b.push("\n");
            }
            if nqp::existskey($escape_counts, "r") {
                @a.push("\\r"); @b.push("\r");
            }
            if nqp::existskey($escape_counts, "t") {
                @a.push("\\t"); @b.push("\t");
            }
            if nqp::existskey($escape_counts, '"') {
                @a.push('\\"'); @b.push('"');
            }
            if nqp::existskey($escape_counts, "/") {
                @a.push("\\/"); @b.push("/");
            }
            if nqp::existskey($escape_counts, "\\") {
                @a.push("\\\\"); @b.push("\\");
            }
            $raw .= trans(@a => @b) if @a;
        } elsif $has_hexcodes or nqp::elems($escape_counts) {
            $raw = $raw.subst(/ \\ (<-[uU]>) || [\\ (<[uU]>) (<[a..f 0..9 A..F]> ** 3)]+ %(<[a..f 0..9 A..F]>) (:m <[a..f 0..9 A..F]>) /,
                -> $/ {
                    if $0.elems > 1 || $0.Str eq "u" || $0.Str eq "U" {
                        my @caps = $/.caps>>.value>>.Str;
                        my $result = $/;
                        my str $endpiece = "";
                        if my $lastchar = nqp::chr(nqp::ord(@caps.tail)) ne @caps.tail {
                            $endpiece = tear-off-combiners(@caps.tail, 0);
                            @caps.pop;
                            @caps.push($lastchar);
                        }
                        my int @hexes;
                        for @caps -> $u, $first, $second {
                            @hexes.push(:16($first ~ $second).self);
                        }

                        CATCH {
                            die "Couldn't decode hexadecimal unicode escape { $result.Str } at { $startpos + $result.from }";
                        }

                        utf16.new(@hexes).decode ~ $endpiece;
                    } else {
                        if nqp::existskey($escapees, nqp::ordat($0.Str, 0)) {
                            my str $replacement = nqp::atkey($escapees, nqp::ordat($0.Str, 0));
                            $replacement ~ tear-off-combiners($0.Str, 0);
                        } else {
                            die "stumbled over unexpected escape code \\{ chr(nqp::ordat($0.Str, 0)) } at { $startpos + $/.from }";
                        }
                    }
                }, :g);
        }

        $pos = $pos - 1;

        $raw
    }

    my sub parse-numeric(str $text, int $pos is rw) {
        my int $startpos = $pos;

        $pos = $pos + 1 while nqp::iscclass(nqp::const::CCLASS_NUMERIC, $text, $pos);

        my $residual := nqp::substr($text, $pos, 1);

        if $residual eq '.' {
            $pos = $pos + 1;

            $pos = $pos + 1 while nqp::iscclass(nqp::const::CCLASS_NUMERIC, $text, $pos);

            $residual := nqp::substr($text, $pos, 1);
        }

        if $residual eq 'e' || $residual eq 'E' {
            $pos = $pos + 1;

            if nqp::eqat($text, '-', $pos) || nqp::eqat($text, '+', $pos) {
                $pos = $pos + 1;
            }

            $pos = $pos + 1 while nqp::iscclass(nqp::const::CCLASS_NUMERIC, $text, $pos);
        }

        +(my $result := nqp::substr($text, $startpos - 1, $pos - $startpos + 1)) // die "at $pos: invalid number token $result.raku()";
    }

    my sub parse-obj(str $text, int $pos is rw) {
        my %result;

        my $key;
        my $value;

        nom-ws($text, $pos);

        if nqp::eqat($text, '}', $pos) {
            $pos = $pos + 1;
            return %result;
        }

        my $thing;
        loop {
            $thing := Any;

            if $key.DEFINITE {
                $thing := parse-thing($text, $pos)
            } else {
                nom-ws($text, $pos);

                if nqp::ordat($text, $pos) == 34 { # "
                    $pos    = $pos + 1;
                    $thing := parse-string($text, $pos)
                } else {
                    die "at end of string: expected a quoted string for an object key" if $pos == nqp::chars($text);
                    die "at $pos: json requires object keys to be strings";
                }
            }
            nom-ws($text, $pos);

            #my str $partitioner = nqp::substr($text, $pos, 1);

            if      nqp::eqat($text, ':', $pos) and   !($key.DEFINITE or      $value.DEFINITE) {
                $key := $thing;
            } elsif nqp::eqat($text, ',', $pos) and     $key.DEFINITE and not $value.DEFINITE {
                $value        := $thing;
                %result{$key}  = $value;
                $key          := Any;
                $value        := Any;
            } elsif nqp::eqat($text, '}', $pos) and     $key.DEFINITE and not $value.DEFINITE {
                $value        := $thing;
                %result{$key}  = $value;
                $pos           = $pos + 1;
                last;
            } else {
                die "at end of string: unexpected end of object." if $pos == nqp::chars($text);
                die "unexpected { nqp::substr($text, $pos, 1) } in an object at $pos";
            }

            $pos = $pos + 1;
        }

        %result
    }

    my sub parse-array(str $text, int $pos is rw) {
        my @result;

        nom-ws($text, $pos);

        if nqp::eqat($text, ']', $pos) {
            $pos = $pos + 1;
            return @result;
        }

        my     $thing;
        my str $partitioner;

        loop {
            $thing := parse-thing($text, $pos);
            nom-ws($text, $pos);

            $partitioner = nqp::substr($text, $pos, 1);
            $pos = $pos + 1;

            if $partitioner eq ']' {
                @result.push: $thing;
                last;
            } elsif $partitioner eq "," {
                @result.push: $thing;
            } else {
                die "at $pos, unexpected $partitioner inside list of things in an array";
            }
        }

        @result
    }

    my sub parse-thing(str $text, int $pos is rw) {
        nom-ws($text, $pos);

        my str $initial = nqp::substr($text, $pos, 1);

        $pos = $pos + 1;

        if nqp::ord($initial) == 34 { # "
            parse-string($text, $pos);
        } elsif $initial eq '[' {
            parse-array($text, $pos);
        } elsif $initial eq '{' {
            parse-obj($text, $pos);
        } elsif nqp::iscclass(nqp::const::CCLASS_NUMERIC, $initial, 0) || $initial eq '-' {
            parse-numeric($text, $pos);
        } elsif $initial eq 'n' {
            if nqp::eqat($text, 'ull', $pos) {
                $pos += 3;
                Any;
            } else {
                die "at $pos: i was expecting a 'null' but there wasn't one: { nqp::substr($text, $pos - 1, 10) }"
            }
        } elsif $initial eq 't' {
            if nqp::eqat($text, 'rue', $pos) {
                $pos = $pos + 3;
                True
            } else {
                die "at $pos: expected 'true', found { $initial ~ nqp::substr($text, $pos, 3) } instead.";
            }
        } elsif $initial eq 'f' {
            if nqp::eqat($text, 'alse', $pos) {
                $pos = $pos + 4;
                False
            } else {
                die "at $pos: expected 'false', found { $initial ~ nqp::substr($text, $pos, 4) } instead.";
            }
        } else {
            my str $rest = nqp::substr($text, $pos - 1, 8).raku;
            die "at $pos: expected a json object, but got $initial (context: $rest)"
        }
    }

    method from-json(Str() $text) {
        CATCH { when X::AdHoc { die JSONException.new(:text($_)) } }

        my str $ntext   = $text;
        my int $length  = $text.chars;
        my int $pos     = 0;
        my     $result := parse-thing($text, $pos);

        try nom-ws($text, $pos);

        if $pos != nqp::chars($text) {
            die "additional text after the end of the document: { substr($text, $pos).raku }";
        }

        $result
    }

#?endif
}

# vim: expandtab shiftwidth=4
