decodeCompactArray = (encoded, decodePassable, skip = 0) => {
  const elements = [];
  let depth = 0;
  // Scan encoded rather than its tail to avoid slow `substring` in XS.
  // https://github.com/endojs/endo/issues/1984
  let nextIndex = skip + 1;
  let currentElementStart = skip + 1;
  for (const { 0: ch, index: i } of encoded.matchAll(/[\^ ]/g)) {
    const index = /** @type {number} */ (i);
    if (index <= skip) {
      if (index === skip) {
        ch === '^' || Fail`Encoded array expected: ${getSuffix(encoded, skip)}`;
      }
    } else if (ch === '^') {
      // This is the start of a nested array.
      // TODO: Since the syntax of nested arrays must be validated as part of
      // decoding the outer one, consider decoding them here into a shared cache
      // rather than discarding information about their contents until the later
      // decodePassable.
      depth += 1;
    } else {
      // This is a terminated element.
      if (index === nextIndex) {
        // A terminator after `[` or an another terminator indicates that an array is done.
        depth -= 1;
        depth >= 0 ||
          // prettier-ignore
          Fail`unexpected array element terminator: ${encoded.slice(skip, index + 2)}`;
      }
      if (depth === 0) {
        // We have a complete element of the topmost array.
        elements.push(
          decodePassable(encoded.slice(currentElementStart, index)),
        );
        currentElementStart = index + 1;
      }
    }
    // Advance the index.
    nextIndex = index + 1;
  }
  depth === 0 || Fail`unterminated array: ${getSuffix(encoded, skip)}`;
  nextIndex === encoded.length ||
    Fail`unterminated array element: ${getSuffix(
      encoded,
      currentElementStart,
    )}`;
  return harden(elements);
}
@param {string} encoded
@param {(encoded: string) => Passable} decodePassable
@param {number} [skip]
@returns {Array}