optimizeValue(value, depth) {
    // Don't generate very deep expressions, as the backend rejects them.
    // The backend's limit is 100, and we want to stay well away from that
    // as a few extra levels of wrapping are always added.
    const DEPTH_LIMIT = 50;

    const isConst = (v) => v.constantValue !== null;
    const serializeConst = (v) => v === ee.apiclient.NULL_VALUE ? null : v;
    // Ensure that any derived ValueNode from a parent node is associated with
    // its parent's source frame, if available, in order to make sure that the
    // final, top-level ValueNodes in the expression are contained in the
    // sourceNodeMap. If the optimizer encounters duplicate ValueNodes, it will
    // retain the reference to the first one found.
    const storeInSourceMap = (parentValue, valueNode) => {
      if (this.sourceNodeMap && this.sourceNodeMap.has(parentValue) &&
          !this.sourceNodeMap.has(valueNode)) {
        this.sourceNodeMap.set(valueNode, this.sourceNodeMap.get(parentValue));
      }
      return valueNode;
    };

    if (isConst(value) || value.integerValue != null ||
        value.bytesValue != null || value.argumentReference != null) {
      return value;
    } else if (value.valueReference != null) {
      const referencedValue = this.values[value.valueReference];

      if (this.referenceCounts === null || (depth < DEPTH_LIMIT &&
          this.referenceCounts[value.valueReference] === 1)) {
        const optimized = this.optimizeValue(referencedValue, depth);
        return storeInSourceMap(value, optimized);
      } else if (ExpressionOptimizer.isAlwaysLiftable(referencedValue)) {
        return storeInSourceMap(value, referencedValue);
      } else {
        const optimized =
            ee.rpc_node.reference(this.optimizeReference(value.valueReference));
        return storeInSourceMap(value, optimized);
      }
    } else if (value.arrayValue != null) {
      const arr = value.arrayValue.values.map(
          v => this.optimizeValue(v, depth + 3));
      const optimized =
          (arr.every(isConst) ? ee.rpc_node.constant(arr.map(
                                    v => serializeConst(v.constantValue))) :
                                ee.rpc_node.array(arr));
      return storeInSourceMap(value, optimized);
    } else if (value.dictionaryValue != null) {
      const values = {};
      let constantValues = {};
      for (const [k, v] of Object.entries(value.dictionaryValue.values || {})) {
        values[k] = this.optimizeValue(
            /** @type {!ee.api.ValueNode} */ (v), depth + 3);
        if (constantValues !== null && isConst(values[k])) {
          constantValues[k] = serializeConst(values[k].constantValue);
        } else {
          constantValues = null;
        }
      }
      if (constantValues !== null) {
        return storeInSourceMap(value, ee.rpc_node.constant(constantValues));
      } else {
        return storeInSourceMap(values, ee.rpc_node.dictionary(values));
      }
    } else if (value.functionDefinitionValue != null) {
      const def = value.functionDefinitionValue;
      const optimized = ee.rpc_node.functionDefinition(
          def.argumentNames || [], this.optimizeReference(def.body || ''));
      return storeInSourceMap(value, optimized);
    } else if (value.functionInvocationValue != null) {
      const inv = value.functionInvocationValue;
      const args = {};
      for (const k of Object.keys(inv.arguments || {})) {
        args[k] = this.optimizeValue(inv.arguments[k], depth + 3);
      }
      const optimized =
          (inv.functionName ?
               ee.rpc_node.functionByName(inv.functionName, args) :
               ee.rpc_node.functionByReference(
                   this.optimizeReference(inv.functionReference || ''), args));
      return storeInSourceMap(value, optimized);
    }
    throw Error('Can\'t optimize value: ' + value);
  }
@param {!ee.api.ValueNode} value
@param {number} depth
@return {!ee.api.ValueNode}