Use getters/setters found through the static types.

This uses the information from the static types now used in inference which
can improve the precision in particular for generic classes. Since type
masks lose the type argument information, we cannot expect locating members
based on the type mask to be more precise than what has been found through
the static types.

This CL also expands the unit test framework to support unit test of
optimizations and the emission model, including tests of the improved
field access handling.

Closes #35433

Change-Id: Ia5de15efaf8b60c8723943bb34de6eec7d380798
Reviewed-on: https://dart-review.googlesource.com/c/88440
Reviewed-by: Stephen Adams <sra@google.com>
diff --git a/pkg/compiler/lib/src/js_model/js_world.dart b/pkg/compiler/lib/src/js_model/js_world.dart
index e277dd8..39e86a0 100644
--- a/pkg/compiler/lib/src/js_model/js_world.dart
+++ b/pkg/compiler/lib/src/js_model/js_world.dart
@@ -456,11 +456,6 @@
         .any((each) => each.isGetter);
   }
 
-  FieldEntity locateSingleField(Selector selector, AbstractValue receiver) {
-    MemberEntity result = locateSingleMember(selector, receiver);
-    return (result != null && result.isField) ? result : null;
-  }
-
   MemberEntity locateSingleMember(Selector selector, AbstractValue receiver) {
     if (includesClosureCall(selector, receiver)) {
       return null;
diff --git a/pkg/compiler/lib/src/ssa/builder_kernel.dart b/pkg/compiler/lib/src/ssa/builder_kernel.dart
index 3ce5084..1c14b38 100644
--- a/pkg/compiler/lib/src/ssa/builder_kernel.dart
+++ b/pkg/compiler/lib/src/ssa/builder_kernel.dart
@@ -4253,11 +4253,11 @@
 
     AbstractValue type = _typeInferenceMap.selectorTypeOf(selector, mask);
     if (selector.isGetter) {
-      push(new HInvokeDynamicGetter(selector, mask, null, inputs, isIntercepted,
-          type, sourceInformation));
+      push(new HInvokeDynamicGetter(selector, mask, element, inputs,
+          isIntercepted, type, sourceInformation));
     } else if (selector.isSetter) {
-      push(new HInvokeDynamicSetter(selector, mask, null, inputs, isIntercepted,
-          type, sourceInformation));
+      push(new HInvokeDynamicSetter(selector, mask, element, inputs,
+          isIntercepted, type, sourceInformation));
     } else if (selector.isClosureCall) {
       assert(!isIntercepted);
       push(new HInvokeClosure(selector, inputs, type, typeArguments)
diff --git a/pkg/compiler/lib/src/ssa/codegen.dart b/pkg/compiler/lib/src/ssa/codegen.dart
index 05c7a6e..9a23841 100644
--- a/pkg/compiler/lib/src/ssa/codegen.dart
+++ b/pkg/compiler/lib/src/ssa/codegen.dart
@@ -1998,8 +1998,8 @@
     }
   }
 
-  void registerSetter(HInvokeDynamic node) {
-    if (node.element != null) {
+  void registerSetter(HInvokeDynamic node, {bool needsCheck: false}) {
+    if (node.element is FieldEntity && !needsCheck) {
       // This is a dynamic update which we have found to have a single
       // target but for some reason haven't inlined. We are _still_ accessing
       // the target dynamically but we don't need to enqueue more than target
@@ -2036,7 +2036,7 @@
     push(js
         .propertyCall(pop(), name, visitArguments(node.inputs))
         .withSourceInformation(node.sourceInformation));
-    registerSetter(node);
+    registerSetter(node, needsCheck: node.needsCheck);
   }
 
   visitInvokeDynamicGetter(HInvokeDynamicGetter node) {
diff --git a/pkg/compiler/lib/src/ssa/logging.dart b/pkg/compiler/lib/src/ssa/logging.dart
new file mode 100644
index 0000000..c213791
--- /dev/null
+++ b/pkg/compiler/lib/src/ssa/logging.dart
@@ -0,0 +1,42 @@
+// Copyright (c) 2018, 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 'nodes.dart';
+import '../util/features.dart';
+
+/// Log used for unit testing optimizations.
+class OptimizationLog {
+  List<OptimizationLogEntry> entries = [];
+
+  void registerFieldGet(HInvokeDynamicGetter node, HFieldGet fieldGet) {
+    Features features = new Features();
+    features['name'] =
+        '${fieldGet.element.enclosingClass.name}.${fieldGet.element.name}';
+    entries.add(new OptimizationLogEntry('FieldGet', features));
+  }
+
+  void registerFieldSet(HInvokeDynamicSetter node, HFieldSet fieldSet) {
+    Features features = new Features();
+    features['name'] =
+        '${fieldSet.element.enclosingClass.name}.${fieldSet.element.name}';
+    entries.add(new OptimizationLogEntry('FieldSet', features));
+  }
+
+  String getText() {
+    return entries.join(',\n');
+  }
+}
+
+/// A registered optimization.
+class OptimizationLogEntry {
+  /// String that uniquely identifies the optimization kind.
+  final String tag;
+
+  /// Additional data for this optimization.
+  final Features features;
+
+  OptimizationLogEntry(this.tag, this.features);
+
+  String toString() => '$tag(${features.getText()})';
+}
diff --git a/pkg/compiler/lib/src/ssa/nodes.dart b/pkg/compiler/lib/src/ssa/nodes.dart
index 432eb78..568ae0b 100644
--- a/pkg/compiler/lib/src/ssa/nodes.dart
+++ b/pkg/compiler/lib/src/ssa/nodes.dart
@@ -1736,6 +1736,10 @@
 }
 
 class HInvokeDynamicSetter extends HInvokeDynamicField {
+  /// If `true` a call to the setter is needed for checking the type even
+  /// though the target field is known.
+  bool needsCheck = false;
+
   HInvokeDynamicSetter(
       Selector selector,
       AbstractValue mask,
@@ -1752,7 +1756,8 @@
 
   List<DartType> get typeArguments => const <DartType>[];
 
-  String toString() => 'invoke dynamic setter: selector=$selector, mask=$mask';
+  String toString() =>
+      'invoke dynamic setter: selector=$selector, mask=$mask, element=$element';
 }
 
 class HInvokeStatic extends HInvoke {
@@ -1914,7 +1919,7 @@
   int typeCode() => HInstruction.FIELD_GET_TYPECODE;
   bool typeEquals(other) => other is HFieldGet;
   bool dataEquals(HFieldGet other) => element == other.element;
-  String toString() => "FieldGet $element";
+  String toString() => "FieldGet(element=$element,type=$instructionType)";
 }
 
 class HFieldSet extends HFieldAccess {
@@ -1936,7 +1941,7 @@
   accept(HVisitor visitor) => visitor.visitFieldSet(this);
 
   bool isJsStatement() => true;
-  String toString() => "FieldSet $element";
+  String toString() => "FieldSet(element=$element,type=$instructionType)";
 }
 
 class HGetLength extends HInstruction {
diff --git a/pkg/compiler/lib/src/ssa/optimize.dart b/pkg/compiler/lib/src/ssa/optimize.dart
index f245b5c..72a69eb 100644
--- a/pkg/compiler/lib/src/ssa/optimize.dart
+++ b/pkg/compiler/lib/src/ssa/optimize.dart
@@ -2,6 +2,7 @@
 // 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 '../common.dart';
 import '../common/codegen.dart' show CodegenRegistry, CodegenWorkItem;
 import '../common/names.dart' show Selectors;
 import '../common/tasks.dart' show CompilerTask;
@@ -26,6 +27,7 @@
 import '../util/util.dart';
 import '../world.dart' show JClosedWorld;
 import 'interceptor_simplifier.dart';
+import 'logging.dart';
 import 'nodes.dart';
 import 'types.dart';
 import 'types_propagation.dart';
@@ -42,6 +44,8 @@
 
   Map<HInstruction, Range> ranges = <HInstruction, Range>{};
 
+  Map<MemberEntity, OptimizationLog> loggersForTesting;
+
   SsaOptimizerTask(this._backend) : super(_backend.compiler.measurer);
 
   String get name => 'SSA optimizer';
@@ -65,12 +69,19 @@
     Set<HInstruction> boundsChecked = new Set<HInstruction>();
     SsaCodeMotion codeMotion;
     SsaLoadElimination loadElimination;
+
+    OptimizationLog log;
+    if (retainDataForTesting) {
+      loggersForTesting ??= {};
+      loggersForTesting[work.element] = log = new OptimizationLog();
+    }
+
     measure(() {
       List<OptimizationPhase> phases = <OptimizationPhase>[
         // Run trivial instruction simplification first to optimize
         // some patterns useful for type conversion.
         new SsaInstructionSimplifier(globalInferenceResults, _options,
-            _rtiSubstitutions, closedWorld, registry),
+            _rtiSubstitutions, closedWorld, registry, log),
         new SsaTypeConversionInserter(closedWorld),
         new SsaRedundantPhiEliminator(),
         new SsaDeadPhiEliminator(),
@@ -79,10 +90,10 @@
         // After type propagation, more instructions can be
         // simplified.
         new SsaInstructionSimplifier(globalInferenceResults, _options,
-            _rtiSubstitutions, closedWorld, registry),
+            _rtiSubstitutions, closedWorld, registry, log),
         new SsaCheckInserter(trustPrimitives, closedWorld, boundsChecked),
         new SsaInstructionSimplifier(globalInferenceResults, _options,
-            _rtiSubstitutions, closedWorld, registry),
+            _rtiSubstitutions, closedWorld, registry, log),
         new SsaCheckInserter(trustPrimitives, closedWorld, boundsChecked),
         new SsaTypePropagator(globalInferenceResults, _options,
             closedWorld.commonElements, closedWorld),
@@ -108,7 +119,7 @@
         // Previous optimizations may have generated new
         // opportunities for instruction simplification.
         new SsaInstructionSimplifier(globalInferenceResults, _options,
-            _rtiSubstitutions, closedWorld, registry),
+            _rtiSubstitutions, closedWorld, registry, log),
         new SsaCheckInserter(trustPrimitives, closedWorld, boundsChecked),
       ];
       phases.forEach(runPhase);
@@ -131,7 +142,7 @@
           new SsaCodeMotion(closedWorld.abstractValueDomain),
           new SsaValueRangeAnalyzer(closedWorld, this),
           new SsaInstructionSimplifier(globalInferenceResults, _options,
-              _rtiSubstitutions, closedWorld, registry),
+              _rtiSubstitutions, closedWorld, registry, log),
           new SsaCheckInserter(trustPrimitives, closedWorld, boundsChecked),
           new SsaSimplifyInterceptors(closedWorld, work.element.enclosingClass),
           new SsaDeadCodeEliminator(closedWorld, this),
@@ -143,7 +154,7 @@
           // Run the simplifier to remove unneeded type checks inserted by
           // type propagation.
           new SsaInstructionSimplifier(globalInferenceResults, _options,
-              _rtiSubstitutions, closedWorld, registry),
+              _rtiSubstitutions, closedWorld, registry, log),
         ];
       }
       phases.forEach(runPhase);
@@ -188,10 +199,11 @@
   final RuntimeTypesSubstitutions _rtiSubstitutions;
   final JClosedWorld _closedWorld;
   final CodegenRegistry _registry;
+  final OptimizationLog _log;
   HGraph _graph;
 
   SsaInstructionSimplifier(this._globalInferenceResults, this._options,
-      this._rtiSubstitutions, this._closedWorld, this._registry);
+      this._rtiSubstitutions, this._closedWorld, this._registry, this._log);
 
   JCommonElements get commonElements => _closedWorld.commonElements;
 
@@ -1103,9 +1115,12 @@
   }
 
   FieldEntity findConcreteFieldForDynamicAccess(
-      HInstruction receiver, Selector selector) {
+      HInvokeDynamicField node, HInstruction receiver) {
     AbstractValue receiverType = receiver.instructionType;
-    return _closedWorld.locateSingleField(selector, receiverType);
+    MemberEntity member = node.element is FieldEntity
+        ? node.element
+        : _closedWorld.locateSingleMember(node.selector, receiverType);
+    return member is FieldEntity ? member : null;
   }
 
   HInstruction visitFieldGet(HFieldGet node) {
@@ -1197,27 +1212,30 @@
       if (folded != node) return folded;
     }
     HInstruction receiver = node.getDartReceiver(_closedWorld);
-    FieldEntity field =
-        findConcreteFieldForDynamicAccess(receiver, node.selector);
-    if (field != null) return directFieldGet(receiver, field);
+    FieldEntity field = node.element is FieldEntity
+        ? node.element
+        : findConcreteFieldForDynamicAccess(node, receiver);
+    if (field != null) {
+      HFieldGet result = _directFieldGet(receiver, field, node);
+      _log?.registerFieldGet(node, result);
+      return result;
+    }
 
-    if (node.element == null) {
-      MemberEntity element = _closedWorld.locateSingleMember(
-          node.selector, receiver.instructionType);
-      if (element != null && element.name == node.selector.name) {
-        node.element = element;
-        if (element.isFunction) {
-          // A property extraction getter, aka a tear-off.
-          node.sideEffects.clearAllDependencies();
-          node.sideEffects.clearAllSideEffects();
-          node.setUseGvn(); // We don't care about identity of tear-offs.
-        }
-      }
+    node.element ??= _closedWorld.locateSingleMember(
+        node.selector, receiver.instructionType);
+    if (node.element != null &&
+        node.element.name == node.selector.name &&
+        node.element.isFunction) {
+      // A property extraction getter, aka a tear-off.
+      node.sideEffects.clearAllDependencies();
+      node.sideEffects.clearAllSideEffects();
+      node.setUseGvn(); // We don't care about identity of tear-offs.
     }
     return node;
   }
 
-  HInstruction directFieldGet(HInstruction receiver, FieldEntity field) {
+  HInstruction _directFieldGet(
+      HInstruction receiver, FieldEntity field, HInstruction node) {
     bool isAssignable = !_closedWorld.fieldNeverChanges(field);
 
     AbstractValue type;
@@ -1225,10 +1243,14 @@
       type = AbstractValueFactory.fromNativeBehavior(
           _nativeData.getNativeFieldLoadBehavior(field), _closedWorld);
     } else {
+      // TODO(johnniwinther): Use the potentially more precise type of the
+      // node + find a test that shows its usefulness.
+      // type = _abstractValueDomain.intersection(
+      //     node.instructionType,
+      //     AbstractValueFactory.inferredTypeForMember(
+      //         field, _globalInferenceResults));
       type = AbstractValueFactory.inferredTypeForMember(
-          // ignore: UNNECESSARY_CAST
-          field as Entity,
-          _globalInferenceResults);
+          field, _globalInferenceResults);
     }
 
     return new HFieldGet(field, receiver, type, isAssignable: isAssignable);
@@ -1241,8 +1263,7 @@
     }
 
     HInstruction receiver = node.getDartReceiver(_closedWorld);
-    FieldEntity field =
-        findConcreteFieldForDynamicAccess(receiver, node.selector);
+    FieldEntity field = findConcreteFieldForDynamicAccess(node, receiver);
     if (field == null || !field.isAssignable) return node;
     // Use `node.inputs.last` in case the call follows the interceptor calling
     // convention, but is not a call on an interceptor.
@@ -1257,6 +1278,7 @@
         // inline this access.
         // TODO(sra): If the input is such that we don't need a type check, we
         // can skip the test an generate the HFieldSet.
+        node.needsCheck = true;
         return node;
       }
       HInstruction other = value.convertType(
@@ -1266,7 +1288,10 @@
         value = other;
       }
     }
-    return new HFieldSet(_abstractValueDomain, field, receiver, value);
+    HFieldSet result =
+        new HFieldSet(_abstractValueDomain, field, receiver, value);
+    _log?.registerFieldSet(node, result);
+    return result;
   }
 
   HInstruction visitInvokeClosure(HInvokeClosure node) {
diff --git a/pkg/compiler/lib/src/util/features.dart b/pkg/compiler/lib/src/util/features.dart
new file mode 100644
index 0000000..57aeba2
--- /dev/null
+++ b/pkg/compiler/lib/src/util/features.dart
@@ -0,0 +1,150 @@
+// Copyright (c) 2019, 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.
+
+/// Set of features used in annotations.
+class Features {
+  Map<String, Object> _features = {};
+
+  void add(String key, {var value: ''}) {
+    _features[key] = value.toString();
+  }
+
+  void addElement(String key, [var value]) {
+    List<String> list = _features.putIfAbsent(key, () => <String>[]);
+    if (value != null) {
+      list.add(value.toString());
+    }
+  }
+
+  bool containsKey(String key) {
+    return _features.containsKey(key);
+  }
+
+  void operator []=(String key, String value) {
+    _features[key] = value;
+  }
+
+  String operator [](String key) => _features[key];
+
+  String remove(String key) => _features.remove(key);
+
+  void forEach(void Function(String, Object) f) {
+    _features.forEach(f);
+  }
+
+  /// Returns a string containing all features in a comma-separated list sorted
+  /// by feature names.
+  String getText() {
+    StringBuffer sb = new StringBuffer();
+    bool needsComma = false;
+    for (String name in _features.keys.toList()..sort()) {
+      dynamic value = _features[name];
+      if (value != null) {
+        if (needsComma) {
+          sb.write(',');
+        }
+        sb.write(name);
+        if (value is List<String>) {
+          value = '[${(value..sort()).join(',')}]';
+        }
+        if (value != '') {
+          sb.write('=');
+          sb.write(value);
+        }
+        needsComma = true;
+      }
+    }
+    return sb.toString();
+  }
+
+  /// Creates a [Features] object by parse the [text] encoding.
+  ///
+  /// Single features will be parsed as strings and list features (features
+  /// encoded in `[...]` will be parsed as lists of strings.
+  static Features fromText(String text) {
+    Features features = new Features();
+    int index = 0;
+    while (index < text.length) {
+      int eqPos = text.indexOf('=', index);
+      int commaPos = text.indexOf(',', index);
+      String name;
+      bool hasValue = false;
+      if (eqPos != -1 && commaPos != -1) {
+        if (eqPos < commaPos) {
+          name = text.substring(index, eqPos);
+          hasValue = true;
+          index = eqPos + 1;
+        } else {
+          name = text.substring(index, commaPos);
+          index = commaPos + 1;
+        }
+      } else if (eqPos != -1) {
+        name = text.substring(index, eqPos);
+        hasValue = true;
+        index = eqPos + 1;
+      } else if (commaPos != -1) {
+        name = text.substring(index, commaPos);
+        index = commaPos + 1;
+      } else {
+        name = text.substring(index);
+        index = text.length;
+      }
+      if (hasValue) {
+        const Map<String, String> delimiters = const {
+          '[': ']',
+          '{': '}',
+          '(': ')',
+          '<': '>'
+        };
+        List<String> endDelimiters = <String>[];
+        bool isList = index < text.length && text.startsWith('[', index);
+        if (isList) {
+          features.addElement(name);
+          endDelimiters.add(']');
+          index++;
+        }
+        int valueStart = index;
+        while (index < text.length) {
+          String char = text.substring(index, index + 1);
+          if (endDelimiters.isNotEmpty && endDelimiters.last == char) {
+            endDelimiters.removeLast();
+            index++;
+          } else {
+            String endDelimiter = delimiters[char];
+            if (endDelimiter != null) {
+              endDelimiters.add(endDelimiter);
+              index++;
+            } else if (char == ',') {
+              if (endDelimiters.isEmpty) {
+                break;
+              } else if (endDelimiters.length == 1 && isList) {
+                String value = text.substring(valueStart, index);
+                features.addElement(name, value);
+                index++;
+                valueStart = index;
+              } else {
+                index++;
+              }
+            } else {
+              index++;
+            }
+          }
+        }
+        if (isList) {
+          String value = text.substring(valueStart, index - 1);
+          if (value.isNotEmpty) {
+            features.addElement(name, value);
+          }
+        } else {
+          String value = text.substring(valueStart, index);
+          features.add(name, value: value);
+        }
+        index++;
+      } else {
+        features.add(name);
+      }
+    }
+    return features;
+  }
+}
diff --git a/pkg/compiler/lib/src/world.dart b/pkg/compiler/lib/src/world.dart
index b2bcb10..0ed8695 100644
--- a/pkg/compiler/lib/src/world.dart
+++ b/pkg/compiler/lib/src/world.dart
@@ -201,11 +201,6 @@
   /// Returns the single [MemberEntity] that matches a call to [selector] on the
   /// [receiver]. If multiple targets exist, `null` is returned.
   MemberEntity locateSingleMember(Selector selector, AbstractValue receiver);
-
-  /// Returns the single field that matches a call to [selector] on the
-  /// [receiver]. If multiple targets exist or the single target is not a field,
-  /// `null` is returned.
-  FieldEntity locateSingleField(Selector selector, AbstractValue receiver);
 }
 
 abstract class OpenWorld implements World {
diff --git a/tests/compiler/dart2js/analyses/analysis_helper.dart b/tests/compiler/dart2js/analyses/analysis_helper.dart
index 8ce572d..9e1560e 100644
--- a/tests/compiler/dart2js/analyses/analysis_helper.dart
+++ b/tests/compiler/dart2js/analyses/analysis_helper.dart
@@ -7,6 +7,7 @@
 
 import 'package:args/args.dart';
 import 'package:async_helper/async_helper.dart';
+import 'package:compiler/src/common.dart';
 import 'package:compiler/src/compiler.dart';
 import 'package:compiler/src/diagnostics/diagnostic_listener.dart';
 import 'package:compiler/src/diagnostics/messages.dart';
@@ -204,18 +205,16 @@
             }
           }
         });
-        _actualMessages.forEach((String uri,
-            Map<String, List<DiagnosticMessage>> actualMessagesMap) {
-          if (!_expectedJson.containsKey(uri)) {
-            actualMessagesMap.forEach(
-                (String message, List<DiagnosticMessage> actualMessages) {
-              if (!expectedMessages.containsKey(message)) {
-                for (DiagnosticMessage message in actualMessages) {
-                  reporter.reportError(message);
-                  errorCount++;
-                }
-              }
-            });
+      }
+    });
+    _actualMessages.forEach(
+        (String uri, Map<String, List<DiagnosticMessage>> actualMessagesMap) {
+      if (!_expectedJson.containsKey(uri)) {
+        actualMessagesMap
+            .forEach((String message, List<DiagnosticMessage> actualMessages) {
+          for (DiagnosticMessage message in actualMessages) {
+            reporter.reportError(message);
+            errorCount++;
           }
         });
       }
diff --git a/tests/compiler/dart2js/analyses/dart2js_allowed.json b/tests/compiler/dart2js/analyses/dart2js_allowed.json
index 7f837eb..cb3e829 100644
--- a/tests/compiler/dart2js/analyses/dart2js_allowed.json
+++ b/tests/compiler/dart2js/analyses/dart2js_allowed.json
@@ -273,6 +273,10 @@
     "Dynamic invocation of '[]='.": 1,
     "Dynamic invocation of 'add'.": 1
   },
+  "pkg/compiler/lib/src/util/features.dart": {
+    "Dynamic invocation of 'sort'.": 1,
+    "Dynamic invocation of 'join'.": 1
+  },
   "pkg/compiler/lib/src/js_emitter/startup_emitter/fragment_emitter.dart": {
     "Dynamic access of 'superclass'.": 1,
     "Dynamic access of 'needsTearOff'.": 1
diff --git a/tests/compiler/dart2js/closure/closure_test.dart b/tests/compiler/dart2js/closure/closure_test.dart
index 3cc81e2..566f192 100644
--- a/tests/compiler/dart2js/closure/closure_test.dart
+++ b/tests/compiler/dart2js/closure/closure_test.dart
@@ -13,6 +13,7 @@
 import 'package:compiler/src/js_model/js_world.dart';
 import 'package:compiler/src/js_model/locals.dart';
 import 'package:compiler/src/universe/codegen_world_builder.dart';
+import 'package:compiler/src/util/features.dart';
 import 'package:expect/expect.dart';
 import '../equivalence/id_equivalence.dart';
 import '../equivalence/id_equivalence_helper.dart';
@@ -27,12 +28,12 @@
   });
 }
 
-class ClosureDataComputer extends DataComputer {
+class ClosureDataComputer extends DataComputer<String> {
   const ClosureDataComputer();
 
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
@@ -54,10 +55,13 @@
             verbose: verbose)
         .run(definition.node);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
 /// Kernel IR visitor for computing closure data.
-class ClosureIrChecker extends IrDataExtractor {
+class ClosureIrChecker extends IrDataExtractor<String> {
   final MemberEntity member;
   final ClosureData closureDataLookup;
   final CodegenWorldBuilder codegenWorldBuilder;
@@ -73,7 +77,7 @@
 
   ClosureIrChecker(
       DiagnosticReporter reporter,
-      Map<Id, ActualData> actualMap,
+      Map<Id, ActualData<String>> actualMap,
       JsToElementMap elementMap,
       this.member,
       this._localsMap,
diff --git a/tests/compiler/dart2js/codegen/model_data/dynamic_get.dart b/tests/compiler/dart2js/codegen/model_data/dynamic_get.dart
new file mode 100644
index 0000000..c014519
--- /dev/null
+++ b/tests/compiler/dart2js/codegen/model_data/dynamic_get.dart
@@ -0,0 +1,62 @@
+// Copyright (c) 2019, 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.
+
+main() {
+  method1(new Class1a());
+  method2(new Class2a<int>());
+  method3(new Class3a());
+  method3(new Class3b());
+  method4(new Class4a());
+  method4(new Class4b());
+}
+
+class Class1a {
+  /*element: Class1a.field1:*/
+  int field1;
+}
+
+@pragma('dart2js:noInline')
+method1(dynamic c) {
+  return c.field1;
+}
+
+class Class2a<T> {
+  /*element: Class2a.field2:*/
+  T field2;
+}
+
+@pragma('dart2js:noInline')
+method2(dynamic c) {
+  return c.field2;
+}
+
+class Class3a {
+  /*element: Class3a.field3:get=simple*/
+  int field3;
+}
+
+class Class3b {
+  /*element: Class3b.field3:get=simple*/
+  int field3;
+}
+
+@pragma('dart2js:noInline')
+method3(dynamic c) {
+  return c.field3;
+}
+
+class Class4a {
+  /*element: Class4a.field4:get=simple*/
+  int field4;
+}
+
+class Class4b implements Class4a {
+  /*element: Class4b.field4:get=simple*/
+  int field4;
+}
+
+@pragma('dart2js:noInline')
+method4(Class4a c) {
+  return c.field4;
+}
diff --git a/tests/compiler/dart2js/codegen/model_data/dynamic_set.dart b/tests/compiler/dart2js/codegen/model_data/dynamic_set.dart
new file mode 100644
index 0000000..ac96348
--- /dev/null
+++ b/tests/compiler/dart2js/codegen/model_data/dynamic_set.dart
@@ -0,0 +1,67 @@
+// Copyright (c) 2019, 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.
+
+main() {
+  method1(new Class1a());
+  method2(new Class2a<int>());
+  method3(new Class3a());
+  method3(new Class3b());
+  method4(new Class4a());
+  method4(new Class4b());
+}
+
+class Class1a {
+  /*element: Class1a.field1:*/
+  int field1;
+}
+
+@pragma('dart2js:noInline')
+method1(dynamic c) {
+  c.field1 = 42;
+}
+
+class Class2a<T> {
+  /*strong.element: Class2a.field2:checked*/
+  /*omit.element: Class2a.field2:*/
+  T field2;
+}
+
+@pragma('dart2js:noInline')
+method2(dynamic c) {
+  c.field2 = 42;
+}
+
+class Class3a {
+  /*strong.element: Class3a.field3:checked*/
+  /*omit.element: Class3a.field3:set=simple*/
+  int field3;
+}
+
+class Class3b {
+  /*strong.element: Class3b.field3:checked*/
+  /*omit.element: Class3b.field3:set=simple*/
+  int field3;
+}
+
+@pragma('dart2js:noInline')
+method3(dynamic c) {
+  c.field3 = 42;
+}
+
+class Class4a {
+  /*strong.element: Class4a.field4:checked*/
+  /*omit.element: Class4a.field4:set=simple*/
+  int field4;
+}
+
+class Class4b implements Class4a {
+  /*strong.element: Class4b.field4:checked*/
+  /*omit.element: Class4b.field4:set=simple*/
+  int field4;
+}
+
+@pragma('dart2js:noInline')
+method4(Class4a c) {
+  c.field4 = 42;
+}
diff --git a/tests/compiler/dart2js/codegen/model_test.dart b/tests/compiler/dart2js/codegen/model_test.dart
new file mode 100644
index 0000000..e51540a
--- /dev/null
+++ b/tests/compiler/dart2js/codegen/model_test.dart
@@ -0,0 +1,120 @@
+// Copyright (c) 2019, 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 'dart:io';
+import 'package:async_helper/async_helper.dart';
+import 'package:compiler/src/closure.dart';
+import 'package:compiler/src/common.dart';
+import 'package:compiler/src/compiler.dart';
+import 'package:compiler/src/diagnostics/diagnostic_listener.dart';
+import 'package:compiler/src/elements/entities.dart';
+import 'package:compiler/src/js_emitter/model.dart';
+import 'package:compiler/src/js_model/element_map.dart';
+import 'package:compiler/src/js_model/js_world.dart';
+import 'package:compiler/src/util/features.dart';
+import 'package:kernel/ast.dart' as ir;
+import '../equivalence/id_equivalence.dart';
+import '../equivalence/id_equivalence_helper.dart';
+import '../helpers/program_lookup.dart';
+
+main(List<String> args) {
+  asyncTest(() async {
+    Directory dataDir =
+        new Directory.fromUri(Platform.script.resolve('model_data'));
+    await checkTests(dataDir, const ModelDataComputer(),
+        testOmit: true, args: args);
+  });
+}
+
+class ModelDataComputer extends DataComputer<String> {
+  const ModelDataComputer();
+
+  /// Compute type inference data for [member] from kernel based inference.
+  ///
+  /// Fills [actualMap] with the data.
+  @override
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
+      {bool verbose: false}) {
+    JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
+    JsToElementMap elementMap = closedWorld.elementMap;
+    MemberDefinition definition = elementMap.getMemberDefinition(member);
+    new ModelIrComputer(compiler.reporter, actualMap, elementMap, member,
+            compiler, closedWorld.closureDataLookup)
+        .run(definition.node);
+  }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
+}
+
+class Tags {
+  static const String needsCheckedSetter = 'checked';
+  static const String getterFlags = 'get';
+  static const String setterFlags = 'set';
+}
+
+/// AST visitor for computing inference data for a member.
+class ModelIrComputer extends IrDataExtractor<String> {
+  final JsToElementMap _elementMap;
+  final ClosureData _closureDataLookup;
+  final ProgramLookup _programLookup;
+
+  ModelIrComputer(
+      DiagnosticReporter reporter,
+      Map<Id, ActualData<String>> actualMap,
+      this._elementMap,
+      MemberEntity member,
+      Compiler compiler,
+      this._closureDataLookup)
+      : _programLookup = new ProgramLookup(compiler),
+        super(reporter, actualMap);
+
+  String getMemberValue(MemberEntity member) {
+    if (member is FieldEntity) {
+      Field field = _programLookup.getField(member);
+      if (field != null) {
+        Features features = new Features();
+        if (field.needsCheckedSetter) {
+          features.add(Tags.needsCheckedSetter);
+        }
+        void registerFlags(String tag, int flags) {
+          switch (flags) {
+            case 0:
+              break;
+            case 1:
+              features.add(tag, value: 'simple');
+              break;
+            case 2:
+              features.add(tag, value: 'intercepted');
+              break;
+            case 3:
+              features.add(tag, value: 'interceptedThis');
+              break;
+          }
+        }
+
+        registerFlags(Tags.getterFlags, field.getterFlags);
+        registerFlags(Tags.setterFlags, field.setterFlags);
+
+        return features.getText();
+      }
+    }
+    return null;
+  }
+
+  @override
+  String computeMemberValue(Id id, ir.Member node) {
+    return getMemberValue(_elementMap.getMember(node));
+  }
+
+  @override
+  String computeNodeValue(Id id, ir.TreeNode node) {
+    if (node is ir.FunctionExpression || node is ir.FunctionDeclaration) {
+      ClosureRepresentationInfo info = _closureDataLookup.getClosureInfo(node);
+      return getMemberValue(info.callMethod);
+    }
+    return null;
+  }
+}
diff --git a/tests/compiler/dart2js/deferred_loading/deferred_loading_test.dart b/tests/compiler/dart2js/deferred_loading/deferred_loading_test.dart
index 099c508..ac0d87e 100644
--- a/tests/compiler/dart2js/deferred_loading/deferred_loading_test.dart
+++ b/tests/compiler/dart2js/deferred_loading/deferred_loading_test.dart
@@ -76,7 +76,7 @@
   return 'OutputUnit(${unit.name}, {$sb})';
 }
 
-class OutputUnitDataComputer extends DataComputer {
+class OutputUnitDataComputer extends DataComputer<String> {
   const OutputUnitDataComputer();
 
   /// OutputData for [member] as a kernel based element.
@@ -86,8 +86,8 @@
   /// fill [actualMap] with the data computed about what the resulting OutputUnit
   /// is.
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
@@ -102,7 +102,7 @@
 
   @override
   void computeClassData(
-      Compiler compiler, ClassEntity cls, Map<Id, ActualData> actualMap,
+      Compiler compiler, ClassEntity cls, Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     OutputUnitData data = closedWorld.outputUnitData;
@@ -119,16 +119,19 @@
         actualMap,
         compiler.reporter);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
-class OutputUnitIrComputer extends IrDataExtractor {
+class OutputUnitIrComputer extends IrDataExtractor<String> {
   final JsToElementMap _elementMap;
   final OutputUnitData _data;
   final ClosureData _closureDataLookup;
 
   OutputUnitIrComputer(
       DiagnosticReporter reporter,
-      Map<Id, ActualData> actualMap,
+      Map<Id, ActualData<String>> actualMap,
       this._elementMap,
       MemberEntity member,
       this._data,
@@ -180,10 +183,10 @@
 /// Set [actualMap] to hold a key of [id] with the computed data [value]
 /// corresponding to [object] at location [sourceSpan]. We also perform error
 /// checking to ensure that the same [id] isn't added twice.
-void _registerValue(Id id, String value, Object object, SourceSpan sourceSpan,
-    Map<Id, ActualData> actualMap, CompilerDiagnosticReporter reporter) {
+void _registerValue<T>(Id id, T value, Object object, SourceSpan sourceSpan,
+    Map<Id, ActualData<T>> actualMap, CompilerDiagnosticReporter reporter) {
   if (actualMap.containsKey(id)) {
-    ActualData existingData = actualMap[id];
+    ActualData<T> existingData = actualMap[id];
     reportHere(reporter, sourceSpan,
         "Duplicate id ${id}, value=$value, object=$object");
     reportHere(
@@ -194,6 +197,6 @@
     Expect.fail("Duplicate id $id.");
   }
   if (value != null) {
-    actualMap[id] = new ActualData(new IdValue(id, value), sourceSpan, object);
+    actualMap[id] = new ActualData<T>(id, value, sourceSpan, object);
   }
 }
diff --git a/tests/compiler/dart2js/equivalence/id_equivalence.dart b/tests/compiler/dart2js/equivalence/id_equivalence.dart
index 73f4299..4d76541 100644
--- a/tests/compiler/dart2js/equivalence/id_equivalence.dart
+++ b/tests/compiler/dart2js/equivalence/id_equivalence.dart
@@ -42,7 +42,9 @@
     return id == other.id && value == other.value;
   }
 
-  String toString() {
+  String toString() => idToString(id, value);
+
+  static String idToString(Id id, String value) {
     switch (id.kind) {
       case IdKind.element:
         ElementId elementId = id;
@@ -207,17 +209,18 @@
   String toString() => '$kind:$value';
 }
 
-class ActualData {
-  final IdValue value;
+class ActualData<T> {
+  final Id id;
+  final T value;
   final SourceSpan sourceSpan;
   final Object object;
 
-  ActualData(this.value, this.sourceSpan, this.object);
+  ActualData(this.id, this.value, this.sourceSpan, this.object);
 
   int get offset {
-    Id id = value.id;
     if (id is NodeId) {
-      return id.value;
+      NodeId nodeId = id;
+      return nodeId.value;
     } else {
       return sourceSpan.begin;
     }
@@ -228,17 +231,16 @@
   }
 
   String toString() =>
-      'ActualData(value=$value,sourceSpan=$sourceSpan,object=$objectText)';
+      'ActualData(id=$id,value=$value,sourceSpan=$sourceSpan,object=$objectText)';
 }
 
-abstract class DataRegistry {
+abstract class DataRegistry<T> {
   DiagnosticReporter get reporter;
-  Map<Id, ActualData> get actualMap;
+  Map<Id, ActualData<T>> get actualMap;
 
-  void registerValue(
-      SourceSpan sourceSpan, Id id, String value, Object object) {
+  void registerValue(SourceSpan sourceSpan, Id id, T value, Object object) {
     if (actualMap.containsKey(id)) {
-      ActualData existingData = actualMap[id];
+      ActualData<T> existingData = actualMap[id];
       reportHere(reporter, sourceSpan,
           "Duplicate id ${id}, value=$value, object=$object");
       reportHere(
@@ -249,8 +251,7 @@
       Expect.fail("Duplicate id $id.");
     }
     if (value != null) {
-      actualMap[id] =
-          new ActualData(new IdValue(id, value), sourceSpan, object);
+      actualMap[id] = new ActualData<T>(id, value, sourceSpan, object);
     }
   }
 }
@@ -270,32 +271,32 @@
 
 /// Abstract IR visitor for computing data corresponding to a node or element,
 /// and record it with a generic [Id]
-abstract class IrDataExtractor extends ir.Visitor with DataRegistry {
+abstract class IrDataExtractor<T> extends ir.Visitor with DataRegistry<T> {
   final DiagnosticReporter reporter;
-  final Map<Id, ActualData> actualMap;
+  final Map<Id, ActualData<T>> actualMap;
 
   /// Implement this to compute the data corresponding to [member].
   ///
   /// If `null` is returned, [member] has no associated data.
-  String computeMemberValue(Id id, ir.Member member);
+  T computeMemberValue(Id id, ir.Member member);
 
   /// Implement this to compute the data corresponding to [node].
   ///
   /// If `null` is returned, [node] has no associated data.
-  String computeNodeValue(Id id, ir.TreeNode node);
+  T computeNodeValue(Id id, ir.TreeNode node);
 
   IrDataExtractor(this.reporter, this.actualMap);
 
   void computeForMember(ir.Member member) {
     ElementId id = computeEntityId(member);
     if (id == null) return;
-    String value = computeMemberValue(id, member);
+    T value = computeMemberValue(id, member);
     registerValue(computeSourceSpan(member), id, value, member);
   }
 
   void computeForNode(ir.TreeNode node, NodeId id) {
     if (id == null) return;
-    String value = computeNodeValue(id, node);
+    T value = computeNodeValue(id, node);
     registerValue(computeSourceSpan(node), id, value, node);
   }
 
diff --git a/tests/compiler/dart2js/equivalence/id_equivalence_helper.dart b/tests/compiler/dart2js/equivalence/id_equivalence_helper.dart
index 5762f41..b046038 100644
--- a/tests/compiler/dart2js/equivalence/id_equivalence_helper.dart
+++ b/tests/compiler/dart2js/equivalence/id_equivalence_helper.dart
@@ -20,6 +20,15 @@
 /// `true` if ANSI colors are supported by stdout.
 bool useColors = stdout.supportsAnsiEscapes;
 
+/// Colorize a message [text], if ANSI colors are supported.
+String colorizeMessage(String text) {
+  if (useColors) {
+    return '${colors.yellow(text)}';
+  } else {
+    return text;
+  }
+}
+
 /// Colorize a matching annotation [text], if ANSI colors are supported.
 String colorizeMatch(String text) {
   if (useColors) {
@@ -78,7 +87,7 @@
   return '${colorizeDelimiter(start)}$text${colorizeDelimiter(end)}';
 }
 
-abstract class DataComputer {
+abstract class DataComputer<T> {
   const DataComputer();
 
   /// Called before testing to setup flags needed for data collection.
@@ -92,7 +101,7 @@
   /// Fills [actualMap] with the data and [sourceSpanMap] with the source spans
   /// for the data origin.
   void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+      Compiler compiler, MemberEntity member, Map<Id, ActualData<T>> actualMap,
       {bool verbose});
 
   /// Returns `true` if [computeClassData] is supported.
@@ -103,8 +112,10 @@
   /// Fills [actualMap] with the data and [sourceSpanMap] with the source spans
   /// for the data origin.
   void computeClassData(
-      Compiler compiler, ClassEntity cls, Map<Id, ActualData> actualMap,
+      Compiler compiler, ClassEntity cls, Map<Id, ActualData<T>> actualMap,
       {bool verbose}) {}
+
+  DataInterpreter<T> get dataValidator;
 }
 
 const String stopAfterTypeInference = 'stopAfterTypeInference';
@@ -130,8 +141,8 @@
 /// [entryPoint] and [memorySourceFiles].
 ///
 /// Actual data is computed using [computeMemberData].
-Future<CompiledData> computeData(Uri entryPoint,
-    Map<String, String> memorySourceFiles, DataComputer dataComputer,
+Future<CompiledData<T>> computeData<T>(Uri entryPoint,
+    Map<String, String> memorySourceFiles, DataComputer<T> dataComputer,
     {List<String> options: const <String>[],
     bool verbose: false,
     bool testFrontend: false,
@@ -159,14 +170,14 @@
   ElementEnvironment elementEnvironment = closedWorld.elementEnvironment;
   CommonElements commonElements = closedWorld.commonElements;
 
-  Map<Uri, Map<Id, ActualData>> actualMaps = <Uri, Map<Id, ActualData>>{};
-  Map<Id, ActualData> globalData = <Id, ActualData>{};
+  Map<Uri, Map<Id, ActualData<T>>> actualMaps = <Uri, Map<Id, ActualData<T>>>{};
+  Map<Id, ActualData<T>> globalData = <Id, ActualData<T>>{};
 
-  Map<Id, ActualData> actualMapFor(Entity entity) {
+  Map<Id, ActualData<T>> actualMapFor(Entity entity) {
     SourceSpan span =
         compiler.backendStrategy.spanFromSpannable(entity, entity);
     Uri uri = span.uri;
-    return actualMaps.putIfAbsent(uri, () => <Id, ActualData>{});
+    return actualMaps.putIfAbsent(uri, () => <Id, ActualData<T>>{});
   }
 
   void processMember(MemberEntity member, Map<Id, ActualData> actualMap) {
@@ -194,7 +205,7 @@
         verbose: verbose);
   }
 
-  void processClass(ClassEntity cls, Map<Id, ActualData> actualMap) {
+  void processClass(ClassEntity cls, Map<Id, ActualData<T>> actualMap) {
     if (skipUnprocessedMembers && !closedWorld.isImplemented(cls)) {
       return;
     }
@@ -281,22 +292,22 @@
     }
   }
 
-  return new CompiledData(
+  return new CompiledData<T>(
       compiler, elementEnvironment, entryPoint, actualMaps, globalData);
 }
 
-class CompiledData {
+class CompiledData<T> {
   final Compiler compiler;
   final ElementEnvironment elementEnvironment;
   final Uri mainUri;
-  final Map<Uri, Map<Id, ActualData>> actualMaps;
-  final Map<Id, ActualData> globalData;
+  final Map<Uri, Map<Id, ActualData<T>>> actualMaps;
+  final Map<Id, ActualData<T>> globalData;
 
   CompiledData(this.compiler, this.elementEnvironment, this.mainUri,
       this.actualMaps, this.globalData);
 
   Map<int, List<String>> computeAnnotations(Uri uri) {
-    Map<Id, ActualData> thisMap = actualMaps[uri];
+    Map<Id, ActualData<T>> thisMap = actualMaps[uri];
     Map<int, List<String>> annotations = <int, List<String>>{};
     thisMap.forEach((Id id, ActualData data1) {
       String value1 = '${data1.value}';
@@ -308,7 +319,7 @@
   }
 
   Map<int, List<String>> computeDiffAnnotationsAgainst(
-      Map<Id, ActualData> thisMap, Map<Id, ActualData> otherMap, Uri uri,
+      Map<Id, ActualData<T>> thisMap, Map<Id, ActualData<T>> otherMap, Uri uri,
       {bool includeMatches: false}) {
     Map<int, List<String>> annotations = <int, List<String>>{};
     thisMap.forEach((Id id, ActualData data1) {
@@ -361,15 +372,15 @@
 }
 
 /// Data collected by [computeData].
-class IdData {
+class IdData<T> {
   final Map<Uri, AnnotatedCode> code;
   final MemberAnnotations<IdValue> expectedMaps;
   final CompiledData _compiledData;
-  final MemberAnnotations<ActualData> _actualMaps = new MemberAnnotations();
+  final MemberAnnotations<ActualData<T>> _actualMaps = new MemberAnnotations();
 
   IdData(this.code, this.expectedMaps, this._compiledData) {
     for (Uri uri in code.keys) {
-      _actualMaps[uri] = _compiledData.actualMaps[uri] ?? <Id, ActualData>{};
+      _actualMaps[uri] = _compiledData.actualMaps[uri] ?? <Id, ActualData<T>>{};
     }
     _actualMaps.globalData.addAll(_compiledData.globalData);
   }
@@ -389,15 +400,20 @@
     return withAnnotations(code[uri].sourceCode, annotations);
   }
 
-  String diffCode(Uri uri) {
+  String diffCode(Uri uri, DataInterpreter<T> dataValidator) {
     Map<int, List<String>> annotations = <int, List<String>>{};
     actualMaps[uri].forEach((Id id, ActualData data) {
-      IdValue value = expectedMaps[uri][id];
-      if (data.value != value || value == null && data.value.value != '') {
-        String expected = value?.toString() ?? '';
+      IdValue expectedValue = expectedMaps[uri][id];
+      T actualValue = data.value;
+      String unexpectedMessage =
+          dataValidator.isAsExpected(actualValue, expectedValue?.value);
+      if (unexpectedMessage != null) {
+        /*if (data.value != expectedValue || expectedValue == null && data.value.value != '') {*/
+        String expected = expectedValue?.toString() ?? '';
+        String actual = dataValidator.getText(actualValue);
         int offset = getOffsetFromId(id, uri);
         String value1 = '${expected}';
-        String value2 = '${data.value}';
+        String value2 = '${actual}';
         annotations
             .putIfAbsent(offset, () => [])
             .add(colorizeDiff(value1, ' | ', value2));
@@ -492,10 +508,10 @@
 /// [setUpFunction] is called once for every test that is executed.
 /// If [forUserSourceFilesOnly] is true, we examine the elements in the main
 /// file and any supporting libraries.
-Future checkTests(Directory dataDir, DataComputer dataComputer,
+Future checkTests<T>(Directory dataDir, DataComputer<T> dataComputer,
     {bool testStrongMode: true,
     List<String> skipForStrong: const <String>[],
-    bool filterActualData(IdValue idValue, ActualData actualData),
+    bool filterActualData(IdValue idValue, ActualData<T> actualData),
     List<String> options: const <String>[],
     List<String> args: const <String>[],
     Directory libDirectory: null,
@@ -603,15 +619,15 @@
           options.add(Flags.omitImplicitChecks);
         }
         MemberAnnotations<IdValue> annotations = expectedMaps[strongMarker];
-        CompiledData compiledData2 = await computeData(
+        CompiledData<T> compiledData2 = await computeData(
             entryPoint, memorySourceFiles, dataComputer,
             options: options,
             verbose: verbose,
             testFrontend: testFrontend,
             forUserLibrariesOnly: forUserLibrariesOnly,
             globalIds: annotations.globalData.keys);
-        if (await checkCode(
-            strongName, entity.uri, code, annotations, compiledData2,
+        if (await checkCode(strongName, entity.uri, code, annotations,
+            compiledData2, dataComputer.dataValidator,
             filterActualData: filterActualData,
             fatalErrors: !testAfterFailures)) {
           hasFailures = true;
@@ -628,15 +644,15 @@
           Flags.laxRuntimeTypeToString
         ]..addAll(testOptions);
         MemberAnnotations<IdValue> annotations = expectedMaps[omitMarker];
-        CompiledData compiledData2 = await computeData(
+        CompiledData<T> compiledData2 = await computeData(
             entryPoint, memorySourceFiles, dataComputer,
             options: options,
             verbose: verbose,
             testFrontend: testFrontend,
             forUserLibrariesOnly: forUserLibrariesOnly,
             globalIds: annotations.globalData.keys);
-        if (await checkCode(
-            trustName, entity.uri, code, annotations, compiledData2,
+        if (await checkCode(trustName, entity.uri, code, annotations,
+            compiledData2, dataComputer.dataValidator,
             filterActualData: filterActualData,
             fatalErrors: !testAfterFailures)) {
           hasFailures = true;
@@ -650,34 +666,75 @@
 
 final Set<String> userFiles = new Set<String>();
 
+/// Interface used for interpreting annotations.
+abstract class DataInterpreter<T> {
+  /// Returns `null` if [actualData] satisfies the [expectedData] annotation.
+  /// Otherwise, a message is returned contain the information about the
+  /// problems found.
+  String isAsExpected(T actualData, String expectedData);
+
+  /// Returns `true` if [actualData] corresponds to empty data.
+  bool isEmpty(T actualData);
+
+  /// Returns a textual representation of [actualData].
+  String getText(T actualData);
+}
+
+/// Default data interpreter for string data.
+class StringDataInterpreter implements DataInterpreter<String> {
+  const StringDataInterpreter();
+
+  @override
+  String isAsExpected(String actualData, String expectedData) {
+    actualData ??= '';
+    expectedData ??= '';
+    if (actualData != expectedData) {
+      return "Expected $expectedData, found $actualData";
+    }
+    return null;
+  }
+
+  @override
+  bool isEmpty(String actualData) {
+    return actualData == '';
+  }
+
+  @override
+  String getText(String actualData) {
+    return actualData;
+  }
+}
+
 /// Checks [compiledData] against the expected data in [expectedMap] derived
 /// from [code].
-Future<bool> checkCode(
+Future<bool> checkCode<T>(
     String mode,
     Uri mainFileUri,
     Map<Uri, AnnotatedCode> code,
     MemberAnnotations<IdValue> expectedMaps,
     CompiledData compiledData,
-    {bool filterActualData(IdValue expected, ActualData actualData),
+    DataInterpreter<T> dataValidator,
+    {bool filterActualData(IdValue expected, ActualData<T> actualData),
     bool fatalErrors: true}) async {
-  IdData data = new IdData(code, expectedMaps, compiledData);
+  IdData<T> data = new IdData<T>(code, expectedMaps, compiledData);
   bool hasFailure = false;
   Set<Uri> neededDiffs = new Set<Uri>();
 
   void checkActualMap(
-      Map<Id, ActualData> actualMap, Map<Id, IdValue> expectedMap,
+      Map<Id, ActualData<T>> actualMap, Map<Id, IdValue> expectedMap,
       [Uri uri]) {
     bool hasLocalFailure = false;
-    actualMap.forEach((Id id, ActualData actualData) {
-      IdValue actual = actualData.value;
+    actualMap.forEach((Id id, ActualData<T> actualData) {
+      T actual = actualData.value;
+      String actualText = dataValidator.getText(actual);
 
       if (!expectedMap.containsKey(id)) {
-        if (actual.value != '') {
+        if (!dataValidator.isEmpty(actual)) {
           reportError(
               data.compiler.reporter,
               actualData.sourceSpan,
               'EXTRA $mode DATA for ${id.descriptor} = '
-              '${colorizeActual('$actual')} for ${actualData.objectText}. '
+              '${colorizeActual('${IdValue.idToString(id, actualText)}')} for ${actualData.objectText}. '
               'Data was expected for these ids: ${expectedMap.keys}');
           if (filterActualData == null || filterActualData(null, actualData)) {
             hasLocalFailure = true;
@@ -685,14 +742,17 @@
         }
       } else {
         IdValue expected = expectedMap[id];
-        if (actual != expected) {
+        String unexpectedMessage =
+            dataValidator.isAsExpected(actual, expected.value);
+        if (unexpectedMessage != null) {
           reportError(
               data.compiler.reporter,
               actualData.sourceSpan,
-              'UNEXPECTED $mode DATA for ${id.descriptor}: '
-              'Object: ${actualData.objectText}\n '
+              'UNEXPECTED $mode DATA for ${id.descriptor}: \n '
+              'detail  : ${colorizeMessage(unexpectedMessage)}\n '
+              'object  : ${actualData.objectText}\n '
               'expected: ${colorizeExpected('$expected')}\n '
-              'actual  : ${colorizeActual('$actual')}');
+              'actual  : ${colorizeActual('${IdValue.idToString(id, actualText)}')}');
           if (filterActualData == null ||
               filterActualData(expected, actualData)) {
             hasLocalFailure = true;
@@ -740,7 +800,7 @@
   checkMissing(data.expectedMaps.globalData, data.actualMaps.globalData);
   for (Uri uri in neededDiffs) {
     print('--annotations diff [${uri.pathSegments.last}]-------------');
-    print(data.diffCode(uri));
+    print(data.diffCode(uri, dataValidator));
     print('----------------------------------------------------------');
   }
   if (missingIds.isNotEmpty) {
@@ -847,56 +907,3 @@
     }
   });
 }
-
-/// Set of features used in annotations.
-class Features {
-  Map<String, Object> _features = <String, Object>{};
-
-  void add(String key, {var value: ''}) {
-    _features[key] = value.toString();
-  }
-
-  void addElement(String key, [var value]) {
-    List<String> list = _features.putIfAbsent(key, () => <String>[]);
-    if (value != null) {
-      list.add(value.toString());
-    }
-  }
-
-  bool containsKey(String key) {
-    return _features.containsKey(key);
-  }
-
-  void operator []=(String key, String value) {
-    _features[key] = value;
-  }
-
-  String operator [](String key) => _features[key];
-
-  String remove(String key) => _features.remove(key);
-
-  /// Returns a string containing all features in a comma-separated list sorted
-  /// by feature names.
-  String getText() {
-    StringBuffer sb = new StringBuffer();
-    bool needsComma = false;
-    for (String name in _features.keys.toList()..sort()) {
-      dynamic value = _features[name];
-      if (value != null) {
-        if (needsComma) {
-          sb.write(',');
-        }
-        sb.write(name);
-        if (value is List<String>) {
-          value = '[${(value..sort()).join(',')}]';
-        }
-        if (value != '') {
-          sb.write('=');
-          sb.write(value);
-        }
-        needsComma = true;
-      }
-    }
-    return sb.toString();
-  }
-}
diff --git a/tests/compiler/dart2js/helpers/program_lookup.dart b/tests/compiler/dart2js/helpers/program_lookup.dart
index ab3ecff..e01108d 100644
--- a/tests/compiler/dart2js/helpers/program_lookup.dart
+++ b/tests/compiler/dart2js/helpers/program_lookup.dart
@@ -79,13 +79,21 @@
       return getLibraryData(function.library).getMethod(function);
     }
   }
+
+  Field getField(FieldEntity field) {
+    if (field.enclosingClass != null) {
+      return getClassData(field.enclosingClass).getField(field);
+    } else {
+      return getLibraryData(field.library).getField(field);
+    }
+  }
 }
 
 class LibraryData {
   final Library library;
-  Map<ClassEntity, ClassData> _classMap = <ClassEntity, ClassData>{};
-  Map<FunctionEntity, StaticMethod> _methodMap =
-      <FunctionEntity, StaticMethod>{};
+  Map<ClassEntity, ClassData> _classMap = {};
+  Map<FunctionEntity, StaticMethod> _methodMap = {};
+  Map<FieldEntity, Field> _fieldMap = {};
 
   LibraryData(this.library) {
     for (Class cls in library.classes) {
@@ -104,6 +112,18 @@
         _methodMap[method.element] = method;
       }
     }
+    for (Field field in library.staticFieldsForReflection) {
+      ClassEntity enclosingClass = field.element?.enclosingClass;
+      if (enclosingClass != null) {
+        ClassData data =
+            _classMap.putIfAbsent(enclosingClass, () => new ClassData(null));
+        assert(!data._fieldMap.containsKey(field.element));
+        data._fieldMap[field.element] = field;
+      } else if (field.element != null) {
+        assert(!_fieldMap.containsKey(field.element));
+        _fieldMap[field.element] = field;
+      }
+    }
   }
 
   ClassData getClassData(ClassEntity element) {
@@ -113,11 +133,17 @@
   StaticMethod getMethod(FunctionEntity function) {
     return _methodMap[function];
   }
+
+  Field getField(FieldEntity field) {
+    return _fieldMap[field];
+  }
 }
 
 class ClassData {
   final Class cls;
-  Map<FunctionEntity, Method> _methodMap = <FunctionEntity, Method>{};
+  Map<FunctionEntity, Method> _methodMap = {};
+  Map<FieldEntity, Field> _fieldMap = {};
+  Map<FieldEntity, StubMethod> _checkedSetterMap = {};
 
   ClassData(this.cls) {
     if (cls != null) {
@@ -125,12 +151,28 @@
         assert(!_methodMap.containsKey(method.element));
         _methodMap[method.element] = method;
       }
+      for (Field field in cls.fields) {
+        assert(!_fieldMap.containsKey(field.element));
+        _fieldMap[field.element] = field;
+      }
+      for (StubMethod checkedSetter in cls.checkedSetters) {
+        assert(!_checkedSetterMap.containsKey(checkedSetter.element));
+        _checkedSetterMap[checkedSetter.element] = checkedSetter;
+      }
     }
   }
 
   Method getMethod(FunctionEntity function) {
     return _methodMap[function];
   }
+
+  Field getField(FieldEntity field) {
+    return _fieldMap[field];
+  }
+
+  StubMethod getCheckedSetter(FieldEntity field) {
+    return _checkedSetterMap[field];
+  }
 }
 
 void forEachNode(js.Node root,
diff --git a/tests/compiler/dart2js/impact/data/async.dart b/tests/compiler/dart2js/impact/data/async.dart
index 7098aef..24ab850 100644
--- a/tests/compiler/dart2js/impact/data/async.dart
+++ b/tests/compiler/dart2js/impact/data/async.dart
@@ -15,7 +15,7 @@
   testLocalAsyncStar(0),
   testLocalSyncStar(0),
   testSyncStar(0)],
-  type=[inst:JSNull]
+ type=[inst:JSNull]
 */
 main() {
   testSyncStar();
diff --git a/tests/compiler/dart2js/impact/impact_test.dart b/tests/compiler/dart2js/impact/impact_test.dart
index c5a6cb9..abef8a2 100644
--- a/tests/compiler/dart2js/impact/impact_test.dart
+++ b/tests/compiler/dart2js/impact/impact_test.dart
@@ -12,6 +12,7 @@
 import 'package:compiler/src/universe/feature.dart';
 import 'package:compiler/src/universe/use.dart';
 import 'package:compiler/src/universe/world_impact.dart';
+import 'package:compiler/src/util/features.dart';
 import 'package:kernel/ast.dart' as ir;
 import '../equivalence/id_equivalence.dart';
 import '../equivalence/id_equivalence_helper.dart';
@@ -32,12 +33,12 @@
   static const String runtimeTypeUse = 'runtimeType';
 }
 
-class ImpactDataComputer extends DataComputer {
+class ImpactDataComputer extends DataComputer<String> {
   const ImpactDataComputer();
 
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     KernelFrontEndStrategy frontendStrategy = compiler.frontendStrategy;
     WorldImpact impact = compiler.impactCache[member];
@@ -71,7 +72,10 @@
       }
     }
     Id id = computeEntityId(node);
-    actualMap[id] = new ActualData(new IdValue(id, features.getText()),
-        computeSourceSpanFromTreeNode(node), member);
+    actualMap[id] = new ActualData<String>(
+        id, features.getText(), computeSourceSpanFromTreeNode(node), member);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
diff --git a/tests/compiler/dart2js/inference/callers_test.dart b/tests/compiler/dart2js/inference/callers_test.dart
index cbcd56c..5a6a719 100644
--- a/tests/compiler/dart2js/inference/callers_test.dart
+++ b/tests/compiler/dart2js/inference/callers_test.dart
@@ -25,12 +25,12 @@
   });
 }
 
-class CallersDataComputer extends DataComputer {
+class CallersDataComputer extends DataComputer<String> {
   const CallersDataComputer();
 
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
@@ -43,16 +43,23 @@
             closedWorld.closureDataLookup)
         .run(definition.node);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
 /// AST visitor for computing side effects data for a member.
-class CallersIrComputer extends IrDataExtractor {
+class CallersIrComputer extends IrDataExtractor<String> {
   final TypeGraphInferrer inferrer;
   final JsToElementMap _elementMap;
   final ClosureData _closureDataLookup;
 
-  CallersIrComputer(DiagnosticReporter reporter, Map<Id, ActualData> actualMap,
-      this._elementMap, this.inferrer, this._closureDataLookup)
+  CallersIrComputer(
+      DiagnosticReporter reporter,
+      Map<Id, ActualData<String>> actualMap,
+      this._elementMap,
+      this.inferrer,
+      this._closureDataLookup)
       : super(reporter, actualMap);
 
   String getMemberValue(MemberEntity member) {
diff --git a/tests/compiler/dart2js/inference/inference_data_test.dart b/tests/compiler/dart2js/inference/inference_data_test.dart
index 4f3d3e9..c0a964f 100644
--- a/tests/compiler/dart2js/inference/inference_data_test.dart
+++ b/tests/compiler/dart2js/inference/inference_data_test.dart
@@ -12,6 +12,7 @@
 import 'package:compiler/src/js_backend/inferred_data.dart';
 import 'package:compiler/src/js_model/element_map.dart';
 import 'package:compiler/src/js_model/js_world.dart';
+import 'package:compiler/src/util/features.dart';
 import 'package:kernel/ast.dart' as ir;
 import '../equivalence/id_equivalence.dart';
 import '../equivalence/id_equivalence_helper.dart';
@@ -31,15 +32,15 @@
   static const String cannotThrow = 'no-throw';
 }
 
-class InferenceDataComputer extends DataComputer {
+class InferenceDataComputer extends DataComputer<String> {
   const InferenceDataComputer();
 
   /// Compute side effects data for [member] from kernel based inference.
   ///
   /// Fills [actualMap] with the data.
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
@@ -48,15 +49,21 @@
             compiler.globalInference.resultsForTesting.inferredData)
         .run(definition.node);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
 /// AST visitor for computing side effects data for a member.
-class InferredDataIrComputer extends IrDataExtractor {
+class InferredDataIrComputer extends IrDataExtractor<String> {
   final JsClosedWorld closedWorld;
   final InferredData inferredData;
 
-  InferredDataIrComputer(DiagnosticReporter reporter,
-      Map<Id, ActualData> actualMap, this.closedWorld, this.inferredData)
+  InferredDataIrComputer(
+      DiagnosticReporter reporter,
+      Map<Id, ActualData<String>> actualMap,
+      this.closedWorld,
+      this.inferredData)
       : super(reporter, actualMap);
 
   JsToElementMap get _elementMap => closedWorld.elementMap;
diff --git a/tests/compiler/dart2js/inference/inference_test_helper.dart b/tests/compiler/dart2js/inference/inference_test_helper.dart
index 37feb43..5b5eb7f 100644
--- a/tests/compiler/dart2js/inference/inference_test_helper.dart
+++ b/tests/compiler/dart2js/inference/inference_test_helper.dart
@@ -41,15 +41,15 @@
   });
 }
 
-class TypeMaskDataComputer extends DataComputer {
+class TypeMaskDataComputer extends DataComputer<String> {
   const TypeMaskDataComputer();
 
   /// Compute type inference data for [member] from kernel based inference.
   ///
   /// Fills [actualMap] with the data.
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
@@ -65,10 +65,13 @@
             closedWorld.closureDataLookup)
         .run(definition.node);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
 /// IR visitor for computing inference data for a member.
-class TypeMaskIrComputer extends IrDataExtractor {
+class TypeMaskIrComputer extends IrDataExtractor<String> {
   final GlobalTypeInferenceResults results;
   GlobalTypeInferenceMemberResult result;
   final JsToElementMap _elementMap;
@@ -77,7 +80,7 @@
 
   TypeMaskIrComputer(
       DiagnosticReporter reporter,
-      Map<Id, ActualData> actualMap,
+      Map<Id, ActualData<String>> actualMap,
       this._elementMap,
       MemberEntity member,
       this._localsMap,
diff --git a/tests/compiler/dart2js/inference/side_effects_test.dart b/tests/compiler/dart2js/inference/side_effects_test.dart
index 4b04662..0e2e5cb 100644
--- a/tests/compiler/dart2js/inference/side_effects_test.dart
+++ b/tests/compiler/dart2js/inference/side_effects_test.dart
@@ -25,7 +25,7 @@
   });
 }
 
-class SideEffectsDataComputer extends DataComputer {
+class SideEffectsDataComputer extends DataComputer<String> {
   const SideEffectsDataComputer();
 
   /// Compute side effects data for [member] from kernel based inference.
@@ -42,15 +42,21 @@
             compiler.globalInference.resultsForTesting.inferredData)
         .run(definition.node);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
 /// AST visitor for computing side effects data for a member.
-class SideEffectsIrComputer extends IrDataExtractor {
+class SideEffectsIrComputer extends IrDataExtractor<String> {
   final JsClosedWorld closedWorld;
   final InferredData inferredData;
 
-  SideEffectsIrComputer(DiagnosticReporter reporter,
-      Map<Id, ActualData> actualMap, this.closedWorld, this.inferredData)
+  SideEffectsIrComputer(
+      DiagnosticReporter reporter,
+      Map<Id, ActualData<String>> actualMap,
+      this.closedWorld,
+      this.inferredData)
       : super(reporter, actualMap);
 
   JsToElementMap get _elementMap => closedWorld.elementMap;
diff --git a/tests/compiler/dart2js/inlining/inlining_test.dart b/tests/compiler/dart2js/inlining/inlining_test.dart
index b71d909..c6a8b92 100644
--- a/tests/compiler/dart2js/inlining/inlining_test.dart
+++ b/tests/compiler/dart2js/inlining/inlining_test.dart
@@ -26,15 +26,15 @@
   });
 }
 
-class InliningDataComputer extends DataComputer {
+class InliningDataComputer extends DataComputer<String> {
   const InliningDataComputer();
 
   /// Compute type inference data for [member] from kernel based inference.
   ///
   /// Fills [actualMap] with the data.
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
@@ -43,17 +43,20 @@
             compiler.backend, closedWorld.closureDataLookup)
         .run(definition.node);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
 /// AST visitor for computing inference data for a member.
-class InliningIrComputer extends IrDataExtractor {
+class InliningIrComputer extends IrDataExtractor<String> {
   final JavaScriptBackend backend;
   final JsToElementMap _elementMap;
   final ClosureData _closureDataLookup;
 
   InliningIrComputer(
       DiagnosticReporter reporter,
-      Map<Id, ActualData> actualMap,
+      Map<Id, ActualData<String>> actualMap,
       this._elementMap,
       MemberEntity member,
       this.backend,
diff --git a/tests/compiler/dart2js/jumps/jump_test.dart b/tests/compiler/dart2js/jumps/jump_test.dart
index ee33c6c..d06c533 100644
--- a/tests/compiler/dart2js/jumps/jump_test.dart
+++ b/tests/compiler/dart2js/jumps/jump_test.dart
@@ -26,7 +26,7 @@
   });
 }
 
-class JumpDataComputer extends DataComputer {
+class JumpDataComputer extends DataComputer<String> {
   const JumpDataComputer();
 
   /// Compute closure data mapping for [member] as a kernel based element.
@@ -34,8 +34,8 @@
   /// Fills [actualMap] with the data and [sourceSpanMap] with the source spans
   /// for the data origin.
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
@@ -45,6 +45,9 @@
             compiler.reporter, actualMap, localsMap.getLocalsMap(member))
         .run(definition.node);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
 class TargetData {
@@ -70,15 +73,15 @@
 }
 
 /// Kernel IR visitor for computing jump data.
-class JumpsIrChecker extends IrDataExtractor {
+class JumpsIrChecker extends IrDataExtractor<String> {
   final KernelToLocalsMap _localsMap;
 
   int index = 0;
   Map<JumpTarget, TargetData> targets = <JumpTarget, TargetData>{};
   List<GotoData> gotos = <GotoData>[];
 
-  JumpsIrChecker(DiagnosticReporter reporter, Map<Id, ActualData> actualMap,
-      this._localsMap)
+  JumpsIrChecker(DiagnosticReporter reporter,
+      Map<Id, ActualData<String>> actualMap, this._localsMap)
       : super(reporter, actualMap);
 
   void processData() {
diff --git a/tests/compiler/dart2js/optimization/data/field_get.dart b/tests/compiler/dart2js/optimization/data/field_get.dart
new file mode 100644
index 0000000..e200302
--- /dev/null
+++ b/tests/compiler/dart2js/optimization/data/field_get.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2019, 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.
+
+main() {
+  method1(new Class1a());
+  method2(new Class2a());
+  method2(new Class2b());
+  method3(new Class3a());
+  method3(new Class3b());
+  method4(new Class4a());
+  method4(new Class4b());
+}
+
+class Class1a {
+  int field1;
+}
+
+/*element: method1:FieldGet=[name=Class1a.field1]*/
+@pragma('dart2js:noInline')
+method1(Class1a c) {
+  return c.field1;
+}
+
+class Class2a {
+  int field2;
+}
+
+class Class2b extends Class2a {}
+
+/*element: method2:FieldGet=[name=Class2a.field2]*/
+@pragma('dart2js:noInline')
+method2(Class2a c) {
+  return c.field2;
+}
+
+class Class3a {
+  int field3;
+}
+
+class Class3b implements Class3a {
+  int get field3 => 42;
+  set field3(int _) {}
+}
+
+@pragma('dart2js:noInline')
+method3(Class3a c) {
+  return c.field3;
+}
+
+class Class4a {
+  int field4;
+}
+
+class Class4b implements Class4a {
+  int field4;
+}
+
+// TODO(johnniwinther,sra): Maybe we should optimize cases like this to a direct
+// property access, because all targets are simple fields?
+@pragma('dart2js:noInline')
+method4(Class4a c) {
+  return c.field4;
+}
diff --git a/tests/compiler/dart2js/optimization/data/field_set.dart b/tests/compiler/dart2js/optimization/data/field_set.dart
new file mode 100644
index 0000000..953fe07
--- /dev/null
+++ b/tests/compiler/dart2js/optimization/data/field_set.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2019, 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.
+
+main() {
+  method1(new Class1a());
+  method2(new Class2a());
+  method2(new Class2b());
+  method3(new Class3a());
+  method3(new Class3b());
+  method4(new Class4a());
+  method4(new Class4b());
+}
+
+class Class1a {
+  int field1;
+}
+
+/*element: method1:FieldSet=[name=Class1a.field1]*/
+@pragma('dart2js:noInline')
+method1(Class1a c) {
+  c.field1 = 42;
+}
+
+class Class2a {
+  int field2 = 42;
+}
+
+class Class2b extends Class2a {}
+
+/*element: method2:FieldSet=[name=Class2a.field2]*/
+@pragma('dart2js:noInline')
+method2(Class2a c) {
+  c.field2 = 42;
+}
+
+class Class3a {
+  int field3;
+}
+
+class Class3b implements Class3a {
+  int get field3 => 42;
+  set field3(int _) {}
+}
+
+@pragma('dart2js:noInline')
+method3(Class3a c) {
+  c.field3 = 42;
+}
+
+class Class4a {
+  int field4;
+}
+
+class Class4b implements Class4a {
+  int field4;
+}
+
+// TODO(johnniwinther,sra): Maybe we should optimize cases like this to a direct
+// property write, because all targets are simple fields?
+@pragma('dart2js:noInline')
+method4(Class4a c) {
+  c.field4 = 42;
+}
diff --git a/tests/compiler/dart2js/optimization/data/finalized_type_variable.dart b/tests/compiler/dart2js/optimization/data/finalized_type_variable.dart
new file mode 100644
index 0000000..a5394b1
--- /dev/null
+++ b/tests/compiler/dart2js/optimization/data/finalized_type_variable.dart
@@ -0,0 +1,64 @@
+// Copyright (c) 2019, 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.
+
+abstract class AppView<T> {
+  T ctx;
+}
+
+class CardComponent {
+  String title;
+}
+
+class ViewCardComponent extends AppView<CardComponent> {
+  /*element: ViewCardComponent.method1:
+   FieldGet=[name=AppView.ctx],
+   FieldSet=[name=CardComponent.title]
+  */
+  @pragma('dart2js:noInline')
+  method1(String value) {
+    ctx.title = value;
+  }
+
+  /*element: ViewCardComponent.method2:
+   FieldGet=[name=AppView.ctx,name=CardComponent.title]
+  */
+  @pragma('dart2js:noInline')
+  method2() {
+    return ctx.title;
+  }
+}
+
+class CardComponent2 {
+  String title;
+}
+
+class ViewCardComponent2 extends AppView<CardComponent2> {
+  /*element: ViewCardComponent2.method1:
+   FieldGet=[name=AppView.ctx],
+   FieldSet=[name=CardComponent2.title]
+  */
+  @pragma('dart2js:noInline')
+  method1(String value) {
+    ctx.title = value;
+  }
+
+  /*element: ViewCardComponent2.method2:
+   FieldGet=[name=AppView.ctx,name=CardComponent2.title]
+  */
+  @pragma('dart2js:noInline')
+  method2() {
+    return ctx.title;
+  }
+}
+
+main() {
+  var c1 = new ViewCardComponent();
+  c1.ctx = new CardComponent();
+  c1.method1('foo');
+  c1.method2();
+  var c2 = new ViewCardComponent2();
+  c2.ctx = new CardComponent2();
+  c2.method1('bar');
+  c2.method2();
+}
diff --git a/tests/compiler/dart2js/optimization/optimization_test.dart b/tests/compiler/dart2js/optimization/optimization_test.dart
new file mode 100644
index 0000000..0f2d013
--- /dev/null
+++ b/tests/compiler/dart2js/optimization/optimization_test.dart
@@ -0,0 +1,163 @@
+// Copyright (c) 2019, 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 'dart:io';
+import 'package:async_helper/async_helper.dart';
+import 'package:compiler/src/closure.dart';
+import 'package:compiler/src/common.dart';
+import 'package:compiler/src/compiler.dart';
+import 'package:compiler/src/diagnostics/diagnostic_listener.dart';
+import 'package:compiler/src/elements/entities.dart';
+import 'package:compiler/src/js_backend/backend.dart';
+import 'package:compiler/src/js_model/element_map.dart';
+import 'package:compiler/src/js_model/js_world.dart';
+import 'package:compiler/src/ssa/logging.dart';
+import 'package:compiler/src/ssa/ssa.dart';
+import 'package:compiler/src/util/features.dart';
+import 'package:kernel/ast.dart' as ir;
+import '../equivalence/id_equivalence.dart';
+import '../equivalence/id_equivalence_helper.dart';
+
+main(List<String> args) {
+  asyncTest(() async {
+    Directory dataDir = new Directory.fromUri(Platform.script.resolve('data'));
+    await checkTests(dataDir, const OptimizationDataComputer(), args: args);
+  });
+}
+
+class OptimizationDataValidator implements DataInterpreter<OptimizationLog> {
+  const OptimizationDataValidator();
+
+  @override
+  String getText(OptimizationLog actualData) {
+    Features features = new Features();
+    for (OptimizationLogEntry entry in actualData.entries) {
+      features.addElement(entry.tag, entry.features.getText());
+    }
+    return features.getText();
+  }
+
+  @override
+  bool isEmpty(OptimizationLog actualData) {
+    return actualData == null || actualData.entries.isEmpty;
+  }
+
+  @override
+  String isAsExpected(OptimizationLog actualLog, String expectedLog) {
+    expectedLog ??= '';
+    if (expectedLog == '') {
+      return actualLog.entries.isEmpty
+          ? null
+          : "Expected empty optimization log.";
+    }
+    if (expectedLog == '*') {
+      return null;
+    }
+    List<OptimizationLogEntry> actualDataEntries = actualLog.entries.toList();
+    Features expectedLogEntries = Features.fromText(expectedLog);
+    List<String> errorsFound = <String>[];
+    expectedLogEntries.forEach((String tag, dynamic expectedEntryData) {
+      List<OptimizationLogEntry> actualDataForTag =
+          actualDataEntries.where((data) => data.tag == tag).toList();
+      if (expectedEntryData == '' ||
+          expectedEntryData is List && expectedEntryData.isEmpty) {
+        if (actualDataForTag.isNotEmpty) {
+          errorsFound.add('Non-empty log found for tag $tag');
+        }
+      } else if (expectedEntryData == '*') {
+        // Anything allowed.
+      } else if (expectedEntryData is List) {
+        for (Object object in expectedEntryData) {
+          Features expectedLogEntry = Features.fromText('$object');
+          bool matchFound = false;
+          for (OptimizationLogEntry actualLogEntry in actualDataForTag) {
+            bool validData = true;
+            expectedLogEntry.forEach((String key, Object expectedValue) {
+              Object actualValue = actualLogEntry.features[key];
+              if ('$actualValue' != '$expectedValue') {
+                validData = false;
+              }
+            });
+            if (validData) {
+              actualDataForTag.remove(actualLogEntry);
+              matchFound = true;
+              break;
+            }
+          }
+          if (!matchFound) {
+            errorsFound.add("No match found for $tag=[$object]");
+          }
+        }
+      } else {
+        errorsFound.add("Unknown expected entry '$expectedEntryData'");
+      }
+    });
+    return errorsFound.isNotEmpty ? errorsFound.join(', ') : null;
+  }
+}
+
+class OptimizationDataComputer extends DataComputer<OptimizationLog> {
+  const OptimizationDataComputer();
+
+  /// Compute type inference data for [member] from kernel based inference.
+  ///
+  /// Fills [actualMap] with the data.
+  @override
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<OptimizationLog>> actualMap,
+      {bool verbose: false}) {
+    JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
+    JsToElementMap elementMap = closedWorld.elementMap;
+    MemberDefinition definition = elementMap.getMemberDefinition(member);
+    new OptimizationIrComputer(compiler.reporter, actualMap, elementMap, member,
+            compiler.backend, closedWorld.closureDataLookup)
+        .run(definition.node);
+  }
+
+  @override
+  DataInterpreter<OptimizationLog> get dataValidator =>
+      const OptimizationDataValidator();
+}
+
+/// AST visitor for computing inference data for a member.
+class OptimizationIrComputer extends IrDataExtractor<OptimizationLog> {
+  final JavaScriptBackend backend;
+  final JsToElementMap _elementMap;
+  final ClosureData _closureDataLookup;
+
+  OptimizationIrComputer(
+      DiagnosticReporter reporter,
+      Map<Id, ActualData<OptimizationLog>> actualMap,
+      this._elementMap,
+      MemberEntity member,
+      this.backend,
+      this._closureDataLookup)
+      : super(reporter, actualMap);
+
+  OptimizationLog getLog(MemberEntity member) {
+    SsaFunctionCompiler functionCompiler = backend.functionCompiler;
+    return functionCompiler.optimizer.loggersForTesting[member];
+  }
+
+  OptimizationLog getMemberValue(MemberEntity member) {
+    if (member is FunctionEntity) {
+      return getLog(member);
+    }
+    return null;
+  }
+
+  @override
+  OptimizationLog computeMemberValue(Id id, ir.Member node) {
+    return getMemberValue(_elementMap.getMember(node));
+  }
+
+  @override
+  OptimizationLog computeNodeValue(Id id, ir.TreeNode node) {
+    if (node is ir.FunctionExpression || node is ir.FunctionDeclaration) {
+      ClosureRepresentationInfo info = _closureDataLookup.getClosureInfo(node);
+      return getMemberValue(info.callMethod);
+    }
+    return null;
+  }
+}
diff --git a/tests/compiler/dart2js/rti/rti_emission_test.dart b/tests/compiler/dart2js/rti/rti_emission_test.dart
index ba12ff1..fbabb7a 100644
--- a/tests/compiler/dart2js/rti/rti_emission_test.dart
+++ b/tests/compiler/dart2js/rti/rti_emission_test.dart
@@ -14,6 +14,7 @@
 import 'package:compiler/src/js_emitter/model.dart';
 import 'package:compiler/src/js_model/element_map.dart';
 import 'package:compiler/src/js_model/js_world.dart';
+import 'package:compiler/src/util/features.dart';
 import 'package:kernel/ast.dart' as ir;
 import '../equivalence/id_equivalence.dart';
 import '../equivalence/id_equivalence_helper.dart';
@@ -91,15 +92,15 @@
   }
 }
 
-class RtiEmissionDataComputer extends DataComputer {
+class RtiEmissionDataComputer extends DataComputer<String> {
   const RtiEmissionDataComputer();
 
   @override
   bool get computesClassData => true;
 
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
@@ -111,19 +112,23 @@
 
   @override
   void computeClassData(
-      Compiler compiler, ClassEntity cls, Map<Id, ActualData> actualMap,
+      Compiler compiler, ClassEntity cls, Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
     new RtiClassEmissionIrComputer(compiler, elementMap, actualMap)
         .computeClassValue(cls);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
-class RtiClassEmissionIrComputer extends DataRegistry with ComputeValueMixin {
+class RtiClassEmissionIrComputer extends DataRegistry<String>
+    with ComputeValueMixin {
   final Compiler compiler;
   final JsToElementMap _elementMap;
-  final Map<Id, ActualData> actualMap;
+  final Map<Id, ActualData<String>> actualMap;
 
   RtiClassEmissionIrComputer(this.compiler, this._elementMap, this.actualMap);
 
@@ -137,7 +142,7 @@
   }
 }
 
-class RtiMemberEmissionIrComputer extends IrDataExtractor
+class RtiMemberEmissionIrComputer extends IrDataExtractor<String>
     with ComputeValueMixin {
   final JsToElementMap _elementMap;
   final ClosureData _closureDataLookup;
@@ -145,7 +150,7 @@
 
   RtiMemberEmissionIrComputer(
       DiagnosticReporter reporter,
-      Map<Id, ActualData> actualMap,
+      Map<Id, ActualData<String>> actualMap,
       this._elementMap,
       MemberEntity member,
       this.compiler,
diff --git a/tests/compiler/dart2js/rti/rti_need_test_helper.dart b/tests/compiler/dart2js/rti/rti_need_test_helper.dart
index 29bbaa1..1d201a1 100644
--- a/tests/compiler/dart2js/rti/rti_need_test_helper.dart
+++ b/tests/compiler/dart2js/rti/rti_need_test_helper.dart
@@ -20,6 +20,7 @@
 import 'package:compiler/src/universe/feature.dart';
 import 'package:compiler/src/universe/resolution_world_builder.dart';
 import 'package:compiler/src/universe/selector.dart';
+import 'package:compiler/src/util/features.dart';
 import 'package:kernel/ast.dart' as ir;
 import '../equivalence/check_helpers.dart';
 import '../equivalence/id_equivalence.dart';
@@ -230,7 +231,7 @@
   }
 }
 
-class RtiNeedDataComputer extends DataComputer {
+class RtiNeedDataComputer extends DataComputer<String> {
   const RtiNeedDataComputer();
 
   @override
@@ -240,8 +241,8 @@
   ///
   /// Fills [actualMap] with the data.
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
@@ -256,13 +257,16 @@
   /// Fills [actualMap] with the data.
   @override
   void computeClassData(
-      Compiler compiler, ClassEntity cls, Map<Id, ActualData> actualMap,
+      Compiler compiler, ClassEntity cls, Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     JsClosedWorld closedWorld = compiler.backendClosedWorldForTesting;
     JsToElementMap elementMap = closedWorld.elementMap;
     new RtiClassNeedIrComputer(compiler, elementMap, actualMap)
         .computeClassValue(cls);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
 abstract class IrMixin implements ComputeValueMixin {
@@ -313,11 +317,11 @@
   }
 }
 
-class RtiClassNeedIrComputer extends DataRegistry
+class RtiClassNeedIrComputer extends DataRegistry<String>
     with ComputeValueMixin, IrMixin {
   final Compiler compiler;
   final JsToElementMap _elementMap;
-  final Map<Id, ActualData> actualMap;
+  final Map<Id, ActualData<String>> actualMap;
 
   RtiClassNeedIrComputer(this.compiler, this._elementMap, this.actualMap);
 
@@ -332,7 +336,7 @@
 }
 
 /// AST visitor for computing inference data for a member.
-class RtiMemberNeedIrComputer extends IrDataExtractor
+class RtiMemberNeedIrComputer extends IrDataExtractor<String>
     with ComputeValueMixin, IrMixin {
   final JsToElementMap _elementMap;
   final ClosureData _closureDataLookup;
@@ -340,7 +344,7 @@
 
   RtiMemberNeedIrComputer(
       DiagnosticReporter reporter,
-      Map<Id, ActualData> actualMap,
+      Map<Id, ActualData<String>> actualMap,
       this._elementMap,
       MemberEntity member,
       this.compiler,
diff --git a/tests/compiler/dart2js/static_type/static_type_test.dart b/tests/compiler/dart2js/static_type/static_type_test.dart
index bdc61dd..9ab4994 100644
--- a/tests/compiler/dart2js/static_type/static_type_test.dart
+++ b/tests/compiler/dart2js/static_type/static_type_test.dart
@@ -34,7 +34,7 @@
   static const String runtimeTypeUse = 'runtimeType';
 }
 
-class StaticTypeDataComputer extends DataComputer {
+class StaticTypeDataComputer extends DataComputer<String> {
   ir.TypeEnvironment _typeEnvironment;
 
   ir.TypeEnvironment getTypeEnvironment(KernelToElementMapImpl elementMap) {
@@ -50,8 +50,8 @@
   ///
   /// Fills [actualMap] with the data.
   @override
-  void computeMemberData(
-      Compiler compiler, MemberEntity member, Map<Id, ActualData> actualMap,
+  void computeMemberData(Compiler compiler, MemberEntity member,
+      Map<Id, ActualData<String>> actualMap,
       {bool verbose: false}) {
     KernelFrontEndStrategy frontendStrategy = compiler.frontendStrategy;
     KernelToElementMapImpl elementMap = frontendStrategy.elementMap;
@@ -65,6 +65,9 @@
                 getTypeEnvironment(elementMap), staticTypeCache))
         .run(node);
   }
+
+  @override
+  DataInterpreter<String> get dataValidator => const StringDataInterpreter();
 }
 
 class TypeTextVisitor implements ir.DartTypeVisitor1<void, StringBuffer> {
@@ -179,11 +182,11 @@
 }
 
 /// IR visitor for computing inference data for a member.
-class StaticTypeIrComputer extends IrDataExtractor {
+class StaticTypeIrComputer extends IrDataExtractor<String> {
   final CachedStaticType staticTypeCache;
 
   StaticTypeIrComputer(DiagnosticReporter reporter,
-      Map<Id, ActualData> actualMap, this.staticTypeCache)
+      Map<Id, ActualData<String>> actualMap, this.staticTypeCache)
       : super(reporter, actualMap);
 
   String getStaticTypeValue(ir.DartType type) {