diff --git a/make/CompileJavaModules.gmk b/make/CompileJavaModules.gmk
index 1b6e95d1afb..b585fb8d5db 100644
--- a/make/CompileJavaModules.gmk
+++ b/make/CompileJavaModules.gmk
@@ -29,6 +29,7 @@ include MakeFileStart.gmk
include JavaCompilation.gmk
include Modules.gmk
+include ToolsJdk.gmk
include CopyFiles.gmk
@@ -68,6 +69,19 @@ 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 := \
+ -Xplugin:"GenValueClassPlugin $(SUPPORT_OUTPUTDIR)/gensrc-valueclasses"
+
+ PROCESSOR_PATH += $(VALUETYPE_GENSRC_PROCESSOR_PATH)
+ DEPENDS += $(BUILD_VALUETYPE_GENSRC)
+endif
+
################################################################################
# Copy zh_HK properties files from zh_TW (needed by some modules)
@@ -107,6 +121,7 @@ $(eval $(call SetupJavaCompilation, $(MODULE), \
MODULE := $(MODULE), \
SRC := $(wildcard $(MODULE_SRC_DIRS)), \
INCLUDES := $(JDK_USER_DEFINED_FILTER), \
+ DEPENDS := $(DEPENDS), \
FAIL_NO_SRC := $(FAIL_NO_SRC), \
BIN := $(COMPILATION_OUTPUTDIR), \
HEADERS := $(SUPPORT_OUTPUTDIR)/headers, \
@@ -120,9 +135,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, \
diff --git a/make/CompileToolsJdk.gmk b/make/CompileToolsJdk.gmk
index c291dbdba0a..62e59bee734 100644
--- a/make/CompileToolsJdk.gmk
+++ b/make/CompileToolsJdk.gmk
@@ -45,7 +45,8 @@ $(eval $(call SetupJavaCompilation, BUILD_TOOLS_JDK, \
build/tools/deps \
build/tools/docs \
build/tools/jigsaw \
- build/tools/depend, \
+ build/tools/depend \
+ build/tools/valhalla, \
BIN := $(BUILDTOOLS_OUTPUTDIR)/jdk_tools_classes, \
DISABLED_WARNINGS := dangling-doc-comments options, \
JAVAC_FLAGS := \
@@ -155,4 +156,26 @@ endif
################################################################################
+$(eval $(call SetupJavaCompilation, BUILD_VALUETYPE_GENSRC, \
+ TARGET_RELEASE := $(TARGET_RELEASE_BOOTJDK), \
+ SRC := $(TOPDIR)/make/jdk/src/classes, \
+ INCLUDES := build/tools/valhalla/valuetypes, \
+ BIN := $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc, \
+ DISABLED_WARNINGS := options, \
+ JAVAC_FLAGS := \
+ --add-exports jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED \
+ --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \
+ --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED \
+))
+
+VALUETYPE_GENSRC_SERVICE_PROVIDER := $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc/META-INF/services/com.sun.source.util.Plugin
+
+$(VALUETYPE_GENSRC_SERVICE_PROVIDER):
+ $(call MakeDir, $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc/META-INF/services)
+ $(ECHO) build.tools.valhalla.valuetypes.GenValueClassPlugin > $@
+
+TARGETS += $(BUILD_VALUETYPE_GENSRC) $(VALUETYPE_GENSRC_SERVICE_PROVIDER)
+
+################################################################################
+
include MakeFileEnd.gmk
diff --git a/make/ToolsJdk.gmk b/make/ToolsJdk.gmk
index b04d7820c91..07c066d561f 100644
--- a/make/ToolsJdk.gmk
+++ b/make/ToolsJdk.gmk
@@ -142,5 +142,16 @@ PANDOC_HTML_MANPAGE_FILTER := $(BUILDTOOLS_OUTPUTDIR)/manpages/pandoc-html-manpa
################################################################################
+# Annotation processor for generating preview sources of annotated value classes.
+
+VALUETYPE_GENSRC_PROCESSOR_NAME := build.tools.valhalla.valuetypes.GenValueClasses
+VALUETYPE_GENSRC_PROCESSOR_PATH := $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc
+
+# Same trick as BUILD_TOOLS_JDK but for the annotation processor.
+BUILD_VALUETYPE_GENSRC := $(call SetupJavaCompilationCompileTarget, \
+ BUILD_VALUETYPE_GENSRC, $(BUILDTOOLS_OUTPUTDIR)/valuetype_gensrc)
+
+################################################################################
+
endif # include guard
include MakeIncludeEnd.gmk
diff --git a/make/common/JavaCompilation.gmk b/make/common/JavaCompilation.gmk
index 33f5d10535a..f8e4434cbcb 100644
--- a/make/common/JavaCompilation.gmk
+++ b/make/common/JavaCompilation.gmk
@@ -161,7 +161,10 @@ endef
# EXTRA_FILES List of extra source files to include in compilation. Can be used to
# specify files that need to be generated by other rules first.
# HEADERS path to directory where all generated c-headers are written.
-# DEPENDS Extra dependency
+# DEPENDS Extra dependencies
+# PROCESSOR_PATH Annotation processor and plugin path (see --processor-path).
+# Needed when specifying annotation processors not found via serivce discovery,
+# and required for plugins when they are used alongside annotation processors.
# KEEP_DUPS Do not remove duplicate file names from different source roots.
# FAIL_NO_SRC Set to false to not fail the build if no source files are found,
# default is true.
@@ -288,6 +291,7 @@ define SetupJavaCompilationBody
endif
$1_AUGMENTED_CLASSPATH := $$($1_CLASSPATH)
+ $1_AUGMENTED_PROCESSOR_PATH := $$($1_PROCESSOR_PATH)
$1_API_TARGET := $$($1_BIN)$$($1_MODULE_SUBDIR)/_the.$1_pubapi
$1_API_INTERNAL := $$($1_BIN)$$($1_MODULE_SUBDIR)/_the.$1_internalapi
@@ -302,13 +306,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_AUGMENTED_PROCESSOR_PATH += $$(BUILDTOOLS_OUTPUTDIR)/depend
endif
ifneq ($$($1_AUGMENTED_CLASSPATH), )
$1_FLAGS += -cp $$(call PathList, $$($1_AUGMENTED_CLASSPATH))
endif
+ ifneq ($$($1_AUGMENTED_PROCESSOR_PATH), )
+ $1_FLAGS += --processor-path $$(call PathList, $$($1_AUGMENTED_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/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClassPlugin.java b/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClassPlugin.java
new file mode 100644
index 00000000000..7346dbbc794
--- /dev/null
+++ b/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClassPlugin.java
@@ -0,0 +1,225 @@
+package build.tools.valhalla.valuetypes;
+
+import com.sun.source.tree.CompilationUnitTree;
+import com.sun.source.util.JavacTask;
+import com.sun.source.util.Plugin;
+import com.sun.source.util.TaskEvent;
+import com.sun.source.util.TaskListener;
+import com.sun.tools.javac.tree.JCTree;
+import com.sun.tools.javac.tree.JCTree.JCClassDecl;
+import com.sun.tools.javac.tree.TreeInfo;
+import com.sun.tools.javac.tree.TreeScanner;
+
+import javax.tools.Diagnostic;
+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.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+
+/**
+ * Plugin for generating preview sources of classes annotated as value classes
+ * for preview mode.
+ *
+ *
Classes seen by this plugin (annotated with {@code @MigratedValueClass}
+ * will have their source files re-written into the specified 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 achieved by injecting the "value" keyword in front of
+ * class declarations for all annotated elements in the original source file.
+ *
+ *
Note that there are two annotations in use for value classes, but since
+ * we must generate sources for abstract classes, we only process one of them.
+ *
+ * - {@code @jdk.internal.ValueBased} appears on concrete value classes.
+ *
- {@code @jdk.internal.MigratedValueClass} appears on concrete and
+ * abstract value classes.
+ *
+ */
+public final class GenValueClassPlugin implements Plugin {
+ private static final String VALUE_CLASS_ANNOTATION = "@jdk.internal.MigratedValueClass";
+
+ @Override
+ public String getName() {
+ return "GenValueClassPlugin";
+ }
+
+ @Override
+ public void init(JavacTask task, String... args) {
+ if (args.length == 0) {
+ throw new IllegalArgumentException("Plugin " + getName() + ": missing output directory argument");
+ }
+ Path outDir = Path.of(args[0]);
+ if (!Files.isDirectory(outDir)) {
+ throw new IllegalArgumentException("Plugin " + getName() + ": no such output directory: " + outDir);
+ }
+ task.addTaskListener(new ValueClassGenerator(outDir));
+ }
+
+ private record ValueClassGenerator(Path outDir) implements TaskListener {
+ @Override
+ public void finished(TaskEvent e) {
+ CompilationUnitTree compilation = e.getCompilationUnit();
+
+ List classes = new ArrayList<>();
+ new TreeScanner() {
+ @Override
+ public void visitClassDef(JCClassDecl cls) {
+ boolean hasAnnotation = cls.getModifiers().getAnnotations().stream()
+ .peek(a -> System.out.println("--> " + a))
+ .anyMatch(a -> a.toString().equals(VALUE_CLASS_ANNOTATION));
+ if (hasAnnotation) {
+ classes.add(cls);
+ }
+ super.visitClassDef(cls);
+ }
+ }.scan((JCTree) compilation);
+
+ if (!classes.isEmpty()) {
+ Path srcPath = filePath(compilation.getSourceFile());
+ String moduleName = compilation.getModule().getName().toString();
+ String packageName = compilation.getPackage().toString();
+ generateValueClassSource(srcPath, moduleName, packageName, classes);
+ }
+ }
+
+ /**
+ * Write a transformed version of the given Java source file with the
+ * {@code value} keyword inserted before the class declaration of each
+ * annotated type element.
+ */
+ private void generateValueClassSource(
+ Path srcPath, String moduleName, String packageName, List classes) {
+
+ System.out.println("Module: " + moduleName);
+ System.out.println("Package: " + packageName);
+ System.out.println("Classes: " + classes.stream().map(c -> c.type.toString()).toList());
+
+ Path relPath = moduleRelativePath(srcPath, packageName);
+ Path outPath = outDir.resolve(moduleName).resolve(relPath);
+
+ System.out.println("Out path: " + outPath);
+
+ try {
+ Files.createDirectories(outPath.getParent());
+
+ List insertPositions =
+ classes.stream().map(this::valueKeywordInsertPosition).sorted().toList();
+
+ System.out.println("Insert positions: " + insertPositions);
+
+ // For partial rebuilds, generated sources may still exist, so we overwrite them.
+ try (Reader reader = new InputStreamReader(Files.newInputStream(srcPath));
+ Writer output = new OutputStreamWriter(
+ Files.newOutputStream(outPath, CREATE, TRUNCATE_EXISTING))) {
+ int curPos = 0;
+ for (int nxtPos : insertPositions) {
+ int nextChunkLen = 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);
+ }
+ curPos = nxtPos;
+ // curPos is at the end of the modifier section, so add a leading space.
+ // curPos ---v
+ // [modifiers] class... -->> [modifiers] value class...
+ output.write(" value");
+ }
+ // Trailing section to end-of-file transferred from original reader.
+ reader.transferTo(output);
+ }
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private Path moduleRelativePath(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;
+ }
+
+ /**
+ * Returns the character offset in the original source file at which to insert
+ * the {@code value} keyword. The offset is the end of the modifiers section,
+ * which must immediately precede the class declaration.
+ */
+ private int valueKeywordInsertPosition(JCClassDecl classDecl) {
+ int pos = TreeInfo.getStartPos(classDecl.getModifiers());
+ if (pos == Diagnostic.NOPOS) {
+ throw new IllegalStateException("Missing position information: " + classDecl);
+ }
+ return pos;
+ }
+ }
+
+ private static Path filePath(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 {
+ // These are short-lived, no need to null the delegate when closed.
+ private final Reader delegate;
+ // This should never go negative.
+ private int remainingChars;
+
+ /**
+ * 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.remainingChars = Math.max(maxCharCount, 0);
+ }
+
+ @Override
+ public int read(char[] cbuf, int off, int len) throws IOException {
+ if (remainingChars > 0) {
+ int readLimit = Math.min(remainingChars, len);
+ int count = delegate.read(cbuf, off, readLimit);
+ // Only update remainingChars if something was read.
+ if (count > 0) {
+ if (count > remainingChars) {
+ throw new IOException(
+ "Underlying Reader exceeded requested read limit." +
+ " Expected at most " + readLimit + " but read " + count);
+ }
+ remainingChars -= count;
+ }
+ // Can return 0 or -1 here (the underlying reader could finish first).
+ return count;
+ } else if (remainingChars == 0) {
+ return -1;
+ } else {
+ throw new AssertionError("Remaining character 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/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClasses.java b/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClasses.java
new file mode 100644
index 00000000000..a00857bb2af
--- /dev/null
+++ b/make/jdk/src/classes/build/tools/valhalla/valuetypes/GenValueClasses.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (c) 2026, 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 build.tools.valhalla.valuetypes;
+
+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.Diagnostic;
+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.Objects;
+import java.util.Optional;
+import java.util.Set;
+
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.util.stream.Collectors.groupingBy;
+
+/**
+ * Annotation processor for generating preview sources of classes annotated as
+ * value classes for preview mode.
+ *
+ * Classes seen by this processor (annotated with {@code @MigratedValueClass}
+ * will have their source files re-written into the specified 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 achieved by injecting the "value" keyword in front of
+ * class declarations for all annotated elements in the original source file.
+ *
+ *
Note that there are two annotations in use for value classes, but since
+ * we must generate sources for abstract classes, we only process one of them.
+ *
+ * - {@code @jdk.internal.ValueBased} appears on concrete value classes.
+ *
- {@code @jdk.internal.MigratedValueClass} appears on concrete and
+ * abstract value classes.
+ *
+ */
+@SupportedAnnotationTypes(GenValueClasses.MIGRATED_VALUE_CLASS_ANNOTATION)
+@SupportedOptions("valueclasses.outdir")
+public final class GenValueClasses extends AbstractProcessor {
+ static final String MIGRATED_VALUE_CLASS_ANNOTATION = "jdk.internal.MigratedValueClass";
+
+ // 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) {
+ // No need to convert '/' for Windows in build tools.
+ this.outDir = Path.of(outDir);
+ this.trees = Trees.instance(this.processingEnv);
+ } else {
+ processingEnv.getMessager().printError(
+ "Must specify -A" + OUTDIR_OPTION_KEY + "=");
+ }
+ }
+
+ /**
+ * 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 extends TypeElement> annotations, RoundEnvironment env) {
+ // Don't do anything if there was an error in init().
+ if (outDir != null) {
+ // We don't have direct access to MigratedValueClass classes here.
+ Optional extends TypeElement> valueClassAnnotation =
+ getAnnotation(annotations, MIGRATED_VALUE_CLASS_ANNOTATION);
+ if (valueClassAnnotation.isPresent()) {
+ getAnnotatedTypes(env, valueClassAnnotation.get()).stream()
+ .collect(groupingBy(this::javaSourceFile))
+ .forEach(this::generateValueClassSource);
+ }
+ }
+ // We may not be the only annotation processor to consume this annotation.
+ return false;
+ }
+
+ /** Find the annotation element by name in the given set. */
+ private static Optional extends TypeElement> getAnnotation(
+ Set extends TypeElement> annotations, String name) {
+ return annotations.stream()
+ .filter(e -> e.getQualifiedName().toString().equals(name))
+ .findFirst();
+ }
+
+ /** Find the type elements (classes) annotated with the given annotation element. */
+ private Set getAnnotatedTypes(RoundEnvironment env, TypeElement annotation) {
+ Set types = new HashSet<>();
+ for (Element e : env.getElementsAnnotatedWith(annotation)) {
+ if (!e.getKind().isClass()) {
+ processingEnv.getMessager().printError("Unexpected element kind (" + e.getKind() + ")", e);
+ continue;
+ }
+ TypeElement type = (TypeElement) e;
+ if (type.getQualifiedName().isEmpty()) {
+ processingEnv.getMessager().printError("Unexpected empty name", e);
+ continue;
+ }
+ types.add(type);
+ }
+ return types;
+ }
+
+ /**
+ * Write a transformed version of the given Java source file with the
+ * {@code value} keyword inserted before the class declaration of each
+ * annotated type element.
+ */
+ private void generateValueClassSource(Path srcPath, List classes) {
+ // We know there's at least one element per source file (by construction).
+ TypeElement classElement = classes.getFirst();
+ Path relPath = moduleRelativePath(srcPath, packageName(classElement));
+ Path outPath = outDir.resolve(moduleName(classElement)).resolve(relPath);
+ try {
+ Files.createDirectories(outPath.getParent());
+
+ List insertPositions =
+ classes.stream().map(this::valueKeywordInsertPosition).sorted().toList();
+
+ // For partial rebuilds, generated sources may still exist, so we overwrite them.
+ try (Reader reader = new InputStreamReader(Files.newInputStream(srcPath));
+ Writer output = new OutputStreamWriter(
+ Files.newOutputStream(outPath, CREATE, TRUNCATE_EXISTING))) {
+ 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);
+ }
+ curPos = nxtPos;
+ // curPos is at the end of the modifier section, so add a leading space.
+ // curPos ---v
+ // [modifiers] class... -->> [modifiers] value class...
+ output.write(" value");
+ }
+ // Trailing section to end-of-file transferred from original reader.
+ reader.transferTo(output);
+ }
+ } catch (IOException e) {
+ processingEnv.getMessager().printError("Failed to write value class source: " + outPath);
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Returns the character offset in the original source file at which to insert
+ * the {@code value} keyword. The offset is the end of the modifiers section,
+ * which must immediately precede the class declaration.
+ */
+ private long valueKeywordInsertPosition(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 of the modifiers section must
+ // be well-defined.
+ long pos = trees.getSourcePositions().getEndPosition(compilationUnit, classTree.getModifiers());
+ if (pos == Diagnostic.NOPOS) {
+ throw new IllegalStateException("Missing position information: " + classElement);
+ }
+ return pos;
+ }
+
+ private Path moduleRelativePath(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 moduleName(TypeElement type) {
+ return processingEnv.getElementUtils().getModuleOf(type).getQualifiedName().toString();
+ }
+
+ private String packageName(TypeElement type) {
+ return processingEnv.getElementUtils().getPackageOf(type).getQualifiedName().toString();
+ }
+
+ private Path javaSourceFile(TypeElement type) {
+ return filePath(processingEnv.getElementUtils().getFileObjectOf(type));
+ }
+
+ private static Path filePath(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 {
+ // These are short-lived, no need to null the delegate when closed.
+ private final Reader delegate;
+ // This should never go negative.
+ private int remainingChars;
+
+ /**
+ * 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.remainingChars = Math.max(maxCharCount, 0);
+ }
+
+ @Override
+ public int read(char[] cbuf, int off, int len) throws IOException {
+ if (remainingChars > 0) {
+ int readLimit = Math.min(remainingChars, len);
+ int count = delegate.read(cbuf, off, readLimit);
+ // Only update remainingChars if something was read.
+ if (count > 0) {
+ if (count > remainingChars) {
+ throw new IOException(
+ "Underlying Reader exceeded requested read limit." +
+ " Expected at most " + readLimit + " but read " + count);
+ }
+ remainingChars -= count;
+ }
+ // Can return 0 or -1 here (the underlying reader could finish first).
+ return count;
+ } else if (remainingChars == 0) {
+ return -1;
+ } else {
+ throw new AssertionError("Remaining character 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)
diff --git a/test/jdk/valhalla/valuetypes/GenValueClassesTest.java b/test/jdk/valhalla/valuetypes/GenValueClassesTest.java
new file mode 100644
index 00000000000..4e098b15391
--- /dev/null
+++ b/test/jdk/valhalla/valuetypes/GenValueClassesTest.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (c) 2026, 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.
+ */
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Assumptions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.spi.ToolProvider;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/*
+ * @test
+ * @summary Tests build tool GenValueClasses annotation processor
+ * @library /tools/lib
+ * @run junit GenValueClassesTest
+ */
+public class GenValueClassesTest {
+ private static final ToolProvider JAVAC_TOOL = ToolProvider.findFirst("javac")
+ .orElseThrow(() -> new RuntimeException("javac tool not found"));
+
+ // We cannot access compiled build tools in JTREG tests, but we can find the
+ // sources and compile them. See findBuildToolsSrcRoot() for more details.
+ private static final Path SRC_ROOT = Path.of("make", "jdk", "src", "classes");
+ private static final Path PROCESSOR_SRC = Path.of(
+ "build", "tools", "valhalla", "valuetypes", "GenValueClasses.java");
+
+ // Compile the annotation processor once for all test cases.
+ @TempDir
+ private static Path processorDir = null;
+
+ @BeforeAll
+ static void compileAnnotationProcessor() {
+ compile(findBuildToolsSrcRoot().resolve(PROCESSOR_SRC), processorDir);
+ }
+
+ @TempDir
+ Path testDir = null;
+
+ @Test
+ public void simpleValueClass() throws IOException {
+ String simpleValueClass =
+ """
+ package test;
+
+ @jdk.internal.MigratedValueClass
+ public /*VALUE*/ class SimpleValueClass {
+ }
+ """;
+ Path relPath = writeTestSource("SimpleValueClass", simpleValueClass);
+ compileTestClass(relPath);
+ String transformedSrc = readTransformedSource(relPath);
+ assertEquals(simpleValueClass.replace("/*VALUE*/", "value"), transformedSrc);
+ assertTrue(transformedSrc.contains(" value class SimpleValueClass "));
+ }
+
+ @Test
+ public void nestedValueClass() throws IOException {
+ String nestedValueClass =
+ """
+ package test;
+
+ public class NestedValueClass {
+ @jdk.internal.MigratedValueClass
+ private static final /*VALUE*/ class Nested {
+ }
+ }
+ """;
+ Path relPath = writeTestSource("NestedValueClass", nestedValueClass);
+ compileTestClass(relPath);
+ String transformedSrc = readTransformedSource(relPath);
+ assertEquals(nestedValueClass.replace("/*VALUE*/", "value"), transformedSrc);
+ assertTrue(transformedSrc.contains(" value class Nested "));
+ }
+
+ @Test
+ public void multipleValueClasses() throws IOException {
+ String multipleValueClasses =
+ """
+ package test;
+
+ @jdk.internal.MigratedValueClass
+ public /*VALUE*/ class MultipleValueClasses {
+
+ @jdk.internal.MigratedValueClass
+ private static final /*VALUE*/ class First { }
+
+ static final class Second { }
+
+ @jdk.internal.MigratedValueClass
+ private static /*VALUE*/ class Third { }
+ }
+ """;
+
+ Path relPath = writeTestSource("MultipleValueClasses", multipleValueClasses);
+ compileTestClass(relPath);
+ String transformedSrc = readTransformedSource(relPath);
+ assertEquals(multipleValueClasses.replace("/*VALUE*/", "value"), transformedSrc);
+ assertTrue(transformedSrc.contains(" value class MultipleValueClasses "));
+ assertTrue(transformedSrc.contains(" value class First "));
+ assertFalse(transformedSrc.contains(" value class Second "));
+ assertTrue(transformedSrc.contains(" value class Third "));
+ }
+
+ @Test
+ public void multilineClassDeclaration() throws IOException {
+ // A slightly extreme case to show that the annotation processor can cope
+ // with multiline class declarations and interleaved comments. The value
+ // keyword is inserted after the last modifier with a leading space.
+ String multilineClassDeclaration =
+ """
+ package test;
+
+ @jdk.internal.MigratedValueClass
+ public
+ /* Some comment */
+ final /*VALUE*/
+ /* Some other comment */
+ class
+
+ MultilineClassDeclaration {
+ }
+ """;
+ Path relPath = writeTestSource("MultilineClassDeclaration", multilineClassDeclaration);
+ compileTestClass(relPath);
+ String transformedSrc = readTransformedSource(relPath);
+ assertEquals(multilineClassDeclaration.replace("/*VALUE*/", "value"), transformedSrc);
+ }
+
+ private Path writeTestSource(String className, String source) throws IOException {
+ // Remove the VALUE tokens to leave "clean" source (otherwise the
+ // token will persist in the transformed output and get messy).
+ // Tokens have a leading space to match how the keyword is injected.
+ assertTrue(source.contains(" /*VALUE*/"), "invalid test source");
+ String actualSrc = source.replace(" /*VALUE*/", "");
+
+ Path relPath = Path.of("test", className + ".java");
+ Path srcFile = testDir.resolve("src").resolve(relPath);
+ Files.createDirectories(srcFile.getParent());
+ Files.writeString(srcFile, actualSrc);
+ return relPath;
+ }
+
+ private String readTransformedSource(Path relPath) throws IOException {
+ return Files.readString(testDir.resolve("out").resolve(relPath));
+ }
+
+ private void compileTestClass(Path srcPath) {
+ compile(testDir.resolve("src").resolve(srcPath), processorDir.resolve("compiled"),
+ "--add-exports", "java.base/jdk.internal=ALL-UNNAMED",
+ "-Avalueclasses.outdir=" + testDir.resolve("out"),
+ "--processor-path", processorDir.toString(),
+ "-processor", "build.tools.valhalla.valuetypes.GenValueClasses");
+ }
+
+ // NOTE: Since the in-memory compiler is limited and doesn't support getting
+ // the Path of the "memo:" URIs is uses, it cannot be used for testing this
+ // annotation processor, so we must perform on-disk compilation.
+ private static void compile(Path srcFile, Path outDir, String... extraArgs) {
+ StringWriter out = new StringWriter();
+ StringWriter err = new StringWriter();
+ List allArgs = new ArrayList<>();
+ allArgs.add("-d");
+ allArgs.add(outDir.toString());
+ allArgs.addAll(Arrays.asList(extraArgs));
+ allArgs.add(srcFile.toString());
+ int exitValue = JAVAC_TOOL.run(
+ new PrintWriter(out),
+ new PrintWriter(err),
+ allArgs.toArray(String[]::new));
+ Assertions.assertEquals(0, exitValue, String.format(
+ """
+ Compilation failed: %s
+ Stdout: %s
+ Stderr: %s
+ """, srcFile, out, err));
+ }
+
+ /**
+ * The source root is {@code make/jdk/src/classes} from the JDK root, but
+ * this may not be available in all test environments (and if it isn't, the
+ * test should be skipped).
+ *
+ * Similar to {@code test/langtools/tools/all/RunCodingRules.java}, we
+ * attempt to locate this directory by walking from the {@code test.src}
+ * directory.
+ */
+ private static Path findBuildToolsSrcRoot() {
+ Path testSrc = Path.of(System.getProperty("test.src", "."));
+ for (Path d = testSrc; d != null; d = d.getParent()) {
+ if (Files.exists(d.resolve("TEST.ROOT"))) {
+ d = d.getParent();
+ Path srcRoot = d.resolve(SRC_ROOT);
+ if (!Files.isDirectory(srcRoot)) {
+ break;
+ }
+ return srcRoot;
+ }
+ }
+ return Assumptions.abort("Build tools source root not found; skipping test");
+ }
+}