Sync changes from internal repo. (#119)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index e438523..21d492b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.10.0
+
+* Breaking change: Add `GeneratedMessage.freeze()`. A frozen message and its
+  sub-messages cannot be changed.
+
 ## 0.9.1
 
 * Fix problem with encoding negative enum values.
diff --git a/lib/meta.dart b/lib/meta.dart
index dbaaffe..8ee607b 100644
--- a/lib/meta.dart
+++ b/lib/meta.dart
@@ -10,8 +10,10 @@
 const List<String> GeneratedMessage_reservedNames = const [
   'hashCode',
   'noSuchMethod',
+  'copyWith',
   'runtimeType',
   'toString',
+  'freeze',
   'fromBuffer',
   'fromJson',
   'hasRequiredFields',
@@ -63,5 +65,6 @@
   r'$_setSignedInt32',
   r'$_setUnsignedInt32',
   r'$_setInt64',
+  'toBuilder',
   'toDebugString',
 ];
diff --git a/lib/src/protobuf/builder_info.dart b/lib/src/protobuf/builder_info.dart
index dec83b9..abc0965 100644
--- a/lib/src/protobuf/builder_info.dart
+++ b/lib/src/protobuf/builder_info.dart
@@ -94,14 +94,6 @@
         tagNumber, name, fieldType, defaultOrMaker, null, valueOf, enumValues);
   }
 
-  // Repeated message.
-  // TODO(skybrian): migrate to pp() and remove.
-  void m<T>(int tagNumber, String name, CreateBuilderFunc subBuilder,
-      MakeDefaultFunc makeDefault) {
-    add<T>(tagNumber, name, PbFieldType._REPEATED_MESSAGE, makeDefault,
-        subBuilder, null, null);
-  }
-
   // Repeated, not a message, group, or enum.
   void p<T>(int tagNumber, String name, int fieldType) {
     assert(!_isGroupOrMessage(fieldType) && !_isEnum(fieldType));
diff --git a/lib/src/protobuf/field_set.dart b/lib/src/protobuf/field_set.dart
index 04444bb..9448fec 100644
--- a/lib/src/protobuf/field_set.dart
+++ b/lib/src/protobuf/field_set.dart
@@ -4,6 +4,28 @@
 
 part of protobuf;
 
+typedef void FrozenMessageErrorHandler(String messageName, [String methodName]);
+
+void defaultFrozenMessageModificationHandler(String messageName,
+    [String methodName]) {
+  if (methodName != null) {
+    throw new UnsupportedError(
+        "Attempted to call $methodName on a read-only message ($messageName)");
+  }
+  throw new UnsupportedError(
+      "Attempted to change a read-only message ($messageName)");
+}
+
+/// Invoked when an attempt is made to modify a frozen message.
+///
+/// This handler can log the attempt, throw an exception, or ignore the attempt
+/// altogether.
+///
+/// If the handler returns normally, the modification is allowed, and execution
+/// proceeds as if the message was writable.
+FrozenMessageErrorHandler frozenMessageModificationHandler =
+    defaultFrozenMessageModificationHandler;
+
 /// All the data in a GeneratedMessage.
 ///
 /// These fields and methods are in a separate class to avoid
@@ -13,6 +35,7 @@
   final GeneratedMessage _message;
   final BuilderInfo _meta;
   final EventPlugin _eventPlugin;
+  bool _isReadOnly = false;
 
   /// The value of each non-extension field in a fixed-length array.
   /// The index of a field can be found in [FieldInfo.index].
@@ -41,7 +64,6 @@
   // Metadata about multiple fields
 
   String get _messageName => _meta.messageName;
-  bool get _isReadOnly => _message._isReadOnly;
   bool get _hasRequiredFields => _meta.hasRequiredFields;
 
   /// The FieldInfo for each non-extension field.
@@ -92,6 +114,32 @@
     return _extensions._getInfoOrNull(tagNumber);
   }
 
+  void _markReadOnly() {
+    if (_isReadOnly) return;
+    _isReadOnly = true;
+    for (var field in _meta.sortedByTag) {
+      if (field.isRepeated) {
+        final entries = _values[field.index];
+        if (entries == null) continue;
+        if (field.isGroupOrMessage) {
+          for (var subMessage in entries as List<GeneratedMessage>) {
+            subMessage.freeze();
+          }
+        }
+        _values[field.index] = entries.toFrozenPbList();
+      } else if (field.isGroupOrMessage) {
+        final entry = _values[field.index];
+        if (entry != null) {
+          (entry as GeneratedMessage).freeze();
+        }
+      }
+    }
+  }
+
+  void _ensureWritable() {
+    if (_isReadOnly) frozenMessageModificationHandler(_messageName);
+  }
+
   // Single-field operations
 
   /// Gets a field with full error-checking.
@@ -163,6 +211,7 @@
   }
 
   void _clearField(int tagNumber) {
+    _ensureWritable();
     var fi = _nonExtensionInfo(tagNumber);
     if (fi != null) {
       // clear a non-extension field
@@ -312,10 +361,7 @@
   void _$set(int index, value) {
     assert(!_nonExtensionInfoByIndex(index).isRepeated);
     assert(_$check(index, value));
-    if (_isReadOnly) {
-      throw new UnsupportedError(
-          "attempted to call a setter on a read-only message ($_messageName)");
-    }
+    _ensureWritable();
     if (value == null) {
       _$check(index, value); // throw exception for null value
     }
@@ -333,6 +379,7 @@
   // Bulk operations reading or writing multiple fields
 
   void _clear() {
+    _ensureWritable();
     if (_unknownFields != null) {
       _unknownFields.clear();
     }
@@ -411,7 +458,7 @@
   int get _hashCode {
     int hash;
 
-    void hashEnumList(PbList enums) {
+    void hashEnumList(PbListBase enums) {
       for (ProtobufEnum enm in enums) {
         hash = 0x1fffffff & ((31 * hash) + enm.value);
       }
@@ -536,15 +583,15 @@
 
     if (fi.isRepeated) {
       if (mustClone) {
-        // fieldValue must be a PbList of GeneratedMessage.
-        PbList<GeneratedMessage> pbList = fieldValue;
+        // fieldValue must be a PbListBase of GeneratedMessage.
+        PbListBase<GeneratedMessage> pbList = fieldValue;
         var repeatedFields = fi._ensureRepeatedField(this);
         for (int i = 0; i < pbList.length; ++i) {
           repeatedFields.add(_cloneMessage(pbList[i]));
         }
       } else {
-        // fieldValue must be at least a PbList.
-        PbList pbList = fieldValue;
+        // fieldValue must be at least a PbListBase.
+        PbListBase pbList = fieldValue;
         fi._ensureRepeatedField(this).addAll(pbList);
       }
       return;
@@ -572,6 +619,7 @@
 
   /// Checks the value for a field that's about to be set.
   void _validateField(FieldInfo fi, var newValue) {
+    _ensureWritable();
     var message = _getFieldError(fi.type, newValue);
     if (message != null) {
       throw new ArgumentError(_setFieldFailedMessage(fi, newValue, message));
diff --git a/lib/src/protobuf/generated_message.dart b/lib/src/protobuf/generated_message.dart
index 2f655da..fb22b2d 100644
--- a/lib/src/protobuf/generated_message.dart
+++ b/lib/src/protobuf/generated_message.dart
@@ -46,11 +46,33 @@
 
   /// Creates a deep copy of the fields in this message.
   /// (The generated code uses [mergeFromMessage].)
+  // TODO(nichite): preserve frozen state on clone.
   GeneratedMessage clone();
 
   UnknownFieldSet get unknownFields => _fieldSet._ensureUnknownFields();
 
-  bool get _isReadOnly => this is ReadonlyMessageMixin;
+  /// Make this message read-only.
+  ///
+  /// Marks this message, and any sub-messages, as read-only.
+  GeneratedMessage freeze() {
+    _fieldSet._markReadOnly();
+    return this;
+  }
+
+  /// Returns a writable copy of this message.
+  // TODO(nichite): Return an actual builder object that lazily creates builders
+  // for sub-messages, instead of cloning everything here.
+  GeneratedMessage toBuilder() => clone();
+
+  /// Apply [updates] to a copy of this message.
+  ///
+  /// Makes a writable copy of this message, applies the [updates] to it, and
+  /// marks the copy read-only before returning it.
+  GeneratedMessage copyWith(void Function(GeneratedMessage) updates) {
+    final builder = toBuilder();
+    updates(builder);
+    return builder.freeze();
+  }
 
   bool hasRequiredFields() => info_.hasRequiredFields;
 
@@ -67,6 +89,7 @@
   // TODO(antonm): move to getters.
   int getTagNumber(String fieldName) => info_.tagNumber(fieldName);
 
+  @override
   bool operator ==(other) {
     if (identical(this, other)) return true;
     return other is GeneratedMessage
@@ -211,7 +234,7 @@
   ///
   /// If not set, returns the extension's default value.
   getExtension(Extension extension) {
-    if (_isReadOnly) return extension.readonlyDefault;
+    if (_fieldSet._isReadOnly) return extension.readonlyDefault;
     return _fieldSet._ensureExtensions()._getFieldOrDefault(extension);
   }
 
@@ -291,7 +314,12 @@
       _fieldSet._$get<T>(index, defaultValue);
 
   /// For generated code only.
-  T $_getN<T>(int index) => _fieldSet._$getN<T>(index);
+  T $_getN<T>(int index) {
+    if (_fieldSet == null) {
+      throw new StateError('Unable to access $index in the proto message');
+    }
+    return _fieldSet._$getN<T>(index);
+  }
 
   /// For generated code only.
   List<T> $_getList<T>(int index) => _fieldSet._$getList<T>(index);
diff --git a/lib/src/protobuf/json.dart b/lib/src/protobuf/json.dart
index f2b945d..63eb41f 100644
--- a/lib/src/protobuf/json.dart
+++ b/lib/src/protobuf/json.dart
@@ -5,12 +5,11 @@
 part of protobuf;
 
 Map<String, dynamic> _writeToJsonMap(_FieldSet fs) {
-  convertToMap(fieldValue, int fieldType) {
+  convertToMap(dynamic fieldValue, int fieldType) {
     int baseType = PbFieldType._baseType(fieldType);
 
     if (_isRepeated(fieldType)) {
-      PbList pbList = fieldValue;
-      return new List.from(pbList.map((e) => convertToMap(e, baseType)));
+      return new List.from(fieldValue.map((e) => convertToMap(e, baseType)));
     }
 
     switch (baseType) {
diff --git a/lib/src/protobuf/pb_list.dart b/lib/src/protobuf/pb_list.dart
index d1d1aab..8517910 100644
--- a/lib/src/protobuf/pb_list.dart
+++ b/lib/src/protobuf/pb_list.dart
@@ -6,23 +6,168 @@
 
 typedef void CheckFunc<E>(E x);
 
-class PbList<E> extends ListBase<E> {
+class FrozenPbList<E> extends PbListBase<E> {
+  FrozenPbList._(List<E> wrappedList) : super._(wrappedList);
+
+  factory FrozenPbList.from(PbList<E> other) =>
+      new FrozenPbList._(other._wrappedList);
+
+  UnsupportedError _unsupported(String method) =>
+      new UnsupportedError("Cannot call $method on an unmodifiable list");
+
+  void operator []=(int index, E value) => throw _unsupported("set");
+  set length(int newLength) => throw _unsupported("set length");
+  void setAll(int at, Iterable<E> iterable) => throw _unsupported("setAll");
+  void add(E value) => throw _unsupported("add");
+  void addAll(Iterable<E> iterable) => throw _unsupported("addAll");
+  void insert(int index, E element) => throw _unsupported("insert");
+  void insertAll(int at, Iterable<E> iterable) =>
+      throw _unsupported("insertAll");
+  bool remove(Object element) => throw _unsupported("remove");
+  void removeWhere(bool test(E element)) => throw _unsupported("removeWhere");
+  void retainWhere(bool test(E element)) => throw _unsupported("retainWhere");
+  void sort([Comparator<E> compare]) => throw _unsupported("sort");
+  void shuffle([math.Random random]) => throw _unsupported("shuffle");
+  void clear() => throw _unsupported("clear");
+  E removeAt(int index) => throw _unsupported("removeAt");
+  E removeLast() => throw _unsupported("removeLast");
+  void setRange(int start, int end, Iterable<E> iterable,
+          [int skipCount = 0]) =>
+      throw _unsupported("setRange");
+  void removeRange(int start, int end) => throw _unsupported("removeRange");
+  void replaceRange(int start, int end, Iterable<E> iterable) =>
+      throw _unsupported("replaceRange");
+  void fillRange(int start, int end, [E fillValue]) =>
+      throw _unsupported("fillRange");
+}
+
+class PbList<E> extends PbListBase<E> {
+  PbList({check = _checkNotNull}) : super._noList(check: check);
+
+  PbList._(List<E> wrappedList) : super._(wrappedList);
+
+  PbList.from(List from) : super._from(from);
+
+  PbList.forFieldType(int fieldType)
+      : super._noList(check: getCheckFunction(fieldType));
+
+  /// Freezes the list by converting to [FrozenPbList].
+  FrozenPbList<E> toFrozenPbList() => new FrozenPbList<E>.from(this);
+
+  /// Adds [value] at the end of the list, extending the length by one.
+  /// Throws an [UnsupportedError] if the list is not extendable.
+  void add(E value) {
+    _validate(value);
+    _wrappedList.add(value);
+  }
+
+  /// Appends all elements of the [collection] to the end of list.
+  /// Extends the length of the list by the length of [collection].
+  /// Throws an [UnsupportedError] if the list is not extendable.
+  void addAll(Iterable<E> collection) {
+    collection.forEach(_validate);
+    _wrappedList.addAll(collection);
+  }
+
+  /// Returns an [Iterable] of the objects in this list in reverse order.
+  Iterable<E> get reversed => _wrappedList.reversed;
+
+  /// Sorts this list according to the order specified by the [compare]
+  /// function.
+  void sort([int compare(E a, E b)]) => _wrappedList.sort(compare);
+
+  /// Shuffles the elements of this list randomly.
+  void shuffle([math.Random random]) => _wrappedList.shuffle(random);
+
+  /// Removes all objects from this list; the length of the list becomes zero.
+  void clear() => _wrappedList.clear();
+
+  /// Inserts a new element in the list.
+  /// The element must be valid (and not nullable) for the PbList type.
+  void insert(int index, E element) {
+    _validate(element);
+    _wrappedList.insert(index, element);
+  }
+
+  /// Inserts all elements of [iterable] at position [index] in the list.
+  ///
+  /// Elements in [iterable] must be valid and not nullable for the PbList type.
+  void insertAll(int index, Iterable<E> iterable) {
+    iterable.forEach(_validate);
+    _wrappedList.insertAll(index, iterable);
+  }
+
+  /// Overwrites elements of `this` with elements of [iterable] starting at
+  /// position [index] in the list.
+  ///
+  /// Elements in [iterable] must be valid and not nullable for the PbList type.
+  void setAll(int index, Iterable<E> iterable) {
+    iterable.forEach(_validate);
+    _wrappedList.setAll(index, iterable);
+  }
+
+  /// Removes the first occurrence of [value] from this list.
+  bool remove(Object value) => _wrappedList.remove(value);
+
+  /// Removes the object at position [index] from this list.
+  E removeAt(int index) => _wrappedList.removeAt(index);
+
+  /// Pops and returns the last object in this list.
+  E removeLast() => _wrappedList.removeLast();
+
+  /// Removes all objects from this list that satisfy [test].
+  void removeWhere(bool test(E element)) => _wrappedList.removeWhere(test);
+
+  /// Removes all objects from this list that fail to satisfy [test].
+  void retainWhere(bool test(E element)) => _wrappedList.retainWhere(test);
+
+  /// Copies [:end - start:] elements of the [from] array, starting from
+  /// [skipCount], into [:this:], starting at [start].
+  /// Throws an [UnsupportedError] if the list is not extendable.
+  void setRange(int start, int end, Iterable<E> from, [int skipCount = 0]) {
+    // NOTE: In case `take()` returns less than `end - start` elements, the
+    // _wrappedList will fail with a `StateError`.
+    from.skip(skipCount).take(end - start).forEach(_validate);
+    _wrappedList.setRange(start, end, from, skipCount);
+  }
+
+  /// Removes the objects in the range [start] inclusive to [end] exclusive.
+  void removeRange(int start, int end) => _wrappedList.removeRange(start, end);
+
+  /// Sets the objects in the range [start] inclusive to [end] exclusive to the
+  /// given [fillValue].
+  void fillRange(int start, int end, [E fillValue]) {
+    _validate(fillValue);
+    _wrappedList.fillRange(start, end, fillValue);
+  }
+
+  /// Removes the objects in the range [start] inclusive to [end] exclusive and
+  /// inserts the contents of [replacement] in its place.
+  void replaceRange(int start, int end, Iterable<E> replacement) {
+    final values = replacement.toList();
+    replacement.forEach(_validate);
+    _wrappedList.replaceRange(start, end, values);
+  }
+}
+
+abstract class PbListBase<E> extends ListBase<E> {
   final List<E> _wrappedList;
   final CheckFunc<E> check;
 
-  PbList({this.check: _checkNotNull}) : _wrappedList = <E>[] {
+  PbListBase._(this._wrappedList, {this.check: _checkNotNull}) {}
+
+  PbListBase._noList({this.check: _checkNotNull}) : _wrappedList = <E>[] {
     assert(check != null);
   }
 
-  PbList.from(List from)
+  PbListBase._from(List from)
       // TODO(sra): Should this be validated?
       : _wrappedList = new List<E>.from(from),
         check = _checkNotNull;
 
-  factory PbList.forFieldType(int fieldType) =>
-      new PbList(check: getCheckFunction(fieldType));
-
-  bool operator ==(other) => (other is PbList) && _areListsEqual(other, this);
+  @override
+  bool operator ==(other) =>
+      (other is PbListBase) && _areListsEqual(other, this);
 
   int get hashCode {
     int hash = 0;
@@ -40,7 +185,7 @@
   Iterator<E> get iterator => _wrappedList.iterator;
 
   /// Returns a new lazy [Iterable] with elements that are created by calling
-  /// `f` on each element of this `PbList` in iteration order.
+  /// `f` on each element of this `PbListBase` in iteration order.
   Iterable<T> map<T>(T f(E e)) => _wrappedList.map<T>(f);
 
   /// Returns a new lazy [Iterable] with all elements that satisfy the predicate
@@ -132,18 +277,41 @@
 
   /// Returns the element at the given [index] in the list or throws an
   /// [IndexOutOfRangeException] if [index] is out of bounds.
+  @override
   E operator [](int index) => _wrappedList[index];
 
+  /// Returns the number of elements in this collection.
+  int get length => _wrappedList.length;
+
+  // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
+  /// Returns the first index of [element] in this list.
+  int indexOf(Object element, [int start = 0]) =>
+      _wrappedList.indexOf(element, start);
+
+  // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
+  /// Returns the last index of [element] in this list.
+  int lastIndexOf(Object element, [int start]) =>
+      _wrappedList.lastIndexOf(element, start);
+
+  /// Returns a new list containing the objects from [start] inclusive to [end]
+  /// exclusive.
+  List<E> sublist(int start, [int end]) => _wrappedList.sublist(start, end);
+
+  /// Returns an [Iterable] that iterates over the objects in the range [start]
+  /// inclusive to [end] exclusive.
+  Iterable<E> getRange(int start, int end) => _wrappedList.getRange(start, end);
+
+  /// Returns an unmodifiable [Map] view of `this`.
+  Map<int, E> asMap() => _wrappedList.asMap();
+
   /// Sets the entry at the given [index] in the list to [value].
   /// Throws an [IndexOutOfRangeException] if [index] is out of bounds.
+  @override
   void operator []=(int index, E value) {
     _validate(value);
     _wrappedList[index] = value;
   }
 
-  /// Returns the number of elements in this collection.
-  int get length => _wrappedList.length;
-
   /// Unsupported -- violated non-null constraint imposed by protobufs.
   ///
   /// Changes the length of the list. If [newLength] is greater than the current
@@ -156,122 +324,6 @@
     _wrappedList.length = newLength;
   }
 
-  /// Adds [value] at the end of the list, extending the length by one.
-  /// Throws an [UnsupportedError] if the list is not extendable.
-  void add(E value) {
-    _validate(value);
-    _wrappedList.add(value);
-  }
-
-  /// Appends all elements of the [collection] to the end of list.
-  /// Extends the length of the list by the length of [collection].
-  /// Throws an [UnsupportedError] if the list is not extendable.
-  void addAll(Iterable<E> collection) {
-    collection.forEach(_validate);
-    _wrappedList.addAll(collection);
-  }
-
-  /// Returns an [Iterable] of the objects in this list in reverse order.
-  Iterable<E> get reversed => _wrappedList.reversed;
-
-  /// Sorts this list according to the order specified by the [compare]
-  /// function.
-  void sort([int compare(E a, E b)]) => _wrappedList.sort(compare);
-
-  /// Shuffles the elements of this list randomly.
-  void shuffle([math.Random random]) => _wrappedList.shuffle(random);
-
-  // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
-  /// Returns the first index of [element] in this list.
-  int indexOf(Object element, [int start = 0]) =>
-      _wrappedList.indexOf(element, start);
-
-  // TODO(jakobr): E instead of Object once dart-lang/sdk#31311 is fixed.
-  /// Returns the last index of [element] in this list.
-  int lastIndexOf(Object element, [int start]) =>
-      _wrappedList.lastIndexOf(element, start);
-
-  /// Removes all objects from this list; the length of the list becomes zero.
-  void clear() => _wrappedList.clear();
-
-  /// Inserts a new element in the list.
-  /// The element must be valid (and not nullable) for the PbList type.
-  void insert(int index, E element) {
-    _validate(element);
-    _wrappedList.insert(index, element);
-  }
-
-  /// Inserts all elements of [iterable] at position [index] in the list.
-  ///
-  /// Elements in [iterable] must be valid and not nullable for the PbList type.
-  void insertAll(int index, Iterable<E> iterable) {
-    iterable.forEach(_validate);
-    _wrappedList.insertAll(index, iterable);
-  }
-
-  /// Overwrites elements of `this` with elements of [iterable] starting at
-  /// position [index] in the list.
-  ///
-  /// Elements in [iterable] must be valid and not nullable for the PbList type.
-  void setAll(int index, Iterable<E> iterable) {
-    iterable.forEach(_validate);
-    _wrappedList.setAll(index, iterable);
-  }
-
-  /// Removes the first occurrence of [value] from this list.
-  bool remove(Object value) => _wrappedList.remove(value);
-
-  /// Removes the object at position [index] from this list.
-  E removeAt(int index) => _wrappedList.removeAt(index);
-
-  /// Pops and returns the last object in this list.
-  E removeLast() => _wrappedList.removeLast();
-
-  /// Removes all objects from this list that satisfy [test].
-  void removeWhere(bool test(E element)) => _wrappedList.removeWhere(test);
-
-  /// Removes all objects from this list that fail to satisfy [test].
-  void retainWhere(bool test(E element)) => _wrappedList.retainWhere(test);
-
-  /// Returns a new list containing the objects from [start] inclusive to [end]
-  /// exclusive.
-  List<E> sublist(int start, [int end]) => _wrappedList.sublist(start, end);
-
-  /// Returns an [Iterable] that iterates over the objects in the range [start]
-  /// inclusive to [end] exclusive.
-  Iterable<E> getRange(int start, int end) => _wrappedList.getRange(start, end);
-
-  /// Copies [:end - start:] elements of the [from] array, starting from
-  /// [skipCount], into [:this:], starting at [start].
-  /// Throws an [UnsupportedError] if the list is not extendable.
-  void setRange(int start, int end, Iterable<E> from, [int skipCount = 0]) {
-    // NOTE: In case `take()` returns less than `end - start` elements, the
-    // _wrappedList will fail with a `StateError`.
-    from.skip(skipCount).take(end - start).forEach(_validate);
-    _wrappedList.setRange(start, end, from, skipCount);
-  }
-
-  /// Removes the objects in the range [start] inclusive to [end] exclusive.
-  void removeRange(int start, int end) => _wrappedList.removeRange(start, end);
-
-  /// Sets the objects in the range [start] inclusive to [end] exclusive to the
-  /// given [fillValue].
-  void fillRange(int start, int end, [E fillValue]) {
-    _validate(fillValue);
-    _wrappedList.fillRange(start, end, fillValue);
-  }
-
-  /// Removes the objects in the range [start] inclusive to [end] exclusive and
-  /// inserts the contents of [replacement] in its place.
-  void replaceRange(int start, int end, Iterable<E> replacement) {
-    final values = replacement.toList();
-    replacement.forEach(_validate);
-    _wrappedList.replaceRange(start, end, values);
-  }
-
-  /// Returns an unmodifiable [Map] view of `this`.
-  Map<int, E> asMap() => _wrappedList.asMap();
-
   void _validate(E val) {
     check(val);
     // TODO: remove after migration to check functions is finished
diff --git a/lib/src/protobuf/readonly_message.dart b/lib/src/protobuf/readonly_message.dart
index b8f03e0..a711961 100644
--- a/lib/src/protobuf/readonly_message.dart
+++ b/lib/src/protobuf/readonly_message.dart
@@ -8,6 +8,8 @@
 abstract class ReadonlyMessageMixin {
   BuilderInfo get info_;
 
+  bool get _isReadOnly => true;
+
   void addExtension(Extension extension, var value) =>
       _readonly("addExtension");
 
@@ -52,8 +54,7 @@
 
   void _readonly(String methodName) {
     String messageType = info_.messageName;
-    throw new UnsupportedError(
-        "attempted to call $methodName on a read-only message ($messageType)");
+    frozenMessageModificationHandler(messageType, methodName);
   }
 }
 
@@ -92,7 +93,6 @@
   }
 
   void _readonly(String methodName) {
-    throw new UnsupportedError(
-        "attempted to call $methodName on a read-only UnknownFieldSet");
+    frozenMessageModificationHandler('UnknownFieldSet', methodName);
   }
 }
diff --git a/pubspec.yaml b/pubspec.yaml
index ee29bc8..c715c6b 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: protobuf
-version: 0.9.1
+version: 0.10.0
 author: Dart Team <misc@dartlang.org>
 description: Runtime library for protocol buffers support.
 homepage: https://github.com/dart-lang/protobuf
diff --git a/test/json_test.dart b/test/json_test.dart
index 1290d0e..8abce40 100644
--- a/test/json_test.dart
+++ b/test/json_test.dart
@@ -4,6 +4,7 @@
 library json_test;
 
 import 'dart:convert';
+import 'package:fixnum/fixnum.dart' show Int64;
 import 'package:test/test.dart';
 
 import 'mock_util.dart' show T;
@@ -11,13 +12,20 @@
 main() {
   T example = new T()
     ..val = 123
-    ..str = "hello";
+    ..str = "hello"
+    ..int32s.addAll(<int>[1, 2, 3]);
 
   test('testWriteToJson', () {
     String json = example.writeToJson();
     checkJsonMap(jsonDecode(json));
   });
 
+  test('testWriteFrozenToJson', () {
+    final frozen = example.clone()..freeze();
+    final json = frozen.writeToJson();
+    checkJsonMap(jsonDecode(json));
+  });
+
   test('writeToJsonMap', () {
     Map m = example.writeToJsonMap();
     checkJsonMap(m);
@@ -34,12 +42,34 @@
     t.mergeFromJsonMap({"1": 123, "2": "hello"});
     checkMessage(t);
   });
+
+  test('testInt64JsonEncoding', () {
+    final value = Int64.parseInt('1234567890123456789');
+    final t = new T()..int64 = value;
+    final encoded = t.writeToJsonMap();
+    expect(encoded["5"], "$value");
+    final decoded = new T()..mergeFromJsonMap(encoded);
+    expect(decoded.int64, value);
+  });
+
+  test('tesFrozentInt64JsonEncoding', () {
+    final value = Int64.parseInt('1234567890123456789');
+    final frozen = new T()
+      ..int64 = value
+      ..freeze();
+    final encoded = frozen.writeToJsonMap();
+    expect(encoded["5"], "$value");
+    final decoded = new T()..mergeFromJsonMap(encoded);
+    expect(decoded.int64, value);
+  });
 }
 
 checkJsonMap(Map m) {
-  expect(m.length, 2);
+  expect(m.length, 3);
   expect(m["1"], 123);
   expect(m["2"], "hello");
+  print(m.toString());
+  expect(m["4"], [1, 2, 3]);
 }
 
 checkMessage(T t) {
diff --git a/test/message_test.dart b/test/message_test.dart
index 4a541d7..2b0f9e0 100644
--- a/test/message_test.dart
+++ b/test/message_test.dart
@@ -36,6 +36,20 @@
     }, throwsError(ArgumentError, "tag 123 not defined in Rec"));
   });
 
+  test('operator== and hashCode works for frozen message', () {
+    final a = new Rec()
+      ..val = 123
+      ..int32s.addAll([1, 2, 3])
+      ..freeze();
+    final b = new Rec()
+      ..val = 123
+      ..int32s.addAll([1, 2, 3]);
+
+    expect(a.hashCode, b.hashCode);
+    expect(a == b, true);
+    expect(b == a, true);
+  });
+
   test('operator== and hashCode work for a simple record', () {
     var a = new Rec();
     expect(a == a, true);
diff --git a/test/readonly_message_test.dart b/test/readonly_message_test.dart
index 6325a0f..c127032 100644
--- a/test/readonly_message_test.dart
+++ b/test/readonly_message_test.dart
@@ -5,56 +5,246 @@
 
 library readonly_message_test;
 
-import 'package:test/test.dart' show test, expect, throwsA, predicate;
+import 'package:test/test.dart';
 
 import 'package:protobuf/protobuf.dart'
-    show GeneratedMessage, ReadonlyMessageMixin, BuilderInfo;
+    show
+        BuilderInfo,
+        GeneratedMessage,
+        PbFieldType,
+        UnknownFieldSetField,
+        frozenMessageModificationHandler,
+        defaultFrozenMessageModificationHandler;
 
-throwsError(Type expectedType, String expectedMessage) =>
+throwsError(Type expectedType, Matcher expectedMessage) =>
     throwsA(predicate((x) {
       expect(x.runtimeType, expectedType);
       expect(x.message, expectedMessage);
       return true;
     }));
 
-class Rec extends GeneratedMessage with ReadonlyMessageMixin {
+class Rec extends GeneratedMessage {
+  static Rec getDefault() => new Rec()..freeze();
+  static Rec create() => new Rec();
+
   @override
-  BuilderInfo info_ = new BuilderInfo("rec");
+  BuilderInfo info_ = new BuilderInfo('rec')
+    ..a(1, 'value', PbFieldType.O3)
+    ..pp<Rec>(2, 'sub', PbFieldType.PM, (_) {}, Rec.create)
+    ..p<int>(10, 'ints', PbFieldType.P3);
+
+  int get value => $_get(0, 0);
+  set value(int v) {
+    $_setUnsignedInt32(0, v);
+  }
+
+  bool hasValue() => $_has(0);
+
+  List<Rec> get sub => $_getList<Rec>(1);
+
+  List<int> get ints => $_getList<int>(2);
+
   @override
-  clone() => throw new UnimplementedError();
+  Rec clone() => new Rec()..mergeFromMessage(this);
+
+  Rec copyWith(void Function(Rec) updates) =>
+      super.copyWith((message) => updates(message as Rec));
 }
 
 main() {
-  test("can write a read-only message", () {
-    expect(new Rec().writeToBuffer(), []);
-    expect(new Rec().writeToJson(), "{}");
+  test('can write a read-only message', () {
+    expect(Rec.getDefault().writeToBuffer(), []);
+    expect(Rec.getDefault().writeToJson(), "{}");
   });
 
   test("can't merge to a read-only message", () {
     expect(
-        () => new Rec().mergeFromJson("{}"),
+        () => Rec.getDefault().mergeFromJson('{"1":1}'),
         throwsError(UnsupportedError,
-            "attempted to call mergeFromJson on a read-only message (rec)"));
+            equals('Attempted to change a read-only message (rec)')));
   });
 
   test("can't set a field on a read-only message", () {
     expect(
-        () => new Rec().setField(123, 456),
+        () => Rec.getDefault().setField(1, 456),
         throwsError(UnsupportedError,
-            "attempted to call setField on a read-only message (rec)"));
+            equals('Attempted to change a read-only message (rec)')));
+  });
+
+  test('can set a field on a read-only message with a custom read-only handler',
+      () {
+    try {
+      int called = 0;
+
+      frozenMessageModificationHandler =
+          (String messageName, [String methodName]) {
+        expect(messageName, 'rec');
+        expect(methodName, isNull);
+        called++;
+      };
+
+      Rec.getDefault().setField(1, 456);
+      expect(called, 1);
+    } finally {
+      frozenMessageModificationHandler =
+          defaultFrozenMessageModificationHandler;
+    }
   });
 
   test("can't clear a read-only message", () {
     expect(
-        () => new Rec().clear(),
+        () => Rec.getDefault().clear(),
         throwsError(UnsupportedError,
-            "attempted to call clear on a read-only message (rec)"));
+            equals('Attempted to change a read-only message (rec)')));
+  });
+
+  test("can't clear a field on a read-only message", () {
+    expect(
+        () => Rec.getDefault().clearField(1),
+        throwsError(UnsupportedError,
+            equals('Attempted to change a read-only message (rec)')));
+  });
+
+  test("can't modify repeated fields on a read-only message", () {
+    expect(() => Rec.getDefault().sub.add(Rec.create()),
+        throwsError(UnsupportedError, contains('add')));
+    var r = Rec.create()
+      ..ints.add(10)
+      ..freeze();
+    expect(
+        () => r.ints.clear(),
+        throwsError(UnsupportedError,
+            equals('Cannot call clear on an unmodifiable list')));
+    expect(
+        () => r.ints[0] = 2,
+        throwsError(UnsupportedError,
+            equals('Cannot call set on an unmodifiable list')));
+    expect(() => r.sub.add(Rec.create()),
+        throwsError(UnsupportedError, contains('add')));
+
+    r = Rec.create()
+      ..sub.add(Rec.create())
+      ..freeze();
+    expect(
+        () => r.sub.add(Rec.create()),
+        throwsError(UnsupportedError,
+            equals('Cannot call add on an unmodifiable list')));
+    expect(() => r.ints.length = 20,
+        throwsError(UnsupportedError, contains('length')));
+  });
+
+  test("can't modify sub-messages on a read-only message", () {
+    var subMessage = Rec.create()..value = 1;
+    var r = Rec.create()
+      ..sub.add(Rec.create()..sub.add(subMessage))
+      ..freeze();
+    expect(r.sub[0].sub[0].value, 1);
+    expect(
+        () => subMessage.value = 2,
+        throwsError(UnsupportedError,
+            equals('Attempted to change a read-only message (rec)')));
   });
 
   test("can't modify unknown fields on a read-only message", () {
     expect(
-        () => new Rec().unknownFields.clear(),
-        throwsError(UnsupportedError,
-            "attempted to call clear on a read-only UnknownFieldSet"));
+        () => Rec.getDefault().unknownFields.clear(),
+        throwsError(
+            UnsupportedError,
+            equals(
+                "Attempted to call clear on a read-only message (UnknownFieldSet)")));
+  });
+
+  test("can rebuild a frozen message with merge", () {
+    final orig = Rec.create()
+      ..value = 10
+      ..freeze();
+    final rebuilt = orig.copyWith((m) => m.mergeFromJson('{"1": 7}'));
+    expect(identical(orig, rebuilt), false);
+    expect(orig.value, 10);
+    expect(rebuilt.value, 7);
+  });
+
+  test("can set a field while rebuilding a frozen message", () {
+    final orig = Rec.create()
+      ..value = 10
+      ..freeze();
+    final rebuilt = orig.copyWith((m) => m.value = 7);
+    expect(identical(orig, rebuilt), false);
+    expect(orig.value, 10);
+    expect(rebuilt.value, 7);
+  });
+
+  test("can clear while rebuilding a frozen message", () {
+    final orig = Rec.create()
+      ..value = 10
+      ..freeze();
+    final rebuilt = orig.copyWith((m) => m.clear());
+    expect(identical(orig, rebuilt), false);
+    expect(orig.value, 10);
+    expect(orig.hasValue(), true);
+    expect(rebuilt.hasValue(), false);
+  });
+
+  test("can clear a field while rebuilding a frozen message", () {
+    final orig = Rec.create()
+      ..value = 10
+      ..freeze();
+    final rebuilt = orig.copyWith((m) => m.clearField(1));
+    expect(identical(orig, rebuilt), false);
+    expect(orig.value, 10);
+    expect(orig.hasValue(), true);
+    expect(rebuilt.hasValue(), false);
+  });
+
+  test("can modify repeated fields while rebuilding a frozen message", () {
+    var orig = Rec.create()
+      ..ints.add(10)
+      ..freeze();
+    var rebuilt = orig.copyWith((m) => m.ints.add(12));
+    expect(identical(orig, rebuilt), false);
+    expect(orig.ints, [10]);
+    expect(rebuilt.ints, [10, 12]);
+
+    rebuilt = orig.copyWith((m) => m.ints.clear());
+    expect(orig.ints, [10]);
+    expect(rebuilt.ints, []);
+
+    rebuilt = orig.copyWith((m) => m.ints[0] = 2);
+    expect(orig.ints, [10]);
+    expect(rebuilt.ints, [2]);
+
+    orig = Rec.create()
+      ..sub.add(Rec.create())
+      ..freeze();
+    rebuilt = orig.copyWith((m) => m.sub.add(Rec.create()));
+    expect(orig.sub.length, 1);
+    expect(rebuilt.sub.length, 2);
+  });
+
+  test("can modify sub-messages while rebuilding a frozen message", () {
+    final subMessage = Rec.create()..value = 1;
+    final orig = Rec.create()
+      ..sub.add(Rec.create()..sub.add(subMessage))
+      ..freeze();
+
+    final rebuilt = orig.copyWith((m) {
+      expect(
+          () => subMessage.value = 5,
+          throwsError(UnsupportedError,
+              equals('Attempted to change a read-only message (rec)')));
+      m.sub[0].sub[0].value = 2;
+    });
+    expect(identical(subMessage, orig.sub[0].sub[0]), true);
+    expect(identical(subMessage, rebuilt.sub[0].sub[0]), false);
+    expect(orig.sub[0].sub[0].value, 1);
+    expect(rebuilt.sub[0].sub[0].value, 2);
+  });
+
+  test("can modify unknown fields while rebuilding a frozen message", () {
+    final orig = Rec.create()
+      ..unknownFields.addField(20, new UnknownFieldSetField()..fixed32s.add(1));
+    final rebuilt = orig.copyWith((m) => m.unknownFields.clear());
+    expect(orig.unknownFields.hasField(20), true);
+    expect(rebuilt.unknownFields.hasField(20), false);
   });
 }