diff --git a/make/CompileJavaModules.gmk b/make/CompileJavaModules.gmk index e1ab4f7a756..a9f32f1ef79 100644 --- a/make/CompileJavaModules.gmk +++ b/make/CompileJavaModules.gmk @@ -68,6 +68,20 @@ MODULESOURCEPATH := $(call GetModuleSrcPath) # Add imported modules to the modulepath MODULEPATH := $(call PathList, $(IMPORT_MODULES_CLASSES)) +################################################################################ +# Setup preprocessor flags +# The output directory must be present in GENERATED_PREVIEW_SUBDIRS in Modules.gmk. +# Temporarily restrict this to java.base, but it can be expanded later. + +ifeq ($(MODULE), java.base) + PREPROCESSOR_FLAGS := \ + -Xlint:-removal -Xlint:-processing \ + -Avalueclasses.outdir=$(SUPPORT_OUTPUTDIR)/gensrc-valueclasses \ + -processor build.tools.valueclasses.GenValueClasses + + PROCESSOR_PATH += $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes +endif + ################################################################################ # Copy zh_HK properties files from zh_TW (needed by some modules) @@ -120,9 +134,11 @@ $(eval $(call SetupJavaCompilation, $(MODULE), \ EXCLUDE_PATTERNS := -files, \ KEEP_ALL_TRANSLATIONS := $(KEEP_ALL_TRANSLATIONS), \ TARGET_RELEASE := $(TARGET_RELEASE), \ + PROCESSOR_PATH := $(PROCESSOR_PATH), \ JAVAC_FLAGS := \ $(DOCLINT) \ $(JAVAC_FLAGS) \ + $(PREPROCESSOR_FLAGS) \ --module-source-path $(MODULESOURCEPATH) \ --module-path $(MODULEPATH) \ --system none, \ @@ -131,26 +147,28 @@ $(eval $(call SetupJavaCompilation, $(MODULE), \ TARGETS += $($(MODULE)) ################################################################################ -# Setup compilation for value classes in the module +# Setup compilation for preview classes in the module # TBD: When $(DOCLINT) was included there was an NPE in JavacTypes.getOverriddenMethods -# Directory and file name suffix for jar file containing value classes -VALUECLASSES_STR := valueclasses +# Directory and file name suffix for jar file containing preview classes/resources. +PREVIEW_CLASSES_LABEL := preview +# Module relative path in which preview classes/resources are placed. +PREVIEW_PATH := META-INF/preview ifneq ($(COMPILER), bootjdk) - MODULE_VALUECLASS_SRC_DIRS := $(call FindModuleValueClassSrcDirs, $(MODULE)) - MODULE_VALUECLASS_SOURCEPATH := $(call GetModuleValueClassSrcPath) - ifneq ($(MODULE_VALUECLASS_SRC_DIRS),) - # Temporarily compile valueclasses into a separate directory, and then copy - # into the correct "META-INF/preview" path location. + MODULE_PREVIEW_SRC_DIRS := $(call FindModulePreviewSrcDirs, $(MODULE)) + MODULE_PREVIEW_SOURCEPATH := $(call GetModulePreviewSrcPath) + ifneq ($(MODULE_PREVIEW_SRC_DIRS),) + # Temporarily compile preview classes into a separate directory, and then + # copy into the correct output path location. # We cannot compile directly into the desired directory because it's the # compiler which creates the original '//...' hierarchy. - TEMP_OUTPUTDIR := $(SUPPORT_OUTPUTDIR)/$(VALUECLASSES_STR) + TEMP_OUTPUTDIR := $(SUPPORT_OUTPUTDIR)/$(PREVIEW_CLASSES_LABEL) - $(eval $(call SetupJavaCompilation, $(MODULE)-$(VALUECLASSES_STR), \ + $(eval $(call SetupJavaCompilation, $(MODULE)-$(PREVIEW_CLASSES_LABEL), \ SMALL_JAVA := false, \ MODULE := $(MODULE), \ - SRC := $(wildcard $(MODULE_VALUECLASS_SRC_DIRS)), \ + SRC := $(wildcard $(MODULE_PREVIEW_SRC_DIRS)), \ INCLUDES := $(JDK_USER_DEFINED_FILTER), \ FAIL_NO_SRC := $(FAIL_NO_SRC), \ BIN := $(TEMP_OUTPUTDIR)/, \ @@ -162,30 +180,30 @@ ifneq ($(COMPILER), bootjdk) DEPENDS := $($(MODULE)), \ JAVAC_FLAGS := \ $(JAVAC_FLAGS) \ - --module-source-path $(MODULE_VALUECLASS_SOURCEPATH) \ + --module-source-path $(MODULE_PREVIEW_SOURCEPATH) \ --module-path $(JDK_OUTPUTDIR)/modules \ --system none \ --enable-preview -source $(JDK_SOURCE_TARGET_VERSION), \ )) - # Don't add '$($(MODULE)-$(VALUECLASSES_STR))' to TARGETS (it's transient). - # The 'valueclasses' target below depends on it, and that's the non-transient + # Don't add '$($(MODULE)-$(PREVIEW_CLASSES_LABEL))' to TARGETS (it's transient). + # The 'preview' target below depends on it, and that's the non-transient # result we care about. # Copy compiled output from "$TEMP_OUTPUTDIR/$MODULE//..." - # to "$COMPILATION_OUTPUTDIR/$MODULE/META-INF/preview//...". + # to "$COMPILATION_OUTPUTDIR/$MODULE/$PREVIEW_PATH//...". MOD_SRC := $(TEMP_OUTPUTDIR)/$(MODULE) MOD_DST := $(COMPILATION_OUTPUTDIR)/$(MODULE) # NOTE: We cannot use '$(CP) -R $(MOD_SRC)/*/ ...' to select sub-directories (it # does not work on MacOS/BSD). Use 'filter-out' to explicitly exclude marker files. - $(MOD_DST)/_the.$(MODULE).valueclasses: $($(MODULE)-$(VALUECLASSES_STR)) - $(RM) -r $(@D)/META-INF/preview - $(MKDIR) -p $(@D)/META-INF/preview - $(CP) -R $(filter-out $(MOD_SRC)/_%, $(wildcard $(MOD_SRC)/*)) $(@D)/META-INF/preview + $(MOD_DST)/_the.$(MODULE).preview: $($(MODULE)-$(PREVIEW_CLASSES_LABEL)) + $(RM) -r $(@D)/$(PREVIEW_PATH) + $(MKDIR) -p $(@D)/$(PREVIEW_PATH) + $(CP) -R $(filter-out $(MOD_SRC)/_%, $(wildcard $(MOD_SRC)/*)) $(@D)/$(PREVIEW_PATH) $(TOUCH) $@ - TARGETS += $(MOD_DST)/_the.$(MODULE).valueclasses + TARGETS += $(MOD_DST)/_the.$(MODULE).preview endif endif diff --git a/make/common/JavaCompilation.gmk b/make/common/JavaCompilation.gmk index 33f5d10535a..bde59a2904a 100644 --- a/make/common/JavaCompilation.gmk +++ b/make/common/JavaCompilation.gmk @@ -302,13 +302,18 @@ define SetupJavaCompilationBody # including the compilation output on the classpath, so that incremental # compilations in unnamed module can refer to other classes from the same # source root, which are not being recompiled in this compilation: - $1_AUGMENTED_CLASSPATH += $$(BUILDTOOLS_OUTPUTDIR)/depend $$($1_BIN) + $1_AUGMENTED_CLASSPATH += $$($1_BIN) + $1_PROCESSOR_PATH += $$(BUILDTOOLS_OUTPUTDIR)/depend endif ifneq ($$($1_AUGMENTED_CLASSPATH), ) $1_FLAGS += -cp $$(call PathList, $$($1_AUGMENTED_CLASSPATH)) endif + ifneq ($$($1_PROCESSOR_PATH), ) + $1_FLAGS += --processor-path $$(call PathList, $$($1_PROCESSOR_PATH)) + endif + # Make sure the dirs exist, or that one of the EXTRA_FILES, that may not # exist yet, is in it. $$(foreach d, $$($1_SRC), \ diff --git a/make/common/MakeBase.gmk b/make/common/MakeBase.gmk index 97ef88932cb..0785f837db3 100644 --- a/make/common/MakeBase.gmk +++ b/make/common/MakeBase.gmk @@ -197,7 +197,7 @@ endef # and outputs the 'fixed' paths into the file in $2. If the file in $2 already exists # it is overwritten. # On non-Windows platforms this instead does a copy, so that $2 can still be used -# as a depenendency of a make rule, instead of having to conditionally depend on +# as a dependency of a make rule, instead of having to conditionally depend on # $1 instead, based on the target platform. ifeq ($(call isTargetOs, windows), true) FixPath = \ diff --git a/make/common/Modules.gmk b/make/common/Modules.gmk index 2566f09889f..24685d5b18e 100644 --- a/make/common/Modules.gmk +++ b/make/common/Modules.gmk @@ -75,7 +75,8 @@ GENERATED_SRC_DIRS += \ $(SUPPORT_OUTPUTDIR)/gensrc \ # -GENERATED_VALUE_CLASS_SUBDIRS += \ +# Directories in which generated preview classes may exists. +GENERATED_PREVIEW_SUBDIRS += \ $(SUPPORT_OUTPUTDIR)/gensrc-valueclasses \ # @@ -148,11 +149,12 @@ FindModuleSrcDirs = \ $(addsuffix /$(strip $1), $(GENERATED_SRC_DIRS) $(IMPORT_MODULES_SRC)) \ $(foreach sub, $(SRC_SUBDIRS), $(addsuffix /$(strip $1)/$(sub), $(TOP_SRC_DIRS))))) -# Find value class source dirs for a particular module (only generated) +# Find preview class source dirs for a particular module. +# Currently this is restricted to generated value classes, but can be extended. # $1 - Module to find source dirs for -FindModuleValueClassSrcDirs = \ +FindModulePreviewSrcDirs = \ $(strip $(wildcard \ - $(addsuffix /$(strip $1), $(GENERATED_VALUE_CLASS_SUBDIRS)))) + $(addsuffix /$(strip $1), $(GENERATED_PREVIEW_SUBDIRS)))) # Find all specs dirs for a particular module # $1 - Module to find specs dirs for @@ -176,10 +178,11 @@ GetModuleSrcPath = \ $(addsuffix /*, $(GENERATED_SRC_DIRS) $(IMPORT_MODULES_SRC)) \ $(foreach sub, $(SRC_SUBDIRS), $(addsuffix /*/$(sub), $(TOP_SRC_DIRS)))) -# Construct the complete module source path for value classes -GetModuleValueClassSrcPath = \ +# Construct the complete module source path for preview classes. +# Currently this is restricted to generated value classes, but can be extended. +GetModulePreviewSrcPath = \ $(call PathList, \ - $(addsuffix /*, $(GENERATED_VALUE_CLASS_SUBDIRS) $(GENERATED_SRC_DIRS) $(IMPORT_MODULES_SRC)) \ + $(addsuffix /*, $(GENERATED_PREVIEW_SUBDIRS) $(GENERATED_SRC_DIRS) $(IMPORT_MODULES_SRC)) \ $(foreach sub, $(SRC_SUBDIRS), $(addsuffix /*/$(sub), $(TOP_SRC_DIRS)))) ################################################################################ diff --git a/make/jdk/src/classes/build/tools/valueclasses/GenValueClasses.java b/make/jdk/src/classes/build/tools/valueclasses/GenValueClasses.java new file mode 100644 index 00000000000..1be9d12f5e5 --- /dev/null +++ b/make/jdk/src/classes/build/tools/valueclasses/GenValueClasses.java @@ -0,0 +1,250 @@ +package build.tools.valueclasses; + +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.CompilationUnitTree; +import com.sun.source.util.TreePath; +import com.sun.source.util.Trees; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedOptions; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.tools.FileObject; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import static java.nio.file.StandardOpenOption.CREATE_NEW; +import static java.util.stream.Collectors.groupingBy; + +/** + * Annotation processor for generating preview sources of existing classes which + * annotated as value classes for Valhalla. + * + *

Classes seen by this processor (annotated with {@code @MigratedValueClass} + * will have their source files re-written into the configured output directory + * for compilation as preview classes. Note that more than one class in a given + * source file may be annotated. + * + *

Class re-writing is simply a matter of injecting the new "value" keyword + * into a copy of the original source file after existing modifiers for all + * annotated elements. + * + *

Note that there are two annotations in use for value classes, but since + * we must generate sources for abstract classes, we only care about one of them. + *

    + *
  • {@code @jdk.internal.ValueBased} appears on concrete value classes. + *
  • {@code @jdk.internal.MigratedValueClass} appears on concrete and + * abstract value classes. + *
+ */ +@SupportedAnnotationTypes({"jdk.internal.MigratedValueClass"}) +@SupportedOptions("valueclasses.outdir") +public final class GenValueClasses extends AbstractProcessor { + // Matches preprocessor option flag in CompileJavaModules.gmk. + private static final String OUTDIR_OPTION_KEY = "valueclasses.outdir"; + + private ProcessingEnvironment processingEnv = null; + private Path outDir = null; + private Trees trees = null; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + this.processingEnv = processingEnv; + String outDir = this.processingEnv.getOptions().get(OUTDIR_OPTION_KEY); + if (outDir == null) { + throw new IllegalStateException( + "Must specify -A" + OUTDIR_OPTION_KEY + "=" + + " for annotation processor: " + GenValueClasses.class.getName()); + } + this.outDir = Path.of(outDir.replace('/', File.separatorChar)); + this.trees = Trees.instance(this.processingEnv); + } + + /** + * Override to return latest version, since the runtime in which this is + * compiled doesn't know about development source versions. + */ + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment env) { + // We don't have direct access to MigratedValueClass classes here. + Optional valueClassAnnotation = + getAnnotation(annotations, "jdk.internal.MigratedValueClass"); + if (valueClassAnnotation.isPresent()) { + Set allValueClasses = getAnnotatedTypes(env, valueClassAnnotation.get()); + Map> moduleToType = + allValueClasses.stream().collect(groupingBy(this::getModuleName)); + for (String modName : moduleToType.keySet()) { + moduleToType.get(modName).stream() + .collect(groupingBy(this::getJavaSourceFile)) + .forEach(this::generateValueClassSource); + } + } + return true; + } + + private static Optional getAnnotation(Set annotations, String name) { + return annotations.stream() + .filter(e -> e.getQualifiedName().toString().equals(name)) + .findFirst(); + } + + private static Set getAnnotatedTypes(RoundEnvironment env, TypeElement annotation) { + Set types = new HashSet<>(); + for (Element e : env.getElementsAnnotatedWith(annotation)) { + if (!e.getKind().isClass()) { + throw new IllegalStateException( + "Unexpected element kind (" + e.getKind() + ") for element: " + e); + } + TypeElement type = (TypeElement) e; + if (type.getQualifiedName().isEmpty()) { + throw new IllegalStateException( + "Unexpected empty name for element: " + e); + } + types.add(type); + } + return types; + } + + private void generateValueClassSource(Path srcPath, List classes) { + try { + // We know there's at least one element per source file (by construction). + TypeElement element = classes.getFirst(); + Path relPath = getModuleRelativePath(srcPath, getPackageName(element)); + Path outPath = outDir.resolve(getModuleName(element)).resolve(relPath); + Files.createDirectories(outPath.getParent()); + + List insertPositions = + classes.stream().map(this::getValueKeywordInsertPosition).sorted().toList(); + + try (Reader reader = new InputStreamReader(Files.newInputStream(srcPath)); + Writer output = new OutputStreamWriter(Files.newOutputStream(outPath, CREATE_NEW))) { + long curPos = 0; + for (long nxtPos : insertPositions) { + int nextChunkLen = Math.toIntExact(nxtPos - curPos); + long written = new LimitedReader(reader, nextChunkLen).transferTo(output); + if (written != nextChunkLen) { + throw new IOException("Unexpected number of characters transferred." + + " Expected " + nextChunkLen + " but was " + written); + } + // Position is the end of modifier chars, so we need a leading space. + // pos ------v + // [modifiers] class... -->> [modifiers] value class... + output.write(" value"); + curPos = nxtPos; + } + reader.transferTo(output); + } + } catch (IOException e) { + // TODO: Decide about errors! + throw new RuntimeException(e); + } + } + + private long getValueKeywordInsertPosition(TypeElement classElement) { + TreePath classDecl = trees.getPath(classElement); + ClassTree classTree = (ClassTree) classDecl.getLeaf(); + CompilationUnitTree compilationUnit = classDecl.getCompilationUnit(); + // Since annotations are held as "modifiers", and since we only process + // elements with annotations, the positions for modifiers must be + // well-defined (otherwise we'd get -1 here). + return trees.getSourcePositions().getEndPosition(compilationUnit, classTree.getModifiers()); + } + + private Path getModuleRelativePath(Path srcPath, String pkgName) { + Path relPath = Path.of(pkgName.replace('.', File.separatorChar)).resolve(srcPath.getFileName()); + if (!srcPath.endsWith(relPath)) { + throw new IllegalStateException(String.format( + "Expected trailing path %s for source file %s", relPath, srcPath)); + } + return relPath; + } + + private String getModuleName(TypeElement t) { + return processingEnv.getElementUtils().getModuleOf(t).getQualifiedName().toString(); + } + + private String getPackageName(TypeElement t) { + return processingEnv.getElementUtils().getPackageOf(t).getQualifiedName().toString(); + } + + private Path getJavaSourceFile(TypeElement type) { + return getFilePath(processingEnv.getElementUtils().getFileObjectOf(type)); + } + + private static Path getFilePath(FileObject file) { + return Path.of(file.toUri()); + } + + /** + * A forwarding reader which guarantees to read no more than + * {@code maxCharCount} characters from the underlying stream. + */ + private static final class LimitedReader extends Reader { + // Since these are expected to be short-lived, no need + // to null the delegate when we're closed. + private final Reader delegate; + // This should never go negative. + private int remaining; + + /** + * Creates a limited reader which reads up to {@code maxCharCount} chars + * from the given stream. + * + * @param delegate underlying reader + * @param maxCharCount maximum chars to read (can be 0) + */ + LimitedReader(Reader delegate, int maxCharCount) { + this.delegate = Objects.requireNonNull(delegate); + this.remaining = Math.max(maxCharCount, 0); + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + if (remaining == 0) { + return -1; + } + if (remaining > 0) { + int readLimit = Math.min(remaining, len); + int count = delegate.read(cbuf, off, readLimit); + // Only update remaining if something was read. + if (count > 0) { + if (count > remaining) { + throw new IOException( + "Underlying Reader exceeded requested read limit." + + " Expected at most " + readLimit + " but read " + count); + } + remaining -= count; + } + return count; + } + throw new IllegalStateException("Remaining count should never be negative!"); + } + + @Override + public void close() { + // Do not close the delegate since this is conceptually just a view. + } + } +} diff --git a/make/modules/java.base/Gensrc.gmk b/make/modules/java.base/Gensrc.gmk index 0406c5b7033..e8236f0b0e4 100644 --- a/make/modules/java.base/Gensrc.gmk +++ b/make/modules/java.base/Gensrc.gmk @@ -37,7 +37,6 @@ include gensrc/GensrcMisc.gmk include gensrc/GensrcModuleLoaderMap.gmk include gensrc/GensrcRegex.gmk include gensrc/GensrcScopedMemoryAccess.gmk -include gensrc/GensrcValueClasses.gmk include gensrc/GensrcVarHandles.gmk ################################################################################ diff --git a/make/modules/java.base/gensrc/GensrcValueClasses.gmk b/make/modules/java.base/gensrc/GensrcValueClasses.gmk deleted file mode 100644 index 6a5b6864b67..00000000000 --- a/make/modules/java.base/gensrc/GensrcValueClasses.gmk +++ /dev/null @@ -1,76 +0,0 @@ -# -# Copyright (c) 2023, 2024, 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. -# - -################################################################################ -# Generate the value class replacements for selected java.base source files - -java.base-VALUE_CLASS-REPLACEMENTS := \ - java/lang/Byte.java \ - java/lang/Short.java \ - java/lang/Integer.java \ - java/lang/Long.java \ - java/lang/Float.java \ - java/lang/Double.java \ - java/lang/Boolean.java \ - java/lang/Character.java \ - java/lang/Number.java \ - java/lang/Record.java \ - java/util/Optional.java \ - java/util/OptionalInt.java \ - java/util/OptionalLong.java \ - java/util/OptionalDouble.java \ - java/time/LocalDate.java \ - java/time/LocalDateTime.java \ - java/time/LocalTime.java \ - java/time/Duration.java \ - java/time/Instant.java \ - java/time/MonthDay.java \ - java/time/ZonedDateTime.java \ - java/time/OffsetDateTime.java \ - java/time/OffsetTime.java \ - java/time/YearMonth.java \ - java/time/Year.java \ - java/time/Period.java \ - java/time/chrono/ChronoLocalDateImpl.java \ - java/time/chrono/MinguoDate.java \ - java/time/chrono/HijrahDate.java \ - java/time/chrono/JapaneseDate.java \ - java/time/chrono/ThaiBuddhistDate.java \ - # - -java.base-VALUE-CLASS-FILES := \ - $(foreach f, $(java.base-VALUE_CLASS-REPLACEMENTS), $(addprefix $(TOPDIR)/src/java.base/share/classes/, $(f))) - -$(eval $(call SetupTextFileProcessing, JAVA_BASE_VALUECLASS_REPLACEMENTS, \ - SOURCE_FILES := $(java.base-VALUE-CLASS-FILES), \ - SOURCE_BASE_DIR := $(TOPDIR)/src/java.base/share/classes, \ - OUTPUT_DIR := $(SUPPORT_OUTPUTDIR)/gensrc-valueclasses/java.base/, \ - REPLACEMENTS := \ - public final class => public final value class ; \ - public abstract class => public abstract value class ; \ - abstract class ChronoLocalDateImpl => abstract value class ChronoLocalDateImpl, \ -)) - -TARGETS += $(JAVA_BASE_VALUECLASS_REPLACEMENTS)