| // Copyright (c) 2020, 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. |
| |
| library kernel.transformations.value_class; |
| |
| import 'package:kernel/type_environment.dart'; |
| |
| import '../ast.dart'; |
| import '../core_types.dart' show CoreTypes; |
| import '../class_hierarchy.dart' show ClassHierarchy; |
| import '../reference_from_index.dart'; |
| import './scanner.dart'; |
| |
| class ValueClassScanner extends ClassScanner<Null> { |
| ValueClassScanner() : super(null); |
| |
| @override |
| bool predicate(Class node) => isValueClass(node); |
| } |
| |
| class JenkinsClassScanner extends ClassScanner<Procedure> { |
| JenkinsClassScanner(Scanner<Procedure, TreeNode?> next) : super(next); |
| |
| @override |
| bool predicate(Class node) { |
| return node.name == "JenkinsSmiHash"; |
| } |
| } |
| |
| class HashCombineMethodsScanner extends ProcedureScanner<Null> { |
| HashCombineMethodsScanner() : super(null); |
| |
| @override |
| bool predicate(Procedure node) { |
| return node.name.text == "combine" || node.name.text == "finish"; |
| } |
| } |
| |
| class AllMemberScanner extends MemberScanner<InstanceInvocationExpression> { |
| AllMemberScanner(Scanner<InstanceInvocationExpression, TreeNode?> next) |
| : super(next); |
| |
| @override |
| bool predicate(Member member) => true; |
| } |
| |
| // Scans and matches all copyWith invocations were the receiver is _ as dynamic |
| // It will filter out the results that are not value classes afterwards |
| class ValueClassCopyWithScanner extends MethodInvocationScanner<Null> { |
| ValueClassCopyWithScanner() : super(null); |
| |
| // The matching construct followed in unit-tests is: |
| // @valueClass V {} |
| // V v; |
| // (v as dynamic).copyWith() as V |
| @override |
| bool predicate(InstanceInvocationExpression node) { |
| return node.name.text == "copyWith" && |
| _isValueClassAsConstruct(node.receiver); |
| } |
| |
| bool _isValueClassAsConstruct(Expression node) { |
| return node is AsExpression && node.type is DynamicType; |
| } |
| } |
| |
| void transformComponent( |
| Component node, |
| CoreTypes coreTypes, |
| ClassHierarchy hierarchy, |
| ReferenceFromIndex? referenceFromIndex, |
| TypeEnvironment typeEnvironment) { |
| ValueClassScanner scanner = new ValueClassScanner(); |
| ScanResult<Class, Null> valueClasses = scanner.scan(node); |
| for (Class valueClass in valueClasses.targets.keys) { |
| transformValueClass( |
| valueClass, |
| coreTypes, |
| hierarchy, |
| referenceFromIndex |
| ?.lookupLibrary(valueClass.enclosingLibrary) |
| ?.lookupIndexedClass(valueClass.name), |
| typeEnvironment); |
| } |
| |
| treatCopyWithCallSites(node, coreTypes, typeEnvironment, hierarchy); |
| |
| for (Class valueClass in valueClasses.targets.keys) { |
| removeValueClassAnnotation(valueClass); |
| } |
| } |
| |
| void transformValueClass( |
| Class cls, |
| CoreTypes coreTypes, |
| ClassHierarchy hierarchy, |
| IndexedClass? indexedClass, |
| TypeEnvironment typeEnvironment) { |
| Constructor? syntheticConstructor = null; |
| for (Constructor constructor in cls.constructors) { |
| if (constructor.isSynthetic) { |
| syntheticConstructor = constructor; |
| } |
| } |
| |
| List<VariableDeclaration> allVariables = queryAllInstanceVariables(cls); |
| List<VariableDeclaration> allVariablesList = allVariables.toList(); |
| allVariablesList.sort((a, b) => a.name!.compareTo(b.name!)); |
| |
| addConstructor(cls, coreTypes, syntheticConstructor!); |
| addEqualsOperator(cls, coreTypes, hierarchy, indexedClass, allVariablesList); |
| addHashCode(cls, coreTypes, hierarchy, indexedClass, allVariablesList); |
| addToString(cls, coreTypes, hierarchy, indexedClass, allVariablesList); |
| addCopyWith(cls, coreTypes, hierarchy, indexedClass, allVariablesList, |
| syntheticConstructor, typeEnvironment); |
| } |
| |
| void addConstructor( |
| Class cls, CoreTypes coreTypes, Constructor syntheticConstructor) { |
| Constructor? superConstructor = null; |
| for (Constructor constructor in cls.superclass!.constructors) { |
| if (constructor.name.text == "") { |
| superConstructor = constructor; |
| } |
| } |
| List<VariableDeclaration> superParameters = superConstructor! |
| .function.namedParameters |
| .map<VariableDeclaration>((e) => VariableDeclaration(e.name, type: e.type) |
| ..parent = syntheticConstructor.function) |
| .toList(); |
| Map<String, VariableDeclaration> ownFields = Map.fromIterable(cls.fields, |
| key: (f) => f.name.text, |
| value: (f) => |
| VariableDeclaration(f.name.text, type: f.type, isRequired: true) |
| ..parent = syntheticConstructor.function); |
| |
| List<Initializer> initializersConstructor = cls.fields |
| .map<Initializer>((f) => |
| FieldInitializer(f, VariableGet(ownFields[f.name.text]!)) |
| ..parent = syntheticConstructor) |
| .toList(); |
| |
| initializersConstructor.add(SuperInitializer( |
| superConstructor, |
| Arguments( |
| superParameters.map<Expression>((f) => VariableGet(f)).toList())) |
| ..parent = syntheticConstructor); |
| |
| syntheticConstructor.function.namedParameters |
| ..clear() |
| ..addAll(ownFields.values) |
| ..addAll(superParameters); |
| syntheticConstructor.initializers = initializersConstructor; |
| } |
| |
| void addEqualsOperator(Class cls, CoreTypes coreTypes, ClassHierarchy hierarchy, |
| IndexedClass? indexedClass, List<VariableDeclaration> allVariablesList) { |
| List<VariableDeclaration> allVariables = allVariablesList.toList(); |
| for (Procedure procedure in cls.procedures) { |
| if (procedure.kind == ProcedureKind.Operator && |
| procedure.name.text == "==") { |
| // ==operator is already implemented, spec is to do nothing |
| return; |
| } |
| } |
| DartType returnType = coreTypes.boolRawType(cls.enclosingLibrary.nonNullable); |
| DartType myType = coreTypes.thisInterfaceType(cls, Nullability.nonNullable); |
| |
| VariableDeclaration other = VariableDeclaration("other", |
| type: coreTypes.objectRawType(Nullability.nonNullable)); |
| |
| Map<VariableDeclaration, Member> targetsEquals = new Map(); |
| Map<VariableDeclaration, Member> targets = new Map(); |
| for (VariableDeclaration variable in allVariables) { |
| Member target = coreTypes.objectEquals; |
| Member targetEquals = coreTypes.objectEquals; |
| DartType fieldsType = variable.type; |
| if (fieldsType is InterfaceType) { |
| targetEquals = |
| hierarchy.getInterfaceMember(fieldsType.classNode, Name("=="))!; |
| target = hierarchy.getInterfaceMember(cls, Name(variable.name!))!; |
| } |
| targetsEquals[variable] = targetEquals; |
| targets[variable] = target; |
| } |
| |
| Name name = Name("=="); |
| Procedure equalsOperator = Procedure( |
| name, |
| ProcedureKind.Operator, |
| FunctionNode( |
| ReturnStatement(allVariables |
| .map((f) => _createEquals( |
| _createGet(ThisExpression(), Name(f.name!), |
| interfaceTarget: targets[f]), |
| _createGet(VariableGet(other, myType), Name(f.name!), |
| interfaceTarget: targets[f]), |
| interfaceTarget: targetsEquals[f] as Procedure)) |
| .fold( |
| IsExpression(VariableGet(other), myType), |
| (previousValue, element) => LogicalExpression( |
| previousValue!, LogicalExpressionOperator.AND, element))), |
| returnType: returnType, |
| positionalParameters: [other]), |
| fileUri: cls.fileUri, |
| reference: indexedClass?.lookupGetterReference(name)) |
| ..fileOffset = cls.fileOffset; |
| cls.addProcedure(equalsOperator); |
| } |
| |
| void addHashCode(Class cls, CoreTypes coreTypes, ClassHierarchy hierarchy, |
| IndexedClass? indexedClass, List<VariableDeclaration> allVariablesList) { |
| List<VariableDeclaration> allVariables = allVariablesList.toList(); |
| for (Procedure procedure in cls.procedures) { |
| if (procedure.kind == ProcedureKind.Getter && |
| procedure.name.text == "hashCode") { |
| // hashCode getter is already implemented, spec is to do nothing |
| return; |
| } |
| } |
| DartType returnType = coreTypes.intRawType(cls.enclosingLibrary.nonNullable); |
| |
| Procedure? hashCombine, hashFinish; |
| HashCombineMethodsScanner hashCombineMethodsScanner = |
| new HashCombineMethodsScanner(); |
| JenkinsClassScanner jenkinsScanner = |
| new JenkinsClassScanner(hashCombineMethodsScanner); |
| ScanResult<Class, Procedure> hashMethodsResult = |
| jenkinsScanner.scan(cls.enclosingLibrary.enclosingComponent!); |
| for (Class clazz in hashMethodsResult.targets.keys) { |
| for (Procedure procedure |
| in hashMethodsResult.targets[clazz]!.targets.keys) { |
| if (procedure.name.text == "combine") { |
| hashCombine = procedure; |
| } |
| if (procedure.name.text == "finish") { |
| hashFinish = procedure; |
| } |
| } |
| } |
| |
| Map<VariableDeclaration, Member> targetsHashcode = new Map(); |
| Map<VariableDeclaration, Member> targets = new Map(); |
| for (VariableDeclaration variable in allVariables) { |
| Member target = coreTypes.objectEquals; |
| Member targetHashcode = coreTypes.objectEquals; |
| DartType fieldsType = variable.type; |
| if (fieldsType is InterfaceType) { |
| targetHashcode = |
| hierarchy.getInterfaceMember(fieldsType.classNode, Name("hashCode"))!; |
| target = hierarchy.getInterfaceMember(cls, Name(variable.name!))!; |
| } |
| targetsHashcode[variable] = targetHashcode; |
| targets[variable] = target; |
| } |
| Name name = Name("hashCode"); |
| cls.addProcedure(Procedure( |
| name, |
| ProcedureKind.Getter, |
| FunctionNode( |
| ReturnStatement(StaticInvocation( |
| hashFinish!, |
| Arguments([ |
| allVariables |
| .map((f) => (_createGet( |
| _createGet(ThisExpression(), Name(f.name!), |
| interfaceTarget: targets[f]), |
| Name("hashCode"), |
| interfaceTarget: targetsHashcode[f]))) |
| .fold( |
| _createGet( |
| StringLiteral( |
| cls.enclosingLibrary.importUri.toString() + |
| cls.name), |
| Name("hashCode"), |
| interfaceTarget: hierarchy.getInterfaceMember( |
| coreTypes.stringClass, Name("hashCode"))), |
| (previousValue, element) => StaticInvocation( |
| hashCombine!, Arguments([previousValue, element]))) |
| ]))), |
| returnType: returnType), |
| fileUri: cls.fileUri, |
| reference: indexedClass?.lookupGetterReference(name)) |
| ..fileOffset = cls.fileOffset); |
| } |
| |
| void addToString(Class cls, CoreTypes coreTypes, ClassHierarchy hierarchy, |
| IndexedClass? indexedClass, List<VariableDeclaration> allVariablesList) { |
| List<Expression> wording = [StringLiteral("${cls.name}(")]; |
| |
| for (VariableDeclaration variable in allVariablesList) { |
| wording.add(StringLiteral("${variable.name}: ")); |
| Member? variableTarget = |
| hierarchy.getInterfaceMember(cls, Name(variable.name!)); |
| Procedure toStringTarget = hierarchy.getInterfaceMember( |
| variable.type is InterfaceType |
| ? (variable.type as InterfaceType).classNode |
| : coreTypes.objectClass, |
| Name("toString")) as Procedure; |
| wording.add(_createInvocation( |
| _createGet(ThisExpression(), Name(variable.name!), |
| interfaceTarget: variableTarget), |
| Name("toString"), |
| Arguments([]), |
| interfaceTarget: toStringTarget)); |
| wording.add(StringLiteral(", ")); |
| } |
| if (allVariablesList.length != 0) { |
| wording[wording.length - 1] = StringLiteral(")"); |
| } else { |
| wording.add(StringLiteral(")")); |
| } |
| DartType returnType = |
| coreTypes.stringRawType(cls.enclosingLibrary.nonNullable); |
| Name name = Name("toString"); |
| cls.addProcedure(Procedure( |
| name, |
| ProcedureKind.Method, |
| FunctionNode(ReturnStatement(StringConcatenation(wording)), |
| returnType: returnType), |
| fileUri: cls.fileUri, |
| reference: indexedClass?.lookupGetterReference(name)) |
| ..fileOffset = cls.fileOffset); |
| } |
| |
| void addCopyWith( |
| Class cls, |
| CoreTypes coreTypes, |
| ClassHierarchy hierarchy, |
| IndexedClass? indexedClass, |
| List<VariableDeclaration> allVariablesList, |
| Constructor syntheticConstructor, |
| TypeEnvironment typeEnvironment) { |
| List<VariableDeclaration> allVariables = allVariablesList.toList(); |
| |
| Map<VariableDeclaration, Member> targetsEquals = new Map(); |
| Map<VariableDeclaration, Member> targets = new Map(); |
| for (VariableDeclaration variable in allVariables) { |
| Member target = coreTypes.objectEquals; |
| Member targetEquals = coreTypes.objectEquals; |
| DartType fieldsType = variable.type; |
| if (fieldsType is InterfaceType) { |
| targetEquals = |
| hierarchy.getInterfaceMember(fieldsType.classNode, Name("=="))!; |
| target = hierarchy.getInterfaceMember(cls, Name(variable.name!))!; |
| } |
| targetsEquals[variable] = targetEquals; |
| targets[variable] = target; |
| } |
| |
| Name name = Name("copyWith"); |
| cls.addProcedure(Procedure( |
| name, |
| ProcedureKind.Method, |
| FunctionNode( |
| ReturnStatement(ConstructorInvocation( |
| syntheticConstructor, |
| Arguments([], |
| named: allVariables |
| .map((f) => NamedExpression(f.name!, VariableGet(f))) |
| .toList()))), |
| namedParameters: allVariables), |
| fileUri: cls.fileUri, |
| reference: indexedClass?.lookupGetterReference(name)) |
| ..fileOffset = cls.fileOffset); |
| } |
| |
| List<VariableDeclaration> queryAllInstanceVariables(Class cls) { |
| Constructor? superConstructor = null; |
| for (Constructor constructor in cls.superclass!.constructors) { |
| if (constructor.name.text == "") { |
| superConstructor = constructor; |
| } |
| } |
| return superConstructor!.function.namedParameters |
| .map<VariableDeclaration>( |
| (f) => VariableDeclaration(f.name, type: f.type)) |
| .toList() |
| ..addAll(cls.fields.map<VariableDeclaration>( |
| (f) => VariableDeclaration(f.name.text, type: f.type))); |
| } |
| |
| void removeValueClassAnnotation(Class cls) { |
| int? valueClassAnnotationIndex; |
| for (int annotationIndex = 0; |
| annotationIndex < cls.annotations.length; |
| annotationIndex++) { |
| Expression annotation = cls.annotations[annotationIndex]; |
| if (annotation is ConstantExpression && |
| annotation.constant is StringConstant) { |
| StringConstant constant = annotation.constant as StringConstant; |
| if (constant.value == 'valueClass') { |
| valueClassAnnotationIndex = annotationIndex; |
| } |
| } |
| } |
| cls.annotations.removeAt(valueClassAnnotationIndex!); |
| } |
| |
| void treatCopyWithCallSites(Component component, CoreTypes coreTypes, |
| TypeEnvironment typeEnvironment, ClassHierarchy hierarchy) { |
| ValueClassCopyWithScanner valueCopyWithScanner = |
| new ValueClassCopyWithScanner(); |
| AllMemberScanner copyWithScanner = AllMemberScanner(valueCopyWithScanner); |
| ScanResult<Member, InstanceInvocationExpression> copyWithCallSites = |
| copyWithScanner.scan(component); |
| for (Member memberWithCopyWith in copyWithCallSites.targets.keys) { |
| Map<InstanceInvocationExpression, ScanResult<TreeNode?, TreeNode?>?>? |
| targets = copyWithCallSites.targets[memberWithCopyWith]?.targets; |
| if (targets != null) { |
| StaticTypeContext staticTypeContext = |
| StaticTypeContext(memberWithCopyWith, typeEnvironment); |
| for (InstanceInvocationExpression copyWithCall in targets.keys) { |
| AsExpression receiver = copyWithCall.receiver as AsExpression; |
| |
| Expression valueClassInstance = receiver.operand; |
| DartType valueClassType = |
| valueClassInstance.getStaticType(staticTypeContext); |
| if (valueClassType is InterfaceType) { |
| Class valueClass = valueClassType.classNode; |
| if (isValueClass(valueClass)) { |
| treatCopyWithCallSite( |
| valueClass, copyWithCall, coreTypes, hierarchy); |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| void treatCopyWithCallSite( |
| Class valueClass, |
| InstanceInvocationExpression copyWithCall, |
| CoreTypes coreTypes, |
| ClassHierarchy hierarchy) { |
| Map<String, Expression> preTransformationArguments = new Map(); |
| for (NamedExpression argument in copyWithCall.arguments.named) { |
| preTransformationArguments[argument.name] = argument.value; |
| } |
| Constructor? syntheticConstructor; |
| for (Constructor constructor in valueClass.constructors) { |
| if (constructor.isSynthetic) { |
| syntheticConstructor = constructor; |
| } |
| } |
| List<VariableDeclaration> allArguments = |
| syntheticConstructor!.function.namedParameters; |
| |
| VariableDeclaration letVariable = |
| VariableDeclaration.forValue(copyWithCall.receiver); |
| Arguments postTransformationArguments = Arguments.empty(); |
| for (VariableDeclaration argument in allArguments) { |
| if (preTransformationArguments.containsKey(argument.name)) { |
| postTransformationArguments.named.add(NamedExpression( |
| argument.name!, preTransformationArguments[argument.name]!) |
| ..parent = postTransformationArguments); |
| } else { |
| postTransformationArguments.named.add(NamedExpression(argument.name!, |
| _createGet(VariableGet(letVariable), Name(argument.name!))) |
| ..parent = postTransformationArguments); |
| } |
| } |
| copyWithCall.replaceWith(Let( |
| letVariable, |
| _createInvocation(VariableGet(letVariable), Name("copyWith"), |
| postTransformationArguments))); |
| } |
| |
| bool isValueClass(Class node) { |
| for (Expression annotation in node.annotations) { |
| if (annotation is ConstantExpression && |
| annotation.constant is StringConstant) { |
| StringConstant constant = annotation.constant as StringConstant; |
| if (constant.value == "valueClass") { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| // TODO(johnniwinther): Ensure correct invocation function type and instance |
| // access kind on InstanceInvocation. |
| Expression _createInvocation( |
| Expression receiver, Name name, Arguments arguments, |
| {Procedure? interfaceTarget}) { |
| if (interfaceTarget != null) { |
| return InstanceInvocation( |
| InstanceAccessKind.Instance, receiver, name, arguments, |
| interfaceTarget: interfaceTarget, |
| functionType: interfaceTarget.getterType as FunctionType); |
| } else { |
| return DynamicInvocation( |
| DynamicAccessKind.Dynamic, receiver, name, arguments); |
| } |
| } |
| |
| Expression _createEquals(Expression left, Expression right, |
| {required Procedure interfaceTarget}) { |
| return EqualsCall(left, right, |
| interfaceTarget: interfaceTarget, |
| functionType: interfaceTarget.getterType as FunctionType); |
| } |
| |
| // TODO(johnniwinther): Ensure correct result type on InstanceGet. |
| Expression _createGet(Expression receiver, Name name, |
| {Member? interfaceTarget}) { |
| if (interfaceTarget != null) { |
| return InstanceGet(InstanceAccessKind.Instance, receiver, name, |
| interfaceTarget: interfaceTarget, |
| resultType: interfaceTarget.getterType); |
| } else { |
| return DynamicGet(DynamicAccessKind.Dynamic, receiver, name); |
| } |
| } |