/*
 *  String built-ins
 *
 *  Most String built-ins must only accept strings (or String objects).
 *  Symbols, represented internally as strings, must be generally rejected.
 *  The duk_push_this_coercible_to_string() helper does this automatically.
 */

/* XXX: There are several limitations in the current implementation for
 * strings with >= 0x80000000UL characters.  In some cases one would need
 * to be able to represent the range [-0xffffffff,0xffffffff] and so on.
 * Generally character and byte length are assumed to fit into signed 32
 * bits (< 0x80000000UL).  Places with issues are not marked explicitly
 * below in all cases, look for signed type usage (duk_int_t etc) for
 * offsets/lengths.
 */

#include "third_party/duktape/duk_internal.h"

#if defined(DUK_USE_STRING_BUILTIN)

/*
 *  Helpers
 */

DUK_LOCAL duk_hstring *duk__str_tostring_notregexp(duk_hthread *thr,
                                                   duk_idx_t idx) {
  duk_hstring *h;

  if (duk_get_class_number(thr, idx) == DUK_HOBJECT_CLASS_REGEXP) {
    DUK_ERROR_TYPE_INVALID_ARGS(thr);
    DUK_WO_NORETURN(return NULL;);
  }
  h = duk_to_hstring(thr, idx);
  DUK_ASSERT(h != NULL);

  return h;
}

DUK_LOCAL duk_int_t duk__str_search_shared(duk_hthread *thr,
                                           duk_hstring *h_this,
                                           duk_hstring *h_search,
                                           duk_int_t start_cpos,
                                           duk_bool_t backwards) {
  duk_int_t cpos;
  duk_int_t bpos;
  const duk_uint8_t *p_start, *p_end, *p;
  const duk_uint8_t *q_start;
  duk_int_t q_blen;
  duk_uint8_t firstbyte;
  duk_uint8_t t;

  cpos = start_cpos;

  /* Empty searchstring always matches; cpos must be clamped here.
   * (If q_blen were < 0 due to clamped coercion, it would also be
   * caught here.)
   */
  q_start = DUK_HSTRING_GET_DATA(h_search);
  q_blen = (duk_int_t)DUK_HSTRING_GET_BYTELEN(h_search);
  if (q_blen <= 0) {
    return cpos;
  }
  DUK_ASSERT(q_blen > 0);

  bpos = (duk_int_t)duk_heap_strcache_offset_char2byte(thr, h_this,
                                                       (duk_uint32_t)cpos);

  p_start = DUK_HSTRING_GET_DATA(h_this);
  p_end = p_start + DUK_HSTRING_GET_BYTELEN(h_this);
  p = p_start + bpos;

  /* This loop is optimized for size.  For speed, there should be
   * two separate loops, and we should ensure that memcmp() can be
   * used without an extra "will searchstring fit" check.  Doing
   * the preconditioning for 'p' and 'p_end' is easy but cpos
   * must be updated if 'p' is wound back (backward scanning).
   */

  firstbyte = q_start[0]; /* leading byte of match string */
  while (p <= p_end && p >= p_start) {
    t = *p;

    /* For ECMAScript strings, this check can only match for
     * initial UTF-8 bytes (not continuation bytes).  For other
     * strings all bets are off.
     */

    if ((t == firstbyte) && ((duk_size_t)(p_end - p) >= (duk_size_t)q_blen)) {
      DUK_ASSERT(q_blen > 0);
      if (duk_memcmp((const void *)p, (const void *)q_start, (size_t)q_blen) ==
          0) {
        return cpos;
      }
    }

    /* track cpos while scanning */
    if (backwards) {
      /* when going backwards, we decrement cpos 'early';
       * 'p' may point to a continuation byte of the char
       * at offset 'cpos', but that's OK because we'll
       * backtrack all the way to the initial byte.
       */
      if ((t & 0xc0) != 0x80) {
        cpos--;
      }
      p--;
    } else {
      if ((t & 0xc0) != 0x80) {
        cpos++;
      }
      p++;
    }
  }

  /* Not found.  Empty string case is handled specially above. */
  return -1;
}

/*
 *  Constructor
 */

DUK_INTERNAL duk_ret_t duk_bi_string_constructor(duk_hthread *thr) {
  duk_hstring *h;
  duk_uint_t flags;

  /* String constructor needs to distinguish between an argument not given at
   * all vs. given as 'undefined'.  We're a vararg function to handle this
   * properly.
   */

  /* XXX: copy current activation flags to thr, including current magic,
   * is_constructor_call etc.  This takes a few bytes in duk_hthread but
   * makes call sites smaller (there are >30 is_constructor_call and get
   * current magic call sites.
   */

  if (duk_get_top(thr) == 0) {
    duk_push_hstring_empty(thr);
  } else {
    h = duk_to_hstring_acceptsymbol(thr, 0);
    if (DUK_UNLIKELY(DUK_HSTRING_HAS_SYMBOL(h) &&
                     !duk_is_constructor_call(thr))) {
      duk_push_symbol_descriptive_string(thr, h);
      duk_replace(thr, 0);
    }
  }
  duk_to_string(thr, 0); /* catches symbol argument for constructor call */
  DUK_ASSERT(duk_is_string(thr, 0));
  duk_set_top(thr, 1); /* Top may be 1 or larger. */

  if (duk_is_constructor_call(thr)) {
    /* String object internal value is immutable */
    flags = DUK_HOBJECT_FLAG_EXTENSIBLE | DUK_HOBJECT_FLAG_FASTREFS |
            DUK_HOBJECT_FLAG_EXOTIC_STRINGOBJ |
            DUK_HOBJECT_CLASS_AS_FLAGS(DUK_HOBJECT_CLASS_STRING);
    duk_push_object_helper(thr, flags, DUK_BIDX_STRING_PROTOTYPE);
    duk_dup_0(thr);
    duk_xdef_prop_stridx_short(thr, -2, DUK_STRIDX_INT_VALUE,
                               DUK_PROPDESC_FLAGS_NONE);
  }
  /* Note: unbalanced stack on purpose */

  return 1;
}

DUK_LOCAL duk_ret_t duk__construct_from_codepoints(duk_hthread *thr,
                                                   duk_bool_t nonbmp) {
  duk_bufwriter_ctx bw_alloc;
  duk_bufwriter_ctx *bw;
  duk_idx_t i, n;
  duk_ucodepoint_t cp;

  /* XXX: It would be nice to build the string directly but ToUint16()
   * coercion is needed so a generic helper would not be very
   * helpful (perhaps coerce the value stack first here and then
   * build a string from a duk_tval number sequence in one go?).
   */

  n = duk_get_top(thr);

  bw = &bw_alloc;
  DUK_BW_INIT_PUSHBUF(
      thr, bw, (duk_size_t)n); /* initial estimate for ASCII only codepoints */

  for (i = 0; i < n; i++) {
    /* XXX: could improve bufwriter handling to write multiple codepoints
     * with one ensure call but the relative benefit would be quite small.
     */

    if (nonbmp) {
      /* ES2015 requires that (1) SameValue(cp, ToInteger(cp)) and
       * (2) cp >= 0 and cp <= 0x10ffff.  This check does not
       * implement the steps exactly but the outcome should be
       * the same.
       */
      duk_int32_t i32 = 0;
      if (!duk_is_whole_get_int32(duk_to_number(thr, i), &i32) || i32 < 0 ||
          i32 > 0x10ffffL) {
        DUK_DCERROR_RANGE_INVALID_ARGS(thr);
      }
      DUK_ASSERT(i32 >= 0 && i32 <= 0x10ffffL);
      cp = (duk_ucodepoint_t)i32;
      DUK_BW_WRITE_ENSURE_CESU8(thr, bw, cp);
    } else {
#if defined(DUK_USE_NONSTD_STRING_FROMCHARCODE_32BIT)
      /* ToUint16() coercion is mandatory in the E5.1 specification, but
       * this non-compliant behavior makes more sense because we support
       * non-BMP codepoints.  Don't use CESU-8 because that'd create
       * surrogate pairs.
       */
      cp = (duk_ucodepoint_t)duk_to_uint32(thr, i);
      DUK_BW_WRITE_ENSURE_XUTF8(thr, bw, cp);
#else
      cp = (duk_ucodepoint_t)duk_to_uint16(thr, i);
      DUK_ASSERT(cp >= 0 && cp <= 0x10ffffL);
      DUK_BW_WRITE_ENSURE_CESU8(thr, bw, cp);
#endif
    }
  }

  DUK_BW_COMPACT(thr, bw);
  (void)duk_buffer_to_string(thr,
                             -1); /* Safe, extended UTF-8 or CESU-8 encoded. */
  return 1;
}

DUK_INTERNAL duk_ret_t
duk_bi_string_constructor_from_char_code(duk_hthread *thr) {
  return duk__construct_from_codepoints(thr, 0 /*nonbmp*/);
}

#if defined(DUK_USE_ES6)
DUK_INTERNAL duk_ret_t
duk_bi_string_constructor_from_code_point(duk_hthread *thr) {
  return duk__construct_from_codepoints(thr, 1 /*nonbmp*/);
}
#endif

/*
 *  toString(), valueOf()
 */

DUK_INTERNAL duk_ret_t duk_bi_string_prototype_to_string(duk_hthread *thr) {
  duk_tval *tv;

  duk_push_this(thr);
  tv = duk_require_tval(thr, -1);
  DUK_ASSERT(tv != NULL);

  if (DUK_TVAL_IS_STRING(tv)) {
    /* return as is */
  } else if (DUK_TVAL_IS_OBJECT(tv)) {
    duk_hobject *h = DUK_TVAL_GET_OBJECT(tv);
    DUK_ASSERT(h != NULL);

    /* Must be a "string object", i.e. class "String" */
    if (DUK_HOBJECT_GET_CLASS_NUMBER(h) != DUK_HOBJECT_CLASS_STRING) {
      goto type_error;
    }

    duk_xget_owndataprop_stridx_short(thr, -1, DUK_STRIDX_INT_VALUE);
    DUK_ASSERT(duk_is_string(thr, -1));
  } else {
    goto type_error;
  }

  (void)duk_require_hstring_notsymbol(
      thr, -1); /* Reject symbols (and wrapped symbols). */
  return 1;

type_error:
  DUK_DCERROR_TYPE_INVALID_ARGS(thr);
}

/*
 *  Character and charcode access
 */

DUK_INTERNAL duk_ret_t duk_bi_string_prototype_char_at(duk_hthread *thr) {
  duk_hstring *h;
  duk_int_t pos;

  /* XXX: faster implementation */

  h = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h != NULL);

  pos = duk_to_int(thr, 0);

  if (sizeof(duk_size_t) >= sizeof(duk_uint_t)) {
    /* Cast to duk_size_t works in this case:
     * - If pos < 0, (duk_size_t) pos will always be
     *   >= max_charlen, and result will be the empty string
     *   (see duk_substring()).
     * - If pos >= 0, pos + 1 cannot wrap.
     */
    DUK_ASSERT((duk_size_t)DUK_INT_MIN >= DUK_HSTRING_MAX_BYTELEN);
    DUK_ASSERT((duk_size_t)DUK_INT_MAX + 1U > (duk_size_t)DUK_INT_MAX);
    duk_substring(thr, -1, (duk_size_t)pos, (duk_size_t)pos + 1U);
  } else {
    /* If size_t is smaller than int, explicit bounds checks
     * are needed because an int may wrap multiple times.
     */
    if (DUK_UNLIKELY(pos < 0 || (duk_uint_t)pos >=
                                    (duk_uint_t)DUK_HSTRING_GET_CHARLEN(h))) {
      duk_push_hstring_empty(thr);
    } else {
      duk_substring(thr, -1, (duk_size_t)pos, (duk_size_t)pos + 1U);
    }
  }

  return 1;
}

/* Magic: 0=charCodeAt, 1=codePointAt */
DUK_INTERNAL duk_ret_t duk_bi_string_prototype_char_code_at(duk_hthread *thr) {
  duk_int_t pos;
  duk_hstring *h;
  duk_bool_t clamped;
  duk_uint32_t cp;
  duk_int_t magic;

  /* XXX: faster implementation */

  DUK_DDD(DUK_DDDPRINT("arg=%!T", (duk_tval *)duk_get_tval(thr, 0)));

  h = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h != NULL);

  pos = duk_to_int_clamped_raw(
      thr, 0 /*index*/, 0 /*min(incl)*/,
      (duk_int_t)DUK_HSTRING_GET_CHARLEN(h) - 1 /*max(incl)*/,
      &clamped /*out_clamped*/);
#if defined(DUK_USE_ES6)
  magic = duk_get_current_magic(thr);
#else
  DUK_ASSERT(duk_get_current_magic(thr) == 0);
  magic = 0;
#endif
  if (clamped) {
    /* For out-of-bounds indices .charCodeAt() returns NaN and
     * .codePointAt() returns undefined.
     */
    if (magic != 0) {
      return 0;
    }
    duk_push_nan(thr);
  } else {
    DUK_ASSERT(pos >= 0);
    cp = (duk_uint32_t)duk_hstring_char_code_at_raw(
        thr, h, (duk_uint_t)pos, (duk_bool_t)magic /*surrogate_aware*/);
    duk_push_u32(thr, cp);
  }
  return 1;
}

/*
 *  substring(), substr(), slice()
 */

/* XXX: any chance of merging these three similar but still slightly
 * different algorithms so that footprint would be reduced?
 */

DUK_INTERNAL duk_ret_t duk_bi_string_prototype_substring(duk_hthread *thr) {
  duk_hstring *h;
  duk_int_t start_pos, end_pos;
  duk_int_t len;

  h = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h != NULL);
  len = (duk_int_t)DUK_HSTRING_GET_CHARLEN(h);

  /* [ start end str ] */

  start_pos = duk_to_int_clamped(thr, 0, 0, len);
  if (duk_is_undefined(thr, 1)) {
    end_pos = len;
  } else {
    end_pos = duk_to_int_clamped(thr, 1, 0, len);
  }
  DUK_ASSERT(start_pos >= 0 && start_pos <= len);
  DUK_ASSERT(end_pos >= 0 && end_pos <= len);

  if (start_pos > end_pos) {
    duk_int_t tmp = start_pos;
    start_pos = end_pos;
    end_pos = tmp;
  }

  DUK_ASSERT(end_pos >= start_pos);

  duk_substring(thr, -1, (duk_size_t)start_pos, (duk_size_t)end_pos);
  return 1;
}

#if defined(DUK_USE_SECTION_B)
DUK_INTERNAL duk_ret_t duk_bi_string_prototype_substr(duk_hthread *thr) {
  duk_hstring *h;
  duk_int_t start_pos, end_pos;
  duk_int_t len;

  /* Unlike non-obsolete String calls, substr() algorithm in E5.1
   * specification will happily coerce undefined and null to strings
   * ("undefined" and "null").
   */
  duk_push_this(thr);
  h = duk_to_hstring_m1(thr); /* Reject Symbols. */
  DUK_ASSERT(h != NULL);
  len = (duk_int_t)DUK_HSTRING_GET_CHARLEN(h);

  /* [ start length str ] */

  /* The implementation for computing of start_pos and end_pos differs
   * from the standard algorithm, but is intended to result in the exactly
   * same behavior.  This is not always obvious.
   */

  /* combines steps 2 and 5; -len ensures max() not needed for step 5 */
  start_pos = duk_to_int_clamped(thr, 0, -len, len);
  if (start_pos < 0) {
    start_pos = len + start_pos;
  }
  DUK_ASSERT(start_pos >= 0 && start_pos <= len);

  /* combines steps 3, 6; step 7 is not needed */
  if (duk_is_undefined(thr, 1)) {
    end_pos = len;
  } else {
    DUK_ASSERT(start_pos <= len);
    end_pos = start_pos + duk_to_int_clamped(thr, 1, 0, len - start_pos);
  }
  DUK_ASSERT(start_pos >= 0 && start_pos <= len);
  DUK_ASSERT(end_pos >= 0 && end_pos <= len);
  DUK_ASSERT(end_pos >= start_pos);

  duk_substring(thr, -1, (duk_size_t)start_pos, (duk_size_t)end_pos);
  return 1;
}
#endif /* DUK_USE_SECTION_B */

DUK_INTERNAL duk_ret_t duk_bi_string_prototype_slice(duk_hthread *thr) {
  duk_hstring *h;
  duk_int_t start_pos, end_pos;
  duk_int_t len;

  h = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h != NULL);
  len = (duk_int_t)DUK_HSTRING_GET_CHARLEN(h);

  /* [ start end str ] */

  start_pos = duk_to_int_clamped(thr, 0, -len, len);
  if (start_pos < 0) {
    start_pos = len + start_pos;
  }
  if (duk_is_undefined(thr, 1)) {
    end_pos = len;
  } else {
    end_pos = duk_to_int_clamped(thr, 1, -len, len);
    if (end_pos < 0) {
      end_pos = len + end_pos;
    }
  }
  DUK_ASSERT(start_pos >= 0 && start_pos <= len);
  DUK_ASSERT(end_pos >= 0 && end_pos <= len);

  if (end_pos < start_pos) {
    end_pos = start_pos;
  }

  DUK_ASSERT(end_pos >= start_pos);

  duk_substring(thr, -1, (duk_size_t)start_pos, (duk_size_t)end_pos);
  return 1;
}

/*
 *  Case conversion
 */

DUK_INTERNAL duk_ret_t
duk_bi_string_prototype_caseconv_shared(duk_hthread *thr) {
  duk_small_int_t uppercase = duk_get_current_magic(thr);

  (void)duk_push_this_coercible_to_string(thr);
  duk_unicode_case_convert_string(thr, (duk_bool_t)uppercase);
  return 1;
}

/*
 *  indexOf() and lastIndexOf()
 */

DUK_INTERNAL duk_ret_t
duk_bi_string_prototype_indexof_shared(duk_hthread *thr) {
  duk_hstring *h_this;
  duk_hstring *h_search;
  duk_int_t clen_this;
  duk_int_t cpos;
  duk_small_uint_t is_lastindexof = (duk_small_uint_t)duk_get_current_magic(
      thr); /* 0=indexOf, 1=lastIndexOf */

  h_this = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h_this != NULL);
  clen_this = (duk_int_t)DUK_HSTRING_GET_CHARLEN(h_this);

  h_search = duk_to_hstring(thr, 0);
  DUK_ASSERT(h_search != NULL);

  duk_to_number(thr, 1);
  if (duk_is_nan(thr, 1) && is_lastindexof) {
    /* indexOf: NaN should cause pos to be zero.
     * lastIndexOf: NaN should cause pos to be +Infinity
     * (and later be clamped to len).
     */
    cpos = clen_this;
  } else {
    cpos = duk_to_int_clamped(thr, 1, 0, clen_this);
  }

  cpos = duk__str_search_shared(thr, h_this, h_search, cpos,
                                is_lastindexof /*backwards*/);
  duk_push_int(thr, cpos);
  return 1;
}

/*
 *  replace()
 */

/* XXX: the current implementation works but is quite clunky; it compiles
 * to almost 1,4kB of x86 code so it needs to be simplified (better approach,
 * shared helpers, etc).  Some ideas for refactoring:
 *
 * - a primitive to convert a string into a regexp matcher (reduces matching
 *   code at the cost of making matching much slower)
 * - use replace() as a basic helper for match() and split(), which are both
 *   much simpler
 * - API call to get_prop and to_boolean
 */

DUK_INTERNAL duk_ret_t duk_bi_string_prototype_replace(duk_hthread *thr) {
  duk_hstring *h_input;
  duk_hstring *h_match;
  duk_hstring *h_search;
  duk_hobject *h_re;
  duk_bufwriter_ctx bw_alloc;
  duk_bufwriter_ctx *bw;
#if defined(DUK_USE_REGEXP_SUPPORT)
  duk_bool_t is_regexp;
  duk_bool_t is_global;
#endif
  duk_bool_t is_repl_func;
  duk_uint32_t match_start_coff, match_start_boff;
#if defined(DUK_USE_REGEXP_SUPPORT)
  duk_int_t match_caps;
#endif
  duk_uint32_t prev_match_end_boff;
  const duk_uint8_t *r_start, *r_end, *r; /* repl string scan */
  duk_size_t tmp_sz;

  DUK_ASSERT_TOP(thr, 2);
  h_input = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h_input != NULL);

  bw = &bw_alloc;
  DUK_BW_INIT_PUSHBUF(
      thr, bw,
      DUK_HSTRING_GET_BYTELEN(
          h_input)); /* input size is good output starting point */

  DUK_ASSERT_TOP(thr, 4);

  /* stack[0] = search value
   * stack[1] = replace value
   * stack[2] = input string
   * stack[3] = result buffer
   */

  h_re = duk_get_hobject_with_class(thr, 0, DUK_HOBJECT_CLASS_REGEXP);
  if (h_re) {
#if defined(DUK_USE_REGEXP_SUPPORT)
    is_regexp = 1;
    is_global = duk_get_prop_stridx_boolean(thr, 0, DUK_STRIDX_GLOBAL, NULL);

    if (is_global) {
      /* start match from beginning */
      duk_push_int(thr, 0);
      duk_put_prop_stridx_short(thr, 0, DUK_STRIDX_LAST_INDEX);
    }
#else  /* DUK_USE_REGEXP_SUPPORT */
    DUK_DCERROR_UNSUPPORTED(thr);
#endif /* DUK_USE_REGEXP_SUPPORT */
  } else {
    duk_to_string(thr, 0); /* rejects symbols */
#if defined(DUK_USE_REGEXP_SUPPORT)
    is_regexp = 0;
    is_global = 0;
#endif
  }

  if (duk_is_function(thr, 1)) {
    is_repl_func = 1;
    r_start = NULL;
    r_end = NULL;
  } else {
    duk_hstring *h_repl;

    is_repl_func = 0;
    h_repl = duk_to_hstring(thr, 1); /* reject symbols */
    DUK_ASSERT(h_repl != NULL);
    r_start = DUK_HSTRING_GET_DATA(h_repl);
    r_end = r_start + DUK_HSTRING_GET_BYTELEN(h_repl);
  }

  prev_match_end_boff = 0;

  for (;;) {
    /*
     *  If matching with a regexp:
     *    - non-global RegExp: lastIndex not touched on a match, zeroed
     *      on a non-match
     *    - global RegExp: on match, lastIndex will be updated by regexp
     *      executor to point to next char after the matching part (so that
     *      characters in the matching part are not matched again)
     *
     *  If matching with a string:
     *    - always non-global match, find first occurrence
     *
     *  We need:
     *    - The character offset of start-of-match for the replacer function
     *    - The byte offsets for start-of-match and end-of-match to implement
     *      the replacement values $&, $`, and $', and to copy non-matching
     *      input string portions (including header and trailer) verbatim.
     *
     *  NOTE: the E5.1 specification is a bit vague how the RegExp should
     *  behave in the replacement process; e.g. is matching done first for
     *  all matches (in the global RegExp case) before any replacer calls
     *  are made?  See: test-bi-string-proto-replace.js for discussion.
     */

    DUK_ASSERT_TOP(thr, 4);

#if defined(DUK_USE_REGEXP_SUPPORT)
    if (is_regexp) {
      duk_dup_0(thr);
      duk_dup_2(thr);
      duk_regexp_match(thr); /* [ ... regexp input ] -> [ res_obj ] */
      if (!duk_is_object(thr, -1)) {
        duk_pop(thr);
        break;
      }

      duk_get_prop_stridx_short(thr, -1, DUK_STRIDX_INDEX);
      DUK_ASSERT(duk_is_number(thr, -1));
      match_start_coff = duk_get_uint(thr, -1);
      duk_pop(thr);

      duk_get_prop_index(thr, -1, 0);
      DUK_ASSERT(duk_is_string(thr, -1));
      h_match = duk_known_hstring(thr, -1);
      duk_pop(
          thr); /* h_match is borrowed, remains reachable through match_obj */

      if (DUK_HSTRING_GET_BYTELEN(h_match) == 0) {
        /* This should be equivalent to match() algorithm step 8.f.iii.2:
         * detect an empty match and allow it, but don't allow it twice.
         */
        duk_uint32_t last_index;

        duk_get_prop_stridx_short(thr, 0, DUK_STRIDX_LAST_INDEX);
        last_index = (duk_uint32_t)duk_get_uint(thr, -1);
        DUK_DDD(DUK_DDDPRINT("empty match, bump lastIndex: %ld -> %ld",
                             (long)last_index, (long)(last_index + 1)));
        duk_pop(thr);
        duk_push_uint(thr, (duk_uint_t)(last_index + 1));
        duk_put_prop_stridx_short(thr, 0, DUK_STRIDX_LAST_INDEX);
      }

      DUK_ASSERT(duk_get_length(thr, -1) <= DUK_INT_MAX); /* string limits */
      match_caps = (duk_int_t)duk_get_length(thr, -1);
    } else {
#else                                         /* DUK_USE_REGEXP_SUPPORT */
    {                        /* unconditionally */
#endif                                        /* DUK_USE_REGEXP_SUPPORT */
      const duk_uint8_t *p_start, *p_end, *p; /* input string scan */
      const duk_uint8_t *q_start;             /* match string */
      duk_size_t q_blen;

#if defined(DUK_USE_REGEXP_SUPPORT)
      DUK_ASSERT(!is_global); /* single match always */
#endif

      p_start = DUK_HSTRING_GET_DATA(h_input);
      p_end = p_start + DUK_HSTRING_GET_BYTELEN(h_input);
      p = p_start;

      h_search = duk_known_hstring(thr, 0);
      q_start = DUK_HSTRING_GET_DATA(h_search);
      q_blen = (duk_size_t)DUK_HSTRING_GET_BYTELEN(h_search);

      p_end -= q_blen; /* ensure full memcmp() fits in while */

      match_start_coff = 0;

      while (p <= p_end) {
        DUK_ASSERT(p + q_blen <= DUK_HSTRING_GET_DATA(h_input) +
                                     DUK_HSTRING_GET_BYTELEN(h_input));
        if (duk_memcmp((const void *)p, (const void *)q_start,
                       (size_t)q_blen) == 0) {
          duk_dup_0(thr);
          h_match = duk_known_hstring(thr, -1);
#if defined(DUK_USE_REGEXP_SUPPORT)
          match_caps = 0;
#endif
          goto found;
        }

        /* track utf-8 non-continuation bytes */
        if ((p[0] & 0xc0) != 0x80) {
          match_start_coff++;
        }
        p++;
      }

      /* not found */
      break;
    }
  found:

    /* stack[0] = search value
     * stack[1] = replace value
     * stack[2] = input string
     * stack[3] = result buffer
     * stack[4] = regexp match OR match string
     */

    match_start_boff = (duk_uint32_t)duk_heap_strcache_offset_char2byte(
        thr, h_input, match_start_coff);

    tmp_sz = (duk_size_t)(match_start_boff - prev_match_end_boff);
    DUK_BW_WRITE_ENSURE_BYTES(
        thr, bw, DUK_HSTRING_GET_DATA(h_input) + prev_match_end_boff, tmp_sz);

    prev_match_end_boff = match_start_boff + DUK_HSTRING_GET_BYTELEN(h_match);

    if (is_repl_func) {
      duk_idx_t idx_args;
      duk_hstring *h_repl;

      /* regexp res_obj is at index 4 */

      duk_dup_1(thr);
      idx_args = duk_get_top(thr);

#if defined(DUK_USE_REGEXP_SUPPORT)
      if (is_regexp) {
        duk_int_t idx;
        duk_require_stack(thr, match_caps + 2);
        for (idx = 0; idx < match_caps; idx++) {
          /* match followed by capture(s) */
          duk_get_prop_index(thr, 4, (duk_uarridx_t)idx);
        }
      } else {
#else  /* DUK_USE_REGEXP_SUPPORT */
      {                      /* unconditionally */
#endif /* DUK_USE_REGEXP_SUPPORT */
        /* match == search string, by definition */
        duk_dup_0(thr);
      }
      duk_push_uint(thr, (duk_uint_t)match_start_coff);
      duk_dup_2(thr);

      /* [ ... replacer match [captures] match_char_offset input ] */

      duk_call(thr, duk_get_top(thr) - idx_args);
      h_repl = duk_to_hstring_m1(thr); /* -> [ ... repl_value ] */
      DUK_ASSERT(h_repl != NULL);

      DUK_BW_WRITE_ENSURE_HSTRING(thr, bw, h_repl);

      duk_pop(thr); /* repl_value */
    } else {
      r = r_start;

      while (r < r_end) {
        duk_int_t ch1;
        duk_int_t ch2;
#if defined(DUK_USE_REGEXP_SUPPORT)
        duk_int_t ch3;
#endif
        duk_size_t left;

        ch1 = *r++;
        if (ch1 != DUK_ASC_DOLLAR) {
          goto repl_write;
        }
        DUK_ASSERT(r <= r_end);
        left = (duk_size_t)(r_end - r);

        if (left <= 0) {
          goto repl_write;
        }

        ch2 = r[0];
        switch (ch2) {
          case DUK_ASC_DOLLAR: {
            ch1 = (1u << 8) + DUK_ASC_DOLLAR;
            goto repl_write;
          }
          case DUK_ASC_AMP: {
            DUK_BW_WRITE_ENSURE_HSTRING(thr, bw, h_match);
            r++;
            continue;
          }
          case DUK_ASC_GRAVE: {
            tmp_sz = (duk_size_t)match_start_boff;
            DUK_BW_WRITE_ENSURE_BYTES(thr, bw, DUK_HSTRING_GET_DATA(h_input),
                                      tmp_sz);
            r++;
            continue;
          }
          case DUK_ASC_SINGLEQUOTE: {
            duk_uint32_t match_end_boff;

            /* Use match charlen instead of bytelen, just in case the input and
             * match codepoint encodings would have different lengths.
             */
            /* XXX: charlen computed here, and also in char2byte helper. */
            match_end_boff = (duk_uint32_t)duk_heap_strcache_offset_char2byte(
                thr, h_input,
                match_start_coff +
                    (duk_uint_fast32_t)DUK_HSTRING_GET_CHARLEN(h_match));

            tmp_sz =
                (duk_size_t)(DUK_HSTRING_GET_BYTELEN(h_input) - match_end_boff);
            DUK_BW_WRITE_ENSURE_BYTES(
                thr, bw, DUK_HSTRING_GET_DATA(h_input) + match_end_boff,
                tmp_sz);
            r++;
            continue;
          }
          default: {
#if defined(DUK_USE_REGEXP_SUPPORT)
            duk_int_t capnum, captmp, capadv;
            /* XXX: optional check, match_caps is zero if no regexp,
             * so dollar will be interpreted literally anyway.
             */

            if (!is_regexp) {
              goto repl_write;
            }

            if (!(ch2 >= DUK_ASC_0 && ch2 <= DUK_ASC_9)) {
              goto repl_write;
            }
            capnum = ch2 - DUK_ASC_0;
            capadv = 1;

            if (left >= 2) {
              ch3 = r[1];
              if (ch3 >= DUK_ASC_0 && ch3 <= DUK_ASC_9) {
                captmp = capnum * 10 + (ch3 - DUK_ASC_0);
                if (captmp < match_caps) {
                  capnum = captmp;
                  capadv = 2;
                }
              }
            }

            if (capnum > 0 && capnum < match_caps) {
              DUK_ASSERT(is_regexp != 0); /* match_caps == 0 without regexps */

              /* regexp res_obj is at offset 4 */
              duk_get_prop_index(thr, 4, (duk_uarridx_t)capnum);
              if (duk_is_string(thr, -1)) {
                duk_hstring *h_tmp_str;

                h_tmp_str = duk_known_hstring(thr, -1);

                DUK_BW_WRITE_ENSURE_HSTRING(thr, bw, h_tmp_str);
              } else {
                /* undefined -> skip (replaced with empty) */
              }
              duk_pop(thr);
              r += capadv;
              continue;
            } else {
              goto repl_write;
            }
#else       /* DUK_USE_REGEXP_SUPPORT */
            goto repl_write; /* unconditionally */
#endif      /* DUK_USE_REGEXP_SUPPORT */
          } /* default case */
        }   /* switch (ch2) */

      repl_write:
        /* ch1 = (r_increment << 8) + byte */

        DUK_BW_WRITE_ENSURE_U8(thr, bw, (duk_uint8_t)(ch1 & 0xff));
        r += ch1 >> 8;
      } /* while repl */
    }   /* if (is_repl_func) */

    duk_pop(thr); /* pop regexp res_obj or match string */

#if defined(DUK_USE_REGEXP_SUPPORT)
    if (!is_global) {
#else
    {                        /* unconditionally; is_global==0 */
#endif
      break;
    }
  }

  /* trailer */
  tmp_sz = (duk_size_t)(DUK_HSTRING_GET_BYTELEN(h_input) - prev_match_end_boff);
  DUK_BW_WRITE_ENSURE_BYTES(
      thr, bw, DUK_HSTRING_GET_DATA(h_input) + prev_match_end_boff, tmp_sz);

  DUK_ASSERT_TOP(thr, 4);
  DUK_BW_COMPACT(thr, bw);
  (void)duk_buffer_to_string(thr, -1); /* Safe if inputs are safe. */
  return 1;
}

/*
 *  split()
 */

/* XXX: very messy now, but works; clean up, remove unused variables (nomimally
 * used so compiler doesn't complain).
 */

DUK_INTERNAL duk_ret_t duk_bi_string_prototype_split(duk_hthread *thr) {
  duk_hstring *h_input;
  duk_hstring *h_sep;
  duk_uint32_t limit;
  duk_uint32_t arr_idx;
#if defined(DUK_USE_REGEXP_SUPPORT)
  duk_bool_t is_regexp;
#endif
  duk_bool_t matched; /* set to 1 if any match exists (needed for empty input
                         special case) */
  duk_uint32_t prev_match_end_coff, prev_match_end_boff;
  duk_uint32_t match_start_boff, match_start_coff;
  duk_uint32_t match_end_boff, match_end_coff;

  h_input = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h_input != NULL);

  duk_push_array(thr);

  if (duk_is_undefined(thr, 1)) {
    limit = 0xffffffffUL;
  } else {
    limit = duk_to_uint32(thr, 1);
  }

  if (limit == 0) {
    return 1;
  }

  /* If the separator is a RegExp, make a "clone" of it.  The specification
   * algorithm calls [[Match]] directly for specific indices; we emulate this
   * by tweaking lastIndex and using a "force global" variant of
   * duk_regexp_match() which will use global-style matching even when the
   * RegExp itself is non-global.
   */

  if (duk_is_undefined(thr, 0)) {
    /* The spec algorithm first does "R = ToString(separator)" before checking
     * whether separator is undefined.  Since this is side effect free, we can
     * skip the ToString() here.
     */
    duk_dup_2(thr);
    duk_put_prop_index(thr, 3, 0);
    return 1;
  } else if (duk_get_hobject_with_class(thr, 0, DUK_HOBJECT_CLASS_REGEXP) !=
             NULL) {
#if defined(DUK_USE_REGEXP_SUPPORT)
    duk_push_hobject_bidx(thr, DUK_BIDX_REGEXP_CONSTRUCTOR);
    duk_dup_0(thr);
    duk_new(thr, 1); /* [ ... RegExp val ] -> [ ... res ] */
    duk_replace(thr, 0);
    /* lastIndex is initialized to zero by new RegExp() */
    is_regexp = 1;
#else
    DUK_DCERROR_UNSUPPORTED(thr);
#endif
  } else {
    duk_to_string(thr, 0);
#if defined(DUK_USE_REGEXP_SUPPORT)
    is_regexp = 0;
#endif
  }

  /* stack[0] = separator (string or regexp)
   * stack[1] = limit
   * stack[2] = input string
   * stack[3] = result array
   */

  prev_match_end_boff = 0;
  prev_match_end_coff = 0;
  arr_idx = 0;
  matched = 0;

  for (;;) {
    /*
     *  The specification uses RegExp [[Match]] to attempt match at specific
     *  offsets.  We don't have such a primitive, so we use an actual RegExp
     *  and tweak lastIndex.  Since the RegExp may be non-global, we use a
     *  special variant which forces global-like behavior for matching.
     */

    DUK_ASSERT_TOP(thr, 4);

#if defined(DUK_USE_REGEXP_SUPPORT)
    if (is_regexp) {
      duk_dup_0(thr);
      duk_dup_2(thr);
      duk_regexp_match_force_global(
          thr); /* [ ... regexp input ] -> [ res_obj ] */
      if (!duk_is_object(thr, -1)) {
        duk_pop(thr);
        break;
      }
      matched = 1;

      duk_get_prop_stridx_short(thr, -1, DUK_STRIDX_INDEX);
      DUK_ASSERT(duk_is_number(thr, -1));
      match_start_coff = duk_get_uint(thr, -1);
      match_start_boff = (duk_uint32_t)duk_heap_strcache_offset_char2byte(
          thr, h_input, match_start_coff);
      duk_pop(thr);

      if (match_start_coff == DUK_HSTRING_GET_CHARLEN(h_input)) {
        /* don't allow an empty match at the end of the string */
        duk_pop(thr);
        break;
      }

      duk_get_prop_stridx_short(thr, 0, DUK_STRIDX_LAST_INDEX);
      DUK_ASSERT(duk_is_number(thr, -1));
      match_end_coff = duk_get_uint(thr, -1);
      match_end_boff = (duk_uint32_t)duk_heap_strcache_offset_char2byte(
          thr, h_input, match_end_coff);
      duk_pop(thr);

      /* empty match -> bump and continue */
      if (prev_match_end_boff == match_end_boff) {
        duk_push_uint(thr, (duk_uint_t)(match_end_coff + 1));
        duk_put_prop_stridx_short(thr, 0, DUK_STRIDX_LAST_INDEX);
        duk_pop(thr);
        continue;
      }
    } else {
#else                                         /* DUK_USE_REGEXP_SUPPORT */
    { /* unconditionally */
#endif                                        /* DUK_USE_REGEXP_SUPPORT */
      const duk_uint8_t *p_start, *p_end, *p; /* input string scan */
      const duk_uint8_t *q_start;             /* match string */
      duk_size_t q_blen, q_clen;

      p_start = DUK_HSTRING_GET_DATA(h_input);
      p_end = p_start + DUK_HSTRING_GET_BYTELEN(h_input);
      p = p_start + prev_match_end_boff;

      h_sep = duk_known_hstring(thr, 0); /* symbol already rejected above */
      q_start = DUK_HSTRING_GET_DATA(h_sep);
      q_blen = (duk_size_t)DUK_HSTRING_GET_BYTELEN(h_sep);
      q_clen = (duk_size_t)DUK_HSTRING_GET_CHARLEN(h_sep);

      p_end -= q_blen; /* ensure full memcmp() fits in while */

      match_start_coff = prev_match_end_coff;

      if (q_blen == 0) {
        /* Handle empty separator case: it will always match, and always
         * triggers the check in step 13.c.iii initially.  Note that we
         * must skip to either end of string or start of first codepoint,
         * skipping over any continuation bytes!
         *
         * Don't allow an empty string to match at the end of the input.
         */

        matched = 1; /* empty separator can always match */

        match_start_coff++;
        p++;
        while (p < p_end) {
          if ((p[0] & 0xc0) != 0x80) {
            goto found;
          }
          p++;
        }
        goto not_found;
      }

      DUK_ASSERT(q_blen > 0 && q_clen > 0);
      while (p <= p_end) {
        DUK_ASSERT(p + q_blen <= DUK_HSTRING_GET_DATA(h_input) +
                                     DUK_HSTRING_GET_BYTELEN(h_input));
        DUK_ASSERT(q_blen > 0); /* no issues with empty memcmp() */
        if (duk_memcmp((const void *)p, (const void *)q_start,
                       (size_t)q_blen) == 0) {
          /* never an empty match, so step 13.c.iii can't be triggered */
          goto found;
        }

        /* track utf-8 non-continuation bytes */
        if ((p[0] & 0xc0) != 0x80) {
          match_start_coff++;
        }
        p++;
      }

    not_found:
      /* not found */
      break;

    found:
      matched = 1;
      match_start_boff = (duk_uint32_t)(p - p_start);
      match_end_coff = (duk_uint32_t)(
          match_start_coff + q_clen); /* constrained by string length */
      match_end_boff = (duk_uint32_t)(match_start_boff + q_blen); /* ditto */

      /* empty match (may happen with empty separator) -> bump and continue */
      if (prev_match_end_boff == match_end_boff) {
        prev_match_end_boff++;
        prev_match_end_coff++;
        continue;
      }
    } /* if (is_regexp) */

    /* stack[0] = separator (string or regexp)
     * stack[1] = limit
     * stack[2] = input string
     * stack[3] = result array
     * stack[4] = regexp res_obj (if is_regexp)
     */

    DUK_DDD(DUK_DDDPRINT("split; match_start b=%ld,c=%ld, match_end "
                         "b=%ld,c=%ld, prev_end b=%ld,c=%ld",
                         (long)match_start_boff, (long)match_start_coff,
                         (long)match_end_boff, (long)match_end_coff,
                         (long)prev_match_end_boff, (long)prev_match_end_coff));

    duk_push_lstring(
        thr,
        (const char *)(DUK_HSTRING_GET_DATA(h_input) + prev_match_end_boff),
        (duk_size_t)(match_start_boff - prev_match_end_boff));
    duk_put_prop_index(thr, 3, arr_idx);
    arr_idx++;
    if (arr_idx >= limit) {
      goto hit_limit;
    }

#if defined(DUK_USE_REGEXP_SUPPORT)
    if (is_regexp) {
      duk_size_t i, len;

      len = duk_get_length(thr, 4);
      for (i = 1; i < len; i++) {
        DUK_ASSERT(i <= DUK_UARRIDX_MAX); /* cannot have >4G captures */
        duk_get_prop_index(thr, 4, (duk_uarridx_t)i);
        duk_put_prop_index(thr, 3, arr_idx);
        arr_idx++;
        if (arr_idx >= limit) {
          goto hit_limit;
        }
      }

      duk_pop(thr);
      /* lastIndex already set up for next match */
    } else {
#else  /* DUK_USE_REGEXP_SUPPORT */
    {
        /* unconditionally */
#endif /* DUK_USE_REGEXP_SUPPORT */
       /* no action */
    }

    prev_match_end_boff = match_end_boff;
    prev_match_end_coff = match_end_coff;
    continue;
  } /* for */

  /* Combined step 11 (empty string special case) and 14-15. */

  DUK_DDD(DUK_DDDPRINT("split trailer; prev_end b=%ld,c=%ld",
                       (long)prev_match_end_boff, (long)prev_match_end_coff));

  if (DUK_HSTRING_GET_BYTELEN(h_input) > 0 || !matched) {
    /* Add trailer if:
     *   a) non-empty input
     *   b) empty input and no (zero size) match found (step 11)
     */

    duk_push_lstring(
        thr, (const char *)DUK_HSTRING_GET_DATA(h_input) + prev_match_end_boff,
        (duk_size_t)(DUK_HSTRING_GET_BYTELEN(h_input) - prev_match_end_boff));
    duk_put_prop_index(thr, 3, arr_idx);
    /* No arr_idx update or limit check */
  }

  return 1;

hit_limit:
#if defined(DUK_USE_REGEXP_SUPPORT)
  if (is_regexp) {
    duk_pop(thr);
  }
#endif

  return 1;
}

/*
 *  Various
 */

#if defined(DUK_USE_REGEXP_SUPPORT)
DUK_LOCAL void duk__to_regexp_helper(duk_hthread *thr, duk_idx_t idx,
                                     duk_bool_t force_new) {
  duk_hobject *h;

  /* Shared helper for match() steps 3-4, search() steps 3-4. */

  DUK_ASSERT(idx >= 0);

  if (force_new) {
    goto do_new;
  }

  h = duk_get_hobject_with_class(thr, idx, DUK_HOBJECT_CLASS_REGEXP);
  if (!h) {
    goto do_new;
  }
  return;

do_new:
  duk_push_hobject_bidx(thr, DUK_BIDX_REGEXP_CONSTRUCTOR);
  duk_dup(thr, idx);
  duk_new(thr, 1); /* [ ... RegExp val ] -> [ ... res ] */
  duk_replace(thr, idx);
}
#endif /* DUK_USE_REGEXP_SUPPORT */

#if defined(DUK_USE_REGEXP_SUPPORT)
DUK_INTERNAL duk_ret_t duk_bi_string_prototype_search(duk_hthread *thr) {
  /* Easiest way to implement the search required by the specification
   * is to do a RegExp test() with lastIndex forced to zero.  To avoid
   * side effects on the argument, "clone" the RegExp if a RegExp was
   * given as input.
   *
   * The global flag of the RegExp should be ignored; setting lastIndex
   * to zero (which happens when "cloning" the RegExp) should have an
   * equivalent effect.
   */

  DUK_ASSERT_TOP(thr, 1);
  (void)duk_push_this_coercible_to_string(thr); /* at index 1 */
  duk__to_regexp_helper(thr, 0 /*index*/, 1 /*force_new*/);

  /* stack[0] = regexp
   * stack[1] = string
   */

  /* Avoid using RegExp.prototype methods, as they're writable and
   * configurable and may have been changed.
   */

  duk_dup_0(thr);
  duk_dup_1(thr);        /* [ ... re_obj input ] */
  duk_regexp_match(thr); /* -> [ ... res_obj ] */

  if (!duk_is_object(thr, -1)) {
    duk_push_int(thr, -1);
    return 1;
  }

  duk_get_prop_stridx_short(thr, -1, DUK_STRIDX_INDEX);
  DUK_ASSERT(duk_is_number(thr, -1));
  return 1;
}
#endif /* DUK_USE_REGEXP_SUPPORT */

#if defined(DUK_USE_REGEXP_SUPPORT)
DUK_INTERNAL duk_ret_t duk_bi_string_prototype_match(duk_hthread *thr) {
  duk_bool_t global;
  duk_int_t prev_last_index;
  duk_int_t this_index;
  duk_int_t arr_idx;

  DUK_ASSERT_TOP(thr, 1);
  (void)duk_push_this_coercible_to_string(thr);
  duk__to_regexp_helper(thr, 0 /*index*/, 0 /*force_new*/);
  global = duk_get_prop_stridx_boolean(thr, 0, DUK_STRIDX_GLOBAL, NULL);
  DUK_ASSERT_TOP(thr, 2);

  /* stack[0] = regexp
   * stack[1] = string
   */

  if (!global) {
    duk_regexp_match(thr); /* -> [ res_obj ] */
    return 1;              /* return 'res_obj' */
  }

  /* Global case is more complex. */

  /* [ regexp string ] */

  duk_push_int(thr, 0);
  duk_put_prop_stridx_short(thr, 0, DUK_STRIDX_LAST_INDEX);
  duk_push_array(thr);

  /* [ regexp string res_arr ] */

  prev_last_index = 0;
  arr_idx = 0;

  for (;;) {
    DUK_ASSERT_TOP(thr, 3);

    duk_dup_0(thr);
    duk_dup_1(thr);
    duk_regexp_match(thr); /* -> [ ... regexp string ] -> [ ... res_obj ] */

    if (!duk_is_object(thr, -1)) {
      duk_pop(thr);
      break;
    }

    duk_get_prop_stridx_short(thr, 0, DUK_STRIDX_LAST_INDEX);
    DUK_ASSERT(duk_is_number(thr, -1));
    this_index = duk_get_int(thr, -1);
    duk_pop(thr);

    if (this_index == prev_last_index) {
      this_index++;
      duk_push_int(thr, this_index);
      duk_put_prop_stridx_short(thr, 0, DUK_STRIDX_LAST_INDEX);
    }
    prev_last_index = this_index;

    duk_get_prop_index(thr, -1, 0); /* match string */
    duk_put_prop_index(thr, 2, (duk_uarridx_t)arr_idx);
    arr_idx++;
    duk_pop(thr); /* res_obj */
  }

  if (arr_idx == 0) {
    duk_push_null(thr);
  }

  return 1; /* return 'res_arr' or 'null' */
}
#endif /* DUK_USE_REGEXP_SUPPORT */

DUK_INTERNAL duk_ret_t duk_bi_string_prototype_concat(duk_hthread *thr) {
  /* duk_concat() coerces arguments with ToString() in correct order */
  (void)duk_push_this_coercible_to_string(thr);
  duk_insert(thr, 0); /* this is relatively expensive */
  duk_concat(thr, duk_get_top(thr));
  return 1;
}

DUK_INTERNAL duk_ret_t duk_bi_string_prototype_trim(duk_hthread *thr) {
  DUK_ASSERT_TOP(thr, 0);
  (void)duk_push_this_coercible_to_string(thr);
  duk_trim(thr, 0);
  DUK_ASSERT_TOP(thr, 1);
  return 1;
}

#if defined(DUK_USE_ES6)
DUK_INTERNAL duk_ret_t duk_bi_string_prototype_repeat(duk_hthread *thr) {
  duk_hstring *h_input;
  duk_size_t input_blen;
  duk_size_t result_len;
  duk_int_t count_signed;
  duk_uint_t count;
  const duk_uint8_t *src;
  duk_uint8_t *buf;
  duk_uint8_t *p;
  duk_double_t d;
#if !defined(DUK_USE_PREFER_SIZE)
  duk_size_t copy_size;
  duk_uint8_t *p_end;
#endif

  DUK_ASSERT_TOP(thr, 1);
  h_input = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h_input != NULL);
  input_blen = DUK_HSTRING_GET_BYTELEN(h_input);

  /* Count is ToNumber() coerced; +Infinity must be always rejected
   * (even if input string is zero length), as well as negative values
   * and -Infinity.  -Infinity doesn't require an explicit check
   * because duk_get_int() clamps it to DUK_INT_MIN which gets rejected
   * as a negative value (regardless of input string length).
   */
  d = duk_to_number(thr, 0);
  if (duk_double_is_posinf(d)) {
    goto fail_range;
  }
  count_signed = duk_get_int(thr, 0);
  if (count_signed < 0) {
    goto fail_range;
  }
  count = (duk_uint_t)count_signed;

  /* Overflow check for result length. */
  result_len = count * input_blen;
  if (count != 0 && result_len / count != input_blen) {
    goto fail_range;
  }

  /* Temporary fixed buffer, later converted to string. */
  buf = (duk_uint8_t *)duk_push_fixed_buffer_nozero(thr, result_len);
  DUK_ASSERT(buf != NULL);
  src = (const duk_uint8_t *)DUK_HSTRING_GET_DATA(h_input);
  DUK_ASSERT(src != NULL);

#if defined(DUK_USE_PREFER_SIZE)
  p = buf;
  while (count-- > 0) {
    duk_memcpy((void *)p, (const void *)src,
               input_blen); /* copy size may be zero, but pointers are valid */
    p += input_blen;
  }
#else  /* DUK_USE_PREFER_SIZE */
  /* Take advantage of already copied pieces to speed up the process
   * especially for small repeated strings.
   */
  p = buf;
  p_end = p + result_len;
  copy_size = input_blen;
  for (;;) {
    duk_size_t remain = (duk_size_t)(p_end - p);
    DUK_DDD(DUK_DDDPRINT(
        "remain=%ld, copy_size=%ld, input_blen=%ld, result_len=%ld",
        (long)remain, (long)copy_size, (long)input_blen, (long)result_len));
    if (remain <= copy_size) {
      /* If result_len is zero, this case is taken and does
       * a zero size copy (with valid pointers).
       */
      duk_memcpy((void *)p, (const void *)src, remain);
      break;
    } else {
      duk_memcpy((void *)p, (const void *)src, copy_size);
      p += copy_size;
    }

    src = (const duk_uint8_t *)buf; /* Use buf as source for larger copies. */
    copy_size = (duk_size_t)(p - buf);
  }
#endif /* DUK_USE_PREFER_SIZE */

  /* XXX: It would be useful to be able to create a duk_hstring with
   * a certain byte size whose data area wasn't initialized and which
   * wasn't in the string table yet.  This would allow a string to be
   * constructed directly without a buffer temporary and when it was
   * finished, it could be injected into the string table.  Currently
   * this isn't possible because duk_hstrings are only tracked by the
   * intern table (they are not in heap_allocated).
   */

  duk_buffer_to_string(thr, -1); /* Safe if input is safe. */
  return 1;

fail_range:
  DUK_DCERROR_RANGE_INVALID_ARGS(thr);
}
#endif /* DUK_USE_ES6 */

DUK_INTERNAL duk_ret_t
duk_bi_string_prototype_locale_compare(duk_hthread *thr) {
  duk_hstring *h1;
  duk_hstring *h2;
  duk_size_t h1_len, h2_len, prefix_len;
  duk_small_int_t ret = 0;
  duk_small_int_t rc;

  /* The current implementation of localeCompare() is simply a codepoint
   * by codepoint comparison, implemented with a simple string compare
   * because UTF-8 should preserve codepoint ordering (assuming valid
   * shortest UTF-8 encoding).
   *
   * The specification requires that the return value must be related
   * to the sort order: e.g. negative means that 'this' comes before
   * 'that' in sort order.  We assume an ascending sort order.
   */

  /* XXX: could share code with duk_js_ops.c, duk_js_compare_helper */

  h1 = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h1 != NULL);

  h2 = duk_to_hstring(thr, 0);
  DUK_ASSERT(h2 != NULL);

  h1_len = (duk_size_t)DUK_HSTRING_GET_BYTELEN(h1);
  h2_len = (duk_size_t)DUK_HSTRING_GET_BYTELEN(h2);
  prefix_len = (h1_len <= h2_len ? h1_len : h2_len);

  rc = (duk_small_int_t)duk_memcmp((const void *)DUK_HSTRING_GET_DATA(h1),
                                   (const void *)DUK_HSTRING_GET_DATA(h2),
                                   (size_t)prefix_len);

  if (rc < 0) {
    ret = -1;
    goto done;
  } else if (rc > 0) {
    ret = 1;
    goto done;
  }

  /* prefix matches, lengths matter now */
  if (h1_len > h2_len) {
    ret = 1;
    goto done;
  } else if (h1_len == h2_len) {
    DUK_ASSERT(ret == 0);
    goto done;
  }
  ret = -1;
  goto done;

done:
  duk_push_int(thr, (duk_int_t)ret);
  return 1;
}

#if defined(DUK_USE_ES6)
DUK_INTERNAL duk_ret_t
duk_bi_string_prototype_startswith_endswith(duk_hthread *thr) {
  duk_int_t magic;
  duk_hstring *h;
  duk_hstring *h_search;
  duk_size_t blen_search;
  const duk_uint8_t *p_cmp_start;
  duk_bool_t result;

  h = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h != NULL);

  h_search = duk__str_tostring_notregexp(thr, 0);
  DUK_ASSERT(h_search != NULL);

  magic = duk_get_current_magic(thr);

  p_cmp_start = (const duk_uint8_t *)DUK_HSTRING_GET_DATA(h);
  blen_search = DUK_HSTRING_GET_BYTELEN(h_search);

  if (duk_is_undefined(thr, 1)) {
    if (magic) {
      p_cmp_start = p_cmp_start + DUK_HSTRING_GET_BYTELEN(h) - blen_search;
    } else {
      /* p_cmp_start already OK */
    }
  } else {
    duk_int_t len;
    duk_int_t pos;

    DUK_ASSERT(DUK_HSTRING_MAX_BYTELEN <= DUK_INT_MAX);
    len = (duk_int_t)DUK_HSTRING_GET_CHARLEN(h);
    pos = duk_to_int_clamped(thr, 1, 0, len);
    DUK_ASSERT(pos >= 0 && pos <= len);

    if (magic) {
      p_cmp_start -=
          blen_search; /* Conceptually subtracted last, but do already here. */
    }
    DUK_ASSERT(pos >= 0 && pos <= len);

    p_cmp_start +=
        duk_heap_strcache_offset_char2byte(thr, h, (duk_uint_fast32_t)pos);
  }

  /* The main comparison can be done using a memcmp() rather than
   * doing codepoint comparisons: for CESU-8 strings there is a
   * canonical representation for every codepoint.  But we do need
   * to deal with the char/byte offset translation to find the
   * comparison range.
   */

  result = 0;
  if (p_cmp_start >= DUK_HSTRING_GET_DATA(h) &&
      (duk_size_t)(p_cmp_start - (const duk_uint8_t *)DUK_HSTRING_GET_DATA(h)) +
              blen_search <=
          DUK_HSTRING_GET_BYTELEN(h)) {
    if (duk_memcmp((const void *)p_cmp_start,
                   (const void *)DUK_HSTRING_GET_DATA(h_search),
                   (size_t)blen_search) == 0) {
      result = 1;
    }
  }

  duk_push_boolean(thr, result);
  return 1;
}
#endif /* DUK_USE_ES6 */

#if defined(DUK_USE_ES6)
DUK_INTERNAL duk_ret_t duk_bi_string_prototype_includes(duk_hthread *thr) {
  duk_hstring *h;
  duk_hstring *h_search;
  duk_int_t len;
  duk_int_t pos;

  h = duk_push_this_coercible_to_string(thr);
  DUK_ASSERT(h != NULL);

  h_search = duk__str_tostring_notregexp(thr, 0);
  DUK_ASSERT(h_search != NULL);

  len = (duk_int_t)DUK_HSTRING_GET_CHARLEN(h);
  pos = duk_to_int_clamped(thr, 1, 0, len);
  DUK_ASSERT(pos >= 0 && pos <= len);

  pos = duk__str_search_shared(thr, h, h_search, pos, 0 /*backwards*/);
  duk_push_boolean(thr, pos >= 0);
  return 1;
}
#endif /* DUK_USE_ES6 */
#endif /* DUK_USE_STRING_BUILTIN */