Skip to content

Commit

Permalink
Adds translation support for Java record types.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 690725433
  • Loading branch information
tomball authored and copybara-github committed Oct 28, 2024
1 parent cc2fc4f commit f9b88d1
Show file tree
Hide file tree
Showing 13 changed files with 707 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
/*
* Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package java.lang;

/**
* This is the common base class of all Java language record classes.
*
* <p>More information about records, including descriptions of the
* implicitly declared methods synthesized by the compiler, can be
* found in section 8.10 of
* <cite>The Java Language Specification</cite>.
*
* <p>A <em>record class</em> is a shallowly immutable, transparent carrier for
* a fixed set of values, called the <em>record components</em>. The Java
* language provides concise syntax for declaring record classes, whereby the
* record components are declared in the record header. The list of record
* components declared in the record header form the <em>record descriptor</em>.
*
* <p>A record class has the following mandated members: a <em>canonical
* constructor</em>, which must provide at least as much access as the record
* class and whose descriptor is the same as the record descriptor;
* a private final field corresponding to each component, whose name and
* type are the same as that of the component; a public accessor method
* corresponding to each component, whose name and return type are the same as
* that of the component. If not explicitly declared in the body of the record,
* implicit implementations for these members are provided.
*
* <p>The implicit declaration of the canonical constructor has the same accessibility
* as the record class and initializes the component fields from the corresponding
* constructor arguments. The implicit declaration of the accessor methods returns
* the value of the corresponding component field. The implicit declaration of the
* {@link Object#equals(Object)}, {@link Object#hashCode()}, and {@link Object#toString()}
* methods are derived from all of the component fields.
*
* <p>The primary reasons to provide an explicit declaration for the
* canonical constructor or accessor methods are to validate constructor
* arguments, perform defensive copies on mutable components, or normalize groups
* of components (such as reducing a rational number to lowest terms.)
*
* <p>For all record classes, the following invariant must hold: if a record R's
* components are {@code c1, c2, ... cn}, then if a record instance is copied
* as follows:
* <pre>
* R copy = new R(r.c1(), r.c2(), ..., r.cn());
* </pre>
* then it must be the case that {@code r.equals(copy)}.
*
* @apiNote
* A record class that {@code implements} {@link java.io.Serializable} is said
* to be a <i>serializable record</i>. Serializable records are serialized and
* deserialized differently than ordinary serializable objects. During
* deserialization the record's canonical constructor is invoked to construct
* the record object. Certain serialization-related methods, such as readObject
* and writeObject, are ignored for serializable records. More information about
* serializable records can be found in the
* <a href="{@docRoot}/../specs/serialization/serial-arch.html#serialization-of-records">
* <cite>Java Object Serialization Specification,</cite> Section 1.13,
* "Serialization of Records"</a>.
*
* @apiNote
* A record class structure can be obtained at runtime via reflection.
* See {@link Class#isRecord()} and {@link Class#getRecordComponents()} for more details.
*
* @jls 8.10 Record Types
* @since 16
*/
public abstract class Record {
/**
* Constructor for record classes to call.
*/
protected Record() {}

/**
* Indicates whether some other object is "equal to" this one. In addition
* to the general contract of {@link Object#equals(Object) Object.equals},
* record classes must further obey the invariant that when
* a record instance is "copied" by passing the result of the record component
* accessor methods to the canonical constructor, as follows:
* <pre>
* R copy = new R(r.c1(), r.c2(), ..., r.cn());
* </pre>
* then it must be the case that {@code r.equals(copy)}.
*
* @implSpec
* The implicitly provided implementation returns {@code true} if
* and only if the argument is an instance of the same record class
* as this record, and each component of this record is equal to
* the corresponding component of the argument; otherwise, {@code
* false} is returned. Equality of a component {@code c} is
* determined as follows:
* <ul>
*
* <li> If the component is of a reference type, the component is
* considered equal if and only if {@link
* java.util.Objects#equals(Object,Object)
* Objects.equals(this.c, r.c} would return {@code true}.
*
* <li> If the component is of a primitive type, using the
* corresponding primitive wrapper class {@code PW} (the
* corresponding wrapper class for {@code int} is {@code
* java.lang.Integer}, and so on), the component is considered
* equal if and only if {@code
* PW.compare(this.c, r.c)} would return {@code 0}.
*
* </ul>
*
* Apart from the semantics described above, the precise algorithm
* used in the implicitly provided implementation is unspecified
* and is subject to change. The implementation may or may not use
* calls to the particular methods listed, and may or may not
* perform comparisons in the order of component declaration.
*
* @see java.util.Objects#equals(Object,Object)
*
* @param obj the reference object with which to compare.
* @return {@code true} if this record is equal to the
* argument; {@code false} otherwise.
*/
@Override
public abstract boolean equals(Object obj);

/**
* Returns a hash code value for the record.
* Obeys the general contract of {@link Object#hashCode Object.hashCode}.
* For records, hashing behavior is constrained by the refined contract
* of {@link Record#equals Record.equals}, so that any two records
* created from the same components must have the same hash code.
*
* @implSpec
* The implicitly provided implementation returns a hash code value derived
* by combining appropriate hashes from each component.
* The precise algorithm used in the implicitly provided implementation
* is unspecified and is subject to change within the above limits.
* The resulting integer need not remain consistent from one
* execution of an application to another execution of the same
* application, even if the hashes of the component values were to
* remain consistent in this way. Also, a component of primitive
* type may contribute its bits to the hash code differently than
* the {@code hashCode} of its primitive wrapper class.
*
* @see Object#hashCode()
*
* @return a hash code value for this record.
*/
@Override
public abstract int hashCode();

/**
* Returns a string representation of the record.
* In accordance with the general contract of {@link Object#toString()},
* the {@code toString} method returns a string that
* "textually represents" this record. The result should
* be a concise but informative representation that is easy for a
* person to read.
* <p>
* In addition to this general contract, record classes must further
* participate in the invariant that any two records which are
* {@linkplain Record#equals(Object) equal} must produce equal
* strings. This invariant is necessarily relaxed in the rare
* case where corresponding equal component values might fail
* to produce equal strings for themselves.
*
* @implSpec
* The implicitly provided implementation returns a string which
* contains the name of the record class, the names of components
* of the record, and string representations of component values,
* so as to fulfill the contract of this method.
* The precise format produced by this implicitly provided implementation
* is subject to change, so the present syntax should not be parsed
* by applications to recover record component values.
*
* @see Object#toString()
*
* @return a string representation of the object.
*/
@Override
public abstract String toString();
}
1 change: 1 addition & 0 deletions jre_emul/jre_sources.mk
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ JAVA_PUBLIC_SOURCES_CORE = \
java/lang/OutOfMemoryError.java \
java/lang/Package.java \
java/lang/Readable.java \
java/lang/Record.java \
java/lang/ReflectiveOperationException.java \
java/lang/Runnable.java \
java/lang/Runtime.java \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,18 @@

package com.google.devtools.j2objc.ast;

import java.util.ArrayList;
import java.util.List;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;

/**
* Node type for a record declaration.
*/
public class RecordDeclaration extends AbstractTypeDeclaration {

private List<RecordComponent> recordComponents = new ArrayList<>();

public RecordDeclaration() {}

public RecordDeclaration(RecordDeclaration other) {
Expand All @@ -41,6 +46,14 @@ public RecordDeclaration copy() {
return new RecordDeclaration(this);
}

public List<RecordComponent> getRecordComponents() {
return recordComponents;
}

public void addRecordComponent(VariableElement component, MethodDeclaration accessor) {
recordComponents.add(new RecordComponent(component, accessor));
}

@Override
protected void acceptInner(TreeVisitor visitor) {
if (visitor.visit(this)) {
Expand All @@ -52,4 +65,28 @@ protected void acceptInner(TreeVisitor visitor) {
}
visitor.endVisit(this);
}

/** A record component is an extended VariableElement which adds an accessor method. */
public static class RecordComponent {
private final VariableElement var;
private final MethodDeclaration accessor;

public RecordComponent(VariableElement component, MethodDeclaration accessor) {
this.var = component;
this.accessor = accessor;
}

public VariableElement getElement() {
return var;
}

public MethodDeclaration getAccessor() {
return accessor;
}

@Override
public String toString() {
return var.getSimpleName().toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@ public enum Kind {
SUPER_METHOD_REFERENCE,
SUPER_FIELD_ACCESS,
SWITCH_CASE,
SWITCH_EXPRESSION,
SWITCH_STATEMENT,
SYNCHRONIZED_STATEMENT,
TAG_ELEMENT,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.TypeTag;
import com.sun.tools.javac.code.Types;
Expand All @@ -190,8 +191,11 @@
import com.sun.tools.javac.tree.JCTree.Tag;
import com.sun.tools.javac.util.Position;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
Expand Down Expand Up @@ -258,8 +262,7 @@ private TreeConverter(CompilationUnitTree javacUnit, JavacEnvironment javacEnv)
sourcePositions = trees.getSourcePositions();
}

@Nullable
private TreeNode convert(Tree node, TreePath parent) {
private @Nullable TreeNode convert(Tree node, TreePath parent) {
if (node == null) {
return null;
}
Expand Down Expand Up @@ -853,7 +856,7 @@ private TreeNode convertFieldAccess(MemberSelectTree node, TreePath parent) {
.setQualifier((Name) convert(selected, path))
.setTypeMirror(typeMirror);
}
if ("super".equals(getMemberName(selected))) {
if (Objects.equals(getMemberName(selected), "super")) {
SuperFieldAccess newNode =
new SuperFieldAccess()
.setVariableElement((VariableElement) element)
Expand Down Expand Up @@ -1298,8 +1301,8 @@ private TreeNode convertPrimitiveType(PrimitiveTypeTree node, TreePath parent) {
return new PrimitiveType(getTypeMirror(getTreePath(parent, node)));
}

@SuppressWarnings("unchecked")
private TreeNode convertRecord(ClassTree node, TreePath parent) {
ErrorUtil.error("Record translation not implemented");
TreePath path = getTreePath(parent, node);
TypeElement element = (TypeElement) getElement(path);
RecordDeclaration newNode = new RecordDeclaration(element);
Expand All @@ -1314,9 +1317,37 @@ private TreeNode convertRecord(ClassTree node, TreePath parent) {
Block block = (Block) convert(javacBlock, path);
newNode.addBodyDeclaration(new Initializer(block, javacBlock.isStatic()));
} else {
newNode.addBodyDeclaration((BodyDeclaration) convert(bodyDecl, path));
BodyDeclaration member = (BodyDeclaration) convert(bodyDecl, path);
newNode.addBodyDeclaration(member);
}
}

// Use reflection to convert record components, so that the translator
// doesn't have to only run on Java 17 and higher. If running on an
// earlier version of Java, the record components field doesn't exist.
// So even though javac won't return a record in previous versions (so
// this method won't be called), compiling j2objc with an older JDK will
// fail if we directly reference javac's Java 17 API.
// TODO(tball): simplify when the minimum supported j2objc JDK is Java 17.
try {
Symbol.ClassSymbol recordClass = (Symbol.ClassSymbol) getElement(path);
Method getRecordComponentsMethod = recordClass.getClass().getMethod("getRecordComponents");
List<VariableElement> recordComponents =
(List<VariableElement>) getRecordComponentsMethod.invoke(recordClass);
for (VariableElement componentVar : recordComponents) {
Field accessorMethodField = componentVar.getClass().getField("accessorMeth");
MethodTree accessor = (MethodTree) accessorMethodField.get(componentVar);
if (accessor != null) {
newNode.addRecordComponent(
componentVar, (MethodDeclaration) convertMethodDeclaration(accessor, path));
} else {
newNode.addRecordComponent(componentVar, null);
}
}
} catch (ReflectiveOperationException e) {
throw new LinkageError("Failed accessing type's recordComponents.", e);
}

return newNode;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import com.google.devtools.j2objc.translate.OuterReferenceResolver;
import com.google.devtools.j2objc.translate.PackageInfoRewriter;
import com.google.devtools.j2objc.translate.PrivateDeclarationResolver;
import com.google.devtools.j2objc.translate.RecordExpander;
import com.google.devtools.j2objc.translate.ReflectionCodeDetector;
import com.google.devtools.j2objc.translate.Rewriter;
import com.google.devtools.j2objc.translate.SerializationStripper;
Expand Down Expand Up @@ -172,6 +173,9 @@ public static void applyMutations(
new OuterReferenceResolver(unit).run();
ticker.tick("OuterReferenceResolver");

new RecordExpander(unit).run();
ticker.tick("RecordExpander");

// Update code that has GWT references.
new GwtConverter(unit).run();
ticker.tick("GwtConverter");
Expand Down
Loading

0 comments on commit f9b88d1

Please sign in to comment.