Browse Source

Project import generated by Copybara

PiperOrigin-RevId: 310497816
Daniel Kurka 5 years ago
parent
commit
ed596ef68d

+ 4 - 93
js/experimental/runtime/kernel/conformance/conformance_testee.js

@@ -8,7 +8,8 @@ const BinaryStorage = goog.require('protobuf.runtime.BinaryStorage');
 const BufferDecoder = goog.require('protobuf.binary.BufferDecoder');
 const BufferDecoder = goog.require('protobuf.binary.BufferDecoder');
 const WireType = goog.require('protobuf.binary.WireType');
 const WireType = goog.require('protobuf.binary.WireType');
 const {Field} = goog.require('protobuf.binary.field');
 const {Field} = goog.require('protobuf.binary.field');
-const {checkCriticalElementIndex, checkCriticalState} = goog.require('protobuf.internal.checks');
+const {checkCriticalState} = goog.require('protobuf.internal.checks');
+const {skipField, tagToFieldNumber, tagToWireType} = goog.require('protobuf.binary.tag');
 
 
 /**
 /**
  * Appends a new entry in the index array for the given field number.
  * Appends a new entry in the index array for the given field number.
@@ -26,26 +27,6 @@ function addIndexEntry(storage, fieldNumber, wireType, startIndex) {
   }
   }
 }
 }
 
 
-/**
- * Returns wire type stored in a tag.
- * Protos store the wire type as the first 3 bit of a tag.
- * @param {number} tag
- * @return {!WireType}
- */
-function tagToWireType(tag) {
-  return /** @type {!WireType} */ (tag & 0x07);
-}
-
-/**
- * Returns the field number stored in a tag.
- * Protos store the field number in the upper 29 bits of a 32 bit number.
- * @param {number} tag
- * @return {number}
- */
-function tagToFieldNumber(tag) {
-  return tag >>> 3;
-}
-
 /**
 /**
  * Creates an index of field locations in a given binary protobuf.
  * Creates an index of field locations in a given binary protobuf.
  * @param {!BufferDecoder} bufferDecoder
  * @param {!BufferDecoder} bufferDecoder
@@ -62,83 +43,13 @@ function buildIndex(bufferDecoder, pivot) {
     const wireType = tagToWireType(tag);
     const wireType = tagToWireType(tag);
     const fieldNumber = tagToFieldNumber(tag);
     const fieldNumber = tagToFieldNumber(tag);
     checkCriticalState(fieldNumber > 0, `Invalid field number ${fieldNumber}`);
     checkCriticalState(fieldNumber > 0, `Invalid field number ${fieldNumber}`);
-
     addIndexEntry(storage, fieldNumber, wireType, bufferDecoder.cursor());
     addIndexEntry(storage, fieldNumber, wireType, bufferDecoder.cursor());
-
-    checkCriticalState(
-        !skipField_(bufferDecoder, wireType, fieldNumber),
-        'Found unmatched stop group.');
+    skipField(bufferDecoder, wireType, fieldNumber);
   }
   }
   return storage;
   return storage;
 }
 }
 
 
-/**
- * Skips over fields until the next field of the message.
- * @param {!BufferDecoder} bufferDecoder
- * @param {!WireType} wireType
- * @param {number} fieldNumber
- * @return {boolean} Whether the field we skipped over was a stop group.
- * @private
- */
-function skipField_(bufferDecoder, wireType, fieldNumber) {
-  switch (wireType) {
-    case WireType.VARINT:
-      checkCriticalElementIndex(
-          bufferDecoder.cursor(), bufferDecoder.endIndex());
-      bufferDecoder.skipVarint();
-      return false;
-    case WireType.FIXED64:
-      bufferDecoder.skip(8);
-      return false;
-    case WireType.DELIMITED:
-      checkCriticalElementIndex(
-          bufferDecoder.cursor(), bufferDecoder.endIndex());
-      const length = bufferDecoder.getUnsignedVarint32();
-      bufferDecoder.skip(length);
-      return false;
-    case WireType.START_GROUP:
-      checkCriticalState(
-          skipGroup_(bufferDecoder, fieldNumber), 'No end group found.');
-      return false;
-    case WireType.END_GROUP:
-      // Signal that we found a stop group to the caller
-      return true;
-    case WireType.FIXED32:
-      bufferDecoder.skip(4);
-      return false;
-    default:
-      throw new Error(`Invalid wire type: ${wireType}`);
-  }
-}
-
-/**
- * Skips over fields until it finds the end of a given group.
- * @param {!BufferDecoder} bufferDecoder
- * @param {number} groupFieldNumber
- * @return {boolean} Returns true if an end was found.
- * @private
- */
-function skipGroup_(bufferDecoder, groupFieldNumber) {
-  // On a start group we need to keep skipping fields until we find a
-  // corresponding stop group
-  // Note: Since we are calling skipField from here nested groups will be
-  // handled by recursion of this method and thus we will not see a nested
-  // STOP GROUP here unless there is something wrong with the input data.
-  while (bufferDecoder.hasNext()) {
-    const tag = bufferDecoder.getUnsignedVarint32();
-    const wireType = tagToWireType(tag);
-    const fieldNumber = tagToFieldNumber(tag);
-
-    if (skipField_(bufferDecoder, wireType, fieldNumber)) {
-      checkCriticalState(
-          groupFieldNumber === fieldNumber,
-          `Expected stop group for fieldnumber ${groupFieldNumber} not found.`);
-      return true;
-    }
-  }
-  return false;
-}
-
 exports = {
 exports = {
   buildIndex,
   buildIndex,
+  tagToWireType,
 };
 };

+ 4 - 29
js/experimental/runtime/kernel/indexer_test.js

@@ -72,12 +72,12 @@ describe('Indexer does', () => {
 
 
   it('fail for invalid wire type (6)', () => {
   it('fail for invalid wire type (6)', () => {
     expect(() => buildIndex(createBufferDecoder(0x0E, 0x01), PIVOT))
     expect(() => buildIndex(createBufferDecoder(0x0E, 0x01), PIVOT))
-        .toThrowError('Invalid wire type: 6');
+        .toThrowError('Unexpected wire type: 6');
   });
   });
 
 
   it('fail for invalid wire type (7)', () => {
   it('fail for invalid wire type (7)', () => {
     expect(() => buildIndex(createBufferDecoder(0x0F, 0x01), PIVOT))
     expect(() => buildIndex(createBufferDecoder(0x0F, 0x01), PIVOT))
-        .toThrowError('Invalid wire type: 7');
+        .toThrowError('Unexpected wire type: 7');
   });
   });
 
 
   it('index varint', () => {
   it('index varint', () => {
@@ -269,33 +269,8 @@ describe('Indexer does', () => {
 
 
   it('fail on unmatched stop group', () => {
   it('fail on unmatched stop group', () => {
     const data = createBufferDecoder(0x0C, 0x01);
     const data = createBufferDecoder(0x0C, 0x01);
-    if (CHECK_CRITICAL_STATE) {
-      expect(() => buildIndex(data, PIVOT))
-          .toThrowError('Found unmatched stop group.');
-    } else {
-      // Note in unchecked mode we produce invalid output for invalid inputs.
-      // This test just documents our behavior in those cases.
-      // These values might change at any point and are not considered
-      // what the implementation should be doing here.
-      const storage = buildIndex(data, PIVOT);
-
-      expect(getStorageSize(storage)).toBe(1);
-      const entryArray = storage.get(1).getIndexArray();
-      expect(entryArray).not.toBeUndefined();
-      expect(entryArray.length).toBe(1);
-      const entry = entryArray[0];
-
-      expect(Field.getWireType(entry)).toBe(WireType.END_GROUP);
-      expect(Field.getStartIndex(entry)).toBe(1);
-
-      const entryArray2 = storage.get(0).getIndexArray();
-      expect(entryArray2).not.toBeUndefined();
-      expect(entryArray2.length).toBe(1);
-      const entry2 = entryArray2[0];
-
-      expect(Field.getWireType(entry2)).toBe(WireType.FIXED64);
-      expect(Field.getStartIndex(entry2)).toBe(2);
-    }
+    expect(() => buildIndex(data, PIVOT))
+        .toThrowError('Unexpected wire type: 4');
   });
   });
 
 
   it('fail for groups without matching stop group', () => {
   it('fail for groups without matching stop group', () => {

+ 312 - 1
js/experimental/runtime/kernel/kernel.js

@@ -26,6 +26,7 @@ const reader = goog.require('protobuf.binary.reader');
 const {CHECK_TYPE, checkCriticalElementIndex, checkCriticalState, checkCriticalType, checkCriticalTypeBool, checkCriticalTypeBoolArray, checkCriticalTypeByteString, checkCriticalTypeByteStringArray, checkCriticalTypeDouble, checkCriticalTypeDoubleArray, checkCriticalTypeFloat, checkCriticalTypeFloatIterable, checkCriticalTypeMessageArray, checkCriticalTypeSignedInt32, checkCriticalTypeSignedInt32Array, checkCriticalTypeSignedInt64, checkCriticalTypeSignedInt64Array, checkCriticalTypeString, checkCriticalTypeStringArray, checkCriticalTypeUnsignedInt32, checkCriticalTypeUnsignedInt32Array, checkDefAndNotNull, checkElementIndex, checkFieldNumber, checkFunctionExists, checkState, checkTypeDouble, checkTypeFloat, checkTypeSignedInt32, checkTypeSignedInt64, checkTypeUnsignedInt32} = goog.require('protobuf.internal.checks');
 const {CHECK_TYPE, checkCriticalElementIndex, checkCriticalState, checkCriticalType, checkCriticalTypeBool, checkCriticalTypeBoolArray, checkCriticalTypeByteString, checkCriticalTypeByteStringArray, checkCriticalTypeDouble, checkCriticalTypeDoubleArray, checkCriticalTypeFloat, checkCriticalTypeFloatIterable, checkCriticalTypeMessageArray, checkCriticalTypeSignedInt32, checkCriticalTypeSignedInt32Array, checkCriticalTypeSignedInt64, checkCriticalTypeSignedInt64Array, checkCriticalTypeString, checkCriticalTypeStringArray, checkCriticalTypeUnsignedInt32, checkCriticalTypeUnsignedInt32Array, checkDefAndNotNull, checkElementIndex, checkFieldNumber, checkFunctionExists, checkState, checkTypeDouble, checkTypeFloat, checkTypeSignedInt32, checkTypeSignedInt64, checkTypeUnsignedInt32} = goog.require('protobuf.internal.checks');
 const {Field, IndexEntry} = goog.require('protobuf.binary.field');
 const {Field, IndexEntry} = goog.require('protobuf.binary.field');
 const {buildIndex} = goog.require('protobuf.binary.indexer');
 const {buildIndex} = goog.require('protobuf.binary.indexer');
+const {createTag, get32BitVarintLength, getTagLength} = goog.require('protobuf.binary.tag');
 
 
 
 
 /**
 /**
@@ -139,6 +140,28 @@ function readRepeatedNonPrimitive(indexArray, bufferDecoder, singularReadFunc) {
   return result;
   return result;
 }
 }
 
 
+/**
+ * Converts all entries of the index array to the template type using the given
+ * read function and return an Array containing those converted values. This is
+ * used to implement parsing repeated non-primitive fields.
+ * @param {!Array<!IndexEntry>} indexArray
+ * @param {!BufferDecoder} bufferDecoder
+ * @param {number} fieldNumber
+ * @param {function(!Kernel):T} instanceCreator
+ * @param {number=} pivot
+ * @return {!Array<T>}
+ * @template T
+ */
+function readRepeatedGroup(
+    indexArray, bufferDecoder, fieldNumber, instanceCreator, pivot) {
+  const result = new Array(indexArray.length);
+  for (let i = 0; i < indexArray.length; i++) {
+    result[i] = doReadGroup(
+        bufferDecoder, indexArray[i], fieldNumber, instanceCreator, pivot);
+  }
+  return result;
+}
+
 /**
 /**
  * Creates a new bytes array to contain all data of a submessage.
  * Creates a new bytes array to contain all data of a submessage.
  * When there are multiple entries, merge them together.
  * When there are multiple entries, merge them together.
@@ -193,6 +216,51 @@ function readMessage(indexArray, bufferDecoder, instanceCreator, pivot) {
   return instanceCreator(accessor);
   return instanceCreator(accessor);
 }
 }
 
 
+/**
+ * Merges all index entries of the index array using the given read function.
+ * This is used to implement parsing singular group fields.
+ * @param {!Array<!IndexEntry>} indexArray
+ * @param {!BufferDecoder} bufferDecoder
+ * @param {number} fieldNumber
+ * @param {function(!Kernel):T} instanceCreator
+ * @param {number=} pivot
+ * @return {T}
+ * @template T
+ */
+function readGroup(
+    indexArray, bufferDecoder, fieldNumber, instanceCreator, pivot) {
+  checkInstanceCreator(instanceCreator);
+  checkState(indexArray.length > 0);
+  return doReadGroup(
+      bufferDecoder, indexArray[indexArray.length - 1], fieldNumber,
+      instanceCreator, pivot);
+}
+
+/**
+ * Merges all index entries of the index array using the given read function.
+ * This is used to implement parsing singular message fields.
+ * @param {!BufferDecoder} bufferDecoder
+ * @param {!IndexEntry} indexEntry
+ * @param {number} fieldNumber
+ * @param {function(!Kernel):T} instanceCreator
+ * @param {number=} pivot
+ * @return {T}
+ * @template T
+ */
+function doReadGroup(
+    bufferDecoder, indexEntry, fieldNumber, instanceCreator, pivot) {
+  validateWireType(indexEntry, WireType.START_GROUP);
+  const fieldStartIndex = Field.getStartIndex(indexEntry);
+  const tag = createTag(WireType.START_GROUP, fieldNumber);
+  const groupTagLength = get32BitVarintLength(tag);
+  const groupLength = getTagLength(
+      bufferDecoder, fieldStartIndex, WireType.START_GROUP, fieldNumber);
+  const accessorBuffer = bufferDecoder.subBufferDecoder(
+      fieldStartIndex, groupLength - groupTagLength);
+  const kernel = Kernel.fromBufferDecoder_(accessorBuffer, pivot);
+  return instanceCreator(kernel);
+}
+
 /**
 /**
  * @param {!Writer} writer
  * @param {!Writer} writer
  * @param {number} fieldNumber
  * @param {number} fieldNumber
@@ -203,6 +271,18 @@ function writeMessage(writer, fieldNumber, value) {
       fieldNumber, checkDefAndNotNull(value).internalGetKernel().serialize());
       fieldNumber, checkDefAndNotNull(value).internalGetKernel().serialize());
 }
 }
 
 
+/**
+ * @param {!Writer} writer
+ * @param {number} fieldNumber
+ * @param {?InternalMessage} value
+ */
+function writeGroup(writer, fieldNumber, value) {
+  const kernel = checkDefAndNotNull(value).internalGetKernel();
+  writer.writeStartGroup(fieldNumber);
+  kernel.serializeToWriter(writer);
+  writer.writeEndGroup(fieldNumber);
+}
+
 /**
 /**
  * Writes the array of Messages into the writer for the given field number.
  * Writes the array of Messages into the writer for the given field number.
  * @param {!Writer} writer
  * @param {!Writer} writer
@@ -215,6 +295,18 @@ function writeRepeatedMessage(writer, fieldNumber, values) {
   }
   }
 }
 }
 
 
+/**
+ * Writes the array of Messages into the writer for the given field number.
+ * @param {!Writer} writer
+ * @param {number} fieldNumber
+ * @param {!Array<!InternalMessage>} values
+ */
+function writeRepeatedGroup(writer, fieldNumber, values) {
+  for (const value of values) {
+    writeGroup(writer, fieldNumber, value);
+  }
+}
+
 /**
 /**
  * Array.from has a weird type definition in google3/javascript/externs/es6.js
  * Array.from has a weird type definition in google3/javascript/externs/es6.js
  * and wants the mapping function accept strings.
  * and wants the mapping function accept strings.
@@ -406,7 +498,8 @@ class Kernel {
           writer.writeTag(fieldNumber, Field.getWireType(indexEntry));
           writer.writeTag(fieldNumber, Field.getWireType(indexEntry));
           writer.writeBufferDecoder(
           writer.writeBufferDecoder(
               checkDefAndNotNull(this.bufferDecoder_),
               checkDefAndNotNull(this.bufferDecoder_),
-              Field.getStartIndex(indexEntry), Field.getWireType(indexEntry));
+              Field.getStartIndex(indexEntry), Field.getWireType(indexEntry),
+              fieldNumber);
         }
         }
       }
       }
     });
     });
@@ -690,6 +783,28 @@ class Kernel {
         writeMessage);
         writeMessage);
   }
   }
 
 
+  /**
+   * Returns data as a mutable proto Message for the given field number.
+   * If no value has been set, return null.
+   * If hasFieldNumber(fieldNumber) == false before calling, it remains false.
+   *
+   * This method should not be used along with getMessage, since calling
+   * getMessageOrNull after getMessage will not register the encoder.
+   *
+   * @param {number} fieldNumber
+   * @param {function(!Kernel):T} instanceCreator
+   * @param {number=} pivot
+   * @return {?T}
+   * @template T
+   */
+  getGroupOrNull(fieldNumber, instanceCreator, pivot = undefined) {
+    return this.getFieldWithDefault_(
+        fieldNumber, null,
+        (indexArray, bytes) =>
+            readGroup(indexArray, bytes, fieldNumber, instanceCreator, pivot),
+        writeGroup);
+  }
+
   /**
   /**
    * Returns data as a mutable proto Message for the given field number.
    * Returns data as a mutable proto Message for the given field number.
    * If no value has been set previously, creates and attaches an instance.
    * If no value has been set previously, creates and attaches an instance.
@@ -714,6 +829,30 @@ class Kernel {
     return instance;
     return instance;
   }
   }
 
 
+  /**
+   * Returns data as a mutable proto Message for the given field number.
+   * If no value has been set previously, creates and attaches an instance.
+   * Postcondition: hasFieldNumber(fieldNumber) == true.
+   *
+   * This method should not be used along with getMessage, since calling
+   * getMessageAttach after getMessage will not register the encoder.
+   *
+   * @param {number} fieldNumber
+   * @param {function(!Kernel):T} instanceCreator
+   * @param {number=} pivot
+   * @return {T}
+   * @template T
+   */
+  getGroupAttach(fieldNumber, instanceCreator, pivot = undefined) {
+    checkInstanceCreator(instanceCreator);
+    let instance = this.getGroupOrNull(fieldNumber, instanceCreator, pivot);
+    if (!instance) {
+      instance = instanceCreator(Kernel.createEmpty());
+      this.setField_(fieldNumber, instance, writeGroup);
+    }
+    return instance;
+  }
+
   /**
   /**
    * Returns data as a proto Message for the given field number.
    * Returns data as a proto Message for the given field number.
    * If no value has been set, return a default instance.
    * If no value has been set, return a default instance.
@@ -744,6 +883,36 @@ class Kernel {
     return message === null ? instanceCreator(Kernel.createEmpty()) : message;
     return message === null ? instanceCreator(Kernel.createEmpty()) : message;
   }
   }
 
 
+  /**
+   * Returns data as a proto Message for the given field number.
+   * If no value has been set, return a default instance.
+   * This default instance is guaranteed to be the same instance, unless this
+   * field is cleared.
+   * Does not register the encoder, so changes made to the returned
+   * sub-message will not be included when serializing the parent message.
+   * Use getMessageAttach() if the resulting sub-message should be mutable.
+   *
+   * This method should not be used along with getMessageOrNull or
+   * getMessageAttach, since these methods register the encoder.
+   *
+   * @param {number} fieldNumber
+   * @param {function(!Kernel):T} instanceCreator
+   * @param {number=} pivot
+   * @return {T}
+   * @template T
+   */
+  getGroup(fieldNumber, instanceCreator, pivot = undefined) {
+    checkInstanceCreator(instanceCreator);
+    const message = this.getFieldWithDefault_(
+        fieldNumber, null,
+        (indexArray, bytes) =>
+            readGroup(indexArray, bytes, fieldNumber, instanceCreator, pivot));
+    // Returns an empty message as the default value if the field doesn't exist.
+    // We don't pass the default value to getFieldWithDefault_ to reduce object
+    // allocation.
+    return message === null ? instanceCreator(Kernel.createEmpty()) : message;
+  }
+
   /**
   /**
    * Returns the accessor for the given singular message, or returns null if
    * Returns the accessor for the given singular message, or returns null if
    * it hasn't been set.
    * it hasn't been set.
@@ -1614,6 +1783,71 @@ class Kernel {
         .length;
         .length;
   }
   }
 
 
+  /**
+   * Returns an Array instance containing boolean values for the given field
+   * number.
+   * @param {number} fieldNumber
+   * @param {function(!Kernel):T} instanceCreator
+   * @param {number|undefined} pivot
+   * @return {!Array<T>}
+   * @template T
+   * @private
+   */
+  getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot) {
+    return this.getFieldWithDefault_(
+        fieldNumber, [],
+        (indexArray, bufferDecoder) => readRepeatedGroup(
+            indexArray, bufferDecoder, fieldNumber, instanceCreator, pivot),
+        writeRepeatedGroup);
+  }
+
+  /**
+   * Returns the element at index for the given field number as a group.
+   * @param {number} fieldNumber
+   * @param {function(!Kernel):T} instanceCreator
+   * @param {number} index
+   * @param {number=} pivot
+   * @return {T}
+   * @template T
+   */
+  getRepeatedGroupElement(
+      fieldNumber, instanceCreator, index, pivot = undefined) {
+    const array =
+        this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot);
+    checkCriticalElementIndex(index, array.length);
+    return array[index];
+  }
+
+  /**
+   * Returns an Iterable instance containing group values for the given field
+   * number.
+   * @param {number} fieldNumber
+   * @param {function(!Kernel):T} instanceCreator
+   * @param {number=} pivot
+   * @return {!Iterable<T>}
+   * @template T
+   */
+  getRepeatedGroupIterable(fieldNumber, instanceCreator, pivot = undefined) {
+    // Don't split this statement unless needed. JS compiler thinks
+    // getRepeatedMessageArray_ might have side effects and doesn't inline the
+    // call in the compiled code. See cl/293894484 for details.
+    return new ArrayIterable(
+        this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot));
+  }
+
+  /**
+   * Returns the size of the repeated field.
+   * @param {number} fieldNumber
+   * @param {function(!Kernel):T} instanceCreator
+   * @return {number}
+   * @param {number=} pivot
+   * @template T
+   */
+  getRepeatedGroupSize(fieldNumber, instanceCreator, pivot = undefined) {
+    return this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot)
+        .length;
+  }
+
   /***************************************************************************
   /***************************************************************************
    *                        OPTIONAL SETTER METHODS
    *                        OPTIONAL SETTER METHODS
    ***************************************************************************/
    ***************************************************************************/
@@ -1801,6 +2035,20 @@ class Kernel {
     this.setInt64(fieldNumber, value);
     this.setInt64(fieldNumber, value);
   }
   }
 
 
+  /**
+   * Sets a proto Group to the field with the given field number.
+   * Instead of working with the Kernel inside of the message directly, we
+   * need the message instance to keep its reference equality for subsequent
+   * gettings.
+   * @param {number} fieldNumber
+   * @param {!InternalMessage} value
+   */
+  setGroup(fieldNumber, value) {
+    checkCriticalType(
+        value !== null, 'Given value is not a message instance: null');
+    this.setField_(fieldNumber, value, writeGroup);
+  }
+
   /**
   /**
    * Sets a proto Message to the field with the given field number.
    * Sets a proto Message to the field with the given field number.
    * Instead of working with the Kernel inside of the message directly, we
    * Instead of working with the Kernel inside of the message directly, we
@@ -3806,6 +4054,69 @@ class Kernel {
     this.addRepeatedMessageIterable(
     this.addRepeatedMessageIterable(
         fieldNumber, [value], instanceCreator, pivot);
         fieldNumber, [value], instanceCreator, pivot);
   }
   }
+
+  // Groups
+  /**
+   * Sets all message values into the field for the given field number.
+   * @param {number} fieldNumber
+   * @param {!Iterable<!InternalMessage>} values
+   */
+  setRepeatedGroupIterable(fieldNumber, values) {
+    const /** !Array<!InternalMessage> */ array = Array.from(values);
+    checkCriticalTypeMessageArray(array);
+    this.setField_(fieldNumber, array, writeRepeatedGroup);
+  }
+
+  /**
+   * Adds all message values into the field for the given field number.
+   * @param {number} fieldNumber
+   * @param {!Iterable<!InternalMessage>} values
+   * @param {function(!Kernel):!InternalMessage} instanceCreator
+   * @param {number=} pivot
+   */
+  addRepeatedGroupIterable(
+      fieldNumber, values, instanceCreator, pivot = undefined) {
+    const array = [
+      ...this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot),
+      ...values,
+    ];
+    checkCriticalTypeMessageArray(array);
+    // Needs to set it back with the new array.
+    this.setField_(fieldNumber, array, writeRepeatedGroup);
+  }
+
+  /**
+   * Sets a single message value into the field for the given field number at
+   * the given index.
+   * @param {number} fieldNumber
+   * @param {!InternalMessage} value
+   * @param {function(!Kernel):!InternalMessage} instanceCreator
+   * @param {number} index
+   * @param {number=} pivot
+   * @throws {!Error} if index is out of range when check mode is critical
+   */
+  setRepeatedGroupElement(
+      fieldNumber, value, instanceCreator, index, pivot = undefined) {
+    checkInstanceCreator(instanceCreator);
+    checkCriticalType(
+        value !== null, 'Given value is not a message instance: null');
+    const array =
+        this.getRepeatedGroupArray_(fieldNumber, instanceCreator, pivot);
+    checkCriticalElementIndex(index, array.length);
+    array[index] = value;
+  }
+
+  /**
+   * Adds a single message value into the field for the given field number.
+   * @param {number} fieldNumber
+   * @param {!InternalMessage} value
+   * @param {function(!Kernel):!InternalMessage} instanceCreator
+   * @param {number=} pivot
+   */
+  addRepeatedGroupElement(
+      fieldNumber, value, instanceCreator, pivot = undefined) {
+    this.addRepeatedGroupIterable(fieldNumber, [value], instanceCreator, pivot);
+  }
 }
 }
 
 
 exports = Kernel;
 exports = Kernel;

+ 380 - 0
js/experimental/runtime/kernel/kernel_repeated_test.js

@@ -7425,3 +7425,383 @@ describe('Kernel for repeated message does', () => {
     }
     }
   });
   });
 });
 });
+
+describe('Kernel for repeated groups does', () => {
+  it('return empty array for the empty input', () => {
+    const accessor = Kernel.createEmpty();
+    expectEqualToArray(
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator), []);
+  });
+
+  it('ensure not the same instance returned for the empty input', () => {
+    const accessor = Kernel.createEmpty();
+    const list1 =
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+    const list2 =
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+    expect(list1).not.toBe(list2);
+  });
+
+  it('return size for the empty input', () => {
+    const accessor = Kernel.createEmpty();
+    expect(accessor.getRepeatedGroupSize(1, TestMessage.instanceCreator))
+        .toEqual(0);
+  });
+
+  it('return values from the input', () => {
+    const bytes1 = createArrayBuffer(0x08, 0x01);
+    const bytes2 = createArrayBuffer(0x08, 0x02);
+    const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1));
+    const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2));
+
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    expectEqualToMessageArray(
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator),
+        [msg1, msg2]);
+  });
+
+  it('ensure not the same array instance returned', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const list1 =
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+    const list2 =
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+    expect(list1).not.toBe(list2);
+  });
+
+  it('ensure the same array element returned for get iterable', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const list1 =
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+    const list2 = accessor.getRepeatedGroupIterable(
+        1, TestMessage.instanceCreator, /* pivot= */ 0);
+    const array1 = Array.from(list1);
+    const array2 = Array.from(list2);
+    for (let i = 0; i < array1.length; i++) {
+      expect(array1[i]).toBe(array2[i]);
+    }
+  });
+
+  it('return accessors from the input', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const [accessor1, accessor2] =
+        [...accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator)];
+    expect(accessor1.getInt32WithDefault(1)).toEqual(1);
+    expect(accessor2.getInt32WithDefault(1)).toEqual(2);
+  });
+
+  it('return accessors from the input when pivot is set', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const [accessor1, accessor2] = [...accessor.getRepeatedGroupIterable(
+        1, TestMessage.instanceCreator, /* pivot= */ 0)];
+    expect(accessor1.getInt32WithDefault(1)).toEqual(1);
+    expect(accessor2.getInt32WithDefault(1)).toEqual(2);
+  });
+
+  it('return the repeated field element from the input', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const msg1 = accessor.getRepeatedGroupElement(
+        /* fieldNumber= */ 1, TestMessage.instanceCreator,
+        /* index= */ 0);
+    const msg2 = accessor.getRepeatedGroupElement(
+        /* fieldNumber= */ 1, TestMessage.instanceCreator,
+        /* index= */ 1, /* pivot= */ 0);
+    expect(msg1.getInt32WithDefault(
+               /* fieldNumber= */ 1, /* default= */ 0))
+        .toEqual(1);
+    expect(msg2.getInt32WithDefault(
+               /* fieldNumber= */ 1, /* default= */ 0))
+        .toEqual(2);
+  });
+
+  it('ensure the same array element returned', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const msg1 = accessor.getRepeatedGroupElement(
+        /* fieldNumber= */ 1, TestMessage.instanceCreator,
+        /* index= */ 0);
+    const msg2 = accessor.getRepeatedGroupElement(
+        /* fieldNumber= */ 1, TestMessage.instanceCreator,
+        /* index= */ 0);
+    expect(msg1).toBe(msg2);
+  });
+
+  it('return the size from the input', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    expect(accessor.getRepeatedGroupSize(1, TestMessage.instanceCreator))
+        .toEqual(2);
+  });
+
+  it('encode repeated message from the input', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    expect(accessor.serialize()).toEqual(bytes);
+  });
+
+  it('add a single value', () => {
+    const accessor = Kernel.createEmpty();
+    const bytes1 = createArrayBuffer(0x08, 0x01);
+    const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1));
+    const bytes2 = createArrayBuffer(0x08, 0x02);
+    const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2));
+
+    accessor.addRepeatedGroupElement(1, msg1, TestMessage.instanceCreator);
+    accessor.addRepeatedGroupElement(1, msg2, TestMessage.instanceCreator);
+    const result =
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+
+    expect(Array.from(result)).toEqual([msg1, msg2]);
+  });
+
+  it('add values', () => {
+    const accessor = Kernel.createEmpty();
+    const bytes1 = createArrayBuffer(0x08, 0x01);
+    const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1));
+    const bytes2 = createArrayBuffer(0x08, 0x02);
+    const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2));
+
+    accessor.addRepeatedGroupIterable(1, [msg1], TestMessage.instanceCreator);
+    accessor.addRepeatedGroupIterable(1, [msg2], TestMessage.instanceCreator);
+    const result =
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+
+    expect(Array.from(result)).toEqual([msg1, msg2]);
+  });
+
+  it('set a single value', () => {
+    const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const subbytes = createArrayBuffer(0x08, 0x01);
+    const submsg = new TestMessage(Kernel.fromArrayBuffer(subbytes));
+
+    accessor.setRepeatedGroupElement(
+        /* fieldNumber= */ 1, submsg, TestMessage.instanceCreator,
+        /* index= */ 0);
+    const result =
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+
+    expect(Array.from(result)).toEqual([submsg]);
+  });
+
+  it('write submessage changes made via getRepeatedGroupElement', () => {
+    const bytes = createArrayBuffer(0x0B, 0x08, 0x05, 0x0C);
+    const expected = createArrayBuffer(0x0B, 0x08, 0x00, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const submsg = accessor.getRepeatedGroupElement(
+        /* fieldNumber= */ 1, TestMessage.instanceCreator,
+        /* index= */ 0);
+    expect(submsg.getInt32WithDefault(1, 0)).toEqual(5);
+    submsg.setInt32(1, 0);
+
+    expect(accessor.serialize()).toEqual(expected);
+  });
+
+  it('set values', () => {
+    const accessor = Kernel.createEmpty();
+    const subbytes = createArrayBuffer(0x08, 0x01);
+    const submsg = new TestMessage(Kernel.fromArrayBuffer(subbytes));
+
+    accessor.setRepeatedGroupIterable(1, [submsg]);
+    const result =
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+
+    expect(Array.from(result)).toEqual([submsg]);
+  });
+
+  it('encode for adding single value', () => {
+    const accessor = Kernel.createEmpty();
+    const bytes1 = createArrayBuffer(0x08, 0x01);
+    const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1));
+    const bytes2 = createArrayBuffer(0x08, 0x00);
+    const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2));
+    const expected =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x00, 0x0C);
+
+    accessor.addRepeatedGroupElement(1, msg1, TestMessage.instanceCreator);
+    accessor.addRepeatedGroupElement(1, msg2, TestMessage.instanceCreator);
+    const result = accessor.serialize();
+
+    expect(result).toEqual(expected);
+  });
+
+  it('encode for adding values', () => {
+    const accessor = Kernel.createEmpty();
+    const bytes1 = createArrayBuffer(0x08, 0x01);
+    const msg1 = new TestMessage(Kernel.fromArrayBuffer(bytes1));
+    const bytes2 = createArrayBuffer(0x08, 0x00);
+    const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2));
+    const expected =
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C, 0x0B, 0x08, 0x00, 0x0C);
+
+    accessor.addRepeatedGroupIterable(
+        1, [msg1, msg2], TestMessage.instanceCreator);
+    const result = accessor.serialize();
+
+    expect(result).toEqual(expected);
+  });
+
+  it('encode for setting single value', () => {
+    const bytes = createArrayBuffer(0x0B, 0x08, 0x00, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const subbytes = createArrayBuffer(0x08, 0x01);
+    const submsg = new TestMessage(Kernel.fromArrayBuffer(subbytes));
+    const expected = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C);
+
+    accessor.setRepeatedGroupElement(
+        /* fieldNumber= */ 1, submsg, TestMessage.instanceCreator,
+        /* index= */ 0);
+    const result = accessor.serialize();
+
+    expect(result).toEqual(expected);
+  });
+
+  it('encode for setting values', () => {
+    const accessor = Kernel.createEmpty();
+    const subbytes = createArrayBuffer(0x08, 0x01);
+    const submsg = new TestMessage(Kernel.fromArrayBuffer(subbytes));
+    const expected = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C);
+
+    accessor.setRepeatedGroupIterable(1, [submsg]);
+    const result = accessor.serialize();
+
+    expect(result).toEqual(expected);
+  });
+
+  it('fail when getting groups value with other wire types', () => {
+    const accessor = Kernel.fromArrayBuffer(createArrayBuffer(
+        0x09, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));
+
+    if (CHECK_CRITICAL_STATE) {
+      expect(() => {
+        accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+      }).toThrow();
+    }
+  });
+
+  it('fail when adding group values with wrong type value', () => {
+    const accessor = Kernel.createEmpty();
+    const fakeValue = /** @type {!TestMessage} */ (/** @type {*} */ (null));
+    if (CHECK_CRITICAL_STATE) {
+      expect(
+          () => accessor.addRepeatedGroupIterable(
+              1, [fakeValue], TestMessage.instanceCreator))
+          .toThrowError('Given value is not a message instance: null');
+    } else {
+      // Note in unchecked mode we produce invalid output for invalid inputs.
+      // This test just documents our behavior in those cases.
+      // These values might change at any point and are not considered
+      // what the implementation should be doing here.
+      accessor.addRepeatedGroupIterable(
+          1, [fakeValue], TestMessage.instanceCreator);
+      const list =
+          accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+      expect(Array.from(list)).toEqual([null]);
+    }
+  });
+
+  it('fail when adding single group value with wrong type value', () => {
+    const accessor = Kernel.createEmpty();
+    const fakeValue = /** @type {!TestMessage} */ (/** @type {*} */ (null));
+    if (CHECK_CRITICAL_STATE) {
+      expect(
+          () => accessor.addRepeatedGroupElement(
+              1, fakeValue, TestMessage.instanceCreator))
+          .toThrowError('Given value is not a message instance: null');
+    } else {
+      // Note in unchecked mode we produce invalid output for invalid inputs.
+      // This test just documents our behavior in those cases.
+      // These values might change at any point and are not considered
+      // what the implementation should be doing here.
+      accessor.addRepeatedGroupElement(
+          1, fakeValue, TestMessage.instanceCreator);
+      const list =
+          accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+      expect(Array.from(list)).toEqual([null]);
+    }
+  });
+
+  it('fail when setting message values with wrong type value', () => {
+    const accessor = Kernel.createEmpty();
+    const fakeValue = /** @type {!TestMessage} */ (/** @type {*} */ (null));
+    if (CHECK_CRITICAL_STATE) {
+      expect(() => accessor.setRepeatedGroupIterable(1, [fakeValue]))
+          .toThrowError('Given value is not a message instance: null');
+    } else {
+      // Note in unchecked mode we produce invalid output for invalid inputs.
+      // This test just documents our behavior in those cases.
+      // These values might change at any point and are not considered
+      // what the implementation should be doing here.
+      accessor.setRepeatedGroupIterable(1, [fakeValue]);
+      const list =
+          accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+      expect(Array.from(list)).toEqual([null]);
+    }
+  });
+
+  it('fail when setting single value with wrong type value', () => {
+    const accessor =
+        Kernel.fromArrayBuffer(createArrayBuffer(0x0B, 0x08, 0x00, 0x0C));
+    const fakeValue = /** @type {!TestMessage} */ (/** @type {*} */ (null));
+    if (CHECK_CRITICAL_STATE) {
+      expect(
+          () => accessor.setRepeatedGroupElement(
+              /* fieldNumber= */ 1, fakeValue, TestMessage.instanceCreator,
+              /* index= */ 0))
+          .toThrowError('Given value is not a message instance: null');
+    } else {
+      // Note in unchecked mode we produce invalid output for invalid inputs.
+      // This test just documents our behavior in those cases.
+      // These values might change at any point and are not considered
+      // what the implementation should be doing here.
+      accessor.setRepeatedGroupElement(
+          /* fieldNumber= */ 1, fakeValue, TestMessage.instanceCreator,
+          /* index= */ 0);
+      const list =
+          accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator);
+      expect(Array.from(list).length).toEqual(1);
+    }
+  });
+
+  it('fail when setting single value with out-of-bound index', () => {
+    const accessor =
+        Kernel.fromArrayBuffer(createArrayBuffer(0x0B, 0x08, 0x00, 0x0C));
+    const msg1 =
+        accessor.getRepeatedGroupElement(1, TestMessage.instanceCreator, 0);
+    const bytes2 = createArrayBuffer(0x08, 0x01);
+    const msg2 = new TestMessage(Kernel.fromArrayBuffer(bytes2));
+    if (CHECK_CRITICAL_STATE) {
+      expect(
+          () => accessor.setRepeatedGroupElement(
+              /* fieldNumber= */ 1, msg2, TestMessage.instanceCreator,
+              /* index= */ 1))
+          .toThrowError('Index out of bounds: index: 1 size: 1');
+    } else {
+      // Note in unchecked mode we produce invalid output for invalid inputs.
+      // This test just documents our behavior in those cases.
+      // These values might change at any point and are not considered
+      // what the implementation should be doing here.
+      accessor.setRepeatedGroupElement(
+          /* fieldNumber= */ 1, msg2, TestMessage.instanceCreator,
+          /* index= */ 1);
+      expectEqualToArray(
+          accessor.getRepeatedGroupIterable(1, TestMessage.instanceCreator),
+          [msg1, msg2]);
+    }
+  });
+});

+ 252 - 0
js/experimental/runtime/kernel/kernel_test.js

@@ -2075,3 +2075,255 @@ describe('Double access', () => {
     }
     }
   });
   });
 });
 });
+
+describe('Kernel for singular group does', () => {
+  it('return group from the input', () => {
+    const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const msg = accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    expect(msg.getBoolWithDefault(1, false)).toBe(true);
+  });
+
+  it('return group from the input when pivot is set', () => {
+    const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const msg = accessor.getGroupOrNull(1, TestMessage.instanceCreator, 0);
+    expect(msg.getBoolWithDefault(1, false)).toBe(true);
+  });
+
+  it('encode group from the input', () => {
+    const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    expect(accessor.serialize()).toEqual(bytes);
+  });
+
+  it('encode group from the input after read', () => {
+    const bytes = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    expect(accessor.serialize()).toEqual(bytes);
+  });
+
+  it('return last group from multiple inputs', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x00, 0x0C, 0x0B, 0x08, 0x01, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const msg = accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    expect(msg.getBoolWithDefault(1, false)).toBe(true);
+  });
+
+  it('removes duplicated group when serializing', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x00, 0x0C, 0x0B, 0x08, 0x01, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    expect(accessor.serialize())
+        .toEqual(createArrayBuffer(0x0B, 0x08, 0x01, 0x0C));
+  });
+
+  it('encode group from multiple inputs', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x00, 0x0C, 0x0B, 0x08, 0x01, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    expect(accessor.serialize()).toEqual(bytes);
+  });
+
+  it('encode group after read', () => {
+    const bytes =
+        createArrayBuffer(0x0B, 0x08, 0x00, 0x0C, 0x0B, 0x08, 0x01, 0x0C);
+    const expected = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    expect(accessor.serialize()).toEqual(expected);
+  });
+
+  it('return group from setter', () => {
+    const bytes = createArrayBuffer(0x08, 0x01);
+    const accessor = Kernel.fromArrayBuffer(new ArrayBuffer(0));
+    const subaccessor = Kernel.fromArrayBuffer(bytes);
+    const submsg1 = new TestMessage(subaccessor);
+    accessor.setGroup(1, submsg1);
+    const submsg2 = accessor.getGroup(1, TestMessage.instanceCreator);
+    expect(submsg1).toBe(submsg2);
+  });
+
+  it('encode group from setter', () => {
+    const accessor = Kernel.fromArrayBuffer(new ArrayBuffer(0));
+    const subaccessor = Kernel.fromArrayBuffer(createArrayBuffer(0x08, 0x01));
+    const submsg = new TestMessage(subaccessor);
+    accessor.setGroup(1, submsg);
+    const expected = createArrayBuffer(0x0B, 0x08, 0x01, 0x0C);
+    expect(accessor.serialize()).toEqual(expected);
+  });
+
+  it('leave hasFieldNumber unchanged after getGroupOrNull', () => {
+    const accessor = Kernel.createEmpty();
+    expect(accessor.hasFieldNumber(1)).toBe(false);
+    expect(accessor.getGroupOrNull(1, TestMessage.instanceCreator)).toBe(null);
+    expect(accessor.hasFieldNumber(1)).toBe(false);
+  });
+
+  it('serialize changes to subgroups made with getGroupsOrNull', () => {
+    const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(intTwoBytes);
+    const mutableSubMessage =
+        accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    mutableSubMessage.setInt32(1, 10);
+    const intTenBytes = createArrayBuffer(0x0B, 0x08, 0x0A, 0x0C);
+    expect(accessor.serialize()).toEqual(intTenBytes);
+  });
+
+  it('serialize additions to subgroups made with getGroupOrNull', () => {
+    const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(intTwoBytes);
+    const mutableSubMessage =
+        accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    mutableSubMessage.setInt32(2, 3);
+    // Sub group contains the original field, plus the new one.
+    expect(accessor.serialize())
+        .toEqual(createArrayBuffer(0x0B, 0x08, 0x02, 0x10, 0x03, 0x0C));
+  });
+
+  it('fail with getGroupOrNull if immutable group exist in cache', () => {
+    const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(intTwoBytes);
+
+    const readOnly = accessor.getGroup(1, TestMessage.instanceCreator);
+    if (CHECK_TYPE) {
+      expect(() => accessor.getGroupOrNull(1, TestMessage.instanceCreator))
+          .toThrow();
+    } else {
+      const mutableSubGropu =
+          accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+      // The instance returned by getGroupOrNull is the exact same instance.
+      expect(mutableSubGropu).toBe(readOnly);
+
+      // Serializing the subgroup does not write the changes
+      mutableSubGropu.setInt32(1, 0);
+      expect(accessor.serialize()).toEqual(intTwoBytes);
+    }
+  });
+
+  it('change hasFieldNumber after getGroupAttach', () => {
+    const accessor = Kernel.createEmpty();
+    expect(accessor.hasFieldNumber(1)).toBe(false);
+    expect(accessor.getGroupAttach(1, TestMessage.instanceCreator))
+        .not.toBe(null);
+    expect(accessor.hasFieldNumber(1)).toBe(true);
+  });
+
+  it('change hasFieldNumber after getGroupAttach when pivot is set', () => {
+    const accessor = Kernel.createEmpty();
+    expect(accessor.hasFieldNumber(1)).toBe(false);
+    expect(
+        accessor.getGroupAttach(1, TestMessage.instanceCreator, /* pivot= */ 1))
+        .not.toBe(null);
+    expect(accessor.hasFieldNumber(1)).toBe(true);
+  });
+
+  it('serialize subgroups made with getGroupAttach', () => {
+    const accessor = Kernel.createEmpty();
+    const mutableSubGroup =
+        accessor.getGroupAttach(1, TestMessage.instanceCreator);
+    mutableSubGroup.setInt32(1, 10);
+    const intTenBytes = createArrayBuffer(0x0B, 0x08, 0x0A, 0x0C);
+    expect(accessor.serialize()).toEqual(intTenBytes);
+  });
+
+  it('serialize additions to subgroups using getMessageAttach', () => {
+    const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(intTwoBytes);
+    const mutableSubGroup =
+        accessor.getGroupAttach(1, TestMessage.instanceCreator);
+    mutableSubGroup.setInt32(2, 3);
+    // Sub message contains the original field, plus the new one.
+    expect(accessor.serialize())
+        .toEqual(createArrayBuffer(0x0B, 0x08, 0x02, 0x10, 0x03, 0x0C));
+  });
+
+  it('fail with getGroupAttach if immutable message exist in cache', () => {
+    const intTwoBytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(intTwoBytes);
+
+    const readOnly = accessor.getGroup(1, TestMessage.instanceCreator);
+    if (CHECK_TYPE) {
+      expect(() => accessor.getGroupAttach(1, TestMessage.instanceCreator))
+          .toThrow();
+    } else {
+      const mutableSubGroup =
+          accessor.getGroupAttach(1, TestMessage.instanceCreator);
+      // The instance returned by getMessageOrNull is the exact same instance.
+      expect(mutableSubGroup).toBe(readOnly);
+
+      // Serializing the submessage does not write the changes
+      mutableSubGroup.setInt32(1, 0);
+      expect(accessor.serialize()).toEqual(intTwoBytes);
+    }
+  });
+
+  it('read default group return empty group with getGroup', () => {
+    const bytes = new ArrayBuffer(0);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    expect(accessor.getGroup(1, TestMessage.instanceCreator)).toBeTruthy();
+    expect(accessor.getGroup(1, TestMessage.instanceCreator).serialize())
+        .toEqual(bytes);
+  });
+
+  it('read default group return null with getGroupOrNull', () => {
+    const bytes = new ArrayBuffer(0);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    expect(accessor.getGroupOrNull(1, TestMessage.instanceCreator)).toBe(null);
+  });
+
+  it('read group preserve reference equality', () => {
+    const bytes = createArrayBuffer(0x0B, 0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const msg1 = accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    const msg2 = accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    const msg3 = accessor.getGroupAttach(1, TestMessage.instanceCreator);
+    expect(msg1).toBe(msg2);
+    expect(msg1).toBe(msg3);
+  });
+
+  it('fail when getting group with null instance constructor', () => {
+    const accessor =
+        Kernel.fromArrayBuffer(createArrayBuffer(0x0A, 0x02, 0x08, 0x01));
+    const nullMessage = /** @type {function(!Kernel):!TestMessage} */
+        (/** @type {*} */ (null));
+    expect(() => accessor.getGroupOrNull(1, nullMessage)).toThrow();
+  });
+
+  it('fail when setting group value with null value', () => {
+    const accessor = Kernel.fromArrayBuffer(new ArrayBuffer(0));
+    const fakeMessage = /** @type {!TestMessage} */ (/** @type {*} */ (null));
+    if (CHECK_CRITICAL_TYPE) {
+      expect(() => accessor.setGroup(1, fakeMessage))
+          .toThrowError('Given value is not a message instance: null');
+    } else {
+      // Note in unchecked mode we produce invalid output for invalid inputs.
+      // This test just documents our behavior in those cases.
+      // These values might change at any point and are not considered
+      // what the implementation should be doing here.
+      accessor.setMessage(1, fakeMessage);
+      expect(accessor.getGroupOrNull(
+                 /* fieldNumber= */ 1, TestMessage.instanceCreator))
+          .toBeNull();
+    }
+  });
+
+  it('reads group in a longer buffer', () => {
+    const bytes = createArrayBuffer(
+        0x12, 0x20,  // 32 length delimited
+        0x00,        // random values for padding start
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+        0x00,  // random values for padding end
+        0x0B,  // Group tag
+        0x08, 0x02, 0x0C);
+    const accessor = Kernel.fromArrayBuffer(bytes);
+    const msg1 = accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    const msg2 = accessor.getGroupOrNull(1, TestMessage.instanceCreator);
+    expect(msg1).toBe(msg2);
+  });
+});

+ 144 - 0
js/experimental/runtime/kernel/tag.js

@@ -0,0 +1,144 @@
+goog.module('protobuf.binary.tag');
+
+const BufferDecoder = goog.require('protobuf.binary.BufferDecoder');
+const WireType = goog.require('protobuf.binary.WireType');
+const {checkCriticalElementIndex, checkCriticalState} = goog.require('protobuf.internal.checks');
+
+/**
+ * Returns wire type stored in a tag.
+ * Protos store the wire type as the first 3 bit of a tag.
+ * @param {number} tag
+ * @return {!WireType}
+ */
+function tagToWireType(tag) {
+  return /** @type {!WireType} */ (tag & 0x07);
+}
+
+/**
+ * Returns the field number stored in a tag.
+ * Protos store the field number in the upper 29 bits of a 32 bit number.
+ * @param {number} tag
+ * @return {number}
+ */
+function tagToFieldNumber(tag) {
+  return tag >>> 3;
+}
+
+/**
+ * Combines wireType and fieldNumber into a tag.
+ * @param {!WireType} wireType
+ * @param {number} fieldNumber
+ * @return {number}
+ */
+function createTag(wireType, fieldNumber) {
+  return (fieldNumber << 3 | wireType) >>> 0;
+}
+
+/**
+ * Returns the length, in bytes, of the field in the tag stream, less the tag
+ * itself.
+ * Note: This moves the cursor in the bufferDecoder.
+ * @param {!BufferDecoder} bufferDecoder
+ * @param {number} start
+ * @param {!WireType} wireType
+ * @param {number} fieldNumber
+ * @return {number}
+ * @private
+ */
+function getTagLength(bufferDecoder, start, wireType, fieldNumber) {
+  bufferDecoder.setCursor(start);
+  skipField(bufferDecoder, wireType, fieldNumber);
+  return bufferDecoder.cursor() - start;
+}
+
+/**
+ * @param {number} value
+ * @return {number}
+ */
+function get32BitVarintLength(value) {
+  if (value < 0) {
+    return 5;
+  }
+  let size = 1;
+  while (value >= 128) {
+    size++;
+    value >>>= 7;
+  }
+  return size;
+}
+
+/**
+ * Skips over a field.
+ * Note: If the field is a start group the entire group will be skipped, placing
+ * the cursor onto the next field.
+ * @param {!BufferDecoder} bufferDecoder
+ * @param {!WireType} wireType
+ * @param {number} fieldNumber
+ */
+function skipField(bufferDecoder, wireType, fieldNumber) {
+  switch (wireType) {
+    case WireType.VARINT:
+      checkCriticalElementIndex(
+          bufferDecoder.cursor(), bufferDecoder.endIndex());
+      bufferDecoder.skipVarint();
+      return;
+    case WireType.FIXED64:
+      bufferDecoder.skip(8);
+      return;
+    case WireType.DELIMITED:
+      checkCriticalElementIndex(
+          bufferDecoder.cursor(), bufferDecoder.endIndex());
+      const length = bufferDecoder.getUnsignedVarint32();
+      bufferDecoder.skip(length);
+      return;
+    case WireType.START_GROUP:
+      const foundGroup = skipGroup_(bufferDecoder, fieldNumber);
+      checkCriticalState(foundGroup, 'No end group found.');
+      return;
+    case WireType.FIXED32:
+      bufferDecoder.skip(4);
+      return;
+    default:
+      throw new Error(`Unexpected wire type: ${wireType}`);
+  }
+}
+
+/**
+ * Skips over fields until it finds the end of a given group consuming the stop
+ * group tag.
+ * @param {!BufferDecoder} bufferDecoder
+ * @param {number} groupFieldNumber
+ * @return {boolean} Whether the end group tag was found.
+ * @private
+ */
+function skipGroup_(bufferDecoder, groupFieldNumber) {
+  // On a start group we need to keep skipping fields until we find a
+  // corresponding stop group
+  // Note: Since we are calling skipField from here nested groups will be
+  // handled by recursion of this method and thus we will not see a nested
+  // STOP GROUP here unless there is something wrong with the input data.
+  while (bufferDecoder.hasNext()) {
+    const tag = bufferDecoder.getUnsignedVarint32();
+    const wireType = tagToWireType(tag);
+    const fieldNumber = tagToFieldNumber(tag);
+
+    if (wireType === WireType.END_GROUP) {
+      checkCriticalState(
+          groupFieldNumber === fieldNumber,
+          `Expected stop group for fieldnumber ${groupFieldNumber} not found.`);
+      return true;
+    } else {
+      skipField(bufferDecoder, wireType, fieldNumber);
+    }
+  }
+  return false;
+}
+
+exports = {
+  createTag,
+  get32BitVarintLength,
+  getTagLength,
+  skipField,
+  tagToWireType,
+  tagToFieldNumber,
+};

+ 221 - 0
js/experimental/runtime/kernel/tag_test.js

@@ -0,0 +1,221 @@
+/**
+ * @fileoverview Tests for tag.js.
+ */
+goog.module('protobuf.binary.TagTests');
+
+const BufferDecoder = goog.require('protobuf.binary.BufferDecoder');
+const WireType = goog.require('protobuf.binary.WireType');
+const {CHECK_CRITICAL_STATE} = goog.require('protobuf.internal.checks');
+const {createTag, get32BitVarintLength, skipField, tagToFieldNumber, tagToWireType} = goog.require('protobuf.binary.tag');
+
+
+goog.setTestOnly();
+
+/**
+ * @param {...number} bytes
+ * @return {!ArrayBuffer}
+ */
+function createArrayBuffer(...bytes) {
+  return new Uint8Array(bytes).buffer;
+}
+
+describe('skipField', () => {
+  it('skips varints', () => {
+    const bufferDecoder =
+        BufferDecoder.fromArrayBuffer(createArrayBuffer(0x80, 0x00));
+    skipField(bufferDecoder, WireType.VARINT, 1);
+    expect(bufferDecoder.cursor()).toBe(2);
+  });
+
+  it('throws for out of bounds varints', () => {
+    const bufferDecoder =
+        BufferDecoder.fromArrayBuffer(createArrayBuffer(0x80, 0x00));
+    bufferDecoder.setCursor(2);
+    if (CHECK_CRITICAL_STATE) {
+      expect(() => skipField(bufferDecoder, WireType.VARINT, 1)).toThrowError();
+    }
+  });
+
+  it('skips fixed64', () => {
+    const bufferDecoder = BufferDecoder.fromArrayBuffer(
+        createArrayBuffer(0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00));
+    skipField(bufferDecoder, WireType.FIXED64, 1);
+    expect(bufferDecoder.cursor()).toBe(8);
+  });
+
+  it('throws for fixed64 if length is too short', () => {
+    const bufferDecoder =
+        BufferDecoder.fromArrayBuffer(createArrayBuffer(0x80, 0x00));
+    if (CHECK_CRITICAL_STATE) {
+      expect(() => skipField(bufferDecoder, WireType.FIXED64, 1))
+          .toThrowError();
+    }
+  });
+
+  it('skips fixed32', () => {
+    const bufferDecoder = BufferDecoder.fromArrayBuffer(
+        createArrayBuffer(0x80, 0x00, 0x00, 0x00));
+    skipField(bufferDecoder, WireType.FIXED32, 1);
+    expect(bufferDecoder.cursor()).toBe(4);
+  });
+
+  it('throws for fixed32 if length is too short', () => {
+    const bufferDecoder =
+        BufferDecoder.fromArrayBuffer(createArrayBuffer(0x80, 0x00));
+    if (CHECK_CRITICAL_STATE) {
+      expect(() => skipField(bufferDecoder, WireType.FIXED32, 1))
+          .toThrowError();
+    }
+  });
+
+
+  it('skips length delimited', () => {
+    const bufferDecoder = BufferDecoder.fromArrayBuffer(
+        createArrayBuffer(0x03, 0x00, 0x00, 0x00));
+    skipField(bufferDecoder, WireType.DELIMITED, 1);
+    expect(bufferDecoder.cursor()).toBe(4);
+  });
+
+  it('throws for length delimited if length is too short', () => {
+    const bufferDecoder =
+        BufferDecoder.fromArrayBuffer(createArrayBuffer(0x03, 0x00, 0x00));
+    if (CHECK_CRITICAL_STATE) {
+      expect(() => skipField(bufferDecoder, WireType.DELIMITED, 1))
+          .toThrowError();
+    }
+  });
+
+  it('skips groups', () => {
+    const bufferDecoder = BufferDecoder.fromArrayBuffer(
+        createArrayBuffer(0x0B, 0x08, 0x01, 0x0C));
+    bufferDecoder.setCursor(1);
+    skipField(bufferDecoder, WireType.START_GROUP, 1);
+    expect(bufferDecoder.cursor()).toBe(4);
+  });
+
+  it('skips group in group', () => {
+    const buffer = createArrayBuffer(
+        0x0B,        // start outter
+        0x10, 0x01,  // field: 2, value: 1
+        0x0B,        // start inner group
+        0x10, 0x01,  // payload inner group
+        0x0C,        // stop inner group
+        0x0C         // end outter
+    );
+    const bufferDecoder = BufferDecoder.fromArrayBuffer(buffer);
+    bufferDecoder.setCursor(1);
+    skipField(bufferDecoder, WireType.START_GROUP, 1);
+    expect(bufferDecoder.cursor()).toBe(8);
+  });
+
+  it('throws for group if length is too short', () => {
+    // no closing group
+    const bufferDecoder =
+        BufferDecoder.fromArrayBuffer(createArrayBuffer(0x0B, 0x00, 0x00));
+    if (CHECK_CRITICAL_STATE) {
+      expect(() => skipField(bufferDecoder, WireType.START_GROUP, 1))
+          .toThrowError();
+    }
+  });
+});
+
+
+describe('tagToWireType', () => {
+  it('decodes numbers ', () => {
+    // simple numbers
+    expect(tagToWireType(0x00)).toBe(WireType.VARINT);
+    expect(tagToWireType(0x01)).toBe(WireType.FIXED64);
+    expect(tagToWireType(0x02)).toBe(WireType.DELIMITED);
+    expect(tagToWireType(0x03)).toBe(WireType.START_GROUP);
+    expect(tagToWireType(0x04)).toBe(WireType.END_GROUP);
+    expect(tagToWireType(0x05)).toBe(WireType.FIXED32);
+
+    // upper bits should not matter
+    expect(tagToWireType(0x08)).toBe(WireType.VARINT);
+    expect(tagToWireType(0x09)).toBe(WireType.FIXED64);
+    expect(tagToWireType(0x0A)).toBe(WireType.DELIMITED);
+    expect(tagToWireType(0x0B)).toBe(WireType.START_GROUP);
+    expect(tagToWireType(0x0C)).toBe(WireType.END_GROUP);
+    expect(tagToWireType(0x0D)).toBe(WireType.FIXED32);
+
+    // upper bits should not matter
+    expect(tagToWireType(0xF8)).toBe(WireType.VARINT);
+    expect(tagToWireType(0xF9)).toBe(WireType.FIXED64);
+    expect(tagToWireType(0xFA)).toBe(WireType.DELIMITED);
+    expect(tagToWireType(0xFB)).toBe(WireType.START_GROUP);
+    expect(tagToWireType(0xFC)).toBe(WireType.END_GROUP);
+    expect(tagToWireType(0xFD)).toBe(WireType.FIXED32);
+
+    // negative numbers work
+    expect(tagToWireType(-8)).toBe(WireType.VARINT);
+    expect(tagToWireType(-7)).toBe(WireType.FIXED64);
+    expect(tagToWireType(-6)).toBe(WireType.DELIMITED);
+    expect(tagToWireType(-5)).toBe(WireType.START_GROUP);
+    expect(tagToWireType(-4)).toBe(WireType.END_GROUP);
+    expect(tagToWireType(-3)).toBe(WireType.FIXED32);
+  });
+});
+
+describe('tagToFieldNumber', () => {
+  it('returns fieldNumber', () => {
+    expect(tagToFieldNumber(0x08)).toBe(1);
+    expect(tagToFieldNumber(0x09)).toBe(1);
+    expect(tagToFieldNumber(0x10)).toBe(2);
+    expect(tagToFieldNumber(0x12)).toBe(2);
+  });
+});
+
+describe('createTag', () => {
+  it('combines fieldNumber and wireType', () => {
+    expect(createTag(WireType.VARINT, 1)).toBe(0x08);
+    expect(createTag(WireType.FIXED64, 1)).toBe(0x09);
+    expect(createTag(WireType.DELIMITED, 1)).toBe(0x0A);
+    expect(createTag(WireType.START_GROUP, 1)).toBe(0x0B);
+    expect(createTag(WireType.END_GROUP, 1)).toBe(0x0C);
+    expect(createTag(WireType.FIXED32, 1)).toBe(0x0D);
+
+    expect(createTag(WireType.VARINT, 2)).toBe(0x10);
+    expect(createTag(WireType.FIXED64, 2)).toBe(0x11);
+    expect(createTag(WireType.DELIMITED, 2)).toBe(0x12);
+    expect(createTag(WireType.START_GROUP, 2)).toBe(0x13);
+    expect(createTag(WireType.END_GROUP, 2)).toBe(0x14);
+    expect(createTag(WireType.FIXED32, 2)).toBe(0x15);
+
+    expect(createTag(WireType.VARINT, 0x1FFFFFFF)).toBe(0xFFFFFFF8 >>> 0);
+    expect(createTag(WireType.FIXED64, 0x1FFFFFFF)).toBe(0xFFFFFFF9 >>> 0);
+    expect(createTag(WireType.DELIMITED, 0x1FFFFFFF)).toBe(0xFFFFFFFA >>> 0);
+    expect(createTag(WireType.START_GROUP, 0x1FFFFFFF)).toBe(0xFFFFFFFB >>> 0);
+    expect(createTag(WireType.END_GROUP, 0x1FFFFFFF)).toBe(0xFFFFFFFC >>> 0);
+    expect(createTag(WireType.FIXED32, 0x1FFFFFFF)).toBe(0xFFFFFFFD >>> 0);
+  });
+});
+
+describe('get32BitVarintLength', () => {
+  it('length of tag', () => {
+    expect(get32BitVarintLength(0)).toBe(1);
+    expect(get32BitVarintLength(1)).toBe(1);
+    expect(get32BitVarintLength(1)).toBe(1);
+
+    expect(get32BitVarintLength(Math.pow(2, 7) - 1)).toBe(1);
+    expect(get32BitVarintLength(Math.pow(2, 7))).toBe(2);
+
+    expect(get32BitVarintLength(Math.pow(2, 14) - 1)).toBe(2);
+    expect(get32BitVarintLength(Math.pow(2, 14))).toBe(3);
+
+    expect(get32BitVarintLength(Math.pow(2, 21) - 1)).toBe(3);
+    expect(get32BitVarintLength(Math.pow(2, 21))).toBe(4);
+
+    expect(get32BitVarintLength(Math.pow(2, 28) - 1)).toBe(4);
+    expect(get32BitVarintLength(Math.pow(2, 28))).toBe(5);
+
+    expect(get32BitVarintLength(Math.pow(2, 31) - 1)).toBe(5);
+
+    expect(get32BitVarintLength(-1)).toBe(5);
+    expect(get32BitVarintLength(-Math.pow(2, 31))).toBe(5);
+
+    expect(get32BitVarintLength(createTag(WireType.VARINT, 0x1fffffff)))
+        .toBe(5);
+    expect(get32BitVarintLength(createTag(WireType.FIXED32, 0x1fffffff)))
+        .toBe(5);
+  });
+});

+ 23 - 56
js/experimental/runtime/kernel/writer.js

@@ -10,6 +10,7 @@ const Int64 = goog.require('protobuf.Int64');
 const WireType = goog.require('protobuf.binary.WireType');
 const WireType = goog.require('protobuf.binary.WireType');
 const {POLYFILL_TEXT_ENCODING, checkFieldNumber, checkTypeUnsignedInt32, checkWireType} = goog.require('protobuf.internal.checks');
 const {POLYFILL_TEXT_ENCODING, checkFieldNumber, checkTypeUnsignedInt32, checkWireType} = goog.require('protobuf.internal.checks');
 const {concatenateByteArrays} = goog.require('protobuf.binary.uint8arrays');
 const {concatenateByteArrays} = goog.require('protobuf.binary.uint8arrays');
+const {createTag, getTagLength} = goog.require('protobuf.binary.tag');
 const {encode} = goog.require('protobuf.binary.textencoding');
 const {encode} = goog.require('protobuf.binary.textencoding');
 
 
 /**
 /**
@@ -92,8 +93,8 @@ class Writer {
   writeTag(fieldNumber, wireType) {
   writeTag(fieldNumber, wireType) {
     checkFieldNumber(fieldNumber);
     checkFieldNumber(fieldNumber);
     checkWireType(wireType);
     checkWireType(wireType);
-    const tag = fieldNumber << 3 | wireType;
-    this.writeUnsignedVarint32_(tag >>> 0);
+    const tag = createTag(wireType, fieldNumber);
+    this.writeUnsignedVarint32_(tag);
   }
   }
 
 
   /**
   /**
@@ -299,6 +300,22 @@ class Writer {
     this.writeSfixed64Value_(value);
     this.writeSfixed64Value_(value);
   }
   }
 
 
+  /**
+   * Writes a sfixed64 value field to the buffer.
+   * @param {number} fieldNumber
+   */
+  writeStartGroup(fieldNumber) {
+    this.writeTag(fieldNumber, WireType.START_GROUP);
+  }
+
+  /**
+   * Writes a sfixed64 value field to the buffer.
+   * @param {number} fieldNumber
+   */
+  writeEndGroup(fieldNumber) {
+    this.writeTag(fieldNumber, WireType.END_GROUP);
+  }
+
   /**
   /**
    * Writes a uint32 value field to the buffer as a varint without tag.
    * Writes a uint32 value field to the buffer as a varint without tag.
    * @param {number} value
    * @param {number} value
@@ -430,67 +447,17 @@ class Writer {
    * @param {!BufferDecoder} bufferDecoder
    * @param {!BufferDecoder} bufferDecoder
    * @param {number} start
    * @param {number} start
    * @param {!WireType} wireType
    * @param {!WireType} wireType
+   * @param {number} fieldNumber
    * @package
    * @package
    */
    */
-  writeBufferDecoder(bufferDecoder, start, wireType) {
+  writeBufferDecoder(bufferDecoder, start, wireType, fieldNumber) {
     this.closeAndStartNewBuffer_();
     this.closeAndStartNewBuffer_();
-    const dataLength = this.getLength_(bufferDecoder, start, wireType);
+    const dataLength =
+        getTagLength(bufferDecoder, start, wireType, fieldNumber);
     this.blocks_.push(
     this.blocks_.push(
         bufferDecoder.subBufferDecoder(start, dataLength).asUint8Array());
         bufferDecoder.subBufferDecoder(start, dataLength).asUint8Array());
   }
   }
 
 
-  /**
-   * Returns the length of the data to serialize. Returns -1 when a STOP GROUP
-   * is found.
-   * @param {!BufferDecoder} bufferDecoder
-   * @param {number} start
-   * @param {!WireType} wireType
-   * @return {number}
-   * @private
-   */
-  getLength_(bufferDecoder, start, wireType) {
-    switch (wireType) {
-      case WireType.VARINT:
-        bufferDecoder.setCursor(start);
-        bufferDecoder.skipVarint();
-        return bufferDecoder.cursor() - start;
-      case WireType.FIXED64:
-        return 8;
-      case WireType.DELIMITED:
-        const dataLength = bufferDecoder.getUnsignedVarint32At(start);
-        return dataLength + bufferDecoder.cursor() - start;
-      case WireType.START_GROUP:
-        return this.getGroupLength_(bufferDecoder, start);
-      case WireType.FIXED32:
-        return 4;
-      default:
-        throw new Error(`Invalid wire type: ${wireType}`);
-    }
-  }
-
-  /**
-   * Skips over fields until it finds the end of a given group.
-   * @param {!BufferDecoder} bufferDecoder
-   * @param {number} start
-   * @return {number}
-   * @private
-   */
-  getGroupLength_(bufferDecoder, start) {
-    // On a start group we need to keep skipping fields until we find a
-    // corresponding stop group
-    let cursor = start;
-    while (cursor < bufferDecoder.endIndex()) {
-      const tag = bufferDecoder.getUnsignedVarint32At(cursor);
-      const wireType = /** @type {!WireType} */ (tag & 0x07);
-      if (wireType === WireType.END_GROUP) {
-        return bufferDecoder.cursor() - start;
-      }
-      cursor = bufferDecoder.cursor() +
-          this.getLength_(bufferDecoder, bufferDecoder.cursor(), wireType);
-    }
-    throw new Error('No end group found');
-  }
-
   /**
   /**
    * Write the whole bytes as a length delimited field.
    * Write the whole bytes as a length delimited field.
    * @param {number} fieldNumber
    * @param {number} fieldNumber

+ 6 - 6
js/experimental/runtime/kernel/writer_test.js

@@ -148,7 +148,7 @@ describe('Writer.writeBufferDecoder does', () => {
     const expected = createArrayBuffer(
     const expected = createArrayBuffer(
         0x08, /* varint start= */ 0xFF, /* varint end= */ 0x01, 0x08, 0x01);
         0x08, /* varint start= */ 0xFF, /* varint end= */ 0x01, 0x08, 0x01);
     writer.writeBufferDecoder(
     writer.writeBufferDecoder(
-        BufferDecoder.fromArrayBuffer(expected), 1, WireType.VARINT);
+        BufferDecoder.fromArrayBuffer(expected), 1, WireType.VARINT, 1);
     const result = writer.getAndResetResultBuffer();
     const result = writer.getAndResetResultBuffer();
     expect(result).toEqual(arrayBufferSlice(expected, 1, 3));
     expect(result).toEqual(arrayBufferSlice(expected, 1, 3));
   });
   });
@@ -159,7 +159,7 @@ describe('Writer.writeBufferDecoder does', () => {
         0x09, /* fixed64 start= */ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
         0x09, /* fixed64 start= */ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
         /* fixed64 end= */ 0x08, 0x08, 0x01);
         /* fixed64 end= */ 0x08, 0x08, 0x01);
     writer.writeBufferDecoder(
     writer.writeBufferDecoder(
-        BufferDecoder.fromArrayBuffer(expected), 1, WireType.FIXED64);
+        BufferDecoder.fromArrayBuffer(expected), 1, WireType.FIXED64, 1);
     const result = writer.getAndResetResultBuffer();
     const result = writer.getAndResetResultBuffer();
     expect(result).toEqual(arrayBufferSlice(expected, 1, 9));
     expect(result).toEqual(arrayBufferSlice(expected, 1, 9));
   });
   });
@@ -170,7 +170,7 @@ describe('Writer.writeBufferDecoder does', () => {
         0xA, /* length= */ 0x03, /* data start= */ 0x01, 0x02,
         0xA, /* length= */ 0x03, /* data start= */ 0x01, 0x02,
         /* data end= */ 0x03, 0x08, 0x01);
         /* data end= */ 0x03, 0x08, 0x01);
     writer.writeBufferDecoder(
     writer.writeBufferDecoder(
-        BufferDecoder.fromArrayBuffer(expected), 1, WireType.DELIMITED);
+        BufferDecoder.fromArrayBuffer(expected), 1, WireType.DELIMITED, 1);
     const result = writer.getAndResetResultBuffer();
     const result = writer.getAndResetResultBuffer();
     expect(result).toEqual(arrayBufferSlice(expected, 1, 5));
     expect(result).toEqual(arrayBufferSlice(expected, 1, 5));
   });
   });
@@ -181,7 +181,7 @@ describe('Writer.writeBufferDecoder does', () => {
         0xB, /* group start= */ 0x08, 0x01, /* nested group start= */ 0x0B,
         0xB, /* group start= */ 0x08, 0x01, /* nested group start= */ 0x0B,
         /* nested group end= */ 0x0C, /* group end= */ 0x0C, 0x08, 0x01);
         /* nested group end= */ 0x0C, /* group end= */ 0x0C, 0x08, 0x01);
     writer.writeBufferDecoder(
     writer.writeBufferDecoder(
-        BufferDecoder.fromArrayBuffer(expected), 1, WireType.START_GROUP);
+        BufferDecoder.fromArrayBuffer(expected), 1, WireType.START_GROUP, 1);
     const result = writer.getAndResetResultBuffer();
     const result = writer.getAndResetResultBuffer();
     expect(result).toEqual(arrayBufferSlice(expected, 1, 6));
     expect(result).toEqual(arrayBufferSlice(expected, 1, 6));
   });
   });
@@ -192,7 +192,7 @@ describe('Writer.writeBufferDecoder does', () => {
         0x09, /* fixed64 start= */ 0x01, 0x02, 0x03, /* fixed64 end= */ 0x04,
         0x09, /* fixed64 start= */ 0x01, 0x02, 0x03, /* fixed64 end= */ 0x04,
         0x08, 0x01);
         0x08, 0x01);
     writer.writeBufferDecoder(
     writer.writeBufferDecoder(
-        BufferDecoder.fromArrayBuffer(expected), 1, WireType.FIXED32);
+        BufferDecoder.fromArrayBuffer(expected), 1, WireType.FIXED32, 1);
     const result = writer.getAndResetResultBuffer();
     const result = writer.getAndResetResultBuffer();
     expect(result).toEqual(arrayBufferSlice(expected, 1, 5));
     expect(result).toEqual(arrayBufferSlice(expected, 1, 5));
   });
   });
@@ -203,7 +203,7 @@ describe('Writer.writeBufferDecoder does', () => {
     const subBuffer = arrayBufferSlice(buffer, 0, 2);
     const subBuffer = arrayBufferSlice(buffer, 0, 2);
     expect(
     expect(
         () => writer.writeBufferDecoder(
         () => writer.writeBufferDecoder(
-            BufferDecoder.fromArrayBuffer(subBuffer), 0, WireType.DELIMITED))
+            BufferDecoder.fromArrayBuffer(subBuffer), 0, WireType.DELIMITED, 1))
         .toThrow();
         .toThrow();
   });
   });
 });
 });