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,
        );
      }
    }
  }
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.