# Some maximum cache sizes we allow at a callsite before we switch to a
# megamorphic strategy.
my int $MEGA-TYPE-CALLSITE-SIZE := 16;
my int $MEGA-METH-CALLSITE-SIZE := 16;

# Return value decontainerization dispatcher. Often we have nothing at all
# to do, in which case we can make it identity. Other times, we need a
# decont. In a few, we need to re-wrap it.
{
    my $container-fallback := -> $Iterable, $cont {
        if nqp::isrwcont($cont) {
            # It's an RW container, so we really need to decont it.
            my $rv := nqp::decont($cont);
            if nqp::istype($rv, $Iterable) {
                my $rc := nqp::create(Scalar);
                nqp::bindattr($rc, Scalar, '$!value', $rv);
                $rc
            }
            else {
                $rv
            }
        }
        else {
            # A read-only container, so just return it.
            $cont
        }
    }

    my $recont := -> $obj {
        my $rc := nqp::create(Scalar);
        nqp::bindattr($rc, Scalar, '$!value', nqp::decont($obj));
        $rc
    }

    nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-rv-decont', -> $capture {
        # If it's heading megamorphic, then we'll install the fallback, without
        # any conditions, which is faster than over-filling the cache and running
        # this dispatch logic every time.
        my int $cache-size := nqp::dispatch('boot-syscall', 'dispatcher-inline-cache-size');
        if $cache-size >= $MEGA-TYPE-CALLSITE-SIZE {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        $capture, 0, nqp::gethllsym('Raku', 'Iterable')),
                    0, $container-fallback));
        }
        else {
            # We always need to guard on type and concreteness.
            my $rv := nqp::captureposarg($capture, 0);
            my $rv_arg := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                    $capture, 0);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $rv_arg);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $rv_arg);

            # Is it a container?
            if nqp::isconcrete_nd($rv) && nqp::iscont($rv) {
                # It's a container. We have special cases for Scalar.
                if nqp::istype_nd($rv, Scalar) {
                    # Check if the descriptor is undefined, in which case it's read-only.
                    my $desc := nqp::getattr($rv, Scalar, '$!descriptor');
                    my $desc_arg := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                            $rv_arg, Scalar, '$!descriptor');
                    nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $desc_arg);
                    if nqp::isconcrete($desc) {
                        # Writeable, so we may need to recontainerize the value if
                        # the type is iterable, otherwise we can decont it.
                        my $value := nqp::getattr($rv, Scalar, '$!value');
                        my $value_arg := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                            $rv_arg, Scalar, '$!value');
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $value_arg);
                        if nqp::istype_nd($value, nqp::gethllsym('Raku', 'Iterable')) {
                            # Need to recont in order to preserve item nature.
                            # Shuffle in the recont code to invoke. We already
                            # read the deconted value, so we insert that as the
                            # arg so it needn't be dereferenced again.
                            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                                        nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                                            $capture, 0),
                                        0, $value_arg),
                                    0, $recont));
                        }
                        else {
                            # Decont, so just evaluate to the read attr (boot-value
                            # ignores all but the first argument).
                            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                                    $capture, 0, $value_arg));
                        }
                    }
                    else {
                        # Read-only, so identity will do.
                        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $capture);
                    }
                }
                else {
                    # Delegate to non-Scalar container fallback.
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                $capture, 0, nqp::gethllsym('Raku', 'Iterable')),
                            0, $container-fallback));
                }
            }
            else {
                # Not containerized, so identity shall do.
                # Unless it is null, then we map it to Mu.
                if nqp::isnull($rv) {
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            $capture, 0, Mu));
                }
                else {
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $capture);
                }
            }
        }
    });

    nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-rv-decont-6c', -> $capture {
        # This emulates a bug where Proxy was never decontainerized no
        # matter what. The ecosystem came to depend on that, so we will
        # accept it for now. We need to revisit this in the future.
        my $rv := nqp::captureposarg($capture, 0);
        if nqp::eqaddr(nqp::what_nd($rv), Proxy) && nqp::isconcrete_nd($rv) {
            my $rv_arg := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                    $capture, 0);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $rv_arg);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $rv_arg);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $capture);
        }
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-rv-decont', $capture);
        }
    });
}


# Assignment dispatcher, which case-analyzes assignments and provides optimized
# paths for a range of common situations, and typically lifting type checks to
# be guards.
{
    sub assign-type-error($desc, $value) {
        Perl6::Metamodel::Configuration.throw_or_die(
            'X::TypeCheck::Assignment',
            "Type check failed in assignment",
            :symbol($desc.name),
            :$desc,
            :got($value),
            :expected($desc.of)
        );
    }

    my $assign-fallback := -> $cont, $value {
        nqp::assign($cont, $value)
    }

    my $assign-scalar-no-whence-no-typecheck := -> $cont, $value {
        nqp::bindattr($cont, Scalar, '$!value', $value)
    }

    my $assign-scalar-nil-no-whence := -> $cont, $value {
        my $desc := nqp::getattr($cont, Scalar, '$!descriptor');
        nqp::bindattr($cont, Scalar, '$!value',
            nqp::getattr($desc, ContainerDescriptor, '$!default'))
    }

    my $assign-scalar-no-whence := -> $cont, $value {
        my $desc := nqp::getattr($cont, Scalar, '$!descriptor');
        my $type := nqp::getattr($desc, ContainerDescriptor, '$!of');
        if nqp::istype($value, $type) {
            if nqp::how_nd($type).archetypes($type).coercive {
                $value := nqp::dispatch('raku-coercion', $type, $value);
            }
            nqp::bindattr($cont, Scalar, '$!value', $value);
        }
        else {
            assign-type-error($desc, $value);
        }
    }

    my $assign-scalar-bindpos-no-typecheck := -> $cont, $value {
        nqp::bindattr($cont, Scalar, '$!value', $value);
        my $desc := nqp::getattr($cont, Scalar, '$!descriptor');
        nqp::bindpos(
            nqp::getattr($desc, ContainerDescriptor::BindArrayPos, '$!target'),
            nqp::getattr_i($desc, ContainerDescriptor::BindArrayPos, '$!pos'),
            $cont);
        nqp::bindattr($cont, Scalar, '$!descriptor',
            nqp::getattr($desc, ContainerDescriptor::BindArrayPos, '$!next-descriptor'));
    }

    my $assign-scalar-bindpos := -> $cont, $value {
        my $desc := nqp::getattr($cont, Scalar, '$!descriptor');
        my $next := nqp::getattr($desc, ContainerDescriptor::BindArrayPos, '$!next-descriptor');
        my $type := nqp::getattr($next, ContainerDescriptor, '$!of');
        if nqp::istype($value, $type) {
            if nqp::how_nd($type).archetypes($type).coercive {
                $value := nqp::dispatch('raku-coercion', $type, $value);
            }
            nqp::bindattr($cont, Scalar, '$!value', $value);
            nqp::bindpos(
                nqp::getattr($desc, ContainerDescriptor::BindArrayPos, '$!target'),
                nqp::getattr_i($desc, ContainerDescriptor::BindArrayPos, '$!pos'),
                $cont);
            nqp::bindattr($cont, Scalar, '$!descriptor', $next);
        }
        else {
            assign-type-error($next, $value);
        }
    }

    my $assign-scalar-uninit-no-typecheck := -> $cont, $value {
        nqp::bindattr($cont, Scalar, '$!value', $value);
        my $desc := nqp::getattr($cont, Scalar, '$!descriptor');
        nqp::bindattr($cont, Scalar, '$!descriptor',
            nqp::getattr($desc, ContainerDescriptor::UninitializedAttribute,
                '$!next-descriptor'));
    }

    my $assign-scalar-uninit := -> $cont, $value {
        my $desc := nqp::getattr($cont, Scalar, '$!descriptor');
        my $next := nqp::getattr($desc, ContainerDescriptor::UninitializedAttribute,
            '$!next-descriptor');
        my $type := nqp::getattr($next, ContainerDescriptor, '$!of');
        if nqp::istype($value, $type) {
            if nqp::how_nd($type).archetypes($type).coercive {
                $value := nqp::dispatch('raku-coercion', $type, $value);
            }
            nqp::bindattr($cont, Scalar, '$!value', $value);
            nqp::bindattr($cont, Scalar, '$!descriptor', $next);
        }
        else {
            assign-type-error($next, $value);
        }
    }

    nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-assign', -> $capture {
        # Whatever we do, we'll guard on the type of the container and its
        # concreteness.
        my $tracked-cont := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-cont);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $tracked-cont);

        # We have various fast paths for an assignment to a Scalar.
        my int $optimized := 0;
        my $cont := nqp::captureposarg($capture, 0);
        if nqp::eqaddr(nqp::what_nd($cont), Scalar) && nqp::isconcrete_nd($cont) {
            # Now see what the Scalar descriptor type is.
            my $tracked-desc := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $tracked-cont, Scalar, '$!descriptor');
            my $desc := nqp::getattr($cont, Scalar, '$!descriptor');
            my $tracked-value := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1);
            my $value := nqp::captureposarg($capture, 1);
            if nqp::eqaddr($desc.WHAT, ContainerDescriptor) && nqp::isconcrete($desc) {
                # Simple assignment, no whence. But is Nil being assigned?
                my $of := $desc.of;
                if nqp::eqaddr($value, Nil) {
                    # Yes; just copy in the default, provided we've a simple type.
                    if $of.HOW.archetypes.nominal {
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-value);
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-desc);
                        my $tracked-of := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                                $tracked-desc, ContainerDescriptor, '$!of');
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $tracked-of);
                        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                $capture, 0, $assign-scalar-nil-no-whence));
                        $optimized := 1;
                    }
                }
                else {
                    # No whence, no Nil. Is it a nominal type? If yes, we can check
                    # it here.
                    nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-desc);
                    if $of.HOW.archetypes.nominal && nqp::istype($value, $of) {
                        # Nominal and passes type check; stack up gurads and delegate to
                        # simple bind.
                        my $tracked-of := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                                $tracked-desc, ContainerDescriptor, '$!of');
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $tracked-of);
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-value);
                        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                $capture, 0, $assign-scalar-no-whence-no-typecheck));
                        $optimized := 1;
                    }
                    else {
                        # Non-nominal or type check error.
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-not-literal-obj', $tracked-value, Nil);
                        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                $capture, 0, $assign-scalar-no-whence));
                        $optimized := 1;
                    }
                }
            }
            elsif nqp::eqaddr($desc.WHAT, ContainerDescriptor::Untyped) && nqp::isconcrete($desc) {
                if nqp::eqaddr($value, Nil) {
                    # Nil case is NYI.
                }
                else {
                    # Assignment to an untyped container descriptor; no type check
                    # is required, just bind the value into place.
                    nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-desc);
                    nqp::dispatch('boot-syscall', 'dispatcher-guard-not-literal-obj',
                        $tracked-value, Nil);
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            $capture, 0, $assign-scalar-no-whence-no-typecheck));
                    $optimized := 1;
                }
            }
            elsif nqp::eqaddr($desc.WHAT, ContainerDescriptor::UninitializedAttribute)
                    && nqp::isconcrete($desc) {
                # First assignment to an attribute where we care about whether it
                # was assigned to. We can produce a fast path for this, though
                # should check what the ultimate descriptor is. It really should
                # be a normal, boring, container descriptor.
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-desc);
                my $next := nqp::getattr($desc, ContainerDescriptor::UninitializedAttribute,
                    '$!next-descriptor');
                if nqp::eqaddr($next.WHAT, ContainerDescriptor) ||
                    nqp::eqaddr($next.WHAT, ContainerDescriptor::Untyped) {
                    # Ensure we're not assigning Nil (not yet fast-pathed, but
                    # probably not terribly likely).
                    unless nqp::eqaddr($value.WHAT, Nil) {
                        # Go by whether we can type check the target.
                        my $tracked-next := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                            $tracked-desc, ContainerDescriptor::UninitializedAttribute,
                            '$!next-descriptor');
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
                            $tracked-next);
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-value);
                        my $of := $next.of;
                        my $delegate := $of.HOW.archetypes.nominal &&
                                (nqp::eqaddr($of, Mu) || nqp::istype($value, $of))
                            ?? $assign-scalar-uninit-no-typecheck
                            !! $assign-scalar-uninit;
                        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                $capture, 0, $delegate));
                        $optimized := 1;
                    }
                }
            }
            elsif nqp::eqaddr($desc.WHAT, ContainerDescriptor::BindArrayPos) {
                # Bind into an array. We can produce a fast path for this, though
                # should check what the ultimate descriptor is. It really should
                # be a normal, boring, container descriptor.
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-desc);
                my $next := nqp::getattr($desc, ContainerDescriptor::BindArrayPos,
                    '$!next-descriptor');
                if nqp::eqaddr($next.WHAT, ContainerDescriptor) ||
                    nqp::eqaddr($next.WHAT, ContainerDescriptor::Untyped) {
                    # Ensure we're not assigning Nil. (This would be very odd, as
                    # a Scalar starts off with its default value, and if we are
                    # vivifying we'll likely have a new container).
                    unless nqp::eqaddr($value.WHAT, Nil) {
                        # Go by whether we can type check the target.
                        my $tracked-next := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                            $tracked-desc, ContainerDescriptor::BindArrayPos,
                            '$!next-descriptor');
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
                            $tracked-next);
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked-value);
                        my $of := $next.of;
                        my $delegate := $of.HOW.archetypes.nominal &&
                                (nqp::eqaddr($of, Mu) || nqp::istype($value, $of))
                            ?? $assign-scalar-bindpos-no-typecheck
                            !! $assign-scalar-bindpos;
                        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                $capture, 0, $delegate));
                        $optimized := 1;
                    }
                }
            }
        }

        # If nothing we could optimize, go for the fallback.
        unless $optimized {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    $capture, 0, $assign-fallback));
        }
    });
}

# Binding type assertion dispatcher, used to do type checks of binds. Evaluates
# to the value itself when the type check passes, installing a guard along the
# way. Otherwise, throws.
{
    sub bind-error($value, $type) {
        Perl6::Metamodel::Configuration.throw_or_die(
            'X::TypeCheck::Binding',
            "Type check failed in binding; expected '" ~
                nqp::how_nd($type).name($value) ~ "' but got '" ~
                nqp::how_nd($value).name($value) ~ "'",
            :got($value),
            :expected($type)
        );
    }

    my $bind-check := -> $value, $value-decont, $type {
        nqp::istype_nd($value-decont, $type) ?? $value !! bind-error($value, $type)
    }

    nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-bind-assert', -> $capture {
        my $value-decont := nqp::captureposarg($capture, 1);
        my $type := nqp::captureposarg($capture, 2);
        if nqp::how_nd($type).archetypes.nominal {
            if nqp::istype_nd($value-decont, $type) {
                # Nominal, so a type guard on the decont'd value will suffice,
                # then produce the original value.
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
                        nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1));
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
                        nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 2));
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $capture);
            }
            else {
                my $value := nqp::captureposarg($capture, 0);
                bind-error($value, $type)
            }
        }
        else {
            # Not a nominal type, can't guard it, so set up a call to do the
            # check late-bound.
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
                    nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 2));
            my $delegate := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $capture, 0, $bind-check);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                $delegate);
        }
    });
}

# Object construction time checking of if a container is initialized. Done as
# a dispatcher primarily to intern .^mixin_type, but also for more compact
# bytecode size in generated BUILDALL.
my $array-init-check := -> $arr {
    my $storage := nqp::getattr($arr, List, '$!reified');
    nqp::isconcrete($storage) && nqp::elems($storage)
}
my $hash-init-check := -> $hash {
    my $storage := nqp::getattr($hash, Map, '$!storage');
    nqp::isconcrete($storage) && nqp::elems($storage)
}
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-is-attr-inited', -> $capture {
    # If there's a non-concrete object observed, then we bound a non-container
    # in place, so trivially initialized.
    my $attr := nqp::captureposarg($capture, 0);
    my $track-attr := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
    my int $inited := 0;
    my $need-elem-check;
    if !nqp::isconcrete_nd($attr) {
        nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-attr);
        $inited := 1;
    }

    # Otherwise, might be a container that was assigned.
    else {
        # Just try and read a descriptor. Also see if we have an array or
        # hash, which needs an additional element check to handle an
        # assignment to an individual element during BUILD.
        my $base;
        my $desc;
        my $track-desc;
        try {
            $base := nqp::how_nd($attr).mixin_base($attr);
            $desc := nqp::getattr($attr, $base, '$!descriptor');
            $track-desc := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                $track-attr, $base, '$!descriptor');
        }

        # If we managed to track a descriptor, then we have a container to
        # see if was uninitialized. The attribute tracking above will have
        # established type/concreteness guards on the attribute, so don't
        # repeat them.
        if $track-desc {
            # Guard on the descriptor type, then outcome depends on if
            # it's an uninitialized attribute descriptor. If it is, then
            # for arrays and hashes we also need an extra check.
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-desc);
            $inited := !nqp::eqaddr($desc.WHAT, ContainerDescriptor::UninitializedAttribute);
            if nqp::istype($base, Array) {
                $need-elem-check := $array-init-check;
            }
            elsif nqp::istype($base, Hash) {
                $need-elem-check := $hash-init-check;
            }
        }

        # Otherwise, bound concrete value. Guard on type and concreteness,
        # outcome is that it's initialized.
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-attr);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-attr);
            $inited := 1;
        }
    }

    # Evaluate to result.
    if nqp::isconcrete($need-elem-check) && !$inited {
        # The descriptor suggests it's not initialized by assignment to
        # the entire array/hash, but individual elements may have been.
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                $capture, 0, $need-elem-check));
    }
    else {
        # It's certainly initialized per the descriptor, so we're done.
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-int',
                $capture, 0, $inited));
    }
});

# Sink dispatcher. Called in void context with the decontainerized value to sink.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-sink', -> $capture {
    # Guard on the type and concreteness.
    my $sinkee := nqp::captureposarg($capture, 0);
    my $track-sinkee := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
    nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-sinkee);
    nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-sinkee);

    # Now consider what we're sinking.
    if nqp::isconcrete_nd($sinkee) {
        # Concrete. See if it has a `sink` method, and then also if it's not
        # Mu.sink, which is a no-op.
        my $meth := nqp::decont(nqp::how_nd($sinkee).find_method($sinkee, 'sink'));
        if nqp::isconcrete($meth) && !nqp::eqaddr($meth, Mu.HOW.find_method(Mu, 'sink')) {
            # Need to do a call to the sink method. Since sink is a Raku thing,
            # assume we can go straight for the Raku method dispatcher.
            my $with-name := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-str', $capture, 0, 'sink');
            my $with-type := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $with-name, 0, nqp::what_nd($sinkee));
            my $with-callee := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $with-type, 0, $meth);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate',
                'raku-meth-call-resolved', $with-callee);
        }
        else {
            # Nothing to do (use boot-value and let void context take care of
            # discarding the value).
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $capture);
        }
    }
    else {
        # Not concrete, nothing to do.
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $capture);
    }
});

# A standard call (such as `func($arg)`, `$obj($arg)`, etc.) It receives the
# decontainerized callee as the first argument, followed by the arguments. Its
# primary purpose is to deal with multi dispatch vs. single dispatch and then
# delegate on to the appropriate dispatcher. It also looks at if we are doing a
# call to a method (either because it was looked up separately, or because we
# are here via lang-call), and in that case sends it via the resolved method
# call dispatcher. This means it will have a working nextsame etc.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-call', -> $capture {
    # Guard on the type and, if it's a routine, on the dispatchees. (We assume
    # that the set of dispatchees shall not change, even over closure clones -
    # this may not always be a good assumption - and so we guard on that. If
    # it's not a dispatcher, we'll be guarding on a literal type object.)
    my $track_callee := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
    nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track_callee);
    my $callee := nqp::captureposarg($capture, 0);
    if nqp::istype_nd($callee, Method) && (my str $meth-name := $callee.name) &&
            (my $inv_param := try { $callee.signature.params.AT-POS(0) }) {
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_callee);
        my $meth-type := $inv_param.type;
        my $with-type := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
            $capture, 1, $meth-type);
        my $with-name := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
            $with-type, 2, $meth-name);
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call-resolved',
            $with-name);
    }
    elsif nqp::istype_nd($callee, Routine) {
        if nqp::can($callee, 'WRAPPERS') {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke-wrapped', $capture);
        }
        elsif nqp::can($callee, 'CALL-ME') {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke', $capture);
        }
        else {
            my int $code-constant := nqp::dispatch('boot-syscall', 'dispatcher-is-arg-literal',
                $capture, 0);
            unless $code-constant {
                nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
                    nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                        $track_callee, Routine, '@!dispatchees'));
            }
            nqp::dispatch('boot-syscall', 'dispatcher-delegate',
                $callee.is_dispatcher ?? 'raku-multi' !! 'raku-invoke', $capture);
        }
    }
    else {
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke', $capture);
    }
});

# A standard method call of the form $obj.meth($arg); also used for the
# indirect form $obj."$name"($arg). It receives the decontainerized invocant,
# the method name, and the args (starting with the invocant including any
# container).
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-meth-call', -> $capture {
    # See if this callsite is heading megamorphic due to loads of different
    # method names or types; if so, we'll try to cope with that.
    my $obj := nqp::captureposarg($capture, 0);
    my str $name := nqp::captureposarg_s($capture, 1);
    my $how := nqp::how_nd($obj);
    my int $cache-size := nqp::dispatch('boot-syscall', 'dispatcher-inline-cache-size');
    if $cache-size >= $MEGA-METH-CALLSITE-SIZE && nqp::istype($how, Perl6::Metamodel::ClassHOW) {
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call-mega',
            $capture);
    }
    else {
        # See if we're making the method lookup on a pun; if so, rewrite the args
        # to do the call on the pun.
        if nqp::istype($how, Perl6::Metamodel::RolePunning) &&
                $how.is_method_call_punned($obj, $name) {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
                nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0));
            $obj := $how.pun($obj);
            $how := $obj.HOW;
            $capture := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
                0, $obj);
            $capture := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2),
                2, $obj);
        }

        # Try to resolve the method call.
        # TODO Assorted optimizations are possible here later on to speed up some
        # kinds of dispatch, including:
        # * Using the dispatcher to directly rewrite args and invoke FALLBACK if
        #   needed
        # * Handling some forms of delegation via the dispatcher mechanism
        my $meth := nqp::decont($how.find_method($obj, $name));

        # Report an error if there is no such method.
        unless nqp::isconcrete($meth) {
            my $class := nqp::getlexcaller('$?CLASS');
            report-method-not-found($obj, $name, $class, $how, nqp::iscont($obj));
        }

        # Establish a guard on the invocant type and method name (however the name
        # may well be a literal, in which case this is free).
        nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
            nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0));
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
            nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1));

        # Add the resolved method and delegate to the resolved method dispatcher.
        my $capture_delegate := nqp::dispatch('boot-syscall',
            'dispatcher-insert-arg-literal-obj', $capture, 0, $meth);
        nqp::dispatch('boot-syscall', 'dispatcher-delegate',
            'raku-meth-call-resolved', $capture_delegate);
    }
});
sub report-method-not-found($obj, $name, $class, $how, $containerized) {
    my $msg := "Method '$name' not found for invocant of class '{$how.name($obj)}'";
    if $name eq 'STORE' {
        Perl6::Metamodel::Configuration.throw_or_die(
            'X::Assignment::RO', $msg, :value($obj));
    }
    else {
        Perl6::Metamodel::Configuration.throw_or_die(
            'X::Method::NotFound',
            $msg,
            :invocant($obj),
            :method($name),
            :typename($how.name($obj)),
            :private(nqp::hllboolfor(0, 'Raku')),
            :in-class-call(nqp::hllboolfor(nqp::eqaddr(nqp::what($obj), $class), 'Raku')),
            :containerized(nqp::hllboolfor($containerized, 'Raku'))
        );
    }
}

nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-meth-call-mega', -> $capture {
    # We're megamorphic in both type and name. Make sure the type has a lookup
    # table, and ensure the method exists. If it does not have a method table
    # then we will not install this dispatcher; we may already have a type
    # megamorphic handler in place and only missed it because we temporarily
    # lacked the calculated flattened method table. This avoids us stacking
    # up the same program repeatedly at the callsite.
    my $obj := nqp::captureposarg($capture, 0);
    my $how := nqp::how_nd($obj);
    unless nqp::isconcrete(nqp::getattr($how, Perl6::Metamodel::ClassHOW,
            '$!cached_all_method_table')) {
        nqp::dispatch('boot-syscall', 'dispatcher-do-not-install');
    }
    my %lookup := $how.all_method_table($obj);
    my str $name := nqp::captureposarg_s($capture, 1);
    if nqp::isconcrete(nqp::atkey(%lookup, $name)) {
        # It exists. We'll set up a dispatch program that tracks the HOW of
        # the type, looks up the cached method table on it, and then tracks
        # the resolution of the method.
        my $track-obj := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        my $track-how := nqp::dispatch('boot-syscall', 'dispatcher-track-how', $track-obj);
        my $track-table := nqp::dispatch('boot-syscall', 'dispatcher-track-attr', $track-how,
            Perl6::Metamodel::ClassHOW, '$!cached_all_method_table');
        my $track-name := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1);
        my $track-resolution := nqp::dispatch('boot-syscall',
            'dispatcher-index-tracked-lookup-table', $track-table, $track-name);

        # This is only a valid dispatch program if the method is found. (If
        # not, we'll run this again to report the error.)
        nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-resolution);
        # Add the resolved method and delegate to the resolved method dispatcher.
        my $capture-delegate := nqp::dispatch('boot-syscall',
            'dispatcher-insert-arg', $capture, 0, $track-resolution);
        nqp::dispatch('boot-syscall', 'dispatcher-delegate',
            'raku-meth-call-resolved', $capture-delegate);
    }
    else {
        # Probably method not found, but sometimes the cache ends up missing
        # entries.
        my $slowpath-found := $how.find_method($obj, $name);
        if nqp::isconcrete($slowpath-found) {
            nqp::dispatch('boot-syscall', 'dispatcher-do-not-install');
            my $capture-delegate := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $capture, 0, $slowpath-found);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate',
                'raku-meth-call-resolved', $capture-delegate);
        }
        else {
            my $class := nqp::getlexcaller('$?CLASS');
            report-method-not-found($obj, $name, $class, $how, nqp::iscont($obj));
        }
    }
});

# Qualified method call dispatcher. This is used for calls of the form
# $foo.Some::ClassOrRole::bar($arg). It receives the decontainerized
# invocant, the method name, the type qualifier, and then the args
# (starting with the invocant including any container).
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-meth-call-qualified', -> $capture {
    # Lookup outers of the caller and locate the first occurence of the
    # symbols of interest, which tell us about the caller type.
    my $ctx := nqp::ctxcaller(nqp::ctx());
    my $caller-type := nqp::null();
    nqp::repeat_while(
        nqp::isnull($caller-type) && !nqp::isnull($ctx),
        nqp::stmts(
            (my $pad := nqp::ctxlexpad($ctx)),
            nqp::if(
                nqp::existskey($pad, '$?CONCRETIZATION'),
                ($caller-type := nqp::atkey($pad, '$?CONCRETIZATION')),
                nqp::if(
                    nqp::existskey($pad, '$?CLASS'),
                    ($caller-type := nqp::atkey($pad, '$?CLASS')),
                )
            ),
            ($ctx := nqp::ctxouterskipthunks($ctx)),
        )
    );

    # Establish a guard on the invocant and qualifying type.
    nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
        nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0));
    nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
        nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 2));

    # Try to resolve the method.
    my $obj := nqp::captureposarg($capture, 0);
    my str $name := nqp::captureposarg_s($capture, 1);
    my $type := nqp::captureposarg($capture, 2);
    my $meth;
    for ($caller-type, $obj.WHAT) {
        if nqp::istype($_, $type) {
            $meth := nqp::how_nd($_).find_method_qualified($_, $type, $name);
            last if nqp::isconcrete($meth);
        }
    }

    # If it's resolved, then:
    # 1. Drop the invocant and type arguments targetted at this resolution
    # 2. Insert the type we resolved the method against before those, for
    #    deferral (the name is retained ahead of this)
    # 3. Finally, prepend the resolved method, and delegate to the resolved
    #    method dispatcher.
    if nqp::isconcrete($meth) {
        my $with_name_and_args := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2),
            0);
        my $with_resolution_start := nqp::dispatch('boot-syscall',
            'dispatcher-insert-arg-literal-obj', $with_name_and_args, 0, $type);
        my $capture_delegate := nqp::dispatch('boot-syscall',
            'dispatcher-insert-arg-literal-obj', $with_resolution_start, 0, $meth);
        nqp::dispatch('boot-syscall', 'dispatcher-delegate',
                'raku-meth-call-resolved', $capture_delegate);
    }

    # Otherwise, exception.
    else {
        Perl6::Metamodel::Configuration.throw_or_die(
            'X::Method::InvalidQualifier',
            "Cannot dispatch to method $name on " ~ nqp::how_nd($type).name($type) ~
                " because it is not inherited or done by " ~ nqp::how_nd($obj).name($obj),
            :method($name), :invocant($obj), :qualifier-type($type));
    }
});

# Maybe method dispatch, of the form $obj.?foo.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-meth-call-me-maybe', -> $capture {
    # Establish a guard on the invocant type.
    nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
        nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0));

    # Try to find the method.
    my $invocant := nqp::captureposarg($capture, 0);
    my str $name := nqp::captureposarg_s($capture, 1);
    my $meth := nqp::decont(nqp::how_nd($invocant).find_method($invocant, $name));
    if nqp::isconcrete($meth) {
        # Found it. Put in resolved method and leave the rest to the resolved
        # method call dispatcher.
        my $capture_delegate := nqp::dispatch('boot-syscall',
            'dispatcher-insert-arg-literal-obj', $capture, 0, $meth);
        nqp::dispatch('boot-syscall', 'dispatcher-delegate',
                'raku-meth-call-resolved', $capture_delegate);
    }
    else {
        # Not found. Insert a Nil value at the start (boot-constant ignores
        # the rest of the args).
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                $capture, 0, Nil));
    }
});

# Private method dispatch. This is actually a fallback, since in the best
# case we can resolve the private method into a constant at code-gen time
# and just invoke that. This happens with private methods in roles.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-meth-private', -> $capture {
    # Find the private method.
    my $type := nqp::captureposarg($capture, 0);
    my str $name := nqp::captureposarg_s($capture, 1);
    my $meth := nqp::how_nd($type).find_private_method($type, $name);

    # If it's found, then we drop the first two arguments, insert the
    # resolved callee, and invoke it. This goes directly to invoke, as
    # there's no deferral (due to no inheritance relationship) or multi
    # dispatch for private methods.
    if nqp::isconcrete($meth) {
        nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
            nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0));
        my $capture_delegate := nqp::dispatch('boot-syscall',
            'dispatcher-insert-arg-literal-obj',
            nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
                $capture, 0, 2),
            0, $meth);
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke',
            $capture_delegate);
    }
    else {
        # TODO typed exception
        nqp::die("No such private method '$name' on " ~ nqp::how_nd($type).name($type));
    }
});

# A linked list is used to model the state of a dispatch that is deferring
# through a set of methods or wrappers (a similar approach is used for
# multi dispatch, but it needs more types of node). The Exhausted class
# is used as a sentinel for the end of the chain (or for dispatchers in
# general that have nothing more to contribute). The current state of the
# dispatch points into the linked list at the appropriate point; the chain
# itself is immutable, and shared over (runtime) dispatches.
my class DeferralChain {
    has $!code;
    has $!next;
    method new($code, $next) {
        my $obj := nqp::create(self);
        nqp::bindattr($obj, DeferralChain, '$!code', $code);
        nqp::bindattr($obj, DeferralChain, '$!next', $next);
        $obj
    }
    method code() { $!code }
    method next() { $!next }
};
my class Exhausted {};

# We also need a strategy for `callwith`. The complication is that arguments
# given to `callwith` should trigger a resumption of the innermost dispatch
# (for example, a wrap), but should also influence the arguments of outer
# dispatches too (for example, a multi and/or method dispatch). The overall
# strategy for `callwith` is:
# 1. Dispatchers for resumable dispatches are always broken into two parts:
#    one that manages the initial dispatch when there is no resume, and
#    another that walks through the candidates.
# 2. When we do a `callwith`, the current innermost of those dispatchers
#    marks itself exhausted. It then re-enters itself, passing along a
#    resumption of kind PROPAGATE_CALLWITH. This includes the information
#    required to put the next immediate call of the innermost dispatch
#    into effect (more in a moment on this).
# 3. The re-entered dispatcher sets its resume init args up using the
#    arguments from the callwith propagation, so that a later `callsame`
#    will use the correct arguments.
# 4. It then tries to use next-resumption to pass the callwith propagation
#    to any enclosing dispatcher. This always uses a common protocol: the
#    arguments are a kind (the PROPAGATE_CALLWITH integer constant), a
#    dispatcher name (string argument), a flag for if the arguments include
#    the invocant (an integer), followed by (typically) some arguments to
#    the named dispatcher. This is the information required to send the
#    dispatch to the destination determined by the *innermost* dispatcher.
# 5. If there is no next resumption, then we do that call.
# 6. Any outer dispatcher, upon receiving a PROPAGATE_CALLWITH, will mark
#    itself exhausted and re-enter itself with the new arguments. It then
#    continues with the above process from step 4.
# Thus, a callwith in the case there is an active wrap, multi, and method
# dispatch, ends up with all the previous resumable dispatchers for these
# marked as exhausted, and new ones created, with the updated arguments.
# Once all of these updated dispatchers are in place, then finally the next
# candidate of the innermost dispatch is invoked.
sub nil-or-callwith-propagation-terminal($capture) {
    unless nqp::dispatch('boot-syscall', 'dispatcher-next-resumption', $capture) {
        if nqp::captureposarg_i($capture, 0) == nqp::const::DISP_PROPAGATE_CALLWITH {
            my str $dispatcher := nqp::captureposarg_s($capture, 1);
            my $invoke-args := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
                $capture, 0, 3);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', $dispatcher, $invoke-args);
        }
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    $capture, 0, Nil));
        }
    }
}

# Resolved method call dispatcher. This is used to call a method, once we have
# already resolved it to a callee. Its first arg is the callee, the second and
# third are the type and name (used in deferral), and the rest are the args to
# the method.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-meth-call-resolved',
    # Initial dispatch
    -> $capture {
        # Save dispatch state for resumption. We don't need the method that will
        # be called now, so drop it.
        my $resume-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
            $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-set-resume-init-args', $resume-capture);

        # Drop the dispatch start type and name, and delegate to multi-dispatch or
        # just invoke if it's single dispatch.
        my $delegate_capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
            $capture, 1, 2);
        my $method := nqp::captureposarg($delegate_capture, 0);
        my int $code-constant := nqp::dispatch('boot-syscall', 'dispatcher-is-arg-literal',
            $capture, 0);
        unless $code-constant {
            # Need at least a type guard on the callee, if it's not constant.
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
                nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0));
        }
        if nqp::istype($method, Routine) {
            if nqp::can($method, 'WRAPPERS') {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke-wrapped',
                    $delegate_capture);
            }
            elsif nqp::can($method, 'CALL-ME') {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke', $delegate_capture);
            }
            else {
                # If it's not a constant, need a guard on whether it's a dispatcher,
                # and if so on the candidate list. (Will want to move this when we
                # have a megamorphic multi solution.)
                unless $code-constant {
                    nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
                        nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                            nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0),
                            Routine, '@!dispatchees'));
                }
                my str $dispatcher := $method.is_dispatcher ?? 'raku-multi' !! 'raku-invoke';
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', $dispatcher,
                    $delegate_capture);
            }
        }
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke', $delegate_capture);
        }
    },
    # Resumption. The capture itself has a first argument indicating the kind
    # of resumption operation we're doing. The resume init capture's first two
    # arguments are the type that we initially did a method dispatch against
    # and the method name respectively. In this resumption we calculate the
    # set of methods we will defer through. We then delegate to another
    # dispatcher to handle movement through that list (this structure helps
    # us to handle `callwith`).
    -> $capture {
        # We put a sentinel value into the resume state in the case that we have
        # already set up the method resumption. We always guard on it too.
        my $state := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-state');
        my $track_state := nqp::dispatch('boot-syscall', 'dispatcher-track-resume-state');
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_state);

        # Mark that the deferral was already set up, so we don't do this
        # again.
        nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal', Exhausted);

        # Guard on the kind of resume we're doing, and get that flag.
        my $track_kind := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_kind);
        my int $kind := nqp::captureposarg_i($capture, 0);

        # If the state is null, it's we are entering a walk through the methods.
        # We can short-circuit this if it's a lastcall (and the Exhausted above
        # puts it into effect).
        if nqp::isnull($state) && $kind != nqp::const::DISP_LASTCALL {
            # No state, so just starting the resumption. Guard on the
            # invocant type and name.
            my $init := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-init-args');
            my $track_start_type := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $init, 0);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track_start_type);
            my $track_name := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $init, 1);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_name);

            # Build up the list of methods to defer through.
            my $start_type := nqp::captureposarg($init, 0);
            my str $name := nqp::captureposarg_s($init, 1);
            my $start_type_how := nqp::how_nd($start_type);
            my @mro := nqp::can($start_type_how, 'mro_unhidden')
                ?? $start_type_how.mro_unhidden($start_type)
                !! nqp::can($start_type_how, 'mro')
                    ?? $start_type_how.mro($start_type)
                    !! [];
            my @methods;
            for @mro {
                my %mt := nqp::hllize(nqp::how_nd($_).method_table($_));
                if nqp::existskey(%mt, $name) {
                    @methods.push(%mt{$name});
                }
            }

            # Turn it into a linked list.
            my $chain := Exhausted;
            if nqp::elems(@methods) >= 2 {
                @methods.shift; # Discard the first one, which we initially called
                while @methods {
                    $chain := DeferralChain.new(@methods.pop, $chain);
                }
            }

            # Determine the args to pass to the method we defer to. If it's a
            # callwith, then the arguments are given to us here. If it's a
            # callwith propagration, we need to use those args also. Otherwise,
            # they are those from the initial capture.
            my $args_with_kind;
            if $kind == nqp::const::DISP_CALLWITH {
                # Rewrite the kind into callsame, since we've already accounted
                # for the callwith. We do need to insert the original invocant.
                my $args := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0);
                my $with_invocant := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    $args, 0,
                    nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $init, 2));
                $args_with_kind := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-int', $with_invocant, 0,
                    nqp::const::DISP_CALLSAME);
            }
            elsif $kind == nqp::const::DISP_PROPAGATE_CALLWITH {
                $args_with_kind := $capture;
            }
            else {
                my $args := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
                    $init, 0, 2);
                $args_with_kind := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    $args, 0, $track_kind);
            }

            # Prepend the chain of methods we dispatch through and defer.
            my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                $args_with_kind, 0, $chain);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-deferral',
                $delegate);
        }

        # Otherwise, we already set up - and presumably completed - walking
        # through the methods.
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    $capture, 0, Nil));
        }
    });

# Method deferral dispatcher.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-meth-deferral',
    # Entry to the deferral, with the chain of methods to walk through as the
    # first argument, the resumption kind as the second argument, and the
    # args to the method (including the invocant) coming next. We assume here
    # that we already established guards on kind and similar, and that if we
    # have a lastcall we'll have handled it rather than sending it here.
    -> $capture {
        # If the chain is Exhausted, then we will delegate to Nil or to
        # the propagate callwith terminal.
        my $chain := nqp::captureposarg($capture, 0);
        if nqp::eqaddr($chain, Exhausted) {
            nil-or-callwith-propagation-terminal(nqp::dispatch('boot-syscall',
                'dispatcher-drop-arg', $capture, 0));
        }

        # If we're propagating a callwith then we need to set the resume init args
        # and then invoke the thing that is being propagated.
        elsif nqp::captureposarg_i($capture, 1) == nqp::const::DISP_PROPAGATE_CALLWITH {
            my $resume-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
                $capture, 1, 4); # Drop kind, dispatcher name, invocant flag, invokee
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-init-args', $resume-capture);
            my str $dispatcher := nqp::captureposarg_s($capture, 2);
            my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
                $capture, 0, 4);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', $dispatcher, $delegate);
        }

        # Otherwise, need to do a dispatch step.
        else {
            # The resume init state for a multi-step deferral is the next
            # thing in the chain prepended to the dispatch arguments.
            my $args := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
                $capture, 0, 2);
            my $resume-capture := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $args, 0, $chain.next);
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-init-args', $resume-capture);

            # Now perform the action needed based upon the kind of resumption
            # we have.
            method-deferral-step($chain, nqp::captureposarg_i($capture, 1), $args);
        }
    },
    # Resumption, wherein we walk another step through the chain.
    -> $capture {
        # Guard on the kind of resume we're doing, and get that flag.
        my $track_kind := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_kind);
        my int $kind := nqp::captureposarg_i($capture, 0);

        # If we're doing a lastcall, set the state to exhausted and we're done.
        if $kind == nqp::const::DISP_LASTCALL {
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal', Exhausted);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    $capture, 0, Nil));
        }

        else {
            # If there's no dispatch state yet, we're on our first round of
            # resumption for this dispatcher. Otherwise, look to the state to
            # find the next method.
            my $init := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-init-args');
            my $state := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-state');
            my $track_state := nqp::dispatch('boot-syscall', 'dispatcher-track-resume-state');
            my $chain;
            my $track_chain;
            if nqp::isnull($state) {
                # Guard that the resume state is null, and then extract the chain
                # from the init state.
                nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_state);
                $chain := nqp::captureposarg($init, 0);
                $track_chain := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $init, 0);
            }
            else {
                # The chain is the state.
                $chain := $state;
                $track_chain := $track_state;
            }

            # If we're exhausted already, then produce Nil.
            if nqp::istype($chain, Exhausted) {
                nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_chain);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        $capture, 0, Nil));
            }

            # If we're propagating new callwith args then mark this dispatcher
            # exhausted and re-enter with the updated args and current chain.
            elsif $kind == nqp::const::DISP_PROPAGATE_CALLWITH {
                nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal', Exhausted);
                my $capture-delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    $capture, 0, $track_chain);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-deferral',
                    $capture-delegate);
            }

            else {
                # Otherwise, guard on the candidate that we shall be invoking.
                my $track_method := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $track_chain, DeferralChain, '$!code');
                nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_method);

                # Now perform the action needed based upon the kind of resumption
                # we have.
                my $args := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $init, 0);
                if $kind == nqp::const::DISP_CALLWITH {
                    # Set the state to exhausted as we're abandoning the walk through
                    # the methods with these args.
                    nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal',
                        Exhausted);

                    # Re-enter this dispatcher with the new args.
                    my $new_args := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                        $capture, 0);
                    my $with_invocant := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                        $new_args, 0,
                        nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $args, 0));
                    my $new_args_with_kind := nqp::dispatch('boot-syscall',
                        'dispatcher-insert-arg-literal-int', $with_invocant, 0,
                        nqp::const::DISP_CALLSAME);
                    my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                        $new_args_with_kind, 0, $track_chain);
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-deferral',
                        $delegate);
                }
                else {
                    # Update dispatch state to point to the next method.
                    my $track_next := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                        $track_chain, DeferralChain, '$!next');
                    nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state', $track_next);

                    # It's a normal step.
                    method-deferral-step($chain, $kind, $args);
                }
            }
        }
    });
sub method-deferral-step($chain-head, int $kind, $args) {
    # Look at the kind of deferral we have to decide what to do.
    my $next_method := $chain-head.code;
    if $kind == nqp::const::DISP_CALLSAME {
        # Call with same (that is, original) arguments. Invoke with those.
        # We drop the first two arguments (which are only there for the
        # resumption), add the code object to invoke, and then leave it
        # to the invoke dispatcher.
        my $delegate_capture := nqp::dispatch('boot-syscall',
            'dispatcher-insert-arg-literal-obj', $args, 0, $next_method);
        if nqp::istype($next_method, Routine) {
            if nqp::can($next_method, 'WRAPPERS') {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke-wrapped',
                    $delegate_capture);
            }
            elsif nqp::can($next_method, 'CALL-ME') {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke',
                    $delegate_capture);
            }
            elsif $next_method.is_dispatcher {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi',
                    $delegate_capture);
            }
            else {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke',
                    $delegate_capture);
            }
        }
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke',
                $delegate_capture);
        }
    }
    elsif $kind == nqp::const::DISP_NEXTCALLEE {
        # We just want method itself, not to invoke it.
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                $args, 0, $next_method));
    }
    else {
        nqp::die('Unexpected resumption kind in method dispatch');
    }
}

# Multi-dispatch dispatcher, used for both multi sub and multi method dispatch.
# Assumes that we have already guarded on a literal code object (methods) or
# ensured consistency of routine (subs where closure cloning may take place).
# This does not do the heart of the dispatch itself, but rather determines if
# we have a simple or complex proto, and thus whether we need to invoke the
# proto at all. In the case of a complex proto, we use dispatch resumption to
# continue with the dispatch.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-multi',
    # Initial dispatch, only setting up resumption if we need to invoke the
    # proto.
    -> $capture {
        my $callee := nqp::captureposarg($capture, 0);
        my int $onlystar := $callee.onlystar;
        my int $simple := $onlystar && simple-args-proto($callee, $capture);
        if $simple {
            # Don't need to invoke the proto itself, so just get on with the
            # candidate dispatch.
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi-core', $capture);
        }
        else {
            # Set resume init args and run the proto.
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-init-args', $capture);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke', $capture);
        }
    },
    # Resumption means that we have reached the {*} in the proto and so now
    # should go ahead and do the dispatch. Make sure we only do this if we
    # are signalled to that it's a resume for an onlystar.
    -> $capture {
        my $track_kind := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_kind);
        my int $kind := nqp::captureposarg_i($capture, 0);
        if $kind == nqp::const::DISP_ONLYSTAR {
            # Put a guard on the dispatchee list, as a given proto may be
            # cloned and used for multiple candidate lists.
            my $init_args := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-init-args');
            my $track_callee := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                $init_args, 0);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
                nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $track_callee, Routine, '@!dispatchees'));
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi-core',
                $init_args);
        }
        elsif !nqp::dispatch('boot-syscall', 'dispatcher-next-resumption', $capture) {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-resume-error',
                $capture);
        }
    });

# We in principle could always call the proto and have correct behavior. It is,
# however, nice when we can go straight to a candidate. To see if that's the
# case, we have to introspect the signature.
sub simple-args-proto($callee, $capture) {
    # If we're compiling the setting, all sorts of things we need to do the
    # analysis are missing, so don't even try. Nothing invoked in the
    # CORE.setting build today that has an onlystar proto actually needs
    # its proto invoking today, so we also regard them all as simple, to
    # avoid a bunch of throwaway compilations.
    return 1 if $*COMPILING_CORE_SETTING;

    # If it's out of range so far as arity goes, we'll call the proto to
    # produce an error.
    my $signature := $callee.signature;
    my int $got-args := nqp::captureposelems($capture) - 1; # First element is the callee
    return 0 if nqp::islt_i($got-args, $signature.arity) || nqp::isgt_n($got-args, $signature.count);

    # Otherwise, arity is alright. Look through the params.
    my int $accepts-any-named := 0;
    for $signature.params.FLATTENABLE_LIST -> $param {
        # If there's a constraint or unpack, need the proto to be run.
        return 0 unless nqp::isnull(nqp::getattr($param, Parameter, '@!post_constraints')) &&
            nqp::isnull(nqp::getattr($param, Parameter, '$!sub_signature'));

        # Otherwise, go by kind of parameter.
        if $param.capture {
            $accepts-any-named := 1;
        }
        elsif $param.slurpy {
            $accepts-any-named := 1 if $param.named;
        }
        elsif $param.named {
            # Can be a bit smarter by diffing what nameds we have or miss, but
            # for now conservatively run the proto.
            return 0;
        }
    }

    # If we get here, it's OK to elide running the proto so long as if we have
    # any named args, it accepts them.
    return nqp::capturehasnameds($capture) ?? $accepts-any-named !! 1;
}

# If we invoke a multi with an argument that is a Proxy (or some other non-Scalar
# container), we need to read the value(s) from the Proxy argument(s) and then go
# on with the dispatch. The ProxyReaderFactory produces code objects that do
# that. We key it on total positional arguments, whether there are nameds to
# strip, and then the indices of the arguments to strip.
class ProxyReaderFactory {
    has $!lock;
    has %!readers;

    method new() {
        self.bless: lock => NQPLock.new, readers => nqp::hash()
    }

    method reader-for($capture, $indices) {
        $!lock.protect: {
            # Form a key.
            my int $num-args := nqp::captureposelems($capture);
            my int $has-nameds := nqp::capturehasnameds($capture);
            my str $key := $num-args ~ ($has-nameds ?? '%' !! '|');
            my int $i;
            while $i < nqp::elems($indices) {
                $key := $key ~ nqp::atpos_i($indices, $i) ~ ',';
                $i++;
            }

            # If we don't already have a reader for this key, produce it.
            unless nqp::existskey(%!readers, $key) {
                %!readers{$key} := self.'!produce-reader'($num-args, $has-nameds, $indices);
            }

            %!readers{$key}
        }
    }

    method !produce-reader($num-args, $has-nameds, $indices) {
        # Create a block taking each positional arg required, adding a
        # slurpy named if needed.
        my $block := QAST::Block.new(:is_thunk);
        my int $i := 0;
        while $i < $num-args {
            $block.push(QAST::Var.new( :name("a$i"), :decl<param>, :scope<local> ));
            $i++;
        }
        if $has-nameds {
            $block.push(QAST::Var.new( :name<n>, :decl<param>, :scope<local>, :named, :slurpy ));
        }

        # Produce a dispatch op with the required arguments decontainerized.
        my $dispatch := QAST::Op.new:
            :op('dispatch'),
            QAST::SVal.new( :value('boot-resume') ),
            QAST::IVal.new( :value(nqp::const::DISP_DECONT) );
        $i := 0;
        my $decont-index := 0;
        while $i < $num-args {
            my $var := QAST::Var.new( :name("a$i"), :scope<local> );
            if nqp::atpos_i($indices, $decont-index) == $i {
                $dispatch.push(QAST::Op.new( :op<decont>, $var ));
                $decont-index++;
            }
            else {
                $dispatch.push($var);
            }
            $i++;
        }
        if $has-nameds {
            $dispatch.push(QAST::Var.new( :name<n>, :scope<local>, :named, :flat ));
        }
        $block.push($dispatch);

        # Compile and return it.
        nqp::getcomp('Raku').compile($block, :from<optimize>)
    }
}
my $PROXY-READERS := ProxyReaderFactory.new;
nqp::bindhllsym('Raku', 'PROXY-READERS', $PROXY-READERS);

# The core of multi dispatch. Once we are here, either there was a simple
# proto that we don't need to inovke, or we already did invoke the proto.
#
# Multiple dispatch is relatively complex in the most general case. However,
# the most common case by far consists of:
#
# * Arguments are not in any containers or in Scalar containers, which we can
#   dereference as part of the dispatch program
# * A candidate that is identified by arity and nominal type, and thus can be
#   identified just using the callsite and guards on argument types
# * No resumption via callsame and friends
#
# This is the case we want to optimize for, and is called a "trivial" multiple
# dispatch. All other cases are "non-trivial", including the case where a
# trivial multiple dispatch is resumed. In a trivial multiple dispatch, we
# save the incoming argument tuple as dispatch state, establish guards, and
# then delegate to raku-invoke to invoke the chose candidate.
#
# The case with non-Scalar containers is handled by first extracting those and
# then proceeding with the dispatch.
#
# The non-trivial multiple dispatch establishes the guards that a trivial
# multiple dispatch does, and then works through the dispatch plan. This is a
# linked list featuring candidates to try calling, terminated either with a
# fallback to the next resumable dispatcher (if we're in a resume) or to the
# production of an error (ambiguous dispatch or no applicable candidate).
#
# The plan nodes are as follows:
# * A candidate to call; we don't expect any bind failure (that is, we
#   consider it safe from nominality alone).
my class MultiDispatchCall {
    has $!candidate;
    has $!next;
    method new($candidate) {
        my $obj := nqp::create(self);
        nqp::bindattr($obj, MultiDispatchCall, '$!candidate', $candidate);
        $obj
    }
    method set-next($next) {
        $!next := $next;
    }
    method candidate() { $!candidate }
    method next() { $!next }
    method debug() {
        "Candidate " ~ $!candidate.signature.raku ~ "\n" ~ $!next.debug
    }
}
# * A candidate to try and invoke; if there's a bind failure, it will be
#   mapped into a resumption
my class MultiDispatchTry is MultiDispatchCall {
    method debug() {
        "Try candidate " ~ self.candidate.signature.raku ~ "\n" ~ self.next.debug
    }
}
# * An ambiguity. It's only an error if it's reached during the initial
#   phase of dispatch, not during a callsame-alike, thus why the chain
#   continues beyond this point.
my class MultiDispatchAmbiguous {
    has $!next;
    method set-next($next) {
        $!next := $next;
    }
    method next() { $!next }
    method debug() {
        "Ambiguous\n" ~ $!next.debug
    }
}
# * The end of the candidates. Either a "no applicable candidates" error
#   if we are in the initial phase of dispatch, or a next resumption (or
#   Nil) otherwise.
my class MultiDispatchEnd {
    method debug() {
        "End"
    }
}
# * Not actually used in a plan, but instead conveys that we have containers
#   that need complex (running code) removal. This is created with the indices
#   of the arguments that need it removing.
my class MultiDispatchNonScalar {
    has $!args;
    method new($args) {
        my $obj := nqp::create(self);
        nqp::bindattr($obj, MultiDispatchNonScalar, '$!args', $args);
        $obj
    }
    method args() { $!args }
    method debug() {
        "Non-Scalar container dispatch"
    }
}
# Apart from the last one, these are used as the dispatch state; this is the
# same immutable linked list traversal approach as used in other kinds of
# resumption.
#
# The plan is calculated by the following code. Note that the trivial case
# also uses this plan calculation routine, but in a "stop on trivial success"
# mode. A resumption of a trivial dispatch will call this again, but with that
# flag not set, and then drop the first candidate from the plan, which was
# already invoked. It will then walk the candidate list as usual.
my int $DEFCON_DEFINED    := 1;
my int $DEFCON_UNDEFINED  := 2;
my int $DEFCON_MASK       := $DEFCON_DEFINED +| $DEFCON_UNDEFINED;
my int $TYPE_NATIVE_INT   := 4;
my int $TYPE_NATIVE_UINT  := 32;
my int $TYPE_NATIVE_NUM   := 8;
my int $TYPE_NATIVE_STR   := 16;
my int $TYPE_NATIVE_MASK  := $TYPE_NATIVE_INT +| $TYPE_NATIVE_UINT +| $TYPE_NATIVE_NUM +| $TYPE_NATIVE_STR;
my int $BIND_VAL_OBJ      := 0;
my int $BIND_VAL_INT      := 1;
my int $BIND_VAL_UINT     := 10;
my int $BIND_VAL_NUM      := 2;
my int $BIND_VAL_STR      := 3;
sub has-named-args-mismatch($capture, %info) {
    # First consider required nameds.
    my $required-name-sets := %info<required_names>;
    my $nameds-list := nqp::dispatch('boot-syscall', 'capture-names-list', $capture);
    if $required-name-sets {
        # Quick exit if we required names, but have none.
        return 1 unless $nameds-list;

        # Otherwise check that required nameds are present.
        my int $i := 0;
        my int $n := nqp::elems($required-name-sets);
        while $i < $n {
            my int $found := 0;
            my $names := nqp::atpos($required-name-sets, $i);
            my int $j := 0;
            my int $m := nqp::elems($names);
            while $j < $m {
                if nqp::captureexistsnamed($capture, nqp::atpos_s($names, $j)) {
                    $found := 1;
                    last;
                }
                $j++;
            }
            return 1 unless $found;
            $i++;
        }
    }

    # If we don't accept all nameds, then check there are acceptable nameds.
    if $nameds-list && !%info<allows_all_names> {
        # Quick exit if there are no allowed nameds.
        my $allowed-names := %info<allowed_names>;
        return 1 unless $allowed-names;

        # Go through the nameds and check they are allowed.
        my int $i;
        my int $n := nqp::elems($nameds-list);
        while $i < $n {
            return 1 unless nqp::existskey($allowed-names, nqp::atpos_s($nameds-list, $i));
            $i++;
        }
    }

    # Otherwise, no mismatch.
    0
}
sub raku-multi-plan(@candidates, $capture, int $stop-at-trivial, $orig-capture = $capture) {
    # First check there's no non-Scalar containers in the positional arguments.
    # If there are, establish guards relating to those and we're done. Native
    # references don't count; we know the native types they shall match up with
    # and don't need to dereference them.
    my int $num_args := nqp::captureposelems($capture);
    my int $i;
    my $non-scalar := nqp::list_i();
    while $i < $num_args {
        my int $got_prim := nqp::captureposprimspec($capture, $i);
        if $got_prim == 0 {
            my $value := nqp::captureposarg($capture, $i);
            if nqp::isconcrete_nd($value) &&
                nqp::iscont($value) && !nqp::istype_nd($value, Scalar) &&
                !(nqp::iscont_i($value) || nqp::iscont_u($value) || nqp::iscont_n($value) || nqp::iscont_s($value)) {
                nqp::push_i($non-scalar, $i);
            }
        }
        $i++;
    }
    if nqp::elems($non-scalar) {
        # Establish guards on types of all positionals, but not on the values
        # inside of them if they are Scalar containers; we just need to make
        # sure we have the appropriate tuple of Proxy vs non-Proxy for the
        # Proxy removal code we'll invoke.
        my int $i;
        while $i < $num_args {
            if nqp::captureposprimspec($capture, $i) == 0 {
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
                    nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, $i));
            }
            $i++;
        }

        # Hand back the indices we need to strip Proxy from.
        return MultiDispatchNonScalar.new($non-scalar);
    }

    # We keep track of the head of the plan as well as the tail node of it,
    # so we know where to add the next step.
    my $current-head := nqp::null();
    my $current-tail := nqp::null();

    # Look through all candidates. Eliminate those that can be ruled out by
    # setting guards on the incoming arguments OR by the shape of the
    # callsite. That callsite shape includes argument count, which named
    # arguments are present, and which arguments are natively typed.
    my int $cur_idx := 0;
    my int $done := 0;
    my $need_scalar_read := nqp::list_i();
    my $need_scalar_rw_check := nqp::list_i();
    my $need_type_guard := nqp::list_i();
    my $need_conc_guard := nqp::list_i();
    my @possibles;
    my $Positional := nqp::gethllsym('Raku', 'MD_Pos');
    until $done {
        # The candidate list is broken into tied groups (that is, groups of
        # candidates that are equally narrow). Those are seperated by a
        # type object sentinel.
        my $cur_candidate := nqp::atpos(@candidates, $cur_idx++);
        if nqp::isconcrete($cur_candidate) {
            # Candidate; does the arity fit? (If not, it drops out on callsite
            # shape.)
            if $num_args >= nqp::atkey($cur_candidate, 'min_arity') &&
                    $num_args <= nqp::atkey($cur_candidate, 'max_arity') {
                # Arity OK; now go through the arguments and see if we can
                # eliminate any of them based on guardable properties.
                my int $type_check_count := nqp::atkey($cur_candidate, 'num_types') > $num_args
                    ?? $num_args
                    !! nqp::atkey($cur_candidate, 'num_types');
                my int $type_mismatch := 0;
                my int $rwness_mismatch := 0;
                my int $i := 0;
                while $i < $type_check_count && !($type_mismatch +| $rwness_mismatch) {
                    # Obtain parameter properties.
                    my $type := nqp::atpos(nqp::atkey($cur_candidate, 'types'), $i);
                    my int $is-mu := nqp::eqaddr($type, Mu);
                    my int $type_flags := nqp::atpos_i(nqp::atkey($cur_candidate, 'type_flags'), $i);
                    my int $definedness := $type_flags +& $DEFCON_MASK;
                    my int $rwness := nqp::atpos_i(nqp::atkey($cur_candidate, 'rwness'), $i);

                    # Get the primitive type of the argument, and go on whether it's an
                    # object or primitive type.
                    my int $got_prim := nqp::captureposprimspec($capture, $i);
                    my int $want_prim := $type_flags +& $TYPE_NATIVE_MASK;
                    if $got_prim == 0 && $want_prim == 0 {
                        # For a type that's exactly Mu we do not need a type
                        # guard, however if it's got a definedness constraint
                        # we do, since we might then wrongly accept a Scalar
                        # container that meets the definedness property.
                        if $definedness || !$is-mu {
                            nqp::bindpos_i($need_type_guard, $i, 1);
                        }

                        # It's an object type. Obtain the value, and go by if it's a
                        # container or not.
                        my $value := nqp::captureposarg($capture, $i);
                        my int $promoted_primitive;
                        if nqp::iscont($value) && nqp::isconcrete_nd($value) {
                            # Containerized. Scalar we handle specially.
                            if nqp::istype_nd($value, Scalar) {
                                nqp::bindpos_i($need_scalar_read, $i, 1);
                                if $rwness {
                                    nqp::bindpos_i($need_scalar_rw_check, $i, 1);
                                    my $desc := nqp::getattr($value, Scalar, '$!descriptor');
                                    unless nqp::isconcrete($desc) {
                                        $rwness_mismatch := 1;
                                    }
                                }
                                $value := nqp::getattr($value, Scalar, '$!value');
                            }
                            # Otherwise, it should be a native reference. We'll
                            # promote these to their boxed type.
                            elsif nqp::iscont_i($value) {
                                $value := Int;
                                $promoted_primitive := 1;
                            }
                            elsif nqp::iscont_u($value) {
                                $value := Int;
                                $promoted_primitive := 1;
                            }
                            elsif nqp::iscont_n($value) {
                                $value := Num;
                                $promoted_primitive := 1;
                            }
                            elsif nqp::iscont_s($value) {
                                $value := Str;
                                $promoted_primitive := 1;
                            }
                            else {
                                nqp::die('Unknown kind of l-value in multiple dispatch');
                            }
                        }
                        else {
                            # If we need an rw argument and didn't get a container,
                            # we're out of luck. Before asserting this, we should make
                            # sure that the original container (which maybe was a
                            # Proxy that was removed in the args we're doing the
                            # dispatch over) was not itself rw.
                            if $rwness && !nqp::iscont(nqp::captureposarg($orig-capture, $i)) {
                                $rwness_mismatch := 1;
                            }
                        }

                        # Ensure the value meets the required type constraints.
                        unless $is-mu ||
                                nqp::istype_nd(nqp::hllizefor($value, 'Raku'), $type) {
                            if $type =:= $Positional {
                                # Things like Seq can bind to an @ sigil.
                                my $PositionalBindFailover := nqp::gethllsym('Raku', 'MD_PBF');
                                unless nqp::istype_nd($value, $PositionalBindFailover) {
                                    $type_mismatch := 1;
                                }
                            } else {
                                $type_mismatch := 1;
                            }
                        }

                        # Also ensure any concreteness constraints are unheld.
                        if !$type_mismatch && $definedness {
                            my int $got := $promoted_primitive || nqp::isconcrete_nd($value);
                            if ($got && $definedness == $DEFCON_UNDEFINED) ||
                                    (!$got && $definedness == $DEFCON_DEFINED) {
                                $type_mismatch := 1;
                            }
                            nqp::bindpos_i($need_conc_guard, $i, 1);
                        }
                    }
                    elsif $got_prim == 0 { # and $want_prim != 0 per last condition
                        # Make sure it's the expected kind of native container.
                        nqp::bindpos_i($need_type_guard, $i, 1);
                        my $contish := nqp::captureposarg($capture, $i);
                        unless (($type_flags +& $TYPE_NATIVE_INT) && nqp::iscont_i($contish)) ||
                               (($type_flags +& $TYPE_NATIVE_UINT) && nqp::iscont_u($contish)) ||
                               (($type_flags +& $TYPE_NATIVE_NUM) && nqp::iscont_n($contish)) ||
                               (($type_flags +& $TYPE_NATIVE_STR) && nqp::iscont_s($contish)) {
                            $type_mismatch := 1;
                        }
                    }
                    else {
                        # It's a primitive type. If we want rw, then we ain't got it.
                        if $rwness {
                            $rwness_mismatch := 1;
                        }
                        # If we want a type object, it's certainly not that either.
                        elsif $definedness == $DEFCON_UNDEFINED {
                            $type_mismatch := 1;
                        }
                        # If we want a primitive type, but got the wrong one, then it's
                        # a mismatch.
                        elsif $want_prim {
                            if (($type_flags +& $TYPE_NATIVE_INT) && $got_prim != $BIND_VAL_INT) ||
                                    (($type_flags +& $TYPE_NATIVE_UINT) && $got_prim != $BIND_VAL_UINT) ||
                                    (($type_flags +& $TYPE_NATIVE_NUM) && $got_prim != $BIND_VAL_NUM) ||
                                    (($type_flags +& $TYPE_NATIVE_STR) && $got_prim != $BIND_VAL_STR) {
                                $type_mismatch := 1;
                            }
                        }
                        # Otherwise, we want an object type. Figure out the correct
                        # one that we shall box to.
                        else {
                            my $test_type := $got_prim == $BIND_VAL_INT  ?? Int !!
                                             $got_prim == $BIND_VAL_UINT ?? Int !!
                                             $got_prim == $BIND_VAL_NUM  ?? Num !!
                                                                            Str;
                            $type_mismatch := 1 unless nqp::istype($test_type, $type);
                        }
                    }
                    $i++;
                }

                # Add it to the possibles list of this group.
                unless $type_mismatch || $rwness_mismatch {
                    nqp::push(@possibles, $cur_candidate);
                }
            }
        }
        else {
            # End of tied group. If there's possibles...
            if nqp::elems(@possibles) {
                # Build a new list of filtered possibles by ruling out any
                # that have unaccepted of missing nameds. Track if we need bind
                # checks or if we have declarative candidates. Also check for
                # defaults and exact arity matches, which we can use for
                # tie-breaking if there are ambiguities.
                my int $i;
                my int $n := nqp::elems(@possibles);
                my int $need-bind-check;
                my int $first-group := nqp::isnull($current-head);
                my @filtered-possibles;
                my @defaults;
                my @exact-arity;
                while $i < $n {
                    my %info := @possibles[$i];
                    unless has-named-args-mismatch($capture, %info) {
                        nqp::push(@filtered-possibles, %info);
                        $need-bind-check++ if nqp::existskey(%info, 'bind_check');
                        my $sub := %info<sub>;
                        nqp::push(@defaults, %info) if nqp::can($sub, 'default') && $sub.default;
                        nqp::push(@exact-arity, %info) if %info<min_arity> == $num_args &&
                           %info<max_arity> == $num_args;
                    }
                    $i++;
                }

                # If we still have multiple possibles and we don't need a bind
                # check, try tie-breakers, and failing that add an ambiguity
                # marker.
                if !$need-bind-check && nqp::elems(@filtered-possibles) > 1 {
                    if nqp::elems(@defaults) == 1 {
                        @filtered-possibles := @defaults;
                    }
                    elsif nqp::elems(@exact-arity) == 1 {
                        @filtered-possibles := @exact-arity;
                    }
                    else {
                        my $node := MultiDispatchAmbiguous.new();
                        if nqp::isnull($current-head) {
                            $current-head := $node;
                        }
                        else {
                            $current-tail.set-next($node);
                        }
                        $current-tail := $node;
                    }
                }

                # Add the filtered possibles to the plan.
                $i := 0;
                $n := nqp::elems(@filtered-possibles);
                while $i < $n {
                    my %info := @filtered-possibles[$i];
                    my $node;
                    if $need-bind-check {
                        # Ensure it's already compiled, otherwise we can have
                        # compiler frames obscuring the bind control record we
                        # use for trying the next candidate.
                        my $sub := %info<sub>;
                        my $cs := nqp::getattr($sub, Code, '@!compstuff');
                        unless nqp::isnull($cs) {
                            my $ctf := $cs[1];
                            $ctf() if $ctf;
                        }
                        $node := MultiDispatchTry.new($sub);
                    }
                    else {
                        $node := MultiDispatchCall.new(%info<sub>);
                    }
                    if nqp::isnull($current-head) {
                        $current-head := $node;
                    }
                    else {
                        $current-tail.set-next($node);
                    }
                    $current-tail := $node;
                    $i++;
                }

                # If we are to stop at a trivial match and nothing needs a
                # bind check, and we've no results before now, we're done.
                if $stop-at-trivial && $first-group &&
                        nqp::elems(@filtered-possibles) == 1 && $need-bind-check == 0 {
                    $done := 1;
                }

                # Otherwise, clear the set of possibles for the next group.
                else {
                    nqp::setelems(@possibles, 0);
                }
            }

            # If we're really at the end of the list, we're done.
            unless nqp::isconcrete(nqp::atpos(@candidates, $cur_idx)) {
                $done := 1;
            }
        }
    }

    # Add an end node.
    if nqp::isnull($current-head) {
        $current-head := MultiDispatchEnd;
    }
    else {
        $current-tail.set-next(MultiDispatchEnd);
    }

    # Install guards as required.
    $i := 0;
    while $i < $num_args {
        my $tracked_value;
        if nqp::atpos_i($need_scalar_read, $i) {
            my $tracked := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, $i);
            if nqp::atpos_i($need_scalar_rw_check, $i) {
                my $tracked_desc := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                        $tracked, Scalar, '$!descriptor');
                nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $tracked_desc);
            }
            $tracked_value := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $tracked, Scalar, '$!value');
        }
        else {
            $tracked_value := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                    $capture, $i);
        }
        if nqp::atpos_i($need_type_guard, $i) {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked_value);
        }
        if nqp::atpos_i($need_conc_guard, $i) {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $tracked_value);
        }
        $i++;
    }

    # Return the dispatch plan.
    $current-head
}
sub form-raku-capture($vm-capture) {
    my $raku-capture := nqp::create(Capture);
    nqp::bindattr($raku-capture, Capture, '@!list',
        nqp::dispatch('boot-syscall', 'capture-pos-args', $vm-capture));
    nqp::bindattr($raku-capture, Capture, '%!hash',
        nqp::dispatch('boot-syscall', 'capture-named-args', $vm-capture));
    $raku-capture
}
sub multi-junction-failover($capture) {
    # Take a first pass to see if there's a Junction arg (we look at both
    # named and positional).
    my int $num-args := nqp::dispatch('boot-syscall', 'capture-num-args', $capture);
    my int $i;
    my $found-junction;
    while $i < $num-args {
        my int $got-prim := nqp::dispatch('boot-syscall', 'capture-arg-prim-spec', $capture, $i);
        if $got-prim == 0 {
            my $value := nqp::dispatch('boot-syscall', 'capture-arg-value', $capture, $i);
            if nqp::isconcrete($value) && nqp::istype($value, Junction) {
                $found-junction := 1;
                last;
            }
        }
        $i++;
    }

    # If there is a Junction arg, then take another pass through to put type
    # guards on all argument types.
    if $found-junction {
        $i := 0;
        while $i < $num-args {
            if nqp::dispatch('boot-syscall', 'capture-arg-prim-spec', $capture, $i) == 0 {
                my $arg := nqp::dispatch('boot-syscall', 'capture-arg-value', $capture, $i);
                my $tracked := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                    $capture, $i);
                if nqp::istype_nd($arg, Scalar) {
                    $tracked := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                        $tracked, Scalar, '$!value');
                }
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $tracked);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $tracked);
            }
            $i++;
        }
    }

    $found-junction
}
sub multi-no-match-handler($target, $dispatch-arg-capture, $orig-capture, $orig-arg-capture) {
    # If no candidates are found but there is a Junction argument, we'll
    # dispatch to that.
    if multi-junction-failover($dispatch-arg-capture) { # Guards added here
        my $with-invocant := nqp::dispatch('boot-syscall',
            'dispatcher-insert-arg-literal-obj', $orig-capture, 0, Junction);
        my $threader := Junction.HOW.find_method(Junction, 'AUTOTHREAD') //
            nqp::die('Junction auto-thread method not found');
        my $capture-delegate := nqp::dispatch('boot-syscall',
            'dispatcher-insert-arg-literal-obj', $with-invocant, 0, $threader);
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke',
            $capture-delegate);
    }
    # Otherwise, it's just an error.
    else {
        Perl6::Metamodel::Configuration.throw_or_die(
            'X::Multi::NoMatch',
            "Cannot call " ~ $target.name() ~ "; no signatures match",
            :dispatcher($target), :capture(form-raku-capture($orig-arg-capture)));
    }
}
sub multi-ambiguous-handler($dispatch-plan, $target, $arg-capture) {
    my @ambiguous;
    my $ambig-call := $dispatch-plan.next;
    while nqp::istype($ambig-call, MultiDispatchCall) {
        nqp::push(@ambiguous, $ambig-call.candidate);
        $ambig-call := $ambig-call.next;
    }
    Perl6::Metamodel::Configuration.throw_or_die(
        'X::Multi::Ambiguous',
        "Ambiguous call to " ~ $target.name(),
        :dispatcher($target), :@ambiguous,
        :capture(form-raku-capture($arg-capture)));
}
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-multi-core',
    # Initial dispatch. Tries to find an initial candidate.
    -> $capture {
        # Obtain the candidate list, producing it if it doesn't already exist.
        my $target := nqp::captureposarg($capture, 0);
        my @candidates := nqp::getattr($target, Routine, '@!dispatch_order');
        if nqp::isnull(@candidates) {
            nqp::scwbdisable();
            @candidates := $target.'!sort_dispatchees_internal'();
            nqp::bindattr($target, Routine, '@!dispatch_order', @candidates);
            nqp::scwbenable();
        }

        # Drop the first argument, to get just the arguments to dispatch on, and
        # then produce a multi-dispatch plan. Decide what to do based upon it.
        my $arg-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0);
        my $dispatch-plan := raku-multi-plan(@candidates, $arg-capture, 1);
        if nqp::istype($dispatch-plan, MultiDispatchCall) &&
                !nqp::istype($dispatch-plan, MultiDispatchTry) &&
                nqp::istype($dispatch-plan.next, MultiDispatchEnd) {
            # Trivial multi dispatch. Set dispatch state for resumption, and
            # then delegate to raku-invoke to run it.
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-init-args', $capture);
            my $candidate := $dispatch-plan.candidate;
            my $capture-delegate := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $arg-capture, 0,
                $candidate);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate',
                nqp::can($candidate, 'WRAPPERS') ?? 'raku-invoke-wrapped' !! 'raku-invoke',
                $capture-delegate);
        }
        elsif nqp::istype($dispatch-plan, MultiDispatchNonScalar) {
            # Need to strip the Proxy arguments and then try again. Produce a
            # proxy reader code object to do so, insert it as the first arg,
            # and delegate to a dispatcher to manage reading the args and
            # then retrying with the outcome.
            my $reader := $PROXY-READERS.reader-for($arg-capture, $dispatch-plan.args);
            my $capture-delegate := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $capture, 0, $reader);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi-remove-proxies',
                $capture-delegate);
        }
        elsif nqp::istype($dispatch-plan, MultiDispatchAmbiguous) &&
                nqp::istype($dispatch-plan.next, MultiDispatchCall) {
            multi-ambiguous-handler($dispatch-plan, $target, $arg-capture);
        }
        elsif nqp::istype($dispatch-plan, MultiDispatchEnd) {
            multi-no-match-handler($target, $arg-capture, $capture, $arg-capture);
        }
        else {
            # It's a non-trivial multi dispatch. Prefix the capture with
            # the dispatch plan, and also a DISP_NONE to indicate this
            # is not a resumption of any kind. Then delegate to the
            # non-trivial multi dispatcher.
            my $capture-with-plan := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $capture, 0,
                $dispatch-plan);
            my $capture-delegate := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-int', $capture-with-plan, 0,
                nqp::const::DISP_NONE);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi-non-trivial',
                $capture-delegate);
        }
    },
    # Resume of a trivial dispatch.
    -> $capture {
        # Obtain and guard on the kind of resume.
        my $kind := nqp::captureposarg_i($capture, 0);
        my $track-kind := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-kind);

        # We'll delegate the hard work to the non-trivial dispatcher. We
        # only want to do that once, however, and so set the dispatch state
        # to exhausted if we've already done it. Check that's not so. Also
        # shortcut here if it's lastcall.
        my $track-state := nqp::dispatch('boot-syscall', 'dispatcher-track-resume-state');
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-state);
        my $state := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-state');
        if nqp::isnull($state) && $kind != nqp::const::DISP_LASTCALL {
            # First time. Set state to exhausted.
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal', Exhausted);

            # Obtain resume initialization arguments and form the plan.
            my $init := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-init-args');
            my $target := nqp::captureposarg($init, 0);
            my @candidates := nqp::getattr($target, Routine, '@!dispatch_order');
            my $arg-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $init, 0);
            my $dispatch-plan := raku-multi-plan(@candidates, $arg-capture, 0);

            # Put a guard on the dispatchees.
            my $track-target := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                $init, 0);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
                nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $track-target, Routine, '@!dispatchees'));

            # We already called the first candidate in the trivial plan, so
            # drop it.
            $dispatch-plan := $dispatch-plan.next;

            # Now go by the kind of resumption we have.
            if $kind == nqp::const::DISP_CALLWITH {
                # It's a callwith, so we'll use the provided args (except
                # for adding the invocant if it's a multi method), and then
                # delegate to the non-trivial dispatch handler.
                my $args := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0);
                if nqp::istype($target, Method) {
                    my $track-invocant := nqp::dispatch('boot-syscall',
                        'dispatcher-track-arg', $arg-capture, 0);
                    $args := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                        $args, 0, $track-invocant);
                }
                my $with-target := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    $args, 0, $track-target);
                my $capture-with-plan := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $with-target, 0,
                    $dispatch-plan);
                my $capture-delegate := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-int', $capture-with-plan,
                    0, nqp::const::DISP_CALLWITH);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi-non-trivial',
                    $capture-delegate);
            }
            elsif $kind == nqp::const::DISP_PROPAGATE_CALLWITH {
                # We're propagating a callwith. Insert our multi dispatch plan
                # and target after the kind and then pass it along to the
                # non-trivial dispatcher.
                my $capture-with-target := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    $capture, 1, $track-target);
                my $capture-with-plan := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $capture-with-target, 1,
                    $dispatch-plan);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi-non-trivial',
                    $capture-with-plan);
            }
            else {
                # Delegate to the non-trivial dispatcher, passing along the kind
                # of dispatch we're doing and the plan.
                my $capture-with-plan := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $init, 0,
                    $dispatch-plan);
                my $capture-delegate := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg', $capture-with-plan, 0, $track-kind);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi-non-trivial',
                    $capture-delegate);
            }
        }
        else {
            # Mark dispatcher exhausted if it's lastcall.
            if $kind == nqp::const::DISP_LASTCALL {
                nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal', Exhausted);
            }

            # Resume next dispatcher, if any, otherwise hand back Nil.
            nil-or-callwith-propagation-terminal($capture);
        }
    });

# The non-trivial multi dispatch has quite similar initial and resume steps,
# and thus the majority of the work is factored out into a subroutine.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-multi-non-trivial',
    # Initialization of a non-trivial dispatch. Receives a dispatch resumption
    # kind, which is zero if we're not resuming.
    -> $capture {
        # Extract and guard on the kind (first argument).
        my $track-kind := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-kind);
        my int $kind := nqp::captureposarg_i($capture, 0);

        # If it's propagating callwith arguments, then we don't want to invoke
        # any multi candidate, but rather set up resume init args and then run
        # the thing propagating args to us.
        if $kind == nqp::const::DISP_PROPAGATE_CALLWITH {
            # The resume init capture should have plan, original target, args.
            # We have kind, plan, original target, 3 values relating to the
            # callwith propagation, and then the args.
            my $without-prop-info := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
                $capture, 3, 3);
            my $init-args := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                $without-prop-info, 0);
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-init-args', $init-args);

            # Drop the plan and original target to get the callwith envelope,
            # and then propagate further or invoke.
            my $callwith-prop := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
                $capture, 1, 2);
            nil-or-callwith-propagation-terminal($callwith-prop);
        }

        # Otherwise, we probably want to run a candidate.
        else {
            # Extract and track the current state.
            my $track-cur-state := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                $capture, 1);
            my $cur-state := nqp::captureposarg($capture, 1);

            # Drop the leading two arguments to get the argument capture prefixed
            # with the original dispatch target, and one more to get the argument
            # capture.
            my $orig-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args',
                $capture, 0, 2);
            my $arg-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                $orig-capture, 0);

            # Perform the step.
            raku-multi-non-trivial-step($kind, $track-cur-state, $cur-state, $orig-capture,
                $arg-capture, 0);
        }
    },
    -> $capture {
        # Extract and guard on the kind (first argument).
        my $track-kind := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-kind);
        my int $kind := nqp::captureposarg_i($capture, 0);

        # Obtain and track state.
        my $track-state := nqp::dispatch('boot-syscall', 'dispatcher-track-resume-state');
        my $state := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-state');

        # If it's a lastcall, we'll poke an Exhausted into the state,
        # propagate it, and produce Nil.
        if $kind == nqp::const::DISP_LASTCALL {
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal', Exhausted);
            if !nqp::dispatch('boot-syscall', 'dispatcher-next-resumption') {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        $capture, 0, Nil));
            }
        }

        # If we're already exhausted, try for the next dispatcher.
        elsif nqp::istype($state, Exhausted) {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-state);
            nil-or-callwith-propagation-terminal($capture);
        }

        # If it's a callwith, then we want to re-enter the non-trivial
        # multi dispatcher with the current position in the dispatch
        # and the new args.
        elsif $kind == nqp::const::DISP_CALLWITH {
            my $args := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0);
            my $init := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-init-args');
            my $target := nqp::captureposarg($init, 1);
            if nqp::istype($target, Method) {
                my $track-invocant := nqp::dispatch('boot-syscall',
                    'dispatcher-track-arg', $init, 2);
                $args := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    $args, 0, $track-invocant);
            }
            my $track-cur-state;
            if nqp::isnull($state) {
                nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-state);
                $track-cur-state := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $init, 0);
            }
            else {
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-state);
                $track-cur-state := $track-state;
            }
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal', Exhausted);
            my $track-target := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                $init, 1);
            my $with-target := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                $args, 0, $track-target);
            my $capture-with-plan := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                $with-target, 0, $track-cur-state);
            my $capture-delegate := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-int', $capture-with-plan,
                0, nqp::const::DISP_CALLWITH);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi-non-trivial',
                $capture-delegate);
        }

        # Otherwise, it's a "normal" step through the candidate list.
        else {
            # Drop the leading argument to get the argument capture prefixed
            # with the original target, one more to get the arguments.
            my $init := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-init-args');
            my $orig-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                $init, 0);
            my $arg-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                $orig-capture, 0);

            # Have dispatch state already, or first resume?
            if nqp::isnull($state) {
                # First resumption. Guard that it is so.
                nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-state);

                # Obtain plan and args from init state.
                my $track-cur-state := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                    $init, 0);
                my $cur-state := nqp::captureposarg($init, 0);
                raku-multi-non-trivial-step($kind, $track-cur-state, $cur-state, $orig-capture,
                    $arg-capture, 1);
            }
            else {
                raku-multi-non-trivial-step($kind, $track-state, $state, $orig-capture,
                    $arg-capture, 1);
            }
        }
    });
sub raku-multi-non-trivial-step(int $kind, $track-cur-state, $cur-state, $orig-capture,
        $arg-capture, $is-resume) {
    if nqp::istype($cur-state, MultiDispatchCall) {
        # Guard on the current state and on the callee (the type guards are
        # implicitly established when we guard the callee).
        my $track-candidate := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
            $track-cur-state, MultiDispatchCall, '$!candidate');
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-candidate);

        # If it's a bind failure or success, the we were doing nextcallee
        # on a dispatch needing a bind check, and want to hand back the
        # candidate or keep walking.
        if $kind == nqp::const::DISP_BIND_SUCCESS {
            # Successful. Peel off the candidate and hand it back.
            peel-off-candidate($is-resume, $track-cur-state, $cur-state, $orig-capture);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    $arg-capture, 0, $track-candidate));
            return;
        }
        elsif $kind == nqp::const::DISP_BIND_FAILURE {
            # Unsuccessful. Skip over the candidate we already tried, and then
            # try again.
            $track-cur-state := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                $track-cur-state, MultiDispatchCall, '$!next');
            $cur-state := $cur-state.next;
            return raku-multi-non-trivial-step(nqp::const::DISP_NEXTCALLEE,
                $track-cur-state, $cur-state, $orig-capture, $arg-capture, $is-resume);
        }

        # If it needs a bind check, then...
        my int $peel-off-candidate := 1;
        my int $try := nqp::istype($cur-state, MultiDispatchTry);
        if $try {
            # If it's nextcallee, we need to resume either on success or failure,
            # and hand back the candidate. This is the only case where we want to
            # not peel off a candidate now.
            if $kind == nqp::const::DISP_NEXTCALLEE {
                my int $failure := nqp::const::DISP_BIND_FAILURE;
                my int $success := nqp::const::DISP_BIND_SUCCESS;
                nqp::dispatch('boot-syscall', 'dispatcher-resume-after-bind',
                    $failure, $success);
                $peel-off-candidate := 0;
            }
            # Otherwise, we just want to call it and resume on bind failure.
            else {
                nqp::dispatch('boot-syscall', 'dispatcher-resume-on-bind-failure', $kind);
            }
        }

        # Peel off one candidate and use that as the next state, unless we
        # aren't meant to.
        if $peel-off-candidate {
            peel-off-candidate($is-resume, $track-cur-state, $cur-state, $orig-capture);
        }

        # Set up the call. If we are doing a callwith then we need to propagate
        # the args also, otherwise we just invoke it right off.
        my $capture-delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
            $arg-capture, 0, $track-candidate);
        my $candidate := $cur-state.candidate;
        my str $disp := $try || $kind != nqp::const::DISP_NEXTCALLEE
            ?? (nqp::can($candidate, 'WRAPPERS') ?? 'raku-invoke-wrapped' !! 'raku-invoke')
            !! 'boot-value';
        if $kind == nqp::const::DISP_CALLWITH {
            my $with-inv-flag := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-int',
                $capture-delegate, 0, 1);
            my $with-disp-name := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                $with-inv-flag, 0, $disp);
            my $with-propagate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-int',
                $with-disp-name, 0, nqp::const::DISP_PROPAGATE_CALLWITH);
            nil-or-callwith-propagation-terminal($with-propagate);
        }
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', $disp, $capture-delegate);
        }
    }
    elsif nqp::istype($cur-state, MultiDispatchEnd) || nqp::istype($cur-state, Exhausted) {
        # If this is the initial dispatch, then error, otherwise try the next
        # resumption (possibly method dispatch), and finally give up.
        if $kind == nqp::const::DISP_NONE {
            my $target := nqp::captureposarg($orig-capture, 0);
            multi-no-match-handler($target, $arg-capture, $orig-capture, $arg-capture);
        }
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-cur-state);
            unless nqp::dispatch('boot-syscall', 'dispatcher-next-resumption') {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        $arg-capture, 0, Nil));
            }
        }
    }
    elsif nqp::istype($cur-state, MultiDispatchAmbiguous) {
        # If this is the initial dispatch, then error.
        if $kind == nqp::const::DISP_NONE {
            my $target := nqp::captureposarg($orig-capture, 0);
            multi-ambiguous-handler($cur-state, $target, $arg-capture);
        }
        # Otherwise, step past it and do whatever comes next.
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-cur-state);
            $track-cur-state := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                $track-cur-state, MultiDispatchAmbiguous, '$!next');
            $cur-state := $cur-state.next;
            return raku-multi-non-trivial-step($kind, $track-cur-state, $cur-state,
                $orig-capture, $arg-capture, $is-resume);
        }
    }
    else {
        nqp::die('Unexpected multi dispatch step ' ~ $cur-state.HOW.name($cur-state));
    }
}
sub peel-off-candidate($is-resume, $track-cur-state, $cur-state, $orig-capture) {
    if $is-resume {
        my $track-next := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
            $track-cur-state, MultiDispatchCall, '$!next');
        nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state', $track-next);
    }
    else {
        my $next := $cur-state.next;
        my $init-state := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
            $orig-capture, 0, $next);
        nqp::dispatch('boot-syscall', 'dispatcher-set-resume-init-args', $init-state);
    }
}

# Proxy removal for multiple dispatch.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-multi-remove-proxies',
    # The dispatch receives (remover, original invokee, args...).
    -> $capture {
        # The resume init state drops the remover.
        nqp::dispatch('boot-syscall', 'dispatcher-set-resume-init-args',
            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0));

        # We then invoke the remover with the arguments (so need to drop the
        # original invokee).
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 1));
    },
    # The resumption is done with the args with proxies stripped.
    -> $capture {
        # Make sure this really is the resume with the proxies stripped,
        # not some inner resume, which we should just pass along.
        my $track_kind := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_kind);
        my int $kind := nqp::captureposarg_i($capture, 0);
        if $kind == nqp::const::DISP_DECONT {
            # Yes, it's the resume we're looking for. Locate the candidates by
            # using the resume init args.
            my $orig-capture := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-init-args');
            my $target := nqp::captureposarg($orig-capture, 0);
            my @candidates := nqp::getattr($target, Routine, '@!dispatch_order');

            # Put a guard on the dispatchees. (TODO This risks the callsite in
            # the generated removers becoming a polymorphic blow-up point; when
            # we can associate it with the dispatch program of the initial
            # dispatch, that will be rather better.)
            my $track_callee := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                $orig-capture, 0);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
                nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $track_callee, Routine, '@!dispatchees'));

            # We now make the dispatch plan using the arguments with proxies
            # removed, put pass along the original arg capture too, for use
            # in `rw`-ness testing.
            my $no-proxy-arg-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                $capture, 0);
            my $orig-arg-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                $orig-capture, 0);
            my $dispatch-plan := raku-multi-plan(@candidates, $no-proxy-arg-capture, 0,
                $orig-arg-capture);

            # Consider the dispatch plan. Note we should always pass along the original
            # arguments when invoking, so anything `is rw` gets the Proxy. We for now
            # also send everything through the non-trivial dispatch path to keep it
            # a little simpler.
            if nqp::istype($dispatch-plan, MultiDispatchNonScalar) {
                nqp::die('FETCH from a Proxy unexpectedly returned another Proxy');
            }
            elsif nqp::istype($dispatch-plan, MultiDispatchAmbiguous) &&
                    nqp::istype($dispatch-plan.next, MultiDispatchCall) {
                multi-ambiguous-handler($dispatch-plan, $target, $orig-arg-capture);
            }
            elsif nqp::istype($dispatch-plan, MultiDispatchEnd) {
                multi-no-match-handler($target, $no-proxy-arg-capture, $orig-capture,
                    $orig-arg-capture);
            }
            else {
                my $capture-with-plan := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $orig-capture, 0,
                    $dispatch-plan);
                my $capture-delegate := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-int', $capture-with-plan, 0, 0);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-multi-non-trivial',
                    $capture-delegate);
            }
        }
        elsif !nqp::dispatch('boot-syscall', 'dispatcher-next-resumption') {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    $capture, 0, Nil));
        }
    });

# This is where invocation bottoms out, however we reach it. By this point, we
# just have something to invoke, which is either a code object (potentially with
# wrappers) or something that hopefully has a CALL-ME.
my $late-coerce := -> $target, $val {
    my $how := $target.HOW;
    my $coercion-type := Perl6::Metamodel::CoercionHOW.new_type(
        (nqp::istype($how, Perl6::Metamodel::ClassHOW) && $how.is_pun($target)
            ?? $target.HOW.pun_source($target)
            !! $target.WHAT),
        $val.WHAT);
    nqp::dispatch('raku-coercion', $coercion-type, $val);
}
my $listy-coercion := -> $coercion-type, *@args {
    my $list := nqp::create(List);
    nqp::bindattr($list, List, '$!reified', @args);
    nqp::dispatch('raku-coercion', $coercion-type, $list);
}
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-invoke', -> $capture {
    # Guard type and concreteness of code object, unless it is a literal one
    # (and also determine if it's a literal, as we can avoid some things in
    # that case).
    my $code := nqp::captureposarg($capture, 0);
    my int $code-constant := nqp::dispatch('boot-syscall', 'dispatcher-is-arg-literal',
        $capture, 0);
    my $code_arg := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
    unless $code-constant {
        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $code_arg);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $code_arg);
    }

    # If it's already a VM-level code reference, just invoke it.
    if nqp::reprname($code) eq 'MVMCode' {
        nqp::dispatch('boot-syscall', 'dispatcher-delegate',
            $code-constant ?? 'boot-code-constant' !! 'boot-code',
            $capture);
    }

    elsif nqp::isconcrete(
        my $custom-dispatcher := nqp::decont(nqp::how_nd($code).find_method(nqp::decont($code), 'CUSTOM-DISPATCHER', :no_fallback))
    ) {
        nqp::dispatch('boot-syscall', 'dispatcher-delegate',
            nqp::unbox_s($custom-dispatcher($code)), $capture);
    }

    # If it has a CALL-ME method then always use that (this means it being a
    # Code object, even).
    elsif nqp::isconcrete(my $call-me := nqp::decont(nqp::how_nd($code).find_method($code, 'CALL-ME', :no_fallback))) {
        # A CALL-ME method is found; make a call to it. We use raku-call-simple
        # to avoid setting up any further deferrals, which may get us into the
        # situation where we set up multiple resumptions.
        my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
            $capture, 0, $call-me);
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-call-simple',
            $delegate);
    }

    # If it's a code object...
    elsif nqp::istype($code, Code) {
        # Concrete code object: extract the $!do, replace the code object,
        # and delegate to boot-code.
        if nqp::isconcrete($code) {
            my $do := nqp::getattr($code, Code, '$!do');
            if $code-constant && !nqp::dispatch('boot-syscall', 'code-is-stub', $do) {
                my $args := pass-decontainerized($code,
                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0));
                my $delegate_capture := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $args, 0, $do);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                    $delegate_capture);
            }
            else {
                my $do_attr := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $code_arg, Code, '$!do');
                my $delegate_capture := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
                    0, $do_attr);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code',
                    $delegate_capture);
            }
        }

        # Invoking non-concrete code object is an error.
        else {
            nqp::die('Cannot invoke a ' ~ $code.HOW.name($code) ~ ' type object');
        }
    }

    # If it's ForeignCode, extract the wrapped code object and delegate to
    # lang-code to run whatever is wrapped there.
    elsif nqp::istype($code, ForeignCode) {
        if nqp::isconcrete($code) {
            my $do_attr := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                $code_arg, ForeignCode, '$!do');
            my $delegate_capture := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
                0, $do_attr);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'lang-call',
                $delegate_capture);
        }
        else {
            nqp::die('Cannot invoke a ' ~ $code.HOW.name($code) ~ ' type object');
        }
    }

    # If it's NQP code (ideally we want a more flexible cross-language solution
    # here) then unrap it also.
    elsif nqp::istype($code, NQPRoutine) {
        if nqp::isconcrete($code) {
            if $code-constant {
                my $do := nqp::getattr($code, NQPRoutine, '$!do');
                my $delegate_capture := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj',
                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
                    0, $do);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                    $delegate_capture);
            }
            else {
                my $do_attr := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $code_arg, NQPRoutine, '$!do');
                my $delegate_capture := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
                    0, $do_attr);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code',
                    $delegate_capture);
            }
        }
        else {
            nqp::die('Cannot invoke a ' ~ nqp::how_nd($code).name($code) ~ ' type object');
        }
    }

    # Otherwise, try the coercion path.
    elsif !nqp::isconcrete($code) && nqp::captureposelems($capture) >= 2 {
        # Looks like a coercion. In the best case we just have one argument
        # and things will be straightforward. Failing that, we'll have to
        # form a list and take the slow-bound path.
        if nqp::captureposelems($capture) == 2 {
            # Work out what we have to coerce.
            my $arg-type;
            my int $could-not-guard;
            my int $prim := nqp::captureposprimspec($capture, 1);
            if $prim == 1    { $arg-type := Int }
            elsif $prim == 2 { $arg-type := Num }
            elsif $prim == 3 { $arg-type := Str }
            else {
                # Object argument, so type guard.
                my $arg := nqp::captureposarg($capture, 1);
                $arg-type := $arg.WHAT;
                my $track-arg := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
                    $capture, 1);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-arg);
                if nqp::isconcrete_nd($arg) && nqp::iscont($arg) {
                    # Containerized. If it's a Scalar, we can deref and guard
                    # on that. If not, we'll have to thunk it and figure it
                    # out each time.
                    if nqp::istype_nd($arg, Scalar) {
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
                            nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                                $track-arg, Scalar, '$!value'));
                    }
                    else {
                        $could-not-guard := 1;
                    }
                }
            }

            # Ensure there's no nameds.
            if nqp::capturehasnameds($capture) {
                Perl6::Metamodel::Configuration.throw_or_die(
                    'X::Coerce::Impossible',
                    "Cannot coerce to " ~ nqp::how_nd($code).name($code) ~ " with named arguments",
                    :target-type($code.WHAT),
                    :from-type($arg-type), :hint("named arguments passed")
                );
            }

            # If we could not guard need to delegate to a late-bound
            # handler.
            if $could-not-guard {
                my $delegate := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $capture,
                    0, $late-coerce);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate',
                    'boot-code-constant', $delegate);
            }

            # Otherwise, can rewrite the callsite directly to do the
            # coercion.
            else {
                # Form the coercion type.
                my $how := nqp::how_nd($code);
                my $coercion-type := Perl6::Metamodel::CoercionHOW.new_type(
                    (nqp::istype($how, Perl6::Metamodel::ClassHOW) && $how.is_pun($code)
                        ?? $how.pun_source($code)
                        !! $code.WHAT),
                    $arg-type);

                # Call $coercion-type.HOW.coerce($coercion-type, $val). We
                # know that there was only one item, so we can drop the
                # callee, prepend the coercion type, the HOW, and then the
                # name and type as raku-meth-call wants.
                my $coercee-only := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                    $capture, 0);
                my $with-coercion-type := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $coercee-only, 0, $coercion-type);
                my $coerce-how := nqp::how_nd($coercion-type);
                my $with-how := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $with-coercion-type, 0, $coerce-how);
                my $with-name := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-str', $with-how, 0, 'coerce');
                my $delegate := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $with-name, 0, $coerce-how);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call',
                    $delegate);
            }
        }
        else {
            # List formation is too complex for a dispatch program, so we
            # form a coercion type, prepend it, do the args that form the
            # list to be coerced, and then delegate to a code object to
            # do the rest of the work.
            if nqp::capturehasnameds($capture) {
                Perl6::Metamodel::Configuration.throw_or_die(
                    'X::Coerce::Impossible',
                    "Cannot coerce to " ~ nqp::how_nd($code).name($code) ~ " with named arguments",
                    :target-type($code.WHAT),
                    :from-type(List), :hint("named arguments passed")
                );
            }
            my $how := nqp::how_nd($code);
            my $coercion-type := Perl6::Metamodel::CoercionHOW.new_type(
                (nqp::istype($how, Perl6::Metamodel::ClassHOW) && $how.is_pun($code)
                    ?? $how.pun_source($code)
                    !! $code.WHAT),
                List);
            my $list-elems-only := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                $capture, 0);
            my $with-coercion-type := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $list-elems-only, 0, $coercion-type);
            my $delegate := nqp::dispatch('boot-syscall',
                'dispatcher-insert-arg-literal-obj', $with-coercion-type, 0, $listy-coercion);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                $delegate);
        }
    }

    # We can't invoke it; complain.
    else {
        my $how := nqp::how_nd($code);
        my $class := nqp::getlexcaller('$?CLASS');
        report-method-not-found($code, 'CALL-ME', $class, $how, nqp::iscont($code));
    }
});

# When we see that the callee expects readonly arguments, and what we are
# going to pass is a Scalar container, we can unwrap them on the caller
# side. This has a number of advantages:
# * Since in a multiple dispatch we have to decontainerize in order to guard
#   on the values, this dispatch program will often already have the
#   values in temporaries. Thus we would be doing the work anyway, and don't
#   need to leave it to the callee.
# * The specializer may do specialization linking and inlining, and may stack
#   up extra guards, which include reading from containers. This would duplicate
#   work in the dispatch program. However, if invocation at the end of the
#   dispatch program is just on values, it can avoid that.
# * Since most calls to an operator like infix:<+> will now be two values, not
#   all permutations of scalar with value and value, we may produce less
#   specializations. Further, value + value case has by far the simplest code
#   to inline (and typically avoids further guards and container reads).
# * Even in the case we can't inline, removing the value from the container
#   caller side rather than callee side means that the container doesn't
#   escape, which - especially as PEA in MoarVM gets more powerful - means it
#   will be able to do a great deal more scalar replacements.
sub pass-decontainerized($code, $args) {
    my $signature := nqp::getattr($code, Code, '$!signature');
    my int $readonly := nqp::getattr_i($signature, Signature, '$!readonly');
    my int $pos-args := nqp::captureposelems($args);
    my int $i := 0;
    while $i < $pos-args {
        # If it should be passed read only, and it's an object...
        if $readonly +& nqp::bitshiftl_i(1, $i) && nqp::captureposprimspec($args, $i) == 0 {
            # If it's in a Scalar container...
            my $arg := nqp::captureposarg($args, $i);
            if nqp::isconcrete_nd($arg) && nqp::what_nd($arg) =:= Scalar {
                # Read it from the container and pass it decontainerized.
                my $track-arg := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $args, $i);
                my $track-value := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $track-arg, Scalar, '$!value');
               $args := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $args, $i),
                    $i, $track-value);
            }
        }
        $i++;
    }
    $args
}

# Entrypoint for dispatch to a wrapped routine. Builds the chain and delegates
# to another dispatcher that will handle the walking through it via resumption.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-invoke-wrapped', -> $capture {
    # Guard on the current set of wrappers (the array of them is immutable,
    # so we can rely on its identity).
    my $routine := nqp::captureposarg($capture, 0);
    my $wrapper-type := $routine.WRAPPER-TYPE;
    my $track-routine := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
    my $track-wrappers := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
            $track-routine, $wrapper-type, '$!wrappers');
    nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-wrappers);

    # With wrappers, we pretty much know we'll be traversing them, so we
    # build the deferral chain up front, unlike in other dispatchers.
    my @all-callees := nqp::clone($routine.WRAPPERS);
    my $chain := Exhausted;
    while nqp::elems(@all-callees) {
        $chain := DeferralChain.new(nqp::pop(@all-callees), $chain);
    }

    # Delegate to the wrap deferral dispatcher with the arguments to call
    # the initial wrapper with, preceded by the calculated chain.
    my $without-routine := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0);
    my $with-chain := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
        $without-routine, 0, $chain);
    my $with-kind := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-int',
        $with-chain, 0, nqp::const::DISP_NONE);
    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-wrapper-deferral', $with-kind);
});

# The wrapper deferral dispatcher that moves through wrappers.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-wrapper-deferral',
    # Initial dispatch, called with the chain to walk through along with the
    # arguments. This is used either in the case we are just starting to walk
    # through the dispatchers or in the event of a callwith.
    -> $capture {
        # Obtain and guard on the first wrapper callee.
        my $cur_deferral := nqp::captureposarg($capture, 1);
        my $track_cur_deferral := nqp::dispatch('boot-syscall', 'dispatcher-track-arg',
            $capture, 1);
        my $track_code := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
            $track_cur_deferral, DeferralChain, '$!code');
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_code);

        # Extract the arguments and set the resume init args to be the next item
        # in the chain prepended to the arguments, so long as there is a next
        # wrapper.
        my $next := $cur_deferral.next;
        my $args := nqp::dispatch('boot-syscall', 'dispatcher-drop-n-args', $capture, 0, 2);
        if nqp::isconcrete($next) {
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-init-args',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    $args, 0, $next));
        }

        # Either invoke the wrapper directly, or via callwith propagation.
        my $code := $cur_deferral.code;
        my $code-capture := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
            $args, 0, $code);
        my int $callwith := nqp::captureposarg_i($capture, 0) == nqp::const::DISP_PROPAGATE_CALLWITH;
        if $callwith {
            my $with-inv-flag := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-int',
                $code-capture, 0, 1);
            my $with-disp := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                $with-inv-flag, 0, 'raku-call-simple');
            my $with-kind := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-int',
                $with-disp, 0, nqp::const::DISP_PROPAGATE_CALLWITH);
            unless nqp::dispatch('boot-syscall', 'dispatcher-next-resumption', $with-kind) {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-call-simple',
                    $code-capture);
            }
        }
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-call-simple', $code-capture);
        }
    },
    # Resumption.
    -> $capture {
        # Guard on the kind of resume we're doing, and get that flag.
        my $track_kind := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_kind);
        my int $kind := nqp::captureposarg_i($capture, 0);

        # Work out which wrapper we'll call next.
        my $init := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-init-args');
        my $state := nqp::dispatch('boot-syscall', 'dispatcher-get-resume-state');
        my $track_cur_deferral;
        my $cur_deferral;
        if $kind == nqp::const::DISP_LASTCALL {
            # It's lastcall; just update the state to Exhausted.
            nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal', Exhausted);
        }
        elsif $kind == nqp::const::DISP_ONLYSTAR {
            nqp::dispatch('boot-syscall', 'dispatcher-next-resumption') ||
                nqp::die('Failed to dispatch to candidate from wrapped proto')
        }
        elsif nqp::isnull($state) {
            # No state, so the initial resumption. Guard on there being no
            # dispatch state.
            my $track_state := nqp::dispatch('boot-syscall', 'dispatcher-track-resume-state');
            nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_state);

            # The current deferral is obtained from the initialization state.
            $track_cur_deferral := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $init, 0);
            $cur_deferral := nqp::captureposarg($init, 0);
        }
        elsif !nqp::istype($state, Exhausted) {
            # Already working through a chain of wrappers deferrals. Thus the
            # current deferral is the current state;
            $track_cur_deferral := nqp::dispatch('boot-syscall', 'dispatcher-track-resume-state');
            $cur_deferral := $state;
        }
        else {
            # Dispatch already exhausted; guard on that and fall through to returning
            # Nil.
            my $track_state := nqp::dispatch('boot-syscall', 'dispatcher-track-resume-state');
            nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_state);
        }

        # If we have a current deferral...
        if $cur_deferral {
            if $kind == nqp::const::DISP_CALLWITH {
                # Mark this dispatcher exhausted since we're moving on from it,
                # and then re-enter the dispatcher with the remaining wrappers
                # and the args given to us. We guard on the next candidate and
                # insert it as a literal, since we may not depend on resume
                # init state if using next resumption with args.
                nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state-literal',
                    Exhausted);
                my $args := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0);
                my $with-chain := nqp::dispatch('boot-syscall',
                    'dispatcher-insert-arg-literal-obj', $args, 0, $cur_deferral);
                my $with-kind := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-int',
                    $with-chain, 0, nqp::const::DISP_PROPAGATE_CALLWITH);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-wrapper-deferral',
                    $with-kind);
            }
            else {
                # Not callwith, so we keep walking the list. Update state to
                # move to the next wrapper in the list and then put into effect
                # the resumption.
                my $track_code := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $track_cur_deferral, DeferralChain, '$!code');
                nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track_code);
                my $track_next := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                    $track_cur_deferral, DeferralChain, '$!next');
                nqp::dispatch('boot-syscall', 'dispatcher-set-resume-state', $track_next);
                if $kind == nqp::const::DISP_CALLSAME {
                    # Invoke the next bit of code. We send the original dispatchee to
                    # boot-code, since it's an unwrapped code handle; the rest, we
                    # treat as Raku calls, since it's possible somebody decided to wrap
                    # some code up with a multi.
                    my $code := $cur_deferral.code;
                    my $delegate_capture := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $init, 0),
                        0, $code);
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-call-simple',
                        $delegate_capture);
                }
                elsif $kind == nqp::const::DISP_NEXTCALLEE {
                    # We just want the code itself, not to invoke it.
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            $capture, 0, $cur_deferral.code));
                }
                else {
                    nqp::die('Unimplemented resumption kind in wrap dispatch');
                }
            }
        }
        else {
            # This dispatcher is exhausted. However, there may be another one
            # we can try (for example, in a wrapped method).
            nil-or-callwith-propagation-terminal($capture);
        }
    });

# Like raku-call, except assumes that any method call we see will already have
# been taken care of.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-call-simple', -> $capture {
    my $track_callee := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
    nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track_callee);
    my $callee := nqp::captureposarg($capture, 0);
    if nqp::istype_nd($callee, Routine) {
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
            nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
                $track_callee, Routine, '@!dispatchees'));
        nqp::dispatch('boot-syscall', 'dispatcher-delegate',
            $callee.is_dispatcher ?? 'raku-multi' !! 'raku-invoke', $capture);
    }
    else {
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-invoke', $capture);
    }
});

# Dispatcher to try to find a method, backing nqp::findmethod, nqp::tryfindmethod,
# and nqp::can.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-find-meth', -> $capture {
    # See if this callsite is going megamorphic and do a fallback if so. We only do
    # this in the non-exceptional case.
    my $obj := nqp::captureposarg($capture, 0);
    my $how := nqp::how_nd($obj);
    my int $cache-size := nqp::dispatch('boot-syscall', 'dispatcher-inline-cache-size');
    my int $exceptional := nqp::captureposarg_i($capture, 2);
    if $cache-size >= $MEGA-METH-CALLSITE-SIZE && !$exceptional &&
            nqp::istype($how, Perl6::Metamodel::ClassHOW) {
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-find-meth-mega',
            $capture);
    }
    else {
        # Guard on the invocant type and method name.
        nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
            nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0));
        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
            nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1));

        # Try to find the method.
        my str $name := nqp::captureposarg_s($capture, 1);
        my $meth := $how.find_method($obj, $name);

        # If it's found, evaluate to it.
        if nqp::isconcrete($meth) {
            my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                $capture, 0, $meth);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant', $delegate);
        }

        # Otherwise, depends on exceptional flag whether we report the missing
        # method or hand back a null.
        else {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
                nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 2));
            if $exceptional {
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'lang-meth-not-found',
                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0));
            }
            else {
                my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    $capture, 0, nqp::null());
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant', $delegate);
            }
        }
    }
});
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-find-meth-mega', -> $capture {
    # Always guard on the exception mode (which should always be false, since we
    # don't handle it here).
    nqp::dispatch('boot-syscall', 'dispatcher-guard-literal',
        nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 2));

    # Make sure that we have a method table build for this type (but we don't
    # actually need the table itself).
    my $obj := nqp::captureposarg($capture, 0);
    my $how := nqp::how_nd($obj);
    unless nqp::isconcrete(nqp::getattr($how, Perl6::Metamodel::ClassHOW,
            '$!cached_all_method_table')) {
        nqp::dispatch('boot-syscall', 'dispatcher-do-not-install');
    }
    $how.all_method_table($obj);

    # Track the HOW and then the attribute holding the table.
    my $track-obj := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
    my $track-how := nqp::dispatch('boot-syscall', 'dispatcher-track-how', $track-obj);
    my $track-table := nqp::dispatch('boot-syscall', 'dispatcher-track-attr', $track-how,
        Perl6::Metamodel::ClassHOW, '$!cached_all_method_table');

    # Do the lookup of the method in the table we found in the meta-object. If
    # it's not found, the outcome will be a null, which is exactly what we want.
    my $track-name := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1);
    my $track-resolution := nqp::dispatch('boot-syscall',
        'dispatcher-index-tracked-lookup-table', $track-table, $track-name);
    my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
        $capture, 0, $track-resolution);
    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $delegate);
});

# The dispatcher backing p6capturelex. If we are passed a code object, then
# extracts the underlying handle and causes it to be captured.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-capture-lex', -> $capture {
    my $code := nqp::captureposarg($capture, 0);
    my $track-code := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
    nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-code);
    if nqp::istype($code, Code) {
        my $do := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
            $track-code, Code, '$!do');
        my $with-do := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
            0, $do);
        my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
            $with-do, 0, 'try-capture-lex');
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-syscall',
            $delegate);
    }
    else {
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $capture);
    }
});

# The dispatcher backing p6capturelexwhere. If we are passed a code object, then
# extracts the underlying handle and looks down the callstack for a caller that
# matches the outer, and captures it.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-capture-lex-callers', -> $capture {
    my $code := nqp::captureposarg($capture, 0);
    my $track-code := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
    nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-code);
    if nqp::istype($code, Code) {
        my $do := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
            $track-code, Code, '$!do');
        my $with-do := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
            0, $do);
        my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
            $with-do, 0, 'try-capture-lex-callers');
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-syscall',
            $delegate);
    }
    else {
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $capture);
    }
});

# The dispatcher backing p6getouterctx. Unwraps the code object, and then
# gets a context object for its enclosing scope.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-get-code-outer-ctx', -> $capture {
    my $code := nqp::captureposarg($capture, 0);
    if nqp::istype($code, Code) {
        my $track-code := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        my $do := nqp::dispatch('boot-syscall', 'dispatcher-track-attr',
            $track-code, Code, '$!do');
        my $with-do := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
            0, $do);
        my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
            $with-do, 0, 'get-code-outer-ctx');
        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-syscall',
            $delegate);
    }
    else {
        nqp::die('raku-get-code-outer-ctx requires a Code object');
    }
});

# Resumption error reporting dispatcher.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-resume-error', -> $capture {
    my str $redispatcher := nqp::getcodename(nqp::callercode());
    Perl6::Metamodel::Configuration.throw_or_die(
        'X::NoDispatcher',
        "$redispatcher is not in the dynamic scope of a dispatcher",
        :$redispatcher
    );
});

# Invokability test dispatcher.
nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-isinvokable', -> $capture {
    # Guard on the type, then evaluate to a constant for if it's a Code type.
    nqp::dispatch('boot-syscall', 'dispatcher-guard-type',
        nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0));
    my $callee := nqp::captureposarg($capture, 0);
    my $delegate := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-int',
        $capture, 0, nqp::istype($callee, Code));
    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-constant', $delegate);
});

# Smartmatch support
{
    my $hllbool     := nqp::getstaticcode(-> $prim { nqp::hllboolfor(nqp::istrue($prim), 'Raku') });
    my $hllbool_not := nqp::getstaticcode(-> $prim { nqp::hllboolfor(nqp::not_i(nqp::istrue($prim)), 'Raku') });

    sub is-routine-setting-only($routine, :$U = 0, :$D = 0) {
        if nqp::istype($routine, Routine) {
            return nqp::istrue(
                $routine.IS-SETTING-ONLY(
                    U => $hllbool($U),
                    D => $hllbool($D),
                    with-proto => $hllbool(1)));
        }
        elsif nqp::istype($routine, Code) {
            return nqp::istrue($routine.file.starts-with('SETTING::'));
        }
        # Non-Raku code objects are considered coming from the setting.
        1
    }

    sub is-method-setting-only($type, str $method-name, :$U = 0, :$D = 0) {
        my $method := nqp::tryfindmethod($type, $method-name);
        return 0 unless nqp::defined($method);
        is-routine-setting-only($method, :$U, :$D)
    }

    nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-boolify', -> $capture {
        my $arg-spec := nqp::captureposprimspec($capture, 0);
        my $arg;
        if $arg-spec == 1 {
            $arg := nqp::captureposarg_i($capture, 0);
        }
        elsif $arg-spec == 2 {
            $arg := nqp::captureposarg_n($capture, 0);
        }
        elsif $arg-spec == 3 {
            $arg := nqp::captureposarg_s($capture, 0);
        }
        else {
            $arg := nqp::captureposarg($capture, 0);
        }
        my $track_arg := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        my $explicit-call := 0;
        if nqp::isconcrete($arg) {
            if $arg-spec {
                nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track_arg);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track_arg);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj', $capture, 0, $hllbool)
                );
            }
            elsif nqp::istype($arg, Bool) {
                nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track_arg);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track_arg);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', $capture);
            }
            else {
                $explicit-call := 1;
            }
        }
        elsif is-method-setting-only($arg, 'Bool', :U) {
            # For non-concrete objects default method Bool candidate would always produce False.
            nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track_arg);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track_arg);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    $capture, 0, $hllbool(0)
                )
            );
        }
        else {
            $explicit-call := 1;
        }
        if $explicit-call {
            # There is no need to guard for type when fallback to method call.
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call',
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj', $capture, 0, nqp::what($arg)),
                    1, 'Bool'));
        }
    });

    my &smartmatch-code := nqp::getstaticcode(-> $topic, $rhs {
        nqp::dispatch('raku-boolify', $rhs.ACCEPTS($topic))
    });
    my &negate-smartmatch-code := nqp::getstaticcode(-> $topic, $rhs {
        nqp::hllizefor($rhs.ACCEPTS($topic), 'Raku').not
    });
    my &nominalizable-sm-code := nqp::getstaticcode(-> $topic, $rhs {
        nqp::hllboolfor(nqp::istype($topic, $rhs), 'Raku')
    });
    my &negate-nominalizable-sm-code := nqp::getstaticcode(-> $topic, $rhs {
        nqp::hllboolfor(nqp::not_i(nqp::istype($topic, $rhs)), 'Raku')
    });

    my sub find-core-symbol(str $sym, :$ctx, :$revision) {
        unless nqp::isconcrete($ctx) {
            $ctx := nqp::ctxcaller(nqp::ctx());
        }
        my $core-rev-sym := 'CORE-SETTING-REV';
        while nqp::isnull(nqp::getlexrel($ctx, $core-rev-sym)) {
            $ctx := nqp::ctxcaller($ctx);
        }
        until nqp::isnull($ctx) {
            my $lexpad := nqp::ctxlexpad($ctx);
            if nqp::existskey($lexpad, $core-rev-sym) {
                last unless nqp::isconcrete($revision)
                            && nqp::isne_s(nqp::atkey($lexpad, $core-rev-sym), $revision);
            }
            $ctx := nqp::ctxouterskipthunks($ctx);
        }
        nqp::die("No symbol '" ~ $sym ~ "' found in CORE" ~ ($revision ?? "." ~ $revision !! "")) if nqp::isnull($ctx);
        nqp::getlexrel($ctx, $sym)
    }

    nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-smartmatch', -> $capture {
        # The dispatch receives:
        # - lhs with containerization preserved
        # - rhs with containerization preserved
        # - boolification flag
        # boolification flag can either be -1 to negate, 0 to return as-is, 1 to boolify
        # Note that boolification flag is not guarded because it is expected to be invariant over call site.
        my $Match               := find-core-symbol('Match', :ctx(nqp::ctxcaller(nqp::ctx())));
        my $lhs                 := nqp::captureposarg($capture, 0);
        my $rhs                 := nqp::captureposarg($capture, 1);
        my $boolification       := nqp::captureposarg_i($capture, 2);
        my $track-lhs           := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        my $track-rhs           := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1);

        if nqp::istype_nd($lhs, Scalar) {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-lhs );
            $track-lhs :=
                nqp::dispatch('boot-syscall', 'dispatcher-track-attr', $track-lhs, Scalar, '$!value');
            $lhs := nqp::getattr($lhs, Scalar, '$!value');
        }

        if nqp::istype_nd($rhs, Scalar) {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs );
            $track-rhs :=
                nqp::dispatch('boot-syscall', 'dispatcher-track-attr', $track-rhs, Scalar, '$!value');
            $rhs := nqp::getattr($rhs, Scalar, '$!value');
        }

        my $explicit-accepts := 1;

        if $boolification == 0 {
            if nqp::isconcrete_nd($rhs) && nqp::istype_nd($rhs, Junction) {
                # Make sure to collapse a Junction.
                # nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-boolification);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-rhs);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                                nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2), # boolification flag
                                0),                                                                # LHS
                            0, nqp::what($rhs)),
                        1, 'Bool'));
                $explicit-accepts := 0;
            }
            elsif nqp::isconcrete_nd($rhs) && nqp::istype_nd($rhs, List) {
                # A list must be reified in order to fire up any code embedded into regexes.
                # nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-boolification);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-rhs);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                                nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2), # boolification flag
                                0),                                                                # LHS
                            0, nqp::what($rhs)),
                        1, 'eager'));
                $explicit-accepts := 0;
            }
            elsif nqp::istype_nd($rhs, Nil) {
                # nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-boolification);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        $capture, 0, nqp::hllboolfor(0, 'Raku')));
                $explicit-accepts := 0;
            }
            else {
                # nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-boolification);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
                # Bypass is normally used with Regex-kind of RHS and it is not specced wether the smartmatch result must
                # be deconted in this case. Therefore we better return what we've got on RHS as-is.
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0));
                $explicit-accepts := 0;
            }
        }
        # Delegate to Junction.BOOLIFY-ACCEPTS if possible and makes sense.
        # - Junction type object on RHS is always a type match and we can pass it to the typematching branch
        # - when boolifying over a concrete Regex ACCEPTS fallback is required too
        # - BOOLIFY-ACCEPTS only makes sense when ACCEPTS doesn't handle junctions in a special way. For now this can
        #   only be guaranteed for CORE's ACCEPTS only.
        elsif nqp::istype_nd($lhs, Junction)
            && nqp::isconcrete_nd($lhs)
            && (nqp::isconcrete_nd($rhs) || !nqp::istype($rhs, Junction))
            && !(nqp::isconcrete_nd($rhs) && $boolification == 1 && nqp::istype_nd($rhs, Regex))
            && is-method-setting-only($rhs, 'ACCEPTS', :D)
        {
            # nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-boolification);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-lhs);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-lhs);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-rhs);
            my $method-capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2);
            $method-capture := nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                $method-capture, 2, nqp::hllboolfor($boolification == -1, 'Raku')); # boolification -> Raku's Bool negation flag
            $method-capture :=
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    $method-capture, 0, nqp::what($lhs));
            $method-capture :=
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                    $method-capture, 1, 'BOOLIFY-ACCEPTS');

            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call', $method-capture);
            $explicit-accepts := 0;
        }
        else {
            if nqp::isconcrete_nd($rhs) {
                if $boolification < 0 {
                    if nqp::istype_nd($rhs, Bool) {
                        # nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-boolification);
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-rhs);
                        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
                                0, $hllbool_not($rhs)));
                        $explicit-accepts := 0;
                    }
                    elsif nqp::istype_nd($rhs, $Match) {
                        # nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-boolification);
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-rhs);
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
                        nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg',
                                        nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2), # boolification flag
                                        0),                                                                # LHS
                                    0, nqp::what($rhs)),
                                1, 'not'));
                        $explicit-accepts := 0;
                    }
                }
                elsif nqp::istype_nd($rhs, Bool) || nqp::istype_nd($rhs, $Match) {
                    # nqp::dispatch('boot-syscall', 'dispatcher-guard-literal', $track-boolification);
                    nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-rhs);
                    nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                        nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0)); # drop LHS
                    $explicit-accepts := 0;
                }
            }
            elsif is-method-setting-only($rhs, 'ACCEPTS', :U) { # Non-concrete RHS
                # A typeobject on RHS with default ACCEPTS can be reduced to nqp::istype, unless LHS is a concrete Junction.
                nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-rhs);
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
                my $rhs-how := $rhs.HOW;
                my $can-archetypes := nqp::can($rhs-how, 'archetypes');

                if !$can-archetypes
                    || !$rhs-how.archetypes($rhs).nominalizable
                    || nqp::isnull($rhs-how.wrappee-lookup($rhs, :subset))
                {
                    nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-lhs);
                    if $can-archetypes && $rhs-how.archetypes($rhs).definite {
                        # If RHS is a definite then concreteness of LHS must be considered.
                        nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-lhs);
                    }

                    my $matches := try nqp::istype_nd($lhs, $rhs);
                    $matches := $boolification < 0 ?? $hllbool_not($matches) !! $hllbool($matches);
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
                            0, $matches));
                }
                else {
                    # Subsets are usually using topic value to ensure matching. Normally it would mean full ACCEPTS+Bool
                    # fallback, but we can shortcut directly to HOW.accepts_type and then boolify.
                    # Note that LHS is not relevant for dispatching in this case.
                    my $sm-code := $boolification < 0 ?? &negate-nominalizable-sm-code !! &nominalizable-sm-code;
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2), # boolification flag
                            0, $sm-code));
                }

                $explicit-accepts := 0;
            }
        }

        if $explicit-accepts {
            nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-rhs);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-rhs);
            if $boolification == 0 || (nqp::isconcrete_nd($rhs) && $boolification > -1 && nqp::istype_nd($rhs, Regex)) {
                # Do not boolify over a Regex RHS.

                # First, drop everything except for LHS.
                my $method-capture :=
                        nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2); # boolification flag
                $method-capture :=
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg',
                        $method-capture, 0,
                        nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1)); # Move RHS to the start
                $method-capture :=
                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $method-capture, 2); # Old RHS
                # Then prepare for raku-meth-call: deconted RHS, method name, RHS, LHS.
                $method-capture :=
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        $method-capture, 0, nqp::what($rhs));
                $method-capture :=
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                        $method-capture, 1, 'ACCEPTS');

                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call', $method-capture);
            }
            else {
                my $sm-code := $boolification < 0 ?? &negate-smartmatch-code !! &smartmatch-code;
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2), # boolify flag
                        0, $sm-code));
            }
        }
    });
}

# Coercion protocol
{ 
    # Coerce by target method name. I.e. $value.TargetType.
    my $coerce-by-type-method := nqp::getstaticcode(-> $coercion, $value, $method, $nominal_target, $target_type {
        nqp::istype((my $coerced_value := $method($value)), $target_type)
            || nqp::istype($coerced_value, nqp::gethllsym('Raku', 'Failure'))
            ?? $coerced_value
            !! nqp::how($coercion)."!invalid_coercion"(
                $value, nqp::how_nd($nominal_target).name($nominal_target), $coerced_value)
    });

    # Indirect methods are `COERCE` or `new` on the target type.
    my $coerce-indirect-method := nqp::getstaticcode(-> $coercion, $value, $method, $nominal_target, $target_type {
        my $*COERCION-TYPE := $coercion;
        nqp::istype((my $coerced_value := $method($nominal_target, $value)), $target_type)
            || nqp::istype($coerced_value, nqp::gethllsym('Raku', 'Failure'))
            ?? $coerced_value
            !! nqp::how($coercion)."!invalid_coercion"($value, $method.name, $coerced_value)
    });

    # The special case of `new` when we suspect it may throw X::Constructor::Positional which we interpret as "no
    # coercion method found".
    my $coerce-new := nqp::getstaticcode(-> $coercion, $value, $method, $nominal_target, $target_type {
        my $exception;
        my $coerced_value := nqp::null();
        try {
            CATCH {
                my $exception_obj := nqp::getpayload($!);

                if $exception_obj.HOW.name($exception_obj) ne 'X::Constructor::Positional' {
                    $exception := $!;
                }
            }

            my $*COERCION-TYPE := $coercion;
            $coerced_value := $method($nominal_target, $value);
        }
        nqp::if(nqp::defined($exception), nqp::rethrow($exception));
        nqp::isnull($coerced_value)
            ?? nqp::how($coercion)."!invalid"($value, "no acceptable coercion method found")
            !! nqp::istype($coerced_value, $target_type)
                || nqp::istype($coerced_value, nqp::gethllsym('Raku', 'Failure'))
                ?? $coerced_value
                !! nqp::how($coercion)."!invalid_coercion"($value, 'new', $coerced_value)
    });

    my $coerce-runtime := nqp::getstaticcode(-> $coercion, $value, *@pos {
        nqp::how($coercion)."!coerce_TargetType"($coercion, $value)
    });

    my $coerce-via-container := nqp::getstaticcode(-> $coercion, $value {
        nqp::dispatch('raku-coercion', $coercion, nqp::decont($value))
    });

    sub select-coercer($coercion, $value, :$with-runtime = 0) {
        my $target_type := nqp::how($coercion).target_type($coercion);
        my $constraint_type := nqp::how($coercion).constraint_type($coercion);
        my $nominal_target := nqp::how($coercion).nominal_target($coercion);
        my $coercer := nqp::null();
        my $method := nqp::null();

        # Routine's method `cando` doesn't work well with a Match object passed into as the first positional.
        # Besides, it requires type conversions on NQP/Raku language boundary. Therefore we use a truncated local
        # version of it.
        my sub method-cando($method, *@pos) {
            my $disp;
            if $method.is_dispatcher {
                $disp := $method;
            }
            else {
                $disp := nqp::create(nqp::what($method));
                nqp::bindattr($disp, Routine, '@!dispatchees', nqp::list($method));
            }
            -> *@_ { $disp.find_best_dispatchee( nqp::usecapture(), 1) }(|@pos)
        }

        # Make sure none of the coercion method candidates uses run-time constraints.
        my sub method-is-optimizable($method) {
            # Can do no assumptions about a non-Routine.
            return 0 unless nqp::istype($method, Routine);
            my @cands := $method.is_dispatcher
                            ?? nqp::getattr($method, Routine, '@!dispatchees')
                            !! nqp::list($method);
            for @cands -> $cand {
                my $signature := nqp::decont($cand.signature);

                # Skip the candidate if it has too few positionals or requires too many of them.
                next if nqp::islt_n(nqp::decont($signature.count), 2) 
                        || nqp::isgt_n(nqp::decont($signature.arity), 2);

                my $vparam := nqp::atpos(nqp::getattr($cand.signature, Signature, '@!params'), 1);
                my $ptype := $vparam.type;
                my $ptypeHOW := nqp::how($ptype);
                return 0 if $ptypeHOW.archetypes($ptype).nominalizable 
                            && !nqp::isnull(my $subset := $ptypeHOW.wrappee-lookup($ptype, :subset))
                            && nqp::isconcrete(nqp::how($subset).refinement($subset));
                # XXX Retreat on any parameter postconstraint present. Some refinement is possible here, but to be
                # done later.
                return 0 if nqp::getattr($vparam, Parameter, '@!post_constraints');

            }
            1
        }

        if $with-runtime
            && nqp::can((my $archetypes := nqp::how($constraint_type).archetypes($constraint_type)), 'coercive') 
            && $archetypes.coercive
        {
            $coercer := $coerce-runtime;
        }
        elsif nqp::defined(
            $method := nqp::tryfindmethod(
                nqp::what($value), 
                nqp::how_nd($nominal_target).name($nominal_target)))
            && method-cando($method, $value)
        {
            # There is .TargetType method on the value, use it.
            $coercer := $coerce-by-type-method;
        }
        elsif nqp::defined($method := nqp::tryfindmethod(
                    (nqp::how_nd($nominal_target).archetypes.composable
                        ?? ($nominal_target := nqp::how_nd($nominal_target).pun($nominal_target))
                        !! $nominal_target),
                    'COERCE'))
                && method-cando($method, $nominal_target, $value)
        {
            # The target type can .COERCE.
            if method-is-optimizable($method) {
                $coercer := $coerce-indirect-method 
            }
        }
        elsif nqp::defined($method := nqp::tryfindmethod($nominal_target, 'new'))
                && (my @cands := method-cando($method, $nominal_target, $value))
        {
            if method-is-optimizable($method) {
                # We can TargetType.new($value).
                if +@cands == 1 {
                    if nqp::eqaddr(@cands[0].package, Mu) {
                        # The only .new candidate for a single positional arg call comes from Mu. That one throws
                        # "only named arguments" error which means no candidates are actually found. Simulate this by
                        # resetting $method and pretend it's never been found.
                        $method := nqp::null();
                    }
                    else {
                        $coercer := $coerce-indirect-method;
                    }
                }
                else {
                    $coercer := $coerce-new;
                }
            }
        }
        elsif $with-runtime && nqp::isconcrete($method) {
            $coercer := $coerce-runtime;
        }

        nqp::list($coercer, $method, $nominal_target)
    }

    nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-coercion', -> $capture {
        # The dispatch receives:
        # - coercion type object
        # - value

        my $track-coercion := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);
        my $track-value := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1);

        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-coercion);
        nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-value);

        my $value := nqp::captureposarg($capture, 1);
        my $coercion := nqp::captureposarg($capture, 0);

        if nqp::istype_nd($value, Scalar) {
            $track-value :=
                nqp::dispatch('boot-syscall', 'dispatcher-track-attr', $track-value, Scalar, '$!value');
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-value);
            $value := nqp::getattr($value, Scalar, '$!value');
        }

        nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-value);        

        my $coercionHOW   := nqp::how($coercion);

        # If the type happens to be another nominalizable, like a subset wrapped around a coercion, then pull out the
        # actual coercion type we need.
        unless nqp::istype($coercionHOW, Perl6::Metamodel::CoercionHOW) {
            $coercion    := $coercionHOW.wrappee($coercion, :coercion);
            $coercionHOW := nqp::how_nd($coercion);
        }

        my $target_type   := $coercionHOW.target_type($coercion);
        my $constraint    := $coercionHOW.constraint_type($coercion);
        my $constraintHOW := nqp::how($constraint);

        my sub runtime-fallback() {
            my $method-capture :=
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj', 
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str', 
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj', $capture, 0, $coercionHOW),
                        0, '!coerce_TargetType'),
                    0, $coercionHOW);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call', $method-capture);
        }

        if nqp::iscont($value) {
            # If despite our efforts the value is still a container then try deconting it first and then re-dispatch.
            my $code-capture :=
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    nqp::dispatch('boot-syscall', 'dispatcher-replace-arg-literal-obj', $capture, 1, $value),
                    0, $coerce-via-container);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant', $code-capture);
        }
        elsif nqp::istype_nd($value, $target_type) {
            # Just matches, use identity.
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value', 
                nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0));
        }
        elsif !nqp::eqaddr($constraint, Mu) && !nqp::istype_nd($value, $constraint) {
            # The value doesn't match constraint type, throw.
            my $method-capture :=
                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
                            0, $coercionHOW),
                        0, '!invalid_type'),
                    0, $coercionHOW);
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call', $method-capture);
        }
        elsif nqp::can((my $archetypes := $constraintHOW.archetypes($constraint)), 'coercive') 
            && $archetypes.coercive 
        { 
            # The constraint type is a coercion on its own. This is not a dispatchable case. Fallback to the metamodel
            # method.
            runtime-fallback();
        }
        else {
            # Try finding one of the coercion methods.

            my @cdesc := select-coercer($coercion, $value);
            my $coercer := @cdesc[0]; # Code object to do the coercion
            my $method := @cdesc[1];
            my $nominal_target := @cdesc[2];

            if nqp::isconcrete($coercer) {
                # We found an acceptable coercer, use it.
                my $code-capture :=
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj', $capture, 
                                    0, $coercer),
                                3, $method),
                            4, $nominal_target),
                        5, $target_type);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant', $code-capture);
            }
            elsif nqp::isconcrete($method) {
                # We found a method but cannot reliably assume that it can be optimized. Let the run-time handle it.
                runtime-fallback();
            }
            else {
                # There is no way we can coerce the value.
                my $method-capture :=
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                                nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-str',
                                    nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 0),
                                    1, "no acceptable coercion method found"),
                                0, $coercionHOW),
                            0, '!invalid'),
                        0, $coercionHOW);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-meth-call', $method-capture);
            }
        }
    });

# Return value type-check dispatcher. The first value is the return value,
# the second is the type that is expected, which may be a definiteness or
# coercion type.
    sub return_error($got, $wanted) {
        Perl6::Metamodel::Configuration.throw_or_die(
            'X::TypeCheck::Return',
            "Type check failed for return value; expected '" ~
                $wanted.HOW.name($wanted) ~ "' but got '" ~
                $got.HOW.name($got) ~ "'",
            :$got,
            :expected($wanted)
        );
    }

    my $check_type_typeobj := -> $ret, $type {
        !nqp::isconcrete($ret) && nqp::istype($ret, $type)
            ?? $ret
            !! return_error($ret, $type)
    }

    my $check_type_concrete := -> $ret, $type {
        nqp::isconcrete($ret) && nqp::istype($ret, $type)
            ?? $ret
            !! return_error($ret, $type)
    }

    my $check_type := -> $ret, $type {
        nqp::istype($ret, $type)
            ?? $ret
            !! return_error($ret, $type)
    }

    # For @cdesc values see select-coercer sub.
    my $check_type_typeobj_coerce := -> $ret, $type, $coercion, @cdesc {
        !nqp::isconcrete($ret) && nqp::istype($ret, $type)
            ?? @cdesc[0]( # coercer code
                $coercion, $ret, 
                @cdesc[1], # coercing method
                @cdesc[2], # nominal target
                nqp::how($coercion).target_type($coercion))
            !! return_error($ret, $type)
    }

    my $check_type_concrete_coerce := -> $ret, $type, $coercion, @cdesc {
        nqp::isconcrete($ret) && nqp::istype($ret, $type)
            ?? @cdesc[0](
                $coercion, $ret, 
                @cdesc[1],
                @cdesc[2],
                nqp::how($coercion).target_type($coercion))
            !! return_error($ret, $type)
    }

    my $check_type_coerce := -> $ret, $type, $coercion, @cdesc {
        nqp::istype($ret, $type)
            ?? @cdesc[0](
                $coercion, $ret, 
                @cdesc[1],
                @cdesc[2],
                nqp::how($coercion).target_type($coercion))
            !! return_error($ret, $type)
    }

    nqp::dispatch('boot-syscall', 'dispatcher-register', 'raku-rv-typecheck', -> $capture {
        # Dispatcher arguments:
        # - return value
        # - type
        # - "is generic" flag
        # If the type is Mu or unset, then nothing is needed except identity.
        my $type := nqp::captureposarg($capture, 1);
        my $is_generic := nqp::captureposarg_i($capture, 2);

        # If the type has been instantiated from a generic then we'd need to track over it too.
        if $is_generic {
            my $track-ret-type := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 1);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-ret-type);
        }

        # We will never need the $is_generic argument, but otherwise the capture is used to call checker subs where the
        # third argument is not anticipated.
        $capture := nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 2);

        if nqp::isnull($type) || $type =:= Mu {
            nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 1))
        }

        # Otherwise, need to look at the type.
        else {
            my sub runtime-only($t) {
                return 0 if nqp::isnull($t);
                nqp::isconcrete(nqp::how_nd($t).refinement($t)) 
                    || nqp::how_nd(my $refinee := nqp::how_nd($t).refinee($t)).archetypes($refinee).nominalizable
                        && runtime-only(nqp::how_nd($refinee).wrappee-lookup($refinee, :subset))
            }

            my $rv := nqp::captureposarg($capture, 0);
            my int $definite-check := -1;
            my $how := nqp::how_nd($type);
            my $coercion-type;
            my $constraint-type := nqp::null();
            my $runtime-check := 0; # Is there a run-time constraint?
            if $how.archetypes($type).nominalizable {
                $runtime-check := runtime-only($how.wrappee-lookup($type, :subset));
                unless nqp::isnull($coercion-type := $how.wrappee-lookup($type, :coercion)) {
                    unless nqp::isnull($constraint-type := nqp::how_nd($coercion-type).constraint_type($coercion-type)) {
                        $runtime-check := $runtime-check 
                            || (nqp::how_nd($constraint-type).archetypes($constraint-type).nominalizable
                                && runtime-only(nqp::how_nd($constraint-type).wrappee-lookup($constraint-type, :subset)));
                    }
                }
                if $how.archetypes($type).definite {
                    my $dtype := $how.wrappee($type, :definite);
                    $definite-check := nqp::how_nd($dtype).definite($dtype);
                }
            } 

            my $track-value := nqp::dispatch('boot-syscall', 'dispatcher-track-arg', $capture, 0);

            if nqp::istype_nd($rv, Scalar) {
                nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-value);
                $track-value := nqp::dispatch(
                    'boot-syscall', 'dispatcher-track-attr', 
                    $track-value, Scalar, '$!value');
            }

            nqp::dispatch('boot-syscall', 'dispatcher-guard-type', $track-value);
            nqp::dispatch('boot-syscall', 'dispatcher-guard-concreteness', $track-value)
                if $definite-check != -1;

            my $need-coercion := $how.archetypes($type).coercive 
                                    && !nqp::istype($rv, 
                                        nqp::how_nd($coercion-type).target_type($coercion-type));
            my $is-Nil := nqp::istype($rv, Nil);

            if $is-Nil || !($runtime-check || $need-coercion)
            {
                # Static typecheck which would either result in identity or error.
                if $is-Nil 
                    || (nqp::istype($rv, $type)
                        && ($definite-check == 0 
                            ?? !nqp::isconcrete($rv) 
                            !! $definite-check == 1 
                                ?? nqp::isconcrete($rv) 
                                !! 1))
                {
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-value',
                        nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 1));
                }
                else {
                    # Not passing the definedness check.
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code', 
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj', $capture, 0, &return_error));
                }
            }
            elsif $runtime-check && !$need-coercion {
                # The case of subset with run-time constraints, no coercion.
                my $checker := 
                    $definite-check == 0
                        ?? $check_type_typeobj
                        !! $definite-check == 1
                            ?? $check_type_concrete
                            !! $check_type;
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant', 
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj', $capture, 0, $checker));
            }
            elsif !$runtime-check && $need-coercion {
                # Coercion only. Make sure we can coerce by matching the constraint type and then dispatch directly to
                # the coercion dispatcher.
                if nqp::eqaddr($constraint-type, Mu) || nqp::istype($rv, $constraint-type) {
                    # Coercion dispatcher uses reverse order of arguments, same as the metamodel method `coerce`.
                    my $coercion-capture :=
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            nqp::dispatch('boot-syscall', 'dispatcher-drop-arg', $capture, 1),
                            0, $type);
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'raku-coercion', $coercion-capture);
                }
                else {
                    # Coercion is not possible.
                    nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant', 
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj', $capture, 0, &return_error));
                }
            }
            else {
                # The most expensive path: subset with constraints and coercion.
                # First, we re-consider defininite check because now we're going to use coercion constraint for that.
                $definite-check := 
                    nqp::how_nd($constraint-type).archetypes($constraint-type).definite
                        ?? nqp::how_nd(
                                my $dt := nqp::how_nd($constraint-type).wrappee($constraint-type, :definite)
                            ).definite($dt)
                        !! -1;
                my $checker := 
                    $definite-check == 0
                        ?? $check_type_typeobj_coerce
                        !! $definite-check == 1
                            ?? $check_type_concrete_coerce
                            !! $check_type_coerce;
                my @cdesc := select-coercer($coercion-type, $rv, :with-runtime);
                my $checker-capture :=
                    nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                        nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj',
                            nqp::dispatch('boot-syscall', 'dispatcher-insert-arg-literal-obj', $capture,
                                2, $coercion-type),
                            3, @cdesc), # coercer code
                        0, $checker);
                nqp::dispatch('boot-syscall', 'dispatcher-delegate', 'boot-code-constant', $checker-capture);
            }
        }
    });
}