makeEncodeToCapData = (encodeOptions = {}) => {
  const {
    encodeRemotableToCapData = dontEncodeRemotableToCapData,
    encodePromiseToCapData = dontEncodePromiseToCapData,
    encodeErrorToCapData = dontEncodeErrorToCapData,
  } = encodeOptions;

  /**
   * Must encode `val` into plain JSON data *canonically*, such that
   * `JSON.stringify(encode(v1)) === JSON.stringify(encode(v1))`. For most
   * encodings, the order of properties of each node of the output
   * structure is determined by the algorithm below without special
   * arrangement, usually by being expressed directly as an object literal.
   * The exception is copyRecords, whose natural enumeration order
   * can differ between copyRecords that our distributed object semantics
   * considers to be equivalent.
   * Since, for each copyRecord, we only accept string property names,
   * not symbols, we can canonically sort the names first.
   * JSON.stringify will then visit these in that sorted order.
   *
   * Encoding with a canonical-JSON encoder would also solve this canonicalness
   * problem in a more modular and encapsulated manner. Note that the
   * actual order produced here, though it agrees with canonical-JSON on
   * copyRecord property ordering, differs from canonical-JSON as a whole
   * in that the other record properties are visited in the order in which
   * they are literally written below. TODO perhaps we should indeed switch
   * to a canonical JSON encoder, and not delicately depend on the order
   * in which these object literals are written.
   *
   * Readers must not care about this order anyway. We impose this requirement
   * mainly to reduce non-determinism exposed outside a vat.
   *
   * @param {any} passable
   * @returns {Encoding} except that `encodeToCapData` does not generally
   * `harden` this result before returning. Rather, `encodeToCapData` is not
   * directly exposed.
   * What's exposed instead is a wrapper that freezes the output before
   * returning. If this turns out to impede static analysis for `harden` safety,
   * we can always put the (now redundant) hardens back in. They don't hurt.
   */
  const encodeToCapDataRecur = passable => {
    // First we handle all primitives. Some can be represented directly as
    // JSON, and some must be encoded as [QCLASS] composites.
    const passStyle = passStyleOf(passable);
    switch (passStyle) {
      case 'null':
      case 'boolean':
      case 'string': {
        // pass through to JSON
        return passable;
      }
      case 'undefined': {
        return { [QCLASS]: 'undefined' };
      }
      case 'number': {
        // Special-case numbers with no digit-based representation.
        if (Number.isNaN(passable)) {
          return { [QCLASS]: 'NaN' };
        } else if (passable === Infinity) {
          return { [QCLASS]: 'Infinity' };
        } else if (passable === -Infinity) {
          return { [QCLASS]: '-Infinity' };
        }
        // Pass through everything else, replacing -0 with 0.
        return is(passable, -0) ? 0 : passable;
      }
      case 'bigint': {
        return {
          [QCLASS]: 'bigint',
          digits: String(passable),
        };
      }
      case 'symbol': {
        assertPassableSymbol(passable);
        const name = /** @type {string} */ (nameForPassableSymbol(passable));
        return {
          [QCLASS]: 'symbol',
          name,
        };
      }
      case 'copyRecord': {
        if (hasOwnPropertyOf(passable, QCLASS)) {
          // Hilbert hotel
          const { [QCLASS]: qclassValue, ...rest } = passable;
          /** @type {Encoding} */
          const result = {
            [QCLASS]: 'hilbert',
            original: encodeToCapDataRecur(qclassValue),
          };
          if (ownKeys(rest).length >= 1) {
            // We harden the entire capData encoding before we return it.
            // `encodeToCapData` requires that its input be Passable, and
            // therefore hardened.
            // The `freeze` here is needed anyway, because the `rest` is
            // freshly constructed by the `...` above, and we're using it
            // as imput in another call to `encodeToCapData`.
            result.rest = encodeToCapDataRecur(freeze(rest));
          }
          return result;
        }
        // Currently copyRecord allows only string keys so this will
        // work. If we allow sortable symbol keys, this will need to
        // become more interesting.
        const names = ownKeys(passable).sort();
        return fromEntries(
          names.map(name => [name, encodeToCapDataRecur(passable[name])]),
        );
      }
      case 'copyArray': {
        return passable.map(encodeToCapDataRecur);
      }
      case 'byteArray': {
        // TODO implement
        throw Fail`marsal of byteArray not yet implemented: ${passable}`;
      }
      case 'tagged': {
        return {
          [QCLASS]: 'tagged',
          tag: getTag(passable),
          payload: encodeToCapDataRecur(passable.payload),
        };
      }
      case 'remotable': {
        const encoded = encodeRemotableToCapData(
          passable,
          encodeToCapDataRecur,
        );
        if (qclassMatches(encoded, 'slot')) {
          return encoded;
        }
        // `throw` is noop since `Fail` throws. But linter confused
        throw Fail`internal: Remotable encoding must be an object with ${q(
          QCLASS,
        )} ${q('slot')}: ${encoded}`;
      }
      case 'promise': {
        const encoded = encodePromiseToCapData(passable, encodeToCapDataRecur);
        if (qclassMatches(encoded, 'slot')) {
          return encoded;
        }
        throw Fail`internal: Promise encoding must be an object with ${q(
          QCLASS,
          'slot',
        )}: ${encoded}`;
      }
      case 'error': {
        const encoded = encodeErrorToCapData(passable, encodeToCapDataRecur);
        if (qclassMatches(encoded, 'error')) {
          return encoded;
        }
        throw Fail`internal: Error encoding must be an object with ${q(
          QCLASS,
          'error',
        )}: ${encoded}`;
      }
      default: {
        throw assert.fail(
          X`internal: Unrecognized passStyle ${q(passStyle)}`,
          TypeError,
        );
      }
    }
  };
  const encodeToCapData = passable => {
    if (isErrorLike(passable)) {
      // We pull out this special case to accommodate errors that are not
      // valid Passables. For example, because they're not frozen.
      // The special case can only ever apply at the root, and therefore
      // outside the recursion, since an error could only be deeper in
      // a passable structure if it were passable.
      //
      // We pull out this special case because, for these errors, we're much
      // more interested in reporting whatever diagnostic information they
      // carry than we are about reporting problems encountered in reporting
      // this information.
      return harden(encodeErrorToCapData(passable, encodeToCapDataRecur));
    }
    return harden(encodeToCapDataRecur(passable));
  };
  return harden(encodeToCapData);
}
@param {EncodeToCapDataOptions} [encodeOptions]
@returns {(passable: Passable) => Encoding}