blob: beea310c117374ed6a2b36b0b58d19b620826e80 [file] [log] [blame]
// Copyright (c) 2021, 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 'ast_model.dart';
import 'visitor_generator.dart';
Uri computeEquivalenceUri(Uri repoDir) {
return repoDir.resolve('pkg/kernel/lib/src/equivalence.dart');
}
Future<void> main(List<String> args) async {
Uri output = args.isEmpty
? computeEquivalenceUri(Uri.base)
: new File(args[0]).absolute.uri;
String result = await generateAstEquivalence(Uri.base);
new File.fromUri(output).writeAsStringSync(result);
}
Future<String> generateAstEquivalence(Uri repoDir, [AstModel? astModel]) async {
astModel ??= await deriveAstModel(repoDir);
return generateVisitor(astModel, new EquivalenceVisitorStrategy());
}
class EquivalenceVisitorStrategy extends Visitor1Strategy {
Map<AstClass, String> _classStrategyMembers = {};
Map<AstField, String> _fieldStrategyMembers = {};
EquivalenceVisitorStrategy();
@override
String get generatorCommand =>
'dart pkg/front_end/tool/generate_ast_equivalence.dart';
@override
String get argumentType => 'Node';
@override
String get argumentName => 'other';
@override
String get returnType => 'bool';
@override
String get visitorName => 'EquivalenceVisitor';
String get strategyName => 'EquivalenceStrategy';
String get internalCheckValues => '_checkValues';
String get checkValues => 'checkValues';
String get matchValues => 'matchValues';
String get internalCheckNodes => '_checkNodes';
String get checkNodes => 'checkNodes';
String get shallowMatchNodes => 'shallowMatchNodes';
String get deepMatchNodes => 'deepMatchNodes';
String get internalCheckReferences => '_checkReferences';
String get checkReferences => 'checkReferences';
String get matchReferences => 'matchReferences';
String get deepMatchReferences => 'deeplyMatchReferences';
String get matchNamedNodes => 'matchNamedNodes';
String get assumeReferences => 'assumeReferences';
String get checkAssumedReferences => 'checkAssumedReferences';
String get checkDeclarations => 'checkDeclarations';
String get internalCheckDeclarations => '_checkDeclarations';
String get shallowMatchDeclarations => 'matchDeclarations';
String get deepMatchDeclarations => 'deepMatchDeclarations';
String get assumeDeclarations => 'assumeDeclarations';
String get checkAssumedDeclarations => 'checkAssumedDeclarations';
String get checkLists => 'checkLists';
String get matchLists => 'matchLists';
String get checkSets => 'checkSets';
String get matchSets => 'matchSets';
String get checkMaps => 'checkMaps';
String get matchMaps => 'matchMaps';
String get checkingState => '_checkingState';
String get resultOnInequivalence => 'resultOnInequivalence';
String get registerInequivalence => 'registerInequivalence';
String classCheckName(AstClass astClass) => 'check${astClass.name}';
String fieldCheckName(AstField field) =>
'check${field.astClass.name}_${field.name}';
@override
void handleDefaultVisit(
AstModel astModel, AstClass astClass, StringBuffer sb) {
sb.writeln('''
return false;''');
}
/// Compute the expression code for shallow matching two values of type
/// [fieldType].
///
/// Shallow matching is used to pair value when checking sets and maps. The
/// checking doesn't traverse the AST deeply and inequivalences are not
/// registered.
///
/// [prefix] is used as the receiver of the invocation.
String computeMatchingHelper(FieldType fieldType, [String prefix = '']) {
String thisName = 'a';
String otherName = 'b';
switch (fieldType.kind) {
case AstFieldKind.value:
return '$prefix$matchValues';
case AstFieldKind.node:
return '$prefix$shallowMatchNodes';
case AstFieldKind.reference:
return '$prefix$matchReferences';
case AstFieldKind.use:
return '$prefix$shallowMatchDeclarations';
case AstFieldKind.list:
ListFieldType listFieldType = fieldType as ListFieldType;
String elementEquivalence =
computeMatchingHelper(listFieldType.elementType);
return '($thisName, $otherName) => $prefix$matchLists('
'$thisName, $otherName, $elementEquivalence)';
case AstFieldKind.set:
SetFieldType setFieldType = fieldType as SetFieldType;
String elementMatching =
computeMatchingHelper(setFieldType.elementType);
String elementEquivalence =
computeEquivalenceHelper(setFieldType.elementType);
return '($thisName, $otherName) => $prefix$checkSets('
'$thisName, $otherName, $elementMatching, $elementEquivalence)';
case AstFieldKind.map:
MapFieldType mapFieldType = fieldType as MapFieldType;
String keyMatching = computeMatchingHelper(mapFieldType.keyType);
String keyEquivalence = computeEquivalenceHelper(mapFieldType.keyType);
String valueEquivalence =
computeEquivalenceHelper(mapFieldType.valueType);
return '($thisName, $otherName) => $prefix$checkMaps('
'$thisName, $otherName, $keyMatching, '
'$keyEquivalence, $valueEquivalence)';
case AstFieldKind.utility:
StringBuffer sb = new StringBuffer();
UtilityFieldType utilityFieldType = fieldType as UtilityFieldType;
registerAstClassEquivalence(utilityFieldType.astClass);
sb.writeln('''($thisName, $otherName, _) {
if (identical($thisName, $otherName)) return true;
if ($thisName is! ${utilityFieldType.astClass.name}) return false;
if ($otherName is! ${utilityFieldType.astClass.name}) return false;
return ${classCheckName(utilityFieldType.astClass)}(
visitor,
$thisName,
$otherName);
}''');
return sb.toString();
}
}
/// Computes the expression code for checking the equivalence of two fields
/// of type [fieldType].
///
/// Checking is used to check the AST for equivalence and inequivalences are
/// registered.
///
/// [prefix] is used as the receiver of the invocation.
String computeEquivalenceHelper(FieldType fieldType, [String prefix = '']) {
String thisName = 'a';
String otherName = 'b';
switch (fieldType.kind) {
case AstFieldKind.value:
return '$prefix$checkValues';
case AstFieldKind.node:
return '$prefix$checkNodes';
case AstFieldKind.reference:
return '$prefix$checkReferences';
case AstFieldKind.use:
return '$prefix$checkDeclarations';
case AstFieldKind.list:
ListFieldType listFieldType = fieldType as ListFieldType;
String elementEquivalence =
computeEquivalenceHelper(listFieldType.elementType);
return '($thisName, $otherName) => $prefix$checkLists('
'$thisName, $otherName, $elementEquivalence)';
case AstFieldKind.set:
SetFieldType setFieldType = fieldType as SetFieldType;
String elementMatching =
computeMatchingHelper(setFieldType.elementType);
String elementEquivalence =
computeEquivalenceHelper(setFieldType.elementType);
return '($thisName, $otherName) => $prefix$checkSets('
'$thisName, $otherName, $elementMatching, $elementEquivalence)';
case AstFieldKind.map:
MapFieldType mapFieldType = fieldType as MapFieldType;
String keyMatching = computeMatchingHelper(mapFieldType.keyType);
String keyEquivalence = computeEquivalenceHelper(mapFieldType.keyType);
String valueEquivalence =
computeEquivalenceHelper(mapFieldType.valueType);
return '($thisName, $otherName) => $prefix$checkMaps('
'$thisName, $otherName, $keyMatching, '
'$keyEquivalence, $valueEquivalence)';
case AstFieldKind.utility:
StringBuffer sb = new StringBuffer();
UtilityFieldType utilityFieldType = fieldType as UtilityFieldType;
registerAstClassEquivalence(utilityFieldType.astClass);
sb.writeln('''($thisName, $otherName, _) {
if (identical($thisName, $otherName)) return true;
if ($thisName is! ${utilityFieldType.astClass.name}) return false;
if ($otherName is! ${utilityFieldType.astClass.name}) return false;
return ${classCheckName(utilityFieldType.astClass)}(
visitor,
$thisName,
$otherName);
}''');
return sb.toString();
}
}
/// Registers that a strategy method is needed for checking [astClass].
///
/// If the method has not already been generated, it is generated and stored
/// in [_classStrategyMembers].
void registerAstClassEquivalence(AstClass astClass) {
if (_classStrategyMembers.containsKey(astClass)) return;
String thisName = 'node';
String otherName = 'other';
StringBuffer classStrategy = new StringBuffer();
classStrategy.writeln('''
bool ${classCheckName(astClass)}(
$visitorName visitor,
${astClass.name}? $thisName,
Object? $otherName) {''');
classStrategy.writeln('''
if (identical($thisName, $otherName)) return true;
if ($thisName is! ${astClass.name}) return false;
if ($otherName is! ${astClass.name}) return false;''');
if (astClass.kind == AstClassKind.named) {
classStrategy.writeln('''
if (!visitor.$matchNamedNodes($thisName, $otherName)) {
return false;
}''');
} else if (astClass.kind == AstClassKind.declarative) {
classStrategy.writeln('''
if (!visitor.$checkDeclarations($thisName, $otherName, '')) {
return false;
}''');
}
if (astClass.kind != AstClassKind.utilityAsStructure) {
classStrategy.writeln('''
visitor.pushNodeState($thisName, $otherName);''');
}
classStrategy.writeln('''
bool result = true;''');
for (AstField field in astClass.fields.values) {
registerAstFieldEquivalence(field);
classStrategy.writeln('''
if (!${fieldCheckName(field)}(visitor, $thisName, $otherName)) {
result = visitor.$resultOnInequivalence;
}''');
}
if (astClass.kind != AstClassKind.utilityAsStructure) {
classStrategy.writeln('''
visitor.popState();''');
}
classStrategy.writeln('''
return result;
}''');
_classStrategyMembers[astClass] = classStrategy.toString();
}
/// Registers that a strategy method is needed for checking [field] in
/// [astClass].
///
/// If the method has not already been generated, it is generated and stored
/// in [_fieldStrategyMembers].
void registerAstFieldEquivalence(AstField field) {
if (_fieldStrategyMembers.containsKey(field)) return;
AstClass astClass = field.astClass;
String thisName = 'node';
String otherName = 'other';
StringBuffer fieldStrategy = new StringBuffer();
fieldStrategy.writeln('''
bool ${fieldCheckName(field)}(
$visitorName visitor,
${astClass.name} $thisName,
${astClass.name} $otherName) {''');
if (field.parentField != null) {
registerAstFieldEquivalence(field.parentField!);
fieldStrategy.writeln('''
return ${fieldCheckName(field.parentField!)}(
visitor, $thisName, $otherName);''');
} else {
switch (field.type.kind) {
case AstFieldKind.value:
fieldStrategy.writeln('''
return visitor.$checkValues(
$thisName.${field.name},
$otherName.${field.name},
'${field.name}');''');
break;
case AstFieldKind.node:
fieldStrategy.writeln('''
return visitor.$checkNodes(
$thisName.${field.name},
$otherName.${field.name},
'${field.name}');''');
break;
case AstFieldKind.reference:
fieldStrategy.writeln('''
return visitor.$checkReferences(
$thisName.${field.name},
$otherName.${field.name},
'${field.name}');''');
break;
case AstFieldKind.use:
fieldStrategy.writeln('''
return visitor.$checkDeclarations(
$thisName.${field.name},
$otherName.${field.name},
'${field.name}');''');
break;
case AstFieldKind.list:
ListFieldType listFieldType = field.type as ListFieldType;
fieldStrategy.writeln('''
return visitor.$checkLists(
$thisName.${field.name},
$otherName.${field.name},
${computeEquivalenceHelper(listFieldType.elementType, 'visitor.')},
'${field.name}');''');
break;
case AstFieldKind.set:
SetFieldType setFieldType = field.type as SetFieldType;
fieldStrategy.writeln('''
return visitor.$checkSets(
$thisName.${field.name},
$otherName.${field.name},
${computeMatchingHelper(setFieldType.elementType, 'visitor.')},
${computeEquivalenceHelper(setFieldType.elementType, 'visitor.')},
'${field.name}');''');
break;
case AstFieldKind.map:
MapFieldType mapFieldType = field.type as MapFieldType;
fieldStrategy.writeln('''
return visitor.$checkMaps(
$thisName.${field.name},
$otherName.${field.name},
${computeMatchingHelper(mapFieldType.keyType, 'visitor.')},
${computeEquivalenceHelper(mapFieldType.keyType, 'visitor.')},
${computeEquivalenceHelper(mapFieldType.valueType, 'visitor.')},
'${field.name}');''');
break;
case AstFieldKind.utility:
UtilityFieldType utilityFieldType = field.type as UtilityFieldType;
registerAstClassEquivalence(utilityFieldType.astClass);
fieldStrategy.writeln('''
'${field.name}';
return ${classCheckName(utilityFieldType.astClass)}(
visitor,
$thisName.${field.name},
$otherName.${field.name});''');
break;
}
}
fieldStrategy.writeln('''
}''');
_fieldStrategyMembers[field] = fieldStrategy.toString();
}
@override
void handleVisit(AstModel astModel, AstClass astClass, StringBuffer sb) {
registerAstClassEquivalence(astClass);
sb.writeln('''
return strategy.${classCheckName(astClass)}(
this, node, $argumentName);''');
}
@override
void handleDefaultVisitReference(
AstModel astModel, AstClass astClass, StringBuffer sb) {
sb.writeln('''
return false;''');
}
@override
void handleVisitReference(
AstModel astModel, AstClass astClass, StringBuffer sb) {
sb.writeln('''
return false;''');
}
@override
void generateHeader(AstModel astModel, StringBuffer sb) {
sb.writeln('''
$preamble
import 'package:kernel/ast.dart';
import 'package:kernel/src/printer.dart';
import 'union_find.dart';
part 'equivalence_helpers.dart';
/// Visitor that uses a $strategyName to compute AST node equivalence.
///
/// The visitor hold a current state that collects found inequivalences and
/// current assumptions. The current state has two modes. In the asserting mode,
/// the default, inequivalences are registered when found. In the non-asserting
/// mode, inequivalences are _not_ registered. The latter is used to compute
/// equivalences in sandboxed state, for instance to determine which elements
/// to pair when checking equivalence of two sets.
class $visitorName$visitorTypeParameters
implements Visitor1<$returnType, $argumentType> {
final $strategyName strategy;
$visitorName({
this.strategy: const $strategyName()});
''');
}
@override
void generateFooter(AstModel astModel, StringBuffer sb) {
sb.writeln('''
/// Returns `true` if [a] and [b] are identical or equal.
bool $internalCheckValues<T>(T? a, T? b) {
return identical(a, b) || a == b;
}
/// Returns `true` if [a] and [b] are identical or equal and registers the
/// inequivalence otherwise.
bool $checkValues<T>(T? a, T? b, String propertyName) {
bool result = $internalCheckValues(a, b);
if (!result) {
registerInequivalence(
propertyName, 'Values \${a} and \${b} are not equivalent');
}
return result;
}
/// Returns `true` if [a] and [b] are identical or equal. Inequivalence is
/// _not_ registered.
bool $matchValues<T>(T? a, T? b) {
return $internalCheckValues(a, b);
}
/// Returns `true` if [a] and [b] are equivalent.
bool $internalCheckNodes<T extends Node>(T? a, T? b) {
if (identical(a, b)) return true;
if (a == null || b == null) {
return false;
} else {
return a.accept1(this, b);
}
}
/// Returns `true` if [a] and [b] are equivalent, as defined by the current
/// strategy, and registers the inequivalence otherwise.
bool $checkNodes<T extends Node>(T? a, T? b,
[String propertyName = '']) {
$checkingState.pushPropertyState(propertyName);
bool result = $internalCheckNodes(a, b);
$checkingState.popState();
if (!result) {
$registerInequivalence(
propertyName, 'Inequivalent nodes\\n1: \${a}\\n2: \${b}');
}
return result;
}
/// Returns `true` if [a] and [b] are identical or equal. Inequivalence is
/// _not_ registered.
bool $shallowMatchNodes<T extends Node>(T? a, T? b) {
return $internalCheckValues(a, b);
}
/// Returns `true` if [a] and [b] are equivalent, as defined by the current
/// strategy. Inequivalence is _not_ registered.
bool $deepMatchNodes<T extends Node>(T? a, T? b) {
CheckingState oldState = $checkingState;
$checkingState = $checkingState.toMatchingState();
bool result = $checkNodes(a, b);
$checkingState = oldState;
return result;
}
/// Returns `true` if [a] and [b] are equivalent, either by existing
/// assumption or as defined by their corresponding canonical names.
/// Inequivalence is _not_ registered.
bool $matchNamedNodes(NamedNode? a, NamedNode? b) {
return identical(a, b) ||
a == null ||
b == null ||
checkAssumedReferences(a.reference, b.reference) ||
new ReferenceName.fromNamedNode(a) ==
new ReferenceName.fromNamedNode(b);
}
/// Returns `true` if [a] and [b] are currently assumed to be equivalent.
bool $checkAssumedReferences(Reference? a, Reference? b) {
return $checkingState.$checkAssumedReferences(a, b);
}
/// Assume that [a] and [b] are equivalent, if possible.
///
/// Returns `true` if [a] and [b] could be assumed to be equivalent. This
/// would not be the case if [a] xor [b] is `null`.
bool $assumeReferences(Reference? a, Reference? b) {
return $checkingState.$assumeReferences(a, b);
}
/// Returns `true` if [a] and [b] are equivalent, either by existing
/// assumption or as defined by their corresponding canonical names.
/// Inequivalence is _not_ registered.
bool $matchReferences(Reference? a, Reference? b) {
return identical(a, b) ||
checkAssumedReferences(a, b) ||
ReferenceName.fromReference(a) ==
ReferenceName.fromReference(b);
}
/// Returns `true` if [a] and [b] are equivalent, either by their
/// corresponding canonical names or by assumption. Inequivalence is _not_
/// registered.
bool $internalCheckReferences(Reference? a, Reference? b) {
if (identical(a, b)) {
return true;
} else if (a == null || b == null) {
return false;
} else if ($matchReferences(a, b)) {
return true;
} else if ($checkAssumedReferences(a, b)) {
return true;
} else {
return false;
}
}
/// Returns `true` if [a] and [b] are equivalent, either by their
/// corresponding canonical names or by assumption. Inequivalence is _not_
/// registered.
bool $deepMatchReferences(Reference? a, Reference? b) {
CheckingState oldState = $checkingState;
$checkingState = $checkingState.toMatchingState();
bool result = $checkReferences(a, b);
$checkingState = oldState;
return result;
}
/// Returns `true` if [a] and [b] are equivalent, either by their
/// corresponding canonical names or by assumption, and registers the
/// inequivalence otherwise.
bool $checkReferences(
Reference? a,
Reference? b,
[String propertyName = '']) {
bool result = $internalCheckReferences(a, b);
if (!result) {
$registerInequivalence(
propertyName, 'Inequivalent references:\\n1: \${a}\\n2: \${b}');
}
return result;
}
/// Returns `true` if declarations [a] and [b] are currently assumed to be
/// equivalent.
bool $checkAssumedDeclarations(dynamic a, dynamic b) {
return $checkingState.$checkAssumedDeclarations(a, b);
}
/// Assume that [a] and [b] are equivalent, if possible.
///
/// Returns `true` if [a] and [b] could be assumed to be equivalent. This
/// would not be the case if [a] is already assumed to be equivalent to
/// another declaration.
bool $assumeDeclarations(dynamic a, dynamic b) {
return $checkingState.$assumeDeclarations(a, b);
}
bool $shallowMatchDeclarations(dynamic a, dynamic b) {''');
for (AstClass cls in astModel.declarativeClasses) {
if (cls.declarativeName != null) {
sb.write('''
if (a is ${cls.name}) {
return b is ${cls.name} &&
a.${cls.declarativeName} == b.${cls.declarativeName};
}
''');
} else {
sb.write('''
if (a is ${cls.name}) {
return b is ${cls.name};
}
''');
}
}
try {
try {
try {
sb.writeln('''
return false;
}
bool $internalCheckDeclarations(dynamic a, dynamic b) {
if (identical(a, b)) {
return true;
} else if (a == null || b == null) {
return false;
} else if ($checkAssumedDeclarations(a, b)) {
return true;
} else if ($shallowMatchDeclarations(a, b)) {
return $assumeDeclarations(a, b);
} else {
return false;
}
}
bool $deepMatchDeclarations(dynamic a, dynamic b) {
CheckingState oldState = $checkingState;
$checkingState = $checkingState.toMatchingState();
bool result = $checkDeclarations(a, b);
$checkingState = oldState;
return result;
}
bool $checkDeclarations(dynamic a, dynamic b,
[String propertyName = '']) {
bool result = $internalCheckDeclarations(a, b);
if (!result) {
result = $assumeDeclarations(a, b);
}
if (!result) {
$registerInequivalence(
propertyName, 'Declarations \${a} and \${b} are not equivalent');
}
return result;
}
/// Returns `true` if lists [a] and [b] are equivalent, using
/// [equivalentValues] to determine element-wise equivalence.
///
/// If run in a checking state, the [propertyName] is used for registering
/// inequivalences.
bool $checkLists<E>(
List<E>? a,
List<E>? b,
bool Function(E?, E?, String) equivalentValues,
[String propertyName = '']) {
if (identical(a, b)) return true;
if (a == null || b == null) return false;
if (a.length != b.length) {
$registerInequivalence(
'\${propertyName}.length', 'Lists \${a} and \${b} are not equivalent');
return false;
}
for (int i = 0; i < a.length; i++) {
if (!equivalentValues(a[i], b[i], '\${propertyName}[\${i}]')) {
return false;
}
}
return true;
}
/// Returns `true` if lists [a] and [b] are equivalent, using
/// [equivalentValues] to determine element-wise equivalence.
///
/// Inequivalence is _not_ registered.
bool $matchLists<E>(
List<E>? a,
List<E>? b,
bool Function(E?, E?, String) equivalentValues) {
CheckingState oldState = $checkingState;
$checkingState = $checkingState.toMatchingState();
bool result = $checkLists(a, b, equivalentValues);
$checkingState = oldState;
return result;
}
/// Returns `true` if sets [a] and [b] are equivalent, using
/// [matchingValues] to determine which elements that should be checked for
/// element-wise equivalence using [equivalentValues].
///
/// If run in a checking state, the [propertyName] is used for registering
/// inequivalences.
bool $checkSets<E>(
Set<E>? a,
Set<E>? b,
bool Function(E?, E?) matchingValues,
bool Function(E?, E?, String) equivalentValues,
[String propertyName = '']) {
if (identical(a, b)) return true;
if (a == null || b == null) return false;
if (a.length != b.length) {
$registerInequivalence(
'\${propertyName}.length', 'Sets \${a} and \${b} are not equivalent');
return false;
}
b = b.toSet();
for (E aValue in a) {
bool hasFoundValue = false;
E? foundValue;
for (E bValue in b) {
if (matchingValues(aValue, bValue)) {
foundValue = bValue;
hasFoundValue = true;
if (!equivalentValues(aValue, bValue,
'\${propertyName}[\${aValue}]')) {
$registerInequivalence(
'\${propertyName}[\${aValue}]',
'Elements \${aValue} and \${bValue} are not equivalent');
return false;
}
break;
}
}
if (hasFoundValue) {
b.remove(foundValue);
} else {
$registerInequivalence(
'\${propertyName}[\${aValue}]',
'Sets \${a} and \${b} are not equivalent, no equivalent value '
'found for \$aValue');
return false;
}
}
return true;
}
/// Returns `true` if sets [a] and [b] are equivalent, using
/// [matchingValues] to determine which elements that should be checked for
/// element-wise equivalence using [equivalentValues].
///
/// Inequivalence is _not_registered.
bool $matchSets<E>(
Set<E>? a,
Set<E>? b,
bool Function(E?, E?) matchingValues,
bool Function(E?, E?, String) equivalentValues) {
CheckingState oldState = $checkingState;
$checkingState = $checkingState.toMatchingState();
bool result = $checkSets(a, b, matchingValues, equivalentValues);
$checkingState = oldState;
return result;
}
/// Returns `true` if maps [a] and [b] are equivalent, using
/// [matchingKeys] to determine which entries that should be checked for
/// entry-wise equivalence using [equivalentKeys] and [equivalentValues] to
/// determine key and value equivalences, respectively.
///
/// If run in a checking state, the [propertyName] is used for registering
/// inequivalences.
bool $checkMaps<K, V>(
Map<K, V>? a,
Map<K, V>? b,
bool Function(K?, K?) matchingKeys,
bool Function(K?, K?, String) equivalentKeys,
bool Function(V?, V?, String) equivalentValues,
[String propertyName = '']) {
if (identical(a, b)) return true;
if (a == null || b == null) return false;
if (a.length != b.length) {
$registerInequivalence(
'\${propertyName}.length',
'Maps \${a} and \${b} are not equivalent');
return false;
}
Set<K> bKeys = b.keys.toSet();
for (K aKey in a.keys) {
bool hasFoundKey = false;
K? foundKey;
for (K bKey in bKeys) {
if (matchingKeys(aKey, bKey)) {
foundKey = bKey;
hasFoundKey = true;
if (!equivalentKeys(aKey, bKey, '\${propertyName}[\${aKey}]')) {
$registerInequivalence(
'\${propertyName}[\${aKey}]',
'Keys \${aKey} and \${bKey} are not equivalent');
return false;
}
break;
}
}
if (hasFoundKey) {
bKeys.remove(foundKey);
if (!equivalentValues(a[aKey], b[foundKey],
'\${propertyName}[\${aKey}]')) {
return false;
}
} else {
$registerInequivalence(
'\${propertyName}[\${aKey}]',
'Maps \${a} and \${b} are not equivalent, no equivalent key '
'found for \$aKey');
return false;
}
}
return true;
}
/// Returns `true` if maps [a] and [b] are equivalent, using
/// [matchingKeys] to determine which entries that should be checked for
/// entry-wise equivalence using [equivalentKeys] and [equivalentValues] to
/// determine key and value equivalences, respectively.
///
/// Inequivalence is _not_ registered.
bool $matchMaps<K, V>(
Map<K, V>? a,
Map<K, V>? b,
bool Function(K?, K?) matchingKeys,
bool Function(K?, K?, String) equivalentKeys,
bool Function(V?, V?, String) equivalentValues) {
CheckingState oldState = $checkingState;
$checkingState = $checkingState.toMatchingState();
bool result = $checkMaps(a, b, matchingKeys, equivalentKeys,
equivalentValues);
$checkingState = oldState;
return result;
}
/// The current state of the visitor.
///
/// This holds the current assumptions, found inequivalences, and whether
/// inequivalences are currently registered.
CheckingState $checkingState = new CheckingState();
/// Runs [f] in a new state that holds all current assumptions. If
/// [isAsserting] is `true`, inequivalences are registered. Returns the
/// collected inequivalences.
///
/// If [f] returns `false`, the returned result is marked as having
/// inequivalences even when non have being registered.
EquivalenceResult inSubState(bool Function() f, {bool isAsserting: false}) {
CheckingState _oldState = $checkingState;
$checkingState = $checkingState.createSubState(isAsserting: isAsserting);
bool hasInequivalences = f();
EquivalenceResult result =
$checkingState.toResult(hasInequivalences: hasInequivalences);
$checkingState = _oldState;
return result;
}
/// Registers that the visitor enters the property named [propertyName] and
/// the currently visited node.
void pushPropertyState(String propertyName) {
$checkingState.pushPropertyState(propertyName);
}
/// Registers that the visitor enters nodes [a] and [b].
void pushNodeState(Node a, Node b) {
$checkingState.pushNodeState(a, b);
}
/// Register that the visitor leave the current node or property.
void popState() {
$checkingState.popState();
}
/// Returns the value used as the result for property inequivalences.
///
/// When inequivalences are currently registered, this is `true`, so that the
/// visitor will continue find inequivalences that are not directly related.
///
/// An example is finding several child inequivalences on otherwise equivalent
/// nodes, like finding inequivalences deeply in the members of the second
/// library of a component even when inequivalences deeply in the members of
/// the first library. Had the return value been `false`, signaling that the
/// first libraries were inequivalent, which they technically are, given that
/// the contain inequivalent subnodes, the visitor would have stopped short in
/// checking the list of libraries, and the inequivalences in the second
/// library would not have been found.
///
/// When inequivalences are _not_ currently registered, i.e. we are only
/// interested in the true/false value of the equivalence test, `false` is
/// used as the result value to stop the equivalence checking short.
bool get $resultOnInequivalence =>
$checkingState.$resultOnInequivalence;
/// Registers an equivalence on the [propertyName] with a detailed description
/// in [message].
void $registerInequivalence(String propertyName, String message) {
$checkingState.registerInequivalence(propertyName, message);
}
/// Returns the inequivalences found by the visitor.
EquivalenceResult toResult() => $checkingState.toResult();
''');
} catch (e, s) {
print(s);
}
} catch (e, s) {
print(s);
}
} catch (e, s) {
print(s);
}
super.generateFooter(astModel, sb);
sb.writeln('''
/// Checks [a] and [b] be for equivalence using [strategy].
///
/// Returns an [EquivalenceResult] containing the found inequivalences.
EquivalenceResult checkEquivalence(
Node a,
Node b,
{$strategyName strategy: const $strategyName()}) {
EquivalenceVisitor visitor = new EquivalenceVisitor(
strategy: strategy);
visitor.$checkNodes(a, b, 'root');
return visitor.toResult();
}
''');
sb.writeln('''
/// Strategy used for determining equivalence of AST nodes.
///
/// The strategy has a method for determining the equivalence of each AST node
/// class, and a method for determining the equivalence of each property on each
/// AST node class.
///
/// The base implementation enforces a full structural equivalence.
///
/// Custom strategies can be made by extending this strategy and override
/// methods where exceptions to the structural equivalence are needed.
class $strategyName {
const $strategyName();
''');
_classStrategyMembers.forEach((key, value) {
sb.write(value);
});
_fieldStrategyMembers.forEach((key, value) {
sb.write(value);
});
sb.writeln(r'''
}
''');
}
}