From f0d805fda81ca5bbeacddda6d5cae10ec1dc942f Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Sun, 10 Jul 2022 20:40:24 +0300 Subject: [PATCH] Support Java 17 records --- .../org/teavm/classlib/impl/JCLPlugin.java | 8 + .../lambda/LambdaMetafactorySubstitutor.java | 41 +--- .../impl/record/ObjectMethodsSubstitutor.java | 182 ++++++++++++++++++ .../org/teavm/classlib/java/lang/TRecord.java | 19 ++ .../teavm/model/util/BasicBlockSplitter.java | 3 +- .../teavm/model/util/InvokeDynamicUtil.java | 66 +++++++ .../test/java/org/teavm/vm/RecordTest.java | 63 ++++++ 7 files changed, 342 insertions(+), 40 deletions(-) create mode 100644 classlib/src/main/java/org/teavm/classlib/impl/record/ObjectMethodsSubstitutor.java create mode 100644 classlib/src/main/java/org/teavm/classlib/java/lang/TRecord.java create mode 100644 core/src/main/java/org/teavm/model/util/InvokeDynamicUtil.java create mode 100644 tests/src/test/java/org/teavm/vm/RecordTest.java diff --git a/classlib/src/main/java/org/teavm/classlib/impl/JCLPlugin.java b/classlib/src/main/java/org/teavm/classlib/impl/JCLPlugin.java index dc1941c25..eb003442e 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/JCLPlugin.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/JCLPlugin.java @@ -27,6 +27,7 @@ import org.teavm.classlib.impl.currency.CountriesGenerator; import org.teavm.classlib.impl.currency.CurrenciesGenerator; import org.teavm.classlib.impl.currency.CurrencyHelper; import org.teavm.classlib.impl.lambda.LambdaMetafactorySubstitutor; +import org.teavm.classlib.impl.record.ObjectMethodsSubstitutor; import org.teavm.classlib.impl.tz.DateTimeZoneProvider; import org.teavm.classlib.impl.tz.DateTimeZoneProviderIntrinsic; import org.teavm.classlib.impl.tz.DateTimeZoneProviderPatch; @@ -104,6 +105,13 @@ public class JCLPlugin implements TeaVMPlugin { ValueType.object("java.lang.String"), ValueType.object("java.lang.invoke.MethodType"), ValueType.arrayOf(ValueType.object("java.lang.Object")), ValueType.object("java.lang.invoke.CallSite")), lms); + host.add(new MethodReference("java.lang.runtime.ObjectMethods", "bootstrap", + ValueType.object("java.lang.invoke.MethodHandles$Lookup"), ValueType.object("java.lang.String"), + ValueType.object("java.lang.invoke.TypeDescriptor"), ValueType.object("java.lang.Class"), + ValueType.object("java.lang.String"), + ValueType.arrayOf(ValueType.object("java.lang.invoke.MethodHandle")), + ValueType.object("java.lang.Object")), + new ObjectMethodsSubstitutor()); StringConcatFactorySubstitutor stringConcatSubstitutor = new StringConcatFactorySubstitutor(); host.add(new MethodReference("java.lang.invoke.StringConcatFactory", "makeConcat", diff --git a/classlib/src/main/java/org/teavm/classlib/impl/lambda/LambdaMetafactorySubstitutor.java b/classlib/src/main/java/org/teavm/classlib/impl/lambda/LambdaMetafactorySubstitutor.java index 1822f8b6e..2e7c6d53a 100644 --- a/classlib/src/main/java/org/teavm/classlib/impl/lambda/LambdaMetafactorySubstitutor.java +++ b/classlib/src/main/java/org/teavm/classlib/impl/lambda/LambdaMetafactorySubstitutor.java @@ -43,7 +43,7 @@ import org.teavm.model.TextLocation; import org.teavm.model.ValueType; import org.teavm.model.emit.ProgramEmitter; import org.teavm.model.emit.ValueEmitter; -import org.teavm.model.instructions.InvocationType; +import org.teavm.model.util.InvokeDynamicUtil; public class LambdaMetafactorySubstitutor implements BootstrapMethodSubstitutor { private static final int FLAG_SERIALIZABLE = 1; @@ -114,7 +114,7 @@ public class LambdaMetafactorySubstitutor implements BootstrapMethodSubstitutor implementorSignature[i + capturedVarCount]); } - ValueEmitter result = invoke(pe, implMethod, passedArguments); + ValueEmitter result = InvokeDynamicUtil.invoke(pe, implMethod, passedArguments); ValueType expectedResult = instantiatedMethodType[instantiatedMethodType.length - 1]; if (result != null && expectedResult != ValueType.VOID) { ValueType actualResult = implementorSignature[implementorSignature.length - 1]; @@ -179,43 +179,6 @@ public class LambdaMetafactorySubstitutor implements BootstrapMethodSubstitutor return callerPe.construct(ctor.getOwnerName(), callSite.getArguments().toArray(new ValueEmitter[0])); } - private ValueEmitter invoke(ProgramEmitter pe, MethodHandle handle, ValueEmitter[] arguments) { - switch (handle.getKind()) { - case GET_FIELD: - return arguments[0].getField(handle.getName(), handle.getValueType()); - case GET_STATIC_FIELD: - return pe.getField(handle.getClassName(), handle.getName(), handle.getValueType()); - case PUT_FIELD: - arguments[0].setField(handle.getName(), arguments[0].cast(handle.getValueType())); - return null; - case PUT_STATIC_FIELD: - pe.setField(handle.getClassName(), handle.getName(), arguments[0].cast(handle.getValueType())); - return null; - case INVOKE_VIRTUAL: - case INVOKE_INTERFACE: - case INVOKE_SPECIAL: { - for (int i = 1; i < arguments.length; ++i) { - arguments[i] = arguments[i].cast(handle.getArgumentType(i - 1)); - } - arguments[0] = arguments[0].cast(ValueType.object(handle.getClassName())); - InvocationType type = handle.getKind() == MethodHandleType.INVOKE_SPECIAL - ? InvocationType.SPECIAL - : InvocationType.VIRTUAL; - return arguments[0].invoke(type, handle.getName(), handle.getValueType(), - Arrays.copyOfRange(arguments, 1, arguments.length)); - } - case INVOKE_STATIC: - for (int i = 0; i < arguments.length; ++i) { - arguments[i] = arguments[i].cast(handle.getArgumentType(i)); - } - return pe.invoke(handle.getClassName(), handle.getName(), handle.getValueType(), arguments); - case INVOKE_CONSTRUCTOR: - return pe.construct(handle.getClassName(), arguments); - default: - throw new IllegalArgumentException("Unexpected handle type: " + handle.getKind()); - } - } - private ValueEmitter tryConvertArgument(ValueEmitter arg, ValueType from, ValueType to) { if (from.equals(to)) { return arg; diff --git a/classlib/src/main/java/org/teavm/classlib/impl/record/ObjectMethodsSubstitutor.java b/classlib/src/main/java/org/teavm/classlib/impl/record/ObjectMethodsSubstitutor.java new file mode 100644 index 000000000..f6494ee5a --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/impl/record/ObjectMethodsSubstitutor.java @@ -0,0 +1,182 @@ +/* + * Copyright 2022 Alexey Andreev. + * + * 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.teavm.classlib.impl.record; + +import java.util.Objects; +import org.teavm.dependency.BootstrapMethodSubstitutor; +import org.teavm.dependency.DynamicCallSite; +import org.teavm.model.BasicBlock; +import org.teavm.model.MethodHandle; +import org.teavm.model.ValueType; +import org.teavm.model.emit.ConditionEmitter; +import org.teavm.model.emit.ConditionProducer; +import org.teavm.model.emit.PhiEmitter; +import org.teavm.model.emit.ProgramEmitter; +import org.teavm.model.emit.ValueEmitter; +import org.teavm.model.util.InvokeDynamicUtil; + +public class ObjectMethodsSubstitutor implements BootstrapMethodSubstitutor { + @Override + public ValueEmitter substitute(DynamicCallSite callSite, ProgramEmitter pe) { + switch (callSite.getCalledMethod().getName()) { + case "equals": + return substituteEquals(callSite, pe); + case "hashCode": + return substituteHashCode(callSite, pe); + case "toString": + return substituteToString(callSite, pe); + default: + throw new RuntimeException("Unexpected method: " + callSite.getCalledMethod().getName()); + } + } + + private ValueEmitter substituteEquals(DynamicCallSite callSite, ProgramEmitter pe) { + ValueType type = callSite.getBootstrapArguments().get(0).getValueType(); + + ValueEmitter thisVar = callSite.getArguments().get(0); + ValueEmitter thatVar = callSite.getArguments().get(1); + BasicBlock joint = pe.prepareBlock(); + PhiEmitter result = pe.phi(ValueType.INTEGER, joint); + pe.when(thisVar.isSame(thatVar)).thenDo(() -> { + pe.constant(1).propagateTo(result); + pe.jump(joint); + }); + ConditionProducer classCondition = () -> thisVar.isNull() + .or(() -> thatVar.invokeVirtual("getClass", Class.class).isNotSame(pe.constant(type))); + pe.when(classCondition).thenDo(() -> { + pe.constant(0).propagateTo(result); + pe.jump(joint); + }); + + ValueEmitter castThatVar = thatVar.cast(type); + + String names = callSite.getBootstrapArguments().get(1).getString(); + int argIndex = 2; + int index = 0; + while (index < names.length()) { + int next = names.indexOf(';', index); + if (next < 0) { + next = names.length(); + } + index = next + 1; + MethodHandle getter = callSite.getBootstrapArguments().get(argIndex++).getMethodHandle(); + ValueEmitter thisField = InvokeDynamicUtil.invoke(pe, getter, thisVar); + ValueEmitter thatField = InvokeDynamicUtil.invoke(pe, getter, castThatVar); + pe.when(compareEquality(pe, getter.getValueType(), thisField, thatField).not()).thenDo(() -> { + pe.constant(0).propagateTo(result); + pe.jump(joint); + }); + } + + pe.constant(1).propagateTo(result); + pe.jump(joint); + pe.enter(joint); + + return result.getValue(); + } + + private ConditionEmitter compareEquality(ProgramEmitter pe, ValueType type, ValueEmitter a, ValueEmitter b) { + if (type instanceof ValueType.Primitive) { + switch (((ValueType.Primitive) type).getKind()) { + case BOOLEAN: + case BYTE: + case SHORT: + case CHARACTER: + case INTEGER: + case LONG: + return a.isEqualTo(b); + case FLOAT: + return pe.invoke(Float.class, "compare", int.class, a, b).isEqualTo(pe.constant(0)); + case DOUBLE: + return pe.invoke(Double.class, "compare", int.class, a, b).isEqualTo(pe.constant(0)); + } + } + return pe.invoke(Objects.class, "equals", boolean.class, a.cast(Object.class), b.cast(Object.class)).isTrue(); + } + + private ValueEmitter substituteHashCode(DynamicCallSite callSite, ProgramEmitter pe) { + ValueEmitter thisVar = callSite.getArguments().get(0); + String names = callSite.getBootstrapArguments().get(1).getString(); + ValueEmitter resultVar = pe.constant(1); + + int argIndex = 2; + int index = 0; + while (index < names.length()) { + int next = names.indexOf(';', index); + if (next < 0) { + next = names.length(); + } + index = next + 1; + MethodHandle getter = callSite.getBootstrapArguments().get(argIndex++).getMethodHandle(); + resultVar = resultVar.mul(31); + ValueEmitter thisField = InvokeDynamicUtil.invoke(pe, getter, thisVar); + resultVar = resultVar.add(hash(pe, getter.getValueType(), thisField)); + } + + return resultVar; + } + + private ValueEmitter hash(ProgramEmitter pe, ValueType type, ValueEmitter a) { + if (type instanceof ValueType.Primitive) { + switch (((ValueType.Primitive) type).getKind()) { + case BOOLEAN: + return pe.invoke(Boolean.class, "hashCode", int.class, a); + case BYTE: + return pe.invoke(Byte.class, "hashCode", int.class, a); + case SHORT: + return pe.invoke(Short.class, "hashCode", int.class, a); + case CHARACTER: + return pe.invoke(Character.class, "hashCode", int.class, a); + case INTEGER: + return pe.invoke(Integer.class, "hashCode", int.class, a); + case LONG: + return pe.invoke(Long.class, "hashCode", int.class, a); + case FLOAT: + return pe.invoke(Float.class, "hashCode", int.class, a); + case DOUBLE: + return pe.invoke(Double.class, "hashCode", int.class, a); + } + } + return pe.invoke(Objects.class, "hashCode", int.class, a.cast(Object.class)); + } + + private ValueEmitter substituteToString(DynamicCallSite callSite, ProgramEmitter pe) { + ValueEmitter thisVar = callSite.getArguments().get(0); + String names = callSite.getBootstrapArguments().get(1).getString(); + ValueEmitter resultVar = pe.construct(StringBuilder.class, pe.constant("[")); + + int argIndex = 2; + int index = 0; + while (index < names.length()) { + int next = names.indexOf(';', index); + if (next < 0) { + next = names.length(); + } + String fieldName = names.substring(index, next); + MethodHandle getter = callSite.getBootstrapArguments().get(argIndex++).getMethodHandle(); + + String fieldTitle = (index == 0 ? "" : ", ") + fieldName + "="; + resultVar = resultVar.invokeVirtual("append", StringBuilder.class, pe.constant(fieldTitle)); + ValueEmitter thisField = InvokeDynamicUtil.invoke(pe, getter, thisVar); + resultVar = resultVar.invokeVirtual("append", StringBuilder.class, thisField); + + index = next + 1; + } + + return resultVar.invokeVirtual("append", StringBuilder.class, pe.constant("]")) + .invokeVirtual("toString", String.class); + } +} diff --git a/classlib/src/main/java/org/teavm/classlib/java/lang/TRecord.java b/classlib/src/main/java/org/teavm/classlib/java/lang/TRecord.java new file mode 100644 index 000000000..66715c0e1 --- /dev/null +++ b/classlib/src/main/java/org/teavm/classlib/java/lang/TRecord.java @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Alexey Andreev. + * + * 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.teavm.classlib.java.lang; + +public abstract class TRecord { +} diff --git a/core/src/main/java/org/teavm/model/util/BasicBlockSplitter.java b/core/src/main/java/org/teavm/model/util/BasicBlockSplitter.java index 7fc8bbe3c..69eb440f1 100644 --- a/core/src/main/java/org/teavm/model/util/BasicBlockSplitter.java +++ b/core/src/main/java/org/teavm/model/util/BasicBlockSplitter.java @@ -114,7 +114,8 @@ public class BasicBlockSplitter { Map> incomingsBySource = new LinkedHashMap<>(); for (Phi phi : block.getPhis()) { for (Incoming incoming : phi.getIncomings()) { - if (mappings[incoming.getSource().getIndex()] == incoming.getSource().getIndex()) { + if (incoming.getSource().getIndex() >= mappings.length + || mappings[incoming.getSource().getIndex()] == incoming.getSource().getIndex()) { continue; } incomingsBySource.computeIfAbsent(incoming.getSource(), b -> new ArrayList<>()).add(incoming); diff --git a/core/src/main/java/org/teavm/model/util/InvokeDynamicUtil.java b/core/src/main/java/org/teavm/model/util/InvokeDynamicUtil.java new file mode 100644 index 000000000..75bbf397e --- /dev/null +++ b/core/src/main/java/org/teavm/model/util/InvokeDynamicUtil.java @@ -0,0 +1,66 @@ +/* + * Copyright 2022 Alexey Andreev. + * + * 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.teavm.model.util; + +import java.util.Arrays; +import org.teavm.model.MethodHandle; +import org.teavm.model.MethodHandleType; +import org.teavm.model.ValueType; +import org.teavm.model.emit.ProgramEmitter; +import org.teavm.model.emit.ValueEmitter; +import org.teavm.model.instructions.InvocationType; + +public final class InvokeDynamicUtil { + private InvokeDynamicUtil() { + } + + public static ValueEmitter invoke(ProgramEmitter pe, MethodHandle handle, ValueEmitter... arguments) { + switch (handle.getKind()) { + case GET_FIELD: + return arguments[0].getField(handle.getName(), handle.getValueType()); + case GET_STATIC_FIELD: + return pe.getField(handle.getClassName(), handle.getName(), handle.getValueType()); + case PUT_FIELD: + arguments[0].setField(handle.getName(), arguments[0].cast(handle.getValueType())); + return null; + case PUT_STATIC_FIELD: + pe.setField(handle.getClassName(), handle.getName(), arguments[0].cast(handle.getValueType())); + return null; + case INVOKE_VIRTUAL: + case INVOKE_INTERFACE: + case INVOKE_SPECIAL: { + for (int i = 1; i < arguments.length; ++i) { + arguments[i] = arguments[i].cast(handle.getArgumentType(i - 1)); + } + arguments[0] = arguments[0].cast(ValueType.object(handle.getClassName())); + InvocationType type = handle.getKind() == MethodHandleType.INVOKE_SPECIAL + ? InvocationType.SPECIAL + : InvocationType.VIRTUAL; + return arguments[0].invoke(type, handle.getName(), handle.getValueType(), + Arrays.copyOfRange(arguments, 1, arguments.length)); + } + case INVOKE_STATIC: + for (int i = 0; i < arguments.length; ++i) { + arguments[i] = arguments[i].cast(handle.getArgumentType(i)); + } + return pe.invoke(handle.getClassName(), handle.getName(), handle.getValueType(), arguments); + case INVOKE_CONSTRUCTOR: + return pe.construct(handle.getClassName(), arguments); + default: + throw new IllegalArgumentException("Unexpected handle type: " + handle.getKind()); + } + } +} diff --git a/tests/src/test/java/org/teavm/vm/RecordTest.java b/tests/src/test/java/org/teavm/vm/RecordTest.java new file mode 100644 index 000000000..06e09c98e --- /dev/null +++ b/tests/src/test/java/org/teavm/vm/RecordTest.java @@ -0,0 +1,63 @@ +/* + * Copyright 2022 Alexey Andreev. + * + * 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.teavm.vm; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.teavm.junit.TeaVMTestRunner; + +@RunWith(TeaVMTestRunner.class) +public class RecordTest { + @Test + public void equalsMethod() { + assertEquals(new A(2, "q"), new A(2, "q")); + assertNotEquals(new A(2, "q"), new A(3, "q")); + assertNotEquals(new A(2, "q"), new A(2, "w")); + } + + @Test + public void hashCodeMethod() { + assertEquals(new A(2, "q").hashCode(), new A(2, "q").hashCode()); + } + + @Test + public void toStringMethod() { + String s = new A(2, "q").toString(); + + int index = 0; + + index = s.indexOf("x", index); + assertTrue(index > 0); + ++index; + + index = s.indexOf("2", index); + assertTrue(index > 0); + ++index; + + index = s.indexOf("y", index); + assertTrue(index > 0); + ++index; + + index = s.indexOf("q", index); + assertTrue(index > 0); + } + + record A(int x, String y) { + } +}