Add tool to compare type masks
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1adc08d..9321bc6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,8 @@
-## 0.6.0-dev.0.0
+## 0.6.1-dev
+
+* added `diff_masks` tool, mostly used by dart2js developers.
+
+## 0.6.0
 
 This release contains several **breaking changes**:
 
diff --git a/bin/diff_masks.dart b/bin/diff_masks.dart
new file mode 100644
index 0000000..ed5ed72
--- /dev/null
+++ b/bin/diff_masks.dart
@@ -0,0 +1,70 @@
+// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'package:args/command_runner.dart';
+
+import 'package:dart2js_info/src/mask_diff.dart';
+import 'package:dart2js_info/src/io.dart';
+import 'package:dart2js_info/src/util.dart';
+
+import 'usage_exception.dart';
+
+/// A command that computes the diff between type-mask from two info files.
+class DiffMaskCommand extends Command<void> with PrintUsageException {
+  final String name = "diff_masks";
+  final String description =
+      "See type mask differences between two dump-info files.";
+
+  void run() async {
+    var args = argResults.rest;
+    if (args.length < 2) {
+      usageException(
+          'Missing arguments, expected two dump-info files to compare');
+      return;
+    }
+
+    var oldInfo = await infoFromFile(args[0]);
+    var newInfo = await infoFromFile(args[1]);
+
+    var overallSizeDiff = newInfo.program.size - oldInfo.program.size;
+    print('total_size_difference $overallSizeDiff');
+    print('');
+    var diffs = diff(oldInfo, newInfo);
+    int total = diffs.length;
+    int onlyNullTotal = 0;
+    for (var diff in diffs) {
+      bool onlyNull = _compareModuloNull(diff.oldMask, diff.newMask);
+      if (onlyNull) {
+        onlyNullTotal++;
+      } else {
+        print('\n${longName(diff.info, useLibraryUri: true)}:\n'
+            '  old: ${diff.oldMask}\n'
+            '  new: ${diff.newMask}');
+      }
+    }
+
+    print("Only null: $onlyNullTotal of $total (remaining: ${total - onlyNullTotal}");
+  }
+}
+
+bool _compareModuloNull(String a, String b) {
+  int i = 0;
+  int j = 0;
+  while (i < a.length && j < b.length) {
+    if (a[i] == b[j]) {
+      i++;
+      j++;
+      continue;
+    }
+
+    if (a[i] == "n" && a.substring(i, i+5) == "null|") {
+      i+=5;
+      continue;
+    }
+
+    return false;
+  }
+
+  return i == a.length && j == b.length;
+}
diff --git a/bin/tools.dart b/bin/tools.dart
index 32e9e33..974f060 100644
--- a/bin/tools.dart
+++ b/bin/tools.dart
@@ -8,6 +8,7 @@
 import 'coverage_log_server.dart';
 import 'debug_info.dart';
 import 'diff.dart';
+import 'diff_masks.dart';
 import 'deferred_library_check.dart';
 import 'deferred_library_size.dart';
 import 'deferred_library_layout.dart';
@@ -26,6 +27,7 @@
     ..addCommand(new CoverageLogServerCommand())
     ..addCommand(new DebugCommand())
     ..addCommand(new DiffCommand())
+    ..addCommand(new DiffMaskCommand())
     ..addCommand(new DeferredLibraryCheck())
     ..addCommand(new DeferredLibrarySize())
     ..addCommand(new DeferredLibraryLayout())
diff --git a/lib/src/mask_diff.dart b/lib/src/mask_diff.dart
new file mode 100644
index 0000000..9e7302a
--- /dev/null
+++ b/lib/src/mask_diff.dart
@@ -0,0 +1,133 @@
+import 'package:dart2js_info/info.dart';
+import 'package:dart2js_info/src/util.dart';
+
+class MaskDiff {
+  final BasicInfo info;
+  final String oldMask;
+  final String newMask;
+  MaskDiff(this.info, this.oldMask, this.newMask);
+}
+
+List<MaskDiff> diff(AllInfo oldInfo, AllInfo newInfo) {
+  var differ = new _InfoDiffer(oldInfo, newInfo);
+  differ.diff();
+  return differ.diffs;
+}
+
+class _InfoDiffer extends InfoVisitor<Null> {
+  final AllInfo _old;
+  final AllInfo _new;
+
+  BasicInfo _other;
+
+  List<MaskDiff> diffs = <MaskDiff>[];
+
+  _InfoDiffer(this._old, this._new);
+
+  void diff() {
+    _diffList(_old.libraries, _new.libraries);
+  }
+
+  @override
+  visitAll(AllInfo info) {
+    throw new StateError('should not diff AllInfo');
+  }
+
+  @override
+  visitProgram(ProgramInfo info) {
+    throw new StateError('should not diff ProgramInfo');
+  }
+
+  @override
+  visitOutput(OutputUnitInfo info) {
+    throw new StateError('should not diff OutputUnitInfo');
+  }
+
+  // TODO(het): diff constants
+  @override
+  visitConstant(ConstantInfo info) {
+    throw new StateError('should not diff ConstantInfo');
+  }
+
+  @override
+  visitLibrary(LibraryInfo info) {
+    var other = _other as LibraryInfo;
+    _diffList(info.topLevelVariables, other.topLevelVariables);
+    _diffList(info.topLevelFunctions, other.topLevelFunctions);
+    _diffList(info.classes, other.classes);
+  }
+
+  @override
+  visitClass(ClassInfo info) {
+    var other = _other as ClassInfo;
+    _diffList(info.fields, other.fields);
+    _diffList(info.functions, other.functions);
+  }
+
+  @override
+  visitClosure(ClosureInfo info) {
+    var other = _other as ClosureInfo;
+    _diffList([info.function], [other.function]);
+  }
+
+  @override
+  visitField(FieldInfo info) {
+    var other = _other as FieldInfo;
+    if (info.type != other.type) {
+      diffs.add(new MaskDiff(info, info.type, other.type));
+    }
+    _diffList(info.closures, other.closures);
+  }
+
+  String _signature(FunctionInfo info) {
+    var sb = new StringBuffer();
+    sb.write(info.returnType);
+    sb.write("(");
+    for (var parameter in info.parameters) {
+      sb.write(parameter.type);
+      sb.write(" ");
+      sb.write(parameter.name);
+      sb.write(",");
+    }
+    sb.write(")");
+    return '$sb';
+  }
+
+  @override
+  visitFunction(FunctionInfo info) {
+    var other = _other as FunctionInfo;
+    var infoSignature = _signature(info);
+    var otherSignature = _signature(other);
+    if (infoSignature != otherSignature) {
+      diffs.add(new MaskDiff(info, infoSignature, otherSignature));
+    }
+    _diffList(info.closures, other.closures);
+  }
+
+  @override
+  visitTypedef(TypedefInfo info) {}
+
+  void _diffList(List<BasicInfo> oldInfos, List<BasicInfo> newInfos) {
+    var oldNames = <String, BasicInfo>{};
+    var newNames = <String, BasicInfo>{};
+    for (var oldInfo in oldInfos) {
+      oldNames[longName(oldInfo, useLibraryUri: true)] = oldInfo;
+    }
+    for (var newInfo in newInfos) {
+      newNames[longName(newInfo, useLibraryUri: true)] = newInfo;
+    }
+    for (var oldName in oldNames.keys) {
+      if (newNames[oldName] == null) {
+        diffs.add(new MaskDiff(oldNames[oldName], "removed", ""));
+      } else {
+        _other = newNames[oldName];
+        oldNames[oldName].accept(this);
+      }
+    }
+    for (var newName in newNames.keys) {
+      if (oldNames[newName] == null) {
+        diffs.add(new MaskDiff(newNames[newName], "", "added"));
+      }
+    }
+  }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 5a34a94..4076f58 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: dart2js_info
-version: 0.6.0
+version: 0.6.1-dev
 
 description: >
   Libraries and tools to process data produced when running dart2js with