private function match(string $input, int $pos, array $preferredTokens): ?Match
    {
        $current_regexp = $this->getRegexpForTokens($preferredTokens);

        if (false === preg_match($current_regexp, $input, $match, 0, $pos)) {
            $error_code = preg_last_error();
            throw new \RuntimeException(
                "PCRE error #" . $error_code
                . " for token at input pos $pos; REGEXP = $current_regexp",
                $error_code
            );
        }

        if (!$match) {
            return null;
        }

        $full_match = $match[0];
        if ('' === $full_match) {
            throw new DevException(
                'Tokens should not match empty string'
                . '; context: `' . substr($input, $pos, 10) . '`'
                . '; expected: ' . json_encode($preferredTokens)
                . "; REGEXP: $current_regexp"
            );
        }

        // remove null, empty "" values and integer keys but [0]
        foreach ($match as $key => $value) {
            if (
                null === $value
                || '' === $value
                || (0 !== $key && is_int($key))
            ) {
                unset($match[$key]);
            }
        }

        $named = $match;
        unset($named[0]);
        if (1 !== count($named)) {
            throw new InternalException('Match with multiple named group');
        }

        $content = reset($named);
        $type = key($named);
        $is_inline = false;
        if (isset($this->aliased[$type])) {
            $type = $this->aliased[$type];
            $is_inline = true;
        }
        $token = new Token($type, $content, $match, $pos, $is_inline);

        $result = new Match();
        $result->token = $token;
        $result->nextOffset = $pos + strlen($full_match);

        return $result;
    }