Supporting data class for JS backend

This commit is contained in:
Peter Rybin
2014-04-14 04:13:21 +04:00
committed by Zalim Bashorov
parent 535cc3eed8
commit ab81d2e261
13 changed files with 534 additions and 7 deletions
@@ -0,0 +1,49 @@
/*
* Copyright 2010-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.k2js.test.semantics;
import org.jetbrains.k2js.test.SingleFileTranslationTest;
public class DataClassTest extends SingleFileTranslationTest {
public DataClassTest() {
super("dataClass/");
}
public void testComponents() throws Exception {
checkFooBoxIsOk();
}
public void testCopy() throws Exception {
checkFooBoxIsOk();
}
public void testEquals() throws Exception {
checkFooBoxIsOk();
}
public void testHashcode() throws Exception {
checkFooBoxIsOk();
}
public void testTostring() throws Exception {
checkFooBoxIsOk();
}
public void testKeyrole() throws Exception {
checkFooBoxIsOk();
}
}
@@ -28,6 +28,7 @@ import org.jetbrains.jet.lang.psi.JetObjectDeclaration;
import org.jetbrains.jet.lang.psi.JetParameter;
import org.jetbrains.jet.lang.types.JetType;
import org.jetbrains.jet.lang.types.TypeConstructor;
import org.jetbrains.jet.lang.types.lang.KotlinBuiltIns;
import org.jetbrains.k2js.translate.context.DefinitionPlace;
import org.jetbrains.k2js.translate.context.Namer;
import org.jetbrains.k2js.translate.context.TranslationContext;
@@ -45,8 +46,7 @@ import static org.jetbrains.k2js.translate.reference.ReferenceTranslator.transla
import static org.jetbrains.k2js.translate.utils.BindingUtils.getClassDescriptor;
import static org.jetbrains.k2js.translate.utils.BindingUtils.getPropertyDescriptorForConstructorParameter;
import static org.jetbrains.k2js.translate.utils.JsDescriptorUtils.getContainingClass;
import static org.jetbrains.k2js.translate.utils.JsDescriptorUtils.getReceiverParameterForDeclaration;
import static org.jetbrains.k2js.translate.utils.JsDescriptorUtils.getSupertypesWithoutFakes;
import static org.jetbrains.k2js.translate.utils.JsDescriptorUtils.*;
import static org.jetbrains.k2js.translate.utils.PsiUtils.getPrimaryConstructorParameters;
import static org.jetbrains.k2js.translate.utils.TranslationUtils.simpleReturnFunction;
@@ -134,6 +134,10 @@ public final class ClassTranslator extends AbstractTranslator {
bodyVisitor.traverseContainer(classDeclaration, declarationContext);
mayBeAddEnumEntry(bodyVisitor.getEnumEntryList(), staticProperties, declarationContext);
if (KotlinBuiltIns.getInstance().isData(descriptor)) {
new JsDataClassGenerator(classDeclaration, declarationContext, properties).generate();
}
boolean hasStaticProperties = !staticProperties.isEmpty();
if (!properties.isEmpty() || hasStaticProperties) {
if (properties.isEmpty()) {
@@ -0,0 +1,192 @@
/*
* Copyright 2010-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.k2js.translate.declaration;
import com.google.dart.compiler.backend.js.ast.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.jet.backend.common.CodegenUtil;
import org.jetbrains.jet.backend.common.DataClassMethodGenerator;
import org.jetbrains.jet.lang.descriptors.*;
import org.jetbrains.jet.lang.psi.JetClassOrObject;
import org.jetbrains.jet.lang.psi.JetParameter;
import org.jetbrains.k2js.translate.context.Namer;
import org.jetbrains.k2js.translate.context.TranslationContext;
import org.jetbrains.k2js.translate.utils.JsAstUtils;
import java.util.ArrayList;
import java.util.List;
class JsDataClassGenerator extends DataClassMethodGenerator {
private final TranslationContext context;
private final List<? super JsPropertyInitializer> output;
JsDataClassGenerator(JetClassOrObject klass, TranslationContext context, List<? super JsPropertyInitializer> output) {
super(klass, context.bindingContext());
this.context = context;
this.output = output;
}
@Override
public void generateComponentFunction(@NotNull FunctionDescriptor function, @NotNull ValueParameterDescriptor parameter) {
JsFunction functionObject = generateJsMethod(function);
JsExpression returnExpression = propertyAccessor(JsLiteral.THIS, context.getNameForDescriptor(parameter).toString());
functionObject.getBody().getStatements().add(new JsReturn(returnExpression));
}
@Override
public void generateCopyFunction(@NotNull FunctionDescriptor function, @NotNull List<JetParameter> constructorParameters) {
JsFunction functionObj = generateJsMethod(function);
JsScope funScope = functionObj.getScope();
assert function.getValueParameters().size() == constructorParameters.size();
List<JsExpression> constructorArguments = new ArrayList<JsExpression>(constructorParameters.size());
for (int i = 0; i < constructorParameters.size(); i++) {
JetParameter constructorParam = constructorParameters.get(i);
JsName paramName = funScope.declareName(constructorParam.getName());
functionObj.getParameters().add(new JsParameter(paramName));
JsExpression argumentValue;
JsExpression parameterValue = new JsNameRef(paramName);
if (constructorParam.getValOrVarNode() == null) {
assert !function.getValueParameters().get(i).hasDefaultValue();
// Caller cannot rely on default value and pass undefined here.
argumentValue = parameterValue;
}
else {
JsExpression defaultCondition = JsAstUtils.equality(new JsNameRef(paramName), context.namer().getUndefinedExpression());
argumentValue = new JsConditional(defaultCondition,
propertyAccessor(JsLiteral.THIS, constructorParam.getName()),
parameterValue);
}
constructorArguments.add(argumentValue);
}
ClassDescriptor classDescriptor = (ClassDescriptor) function.getContainingDeclaration();
ConstructorDescriptor constructor = classDescriptor.getConstructors().iterator().next();
JsExpression constructorRef = context.getQualifiedReference(constructor);
JsExpression returnExpression = new JsNew(constructorRef, constructorArguments);
functionObj.getBody().getStatements().add(new JsReturn(returnExpression));
}
@Override
public void generateToStringMethod(@NotNull List<PropertyDescriptor> classProperties) {
// TODO: relax this limitation, with the data generation logic fixed.
assert !classProperties.isEmpty();
FunctionDescriptor prototypeFun = CodegenUtil.getAnyToStringMethod();
JsFunction functionObj = generateJsMethod(prototypeFun);
JsProgram jsProgram = context.program();
JsExpression result = null;
for (int i = 0; i < classProperties.size(); i++) {
String name = classProperties.get(i).getName().toString();
JsExpression literal = jsProgram.getStringLiteral((i == 0 ? (getClassDescriptor().getName() + "(") : ", ") + name + "=");
JsExpression expr =
new JsInvocation(new JsNameRef("toString", Namer.KOTLIN_OBJECT_REF), propertyAccessor(JsLiteral.THIS, name));
JsExpression component = JsAstUtils.sum(literal, expr);
if (result == null) {
result = component;
}
else {
result = JsAstUtils.sum(result, component);
}
}
assert result != null;
result = JsAstUtils.sum(result, jsProgram.getStringLiteral(")"));
functionObj.getBody().getStatements().add(new JsReturn(result));
}
@Override
public void generateHashCodeMethod(@NotNull List<PropertyDescriptor> classProperties) {
FunctionDescriptor prototypeFun = CodegenUtil.getAnyHashCodeMethod();
JsFunction functionObj = generateJsMethod(prototypeFun);
JsProgram jsProgram = context.program();
List<JsStatement> statements = functionObj.getBody().getStatements();
JsName varName = functionObj.getScope().declareName("result");
int typeHash = context.getQualifiedReference(getClassDescriptor()).toString().hashCode();
statements.add(new JsVars(new JsVars.JsVar(varName, jsProgram.getNumberLiteral(typeHash))));
for (PropertyDescriptor prop : classProperties) {
// TODO: we should statically check that we can call hashCode method directly.
JsExpression component = new JsInvocation(new JsNameRef("hashCode", Namer.KOTLIN_OBJECT_REF),
propertyAccessor(JsLiteral.THIS, prop.getName().toString()));
JsExpression newHashValue = JsAstUtils.sum(JsAstUtils.mul(new JsNameRef(varName), jsProgram.getNumberLiteral(31)), component);
JsExpression assignment = JsAstUtils.assignment(new JsNameRef(varName),
new JsBinaryOperation(JsBinaryOperator.BIT_OR, newHashValue,
jsProgram.getNumberLiteral(0)));
statements.add(assignment.makeStmt());
}
statements.add(new JsReturn(new JsNameRef(varName)));
}
@Override
public void generateEqualsMethod(@NotNull List<PropertyDescriptor> classProperties) {
assert !classProperties.isEmpty();
FunctionDescriptor prototypeFun = CodegenUtil.getAnyEqualsMethod();
JsFunction functionObj = generateJsMethod(prototypeFun);
JsScope funScope = functionObj.getScope();
JsName paramName = funScope.declareName("other");
functionObj.getParameters().add(new JsParameter(paramName));
JsExpression referenceEqual = JsAstUtils.equality(JsLiteral.THIS, new JsNameRef(paramName));
JsExpression isNotNull = JsAstUtils.inequality(new JsNameRef(paramName), JsLiteral.NULL);
JsExpression prototypeEqual =
JsAstUtils.equality(new JsInvocation(new JsNameRef("getPrototypeOf", new JsNameRef("Object")), JsLiteral.THIS),
new JsInvocation(new JsNameRef("getPrototypeOf", new JsNameRef("Object")), new JsNameRef(paramName)));
JsExpression fieldChain = null;
for (PropertyDescriptor prop : classProperties) {
String name = prop.getName().toString();
JsExpression next = new JsInvocation(new JsNameRef("equals", Namer.KOTLIN_OBJECT_REF),
propertyAccessor(JsLiteral.THIS, name),
propertyAccessor(new JsNameRef(paramName), name));
if (fieldChain == null) {
fieldChain = next;
}
else {
fieldChain = JsAstUtils.and(fieldChain, next);
}
}
assert fieldChain != null;
JsExpression returnExpression =
JsAstUtils.or(referenceEqual, JsAstUtils.and(isNotNull, JsAstUtils.and(prototypeEqual, fieldChain)));
functionObj.getBody().getStatements().add(new JsReturn(returnExpression));
}
private static JsExpression propertyAccessor(JsExpression object, String propertyName) {
// Might be not accurate enough.
return new JsNameRef(propertyName, object);
}
private JsFunction generateJsMethod(@NotNull FunctionDescriptor functionDescriptor) {
JsName functionName = context.getNameForDescriptor(functionDescriptor);
JsScope enclosingScope = context.scope();
JsFunction functionObject = JsAstUtils.createFunctionWithEmptyBody(enclosingScope);
JsPropertyInitializer initializer = new JsPropertyInitializer(functionName.makeRef(), functionObject);
output.add(initializer);
return functionObject;
}
}
@@ -65,6 +65,12 @@ public final class TopLevelFIF extends CompositeFIF {
return new JsBinaryOperation(JsBinaryOperator.REF_EQ, receiver, arguments.get(0));
}
};
@NotNull
public static final DescriptorPredicate HASH_CODE_IN_ANY = pattern("kotlin", "Any", "hashCode");
@NotNull
public static final KotlinFunctionIntrinsic KOTLIN_HASH_CODE = new KotlinFunctionIntrinsic("hashCode");
@NotNull
private static final FunctionIntrinsic RETURN_RECEIVER_INTRINSIC = new FunctionIntrinsic() {
@NotNull
@@ -156,6 +162,7 @@ public final class TopLevelFIF extends CompositeFIF {
add(pattern("kotlin", "toString").receiverExists(), TO_STRING);
add(pattern("kotlin", "equals").receiverExists(), KOTLIN_EQUALS);
add(pattern("kotlin", "identityEquals").receiverExists(), IDENTITY_EQUALS);
add(HASH_CODE_IN_ANY, KOTLIN_HASH_CODE);
add(pattern(NamePredicate.PRIMITIVE_NUMBERS, "equals"), KOTLIN_EQUALS);
add(pattern("String|Boolean|Char|Number.equals"), KOTLIN_EQUALS);
add(pattern("kotlin", "arrayOfNulls"), new KotlinFunctionIntrinsic("nullArray"));
@@ -139,6 +139,11 @@ public final class JsAstUtils {
return new JsBinaryOperation(JsBinaryOperator.SUB, left, right);
}
@NotNull
public static JsBinaryOperation mul(@NotNull JsExpression left, @NotNull JsExpression right) {
return new JsBinaryOperation(JsBinaryOperator.MUL, left, right);
}
@NotNull
public static JsPrefixOperation not(@NotNull JsExpression expression) {
return new JsPrefixOperation(JsUnaryOperator.NOT, expression);
@@ -0,0 +1,24 @@
package foo
data class Dat(val start: String, middle: String, val end: String) {
fun getLabel() : String {
return start + end
}
}
fun assertEquals<T>(expected: T, actual: T) {
if (expected != actual) throw Exception("expected: $expected, actual, $actual")
}
fun assertNotEqual<T>(expected: T, actual: T) {
if (expected == actual) throw Exception("unexpectedly equal: $expected and $actual")
}
fun box(): String {
val d = Dat("max", "-", "min")
assertEquals("maxmin", d.getLabel())
val (p1, p2) = d
assertEquals("max", p1)
assertEquals("min", p2)
return "OK"
}
@@ -0,0 +1,11 @@
package foo
data class Dat(val start: String, middle: String, val end: String)
fun box(): String {
val d1 = Dat("OO", "-", "PS")
val d2: Dat = d1.copy(end = "K", middle = "+")
val d3: Dat = d2.copy(start = "O", middle = "-")
val (p1, p2) = d3
return p1 + p2
}
@@ -0,0 +1,49 @@
package foo
data class Holder<T>(val v: T)
data class Dat(val start: String, val end: String)
data class Dat2(val start: String, val end: String)
class Obj(val start: String, val end: String)
fun assertEquals<T>(expected: T, actual: T) {
if (expected != actual) throw Exception("expected: $expected, actual, $actual")
}
fun assertNotEqual<T>(expected: T, actual: T) {
if (expected == actual) throw Exception("unexpectedly equal: $expected and $actual")
}
fun box(): String {
val d1 = Dat("a", "b")
val d2 = Dat("a", "b")
val d3 = Dat("a", "c")
val otherD1 = Dat2("a", "b")
assertEquals(d1, d1)
assertEquals(d1, d2)
assertNotEqual(d1, d3)
assertNotEqual(d1, otherD1)
var hd1 = Holder(Dat("y", "n"))
var hd2 = Holder(Dat("y", "n"))
var hd3 = Holder(Dat("1", "2"))
assertEquals(hd1, hd1)
assertEquals(hd1, hd2)
assertNotEqual(hd1, hd3)
var ho1 = Holder(Obj("+", "-"))
var ho2 = Holder(Obj("+", "-"))
var ho3 = Holder(Obj("*", "*"))
assertEquals(ho1, ho1)
assertNotEqual(ho1, ho2)
assertNotEqual(ho1, ho3)
return "OK"
}
@@ -0,0 +1,88 @@
package foo
import java.util.ArrayList
data class Holder<T>(val v: T)
data class Dat(val start: String, val end: String)
class Obj(val start: String, val end: String)
fun assertEquals<T>(expected: T, actual: T) {
if (expected != actual) throw Exception("expected: $expected, actual, $actual")
}
fun assertSomeNotEqual<T>(c: Iterable<T>) {
val it = c.iterator()
val first = it.next()
while (it.hasNext()) {
val item: T = it.next()
if (item != first) {
return;
}
}
throw Exception("All elements are the same: $first")
}
fun assertAllEqual<T>(c: Iterable<out T>) {
val it = c.iterator()
val first = it.next()
while (it.hasNext()) {
val item: T = it.next()
assertEquals(first, item)
}
}
val hashCoder: (o: Any) -> Int = { o -> o.hashCode() }
val <T> wrapInH = { (t: T) -> Holder(t) }
fun box(): String {
// Check that same Dat's have the same hashcode.
val sameDs = listOf(Dat("a", "b"), Dat("a", "b"))
assertAllEqual(map(sameDs, hashCoder))
// Check that different Dat's have different hashcodes (at least some of them).
val differentDs = listOf(Dat("a", "b"), Dat("a", "c"), Dat("a", "d"))
assertSomeNotEqual(map(differentDs, hashCoder))
// Check the same on Obj's, which should be always different and with different hashcodes.
val sameOs = listOf(Obj("a", "b"), Obj("a", "b"), Obj("a", "b"))
val differentOs = listOf(Obj("a", "b"), Obj("a", "b"), Obj("a", "b"))
// Obj's are always different.
assertSomeNotEqual(map(sameOs, hashCoder))
assertSomeNotEqual(map(differentOs, hashCoder))
// Both Dat's and Obj's wrapped as Holder should retain their hashcode relations.
val sameHDs = map(sameDs, wrapInH)
assertAllEqual(map(sameHDs, hashCoder))
val differentHDs = map(differentDs, wrapInH)
assertSomeNotEqual(map(differentHDs, hashCoder))
val sameHOs = map(sameOs, wrapInH)
assertSomeNotEqual(map(sameHOs, hashCoder))
val differentHOs = map(differentOs, wrapInH)
assertSomeNotEqual(map(differentHOs, hashCoder))
return "OK"
}
fun listOf<T>(vararg a: T): List<T> {
val list = ArrayList<T>()
for (e in a) {
list.add(e)
}
return list
}
fun map<T, S>(c : Iterable<T>, transform : (T) -> S) : List<S> {
val list = ArrayList<S>()
for (t in c) {
list.add(transform(t))
}
return list
}
@@ -0,0 +1,27 @@
package foo
data class Holder<T>(val v: T)
data class Dat(val start: String, val end: String)
class Obj(val start: String, val end: String)
fun assertEquals<T>(expected: T, actual: T) {
if (expected != actual) throw Exception("expected: $expected, actual, $actual")
}
fun box(): String {
val setD = java.util.HashSet<Holder<Dat>>()
setD.add(Holder(Dat("a", "b")))
setD.add(Holder(Dat("a", "b")))
setD.add(Holder(Dat("a", "b")))
assertEquals(1, setD.size())
val setO = java.util.HashSet<Holder<Obj>>()
setO.add(Holder(Obj("a", "b")))
setO.add(Holder(Obj("a", "b")))
setO.add(Holder(Obj("a", "b")))
assertEquals(3, setO.size())
return "OK"
}
@@ -0,0 +1,27 @@
package foo
data class Holder<T>(val v: T)
data class Dat(val start: String, val end: String)
class Obj(val start: String, val end: String)
fun assertEquals<T>(expected: T, actual: T) {
if (expected != actual) throw Exception("expected: $expected, actual, $actual")
}
fun box(): String {
val d = Dat("a", "b")
assertEquals("Dat(start=a, end=b)", "${d}")
var hd = Holder(Dat("y", "n"))
assertEquals("Holder(v=Dat(start=y, end=n))", "${hd}")
var ho = Holder(Obj("+", "-"))
assertEquals("Holder(v=[object Object])", "${ho}")
return "OK"
}
+44
View File
@@ -48,6 +48,27 @@
return obj1 === obj2;
};
Kotlin.hashCode = function (obj) {
if (obj == null) {
return 0;
}
if ("function" == typeof obj.hashCode) {
return obj.hashCode();
}
var objType = typeof obj;
if ("object" == objType || "function" == objType) {
return getObjectHashCode(obj);
} else if ("number" == objType) {
// TODO: a more elaborate code is needed for floating point values.
return obj | 0;
} if ("boolean" == objType) {
return Number(obj)
}
var str = String(obj);
return getStringHashCode(str);
};
Kotlin.toString = function (o) {
if (o == null) {
return "null";
@@ -97,6 +118,29 @@
};
}
/** @const */
var POW_2_32 = 4294967296;
// TODO: consider switching to Symbol type once we are on ES6.
/** @const */
var OBJECT_HASH_CODE_PROPERTY_NAME = "kotlinHashCodeValue$";
function getObjectHashCode(obj) {
if (!(OBJECT_HASH_CODE_PROPERTY_NAME in obj)) {
var hash = (Math.random() * POW_2_32) | 0; // Make 32-bit singed integer.
Object.defineProperty(obj, OBJECT_HASH_CODE_PROPERTY_NAME, { value: hash, enumerable: false });
}
return obj[OBJECT_HASH_CODE_PROPERTY_NAME];
}
function getStringHashCode(str) {
var hash = 0;
for (var i = 0; i < str.length; i++) {
var code = str.charCodeAt(i);
hash = (hash * 31 + code) | 0; // Keep it 32-bit.
}
return hash;
}
/**
* @interface
* @template T
+5 -5
View File
@@ -64,12 +64,12 @@
}
function equals_fixedValueHasEquals(fixedValue, variableValue) {
return fixedValue.equals(variableValue);
return fixedValue.equals_za3rmp$(variableValue);
}
function equals_fixedValueNoEquals(fixedValue, variableValue) {
return (typeof variableValue.equals == FUNCTION) ?
variableValue.equals(fixedValue) : (fixedValue === variableValue);
return (typeof variableValue.equals_za3rmp$ == FUNCTION) ?
variableValue.equals_za3rmp$(fixedValue) : (fixedValue === variableValue);
}
function createKeyValCheck(kvStr) {
@@ -138,7 +138,7 @@
Bucket.prototype = {
getEqualityFunction: function (searchValue) {
return (typeof searchValue.equals == FUNCTION) ? equals_fixedValueHasEquals : equals_fixedValueNoEquals;
return (typeof searchValue.equals_za3rmp$ == FUNCTION) ? equals_fixedValueHasEquals : equals_fixedValueNoEquals;
},
getEntryForKey: createBucketSearcher(ENTRY),
@@ -687,7 +687,7 @@ Kotlin.PrimitiveHashSet = Kotlin.createClassNow(Kotlin.AbstractCollection,
return h;
};
this.equals = function (o) {
this.equals_za3rmp$ = function (o) {
if (o === null || o === undefined) return false;
if (this.size() === o.size()) {
var iter1 = this.iterator();